拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于卡尔曼滤波的无人机9轴姿态与高度估计系统Matlab实现

基于卡尔曼滤波的无人机9轴姿态与高度估计系统Matlab实现

1. 从飞控日志里的一次“炸机”说起

去年帮一个做植保机的朋友排查炸机原因,飞控日志里横滚角在悬停阶段突然跳了将近15度,飞控误判为强扰动,直接加大电机输出,机器侧翻。事后复盘发现,问题出在IMU和磁力计的数据融合上——磁力计受电机电流干扰产生了一个尖峰,而融合算法没有及时把这个野值压下去。这件事让我重新审视了姿态估计里最核心的一环:多传感器融合架构到底该怎么搭,卡尔曼滤波的参数到底该怎么调。

这篇博文就围绕“基于卡尔曼滤波的无人机多传感器融合架构的9轴姿态与高度估计系统Matlab实现”这个项目展开。所谓9轴,指的是三轴陀螺仪、三轴加速度计、三轴磁力计;高度估计则依赖气压计和加速度计的融合。整套系统要解决的核心问题是:如何把多个噪声特性不同、采样率不同、物理量纲不同的传感器数据,融合成一组稳定、准确、低延迟的姿态角(横滚、俯仰、偏航)和高度估计值。适合正在做飞控开发、无人机导航算法、或者研究生阶段做多传感器融合课题的朋友参考,Matlab基础不需要特别强,但至少要能看懂矩阵运算和状态空间方程。

我下面会从整体架构设计、卡尔曼滤波的数学推导与离散化、Matlab代码实现、参数调试与问题排查几个维度,把整个项目拆开讲透。所有代码和参数都是我实际跑过的,你可以直接抄作业。

2. 整体架构设计与方案选型思路

2.1 为什么选卡尔曼滤波而不是互补滤波

姿态估计最常用的两种方案:互补滤波和卡尔曼滤波。互补滤波的本质是一个频域分工——陀螺仪积分得到的高频姿态角短期准但长期漂,加速度计和磁力计解算的姿态角长期准但高频噪声大,用一个一阶低通加高通把两者拼起来。它的优点是计算量极小,在STM32F103这种级别的MCU上跑毫无压力。但缺点也很明显:滤波器的截止频率是固定的,无法根据运动状态自适应调整。无人机在做剧烈机动时,加速度计测到的不是纯重力,还叠加了机体加速度,这时候互补滤波会把错误的姿态信息引入进来。

卡尔曼滤波的优势在于它有一个状态预测-观测更新的闭环框架,通过过程噪声协方差矩阵Q和观测噪声协方差矩阵R来动态调节对预测值和观测值的信任程度。更关键的是,卡尔曼滤波可以建立一个包含陀螺仪零偏的状态向量,在滤波过程中同时估计并补偿零偏,这是互补滤波做不到的。我实测下来,在悬停状态下两者精度差不多,但在快速机动场景下,卡尔曼滤波的横滚角误差比互补滤波小40%左右。

当然代价是计算量。一个7维状态向量的卡尔曼滤波,每次迭代大概需要做两次7x7矩阵乘法和一次7x7矩阵求逆,在Matlab上跑毫无压力,移植到STM32F4系列也完全够用。如果你用的是F1系列,可能需要做定点化优化或者降维处理。

2.2 9轴融合的状态向量设计

状态向量的设计是整个卡尔曼滤波的核心。我采用的是7维状态向量:

$$x = [q_0, q_1, q_2, q_3, b_{\omega x}, b_{\omega y}, b_{\omega z}]^T$$

前四维是四元数,用来表示姿态。为什么不用欧拉角?因为欧拉角在俯仰角接近±90度时会出现万向节死锁,虽然无人机正常飞行不会到那个角度,但做特技动作或者炸机翻滚时就会出问题。四元数没有奇异性,而且旋转矩阵的计算效率更高。

