【STM32】I2C

虽有尽善尽美之心,奈何学识有限,水平一般,不可一蹴而就。故而留待以后不断打磨完善。


一、I2C是什么?

是一种支持多主机多从机半双工 、同步串行低速通讯方式。多用于芯片间通讯。

二、发展历史

由荷兰飞利浦公司于1980年代发明。

自2006年10月1日起,使用I²C协议已经不需要支付专利费。

I²C全称叫 Inter-Integrated Circuit,字面上的意思是集成电路之间,它其实是I²C Bus简称。

三、物理层

在这里插入图片描述
1、总线上可以挂载多个主机和从机。
2、I2C仅有一根线用于传输数据,即SDA,另一根SCL是用于辅助SDA同步时钟的。
3、总线上的每个设备都有独立的地址,主从设备通过该地址确定发送给谁,以及从谁那里接收数据。
4、总线上最大的节点数目被地址空间所限制(7位地址空间理论支持127个设备,I2C还有10位地址空间),但实际上也会被总线上的总电容所限制住,一般而言为400 pF。

为什么会有电容限制?
当总线电容增大时,数据上升沿斜率会变大,当增大到一定程度,方波就变形而无法识别了

5、两根线均通过上拉电阻拉成高电平。
6、多个主机想要同时使用总线时,会利用仲裁方式决定由哪个主机占用总线。

主机即发送数据的设备,从机即接收数据的设备

7、支持多种传输模式:低速模式(10 kbit/s)、标准模式(100 kbit/s)、快速模式(400 kbit/s)、快速+模式(1 Mbit/s)、高速模式(3.4 Mbit/s)、超高速模式(5 Mbit/s)。想要快速传输具体还得看设备支不支持。

四、协议层

1、基本读写过程

(1)写数据

在这里插入图片描述
在这里插入图片描述
主机想要写数据到从机,分为这么几步。
主机先在总线上大吼一声,所有从机看向我,我要发送数据了!这一步即发送起始信号(Start);
此时所有从机都惴惴不安地静候着,期待着自己能被选中。连接到总线上的所有从机都能接收到起始信号;
主机说10032号,就是你了!
此时其他从机都失望地离开了,走的时候顺便向10032号致以祝贺;这一步叫发送从机地址,选择数据传输对象;
此时,总线上就剩下主机和10032号从机;
主机说:“有些话必须要对你说。”,10032紧张地把耳朵凑了过去,这一步叫做选择传输方向,你说我听;
10032脸庞微红,“你说吧,我听着呢。”这一步叫从机向主机发送应答信号ACK。
主机:“过几天我就要走了,”主机发送第一段信息;
从机:“这么急?”从机应答。
主机:“该准备的都备好了,”主机发送第二段信息;
从机:“祝贺你,”从机应答。
主机:“但还有件东西没带,”主机发送第三段信息;
从机:“什么呢?”从机应答。
主机:“就是我的公主你啊”主机发送第三段信息;
从机:“你真是…。”从机应答。
主机感动地笑了,这一刻再没有语言能表达他的感情了。主机发送停止信号,写数据完成。

注意,主机发送的数据包都是8位的。
主机每发送完一个字节数据,都要等待从机应答信号。
可以无限制向从机发送数据。

(2)读数据

在这里插入图片描述

与写数据相似,主机先发送起始信号,然后发送从机地址,再发送数据传输方向,此时为1(表示读),从机应答,从机将第一个8位数据包发送给主机,主机应答,从机发送第二个数据,主机应答,此过程可以重复N次,直到主机不想接受数据了,向从机发送一个非应答信号(NACK),则主机停止发送,主机最后发送停止信号,通讯结束。

(3)读写数据

在这里插入图片描述
这是一种复合模式,与前两种不同,它有两次起始信号。两次数据通讯都可以读或者写,所以一共有4种情况。

下面说明其中一种情况,第一次通讯主机写,第二次通讯主机读。

在这里插入图片描述
主机发送起始信号,发送从机地址,传输方向为0(写),写入要读取的寄存器地址(先找到是哪个设备,再告诉该设备我要读你哪个地方的数据),从机应答/非应答,第一次通讯结束。

