基于51单片机和TM1638模块的小游戏《打地鼠》

系列文章目录


前言

有两个版本,普中开发板版本和最小系统板版本,两个版本差别在于晶振频率不一样,其他的都相同。

本文代码对应的是普中开发板版本。

【单片机】STC89C52RC
【频率】12T@11.0592MHz

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

一、效果展示

在这里插入图片描述

二、原理分析

1、TM1638模块

该模块可以控制8个LED、8个数码管的显示,并且可以检测8个按键,并且与单片机用三根线进行通信,只占用单片机的3个IO口,极大节省了单片机IO口的使用。

该模块内部有数码管自动扫描电路,只需要把显示内容对应的数据写入TM1638芯片的寄存器内,芯片就会自动扫描显示,LED的显示也是写数据到寄存器就行了。TM1638也会自动扫描检测按键,并且把检测结果保存到寄存器内,单片机从寄存器中读出数据后,再将数据进行处理就可以知道哪个按键按下了。

2、短按、长按、松手

跟普通的独立按键、矩阵按键的检测差不多,利用定时器,隔20ms检测一次按键,并跟上一次的检测结果对比,可以判断出是短按、长按还是松手。这样可以实现更加多的功能。例如,本案例中老夫就是通过长按S8开始游戏或者重新开始游戏。

3、地鼠的显示与更换

用LED表示地鼠,按下对应的按键表示打地鼠,打完后再在剩余7个LED中随机选一个作为地鼠。

这是真随机,不是假随机,按键按下的时刻影响随机结果。

4、倒计时和分数的显示

最左边的两个数码管显示倒计时的时间,30s(代码中可修改)。

最右边的三个数码管显示得分,每打一次袋鼠,分数增加一。

5、开始游戏和重新开始游戏

通过长按S8开始游戏或者重新开始游戏。

6、注意事项

定时器中断函数中需要检测按键,即跟TM1638通信,读取TM1638寄存器中的数据,如果定时器中断时主函数中正在和TM1638通信,写数据到TM1638的寄存器的话,则会被打断,时序受到影响,显示不正常。所以主函数中在和TM1638通信时需要暂停检测按键的定时器0,为了使倒计时不受影响,需要用到定时器1,并且要设置定时器1的优先级比定时器0的高。

三、各模块代码

1、TM1638模块

h文件

#ifndef	__TM1638_H__
#define	__TM1638_H__
 
void TM1638_WriteByte(unsigned char	Byte);
unsigned char TM1638_ReadByte(void);
void TM1638_WriteCommand(unsigned char Command);
void TM1638_SendData(unsigned char Address,unsigned char Data);
void TM1638_OneLED(unsigned char Location,unsigned char State);
void TM1638_EightLED(unsigned char Data);
void TM1638_Nixie(unsigned char Location,unsigned char Number);
unsigned char TM1638_Key(void);
void TM1638_Init(void);
unsigned char Key(void);
void Key_Tick(void);

#endif

c文件

#include <REGX52.H>

/*TM1638模块引脚定义*/
sbit TM1638_DIO=P2^0;
sbit TM1638_CLK=P2^1;
sbit TM1638_STB=P2^2;

unsigned char KeyNumber;

/*共阴数码管段码表*/
unsigned char code Nixie_SegCode[]={
0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,	//0~7
0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71,	//8~F
0xBF,0x86,0xDB,0xCF,0xE6,0xED,0xFD,0x87,	//0~7(带小数点)
0xFF,0xEF,0xF7,0xFC,0xB9,0xDE,0xF9,0xF1,	//8~F(带小数点)
0x40,0x00,	//“-”,无显示
};

/**
  * 函    数:TM1638写入一个字节
  * 参    数:Byte 要写入的字节
  * 返 回 值:无
  */
void TM1638_WriteByte(unsigned char	Byte)
{
	unsigned char i;
	for(i=0;i<8;i++)
	{
		TM1638_CLK=0;
		TM1638_DIO=Byte&(0x01<<i);
		TM1638_CLK=1;
	}
}

/**
  * 函    数:TM1638读取一个字节
  * 参    数:Byte 要读取的字节
  * 返 回 值:无
  */
unsigned char TM1638_ReadByte(void)
{
	unsigned char i;
	unsigned char Byte=0;
	TM1638_DIO=1;	//设置为输入
	for(i=0;i<8;i++)
	{
		TM1638_CLK=0;
		if(TM1638_DIO){Byte|=(0x01<<i);}
		TM1638_CLK=1;
	}
	return Byte;
}

/**
  * 函    数:TM1638写命令
  * 参    数:Command 要写入的命令
  * 返 回 值:无
  */
