I2C通讯协议篇

 I2C构成:由一台主机和1个或多个从机通过时钟线(SCL)和数据线(SDA)两条线相接进行通信(两条线上各有一个上拉电阻)
时钟线:由主机提供时钟信号,用来控制整个I2C的速度和节奏
数据线:主机可以向从机发送数据,从机也可以向主机发送数据
注:在I2C通讯协议中,所有设备的SCL和SDA都要设置为复用开漏输出模式

为什么需要I2C通信协议(优点):I2C协议占用单片机io接口少,只需要两个io引脚就可以与128个设备进行通信,极其省资源

为什么两条线需要有上拉电阻
首先将两条线上的上拉电阻舍去,当所有的设备都处于高电平状态,也是就开路,这个时候两条线上的电平信号不确定,那么设备内部的硬件电路在读取电平信号时就会乱读,而如果分别接上一个上拉电阻之后,事情就不一样了,设备处于开路时,线上的电平信号通过上拉电阻能够稳定在高电平
为什么不用下拉电阻:因为I2C协议规定引脚设置为开漏输出模式,设备能够输出低电平0而不能输出高电平1(开路状态),因此线上需要一个上拉电阻在设备输出高电平的时候拉高

设备是如何知晓发送的信号是高电平还是低电平?
回顾开漏输出模式,高电平高阻抗约等于开路,低电平导通表示0;
每个支持I2C通信协议的设备内部都有能够检测输入的硬件电路;(如施密特触发器)
设备输出低电平线上的电平会被拉低为低电平,输出高电平线上的电平会被上拉电阻拉高为高电平,而其他设备则通过输入检测电路捕捉线上的高低电平变化即可

通信过程/步骤(主发从收):主机发送起始位——主机再次发送7位地址+读写位(W-0)——该地址的从机发送应答信号且主机接收——主发送字节数据1——从发送应答信号并被接收——主发送字节数据n——从发送应答信号并被接收——主机发送停止位;  至此整个通信过程结束主机完美的向从机写入一段数据。

通信过程/步骤(从发主收——1个字节):主机发送起始位——主机再次发送7位地址+读写位(R-1)——该地址的从机发送应答信号且主机接收——设置停止位和应答位为NCK并发送——从机发送字节数据1——主机接收数据并读取;  至此整个通信过程结束主机完美的向从机读出2个字节的数据。

通信过程/步骤(从发主收——2个字节):主机发送起始位——主机再次发送7位地址+读写位(R-1)——该地址的从机发送应答信号且主机接收——设置自动应答位为ACK——从机发送字节数据1——主机接收数据并读取——设置停止位和自动应答位为NCK——从机发送字节数据2——主机接收数据并读取;  至此整个通信过程结束主机完美的向从机读出2个字节的数据。

通信过程/步骤(从发主收——n个字节):主机发送起始位——主机再次发送7位地址+读写位(R-1)——该地址的从机发送应答信号且主机接收——设置自动应答位为ACK——(从机发送字节数据1——主机接收数据并读取) * n-1次——设置停止位和自动应答位为NCK并发送——从机发送字节数据n——主机接收数据并读取;  至此整个通信过程结束主机完美的向从机读出n个字节的数据。

起始位信号:时钟线高电平,数据线从高电平变为低电平产生一个下降沿标志起始位信号
停止位信号:时钟线高电平,数据线从低电平变为高电平产生一个上升沿标志停止位信号
自动应答位:当设备接收到一个字节后会自动发送一个应答信号,通过函数设置后自动应答位会长久保持,只能手动更改应答位设置
七位地址+读写位:众所周知,两个设备之间想要确立通信首先得确保能找到设备,找到设备所需要的就是设备的地址;支持I2C通信的设备都有一个8位二进制地址,但是由于最高位为零,因此只有七位是有效的,也叫七位地址;读写位就是主机对从机的操作,是向从机写数据(0)还是读数据(1);
组合方法:地址舍去最高位,读写位补最低位重新构成一个八位二进制
例:设备地址0x75   读          0x75 = 0111 0101     读=1         发送的地址+读写位 = 1110 1011 = 0xEB
寻址应答过程:主机发送寻址数据后释放总线,此时由于上拉电阻,总线电平逐渐拉高,此时从机应答,再次拉低总线完成应答

 

