1 IIC 通信协议
2 24C02 操作
3 实验程序讲解

IIC协议是我们的一种硬件协议方式,也有软件的IIC,接下来讲一下IIC协议是怎样的,以24C02为例给大家讲一下单片机是如何通过IIC和24C02通信

IIC通信

I2C (IIC,Inter-Integrated Circuit),两线式串行总线,由 PHILIPS 公司开发用于连接微控制器及其外围设备。
它是由数据线 SDA 和时钟 SCL 构成的串行总线,可发送和接收数据。在 CPU 与被控 IC 之间、IC 与 IC 之间进行双向传送,高速 IIC 总线一般可达 400kbps 以上。

IIC只有一条数据线,所以IIC在同一时刻只能往一个方向发送,由于他有时钟线,所以我们可以分析出IIC 是半双工同步通信方式
    回顾一下:带时钟线和不带时钟线是两种方式,一种是同步一种是异步,带时钟线(CLK)的是同步,不带时钟线(无CLK)的是异步

三种通信方式:单工,半双工,全双工

多主机I2C总线系统结构

  链式连接方式

I2C总线上面要挂载一个4.7k的上拉电阻,I2C的地址为0XFF,所以说他的地址最大值为255(地址0不算),所以I2C最多能挂载255个从机,这些从机可以不同的设备类型(24C02,OLED,NFC)

I2C 通常采用链式连接方式,不可以使用环形连接和星形连接
I2C 布线要遵循等距、等宽、等长原则

环形连接是怎么样的呢?

环形连接是我们的总线又回到我们的stm32,导致stm的总线收尾相连,这种方式叫环形连接,在我们的板子上如果说一旦出现了一个大环,这个大环就会造成天线效应(类似NFC),就会造成电磁干扰,所以不能用环形连接, I2C的信号是从第一个地址从头往后传的,如果说你把他收尾相连了,他就不知道头在哪里尾在哪里了,

星形连接:

  从机1下面又挂载一个从机4,从机2下面又挂载一个从机5,从机3下面又挂载一个从机6,还有更狠的,在从机6下又挂载一个从机7,这就是信号衰减,下面每挂载一个从机,就要衰减一次,当你的信号线过长,一般末端还会加上一个上拉电阻,为了增强他的信号,像我们的板子并不大的情况下,末端就不用挂载一个上拉电阻了

环形连接主要是由于我们的天线效应对我们的信号造成信号干扰,星形连接是由于信号衰减效应造成总线末端的从机收到的信号质量由于星形连接导致信号大大衰减,最后吸收不到完整的信号而导致通信失败

I2C协议

①空闲状态

②开始信号

③停止信号

④应答信号

⑤数据的有效性(ACK / NACK)

⑥数据传输

1)空闲状态

I2C总线的SDA和SCL两条信号线同时处于高电平时(4.7k的上拉电阻提供),规定为总线的空闲状态。此时各个器件的输出级场效应管均处在截止状态,即释放总线,由两条信号线各自的上拉电阻把电平拉高。SDA必须采用复用开漏模式(硬件I2C),软件I2C(开漏输出)

如何区分硬件I2C和软件I2C?

  比如8051单片机只有IO、中断和串口,I2C是属于单片机的高级资源,一般低端的单片机内部没有I2C的硬件资源,但是我们用8051单片机依然可以用I2C,因为单片机的IO口拿来模拟I2C协议,所以硬件I2C和软件I2C的区别在于谁在控制I2C,硬件I2C是IO外设控制,而软件I2C是写一个软件驱动来模拟I2C的协议来手动控制IO口进行高低电平的变化,手动控制的效果和硬件控制的效果是一样的。所以软件I2C是不依赖硬件资源的

2)起始信号与停止信号      

 空闲电平:高电平    工作电平:低电平

- 起始信号:当SCL为高期间,SDA由高到低的跳变;启动信号是一种电平跳变时序信号,而不是一个电平信号。

