
最近把吃灰半年的一块PIC32开发板翻出来做了一个能玩俄罗斯方块、贪吃蛇和打砖块的小型街机。先说明一下不是那种跑Linux的高大上模拟器方案而是纯MCU实现的“硬核”迷你街机。整个项目从选型、画板到写游戏逻辑折腾了大概三周今天把完整的设计过程和踩坑记录整理出来。如果你手上也有PIC32系列开发板或者正纠结要不要用MCU做游戏项目这篇内容应该可以帮你少走不少弯路。这个项目为什么值得做因为PIC32这类32位MCU的性能刚好卡在一个很有趣的甜点位置比8位单片机强得多能跑320x240分辨率的图形界面又能维持稳定的游戏帧率但和树莓派这类带操作系统的方案比它又保留了“裸机编程”的纯粹感所有时序、渲染、输入扫描都要自己控制。做完这个项目你对中断、定时器、DMA、外设配置这些嵌入式基本功的理解会明显上一个大台阶。1. 项目整体思路与架构拆解1.1 为什么选PIC32而不是树莓派或Arduino这是我在构思阶段考虑最久的问题。市面上做“居家街机”的方案其实很多但差异非常大方案成本启动时间游戏帧率稳定性调试难度适合人群树莓派RetroPie高慢十几秒甚至更久高但依赖系统调度低想快速玩大量游戏的人Arduino Mega低即时中低8位性能瓶颈中想学习基础嵌入式的人ESP32低快较高但并行外设不如PIC32丰富中喜欢Wi-Fi功能的人PIC32中低即时高硬件外设全直控中高想深入学习MCU底层的人我最终选择PIC32核心原因是看中了它的“无操作系统 硬件外设直控”特性。游戏循环最怕的就是不确定性树莓派上如果你跑的进程被系统调度抢占了CPU哪怕这个延迟只有几毫秒玩家都能感觉到画面卡顿。而PIC32上所有外设都是寄存器直接控制的没有调度器在背后抢时间片只要你把定时器中断设置好渲染和更新逻辑就是严格按时间片执行。还有一个原因是学习价值。用树莓派做街机大部分工作其实是“装系统、下载镜像、拷贝ROM”这对硬件理解帮助有限。但用PIC32做你需要自己去配置采样时钟、操作SPI刷屏、处理按键抖动整套流程走完你会对“一块芯片是怎么跑起来的”有非常扎实的理解。1.2 整体系统架构显示、输入、音频三大模块怎么协同整个街机系统的核心模块可以分成三块显示子系统、输入子系统和音频子系统。它们围绕一颗PIC32主控MCU来工作系统框图大致是这样主控PIC32负责运行游戏逻辑、渲染画面、采样输入、生成音效是整个系统的核心。显示屏这里选的是SPI接口的TFT液晶屏主控通过SPI总线把像素数据实时刷到屏幕上。输入包含一个二维摇杆和若干按键摇杆用ADC采样模拟量数字按键通过GPIO扫描读取。音频主控的OC比较输出模块产生PWM方波经过一个简单的RC低通滤波电路之后驱动一个小喇叭发声。电源USB 5V输入经稳压芯片降压到3.3V给主控和其他模块供电按键背光LED则直接从5V取电。打个比方这套系统就像一个小型交响乐团主控是总指挥显示屏、输入、音频是三个乐手定时器中断就是指挥棒挥下的节拍点。每一次节拍到来总指挥就要依次确认当前按键状态是什么游戏逻辑要更新到哪一帧屏幕哪些区域要重绘这一帧该不该发出声音。只要这三个模块的时序配合得当整个系统就能流畅运行。1.3 功能与扩展性设计做这个项目时我刻意控制了一个原则核心功能做扎实外围功能留好扩展点。核心功能包括三款内置游戏俄罗斯方块、贪吃蛇、打砖块通过开机菜单选择。最高分记录写入PIC32内部Flash掉电不丢失。独立按键音效和背景音乐切换功能。扩展点方面我预留了UART串口和几个空闲GPIO。一个很实用的扩展是串口日志开发阶段通过UART把调试信息输出到PC上位机这比用示波器猜状态要高效得多。另外我还计划后续加一个MicroSD卡槽用来加载外部图片素材和更多游戏数据这块在硬件设计时就把SPI的片选信号预留了。2. 硬件搭建核心器件选型与接线方案2.1 主控选型PIC32MX270F256B我用的主控是Microchip的PIC32MX270F256B这颗芯片在PIC32家族里算性价比很高的型号。具体参数参数数值内核MIPS M4K80MHz主频Flash256KBRAM64KBADC10位最多13通道定时器5个16位可级联为32位SPI2路UART2路PWM输出最多5路OC比较输出封装28引脚SPDIP/SOIC或44引脚TQFP选它的核心原因64KB RAM在MCU阵营里算是“大内存”可以做画面缓冲80MHz主频足够完成简单图形渲染每帧几十条图元绘制指令28引脚封装在焊接和手搓面包板时会轻松很多。如果预算稍微充裕一点用PIC32MX270F512B512KB Flash版本也可以但256KB Flash对三款小游戏加图形库来说其实已经绰绰余了。2.2 显示屏SPI屏与并口屏的取舍这是整个硬件设计里最值得思考的部分。当前低成本TFT屏有两种主流接口一种是SPI接口比如市面上常见的1.8寸128x160和2.2寸320x240TFT屏引脚少、接线简单但刷新率受SPI时钟限制。PIC32MX270的SPI外设最高可以跑到40MHz左右实测刷一块320x240全彩图分辨率x 16bit理想情况也需要大约320x240x2字节/10Mbps≈0.12秒所以整屏刷新做动画帧率不够必须用局部重绘策略。另一种是MCU并口16位数据总线刷新速度快很多但需要占用大量GPIO。以PIC32MX270F256B的28脚封装为例GPIO总数只有22个左右接16位并口屏几乎把所有引脚占用完就没法再接摇杆和按键了所以这个封装下并口屏并不现实。我实际选的是2.2寸SPI接口TFT屏内置ILI9341控制器。在4线SPI模式下每次命令或数据的协议开销是可接受的实测用DMA SPI发送120x160区域的局部动画可以做到稳定30FPS关键部分再加一点优化比如只重绘变化区域俄罗斯方块这样的小游戏体验已经非常流畅。显示屏的光源和触摸方面我选的这块屏是带电阻触摸的但真正做街机时触摸体验不如实体按键所以触摸功能只是用来做校准用的操作以摇杆和按键为主。2.3 音频输出PWM直推与RC滤波音频是这个项目里容易被忽略但影响体验很大的模块。街机如果无声整个氛围感会差很多。PIC32的OCOutput Compare模块可以很方便地产生PWM信号。我用的方案是用一个定时器作为时间基准PWM频率设为约8kHz通过占空比变化来模拟简单方波音效。音高变化通过改变PWM周期实现音量通过占空比控制。关键是PWM信号直接接喇叭会有刺耳的“咔咔”声需要加一个RC低通滤波器。我实测用一个100Ω电阻串联加一个10uF电容并联到地可以滤掉大部分高频载波成分声音会柔和不少。如果想做更高质量的音效可以外接一个便宜的PAM8403功放模块从PWM输出接一个10uF电容隔直后进入功放输入端音质会有明显提升。我最终做的是PWM直连一个小型8Ω喇叭没有外接功放声音音量和音质够用但低音基本没有只适合播放简单游戏音效。如果以后做音乐播放我可能会换I2S接口DAC模块再加一个小功率功放。2.4 输入设备摇杆与按键接线街机的输入要做到“快、准、稳”。我用的是街机常见的小型模拟摇杆内部是两个10k电位器横轴和纵轴输出接到PIC32的ADC输入通道。ADC每次采样大约耗时1-2us轮询摇杆的延迟几乎可以忽略。按键方面我用了6个按键上、下、左、右、A、B加上摇杆基本可以覆盖所有游戏操作。按键接法有两种下拉电阻方式按键连接GPIO到高电平按下时读到高电平需要外加下拉电阻到地。内部上拉方式PIC32的GPIO内置弱上拉按键一端接GPIO另一端接地按下时读到低电平代码里反转逻辑即可。我采用的是内部上拉方式好处是省掉了6个外部电阻接线更清爽。需要注意的是内置上拉电阻阻值大约是50k-100k抗干扰能力不如外部10k上拉强所以我在按键到GPIO之间串了一个100Ω电阻做简单保护同时在软件里做了10ms消抖处理。3. 软件核心实现图形引擎、游戏循环与音效3.1 显示驱动SPI刷屏与帧缓冲取舍软件部分是我花时间最多的地方也是这个项目的灵魂。显示驱动是最底层的一环。PIC32MX270的64KB RAM说多不多说少也真不少。如果做全屏帧缓冲320x240x2字节RGB565 150KB完全放不下。所以不能做整帧缓冲必须采用“局部重绘 行缓冲”策略。思路是游戏画面里只有少数区域发生变化比如俄罗斯方块新方块落下、消行、分数变化所以代码只需要在变化发生的位置把那一小块区域的像素数据更新到屏幕即可。实现上我在RAM里维护一个160x120的逻辑画面缓存用RGB565存储大约38KB可用每次逻辑更新后计算发生变化的最小矩形区域只把该区域的像素推送到SPI屏幕。这样做的效果很好实测俄罗斯方块每帧重绘的区域通常只有几个小矩形SPI发送量很小会非常流畅。如果用整帧重绘SPI会成为瓶颈帧率会降到个位数体验很差。下面是一段简化的SPI写像素函数使用PIC32的SPI外设库#include xc.h // SPI1发送一个字节 void spi1_send_byte(unsigned char data) { while (SPI1STATbits.SPITBF); // 等待发送缓冲空 SPI1BUF data; // 写入数据 } // 写ILI9341命令D/C引脚拉低 void lcd_write_cmd(unsigned char cmd) { LCD_DC_LAT 0; // D/C低电平表示命令 LCD_CS_LAT 0; // 片选拉低 spi1_send_byte(cmd); LCD_CS_LAT 1; } // 写ILI9341数据D/C引脚拉高 void lcd_write_data(unsigned char data) { LCD_DC_LAT 1; // D/C高电平表示数据 LCD_CS_LAT 0; spi1_send_byte(data); LCD_CS_LAT 1; } // 填充一块矩形区域为纯色比如消除行之前的闪烁效果 void lcd_fill_rect(int x, int y, int w, int h, unsigned short color) { lcd_write_cmd(0x2A); // 设置列地址 lcd_write_data(x 8); lcd_write_data(x 0xFF); lcd_write_data((x w - 1) 8); lcd_write_data((x w - 1) 0xFF); lcd_write_cmd(0x2B); // 设置行地址 lcd_write_data(y 8); lcd_write_data(y 0xFF); lcd_write_data((y h - 1) 8); lcd_write_data((y h - 1) 0xFF); lcd_write_cmd(0x2C); // 写显存 for (int i 0; i w * h; i) { lcd_write_data(color 8); lcd_write_data(color 0xFF); } }这段代码其实就是ILI9341的标准操作流程不算难但有几个容易错的地方需要特别注意写命令和写数据之前一定要把D/C引脚电平设置对。如果顺序反了屏幕显示会错乱而且这个错误在调试时特别难发现因为大部分像素内容看起来只是“乱码”或“颜色不对”不好定位。片选信号在每个命令/数据交互期间必须保持低电平不能在整个矩形绘制过程中一直拉低否则其他外设比如后续要加的SD卡无法正常操作。地址窗口0x2A/0x2B设置非常重要每次填充区域前都设置一次。如果忘记重置地址窗口屏幕上会出现残留内容被重复覆盖的现象。3.2 图形绘制像素、精灵贴图与文字渲染有了显示驱动之后要做的是图形绘制库。我定义的绘制接口包括绘制像素点、绘制矩形、绘制精灵图小图标、绘制文字。精灵图是游戏里最常用的元素。比如俄罗斯方块的7种方块、贪吃蛇的身体、打砖块的砖块和挡板这些都预先做成8x8或者16x16像素的位图数组以二进制数组形式直接放在Flash中// 一个8x8的“方块”精灵示例每个字节代表一行像素 // 1表示显示颜色0表示透明 const unsigned char TETROMINO_I[8] { 0b00000000, 0b00111100, 0b00111100, 0b00111100, 0b00111100, 0b00000000, 0b00000000, 0b00000000 };绘制精灵时函数会把每个非0的像素位置用当前颜色填充。为了让游戏画面好看我在每个“实心”方块边缘额外绘制了一圈浅色高亮和深色阴影让方块看起来有立体感。基础精灵绘制代码如下void draw_sprite(int x, int y, const unsigned char *sprite, int w, int h, unsigned short color) { for (int row 0; row h; row) { unsigned char current_row sprite[row]; for (int col 0; col w; col) { if (current_row (1 (7 - col))) { // 判断该像素是否有效 draw_pixel(x col, y row, color); } } } }文字渲染我用了简单的方式内置一个4x6大小的ASCII字库每个字符用一个16位整数数组表示行信息字体不大但足够显示游戏标题和分数。如果想显示更丰富的菜单界面可以后面再做一个更大字库。3.3 输入扫描与去抖按键输入如果不做处理会面临两个问题抖动和粘连。抖动是因为机械按键在按下和释放的瞬间触点会快速通断几次时间在5-20ms左右。如果不处理可能导致一次按下被识别成多次触发游戏里的“下落一格”就会变成“连锁下落两三格”。我用的去抖方法是在主循环里每隔10ms采样一次按键状态连续两次采样结果一致才确认状态变化// 简单的10ms去抖实现 unsigned char keys_debounced(void) { unsigned char raw read_buttons(); // 读取GPIO原始状态 static unsigned char last_raw 0; static unsigned char result 0; if (raw last_raw) { result raw; // 连续两次一致确认稳定 } last_raw raw; return result; }摇杆的ADC读取也有技巧。ADC采到的电位器值一般是0-1023直接使用时摇杆在中间位置会有300-700的抖动范围容易导致游戏画面漂移。解决办法是设置一个“死区”窗口比如中间值512附近假定±80内都视为“回中”if (adc_value 432) // 小于432判定为“左/上” joystick_dir -1; else if (adc_value 592) // 大于592判定为“右/下” joystick_dir 1; else joystick_dir 0; // 在死区内视为中位3.4 游戏循环固定时间步长维持60FPS游戏循环的设计是整个软件架构里最重要的一环。很多新手会直接在while(1)里写完逻辑、刷完屏、再循环但这样会导致游戏速度依赖于CPU占用率。如果渲染部分耗时不稳定就会出现“有时候快、有时候慢”的体验。我采用的是固定时间步长结构用一个定时器产生1ms中断主循环里根据一个“累积时间”变量决定何时推进逻辑帧。目标帧率设为30FPS两帧间隔约33ms实测对俄罗斯方块、贪吃蛇这类游戏已经足够流畅。这样游戏逻辑更新和渲染节奏完全分离帧率就不会受渲染复杂度影响unsigned int last_time 0; unsigned int accumulator 0; const unsigned int frame_time_ms 33; // 30FPS while (1) { unsigned int now get_tick_ms(); // 从定时器获取当前毫秒时间 accumulator (now - last_time); last_time now; // 只有当累积时间超过一帧间隔时才推进逻辑 while (accumulator frame_time_ms) { game_logic_update(); // 更新游戏状态 render_game(); // 渲染画面只重绘变化区域 accumulator - frame_time_ms; } }这种循环方式有很多好处逻辑更新发生在固定的时间点误差很小如果某一帧渲染耗时较长也只是下一帧稍微延后不会造成游戏速度不均匀。3.5 音效合成与触发音乐这块我用PIC32的OC模块来产生PWM方波。实现方式不复杂在定时器中断里根据当前音效的“频率表”改变PWM周期寄存器值。比如“翻转方块”音效我会设置一个持续80ms的短音800Hz音量通过占空比调小一点约30%占空比而不是50%听起来会更柔和。音效触发写在游戏逻辑里比如void play_sound(unsigned short frequency_hz, unsigned int duration_ms) { sound_freq frequency_hz; sound_duration duration_ms; sound_remaining duration_ms; }在游戏更新函数里每次检测到得分、翻转、碰撞等事件就调用 play_sound() 设置对应的频率和时长。定时器中断会逐ms递减音效剩余时间时间到就关闭PWM输出。这里有个实践心得音效不要做得太“尖”。高频方波音效如果持续时间超过200ms听久了会很难受。我试过把得分音设成1200Hz持续300ms体验下来很吵后来改成800Hz持续80ms感觉好了很多。4. 开发环境与固件配置4.1 工具链搭建MPLAB X IDE、XC32与MCC开发环境是进入PIC32世界的第一道门槛也是很多新手卡住最多的地方。当前Microchip主推的是MPLAB X IDE配合XC32编译器。MPLAB X是基于NetBeans的IDE界面比老旧的MPLAB 8要现代化很多。安装过程相对顺利但有几个坑需要提前知道XC32编译器虽然可以免费使用但默认是优化较弱的免费版。破解版或付费版可以开启更高优化等级。对于做游戏这种对性能有一定要求的项目我建议至少开启-O1优化实测代码体积会减少30%以上执行速度也有提升。在MPLAB X里创建一个PIC32MX270F256B的工程时需要手动选择器件型号并在“Project Properties”里确认编译器版本和PICkit型号匹配否则烧录时报错是大概率事件。MCCMPLAB Code Configurator强烈推荐安装。它是一个图形化配置工具可以自动生成时钟树、引脚复用、外设初始化代码。对于新手这能省去大量翻寄存器手册的时间就算是我这种习惯手写寄存器的也会用MCC快速核对引脚复用配置。4.2 工程关键配置时钟、PLL与配置位时钟树配置是整个PIC32项目能否跑起来的最关键环节。PIC32MX270默认使用内部快速RCFRC 8MHz但如果要跑SPI屏幕和游戏逻辑8MHz远远不够必须开启PLL倍频到80MHz。我最终的时钟配置是参数值主时钟来源外部8MHz晶振因为内部振荡器精度不够会影响UART通信PLL输入分频28 / 2 4MHzPLL倍频204 * 20 80MHzPLL输出分频280 / 2 40MHzPB时钟外设总线40MHz主时钟/2系统主时钟80MHz这个链路逻辑一定要理清楚外设总线和CPU主时钟频率不一样定时器、SPI、UART这些外设用的是PB时钟而CPU计算用的是SYSCLK。如果外设时钟配置和初始化不匹配就会出现数据错乱、时序不对这类诡异问题。配置位Configuration Bits也是必踩的坑。它们相当于芯片上电时的硬件“启动设置”在代码里用#pragma config指令写入Flash的配置区域。我用的关键配置位如下#pragma config FNOSC PRIPLL // 主振荡器PLL #pragma config FPLLIDIV DIV_2 // PLL输入分频2 #pragma config FPLLMUL MUL_20 // PLL倍频20 #pragma config FPLLODIV DIV_2 // PLL输出分频2 #pragma config FPBDIV DIV_2 // 外设时钟系统时钟/2 #pragma config FWDTEN OFF // 关闭看门狗非常重要 #pragma config ICESEL ICS_PGx1 // 调试接口选择 #pragma config BOREN OFF // 关闭掉电复位可减少意外重启注意FWDTEN OFF这一项新手特别容易漏掉。如果看门狗意外被使能程序会在正常运行几百ms后不断复位表现症状非常像“程序跑飞了”排查起来非常耗时间。4.3 烧录与调试经验PICkit、逻辑分析仪调试工具我用的是PICkit 4配合MPLAB X自带的调试器可以单步执行、设置断点、观察寄存器和内存值。PICkit 4烧录速度很快固件也就100KB左右几秒钟就完成。有一个很实用的调试技巧如果遇到显示错乱或者画面残留先别急着改代码用逻辑分析仪抓一下SPI的SCLK、MOSI和CS信号看看时序是否符合ILI9341的规格。我在调试过程中遇到过一个问题SPI极性CPOL和相位CPHA配置错了一位导致屏幕颜色完全不对。单看代码很难发现用逻辑分析仪一抓立刻就能看出数据位和时钟沿对不上。另一个调试建议是写一个小型的“诊断模式”固件。这个固件不做游戏逻辑只做三件事点一个常亮LED、把一个固定颜色循环渐变的图案刷到屏幕、反复播放一段音阶。这样在硬件接线出错时可以快速区分是硬件还是软件的问题减少排查范围。5. 整机组装与问题排查5.1 外壳与面板布局硬件调试通过之后就是整机组装。一个好的外壳能让项目体验提升一个档次。我用的方案是3D打印外壳但实际上用木板和亚克力也能做。面板布局我最后定的是中间一块2.2寸屏幕屏幕下方是6个操作按钮左右两侧各留一个摇杆位置。其中最需要注意的尺寸参数组件开孔直径/尺寸说明2.2寸TFT屏幕40mm x 55mm长方形开孔稍微留1mm余量做边框标准街机按键30mm常用的26mm/30mm规格我用的30mm小型模拟摇杆28mm测量的是摇杆底座直径小型喇叭62mm开孔要留螺丝孔位用喇叭网罩电源开关12mm6脚船型开关如果使用3D打印打印时要注意底座厚度至少留2mm厚度用于螺丝孔和卡槽强度。如果使用木板建议用5mm厚的多层板硬度和手感都会好一些。5.2 供电与地线设计供电是整个项目里最容易出问题但最容易被忽略的部分。我的系统里PIC32需要3.3V屏幕也用3.3V供电按键LED和喇叭需要5V所以必须把电压分离设计。我的供电拓扑是USB 5V → 开关电源转换器DCDC降压到3.3V→ 主控和屏幕USB 5V直连按键LED和喇叭。这里的核心要求是3.3V和5V的地线必须共地否则GPIO信号会产生电位差导致逻辑电平识别混乱。在实际布线中我发现一个规律数字地线GPIO、SPI信号回流和功率地线LED、喇叭在靠近主控的地端需要单点连接避免大电流在地线上产生噪声干扰主控的ADC采样。最初我把所有地线都直接连在一起摇杆ADC值总是有±20左右的毛刺后来把功率地单独走一段到USB地端噪声明显下降。5.3 常见问题速查表整个项目做下来我遇到过不少典型的坑。这里整理成一张速查表按照症状、可能原因、解决方法三列来呈现症状可能原因解决方法上电后白屏屏幕复位延迟不够初始化代码里先延时50ms以上再发初始化命令画面颜色异常或颜色错乱SPI极性/相位配置错误用逻辑分析仪抓SPI波形与ILI9341数据手册比对时钟极性按键识别错位或按下没反应GPIO上拉未使能或共地不良检查按键到GND的连线确认内部上拉已开启摇杆读取值抖动剧烈地线噪声或ADC采样时钟过低功率地和数字地分开走确保ADC采样时钟在1MHz以上系统每隔几百毫秒自动重启看门狗未关闭或电压不稳检查配置位FWDTEN OFF测量3.3V电源纹波音效有“咔咔”声或刺耳噪声PWM缺少低通滤波加100Ω串联电阻10uF并联电容或外接功放模块游戏速度不固定时而快时而慢游戏循环没有用固定时间步长改成定时器毫秒计数累积时间判断逻辑帧高分记录丢失Flash写入失败或没有擦除扇区PIC32 Flash必须先擦后写且需要按扇区4KB对齐最后再分享一点自己实际操作中的体会。用PIC32做街机最大的收获其实不是那三款游戏本身而是对整个MCU系统的掌控感。当你把时钟树、外设、中断、帧循环这些东西都真正调通之后再看其他单片机项目会轻松很多。如果你也想做建议从俄罗斯方块开始它的逻辑简单、状态清晰很适合用来练习局部重绘和输入扫描。等这一套流程走通再去加贪吃蛇和打砖块改动成本会非常低。希望这篇记录能帮你少走一些我没绕过去的弯路。