ROMs
How ROMs are compiled into firmware, the iNES format, flash capacity, and legal use.
Legal ROM use
NESpresso32 does not include any ROM files. The project provides an emulation engine what you run on it is your responsibility.
For testing and development, the right starting point is homebrew software and purpose-built emulator test ROMs. These are created specifically to run on emulators and have clear distribution terms.
- nesdev.org emulator test ROMs specifically designed to test emulator accuracy
- NES homebrew games original software made for the NES hardware and emulators
Do not download commercial ROM files from the internet. The fact that a game is old does not make it legally free to copy. Check the terms for any ROM you use.
Workflow
The full pipeline from .nes file to running game:
- Copy
.nesfiles intoESP32/NESpresso32/roms/ - Run
pio run -t upload gen_roms.pyruns automatically as a pre-build step- The script generates one
.cppfile per ROM and a shared headergbrom.h - PlatformIO compiles everything and produces
firmware.bin - The firmware is flashed to the ESP32
- At startup, the OSD browser appears if more than one ROM is present
- Selecting a ROM loads PRG and CHR data from flash into DRAM
The script caches build results based on file modification time and size. Only new or modified ROMs are regenerated unchanged ROMs reuse the cached output and are not recompiled.
gen_roms.py
The script at scripts/gen_roms.py runs via extra_scripts = pre:scripts/gen_roms.py in platformio.ini. For each .nes file in roms/:
- Validates the iNES magic bytes (
NES\x1A) - Reads the mapper number, PRG-ROM size, and CHR-ROM size from the header
- Reads the full file as raw bytes
- Emits a
.cppfile with aconst uint8_tarray tagged withPROGMEM
Example of generated output:
The script also generates dataFlash/gbrom.h, which contains a struct array with one entry per ROM: title string, pointer to the array, size, mapper number, and a boolean flag indicating whether the mapper is supported.
A 40 KB ROM file becomes approximately 240 KB of C++ source text (each byte becomes 0xNN, = 6 characters). The compiler parses that and emits 40 KB in the final binary. The text blowup only exists during compilation, not in the firmware.
iNES format
Most NES ROM files use the iNES format. The first 16 bytes are a header:
| Bytes | Value | Meaning |
|---|---|---|
| 0–3 | NES\x1A | Magic identifier |
| 4 | n | PRG-ROM size in 16 KB units |
| 5 | n | CHR-ROM size in 8 KB units (0 = CHR-RAM) |
| 6 | flags | Mapper low nibble, mirroring, SRAM, trainer |
| 7 | flags | Mapper high nibble |
| 8–15 | Mostly zero in older ROMs |
Mapper number = (flags6 >> 4) | (flags7 & 0xF0). PRG-ROM is the 6502 program code. CHR-ROM is the pattern table data for the PPU (sprites and background tiles). After the 16-byte header, PRG-ROM data comes first, followed by CHR-ROM data.
The iNES 2.0 format adds more fields to bytes 8–15 for extended mapper information. NESpresso32 uses the basic iNES 1.0 fields, which is sufficient for the mappers it supports.
Custom partition table
The standard ESP32 Arduino partition table reserves space for two OTA partitions and SPIFFS. NESpresso32 uses a custom partition table (custom_partitions.csv) that removes all of that in favour of a single large app partition:
The app partition is 0x3C0000 bytes = 3,932,160 bytes = 3.75 MB. No OTA second partition, no SPIFFS. This gives the firmware plus compiled ROM data the maximum possible space on a 4 MB flash chip.
How many ROMs fit
The firmware itself is approximately 1.3 MB, leaving roughly 2.4 MB for ROM data.
| ROM type | Typical size | Approx. count in 2.4 MB |
|---|---|---|
| NROM (32 KB PRG + 8 KB CHR) | ~40 KB | ~60 |
| Mid-size (128 KB PRG + 128 KB CHR) | ~256 KB | ~9 |
| Mixed collection | varies | 20–40 realistic |
The build script prints the total ROM payload size and warns if it exceeds the 2,000 KB soft limit. If the firmware does not fit in the partition, the linker will fail with a size error.
Where the data lives at runtime
PROGMEM on ESP32 maps to __attribute__((section(".irom.text"))). The linker places these arrays in the DROM (data ROM) region, which the MMU makes accessible at virtual address 0x3F400000. The SPI flash is physically separate from the CPU, but this mapping makes flash-resident data readable as normal memory.
When a ROM is selected in the OSD, rom_load_from_memory() copies the PRG and CHR data from flash into DRAM:
After loading, the emulator runs entirely from DRAM. Flash is not accessed again during emulation. DRAM access is one CPU cycle; flash access on a cache miss is 80–100 ns. Running from DRAM is essential for the timing budget.
Frequently asked questions
ROM questions
- How does NESpresso32 load ROMs?
- NESpresso32 uses a Python pre-build script called gen_roms.py that converts .nes files into C++ source files with PROGMEM arrays. PlatformIO compiles these into the firmware binary, so ROMs are embedded directly in SPI flash and available at startup without a filesystem or SD card.
- Does NESpresso32 use an SD card?
- No. NESpresso32 does not use an SD card or any external storage. All ROM data is compiled into the firmware and stored in the ESP32 SPI flash. The OSD ROM browser reads ROM entries from flash-resident data at startup.
- Which ROMs can I use legally?
- NESpresso32 does not include ROM files. For testing and development, use homebrew NES titles or emulator test ROMs from nesdev.org, which are created for this purpose and have clear distribution terms. Commercial NES game ROMs are copyrighted and not freely distributable regardless of their age.
For wiring and hardware setup, see the hardware page. For which mappers are supported, see compatibility.