• 欢迎访问少将全栈,学会感恩,乐于付出,珍惜缘份,成就彼此、推荐使用最新版火狐浏览器和Chrome浏览器访问本网站。
  • 吐槽,投稿,删稿,交个朋友
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏少将全栈吧

Microduck 的 50 Hz 控制循环:从 Dynamixel 总线到策略推理的完整链路

Build in Public admin 1小时前 5次浏览 已收录 扫描二维码

深入拆解 Microduck 的实时控制循环——一条 UART 上挂 16 个设备、50 Hz 精确节拍、ONNX 策略推理和两层安全门禁的完整数据流。


当你按下手柄的摇杆,Microduck 往前走了一步。从摇杆输入到舵机转动,中间只隔了 20 毫秒。这 20 毫秒里发生了什么,决定了这只鸭子是稳稳迈步还是一个踉跄栽倒。

Pollen Robotics 在 robotd-design.md 里把这条链路写得非常清楚。这篇文章不讲概念,直接拆代码架构——从物理层到策略推理,再到安全层,一条完整的 50 Hz 控制循环长什么样。

一条 UART,16 个设备

Microduck 的硬件总线方案简洁到有点粗暴:15 个舵机和 1 块 IMU 板,全部挂在同一条 UART 上。没有第二条总线,没有第二个端口。

robotd — control thread
  │ duck_control::bus::DynamixelIo
  │ serialport · TIOCEXCL
  ▼
/dev/ttyS2 · 1 Mbps · Dynamixel protocol v2
  │
  ├── id 200    imu_to_dxl v2 board(IMU 板)
  ├── id 20-24  左腿(hip_yaw/hip_roll/hip_pitch/knee/ankle)
  ├── id 30-34  脖子和头(neck_pitch/head_pitch/head_yaw/head_roll + mouth)
  └── id 10-14  右腿

IMU 板的 id 是 200,排在查询向量的第一个。为什么要这么设计?因为 v2 板子坐在 Dynamixel 总线上,用一个 SFLP 四元数响应同一个 register block 的查询。一条 sync_read 命令读完所有 16 个设备,IMU 数据和服务器的角度、速度同时到达。没有额外延时,没有二次查询。

波特率 1 Mbps,Dynamixel Protocol 2.0。这个速度在 50 Hz 下够用——每次总线事务大约传输 200-300 字节,1 Mbps 的理论极限是 125 KB/s,实际利用率不到 10%。

但总线独占是个现实问题。Armbian 默认在 UART2 上跑 serial-getty 登录控制台,一个 agetty 进程占着端口,舵机全看不见。setup-board.sh 的第一件事就是 mask 掉这个 unit。排查时用 fuser -v /dev/ttyS2——如果返回的是 agetty,你就知道问题在哪了。

每个节拍三件事

50 Hz 控制循环的核心代码在 duck-control crate 里,每个 tick 做三件事:

  1. **read()** — 一次 sync_read,读 IMU 板 + 15 个舵机的 registers 124-136(当前角度、速度、负载)
  2. **decide()** — 构建观测向量 → 策略推理 → 计算目标角度 → 安全限幅
  3. **write()** — 一次 sync_write,写所有舵机的目标位置

每 1 秒多一个慢速传感器查询:registers 144-146,读电压和温度。

这里有个重要的架构决策:duck-control 是一个库,不是服务。它没有 tokio runtime,没有 socket,没有 systemd 依赖。robotd 进程是包裹它的壳。编译器强制了这个边界——这意味着 control crate 可以独立测试、独立编译,甚至未来可以单独提出来给运行时用。

61 维观测向量和策略推理

观测空间 61 维,分成两块:

  • **48 维本体感受**:15 个关节的角度、速度、负载,IMU 的 roll/pitch/yaw 和加速度
  • **13 维指令块**:twist(3) + head_pose(4) + body_pose(6)

这个 61 维的契约在所有策略间共享。不管是 walk、recover、sit 还是 kick,输入都是同样的 61 维向量。不用的指令槽 ZERO-PAD,绝不删除槽位。这就是策略热切换的基础——任何时刻换策略,观测空间的格式不变。

策略本身是 ONNX 格式,加载时已经烤进了观测归一化器。从 microduck_rl 训练完成后,scripts/export.py 自动做这件事。策略推理的输出是一个 [f32; 14] 的动作向量——对应 14 个舵机的目标角度(嘴巴舵机不参与步态,单独控制)。

状态机:选哪个策略干活

策略推理不是简单地把观测扔进神经网络然后输出动作。在那之前,有一个状态机决定当前选哪个策略来推理。

