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

Microduck 的总线拓扑:15 个舵机和 1 个 IMU 怎么挤在一条 UART 上

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

Microduck 的总线拓扑:15 个舵机和 1 个 IMU 怎么挤在一条 UART 上

做过机器人总线设计的人都知道,设备多了麻烦就多。地址冲突、总线拥塞、读取延迟——每个都能单独搞死你的控制循环。Microduck 的方案简单粗暴:一条 UART,1 Mbps,挂在上面 16 个设备,50 Hz 循环每个 tick 全读一遍再全写一遍。今天拆开看这条总线怎么做到不堵。

一条线,16 个设备,没有第二条

Microduck 的硬件架构里只有一个串口:/dev/ttyS2,Dynamixel Protocol v2,波特率 1 Mbps。这条线上挂着 15 个 XL330 舵机和 1 个 IMU 板,总共 16 个设备。没有第二条总线,没有独立的 IMU SPI 或 I2C 通道。

设备 ID 分配是有讲究的:

  • ID 200 — IMU 板(imu_to_dxl v2)。排在 ID 向量的最前面,因为它的响应在 sync_read 里最先返回。
  • ID 20-24 — 左腿,5 个舵机。
  • ID 30-34 — 颈部、头部、嘴部,5 个舵机。
  • ID 10-14 — 右腿,5 个舵机。

这个分组不是随便排的。左腿和右腿的 ID 段(10-14、20-24)隔开了一段距离,避免相邻物理设备在总线上的地址也相邻——这在调试时有点用,你能通过 ID 范围快速定位是哪条腿出了问题。头部舵机放在 30-34 段,夹在两条腿之间,跟物理布局对应。

IMU 怎么上了舵机总线

这是 Microduck 硬件设计里最聪明的一个决定。大多数机器人方案里,IMU 走的是独立的 SPI 或 I2C 总线,跟舵机总线物理隔离。好处是互不干扰,坏处是你需要两个独立的读取路径,两条总线的时序不同步,IMU 数据和关节位置的采样时间戳之间永远有一个偏差。

Microduck 的 v2 imu_to_dxl 板做了一个不一样的事:它把 IMU 做成了 Dynamixel 总线上的一个设备。这块板子上面有一颗 IMU 芯片,但对外暴露的是 Dynamixel Protocol v2 接口。它的 ID 是 200,响应跟舵机一样的 sync_read 指令。

关键在于数据格式。舵机在 sync_read 时返回的是位置、速度、力矩、温度这些寄存器值。imu_to_dxl 板返回的是 SFLP 四元数——一个 4 分量的姿态表示,从同样的寄存器块里读出来。对 robotd 来说,读 IMU 和读舵机是同一个代码路径,同一个 sync_read 调用,同一个时间戳。

这意味着什么?每个 tick 的 16 次读取是在一个总线事务里完成的。IMU 的四元数和 15 个舵机的关节角度是同一批数据,采样时间差在微秒级别。策略网络拿到的 observation 是严格时间对齐的——关节角度和躯干姿态来自同一个瞬间。这一点对 sim2real 迁移至关重要:训练时仿真器每一步同时生成所有观测,部署时如果 IMU 和关节位置的采样时间差了几毫秒,策略的输入就跟训练分布不一致了。

一个 tick 的总线时序

50 Hz 循环每 20 毫秒完成一次 read-decide-write。这 20 毫秒里总线要完成多少事?

读取阶段:一个 sync_read 指令同时寻址 16 个设备。Dynamixel Protocol v2 的 sync_read 是广播式读取——主机发一条指令,所有匹配 ID 的设备在同一次事务里返回数据。16 个设备的返回数据按 ID 顺序排列,不需要 16 次独立的 query-response 往返。

单次 sync_read 的总线时间粗算:指令包大概 20 字节,16 个设备的返回包每个大概 20 字节,加上协议开销和帧间间隔,总共约 400 字节的数据量。1 Mbps 的波特率下一个字节传输需要 10 个 bit(start + 8 data + stop),400 字节就是 4000 个 bit,约 4 毫秒。实际时间会稍微长一点,因为还有设备间的响应延迟和总线切换时间。

写入阶段:sync_write 同时向 15 个舵机发送目标位置。数据量类似,大约 3-4 毫秒。

读取加写入总共约 7-8 毫秒,剩下 12 毫秒留给策略推理和安全层检查。RK3566 跑一个 ONNX MLP 推理不到 1 毫秒,安全层的 clamp 和限幅检查也是微秒级。所以 50 Hz 是有裕量的,不是卡着边界跑。

总线独占权:为什么 serialport 的 TIOCEXCL 不够

robotd 在打开 /dev/ttyS2 时设置了 TIOCEXCL 标志。这个标志会让后续的非特权 open 返回 EBUSY,看起来像是独占了串口。但问题在于 robotd 以 root 权限运行——舵机控制需要访问字符设备。而 TIOCEXCL 不拦 root。

这意味着如果有另一个 root 进程打开同一个串口,它不会被 EBUSY 拒绝。两个进程同时往 Dynamixel 总线发指令,帧会碰撞,舵机收到的是乱码,整个总线挂掉。

Microduck 的解法是 advisory lock——咨询式文件锁。robotd 启动时先拿 <socket>.lock 文件锁,这个锁通过内核的 flock 机制实现。另一个进程(比如 robotd init 命令行工具)在打开串口前必须先拿这个锁。锁是协作式的——只有遵守规则的程序才会检查它。不遵守的程序直接 open 串口还是会成功,但这在生产环境里不是问题,因为板上只有 robotd 和 init 工具会碰串口。

