☰
51单片机贪吃蛇游戏设计:从工程结构到二次开发全解析
2026/10/17 7:15:28 网站建设 项目流程

简介:这份资源面向电子信息、自动化等专业的本科学生及单片机初学者,提供一套完整的51单片机贪吃蛇游戏课程设计解决方案,帮助读者完成从硬件仿真到代码实现、再到报告答辩的全流程任务。压缩包共18个文件,约102.42MB,涵盖Proteus仿真工程、Keil C源码与头文件、编译生成的hex与obj文件、演示视频、课程设计报告docx以及答辩PPT等,文件类型覆盖pdsprj、c、hex、docx、pptx等,结构完整、层次分明。游戏规则为四方向键控制蛇移动吃豆,吃豆后蛇身加长且速度提升,撞墙或撞到自身即结束,逻辑清晰、便于二次修改与功能扩展。目前已有2438人学习下载,读者可借助仿真文件直接运行验证,参考源码理解按键扫描、定时器与显示驱动等模块,并利用报告与PPT快速整理设计思路,适合作为课程设计、毕业设计或单片机入门练手项目。

1. 从一块 51 最小系统板说起:这个贪吃蛇项目到底能跑出什么效果

很多人第一次接触单片机课程设计,最怕的不是写代码,而是拿到一个压缩包之后不知道从哪下手。这个基于 51 单片机的贪吃蛇游戏设计,本质上就是一份可以直接烧录运行的完整工程:一块 51 最小系统板、一个 8x8 或 16x16 点阵屏(或者 LCD12864)、几个按键,就能跑出一个能玩、能计分、能加速的贪吃蛇。它适合两类人:一类是正在做课程设计、需要快速跑通并理解每一行代码的本科生;另一类是想拿它当模板,改成俄罗斯方块、推箱子这类同构小游戏的开发者。整个项目的核心不在游戏本身,而在于怎么用有限的 RAM 和定时器资源,把刷新、输入、逻辑三件事调度得不出撕裂、不丢按键。

2. 工程结构拆解:从 Keil 工程到点阵驱动的分层逻辑

2.1 目录里到底放了什么

拿到压缩包解压后,常见的目录结构是这样的(不同版本可能略有差异,但分层思路一致):

Snake_51/ ├── Project/ # Keil uVision 工程目录 │ ├── Snake.uvproj # 工程文件,双击直接打开 │ ├── Snake.uvopt # 调试配置,保存断点和窗口布局 │ └── Objects/ # 编译输出,.hex 文件在这里 ├── User/ │ ├── main.c # 主循环,调度入口 │ ├── snake.c / snake.h # 蛇身逻辑、移动、吃食物、碰撞 │ └── config.h # 引脚定义、屏幕尺寸、速度等级 ├── Drivers/ │ ├── lcd12864.c / .h # 点阵屏驱动,写命令/写数据 │ ├── key.c / .h # 按键扫描,含消抖 │ └── timer.c / .h # 定时器初始化,提供 1ms 时基 └── Doc/ └── 原理图.pdf # 引脚连接说明

这个分层不是摆设。User层只关心游戏规则,Drivers层只关心硬件时序,config.h把两者粘在一起。改屏幕型号时,只需要动Drivers和config.h,snake.c里的逻辑一行不用碰。我见过太多课程设计把 LCD 写命令和蛇的移动混在一个while(1)里,最后想换个屏就得重写一半代码,这个工程的结构就是用来避免那种翻车的。

2.2 主循环的调度骨架

51 单片机没有 RTOS,所有任务都靠一个主循环加定时器中断来分时。这个项目的main.c典型写法如下:

#include "config.h" #include "timer.h" #include "key.h" #include "lcd12864.h" #include "snake.h" void main(void) { Timer0_Init(); // 1ms 时基,用于游戏速度控制和按键消抖 LCD_Init(); // 初始化点阵屏,清屏 Key_Init(); // 按键引脚配置 Snake_Init(); // 蛇身复位到初始位置,长度 3,方向向右 while (1) { uint8_t key = Key_Scan(); // 非阻塞扫描,返回键值或 0 if (key != 0) { Snake_ChangeDirection(key); // 只改方向,不直接移动 } if (g_game_tick >= g_speed_level) // g_game_tick 由定时器中断累加 { g_game_tick = 0; Snake_Move(); // 按当前方向走一步 if (Snake_CheckCollision()) // 撞墙或撞自己 { Game_Over(); } if (Snake_EatFood()) // 吃到食物,长度+1,刷新食物 { Score_Update(); } LCD_Refresh(); // 把显存刷到屏幕 } } }