主机再次发送起始信号,发送从机地址,传输方向为1(读),从机应答,从机根据第一次通讯获取的地址将主机想要的数据发送出去(你想要哪个地方的数据,我都给你),主机应答/非应答,主机发送停止信号,第二次通讯结束。

2、起始与停止信号

起始和停止信号都由主机产生
在这里插入图片描述
起始信号是SCL为高电平时,SDA从高电平变为低电平;
停止信号是SCL为高电平时,SDA从低电平变为高电平。

为什么是这样?
因为总线的默认状态是高电平(被拉高了),拉低表示有数据要传送,传送完了再回到空闲状态。

注意:SCL只有在有数据传输时才会处于高低电平往复的状态,空闲时保持高电平。

3、数据有效性

在这里插入图片描述
SCL高电平时完成数据采样,低电平时完成数据变换。

3、响应

Ⅰ、传输时主机产生时钟,主机先发送完8位数据;

Ⅱ、在第 9 个时钟时,数据发送端会释放 SDA 的控制权(将SDA置1),由数据接收端控制SDA;

Ⅲ、数据接收端若将 SDA 置为高电平,则表示非应答信号 (NACK);

Ⅳ、数据接收端若将 SDA 置为低电平,则表示应答信号 (ACK)。
在这里插入图片描述

五、软件模拟I2C

按照上述协议要求,我们就可以在STM32上随便找两个GPIO口分别作为SCL与SDA实现I2C通讯。这叫做软件模拟I2C

1、硬件

在这里插入图片描述
AT24C02是一种EEPROM芯片,这里我们要实现该芯片与STM32的I2C通讯。

AT24C02设备地址一共7位,高4位固定为1010b,低3位由A0、A1、A2决定,故这里AT24C02地址为1010000b。
在这里插入图片描述
由于I2C设备地址常与读写方向位构成一个字节,当要写入数据时,R/W位为0,故该字节为0xA0,该值称为写地址

当要读取数据时,R/W位为1,故该字节为0xA1,该值称为读地址

WP引脚具有写保护功能,当其为高电平时,就无法写入数据,这里接地。

AT24C02的SCL与SDA与STM32直接相连,且已经拉高。

2、程序编写

SCL与SDA均设为开漏输出模式。

static void i2c_CfgGpio(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;

	RCC_APB2PeriphClockCmd(EEPROM_RCC_I2C_PORT, ENABLE);	/* 打开GPIO时钟 */

	GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SCL_PIN | EEPROM_I2C_SDA_PIN;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;  	/* 开漏输出 */
	GPIO_Init(EEPROM_GPIO_PORT_I2C, &GPIO_InitStructure);

	/* 给一个停止信号, 复位I2C总线上的所有设备到待机模式 */
	i2c_Stop();
}

起始信号

void i2c_Start(void)
{
	/* 当SCL高电平时,SDA出现一个下跳沿表示I2C总线启动信号 */
	EEPROM_I2C_SDA_1();
	EEPROM_I2C_SCL_1();
	i2c_Delay();
	EEPROM_I2C_SDA_0();
	i2c_Delay();
	EEPROM_I2C_SCL_0();
	i2c_Delay();
}

停止信号

void i2c_Stop(void)
{
	/* 当SCL高电平时,SDA出现一个上跳沿表示I2C总线停止信号 */
	EEPROM_I2C_SDA_0();
	EEPROM_I2C_SCL_1();
	i2c_Delay();
	EEPROM_I2C_SDA_1();
}

应答信号

void i2c_Ack(void)
{
	EEPROM_I2C_SDA_0();	/* CPU驱动SDA = 0 */
	i2c_Delay();
	EEPROM_I2C_SCL_1();	/* CPU产生1个时钟 */
	i2c_Delay();
	EEPROM_I2C_SCL_0();
	i2c_Delay();
	EEPROM_I2C_SDA_1();	/* CPU释放SDA总线 */
}

非应答信号

void i2c_NAck(void)
{
	EEPROM_I2C_SDA_1();	/* CPU驱动SDA = 1 */
	i2c_Delay();
	EEPROM_I2C_SCL_1();	/* CPU产生1个时钟 */
	i2c_Delay();
	EEPROM_I2C_SCL_0();
	i2c_Delay();	
}