kick > ground pick > sit/rise > stand (by |twist|, or forced) > walk

这个优先级顺序是硬编码的:踢腿动作的优先级最高,如果 kick 触发,其他全部让路。走路优先级最低——一个行走中的鸭子如果触发了 recover,会立刻切换到恢复站立的策略。

选好策略后:

  1. **Policy::infer()** → 输出 14 维动作向量
  2. **home pose + scale × action** → 从默认姿态叠加动作缩放,得到 15 个关节的目标角度
  3. **低通滤波** → 头部和腿部做 EMA 平滑,防止策略输出抖动直接传给舵机

这里值得注意:策略本身是不滤波的。训练时不加低通滤波(unfiltered),低通是部署时加的。如果训练时加了 EMA 而部署时没加,或者反过来,sim2real transfer 直接崩。所以官方文档说得清楚:滤波必须在训练和部署两侧保持一致。

安全层:谁也不能直接写舵机

这是整个架构里最精妙的部分。safety 模块持有唯一的 RobotIo 实例——谁拿到这个实例,谁就能写舵机。而拿到的唯一方式是通过 safety.apply()

safety.apply 做的事情:
  · 拒绝非有限值(NaN 或 Inf 直接丢弃)
  · 限幅到执行器物理范围(动作空间边界)
  · 没有 fall gate——安全层只报告裁决,不替策略做决定

策略、控制器、任何客户端都可以提议目标角度,但没有任何东西能绕过 safety 直接写总线。这不是一个编程规范,这是一个类型系统约束——RobotIo 是私有的,只有 safety 模块能访问它。

fall detection 也在 safety 层:safety.observe() 检查 IMU 和接触传感器,debounce 后判断是否摔倒。如果检测到摔倒,状态机会自动触发 recover 策略。

总线的锁机制

多个进程可能争抢同一条 UART。robotd 的锁机制有三层:

  1. **TIOCEXCL** — serialport 库设置这个标志后,第二个非 root 进程 open 会返回 EBUSY
  2. **Advisory file lock** — 对 `/run/robotd.sock.lock` 做 flock,因为 root 可以绕过 TIOCEXCL
  3. **Socket 绑定锁** — robotd 在绑定监听 socket 前先拿锁,拿到锁才开控制线程

关键细节:锁文件永不删除。即使进程挂了,内核会在 fd 关闭时自动释放 advisory lock。删除再创建会导致两个不同 inode 的锁文件同时存在,竞争条件就出现了。fuser -v /dev/ttyS2 仍然是排查”谁占了总线”的最可靠方法。

仿真模式:没有舵机也能跑

没有真机怎么开发和测试?scripts/duck-sim 启动一个 MuJoCo 仿真环境,robotd 的控制循环直接连到仿真舵机而不是真实 Dynamixel 总线。

有意思的是:仿真模式下跑的是一样的 robotd 进程,不是简化版。控制循环、策略推理、安全层——全都一样。区别只在总线层:真实模式走 DynamixelIo,仿真模式走 MuJoCoIo。这意味着你在仿真里验证的逻辑,部署到真机上是同一套代码,不需要做任何适配。

官方甚至支持同时跑 4 个仿真鸭子,每个都是独立的 MuJoCo 实例,你可以 ssh 进去分别操作——这在测试多机器人通信场景时很有用。

从总线到策略的完整时延

把整条链路串起来,一次 50 Hz tick 的耗时分布大概是:

  • sync_read(16 设备):~3-5 ms
  • 观测构建 + 策略推理(ONNX runtime):~2-4 ms
  • 安全层检查:< 0.1 ms
  • sync_write(15 舵机):~2-3 ms
  • 余量:~8-13 ms

20 ms 的周期里,实际计算只用了不到一半。这个余量不是浪费——它是给通信重试和偶发总线拥堵留的 buffer。如果某个 tick 超时了,robotd 会跳过写操作,保持上一次的目标位置。

这整套设计——单总线、无锁观测、编译期强制的安全边界——不是 Microduck 独有的。但在一个 25 cm、800 g、售价 399 美元的开源双足机器人上看到这种级别的工程考量,确实能看出团队在可靠性上下了真功夫。

数据来源:pollen-robotics/microduck 仓库 docs/design/robotd-design.md (2026)、docs/design/architecture.md (2026)、GitHub README (2026)。

喜欢 (0)
[🍬谢谢你请我吃糖果🍬🍬~]
分享 (0)
关于作者:
少将,关注Web全栈开发、项目管理,持续不断的学习、努力成为一个更棒的开发,做最好的自己,让世界因你不同。