【制作】基於金沙灘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站看效果~)
可以完整實現貪吃蛇游戲的效果。
三、總結
- 狀態機是一個很不錯的東西,在裸機的情況下很實用。
- 興趣是最好的老師,希望同學們能因此對單片機感興趣,從而去學習它,單片機真的是個很有用的好東西!