void TM1638_WriteCommand(unsigned char Command)
{
	TM1638_STB=0;	//STB为低后的第一个字节为命令
	TM1638_WriteByte(Command);
	TM1638_STB=1;
}

/**
  * 函    数:TM1638指定地址写入数据
  * 参    数:Address 写入数据的地址
  * 参    数:Data 要写入的数据
  * 返 回 值:无
  */
void TM1638_SendData(unsigned char Address,unsigned char Data)
{
	TM1638_WriteCommand(0x44);
	TM1638_STB=0;
	TM1638_WriteByte(0xC0|Address);
	TM1638_WriteByte(Data);
	TM1638_STB=1;
}

/**
  * 函    数:TM1638控制一个LED的显示
  * 参    数:Location 要控制LED状态的位置,范围:1~8,1在左,8在右
  * 参    数:State 状态,范围:0~1,0为灭,1为亮
  * 返 回 值:无
  */
void TM1638_OneLED(unsigned char Location,unsigned char State)
{
	if(State){TM1638_SendData(2*(Location-1)+1,1);}
	else{TM1638_SendData(2*(Location-1)+1,0);}
}

/**
  * 函    数:TM1638控制八个LED的显示
  * 参    数:Data LED显示对应的数据,高位在左,1为亮,0为灭
  * 返 回 值:无
  * 说    明:例如,Data=0xA5(1010 0101)时,八个LED的状态为:亮灭亮灭 灭亮灭亮
  */
void TM1638_EightLED(unsigned char Data)
{
	unsigned char i;
	for(i=0;i<8;i++)
	{
		if(Data&(0x80>>i)){TM1638_SendData(2*i+1,1);}
		else{TM1638_SendData(2*i+1,0);}
	}
}

/**
  * 函    数:TM1638驱动数码管显示数字
  * 参    数:Location 要显示的数码管的位置,范围:1~8
  * 参    数:Number 数码管要显示的数字
  * 返 回 值:无
  */
void TM1638_Nixie(unsigned char Location,unsigned char Number)
{
	TM1638_WriteCommand(0x44);
	TM1638_STB=0;
	TM1638_WriteByte(0xC0|(2*(Location-1)));
	TM1638_WriteByte(Nixie_SegCode[Number]);
	TM1638_STB=1;
}

/**
  * 函    数:TM1638读取键码
  * 参    数:无
  * 返 回 值:键码值,范围:0~8,无按键按下则返回0
  */
unsigned char TM1638_Key(void)
{
	unsigned char i;
	unsigned char KeyTemp[4];	//存储读取键扫数据的四个字节
	unsigned char Temp=0;	//要返回的键码值,要设置初值为零,否则松开后,读取键码值不为零
	unsigned char KeyValue=0;	//保存四个字节合成为一个字节后的数据,要设置初值为零,否则读取的键码值出错
	TM1638_STB=0;
	TM1638_WriteByte(0x42);	//发送读键扫数据的命令
	for(i=0;i<4;i++){KeyTemp[i]=TM1638_ReadByte();}
	TM1638_STB=1;
	for(i=0;i<4;i++){KeyValue|=(KeyTemp[i]<<i);}
	for(i=0;i<8;i++)
	{
		if(KeyValue==(0x01<<i))	//判断合成的字节第几位为1(第几位为1就是第几个按键按下)
		{
			Temp=i+1;
			break;
		}
	}
	return Temp;
}

/**
  * 函    数:TM1638初始化
  * 参    数:无
  * 返 回 值:无
  */
void TM1638_Init(void)
{
	unsigned char i;
	TM1638_WriteCommand(0x8B);	//亮度设置,8级亮度可调,范围:0x88~0x8F
	TM1638_WriteCommand(0x40);	//采用地址自动加1
	TM1638_STB=0;
	TM1638_WriteByte(0xC0);    //设置起始地址
	for(i=0;i<16;i++){TM1638_WriteByte(0x00);}	//传送16个字节的数据
	TM1638_STB=1;
}

/**
  * 函    数:获取独立按键键码
  * 参    数:无
  * 返 回 值:按下按键的键码,范围:0~25,0表示无按键按下
  * 说    明:主程序中获取键码值之后键码值清零,在下一次定时器扫描按键之前再次获取键码值,一定会返回0
  * 说    明:第八个按键(S8)按下超过0.7s时返回25
  */
unsigned char Key(void)
{
	unsigned char Temp=0;
	Temp=KeyNumber;
	KeyNumber=0;
	return Temp;
}

/**
  * 函    数:在中断中调用
  * 参    数:无
  * 返 回 值:无
  */
