功能简介
一个贪吃蛇游戏,运行在51单片机入门开发板上,显示在8*8点阵上,可以发出声音
代码
main.c
/*贪吃蛇第二版,初始长度为3的贪吃蛇,可移动,撞到边界或自己会死亡
相比第一版,改变了按键位置,使操作更方便,禁止了贪吃蛇原地回头
增加了贪吃蛇吃豆功能
增加了自动移动,添加了游戏音效,添加了死亡动画*/
/*
连线说明:请将J24上OE与GND链接,从而使点阵屏能显示
操作说明:K1贪吃蛇向上移动、K2贪吃蛇向下移动、K3贪吃蛇向左移动、K4贪吃蛇向右移动
*/
/*
使用数组screen作为显示存储器,一个数组元素控制一列像素,screen[0]控制最左列像素,一个元素的第一位控制最下面一个像素,置1表示像素点亮,
置0表示像素点灭。8*8点阵,纵向取模,字节正序
*/
#include<reg51.h>
#include<intrins.h>
#include<Music.h> //音乐播放头文件
#include<queue.h> //队列操作头文件
#include <stdlib.h> //随机数头文件
#define uchar unsigned char
#define uint unsigned int
#define T0_NUM 1000
#define GPIO_KEY P1 //按键接口输入
#define DIE 0 //贪吃蛇死亡
#define EAT 1 //吃豆
#define MOV 2 //移动
#define NUL 3 //未定义指令
bit flag_s = 1;
uchar code number[]={0x00,0x00,0x22,0x7E,0x02,0x00,0x22,0x46,0x4A,0x32,0x00,0x42,0x52,0x52,0x6C,0x00,0x3C,
0x04,0x7E,0x04,0x00,0x72,0x52,0x52,0x4C,0x00,0x3C,0x52,0x52,0x0C,0x00,0x40,0x4E,
0x50,0x60,0x00,0x2C,0x52,0x52,0x2C,0x00,0x30,0x4A,0x4A,0x3C,}; //mun : num*5-4 ~ num*5
sbit Clock = P3^6; //Clock移位信号
sbit Data = P3^4; //Data数据输出
sbit Lock = P3^5; //Lock锁存信号
void send_74HC595(uchar LedVer); //向移位寄存器发送字节
/*以上是对点阵的纵向控制的准备,由于使用的入门开发板,点阵屏的纵向由移位寄存器控制,所以纵向数据需要通过移位寄存器输入*/
void JzKey(); //矩阵按键
void KeyCheck(); //按键执行
void MoveServ(uchar STA); //移动后服务
uchar CheckSnake(uchar dire); //贪吃蛇的检查与移动
unsigned char screen[8],die[]={0xFB,0x8B,0x73,0x8B,0xFB,0x8B,0xFB,0xAB}; //显存,死亡画面
//void DrawSnake(); //贪吃蛇显示程序,将贪吃蛇画在显存中
uchar KeyValue=0;
uchar key_step=0; // 按键执行步骤
SqQueue snake; //创建结构体snake
uchar Bean; //豆子坐标
bit flag_die = 0;
void DrawPoint(uchar point);
void NewBean();
void main()
{
TMOD &= 0X0F;
TMOD |= 0X10;
TH1 = (65536 - T0_NUM) / 256;
TL1 = (65536 - T0_NUM) % 256; //计时器0每2ms对点阵屏进行一次扫描
EA = 1;
ET1 = 1;
TR1 = 1;
MainInitialize_Music(); //音乐模块初始化
InitQueue(&snake); //贪吃蛇队列初始化
AddQueue(&snake,0x03); //为贪吃蛇添加两节身体
AddQueue(&snake,0x13);
DrawPoint(GetHead(&snake));
DrawPoint(GetRear(&snake));
NewBean();
while(1)
{
if(flag_s)
Auto_PlayMusic();
}
}
void DrawPoint(uchar point)
{
screen[point >> 4]|=0x01 << (point&0x0f); //画上点
}
void NewBean()
{
uchar i,j,s_rear;
s_rear = snake.rear;
for(i=0;i<8;i++)
for(j=0;j<8;j++)
if(!(screen[i]&(0x01<<j)))
AddQueue(&snake,(i<<4)+j);
snake.rear=s_rear;
srand(rand());
Bean = snake.base[(snake.rear+rand()%((snake.head+64-snake.rear)%64))%64];
DrawPoint(Bean);
}
/*下面是计时器0的中断服务程序,我们需要在下面的程序中完成,对按键的扫描,和屏幕的刷新,
如果按键按下,那么要在屏幕刷新前,完成对贪吃蛇位置的计算,和对显示存储数组的刷新*/
void T1_int() interrupt 3
{
static unsigned char NowVer = 0,fine = 0x80; //NowVer当前扫描列
TH1 = (65536 - T0_NUM) / 256;
TL1 = (65536 - T0_NUM) % 256; //重置计数器
if(!flag_die)
{
JzKey(); //矩阵按键检测
KeyCheck(); //执行按键效果
}
/*点阵屏刷新程序*/
P0 = 0xff; //位选信号(行信号)置零,消鬼
if(!flag_die)
send_74HC595(screen[NowVer]); //(列信号)赋值
else
send_74HC595(die[NowVer]); //死亡画面
P0 = ~(fine>>NowVer); //点阵行是低电平触发,数组内容是高电平有效,所以取反
NowVer++; //每中断1次(2ms),NowVer加1
if(NowVer>=8) //列回车判断,完成一次扫描周期 16ms
{
NowVer=0;
}
}
void JzKey() //矩阵按键程序
{
static uchar keybuf = 0; //按键缓冲区,记录按键的历史检测值
GPIO_KEY=0x0f; //矩阵按键低4位低电平,高四位为高电平,测试行
if(GPIO_KEY!=0x0f)
keybuf = (keybuf << 1) | 0;
else
keybuf = (keybuf << 1) | 1;
if ((keybuf == 0x00)&(key_step == 0)) //连续 8 次扫描值都为 0,即 16ms 内都只检测到按下状态时,可认为按键已按下
{
key_step = 1; //key_step=1:第一步,读取按键值
switch(GPIO_KEY)
{
case(0x07):KeyValue=1;break;
case(0x0b):KeyValue=2;break;
case(0x0d):KeyValue=3;break;
case(0x0e):KeyValue=4;break;
}
GPIO_KEY=0xf0;//与上相反,测试列
switch(GPIO_KEY)
{
case(0x70):KeyValue=KeyValue;break;
case(0xb0):KeyValue=KeyValue+4;break;
case(0xd0):KeyValue=KeyValue+8;break;
case(0xe0):KeyValue=KeyValue+12;break;
}
}
else if ((keybuf == 0xff)&(key_step==2)) //连续 8 次扫描值都为 1,即 16ms 内都只检测到弹起状态时,可认为按键已弹起
{
key_step = 0; //第三步,归零步骤
}
//其它情况则说明按键状态尚未稳定
}
void KeyCheck() //按键检测程序,执行按键命令
{
if(key_step==1) //按键状态变换
{
key_step = 2; //key_step=2:第二步,执行按键命令
MoveServ(CheckSnake(KeyValue));
}
}
uchar CheckSnake(uchar dire) //执行蛇头移动,dire:移动方向
{
uchar SnakeHead,x,y;
static uchar back = 16; //初始移动方向
SnakeHead = GetRear(&snake);
x = SnakeHead >> 4; //0~7
y = SnakeHead & 0x0f; //0~7
switch(dire) //第一步,检查是否越界或回头
{
case 11: //向上移动
if(back==15)
return NUL;
else if(y < 7)
SnakeHead += 0x01;
else
return DIE;
break;
case 15: //向下移动
if(back==11)
return NUL;
else if(y > 0)
SnakeHead -= 0x01;
else
return DIE;
break;
case 14: //向左移动
if(back==16)
return NUL;
else if(x > 0)
SnakeHead -= 0x10;
else
return DIE;
break;
case 16: //向右移动
if(back==14)
return NUL;
else if(x < 7)
SnakeHead += 0x10;
else
return DIE;
break;
case 1:
flag_s = !flag_s;
default:
return NUL;
break;
}
// P2=~SnakeHead>>4;
back = dire;
if(SnakeHead == Bean) //二步,检查是否吃豆
{
AddQueue(&snake,SnakeHead); //新蛇头
DrawPoint(GetRear(&snake));
// screen[(GetRear(&snake)>>4)]|=0x01 << (GetRear(&snake)&0x0f); //画上蛇头
NewBean();
return EAT;
}
else if(screen[SnakeHead>>4] & (0x01 << (SnakeHead&0x0f))) //第三步,检查是否自食
return DIE;
else
{
AddQueue(&snake,SnakeHead); //新蛇头
DrawPoint(GetRear(&snake));
// screen[(GetRear(&snake)>>4)]|=0x01 << (GetRear(&snake)&0x0f); //画上蛇头
screen[(GetHead(&snake)>>4)]&=~(0x01 << (GetHead(&snake)&0x0f)); //擦除蛇尾
DelQueue(&snake); //弃蛇尾
return MOV;
}
}
void MoveServ(uchar STA) //移动后服务
{
switch(STA) //检查移动状态
{
case MOV:
WriteMusicOrder(1);
break;
case NUL:
WriteMusicOrder(2); break;
case EAT:
WriteMusicOrder(3);break;
case DIE:
flag_die = 1; break;
}
}
//void DrawSnake() //贪吃蛇显示程序,将贪吃蛇画在显存中
//{
//
// screen[(GetHead(&snake)>>4)]&=~(0x01 << (GetHead(&snake)&0x0f));
// screen[(GetRear(&snake)>>4)]|=0x01 << (GetRear(&snake)&0x0f);
//
// //snake高四位作为x坐标,低四位作为y坐标
// //x坐标控制点画在屏幕的第几列,y坐标控制点向上移动多少行
//}
void send_74HC595(uchar LedVer) //发送列信号LedVer
{
uchar i,temp = 0x80;
for(i = 0;i<8;i++)
{
if( LedVer&(temp>>i) ) //按位分解LedVer
Data = 1;
else
Data = 0;
Clock = 0;
Clock = 1; //移位
}
Lock = 0;
Lock = 1; //锁存
}
queue.h
/*********************queue.h*************************************/
typedef unsigned char Status; // Status 是函数的类型,其值是函数结果状态代码,如 OK 等
typedef unsigned char QElemType;
#define uchar unsigned char
#define MAXQSIZE 64 // 最大队列长度(对于循环队列,最大队列长度要减 1)
typedef struct
{
uchar base[MAXQSIZE]; // 初始化的静态分配内存
uchar head; // 头指针,若队列不空,指向队列头元素
uchar rear; // 尾指针,若队列不空,指向队列尾元素的下一个位置
}SqQueue;
//--定义使用的IO口--//
//--声明调用函数--//
Status InitQueue(SqQueue *Q);
Status AddQueue(SqQueue *Q,QElemType e); //new蛇头
Status DelQueue(SqQueue *Q); //del蛇尾
Status GetHead(SqQueue* Q); //get蛇尾
Status GetRear(SqQueue* Q); //get蛇头
Status QueueLength(SqQueue *Q); //get蛇长
queue.c
/*纯c语言队列*/
#include<stdio.h>
#define OK 1
#define ERROR 0
//typedef unsigned char uchar; // Status 是函数的类型,其值是函数结果状态代码,如 OK 等
//typedef unsigned char Status;
//typedef unsigned char QElemType;
//#define MAXQSIZE 64 // 最大队列长度(对于循环队列,最大队列长度要减 1)
#include<queue.h> //队列操作头文件
//typedef struct
//{
// uchar base[MAXQSIZE]; // 初始化的静态分配内存
// uchar head; // 头指针,若队列不空,指向队列头元素
// uchar rear; // 尾指针,若队列不空,指向队列尾元素的下一个位置
//}SqQueue;
Status InitQueue(SqQueue *Q)
{
// 初始化空队列Q,
(*Q).head=(*Q).rear=0;
return OK;
}
Status AddQueue(SqQueue *Q,QElemType e) //new蛇头
{
// 插入元素e为Q的新的队尾元素,如果队伍满了怎么办 (*Q)
if(((*Q).rear+1)%MAXQSIZE==(*Q).head)
{
return ERROR;
}
(*Q).base[(*Q).rear]=e;
(*Q).rear=((*Q).rear+1)%MAXQSIZE;
return OK;
}
Status DelQueue(SqQueue *Q) //del蛇尾
{
// 若队列不空, 则删除Q的队头元素,并返回e; 否则返回ERROR
uchar e;
if((*Q).head==(*Q).rear)
{
return ERROR;
}
e=(*Q).base[(*Q).head];
(*Q).head=((*Q).head+1)%MAXQSIZE;
return e;
}
Status GetHead(SqQueue* Q) //get蛇尾
{
// 若队列不空,则返回队头元素,否则返回ERROR
if((*Q).head==(*Q).rear)
{
return ERROR;
}
return (*Q).base[(*Q).head];
}
Status GetRear(SqQueue* Q) //get蛇头
{
return (*Q).base[((*Q).rear+MAXQSIZE-1)%MAXQSIZE];
}
Status QueueLength(SqQueue *Q) //get蛇长
{
// 返回Q的元素个数
return ((*Q).rear-(*Q).head+MAXQSIZE)%MAXQSIZE;
}
Music.h
/*********************Music.h*************************************/
#define uchar unsigned char
#define uint unsigned int
//--定义使用的IO口--//
sbit BUZZ = P2^5; //蜂鸣器控制引脚
//--声明调用函数--//
void WriteMusicOrder(uchar num);
void MainInitialize_Music(); //主函数初始化函数
void Auto_PlayMusic(); //预设音乐演奏函数
uchar PlayMusic(uchar *MusicNote,uchar *MusicBeat,uchar MusicLong);
//音乐播放函数,音符数组,节拍数组,音符/节拍长度
Music.c
/* ---------------------------
Music音乐播放函数
----------------------------*/
#include <reg52.h>
#include<string.h>
#include<Music.h>
#define uchar unsigned char
#define uint unsigned int
unsigned char T0RH = 0xFF; //T0重载值的高字节
unsigned char T0RL = 0x00; //T0重载值的低字节
uchar MusicOrder=0; //音乐播放器控制指令
unsigned int code NoteFrequ[] = { //低音1-7中音1-7和高音1-7对应频率列表
262, 294, 330, 349, 392, 440, 494, //低音1-7
523, 587, 659, 698, 784, 880, 988, //中音1-7
1047, 1175, 1319, 1397, 1568, 1760, 1976 //高音1-7
};
unsigned int code NoteReload[] = { //低音1-7中音1-7和高音1-7对应的定时器重载值
65536 - (11059200/12) / (262*2), //低音1
65536 - (11059200/12) / (294*2), //2
65536 - (11059200/12) / (330*2), //3
65536 - (11059200/12) / (349*2), //4
65536 - (11059200/12) / (392*2), //5
65536 - (11059200/12) / (440*2), //6
65536 - (11059200/12) / (494*2), //7
65536 - (11059200/12) / (523*2), //中音1
65536 - (11059200/12) / (587*2), //2
65536 - (11059200/12) / (659*2), //3
65536 - (11059200/12) / (698*2), //4
65536 - (11059200/12) / (784*2), //5
65536 - (11059200/12) / (880*2), //6
65536 - (11059200/12) / (988*2), //7
65536 - (11059200/12) / (1047*2), //高音1
65536 - (11059200/12) / (1175*2), //2
65536 - (11059200/12) / (1319*2), //3
65536 - (11059200/12) / (1397*2), //4
65536 - (11059200/12) / (1568*2), //5
65536 - (11059200/12) / (1760*2), //6
65536 - (11059200/12) / (1976*2), //7
};
//预设音符表1
uchar code NormolNote[]={10,14};
uchar code NormolBeat[]={1,1};
//预设音符表2
uchar code ErrorNote[]={4,2};
uchar code ErrorBeat[]={1,1};
//预设音符表3
uchar code GoodNote[]={17,19,20};
uchar code GoodBeat[]={1,1,2};
bit enable = 0; //蜂鸣器发声使能标志
bit tmrflag = 0; //定时器中断完成标志
void WriteMusicOrder(uchar num)
{
MusicOrder=num;
}
void MainInitialize_Music()
{
EA = 1; //使能全局中断
TMOD &= 0xf0; //配置T0工作在模式1
TMOD |= 0x01;
TH0 = T0RH;
TL0 = T0RL;
ET0 = 1; //使能T0中断
TR0 = 1; //启动T0
}
void Auto_PlayMusic() //预设音乐演奏函数,放在main函数while(1)循环中
{
switch(MusicOrder)
{
case 1: PlayMusic(NormolNote,NormolBeat,2);break;
case 2: PlayMusic(ErrorNote,ErrorBeat,2);break;
case 3: PlayMusic(GoodNote,GoodBeat,3);break;
}
}
/* 乐曲播放函数 */
uchar PlayMusic(uchar *MusicNote,uchar *MusicBeat,uchar MusicLong)
{
unsigned char beat; //当前节拍索引
unsigned char note; //当前节拍对应的音符
unsigned int time = 0; //当前节拍计时
unsigned int beatTime = 0; //当前节拍总时间
unsigned int soundTime = 0; //当前节拍需发声时间
MusicOrder=0; //音乐指令
for (beat=0; beat<MusicLong; ) //用节拍索引作为循环变量
{
while (!tmrflag); //每次定时器中断完成后,检测并处理节拍
tmrflag = 0;
if(MusicOrder!=0) //检测到中止信号后退出程序
return 0;
if (time == 0) //当前节拍播完则启动一个新节拍
{
note = *(MusicNote+beat) - 1;
T0RH = NoteReload[note] >> 8;
T0RL = NoteReload[note];
//计算节拍总时间,右移2位相当于除4,移位代替除法可以加快执行速度
beatTime = ((*(MusicBeat+beat)) * NoteFrequ[note]) >> 2;
//计算发声时间,为总时间的0.75,移位原理同上
soundTime = beatTime - (beatTime >> 2);
enable = 1; //指示蜂鸣器开始发声
time++;
}
else //当前节拍未播完则处理当前节拍
{
if (time >= beatTime) //当前持续时间到达节拍总时间时归零,
{ //并递增节拍索引,以准备启动新节拍
time = 0;
beat++;
}
else //当前持续时间未达到总时间时,
{
time++; //累加时间计数
if (time == soundTime) //到达发声时间后,指示关闭蜂鸣器,
{ //插入0.25*总时间的静音间隔,
enable = 0; //用以区分连续的两个节拍
}
}
}
}
return 0;
}
/* T0中断服务函数,用于控制蜂鸣器发声 */
void InterruptTimer0() interrupt 1
{
TH0 = T0RH; //重新加载重载值
TL0 = T0RL;
tmrflag = 1;
if (enable) //使能时反转蜂鸣器控制电平
BUZZ = ~BUZZ;
else //未使能时关闭蜂鸣器
BUZZ = 1;
}