【制作】基于金沙滩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站看效果~)
可以完整实现贪吃蛇游戏的效果。
三、总结
- 状态机是一个很不错的东西,在裸机的情况下很实用。
- 兴趣是最好的老师,希望同学们能因此对单片机感兴趣,从而去学习它,单片机真的是个很有用的好东西!