首页 > 其他分享 >基于51单片机和LCD1602的自制独立按键控制的小游戏《贪吃蛇》

基于51单片机和LCD1602的自制独立按键控制的小游戏《贪吃蛇》

时间:2024-12-11 18:27:25浏览次数:6  
标签:16 0x00 void 51 unsigned char 单片机 LCD 小游戏

目录

系列文章目录


前言

《贪吃蛇》,一款经典的、怀旧的小游戏,单片机入门必写程序。

基于51单片机和8X8LED点阵屏(板载74HC595驱动)的矩阵键盘控制的小游戏《贪吃蛇》
基于51单片机和8X8LED点阵屏(MAX7219驱动)的自制独立按键控制的小游戏《贪吃蛇》
基于51单片机和16X16LED点阵屏(MAX7219驱动)的自制独立按键控制的小游戏《贪吃蛇》
基于51单片机和16X16LED点阵屏(MAX7219驱动)的普中开发板矩阵按键控制的小游戏《贪吃蛇》

之前做了8X8LED点阵屏和16X16LED点阵屏的贪吃蛇小游戏,现在做一个LCD1602显示的贪吃蛇小游戏。

用到51单片机最小开发板,用八位自制的独立按键控制,单片机芯片为STC89C52RC,晶振@12.0000MHz。用LCD1602进行显示。

B站对应视频下载链接里有普中开发板矩阵按键版本,有需要的可以前去下载。

效果查看/操作演示:B站搜索“甘腾胜”或“gantengsheng”查看。
源代码下载:B站对应视频的简介有下载链接。

一、效果展示

在这里插入图片描述

二、原理分析

整个程序的框架跟点阵屏的差不多,游戏原理也是一样的,不再赘述,不清楚的可以点击前言中8X8LED点阵屏的两个链接查看。

LCD1602贪吃蛇小游戏的难点在于怎么显示,LCD1602只能自定义8个字符,即CGRAM中只能写入64个字节(一个自定义字符的字模需要8个字节,用到每个字节的低5位)。想要显示多个汉字或者蛇身,则需要将整个屏幕分成四个区域,再动态扫描显示。具体一点的原理可以看我写的另一篇文章《基于51单片机和LCD1602的多汉字动态扫描显示》
在这里插入图片描述
该如何显示蛇身呢?如下图所示,LCD1602总共有16X2=32个点阵,每个点阵的像素是5X8=40个,并且相邻点阵之间相隔一个像素,我们可以以4个像素(2X2)作为蛇身的最小单位,这样,“最小单位之间”的间隔是一样的,都是相隔一个像素。
在这里插入图片描述
跟多汉字显示不同,汉字的字模保存到ROM中,数据不能改变,显示出来的蛇是需要不停移动的,食物也是真随机生成的,所以动态显示对应的数据是要不断改变的。所以,需要定义一个显示缓存数组,生成新的食物或者蛇移动了,更改缓存数组的数据就行了,剩下的交给定时器和显示函数动态扫描。具体怎么转换数据和动态扫描显示,大家可以自行分析一下代码。

屏幕布局:左半屏是游戏区域,对应96个“最小单位”(每一个点阵可以显示6个“最小单位”),所以需要一个大小为96的一位数组(数据类型为无符号字符型)存储蛇身数据,高四位(范围:0~15)对应1~16列,低四位(范围:0~5)对应1~6行,用一个无符号字符型变量(unsigned char)存储蛇身的行列数据,动态扫描的显示缓存也是需要96个无符号字符型数据。右半屏实时显示得分(即蛇的长度)和已玩时间。

游戏结束全屏闪烁,可以用LCD1602的两个指令实现。屏幕显示的指令:0x0C,屏幕不显示的指令:0x08。

注意:如果用下图所示的小屏,也能玩,不过上下两行的间距较大,不是一个像素的距离,游戏效果差一些。
在这里插入图片描述

三、各模块代码

1、定时器0

h文件

#ifndef __TIMER0_H__
#define __TIMER0_H__

void Timer0_Init(void);

#endif

c文件

#include <REGX52.H>

/**
  * @brief	定时器0初始化,1毫秒@12.000MHz
  * @param  无
  * @retval 无
  */
void Timer0_Init(void)
{
	TMOD&=0xF0;	//设置定时器模式(高四位不变,低四位清零)
	TMOD|=0x01;	//设置定时器模式(通过低四位设为“定时器0工作方式1”的模式)
	TL0=0x18;	//设置定时初值,定时1ms
	TH0=0xFC;	//设置定时初值,定时1ms
	TF0=0;	//清除TF0标志
	TR0=1;	//定时器0开始计时
	ET0=1;	//打开定时器0中断允许
	EA=1;	//打开总中断
	PT0=0;	//当PT0=0时,定时器0为低优先级,当PT0=1时,定时器0为高优先级
}

/*定时器中断函数模板
void Timer0_Routine() interrupt 1	//定时器0中断函数
{
	static unsigned int T0Count;	//定义静态变量
	TL0=0x18;	//设置定时初值,定时1ms
	TH0=0xFC;	//设置定时初值,定时1ms
	T0Count++;
	if(T0Count>=1000)
	{
		T0Count=0;
		
	}
}
*/

2、八位独立按键

h文件

#ifndef __KEYSCAN_8_H__
#define __KEYSCAN_8_H__

unsigned char Key(void);
void Key_Tick(void);

#endif

c文件

#include <REGX52.H>

sbit Key1=P1^0;
sbit Key2=P1^1;
sbit Key3=P1^2;
sbit Key4=P1^3;
sbit Key5=P1^4;
sbit Key6=P1^5;
sbit Key7=P1^6;
sbit Key8=P1^7;

unsigned char KeyNumber;

/**
  * @brief  获取独立按键键码
  * @param  无
  * @retval 按下按键的键码,范围:0,1~24,0表示无按键按下
  */
unsigned char Key(void)
{
	unsigned char KeyTemp=0;
	KeyTemp=KeyNumber;
	KeyNumber=0;	//主程序中获取键码值之后键码值清零,在下一次定时器扫描按键之前再次获取键码值,一定会返回0
	return KeyTemp;
}

/**
  * @brief  获取当前按下按键的状态,无消抖及松手检测
  * @param  无
  * @retval 按键值,范围:0~8,无按键按下时返回值为0
  */
unsigned char Key_GetState()
{
	unsigned char KeyValue=0;
	
	if(Key1==0){KeyValue=1;}
	if(Key2==0){KeyValue=2;}
	if(Key3==0){KeyValue=3;}
	if(Key4==0){KeyValue=4;}
	if(Key5==0){KeyValue=5;}
	if(Key6==0){KeyValue=6;}
	if(Key7==0){KeyValue=7;}
	if(Key8==0){KeyValue=8;}
	
	return KeyValue;
}


/**
  * @brief  按键驱动函数,在中断中调用
  * @param  无
  * @retval 无
  */
void Key_Tick(void)
{
	static unsigned char NowState,LastState;
	LastState=NowState;	//按键状态更新
	NowState=Key_GetState();	//获取当前按键状态
	
	//如果上个时间点按键未按下,这个时间点按键按下,则是按下瞬间
	if(LastState==0)
	{
		switch(NowState)
		{
			case 1:KeyNumber=1;break;
			case 2:KeyNumber=2;break;
			case 3:KeyNumber=3;break;
			case 4:KeyNumber=4;break;
			case 5:KeyNumber=5;break;
			case 6:KeyNumber=6;break;
			case 7:KeyNumber=7;break;
			case 8:KeyNumber=8;break;
			default:break;
		}
	}
	
	//如果上个时间点按键按下,这个时间点按键按下,则是一直按住按键
	if(LastState && NowState)
	{
		if(LastState==1 && NowState==1){KeyNumber=9;}
		if(LastState==2 && NowState==2){KeyNumber=10;}
		if(LastState==3 && NowState==3){KeyNumber=11;}
		if(LastState==4 && NowState==4){KeyNumber=12;}
		if(LastState==5 && NowState==5){KeyNumber=13;}
		if(LastState==6 && NowState==6){KeyNumber=14;}
		if(LastState==7 && NowState==7){KeyNumber=15;}
		if(LastState==8 && NowState==8){KeyNumber=16;}
	}
	
	//如果上个时间点按键按下,这个时间点按键未按下,则是松手瞬间
	if(NowState==0)
	{
		switch(LastState)
		{
			case 1:KeyNumber=17;break;
			case 2:KeyNumber=18;break;
			case 3:KeyNumber=19;break;
			case 4:KeyNumber=20;break;
			case 5:KeyNumber=21;break;
			case 6:KeyNumber=22;break;
			case 7:KeyNumber=23;break;
			case 8:KeyNumber=24;break;
			default:break;
		}
	}
}

