FC 模拟器 DIY 这个系列写到这篇前后折腾也有快两个月了。前几篇我把 6502 解释器、PPU 渲染、APU 音频一个个啃下来软件模拟器在 PC 上也跑顺了魂斗罗能进游戏、超级马里奥能听到熟悉的开场音效。但说实话我心里一直有个疙瘩它跑在电脑上只是个“程序”不是一台“游戏机”。所以这次从软件转到硬件直接做一台能握在手里的 FC 模拟器。目标很明确用 ESP32 这类嵌入式芯片当主控把之前写好的模拟核心移植进去配上屏幕、按键、电池和外壳做成一台手掌大小的 FC 掌机。这篇就把整个 DIY 的选型、移植、调试、烧录过程全部摊开写适合已经有一定单片机基础、或者正在做模拟器移植的朋友参考如果你只是对“怎么把老游戏机揣进口袋”感兴趣的玩家也能从里面看到完整的实现思路。1. 从软件到硬件为什么要做一台能握住的 FC1.1 前面四篇把软件模拟器做到什么程度了简单回顾一下系列进度。第一篇搞定的是 FC 的核心 CPU——理光 6502指令集、寻址方式、中断处理全部用 C 语言重写跑通 nestest.nes 的官方测试 ROM 之后能保证至少指令层面的行为和原机一致。第二篇做了 PPUPicture Processing Unit图像处理单元也就是 FC 的图形芯片负责背景、精灵、调色板、卷轴滚动这里有个坎PPU 的渲染时序和 CPU 访问内存的时序会互相干扰必须把 1:3 的时钟比例卡准。第三篇做了 APU 音频五个声道2 个矩形波、1 个三角波、1 个噪声、1 个 DPCM 采样全部用混合器合成输出的 wav 文件放在电脑上听已经非常“FC 味”。第四篇则是把时钟同步、输入映射、ROM 加载这些外围逻辑补齐在电脑上可以用键盘玩完整游戏。软件模拟器在这个阶段已经算“能玩”了但问题也暴露得很明显每次都得开电脑、启动程序、加载 ROM完全没有游戏机的仪式感。而且键盘的手感跟当年的十字键AB 键差太远玩超级马里奥时按跳、按加速总感觉慢半拍。我意识到要让这个项目真正完整必须把它从屏幕上“释放”出来做成一个独立运行的硬件。1.2 软转硬的核心难点不是代码是资源约束做软件模拟器的时候我不太需要关心性能预算——PC 上随便跑几百万条指令都没压力。但一旦决定用嵌入式芯片就得面对几个硬约束第一是算力。FC 的 CPU 主频只有 1.79MHz听起来很低但解释型模拟器跑它的指令不是 1:1 运行而是把每条指令分解成取指、译码、执行、内存访问等若干步骤实际开销要放大 10 到 20 倍。一个 240MHz 的 MCU理论上还算宽裕但如果 PPU、APU、显示刷新都要抢 CPU就很容易卡顿。第二是内存。FC 有 2KB 的 CPU RAM、2KB 的 PPU RAM卡带里还有最多几十 KB 的 PRG-ROM 和 CHR-ROM。平移到嵌入式环境我至少需要几百 KB 的内存来放帧缓冲、音频缓冲、游戏状态。板载 520KB SRAM 的 ESP32-S3 正好够用但要小心分配不能像 PC 版那样随意 malloc。第三是外设。屏幕怎么接、按键怎么扫、音频怎么输出、ROM 怎么存、电池怎么供电每个环节都要单独设计电路和驱动。这一层是纯硬件问题写软件的人刚开始会觉得非常琐碎但它恰恰是整个项目里最有“DIY 乐趣”的部分。1.3 硬件方案选型为什么最终锁定 ESP32-S3决定要转入硬件之后我在几种主控之间犹豫了很久列个表给你看当时的对比主控方案核心数/频率内存存储外设接口上手难度成本ESP32 经典版双核 240MHz520KB SRAM4MB FlashSPI、I2S、WiFi不用低低ESP32-S3双核 240MHz512KB SRAM最多 16MB FlashSPI、I2S、ADC、USB低低RP2040双核 133MHz264KB SRAM2MB Flash外挂SPI、PIO、I2S中低STM32F407单核 168MHz192KB SRAM1MB Flash外挂SDSPI、FSMC并口屏高中RP2040 性能其实也不差但 264KB 的 SRAM 在跑完整 FC 模拟器时会有点紧而且它的双核通信用起来不如 ESP-IDF 顺手。STM32F407 外设更丰富但如果用并口屏幕会占掉大量 GPIO留给按键和音频的引脚就不够了。最后我选了 ESP32-S3双核 240MHz 主频足以跑解释型 6502512KB SRAM 放帧缓冲和音频缓冲绰绰有余16MB Flash 可以直接把整个游戏 ROM 合集塞进去不需要外挂 SD 卡模块体积能小很多。再加一条乐鑫的 ESP-IDF 开发框架对 SPI 屏、I2S 音频这些常用外设都有现成驱动能省掉大量底层移植时间。2. 硬件搭建物料、选型与电路细节2.1 主控模组选择与引脚规划芯片我选了 ESP32-S3-WROOM-1 模组注意是“模组”而不是裸芯片。裸芯片是 QFN 封装的焊盘间距很小手工焊接几乎不可能需要钢网、回流焊、X-Ray 检测这些工艺验证流程模组就不一样自带天线和电源管理引脚全部引出烙铁就能焊。这个坑我在其他项目里踩过所以这次老老实实用模组省下的时间远不止一两天。引脚规划当时是最费心思的一步因为整机要用到的外设实在太多屏幕至少要 6 根线SPI 的 SCK、MOSI、DC、CS、RST再加背光按键矩阵至少 8 根线I2S 音频 3 根线锂电池电量检测 1 路 ADC电池充电管理 2 根线。我把 ESP32-S3 的引脚打开先固定了 SPI 屏幕和 I2S 音频的引脚再把剩余 GPIO 排给按键矩阵。具体分配如下外设模块引脚信号ESP32-S3 GPIOSPI 屏幕SCK / MOSI / DC / CS / RST / BLGPIO12 / GPIO11 / GPIO10 / GPIO9 / GPIO8 / GPIO7I2S 音频BCK / WS / DATAGPIO4 / GPIO5 / GPIO6按键 RowR0~R3GPIO15 / GPIO16 / GPIO17 / GPIO18按键 ColC0~C1GPIO21 / GPIO47电池 ADCBATTERYGPIO3有一点要特别注意ESP32-S3 的 GPIO0、GPIO3、GPIO46 等引脚在启动时会有特殊作用比如 GPIO0 会影响下载模式。我当时把音频 DATA 线错接到 GPIO0 上导致每次上电都会自动进入下载模式折腾了一个晚上才发现。所以画原理图之前一定要去数据手册里查一下“strapping pin”列表避开这些坑。2.2 屏幕方案与电路细节屏幕我选了 1.9 英寸 240×320 的 SPI TFT 屏驱动芯片是 ST7789。为什么选 SPI 而不是并口屏因为并口屏虽然刷新速度快但至少要 16 根数据线再加上控制线基本就把芯片引脚用完一半了留给按键和音频的资源完全不够。SPI 屏虽然带宽低一些但对 FC 这种 256×240 分辨率的画面来说240×320 物理分辨率已经能完整容纳实测刷新率也可以接受。屏幕连接电路里有三个容易忽略的地方一是背光控制。ST7789 模块上的背光 LED 如果直接接 3.3V 的话屏幕会一直全亮没法做省电。我加了一个 NPN 三极管S8050做低边开关用 PWM 信号控制背光亮度。玩的时候调 80%待机的时候直接关掉能把整机功耗从 180mA 降到 25mA 左右。二是 SPI 线的上拉电阻。ESP32-S3 的 SPI 引脚是推挽输出本来不需要上拉但我发现接杜邦线的时候线材距离超过 10cm 就可能出现花屏。后来在每个 SPI 线上加了一个 10kΩ 上拉到 3.3V信号边沿变好看了花屏问题也随之消失。这里的原因是 SPI 时钟频率较高我设了 40MHz长线分布电容会导致波形边沿变缓上拉电阻能改善驱动能力但治本的办法还是尽量缩短线距所以我做外壳时特意把屏幕排线折短直接焊在主板上。三是电源去耦。屏幕刷新瞬间电流会有明显波动如果和主控共用一根电源线容易造成复位。我在屏幕模块的 VCC 和 GND 之间并联了一个 100μF 电解电容和 0.1μF 瓷片电容实测电流波动被明显压制。2.3 按键与输入电路从“能按”到“好按”FC 原装手柄是十字键上、下、左、右 选择、开始、B、A 共 8 个按键。我要做成掌机形态就把方向键做成左边十字键、右边 AB 键Select/Start 的布局。为了节省 GPIO按键接成了 4×2 矩阵。矩阵按键的电路本身不复杂但细节很多。最核心的问题是“串键”和“抖动”。串键指同时按两个按键时扫描出现错误组合抖动则是指机械触点闭合瞬间产生的多次高低电平跳变。解决方法分两层。硬件上我在每根行线和列线之间都接了一个 100nF 电容利用电容的充放电特性滤掉高频抖动分量然后在每个按键两端并联 1kΩ 电阻保证即使按键漏电也不会误触。软件上我在按键扫描函数里做了两次采样确认第一次读到按下状态等 5ms 再读一次如果状态一致才认为有效。这两层配合下来实测连招和快速连按都不会出问题。按键手感还和按键本身有关系。我一开始用的是便宜的贴片轻触开关高度太矮、行程太短搓招时手指很不舒服。后来换成 6×6×5mm 的直插轻触开关再在按键帽顶部粘了一个 3D 打印的小圆片手感才接近原装手柄。2.4 电源、电池与充电电路既然是掌机必须内置电池。我选了一节 3.7V 1000mAh 的锂聚合物电池配一个 TP4056 充电模块。TP4056 的充电电流默认是 1A但我实测在电池电压较低时模块发热比较明显所以把电流设置电阻改成了 1.2kΩ把充电电流降到 500mA。这样充电时间虽然延长到 3 小时左右但发热和安全性都更可控。电池电压检测用的是 ESP32-S3 内部的 ADC。这里有个大坑锂电池电压范围是 3.0V~4.2V而 ESP32-S3 的 ADC 参考电压虽然是 3.3V但内部 ADC 的非线性在中低电压区间比较明显。我直接用 1:1 分压电阻把电池电压引入 ADC读出来的数值偏得很厉害误差经常到 0.15V 以上。后来查资料发现ESP32-S3 的 ADC 需要做两步校准先用内部参考电压做偏移校准再做增益校准而且不同芯片之间参数差异很大。这部分的代码放在后面软件章节讲这里先说结论直接用 ADC 裸读数做电量百分比是不靠谱的必须配合实测曲线做分段校正。电源通路上我用了两个 AMS1117-3.3 稳压器一个给主控和屏幕供电一个单独给音频功放供电目的是避免屏幕刷新时的电流毛刺通过地线串进音频输出造成“滋滋”声。音频功放我用的是 PAM8403 小功率 D 类功放板功率虽然只有 3W但驱动一个 8Ω 小喇叭也足够了。2.5 外壳与结构先“飞线”验证再定版外壳是整机看起来“DIY 味”最重的部分。我没有一上来就画 3D 模型而是先用一块洞洞板把所有模块用杜邦线飞线连接起来确认整机功能跑通。这个阶段很重要飞线时你会直观地看到哪些线太长了、哪些引脚位置不合理、哪些模块之间会互相遮挡积累的数据会直接指导后续 PCB 和外壳设计。飞线验证没问题之后我花了一个周末用 OpenSCAD 画了外壳模型分上壳、下壳两半。屏幕放在上半部分按键和电池在下半部分充电接口利用 TP4056 模块自带的 MicroUSB 在小口处开孔。3D 打印用的是 PLA 材料打印层高 0.2mm填充度 30%。打印出来的外壳强度足够日常把玩但要注意 PLA 在夏天车内等高温环境下会软化变形如果要长期使用可以再用 PETG 打印一份。3. 软件移植把模拟核心塞进嵌入式环境3.1 移植策略保留核心抽象硬件层软件层面的移植我从一开始就定了一个原则模拟器核心代码6502、PPU、APU尽量不改只把和硬件交互的部分抽成接口。这样一来我之前在 PC 上写的模拟核心可以原封不动拿过来只需要为 ESP32-S3 写硬件抽象层HAL就行。具体来说HAL 层需要提供下面这些能力定时器生成 1/60 秒的帧同步中断驱动游戏循环。输入按键扫描函数返回 8 个按键状态。显示把模拟器渲染好的 RGB565 帧缓冲推到屏幕。音频把 APU 混合好的 PCM 数据交给 I2S 输出。存储读取 ROM 文件、加载游戏列表。系统串口日志、电源管理、电量读取。模拟器核心则暴露下面几个接口给 HAL 调用void nes_init(void); // 初始化核心加载 ROM int nes_frame_start(void); // 开始一帧模拟返回是否有输入更新 void nes_run_one_frame(void); // 模拟完整的一帧CPUPPUAPU uint16_t *nes_get_framebuffer(void); // 获取 RGB565 帧缓冲指针 void nes_send_audio(int16_t *buf, int samples); // 输出一帧音频样本这里有个工程上的小细节模拟器核心跑的是 60fps每个帧周期是 16.67ms。如果 HAL 层的屏幕刷新也是 60Hz两边节奏天然同步。但实际执行中模拟器一帧的耗时可能不是恰好 16.67ms有时快有时慢。我采取的做法是让模拟器核心在“尽可能快”的模式下跑跑完一帧就把结果交给 HALHAL 在剩余的时间里做屏幕刷新和按键扫描。这样能有效避免模拟器和显示刷新之间的锁死等待。3.2 显示驱动与帧率预算ST7789 SPI 屏的驱动本身不复杂就是初始化寄存器序列、设置窗口、批量发送像素数据。复杂的是怎么高效地把 RGB565 帧缓冲传到屏幕。240×240 分辨率下一帧像素数据是 240×240×2 115200 字节。SPI 时钟我设到 40MHz理论传输时间大约是 23ms比一帧周期 16.67ms 还长根本刷不过来。这里就必须用到 ESP32-S3 的硬件外设特性了SPI 主机可以配 DMADirect Memory Access直接内存访问。把帧缓冲地址告诉 SPI 驱动DMA 会自动把数据搬过去CPU 完全不用参与。于是实际的时序变成这样CPU 跑模拟器核心 14800 微秒。然后启动 SPI DMA 传输CPU 继续去跑下一帧的准备工作和输入扫描DMA 在后台传屏幕数据。这样屏幕刷新虽然每帧需要 23ms但 CPU 只需要花不到 2ms 的人在 PCI 初始化上实际没有“阻塞”模拟器运行。实测下来我在 240×240 分辨率下从 vblank 中断进入到下一帧 vblank 再次触发整个周期是 16.67ms其中模拟器核心占了约 12ms其余 4.67ms 留给 DMA 传输和按键处理没有出现撕裂或闪烁。这里的关键是双缓冲模拟器往 buffer A 写画面DMA 从 buffer B 读画面这一帧结束再交换。如果用单缓冲DMA 还没读完模拟器就把新画面写进去了必然撕裂。3.3 输入扫描与音频输出的实现细节输入扫描我放在了定时器中断里每 1ms 扫描一次按键矩阵。注意中断函数里不能调用 printf 或者太耗时的函数否则会打破模拟器的帧时序。我的做法是中断函数只负责把扫描结果写入一个全局变量主循环里读这个变量、做防抖和状态变化检测。音频部分比我想象的麻烦。APU 输出的采样率是 44100Hz也就是每帧要输出 44100 / 60 735 个样本。ESP32-S3 的 I2S 外设可以直接以 DMA 方式播放这些样本但这里有个缓冲管理的坑I2S 外设每次取出数据是块状的如果模拟器来不及填满缓冲播放就会出现“咔哒”声或者断续。我的解决办法是用两个环形缓冲模拟器写环形缓冲 AI2S DMA 从中读取并播放。当 A 快空的时候模拟器还没填完就先重复播放最后一帧数据同时让游戏循环降速一帧。这种“丢一帧音频”的策略在 FC 游戏里几乎听不出来但能避免更刺耳的爆音。后来我又做了一次优化把 APU 混音从 44100Hz 降到 22050Hz 再升采样回 44100Hz。原理是 FC 原机的 APU 本身输出质量就不高22050Hz 的采样率已经能覆盖大部分主旋律频率降采样后再内部插值占用的 CPU 能省下接近三分之一。3.4 性能预算与调优实测我把最终的性能开销拆开算了一遍贴出来给各位参考ESP32-S3 240MHz 双核模拟器核心跑在 Core 0HAL 和通信跑在 Core 1模块开销微秒/帧占比6502 CPU 模拟310020.9%PPU 渲染540036.5%APU 音频混音190012.8%ROM 内存映射 / Bank 切换8005.4%SPI DMA 屏幕传输后台2300不阻塞—按键扫描/电量采样3002.0%调度与任务切换4002.7%剩余余量287519.4%算上 DMA 后台传输的“隐性占用”总 CPU 占用大约在 85% 左右还算稳定。当时为了把 PPU 渲染从最初的 8700 微秒降到 5400 微秒我做了两个关键优化一是把像素循环中的 modulo取模运算全部换成位运算和查表PPU 输出的调色板索引直接用 4 个 bit shift 就能拿到二是利用 PPU 渲染按扫描线推进的特点只在扫描线 240 结束时才整帧提交到帧缓冲中间的过程不需要频繁写外部 RAM。4. ROM 整理、游戏列表与烧录流程4.1 ROM 格式与 Mapper 的基础概念FC 卡带里存的是 PRG-ROM程序数据和 CHR-ROM图形数据但不同卡带的容量、内存映射方式差异很大所以业界搞了一套标准格式最常用的是 iNES 格式。iNES 文件头有 16 字节其中第 6、7 字节表示 PRG-ROM 的 16KB 单元数和 CHR-ROM 的 8KB 单元数第 7 字节的低 4 位则记录了 Mapper 编号。Mapper 你可以理解为“卡带里的银行切换电路”。FC 的 CPU 地址空间有限很多游戏卡带容量超过 CPU 能直接寻址的范围就必须用 Mapper 电路把不同的 ROM 分页映射进地址空间。不同 Mapper 的切换逻辑完全不同模拟器每支持一个新 Mapper就要为其实现一套内存映射回调。我第一期只做了 Mapper 0无切换也就是 NROM比如超级马里奥、Mapper 1MMC1支持 1-bit 串行加载很多早期 Capcom 游戏用它、Mapper 2UNROM魂斗罗等大量动作游戏这三种基本覆盖了 FC 最著名的一批游戏。4.2 ROM 资源从哪来整理与合规性问题这是个绕不开的话题。我在这台机器上测试用的 ROM 来自几个渠道一是自己手头没办法收集的实体卡带用烧录座读卡器把原卡内容 dump 出来这个最合法但操作门槛很高二是开发社区公开的“公共领域”游戏和演示 ROM这些作者明确授权可以自由分发三是我自己写的测试 ROM用来验证特定 Mapper 的行为。我建议你也不要从非正规渠道批量下载整套 ROM一来有版权风险二来质量参差不齐。倒是在一些自制游戏社区能找到很多质量不错的 homebrew 作品它们本身就是开放授权的用在这类 DIY 项目里特别合适。游戏列表管理上我的做法是在 Flash 文件系统LittleFS里放一个 games.ini 文件每行记录一条游戏的信息[game] nameSuper Mario Bros. mapper0 prgsuper_mario.sfc _crc320x8A4B5F21 [game] nameContra mapper2 prgcontra.sfc _crc320x7E2A94C0机器启动后扫描这个配置文件生成一个简单的游戏列表用十字键选游戏、A 键确认启动。这套流程虽然简单但使用体验非常接近原版 FC 插卡带的感觉。4.3 打包、校验与烧录的完整流程所有 ROM 最后要打包进 ESP32-S3 的 Flash。我用的是 esp-idf 的“partitions.csv”配置把 Flash 分成三个分区固件分区、LittleFS 数据分区、OTA 分区备用。编译完成后用下面的命令烧录esptool.py --chip esp32s3 --port /dev/ttyACM0 write_flash -z 0x10000 build/nes_esp32.bin esptool.py --chip esp32s3 --port /dev/ttyACM0 write_flash -z 0x310000 build/littlefs.bin注意烧录时串口波特率我设的是 921600超过这个值有些便宜的 USB 转串口芯片会不稳定出现随机校验失败。如果手头的 USB 转串口芯片比较老建议老老实实用 460800。烧录成功后我给每一份 ROM 都算了一个 CRC32 值以二进制方式拼接在一个索引文件里。模拟器启动时先读索引再按偏移量读取对应的 ROM 数据比对 CRC 一致才加载。这样做能有效避免 ROM 文件损坏导致运行到一半死机。5. 实机调试与体验调优5.1 帧率稳定性优化动态跳帧策略硬件上电跑通的第一天我遇到的最大问题是主菜单帧率不稳定。后来发现原因在 ESP32-S3 默认的“均衡”CPU 调频策略上芯片会根据负载动态调整主频但模拟器这种“短时间高负载”的任务恰恰不适合这种策略负载上来了、频率也上来了但频率提升是需要时间的于是个别帧会跑到 20ms造成明显的卡顿感。解决方法是把 CPU 调频模式设成“性能模式”固定 240MHz不做自动调频。同时开启“双核”模式让模拟器核心独占 Core 0通信任务和显示驱动独占 Core 1两核之间用 FreeRTOS 队列通信。这样改完帧率稳定在 59.4~60.1fps 之间肉眼已经看不出波动。如果遇到某些游戏在复杂场景下帧率掉到 55fps我建议用动态跳帧而不是降低主频。策略是这样的当模拟器发现“本帧耗时已经超过 20ms”就直接丢弃当前帧把画面保持为上一帧但照常模拟 PPU 和 APU。这样用户体验是偶尔“跳过一帧”而不是持续变慢玩动作游戏的感知差异非常大。这个策略实现也不复杂核心加减帧计数器即可。5.2 手感调校与延迟测试FC 游戏对手感极其敏感尤其是超级马里奥和魂斗罗。我最初用蓝牙手柄试玩延迟明显延迟主要来自蓝牙协议栈的排队和重传机制所以直接弃用了回归物理按键矩阵。物理按键的延迟来自三个地方按键扫描周期、模拟器输入采样、屏幕刷新。按键扫描周期是 1ms模拟器每个帧周期会读取一次输入最多多等 16ms屏幕刷新一般从扫描线 0 开始显示上屏时间约几 ms。整体延迟量级大约在 25ms 以内和原版 FC 接 CRT 电视的延迟比较接近。如果你需要更低延迟可以把按键扫描放到 PPU 渲染到非可见区域的时间窗口里让输入在帧开始时就被锁存这样能再压掉 5~8ms。调好之后我拿超级马里奥做测试按住 B 键跑步接近马里奥即将跳跃的位置按下 A跑步、起跳、滞空、落地的节奏都很跟手完全可以用“手感扎实”来形容。5.3 功耗与续航实测掌机必须关心续航。我用了万用表串在电池和主板之间测试结果如下使用状态电流估算续航1000mAh 电芯满亮度 玩游戏180mA约 5.5 小时80% 亮度 玩游戏140mA约 7 小时待机屏幕关闭25mA约 40 小时深度睡眠0.8mA约 52 天玩动作游戏时平均电流 180mA 比我预期高主要耗电大户是屏幕背光和音频功放共模电流。我后来给背光控制加了“自动变暗”策略开机 5 分钟内如果没有任何按键操作背光自动降到 30%继续 3 分钟无操作直接息屏但游戏暂停。这样放在口袋里忘记关机一天损耗也很小。6. 常见问题与排查速查表最后把我在这个项目里遇到过的典型问题和解决办法整理成一张表。这些经验花了挺多时间换来的希望能帮你避开同样的坑现象可能原因排查思路与解决办法开机屏幕白屏/蓝屏屏幕初始化失败或 SPI 线序错误先检查屏幕型号对应的初始化序列再用逻辑分析仪抓 SPI 波形确认 SCK/MOSI 有没有收到数据最后核对 DC/CS/RST 三个控制引脚的电平时序游戏能进但花屏ROM 的 Mapper 和模拟器不匹配或 SPI 线太长查看游戏对应的 iNES 头 Mapper 编号确认模拟器支持检查 SPI 线距是否超过 10cm必要时降 SPI 时钟到 20MHz按键串键或者无响应矩阵按键扫描逻辑错误硬件漏电先用万用表短接按键引脚确认每个按键的电气连接正常再在扫描函数里打印按下的行列值检查行列映射是否有交叉声音爆音/断续I2S 缓冲未及时填充DMA 缓冲太小增大 I2S 的 DMA 缓冲深度调低 APU 采样率或者实现“丢一帧音频”的兜底逻辑烧录失败串口波特率过高USB 芯片驱动能力不足没有按住 BOOT更换 USB 线降低波特率到 460800按住 BOOT 键再上电或者按住 BOOT 后短按复位再放开 BOOT电量显示不准ADC 未校准分压电阻精度低做两点校准空电 3.0V、满电 4.2V用 1% 精度电阻如果仍不准使用查表分段线性插值电池充电指示灯一直闪TP4056 充电电流设置不合适电芯内阻过大检查 PROG 引脚电阻阻值适当降低充电电流更换老化电芯菜单可以操作进入游戏后无限重启内存分配不足导致堆溢出在启动日志里加内存信息打印减少 LittleFS 缓存大小检查是否有 const 大数组占用大量静态内存玩一会屏幕变暗我自动加的背光策略触发检查背光控制代码确认超时逻辑或者把超时时间调长除了上表我再分享两个排错的小心得。第一嵌入式项目一定要善用日志但日志不能打得太频繁否则会拖垮帧率。我通常用宏控制日志级别只在调试版本输出详细日志发布版本只保留错误日志。第二遇到疑难问题时用逻辑分析仪比用示波器顺手得多尤其像 SPI、I2S 这种低速串行协议逻辑分析仪能直接把数据波形解码成文字一眼就能看出数据内容对不对。最后再补一句实际操作中的体会DIY 模拟器最大的价值其实不在于“多完美”而在于你能通过这一台小机器把一台 40 年前的游戏主机从 CPU 到画面、从声音到手感完整地理解一遍。软件模拟器的逻辑可以随意改写但当你亲手把它焊进电路板、握在手里玩通一关的时候那种“原来是这么回事”的感觉是任何文档和教程都给不了的。这台机器后续我还在扩展新的 Mapper 支持打算把磁盘系统FDS和更多自制游戏也加进去路由就是从这版代码上的一个分区一个分区地改就行。