imuncle.github.io icon indicating copy to clipboard operation
imuncle.github.io copied to clipboard

MPU9250姿态解析

Open imuncle opened this issue 5 years ago • 9 comments

前段时间尝试使用陀螺仪作为机器人云台的位置环反馈值,在底盘跟随的大前提下,这样做是更符合第一视角的操作的。

我去年比赛的时候买了几个维特智能的型号为WT901的九轴,当时时间紧没有用上,结果不用不知道,一用吓一跳,陀螺仪延迟高的吓人,使用J-Scope看到的图像如下: image

从图中可以看出延迟大约有200ms,这种延时已经不再具备方位参考值了。

我在仓库里东翻西找,找到了两个三十多块的MPU9250,相比于我买的一百来块的陀螺仪来说便宜了很多,于是我开始尝试手动解析数据。

读取原始数据

MPU9250内部其实是由6轴陀螺仪MPU6500和磁力计AK8963组成,可以测量三轴加速度,三轴角速度和三轴磁场强度,此外还可以测量温度,但这里我没有用到温度的数据。

数据解析的第一步就是读取原始数据。MPU6500支持IIC通信和SPI通信,AK8963只支持IIC通信,但可以通过MPU9250为中介使用SPI通信,只是比较麻烦,所以这里我选择了IIC通信。

IIC地址

MPU9250的IIC地址取决于AD0口的电平高低,如果AD0接GND,则地址为0x68,如果接VCC,则地址为0x69,。

这里说一下一个坑:

在读取时,注意需要人工将地址左移1位(I2C读写为左对齐,第8位要存读写标志位)

最开始我不知道这个,一直把地址写成0x68,结果死活读不到数据,后来才发现要写成0xD0。

MPU9250的其他地址可以查看它的寄存器数据手册,我这里列出了我用到的一部分寄存器:

#define MPU9250_ADDRESS 0xD0   //AD0接GND时地址为0x68,接VCC时地址为0x69
#define MPU_PWR_MGMT1_REG		0X6B	//电源管理寄存器1
#define MPU_GYRO_CFG_REG		0X1B	//陀螺仪配置寄存器
#define MPU_ACCEL_CFG_REG		0X1C	//加速度计配置寄存器
#define MPU_SAMPLE_RATE_REG		0X19	//陀螺仪采样频率分频器
#define MPU_INT_EN_REG			0X38	//中断使能寄存器
#define MPU_USER_CTRL_REG		0X6A	//用户控制寄存器
#define MPU_FIFO_EN_REG			0X23	//FIFO使能寄存器
#define MPU_INTBP_CFG_REG		0X37	//中断/旁路设置寄存器
#define MPU_DEVICE_ID_REG		0X75	//器件ID寄存器
#define MPU_PWR_MGMT2_REG		0X6C	//电源管理寄存器2
#define MPU_CFG_REG				0X1A	//配置寄存器 低通滤波器配置寄存器

#define MPU_TEMP_OUTH_REG		0X41	//温度值高8位寄存器
#define MPU_TEMP_OUTL_REG		0X42	//温度值低8位寄存器

#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_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 AK8963_ADDRESS 0x18		//磁力计地址0x0C
#define AK8963_CNTL1		0x0A  //磁力计读取寄存器
#define AK8963_HXL			0x03	//磁力计X轴低8位寄存器
#define AK8963_HXH			0x04	//磁力计X轴高8位寄存器
#define AK8963_HYL			0x05	//磁力计Y轴低8位寄存器
#define AK8963_HYH			0x06	//磁力计Y轴高8位寄存器
#define AK8963_HZL			0x07	//磁力计Z轴低8位寄存器
#define AK8963_HZH			0x08	//磁力计Z轴高8位寄存器

配置MPU9250

读取数据之前要对MPU9250进行一系列的配置,比如设置采样频率,设置量程等等,具体的如下:

