NES + ESP = NEESP — a handheld NES emulator for ESP32, with wireless two-player multiplayer over ESP-NOW. No router, no accounts: two consoles, one couch (or workbench).
Built on the excellent DSN-Nes-Emulator-Universal / Nofrendo stack, heavily adapted for real hardware: ST7789 display, I2S audio, LittleFS game storage, and a custom network session for 1P/2P play.
| Repo | github.com/darrrgghh/NEESP |
| Flash guide | docs/FLASHING.md |
| Pinouts | docs/HARDWARE.md |
| Controls | Controls (this file) |
| Multiplayer | docs/MULTIPLAYER.md |
On the main handheld (ESP32-S3 N16R8), the flash partition gives you about 13.4 MB of LittleFS for ROMs — enough for on the order of ~60 small .nes files if you optimize the set.
The menu title “NEESP 50 in 1” is simply the curated pack I liked: I only kept 50 games that ran well on this hardware. You can change that name and the game list anytime — drop your own ROMs into games/ and, if you want a different banner, edit one string in firmware/src/osd.cpp (MAX_GAMES is 50 in the same file for the menu list).
This repository does not ship copyrighted ROMs. See games/README.md.
- Two-player multiplayer (ESP-NOW) — host = player 1, client = player 2; inputs synced per frame; pause sync; host can approve or force game launch.
- ROM pull — if player 2 is missing a game, the host can send it over the air into LittleFS (handy on the small-flash V3 build).
- All-in-one handheld — ESP32-S3, 2.0" ST7789 320×240, MAX98357 I2S speaker, GPIO gamepad, many games in internal flash (no SD card in firmware).
- Three hardware targets — two orientations of the same S3 build (V1 / V2) plus a breadboard ESP32 DevKit variant (V3).
- Save states — SNSS slots on LittleFS under
/littlefs/saves/- easily save your states in all games, load and delete them (max 5 saves per game).
Not in scope (yet): 3+ players, online play over the internet.
Assembled handheld, then the same unit opened up.
V2 is the same PCB and wires as V1. Only the display rotation (180°) and D-pad GPIO mapping change so the shell mounting direction still feels right.
Smaller flash, analog joystick, five ROMs baked in by default — great for experiments and multiplayer with ROM pull from a full S3 host.
| MCU | ESP32-S3, 16 MB flash, 8 MB OPI PSRAM |
| Display | 2.0" SPI ST7789 (GMT020 class), landscape 320×240 |
| Audio | MAX98357A (I2S) |
| Input | 8× tactile buttons (pull-up, active low to GND) |
| Storage | LittleFS (~13.4 MB for ROMs) — no microSD in firmware |
| PlatformIO | esp32-s3-n16r8 |
V1 vs V2 — only these differ:
| V1 | V2 | |
|---|---|---|
HARDWARE_PROFILE |
1 | 2 |
| Screen rotation | DISPLAY_TFT_ROTATION = 3 |
= 1 (180°) |
| D-pad GPIO | profile 1 table | remapped (vertical ↔ horizontal, up ↔ down) |
| A / B / START / SELECT | same pins | same pins |
| TFT / I2S wiring | same | same |
Full pin tables: docs/HARDWARE.md.
| MCU | Classic ESP32 DevKit (~4 MB flash) |
| Display | ST7789, VSPI pinout (see V3 section in hardware doc) |
| Input | A/B/Start/Select on GPIO + ADC joystick for D-pad |
| ROMs in image | First 5 .nes files (alphabetical); more via multiplayer ROM pull |
| PlatformIO | esp32d-v3 (HARDWARE_PROFILE=3) |
flowchart TD
start[What board do you have?]
start -->|ESP32-S3 N16R8 handheld| s3[Environment esp32-s3-n16r8]
start -->|ESP32 DevKit breadboard| v3[Environment esp32d-v3]
s3 --> orient[How is the display mounted?]
orient -->|Default| p1[HARDWARE_PROFILE 1 — V1]
orient -->|Upside down| p2[HARDWARE_PROFILE 2 — V2]
| Target | Set profile | Commands (from firmware/) |
|---|---|---|
| V1 | HARDWARE_PROFILE 1 in board_profile.h |
pio run -e esp32-s3-n16r8 -t upload then uploadfs |
| V2 | HARDWARE_PROFILE 2 (same env) |
same |
| V3 | automatic in esp32d-v3 |
pio run -e esp32d-v3 -t upload then uploadfs |
First time on a chip: always flash both upload (app) and uploadfs (games partition).
V1 ↔ V2 only: changing profile needs upload only, not uploadfs, if games are already on the device.
Details: docs/FLASHING.md.
- Install PlatformIO.
- Clone this repo and add your legal
.nesfiles togames/. cd firmware- Pick your environment (see table above) and run upload + uploadfs.
- Open serial monitor @ 115200 — look for
TFT OKandLittleFS OK.
If the PC does not see the port: hold BOOT, press RESET, release BOOT.
NEESP uses a familiar NES-style layout on every build. On the V3 breadboard, the D-pad is an analog joystick (X/Y); A, B, START, and SELECT are separate GPIO buttons. On V1/V2 handhelds, all eight directions are tactile buttons.
| Button | NES meaning | NEESP menus |
|---|---|---|
| D-pad | Up / Down / Left / Right | Move cursor, adjust values |
| A | NES A button | Confirm / select |
| B | NES B button | Cancel / back (in menus) |
| START | NES Start | Start game from list; in pause menu — resume |
| SELECT | NES Select | Open Settings from the game list; in-game — Select as on a real NES |
Buttons are active low (press = connect to GND). There is a short debounce (~150 ms) on menu repeats.
After power-on you land on the game list (“NEESP 50 in 1” title, scrollable ROM names, 1/N counter top-right).
| Input | Action |
|---|---|
| Up / Down | Move selection (list scrolls when there are more than 5 games) |
| Left / Right | Jump by page (5 titles per page) |
| A or START (without SELECT) | Launch highlighted game |
| SELECT | Open Settings (see below) |
When multiplayer is connected, launching may ask the host to approve (2P) or start immediately (1P). See Multiplayer.
Press SELECT on the game list, then:
| Menu item | Keys inside |
|---|---|
| Volume | Up / Down — change level (0–200%); A — keep; B — revert and back |
| Connection | Up / Down — move; A — activate row; B — back to game list. Rows: Initialize connection (1P host), Establish connection (2P client), Disconnect, Back |
| Back | A on “Back”, or B anywhere — return to game list |
Connection screen legend is shown at the bottom (host vs client hints). Screenshot: menu.jpg.
| Input | Action |
|---|---|
| D-pad, A, B, START, SELECT | Passed to the NES as on original hardware (Start/Select work in-game for games that use them) |
| START + SELECT together | Open the pause menu (game freezes; pause can sync to the other console in multiplayer) |
Hold both START and SELECT until the pause window appears, then release both (the firmware waits for no buttons pressed so Start does not instantly resume).
Opened with START + SELECT. Main items:
| Item | Action |
|---|---|
| Resume | A on Resume, or START / B from the main pause list — continue playing |
| Volume | Up / Down adjust; A confirm; B cancel changes |
| Load game | Pick slot 1–5 (Up/Down), A load (confirm dialog), B delete save in slot, START back to pause main |
| Save game | Same slot UI; A save (overwrite confirm if slot used) |
| Main menu | Quit to game list (emulator stops) |
Save slots: 5 per game (Slot 1 … Slot 5), stored on LittleFS under /littlefs/saves/.
Multiplayer: on player 2 (client) console, Load game and Save game are disabled (only 1P / host may read/write saves).
Confirm dialogs (load / save / delete): A yes, B no.
Power on
└── Game list ──SELECT──► Settings ──► Volume / Connection
│ A or START
└── Playing ──START+SELECT──► Pause ──► Resume / Volume / Load / Save / Main menu
- Put
.nesfiles ingames/(repo root). - Build from
firmware/—copy_games_to_data.pycopies them into the LittleFS image with safe filenames. pio run -e <your-env> -t uploadfs
Legal note: you are responsible for ROMs you use. This project does not distribute Nintendo or third-party commercial ROMs.
Short version:
- Settings → Connection on both consoles (see screenshot below).
- Console A: Initialize connection (host / 1P).
- Console B: Establish connection (client / 2P).
- Pick a game; host = player 1, client = player 2.
Full walkthrough, ROM pull, and troubleshooting: docs/MULTIPLAYER.md.
| Area | Upstream-style Nofrendo / DSN | NEESP |
|---|---|---|
| Platform | Desktop / generic ports | Arduino on ESP32-S3 and ESP32 |
| Video | SDL or similar | TFT_eSPI, banded pushImage, ESP32-S3 FSPI fix |
| Storage | Files / SD paths | LittleFS in flash, ROM preload |
| Display | Often larger panels | ST7789 2.0" 320×240, rotation profiles, CGRAM patch script |
| Audio | Platform default | I2S ring buffer + dedicated task; autoframeskip underrun fix (notes) |
| UI | Minimal | Game menu, volume, connection settings, pause + save slots |
| Multiplayer | — | ESP-NOW net_session (2 players) |
| Hardware | One reference design | Profiles 1 / 2 / 3 (rotation, DevKit + joystick) |
Core CPU/PPU/APU emulation code still traces back to Nofrendo and the DSN port; the “product” layer around it is new.
NEESP/
├── firmware/ PlatformIO project (build & flash here)
│ ├── src/ Emulator + NEESP UI + net_session
│ ├── include/ Pins, board profiles, TFT setups
│ └── data/ ROM staging for LittleFS (generated from games/)
├── games/ Put your .nes files here (not in git)
├── docs/
│ ├── HARDWARE.md
│ ├── FLASHING.md
│ ├── MULTIPLAYER.md
│ └── images/ Photos (v1a/b, v2a/b, v3a/b, menu)
└── LICENSE GPL v3 (see upstream DSN)
| Problem | Hint |
|---|---|
| Black screen, backlight on | Check SPI pins, TFT OK in serial — docs/HARDWARE.md |
| Red/blue screen ~3 s | No ROMs — run uploadfs after adding games to games/ |
Guru Meditation 0x10 on S3 |
USE_FSPI_PORT in User_Setup.h; clean rebuild |
| Left garbage strip | Rebuild so patch_st7789_rotation.py runs |
V3 won’t link (dram0_0_seg) |
Use current tree (large buffers on heap) |
| Multiplayer won’t pair | Host initializes first; see docs/MULTIPLAYER.md |
No STL files live in this repo. Start from DSN-Nes-Emulator-Universal enclosure models, then adapt for a 2.0" panel instead of the original 2.8" design.
- Emulator base: Nofrendo / DSN-Nes-Emulator-Universal by @derdacavga
- NEESP firmware, hardware profiles, UI, and ESP-NOW multiplayer: Alexey Voronin (darrrgghh)
- Libraries: TFT_eSPI, Espressif Arduino/IDF
Copyright (c) Alexey Voronin for NEESP-specific code. Licensed under the GNU General Public License v3.0 — see LICENSE and NOTICE. You may use, modify, and share this project under GPL v3; comply with GPL when you distribute binaries or forks.
Issues and PRs welcome on GitHub. If you build a variant (different screen, 4-player dream, etc.), we’d love to see photos in discussions.







