STM32双IIC融合MPU6050与MPU9250的卡尔曼姿态解算
2026/9/9 15:25:44 网站建设 项目流程

简介:面向STM32F1嵌入式开发者,以MPU6050(IIC1)与MPU9250(IIC2)双传感器读取为基础,结合卡尔曼滤波输出pitch、roll、yaw姿态角及指南针角度,适合学习惯性数据融合与姿态解算的入门及进阶实践。工程共179个文件,约1.91MB,以C/H/C++源码为主,辅以IAR与Keil工程文件(uvprojx/uvoptx)、编译生成文件(o/hex/axf)及说明文档,覆盖双路IIC通信配置、传感器寄存器读写、滤波参数调整等关键代码,可帮助读者快速定位逻辑并理解工程结构。已有1066人学习下载。通过学习此工程,读者不仅能了解双路IIC同时挂载不同传感器的设计思路,还能掌握卡尔曼滤波在加速度计、陀螺仪、磁力计数据融合中的实际用法,为平衡车、四轴飞行器、电子罗盘等方向提供可直接参考的代码基础,尤其适合在Keil环境下对照学习实时姿态解算流程。 做姿态解算,绕不开三个核心传感器:陀螺仪拿角速度、加速度计拿重力方向、磁力计拿地磁北方向。这次我在 STM32F103 上同时挂了两颗传感器——MPU6050 走 IIC1,9250(MPU9250)走 IIC2——把 6 轴数据和磁力计数据合起来做卡尔曼滤波,输出俯仰(pitch)、横滚(roll)、偏航(yaw)三个姿态角,外带一路经过倾斜补偿的指南针角度。项目本身不复杂,但真正跑稳却有几步关键动作,尤其是两个传感器默认 I2C 地址相同、磁力计需要单独校准、卡尔曼滤波参数怎么配,这三个点几乎决定了调试是否顺利。这篇就把整个链路从硬件接线到滤波解算,再到问题排查完整复现一遍,适合准备写飞控、平衡车或者想把手头 IMU 模块用起来的开发者参考。

1. 项目整体设计与思路拆解

1.1 为什么用 STM32F1 双 IIC 方案

STM32F103 算是入门级 MCU 里最务实的选择,价格低、资料多、两个硬件 I2C 外设刚好够用。同一个工程里,I2C1 接 MPU6050,I2C2 接 9250,互不干扰。有人会纠结硬件 I2C 不如软件模拟稳定,实测下来,STM32 硬件 I2C 配合阻塞式读写,在 400kHz 下读取 6 轴和磁力计数据完全够用;真正影响稳定性的往往是接线和上拉电阻,而不是芯片外设本身。

标题里的“9250”,我按最常见方案理解为 MPU9250,也就是内部含 AK8963 磁力计的 9 轴传感器。在这个项目里,它主要用来读磁场,配合 MCU 算出指南针角度。如果你手头拿的是单独磁力计模块,比如 HMC5883L 或 QMC5883L,寄存器结构不同,但读取思路完全一致。

1.2 为什么不把两个传感器挂同一条 IIC 总线

这个点值得单独说。MPU6050 和 MPU9250 的默认 I2C 地址都是 0x68,如果挂同一条总线,就必须通过 AD0 引脚改地址,一个设 0x68,另一个设 0x69。不过地址改完还有隐患:总线上的电容负载变大,接线一旦拉长,通信时序容易劣化;而且 MPU9250 内部的磁力计 AK8963 还挂在 0x0C 地址,继续往同一条总线上增加设备,后面出问题很难定位是哪个设备把总线拉死的。

换用两条独立 IIC 总线之后,I2C1 只管 6050,I2C2 只管 9250 和 AK8963,每个总线上的设备少、地址简单,逻辑也干净。调试时还能单独拉一条总线的波形,快速确认哪个设备没响应,不用在混挂环境里猜。对于这种只有两路 I2C 外设的 MCU,两个传感器各占一条是性价比最高的接线方式。

1.3 卡尔曼滤波对比互补滤波

