Reference Guide

ESP32-C3 FM Radio Clock Build

A complete build reference for the RDA5807M + DS3231 + SSD1306 radio clock — wiring, pin map, firmware flow, and every hard-won lesson from getting it stable.

Platform
PlatformIO + Arduino framework
MCU
ESP32-C3 Super Mini
Peripherals
RDA5807M · DS3231 · SSD1306
Bus
Shared I2C — GPIO4 (SDA) / GPIO5 (SCL)
01 — Hardware

Parts List

Everything shares one I2C bus, so the wiring stays simple regardless of how many modules you add.

ComponentInterfaceNotes
ESP32-C3 Super MiniNative USB-Serial/JTAG, no CH340/CP2102 chip onboard
RDA5807M FM tuner moduleI2CNeeds ~75cm wire antenna; L/R audio out to a jack or amp
DS3231 RTC moduleI2CCR2032 battery keeps time when unpowered
SSD1306 0.96" OLED, 128×64I2CAddress 0x3C
3× tactile push buttonsDigital inUP / DOWN / MODE, wired to GND with internal pull-ups
02 — Hardware

ESP32-C3 Super Mini Pinout

Pins actually used in this build are highlighted in green. Pins to avoid are marked amber.

USB-C ESP32-C3 Super Mini 5V GND — common ground GPIO0 — BOOT, avoid GPIO1 — BTN_UP GPIO2 — strapping, avoid GPIO3 — free GPIO4 — I2C SDA GPIO5 — I2C SCL GPIO6 — BTN_DOWN GPIO7 — free GPIO8 — strapping, avoid GPIO9 — BOOT strap, avoid 3V3 — power to modules GPIO10 — BTN_MODE GPIO18 — USB D−, avoid GPIO19 — USB D+, avoid GPIO20 — UART RX, free GPIO21 — UART TX, free Used in this build Avoid (strapping / USB) Free / unused
ESP32-C3 SUPER MINI — SIMPLIFIED PIN REFERENCE (NOT TO SCALE)
03 — Hardware

Complete Pin Map

Final, verified pin assignments after full build and debug.

Power & I2C bus (shared by all three modules)

SignalESP32-C3 PinConnects To
3V33V3VCC on RDA5807M, DS3231, SSD1306
GroundGNDGND on all 3 modules + all 3 buttons
I2C DataGPIO4SDA on RDA5807M, DS3231, SSD1306
I2C ClockGPIO5SCL on RDA5807M, DS3231, SSD1306

Buttons

ButtonESP32-C3 PinOther LegFunction
UPGPIO1GNDNext preset station
DOWNGPIO6GNDPrevious preset station
MODEGPIO10GNDToggle Clock / Radio screen

Pins to avoid on this board

PinReason
GPIO0, GPIO9BOOT strapping pins
GPIO2, GPIO8Strapping pins
GPIO18, GPIO19Native USB D− / D+ — permanently tied to the USB-C connector, constant traffic noise while powered
04 — Hardware

Wiring Diagram

Schematic view — all three I2C peripherals branch off the same SDA/SCL pair.

ESP32-C3 Super Mini SDA→GPIO4 / SCL→GPIO5 RDA5807M FM Tuner (I2C) ANT: ~75cm wire DS3231 RTC (I2C) CR2032 backup cell SSD1306 128×64 OLED (I2C) Address 0x3C UP → GPIO1 other leg → GND DOWN → GPIO6 other leg → GND MODE → GPIO10 other leg → GND All modules: VCC → 3V3 · GND → common ground rail
SCHEMATIC — DASHED LINES ARE LOGICAL CONNECTIONS, NOT PHYSICAL LAYOUT
05 — Software

Project Setup

  1. Install PlatformIO in VS Code

    Install the PlatformIO IDE extension, then create a new project targeting board esp32-c3-devkitm-1, framework Arduino.

  2. Set up platformio.ini

    Set upload speed to 115200 for reliability, and add the four required libraries (see below).

  3. Place firmware in src/main.cpp

    Only one entry-point file may exist in src/. Remove any leftover .ino or backup .cpp files — duplicate globals cause linker errors.

  4. Wire the hardware per the pin map above

    Double-check GND is common across every module and button before powering on.

  5. Build and upload

    Use the PlatformIO toolbar icons (checkmark = build, arrow = upload) rather than a plain terminal, since pio often isn't on Windows' PATH by default.

platformio.ini

; ESP32-C3 Super Mini — FM Radio + Digital Clock

[env:esp32-c3-devkitm-1]
platform = espressif32
board = esp32-c3-devkitm-1
framework = arduino

upload_speed = 115200
monitor_speed = 115200

lib_deps =
    https://github.com/pu2clr/RDA5807.git
    adafruit/RTClib@^2.1.4
    adafruit/Adafruit SSD1306@^2.5.13
    adafruit/Adafruit GFX Library@^1.11.11
    adafruit/Adafruit BusIO@^1.16.1
06 — Software

Firmware Flow

How the sketch moves from boot through the button/screen state machine.

POWER ON / BOOT setup() Init I2C · OLED · RTC · Radio Tune to preset[0], show splash loop() ↻ every ~150ms handleButtons() debounced edge detection UP pressed next preset → setFrequency() DOWN pressed prev preset → setFrequency() MODE pressed toggle currentScreen currentScreen == CLOCK ? yes drawClock() RTC time + date to OLED no drawRadio() freq + station + stereo status
FIRMWARE STATE FLOW — BOOT THROUGH MAIN LOOP