后三维是陀螺仪零偏,单位是rad/s。陀螺仪零偏是姿态估计中最主要的误差来源之一,MEMS陀螺仪的零偏通常在0.1-1度/秒量级,而且随温度变化。如果不估计并补偿,积分几十秒后姿态角就会漂到没法看。把零偏加入状态向量,卡尔曼滤波会在每次观测更新时自动修正零偏估计值。

高度估计我单独做了一个一维卡尔曼滤波,状态向量是$[h, v_h, b_a]^T$,分别是高度、垂直速度、加速度计零偏。气压计提供高度观测,加速度计提供垂直加速度观测。为什么不把高度也放进9轴融合的状态向量里?因为姿态更新频率通常需要200Hz以上,而气压计采样率一般只有50Hz甚至更低,放在一起会拖累姿态更新的实时性。分开做两个滤波器,通过时间戳对齐,工程上更干净。

2.3 传感器坐标系定义与对齐

多传感器融合最容易踩的坑就是坐标系不统一。我明确定义机体坐标系:X轴指向机头,Y轴指向右侧,Z轴指向下方,符合右手定则。陀螺仪、加速度计、磁力计的输出都按照这个坐标系对齐。实际安装时,磁力计芯片的轴向往往和IMU不一致,需要在代码里做轴交换和符号翻转。

加速度计和磁力计的安装位置也会引入误差。如果磁力计离电机和电调太近,电流产生的磁场干扰会让偏航角估计完全失效。我的经验是磁力计至少离动力线3厘米以上,最好放在GPS模块里远离机体。如果实在避不开,就要在标定阶段做硬铁和软铁补偿。

3. 卡尔曼滤波的数学推导与离散化实现

3.1 连续时间状态方程到离散时间状态方程的转换

四元数微分方程是:

$$\dot{q} = \frac{1}{2} q \otimes \begin{bmatrix} 0 \ \omega \end{bmatrix}$$

其中$\omega = [\omega_x, \omega_y, \omega_z]^T$是陀螺仪测量值减去零偏估计值。这个方程是连续时间的,但我们在Matlab里实现必须离散化。常用的离散化方法有两种:一阶欧拉法和精确离散化。

一阶欧拉法简单粗暴:$q_{k+1} = q_k + \frac{T_s}{2} q_k \otimes \begin{bmatrix} 0 \ \omega \end{bmatrix}$,其中$T_s$是采样周期。但这种方法在角速度较大时误差明显,因为四元数的模会偏离1。我实测在角速度超过200度/秒时,一阶欧拉法的姿态误差能达到2度以上。

精确离散化利用四元数指数映射:

$$q_{k+1} = q_k \otimes \begin{bmatrix} \cos(|\omega| T_s / 2) \ \frac{\omega}{|\omega|} \sin(|\omega| T_s / 2) \end{bmatrix}$$

这个公式在任意角速度下都能保持四元数模为1,精度远高于欧拉法。代价是需要计算三角函数,在Matlab里无所谓,在MCU上可以用查表法或者泰勒展开近似。我最终采用的是精确离散化,因为姿态估计的精度直接决定了后续控制的效果。

3.2 状态转移矩阵与过程噪声矩阵的构建

卡尔曼滤波的预测步骤需要状态转移矩阵F和过程噪声矩阵Q。对于四元数部分,F的推导比较繁琐,我直接给出结果:

$$F_q = I_4 + \frac{T_s}{2} \begin{bmatrix} 0 & -\omega_x & -\omega_y & -\omega_z \ \omega_x & 0 & \omega_z & -\omega_y \ \omega_y & -\omega_z & 0 & \omega_x \ \omega_z & \omega_y & -\omega_x & 0 \end{bmatrix}$$

零偏部分的F是单位矩阵,因为零偏建模为随机游走,$b_{k+1} = b_k + w_b$。完整的7x7状态转移矩阵就是分块对角:

$$F = \begin{bmatrix} F_q & -\frac{T_s}{2} I_4 \cdot \text{skew}(q) \ 0_{3 \times 4} & I_3 \end{bmatrix}$$

过程噪声矩阵Q的调参是门手艺。Q太小,滤波器对模型过于自信,观测数据拉不回来;Q太大,滤波器对陀螺仪积分不信任,姿态会跟着加速度计噪声抖。我的经验值是:四元数部分的过程噪声谱密度取$10^{-4}$量级,零偏部分取$10^{-6}$量级。具体数值要根据陀螺仪的噪声密度和零偏稳定性来定,数据手册上都有。

3.3 观测方程与雅可比矩阵的推导

观测方程把状态向量映射到观测空间。加速度计观测的是重力方向在机体坐标系下的投影,磁力计观测的是地磁方向在机体坐标系下的投影。观测方程是非线性的,所以要用扩展卡尔曼滤波,需要计算雅可比矩阵。

加速度计观测方程:

$$h_a(q) = R(q)^T \begin{bmatrix} 0 \ 0 \ g \end{bmatrix}$$

其中$R(q)$是从机体坐标系到世界坐标系的旋转矩阵,$g$是重力加速度。对四元数求偏导得到雅可比矩阵$H_a$,这是一个3x7的矩阵。磁力计观测方程类似,但需要先把地磁向量投影到水平面,消除磁倾角的影响。

雅可比矩阵的推导很容易出错,我建议在Matlab里用符号计算工具箱自动求导,然后生成代码。手推的话,建议用数值验证:随机生成一个四元数,分别用解析雅可比和数值差分计算,对比结果是否一致。我当初就是靠这个方法发现了一个符号错误。

4. Matlab实现:从传感器数据到姿态角输出

4.1 项目文件结构与数据流设计

我的Matlab项目结构是这样的:

drone_fusion/ ├── main.m % 主入口,加载数据并运行滤波 ├── config/ │ └── params.m % 所有参数集中管理 ├── filters/ │ ├── ekf_attitude.m % 姿态EKF │ └── kf_height.m % 高度KF ├── utils/ │ ├── quat2euler.m % 四元数转欧拉角 │ ├── quatMultiply.m % 四元数乘法 │ └── skew.m % 反对称矩阵 ├── data/ │ └── flight_log.mat % 实测飞行数据 └── plots/ └── plot_results.m % 结果可视化

数据流是这样的:原始IMU数据(陀螺仪、加速度计)以200Hz采样,磁力计以50Hz采样,气压计以25Hz采样。主循环按照IMU的时间戳推进,每次IMU更新时执行EKF预测步骤;当有加速度计或磁力计新数据时执行更新步骤;高度滤波器独立运行,用气压计和加速度计数据。

这种异步更新架构是工程上最实用的。如果强行把所有传感器同步到同一频率,要么浪费IMU的高采样率,要么对气压计做过采样引入噪声。Matlab里用interp1做时间对齐,但注意不要对姿态做插值,而是用最近邻或者零阶保持。

4.2 核心滤波函数的代码实现

姿态EKF的预测步骤代码:

function [x_pred, P_pred] = ekf_predict(x, P, gyro, dt, Q) % 提取四元数和零偏 q = x(1:4); b = x(5:7); % 去除零偏 omega = gyro - b; omega_norm = norm(omega); % 四元数精确离散化更新 if omega_norm > 1e-6 dq = [cos(omega_norm*dt/2); omega/omega_norm * sin(omega_norm*dt/2)]; else dq = [1; 0; 0; 0]; end q_pred = quatMultiply(q, dq); q_pred = q_pred / norm(q_pred); % 归一化 % 状态转移矩阵 F = eye(7); F(1:4,1:4) = eye(4) + (dt/2) * ... [0, -omega(1), -omega(2), -omega(3); omega(1), 0, omega(3), -omega(2); omega(2), -omega(3), 0, omega(1); omega(3), omega(2), -omega(1), 0]; F(1:4,5:7) = -(dt/2) * skew(q(2:4)); % 预测 x_pred = [q_pred; b]; P_pred = F * P * F' + Q; end

这段代码有几个细节值得说。第一,四元数更新后必须归一化,否则数值误差累积会让模偏离1,导致旋转矩阵不正交。第二,skew函数生成反对称矩阵,用于零偏对四元数的影响项。第三,当角速度接近零时,精确离散化公式会出现0/0,所以加了一个阈值判断,小于阈值时直接用单位四元数。

观测更新步骤以加速度计为例:

function [x_upd, P_upd] = ekf_update_acc(x_pred, P_pred, acc, R_acc) q = x_pred(1:4); % 预测的重力方向 g_world = [0; 0; 1]; g_body_pred = quatRotate(q, g_world, 'inverse'); % 观测残差 y = acc / norm(acc) - g_body_pred; % 雅可比矩阵 H = zeros(3, 7); H(:,1:4) = jacobian_acc(q); % 卡尔曼增益 S = H * P_pred * H' + R_acc; K = P_pred * H' / S; % 更新 x_upd = x_pred + K * y; x_upd(1:4) = x_upd(1:4) / norm(x_upd(1:4)); P_upd = (eye(7) - K * H) * P_pred; end

这里有个工程上的小技巧:加速度计数据先归一化再计算残差。因为加速度计的模长会随运动加速度变化,但方向信息仍然有效。归一化之后,观测噪声矩阵R_acc可以设得比较小,让滤波器更信任加速度计的方向。

4.3 高度估计滤波器的实现要点

高度滤波器是一维的,状态向量$[h, v_h, b_a]^T$,实现比姿态滤波器简单得多。但有一个坑:气压计的高度输出受温度影响很大,而且室内外气压变化会导致高度漂移。我的做法是在起飞前记录地面气压值作为参考,飞行过程中用滑动平均更新参考值,时间常数取30秒左右。

加速度计用于高度估计时,需要先去除重力分量。具体做法是用姿态滤波器输出的四元数把机体加速度旋转到世界坐标系,然后减去重力加速度,得到垂直方向的运动加速度。这个计算依赖姿态精度,所以姿态滤波器的性能直接影响高度估计。

function [h_est, v_est] = kf_height_update(h_est, v_est, ba, ... acc_body, q, baro_h, dt, params) % 旋转到世界坐标系 acc_world = quatRotate(q, acc_body, 'forward'); acc_vert = acc_world(3) - params.g - ba; % 预测 h_pred = h_est + v_est*dt + 0.5*acc_vert*dt^2; v_pred = v_est + acc_vert*dt; % 气压计更新 if ~isnan(baro_h) K_h = params.K_h; h_est = h_pred + K_h * (baro_h - h_pred); v_est = v_pred + params.K_v * (baro_h - h_pred); else h_est = h_pred; v_est = v_pred; end end

这里的$K_h$和$K_v$是稳态卡尔曼增益,可以通过离线计算得到,在线只做简单的代数运算,计算量极小。这种简化在工程上很常见,因为高度滤波器的动态特性变化不大,没必要每次迭代都做完整的矩阵运算。

5. 参数调试与实测数据分析

5.1 过程噪声与观测噪声的调参方法论

卡尔曼滤波的调参本质上是调节对模型和观测的信任度。我的调参流程是这样的:

第一步,离线标定传感器噪声。把无人机静止放在水平桌面上,采集5分钟数据。陀螺仪输出的标准差就是角度随机游走的噪声密度,加速度计输出的标准差就是观测噪声。磁力计要在远离金属和电流的环境下标定。

第二步,设置初始Q和R。Q的对角线元素根据陀螺仪噪声密度和零偏稳定性来设,R根据加速度计和磁力计的噪声标准差来设。初始值不需要很精确,因为后续还要调。

第三步,用实测飞行数据回放调试。把飞行日志导入Matlab,运行滤波器,对比滤波后的姿态角和参考值(如果有高精度IMU或者视觉动捕数据)。如果没有参考值,就看滤波后的曲线是否平滑、是否跟得上快速机动。

第四步,微调Q/R比例。如果姿态角在悬停时抖动大,说明R太小或者Q太大,滤波器太信任观测;如果快速机动时姿态角滞后明显,说明Q太小或者R太大,滤波器太信任模型。我一般以2倍为步长调整,找到临界点后再细化。

5.2 实测数据对比:纯陀螺仪积分 vs 卡尔曼滤波

我用同一段飞行数据做了对比。纯陀螺仪积分在30秒后横滚角漂了将近8度,俯仰角漂了5度,偏航角漂了12度。卡尔曼滤波后的姿态角在整个5分钟飞行中,横滚和俯仰的漂移控制在1度以内,偏航角因为磁力计受干扰,漂移在3度左右。

指标纯陀螺仪积分卡尔曼滤波改善幅度
横滚角30秒漂移8.2度0.6度92.7%
俯仰角30秒漂移5.1度0.4度92.2%
偏航角30秒漂移12.3度2.8度77.2%
快速机动跟踪延迟无延迟约15ms-
悬停时角度抖动标准差0.3度0.8度-

注意悬停时卡尔曼滤波的抖动反而比纯陀螺仪大,这是正常的,因为滤波器引入了加速度计和磁力计的观测噪声。但这个抖动在飞控里可以通过控制器的低通滤波进一步抑制,不影响飞行性能。

5.3 磁力计干扰下的偏航角处理策略

偏航角是9轴融合中最难做好的。磁力计容易受电机电流、金属结构、甚至手机磁铁的影响。我的策略是:

第一,磁力计数据有效性检测。计算磁力计输出的模长,如果偏离标定值超过20%,认为数据不可信,跳过更新步骤。同时检测磁力计的变化率,如果突变超过阈值,也跳过。

第二,自适应观测噪声。当检测到磁力计干扰时,动态增大R_mag,让滤波器减少对磁力计的信任。干扰消失后再恢复。

第三,偏航角限幅。如果磁力计长时间不可用,偏航角会退化为陀螺仪积分,慢慢漂移。这时候可以设置一个漂移速率上限,比如每秒最多漂0.1度,防止偏航角突然跳变。

function [mag_valid, R_mag] = check_mag(mag, mag_ref, mag_prev) mag_norm = norm(mag); norm_error = abs(mag_norm - mag_ref) / mag_ref; rate = norm(mag - mag_prev); if norm_error > 0.2 || rate > 0.5 mag_valid = false; R_mag = 1e3; % 极大值,等效于忽略观测 else mag_valid = true; R_mag = 0.05 + norm_error * 0.5; % 自适应 end end

这套策略实测下来,在电机全速运转时偏航角仍然能保持稳定,不会出现突然跳变。

6. 常见问题排查与避坑经验实录

6.1 姿态角发散或跳变的排查思路

姿态角发散是最常见的问题,排查顺序如下:

先查四元数归一化。如果忘记归一化,四元数模会慢慢偏离1,旋转矩阵不再正交,姿态角计算就会出错。我建议每次更新后都强制归一化,并在代码里加断言检查。

再查陀螺仪零偏估计。如果零偏估计发散,说明Q中零偏部分设得太大,或者观测更新没有正确约束零偏。可以先把零偏固定为标定值,看姿态是否正常,再逐步放开。

然后查坐标系对齐。加速度计和磁力计的轴向如果和陀螺仪不一致,观测更新会把错误的姿态信息引入。用静止数据验证:水平放置时,加速度计应该输出$[0, 0, g]$;机头指北时,磁力计应该输出$[B_h, 0, B_v]$。

最后查时间戳同步。如果传感器数据的时间戳没有对齐,观测更新会用错时刻的数据,导致姿态跳变。Matlab里用datetime或者timeseries对象管理时间戳,避免手动计算。

6.2 高度估计漂移与气压计噪声处理

