ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

C51单片机入门实战:从零实现8x8点阵贪吃蛇

C51单片机入门实战:从零实现8x8点阵贪吃蛇 简介这是一份基于C51单片机8051系列的贪吃蛇小游戏完整工程资源面向单片机初学者和嵌入式硬件爱好者可作为课程设计或大作业参考也能帮助理解从C51编程到硬件控制的完整流程。包内共30个文件压缩包仅66KB含6个C源码、头文件、启动文件、Keil工程文件、hex烧录文件及obj/lst/m51等中间文件结构清晰方便直接编译和烧录。已有1027人浏览学习适合想快速上手51单片机游戏项目、学习定时器、中断、IO操作与算法设计的开发者。通过该工程可掌握C51语法、定时器控制游戏帧率、按键检测、蛇的移动与碰撞判定、食物随机生成以及Keil编译与在线调试的流程是理解嵌入式系统开发的良好实践。 把 C51 单片机从“点亮一颗 LED”带到“能完整玩一局贪吃蛇”中间隔的其实不是多少行代码而是有没有一套完整的设计思路。我是在实验室跟一个学弟聊毕设选题时重新做了一遍这个项目的起因很简单——他流水灯、数码管、按键中断都练过但真让他独立做一个带完整闭环的小项目他完全不知道从哪里下手。贪吃蛇恰好是这样一个项目硬件上只需要单片机、8×8 点阵、四个按键和几个电阻软件上却能覆盖定时器、中断、动态扫描、状态机、坐标换算、随机数、碰撞检测这些入门阶段最该吃透的东西。这篇就按我自己从零搭的过程来写适合学完基础例程、准备做第一个完整项目的朋友也适合蓝桥杯国赛前拿它熟悉整套流程的人。1. 为什么用 C51 做贪吃蛇一个“小游戏”背后的需求拆解先说明一个很多人忽略的事实贪吃蛇在电脑上写用链表加图形库也就是一两百行的事为什么非要拿到 8 位单片机上折腾因为在电脑上写重点是“游戏”在 C51 上写重点是“系统”。这颗芯片的 CPU 在 12MHz 晶振下实际指令周期只有 1MHz内部 RAM 只有 128 字节没有操作系统没有现成的图形库所有任务都要自己排时间表——这个约束才是项目的核心价值。1.1 把游戏需求翻译成单片机任务贪吃蛇表面上有三件事显示画面、接收方向、更新蛇身。落到单片机上对应关系是这样的显示画面 → 8×8 LED 点阵的动态扫描需要定时刷新行数据接收方向 → 独立按键的扫描与防抖需要周期性读取 IO更新蛇身 → 游戏逻辑和碰撞检测需要按固定节拍推进。这三件事不能全挤在死循环里顺序执行也不能互相用 delay 阻塞否则会出现按键没响应、画面闪烁这类问题。所以项目的第一步就是拆时间片把“游戏逻辑”“按键扫描”“显示刷新”安排成三个并行的小任务。1.2 一份可以直接照抄的硬件清单我用的主控是 STC89C52RC兼容标准 8051 指令集Keil 里选 AT89S52 或 STC89C52 都能编译。硬件清单如下主控STC89C52RC 或 AT89S52DIP40 封装显示8×8 单色点阵模块共阴或共阳均可代码里留极性宏输入4 个独立按键分别控制上、下、左、右再加 1 个复位键晶振12MHz 陶瓷晶振或直插晶振配两个 30pF 电容辅助10kΩ 按键上拉排阻、1 个 10μF 电解电容做复位、若干跳线。需要说明的是如果用 P0 口驱动点阵的列线务必外接 4.7kΩ10kΩ 上拉电阻。标准 51 的 P0 口是开漏输出不加上拉高电平驱动能力非常弱点阵亮度会明显发暗Proteus 仿真里还不太容易看出来实物的坑尤其明显。2. 硬件选型为什么选 8×8 点阵加独立按键而不是 LCD1602做显示方案时网上大量例程用 LCD1602 显示贪吃蛇比如把蛇身画成自定义字符。这个方案能做但我劝你第一个版本别这么做。LCD1602 本身是字符屏一行显示 16 个字符一个字符要占 5×8 的点阵想画一个连续的、会移动的蛇身你得不断刷新整个字符缓冲区代码复杂度并不低而且画面视觉上非常“块状”谈不上游戏感。8×8 点阵就不一样它本身就是像素矩阵蛇身每格对应一个 LED移动就是改一个坐标点的亮灭思维模型和游戏逻辑完全一致。缺点是 I/O 占用偏多——16 个引脚初学者直接用两个 8 位口驱动是最容易理解的方案后面熟练了再换成 74HC595 串转并来省引脚。2.1 点阵动态扫描的驱动逻辑8×8 点阵内部是 8 行 8 列共 64 个 LED。以共阴模块为例给某一行接高电平、某一列接低电平交叉点的 LED 就会点亮。如果你一个个地点64 个点根本忙不过来实际做法是“逐行扫描”每次只点亮一行这一行里需要亮的列就拉低其他行全部关断然后切到下一行。只要每秒刷新的次数超过 50 次人眼因为视觉暂留会看到一整幅完整的画面。我用定时器中断做扫描每 2ms 切换一行8 行一共 16ms 扫完一轮刷新率约 62Hz画面稳定不闪烁。如果扫得太快比如低于 50Hz画面会明显闪扫得太快又没有意义因为每行点亮时间缩短亮度反而下降。2.2 引脚分配与最小系统电路我这次的引脚分配很简单适合直接照抄功能引脚说明点阵行选P2.0P2.7逐行输出高电平点阵列数据P0.0P0.7低电平点亮P0 需上拉方向键上P3.0独立按键低电平有效方向键下P3.1同上方向键左P3.2同上方向键右P3.3同上蜂鸣器P1.0可选用于吃食音效按键接法是最常见的一端接 GND另一端接 IO 口并接上拉到 VCC按下时 IO 变成低电平。这样在程序里读引脚时默认读到高电平按键触发读低电平判断逻辑简单。晶振选择 12MHz是因为 12MHz 下机器周期刚好是 1μs定时器初值计算非常方便。3. 软件骨架时间片轮询如何同时管住刷新、按键和游戏逻辑很多初学者的第一个完整项目通常是在 main 函数里写一个大死循环每步都 delay 一下。比如游戏逻辑处理完 delay 200ms再扫描按键。这个写法在贪吃蛇项目里会立刻暴露问题你按方向键时主循环可能正卡在 delay 里按键要等几百毫秒才被响应游戏体验非常差。3.1 前后台结构与时基的产生我用的是典型的前后台结构定时器中断作为“前台”负责产生稳定的时间片main 死循环作为“后台”只在标志位置位的时候执行对应任务。这样按键扫描每隔固定时间执行一次游戏逻辑每隔固定节拍执行一次显示扫描用最高的频率刷新三者互不阻塞。定时器 0 配置为方式 1也就是 16 位定时器。12MHz 晶振下机器周期 1μs我要 2ms 中断一次初值计算如下// 2ms 2000 个机器周期 // 初值 65536 - 2000 63536 0xF830 TMOD 0x01; // 定时器0方式1 TH0 0xF8; TL0 0x30; ET0 1; // 开定时器0中断 EA 1; // 开总中断 TR0 1; // 启动定时器中断里除了切换点阵扫描行还放了两个计数器累计 10 次中断20ms置位“按键扫描”标志累计 250 次中断500ms置位“游戏移动”标志。移动节拍后面要做加速所以这个 500ms 可以改成全局变量每吃到一个食物减少一点。void t0_isr(void) interrupt 1 { static unsigned char row 0; unsigned char tick; TH0 0xF8; TL0 0x30; // 重装初值 // 点阵下一行扫描 row (row 1) 0x07; set_row(row); write_col(map_buffer[row]); // 时间片计数 if (tick_scan 10) { tick_scan 0; key_flag 1; } if (tick_move move_speed) { tick_move 0; move_flag 1; } }3.2 主循环里只做慢任务主循环看起来非常干净所有快速操作都在中断里完成了main 里只处理事件void main(void) { sys_init(); game_init(); while (1) { if (key_flag) { key_flag 0; key_proc(); } if (move_flag) { move_flag 0; game_update(); } } }这种结构的最大好处是按键扫描和游戏逻辑永远以固定的时间周期执行不会因为某次游戏逻辑运算较长而影响下一帧的响应。这也是之后学 RTOS 前最好的一课——时间片轮询本身就是最轻量的多任务思想。后面你换 STM32、用定时器中断做调度思路完全一样。4. 贪吃蛇核心算法蛇身存储、食物生成与碰撞检测的 C51 实现硬件和骨架搭好后真正有趣的游戏逻辑就来了。C51 的资源有限算法必须为单片机“定制”不能直接把电脑上的写法搬过来。4.1 蛇身存储静态数组不用链表电脑上做贪吃蛇很多人会用链表因为蛇身长度动态变化增删节点方便。但在 51 单片机上我明确建议用静态数组原因有两个一是 128 字节的 RAM 很小链表每个节点要存数据加指针开销明显二是 C51 的 malloc 支持并不方便而且动态内存容易产生碎片在一个游戏项目里完全没有必要。我的数据结构是这样#define MAP_W 8 #define MAP_H 8 #define MAX_SNAKE_LEN 32 unsigned char snake_x[MAX_SNAKE_LEN]; unsigned char snake_y[MAX_SNAKE_LEN]; unsigned char snake_len; unsigned char food_x, food_y; unsigned char g_dir; // 0上 1下 2左 3右坐标用 unsigned char正好装下 07 的范围。蛇头在数组下标 0蛇尾在 snake_len-1。移动时整体向前挪一位把新蛇头填入数组头部如果没吃到食物就直接把最后一个元素丢弃。4.2 移动、食物生成与碰撞检测的代码移动函数的核心逻辑如下void game_update(void) { unsigned char i; signed char dx, dy; switch (g_dir) { case 0: dx 0; dy -1; break; case 1: dx 0; dy 1; break; case 2: dx -1; dy 0; break; default: dx 1; dy 0; break; } // 计算新蛇头坐标 char new_x (signed char)snake_x[0] dx; char new_y (signed char)snake_y[0] dy; // 撞墙检测 if (new_x 0 || new_x MAP_W || new_y 0 || new_y MAP_H) { game_over(); return; } // 吃食物判断 unsigned char eat (new_x food_x new_y food_y) ? 1 : 0; // 蛇身前移 if (eat) { for (i snake_len; i 0; i--) { snake_x[i] snake_x[i - 1]; snake_y[i] snake_y[i - 1]; } snake_len; } else { for (i snake_len - 1; i 0; i--) { snake_x[i] snake_x[i - 1]; snake_y[i] snake_y[i - 1]; } } snake_x[0] new_x; snake_y[0] new_y; // 撞自身检测绕过蛇头本身从下标1开始 for (i 1; i snake_len; i) { if (snake_x[i] snake_x[0] snake_y[i] snake_y[0]) { game_over(); return; } } if (eat) { gen_food(); if (move_speed 20) move_speed - 2; // 加速 } update_buffer(); }食物生成这里有个坑C51 标准库自带 rand()但每次上电默认种子都是一样的导致第一局食物位置固定。我在游戏初始化时用定时器的计数值当种子void gen_food(void) { do { food_x rand() % MAP_W; food_y rand() % MAP_H; // 利用 TL0 的低位打乱 rand 的低位避免规律性明显 if (TL0 0x01) food_x MAP_W - 1 - food_x; } while (cell_on_snake(food_x, food_y)); }4.3 方向键的防反逻辑还有一个小细节游戏进行中如果蛇正在向上走你按“下”蛇会直接掉头撞上自己。处理办法很简单按键扫描时对比当前方向和欲设置方向是否为相反方向如果是就丢弃这次按键void key_proc(void) { if (key_up_pressed g_dir ! 1) g_dir 0; if (key_down_pressed g_dir ! 0) g_dir 1; if (key_left_pressed g_dir ! 0 g_dir ! 1) g_dir 2; if (key_right_pressed g_dir ! 0 g_dir ! 1) g_dir 3; }这样蛇只能向左转或向右转不能 180 度调头游戏逻辑才符合直觉。5. 开发调试中的五个坑从 Keil C51 支持包到 Proteus 仿真这部分是我最想写的因为这些坑浪费了我大量时间而且网上资料往往只讲一半。5.1 Keil5 装 C51 支持包MDK 和 C51 不是同一个东西很多新手电脑上装的是 Keil5默认装的是 MDK-ARM用来编 STM32 的。打开 Keil 后发现找不到 51 单片机型号以为软件坏了。其实 Keil5 的 C51 支持是要单独安装的装完 Pack Installer 里选 Legacy Device 或对应 51 型号即可。MDK-ARM 和 C51 可以共存不用卸载重装。另外注意同一台电脑装 Keil4 和 Keil5 也能共存但 Keil4 创建的工程被 Keil5 打开后工程文件格式会升级再拿回 Keil4 打开可能报错。建议一步到位用 Keil5 C51 支持包。官方评估版有 2KB 代码限制贪吃蛇这种小项目完全够用学习阶段不需要额外折腾授权。5.2 调试 C51 的时候System Viewer 空白是正常的网上有人问“8051 单片机在 Keil 仿真时 System Viewer 是空白的”我一开始也以为仿真坏了。后来才明白System Viewer 主要给 ARM 内核用C51 工程里它就是一片空白。要看 51 的寄存器、IO 状态应该打开菜单栏的 Peripherals在里面选 Timer、Parallel Ports、Serial 等窗口P0/P1/P2 的高低电平、定时器计数值都能实时看。仿真时配合这些窗口调点阵扫描顺序比盲调快得多。5.3 Proteus 仿真 STC 单片机时xdata 是个坑用 Proteus 仿真 STC 增强型单片机时如果代码里声明了大数组比如 unsigned char xdata buf[512]会出现数据不刷新、甚至仿真直接卡死的情况。这属于仿真模型对 xdata 内存支持不完整不是真实芯片的问题。贪吃蛇的地图只有 8×8坐标数组放内部 data 区完全够用没必要碰 xdata。如果以后要做 16×16 的大地图再考虑外扩 RAM 或者换仿真模型。5.4 按键防抖不能 delay点阵要防“鬼影”C51 例程里最常见的按键防抖写法是 delay(20)这在流水灯项目里没问题但在贪吃蛇这种持续刷新的项目里是灾难按下按键的瞬间主循环被 delay 卡住点阵扫描中断虽然还在走但屏幕会明显闪一下游戏移动节拍也会被干扰。正确做法是每 20ms 扫描一次按键连续两次读到同一键按下才认为有效。这样既不阻塞又很稳。点阵动态扫描还有一个经典问题叫“鬼影”上一行的 LED 还没完全熄灭下一行的数据就送进来了画面会出现残影。解决方法是切换行时先把显示关掉更新行选和列数据再开显示。简单说就是“先灭、再换行、再亮”顺序不能反。5.5 P0 口一定要接上拉电阻前面提过这里再强调一次P0 口内部没有上拉电阻驱动点阵列线时必须外接上拉。漏接的话Proteus 仿真可能一切正常实物上点阵亮度极低、甚至高电平拉不起来。我见过不少人调了半天以为是代码问题最后发现是电路漏了 8 个电阻。6. 还能怎么玩难度分级、分数显示与双人模式的扩展思路做完基础版之后这个项目还有很多可以延展的地方我列几个我觉得性价比比较高的方向。6.1 难度分级与最高分记录基础版的移动节拍是固定 500ms我把它做成全局变量后可以很自然地分出三档难度简单 500ms、普通 300ms、困难 150ms。开机时按一次按键选择难度即可。再进一步把“最高分”存到单片机内部 EEPROM 或外部 AT24C02 里掉电不丢失。STC89C52 内部自带 EEPROM不用额外接芯片这个功能直接让项目从“课程设计”变成“能展示的完整作品”。6.2 加一块 LCD1602 做信息面板点阵主屏负责游戏画面分数、难度、游戏状态这些文字信息可以放到一块 LCD1602 上显示。这样两个模块各司其职成绩展示也更直观。注意 LCD1602 和点阵都用 P0/P2 口的话需要加锁存器或者分时复用总线建议给 LCD1602 单独分配一组 IO比如 P1 口代码逻辑会简单很多。6.3 音效、双人对战和大地图蜂鸣器接到 P1.0吃到食物时响一声短音游戏结束时响一段长音成本不到两块钱体验提升非常明显。双人模式则是把地图从 8×8 扩展到 16×16、用两块点阵拼接两个人各控制一条蛇比赛谁先撞死。这个版本我建议放到基础版跑通之后再做因为 16×16 的地图意味着坐标计算、点阵扫描、食物生成逻辑都要重写一遍工作量并不小但对提升 C51 的熟练度非常有效。做完这个项目我最直观的感受是很多在电脑上顺理成章的事在只有 128 字节 RAM 的芯片上都要重新设计。比如蛇身用链表当然优雅但静态数组更省比如游戏逻辑用大循环自然但要学会和 2ms 的中断抢时间片。正是这些约束逼着你去理解状态机和时间片轮询而不是写完就跑。如果你也想做一个真正拿得出手的单片机入门项目别急着上 32 位平台先用 C51 把这套东西走通后面换 STM32 也只是 API 不同、思路相通。个人建议先 Proteus 仿真验证逻辑再焊最小系统接实物点阵两个阶段踩到的坑会完全不一样但都值得踩一遍。本文还有配套的精品资源点击获取
返回列表