简介:这份资源面向电子信息、自动化等专业的本科学生及单片机初学者,提供一套完整的51单片机贪吃蛇游戏课程设计实现方案,可用于课程设计、毕业设计参考或单片机入门练手。压缩包共18个文件,约102.42MB,包含C语言源码、STARTUP.A51启动文件、hex烧录文件、Proteus仿真工程(pdsprj)、Keil工程文件(uvproj、uvopt)以及课程报告文档、演示PPT等,覆盖从代码编写、仿真验证到报告撰写的完整流程。资源中附带演示视频与课程报告,能帮助读者理解方向键控制蛇移动、吃豆加长加速、撞墙或撞自身判定结束等核心逻辑,并对照仿真工程快速复现运行效果。目前已有2438人学习下载,适合需要完整课设方案、仿真与代码参考的同学直接借鉴使用。
1. 从一块 51 最小系统板说起:这份贪吃蛇资源到底能跑出什么效果
很多人第一次拿到“基于51单片机贪吃蛇游戏设计.zip”这类资源,第一反应是“这不就是个课程设计作业吗”,然后解压、打开 Keil、点编译,发现一堆报错,或者烧进去屏幕不亮、按键没反应,最后扔进回收站。我当年也干过同样的事。但后来带过几届学生的课设才发现,这份资源真正的价值不在“贪吃蛇”三个字,而在于它把 51 单片机做交互式图形项目的一整套链路都串起来了:定时器扫描、行列按键消抖、点阵/液晶驱动、状态机切换、随机数生成、蜂鸣器提示。你把它跑通一遍,等于把 51 的定时器、中断、IO 口操作全过了一遍,这比单独点个 LED 有意义得多。
这份资源适合三类人:正在做单片机课程设计、需要一份能改能交差的完整工程的学生;想从 51 入门嵌入式、但不想只点灯就想做个“看得见反馈”的小项目的新手;以及需要一份带注释源码当模板、往里面塞自己逻辑的开发者。它不解决高深算法问题,但它能让你在半天内看到一个能玩、能改、能讲的完整系统。下面我按“先看懂结构、再动手复现、最后避坑和进阶”的顺序,把这份资源拆开讲清楚。
2. 拆包看结构:源码、原理图和烧录文件怎么对应
2.1 压缩包里通常有什么,先别急着编译
这类课程设计资源解压后一般包含几个固定部分:Keil 工程目录(.uvproj 或 .uvprojx)、C 源码文件(main.c、lcd.c、key.c、snake.c 之类)、头文件、原理图(SchDoc 或 PDF)、可能还有一份 Word 版设计报告和 hex 烧录文件。我拿到手的第一件事不是双击工程,而是先看目录树和文件时间戳,判断这份资源是“能直接跑的完整工程”还是“只给了源码、库文件缺失”的半成品。
常见做法是:先找 .uvprojx 文件,用 Keil 打开,看左侧 Project 栏里有没有缺失文件(带黄色感叹号)。如果缺,去压缩包里找同名 .c 或 .h 手动添加。然后看 Options for Target 里的晶振频率设置,51 项目常见是 11.0592MHz 或 12MHz,这个值直接影响定时器初值计算,设错了蛇的移动速度会离谱。最后看 Output 选项卡有没有勾选“Create HEX File”,没有的话编译完找不到烧录文件。
提示:如果压缩包里只有 .c 没有工程文件,别慌,新建一个 Keil C51 工程,选对应芯片型号(如 AT89C51、STC89C52),把 .c 文件加进去,再手动配置晶振和输出 HEX 即可。
2.2 硬件连接:液晶、按键、蜂鸣器的引脚映射
资源里最容易被忽略的是引脚定义。贪吃蛇项目通常用 8x8 或 16x16 LED 点阵,或者 12864 液晶、1602 液晶加自定义字符。不同方案引脚占用差别很大。我一般先打开源码里的头文件,找类似#define LCD_DATA P0、sbit KEY_UP = P3^2这样的宏,把引脚分配抄到纸上,再对照原理图确认。
以常见的 12864 液晶方案为例,数据口一般接 P0,控制线 RS、RW、EN 接 P2 的某几位,按键接 P3 的独立引脚或矩阵。如果源码里用的是矩阵按键,还要看行线和列线分别接哪个口。这一步不做,烧进去大概率白屏或者按键乱跳。我见过一份资源,源码里按键定义是 P3^0 到 P3^3,但原理图上实际接的是 P1 口,学生照着焊板子,结果按键全失灵,查了一晚上才发现是资源本身引脚定义和原理图不一致。
2.3 编译与烧录:从 Keil 到单片机的完整链路
确认工程完整、引脚对应后,编译一般不会有大问题。但 51 项目常见的编译报错集中在两处:一是REG51.H或REG52.H找不到,说明 Keil 的器件库没装好;二是main.c里调用了未包含的头文件函数,比如delay()没声明。前者重装 C51 包,后者在文件顶部补#include或函数声明即可。
编译通过后,用 STC-ISP 或类似工具烧录。如果是 STC 系列,注意冷启动时机:点“下载”后再给单片机上电。如果是 AT 系列,需要专用编程器。烧录成功后,液晶应该显示蛇和食物,按键能控制方向。如果屏幕亮但没内容,先调液晶对比度电位器;如果蛇不动,查定时器初值;如果蛇移动方向错乱,查按键扫描逻辑。
// 典型定时器0初始化,用于控制蛇移动速度 void Timer0_Init() { TMOD &= 0xF0; // 清除T0控制位 TMOD |= 0x01; // T0工作方式1,16位定时器 TH0 = 0xFC; // 初值,对应约1ms@12MHz TL0 = 0x18; ET0 = 1; // 使能T0中断 EA = 1; // 开总中断 TR0 = 1; // 启动T0 }上面这段代码里,TH0和TL0的初值决定了中断周期。12MHz 晶振下,机器周期 1us,16 位定时器最大计 65536us,要得到 1ms 中断,初值 = 65536 - 1000 = 64536 = 0xFC18。如果晶振换成 11.0592MHz,同样的初值中断周期会变成约 1.085ms,蛇的移动速度会慢一点点,肉眼几乎看不出,但如果你用定时器做精确计时,这个偏差会累积。所以改晶振后最好重新算初值。
3. 核心逻辑怎么改:蛇的移动、食物生成和碰撞检测
3.1 用数组还是链表:51 内存下的取舍
贪吃蛇的蛇身存储方式直接决定代码复杂度和内存占用。51 单片机内部 RAM 只有 128 或 256 字节,外部扩展另说。常见做法是用一个二维数组unsigned char snake[20][2]存每节坐标,蛇头索引和蛇尾索引循环移动。这种“循环队列”方式最省内存,也最容易在 51 上跑起来。链表虽然灵活,但每个节点要存指针,在 51 上指针占 2 字节,加上数据,内存开销反而大,而且动态分配容易产生碎片,不建议在 51 项目里用。
我一般会先确定最大蛇长。如果屏幕是 16x16 点阵,最多 256 个格子,蛇最长也就 256 节,但实际游戏不会那么长。用#define MAX_SNAKE 64定义上限,数组占 64*2=128 字节,刚好卡在 51 内部 RAM 边缘。如果不够,可以把坐标压缩到一个字节里(高 4 位行、低 4 位列),这样 64 节只占 64 字节。
#define MAX_SNAKE 64 unsigned char snake[MAX_SNAKE][2]; // 每节存 row, col unsigned char snake_len; // 当前蛇长 unsigned char head_idx, tail_idx; // 循环队列头尾索引 // 蛇移动:新头覆盖旧尾,实现“不增长时整体前移” void Snake_Move(unsigned char new_row, unsigned char new_col) { head_idx = (head_idx + 1) % MAX_SNAKE; snake[head_idx][0] = new_row; snake[head_idx][1] = new_col; if (snake_len < MAX_SNAKE) { snake_len++; } else { tail_idx = (tail_idx + 1) % MAX_SNAKE; // 丢弃旧尾 } }这段逻辑的关键在于:蛇的移动不是真的把每一节往前挪,而是头索引加一、写入新坐标,尾索引在蛇长达到上限后也加一。这样每次移动只改两个索引和两个坐标,效率极高。head_idx和tail_idx始终在 0 到 MAX_SNAKE-1 之间循环,不会越界。注意snake_len在未达上限时递增,达到上限后保持不变,此时尾索引开始追赶头索引。
3.2 食物随机生成:别让 rand() 把你坑了
51 的rand()函数需要stdlib.h,但标准库的随机数种子默认是 1,每次上电食物位置一模一样。常见做法是用定时器计数值做种子,比如在游戏开始前读TH0和TL0拼成一个种子传给srand()。但更简单的办法是:在等待按键开始的那段时间里,不断读取定时器低字节,玩家按下开始键的瞬间,这个值就是随机的。
食物生成还要避免两个坑:一是生成在蛇身上,二是生成在屏幕外。我一般写一个循环,随机出坐标后检查是否与蛇身重合,重合就重新随机,直到合法为止。51 上这个循环不会跑太久,因为蛇身占的格子比例不大。但要注意,如果蛇快占满屏幕,这个循环可能长时间出不来,所以最好加一个尝试次数上限,超过就遍历所有空格子选一个。
// 食物生成:带蛇身冲突检测和尝试上限 void Food_Generate() { unsigned char r, c, i, tries = 0; do { r = rand() % ROWS; c = rand() % COLS; tries++; if (tries > 200) { // 防止死循环 for (r = 0; r < ROWS; r++) for (c = 0; c < COLS; c++) if (!Snake_Occupies(r, c)) goto found; } } while (Snake_Occupies(r, c)); found: food_row = r; food_col = c; }Snake_Occupies函数遍历蛇身数组,判断给定坐标是否被占用。tries上限设为 200 是经验值,实际在 16x16 屏幕上,蛇长 50 节时随机冲突概率约 20%,200 次尝试几乎不可能失败。如果真到了蛇占满屏幕的情况,说明游戏已经赢了,应该触发胜利逻辑而不是继续生成食物。
3.3 碰撞检测:撞墙、撞自己、吃食物三种分支
碰撞检测在每次蛇头移动后立即执行。撞墙判断很简单:新坐标超出屏幕范围就游戏结束。撞自己则要遍历蛇身,看新头坐标是否与某一节重合。注意,蛇尾那一节在移动后会消失,所以如果新头正好移到当前蛇尾的位置,不算撞自己。这个细节很多资源没处理好,导致蛇贴着尾巴走会误判死亡。
吃食物判断:新头坐标等于食物坐标,则蛇长加一(不删尾),食物重新生成,分数加一。如果没吃到,正常移动(删尾)。这三个分支在代码里通常写成一个switch或if-else链,顺序很重要:先判撞墙,再判撞自己,最后判吃食物。如果先判吃食物,蛇头撞墙的同时又碰到食物,会先加分再死亡,逻辑就乱了。
// 蛇头移动后的碰撞处理 unsigned char Snake_Update(unsigned char new_row, unsigned char new_col) { if (new_row >= ROWS || new_col >= COLS) return 0; // 撞墙 if (Snake_Occupies(new_row, new_col)) { // 检查是否是蛇尾(蛇尾即将移走,不算撞) if (!(new_row == snake[tail_idx][0] && new_col == snake[tail_idx][1])) return 0; // 撞自己 } if (new_row == food_row && new_col == food_col) { Snake_Move(new_row, new_col); // 不删尾,蛇变长 Food_Generate(); score++; } else { Snake_Move(new_row, new_col); // 正常移动,删尾 } return 1; // 存活 }这段代码里,Snake_Move内部已经处理了删尾逻辑,所以吃食物时只需要让snake_len不达到上限即可。但注意,如果蛇已经达到 MAX_SNAKE,即使吃到食物也不会再变长,此时应该把食物当作普通移动处理,否则尾索引会出错。实际项目中我会在吃食物分支里加一个判断:只有snake_len < MAX_SNAKE时才不删尾。
4. 避坑与排查:烧进去不亮、按键乱跳、蛇跑飞了怎么办
4.1 现象:液晶白屏或全黑,没有任何显示
原因通常有三个:对比度电位器没调好、数据口接反、初始化时序不对。先调电位器,这是最常见的。如果调了没用,用万用表测数据口电压,看是否有变化。如果数据口一直高或一直低,检查排线是否虚焊。初始化时序问题多见于 12864,有些模块需要延时足够长才能接收命令,源码里的delay函数如果被优化掉,初始化就会失败。解决办法是在delay函数前加volatile或者用_nop_()空操作。
4.2 现象:按键按了没反应,或者一按就连续触发
没反应先查引脚定义和原理图是否一致,这是血泪经验。一按连续触发是消抖没做好。51 项目里按键消抖常见两种:硬件加电容,软件延时 10ms 后重读。如果源码里只有if (KEY == 0)就直接执行,没有延时重读,那按键抖动会导致一次按下被识别成多次。我一般会在按键扫描函数里加 10ms 延时,再确认一次电平,确认后再等按键释放,这样最稳。
4.3 现象:蛇移动速度忽快忽慢,或者直接飞出屏幕
速度不稳通常是定时器中断里做了太多事情,比如在中断里刷新液晶。液晶刷新耗时较长,放在中断里会阻塞其他中断,导致定时不准。正确做法是定时器中断只置一个标志位,主循环里检测标志位再执行蛇移动和刷新。蛇飞出屏幕一般是边界判断写错,比如用了>而不是>=,或者行列最大值搞反。检查ROWS和COLS的定义,以及Snake_Update里的边界条件。
4.4 现象:食物生成在蛇身上,或者游戏开始蛇就死了
食物生成在蛇身上说明冲突检测没生效,检查Snake_Occupies函数是否遍历了所有蛇节,包括头尾。游戏开始蛇就死,多半是初始蛇身坐标和初始移动方向冲突,比如蛇头初始位置在边界上,第一次移动就撞墙。把初始蛇身放在屏幕中央,初始方向设为向右或向下,留出足够空间。
4.5 现象:编译通过但烧录后没反应,重新上电也不行
先确认烧录工具是否识别到芯片,如果识别不到,检查串口线、电源、晶振。如果识别到但烧录失败,看芯片是否被加密,需要先擦除。烧录成功后没反应,用示波器测晶振引脚是否有波形,没有波形说明晶振没起振,换晶振或匹配电容。如果晶振正常,测复位引脚电压,复位电路有问题也会导致程序不跑。
5. 进阶玩法:把贪吃蛇改成可配置的速度等级和双人对战
5.1 速度等级:用定时器重载值做档位切换
原版贪吃蛇速度固定,玩久了会腻。改速度等级最简单的方法是准备一个初值数组,根据等级选择不同的TH0/TL0重载值。比如等级 1 对应 20ms 中断,等级 5 对应 5ms 中断。在定时器中断服务程序里,每次重载时根据当前等级查表赋值。注意,等级切换最好在游戏开始前或暂停时进行,游戏中切换会导致蛇突然加速,容易撞墙。
// 速度等级对应的定时器重载值(12MHz,方式1) unsigned int code speed_table[5] = { 0xFC18, // 1ms 0xFE0C, // 0.5ms 0xFF06, // 0.25ms 0xFF83, // 0.125ms 0xFFC1 // 0.0625ms }; unsigned char speed_level = 0; // 0~4 void Timer0_ISR() interrupt 1 { TH0 = speed_table[speed_level] >> 8; TL0 = speed_table[speed_level] & 0xFF; move_flag = 1; // 主循环检测此标志 }这个表里,0xFC18对应 1ms,0xFE0C对应约 0.5ms,依次减半。实际游戏中,蛇每移动一格需要多个中断周期,所以速度等级影响的是移动间隔,不是中断周期本身。我一般会在主循环里用一个计数器,每收到 N 次move_flag才移动一次蛇,N 根据等级调整。这样中断周期固定,移动速度通过软件分频控制,更灵活。
5.2 双人对战:两个方向键和两条蛇的碰撞逻辑
双人对战需要两套按键和两条蛇的数组。51 的 IO 口有限,如果原来用独立按键,现在要再加四个方向键,可能不够用。常见做法是改用矩阵按键,或者用两个摇杆模块。两条蛇的碰撞检测除了各自的撞墙、撞自己,还要加互撞判断:蛇 A 的头碰到蛇 B 的身体,蛇 A 死;两条蛇头相撞,同归于尽。食物可以共用,谁吃到谁加分。
实现时,把原来的单蛇逻辑封装成函数,传入蛇的数组指针和长度指针,这样两条蛇共用同一套移动和碰撞代码。主循环里依次更新两条蛇,注意更新顺序会影响互撞判定结果,一般先更新玩家 1 再更新玩家 2,但判定时要同时检查两条蛇的新位置。这个改动代码量不大,但能让一个课设项目从“及格”变成“优秀”,我见过不少学生靠这个加分。
5.3 验证方法:用 Keil 仿真和串口打印看内部状态
改完代码别急着烧板子,先用 Keil 的软件仿真跑一遍。在Snake_Update和Food_Generate里设断点,看蛇的坐标、长度、食物位置是否符合预期。如果条件允许,加一个串口打印函数,把关键变量发到电脑上看。51 的串口配置简单,SCON = 0x50,TMOD设好波特率,用printf重定向到串口即可。这样调试比盯着液晶猜快得多。
从那以后我每次改完 51 项目,都强制走一遍“仿真看变量、串口打日志、再烧板子”的流程,省下来的时间够多打两把游戏。希望帮到你。
本文还有配套的精品资源,点击获取