- 停止信号:当SCL为高期间,SDA由低到高的跳变;停止信号也是一种电平跳变时序信号,而不是一个电平信号。

3)应答信号ACK

发送器每发送一个字节,就在时钟脉冲9期间释放数据线(一个字节有8位,为数据位,所以只能在第九个时钟脉冲期间进行应答),由接收器反馈一个应答信号。应答信号为低电平时,规定为有效应答位(ACK简称应答位),表示接收器已经成功地接收了该字节;应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。

对于反馈有效应答位ACK的要求是,接收器在第9个时钟脉冲之前的低电平期间将SDA线拉低,并且确保在该时钟的高电平期间为稳定的低电平。如果接收器是主控器,则在它收到最后一个字节后,发送一个NACK信号,以通知被控发送器结束数据发送,并释放SDA线,以便主控接收器发送一个停止信号P。

4)数据有效性

I2C总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。

即:数据在SCL的上升沿到来之前就需准备好。并在下降沿到来之前必须稳定。

如果说SCL处于高电平期间,SDA电平不稳发生了高电平变为低电平的跳变,这个时候就是我们的起始信号,如果发生的是低电平变为高电平的跳变,这个时候就是我们的停止信号

这些都不是一个有效的数据,所以在I2C当中,规定我们的数据必须是在SCL处于高电平期间,SDA上面的电平不能发生跳变,一旦有任何跳变都会被理解为起始信号或者停止信号,这就是我们的数据有效性。

SCL处于低电平期间,SDA无论怎么变都没事

也就是说SDA的周期一定是宽于SCL的

以24C02为例讲一下I2C是如何和24C02进行通信的

24C02字节写时序

举个例子:

可以把这个 I²C 总线的字节写操作类比成去快递驿站寄快递的过程:

### 1. START(起始信号)→ 走进快递驿站

就像 `START` 信号是 I²C 通信的“开场铃”,你走进快递驿站,相当于告诉驿站“我要开始寄件啦”,这是整个“通信(寄快递)”的起始动作。

### 2. DEVICE ADDRESS(设备地址)→ 报出快递公司代号

驿站里可能有多家快递公司的代收点(比如顺丰、圆通等,对应不同的 `I²C 设备`)。你要寄快递,得先报出**快递公司的代号**(比如“我要寄顺丰的件”),这就对应 `DEVICE ADDRESS` —— 告诉总线“我要和哪个设备通信”。

### 3. WRITE(写标志位)→ 说“我要寄件(写数据)”

报完快递公司,你得告诉驿站“我是来寄件的,不是取件的”,这就像 `WRITE` 位 —— 明确操作是“写(寄)”,不是“读(取)”。

### 4. WORD ADDRESS(寄存器地址)→ 告诉驿站“包裹放哪个货架”

驿站里有很多货架,每个货架对应不同的“存储区域”(类比设备里的寄存器)。你得告诉驿站“我的包裹要放在 **×× 号货架**”,这就对应 `WORD ADDRESS` —— 指定“数据要存在设备的哪个寄存器里”。

### 5. DATA(数据)→ 把要寄的“物品(数据)”给驿站

确定好货架后,你把要寄的“物品”(比如一个快递包裹,类比要传输的数据)交给驿站,这就是 `DATA` —— 传输实际要写进寄存器的内容。

### 6. ACK(应答)→ 驿站说“收到啦”

每次你报完代号、说清操作、给完地址或数据后,驿站会回你一句“收到”(比如“好的,顺丰知道了”“货架号记下来了”“包裹收到了”),这就是 `ACK` 应答 —— 设备告诉主设备“我收到你的指令/数据了”。

### 7. STOP(停止信号)→ 寄完快递,离开驿站

所有步骤完成后,你离开驿站,相当于 `STOP` 信号 —— 告诉总线“这次通信结束了”。