设计文档里特别标注了一个历史 bug:Armbian 默认在 UART2 上跑 serial-getty@ttyS2——一个 login 控制台。这个 agetty 进程持有串口,导致 robotd 看不到舵机。排查方法是 fuser -v /dev/ttyS2,如果输出里看到 agetty,就是这个问题。setup 脚本现在会自动 mask 掉这个 systemd unit,但这个坑在每次刷系统后都可能重新出现。

IMU 数据怎么进入观测向量

IMU 板返回的 SFLP 四元数是 4 个 float:[w, x, y, z]。robotd 收到后不直接把四元数塞进观测向量——策略网络不需要完整的四元数,它需要的是”重力在机器人坐标系下的投影”。

这个投影是 projected_gravity,一个 3 维向量。计算方式是把世界坐标系下的重力向量 [0, 0, -1] 用 IMU 四元数的逆旋转到机器人坐标系。直观理解:机器人直立时 projected_gravity 是 [0, 0, -1]——重力指向脚底。机器人前倾时 z 分量变小,x 分量变成负值——重力向量往前偏了。

这个 3 维向量就是策略网络从 IMU 拿到的全部信息。不是完整的角速度,不是加速度——就是重力投影。为什么够用?因为双足行走的核心稳定性指标就是躯干姿态,而 projected_gravity 编码了躯干相对于重力的方向。倒立摆模型里你需要的也是这个量——重心投影在脚底支撑多边形里就是稳定,出去了就要摔。

cheatsheet 里提到 fallen 判定也是从 projected_gravity 来的。判定逻辑很直接:projected_gravity 的 z 分量低于某个阈值就判定为摔倒。这意味着”摔倒”不是通过加速度冲击检测的,而是通过持续的姿态偏移检测的——短暂的前倾(走路时的正常摆动)不会触发,但持续的前倾(真的要摔了)会。

为什么不独立给 IMU 一条总线

从工程角度看,把 IMU 放在舵机总线上有几个好处:

省了一条总线和对应的驱动代码。RK3566 的 UART 数量有限,省下一个就能接别的东西。代码层面,sync_read 的代码路径只有一条,不需要同时管理两个不同协议的总线。

时序同步是自动的。两个独立总线意味着两个独立的采样时钟。IMU 跑在 SPI 上可能是 1 kHz,舵机在 UART 上是 50 Hz,两者的时间戳通过软件对齐。软件对齐永远不完美——哪怕你用了 hardware trigger,SPI 的中断响应延迟和 UART 的 DMA 完成延迟之间总有几微秒到几十微秒的差。在 Microduck 的方案里这个差是零。

坏处也有。总线上有 16 个设备时,单个设备出问题可能拖累整条总线。一个舵机的响应变慢,sync_read 的超时就会触发,整个 tick 的数据可能部分缺失。Microduck 的安全层对此有兜底:stale-read counter 记录每个设备连续多少次没有响应,health 报告里会显示 orientation frozen — 25 stale reads 这种信息,告诉你 IMU 已经 25 个 tick 没更新了。

另一个坏处是带宽限制。1 Mbps 的 Dynamixel 总线对 16 个设备来说够用,但如果要加更多传感器——比如在总线上再挂一个力矩传感器——带宽就开始紧张了。Microduck 的 ToF 深度传感器就没走这条路,它有自己独立的 I2C/SPI 通道和独立的 tofd 守护进程。

总线设计的可复用经验

Microduck 的总线方案对小型机器人设计有几个直接参考:

减少总线数量比增加带宽重要。 一条总线 16 个设备、1 Mbps,看起来很挤,但 sync_read 的批量读取效率比 16 次独立读取高得多。两条总线看起来更宽松,但同步问题带来的软件复杂度不值。

把传感器做成总线设备。 imu_to_dxl 板的设计让 IMU 跟舵机用同一套驱动代码、同一个读取路径。这不是”统一抽象”的软件设计,是”统一协议”的硬件设计。软件层面零特殊 case。

时序同步靠硬件,不靠软件。 如果你的机器人在 sim2real 时有莫名的姿态抖动,检查一下 IMU 和关节位置的采样时间差。差超过 1 毫秒就能让策略行为变形。把它们放在同一条总线上是最简单的解法。

advisory lock 够用,别过度设计。 Microduck 没有用内核级别的设备独占机制,就是 flock 加上一个 socket lock 文件。在嵌入式 Linux 上,你能控制的进程是有限的,不需要做成通用操作系统的权限系统。

数据来源

  • GitHub: pollen-robotics/microduck — docs/design/robotd-design.md(总线拓扑图、ID 分配、TIOCEXCL 与 advisory lock、imu_to_dxl v2 板设计)
  • GitHub: pollen-robotics/microduck — docs/design/architecture.md(七守护进程架构、robotd 唯一触碰总线、tofd 独立 I2C/SPI 通道)
  • GitHub: pollen-robotics/microduck — docs/robot/cheatsheet.md(projected_gravity、fallen 判定、stale-read counter、robotctl monitor 输出格式)
喜欢 (0)赏
[🍬谢谢你请我吃糖果🍬🍬~]
分享 (0)
关于作者:
少将,关注Web全栈开发、项目管理,持续不断的学习、努力成为一个更棒的开发,做最好的自己,让世界因你不同。