51单片机实验二:实现秒表功能(基础)

448 阅读8分钟

一、实验的基本功能:

        1.实现秒表功能系统,在51单片机的数码管中显示:分-秒-毫秒


        2.实现功能:开始计时、重置计时时间、存入当前时间、读取已存入时间

二、硬件上的准备:

        1.数码管显示,配置数数码管显示功能(略)
        
        2.51单片机的存储功能和读取功能,AT24C02存取器件,与之通信的I2C通讯需配置(详)
        
        3.数码管的循环显示,采用定时器定时调用,优化独立按键的Delay()函数(详)
        

三、配置I2C和AT24C02之间通讯

1.硬件的结构知识

        ·I2C用途:相比于UART用于板与板,板与电脑之间的通信,I2C多用于板内部通信
        ·AT24C02;是一种掉电数据不丢失储存器件,挂在I2C总线上
                    

微信图片_20230322160426.png

        ·开发板CPU有I2C总线连接各种储存器件,且可挂多个
        ·SCL:时钟线负责收送双方的时钟的节拍
        ·SDA:数据线负责传输数据

微信图片_20230322160417.png

        ·I2C总线连接AT24C02的结构图,采用开漏输出和上拉电阻共同实现了“线与”的功能           

2. I2C通讯的时序定义

微信图片_20230322160429.jpg

        ·通讯存在3个部分:起始信号,数据传输信号,停止信号(后续代码演示解释)
        ·数据帧:数据输出信号要修饰,比如:哪个存储器件,哪个数据位等等,整体叫一个数据帧

四、I2C通信功能实现的功能函数

void I2C_Start(void); 
void I2C_Stop(void);
void I2C_SendByte(unsigned char Byte);
unsigned char I2C_ReceiveByte(void);
void I2C_SendAck(unsigned char AckBit);
unsigned char I2C_ReceiveAck(void);
void AT24C02_WriteByte(unsigned char WordAddress,Date);
unsigned char AT24C02_ReadByte(unsigned char WordAddress);

五、代码演示和解释说明

1.端口的调用和地址的重置命名

#define AT24C02_ADDRESS 0XA0   //储存器件的地址
sbit I2C_SCL = P2^1;  
sbit I2C_SDA = P2^0;
· 将51端口和写入命令的指令进行重置命名,实现更加清晰化及调用更加方便,提升代码的阅读性

2.I2C起始信号(数据传输开始)

void I2C_Start(void)
{	
	I2C_SCL = 1;		//  先让时钟信号置1,SCL某些情况不是1
	I2C_SDA = 1;		//  同上
	I2C_SDA = 0;		//  当SCL,SDAc从高电平变低电平
	I2C_SCL = 0;		//  代表数据传输开始
}
·起始信号定义:SCL处于高电平期间,SDA有一个下降沿
·为保证起始信号的正确性,先要让SDA与SCL置1,否则刚开始SDA与SC未必是1

3.I2C结束信号(数据传输结束)

void I2C_Stop(void)
{
	I2C_SDA = 0;		//  此时SDA在表达ACK时,可能为0/1,所以要先置0;
	I2C_SCL = 1;		//  最后一位的应答信时,最后SCL要置0的	
	I2C_SDA = 1;		//  只需要把SCL,SDA从低到高即可
}
·结束信号定义:SCL处于高电平期间,SDA有一个上沿

4.I2C数据发送

void I2C_SendByte(unsigned char Byte)
{
	unsigned char i;
	//一个数据帧
	for(i=0;i<8;i++)
	{
		I2C_SDA = Byte&(0x80>>i);  //获得传来的Byte的数据
		I2C_SCL = 1;	
		I2C_SCL = 0;
	}
}
·数据传输信号定义:当SCL处于低电平期间,SDA才可以改变SDA的数据。
                  当SCL处于高电平期间,SDA不可以改变,此时接收端接收当前信号
·处理发送发送端,知道如何把数据发给硬件即可,硬件怎么接收无需关心              

5.I2C数据接收

unsigned char I2C_ReceiveByte(void)
{
	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;
}
·硬件将信号发送到SDA端,程序通过SDA将其提取出来,存储道单片机核心芯片上
·同上,无需关心硬件是怎么把数据发送到SDA端上的

