基于STM32的四旋翼无人机项目(二):MPU6050姿态解算(含上位机3D姿态显示教学)

前言:本文为手把手教学飞控核心知识点之一的姿态解算——MPU6050 姿态解算(飞控专栏第2篇)。项目中飞行器使用 MPU6050 传感器对飞行器的姿态进行解算(四元数方法),搭配设计的卡尔曼滤波器与一阶低通滤波器进行数据滤波。当然,本篇博客也将为读者朋友教学业内匿名者上位机的代码移植和使用方法。为了方便读者朋友学习,本博客将使用传感器模块替代整机进行教学,方便读者朋友后续根据自己实际情况移植!(文末有代码开源!

实验硬件: STM32F103C8T6;MPU6050;USB转TTL

硬件实物图:

效果图:

一、飞行器姿态解算

1.1 MPU6050概述 

飞行器通常搭载一款姿态传感器(不管是六轴还是九轴姿态传感器),本项目中以最常见的 MPU6050 为例。MPU6050 传感器其实并不能直接输出我们飞行器飞行过程中的欧拉角(Euler-angles),通过读取它的传感器我们可以得到:3轴角速度+3轴角加速度。得到的角速度和角加速度信息我们是无法直接使用的,这个时候我们可以选择使用 DMP 去解算此时飞行器的欧拉角(Euler-angles)情况。当然,作者在项目中并没有使用 DMP 去解算飞行器的欧拉角(Euler-angles),而是使用了四元数解算的方法!

DMP(Digital Motion Processor)是一种数字运动处理器,它可以从MPU6050等传感器中读取数据,并进行解算以获取姿态信息。下面是DMP解算MPU6050的优缺点:

优点:

  1. DMP使用简单,可以直接套用官方库进行开发,无需自己编写解算算法。
  2. DMP不会占用太多的资源,因为它只需要读取传感器数据并进行简单的解算。
  3. DMP的输出数据经过处理,可以直接用于姿态控制等应用,无需再进行复杂的计算。

缺点:

  1. DMP的输出数据精度可能不够高,特别是在高精度传感场景下。
  2. DMP的输出数据不稳定,可能会受到噪声等因素的影响。
  3. DMP无法测量偏航角,只能获取滚动角和俯仰角的信息。

1.2 四元数姿态解算

本小节将以下方思维导图进行分析讲解:

初次接触的读者朋友可能对四元数较为陌生,这里作者建议大家直接去阅读秦永元的《惯性导航》,里面有非常好的讲解,大家可以直接看绪论第九章就可以。

《惯性导航》PDF地址:惯性导航(第三版) (sciencereading.cn)

下面我们根据思维导图用程序来一步一步实现如何求解欧拉角:

1、定义初始四元数的值为q0=1,q1=0,q2=0,q3=0。

2、读取加速度计值、角速度值,程序定义变量分别为ax、ay、az,gx、gy、gz,将陀螺仪值转为弧度,转换如下:

gx = gx * 0.0174f;        //1度=0.0174弧度
gy = gy * 0.0174f; 
gz = gz * 0.0174f; 

3、对加速度值进行归一化

//提取等效旋转矩阵中的重力分量 
Gravity.x = 2*(NumQ.q1 * NumQ.q3 - NumQ.q0 * NumQ.q2);								
Gravity.y = 2*(NumQ.q0 * NumQ.q1 + NumQ.q2 * NumQ.q3);						  
Gravity.z = 1-2*(NumQ.q1 * NumQ.q1 + NumQ.q2 * NumQ.q2);	

//加速度归一化
NormAcc = 1/sqrt(squa(MPU6050.accX)+ squa(MPU6050.accY) +squa(MPU6050.accZ));
	
//归一化计算
Acc.x = pMpu->accX * NormAcc;
Acc.y = pMpu->accY * NormAcc;
Acc.z = pMpu->accZ * NormAcc;

4、提取姿态矩阵中的重力分量,我们已经得到数学计算公式为

//提取等效旋转矩阵中的重力分量 
Gravity.x = 2*(NumQ.q1 * NumQ.q3 - NumQ.q0 * NumQ.q2);								
Gravity.y = 2*(NumQ.q0 * NumQ.q1 + NumQ.q2 * NumQ.q3);						  
Gravity.z = 1-2*(NumQ.q1 * NumQ.q1 + NumQ.q2 * NumQ.q2);	

5、求姿态误差,对两向量进行叉乘(定义ex、ey、ez为三个轴误差元素),数学计算为: 

//向量差乘得出的值
AccGravity.x = (Acc.y * Gravity.z - Acc.z * Gravity.y);
AccGravity.y = (Acc.z * Gravity.x - Acc.x * Gravity.z);
AccGravity.z = (Acc.x * Gravity.y - Acc.y * Gravity.x);

6、互补滤波,将误差输入PID控制器后与陀螺仪测得的角速度相加,修正角速度值,程序实现如下(Kp为互补滤波系数这里取Kp=0.5,实际值根据需要进行调整): 

//角速度融合加速度积分补偿值
Gyro.x = pMpu->gyroX * Gyro_Gr + KpDef * AccGravity.x  +  GyroIntegError.x;//弧度制
Gyro.y = pMpu->gyroY * Gyro_Gr + KpDef * AccGravity.y  +  GyroIntegError.y;
Gyro.z = pMpu->gyroZ * Gyro_Gr + KpDef * AccGravity.z  +  GyroIntegError.z;

7、解四元数微分方程,其数学计算如下(初始值q0 = 1,q1 = 0,q2 = 0,q3 = 0,w_{x},w_{y},w_{z}为角速度,\bigtriangleup t为周期时间)

// 一阶龙格库塔法, 更新四元数
q0_t = (-NumQ.q1*Gyro.x - NumQ.q2*Gyro.y - NumQ.q3*Gyro.z) * HalfTime;
q1_t = ( NumQ.q0*Gyro.x - NumQ.q3*Gyro.y + NumQ.q2*Gyro.z) * HalfTime;
q2_t = ( NumQ.q3*Gyro.x + NumQ.q0*Gyro.y - NumQ.q1*Gyro.z) * HalfTime;
q3_t = (-NumQ.q2*Gyro.x + NumQ.q1*Gyro.y + NumQ.q0*Gyro.z) * HalfTime;

NumQ.q0 += q0_t;
NumQ.q1 += q1_t;
NumQ.q2 += q2_t;
NumQ.q3 += q3_t;

8、四元数归一化,归一化方法与加速度归一化方法一样;

// 四元数归一化
NormQuat = 1/sqrt(squa(NumQ.q0) + squa(NumQ.q1) + squa(NumQ.q2) + squa(NumQ.q3));
NumQ.q0 *= NormQuat;
NumQ.q1 *= NormQuat;
NumQ.q2 *= NormQuat;
NumQ.q3 *= NormQuat;	

9、计算姿态角,数学公式为:

	#ifdef	YAW_GYRO
	*(
	float *)pAngE = atan2f(2 * NumQ.q1 *NumQ.q2 + 2 * NumQ.q0 * NumQ.q3, 1 - 2 * NumQ.q2 *NumQ.q2 - 2 * NumQ.q3 * NumQ.q3) * RtA;  //yaw
	#else
	float yaw_G = pMpu->gyroZ * Gyro_G;
	if((yaw_G > 1.0f) || (yaw_G < -1.0f)) //数据太小可以认为是干扰,不是偏航动作
	{
		pAngE->yaw  += yaw_G * dt;		
	}
	#endif
pAngE->pitch  =  asin(2 * NumQ.q0 *NumQ.q2 - 2 * NumQ.q1 * NumQ.q3) * RtA;						
		
pAngE->roll	= atan2(2 * NumQ.q2 *NumQ.q3 + 2 * NumQ.q0 * NumQ.q1, 1 - 2 * NumQ.q1 *NumQ.q1 - 2 * NumQ.q2 * NumQ.q2) * RtA;	//PITCH 	

二、卡尔曼滤波详解

卡尔曼的本质:递归式最优评估。
卡尔曼的好处是:①效率最高甚至是最有用的,在系统中能够快速的消除高速白噪声;②不会产生严重滞后;③所需数据存储量较小,便于进行实时处理;

在飞行器中卡尔曼滤波的高效率性是十分优秀的,但是卡尔曼不能抵抗突变干扰,这点在飞行器中一般不会出现数据突变跳变,所以卡尔曼很适合运用于四轴飞行器。

飞行器项目中卡尔曼滤波的目标对象是加速度,三轴加速度都是独立变量,可以分别独立用一维线性卡尔曼进行滤波。由于加速度容易受震动干扰,它就是一个很典型的高频高斯白噪声(噪声随机,对称,符合高斯分布的噪声),所以加速度用卡尔曼滤波。

角速度不容易受到干扰,就用简单的一阶低通互补滤波。

飞控中的滤波算法:角加速度(卡尔曼滤波);角速度(一阶互补滤波);

将上述数学公式代码化后可以得到卡尔曼滤波代码:

#include "kalman.h"

//一维卡尔曼滤波
void kalmanfiter(struct KalmanFilter *EKF,float input)
{
	EKF->NewP = EKF->LastP + EKF->Q;
	EKF->Kg = EKF->NewP / (EKF->NewP + EKF->R);
	EKF->Out = EKF->Out + EKF->Kg * (input - EKF->Out);
	EKF->LastP = (1 - EKF->Kg) * EKF->NewP;
}

三、CubeMX配置

1、RCC配置外部高速晶振(精度更高)——HSE;

2、SYS配置:Debug设置成Serial Wire否则可能导致芯片自锁);

