13【模块学习】AT24C02(一):使用学习
AT24C02
- 1、I2C协议
- 1.1、I2C简介
- 1.2、通信原理
- 1.3、通信时序
- 1.4、I2C为什么需要上拉电阻
- 2、AT24C02
- 2.1、AT24C02简介
- 2.2、代码演示
- 3、项目:使用AT24C02记录上电次数
1、I2C协议
1.1、I2C简介
I2C协议是一种同步半双工总线结构通信协议,通常用来在主设备和从设备之间进行通信。所有连接到总线的设备共用这两根线。主设备通过发送从设备的地址来与特定的从设备通信,设备通过各自的唯一地址进行识别。所以I2C可以实现一主多从。I2C总线结构如下图所示:
I2C在硬件上的接法如上图所示:主控芯片引出两条线SCL(时钟线)和SDA(数据线),在一条I2C总线上可以接很多I2C设备,我们还在总线上连接一个上拉电阻。为什么需要连接上拉电阻后面讲解。
I2C协议和串口通信UART协议对比:
😀相同点:
---------------都是串行通信
😆不同点:
-------------UART是异步双工通信(即同一时间段既能发送数据,又能接受数据,有2个数据线:Tx和Rx,低位先行)
-------------I2C是同步半双工通信(即同一时间段只能发送数据or只能接受数据,有1个数据线:SDA,高位先行)
1.2、通信原理
①主机向从机发送数据的步骤:
①发送起始信号
②发送一个字节的数据(从机地址7位+W)
③读取应答信号
④发送从机寄存器地址
⑤读取应答信号
⑥发送数据
⑦读取应答信号
⑧发送停止信号
【注意】读取的应答信号是从机给主机发送的应答信号,所以主机读取应答信号时,I2C的SDA总线应该交给从机控制。
②主机向从机读取数据的步骤:
①发送起始信号
②发送一个字节的数据(从机地址7位+W)
③读取应答信号
④发送从机寄存器地址
⑤读取应答信号
⑥再发送一个起始信号
⑦发送一个字节的数据(从机地址7位+R)
⑧读取应答信号
⑨读取数据
⑩发送应答信号
⑪发送停止信号
1.3、通信时序
①起始信号:
主机需要发送起始信号来建立通信,I2C总线默认电平为高电平。在SCL高电平时,向SDA写入低电平0表示发送起始信号。这样做与向SDA写入数据有所区别。
起始信号代码如下:
#include "I2C.h"sbit I2C_SCK = P1^0; //通信引脚SCK = P1.0
sbit I2C_SDA = P1^1; //通信引脚SDA = P1.1void Delay5us() //@11.0592MHz
{
}/*** 主机发送起始信号*/
void I2C_Start(void)
{I2C_SCK = 1;I2C_SDA = 1;Delay5us();I2C_SDA = 0; //拉低SDA发送起始信号Delay5us();I2C_SCK = 0;
}
②主机给SDA总线上写入数据:
将SCL(时钟线)拉低,然后通过拉低(写入0)/拉高(写入1)SDA来表示向SDA写入数据。
让后将SCL(时钟线)拉高,延时一段时间,等待从机读取总线上的数据。
主机向从机上写入一个字节的数据代码如下:
/*** 主机向从机上写入一个字节的数据*/
void I2C_SendByte(unsigned char Byte)
{unsigned char i = 0;unsigned char temp = 0;I2C_SCK = 0; //拉低SCK,准备写入第一位数据for(i = 0; i<8; i++){I2C_SDA = ((0x80 >> i) & Byte); //向总线上写入数据,高位先行I2C_SCK = 1; //拉高让从机准备读Delay5us(); //延时一段时间,让从机有充分的时间读取I2C_SCK = 0; //拉低准备写入下一位数据}
}
③主机读取SDA总线上的数据:
主机将SCL(时钟线)拉低,将SDA拉高释放总线,延时一段时间,等待从机向总线上写入数据。
然后将SCL(时钟线)拉高,然后读取SDA上的电平
主机向从机上读取一个字节的数据代码如下:
/*** 主机向从机读取一个字节的数据*/
unsigned char I2C_ReceiveByte(void)
{unsigned char ReceiveData = 0x00;unsigned char i = 0;I2C_SCK = 0; //将SCK拉低,让从机准备写入数据I2C_SDA = 1; //主机释放总线Delay5us(); //延时一段时间,让从机有充分的时间写入for(i = 0; i<8; i++){I2C_SCK = 1; //拉高主机读取数据if(I2C_SDA){ReceiveData |= (0x80 >> i);}I2C_SCK = 0; //拉低,让从机准备写入第二位数据Delay5us();}return ReceiveData;
}
④主机读取应答:
主机向总线上写入一个字节的数据后,需要读取应答信号来知晓从机是否接收到数据。
主机读取应答信号时,总线SDA交给从机控制。
😀若读取SDA为高电平1,则代表从机没有给应答信号
😀若读取SDA为低电平0,则代表从机给应答信号
主机读取从机的应答信号代码如下:
/*** 主机向从机读取应答(0)/非应答(1)信号*/
bit Receive_ACK(void)
{bit ACK;I2C_SDA = 1; //释放SDA总线Delay5us(); //延时一段时间,让从机有充分的时间写入应答信号I2C_SCK = 1; //拉高SCK,读取应答信号ACK = I2C_SDA; //读取ACKI2C_SCK = 0; //拉低,准备写入第二个字节数据return ACK;
}
⑤主机发送应答:
主机读取了一个字节的数据后,需要向从机发送应答信号来告诉从机,知否继续接收数据。
主机发送应答信号时,总线SDA交给主机控制。
即主机将SCL拉低,然后写入SDA的电平。
😀若向SDA写入高电平1,则代表主机没有给应答信号。用于主机不在想要读取从机数据。发送非应答信号后,从机不在向SDA上写入数据。
😀若向SDA写入低电平0,则代表主机给应答信号。用于主机继续读取从机数据,发送应答信号后,从机继续向SDA上写入数据
主机向从机发送应答/非应答信号代码如下:
/*** 主机向从机发送应答信号*/
void I2C_SendACK(void)
{I2C_SDA = 0; //将总线拉低,代码应答信号I2C_SCK = 1; //拉高SCK,让从准备读取信号Delay5us(); //延时一段时间,让从机有充分的时间读取I2C_SCK = 0; //让从机准备写下一个字节
} /*** 主机向从机发送非应答信号*/
void I2C_SendNACK(void)
{I2C_SDA = 1; //将总线拉低,代码非应答信号I2C_SCK = 1; //拉高SCK,让从准备读取信号Delay5us(); //延时一段时间,让从机有充分的时间读取I2C_SCK = 0; //拉低,准备发送停止信号
}
⑥停止信号:
主机需要发送一个停止信号来结束通信。需要将SCL和SDA都恢复置默认高电平1。但是为和向SDA上写入数据之间作出区别,所以先将SCL拉高,然后再将SDA拉高。
发送停止信号的代码如下:
/*** 主机发送停止信号*/
void I2C_Stop(void)
{I2C_SCK = 0;I2C_SDA = 0;I2C_SCK = 1;Delay5us();I2C_SDA = 1;
}
总结:😀起始信号和停止信号都是由主机发送,都是在SCL拉高时,向SDA写入数据。😀SCL拉低时,主句/从机向SDA写入数据。SCL拉高时,主句/从机读取SDA数据😀SCL一直由主机控制
1.4、I2C为什么需要上拉电阻
2、AT24C02
2.1、AT24C02简介
AT24C02是 低工作电压的 2Kb
串行电可擦除 只读存储器,可存储 256
个字节 数据,内部有一个 16
字节页写缓冲器。AT24C02工作电压 1.8~5.5V,采用二线制 IIC数据传输协议,支持硬件写保护,能擦写 100万次,数据掉电不丢失。
引脚:
片内结构:
如图:AT24C02的存储容量为 256 Byt
e,由 32页
组成且每页8 Byte
。
向AT24C02写入数据的步骤:
①发送起始信号
②发送一个字节的数据(设备地址7位+W(0))
③读取应答信号
④发送从机寄存器地址(片内保存数据的地址)
⑤读取应答信号
⑥发送数据
⑦读取应答信号
⑧发送停止信号
【注意】向AT24C02写入多个字节的数据时,只需要发送一次从机寄存器地址(片内保存数据的地址)即可。但是一次性最多写入一页的数据即8Byte。否则就会在这一页进行覆盖。
向AT24C02读取数据的步骤:
①发送起始信号
②发送一个字节的数据(设备地址7位+W(0))
③读取应答信号
④发送从机寄存器地址
⑤读取应答信号
⑥再发送一个起始信号
⑦发送一个字节的数据(设备地址7位+R(1))
⑧读取应答信号
⑨读取数据
⑩发送应答信号
⑪发送停止信号
【注意】向AT24C02读取数据时,只需要发送一次从机寄存器地址(片内保存数据的地址)即可。一次性可读取超过一页的数据,但是不能超过256个数据。
2.2、代码演示
①I2C.c文件的代码如下:
/************************************/
/* 此文件为I2C通信协议底层驱动函数 */
/* 通信引脚SCK:P1^0 */
/* 通信引脚SDA:P1^1 */
/************************************/
#include "I2C.h"sbit I2C_SCK = P1^0; //通信引脚SCK = P1.0
sbit I2C_SDA = P1^1; //通信引脚SDA = P1.1void Delay5us() //@11.0592MHz
{
}/*** 主机发送起始信号*/
void I2C_Start(void)
{I2C_SCK = 1;I2C_SDA = 1;Delay5us();I2C_SDA = 0; //拉低SDA发送起始信号Delay5us();I2C_SCK = 0;
}/*** 主机向从机上写入一个字节的数据*/
void I2C_SendByte(unsigned char Byte)
{unsigned char i = 0;unsigned char temp = 0;I2C_SCK = 0;for(i = 0; i<8; i++){I2C_SDA = ((0x80 >> i) & Byte);I2C_SCK = 1; //拉高从机准备读Delay5us();I2C_SCK = 0;}
}/*** 主机向从机读取应答(0)/非应答(1)信号*/
bit Receive_ACK(void)
{bit ACK;I2C_SDA = 1; //释放SDA总线Delay5us();I2C_SCK = 1;ACK = I2C_SDA; //读取ACKI2C_SCK = 0;return ACK;
}/*** 主机发送停止信号*/
void I2C_Stop(void)
{I2C_SCK = 0;I2C_SDA = 0;I2C_SCK = 1;Delay5us();I2C_SDA = 1;
}/*** 主机向从机读取一个字节的数据*/
unsigned char I2C_ReceiveByte(void)
{unsigned char ReceiveData = 0x00;unsigned char i = 0;I2C_SCK = 0;I2C_SDA = 1;//释放总线Delay5us();for(i = 0; i<8; i++){I2C_SCK = 1;if(I2C_SDA){ReceiveData |= (0x80 >> i);}I2C_SCK = 0;Delay5us();}return ReceiveData;
} /*** 主机向从机发送应答信号*/
void I2C_SendACK(void)
{I2C_SDA = 0;I2C_SCK = 1;Delay5us();I2C_SCK = 0; //让从机准备写下一个字节
} /*** 主机向从机发送非应答信号*/
void I2C_SendNACK(void)
{I2C_SDA = 1;I2C_SCK = 1;Delay5us();I2C_SCK = 0;
}
②I2C.h文件的代码如下:
#ifndef __I2C_H
#define __I2C_H
#include <REGX52.H> //包含51头文件,里面全是寄存器地址void I2C_Start(void); //发送起始信号
void I2C_SendByte(unsigned char Byte); //主机向总线上发送一个字节的数据
bit Receive_ACK(void); //主机读取应答信号
void I2C_Stop(void); //发送停止信号
unsigned char I2C_ReceiveByte(void); //主机读取从机的应答信号
void I2C_SendACK(void); //主机发送应答信号
void I2C_SendNACK(void); //主机发送非应答信号#endif
③AT24C02.c文件的代码如下:
/**************************************/
/* AT24C02一共能存储256个字节的数据 地址0x00~0xFF */
/* 且每8个字节被划分为1页,一共有32页 */
/* 一次性的读或写最多都能只能操作1页 */
/**************************************/
#include "AT24C02.h"void Delay5ms() //@11.0592MHz
{unsigned char i, j;i = 54;j = 199;do{while (--j);} while (--i);
}/*** 向AT24C02写入1个字节的数据* Addr:AT24C02设备地址* InAddr:内部存储地址* Byte:写入的数据*/
void AT24C02_WriteByte(unsigned char Addr ,unsigned char InAddr,unsigned char Byte)
{I2C_Start(); //发送起始信号I2C_SendByte((Addr << 1) | 0); //发送设备地址+WReceive_ACK(); //主机读取从机应答信号I2C_SendByte(InAddr); //发送内部寄存器地址Receive_ACK(); //主机读取从机应答信号I2C_SendByte(Byte); //主机发送数据Receive_ACK(); //主机读取应答信号I2C_Stop(); //发送停止信号
}/*** 向AT24C02读取1个字节的数据* Addr:AT24C02设备地址* InAddr:内部存储地址*/
unsigned char AT24C02_ReadByte(unsigned char Addr ,unsigned char InAddr)
{unsigned char Receive_Data = 0;I2C_Start(); //发送起始信号I2C_SendByte((Addr << 1) | 0); //发送设备地址+WReceive_ACK(); //主机读取从机应答信号I2C_SendByte(InAddr); //发送内部寄存器地址Receive_ACK(); //主机读取从机应答信号I2C_Start(); //发送起始信号I2C_SendByte((Addr << 1) | 1); //发送设备地址+RReceive_ACK(); //主机读取从机应答信号Receive_Data = I2C_ReceiveByte();//读取数据I2C_SendNACK(); //发送非应答I2C_Stop(); //发送停止信号return Receive_Data;
}/*** 连续向AT24C02写入多个字节的数据* Addr:AT24C02设备地址* InAddr:内部存储地址,写多个字节的数据时最好从页的起始地址开始写入* Byte:写入的数据指针* ByteLen:写入的数据长度,最写1页(ByteLen <= 8)*/
void AT24C02_WriteMultBytes(unsigned char Addr,unsigned char InAddr,unsigned char *Byte,unsigned char ByteLen)
{unsigned char i = 0;I2C_Start();I2C_SendByte((Addr << 1) | 0);Receive_ACK();I2C_SendByte(InAddr);Receive_ACK();for(i = 0; i<ByteLen; i++){I2C_SendByte(Byte[i]);Receive_ACK();}I2C_Stop();
}/*** 连续向AT24C02读取多个字节的数据* Addr:AT24C02设备地址* InAddr:内部存储地址* Byte:存储数据缓存区* ByteLen:读取的数据长度,最多256*/
void AT24C02_ReadMultBytes(unsigned char Addr,unsigned char InAddr,unsigned char *Byte,unsigned char ByteLen)
{unsigned char i = 0;I2C_Start();I2C_SendByte((Addr << 1) | 0);Receive_ACK();I2C_SendByte(InAddr);Receive_ACK();I2C_Start(); I2C_SendByte((Addr << 1) | 1);Receive_ACK();for(i = 0; i<ByteLen; i++){Byte[i] = I2C_ReceiveByte();if(i < ByteLen - 1){I2C_SendACK();}else{I2C_SendNACK();}}I2C_Stop();
}/*** 连续向AT24C02发送多页字节的数据(超过1页字节的数据)* Addr:AT24C02设备地址* WhichPage:从哪一页开始写入(1~32)* *Byte:写入的数据数组地址* ByteLen:写入的数据长度*/
/**页起始地址如下0x00 0x20 0x40 0x60 0x80 0xA0 0xC0 0xE0 0x08 0x28 0x48 0x68 0x88 0xA8 0xC8 0xE8 0x10 0x30 0x50 0x70 0x90 0xB0 0xD0 0xF0 0x18 0x38 0x58 0x78 0x98 0xB8 0xD8 0xF8 */
void AT24C02_WritePageBytes(unsigned char Addr,unsigned char WhichPage,unsigned char *Byte,unsigned char ByteLen)
{unsigned char Pages = ByteLen / 8; //通过数据个数计数出需要写几页unsigned char Yushu = ByteLen % 8; //最后一页写入多少个数据unsigned char j = 0;if(Yushu != 0) //若存在余数{Pages++; }for(j = 0; j < Pages; j++) //1页1页的写入{unsigned char i = 0;I2C_Start();I2C_SendByte((Addr << 1) | 0);Receive_ACK();I2C_SendByte((WhichPage - 1) * 8 + j * 8); //将页转换为寄存器的初始地址Receive_ACK();if((ByteLen - j * 8) >= 8 ) //若不是需要写入最后一页的数据{for(i = 0; i < 8; i++) //写入数据{I2C_SendByte(Byte[i + j * 8]);Receive_ACK();}}else //若是需要写入最后一页的数据{for(i = 0; i < Yushu; i++) //写入数据{I2C_SendByte(Byte[i + j * 8]);Receive_ACK();}} I2C_Stop();Delay5ms(); //延时等待写入成功}
}/*** 连续向AT24C02读取多页字节的数据(超过1页字节的数据)* Addr:AT24C02设备地址* WhichPage:从哪一页开始读取(1~32)* *Byte:保存读取数据缓存区的地址* ByteLen:需要读取数据长度*/
void AT24C02_ReadPageBytes(unsigned char Addr,unsigned char WhichPage,unsigned char *Byte,unsigned char ByteLen)
{unsigned char i = 0;I2C_Start();I2C_SendByte((Addr << 1) | 0);Receive_ACK();I2C_SendByte((WhichPage - 1) * 8); //将页转换为寄存器的初始地址Receive_ACK();I2C_Start(); I2C_SendByte((Addr << 1) | 1);Receive_ACK();for(i = 0; i<ByteLen; i++){Byte[i] = I2C_ReceiveByte();if(i < ByteLen - 1){I2C_SendACK();}else{I2C_SendNACK();}}I2C_Stop();
}
④AT24C02.h文件的代码如下:
#ifndef __AT24C2_H
#define __AT24C2_H
#include <REGX52.H> //包含51头文件,里面全是寄存器地址
#include "I2C.h"/* 向AT24C02写入1个字节的数据 */
void AT24C02_WriteByte(unsigned char Addr ,unsigned char InAddr,unsigned char Byte);
/* 向AT24C02读取1个字节的数据 */
unsigned char AT24C02_ReadByte(unsigned char Addr ,unsigned char InAddr);
/* 向AT24C02写入多个字节的数据 */
void AT24C02_WriteMultBytes(unsigned char Addr,unsigned char InAddr,unsigned char *Byte,unsigned char ByteLen);
/* 向AT24C02读取多个字节的数据 */
void AT24C02_ReadMultBytes(unsigned char Addr,unsigned char InAddr,unsigned char *Byte,unsigned char ByteLen);
/* 向AT24C02写入多页字节的数据 */
void AT24C02_WritePageBytes(unsigned char Addr,unsigned char WhichPage,unsigned char *Byte,unsigned char ByteLen);
/* 向AT24C02读取多页字节的数据 */
void AT24C02_ReadPageBytes(unsigned char Addr,unsigned char WhichPage,unsigned char *Byte,unsigned char ByteLen);#endif
⑤main.c文件的代码如下:
#include "LCD1602.h"
#include "AT24C02.h"
#include "Delay.h"
#include "Timer.h"unsigned char Data[] = {0x10,0x20,0x30,0x40,0x50,0x60,0x70,0x80,0x90,0xA0};
unsigned char Byte[10] = {0};void main(void)
{unsigned char i = 0;LCD_Init();Time0_Intrrupt_Init();AT24C02_WritePageBytes(0x50,1,Data,10); //从第一页开始写入10个字节的数据Delay_ms(10);AT24C02_ReadPageBytes(0x50, 1, Byte,10);//从第一页开始读取10个字节的数据for(i = 0; i<10; i++){if(i < 5){LCD_ShowHexNum(1, 1+2*i, Byte[i],2);}else{LCD_ShowHexNum(2, (1+2*i) - 10, Byte[i],2);}}while(1){}
}
时序分析如下:
3、项目:使用AT24C02记录上电次数
⑤main.c文件的代码如下:
#include "LCD1602.h"
#include "AT24C02.h"
#include "LCD1602.h"
#include "Delay.h"
#include "Timer.h"#define AT24C02_Address 0x50
#define AT24_RegsAddre 0x00
#define First_Flag 0xAA
#define Count_MAX 200
/** * 上电次数的实现,使用AT24C02的地址为0x00,0x01,0x02进行实现* 第一次上电时,向0x00地址写0xAA,然后将*/
void main(void)
{unsigned char Open_Cont[3] = {0};unsigned long OpenPower_Count = 0;LCD_Init();Time0_Intrrupt_Init();AT24C02_ReadMultBytes(AT24C02_Address, AT24_RegsAddre,Open_Cont,3);if(Open_Cont[0] != First_Flag) //表示第一次上电{Open_Cont[0] = First_Flag;Open_Cont[1] = 1;Open_Cont[2] = 0;}else //不是第一次上电{Open_Cont[1]++;if(Open_Cont[1] == Count_MAX) //逢200进1,一共能计40000次{Open_Cont[1] = 0;Open_Cont[2]++;}}AT24C02_WriteMultBytes(AT24C02_Address, AT24_RegsAddre, Open_Cont, 3);OpenPower_Count = Open_Cont[1] + Open_Cont[2] * Count_MAX;LCD_ShowNum(1,1,OpenPower_Count,6);while(1){}
}