这样类比下来,整个 I²C 字节写操作,就像一次“目标明确、步骤清晰的寄快递流程”:从开场(START)→ 选快递公司(DEVICE ADDRESS)→ 说寄件(WRITE)→ 指货架(WORD ADDRESS)→ 交包裹(DATA)→ 得到回应(ACK)→ 结束离开(STOP)。

24C02读时序

可以把 24C02 的随机读(Random Read)时序类比成去图书馆找书并借阅的过程:

### 1. 第一个 START(起始信号)→ 走进图书馆

`START` 信号是 I²C 通信的“开始标识”,就像你走进图书馆,意味着“我要开始借书流程啦”。

### 2. DEVICE ADDRESS(设备地址) + WRITE(写标志位)→ 告诉管理员“我要查书(准备写地址)”

图书馆里有不同的阅览区(类比不同的 `I²C 设备`,比如文学区、科技区)。你先告诉管理员“我要去**文学区**(设备地址)”,并且说“我现在要查这本书的位置(写操作,因为第一步是要把‘想读的书的位置’告诉系统)”。

### 3. WORD ADDRESS n(寄存器地址)→ 告诉管理员“想读的书在哪个书架层”

文学区有很多书架,每个书架又有不同层。你告诉管理员“我想读的书在 **×× 书架 ×× 层**(寄存器地址)”,这一步是指定“要读的数据存在设备的哪个位置”。

### 4. 第二个 START(起始信号)→ 跟管理员说“现在要借书了”

相当于流程切换,从“查书的位置”切换到“正式借书”,用新的 `START` 信号告诉系统“接下来要读数据了”。

### 5. DEVICE ADDRESS(设备地址) + READ(读标志位)→ 告诉管理员“我要借这本书(读操作)”

再次报“文学区(设备地址)”,并说“现在我要借这本书(读操作)”,明确“这次是要获取数据”。

### 6. DATA n(数据)→ 管理员把书给你

管理员根据你之前说的“书架层”,把对应的书(数据)拿给你,这就是设备把“指定位置的内容”传输出来。

### 7. NO ACK(无应答) + STOP(停止信号)→ 你说“书拿到了,结束”

你拿到书后,不需要再回应(无应答),然后离开图书馆(`STOP` 信号),表示“这次借书流程结束”。

这样类比后,24C02 随机读的过程就像:进图书馆→找对应区域+说要查书位置→指定想读书的书架层→切换到借书流程→找对应区域+说要借书→拿到书→离开,每一步都对应时序里的信号和操作~

iic.c

#include "iic.h"
#include "delay.h"

/*
//初始化IIC
void IIC_Init(void)
{					     
	GPIO_InitTypeDef GPIO_InitStructure;
	//RCC->APB2ENR|=1<<4;//先使能外设IO PORTC时钟 
	RCC_APB2PeriphClockCmd(	RCC_APB2Periph_GPIOC, ENABLE );	
	   
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP ;   //推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOC, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP ;   //推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOC, &GPIO_InitStructure);
 
	IIC_SCL=1;
	IIC_SDA=1;
}*/
 
