四元数姿态解算:从数学原理到飞控工程实践
2026/9/9 21:26:59 网站建设 项目流程

我第一次接触四元数,是在调飞控姿态解算的时候。当时IMU库里直接给了一组(w, x, y, z),文档上就写着“表示姿态”,我以为把四个数转成欧拉角就能看清姿态了。结果一跑起来,俯仰角到90度附近时数据乱跳,我还以为是板子坏了,排查了半天才发现,是欧拉角表示本身在奇点附近退化。这个经历让我意识到,搞姿态处理的人如果不懂四元数,迟早要被奇点坑一回。后来我把四元数从数学规则到飞控里的实际用法完整捋了一遍,发现无人机、机器人、云台甚至AR设备里到处都在用它,尤其是姿态解算环节,绕不开。这篇就当一次复盘,讲讲四元数到底解决了什么问题、数学上怎么工作、实际工程里怎么用,给正在学IMU或做姿态估计的朋友一个接地气的参考。

1. 从姿态描述的痛点说起:为什么绕不开四元数

1.1 欧拉角很直观,但90度附近的坑很难受

描述物体在空间中的朝向,最直觉的办法是欧拉角:先绕一个轴转偏航角yaw,再绕另一个轴转俯仰角pitch,最后绕第三个轴转横滚角roll。姿态传感器、云台、机械臂控制里用这套非常普遍,因为你看到的就是角度,调试方便,脑子里能直接想象出来。

但欧拉角有个绕不过去的坑,叫万向锁。当pitch转到正负90度时,yaw和roll的旋转轴重合在一起,两个自由度塌缩成一个,姿态变得不可区分。想象你把手机竖直立起来,镜头朝上,这时你绕着屏幕法线转和绕着手机长边转,画面都在同一个轴上旋转,你就没法再分开控制这两个角了。这个现象不是传感器坏了,是数学表示本身的奇点。

对无人机这种可以做任意姿态飞行的系统来说,一旦出现这种状态,姿态控制器会完全迷失方向。我早期调云台时踩过这个坑:目标俯仰角超过85度以后,画面就开始诡异地扭。查遍代码和数据,发现运算本身没毛病,就是欧拉角在接近奇点时退化了,矩阵解算病态,输出不稳定。所以只要你的系统允许大角度姿态变化,欧拉角就只适合用来给人看,不适合作为内部运算的核心表示。

1.2 旋转矩阵没奇点,但运算成本不划算

欧拉角不行,直接用旋转矩阵呢?3x3旋转矩阵可以无奇异地描述任意姿态,不会出现万向锁,空间变换关系也十分清晰,在许多标定和坐标系变换里都是标准表达。

但它的代价也很直接:9个元素要存,乘法运算量几乎比四元数翻倍。更麻烦的是,旋转矩阵必须满足正交且行列式为1的约束。在循环更新过程中,浮点数值误差会慢慢让矩阵变得“不像旋转矩阵”,必须定期做施密特正交化修正回来。姿态更新通常跑在1kHz甚至更高频率,每步都做正交化,计算开销一下就上去了。

有人会说,嵌入式芯片性能也不差,省这点运算有意义吗?其实姿态解算只是飞控里的一小部分,后面还有控制律、传感器融合滤波、电调PWM更新。资源能省则省,而且省下来的不只是运算,还有代码复杂度和维护成本。四元数只有4个数字,更新一次只需要若干次乘加,然后做一次归一化就好,没有正交性约束的问题,光这一点就比旋转矩阵干净很多。

1.3 四元数一次解决两个问题:没有奇点,也更省计算

四元数是1843年哈密顿发明的,从数学上看是复数在三维空间的扩展,一个标量加上三个虚数单位。真正让它成为姿态解算主流的原因是:它能无奇异地表示空间中的任意旋转,四个参数比旋转矩阵紧凑,旋转复合用一次四元数乘法就能完成,还天然适合做旋转插值。

从几何上讲,任何姿态都可以理解为“绕某个轴的单一旋转”,四元数直接保存的就是这个轴的朝向和转过角度的一半。因为不用把姿态拆成三段固定轴的欧拉角,所以不存在“中间某个角度导致轴重合”的问题。在工程上,这意味着从-180度到180度随便转,甚至在空间里面打滚,姿态表示都不会崩。这就是四元数在姿态解算里不可替代的位置。

2. 四元数的数学内核:别看只是四个数,规则大有讲究

2.1 一个标量加一个三维向量:半角设计是理解关键

四元数一般写成q = w + xi + yj + zk,也可以看作(w, x, y, z)。w是标量部分,后面的(x, y, z)是向量部分。描述旋转时,把w理解成cos(θ/2),虚部向量理解成旋转轴单位向量乘以sin(θ/2),θ是绕该轴旋转的角度。

这里最关键的是半角设计。为什么不是直接存cosθ?因为如果直接存θ对应项,两次旋转复合时的公式会非常复杂;存半角后,旋转复合退化成一次简洁的四元数乘法。也就是说,使用四元数时你存的是一个旋转动作的“一半”,这个别扭的设计恰恰是四元数好用的原因。

在无人机飞控里,一个单位四元数就相当于一条“旋转指令”:旋转轴在哪、转多少度。比如绕Z轴转90度,对应的四元数就是(cos45°, 0, 0, sin45°)。你不需要去想象什么万向锁,也不需要记住一群矩阵乘法的顺序,只要盯着这个轴和角就够了。

2.2 乘法、共轭、模长:绕开这三个概念就绕不开四元数

四元数乘法(哈密顿积)定义的展开式很长,看着吓人,但工程上通常封装成函数后就没几个人再去手工展开它。比起记住公式,更重要的是记住两个性质。

第一,四元数乘法不满足交换律,q1⊗q2和q2⊗q1通常不一样。这对应三维旋转复合不交换的现象:先绕Z轴转90度再绕X轴转90度,和先绕X轴转90度再绕Z轴转90度,结果完全不同。代码里谁左谁右一定要按公式来,否则姿态会诡异到让你怀疑人生。

第二,单位四元数的乘法等价于旋转复合。一个单位四元数q1表示旋转R1,q2表示旋转R2,那么先做R2再做R1,整体的合成旋转可以用q1⊗q2表示。但网上不同资料里的符号约定不一致,有人用q2⊗q1,所以动手实现前一定要先确认自己代码里的旋转顺序约定,再写乘法顺序。

然后是共轭和模长。共轭就是虚部取反:q* = (w, -x, -y, -z)。模长|q|就是平方和开根号。单位四元数的逆等于它的共轭,这表示反方向旋转。想取消一次旋转,直接乘它的共轭就行,这个操作在修正算法里经常出现。

2.3 旋转向量的标准公式:v' = q ⊗ v ⊗ q*

要把一个三维向量v按四元数q旋转,先把它写成纯四元数pv = (0, vx, vy, vz),然后计算pv' = q ⊗ pv ⊗ q*,得到的纯四元数虚部就是旋转后的向量。

这个“两边都要乘”的结构可以理解为:左边q负责把向量转过去,右边q*负责把坐标系“换回来”,并保证模长不变。完整展开这个公式,其实就是四元数到旋转矩阵的36个系数,很多库函数里提供的quat_to_mat就是这么推导出来的。

学习阶段我建议用几个特殊用例验证公式和代码。比如q取单位四元数(1,0,0,0),旋转后向量应该完全不变;再比如q表示绕Z轴转90度,作用到X轴单位向量(1,0,0)上,结果应该是Y轴方向(0,1,0)。这三个用例能排查掉一大半符号错误和轴顺序问题,我在换传感器平台后都会先跑一遍这组验证。

这里也能解释为什么旋转向量公式能避开万向锁:它本质上只做“绕一个三维空间中的轴直接转一个角度”,而不是把旋转拆成三段固定轴旋转。欧拉角的问题在于它把一次旋转强制分解成三次固定轴的连续旋转,当中间某个角的奇点出现时,前后两个轴就重合了。四元数不搞这种分解,所以无论物体怎么翻,表示都不会退化。

