把 NES 红白机模拟器塞进一颗只有 400KB SRAM 的 ESP32-C3,能跑得动魂斗罗吗?答案是:能!本文带你从零复现这个项目。
🎯 项目简介
本项目基于 arduino-nofrendo 改造,针对 ESP32-C3 单片机进行了深度优化,让一颗成本不到 10 元的 MCU 也能流畅运行 NES 游戏。
✨ 核心特性
- 🖥️ ST7789 240×240 全屏显示,NES 256×240 自动裁剪适配
- 🎮 PG-9193 蓝牙手柄 支持,通过 NimBLE 协议栈实现
- 💾 mmap 零拷贝 ROM 加载,DRAM 占用为 0
- 📀 存档/读档 功能,X/Y 键即可保存/恢复游戏状态
- 🔌 串口键盘 备用控制(WSAD + JK)
🔧 硬件接线
| TFT 引脚 | ESP32-C3 GPIO | 说明 |
|---|---|---|
| VCC | 3V3 | 电源 |
| GND | GND | 接地 |
| SDA | GPIO 6 | SPI MOSI |
| SCK | GPIO 4 | SPI SCLK |
| DC | GPIO 1 | 数据/命令 |
| RES | GPIO 7 | 复位 |
| BLK | GPIO 5 | 背光 |
💡 技术亮点
1. mmap 零拷贝:榨干 Flash 当 RAM 用
ESP32-C3 只有 400KB SRAM,但很多 NES 游戏 ROM 都超过 256KB(比如恶魔城)。常规做法是把 ROM 读到 RAM 里,但 RAM 根本不够用。解决方案:
// 把 SPIFFS 分区直接映射到 CPU 地址空间
const esp_partition_t *part = esp_partition_find_first(
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, NULL);
esp_partition_mmap(part, 0, part->size, ESP_PARTITION_MMAP_DATA, &rom_ptr, &handle);
// 现在 rom_ptr 像普通指针一样可以读取!
CPU/PPU 访问 ROM 就像访问内存一样,DRAM 占用为 0,经测试,可以跑337KB以内的任意 ROM。
2. NimBLE 替代 Bluedroid:省下 80KB RAM
ESP32 默认的 Bluedroid 蓝牙协议栈大约占 110KB RAM,对 C3 来说太奢侈了。换成 NimBLE 只占 30KB,把省下的 80KB 留给 NES 模拟器。
3. 后台 BLE 任务 + 暂停游戏定时器
蓝牙连接是耗时操作,会抢 CPU 时间导致游戏卡顿。解决方法:
- BLE 扫描/连接放到独立 FreeRTOS 任务中
- BLE 初始化期间暂停 NES 帧定时器,避免竞争
- 连接成功后恢复,玩家几乎感觉不到
4. 存档存在 coredump 分区
ESP32 默认有个 64KB 的 coredump 分区用来存崩溃信息,这里被我"借用"来存游戏存档:2 个槽位,按 X 存档、按 Y 读档。
🎮 按键映射
| PG-9193 手柄 | NES 功能 |
|---|---|
| 方向键 | 方向键 |
| A / B | A / B |
| SELECT / START | SELECT / START |
| X | 💾 存档 |
| Y | 📂 读档 |
🚀 快速开始
- Arduino IDE 安装
Arduino_GFX和NimBLE-Arduino库 - 开发板选 ESP32C3 Dev Module,分区方案选 Default 4MB with SPIFFS
- 编译烧录主程序到 ESP32-C3
- 运行
upload_spiffs.ps1 -Rom .\data\Chase.nes烧录 ROM - 上电!蓝牙手柄按 Home 键配对,自动连接
⚠️ 踩过的坑
- 不要装 Arduino IDE 的 nofrendo 库:项目自带
src/目录已包含完整源码,重复会冲突 - ESP32-C3 是单核:复杂场景会掉帧,关闭 BLE 可略微提升性能
- 音频已禁用:项目硬件没接 DAC/PWM 喇叭,留给后续扩展
📚 致谢
- arduino-nofrendo — 原始项目
- NimBLE-Arduino — 轻量蓝牙协议栈
- Arduino_GFX — TFT 屏幕驱动库
欢迎到 GitHub 给项目点个 ⭐!有问题或者改进建议都可以提 Issue~