void IIC_Init(void)
{					     
		GPIO_InitTypeDef GPIO_InitStructure;
		RCC_APB2PeriphClockCmd(	RCC_APB2Periph_GPIOB, ENABLE );
		
		GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;	//PB6  SCL
		GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD ;   //推挽输出
		GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
		GPIO_Init(GPIOB, &GPIO_InitStructure);
	
		GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;	//PB7  SDA
		GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD ;   //推挽输出
		GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
		GPIO_Init(GPIOB, &GPIO_InitStructure);
		
		IIC_SCL=1;
		IIC_SDA=1;
}
 
 
//产生IIC起始信号
void IIC_Start(void)
{
		SDA_OUT();     //sda线输出
		IIC_SDA=1;	  	  
		IIC_SCL=1;
		delay_us(4);
		IIC_SDA=0;//START:when CLK is high,DATA change form high to low 
		delay_us(4);
		IIC_SCL=0;//钳住I2C总线,准备发送或接收数据 
}
 
 
//产生IIC停止信号
void IIC_Stop(void)
{
		SDA_OUT();//sda线输出
		IIC_SCL=0;
		IIC_SDA=0;//STOP:when CLK is high DATA change form low to high
		delay_us(4);
		IIC_SCL=1; 
		delay_us(4);
		IIC_SDA=1;//发送I2C总线结束信号
}
 
 
//等待应答信号到来
//返回值:1,接收应答失败
//        0,接收应答成功
u8 IIC_Wait_Ack(void)
{
		u8 ucErrTime=0;
		SDA_IN();      //SDA设置为输入  
		IIC_SDA=1;delay_us(1);	   
		IIC_SCL=1;delay_us(1);	 
		while(READ_SDA)
		{
				ucErrTime++;
				if(ucErrTime>250)
				{
						IIC_Stop();
						return 1;
				}
		}
		IIC_SCL=0;//时钟输出0 	   
		return 0;  
}
 
 
//产生ACK应答
void IIC_Ack(void)
{
		IIC_SCL=0;
		SDA_OUT();
		IIC_SDA=0;
		delay_us(2);
		IIC_SCL=1;
		delay_us(2);
		IIC_SCL=0;
}
 
 
//不产生ACK应答		    
void IIC_NAck(void)
{
		IIC_SCL=0;
		SDA_OUT();
		IIC_SDA=1;
		delay_us(2);
		IIC_SCL=1;
		delay_us(2);
		IIC_SCL=0;
}
 
 
//IIC发送一个字节
//返回从机有无应答
//1,有应答
//0,无应答			  
void IIC_Send_Byte(u8 txd)
{
		u8 t;   
		SDA_OUT(); 	    
		IIC_SCL=0;//拉低时钟开始数据传输
		for(t=0;t<8;t++)
		{              
				IIC_SDA=(txd&0x80)>>7;
				txd<<=1; 	  
				delay_us(2);   //对TEA5767这三个延时都是必须的
				IIC_SCL=1;
				delay_us(2); 
				IIC_SCL=0;	
				delay_us(2);
		}	 
}
 
 
//读1个字节,ack=1时,发送ACK,ack=0,发送nACK   
u8 IIC_Read_Byte(unsigned char ack)
{
		unsigned char i,receive=0;
		SDA_IN();//SDA设置为输入
		for(i=0;i<8;i++ )
		{
				IIC_SCL=0; 
				delay_us(2);
				IIC_SCL=1;
				receive<<=1;
				if(READ_SDA)
						receive++;   
				delay_us(1); 
		}
		if (!ack)
				IIC_NAck();//发送nACK
		else
				IIC_Ack(); //发送ACK   
		return receive;
}

iic.h

#ifndef __IIC_H
#define __IIC_H
#include "sys.h"
 
 
   /*
//IO方向设置
#define SDA_IN()  {GPIOC->CRH&=0XFFFF0FFF;GPIOC->CRH|=8<<12;}
#define SDA_OUT() {GPIOC->CRH&=0XFFFF0FFF;GPIOC->CRH|=3<<12;}
//IO操作函数	 
#define IIC_SCL    PCout(12) //SCL
#define IIC_SDA    PCout(11) //SDA	 
#define READ_SDA   PCin(11)  //输入SDA 
*/
 
//IO方向设置
#define SDA_IN()  {GPIOB->CRL&=0x0FFFFFFF;GPIOB->CRL|=(u32)8<<28;} //PB7设置为上拉输入
#define SDA_OUT() {GPIOB->CRL&=0x0FFFFFFF;GPIOB->CRL|=(u32)3<<28;} //PB7设置为推挽输出
 
//IO操作函数	 
#define IIC_SCL    PBout(6) //SCL
#define IIC_SDA    PBout(7) //SDA	 
#define READ_SDA   PBin(7)  //输入SDA 
 
 
 