很多人喜欢用互补滤波,代码短、跑得快,几行就能得到角度。但互补滤波本质是把加速度计得到的角度和陀螺仪积分的角速度做加权平均,它对陀螺仪零漂的估计能力弱,长时间静止会看到角度慢慢跑偏。卡尔曼滤波则把陀螺仪的零偏当成状态量,和角度一起估计,每次用加速度计或磁力计观测值去修正,表现要稳不少。

当然,卡尔曼滤波不是万能的,它需要先建模,再调 Q 和 R 两个噪声协方差参数,模型写错或参数差太远,滤波效果还不如互补滤波。这个项目坚持用卡尔曼,是因为偏航角(yaw)要融合磁力计数据,磁力计信号容易被环境磁干扰污染,有量测噪声模型来处理,会比单纯的加权平均稳定得多。

2. 硬件连接与 IIC 初始化

2.1 引脚分配与接线对照

STM32F103 的 I2C1 固定为 PB6(SCL)、PB7(SDA),I2C2 固定为 PB10(SCL)、PB11(SDA),接线如下表:

信号STM32F103MPU6050(IIC1)MPU9250(IIC2)
电源3.3VVCCVCC
GNDGNDGND
SCL1PB6SCL-
SDA1PB7SDA-
SCL2PB10-SCL
SDA2PB11-SDA
AD0GNDAD0-

供电用 3.3V,不要图方便直接上 5V。虽然不少模块板载了稳压,但上拉电阻很容易把 3.3V 的 MCU IO 灌坏。两根 IIC 信号线要各放一个 4.7kΩ 上拉电阻到 3.3V;有些现成模块板上已经焊好上拉,不需要额外加,但也有模块为了兼容 5V 系统没焊,上电后最好先用逻辑分析仪或示波器确认 SCL/SDA 空闲电平是 3.3V 左右。

这个接线方案我在 20cm 杜邦线范围内验证过,400kHz 速度稳定。如果飞线超过 30cm,建议把速度降到 100kHz,或者换带屏蔽的短线,否则波形振铃会带来偶发的 ACK 丢包。

2.2 IIC 总线参数设置

使用 STM32CubeMX 生成工程时,I2C1 和 I2C2 的参数可以按同一套配置,初始化代码大致如下:

void MX_I2C1_Init(void) { hi2c1.Instance = I2C1; hi2c1.Init.ClockSpeed = 400000; hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 = 0; hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; HAL_I2C_Init(&hi2c1); }

I2C2 的代码只是把hi2c1换成hi2c2I2C1换成I2C2,其余相同。MPU6050 数据手册规定 I2C 时钟最大 400kHz,AK8963 也支持 400kHz,所以统一设置为 400kHz。如果模块上的上拉电阻低于 1kΩ,高速传输时信号会有过冲,出现怪异的读写异常,此时把 ClockSpeed 降到 200kHz 到 300kHz 就能解决。

GPIO 配置时要注意,I2C 引脚必须设置为开漏输出,并开启内部上拉或外部上拉。有人习惯把引脚设成推挽输出,这会直接把总线电平拉死,导致 ACK 永远回不来,属于入门阶段最隐蔽的坑之一。

2.3 上电自检流程

初始化顺序建议这样:先初始化两个 I2C 外设,再逐个读取 who_am_i 寄存器确认设备在线,然后清除休眠位。MPU6050 的 who_am_i 返回 0x68,MPU9250 的主芯片也返回 0x68,AK8963 返回 0x48。上电后如果读回来 0xFF 或 0x00,八成是接线、供电或上拉问题。

可以写一个简单的探测函数,在启动阶段扫描三个地址并把结果通过串口打出来:

uint8_t who; HAL_I2C_Mem_Read(&hi2c1, 0x68<<1, 0x75, I2C_MEMADD_SIZE_8BIT, &who, 1, 100); printf("IIC1 MPU6050: 0x%02X\n", who); HAL_I2C_Mem_Read(&hi2c2, 0x68<<1, 0x75, I2C_MEMADD_SIZE_8BIT, &who, 1, 100); printf("IIC2 MPU9250: 0x%02X\n", who); HAL_I2C_Mem_Read(&hi2c2, 0x0C<<1, 0x00, I2C_MEMADD_SIZE_8BIT, &who, 1, 100); printf("IIC2 AK8963: 0x%02X\n", who);