I2C片上外设介绍:48脚STM32有两个I2C外设(I2C1/I2C2),由APB1管理,以下是对应的功能引脚和重映射引脚
复用功能   I2C1_REMAP=0    I2C1_REMAP=1(重映射)      复用功能      无重映射
I2C1_SCL      PB6          PB8           I2C2_SCL      PB10
I2C1_SDA      PB7          PB9           I2C2_SDA      PB11

5种传输模式:
标准模式    Sm    <=100kbps
快速模式    Fm    <=400kbps
快速增强模式     Fm+    <=1Mbps
高速模式    HSm    <=3.4Mbps
超快模式    UFm    <=5Mbps
STM32F103C8T6适用标准模式和快速模式,其中快速模式下可以设置占空比
占空比:一个时钟信号会跳变,占空比就决定一个时钟信号跳变的距离,有两种占空比    低/高 = 2/1 = 16/9,将一个时钟信号分成3/25份,低占2/16份,高占1/9份

 

介绍一下I2C硬件电路有什么内容以及作用
发送数据寄存器:存放想要发送的1个字节,发送移位寄存器为空时将存放的字节移动到发送移位寄存器中
发送移位寄存器:将1个字节从高位到低位依次发送出去
接收数据寄存器:接收1个字节,当接收移位寄存器接收到8个比特后将其移动至接收数据寄存器中组成一个字节
接收移位寄存器:从高到低接收8个比特,然后将其移动至接收数据寄存器中
时钟总开关:开启时钟线的总开关,如果不开启,整个I2C通信则不起作用
SDA控制:控制发送的波形或解析接收的波形,里面还内置START/STOP/ACK的波形可以通过函数用来发送
SCL控制:控制时钟信号,通过初始化函数中的结构体变量进行设置(标注/快速)(占空比)(波特率)
比较重要的标志位(RESET = 0  SET = 1)
BUSY-总线标志位:0 = 总线空闲,1 = 总线繁忙
SB-起始位发送标志位:0 = 起始位未发送,1 = 起始位已发送
ADDR-寻址标志位:1 = 寻址成功
AF-应答标志位:1 = 未收到ACK即应答失败
TxE-发送数据寄存器标志位:1 = 发送数据寄存器位空
RxNE-接收数据寄存器标志位:1 = 接收成功
BTF-移位寄存器标志位:1 = 移位寄存器数据发送完成为空

START-起始位:通过函数控制,向总线发送一个起始位
STOP-停止位:通过函数控制,向总线发送一个停止位表示结束,作用在正在被接收的字节之后
ACK-自动应答位:通过函数控制接收一个字节后是否应答,作用在正在被接收的字节之后
什么是作用在正在被接收的字节之后?
当字节正在接受的过程中,设置停止位和应答位,那么在接收完字节后就会执行,而当一个字节接收完成后再设置,那么设置则会在下一个字节接受完成后执行

 

函数介绍

RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2Cx,ENABLE);
作用:开启I2Cx片上外设的时钟,即该外设是否工作
参数1:RCC_APB1Periph_I2Cx,x={1,2},选择I2C模块
参数2:ENABLE/DISABLE,启动/关闭

 

RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2Cx,ENABLE);
作用:将I2Cx外设上的所有寄存器重置为默认值
参数1:RCC_APB1Periph_I2Cx,x={1,2},选择I2C模块
参数2:ENABLE/DISABLE,置位/清零
注意:该函数需要使用两次,一次ENABLE一次DISABLE;ENABLE寄存器清零,DISABLE寄存器恢复默认

 

