Parts List
Everything shares one I2C bus, so the wiring stays simple regardless of how many modules you add.
| Component | Interface | Notes |
|---|---|---|
| ESP32-C3 Super Mini | — | Native USB-Serial/JTAG, no CH340/CP2102 chip onboard |
| RDA5807M FM tuner module | I2C | Needs ~75cm wire antenna; L/R audio out to a jack or amp |
| DS3231 RTC module | I2C | CR2032 battery keeps time when unpowered |
| SSD1306 0.96" OLED, 128×64 | I2C | Address 0x3C |
| 3× tactile push buttons | Digital in | UP / DOWN / MODE, wired to GND with internal pull-ups |
ESP32-C3 Super Mini Pinout
Pins actually used in this build are highlighted in green. Pins to avoid are marked amber.
Complete Pin Map
Final, verified pin assignments after full build and debug.
Power & I2C bus (shared by all three modules)
| Signal | ESP32-C3 Pin | Connects To |
|---|---|---|
| 3V3 | 3V3 | VCC on RDA5807M, DS3231, SSD1306 |
| Ground | GND | GND on all 3 modules + all 3 buttons |
| I2C Data | GPIO4 | SDA on RDA5807M, DS3231, SSD1306 |
| I2C Clock | GPIO5 | SCL on RDA5807M, DS3231, SSD1306 |
Buttons
| Button | ESP32-C3 Pin | Other Leg | Function |
|---|---|---|---|
| UP | GPIO1 | GND | Next preset station |
| DOWN | GPIO6 | GND | Previous preset station |
| MODE | GPIO10 | GND | Toggle Clock / Radio screen |
Pins to avoid on this board
| Pin | Reason |
|---|---|
| GPIO0, GPIO9 | BOOT strapping pins |
| GPIO2, GPIO8 | Strapping pins |
| GPIO18, GPIO19 | Native USB D− / D+ — permanently tied to the USB-C connector, constant traffic noise while powered |
Wiring Diagram
Schematic view — all three I2C peripherals branch off the same SDA/SCL pair.
Project Setup
-
Install PlatformIO in VS Code
Install the PlatformIO IDE extension, then create a new project targeting board
esp32-c3-devkitm-1, framework Arduino. -
Set up
platformio.iniSet upload speed to
115200for reliability, and add the four required libraries (see below). -
Place firmware in
src/main.cppOnly one entry-point file may exist in
src/. Remove any leftover.inoor backup.cppfiles — duplicate globals cause linker errors. -
Wire the hardware per the pin map above
Double-check GND is common across every module and button before powering on.
-
Build and upload
Use the PlatformIO toolbar icons (checkmark = build, arrow = upload) rather than a plain terminal, since
piooften 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
Firmware Flow
How the sketch moves from boot through the button/screen state machine.
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.
Preset Stations
UP/DOWN jump directly between these instead of stepping 0.1MHz at a time.
| Station | Frequency |
|---|---|
| Gold 905 | 90.5 MHz |
| Symphony 924 | 92.4 MHz |
| YES 933 | 93.3 MHz |
| CNA938 | 93.8 MHz |
| Warna 942 | 94.2 MHz |
| Class 95 | 95.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.
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 pin | Connects to |
|---|---|
| VCC | 5V (from USB rail — not 3V3) |
| GND | Common ground rail |
| L_IN | RDA5807M Left audio out |
| R_IN | RDA5807M Right audio out |
| L_OUT / R_OUT | 4Ω–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:
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 color | Channel |
|---|---|
| Red | Right |
| White or thin black | Left |
| Bare copper / shield | Ground |
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 contact | Channel |
|---|---|
| Tip | Left |
| Ring | Right |
| Sleeve | Ground (shared/common) |
Confirming before committing wire
-
Test with real headphones first
Confirm you get sound through the jack at all before modifying anything.
-
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.
-
Wire to the amp
Tip → L_IN, Ring → R_IN, Sleeve → GND on the PAM8403.
Practical notes
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.
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.
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.
Lessons Learned
Real issues hit during this build, and what actually fixed them — worth reading before your next build to skip the detour.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 |