时间 API 与双时钟体系
IMU 200Hz、相机 30Hz,时间戳差一点数据就对不上。本课讲清 ROS2 为什么自建时间抽象层、系统时钟与 /clock 仿真时钟如何透明切换,以及 create_timer 与 Header.stamp 的正确姿势。
目标本课学完,你应该能做到
能阐述 Clock 的双时间源机制:use_sim_time 是唯一开关,get_clock().now() 是唯一出口;系统时钟与 /clock 仿真时钟如何透明切换。
能正确使用 get_clock().now()、create_timer(period, callback),以及 Time 的 nanoseconds / seconds_nanoseconds 转换。
独立编写一个用定时器周期采样 MQ5、并为每条数据的 Header.stamp 打上采集时刻时间戳的发布节点。
能分析"消息时间戳不一致"的成因(系统时钟 vs 传感器硬件时钟),并用 ros2 topic delay 测量端到端延迟、识别负延迟的含义。
难点:开着
use_sim_time=true 却没有 /clock 发布者时,节点会永久阻塞——这个现象很难自己猜出来。use_sim_time 的前提,务必学扎实。01问题引入:时间不对齐,数据全白采
time.time()?仿真里"1 秒"可能只要真实 0.1 秒,录包回放时两条消息的时间戳也根本对不上。生活化地说:多传感器融合就像一场合唱——IMU 是快声部(200 拍/秒),相机是慢声部(30 拍/秒),必须都看着同一个指挥(统一时钟)才能合上拍。谁自己偷偷看手机计时(time.time()),整团就乱。
😤 各看各的表:time.time() 混用四宗罪
- 仿真失真:Gazebo 0.5 倍速时,仿真"1 秒"= 真实 2 秒,时间戳全错
- 回放失效:录包回放时 stamp 是"当时录制时刻",和新数据永远对不上
- 跨机负延迟:两台电脑钟差 50ms,算出 -50ms 的"延迟"
- 配对失败:温度 1.002s、湿度 1.008s 到达,谁配谁?
😎 全团看指挥:get_clock().now()
- 所有节点问同一个 Clock,答案永远一致
- 仿真模式自动换"指挥"为 /clock 话题,代码零改动
- rosbag 回放发 /clock,历史时间轴整体复现
- stamp 盖在采集时刻,延迟测量才有物理意义
get_clock().now() 永远返回"当前正确的时间"。02先看现象:让时间看得见
跑起 time_demo_node,用三条命令把"时间"变成看得见的数据。命令背后的代码在 05 节逐行拆。
先进入源码目录(与实验 6 的 common.py 同目录,才能直接 import 它读 MQ5),再启动节点——1Hz 采样定时器 + 5s 摘要定时器同时开跑,日志里直接打印 stamp:
cd ~/ros2_ws/src/xflz_eai_car1/driver/ros_sensor/ros_sensor/ros_sensor python3 time_demo_node.py
stamp 的 sec 一格一格 +1——这就是"时间在数据里流动"的样子:
ros2 topic echo /sensor_data

平均频率应贴近 1.000 —— 偏差大说明回调太重拖了后腿(03 节原理):
ros2 topic hz /sensor_data

