继"ROS2 Jazzy + C++ 实战路线------ros2_control"继续学习
这一课只需要搞明白:
机器人系统里的"时间"到底从哪里来。
普通 C++ 程序:
程序
↓
系统时间
↓
2026-09-18 15:30:20
看起来没问题。
但机器人仿真就麻烦了。
比如 MuJoCo / Gazebo:
仿真时间:
0s
0.01s
0.02s
0.03s
...
现实世界可能才过去:
0.001s
甚至:
暂停仿真
这时候:
程序不能简单地认为"现实时间 = 仿真时间"。
ROS2 因此提供了抽象的 ROS Time,并支持通过 /clock 提供仿真/回放时间。(GitHub)
你只需要先认识三个时间
System Time
Steady Time
ROS Time
1. System Time
就是:
电脑现在几点。
比如:
15:30:20
它跟操作系统的时间有关。
问题是:
如果系统管理员突然修改电脑时间:
15:30:20
↓
15:20:00
时间居然倒退了。
所以它不适合单纯拿来做:
"我这个程序到底运行了多少秒?"
2. Steady Time
这个很好理解:
只负责稳定地往前走,用来测时间间隔。
例如:
开始
↓
任务执行
↓
结束
你想知道:
用了 20 ms
这种场景就非常适合稳定时钟。
所以你可以记:
System Time
→ 现在几点
Steady Time
→ 过了多久
3. ROS Time
这是 ROS2 最重要的。
它可以理解为:
ROS2 自己使用的一套逻辑时间。
它可以来自:
真实世界
也可以来自:
仿真器
也可以用于:
rosbag 回放
所以:
真实机器人:
系统时间
↓
ROS Time
仿真:
仿真器
↓
/clock
↓
ROS Time
rosbag:
bag 数据
↓
/clock
↓
ROS Time
ROS 官方设计文档就是为了解决"真实运行、仿真、加速/减速回放"这些不同时间场景。(GitHub)
4. /clock
这是这一课最重要的东西。
假设:
Gazebo / MuJoCo
现在仿真到了:
10.52 s
它可以发布:
/clock
然后:
/clock
│
┌────────┼────────┐
↓ ↓ ↓
Node A Node B Node C
大家都按照这个 ROS 时间运行。
这样就能保证:
仿真里的各个 ROS2 节点使用同一个时间基准。
ROS 官方设计中,/clock 可以作为 ROS Time 的时间源;当使用模拟时间时,节点通过它获得仿真时间。(GitHub)
5. use_sim_time
你以后会经常看到:
use_sim_time
这个参数非常重要。
它可以简单理解为:
false
↓
正常使用系统时间
true
↓
使用 ROS 仿真时间
例如:
ros2 param set /your_node use_sim_time true
然后系统有:
/clock
这个节点就会使用 /clock 提供的时间。
6. Time 和 Timestamp
这个区别你也要建立。
Time
回答:
现在是什么时间?
例如:
t = 10.25s
Timestamp
回答:
这条数据是什么时候产生的?
例如:
Camera Frame
timestamp = 10.25s
所以:
Camera
│
├── image
└── timestamp = 10.25s
这对:
相机
IMU
LiDAR
动捕
关节
全部重要。
7. Duration
Duration 就是:
时间间隔。
例如:
10.20s
10.25s
差:
0.05s
这个:
0.05s
就是 Duration。
所以:
Time
↓
某个时刻
Duration
↓
两个时刻之间相差多久
8. Rate
你以前已经接触过:
50 Hz
在 ROS2 里就经常涉及 Rate。
比如:
50 Hz
意味着:
每秒执行 50 次
周期:
20 ms
所以:
Rate
↓
控制循环频率
比如:
控制器
↓
50 Hz
就是:
20ms
20ms
20ms
20ms
...
执行一次。
Time
↓
某个时刻
Duration
↓
两个时刻之间的间隔
Rate
↓
多长时间执行一次
Clock
↓
告诉程序"现在是什么时间"
/clock
↓
仿真/回放提供 ROS 时间
真实世界
↓
系统时间 / ROS Time
↓
传感器
↓
ROS2
↓
控制器
↓
机器人
仿真机器人
MuJoCo / Gazebo
↓
/clock
↓
ROS Time
↓
ROS2
↓
控制器
| 东西 | 大白话 |
|---|---|
| System Time | 电脑现在几点 |
| Steady Time | 稳定地计算过了多久 |
| ROS Time | ROS2 系统使用的逻辑时间 |
/clock |
给 ROS2 提供时间 |
use_sim_time |
是否使用仿真时间 |
| Time | 某个时刻 |
| Timestamp | 数据产生的时刻 |
| Duration | 时间间隔 |
| Rate | 多久执行一次 |
这时候你已经不是单纯在学几个 ROS2 命令,而是在建立:
ROS2 系统工程的整体框架。
9. Lifecycle
这一课你先抓住一个核心:
Lifecycle 是给 ROS2 节点增加"状态管理"的机制。
普通节点:
启动
↓
运行
↓
退出
Lifecycle 节点则更像机器人设备:
创建
↓
配置
↓
准备
↓
激活
↓
运行
↓
停止
↓
清理
这对真实机器人系统非常有意义。最核心的几个状态
Unconfigured
Inactive
Active
Finalized
最重要的是:
Unconfigured
↓
Inactive
↓
Active
1. Unconfigured
刚创建,还没有完成配置。
可以理解:
"我这个节点还没准备好。"
例如相机:
Camera Node
状态:
Unconfigured
还没有:
打开设备
读取参数
创建资源
2. Inactive
已经配置好了,但是:
还没有正式工作。
例如:
相机
↓
设备已经打开
↓
参数已经加载
↓
但是暂时不发布图像
就是:
Inactive
这个状态非常有用。
因为机器人启动时,可以先把所有模块准备好:
Camera → Inactive
LiDAR → Inactive
Controller → Inactive
全部检查完:
OK
然后统一进入:
Active
3. Active
这就是:
正式工作状态。
例如:
Camera
↓
Active
↓
发布 Image
控制器:
Controller
↓
Active
↓
开始控制机器人
所以:
Inactive
↓
Active
可以理解成:
"准备好了,现在正式开始干活。"
10. Lifecycle 的状态变化
可以先记这张图:
configure
Unconfigured ─────────→ Inactive
│
│ activate
↓
Active
│
│ deactivate
↓
Inactive
如果要重新配置:
Inactive
↓
cleanup
↓
Unconfigured
最终:
Active
↓
shutdown
↓
Finalized
Lifecycle 不是:
PID
MPC
IK
轨迹规划
它解决的是:
节点什么时候准备、什么时候工作、什么时候停止。
所以:
Lifecycle
↓
管理节点生命周期
而:
Controller
↓
控制机器人运动
是两件不同的事情。
11. 和 Launch 联系起来
你之前已经学过 Launch。
现在就可以理解一个真实机器人启动过程:
Launch
│
├── 启动 IMU
├── 启动 Camera
├── 启动 Robot Driver
├── 启动 Controller
└── 启动其他节点
如果这些节点都是 Lifecycle Node:
Launch
↓
创建节点
↓
configure
↓
检查
↓
activate
↓
机器人正式运行
所以以后你看到:
Lifecycle
Launch
Controller
不要把它们当成三个完全独立的东西。
它们可以配合起来做:
机器人系统的有序启动。
普通 Node:
启动 → 干活
Lifecycle Node:
启动
↓
配置
↓
检查
↓
准备
↓
激活
↓
干活
↓
停止
对于机器人来说:
"什么时候允许这个节点真正开始工作"是非常重要的。
Launch
│
↓
Robot System
│
┌──────────┼──────────┐
↓ ↓ ↓
IMU Camera Controller
│ │ │
Lifecycle Lifecycle Lifecycle
│ │ │
└──────────┼──────────┘
↓
ros2_control
↓
Hardware Driver
↓
电机
这就越来越接近真实机器人软件架构了。
Lifecycle
=
管理"节点什么时候可以工作"
而不是:
Lifecycle
=
控制机器人运动
前面你已经知道:
Node
↓
创建 Publisher / Subscriber / Timer / Service
↓
等待事件
↓
有事情发生 → 执行对应函数
现在我们要解决一个很关键的问题:
到底是谁负责调用这些函数?
答案就是:
Executor(执行器)
ROS2 的 Executor 负责调度 Node 中可以执行的 callback,比如订阅回调、Timer 回调、Service 回调等。(测试ROS2仓库)
12. Callback
Callback 翻译成:
回调函数
你可以先把它理解成:
"发生某件事情以后,ROS2 自动叫你的函数。"
例如:
subscription_ = this->create_subscription<std_msgs::msg::String>(
"chatter",
10,
std::bind(&MyNode::callback, this, std::placeholders::_1)
);
这里的:
callback()
就是一个 Callback。
当:
/chatter
↓
收到消息
↓
ROS2
↓
callback()
你不需要自己不停地:
while(1)
{
有没有消息?
}
ROS2 会负责调度。
13. 哪些东西会产生 Callback?
这个非常重要。
你前面学过:
1. Subscription
收到 Topic
↓
Subscription Callback
2. Timer
时间到了
↓
Timer Callback
比如:
create_wall_timer(
20ms,
timer_callback
);
50 Hz:
20ms
↓
timer_callback()
这和你之前做的50 Hz 动捕系统就直接联系起来了。
3. Service
收到 Service 请求
↓
Service Callback
4. Action
Action 里面也有多个 Callback,例如:
收到 Goal
↓
Goal Callback
取消请求
↓
Cancel Callback
执行任务
↓
Execute Callback
所以你现在可以形成一个非常重要的认识:
ROS2 中大量"事情发生后执行的函数",本质上都是 Callback。
官方文档也是这样定义的,包括 subscription、timer、service、action 等。(ROS 文档)
14. 那么 Executor 是干什么的?
一句话:
Executor 就是 ROS2 的"调度员"。
假设一个 Node 里面有:
Timer Callback
Subscription Callback
Service Callback
突然同时发生:
Timer 到时间了
+
收到一条 Topic
+
收到 Service 请求
谁决定:
现在执行哪个?
就是:Executor
可以把它想成:
┌── Timer Callback
│
Node ───────────┼── Subscription Callback
│
└── Service Callback
↑
│
Executor
调度执行
ROS2 官方对 Executor 的核心描述也是:它负责协调可执行任务,并决定这些 callback 如何被执行。(ROS 2 文档)
15. spin()
你以前应该已经见过:
rclcpp::spin(node);
现在你应该知道:
spin 并不是"让程序转起来"这么简单。
它背后实际上是在:
让 Executor 持续工作,等待并执行 Callback。
大概理解成:
rclcpp::spin(node)
↓
Executor
↓
不断检查有没有事情要处理
↓
有事情
↓
执行对应 Callback
↓
继续等待
所以:
rclcpp::spin(node);
可以暂时翻译成:
"让这个 Node 进入 ROS2 的事件循环,持续处理各种回调。"
16. SingleThreadedExecutor
最简单的是:
单线程执行器
你可以想象成一个人干活:
Executor
│
┌────────┼────────┐
↓ ↓ ↓
Timer Topic Service
│ │ │
└────────┼────────┘
↓
一个线程
一次只处理一个 Callback。
比如:
Timer Callback
↓
执行 100 ms
↓
结束
↓
Subscription Callback
↓
执行
如果 Timer Callback 卡住:
Timer Callback
↓
卡住 1 秒
↓
其他 Callback
↓
全部等着
这就是一个非常重要的问题。
17. MultiThreadedExecutor
也就是:
多线程执行器
变成:
Executor
/ | \
/ | \
Thread1 Thread2 Thread3
↓ ↓ ↓
Timer Topic Service
这样可以让不同 Callback 在满足条件时并行执行。
ROS2 提供 SingleThreadedExecutor 和 MultiThreadedExecutor 等执行模型。(测试ROS2仓库)
18. Callback Group
Callback Group 用来规定哪些 Callback 可以同时执行,哪些不能同时执行。
ROS2 主要有两种:
Mutually Exclusive
↓
同组 Callback 不允许并行
Reentrant
↓
允许并行执行
官方文档也是这样定义的。(ROS 文档)
你可以先理解成:
MultiThreadedExecutor
│
┌───────────┴───────────┐
↓ ↓
Callback Group A Callback Group B
↓ ↓
Timer Subscription
↓ ↓
不能并行? 可以并行?
现在你的 ROS2 大脑地图应该变成:
ROS2
│
┌────────┴────────┐
│ │
Node Node
│
┌───────┼────────┐
↓ ↓ ↓
Topic Timer Service
│ │ │
↓ ↓ ↓
Callback Callback Callback
│ │ │
└───────┼────────┘
↓
Callback Group
↓
Executor
↓
一个/多个线程
↓
CPU
这张图非常重要。
Executor 并不是机器人算法。
它更像是:
ROS2 系统里面负责"什么时候让哪个函数干活"的调度层。
Callback
↓
发生事件后执行的函数
Executor
↓
负责调度 Callback
spin()
↓
让 Executor 持续工作
SingleThreadedExecutor
↓
一个线程
MultiThreadedExecutor
↓
多个线程
1. 为什么需要 Callback Group?
假设一个机器人 Node:
robot_node
│
├── 动捕数据 Callback
├── IMU Callback
├── 控制 Timer
└── Service Callback
如果使用:
MultiThreadedExecutor
理论上多个 Callback 可以被不同线程执行。
例如:
线程1 → 动捕 Callback
线程2 → IMU Callback
线程3 → 控制 Timer
但是问题来了:
如果两个 Callback 操作的是同一个数据怎么办?
比如:
joint_state
同时:
线程1 → 修改 joint_state
线程2 → 读取 joint_state
就可能出现线程安全问题。
所以 ROS2 引入:Callback Group
它本质上就是:
给 Callback 分组,并规定这些 Callback 的并发规则。
ROS2 官方目前主要定义 Mutually Exclusive 和 Reentrant 两种 Callback Group。(ROS 文档)
2. Mutually Exclusive
这个名字看起来很复杂。
其实就是:
互斥组
假设:
Callback Group A
├── Timer Callback
├── IMU Callback
└── Service Callback
设置成:
Mutually Exclusive
意思就是:
同一时间只能执行一个。
例如:
Timer Callback
↓
正在执行
↓
IMU Callback
↓
等待
↓
Timer结束
↓
IMU Callback开始
即使你使用:
MultiThreadedExecutor
同一个互斥 Callback Group 里的 Callback 也不会同时执行。
3. Reentrant
Reentrant 可以简单理解成:
允许并发。
例如:
Callback Group B
├── Camera Callback
├── IMU Callback
└── Timer Callback
如果设置成 Reentrant:
Thread 1 → Camera Callback
Thread 2 → IMU Callback
Thread 3 → Timer Callback
它们允许同时执行。
但是:
允许并发 ≠ 一定并发。
最终还要看 Executor 有没有线程、系统有没有资源等。
| Callback Group | 能不能同时执行 |
|---|---|
| Mutually Exclusive | 同组不能同时执行 |
| Reentrant | 允许同时执行 |
所以:
Callback Group
↓
规定 Callback 之间的"交通规则"
4. Executor 和 Callback Group 的关系
现在把上一节和这一节合起来:
Node
│
┌─────────────┼─────────────┐
↓ ↓ ↓
Callback Callback Callback
│ │ │
└─────────────┼─────────────┘
↓
Callback Group
↓
Executor
↓
Threads
但是更准确一点:
Executor
/ \
Thread 1 Thread 2
│ │
↓ ↓
Callback Group A Callback Group B
│ │
Callback Callback
所以你以后看到:
MultiThreadedExecutor
不要只想到:
"多线程。"
你应该想到:
"多个 Callback 有没有机会并行执行?"
然后马上继续问:
"它们属于哪个 Callback Group?"
不要认为:
用了 MultiThreadedExecutor = 系统就自动变快。
不是。
多线程只是:
提供并发执行的可能。
如果你的代码:
Callback A
↓
锁住资源
Callback B
↓
等待
Callback C
↓
等待
最后还是:
一个一个执行
甚至可能更慢。
所以多线程真正解决的是:
让彼此独立的任务有机会同时工作。
rclcpp::executors::MultiThreadedExecutor executor;
executor.add_node(node);
executor.spin();
你现在应该能翻译成:
创建一个多线程 Executor
↓
把 Node 交给它
↓
开始不断处理 Callback
如果再加 Callback Group:
Node
│
├── Group A
│ ├── Timer
│ └── Service
│
└── Group B
├── IMU
└── Camera
↓
MultiThreadedExecutor
↓
多个线程
整个运行机制就出来了。