STM32贪吃蛇项目库函数版:从外设配置到游戏逻辑全拆解

STM32贪吃蛇项目库函数版:从外设配置到游戏逻辑全拆解 简介基于STM32微控制器的贪吃蛇游戏完整工程采用库函数开发方式适合嵌入式初学者与电子设计爱好者对照学习。工程覆盖从底层启动、GPIO/定时器配置到OLED绘图和游戏逻辑的完整链路围绕经典贪吃蛇玩法实现方向控制、食物生成、碰撞检测与分数刷新。压缩包共112个文件主要包含C/H源代码、OLED驱动、启动文件、Keil工程配置以及编译生成的hex/axf等文件总大小仅1.75MB结构清晰便于直接查看和二次开发。该工程已有647人学习代码中涉及HAL/LL库外设操作、中断服务、按键扫描、数据存储和界面刷新等技术点尤其对OLED显示驱动进行了功能拓展有助于快速掌握嵌入式游戏项目的搭建、调试和优化思路。 打开这个zip包之后先别急着往Keil里拖。我就是那种拿到别人的工程先跑一遍再说的人后来发现如果不懂背后的外设配置和游戏逻辑跑通了也只会改个速度和颜色遇到按键失灵、蛇乱跳、屏闪这些问题照样抓瞎。这篇博文我就以“STM32贪吃蛇项目-库函数版本”为例把整个项目的设计思路、核心模块、工程搭建和排坑记录完整拆一遍希望能帮你把这个经典小项目真正吃透。先说清楚这个项目是什么。它就是一个跑在STM32上的贪吃蛇游戏显示用TFT LCD或者OLED屏按键控制方向蛇吃到食物变长撞墙撞自己游戏结束。放到平时学的点灯、按键、中断、定时器这些单点知识来看贪吃蛇最大的价值在于它把GPIO、外部中断、定时器中断、显示驱动、状态机、数据结构全部串起来了是一个综合性很强的练手项目。库函数版本则意味着代码基于STM32标准外设库Standard Peripheral LibraryV3.5写的不是HAL库也不是纯寄存器操作。这个版本非常适合正在学习标准库、想理解外设底层机制的朋友不管是刚入门的大三学生还是自学转嵌入式的工程师都能从里面捞出不少干货。1. 项目整体设计与思路拆解1.1 为什么贪吃蛇是STM32入门的“综合题”嵌入式学习有一条很经典的路线先点灯再学按键输入然后学定时器、中断、串口每个都是独立的知识点。很多人在这一步就停住了因为单个外设学完没什么成就感也不知道能拿来做什么。贪吃蛇恰好填补了这个空档——它需要你同时操作输入设备按键和输出设备屏幕需要一个稳定的时间基准定时器来驱动游戏节拍还需要一套清晰的数据结构来管理蛇身和食物。写一遍贪吃蛇基本等于把MCU外设应用的所有基础搭建过程都过了一遍。从我个人的经验来说这个项目的难度曲线设计得刚刚好。贪吃蛇的游戏规则全世界都知道不需要额外的领域知识你可以把全部精力放在“怎么用STM32实现”这件事上。相比做一个温湿度计或者智能台灯贪吃蛇没有复杂的传感器协议、没有外部芯片依赖核心逻辑就那几块非常适合作为第一个从设计到实现完全自己掌控的项目。1.2 库函数版本和HAL版本新手到底选哪个现在STM32的教程鱼龙混杂有寄存器版、标准库版、HAL版很多刚入门的同学就卡在选型上。这个zip包的版本是库函数版本也就是标准外设库我个人觉得这个选择很合理尤其是对初学者来说。说句实在话HAL库配合CubeMX确实能快速生成工程但很多用HAL库的同学写着写着就开始“调包”对定时器到底怎么配置的、GPIO的AF映射怎么回事一无所知出了问题只能在论坛里求救。标准库不同它是把寄存器操作包装成函数比如GPIO_Init()、TIM_Cmd()这类API你能看到函数内部最终操作了哪些寄存器数据手册上的概念在这儿还一一对应得上。等你把标准库吃透了再去看HAL库的抽象逻辑半小时就能上手反过来从HAL库转标准库反而会觉得很多东西不知从何下手。当然标准库也有它的局限。ST官方早就停止更新标准库了像F4、H7系列已经不再提供标准外设库新产品开发确实更应该用HAL库。但作为学习硬件原理、阅读理解芯片手册的途径库函数版本依然是很好的选择。这个贪吃蛇项目用它来写代码量不大性价比很高。1.3 硬件资源分配与功能拆解拿一个典型的STM32F103C8T6最小系统板来说实现贪吃蛇需要的资源非常清晰外设功能示例引脚GPIO输入四个方向按键PB0-PB3上拉输入SPI1驱动TFT LCD屏PA5SCK, PA7MOSI, PA3CS, PA2DC, PA1RST, PA0BLTIM2产生1ms时基累计游戏节拍内部时钟无需引脚USART1调试打印蛇头坐标和分数PA9TX, PA10RXSysTick按键去抖延时内部如果你的屏幕是0.96寸OLEDSSD1306那协议会变成I2C或者SPI引脚完全不同。所以我拿到别人的zip包第一件事就是看清他用的什么屏、什么引脚再决定要不要改硬件连接。这个项目的核心外设无非三类按键、显示、定时所有代码都是围绕这三个点在转。2. 核心模块实现与库函数实操要点2.1 按键输入从硬件电路到软件去抖贪吃蛇的方向控制看着简单实际上按键处理是整个项目里最容易出隐蔽bug的地方。常见的接法是按键一端接GPIO另一端接GNDGPIO配置为上拉输入按下时读到低电平。标准库里的配置代码长这样GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; // 上拉输入 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure);这里有个细节按键扫描不能只读一次电平就做判断。机械按键按下和松开的瞬间会有抖动电平会在0和1之间跳变好几次如果不去抖按一下方向键蛇可能连续走两格或者直接触发两个方向。最简单的处理是配合SysTick延时去抖检测到电平变化后延时10ms再读一次确实为低电平才算数。在主循环里用轮询方式扫描按键对贪吃蛇这个场景完全够用。从我的实际测试来看更稳的做法是把按键扫描放进一个“节拍任务”里每10ms执行一次这样能避免低优先级的主循环被LCD绘制任务拖慢时按键响应变得很迟钝。2.2 定时器节拍游戏主循环的心脏贪吃蛇不是“越快越好”它需要一个稳定的时间节拍来控制蛇身移动速度。如果只靠delay_ms()硬等主循环被阻塞按键扫描和屏幕刷新都会受影响整个游戏会变得一卡一卡的。标准做法是用一个定时器产生固定时基然后在主循环里查询标志位。void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); tick_count; } }假设TIM2配置为1ms中断一次我们在一个变量tick_count里累加。每次主循环检查到200个tick就把game_tick标志置1然后蛇移动一格。游戏加速本质上就是改变这个阈值比如从200ms缩短到150ms、100ms。这个设计的好处是游戏逻辑和硬件计时完全解耦你想暂停游戏只需要在暂停状态下不处理game_tick标志即可时间基准始终不会丢。2.3 LCD渲染画面是如何画出来的TFT屏显示贪吃蛇的基本原理就是“画方块格子”。地图可以划分为16x16或者20x20的网格每个格子对应屏幕上的一块区域比如8x8像素。蛇身就是一连串实心蓝色方块食物是一个红色方块背景是黑色或白色。库函数版本驱动这类SPI屏时LCD_Fill()这类函数是后续所有绘图的基石。我拆解这个项目时发现最影响体验的一点是刷新策略。很多人直接在每次移动后对整个屏幕执行LCD_Clear()再全量重画结果就是屏幕疯狂闪烁因为SPI刷一帧全屏要好几毫秒再加上清屏和重绘的时间游戏看起来就是一闪一闪的。正确的做法是只重绘发生变化的位置旧蛇尾格子擦成背景色新蛇头格子画成蛇身色吃到食物后再把食物格画出来。这样每帧实际只需操作两个格子屏幕显示稳定流畅代码也没有复杂多少。这一条建议无论你用什么屏幕、什么驱动芯片都适用。2.4 蛇的移动逻辑与碰撞检测蛇的移动是整个项目的数据结构核心。我在看这个zip包源码的时候发现它的做法很标准用结构体数组保存蛇身坐标数组下标从0到snake_len-1下标0是蛇头。typedef struct { uint8_t x; uint8_t y; } SnakeNode; SnakeNode snake[SNAKE_MAX_LEN]; uint8_t snake_len; uint8_t dir; // 0上 1下 2左 3右 uint8_t game_state;蛇移动的关键技巧是“从尾到头覆盖”把snake[snake_len-1]的位置依次赋给前一个也就是尾巴跟上身体最后根据方向计算新蛇头位置。如果这一步没吃到食物就正常前移数组长度不变如果吃到了食物就直接把新蛇头放在数组末尾位置长度加1相当于蛇没有丢掉尾巴身体就变长了。整个过程不需要复杂的链表操作数组加下标控制就够用。碰撞检测分两块撞墙和撞自己。撞墙简单判断新蛇头的x或y坐标是否超出地图边界。撞自己在逻辑上要注意一个细节蛇头不能和去掉尾巴后的身体比较而要和新位置更新后的身体比较。我曾经因为顺序搞反导致蛇贴着尾巴走的时候被误判为撞到自己明明正常行走却游戏结束当时排查了很久才意识到是这个顺序问题。3. 从零搭建工程完整实操流程3.1 搭建标准库工程模板拿到zip包后如果你也想自己从头搭一个可以按下面的套路来。从ST官网或者各类资料站下载STM32F10x标准外设库V3.5.0解压后只需要关注两个目录Libraries/CMSIS和Libraries/STM32F10x_StdPeriph_Driver。前者是内核启动文件和底层系统初始化后者就是外设驱动库的源码里面按外设拆成了inc和src两个文件夹。在Keil uVision5里新建工程器件选择STM32F103C8然后建议把源文件分组管理分组名内容Usermain.c、stm32f10x_it.c、游戏逻辑文件CMSISstartup_stm32f10x_md.s、system_stm32f10x.cFWLIBstm32f10x_gpio.c、stm32f10x_rcc.c、stm32f10x_tim.c等用到的外设源文件Hardwarelcd.c、key.c、timer.c、uart.c等自己写的硬件驱动工程配置里有几个容易踩的坑。第一C/C选项卡的Define里必须加上STM32F10X_MD否则标准库找不到器件型号的定义。第二宏定义USE_STDPERIPH_DEVICE也不能少。第三勾选“Use MicroLIB”这样printf才能顺利重定向到串口。这三步漏掉任何一个编译都会报一堆看不懂的错误。3.2 核心数据结构与全局变量设计游戏主逻辑建议单独放在game.c里不要和main函数挤在一起。全局变量包含蛇的数组、长度、方向、游戏状态、食物坐标、得分、速度等级可以这样组织#define MAP_W 16 #define MAP_H 16 #define SNAKE_MAX_LEN 200 typedef enum { GAME_STOP, GAME_RUNNING, GAME_PAUSE, GAME_OVER } GameState; SnakeNode snake[SNAKE_MAX_LEN]; uint8_t snake_len; uint8_t dir; uint8_t new_dir; uint8_t food_x, food_y; uint16_t score; uint16_t speed_level; GameState game_state;这里我特地提一下new_dir。按键触发的方向变化先写入new_dir在游戏节拍到来、蛇准备移动之前才把new_dir赋给dir。这么做是为了防止一帧内按多次按键导致蛇往相反方向移动。举个例子蛇正在向右走你在一瞬间先按了上、再按了左如果每次按键都立即改dir最终蛇会向左和自己的身体相撞却躲避不及。通过缓存方向的机制这一帧只接受最后一次合法方向可以明显减少误操作。3.3 游戏状态机与核心代码走读游戏核心可以收敛为一个简单的状态机。主循环建议这样设计while (1) { key_scan(); // 按键扫描 if (game_state GAME_RUNNING game_tick_flag) { game_tick_flag 0; dir new_dir; // 生效本次方向 snake_move(); // 移动 check_eat_food(); // 检查是否吃到食物 check_collision(); // 碰撞检测 render(); // 刷新画面 } }snake_move()的核心实现类似下面这样void snake_move(void) { uint8_t i; SnakeNode next; next.x snake[0].x; next.y snake[0].y; // 从尾到头覆盖尾巴跟上身体 for (i snake_len; i 0; i--) { snake[i] snake[i - 1]; } // 根据方向计算新蛇头 switch (dir) { case DIR_UP: next.y--; break; case DIR_DOWN: next.y; break; case DIR_LEFT: next.x--; break; case DIR_RIGHT: next.x; break; } snake[0] next; // 如果没有吃到食物去掉最后一个节点 // 实际在check_eat里处理或者这里维护一个标志位 }这段代码的优点是逻辑直白方便在串口里打印蛇头坐标来调试。游戏开始时蛇的初始位置不要放在地图正中央而是建议放在左上角附近保证初始化状态时蛇头一直往下或往右移动不容易立刻撞墙。3.4 烧录调试与运行实测工程编译通过后用ST-Link或者J-Link下载程序。我在Keil里用的下载器配置是CMSIS-DAP Debugger勾选Reset and Run这样下载完程序它会自动重启运行省得每次手动按复位键。烧录之后第一件事不是急着玩而是先通过串口看状态。把USART1波特率设为115200程序初始化时打印一行snake game start。手动复位开发板如果串口助手能收到这行字说明时钟、串口、基本外设已经通了后面再调试游戏逻辑就轻松很多。实测下来我用这个顺序排查大多数问题都能在五分钟内定位到具体模块而不是在整份代码里大海捞针。如果你不想用外部晶振可以配置F103使用内部HSI时钟运行修改系统时钟配置即可。这个项目的时序要求不高用内部晶振完全能跑。4. 常见问题与排查技巧实录4.1 按键方向失控、一次移动两格这是贪吃蛇项目里出现频率最高的问题。现象是按一下按键蛇连续走了两步或者偶尔出现快速连续改变方向。原因通常有两个。第一按键去抖没做好抖动被当成多次有效按键。我的解决办法是把按键扫描和去抖放到一个10ms的定时任务里保证同一个“节拍任务”周期内只扫一次按键。第二主循环跑得飞快一帧游戏逻辑里扫描了多次按键状态。核心的修复思路就是在游戏移动节拍里再增加一个方向锁dir_changed标志只有这个标志为0时按键才对new_dir有写入权限写入后立即置1等蛇移动完成后再清0。这样即使抖动或者快速连按一帧也只会改变一次方向。4.2 屏幕闪烁、拖影严重这个问题我在前面已经说过一遍但真的是重灾区再单独拎出来强调。屏幕闪烁的根源几乎都是全屏重绘。我在修改这个项目时优化策略很简单维护一个“脏格子”概念只有格子状态变化时才调用LCD_Fill()刷新对应坐标。蛇移动时旧尾巴的格子从蛇身色变成背景色新头格子从背景色变成蛇身色就这两次操作。如果你不管怎么优化还是闪那就去查LCD驱动里的LCD_SetCursor和LCD_WriteData时序看看每次写像素时地址是否连续。我自己曾经因为写命令和数据之间少了一个延时导致屏幕上出现随机拖影排查了整整一个下午才发现是SPI通信时序太紧的问题。4.3 编译报错找不到头文件或者外设没定义拿到别人的zip工程在自己电脑上编译时最常见的报错是fatal error: stm32f10x.h: No such file or directory。这通常是工程路径和include路径没配对。我在Keil里新建工程后会把标准库的头文件路径统一加进C/C Include Paths并全部使用相对路径..\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x\inc这种写法避免换电脑后绝对路径失效。还有一种情况是编译报unknown type name GPIO_TypeDef这基本可以确定是宏定义或者器件头文件路径的问题。先检查Define里有没有STM32F10X_MD再确认头文件路径有没有包含CMSIS里的stm32f10x.h所在目录。这两步做完90%的编译报错都能解决。4.4 游戏运行正常但串口输出乱码游戏画面正常说明MCU跑起来了但串口打印乱码问题九成出在时钟频率和波特率对不上。如果代码按外部8MHz晶振、72MHz主频配置但你板子上没有焊外部晶振那么实际主频可能只是内部HSI的某个值波特率自然就对不上了。这个问题的解决办法有两种一是板子上补上8MHz晶振二是在代码里把RCC的时钟源改成内部HSI并重新计算PLL倍频系数。比如想让主频接近72MHz可以配置RCC_PLLConfig(RCC_PLLSource_HSI_Div2, RCC_PLLMul_18);这样4MHz经过18倍频得到72MHz。需要注意HSI精度不如外部晶振如果项目还要用USB通信就必须使用外部晶振否则USB枚举会不稳定。贪吃蛇这种简单显示类项目用内部HSI是没问题的实测串口在115200波特率下通信正常偶尔出现的乱码只要重新上电就能恢复。4.5 常见问题速查表问题可能原因解决思路按键按了没反应GPIO模式不对、上拉未配置检查按键接法改用上拉输入按一下走两格未去抖、未锁方向加入10ms定时扫描方向锁屏幕花屏LCD初始化失败、SPI引脚不对对照屏幕型号检查驱动和引脚屏幕闪烁全屏重绘太慢只刷新变化的格子串口乱码时钟配置与波特率不匹配检查是否缺外部晶振修正RCC跑一会儿死机数组越界或蛇长超过最大值检查snake_len上限和碰撞判断再分享一个我自己比较受用的调试习惯游戏逻辑尽量做到和硬件显示分离。我在开发这个项目时会先在串口里打印蛇头坐标、方向和长度通过纯文本观察逻辑是否正确确认无误后再去优化显示效果。这样能把“逻辑bug”和“显示bug”分开排查效率高很多。这个项目从拿到手到真正吃透前后花了我大概三天时间。第一天跑通、第二天重构、第三天把各模块拆开再组装。等你能不看源码就在Keil里重新写出来并且能把速度、地图大小、配色这些参数自由调整说明这个项目的价值你才算真正拿到手了。接下来可以试着加一些自己的扩展比如障碍物、穿墙模式、双人模式改动过程会比想象中有意思得多。本文还有配套的精品资源点击获取