Debounce logic (the part worth getting right)

Each button tracks two separate values: a raw reading (updates instantly every loop) and a confirmed stable reading (only updates once the raw value has held steady for 40ms). The action fires only when the stable value changes — this is what prevents one physical press from registering as multiple rapid actions.

07 — Software

Preset Stations

UP/DOWN jump directly between these instead of stepping 0.1MHz at a time.

StationFrequency
Gold 90590.5 MHz
Symphony 92492.4 MHz
YES 93393.3 MHz
CNA93893.8 MHz
Warna 94294.2 MHz
Class 9595.0 MHz

Edit presetFreqs[] and presetNames[] together (same order, same length) to change the list. Frequencies are in units of 10kHz — 90.5 MHz is written as 9050.

08 — Extensions

Speaker Output

Swapping the 3.5mm earpiece for a real speaker needs an amplifier stage in between — the RDA5807M's output is line-level, not enough to drive a speaker cone directly.

Add a class-D amp module

A PAM8403 breakout is the standard hobbyist choice — cheap, stereo, and simple to wire.

PAM8403 pinConnects to
VCC5V (from USB rail — not 3V3)
GNDCommon ground rail
L_INRDA5807M Left audio out
R_INRDA5807M Right audio out
L_OUT / R_OUT4Ω–8Ω speaker terminals

Finding L/R on the RDA5807M module

Budget RDA5807M breakouts route audio straight to an onboard 3.5mm jack rather than breaking L/R out to the header pins — so there's nothing labeled "L" or "R" to find on the pin header itself. You have two ways to tap into it:

EASIEST
Cut a 3.5mm audio cable

Buy a male-to-bare-wire 3.5mm cable, plug it into the module's jack, wire the other end into the PAM8403 inputs. Typical color convention — verify with a multimeter, as it isn't universal:

Wire colorChannel
RedRight
White or thin blackLeft
Bare copper / shieldGround
COMPACT
Solder directly to the jack's underside pads

Flip the module over — the jack's solder tabs on the PCB follow standard TRS order:

TRS contactChannel
TipLeft
RingRight
SleeveGround (shared/common)

Confirming before committing wire

  1. Test with real headphones first

    Confirm you get sound through the jack at all before modifying anything.

  2. Map the pads with a multimeter

    Insert a spare 3.5mm plug, set the meter to continuity mode, and probe each PCB solder tab against the plug's tip/ring/sleeve to confirm which pad is which — exact layouts vary between manufacturers even for the "same" module.

  3. Wire to the amp

    Tip → L_IN, Ring → R_IN, Sleeve → GND on the PAM8403.

Practical notes

POWER
Amp needs 5V, not 3.3V

Tap the amp's VCC from the board's 5V pin (present before the onboard regulator steps it down), not from the 3V3 rail the other modules use.

NOISE
Keep amp wiring away from the antenna

A class-D amp switches at high frequency and can inject noise into a nearby FM antenna. Route speaker/amp wiring on the opposite side of the board from the antenna.

SOFTWARE
Volume control is unchanged

radio.setVolume() still controls the level — the amp just boosts what's already coming out of L/R, it doesn't add a separate volume stage unless your specific module includes one.

09 — Reference

Lessons Learned

Real issues hit during this build, and what actually fixed them — worth reading before your next build to skip the detour.

HARDWARE
GPIO18 / GPIO19 look free but aren't

On the Super Mini, these are hard-wired to the native USB-C data lines (D−/D+). Using them for buttons picks up USB traffic as false presses.

HARDWARE
GPIO0, 2, 8, 9 are strapping pins

Pulling these low at boot can change boot mode or prevent normal startup entirely. Avoid wiring anything to them.

SOFTWARE
Level-checking vs. edge-detection debounce

Checking "is the pin LOW right now" every loop cycle fires repeatedly while a button is held. Track a confirmed stable state separately from the raw reading, and only act when the stable state changes.

SOFTWARE
Never block inside loop()

A while() wait loop (e.g. for long-press detection) freezes every other button and the display for its entire duration. Use non-blocking millis() timestamp comparisons instead.

SOFTWARE
radio.seek() is a blocking call

With weak reception it can scan for a long time before returning, freezing the whole board. Skip it, or wrap it with a hard timeout, rather than calling it directly from the main loop.

PLATFORMIO
Only one entry-point file in src/

A leftover main_old.cpp or stray .ino alongside main.cpp causes "multiple definition" linker errors. Move backups outside src/ entirely.

PLATFORMIO
pio often isn't on Windows' PATH

Use the PlatformIO toolbar icons in VS Code, or call the full path directly: C:\Users\<you>\.platformio\penv\Scripts\pio.exe.

DEBUGGING
No serial access? Use the OLED itself

Printing raw digitalRead() values directly to the display gave a reliable way to isolate hardware vs. software faults without ever touching the serial monitor.

DEBUGGING
The swap test isolates button vs. wiring

Physically moving a suspect button to a known-good pin (and vice versa) tells you definitively whether the fault follows the button or the connection.

10 — Reference

Build Checklist

All modules on shared I2C: SDA→GPIO4, SCL→GPIO5
Common GND rail connects all 3 modules + all 3 buttons
Buttons avoid GPIO0/2/8/9/18/19
Only src/main.cpp present — no stray .ino or backup files
Antenna wire ~75cm on RDA5807M ANT pad
rtc.adjust() line commented out after first successful flash
Debug button readout removed once buttons confirmed working