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

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

解读 07 · model.rs 与 io.rs:机器人描述与硬件 IO

解读对象:duck-control/src/model.rs(225 行)、duck-control/src/io.rs(335 行)
需要的前置:《什么鸭公开课》第 04 课(61→14 契约);总线背景见解读 01

1. 机器人是一组表,不是一个类

model.rs 的模块注释只有一句话:"The robot, as data"(model.rs:1)。这台鸭子只有一个版本 alpha,没有类型层级,只有几张并列的表:NUM_JOINTS = 15(model.rs:12);JOINT_IDS 按"左腿 5 · 颈头嘴 5 · 右腿 5"排列——Dynamixel ID 20–24、30–34、10–14(model.rs:15-19);DEFAULT_POSITION 是 15 个弧度值的 home 位(model.rs:39-55)。

表会漂移,const 块断言(const 块是编译期求值的代码,断言失败即编译失败)负责钉死:JOINT_NAMES.len() == NUM_JOINTS(model.rs:26)。JOINT_NAMES 从协议 crate pub use 再导出——总线上按数组下标索引关节,名字和顺序不许有两份副本。测试再补一层:ids_are_unique(ID 重复的症状"像接线故障",model.rs:144-153)、home_pose_legs_are_mirrored 检查左右腿 home 位互为相反数(model.rs:210-224)——符号写错肉眼看不见,只会让鸭子站歪。

嘴是唯一的特例:MOUTH_INDEX = 9(model.rs:31),不在任何 14 动作策略里。控制嘴的全部代码是 MOUTH_CLOSED(-5°)、MOUTH_OPEN(+30°)和 mouth_target(model.rs:62-74)——开合比例先钳到 0..1 再插值,NaN 归零,坏数值不会变成超程指令。

2. 数值来自实测,不来自数据手册

model.rs:7-9 的注释值得抄下来:数值来自 microduck_runtime 的 motor.rs,是"在硬件上量出来的",从数据手册重推"恰恰是那种看起来对、走起来错的改动"。两个例子:

3. io.rs:让控制循环可以没有鸭子的接缝

模块注释点明意图(io.rs:1-4):RobotIo::read 和 write 之间的一切都是纯计算——trait(特征,Rust 的接口)是把硬件挡在门外的那道缝。Sensors(io.rs:16)把关节和 IMU 打包成"一次原子采样":IMU 板挂在 Dynamixel 总线上,和舵机同一笔事务读回,拆成两个 trait"反而发明了总线没有的区别"(io.rs:6-9)。

trait 方法各带理由:set_gain 在这里是因为"瘫软"必须有意义——拒绝指挥摔倒的机器人只是把它冻在原姿势里,降 P 增益(原型 kP 50 对运行值 200)才让它卸力(io.rs:119-124);set_torque 的注释大写强调"启动时没有任何东西调用它"(io.rs:132-134),更新器重启守护进程不能让站着的鸭子动一下。SlowSensors(io.rs:81)单独一个方法:电压和逐关节温度约每秒一采,不走 20 ms 关键路径——温度要逐关节不是取平均,"蹲着受力的膝盖比嘴热得多",平均值恰好藏起那个即将过热锁存的舵机(io.rs:86-89)。

另一半是 FakeIo(io.rs:166):永远编译进 crate,cargo test 因此不需要硬件。字段按"要能表达哪类测试"设计:fail_reads 模拟"板子先起、舵机后上电",frozen() 模拟被手推的瘸腿机器人,torque: Option<bool> 的 None 是"重启没让机器人动过"的样子(io.rs:187-193)。

你带走的收获

延伸

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