3、TIM1配置:在TIM1的中断回调函数中发生MPU6050姿态解算与控制都是,中断周期:3ms;

4、I2C1配置:配置MCU与MPU6050之间的通讯协议;

5、UART配置:通过UART1与匿名上位机进行通讯,显示飞行器3D姿态;

6、时钟树配置

7、工程配置

四、代码与解析

4.1 MPU6050代码

mpu6050.h代码:

mpu6050代码中核心是通过I2C通讯读取寄存器地址为:0X3B 0x43 的数值(分别为角加速度和角速度)。

#ifndef __MPU6050_H
#define __MPU6050_H
 	
#include "stm32f1xx_hal.h"//用什么系列就是什么									  	  
 
//#define MPU_ACCEL_OFFS_REG		0X06	//accel_offs寄存器,可读取版本号,寄存器手册未提到
//#define MPU_PROD_ID_REG			0X0C	//prod id寄存器,在寄存器手册未提到
#define MPU_SELF_TESTX_REG		0X0D	//自检寄存器X
#define MPU_SELF_TESTY_REG		0X0E	//自检寄存器Y
#define MPU_SELF_TESTZ_REG		0X0F	//自检寄存器Z
#define MPU_SELF_TESTA_REG		0X10	//自检寄存器A
#define MPU_SAMPLE_RATE_REG		0X19	//采样频率分频器
#define MPU_CFG_REG			0X1A	//配置寄存器
#define MPU_GYRO_CFG_REG		0X1B	//陀螺仪配置寄存器
#define MPU_ACCEL_CFG_REG		0X1C	//加速度计配置寄存器
#define MPU_MOTION_DET_REG		0X1F	//运动检测阀值设置寄存器
#define MPU_FIFO_EN_REG			0X23	//FIFO使能寄存器
#define MPU_I2CMST_CTRL_REG		0X24	//IIC主机控制寄存器
#define MPU_I2CSLV0_ADDR_REG	        0X25	//IIC从机0器件地址寄存器
#define MPU_I2CSLV0_REG			0X26	//IIC从机0数据地址寄存器
#define MPU_I2CSLV0_CTRL_REG	        0X27	//IIC从机0控制寄存器
#define MPU_I2CSLV1_ADDR_REG	        0X28	//IIC从机1器件地址寄存器
#define MPU_I2CSLV1_REG			0X29	//IIC从机1数据地址寄存器
#define MPU_I2CSLV1_CTRL_REG	        0X2A	//IIC从机1控制寄存器
#define MPU_I2CSLV2_ADDR_REG	        0X2B	//IIC从机2器件地址寄存器
#define MPU_I2CSLV2_REG			0X2C	//IIC从机2数据地址寄存器
#define MPU_I2CSLV2_CTRL_REG	        0X2D	//IIC从机2控制寄存器
#define MPU_I2CSLV3_ADDR_REG	        0X2E	//IIC从机3器件地址寄存器
#define MPU_I2CSLV3_REG			0X2F	//IIC从机3数据地址寄存器
#define MPU_I2CSLV3_CTRL_REG	        0X30	//IIC从机3控制寄存器
#define MPU_I2CSLV4_ADDR_REG	        0X31	//IIC从机4器件地址寄存器
#define MPU_I2CSLV4_REG			0X32	//IIC从机4数据地址寄存器
#define MPU_I2CSLV4_DO_REG		0X33	//IIC从机4写数据寄存器
#define MPU_I2CSLV4_CTRL_REG	        0X34	//IIC从机4控制寄存器
#define MPU_I2CSLV4_DI_REG		0X35	//IIC从机4读数据寄存器
 