3、LCD1602

h文件

#ifndef __LCD1602_H__
#define __LCD1602_H__

void LCD_WriteCommand(unsigned char Command);
void LCD_WriteData(unsigned char Data);
void LCD_SetCursor(unsigned char Line,unsigned char Column);
void LCD_Init();
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char);
void LCD_ShowString(unsigned char Line,unsigned char Column,char *String);
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length);
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_MakeChar(void);
void LCD_Clear(void);
void LCD_MoveLeft(void);
void LCD_MoveRight(void);
void LCD_Convert_MoveLeft(unsigned char *Array,Part,Offset);
void LCD_Convert_MoveUp(unsigned char *Array,Part,Offset);
void LCD_ShowChinese(unsigned char Part,PartSum);

#endif

c文件

#include <REGX52.H>

//引脚配置:
sbit LCD_RS=P2^5;
sbit LCD_RW=P2^6;
sbit LCD_EN=P2^7;
#define LCD_DataPort P0

//自定义字符,最多8个(8个字节对应一个字符)
unsigned char idata CGRAMData[64];

//函数定义:
/**
  * @brief  LCD1602私有延时函数,12MHz调用可延时35us
  * @param  无
  * @retval 无
  */
void LCD_Delay35us(void)
{
	unsigned char i;
	i=15;
	while(--i);
}

/**
  * @brief  LCD1602私有延时函数,12MHz调用可延时2ms
  * @param  无
  * @retval 无
  */
void LCD_Delay2ms(void)
{
	unsigned char i, j;
	i=4;
	j=225;
	do
	{
		while(--j);
	}while(--i);
}

/**
  * @brief  LCD1602写指令
  * @param  Command 要写入的指令
  * @retval 无
  */
void LCD_WriteCommand(unsigned char Command)
{
	LCD_RS=0;
	LCD_RW=0;
	LCD_DataPort=Command;
	LCD_EN=1;
	LCD_Delay35us();
	LCD_EN=0;
	LCD_Delay35us();
}

/**
  * @brief  LCD1602写数据
  * @param  Data 要写入的数据
  * @retval 无
  */
void LCD_WriteData(unsigned char Data)
{
	LCD_RS=1;
	LCD_RW=0;
	LCD_DataPort=Data;
	LCD_EN=1;
	LCD_Delay35us();
	LCD_EN=0;
	LCD_Delay35us();
}

/**
  * @brief  LCD1602设置光标位置
  * @param  Line 行位置,范围:1~2
  * @param  Column 列位置,范围:1~16
  * @retval 无
  */
void LCD_SetCursor(unsigned char Line,unsigned char Column)
{
	if(Line==1)
	{
		LCD_WriteCommand(0x80|(Column-1));
	}
	else if(Line==2)
	{
		LCD_WriteCommand(0x80|(Column-1+0x40));
	}
}

/**
  * @brief  LCD1602初始化函数
  * @param  无
  * @retval 无
  */
void LCD_Init()
{
	LCD_WriteCommand(0x38);	//八位数据接口,两行显示,5*7点阵
	LCD_WriteCommand(0x0C);	//显示开,光标关,闪烁关
	LCD_WriteCommand(0x06);	//数据读写操作后,光标自动加一,画面不动
	LCD_WriteCommand(0x01);	//光标复位,清屏
	LCD_Delay2ms();	//清屏指令执行需要较长时间,需要较长的延时
}

/**
  * @brief  在LCD1602指定位置上显示一个字符
  * @param  Line 行位置,范围:1~2
  * @param  Column 列位置,范围:1~16
  * @param  Char 要显示的字符
  * @retval 无
  */
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char)
{
	LCD_SetCursor(Line,Column);
	LCD_WriteData(Char);
}

/**
  * @brief  在LCD1602指定位置开始显示所给字符串
  * @param  Line 起始行位置,范围:1~2
  * @param  Column 起始列位置,范围:1~16
  * @param  String 要显示的字符串
  * @retval 无
  */
void LCD_ShowString(unsigned char Line,unsigned char Column,char *String)
{
	unsigned char i;
	LCD_SetCursor(Line,Column);
	for(i=0;String[i]!='\0';i++)
	{
		LCD_WriteData(String[i]);
	}
}

/**
  * @brief  返回值=X的Y次方
  */
int LCD_Pow(int X,int Y)
{
	unsigned char i;
	int Result=1;
	for(i=0;i<Y;i++)
	{
		Result*=X;
	}
	return Result;
}

/**
  * @brief  在LCD1602指定位置开始显示所给数字
  * @param  Line 起始行位置,范围:1~2
  * @param  Column 起始列位置,范围:1~16
  * @param  Number 要显示的数字,范围:0~65535
  * @param  Length 要显示数字的长度,范围:1~5
  * @retval 无
  */
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
	unsigned char i;
	LCD_SetCursor(Line,Column);
	for(i=Length;i>0;i--)
	{
		LCD_WriteData(Number/LCD_Pow(10,i-1)%10+'0');
	}
}

/**
  * @brief  在LCD1602指定位置开始以有符号十进制显示所给数字
  * @param  Line 起始行位置,范围:1~2
  * @param  Column 起始列位置,范围:1~16
  * @param  Number 要显示的数字,范围:-32768~32767
  * @param  Length 要显示数字的长度,范围:1~5
  * @retval 无
  */
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length)
{
	unsigned char i;
	unsigned int Number1;
	LCD_SetCursor(Line,Column);
	if(Number>=0)
	{
		LCD_WriteData('+');
		Number1=Number;
	}
	else
	{
		LCD_WriteData('-');
		Number1=-Number;
	}
	for(i=Length;i>0;i--)
	{
		LCD_WriteData(Number1/LCD_Pow(10,i-1)%10+'0');
	}
}

/**
  * @brief  在LCD1602指定位置开始以十六进制显示所给数字
  * @param  Line 起始行位置,范围:1~2
  * @param  Column 起始列位置,范围:1~16
  * @param  Number 要显示的数字,范围:0~0xFFFF
  * @param  Length 要显示数字的长度,范围:1~4
  * @retval 无
  */
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
	unsigned char i,SingleNumber;
	LCD_SetCursor(Line,Column);
	for(i=Length;i>0;i--)
	{
		SingleNumber=Number/LCD_Pow(16,i-1)%16;
		if(SingleNumber<10)
		{
			LCD_WriteData(SingleNumber+'0');
		}
		else
		{
			LCD_WriteData(SingleNumber-10+'A');
		}
	}
}

/**
  * @brief  在LCD1602指定位置开始以二进制显示所给数字
  * @param  Line 起始行位置,范围:1~2
  * @param  Column 起始列位置,范围:1~16
  * @param  Number 要显示的数字,范围:0~1111 1111 1111 1111
  * @param  Length 要显示数字的长度,范围:1~16
  * @retval 无
  */
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
	unsigned char i;
	LCD_SetCursor(Line,Column);
	for(i=Length;i>0;i--)
	{
		LCD_WriteData(Number/LCD_Pow(2,i-1)%2+'0');
	}
}

