Microduck 的舵机总线:16个设备一条 UART 怎么跑出 50Hz
做过机器人的人都知道,总线是命脉。Microduck 的 RK3566 主控板上,15 个 XL330 舵机和一个 IMU 板共享一条 UART,跑在 1 Mbps 的 Dynamixel 协议 v2 上。50Hz 的控制循环意味着每 20ms 完成一次全量读写。今天拆开看这条总线是怎么做到不卡顿的。
物理拓扑:一条线串 16 个设备
Microduck 的总线拓扑很简单粗暴。一条 /dev/ttyS2 UART,1 Mbps 波特率,Dynamixel 协议 v2。16 个设备按 ID 排列:
- ID 200:IMU 板(imu_to_dxl v2),同一个寄存器块返回 SFLP 四元数
- ID 20-24:左腿 5 个舵机
- ID 30-34:颈部、头部、嘴部 5 个舵机
- ID 10-14:右腿 5 个舵机
IMU 的 ID 是 200,排在读取向量的第一位。这不是随便排的——它需要在舵机爆发式应答之前先回答。v2 板子坐在 Dynamixel 总线上,跟舵机用的是同一套寄存器读取机制。一个 sync_read 就把 IMU 的四元数和 15 个舵机的状态全拿回来了。
关键点:没有第二条总线,没有第二个端口。所有东西都在一条线上。
一个 tick 的总线时间线
50Hz 循环里,每个 tick 做两次总线事务:
第一次是 sync_read,读地址 124-136 的连续寄存器块——PWM、电流、速度、位置。16 个设备的应答数据在同一个事务里返回。第二次是 sync_write,写目标位置回 15 个舵机(嘴部舵机排除在外,所以是 14 个写入目标)。
另外每秒做一次慢速读取,读地址 144-146——电压和温度。这两个值不需要 50Hz 采样,但它们离位置寄存器只有 8 个字节的距离,中间夹着 12 个字节的轨迹寄存器没人用。如果把它并进 tick 的读取范围,每次就要读 22 字节/设备而不是 10 字节,50Hz 下白白浪费 60% 的带宽。所以拆成独立事务,每秒跑一次,大约 1ms 开销。
fast sync read:从 16 次应答到 1 次
Dynamixel 协议 v2 有个指令叫 fast sync read(0x8A)。普通 sync read 的问题在于:每个设备各自返回一个状态包,每个包有 10 字节协议头,加上每个设备自己的 turnaround 时间。16 个设备就是 16 次总线转向。
fast sync read 改变了应答方式。设备们把自己的数据块追加到一个来自广播 ID 的单一状态包里。总线只转向一次,所有设备的数据按 ID 顺序排列在同一个包里。
这跟 return_delay_time 的优化是叠加的。XL330 出厂的 return_delay_time 默认是 250,每个设备应答前等 500μs。16 个设备就是 8ms,占 20ms 预算的 40%。Microduck 在启动时把每个舵机的这个寄存器强制写成 0。
两个优化合在一起:总线转向从 16 次降到 1 次,每次转向的等待从 500μs 降到 0。这是 50Hz 能跑得动的核心原因。
但 fast sync read 有个硬件要求:总线上所有设备都必须支持指令 0x8A。XL330 的固件必须是 v46 或以上,imu_to_dxl 板也要实现它。一个不支持的老固件设备不会返回短应答——它根本不回答,整个读取超时。这就是为什么 bus.fast_sync_read 是默认开启但可关闭的开关:如果总线上有老固件设备,退回普通 sync read 至少能工作。
EEPROM 寄存器检查:启动时的强制校准
Microduck 在每次启动时跑一遍 EEPROM 寄存器检查,对每个舵机做四项校准:
return_delay_time 写 0。前面说了,250 的默认值吃掉 40% 的 tick 预算。一个被恢复出厂设置的舵机会回到 250,不检查就会让某只鸭子莫名其妙变卡。
baud_rate 写 3(1 Mbps)。出厂是 57600,必须改。
pwm_slope 写 255。这个影响 PWM 控制的线性度。
shutdown 写 52 而不是工厂默认的 53。53 的二进制是 0b110101,52 是 0b110100——差在 input voltage 那一位。工厂默认开启了输入电压保护,但 XL330 的 Max Voltage Limit 默认是 7.0V,而 2S 电池充满电是 8.4V。如果不关这一位,电池满电时舵机会自动锁定关闭。Microduck 的设计是让舵机跑完整电池电压范围,所以必须把这一位清掉。
还有 homing_offset。膝盖舵机(ID 13 和 23)的 offset 是 -512,其他都是 0。所有姿态和策略都假设膝盖带着这个 offset。一个新舵机从盒子里出来 offset 是 0,不校准的话膝盖的零位就错了 512 个 count——大约 45 度。鸭子会站不直。
舵机自动 adoption:换上去就能用
这个功能比听起来更实用。Microduck 的设计假设是:非开发者用户也会换舵机。
一个全新的 XL330 出厂时 ID 是 1,波特率 57600。Microduck 的总线上 ID 1 不存在(用的是 10-14、20-24、30-34、200)。启动时 open_bus 先 ping 15 个预期 ID。如果恰好只有一个不回答,它就去搜 ID 1——先在 1 Mbps 下搜,搜不到就重新打开端口在 57600 下搜。
找到了就把缺失的 ID 和总线波特率写进去,重新在 1 Mbps 下打开,跑一遍寄存器检查,然后重启这个舵机。重启是为了清除写入过程中触发的 hardware error alert,否则这个 alert 会保持 torque off 直到有人拔电池。
如果有两个或更多舵机不回答,就不动了。因为你不知道新舵机该替换哪个位置——两个缺失意味着不确定,自动猜测比报错更危险。日志里会记录下来,等人来处理。
整个 adoption 过程只花 15 次 ping。57600 的探测只在真正有舵机缺失时才跑。正常的总线上这个开销几乎为零。
IMU 也在总线上
这是 Microduck 设计里一个容易被忽略但很重要的决定。IMU 不是通过 SPI 或 I2C 单独连到主控的,而是坐在 Dynamixel 总线上,作为一个 ID 200 的设备。
v2 的 imu_to_dxl 板从同一个寄存器块返回片上 SFLP 四元数。读取方式跟舵机完全一样——同一个 sync_read,同一个事务,同一个应答包。没有 IMU 抽象层,没有第二条总线,没有额外的 SPI/I2C 驱动代码。
好处是显然的:总线读取只有一条代码路径。坏处是 IMU 必须支持 fast sync read(0x8A),否则整个 tick 的 fast read 会超时。这也是为什么文档里反复强调 imu_to_dxl 固件版本的重要性。
电压和温度怎么读
电压是 15 个舵机取平均值。它们共享同一个电池包,读一个跟读十五个差别只是噪声。如果某个舵机不回答(返回 0),它会被过滤掉而不是拉低平均值——否则一个坏舵机会让系统以为电池快没电了。
温度不做平均。每个舵机的温度单独返回,报告最热的那个,而且按名字报。因为膝盖舵机在蹲姿下温度远高于嘴部舵机——15 个的平均值会把过热的那个藏起来。robotctl health 显示的是”左膝 68°C”而不是”平均 42°C”,这对故障排查有用得多。
为什么不用第二条总线
一个自然的问题是:为什么不把 IMU 拆到单独的 SPI/I2C 上,或者把左右腿分到两条 UART 上?
答案还是那个核心设计原则:简单。一条总线意味着一套读取代码、一个事务、一次总线转向。两条总线意味着同步问题——两个事务什么时候发起、谁等谁、怎么处理其中一个超时。在一个 800g 的机器人上,这种复杂度不值得。
而且 1 Mbps 的 Dynamixel 总线在 fast sync read + return_delay_time=0 的配置下,16 个设备全量读写一次大约 3-4ms。50Hz 的预算是 20ms。带宽还剩 80%,够用。
对复刻者的启示
如果你在搭建自己的小型机器人舵机系统,Microduck 的总线设计有几个值得借鉴的点。
把所有执行器和一个关键传感器放在同一条总线上。这看起来是限制,实际上是简化——读取逻辑只有一条路径,没有多总线同步问题。
用 fast sync read。如果你的舵机支持协议 v2 的指令 0x8A,一定要开。16 个设备的总线转向从 16 次降到 1 次,这是数量级的改善。不支持就退回普通 sync read,但要确保所有设备固件版本达标。
启动时校准 EEPROM。不要假设舵机的出厂设置跟你需要的一样。return_delay_time、baud_rate、shutdown 这几个寄存器不检查,迟早会踩坑。尤其是 shutdown 的 input voltage 位——如果你的电池电压范围超过舵机的 Max Voltage Limit,这一位会让舵机在满电时自动锁定。
慢速传感器拆到独立事务。不是所有数据都需要 50Hz。电压和温度每秒读一次够了,拆出来不挤占 tick 的读取带宽。
舵机 adoption 值得做。如果你的机器人面向非开发者用户,换舵机是会发生的事。自动检测、自动配置、自动重启,比让用户手动改 ID 靠谱得多。
—
数据来源:pollen-robotics/microduck GitHub 仓库 docs/design/robotd-design.md(总线拓扑、fast sync read 机制、EEPROM 寄存器检查、舵机 adoption 流程、电压/温度读取策略、tick 时间线),docs/design/architecture.md(设备 ID 分配、IMU 总线集成)。
