2、坐标系与旋转表示:地理坐标系、载体坐标系、欧拉角、旋转矩阵、四元数基础

好,咱们正式开始聊姿态估计。在做卡尔曼滤波之前,有个坎儿必须迈过去——坐标系和旋转表示。说白了,你得先知道“我在哪”和“我朝哪”,才能谈“我接下来会怎样”。

我刚开始做惯导那会儿,就吃过坐标系的亏。一个旋转矩阵写反了,整个导航结果直接飞到了太平洋对面。嗯,从那以后,我对这部分内容就格外小心。

2.1 地理坐标系:我们到底在说哪个“北”?

地理坐标系,也叫导航坐标系。它是个固定在地球上的参考系。我们通常用 n 系 来表示。

  • X轴:指向东(East)
  • Y轴:指向北(North)
  • Z轴:指向天(Up),也就是垂直于地面向上

这就是经典的 ENU(东-北-天) 坐标系。你想想看,飞机平飞的时候,它的高度变化就是沿着Z轴在动。

个人习惯: 我建议初学者先把ENU坐标系刻在脑子里。因为后面所有的姿态角、旋转矩阵,都是相对于这个“大地基准”来定义的。搞混了方向,后面全白搭。

2.2 载体坐标系:传感器自己的“小世界”

载体坐标系,也叫 b 系。它固定在运动物体上,比如你的手机、无人机或者汽车。

  • X轴:指向载体的前方(Forward)
  • Y轴:指向载体的右侧(Right)
  • Z轴:指向载体的下方(Down)

这就是 FRD(前-右-下) 坐标系。注意,这里的Z轴是朝下的,和地理坐标系的天向正好相反。为什么会这样?因为航空航天的老传统,加速度计感受到的重力是向下的,所以Z轴朝下更符合物理直觉。

避坑指南: 我曾经在项目里把IMU的安装方向搞反了。IMU的Z轴朝上,但代码里按FRD处理。结果就是,静止时测出来的重力加速度是正的,但姿态解算出来却是倒立的。折腾了两天才发现是坐标系定义没对齐。

2.3 欧拉角:俯仰、横滚、偏航

欧拉角是描述“载体坐标系”相对于“地理坐标系”旋转的最直观方式。说白了,就是三个角度:

角度 英文 绕哪个轴转 范围
俯仰角 Pitch (θ) 绕Y轴(右) -90° ~ +90°
横滚角 Roll (φ) 绕X轴(前) -180° ~ +180°
偏航角 Yaw (ψ) 绕Z轴(下) 0° ~ 360°

你想想看,飞机抬头就是俯仰角为正,侧身转弯就是横滚角,机头指向就是偏航角。非常直观。

但是,欧拉角有个著名的毛病——万向锁。当俯仰角接近 ±90° 时,横滚和偏航的旋转轴会重合,导致丢失一个自由度。我记得在调试一个四轴飞行器时,飞控突然抽风,就是因为俯仰角太大触发了万向锁。从那以后,我只要做全姿态估计,就坚决不用欧拉角做内部运算。

2.4 旋转矩阵:数学上的“硬核”表达

旋转矩阵 C 是一个 3x3 的矩阵。它能把一个向量从载体坐标系变换到地理坐标系,或者反过来。

比如,载体坐标系下的一个向量 vb,要转换到地理坐标系 vn,公式就是:

v^n = C_b^n * v^b

其中 Cbn 就是从b系到n系的旋转矩阵。它由三个基本旋转矩阵相乘得到:

C_b^n = C_z(ψ) * C_y(θ) * C_x(φ)

这里 C_z(ψ) 是绕Z轴转偏航角,C_y(θ) 是绕Y轴转俯仰角,C_x(φ) 是绕X轴转横滚角。注意顺序,先转横滚,再转俯仰,最后转偏航。

核心要点: 旋转矩阵是正交矩阵,它的逆等于它的转置。这意味着 C_n^b = (C_b^n)^T。这个性质在卡尔曼滤波的观测更新里非常有用,可以省掉一次矩阵求逆的运算。

2.5 四元数基础:为什么它比欧拉角更香?

四元数,听起来很玄乎,其实就是一个四维的复数扩展。形式是:

q = q0 + q1*i + q2*j + q3*k

其中 q0 是实部,q1, q2, q3 是虚部。它满足 i² = j² = k² = i*j*k = -1

为什么我们要用四元数?三个理由:

  1. 无万向锁:这是最大的优势。无论你怎么转,四元数都能平滑表示。
  2. 计算量小:旋转矩阵有9个元素,四元数只有4个。在嵌入式系统里,少算几个乘法,CPU就少发热一点。
  3. 易于插值:在两个姿态之间做平滑过渡,四元数的球面线性插值(SLERP)非常优雅。

四元数表示旋转的公式也很简洁。一个向量 v 绕单位轴 u 旋转 θ 角度,对应的四元数是:

q = [cos(θ/2), u*sin(θ/2)]

然后旋转后的向量 v' 就是:

v' = q * v * q_conjugate

这里 q_conjugate 是四元数的共轭,就是把虚部取反。嗯,这个乘法在代码里实现起来,其实就是几个乘法和加法,非常高效。

我的建议: 在卡尔曼滤波的状态向量里,我通常用四元数来表示姿态。虽然它需要满足单位模长的约束(q0²+q1²+q2²+q3²=1),但处理起来比欧拉角省心多了。你只需要在每次更新后做一次归一化就行。

2.6 它们之间的转换:实战必备

在实际项目中,你经常需要在欧拉角、旋转矩阵和四元数之间来回切换。比如,传感器输出的是欧拉角,但卡尔曼滤波的状态量是四元数。

下面是一个简单的转换示例,从四元数到欧拉角:

// 已知四元数 q = [q0, q1, q2, q3]
// 计算俯仰角 pitch
float sin_pitch = 2.0f * (q0 * q2 - q3 * q1);
float pitch = asinf(sin_pitch);  // 注意范围限制

// 计算横滚角 roll
float sin_roll_cos_pitch = 2.0f * (q0 * q1 + q2 * q3);
float cos_roll_cos_pitch = 1.0f - 2.0f * (q1 * q1 + q2 * q2);
float roll = atan2f(sin_roll_cos_pitch, cos_roll_cos_pitch);

// 计算偏航角 yaw
float sin_yaw_cos_pitch = 2.0f * (q0 * q3 + q1 * q2);
float cos_yaw_cos_pitch = 1.0f - 2.0f * (q2 * q2 + q3 * q3);
float yaw = atan2f(sin_yaw_cos_pitch, cos_yaw_cos_pitch);

这段代码我用了很多年。注意 asin 函数,当俯仰角接近 ±90° 时,输入值可能略超 [-1, 1] 范围,记得做钳位处理。

曾经踩过的坑: 我在一个无人机项目里,直接用 atan2 算偏航角,结果发现偏航角在 0° 和 360° 附近跳变。后来才意识到,四元数到欧拉角的转换,偏航角的结果范围是 [-π, π],而我们需要 [0, 2π]。加个判断就能解决:if (yaw < 0) yaw += 2*M_PI;

好了,这一章的内容就到这里。坐标系和旋转表示是姿态估计的基石。你想想看,如果连“北”都指错了,后面的卡尔曼滤波再漂亮也是白搭。下一章,我们会把这些旋转表示用到卡尔曼滤波的状态方程里,真正开始做姿态解算。