/**
  * @brief  在LCD1602的CGRAM中写入8个自定义字符的64个字节,每个5*8点阵横向取模,使用每个字节的低5位
  * @param  无
  * @retval 无
  */
void LCD_MakeChar(void)
{
	unsigned char i;
	LCD_WriteCommand(0x40);	//CGRAM起始地址为0x40
	for(i=0;i<64;i++)	//01XX XXXX,最多可以在CGRAM中写入64个数据,一个自定义字符对应8个数据
	{
		LCD_WriteData(CGRAMData[i]);
	}
}

/**
  * @brief  LCD1602的光标复位,清屏
  * @param  无
  * @retval 无
  */
void LCD_Clear(void)
{
	LCD_WriteCommand(0x01);
	LCD_Delay2ms();
}

/**
  * @brief  LCD1602的屏幕向左移动一个字符位,光标不动
  * @param  无
  * @retval 无
  */
void LCD_MoveLeft(void)
{
	LCD_WriteCommand(0x18);
}

/**
  * @brief  LCD1602的屏幕向右移动一个字符位,光标不动
  * @param  无
  * @retval 无
  */
void LCD_MoveRight(void)
{
	LCD_WriteCommand(0x1C);
}

/**
  * @brief  将汉字取模得到的数据进行处理,处理后保存到CGRAMData缓存数组里,以便写入LCD的CGRAM中
  * @param  Array 传递过来的地址
  * @param  Part LCD的区域编号(将整个屏幕分成四部分),范围:1~4
  * @param  Offset 偏移量,整体向左移动Offset*15个像素(本来应该是16个像素,舍去了汉字的最后一列了),即整体向左移动Offset个汉字
  * @retval 无
  */
void LCD_Convert_MoveLeft(unsigned char *Array,Part,Offset)	//向左移动
{
    unsigned char i;
	Array+=Offset*32;
	
	//将一个汉字拆成6个自定义字符
	if(Part==1)
	{
		for(i=0;i<16;i++){CGRAMData[i]=*(Array+i)>>3;}
		for(i=0;i<16;i++){CGRAMData[i+16]=(*(Array+i)<<2)|(*(Array+i+16)>>6);}
		for(i=0;i<16;i++){CGRAMData[i+32]=*(Array+i+16)>>1;}
		for(i=0;i<16;i++){CGRAMData[i+48]=*(Array+i+32)>>3;}
	}
	if(Part==2)
	{
		for(i=0;i<16;i++){CGRAMData[i]=(*(Array+i+32)<<2)|(*(Array+i+48)>>6);}
		for(i=0;i<16;i++){CGRAMData[i+16]=*(Array+i+48)>>1;}
		for(i=0;i<16;i++){CGRAMData[i+32]=*(Array+i+64)>>3;}
		for(i=0;i<16;i++){CGRAMData[i+48]=(*(Array+i+64)<<2)|(*(Array+i+80)>>6);}
	}
	if(Part==3)
	{
		for(i=0;i<16;i++){CGRAMData[i]=*(Array+i+80)>>1;}
		for(i=0;i<16;i++){CGRAMData[i+16]=*(Array+i+96)>>3;}
		for(i=0;i<16;i++){CGRAMData[i+32]=(*(Array+i+96)<<2)|(*(Array+i+112)>>6);}
		for(i=0;i<16;i++){CGRAMData[i+48]=*(Array+i+112)>>1;}
	}
	if(Part==4)
	{
		for(i=0;i<16;i++){CGRAMData[i]=*(Array+i+128)>>3;}
		for(i=0;i<16;i++){CGRAMData[i+16]=(*(Array+i+128)<<2)|(*(Array+i+144)>>6);}
		for(i=0;i<16;i++){CGRAMData[i+32]=*(Array+i+144)>>1;}
		for(i=0;i<16;i++){CGRAMData[i+48]=*(Array+i+160)>>3;}
	}
}

/**
  * @brief  将汉字取模得到的数据进行处理,处理后保存到CGRAMData缓存数组里,以便写入LCD的CGRAM中
  * @param  Array 传递过来的地址
  * @param  Part LCD的区域编号(将整个屏幕分成四部分),范围:1~4
  * @param  Offset 偏移量,整体向上移动Offset个像素
  * @retval 无
  */
void LCD_Convert_MoveUp(unsigned char *Array,Part,Offset)
{
    unsigned char i,m,n;
	m=Offset/16;
	n=Offset%16;
	Array+=m*192;
	if(Part==1)
	{
		for(i=0;i<16;i++)
		{
			if(i<16-n){CGRAMData[i]=*(Array+i+n)>>3;}
			else{CGRAMData[i]=*(Array+i+n+176)>>3;}
		}
		for(i=0;i<16;i++)
		{
			if(i<16-n){CGRAMData[i+16]=(*(Array+i+n)<<2)|(*(Array+i+16+n)>>6);}
			else{CGRAMData[i+16]=(*(Array+i+n+176)<<2)|(*(Array+i+16+n+176)>>6);}
		}
		for(i=0;i<16;i++)
		{
			if(i<16-n){CGRAMData[i+32]=*(Array+i+16+n)>>1;}
			else{CGRAMData[i+32]=*(Array+i+16+n+176)>>1;}
		}
		for(i=0;i<16;i++)
		{
			if(i<16-n){CGRAMData[i+48]=*(Array+i+32+n)>>3;}
			else{CGRAMData[i+48]=*(Array+i+32+n+176)>>3;}
		}
	}
	if(Part==2)
	{
		for(i=0;i<16;i++)
		{
			if(i<16-n){CGRAMData[i]=(*(Array+i+32+n)<<2)|(*(Array+i+48+n)>>6);}
			else{CGRAMData[i]=(*(Array+i+32+n+176)<<2)|(*(Array+i+48+n+176)>>6);}
		}
		for(i=0;i<16;i++)
		{
			if(i<16-n){CGRAMData[i+16]=*(Array+i+48+n)>>1;}
			else{CGRAMData[i+16]=*(Array+i+48+n+176)>>1;}
		}
		for(i=0;i<16;i++)
		{
			if(i<16-n){CGRAMData[i+32]=*(Array+i+64+n)>>3;}
			else{CGRAMData[i+32]=*(Array+i+64+n+176)>>3;}
		}
		for(i=0;i<16;i++)
		{
			if(i<16-n){CGRAMData[i+48]=(*(Array+i+64+n)<<2)|(*(Array+i+80+n)>>6);}
			else{CGRAMData[i+48]=(*(Array+i+64+n+176)<<2)|(*(Array+i+80+n+176)>>6);}
		}
	}
	if(Part==3)
	{
		for(i=0;i<16;i++)
		{
			if(i<16-n){CGRAMData[i]=*(Array+i+80+n)>>1;}
			else{CGRAMData[i]=*(Array+i+80+n+176)>>1;}
		}
		for(i=0;i<16;i++)
		{
			if(i<16-n){CGRAMData[i+16]=*(Array+i+96+n)>>3;}
			else{CGRAMData[i+16]=*(Array+i+96+n+176)>>3;}
		}
		for(i=0;i<16;i++)
		{
			if(i<16-n){CGRAMData[i+32]=(*(Array+i+96+n)<<2)|(*(Array+i+112+n)>>6);}
			else{CGRAMData[i+32]=(*(Array+i+96+n+176)<<2)|(*(Array+i+112+n+176)>>6);}
		}
		for(i=0;i<16;i++)
		{
			if(i<16-n){CGRAMData[i+48]=*(Array+i+112+n)>>1;}
			else{CGRAMData[i+48]=*(Array+i+112+n+176)>>1;}
		}
	}
	if(Part==4)
	{
		for(i=0;i<16;i++)
		{
			if(i<16-n){CGRAMData[i]=*(Array+i+128+n)>>3;}
			else{CGRAMData[i]=*(Array+i+128+n+176)>>3;}
		}
		for(i=0;i<16;i++)
		{
			if(i<16-n){CGRAMData[i+16]=(*(Array+i+128+n)<<2)|(*(Array+i+144+n)>>6);}
			else{CGRAMData[i+16]=(*(Array+i+128+n+176)<<2)|(*(Array+i+144+n+176)>>6);}
		}
		for(i=0;i<16;i++)
		{
			if(i<16-n){CGRAMData[i+32]=*(Array+i+144+n)>>1;}
			else{CGRAMData[i+32]=*(Array+i+144+n+176)>>1;}
		}
		for(i=0;i<16;i++)
		{
			if(i<16-n){CGRAMData[i+48]=*(Array+i+160+n)>>3;}
			else{CGRAMData[i+48]=*(Array+i+160+n+176)>>3;}
		}
	}
}

