编码器角度平均:修好了跨零 Bug,为什么还是会算错?

首发于公众号「如是有为」 · 查看原文

编码器角度平均:修好了跨零 Bug,为什么还是会算错?

前几天饭桌上,有人问了一个看起来很初级的问题:

1° 和 359° 的平均值是多少?

我说,这题你别急着笑。它能把一个编码器项目里所有“凭感觉正确”的前提,全翻出来。

第一遍:当然是 180°

先别管编码器,就把它们当普通数字:

(1° + 359°) / 2 = 180°

小学算术完全正确。

但把两个角度画到编码器圆盘上,你会马上觉得不对:1° 和 359° 明明挨着,平均怎么跑到对面去了?

于是第一次反转来了。

第二遍:不,应该是 0°

如果这两个数表示两个无序、等权的圆周方向,那么它们之间的短弧只有 2°,短弧中点就是 0°。

向量法同样得到 0°:

θ̄ = atan2(sin 1° + sin 359°, cos 1° + cos 359°) = 0°

到这里,多数教程会说:角度不是普通数字,要走短弧,用 wrap 或 atan2。

听起来已经结束了。

其实没有。

第三遍:它也可能是 180°

现在只换一个条件。

这两个数不再是“两个无序方向”,而是编码器按时间读到的两帧:

上一帧:1°
下一帧:359°

真实运动有两种可能。

第一种,反向转了 2°:

1° → −1°

两帧的连续位置平均是:

(1° + (−1°)) / 2 = 0°

第二种,正向转了 358°:

1° → 359°

两帧的连续位置平均变成:

(1° + 359°) / 2 = 180°

编码器角度平均:修好了跨零 Bug,为什么还是会算错?

同样两个编码器读数,0° 和 180° 都能成立。

不是数学打架,而是两个模 360° 的数字没有告诉你:磁铁到底走了哪条路,也没有告诉你转了几圈。

严格地说:对圆周方向集合,1° 和 359° 的圆均值是 0°;对连续轨迹中的两个模角度观测,如果没有运动路径和圈数约束,轨迹平均位置不能唯一确定。

第四遍:工程现场,它通常又是 0°

那为什么编码器工程师看到 1° 和 359°,几乎都会直接选 0°?

因为代码背后偷偷使用了一个通常没写出来的条件:

Δθ真实
< 180°

也就是:一个采样周期里,机械角不可能转过半圈。

在这个约束下,358° 的正向长路被排除,只剩下 -2° 的短路。

wrap 到底算什么

wrap 不是求平均,也不是专门修补 0°。它只做一件事:

把一个角度差折回到 [-180°, 180°),得到最短有符号角差。

wrap(358°)   = -2°
wrap(-358°)  = +2°
wrap(181°)   = -179°
wrap(-181°)  = +179°

例如:

wrap(359° − 1°) = −2°

这并没有证明真实运动一定反向转了 2°。它只是利用“单帧位移小于 180°”这个工程约束,排除了其他分支。

编码器角度平均:修好了跨零 Bug,为什么还是会算错?

所以更严格的说法是:

wrap 不是在发现真相,而是在已知最大速度和采样周期的前提下,选择唯一允许的角位移。

那么,编码器角度平均到底怎么算?

这时候不能再问“有没有一个万能公式”,而要先问:

你平均的是同一位置的重复测量,还是一段连续运动轨迹?

情况一:磁铁基本没动,只是在同一位置重复采样

例如:

359.9°,0.1°,359.8°,0.2°

这组数据代表同一个真实方向附近的噪声。做法是选一个共同参考角,所有样本都和它比较:

δᵢ = wrap(θᵢ − θref)

再对这些连续的小角差做等权平均:

θ̄ = wrap(θref + (1/N) × Σ δᵢ)

注意,平均的是相对于同一个参考角的角度差,不是把每个绝对角度直接 wrap 后相加。

情况二:磁铁连续运动,可能跨过 360°,甚至走过半圈

例如从 0° 一点点正转到 200°:

0°,1°,2°,……,199°,200°

这时不能把每个样本都相对 0° 取最短路,否则 181° 会被折成 -179°,轨迹会被错误地扳回去。

正确做法是利用时间顺序,先计算每一帧相对上一帧的短角差:

Δᵢ = wrap(θᵢ − θᵢ₋₁)

再累加,恢复连续角度:

Θᵢ = Θᵢ₋₁ + Δᵢ

得到:

0°,1°,2°,……,200°

然后对连续角度做普通平均:

Θ̄ = (1/N) × Σ Θᵢ

如果从 0° 均匀采到 200°,平均位置就是:

Θ̄ = 100°

还有一个很常见、而且真的会算错的递推写法

不少代码会先平均前两个角,再拿旧平均值和第三个角五五开:

mean = midpoint(mean, new_angle);

这不是累计平均。

例如三个普通数 0°、100°、200°:

m₂ = 50°

m₃ = (50° + 200°) / 2 = 125°

但真正等权平均是 100°。

因为第三个样本占了 50% 权重,前两个各占 25%。越晚来的数据,权重越大。

标准累计平均必须让第 n 个样本只占 1/n:

Θ̄ₙ = Θ̄ₙ₋₁ + (Θₙ − Θ̄ₙ₋₁) / n

continuous += wrap(current - previous);
mean += (continuous - mean) / sample_count;

第一行负责恢复轨迹,第二行负责等权累计平均。它们是两件事,不能混成“每次再取一次中点”。最后一翻:有时候根本没有平均角

读到这里,很容易又产生一种安全感:

静止采样用共同参考角;连续轨迹先解包;递推权重用 1/n。终于完整了。

最后再看一组数据:

0°,90°,180°,270°

它们均匀分布在整个圆周上。

向量和为零:

Σ cos θᵢ = 0,Σ sin θᵢ = 0

没有任何一个方向比另一个方向更有代表性。

这时 atan2(0,0) 即使返回了一个数字,那个数字也没有物理意义。

真正成熟的角度平均函数,不能保证任何输入都返回一个角度。它必须允许返回:无唯一解。

最终结论

1° 和 359° 的平均值到底是多少?

如果它们是两个无序、等权的圆周方向,答案是 0°。

如果它们是连续轨迹中的两个模角度观测,又没有给出路径、圈数和最大位移约束,答案不能唯一确定,0°和180°都可能对应某种连续解释。

如果编码器满足单帧真实角位移小于180°,wrap 会排除长路,工程上重新得到 0°。

而如果样本均匀铺满整个圆周,正确答案不是0°、180°或任何别的角度,而是:

这组数据没有唯一平均方向。

所以编码器角度平均真正的标准,不是一条孤立公式,而是:

先定义对象: 方向集合、同一位置测量,还是连续轨迹。 再写出先验: 最大速度、采样周期、是否丢帧、是否保留圈数。 最后才计算: 共同参考、连续解包、等权平均,或者直接返回无解。

很多角度算法不是公式写错,而是把“凭经验成立的条件”藏在了代码外面。

说明:文中所有数学公式均采用 Obsidian 兼容的 LaTeX 表达。单文件 HTML 中图片已内嵌,可离线打开;微信公众号编辑器复制时建议使用浏览器全选正文后粘贴。

应作如是观
公众号「如是有为」二维码

关注公众号「如是有为」

微信扫一扫,新文章先在公众号发出。