Skip to main content

ESP32 Firmware

ESP-IDF v5.x firmware for the ESP32 Emu Turbo hardware. Phase 1 validates all hardware subsystems, Phase 2 integrates Retro-Go, Phase 3 enables all emulator cores.


Phase 1 — Hardware Abstraction

Standalone ESP-IDF v5.x project in software/ that validates all hardware before integrating Retro-Go. See software/README.md for build instructions.

StepTaskDetailsStatus
1.1ESP-IDF v5.x project setupsdkconfig for N16R8 (240MHz, 16MB flash, 8MB PSRAM)✅ Done
1.2ILI9488 display driver (i80 8-bit parallel)esp_lcd_panel_io_i80 + esp_lcd_ili9488 component, 20MHz✅ Done
1.3Display test patternColor bars, fill screen, status indicators✅ Done
1.4SD card (SPI mode)esp_vfs_fat_sdspi_mount, FAT32, ROM directory scanner✅ Done
1.512-button inputGPIO polling @ 1ms, bitmask API, HW RC debounce✅ Done
1.6Audio outputi2s_pdm_tx PDM sigma-delta 32kHz 16-bit mono, 440Hz test tone. Only DOUT (GPIO17) is routed — BCLK/LRCK unused; the PAM8403 input RC network reconstructs the analog signal.✅ Done
1.7Power managementNot available on this board: the IP5306's I2C is not routed (GPIO33/34 belong to the Octal PSRAM — see board_config.h). Charge state is the on-board LED only; power.c survives as a stub⚠️ N/A on hardware

Firmware project structure

software/
├── CMakeLists.txt ESP-IDF project root
├── sdkconfig.defaults ESP32-S3 N16R8 hardware config
├── partitions.csv 4MB app + 12MB storage
└── main/
├── idf_component.yml esp_lcd_ili9488 ^1.4.0
├── board_config.h All GPIO pin definitions (source of truth)
├── main.c Test harness → interactive button display
├── display.c/h ILI9488 320×480 i80 parallel (backlight is hardwired — `display_set_backlight()` is a no-op)
├── input.c/h 12 buttons, active-low, bitmask polling
├── sdcard.c/h SPI @ 20MHz, FAT32, ROM listing
├── audio.c/h I2S PDM TX (sigma-delta) → PAM8403 amplifier
└── power.c/h stub — IP5306 I2C is NOT routed on this PCB (GPIO33/34 = Octal PSRAM)

Build & flash (Docker)

No local toolchain needed — the build runs inside the official espressif/idf:v5.4 Docker image.

# Build firmware
make firmware-build

# Flash + serial monitor (connect board, hold SELECT at power-on)
make firmware-flash

# Custom USB port
ESP_PORT=/dev/ttyACM0 make firmware-flash

Native ESP-IDF is also supported — see software/README.md for details.

Test sequence on boot

  1. Display shows color bars for 3 seconds (verifies 8-bit data bus)
  2. Power status line in the serial log (IP5306 I2C is not routed — no battery %/charge readout; charge state is the on-board LED only)
  3. All 12 button GPIOs initialized
  4. SD card mounted, ROM directories scanned
  5. 440 Hz test tone plays for 2 seconds
  6. Interactive mode: button presses shown on screen + serial

SD Card Setup

The console loads ROMs from a micro SD card formatted as FAT32. Each emulated system has its own folder under /roms/.

Directory structure

SD Card (FAT32)
└── roms/
├── nes/ .nes files
├── snes/ .smc / .sfc files
├── gb/ .gb files
├── gbc/ .gbc files
├── sms/ .sms files
├── gg/ .gg files
├── pce/ .pce files
├── gen/ .bin / .md files
├── lynx/ .lnx files
└── gw/ .gw files

Preparation steps

  1. Format the micro SD card as FAT32 (most cards come pre-formatted)
  2. Create the roms/ directory in the root of the card
  3. Create sub-folders for each system you want to emulate
  4. Copy ROM files into the matching folder

Automated setup

A script is provided to format the SD card and copy test ROMs in one step:

# Format SD card as FAT32 + copy all homebrew test ROMs
sudo ./scripts/setup-sdcard.sh /dev/sdX

# Copy only (skip formatting)
sudo ./scripts/setup-sdcard.sh /dev/sdX --no-format

Included homebrew test ROMs

The project includes 8 freely distributable homebrew ROMs in test-roms/ for testing without commercial ROMs:

SystemROMAuthorSize
NESOwliaGradual Games512 KB
GBBlargg's CPU InstructionsBlargg64 KB
GBCucity v1.3AntonioND128 KB
SMSSilver ValleyEnrique Ruiz256 KB
GGSwabby v1.11Anders S. Jensen128 KB
PCEReflectronAetherbyte256 KB
GenesisMiniplanetsSik256 KB
SNESSuper Boss Gaiden v1.2Dieter Von Laser512 KB
SystemROMFileSizeWhy
NESSuper Mario Brossmb.nes40 KBUniversal test — scrolling, sprites, audio
SNESSuper Mario Worldsmw.smc512 KBGood baseline — 2 BG layers, Mode 1
SNESFF6ff6.smc3 MBTurn-based RPG — best SNES genre for ESP32
GBTetristetris.gb32 KBMinimal — verifies basic emulation
GenesisSonicsonic.bin512 KBFast scrolling stress test

Size limits

ConstraintValue
Max ROM size (PSRAM)6 MB
SD card formatFAT32 (max 32 GB recommended)
Max filename length255 characters (long filename support enabled)
SNES ROM sizes

Most SNES games are 1–4 MB. Games with special chips (SA-1, SuperFX) are larger and may not be compatible with snes9x on ESP32-S3.


Phase 2 — Retro-Go Integration

Fork and adapt Retro-Go for our hardware. Retro-Go is included as a git submodule at retro-go/ and built via a separate Docker Compose file.

StepTaskDetailsStatus
2.1Add ducalex/retro-go as submoduleretro-go/ directory, upstream repo✅ Done
2.2Create target targets/esp32-emu-turbo/config.h + env.py + sdkconfig✅ Done
2.3Docker build pipelinedocker-compose.retro-go.yml + Makefile targets✅ Done
2.4Custom display driver ili9488_i80.h8-bit i80 parallel via esp_lcd_panel_io_i80, async DMA, 5-buffer pool✅ Done
2.5Frame scalingAutomatic via Retro-Go core (320x480 portrait, integer scale + letterbox)✅ Done
2.6Input mapping12 GPIO direct buttons + MENU=SELECT (GPIO 0)✅ Done
2.7Audio routingI2S PDM TX on DOUT only (GPIO17) → C22 → PAM8403. No external DAC, no BCLK/LRCK — same path as step 1.6✅ Done
2.8First boot: NES testnofrendo running Super Mario Bros at 60fps⏳ Needs hardware

Build & flash (Docker)

Retro-Go uses a separate Docker Compose file (docker-compose.retro-go.yml) with the espressif/idf:v5.4 image.

# Build all Retro-Go apps (launcher + emulators)
make retro-go-build

# Build launcher only (quick test)
make retro-go-build-launcher

# Flash firmware + serial monitor
make retro-go-flash

# Serial monitor only
make retro-go-monitor

# Custom USB port
ESP_PORT=/dev/ttyACM0 make retro-go-flash

# Clean build cache
make retro-go-clean

Build output

All 5 Retro-Go applications compile successfully for the ESP32 Emu Turbo target (ESP-IDF v5.4, ESP32-S3):

BinaryContentsSizePartition free
launcher.binRetro-Go launcher UI + ROM browser1037 KB67%
retro-core.binAll emulators (NES, GB, GBC, SMS, GG, PCE, Lynx, SNES, G&W)~2.5 MB~17%
gwenesis.binSega Genesis / Mega Drive (standalone)~1.5 MB~50%
prboom-go.binDoom port (PrBoom)~1.5 MB~50%
fmsx.binMSX emulator655 KB79%
note

The build produces Device doesn't support fw format, try build-img! at the end — this is expected. Our target uses individual app flashing via make retro-go-flash, not a combined firmware image.

Target configuration

The target lives at retro-go/components/retro-go/targets/esp32-emu-turbo/ with:

  • config.h — GPIO mapping, display/audio/input config (mirrors board_config.h)
  • env.pyIDF_TARGET = "esp32s3", firmware format
  • sdkconfig — ESP-IDF config (240MHz, 16MB flash QIO, 8MB Octal PSRAM)

GPIO mapping verification

All 31 GPIO pins have been cross-verified between three sources with zero discrepancies:

GroupPinsboard_config.hRetro-Go config.hKiCad schematic
Display data D0–D7GPIO 4–11
Display controlGPIO 12–14, 46
Display hardwiredRD → +3V3, BL → +5V via R27 (no GPIO)
SD card SPIGPIO 44, 43, 38, 39
Audio (PDM DOUT only)GPIO 17
D-padGPIO 40, 41, 42, 1
Face buttonsGPIO 2, 48, 47, 21
System buttonsGPIO 18, 0
Shoulder buttonsGPIO 45, 3

Notes:

  • MENU and SELECT share GPIO 0 in Retro-Go (intentional — 12 physical buttons, 13 logical)
  • GPIO 19/20 are used for native USB data (D-/D+) — firmware flash + CDC debug console
  • GPIO 3 is BTN_R, GPIO 45 is BTN_L (shoulder buttons freed by hardwiring LCD_RD and the backlight on the PCB)
  • GPIO 43 is SD_MISO (was TX0 UART debug, replaced by USB native)
  • GPIO 26–32 are the module's internal SPI flash bus and GPIO 33–37 the Octal PSRAM — neither may be used
  • GPIO 15/16 are unconnected: the audio path is PDM and needs only DOUT

Display driver: ili9488_i80.h

Custom driver replacing Retro-Go's SPI-based ili9341.h with 8-bit 8080 parallel interface. Located at retro-go/components/retro-go/drivers/display/ili9488_i80.h.

FeatureValue
Bus8-bit i80 parallel (esp_lcd_panel_io_i80)
Clock20 MHz write clock
Resolution320x480 portrait
Color formatRGB565 (16-bit)
DMAAsync with 5-buffer pool
BacklightAlways-on — LED-A fed from +5V through R27 (20 Ω) on the PCB, no GPIO control
Driver IDRG_SCREEN_DRIVER 2

The driver uses esp_lcd_panel_io_tx_param for commands (CASET/RASET) and esp_lcd_panel_io_tx_color for async DMA pixel transfers. A completion callback recycles buffers to the pool, providing natural backpressure without explicit sync.


Phase 3 — All Emulators at Full Speed

Enable and test each emulator core.

StepCoreTest ROMTarget
3.1nofrendo (NES)Super Mario Bros60 fps
3.2gnuboy (GB)Tetris60 fps
3.3gnuboy (GBC)Pokemon Crystal60 fps
3.4smsplus (SMS)Sonic the Hedgehog60 fps
3.5smsplus (GG)Sonic Triple Trouble60 fps
3.6pce-go (PCE)Bonk's Adventure60 fps
3.7handy (Lynx)California Games60 fps
3.8gwenesis (Genesis)Sonic the Hedgehog50-60 fps
3.9gw-emulator (G&W)Ball60 fps

For SNES-specific optimization (Phase 4) and v2 hardware audio coprocessor (Phase 5), see SNES Optimization.


Build & Flash

# Clone fork
git clone https://github.com/pjcau/retro-go.git
cd retro-go

# Build for ESP32 Emu Turbo
python3 rg_tool.py --target=esp32-emu-turbo build

# Flash via USB-C (GPIO0/SELECT = download mode at boot)
python3 rg_tool.py --target=esp32-emu-turbo flash

# Copy ROMs to SD card
# /roms/nes/ — .nes files
# /roms/snes/ — .smc/.sfc files
# /roms/gb/ — .gb files
# /roms/gbc/ — .gbc files
# /roms/sms/ — .sms files
# /roms/gg/ — .gg files
# /roms/pce/ — .pce files
# /roms/gen/ — .bin/.md files

For the full software architecture overview, see Software Architecture.