本文结合 rm_pid_tuner(ROS Noetic 自动调参系统)的实际开发经验,讲清楚 ROS2 到底改了什么、为什么要改,以及把 ROS1 工程迁到 ROS2 时每一步会踩什么坑。不满足于”会建包、会发布订阅”,而是让你理解 DDS 通信模型 → QoS 契约 → 执行模型 → 迁移实战 的完整链路。
为什么要有 ROS2:ROS1 的架构性缺陷
ROS1 的核心是 roscore(master):所有节点启动时向 master 注册,通信双方通过 master 交换地址信息后建立点对点连接。这个架构有三个致命问题:
- 单点故障:master 挂了整个系统瘫痪。机器人比赛/产线上不可能接受”调度器进程崩溃 = 全车失联”
- 无实时性保证:roscpp 的发布订阅基于 TCP/UDP socket,传输层不可控,回调触发时机不确定
- 无安全/多机支持:ROS1 的跨机通信需要所有机器共享同一 ROS_MASTER_URI,没有加密、没有权限隔离,多机器人场景下 QoS 完全无法区分
ROS2 的答案是:把通信层整个换成 DDS(Data Distribution Service)。DDS 是 OMG 标准,自带服务发现(不依赖中心节点)、QoS 策略、多机组播发现。ROS2 里不存在 master——节点通过 DDS 的 RTPS 发现协议(SDP:向组播地址 239.255.0.1:7400 广播 announcement,收到者单播回复地址信息,交换 topic/类型/QoS 后建立数据通道)在同一网段内互相发现,这从架构上消灭了单点故障。
踩坑:跨网段/跨机器通信失败,90% 是组播被交换机或防火墙挡了。实战中 RoboMaster 哨兵/步兵多机通信,要么保证同一二层网段,要么配置 ROS_DOMAIN_ID 隔离 + 放行 UDP 发现端口(7400-7500)。Docker 里 --net=host 能通、bridge 网络不通,也是这个原因(下文 Docker 部分再讲)。
QoS:通信双方必须签的”契约”
QoS(Quality of Service)是 ROS2 相对于 ROS1 最重要的新概念。ROS1 里发布方和订阅方只要 topic 名+类型一致就能通信;ROS2 里还要求 QoS 策略兼容,不兼容直接收不到消息且不报错——这是从 ROS1 迁过来的人最先疯掉的地方。
常用策略:
| 策略 | 取值 | 含义 | 实战建议 |
|---|---|---|---|
| Reliability | RELIABLE / BEST_EFFORT | 是否保证送达 | 控制指令用 RELIABLE;图像/点云用 BEST_EFFORT |
| Durability | VOLATILE / TRANSIENT_LOCAL | 晚加入的订阅者能否收到历史数据 | 地图、静态参数用 TRANSIENT_LOCAL |
| Depth | 1~N | 队列深度 | 图像给 1(丢旧帧优于积压),日志给大值 |
| Deadline | 时长 | 两次发布的最大间隔 | 心跳/看门狗检测用 |
为什么图像传输必须 BEST_EFFORT + depth=1? 考虑 30fps 的 720p 图像,每帧约 1.3MB。用 RELIABLE 时,网络抖动会导致 DDS 重传丢帧并阻塞后续帧,订阅方拿到的永远是”迟到的最新帧”,延迟从 33ms 劣化到几百 ms——视觉引导的云台瞄准直接废掉。BEST_EFFORT + depth=1 的语义是”只保留最新一帧,旧的直接覆盖”,丢帧无感、延迟恒定,跟控制回路”宁可丢一拍也不等旧数据”的思想一致。
QoS 兼容性判据(发布方 P 与订阅方 S):P 的 Reliability 必须 ≥ S 的(即 S 要求 RELIABLE 时 P 必须 RELIABLE);Depth 等由 S 决定。可以用命令行验证:
1 | ros2 topic info /cmd_vel --verbose # 显示双方 QoS,Compatibility 一栏直接告诉你是否兼容 |
构建系统与包结构:ament_cmake + colcon
创建功能包
1 | # ROS1 |
编译系统对比
| ROS1 | ROS2 | |
|---|---|---|
| 构建工具 | catkin_make / catkin build | colcon build |
| 包格式 | package.xml (format 2) | package.xml (format 3) |
| CMake | find_package(catkin REQUIRED) |
find_package(ament_cmake REQUIRED) |
| 依赖链接 | catkin_package() 自动传递 |
ament_target_dependencies() 显式声明 |
| 安装 | 可选 | 必须 install(ament 强制安装阶段) |
colcon 的目录布局和 catkin 有本质区别:所有包平铺在 build/、install/、log/ 三个目录下,没有 devel 空间。开发时务必加 --symlink-install,否则改代码要重新 colcon build 整个包:
1 | colcon build --symlink-install --packages-select my_pkg |
CMakeLists 的工程化写法
1 | cmake_minimum_required(VERSION 3.8) |
坑:忘了 install() 步骤,ros2 run 找不到可执行文件(报 executable not found)——因为 ament 只从 install 空间找程序,不打包进 install 就等于没编译。
元包(metapackage)在 ROS2 里同样靠 <export><build_type>ament_cmake</build_type></export> 标记,CMakeLists 只留 ament_package(),依赖列在 package.xml 的 <depend> 里。
执行模型:executor 与回调饿死
这是 ROS2 与 ROS1 运行时行为上最大的差异。ROS1 每个节点自带多个 spinner 线程,回调并行执行;ROS2 所有回调(订阅、定时器、service)都由 executor 调度,默认 SingleThreadedExecutor 只有一个线程,一个回调里 sleep/阻塞,整个节点卡死。
rm_pid_tuner 里最典型的场景:调参节点同时要 ① 10ms 周期跑调参状态机(定时器回调)② 接收裁判系统/传感器数据(订阅回调)③ 响应调参服务请求。如果全挤在单线程 executor 里,某个回调里一次 rclcpp::spin_until_future_complete 等待 LLM API 返回(网络延迟可能 2 秒),控制状态机就断了。
工程解法——按实时性分线程:
1 | // 控制回路:独立线程 + 独立 callback group,绝不与其他回调混用 |
坑 1:MutuallyExclusive group 内回调互斥;如果两个定时器要并行,放不同 group。坑 2:写回调里绝不能 sleep/spin,需要异步等待就用 rclcpp::AsyncParametersClient 或把耗时计算丢进 std::async。坑 3:实时性要求高的控制循环,理想做法是绕开 executor——自己起 std::thread 直接 rclcpp::spin_some 之外独立跑 RT 线程,DDS 只做数据搬运。
从 ROS1 迁移实战:以 rm_pid_tuner 为例
把 ROS Noetic 的调参系统迁到 ROS2,API 映射表如下(逐行对应):
| ROS1 (roscpp) | ROS2 (rclcpp) | 注意 |
|---|---|---|
ros::NodeHandle nh |
rclcpp::Node::make_shared("node") |
参数要在构造函数里 declare_parameter |
nh.subscribe("/topic", 10, cb) |
create_subscription<T>(..., 10, cb) |
队列长度语义≈QoS depth |
nh.advertiseService |
create_service |
回调签名 (req, resp) 带 shared_ptr |
dynamic_reconfigure |
参数系统 + parameter events | 见下 |
ros::Time::now() |
this->now() / rclcpp::Clock(RCL_SYSTEM_TIME) |
注意 use_sim_time |
ros::Rate(100).sleep() |
rclcpp::Rate(100).sleep() |
无本质变化 |
tf::TransformListener |
tf2_ros::Buffer + TransformListener |
API 变化大,建议直接用 tf2 |
动态重配置 → 参数系统
ROS1 的 dynamic_reconfigure(调参时不需要重启节点)在 ROS2 里被参数系统替代,但写法完全不同。ROS1 是生成 cfg 文件 + 回调;ROS2 是声明参数 + 监听事件:
1 | // 声明参数(带默认值和描述) |
命令行改参:ros2 param set /tuner kp 4.2。注意:参数回调运行在 executor 线程上,如果回调里做耗时操作(如重新编译参数缓存),一样会饿死其他回调——和上面是同一个坑。
时间 API 的坑:use_sim_time
rclcpp::Clock 默认 RCL_SYSTEM_TIME(墙上时钟),但一旦有人设置 use_sim_time: true(如 bag 回放、gazebo 仿真),所有用 this->now() 的地方都会跳到仿真时间。如果系统里混用——控制节点用系统时间、导航节点用仿真时间——时间戳对不上,TF 直接报 “Lookup would require extrapolation”。迁移时统一从一个 rclcpp::Clock 取时间,或显式声明 rclcpp::Clock(RCL_STEADY_TIME) 用于纯间隔测量(不受任何时间源影响)。
ros_control → ros2_control
ROS1 的 RobotHW 基类 + controller_manager 在 ROS2 里变成 hardware_interface::SystemInterface + 插件化 controller。结构上:
1 | controller_manager (node) |
Controller 继承 controller_interface::ControllerInterface,生命周期由 controller_manager 管理(configure → activate → update → deactivate)。关键点:
1 |
|
继承 hardware_interface 写自己的 rmHW,就是写自己的硬件抽象层——在 ROS1 里是
RobotHW,在 ROS2 里是SystemInterface。区别是 ROS2 强制把”读”和”写”分成read()/write()两个明确接口,controller 与 hardware 完全解耦(换电机驱动不需要改 controller 代码)。
周期与实时性:controller_manager 用 realtime 线程周期调用各 controller 的 update()(默认 100Hz,可配),与 executor 完全独立——这是 ROS2 相比 ROS1 控制栈最大的进步。代价:controller 代码里禁止内存分配/加锁/打印等非确定操作,调试日志要写到循环外。
Docker 开发环境
一个能用的 Dockerfile(含非 root 用户)
1 | FROM osrf/ros:humble-desktop |
1 | docker build -t ros2-dev . |
DDS 在 Docker 里的坑
--net=host是 ROS2 容器的事实标准。用 bridge 网络时容器内组播地址(239.255.0.1)被 NAT 隔离,节点互相发现不了;--net=host直接共享宿主机网络栈,发现协议原样通过- 多容器/多机共用一个网络时用
ROS_DOMAIN_ID隔离(export ROS_DOMAIN_ID=1)——不同域 ID 的节点物理上不会互相发现,比 ROS1 的命名空间隔离可靠得多 - 大消息(点云/图像)跨容器走 UDP 会打爆带宽,可用 iceoryx 共享内存传输(
RMW_IMPLEMENTATION=rmw_iceoryx_cpp),同机零拷贝,延迟从 ~1ms 降到 ~0.1ms 量级——多传感器机器人强烈建议
CLion 配置 ROS2
- 在 CLion 中打开 ROS2 工作空间(
src/目录),Build Tool 选colcon - CMake 参数添加
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON(代码提示/静态检查用) - 必须在已 source 过 ROS2 环境的终端中启动 CLion(
source /opt/ros/humble/setup.bash),否则 CMake 找不到 ament 包 - 调试节点:Run/Debug Configurations 选
colcon+ 你的可执行文件;或先ros2 run起节点再 attach(gdb)——对调参节点这种带状态机的,attach 方式更实用(--ros-args -r __ns:=/debug换命名空间起第二实例,可与正式实例并行对比日志)。
关键命令速查
1 | # 编译(报 has-built-by-catkin_build 错误 = 混用了 catkin/colcon 缓存,先清) |
踩坑清单(实战汇总)
- 收不到消息先查 QoS:
ros2 topic info --verbose,Reliability/Durability 不兼容是头号原因,且系统不报错 - 单线程 executor 饿死控制回路:控制/状态机回调与网络/服务回调混在一个节点,必须 MultiThreadedExecutor + 独立 callback group
- colcon 没 install 目标:
ros2 run报 executable not found,看 CMakeLists 的install(TARGETS ...) - 跨机通信失败:先查组播/二层网络,再试
--net=host,最后换 RMW(Cyclone) - 混用时间源:use_sim_time 与系统时间混用导致 TF 报 extrapolation,统一时钟源
- ROS1 迁移隐藏差异:
advertise的 queue size 在 ROS2 里是 QoS depth,语义是”未消费消息上限”而非”发送缓冲” - Docker 权限:以 root 建的 install/ 目录,非 root 用户 source 报权限错——容器内统一用户,或把 install 挂到可写卷
迁移 Checklist(收工前逐项过):①
ros2 doctor全绿 ② 所有 topic 用--verbose查过 QoS 兼容 ③ 控制节点确认独立 callback group ④ 时间源统一 ⑤rosdep解析通过 ⑥colcon build无 warning。