资讯动态

51单片机128字节RAM实现8×8点阵贪吃蛇

发布时间:2026/10/26 2:25:02 来源:尧图企业网站定制
简介这是一套面向高校单片机课程设计与本科毕业设计入门者的51单片机贪吃蛇游戏完整方案围绕方向键控制蛇移动、吃豆加速、撞墙或撞身结束等经典规则展开适合用于Proteus仿真、Keil程序调试与课程答辩展示。压缩包共18个文件约102.42MB包含C源程序、A51汇编启动文件、HEX可执行文件、Proteus工程与工作区文件以及docx设计报告、pptx展示文稿和演示视频等覆盖从代码编译、电路仿真到报告撰写的完整链路。已有2443人学习下载。读者可据此理清定时器扫描、按键消抖、蛇身增长与速度变化、边界及自碰撞判定等实现思路并借助仿真工程和现成文档核对运行效果、快速搭建课程设计框架减少从零调试的时间。1. 用8×8点阵和128字节RAM把贪吃蛇跑出街机手感先给结论基于51单片机的贪吃蛇游戏卡人的地方不在游戏逻辑而在128字节直接寻址RAMdata区里同时塞下蛇身队列、帧缓存和游戏状态。整套C51代码约400行硬件成本能压在30元以内。选STC89C52用一个定时器做扫描、消抖、刷新三件事主循环只负责状态机是成本最低也最稳的分工。适合刚点亮LED、想做一个完整小游戏收尾的人也适合对时间和内存预算怎么分配还没手感的老手。读完后你能自己搭一块8×8点阵板、写完烧录、调出手感顺滑的贪吃蛇并提前知道哪个环节最容易翻车。这套方案的现实边界要说清楚8×8点阵只够画蛇、食物和简单分数条做不了复杂菜单12MHz晶振下刷新率约125Hz余量只够34个任务分时。想加花活比如难度选择界面得换16×16点阵或加一块LCD1602。先把最小闭环跑通再谈扩展顺序反了就是无尽的调试。2. 硬件选型与最小系统STC89C52的RAM到底够不够动手之前先把预算算清楚。很多人焊完板子烧程序跑起来才发现数组越界、蛇身画到一半乱码回头改硬件成本反而更高。这一章把主控、显示、输入三块的账先算明白。2.1 128字节data区的内存预算STC89C52片内RAM标称512字节但最快能直接用变量名寻址的是低128字节data区高128字节要用指针间接访问再往上的256字节扩展RAM走MOVX指令速度更慢。贪吃蛇对速度敏感的部分全在中断里能放data区就放data区。先列一份预算表后面所有代码都按这张表来分配变量/数组类型占用说明snake[32]unsigned char32B蛇身环形队列每节1字节高4位X、低4位Ydisplay[8]unsigned char8B帧缓存每字节一行bit0~bit7对应8列head_alias / tail_aliasunsigned char2B环形队列头尾指针snake_lenunsigned char1B当前蛇长cur_dir / next_dirunsigned char2B当前方向和缓冲方向food_posunsigned char1B食物坐标同样位压缩scoreunsigned int2B0~99够用但留16位方便以后扩展game_stateunsigned char1B运行/暂停/结束三态game_div / key_divunsigned char2B中断分频计数rnd_seedunsigned char1B伪随机种子临时变量—~10B循环变量、运算中间值合计约62字节128字节data区还剩一半。别小看这一半——它决定了你能不能加暂停菜单、能不能存最高分。我的习惯是先按这张表写完编译后看.map文件里DATA段的实际占用超过90字节就回头砍功能别等到跑起来才发现栈溢出。栈一般在data区高地址往下长剩60多字节做栈够用但如果函数嵌套太深、局部数组开太多照样翻车。有一类常见误用是把蛇身坐标拆成两个数组snake_x[32]和snake_y[32]各占32字节合计64字节。位压缩之后省一半代价是每次读写要做一次移位和掩码51上两条指令的事完全值得。2.2 8×8点阵74HC138行选加P0列驱的接线8×8点阵模块16个脚8行8列没内置译码。行选我用74HC1383-8译码器列驱直接用P0口。这套接法占IO最少元件也便宜。引脚分配如下51引脚功能连接对象P0.0~P0.7列数据点阵8列每列串220Ω限流P2.0~P2.2行选ABC74HC138的A、B、C输入P3.0~P3.3四个方向键独立按键到GNDP2.3蜂鸣器有源蜂鸣器经8550三极管驱动XTAL1/XTAL212MHz晶振两端各接30pF到GNDRST复位10μF电容到VCC10K电阻到GNDEA片选接VCC两个细节必须注意。第一P0口是开漏输出驱动点阵列必须外接10K上拉排阻否则高电平拉不上去LED亮度不均甚至全暗。第二74HC138输出是低电平有效Y0~Y7分别接点阵的8行任一时刻只有一路被拉低选中对应行。行列的极性要和点阵模块匹配。常见8×8点阵分共阴和共阳两种买模块时看清楚型号。我一般用共阴模块行低电平选通列高电平点亮。如果你的模块是共阳的列数据要取反否则显示出来是负片效果。2.3 按键、蜂鸣器与复位电路四个方向键接P3.0~P3.3另一端统一接GND。51的P3口内部有弱上拉理论上不接外部电阻也能读但为了按键响应可靠建议每个脚并一个10K上拉电阻到VCC。按键线不要和晶振线走平行容易串扰。蜂鸣器用有源蜂鸣器P2.3经8550三极管驱动基极串1K电阻。P2.3输出高电平时三极管导通、蜂鸣器响。有源蜂鸣器内部自带振荡给电平就响省掉软件产生方波的麻烦代价是只能发一个音调想要不同音效得换无源蜂鸣器加定时器PWM那是进阶章的事。复位电路是新手最容易省掉的部分。10μF电解电容正极接VCC、负极接RST10K电阻从RST到GND。上电瞬间电容相当于短路RST被拉高持续约10ms后电容充满、RST被10K电阻拉低完成复位。少了这个电容上电时程序可能从随机地址开始跑。晶振用12MHz两端各接一个30pF瓷片电容到GND。12MHz配合标准8051的12T模式机器周期正好1μs定时器初值算起来最省心。如果换成11.0592MHz串口波特率更准但定时器初值会出现小数1ms定时不再精确对贪吃蛇这种靠时间分频的游戏来说反而添乱。3. 蛇身队列与碰撞检测128字节RAM里的贪吃蛇内核硬件定了接下来是游戏内核。这一章回答三个问题蛇身用什么结构存、方向输入怎么防自撞、食物和碰撞的边界条件有哪些。三个问题不解决写出来的蛇要么穿墙、要么一按键就原地掉头。3.1 为什么用环形数组而不是链表51上没有可用的堆管理器malloc要么没有、要么用了也不安全链表这条路直接堵死。就算强行用静态节点池每个节点也要2字节指针加1字节数据32节就是96字节data区根本放不下。环形数组是唯一实际的选择固定32字节入队出队都是O(1)头尾指针用掩码代替取模。32是2的5次方取模可以写成 0x1F比% 32快得多。#include reg52.h #define MAX_SNAKE 32 /* 必须是2的幂掩码才成立 */ #define GRID_W 8 #define GRID_H 8 /* 蛇身环形队列每节1字节高4位X低4位Y */ unsigned char snake[MAX_SNAKE]; unsigned char head_alias 2; /* 初值蛇长3占0/1/2三格 */ unsigned char tail_alias 0; unsigned char snake_len 3; /* 取第i节0为尾len-1为头 */ #define SNAKE_AT(i) \ (snake[(tail_alias (unsigned char)(i)) (MAX_SNAKE - 1)]) /* 头入队 */ static void snake_push(unsigned char pos) { head_alias (head_alias 1) (MAX_SNAKE - 1); snake[head_alias] pos; } /* 尾出队 */ static void snake_pop(void) { tail_alias (tail_alias 1) (MAX_SNAKE - 1); }逻辑说明head_alias和tail_alias都在0~31内循环snake_push先移动头指针再写数据snake_pop只移动尾指针。取蛇身第i节时从tail_alias往后数i格用掩码保证不越界。参数上唯一要盯的是MAX_SNAKE必须是2的幂——写成30、掩码还是0x1F就会读到没写过的格子表现为蛇身某节突然闪一下或者卡住。初始化时三个格子要手动填好比如snake[0]0x33, snake[1]0x32, snake[2]0x31表示一条横着的蛇。别指望全局数组自动清零后就是合理初始状态——全是0意味着三节蛇叠在(0,0)同一格第一次移动就会自撞判定。3.2 方向输入与防反向的缓冲逻辑方向用两个变量cur_dir记录当前生效方向next_dir记录缓冲方向。按键只改next_dir每移动一格后cur_dir next_dir。这样做的原因是玩家在一个移动周期内可能连按两次方向如果直接改cur_dir蛇会在一格内连续转向看起来像抽搐。方向编码用0~3编码方式直接决定防反向判断能不能写成一行#define DIR_UP 0 #define DIR_DOWN 1 #define DIR_LEFT 2 #define DIR_RIGHT 3 /* 方向d是否与当前方向dir相反异或为1即是反向 */ static bit is_reverse(unsigned char d, unsigned char dir) { return ((d ^ dir) 1); } /* 按键扫描后更新next_dir */ static void update_next_dir(unsigned char d) { if (!is_reverse(d, cur_dir)) { next_dir d; } }逻辑说明把上下编码成0和1、左右编码成2和3相反方向异或结果恰好都是1。这样is_reverse只要一次异或加一次比较两个机器周期完事。如果随便编码比如上0下2左4右6就得写四组判断或者查表代码量和执行时间都翻倍。参数上唯一要保证的是一组相反方向必须差1这是编码约定不是巧合。还有一点next_dir在移动周期内可以被改多次但只在执行移动时消费一次。玩家快速连按「右、下」如果当前朝右第一下右被反向判断挡掉右和右不反向其实会生效成右第二下下生效蛇下一步往下走。这个缓冲机制让操作手感明显好于直接改cur_dir。3.3 食物生成与碰撞检测的四个边界条件碰撞检测要覆盖四种情况撞墙、撞自身、吃到食物、以及食物生成时不能落在蛇身上。前三个每次移动都要查第四个只在吃食物后触发。#define POS(x, y) (((x) 4) | (y)) /* 判断某格是否在蛇身上跳过即将移走的尾 */ static bit pos_in_snake(unsigned char pos) { unsigned char i; for (i 0; i snake_len - 1; i) { if (SNAKE_AT(i) pos) return 1; } return 0; } /* 生成食物落在空格上返回0xFF表示没找到 */ static unsigned char gen_food(void) { unsigned char pos, tries 0; do { pos (rnd_seed ^ (sys_tick 3)) 0x3F; if (tries 64) return 0xFF; } while (pos_in_snake(pos)); return pos; } /* 一次移动返回1表示吃到食物0表示正常2表示撞墙或撞自身 */ static unsigned char game_tick(void) { unsigned char nx, ny, nh, pos; unsigned char cur; /* 解出当前头坐标 */ cur SNAKE_AT(snake_len - 1); nx (cur 4) 0x07; ny cur 0x07; switch (next_dir) { case DIR_UP: if (ny 0) return 2; ny--; break; case DIR_DOWN: if (ny GRID_H - 1) return 2; ny; break; case DIR_LEFT: if (nx 0) return 2; nx--; break; case DIR_RIGHT: if (nx GRID_W - 1) return 2; nx; break; } cur_dir next_dir; nh POS(nx, ny); if (pos_in_snake(nh)) return 2; /* 撞自身 */ snake_push(nh); if (nh food_pos) { snake_len; food_pos gen_food(); if (food_pos 0xFF) return 2; /* 满屏判胜 */ return 1; } snake_pop(); return 0; }逻辑说明先解出头坐标和下一步坐标越界直接返回2。pos_in_snake遍历时只查到snake_len - 2即i snake_len - 1因为第0节是尾巴这一步马上会被移走不参与碰撞判定——少这个细节蛇头追着尾巴走的时候会误判自撞血泪经验。吃到食物时snake_len加1但不执行snake_pop蛇身净增长一节。参数上要盯两处gen_food的tries上限防止蛇快占满时死循环rnd_seed每调用一次自增再和sys_tick异或避免食物连续出现在同一格。sys_tick是定时器中断里的全局计数后面会定义。4. 定时器中断与点阵刷新把游戏循环塞进T0的1ms节拍51上没有RTOS所有周期性工作都得挂在一个定时器上。这一章把1ms中断拆成三份显示扫描、按键消抖、游戏分频。分时没分好要么显示闪、要么按键迟钝、要么游戏速度不稳。4.1 T0初值计算与12MHz下的1ms时基12MHz晶振、12T模式下机器周期1μs定时器每计1个数就是1μs。要1ms中断一次计1000个数16位模式初值65536−1000645360xFC18。void timer0_init(void) { TMOD (TMOD 0xF0) | 0x01; /* T0模式116位 */ TH0 0xFC; /* 高8位 */ TL0 0x18; /* 低8位 */ ET0 1; /* 开T0中断 */ EA 1; /* 开总中断 */ TR0 1; /* 启动计数 */ } void timer0_isr(void) interrupt 1 { TH0 0xFC; /* 手动重装模式1不是自动重装 */ TL0 0x18; /* 分时任务在4.2节展开 */ }逻辑说明模式1是16位定时器溢出后不自动重装必须在中断里手动写回初值。漏写这一句定时器会从0开始重新计数下一次中断在65ms后游戏速度直接掉到15帧都不到。参数上0xFC18对应1ms想要2ms就改成0xF830但贪吃蛇的显示扫描需要1ms切一行2ms会让刷新率掉到62Hz开始能看出闪烁。STC89C52默认12T模式但如果你的芯片被配置成1T模式某些下载软件里有选项同样的初值会变成83μs中断一次游戏快得没法玩。烧录时确认一下时序选项或者干脆在代码里不依赖精确1ms、只依赖分频比例。4.2 扫描、消抖、刷新三个任务的分时调度1ms中断里塞三件事显示扫描每1ms切一行按键每5ms采样一次游戏每100ms走一步。用两个分频计数器管后两件。volatile unsigned char game_div 0; volatile unsigned char key_div 0; volatile bit game_tick_flag 0; volatile unsigned int sys_tick 0; void timer0_isr(void) interrupt 1 { TH0 0xFC; TL0 0x18; sys_tick; display_scan(); /* 每1ms刷新一行8ms一帧 */ if (key_div 5) { /* 每5ms采一次按键 */ key_div 0; key_scan(); } if (game_div 100) { /* 每100ms走一步 */ game_div 0; game_tick_flag 1; /* 只置标志主循环处理 */ } }逻辑说明显示扫描必须在中断里做因为8行轮流点亮靠的是精确1ms切换放主循环会被游戏逻辑拖慢导致闪烁。按键扫描放中断里是为了本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