目录
- 1.I2C介绍
-
- [1.1 I2C物理层](#1.1 I2C物理层)
- [1.2 I2C协议层](#1.2 I2C协议层)
-
- [1.2.1 数据有效性规定](#1.2.1 数据有效性规定)
- [1.2.2 起始和终止信号](#1.2.2 起始和终止信号)
- 1.2.3应答响应
- [1.2.4 总线寻址方式](#1.2.4 总线寻址方式)
- [1.2.5 数据传输](#1.2.5 数据传输)
- 2.AT24C02介绍
- 3.硬件设计
- 4.iic代码理解
- 5.软件设计
- 6.运行结果
- 6.创建多文件工程详细版本
1.I2C介绍
IIC它是一个总线,由飞利浦公司开发的两线串行总线,用于连接微控制器,以及外围设备,是微电子通信控制领域广泛采用的一种总线标准,它是同步通信的一种特殊形式,接口线少,控制方法简单,器件封装形式小,通信速率较高的一个优点。
IIC总线只有两根双向信号线,一根是时钟信号线(SCL),另外一根是数据信号线(SDA), 只有两个线,所以它占用管脚少,硬件使用简单,拓展性强,因此被广泛的使用到集成芯片中,像STM32,或增强型51芯片。
像现在有很多IIC接口设备,比如OLED屏幕,可以通过单片机的IIC接口跟设备进行连接,实现IIC通信,还有一些存储芯片EEPROM,它也使用的一些IIC接口,还有很多LCD的屏也有IIC接口,以及相应的一些传感器,激光传感器,等等,用的比较多的IIC接口,SPI接口,IIC它占据IO口管脚比较少,只有两根线,时钟线(SCL)和数据线(SDA),所以应用广泛。
1.1 I2C物理层

IIC总线它可以支持多设备连接,在总线上面可以挂载多个设备,MCU,DeviceA,DeviceB,DeviceC等等,在IIC可以看见接了两个上拉电阻,上拉电阻的作用是保证所有设备处在空闲状态下,始终处于高电平。
特点:
①支持多设备总线,总设备是多设备公用的线,支持多个设备主机以及多个设备从机
②IIC总线上面只使用了两条总线线路,一条是数据线(SDA),一条是时钟线(SCL),数据线是用来表示数据,时钟线是用来数据收发的同步。
③每一个连接总线的设备,都有一个独立的地址,主机可以利用地址来访问不同的设备。
IIC是支持多设备的连接的,假设IIC上面有一个主机MCU,有三个从机A,B,C,三个从机的设备的地址要不同,如果假设A的地址跟B的地址相同,那么MCU访问B的时候,那么依靠的就是设备地址,如果这个地址相同,那么MCU是访问A还是B呢,MCU主机就不知道了,所以要保证这些设备的地址是不同的,这样MCU可以根据器件地址,来访问对应的设备。
④总线是通过上拉电阻接的电源的,当IIC设备,ABC设备空闲的时候,会输出高阻态,当所有设备都处于空闲状态时候,输出都是高阻态,这时候要保证总线要保证一个稳定的电平,这时候依靠的是上拉电阻,将总线的电平拉高了,保证了其他设备空闲下有一个稳定的高电平。
⑤多个主机同时使用总线时,为了防止数据冲突,会利用总裁方式,决定由哪一个占用总线,这就是根据器件地址,从而识别哪一个设备进行访问。
⑥IIC它有三种传输模式,标准传输的速率是100kbit/s,快速传输模式,400kbit/s,高速传输模式,3.4Mbit/s,但目前IIC设备不支持高速模式,用的比较多的是标准传输模式和快速传输模式。
⑦连接到相同总线IIC器件数量,并不是无限的连接,受到总线的最大电容400PF限制,因为我们知道IIC可以连接多个设备多个主机,那么这里多个并不是指的是无限的连接,它有一个最大额度的,最大额度是受到400PF的电容限制,像我们连接个5,6个是没问题的,一般不会连接很多。
专业术语:
主机是启动数据传输并产生时钟信号的一个设备,比如说这里的MCU, 总线要进行访问这些设备的时候,由MCU主机来发送启动数据传输。
从机自然是被主机寻址的一个设备。
多主机是同时有多个,多于一个主机尝试去控制总线。
主模式是IIC支持的自动字节的一个计数模式,从而控制数据接收和发送。
从模式是发送和接收操作由IIC模块自动控制。
比如在我们这个主机读取设备的从机数据的时候,那么由从机的设备,自动返回这些数据到主机。
仲裁是指一个或多个主机同时尝试控制总线,但只允许其中一个控制总线并使传输数据不被破坏一个过程。
就是说在同一个时刻,只有一个设备主机去访问从机,不能说几个主机都在访问这个从机,只能说同一时刻,只能保证一个主机去访问,那么依靠的就是这个仲裁的一个机制。
同步是指两个或多个同步时钟信号的过程。
发送器和接收器,发送器是发送数据到总线器件,比如我们的主机要发送数据,接收器是指从总线接收数据的器件。
1.2 I2C协议层
1.2.1 数据有效性规定

在这张图中,当SCL是高电平的时候,必须保证SDA数据稳定,当SCL是低电平的时候,SDA数据才是可以允许变化的。
我们在发送数据的时候,当SCL时钟线为低电平的时候,就可以变化数据,等SCL为高电平的时候,数据就需要要求稳定。
每次数据传输的时候都是以字节为单位,一个字节等于8位比特位,它可以进行多字节发送,但是始终它是以字节为单位进行发送的。
1.2.2 起始和终止信号

SCL为高电平的时候,SDA由高电平变化成低电平,这个时候就是起始信号,SCL为高电平的时候,SDA由低电平变成高电平,这时候是终止信号。
起始信号于终止信号作用:
当我们进行IIC通信的时候,我们主机首先发送一个起始信号,来进行一个数据传输,所以当SCL为高电平的时候,SDA由高电平变成低电平,这是起始信号,起始信号和终止信号都是由主机来发出的,在起始信号产生之后总线就处于一个占用状态,在终止信号发出之后,总线就处于空闲状态。
1.2.3应答响应

主机发送数据到从机:
每当器件传输一个字节之后,后面紧跟一个校验位,这个校验位是接收端通过控制,SDA这个数据线来实现的,来提醒发送端的数据,我这边已经接收完成,数据可以继续进行,那么这个校验位其实就是数据和地址传输过程当中的一个响应,响应也包括:应答(ACK)、非应答(NACL),作为数据接收端的时候,当这个设备,无论是主机还是从机,接收到的IIC传输一个字节的数据或者地址之后,如果希望对方继续发送数据,就需要向对方应答信号(ACK),发送了ACK应答信号之后,发送方就会继续发送下一个数据,那么这个ACK信号是一个低电平的信号,如果接收端希望结束这个数据的传输,就需要向对方发送一个非应答信号(NACK),那么发送端接收到这个信号之后,发送方就会立刻停止,产生一个停止信号,结束我们这一次传输,NACK是一个特定的高脉冲的特定信号。
当我们主机时钟信号线在一直的切换,那么主机来了一个发送端的起始信号,开始进行数据传输,SCL在高电平的时候,SDA稳定,SCL低电平的时候,SDA可以变换,那么传输完成一个字节,也就是八位数据,传输完成一个字节之后,那么从机接收到数据,是想让主机继续发送,还是结束这次发送,那么可以根据从机响应,也就是ACL或者是NACL,如果从机发送了一个非应答也就是特定的高脉冲,那么主机接收到了,主机就会发送一个停止信号,结束我们的传输,如果从机发送了一个应答信号,那么主机接收到了应答信号,还会继续发送下一个数据,每一个字节必须保证八位的一个长度,数据传输的时候先传输高位,MSB是最高位,从高位往低位进行传输,每一个传输字节后面都必须跟一个应答位。
由于某种原因,从机不对主机寻地址信号应答的时候,那么它必须将数据线,变成高电平,设置高,而由主机产生一个终止信号,来结束这个总线的数据传输,因为由数据线置高,相当于从机发送了一个非应答信号,那么主机呢就会产生一个终止信号,来结束这一次数据传输。
如果从机对主机,进行应答了,那么在数据传输一段时间之后,无法继续接收更多的数据的时候,那么从机可以通过,对无法接收的第一个数据字节的非应答,来通知主机,主机则发出终止信号,结束数据传输,当主机接收了数据时候,收到最后一个数据的字节之后,必须向从机发送一个结束,传输的一个信号,这个信号是由从机对非应答来实现的,从机就会释放SDA,允许主机产生终止信号,结束这一次传输。
在这些信号当中,起始信号必须要有的,对于终止信号,也就是应答信号和非应答信号可以不要的。
通过这个图中,始终一个有一个主机,一个从机,主机发送数据,首先产生一个起始信号,产生起始信号之后,那么主机向从机发送数据,如果从机接收到这个数据之后,如果从机要想主机继续发送数据,从机需要发送一个应答信号给主机,如果不需要主机发送数据,从机需要发送一个非应答信号,主机收到非应答之后,就会产生一个终止信号,就会结束这一次IIC通信了。
从机反数据到主机:
开始时候,主机产生一个起始信号,数据是从机发送到主机的,主机接收到这个数据之后,主机也会产生一个应答,或者非应答,如果主机产生了一个应答,那么主机还需要向从机这里读取数据过来,从机接收到主机的应答信号,就会继续发送数据,当主机想结束这段数据,主机发送一个非应答信号,那么从机接收到了主机的非应答信号, 就会停止数据的发送,主机就会产生一个终止信号,结束IIC通信。
1.2.4 总线寻址方式

总线寻址分为两种:第一种是7位,另外一种是10位。
采用7位寻址,上图可以看到,从第一到第七,采用从机的地址,第零位是传输数据方向控制位,如果这一位为0,那么代表的是w,向从机写数据,如果这一位是1,代表R,向从机读取数据,所以通过第零位可以控制读/写,也就是数据的传输方向。
采用十位寻址,与七位寻址是兼容,可以结合使用,十位寻址不会影响已有的七位寻址,有七位和十位地址的器件,可以连接到相同的IIC总线。
当主机发送地址之后,那么主机上面的每一个器件都将头七位,也就是第一位到第七位,与它自己的地址,进行比对。
例如IIC上面有给主机A,从机B和从机C,当主机发送了一个地址,那么B和C从机根据主机发送的地址,第一位到第七位,自身的地址进行比较,如果一样,那么器件判断被主机寻址了,假设主机A发送了地址0X01过来,从机B地址也是0X01,从机C地址是0X02,那么会跟主机主机上的地址比对,发现从机B与主机地址一样的,与从机C地址不一样,主机就会访问从机B,不会访问从机C,至于从机接收器,还是从机发送器,就是数据的传输的方向由第零位控制,从机的地址由固定部分 + 可编程部分组成,在一个系统中,可能希望接入多个相同的从机,那么从机的地址,可编程部分,决定了设备的最大数目,比如一个从机地址7位,它是由四位是固定,3位是可以编程,所以通过,可编程的3位去寻址2的3次方相同的器件,它的地址可以是000,001,010,等等,有八种连接,同时可以挂载八个相同的器件接入到IIC总线系统上面,
1.2.5 数据传输

黑色的主机,白色表示从机
写0:主机向从机发送数据
读1:从机向主机发送数据
a图
主机向从机发送数据,数据传输方向在整个传送过程中不变。
首先由主机产生起始信号,然后主机发送从机的地址是七位的,最后面一位是方向,写,然后从机向主机发送应答信号还是非应答信号,如果发送的是应答,主机继续向从机发送,如果是非应答,主机不再向从机发送,而这里从机第一次回复应答A,然后主机发送数据,然后从机第二次应答A,主机继续发送数据,然后从机第三次发送非应答,主机停止发送,紧跟着是一个终止信号。
b图
主机在第一个字节之后,立刻向从机读取数据。
首先主机依然产生起始信号,主机发送从机地址,最后一位是读,紧跟着从机发送一个应答信号,然后主机读取从机发送过来的数据,紧跟着主机发送一个应答,还需要从机继续发送,紧跟着从机继续发送一个数据给主机读取,此时主机回复一个非应答信号,最后主机发送一个停止信号,终止信号。
c图
在传送过程中,当需要改变传送方向时,起始信号和从机地址都被重复产生一次,但两次读/写方向位正好相反
首先主机产生起始信号,主机向从机发送一个字节的地址,第八位是写,接着从机回复一个应答,主机发送数据,从机回复一个非应答或者应答,然后起始信号又发送一次,主机向从机发送一个字节地址,最后一位是读,从机回复应答,从机向主机发送数据,主机回复非应答信号,主机后面产生一个终止信号。
通常情况下C图用的比较多。
2.AT24C02介绍

AT24C02是AT24C系列的一个型号,那么它还有AT24C01、02、04、06、08、16、256等等,这类芯片是EEPROM芯片,也就是它里面保存的数据掉电不会丢失,所以我们可以吧一些重要的数据可以存放在AT24C02当中,那么这种芯片它是个1k、2k、4k、8k、16kbit, 所以它1k、2k根据芯片的型号,也就是说,01是1k,02是2k,04是4k,以此类推,它是一个串行的CMOS的一个器件,它内部含有128,256,512,1024,2048等等这些字节, 位跟字节转换,一字节等于8位,像我们这个AT24C02是一个256字节容量的芯片,可以在AT24C02里面存放256个字节数据,超过256字节数据就不行了,那么AT24C系列的有一个像C01,有一个八字节 ,页写缓冲器。
对于ATC02、04、08、16等等,它有一个16字节写缓冲器,那么这个器件是通过IIC总线操作的,有一个专门的写保护功能,这个芯片具备有IIC通信的接口。
AT24C02管脚图
那么它的SCL和SDA是IIC总线的接口,在这个芯片中,存放的数据掉电的情况下都不会丢失,通常存放一些比较重要的数据。
1、2、2(A0、A1、A2):地址输入引脚
前面介绍协议层IIC的时候,它有7个地址,有4个是固定,有3个是可编程的,就是对应的1、2、3引脚,如果吧这三个引脚接了000,这是一个地址,如果在IIC又挂载了一个AT24C02,现在吧1、2、3引脚接成001,这个时候地址不一样的,它可以挂载8个这样相同的芯片,到总线上面,主机访问的就是1、2、3引脚的地址。

高四位是固定的四位,1010,后面的三位是可编程的对应的A0、A1、A2,设置000,最后一位是确认数据的传输方向,读/写,如果这一位是写的时候,设置成0,那么最终数据是1010 0000换算成16进制就是0XA0,设置0XA0表示向从机设备写入数据,如果最后一位是1的时候,就是读取数据,最终数据是1010 0001换算成16进制就是0XA1,设置0XA1向从机设备读取数据。
4引脚VSS:接地
8引脚VCC:接电源
7引脚:写保护
如果这个引脚接的GND的时候,是允许数据读写操作的,如果这个引脚接了VCC的那么它具有写保护的,只能读不能写,当然我们希望具备读和写的操作,所以接在GND上面。
5,6:IIC时钟,数据线和时钟线,接在SDA和SCL总线上面。
在这两个引脚上,通常会有一个上拉电阻,上拉电阻大概是10k,或者4.7k到10k范围之内,通过这个上拉电阻,可以保证在空闲状态下也是一个稳定的电平信号。

在这张图中需要知道,SCL和SDA由低电平变高电平,或者由高电平变成低电平,这么一个时间,需要在对应的芯片数据手册中去查询,对应的一个延时时间数据,不同的生产厂家,可能在延时时间这块可能不同,需要在芯片数据手册去查询。
3.硬件设计

4.iic代码理解
实现的功能是:系统运行时,数码管右3位显示0,按K1键将数据写入到EEPROM内保存,按K2键读取EEPROM
内保存的数据,按K3键显示数据加1,按K4键显示数据清零,最大能写入的数据是255。

iic.c
c
//起始信号
void iic_start(void) {
IIC_SCL = 1;
IIC_SDA = 1;
delay_10us(10);
IIC_SDA = 0;
delay_10us(10);
IIC_SCL = 0; //总线处于占用状态
}
//停止信号
void iic_stop(void) {
IIC_SCL = 1;
IIC_SDA = 0;
delay_10us(10);
IIC_SDA = 1;
delay_10us(10);
}
起始信号,开始时候,SCL为高电平的时候,SDA由高变低,期间需要延时一段时间,纳秒级别的,然后过了一段时间SCL变成低电平,总线处于占用状态
终止信号,当SCL为高电平时候,SDA由低电平变成高电平,期间也需要一段时间延时,延时也是纳秒级别,然后SCL时钟处于高电平状态,空闲状态了。

c
//应答信号
void iic_ack(void) {
IIC_SCL = 0;
IIC_SDA = 0;
delay_10us(10);
IIC_SCL = 1;
delay_10us(10);
IIC_SCL = 0;
}
//非答信号
void iic_nack(void) {
IIC_SCL = 0;
IIC_SDA = 1;
delay_10us(10);
IIC_SCL = 1;
delay_10us(10);
IIC_SCL = 0;
}
SCL是低电平的时候,SDA是可变的,SCL是高电平的时候,SDA是稳定的
应答信号,开始SCL是低电平,SDA是可变的,SDA设置0,表示应答信号,然后延时一段时间,纳秒级别,然后SCL设置1,延时一段时间,变成低电平,等待下一次主机的发送
非应答信号,跟应答信号一样,就是将SDA改成高电平就是非应答信号。
等待从机的应答信号
c
//等待从机应答
u8 iic_wait_ack(void) {
u8 time_temp = 0;
IIC_SCL = 1;
delay_10us(10);
while (IIC_SDA) { //等待从机的应答信号
if (time_temp > 100) {
iic_stop(); //停止信号
return 1;
}
time_temp++;
}
IIC_SCL = 0;
return 0;
}
当主机发送了一段数据,需要等待从机返回应答还是非应答,当从机需要主机继续发送数据,则返回应答信号,也就是SDA为0,当从机不需要主机发送数据或者从机过了一段时间什么信号也不发,默认它是发送给主机一个非应答信号,那么就执行停止信号。
写入一字节数据,主机向从机发送一字节数据
c
//写入一字节数据
void iic_write_byte(u8 dat) {
u8 i = 0;
IIC_SCL = 0;
delay_10us(10);
for (i = 0; i < 8; i++) {
if (dat & 0x80) //最高位进行 与 运算
IIC_SDA = 1;
else
IIC_SDA = 0;
dat <<= 1;
IIC_SCL = 1;
delay_10us(10);
IIC_SCL = 0;
delay_10us(10);
}
}
首先将SCL拉低,然后一段时间,SDA是可变状态,延时稳定一段时间,然后循环八次,每次通过dat & 0x80获取最高位,如果是1就设置SDA为1,如果是0设置SDA为0,然后dat变成,将原来dat每一个数据向左移动一位,为准备获取次高位做准备,然后SCL拉高电平,稳定下,SDA不可以变化,然后SCL拉低电平,此时SDA可以变化,延时一段时间,让SDA当前数据更稳定。
读取一字节数据,从机向主机发送一字节数据
c
//读取一个字节数据
//读取一个字节数据
u8 iic_read_byte(u8 ack) {
u8 i = 0, receive = 0;
IIC_SCL = 0;
delay_10us(10);
for (i = 0; i < 8; i++) {
IIC_SCL = 1;
delay_10us(10);
receive = (receive<<1) | IIC_SDA;
IIC_SCL = 0;
delay_10us(10);
}
if (!ack)
iic_nack();
else
iic_ack();
return receive;
}
开始让SCL拉低,延时一段时间,SDA此时可以变化,通过循环八次,每次获取一个SDA上的数据,获取数据每次是最低位获取。
c
receive = (receive<<1) | IIC_SDA;
第一次 receive 是 0000 0000 然后左移动还是不变 然后位或运算如果SDA是1,此时receive是0000 0001
第二次receive是0000 0001 然后左移变成0000 0010 然后位或运算如果SDA是1,此时receive是0000 0011
每次获取完SDA数据后,然后整体向左移动一位,给最低为腾出位置,然后进行位或运算
这里还有一种写法
c
receive <<=1;
if (IIC_SDA) receive++;
这种写法,每次receive向左移动一位,然后如果当前SDA是1,则receive最低位+1
第一次 receive 是 0000 0000 然后左移动还是不变 然后如果SDA是1,然后自增运算,此时receive是0000 0001
第二次receive是0000 0001 然后左移变成0000 0010 然后如果SDA是0,此时receive是0000 0010
第三次receive是0000 0010 然后左移动变成0000 0100 然后如果SDA是1,然后自增运算,此时receive是0000 0101
需要注意的是,写一字节数据的时候,每次写入最高位数据,也就是主机向从机发送一个字节八位数据的时候,一位一位的发,是从最高位往最低位写入。
读取一字节数据的时候,每次读取一位SDA数据,也就是从机向主机发送一位数据时候,主机这边接收数据的时候,从最低位开始接收。
读写操作
c
void at24c02_write_one_byte(u8 addr, u8 dat) {
iic_start();
iic_write_byte(0xA0);
iic_wait_ack();
iic_write_byte(addr);
iic_wait_ack();
iic_write_byte(dat);
iic_wait_ack();
iic_stop();
delay_ms(10);
}
u8 at24c02_read_one_byte(u8 addr) {
u8 temp = 0;
iic_start();
iic_write_byte(0xA0);
iic_wait_ack();
iic_write_byte(addr);
iic_wait_ack();
iic_start();
iic_write_byte(0xA1);
iic_wait_ack();
temp = iic_read_byte(0);
iic_stop();
return temp;
}
1.写入数据格式是什么?
因为在c02中,存储的每一个数据都有一个地址,所以在写入一个数据之前需要先写入一个地址。
写的操作格式是:
起始信号->写C02地址(找到哪一个从机)->应答信号->写一个地址(第一个数据地址)->应答信号->写一个数据->非应答信号->停止信号->延时10ms
2.读取数据的格式是什么?读的操作格式:
起始信号->写c02地址并且最后一位设置0,写入方式0XA0(找到哪一个从机)->应答信号->写入数据地址(用来查找读取到的数据)->应答信号->起始信号->写入c02地址并且最后一位设置1,读取方式0XA1(找到哪一个从机)->应答信号->开始读取->停止信号
3.为什么读操作的时候还需要将写操作还要做一遍?因为需要定位到读取哪一个数据,例如我写入的时候,有3个数据,甚至5个数据,但是我想读取第二个数据,第四个数据,读取指定位置的数据,而这个写的操作就是帮助我们去定位我们要读取的数据位置。
4.什么是单字节写入?例如:
数据1:地址 0x11 、数值 0x0A
数据2:地址 0x22 、数值 0x0B
数据3:地址 0x33 、数值 0x0C
写入流程(3个数据=3次完整流程,依次执行)
写数据1:起始→发 0xA0 →芯片ACK→发 0x11 →芯片ACK→发 0x0A →芯片ACK→停止→延时10ms
写数据2:起始→发 0xA0 →芯片ACK→发 0x22 →芯片ACK→发 0x0B →芯片ACK→停止→延时10ms
写数据3:起始→发 0xA0 →芯片ACK→发 0x33 →芯片ACK→发 0x0C →芯片ACK→停止→延时10ms
通过这种每次写入单个数据,每次写入一个数据之前,都先写入一个地址
5.什么是页字节写入?一次起始连续写≤8字节
例如有8个数据,0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88,发送起始地址是0x00,写入流程
流程:起始→发 0xA0(ch340地址) →ACK→发 0x00(起始地址) →ACK→发0x11→ACK→发0x22→ACK→发0x33→ACK→发0x44→ACK→发0x55→ACK→发0x66→ACK→发0x77→ACK→发0x88→ACK→停止→延时10ms
对应的地址从0x00开始到0x07,第N个数据的地址是(起始地址 + N-1),第8个数据的地址 = 起始地址0x00 + 当前数据8 - 1 = 0x07,第7个数据地址是0x06.
如果写入9个数据呢?那么第9个数据就将第一个数据覆盖,一页最多只能写8个数据,并且这些数据的地址是起始地址开始算,起始地址+偏移量,就是访问到的第几个数据
6.关于等待应答函数的理解这个是由硬件自动完成的,当我发送完一个字节数据后,然后SDA会自动拉低电平(应答),由硬件自动完成的,当我发送8位数据后,硬件将SDA自动设置0应答大概需要10个us级别,所以在读取操作的时候,发送0XA1后,本来在第八个数据之后SDA是1的,然后经过了10个us后,SDA变成0,所以后面执行等待SDA应答时候,下面这个代码这样写,开始SCL设置1,延时10个us,此时SDA变成0了,while不成立,最后拉低SCL,应答成功。
c
//等待从机应答
u8 iic_wait_ack(void) {
u8 time_temp = 0;
IIC_SCL = 1;
delay_10us(10);
while (IIC_SDA) { //等待从机的应答信号
if (time_temp > 100) {
iic_stop(); //停止信号
return 1;
}
time_temp++;
}
IIC_SCL = 0;
return 0;
}
7.SCL是1的时候,SDA处于稳定状态,数据不可变,SCL是0的时候,SDA处于可变状态。

8.为什么写入设备地址是A0和A1
因为C02前面4位是固定的1010后三位是A2,A1,A0而在电路图中全部接地,所以这3位是0,最后一位是读写位,设置0写,设置1读,还有就是A0,A1,A2可以从000一直到111,八种状态,那么可以对8个c02通信,通过接地和接vcc控制A0,A1,A2地址状态,从而每次读写操作前,先写入通信设备的地址,因为这个C02前四位是固定是1010后三位是0,读的是1,写的是0,所以如果是写的话1010 0000就是A0,如果是读的话1010 0001就是A1,所以每次对A0写入设备地址是写,每次对A1写入设备地址是读,通过这个地址可以查找到对哪个C02通信。
9.写入时候,从高位到低位,一位一位的写入,而读取的时候是从低位到高位,一位一位的读。
AT24C02 读写操作核心笔记(含实操举例)
一、基础核心规则
- 设备地址:写模式 0xA0 、读模式 0xA1 (I²C总线从机寻址用)
- 内部地址:芯片存储单元(0x00~0xFF),共256字节,读写前必须指定
- 应答规则:主机发地址/数据→芯片回ACK;单字节读→主机收数后发NACK(告知芯片停止)
- 写入周期:每次写操作后延时5~10ms,等待缓存写入非易失存储
- 页写限制:一页固定8字节,地址范围 起始地址~起始地址+7 ,超出则页内循环覆盖
二、两种写入方式(附实操举例,按存储需求选择)
方式1:单字节写(散点存储,精准定位首选)
适用场景:数据存在独立非连续地址,后续需单独精准读取(如配置参数、独立标识)
核心:一个数据=一次完整流程,各数据地址互不关联
举例:写入3个独立数据(地址+数据自定义)
- 数据1:地址 0x11 、数值 0x0A
- 数据2:地址 0x22 、数值 0x0B
- 数据3:地址 0x33 、数值 0x0C
写入流程(3个数据=3次完整流程,依次执行)
- 写数据1:起始→发 0xA0 →芯片ACK→发 0x11 →芯片ACK→发 0x0A →芯片ACK→停止→延时10ms
- 写数据2:起始→发 0xA0 →芯片ACK→发 0x22 →芯片ACK→发 0x0B →芯片ACK→停止→延时10ms
- 写数据3:起始→发 0xA0 →芯片ACK→发 0x33 →芯片ACK→发 0x0C →芯片ACK→停止→延时10ms
方式2:页写(连续存储,批量写入高效)
适用场景:数据需存在连续地址,批量存储(如传感器采集数据、连续日志)
核心:一次起始连续写≤8字节,地址自动+1,需记录起始地址(后续读取靠它推导)
举例:页写3个连续数据(起始地址自定义,选0x00)
- 起始地址: 0x00
- 数据1:地址 0x00 (起始地址)、数值 0x10
- 数据2:地址 0x01 (0x00+1)、数值 0x20
- 数据3:地址 0x02 (0x00+2)、数值 0x30
写入流程(一次完整流程,连续发数据)
起始→发 0xA0 →芯片ACK→发 0x00 (起始地址)→芯片ACK→发 0x10 →ACK→发 0x20 →ACK→发 0x30 →ACK→停止→延时10ms
地址推导公式:第N个数据地址 = 起始地址 + (N-1)(N从1开始)
三、读取操作(通用流程,附两种写入方式的读取举例)
核心前提(必记):无论哪种方式写入,读取前必须做伪写操作(纯定位、不写数据),将芯片内部地址指针指向目标数据地址,无伪写则读取结果随机
通用单字节读流程(4步走,所有场景通用)
- 伪写定位:起始→发 0xA0 →芯片ACK→发目标数据内部地址→芯片ACK
- 切换模式:发重复起始信号(无停止,保留指针)→发 0xA1 →芯片ACK
- 接收数据:芯片主动发目标数据→主机接收→主机发NACK(单字节读专属,告知芯片停止)
- 结束操作:停止信号→解析接收的数值
举例1:读取「单字节写」的目标数据(直接用写入时的独立地址)
需求:读取上述单字节写中地址0x22、数值0x0B的数2
读取流程
- 伪写定位:起始→发 0xA0 →ACK→发 0x22 →ACK
- 切换读模式:重复起始→发 0xA1 →ACK
- 收数:接收 0x0B →发NACK
- 停止→解析数据为 0x0B (正确)
举例2:读取「页写」的目标数据(先推导地址,再伪写)
需求:读取上述页写中第二个数据(数值0x20)
步骤1:推导实际地址(关键)
起始地址 0x00 ,第2个数据→地址=0x00 + (2-1) = 0x01
步骤2:按通用流程读取(伪写推导后的地址0x01)
- 伪写定位:起始→发 0xA0 →ACK→发 0x01 →ACK
- 切换读模式:重复起始→发 0xA1 →ACK
- 收数:接收 0x20 →发NACK
- 停止→解析数据为 0x20 (正确)
四、混合读写举例(实战高频场景)
需求:先页写4个连续数据(起始0x10,数值0x01~0x04),再单字节写1个独立数据(地址0x50,数值0x88),最后读取页写的第3个数据+独立数据
步骤1:页写4个连续数据(起始0x10)
- 地址:0x10(0x01)、0x11(0x02)、0x12(0x03)、0x13(0x04)
- 流程:起始→0xA0→ACK→0x10→ACK→0x01→ACK→0x02→ACK→0x03→ACK→0x04→ACK→停止→延时10ms
步骤2:单字节写独立数据(地址0x50,数值0x88)
- 流程:起始→0xA0→ACK→0x50→ACK→0x88→ACK→停止→延时10ms
步骤3:读取页写第3个数据(0x03)
- 地址推导:0x10 + (3-1) = 0x12 → 按通用流程伪写0x12,读取结果0x03
步骤4:读取独立数据(0x88)
- 直接伪写0x50 → 按通用流程读取,结果0x88
五、核心避坑&关键总结
- 散点存储别用页写:非连续地址用页写会导致地址错乱,后续无法精准读取
- 页写绝不超8字节:如从0x00开始写9个数据,第9个会覆盖0x00的数
- 写后必延时:无延时直接进行下一次操作,会导致数据写入失败
- NACK只在读时用:写入操作中主机永远不发NACK,所有ACK均由芯片发出
- 精准读取关键:单字节写记独立地址,页写记起始地址+会用偏移推导
5.软件设计
smg.c
c
#include "smg.h"
u8 gsmg_code[17] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,
0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71}; //共阴数码管0-F
void smg_display(u8 dat[], u8 pos) {
u8 i = 0;
u8 pos_temp = pos-1;
for (i = pos_temp; i < 8; i++)
{
switch(i) {
case 0: LSA = 0; LSB = 0; LSC = 0; break;
case 1: LSA = 1; LSB = 0; LSC = 0; break;
case 2: LSA = 0; LSB = 1; LSC = 0; break;
case 3: LSA = 1; LSB = 1; LSC = 0; break;
case 4: LSA = 0; LSB = 0; LSC = 1; break;
case 5: LSA = 1; LSB = 0; LSC = 1; break;
case 6: LSA = 0; LSB = 1; LSC = 1; break;
case 7: LSA = 1; LSB = 1; LSC = 1; break;
}
SMG_A_DP_POST = gsmg_code[dat[i - pos_temp]];
delay_10us(100);
SMG_A_DP_POST = 0X00;//消影
}
}
smg.h
c
#ifndef _smg_H
#define _smg_H
#include "public.h"
#define SMG_A_DP_POST P0
sbit LSA = P2^2;
sbit LSB = P2^3;
sbit LSC = P2^4;
extern u8 gsmg_code[17];
void smg_display(u8 dat[], u8 pos);
#endif
key.c
c
#include "key.h"
u8 key_scan(u8 mode) {
static u8 key = 1; //单次按下
if (mode) key = 1; //连续按下
if (key == 1 && (KEY1 == 0 || KEY2 == 0 || KEY3 == 0 || KEY4 == 0)) {
delay_10us(1000);
key = 0;
if (KEY1 == 0)
return KEY1_PRESS;
else if (KEY2 == 0)
return KEY2_PRESS;
else if (KEY3 == 0)
return KEY3_PRESS;
else if (KEY4 == 0)
return KEY4_PRESS;
}
else if (KEY1 == 1 && KEY2 == 1 && KEY3 == 1 && KEY4 == 1) {
key = 1;
}
return KEY0_UNPRESS;
}
key.h
c
#ifndef _key_H
#define _key_H
#include "public.h"
sbit KEY1 = P3^0;
sbit KEY2 = P3^1;
sbit KEY3 = P3^2;
sbit KEY4 = P3^3;
#define KEY1_PRESS 1
#define KEY2_PRESS 2
#define KEY3_PRESS 3
#define KEY4_PRESS 4
#define KEY0_UNPRESS 0
u8 key_scan(u8 mode);
#endif
iic.c
c
#include "iic.h"
//起始信号
void iic_start(void) {
IIC_SCL = 1;
IIC_SDA = 1;
delay_10us(10);
IIC_SDA = 0;
delay_10us(10);
IIC_SCL = 0; //总线处于占用状态
}
//停止信号
void iic_stop(void) {
IIC_SCL = 1;
IIC_SDA = 0;
delay_10us(10);
IIC_SDA = 1;
delay_10us(10);
}
//应答信号
void iic_ack(void) {
IIC_SCL = 0;
IIC_SDA = 0;
delay_10us(10);
IIC_SCL = 1;
delay_10us(10);
IIC_SCL = 0;
}
//非答信号
void iic_nack(void) {
IIC_SCL = 0;
IIC_SDA = 1;
delay_10us(10);
IIC_SCL = 1;
delay_10us(10);
IIC_SCL = 0;
}
//等待从机应答
u8 iic_wait_ack(void) {
u8 time_temp = 0;
IIC_SCL = 1;
delay_10us(10);
while (IIC_SDA) { //等待从机的应答信号
if (time_temp > 100) {
iic_stop(); //停止信号
return 1;
}
time_temp++;
}
IIC_SCL = 0;
return 0;
}
//写入一字节数据
void iic_write_byte(u8 dat) {
u8 i = 0;
IIC_SCL = 0;
delay_10us(10);
for (i = 0; i < 8; i++) {
if (dat & 0x80) //最高位进行 与 运算
IIC_SDA = 1;
else
IIC_SDA = 0;
dat <<= 1;
IIC_SCL = 1;
delay_10us(10);
IIC_SCL = 0;
delay_10us(10);
}
}
//读取一个字节数据
u8 iic_read_byte(u8 ack) {
u8 i = 0, receive = 0;
IIC_SCL = 0;
delay_10us(10);
for (i = 0; i < 8; i++) {
IIC_SCL = 1;
delay_10us(10);
receive = (receive<<1) | IIC_SDA;
IIC_SCL = 0;
delay_10us(10);
}
if (!ack)
iic_nack();
else
iic_ack();
return receive;
}
iic.h
c
#ifndef _iic_H
#define _iic_H
#include "public.h"
sbit IIC_SDA = P2^0;
sbit IIC_SCL = P2^1;
//起始信号
void iic_start(void);
//停止信号
void iic_stop(void);
//应答信号
void iic_ack(void);
//非答信号
void iic_nack(void);
//等待从机应答
u8 iic_wait_ack(void);
//写入一字节数据
void iic_write_byte(u8 dat);
//读取一个字节数据
u8 iic_read_byte(u8 ack);
#endif
24c02.c
c
#include "24c02.h"
void at24c02_write_one_byte(u8 addr, u8 dat) {
iic_start();
iic_write_byte(0xA0);
iic_wait_ack();
iic_write_byte(addr);
iic_wait_ack();
iic_write_byte(dat);
iic_wait_ack();
iic_stop();
delay_ms(10);
}
u8 at24c02_read_one_byte(u8 addr) {
u8 temp = 0;
iic_start();
iic_write_byte(0xA0);
iic_wait_ack();
iic_write_byte(addr);
iic_wait_ack();
iic_start();
iic_write_byte(0xA1);
iic_wait_ack();
temp = iic_read_byte(0);
iic_stop();
return temp;
}
24c02.h
c
#ifndef _24c02_H
#define _24c02_H
#include "public.h"
#include "iic.h"
//写一字节数据
void at24c02_write_one_byte(u8 addr, u8 dat);
//读一字节数据
u8 at24c02_read_one_byte(u8 addr);
#endif
public.c
c
#include "public.h"
void delay_10us(u16 us) {
while(us--);
}
void delay_ms(u16 ms)
{
u16 i,j;
for(i=ms;i>0;i--)
for(j=110;j>0;j--);
}
public.h
c
#ifndef _public_H
#define _public_H
#include "reg52.h"
typedef unsigned int u16;
typedef unsigned char u8;
void delay_10us(u16 us);
void delay_ms(u16 ms);
#endif
main.c
c
#include "public.h"
#include "key.h"
#include "smg.h"
#include "24c02.h"
#define EEPROM_ADDRESS 0
void main() {
u8 key_temp = 0;
u8 save_value = 0;
u8 save_buf[3];
while(1) {
key_temp = key_scan(0);
if (key_temp == KEY1_PRESS) {
at24c02_write_one_byte(EEPROM_ADDRESS, save_value); //写入指定地址的数据
}
else if (key_temp == KEY2_PRESS) {
save_value = at24c02_read_one_byte(EEPROM_ADDRESS);
}
else if (key_temp == KEY3_PRESS) {
save_value++;
if (save_value >= 255) save_value = 255;
}
else if (key_temp == KEY4_PRESS) {
save_value = 0;
}
save_buf[0] = save_value / 100; //百位
save_buf[1] = save_value / 10 % 10; //十位
save_buf[2] = save_value % 10; //个位
smg_display(save_buf, 6);
}
}
文件夹设计


6.运行结果
录制_2026_02_05_10_51_45_453
功能描述:
按键1控制,写入到C02中,按键2控制,读取C40中数据,按键3控制,数码管值+1,按键4控制数码管值清0
当按键3一直按,比如按到11,然后按下按键1,吧11保存到c02中,然后关闭电源,重新打开电源,直接按下按键2,直接从c02去读取上一次保存的数据,然后显示在数码管中,按键4按下,将数码管清0.