I2C通信外设(硬件)
摘要:STM32的硬件I2C外设支持多主机模式(固定/可变)、7/10位地址、100-400kHz速率,通过移位寄存器和事件标志实现通信。发送流程涉及EV5/6/8事件检测,接收流程需处理EV7事件和应答配置。以MPU6050为例,展示了起始条件生成、地址发送、数据读写和终止条件设置等关键操作步骤,并提供了超时处理机制确保程序可靠性。初始化时需配置GPIO为复用开漏输出模式,并设置时钟、应答等参数
I2C外设介绍
1.STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU的负担
2.支持多主机模型(主机就是拥有主动控制总线的权利,从机只能在主机允许的情况下,才能控制总线,对于多主机模型又可分为固定多主机和可变多主机)
2.1固定多主机:就是总线上有两个或更多个固定的主机
2.2可变多主机:总线上没有固定的主机和从机,任何一个设备都可以在总线空闲时跳出来作为主机,指定其他任何一个设备进行通信
3.支持7位/10位地址模式(最常用的是七位地址)
4.支持不同的通讯速度,标准速度(高达100 kHz),快速(高达400 kHz)
5.支持DMA(数据转运)
6.兼容SMBus协议(系统管理总线)
7.STM32F103C8T6 硬件I2C资源:I2C1、I2C2
I2C框图

I2C基本结构

首先移位寄存器和数据寄存器DR是通信的核心部分
发送的时候:高位先行(所以是向左移位)从高位到低位,依次放到SDA线上
接收的时候:数据通过GPIO口从右边依次移进来(移动8次,一个字节就接收完成了)
之后GPIO口(使用硬件I2C)
对应的GPIO口(I2C1 I2C2)


其中I2C1可以通过重映射到PB8、PB9两个引脚
![]()
都要配置成复用开漏输出的模式(Mode)
复用:就是GPIO的状态是由片上外设来控制的
然后是SCL这里
时钟控制器通过GPIO去控制时钟线(这里简化成一主多从的模型,所以时钟这里只画了输出的方向)
如果是多主机的模型,时钟线也会进行输入的
最后是开关控制(cmd)
I2C_cmd 配置好了就使能外设,外设就能正常工作了
I2C操作流程
主机发送

产生起始条件:在控制寄存器 CR1 中,在 START 位写 1,就可以产生起始条件了
EV(Event 事件)x:来代替标志位的
1. 为什么要用 EV 来代替标志位?
因为有的状态会产生多个标志位,可以这么理解 EVx(1,2,3...等等)就是组合了多个标志位的大标志位,在库函数中也有对应的检查 EVx 事件是否发生的函数
2.EV5 事件:(意味着)SB=1 这一位置 1,表示起始条件已发送(这个状态寄存器不需要手动清零的)
3.EV6 事件:ADDR=1 这一位置 1,在主模式下表示地址发送结束
4.EV8_1 事件:TxE=1 移位寄存器空,数据寄存器空,一旦检测到 EV8_1 事件,这时需要我们写入数据寄存器 DR 进行数据发送
5.EV8 事件:TxE=1 移位寄存器非空,数据寄存器空,一旦检测到 EV8 事件,就可以写入下一个数据,最后当想要发送的数据写完后,这时就没有新的数据可以写入到数据寄存器了
6.EV8_2 事件:TxE=1 BTF=1 当检测到 EV8_2 时,就可以产生终止条件
总结:写入控制寄存器 CR 或数据寄存器 DR,控制时序单元的发生,比如:产生起始条件、发送一个字节数据等等,时序单元发生后检查相应的 EV 事件,来等待时序单元发送完成,依次按照这个流程,操作、等待、操作、等待...这样就能实现时序了
主机接收