CPU产生一个时钟,并读取器件的ACK应答信号

uint8_t i2c_WaitAck(void)
{
	uint8_t re;

	EEPROM_I2C_SDA_1();	/* CPU释放SDA总线 */
	i2c_Delay();
	EEPROM_I2C_SCL_1();	/* CPU驱动SCL = 1, 此时器件会返回ACK应答 */
	i2c_Delay();
	if (EEPROM_I2C_SDA_READ())	/* CPU读取SDA口线状态 */
	{
		re = 1;
	}
	else
	{
		re = 0;
	}
	EEPROM_I2C_SCL_0();
	i2c_Delay();
	return re;
}

CPU向I2C总线设备发送8bit数据

void i2c_SendByte(uint8_t _ucByte)
{
	uint8_t i;

	/* 先发送字节的高位bit7 */
	for (i = 0; i < 8; i++)
	{		
		if (_ucByte & 0x80)
		{
			EEPROM_I2C_SDA_1();
		}
		else
		{
			EEPROM_I2C_SDA_0();
		}
		i2c_Delay();
		EEPROM_I2C_SCL_1();
		i2c_Delay();	
		EEPROM_I2C_SCL_0();
		if (i == 7)
		{
			 EEPROM_I2C_SDA_1(); // 释放总线
		}
		_ucByte <<= 1;	/* 左移一个bit */
		i2c_Delay();
	}
}

CPU从I2C总线设备读取8bit数据

uint8_t i2c_ReadByte(void)
{
	uint8_t i;
	uint8_t value;

	/* 读到第1个bit为数据的bit7 */
	value = 0;
	for (i = 0; i < 8; i++)
	{
		value <<= 1;
		EEPROM_I2C_SCL_1();
		i2c_Delay();
		if (EEPROM_I2C_SDA_READ())
		{
			value++;
		}
		EEPROM_I2C_SCL_0();
		i2c_Delay();
	}
	return value;
}

有了以上函数,将其按照时序组合就可以完成读写操作,以写数据为例

uint8_t ee_WriteBytes(uint8_t *_pWriteBuf, uint16_t _usAddress, uint16_t _usSize)
{
	uint16_t i,m;
	uint16_t usAddr;
	
	/* 
		写串行EEPROM不像读操作可以连续读取很多字节,每次写操作只能在同一个page。
		对于24xx02,page size = 8
		简单的处理方法为:按字节写操作模式,没写1个字节,都发送地址
		为了提高连续写的效率: 本函数采用page wirte操作。
	*/

	usAddr = _usAddress;	
	for (i = 0; i < _usSize; i++)
	{
		/* 当发送第1个字节或是页面首地址时,需要重新发起启动信号和地址 */
		if ((i == 0) || (usAddr & (EEPROM_PAGE_SIZE - 1)) == 0)
		{
			/* 第0步:发停止信号,启动内部写操作 */
			i2c_Stop();
			
			/* 通过检查器件应答的方式,判断内部写操作是否完成, 一般小于 10ms 			
				CLK频率为200KHz时,查询次数为30次左右
			*/
			for (m = 0; m < 1000; m++)
			{				
				/* 第1步:发起I2C总线启动信号 */
				i2c_Start();
				
				/* 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
				i2c_SendByte(EEPROM_DEV_ADDR | EEPROM_I2C_WR);	/* 此处是写指令 */
				
				/* 第3步:发送一个时钟,判断器件是否正确应答 */
				if (i2c_WaitAck() == 0)
				{
					break;
				}
			}
			if (m  == 1000)
			{
				goto cmd_fail;	/* EEPROM器件写超时 */
			}
		
			/* 第4步:发送字节地址,24C02只有256字节,因此1个字节就够了,如果是24C04以上,那么此处需要连发多个地址 */
			i2c_SendByte((uint8_t)usAddr);
			
			/* 第5步:等待ACK */
			if (i2c_WaitAck() != 0)
			{
				goto cmd_fail;	/* EEPROM器件无应答 */
			}
		}
	
		/* 第6步:开始写入数据 */
		i2c_SendByte(_pWriteBuf[i]);
	
		/* 第7步:发送ACK */
		if (i2c_WaitAck() != 0)
		{
			goto cmd_fail;	/* EEPROM器件无应答 */
		}

		usAddr++;	/* 地址增1 */		
	}
	
	/* 命令执行成功,发送I2C总线停止信号 */
	i2c_Stop();
	return 1;

cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */
	/* 发送I2C总线停止信号 */
	i2c_Stop();
	return 0;
}