I2C_Init(I2Cx,&I2C_InitStruct);
作用:设置I2C的工作参数
参数1:I2Cx,x={1,2},选择I2C模块
参数2:&结构体地址,里面有对应的工作变量以及参数
I2C_InitTypeDef I2C_InitStruct
{
  uint32_t I2C_ClockSpeed;    //时钟频率(100000Hz/400000Hz)
  uint16_t I2C_Mode;    //工作模式
  uint16_t I2C_DutyCycle;    //占空比
  uint16_t I2C_OwnAddress1;    //设置设备自己的通信地址
  uint16_t I2C_Ack;    //接收数据后是否自动应答
  uint16_t I2C_AcknowledgedAddress;    //寻址地址的格式长度
};

 

I2C_Cmd(I2Cx, ENABLE);
作用:I2C时钟线的总开关是否开启
参数1:I2Cx,x={1,2},选择I2C模块
参数2:ENABLE/DISABLE,启动/关闭

 

I2C_GetFlagStatus(I2Cx,I2C_FLAG_xxxx);
作用:检查I2Cx外设中的某个标志位是否被置位(即设备现在的状态如何)
参数1:I2Cx,x={1,2},选择I2C模块
参数2:I2C_FLAG_xxxx,xxxx={BUSY,SB,ADDR,TXE,RXNE,BTF,STOPF,AF,ARLO,BERR,OVR},选择查询的标志位
返回值:RESET = 0 = 未使用,SET = 1 = 已使用

 

I2C_GenerateSTART(I2Cx,ENABLE);
作用:是否向总线发送一个起始位唤醒所有的从机准备工作接收后续的指令
参数1:I2Cx,x={1,2},选择I2C模块
参数2:ENABLE/DISABLE(极少使用),产生/取消

 

I2C_GenerateSTOP(I2Cx,ENABLE);
作用:使设备释放总线的控制权,以便其他设备使用总线
参数1:I2Cx,x={1,2},选择I2C模块
参数2:ENABLE/DISABLE(极少使用),产生/取消

 

I2C_AcknowledgeConfig(I2Cx,ENABLE);
作用:接收一个字节后,自动发送ACK(应答)还是NCK(非应答)
参数1:I2Cx,x={1,2},选择I2C模块
参数2:ENABLE/DISABLE,1-应答/0-非应答

 

I2C_ClearFlag(I2Cx,I2C_FLAG_xxxx);
作用:手动清除I2C外设中指定的标志位
参数1:I2Cx,x={1,2},选择I2C模块
参数2:I2C_FLAG_xxxx,xxxx={BUSY,SB,TXE,RXNE,BTF,STOPF,AF,ARLO,BERR,OVR},选择清除的标志位

I2C_SendData(I2Cx,0xXX);
作用:发送一个字节的数据
参数1:I2Cx,x={1,2},选择I2C模块
参数2:一个字节的二进制转成十六进制

 

I2C_ReadRegister(I2Cx, I2C_Register_XXX); 
作用:读取某个状态寄存器的全部32位值并返回
参数1:I2Cx,x={1,2},选择I2C模块
参数2:I2C_Register_XXX, XXX = {SR1,SR2,CR1,CR2},选择读取的状态寄存器
返回值:uint32_t类型的值
注意:当读取某个状态寄存器时,里面有些标志位会被自动清除,当查询SR1和SR2时,ADDR标志位会被清除
因此在使用该方法时要特别谨慎
另外为什么清除ADDR标志位不使用I2C_ClearFlag()函数,问deepseek给出的解释是ADDR标志位不支持写入所以不能使用

 

I2C及其常用函数都以介绍完,那么接下来就是读写的详细步骤啦

 

写数据

1. 检测总线:检测BUSY标志位等待总线空闲
2. 发送起始位:发送起始位并且检测SB标志位判断起始位是否发送成功
3. 寻找设备:清除AF标志位,发送设备地址,检测AF和ADDR标志位,判断从机应答状态(应答失败发送STOP并返回值)以及寻址是否成功
4. 清除ADDR
5. 发送数据:检测AF标志位判断上一个数据应答状态,再检测TXE标志位判断发送数据寄存器是否为空,发送数据
6. 最后应答检测:检测检测AF标志位判断上一个数据应答状态(即最后一个数据的应答状态)
7. 发送停止位,写数据结束