这里有几个关键点值得说清楚。g_game_tick是一个全局变量,在定时器中断里每 1ms 加一,主循环通过比较它和g_speed_level来决定什么时候走一步。这样做的目的是把“游戏速度”和“CPU 执行速度”解耦——你想让蛇走得快,就减小g_speed_level,而不是去改延时函数。Key_Scan()必须是非阻塞的,否则按住按键时蛇会卡住不动。LCD_Refresh()放在逻辑更新之后,保证屏幕上显示的一定是当前帧的完整状态,不会出现蛇头已经转弯、蛇尾还没擦掉的撕裂感。

2.3 显存与屏幕的映射关系

点阵屏的一个像素对应显存里的一个 bit。以 12864 为例,屏幕 128x64 像素,按页寻址分成 8 页,每页 8 行,每行 128 列。显存通常定义成一个二维数组:

// config.h 中的显存定义 #define LCD_PAGES 8 #define LCD_COLS 128 uint8_t g_lcd_buffer[LCD_PAGES][LCD_COLS]; // 每字节 8 个垂直像素 // 把显存某一点置位或清零 void LCD_SetPixel(uint8_t x, uint8_t y, uint8_t on) { uint8_t page = y / 8; uint8_t bit = y % 8; if (on) g_lcd_buffer[page][x] |= (1 << bit); else g_lcd_buffer[page][x] &= ~(1 << bit); }

LCD_Refresh()做的事就是把g_lcd_buffer整块写进屏幕的 GDDRAM。注意 51 的 RAM 很小,g_lcd_buffer占 8x128=1024 字节,对 51 来说已经不小了,所以有些版本会用 8x8 点阵来省内存。如果你要改成 16x16 的点阵,显存会变成 32 字节,反而更省,但驱动方式要从行扫描改成列扫描,这是二次开发时第一个要动的地方。

3. 核心模块逐段讲:蛇怎么动、食物怎么刷、按键怎么不丢

3.1 蛇身的数据结构

蛇身不能用链表,51 的指针操作和动态内存都不划算。常见做法是用一个固定长度的数组加头尾索引:

// snake.h #define MAX_SNAKE_LEN 64 typedef struct { uint8_t x[MAX_SNAKE_LEN]; uint8_t y[MAX_SNAKE_LEN]; uint8_t head; // 头在数组中的下标 uint8_t tail; // 尾在数组中的下标 uint8_t len; // 当前长度 uint8_t dir; // 0=上,1=下,2=左,3=右 } Snake_t; Snake_t g_snake;

移动的逻辑是:新头位置 = 当前头位置 + 方向偏移,然后把新头写入数组,head前移,tail前移,len不变。如果吃到食物,tail不动,len加一。这样数组是循环使用的,不会越界,也不需要搬移数据。