看到 68、68、48 三个地址依次回 ACK,才能进入姿态解算主循环。这一步能帮你把问题隔离在读写环节,而不是错误地在滤波环节里找 bug。

3. MPU6050 加速度计/陀螺仪读取

3.1 核心寄存器配置

MPU6050 的配置集中在几个寄存器,用一张表可以讲清:

寄存器地址写入值含义
PWR_MGMT_10x6B0x00退出休眠,使用内部 8MHz 时钟
SMPLRT_DIV0x190x07采样率 8kHz / 8 = 1kHz
CONFIG0x1A0x01DLPF 184Hz,滤掉高频噪声
GYRO_CONFIG0x1B0x08±500°/s,灵敏度 65.5 LSB/°/s
ACCEL_CONFIG0x1C0x00±2g,灵敏度 16384 LSB/g

选 ±2g 和 ±500°/s,是为了平衡分辨率和量程。四轴、平衡车这类场景,横滚和俯仰角速率一般不会超过 400°/s,500°/s 够用;如果做高速旋转平台,就要扩到 ±2000°/s,但灵敏度会从 65.5 掉到 16.4,噪声会明显变大。

DLPF 选 184Hz 是比较均衡的选择。如果发现数据在剧烈运动下有明显延迟,可以改到 0x02(98Hz)或 0x03(42Hz),但滤波效果会变弱,需要靠卡尔曼滤波去补。

3.2 读取原始数据与单位转换

MPU6050 的寄存器 0x3B 开始,按 X/Y/Z 顺序存放 16 位加速度计数据,跳过温度寄存器后,0x43 开始存放 16 位陀螺仪数据。用一次连续读取,16 个字节就能拿到全部数据:

uint8_t buf[14]; int16_t acc[3], gyro[3]; HAL_I2C_Mem_Read(&hi2c1, 0x68<<1, 0x3B, I2C_MEMADD_SIZE_8BIT, buf, 14, 100); acc[0] = (int16_t)(buf[0]<<8 | buf[1]); acc[1] = (int16_t)(buf[2]<<8 | buf[3]); acc[2] = (int16_t)(buf[4]<<8 | buf[5]); gyro[0] = (int16_t)(buf[8]<<8 | buf[9]); gyro[1] = (int16_t)(buf[10]<<8 | buf[11]); gyro[2] = (int16_t)(buf[12]<<8 | buf[13]);

这里要注意寄存器读回的是大端字节序,高字节在前、低字节在后,直接buf[0]<<8 | buf[1]就能拼成 int16_t。如果发现符号不对,多半是寄存器地址错了或者模块方向装反。

单位转换很简单:

float acc_g[3], gyro_dps[3]; acc_g[0] = acc[0] / 16384.0f; acc_g[1] = acc[1] / 16384.0f; acc_g[2] = acc[2] / 16384.0f; gyro_dps[0] = gyro[0] / 65.5f; gyro_dps[1] = gyro[1] / 65.5f; gyro_dps[2] = gyro[2] / 65.5f;

除法后面一定要加.0f,否则 C 语言整数除法会把数值直接截断成 0,这个低级错误我在调试群里见过不止一次。

3.3 加速度计解算 roll/pitch 角度

加速度计在静止状态下测的是比力,也就是重力方向,所以可以从它还原出横滚和俯仰角。以常见的 X 轴朝前、Y 轴朝左、Z 轴朝上的机体坐标系为例:

float roll_acc = atan2(-acc_g[0], sqrt(acc_g[1]*acc_g[1] + acc_g[2]*acc_g[2])) * 180.0f / M_PI; float pitch_acc = atan2(acc_g[1], acc_g[2]) * 180.0f / M_PI;

符号取决于你的安装方向和坐标系定义,跑起来之后把板子绕对应轴转一下,方向反了就改对应轴的正负号。需要提醒的是,加速度计解算的 roll/pitch 在快速运动时会被线性加速度污染,所以这里算出的角度只适合作为卡尔曼滤波的观测值,而不是直接输出。

