解读 198 · 50Hz 的由来
解读对象:
duck-control/src/model.rs(241 行,常量区自第 80 行起)· 交叉验证infer_policy.py
需要的前置:解读 107(model.rs 常量全表);解读 01(一个节拍里怎么安排 16 个对话)。
一句话答案
50Hz 不是拍脑袋,是三道算术互相咬合的结果:仿真侧 0.005s × 4 步 = 20ms;总线侧 16 台设备在 1Mbps 上的问答必须塞进这 20ms;而且修好的关键参数不是波特率,是一个出厂默认值。
第一道:训练侧的 0.005 × 4
回放脚本 infer_policy.py 启动时打印:Control frequency: 50 Hz (decimation: 4),配 model.opt.timestep = 0.005。物理引擎每 5 毫秒推一步,策略每攒 4 步(20ms)想一次——这是训练时就定下的节拍。策略在什么节拍上学的,部署就必须在什么节拍上跑,50Hz 首先是训练合同的复制。
第二道:总线侧的 8ms 账单
真机上每个节拍要干一件事:一条 sync_read 问遍总线上的 16 台设备(IMU 板 ID 200 + 15 只舵机),一条 sync_write 写回目标位置。总线带宽是 BAUD_RATE: u32 = 1_000_000(model.rs:80,1Mbps)。真正的暗雷在 EXPECTED_REGISTERS 的注释里(model.rs:89–94):
return_delay_timeis the load-bearing one: the XL330 ships at 250, which is 500 µs of turnaround per device. Across 16 devices that is 8 ms per tick — 40% of a 20 ms budget — spent waiting for servos to get around to answering.
翻译:每只舵机收到指令后要默认等 500 微秒才应答(出厂寄存器值 250,单位 2µs)。16 台设备排队,光"轮到你我再开口"就要烧掉 8 毫秒——20ms 预算的 40% 什么都没干,纯等。所以启动时 check_registers 会把这个寄存器强制改写成 0:("return_delay_time", 0)。8ms 的账单一笔勾销,读写才塞得进一个节拍。同表里 ("baud_rate", 3) 也是防漂移:寄存器值 3 必须 = 1Mbps,与 BAUD_RATE 对账。
第三道:预算的纪律
20ms 里不只有问答:61 维观测要拼装、ONNX 要推理、14 个动作要写回、安全检查要过。bus.rs 为此把每个环节都按预算设计:
READ_TIMEOUT = Duration::from_millis(30)不是"可以超 30ms",而是封顶:"a missing device costs a bounded hiccup rather than stalling the loop"(一只设备失联只许付有限代价,不许拖死循环);- 一条
sync_read而不是每关节一条:IMU 排第一位先应答,舵机数据紧随其后; - 电压、温度这类慢变量挪出主节拍,约一秒一次(
slow_sensors),别占 50Hz 的奶油; STALE_RUN_WARN: u64 = 25的注释自己算给你看:"25 reads is half a second at 50 Hz"——连常量都以 50Hz 为刻度。
超时 30ms 比 20ms 大,矛盾吗?不——它是"坏情况的上限",健康时 16 台设备的问答远到不了 20ms,预算余量就是用来吸收失联那一拍的。
你带走的收获
- 50Hz = 训练节拍(decimation 4 × 0.005s)在真机上的复刻:仿真与实机必须同频。
- 出厂默认值也能吃预算:一只 500µs 的应答延迟 × 16 台 = 8ms = 40% 预算,启动时强制清零。
- 换算口诀:寄存器值 × 2µs = 每台周转时间;20ms = 一个控制节拍;25 次 = 半秒。
- 实时性的本质是预算管理:封顶超时、合并事务、慢变量降频,都是给 20ms 省时间的手段。
- "由来"类的数字都有出处可查:
BAUD_RATE在 model.rs:80,8ms 账单在其 89–94 行注释里。