#define MPU_I2CMST_STA_REG		0X36	//IIC主机状态寄存器
#define MPU_INTBP_CFG_REG		0X37	//中断/旁路设置寄存器
#define MPU_INT_EN_REG			0X38	//中断使能寄存器
#define MPU_INT_STA_REG			0X3A	//中断状态寄存器
 
#define MPU_ACCEL_XOUTH_REG		0X3B	//加速度值,X轴高8位寄存器
#define MPU_ACCEL_XOUTL_REG		0X3C	//加速度值,X轴低8位寄存器
#define MPU_ACCEL_YOUTH_REG		0X3D	//加速度值,Y轴高8位寄存器
#define MPU_ACCEL_YOUTL_REG		0X3E	//加速度值,Y轴低8位寄存器
#define MPU_ACCEL_ZOUTH_REG		0X3F	//加速度值,Z轴高8位寄存器
#define MPU_ACCEL_ZOUTL_REG		0X40	//加速度值,Z轴低8位寄存器
 
#define MPU_TEMP_OUTH_REG		0X41	//温度值高八位寄存器
#define MPU_TEMP_OUTL_REG		0X42	//温度值低8位寄存器
 
#define MPU_GYRO_XOUTH_REG		0X43	//陀螺仪值,X轴高8位寄存器
#define MPU_GYRO_XOUTL_REG		0X44	//陀螺仪值,X轴低8位寄存器
#define MPU_GYRO_YOUTH_REG		0X45	//陀螺仪值,Y轴高8位寄存器
#define MPU_GYRO_YOUTL_REG		0X46	//陀螺仪值,Y轴低8位寄存器
#define MPU_GYRO_ZOUTH_REG		0X47	//陀螺仪值,Z轴高8位寄存器
#define MPU_GYRO_ZOUTL_REG		0X48	//陀螺仪值,Z轴低8位寄存器
 
#define MPU_I2CSLV0_DO_REG		0X63	//IIC从机0数据寄存器
#define MPU_I2CSLV1_DO_REG		0X64	//IIC从机1数据寄存器
#define MPU_I2CSLV2_DO_REG		0X65	//IIC从机2数据寄存器
#define MPU_I2CSLV3_DO_REG		0X66	//IIC从机3数据寄存器
 
#define MPU_I2CMST_DELAY_REG	        0X67	//IIC主机延时管理寄存器
#define MPU_SIGPATH_RST_REG		0X68	//信号通道复位寄存器
#define MPU_MDETECT_CTRL_REG	        0X69	//运动检测控制寄存器
#define MPU_USER_CTRL_REG		0X6A	//用户控制寄存器
#define MPU_PWR_MGMT1_REG		0X6B	//电源管理寄存器1
#define MPU_PWR_MGMT2_REG		0X6C	//电源管理寄存器2 
#define MPU_FIFO_CNTH_REG		0X72	//FIFO计数寄存器高八位
#define MPU_FIFO_CNTL_REG		0X73	//FIFO计数寄存器低八位
#define MPU_FIFO_RW_REG			0X74	//FIFO读写寄存器
#define MPU_DEVICE_ID_REG		0X75	//器件ID寄存器,who am i寄存器

//如果AD0脚(9脚)接地,IIC地址为0X68(不包含最低位).
//如果接V3.3,则IIC地址为0X69(不包含最低位).
#define MPU_ADDR				0X68
 
//因为MPU6050的AD0接GND,所以则读写地址分别为0XD1和0XD0
//            (如果AD0接VCC,则读写地址分别为0XD3和0XD2)  
#define MPU_READ    0XD1
#define MPU_WRITE   0XD0
 
uint8_t MPU_Init(void); 						//初始化MPU6050
uint8_t MPU_Write_Len(uint8_t reg,uint8_t len,uint8_t *buf);                           //IIC连续写
uint8_t MPU_Read_Len(uint8_t reg,uint8_t len,uint8_t *buf);                         //IIC连续读 
uint8_t MPU_Write_Byte(uint8_t reg,uint8_t data);				//IIC写一个字节
uint8_t MPU_Read_Byte(uint8_t reg);					//IIC读一个字节
 
uint8_t MPU_Set_Gyro_Fsr(uint8_t fsr);
uint8_t MPU_Set_Accel_Fsr(uint8_t fsr);
uint8_t MPU_Set_LPF(uint16_t lpf);
uint8_t MPU_Set_Rate(uint16_t rate);
uint8_t MPU_Set_Fifo(uint8_t sens);
 
float MPU_Get_Temperature(void);
uint8_t MPU_Get_Gyroscope(short *gx,short *gy,short *gz);
uint8_t MPU_Get_Accelerometer(short *ax,short *ay,short *az);

void MpuGetData(void);
 
#endif

mpu6050.c代码:

#include "mpu6050.h" 
#include "alldata.h"
#include "kalman.h"
#include "stdio.h"
#include "i2c.h"

static volatile int16_t *pMpu = (int16_t *)&MPU6050;
int16_t MpuOffset[6] = {0};		//MPU6050补偿数值