//IIC所有操作函数
void IIC_Init(void);                //初始化IIC的IO口				 
void IIC_Start(void);				//发送IIC开始信号
void IIC_Stop(void);	  			//发送IIC停止信号
void IIC_Send_Byte(u8 txd);			//IIC发送一个字节
u8 IIC_Read_Byte(unsigned char ack);//IIC读取一个字节
u8 IIC_Wait_Ack(void); 				//IIC等待ACK信号
void IIC_Ack(void);					//IIC发送ACK信号
void IIC_NAck(void);				//IIC不发送ACK信号
 
void IIC_Write_One_Byte(u8 daddr,u8 addr,u8 data);
u8 IIC_Read_One_Byte(u8 daddr,u8 addr);	  
#endif

24cxx.c

#include "24cxx.h" 
#include "delay.h"

 
//初始化IIC接口
void AT24CXX_Init(void)
{
		IIC_Init();
}
 
 
//在AT24CXX指定地址读出一个数据
//ReadAddr:开始读数的地址  
//返回值  :读到的数据
u8 AT24CXX_ReadOneByte(u16 ReadAddr)
{
		u8 temp=0;		  	    																 
		IIC_Start();  
		if(EE_TYPE>AT24C16)
		{
				IIC_Send_Byte(0XA0);	   //发送写命令
				IIC_Wait_Ack();
				IIC_Send_Byte(ReadAddr>>8);//发送高地址
//				IIC_Wait_Ack();		 			//这句也得考虑注释掉
		}
		else
				IIC_Send_Byte(0XA0+((ReadAddr/256)<<1));   //发送器件地址0XA0,写数据 	 
 
		IIC_Wait_Ack(); 						//这句去掉就OK了,就可以通过AT24CXX_Check()了
		IIC_Send_Byte(ReadAddr%256);   //发送低地址
		IIC_Wait_Ack();	    
		IIC_Start();  	 	   
		IIC_Send_Byte(0XA1);           //进入接收模式			   
		IIC_Wait_Ack();	 
		temp=IIC_Read_Byte(0);		   
		IIC_Stop();//产生一个停止条件	    
		return temp;
}
 
 
//在AT24CXX指定地址写入一个数据
//WriteAddr  :写入数据的目的地址    
//DataToWrite:要写入的数据
void AT24CXX_WriteOneByte(u16 WriteAddr,u8 DataToWrite)
{
		IIC_Start();  
		if(EE_TYPE>AT24C16)
		{
				IIC_Send_Byte(0XA0);	    //发送写命令
				IIC_Wait_Ack();
				IIC_Send_Byte(WriteAddr>>8);//发送高地址
		}
		else
		{
				IIC_Send_Byte(0XA0+((WriteAddr/256)<<1));   //发送器件地址0XA0,写数据 
		}	 
		IIC_Wait_Ack();	   
		IIC_Send_Byte(WriteAddr%256);   //发送低地址
		IIC_Wait_Ack(); 	 										  		   
		IIC_Send_Byte(DataToWrite);     //发送字节							   
		IIC_Wait_Ack();  		    	   
		IIC_Stop();//产生一个停止条件 
		delay_ms(10);	 
}
 
 
//在AT24CXX里面的指定地址开始写入长度为Len的数据
//该函数用于写入16bit或者32bit的数据.
//WriteAddr  :开始写入的地址  
//DataToWrite:数据数组首地址
//Len        :要写入数据的长度2,4
void AT24CXX_WriteLenByte(u16 WriteAddr,u32 DataToWrite,u8 Len)
{
		u8 t;
		for(t=0;t<Len;t++)
		{
				AT24CXX_WriteOneByte(WriteAddr+t,(DataToWrite>>(8*t))&0xff);
		}												    
}
 
 
//在AT24CXX里面的指定地址开始读出长度为Len的数据
//该函数用于读出16bit或者32bit的数据.
//ReadAddr   :开始读出的地址 
//返回值     :数据
//Len        :要读出数据的长度2,4
u32 AT24CXX_ReadLenByte(u16 ReadAddr,u8 Len)
{
		u8 t;
		u32 temp=0;
		for(t=0;t<Len;t++)
		{
				temp<<=8;
				temp+=AT24CXX_ReadOneByte(ReadAddr+Len-t-1); 	 				   
		}
		return temp;												    
}
 
 
//检查AT24CXX是否正常
//这里用了24XX的最后一个地址来存储标志字.24C02是255,24C512是65535
//如果用其他24C系列,这个地址要修改
//返回1:检测失败
//返回0:检测成功
u8 AT24CXX_Check(void)
{
		u8 temp;
		temp=AT24CXX_ReadOneByte(0xFF);//避免每次开机都写AT24CXX			   
		if(temp==0X55)
				return 0;		   
		else//排除第一次初始化的情况
		{
				AT24CXX_WriteOneByte(0xFF,0X55);
				temp=AT24CXX_ReadOneByte(0xFF);
				if(temp==0X55)
						return 0;
		}
		return 1;											  
}
 
 
//在AT24CXX里面的指定地址开始读出指定个数的数据
//ReadAddr :开始读出的地址 对24c02为0~255
//pBuffer  :数据数组首地址
//NumToRead:要读出数据的个数
void AT24CXX_Read(u16 ReadAddr,u8 *pBuffer,u16 NumToRead)
{
		while(NumToRead)
		{
				*pBuffer++=AT24CXX_ReadOneByte(ReadAddr++);	
				NumToRead--;
		}
}
 
 
//在AT24CXX里面的指定地址开始写入指定个数的数据
//WriteAddr :开始写入的地址 对24c02为0~255
//pBuffer   :数据数组首地址
//NumToWrite:要写入数据的个数
void AT24CXX_Write(u16 WriteAddr,u8 *pBuffer,u16 NumToWrite)
{
		while(NumToWrite--)
		{
				AT24CXX_WriteOneByte(WriteAddr,*pBuffer);
				WriteAddr++;
				pBuffer++;
		}
}
 
 
 