/**
  * @brief  扫描显示函数
  * @param  Part 要显示的区域的编号,范围:1~4(从左到右)
  * @retval 无
  */
void LCD_ShowChinese(unsigned char Part,PartSum)
{
	Part--;
	
	//显示一段时间后清空显示,防止多个区域都显示相同的内容
	//清空上一个区域的内容
	if(Part)
	{
		LCD_WriteCommand(0x80+(Part-1)*4);
		LCD_WriteData(0x20);	//清空显示,写“0x20”或直接写“20”都可以
		LCD_WriteData(0x20);
		LCD_WriteData(0x20);
		LCD_WriteData(0x20);
		LCD_WriteCommand(0xC0+(Part-1)*4);
		LCD_WriteData(0x20);
		LCD_WriteData(0x20);
		LCD_WriteData(0x20);
		LCD_WriteData(0x20);
	}
	else
	{
		LCD_WriteCommand(0x80+4*(PartSum-1));
		LCD_WriteData(0x20);
		LCD_WriteData(0x20);
		LCD_WriteData(0x20);
		LCD_WriteData(0x20);
		LCD_WriteCommand(0xC0+4*(PartSum-1));
		LCD_WriteData(0x20);
		LCD_WriteData(0x20);
		LCD_WriteData(0x20);
		LCD_WriteData(0x20);
	}
	
	//将CGRAMData数组中的64个数据写入CGRAM进行更新
	LCD_MakeChar();
	
	//在对应位置写入8个自定义字符,先写第一行,在写第二行
	LCD_WriteCommand(0x80+Part*4);
	LCD_WriteData(0);
	LCD_WriteData(2);
	LCD_WriteData(4);
	LCD_WriteData(6);
	LCD_WriteCommand(0xC0+Part*4);
	LCD_WriteData(1);
	LCD_WriteData(3);
	LCD_WriteData(5);
	LCD_WriteData(7);
}

四、主函数

main.c

/*
by甘腾胜@20241208
效果查看/操作演示:可以在B站搜索“甘腾胜”或“gantengsheng”查看
单片机:STC89C52RC
晶振:12T@12.0000MHz
外设:自制的8位独立按键、LCD1602
注意:因为用了动态扫描的方式显示汉字和蛇,所以会有些模糊,需要将对比度调大一些
*/

#include <REGX52.H>	//包含寄存器的定义
#include <STDLIB.H>	//包含随机函数的声明
#include "KeyScan_8.h"
#include "LCD1602.h"
#include "Timer0.h"

unsigned char KeyNum;	//存储获得的键码值的变量
unsigned char Mode;	//游戏模式,0:向上滚动显示游戏名称“《贪吃蛇》”,1:向左滚动显示汉字“难度: 1”,
					//2:难度选择界面(数字范围是1~5),3:游戏进行中,4:游戏结束全屏闪烁,
					//5:显示作者姓名和编程日期
unsigned char MoveSnakeFlag;	//移动蛇身的标志,1:移动,0:不移动
unsigned char Direction=1;	//蛇头移动的方向,1:向右,2:向上,3:向左,4:向下,游戏开始时默认向右移动
unsigned char LastDirection=1;	//蛇头上一次移动的方向,1:向右,2:向上,3:向左,4:向下,游戏开始时默认向右移动
unsigned char SnakeLength=2;	//蛇的长度,初始值为2
unsigned char SnakeHead=1;	//保存整条蛇的数据的数组(共96个数据,数据索引为:0~95)中,蛇头对应的数据的索引,蛇的初始长度为2,
							//开始时只用了两个数据(数组的第1个数据和第2个数据),蛇头对应的是第2个数据(索引为1),SnakeHead的范围:0~95
unsigned char GameOverFlag;	//游戏结束的标志,1:游戏结束,0:游戏未结束
unsigned char FlashFlag;	//闪烁的标志,1:不显示,0:显示
unsigned int Food;	//保存创造出来的食物的位置,高四位(范围:0~15)对应列(1~16列),低四位(范围:0~5)对应行(1~6行)
					//从左往右数,分别是1~16列,从上往下数,分别是1~6行
unsigned int Offset;	//偏移量,用来控制汉字滚动显示
unsigned char RollFlag;	//滚动的标志,1:滚动,0:不滚动
unsigned char RollDirection;	//滚动显示的方向,0:向左滚动,1:向上滚动
unsigned char ExecuteOnceFlag=1;	//各模式中只执行一次的标志,1:执行,0:不执行,默认为1
unsigned char SnakeMoveSpeed=100;	//蛇移动的速度,值越小,速度越快,上电默认1s移动一次(定时器计时时间为10ms)
unsigned char T0Count0,T0Count1,T0Count2,T0Count3;	//定时器计数的变量
unsigned char PauseFlag;	//暂停的标志,1:暂停,0:不暂停
unsigned char PartSum=4;	//LCD总共要动态扫描显示的区域数,范围:1~4
unsigned char PartSelect;	//LCD显示区域的选择,范围:0~PartSum-1(对应区域1~PartSum),中断中使用,用来切换显示的区域
unsigned char *Pointer;	//传递给滚动显示函数LCD_Convert_MoveLeft和LCD_Convert_MoveUp的地址(数组首地址)
unsigned char Difficulty=1;	//游戏难度,范围:1~5,上电后默认是难度1
unsigned char ChangedFlag;	//游戏难度选择界面,游戏难度有变动的标志,1:有变动,0:无变动,用于更新数字的显示
unsigned int GameTime;	//游戏时间,玩游戏时用来实时显示已玩时间
unsigned char pdata DisplayBuffer[160];	//显示缓存,用pdata修饰是因为片内RAM不够用,所以保存到片外RAM
unsigned char pdata SnakeBody[96];	//蛇身最长的长度是6X16=96,需要用96个数据记录蛇身的数据

//阴码(亮点为1),行列式取模,高位在左(所有取模都必须按这个格式,否则需要修改函数)
unsigned char code Table1[]={	// “《贪吃蛇》”
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,	//前面数据为0,是为了要做成向上滚动的效果

0x00,0x00,0x00,0x00,0x01,0x02,0x04,0x09,0x04,0x02,0x01,0x00,0x00,0x00,0x00,0x00,
0x10,0x24,0x48,0x90,0x20,0x40,0x80,0x00,0x80,0x40,0x20,0x90,0x48,0x24,0x10,0x00,/*"《",0*/
0x01,0x02,0x04,0x09,0x30,0xCF,0x00,0x00,0x1F,0x10,0x11,0x11,0x11,0x02,0x0C,0x70,
0x00,0x80,0x40,0x20,0x98,0xE6,0x40,0x80,0xF0,0x10,0x10,0x10,0x10,0x60,0x18,0x04,/*"贪",1*/
0x00,0x00,0x79,0x49,0x4A,0x4C,0x49,0x48,0x48,0x48,0x78,0x49,0x02,0x02,0x01,0x00,
0x80,0x80,0x00,0xFE,0x00,0x00,0xF8,0x08,0x10,0x60,0x80,0x00,0x02,0x02,0xFE,0x00,/*"吃",2*/
0x10,0x10,0x10,0x7D,0x55,0x56,0x54,0x54,0x7C,0x50,0x10,0x14,0x1E,0xE2,0x40,0x00,
0x20,0x10,0x10,0xFE,0x02,0x04,0x80,0x88,0x90,0xA0,0xC0,0x82,0x82,0x82,0x7E,0x00,/*"蛇",3*/
0x08,0x24,0x12,0x09,0x04,0x02,0x01,0x00,0x01,0x02,0x04,0x09,0x12,0x24,0x08,0x00,
0x00,0x00,0x00,0x00,0x80,0x40,0x20,0x90,0x20,0x40,0x80,0x00,0x00,0x00,0x00,0x00,/*"》",4*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,/*" ",5*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,/*" ",6*/
};