//初始化MPU6050
//返回值:0,成功
//    其他,错误代码
uint8_t MPU_Init(void)
{ 
  uint8_t res;
  extern I2C_HandleTypeDef hi2c1;
  HAL_I2C_Init(&hi2c1);
  MPU_Write_Byte(MPU_PWR_MGMT1_REG,0X80);	//复位MPU6050
  MPU_Write_Byte(MPU_PWR_MGMT1_REG,0X00);	//唤醒MPU6050 
  MPU_Set_Gyro_Fsr(3);					//陀螺仪传感器,±2000dps
  MPU_Set_Accel_Fsr(0);					//加速度传感器,±2g
  MPU_Set_Rate(50);						//设置采样率50Hz
  MPU_Write_Byte(MPU_INT_EN_REG,0X00);	//关闭所有中断
  MPU_Write_Byte(MPU_USER_CTRL_REG,0X00);	//I2C主模式关闭
  MPU_Write_Byte(MPU_FIFO_EN_REG,0X00);	//关闭FIFO
  MPU_Write_Byte(MPU_INTBP_CFG_REG,0X80);	//INT引脚低电平有效
  res=MPU_Read_Byte(MPU_DEVICE_ID_REG);
	printf("\r\nMPU6050:0x%2x\r\n",res);
  if(res==MPU_ADDR)//器件ID正确
  {
    MPU_Write_Byte(MPU_PWR_MGMT1_REG,0X01);	//设置CLKSEL,PLL X轴为参考
    MPU_Write_Byte(MPU_PWR_MGMT2_REG,0X00);	//加速度与陀螺仪都工作
    MPU_Set_Rate(50);						//设置采样率为50Hz
  }else 
		return 1;
  return 0;
}
//设置MPU6050陀螺仪传感器满量程范围
//fsr:0,±250dps;1,±500dps;2,±1000dps;3,±2000dps
//返回值:0,设置成功
//    其他,设置失败 
uint8_t MPU_Set_Gyro_Fsr(uint8_t fsr)
{
	return MPU_Write_Byte(MPU_GYRO_CFG_REG,fsr<<3);//设置陀螺仪满量程范围  
}
//设置MPU6050加速度传感器满量程范围
//fsr:0,±2g;1,±4g;2,±8g;3,±16g
//返回值:0,设置成功
//    其他,设置失败 
uint8_t MPU_Set_Accel_Fsr(uint8_t fsr)
{
	return MPU_Write_Byte(MPU_ACCEL_CFG_REG,fsr<<3);//设置加速度传感器满量程范围  
}
//设置MPU6050的数字低通滤波器
//lpf:数字低通滤波频率(Hz)
//返回值:0,设置成功
//    其他,设置失败 
uint8_t MPU_Set_LPF(uint16_t lpf)
{
	uint8_t data=0;
	if(lpf>=188)data=1;
	else if(lpf>=98)data=2;
	else if(lpf>=42)data=3;
	else if(lpf>=20)data=4;
	else if(lpf>=10)data=5;
	else data=6; 
	return MPU_Write_Byte(MPU_CFG_REG,data);//设置数字低通滤波器  
}
//设置MPU6050的采样率(假定Fs=1KHz)
//rate:4~1000(Hz)
//返回值:0,设置成功
//    其他,设置失败 
uint8_t MPU_Set_Rate(uint16_t rate)
{
	uint8_t data;
	if(rate>1000)rate=1000;
	if(rate<4)rate=4;
	data=1000/rate-1;
	data=MPU_Write_Byte(MPU_SAMPLE_RATE_REG,data);	//设置数字低通滤波器
 	return MPU_Set_LPF(rate/2);	//自动设置LPF为采样率的一半
}
 
//得到温度值
//返回值:温度值(扩大了100倍)
float MPU_Get_Temperature(void)
{
  unsigned char  buf[2]; 
  short raw;
  float temp;
  
  MPU_Read_Len(MPU_TEMP_OUTH_REG,2,buf); 
  raw=(buf[0]<<8)| buf[1];  
  temp=(36.53+((double)raw)/340)*100;  
//  temp = (long)((35 + (raw / 340)) * 65536L);
  return temp/100.0f;
}
//得到陀螺仪值(原始值)
//gx,gy,gz:陀螺仪x,y,z轴的原始读数(带符号)
//返回值:0,成功
//    其他,错误代码
uint8_t MPU_Get_Gyroscope(short *gx,short *gy,short *gz)
{
    uint8_t buf[6],res;  
	res=MPU_Read_Len(MPU_GYRO_XOUTH_REG,6,buf);
	if(res==0)
	{
		*gx=((uint16_t)buf[0]<<8)|buf[1];  
		*gy=((uint16_t)buf[2]<<8)|buf[3];  
		*gz=((uint16_t)buf[4]<<8)|buf[5];
	} 	
    return res;
}
//得到加速度值(原始值)
//gx,gy,gz:陀螺仪x,y,z轴的原始读数(带符号)
//返回值:0,成功
//    其他,错误代码
uint8_t MPU_Get_Accelerometer(short *ax,short *ay,short *az)
{
    uint8_t buf[6],res;  
	res=MPU_Read_Len(MPU_ACCEL_XOUTH_REG,6,buf);
	if(res==0)
	{
		*ax=((uint16_t)buf[0]<<8)|buf[1];  
		*ay=((uint16_t)buf[2]<<8)|buf[3];  
		*az=((uint16_t)buf[4]<<8)|buf[5];
	} 	
    return res;;
}
 
 
//IIC连续写
uint8_t MPU_Write_Len(uint8_t reg,uint8_t len,uint8_t *buf)
{
  extern I2C_HandleTypeDef hi2c1;
  HAL_I2C_Mem_Write(&hi2c1, MPU_WRITE, reg, I2C_MEMADD_SIZE_8BIT, buf, len, 0xfff);
  HAL_Delay(100);
  
  return 0;
}
//IIC连续读
//addr:器件地址
//reg:要读取的寄存器地址
//len:要读取的长度
//buf:读取到的数据存储区
//返回值:0,正常
//    其他,错误代码
uint8_t MPU_Read_Len(uint8_t reg,uint8_t len,uint8_t *buf)
{ 
  extern I2C_HandleTypeDef hi2c1;
  HAL_I2C_Mem_Read(&hi2c1, MPU_READ, reg, I2C_MEMADD_SIZE_8BIT, buf, len, 0xfff);
  HAL_Delay(100);
  
  return 0;	
}
//IIC写一个字节 
//reg:寄存器地址
//data:数据
//返回值:0,正常
//    其他,错误代码
uint8_t MPU_Write_Byte(uint8_t reg,uint8_t data) 				 
{ 
  extern I2C_HandleTypeDef hi2c1;
  unsigned char W_Data=0;
 
  W_Data = data;
  HAL_I2C_Mem_Write(&hi2c1, MPU_WRITE, reg, I2C_MEMADD_SIZE_8BIT, &W_Data, 1, 0xfff);
  HAL_Delay(100);
  
  return 0;
}
//IIC读一个字节 
//reg:寄存器地址 
//返回值:读到的数据
uint8_t MPU_Read_Byte(uint8_t reg)
{
  extern I2C_HandleTypeDef hi2c1;
  unsigned char R_Data=0;
  
  HAL_I2C_Mem_Read(&hi2c1, MPU_READ, reg, I2C_MEMADD_SIZE_8BIT, &R_Data, 1, 0xfff);
  HAL_Delay(100);
  
  return R_Data;		
}

