STM32学习:深入理解STM32的I2C外设

目录

前言

一、I2C外设

二、硬件I2C操作流程

1.主机发送时序

3.其他时序

4.拓展:软/硬件I2C波形对比

三、硬件I2C读写MPU6050

总结


前言

声明:学习笔记来自江科大自化协B站教程,仅供学习交流!!

上接上次学习的I2C协议和软件读写MPU6050,接下来继续学习STM32的I2C外设和硬件读写I2C!姿态传感器在平衡车、无人机等方面应用广泛!

不同于串口,因为I2C位同步通信软件模拟协议应用也很广泛!但硬件实现协议也具有独特优势:执行效率高节省软件资源、功能强大可实现完整的多主机通信模型、时序波形规整、通信速率快等。本次学习只需掌握一主多从、7位地址模式即可。


一、I2C外设

概述

1、STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU的负担。软件只需要写入控制寄存器CR和数据寄存器DR,就可实现协议,当然,为了实现实时监控时序的状态,软件还需要读取状态寄存器SR。

2、支持多主机模型。分为固定多主机和可变多主机,“固定”就是某几个设备一直是主机,“可变”就是随时可能一个设备主从切换,冲突时进行总线仲裁。STM32的I2C是可变多主机设计。先了解!

3、支持7位/10位地址模式。如果设备超过128个或者同一型号的芯片太多时,就要用到10位地址模式,1024种可能。前两个字节都用于寻址,多的位是标志位。

4、支持不同的通讯速度,标准速度(高达100 kHz),快速(高达400 kHz) 。

5、支持DMA 。多字节传输是可提升传输效率,比如指定地址读/写多字节的时序,用DMA自动帮忙转运数据。先了解!

6、兼容SMBus协议。System Management Bus,系统管理总线,是基于I2C总线改进而来,主要用于电源管理系统。先了解!

STM32F103C8T6 硬件I2C资源:I2C1、I2C2。只可有两路硬件I2C总线,但软件I2C一般没限制。

框图

        

左边为SDA和SCL引脚,SMBALERT是SMBus用的。这些外设的引脚一般都借助GPIO的复用模式,比如根据引脚定义表I2C1_SCL为PB6,I2C1_SDA为PB7,I2C2_SCL为PB10,I2C2_SDA为PB11。

绿框是与SDA相关的数据控制部分,核心是DATA REGISTER 数据移位寄存器。当需要发送数据时,可将数据写到DR,当移位寄存器没有数据移位时,DR中的值(字节)就会进一步转到移位寄存器里;在数据移位时,就可将下一个数据放到DR里等着了,无缝衔接。当数据由DR转到移位寄存器时,会置状态寄存器SR的TXE位为1,表示发送寄存器为空。输入数据时,数据一位一位地从SDA引脚移动到移位寄存器里,当一个字节收齐后数据就整体从移位寄存器转到DR,同时置标志位RXNE,表示接收寄存器非空,这时就可把数据从DR读出。与串口相似,只不过因为串口时全双工,发送和接收部分是分开设计的。至于什么时候接收、什么时候发送、起始位、终止位、应答位等需要写入控制寄存器CR的对应位完成,数据控制电路会完成对应功能。这部分重点掌握!

数据收发之后还有两个功能,一个是比较器和自身地址寄存器、双地址寄存器,另一个是帧错误校验(PEC)寄存器和帧错误校验(PEC)计算。第一个功能是从机模式时使用的,当STM32作为从机时,自身地址寄存器指定自己的从机地址,比较器用于比较外部召唤地址和自身从机地址,并且STM支持同时响应两个从机地址,所以多了个双地址寄存器。第二个功能用于数据校验,当发送一个多字节的数据帧时,硬件可自动执行CRC校验计算,CRC是一种很常见的数据检验算法,最后得到一个数据检验位附加在数据后面,接收到数据后STM32硬件也可自动执行校验的判定,错误的话会置数据错误标志位,类似于串口奇偶校验。这部分先了解即可,下面暂时用不到!

篮框,写入时钟控制寄存器,时钟控制电路就会执行对应功能;写入控制寄存器,控制逻辑电路可对整个电路进行控制;读取状态寄存器,可得知电路的工作状态;中断,当内部一些标志位置1后可申请中断,如果开启了中断,事件发生后程序就可跳到中断函数处理此事件;DMA请求与响应,指定地址读/写多字节的时序,用DMA自动帮忙转运数据,提高效率!

I2C外设使用最简图(一主多从)

二、硬件I2C操作流程

下面的内容一定要对照芯片手册理解,24章关于CR、DR、SR寄存器每一位的介绍!

同时写程序时也要对应着图写!

1.主机发送时序

7位主发送,起始条件后的一个字节是寻址;10位主发送,起始条件后的两个字节是寻址,前一个字节帧头内容是5位标志位11110+2位地址+1位读写位,后一个字节就是纯粹的8位地址,构成10位寻址。

