在这里插入图片描述

02-50Hz控制环:robotd如何用一条串口总线驱动15个舵机

引子:机器人的"心跳"

大家好,我是黒漂技术佬。

上一篇我们解剖了 microduck 的整体架构,认识了那个 50Hz 的控制核心 robotd。今天这篇,我们要钻进 robotd 内部,看看它的核心——控制环(control loop)——到底是怎么运转的。

先理解一个物理事实:机器人不会自己待着不动。 重力时刻在拉着它,任何一个关节的角度偏差都会累积成摔倒。所以机器人的控制必须是一个持续不断的闭环:读传感器 → 算动作 → 发指令 → 再读传感器……如此往复,一刻不能停。

这个循环的每一次迭代,就叫一个控制周期(tick)。microduck 的控制周期是 20 毫秒(50Hz)。

20 毫秒意味着什么?你眨一次眼大约 100~150 毫秒,也就是说在人类眨一次眼的时间里,机器人已经完成了 5~7 次完整的"感知-决策-执行"。这个速度对于双足平衡是"够用但不宽裕"的——这决定了 robotd 里每一个函数都要为这 20ms 服务。

说句题外话:人形机器人的控制频率通常更高(MIT 的 Cheetah 能跑到 1kHz 以上),但 microduck 用 50Hz 就够——因为它是 25cm 的小个子,惯性小,对控制频率的要求天然低。控制频率不是越高越好,而是"够用就好",因为频率越高,每周期可用的计算预算越少。


一、先看总线:15 个舵机怎么共享一条串口

microduck 用的舵机是 Dynamixel 系列——这几乎是双足小机器人界的"标准件"。Dynamixel 是韩国 ROBOTIS 公司做的智能串行总线舵机,每个舵机内置 MCU、驱动、编码器,可以通过一条总线菊花链串联。

关键信息在架构文档里:

15 个 Dynamixel 舵机 + 1 个 IMU,共享同一路 UART(一条串行总线)。控制环独占这条总线。

┌──────────────────────────────────────────────┐
│                  robotd                      │
│              控制环(50Hz 独占)               │
└──────────────────────┬───────────────────────┘
                       │ 一根 TX/RX 串口线
                       ▼
┌──────────┐  ┌──────────┐  ┌──────────┐      ┌──────┐
│ Dynamixel │  │ Dynamixel │  │ Dynamixel │  …  │ IMU  │
│   #1      │  │   #2      │  │   #3      │      │      │
└──────────┘  └──────────┘  └──────────┘      └──────┘

为什么 15 个舵机能共用一条 UART?因为 Dynamixel 是半双工总线协议:每个舵机有唯一 ID,主机发指令帧时带上目标 ID,只有匹配的舵机响应。整条总线上的设备分时共享一根线——发送、接收轮流来。

这里就引出了嵌入式实时控制里一个非常经典的总线时序问题

50Hz 控制环 × 15 个舵机 = 每个周期只有 20ms,要完成 15 个舵机的读 + 15 个舵机的写。

  • 读:查询每个舵机当前角度(比如用 Read 指令);
  • 写:下发每个舵机的目标位置/速度/力矩(Write 指令)。

一个 Dynamixel 帧大约几十字节,在 1Mbps 波特率下传一个帧大约零点几毫秒。但串口是半双工的,一次只能传一个方向,所以 30 个操作按顺序排下来,一个周期内总线就占用得七七八八了。这是控制环设计中必须精打细算的预算项。

有的机器人会为每条腿配一路串口来提升带宽,microduck 只用了 1 路——这又是"够用就好"的哲学。代价是控制环的时序必须把总线带宽算进去,这反而让它的设计更值得学习。


二、控制环内部:一个 20ms 的循环里发生了什么

架构文档里 robotd 的职责清单是:

motor control(电机控制)、kinematics(运动学)、odometry(里程计)、gait policies(步态策略)、sensor loop(传感器循环)、safety(安全)。

