STM32F407贪吃蛇实战:FSMC驱动与状态机设计解析

发布时间:2026/9/9 20:24:11
STM32F407贪吃蛇实战:FSMC驱动与状态机设计解析 简介面向STM32F407及嵌入式初学者的贪吃蛇游戏完整工程基于Cortex-M4内核与HAL库开发可帮助理解中断管理、定时器应用、GPIO显示控制、USART通信及碰撞检测等实时系统关键环节。代码采用模块化组织配合STM32CubeMX生成的初始化框架读者可对照工程清晰地体会从外设配置到游戏逻辑的实现流程。工程共159个文件以77个C源文件与75个H头文件为主另有Keil工程配置uvprojx/uvoptx、可直接烧录的hex固件及清理脚本压缩包仅831KB。已有2810人学习下载。实现中覆盖定时器中断控制帧率、按键中断驱动、蛇身坐标数组管理、食物随机生成、边界与自身碰撞判断等核心算法并预留了LCD/LED显示及串口调试接口既适合课程设计也可作为嵌入式入门实战的高性价比参考工程。 用STM32F407做贪吃蛇听起来是个入门级练手项目但我做完一轮之后想说这活儿真不简单它能把嵌入式开发里一大半基础技能串起来。从GPIO和定时器到中断优先级、LCD显示驱动、状态机设计再到串口日志和触摸校准每一个环节都能“恰到好处”地出点幺蛾子。如果你手头正好有一块STM32F407开发板又不想天天跑LED流水灯贪吃蛇是一个性价比极高的选择——它不挑复杂外设只用一块彩屏、几个按键就能把MCU的资源调度和代码思路完整练一遍。这篇内容会从整体方案设计讲起拆LCD驱动和游戏逻辑的实现再给一套基于CubeMXHAL库的完整工程配置流程最后把调试时最容易踩的坑和触摸屏扩展一起聊透。适合三类人看刚学完标准库想进阶HAL库的、想把手里F407板子玩出花样的、以及做课程设计想抄一份能跑的完整方案的。1. 项目整体思路与硬件选型1.1 为什么选STM32F407做这种小游戏在很多人眼里贪吃蛇用51单片机、用ESP32甚至直接用电脑写个C语言控制台版本都行非要选STM32F407看着像小题大做。但实际上F407这颗芯片的核心优势在于主频和FSMC接口——168MHz的Cortex-M4内核跑游戏逻辑绰绰有余FSMC可以把LCD当成“外部SRAM”直接映射到内存地址空间刷屏速度比GPIO模拟时序快一个量级。这个项目真正考验的不是“能不能跑贪吃蛇”而是“在有多余性能的情况下代码结构能不能撑得住后续扩展”。比如后面想加触摸屏校准、加计分动画、加历史记录存储MCU性能冗余足够你只需要在软件架构上做文章不用担心底层算力不够。我选的是经典的STM32F407ZGT6开发板板载2.8寸TFT彩屏驱动芯片是ILI9341接口方式是FSMC 16位并口。方向控制用板载独立按键没有直接上触摸屏——先把基本功练熟后面再在第四节把电阻触摸校准补上。1.2 显示方案对比FSMC并口屏、SPI屏、OLED屏很多人会纠结用哪种屏幕我给个直观对比屏幕类型接口刷新表现驱动难度适合场景2.8寸TFT ILI9341FSMC并口快全屏刷新不掉帧中本项目的首选1.44寸TFT ST7735SPI慢全屏刷新能看到闪烁低小型项目、引脚受限0.96寸OLED SSD1306I2C/SPI中分辨率低低简单展示、省电应用3.5寸TFT ILI9488FSMC/SPI取决于接口方式中需要更大显示面积时如果手头是SPI屏也能做但建议尽量把游戏区域缩小一点用局部刷新替代全屏刷新不然蛇一长肉眼可见地在闪。FSMC方案的优势是在写入像素时CPU只需要执行“向某个地址写数据”的指令硬件自动完成时序拼接不占GPIO翻转的开销。还有一点值得注意F407的FSMC外设虽然叫“FMC”实际上在F1系列上是FSMCF4系列叫FMC只是名称差异用法差不多。CubeMX里配置时选“FMC”选项卡然后配成NOR/SRAM控制器模式就行。1.3 按键输入与状态机设计思路贪吃蛇的方向控制最直接的实现就是读GPIO电平判断按键按下。但这里有个细节机械按键存在抖动单独用延时消抖会占用CPU最好的做法是用定时器扫描。我开了两个定时器一个负责游戏主循环节拍比如200ms一帧一个负责按键扫描消抖每10ms读一次按键状态连续两次相同才判定有效。状态机是整个游戏程序的灵魂。我设计了四个状态START、RUNNING、PAUSE、GAME_OVER。截图说明可能不太直观但代码逻辑其实很清晰按键1启动/暂停按键2重置方向键控制蛇头朝向。状态切换只发生在按键事件里游戏逻辑只关心当前状态是什么这样代码的可维护性好很多。比较建议在状态机外层套一个“事件触发”模式——按键按下时不直接改游戏数据而是把事件发到一个环形队列里主循环再从队列取事件处理。虽然F407性能冗余但这个设计能防止在定时器中断里做太多事情导致逻辑混乱后面扩展触摸操作也能直接兼容。2. 显示驱动与贪吃蛇核心逻辑拆解2.1 用FSMC驱动ILI9341的底层原理FSMC驱动LCD的核心是把LCD屏当成一个静态存储器设备。ILI9341的接口上有一根DC数据/命令选择引脚通常接到FSMC的地址线A6上。CS片选接到NE1对应的映射地址是0x60000000。由于A6是第6根地址线当CPU写0x60000000时A6保持低电平代表命令写0x60000040即第6位置1时A6是高电平代表数据。代码里一般这样定义#define LCD_CMD_ADDR ((uint32_t)0x60000000) #define LCD_DATA_ADDR ((uint32_t)0x60020000) // A18为高或者按实际接线调整 #define LCD_WR_CMD(cmd) (*(volatile uint16_t *)LCD_CMD_ADDR (cmd)) #define LCD_WR_DATA(data) (*(volatile uint16_t *)LCD_DATA_ADDR (data))有人可能会问为什么数据地址要单独设一个偏移因为DC引脚接到哪根地址线偏移就对应哪一位。我的板子上DC接了A18所以数据地址是0x60020000第二位置1。你在实际移植时先确认原理图的DC引脚接的是FSMC的哪个地址线再算偏移量这一步做错的话屏幕要么花屏要么根本点不亮。像素填充方面ILI9341支持直接写GRAM的方式设置好坐标窗口后连续写数据就能快速填充矩形区域。贪吃蛇的背景网格和食物绘制本质就是填充多个小矩形所以我把“画矩形填充”封装成了一个基础函数后面所有界面都复用它。2.2 贪吃蛇的数据结构选型与坐标规划贪吃蛇的移动本质上是一个“头进尾出”的过程。最简单的做法是用一个二维数组标记整张地图每次移动都遍历更新。但我推荐用链表蛇的每一节是一个节点头节点根据方向移动新节点插入头部尾部节点删除这样每一步移动的复杂度是O(1)。typedef struct SnakeNode { uint8_t x; uint8_t y; struct SnakeNode *next; } SnakeNode;游戏区域我规划成25列×19行每个格子对应屏幕上12×12像素。这样整屏游戏区正好是300×228像素留出底部区域显示得分和提示信息。格子大小太大会让游戏显得很空太小则手指或者按键判断容易误触12像素在2.8寸屏上表现刚好。食物的生成逻辑有一处容易忽视新食物不能生成在蛇身上。如果图省事用随机数直接产生坐标蛇比较长时很容易刷到蛇身上出现“食物刚出现就消失”的错觉。我建议先生成一个候选坐标如果命中蛇身坐标就重新随机最多尝试50次超过则判定游戏胜利因为蛇已经快占满屏幕了。2.3 碰撞检测与移动节奏控制碰撞检测有两种撞墙检测和撞自己检测。撞墙很简单——蛇头坐标超出边界就算死撞自己则需要遍历蛇身节点把蛇头坐标和每一个身体节点比对。实际测试中遍历判定会造成一个“视觉延迟”蛇头看起来还没到就提前判死了。原因在于头部移动后、尾部未删除前头坐标和原来的颈部坐标重合误判成撞自己。解决办法是先把尾巴节点信息保存下来等新头坐标计算完成后再删除尾部。伪代码如下// 保存尾部 oldTail snakeTail; // 删除尾部 removeTail(); // 更新头部 newHead.x oldHead.x dirX; newHead.y oldHead.y dirY; // 检查是否撞到自己 if (checkCollision(newHead)) gameOver(); // 把新头插入链表头 insertHead(newHead);移动节奏方面我建议用一个10ms一次的定时器作为“心跳”在中断里对计数变量累加到达“移动间隔”时置一个标志位主循环检测到标志位再执行蛇移动和画面刷新。这样调整游戏速度只需要改变移动间隔数字比如初始300ms每吃一个食物减5ms最低100ms避免无限加速导致无法操控。3. 基于STM32CubeMX HAL库的完整实现流程3.1 CubeMX工程配置要点时钟、FSMC、定时器如果你还在手写寄存器配置LCDC驱动时序我个人强烈建议换成CubeMXHAL库。不是因为寄存器不好而是FSMC和时钟树的配置太繁琐手写容易漏引脚复用而CubeMX几秒钟生成代码省下来的时间能用来调试游戏逻辑。CubeMX里需要做这些关键配置RCC选择HSE外部晶振时钟树调到168MHzF407最高主频USB等外设依赖这个频率FMC选NOR/SRAM Bank1配置NE1片选、16位数据宽度、读写时序参数FSMC引脚自动分配检查一下有没有和板载其它外设冲突TIM3设为10ms定时中断负责按键扫描和游戏节拍统计USART1启用用于printf日志输出FSMC的时序参数需要关注读/写建立时间和保持时间。LCD的数据手册里有最小时序要求CubeMX里可以直接填。如果屏幕花屏不要急着查代码先检查时序参数是不是设置得太极限我吃过亏把建立时间从15ns压到10ns屏幕直接出噪点。3.2 显示层封装画点、填充、字符串显示游戏界面显示内容比较杂背景、蛇身、食物、文字、分数。强烈建议把显示层按功能拆分不要全部写在main.c里。我是这样规划的// lcd_driver.c 底层 void LCD_Init(void); void LCD_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1); void LCD_Fill(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color); // gui.c 界面层 void GUI_DrawGameBg(void); void GUI_DrawSnake(SnakeNode *head); void GUI_DrawFood(uint8_t x, uint8_t y); void GUI_ShowScore(uint16_t score);界面层只管把游戏数据变成像素不关心数据怎么变。数据层只改链表和状态不关心界面。这样在跑逻辑的时候可以先关掉界面刷新用串口日志打印数据变化来定位问题调试效率翻倍。字符串显示是很多新手容易卡住的地方。ILI9341没有内置字库需要往控制器里写字符点阵数据。我用的是网上找的12×12和16×24 ASCII字库取模方式为“逐行取模高位在前”。如果你用的屏显示文字是镜像或者缺笔画99%是取模方式设错了对照字库数据检查一下高低位和扫描方向即可。3.3 游戏逻辑完整代码框架这里我把最核心的游戏逻辑结构放出来。它不是完整代码但搭出了清晰骨架你照着填就行typedef enum { GAME_START, GAME_RUNNING, GAME_PAUSE, GAME_OVER } GameState; GameState state GAME_START; uint8_t dirX 1, dirY 0; uint8_t pendingDirX 1, pendingDirY 0; uint16_t score 0; uint16_t moveSpeed 300; uint16_t speedCounter 0; void Game_OnTimerTick(void) { if (state GAME_RUNNING) { speedCounter 10; if (speedCounter moveSpeed) { speedCounter 0; Game_MoveSnake(); } } } void Game_MoveSnake(void) { // 先应用待处理方向再移动 dirX pendingDirX; dirY pendingDirY; // 计算新头部 uint8_t newX snakeHead-x dirX; uint8_t newY snakeHead-y dirY; // 撞墙判断 if (newX MAP_W || newY MAP_H) { state GAME_OVER; return; } // 临时保留尾巴 SnakeNode *oldTail snakeTail; removeTail(); // 检查撞自身 if (isSnakeNode(newX, newY)) { state GAME_OVER; return; } // 插入新头 insertHead(newX, newY); // 吃到食物 if (newX foodX newY foodY) { score; moveSpeed (moveSpeed 100) ? moveSpeed - 5 : 100; // 把旧尾巴加回来相当于蛇变长 appendNode(oldTail); generateFood(); } }按键扫描部分我开了独立按键中断但中断里只做“按键事件入队”真正的状态切换在game_task里完成。为什么这么做因为如果在中断里直接改游戏状态可能会在移动逻辑执行了一半时打断操作导致链表指针混乱。这是个非常隐蔽的bug来源在中断服务函数里操作数据结构务必要三思。3.4 串口日志与调试技巧printf重定向调试嵌入式应用没有日志就像闭着眼睛开车。我用的调试方案是串口重定向printf加开关宏控制日志等级。#define DEBUG_LEVEL 1 #if DEBUG_LEVEL 0 #define DBG_INFO(fmt, ...) printf([INFO] fmt \r\n, ##__VA_ARGS__) #else #define DBG_INFO(fmt, ...) #endif重定向printf需要重写fputc函数int fputc(int ch, FILE *f) { while (!(USART1-SR USART_FLAG_TXE)); USART1-DR (uint8_t)ch; return ch; }游戏运行时每次移动都打印蛇头坐标、方向和当前状态这样逻辑错误可以快速定位。比如明明按了向上蛇却往左走了大概率是方向键的取反逻辑问题看一眼日志就能判断。4. 调试踩坑、触摸屏扩展与细节优化4.1 常见问题速查花屏、闪屏、方向反转我在调试中积累了一些高频问题整理成速查表问题现象可能原因解决方法屏幕白屏或花屏FSMC时序配置过极限放宽读/写建立时间屏幕白屏或花屏DC地址线偏移算错核对原理图DC接入地址线屏幕显示镜像字库取模方向不对检查逐行/逐列、高位/低位蛇移动时闪屏全屏刷新导致改为局部刷新脏矩形按键方向偶尔不响应按键未消抖或扫描周期太长定时器10ms扫描连续采样蛇刚移动就算撞自己头部计算和尾部删除顺序错误先删尾部再插入新头食物刷在蛇身上随机坐标未检测蛇身添加碰撞检测循环串口输出乱码波特率不匹配或时钟配置错误核对USART配置与终端参数屏幕闪屏问题的根因值得展开说说。如果每帧都调用LCD_Clear全屏清屏ILI9341的GRAM刷新需要时间肉眼就能看到闪烁。优化方案是“脏矩形”刷新只重绘蛇尾擦除区域和蛇头新增区域。由于背景是纯色把旧蛇尾画成背景色再把新蛇头画成蛇身色视觉上就是平滑移动了CPU占用也低了很多。4.2 串口日志存储到Flash简易日志记录方法根据热词“基于stm32f407的日志存储记录方法”这其实是一个很实用的扩展。在调试阶段数据能通过串口实时打印但有些问题只在掉电重启后复现串口日志来不及看就需要把日志写入Flash下次开机时可以回看。F407内置Flash容量大可以通过HAL库的Flash接口擦写扇区。我在片内Flash末尾开辟了一个LOGDATA扇区用于记录最近一次运行的关键日志#define LOG_SECTOR_ADDR 0x080E0000 // 按实际芯片容量选末尾扇区 void Log_Write(uint16_t line, uint16_t data) { HAL_FLASH_Unlock(); // 擦除扇区 FLASH_EraseInitTypeDef erase {0}; erase.TypeErase FLASH_TYPEERASE_SECTORS; erase.Sector FLASH_SECTOR_11; erase.NbSectors 1; erase.VoltageRange FLASH_VOLTAGE_RANGE_3; HAL_FLASHEx_Erase(erase, err); // 写入数据 HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, LOG_SECTOR_ADDR, line); HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, LOG_SECTOR_ADDR 2, data); HAL_FLASH_Lock(); }注意Flash擦写次数有限不能在游戏死循环里频繁调用只适合在异常状态、关键节点写入。我通常只在碰撞检测触发时记录一次保存当时蛇的长度和出错码方便分析是逻辑误判还是外部干扰。4.3 触摸屏扩展TFT电阻触摸四点校准法如果你手头的板子是TFT电阻触摸屏或者想把这个项目升级成触控操作那就绕不开触摸校准。热词里提到的“四点校准法”是电阻触摸屏的主流校准方案。其原理是LCD坐标和ADC采样坐标之间存在一个线性变换关系通过四个已知点的坐标对求出变换矩阵的六个系数。线性变换公式可以写成// 假设ADC采样值 adcX, adcYLCD坐标为 lcdX, lcdY // lcdX (A * adcX B * adcY C) / K // lcdY (D * adcX E * adcY F) / K四点在屏幕上尽量靠边的位置各放一个校准点依次点击后得到四组坐标联立方程解出K、A、B、C、D、E、F。实际工程中一般不用自己手推矩阵网上有现成的触摸校准库比如Xnake项目里的touch_calibrate.c把四点坐标结构体填进去自动算出参数。我自己在实际操作中验证过四点校准比三点校准稳定性好很多。三点校准虽然少按一次但碰到边缘区域偏差较大。校准完成后别忘了加一个“参数保存”功能把计算出的系数存到EEPROM没有EEPROM就存Flash末尾扇区否则每次开机都要重新校准很考验耐心。4.4 基于模拟I2C的扩展与外设补充最后再说一个F407开发中绕不开的知识点模拟I2C。如果你后续想给贪吃蛇项目加外部存储EEPROM、加OLED小屏显示分数、或者加传感器记录环境数据I2C是最高频的通信之一。热词里特意提了“stm32f407使用hal库模拟i2c”说明很多人都踩过硬件I2C的坑。HAL库的硬件I2C在F4上有时候会卡死在事件等待上模拟I2C反而更可靠。模拟I2C的核心就是两个GPIO引脚手动拉高拉低模拟时钟和数据时序。I2C通信协议分四段起始信号、从机地址读写位、数据字节、停止信号每发一个字节后还要等待从机拉低SCL应答位。贪吃蛇项目用I2C能扩展什么最简单的场景是把当前最高分存到外部EEPROM掉电不丢失。这样每次开机都能显示历史成绩比内置Flash擦写方便也不占用Flash寿命。如果你在调试I2C时发现数据读回来全是0xFF先查上拉电阻——很多板载EEPROM电路偷工减料没加上拉模拟I2C模式下需要外部上拉到VCC才能通信。写在最后的经验做完这个贪吃蛇项目我最深的体会是嵌入式的乐趣不在于代码能跑而在于你能彻底掌控从“按键按下”到“屏幕上显示变化”的完整链路。相比之下单纯用C语言在电脑上写一个贪吃蛇控制台版本虽然也能锻炼逻辑但少了中断、外设、时序这些硬件层面的约束缺少了那层“在资源受限条件下做取舍”的刺激感。如果你准备复刻这个项目我的建议是先别急着加触摸和其它高级功能把FSMC驱动、链表、状态机三个核心点吃透能在LCD上稳定跑出200ms一帧不闪屏再谈扩展。触摸校准、I2C存储这些都可以逐步叠加上去。遇到花屏不要慌先怀疑时序和地址映射遇到逻辑错乱不要急打开串口日志看数据流绝大多数问题都能在十分钟内定位。最后再分享一个小技巧把游戏区域的颜色做成可配置的比如深色背景配亮色蛇身视觉疲劳会减轻很多。如果后续有精力可以再把AI自动寻路的贪吃蛇加进去那就是另一个维度的算法挑战了。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询