Skip to content

Danilop95/NESpresso32

Repository files navigation


NESpresso32 Logo

NESpresso32

A NES emulator for the bare ESP32-WROOM-32.
VGA over GPIO · ROMs in flash · OSD browser · No PSRAM · No SD card


License: MIT Platform VGA Super Mario Bros Build



The interesting part is not that it runs.
The interesting part is what had to be removed, inlined, measured, and fixed to make it run on a chip this small.


Real hardware output

Intro

NESpresso32 boot intro running on real VGA output.

Rommanager

Built-in ROM browser running on the ESP32.

ROMs visible in screenshots may come from local testing only and are not distributed with this repository.


Current status

Super Mario Bros now reaches 60 FPS on real ESP32-WROOM-32 hardware. Accuracy and compatibility are still being improved, especially for more complex mappers and timing-sensitive games.

NESpresso32 currently focuses on squeezing NES emulation into the standard ESP32-WROOM-32 without PSRAM, an SD card, or a GPU. It is a working embedded emulator project, not a claim of perfect NES emulation or full library compatibility.


Overview

NESpresso32 runs on a standard ESP32-WROOM-32: no add-ons, no PSRAM expansion, no SD card. It outputs video through VGA using the ESP32's I2S peripheral to generate a pixel clock, with GPIO 25, 26, and 27 driving R/G/B through 330 Ω resistors.

ROMs go into a roms/ folder before you build. A Python script converts each .nes file into a C array compiled into the firmware. No filesystem, no dynamic loading: firmware and ROM data ship as a single binary. An on-screen browser lets you pick which ROM to run.

NESpresso32 has gone through focused optimization work to make NES emulation practical on the bare module. The core changes include fixing an APLL calibration bug, inlining the pixel write path, and removing function-call overhead from memory accesses.


Why this project is interesting

The ESP32-WROOM-32 has no GPU, no native VGA output, and limited internal RAM. NESpresso32 still runs a NES emulator, generates VGA over GPIO, and embeds ROM data directly into flash on the standard module.

The point of the project is pushing cheap hardware further than expected without hiding the constraints.

The ESP32-WROOM-32 has 320 KB of DRAM. Two framebuffers consume 130 KB of that. NES RAM needs 64 KB, PPU RAM needs 16 KB. What remains covers stack, heap, and everything else.

The pixel clock for 272×240@60 Hz is 12 587 500 Hz. The ESP32 generates this through its APLL, an audio PLL driven by a register bus separate from the normal I2C interface. The standard SDK call doesn't always lock correctly. You get a black screen with no error. The fix requires writing calibration values directly to internal registers.

The 6502 runs at 1 789 773 Hz. One NES frame = 29 780 CPU cycles. At 240 MHz the ESP32 has 134 of its own cycles per 6502 cycle, which sounds like plenty until you add up overhead across 61 440 pixel writes and 44 000 memory accesses per frame.


Features

  • NES CPU/PPU emulation on a real ESP32-WROOM-32 target.
  • VGA output through GPIO using I2S/APLL-based timing.
  • 272×240 @ 60 Hz VGA mode with RGB on GPIO25/26/27 and sync on GPIO23/22.
  • ROM conversion at build time through scripts/gen_roms.py.
  • ROMs embedded into flash as C/C++ data before firmware upload.
  • Built-in OSD ROM browser.
  • Original NES controller input over GPIO using LATCH, CLOCK, and DATA.
  • Mapper support for NROM, MMC1, UNROM, CNROM, and MMC3 as documented below.
  • No PSRAM and no SD card required.

What was slow and how it was fixed

Pixel writes through four layers of indirection

The original draw path: PPU → backend → SDL surface → VGA buffer. Each non-inlined call on Xtensa LX6 costs ~5 cycles for register window setup.

61 440 pixels/frame × 5 cycles = 307 200 cycles = 1.3 ms @ 240 MHz

The fix is one inlined write:

gb_buffer_vga[y][x ^ 2] = gb_color_vga[nescolor];