unsigned char code Table2[]={	// “难度: 1”
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,	//前面数据为0,是为了要做成向左滚动的效果

0x00,0x00,0xFC,0x05,0x05,0x4B,0x2D,0x29,0x11,0x11,0x29,0x25,0x45,0x81,0x01,0x01,
0xA0,0x90,0x80,0xFE,0x10,0x10,0xFC,0x10,0x10,0xFC,0x10,0x10,0x10,0xFE,0x00,0x00,/*"难",0*/
0x01,0x00,0x3F,0x22,0x22,0x3F,0x22,0x22,0x23,0x20,0x2F,0x24,0x42,0x41,0x86,0x38,
0x00,0x80,0xFE,0x20,0x20,0xFC,0x20,0x20,0xE0,0x00,0xF0,0x10,0x20,0xC0,0x30,0x0E,/*"度",1*/
0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x00,0x00,0x00,0x00,0x18,0x18,0x00,0x00,/*":",2*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,/*" ",3*/
0x00,0x00,0x00,0x00,0x07,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x0F,0x00,0x00,
0x00,0x00,0x00,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0xF8,0x00,0x00,/*"1",0*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,/*" ", */
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,/*" ", */
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,/*" ", */
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,/*" ", */
};

unsigned char code Table3[]={	//难度的数字(16X16)的字模:1~5
0x00,0x00,0x00,0x00,0x07,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x0F,0x00,0x00,
0x00,0x00,0x00,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0xF8,0x00,0x00,/*"1",1*/
0x00,0x00,0x00,0x0F,0x30,0x38,0x10,0x00,0x00,0x01,0x06,0x08,0x30,0x3F,0x00,0x00,
0x00,0x00,0x00,0xF0,0x18,0x0C,0x18,0x18,0x60,0x80,0x00,0x04,0x0C,0xF8,0x00,0x00,/*"2",2*/
0x00,0x00,0x00,0x0F,0x30,0x38,0x00,0x00,0x01,0x00,0x00,0x38,0x30,0x0F,0x00,0x00,
0x00,0x00,0x00,0xE0,0x18,0x18,0x18,0x60,0xF0,0x18,0x0C,0x0C,0x18,0xE0,0x00,0x00,/*"3",3*/
0x00,0x00,0x00,0x00,0x00,0x01,0x02,0x0C,0x10,0x20,0x7F,0x00,0x00,0x03,0x00,0x00,
0x00,0x00,0x00,0x30,0xF0,0x70,0x70,0x70,0x70,0x70,0xFE,0x70,0x70,0xFE,0x00,0x00,/*"4",4*/
0x00,0x00,0x00,0x1F,0x10,0x10,0x10,0x17,0x18,0x00,0x00,0x38,0x30,0x0F,0x00,0x00,
0x00,0x00,0x00,0xF8,0x00,0x00,0x00,0xF0,0x18,0x0C,0x0C,0x0C,0x18,0xE0,0x00,0x00,/*"5",5*/
};

/**
  * @brief  更新显示缓存数组DisplayBuffer的数据
  * @param  Position 需要更新显示的位置,字节高四位范围:0~15(对应1~16列),字节低四位范围:0~5(对应1~6行)
  * @param  State 需要更新成的状态,范围:0~1,0:熄灭,1:点亮
  * @retval 无
  */
void UpdateDisplay(unsigned char Position,State)
{
	char UpdateOffset;	//偏移量
	
	if(Position%16/3){UpdateOffset=-1;}	//4~6行的偏移量
	else{UpdateOffset=1;}	//1~3行的偏移量
	
	if(State==1)	//如果需要点亮
	{
		switch(Position/16%6)
		{
			case 0:
				DisplayBuffer[Position/16/3*16+Position%16*3] |= 0xC0;	//一个字节对应蛇的三个点(每个点有四个小点)
				DisplayBuffer[Position/16/3*16+Position%16*3+UpdateOffset] |= 0xC0;	//总共有16行
				break;
			case 1:
				DisplayBuffer[Position/16/3*16+Position%16*3] |= (0xC0>>3);
				DisplayBuffer[Position/16/3*16+Position%16*3+UpdateOffset] |= (0xC0>>3);
				break;
			case 2:
				DisplayBuffer[Position/16/3*16+Position%16*3] |= (0xC0>>5);
				DisplayBuffer[Position/16/3*16+Position%16*3+UpdateOffset] |= (0xC0>>5);
				break;
			case 3:
				DisplayBuffer[Position/16/3*16+Position%16*3] |= 0xC0;
				DisplayBuffer[Position/16/3*16+Position%16*3+UpdateOffset] |= 0xC0;
				break;
			case 4:
				DisplayBuffer[Position/16/3*16+Position%16*3] |= (0xC0>>2);
				DisplayBuffer[Position/16/3*16+Position%16*3+UpdateOffset] |= (0xC0>>2);
				break;
			case 5:
				DisplayBuffer[Position/16/3*16+Position%16*3] |= (0xC0>>5);
				DisplayBuffer[Position/16/3*16+Position%16*3+UpdateOffset] |= (0xC0>>5);
				break;
			default:break;
		}
	}
	else	//如果需要熄灭
	{
		switch(Position/16%6)
		{
			case 0:
				DisplayBuffer[Position/16/3*16+Position%16*3] &= ~0xC0;
				DisplayBuffer[Position/16/3*16+Position%16*3+UpdateOffset] &= ~0xC0;
				break;
			case 1:
				DisplayBuffer[Position/16/3*16+Position%16*3] &= ~(0xC0>>3);
				DisplayBuffer[Position/16/3*16+Position%16*3+UpdateOffset] &= ~(0xC0>>3);
				break;
			case 2:
				DisplayBuffer[Position/16/3*16+Position%16*3] &= ~(0xC0>>5);
				DisplayBuffer[Position/16/3*16+Position%16*3+UpdateOffset] &= ~(0xC0>>5);
				break;
			case 3:
				DisplayBuffer[Position/16/3*16+Position%16*3] &= ~0xC0;
				DisplayBuffer[Position/16/3*16+Position%16*3+UpdateOffset] &= ~0xC0;
				break;
			case 4:
				DisplayBuffer[Position/16/3*16+Position%16*3] &= ~(0xC0>>2);
				DisplayBuffer[Position/16/3*16+Position%16*3+UpdateOffset] &= ~(0xC0>>2);
				break;
			case 5:
				DisplayBuffer[Position/16/3*16+Position%16*3] &= ~(0xC0>>5);
				DisplayBuffer[Position/16/3*16+Position%16*3+UpdateOffset] &= ~(0xC0>>5);
				break;
			default:break;
		}
	}

}

/**
  * @brief  创造出随机位置的食物,数据的高四位(范围:0~15)代表食物所在的列(1~16),数据的低四位(范围:0~5)代表食物所在的行(1~6)
  * @brief  从左往右数,分别是1~16列,从上往下数,分别是1~6行
  * @param  无
  * @retval 创造出的食物位置的数据
  */