首先初始化之后总线默认空闲状态,STM32默认是从模式,为了产生一个起始条件,STM32需要写入控制寄存器(Start位写1启动,起始条件发出后,硬件自动将其清0),之后STM32由从模式转为主模式(也就是多主机模型下,STM32有数据要发,就要跳出来)。控制完硬件电路后,要检查标志位,查看硬件是否达到想要的状态:起始位之后会发生EV5(Event5)事件,可把它当作一个组合多标志位的大标志位,库函数里也有检查EVx事件是否发生的函数,见图解释和芯片手册24.6.6状态寄存器。(BTF:Byte Transfer Finished)

整个时序过程看着操作比较多,但是简单来说就是:写入控制寄存器CR或者数据寄存器DR,就可控制时序单元的发生。查看EVx事件需要查看状态寄存器SR。用库函数操作很简单!!

2.主机接收时序

上图给的时序是当前地址读的时序,指定地址读的复合格式这里没有给需要我们自己根据原理拼接!

3.其他时序

手册中还有从机发送和从机接收两种时序,不过这是在多主机模式下的,STM32作为从机时相关的,本次学习不涉及!

4.拓展:软/硬件I2C波形对比

 以上是指定地址读的时序,从引脚电平变化趋势上看,两个波形一样,对应数据也一样。

从时钟线规整程度看,硬件I2C的更加规整,周期和占空比非常一致;而软件I2C操作引脚之后都加了延时,造成不规整,不过因为I2C为同步时序,SCL线不规整也不影响。

之前学过,SCL低电平写,高电平读,虽然整个电平的任意时候都可读写,但一般要保证尽早原则,所以可认为SCL下降沿写,上升沿读。软件I2C在下降沿之后,因为操作端口后会有些延时,所以等了一会才写;但在硬件I2C里数据写入都紧贴着SCL下降沿,读也是紧贴上升沿(图上划线有点不准)。

三、硬件I2C读写MPU6050

接线图

接线图与上次的软件读写一样,但是这里的GPIO接口不可随意更改!这里PB10、PB11说明用的I2C2

程序代码,与软件I2C不同的在于通信的底层MyI2C.c文件!这里不需要它了,移除掉!那么MPU6050.c里用到MyI2C.c的地方也要换做硬件配置实现,其余的对于MPU650的1配置和读取数据代码不用更改

#include "stm32f10x.h"                  
#include "MPU6050_Reg.h"

#define MPU6050_ADDRESS  0xD0

void MPU6050_WriteReg(uint8_t RegAddress,uint8_t Data){//指定地址写函数,控制外设电路实现
//	MyI2C_Start();
//	MyI2C_SendByte(MPU6050_ADDRESS);
//	MyI2C_ReceiveAck();
//	MyI2C_SendByte(RegAddress);
//	MyI2C_ReceiveAck();
//	
//	MyI2C_SendByte(Data);
//	MyI2C_ReceiveAck();
//
//	MyI2C_Stop();
	//此处参考学习笔记注意事项1
	I2C_GenerateSTART(I2C2,ENABLE);
    while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);//等待EV5
	
	I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS ,I2C_Direction_Transmitter);//自带接收应答过程(如果错误会通过置标志位或中断提示)
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS);//等待EV6
	
	//EV8_1事件不需要等待,它是告诉该写入DR发送数据了,下一步直接写入数据
	I2C_SendData(I2C2,RegAddress);
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS);//等待EV8
	
	I2C_SendData(I2C2,Data);//这里就发送1个字节
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS);//最后等待EV8_2事件
	
	I2C_GenerateSTOP(I2C2,ENABLE);
	
	
}

uint8_t MPU6050_ReadReg(uint8_t RegAddress){//指定地址读函数,控制外设电路实现
	uint8_t Data;
//	MyI2C_Start();
//	MyI2C_SendByte(MPU6050_ADDRESS);
//	MyI2C_ReceiveAck();
//	MyI2C_SendByte(RegAddress);
//	MyI2C_ReceiveAck();
	I2C_GenerateSTART(I2C2,ENABLE);
    while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);//等待EV5
	I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS ,I2C_Direction_Transmitter);//第三个参数为读写标志
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS);//等待EV6
	I2C_SendData(I2C2,RegAddress);
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS);//等待EV8	
//	MyI2C_Start();
//	MyI2C_SendByte(MPU6050_ADDRESS | 0x01);
//	MyI2C_ReceiveAck();
//	Data = MyI2C_ReceiveByte();
//	MyI2C_SendAck(1);
//	MyI2C_Stop();
	I2C_GenerateSTART(I2C2,ENABLE);
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);//等待EV5
	I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Receiver);//第三个参数为读写标志
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED) != SUCCESS);//等待EV6
	
	//此处参考下学习笔记注意事项2
	I2C_AcknowledgeConfig(I2C2,DISABLE);
	I2C_GenerateSTOP(I2C2,ENABLE);//这两步按规定提前到了接收一个字节之前
	//...接收一个字节过程
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_RECEIVED) != SUCCESS);//等待EV7。等待到EV7后,一个字节的数据就在DR里面了
	Data = I2C_ReceiveData(I2C1);//读取数据
	
	I2C_AcknowledgeConfig(I2C2,ENABLE);//默认情况下ACK为1,方便之后改进代码接收多个字节
	return Data;
}