int My_I2C_SendBytes(I2C_TypeDef *I2Cx, uint8_t Addr, uint8_t *pData, uint16_t Size)
{
	// 1. 等待总线空闲
	while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_BUSY) == SET);
	
	// 2. 发送起始位
	I2C_GenerateSTART(I2Cx,ENABLE);
	
	while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_SB) == RESET);
	
	// 3. 寻址阶段
	I2C_ClearFlag(I2Cx, I2C_FLAG_AF);		//清零应答失败标志位
	
	I2C_SendData(I2Cx,Addr & 0xfe);		//按位与,1与任何数等于任何数,0与任何数等于0
	
	while(1)
	{
		if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_ADDR) == SET)
		{
			break;		//寻址成功,退出循环
		}
		if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_AF) == SET)
		{
			// 应答失败
			I2C_GenerateSTOP(I2Cx,ENABLE);		//发送停止位停止发送
			return -1;
		}
	}
	
	//清除ADDR
	I2C_ReadRegister(I2Cx, I2C_Register_SR1);		//先读SR1寄存器
	I2C_ReadRegister(I2Cx, I2C_Register_SR2);		//后读SR2寄存器
	
	// 4. 发送数据
	for(uint16_t i = 0; i < Size; i++)
	{
		while(1)
		{
			//先判断上一个发送的数据是否应答失败
			if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_AF) == SET)
			{
				//如果应答失败则发送停止位停止发送
				I2C_GenerateSTOP(I2Cx,ENABLE);
				return -2;		//并且返回-2,表示发送的数据被拒绝
			}
			//判断发送数据寄存器是否为空如果为空直接跳出循环
			if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_TXE) == SET)
			{
				break;
			}
		}
		
		I2C_SendData(I2Cx,pData[i]);
	}
	
	while(1)
	{
		//判断最后一个发送的数据是否应答失败
		if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_AF) == SET)
		{
			//如果应答失败则发送停止位停止发送
			I2C_GenerateSTOP(I2Cx,ENABLE);
			return -2;		//并且返回-2,表示发送的数据被拒绝
		}
		//判断移位寄存器中的数据是否发送成功,发送成功直接跳出循环
		if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_BTF) == SET)
		{
			break;
		}
	}
	
	// 5. 发送停止位
	I2C_GenerateSTOP(I2Cx,ENABLE);
	return 0;		//数据发送完成
}

  

 读数据
1. 检测总线:检测BUSY标志位等待总线空闲
2. 发送起始位:发送起始位并且检测SB标志位判断起始位是否发送成功
3. 寻找设备:清除AF标志位,发送设备地址,检测AF和ADDR标志位,判断从机应答状态(应答失败发送STOP并返回值)以及寻址是否成功
4. 判断读取的数据长度:1个字节(6)/2个字节(7-8)/大于2个字节(6)
5. 清除ADDR
6. 1个字节:设置自动应答位NCK和停止位,检测RXNE标志位判断是否接收1个字节,然后读取并保存
7. 2个字节:设置自动应答位ACK,检测RXNE标志位判断是否接收1个字节,然后读取并保存(第一个字节)
8. 2个字节:设置自动应答位NCK和停止位,检测RXNE标志位判断是否接收1个字节,然后读取并保存(第二个字节)
9. n个字节:设置自动应答位ACK,检测RXNE标志位判断是否接收1个字节,然后读取并保存(检测保存循环n-1次)
10. n个字节:设置自动应答位NCK和停止位,检测RXNE标志位判断是否接收1个字节,然后读取并保存(最后一个字节)