/* 读取MPU6050数据并加滤波 */
void MpuGetData(void)
{
	uint8_t i;
    uint8_t buffer[12];
	
	HAL_I2C_Mem_Read(&hi2c1, MPU_READ, 0X3B, I2C_MEMADD_SIZE_8BIT, buffer, 6, 0xfff);				/* 读取角加速度 */
	HAL_I2C_Mem_Read(&hi2c1, MPU_READ, 0x43, I2C_MEMADD_SIZE_8BIT, &buffer[6], 6, 0xfff);		/* 读取角速度 */
	
	for(i=0;i<6;i++)
	{
		pMpu[i] = (((int16_t)buffer[i<<1] << 8) | buffer[(i<<1)+1])-MpuOffset[i];							/* 将数据整为16bit,并减去水平校准值 */
		if(i < 3)		/* 角加速度卡尔曼滤波 */
		{
			{
				static struct KalmanFilter EKF[3] = {{0.02,0,0,0,0.001,0.543},{0.02,0,0,0,0.001,0.543},{0.02,0,0,0,0.001,0.543}};	
				kalmanfiter(&EKF[i],(float)pMpu[i]);  
				pMpu[i] = (int16_t)EKF[i].Out;
			}
		}
		if(i > 2)		/* 角速度一阶互补滤波 */
		{	
			uint8_t k=i-3;
			const float factor = 0.15f;  	
			static float tBuff[3];		

			pMpu[i] = tBuff[k] = tBuff[k] * (1 - factor) + pMpu[i] * factor;                
		}
	}	
}

4.2 Kalman和一阶互补滤波滤波代码

kalman.c:

#include "kalman.h"

//一维卡尔曼滤波
void kalmanfiter(struct KalmanFilter *EKF,float input)
{
	EKF->NewP = EKF->LastP + EKF->Q;
	EKF->Kg = EKF->NewP / (EKF->NewP + EKF->R);
	EKF->Out = EKF->Out + EKF->Kg * (input - EKF->Out);
	EKF->LastP = (1 - EKF->Kg) * EKF->NewP;
}

一阶互补滤波:

if(i > 2)		/* 角速度一阶互补滤波 */
{	
	uint8_t k=i-3;
	const float factor = 0.15f;  	/* 互补滤波的影响因子 */
	static float tBuff[3];		
	pMpu[i] = tBuff[k] = tBuff[k] * (1 - factor) + pMpu[i] * factor;                
}

以上 2 种滤波都存在 mpu6050.c 代码中进行调用:

这个适当取 P = 0.02 默认初始第一比数据的协方差不为 0,也就是不承认第一笔数据完全准确。取 Q=0.001,就是认为测量方差的过程噪声比较小,实际上多小也没人知道,随便给个值优化输出。而 R 比较大,默认噪声都是信号采集就已经产生的了(XYZ 三轴加速度都是一个样,所以参数相同 )。

Q/R 决定着滤波的强度,就是收敛速度。收敛速度越快,越容易对正确的信号不信任,导致正常的信号也被衰减,所以滤波有好处有坏处,适当就好。 

现在来介绍一下角速度的滤波。由于角速度不容易受干扰,又需要保持最快速度反应。只要稍微适当给个滤波去除白噪声。本次采用一阶低通滤波。

一阶低通数字滤波器的公式为:
y(n) = q*x(n) + (1-q)*y(n-1)

4.3 IMU代码(姿态解算)

这部分代码其实就是第一章节的四元数姿态求解,具体推导过程和代码思路参考上文,还是建议大家多花点时间消化消化。

imu.h:

#ifndef __IMU_H
#define	__IMU_H

#include "alldata.h"

#define squa( Sq )        (((float)Sq)*((float)Sq))

extern void GetAngle(const _st_Mpu *pMpu,_st_AngE *pAngE, float dt);
extern void imu_rest(void);

#endif

imu.c:

#include "imu.h"
#include "mpu6050.h"
#include <math.h>

const float M_PI = 3.1415926535;
const float RtA = 57.2957795f;
const float AtR = 0.0174532925f;
const float Gyro_G = 0.03051756f*2;	  	//陀螺仪初始化量程+-2000度每秒于1 / (65536 / 4000) = 0.03051756*2		
const float Gyro_Gr = 0.0005326f*2;     //面计算度每秒,转换弧度每秒则 2*0.03051756	 * 0.0174533f = 0.0005326*2

static float NormAcc;

/* 四元数系数 */
typedef volatile struct {
  float q0;
  float q1;
  float q2;
  float q3;
} Quaternion;
Quaternion NumQ = {1, 0, 0, 0};

/* 陀螺仪积分误差 */
struct V{
	float x;
	float y;
	float z;
};	
volatile struct V GyroIntegError = {0};

/* 四元数解法初始化 */
void imu_rest(void)
{
	NumQ.q0 =1;
	NumQ.q1 = 0;
	NumQ.q2 = 0;
	NumQ.q3 = 0;	
	GyroIntegError.x = 0;
	GyroIntegError.y = 0;
	GyroIntegError.z = 0;
	Angle.pitch = 0;
	Angle.roll = 0;
}