void Key_Tick(void)
{
	static unsigned char NowState,LastState;
	static unsigned int KeyCount;
	
	LastState=NowState;	//保存上一次的按键状态
	
	NowState=0;	//如果没有按键按下,则NowState为0

	//获取当前按键状态
	NowState=TM1638_Key();
	
	//如果上个时间点按键未按下,这个时间点按键按下,则是按下瞬间
	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)
	{
		KeyCount++;

		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;}
		
		//长按按键超过0.7s(定时器中断函数中每隔20ms检测一次按键)
		if(KeyCount>=35)
		{
			if(LastState==8 && NowState==8){KeyNumber=25;}
		}
	}
	else
	{
		KeyCount=0;
	}

	//如果上个时间点按键按下,这个时间点按键未按下,则是松手瞬间
	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;
		}
	}
	
}

2、定时器0

h文件

#ifndef __TIMER0_H__
#define __TIMER0_H__

void Timer0_Init(void);

#endif

c文件

#include <REGX52.H>

/**
  * 函    数:定时器0初始化
  * 参    数:无
  * 返 回 值:无
  */
void Timer0_Init(void)
{	
//	AUXR&=0x7F;	//定时器时钟12T模式(STC89C52RC是12T单片机,无需设置)
	TMOD&=0xF0;	//设置定时器模式(高四位不变,低四位清零)
	TMOD|=0x01;	//设置定时器模式(通过低四位设为16位不自动重装)
	TL0=0x00;	//设置定时初值,定时10ms,12T@11.0592MHz
	TH0=0xDC;	//设置定时初值,定时10ms,12T@11.0592MHz
	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=0x00;	//设置定时初值,定时10ms,12T@11.0592MHz
	TH0=0xDC;	//设置定时初值,定时10ms,12T@11.0592MHz
	T0Count++;
	if(T0Count>=1000)
	{
		T0Count=0;
		
	}
}
*/

3、定时器1

h文件

#ifndef __TIMER1_H__
#define __TIMER1_H__
 
void Timer1_Init(void);

#endif

c文件

#include <REGX52.H>

/**
  * 函    数:定时器1初始化
  * 参    数:无
  * 返 回 值:无
  */
void Timer1_Init(void)
{
//	AUXR&=0xBF;	//定时器时钟12T模式(STC89C52RC是12T单片机,无需设置)
	TMOD&=0x0F;	//设置定时器模式(低四位不变,高四位清零)
	TMOD|=0x10;	//设置定时器模式(通过高四位设为16位不自动重装的模式)
	TL1=0x00;	//设置定时初始值,50ms,12T@11.0592MHz
	TH1=0x4C;	//设置定时初始值,50ms,12T@11.0592MHz
	TF1=0;	//清除TF1标志
	TR1=1;	//定时器1开始计时
	ET1=1;	//打开定时器1中断允许
	EA=1;	//打开总中断
	PT1=1;	//当PT1=0时,定时器1为低优先级,当PT1=1时,定时器1为高优先级
}

/*定时器中断函数模板
void Timer1_Routine() interrupt 3	//定时器1中断函数
{
	static unsigned int T1Count;	//定义静态变量
	TL1=0x00;	//设置定时初始值,50ms,12T@11.0592MHz
	TH1=0x4C;	//设置定时初始值,50ms,12T@11.0592MHz
	T1Count++;
	if(T1Count>=1000)
	{
		T1Count=0;
		
	}
}
*/

四、主函数

main.c

/*by甘腾胜@20250329
【效果查看/操作演示】B站搜索“甘腾胜”或“gantengsheng”查看
【单片机】STC89C52RC
【频率】12T@11.0592MHz
【外设】TM1638模块
【接线】TM1638模块:DIO接P20,CLK接P21,STB接P22
【简单的原理分析】https://blog.youkuaiyun.com/gantengsheng/article/details/143581157
【注意】
TM1638的工作电流较大,如果电脑的USB供电不足导致模块不能正常显示的话,
需要给TM1638模块独立供电,独立电源需要跟单片机的电源共地(负极接在一起)
【操作说明】
(1)长按S8开始游戏或重新开始游戏
(2)LED代表地鼠,按对应的按键则LED熄灭,再从另外七个LED中随机选一个亮
*/

#include <REGX52.H>
#include <STDLIB.H>	//包含随机函数的声明
#include "TM1638.h"
#include "Timer0.h"
#include "Timer1.h"

unsigned char KeyNum;	//存储获得的键码值
unsigned char Mole;	//用LED灯表示地鼠,8个LED只亮一个
unsigned char Mode;	//游戏模式,0:未开始或游戏结束,1:正在游戏
char GameTime;	//游戏时间
unsigned char Score;	//游戏得分
unsigned char T1Count;	//定时器1计数变量
unsigned char OnceFlag;	//特定前提下只执行一次的标志,1:执行,0:不执行
unsigned char HitFlag;	//“地鼠”被打到的标志,1:被打到,0:没被打到

