0%

🎮 ESP32-C3 NES 模拟器:在单片机上看魂斗罗

把 NES 红白机模拟器塞进一颗只有 400KB SRAM 的 ESP32-C3,能跑得动魂斗罗吗?答案是:能!本文带你从零复现这个项目。

📦 项目地址:github.com/noahgeek/esp32-nofrendo

🎯 项目简介

本项目基于 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说明
VCC3V3电源
GNDGND接地
SDAGPIO 6SPI MOSI
SCKGPIO 4SPI SCLK
DCGPIO 1数据/命令
RESGPIO 7复位
BLKGPIO 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 / BA / B
SELECT / STARTSELECT / START
X💾 存档
Y📂 读档

🚀 快速开始

  1. Arduino IDE 安装 Arduino_GFXNimBLE-Arduino
  2. 开发板选 ESP32C3 Dev Module,分区方案选 Default 4MB with SPIFFS
  3. 编译烧录主程序到 ESP32-C3
  4. 运行 upload_spiffs.ps1 -Rom .\data\Chase.nes 烧录 ROM
  5. 上电!蓝牙手柄按 Home 键配对,自动连接

⚠️ 踩过的坑

  • 不要装 Arduino IDE 的 nofrendo 库:项目自带 src/ 目录已包含完整源码,重复会冲突
  • ESP32-C3 是单核:复杂场景会掉帧,关闭 BLE 可略微提升性能
  • 音频已禁用:项目硬件没接 DAC/PWM 喇叭,留给后续扩展

📚 致谢

欢迎到 GitHub 给项目点个 ⭐!有问题或者改进建议都可以提 Issue~