软件I2C的两个通信引脚是可以任意更改的,软件I2C的引脚就是普通的开漏输出模式,硬件接在哪个引脚上,程序中就对应操作哪个引脚即可,但是硬件I2C,通信引脚是不可以任意指定的,我们需要查询以上引脚定义表,来进行引脚的规划,如果使用硬件的I2C1,就需要接在PB6和PB7,I2C1可以重映射,还可以更换为PB8和PB9。I2C2引脚为PB10和PB11,但没有重映射的引脚,所以I2C2只能选择PB10和PB11.
我们使用硬件I2C来读写MPU6050的应用层,也就数主函数和程序现象和上一次的软件I2C是一样的。软件I2C和硬件I2C的区别就在通信的底层,之前都是程序手动翻转引脚,现在有了硬件,这些底层的东西,就可以交给硬件来完成。
我们把MPU6050.c文件中用到软件I2C的模块先注释掉,方便等一下参考,把这些软件I2C的代码都转换成硬件I2C实现就好了。
由于我们只替换最底层的通信层,所以后面的基于通信层的芯片配置和读取数据的逻辑都不需要更改。
第一步:配置I2C外设,对I2C2外设进行初始化,来替换MyI2C_Init()
第二步:控制外设电路,实现指定地址写的时序,来替换MPU6050_WriteReg()
第三步:控制外设电路,实现指定地址读的时序,来替换MPU6050_ReadReg()
配置I2C外设参考以上两张硬件电路的框图
实现读写时序,参考上面这两张主机发送和主机接受的流程图
实现配置I2C外设并不难,第一步开启I2C外设和对应的GPIO口的时钟,第二步,把I2C外设对应的GPIO口初始化为复用开漏模式,第三步使用结构体对整个I2C进行配置,第四步,I2C_Cmd使能I2C。我们先来看一下I2C的库函数:
void I2C_DeInit(I2C_TypeDef* I2Cx);//缺省配置
void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);//初始化
void I2C_StructInit(I2C_InitTypeDef* I2C_InitStruct);//结构体赋默认值
void I2C_Cmd(I2C_TypeDef* I2Cx, FunctionalState NewState);//使能
void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState);//生成起始条件
void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState);//生成终止条件
这两个函数就是操作CR1的Start位和Stop位,分别能生成主模式和从模式下的起始条件和终止条件
void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);//配置CR1的Ack位,就是STM32作为主机在收到一个字节是给从机应答还是非应答,0无应答返回,1返回一个应答
void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data);//就是把Data直接写入到DR寄存器中,DR用于存放接收到的数据或放置用于发送到总线的数据。在发送模式下,当写入一个字节至DR寄存器中,自动启动数据传输,一旦传输开始(Tx = 1),如果能及时,把下一个需传输的数据写入DR寄存器,I2C模块将保持连续的数据流。
uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx);//这个函数就是读取DR的数据,作为返回值。DR在接收器模式下,接收到的字节被拷贝到DR寄存器(RxNE = 1)。在接收到下一个字节(RxNE = 1)之前读出数据寄存器,即可实现连续的数据传送。
void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction);//发送7位地址的专用函数,Address参数也是通过DR发送的,只不过在发送之前帮我们设置了Address最低的读写位,也可以调用之前的SendData来发送地址
如上硬件时序图中的几个EV事件,STM32有时候可能同时置多个标志位,如果只检查某一个标志位,就认为这个状态已经发生了会不太严谨,而如果用GetFlagStatus函数读多次,再进行判断又比较麻烦,所以这里库函数给了我们多种监控标志位的方案。
其中第一种:基本状态监控
使用I2C_CheckEvent函数,这种方式是同时判断一个或多个标志位,来确定EV事件的几个状态是否发生,
第二种:高级状态监控
使用I2C_GetLastEvent函数,直接把SR1和SR2两个状态寄存器拼接成32位的数据返回。
第三种:基于状态位的状态监控
使用I2C_GetFlagStatus函数,就是我们一直使用的方法,可以判断某一个标志位是否置1了。
FlagStatus I2C_GetFlagStatus(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG);
void I2C_ClearFlag(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG);
ITStatus I2C_GetITStatus(I2C_TypeDef* I2Cx, uint32_t I2C_IT);
void I2C_ClearITPendingBit(I2C_TypeDef* I2Cx, uint32_t I2C_IT);
比较常用的获取状态寄存器标志位、中断标志位,及清除其标志位。
到此库函数都看完了,接下来开始写硬件I2C的代码,首先是要对硬件I2C进行初始化
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);//注意I2C1和I2C2都是APB1的外设
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;//复用开漏模式,复用就是GPIO的控制权交给硬件外设
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11 | GPIO_Pin_10;
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;//标准速度100KHz,最高400KHz
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;//时钟占空比参数,只有在时钟频率大于100KHz也就是进入快速状态时才有用
//否则就是固定的1:1,就是高电平时间比低电平时间约为1:1
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;//应答位配置,用于确定在接收一个字节后是否给从机应答
//之后需要更改的话,可以在使用另一个单独的函数修改
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;//指定STM32作为从机可以响应几位的地址,可以选10或7位地址
I2C_InitStructure.I2C_OwnAddress1 = 0x00;//自身地址1,也是STM32作为从机使用的,前面选几位这里填几位
I2C_Init(I2C2, &I2C_InitStructure);
I2C_Cmd(I2C2, ENABLE);
这是100kHz时钟频率的I2C波形图,我们可以注意到波形图中,下降沿很迅速,是一个比较垂直的形态,而上升沿因为是弱上拉电阻,所以回弹上升沿的过程会比较缓慢,而这个现象在更高频率的时钟状态下更为明显。下面是101kHz的波形
101kHz和100kHz差不多,但是101kHz已经进入了快速状态了,这是I2C会对SCL占空比进行调节,低电平比高电平由原来的1:1变为2:1,增大了低电平时间占整个周期的比例,为什么要增大低电平的比例,因为低电平数据变化,高电平数据读取,数据变化需要一定时间来翻转波形,尤其是数据的上升沿,变化比较慢,所以在快速传输的状态下,要给低电平多分配一些资源,不然低电平数据变化来不及,高电平数据读取也没用
这是400kHz频率下的波形,可以发现这时的波形已经十分明显了,SCl还没完全回弹到高电平,马上就被低电平拉下去了,在快速传输过程中,这个回弹高电平的延时就限制了I2C总线的最大传输速度。
接下来我们来替换写寄存器的内容,也就是指定写一个字节的时序
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
I2C_GenerateSTART(I2C2, ENABLE);//替换MyI2C_Start(),软件I2C的这些函数内部都有Delay操作
//都是一种阻塞式的流程,也就是函数运行完成之后,对应的波形肯定发送完毕了,所以上一个函数运行完后
//就可以紧接着下一个函数,但是下一个函数,包括之后的硬件I2C函数,都不是阻塞式的
//这些硬件函数只管给寄存器的位置置1,或者只在DR写入数据,就结束退出函数,至于波形是否发送完毕是不管的
//所以对这种非阻塞式的程序,在函数结束后,我们都要等待相应的标志位,来确保函数的操作执行到位了。
//参考上图,当起始条件的波形确实发出了,会产生EV5事件,所以程序中,我们要等待EV5事件的到来
while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);
//因为STM32默认为从机,发送起始条件后变为主机,所以EV5时间也可以叫做主机模式已选择事件
//返回值为SUCCESS表示最后一次事件等于我们指定的事件,ERROR表示指定时间没发生
//这种while循环加多了,一旦总线出问题就很容易造成整个程序卡死,所以我们需要设计一个超时退出的机制
I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);//发送数据都自带了接受应答的过程
//同样接收数据也自带了发送应答的过程,如果应答错误,硬件会通过标志位和中断来提示,所以发送地址后应答位不需要处理
while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS);
//当地址发出接收应答位之后,就会产生EV6事件
//EV6事件之后有一个EV8_1事件,这个事件是提示该写入DR发送数据了,我们并不需要等待EV8_1事件,库函数里也没有这个参数
//所以这时我们就直接写入DR,发送数据
I2C_SendData(I2C2, RegAddress);
while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS);//EV8
I2C_SendData(I2C2, Data);
while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS);
//当我们有连续的数据需要发送时,在发送过程中,我们需要等待EV8事件,而当我们发送完最后一个字节时
//需要等待的就是EV8_2事件了,我们需要等待硬件把两级缓存,所有数据都清空才能产生终止条件
I2C_GenerateSTOP(I2C2, ENABLE);
}
接下来就是读寄存器函数,前面读取地址的部分,和写寄存器的部分是一样的

Timeout = 10000;
{
Timeout–;
if (Timeout) {break;}
}
{
uint32_t Timeout;
Timeout = 10000;
while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS)
{
Timeout–;
if (Timeout) {break;}
};
}