unsigned char CreateFood(void)
{
	unsigned char FoodTemp;
	unsigned char i,j,m,n;
	m=rand()%16;	//产生一个0~15的随机数
	n=rand()%6;	//产生一个0~5的随机数
	for(j=0;j<6;j++)	//产生一个随机位置,判断该位置是否是蛇身,如果不是,就返回该位置所对应的数据
	{					//如果该位置不是蛇身的位置,则从该点向周围寻找不是蛇身的空位置
		for(i=0;i<16;i++)
		{
//			if( DisplayBuffer[((m+i)%16)/3*16+((n+j)%6)*3] & (0x80>>(m+i)%16%3*3) == 0 )	//这样写返回的值一定是0x00,暂不知道什么原因
			if( !(DisplayBuffer[(m+i)%16/3*16+(n+j)%6*3] & (0x80>>(m+i)%16%3*3)) )
			{
				FoodTemp=(m+i)%16*16+(n+j)%6;
				break;	//找到了空位置就退出循环
			}
		}
	}
	return FoodTemp;
}

/**
  * @brief  控制蛇的移动
  * @param  无
  * @retval 无
  */
void MoveSnake(void)
{
	if(Direction==1)	//如果向右移动
	{
		if((SnakeBody[SnakeHead]/16)==15){GameOverFlag=1;}	//移动前判断一下移动后是否撞墙,如果是,则游戏结束,游戏结束的标志置1
		
		//(SnakeHead+1)%96,取余的目的是为了防止越界,SnakeBody数组的索引范围是:0~95
		else{SnakeBody[(SnakeHead+1)%96]=SnakeBody[SnakeHead]+16;}	//SnakeBody数组中蛇头的下一个数据等于上一个数据加16(即高四位加1),即蛇头移动到了右边这一列
	}
	if(Direction==2)	//如果向上移动
	{
		if((SnakeBody[SnakeHead]%16)==0){GameOverFlag=1;}
		else{SnakeBody[(SnakeHead+1)%96]=SnakeBody[SnakeHead]-1;}	//SnakeBody数组中蛇头的下一个数据等于上一个数据减1(即低四位减1),即蛇头移动到了上边这一行
	}
	if(Direction==3)	//如果向左移动
	{
		if((SnakeBody[SnakeHead]/16)==0){GameOverFlag=1;}
		else{SnakeBody[(SnakeHead+1)%96]=SnakeBody[SnakeHead]-16;}	//SnakeBody数组中蛇头的下一个数据等于上一个数据减16(即高四位减1),即蛇头移动到了左边这一列
	}
	if(Direction==4)	//如果向下移动
	{
		if((SnakeBody[SnakeHead]%16)==5){GameOverFlag=1;}
		else{SnakeBody[(SnakeHead+1)%96]=SnakeBody[SnakeHead]+1;}	//SnakeBody数组中蛇头的下一个数据等于上一个数据加1(即低四位加1),即蛇头移动到了下边这一行
	}
	
	if(GameOverFlag==0)	//如果没撞墙
	{
		if(SnakeBody[(SnakeHead+1)%96]==Food)	//判断蛇头移动后的位置是否是食物所在的位置
		{	//如果是
			SnakeLength++;	//蛇身长度加1
			UpdateDisplay(Food,1);	//防止食物闪烁刚好不显示的时候进入此函数,导致蛇身一个点不显示
			Food=CreateFood();	//重新创造一个食物
			UpdateDisplay(Food,1);	//更新显示
			FlashFlag=0;	//创造出新的食物时,食物暂不闪烁
			T0Count2=0;	//定时器T0Count2重新计数
		}
		else if( (DisplayBuffer[SnakeBody[(SnakeHead+1)%96]/16/3*16+(SnakeBody[(SnakeHead+1)%96]%16%6)*3] 
			& (0x80>>(SnakeBody[(SnakeHead+1)%96]/16%3)*3)) )
		{	//如果蛇头移动后的位置撞在蛇身上,则游戏结束
			GameOverFlag=1;	//游戏结束的标志置1
		}
		else	//如果蛇头移动后的位置不是食物,也不是撞墙,也不是撞到蛇身的话
		{
			//显示缓存数组DisplayBuffer中蛇头前进后的新位置对应的位写1
			UpdateDisplay(SnakeBody[(SnakeHead+1)%96],1);
			
			//显示缓存数组DisplayBuffer中蛇尾的位置清0(蛇身移动,如果没有吃到食物,相当于蛇尾对应的点跑到了
			//蛇头的前一个点,变成了蛇头,原来的蛇头变成蛇身)整条蛇中间的数据不用操作
			UpdateDisplay(SnakeBody[(SnakeHead+96-SnakeLength+1)%96],0);

			//数组SnakeBody中,蛇尾的数据清零,更新数据
			//SnakeHead+96:+96是因为SnakeHead为95后再加1,就会变成了0,防止SnakeHead+96-SnakeLength+1为负数
			//即我们是循环使用SnakeBody数组中的96个数据,蛇头对应数组SnakeBody的第96个数据(对应索引为95)后,
			//再移动一次,蛇头就来到了数组的第1个数据(对应索引为0)
			SnakeBody[(SnakeHead+96-SnakeLength+1)%96]=0;	//其实可以不清零
		}
	}
	
	SnakeHead++;	//SnakeBody数组中,蛇头对应的数据的序号加1
	SnakeHead%=96;	//蛇头变量SnakeHeadd的范围是0~95(蛇最长是96个“点”)
}