void GetAngle(const _st_Mpu *pMpu,_st_AngE *pAngE, float dt) 
{		
	volatile struct V Gravity,Acc,Gyro,AccGravity;

	static  float KpDef = 0.5f ;
	static  float KiDef = 0.0001f;
//static  float KiDef = 0.00001f;
	
	float q0_t,q1_t,q2_t,q3_t;
  //float NormAcc;	
	float NormQuat; 
	float HalfTime = dt * 0.5f;

	//提取等效旋转矩阵中的重力分量 
	Gravity.x = 2*(NumQ.q1 * NumQ.q3 - NumQ.q0 * NumQ.q2);								
	Gravity.y = 2*(NumQ.q0 * NumQ.q1 + NumQ.q2 * NumQ.q3);						  
	Gravity.z = 1-2*(NumQ.q1 * NumQ.q1 + NumQ.q2 * NumQ.q2);	
	// 加速度归一化
	//printf("accX:%d\r\n",MPU6050.accX);
	NormAcc = 1/sqrt(squa(MPU6050.accX)+ squa(MPU6050.accY) +squa(MPU6050.accZ));
	//printf("NorAcc%f\r\n",NormAcc);
	//	NormAcc = Q_rsqrt(squa(MPU6050.accX)+ squa(MPU6050.accY) +squa(MPU6050.accZ));
	
  Acc.x = pMpu->accX * NormAcc;
  Acc.y = pMpu->accY * NormAcc;
  Acc.z = pMpu->accZ * NormAcc;
	
 	//向量差乘得出的值
	AccGravity.x = (Acc.y * Gravity.z - Acc.z * Gravity.y);
	AccGravity.y = (Acc.z * Gravity.x - Acc.x * Gravity.z);
	AccGravity.z = (Acc.x * Gravity.y - Acc.y * Gravity.x);
	
	//再做加速度积分补偿角速度的补偿值
  GyroIntegError.x += AccGravity.x * KiDef;
  GyroIntegError.y += AccGravity.y * KiDef;
  GyroIntegError.z += AccGravity.z * KiDef;
	
	//角速度融合加速度积分补偿值
  Gyro.x = pMpu->gyroX * Gyro_Gr + KpDef * AccGravity.x  +  GyroIntegError.x;//弧度制
  Gyro.y = pMpu->gyroY * Gyro_Gr + KpDef * AccGravity.y  +  GyroIntegError.y;
  Gyro.z = pMpu->gyroZ * Gyro_Gr + KpDef * AccGravity.z  +  GyroIntegError.z;
	
	// 一阶龙格库塔法, 更新四元数
	q0_t = (-NumQ.q1*Gyro.x - NumQ.q2*Gyro.y - NumQ.q3*Gyro.z) * HalfTime;
	q1_t = ( NumQ.q0*Gyro.x - NumQ.q3*Gyro.y + NumQ.q2*Gyro.z) * HalfTime;
	q2_t = ( NumQ.q3*Gyro.x + NumQ.q0*Gyro.y - NumQ.q1*Gyro.z) * HalfTime;
	q3_t = (-NumQ.q2*Gyro.x + NumQ.q1*Gyro.y + NumQ.q0*Gyro.z) * HalfTime;
	
	NumQ.q0 += q0_t;
	NumQ.q1 += q1_t;
	NumQ.q2 += q2_t;
	NumQ.q3 += q3_t;
	// 四元数归一化
	NormQuat = 1/sqrt(squa(NumQ.q0) + squa(NumQ.q1) + squa(NumQ.q2) + squa(NumQ.q3));
	NumQ.q0 *= NormQuat;
	NumQ.q1 *= NormQuat;
	NumQ.q2 *= NormQuat;
	NumQ.q3 *= NormQuat;	
	
	// 四元数转欧拉角
	{
		 
			#ifdef	YAW_GYRO
			*(
		float *)pAngE = atan2f(2 * NumQ.q1 *NumQ.q2 + 2 * NumQ.q0 * NumQ.q3, 1 - 2 * NumQ.q2 *NumQ.q2 - 2 * NumQ.q3 * NumQ.q3) * RtA;  //yaw
			#else
				float yaw_G = pMpu->gyroZ * Gyro_G;
				if((yaw_G > 1.0f) || (yaw_G < -1.0f)) //数据太小可以认为是干扰,不是偏航动作
				{
					pAngE->yaw  += yaw_G * dt;
					printf("Yaw:%f\r\n",pAngE->yaw);
				}
			#endif
			pAngE->pitch  =  asin(2 * NumQ.q0 *NumQ.q2 - 2 * NumQ.q1 * NumQ.q3) * RtA;						
		
			pAngE->roll	= atan2(2 * NumQ.q2 *NumQ.q3 + 2 * NumQ.q0 * NumQ.q1, 1 - 2 * NumQ.q1 *NumQ.q1 - 2 * NumQ.q2 * NumQ.q2) * RtA;	//PITCH 			
			printf("Pitch:%f;\r\n",pAngE->pitch);
			printf("Roll:%f;\r\n",pAngE->roll);
	}
}

4.4 main函数

该阶段下的 main 函数为 while 空循环,MPU6050 读取和解算欧拉角都在 TIM1 的中断回调函数中进行,代码如下:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim == (&htim1))	//如果定时器TIM1中断发生
    {
			MpuGetData();
			GetAngle(&MPU6050,&Angle,0.003f);
			Send_ANOTC();
		}
}

五、匿名上位机3D姿态显示

5.1 匿名上位机概述

匿名上位机为匿名科创团队自主设计的知名上位机,通常用于与无人机或飞行器进行通信和数据传输。它可以通过串口蓝牙Wi-FiUSB等通信方式与无人机连接,并可以实时接收和显示来自无人机的数据,如传感器数据、GPS定位信息、飞行控制指令等。此外,匿名上位机还可以进行数据存储、分析和可视化3D模型,方便用户对无人机数据进行处理和分析(作者本次仅为大家提供串口协议下的可视化3D姿态代码)。

匿名科创官网:welcome [匿名科创] (anotc.com)

5.2 匿名上位机通讯

