ROS2系列教程:MicroROS接入(原理+仿真)
本文摘要
本文详解ROS2 MicroROS接入原理与仿真实践,解决嵌入式设备与上位机通信的通用性难题。通过MicroROS架构(Client+Agent+传输层),实现MCU作为ROS2节点无缝接入系统…
本文是 ROS2 系列教程的第 27 篇
本文是 ROS2 系列教程的第 27 篇:MicroROS 接入(原理+仿真)。第 26 篇我们建立了嵌入式开发的基础认知:GPIO、点灯、串口调试。但嵌入式程序是“孤岛”——它怎么和 ROS2 上位机对话?传统做法是自己定义串口协议,麻烦且不通用。ROS2 官方给出的标准答案是 MicroROS(micro-ROS):让单片机直接作为 ROS2 节点接入系统。本篇讲解 MicroROS 的原理、Agent/Client 架构,并用仿真环境完整跑通“单片机节点”与上位机通信的流程,为第 28 篇 ROS2-Control 打基础。
一、为什么需要 MicroROS
1.1 传统串口协议的痛点
在第 26 篇我们提到,上位机与下位机最常见的通信方式是自定义串口协议:上位机发 "CMD:FORWARD:SPEED=0.5",下位机解析执行。这种方式在简单项目里够用,但有明显问题:
| 痛点 | 说明 |
|---|---|
| 协议不通用 | 每家机器人厂商的协议都不同,换硬件就得重写通信层 |
| 语义丢失 | 数据帧里没有“话题名、类型、时间戳”等 ROS2 概念 |
| 工具链断裂 | 无法用 ros2 topic、rviz2、ros2 bag 直接查看下位机数据 |
| 双向开发成本高 | 上位机和下位机各写一套解析代码,容易不一致 |
| 无 QoS 支持 | 无法表达“可靠性、易失性”等传输需求 |
1.2 MicroROS 的答案:让 MCU 成为 ROS2 节点
MicroROS 是 ROS2 官方的嵌入式方案,核心思想:在资源极其有限的 MCU 上运行 ROS2 的客户端库(rcl),并通过串口/USB 与上位机上的 micro-ROS Agent 桥接。对上层 ROS2 系统来说,MCU 就是一个普通节点:能发布话题、订阅话题、调服务、用参数,完全透明。
架构图:
┌─────────────────── 上位机(Linux) ───────────────────┐│ ROS2 图:/micro_ros_node 发布 /cmd_vel 订阅 /odom ││ │ ││ micro-ROS Agent(代理,把串口数据转成 ROS2 DDS) │└──────────────│────────────────────────────────────────┘ │ 串口 / USB / Wi-Fi(UART/Serial/UDP)┌──────────────│────────────────────────────────────────┐│ │ ││ micro-ROS Client(运行在 MCU 上的 rcl 实现) ││ MCU:STM32 / ESP32 / Arduino ... │└───────────────────────────────────────────────────────┘关键组件:
- micro-ROS Client:运行在 MCU 上,是 ROS2 客户端库(rcl)的精简实现(rclc),提供
rclc_init、rclc_node_init、rclc_publisher_init等 API。代码在 MCU 上编译。 - micro-ROS Agent:运行在上位机(或局域网主机),是一个 ROS2 节点,负责与 Client 建立传输通道(串口、USB、Wi-Fi),并把 Client 的请求代理到 ROS2 图。Agent 本质上是“网桥”。
- 传输层:Client 与 Agent 之间的通信链路,常用 UART(串口)、UDP(Wi-Fi/以太网)。
1.3 MicroROS 与 ROS2 原生节点的关系
MicroROS 节点和原生 ROS2 节点在图层面完全等价:ros2 node list 能看到它,ros2 topic echo 能收它的数据,RViz 能订阅它的消息。区别只在内部实现:
| 对比项 | ROS2 原生节点 | MicroROS 节点 |
|---|---|---|
| 运行环境 | Linux + 完整 DDS 栈 | MCU + 精简 DDS(Micro XRCE-DDS) |
| 内存需求 | 数百 MB | 几十 KB |
| 实时性 | 一般 | 强 |
| 支持的消息类型 | 全部 | 常用类型(可自定义生成) |
| 传输 | DDS 网络 | 串口/USB/Wi-Fi 代理 |
理解 MicroROS 的关键一句话:MicroROS 是“把 ROS2 的接口跑在单片机上的方案”,MCU 依然是节点,只是数据通路经过了 Agent 代理。
二、MicroROS 核心机制(原理)
2.1 三层架构
MicroROS 的技术栈从上到下分三层:
- 应用层:我们的业务代码,用 rclc API 写,比如
rclc_publisher_init创建一个发布者。 - 中间层:rcl(ROS2 客户端库的精简版)→ rmw(ROS2 中间件接口)→ Micro XRCE-DDS(精简 DDS 协议)。
- 传输层:XRCE 协议通过 UART/UDP 传输到 Agent。
其中 Micro XRCE-DDS 是核心协议:它在 MCU 端维护一个 DDS 客户端的“影子”,由 Agent 代为完成 DDS 网络的真正通信。MCU 只管本地逻辑,网络细节全部交给 Agent。
2.2 Agent 的两种模式
Agent 可以运行在:
- 本机模式:Agent 和 ROS2 上位机在同一台机器(如树莓派、Jetson),MCU 通过串口连上位机。最常见。
- 远程模式:Agent 运行在局域网某台 PC 上,MCU 通过 Wi-Fi(UDP)连到该 PC。适合 MCU 离上位机较远的情况。
无论是哪种模式,对 MCU 代码来说几乎无差别(只是传输参数不同),这正是 MicroROS 设计的巧妙之处。
2.3 生命周期
MicroROS 节点和原生节点一样有生命周期:创建上下文 → 创建节点 → 创建发布者/订阅者 → 进入执行循环(rclc_executor_spin)→ 关闭。
rclc_support_init 初始化运行时上下文 ↓rclc_node_init_default 创建节点 ↓rclc_publisher_init_* 创建发布者(或订阅者/服务) ↓rclc_executor_init 创建执行器(绑定节点) ↓rclc_executor_add_* 注册订阅者回调等 ↓rclc_executor_spin 循环执行(主循环)三、仿真环境准备
3.1 本教程的仿真思路
在没有真实 MCU 的情况下,怎么学 MicroROS?答案是:MicroROS 官方提供了“Host 模式”——在 Linux 上位机上,把 PC 模拟成 MicroROS Client(用 PC 的处理器跑 Client 代码)。这样我们不需要任何硬件,就能:
- 体验完整的 MicroROS 开发流程(写 Client 代码、编译、运行)。
- 看到 MCU 节点如何被 Agent 桥接到 ROS2 图。
- 为后续真机(ESP32/STM32)移植做好代码层面的准备。
3.2 环境要求与安装
仿真需要 Docker(最省事)或手动源码编译。推荐 Docker:
# 1. 拉取 MicroROS Docker 镜像(官方提供 ROS2 Humble 版本)docker pull microros/micro-ros-agent:humble
# 2. 安装 micro-ros-agent 到本机(也可在 Docker 内运行)# 方式 A:Docker 运行(推荐,隔离环境)docker run -it --rm -v /dev:/dev --privileged --net=host \ microros/micro-ros-agent:humble serial --dev /dev/ttyUSB0 -b 115200
# 方式 B:本机源码安装# 建工作空间,clone micro-ros-agent 源码后 colcon buildmkdir -p ~/micro_ros_agent_ws/src && cd ~/micro_ros_agent_wsgit clone https://github.com/micro-ROS/micro-ROS-Agent.git src/micro-ROS-Agentcolcon buildsource install/setup.bash3.3 准备 micro_ros_agent 工作空间
在 Docker 之外,我们还需要在本机 ROS2 环境里安装 micro-ros-agent 的包,这样 ros2 run micro_ros_agent micro_ros_agent 命令才可用。完整流程(Humble):
# 安装依赖sudo apt updatesudo apt install -y python3-pip python3-rosdep
# 创建工作空间并克隆mkdir -p ~/micro_ros_agent_ws/src && cd ~/micro_ros_agent_wsgit clone -b humble https://github.com/micro-ROS/micro-ROS-Agent.git src/micro-ROS-Agent
# 安装依赖并编译(注意 source 环境)source /opt/ros/humble/setup.bashrosdep install --from-paths src --ignore-src -ycolcon buildsource install/setup.bash
# 验证ros2 run micro_ros_agent micro_ros_agent serial --help四、示例 1:启动 micro-ROS Agent
Agent 是 MicroROS 通信的“桥梁”,必须最先启动。它本质是一个 ROS2 节点,所以可以和其他节点一样用 ros2 run 启动。
# 终端 1:启动 ROS2 环境source /opt/ros/humble/setup.bash
# 启动 Agent(本机模式,串口连接,波特率 115200)ros2 run micro_ros_agent micro_ros_agent serial --dev /dev/ttyUSB0 -b 115200
# 如果使用 UDP 模式(Wi-Fi 连接),用:# ros2 run micro_ros_agent micro_ros_agent udp4 --port 8888启动后终端会显示等待 Client 连接的信息。验证 Agent 节点是否出现在 ROS2 图:
# 终端 2:查看节点列表,应能看到 /micro_ros_agentros2 node list# 预期输出包含:小技巧:Agent 支持同时连接多个 Client,每个 Client 有自己的命名空间,方便区分多个下位机。
五、示例 2:Host 模式运行 Client(PC 模拟 MCU)
这是本篇的核心仿真实验:在没有硬件的 PC 上,运行一个 MicroROS Client 节点,让它与 Agent 建立连接,成为 ROS2 图里的一员。
5.1 准备 Client 编译环境
MicroROS Client 代码用 C 编写,使用 micro-ROS 提供的构建工具链编译。官方提供 micro_ros_setup 或 Docker 镜像:
# 拉取 Client 编译镜像(Humble 版本)docker pull microros/micro-ros-agent:humble
# 或者用 micro-ros-setup 脚本(推荐在 Docker 里)docker run -it --rm -v $(pwd):/work --net=host \ microros/micro-ros-agent:humble bash5.2 编写 Client 代码:发布者(心跳节点)
在容器内创建源码文件 heartbeat_pub.c,这是一个“心跳发布者”:定时向话题 /heartbeat 发布一条消息,模拟 MCU 上报状态。
// heartbeat_pub.c:MicroROS 心跳发布者(模拟 MCU 节点)#include <rcl/rcl.h>#include <rclc/rclc.h>#include <rclc/executor.h>#include <std_msgs/msg/int32.h>
#define RCCHECK(fn) { rcl_ret_t temp_rc = fn; \ if ((temp_rc != RCL_RET_OK)) { for (;;) {} } }#define RCSOFTCHECK(fn) { rcl_ret_t temp_rc = fn; \ if ((temp_rc != RCL_RET_OK)) {} }
rcl_publisher_t publisher;std_msgs__msg__Int32 msg;
// 定时回调:心跳值加 1void timer_callback(rcl_timer_t *timer, int64_t last_call_time) { (void) last_call_time; if (timer != NULL) { msg.data++; rcl_publish(&publisher, &msg, NULL); printf("heartbeat: %d\n", msg.data); }}
int main() { rcl_allocator_t allocator = rcl_get_default_allocator(); rclc_support_t support; rcl_init_options_t init_options = rcl_get_zero_initialized_init_options(); RCCHECK(rcl_init_options_init(&init_options, rcl_get_default_allocator())); RCCHECK(rclc_support_init_with_options(&support, 0, NULL, &init_options, &allocator));
// 创建节点 rcl_node_t node; RCCHECK(rclc_node_init_default(&node, "heartbeat_node", "", &support));
// 创建发布者 RCCHECK(rclc_publisher_init_default( &publisher, &node, ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Int32), "/heartbeat"));
// 创建定时器(500ms 触发一次) rcl_timer_t timer; RCCHECK(rclc_timer_init_default( &timer, &support, RCL_MS_TO_NS(500), timer_callback));
// 创建执行器并注册定时器 rclc_executor_t executor; RCCHECK(rclc_executor_init(&executor, &support.context, 1, &allocator)); RCCHECK(rclc_executor_add_timer(&executor, &timer));
printf("heartbeat_node started, publishing to /heartbeat\n");
// 主循环 for (;;) { rclc_executor_spin_some(&executor, RCL_MS_TO_NS(100)); }
RCCHECK(rcl_publisher_fini(&publisher, &node)); RCCHECK(rcl_node_fini(&node)); return 0;}5.3 编译并运行
在 Docker 容器内编译(Host 模式编译成 PC 可执行文件):
# 在容器内,micro_ros_agent 镜像自带构建脚本# 以 Host 模式构建source /uros_ws/install/setup.bashcolcon build --packages-select micro_ros_agent # 构建 Agent(可选)
# 编译上面的 C 文件(需要链接 micro_ros_client 库)# 更简单的方式:使用官方 micro_ros_static_library 构建脚本# 假设已在 /uros_ws 下创建了 microros 静态库gcc heartbeat_pub.c -o heartbeat_pub \ -I/uros_ws/install/include \ -L/uros_ws/lib -lmicroros对于没有 Docker 环境的朋友,官方还提供了 micro_ros_utilities 静态库生成脚本,这里不展开,重点是理解流程。
5.4 在 ROS2 侧验证
Host 模式的 Client 运行时,会在本机直接连接 Agent(或通过 UDP 连接远程 Agent):
# 终端 1:启动 Agent(UDP 模式,因为 Host 模式走 UDP)ros2 run micro_ros_agent micro_ros_agent udp4 --port 8888
# 终端 2:运行 Client(在编译出 heartbeat_pub 的容器/目录下)./heartbeat_pub
# 终端 3:验证 ROS2 侧ros2 node list# 应看到:/micro_ros_agent 和 /heartbeat_node
ros2 topic list# 应看到:/heartbeat
ros2 topic echo /heartbeat std_msgs/msg/Int32# 应不断打印:data: 1, data: 2, data: 3 ...这一刻的意义:一个“单片机”(其实是 PC 模拟的)通过 Agent 成功接入 ROS2 图,其他节点可以直接订阅它的数据。这就是 MicroROS 的核心价值——换到真实 MCU 上,代码逻辑完全一样,只是编译目标不同。
六、示例 3:订阅者(接收上位机指令)
有发布就要有订阅。MCU 上最常见的场景:订阅上位机发来的控制指令。这里模拟“MCU 收到速度指令后打印执行”。
// cmd_sub.c:MicroROS 订阅者(接收 /cmd_vel 指令,模拟执行)#include <rcl/rcl.h>#include <rclc/rclc.h>#include <rclc/executor.h>#include <geometry_msgs/msg/twist.h>
#define RCCHECK(fn) { rcl_ret_t temp_rc = fn; \ if ((temp_rc != RCL_RET_OK)) { for (;;) {} } }
rcl_subscription_t subscriber;geometry_msgs__msg__Twist msg;
// 订阅回调:收到速度指令后"执行"(这里打印)void subscription_callback(const void *msgin) { const geometry_msgs__msg__Twist *twist = msgin; printf("exec cmd_vel: linear=%.2f angular=%.2f\n", twist->linear.x, twist->angular.z);}
int main() { rcl_allocator_t allocator = rcl_get_default_allocator(); rclc_support_t support; RCCHECK(rclc_support_init(&support, 0, NULL, &allocator));
rcl_node_t node; RCCHECK(rclc_node_init_default(&node, "cmd_sub_node", "", &support));
RCCHECK(rclc_subscription_init_default( &subscriber, &node, ROSIDL_GET_MSG_TYPE_SUPPORT(geometry_msgs, msg, Twist), "/cmd_vel"));
rclc_executor_t executor; RCCHECK(rclc_executor_init(&executor, &support.context, 1, &allocator)); RCCHECK(rclc_executor_add_subscription( &executor, &subscriber, &msg, &subscription_callback, ON_NEW_DATA));
printf("cmd_sub_node started, waiting for /cmd_vel...\n");
for (;;) { rclc_executor_spin_some(&executor, RCL_MS_TO_NS(100)); }
RCCHECK(rcl_subscription_fini(&subscriber, &node)); RCCHECK(rcl_node_fini(&node)); return 0;}验证(注意:ON_NEW_DATA 是订阅者回调触发模式,表示有新消息才回调):
# 终端 1:启动 Agentros2 run micro_ros_agent micro_ros_agent udp4 --port 8888
# 终端 2:运行订阅者 Client./cmd_sub
# 终端 3:用 ros2 发送速度指令(模拟上位机控制)ros2 topic pub /cmd_vel geometry_msgs/msg/Twist \ "{linear: {x: 0.5, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.3}}" \ --once
# Client 侧应打印:# exec cmd_vel: linear=0.50 angular=0.30这个实验展示了 MCU 最常见的角色:执行上位机的指令。后面 ROS2-Control(第 28 篇)的硬件接口,本质就是这个模式——只是把“打印”换成“给电机 PWM”。
七、示例 4:服务端(MCU 提供服务)
有些场景下 MCU 不只是执行者,还要提供服务:比如“获取电池电量”“设置 LED 状态”。MicroROS 也支持服务端。
// led_srv.c:MicroROS 服务端(设置 LED 状态的服务)#include <rcl/rcl.h>#include <rclc/rclc.h>#include <rclc/executor.h>#include <example_interfaces/srv/set_bool.h>
#define RCCHECK(fn) { rcl_ret_t temp_rc = fn; \ if ((temp_rc != RCL_RET_OK)) { for (;;) {} } }
rcl_service_t service;example_interfaces__srv__SetBool_Request req;example_interfaces__srv__SetBool_Response res;
// 服务回调:处理"设置 LED"请求void service_callback(const void *request, void *response) { const example_interfaces__srv__SetBool_Request *r = request; example_interfaces__srv__SetBool_Response *s = response; s->success = true; if (r->data) { s->message = "led on"; // 真实 MCU 这里调用 HAL_GPIO_WritePin } else { s->message = "led off"; } printf("set led: %s\n", s->message);}
int main() { rcl_allocator_t allocator = rcl_get_default_allocator(); rclc_support_t support; RCCHECK(rclc_support_init(&support, 0, NULL, &allocator));
rcl_node_t node; RCCHECK(rclc_node_init_default(&node, "led_srv_node", "", &support));
RCCHECK(rclc_service_init_default( &service, &node, ROSIDL_GET_SRV_TYPE_SUPPORT(example_interfaces, srv, SetBool), "/set_led"));
rclc_executor_t executor; RCCHECK(rclc_executor_init(&executor, &support.context, 1, &allocator)); RCCHECK(rclc_executor_add_service( &executor, &service, &req, &res, service_callback));
printf("led_srv_node started, service /set_led ready\n");
for (;;) { rclc_executor_spin_some(&executor, RCL_MS_TO_NS(100)); } return 0;}验证:
# 终端 2:运行 Client./led_srv
# 终端 3:调用服务(模拟上位机请求)ros2 service call /set_led example_interfaces/srv/SetBool "{data: true}"# 响应:success: true, message: "led on"
# 也可以服务发现ros2 service list# 应包含:/set_led注意 MicroROS 服务端与原生的一个差异:请求/响应消息内存需要在代码里静态分配(req、res 变量),因为 MCU 没有动态内存管理。这是嵌入式开发的核心思维——一切资源显式管理。
八、MicroROS 与真机的桥接思路
8.1 从 Host 模式到真机
Host 模式跑通后,移植到真机(如 ESP32)只需两步:
- 换编译目标:用 micro_ros_espidf 组件或 PlatformIO 的 micro-ROS 支持,把同样的 C 代码交叉编译成 ESP32 固件。
- 换传输层:Host 模式用 UDP,真机用 UART(串口)或 Wi-Fi UDP,修改 Agent 启动参数和 Client 的传输配置即可。
# 真机场景(ESP32 + 串口连接)ros2 run micro_ros_agent micro_ros_agent serial --dev /dev/ttyUSB0 -b 115200
# 真机场景(ESP32 + Wi-Fi UDP)ros2 run micro_ros_agent micro_ros_agent udp4 --port 88888.2 消息类型限制与自定义
MicroROS 默认支持常用消息类型(std_msgs、geometry_msgs、sensor_msgs 等)。自定义消息需要先在本机生成 micro-ROS 消息包,再交叉编译进固件。流程:
# 生成自定义消息的 micro-ROS 版本docker run -it --rm -v $(pwd):/work microros/micro-ros-agent:humble bash# 容器内:用 ros2 的 msg 生成 + micro_ros_msgs 工具链# 然后 colcon build 生成静态库,链接进固件8.3 时序与实时性注意事项
MCU 上做 MicroROS 要特别留意:
- 回调里禁止阻塞:
rclc_executor_spin_some是单线程轮询,回调里若做耗时操作(如大延时),会阻塞其他订阅/定时器。 - 内存预算:每个发布者/订阅者的消息缓冲区都是静态分配的,注意栈空间。
- 断线重连:串口/USB 断开后 Client 需要重新建立会话,生产代码要处理重连逻辑。
九、常见问题与排障
| 现象 | 原因 | 解决 |
|---|---|---|
| Client 连不上 Agent | 传输参数不匹配(端口/波特率) | 两端统一端口(8888)或波特率(115200) |
ros2 node list 看不到 Client |
Agent 没启动 / 会话未建立 | 先启动 Agent,看 Agent 日志是否“create session” |
| 话题收到了但数据为 0 | 消息初始化未完成 | Client 端先 msg.data = 0 初始化再发布 |
| 服务调用超时 | 服务端未注册 / 类型不匹配 | 检查服务名、类型、命名空间 |
| 编译报 memory 不足 | 静态缓冲过大 | 减小消息队列长度、消息类型换小 |
| 断开后无法重连 | Client 未处理会话关闭 | 实现重连逻辑,重新 rclc_support_init |
十、小结与下一篇预告
本篇核心:
- MicroROS = 让 MCU 直接作为 ROS2 节点,通过 Agent 桥接进 ROS2 图,无需自定义协议。
- 架构三件套:Client(MCU 端 rcl 精简实现)+ Agent(上位机代理)+ 传输层(串口/UDP)。
- Host 模式仿真:PC 模拟 MCU,完整跑通 发布/订阅/服务 三种通信,为真机移植铺路。
- 真机移植 = 换编译目标 + 换传输层,业务代码几乎不变。
至此我们理解了“单片机如何接入 ROS2”。下一篇(第 28 篇)进入 ROS2-Control 与控制器(原理):ROS2 官方如何统一管理机器人关节控制(diff_drive_controller、Joint Trajectory Controller),以及它和 MicroROS 的分工——上位机负责“想”,下位机负责“动”,中间用标准接口衔接。硬件控制阶段最硬核的部分即将展开。
ROS2系列教程:MicroROS接入(原理+仿真)
作者:Wang Shijie
本文链接:https://wsj-blog.pages.dev/posts/ros2-xi-lie-jiao-cheng-microros-jie-ru-yuan-li-fang-zhen/
本文采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
留言评论
评论资源加载失败,请重试。