6.I2C数据发送应答0/1

void I2C_SendAck(unsigned char AckBit)
{
	I2C_SDA = AckBit;
	I2C_SCL = 1;
	I2C_SCL = 0;
}
·数据传输的最后后一位,应答信号,根据读取的0/1判断是否应答

7.I2C数据接收应答0/1

unsigned char I2C_ReceiveAck(void)
{
	unsigned char AckBit;
	I2C_SDA = 1;
	I2C_SCL = 1;
	AckBit =I2C_SDA;
	I2C_SCL=0;
	return AckBit;
}

8.I2C发送数据帧

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_Stop();//数据传输停止
}
·发送一个数据帧的步骤(根据通信时序):
    1.数据传输开始信号调用I2C_Start()
    2.数据传输部分,存储到哪个存储器,该数据表示存储器的地址,调用I2C_SendByte()
    3.数据传输部分,存储到哪个存储器的位置,该数据表示存储器的地址,调用I2C_SendByte()
    4.数据传输部分,发送所需数据
    5.接收应答信号,调用I2C_ReceiveAck(),数据传输部分都要调用
    6.数据传输结束信号调用2C_Stop()

9.I2C读取数据帧

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();
	Date = I2C_ReceiveByte(); //获取到数据
	I2C_SendAck(1);
	I2C_Stop();
	return Data;
}
·读取一个数据帧的步骤(根据通信时序):
    1.由于I2C通信时序统一,发送和接收用的一个通信时序,所以接收或是发送区别于两个I2C_Start();
    2.读取与发送都要选定哪个存储器件和哪个数据位,所以同上
    3.区别于发送接收:要再I2C_Start()表示读取含义

六、实现数码管的循环显示代码演示和解释说明

1.数码管的循环显示(采用定时器)

void Nixie_Loop(void)
{
	static unsigned char i = 1;
	Nixie(i,Nixie_Buf[i]);
	i++;
	if(i>=9){i=1;}
}

void Nixie_SetBuf(unsigned char Location,Number)
{
	Nixie_Buf[Location] = Number;
}
·Nixie_Loop()函数它的作用只能单循环,但只要不断调用就可实现一起显示,而调用定时器可实现
·Nixie_SetBuf()的意义是:数码管.c文件中有数码管显示缓存区Nixie_Buf[9],实现秒表中的数值
 跳动Sec_Loop()函数,也就说数码管显示区值要不断的改,Nixie_SetBuf()更改数组的函数,在main函数中调用 

2.实现秒表中的数值跳动

//根据分秒毫秒进行进制
//  1000毫秒 = 1秒
void Sec_Loop(void)
{
    if(RunFlag)
    {
        MiniSec++;
        if(MiniSec>=100)    //1000ms 进制 1s
        {
            MiniSec=0;
            Sec++;
            if(Sec>=60)     //60s    进制 1min
            {
                Sec=0;
                Min++;
                if(Min>=60)
                {
                    Min=0;
                }
            }
        }
    }
}

·Sec_Loop()函数是在断函数进行,即固定10ms调用Sec_Loop(),实现秒表的进制

3.优化独立按键(去除Delay()函数)

void Key_Loop(void)
{
	static unsigned char NowState, LastState;
	LastState  = NowState;
	NowState = SmallKey();
	if(LastState == 1  && NowState == 0)
	{
		Key_KeyNumber = 1;		
	}
	if(LastState == 2  && NowState == 0)
	{
		Key_KeyNumber = 2;		
	}
	if(LastState == 3  && NowState == 0)
	{
		Key_KeyNumber = 3;		
	}
	if(LastState == 4  && NowState == 0)
	{
		Key_KeyNumber = 4;		
	}

}

 · Keey_Loop()作用:固定获取2种状态:Key_keyNumber的位数1/0 -----位数为(低电平/按下) ,0为(空闲和松开按键)
 · 定时器的用途,每隔20ms调用Kee_Loop(),去除20ms的抖动
 · 去抖动原理:抖动只有20ms,当获取到抖动的20ms中的1/0时,为1时没关系。为0时,表示按下了,但是后也至少有20ms的低电平
 · 也就是说,即使触发抖动的值,但也有至少20ms的保持状态
 · 按键为向上沿有效,即松开就有效
 · SmallKey()为辅助,作用就是获取端口的按键按下的低电平
 · Key_Loop()实时监控向下沿的状态
 · key()判断按下以及松手,并且传值(哪个按键按下)        

