系列文章目录
前言
有两个版本,普中开发板版本和最小系统板版本,两个版本差别在于晶振频率不一样,其他的都相同。
本文代码对应的是普中开发板版本。
【单片机】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和数码管显示的时序。
出现问题要多思考,找出原因,并想办法解决。

被折叠的 条评论
为什么被折叠?



