简介:基于MPU9250获取姿态(航向角)的完整源码包,面向 STM32 与 MATLAB 开发者,适合需要完成九轴传感器数据融合、姿态解算或航向角输出的入门及中级项目。工程基于正点原子阿波罗 F429 开发板,首先读取 MPU9250 的加速度、陀螺仪与磁力计原始数据;针对磁力计易受干扰的问题,采用基于最小二乘的椭球矫正方法进行标定,配合均值滤波初步处理,再交由基于四元数的 Madgwick 算法融合,最终稳定输出航向角,同时可换算横滚角与俯仰角,并配套 MATLAB 源码用于算法验证与可视化分析。压缩包整体约 18.73MB,以 STM32 工程文件、C 源码与 MATLAB 脚本为主,结构上按功能模块划分,便于直接查看解算流程和修改参数。已有 4908 人学习下载,作者也指出均值滤波部分可自行改进,适合在此基础上做更深度的滤波或融合算法调优。 做过姿态解算的朋友应该都有体会:用六轴MPU6050测俯仰角和横滚角,效果还算能看,但一旦要看航向角(偏航角),那就开始头疼了。静止状态下数据还在慢慢飘,稍微转动几圈再回正,角度早就偏到不知哪里去了。我当初也在这个问题上绕了不少路,后来把传感器换成MPU9250,把磁力计数据真正拉进解算回路,航向角才算稳定下来。这篇笔记就把我基于STM32和MATLAB搭的这套MPU9250姿态解算方案完整记录下来,重点说清楚航向角是怎么算出来的,MPU6050和MPU9250在航向上到底差在哪,以及源码里那些容易让人浪费一整天的坑。
先说结论:MPU9250本质上就是MPU6050的九轴版本,内部有一颗三轴加速度计、一颗三轴陀螺仪,外加一个AK8963三轴磁力计。多出来的这个磁力计,正好补上了六轴方案在偏航轴(Z轴)上没法感知绝对角度的短板。我的实现主要分三条线:第一,STM32通过I2C采集九轴原始数据,做单位换算;第二,在STM32上用四元数互补滤波做姿态解算,输出偏航角、俯仰角、横滚角;第三,同一份数据传到MATLAB里用相同算法离线跑一遍,用来调参和可视化分析。下面逐块讲。
1. 为什么航向角必须上九轴:MPU6050在偏航轴上的天然缺陷
1.1 偏航角的积分漂移为什么压不住
六轴方案里的航向角完全靠陀螺仪的Z轴角速度积分。陀螺仪本身存在零偏,也就是静止时输出不为零,比如真实角速度是0,但传感器读出来可能有0.3度/秒,这个偏差如果只是恒定值还好,麻烦的是它随温度、供电电压甚至振动而变化。积分操作会把这种微小误差一点点累积起来,一分钟可能漂移几度,十分钟就是几十度。更难受的是,积分产生的误差是只增不减的,除非有外部参考量来纠正。
加速度计能纠正俯仰和横滚,因为它检测的是重力方向,重力在水平面的投影为0,所以只要静止下来,俯仰和横滚的误差就会被拉回绝对参考。但是偏航轴是绕着重力方向旋转的,旋转前后重力矢量在机体系下的分量几乎不变,加速度计对偏航角完全没有约束力。这就是六轴航向角长期漂移的根源。
1.2 磁力计如何把航向“钉住”
磁力计单独拿出来看,就像一个电子指南针,它能感知地磁场方向。地磁场在局部区域可以近似为一个固定矢量(磁北方向),把这个磁场方向投影到水平面,就能得到一个绝对的航向参考。也就是说,磁力计相当于给偏航轴装了一根“锚”,让航向角不再仅靠陀螺仪积分,而是不断被磁场方向拉回真实值。
六轴与九轴在偏航角上的差距,说白了就是“纯积分”和“积分+绝对修正”的差距。互补滤波的做法正是这样:短时间内以陀螺仪的积分结果为主,因为它的动态响应快、不会受磁场干扰;长时间段内用加速度计和磁力计的观测值做修正,把累积误差拉回来。两者加权融合之后,航向角在不剧烈磁干扰环境下基本能稳定住。
1.3 MPU9250和MPU6050在硬件上的具体差异
- 六轴MPU6050包含三轴加速度计、三轴陀螺仪,没有磁力计。
- 九轴MPU9250包含三轴加速度计、三轴陀螺仪、AK8963磁力计。
- 寄存器层面,MPU9250兼容MPU6050的大部分配置方式,I2C地址同样是0x68(AD0接地时),但WHO_AM_I寄存器返回值不同,MPU6050一般是0x68,MPU9250一般是0x71。
- 磁力计AK8963有自己的I2C从机地址0x0C,但它是挂在MPU9250内部I2C总线上的,外部主机能不能直接访问,取决于旁路(BYPASS)或I2C主模式配置。
这些差异决定了代码不能简单把MPU6050的驱动搬过来直接用,磁力计读取是需要单独处理的。
2. STM32端硬件初始化:把MPU9250的九轴数据稳定读出来
2.1 硬件连接与电源注意事项
我使用的是最常见的四针或六针MPU9250模块,VCC接3.3V,GND接地,SCL接I2C时钟,SDA接I2C数据。这里有个很容易被忽视的坑:MPU9250模块上的I2C上拉电阻有些是焊接好的,有些是空焊的,如果模块没有板上拉,而STM32内部上拉又太弱,I2C总线波形畸变会导致数据读出来的数不对,隔三差五卡死在等待应答。排查方法很简单,把示波器或逻辑分析仪挂到SCL上,看沿是不是足够陡,如果不陡,就在SCL和SDA上加两个4.7k欧姆上拉电阻到3.3V。
复位脚如果引出的话,建议拉高,防止模块一直锁在复位状态。
2.2 I2C配置与寄存器读取的几个关键点
STM32端我用的标准库和HAL库都试过,流程大差不差。先初始化I2C,再按顺序做这几件事:
- 延时100毫秒,等待传感器内部上电稳定。MPU9250刚上电时直接去读寄存器经常拿到0xFF或0x00,这不是芯片坏了,是没稳定。
- 读WHO_AM_I(0x75),校验是否为0x71。我这里用的是标准库GPIO模拟I2C,因为调试方便,出问题可以直接看到波形;也有朋友用硬件I2C,调通之后更省CPU。但硬件I2C如果遇到死锁,优先检查波形和总线释放逻辑。
- 写电源管理寄存器(PWR_MGMT_1,地址0x6B),清复位并选择时钟源为PLL(内部PLL带高精度陀螺仪时钟),配置为0x01即可,不要一直归零。
- 配置陀螺仪量程(GYRO_CONFIG,0x1B)和加速度计量程(ACCEL_CONFIG,0x1C)。我设置陀螺仪为±500度/秒,对应的灵敏度为65.5 LSB/(度/秒),加速度计为±4g,灵敏度为8192 LSB/g。量程选择要结合应用场景:如果只是桌面静止测姿态,±250也可以;如果做运动追踪,量程太小会饱和,解算结果直接飞掉。
- 配置数字低通滤波器(DLPF),带宽我取21Hz。把陀螺仪和加速度计的高频噪声先滤一截,后面算法压力会小很多。带宽太低会牺牲动态响应,这个后面调参再说。
- AK8963磁力计配置:先向AK8963的CNTL1寄存器(地址0x0A)写入连续测量模式2(0x16),输出14位分辨率,重复100Hz模式。如果模块支持旁路,外部主机可以经0x0C地址直接读写AK8963。读取磁力计数据前,需要等待DRDY或ST1寄存器中数据就绪位拉高,否则连续读取时会偶发读到同一组旧数据。
关于MPU9250磁力计的读取,我要多说一句。模块内部其实是两套从机地址,外部主机通过I2C总线读0x68时访问的是MPU6050部分,读0x0C时访问的是AK8963部分。有些驱动里的做法是启用I2C主模式(I2C_MST_EN),让MPU9250自动把磁力计数据搬运到扩展传感器数据寄存器,然后外部统一读0x68就行。这样逻辑上更统一,但寄存器配置繁琐,在非官方库上还容易遇到跳变,所以我直接用旁路模式,把两套地址当作两个独立器件处理,简单粗暴但稳定。
2.3 数据帧格式设计:串口怎么发才能方便MATLAB解析
STM32算出的四元数和欧拉角,最终要通过串口发出去,方便MATLAB端接收验证。数据帧格式我在这个项目里做了两次版本迭代。第一版是纯二进制帧,结构紧凑,但是MATLAB解析不方便,串口监视器也看不出门道。第二版改成文本帧,用固定帧头加逗号分隔字段,实测更省心。
推荐的数据帧格式长这样:
$MPU,Yaw,Pitch,Roll,q0,q1,q2,q3,AX,AY,AZ,GX,GY,GZ,MX,MY,MZ字段含义依次是偏航角、俯仰角、横滚角(单位度)、四元数四个分量、加速度计原始值、陀螺仪原始值、磁力计原始值。转发频率我取50Hz,定量定时器中断里处理一次解算和发送。附带原始九轴数据的好处是,MATLAB端可以在不依赖STM32算法的情况下重新解算,做算法对比,这个优势在调参时非常明显。
串口波特率我设为115200,50Hz下帧长度约100字节,带宽完全够,留了不少余量。
3. 四元数互补滤波解算:从原始数据到可用航向角的关键一跳
3.1 为什么用四元数而不是欧拉角
欧拉角(俯仰、横滚、偏航)直观,但在解算过程中存在万向锁问题,而且三角运算多,STM32上计算开销偏大。四元数用一个四维单位向量表示空间旋转,没有奇点,适合做姿态递推。输出阶段可以用四元数转欧拉角公式换算给人看,内部递推全部用四元数。
姿态更新核心是一个微分方程:四元数对时间的导数等于0.5倍的四元数乘以角速度四元数。STM32端每10毫秒执行一次,陀螺仪角速度换算成弧度每秒后进入递推公式。
3.2 互补滤波怎么把加速度计和磁力计一起用起来
互补滤波的关键在于,陀螺仪提供快速、平滑的姿态变化量,但会漂移;加速度计和磁力计提供绝对参考方向,但易受运动加速度和磁场干扰。所以最终实现是:陀螺仪积分结果占主导,两个传感器作为外部观测,在频率域上形成互补。
我采用的实现流程:
- 由当前四元数推算出重力方向在机体坐标系下的估计值。
- 将加速度计测量值与重力估计做叉积,得到俯仰和横滚方向的修正误差。
- 将磁力计水平分量与当地磁偏角修正后的磁场方向做叉积,得到偏航方向的修正误差。
- 把误差送入PI调节器,输出叠加到陀螺仪角速度上。
- 用修正后的角速度执行四元数积分,再归一化,最后转欧拉角。
简化版核心逻辑用公式表示是这样的:
// 误差由加速度计与重力估计交叉相乘得到 error = accelerometer_measurement x gravity_estimate; // 磁力计误差类似,但需要先处理磁场方向在水平面的投影 error_mag = magnetometer_measurement x mag_reference; // PI融合修正 gyro.x += Kp * error.x + Ki * integral.x; gyro.y += Kp * error.y + Ki * integral.y; gyro.z += Kp * error.z + Ki * integral.z + Kp_mag * error_mag.x + Ki_mag * integral_mag.x; // 四元数更新 q += 0.5 * q * [0, gyro.x, gyro.y, gyro.z] * dt; // 归一化,防止浮点误差累积导致四元数失规范化 normalize(q);这里有个细节:磁力计的修正不能直接拿原始磁场矢量去做叉积,因为地磁场不仅有水平分量,还有垂直分量,直接使用会导致俯仰和横滚方向也被磁场带偏,产生耦合。正确做法是先把磁场矢量投影到水平面,再用水平面磁场方向估算航向,或者比较优雅的做法是构造“虚拟参考磁场向量”,确保它与重力向量正交。
我实测用AHRS库简化处理:先用四元数把磁力计矢量旋转到地理坐标系,取水平面的X、Y分量,然后重新构造一个与重力正交的参考磁场向量,再反向旋转回机体系,与原始磁力计做叉积。这样偏航修正就不会污染俯仰和横滚。很多开源代码(可以参考MahonyAHRS的实现)也是这么做的。
3.3 参数调节:Kp和Ki怎么定
互补滤波里最核心的两个参数是Kp和Ki。Kp是对外部传感器误差的响应比例,Kp越大,姿态越快地追随加速度计和磁力计的观测值,但过大会把运动加速度和高频磁场波动带进结果,导致角度抖动。Ki是积分项,用于消除稳态误差,但太大会让瞬态响应振荡。
我的起调值:Kp=0.5,Ki=0.05,放在桌面静止测试时效果不错,但拿着板子快速摆动时横滚角会有些抖动。后来按动态场景把Kp降到0.2,Ki保持0.02,稳定性明显提升,代价是恢复到水平位置时需要约半秒时间。做飞行器的话,Kp通常需要再小一点;做姿态记录,Kp反而可以适当调大。最终参数不是固定的,得跟据你的应用场景来。
调参的实操方法:先在MATLAB里对比同一组数据的解算结果,先静态后动态,一步步看哪个参数引起的抖动可接受、哪个参数导致回正过冲。
4. MATLAB离线验证:同一套算法在PC端跑一遍,把问题看透
4.1 为什么非要多此一举用MATLAB
直接在STM32串口上打印角度,只能看到一堆数字,很难判断姿态轨迹是否正确。MATLAB环境的优势是能一次性加载几分钟的传感器数据,用和STM32完全相同的滤波算法在PC上离线解算,然后画出姿态曲线、分析误差和噪声频谱。调参时先在MATLAB里模拟,得到合适的参数后再移植到STM32,省去了反复下载固件的麻烦。
MATLAB端我用的是串口对象读取CSV文本帧,也可以直接读取STM32串口实时数据。关键是把STM32的采样频率和单位换算对齐,保证解算输入一致。
4.2 MATLAB里的互补滤波实现
核心实现和STM32端几乎一一对应。下面是一个简化版的读取与解算骨架:
data = readmatrix('imu_log.csv'); t = data(:,1) / 1000; % 时间戳转秒 acc = data(:,2:4); % 加速度计原始数据 gyro = data(:,5:7); % 陀螺仪原始数据,单位deg/s mag = data(:,8:10); % 磁力计原始数据 fs = 50; % 采样率 dt = 1 / fs; q = [1 0 0 0]; % 初始四元数 for i = 2:size(acc,1) % 单位换算:加速度计、陀螺仪、磁力计都需要转成物理量 gx = gyro(i,1) * deg2rad; % deg/s -> rad/s % ... 其余轴同理 ... [q, yaw, pitch, roll] = ahrsUpdate(q, acc, gyro, mag, dt); store(i) = [yaw, pitch, roll]; end plot(t, store); legend('Yaw', 'Pitch', 'Roll'); xlabel('Time (s)'); ylabel('Angle (deg)');MATLAB里不需要手动处理浮点运算效率,可以把重点放在算法验证和参数测试上。我用这个流程验证过:同一个传感器数据,使用相同Kp、Ki的互补滤波,MATLAB端和STM32端输出完全吻合,确认后我再放心地把参数固化到STM32代码中。
注意:在MATLAB处理串口实时数据时,设置波特率后还需要调用configureTerminator确保终止符一致,否则串口数据会经常读取不完整。另外,如果你用的是某些基于FT232或CH340的STM32开发板,MATLAB串口对象可能会触发DTR/RTS信号导致开发板复位,处理方法是在打开串口后立即设置setDTR(false)、setRTS(false)。
4.3 用MATLAB做磁力计校准
磁力计校准不到位,航向角会出现两个明显的症状:一是360度旋转一周,航向角不是均匀变化,而是跳变;二是同一朝向在不同高度或不同俯仰角下,航向角不一致。原因是周围铁磁性物质(电池、螺丝、导线)产生了附加磁场,以及传感器出厂灵敏度不统一。
我用MATLAB做最基础的硬磁校准,思路极简单:
- 让板子水平缓慢旋转至少一整圈,采样磁力计X、Y原始数据。
- 求出X、Y的最大最小值,偏移量为
(max+min)/2。 - 让板子在垂直平面内翻转,求出Z轴的偏移量。
- 在实际读取的磁力计数值上减去偏移量,即完成硬磁补偿。
如果还想做软磁校准,需要采集三维球面上的数据(让板子沿各个方向旋转),然后做椭球拟合,MATLAB的lsqNonlin或sphereFit都有人用。对多数室内调试来说,硬磁补偿已经能把航向角稳到正负3度左右。
5. 实测中踩过的坑与调参心得
5.1 磁力计干扰:一进实验室航向就乱跳
这是我遇到最棘手的问题。在空旷位置测试,航向角很稳定,一旦把板子放到电脑旁或者靠近金属桌腿,航向角能瞬间歪十几度,甚至剧烈抖动。磁力计测量的原理是感知磁场,电机、电源适配器、音箱都能改变局部磁场方向。条件允许时,传感器尽量远离大电流线路和大块金属。另一个办法是给系统加入GPS或其他外部航向参考,在长时间尺度上融合纠正。
检测磁力计是否被干扰,可以在MATLAB里画磁场强度模值曲线,正常情况模值在平坦地形处几乎恒定,如果模值随方向变化很大,说明软磁干扰严重,校准也没法完全解决。我是把这类数据标记为“不可信”,在滤波中自动降低磁力计的权重。
5.2 SWD连接失败与STM32下载异常的处理
一次调代码时突然遇到Keil报error: no stm32 target found,我第一反应就是接线问题,检查SWDIO、SWCLK和GND发现没问题,又反复按复位键也没用。最后发现是目标板电源不稳导致SWD调试器无法稳定握手。给开发板换成独立USB电源后立刻就能连接了。还有一次是芯片不小心开启了读保护,导致只能连接但无法烧录,用ST-Link Utility做全片擦除才恢复。遇到连接失败时的排查顺序建议是:先看电源是否稳定,再查SWD线序和引脚占用,紧接着确认芯片没有处于读保护状态,最后才是降低SWD频率。
SWD频率偶尔也有影响,线长超过20厘米时,默认4MHz的SWD时钟容易通讯失败,可以改用1MHz或更低。
5.3 数据丢帧和程序卡死的处理
I2C读取磁力计需要等数据就绪位,如果代码逻辑不严谨,等待DRDY位拉高时没有加超时退出的保护,一旦磁力计异常,程序就会死循环在I2C等待里。这个问题我在首次移植时踩得非常狠,表现为上电后串口偶尔不输出任何数据。解决方法是所有I2C等待都加超时退出,比如连续等待100次仍无效就跳过本次读取,同时打一个错误标志位,上位机看到标志位就知道传感器异常了。
串口发送也注意别在主循环里阻塞。我把解算放在定时器中断,串口发送用DMA,主循环只负责更新状态,这样即使串口外设忙碌,也不会拖累姿态解算的实时性。
5.4 桩桩件件的调参感受
最后说点个人体会。很多人拿到MPU9250第一件事就是找DMP库,觉得DMP里直接能读航向角。MPU9250的DMP对磁力计融合支持不理想,而且不开源,一旦表现不好,成本反而更高。自己用互补滤波虽然多写几百行代码,但是各个参数都是可控的,出了问题能查、能调、能改,后续扩展卡尔曼滤波方案时思路也连贯得多。这套方案做完后,我又陆续把同样的数据接到不同的滤波算法里做对比,发现MATLAB端有一份可复现的算法模型,确实是后期持续迭代的好底子。
如果你也是刚开始做姿态解算,建议先把九轴原始数据在MATLAB里画出来,感受一下三种传感器各自的噪声特性,再上手滤波算法。数据质量是算法效果的上限,算法只能在那个上限之内尽量发挥。
本文还有配套的精品资源,点击获取