STM32使用快速硬件IIC连续多字节读取MPU6050,帮你有效节省CPU资源。
STM32使用快速硬件IIC读取连续多字节读取MPU6050,帮你有效节省CPU资源。
文章目录
前言
当我在家自己调我的小破平衡车的时候,我发现用江科大的软件IIC读取MPU6050实在只有点太消耗资源了,完成一轮完整的读取AX,AY,AZ,GX,GY,GZ大约需要13ms还要多,这对我平衡车的角速度环非常不友好,于是我决定换成硬件IIC,后面在温习了江科大的硬件IIC读取MPU6050的视频之后,先是使用了硬件IIC,后面又把速率从100KHZ(标速)调整到了400KHZ(快速),发现确实有了好转,但是进一步一想,江科大说可以连续读多个字节,而我在查寻资料并加以实践后发现,MPU6050也确实可以连续多字节读取,因为需要读取的寄存器地址其实是连续的,那我就在这给大家分享一下我的学习经验吧,也希望能对大家有所帮助。
道歉:之前我文章中对IIC的高速模式的描述有误,把快速模式和高速模式混淆了,我所用的快速模式模式,这里给大家道个歉!
一、碎碎念
这里我就先直接贴出在江科大源码基础上直接改进过的代码了,后面再进行原理分析,想看分析的也可以先翻到后面先看分析再看代码。有需要的可以直接复制使用(OLED没有粘贴进来,相信大佬们肯定能发现o.0)。
二、直接可用的代码
#include "stm32f10x.h" // Device heade
#include "OLED.h" //
#define MPU6050_ADDRESS 0xD0 //MPU6050的I2C从机地址
#define MPU6050_SMPLRT_DIV 0x19 //MPU6050配置区地址
#define MPU6050_CONFIG 0x1A
#define MPU6050_GYRO_CONFIG 0x1B
#define MPU6050_ACCEL_CONFIG 0x1C
#define MPU6050_ACCEL_XOUT_H 0x3B //MPU6050采样寄存器地址
#define MPU6050_ACCEL_XOUT_L 0x3C
#define MPU6050_ACCEL_YOUT_H 0x3D
#define MPU6050_ACCEL_YOUT_L 0x3E
#define MPU6050_ACCEL_ZOUT_H 0x3F
#define MPU6050_ACCEL_ZOUT_L 0x40
#define MPU6050_TEMP_OUT_H 0x41
#define MPU6050_TEMP_OUT_L 0x42
#define MPU6050_GYRO_XOUT_H 0x43
#define MPU6050_GYRO_XOUT_L 0x44
#define MPU6050_GYRO_YOUT_H 0x45
#define MPU6050_GYRO_YOUT_L 0x46
#define MPU6050_GYRO_ZOUT_H 0x47
#define MPU6050_GYRO_ZOUT_L 0x48
#define MPU6050_PWR_MGMT_1 0x6B //MPU6050配置区地址
#define MPU6050_PWR_MGMT_2 0x6C
#define MPU6050_WHO_AM_I 0x75
int16_t AX,AY,AZ,GX,GY,GZ; //合并后的6轴数据
uint8_t Data[14]; //多字节读取数据暂存数组
/**
* 函 数:MPU6050等待事件
* 参 数:同I2C_CheckEvent
* 返 回 值:无
*/
void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{
uint32_t Timeout;
Timeout = 3000; //给定超时计数时间,这里我给缩短了一点
while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS) //循环等待指定事件
{
Timeout --; //等待时,计数值自减
if (Timeout == 0) //自减到0后,等待超时
{
/*超时的错误处理代码,可以添加到此处*/
break; //跳出等待,不等了
}
}
}
/**
* 函 数:MPU6050写寄存器
* 参 数:RegAddress 寄存器地址,范围:参考MPU6050手册的寄存器描述
* 参 数:Data 要写入寄存器的数据,范围:0x00~0xFF
* 返 回 值:无
*/
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
I2C_GenerateSTART(I2C2, ENABLE); //硬件I2C生成起始条件
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); //等待EV5
I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter); //硬件I2C发送从机地址,方向为发送
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); //等待EV6
I2C_SendData(I2C2, RegAddress); //硬件I2C发送寄存器地址
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING); //等待EV8
I2C_SendData(I2C2, Data); //硬件I2C发送数据
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED); //等待EV8_2
I2C_GenerateSTOP(I2C2, ENABLE); //硬件I2C生成终止条件
}
/**
* 函 数:MPU6050读寄存器
* 参 数:RegAddress 寄存器地址,范围:参考MPU6050手册的寄存器描述
* 返 回 值:读取寄存器的数据,范围:0x00~0xFF
*/
void MPU6050_ReadReg(void) //改良过,一次性读多个字节
{
uint8_t i;
//硬件I2C生成起始条件
I2C_GenerateSTART(I2C2, ENABLE);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); //等待EV5
//硬件I2C发送从机地址,方向为发送
I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); //等待EV6
//硬件I2C发送寄存器地址 从ACC_H开始
I2C_SendData(I2C2, MPU6050_ACCEL_XOUT_H);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED); //等待EV8_2
//硬件I2C生成重复起始条件
I2C_GenerateSTART(I2C2, ENABLE);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); //等待EV5
//硬件I2C发送从机地址,方向为接收
I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED); //等待EV6
//以下则是我改动过的部分
for(i=0;i<14;i++)
{
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED); //等待EV7
Data[i] = I2C_ReceiveData(I2C2);
if(i==12)
{
I2C_AcknowledgeConfig(I2C2, DISABLE); //在接收最后一个字节之前提前将应答失能
I2C_GenerateSTOP(I2C2, ENABLE); //在接收最后一个字节之前提前申请停止条件
}
}
I2C_AcknowledgeConfig(I2C2, ENABLE);
//将应答恢复为使能,为了不影响后续可能产生的读取多字节操作
}
/**
* 函 数:MPU6050初始化
* 参 数:无
* 返 回 值:无
*/
void MPU6050_Init(void)
{
/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE); //开启I2C2的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟
/*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); //将PB10和PB11引脚初始化为复用开漏输出
/*I2C初始化*/
I2C_InitTypeDef I2C_InitStructure; //定义结构体变量
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; //模式,选择为I2C模式
//注意我这个也改动了,把原本的标速IIC改成了高速的
I2C_InitStructure.I2C_ClockSpeed = 400000; //时钟速度,选择为400KHz
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;//时钟占空比,选择Tlow/Thigh = 2
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; //应答,选择使能
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; //应答地址,选择7位,从机模式下才有效
I2C_InitStructure.I2C_OwnAddress1 = 0x00; //自身地址,从机模式下才有效
I2C_Init(I2C2, &I2C_InitStructure); //将结构体变量交给I2C_Init,配置I2C2
/*I2C使能*/
I2C_Cmd(I2C2, ENABLE); //使能I2C2,开始运行
/*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
}
/**
* 函 数:MPU6050获取数据
* 参 数:AccX AccY AccZ 加速度计X、Y、Z轴的数据,使用输出参数的形式返回,范围:-32768~32767
* 参 数:GyroX GyroY GyroZ 陀螺仪X、Y、Z轴的数据,使用输出参数的形式返回,范围:-32768~32767
* 返 回 值:无
*/
//这里也有过改良,直接读取多个字节
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ,
int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{
MPU6050_ReadReg(); //读取
*AccX = (Data[0] << 8) | Data[1]; //数据拼接,通过输出参数返回
*AccY = (Data[2] << 8) | Data[3]; //数据拼接,通过输出参数返回
*AccZ = (Data[4] << 8) | Data[5]; //数据拼接,通过输出参数返回
*GyroX = (Data[8] << 8) | Data[9]; //数据拼接,通过输出参数返回
*GyroY = (Data[10] << 8) | Data[11]; //数据拼接,通过输出参数返回
*GyroZ = (Data[12] << 8) | Data[13]; //数据拼接,通过输出参数返回
}
void main()
{
MPU6050_Init();
OLED_Init(); //OLED初始化
while(1)
{
MPU6050_GetData(&AX,&AY,&AZ,&GX,&GY,&GZ);
OLED_ShowSignedNum(2, 1, AX, 5); //OLED显示数据
OLED_ShowSignedNum(3, 1, AY, 5);
OLED_ShowSignedNum(4, 1, AZ, 5);
OLED_ShowSignedNum(2, 8, GX, 5);
OLED_ShowSignedNum(3, 8, GY, 5);
OLED_ShowSignedNum(4, 8, GZ, 5);
}
}
备注(移植需知)
这里面的代码我基于江科大的源码修改了三处地方,大家如果想自己修改的时候请务必注意
1.在硬件IIC初始化的位置I2C_InitStructure.I2C_ClockSpeed = 400000;把频率提到了400KHZ
2.MPU6050_ReadReg函数去掉了形参,在内部直接读取14个数并赋值到全局的Data数组
3.MPU6050_GetData函数我也进行了修改,把原有的单个读取之后再进行数据合并换成了我的一次性读取完再合并
三、原理分析
首先我们要知道什么是IIC,由于我也还是一个电子信息刚读大一不久的新生,实在是才疏学浅,所以就不在这里献丑了,但是了解IIC我觉得其实也还是很有必要的。
软件IIC和硬件IIC的在使用上的区别
1.软件IIC一般只支持标准(最高)100KHZ的通信速率,模拟时序,引脚可以自己分配,兼容性好。
2.对于STM32F103C8T6的而言,硬件IIC支持标准(最高)100KHZ或者快速(最高)400KHZ的通信速率,具体用哪种取决对其的配置(需求)和从机是否支持快速IIC(400KHZ),是硬件自动控制时序,引脚一般不可改(映射除外),通信稳定性好,效率高。补充一点是硬件IIC其实是有高速模式的,在高数模式下最高支持3.4MHZ的速率,但是STM32F103C8t6没有高速模式,只支持标速和快速模式,其他型号可以查上网搜搜。
由于IIC其实是半双工同步通信,它是有一个时钟线(SCL)来同步数据的传输,所以硬件IIC速率只要不超过400KHZ基本都是可行的。由于软件IIC时通过代码直接操控两个用于和从机通信的GPIO,模拟IIC的时序,从而达到通信的目的,但是考虑到电平抖动和通信稳定性的问题,所以需要在每次改变GPIO电平之后,需要进行一段10us的延时以保障通信的稳定性,这也是一般软件IIC只支持100KHZ一个重要原因。值得一提的是,在MPU6050的手册上有指出,MPU6050是支持400KHZ的快速通信的。
IIC的连续多字节读取
再说IIC的连续多字节读取,相信大家在学习EEPROM或者其他IIC存储器的时候有了解过。
但是,我们要知道连续读多个字节并不是盯着一个寄存器一直读很多次,而是从指定寄存器依次顺着读,每读完一个寄存器,其地址指针就已经指向了后面的一个地址了,这个特质并不仅限于EEPRO这样的存储容器,MPU6050这样的设备其实也是的可行的,只要你需要读连续的寄存器。
然后,我们知道MPU6050每个轴对应的数据是放在两个寄存器的,但是通过观察MPU6050的数据寄存器地址不难发现,
从加速度X轴高位(MPU6050_ACCEL_XOUT_H)
到角速度Z轴低位(MPU6050_GYRO_ZOUT_L)
MPU6050的采样数据寄存器地址是连续的,如下图:
//MPU6050采样寄存器地址
#define MPU6050_ACCEL_XOUT_H 0x3B
#define MPU6050_ACCEL_XOUT_L 0x3C
#define MPU6050_ACCEL_YOUT_H 0x3D
#define MPU6050_ACCEL_YOUT_L 0x3E
#define MPU6050_ACCEL_ZOUT_H 0x3F
#define MPU6050_ACCEL_ZOUT_L 0x40
#define MPU6050_TEMP_OUT_H 0x41
#define MPU6050_TEMP_OUT_L 0x42
#define MPU6050_GYRO_XOUT_H 0x43
#define MPU6050_GYRO_XOUT_L 0x44
#define MPU6050_GYRO_YOUT_H 0x45
#define MPU6050_GYRO_YOUT_L 0x46
#define MPU6050_GYRO_ZOUT_H 0x47
#define MPU6050_GYRO_ZOUT_L 0x48
所以连续读多个字节之后再进行数据合并理论上是可行,经过我的实践的检验,最终发现也确实可行。
四、结论
其实结论也挺让我震惊的,在快速硬件IIC连续读取MPU6050多字节的条件下,完成一次六轴数据的采集(温度也一并采集了但是我没算进去)只需要大约400us就行,比我之前的软件IIC单个读取快了25倍不止。
以上数据是我粗了计算得到到,与实际情况有略有出入。
最后,本期的内容就到此为止了,如果有地方讲错的地方还请大家及时指出,因为是第一次写贴子,有没讲清楚也希望大家多多包涵,也欢迎大家在评论区一起讨论学习。
作者:天凉咯ღ