The ^ 2 corrects byte order for how I2S DMA reads 32-bit words. One XOR, one store.

Memory accesses were function calls

The 6502 does a memory access on every opcode:

29 780 opcodes × 1.5 avg accesses = ~44 000 memory calls/frame
44 000 × 7 cycles saved = ~308 000 cycles = 1.3 ms @ 240 MHz

Declaring the read/write path inline lets the compiler collapse the call on the hot path.

The APLL pixel clock was silently wrong

rtc_clk_apll_enable() sometimes reports "locked" but outputs the wrong frequency. The fix writes sdm0, sdm1, sdm2, and o_div directly to the APLL calibration registers via the internal I2C bus (regi2c_apll.h). Stable clock, stable image, deterministic across resets.

Frame loop ran on a software delay

The old loop called delay() to target 60 FPS: conservative, always slightly wrong. vga_swap_buffers(true) blocks until the DMA flips buffers at the real pixel-clock edge. No drift, no tearing.


Performance

Super Mario Bros: 60 FPS on real ESP32-WROOM-32 hardware.

Performance depends heavily on the game, mapper, scene complexity and current emulator timing. The 60 FPS result is a current real-hardware milestone for Super Mario Bros, not a guarantee that every game reaches the same result.

Enable use_lib_measure_time in gbConfig.h or use the benchmark mode documented in docs/BENCHMARKING.md to collect your own timing data.


Hardware

Everything you need:

Component Notes
ESP32-WROOM-32 dev board Standard module, 4 MB flash minimum
VGA monitor (DE-15) Any monitor with a VGA input
3× 330 Ω resistors R, G, B signal lines; do not skip
Original NES controller Current documented input device
ELEGOO passive buzzer Optional documented audio output on GPIO14
USB-serial adapter CP2102 or CH340, 3.3 V logic

VGA wiring

ESP32 GPIO25 ──[330 Ω]── VGA pin 1   Red
ESP32 GPIO26 ──[330 Ω]── VGA pin 2   Green
ESP32 GPIO27 ──[330 Ω]── VGA pin 3   Blue
ESP32 GPIO23 ─────────── VGA pin 13  HSYNC
ESP32 GPIO22 ─────────── VGA pin 14  VSYNC
ESP32 GND    ─────────── VGA pins 5,6,7,8,10

Driver notes from the hardware documentation: I2S1 LCD parallel mode, APLL fix enabled, 3-bit color / 8 colors, 272×240 @ 60 Hz.

Original NES controller

Power the NES controller from 3V3, not 5V.

NES controller wire Function ESP32 GPIO
Pin 7 / white VCC 3V3
Pin 1 / brown GND GND
Pin 3 / orange LATCH GPIO32
Pin 2 / yellow CLOCK GPIO33
Pin 4 / red DATA GPIO34
Pin 5 NC Do not connect
Pin 6 NC Do not connect

The firmware reads the controller in NES order: A, B, SELECT, START, UP, DOWN, LEFT, RIGHT. In the ROM browser, D-pad moves, A or START confirms, B goes back, and SELECT+START opens the OSD menu. The built-in BOOT button on GPIO0 still acts as ENTER in the ROM browser and START in-game.

Earlier prototypes used simpler GPIO inputs before settling on the real NES pad. The most recent previous setup was a 4x4 matrix keypad, OKY0272 style, with rows on GPIO4/5/18/19 and columns on GPIO13/15/21/17. That keypad mapping is now historical documentation only; the current firmware path is the original NES controller above.

Optional passive buzzer audio

The current config keeps game sound disabled for maximum FPS. The documented passive buzzer wiring is:

ELEGOO buzzer S   -> ESP32 GPIO14
ELEGOO buzzer +   -> ESP32 3V3
ELEGOO buzzer -   -> ESP32 GND

Full pinout and electrical notes → HARDWARE.md


ROM pipeline and legal use

Drop legal .nes files in roms/ before building. The pre-build script scripts/gen_roms.py reads the iNES headers, generates C arrays with PROGMEM, and builds a ROM table with titles, sizes, and mapper numbers. Everything compiles into a single firmware binary.

