【制作】基于金沙滩51单片机的贪吃蛇程序

【制作】基于金沙滩51单片机的贪吃蛇程序

零、起因

要离开实验室了,但是还是有点不放心学弟们的学习,为了让他们知道单片机能干嘛,体会到单片机的快乐,特意作此程序,以提高他们对单片机的学习兴趣。
要实现以下功能:

  1. 食物根据随机种子的不同出现的序列也不同
  2. 经典贪吃蛇游戏,能穿墙
  3. 贪吃蛇速度随分数加快,分数越高,贪吃蛇速度越快
  4. 能显示分数

一、电路原理图

用的是金沙滩的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站看效果~)
可以完整实现贪吃蛇游戏的效果。

三、总结

  • 状态机是一个很不错的东西,在裸机的情况下很实用。
  • 兴趣是最好的老师,希望同学们能因此对单片机感兴趣,从而去学习它,单片机真的是个很有用的好东西!
原文地址:https://www.cnblogs.com/minuy/p/13999199.html