我们把它画成一个 50Hz 的流水线,一个 tick 大致长这样:

┌──────────────────── 一个控制周期(20ms)────────────────────┐
│                                                          │
│  ① 读传感器   ──►  ② 状态估计   ──►  ③ 策略推理   ──►  ④ 安全校验   ──►  ⑤ 下发指令  │
│  (15舵机角度)     (更新里程计/     (神经网络算出      (检查关节限位、    (写Dynamixel   │
│   + IMU姿态)       姿态等)          目标动作)          温度、摔倒)        总线)        │
│                                                          │
│  ⑥ 计时等待 —— 如果提前完成,sleep 到周期边界                         │
│                                                          │
└──────────────────────────────────────────────────────────┘

几个细节值得展开:

2.1 里程计(odometry)不是独立服务

注意架构文档里这句话:

odometry 是循环内的一个 struct,不是独立服务。它的输入恰好是循环已经读取的样本。

很多机器人系统会把里程计做成一个独立的模块或进程,但 microduck 选择把它塞进控制环内部。为什么?

因为里程计需要的是"当前周期刚读到的舵机角度",而不是"可能延迟了几毫秒的别人算好的结果"。把状态估计放进控制环里,可以保证它读到的一定是最新鲜的样本,且不需要跨进程通信的开销。 这是嵌入式实时系统的经典取舍:与周期强耦合的计算,就应该住在周期里。

2.2 策略推理:神经网络在 50Hz 下跑

microduck 的步态不是写死的正弦曲线,而是神经网络策略(在仿真里用强化学习训练出来的,这个我们第 05 篇细讲)。

策略模型的输入是观察(observation)——包含关节角度、IMU 姿态、上一时刻动作等;输出是动作(action)——通常是目标关节位置或力矩。

在 RK3566(四核 A55,无 GPU 参与)上跑一个 ONNX 格式的小型 MLP 网络,单次推理大约几毫秒——完全装得进 20ms 的预算。这也是强化学习落地嵌入式的前提:模型必须小到端侧推理不吃掉整个控制周期。

2.3 安全层:在"意图"和"电机"之间

上一篇提到过,robotd 收到的是各种客户端发来的 intent(意图),比如"站起来"“往前走”。安全层就在意图和电机指令之间:

客户端意图 ──► 安全层仲裁 ──► 电机指令
                 │
                 ├── 摔倒检测(姿态异常?)
                 ├── 关节限位(角度超界?)
                 ├── 温度限制(舵机过热?)
                 └── 安全姿态(safe pose)逻辑

任何一个检查不过,意图就会被降级或拒绝。没有任何客户端能绕过这一层——本地手柄不行,远程 WebRTC 不行,脚本不行。这在机器人领域叫"权威仲裁":本地客户端可以抢占远程客户端,但谁都不能跳过安全。


三、实时性的三个铁律

微控制器社区有个老梗:“实时系统里,没有’等一下’,只有’刚好’和’来不及’。” robotd 的设计里藏着三条保证实时性的铁律,值得我们每个做嵌入式的人抄作业。

铁律一:控制环绝不阻塞

架构文档原话:

控制环绝不阻塞于其他服务;跨服务读取均为 last-value-wins 缓存,非同步 RPC。

翻译成人话:

  • robotd 需要其他服务的数据(比如 mediad 算出来的"球的位置")时,不调用阻塞式 RPC 去等结果,而是读一个"最新值缓存"——拿到什么算什么,拿不到就用上一次的;
  • 如果一个服务响应慢,robotd 不会等它,直接带着旧值继续跑。

为什么要这么极端?因为在 20ms 的周期里,一次网络阻塞可能就是 5~10ms 的延迟——控制环等不起。等一次可能没事,累积几次,机器人就摔了。

铁律二:deadman/heartbeat 心跳机制

安全章节里有一条:

deadman/heartbeat:RTT 超阈值自动停止。

如果你连着手机遥控鸭子,但手机和应用之间的链路延迟爆炸了——机器人必须能察觉"客户端失联",然后自动进入安全姿态(比如停住、坐稳),而不是在等一个永远不会来的指令。

实现上通常是一个看门狗式心跳:客户端定期发心跳包,robotd 如果超过 N 个周期没收到,就判定链路失效,触发安全停止。这跟我们做无人售货柜设备与服务器的断线重连逻辑异曲同工,只不过机器人这边"断线后果"更严重——是物理性的摔倒。

铁律三:每个状态只有一个写者

架构文档的四条不变式(invariants)里有一条:

单写者每状态(single-writer per state)。

任何一个共享状态(关节角度、机器人姿态、任务状态),在任意时刻只能有一个进程/模块负责写入。多写者必然带来竞态和不确定性——在实时系统里,竞态不是 bug,是事故。

这和分布式系统的"单一权威"思想一脉相承,在 robotd 内部体现为:机器人状态只由控制环更新,其他模块只读。


四、可观测性:实时系统怎么"看病"

一个 50Hz 的系统,出了问题往往一闪而过——你没法"暂停"它来调试。所以 robotd 对可观测性的重视是刻进骨子里的:

手段说明
结构化日志各服务 stderr → journald(systemd 日志),首行统一格式 WARN starting service=... exe=...
健康状态接口robotd 暴露 robot.health,一个命令/一次调用就能看全机健康
robotctl health命令行一键体检,非零退出码可以直接做脚本门控
robotctl version报告四维版本信息(系统/安装/运行/回滚),判断状态分歧

“能不能一命令看出所有东西的状态”——这是判断一个嵌入式系统工程成熟度最直观的标尺。很多项目代码写得花团锦簇,可现场排障时连"现在跑的是哪个版本、哪个服务活着"都查不清楚,那就是可观测性欠债。microduck 在这点上是个模范生。


五、我们能学到什么?

这一篇的技术点,落到我们自己的项目上,可以提炼成四条可迁移经验:

  1. 控制频率 = 需求驱动,不是越大越好。 先算清楚物理系统的惯性时间常数,再定频率;频率定下来,每个周期能用的计算预算就定了,所有优化都要在这个预算里做。
  2. 与周期强耦合的计算住进周期里。 里程计、状态估计这种"必须用最新样本"的模块,别拆成独立进程,放循环里最稳。
  3. 安全执行权要收敛。 在安全关键系统里,唯一能"动手"的模块必须是受控的、可审计的;其他一切模块只能"提建议"(发意图)。这是 robotd 整个架构的魂。
  4. 实时系统必须自带心跳和看门狗。 不管是机器人还是设备端,凡是"物理后果严重"的链路,都要有超时即停的兜底。

小结

robotd 的 50Hz 控制环,本质上是一个**"在 20ms 预算里完成感知-决策-执行闭环"的工程**。它的精彩之处不在某一行代码,而在于每一个设计决策都服务于同一个目标:让这个 20ms 的循环稳定、安全、可观测地永远跑下去。

  • 15 个舵机 + IMU 挤在一条 UART 上 → 逼出了对总线时序的精打细算;
  • 神经网络策略跑在 50Hz → 逼出了"模型必须小到装得进预算"的约束;
  • 多客户端控制 → 逼出了 intent + 安全层的权威仲裁架构;
  • 实时性要求 → 逼出了"绝不阻塞 + last-value-wins + 单写者"三条铁律。

资源受限不是缺陷,而是设计的催化剂。 这句话在 microduck 身上体现得淋漓尽致。

下一篇,我们从 robotd 走出来,看看它周围那 6 个守护进程是怎么通过 Unix socket 上的 JSON-RPC 协作成一个"微服务生态"的。

我是黒漂技术佬,咱们下篇见。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