51单片机+8×8点阵写贪吃蛇:从硬件接线到C语言实现全攻略

发布时间:2026/10/12 14:39:11
51单片机+8×8点阵写贪吃蛇:从硬件接线到C语言实现全攻略 如果你学过C语言又刚好有点单片机基础那“51单片机 8*8点阵 贪吃蛇”这个组合绝对是最值得上手的一个练手项目。它不复杂但GPIO控制、动态扫描、按键输入、状态机设计全都能练到。我前前后后做过三版也帮同学排查过不少问题踩过的坑攒下来足够写一篇了。这篇不堆原理就按我的实际做法走一遍硬件怎么接、限流电阻怎么算、C语言代码怎么分层、蛇怎么动才不自己撞死最后再给一份高频问题排查清单。适合刚学完51外设想动手做小游戏的同学也适合课程设计要交差的朋友参考。1. 整体架构与设计思路1.1 为什么这个组合适合练手这个项目最容易被低估的地方是它看起来“小”但涉及的环节一个都不少。51单片机本身资源有限不能跑复杂系统但跑贪吃蛇刚刚好8x8点阵只有64个点分辨率低但不影响表达游戏核心C语言写逻辑比汇编直观很多。最关键的是这三者组合起来的复杂度对新手非常友好显示刷新用动态扫描游戏逻辑用状态机按键用查询方式全是单片机基础课的必修内容又比流水灯、数码管计数有成就感。我说句实话很多同学做课程设计喜欢选“多功能时钟”“温度报警器”代码量看着大但实际核心逻辑往往只有一处。贪吃蛇不一样它逼迫你去思考“数据怎么组织”“状态怎么切换”“输入怎么响应”这些才是嵌入式开发真正要练的本事。如果能把8x8点阵贪吃蛇调通后面再去碰OLED菜单、蓝牙遥控小车这类项目思路会顺很多。另外这个项目的成本很低。一块开发板、一块点阵模块、四个按键、一些杜邦线就能搞定不需要额外买什么昂贵传感器。就算中途焊错了、烧坏了换一块点阵模块也就几块钱试错成本完全可以接受。1.2 显示与控制的方案取舍网上搜“8x8点阵贪吃蛇”能找到不少方案归纳起来就三类直接IO驱动、74HC595扩展驱动、点阵模块自带驱动。直接IO驱动就是把点阵的8根行线和8根列线直接接到单片机的P2口和P0口硬件最简单代码也最直观。74HC595扩展是用两个移位寄存器串行控制行和列只占3到6个IO节省引脚但要多理解串行时序。点阵模块自带驱动芯片的话MCU只要送数据字节开发速度最快但对底层扫描的理解就弱了一些。我自己做的是直接IO驱动。原因很简单我想把动态扫描的原理彻底搞明白而不是拿来一个模块只管写数据。8x8点阵总共16根引脚需要16个IO口而51单片机有32个IO口完全够用引脚多了虽然接线看起来密但排查问题最方便一个逻辑分析仪夹上去就能看到行选通和列数据的变化。如果以后要做16x16的大屏再切换到595方案也不迟那时你对扫描原理已经有底子了。选择方案时也要看你手头的硬件。如果手里点阵模块已经带了595驱动芯片那就没必要硬拆掉按模块资料写代码即可如果手里是裸点阵那就走直驱方案。这个项目价值不在于用哪种芯片而在于把一条数据从“蛇头的坐标”变成“屏幕上一颗亮的LED”的完整链路打通。1.3 游戏状态怎么拆才不乱游戏逻辑虽然简单但我强烈建议用状态机的方式来组织。贪吃蛇至少有三种状态初始化、运行中、游戏结束。如果不用状态机直接写if堆逻辑初版可能跑得通但后面加暂停、加菜单、加计分的时候代码会越来越乱。我用一个全局变量game_state来表示当前状态。初始化状态负责生成蛇的初始位置、随机食物、清空显示缓冲运行状态每个节拍执行一次移动和碰撞检测游戏结束状态停住所有移动做闪烁提示等玩家按键复位。状态转移只有两条路从初始化进入运行、从运行进入游戏结束。这样代码里只需要在update_game函数开头判断一次当前状态逻辑边界非常清晰。有个细节值得注意游戏结束之后按键不应该再影响蛇的方向否则玩家“死后”乱按方向缓存可能残留到下局开局就朝错误方向撞墙。我的做法是main循环里只有game_state GAME_RUN时才调用update_gameGame Over后按键扫描只负责检测复位键不更新dir_buf。2. 硬件搭建与关键参数计算2.1 8x8点阵引脚识别与接线8x8点阵虽然有8根行、8根列但不同厂家的模块引脚排列完全不一样一开始不要死记。我拿到一块陌生点阵先用万用表二极管档测红表笔固定一个引脚黑表笔扫其他引脚看到LED亮起就记一组正极和负极再换下一组。这样扫一轮就能画出引脚对照表。这步虽然烦但非常值得做因为网上搜到的引脚号很可能和你手上的模块对不上直接照抄十有八九会翻车。绝大多数红色单色点阵是行阳列阴结构也就是每一行的LED阳极连到一起每一列的LED阴极连到一起。点亮某个点需要行给高电平、列给低电平电流从行流入、从列流出。因此我的接线约定是行线接P2口列线接P0口列线上串限流电阻。P2口输出高电平作为行选通P0口输出低电平作为列数据点亮的LED就在行和列交叉的位置。如果是行阴极、列阳极结构的点阵那接线要反过来列线接P2行线接P0并且选通极性也要翻转。判断方法很简单用万用表测一次就能知道。千万注意P0口是开漏输出结构不能直接输出高电平驱动必须外接上拉电阻否则列数据里的“不亮”状态不稳定。我习惯在P0口接一个10kΩ排阻拉到VCC效果稳定。功能单片机引脚中间器件点阵引脚行选通P2.0-P2.774HC245同相驱动行1-行8列数据P0.0-P0.7330-470Ω限流电阻列1-列8方向按键P1.0-P1.3按键另一端接GND-如果你用的是开发板自带的点阵模块或者点阵模块已经焊好了驱动芯片和限流电阻那就不必再管行驱动和列上拉直接对照模块丝印接VCC、GND、数据口就行。这里最容易出的问题是“模块丝印标注不清”所以上手之前先确认模块资料不要凭感觉乱接。2.2 限流电阻和驱动能力怎么算发光二极管不能直接接5V必须串限流电阻。以红色LED为例正向压降约2VP0输出低电平时的饱和压降约0.3V。假设目标峰值电流5mA电阻值就是(5-2-0.3) / 0.005 540Ω所以470Ω到560Ω是稳妥档位。如果觉得亮度不够把电流提到10mA电阻约270Ω选330Ω。我建议先从470Ω开始试太暗再换330Ω尽量不要低于220Ω否则P0脚的灌电流会超规格。但这里有个动态扫描特有的知识点点阵是逐行点亮的每一行只有1/8的时间在通电所以LED的“峰值电流”可以比静态驱动时高一些视觉亮度由平均电流决定。人眼对低占空比的闪烁并不敏感只要频率足够高看起来就是连续点亮。这也是为什么扫描频率必须够高的原因低于50Hz就会看到明显闪烁。驱动能力方面51单片机的IO口并不强P2口高电平输出电流能力很弱如果直接去推一行里同时点亮的多个LED亮度会明显不均匀。我实际试过直驱能亮但一行亮4个点以上时亮度就开始塌陷。解决方法是加一个74HC245同相驱动器在行线上输入接P2口输出接点阵行引脚由外部电源提供行电流。如果你手上没有驱动器也可以把LED电流限制在3-5mA牺牲亮度换取稳定但白天强光下基本看不清。注意如果点阵模块自带595驱动电路就不要按上面的直驱接法接线先看模块资料确认数据位顺序和OE引脚再写代码。2.3 方向按键的接法与去抖方向按键我用了四个接在P1.0到P1.3上分别对应上、下、左、右。接线方式按键一端接P1口引脚另一端接GND。51的P1口有内部上拉平时读1按下时读0。这种接法比按键一端接VCC更合理因为读到的电平逻辑干净不会和上拉电阻冲突。硬件去抖最简单的是在每个按键引脚对地并联一个0.1μF电容能滤掉大部分机械抖动。如果不想加电容软件去抖也完全可以检测到低电平后延时10ms左右再读一次确认仍然是低电平才认为按下有效。我建议这两个方法结合电路上并联电容程序里加延时判断双保险。这里要提醒一个新手常犯的错误不要把按键接到P0口。P0口是开漏结构没有内部上拉做按键输入必须外接上拉电阻否则按键按下去读到的电平不稳定。就算你接好了上拉P0口还要用来驱动点阵列复用时会有干扰。老老实实把按键放在P1口可以省掉一大半麻烦。3. C语言代码架构与核心逻辑实现3.1 工程划分、数据结构与状态定义我习惯把所有代码放在一个main.c里但内部按功能拆成几个函数块显示、按键、游戏逻辑、定时器。这个规模的项目不用强行分多个文件但函数边界一定要清晰。数据定义如下#include reg52.h #include stdlib.h #define ROW_PORT P2 #define COL_PORT P0 sbit KEY_UP P1^0; sbit KEY_DOWN P1^1; sbit KEY_LEFT P1^2; sbit KEY_RIGHT P1^3; #define SNAKE_MAX 64 #define GAME_RUN 0 #define GAME_OVER 1 unsigned char code row_sel[8] {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80}; typedef struct { unsigned char x; unsigned char y; } Point; Point snake[SNAKE_MAX]; unsigned char snake_len 3; Point food; unsigned char disp_buf[8]; unsigned char dir, dir_buf; unsigned char game_state; unsigned char tick_100ms;蛇身用Point数组保存索引0是蛇头最后一个元素是尾巴。一个格子的坐标用x和y表示范围都是0到7对应8x8点阵的行列。最大长度就是64因为整屏只有64个格子蛇不可能长到65格这个上限在代码里直接定义成宏。方向用unsigned char表示上下左右分别用0、1、2、3。dir是当前实际方向dir_buf是按键写入的方向缓存为什么需要两个变量后面讲按键时会详细说。初始蛇身长度设为3蛇头在(3,3)身体在(2,3)和(1,3)初始方向向右游戏立即进入运行状态然后生成第一个食物并刷新显示缓冲。3.2 动态扫描显示的实际写法动态扫描的核心思路是逐行点亮。扫第0行时只显示第0行对应的数据扫第1行时只显示第1行数据八行都扫一遍就是一帧。人眼有视觉暂留只要一帧总时间小于20ms也就是刷新率高于50Hz就不会闪烁。我的display函数扫8行每行延时1ms一帧大约8ms刷新率超过120Hz完全够用。void render(void) { unsigned char i; for (i 0; i 8; i) { disp_buf[i] 0; } for (i 0; i snake_len; i) { disp_buf[snake[i].y] | (1 snake[i].x); } disp_buf[food.y] | (1 food.x); } void display(void) { unsigned char i; for (i 0; i 8; i) { ROW_PORT 0x00; // 先灭所有行 COL_PORT ~disp_buf[i]; // 列低电亮点亮 ROW_PORT row_sel[i]; // 选通第i行 delay_us(1000); } }render函数负责把蛇身和食物画到disp_buf缓冲区里每个字节对应一行bit0对应x坐标为0的那个点1表示亮。这样把“游戏逻辑”和“显示扫描”解耦逻辑层只管改缓冲区显示层只管扫描缓冲区后面加特效会非常方便。写display函数时最重要的是顺序先灭行、再给列数据、最后选通行。如果先选通行再给列数据上一行残留的列数据会漏到当前行产生“鬼影”和“串亮”。我第一次写的时候就是顺序反了屏幕上全是幻影折腾了一个晚上才反应过来。提示如果你的点阵是列高电亮点亮只需要把COL_PORT ~disp_buf[i]改成COL_PORT disp_buf[i]行选通方向也要相应调整。3.3 蛇的移动、食物生成与碰撞检测蛇的移动算法很多同学上来就写错。最常见的错法是“先移动再判断食物”结果蛇头到达食物格子的那一瞬间旧尾巴已经往前挪了长度永远加不上。正确顺序是根据当前方向算出新头坐标先做撞墙和撞身检测再看新头是否吃到食物最后统一平移蛇身。void spawn_food(void) { unsigned char occupy, i; do { occupy 0; food.x rand() % 8; food.y rand() % 8; for (i 0; i snake_len; i) { if (snake[i].x food.x snake[i].y food.y) { occupy 1; break; } } } while (occupy); } void update_game(void) { unsigned char nx, ny, i; if (!( (dir UP dir_buf DOWN) || (dir DOWN dir_buf UP) || (dir LEFT dir_buf RIGHT) || (dir RIGHT dir_buf LEFT) )) { dir dir_buf; } nx snake[0].x; ny snake[0].y; switch (dir) { case UP: if (ny 0) { game_state GAME_OVER; return; } ny--; break; case DOWN: if (ny 7) { game_state GAME_OVER; return; } ny; break; case LEFT: if (nx 0) { game_state GAME_OVER; return; } nx--; break; case RIGHT: if (nx 7) { game_state GAME_OVER; return; } nx; break; } for (i 1; i snake_len; i) { if (snake[i].x nx snake[i].y ny) { game_state GAME_OVER; return; } } if (nx food.x ny food.y) { snake_len; if (snake_len SNAKE_MAX) { game_state GAME_OVER; return; } spawn_food(); } for (i snake_len - 1; i 0; i--) { snake[i] snake[i - 1]; } snake[0].x nx; snake[0].y ny; render(); }移动时新头坐标先用nx和ny算出来不直接修改snake[0]是为了先做碰撞检测。检测撞墙就是在方向对应的边界上判断是否越界如果越界就把状态置成GAME_OVER并返回。检测撞身则从索引1开始比较因为索引0是旧蛇头新头不可能和自己在同一个位置。吃食物时先让snake_len自增再统一平移蛇身这样旧尾巴就不会被丢掉长度自然增加1。如果没吃到食物长度不变整体平移时旧尾巴就被数组覆盖等价于尾随移动。这个“先判断、后移动”的顺序是整个游戏逻辑最关键的地方我建议你在纸上画一下长度为3的蛇走一步的情况理解之后再去写代码。食物生成用的do-while循环先在0到7范围内随机生成一个坐标再检查是否落在蛇身上如果是就重新生成。64个格子减掉蛇身长度剩余格子一定大于0所以这个循环理论上总会退出。当蛇吃到第64个格子时我直接判游戏结束因为此时整屏都被蛇占满已经没有空位再生成食物了。3.4 主循环节拍与按键方向防坑游戏不能每个主循环都移动否则速度取决于while循环的运行时长可能快到看不清。我用定时器0产生50ms中断中断里累计2次也就是100ms设置一个tick_100ms标志。主循环发现这个标志后才执行一次update_game。这样移动速度就和主循环里的显示扫描、按键扫描解耦了。定时器初始化代码如下晶振按12MHz计算一个机器周期1μs50ms需要50000个计数所以初值是65536-5000015536也就是0x3CB0。void timer0_init(void) { TMOD 0xF0; TMOD | 0x01; TH0 (65536 - 50000) / 256; TL0 (65536 - 50000) % 256; ET0 1; EA 1; TR0 1; } void timer0_isr(void) interrupt 1 { TH0 (65536 - 50000) / 256; TL0 (65536 - 50000) % 256; tick_cnt; if (tick_cnt 2) { tick_cnt 0; tick_100ms 1; } } void main(void) { timer0_init(); init_game(); while (1) { display(); scan_key(); if (tick_100ms) { tick_100ms 0; if (game_state GAME_RUN) { update_game(); } } } }如果换用11.0592MHz晶振50000个机器周期不再是50ms初值要重新算。我习惯在注释里写清楚“当前按12MHz计算”防止换板子时忘记。按键扫描和方向控制是个大坑。蛇正在向右走你按上再按左如果每次按键都直接改dir最后一次按左就会把方向设成左而左是当前右方向的反方向蛇会一头撞到自己。正确做法是按键只改dir_bufupdate_game在一个节拍开始的时候再把dir_buf同步给dir同时拒绝反向。这样即使用户手速再快蛇也只会在每一拍转向一次。简单版按键扫描如下void scan_key(void) { if (KEY_UP 0) { delay_ms(10); if (KEY_UP 0) dir_buf UP; } else if (KEY_DOWN 0) { delay_ms(10); if (KEY_DOWN 0) dir_buf DOWN; } else if (KEY_LEFT 0) { delay_ms(10); if (KEY_LEFT 0) dir_buf LEFT; } else if (KEY_RIGHT 0) { delay_ms(10); if (KEY_RIGHT 0) dir_buf RIGHT; } }这个版本能跑但有一个小问题如果按键一直按住每次扫描都会更新dir_buf等蛇转到那个方向之后就会一路朝墙走。要解决可以加一个“上一次按键值”变量只在按键从松开变为按下时处理。这种下降沿判断是按键处理的标准做法代码量不大网上教程也很多建议自己实现一版。4. 编译调试与实物排查经验4.1 高频问题速查表我把带同学调试时遇到的高频问题整理成一个表照着排查基本能解决90%的问题。现象可能原因排查思路整屏不亮点阵电源没接对、P0口上拉缺失先量VCC和GND电压再检查上拉排阻用万用表二极管档确认点阵好坏显示乱闪扫描延时太长、主循环被长延时阻塞检查每行延时是否超过2ms确保主循环里没有大段delay出现鬼影扫描顺序不对先选行后写列改成“灭行-写列-选行”的顺序某一列特别暗P0驱动能力不足或限流电阻过大换低阻值限流电阻或加驱动芯片按键没反应按键接在P0口、按键接VCC方向接反移到P1口确认按键另一端接GND软件去抖延时足够蛇突然反向自杀按键直接改dir没有方向缓存和防反向引入dir_buf在update_game里同步并拒绝反向食物总是同一位置rand()种子固定用TL0或TH0做随机种子srand一次蛇身变长后屏幕亮度下降供电不足或LED驱动电流不够用独立5V电源或加74HC245驱动行线上面几条里最值得重视的是“方向缓存”那条。这个问题平时不遇到一旦遇到就是瞬间Game Over非常打击信心。我自己做过第二版的时候就是因为在按键中断里直接改了方向蛇拐弯时总会莫名其妙撞到自己后来画状态图才定位到原因。4.2 几个我实际踩过的坑第一个坑是显示残影。我一开始写的扫描顺序是先选通行再写列数据结果屏幕上总有一条斜向的亮带跟着蛇移动。后来查阅资料才明白行选通切换后列数据端口还残存上一行的电平这些残留电流会通过刚选通的下一行LED形成亮带。解决办法就是严格执行“灭行-写列-选行”让P2口先输出全低再把列数据准备好最后再拉高特定行。改完之后残影立刻消失。第二个坑是随机数。我的第一种写法没有srand结果每次开机后食物都出现在同一个位置玩两把就能背板。原因是C51的rand()默认种子固定生成的序列也是固定的。解决办法是在init_game里用当前定时器值做种子srand(TL0 ^ TH0)。因为每次上电后TL0和TH0的初值有一定的随机性食物位置就不再固定了。如果需要更强的随机性可以用主循环里不断变化的变量做种子但做这个小游戏完全没必要。第三个坑是按键串扰。有同学做实物时按下“上”键蛇经常同时往右偏。查了一圈发现是按键引脚飞线过长按键抖动波形干扰到了旁边引脚。加了0.1μF电容之后瞬间解决。这里提醒一句面包板上的飞线越短越好按键的地线不要绕一大圈再接GND否则就是天线。第四个坑是供电。蛇吃到二十多格时整屏可能同时点亮三四十个LED每个按5mA算也有150mA以上再加上开发板供电来自USB转串口线电压一跌单片机直接复位。我换成独立5V适配器供电后就没再出现过。这个现象在仿真软件里根本看不到只有实物才会暴露。4.3 从仿真到实物的关键差异如果你先用仿真软件调通再转实物有几个差异必须心里有数。仿真软件里P0口不接上拉也能输出高电平但实物必须接上拉否则列数据完全不对显示出来的图案会乱得没法看。这是转实物时最常见的第一道坎。仿真里LED的亮度模型很理想电流算小一点也能看到亮光实物则要现场看亮度再决定限流电阻。我建议先在实物上点亮一行8个LED肉眼确认亮度和均匀性再固定阻值。另外仿真不会模拟点阵引脚排列差异你从仿真直接照搬接线到实物很可能完全不亮必须在实物上用万用表确认行列关系。速度感知也不一样。仿真里定时器节拍和代码执行速度不会让你有“蛇太快看不清”的感觉但实物上LED点亮和熄灭有真实响应时间如果每行扫描延时和移动节拍没有拉开差距画面就会拖影。我的个人经验是显示扫描延时保持在1ms左右移动节拍至少100ms这样人眼才能清晰跟踪蛇的方向。5. 扩展优化与项目复盘心得5.1 后续可以加的功能这个项目做完之后非常推荐继续往上加功能因为基础架构已经搭好每加一个功能都是在强化对状态机和外设的理解。速度递增是最容易做的一个功能。准备一个move_period变量初始值为2也就是每2个50ms中断移动一次等价于100ms。每吃到3个食物move_period减1最快减到1。tick_100ms的判断改成tick_cnt move_period这样移动速度就会随着蛇长增加而逐步变快游戏性立刻提升一个档次。暂停功能也很实用。加一个按键按一下在RUN和PAUSE之间切换状态。因为状态机已经存在只需要在状态转移处加一个分支game_state等于PAUSE时update_game直接return即可显示可以保持最后一帧画面方便玩家观察局面。计分显示可以再接两个数码管用定时器中断扫描显示分数数值就是吃到食物的数量。数码管扫描和点阵扫描其实是一套思路如果你愿意可以把数码管扫描插到同一个定时器中断里两个显示场景共用一套节拍。障碍物功能就更进阶了在init_game里往disp_buf里预写几个固定亮点update_game里把新头坐标和障碍坐标比较撞到就Game Over。穿墙模式也可以做边界判断改成取模运算蛇从左边界出去会从右边界回来玩起来风格完全不同。5.2 做完这个项目后的一些感想这个项目虽然小但它最大的价值是让你走完一个“从需求到硬件到代码”的完整闭环。我建议你动手之前先花十分钟在纸上画状态转移图哪怕画得很丑也要把蛇的运动流程写清楚。很多代码写不顺的同学问题往往出在没想清楚就开始敲键盘而不是不懂语法。调试的时候一定一次只改一个变量。我见过最耗时的Debug是同学把显示缓冲区里的某一行数据写反了又同时改了扫描顺序结果两个错误叠加在一起现象非常诡异查了两个晚上才发现是两个问题。老实说这个项目里大部分Bug只要把问题拆开分别验证很快就能定位。还有一个小技巧写代码时把所有硬件相关的常量都用宏定义或者define集中放好比如行选通表、方向值、限流电阻对应的端口。这样以后换一块板子、换一种点阵模块只需要改开头那几行定义不用满文件找魔数。这个习惯是我做了几版之后总结出来的对后续做其他项目的帮助也很大。做这个项目的过程中我觉得最有收获的不是“我会写贪吃蛇了”而是我终于理解了“程序里改一个标志位硬件上就亮一颗灯”到底是怎么一步一步发生的。51单片机虽然老但这种软硬件之间的直接对应关系反而是现在很多自带WiFi模块的开发板给不了你的。希望这篇文章能帮你少踩几个我踩过的坑早点把屏幕上的小蛇调出来。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询