MPU6050使用一阶互补和卡尔曼滤波算法平滑角度数据

时间:2024-03-02 18:25:36

最近项目上想用MPU6050来自动探测物体的转向角度,花了2天时间学习如何拿陀螺仪的姿态角度,发现蛮难的,写点笔记。
下面是哔哩哔哩的一堆废话讲解,只想看代码本体的可以直接跳到最后。

应用场景是51单片机环境,有一块MPU6060,需要知道硬件板子水平摆放时,板子摆放的姿态和旋转的角度。编译环境只能用C语言。
首先单片机通过TTL串口接到MPU6050上拿到通信数据,水平旋转角度需要另外加地磁仪通过南北极磁性拿到。很遗憾设计硬件时没注意这茬,只用了一块MPU6050。不过呢可以用旋转时的角速度求出旋转幅度(这个下篇说)。但是拿到原始数据后,发现原始数据的跳动非常厉害,需要用带滤波的积分算法平滑过滤。

开始网上翻相关的算法,看到卡尔曼滤波和互补滤波算法,但是没有能跑起来的代码。但是很多纯写理论,我这种数学白痴完全看不懂。要么直接甩代码,云里雾里不明就里,根据理论推论很多代码甚至是错误的,各种计算方式,变量和函数名字也完全理解不了。
又翻了很久,在某代码里发现一句注释,找到国外TKJ Electronics的一系列代码,使用结合卡尔曼滤波和互补滤波算法的演示。终于搞了份可以用的代码。
TKJElectronics在github上有两份代码( https://github.com/TKJElectronics),KalmanFilter和Example-Sketch-for-IMU-including-Kalman-filter,分别是卡尔曼滤波算法,和IMU融合卡尔曼滤波的样例。

代码演示了计算Roll,Pitch角和Yaw角并用卡尔曼过滤算法。
样例里面有四种芯片的演示,我用的是MPU6050,就直接看这个目录了,里面还有MPU6050+HMC5883L磁力计的样式,可惜手头板子当初没有想过算Yaw角旋转要磁力计,也就莫法实现读取Yaw角。加HMC5883L磁力计的那个样例,是读的磁力计的数据来算Yaw轴角度。
MPU6050的重力加速度出来的z轴数据基本不咋变化,仅依靠x和y轴数据肯定不准的,所以这里通过重力加速度算出来Yaw轴的角度无意义。

继续回来说代码。
下载回来的样例代码扩展名是.ino,搞不懂啥,改成.c,一样看,c语言万岁!
首先要先拿到陀螺仪的数据,角速度和重力加速度。
如何读取MPU6050的数据我略过。网上有很多现成的样例,直接拿来用。

/* IMU Data */
float accX, accY, accZ;
float gyroX, gyroY, gyroZ;
accX = ((i2cData[0] << 8) | i2cData[1]);
accY = ((i2cData[2] << 8) | i2cData[3]);
accZ = ((i2cData[4] << 8) | i2cData[5]);
//tempRaw = (i2cData[6] << 8) | i2cData[7];
gyroX = (i2cData[8] << 8) | i2cData[9];
gyroY = (i2cData[10] << 8) | i2cData[11];
gyroZ = (i2cData[12] << 8) | i2cData[13];

i2cData是MPU6050读到的14个字节的数据。
i2cData[6]和[7]是温度不用。
6个变量,样例里是double类型,我改float了,用不到那么高精度,且我是51单片机运算,计算资源极其有限。

acc打头的变量是重力加速度,gyro打头的变量是角速度。
这里角速度和重力加速度容易让人搞晕。
角速度反应的是芯片的运动加速度,静止时是个很小值,芯片运动的时候XYZ三轴值会对应速度变化。
重力加速度跟重力有关,反应芯片的摆放状态,垂直于地心引力。要获取芯片的摆放姿态,只需要重力加速度就够了。

芯片读出来的数据是16位,数据范围是2的16次方65536,10进制整数范围是-32768到32768。
这里要根据MPU6050初始化时,设置的角速度和重力加速度各自的量程换算下来,我设置的角速度是±2000deg/s,重力加速度是2g,分别使用32768除以2和2000就是各自换算单位。

TKJ Electronics样例里面double gyroXrate = gyroX / 131.0; 是用的250deg/s的量程,就是32768/250≈131
btw,2000deg/s是有多快?我感觉一般手机摇一摇这种,250deg/s应该就够吧?
我自己修改的代码中(32768/2)和(32768/2000)是使用的2000deg/s的量程计算单位

accX /= 16384.0f; // (32768 / 2) = 16384.0f
accY /= 16384.0f;
accZ /= 16384.0f;

gyroX /= 16.384f; // (32768 / 2000) = 16.384f
gyroY /= 16.384f;
gyroZ /= 16.384f;

下面是如何根据acc重力加速度计算芯片目前相对地面的两个偏转角度(roll和pitch角)。
上面说过,靠重力加速度无法计算yaw角,所以这里也略过yaw。

// Source: http://www.freescale.com/files/sensors/doc/app_note/AN3461.pdf
// eq. 25 and eq. 26
// atan2 outputs the value of -π to π (radians) - see http://en.wikipedia.org/wiki/Atan2
// It is then converted from radians to degrees
#ifdef RESTRICT_PITCH  // Eq. 25 and 26
	float roll  = atan2(accY, accZ) * RAD_TO_DEG;
	float pitch = atan(-accX / sqrt(accY * accY + accZ * accZ)) * RAD_TO_DEG;
#else    // Eq. 28 and 29
	float roll  = atan(accY / sqrt(accX * accX + accZ * accZ)) * RAD_TO_DEG;
	float pitch = atan2(-accX, accZ) * RAD_TO_DEG;
#endif

这个计算的理论公式我自己没去推算(人笨...)。
国外有个学习网站详细描述了各种计算公式可以收藏参考,我看网上很多文章也就是照搬上面的图片和翻译。
数学达人收藏推荐:
http://www.euclideanspace.com/maths/geometry/rotations/euler/index.html
喜欢刨根问底的理论基础的可以拿去学习。

回来继续说代码。

RAD_TO_DEG 是弧度到度的转换单位,定义如下,我也稍调整了下,把即时计算改成了静态值定义

//#define RAD_TO_DEG (360/PI/2) // 弧度转角度的转换率
//#define DEG_TO_RAD (2*PI/360) // 角度转弧度的转换率
#define RAD_TO_DEG 57.295779513082320876798154814105   // 弧度转角度的转换率
#define DEG_TO_RAD 0.01745329251994329576923690768489 // 角度转弧度的转换率

有个RESTRICT_PITCH宏定义,貌似用来约束atan输出的角度?不同的翻转计算方式不一样?
注释里有链接,里面是算法解释,全是矩阵方程,数学大牛可以看看。
欧拉角三维翻转有个方向死锁问题,不过一般应用,平衡小车,四轴飞行器都不会用到翻转90°,我这里应用场景也是不会发生垂直翻转场景,所以这里我是默认定义没去深究。

接下来就是卡尔曼滤波了,卡尔曼滤波一开始要先设置初始角度,原代码如下

kalmanX.setAngle(roll);
kalmanY.setAngle(pitch);

样例代码是cpp,搞了两个滤波对象对应x和y轴,分别初始化设置。
为了在单片机上跑,我给改成了c形式。

typedef struct
{
	float Q_angle;
	float Q_bias;
	float R_measure;
	float angle; // Reset the angle
	float bias; // Reset bias
	float P[2][2];
	// Unbiased rate calculated from the rate and the calculated bias - you have to call getAngle to update the rate
	float rate;
} KalmanFilter_t;

typedef struct
{
	KalmanFilter_t *pKalmanX;
	KalmanFilter_t *pKalmanY;
	float gyroXangle, gyroYangle; // Angle calculate using the gyro only
	float compAngleX, compAngleY; // Calculated angle using a complementary filter
	float kalAngleX, kalAngleY;  // Calculated angle using a Kalman filter
} KalmanFilterSys_t;

// Need set starting angle
static KalmanFilterSys_t *Get_Kalman_Filter(float roll, float pitch)
{
	KalmanFilterSys_t *pSys = (KalmanFilterSys_t *)calloc(1, sizeof(KalmanFilterSys_t));
	pSys->pKalmanX = (KalmanFilter_t *)calloc(1, sizeof(KalmanFilter_t));
	pSys->pKalmanY = (KalmanFilter_t *)calloc(1, sizeof(KalmanFilter_t));

	/* We will set the variables like so, these can also be tuned by the user */
	pSys->pKalmanX->Q_angle = pSys->pKalmanY->Q_angle = 0.001f;
	pSys->pKalmanX->Q_bias = pSys->pKalmanY->Q_bias = 0.003f;
	pSys->pKalmanX->R_measure = pSys->pKalmanY->R_measure = 0.03f;

	pSys->pKalmanX->angle = roll; // Reset the angle
	pSys->pKalmanY->angle = pitch; // Reset bias

	// Since we assume that the bias is 0 and we know the starting angle (use setAngle),
	// the error covariance matrix is set like so -
	// see: http://en.wikipedia.org/wiki/Kalman_filter#Example_application.2C_technical
	pSys->pKalmanX->P[0][0] = 0.0f;
	pSys->pKalmanX->P[0][1] = 0.0f;
	pSys->pKalmanX->P[1][0] = 0.0f;
	pSys->pKalmanX->P[1][1] = 0.0f;

	pSys->pKalmanY->P[0][0] = 0.0f;
	pSys->pKalmanY->P[0][1] = 0.0f;
	pSys->pKalmanY->P[1][0] = 0.0f;
	pSys->pKalmanY->P[1][1] = 0.0f;

	pSys->gyroXangle = roll;
	pSys->gyroYangle = pitch;
	pSys->compAngleX = roll;
	pSys->compAngleY = pitch;

	return pSys;
};

这里创建结构时,附带就把roll和pitch对应的angle初始化了。
结构创建好,直接设置数据计算即可,同样函数我给改成了c语言函数,同时设置roll和pitch。

float dt = 10.0 / 1000;
Kalman_Fileter_SetAngle(m_pKalmanFilter, roll, pitch, gyroX, gyroY, dt);

这里的dt直接影响一阶互补滤波的计算权重,我测试是10毫秒读一次数据,1秒作为拟合周期,dt就是10.0 / 1000毫秒,需要根据自身使用情况调整。TKJ Electronics的样例是用的1微妙,拟合周期也是1秒。
调用时dt取两次读取数据的实际间隔时间。我是用单片机中断读取数据,延迟时间很稳定,所以取固定值10ms。

注意自己的采样频率,如果是125Hz采样频率,dt间隔时间就是8ms,每1秒钟要读125次角度数据。
卡尔曼滤波的参数用的默认值,几个参数具体如何调整,数学大牛可以继续问看度娘,我也没去细看算法理论。
每次采样数据,经过Kalman_Fileter_SetAngle计算后,打印下各个数据结果看看

char text[260];
_snprintf_s(text, _TRUNCATE,
	_T("roll(%.4f %.4f %.4f %.4f) pitch(%.4f %.4f %.4f %.4f)\n"),
	roll,
	m_pKalmanFilter->gyroXangle,
	m_pKalmanFilter->compAngleX,
	m_pKalmanFilter->kalAngleX,
	pitch, 
	m_pKalmanFilter->gyroYangle,
	m_pKalmanFilter->compAngleY,
	m_pKalmanFilter->kalAngleY);
printf(text);

这里打印的结果,
roll和pitch是原始的计算角度,会看到跳变会比较厉害,时大时小。
gyroXangle和gyroYangl经过dt乘积后,会看到数据基本抑制住跳变,但数据会有一点不断的累加漂移。
compAngleX和compAngleY是角速度与重力加速度算出的偏转角,进行一阶互补过滤后的角度,这个两个数据相对就比较稳定了。
kalAngleX和kalAngleY就是卡尔曼过滤算法后的角度数据了。这两个数据就是最稳定的。

这里的一阶互补过滤默认参数0.093和0.07权重占比,卡尔曼过滤也是用的默认参数,手动翻转电路板晃来晃去,目测计算的结果很精准,感觉不需要再调这个权重比例。
网上其他很多卡尔曼滤波代码拿来测试过,但是这个权重则是各种参数,算法看代码也是各种形式五花八门,实际跑出来的结果乱飘,真心不能直接抄…
可惜以前写的一个绘制频谱的软件代码搞丢了,不然可以实时显示对比下这些不同算法的计算结果曲线,对比看那个代码是准确的且平滑效果最佳。但我目测下,感觉就TKJ Electronics网站上这个样例算法最佳。

最后附上整个我自己整合修改后的卡尔曼角度滤波代码。原版代码是C++,我照顾51单片机里运行改成了C代码。

KalmanFilter.h

#pragma once

//#define RAD_TO_DEG (360/PI/2) // 弧度转角度的转换率
//#define DEG_TO_RAD (2*PI/360) // 角度转弧度的转换率
#define RAD_TO_DEG 57.295779513082320876798154814105  // 弧度转角度的转换率
#define DEG_TO_RAD 0.01745329251994329576923690768489 // 角度转弧度的转换率

// Comment out to restrict roll to ±90deg instead -
// please read: http://www.freescale.com/files/sensors/doc/app_note/AN3461.pdf
#define RESTRICT_PITCH

typedef struct
{
	float Q_angle;
	float Q_bias;
	float R_measure;
	float angle; // Reset the angle
	float bias; // Reset bias
	float P[2][2];
	// Unbiased rate calculated from the rate and the calculated bias - you have to call getAngle to update the rate
	float rate;
} KalmanFilter_t;

typedef struct
{
	KalmanFilter_t *pKalmanX;
	KalmanFilter_t *pKalmanY;
	float gyroXangle, gyroYangle; // Angle calculate using the gyro only
	float compAngleX, compAngleY; // Calculated angle using a complementary filter
	float kalAngleX, kalAngleY;  // Calculated angle using a Kalman filter
} KalmanFilterSys_t;

// Need set starting angle
static KalmanFilterSys_t *Get_Kalman_Filter(float roll, float pitch)
{
	KalmanFilterSys_t *pSys = (KalmanFilterSys_t *)calloc(1, sizeof(KalmanFilterSys_t));
	pSys->pKalmanX = (KalmanFilter_t *)calloc(1, sizeof(KalmanFilter_t));
	pSys->pKalmanY = (KalmanFilter_t *)calloc(1, sizeof(KalmanFilter_t));

	/* We will set the variables like so, these can also be tuned by the user */
	pSys->pKalmanX->Q_angle = pSys->pKalmanY->Q_angle = 0.001f;
	pSys->pKalmanX->Q_bias = pSys->pKalmanY->Q_bias = 0.003f;
	pSys->pKalmanX->R_measure = pSys->pKalmanY->R_measure = 0.03f;

	pSys->pKalmanX->angle = roll; // Reset the angle
	pSys->pKalmanY->angle = pitch; // Reset bias

	// Since we assume that the bias is 0 and we know the starting angle (use setAngle),
	// the error covariance matrix is set like so -
	// see: http://en.wikipedia.org/wiki/Kalman_filter#Example_application.2C_technical
	pSys->pKalmanX->P[0][0] = 0.0f;
	pSys->pKalmanX->P[0][1] = 0.0f;
	pSys->pKalmanX->P[1][0] = 0.0f;
	pSys->pKalmanX->P[1][1] = 0.0f;

	pSys->pKalmanY->P[0][0] = 0.0f;
	pSys->pKalmanY->P[0][1] = 0.0f;
	pSys->pKalmanY->P[1][0] = 0.0f;
	pSys->pKalmanY->P[1][1] = 0.0f;

	pSys->gyroXangle = roll;
	pSys->gyroYangle = pitch;

	pSys->compAngleX = roll;
	pSys->compAngleY = pitch;

	return pSys;
}

// Source: http://www.freescale.com/files/sensors/doc/app_note/AN3461.pdf
//eq. 25 and eq. 26
// atan2 outputs the value of -π to π (radians) - see http://en.wikipedia.org/wiki/Atan2
// It is then converted from radians to degrees
static void Accel_To_Angle(float *p_roll, float *p_pitch, float accX, float accY, float accZ)
{
#ifdef RESTRICT_PITCH // Eq. 25 and 26
	*p_pitch = atan(-accX / sqrt(accY * accY + accZ * accZ)) * RAD_TO_DEG;
	*p_roll = atan2(accY, accZ) * RAD_TO_DEG;
#else     // Eq. 28 and 29
	*p_pitch = atan2(-accX, accZ) * RAD_TO_DEG;
	*p_roll = atan(accY / sqrt(accX * accX + accZ * accZ)) * RAD_TO_DEG;
#endif
};

// The angle should be in degrees and the rate should be in degrees per second and the delta time in seconds
static float Kalman_Filter_GetAngle(KalmanFilter_t *pSys, float newAngle, float newRate, float dt)
{
	// KasBot V2  -  Kalman filter module - http://www.x-firm.com/?page_id=145
	// Modified by Kristian Lauszus
	// See my blog post for more information:  http://blog.tkjelectronics.dk/2012/09/a-practical-approach-to-kalman-filter-and-how-to-implement-it
	// Discrete Kalman filter time update equations - Time Update ("Predict")
	// Update xhat - Project the state ahead
	/* Step 1 */
	pSys->rate = newRate - pSys->bias;
	pSys->angle += dt * pSys->rate;

	// Update estimation error covariance - Project the error covariance ahead
	/* Step 2 */
	pSys->P[0][0] += dt * (dt*pSys->P[1][1] - pSys->P[0][1] - pSys->P[1][0] + pSys->Q_angle);
	pSys->P[0][1] -= dt * pSys->P[1][1];
	pSys->P[1][0] -= dt * pSys->P[1][1];
	pSys->P[1][1] += pSys->Q_bias * dt;

	// Discrete Kalman filter measurement update equations - Measurement Update ("Correct")
	// Calculate Kalman gain - Compute the Kalman gain
	/* Step 4 */
	float S = pSys->P[0][0] + pSys->R_measure; // Estimate error

	/* Step 5 */
	float K[2]; // Kalman gain - This is a 2x1 vector
	K[0] = pSys->P[0][0] / S;
	K[1] = pSys->P[1][0] / S;

	// Calculate angle and bias - Update estimate with measurement zk (newAngle)
	/* Step 3 */
	float y = newAngle - pSys->angle; // Angle difference

	/* Step 6 */
	pSys->angle += K[0] * y;
	pSys->bias += K[1] * y;

	// Calculate estimation error covariance - Update the error covariance
	/* Step 7 */
	float P00_temp = pSys->P[0][0];
	float P01_temp = pSys->P[0][1];

	pSys->P[0][0] -= K[0] * P00_temp;
	pSys->P[0][1] -= K[0] * P01_temp;
	pSys->P[1][0] -= K[1] * P00_temp;
	pSys->P[1][1] -= K[1] * P01_temp;

	return pSys->angle;
};

static void Kalman_Fileter_SetAngle(KalmanFilterSys_t *pSys, float roll, float pitch,
	float gyroXrate, float gyroYrate, float dt)
{
#ifdef RESTRICT_PITCH
	// This fixes the transition problem when the accelerometer angle jumps between -180 and 180 degrees
	if ((roll < -90 && pSys->kalAngleX > 90) || (roll > 90 && pSys->kalAngleX < -90)) {
		pSys->pKalmanX->angle = roll;
		pSys->compAngleX = roll;
		pSys->kalAngleX = roll;
		pSys->gyroXangle = roll;
	}
	else
		pSys->kalAngleX = Kalman_Filter_GetAngle(pSys->pKalmanX, roll, gyroXrate, dt); // Calculate the angle using a Kalman filter

	if (abs(pSys->kalAngleX) > 90)
		gyroYrate = -gyroYrate; // Invert rate, so it fits the restriced accelerometer reading
	pSys->kalAngleY = Kalman_Filter_GetAngle(pSys->pKalmanY, pitch, gyroYrate, dt);
#else
	// This fixes the transition problem when the accelerometer angle jumps between -180 and 180 degrees
	if ((pitch < -90 && pSys->kalAngleY > 90) || (pitch > 90 && pSys->kalAngleY < -90)) {
		pSys->pKalmanY->angle = pitch;
		pSys->compAngleY = pitch;
		pSys->kalAngleY = pitch;
		pSys->gyroYangle = pitch;
	}
	else
		pSys->kalAngleY = Kalman_Filter_GetAngle(pSys->pKalmanY, pitch, gyroYrate, dt); // Calculate the angle using a Kalman filter

	if (abs(pSys->kalAngleY) > 90)
		gyroXrate = -(gyroXrate); // Invert rate, so it fits the restriced accelerometer reading
	pSys->kalAngleX = Kalman_Filter_GetAngle(pSys->pKalmanX, roll, gyroXrate, dt); // Calculate the angle using a Kalman filter
#endif

	pSys->gyroXangle += gyroXrate * dt; // Calculate gyro angle without any filter
	pSys->gyroYangle += gyroYrate * dt;

	//pSys->gyroXangle += kalmanX.getRate() * dt; // Calculate gyro angle using the unbiased rate
	//pSys->gyroYangle += kalmanY.getRate() * dt;

	// Calculate the angle using a Complimentary filter
	pSys->compAngleX = 0.93 * (pSys->compAngleX + gyroXrate * dt) + 0.07 * roll;
	pSys->compAngleY = 0.93 * (pSys->compAngleY + gyroYrate * dt) + 0.07 * pitch;

	// Reset the gyro angle when it has drifted too much
	if (pSys->gyroXangle < -180 || pSys->gyroXangle > 180)
		pSys->gyroXangle = pSys->kalAngleX;
	if (pSys->gyroYangle < -180 || pSys->gyroYangle > 180)
		pSys->gyroYangle = pSys->kalAngleY;
};

static void Delete_Kalman_Filter(KalmanFilterSys_t **ppSys)
{
	free((*ppSys)->pKalmanX);
	free((*ppSys)->pKalmanX);
	free((*ppSys));
	*ppSys = 0;
};