产生起始条件:在控制寄存器 CR1 中,在 START 位写 1,就可以产生起始条件了
EV(Event 事件)x:来代替标志位的
1.EV5 事件:(意味着)SB=1 这一位置 1,表示起始条件已发送(这个状态寄存器不需要手动清零的)
2.EV6 事件:ADDR=1 这一位置 1,在主模式下表示地址发送结束
3.EV6_1 事件:数据 1 还正在移位,还没收到,所以这个事件没有标志位,当这个时序完成时硬件会根据我们的配置,把应答位发送出去
3.1.如何配置是否给应答?控制寄存器 CR 中,在 ACK(使能应答)位写 1,就在接收到一个字节后就返回一个应答,写 0,就是不给应答
4.EV7 事件:RxNE=1,表示数据寄存器非空,也就是收到一个字节的数据,但我们把数据读走后,这个事件就没有了,上面这边当 EV7 事件没有了,说明此时数据 1 被读走,当然数据 1 还没被读走时,数据 2 就可以直接移入移位寄存器了,之后,数据 2 移位完成,收到数据 2,产生 EV7 事件,读走数据 2,EV7 事件就没有了,按照这个流程,就可以一直接收数据了
5.EV7_1 事件:当我们不需要继续接收时,需要在最后一个时序单元发生时,把应答位 ACK 置 0,并且设置终止条件请求,也就是RxNE=1 ACK=0 STOP请求
总结:写入控制寄存器 CR 和读取数据寄存器 DR,产生时序单元,然后等待相应的事件,确保时序单元完成
硬件 I2C 读取 MPU6050
根据前面的介绍和框图和流程来实现读取 MPU6050
I2C 常用库函数
I2C_Init 初始化(以下是参数配置):
I2C_Mode 模式
I2C_DutyCycle 配置 SCL的时钟频率(数值越高,SCL的频率越高,最大值小于 400KHz)
I2C_DutyCycle 时钟占空比(只有在快速状态时才有用(100KHz 以上)在小于 100KHz 的标准速度下占空比是固定的 1:1)
I2C_Ack 应答位配置 (配置收到一个字节后是否给从机应答)
I2C_AcknowledgedAddress 作为从机响应几位的地址
I2C_OwnAddress1 作为从机指定自身地址,方便别的主机呼叫(暂时不需要做从机被使用的话,地址可以随便给,只要不和总线其他设备的地址重复就行)
I2C_Cmd 使能或失能
I2C_GenerateSTART 生成起始条件
I2C_GenerateSTOP 生成终止条件
I2C_AcknowledgeConfig 配置收到一个字节后是否给从机应答(初始化配置好后,想修改可以用这个函数修改)
I2C_SendData 发送数据
I2C_ReceiveData 接收数据
I2C_Send7bitAddress 发送七位地址
I2C_CheckEvent 基本状态监控 同时判断一个或多个标志位(来确定 EVx 这个状态是否发生)
I2C_GetFlagStatus 读取标志位
I2C_ClearFlag 清除标志位
I2C_GetITStatus 读取中断标志位
I2C_ClearITPendingBit 清除中断标志位
I2C 的初始化代码
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
I2C_InitTypeDef I2C_InitStructure;
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
I2C_InitStructure.I2C_ClockSpeed = 50000;
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_InitStructure.I2C_OwnAddress1 = 0x00;
I2C_Init(I2C2,&I2C_InitStructure);
I2C_Cmd(I2C2,ENABLE);
I2C 的写入(发送)
这些硬件 I2C 函数只管给寄存器的位置 1 或在 DR 写入数据,就结束,波形是否发送完毕,它是不管的,对于这种非阻塞式的程序,在函数结束后,要等待相应的标志位,来确保这个函数的操作执行到位
I2C_GenerateSTART(I2C2,ENABLE); //起始
MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT); //监测EV5事件是否发生
I2C_Send7bitAddress(I2C2,MPU6050__ADDRESS,I2C_Direction_Transmitter); //从机地址
MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); //监测EV6(主机发送)事件是否发生
I2C_SendData(I2C2,RegAddress);
MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTING); //监测EV8事件是否发生
I2C_SendData(I2C2,Data);
MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED); //监测EV8_2事件是否发生
I2C_GenerateSTOP(I2C2,ENABLE);
I2C 的读取
因为是指定地址读一个字节,所以在EV6事件之后,要把 ACK 置 0,同时把停止条件生成位 STOP 置 1
此时不是接收数据吗?数据都没收到,就要产生停止条件?
因为在接收最后一个字节之前(指定地址读一个字节的话),就要提前把 ACK 置 0,同时把停止条件生成位 STOP 置 1,时序不等人,如果没有提前的话会导致多一个字节出来
uint8_t Data;
I2C_GenerateSTART(I2C2,ENABLE); //起始
MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT); //监测EV5事件是否发生
I2C_Send7bitAddress(I2C2,MPU6050__ADDRESS,I2C_Direction_Transmitter); //从机地址
MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); //监测EV6事件是否发生
I2C_SendData(I2C2,RegAddress);
MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED); //监测EV8事件是否发生
I2C_GenerateSTART(I2C2,ENABLE); //起始
MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT); //监测EV5事件是否发生
I2C_Send7bitAddress(I2C2,MPU6050__ADDRESS,I2C_Direction_Receiver); //从机地址
MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED); //监测EV6(主机接收)事件是否发生
I2C_AcknowledgeConfig(I2C2,DISABLE); //应答位置0
I2C_GenerateSTOP(I2C2,ENABLE); //停止
MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_RECEIVED); //监测EV7事件是否发生
Data = I2C_ReceiveData(I2C2);
I2C_AcknowledgeConfig(I2C2,ENABLE);
return Data;
简单的超时退出机制
因为用了许多的死循环,为了避免卡死的超时退出机制
void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT) //超时退出
{
uint32_t Timeout;
Timeout = 10000;
while(I2C_CheckEvent(I2Cx,I2C_EVENT) != SUCCESS) //等待
{
Timeout --;
if(Timeout == 0) //超时退出机制
{
break; //跳出循环
}
}
}
更多推荐



所有评论(0)