/*************************************************************************************/


void MPU6050_Init(void){
    //MyI2C_Init();
	//配置硬件I2C2外设,实现初始化:
	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;//最大400kHz
	I2C_InitStructure.I2C_DutyCycle= I2C_DutyCycle_2;//这个占空比是为了快速传输设计的
	I2C_InitStructure.I2C_Ack= I2C_Ack_Enable;
	I2C_InitStructure.I2C_AcknowledgedAddress= I2C_AcknowledgedAddress_7bit;//STM32作为从机时的响应地址位数
	I2C_InitStructure.I2C_OwnAddress1= 0x00;//STM32作为从机时,设定的从机地址
	I2C_Init(I2C2,&I2C_InitStructure);
	
	I2C_Cmd(I2C2,ENABLE);
	//硬件I2C初始化完毕
	
	MPU6050_WriteReg(MPU6050_PWR_MGMT_1,0x01);
	MPU6050_WriteReg(MPU6050_PWR_MGMT_2,0x00);
	MPU6050_WriteReg(MPU6050_SMPLRT_DIV,0x09);
	MPU6050_WriteReg(MPU6050_CONFIG	,0x06);
	MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x18);
	MPU6050_WriteReg(MPU6050_ACCEL_CONFIG,0x18);
}

void MPU6050_GetData(int16_t *AccX,int16_t *AccY,int16_t *AccZ,
	                 int16_t *GyroX,int16_t *GyroY,int16_t *GyroZ)
{
	uint16_t DataH, DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);
	*AccX = (DataH << 8)|DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
	*AccY = (DataH << 8)|DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
	*AccZ = (DataH << 8)|DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
	*GyroX = (DataH << 8)|DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
	*GyroY = (DataH << 8)|DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
	*GyroZ = (DataH << 8)|DataL;
}

uint8_t MPU6050_GetID(void){
	return MPU6050_ReadReg(MPU6050_WHO_AM_I	);
}

注意事项1:上面被注释掉的软件I2C,每一步内部都有Delay操作,是一种阻塞式流程,也就是(每一步)函数运行完毕之后对应的波形肯定也发送完毕,函数之间紧跟。而下面的硬件I2C的(每一步)函数,只管给特定寄存器位置置1,至于波形是否发送它们不管,属于非阻塞模式,所以在后面都要等待相应的标志位,来确保这个函数执行到位。

注意事项2:此处进入到主机接收的模式后,就开始接收从机发送的数据波形了,对照时序图在接收一个数据字节时有个EV6_1事件,此事件没有标志位也不需要等待,适合接受一个字节的情况,我们此处演示也正是接收一个字节,要清除响应和停止条件的产生位。也就是此时要把应答位ACK置0,同时把停止条件生成位STOP置1。难道在没接收到一个字节就要把停止位置1?是的,这里规定就要提前把ACK置0,STOP置1。再接收一个字节。

注意事项3:可见程序中出现大量while死循环等待标志位,这种大量死循环对于程序十分危险,一旦有个事件一直没有产生,就会使整个程序卡死,所以在死循环等待后面可以加上一个超时退出机制,以第一个while死循环为例改进:

uint32_t Timeout;

I2C_GenerateSTART(I2C2,ENABLE);
Timeout = 10000;//实际需要多长时间可实验下
while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS){
    Timeout--;
    if(Timeout == 0){
        break;
        }
}

不过这样写不太美观,封装一个函数:

void MPU6050_WaitEnvent(I2C_TypeDef *I2Cx, uint32_t I2C_EVENT){
    uint32_t Timeout;
    I2C_GenerateSTART(I2C2,ENABLE);
    Timeout = 10000;//实际需要多长时间可实验下
    while(I2C_CheckEvent(I2Cx,I2C_EVENT) != SUCCESS){//传递参数,可通用
        Timeout--;
        if(Timeout == 0){
            break;//当然这里可写一些紧急处理函数,比如显示报错等
            }
    }

}
...

//然后把while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);改为
MPU6050_WaitEnvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT);

...

总结

遇到挫折,要有勇往直前的信念,马上行动,坚持到底,决不放弃,成功者决不放弃,放弃者绝不会成功。成功的道路上,肯定会有失败;对于失败,我们要正确地看待和对待,不怕失败者,则必成功;怕失败者,则一无是处,会更失败。
今天的学习分享到此就结束了,我们下次再见!!

往期精彩:
STM32定时器输入捕获(IC)
STM32定时器输出比较(PWM波)
STM32定时中断
STM32外部中断
STM32GPIO精讲

物联沃分享整理
物联沃-IOTWORD物联网 » STM32学习:深入理解STM32的I2C外设

发表评论