$ cp ~/Downloads/mygame.nes roms/
$ pio run
$ pio run -t upload
[gen_roms] Detected 3 ROM(s), total 680.0 KB
[gen_roms] Generated mygame.nes → gb_rom_2.cpp
[gen_roms] Generated gbrom.h for 3 ROM(s).
Uploading .pio/build/NESpresso32/firmware.bin

The firmware image is ~1.3 MB. The remaining ~2.4 MB of the 4 MB flash holds ROM data, depending on ROM size.

No commercial ROMs are included in this repository. Use homebrew ROMs, test ROMs, or ROMs you are legally allowed to use. ROM screenshots may come from local testing only and do not mean that those ROMs are distributed with the project.


Test ROMs

NESpresso32 keeps a small local validation set in roms/ while developing CPU, PPU, mapper, and memory behavior. These ROM files are local test inputs and must only be used when they are legally redistributable or privately owned. Do not commit commercial ROM binaries.

Current local ROM set seen by the build script:

ROM file Purpose Mapper Current result
apu_test.nes APU/audio behavior 1 Pending retest; audio is normally disabled for performance
color_test.nes Palette/color output 0 Pending retest
cpu_dummy_reads.nes CPU memory access side effects 3 Pending retest
cpu_timing_test.nes 6502 official opcode timing 0 PASS on real ESP32-WROOM-32 VGA output
ppu_open_bus.nes PPU open-bus behavior 0 Pending retest
ram_retain.nes CPU RAM retention/reset behavior 0 Pending retest

Recent validation notes:

Test Hardware/config Observed result Notes
6502 TIMING TEST (16 SECONDS) ESP32-WROOM-32, no PSRAM, VGA GPIO/I2S, NROM PASSED Confirms the current CPU timing fixes for official instructions under the scanline scheduler

Keep adding rows here as tests are run. Prefer concise notes: ROM name, hardware/config, exact screen result, and any visible glitches.


Supported mappers

Mapper Status Example titles for mapper reference
0 - NROM Supported Super Mario Bros., Donkey Kong
1 - MMC1 Experimental Metroid, The Legend of Zelda, Mega Man 2
2 - UNROM Supported / experimental Contra, Mega Man
3 - CNROM Supported / experimental Gradius, Arkanoid
4 - MMC3 Experimental Super Mario Bros. 3, Kirby's Adventure

The ROM browser flags unsupported mappers before you try to load them. Compatibility is not guaranteed for every game even on supported mappers.


Build and upload

# From the repository root:
# 1. Add legal .nes files to roms/
pio run
pio run -t upload

If flashing stalls at Connecting........_____, hold the BOOT button while it retries.

pio device monitor   # 115200 baud

With use_lib_measure_time enabled, the serial monitor prints frame timing data for local testing.


Limitations

  • NESpresso32 is not a perfect NES emulator.
  • Compatibility varies by game and mapper.
  • Timing-sensitive effects may still glitch.
  • More complex mappers are still experimental.
  • Commercial ROMs are not provided.
  • This is an experimental embedded emulator for a constrained microcontroller.

Documentation

Changelog What changed in each version
Roadmap What's planned next
Contributing How to contribute
Hardware quickstart Step-by-step wiring
ROM compatibility Mapper and game notes
Benchmarking How to collect real performance data
Divulgative paper PDF / LaTeX source Spanish narrative article for non-specialist readers

Legal notice NESpresso32 does not include commercial ROMs. Use homebrew ROMs, test ROMs, or ROMs you have the legal right to use. ROM screenshots may come from local testing only and do not mean that those ROMs are distributed with the project. This project is for educational and homebrew purposes only. Not affiliated with Nestlé, Nespresso, Nintendo, or Espressif Systems.


Built by Daniel Puente García (Danilop95).

About

NES emulator for bare ESP32-WROOM-32: VGA over GPIO, ROMs in flash, OSD browser, no PSRAM.

Topics

Resources

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages