RetroDS is a handheld NES emulator I've been building on an ESP32. Version 1 was a hand-soldered perfboard stack with a classic ESP32 and a 240×240 screen. It ran games, but the wiring was fragile, there was no sound and no way to keep save games.
Version 2 moves everything onto a custom 4-layer PCB that I designed in KiCad and assembled by hand: an ESP32-S3 with native USB-C, a Li-ion charger with load sharing, an I²S audio amp, a dedicated microSD bus and silk-screened controls. It now plays games at the correct NTSC speed, with sound, 15-step volume, battery saves and a 3D-printed case.
Credit where it's due: the emulator core is the open-source NesCat ESP32 NES emulator (by Nathalis), which also includes code from Peter Barrett's esp_8_bit. I didn't write the 6502/PPU core. My work is the hardware, the assembly and bring-up, and porting the firmware to this new board (details below).
HardwareThe power path is load-sharing: USB VBUS → MCP73871 → VOUT → slide switch → VSYS → AP2112K LDO → 3.3 V. With USB plugged in, the charger runs the system and tops up the cell at the same time. Unplug USB and it hands over to the battery. The battery charges whether the power switch is on or off, because the charge path doesn't go through the switch.
The display (ST7789, SPI3) and the SD card (SPI2) are on separate SPI peripherals so neither blocks the other. All 10 buttons are active-low with internal pull-ups.
Placement was an assembly decision too. Everything that needs hot air or a hotplate (the ESP32-S3 module, the MCP73871 QFN and the SOT-23 AP2112K regulator) is on the back, along with the plug-in amp and SD modules. The front only has the buttons and the slide switch, which I soldered last with an iron. That also keeps the front flat so the button caps sit flush against the printed case.
Two parts are deliberately left unpopulated on this board: the USB ESD diode (D1) and the LC709203F battery fuel gauge (U4), which I couldn't source anywhere. Everything works without them; the only thing missing is a battery percentage.
On first power-up the PC saw a USB device attach and drop a second later, on a fixed ~2 s cycle (dmesg: descriptor read error -71). The ESP32-S3 re-enumerates every time it resets, so a fixed period means it's brown-out resetting in a loop. That pointed at the power path, not USB or firmware.
Measuring one node at a time with USB connected: VBUS 5.0 V, but the charger output, VSYS and 3.3 V were all 0 V. So power reached the board but the MCP73871 wasn't passing it through. It's a 20-pin QFN with an exposed centre pad; I reflowed the pad and the IN/OUT pins, VOUT came up to ~5 V, the 3.3 V rail appeared and the board enumerated and took a flash.
Bring-up 2: a short I couldn't seeAfter fitting the display, SD and volume-button modules the board went dark again, and 3.3 V measured a dead short to ground. Under magnification nothing was bridged.
A continuity beep isn't a short test here (the display bus alone has ~10 µF that beeps while it charges), so I measured resistance instead: 0–3 Ω, the same in both probe directions and stable, which is a real short rather than capacitance. Lifting one plug-in module at a time cleared it: the short was a joint under one of the modules. Reflowing that connector brought the rail back to 3.30 V and the display started drawing.
Firmware portThe emulator core carried over, but almost everything around it had to change for the new hardware:
- Display: TFT_eSPI crashed on arduino-esp32 core 3.x, so I moved to Arduino_GFX and then LovyanGFX with DMA, which roughly doubled the full-frame blit to ~15 fps. Each 256-pixel NES line is stretched to the 320-pixel panel.
- Audio: MAX98357A over I²S with signed samples (the old PCM5102 path used offset-binary and would have been silent), a 15-step perceptual volume curve and an on-screen volume bar.
- Timing: the inherited code ran at PAL 50 Hz, so NTSC ROMs played ~17% slow. Fixed to 60 Hz.
- Saves: battery-backed SRAM written to /NES/<rom>.nes.sav, plus a 3-minute autosave.
- Storage: ROMs streamed from a dedicated flash partition, SD on its own hardware SPI bus with hot-insert retry. The card must be FAT32.
- Charging: the MCP73871 PG/STAT1/STAT2 pins are decoded to show charging status in the menu and a low-battery banner in game.
- Thermal vias under the MCP73871 exposed pad (it has none).
- A VBAT divider to a spare ADC pin, so battery level works without the unobtainable fuel gauge.
- Test points on the power rails. They would have made both bring-up faults quicker to find.
- A faster display path (async double-buffering) to go past ~15 fps.
The full build log, schematics and more photos are on my portfolio: https://andresmoreno2702-gif.github.io/nes-emulator-v2-project.html📷



_4YUDWziWQ8.png?auto=compress%2Cformat&w=48&h=48&fit=fill&bg=ffffff)







_Ujn5WoVOOu.png?auto=compress%2Cformat&w=40&h=40&fit=fillmax&bg=fff&dpr=2)




Comments