两个 Bug 为什么能互相抵消:一次运动时间公式的误导性正确

Babel36acl 嵌入式实战 无~ 25 次阅读 预计阅读时间: 11 分钟 发布于 4 天前 最后更新于 2 小时前 2382 字


摘要:当两个独立错误恰好把结果拉回到“看起来差不多”时,测试通过不代表公式正确,尤其是在运动控制这类边界敏感场景里。

这篇文章复盘一个很适合拿来讲工程直觉的案例:一个步进运动时间公式里同时存在两处错误,单看任何一处都足以引发异常,但它们叠在一起以后,居然在大部分工况下给出了“看着还行”的结果。真正把问题暴露出来的,是一个短行程边界条件。

两个 Bug 为什么能互相抵消:一次运动时间公式的误导性正确

很多人对 bug 的直觉是:有错就会立刻错得很明显。

可工程里更危险的情况,恰恰是“错了,但没完全错”。结果不离谱,流程能跑,测试大部分时候也过,于是所有人都会下意识地相信那套公式本身没有问题。

这次踩到的就是这种坑。

事情是怎么开始的

某段步进动作在正常工况下表现一直还行,直到一个短行程场景反复在超时边界附近出现异常。现象不是完全失控,而是:

  • 大多数动作都能跑完
  • 只有少数短段动作容易误判超时
  • 现场看起来像“时间预算有点偏”

这种表现最容易让人先去怀疑:

  • 超时阈值配小了
  • 电机加速度太激进
  • 机械阻力波动导致实测时间偏大

结果顺着公式往回推,才发现根因比“参数不准”更严重:公式本身有两处独立错误。

那两个错误分别是什么

示意代码如下:

float time_to_accel(float start, float target, float accel, float pulses)
{
    float v = sqrtf(start * start + accel * pulses);
    float t = (v - start) / accel;
    return t;
}

如果把它和理论推导对照,会看到两件事。

错误一:距离公式里少了一个系数

本来该进入根号的是:

start² + 2 * accel * pulses

结果实现里写成了:

start² + accel * pulses

这会让推导出的中间速度偏小,进一步把时间也算短。

错误二:只算了单程,没有算完整的加减速过程

对于对称的短行程运动,如果你的目标是估算完整动作时间,就不能只取单边时间。可实现里只返回了一次加速段的结果,没有把另一半过程计进去。

这又会把结果再压短一截。

最麻烦的地方:两个错误叠起来,结果居然没那么离谱

这才是这个案例真正有意思的地方。

两个错误都在把结果往“更短”拉,但它们作用的方式并不完全一样。某些参数区间里,误差不会简单地线性放大,反而会在数值上落到一个“勉强像对的”范围。

于是现场就出现了一种非常误导人的局面:

  • 长行程看起来没什么问题
  • 常规动作时间和经验值差得不算夸张
  • 绝大多数测试都能通过

这会让人自然得出一个错误结论:公式大体没问题,只是边角参数需要再调。

实际上,公式从根上就是错的。

为什么短行程更容易把问题掀出来

短行程动作对时间估算更敏感,因为它没有足够长的稳定段去“稀释”前面公式的偏差。

一旦动作脉冲数很少,就会出现这些情况:

  • 加速段占比变大
  • 公式误差不再被长匀速段掩盖
  • 超时阈值和实际动作时间更容易贴边

于是原本在长段动作里不显山不露水的错误,到了短段动作里就会被放大成状态机误判。

这个问题真正说明了什么

它说明“测试通过”和“模型正确”是两回事。

尤其是运动控制、滤波、控制参数换算这类问题,只要满足下面任一条件,就很容易出现“看起来对”的错:

  • 测试样本主要集中在常规工况
  • 误差方向相同,但量级恰好接近
  • 系统上层有超时、容差或经验参数在兜底

当这些东西叠在一起时,错误不会第一时间以“完全失效”的方式暴露出来,而是以“偶发边界异常”的形式出现。

更稳妥的做法,不是多试几次,而是做反推校验

这类公式一旦代码化,我很建议加一轮 back-check,也就是反推验证。

例如给出一组明确输入:

start = 2000
target = 60000
accel = 90000
pulses = 3333

然后同时做三件事:

  1. 手算或脚本计算理论值
  2. 跑实现函数拿到输出
  3. 拿实际示波器或脉冲计数结果做对照

如果三者对不上,就别急着调阈值,先怀疑公式。

代码层面还能补哪些防线

除了修公式本身,我更建议补这几类工程防线:

  • 给关键公式单独写单元测试,至少覆盖长行程、短行程和边界脉冲数
  • 把三角波和梯形两种路径拆开,不要混在一个“看似通用”的实现里
  • 在注释里直接写明物理意义,而不是只写变量名
  • 对“超时预算”这类上层依赖值,记录推导来源

这些工作看起来没有“修 bug”那么立竿见影,但它们能大幅降低下一次再被“误导性正确”骗过去的概率。

最后一句

真正危险的错误,不是算出来离谱得一眼能看出来的那种,而是算出来“差不多对”的那种。

因为前者会逼你立刻修,后者会让它活很久,直到某个边界条件把所有侥幸都收走。

此作者没有提供个人介绍。
最后更新于 2026-05-30