平衡车学习记录(4)
这一章我将学习使用MPU6050模块,结算欧拉角等。
第一部分 MPU6050简介

如图,最左边就是单片机的MPU6050芯片

传感器主要由加速度计和陀螺仪组成,以上是MPU6050可以测量的参数。

MU6050通过I2C接口和单片机通信。

接受6个数据,通过DMP处理获得四元数。四元数等效于欧拉角。但本项目里,我们手动计算欧拉角。虽然DMP结算效果更好,但是我这里先跟着教程手动解算,了解欧拉角解算原理,后续再尝试DMP解算。用DMP需要移植一个库,为了教学就先采用手动解算的的方式。
对于手动解算,采用互补滤波的方式,对于卡尔曼滤波,更复杂也需要更多数学知识,所以后续再进行相关研究。
第一部分 传感器原理

如图是传感器的工作原理。只是模拟一下原理,假设物体向右加速运动,小球根据惯性就会向左压缩弹簧,就可以间接求出ay。ax,az同理。

把MPU6050安装在小车里,规定的x,y,z方向可以看芯片左上角的小圆点,此时小车向y方向加速运动,中间的这根弹簧,y+方向被拉长,y-方向被压缩。可以根据ay=a求出加速度。在x方向没有运动的加速度,所以加速度是0。

如图,提出一个问题:小车此时静止且保持直立,是否能得出三个方向的加速度为0?

由于存在重力,z轴方向有一个重力加速度。

如果是倾斜的情况,受力分析略,计算的俯仰角如图。

如图是测量角速度的原理,如果外面的箱体旋转起来,小球也会跟着偏移。
第二部分 寄存器读写
先把任务分成如图的几个部分

首先,先看一下通信的硬件图

如图,找到I2C通信最重要的四根线,VCC,GND,SCL,SDA,接着找出后两根线连接单片机的引脚。

分别是PB8和PB9。

打开数据手册,找到两个引脚的功能,默认是TIM4的通道3和通道4,使用重映射功能才可以复用成SCL和SDA。
先进行重映射,再配置PB8和PB9为输出开漏模式。接着设置I2C的初始化参数。

