DEEP 04 · 第一辑 · 运动控制核心 · LEARN NOTES

收进学习手册 · 2026-09-11 · duck.whatled.com

解读 04 · safety.rs:安全层怎么写进代码

解读对象:duck-control/src/safety.rs(580 行)
需要的前置:《什么鸭公开课》第 04 课(61→14 契约)

这个模块的全部权威来自一句话:"它持有通往机器人的唯一写句柄"(safety.rs:3-6)。Safety<T: RobotIo> 泛型包住 IO(T 是约束在 RobotIo trait 上的类型参数,Rust 用它实现"传什么类型都能调同一套接口"),策略、仲裁器、客户端谁也拿不到 RobotIo,谁也就无法命令电机。注释说这由借用检查器强制,而不是靠"加第八个技能的前夜还得记得规矩"。

1. 两条铁律加一道门

apply(safety.rs:234)是唯一的电机写入路径,内嵌两条无条件规则。其一,非有限值拒绝:targets.iter().any(|v| !v.is_finite()) 命中就放弃目标、写入 hold 位姿并上报 Limit::NotFinite(safety.rs:251-255)。注释解释了为什么 NaN 不能钳位:钳位会产出边界值——一个"看起来合理的关节角",机器人会猛地窜向限位,而拒绝移动则安然无恙。其二,行程钳位:每个目标 clamp 在 ACTUATOR_MIN/MAX = ∓π(safety.rs:47-48,XL330 一圈的行程),被改动的 tick 记一个 Limit::Range。注释同样坦白:这是执行器行程,不是解剖学限位,挡得住 NaN 和垃圾张量,挡不住"机械上不明智"的姿势。

门槛是 gate(safety.rs:218):意图超过 deadman = 500ms(SafetyConfig,safety.rs:74)没来,就把指令的 twist 三元组清零,只清 twist——头部目标过期无害,过期速度却会让机器人走进墙。注释点了题:"停止不是瘫软",失联的双脚机器人该站住,因为站立才是安全态。

2. 摔倒是报告,不是规则

observe(safety.rs:181)每 tick 更新摔倒判定:投影重力 z 越过 fall_gravity_z = -0.5(直立约 -1.0,侧躺接近 0)持续 fall_debounce = 200ms 才算摔,任何一个直立样本立即清零累积。双向去抖的理由:一次硬落脚的冲击读起来就像摔。

但关键设计是这个判定不拦截任何东西:摔倒的机器人照样被驱动,apply 里明写"这里没有摔倒闸"(safety.rs:243-246)。摔了怎么办是控制决策,住在上一层的 robotd:limp-fall 模式用 fall.rs 的预测器决定软下去,并像所有普通调用者一样走 apply,没有豁免通道。注释的论证值得抄:若这里有闸,起卧调姿就必须给闸开旁路——"带旁路的安全规则不是安全规则"。

3. 一个真实事故:未收敛的滤波器投了票

observe 开头有一步容易被删掉的守卫:if !self.io.imu_ready() { return; }(safety.rs:198)。SFLP 滤波器需要几秒样本四元数才有意义,没收敛时投影重力"读起来像在阈值之上",200 ms 后台面上的直立机器人就被判定摔倒,增益被写成 gain_limp = 50,几秒后自愈——增益却留在 50,没有任何可见状态能解释(safety.rs:184-193 的注释记了一块板子上的真实下午:手动设 137 的关节读回 50)。守卫在两个方向都取安全默认:启动时维持"未摔",运行中滤波器失就绪则维持"已摔"。

4. 省出来的总线:增益只写一次

set_gain 缓存上次写入值,相同就跳过(safety.rs:272-279)。测试算过账:50 Hz 下无脑重写是每秒 750 次多余的总线写(safety.rs:550-551),全花在控制环最需要的那条总线上。gain() 查询返回的是"实际在跑的",不是"调用方要的"——两者在守卫生效时不同。

你带走的收获

延伸

什么鸭 WHATDUCK · 复刻学习笔记 ← 全部手册 · 首页