yaw 角无法从加速度计求出,因为它感受不到绕重力轴旋转的变化。这个缺口由陀螺仪积分和磁力计补上。

4. 9250 磁力计读取与罗盘角计算

4.1 AK8963 寄存器读取流程

在 MPU9250 模块中,AK8963 是独立挂在 I2C 总线上的磁力计芯片,地址为 0x0C。读取流程比 MPU6050 多一步:先配置工作模式,再判断数据就绪位。

AK8963 的 CNTL1 寄存器(0x0A)写入 0x16,表示 16 位输出、100Hz 连续测量模式 2。这个模式适合一直在转动的机器人平台;如果设备大部分时间静止,用单次测量模式更省电。

读数据时,先读 ST1(0x02)确认 bit0 的 DRDY 置 1,然后连续读 0x03 到 0x08 六个字节,最后必须读 ST2(0x09)清除中断标志。跳过 ST2 会导致下次 DRDY 状态是脏的,拿到的可能是上一组残留数据,这是磁力计读数跳变的一个隐蔽原因。

uint8_t status, raw[6], st2; HAL_I2C_Mem_Read(&hi2c2, 0x0C<<1, 0x02, I2C_MEMADD_SIZE_8BIT, &status, 1, 100); if (status & 0x01) { HAL_I2C_Mem_Read(&hi2c2, 0x0C<<1, 0x03, I2C_MEMADD_SIZE_8BIT, raw, 6, 100); HAL_I2C_Mem_Read(&hi2c2, 0x0C<<1, 0x09, I2C_MEMADD_SIZE_8BIT, &st2, 1, 100); mag[0] = (int16_t)(raw[0]<<8 | raw[1]); mag[1] = (int16_t)(raw[2]<<8 | raw[3]); mag[2] = (int16_t)(raw[4]<<8 | raw[5]); }

注意 AK8963 同样是大端输出,拼字节时要保持一致。

4.2 罗盘角度计算与倾斜补偿

水平放置时,罗盘角可以直接用 X、Y 轴的磁场分量计算:

float heading = atan2((float)mag[1], (float)mag[0]) * 180.0f / M_PI; if (heading < 0) heading += 360.0f;

但实际设备很少保持水平,板子一旦有 pitch 或 roll,直接算出来的航向角会跟着姿态一起倾斜,误差很大。需要先把磁场矢量投影回水平面:

float mx = mag[0], my = mag[1], mz = mag[2]; float xh = mx * cos(pitch) + my * sin(roll) * sin(pitch) + mz * cos(roll) * sin(pitch); float yh = my * cos(roll) - mz * sin(roll); float heading = atan2(yh, xh) * 180.0f / M_PI; if (heading < 0) heading += 360.0f;

这里的 pitch 和 roll 就是卡尔曼滤波输出的角度,用上一帧的滤波值即可,不需要再额外处理。做倾斜补偿时,角度单位必须统一成弧度,C 语言里的sincos只接受弧度入参,这个错位也会导致航向角在特定姿态下突然翻转。

4.3 磁力计校准

直接读出来的磁力计数据通常是不准的,因为环境磁场、PCB 上走线、附近金属结构都会叠加出偏移量。最常用的校准方法叫“最小值最大值校准”,把模块朝各个方向缓慢旋转一个八字形,每根轴都经历磁场最大值和最小值,然后计算偏移和缩放:

mag_offset[i] = (max[i] + min[i]) / 2.0f; mag_scale[i] = (max[i] - min[i]) / 2.0f; calib_mag[i] = (raw_mag[i] - mag_offset[i]) / mag_scale[i];

如果三个轴的 scale 差异不大,说明软磁干扰较轻,只做偏移修正就够了;如果差异明显,就要用完整的 3x3 软磁矩阵校准。实际操作中,室内环境几乎都要做硬磁校准,不然指南针角度会有一个固定的偏置角,看起来“挺正常”,一转弯就对不上。校准完成后把 offset 和 scale 存到 Flash 或 EEPROM,冷启动直接加载。

