网站制作内联框设计类公司简介网页
网站制作内联框,设计类公司简介网页,行政单位网站信息建设政策,不属于常用网站建设的是1. 项目缘起#xff1a;为什么用STM32做贪吃蛇#xff1f;
很多朋友刚接触STM32的时候#xff0c;可能都是从点灯、按键扫描、串口打印这些基础实验开始的。学了一段时间#xff0c;外设也调通了几个#xff0c;但总感觉少了点什么——没错#xff0c;就是项目实战的成就…1. 项目缘起为什么用STM32做贪吃蛇很多朋友刚接触STM32的时候可能都是从点灯、按键扫描、串口打印这些基础实验开始的。学了一段时间外设也调通了几个但总感觉少了点什么——没错就是项目实战的成就感。看再多理论都不如亲手做一个能跑起来、看得见摸得着的项目来得实在。贪吃蛇这个经典游戏就是一个绝佳的嵌入式入门实战项目。它麻雀虽小五脏俱全你需要驱动屏幕来显示游戏画面需要读取按键来控制蛇的移动需要设计游戏逻辑来处理蛇的成长、碰撞和食物生成还需要管理定时器来控制游戏节奏。这一个项目下来GPIO、外部中断、定时器、LCD显示、状态机编程这些核心技能点你全都能练到。我当年学STM32的时候也是从俄罗斯方块、贪吃蛇这些小游戏入手的。那种看着自己写的代码在硬件上“活”起来的感觉特别能激发学习兴趣。今天我就带你从零开始在STM32上实现一个完整的贪吃蛇游戏。我会把每一步的原理、踩过的坑、优化的技巧都掰开揉碎了讲清楚保证你跟着做就能成功。2. 硬件准备与平台搭建工欲善其事必先利其器。我们先来看看需要准备哪些硬件以及如何搭建最简开发环境。2.1 核心硬件清单贪吃蛇项目对硬件要求不高市面上最常见的STM32开发板都能胜任。为了有最好的学习效果和兼容性我推荐以下配置主控芯片STM32F103C8T6也就是常说的“蓝桥杯”或“最小系统板”核心。这颗芯片性价比极高拥有72MHz的Cortex-M3内核、64KB Flash、20KB RAM驱动一个小游戏绰绰有余。而且资料丰富社区支持好。显示设备一块SPI接口的0.96寸或1.3寸OLED屏幕分辨率128x64。我强烈推荐用OLED因为它不需要背光显示黑色时像素完全关闭对比度高画面对比度好而且SPI接口接线简单只需要3-4根线。当然如果你手头有并口TFT彩屏比如ILI9341驱动的也可以只是驱动代码会稍复杂一些。输入设备四个独立按键。分别对应贪吃蛇的上、下、左、右四个方向。你也可以用一个五向摇杆按键来代替但独立按键逻辑更清晰更适合初学者。供电与调试一根Micro-USB线用于给开发板供电和程序下载/调试。一个ST-Link V2调试器或者使用板载的USB转串口芯片配合串口下载方式。这里有个小建议在购买OLED屏时最好选择已经焊好了排针的模块并且确认卖家提供了对应的STM32驱动例程。这能帮你省去很多查找引脚定义和调试初始化的时间。2.2 软件环境与工程创建软件方面我们主要用到两个工具STM32CubeMX意法半导体官方的图形化配置工具。它就像个“硬件图形界面”你点点鼠标就能配置芯片的时钟、引脚功能、外设参数然后它自动生成初始化代码框架。这对新手来说简直是神器能避免很多底层配置错误。Keil MDK-ARM或STM32CubeIDE集成开发环境IDE用来写代码、编译和调试。Keil在国内用得很广但需要授权STM32CubeIDE是基于Eclipse的免费且功能强大我后面会用CubeIDE来演示。第一步用CubeMX创建工程打开CubeMX选择你的芯片型号STM32F103C8Tx。首先配置时钟树Clock Configuration将HCLK调到72MHz这是F103的极限速度性能拉满。然后到引脚分配视图Pinout View给OLED的SPI或I2C引脚比如SCKPA5、MOSIPA7、RES复位PC0、DC数据/命令PC1、CS片选PA4如果支持硬件片选。给四个按键配置为GPIO输入模式并启用上拉电阻这样按键没按下时引脚就是高电平按下接地变为低电平抗干扰好。比如KEY_UPPA0 KEY_DOWNPA1 KEY_LEFTPA2 KEY_RIGHTPA3。别忘了配置一个系统定时器SysTick或者一个通用定时器如TIM2用来产生游戏的主循环节拍。配置完成后在Project Manager里设置好工程名、路径选择Toolchain为“STM32CubeIDE”然后点击“GENERATE CODE”。CubeMX就会为你生成一个完整的、包含所有外设初始化代码的工程。3. 显示驱动让贪吃蛇“现身”有了工程骨架我们首先要解决“显示”问题。贪吃蛇和食物都得画在屏幕上。3.1 OLED屏幕驱动基础OLED屏是像素点自发光的我们的游戏画面就是由无数个点亮或熄灭的像素点组成的。驱动OLED本质上是向屏幕的显存GRAM里写入数据。通常卖家提供的驱动库里会有几个核心函数// 初始化OLED void OLED_Init(void); // 设置写数据的起始坐标x: 0-127, y: 0-7因为128x64的屏通常被分为8页每页8行 void OLED_SetPos(uint8_t x, uint8_t y); // 写一个字节的数据到GRAM点亮8个垂直的像素点 void OLED_WR_Byte(uint8_t dat, uint8_t cmd); // 清空整个屏幕 void OLED_Clear(void); // 在指定位置显示一个字符需要字库 void OLED_ShowChar(uint8_t x, uint8_t y, uint8_t chr, uint8_t size); // 在指定位置显示字符串 void OLED_ShowString(uint8_t x, uint8_t y, uint8_t *chr, uint8_t size);你需要将这些驱动文件通常有oled.c,oled.h,font.h添加到你的CubeIDE工程中并根据你在CubeMX里配置的引脚修改驱动文件里的引脚定义宏。编译无误后写个简单的测试程序能在屏幕上显示“Hello Snake!”第一步就成功了。3.2 游戏地图与元素的绘制我们的贪吃蛇游戏需要一个固定的游戏区域。我们可以把128x64的屏幕划分成一个网格比如每个格子10x10像素那么游戏区域就是12列 x 6行留出边距。这样蛇和食物的位置就可以用网格坐标x, y来表示大大简化了逻辑。我们需要编写几个基础的绘图函数// 绘制一个实心方块代表蛇的一节身体或食物 void DrawBlock(uint8_t grid_x, uint8_t grid_y, uint8_t is_erase) { uint16_t start_x GRID_OFFSET_X grid_x * BLOCK_SIZE; uint16_t start_y GRID_OFFSET_Y grid_y * BLOCK_SIZE; // 使用OLED的矩形填充函数或者用画点函数循环实现 if(is_erase) { OLED_Fill(start_x, start_y, start_xBLOCK_SIZE-1, start_yBLOCK_SIZE-1, 0); // 清除 } else { OLED_Fill(start_x, start_y, start_xBLOCK_SIZE-1, start_yBLOCK_SIZE-1, 1); // 绘制 } } // 绘制游戏区域的边界 void DrawGameBorder(void) { // 画一个矩形框标出游戏区域 OLED_DrawRectangle(GRID_OFFSET_X, GRID_OFFSET_Y, GRID_OFFSET_X GRID_WIDTH * BLOCK_SIZE, GRID_OFFSET_Y GRID_HEIGHT * BLOCK_SIZE); }在游戏初始化时调用DrawGameBorder画出边界。蛇的移动和食物的出现都通过调用DrawBlock在对应网格坐标上“画方块”或“擦除方块”来实现。这种基于网格的坐标系统是游戏逻辑清晰的关键。4. 贪吃蛇的核心逻辑设计这是整个项目的“大脑”。我们需要用代码定义蛇是什么它怎么动吃什么以及何时游戏结束。4.1 数据结构如何表示一条蛇在C语言里我们可以用数组和结构体来优雅地表示蛇。我推荐下面这种结构#define MAX_SNAKE_LENGTH 50 // 蛇的最大长度根据网格大小设定 typedef struct { int8_t x; // 网格坐标x int8_t y; // 网格坐标y } Point; typedef struct { Point body[MAX_SNAKE_LENGTH]; // 身体节坐标数组 uint8_t length; // 当前长度 int8_t direction; // 移动方向0上1下2左3右 Point food; // 食物位置 uint8_t score; // 得分 } SnakeGame_t; SnakeGame_t game;game.body[0]是蛇头game.body[length-1]是蛇尾。蛇的移动可以看作是在数组头部插入一个新的坐标新的蛇头并在尾部删除一个坐标旧的蛇尾。如果吃到了食物就不删除尾从而实现增长。4.2 移动、生长与碰撞检测蛇的移动逻辑在一个定时器中断或者主循环中周期性执行。其伪代码如下void Snake_Move(void) { Point new_head game.body[0]; // 获取当前蛇头 // 根据当前方向计算新的蛇头坐标 switch(game.direction) { case DIR_UP: new_head.y--; break; case DIR_DOWN: new_head.y; break; case DIR_LEFT: new_head.x--; break; case DIR_RIGHT: new_head.x; break; } // 碰撞检测1是否撞墙 if(new_head.x 0 || new_head.x GRID_WIDTH || new_head.y 0 || new_head.y GRID_HEIGHT) { Game_Over(); return; } // 碰撞检测2是否撞到自己 for(uint8_t i 0; i game.length; i) { if(new_head.x game.body[i].x new_head.y game.body[i].y) { Game_Over(); return; } } // 碰撞检测3是否吃到食物 uint8_t eat_food 0; if(new_head.x game.food.x new_head.y game.food.y) { eat_food 1; game.score; Generate_Food(); // 在随机空位置生成新食物 } // 更新蛇身数组 // 将整个身体数组向后移动一位为新蛇头腾出位置 for(int i game.length; i 0; i--) { game.body[i] game.body[i-1]; } game.body[0] new_head; // 设置新的蛇头 // 如果没吃到食物需要擦除旧的蛇尾数组最后一位 if(!eat_food) { DrawBlock(game.body[game.length].x, game.body[game.length].y, 1); // 擦除 } else { // 吃到了长度增加 game.length; // 注意数组长度不能超过MAX_SNAKE_LENGTH } // 绘制新的蛇头 DrawBlock(new_head.x, new_head.y, 0); }这里有个关键技巧判断撞到自己时循环可以从i 0开始但理论上新蛇头不可能和旧蛇头重合所以可以从i 1开始稍微优化一点。Generate_Food函数需要随机生成一个不在蛇身上的坐标可以用rand()函数配合取模运算来实现记得用定时器或ADC值初始化随机种子。4.3 按键控制与方向输入控制蛇的转向是通过外部中断或主循环中扫描按键状态来实现的。为了体验更流畅我推荐使用外部中断来捕获按键按下事件。在CubeMX中将四个方向键对应的GPIO引脚配置为“外部中断模式”并设置为“下降沿触发”因为我们的按键是按下接地。然后在生成的代码中找到对应的中断服务函数比如EXTI0_IRQHandler在里面修改蛇的direction。这里有一个非常重要的细节蛇不能直接反向移动。比如当前正在向右移动此时按下左键如果立刻转向蛇头会立刻向左等于原地掉头撞到自己身体第二节游戏会瞬间结束。这不符合经典贪吃蛇的规则。所以在中断函数里我们需要做方向限制void EXTI0_IRQHandler(void) { // 假设PA0是“上”键 if(__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) ! RESET) { __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0); // 清除中断标志 // 如果当前不是向下移动才能改为向上 if(game.direction ! DIR_DOWN) { game.direction DIR_UP; } } }其他三个按键的中断服务函数同理分别限制不能反向即可。这样处理之后按键控制就既灵敏又安全了。5. 游戏循环、计时与状态管理一个完整的游戏需要有开始、进行中、结束等不同状态并且要按照一个稳定的节奏运行。5.1 利用SysTick实现游戏主循环我们不希望蛇的移动速度受主循环执行时间的影响因此需要一个稳定的时间基准。STM32的SysTick定时器系统嘀嗒定时器非常适合做这个事。它在CubeMX初始化时默认被配置为1ms中断一次。我们可以设置一个全局变量game_speed例如值为200表示每200ms移动一次然后在SysTick中断服务函数里对一个计数器进行递减volatile uint32_t g_tick_count 0; void SysTick_Handler(void) { HAL_IncTick(); // Cube HAL库用必须保留 if(g_tick_count 0) g_tick_count--; } // 在主循环中 while (1) { // 处理按键等实时事件中断已处理方向这里可能处理暂停/开始键 if(KEY_PAUSE_PRESSED()) { game_state GAME_PAUSED; // 显示暂停信息 } // 游戏运行状态下的逻辑 if(game_state GAME_RUNNING) { if(g_tick_count 0) { g_tick_count game_speed; // 重置计时器 Snake_Move(); // 执行一次蛇的移动和逻辑判断 Update_Score_Display(); // 更新分数显示 } } else if(game_state GAME_OVER) { // 显示游戏结束画面等待重启 if(KEY_RESTART_PRESSED()) { Game_Init(); // 重新初始化游戏 } } // ... 其他处理 }这种结构确保了游戏逻辑Snake_Move以固定的时间间隔被调用与CPU执行其他代码的快慢无关游戏速度是稳定的。5.2 游戏状态机与分数系统我们可以用一个枚举来清晰定义游戏状态typedef enum { GAME_START, // 开始画面 GAME_RUNNING, // 运行中 GAME_PAUSED, // 暂停 GAME_OVER // 结束 } GameState_t; GameState_t game_state GAME_START;在GAME_START状态显示一个简单的开始界面或标题。在GAME_RUNNING状态执行我们上面说的主循环逻辑。在GAME_PAUSED状态停止蛇的移动但保持画面。在GAME_OVER状态显示最终得分和“Game Over”提示等待玩家按键重启。分数系统很简单每吃一个食物game.score加一分。可以在屏幕的固定区域比如游戏区域上方用OLED_ShowString函数实时显示出来。随着分数增加你可以通过减小game_speed的值来让蛇移动得更快增加游戏难度。6. 进阶优化与功能扩展基础功能实现后你的贪吃蛇已经能玩了。但我们可以让它变得更专业、更好玩。6.1 提升显示效果绘制更圆滑的蛇之前我们用实心方块代表蛇身看起来有点“方头方脑”。我们可以优化绘图函数让方块的四个角变成圆角或者用不同的颜色区分蛇头和蛇身如果是彩色屏。对于单色OLED可以尝试绘制中间留白的矩形框作为蛇身用实心矩形作为蛇头。void DrawSnakeBlock(uint8_t grid_x, uint8_t grid_y, uint8_t is_head) { uint16_t x GRID_OFFSET_X grid_x * BLOCK_SIZE; uint16_t y GRID_OFFSET_Y grid_y * BLOCK_SIZE; if(is_head) { // 画实心矩形作为蛇头 OLED_Fill(x, y, xBLOCK_SIZE-1, yBLOCK_SIZE-1, 1); } else { // 画空心圆角矩形作为蛇身 OLED_DrawRoundRect(x, y, xBLOCK_SIZE-1, yBLOCK_SIZE-1, 2, 1); } }6.2 添加音效与震动反馈可选如果你的开发板有蜂鸣器Buzzer或马达可以增加简单的反馈。吃到食物时让蜂鸣器短促地响一声游戏结束时长响一声。这只需要在对应的逻辑点调用蜂鸣器驱动函数即可。震动马达同理通过一个GPIO口控制即可。这些感官反馈能极大提升游戏的沉浸感。6.3 存档与最高分记录想让你的游戏更有挑战性可以加入EEPROM存储最高分的功能。STM32F103C8T6内部没有EEPROM但我们可以利用其Flash的最后一页通常不会被程序占用来模拟EEPROM存储一个最高分记录。// 将得分写入Flash的指定地址 void Write_HighScore_To_Flash(uint16_t score) { HAL_FLASH_Unlock(); // 解锁Flash写保护 // ... 擦除页、写入数据的操作需按Flash编程手册操作 HAL_FLASH_Lock(); } // 从Flash读取最高分 uint16_t Read_HighScore_From_Flash(void) { // ... 读取操作 }在游戏开始时读取最高分并显示在游戏结束时如果当前得分更高则更新最高分并写入Flash。这样每次开机历史记录都在。6.4 代码结构优化与模块化最后从工程角度优化你的代码。把不同功能的代码放到不同的.c/.h文件对里snake_game.c/.h纯粹的游戏逻辑数据结构、移动、碰撞检测、食物生成。oled_graphics.c/.h所有屏幕绘图函数包括画方块、画边框、显示文字等。input_handler.c/.h按键扫描或中断处理函数。main.c主要包含初始化和主循环协调各个模块。这样的结构清晰明了以后你想移植到别的屏幕、或者增加新功能只需要修改对应的模块不会牵一发而动全身。这也是嵌入式开发中非常重要的工程化思维。