简介:基于MATLAB平台的9轴IMU卡尔曼滤波源码,面向惯性导航、姿态估计或传感器融合方向的开发者与学生,解决多传感器数据噪声大、漂移明显等问题,从而提升姿态解算的精度与稳定性,既适合新手学习原理,也方便开发者二次调试。压缩包共15个文件,以13个.m源码文件为主体,辅以1个txt说明与1个.mat示例数据,整体仅103KB;目录中包含四元数库、MahonyAHRS与MadgwickAHRS两种滤波器实现、测试脚本及Readme文档,其中四元数库提供欧拉角、旋转矩阵等常用转换函数,结构清晰,方便按模块调用与二次开发。已有2953人学习下载,适合有一定MATLAB基础、正在研究卡尔曼滤波、IMU数据融合或姿态解算的读者。通过源码可完整学习状态模型构建、观测更新、协方差递推等核心步骤,也能对比互补滤波与卡尔曼滤波在9轴数据上的效果,并可直接修改参数适配不同传感器配置与应用场景。 几年前做运动追踪设备数据回放的时候,我对着示波器里那条疯狂抖动的roll角曲线,第一次意识到9轴IMU的原始数据离“能用”差得有多远。后来在MATLAB里把加速度计、陀螺仪、磁力计三路数据用卡尔曼滤波融合到一起,姿态曲线才终于稳定下来。那份代码后来被我反复重写,也从最初的单通道低通滤波进化成了完整的9轴融合工程。这篇文章就是把那一整套基于MATLAB的卡尔曼滤波9轴IMU数据处理源码拆开讲清楚,包括滤波器怎么建模、核心代码怎么组织结构、噪声参数怎么试出来,以及我在实际数据上踩过的几个坑。如果你正准备处理MPU9250、JY901这类模块的log数据,或者想做无人车、机器人、AR设备的姿态解算,这套思路可以直接接手。
1. 9轴IMU为什么需要卡尔曼滤波
1.1 三种传感器的角色与短板
9轴IMU说的其实是三个三轴传感器:加速度计、陀螺仪、磁力计。它们各有各的本事,也各有各的毛病。
加速度计测量的是比力,静止时能给出重力方向,因此可以算出俯仰角和横滚角。问题在于它极其敏感,电机振动、行车颠簸、手部抖动都会让输出剧烈跳动,动态场景下算出的角度几乎没法直接用。
陀螺仪测量角速度,短时间内的角增量非常准确,响应快、不受振动影响。但它测的是变化率,要得到角度就得积分,而积分会把零偏一点点累积放大,几秒钟看不出问题,几十秒后角度就开始缓慢漂移,时间越长越离谱。
磁力计测量磁场,能提供绝对航向角,弥补陀螺仪积分漂移的长期误差。它的缺陷是容易受环境干扰,室内钢筋、电机磁场、旁边的铁器都会让读数发生明显偏移。
单看任何一个传感器都不够用。加速度计短时噪声大但无长期漂移,陀螺仪短时精准但长期漂移无法容忍,磁力计提供绝对基准但信任度有限。卡尔曼滤波的价值就在于按照各传感器的统计特性动态分配权重,让最终姿态输出同时具备“陀螺仪的短时平滑”和“加速度计/磁力计的长期稳定”。
1.2 从原始数据到可用姿态,中间隔了三层问题
拿到IMU模块的log数据之后,第一件事往往不是滤波,而是洗数据。我处理的典型记录文件长这样:时间戳之外还有加速度、角速度、磁场强度各三个分量,采样频率标称100Hz,实际却经常出现时间间隔不均匀的情况。有些模块内部还会做一次姿态解算,导出的欧拉角直接从串口输出,这类数据相对干净;更需要处理的,是那种只输出原始传感器ADC值的记录,需要自己做单位换算和坐标变换。
处理过程中通常要过三道关。
单位换算是第一关。加速度计输出可能是原始的LSB计数值,也可能是以g为单位的小数;陀螺仪可能是原始码值,也可能是度/秒;磁力计可能是LSB计数值,也可能是微特斯拉量级的浮点数。不同的模块、不同的量程配置,换算系数完全不同,这一步错了,后面所有算法全部白搭。
时间对齐是第二关。三路传感器虽然来自同一个芯片内部,但采样时刻并不一定是均匀间隔。MATLAB里处理这类数据时,我先检查时间戳差分的中位数是否稳定,如果抖动超过一个采样周期,就按固定步长重新插值。否则,卡尔曼滤波那一套递推过程会因为不准确的采样间隔而扭曲。
坐标系统一是第三关。加速度计、陀螺仪、磁力计各自的三轴定义,在不同型号里并不一致,有的z轴朝上,有的z轴朝下,磁力计的x轴方向和加速度计的x轴方向也可能差90度。如果忽略这一层,滤波器的观测量会出现符号错误甚至方向判断颠倒。
1.3 这个源码解决的是离线姿态解算任务
这套MATLAB源码的处理对象是离线log文件,输入是一段带时间戳的9轴原始数据,输出是滤波后的roll、pitch、yaw姿态角以及对比曲线。适合的场景包括:算法验证、传感器性能评估、跑实验后处理数据、给学生或新人做教学示例。
和嵌入式实时方案相比,离线处理的好处是可以随意调整参数和观测模型,不会因为设备还在跑而束手束脚。我在实际开发中的习惯是,先用MATLAB把参数和模型调明白,确认效果达标后,再把同样的算法移植到单片机或者C++端。所以这套源码虽然不能直接扔进Keil工程里跑,但它把核心思路完整呈现了,移植成本很低。
2. 滤波器建模:为什么选EKF而不是普通卡尔曼
2.1 状态量选择:四元数比欧拉角好在哪
设计卡尔曼滤波器的第一步是确定状态量。最直观的选择是直接用欧拉角roll、pitch、yaw作为状态向量的三个分量,状态转移方程相当于对角速度积分,观测量则是加速度计和磁力计直接解算出的欧拉角。结构确实简单,但它有个致命问题:万向锁。当pitch角接近正负90度时,roll和yaw的旋转轴重合,系统会丢失一个自由度,姿态表示出现奇异,滤波器在极端姿态下直接发散。
所以我在这套源码里选择四元数作为状态量。四元数用四个分量表示三维旋转,没有奇异性,对任意姿态都有效。代价是状态转移方程和观测方程都变成了非线性形式,普通线性卡尔曼滤波不再适用,需要使用扩展卡尔曼滤波(EKF),在每个递推时刻对非线性函数求雅可比矩阵做局部线性化。
选型时也考虑过互补滤波方案,Mahony算法那种做法实现简单、运算量小,在很多无人机飞控里用得很好。但它本质上是固定增益的比例调节,无法根据传感器噪声水平自适应调整权值。卡尔曼滤波的收益在于,加速度计噪声大的时候系统自动降低加速度计观测的权重,陀螺仪漂移大的时候系统自动加大对加速度计的依赖,这种动态平衡在剧烈运动场景下优势明显。
2.2 状态方程和观测方程怎么写
状态向量定义为归一化四元数q,具体形式是:
x = [q0, q1, q2, q3]
状态预测由陀螺仪角速度驱动。四元数的微分方程可以写成矩阵形式,其中角速度omega来自陀螺仪输出。在MATLAB中,预测步的核心代码如下:
function [q, P] = predictState(q, P, gyro, dt, Q) % gyro单位为rad/s w = gyro; % 构造四元数微分方程的系数矩阵 Omega = [ 0, -w(1), -w(2), -w(3); w(1), 0, w(3), -w(2); w(2), -w(3), 0, w(1); w(3), w(2), -w(1), 0 ]; % 离散化状态转移矩阵 F = eye(4) + 0.5 * Omega * dt; q = F * q; q = q / norm(q); % 四元数必须保持单位范数 P = F * P * F' + Q; end观测方程分为两部分。加速度计观测用于修正roll和pitch,它不能提供yaw信息,因为重力方向本身与航向无关。另一个观测来源是磁力计,单独修正yaw角。在EKF框架里,这两个观测可以放在同一个更新步骤,也可以拆成两次独立的更新调用。源码里拆成了两步,好处是每个观测方程的雅可比矩阵都更简单,代码的可读性也更好。
加速度计的观测模型是:静态或准静态条件下,加速度计测量的比力方向应当与重力方向一致,由此建立观测值与姿态四元数之间的非线性关系。为了降低实现复杂度,我在更新步中先用加速度计解算出roll和pitch的测量值,再在滤波框架中把这些角度作为观测值。实际计算流程如下:
function [q, P] = updateAccel(q, P, accel, R_acc) % 由加速度计解算出roll和pitch观测值 roll_meas = atan2(accel(2), accel(3)); pitch_meas = atan2(-accel(1), sqrt(accel(2)^2 + accel(3)^2)); z_meas = [roll_meas; pitch_meas]; for i = 1:2 % 迭代两次加速收敛 [roll, pitch, ~] = quat2euler(q); z_pred = [roll; pitch]; H = computeH_attitude(q); % 数值法求雅可比 y = z_meas - z_pred; % 观测残差 S = H * P * H' + R_acc; K = P * H' / S; q = q + K * y; q = q / norm(q); P = (eye(4) - K * H) * P; end end注意这里的四元数更新用了加法近似,严格的EKF应该把残差映射为四元数增量再做四元数乘法,那样更严谨。完整工程里我用了后者,博客里简化处理是为了把流程说清楚。雅可比矩阵计算我直接用了MATLAB的数值差分法,虽然相比解析推导费一点时间,但好处是修改模型时不用重新手算偏导。
2.3 为什么必须做归一化和零偏处理
四元数预测步之后必须单位化,这一步不能省。数值误差会逐渐让四元数范数偏离1,导致姿态矩阵不再正交,观测残差失真,滤波器性能快速退化。
陀螺仪零偏是另一个隐藏杀手。即使静止不动,陀螺仪输出也不会恰好是零,这个固定偏移经过四元数微分方程积分后,会转化为随时间线性增长的角度误差。在不额外辨识零偏的情况下,滤波后的姿态在静态时仍然会出现缓慢漂移。源码里提供了一个简单的一次性校准函数:上电后保持静止采集100帧数据,取角速度的平均值作为零偏减掉,效果立竿见影。
3. 源码拆解:从读取log到输出姿态角的完整链路
3.1 数据格式与读取预处理
整个工程文件按照功能拆分成了几个部分:主脚本负责流程控制,三个函数分别负责数据读取、滤波解算、结果绘图,参数配置集中放在主脚本最前面,方便统一修改。
我处理的log文件是CSV格式,表头如下:
| 列 | 名称 | 单位 |
|---|---|---|
| 1 | timestamp | ms |
| 2-4 | acc_x, acc_y, acc_z | g |
| 5-7 | gyro_x, gyro_y, gyro_z | deg/s |
| 8-10 | mag_x, mag_y, mag_z | uT |
读取与预处理的核心逻辑是这样的:
rawData = readmatrix('imu_log_01.csv', 'NumHeaderLines', 1); t = rawData(:, 1) / 1000; % 转为秒 accel = rawData(:, 2:4); % 单位g gyro = deg2rad(rawData(:, 5:7)); % 转为rad/s mag = rawData(:, 8:10); % 单位uT % 时间戳不均匀时,统一重采样到100Hz fs = 100; t_uniform = t(1):1/fs:t(end); accel = interp1(t, accel, t_uniform); gyro = interp1(t, gyro, t_uniform); mag = interp1(t, mag, t_uniform); t = t_uniform;数据读取时还有个经常被忽略的细节:不同模块导出的CSV分隔符可能不一样,用readmatrix之前最好先打开文件看一眼。遇到过逗号分隔和数据之间带分号的情况,readmatrix解析出的矩阵形状完全不对,调试了半天才发现只是分隔符问题。
3.2 滤波主循环:预测与更新的实现
预处理结束之后进入主循环。每处理一帧数据,先调用predictState做状态预测,再依次调用updateAccel和updateMag做观测更新。循环体很短:
q = [1; 0; 0; 0]; % 初始四元数 P = eye(4); % 初始协方差矩阵 euler_history = zeros(length(t), 3); q_history = zeros(length(t), 4); for i = 2:length(t) dt = t(i) - t(i-1); q = predictState(q, P, gyro(i, :), dt, Q); q = updateAccel(q, P, accel(i, :), R_acc); q = updateMag(q, P, mag(i, :), R_mag); q_history(i, :) = q'; euler_history(i, :) = rad2deg(quat2euler(q)')'; end这里有一个值得注意的设计:dt是逐帧计算的,不是固定传一个常量。虽然预处理阶段做过均匀重采样,但实际递推时用真实时间间隔总归更稳妥。另外更新步我传的是陀螺仪的当前帧,实际工程中可以改用中间时刻的角速度来提升精度,但对绝大多数场景没那么敏感,不必过度设计。
3.3 姿态输出与可视化
滤波结果的输出环节包括角度换算和对比绘图。四元数转换欧拉角可以自己写公式,也可以用MATLAB自带的工具函数,源码里保留了独立函数quat2euler,方便没有相关工具箱的环境直接运行。
绘图部分最常用的是对比曲线图,把加速度计直接解算的角度和卡尔曼滤波后的角度放在同一张图上,效果一目了然:
figure; subplot(3,1,1); plot(t, euler_history(:,1), 'LineWidth', 1.5); hold on; plot(t, roll_accel_raw, '--', 'LineWidth', 1); legend('卡尔曼滤波','加速度计直接解算'); ylabel('roll (deg)'); grid on; % pitch和yaw的绘图逻辑类似,省略这种曲线图除了给自己看效果,也是调参时的重要依据。如果滤波后的曲线太过平滑、跟不上实际运动,说明过程噪声设小了或者观测噪声设大了;如果曲线仍然抖动剧烈,说明对加速度计的信任权重太高。
4. 参数整定:过程噪声、测量噪声那些经验值
4.1 噪声协方差矩阵的初始值
卡尔曼滤波里最难调的其实不是状态方程,而是Q和R这两个噪声协方差矩阵。它们描述的分别是过程模型和测量模型的可信程度,但它们并不是直接测出来的,而是需要通过实验估计和反复试凑。
我这套源码里常用的初始值如下:
| 矩阵 | 初始值 | 含义 |
|---|---|---|
| Q | 1e-4 * eye(4) | 四元数过程噪声协方差 |
| R_acc | 0.01 * eye(2) | 加速度计观测噪声协方差 |
| R_mag | 0.1 * eye(1) | 磁力计观测噪声协方差 |
Q取1e-4量级,意味着在每一步预测中引入了约0.01弧度量级的不确定性,这个值给了陀螺仪预测较高的信任度。R_acc取0.01,单位是弧度平方,相当于认为加速度计解算角度的测量误差标准差在0.1弧度(约5.7度)左右,这个值对受振动干扰的场景算合理。R_mag取0.1,相当于认为磁力计给出的航向角测量误差标准差在0.3弧度(约18度)左右,因为室内磁场扰动比较明显,我对它的信任度设得比较低。
参数在代码里放在最前面,方便随时改:
Q = 1e-4 * eye(4); R_acc = 0.01 * eye(2); R_mag = 0.1;4.2 调参顺序与判断标准
调参的正确顺序不是一上来就同时改所有参数,而是一条路走到底。我的习惯是先用静态数据调R,再用动态数据调Q。静态测试时设备放在桌上不动,此时真实姿态就是恒定值,滤波输出应该趋于静止。如果R_acc设置得太大,滤波器会过于信任陀螺仪预测,静态时姿态会出现缓慢漂移,这时逐渐减小R_acc直到输出平稳且噪声可接受。
动态测试时手持设备做大幅摆动,观察姿态响应是否跟手、是否平滑。如果响应太慢,说明Q太小或R太大,需要增大Q。如果输出毛刺多,说明R太小,需要适当加大。反复试几轮,就能找到一个兼顾平滑和响应的平衡点。
这里要特别提一个经验:R_acc不要直接给到非常小,比如0.0001这种量级,否则相当于完全信任加速度计的角度观测,高频振动会直接穿透滤波器。实际测试中,振动环境下加速度计解算角度的误差分布往往不是高斯分布,卡尔曼滤波的“最优性”前提并不完全成立,保留一定的观测噪声余量反而更稳健。
5. 实测复盘:坐标系陷阱、磁力计干扰和几个隐藏的坑
5.1 坐标系不一致引起的怪现象
第一次跑通滤波代码时,输出的pitch角符号跟预期相反,roll角方向也乱七八糟。检查滤波逻辑和参数都没问题,最后发现是加速度计的z轴方向理解错了——模块静止平放时,加速度计的z轴读数约等于+1g,但我用的算法公式里默认静止时z轴读数应该等于-1g,相当于把重力方向反了180度。
这种坐标系错位不会体现为程序报错,只会让姿态曲线“差一点”,很容易被误判为滤波发散。排查办法是单独静态测试每个传感器:把模块分别沿x、y、z轴竖直放置,记录三个方向的输出符号,与算法假设逐一核对。想省事的话,在源码的readData阶段就完成坐标统一,把所有传感器的轴定义都对到同一个右手坐标系,后面写公式时不容易出错。
磁力计也有类似的坐标系问题。有些模块的磁力计x轴和加速度计x轴方向一致,有些则相反。磁力计yaw角解算如果对不上方向,偏航角会出现180度级别的跳变,而且转圈时方向会反转,这种症状比坐标反向更明显。
5.2 磁力计受干扰的识别与预处理
室内测试时,磁力计的读数波动比室外大得多,yaw角即使静止时也会有几度的缓慢摆动。如果只是做短时间的姿态解算,可以适当增大R_mag,让滤波器对磁力计的信任度降低,依靠陀螺仪积分维持短期航向。但如果长时间运行,磁力计的作用不可替代,不修正的话yaw角还是会漂移。
更麻烦的是附近有铁磁性物质,比如金属桌子、电源适配器、手机扬声器,磁场会被明显扭曲。这种干扰不是高斯白噪声,卡尔曼滤波无法很好处理。实测时遇到yaw角突然偏了十几度、好几秒不恢复的情况,十有八九是强磁干扰。判断方法是看磁力计三个轴的模值是否明显偏离当地地磁场强度,如果平稳时段磁通量模值突然跳变,那这一段数据的磁力计观测就不可信。
一个简单易行的缓解措施是:解算yaw前先做磁力计校准,采集模块在水平面旋转一圈的数据,求出x和y轴的偏置和比例系数,做成硬铁校准。这个校准只需要一次,后面处理所有log数据时都能用。
5.3 初始姿态估计:一个经常被忽略的细节
滤波器的初始四元数如果直接设为单位四元数,相当于假设设备初始姿态与参考系完全对齐。但实际拿在手上时,设备几乎不可能正好处于这个理想姿态,滤波器在启动阶段就会出现明显的收敛过程,曲线从零逐渐追到真实姿态,动态场景下甚至要好几秒才能追上。
解决这个问题很简单:用静止时的第一帧加速度计和磁力计数据估算初始姿态。加速度计的读数能给出roll和pitch,磁力计读数结合已知的roll和pitch能解出yaw,三个角合成初始四元数,滤波器从一开始就处在正确姿态附近,收敛速度显著加快。
源码里我用一个单独函数实现初始姿态估计,在进入主循环之前调用一次。这是一个容易被忽略但对实际体验影响很大的细节。
最后再说说移植的事。这套MATLAB源码的核心价值不在于直接产出一个可以用在嵌入式设备上的滤波器,而在于把9轴IMU数据融合的处理链路完整走了一遍:从原始log读取,到坐标系统一,到EKF建模,到参数整定,再到结果可视化。我自己后续做单片机移植时,就是照着这套MATLAB逻辑,把四元数预测和观测更新改写成C语言结构体,参数直接用这里调试好的初始值再微调。如果你也有类似的移植计划,建议先把MATLAB侧的效果调到位再动手,那会比直接在一堆调试日志里摸索C代码快得多。
本文还有配套的精品资源,点击获取