zoukankan      html  css  js  c++  java
  • 【制作】基于金沙滩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站看效果~)
    可以完整实现贪吃蛇游戏的效果。

    三、总结

    • 状态机是一个很不错的东西,在裸机的情况下很实用。
    • 兴趣是最好的老师,希望同学们能因此对单片机感兴趣,从而去学习它,单片机真的是个很有用的好东西!
  • 相关阅读:
    MySQL日期数据类型、时间类型使用总结
    mybatis中的mapper接口文件以及example类的实例函数以及详解
    IDEA 的快捷键简单使用
    Enum强制转换
    保存信息到配置文件
    通过配置文件判断程序首次启动
    StackPanel Binding
    RadGridView样式设置与Binding
    虚拟键盘输入之回车事件绑定与鼠标点击事件绑定
    数据库基础之-范式
  • 原文地址:https://www.cnblogs.com/minuy/p/13999199.html
Copyright © 2011-2022 走看看