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

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

解读 01 · bus.rs:一条 1 Mbps 总线上的 15 个对话

解读对象:duck-control/src/bus.rs(507 行)
需要的前置:《什么鸭公开课》第 10 课(舵机从 SG90 到总线)

15 个舵机加一块 IMU 板挂在同一条半双工串行总线上(BAUD_RATE = 1_000_000,见 model.rs:80),控制循环跑 50 Hz。每个 tick 只有一天 20 ms,这一篇看代码怎么把总线时间花在刀刃上。

1. 一个 tick 一次对话:IMU 排第一

read() 的核心是一句 sync_read_raw_data(&self.ids, READ_ADDR, READ_LEN)(bus.rs:248):用 Dynamixel 的 sync_read 一条指令问遍所有设备。READ_ADDR = 124、READ_LEN = 12(bus.rs:27-28),12 字节正好装下 present_pwm、present_current、present_velocity、present_position 四个寄存器。IMU 板的 ID(IMU_DXL_ID = 200,model.rs:78)被排在 ids 的第 0 位(bus.rs:114-116),所以它的应答先回来,blocks[0] 就是 IMU。

为什么合并成一次读?半双工总线上每笔事务都要付寻址和等待的固定开销,16 次单读的握手时间远大于 1 次批量读。这也是 io.rs 里 RobotIo trait 把"读"设计成一次拿全 Sensors 的原因。

2. 快慢分离:电压和温度单独一笔账

电池电压在寄存器 144、温度在 146,中间隔着 velocity_trajectory/position_trajectory 共 12 字节没人要的数据(bus.rs:37-40)。硬把它们并进 tick 的读块,等于每次 tick 让每个舵机多答 8 字节,耗的总线时间反而超过单独读。于是有了 SLOW_READ_ADDR = 144、SLOW_READ_LEN = 3(bus.rs:41-42):一笔 3 字节的 sync_read 同时拿回两个量,约每秒一次而非每 tick。slow_sensors()(bus.rs:351)里电压取 15 个舵机的平均——同一块电池;温度不平均,因为"一个发热的关节"才是要看的信号,均值会把它藏掉。

3. 开机体检:把"这台车为什么慢"消灭在启动时

check_registers()(bus.rs:132)逐舵机核对 EXPECTED_REGISTERS 里的 EEPROM 寄存器(return_delay_time、baud_rate、pwm_slope、shutdown),不符就写回并计数。理由写在注释里:被恢复出厂的舵机 return_delay_time = 250,仅此一项就吃掉全总线约 40% 的 tick 预算(bus.rs:129-131)。启动时每寄存器一次读,换掉一整类"这台机器人莫名慢"的排查。

同类心思还出现在 set_gain()(bus.rs:316):写 kP 时顺手把 ki、kd 固定为 0(const KI/KD: u16 = 0)。这些是 RAM 寄存器,每次上电回到出厂值,而出厂 D 增益非零——不归零,同样 kP 下机器人会比原型"明显偏软"(bus.rs:317-322)。

4. StaleImuTracker:给"沉默的谎言"计数

总线读成功不代表数据是新的:IMU 板可能一直回同一个采样(StaleImuTracker,bus.rs:67)。observe()(bus.rs:78)比较当前块与上一块,相同则累计,返回连续重复长度。连续 STALE_RUN_WARN = 25 次——50 Hz 下的半秒(bus.rs:59)——才告警一次,之后每 500 次才再提醒(bus.rs:270)。注释说得很直白:第一次重复就报,只会教会所有人忽略这条消息。另外 set_torque()(bus.rs:202)故意"写完全部再报错":一个丢应答曾让关机流程半路死掉,腿还锁着就断电了。

你带走的收获

延伸

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