/**
  * 函    数:主函数(有且仅有一个)
  * 参    数:无
  * 返 回 值:无
  * 说    明:主函数是程序执行的起点,负责执行整个程序的主要逻辑‌
  */
void main()
{
	P2_5=0;	//防止开发板上的蜂鸣器发出声音
	TM1638_Init();	//TM1638初始化
	Timer0_Init();  //定时器0初始化
	Timer1_Init();  //定时器1初始化

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

		if(KeyNum)	//如果有按键按下
		{
			srand(TL1);	//以定时器1的低八位数据作为随机数的种子,用来产生真随机的数据

			if(KeyNum==25)	//如果长按S8超过0.7s
			{
				Mode=0;	//游戏开始或重新开始
				OnceFlag=1;	//切换为其他模式前只执行一次的标志置1
			}
			if(KeyNum==24)	//如果松开S8
			{
				Mode=1;	//开始游戏
			}

			if(KeyNum==Mole || KeyNum==Mole+8)	//如果按下(短按或长按)LED(地鼠)对应的按键
			{
				HitFlag=1;
			}
		}
		
		if(Mode==0)	//游戏未开始或游戏结束
		{
			if(OnceFlag)
			{
				OnceFlag=0;	//切换为其他模式前只执行一次的标志清零
				
				/*游戏初始化*/
				Mole=rand()%8+1;	//游戏开始前给Mole取一个随机数
				GameTime=30;	//初始化游戏时间
				Score=0;	//游戏得分清零
				T1Count=0;	//定时器1的计数变量清零
			}

			//暂时关闭定时器0中断允许
			//防止显示LED和数码管时进中断破坏时序(定时器0中断函数中按键检测也用到TM1638的时序)
			ET0=0;

			TM1638_EightLED(0x00);	//LED灯无显示
			TM1638_Nixie(1,GameTime/10);	//游戏时间十位
			TM1638_Nixie(2,GameTime%10);	//游戏时间个位
			TM1638_Nixie(6,Score/100%10);	//分数百位
			TM1638_Nixie(7,Score/10%10);	//分数十位
			TM1638_Nixie(8,Score%10);		//分数个位

			ET0=1;	//打开定时器0中断允许
		}
		else if(Mode==1)	//游戏开始
		{
			if(HitFlag)
			{
				HitFlag=0;

				Score++;
				while(KeyNum==Mole || KeyNum==Mole+8)	//随机产生一个跟上次不一样的“地鼠”
				{
					Mole=rand()%8+1;
				}
			}
			if(GameTime)	//防止游戏结束后松开S8按键LED闪烁
			{
				ET0=0;

				TM1638_EightLED(0x80>>(Mole-1));	//显示LED(地鼠)
				TM1638_Nixie(1,GameTime/10);	//游戏时间十位
				TM1638_Nixie(2,GameTime%10);	//游戏时间个位
				TM1638_Nixie(6,Score/100%10);	//分数百位
				TM1638_Nixie(7,Score/10%10);	//分数十位
				TM1638_Nixie(8,Score%10);		//分数个位

				ET0=1;
			}
		}

		if(GameTime==0)	//如果倒计时结束
		{
			Mode=0;	//游戏结束
		}

	}
}

/**
  * 函    数:定时器0中断函数
  * 参    数:无
  * 返 回 值:无
  */
void Timer0_Routine() interrupt 1
{
	static unsigned int T0Count;	//定义静态变量
	TL0=0x00;	//设置定时初值,定时10ms,12T@11.0592MHz
	TH0=0xDC;	//设置定时初值,定时10ms,12T@11.0592MHz
	T0Count++;
	if(T0Count>=2)
	{
		T0Count=0;
		Key_Tick();
	}
}

/**
  * 函    数:定时器1中断函数
  * 参    数:无
  * 返 回 值:无
  */
void Timer1_Routine() interrupt 3
{
	TL1=0x00;	//设置定时初始值,50ms,12T@11.0592MHz
	TH1=0x4C;	//设置定时初始值,50ms,12T@11.0592MHz
	T1Count++;
	if(T1Count>=20)
	{
		T1Count=0;
		GameTime--;
		if(GameTime<0)GameTime=0;
	}
}

总结

本来是有点怀疑能不能只用TM1638这个模块做出来,后来才发现是定时器中断程序中,按键的检测打乱了主函数中LED和数码管显示的时序。

出现问题要多思考,找出原因,并想办法解决。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值