简介:这份资源面向电子信息、自动化等专业的本科生及单片机初学者,提供一套基于51单片机的贪吃蛇游戏完整课程设计资料,可用于课程设计、毕业设计或自学练手。压缩包共18个文件,约102.42MB,涵盖C语言源码、A51汇编启动文件、Proteus仿真工程、Keil工程配置、HEX烧录文件,以及课程报告文档和PPT展示,从代码到仿真再到答辩材料一应俱全。资源完整呈现了贪吃蛇的核心逻辑:四方向键控制蛇移动,吃豆后蛇身加长且速度加快,撞墙或撞到自身即结束游戏,便于读者理解按键扫描、定时器刷新、LCD显示与碰撞检测的实现思路。目前已有2438人学习下载,读者可借助仿真文件直接运行验证效果,对照源码与报告梳理设计流程,快速完成从原理到成品的搭建,也可在此基础上扩展难度等级、计分与音效等功能。
1. 从一块 51 最小系统板说起:这套贪吃蛇课设到底要交什么
很多同学拿到「基于51单片机贪吃蛇游戏设计」这个题目时,第一反应是去网上抄一段点阵屏驱动,再拼一个按键扫描,能跑起来就算完事。但真正答辩被问住的,往往不是代码能不能跑,而是「你的游戏状态机怎么设计的」「蛇身坐标怎么存储」「撞墙判定和食物生成怎么保证不死循环」。这套方案要解决的,就是让一份单片机贪吃蛇课程设计从「能亮」变成「讲得清、改得动、测得完」。它适合正在做单片机课程设计、电子工艺实习或者本科毕设前期验证的同学,也适合需要一份完整可参考工程结构的从业者。整份资源围绕 51 单片机最小系统展开,核心模块包括显示驱动、按键输入、游戏逻辑和定时调度,最终交付的是一个可烧录、可演示、可答辩的完整工程。
2. 需求拆解与技术选型:为什么是 51 而不是 STM32
2.1 功能需求到底有几条
先把需求列清楚,不然后面写代码全是拍脑袋。一套合格的贪吃蛇课设,功能需求通常拆成下面这几条:
- 显示区域:至少 8×8 点阵,能显示蛇身、食物和边界,进阶一点用 16×16 或者 LCD12864。
- 输入方式:4 个独立按键控制上下左右,或者用矩阵键盘。
- 游戏逻辑:蛇身增长、食物随机生成、撞墙判定、撞自身判定、分数累计。
- 状态管理:开始界面、游戏中、暂停、游戏结束四个状态可切换。
- 速度控制:随分数增加或固定档位调整移动速度。
这几条看着简单,但每一条背后都有实现代价。比如 8×8 点阵只能显示 64 个像素点,蛇身一长就挤,所以很多方案会选 16×16 点阵或者 8×16 的 LCD 屏。选型的时候要把「显示精度」和「IO 口占用」放在一起权衡。
2.2 主控与显示选型的取舍
51 单片机型号很多,STC89C52RC 是最常见的选择,原因是 Flash 8KB、RAM 512B、IO 口 32 个,价格低、资料多、烧录方便。STM32 性能强很多,但对于贪吃蛇这种逻辑简单、刷新率要求不高的场景,属于杀鸡用牛刀,而且外设配置复杂度直接翻倍,课设周期内不一定调得完。
显示部分常见三种方案,对比如下:
| 方案 | 分辨率 | IO 占用 | 驱动难度 | 适用场景 |
|---|---|---|---|---|
| 8×8 点阵 | 64 点 | 8+8 或 3+8 | 中 | 基础版,蛇身短 |
| 16×16 点阵 | 256 点 | 16+16 或 4+16 | 高 | 完整版,显示清晰 |
| LCD12864 | 128×64 | 8 或 3 | 中 | 带字符,可显示分数 |
我一般会建议课设选 16×16 点阵配 74HC595 串转并,或者直接上 LCD12864。前者能体现扫描驱动功底,后者开发快、显示信息多。如果导师明确要求「必须用点阵」,那就老老实实做 16×16,别偷懒用 8×8,答辩时蛇一长就穿模,很难看。
2.3 定时器与刷新率的匹配
贪吃蛇的移动节奏靠定时器中断驱动,不能用 delay 硬等。通常用 Timer0 做 1ms 基准中断,在中断里累加计数,每 200ms 触发一次蛇的移动。刷新率则要更高,点阵扫描一般 1ms 扫一行,16 行就是 16ms 一帧,人眼看起来不闪。
这里有个容易翻车的点:如果移动定时和扫描定时共用一个定时器,中断里逻辑太长会导致扫描变慢、屏幕闪烁。稳妥做法是 Timer0 管扫描,Timer1 管游戏节拍,两个中断各干各的。
// Timer0 初始化:1ms 扫描基准 void Timer0_Init(void) { TMOD &= 0xF0; // 清除 T0 控制位 TMOD |= 0x01; // T0 工作方式 1,16 位定时 TH0 = (65536 - 1000) / 256; // 1ms 初值,12MHz 晶振 TL0 = (65536 - 1000) % 256; ET0 = 1; // 使能 T0 中断 EA = 1; // 开总中断 TR0 = 1; // 启动 T0 } // Timer1 初始化:10ms 游戏节拍基准 void Timer1_Init(void) { TMOD &= 0x0F; TMOD |= 0x10; // T1 工作方式 1 TH1 = (65536 - 10000) / 256; // 10ms 初值 TL1 = (65536 - 10000) % 256; ET1 = 1; TR1 = 1; }这段代码里,Timer0 每 1ms 进一次中断做行扫描,Timer1 每 10ms 进一次中断累加游戏节拍。参数改的时候注意晶振频率,12MHz 和 11.0592MHz 的初值不一样,算错了要么太快要么太慢。中断服务函数里只做标志位翻转和计数,具体逻辑放到主循环处理,这是 51 上跑游戏的基本纪律。
3. 总体架构设计:四个模块与一条数据流
3.1 模块划分与职责边界
整套工程按功能切成四块,每块单独一个 .c 文件,头文件暴露接口,内部实现不互相串:
display.c:负责点阵或 LCD 的扫描刷新,对外只提供Display_ShowFrame()和Display_Clear()。key.c:按键扫描与消抖,对外提供Key_GetDirection(),返回枚举值。game.c:核心状态机,管蛇身数组、食物坐标、碰撞判定、分数。main.c:初始化外设,主循环里调度状态机。
这样分的好处是,答辩时导师问「显示和逻辑怎么解耦的」,你能直接说「显示层只认帧缓冲,逻辑层只改坐标数组,中间靠一个刷新标志同步」。模块之间通过全局的帧缓冲数组通信,不直接调对方内部函数。
3.2 蛇身数据结构的选型
蛇身怎么存,直接决定代码复杂度。常见两种做法:
第一种是数组队列,用x[64]和y[64]两个数组存每节坐标,头指针和尾指针移动。优点是内存固定、访问快;缺点是长度上限写死。
第二种是链表,每节一个节点动态申请。51 的 RAM 只有 512B,动态内存容易碎片化,课设里不推荐。
我一般用数组队列,长度上限设 32 节,够用了。移动的时候,从尾到头依次把前一节的坐标赋给后一节,再更新头坐标。这样蛇身自然跟随,不用每节都算方向。
#define MAX_LEN 32 typedef struct { unsigned char x[MAX_LEN]; unsigned char y[MAX_LEN]; unsigned char len; // 当前长度 unsigned char dir; // 当前方向 0-3 } Snake_t; Snake_t snake; // 蛇移动一步:从尾向头覆盖坐标 void Snake_Move(void) { unsigned char i; for (i = snake.len - 1; i > 0; i--) { snake.x[i] = snake.x[i - 1]; snake.y[i] = snake.y[i - 1]; } // 根据方向更新头坐标 switch (snake.dir) { case 0: snake.y[0]--; break; // 上 case 1: snake.y[0]++; break; // 下 case 2: snake.x[0]--; break; // 左 case 3: snake.x[0]++; break; // 右 } }这段逻辑的关键是「先移身体再移头」,顺序反了蛇会瞬移。dir用 0 到 3 表示四个方向,避免在中断里做字符串比较。坐标范围要跟显示区域对齐,16×16 点阵就是 0 到 15,越界直接判撞墙。
3.3 状态机与主循环调度
游戏状态用枚举管理,主循环里用 switch 分发。状态切换靠按键和游戏事件触发,不能在中断里直接改状态,否则容易出现「中断里改了状态,主循环还在跑旧逻辑」的竞态。
typedef enum { STATE_MENU, // 开始界面 STATE_PLAY, // 游戏中 STATE_PAUSE, // 暂停 STATE_OVER // 结束 } GameState_t; GameState_t state = STATE_MENU; void main(void) { Timer0_Init(); Timer1_Init(); Display_Init(); Game_Init(); while (1) { switch (state) { case STATE_MENU: if (Key_GetDirection() != DIR_NONE) state = STATE_PLAY; break; case STATE_PLAY: if (tick_flag) { // Timer1 置位的节拍标志 tick_flag = 0; Game_Update(); // 移动、判定、生成食物 } break; case STATE_PAUSE: if (Key_GetDirection() == DIR_PAUSE) state = STATE_PLAY; break; case STATE_OVER: if (Key_GetDirection() == DIR_RESET) { Game_Init(); state = STATE_MENU; } break; } Display_ShowFrame(); // 每轮循环刷新一次 } }主循环里tick_flag由 Timer1 中断置位,主循环检测到才更新游戏逻辑,这样游戏速度和主循环执行时间解耦。Display_ShowFrame()放循环末尾,保证每轮都刷。注意Game_Update()里要做碰撞判定,撞墙或撞自身就切STATE_OVER。
4. 关键模块实现:从食物生成到碰撞判定
4.1 食物随机生成与死循环规避
食物生成看着简单,坑最多。常见错误是直接rand() % 16生成坐标,不检查是否落在蛇身上,结果食物被蛇挡住,玩家以为游戏卡死。
正确做法是循环随机直到找到空位,但要加最大尝试次数,防止蛇占满屏幕时死循环。
// 在空位生成食物,最多尝试 100 次 void Food_Generate(void) { unsigned char tx, ty, i, tries = 0; unsigned char occupied; do { tx = rand() % 16; ty = rand() % 16; occupied = 0; for (i = 0; i < snake.len; i++) { if (snake.x[i] == tx && snake.y[i] == ty) { occupied = 1; break; } } tries++; } while (occupied && tries < 100); if (tries < 100) { food.x = tx; food.y = ty; } else { // 找不到空位,说明蛇已占满,判胜利 state = STATE_OVER; } }tries上限设 100 是经验值,16×16 共 256 格,蛇长 32 时随机命中空位概率很高,100 次足够。如果真占满,直接判胜利比死循环体面。rand()需要先srand()播种,可以用定时器初值做种子,保证每次上电食物位置不同。
4.2 碰撞判定的三个分支
碰撞判定分三种:撞墙、撞自身、吃到食物。三个分支要按顺序判,先判撞墙再判撞自身,最后判吃食物。
void Game_Update(void) { unsigned char head_x, head_y, i; Snake_Move(); head_x = snake.x[0]; head_y = snake.y[0]; // 分支一:撞墙 if (head_x > 15 || head_y > 15) { state = STATE_OVER; return; } // 分支二:撞自身,从头后一节开始比 for (i = 1; i < snake.len; i++) { if (snake.x[i] == head_x && snake.y[i] == head_y) { state = STATE_OVER; return; } } // 分支三:吃到食物 if (head_x == food.x && head_y == food.y) { if (snake.len < MAX_LEN) snake.len++; score += 10; Food_Generate(); } }撞自身判定要从i = 1开始,因为i = 0是头自己,永远相等。吃到食物时长度加一,但要注意数组上限,超了就不加,否则越界写会踩内存。score可以显示在 LCD 上,点阵屏的话用 LED 或数码管辅助显示。
4.3 按键消抖与方向互斥
按键用独立按键接 IO 口,按下为低电平。消抖用 10ms 延时或者定时器计数,别用delay_ms(10)阻塞主循环,否则游戏会卡顿。
方向互斥是指蛇不能 180 度掉头。当前方向是「上」时,按「下」要忽略,否则蛇直接撞自己。
// 方向互斥检查 unsigned char Key_GetDirection(void) { unsigned char raw = Key_Scan(); // 返回原始键值 if (raw == DIR_NONE) return DIR_NONE; // 上下互斥、左右互斥 if ((snake.dir == 0 && raw == 1) || (snake.dir == 1 && raw == 0) || (snake.dir == 2 && raw == 3) || (snake.dir == 3 && raw == 2)) { return DIR_NONE; } return raw; }Key_Scan()里做消抖和键值映射,返回 0 到 3 对应四个方向。互斥判断放在Key_GetDirection()里,游戏逻辑层不用关心。这样改方向规则时只动一个函数,不会牵连一片。
5. 避坑与常见问题排查
5.1 点阵显示闪烁或重影
现象:屏幕整体发暗,或者蛇身移动时有拖影。
原因:扫描频率太低,或者行扫描和列数据更新不同步。常见于在中断里做太多逻辑,导致扫描周期被拉长。
解决:把扫描中断里的代码压到最短,只做行选和列数据输出,游戏逻辑全部挪到主循环。扫描频率保持在 1ms 一行,16 行 16ms 一帧,低于 50Hz 人眼就会觉得闪。
5.2 蛇移动速度忽快忽慢
现象:游戏跑一会儿变快,或者按键时卡顿。
原因:游戏节拍和主循环耦合,主循环里做了耗时操作(比如 LCD 刷屏、长延时),导致tick_flag被漏检或堆积。
解决:游戏节拍用独立定时器置标志位,主循环只检测标志位,不直接调延时。所有耗时操作拆成小步,分多轮循环完成。
5.3 食物生成在蛇身上
现象:蛇吃到食物后长度没变,或者食物看不见。
原因:随机生成没做占用检查,或者检查时蛇身数组长度没更新。
解决:按 4.1 的写法加占用检查和尝试上限。注意snake.len要在吃食物后立即更新,再调Food_Generate(),顺序反了会漏检新长出来的那节。
5.4 按键不灵敏或连按
现象:按一下方向键蛇走两格,或者按了没反应。
原因:消抖时间太短或太长,或者按键扫描被游戏逻辑阻塞。
解决:消抖用 10 到 20ms,用定时器计数实现,别用阻塞延时。按键扫描放主循环开头,保证每轮都扫。如果按键接 IO 口没加上拉电阻,浮空输入会乱跳,硬件上补 10K 上拉。
5.5 烧录后不运行或跑飞
现象:程序烧进去没反应,或者跑几秒复位。
原因:看门狗没关、堆栈溢出、中断嵌套太深。
解决:51 默认看门狗关着,但如果用了带看门狗的型号要手动关。堆栈溢出常见于中断里调函数太深,把中断服务函数写成只置标志位。中断嵌套用IP寄存器设优先级,别让两个中断互相打断。
6. 验收与答辩:怎么证明这套设计真的跑通了
验收不是「能亮就行」,导师要看的是你对自己代码的理解程度。我一般会按下面这张表准备验收材料,每一项都能当场演示或翻代码指出来:
| 验收项 | 演示方式 | 对应代码位置 |
|---|---|---|
| 显示刷新 | 上电显示开始界面 | display.c 扫描中断 |
| 方向控制 | 四键各按一次,蛇转向正确 | key.c 消抖与互斥 |
| 食物生成 | 连续吃 5 个食物,位置不重叠 | game.c Food_Generate |
| 碰撞判定 | 主动撞墙、撞自身,均进结束态 | game.c Game_Update |
| 速度节拍 | 观察蛇移动节奏稳定 | Timer1 中断与 tick_flag |
| 状态切换 | 开始、暂停、结束、重置全走一遍 | main.c 状态机 |
答辩时最容易被问的三个问题,提前准备好答案:
第一个是「蛇身为什么用数组不用链表」。答:51 的 RAM 只有 512B,动态内存容易碎片化,数组队列内存固定、访问 O(1),长度上限 32 节对 16×16 点阵足够。
第二个是「游戏速度和主循环怎么解耦的」。答:Timer1 每 10ms 置一次tick_flag,主循环检测到才更新逻辑,主循环执行时间不影响游戏节拍。
第三个是「食物生成会不会死循环」。答:有尝试次数上限,超了判胜利,不会卡死。
演示的时候有个小技巧:先把蛇故意撞墙,展示结束态;再重置,连续吃几个食物展示增长;最后按暂停再恢复,展示状态机完整。整套流程走下来两分钟,比干讲代码有说服力。
从那以后我每次带课设验收,都强制让学生先跑一遍这张表,再翻代码对应位置。能指着代码说清楚每一行在干嘛的,基本不会被问倒;只会说「网上抄的」的,一问就露馅。希望帮到你。
本文还有配套的精品资源,点击获取