void MPU9250_Init(void)
{
	unsigned char pdata;
	//检查设备是否准备好
	HAL_I2C_IsDeviceReady(&hi2c1, MPU9250_ADDRESS, 10, HAL_MAX_DELAY);
	//检查总线是否准备好
	HAL_I2C_GetState(&hi2c1);

	pdata=0x80; //复位MPU
	HAL_I2C_Mem_Write(&hi2c1, MPU9250_ADDRESS, MPU_PWR_MGMT1_REG, 1, &pdata, 1, HAL_MAX_DELAY);
	HAL_I2C_IsDeviceReady(&hi2c1, MPU9250_ADDRESS, 10, HAL_MAX_DELAY);
	
	HAL_Delay(500);  //复位后需要等待一段时间,等待芯片复位完成

	pdata=0x01;	//唤醒MPU
	HAL_I2C_Mem_Write(&hi2c1, MPU9250_ADDRESS, MPU_PWR_MGMT1_REG, 1, &pdata, 1, HAL_MAX_DELAY);

	pdata=3<<3; //设置量程为2000
	HAL_I2C_Mem_Write(&hi2c1, MPU9250_ADDRESS, MPU_GYRO_CFG_REG, 1, &pdata, 1, HAL_MAX_DELAY); 

	pdata=01;	//设置加速度传感器量程±4g
	HAL_I2C_Mem_Write(&hi2c1, MPU9250_ADDRESS, MPU_ACCEL_CFG_REG, 1, &pdata, 1, HAL_MAX_DELAY); 

	pdata=0; //陀螺仪采样分频设置
	HAL_I2C_Mem_Write(&hi2c1, MPU9250_ADDRESS, MPU_SAMPLE_RATE_REG, 1, &pdata, 1, HAL_MAX_DELAY); 

	pdata=0;	//关闭所有中断
	HAL_I2C_Mem_Write(&hi2c1, MPU9250_ADDRESS, MPU_INT_EN_REG, 1, &pdata, 1, HAL_MAX_DELAY); 
	
	pdata=0;	//关闭FIFO
	HAL_I2C_Mem_Write(&hi2c1, MPU9250_ADDRESS, MPU_FIFO_EN_REG, 1, &pdata, 1, HAL_MAX_DELAY); 
	
	pdata=0X02;	//设置旁路模式,直接读取AK8963磁力计数据
	HAL_I2C_Mem_Write(&hi2c1, MPU9250_ADDRESS, MPU_INTBP_CFG_REG, 1, &pdata, 1, HAL_MAX_DELAY); 
	HAL_Delay(10);	//需要一段延时让磁力计工作
	
	pdata = 4;	//设置MPU9250的数字低通滤波器
	HAL_I2C_Mem_Write(&hi2c1, MPU9250_ADDRESS, MPU_CFG_REG, 1, &pdata, 1, HAL_MAX_DELAY);

	pdata=0;	//使能陀螺仪和加速度工作
	HAL_I2C_Mem_Write(&hi2c1, MPU9250_ADDRESS, MPU_PWR_MGMT2_REG, 1, &pdata, 1, HAL_MAX_DELAY);
	
	pdata = 0x01;
	HAL_I2C_Mem_Write(&hi2c1, AK8963_ADDRESS, AK8963_CNTL1, 1, &pdata, 1, HAL_MAX_DELAY);
	HAL_Delay(10);
}

读取数据

配置好了之后就可以读取数据了,这就很简单了,直接从指定寄存器读取就可以了。这里要注意的是磁力计不能读取太频繁,AK8963本身的数据采样就有一个硬件上限,大约200Hz左右,但实测只能以一百多赫兹的频率读取。且每次读取都需要往磁力计的CNTL1寄存器写1,否则会读不到数据。

读取数据的具体步骤如下:

