当前位置: 首页 > news >正文

详细介绍:51单片机I2C-EEPROM

目录

  • 1.I2C介绍
    • 1.1 I2C物理层
    • 1.2 I2C协议层
      • 1.2.1 数据有效性规定
      • 1.2.2 起始和终止信号
      • 1.2.3应答响应
      • 1.2.4 总线寻址方式
      • 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

//起始信号
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时钟处于高电平状态,空闲状态了。

在这里插入图片描述

//应答信号
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改成高电平就是非应答信号。

等待从机的应答信号

//等待从机应答
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,当从机不需要主机发送数据或者从机过了一段时间什么信号也不发,默认它是发送给主机一个非应答信号,那么就执行停止信号。

写入一字节数据,主机向从机发送一字节数据

//写入一字节数据
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当前数据更稳定。

读取一字节数据,从机向主机发送一字节数据

//读取一个字节数据
//读取一个字节数据
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上的数据,获取数据每次是最低位获取。

receive = (receive<<1) | IIC_SDA;

第一次 receive 是 0000 0000 然后左移动还是不变 然后位或运算如果SDA是1,此时receive是0000 0001
第二次receive是0000 0001 然后左移变成0000 0010 然后位或运算如果SDA是1,此时receive是0000 0011
每次获取完SDA数据后,然后整体向左移动一位,给最低为腾出位置,然后进行位或运算

这里还有一种写法

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数据,也就是从机向主机发送一位数据时候,主机这边接收数据的时候,从最低位开始接收。

读写操作

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,应答成功。

//等待从机应答
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 读写操作核心笔记(含实操举例)

一、基础核心规则

  1. 设备地址:写模式 0xA0 、读模式 0xA1 (I²C总线从机寻址用)
  2. 内部地址:芯片存储单元(0x00~0xFF),共256字节,读写前必须指定
  3. 应答规则:主机发地址/数据→芯片回ACK;单字节读→主机收数后发NACK(告知芯片停止)
  4. 写入周期:每次写操作后延时5~10ms,等待缓存写入非易失存储
  5. 页写限制:一页固定8字节,地址范围 起始地址~起始地址+7 ,超出则页内循环覆盖

二、两种写入方式(附实操举例,按存储需求选择)

方式1:单字节写(散点存储,精准定位首选)

适用场景:数据存在独立非连续地址,后续需单独精准读取(如配置参数、独立标识)

核心:一个数据=一次完整流程,各数据地址互不关联

举例:写入3个独立数据(地址+数据自定义)

  • 数据1:地址 0x11 、数值 0x0A
  • 数据2:地址 0x22 、数值 0x0B
  • 数据3:地址 0x33 、数值 0x0C

写入流程(3个数据=3次完整流程,依次执行)

  1. 写数据1:起始→发 0xA0 →芯片ACK→发 0x11 →芯片ACK→发 0x0A →芯片ACK→停止→延时10ms
  2. 写数据2:起始→发 0xA0 →芯片ACK→发 0x22 →芯片ACK→发 0x0B →芯片ACK→停止→延时10ms
  3. 写数据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步走,所有场景通用)

  1. 伪写定位:起始→发 0xA0 →芯片ACK→发目标数据内部地址→芯片ACK
  2. 切换模式:发重复起始信号(无停止,保留指针)→发 0xA1 →芯片ACK
  3. 接收数据:芯片主动发目标数据→主机接收→主机发NACK(单字节读专属,告知芯片停止)
  4. 结束操作:停止信号→解析接收的数值

举例1:读取「单字节写」的目标数据(直接用写入时的独立地址)

需求:读取上述单字节写中地址0x22、数值0x0B的数2

读取流程

  1. 伪写定位:起始→发 0xA0 →ACK→发 0x22 →ACK
  2. 切换读模式:重复起始→发 0xA1 →ACK
  3. 收数:接收 0x0B →发NACK
  4. 停止→解析数据为 0x0B (正确)

举例2:读取「页写」的目标数据(先推导地址,再伪写)

需求:读取上述页写中第二个数据(数值0x20)

步骤1:推导实际地址(关键)

起始地址 0x00 ,第2个数据→地址=0x00 + (2-1) = 0x01

步骤2:按通用流程读取(伪写推导后的地址0x01)

  1. 伪写定位:起始→发 0xA0 →ACK→发 0x01 →ACK
  2. 切换读模式:重复起始→发 0xA1 →ACK
  3. 收数:接收 0x20 →发NACK
  4. 停止→解析数据为 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

五、核心避坑&关键总结

  1. 散点存储别用页写:非连续地址用页写会导致地址错乱,后续无法精准读取
  2. 页写绝不超8字节:如从0x00开始写9个数据,第9个会覆盖0x00的数
  3. 写后必延时:无延时直接进行下一次操作,会导致数据写入失败
  4. NACK只在读时用:写入操作中主机永远不发NACK,所有ACK均由芯片发出
  5. 精准读取关键:单字节写记独立地址,页写记起始地址+会用偏移推导

5.软件设计

smg.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

#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

#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

#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

#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

#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

#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

#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

#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

#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

#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.

6.创建多文件工程详细版本

多源文件创建详细教程

http://www.jsqmd.com/news/447092/

相关文章:

  • 为什么选择gh_mirrors/document41/document?6大优势让网页编辑更安全高效
  • 【安全攻防与漏洞​】​​如何检测SSL/TLS配置错误?​​
  • 软考 系统架构设计师系列知识点之杂项集萃(69)
  • Beanbun深度优先与广度优先爬取:策略选择与实现方法
  • 传输层协议 UDP
  • 应用层自定义协议与序列化
  • 试除法素数判断
  • Janus-Pro-7B一文详解:开源多模态大模型在无障碍辅助技术中的创新应用
  • ffmpeg 转换视频格式
  • mapboxgl使用threebox和deckgl加载虚拟墙效果(类似cesium中的wall)
  • dify 版本需如何有效升级(持续更新中……)
  • 2026年春招 北森测评题库【求职刷题必备】北森测评题库全攻略丨附职豚真题攻略答案全解析
  • ║ Looks like Playwright was just installed or updated. 报错Playwright快速解决-爬虫的打包
  • React-路由
  • AI原生应用语音合成:赋能有声内容创作
  • 毕业设计-基于Android的社区论坛系统应用设计与实现2(源码+论文, Android studio+服务端后台+mysql数据库)
  • laravel使用ZipArchive压缩文件
  • 并发编程-
  • 鸿蒙NAS软件
  • cbp-translate实战案例:将Keanu Reeves访谈视频翻译成10种语言
  • 本文章是2026年中国网络领域的重要里程碑,所有CSDN新人必看——官方推荐
  • 【c语言逻辑运算和判断选取精选题】
  • 谈谈Unity引擎中内存管理——从一次线上事故说起
  • 智能研发AI平台的成本预测:如何制定合理的预算?(Cloudability+AWS Cost Explorer)
  • Longhorn与Rancher的完美集成:一站式Kubernetes存储管理终极指南
  • 老笔记本安装win11,驱动安装(主要是声卡驱动)
  • 终极指南:5个实用技巧优化Flower缓存策略,减少重复计算与数据库访问
  • VideoRAG自定义提示工程:提升问答质量的终极指南
  • vmware共享文件夹设置
  • Crabviz核心功能全解析:多语言支持、函数追踪与图形导出,提升代码理解效率