补充说明:匿名科创团队制作的匿名上位机一直在迭代升级,不同版本之间的通讯协议和功能使用时可能存在一定差异。作者使用的匿名上位机V5.0版本上位机软件在文末资源包内!

匿名科创上位机为使用者提供了详细的通讯协议资料,点击左边程序设置图标,点击红框所示的通信协议。

匿名上位机提供了很多数据传输协议模板,此时飞控项目我们仅考虑飞机姿态的基本信息,从WPS的表格中找到如下信息:

通过上图可以清晰的发现,匿名上位机的通讯是使用数据帧格式的(即存在规定格式的数据格式)。接下来作者帮大家解析一下STATUS帧下的数据格式(飞机姿态信息帧)。

为了方便读者朋友对数据格式的解读,作者这里将匿名上位机的数据帧分为4部分:

第一部分:帧头“AAAA”,匿名上位机每次接受数据都需要“AAAA”格式的帧头打前阵;

第二部分:功能字+长度部分,STATUS帧下的功能字为0x01;▲协议中长度字节LEN表示该数据帧内包含数据的字节总长度,不包括帧头、功能字、长度字节和最后的校验位,只是数据的字节长度和。比如该帧数据内容为3个int16型数据,那么LEN等于6,一个字节(8位)算一个长度。故STATUS帧下的数据长度为:2+2+2+4+1+1=12

第三部分:核心数据部分,该部分为通讯协议中的核心部分。定义一个数组,写入:翻滚角roll*100,俯仰角pitch*100,偏航角yaw*100,0(为使用气压计),0(飞行模式),0(加锁)

第四部分:SUM即校验位,▲SUM等于从该数据帧第一字节开始,也就是帧头开始,至该帧数据的最后一字节所有字节的和,只保留低八位,高位舍去。当然,校验位SUM并非必须的,你可以不写

贴心寄语:在实际嵌入式工程项目中,对于数据帧的处理是非常常见的。很多时候往往需要工程师自己制定和编写收发数据帧格式,作者建议大家可以多磨练这方面的能力。

作者通过串口usart1与匿名上位机进行通讯,通讯协议代码如下,供大家参考:

comm.c代码:

#include "comm.h"
#include "alldata.h"
#include "usart.h"

/* ANOTC匿名站通讯机制(串口版本) */
void Send_ANOTC()
{
	uint8_t i;
	uint8_t len=0;							
	int16_t Anto[12];
	int8_t *pt = (int8_t*)(Anto);		
	
	Anto[2] = (int16_t)(Angle.roll*100);
	Anto[3] = (int16_t)(Angle.pitch*100);
	Anto[4] = -(int16_t)(Angle.yaw*100);
	Anto[5] = 0;//没有高度数据
	Anto[6] = 0;//飞行模式
	Anto[7] = ALL_flag.unlock<<8;//解锁信息
	len = 12;						//数据长度
	
	Anto[0] = 0XAAAA;            //帧头
	Anto[1] = len | 0x01<<8;     //功能字+长度
	pt[len+4] = (int8_t)(0xAA+0xAA);
	
	pt[len+4] = (int8_t)(0xAA+0xAA);
	for(i=2;i<len+4;i+=2)    //a swap with b;
	{
		pt[i] ^= pt[i+1];
		pt[i+1] ^= pt[i];
		pt[i] ^= pt[i+1];
		pt[len+4] += pt[i] + pt[i+1];
	}
	
	HAL_UART_Transmit(&huart1,pt,len+5,0xFFFF);    //采用串口发送到匿名上位机
}

5.3 匿名上位机使用

匿名上位机V5.0版本提供了3种类型的通讯方式,我们这里选择串口模式,波特率设置为115200,具体如下图所示:

第一步:点击右下角打开连接;第二步:点击飞控状态图标;(作者没有写数据校验位故右下角警告可能出现警告,无视即可!)

我们仅需要关注ROLRITYAW飞控状态即可,可以看出来匿名上位机的使用是非常简单!

六、项目效果

无人机3D姿态显示

七、项目代码

代码地址: 基于STM32的四旋翼无人机项目(二):MPU6050姿态解算(含匿名上位机串口通讯版本代码)-嵌入式文档类资源-CSDN文库icon-default.png?t=N5F7https://download.csdn.net/download/black_sneak/87934739

如果积分不够的朋友,点波关注评论区留下邮箱,作者无偿提供源码和后续问题解答。求求啦关注一波吧 !!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/30755.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

软件设计模式之原型模式

一.定义 原型模式(Prototype Pattern)的简单程度仅次于单例模式和迭代器模式。正是由于简单&#xff0c;使用的场景才非常地多&#xff0c;其定义如下: Specify the kinds of objects to create using a prototypical instance, and create new objects by copyingthis protot…

第七章 测试

文章目录 第七章 测试7.1 编码7.1.1 选择程序设计语言1. 计算机程序设计语言基本上可以分为汇编语言和高级语言2. 从应用特点看&#xff0c;高级语言可分为基础语言、结构化语言、专用语言 7.1.2 编码风格 7.2 软件测试基础7.2.1 软件测试的目标7.2.2 软件测试准则7.2.3 测试方…

边缘智能:边缘计算驱动实时深度学习

边缘智能 作为人工智能领域的当红炸子鸡&#xff0c;深度学习技术近年来得到了学术界与产业界的大力追捧。目前&#xff0c;深度学习技术已在计算机视觉、自然语言处理以及语音识别等领域大放异彩&#xff0c;相关产品正如雨后春笋般涌现。由于深度学习模型需要进行大量的计算…

Delta 一个新的 git diff 对比显示工具

目录 介绍git diff 介绍delta介绍 一、安装1.下载 Git2.下载 delta3.解压4.修改配置文件5. 修改主题6.其他配置和说明 二、对比命令1.在项目中 git diff 常用命令2.对比电脑上两个文件3.对比电脑上的两个文件夹 三、在Git 命令行中使用效果四、在idea 的Terminal命令行中使用效…

linux 内核版本和发行版本

当要明确自己的Linux系统的版本号时&#xff0c;大多数情况是用命令确定Linux内核版本的。不过这个还是要与CentOS的版本号&#xff08;就是你使用的Linux系统的发行版本&#xff09;区分开来&#xff0c;这两个不是一个东西。 一、发行版本号 比如当时安装CentOS时&#x…

