51单片机I2C-EEPROM

目录

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 读写操作核心笔记(含实操举例)

一、基础核心规则

  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

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.

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

多源文件创建详细教程

相关推荐
代码游侠2 小时前
学习笔记——Linux字符设备驱动
linux·运维·arm开发·嵌入式硬件·学习·架构
来自晴朗的明天2 小时前
10、LM2904 单电源反向比例运算放大器电路
单片机·嵌入式硬件·硬件工程
jiang_changsheng2 小时前
MCP协议的核心架构基础
c语言·开发语言·c++·python·comfyui
1+α3 小时前
工业通讯中的“顶梁柱”——RS485科普
c语言·stm32·嵌入式硬件·网络协议
网易独家音乐人Mike Zhou3 小时前
【RealMCU】瑞昱官方LOG信息保存及解析,DebugAnalyzer自动化接收脚本(不需要用到ROM.trace文件)
单片机·mcu·物联网·自动化·嵌入式·iot·瑞昱
逐步前行3 小时前
STM32_芯片介绍
stm32·单片机·嵌入式硬件
晓13133 小时前
第三章 【C语言篇:结构化编程】 分支循环数组函数
c语言
Z9fish3 小时前
sse哈工大C语言编程练习22
c语言·开发语言·算法
听风吹雨yu3 小时前
STM32F407-LWIP-Onvif协议控制海康相机
stm32·嵌入式硬件·数码相机