5. 卡尔曼滤波姿态解算实现

5.1 状态方程与观测方程

卡尔曼滤波在这个项目里,每个角度(roll、pitch、yaw)各用一个独立的一维滤波器,核心思想是维护一个状态向量 x = [angle, bias]^T。angle 是融合后的角度,bias 是陀螺仪零漂估计值。

预测方程是:

angle' = angle + (gyro_rate - bias) * dt bias' = bias

这里的 gyro_rate 是陀螺仪输出的角速度,bias 会随着观测不断修正。观测值是加速度计解算的 roll_acc / pitch_acc,以及磁力计解算的 heading,分别作为对应角度的测量输入。

协方差预测和卡尔曼增益不细展开,后面代码里直接体现。整个滤波器的输入有三个:当前角速度、观测角度、时间差 dt。输出是融合后的角度,同时内部维护 bias。

5.2 核心 C 代码实现

卡尔曼滤波代码可以直接复用经典实现,结构体定义如下:

typedef struct { float Q_angle; float Q_bias; float R_measure; float angle; float bias; float P[2][2]; } Kalman_t;

核心函数:

float Kalman_GetAngle(Kalman_t* k, float newAngle, float newRate, float dt) { float S, K0, K1, P00, P01, P10, P11, y; // 预测 k->angle += dt * (newRate - k->bias); k->P[0][0] += dt * (dt*k->P[1][1] - k->P[0][1] - k->P[1][0] + k->Q_angle); k->P[0][1] -= dt * k->P[1][1]; k->P[1][0] -= dt * k->P[1][1]; k->P[1][1] += k->Q_bias * dt; // 卡尔曼增益 S = k->P[0][0] + k->R_measure; K0 = k->P[0][0] / S; K1 = k->P[1][0] / S; // 更新 y = newAngle - k->angle; k->angle += K0 * y; k->bias += K1 * y; // 协方差更新 P00 = k->P[0][0]; P01 = k->P[0][1]; P10 = k->P[1][0]; P11 = k->P[1][1]; k->P[0][0] -= K0 * P00; k->P[0][1] -= K0 * P01; k->P[1][0] -= K1 * P10; k->P[1][1] -= K1 * P11; return k->angle; }

在主循环里,roll 和 pitch 直接用加速度计算出的角度作观测,yaw 用磁力计 heading 作观测:

Kalman_t kal_roll, kal_pitch, kal_yaw; // 初始化 kal_roll.angle = roll_acc; kal_roll.bias = 0; kal_pitch.angle = pitch_acc; kal_pitch.bias = 0; kal_yaw.angle = heading; kal_yaw.bias = 0; kal_roll.Q_angle = 0.001f; kal_roll.Q_bias = 0.003f; kal_roll.R_measure = 0.03f; // 另外两个角度用相同参数 // 主循环 float dt = 0.01f; // 根据实际调用间隔计算 float roll = Kalman_GetAngle(&kal_roll, roll_acc, gyro_dps[0], dt); float pitch = Kalman_GetAngle(&kal_pitch, pitch_acc, gyro_dps[1], dt); float yaw = Kalman_GetAngle(&kal_yaw, heading, gyro_dps[2], dt);

yaw 的角度会跨越 -180°/180° 边界,做观测更新前要先处理角度差值:

float delta = heading - kal_yaw.angle; if (delta > 180.0f) delta -= 360.0f; if (delta < -180.0f) delta += 360.0f; kal_yaw.angle += delta;

这里直接修正状态角度,再用Kalman_GetAngle的更新逻辑,效果等同于处理过边界条件的卡尔曼观测。

dt 一定不能用固定的假值,要在每次调用时用定时器计时。我习惯用一个 1ms 系统节拍,记录上次调用的时间戳,相减得到真实 dt。如果循环偶尔被串口打印卡住,dt 偏大,固定值会导致角度积分过量,最后整个数据流都会漂。这个问题在调试阶段很常见。

5.3 参数调整与效果实测

卡尔曼的调参比较玄学,但有几个基本规律可以分享:

参数调大效果调小效果
Q_angle响应更快,噪声更大更平滑,但跟随变慢
Q_bias对零漂修正更激进,动态好但抖动大零漂修正慢
R_measure更信任角速度,过滤噪声更强,滞后明显更信任角速度观测,响应快但噪声大

我的初始建议是 Q_angle=0.001、Q_bias=0.003、R_measure=0.03,然后一边转动板子一边看稳定性和延迟。如果角度跟不上动作,就把 Q_angle 调大 10 倍再试;如果静止时角度还在小幅抖动,就把 R_measure 调大 10 倍再试。这个调节过程不复杂,但一次只调一个参数,否则出了问题很难定位。

实测下来,滤波后的 roll/pitch 在静止时波动在 ±0.1° 以内,快速晃动后能在 0.3 秒内回到稳定值;yaw 在室内有磁干扰的环境下,静止时波动约 ±0.5°,经过磁力计校准后,直线行走并转弯的累计误差明显小于直接用陀螺仪积分的方案。

6. 常见问题与排查技巧实录

6.1 IIC 通信失败的排查顺序

遇到 I2C 读不到数据,先不要怀疑代码,按下面顺序排查:

现象原因排查方法
who_am_i 读回 0xFF上拉缺失或接线错误测 SCL/SDA 空闲电平,补充 4.7kΩ 上拉
地址探测无 ACK传感器地址不对或 AD0 电平错误确认 0x68/0x69、0x0C 的地址转换
偶尔通信超时飞线过长或干扰降速到 100kHz,缩短杜邦线
读回数据全为 0寄存器地址或字节序错误先读 who_am_i,再逐寄存器验证

用逻辑分析仪抓 IIC 时序是最直接的定位方式。正常通信时,SCL 空闲为高,起始位后能看到设备回 ACK;如果 SDA 一直被拉低,说明某个设备把总线劫持了,把总线上的设备逐个拔掉,就能找到罪魁祸首。

6.2 数据跳变和漂移问题

加速度计在震动环境下噪声会明显增大,这时候不能只靠卡尔曼滤波硬撑。我在调试平衡车时遇到过角度在 ±2° 之间无规律跳变,后来在卡尔曼之前加了一个简单的滑动窗口平均,把加速度计原始值先平滑一遍,问题立刻消失。注意窗口不能太大,否则响应滞后,我一般取 5 个采样点。

陀螺仪零漂是所有角度漂移的直接原因。每次冷启动后,让传感器保持静止 1 秒,采集 100 次陀螺仪数据求平均,得到零偏,再把这个零偏写入卡尔曼的 bias 初值。这样能大幅缩短卡尔曼收敛时间,刚上电的几分钟内角度不会乱跑。

磁力计数据最容易受环境干扰。调试时,让模块远离电机、电源线和铁磁性物体至少 10cm。如果指南针角度在某个特定方向明显偏差,多半是该方向有金属物体反射或增强了磁场,换个位置再校准。

6.3 调参与检查顺序心得

整个项目做完之后,我个人的调试顺序已经固定下来,每一步确认无误再走下一步:

  1. 先确认三个 I2C 地址都能 ACK,并且 who_am_i 返回值正确。
  2. 读原始加速度计和陀螺仪,静止时观察输出是否稳定,转动板子看数值是否跟着变化。
  3. 算 roll_acc 和 pitch_acc,水平放置时应接近 0°,转动 90° 应接近 90°。
  4. 加卡尔曼滤波,先只做 roll 和 pitch,确认输出平滑、响应灵敏。
  5. 再读磁力计并做校准,把倾斜补偿公式加进去。
  6. 最后把 yaw 和磁力计融合,整个系统才算完成。

第一次调 yaw 时我吃了倾斜补偿的亏,没做投影直接算角度,结果板子一翘,航向角就跟着翻,排查了很久才发现公式少了一项。做这种多传感器融合,调试节奏千万不能急,把链路拆成一个个能验证的小块,每块跑通再去拼下一个,整个过程会顺利很多。卡尔曼滤波不是玄学,就是把每个传感器的优点留下来、缺点交给队友去补,调明白之后回头看,整个系统其实非常清晰。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询