/*
AT24C512的读写问题?- 百度文库  
https://wenku.baidu.com/view/cfc89adaa48da0116c175f0e7cd184254b351bb6.html
阿波罗F4/F7开发板例程不支持24C512的解决办法-OpenEdv-开源电子网  
http://www.openedv.com/forum.php?mod=viewthread&tid=93162&highlight=24C512
*/

24cxx.h

#ifndef __24CXX_H
#define __24CXX_H
#include "iic.h"   

#define AT24C01		127
#define AT24C02		255
#define AT24C04		511
#define AT24C08		1023
#define AT24C16		2047
#define AT24C32		4095
#define AT24C64	  8191
#define AT24C128	16383
#define AT24C256	32767  
#define AT24C512	65535
//Mini STM32开发板使用的是24c02,所以定义EE_TYPE为AT24C02
//#define EE_TYPE AT24C02
 
#define EE_TYPE AT24C02
						
u8 AT24CXX_ReadOneByte(u16 ReadAddr);							//指定地址读取一个字节
void AT24CXX_WriteOneByte(u16 WriteAddr,u8 DataToWrite);		//指定地址写入一个字节
void AT24CXX_WriteLenByte(u16 WriteAddr,u32 DataToWrite,u8 Len);//指定地址开始写入指定长度的数据
u32 AT24CXX_ReadLenByte(u16 ReadAddr,u8 Len);					//指定地址开始读取指定长度数据
void AT24CXX_Write(u16 WriteAddr,u8 *pBuffer,u16 NumToWrite);	//从指定地址开始写入指定长度的数据
void AT24CXX_Read(u16 ReadAddr,u8 *pBuffer,u16 NumToRead);   	//从指定地址开始读出指定长度的数据
 
u8 AT24CXX_Check(void);  //检查器件
void AT24CXX_Init(void); //初始化IIC
#endif

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