void GetImuData(void)
{
	uint8_t imu_data[14]={0};
	uint8_t mag_data[6] = {0};
	uint8_t pdata;
	short accx,accy,accz;
	short gyrox,gyroy,gyroz;
	short magx,magy,magz;
	
	float gyro_sensitivity = 16.384f;
	int acc_sensitivity = 8192;
	
	static short mag_count = 0;
	
	HAL_I2C_Mem_Read(&hi2c1, MPU9250_ADDRESS, MPU_ACCEL_XOUTH_REG, 1, imu_data, 14, HAL_MAX_DELAY);	//读取陀螺仪和加速度计的数据
	accx = (imu_data[0]<<8)|imu_data[1];
	accy = (imu_data[2]<<8)|imu_data[3];
	accz = (imu_data[4]<<8)|imu_data[5];
	gyrox = (imu_data[8]<<8)|imu_data[9];
	gyroy = (imu_data[10]<<8)|imu_data[11];
	gyroz = (imu_data[12]<<8)|imu_data[13];
	
	mpu9250.gyro.x = (float)(gyrox-GYROX_BIAS)/gyro_sensitivity;
	mpu9250.gyro.y = (float)(gyroy-GYROY_BIAS)/gyro_sensitivity;
	mpu9250.gyro.z = (float)(gyroz-GYROZ_BIAS)/gyro_sensitivity;
	
	mpu9250.acc.x = (float)(accx-ACCX_BIAS)/acc_sensitivity;
	mpu9250.acc.y = (float)(accy-ACCY_BIAS)/acc_sensitivity;
	mpu9250.acc.z = (float)(accz-ACCZ_BIAS)/acc_sensitivity;
	
	mag_count++;
	if(mag_count == 10)	//磁力计不能读取太频繁
	{
		HAL_I2C_Mem_Read(&hi2c1, AK8963_ADDRESS, AK8963_HXL, 1, mag_data, 6, HAL_MAX_DELAY);	//读取磁力计数据
		magx = (mag_data[0]<<8)|mag_data[1];
		magy = (mag_data[2]<<8)|mag_data[3];
		magz = (mag_data[4]<<8)|mag_data[5];
		mpu9250.mag.x = (float)magy/1000.0f;		//磁力计的坐标方位不同
		mpu9250.mag.y = (float)magx/1000.0f;
		mpu9250.mag.z = -(float)magz/1000.0f;
		pdata = 1;
		HAL_I2C_Mem_Write(&hi2c1, AK8963_ADDRESS, AK8963_CNTL1, 1, &pdata, 1, HAL_MAX_DELAY);	//为下一次读取磁力计数据做准备
		mag_count = 0;
	}
}

这个函数的执行为1kHz,也就是加速度和陀螺仪数据采样频率是1kHz,磁力计采样频率为100Hz。

姿态解析

光拿到这九轴的数据是不行的,对我们有用的是欧拉方位角,那么拿到数据之后怎么才能得到欧拉角呢?使用四元数就能解决这个问题。

四元数

image

说实话四元数真不太容易理解,我现在也还是一脸懵逼,但不用慌,我们只需要掌握四元数与欧拉角之间的转换就行了,公式如下:

image

image

对四元数本身感兴趣的可以看这个视频(地址),看过之后我还是不太懂。。。

那么怎么根据我们现有的九轴数据求解四元数呢?

其实都用不到九轴,我们只需要用到陀螺仪的数据,也就是角速度就可以求解到四元数,计算公式如下:

image

(这里利用一阶龙格库塔公式进行积分求解四元数)

也就是说姿态解析中陀螺仪才是主角,我们通过陀螺仪积分得到角度,但是时间久了陀螺仪会产生误差,也就是漂移,时间短还可以忍受,时间一长误差就很大了,所以需要加速度和磁力计来纠正陀螺仪的数据。

互补滤波

加速度计主要测量的是地球的重力加速度,根据重力加速度在三个轴方向的分量可以推算出当前物体的姿态。磁力计也是相同的道理。

加速度计可以纠正pitch轴和roll轴的角度误差,因为航向角不管指向何方,加速度的读数都是一样的,所以需要磁力计来纠正yaw轴的角度误差。

那么怎么纠正呢?我这里使用的是互补滤波。

因为我们要求的是物体坐标相对于地面坐标的三轴偏转角度,所以要把加速度的各个分量(在物体的坐标系下)转换为在地面坐标的分量。进而得到物体的方位,因为陀螺仪本身有误差,所以由陀螺仪计算出来的方位肯定与加速度计算出来的方位有误差,这个误差用向量的叉乘表示,将这个误差叠加在陀螺仪计算出来的结果上就可以抵消误差,进而达到纠正角度的作用。磁力计同上。

