ROS2系列教程:MicroROS接入(原理+仿真)

🕒 阅读时间:19 分钟📝 字数:4701👀 阅读量:Loading...

本文是 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 的技术栈从上到下分三层:

  1. 应用层:我们的业务代码,用 rclc API 写,比如 rclc_publisher_init 创建一个发布者。
  2. 中间层:rcl(ROS2 客户端库的精简版)→ rmw(ROS2 中间件接口)→ Micro XRCE-DDS(精简 DDS 协议)。
  3. 传输层: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 代码)。这样我们不需要任何硬件,就能:

  1. 体验完整的 MicroROS 开发流程(写 Client 代码、编译、运行)。
  2. 看到 MCU 节点如何被 Agent 桥接到 ROS2 图。
  3. 为后续真机(ESP32/STM32)移植做好代码层面的准备。

3.2 环境要求与安装

仿真需要 Docker(最省事)或手动源码编译。推荐 Docker:

Terminal window
# 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 build
mkdir -p ~/micro_ros_agent_ws/src && cd ~/micro_ros_agent_ws
git clone https://github.com/micro-ROS/micro-ROS-Agent.git src/micro-ROS-Agent
colcon build
source install/setup.bash

3.3 准备 micro_ros_agent 工作空间

在 Docker 之外,我们还需要在本机 ROS2 环境里安装 micro-ros-agent 的包,这样 ros2 run micro_ros_agent micro_ros_agent 命令才可用。完整流程(Humble):

Terminal window
# 安装依赖
sudo apt update
sudo apt install -y python3-pip python3-rosdep
# 创建工作空间并克隆
mkdir -p ~/micro_ros_agent_ws/src && cd ~/micro_ros_agent_ws
git clone -b humble https://github.com/micro-ROS/micro-ROS-Agent.git src/micro-ROS-Agent
# 安装依赖并编译(注意 source 环境)
source /opt/ros/humble/setup.bash
rosdep install --from-paths src --ignore-src -y
colcon build
source install/setup.bash
# 验证
ros2 run micro_ros_agent micro_ros_agent serial --help

四、示例 1:启动 micro-ROS Agent

Agent 是 MicroROS 通信的“桥梁”,必须最先启动。它本质是一个 ROS2 节点,所以可以和其他节点一样用 ros2 run 启动。

Terminal window
# 终端 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 图:

/micro_ros_agent
# 终端 2:查看节点列表,应能看到 /micro_ros_agent
ros2 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 镜像:

Terminal window
# 拉取 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 bash

5.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;
// 定时回调:心跳值加 1
void 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 可执行文件):

Terminal window
# 在容器内,micro_ros_agent 镜像自带构建脚本
# 以 Host 模式构建
source /uros_ws/install/setup.bash
colcon 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):

Terminal window
# 终端 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 是订阅者回调触发模式,表示有新消息才回调):

Terminal window
# 终端 1:启动 Agent
ros2 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;
}

验证:

Terminal window
# 终端 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)只需两步:

  1. 换编译目标:用 micro_ros_espidf 组件或 PlatformIO 的 micro-ROS 支持,把同样的 C 代码交叉编译成 ESP32 固件。
  2. 换传输层:Host 模式用 UDP,真机用 UART(串口)或 Wi-Fi UDP,修改 Agent 启动参数和 Client 的传输配置即可。
Terminal window
# 真机场景(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 8888

8.2 消息类型限制与自定义

MicroROS 默认支持常用消息类型(std_msgs、geometry_msgs、sensor_msgs 等)。自定义消息需要先在本机生成 micro-ROS 消息包,再交叉编译进固件。流程:

Terminal window
# 生成自定义消息的 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

十、小结与下一篇预告

本篇核心:

  1. MicroROS = 让 MCU 直接作为 ROS2 节点,通过 Agent 桥接进 ROS2 图,无需自定义协议。
  2. 架构三件套:Client(MCU 端 rcl 精简实现)+ Agent(上位机代理)+ 传输层(串口/UDP)。
  3. Host 模式仿真:PC 模拟 MCU,完整跑通 发布/订阅/服务 三种通信,为真机移植铺路。
  4. 真机移植 = 换编译目标 + 换传输层,业务代码几乎不变。

至此我们理解了“单片机如何接入 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 国际许可协议进行许可。

Creative Commons

留言评论