void Snake_Move(void) { uint8_t new_x = g_snake.x[g_snake.head]; uint8_t new_y = g_snake.y[g_snake.head]; switch (g_snake.dir) { case 0: new_y--; break; // 上 case 1: new_y++; break; // 下 case 2: new_x--; break; // 左 case 3: new_x++; break; // 右 } g_snake.head = (g_snake.head + 1) % MAX_SNAKE_LEN; g_snake.x[g_snake.head] = new_x; g_snake.y[g_snake.head] = new_y; if (!g_snake.grow_flag) // 没吃到食物,尾巴要缩 { g_snake.tail = (g_snake.tail + 1) % MAX_SNAKE_LEN; } else { g_snake.grow_flag = 0; // 吃到食物,这一帧不缩尾 g_snake.len++; } }

grow_flag这个标志位很关键。吃到食物的那一帧,尾巴不能缩,否则蛇的长度不会增加。很多初学者在这里翻车:先判断吃食物再移动,结果食物被吃了但蛇没变长,或者蛇变长了但尾巴没跟上,屏幕上看起来像多了一截断尾。

3.2 食物生成的随机性

51 没有硬件随机数,rand()又依赖srand()的种子。常见做法是用定时器计数值做种子:

void Food_Generate(void) { uint8_t valid = 0; while (!valid) { g_food.x = rand() % LCD_COLS; g_food.y = rand() % LCD_ROWS; valid = 1; // 食物不能生成在蛇身上 for (uint8_t i = 0; i < g_snake.len; i++) { uint8_t idx = (g_snake.tail + i) % MAX_SNAKE_LEN; if (g_snake.x[idx] == g_food.x && g_snake.y[idx] == g_food.y) { valid = 0; break; } } } }

rand()的种子一般在main开头用srand(Timer0_GetCount())设置,这样每次上电的食物位置不同。注意LCD_COLS和LCD_ROWS要按实际屏幕尺寸改,如果屏幕是 128x64,LCD_ROWS就是 64,但蛇的坐标范围要留出边界,否则食物可能生成在屏幕边缘导致蛇撞墙。

3.3 按键消抖与方向锁

按键扫描不能直接用if (P3_0 == 0),机械按键有 10ms 左右的抖动。这个项目用定时器时基做状态机消抖:

uint8_t Key_Scan(void) { static uint8_t key_state = 0; static uint16_t key_timer = 0; uint8_t key_val = 0; switch (key_state) { case 0: // 空闲,检测是否有键按下 if (KEY_UP == 0 || KEY_DOWN == 0 || KEY_LEFT == 0 || KEY_RIGHT == 0) { key_state = 1; key_timer = 0; } break; case 1: // 消抖确认 if (++key_timer >= 10) // 10ms { if (KEY_UP == 0) key_val = KEY_VAL_UP; else if (KEY_DOWN == 0) key_val = KEY_VAL_DOWN; else if (KEY_LEFT == 0) key_val = KEY_VAL_LEFT; else if (KEY_RIGHT == 0) key_val = KEY_VAL_RIGHT; key_state = 2; } break; case 2: // 等待释放 if (KEY_UP == 1 && KEY_DOWN == 1 && KEY_LEFT == 1 && KEY_RIGHT == 1) { key_state = 0; } break; } return key_val; }

方向锁是另一个容易忽略的点:蛇不能 180 度掉头。如果当前方向是右,按左键应该被忽略。这个判断放在Snake_ChangeDirection里:

void Snake_ChangeDirection(uint8_t key) { uint8_t new_dir = g_snake.dir; switch (key) { case KEY_VAL_UP: new_dir = 0; break; case KEY_VAL_DOWN: new_dir = 1; break; case KEY_VAL_LEFT: new_dir = 2; break; case KEY_VAL_RIGHT: new_dir = 3; break; } // 禁止反向:上不能转下,左不能转右 if ((g_snake.dir == 0 && new_dir == 1) || (g_snake.dir == 1 && new_dir == 0) || (g_snake.dir == 2 && new_dir == 3) || (g_snake.dir == 3 && new_dir == 2)) { return; } g_snake.dir = new_dir; }

这个逻辑必须在移动之前执行,否则会出现“按了左键但蛇先向右走了一步再掉头”的玄学现象。

4. 编译、烧录与上电验证:从 Keil 到开发板的完整链路

4.1 Keil 工程配置要点

打开Project/Snake.uvproj后,先确认三件事:芯片型号选的是不是你板子上的 51 型号(如 STC89C52、AT89S52),晶振频率填的是不是实际值(常见 11.0592MHz 或 12MHz),输出勾选了Create HEX File。这三项任何一项不对,烧录后要么不跑,要么时序全乱。

# 编译输出示例(Keil 的 Build Output 窗口) Build target 'Target 1' compiling main.c... compiling snake.c... compiling lcd12864.c... linking... Program Size: data=45.0 xdata=1024 code=3876 creating hex file from ".\Objects\Snake"... ".\Objects\Snake" - 0 Error(s), 0 Warning(s).

data=45.0是片内 RAM 占用,xdata=1024是片外 RAM 或显存占用。如果data超过 128 字节,51 会不够用,需要把大数组放到xdata或code里。code=3876是 Flash 占用,一般 8KB 的 51 够用。

4.2 烧录与接线检查

烧录用 STC-ISP 或对应的下载工具,选对 COM 口和波特率,冷启动上电。接线方面,重点检查三组线:LCD 的数据线和控制线(RS、RW、EN)、按键的四个引脚、以及晶振和复位电路。如果上电后屏幕全亮或全暗,先量 LCD 的对比度电压(V0),再检查初始化时序里的延时够不够。

提示:51 的 P0 口是开漏输出,驱动 LCD 数据线时必须接上拉电阻(通常 10K 排阻),否则数据写不进去,屏幕会显示乱码或全黑。

4.3 上电后的功能验证顺序

不要一上来就玩游戏,按这个顺序验证:先看屏幕有没有正确初始化(通常显示一个边框或欢迎界面),再按按键看屏幕有没有反应(比如光标移动),最后才让蛇动起来。如果蛇动了但按键没反应,问题在按键扫描或中断优先级;如果按键有反应但蛇不动,问题在定时器中断或速度阈值。

5. 避坑与排查:课程设计里最容易翻车的五个点

5.1 屏幕花屏或只显示一半

现象:上电后屏幕出现随机点阵,或者只有上半部分有显示。原因通常是显存数组越界,或者LCD_Refresh()的页循环写错了范围。解决:检查g_lcd_buffer的维度是否和屏幕页数一致,刷新时for (page = 0; page < LCD_PAGES; page++)的边界不要写成<=。

5.2 蛇移动速度忽快忽慢

现象:蛇有时候走得快,有时候走得慢,甚至卡顿。原因是g_game_tick在中断里累加,但主循环里判断速度时用了if (g_game_tick >= g_speed_level)而没有清零,或者中断里忘了关中断保护导致变量被读脏。解决:在中断里只做g_game_tick++,主循环里判断后立即g_game_tick = 0,并且g_game_tick用volatile修饰。

5.3 按键按了没反应或连跳

现象:按一下方向键,蛇转了两次;或者按住不放,蛇不动。原因是消抖状态机没有等待释放,或者Key_Scan()被主循环里的长延时阻塞了。解决:确保Key_Scan()每轮主循环都调用,且状态机在case 2里必须等到所有按键释放才回到case 0。

5.4 吃到食物后蛇身断裂

现象:蛇吃到食物后,长度增加了,但中间少了一节,看起来像断成两截。原因是grow_flag的处理顺序错了:先缩了尾巴再判断吃食物,导致那一帧尾巴被擦掉但新头已经画上。解决:在Snake_Move()里先判断新头是否和食物重合,设置grow_flag,再决定是否缩尾。

5.5 烧录后完全没反应

现象:下载成功但板子没任何显示。原因可能是晶振没起振、复位电路一直拉低、或者电源电压不够。解决:用示波器看晶振引脚有没有正弦波,量复位引脚电压是否在 4.5V 以上,检查 LCD 的背光是否亮(背光不亮不代表没跑,但能辅助判断电源)。

6. 二次开发怎么改:从贪吃蛇到其他小游戏的改造思路

这个工程最值钱的地方不是贪吃蛇本身,而是它把“定时器时基 + 非阻塞按键 + 显存刷新”这套骨架搭好了。你想改成俄罗斯方块,只需要换掉snake.c里的逻辑,保留timer.c、key.c、lcd12864.c不动。具体来说,俄罗斯方块需要的是一个 10x20 的网格、七种方块形状、行消除判断,这些都可以在User层新增一个tetris.c来实现,主循环里的调度结构完全复用。

改的时候注意三个约束。第一,51 的 RAM 有限,俄罗斯方块的网格用uint8_t grid[20]按位存储,每行 10 个格子用两个字节就够,不要用int数组。第二,方块的旋转用查表法,把七种方块每种四个方向的形状预先存在code区,运行时只查表不计算,省 CPU 也省 Flash。第三,速度控制沿用g_speed_level,但俄罗斯方块需要随分数递减,可以在Score_Update()里动态调整。

验证改造是否成功,我一般会走一遍这个清单:上电后屏幕是否先清屏再画边框;按键是否只响应一次;方块下落时是否和已有方块正确堆叠;满行是否消除并计分;游戏结束后是否能用复位键重新开始。这套清单走完,基本不会出现“玩到一半死机”的情况。

从那以后我每次拿到一个 51 小游戏工程,都强制先跑通它的最小显示和按键,再动逻辑代码。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询