基于卡尔曼滤波的九轴姿态估计:Matlab实现与嵌入式移植

发布时间:2026/10/1 1:46:10
基于卡尔曼滤波的九轴姿态估计:Matlab实现与嵌入式移植
1. 从飞控工程师的视角看姿态估计这件事搞无人机飞控的人都有一个共识姿态估计是整个控制回路的地基。你可以把PID调得天花乱坠可以把路径规划做得再优雅但只要姿态角估计出现几度偏差轻则悬停漂移重则直接翻车。我见过太多刚入行的朋友拿到MPU6050或者ICM42688的原始数据之后直接做atan2算角度结果飞机一振动就疯掉然后开始怀疑人生。这个项目的核心目标很明确用卡尔曼滤波把陀螺仪、加速度计、磁力计这三类传感器的数据融合起来输出稳定的横滚Roll、俯仰Pitch、偏航Yaw三个姿态角同时结合气压计做高度估计。整个系统在Matlab里实现方便做算法验证和参数调优验证通过之后再移植到STM32或者其它嵌入式平台。为什么一定要做融合因为每种传感器单独拿出来都有致命缺陷。陀螺仪动态响应好、短期精度高但积分会漂移时间一长角度就飞了加速度计能感知重力方向长期不漂但振动和机动加速度会严重污染信号磁力计能提供绝对航向参考但容易被电机磁场和周围铁磁物质干扰。卡尔曼滤波的价值就在于用陀螺仪的短期精度去平滑加速度计和磁力计的噪声同时用加速度计和磁力计的长期稳定性去修正陀螺仪的漂移。这篇文章适合谁看如果你正在做无人机飞控开发、机器人姿态估计、或者任何需要多传感器融合的项目并且希望用Matlab快速验证算法再移植到嵌入式平台那这篇内容会对你有直接帮助。我会把整个系统的架构设计、卡尔曼滤波的建模过程、Matlab实现细节、参数调优经验、以及实际踩过的坑都讲清楚。2. 九轴姿态估计的系统架构与传感器角色分工2.1 三类传感器各自的能力边界在动手写代码之前必须先把每个传感器的特性吃透。我见过很多人上来就开始写卡尔曼滤波结果连输入数据的物理意义都没搞清楚最后调参调到崩溃。陀螺仪输出的是角速度单位是度每秒或者弧度每秒。它的优势是响应快、噪声相对小、不受线性加速度影响。但它的输出需要积分才能得到角度而积分会累积误差。零偏bias是陀螺仪最大的敌人即使你静止不动陀螺仪的输出也不是严格的零这个微小的偏差经过积分之后会越来越大。以一款常见的MEMS陀螺仪为例零偏稳定性大概在0.01度每秒量级看起来很小但积分60秒之后就是0.6度的误差积分10分钟就是6度。所以陀螺仪必须被修正。加速度计测量的是比力静止时输出的是重力加速度在三轴上的分量。通过重力分量可以反算出横滚和俯仰角而且这个角度是绝对参考不会随时间漂移。但问题在于当无人机加速运动时加速度计测量的是重力加速度和运动加速度的叠加这时候算出来的姿态角就是错的。另外电机振动会直接耦合到加速度计上如果不做低通滤波数据基本没法用。磁力计测量的是地磁场在三轴上的分量可以提供绝对的航向参考。没有磁力计的话偏航角只能靠陀螺仪积分漂移会非常严重。但磁力计非常脆弱电机电流产生的磁场、电池的磁场、周围钢筋结构的磁场都会干扰它。在实际飞行中磁力计的读数经常需要做椭球拟合校准否则航向误差可能达到几十度。2.2 融合架构的整体数据流整个系统的数据流可以这样理解陀螺仪提供状态预测加速度计和磁力计提供观测修正。卡尔曼滤波在每个采样周期做两件事——预测和更新。预测阶段用上一时刻的姿态角估计值加上陀螺仪测得的角速度乘以采样时间得到当前时刻的姿态角先验估计。同时状态协方差矩阵也要根据过程噪声进行传播。更新阶段用加速度计和磁力计的计算结果作为观测量与先验估计进行比较计算卡尔曼增益然后修正状态估计和协方差矩阵。高度估计的通道稍微不同。气压计提供绝对高度观测但气压计受温度、气流、天气影响很大短期噪声也不小。通常的做法是用气压计做长期高度参考用加速度计的Z轴积分做短期高度变化估计两者通过卡尔曼滤波融合。在Matlab里我建议把整个系统拆成几个模块数据采集与预处理模块、姿态解算模块、高度估计模块、可视化模块。每个模块用独立的函数或类来实现方便单独调试和替换。2.3 坐标系定义与旋转顺序这是最容易被忽视但最不能出错的地方。坐标系定义错了后面所有计算都是白搭。我采用的是北东地NED坐标系作为导航坐标系X轴指向北Y轴指向东Z轴指向地。机体坐标系采用前右下X轴指向机头Y轴指向右侧Z轴指向下方。旋转顺序采用Z-Y-X也就是先绕Z轴转偏航角再绕Y轴转俯仰角最后绕X轴转横滚角。这个顺序对应的旋转矩阵是% 从机体坐标系到导航坐标系的旋转矩阵 function R euler2rotm(roll, pitch, yaw) cr cos(roll); sr sin(roll); cp cos(pitch); sp sin(pitch); cy cos(yaw); sy sin(yaw); R [cy*cp, cy*sp*sr - sy*cr, cy*sp*cr sy*sr; sy*cp, sy*sp*sr cy*cr, sy*sp*cr - cy*sr; -sp, cp*sr, cp*cr]; end注意不同的飞控固件可能采用不同的旋转顺序和坐标系定义。在移植代码之前一定要确认你的飞控采用的约定否则姿态角会出现莫名其妙的耦合。3. 卡尔曼滤波器的建模从连续系统到离散实现3.1 状态方程与观测方程的设计思路卡尔曼滤波的核心在于状态空间模型的设计。对于姿态估计最直接的做法是把姿态角作为状态量。但姿态角的运动学方程是非线性的因为角速度到欧拉角变化率的转换矩阵依赖于当前的姿态角。一种常见的简化方案是在小角度假设下做线性化但对于无人机这种大角度机动的场景线性化误差会比较大。另一种方案是用四元数作为状态量避免万向节锁问题但四元数的归一化约束在标准卡尔曼滤波中不好处理。我在这个项目里采用的是误差状态卡尔曼滤波的思路用名义状态做预测用误差状态做滤波。名义状态用陀螺仪积分更新误差状态用卡尔曼滤波估计然后反馈修正名义状态。这样做的好处是误差状态始终是小量线性化精度高。具体来说状态量取姿态误差角横滚、俯仰、偏航的误差和陀螺仪零偏误差共6维% 状态向量: [delta_roll, delta_pitch, delta_yaw, bias_x, bias_y, bias_z] n_state 6;状态转移矩阵F根据陀螺仪的测量值和当前姿态角构建function F build_state_transition(omega, dt) % omega: 三轴角速度测量值 [wx; wy; wz] % dt: 采样周期 wx omega(1); wy omega(2); wz omega(3); % 姿态误差角的运动学矩阵 F_att [1, 0, 0; 0, 1, 0; 0, 0, 1]; % 陀螺仪零偏对姿态误差的影响 F_bias -eye(3) * dt; F [F_att, F_bias; zeros(3,3), eye(3)]; end观测方程用加速度计和磁力计构建。加速度计观测的是重力方向磁力计观测的是地磁方向。观测矩阵H需要根据当前姿态角计算function H build_observation_matrix(roll, pitch, yaw) % 加速度计对姿态误差的雅可比 % 磁力计对姿态误差的雅可比 % 这里省略具体推导核心思想是对旋转矩阵求偏导 H_acc compute_acc_jacobian(roll, pitch, yaw); H_mag compute_mag_jacobian(roll, pitch, yaw); H [H_acc, zeros(3,3); H_mag, zeros(3,3)]; end3.2 过程噪声与观测噪声的整定逻辑过程噪声矩阵Q和观测噪声矩阵R的整定是卡尔曼滤波调参的核心。很多人调参就是瞎试其实背后有明确的物理意义。Q矩阵反映的是你对状态预测的信任程度。Q越大滤波器越信任观测值Q越小滤波器越信任预测值。对于姿态估计Q的主要来源是陀螺仪的噪声和零偏不稳定性。陀螺仪的噪声密度可以从数据手册查到零偏不稳定性也可以通过Allan方差分析得到。R矩阵反映的是你对观测值的信任程度。加速度计的噪声主要来自振动磁力计的噪声主要来自环境干扰。在实际调参时我会先根据数据手册给一个初始值然后用实际采集的静态数据做验证。% 过程噪声矩阵 sigma_gyro_noise 0.01; % 陀螺仪噪声密度度/秒/根号Hz sigma_gyro_bias 0.001; % 陀螺仪零偏不稳定性 Q diag([sigma_gyro_noise^2*dt, sigma_gyro_noise^2*dt, sigma_gyro_noise^2*dt, ... sigma_gyro_bias^2*dt, sigma_gyro_bias^2*dt, sigma_gyro_bias^2*dt]); % 观测噪声矩阵 sigma_acc 0.05; % 加速度计噪声g sigma_mag 0.1; % 磁力计噪声归一化单位 R diag([sigma_acc^2, sigma_acc^2, sigma_acc^2, ... sigma_mag^2, sigma_mag^2, sigma_mag^2]);实操心得Q和R的比值比绝对值更重要。如果你发现滤波器响应太慢跟不上真实的姿态变化说明Q相对R太小了需要增大Q或者减小R。反之如果估计结果抖动厉害说明Q相对R太大了。3.3 连续系统离散化的实现细节陀螺仪的输出是离散的角速度采样但物理过程是连续的。在做卡尔曼滤波时需要把连续的状态方程离散化。最简单的方法是一阶近似F_discrete eye(n_state) F_continuous * dt;但对于采样率较低或者角速度较大的情况一阶近似的误差会比较大。更精确的做法是用矩阵指数F_discrete expm(F_continuous * dt);在Matlab里expm函数可以直接计算矩阵指数。虽然计算量比一阶近似大但在现代处理器上完全可以接受。我实测下来在200Hz采样率下两种方法的差异很小但在50Hz采样率下差异就比较明显了。4. Matlab实现从数据预处理到姿态解算的完整链路4.1 原始传感器数据的预处理拿到原始数据之后不能直接扔进卡尔曼滤波器。预处理这一步做不好后面怎么调参都白费。加速度计的低通滤波是必须的。电机振动的主频通常在100-300Hz而姿态变化的带宽一般不超过20Hz。用一个截止频率50Hz左右的二阶巴特沃斯低通滤波器可以显著改善加速度计信号质量。% 设计低通滤波器 fs 200; % 采样率 fc 50; % 截止频率 [b, a] butter(2, fc/(fs/2)); % 对加速度计三轴数据滤波 acc_filtered filtfilt(b, a, acc_raw);注意filtfilt做的是零相位滤波不会引入相位延迟适合离线处理。如果要在嵌入式平台上实时运行需要用filter函数但要注意相位延迟对姿态估计的影响。磁力计的椭球拟合校准是另一个关键步骤。未校准的磁力计数据分布在一个偏移的椭球上直接使用会导致航向角严重偏差。校准的方法是采集各个方向的数据拟合椭球参数然后做变换。% 简化的椭球拟合校准 % 假设已经采集了N组磁力计数据 mag_data (N x 3) % 拟合椭球方程: (x-c)*A*(x-c) 1 % 然后变换为单位球 % 这里用最小二乘法拟合 % 具体实现略核心思想是求解椭球参数陀螺仪的零偏校准在静止状态下进行。采集几百个样本取平均值作为零偏估计然后在后续数据中减去这个零偏。% 静止状态下的陀螺仪零偏校准 num_samples 500; gyro_bias mean(gyro_raw(1:num_samples, :), 1); gyro_calibrated gyro_raw - gyro_bias;4.2 卡尔曼滤波主循环的代码结构整个卡尔曼滤波的主循环可以分成预测和更新两个阶段。在Matlab里我习惯把滤波器封装成一个类这样状态管理更清晰。classdef AttitudeEKF handle properties x % 状态向量 P % 协方差矩阵 Q % 过程噪声 R % 观测噪声 dt % 采样周期 end methods function obj AttitudeEKF(dt) obj.dt dt; obj.x zeros(6, 1); obj.P eye(6) * 0.1; obj.Q diag([1e-4, 1e-4, 1e-4, 1e-6, 1e-6, 1e-6]); obj.R diag([2.5e-3, 2.5e-3, 2.5e-3, 1e-2, 1e-2, 1e-2]); end function predict(obj, gyro, roll, pitch, yaw) % 构建状态转移矩阵 F obj.build_F(gyro); % 状态预测 obj.x F * obj.x; % 协方差预测 obj.P F * obj.P * F obj.Q; end function update(obj, acc, mag, roll, pitch, yaw) % 构建观测矩阵 H obj.build_H(roll, pitch, yaw); % 计算观测残差 y obj.compute_residual(acc, mag, roll, pitch, yaw); % 卡尔曼增益 S H * obj.P * H obj.R; K obj.P * H / S; % 状态更新 obj.x obj.x K * y; % 协方差更新 obj.P (eye(6) - K * H) * obj.P; end end end主循环的调用逻辑ekf AttitudeEKF(1/200); for k 2:length(data) % 获取当前传感器数据 gyro data.gyro(k, :); acc data.acc(k, :); mag data.mag(k, :); % 预测 ekf.predict(gyro, roll, pitch, yaw); % 更新 ekf.update(acc, mag, roll, pitch, yaw); % 反馈修正 roll roll ekf.x(1); pitch pitch ekf.x(2); yaw yaw ekf.x(3); % 记录结果 attitude_log(k, :) [roll, pitch, yaw]; end4.3 高度估计通道的实现高度估计用的是类似的结构但状态量不同。我采用的状态量是高度和垂直速度% 高度估计的状态向量: [height, velocity_z] % 观测: 气压计高度 % 控制输入: 加速度计Z轴 function [h_est, v_est, P] height_ekf(h_est, v_est, P, acc_z, baro_h, dt) % 状态转移 F [1, dt; 0, 1]; B [0.5*dt^2; dt]; % 预测 x_pred F * [h_est; v_est] B * acc_z; P_pred F * P * F Q_height; % 更新 H [1, 0]; y baro_h - x_pred(1); S H * P_pred * H R_baro; K P_pred * H / S; x_update x_pred K * y; P (eye(2) - K * H) * P_pred; h_est x_update(1); v_est x_update(2); end气压计的高度数据需要做温度补偿和低通滤波。我通常用一个截止频率1Hz左右的低通滤波器因为气压计的变化本身就很慢。5. 调参过程中那些让我抓狂的坑5.1 加速度计振动耦合导致的姿态抖动这个问题我遇到过不止一次。飞机在地面静止时姿态估计很稳一上电机动起来就开始抖。排查了半天最后发现是电机振动通过机架传到了飞控板上加速度计的输出里混入了高频振动信号。解决方案分两层硬件上飞控板要加减振棉而且减振棉的硬度要匹配机架和电机的振动频率软件上加速度计的低通滤波截止频率要调低我一般设在30-50Hz之间。但截止频率也不能太低否则机动时的姿态响应会变慢。实测经验如果你的加速度计数据在静止时方差就很大先别急着调卡尔曼滤波参数先把振动问题解决了。用Matlab对静止数据做FFT分析看看振动主频在哪里然后针对性地设计滤波器。5.2 磁力计干扰导致的偏航角跳变磁力计的问题更隐蔽。室内飞行时钢筋结构、电脑、音箱都会产生磁场干扰。我有一次在实验室调试偏航角每隔几秒就跳几十度查了半天才发现是桌上的音箱在作怪。磁力计的校准不能一劳永逸。每次换场地都要重新校准而且校准时要远离铁磁物质。在Matlab里我写了一个简单的校准脚本采集数据后自动拟合椭球参数。另一个技巧是给磁力计加一个可信度判断。当磁力计读数与预期值偏差过大时暂时降低它在卡尔曼滤波中的权重甚至完全不用它来修正偏航角只靠陀螺仪积分撑一段时间。% 磁力计可信度判断 mag_norm norm(mag); expected_norm 1.0; % 归一化后的期望值 if abs(mag_norm - expected_norm) 0.3 % 磁力计数据不可信增大R_mag R_mag R_mag * 100; else R_mag R_mag_normal; end5.3 陀螺仪零偏随温度漂移MEMS陀螺仪的零偏对温度非常敏感。冬天在室外飞和夏天在室内飞零偏可能差好几度每秒。如果不做温度补偿姿态估计会慢慢漂移。我的做法是在飞控上电后先静置一段时间让陀螺仪温度稳定然后采集零偏。如果条件允许可以在飞控板上加一个温度传感器建立零偏-温度查找表实时补偿。在Matlab仿真阶段我通常会人为给陀螺仪数据加一个缓慢变化的零偏测试卡尔曼滤波器对零偏的估计能力。如果滤波器能收敛到真实零偏附近说明Q矩阵中零偏对应的噪声设置是合理的。5.4 采样率不匹配引发的时序问题这个问题在热词里也有人问无人机IMU采样率达不到200Hz会造成什么影响。我的实测经验是如果IMU采样率低于100Hz姿态估计的延迟会明显增大快速机动时姿态角会滞后。如果采样率低于50Hz基本上没法做稳定的姿态控制。卡尔曼滤波的dt参数必须与实际采样周期严格一致。如果IMU采样率是100Hz但你在代码里写的是1/200那状态预测就会出错。更隐蔽的问题是采样抖动如果采样周期不稳定dt时大时小滤波器的性能会下降。解决方案是在嵌入式平台上用定时器触发采样保证采样周期的稳定性。在Matlab仿真时可以用实际记录的时间戳来计算dt而不是用固定的采样周期。6. 仿真验证与嵌入式移植的衔接6.1 用Matlab做闭环仿真验证算法在Matlab里跑通之后不要急着移植到嵌入式平台。先在Matlab里做闭环仿真验证算法在各种机动条件下的表现。我通常会构造几组测试场景静态悬停、缓慢倾斜、快速翻滚、偏航旋转。每组场景下用真实的传感器数据或者模拟数据驱动卡尔曼滤波器然后对比估计姿态与参考姿态的误差。% 计算姿态估计误差 error_roll attitude_est(:,1) - attitude_ref(:,1); error_pitch attitude_est(:,2) - attitude_ref(:,2); error_yaw attitude_est(:,3) - attitude_ref(:,3); % 计算RMS误差 rms_roll sqrt(mean(error_roll.^2)); rms_pitch sqrt(mean(error_pitch.^2)); rms_yaw sqrt(mean(error_yaw.^2)); fprintf(Roll RMS Error: %.2f deg\n, rms_roll); fprintf(Pitch RMS Error: %.2f deg\n, rms_pitch); fprintf(Yaw RMS Error: %.2f deg\n, rms_yaw);经验值参考在良好的校准和调参条件下静态时横滚和俯仰的RMS误差可以做到0.5度以内偏航可以做到1-2度以内。动态时误差会大一些但横滚和俯仰通常不超过2度。6.2 定点化与计算量优化从Matlab移植到STM32这类嵌入式平台时浮点运算的开销是需要考虑的。如果MCU没有硬件浮点单元单精度浮点运算会非常慢。这时候需要考虑定点化。定点化的核心是确定每个变量的Q格式。姿态角的范围是-180到180度用Q8格式8位小数可以表示到0.004度的精度足够了。协方差矩阵的元素范围变化很大需要动态调整Q格式。另一个优化点是减少矩阵运算的维度。6维状态向量的卡尔曼滤波每次迭代涉及多次6x6矩阵乘法计算量不小。如果MCU资源紧张可以考虑降维比如把陀螺仪零偏建模为一阶马尔可夫过程而不是随机游走或者干脆不估计零偏只做姿态修正。在Matlab里可以用tic/toc来测量每次迭代的耗时评估算法的实时性。% 测量卡尔曼滤波单次迭代耗时 num_iterations 1000; tic; for i 1:num_iterations ekf.predict(gyro, roll, pitch, yaw); ekf.update(acc, mag, roll, pitch, yaw); end elapsed toc; fprintf(Average iteration time: %.3f ms\n, elapsed/num_iterations*1000);6.3 代码生成与硬件在环测试Matlab的Embedded Coder可以直接把算法代码生成C代码省去手工移植的麻烦。但自动生成的代码通常可读性较差而且可能包含一些不必要的内存分配。我的做法是先用Embedded Coder生成一版代码然后手工优化关键部分。硬件在环测试是移植前的最后一道关卡。把生成的代码烧录到飞控板上通过串口把姿态估计结果实时传回Matlab与Matlab端的估计结果做对比。如果两者一致说明移植成功如果有偏差通常是数据类型或者运算顺序的问题。7. 几个容易被忽略但很关键的工程细节7.1 初始对准的重要性卡尔曼滤波器需要一个初始状态。如果初始姿态角误差太大滤波器需要很长时间才能收敛。在实际使用中我通常会在上电后做一次初始对准用加速度计计算初始的横滚和俯仰角用磁力计计算初始偏航角。% 初始对准 roll_init atan2(acc(2), acc(3)); pitch_init atan2(-acc(1), sqrt(acc(2)^2 acc(3)^2)); yaw_init atan2(mag(2)*cos(roll_init) - mag(3)*sin(roll_init), ... mag(1)*cos(pitch_init) mag(2)*sin(pitch_init)*sin(roll_init) ... mag(3)*sin(pitch_init)*cos(roll_init));初始对准时飞机必须静止否则加速度计和磁力计的读数都不准。7.2 万向节锁的规避欧拉角在俯仰角接近正负90度时会出现万向节锁这是欧拉角表示法的固有缺陷。对于大多数无人机应用俯仰角不会接近90度所以问题不大。但如果你的应用场景涉及大角度机动建议用四元数表示姿态。在Matlab里可以用quaternion类来做四元数运算然后转换成欧拉角输出。7.3 数据记录与回放机制调试姿态估计算法时数据记录和回放非常重要。我习惯把原始传感器数据、滤波中间变量、最终估计结果都记录下来存成mat文件。这样每次调参之后可以用同一组数据回放对比不同参数下的估计效果。% 数据记录 log.gyro gyro_data; log.acc acc_data; log.mag mag_data; log.attitude_est attitude_log; log.P_diag P_diag_log; save(flight_log_001.mat, log);回放的时候只需要把记录的数据重新喂给滤波器就可以复现当时的估计过程。这个习惯帮我省了大量时间尤其是在排查偶发性问题时。7.4 实时性监控与降级策略在嵌入式平台上运行时要监控卡尔曼滤波的单次执行时间。如果某次迭代超时了说明系统负载过高需要降级处理。降级策略可以是降低滤波器的更新频率、简化观测模型、或者暂时切换到互补滤波。在Matlab仿真阶段我通常会人为加入计算延迟测试算法对时序抖动的鲁棒性。如果延迟超过一定阈值滤波器性能会明显下降这时候就需要优化代码或者提高MCU的主频。8. 从算法验证到产品化的思考把卡尔曼滤波姿态估计从Matlab仿真做到能实际飞中间还有不少路要走。Matlab的价值在于快速验证算法逻辑和调参但最终产品需要考虑的东西更多传感器的选型、PCB布局、减振设计、温度补偿、电磁兼容、故障检测与容错。我个人的体会是算法本身只占整个工程量的30%左右剩下70%都是工程细节。一个在Matlab里跑得很漂亮的算法如果传感器数据质量不行、时序不对、振动没处理好实际表现可能还不如一个简单的互补滤波。所以我的建议是在Matlab里把算法验证充分之后尽早做硬件在环测试尽早暴露工程问题。不要等到算法完美了才开始移植因为工程问题往往会反过来要求你修改算法。另外卡尔曼滤波不是万能的。如果传感器数据质量太差或者模型与实际系统偏差太大卡尔曼滤波也会失效。这时候需要考虑更鲁棒的方案比如自适应卡尔曼滤波、H无穷滤波、或者基于优化的方法。但对于大多数无人机姿态估计场景标准卡尔曼滤波配合良好的传感器校准和预处理已经足够用了。最后分享一个我在实际项目中总结的参数调优顺序先调加速度计和磁力计的预处理滤波器确保输入数据干净然后调Q和R的比值让滤波器在静态时稳定、动态时跟得上最后调初始协方差矩阵P0加快收敛速度。这个顺序可以避免很多无效的调参尝试。