一、实验的基本功能:
1.实现秒表功能系统,在51单片机的数码管中显示:分-秒-毫秒
2.实现功能:开始计时、重置计时时间、存入当前时间、读取已存入时间
二、硬件上的准备:
1.数码管显示,配置数数码管显示功能(略)
2.51单片机的存储功能和读取功能,AT24C02存取器件,与之通信的I2C通讯需配置(详)
3.数码管的循环显示,采用定时器定时调用,优化独立按键的Delay()函数(详)
三、配置I2C和AT24C02之间通讯
1.硬件的结构知识
·I2C用途:相比于UART用于板与板,板与电脑之间的通信,I2C多用于板内部通信
·AT24C02;是一种掉电数据不丢失储存器件,挂在I2C总线上

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

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

·通讯存在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(),实现秒表的进制