从AT24C02数据存储(I2C总线)到定时中断回调函数(或驱动函数)(51单片机学习笔记) 在上一章《从DS1302可调时钟到蜂鸣器播放生日快乐歌51单片机学习笔记》中我们探索了实时时钟模块和蜂鸣器音乐播放的应用。本章我们将继续深入51单片机的外设学习重点介绍AT24C02 EEPROM存储器和I2C总线通信协议以及定时中断回调函数的应用。通过这两个核心内容的学习您将掌握单片机数据持久化存储和高效定时任务调度的关键技术。AT24C02是一种基于I2C总线的EEPROM存储器具有掉电不丢失数据的特性非常适合保存系统配置、用户设置等关键信息。而定时中断回调函数则是单片机编程中实现多任务调度、提高系统响应性的重要设计模式。本章将通过详细的原理讲解、时序分析和完整代码示例帮助您从理论到实践全面掌握这两项技术。如果觉得有收获可以为我的文章点点小赞这将激励我创作更好的内容谢谢一、AT24C02与I2C总线1.1 存储器介绍1.2 存储器简化模型1.3 AT24C02介绍AT24C02是一种可以实现掉电不丢失的存储器可用于保存单片机运行时想要永久保存的数据信息存储介质E2PROM通讯接口I2C总线容量256字节1.4 引脚及应用电路1.5 内部结构框图1.6 I2C总线介绍I2C总线Inter IC BUS是由Philips公司开发的一种通用数据总线两根通信线SCLSerial Clock、SDASerial Data同步、半双工带数据应答通用的I2C总线可以使各种设备的通信标准统一对于厂家来说使用成熟的方案可以缩短芯片设计周期、提高稳定性对于应用者来说使用通用的通信协议可以避免学习各种各样的自定义协议降低了学习和应用的难度1.7 I2C电路规范所有I2C设备的SCL连在一起SDA连在一起设备的SCL和SDA均要配置成开漏输出模式SCL和SDA各添加一个上拉电阻阻值一般为4.7KΩ左右开漏输出和上拉电阻的共同作用实现了“线与”的功能此设计主要是为了解决多机通信互相干扰的问题1.8 I2C时序结构16种基本操作起始条件SCL高电平期间SDA从高电平切换到低电平终止条件SCL高电平期间SDA从低电平切换到高电平根据I2C协议的流程使用代码控制SCL和SDA的引脚我们可以写出以下代码#include REGX52.H sbit I2C_SCLP2^1; sbit I2C_SDAP2^0; /** * brief I2C开始 * param 无 * retval 无 */ void I2C_Start(void)//I2C起始函数 { I2C_SDA1;//确认SDA的电平为1 I2C_SCL1;//确认SCL的电平为1 I2C_SDA0;//起始标志 I2C_SCL0;//准备完毕 } /** * brief I2C停止 * param 无 * retval 无 */ void I2C_Stop(void)//I2C终止函数 { I2C_SDA0;//确认SDA的电平为0 I2C_SCL1;//终止标志 I2C_SDA1;//终止完毕 }发送一个字节SCL低电平期间主机将数据位依次放到SDA线上高位在前然后拉高SCL从机将在SCL高电平期间读取数据位所以SCL高电平期间SDA不允许有数据变化依次循环上述过程8次即可发送一个字节/** * brief I2C发送一个字节 * param Byte 要发送的字节 * retval 无 */ void I2C_SendByte(unsigned char Byte)//I2C发送一个字节函数 { unsigned char i; for(i0;i8;i) { I2C_SDAByte(0x80i);//传入字节的每一位 I2C_SCL1;//发送一位完毕 I2C_SCL0;//改回I2C_SCL的值 //我的单片机运行速度不快可以不插入Delay } }接收一个字节SCL低电平期间从机将数据位依次放到SDA线上高位在前然后拉高SCL主机将在SCL高电平期间读取数据位所以SCL高电平期间SDA不允许有数据变化依次循环上述过程8次即可接收一个字节主机在接收之前需要释放SDA/** * brief I2C接收一个字节 * param 无 * retval 接收到的一个字节数据 */ unsigned char I2C_ReceiveByte(void)//I2C接收一个字节函数 { unsigned char i,Byte0x00; I2C_SDA1; for(i0;ilt;8;i) { I2C_SCL1;//开始接收一位 if(I2C_SDA){Byte|(0x80gt;gt;i);}//接收一位完毕 I2C_SCL0;//改回I2C_SCL的值 } return Byte; }发送应答在接收完一个字节之后主机在下一个时钟发送一位数据数据0表示应答数据1表示非应答接收应答在发送完一个字节之后主机在下一个时钟接收一位数据判断从机是否应答数据0表示应答数据1表示非应答主机在接收之前需要释放SDA/** * brief I2C发送应答 * param AckBit 应答位0为应答1为非应答 * retval 无 */ void I2C_SendAck(unsigned char AckBit)//I2C发送应答 { I2C_SDAAckBit;//写入应答与否 I2C_SCL1;//发送应答与否 I2C_SCL0;//改回I2C_SCL的值 } /** * brief I2C接收应答位 * param 无 * retval 接收到的应答位0为应答1为非应答 */ unsigned char I2C_ReceiveAck(void)//I2C接收应答 { unsigned char AckBit; I2C_SDA1;//释放I2C_SCL确认SDA的电平为1 I2C_SCL1;//开始接收应答与否 AckBitI2C_SDA;//接收应答与否 I2C_SCL0;//改回I2C_SCL的值 return AckBit;//返回应答 }23种基本格式的数据帧AT24C02 的固定地址为 1010可配置地址本开发板上为 000所以 SLAVE ADDRESSW 为 0xA0SLAVE ADDRESSR 为 0xA11.发送一帧数据完成任务向谁发什么2.接收一帧数据完成任务向谁收什么3.先发送再接收数据帧复合格式完成任务向谁收指定的什么有时客机的发送端要知道主机要接收什么数据3AT24C02规定的2种拓展数据帧1.字节写在WORD ADDRESS处写入数据DATA根据I2C协议的流程利用第1点的6个基本操作函数我们可以写出以下代码#include REGX52.H #include I2C.h #define AT24C02_ADDRESS 0xA0 /** * brief AT24C02写入一个字节 * param WordAddress 要写入字节的地址 * param Date 要写入的数据 * retval 无 */ void AT24C02_WriteByte(unsigned char WordAddress,Date) { I2C_Start(); I2C_SendByte(AT24C02_ADDRESS); I2C_ReceiveAck(); I2C_SendByte(WordAddress); I2C_ReceiveAck(); I2C_SendByte(Date); I2C_ReceiveAck(); I2C_Stop(); Delay(5);//等待5ms让数据写入完毕 }注意注意AT24C02在字节写时数据手册有一个最大写入时间5ms写入字节后须等待5ms让数据写入完毕↓↓↓↓2.随机读读出在WORD ADDRESS处的数据DATAAT24C02的固定地址为1010可配置地址本开发板上为000所以SLAVE ADDRESSW为0xA0SLAVE ADDRESSR为0xA1/** * brief AT24C02读取一个字节 * param WordAddress 要读出字节的地址 * retval 读出的数据 */ unsigned char AT24C02_ReadByte(unsigned char WordAddress) { unsigned char Date; I2C_Start(); I2C_SendByte(AT24C02_ADDRESS); I2C_ReceiveAck(); I2C_SendByte(WordAddress); I2C_ReceiveAck(); I2C_Start(); I2C_SendByte(AT24C02_ADDRESS|0x01);//把写地址变成读地址然后发送 I2C_ReceiveAck(); DateI2C_ReceiveByte(); I2C_SendAck(1); return Date; }以上三点根据数据手册提炼出的更易理解的模型数据手册里对应这2种数据帧的实际描述如下完整的I2C与AT24C02代码模块I2C.c文件↓----------------------------#include REGX52.H sbit I2C_SCLP2^1; sbit I2C_SDAP2^0; /** * brief I2C开始 * param 无 * retval 无 */ void I2C_Start(void)//I2C起始函数 { I2C_SDA1;//确认SDA的电平为1 I2C_SCL1;//确认SCL的电平为1 I2C_SDA0;//起始标志 I2C_SCL0;//准备完毕 } /** * brief I2C停止 * param 无 * retval 无 */ void I2C_Stop(void)//I2C终止函数 { I2C_SDA0;//确认SDA的电平为0 I2C_SCL1;//终止标志 I2C_SDA1;//终止完毕 } /** * brief I2C发送一个字节 * param Byte 要发送的字节 * retval 无 */ void I2C_SendByte(unsigned char Byte)//I2C发送一个字节函数 { unsigned char i; for(i0;i8;i) { I2C_SDAByte(0x80i);//传入字节的每一位 I2C_SCL1;//发送一位完毕 I2C_SCL0;//改回I2C_SCL的值 //我的单片机运行速度不快可以不插入Delay } } /** * brief I2C接收一个字节 * param 无 * retval 接收到的一个字节数据 */ unsigned char I2C_ReceiveByte(void)//I2C接收一个字节函数 { unsigned char i,Byte0x00; I2C_SDA1; for(i0;i8;i) { I2C_SCL1;//开始接收一位 if(I2C_SDA){Byte|(0x80i);}//接收一位完毕 I2C_SCL0;//改回I2C_SCL的值 } return Byte; } /** * brief I2C发送应答 * param AckBit 应答位0为应答1为非应答 * retval 无 */ void I2C_SendAck(unsigned char AckBit)//I2C发送应答 { I2C_SDAAckBit;//写入应答与否 I2C_SCL1;//发送应答与否 I2C_SCL0;//改回I2C_SCL的值 } /** * brief I2C接收应答位 * param 无 * retval 接收到的应答位0为应答1为非应答 */ unsigned char I2C_ReceiveAck(void)//I2C接收应答 { unsigned char AckBit; I2C_SDA1;//释放I2C_SCL确认SDA的电平为1 I2C_SCL1;//开始接收应答与否 AckBitI2C_SDA;//接收应答与否 I2C_SCL0;//改回I2C_SCL的值 return AckBit;//返回应答 }----------------------------AT24C02.c文件↓----------------------------#include REGX52.H #include I2C.h #include Delay.h #define AT24C02_ADDRESS 0xA0 /** * brief AT24C02写入一个字节 * param WordAddress 要写入字节的地址 * param Date 要写入的数据 * retval 无 */ void AT24C02_WriteByte(unsigned char WordAddress,Date) { I2C_Start(); I2C_SendByte(AT24C02_ADDRESS); I2C_ReceiveAck(); I2C_SendByte(WordAddress); I2C_ReceiveAck(); I2C_SendByte(Date); I2C_ReceiveAck(); I2C_Stop(); Delay(5);//等待5ms让数据写入完毕 } /** * brief AT24C02读取一个字节 * param WordAddress 要读出字节的地址 * retval 读出的数据 */ unsigned char AT24C02_ReadByte(unsigned char WordAddress) { unsigned char Date; I2C_Start(); I2C_SendByte(AT24C02_ADDRESS); I2C_ReceiveAck(); I2C_SendByte(WordAddress); I2C_ReceiveAck(); I2C_Start(); I2C_SendByte(AT24C02_ADDRESS|0x01);//把写地址变成读地址然后发送 I2C_ReceiveAck(); DateI2C_ReceiveByte(); I2C_SendAck(1); return Date; }----------------------------1.9 实例一12.1.1AT24C02数据存储main.c文件↓----------------------------#include REGX52.H #include LCD1602.h #include Key.h #include AT24C02.h #include Delay.h unsigned char Date,KeyNum; unsigned int Num; void main() { LCD_Init(); LCD_ShowNum(1,1,Num,5); while(1) { KeyNumKey(); if(KeyNum1) { Num; LCD_ShowNum(1,1,Num,5); } if(KeyNum2) { Num--; LCD_ShowNum(1,1,Num,5); } if(KeyNum3) { AT24C02_WriteByte(0,Num%256); AT24C02_WriteByte(1,Num/256); LCD_ShowString(2,1,Write OK); Delay(1000); LCD_ShowString(2,1, ); } if(KeyNum4) { NumAT24C02_ReadByte(0); Num|AT24C02_ReadByte(1)8; LCD_ShowNum(1,1,Num,5); LCD_ShowString(2,1,Read OK); Delay(1000); LCD_ShowString(2,1, ); } } }----------------------------二、定时中断回调函数或驱动函数2.1定时中断回调函数/驱动函数有些时候多个模块都需要用到单片机的定时器的功能但定时器只有一个我们无法在多个模块中调用同一个定时器中断函数我们不能在程序中定义多个中断函数这时我们需要有“驱动”的理念我们可以在main.c中写中断函数在各个模块写一个函数这种函数是一个中断函数的调用然后在main.c里面调用这个函数假如定时器计时1毫秒计次加1可以是多个同时开始的计次模块A要被调用的函数需要30毫秒调用一次模块B要被调用的函数需要2毫秒调用一次中断函数里给模块A的计次满30次就调用一次模块A要被调用的函数模块B同理void Timer0_Routine() interrupt 1 /*写作定时器函数里面的逻辑操作一定不要过长*/ /*尤其是函数和函数里面的函数不要使用Delay*/ { static unsigned int T0Count10,T0Count20,T0Count30; TL00x66; //设置定时初值 TH00xFC; //设置定时初值 T0Count1;//独立按键定时器驱动或循环调用 if(T0Count1gt;20) { T0Count10; Key_Loop(); } T0Count2;//数码管定时器驱动或循环调用 if(T0Count2gt;2) { T0Count20; Nixie_Loop(); } T0Count3;//时钟定时器驱动或循环调用 if(T0Count3gt;10) { T0Count30; Sec_Loop(); } }这种结构可以有效避免代码模块里的内容写入定时器Timer0模块避免模块间交叉引用函数出现较高的耦合性如果函数出现较高的耦合性这将不利于我们的代码管理2.2 Key.c独立按键与Nixie.c数码管模块的升级通过每20ms扫描获取按键引脚电平,能充分消除按键抖动的影响,不会出现按住按键时其他模块无法正常工作Key_Task.c文件↓----------------------------#include REGX52.H #include Delay.h unsigned char Key_KeyNumber; /** * brief 返回独立按键键码——进阶版——搭配定时器使用 * param 无 * retval 按下按键的键码范围0~4无按键按下时返回值为0 */ unsigned char Key(void) { unsigned char Temp0; TempKey_KeyNumber; Key_KeyNumber0; return Temp; } /** * brief 获取/扫描独立按键键码——进阶版——搭配定时器使用 * param 无 * retval 按下按键的键码范围0~4无按键按下时返回值为0 */ unsigned char Key_GetState() { unsigned char KeyNumber0; if(P3_10){KeyNumber1;} if(P3_00){KeyNumber2;} if(P3_20){KeyNumber3;} if(P3_30){KeyNumber4;} return KeyNumber; } /** * brief 独立按键松手检测——搭配定时器使用 * param 无 * retval 无 */ void Key_Loop(void) { static unsigned char NowState,LastState; LastStateNowState; NowStateKey_GetState(); if(LastState1 NowState0)//松手才有效 { Key_KeyNumber1; } if(LastState2 NowState0) { Key_KeyNumber2; } if(LastState3 NowState0) { Key_KeyNumber3; } if(LastState4 NowState0) { Key_KeyNumber4; } }----------------------------Nixie_Task.c文件↓----------------------------#include REGX52.H #include Delay.h /** * brief 数码管显示数字——进阶版——搭配定时器使用 * param Location 在开发板从左往右第几位数码管显示范围1~8 * param Number 显示的数字范围0~9 * retval 无 */ unsigned char Nixie_Buf[9]{0,10,10,10,10,10,10,10,10}; unsigned char NixieTable[17]{0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x00,0x40,}; /0到9无显示“-”/ void Nixie_SetBuf(unsigned char Location,Number) { Nixie_Buf[Location]Number; } /** * brief 数码管扫描数字——搭配定时器使用 * param Location 在开发板从左往右第几位数码管在定时器触发时显示完成扫描范围1~8 * param Number 定时器触发时显示的数字也就是要扫描的数字范围0~9 * retval 无 */ void Nixie_Scan(unsigned char Location,Number) { P00x00; switch(Location) { case 1:P2_41,P2_31,P2_21;break; case 2:P2_41,P2_31,P2_20;break; case 3:P2_41,P2_30,P2_21;break; case 4:P2_41,P2_30,P2_20;break; case 5:P2_40,P2_31,P2_21;break; case 6:P2_40,P2_31,P2_20;break; case 7:P2_40,P2_30,P2_21;break; case 8:P2_40,P2_30,P2_20;break; } P0NixieTable[Number]; } void Nixie_Loop(void) { static unsigned char i1; Nixie_Scan(i,Nixie_Buf[i]); i; if(i9){i1;} }----------------------------2.3 实例二12.2.1秒表定时器扫描按键数码管利用以上知识和上一节AT24C02掉电仍存储数据的特性,我们能写出一个可记录时间的秒表:main.c文件↓----------------------------#include REGX52.H #include Timer0.h #include Nixie_Task.h #include Key_Task.h #include Delay.h #include I2C.h #include AT24C02.h unsigned char KeyNum; unsigned char Min,Sec,MiniSec; unsigned char RunFlag; void main() { Timer0_Init(); while(1) { KeyNumKey(); if(KeyNum1) { RunFlag!RunFlag; } if(KeyNum2) { Min0,Sec0,MiniSec0; } if(KeyNum3) { AT24C02_WriteByte(0,Min); AT24C02_WriteByte(1,Sec); AT24C02_WriteByte(2,MiniSec); } if(KeyNum4) { MinAT24C02_ReadByte(0); SecAT24C02_ReadByte(1); MiniSecAT24C02_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); } } void Sec_Loop(void) { if(RunFlag) { MiniSec; if(MiniSec100) { MiniSec0; Sec; if(Sec60) { Sec0; Min; if(Min60) { Min0; } } } } } void Timer0_Routine() interrupt 1 /写作定时器函数里面的逻辑操作一定不要过长/ /尤其是函数和函数里面的函数不要使用Delay/ { static unsigned int T0Count10,T0Count20,T0Count30; TL00x66; //设置定时初值 TH00xFC; //设置定时初值 T0Count1;//独立按键定时器驱动或循环调用 if(T0Count1gt;20) { T0Count10; Key_Loop(); } T0Count2;//数码管定时器驱动或循环调用 if(T0Count2gt;2) { T0Count20; Nixie_Loop(); } T0Count3;//时钟定时器驱动或循环调用 if(T0Count3gt;10) { T0Count30; Sec_Loop(); } }----------------------------三、总结本章我们系统学习了AT24C02 EEPROM存储器和I2C总线通信协议以及定时中断回调函数的设计与应用。通过本章的学习您应该掌握以下核心知识点1. AT24C02与I2C总线AT24C02是一种256字节容量的EEPROM存储器支持I2C总线通信具有掉电不丢失数据的特性I2C总线采用SCL时钟线和SDA数据线两根线实现同步半双工通信支持多主机多从机架构I2C协议包含起始条件、终止条件、发送字节、接收字节、发送应答、接收应答等6种基本操作AT24C02支持字节写和随机读两种数据帧格式写入后需要等待5ms的写入时间2. 定时中断回调函数通过定时中断回调函数可以实现模块化、低耦合的代码设计在main.c中编写统一的中断函数在各模块中编写回调函数通过定时器计数实现不同频率的任务调度这种设计避免了模块间交叉引用提高了代码的可维护性和可扩展性3. 实践应用通过AT24C02数据存储实例实现了按键计数的掉电保存功能通过秒表示例将AT24C02存储与定时中断回调函数结合实现了完整的秒表功能独立按键和数码管模块升级为定时器驱动版本消除了按键抖动影响本章内容与上一章的DS1302时钟和蜂鸣器应用形成了良好的技术衔接。掌握了这些技术后您可以设计更加复杂的单片机系统如数据采集系统、智能控制器等。下一章我们将探索51单片机的DS18B20温度传感器和LCD1602液晶显示器。都看到这了点点赞吧求求了