前言:
本篇模块化代码是根据基础改的,先教基础,后面解释为什么这样改
演示视频:
秒表(数据存储)
源代码
如上图将13个文放在Keli5 中即可,然后烧录在单片机中就行了
烧录软件用的是STC-ISP,不知道怎么安装的可以去看江科大的视频:
【51单片机入门教程-2020版 程序全程纯手打 从零开始入门】https://www.bilibili.com/video/BV1Mb411e7re?p=2&vd_source=ada7b122ae16cc583b4add52ad89fd5e
源代码:
头文件要记得宏定义和重定义,避免重复调用:
#ifndef _Timer0_h_//名字根据文件名定义即可
#define _Timer0_h_
//声明函数……
#endif
main.c
#include <STC89C5xRC.H>
#include "Timer0.h"
#include "Key.h"
#include "Nixie.h"
#include "Delay.h"
#include "AT24C02.h"
unsigned char M,S,MS;//分秒毫秒
unsigned char KeyNum;//独立按键获取
unsigned char RunFlag;//秒表的暂停和启动
//主函数
void main(){
Timer0_Init();//定时器/计数器、中断初始化
while(1){//无限循环
KeyNum=Key();//获取按键
if(KeyNum==2){//如果摁下第2个按键,秒表清零
M=0;
S=0;
MS=0;
}
if(KeyNum==1){//如果摁下第1个按键,秒表暂停
RunFlag=!RunFlag;
}
if(KeyNum==3){//如果摁下第3个按键,秒表数据存储
AT24C02_WriteByte(0,M);//分存在字地址0里
Delay(5);//写周期
AT24C02_WriteByte(1,S);//秒存在字地址1里
Delay(5);//写周期
AT24C02_WriteByte(2,MS);//毫秒存在字地址2里
Delay(5);//写周期
}
if(KeyNum==4){//如果摁下第4个按键,秒表清零
M=AT24C02_ReadByte(0);//读字地址0的分数据
S=AT24C02_ReadByte(1);//读字地址1的秒数据
MS=AT24C02_ReadByte(2);//读字地址2的毫秒数据
}
Nixie_SetBuf(1,M/10);//第1个数码管显示分的十位
Nixie_SetBuf(2,M%10);//第2个数码管显示分的个位
Nixie_SetBuf(3,11);//第3个数码管显示"-"
Nixie_SetBuf(4,S/10);//第4个数码管显示秒的十位
Nixie_SetBuf(5,S%10);//第5个数码管显示秒的个位
Nixie_SetBuf(6,11);//第6个数码管显示"-"
Nixie_SetBuf(7,MS/10);//第7个数码管显示毫秒的十位
Nixie_SetBuf(8,MS%10);//第8个数码管显示毫秒的个位
}
}
//秒表中断函数
void Sec_Loop(){
if(RunFlag){//如果RunFlag等于1,秒表就启动
MS++;//启动后,毫秒递增
if(MS>=100){//判断毫秒临界值
MS=0;
S++;//达到临界值秒递增
if(S>=60){//判断秒临界值
S=0;
M++;//达到临界值分递增
if(M>=60){//判断分临界值
M=0;
}
}
}
}
}
//中断程序函数
void Timer0_Routine() interrupt 1
{
static unsigned int T0Count1,T0Count2,T0Count3;
TL0 = 0x66; //设置定时初值
TH0 = 0xFC; //设置定时初值
T0Count1++;
if(T0Count1>=20){//独立按键中断,20ms执行一次
T0Count1=0;
Key_Loop();
}
T0Count2++;
if(T0Count2>=2){
T0Count2=0;//动态数码管中断,2ms执行一次
Nixie_Loop();
}
T0Count3++;
if(T0Count3>=10){
T0Count3=0;//秒表中断,10ms(0.1秒)执行一次
Sec_Loop();
}
}
Nixie.c
#include "Delay.h"
#include <STC89C5xRC.H>
//定义当前数码管状态(一个8个数码管,从1开始,0没用,10的意思是熄灭当前数码管)
unsigned char Nixie_Buf[9]={0,10,10,10,10,10,10,10,10};
//当前数码管显示0,1,2,3,4,5,6,7,8,9,熄灭,"-";
unsigned char NixieTable[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x00,0x40};
//控制数码管显示
void Nixie_SetBuf(unsigned char Location,Number){
Nixie_Buf[Location]=Number;//Location表示第几个数码管,Number表示显示啥
}
//数码管显示
void Nixie_Scan(unsigned char Location,Number){
P0=0x00;//消影
switch(Location){
//Location表示第几个数码管,Number表示显示啥
case 1:P24=1;P23=1;P22=1;break;
case 2:P24=1;P23=1;P22=0;break;
case 3:P24=1;P23=0;P22=1;break;
case 4:P24=1;P23=0;P22=0;break;
case 5:P24=0;P23=1;P22=1;break;
case 6:P24=0;P23=1;P22=0;break;
case 7:P24=0;P23=0;P22=1;break;
case 8:P24=0;P23=0;P22=0;break;
}
P0=NixieTable[Number];
}
//数码管中断函数,作用:让8个数码管2ms更新一次,快速扫描数码管
void Nixie_Loop(){
static unsigned char i=1;
Nixie_Scan(i,Nixie_Buf[i]);
i++;
if(i>=9){i=1;}
}
NIxie.h
//Nixie.h
#ifndef __Nixie_H__
#define __Nixie_H__
unsigned char NixieTable[];
void Nixie_Loop();
void Nixie_SetBuf(unsigned char Location,Number);
void Nixie_Scan(unsigned char Location,Number);
#endif
AT24C02.c
//AT24C02.c
#include <STC89C5xRC.H>
#include "I2C.h"
#define AT24C02_ADDRESS 0xA0
/**
* @brief AT24C02写入一个字节
* @param WordAddress 要写入字节的地址
* @param Data 要写入的数据
* @retval 无
*/
void AT24C02_WriteByte(unsigned char WordAddress,Data){
I2C_Start();
I2C_SendByte(AT24C02_ADDRESS);
I2C_ReceiveAck();
I2C_SendByte(WordAddress);
I2C_ReceiveAck();
I2C_SendByte(Data);
I2C_ReceiveAck();
I2C_Shop();
}
/**
* @brief AT24C02读取一个字节
* @param WordAddress 要读出字节的地址
* @retval 读出的数据
*/
unsigned char AT24C02_ReadByte(unsigned char WordAddress){
unsigned char Data;
I2C_Start();
I2C_SendByte(AT24C02_ADDRESS);
I2C_ReceiveAck();
I2C_SendByte(WordAddress);
I2C_ReceiveAck();
I2C_Start();
I2C_SendByte(AT24C02_ADDRESS|0x01);
I2C_ReceiveAck();
Data=I2C_ReceiveByte();
I2C_SendAck(1);
I2C_Shop();
return Data;
}
AT24C02.h
//AT24C02.h
#ifndef __AT24C02_H__
#define __AT24C02_H__
void AT24C02_WriteByte(unsigned char WordAddress,Data);
unsigned char AT24C02_ReadByte(unsigned char WordAddress);
#endif
Key.c
#include <STC89C5xRC.H>
#include "Delay.h"
unsigned char Key_KeyNumber;//获取独立按键变量
//返回按键值
unsigned char Key(){
unsigned char Temp=0;//临时变量
Temp=Key_KeyNumber;//独立按键变量赋值给临时变量
Key_KeyNumber=0;//清空当前按键值,防止显示错误,为下次获取按键值做准备
return Temp;//返回当前按键值
}
//获取按键值
unsigned char Key_GetState()//获取独立按键
{
unsigned char KeyNumber=0;
//进行判断是否摁下按键和
if(P31==0){KeyNumber=1;}//摁下是0,返回按键的数字,松开为1,返回的是0
if(P30==0){KeyNumber=2;}
if(P32==0){KeyNumber=3;}
if(P33==0){KeyNumber=4;}
return KeyNumber;
}
//按键中断函数,作用,按键消抖(增加按键灵敏度)
void Key_Loop(){
//定义当前状态和前状态
static unsigned char NowState,LastState;//摁下是0,返回按键的数字,松开为1,返回的是0
LastState=NowState;//前状态赋值为当前状态值
NowState=Key_GetState();//当前状态赋值为当前按键值
if(LastState==1 && NowState==0){
Key_KeyNumber=1;//按键1
}
if(LastState==2 && NowState==0){
Key_KeyNumber=2;//按键2
}
if(LastState==3 && NowState==0){
Key_KeyNumber=3;//按键3
}
if(LastState==4 && NowState==0){
Key_KeyNumber=4;//按键4
}
}
Key.h
#ifndef _Key_h_
#define _Key_h_
unsigned char Key();
unsigned char Key_GetState();
void Key_Loop();
#endif
I2C.c
//I2C.c
#include <STC89C5xRC.H>
sbit I2C_SCL=P2^1;
sbit I2C_SDA=P2^0;
//I2C开始
void I2C_Start(){
I2C_SDA=1;
I2C_SCL=1;
I2C_SDA=0;
I2C_SCL=0;
}
//I2C停止
void I2C_Shop(){
I2C_SDA=0;
I2C_SCL=1;
I2C_SDA=1;
}
//I2C发送一个字节
void I2C_SendByte(unsigned char Byte){
unsigned char i;
for(i=0;i<8;i++){
I2C_SDA=Byte&(0x80>>i);
I2C_SCL=1;
I2C_SCL=0;
}
}
//I2C接收一个字节
unsigned char I2C_ReceiveByte(){
unsigned char i,Byte=0x00;
I2C_SDA=1;
for(i=0;i<8;i++){
I2C_SCL=1;
if(I2C_SDA){Byte|=(0x80>>i);}
I2C_SCL=0;
}
return Byte;
}
//I2C发送应答
void I2C_SendAck(unsigned char AckBit){
I2C_SDA=AckBit;
I2C_SCL=1;
I2C_SCL=0;
}
//I2C接收应答
unsigned char I2C_ReceiveAck(){
unsigned char AckBit;
I2C_SDA=1;
I2C_SCL=1;
AckBit=I2C_SDA;
I2C_SCL=0;
return AckBit;
}
I2C.h
//I2C.h
#ifndef __I2C_H__
#define __I2C_H__
unsigned char I2C_ReceiveAck();
void I2C_SendAck(unsigned char AckBit);
unsigned char I2C_ReceiveByte();
void I2C_SendByte(unsigned char Byte);
void I2C_Shop();
void I2C_Start();
#endif
Delay.c
//Delay.c
#include <STC89C5xRC.H>
#include <INTRINS.H>
//延时函数
void Delay(unsigned int xms) //@11.0592MHz
{
unsigned char i, j;
while(xms){
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
xms--;
}
}
Delay.h
//Delay.h
#ifndef __Delay_H__
#define __Delay_H__
//延时函数头文件
void Delay(unsigned int xms);
#endif
Timer0.c
#include <STC89C5xRC.H>
//void Timer0_Init()
//{
// TF0=0;TR0=1;//TCON,寄存器
// //TMOD=0x01;//0000 0001,寄存器
// TMOD=TMOD&0xF0;//把TMOD的低四位清零,高四位不变,方便使用两个定时器
// TMOD=TMOD&0x01;//把TMOD的最低位置1,高四位不变,方便使用两个定时器
// TH0=64535/256;//高电位,寄存器,1毫秒
// TL0=64535%256;//低电位,寄存器,1毫秒
// ET0=1;EA=1;PT0=0;//打开中断开关
//
//}
//定时器0初始化函数
void Timer0_Init() //1毫秒@11.0592MHz
{
// AUXR &= 0x7F; //定时器时钟12T模式
TMOD &= 0xF0; //设置定时器模式
TMOD |= 0x01; //设置定时器模式
TL0 = 0x66; //设置定时初值
TH0 = 0xFC; //设置定时初值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
ET0=1;//允许中断
EA=1;//允许总中断
PT0=0;//低优先级
}
中断程序函数
//中断函数模版
//void Timer0_Routine() interrupt 1
//{
// static unsigned int T0Count;
// TL0 = 0x66; //设置定时初值
// TH0 = 0xFC; //设置定时初值
// T0Count++;
// if(T0Count>=1000){
// T0Count=0;
// P20=~P20;
// }
//}
Timer0.h
#ifndef _Timer0_h_
#define _Timer0_h_
void Timer0_Init();
#endif
代码解析与教程:
Dealy模块
- 包含源代码与头文件,不需要知道怎么实现的会用即可,后续使用,直接将头文件和源代码拿过来用即可;
xms是定义的毫秒,1000毫秒就是1秒;模版生成的是1毫秒的,因此xms等于1000
Nixie模块(74HC138)
- 先看原理图:
- 先看第二个图,数码管一共有8个,要想第几个亮,直接选择第几个,比如第一个亮,LED8;如何打开LED8呢,看第一个图,对应Y7,想控制Y7,就是让P22(A),P23(B),P24(C)二进制CBA等于7(01(C)1(B)1(A))也就是P24(C)=1,P23(B)=1,P22(A)=1,Y6就是(01(C)1(B)0(A))P24(C)=1,P23(B)=1,P22(A)=0;以此类推;
- 控制完第几个亮,在控制每一个小的管,dp始终是0,并让他们倒过来进行二进制控制,比如想使第一个灯管亮并显示1,就是选择LED8,然后让小管bc亮,然后让二进制(0gfe dcba)来控制谁亮,比如bc亮二进制就是000 0110=0x06;让显示0,除了g都亮,二进制就是0011 1111=0x3F;以此类推,这些小管子由P0控制二进制;
#include "Delay.h" #include <STC89C5xRC.H> unsigned char NixieTable[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F}; void Nixie(unsigned char Location,Number){ switch(Location){ case 1:P24=1;P23=1;P22=1;break; case 2:P24=1;P23=1;P22=0;break; case 3:P24=1;P23=0;P22=1;break; case 4:P24=1;P23=0;P22=0;break; case 5:P24=0;P23=1;P22=1;break; case 6:P24=0;P23=1;P22=0;break; case 7:P24=0;P23=0;P22=1;break; case 8:P24=0;P23=0;P22=0;break; } P0=NixieTable[Number]; }
Nixie(1,0);//初始化数码管
来看代码,Nixie(1,0)此时Location=1,Number=0,前者为1,P24,P23,P22都是1,加起来等于7,也就是Y7,就是LED8,就是第一个灯亮;后者为0,就是P0等于NixieTable[0]=0x3F,显示0;以此类推;
Key模块
- 包含源代码与头文件,不需要知道怎么实现的会用即可,后续使用,直接将头文件和源代码拿过来用即可;
序号1是按键的防抖操作,不需要理解,有按键的地方直接用即可
序号2是独立按键控制变量。
KeyNumber就是返回值,按键K1就返回1,其他同理
I2C总线模块
- 包含源代码与头文件,需要知道怎么实现的,会用,后续使用,直接将头文件和源代码拿过来用即可;因为这部分比较难,轻度认识理解就行,越深越难
- 在此之前,我们要认识一下存储器:
- 本篇博客,以及博主的板子型号(STC89c52rc),用的储存器就是E2PROM储存器,简单介绍一下,RAM就是可读写储存器,SRAM是静态的,容量较大,成本高,类似于我们手机内存,DRAM是动态的,容量较小,成本较低,类似于我们手机的运行内存;ROM按常理来说是不可写的,只能读,但是他比RAM不易丢失,有断电保护,断电后数据不易丢失,因此非常受大家的喜欢,所以在后续的更新中,好多ROM也可以读写了,例如E2PROM。
- 了解上面之后,I2C总线可以理解成一个传输协议,或者传输方式,规则;作用是将一个机器(主机)的数据传输到另一个机器(从机);来看E2PROM原理图
- 可以看到SCL,SDA是I2C接口,A0,A1,A2是I2C地址,通过这些东西,将主机(单片机)数据传递到从机(E2PROM);下面来看传输是怎么实现的
- I2C总线传输时有两大部分,时序结构和数据帧,时序结构就是SCL,SDA这种东西的先后顺序或者说他俩的一些重要关系
- 时序结构有6个小模块,看图:
这是开始和结束两部分,中间框起来的部分是数据确认阶段,高电平是1(高一点的部分),低电平是0(低一点的部分);图中可以看到在数据确认阶段SCL始终为1,SDA也会变化,因此有:
//定义SCL,SDA的引脚 sbit I2C_SCL=P2^1; sbit I2C_SDA=P2^0; //I2C开始 void I2C_Start(){ I2C_SDA=1; I2C_SCL=1; I2C_SDA=0; I2C_SCL=0; } //I2C停止 void I2C_Shop(){ //因为开始后SCL=0了,所有这里不需要重复定义了 I2C_SDA=0; I2C_SCL=1; I2C_SDA=1; }
接下来是发送数据,发送一个字节(8个bit),在数据确认期间,SCL先0后1再0,SDA是数据,有可能是1,有可能是0,也会变化,即可发送一个bit,然后循环8次就是一个字节:
//I2C发送一个字节 void I2C_SendByte(unsigned char Byte){ unsigned char i; for(i=0;i<8;i++){ I2C_SDA=Byte&(0x80>>i); I2C_SCL=1; I2C_SCL=0; } }
发送字节是参数Byte,i是循环次数,也是右移几个单位,重点来了:注意看图中的先后顺序,SDA先变化,数据是高位在前,0x80是1000 0000,Byte&(有0即0)0x80就是保留Byte的第一位(最高位),其他全变0;然后变化SCL0,1,0;循环八次
接下来是接收数据,接收一个字节(8个bit),在数据确认期间,SCL先0后1再0,SDA是数据,有可能是1,有可能是0,也会变化,即可发送一个bit,然后循环8次就是一个字节:
//I2C接收一个字节 unsigned char I2C_ReceiveByte(){ unsigned char i,Byte=0x00; I2C_SDA=1; for(i=0;i<8;i++){ if(I2C_SDA){Byte|=(0x80>>i);} I2C_SCL=1; I2C_SCL=0; } return Byte; }
接收字节是无参数,返回值是Data,i是循环次数,也是右移几个单位,重点来了:注意,主机接收之前需要释放SDA(使SDA=1);注意看图中的先后顺序,SDA先变化,数据是高位在前,0x80是1000 0000,Byte&(有0即0)0x80就是保留Byte的第一位(最高位),其他全变0;然后变化SCL0,1,0;循环八次
![]()
接下来是发送应答和就收应答,作用就是在发送和接收时进行一个反馈,告诉你发送或者接收成功。
发送应答参数AckBit是应答数据,0就是表示应答,1是不应答; 注意看图中的先后顺序,SDA先变化,将应答数据赋值给SDA;然后变化SCL0,1,0;//I2C发送应答 void I2C_SendAck(unsigned char AckBit){ I2C_SDA=AckBit; I2C_SCL=1; I2C_SCL=0; } //I2C接收应答 unsigned char I2C_ReceiveAck(){ unsigned char AckBit; I2C_SDA=1; AckBit=I2C_SDA; I2C_SCL=1; I2C_SCL=0; return AckBit; }
接收应答无参数,返回值是AckBit是应答数据,0就是表示应答,1是不应答; 注意,主机接收之前需要释放SDA(使SDA=1);注意看图中的先后顺序,SDA先变化将SDA赋值给AckBit;然后变化SCL0,1,0;
- 了解时序结构之后,来看数据帧,也就是数据是怎么在I2C中传输的:
发送一帧数据是有I2C时序结构的6个小模块组成的,图中都标注了;需要注意的是要告诉I2C总线向谁发送,也就是要先发送从机地址,从机地址最后一个是告诉你是读还是写;
接收一帧数据是有I2C时序结构的6个小模块组成的,图中都标注了;需要注意的是要告诉I2C总线向谁接收,也就是要先发送从机地址,从机地址最后一个是告诉你是读还是写;注意最后一个发送应答是1,表示非应答
复合格式数据是有I2C时序结构的6个小模块组成的,图中都标注了;需要注意的是要告诉I2C总线向谁发送并接收,也就是要先发送从机地址,从机地址最后一个是告诉你是读还是写;注意最后一个发送应答是1,表示非应答
AT24C02模块 /E2PROM模块
- 包含源代码与头文件,需要知道怎么实现的,会用,后续使用,直接将头文件和源代码拿过来用即可;因为这部分比较难,轻度认识理解就行,越深越难
- AT24C02是E2PROM模块中的一小块,可以理解上述中所提到的从机,主机就是单片机,因此从机地址就是AT24C02地址;
字节写,随机读数据帧是有I2C时序结构的6个小模块组成的,图中不再标注了;需要注意的是要告诉I2C总线读还是写,也就是要发送从机地址SLAVE ADDRESS,从机地址最后一个是告诉你是读还是写;还有一个就是发送存储字节地址WORD ADDRESS;注意最后一个发送应答是1,表示非应答
//AT24C02.c #include <STC89C5xRC.H> #include "I2C.h" #define AT24C02_ADDRESS 0xA0 /** * @brief AT24C02写入一个字节 * @param WordAddress 要写入字节的地址 * @param Data 要写入的数据 * @retval 无 */ void AT24C02_WriteByte(unsigned char WordAddress,Data){ I2C_Start(); I2C_SendByte(AT24C02_ADDRESS); I2C_ReceiveAck(); I2C_SendByte(WordAddress); I2C_ReceiveAck(); I2C_SendByte(Data); I2C_ReceiveAck(); I2C_Shop(); } /** * @brief AT24C02读取一个字节 * @param WordAddress 要读出字节的地址 * @retval 读出的数据 */ unsigned char AT24C02_ReadByte(unsigned char WordAddress){ unsigned char Data; I2C_Start(); I2C_SendByte(AT24C02_ADDRESS); I2C_ReceiveAck(); I2C_SendByte(WordAddress); I2C_ReceiveAck(); I2C_Start(); I2C_SendByte(AT24C02_ADDRESS|0x01); I2C_ReceiveAck(); Data=I2C_ReceiveByte(); I2C_SendAck(1); I2C_Shop(); return Data; }
按照图中的I2C总线时序结构,依次调用函数,即可实现
注意写周期是5ms,意味着每次写完后要延时5ms;
Timer0模块
- 包含源代码与头文件,需要知道怎么实现,会用
- 51单片机的定时器和计数器十分重要,要理解怎么用,要知道原理是什么,要结合原理图来分析怎么做,先看代码
#include <STC89C5xRC.H> //void Timer0_Init() //{ // TF0=0;TR0=1;//TCON,寄存器 // //TMOD=0x01;//0000 0001,寄存器 // TMOD=TMOD&0xF0;//把TMOD的低四位清零,高四位不变,方便使用两个定时器 // TMOD=TMOD&0x01;//把TMOD的最低位置1,高四位不变,方便使用两个定时器 // TH0=64535/256;//高电位,寄存器,1毫秒 // TL0=64535%256;//低电位,寄存器,1毫秒 // ET0=1;EA=1;PT0=0;//打开中断开关 // //} //定时器0初始化函数 void Timer0_Init() //1毫秒@11.0592MHz { // AUXR &= 0x7F; //定时器时钟12T模式 TMOD &= 0xF0; //设置定时器模式 TMOD |= 0x01; //设置定时器模式 TL0 = 0x66; //设置定时初值 TH0 = 0xFC; //设置定时初值 TF0 = 0; //清除TF0标志 TR0 = 1; //定时器0开始计时 ET0=1;//允许中断 EA=1;//允许总中断 PT0=0;//低优先级 } 中断程序函数 //中断函数模版 //void Timer0_Routine() interrupt 1 //{ // static unsigned int T0Count; // TL0 = 0x66; //设置定时初值 // TH0 = 0xFC; //设置定时初值 // T0Count++; // if(T0Count>=1000){ // T0Count=0; // //下面是代码区 // } //}
- 最上面注释掉的代码是要求理解的;中间的代码是STC-ISP软件生成的;最下面的代码是中断函数模版,拿到main.c中可直接使用,但是也要了解原理:
定时器/计数器、中断教程(重点!!!!)
- 首先,要理解原理,会认原理图:
(本篇均是我自己理解的,只是帮助大家理解,若像深学深究,请去自行找资源,如有不对,希望大家指出)
先看官方解释:
- 红色部分是定时器,作用是自己设定一个最大值,让计数器达到时,完成什么什么,比如执行中断
- 黄色部分是计数器,作用是自己设定,让其变化,比如+1,毫秒,微妙,完成时继续怎么怎
- 蓝色部分是中断器,中断当前执行,执行自己设定的东西,中断这部分可以嵌套,像if函数一样,低优先级让高优先级
他们三者的关系非常微妙,仅仅相连,相辅相成:
- 定时器就是设定一个时间,计数器开始计时,到点了中断器开始执行;举个例子:你订一个10.00的闹钟(定时器)提醒你起床,时间一点一点的过去(计数器),10.00的时候闹钟提醒你(定时器),随后你开始起床(中断器);那么他们是怎么实现的呢
定时器/计数器(模式一)
- 先看原理图:
- 相关寄存器:
- 官方解释
- 模式一官方解释
- 下面开始来解释我的理解(再看原理图):
- 序号1是非门:反向输出,例如:GATE是1,输出0,是0,输入1。
- 序号2是或门:符号为梯形(或弧形缺口),逻辑上满足 “有 1 出 1,全 0 出 0”。
- 序号3是与门:符号为矩形缺口,逻辑上满足 “全 1 出 1,有 0 出 0”。
1,2,3序号均是TMOD里的,请看原理图:
- 代码TMOD=0x01,是16进制,转化为二进制为0000 0001,前(高)四位对应着定时器1;后(低)四位对应着定时器0;本代码使用的是定时器0, 0001分别对应GATE,C/T,M1,M0,由上面的官方解释可得,M1=0,M0=1时,就是使用并启动本寄存器。
但是这样定义有弊端,也就是定时器1和定时器0不能一起使用,因次,使用下面的代码:TMOD &=0xF0,也就是TMOD = TMOD & 0xF0(1111 0000)。看不懂没关系,举个例子:1010 0101 & 1111 0000 = 1010 0000,也就是有0出0;
1010 0101 | 1111 0000 = 1111 0101,也就是有1出1;因此使用代码:就可以定义定时器0;
TMOD &= 0xF0; //设置定时器模式
TMOD |= 0x01; //设置定时器模式
- 因此,当GATE=0时,通过序号1,输出1;然后通过序号2,输出1;因此,只需要将TR0设定成1,输出就是1,就可以启动寄存器了。
- 序号4是高电位和低电位寄存器:用来设置定时初值,如图:
- 先解释上面的代码,可帮助理解,下面的代码可以不理解(后续可生成):TH0和TL0,最大位就是65535,为了分开储存,用TH0和TL0分别储存,例如:现在有一个数123,但是一个盒子只能装进2位数,因此分成123/100=1和123%100=23储存,同理身为16进制,就要用256来做除数;代码中设定为64535的目的是因为1000毫秒就是1秒,65535-64535=1000,用来表示1秒,计时器每次加1秒。
- 序号7.8(图中忘记标了(TR0))是TCON(定时器控制)寄存器:TF0=0时,可以理解成初始化;TR0=1时,表示允许计时;反之两个就是反义理解
- 序号5.6是TMOD(定时器模式)寄存器:用来控制定时器和计数器的模式,本篇只讲用的多的模式一
中断器
- 先看原理图:
这里我们定义好定时器0后,TF0=1,ET0=1,打开中断,随后PT0=0,接入低级优先:理解上面的东西后,再看中断函数:
运用静态局部变量T0Count,来表示定时区间,达到1000毫秒(1秒)后重新执行该函数
P20行代码是代码区,也就是放你想每1秒就重复执行的代码。
main模块
- 注释写的很清楚,这里不做解释了
#include <STC89C5xRC.H> #include "Timer0.h" #include "Key.h" #include "Nixie.h" #include "Delay.h" #include "AT24C02.h" unsigned char M,S,MS;//分秒毫秒 unsigned char KeyNum;//独立按键获取 unsigned char RunFlag;//秒表的暂停和启动 //主函数 void main(){ Timer0_Init();//定时器/计数器、中断初始化 while(1){//无限循环 KeyNum=Key();//获取按键 if(KeyNum==2){//如果摁下第2个按键,秒表清零 M=0; S=0; MS=0; } if(KeyNum==1){//如果摁下第1个按键,秒表暂停 RunFlag=!RunFlag; } if(KeyNum==3){//如果摁下第3个按键,秒表数据存储 AT24C02_WriteByte(0,M);//分存在字地址0里 Delay(5);//写周期 AT24C02_WriteByte(1,S);//秒存在字地址1里 Delay(5);//写周期 AT24C02_WriteByte(2,MS);//毫秒存在字地址2里 Delay(5);//写周期 } if(KeyNum==4){//如果摁下第4个按键,秒表清零 M=AT24C02_ReadByte(0);//读字地址0的分数据 S=AT24C02_ReadByte(1);//读字地址1的秒数据 MS=AT24C02_ReadByte(2);//读字地址2的毫秒数据 } Nixie_SetBuf(1,M/10);//第1个数码管显示分的十位 Nixie_SetBuf(2,M%10);//第2个数码管显示分的个位 Nixie_SetBuf(3,11);//第3个数码管显示"-" Nixie_SetBuf(4,S/10);//第4个数码管显示秒的十位 Nixie_SetBuf(5,S%10);//第5个数码管显示秒的个位 Nixie_SetBuf(6,11);//第6个数码管显示"-" Nixie_SetBuf(7,MS/10);//第7个数码管显示毫秒的十位 Nixie_SetBuf(8,MS%10);//第8个数码管显示毫秒的个位 } } //秒表中断函数 void Sec_Loop(){ if(RunFlag){//如果RunFlag等于1,秒表就启动 MS++;//启动后,毫秒递增 if(MS>=100){//判断毫秒临界值 MS=0; S++;//达到临界值秒递增 if(S>=60){//判断秒临界值 S=0; M++;//达到临界值分递增 if(M>=60){//判断分临界值 M=0; } } } } } //中断程序函数 void Timer0_Routine() interrupt 1 { static unsigned int T0Count1,T0Count2,T0Count3; TL0 = 0x66; //设置定时初值 TH0 = 0xFC; //设置定时初值 T0Count1++; if(T0Count1>=20){//独立按键中断,20ms执行一次 T0Count1=0; Key_Loop(); } T0Count2++; if(T0Count2>=2){ T0Count2=0;//动态数码管中断,2ms执行一次 Nixie_Loop(); } T0Count3++; if(T0Count3>=10){ T0Count3=0;//秒表中断,10ms(0.1秒)执行一次 Sec_Loop(); } }
模块化修改解释:
本次项目一共修改了两个模块,分别是Key,Nixie;
为什么要修改?因为完成此项目,需要定时器/计数器,中断的接入,但是中断不能重复使用,因此只能通过重复使用函数,或者模块来完成。
Key修改
#include <STC89C5xRC.H> #include "Delay.h" unsigned char Key_KeyNumber;//获取独立按键变量 //返回按键值 unsigned char Key(){ unsigned char Temp=0;//临时变量 Temp=Key_KeyNumber;//独立按键变量赋值给临时变量 Key_KeyNumber=0;//清空当前按键值,防止显示错误,为下次获取按键值做准备 return Temp;//返回当前按键值 } //获取按键值 unsigned char Key_GetState()//获取独立按键 { unsigned char KeyNumber=0; //进行判断是否摁下按键和 if(P31==0){KeyNumber=1;}//摁下是0,返回按键的数字,松开为1,返回的是0 if(P30==0){KeyNumber=2;} if(P32==0){KeyNumber=3;} if(P33==0){KeyNumber=4;} return KeyNumber; } //按键中断函数,作用,按键消抖(增加按键灵敏度) void Key_Loop(){ //定义当前状态和前状态 static unsigned char NowState,LastState;//摁下是0,返回按键的数字,松开为1,返回的是0 LastState=NowState;//前状态赋值为当前状态值 NowState=Key_GetState();//当前状态赋值为当前按键值 if(LastState==1 && NowState==0){ Key_KeyNumber=1;//按键1 } if(LastState==2 && NowState==0){ Key_KeyNumber=2;//按键2 } if(LastState==3 && NowState==0){ Key_KeyNumber=3;//按键3 } if(LastState==4 && NowState==0){ Key_KeyNumber=4;//按键4 } }
对比原代码
#include <STC89C5xRC.H> #include "Delay.h" unsigned char Key()//获取独立按键 { unsigned char KeyNumber=0; //进行判断是否摁下按键和防抖操作 if(P31==0){Delay(20);while(P31==0);Delay(20);KeyNumber=1;} if(P30==0){Delay(20);while(P30==0);Delay(20);KeyNumber=2;} if(P32==0){Delay(20);while(P32==0);Delay(20);KeyNumber=3;} if(P33==0){Delay(20);while(P33==0);Delay(20);KeyNumber=4;} return KeyNumber; }
可以看到,修改后的代码还是要进行获取和防抖操作,代码解析很详细,认真看就行
Nixie修改
#include "Delay.h" #include <STC89C5xRC.H> //定义当前数码管状态(一个8个数码管,从1开始,0没用,10的意思是熄灭当前数码管) unsigned char Nixie_Buf[9]={0,10,10,10,10,10,10,10,10}; //当前数码管显示0,1,2,3,4,5,6,7,8,9,熄灭,"-"; unsigned char NixieTable[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x00,0x40}; //控制数码管显示 void Nixie_SetBuf(unsigned char Location,Number){ Nixie_Buf[Location]=Number;//Location表示第几个数码管,Number表示显示啥 } //数码管显示 void Nixie_Scan(unsigned char Location,Number){ P0=0x00;//消影 switch(Location){ //Location表示第几个数码管,Number表示显示啥 case 1:P24=1;P23=1;P22=1;break; case 2:P24=1;P23=1;P22=0;break; case 3:P24=1;P23=0;P22=1;break; case 4:P24=1;P23=0;P22=0;break; case 5:P24=0;P23=1;P22=1;break; case 6:P24=0;P23=1;P22=0;break; case 7:P24=0;P23=0;P22=1;break; case 8:P24=0;P23=0;P22=0;break; } P0=NixieTable[Number]; } //数码管中断函数,作用:让8个数码管2ms更新一次,快速扫描数码管 void Nixie_Loop(){ static unsigned char i=1; Nixie_Scan(i,Nixie_Buf[i]); i++; if(i>=9){i=1;} }
对比原代码
#include "Delay.h" #include <STC89C5xRC.H> unsigned char NixieTable[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F}; void Nixie(unsigned char Location,Number){ switch(Location){ case 1:P24=1;P23=1;P22=1;break; case 2:P24=1;P23=1;P22=0;break; case 3:P24=1;P23=0;P22=1;break; case 4:P24=1;P23=0;P22=0;break; case 5:P24=0;P23=1;P22=1;break; case 6:P24=0;P23=1;P22=0;break; case 7:P24=0;P23=0;P22=1;break; case 8:P24=0;P23=0;P22=0;break; } P0=NixieTable[Number]; Delay(1); P0=0x00;//消影 }
同样进行消影操作,快速扫描来实现数码管全显示
注:该代码是本人自己所写,可能不够好,不够简便,欢迎大家指出我的不足之处。如果遇见看不懂的地方,可以在评论区打出来,进行讨论,或者联系我。上述内容全是我自己理解的,如果你有别的想法,或者认为我的理解不对,欢迎指出!!!如果可以,可以点一个免费的赞支持一下吗?谢谢各位彦祖亦菲!!!!!