delay = 收到时刻 − stamp,反映通信栈实时性能;出现负值先怀疑时钟不同步:
ros2 topic delay /sensor_data
✔ 三条命令读完,你已经把"时间"从抽象概念变成了三个可测量的数:stamp、rate、delay。
03原理讲解
一行调用,两套时钟,一个 bool 开关
① 调用:想获取现在的时间戳,命令行里写 now = self.get_clock().now(),功能就一句话——问当前的钟"现在几点"。
② now 是什么:now 是一个 ROS 2 时间对象(rclpy 的 Time),里面装着 sec(秒)和 nanosec(纳秒)两段数值;它不是字符串,调用 now.to_msg() 就能变成 Header.stamp 那样的时间戳,直接盖到消息上。
③ 和 /clock 什么关系:这个 now 的时间从哪来,由钟自己决定——当 use_sim_time 开成 true 时,get_clock().now() 读的就是 /clock 话题馈进来的时间;普通模式下读的则是操作系统墙上钟。
④ 两套钟,一个开关:系统墙上钟、仿真钟两套并存,靠 use_sim_time 这个 bool 值切换(false 用墙钟、true 用 /clock)——你自己的代码一行都不用改,永远只有上面那一行调用。先看框图,再看详解:
node.get_clock().now()
│
├── use_sim_time = false(默认)
│ 时间源:操作系统墙钟(现实时间)
│ 特征:和现实世界一秒一秒同步,不能快进/暂停
│ 适用:真机、真实传感器(USB 相机 / IMU / 雷达)
│
└── use_sim_time = true
时间源:订阅 /clock 话题
特征:Gazebo / rosbag play 说了算——可 0.5 倍速、可暂停、可回退
适用:仿真、录包回放
⚠️ 坑:true 了却没人发 /clock → 节点拿不到时间,死等不走(04 节实操 A)
/clock 话题的钟。整套系统开关必须统一。| 项目 | use_sim_time=false(墙钟) | use_sim_time=true(仿真时间) |
|---|---|---|
| 时间来源 | Linux/Windows 系统时钟 | 话题 /clock(消息里的时间) |
| 时间流速 | 固定 = 现实时间 | 任意:可快、可慢、暂停、回退 |
| rosbag 回放 | 回放时代码里的时间还在往前走,不跟着 bag 放慢 | 回放时间跟着 bag 走,算法行为和录制时一模一样 |
| 适用场景 | 真机,真实 IMU / 相机 / 雷达 | Gazebo 仿真、rosbag 离线回放、算法复现 |
get_clock().now() 按开关自动选数据源,业务代码一行不改,也不需要把两个时间对齐——同一节点同一时刻永远只用一套。② 所有参与融合的节点,开关必须统一:激光
true + IMU 融合 false = 两个节点时间轴不一样,时间戳错乱,融合直接炸——真机全 false,仿真全 true。use_sim_time:=true,会怎样?
查看答案
节点继续读系统墙钟;仿真加速跑时消息疯狂涌出、算法来不及处理,容易丢数据、融合发散。若别的节点开了true,还会和它时间轴对不上——开关混用的后果见坑表「开关不统一」行。Q2 · launch 里写了
use_sim_time:=true,但没有启动 Gazebo / rosbag play,节点会怎样?
查看答案
没人发/clock,get_clock().now() 拿不到时间——节点阻塞在那里不工作(04 节实操 A 复现的就是它)。python3 time_demo_node.py --ros-args -p use_sim_time:=true;或 launch 字典里写 'use_sim_time': True(EXP-17 用这个口子)。② 真机默认
false 不动;一旦开了 true,必须有节点往 /clock 发消息,否则本节点永久等待——04 节实操 A 就是在复现它。③ 运行中也能改,改了立即生效:节点跑起来后执行
ros2 param set /节点名 use_sim_time true,TimeSource 的参数回调当场切换时间源(订阅/退订 /clock),不用重启。但切换瞬间 now() 会从墙上钟绝对秒(17 亿级)跳到仿真起点(或反跳),跨切换点的时间差、超时、TF 外推全会算错——所以稳妥做法仍是启动时定好(先设后用)。04 节实操 A 用的就是①里的命令。下面的交互演示可以亲手切换时间源,建议把每个按钮都点一遍:
ROS_TIME / ROS_SYSTEM,容易误以为业务代码要自己判断时钟类型。实际只有一个开关 use_sim_time、一个出口 get_clock()——分辨时间源是 Clock 的内部实现,业务代码不许自己判断。图 11-1 · 一行代码,两种世界 | 回答:切换时间源后节点的 stamp 跟着怎么变?
Clock 读墙上钟,stamp 与真实世界同步——真机 B2 实验箱的唯一正确选择,也是你前十个实验一直在用的模式。
Clock 改听 rosgraph_msgs/msg/Clock:仿真器推进一步发一次,stamp 跟着走一步——可快、可慢、可停、可回到录制时的历史。
演示里「卡 4 秒」只是预演;真机上误开 true 且没有仿真器,Clock 会永久等待——节点启动后一片寂静(06 节实操 B 复现)。
整个演示里顶部那行代码从未变过——真机 ↔ 仿真 ↔ 回放切换,一行代码都不用改,这就是时间抽象层的全部意义。
时间的两副面孔:代码里一副,网络上一副
同一个"时刻"在两个场合长得不一样:写代码时要能加减比较(好算逻辑),发消息时要紧凑可序列化(好传输)。ROS2 干脆拆成两个类型,分工明确:
now = clock.now() later = now + Duration(seconds=5.0) # Time ± Duration dt = t2 - t1 # → 得 Duration now > past # 可直接比较
像个计算器:能加减、能比较。回调里判断"超时没?差多久?"全靠它——但没法直接塞进消息发出去。
stamp: sec: 1786615681 # int32 · Unix 秒 nanosec: 44442152 # uint32 · 纳秒
像个快递单:只有两个字段,紧凑、可序列化,嵌在 Header.stamp 里跨机传输——但自己不会算术。
to_msg(),消息 → API 用 Time.from_msg()。05 节代码精讲里 now.to_msg() 那一行干的就是这件事。create_timer 的真实行为与 stamp 规范
create_timer(period, cb) 注册触发源;spin 每轮检查到点就把回调加入执行队列——没有 spin 就永远不触发,取代 ROS1 的 while+Rate.sleep()。
周期只是名义下限:回调 C=0.3s > 周期 T=0.2s 时,上一回调结束下一触发点早已过期 → 实际间隔≈C,f ≈ 1/max(T,C) = 1/0.3 ≈ 3.3Hz,永远到不了 5Hz。I/O 密集任务要异步化。
demo 节点同时挂 1s 采样定时器 + 5s 摘要定时器,各自独立计时,但回调在同一事件循环里排队——一个堵全堵。
在读传感器的那行代码处取 now();等处理完再盖戳,等于把处理延迟算进了传输延迟——排查延迟问题时会误导自己。
04实验步骤
工程化顺序:先看全景(图 11-3)→ 再看节点内流程(图 11-4)→ 最后逐行读代码。
time_demo_node 逐行拆解
源码来自指导手册实验 11 的 time_demo_node.py:全文件只做三件事——两个定时器、盖时间戳、发摘要。
第一站 · __init__:发布器 + 两个定时器
# 来源:time_demo_node.py __init__ self._publisher = self.create_publisher(Header, 'sensor_data', 10) # 用 Header 消息演示 stamp 字段——它正是所有传感器消息"头部"的原型 self._timer1 = self.create_timer(1.0, self._timer1_callback) # 1Hz:采样发布 self._sample_count = 0 self._timer2 = self.create_timer(5.0, self._timer2_callback) # 0.2Hz:状态摘要 self.get_logger().info(f'系统时钟模式: use_sim_time={self.get_parameter("use_sim_time").value}') # use_sim_time 是系统预声明参数——不用 declare 就能 get,它由 --ros-args -p 控制
第二站 · timer1_callback:盖时间戳的规范姿势
# 来源:time_demo_node.py _timer1_callback(每秒一次) now = self.get_clock().now() # ① 在"采集动作"这一行取时间——不是函数末尾 now_msg = now.to_msg() # ② API 类型 → 消息类型(rclpy.time.Time → builtin_interfaces/Time) msg = Header() msg.stamp = now_msg # ③ ⭐ 核心:把采集时刻盖进消息头 msg.frame_id = 'sensor_frame' # 坐标系名,TF2 章节会再见到它 self._publisher.publish(msg) self.get_logger().info( f'采样 #{self._sample_count:04d} | stamp: {now_msg.sec}.{now_msg.nanosec:09d} | ...') # nanosec 用 :09d 补零到 9 位——否则 44442152 会被误读成更高精度
第三站 · timer2_callback 与时间运算
# 来源:time_demo_node.py _timer2_callback(每 5 秒摘要) self.get_logger().info( f'[状态摘要] 累计采样次数: {self._sample_count}, ' f'当前ROS2时间: {self.get_clock().now().nanoseconds} ns') # nanoseconds:Unix 纪元起的总纳秒数(int,无溢出风险;C++ 要用 uint64_t) # 时间运算三件套(可加进节点练习) from rclpy.time import Duration future = now + Duration(seconds=5.0) # Time ± Duration = Time diff = now - past # Time − Time = Duration(.nanoseconds/1e9 得秒) print(now > past) # Time 之间可直接比较
rclpy.time.Time(API 对象,可加减)与 builtin_interfaces/msg/Time(传输结构,字段 sec/nanosec)——过界必须 to_msg() / Time.from_msg() 转一道,直接赋值字段会类型报错。rclpy.spin(node) 的事件循环上:spin 不跑,定时器永远不触发。同一节点多定时器在同一线程串行——一个回调堵 3 秒,另一个也跟着饿 3 秒。亲手"接管"一个节点的时间轴
让节点不再看墙上钟,改听手动发布的 /clock——卡在哪、怎么复活,看图 11-5:
复现 02 节演示器预演过的场景——节点启动后一片寂静:
cd ~/ros2_ws/src/xflz_eai_car1/driver/ros_sensor/ros_sensor/ros_sensor python3 time_demo_node.py --ros-args -p use_sim_time:=true
从仿真秒 100 开始,每秒推进 1 仿真秒——观察终端 A 的 stamp 被"接管":
ros2 topic pub /clock rosgraph_msgs/msg/Clock "{clock: {sec: 100, nanosec: 0}}" --rate 1
✔ 再试 --rate 0.5(0.5 倍速):stamp 推进变慢,1Hz 定时器真实触发频率约 0.5Hz——仿真时间与真实时间彻底解耦。
| # | 证据项 | 记录方式 | 合格标准 |
|---|---|---|---|
| 1 | stamp 可见 | topic echo 截图 | sec 逐条递增、nanosec 补齐 9 位 |
| 2 | 节拍准确 | topic hz 截图 | average rate ≈ 1.000 |
| 3 | 延迟量级 | topic delay 截图 | 正值、毫秒级;能解释负值含义 |
| 4 | 卡死复现 | true 无 /clock 时的终端截图 | 节点启动后无任何采样输出 |
| 5 | 时间轴接管 | pub /clock 后终端 A 截图 | stamp 从 sec=100 开始递增 |
| 6 | 变速体验 | --rate 0.5 后对比截图 | 说出真实频率与仿真频率的关系 |
05注意事项与验收清单
- 默画双时钟架构:use_sim_time 是源开关,get_clock().now() 是统一出口
- 说清 false=墙钟(真机)、true=/clock(仿真),并强调同一系统所有节点开关必须统一
- 讲清 Time(API)与 builtin_interfaces/Time(消息)的互转关系
- 实测 topic hz ≈ 1.000、topic delay 正常量级,理解负值的含义
- 亲手体验一次:true 开着没 /clock 的阻塞,以及手动 pub 的"复活"
- 说出 stamp 应该盖在采集时刻的哪一行代码
- 解释回调超时为什么会拖低实际频率(名义周期 ≠ 实际频率)
| 坑 | 症状 | 解法 |
|---|---|---|
| 忘调 spin | 定时器永远不触发 | 节点创建后必须 rclpy.spin(node) 启动事件循环 |
| init 顺序错 | get_clock().now() 报 Clock not initialized | 先 rclpy.init() 再建节点,之后才能取钟 |
| 真机误开 use_sim_time | 节点卡死没有任何输出 | 无 /clock 发布者即永久等待——真机保持 false |
| delay 出现负值 | 跨机测量越测越离谱 | 两机 NTP 对时;单机则查 stamp 是不是盖晚了 |
| topic delay 报 no header | msg does not have header | delay 要的是嵌套 .header 字段——String、裸 Header 都会这样报错(EXP-16 的 /chatter 同理),换带 header 的话题即可 |
| 运行中切换 use_sim_time | 时间大跳变(墙钟 17 亿秒 ↔ 仿真 0),超时/TF 一串算错 | ros2 param set 改了立即生效、无需重启;代价是切换跳变,稳妥仍是启动时定好 |
| 开关不统一 | 融合节点时间戳错乱、数据对不上 | 同一系统所有节点 use_sim_time 必须设成同一个值——真机全 false,仿真全 true |
true → 仿真时间(/clock 话题时间):不读系统钟,专门订阅
/clock 拿时间,由仿真器 / rosbag play 发布——可暂停、倍速、回退。不需要"两个时间保持一致":同一节点同一时刻只选一套时间源,二选一,不会同时用。
对应关系:真机实车真实传感器 →
false;Gazebo 仿真、rosbag 回放 → true——你理解的这套就是标准答案;唯一强制要求是全系统开关统一。get_clock()),只有一个开关(use_sim_time)。凡是绕过出口直接读 time.time() 的代码,在仿真与回放里一定会出错;凡是开着 use_sim_time=true 却没人发 /clock 的机器,节点一定会永久卡住。这两条覆盖了本课 90% 的故障。06本课总结与验证题
Clock 吸收系统时钟与 /clock 的差异,上层代码仿真真机零改动。
to_msg()/from_msg() 互转;sec+nanosec 结构贯穿所有 Header。
名义周期受回调耗时拖累;多定时器同循环串行。
delay/hz 诊断通信质量;负延迟先怀疑时钟未对齐。
-p use_sim_time:=true 运行,/clock 正以 1Hz 持续发布。把参数改成 false 重启后,stamp 的数值来源和步进会怎么变?
参考答案与得分点
得分点:①来源切回系统墙上钟(Unix 真实时间)②stamp 立即跳到当前真实时刻并按真实秒递增 ③不再受 /clock 影响(①②各 40%,③ 20%)timer1_callback 里的 now = self.get_clock().now() 挪到 publish() 之前,且取 now 前先 sleep(0.2)。ros2 topic delay(读数 = 收到时刻 − stamp,见 02 节)的读数会偏大还是偏小?还测得准"采集→送达"的真实延迟吗?
参考答案与得分点
得分点:①stamp 盖晚 0.2s → delay = 收到 − stamp 变偏小,极端时可为负 ②测不到"采集时刻",读数丢失处理段,不再是采集→送达延迟 ③印证 stamp 必须盖在采集行。(①②各 40%,③ 20%)delay = -50ms。写出等式成因,并给出两种修复。
参考答案与得分点
得分点:①t_recv − t_stamp = −0.05s → 钟差/盖晚(本机钟慢 50ms 或 stamp 晚于真实采集)②修复 A:两机 chrony/NTP 对时 ③修复 B:核查 stamp 是否在采集那一刻取。(① 50%,②③各 25%)--rate 1 改成 --rate 0.5。以真实秒计,1Hz 采样定时器的实际触发频率约多少?每次采样的 stamp 相比上一条增加多少?
参考答案与得分点
得分点:①仿真时间流速 = 0.5 仿真秒/真实秒 → 定时器周期 1.0 仿真秒 = 2.0 真实秒 → 实际约 0.5Hz ②每次触发 stamp 读数 +1.0 仿真秒(按仿真轴走)③真实频率减半、仿真步进不变。(①②各 40%,③ 20%)now_msg = now.to_msg(),直接写 msg.stamp = now。会发生什么?正确写法里 msg.stamp 的类型是什么?
参考答案与得分点
得分点:①类型不匹配报错:rclpy.time.Time ≠ 消息字段类型 ②正确类型 builtin_interfaces/msg/Time(sec + nanosec)③过界必转 to_msg()/from_msg()。(①②各 40%,③ 20%)