这里使用互补滤波来进行纠正。公式如下:

image

这里构造了一个比例-积分控制器,有一个比例系数和一个积分系数。比例系数越大,表示越信任加速度计和磁力计,积分系数越大,表示越信任陀螺仪。

image

把误差叠加上去就行了。

上述步骤不断循环,最后使误差越来越小,角度越来越接近真实值。

代码参考

最后附上我的姿态解析代码:

void imuUpdate(struct Axisf acc, struct Axisf gyro, struct Axisf mag)
{
	float q0q0 = q0 * q0;
	float q1q1 = q1 * q1;
	float q2q2 = q2 * q2;
	float q3q3 = q3 * q3;

	float q0q1 = q0 * q1;
	float q0q2 = q0 * q2;
	float q0q3 = q0 * q3;
	float q1q2 = q1 * q2;
	float q1q3 = q1 * q3;
	float q2q3 = q2 * q3;
	
	float normalise;
	float ex, ey, ez;
	float halfT;
	float hx, hy, hz, bx, bz;
	float vx, vy, vz, wx, wy, wz;
	
	now_update = HAL_GetTick(); //单位ms
	halfT = ((float)(now_update - last_update) / 2000.0f);
	last_update = now_update;
	
	gyro.x *= DEG2RAD;	/*度转弧度*/
	gyro.y *= DEG2RAD;
	gyro.z *= DEG2RAD;
	
	/* 对加速度计数据进行归一化处理 */
	if(acc.x != 0 || acc.y != 0 || acc.z != 0)
	{
		normalise = sqrt(acc.x * acc.x + acc.y * acc.y + acc.z * acc.z);
		acc.x /= normalise;
		acc.y /= normalise;
		acc.z /= normalise;
	}
	
	/* 对磁力计数据进行归一化处理 */
	if(mag.x != 0 || mag.y != 0 || mag.z != 0)
	{
		normalise = sqrt(mag.x * mag.x + mag.y * mag.y + mag.z * mag.z);
		mag.x /= normalise;
		mag.y /= normalise;
		mag.z /= normalise;
	}
	
	/* 计算磁力计投影到物体坐标上的各个分量 */
	hx = 2.0f*mag.x*(0.5f - q2q2 - q3q3) + 2.0f*mag.y*(q1q2 - q0q3) + 2.0f*mag.z*(q1q3 + q0q2);
	hy = 2.0f*mag.x*(q1q2 + q0q3) + 2.0f*mag.y*(0.5f - q1q1 - q3q3) + 2.0f*mag.z*(q2q3 - q0q1);
	hz = 2.0f*mag.x*(q1q3 - q0q2) + 2.0f*mag.y*(q2q3 + q0q1) + 2.0f*mag.z*(0.5f - q1q1 - q2q2);         
	bx = sqrt((hx*hx) + (hy*hy));
	bz = hz; 
	
	/* 计算加速度计投影到物体坐标上的各个分量 */
	vx = 2.0f*(q1q3 - q0q2);
	vy = 2.0f*(q0q1 + q2q3);
	vz = q0q0 - q1q1 - q2q2 + q3q3;
	
	/* 处理过后的磁力计新分量 */
	wx = 2.0f*bx*(0.5f - q2q2 - q3q3) + 2.0f*bz*(q1q3 - q0q2);
	wy = 2.0f*bx*(q1q2 - q0q3) + 2.0f*bz*(q0q1 + q2q3);
	wz = 2.0f*bx*(q0q2 + q1q3) + 2.0f*bz*(0.5f - q1q1 - q2q2); 
	
	/* 叉积误差累计,用以修正陀螺仪数据 */
	ex = (acc.y*vz - acc.z*vy) + (mag.y*wz - mag.z*wy);
	ey = (acc.z*vx - acc.x*vz) + (mag.z*wx - mag.x*wz);
	ez = (acc.x*vy - acc.y*vx) + (mag.x*wy - mag.y*wx);
	
	/* 互补滤波 PI */
	exInt += ex * Ki * halfT;
	eyInt += ey * Ki * halfT;	
	ezInt += ez * Ki * halfT;
	gyro.x += Kp*ex + exInt;
	gyro.y += Kp*ey + eyInt;
	gyro.z += Kp*ez + ezInt;
	
	/* 使用一阶龙格库塔更新四元数 */
	q0 += (-q1 * gyro.x - q2 * gyro.y - q3 * gyro.z) * halfT;
	q1 += ( q0 * gyro.x + q2 * gyro.z - q3 * gyro.y) * halfT;
	q2 += ( q0 * gyro.y - q1 * gyro.z + q3 * gyro.x) * halfT;
	q3 += ( q0 * gyro.z + q1 * gyro.y - q2 * gyro.x) * halfT;
	
	/* 对四元数进行归一化处理 */
	normalise = sqrt(q0 * q0 + q1 * q1 + q2 * q2 + q3 * q3);
	q0 /= normalise;
	q1 /= normalise;
	q2 /= normalise;
	q3 /= normalise;
	
	/* 由四元数求解欧拉角 */
	mpu9250.attitude.x = -asinf(-2*q1*q3 + 2*q0*q2) * RAD2DEG;	//pitch
	mpu9250.attitude.y = atan2f(2*q2*q3 + 2*q0*q1, -2*q1*q1 - 2*q2*q2 + 1) * RAD2DEG;	//roll
	mpu9250.attitude.z = atan2f(2*q1*q2 + 2*q0*q3, -2*q2*q2 - 2*q3*q3 + 1) * RAD2DEG;	//yaw
	
	yaw = mpu9250.attitude.z*100;		//用于J-Scope读取
	pitch = mpu9250.attitude.x*100;
	roll = mpu9250.attitude.y*100;
}