七、mian函数运行

1.主函main演示

#include <regx52.h>
#include "Timer0.h"
#include "Better_Small_key.h"
#include "Better_Nixie.h"
#include "I2C.H"

unsigned char KeyNum;               //选秒表的功能如:1.开始 2.暂停 3.重置
unsigned char Min,Sec,MiniSec;      //分别储存:分,秒,毫秒
unsigned char RunFlag;              //控制开始

void main()
{   Timer0_Init();
    while(1)
    {
        KeyNum=Key();
        if(KeyNum==1)			//K1按键按下
        {
                RunFlag=!RunFlag;	//启动标志位翻转
        }
        if(KeyNum==2)			//K2按键按下
        {
                Min=0;				//分秒清0
                Sec=0;
                MiniSec=0;
        }
        if(KeyNum==3)			//K3按键按下
        {
                AT24C02_WriteByte(0,Min);	//将分秒写入AT24C02
                Delay(5);
                AT24C02_WriteByte(1,Sec);
                Delay(5);
                AT24C02_WriteByte(2,MiniSec);
                Delay(5);
        }
        if(KeyNum==4)			//K4按键按下
        {
                Min=AT24C02_ReadByte(0);	//读出AT24C02数据
                Sec=AT24C02_ReadByte(1);
                MiniSec=AT24C02_ReadByte(2);
        }
        //设置数码管的数值:分,秒,毫秒
        Nixie_SetBuf(1,Min/10);
        Nixie_SetBuf(2,Min%10);
        Nixie_SetBuf(3,11);
        Nixie_SetBuf(4,Sec/10);
        Nixie_SetBuf(5,Sec%10);
        Nixie_SetBuf(6,11);
        Nixie_SetBuf(7,MiniSec/10);
        Nixie_SetBuf(8,MiniSec%10);
    }

}
 ·KeyNum == 1实现的是秒表的暂停,暂停的本质就是秒表的进制停止工作,改变全局变量的RunFlay
  (RunFlay控制的是Sec_Loop()函数的毫秒的控制端),停止毫秒的递加,但中断函数还是不断调用
 ·KeyNum == 2实现的秒表清零,将全局变量的Min,Sec,MiniSec置空即可
 ·KeyNum == 3实现的Min,Sec,MiniSec进入AT24C02,调用AT24C02_WriteByte()
 ·KeyNum == 4实现的读取AT24C02Min,Sec,MiniSec,调用AT24C02_ReadByte()
 ·最后设置数码管显示区

2.定时器中断程序演示

void Timer0_Routine() interrupt 1
{
    static unsigned int T0Count1,T0Count2,T0Count3;
    TL0  = 0x18;
    TH0  = 0xFC;
    T0Count1++;
    if (T0Count1>=20)       //定时20ms执行Key_Loop(),消除对Delay()影响
    {
       T0Count1 = 0;
       Key_Loop();
    } 
    T0Count2++;             
    if (T0Count2>=2)        //定时2ms执行Nixie_Loop(),消除对位选,断选的影响,更好执行8个数码管工作
    {                       //需要注意的,不得大于2ms,否则就有虚影
       T0Count2 = 0;
       Nixie_Loop();
    } 
    T0Count3++;
	if(T0Count3>=10)        //驱动秒表函数:固定10ms调用Sec_Loop(),实现秒表的进制
	{
		T0Count3=0;
		Sec_Loop();	//10ms调用一次数秒表驱动函数
	}
       
}

·定时2ms执行Nixie_Loop(),显示数码管实现区的数组。2ms消除对位选,断选的影响
·定时20ms执行Key_Loop(),消除对Delay()影响
·定时10ms调用Sec_Loop(),实现秒表的进制