void main()
{
	unsigned char i;
	LCD_Init();	//LCD1602初始化
	Timer0_Init();	//定时器初始化
	Pointer=Table1;	//上电后显示游戏名称“《贪吃蛇》”,数组名就是数组的首地址
	RollDirection=1;	//上电后向上滚动游戏名称“《贪吃蛇》”

	while(1)
	{
		
		KeyNum=Key();	//获取键码值

		if(KeyNum)	//如果有按键按下
		{
			srand(TL0);	//以定时器0的低八位数据作为随机数的种子,用来产生真随机的数据
						
			if(Mode==5 && KeyNum==2)	//如果是显示作者姓名和编程日期的界面,且按下返回键(K2)
			{
				Mode=2;	//返回难度选择界面
				ExecuteOnceFlag=1;
				ChangedFlag=1;
			}

			if(Mode==4)	//如果游戏结束全屏闪烁模式
			{
				if(KeyNum==2)	//如果按下返回键(K2)
				{
					Mode=2;	//返回难度选择界面
					ExecuteOnceFlag=1;
					ChangedFlag=1;
				}
				if(KeyNum==3)	//如果按下K3
				{
					Mode=5;	//切换到显示作者姓名和编程日期的界面
					ExecuteOnceFlag=1;
					Offset=0;
				}
			}
			
			if(Mode==3)	//如果是游戏进行模式
			{
				if(KeyNum==1)	//按下K1暂停或继续
				{
					PauseFlag=!PauseFlag;
				}
				if(PauseFlag==0)	//如果不是暂停
				{	//长按和短按都进行检测,这样控制方向更有效,防止短按没检测出来导致没能改变方向
					if((KeyNum==8 || KeyNum==16) && LastDirection!=1)
					{	//如果短按或长按“左”键,且蛇头原来的移动方向不是向右
						Direction=3;	//则方向蛇头方向改为向左
					}
					if((KeyNum==7 || KeyNum==15) && LastDirection!=4)
					{	//如果短按或长按“上”键,且蛇头原来的移动方向不是向下
						Direction=2;	//则方向蛇头方向改为向上
					}
					if((KeyNum==6 || KeyNum==14) && LastDirection!=2)
					{	//如果短按或长按“下”键,且蛇头原来的移动方向不是向上
						Direction=4;	//则方向蛇头方向改为向左
					}
					if((KeyNum==5 || KeyNum==13) && LastDirection!=3)
					{	//如果短按或长按“右”键,且蛇头原来的移动方向不是向左
						Direction=1;	//则方向蛇头方向改为向左
					}
				}
			}
			
			if(Mode==2)	//如果是难度选择界面
			{
				if(KeyNum==7)	//如果按了“上”键
				{
					Difficulty++;
					if(Difficulty>5){Difficulty=1;}
					ChangedFlag=1;	//更改了难度,更新显示
				}
				if(KeyNum==6)	//如果按了“下”键
				{
					Difficulty--;
					if(Difficulty<1){Difficulty=5;}
					ChangedFlag=1;	//更改了难度,更新显示
				}
				if(KeyNum==1)	//如果按了开始键(K1),则开始游戏
				{
					Mode=3;	//切换到游戏模式
					ExecuteOnceFlag=1;
				}
			}

			if(KeyNum<=8)	//如果按下任意按键
			{
				/*两个if的顺序不能调换,如果调换了,就从模式0直接跳到模式2了*/
				
				if(Mode==1)	//跳过汉字“难度: 1”的滚动显示,切换到难度选择界面
				{
					Mode=2;
					ExecuteOnceFlag=1;
					ChangedFlag=1;
				}
				
				if(Mode==0)	//跳过游戏名“《贪吃蛇》”的显示,切换到汉字“难度: 1”的滚动显示界面
				{
					Mode=1;
					ExecuteOnceFlag=1;
				}		
			}
		}

		if(Mode==0)	//如果是显示游戏名称“《贪吃蛇》”的模式
		{
			if(ExecuteOnceFlag)	//进入到该模式后,此if中的内容只执行1次
			{
				if(RollFlag)	//如果滚动的标志RollFlag为1(定时器中每隔0.5s将此标志置1)
				{
					RollFlag=0;	//滚动的标志RollFlag清零
					Offset+=2;	//每次向上偏移2像素
					if(Offset==16){ExecuteOnceFlag=0;}	//向上偏移16像素后保持不变
				}
			}
		}
		if(Mode==1)	//如果是滚动显示汉字“难度: 1”的模式
		{
			if(ExecuteOnceFlag)
			{
				ExecuteOnceFlag=0;
				Offset=0;	//显示的偏移量清零
				RollDirection=0;	//向左滚动
				Pointer=Table2;	//显示Table2的内容,即向左滚动显示“难度: 1”
			}
			if(RollFlag)	//如果滚动的标志RollFlag为1(定时器中每隔0.5s将此标志置1)
			{
				RollFlag=0;	//滚动的标志RollFlag清零
				Offset+=1;	//每次向左偏移一个汉字(15像素)
				if(Offset==6)	//“难”字滚到最左端就自动切换为难度选择模式
				{
					Mode=2;
					ExecuteOnceFlag=1;
					ChangedFlag=1;
				}
			}
		}
		if(Mode==2)	//如果是难度选择模式
		{
			if(ExecuteOnceFlag)
			{
				ExecuteOnceFlag=0;
				LCD_WriteCommand(0x0C);	//防止模式4(游戏结束全屏闪烁模式)恰好不显示的时候返回模式2,导致屏幕不显示
				for(i=0;i<96;i++)	//显示LCD的前3个区域(前12列)
				{
					DisplayBuffer[i]=Table2[i+192];
				}
				Offset=0;
				Pointer=DisplayBuffer;
				LCD_Clear();	//清屏
				PartSelect=0;	//从第一个区域开始扫描,防止出错(防止第四个区域写入CGRAM的字模)
				PartSum=3;	//难度选择界面动态显示前3个区域
			}
			if(ChangedFlag)
			{
				ChangedFlag=0;
				for(i=0;i<32;i++){DisplayBuffer[i+96]=Table3[i+32*(Difficulty-1)];}
				switch(Difficulty)
				{
					case 1:SnakeMoveSpeed=100;break;	//1s移动一次
					case 2:SnakeMoveSpeed=75;break;		//0.75s
					case 3:SnakeMoveSpeed=50;break;		//0.5s
					case 4:SnakeMoveSpeed=25;break;		//0.25s
					case 5:SnakeMoveSpeed=12;break;		//0.12s
					default:break;
				}
			}
		}
		if(Mode==3)	//如果是游戏进行模式
		{
			if(ExecuteOnceFlag)	//切换到该模式后,此if中的内容只执行1次
			{
				ExecuteOnceFlag=0;
				for(i=0;i<96;i++)	//用到前两个区域,只需用缓存数组的一部分数据
				{
					DisplayBuffer[i]=0;
				}
				DisplayBuffer[3]=0x1E;	//蛇的初始位置是2列2行(蛇尾)和3列2行(蛇头)
				DisplayBuffer[4]=0x1E;	//蛇的初始位置是2列2行(蛇尾)和3列2行(蛇头)
				LCD_Clear();	//清屏
				Pointer=DisplayBuffer;	//LCD前两个区域显示缓存数组内容
				PartSelect=0;
				PartSum=2;	//动态扫描显示前两个区域(1~8列)
				GameTime=0;	//游戏时间清零
				GameOverFlag=0;	//游戏结束标志清零
				PauseFlag=0;	//游戏暂停标志清零
				Direction=1;	//蛇头默认向右移动
				LastDirection=1;	//上一次蛇头默认向右移动
				SnakeLength=2;	//蛇的初始长度为2
				SnakeHead=1;	//蛇头对应数组中的第2个数据(索引为1)
				for(i=0;i<96;i++)	//蛇身数据全部清零
				{
					SnakeBody[i]=0;	//其实可以不清零
				}
				SnakeBody[0]=1*16+1;	//蛇身数据全部清零后,写入蛇身初始的两个数据
				SnakeBody[1]=2*16+1;
				Food=CreateFood();	//进入游戏前,先创造出一个食物
				UpdateDisplay(Food,1);	//更新显示
				MoveSnakeFlag=0;	//蛇移动的标志清零
				T0Count1=0;	//定时器计数变量T0Count1清零,重新计数
				LCD_ShowString(1,9,"SCORE:");
				LCD_ShowString(2,9,"TIME:");
			}
			
			if(PauseFlag)	//如果暂停了
			{
				UpdateDisplay(Food,1);	//食物不闪烁,一直显示
			}			
			else if(FlashFlag)	//如果不暂停,且闪烁标志为1
			{
				UpdateDisplay(Food,0);	//不显示食物
			}
			else	//如果不暂停,且闪烁标志为0
			{
				UpdateDisplay(Food,1);	//显示食物
			}

			if(MoveSnakeFlag && GameOverFlag==0 && PauseFlag==0)
			{	//如果移动的标志为1,且不暂停,且游戏也没结束
				LastDirection=Direction;	//保存上一次移动的方向,用于按键的判断(蛇不能往后移动)
				MoveSnakeFlag=0;	//移动标志清零
				MoveSnake();	//移动一次
			}
			
			if(GameOverFlag==1)	//如果游戏结束
			{
				UpdateDisplay(Food,1);	//显示食物	
				Mode=4;	//切换到全屏闪烁模式
				ExecuteOnceFlag=1;
			}
		}
		
		if(Mode==4)	//游戏结束全屏闪烁模式
		{
			//闪烁操作放在定时中断函数中进行,放这里会有显示问题
		}

		if(Mode==5)
		{
			if(ExecuteOnceFlag)
			{
				ExecuteOnceFlag=0;
				LCD_Clear();
				LCD_WriteCommand(0x0C);	//防止模式4(全屏闪烁模式)刚好不显示的时候切换到模式5,导致屏幕不显示
				LCD_ShowString(1,1,"by gantengsheng");
				LCD_ShowString(2,1,"at 20241207");
			}
		}
	}
}

