【制作】基於金沙灘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站看效果~)
可以完整實現貪吃蛇游戲的效果。

三、總結

  • 狀態機是一個很不錯的東西,在裸機的情況下很實用。
  • 興趣是最好的老師,希望同學們能因此對單片機感興趣,從而去學習它,單片機真的是個很有用的好東西!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM