简介:面向无人机、机器人及嵌入式系统开发者的磁力计姿态估计 MATLAB 工程包,聚焦基于磁力计数据求解姿态角的完整流程,涵盖欧拉角与四元数两种描述方式,并引入卡尔曼滤波等融合思路以提升偏航角估计精度。压缩包共 53 个文件,约 2.91MB,其中包含 26 个 mat 数据文件用于仿真输入,14 个 m 脚本实现主程序、状态方程、量测矩阵与误差补偿,另有 fig/bmp 姿态对比图便于直观验证结果。目前已有 235 人学习,适合正在研究姿态解算算法或需要 MATLAB 参考实现的本科生与工程师。通过这份资源可掌握磁力计硬铁/软铁校正、欧拉角与四元数相互转换、扩展卡尔曼滤波设计等关键环节,并借助自带数据快速运行出姿态轨迹与对比曲线,为后续多传感器融合开发提供可复用的代码基础。
1. 磁力计数据与姿态角估计:从传感器原始值到可用姿态的第一步
拿到一个名为 "magnetometer.zip_attitude euler_magnetometer matlab_四元数 姿态角_姿态角估" 的压缩包,大概率是师兄师姐留下的传感器实测数据,或者是某次实验采集的原始磁力计日志。这类数据包在姿态解算相关的课题里非常常见:里面可能有一个或多个 CSV 文件,记录着磁力计的三轴输出,偶尔混着加速度计和陀螺仪的数据,但最显眼的往往是那个名为 magnetometer 的文件。目标也很明确:利用 MATLAB 把磁力计的原始读数转换成有物理意义的姿态角——横滚(roll)、俯仰(pitch)、偏航(yaw),或者以四元数形式输出。
磁力计在姿态解算中承担的角色很特殊:它不依赖外部参考系,直接测量地球磁场在载体坐标系下的分量,所以理论上可以独立给出偏航角。但很多初学者在第一步就栽了跟头——直接把磁力计的 XYZ 原始值扔进atan2里求角度,出来的结果要么剧烈跳动,要么和罗盘读数差了十万八千里。这不是算法错了,而是漏掉了磁力计数据处理的三个前置环节:单位统一(需要归一化的磁场强度而非原始 ADC 值)、椭球校准(消除硬磁和软磁干扰)、坐标系对齐(磁力计 Z 轴方向与加速度计必须一致)。这三个环节任何一个没做好,后续的四元数融合都无从谈起。
这篇文章不打算只贴一段quat2eul的调用就收工,而是按我自己处理这类数据包的完整思路来组织:先讲清楚磁力计在姿态解算中的数学模型,接着给出从原始 CSV 到可用数据的预处理流程,再落到具体的 MATLAB 实现——分别用互补滤波和扩展卡尔曼滤波(EKF)两种方式把磁力计数据融合成四元数,最后讨论偏航角修正的实用技巧和验证方法。整个过程适合两类读者:一是正在做惯性导航课设、手里恰好有一份传感器日志的本科生,二是刚开始接触 AHRS 但想避开磁力计标定坑的工程师。下面直接进入正题。
2. 姿态角估计的数学基础:欧拉角、旋转矩阵与四元数的内在联系
2.1 磁力计测量模型:从地球磁场到载体坐标系的投影关系
磁力计输出的本质是地球磁场向量在载体坐标系(通常以右-前-上或前-左-上为轴)下的投影。设地球磁场在导航坐标系(北-东-地,NED)中的分量为 ( \mathbf{m}^n = [m_N, m_E, m_D]^T ),载体坐标系下的测量值为 ( \mathbf{m}^b = [m_x, m_y, m_z]^T ),两者通过方向余弦矩阵(DCM,即旋转矩阵)联系:
[ \mathbf{m}^b = \mathbf{R}_n^b \cdot \mathbf{m}^n + \mathbf{b} + \boldsymbol{\epsilon} ]
其中 ( \mathbf{R}_n^b ) 是由姿态决定的 3x3 旋转矩阵,( \mathbf{b} ) 是硬磁偏差(来自载体本身的固定磁场,如扬声器磁铁、电机磁钢),( \boldsymbol{\epsilon} ) 是测量噪声。注意:这里没有考虑软磁效应,即外界磁场被载体上的铁磁材料扭曲后产生的随姿态变化的影响。完整的误差模型应该写作:
[ \mathbf{m}^b = \mathbf{S} \cdot \mathbf{R}_n^b \cdot \mathbf{m}^n + \mathbf{b} + \boldsymbol{\epsilon} ]
( \mathbf{S} ) 是一个对称矩阵,描述软磁干扰。工程上通常把 ( \mathbf{S} ) 和 ( \mathbf{b} ) 合并成一个仿射变换来处理,这就是椭球校准的数学基础——在空间中旋转载体采集足够多方向的磁场数据后,这些点会分布在一个椭球面上,而真实的地磁场模长是常数,所以校准的目标就是把椭球还原成球。
姿态角估计的任务,就是从 ( \mathbf{m}^b )(以及可能同时存在的加速度计输出 ( \mathbf{a}^b ) 和陀螺仪角速度 ( \boldsymbol{\omega}^b ))中反解出 ( \mathbf{R}_n^b ),进而分解出欧拉角。但欧拉角本身有一个绕不开的问题:万向节锁。当俯仰角接近 ±90° 时,横滚和偏航的旋转轴重合,此时二者不可区分,导致姿态解算出现奇异点。四元数没有这个问题,这就是为什么姿态解算的工程实现几乎全部采用四元数而非欧拉角。
2.1.1 为什么单独用磁力计无法得到横滚和俯仰
地球磁场在水平面内的分量方向是确定的(指向磁北),但它在垂直方向的分量大小随纬度变化,在赤道附近接近零,在极地附近等于总场强。如果你只用磁力计去反解姿态,横滚和俯仰的可观性会很差,因为磁场向量在这两个轴上的投影对姿态变化不敏感。更致命的是,磁场向量既可以朝上也可以朝下(南半球和北半球的 ( m_D ) 符号相反),这会导致二义性。所以磁力计在九轴姿态解算中只负责修正偏航角,横滚和俯仰必须由加速度计(提供重力参考)来约束。
2.2 MATLAB 中欧拉角与四元数的互相转换:quat2eul与eul2quat的正确用法
MATLAB 的 Aerospace Toolbox 提供了一组方向余弦矩阵(DCM)、四元数、欧拉角的转换函数,但很多人第一次用就踩了旋转顺序的坑。eul2quat(eul, sequence)的默认旋转顺序是ZYX,即先绕 Z 轴(偏航),再绕 Y 轴(俯仰),最后绕 X 轴(横滚)。这个顺序对应航空领域的常规约定,但如果你用的是自己写的旋转矩阵推导,顺序不匹配会导致姿态角全部错乱。
% 从欧拉角(度)转换为四元数 eul_deg = [30, -10, 45]; % [yaw, pitch, roll],单位度 eul_rad = deg2rad(eul_deg); quat = eul2quat(eul_rad, 'ZYX'); % 输出 [w, x, y, z] % 从四元数转回欧拉角,验证往返一致性 eul_back = quat2eul(quat, 'ZYX'); eul_back_deg = rad2deg(eul_back); disp(eul_back_deg); % 应接近 [30, -10, 45]这段代码的逻辑是先确定旋转顺序,再做转换。注意 MATLAB 的四元数分量顺序是[w, x, y, z],而很多论文和 C/C++ 库(比如 Eigen)使用[x, y, z, w]的顺序,如果你从外部文件读取四元数,必须先确认分量排列顺序再做转换,否则姿态会完全错乱。
2.2.1 旋转矩阵的构造:从四元数到 DCM 的 3x3 矩阵
当你要把姿态投影到外部坐标系,或者反过来将传感器测量值变换到导航坐标系时,四元数需要先转化为旋转矩阵。在 MATLAB 中可以用quat2rotm:
R = quat2rotm(quat); % 输入 Nx4 的四元数,输出 3x3xN 的旋转矩阵 % 单样本情形:R(:,:,1) 即为载体坐标系到导航坐标系的旋转矩阵如果你需要自己做这个变换而不是依赖工具箱,四元数 ( q = [w, x, y, z] )(归一化后)对应的旋转矩阵是:
[ R = \begin{bmatrix} 1-2(y^2+z^2) & 2(xy-wz) & 2(xz+wy) \ 2(xy+wz) & 1-2(x^2+z^2) & 2(yz-wx) \ 2(xz-wy) & 2(yz+wx) & 1-2(x^2+y^2) \end{bmatrix} ]
这个矩阵的构造在滤波器中非常关键,因为 EKF 的观测方程需要把预测的磁场向量投影到载体坐标系,就要用当前姿态构建的旋转矩阵去乘导航坐标系下的磁场参考值。我在写滤波器时习惯手动构造这个矩阵,而不是调用工具箱函数,因为前者便于向量化处理整个时间序列的批量数据。
2.3 姿态解算的核心问题:如何融合多传感器数据得到一致的四元数
磁力计、加速度计、陀螺仪三个传感器各有优缺点:陀螺仪短时间积分准确但会漂移,加速度计提供绝对的重力参考但容易受线性加速度干扰,磁力计提供绝对的航向参考但容易受环境磁场干扰。姿态解算的实质,就是在概率意义上把这三路观测融合起来,得到对真实姿态 ( q ) 的最优估计。这里的“最优”在工程上有两种主流实现方式:互补滤波用频域上的互补特性做加权融合,EKF 用贝叶斯更新做方差最小化融合。两者的核心思想都是:用陀螺仪做短期预测(因为角速度信噪比高),用加速度计和磁力计做长期修正(因为它们是绝对测量,不会漂移)。
互补滤波的灵感来自于一个简单的观察:陀螺仪的姿态输出在低频段误差大(因为积分漂移是低频项),而加速度计和磁力计的姿态输出在高频段误差大(因为振动和磁干扰是高频项)。所以在频域上做一个交叉滤波,低频段相信后者,高频段相信前者,二者互补覆盖整个频段。梯度下降法(Madgwick 滤波的数学内核)则把这个问题转化为一个优化问题:寻找一个四元数,使得“重力参考投影到载体坐标系的结果与实际加速度计读数之差”和“磁场参考投影到载体坐标系的结果与实际磁力计读数之差”同时最小化,然后以一定比例(基于滤波器增益)和陀螺仪的积分结果混合。两者的 MATLAB 实现思路我在第 4 章给出,这里先明确一个前提:无论用哪种算法,磁力计的校准质量直接决定偏航角的最终收敛精度。如果输入的是没有经过椭球校准的磁场数据,滤波器无论怎么调参,yaw 都会带着一个随载体姿态变化的系统性偏差。
3. 磁力计数据预处理:从压缩包中的原始 CSV 到可用的姿态解算输入
3.1 读取 magnetometer.zip:多种数据格式的定位与批量加载
拿到压缩包后,先解压并查看目录结构。常见的情况是里面有一个传感器采集工具生成的时间序列文件,格式可能是纯逗号分隔的三列(磁力计 XYZ),也可能是混合了时间戳、加速度计、陀螺仪的九列以上数据。用手动importdata读取时,经常因为表头行数不一致、分隔符混用(有些日志用 tab 有些用逗号)、或者缺失值标记不同而出错。我习惯先做一次统一的文件扫描和自动识别:
function data = load_sensor_zip(zip_path) % 解压到临时目录 unzip_dir = tempname; unzip(zip_path, unzip_dir); files = dir(fullfile(unzip_dir, '**', '*.*')); % 寻找 CSV 或 TXT 文件,尝试自动识别格式 data = []; for i = 1:length(files) if files(i).bytes > 0 [~, ~, ext] = fileparts(files(i).name); if strcmpi(ext, '.csv') || strcmpi(ext, '.txt') try tmp = readmatrix(fullfile(files(i).folder, files(i).name), 'NumHeaderLines', 0); disp(['识别到文件: ', files(i).name, ' 尺寸: ', num2str(size(tmp))]); data = tmp; break; catch % readmatrix 失败时尝试 readtable t = readtable(fullfile(files(i).folder, files(i).name)); data = table2array(t); end end end end % 清理临时目录 rmdir(unzip_dir, 's'); endreadmatrix的NumHeaderLines参数用于跳过表头,但很多采集工具生成的日志会在表头之后还有一行单位说明,所以读取后必须检查前几行的数值范围是否合理。我见过最离谱的情况是前 100 行全是设备自检输出的字符串,直接导致解析失败或数值异常。加载完成后,先看数据的列数和范围再决定怎么切片。
3.1.1 时间戳对齐:判断传感器数据是同步采集还是各自独立时间轴
如果是无人机或机器人平台采集的数据,三个传感器往往各有独立的时间戳,需要进行插值对齐才能做融合。检查数据的第一列和最后一列时间戳,如果时间轴不均匀,用interp1统一重采样到固定频率:
% 假设 data 是 [t, mag_x, mag_y, mag_z] 格式 t_raw = data(:,1); fs = 100; % 设定重采样频率,根据实际采集率调整 t_uniform = (t_raw(1):1/fs:t_raw(end))'; mag_uniform = interp1(t_raw, data(:,2:4), t_uniform, 'linear');interp1的默认插值方式是 linear,对传感器数据足够。如果三个传感器的数据存储在不同文件里,先把各自的时间轴映射到统一网格,否则滤波器的观测更新会引入时间偏差,导致姿态角出现高频抖动。
3.2 椭球校准:消除硬磁和软磁干扰的完整 MATLAB 实现
磁力计校准在整个姿态解算流程里是最容易跳过但影响最大的环节。硬磁偏差(由载体上的固定磁场产生)会使磁力计输出整体偏移,表现为空间中的测量点不再分布在一个以原点为中心的球面上,而是一个圆心偏移的球;软磁偏差(由铁磁材料对磁场线的扭曲产生)则会把球面扯成椭球面。所以校准分两步:先估计偏移量和缩放比例,然后把原始测量值变换为真实的磁场向量。
工程上最常用的校准方法是利用地磁场模长恒定的特性:让设备在空间中旋转,采集足够多的磁场样本,所有样本的模长应该相等(等于当地地磁场强度)。这一步在 MATLAB 中实现为最小二乘椭球拟合:
% mag_samples: Nx3 矩阵,每行是 [x, y, z] 原始磁力计读数 function [A, b, ellip_params] = ellipsoid_fit(mag_samples) % 构造线性系统: x^2+y^2+z^2 = [x,y,z,1] * [2bx, 2by, 2bz, c] % 最小二乘求解,然后重建椭球参数 N = size(mag_samples, 1); D = [mag_samples.^2, mag_samples, ones(N,1)]; d = mag_samples(:,1).^2 + mag_samples(:,2).^2 + mag_samples(:,3).^2; % 用带约束的最小二乘保证椭球正定性 % 简化做法:直接用 backslash 求最小二乘解 theta = D \ d; % 重建椭球参数 offset = theta(4:6)' / 2; % 构造对称矩阵 E = [theta(1), theta(7)/2, theta(8)/2; theta(7)/2, theta(2), theta(9)/2; theta(8)/2, theta(9)/2, theta(3)]; [V, D_eig] = eig(E); % 缩放变换矩阵 scale = inv(sqrtm(abs(D_eig))) * V'; b = -scale * offset'; A = scale \ eye(3); ellip_params = struct('offset', offset, 'A', A, 'scale', scale); end拟合完成后,校准公式为:
[ \mathbf{m}{cal} = \mathbf{A}^T (\mathbf{m}{raw} - \mathbf{b}) ]
其中 ( \mathbf{b} ) 是偏移向量,( \mathbf{A} ) 是 3x3 的校准变换矩阵。用校准后的数据画三维散点图,如果所有点都落在一个球面上,模长基本恒定,说明校准有效。注意这里没有做温度补偿,如果设备工作环境温差很大,磁力计的灵敏度和零偏都会随温度漂移,这就超出了椭球校准的适用范围。
3.2.1 磁场参考向量的设定:为什么需要知道当地的磁场倾角
椭球校准只能保证磁场向量的模长正确,不能保证方向正确。在姿态解算中,滤波器需要一个导航坐标系下的磁场参考向量 ( \mathbf{m}^n )。这个向量的方向取决于当地的地磁倾角(磁场向量与水平面的夹角)和磁偏角(磁北与地理北的夹角)。如果不知道倾角,常见的做法是假设磁场完全沿水平方向(倾角为 0),但这会导致偏航角的精度在极地或高纬度地区严重恶化。实际上,倾角可以通过一个简单的实验估算:将设备水平放置,此时磁力计的 Z 轴分量与水平面不平行,利用已校准的数据和加速度计估计的姿态即可反推倾角。这个步骤在标准的 AHRS 初始化流程中称为“磁力计倾斜补偿”或“磁场向量归一化”。
3.3 重力参考与坐标系统一:确保加速度计和磁力计使用同一右手坐标系
这是一个很容易被忽略但极其关键的问题。不同传感器模块的坐标轴定义可能完全不同:有些设备采用右手坐标系(X 右,Y 前,Z 上),有些采用左手坐标系(X 前,Y 左,Z 上),还有的传感器数据手册里轴的指向是反的。如果加速度计和磁力计的坐标系不一致,融合结果会出现横滚和俯仰的严重耦合,表现为“倾斜设备时偏航角跟着变”。
验证坐标系是否一致的最简单方法:把设备平放在桌上,记录此时两个传感器的输出。加速度计应该近似输出 [0, 0, 9.8] 或 [0, 9.8, 0](取决于 Z 轴是向上还是向下),磁力计应该输出一个模长为当地磁场强度的向量。然后把设备绕 Z 轴旋转 90°,如果磁力计的 X 轴和 Y 轴读数变化符合右手定则(X 变到 Y 的位置),说明坐标轴顺序正确。另一个验证方法是在同一姿态下比较旋转矩阵对两个传感器的投影结果:
% 假设从加速度计得到横滚和俯仰,构建旋转矩阵 pitch = atan2(-acc_x, sqrt(acc_y^2 + acc_z^2)); roll = atan2(acc_y, acc_z); % 用旋转矩阵把磁力计读数变换到水平面 R = eul2rotm([0, pitch, roll]); mag_horiz = R * mag_cal'; % 投影后的水平分量 yaw = atan2(-mag_horiz(2), mag_horiz(1)); % 正常时 yaw 应该在设备旋转时变化,横滚俯仰变化时 yaw 相对稳定如果 yaw 在滚动和俯仰时明显跳变,先检查坐标系的一致性,而不是急着调滤波算法。这一步排查能省去后续大量调试时间。
4. MATLAB 实现姿态解算:互补滤波与扩展卡尔曼滤波的完整代码
4.1 互补滤波算法:增益参数的意义与 MATLAB 向量化实现
互补滤波的核心是一个比例调节环:把加速度计和磁力计融合得到的姿态与陀螺仪积分的姿态之间的误差,以比例增益 ( K_p ) 反馈到陀螺仪上。简化的微分方程形式为:
[ \dot{q} = \frac{1}{2} q \otimes \omega - K_p \cdot \text{error}(q_{est}, q_{accel_mag}) ]
其中 ( \otimes ) 表示四元数乘法,( \text{error} ) 是当前估计姿态与传感器参考姿态之间的差值。这个增益 ( K_p ) 的选取决定了滤波器的带宽:越大,传感器修正越快,但对振动和磁干扰越敏感;越小,陀螺仪积分的主导性越强,姿态越平滑但漂移越大。典型的初始值在 0.1 到 1.0 之间,需要根据实际数据调整。
下面给出一个完整的互补滤波实现,输入是陀螺仪角速度(rad/s)、加速度计和校准后的磁力计数据,输出是逐时刻的四元数序列:
function quat_series = complementary_filter(gyro_data, acc_data, mag_data, dt, Kp) % gyro_data: Nx3, 角速度(fixed frame), 单位 rad/s % acc_data: Nx3, 加速度计输出, 单位 m/s^2 (归一化前) % mag_data: Nx3, 校准后的磁力计输出 % dt: 采样间隔(秒) % Kp: 互补滤波比例增益 N = size(gyro_data, 1); quat_series = zeros(N, 4); q = [1, 0, 0, 0]; % 初始四元数,假设初始姿态为水平向北 for i = 1:N % 1. 陀螺仪积分 omega = gyro_data(i,:); dq = 0.5 * quat_multiply(q, [0, omega]); q_int = q + dq * dt; q_int = q_int / norm(q_int); % 2. 从加速度计估计横滚和俯仰 acc = acc_data(i,:) / norm(acc_data(i,:)); pitch = atan2(-acc(1), sqrt(acc(2)^2 + acc(3)^2)); roll = atan2(acc(2), acc(3)); % 3. 从磁力计估计偏航(带倾斜补偿) mag = mag_data(i,:); % 用当前姿态把磁力计变换到水平面 R = quat2rotm(q_int); mag_horiz = R * mag'; yaw = atan2(-mag_horiz(2), mag_horiz(1)); % 4. 构造参考四元数,计算误差 q_ref = eul2quat([yaw, pitch, roll], 'ZYX'); q_error = quat_multiply(q_ref, quat_conjugate(q_int)); % 5. 用误差修正积分结果 q_corr = q_int + Kp * dt * [0, q_error(2:4)]; % 简化的修正项 q = q_corr / norm(q_corr); quat_series(i, :) = q; end end % 四元数乘法(Hamilton 积) function q_out = quat_multiply(q1, q2) w1 = q1(1); v1 = q1(2:4); w2 = q2(1); v2 = q2(2:4); q_out = [w1*w2 - dot(v1,v2), w1*v2 + w2*v1 + cross(v1,v2)]; end function q_conj = quat_conjugate(q) q_conj = [q(1), -q(2:4)]; end参数说明:代码里的dt必须与实际采样间隔一致,如果数据不是均匀采样,需要先做重采样。Kp的单位是 1/s,含义是误差修正的速率。射频数据(比如 f=100Hz,dt=0.01)配合 Kp=0.5,收敛时间常数在 2 秒左右——如果你想看到更快的响应可以把 Kp 加大到 2.0,但姿态会不再平滑。步骤 3 中mag_horiz的计算用到了quat2rotm,这一步正是整个算法里最消耗算力的部分,在 MATLAB 里可以用预先构建好的旋转矩阵批量计算来加速。
4.1.1 互补滤波的收敛性与参数调试:观察 Kp 对 yaw 收敛速度的影响
调试 Kp 的最直观方法是画出手持设备静止时偏航角的收敛曲线。如果初始偏航角设成了 0,但真实偏航是 90°,互补滤波应该在大约 5-10 倍于时间常数的周期内收敛到正确值。时间常数 ( \tau = 1/K_p ),Kp=0.5 对应 2 秒的时间常数,大约 10 秒内应完成收敛。如果收敛太慢,增大 Kp;如果收敛后有明显的周期性波动(振动干扰导致的),减小 Kp。另外注意,互补滤波在一自由度的解析解上可以通过拉普拉斯变换严格分析,但在三轴耦合的非线性情形下,Kp 的整定基本还是靠经验扫描。
4.2 扩展卡尔曼滤波:状态向量、观测模型与雅可比矩阵推导
EKF 的处理方式比互补滤波更“正式”:把姿态四元数和传感器零偏组成状态向量,用陀螺仪的角速度作为控制输入做状态预测,用加速度计和磁力计的测量值作为观测做修正。这里的关键是状态向量选择四元数时,协方差矩阵的更新必须保证四元数的归一化约束不被破坏。
状态向量取 7 维:( \mathbf{x} = [q_w, q_x, q_y, q_z, b_x, b_y, b_z]^T ),其中 ( b ) 是陀螺仪零偏。状态转移方程为四元数积分加上零偏随机游走:
[ \dot{\mathbf{q}} = \frac{1}{2} \mathbf{q} \otimes (\boldsymbol{\omega}_{measured} - \mathbf{b}) ]
观测方程有两个:加速度计观测模型把重力参考向量 ( \mathbf{g}^n = [0,0,g] ) 投影到载体坐标系:
[ \mathbf{z}_{acc} = \mathbf{R}n^b \mathbf{g}^n + \mathbf{v}{acc} ]
磁力计观测模型把磁场参考向量 ( \mathbf{m}^n ) 投影到载体坐标系:
[ \mathbf{z}_{mag} = \mathbf{R}n^b \mathbf{m}^n + \mathbf{v}{mag} ]
在 MATLAB 中实现 EKF 时,雅可比矩阵的手动推导容易出错,但可以利用 Symbolic Math Toolbox 验证数值计算。下面给出一个不依赖工具箱手写雅可比的简化版本:
function [x_est, P] = ekf_attitude_update(x_pred, P_pred, z_acc, z_mag, R_acc, R_mag, g_n, m_n) % x_pred: 预测状态 (7x1) % P_pred: 预测协方差 (7x7) % z_acc, z_mag: 当前时刻的加速度计和磁力计观测 (3x1) % R_acc, R_mag: 观测噪声协方差 (3x3) % g_n: 导航坐标系重力向量 [0;0;9.8] % m_n: 导航坐标系磁场参考向量 (3x1) q = x_pred(1:4); b = x_pred(5:7); R_nb = quat2rotm(q'); % 载体到导航的旋转矩阵 % 预测观测值(加速度计和磁力计) z_acc_pred = R_nb' * g_n; % 导航到载体的投影 z_mag_pred = R_nb' * m_n; % 观测残差 y_acc = z_acc - z_acc_pred; y_mag = z_mag - z_mag_pred; y = [y_acc; y_mag]; % 观测矩阵 H 的数值计算(通过扰动法) H = zeros(6, 7); eps_ = 1e-6; for i = 1:7 x_pert = x_pred; x_pert(i) = x_pert(i) + eps_; q_pert = x_pert(1:4); R_pert = quat2rotm(q_pert'); H(1:3, i) = (R_pert' * g_n - z_acc_pred) / eps_; H(4:6, i) = (R_pert' * m_n - z_mag_pred) / eps_; end R_total = blkdiag(R_acc, R_mag); % 卡尔曼增益 S = H * P_pred * H' + R_total; K = P_pred * H' / S; % 状态更新 x_est = x_pred + K * y; % 四元数归一化 x_est(1:4) = x_est(1:4) / norm(x_est(1:4)); % 协方差更新 P = (eye(7) - K * H) * P_pred; end这段代码用了数值扰动法计算观测矩阵 ( H ),避免了手推复杂四元数求导,代价是计算速度较慢。如果处理的是离线数据(大多数课设场景),这个速度完全可以接受。
4.2.1 EKF 噪声矩阵的设定与敏感性分析
EKF 的调参没有银弹,但有几个可循的经验法则。过程噪声协方差 ( Q ) 控制陀螺仪积分的信任程度:设置太小会导致滤波器过于相信陀螺仪,姿态会漂移;设置太大会导致姿态跟随传感器噪声,跳动明显。我会先用一段静止数据(设备完全静止)做测试,确保滤波后的姿态角稳定在初始值附近,此时如果姿态漂移超过 1°/分钟,说明 Q 设置过大。观测噪声协方差 ( R_{acc} ) 和 ( R_{mag} ) 从传感器数据手册的噪声谱密度推算,但更实用的做法是记录一段静止数据的统计方差,直接作为观测噪声的初始估计:
% 取一段静止数据估计观测噪声 acc_norm = sqrt(sum(acc_data.^2, 2)); R_acc = var(acc_norm) * eye(3); % 若 acc_norm 波动大,R_acc 应调大 % 对磁力计,先经过椭球校准后再估计噪声 mag_cal_norm = sqrt(sum(mag_cal.^2, 2)); R_mag = var(mag_cal_norm) * eye(3);这个启发式方法在有振动环境下会高估噪声,但作为初始值是合理的。
4.3 Madgwick 梯度下降法:无需矩阵求逆的轻量级融合方案
Madgwick 滤波在嵌入式领域很流行,因为它的计算量比 EKF 小很多,而精度在大多数场景下接近。它的核心是用梯度下降法在每一步寻找一个四元数增量,把重力参考和磁场参考的投影误差最小化,再与陀螺仪积分结果做加权平均。MATLAB 实现如下:
function quat_series = madgwick_filter(gyro_data, acc_data, mag_data, dt, beta) % beta: 梯度下降步长,典型值 0.01 ~ 0.5 % 数值越大,对传感器修正的响应越快 N = size(gyro_data, 1); quat_series = zeros(N, 4); q = [1, 0, 0, 0]; for i = 1:N acc = acc_data(i,:) / norm(acc_data(i,:)); mag = mag_data(i,:) / norm(mag_data(i,:)); % 梯度下降步:最小化目标函数 % 目标函数 f = [acc_est - acc_ref; mag_est - mag_ref] % 简化后的梯度方向(已在论文中推导) J = compute_jacobian(q, mag); f = objective_function(q, acc, mag); grad = J' * f; q_grad = -beta * grad / norm(grad); % 陀螺仪积分 omega = gyro_data(i,:); q_gyro = 0.5 * quat_multiply(q, [0, omega]); % 加权融合 q_dot = q_gyro - q_grad; q = q + q_dot * dt; q = q / norm(q); quat_series(i,:) = q; end end参数beta代替了互补滤波中的Kp,控制的是梯度下降的步长。Madgwick 的原论文建议 beta 的初始值可以取 0.041(对应约 0.3 的 Kp),但我实际用下来在强振动环境下需要把 beta 下限调到 0.01 以下,否则横滚和俯仰会被加速度计的线性加速度分量带偏。注意:这里的compute_jacobian和objective_function需要根据四元数和磁场参考向量展开,完整推导有十几行矩阵运算,论文里给出了闭式表达式,工程上可以直接抄。
5. 偏航角修正与数据验证:用磁力计锁定 yaw 的可靠技巧
5.1 为什么磁力计修正偏航时横滚和俯仰会串扰
这是处理磁力计时最常见的工程陷阱。理论上磁力计只影响 yaw,但在实际滤波器中,如果磁力计数据中含有未被校准掉的姿态相关误差(比如软磁残差),那么在 EKF 或互补滤波的观测更新中,磁力计的残差会同时贡献到横滚和俯仰的修正项里。具体表现为:设备倾斜时(pitch 不为 0),yaw 读数会跟着变化 10°-20°。
解决串扰有两个层面。第一是严格的椭球校准,残差越小越好;第二是在滤波器中采用分步骤的修正策略,先单独用加速度计修正横滚俯仰,再用磁力计修正偏航,两个步骤分开做卡尔曼更新。后者在工程上被称为“两步校正法”,实现起来只需要在 EKF 的观测更新里拆成两个独立的步骤:
% 第一步:只用加速度计更新 [x, P] = ekf_attitude_update(x, P, z_acc, [], R_acc, [], g_n, []); % 第二步:只用磁力计更新(此时横滚俯仰已经固定) [x, P] = ekf_attitude_update(x, P, [], z_mag, [], R_mag, [], m_n);这样做的好处是磁力计的残差不会再通过观测矩阵的耦合项影响横滚和俯仰,因为它们在前一步已经被加速度计“钉死”了。代价是如果加速度计本身受到线性加速度干扰,横滚俯仰的错误会带动 yaw 一起错。
5.2 验证姿态解算结果的三种方法:静止测试、旋转对比、与参考值比对
5.2.1 静止测试:检查输出姿态角的方差和收敛性
将传感器水平放置并记录 60 秒数据,解算后的横滚和俯仰应当收敛到 0°(或设备实际的安装角),波动幅度在 ±1° 以内。偏航角应当保持恒定,波动幅度取决于磁干扰水平,一般 ±2° 以内可以接受。如果在静止时偏航出现缓慢漂移,检查是不是有附近的金属物体或电流产生的磁场在干扰,排除环境因素后再调参。
5.2.2 旋转对比:绕单独轴转动验证解耦性
把设备绕 Y 轴(俯仰轴)旋转 ±30°,观察解算结果中的横滚和偏航是否基本不变。这个测试能暴露轴间耦合问题。如果横滚跟着俯仰变化,几乎可以肯定是坐标系未对齐或者磁力计残差过大。在 MATLAB 中做这个测试时,可以先将设备固定的角度序列搞清楚,再对比解算值。
% 手动旋转测试:绕 Y 轴旋转 0° -> 30° -> 0° -> -30° -> 0° % 解算完成后检查 yaw 是否稳定在初始值附近 yaw_deviation = max(abs(yaw_estimate - yaw_initial)); % 若 yaw_deviation > 5°,说明存在明显的轴间耦合,检查磁力计校准5.2.3 与惯性测量单元参考姿态比对
如果你手头有另一个经过标定的姿态参考系统(比如光学动捕系统的输出),可以把两者的欧拉角放在同一时间轴下画图对比。这个方法的准确性最高,但在大多数课设场景下不具备条件。退而求其次的做法是让设备做一组已知运动(先水平旋转 90°,再俯仰 30°),观察滤波器的输出是否与运动指令一致。
5.3 磁力计校准状态检查:在 MATLAB 中评估椭球拟合的残差
即使做了椭球校准,也需要量化评估校准的质量。计算校准后所有样本的模长,看它是否不再随姿态变化。具体指标可以用模长的标准差与均值之比来量化:
mag_cal_norm = sqrt(sum(mag_cal.^2, 2)); ratio = std(mag_cal_norm) / mean(mag_cal_norm); % 如果 ratio > 0.05,即模长波动超过 5%,说明校准不充分 fprintf('校准后磁场模长波动比: %.4f\n', ratio);如果比例偏高,可以检查采集的数据是否覆盖了足够多的姿态方向。椭球拟合需要至少 4 个方向的数据支撑,但实际工程上建议采集超过 200 个分布在各个方位的样本,并且采样时要缓慢旋转,避免磁力计带宽限制导致数据失真。另外要检查拟合出的椭球参数是否有明显的物理意义异常——比如偏移量应该不大(在设备本身的磁场强度量级),缩放矩阵应该接近单位矩阵(除非有严重的软磁干扰)。
6. 磁力计姿态解算的进阶验证:磁场模型修正与参考系统对比脚本
在处理磁力计数据时,还有一个容易被忽略的变量:磁偏角。MATLAB 的 Aerospace Toolbox 提供了magfield函数可以估算特定经纬度和日期下地球磁场的完整向量,包括磁偏角、磁倾角和总强度。如果你知道实验所在地的经纬度,可以用这个函数获得一个更精确的磁场参考向量,替代简单假设的水平磁场:
% 以北京(39.9N, 116.4E)为例,设定日期 2024 年 1 月 1 日 lat = 39.9; lon = 116.4; h = 50; % 海拔高度,单位米 date_datenum = datenum(2024, 1, 1); [mag_n, H, decl, incl, total] = magfield(lat, lon, h, date_datenum); % mag_n: 导航坐标系下的磁场向量 (北-东-下即 NED 分量) % decl: 磁偏角(单位度),decl = 0 时磁北与地理北重合 fprintf('磁偏角: %.2f°, 磁倾角: %.2f°\n', decl, incl);magfield使用世界地磁模型(WMM 或 IGRF),计算出的磁场向量可以作为 EKF 观测模型中的 ( \mathbf{m}^n ) 参考。如果你的实验地点不在北京,替换经纬度参数即可。这一步的意义在于:如果你直接把滤波器的 yaw 输出当成相对于地理北的方向,而实验地有 7° 的磁偏角(比如北京大约是 -7°),那么你的偏航角会系统性偏差 7°。用磁场模型修正后,把 yaw 加上磁偏角,就能得到真正的地理航向。
% 将滤波得到的偏航角(相对于磁北)转换为地理航向 yaw_true = yaw_filtered_deg + decl; % 注意 decl 的单位是度,东偏为正,西偏为负这个修正对室外导航场景至关重要。如果你只是做室内桌面级别的姿态演示,磁偏角的影响不大,但在无人机或移动机器人的室外航线规划里,7° 的偏差在飞行几十米后会转化为数米的横向误差,绝对不可忽略。此外,magfield返回的磁场总量total也可以作为椭球校准的模长参考,用来验证校准后的数据是否存在比例偏移(通常是灵敏度矩阵未对齐导致)。
验证姿态精度的另一个实用方法是“旋转后回正检查”:把设备绕某个轴旋转一定角度再回到初始位置,观察姿态是否回到初始值。如果产生了余差(回不到初始值),说明存在积分漂移或传感器偏差未被完全补偿。这个测试也常用来评估互补滤波和 EKF 在实际数据上的性能差异——EKF 因为有零偏在线估计,通常回正效果更好。你可以用下面这段脚本快速比较两种算法对同一份数据的处理结果:
% 对比两种算法的 yaw 输出 quat_cf = complementary_filter(gyro_data, acc_data, mag_cal, dt, 0.5); quat_ekf = ekf_full(gyro_data, acc_data, mag_cal, dt); % 自行封装 EKF 主循环 eul_cf = quat2eul(quat_cf, 'ZYX'); eul_ekf = quat2eul(quat_ekf, 'ZYX'); t = (0:length(gyro_data)-1) * dt; figure; subplot(2,1,1); plot(t, rad2deg(eul_cf(:,1))); hold on; plot(t, rad2deg(eul_ekf(:,1))); legend('互补滤波 Yaw', 'EKF Yaw'); xlabel('时间 (s)'); ylabel('偏航角 (deg)'); title('偏航角对比'); subplot(2,1,2); plot(t, rad2deg(eul_cf(:,2))); hold on; plot(t, rad2deg(eul_ekf(:,2))); legend('互补滤波 Pitch', 'EKF Pitch'); xlabel('时间 (s)'); ylabel('俯仰角 (deg)');这段脚本的对比逻辑很直观:把两条曲线放在同一张图上,观察它们是否吻合、是否有相对延迟、以及静态段是否都有漂移。通常 EKF 在动静态切换时会比互补滤波平滑,但两者的差距在磁力计噪声大的场景下会缩小,原因是磁力计的测量噪声主导了修正环节,算法层面的差异被传感器误差掩盖了。对于 "magnetometer.zip" 这份数据的具体结果,可能差异最明显的地方在于快速旋转后的回零能力:如果压缩包里的数据包含大幅机动(比如手持设备快速翻转),EKF 因为在线估计陀螺仪零偏,在运动结束后的姿态保持上应该优于互补滤波。你可以用脚本跑完对比后,把差异明显的时间段放大观察,多半是发生在快速旋转停止后的最初几秒——这一段正是陀螺仪零偏估计收敛的窗口期。
本文还有配套的精品资源,点击获取