51单片机学习笔记13-AT24C02(I2C)

目录

[13.1 AT24C02 基础知识](#13.1 AT24C02 基础知识)

[13.2 AT24C02 引脚定义](#13.2 AT24C02 引脚定义)

[13.3 I2C总线](#13.3 I2C总线)

[13.3.1 I2C简介](#13.3.1 I2C简介)

[13.3.2 I2C时序结构](#13.3.2 I2C时序结构)

⭐起始条件

⭐终止条件

⭐发送一个字节

⭐接收一个字节

⭐发送应答

⭐接收应答

✅I2C.c

✅I2C.h

[13.3.3 I2C数据帧](#13.3.3 I2C数据帧)

[⭐ I2C发送一帧数据](#⭐ I2C发送一帧数据)

[⭐ I2C接收一帧数据](#⭐ I2C接收一帧数据)

[⭐ I2C先发送再接收一帧数据](#⭐ I2C先发送再接收一帧数据)

[13.4 AT24C02数据帧](#13.4 AT24C02数据帧)

✅AT24C02.c

✅AT24C02.h

[13.5 项目示例1:AT24C02数据存储](#13.5 项目示例1:AT24C02数据存储)

✅项目功能:

✅项目架构:

✅main.c主函数

[13.6 项目示例2:秒表(定时器扫描数码管)](#13.6 项目示例2:秒表(定时器扫描数码管))

✅项目功能:

✅项目架构:

✅main.c主函数


13.1 AT24C02 基础知识

13.2 AT24C02 引脚定义


AT24C02 为标准 8 引脚芯片,不管是直插还是贴片,引脚顺序和功能 完全固定核心有用引脚仅 4 个,其余为辅助引脚,超级简单!

  • 引脚1(A0)、引脚2(A1)、引脚3(A2) :器件地址引脚 → 全部接 GND 即可!(固定接法,不用改)
  • 引脚4(GND) :电源地 → 接单片机GND
  • 引脚5(SDA) :I2C串行数据线 → 接单片机任意IO口(软件模拟,如P2^0)
  • 引脚6(SCL) :I2C串行时钟线 → 接单片机任意IO口(软件模拟,如P2^1)
  • 引脚7(WP) :写保护引脚 → 【重中之重】接GND=允许读写;接VCC=只读模式(禁止写入)
  • 引脚8(VCC) :电源正极 → 接单片机5V电源
    ✔ 引脚核心重点总结
  1. A0/A1/A2 必须接 GND,这是 51 单片机驱动 AT24C02 的固定接法,唯一选择;
  2. SDA、SCL 是灵魂引脚,I2C 总线的两根线,所有数据收发都靠这两个引脚;
  3. WP 引脚 一定要接 GND!如果接 VCC,你只能读数据,永远写不进去,新手 90% 的写失败都是这个原因;

13.3 I2C总线

13.3.1 I2C简介

13.3.2 I2C时序结构

⭐起始条件

  • 【标准定义】

SCL 保持高电平期间,SDA 引脚从高电平向低电平 跳变 → 产生起始信号,告知所有挂载在 I2C 总线上的从设备: 主机要开始发起通信了,请准备接收数据。
起始条件:SCL高 → SDA 高 ↓ 低

  • 【时序结构步骤】(严格按顺序执行,一步都不能错)

① 置 SDA = 1 (释放数据线,拉高)
② 置 SCL = 1 (释放时钟线,拉高,进入总线空闲状态)
③ 短暂延时 (Delay(1)) → 稳定电平,避免毛刺
④ 置 SDA = 0 (核心跳变:SCL高电平时,SDA由高变低)
⑤ 短暂延时 (Delay(1)) → 让从机识别到起始信号
⑥ 置 SCL = 0 (拉低时钟线,锁定起始信号,准备发送后续数据)

  • 【对应你的 51 驱动代码】(一字不差匹配)
cpp 复制代码
void I2C_Start(void)
{
	I2C_SDA = 1;  //① SDA拉高
	I2C_SCL = 1;  //② SCL拉高
	 Delay(1);         //③ 延时稳定
	I2C2_SDA = 0;  //④ SCL高,SDA高→低 起始信号
	 Delay(1);         //⑤ 延时识别
	I2C_SCL = 0;  //⑥ SCL拉低,准备发数据
}

⭐终止条件

  • 【标准定义】

SCL 保持高电平期间,SDA 引脚从低电平 向 高电平 跳变 → 产生终止信号,告知所有从设备: 主机本次通信结束,总线恢复空闲状态
终止条件:SCL高 → SDA 低 ↑ 高

  • 【时序结构步骤】(严格按顺序执行,一步都不能错)

① 置 SDA = 0 (拉低数据线,为跳变做准备)
② 置 SCL = 1 (拉高时钟线,满足SCL高电平的前提)
③ 短暂延时 (Delay(1)) → 稳定电平
④ 置 SDA = 1 (核心跳变:SCL高电平时,SDA由低变高)
⑤ 短暂延时 (Delay(1)) → 让从机识别到终止信号

  • 【对应你的 51 驱动代码】(一字不差匹配)
cpp 复制代码
void I2C_Stop(void)
{
	I2C_SDA = 0;  //① SDA拉低
	I2C_SCL = 1;  //② SCL拉高
 	Delay(1);         //③ 延时稳定
	I2C_SDA = 1;  //④ SCL高,SDA低→高 终止信号
	 Delay(1);         //⑤ 延时识别
}

⭐发送一个字节

  • 【标准定义】
    主机在发起起始信号后,通过 SDA 引脚逐位发送 8 位二进制数据 ,配合 SCL 时钟的高低电平完成数据传输, 固定先发最高位 (bit7),最后发最低位 (bit0),无任何例外。
  • 【时序结构步骤】(严格按顺序执行,一步都不能错)

① 置 SCL = 0 (拉低时钟,允许SDA电平变化,准备发送当前位)
② 将待发送字节的【当前最高位】放到SDA引脚上
③ 短暂延时 (Delay(1)) → 稳定当前位数据
④ 置 SCL = 1 (拉高时钟,从机在此时采样SDA电平,读取当前位)
⑤ 短暂延时 (Delay(1)) → 让从机完成采样
⑥ 置 SCL = 0 (拉低时钟,允许SDA准备下一位)
⑦ 将待发送字节【左移一位】,把次高位变为新的最高位
⑧ 重复①~⑦,直到发送完8位数据

  • 【对应你的 51 驱动代码】(一字不差匹配)
cpp 复制代码
void I2C_SendByte(unsigned char Byte)
{
	unsigned char i;
	for(i=0;i<8;i++)  //循环8次,发送8位
	{
		I2C_SDA = Byte&0x80;  //② 取Byte的最高位,赋值给SDA
		Delay(1);                 //③ 稳定电平
		I2C_SCL = 1;          //④ 拉高时钟,从机采样
		Delay(1);                 //⑤ 采样完成
		I2C_SCL = 0;          //⑥ 拉低时钟,准备下一位
		Byte<<=1;                 //⑦ 字节左移一位,更新最高位
	}
}

⭐接收一个字节

  • 【标准定义】

主机在发起起始信号 + 发送从机地址后,切换为接收模式,通过 SDA 引脚逐位读取8 位二进制数据,配合 SCL 时钟的高低电平完成数据接收,固定先收最高位 (bit7),最后收最低位 (bit0),无任何例外。
✔ 接收与发送的区别:接收时, 主机的 SDA 引脚为输入状态,需要先释放 SDA(置 1),由从机控制 SDA 的电平高低。

  • 【时序结构步骤】(循环 8 次,接收 8 个 bit)

① 置 SDA = 1 (释放数据线,主机改为输入模式,由从机控制SDA电平)
② 置 SCL = 1 (拉高时钟,主机在此时采样SDA电平,读取当前位)
③ 短暂延时 (Delay(1)) → 稳定电平,完成采样
④ 将读取到的SDA电平值,存入接收字节的【最低位】
⑤ 置 SCL = 0 (拉低时钟,允许从机切换SDA电平,准备下一位)
⑥ 短暂延时 (Delay(1)) → 稳定电平
⑦ 将接收字节【左移一位】,为接收下一位腾出最低位
⑧ 重复①~⑦,直到接收完8位数据

  • 【对应你的 51 驱动代码】(一字不差匹配)
cpp 复制代码
unsigned char I2C_ReceiveByte(void)
{
	unsigned char i,Byte=0;
	I2C_SDA = 1;  //① 释放SDA,主机转为输入
	for(i=0;i<8;i++)  //循环8次,接收8位
	{
		I2C_SCL = 1;  //② 拉高时钟,主机采样SDA
		Delay(1);         //③ 稳定电平
		Byte<<=1;         //⑦ 字节左移一位,腾出最低位
		Byte|=I2C_SDA;   //④ 读取SDA电平,存入最低位
		Delay(1);
		I2C_SCL = 0;  //⑤ 拉低时钟,准备下一位
	}
	return Byte;
}

⭐发送应答

  • 【标准定义】
    主机从从机读取数据(如:从 AT24C02 读取存储的数据)后, 主机需要向从机发送应答信号,告知从机「是否继续发送下一个字节」,是读数据的必备时序
  • 【时序结构步骤】(循环 8 次,接收 8 个 bit)

① 置 SCL = 0 (拉低时钟,允许SDA电平变化,准备发送应答位)
② 将应答位电平(0=ACK,1=NACK)赋值给SDA引脚
③ 短暂延时 (Delay(1)) → 稳定电平
④ 置 SCL = 1 (拉高时钟,从机采样SDA电平,读取应答位)
⑤ 短暂延时 (Delay(1)) → 采样完成
⑥ 置 SCL = 0 (拉低时钟,结束应答位发送)

  • 【对应你的 51 驱动代码】(一字不差匹配)
cpp 复制代码
void I2C_SendAck(unsigned char AckBit)
{
	I2C_SCL = 0;  //① 拉低时钟
	I2C_SDA = AckBit; //② 赋值应答位:0=ACK,1=NACK
	 Delay(1);         //③ 稳定电平
	I2C_SCL = 1;  //④ 拉高时钟,从机采样
	 Delay(1);         //⑤ 采样完成
	I2C_SCL = 0;  //⑥ 拉低时钟,结束应答
}

⭐接收应答

  • 【标准定义】
    主机向从机发送数据(如:给 AT24C02 发从机地址、发存储地址、发写入数据)后, 主机需要读取从机的应答信号 ,判断从机是否成功接收数据,是你代码中 最常用的应答时序
  • 【时序结构步骤】(循环 8 次,接收 8 个 bit)

① 置 SDA = 1 (释放数据线,主机转为输入,由从机控制SDA电平)
② 置 SCL = 1 (拉高时钟,进入第9个时钟周期,主机采样应答位)
③ 短暂延时 (Delay(1)) → 稳定电平,读取SDA的电平值(应答位)
④ 记录应答位的电平值(0=ACK成功,1=NACK失败)
⑤ 置 SCL = 0 (拉低时钟,结束应答位采样)
⑥ 返回应答位的电平值

  1. 【对应你的 51 驱动代码】(一字不差匹配)
cpp 复制代码
unsigned char I2C_ReceiveAck(void)
{
 	unsigned char AckBit;
	I2C_SDA = 1;  //① 释放SDA,主机输入
	 Delay(1);
	I2C_SCL = 1;  //② 拉高时钟,采样应答位
 	Delay(1);
	AckBit = AT24C02_SDA; //④ 读取应答位电平
 	Delay(1);
	I2C_SCL = 0;  //⑤ 拉低时钟,结束应答
	 return AckBit;     //⑥ 返回应答状态:0=成功,1=失败
}

I2C.c

cpp 复制代码
#include <REGX52.H>

//I2C总线引脚定义:SCL时钟线接P2^1,SDA数据线接P2^0
sbit I2C_SCL=P2^1;
sbit I2C_SDA=P2^0;

/**
  * @brief  I2C开始信号
  * @param  无
  * @retval 无
  * @note   标准I2C起始条件:SCL高电平期间,SDA由高电平拉低为低电平
  */
void I2C_Start(void)
{
	I2C_SDA=1;	//释放数据线,置高电平
	I2C_SCL=1;	//释放时钟线,置高电平
	I2C_SDA=0;	//SCL高电平时,SDA高变低,产生起始信号
	I2C_SCL=0;	//拉低时钟线,锁定总线,准备后续数据传输
}

/**
  * @brief  I2C停止信号
  * @param  无
  * @retval 无
  * @note   标准I2C终止条件:SCL高电平期间,SDA由低电平拉高为高电平
  */
void I2C_Stop(void)
{
	I2C_SDA=0;	//拉低数据线,为电平跳变做准备
	I2C_SCL=1;	//释放时钟线,置高电平
	I2C_SDA=1;	//SCL高电平时,SDA低变高,产生停止信号,总线恢复空闲
}

/**
  * @brief  I2C发送一个字节数据
  * @param  Byte 要发送的单字节数据(0x00~0xFF)
  * @retval 无
  * @note   高位先行,依次发送8位数据;SCL高电平采样,低电平准备数据
  */
void I2C_SendByte(unsigned char Byte)
{
	unsigned char i;
	for(i=0;i<8;i++)	//循环8次,发送完整的8位数据
	{
		//0x80为二进制10000000,右移i位依次匹配bit7~bit0,按位与取出当前发送位
		I2C_SDA=Byte&(0x80>>i);
		I2C_SCL=1;		//拉高时钟线,从机在此刻采样SDA电平,读取当前位数据
		I2C_SCL=0;		//拉低时钟线,允许SDA电平变化,准备下一位数据
	}
}

/**
  * @brief  I2C接收一个字节数据
  * @param  无
  * @retval 接收到的单字节数据(0x00~0xFF)
  * @note   高位先行,依次接收8位数据;主机需先释放SDA,由从机控制SDA电平
  */
unsigned char I2C_ReceiveByte(void)
{
	unsigned char i,Byte=0x00;	//初始化接收字节为0,存储最终接收的数据
	I2C_SDA=1;					//释放数据线,主机转为输入模式,由从机控制SDA电平
	for(i=0;i<8;i++)			//循环8次,接收完整的8位数据
	{
		I2C_SCL=1;				//拉高时钟线,主机在此刻采样SDA电平,读取当前位数据
		if(I2C_SDA)				//如果采样到SDA为高电平
		{
			Byte|=(0x80>>i);	//将对应位赋值为1,低电平则保持0不变
		}
		I2C_SCL=0;				//拉低时钟线,允许从机切换SDA电平,准备下一位数据
	}
	return Byte;				//返回接收到的完整字节数据
}

/**
  * @brief  I2C发送应答位
  * @param  AckBit 应答位数据,0为应答(ACK),1为非应答(NACK)
  * @retval 无
  * @note   应答位为第9个时钟周期,主机接收数据后,向从机反馈的应答信号
  */
void I2C_SendAck(unsigned char AckBit)
{
	I2C_SDA=AckBit;	//赋值应答位电平,0=继续传输,1=终止传输
	I2C_SCL=1;		//拉高时钟线,从机采样应答位
	I2C_SCL=0;		//拉低时钟线,应答完成
}

/**
  * @brief  I2C接收应答位
  * @param  无
  * @retval 接收到的应答位,0为应答(ACK),1为非应答(NACK)
  * @note   应答位为第9个时钟周期,从机接收数据后,向主机反馈的应答信号
  */
unsigned char I2C_ReceiveAck(void)
{
	unsigned char AckBit;	//存储应答位状态
	I2C_SDA=1;				//释放数据线,主机转为输入模式,由从机控制应答位电平
	I2C_SCL=1;				//拉高时钟线,主机采样应答位电平
	AckBit=I2C_SDA;			//读取并保存应答位状态
	I2C_SCL=0;				//拉低时钟线,应答完成
	return AckBit;			//返回应答状态,0=成功应答,1=无应答
}

✅I2C.h

cpp 复制代码
#ifndef __I2C_H__
#define __I2C_H__

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);


#endif

13.3.3 I2C数据帧

⭐ I2C发送一帧数据

⭐ I2C接收一帧数据

⭐ I2C先发送再接收一帧数据

13.4 AT24C02数据帧

✅AT24C02.c

cpp 复制代码
#include <REGX52.H>
#include "I2C.h"	// 包含软件模拟I2C的底层驱动函数头文件

#define AT24C02_ADDRESS		0xA0	// AT24C02的I2C设备固定写地址,不可修改

/**
  * @brief  AT24C02写入一个字节数据
  * @param  WordAddress 要写入字节的存储地址,范围0x00~0xFF
  * @param  Data 要写入的单字节数据,范围0x00~0xFF
  * @retval 无
  * @note   遵循I2C写时序,写完自动产生停止信号,AT24C02为字节写入模式
  */
void AT24C02_WriteByte(unsigned char WordAddress,unsigned char Data)
{
	I2C_Start();					// 1. 发送I2C起始信号,开启通信
	I2C_SendByte(AT24C02_ADDRESS);	// 2. 发送AT24C02的I2C写地址 0xA0
	I2C_ReceiveAck();				// 3. 等待接收从机应答,确认设备在线
	I2C_SendByte(WordAddress);		// 4. 发送要写入数据的存储单元地址
	I2C_ReceiveAck();				// 5. 等待接收从机应答,确认地址接收成功
	I2C_SendByte(Data);				// 6. 发送需要写入的单字节数据
	I2C_ReceiveAck();				// 7. 等待接收从机应答,确认数据接收成功
	I2C_Stop();						// 8. 发送I2C停止信号,结束本次写操作
}

/**
  * @brief  AT24C02读取一个字节数据
  * @param  WordAddress 要读出字节的存储地址,范围0x00~0xFF
  * @retval 成功读出的单字节数据
  * @note   遵循I2C随机地址读时序,先写地址再读数据,读单字节发非应答
  */
unsigned char AT24C02_ReadByte(unsigned char WordAddress)
{
	unsigned char Data;			// 定义变量,存储读取到的数据
	I2C_Start();					// 1. 发送I2C起始信号,开启通信
	I2C_SendByte(AT24C02_ADDRESS);	// 2. 发送AT24C02的I2C写地址 0xA0
	I2C_ReceiveAck();				// 3. 等待接收从机应答,确认设备在线
	I2C_SendByte(WordAddress);		// 4. 发送要读取数据的存储单元地址
	I2C_ReceiveAck();				// 5. 等待接收从机应答,确认地址接收成功
	
	I2C_Start();					// 6. 发送I2C重复起始信号,切换读写模式
	I2C_SendByte(AT24C02_ADDRESS|0x01);	//7.发送AT24C02的I2C读地址 0xA1
	I2C_ReceiveAck();				// 8. 等待接收从机应答,确认进入读模式
	Data=I2C_ReceiveByte();			// 9. 接收从机返回的一个字节数据
	I2C_SendAck(1);					//10.发送【非应答位】,告知从机只读1个字节,结束读取
	I2C_Stop();						//11.发送I2C停止信号,结束本次读操作
	return Data;					//返回读取到的数据
}

✅AT24C02.h

cpp 复制代码
#ifndef __AT24C02_H__
#define __AT24C02_H__

void AT24C02_WriteByte(unsigned char WordAddress,Data);
unsigned char AT24C02_ReadByte(unsigned char WordAddress);


#endif

13.5 项目示例1:AT24C02数据存储

✅项目功能:

✅项目架构:

main.c主函数

cpp 复制代码
#include <REGX52.H>
#include "LCD1602.h"    // 包含LCD1602液晶显示驱动头文件
#include "Key.h"        // 包含独立按键扫描驱动头文件
#include "AT24C02.h"    // 包含AT24C02存储芯片驱动头文件
#include "Delay.h"      // 包含延时函数驱动头文件

unsigned char KeyNum;   // 定义按键编号变量,存储扫描到的按键值
unsigned int Num;       // 定义无符号整型变量(2字节/16位),存储核心数值 范围0~65535

void main()
{
	LCD_Init();                // 初始化LCD1602液晶显示模块
	LCD_ShowNum(1,1,Num,5);    // 初始化显示:在LCD第1行第1列,显示Num的值,固定显示5位数字
	while(1)                   // 死循环,持续扫描按键+执行对应功能
	{
		KeyNum=Key();          // 调用按键扫描函数,获取当前按下的按键编号
		
		if(KeyNum==1)	        // 如果按下K1按键,执行数值自增功能
		{
			Num++;             // 数值+1
			LCD_ShowNum(1,1,Num,5); // 更新LCD显示最新数值
		}
		if(KeyNum==2)	        // 如果按下K2按键,执行数值自减功能
		{
			Num--;             // 数值-1
			LCD_ShowNum(1,1,Num,5); // 更新LCD显示最新数值
		}
		if(KeyNum==3)	        // 如果按下K3按键,执行【数值写入AT24C02】功能
		{
			// AT24C02每个地址只能存储1字节(8位)数据,Num是2字节(16位)整型,需拆分为高低位分别存储
			AT24C02_WriteByte(0,Num%256);  // 把Num的【低8位数据】写入AT24C02的0号存储地址
			Delay(5);                     // 必须延时5ms,等待AT24C02完成写入操作
			AT24C02_WriteByte(1,Num/256);  // 把Num的【高8位数据】写入AT24C02的1号存储地址
			Delay(5);                     // 必须延时5ms,等待AT24C02完成写入操作
			
			LCD_ShowString(2,1,"Write OK"); // LCD第2行第1列显示"写入成功"提示
			Delay(1000);                    // 提示语显示1秒钟
			LCD_ShowString(2,1,"        "); // 清掉提示语,显示8个空格占位
		}
		if(KeyNum==4)	        // 如果按下K4按键,执行【从AT24C02读取数值】功能
		{
			// 从AT24C02的对应地址读取高低位数据,再拼接还原成完整的2字节整型数据
			Num=AT24C02_ReadByte(0);              // 先读取0号地址的【低8位数据】赋值给Num
			Num|=AT24C02_ReadByte(1)<<8;          // 读取1号地址的【高8位数据】左移8位后,拼接至Num高位
			
			LCD_ShowNum(1,1,Num,5);               // 更新LCD显示读取到的数值
			LCD_ShowString(2,1,"Read OK ");       // LCD第2行第1列显示"读取成功"提示
			Delay(1000);                          // 提示语显示1秒钟
			LCD_ShowString(2,1,"        ");       // 清掉提示语,显示8个空格占位
		}
	}
}

13.6 项目示例2:秒表(定时器扫描数码管)

✅项目功能:

✅项目架构:

✅main.c主函数

cpp 复制代码
#include <REGX52.H>
#include "Timer0.h"    // 包含定时器0初始化驱动头文件
#include "Key.h"       // 包含按键扫描驱动头文件
#include "Nixie.h"     // 包含8位数码管显示驱动头文件
#include "Delay.h"     // 包含延时函数驱动头文件
#include "AT24C02.h"   // 包含AT24C02存储芯片驱动头文件

unsigned char KeyNum;        // 存储扫描到的按键编号 1-4
unsigned char Min,Sec,MiniSec;// 秒表计时变量:Min=分钟,Sec=秒钟,MiniSec=10ms单位的毫秒位
unsigned char RunFlag;       // 秒表启停标志位 0=停止计时 1=开始计时

void main()
{
	Timer0_Init();            // 初始化定时器0,开启定时器中断,作为整个程序的时基核心
	while(1)                  // 死循环,持续执行:按键扫描+功能执行+数码管显示缓存刷新
	{
		KeyNum=Key();         // 调用按键扫描函数,获取当前按下的按键值
		
		if(KeyNum==1)			// K1按键按下
		{
			RunFlag=!RunFlag;	// 启停标志位取反翻转 停止→启动  启动→停止
		}
		if(KeyNum==2)			// K2按键按下
		{
			Min=0;				// 分钟清0
			Sec=0;				// 秒钟清0
			MiniSec=0;			// 毫秒位清0  实现秒表清零复位
		}
		if(KeyNum==3)			// K3按键按下
		{
			// 将当前秒表的【分钟、秒钟、毫秒位】分别写入AT24C02对应地址,实现掉电保存
			AT24C02_WriteByte(0,Min);	    // 分钟数据写入AT24C02的0号地址
			Delay(5);                     // 延时5ms,等待AT24C02完成写入(硬件必要时间)
			AT24C02_WriteByte(1,Sec);	    // 秒钟数据写入AT24C02的1号地址
			Delay(5);
			AT24C02_WriteByte(2,MiniSec);	// 毫秒位数据写入AT24C02的2号地址
			Delay(5);
		}
		if(KeyNum==4)			// K4按键按下
		{
			// 从AT24C02对应地址读取存储的计时数据,还原到秒表变量,实现掉电恢复计时
			Min=AT24C02_ReadByte(0);	    // 读取0号地址 → 分钟
			Sec=AT24C02_ReadByte(1);	    // 读取1号地址 → 秒钟
			MiniSec=AT24C02_ReadByte(2);	// 读取2号地址 → 毫秒位
		}
		
		// 设置数码管显示缓存,分配8位数码管的显示内容,驱动数码管显示当前计时数据
		Nixie_SetBuf(1,Min/10);    // 第1位数码管 → 分钟的十位
		Nixie_SetBuf(2,Min%10);    // 第2位数码管 → 分钟的个位
		Nixie_SetBuf(3,11);        // 第3位数码管 → 显示分隔符【-】(11为数码管驱动中定义的分隔符码值)
		Nixie_SetBuf(4,Sec/10);    // 第4位数码管 → 秒钟的十位
		Nixie_SetBuf(5,Sec%10);    // 第5位数码管 → 秒钟的个位
		Nixie_SetBuf(6,11);        // 第6位数码管 → 显示分隔符【-】
		Nixie_SetBuf(7,MiniSec/10);// 第7位数码管 → 毫秒位的十位
		Nixie_SetBuf(8,MiniSec%10);// 第8位数码管 → 毫秒位的个位
	}
}

/**
  * @brief  秒表计时驱动函数,定时器10ms调用一次该函数
  * @param  无
  * @retval 无
  * @note   只有RunFlag=1时才会计时,计时规则:100个10ms=1秒,60秒=1分钟,60分钟清零
  */
void Sec_Loop(void)
{
	if(RunFlag)  // 判断秒表启停标志位,为1则开始计时,为0则停止计时
	{
		MiniSec++;  // 10ms单位的毫秒位自增1 → 每调用一次+10ms
		if(MiniSec>=100)  // 毫秒位累计到100 → 等价于 10ms*100 = 1000ms = 1秒钟
		{
			MiniSec=0;    // 毫秒位清零
			Sec++;        // 秒钟自增1
			if(Sec>=60)   // 秒钟累计到60 → 1分钟
			{
				Sec=0;     // 秒钟清零
				Min++;     // 分钟自增1
				if(Min>=60)// 分钟累计到60 → 秒表最大计时60分钟,满60清零循环
				{
					Min=0; // 分钟清零
				}
			}
		}
	}
}

/**
  * @brief  定时器0中断服务函数,系统核心时基,定时500us进入一次中断
  * @param  无
  * @retval 无
  * @note   中断内实现:按键消抖扫描、数码管动态刷新、秒表计时 分时复用,互不干扰
  */
void Timer0_Routine() interrupt 1
{
	// 定义静态计数变量,静态变量中断中值不丢失,用于累计中断次数,实现不同时长的定时调用
	static unsigned int T0Count1,T0Count2,T0Count3;
	
	TL0 = 0x18;		// 重装定时器0低8位初值
	TH0 = 0xFC;		// 重装定时器0高8位初值  定时初值FC18 → 定时500微秒(0.5ms)
	
	T0Count1++;     // 累计0.5ms中断次数
	if(T0Count1>=20)// 累计20次 → 0.5ms*20 = 20ms
	{
		T0Count1=0; // 计数清零
		Key_Loop(); // 20ms调用一次按键扫描函数,实现按键消抖和扫描
	}
	
	T0Count2++;     // 累计0.5ms中断次数
	if(T0Count2>=2) // 累计2次 → 0.5ms*2 = 2ms
	{
		T0Count2=0; // 计数清零
		Nixie_Loop();//2ms调用一次数码管驱动函数,实现8位数码管动态显示,无闪烁
	}
	
	T0Count3++;     // 累计0.5ms中断次数
	if(T0Count3>=10)// 累计10次 → 0.5ms*10 = 10ms
	{
		T0Count3=0; // 计数清零
		Sec_Loop(); // 10ms调用一次秒表计时函数,驱动秒表走时
	}
}
相关推荐
麦托团子16 小时前
51单片机学习笔记14-DS18B20(单总线)
51单片机
麦托团子1 天前
51单片机学习笔记12-蜂鸣器
51单片机
麦托团子1 天前
51单片机学习笔记15-PWM脉冲编码调制
51单片机
麦托团子2 天前
51单片机学习笔记10-点阵屏
51单片机
恶魔泡泡糖2 天前
51单片机外部中断
c语言·单片机·嵌入式硬件·51单片机
项目題供诗2 天前
51单片机入门(五)
单片机·嵌入式硬件·51单片机
A-code2 天前
嵌入式UI刷新:观察者模式实战
stm32·单片机·mcu·物联网·51单片机
项目題供诗3 天前
51单片机入门(四)
单片机·嵌入式硬件·51单片机
项目題供诗3 天前
51单片机入门(三)
单片机·嵌入式硬件·51单片机