2.4 插值平滑:动画和路径规划也用得上四元数

四元数还有一个欧拉角和旋转矩阵都不好替代的优势:旋转插值非常平滑。在3D动画、云台轨迹过渡、机械臂路径规划里,经常需要让姿态从A连续过渡到B。如果直接对旋转矩阵做线性插值,中间会出现缩放和不自然的扭曲;对欧拉角线性插值,在奇点附近更是会翻车。

四元数的球面线性插值slerp是标准做法:先计算两个四元数之间的夹角,再按插值参数t在球面上等比例取点。公式可以写成slerp(q1, q2, t) = q1 * sin((1-t)θ)/sinθ + q2 * sin(tθ)/sinθ,其中θ是两四元数夹角。实现时要注意,如果q1和q2的点积为负,说明它们表示同一旋转但符号相反,需要先把其中一个取负再做插值,否则会绕远路。姿态解算本身一般不直接做slerp,但云台平滑和动画混合里它是主力。

3. 四元数姿态解算:从IMU数据到空间姿态

3.1 先搞清楚传感器各自擅长什么

四元数姿态解算在飞控里的典型输入来自IMU,也就是惯性测量单元,里面通常集成了陀螺仪和加速度计,外接或内置磁力计。这三类传感器各有个性和短板,搞不清它们就调不好算法。

陀螺仪测量角速度,短时间内积分得到的角度非常准,响应也快,但长时间积分会漂移,因为零偏和噪声会不断累积。加速度计测量比力,在静止时能提供重力方向在机体坐标系的投影,从而推算俯仰和横滚,但它对运动加速度很敏感,无人机一加速,数据就会带上一大堆干扰。磁力计测量地磁场方向,能提供绝对航向,可它极其容易被电机、电调和金属结构干扰。

姿态解算要做的事就是把它们融合起来。我的理解是把陀螺仪当作短期姿态的“记忆者”,把加速度计和磁力计当作长期姿态的“修正者”。记忆者短期可靠、长期偏航,修正者长期靠谱、短期噪声大。算法设计的核心就是怎么加权、怎么反馈。

3.2 四元数微分方程和一阶积分更新

陀螺仪读到的机体角速度是ω=(ωx, ωy, ωz),在四元数表示里把它写成纯四元数ω_q = (0, ωx, ωy, ωz)。四元数对时间的导数满足dq/dt = 0.5 * q ⊗ ω_q,这里的乘法顺序取决于坐标系定义和旋转方向约定,实践中必须严格对齐,否则会发生旋转方向反转的诡异问题。

一阶龙格库塔更新公式很直接:q_new = q + dq/dt * dt,然后立刻归一化。下面是Python版本,方便你快速验证。

import numpy as np def quat_update(q, gyro, dt): # q: (w, x, y, z),gyro: (wx, wy, wz) 单位为 rad/s,dt 单位为 s w, x, y, z = q gx, gy, gz = gyro # dq/dt = 0.5 * q * (0, wx, wy, wz) dw = 0.5 * (-gx*x - gy*y - gz*z) dx = 0.5 * ( gx*w + gy*z - gz*y) dy = 0.5 * (-gx*z + gy*w + gz*x) dz = 0.5 * ( gx*y - gy*x + gz*w) qn = np.array([w + dw*dt, x + dx*dt, y + dy*dt, z + dz*dt]) return qn / np.linalg.norm(qn)

这段更新本身不难,难点在外面。第一,角速度符号和机体坐标轴的对应关系取决于IMU的安装方向和右手定则,传感器数据手册通常会给,但你最好自己做一个已知方向的旋转来验证。第二,dt必须跟实际采样间隔一致。我见过有人陀螺仪是1kHz采样,但循环里忘了换算时间,积分快了1000倍,姿态直接疯转。用固定模拟输入和理论值对比,很容易发现这种问题。

3.3 用加速度计修正漂移:Mahony算法怎么做

如果只用上面的纯积分,姿态很快会飘走,因为陀螺仪零偏和噪声一直在累积。经典的Mahony互补滤波算法思路清晰、计算量小、调参直观,是四元数姿态解算里非常实用的方案。

算法的核心思想是:根据当前四元数预测机体坐标系下的重力方向v,再与实际加速度计测量值a做叉积,得到误差向量e。这个叉积误差方向就是“当前姿态偏了多少”的信息,通过比例积分(PI)反馈补偿到陀螺仪角速度上,然后再做四元数积分更新。

预测重力的公式(符号约定需要严格对齐)为:

vx = 2 * (q1q3 - q0q2)
vy = 2 * (q0q1 + q2q3)
vz = q0q0 - q1q1 - q2q2 + q3q3

叉积误差:ex = ayvz - azvy,ey = azvx - axvz,ez = axvy - ayvx。

然后做PI修正:gx += Kpex + Kiex_int,gy += Kpey + Kiey_int,gz += Kpez + Kiez_int。其中ex_int、ey_int、ez_int是误差积分项,用来补偿陀螺零偏。Kp典型值在0.1到1.0之间,Ki建议先给0,观察静态误差收敛情况,再慢慢增加。Ki给太大会导致姿态振荡。

代码实现可以直接参考开源社区里常见的MahonyAHRS库,很多MCU工程里都有移植。真正需要花心思的不是抄代码,而是把坐标系对准:加速度计三个轴的方向、陀螺仪三个轴的方向、四元数的右手定则,这三者对不上,修正项就是错的。

3.4 磁力计修正:需要航向时再加

如果只需要俯仰和横滚,用加速度计修正就够了。需要航向yaw时,就得引入磁力计。磁力计测得的是地磁场在机体坐标系下的方向,先转换到水平面,再跟当前四元数预测的航向做差,用同样的PI结构去修正。

磁力计的坑在于它太容易受干扰了。无人机上电调一开启,磁场一变,yaw就开始飘,甚至出现姿态整体拧着走的现象。解决办法是做好磁力计校准,一般用椭球拟合把硬磁和软磁干扰消掉,同时在安装上尽量远离电机和粗大金属件。在修正增益上,磁力计要设置得比加速度计更保守,否则稍微有点干扰,航向就抖得没法用。

4. 实操落地:我在飞控里用四元数的几个关键环节

4.1 初始化:别从全零开始

四元数初始值必须是单位四元数。最稳妥的做法是用初始欧拉角或初始旋转矩阵去构造四元数,再归一化。比如上电时传感器水平静止,那么姿态就是0,对应(1,0,0,0)。但如果传感器安装有偏差,或者上电时就不是水平状态,就需要先用加速度计算出初始pitch和roll,再用磁力计算出初始yaw,最后用欧拉角转四元数。

从欧拉角转四元数的公式有很多等价写法,常见航空惯例ZYX顺序下:

q0 = cos(r/2)cos(p/2)cos(y/2) + sin(r/2)sin(p/2)sin(y/2)
q1 = sin(r/2)cos(p/2)cos(y/2) - cos(r/2)sin(p/2)sin(y/2)
q2 = cos(r/2)sin(p/2)cos(y/2) + sin(r/2)cos(p/2)sin(y/2)
q3 = cos(r/2)cos(p/2)sin(y/2) - sin(r/2)sin(p/2)cos(y/2)

注意这个公式的写法依赖旋转顺序定义。不同飞控或库可能用不同约定,工程上一定要先确认姿态约定再套公式。我在移植代码时遇到过一次姿态翻转的问题,最后发现就是初始四元数的旋转顺序跟后级不一致。

4.2 每步都归一化,但要聪明地处理

单位四元数才表示纯旋转,模长一旦偏离1,旋转就会附带一个缩放效果,姿态更新越来越离谱。所以每步更新后必须归一化,这基本是共识。

