ROS2 Jazzy + C++ 时间系统——Time / Clock / Duration

继"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
            ↓
        多个线程

整个运行机制就出来了。

相关推荐
克里斯蒂亚诺更新1 小时前
理解机器学习的各种算法
人工智能·算法·机器学习
mtouch3331 小时前
技术解析|无人机倾斜摄影3D模型全天候动态光照融合方案
人工智能·无人机·虚拟现实·电子沙盘·数字沙盘·无人机倾斜摄影
小范的技术工坊1 小时前
大模型学习操作文档(13个核心概念串讲)
人工智能·学习·算法·大模型
索端阳1 小时前
I.MX6ULL 裸机开发:LCD Framebuffer 与 RGB888 学习笔记
笔记·vscode·嵌入式硬件·学习
DP DPharness1 小时前
按下撤回却没变化:dsh-recall-plugin 故障速查与排错清单
人工智能·dpharness
MetaEnchanter1 小时前
Word 提取图片:不用逐张截图,Excel、PPT 也能取
人工智能·ai·百万工具
腾渊信息科技公司1 小时前
腾渊科技重磅出品——工业软件智能化落地指南:从AI辅助到AI原生的架构演进
人工智能·科技·ai-native
2401_865261631 小时前
亦唐科技(YIKTONG):国产贴片机行业的技术先锋与市场领军者
人工智能·科技
重生之小比特1 小时前
【C++进阶】unordered_set 和unordered_map 深度解析
c++·哈希算法