基于STM32的复古打地鼠掌机:从硬件设计到游戏逻辑的嵌入式开发实践

发布时间:2026/9/3 2:12:07
基于STM32的复古打地鼠掌机:从硬件设计到游戏逻辑的嵌入式开发实践 简介这是一套面向嵌入式初学者与毕业设计学生的STM32实战项目资源聚焦趣味性与工程性结合的打地鼠掌机开发覆盖硬件驱动、人机交互与音视频反馈全流程。资源包含完整软硬件资料103个文件以42个.h头文件和36个.c源码为主涵盖TIM、RCC、USART、I2C、ADC等标准外设驱动及OLED显示、蜂鸣器音乐控制逻辑辅以原理图.schdoc、PCB设计.pcbdoc、Keil工程.uvprojx及可直接烧录的.hex固件包体仅4.61MB结构清晰、模块解耦便于理解与二次开发。已有225人学习下载适用于课程设计、毕设选题、电子竞赛立项及单片机入门练手。用户获取后无需PCB制板按引脚定义用面包板杜邦线OLED/蜂鸣器模块即可快速复现所有代码经实测运行稳定并附详细说明文档与开发支持助力高效上手与功能扩展。1. 项目缘起从零到一打造一台属于自己的复古掌机几年前我在整理旧物时翻出了一台早已报废的Game Boy看着它斑驳的外壳一个念头突然冒了出来能不能用现在手边最普及的微控制器复刻那种简单纯粹的掌上游戏乐趣不是模拟器而是从硬件电路到游戏逻辑完完全全自己实现一台。STM32系列MCU以其强大的性能、丰富的外设和极高的性价比成为了我的首选。而“打地鼠”这个游戏规则简单直观交互反馈明确非常适合作为嵌入式开发的入门综合项目既能玩转GPIO、定时器、中断又能接触到显示、音频等更高级的外设驱动。于是这个“基于STM32设计的打地鼠掌机”项目就诞生了。它不仅仅是一个玩具更是一个完整的嵌入式系统开发实践。项目涵盖了从硬件选型、电路设计、PCB绘制到软件层面的外设驱动、游戏逻辑、状态机设计再到最后的整机调试与优化。最终实现的功能包括一个完整的打地鼠游戏带有随机出现的地鼠、得分与生命值系统一块0.96寸的OLED屏幕用于实时显示游戏状态、分数和菜单一个蜂鸣器用于播放简单的背景音乐和游戏音效如击中、错过、游戏结束。本文将毫无保留地分享整个项目的软硬件设计细节、开发过程中的关键决策以及那些只有亲手做过才会知道的“坑”和技巧。2. 硬件架构设计与核心元器件选型一台掌机的硬件是它的骨骼。我的设计原则是在满足功能的前提下力求简洁、可靠、易于制作。整个系统的核心是一块STM32F103C8T6也就是常说的“蓝色药丸”核心板。选择它原因很简单72MHz的Cortex-M3内核性能足够驱动游戏逻辑和显示拥有足够的GPIO、定时器和中断资源价格极其低廉且社区资源丰富遇到问题几乎都能找到答案。2.1 主控与电源管理STM32F103C8T6作为主控负责一切计算与调度。其外围电路设计遵循最小系统原则一个8MHz的外部晶振提供系统时钟两个22pF的负载电容一个复位电路10k上拉电阻0.1uF电容到地以及必要的滤波电容。电源部分我采用了一节3.7V的锂电池供电通过一个AMS1117-3.3稳压芯片将电压稳定到3.3V为MCU和所有外设供电。这里有个细节AMS1117的输入和输出端都需要并联一个10uF的钽电容和一个0.1uF的陶瓷电容分别用于储能和高频滤波确保系统供电稳定尤其在蜂鸣器鸣叫这种瞬时电流较大的场景下电压不会出现剧烈波动导致MCU复位。2.2 输入设备按键与“地鼠”游戏输入需要9个按键8个对应8个“地洞”1个作为开始/暂停/确认功能键。为了节省IO口和简化电路我采用了矩阵键盘的设计。将8个游戏按键排成4行2列功能键单独占用一个IO口设置为上拉输入按键接地。矩阵键盘扫描使用经典的“行扫描列读取”法利用STM32的GPIO推挽输出和上拉输入模式配合定时器中断进行周期性的扫描既能可靠检测按键又不会过多占用CPU时间。“地鼠”本身是用LED来模拟的。在每个“地洞”位置安装一颗普通的3mm草帽LED。LED的阴极接地阳极通过一个220欧姆的限流电阻连接到STM32的GPIO口。当某个地洞需要“冒出地鼠”时程序就控制对应的GPIO输出高电平点亮LED。限流电阻的阻值需要计算假设LED正向压降约为2VGPIO高电平为3.3V则电阻需要分担1.3V的电压。为了让LED亮度适中且不超过GPIO口的最大驱动电流通常25mA选择220欧姆此时电流约为(3.3V-2V)/220Ω ≈ 5.9mA非常安全。2.3 输出设备OLED显示与蜂鸣器显示部分选择的是0.96寸的OLED屏幕驱动芯片为SSD1306分辨率128x64I2C接口。选择I2C版本而非SPI主要是为了节省IO口仅需2根线SCL和SDA虽然刷新速度稍慢但对于打地鼠这种刷新率要求不高的游戏完全足够。屏幕的供电同样是3.3V。在PCB布局时需要将I2C的上拉电阻通常4.7kΩ放置得离屏幕接口近一些以保证信号完整性。音频部分采用了一个无源蜂鸣器。这里有关键区别有源蜂鸣器内部自带振荡电路给电就响只能发出固定频率的声音而无源蜂鸣器内部没有振荡源需要外部输入PWM波才能发声可以通过改变PWM频率来播放不同音调的音乐。为了实现背景音乐必须选择无源蜂鸣器。我选用的是5V驱动的无源蜂鸣器但STM32的GPIO是3.3V电平直接驱动可能音量较小。因此我增加了一个简单的三极管驱动电路STM32的PWM输出引脚连接一个1kΩ的电阻到NPN三极管如S8050的基极蜂鸣器接在集电极和5V电源之间发射极接地。当PWM波为高电平时三极管导通蜂鸣器通电低电平时关闭。这样就用3.3V的IO口控制了一个5V的负载。5V电源可以从AMS1117的输入端锂电池电压经过一个二极管降压后取得大约4.3V也足够驱动蜂鸣器。3. 软件框架与核心驱动实现软件部分采用裸机开发基于标准外设库Standard Peripheral Library。整个程序由一个主循环和若干中断服务例程构成采用前后台超级循环系统。3.1 系统时钟与定时器调度首先初始化系统时钟将频率设置为72MHz。游戏的核心节奏由两个定时器控制SysTick定时器用于提供毫秒级的系统延时函数delay_ms()这个函数在驱动初始化、按键消抖等地方会用到。TIM2定时器配置为1ms中断一次。这个中断是系统的“心跳”所有基于时间的任务都在这里处理例如游戏逻辑计时地鼠出现持续时间、游戏总时长。矩阵键盘扫描每10ms扫描一次兼顾响应速度和防抖。OLED屏幕刷新计时可以设定每50ms刷新一帧避免刷新过快导致I2C总线拥堵。蜂鸣器音乐播放的节拍控制。这种集中式的定时调度避免了在代码中到处使用delay函数导致CPU阻塞让系统能够“同时”处理多项任务。3.2 OLED显示驱动与图形界面SSD1306的驱动代码需要自己编写或移植。核心是实现几个基本函数OLED_Init()初始化、OLED_WR_Byte()写数据/命令、OLED_Set_Pos()设置光标、OLED_Clear()清屏。在此基础上实现一个OLED_ShowChar()和OLED_ShowString()函数用于显示文字。为了显示更丰富的游戏界面我定义了一个128x64比特的显存数组OLED_GRAM[8][128]因为SSD1306是页寻址每页8行像素共8页。所有绘图操作画点、画线、显示字符都先修改这个数组然后通过OLED_Refresh()函数将整个数组通过I2C一次性刷到屏幕上。这种方式避免了频繁操作I2C提高了效率。游戏界面被设计为几个“页面”主页菜单显示游戏标题、开始游戏、设置等选项。游戏主界面中央是8个地洞的布局用方框或圆圈表示地鼠出现时对应位置的LED亮起同时屏幕上该位置也显示一个地鼠图案。顶部显示当前分数Score和剩余生命Life底部显示时间或关卡。结束界面显示本次游戏得分、历史最高分并提供重新开始选项。界面的切换通过一个状态机State Machine来控制状态包括MENU_STATE,PLAYING_STATE,GAME_OVER_STATE等。状态机保证了程序逻辑清晰易于扩展。3.3 蜂鸣器音乐播放引擎这是项目的趣味点之一。让无源蜂鸣器播放音乐本质就是按照乐谱的节拍和音调动态改变输出PWM波的频率和开关时长。首先需要定义音调频率。我将蜂鸣器连接的定时器如TIM3配置为PWM输出模式并预分频使得计数周期可以灵活调整。然后定义一个音调-频率对照表// 定义中音C调各音符频率 (Hz) #define NOTE_DO 523 #define NOTE_RE 587 #define NOTE_MI 659 #define NOTE_FA 698 #define NOTE_SO 784 #define NOTE_LA 880 #define NOTE_SI 988 // 高音Do #define NOTE_DO_H 1047一首音乐可以表示为一个二维数组每一行包含两个元素音调对应上面的宏定义和持续时间以系统节拍为单位比如1拍200ms。const uint16_t game_bgm[][2] { {NOTE_DO, 2}, {NOTE_RE, 2}, {NOTE_MI, 2}, {NOTE_FA, 2}, {NOTE_SO, 4}, {NOTE_SO, 4}, // ... 更多音符 {0, 2} // 0表示休止符 };音乐播放引擎在TIM2的1ms中断里工作。它维护一个当前音符索引和持续时间计数器。每进入一次中断计数器减1。当计数器减到0时就切换到下一个音符如果音调不为0则根据音调值重新计算并设置TIM3的自动重装载值ARR以改变PWM频率从而发出该音调如果音调为0则关闭PWM输出静音。同时重置持续时间计数器。通过这种方式就可以播放简单的《小星星》、《欢乐颂》等旋律作为背景音乐。游戏音效如击中声“叮”错过声“噗”也可以用短促的音符序列来实现。注意蜂鸣器音乐播放会占用一个定时器用于PWM生成并且中断服务程序中的计算要尽可能快避免影响其他定时任务。如果音乐比较复杂可以考虑将频率计算涉及除法提前算好在数组中直接存储ARR值而不是频率值。4. 游戏逻辑设计与状态机实现打地鼠的游戏逻辑是项目的核心灵魂。它需要管理地鼠的随机出现、玩家的输入响应、得分计算和游戏难度 progression。4.1 地鼠管理机制我定义了一个结构体数组来管理8个地洞的状态typedef struct { uint8_t is_active; // 当前是否有地鼠1有0无 uint32_t active_ticks; // 地鼠已出现的时间单位系统节拍如10ms uint32_t max_ticks; // 地鼠最大出现时间随难度递减 uint8_t led_pin; // 对应的LED引脚 uint8_t key_row; // 对应的按键矩阵行 uint8_t key_col; // 对应的按键矩阵列 } MoleHole_t; MoleHole_t moles[8];游戏主循环或在一个定时任务中会定期比如每100ms遍历这个数组对于is_active为1的地洞增加其active_ticks。如果active_ticks超过max_ticks则判定玩家未击中地鼠消失is_active0LED熄灭并扣除一条生命。对于is_active为0的地洞根据一个随机算法决定是否生成新的地鼠。这个算法需要考虑游戏难度在简单模式下生成概率低max_ticks长在困难模式下生成概率高max_ticks短。可以使用STM32的硬件随机数发生器RNG或一个伪随机数函数来产生随机性。4.2 输入响应与得分判定在矩阵键盘扫描中断中一旦检测到有按键按下经过消抖确认后就需要遍历moles数组查找是否有地洞的key_row和key_col与按下的键匹配。如果匹配且该地洞is_active为1恭喜打中了立即将该地洞状态置为无效is_active0LED熄灭播放击中音效并根据地鼠已出现的时间active_ticks给予分数奖励出现时间越短得分越高鼓励快速反应。如果匹配但该地洞is_active为0玩家打空了。可以播放一个搞笑的失败音效并酌情扣一点分或者只是忽略。如果没有匹配按下了功能键则交给功能键处理程序用于开始、暂停游戏或操作菜单。4.3 游戏难度与进程游戏难度可以设计为随时间或分数递增。例如每过30秒或每获得100分游戏进入下一关。关卡提升表现为地鼠生成的概率spawn_probability增加。地鼠最大出现时间max_ticks减少。可以同时出现的地鼠数量上限max_concurrent增加。这些参数的变化会让游戏节奏越来越快挑战性越来越大。游戏进程关卡、分数、生命值需要实时显示在OLED屏幕上。5. 系统整合、调试与深度优化当所有模块的代码都准备好后真正的挑战才刚刚开始把它们整合成一个稳定、流畅运行的系统。5.1 中断优先级与资源冲突管理系统中存在多个中断源SysTick、TIM2系统心跳、TIM3PWM输出、可能还有外部中断用于按键。必须合理配置它们的抢占优先级和子优先级。SysTick通常设置为最低优先级因为它只用于提供延时基准不涉及关键操作。TIM21ms系统心跳这是调度核心优先级应设为较高确保定时准确。但其中的任务执行要快不能有长时间操作如复杂的图形计算。矩阵键盘扫描如果放在TIM2中断里扫描算法必须高效。我采用的方法是每次中断只扫描一行4行轮流扫描这样每4ms才能完整扫描一次矩阵但大大减少了单次中断的执行时间。I2C通信OLED驱动使用轮询方式等待I2C事件标志这些等待循环会阻塞CPU。绝对不能在中断服务程序中进行I2C通信。我的做法是在TIM2中断里设置一个oled_refresh_flag标志然后在主循环中检查这个标志如果置位则调用OLED_Refresh()函数进行刷屏。这样就把耗时的I2C操作移出了中断。5.2 功耗与性能平衡虽然掌机一直插着电池但优化功耗仍是一个好习惯。在软件上可以做的包括睡眠模式在游戏菜单界面等待时如果没有按键输入可以让STM32进入睡眠Sleep模式。任何按键中断都可以将其唤醒。这需要将按键配置为外部中断唤醒源。动态频率调整在游戏进行时全速运行72MHz。在菜单或某些静态显示界面可以适当降低系统时钟频率以节省功耗。外设管理不使用时关闭外设时钟。例如在播放完一段音乐后可以关闭TIM3的时钟。5.3 那些“踩过坑”才得到的经验OLED显示残影/乱码初期调试时屏幕经常出现奇怪的字符或残影。原因是OLED_GRAM显存数组在初始化时没有全部清零或者OLED_Refresh()函数在传输数据时起始地址和传输长度设置错误没有覆盖整个屏幕区域。解决方法是确保清屏函数被正确调用并仔细核对I2C传输的从机地址、控制字节和数据序列。蜂鸣器音乐卡顿或跑调如果音乐播放不流畅首先检查TIM2中断系统心跳的优先级是否被其他更高优先级的中断频繁打断。其次检查音乐数据数组的索引和计时逻辑是否正确确保在切换音符时PWM频率的设置和关闭/开启操作是原子的不会被中断。另外计算音调频率对应的ARR值时注意定时器时钟分频后的计数频率公式为ARR (TimClock / (PWM_Freq * Prescaler)) - 1。计算错误会导致音调不准。按键连击或失灵矩阵键盘的消抖算法很关键。单纯的延时消抖在中断里不合适。我采用的方法是在扫描到按键按下后记录该键值并启动一个“确认计数器”。在接下来的几次扫描中比如连续3次每次间隔10ms如果都检测到同一个键被按下才确认为一次有效按键。释放判断同理。这样可以有效滤除抖动。游戏逻辑“卡死”检查主循环和中断中的while循环。确保没有任何地方存在“死等”某个条件的情况。所有等待都应使用超时机制或者通过状态标志在循环中判断。例如等待I2C事件标志位时应该增加一个超时计数器超过一定时间后跳出循环并报告错误而不是无限等待。PCB布局的教训第一版PCB打样回来蜂鸣器声音特别小。排查后发现驱动三极管基极的限流电阻我用成了10kΩ导致基极电流太小三极管没有完全饱和导通蜂鸣器上的电压不足。换成1kΩ后问题解决。另外电源走线要尽量粗特别是给蜂鸣器供电的5V线路避免大电流时产生压降。6. 项目总结与扩展思考完成这个打地鼠掌机项目是一次非常扎实的STM32全栈开发体验。它强迫你从宏观的系统架构思考到微观的寄存器配置从硬件的电流电压计算到软件的状态机设计每一个环节都紧密相连。这个项目本身还有很大的扩展空间增加更多游戏同样的硬件平台可以很容易地移植“贪吃蛇”、“俄罗斯方块”等经典游戏。只需要修改游戏逻辑和显示界面即可。加入振动马达在击中地鼠时让掌机产生短促振动体验更佳。只需要一个GPIO口驱动一个微型振动马达即可。实现分数存储利用STM32内部的Flash模拟EEPROM存储历史最高分即使断电也不会丢失。无线对战如果增加一个蓝牙模块如HC-05就可以实现两台掌机之间的联机对战看谁打地鼠更快。外壳与美化使用3D打印为掌机制作一个漂亮的外壳并设计个性化的贴纸让它从一块开发板变成一个真正的消费级产品。回过头看嵌入式开发的乐趣就在于这种从无到有的创造过程以及解决一个个具体问题后带来的成就感。这个项目用到的GPIO、中断、定时器、PWM、I2C通信、状态机等知识是嵌入式开发的通用基础。掌握了它们你就具备了去挑战更复杂项目如智能小车、四轴飞行器、物联网设备的底气。希望这份详细的分享能为你点亮自己动手制作的道路。本文还有配套的精品资源点击获取