- 智能硬件
- 机器人
- 嵌入式
- 物联网
【免费下载链接】oomwoo
Open-source vacuum robot cleaner
OOMWOO 是一台开源的 ROS2 扫地机器人(架构说明 定义其 CPU/MCU 双处理器拆分:CPU 负责 ROS2/SLAM/Nav2,STM32G473 MCU 独占电机、编码器、传感器与硬安全)。本文聚焦 Creative-Dhanush 的 MCU I/O 固件贡献:它用一台笔记本把"CPU↔MCU 链路"端到端跑通——ROS 2 栈驱动真实的 MCU 安全逻辑、走真实的二进制线格式,且当 ROS 2 一侧被 kill 时,MCU 会自行刹停电机。读完本文你将掌握:如何在纯软件环境复现这套演示、固件五个核心翻译单元各自承担什么职责、54 项测试与差分模糊测试如何证明线格式正确,以及"MCU 独占安全、刻意保持简单"这一安全立场如何在源码层面落地。
本文主体内容来自 contributions/mcu-io-firmware/Creative-Dhanush/README.md,并辅以同仓库的 CPU/MCU 串口契约草案、ROS 2 映射草案 与参考编解码实现 oomwoo_mcu_frame.py 作纵深佐证。
贡献模型:两个仓库,各自独立 CI
按本模块的贡献模型,该实现拆成两个仓库,MCU 侧与 CPU 侧各自持有 CI:
| 仓库 | 职责 | CI |
|---|---|---|
| oomwoo-io-firmware | MCU 侧:帧编解码(frame codec)、流式解码器、安全状态机、链路层,以及一个 MCU 模拟器 | firmware CI |
| oomwoo-mcu-bridge | CPU 侧:oomwoo_mcu_bridgeROS 2 节点(由 ros2_mapping.md 起草并命名) | bridge CI |
开始阅读其余内容前,请先明确一个边界:已被证明的是——线格式相对一个独立实现是正确的、解析器能扛住损坏与分片输入、安全策略在每种指定条件下行为正确——这些全部发生在笔记本上;尚未被证明的是——与真实芯片相关的任何事:没有实测反应时间、没有中断优先级、没有证据表明一个挂死的 Arduino 级任务无法击穿切断逻辑。那是 模块里程碑 6,需要一块 Nucleo。这是安全系统脚下的地基,而不是安全系统本身。
眼见为实:在笔记本上让整条链路跑起来
运行前提:Linux(模拟器依赖openpty)与 ROS 2 Jazzy。把两个仓库克隆到相邻目录后:
# MCU 侧 —— 把安全核心编译成宿主二进制,跑在伪终端上 git clone https://github.com/Creative-Dhanush/oomwoo-io-firmware pip install -U platformio (cd oomwoo-io-firmware && pio run -e native_sim) # CPU 侧 —— ROS 2 桥接节点 git clone https://github.com/Creative-Dhanush/oomwoo-mcu-bridge cd oomwoo-mcu-bridge && colcon build && source install/setup.bash ros2 launch oomwoo_mcu_bridge demo.launch.py \ sim_binary:="$PWD/../oomwoo-io-firmware/.pio/build/native_sim/program"launch 文件会启动模拟器、创建 pty,并驱动桥接节点走完configure→activate,因此稳定下来时心跳已在运行、遥测已在流动。用ros2 lifecycle get /oomwoo_mcu_bridge应看到active。
驱动它、破坏它,看谁刹停机器人
| 操作 | 现象 |
|---|---|
ros2 run teleop_twist_keyboard teleop_twist_keyboard | 轮子转动,/joint_states与/odom前进 |
向模拟器 stdin 输入cliff left on | /oomwoo/io/cliff→ 1,轮子停止,/cmd_vel无法覆盖它 |
输入cliff left off | 仍然停止——cliff 闩锁不会自清除 |
ros2 service call /oomwoo/io/clear_faults std_srvs/srv/Trigger | 故障释放,运动恢复 |
| kill 桥接节点 | 心跳停止 →MCU 自行刹停电机,没有任何 ROS 2 侧指令要求它这么做 |
最后一行就是设计本身,而不是兜底方案。其契约依据见 CPU/MCU 串口契约草案 的时序规则:CPU 以 20–50 Hz 发布HEARTBEAT,MCU 在错过心跳后 150 ms 硬停;契约"失败行为"表也明确规定"CPU 心跳超时 → 停止驱动与清洁电机,发出CPU_HEARTBEAT_TIMEOUT"。
此刻实际在运行的拓扑
Nav2 / recovery / jobs ow_sim_mcu (今天,一台笔记本) │ real I/O board (将来,协议不变) ▼ ▲ oomwoo_mcu_bridge ──── OW binary frames, CRC-16 ──────┘ │ (现在是 pty,将来是 UART) ▼ /joint_states /odom /battery_state /oomwoo/io/* /diagnostics注意ow_sim_mcu上方的"real I/O board (later, unchanged protocol)"——设计意图是模拟器与真实板卡之间协议不变,桥接节点无需改动即可对接真实硬件。
为什么说它是 drop-in 替换
模拟器是 oomwoo-install 中换行分隔 JSON 桩(ubuntu/tools/oomwoo_sim_mcu_serial.py)的直接替代品——同样的--link、--period、--battery-mv参数——但它说的是真正的契约。而且它不是固件的模型:它运行的就是固件本身。ow_frame/ow_stream_decoder/ow_safety_core这三个翻译单元,与交叉编译到 STM32G473 的是同一份代码。只有一份实现,所以模拟器与目标板永远不会漂移。
已实现的模块逐一看
| 模块 | 用途 |
|---|---|
ow_frame.{h,cpp} | CRC-16/CCITT-FALSE、小端辅助函数、全部 12 种已定义消息类型的编解码——逐字节构建,绝不通过 struct 做memcpy,从而绕开 C++/struct.pack的结构体填充(padding)不一致问题 |
ow_stream_decoder.{h,cpp} | 固定缓冲流式解码器:跨多次读取缓冲不完整帧,损坏后逐字节重新同步 |
ow_safety_core.{h,cpp}+ 设计文档 | 安全状态机——心跳超时、设定点过期、bumper/cliff/wheel-drop/过流/e-stop、闩锁故障 |
ow_link.{h,cpp} | 把上述三者绑成一个可用的 MCU:字节进 → 策略 → 成帧字节出,带出站排序。不拥有任何传输,这正是同一份代码既能零 I/O 单元测试、又能跑在 pty 上、还能交叉编译到目标板的原因 |
ow_telemetry.{h,cpp} | 按 ros2_mapping.md 要求的 50–100 Hz 周期性FAST_TELEMETRY |
ow_sim_mcu | 跑在伪终端上的 MCU,支持 stdin 故障注入 |
oomwoo_mcu_bridge | rclpy lifecycle 节点:9 个订阅 + 9 个发布接口、四种规定的 lifecycle 状态,以及仲裁钳位(clamp) |
关键设计:桥接节点直接 import 参考编解码器
桥接节点原封不动地 importxbattlax 的oomwoo_mcu_frame.py,而不是重新实现一份。这样一来,链路两端共享同一个线格式定义,而不是对契约的两次独立解读。该参考编解码器正是 CPU/MCU 串口契约草案 所述"给固件、ROS2 与测试工作一个共享可执行参考"的实现,其帧头布局为:
offset size field 0 2 magic: ASCII "OW" 2 1 protocol version: 1 3 1 flags 4 2 sequence number 6 2 message type 8 2 payload length 10 N payload 10+N 2 CRC-16/CCITT-FALSE over header + payloadmagic 字节计入 CRC;解码器必须拒绝版本错误、长度不可能或 CRC 错误的帧;流式解码器可以把噪声丢弃到下一个OWmagic 为止。而ow_frame在 C++ 侧逐字节构建帧、从不通过结构体memcpy,正是为了规避 C++ 结构体填充与 Pythonstruct.pack的 padding 不一致——这是两端共享同一份格式定义时必须处理的实际工程问题。
证据:CI 里 2 分钟跑完的验证矩阵
以下所有检查都在每次 push 时的 CI 中运行,约 2 分钟,跑在一台非作者所有的机器上:
| 检查 | 结果 |
|---|---|
帧编解码、流式解码器、安全核心、链路回环(pio test -e native,ASan+UBSan) | 54 项测试——10 / 9 / 15 / 20 |
| 金样本帧向量从上游参考再生成,字节级一致 | pass |
| 差分模糊测试:流式解码器 vs Python 参考 | 2000 例,0 失配 |
| pty 上的模拟器,每个字节都由上游参考解码并重新编码 | 6 例,字节精确 |
| 桥接节点组帧单元测试 | 10 |
| 桥接 ↔ 模拟器端到端,只断言 ROS 2 话题 | 5 |
pio run -e nucleo_g474re交叉编译 | pass |
两项最不该被伪造的检查
- 线格式检查以上游参考实现为 oracle。模拟器发出的每一帧,都被 vendored 的上游参考解码、重新编码,并要求与原字节完全一致。"说的是真实协议"这件事是被检验的,而不是被断言的。
- 回环测试套件全程走线。与安全核心测试直接把已解码帧交给
SafetyCore不同,test/test_link里的每个测试都以字节进入、以字节离开——因此组帧、CRC、排序或载荷打包的任何缺陷都会让测试失败。
桥接节点 CI 日志原文(逐字):
/cmd_vel moved the wheels 0.000 -> 6.743 rad, odom x=0.236 m cliff latched; /cmd_vel could not override it and the sensor clearing did not release it /oomwoo/io/clear_faults released the latch and motion resumed heartbeat stopped on deactivate and the MCU stopped the wheels by itself 5/5 passed这与上面演示表的每一行一一对应:/cmd_vel驱动轮子、cliff 闩锁无法被覆盖、clear_faults释放闩锁、deactivate 后 MCU 自行刹停。
立场:MCU 拥有的安全,留在 MCU,而且刻意保持简单
这一节是该实现的架构宣言,可逐一在源码结构中找到印证:
- 安全核心只会在故障时停止执行器。它不后退、不扫描、不重规划、不决定机器人下一步去哪。导航、建图以及一切需要"完整"上下文的工作都留在 CPU 上(这正是 #49 讨论中 @kaiaai 问过的拆分——在这里通过"核心没有任何办法命令运动,只能扣住运动"来强制落实)。
- 闩锁故障不自清除。Cliff、wheel-drop 与 e-stop 只在显式
CLEAR_LATCHED_FAULT后释放。一条"悬崖传感器一闪就立即恢复行驶"的机器人,正是这个设计排除的失败模式。wheel-drop 是唯一刻意的例外——它随轮子重新触地而清除,与契约的字面措辞一致。对应消息见契约最小消息目录:0x0003 CLEAR_LATCHED_FAULT(u16 fault_mask)与0x8002 SAFETY_EVENT(u16 event, u8 active, u16 detail)。 - 过流只停受影响的电机,并在
SAFETY_EVENT.detail中报告是哪一台,而不是一刀切全停。契约中事件 7(BRUSH_OVERCURRENT,停止受影响的刷、detail 带刷 ID)与事件 8(FAN_OVERCURRENT,停风扇、驱动交由 MCU 策略)正是这一行为。 - 每个入口点都把当前时间作为参数传入,从不读取时钟——所以一个 150 ms 的超时可以在恰好 149 ms 和 151 ms 处确定性测试,无需任何 sleep。这与契约"MCU 硬停 150 ms"草案一致,且时间作为参数的设计让边界测试不依赖调度器。
- 核心内无堆、无异常、无 Arduino 头文件。
operator new在编译期被删除。同一份源码既能以普通宿主 C++17 构建,也能构建为目标固件。
安全状态如何重建:SAFETY_STATE与兼容字段
契约在实现两端之后新增了0x8005 SAFETY_STATE(u32 timestamp_ms, u16 active_flags, u16 latched_flags)作为权威的周期性安全快照:安全事件码 N 映射到位 N−1,当前 1–10 号事件因此恰好塞进一个u16。旧的FAST_TELEMETRY.safety_latched_flags单字节保留为低 8 位兼容字段,但新桥接节点必须用SAFETY_STATE重建完整的 active/latched 状态;未知高位必须保留用于诊断,并视为抑制运动,直到其含义被明确。oomwoo_mcu_frame.py中safety_event_flag()返回1 << (event - 1),正是这条位映射的可执行表达。
启动进展与尚未开始的部分
对照 模块里程碑:
| # | 里程碑 | 状态 |
|---|---|---|
| 1 | 在 G473 开发板上 Blink + SWD + 串口回显 | 未开始——需要硬件 |
| 2 | CPU 串口链路:组帧 + 健康/看门狗握手,回环测试 | 宿主端完成 |
| 3 | 一个驱动电机,闭环 | 未开始 |
| 4 | 全部执行器 | 未开始 |
| 5 | 全部传感器 | 未开始 |
| 6 | ISR 级安全、IWDG、实测最坏反应时间 | 未开始——策略已存在,ISR 与定时工作尚未做 |
| 7 | 充电监管 | 未开始 |
| 8 | 与 CPU 或模拟 MCU 串口工具联调 | 模拟 MCU 一半已完成,ROS 2 桥接节点端到端驱动它 |
里程碑 3–7 仍开放、无人认领。这里也没有任何东西排斥不同的固件框架——线契约就是接口,所以一个能说这种协议的 Zephyr MCU 可以直接搭配此桥接节点,无需改动。
未实现事项,直说
- 没有 STM32 HAL 或板级 bring-up。以上全部是可在宿主上测试的逻辑。
- 没有电机 PWM、电机电源使能 GPIO、充电或 IWDG。安全核心发出的是意图(stop、
SAFETY_EVENT、NACK);由调用方把它们接到硬件上。任何电机负载都不应接在这里。 - 没有实测最坏反应时间。模拟器时序是抢占式调度下的笔记本时序,说明不了真实器件。
- 编码器与轮距常量是占位符,并在源码中如实标注,因为 SPEC.md 尚未定齿轮箱减速比与编码器分辨率。模拟器的里程计距离无意义;方向、符号以及"安全说停就停"这个事实有意义。
POWER_TELEMETRY与MCU_DIAGNOSTIC在契约中没有载荷布局,这正是/battery_state的充电字段、/oomwoo/io/mcu_status与四个/oomwoo/dock_ir/*话题发不出来的原因。这些话题选择不发布,而不是塞入看似合理的值——在标准 ROS 2 话题上给一个编造的battery.percentage,比缺失一个更糟。参考实现里这两个消息在 conformance 清单中以payload_status: open保留,与此一致。
同时实现两端挖出的契约缺口
同时按同一份文档实现 CPU 侧与 MCU 侧,产出了这些有价值的发现(接口契约现已全部解决,同时保留既有 protocol-v1 帧):
- 完整安全快照:
SAFETY_STATE携带 16 位 active 与 latched 掩码。旧FAST_TELEMETRY.safety_latched_flags单字节保留为兼容字段,事件 9、10 在权威周期快照中呈现。 - 迟到的 CPU 接入:
IDENTIFY_REQUEST(0x0004,空载荷)让 CPU 在每次连接/重连后请求一份全新的MCU_HELLO,无需武装输出或刷新心跳。契约明确:请求不武装输出、不刷新心跳、不重放此前命令,MCU 在启动时仍自发发出MCU_HELLO,并在每个合法 identify 请求后重新发出。 - 拆分的 magic:参考
StreamDecoder现在保留结尾孤立的一个O,并有回归测试证明当W在下一次读取到达时能完成整帧。这与 oomwoo_mcu_frame.py 中StreamDecoder.feed()的逻辑吻合:缓冲以MAGIC首字节结尾时保留该字节,等待下一块数据补齐。
实现方仍需采用两个新消息 ID 后,才能移除各自的临时 workaround。未定义的POWER_TELEMETRY、MCU_DIAGNOSTIC与 dock-IR 载荷仍是独立的契约决策,与板级 bring-up 测量绑定。
关于CPU 心跳超时:维护者在 #49 中答复约为5 分钟,用于 CPU 启动时间。这是与稳态心跳超时不同的定时器——后者契约仍标记为草稿("100、150 还是 250 ms?")——因此稳态值是一个Config字段,默认 150 ms,而不是硬编码常量。启动通过结构而非长超时处理:启动或重连后执行器保持禁用,直到一份新鲜的HEARTBEAT且一份新鲜的DRIVE_SETPOINT都到达——所以无论数值是多少,一个慢启动的 CPU 都无法产生运动。一个待决的小问题:MCU 当前在启动约 150 ms 后就会报告心跳超时,而此时 Linux 还在起来。由于运动已被门控而无害——但如果意图是"直到 CPU 说过话之前保持安静",那是一个小改动,值得决定。
深入仓库继续验证
- 契约与消息目录全文:CPU/MCU 串口契约草案
- ROS 2 话题/服务映射、QoS 与生命周期行为:ROS 2 映射草案——桥接节点的 9 订阅/9 发布、四种 lifecycle 状态与仲裁钳位均由此定义
- 参考编解码器:oomwoo_mcu_frame.py(CRC-16/CCITT-FALSE、12+ 消息的 pack/unpack、
StreamDecoder) - 一致性工具包:conformance 目录(
protocol_v1.json、金样本向量、C11/C++17 验证器) - ROS 2 硬件桥接草案:SOFTWARE_INTERFACES.md §Hardware Bridge Draft
- 系统级 CPU/MCU 拆分与安全理由:ARCHITECTURE.md §5.4
这套实现的完整故事可以概括为一句话:线格式对、解析器扛打、安全策略在每种规定条件下行为正确——而且这一切都在一块真实 MCU 到位之前,就已在笔记本上被端到端证明。剩下的,是把这份证明搬到真实芯片上,那正是里程碑 6 与后续板级工作要做的事。
- 智能硬件
- 机器人
- 嵌入式
- 物联网
【免费下载链接】oomwoo
Open-source vacuum robot cleaner
相关推荐
OOMWOO I/O Board 软件接口实战指南:CPU↔MCU 串行契约、安全机制与 ROS2 桥接设计
OOMWOO I/O Board 软件接口实战指南:CPU↔MCU 串行契约、安全机制与 ROS2 桥接设计 OOMWOO 开源扫地机器人通过一张定制 I/O
智能硬件机器人嵌入式物联网OOMWOO CPU/MCU 串行接口契约全解:ROS2 桥与 STM32 固件的安全集成规范与验证方案
OOMWOO CPU/MCU 串行接口契约全解:ROS2 桥与 STM32 固件的安全集成规范与验证方案 OOMWOO 是一个开源真空扫地机器人项目,本文聚焦其
智能硬件机器人嵌入式物联网OOMWOO 开箱验证计划:从协议测试到整机带载的 CPU/MCU 桥接七阶段排雷手册
OOMWOO 开箱验证计划:从协议测试到整机带载的 CPU/MCU 桥接七阶段排雷手册 导读 本文以 OOMWOO(开源扫地机器人)项目中 xbattlax 的
智能硬件机器人嵌入式物联网
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考