MySQL是什么,如何整合SpringBoot,以及使用优势

目录 一、MySQL是什么 二、如何整合SpringBoot 三、MySQL使用优势 一、MySQL是什么 MySQL是一种开源的关系型数据库管理系统&#xff0c;采用客户机/服务器模式实现数据存储和管理。其最初由瑞典的MySQL AB公司开发&#xff0c;后来被Sun Microsystems收购&#xff0c;最终…

【Python 随练】寻找完数

题目&#xff1a; 一个数如果恰好等于它的因子之和&#xff0c;这个数就称为"完数"。例如 61&#xff0b;2&#xff0b;3.编程找出 1000 以内的所有完数。 简介&#xff1a; 在本篇博客中&#xff0c;我们将解决一个数学问题&#xff1a;如何找出 1000 以内的所有…

AI绘画基于 Kaggle 10 分钟搭建 Stable Diffusion(保姆级教程)

AI绘画基于 Kaggle 10 分钟搭建 Stable Diffusion&#xff08;保姆级教程&#xff09; 一、引言二、安装教程1. 注册 Kaggle2. Edit My Copy3. 进行手机号的验证4. 打开 “internet off” 开关&#xff0c;并选择显卡5. 开启 session&#xff0c;运行脚本 三、主界面介绍四、注…

Windows提示“找不到rgss202j.dll”怎么办?

Rgss202j.dll文件是Windows操作系统最重要的系统文件之一&#xff0c;它包含了一组程序和驱动函数。如果此文件丢失或损坏&#xff0c;驱动程序将无法正常工作&#xff0c;并且相应的应用程序也将无法正常启动且运行。通常情况下&#xff0c;造成Rgss202j.dll文件无法找到的原因…

计算机的工作过程和主要性能指标

一、计算机的工作过程 为使计算机按预定要求工作&#xff0c;首先要编制程序。 程序是一个特定的指令序列&#xff0c;它告诉计算机要做哪些事&#xff0c;按什么步骤去做。 指令是一组二进制信息的代码&#xff0c;用来表示计算机所能完成的基本操作。 编制好的程序放在主存中…

redis 的基本介绍以及 五种 数据类型

一、redis是什么&#xff1f; 一句话&#xff1a;redis 是一个开源的、使用C语言编写的、支持网络交互&#xff0c;基于内存也可持久化的 key-value &#xff08;非关系型&#xff09;数据库 redis作者博客&#xff0c;有兴趣的小伙伴可以去逛一逛&#xff1a;http://github.…

Android 9 蓝牙协议初始化

先讲一下Application类的使用 要使用自定义的Application&#xff0c;首先就是要自己新建一个Application的子类&#xff0c;然后把它的名字写在manifest文件里面的application标签里的android:name属性就行&#xff0c;如我的Application子类名字是BaseApplication&#xff0c…

Selenium 环境配置

如果你做过 Web 测试的工作&#xff0c;那么你应该明白 Web 测试中最重要的一部分工作就是自动化测试。自动化测试&#xff0c;顾名思义就是让浏览器自动运行&#xff0c;而无需手动操作。这和我们爬虫工作原理有些相似&#xff0c;我们爬虫也需要让浏览器运行网址来获取我们需…

基于QFT的量子加法器的原理与实现-mindspore quantum

1 量子Fourier变换 离散Fourier变换以一一个复向量 x 0 , . . . , x N − 1 {x_0},...,{x_{N - 1}} x0​,...,xN−1​为输入&#xff0c;输出的数据是如下复向量 y 0 , . . . , y N − 1 {y_0},...,{y_{N - 1}} y0​,...,yN−1​&#xff1a; y k ≡ 1 N ∑ j 0 N − 1 x j …

Debian12.0.0更换系统语言中文到英文

6月10号&#xff0c;Debian12.0.0更新&#xff0c;想尝尝鲜&#xff0c;在虚拟机里安装好&#xff0c;想将中文改为英文&#xff0c;因为Terminal下输入命令&#xff0c;中文切换麻烦。 一、步骤如下 #1、查看当前语言环境 env | grep LANG #2、en表示语言&#xff0c;US表示…

ffmpeg capture decklink

确保decklink设备已经接入 lspci | grep Blackm02:00.0 Multimedia video controller: Blackmagic Design DeckLink SDI Micro 确保decklink驱动已经正确安装 lsmod | grep blackmagicblackmagic_io 2068480 2 确保ffmpeg已经启用decklink&#xff0c;如何配置之前的博文已经…

岩土工程监测案例:完整链条的振弦传感器、采集仪和在线监测系统

岩土工程监测案例&#xff1a;完整链条的振弦传感器、采集仪和在线监测系统 在岩土工程监测中&#xff0c;振弦传感器被广泛应用于测量土体或岩体的振动情况&#xff0c;以了解地震或其他振动事件对结构物或地基的影响。振弦传感器具有高精度、快速响应、易于安装和低成本等优…

C语言指针初阶+进阶(看这一篇就够了)

目录 本章重点 1. 指针是什么 2. 指针和指针类型 3. 野指针 4. 指针运算 5. 指针和数组 6. 二级指针 7. 指针数组 8. 字符指针 9.数组指针 10. 指针数组 11数组传参和指针传参 12. 函数指针 13. 函数指针数组 14. 指向函数指针数组的指针 15. 回调函数 16 指针和数组面试题的解…

c++lambda函数笔记

1、labmda函数用途&#xff1a; 用于简短功能函数的定义&#xff0c;并传递到std算法中。 2、一般函数与lambda函数比较示例 3、如何定义lambda 如下为lambda通用定义式子&#xff1a; [capture] (params) opt->ret{body;}; capture——捕获列表&#xff0c;[]为不捕获变量…

制造执行系统(MES)的核心功能是什么?

制造执行系统&#xff08;MES&#xff09;的核心功能是什么? 01 什么是MES 制造执行系统&#xff08;MES&#xff09;是一种用于监控、控制和优化制造过程的软件系统。它通过与企业资源计划&#xff08;ERP&#xff09;系统和自动化系统的集成&#xff0c;实现对生产过程的管…