void Timer0_Routine() interrupt 1	//定时器0中断函数
{
	TL0=0xF0;	//设置定时初值,定时10ms,晶振@12.00000MHz
	TH0=0xD8;	//设置定时初值,定时10ms,晶振@12.00000MHz
	T0Count0++;
	if(PauseFlag==0)	//不暂停时,T0Count1和T0Count2才计数
	{
		T0Count1++;
		T0Count2++;
	}
	T0Count3++;
	if(T0Count0>=2)	//20ms扫描一次按键
	{
		T0Count0=0;
		Key_Tick();
		if(Mode!=5)
		{
			if(RollDirection){LCD_Convert_MoveUp(Pointer,PartSelect+1,Offset);}
			else{LCD_Convert_MoveLeft(Pointer,PartSelect+1,Offset);}
			LCD_ShowChinese(PartSelect+1,PartSum);
			PartSelect++;
			PartSelect%=PartSum;
		}
	}
	if(T0Count1>=SnakeMoveSpeed)	//用来控制蛇移动的速度
	{
		T0Count1=0;
		MoveSnakeFlag=1;
	}
	if(T0Count2>=50)	//0.5s取反闪烁标志FlashFlag的值
	{
		T0Count2=0;
		FlashFlag=!FlashFlag;
		if(PauseFlag==0 && GameOverFlag==0 && Mode==3){GameTime++;}
	}
	if(T0Count3>=50)	//控制滚动的速度,500ms滚动一次
	{
		T0Count3=0;
		RollFlag=1;
	}
	if(Mode==3)	//游戏时间和分数的更新必须放在中断中执行,否则左半屏动态显示的时候会出错
	{
		LCD_ShowNum(1,15,SnakeLength,2);
		LCD_ShowNum(2,14,GameTime/2,3);
	}
	if(Mode==4)	//闪烁的代码必须放在中断中执行,否则第二行第九列的显示会出问题
	{
		if(FlashFlag){LCD_WriteCommand(0x08);}
		else{LCD_WriteCommand(0x0C);}
	}
}

附录A:编程遇到的问题

在这里插入图片描述
问题1:由显示4个区域切换为显示前3个区域时(第4个不显示),虽然进行了清屏处理,但是第4个区域还是随机性地有显示。
原因:切换时选择区域显示的变量有四分之一的概率发生越界。
解决:定义一个全局变量PartSelect,切换显示区域时,PartSelect重新赋值,从左边第一个区域开始扫描。
在这里插入图片描述
问题2:玩一段时间后,屏幕显示不正常。
原因:保存蛇身数据的数组SnakeBody的下标忘记取余了,导致越界。所以实际上是移动了94次之后就会出现问题。
解决:总共96个数据,下标对96取余就行了。

在这里插入图片描述
问题3:实时显示得分和游戏已玩时间时,左半屏第二行显示不正常,会显示得分和时间的数字。
原因:如果在主循环中更新得分和时间时,刚好在设置完LCD的光标后被计时中断打断,返回主循环时,光标被重新设置在左半屏了。
解决:在中断中更新得分和游戏已玩时间。
在这里插入图片描述
问题4:游戏结束全屏闪烁时,第二行第九列的英文字母“T”被“点”取代了。
原因:跟问题3的一样。
解决:同样将实现闪烁的代码从主循环中移到定时中断函数中。

总结

相比8X8LED点阵屏和16X16LED点阵屏的贪吃蛇,LCD1602贪吃蛇出现的问题要多一些,还好都把这些问题给解决了。

标签:16,0x00,void,51,unsigned,char,单片机,LCD,小游戏
From: https://blog.csdn.net/gantengsheng/article/details/144324604

相关文章

  • 制作一个简单的单片机上的boot系统
    此篇文章在2023年3月24日被记录ARM单片机使用自定义bootloader什么是BOOT懂得计算机的同学都知道,电脑在开机时,从上电的那一刻开始,首先会进入bios,这个bios的作用就类似于单片机中的bootloader。万一我们浏览某些不可言状的网站导致系统崩溃时,我们就可以在这个临时的系统(bios)中......
  • 计算机毕业设计必看必学!90676,基于协同过滤推荐的流媒体电影推荐系统~原创定制程序单片
    基于协同过滤推荐的流媒体电影推荐系统摘 要本文介绍了一个基于Django的流媒体电影推荐系统的设计与实现。该系统旨在提供一个高效、个性化的电影推荐平台,满足用户对电影观看的需求。通过收集用户的观影历史、喜好和评价等数据,系统使用协同过滤算法分析用户的行为模式,并......
  • 计算机毕业设计必看必学!!42576,djangoNBA球员数据可视化系统LW原创定制程序单片机,java
    摘 要在当今的数字化时代,数据可视化已经成为分析和理解复杂数据的重要手段。对于NBA球员数据来说,可视化能够更直观地展示球员的表现、统计信息以及比赛趋势,为球队管理、球迷分析和媒体报道等提供有力支持。本系统旨在通过Django框架Python技术,构建一个NBA球员数据可......
  • 51单片机基础之数码管、模块化及模板
    数码管根据连接方式分为共阴极和共阳极数码管,数码管的统一逻辑就是先位选再段选1、静态数码管/*头文件区域*/#include<REGX52.H>#include<intrins.h>/*延时函数*/voidDelay(unsignedintxms) //@12.000MHz{ while(xms--) { unsignedchari,j; i=2;......
  • 51单片机基础之LED彩灯控制系统
    (一)显示界面数码管需要A和B,则需要在数码管段选数据储存数组添加0x77与0x7f(二)按键功能按键需要启动,暂停,则需要创建一个bit类型标志位,进行判断,实现四个模式的切换,需要定义模式的变量,用switch列举出来(三)彩灯模式34模式需要一个从外到内的LED数据数组,并建立一个用于遍历数组......
  • 2024-12-10单片机-8*8点阵闪烁
    我们设计横排点阵显示名字和竖排显示名字,通过四个按钮实现名字上下左右平移;例如我的名字,SYM开头。其实算法很简单,写一个简单的滑动窗口就好,实现数组的平移。实现代码:#include"reg51.h"typedefunsignedintu16;typedefunsignedcharu8;voiddelay_10us(u16ten_us){......
  • 基于STM32单片机的智能点滴输液报警器液位检测电机无线WiFi手机APP设计DR-01非接触液
    25-040-点滴检测+药水液位+电机控制+上下限+按键+声光提醒+TFT彩屏+WiFi产品功能描述:本系统由STM32F103C8T6单片机核心板、TFT液晶显示电路、无线无线WIFI/、点滴检测模块、步进电机控制电路、DR-01非接触液位传感器检测电路、蜂鸣器声光报警、按键电路、电源电路组成。【1......
  • 猜数字小游戏
    1.初始化随机数生成器:使用srand(time(NULL))来确保每次运行程序时生成的随机数不同。2.生成随机数:使用rand()%100+1生成一个1到100之间的随机数。3.提示信息:告诉用户游戏开始和范围。4.循环等待用户输入:使用while(1)创建一个无限循环,直到用户猜对数字。5.获取用......
  • STM32单片机芯片与内部13 TIM-通用定时器TIM2345 高级定时器TIM18-定时计数功能、库函
    目录一、通用定时器库函数工程模板1、TIM_TimeBaseInitTypeDef2、时钟3、初始化4、中断服务函数二、通用定时器库函数API1、初始化封装2、中断服务函数封装三、高级定时器库函数工程模板1、TIM_TimeBaseInitTypeDef2、时钟3、初始化4、中断服务函数四、高级定时......
  • STM32单片机芯片与内部12 TIM-基本定时器TIM67 -定时计数功能、库函数配置、HAL库配置
    目录一、功能二、库函数工程模板1、NVIC_InitTypeDef与TIM_TimeBaseInitTypeDef2、时钟使能3、初始化4、清除中断5、开启/关闭中断6、使能/失能计数器三、库函数API1、初始化的封装2、中断服务函数四、HAL库工程模板1、TIM_HandleTypeDef2、TIM_MasterConfigType......