(51单片机)秒表数据存储(定时器/计数器、中断)(动态数码管教程)(独立按键教程)(延时函数教程)(I2C总线认识)(AT24C02认识)(74HC138译码器)

前言:

本篇模块化代码是根据基础改的,先教基础,后面解释为什么这样改

 演示视频:

秒表(数据存储)

 源代码

如上图将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;循环八次


接下来是发送应答和就收应答,作用就是在发送和接收时进行一个反馈,告诉你发送或者接收成功。
 
//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;然后变化SCL0,1,0;
接收应答无参数,返回值是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. 首先,要理解原理,会认原理图:

(本篇均是我自己理解的,只是帮助大家理解,若像深学深究,请去自行找资源,如有不对,希望大家指出)

先看官方解释:

  • 红色部分是定时器,作用是自己设定一个最大值,让计数器达到时,完成什么什么,比如执行中断
  • 黄色部分是计数器,作用是自己设定,让其变化,比如+1,毫秒,微妙,完成时继续怎么怎
  • 蓝色部分是中断器,中断当前执行,执行自己设定的东西,中断这部分可以嵌套,像if函数一样,低优先级让高优先级

他们三者的关系非常微妙,仅仅相连,相辅相成:

  • 定时器就是设定一个时间,计数器开始计时,到点了中断器开始执行;举个例子:你订一个10.00的闹钟(定时器)提醒你起床,时间一点一点的过去(计数器),10.00的时候闹钟提醒你(定时器),随后你开始起床(中断器);那么他们是怎么实现的呢
 定时器/计数器(模式一)
  • 先看原理图:

​​

  • 相关寄存器:

​​

  • 官方解释

​​

​​

  • 模式一官方解释

​​

  • 下面开始来解释我的理解(再看原理图):
    ​​
  1. 序号1是非门:反向输出,例如:GATE是1,输出0,是0,输入1。
  2. 序号2是或门:符号为梯形(或弧形缺口),逻辑上满足 “有 1 出 1,全 0 出 0”。
  3. 序号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;因此使用代码:
       
           
      1. TMOD &= 0xF0; //设置定时器模式

      2. TMOD |= 0x01; //设置定时器模式

      就可以定义定时器0;
    • 因此,当GATE=0时,通过序号1,输出1;然后通过序号2,输出1;因此,只需要将TR0设定成1,输出就是1,就可以启动寄存器了。
  4. 序号4是高电位和低电位寄存器:用来设置定时初值,如图:​​
    • 先解释上面的代码,可帮助理解,下面的代码可以不理解(后续可生成):TH0和TL0,最大位就是65535,为了分开储存,用TH0和TL0分别储存,例如:现在有一个数123,但是一个盒子只能装进2位数,因此分成123/100=1和123%100=23储存,同理身为16进制,就要用256来做除数;代码中设定为64535的目的是因为1000毫秒就是1秒,65535-64535=1000,用来表示1秒,计时器每次加1秒。
  5. 序号7.8(图中忘记标了(TR0))是TCON(定时器控制)寄存器:TF0=0时,可以理解成初始化;TR0=1时,表示允许计时;反之两个就是反义理解
  6. 序号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;//消影
}

同样进行消影操作,快速扫描来实现数码管全显示

 注:该代码是本人自己所写,可能不够好,不够简便,欢迎大家指出我的不足之处。如果遇见看不懂的地方,可以在评论区打出来,进行讨论,或者联系我。上述内容全是我自己理解的,如果你有别的想法,或者认为我的理解不对,欢迎指出!!!如果可以,可以点一个免费的赞支持一下吗?谢谢各位彦祖亦菲!!!!!

以下是基于51单片机定时器产生0.01S的用数码管显示的电子秒表的代码,供参考: ``` #include <reg51.h> #define uint unsigned int #define uchar unsigned char uchar code table[] = {0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f}; // 数码管显示表 uchar cnt = 0; // 计数器,0.01秒一次 uchar second = 0; // 秒数 uchar minute = 0; // 分钟数 uchar hour = 0; // 小时数 uchar flag = 0; // 计时标志位,0表示停止计时,1表示正在计时 void delay(uint t) // 延时函数 { uint i, j; for (i = t; i > 0; i--) for (j = 110; j > 0; j--); } void display() // 数码管显示函数 { uchar i; uchar code num[] = {hour / 10, hour % 10, minute / 10, minute % 10, second / 10, second % 10}; // 将时间转换为显示格式 for (i = 0; i < 6; i++) // 数码管扫描循环 { P2 = 0x7f; // 关闭所有数码管 P0 = table[num[i]]; // 设置当前数码管的显示值 P2 = ~(1 << i); // 打开当前数码管 delay(1); // 延时一段时间,以保证显示稳定 } } void timer0() interrupt 1 // 定时器0中断服务程序,每10ms执行一次 { TH0 = 0xff; // 重新设置定时器初值 TL0 = 0x9c; cnt++; // 计数器加1 if (cnt >= 100) // 1秒钟到达 { cnt = 0; // 计数器清零 if (flag) // 如果正在计时 { second++; // 秒数加1 if (second >= 60) // 分钟数到达 { second = 0; // 秒数清零 minute++; // 分钟数加1 if (minute >= 60) // 小时数到达 { minute = 0; // 分钟数清零 hour++; // 小时数加1 if (hour >= 24) // 超过一天,从头开始计时 { hour = 0; // 小时数清零 } } } } } } void main() { TMOD = 0x01; // 定时器0工作在模式1(16位定时器)下 TH0 = 0xff; // 定时器初值为65536-1000=64536(10ms) TL0 = 0x9c; ET0 = 1; // 开启定时器0中断 EA = 1; // 开启总中断 TR0 = 1; // 启动定时器0 P2 = 0x7f; // 初始化数码管 P0 = 0x00; while (1) { display(); // 数码管显示 if (P1 == 0xfe) // 如果P1.0被按下 { flag = 1; // 开始计时 } if (P1 == 0xfd) // 如果P1.1被按下 { flag = 0; // 停止计时 } if (P1 == 0xfb) // 如果P1.2被按下 { flag = 0; // 停止计时 hour = 0; // 小时数清零 minute = 0; // 分钟数清零 second = 0; // 秒数清零 } } } ``` 在上述代码中,使用了定时器0来产生10ms的中断,通过计数器实现0.01秒的计时。在中断服务程序中,更新计时器计数值,并将计数值转换为数码管显示格式。在主函数中,初始化数码管,启动定时器,并进入循环等待状态。在循环中不断检测是否有按键按下,若有按键按下,则相应地执行相应的操作,如开始计时、暂停计时、复位计时等。在数码管显示时,通过数码管的扫描方式和显示格式,保证显示正确。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值