2.34 硬件I2C读写MPU6050

MPU6050接线图

I2C库函数

I2C库函数

1. `I2C_DeInit(I2C_TypeDef* I2Cx);`

– **用途**:将指定的I2C外设寄存器重置为默认值。
– **作用**:通常用于在重新配置I2C之前进行复位。

2. `I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);`

– **用途**:根据指定的参数初始化I2C外设。
– **参数**:`I2C_InitStruct` 包含I2C的时钟速度、模式、地址等配置。

3. `I2C_StructInit(I2C_InitTypeDef* I2C_InitStruct);`

– **用途**:用默认值初始化 `I2C_InitTypeDef` 结构体。
– **作用**:为配置结构体提供初始值,确保参数有效性。

4. `I2C_Cmd(I2C_TypeDef* I2Cx, FunctionalState NewState);`

– **用途**:使能或禁用指定的I2C外设。
– **参数**:`NewState` 可以是 `ENABLE` 或 `DISABLE`。

5.`I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState);` 是用于在I2C通信中生成起始条件的函数。

### 用途

– **功能**:生成I2C总线的起始信号。
– **参数**:
– `I2Cx`:指定要使用的I2C外设(如I2C1, I2C2)。
– `NewState`:使用 `ENABLE` 生成起始条件,`DISABLE` 停止生成。

### 作用

– **起始条件**:用于开始I2C通信,通知从设备接收数据。

6.`I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState);` 是用于在I2C通信中生成停止条件的函数。

### 用途

– **功能**:生成I2C总线的停止信号。
– **参数**:
– `I2Cx`:指定要使用的I2C外设(如I2C1, I2C2)。
– `NewState`:使用 `ENABLE` 生成停止条件。

### 作用

– **停止条件**:用于结束I2C通信,释放总线。

7. `I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);`

– **用途**:配置I2C的应答位。
– **参数**:
– `I2Cx`:指定I2C外设。
– `NewState`:`ENABLE` 启用应答,`DISABLE` 禁用应答。

8. `I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data);`

– **用途**:通过I2C总线发送一个字节的数据。
– **参数**:
– `I2Cx`:指定I2C外设。
– `Data`:要发送的字节数据。

9. `uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx);`

– **用途**:从I2C总线接收一个字节的数据。
– **返回值**:接收到的字节数据。

10. `I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction);`

– **用途**:发送7位从设备地址。
– **参数**:
– `I2Cx`:指定I2C外设。
– `Address`:要发送的从设备地址。
– `I2C_Direction`:指定传输方向(发送或接收)。

硬件I2C代码

MPU6050初始化

//初始化MPU6050
void MPU6050_Init(void)
{
	//开启I2C外设和GPIO口的时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	//初始化GPIO口
	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外设
	I2C_InitTypeDef I2C_InitStructure;
	I2C_InitStructure.I2C_Ack=I2C_Ack_Enable;//默认给应答,之后可以用单独函数进行更改。
	I2C_InitStructure.I2C_AcknowledgedAddress=I2C_AcknowledgedAddress_7bit;//这个是STM32作为从机,可以响应几位地址
	I2C_InitStructure.I2C_ClockSpeed= 50000;//配置SCL的时钟频率50KHz
	I2C_InitStructure.I2C_DutyCycle=I2C_DutyCycle_2;//时钟占空比,只有在时钟频率大于100KHz,进入快速状态时才有用。在小于等于100KHz的情况下,占空比是固定的1:1
	I2C_InitStructure.I2C_Mode=I2C_Mode_I2C; //I2C模式
	I2C_InitStructure.I2C_OwnAddress1=0X00;//这个是STM32作为从机,用于指定STM32的自身地址。
	I2C_Init(I2C2,&I2C_InitStructure);
	I2C_Cmd(I2C2,ENABLE);
	
	/*MPU6050寄存器初始化,需要对照MPU6050手册的寄存器描述配置,此处仅配置了部分重要的寄存器*/
	MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);		//电源管理寄存器1,取消睡眠模式,选择时钟源为X轴陀螺仪
	MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);		//电源管理寄存器2,保持默认值0,所有轴均不待机
	MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);		//采样率分频寄存器,配置采样率
	MPU6050_WriteReg(MPU6050_CONFIG, 0x06);			//配置寄存器,配置DLPF
	MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);	//陀螺仪配置寄存器,选择满量程为±2000°/s
	MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);	//加速度计配置寄存器,选择满量程为±16g
}

I2C_InitStructure.I2C_DutyCycle 是时钟占空比,只有在时钟频率大于100KHz,进入快速状态时才有用。在小于等于100KHz的情况下,占空比是固定的1:1。

那为什么要设置这个占空比呢?I2C改用这个占空比有什么用呢?

对SCL占空比进行调节之后,低电平:高电平 我们设置 2:1,增大了低电平时间占整个周期的比例,为什么要增大低电平的比例呢?因为低电平数据变化,高电平数据读取,数据变化需要一定时间来翻转波形,尤其是上升沿,变化相对来说比较缓慢,所以,在快速传输的状态下,需要给低电平分配多一些资源,要不然低电平数据变化来不及,你高电平数据读取也没用。