参考

  1. 【教程】四旋翼飞行器姿态解算算法入门学习-Rick Grimes
  2. [UVA]Pixhawk之姿态解算篇(2)

imuncle avatar Mar 18 '19 10:03 imuncle

太强啦!

LUCKandII avatar Mar 18 '19 10:03 LUCKandII

附上最后的效果: image 红线是电机码盘,绿线是陀螺仪数据(yaw轴),几乎完全重合,并且无漂移

imuncle avatar Mar 18 '19 15:03 imuncle

谢老板🐮b

pinocchiolhw avatar Jul 26 '19 14:07 pinocchiolhw

Thanks

lauchinyuan avatar Aug 22 '19 08:08 lauchinyuan

博主你好,我想参考一下您关于磁力计数据读取函数的代码可以吗,提前谢谢啦。

zcf0619 avatar Jan 15 '21 03:01 zcf0619

前辈你好,如果可以的话可以分享一下您完整的工程吗。

zcf0619 avatar Jan 20 '21 03:01 zcf0619

@zcf0619 在这里: https://github.com/imuncle/Embedded_Peripheral_Libs

imuncle avatar Jan 25 '21 03:01 imuncle

你好前辈,非常感谢!

在2021年1月25日 11:40,big_uncle[email protected] 写道:

@zcf0619 在这里: https://github.com/imuncle/Embedded_Peripheral_Libs

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or unsubscribe.

zcf0619 avatar Jan 25 '21 03:01 zcf0619

前辈你好, 冒昧致函。想向您请教在AHRS解算中,Kp和Ki参数的大致范围应该如何确定呢。 望不吝赐教。

赵存飞

北京航空航天大学宇航学院 图像处理中心 通讯地址:北京市昌平区沙河高教园南三街9号 邮政编码:102206 在2021年1月25日 11:42,赵存飞[email protected] 写道:

你好前辈,非常感谢!

在2021年1月25日 11:40,big_uncle[email protected] 写道:

@zcf0619 在这里: https://github.com/imuncle/Embedded_Peripheral_Libs

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or unsubscribe.

zcf0619 avatar Jan 28 '21 04:01 zcf0619