对于通信速率 ,打开MPU6050数据手册,找到支持的快速模式400Khz。
完整初始化代码如下:
void App_MPU6050_Init(void)
{
//#1.初始化I2C总线 PB8 PB9 - I2C1
//将I2C的引脚重映射到PB8和PB9
RCC_APB2PeriphClockCmd (RCC_APB2Periph_AFIO,ENABLE);
GPIO_PinRemapConfig(GPIO_Remap_I2C1,ENABLE);
//#2.初始化PB8和PB9 - AF_OD
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitTypeDef GPIO_InitStruct={0};
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_8|GPIO_Pin_9;
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_AF_OD;
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_2MHz;
GPIO_Init(GPIOB,&GPIO_InitStruct);
//#2.初始化I2C1
//开启I2C1的时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1,ENABLE);
//设置I2C的参数
I2C_InitTypeDef I2C_InitStruct={0};
I2C_InitStruct.I2C_ClockSpeed=400000;//400khz,I2C通信速度,数据手册查找得知支持快速模式
I2C_InitStruct.I2C_DutyCycle=I2C_DutyCycle_2;//占空比没有特殊要求默认2比1
I2C_InitStruct.I2C_Mode=I2C_Mode_I2C;//选用标准I2C接口模式
I2C_Init(I2C1,&I2C_InitStruct);
初始化完成后,接着对寄存器进行读写操作,设置量程还有读取寄存器的值。

如图,是写寄存器和读寄存器的示意图。
为了使用方便,封装两个函数,用于读写寄存器

根据以上思路,编写函数,其中I2C.h是模板工程里的封装好的驱动文件,暂时不研究。
#include"i2c.h"
//
//@简介:向寄存器写值
//@参数 reg - 要写入寄存器的地址 value - 要写入的值
//
static void reg_Write(uint8_t reg, uint8_t value)
{
uint8_t bytesToSend[]={reg,value};
My_I2C_SendBytes(I2C1,0xd0,bytesToSend,2);
}
//
//@简介:向寄存器读值
//@参数 reg - 要读取寄存器的地址 返回值:表示读取到的值
//
static uint8_t reg_Read(uint8_t reg)
{
uint8_t regValue;
My_I2C_SendBytes(I2C1,0xd0,®,1);//发送寄存器的地址
My_I2C_ReceiveBytes(I2C1,0xd0,®Value,1);//读取一个字节
return regValue;
}
其中,关于为什么发送0xd0的地址,需要到MPU6050规格书里查找

从机地址如上,最后以为X受硬件引脚影响,
![]()
因为是接地,取0.

具体计算结果如上。
编写好读写函数后,设置参数部分的代码,up主提前给出了,用到刚才的读写函数

为了搞明白具体含义,打开寄存器手册,查找相关内容。

上表是电源管理相关的寄存器,地址是0x6B,和刚才代码对应

对于Bit7的解释是,设置成1的时候,设备所有的寄存器都会恢复到默认值。
所以把Bit7设置成1,其他都是0,那么寄存器的具体内容就是0x80。
修改陀螺仪量程的寄存器如下
Bit3和Bit4用于量程的选择,一共有4档量程可以选择。

对于平衡车来说,一秒钟旋转360°每秒已经很大了,取两倍720°,所以我们选择1000或者2000比较合适。这里选择2000;所以4,3位填11,代表二进制的3。其他位不动,0001 1000,所以填18。
修改加速度计量程的寄存器如下

设置的方式类似陀螺仪寄存器
具体分析一下,加速度的g分解到每个方向上的分量不会超过g,取2g作为上限,所以填0。
那么这个寄存器的值全是0,填00。
具体代码同上面的代码截图,这里就不写。
初始化完成后,接着就可以读取寄存器的值了。

以ax为例,先读取高八位数据,再读取第八位数据,最后合并成带符号的16位数据。
以下是读取到的所有数据的代码,包括三个方向的加速度还有角速度
int16_t ax_raw=(int16_t)((reg_Read(0x3b)<<8)+reg_Read(0x3c));//ax的原始数据
int16_t ay_raw=(int16_t)((reg_Read(0x3d)<<8)+reg_Read(0x3e));//ay的原始数据
int16_t az_raw=(int16_t)((reg_Read(0x3f)<<8)+reg_Read(0x40));//az的原始数据
int16_t temperature_raw=(int16_t)((reg_Read(0x41)<<8)+reg_Read(0x42));//温度计的原始数据
int16_t gx_raw=(int16_t)((reg_Read(0x43)<<8)+reg_Read(0x44));//gx的原始数据
int16_t gy_raw=(int16_t)((reg_Read(0x45)<<8)+reg_Read(0x46));//gy的原始数据
int16_t gz_raw=(int16_t)((reg_Read(0x47)<<8)+reg_Read(0x48));//gz的原始数据
得到原始数据后,因为是带符号的16位整数,范围从-32768——+32768,要进行单位转换。比如对加速度进行转换,实际量程是+-2g,那么计算出的转换因子就是2/32768=6.1035e-5,需要把原始数据都乘上这个数。角速度同理,转换因子是2000/32768=6.1035e-2。
温度值的转换,用到数据手册的公式
up主提到了实际上使用的芯片型号并不是6050,而是6500,其他地方区别不大,但是在温度计计算上有区别。

ax=ax_raw*6.1035e-5f;
ay=ay_raw*6.1035e-5f;
az=az_raw*6.1035e-5f;
temperature=temperature_raw/333.87f+21.0f;//MPU6500的计算公式,区别于MPU6050
gx=gx_raw*6.1035e-2f;
gy=gy_raw*6.1035e-2f;
gz=gz_raw*6.1035e-2f;
上面的代码就是换算公式,写在App_MPU6050_Update下。
再编写一些外部获取数据的接口
//
//@简介:获取x轴向加速度,单位g
//
float App_MPU6050_GetAx(void)
{
return ax;
}
//
//@简介:获取y轴向加速度,单位g
//
float App_MPU6050_GetAy(void)
{
return ay;
}
//
//@简介:获取z轴向加速度,单位g
//
float App_MPU6050_GetAz(void)
{
return az;
}
//
//@简介:获取温度计的值,单位摄氏度
//
float App_MPU6050_Temperature(void)
{
return temperature;
}
//
//@简介:获取绕x轴的角速度,单位°/s
//
float App_MPU6050_GetGx(void)
{
return gx;
}
//
//@简介:获取绕y轴的角速度,单位°/s
//
float App_MPU6050_GetGy(void)
{
return gy;
}
//
//@简介:获取绕z轴的角速度,单位°/s
//
float App_MPU6050_GetGz(void)
{
return gz;
}
接下去编写测试代码,获取数据。
#include"MPU6050_test.h"
#include "app_usart2.h"
#include "app_mpu6050.h"
#include "delay.h"
void MPU6050_Test(void)
{
App_USART2_Init();
App_MPU6050_Init();
while(1)
{
App_MPU6050_Update();
float ax=App_MPU6050_GetAx();
float ay=App_MPU6050_GetAy();
float az=App_MPU6050_GetAz();
float temperature=App_MPU6050_GetTemperature();
float gx=App_MPU6050_GetGx();
float gy=App_MPU6050_GetGy();
float gz=App_MPU6050_GetGz();
My_USART_Printf(USART2,"%f,%f,%f,%f,%f,%f,%f\n",ax,ay,az,temperature,gx,gy,gz);
Delay(10);
}
}
调用相关函数,获取数据后,用串口打印出数据。

数据经过验证,实验成功。az约等于1,室内温度也差不多。
第三部分 解算欧拉角(互补滤波)陀螺仪部分
欧拉角由三个角组成:偏航角,俯仰角,翻滚角。如图所示


陀螺仪会受漂移影响,加速度计会受噪声影响,将它们通过互补滤波融合的结果更加准确。
举例,如下计算偏航角的方式

核心思想就是利用积分,求角速度关于时间的积分
同理,俯仰角和翻滚角也可以用类似的递推公式表示

其中翻滚角,因为芯片摆放的缘故,变化的方向和翻滚角规定的方向刚好相反,所以是减号。
Δt取最大频率的5ms。

对于裸机多任务模型,我们需要创建多个进程函数,上述的计算过程也需要写成进程函数的形式,通过对系统时间的比较来达到延时的目的,而不是使用阻塞型的delay函数。
创建一个陀螺仪解算欧拉角的进程函数,根据上面的公式写代码
void App_MPU6050_Proc(void)
{
//通过陀螺仪的测量结果计算欧拉角
App_MPU6050_Update();//更新传感器的值
PERIODIC(5)//进程函数每5s执行一次
// static uint32_t nxt=0;//下次程序运行的时间
//
// if(GetTick()<nxt)return;
yaw=yaw+gz*0.005;
pitch=pitch+gx*0.005;
roll=roll-gy*0.005;
}
其中原本的延时代码被注释掉了,为了方便,up提供了一个通用的宏,只要填写延迟的时间,单位ms,就可以正常使用非阻塞性延迟。
编写测试代码检验实验是否正确,按照初始化和while(1)循环里,写进程函数的形式。
static void Usart2_Proc(void)
{
PERIODIC(10)
float yaw=App_MPU6050_Getyaw();
float pitch=App_MPU6050_Getpitch();
float roll=App_MPU6050_Getroll();
My_USART_Printf(USART2,"%f,%f,%f\n",yaw,pitch,roll);
}
//
//@简介:测试欧拉角,用Vofa软件展示数据
//
void MPU6050_EularAngleTest(void)
{
App_MPU6050_Init();
App_USART2_Init();
while(1)
{
App_MPU6050_Proc();
Usart2_Proc();
}
}
打开vofa观察实验结果,导入平衡车模型,绑定三个欧拉角和坐标轴,发现结果基本上符合实际小车运动趋势,但是因为存在误差,三个数据都在不断自增,这是后面需要解决的问题。

第四部分 加速度计部分

对重力进行分解就可以求出分力,进而求出加速度。注意,这里一般不计算偏航角,因为重力始终垂直于偏航角的平面,无法分解。
首先计算俯仰角,原理图如下

其中,z轴上的重力加速度,我们会认为是负方向理应是-g,但是实际情况是,MPU6050测出来的是+g,因为弹簧被拉伸的方向才是MPU6050加速度计的加速度方向,用vofa观察结果如下

z方向的g确实是+g左右。
首先计算俯仰角,示意图如下

但是这里的arctan涉及到了取值范围的问题,arctanx这个函数在c语言里只能表示-90°-90°的角度,如果是-135°,tan(-135°)的值也是1,但是在函数里只能表示成45°,所以需要用atan2这个函数,能把范围扩大到-180°-180°。

虽然我认为这个小车项目里,俯仰角的角度就是-90°-90°,因为小车不可能倒过来,但是用于飞行器之类的项目时,是可能出现超过这个范围的情况的,所以现在学习这个知识点仍然是有必要的。

两个函数具体的区别,在于atanx会丢失边长的符号信息,还是以-135°举例,对边和邻边都是负二分之根号二,两者相除就变成1,可能会错误识别成45°。atan2需要输入原始两个边长,就可以准确判断象限了。
另外还需要考虑到单位换算问题,弧度和角度的互换,这个知识点是高中数学的内容,就不赘述了。
接着算翻滚角,示意图如下。另外,正常的平地行驶也是用不到这个角的,也是斜坡或者无人机会用上。

//通过加速度计解算欧拉角
yaw=0;//强制赋值0,解算不出
pitch=atan2(ay,az)/3.1415927f*180;
roll=atan2(ax,az)/3.1415927f*180;
编写代码,非常简单,替换掉上次写的陀螺仪代码进行测试。

结果如下,实际发现模型会产生抖动,因为受到外界噪声的影响,比如一些外部的抖动,因为不是理想环境。
第五部分 互补滤波

基于上述两种计算模式的优缺点,将两者的结果进行互补滤波,效果图如图。
//通过陀螺仪解算欧拉角
float yaw_g=yaw+gz*0.005;
float pitch_g=pitch+gx*0.005;
float roll_g=roll-gy*0.005;
//通过加速度计解算欧拉角
float pitch_a=atan2(ay,az)/3.1415927f*180;
float roll_a=atan2(ax,az)/3.1415927f*180;
//使用互补滤波器对陀螺仪和加速度计的计算结果进行融合
yaw=yaw_g;
pitch=pitch_g*0.95238+(1-0.95238)*pitch_a;
roll=roll_g*0.95238+(1-0.95238)*roll_a;
代码如上。下载到单片机看结果,现在的小车姿态非常平稳了,和实际运动姿态基本一致。

如图是互补滤波的原理,集体原理不细究了。
第六部分 快速三角函数
首先,介绍一下快速三角函数的背景,因为普通的三角函数结算非常耗时间,会影响时间片。

为了让我们直观地理解各种计算的耗时时间,up主做了以下实验,这里我就不进行操作了。

示意图如下:

代码如下:

其中c不用到会警告,加(void)强制转换即可。
其余函数不展示,展示结果即可


由结果可知,三角函数的计算及其耗时,由此引入一种查表法



这是快速三角函数的计算速度,相比较之前快了很多。
将之前的三角函数全部替换成快速三角函数即可。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)