【制作】基于金沙滩51单片机的贪吃蛇程序
【制作】基于金沙滩51单片机的贪吃蛇程序
零、起因
要离开实验室了,但是还是有点不放心学弟们的学习,为了让他们知道单片机能干嘛,体会到单片机的快乐,特意作此程序,以提高他们对单片机的学习兴趣。
要实现以下功能:
- 食物根据随机种子的不同出现的序列也不同
- 经典贪吃蛇游戏,能穿墙
- 贪吃蛇速度随分数加快,分数越高,贪吃蛇速度越快
- 能显示分数
一、电路原理图
用的是金沙滩的51单片机开发板,同款的电路应该是一致的,这部分可略过。
单片机最小系统部分
跳线部分
这部分连的都是ADDR。
数码管、LED部分
这部分使用74HC245三态缓冲器来提高单片机P0口的负载能力,通过138译码器提高单片机的IO口复用。
按键部分
这部分为矩阵按键,连接到单片机的P2口。
蜂鸣器部分
蜂鸣器使用无源蜂鸣器,更自由,可以自定义音调等。
二、代码
新建51单片机工程,输入以下代码:
/* 2020-11-17 Minuye */ #include <reg52.h> #include <stdlib.h> /* IO引脚分配定义 */ sbit KEY_IN_1 = P2^4; //矩阵按键的扫描输入引脚1 sbit KEY_IN_2 = P2^5; //矩阵按键的扫描输入引脚2 sbit KEY_IN_3 = P2^6; //矩阵按键的扫描输入引脚3 sbit KEY_IN_4 = P2^7; //矩阵按键的扫描输入引脚4 sbit KEY_OUT_1 = P2^3; //矩阵按键的扫描输出引脚1 sbit KEY_OUT_2 = P2^2; //矩阵按键的扫描输出引脚2 sbit KEY_OUT_3 = P2^1; //矩阵按键的扫描输出引脚3 sbit KEY_OUT_4 = P2^0; //矩阵按键的扫描输出引脚4 sbit ADDR0 = P1^0; //LED位选译码地址引脚0 sbit ADDR1 = P1^1; //LED位选译码地址引脚1 sbit ADDR2 = P1^2; //LED位选译码地址引脚2 sbit ADDR3 = P1^3; //LED位选译码地址引脚3 sbit ENLED = P1^4; //LED显示部件的总使能引脚 sbit BUZZ = P1^6; //蜂鸣器控制引脚 #define MAP_SIZE 8 //地图大小 #define MAP_DATA_SIZE 64 //地图数据大小 #define SLEEP_TIME 100 //每帧间隔时间 #define SNAKE_DEFAULT_LEN 3 //蛇默认长度 //按键值 #define KEY_VAL_W 0x26 //向上键 #define KEY_VAL_A 0x27 //左 #define KEY_VAL_S 0x28 //下 #define KEY_VAL_D 0x25 //右 //map: 地图, 每个元素的映射, -1为食物 0为空地 大于0为蛇(值为存活回合) char pdata map[MAP_DATA_SIZE]; unsigned char dztBuff[8]; unsigned char isShowHeader; unsigned char len, i, X, Y; unsigned char move, inputBuf; //随机算法相关 unsigned char seed; //矩阵按键到标准键码的映射表//矩阵按键到标准键码的映射表 const unsigned char code KeyCodeMap[4][4] = { { '1', '2', '3', 0x26 }, //数字键1、数字键2、数字键3、向上键 { '4', '5', '6', 0x25 }, //数字键4、数字键5、数字键6、向左键 { '7', '8', '9', 0x28 }, //数字键7、数字键8、数字键9、向下键 { '0', 0x1B, 0x0D, 0x27 } //数字键0、ESC键、 回车键、 向右键 }; //全部矩阵按键的当前状态 unsigned char pdata KeySta[4][4] = { {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1} }; //数码管真值表 unsigned char code LedChar[] = { 0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E}; //Led显存 unsigned char ledBuff; //数码管显存 #define SMG_BUFF_SIZE 6 unsigned char smgBuff[SMG_BUFF_SIZE]; //Led点阵显存 #define DZT_BUFF_SIZE 8 unsigned char dztBuff[8]; //当前状态(状态机) unsigned char mode = 1; //当前按键值 unsigned char currentKeyVal = 0; //蜂鸣器开关,打开后蜂鸣器响,并自动置0 bit flagBuzzOn = 0; unsigned char _kbhit() { if(currentKeyVal) { return 1; } return 0; } unsigned char _getch() { unsigned char ckv = currentKeyVal; currentKeyVal = 0; return ckv; } void UpdateSmg(unsigned int val) { ledBuff = ~(0x80>>(val%8)); smgBuff[0] = LedChar[val%10]; smgBuff[1] = LedChar[val/10%10]; smgBuff[2] = LedChar[val/100%10]; smgBuff[3] = LedChar[val/1000%10]; smgBuff[4] = LedChar[val/10000%10]; smgBuff[5] = LedChar[val/100000%10]; } //游戏初始化 void InitGreedySnake() { unsigned char j; move = KEY_VAL_D;//初始化方向 inputBuf = 0;//重置输入缓存 len = SNAKE_DEFAULT_LEN;//设置蛇的长度 X = 0;//初始化蛇头坐标 Y = 0; //初始化地图 for (j = 0; j < MAP_DATA_SIZE; j++) { map[j] = 0; } //初始化随机 srand(seed); //找一块空地,等下设置食物 while (map[i = rand() % MAP_DATA_SIZE]); //设为食物 map[i] = -1; } //贪吃蛇游戏 unsigned char GreedySnake() { char mi,temp; char * p = 0; /* //蛇头闪烁 if (isShowHeader) { //使用位操作把蛇头置空 dztBuff[Y] = dztBuff[Y] & (~(0x80 >> (X % MAP_SIZE))); isShowHeader = 0; } else { isShowHeader = 1; } */ //如果没按退出键 if(inputBuf != 0x1B) { //检测输入 if (_kbhit()) { //获取输入 inputBuf = _getch(); switch (inputBuf)//动作冲突检测,如果与原动作不冲突,则覆盖原动作 { case KEY_VAL_A:if (move != KEY_VAL_D)move = KEY_VAL_A; break; case KEY_VAL_D:if (move != KEY_VAL_A)move = KEY_VAL_D; break; case KEY_VAL_S:if (move != KEY_VAL_W)move = KEY_VAL_S; break; case KEY_VAL_W:if (move != KEY_VAL_S)move = KEY_VAL_W; break; } } //输入 switch (move) { case KEY_VAL_A:p = &X, *p -= 1; break;//p指向对应轴, 并更新坐标 case KEY_VAL_D:p = &X, *p += 1; break; case KEY_VAL_S:p = &Y, *p += 1; break;//因为Y轴向下为正, 所以这里是加1 case KEY_VAL_W:p = &Y, *p -= 1; break; } //如果越界, 则移动至另一端 *p = (*p + MAP_SIZE) % MAP_SIZE; //p指向蛇头对应的地图元素 p = map + X + Y * MAP_SIZE; if (*p > 1)//如果撞到自己 { //游戏结束 (1为蛇尾) return 1; } if (*p == -1)//如果为食物 { //寻找空地 while (map[i = rand() % MAP_DATA_SIZE]); //设置食物, 蛇长+1 map[i] = -1, len += 1; //蜂鸣器响 flagBuzzOn = 1; } else { //空地 for (i = 0; i < MAP_DATA_SIZE; i++) { //遍历地图, 所有蛇的值-1 (去掉蛇尾) if (map[i] > 0) { map[i]--; } } } //状态判断 p指向地图元素, i为空地下标 for (*p = len,mi = 0, i = 0,temp = 0; i < MAP_DATA_SIZE;) //蛇头赋值, 遍历地图 { if (map[i] == 0) { dztBuff[mi] = dztBuff[mi] & (~(0x80 >> (temp))); } else if (map[i] > 0) { dztBuff[mi] = dztBuff[mi] | (0x80 >> (temp)); } else {//食物 dztBuff[mi] = dztBuff[mi] | (0x80 >> (temp)); } i++; temp = i % MAP_SIZE; if (temp == 0) {//如果到下一行的元素 mi++; } } //正常调用 return 0; } else { //按了退出键,执行退出程序 return 1; } } //延迟5ms*unit void DelayN5ms(unsigned char unit) { unsigned char a,b,c; while(unit--) { for(c=1;c>0;c--) for(b=200;b>0;b--) for(a=10;a>0;a--); } } //按键驱动 void KeyDriver() { unsigned char i, j; static unsigned char pdata backup[4][4] = { //按键值备份,保存前一次的值 {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1} }; for (i=0; i<4; i++) //循环检测4*4的矩阵按键 { for (j=0; j<4; j++) { if (backup[i][j] != KeySta[i][j]) //检测按键动作 { if (backup[i][j] != 0) //按键按下时执行动作 { if(currentKeyVal == 0) { currentKeyVal = KeyCodeMap[i][j]; } } backup[i][j] = KeySta[i][j]; //刷新前一次的备份值 } } } } void InitSys(unsigned char val) { unsigned char i; flagBuzzOn = 1; ledBuff = val; for(i=0;i<DZT_BUFF_SIZE;i++) { if(i<SMG_BUFF_SIZE) { smgBuff[i] = val; } dztBuff[i] = ~val; } } void main() { unsigned char i; EA = 1; //使能总中断 ENLED = 0; //使能U3 TMOD = 0x11; //设置T1为模式1,T0为模式1 ET1 = 1; //使能T1中断 TR1 = 1; //启动T1 ET0 = 1; //使能T0中断 TR0 = 1; //启动T0 while (1) { switch(mode) { case 1://初始化模式,自检 InitSys(0); //延时1秒,让灯全亮以检查 DelayN5ms(200); InitSys(0xff); mode = 2; break; case 2://随机种子模式,输入初始化随机种子 KeyDriver(); if(currentKeyVal == 0x0D) { InitSys(0xff); mode = 3; break; } //随机种子 seed += _getch(); //显示随机种子 UpdateSmg(seed); break; case 3://初始化游戏 InitGreedySnake(); mode = 4; break; case 4://游戏中 i = 50 - (len*4); if(i<20){ i = 20; } DelayN5ms(i); KeyDriver(); if (GreedySnake()) { //游戏结束 mode = 5; ledBuff = 0; flagBuzzOn = 1; DelayN5ms(200); flagBuzzOn = 1; DelayN5ms(200); flagBuzzOn = 1; } //显示分数 UpdateSmg(len - SNAKE_DEFAULT_LEN); // break; case 5: KeyDriver(); DelayN5ms(10); i++; if(i>240) { i = 0; } if(i%10 == 0) { flagBuzzOn = 1; } if(_getch() == 0x1b)//按下退出 { InitSys(0xff); mode = 2; } break; } } } //以下代码完成数码管动态扫描刷新 void SmgRefresh() { static unsigned char i = 0; //显示消隐 P0 = 0xFF; ADDR3 = 1; switch (i) { case 0: ADDR2=0; ADDR1=0; ADDR0=0; i++; P0=smgBuff[0]; break; case 1: ADDR2=0; ADDR1=0; ADDR0=1; i++; P0=smgBuff[1]; break; case 2: ADDR2=0; ADDR1=1; ADDR0=0; i++; P0=smgBuff[2]; break; case 3: ADDR2=0; ADDR1=1; ADDR0=1; i++; P0=smgBuff[3]; break; case 4: ADDR2=1; ADDR1=0; ADDR0=0; i++; P0=smgBuff[4]; break; case 5: ADDR2=1; ADDR1=0; ADDR0=1; i++; P0=smgBuff[5]; break; case 6: ADDR2=1; ADDR1=1; ADDR0=0; i=0; P0=ledBuff; break; default: break; } } void DzlRefresh() { static unsigned char i = 0; P0 = 0xFF; ADDR3=0; switch(i) { case 0: ADDR2=0; ADDR1=0; ADDR0=0; i++; P0=~dztBuff[0]; break; case 1: ADDR2=0; ADDR1=0; ADDR0=1; i++; P0=~dztBuff[1]; break; case 2: ADDR2=0; ADDR1=1; ADDR0=0; i++; P0=~dztBuff[2]; break; case 3: ADDR2=0; ADDR1=1; ADDR0=1; i++; P0=~dztBuff[3]; break; case 4: ADDR2=1; ADDR1=0; ADDR0=0; i++; P0=~dztBuff[4]; break; case 5: ADDR2=1; ADDR1=0; ADDR0=1; i++; P0=~dztBuff[5]; break; case 6: ADDR2=1; ADDR1=1; ADDR0=0; i++; P0=~dztBuff[6]; break; case 7: ADDR2=1; ADDR1=1; ADDR0=1; i=0; P0=~dztBuff[7]; break; default: break; } } //按键扫描程序 void KeyScan() { unsigned char i; static unsigned char keyout = 0; //矩阵按键扫描输出索引 static unsigned char keybuf[4][4] = { //矩阵按键扫描缓冲区 {0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF} }; //将一行的4个按键值移入缓冲区 keybuf[keyout][0] = (keybuf[keyout][0] << 1) | KEY_IN_1; keybuf[keyout][1] = (keybuf[keyout][1] << 1) | KEY_IN_2; keybuf[keyout][2] = (keybuf[keyout][2] << 1) | KEY_IN_3; keybuf[keyout][3] = (keybuf[keyout][3] << 1) | KEY_IN_4; //消抖后更新按键状态 for (i=0; i<4; i++) //每行4个按键,所以循环4次 { if ((keybuf[keyout][i] & 0x07) == 0x00) { //连续4次扫描值为0,即4*4ms内都是按下状态时,可认为按键已稳定的按下 KeySta[keyout][i] = 0; } else if ((keybuf[keyout][i] & 0x07) == 0x07) { //连续4次扫描值为1,即4*4ms内都是弹起状态时,可认为按键已稳定的弹起 KeySta[keyout][i] = 1; } } //执行下一次的扫描输出 keyout++; //输出索引递增 keyout &= 0x03; //索引值加到4即归零 switch (keyout) //根据索引值,释放当前输出引脚,拉低下次的输出引脚 { case 0: KEY_OUT_4 = 1; KEY_OUT_1 = 0; break; case 1: KEY_OUT_1 = 1; KEY_OUT_2 = 0; break; case 2: KEY_OUT_2 = 1; KEY_OUT_3 = 0; break; case 3: KEY_OUT_3 = 1; KEY_OUT_4 = 0; break; default: break; } } /* 定时器1中断服务函数 */ void InterruptTimer1() interrupt 3 { static unsigned char cnt = 0; TH1 = 0xFC; //重新加载初值 TL1 = 0x66; cnt++; KeyScan(); if(cnt%2 == 0){ SmgRefresh(); }else{ DzlRefresh(); } } /* T0中断服务函数,执行串口接收监控和蜂鸣器驱动 */ void InterruptTimer0() interrupt 1 { static unsigned char cnt = 0; TH0 = 0xFD; //重新加载重载值 TL0 = 0x34; if (flagBuzzOn) //执行蜂鸣器鸣叫或关闭 { BUZZ = ~BUZZ; cnt++; if(cnt>240) { cnt = 0; flagBuzzOn = 0; } } else { BUZZ = 1; } }
代码只有525行,还包括注释和空行!!!
主要使用了状态机和随机种子来管理整个项目。
注释很完整了,有问题可以下方留言讨论哦~
三、效果演示
Bilibili:https://b23.tv/f12pdg(点击连接到B站看效果~)
可以完整实现贪吃蛇游戏的效果。
三、总结
- 状态机是一个很不错的东西,在裸机的情况下很实用。
- 兴趣是最好的老师,希望同学们能因此对单片机感兴趣,从而去学习它,单片机真的是个很有用的好东西!
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步