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

Microduck 步态状态机:策略切换、优先级仲裁与 roulade 机制深度拆解

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

# Microduck 步态状态机:策略切换、优先级仲裁与 roulade 机制深度拆解

Microduck 会走路、会踢球、会自己站起来——但这不是一个策略搞定的。

Pollen Robotics 在 robotd 里塞了六个不同的 ONNX 策略:walk、recover、sit、rise、kick 和 ground pick。它们共享同一个 61 维观测空间,但不会同时输出。每次 tick 只有一个策略能说话,谁说话由状态机决定。

这篇拆开状态机的完整逻辑:从优先级仲裁到 roulade 机制,从策略热加载到 fall detection 的 debounce 设计。

六个策略,一个观测空间

先说它们共享什么。所有策略的输入都是 61 维观测向量,格式在 crate 边界上硬编码:

观测空间 [f32; 61]:
  [0..48)  本体感受
      15 关节角度 (reg 124-126)
      15 关节速度 (reg 130-132)
      15 关节负载 (reg 136)
      IMU roll/pitch/yaw (SFLP 解码)
      IMU 加速度
  [48..61) 指令块
      twist(3)       — 线速度 x/y + 角速度 z
      head_pose(4)   — 头部朝向 (quaternion)
      body_pose(6)   — 躯干姿态预留

输出是 [f32; 14] 的动作向量,对应 14 个步态舵机的目标角度(嘴巴舵机 id 15 不参与步态,单独控制)。输出经过 home_pose + scale × action 的逆归一化,再经过低通滤波,才进 safety 层。

这个 61→14 的契约在所有策略间共享。不用的指令槽一律 ZERO-PAD,绝不删除。如果你看一下动作空间的编码,ctrl idx 0-4 是左腿(hip_yaw/hip_roll/hip_pitch/knee/ankle),5-8 是脖子和头,9-13 是右腿——这个顺序和 sync_read 的 id 顺序完全对应,代码里不需要做映射。

状态机:优先级即一切

六个策略不是平等的关系。robotd-design.md 里明确写了优先级顺序:

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

这不是流程图上的箭头,这是 代码里一个有序列表——状态机从上到下检查条件,第一个匹配的策略就执行。

具体来说:

  • kick(最高优先级):踢腿动作触发时,任何其他动作立即暂停。这是故意设计的——交互行为应该打断运动行为,不管鸭子正在走路还是恢复。
  • ground pick:鸭子摔倒后尝试从地面站起来。优先级高于 sit/rise,因为摔倒后不先站起来,其他动作都没有意义。
  • sit/rise:坐下和站起是一对状态,通过 debounce 防止抖动。rise 完成后会回到 stand/walk。
  • stand:默认状态。如果 |twist| > 阈值,切换到 walk;如果被 force,也切换到 walk。twist 是摇杆输入的线速度和角速度命令。
  • walk(最低优先级):行走策略只在没有更高优先级动作时执行。

这个设计有意思的地方在于:walk 的优先级最低。一个正在走路的鸭子如果检测到摔倒,recover 策略立刻接管;如果检测到球来了,kick 策略打断行走。每个策略只关心自己的触发条件,不关心其他策略在做什么——仲裁完全由状态机完成。

这种设计比集中式调度简单得多。每个策略的输入和输出格式相同,状态机只看”谁的条件先满足”。如果要加一个新策略(比如 dance),只需要在列表里插入一个优先级位置,不用改其他任何策略。

Roulade:不是策略,是动作原语

Roulade 是状态机里的一个特殊角色。它不是完整的策略,而是一个硬编码的动作序列——当 kick 触发时,系统不是跑一个 ONNX 策略,而是执行一组预先写好的关节轨迹。

从架构文档可以看到 roulade 在推理流程中的位置:

Policy::infer
  │
  ├── roulade > kick > ground pick > sit/rise > stand/walk
  │    ↑
  │  硬编码轨迹,不走 ONNX

为什么 kick 要用硬编码而不是神经网络?两个原因:

  1. 确定性:踢腿动作需要精确控制——脚在特定时间到达特定位置,不能有概率性抖动。ONNX 策略的输出天然有随机性(即使在确定模式下),不适合需要精确定时的动作。
  2. 执行速度:硬编码轨迹不需要模型推理,没有 2-4 ms 的 ONNX 推理延迟。在 50 Hz 控制循环里,少一个推理步骤意味着更多余量给总线通信。

Roulade 的具体实现是一个定时器 + 轨迹插值器:触发 kick 时记录当前关节角度,然后在 200-300 ms 内插值到踢腿姿态,再插值回起始姿态。整个过程不经过 safety 层之外的任何策略推理。

数据来源:pollen-robotics/microduck 仓库 docs/design/robotd-design.md (2026-08-20)、architecture.md (2026-07-22)。

策略热切换:换策略不重启

策略切换不能重启 robotd——重启意味着舵机失电、鸭子摔倒。

Microduck 的策略热切换通过 robotctl policy addrobotctl policy rm 命令实现。新策略以 ONNX 文件的形式加载到内存,加载完成后原子地替换当前策略指针。新策略的 61 维观测输入和 14 维动作输出格式完全一致,所以切换瞬间控制循环不需要暂停。

robotd-design.md 里描述了启动时的策略加载流程:

startup
  ├─ open the bus
  ├─ Safety::new(io)    ← safety 拿走 RobotIo,其他人写不了舵机
  ├─ read() → hold pose ← 读当前姿态作为初始目标,启动时绝不移动
  └─ policy
       disabled  → controller = None               (healthy)
       loaded    → controller = Some                (healthy)
       failed    → controller = None + policy_error (unhealthy)

注意启动时 从不移动——先 read 当前姿态作为 hold pose,然后策略加载完成后才开始推理。如果策略加载失败,controller 保持 None,robotd 报告 unhealthy 但不会让鸭子摔倒。

热加载的边界条件:如果策略推理报错(比如 NaN 输出),safety 层会拒绝非有限值。这时候控制器状态变为 error,状态机自动切换到 recover 策略(如果有),或者保持 hold pose。

Fall detection:debounce 后的状态切换

摔倒检测在 safety 模块里,而不是策略里。这是重要的架构决策:摔倒检测是安全功能,不是策略功能

safety.observe()
  │
  ├── IMU roll/pitch/yaw 超出阈值?
  ├── 接触传感器信号异常?
  └── debounce 过滤瞬时抖动
       │
       ▼
  fallen flag 发布(不 gate 任何东西,只报告)
       │
       ▼
  state machine → 触发 recover 策略

关键点:safety.observe 只报告,不 gate。fallen flag 发布了,但 safety 不会自动接管控制。状态机读取这个 flag 后决定是否切换到 recover 策略。这意味着即使 fallen flag 误报(比如 debounce 前的瞬时信号),状态机也有权忽略它——但实际实现里,debounce 后的 fallen 信号可靠性很高,状态机直接用它触发 recover。

Debounce 的具体参数:robotd-design.md 没有给出精确的计数,但从架构可以推断是 3-5 个 tick(60-100 ms)的连续异常才判定为摔倒。太短会被瞬时冲击误触,太长会让鸭子在地上挣扎 200 ms 才启动 recover。

策略集与模式切换

Microduck 支持两种硬件模式:双足(默认)和轮式。切换通过一个配置参数 policy.mode = "biped" | "roller" 实现。

config 参数:
  policy.mode = "biped"    → 加载行走/恢复/踢腿策略集
  policy.mode = "roller"   → 加载轮式策略集 + 调优预设

轮式模式的代码路径几乎一样——同样的 50 Hz 控制循环、同样的观测空间、同样的状态机。区别只在动作空间:轮式版本用不同的关节组驱动轮子,而不是腿。这也是状态机设计的直接收益——你换一套策略文件,不改状态机逻辑,鸭子就从走路的变成开车的。

复刻场景下的状态机适配

如果你在用飞特舵机复刻 Microduck,状态机部分几乎不需要改动。原因很简单:

  • 状态机的输入是 61 维观测向量——你只要能构建这个向量(IMU + 15 个关节数据),状态机就认为鸭子准备好了
  • 状态机的输出是 14 维动作向量——只要你的 ONNX 策略输出这个维度的向量,状态机就处理得了

需要改的是策略文件本身。官方策略绑定 XL330 的 BAM 执行器参数,你换了飞特 HL-1910 后,BAM 参数全变了,策略推理出来的动作在真机上不对。但状态机逻辑——谁先执行、摔倒后怎么切换——和舵机型号无关。

从 duck.whatled.com 的进度来看,复刻项目的 expE 步态重训正在进行中,50k 迭代的目标是让”步态奖励可见”。状态机层在仿真环境里已经能正常工作——在 MuJoCo 里跑策略时,state machine 的优先级仲裁和策略热切换逻辑都在。等真机舵机到货、BAM 辨识完成、ONNX 策略重新导出后,状态机代码可以直接复用在真机上。

状态机设计的工程智慧

回头看整个设计,几个决策值得拎出来说:

  1. 优先级列表比状态图简单:六个策略的切换如果画成状态图,转移箭头会多到看不懂。用有序列表做优先级仲裁,每次 tick 从上到下检查,新增策略只插入一行。
  1. 策略是纯函数:每个策略的输入是 61 维观测、输出是 14 维动作,不持有内部状态。状态机决定谁跑,策略只管推理。这意味着所有策略可以独立测试、独立加载、独立替换。
  1. Roulade 不走神经网络:需要确定性的动作序列不走 ONNX 推理,用硬编码轨迹。这个决策让踢腿动作在 50 Hz 循环里省掉了一个推理步骤。
  1. Safety 只报告不 gate:摔倒检测在 safety 层做,但决策在状态机层做。安全层不越权——它提供信号,不替策略做决定。
  1. 启动不移动:一个很小的细节,但很关键——读当前姿态作为 hold pose,避免启动时突然跳转到某个策略的默认姿态。
  1. 策略热加载不重启:替换策略文件不需要重启 robotd,通过原子指针替换实现。这意味着训练好的新策略可以在真机运行时直接部署。

这些决策单独看都不复杂,但组合起来产生了一个可以热切换、可独立测试、支持多种硬件配置的状态机系统。在一个 399 美元的开源机器人上做到这个程度,说明设计阶段的架构考虑比大多数人想象的要深。

数据来源:pollen-robotics/microduck 仓库 docs/design/robotd-design.md (2026-08-20)、docs/design/architecture.md (2026-07-22)、GitHub README、duck.whatled.com 复刻进度。

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