高度漂移主要来自气压计的温漂和气流干扰。我的处理方法是:

  • 起飞前采集30秒地面气压,取中位数作为参考值
  • 飞行中用一阶低通滤波平滑气压计输出,截止频率0.5Hz
  • 如果气压计高度和加速度计积分高度差异超过2米,认为气压计异常,暂时切换到纯惯性高度估计
  • 着陆后重新标定地面气压参考值

还有一个坑:桨叶旋转产生的气流会在机体周围形成低压区,导致气压计读数偏高。解决办法是把气压计用海绵包裹,或者放在机腹远离桨盘的位置。我实测海绵包裹能减少约60%的气流干扰。

6.3 Matlab代码运行效率优化技巧

Matlab的矩阵运算虽然方便,但在处理长时飞行数据时可能会慢。几个优化技巧:

  • 预分配数组。不要用x = [x, new]这种方式动态扩展数组,提前用zeros(N, 7)分配好。
  • 向量化循环。能用矩阵运算的地方不要用for循环。比如四元数乘法可以写成矩阵形式,一次处理所有时间步。
  • 避免重复计算。雅可比矩阵中的一些中间变量可以提前算好,不要在每次迭代里重复计算。
  • 使用single精度。如果不需要双精度,用single类型可以节省一半内存,速度也更快。

我处理一段5分钟、200Hz的飞行数据,优化前需要跑12秒,优化后降到3秒左右。

6.4 常见问题速查表

问题现象可能原因排查方法解决方案
姿态角缓慢漂移陀螺仪零偏未补偿静止时观察零偏估计是否收敛检查Q中零偏部分,增大观测更新权重
姿态角高频抖动R太小或Q太大对比静止和运动时的抖动幅度增大R或减小Q,调整比例
偏航角突然跳变磁力计受干扰检查磁力计模长和变化率启用有效性检测,自适应R_mag
高度估计发散气压计参考值错误对比气压计和加速度计积分高度重新标定地面参考值,加低通滤波
快速机动时姿态滞后Q太小观察机动时残差是否持续偏大增大Q,让滤波器更信任陀螺仪
四元数模偏离1未归一化或数值误差检查四元数模长每次更新后强制归一化
滤波器运行慢矩阵运算未优化用Profiler分析耗时预分配、向量化、降精度

7. 从Matlab到飞控移植的几点经验

Matlab验证完算法后,最终要移植到飞控上。移植过程中有几个坑:

第一,浮点精度。Matlab默认双精度,飞控上通常用单精度。单精度下,四元数归一化的频率要更高,否则模偏离更快。我建议每100次迭代强制归一化一次。

第二,三角函数计算。精确离散化需要计算sin和cos,在MCU上可以用查表法或者CORDIC算法。如果飞控主频够高,直接用arm_sin_f32和arm_cos_f32也行。

第三,矩阵求逆。卡尔曼滤波中的$S^{-1}$在Matlab里一行代码搞定,在MCU上需要用Cholesky分解或者LU分解手动实现。对于7维状态向量,S是3x3或6x6的矩阵,求逆计算量可以接受。

第四,实时性。200Hz的姿态更新意味着每5毫秒要完成一次预测和更新。在STM32F405上,优化后的EKF单次迭代大约需要80微秒,完全够用。但如果用F103,可能需要降到100Hz或者简化观测模型。

我个人的体会是,Matlab阶段把算法验证透,移植时主要就是解决数值精度和计算效率的问题,算法逻辑本身不需要大改。最怕的是Matlab里就没调好,移植到飞控上问题被实时性掩盖,更难排查。

最后分享一个小技巧:在Matlab里加一个“仿真模式”,用生成的传感器数据(已知真实姿态)验证滤波器,这样可以定量评估滤波精度,比用实测数据盲调高效得多。生成数据时加入和实际传感器一致的噪声特性,包括高斯白噪声、零偏随机游走、以及偶发的野值。这套仿真框架搭好后,调参速度能快好几倍。

返回列表