指定地址写

//指定地址写
void MPU6050_WriteReg(uint8_t RegAddress,uint8_t Data)
{
	//产生起始条件
	I2C_GenerateSTART(I2C2,ENABLE);
	//判断EV5事件是否到来
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT) == ERROR);
	/*----------------------------------*/
	
	//发送从机地址(发送一个字节)
	I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Transmitter);//发送最低位清零,接收最低位置1.
	//接受应答
	/* 在库函数中并不需要接收应答,发送数据都自带了接受应答的过程;同样,接收数据也自带了发送应答的过程。
	如果应答错误,硬件会通过标志位和中断来提醒我们。*/
	//判断EV6事件是否到来
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) == ERROR);
	
	/*----------------------------------*/
	//写入DR,发送数据。
	I2C_SendData(I2C2,RegAddress);
	//判断EV8事件是否到来(中间字节发送字节需要等待EV8事件)
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTING) == ERROR);
	
	/*----------------------------------*/
	//写入DR,发送数据。
	I2C_SendData(I2C2,Data);
	//判断EV8_2事件是否到来(最后一个字节发送完后,需要等待EV8_2事件)
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED) == ERROR);
	
	/*----------------------------------*/
	//产生终止条件
	I2C_GenerateSTOP(I2C2,ENABLE);
}

 指定地址读

这个时序是当前地址读的时序,指定地址读的这里并没有给,需要我们自己组合一下。

我们继续看7位主接收的数据1这里,代表数据正在通过移位寄存器进行输入,同时会发生一个EV6_1事件,看图可知,EV6_1发生时,其实数据1还在移位寄存器里移位,并没有完成移位。

当我们发送完字节不需要继续接收的时候,需要在最后一个时序单元发生时,提前把ACK置0,并设置终止条件(EV7_1事件)。

为什么要提前把ACK置0并设置终止条件?

如果你不提前在数据还没有收到的时候给ACK置0,那等数据时序完全接收完得到应答后再设置的话,你再说你要设置ACK置0,你要给非应答,这时就完(晚)啦!数据收到之前,应答位就已经发送出去了,你这时再给ACK置0,那只能是在下一个数据之后给非应答了,时序不等人,所以要提前设置ACK=0。

//指定地址读
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
	uint8_t Data;
	//产生起始条件
	I2C_GenerateSTART(I2C2,ENABLE);
	//判断EV5事件是否到来
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT) == ERROR);
	
	/*----------------------------------*/
	//发送从机地址(发送一个字节)
	I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Transmitter);//发送最低位清零,接收最低位置1.
	//接受应答
	/* 在库函数中并不需要接收应答,发送数据都自带了接受应答的过程;同样,接收数据也自带了发送应答的过程。
	如果应答错误,硬件会通过标志位和中断来提醒我们。*/
	//判断EV6事件是否到来
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) == ERROR);
	
	/*----------------------------------*/
	//写入DR,发送数据。
	I2C_SendData(I2C2,RegAddress);
	//判断EV8_2事件是否到来(最后字节发送字节需要等待EV8_2事件)
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED) == ERROR);
	
	/*----------------------------------*/
	//产生重复起始条件
	I2C_GenerateSTART(I2C2,ENABLE);
	//判断EV5事件是否到来
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT) == ERROR);
	
	/*----------------------------------*/
	//发送从机地址(发送一个字节)
	I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Receiver);//发送最低位清零,接收最低位置1.
	//等待EV6事件
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED) == ERROR);
	
	/*----------------------------------*/
	//主机不给应答(接收之前提前设置ACK和STOP)
	I2C_AcknowledgeConfig(I2C2,DISABLE);
	//发送停止位
	I2C_GenerateSTOP(I2C2,ENABLE);
	
	/*----------------------------------*/
	//等待EV7事件,EV7事件产生后,就代表接收到了这一字节的代码
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_RECEIVED) == ERROR);
	//读取数据DR
	Data = I2C_ReceiveData(I2C2);
	
	/*----------------------------------*/
	//最后将ACK置为1,为什么这样做呢?
	/*我们的想法是,默认状态下,ACK是1,给从机应答,在收最后一个字节之前,临时把ACK置0,给非应答;所以
	要在接收函数的最后,要恢复默认的ACK = 1,方便以后接收多个字节。*/
	I2C_AcknowledgeConfig(I2C2,ENABLE);
	return Data;
	
}

函数代码优化

我们在上述函数中使用了大量的while函数,这样对于整个工程来讲是致命的,很可能会在某个条件产生一个死循环,所以要对while函数进行如下优化:

	uint32_t Time;
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT) == ERROR)
	{
		Time--;
		if(Time == 0)
		{
			return;
		}
	}

这里可以自己加上报错的数值,这样代码更加完善,当然这里就不再写错误处理对应的内容了。