六、硬件I2C

STM32具有自己的I2C外设,相比模拟I2C需要自己从头造轮子,硬件I2C只需要配置好相关参数,就可以用库函数实现通讯了,软件编写得到了简化。

1、I2C外设架构

在这里插入图片描述

(1)引脚

SMBA:用于SMBus的警告信号,忽略。

(2)时钟控制逻辑

在这里插入图片描述F/S:I2C主模式选项
0:标准模式的I2C(100kbps); 1:快速模式的I2C(400kbps)。
DUTY:快速模式时的占空比,差别不大,随便选
CCR:配置具体通讯速率,F/S只是规定了速率上限。

例如,我们的 PCLK1=36MHz,想要配置 400Kbit/s 的速率,计算方式如下:
PCLK 时钟周期: TPCLK1 =1/36000000
目标 SCL 时钟周期: TSCL = 1/400000
快速模式中 Tlow/Thigh=2 情况下,
SCL时钟周期内的高电平时间: THIGH = TSCL/3
SCL 时钟周期内的低电平时间: TLOW = 2*TSCL/3
计算 CCR 的值: CCR = THIGH/TPCLK1 = 30
计算结果得出 CCR 为 30,向该寄存器位写入此值则可以控制 IIC 的通讯速率为 400KHz。

上面这些计算都不需要我们亲自做,库函数自己就做了,我们只要写入目标速率就行。

(3)数据控制逻辑

在这里插入图片描述
DR:8位数据寄存器,用于存放接收到的数据或放置用于发送到总线的数据

(4)整体控制逻辑

在这里插入图片描述
SWRST:软件复位
ACK:应答使能
SMBUS:为0时启用I2C模式
PE:I2C模块使能
F/S:I2C主模式选项

在这里插入图片描述
DMAEN:DMA请求使能
ITBUFEN:置1时当TxE=1或RxNE=1时,产生事件中断
在这里插入图片描述
AF:应答失败
TxE:数据寄存器为空(发送时)
RxNE:数据寄存器非空(接收时)
BTF:字节发送结束
ADDR:地址已被发送(主模式),为1时表示地址已经发送
SB:起始位(主模式),为1时表示起始条件已发送

在这里插入图片描述

2、通讯过程

(1)主机发送数据过程

在这里插入图片描述
主机发送起始信号,产生事件EV5,SB=1表示起始信号已发送;

发送设备地址,等待应答,若有从机应答,产生EV6(地址已发送)、EV8(数据寄存器为空);

写入数据data1,这时产生EV8表示数据寄存器非空,数据传输完毕又会产生EV8表示数据寄存器为空,故data1下面的EV8其实有两次;

重复上述过程,发送多个字节;

发送完成,主机发送停止信号,会产生EV8_2 事件,SR1 的 TXE 位及 BTF 位都被置 1,表示通讯结束。

假如我们使能了 I2C 中断,以上所有事件产生时,都会产生 I2C 中断信号,进入同一
个中断服务函数,到 I2C 中断服务程序后,再通过检查寄存器位来判断是哪一个事件。

(2)主机接收数据过程

在这里插入图片描述
与写数据类似。

3、程序编写

同样先设置SDA、SCL为开漏输出模式

随后设置I2C工作参数

static void I2C_Mode_Configu(void)
{
  I2C_InitTypeDef  I2C_InitStructure;
  /* I2C 模式 */
  I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
	/* 设置SCL 时钟线的占空比为2:1 */
  I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
  /* 设置STM32自己的I2C设备地址 */
  I2C_InitStructure.I2C_OwnAddress1 =I2Cx_OWN_ADDRESS7; 
  /* 使能响应*/
  I2C_InitStructure.I2C_Ack = I2C_Ack_Enable ;
	/* I2C地址长度 */
  I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
	/* 通信速率 */
  I2C_InitStructure.I2C_ClockSpeed = I2C_Speed;
	/* I2C 初始化 */
  I2C_Init(EEPROM_I2Cx, &I2C_InitStructure);
	/* 使能 I2C */
  I2C_Cmd(EEPROM_I2Cx, ENABLE);   
}

