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

Microduck 的安全层设计:为什么一只鸭子摔了不会烧舵机

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

Microduck 的安全层设计:为什么一只鸭子摔了不会烧舵机

做机器人控制的人都知道一个恐怖场景:策略跑着跑着突然输出了一个 NaN,舵机瞬间收到一个离谱的目标位置,电机全力冲过去——然后堵转,电流飙到峰值,线圈烧了。800克的鸭子不算大,但14个舵机换一遍也不便宜。

Microduck 的 robotd 里有一道安全层,卡在策略输出和总线写入之间。它不是什么花哨的AI安全框架,就是几条硬规则,但每条都是踩过坑之后加上的。

safety.apply:最后一道门

robotd 的控制循环每秒跑50次,数据流是这样的:读总线拿到传感器数据,构建61维观测向量,扔给ONNX策略推理,拿到14维动作输出,加到home pose上得到目标位置,然后写回总线。

关键在于,策略输出的目标位置不是直接发给舵机的。它们要先过 safety.apply。

safety 持有整个系统唯一的 RobotIo——这是Rust借用检查器强制的,不是靠程序员自觉。意味着策略、控制器、客户端,谁都能提出目标,但谁都不能直接写总线。只有 safety 能。

这个设计的好处是什么?就算策略输出了垃圾值,safety 也能在写入总线之前拦住。Rust的所有权机制保证了这一点——编译器不允许任何其他代码路径拿到 RobotIo,不是运行时检查,是编译时就过不了。

三条硬规则

safety.apply 做的事情其实很少,但每条都是针对真实故障模式的。

第一,拒绝非有限值。 策略输出 NaN 或者 infinity 的情况比你想象的常见。ONNX 推理在某些边界条件下会产生 NaN——比如输入了一个训练时没见过的观测向量组合,归一化之后某个值溢出了。没有这道检查,NaN 会被转换成舵机目标位置0(因为NaN转整数的行为),舵机瞬间转到极限,机械结构受力突变。safety 检测到非有限值就拒绝写入,保持上一帧的目标位置不变。

第二,clamp到执行器范围。 每个关节都有自己的物理范围,hip_yaw 能转的角度和 knee 不一样。策略输出的是归一化的动作值(大致在-1到1之间),乘上scale加到home pose上之后,结果可能超出关节的物理极限。safety 把每个目标位置 clamp 到该关节的实际范围内。这不算什么高级操作,但它是”最后一道防线”——如果上层的scale设置错了,这里能兜住。

第三,不拦截摔倒。 这条有点反直觉。safety 知道机器人摔了(通过IMU数据和触觉传感器判断),但它不会在摔倒时冻结电机或者切到某个”安全姿态”。它只是报告摔倒状态,让策略自己处理。

为什么不拦截?因为冻结电机可能更危险。机器人摔倒的过程中,关节如果突然锁死,撞击地面的冲击力会全部作用在机械结构上,可能把支架或者齿轮搞坏。让策略继续输出——一个好的策略会在摔倒后执行standup动作,自己爬起来。Microduck 有专门的 standup 策略,通过状态机切换进来。

safety 的角色是”报告而不是干预”。它告诉你摔了,但不替你做决定。

低通滤波:不是 safety 的一部分但跟它配合

策略输出到目标位置之间有一个低通滤波器,作用在头部和腿部的关节上。这不是 safety 层的东西,但跟安全密切相关。

RL 策略训练时没有任何滤波——AGENTS.md 里明确写了”policies are UNFILTERED”,训练时加滤波会导致 sim2real 传输失败(训练时有滤波、部署时没有,或者反过来,都会出问题)。滤波是在部署时加的。

为什么要加?ONNX 策略在50Hz下推理,每个tick都可能输出一个跟上一帧差距不小的目标位置。舵机收到突变的目标位置会快速加速,产生抖动。低通滤波把这个突变平滑掉。

关键约束是:训练时不加,部署时加的滤波参数必须固定且经过transfer测试。AGENTS.md 里有一条硬规则——”Don’t add EMA filtering without a matched runtime flag and a transfer test”。随便调滤波参数是 sim2real 翻车的经典路径。

deadman 开关:客户端断了怎么办

robotd 接收的命令来自多个客户端:手柄(padd)、手机APP(通过btd的BLE)、远程控制(通过mediad的WebRTC)。每个客户端发送 intent——”走这么快”、”头转这边”、”站起来”。

如果客户端断连了怎么办?比如手柄没电了,或者蓝牙断了。

robotd 有一个 deadman gate。客户端必须持续发送命令,如果一段时间没收到新的 intent,gate 会把命令回退到 nominal——速度归零,头部姿态回到默认。机器人会停下来,不会继续按上次给的命令走下去。

这个设计的前提是:命令是”意图”不是”状态”。客户端不是说”把关节设到这个角度”,而是说”我想往这个方向走”。意图过期了就丢弃,机器人回到安全状态。这也是为什么所有客户端发送的是相同结构的 intent 而不是直接的关节目标——padd 读手柄发的 intent,跟手机APP发的 intent,结构完全一样,只是传输路径不同。

总线层面的保护

safety 层之外,Dynamixel 协议本身也有保护机制。XL330 舵机有最大扭矩限制和温度保护,固件层面会在过热时降低扭矩输出。但 Microduck 的软件设计没有依赖这些——safety.apply 的拦截发生在总线写入之前,是软件层面的第一道防线。

总线只有一个端口(/dev/ttyS2,1Mbps),所有15个舵机和IMU板都挂在上面。robotd 用 TIOCEXCL 独占这个端口,加上 advisory lock 防止其他进程抢总线。如果 robotd 挂了,updaterd 想重启它之前会先问 robot.safeToRestart——确认当前状态是否安全可以重启。如果机器人正在执行动作,重启可能导致总线状态不一致,updaterd 会等待。

从架构看安全设计哲学

Microduck 的安全设计有一个清晰的分层思路。

最底层是硬件保护:舵机固件的过热保护、扭矩限制。这层不可控,只能配置。

中间层是 safety.apply:软件层面的数值检查和范围限制。这层是 Rust 编译器保证的独占写入,不依赖运行时检查。

最上层是策略层面的安全:standup 策略、状态机切换、deadman gate。这层处理的是”机器人应该做什么”而不是”数值是否合法”。

三层各管各的,互不重叠。safety 不试图判断”这个动作是否安全”——它只检查数值是否有效、范围是否合法。是否应该在摔倒时继续执行动作,那是策略的判断。这种分离让 safety 层极其简单,几百行代码,容易验证,不容易出bug。

在机器人安全设计里,有个常见的错误是试图在一个层做所有事情。比如在安全层加”摔倒检测+紧急停止”,听起来合理,但紧急停止本身可能造成新的安全问题(冲击力、机械损伤)。Microduck 的做法是让安全层只做它能做好的事——数值检查和范围限制——把行为决策留给专门的行为层。

对做机器人开发的人来说,这套设计有几个可以直接借鉴的点。用语言的所有权机制强制单一写入路径,比靠纪律管用得多。安全层做最少但最关键的事,不越界做行为决策。客户端命令设计成意图而不是状态,过期就丢弃。这三条放在任何小型机器人项目上都适用。

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