int My_I2C_ReceiveBytes(I2C_TypeDef *I2Cx, uint8_t Addr, uint8_t *pBuffer, uint16_t Size)
{
	// 检测总线是否空闲
	while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_BUSY) == SET);
	
	// 1. 发送起始位,I2C_GenerateSTART函数发送起始位,判断SB标志位是否发送成功
	I2C_GenerateSTART(I2Cx,ENABLE);
	
	while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_SB) == RESET);	//0-表示未发送成功
	
	// 2. 发送寻址地址并判断从机是否回复ACK
	I2C_ClearFlag(I2Cx,I2C_FLAG_AF);		// 清除AF标志位
	
	I2C_SendData(I2Cx,Addr | 0x01);		// 发送地址和读写位
	
	while(1)
	{
		if(I2C_GetFlagStatus(I2Cx, I2C_FLAG_AF) == SET)		// AF标志位=1  应答失败
		{
			I2C_GenerateSTOP(I2Cx,ENABLE);		// 发送停止位
			return -1;		// 应答失败
		}
		
		if(I2C_GetFlagStatus(I2Cx, I2C_FLAG_ADDR) == SET)		// ADDR标志位=1  寻址成功
		{
			break;		// 寻址成功跳出循环
		}
	}
	
	// 3. 判断需要几个字节,以及如何接收
	
	if(Size == 1)
	{
		// 清除ADDR标志位
		I2C_ReadRegister(I2Cx,I2C_Register_SR1);
		I2C_ReadRegister(I2Cx,I2C_Register_SR2);
		// 清除标志位后立即向ACK写0并且发送停止位
		I2C_AcknowledgeConfig(I2Cx,DISABLE);
		// 上面的函数对硬件的影响是持续性的,除非手动设置,否则每接受一个数据主机都会自动应答一个ack或nck
		
		I2C_GenerateSTOP(I2Cx,ENABLE);
		// 判断rxne标志位是否置一,置一表示接收数据完成
		while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_RXNE) == RESET);
		// 读取数据并保存
		pBuffer[0] = I2C_ReceiveData(I2Cx);
	}else if(Size == 2){
		// 清除ADDR标志位
		I2C_ReadRegister(I2Cx,I2C_Register_SR1);
		I2C_ReadRegister(I2Cx,I2C_Register_SR2);
		// 清除标志位后立即向ACK写1
		I2C_AcknowledgeConfig(I2Cx,ENABLE);
		// 判断rxne标志位是否置一,置一表示接收数据完成
		while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_RXNE) == RESET);
		// 读取第一个数据并保存
		pBuffer[0] = I2C_ReceiveData(I2Cx);
		
		// 第一个字节接收完成后立即向ACK写0并且发送停止位
		I2C_AcknowledgeConfig(I2Cx,DISABLE);
		I2C_GenerateSTOP(I2Cx,ENABLE);
		// 判断rxne标志位是否置一,置一表示接收数据完成
		while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_RXNE) == RESET);
		// 读取第二个数据并保存
		pBuffer[1] = I2C_ReceiveData(I2Cx);
	}else{
		// 清除ADDR标志位
		I2C_ReadRegister(I2Cx,I2C_Register_SR1);
		I2C_ReadRegister(I2Cx,I2C_Register_SR2);
		// 清除标志位后立即向ACK写1
		I2C_AcknowledgeConfig(I2Cx,ENABLE);
		
		for(uint16_t i = 0; i < Size - 1; i++)
		{
			// 判断rxne标志位是否置一,置一表示接收数据完成
			while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_RXNE) == RESET);
			// 读取第i个数据并保存
			pBuffer[i] = I2C_ReceiveData(I2Cx);
		}
		
		// 倒数第二个字节接收完成后立即向ACK写0并且发送停止位
		I2C_AcknowledgeConfig(I2Cx,DISABLE);
		I2C_GenerateSTOP(I2Cx,ENABLE);
		// 判断rxne标志位是否置一,置一表示接收数据完成
		while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_RXNE) == RESET);
		// 读取最后个数据并保存
		pBuffer[Size - 1] = I2C_ReceiveData(I2Cx);
	}
	
	return 0;		//接收成功
}

  

posted on 2026-04-17 23:10  绝情世家  阅读(96)  评论(0)    收藏  举报