但这不意味着每次都要无脑开平方。在MCU上,如果主频不高,可以先算模长平方,偏离阈值大于某个值(比如1e-6)再做一次归一化,或者用快速平方根倒数近似。现代单片机带FPU的话,直接sqrt也没压力,关键是别漏掉这个步骤。

还有一个容易被忽略的符号问题:q和-q表示同一个旋转。在滤波器内部,要尽量保持符号一致,否则在姿态融合或插值时会出现跳变。常见的做法是保证w分量非负,或者在连续两次更新之间检查点积不为负。这个细节在处理长时间运行的日志数据时特别重要。

4.3 四元数转欧拉角:只给调试看

内部运算用四元数,但调试时还是想看角度。标准转换公式是:

roll = atan2(2*(q0q1 + q2q3), 1 - 2*(q1q1 + q2q2))
pitch = asin(2*(q0q2 - q3q1))
yaw = atan2(2*(q0q3 + q1q2), 1 - 2*(q2q2 + q3q3))

写C代码时尤其注意,atan2在C标准库里的参数顺序是(y, x),别写成(x, y)。pitch的asin参数是2*(q0q2 - q3q1),由于浮点误差可能略超[-1,1],要手动钳制到[-1,1],否则返回NaN。我在日志里见过大量NaN,查到最后都是这里漏了钳制。

至于俯仰角到90度附近yaw和roll突然跳变,这是欧拉角表示的固有奇点,不是解算错了。所以内部控制回路一定不要用欧拉角,只在给人看的时候转换。调试时如果必须看角度,可以把pitch限制在±89度范围内,至少日志不会出现恐怖的跳变。

4.4 常见问题与排查技巧

我把自己调试过程中踩过的坑整理成了一张速查表,遇到问题先照着排查。

现象可能原因排查与处理
静止时姿态缓慢漂移陀螺零偏未补偿上电静态采样500到1000个点求平均零偏,初始化时减掉;或调大Ki
输出角度来回抖动加速度计噪声大、Kp过高降低Kp,对加速度计做低通滤波,或提高采样率后均值
旋转到某位置后数据变NaN欧拉角转换时asin参数超界钳制asin参数;检查四元数是否归一化
航向始终慢慢偏磁力计未校准或受磁干扰做椭球拟合校准;远离电机磁场再测
四元数更新后模长越来越大更新后忘归一化每步强制归一化
旋转方向反了坐标系轴方向约定不一致用已知方向旋转验证,必要时把某个轴的角速度取负
yaw在180度附近来回跳atan2在边界处的符号变化输出层做角度差mod处理,或直接保留四元数用于控制

其中磁力计那条,我在无人机上折腾了最久。当时地面测试一切正常,一键起飞后航向就开始偏,后来才发现是磁力计离电调太近,电调一上电,磁场方向就被干扰到没法看。最后换到机臂末端并做了校准,问题才解决。这类问题不是算法能补救的,安装和布线往往比调参更重要。

5. 最后分享一个调四元数时的习惯

我自己调姿态解算时有一个固定动作:先录一段传感器日志,然后在离线环境里跑同样一组数据,把前后两次的四元数点积打印出来。如果点积接近1,说明姿态连续平滑;如果偶尔点积变负,说明四元数发生了符号翻转,需要统一符号。这个小技巧排查跳变和插值异常非常有效,比盯着一堆角度日志直观得多。

调参顺序上,建议别贪多。先从最简的纯积分开始,确认陀螺仪方向和数据率都对,再把加速度计修正接进来,最后才加磁力计。每加一层就验证一层,等全部加完,系统出问题的概率会小很多,排错范围也能快速缩小。Kp可以先给0.2,Ki给0,观察静态误差,如果姿态能在几秒内收敛且在运动时跟手,再慢慢调Ki补偿零偏。

四元数确实反直觉,但一旦接受它那套运算规则,你会发现姿态表达、旋转复合、插值变换都变得特别干净。我后来做机械臂和AR姿态处理时,直接复用同一套四元数核心,几乎没有再被姿态表示的坑绊过。希望你也能少走我走过的弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询