配置完后就可以使用库函数完成读写数据了

void I2C_DeInit(I2C_TypeDef* I2Cx);//恢复默认设置
void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);//初始化参数
void I2C_StructInit(I2C_InitTypeDef* I2C_InitStruct);//填充默认值到I2C初始化结构体
void I2C_Cmd(I2C_TypeDef* I2Cx, FunctionalState NewState);//使能I2C外设
void I2C_DMACmd(I2C_TypeDef* I2Cx, FunctionalState NewState);//使能DMA
void I2C_DMALastTransferCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState);//起始信号
void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState);//停止信号
void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);//使能响应
void I2C_OwnAddress2Config(I2C_TypeDef* I2Cx, uint8_t Address);//设置STM32上I2C设备自己的地址
void I2C_DualAddressCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);//双地址模式
void I2C_GeneralCallCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_ITConfig(I2C_TypeDef* I2Cx, uint16_t I2C_IT, FunctionalState NewState);//使能中断
void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data);//发数据
uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx);//接收数据
void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction);//发送7位地址
uint16_t I2C_ReadRegister(I2C_TypeDef* I2Cx, uint8_t I2C_Register);//读寄存器
void I2C_SoftwareResetCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);//软件复位使能
void I2C_NACKPositionConfig(I2C_TypeDef* I2Cx, uint16_t I2C_NACKPosition);
void I2C_SMBusAlertConfig(I2C_TypeDef* I2Cx, uint16_t I2C_SMBusAlert);
void I2C_TransmitPEC(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_PECPositionConfig(I2C_TypeDef* I2Cx, uint16_t I2C_PECPosition);
void I2C_CalculatePEC(I2C_TypeDef* I2Cx, FunctionalState NewState);
uint8_t I2C_GetPEC(I2C_TypeDef* I2Cx);
void I2C_ARPCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_StretchClockCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_FastModeDutyCycleConfig(I2C_TypeDef* I2Cx, uint16_t I2C_DutyCycle);//时钟占空比设置
FlagStatus I2C_GetFlagStatus(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG);//获取标志位
ITStatus I2C_GetITStatus(I2C_TypeDef* I2Cx, uint32_t I2C_IT);//检查中断是否产生

关于I2C总线在具体器件上的应用后面会单独编写,限于篇幅,这里不再列出。

七、答疑

1、I2C总线为什么要上拉?

答:因为所有的I2C设备都必须使用开漏输出模式,该模式下想要输出高电平必须加上拉电阻。

为什么一定要使用开漏模式?使用推挽输出模式不行吗?
(1)推挽输出模式会发生短路,开漏模式不会

假设使用推挽模式:
I2C设计之初就是为了实现多主机多从机通讯,如果总线上一个设备输出高电平,一个输出低电平,此时设备A与设备B之间电流流向如下图红线所示,由于MOS管的导通电阻很低,故电流I将很大,相当于短路,可能烧毁设备。
在这里插入图片描述

使用开漏输出又是什么情况呢?
同样一个设备想输出高电平,一个想输出低电平,此时总线上电流流向如下图红线所示。设备A对外输出高阻态,总线电流I被电阻R所限制。
在这里插入图片描述
(2)多设备通信仲裁需要做线与逻辑,开漏输出能做到,如果用推挽输出做线与就会造成短路。
当总线上多个器件同时传输数据时,只要有一方为0,则总线上为0。因为0与任何数都为0。

2、I2C总线优缺点

(1)优点
线束少:只有2根线。
支持多主机多从机:7位地址时总线上最多可接127个设备。
(2)缺点
传输距离小:I2C总线的最大长度是200mm~300mm。
传输速度慢:Kb级别的传输速度。

参考资料

1、https://zh.wikipedia.org/zh-cn/I%C2%B2C
2、https://www.sekorm.com/faq/54809.html
3、https://zhuanlan.zhihu.com/p/408672862
4、https://blog.csdn.net/lpwsw/article/details/121778724
5、《STM32库开发实战指南》

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值