本节摘要:Gazebo 世界里同时存在三种时间——墙钟时间、仿真时间、步长计数,实时因子 RTF 是前两者之比。所有系统(物理、传感器、你的控制器)都必须以
/clock广播的仿真时间为唯一时间源;混用墙钟时间是仿真代码最常见的隐性 bug 来源。本节给出 RTF 的计算口径、时间源对照表和一套 RTF 掉速排障流程。
ROS 2 的 RViz 默认用本机时钟给数据盖时间戳,而仿真里的传感器消息盖的是仿真时钟。当 RTF 不是 1.0 时,两套时间线的偏差持续拉大——tf 树报"extrapolation into the future"、message_filters 把正常数据当过期丢弃,全是这笔账没对齐的症状。解法只有一个:
# 让你的 ROS 节点使用仿真时钟:桥接 /clock 并声明 use_sim_time ros2 run ros_gz_bridge parameter_bridge /clock@rosgraph_msgs/msg/Clock[gz.msgs.Clock export GZ_SIM_REAL_TIME_FACTOR=1.0 # 需要严格实时时锁死 RTF # ROS 侧所有节点参数 use_sim_time:=true
为什么必须这么麻烦?因为仿真时间可以被暂停、加速、倒回,而墙钟时间不能。仿真里按了暂停键,物理和传感器全停,你的控制器如果看墙钟,会以为世界还在流逝、然后基于"过期的世界"继续做决策。仿真世界里,墙钟时间是伪钞。
| 时间 | 定义 | 由谁推进 | 用在哪 |
|---|---|---|---|
| 仿真时间 sim time | 世界累计步数 × 步长 | 物理系统每步 +dt | 所有系统的时间戳、你的控制器 |
| 墙钟时间 real time | 进程真实经历的时间 | 操作系统 | 只用于计算 RTF,不该进业务逻辑 |
| 步长 step size | 一步的 dt(如 1ms) | 世界配置 | 物理精度与成本的旋钮 |
RTF(Real Time Factor)= 仿真时间增量 ÷ 墙钟增量。RTF=1 即实时;RTF=0.5 意味着 1 小时测试要 2 小时墙钟;RTF=4 则是"快进"。gz-sim 的 GUI 状态栏实时显示它。
kp 设得极大导致求解器迭代暴涨——CPU 侧还不起账。-s)直接消掉这项。排障顺序按"先隔离、后归因"走:
多系统协作的顺序问题由服务器的迭代调度管:一次迭代 = 各系统按配置顺序执行(通常物理最后写位姿)→ 仿真时间 += dt → 发布 /clock。两个实用推论:
<update_rate>10</update_rate>,账记在仿真时间上,与墙钟无关。/cmd_vel 发到总线上,下一次迭代才被 UserCommands 系统消费。真实世界里控制延迟是纳秒到毫秒级,仿真里这个"最多一迭代"的延迟通常更小——一笔天然存在的虚实账差,做延迟补偿研究时要显式补上。gz topic -e -t /clock --num 10 | grep sim # 看仿真时间是否单调推进 # GUI 状态栏看 RTF;或服务端日志的 real time factor 行
逐项核对:所有 ROS 节点 use_sim_time:=true?传感器 <update_rate> 与控制器周期是否匹配(控制器 50Hz 而雷达 5Hz,融合算法会饿)?RTF<1 时你的记录脚本有没有把"仿真时长"误当"真实时长"统计?步长是否小于系统最快动态周期的十分之一(否则积分误差吃掉精度,第 3 章展开)?
GUI 状态栏的 RTF 是瞬时值,掉速问题的完整证据是一条随仿真时间变化的 RTF 曲线——很多瓶颈只在场景复杂度上来之后才现形(比如机器人抓起第 30 个物体那一刻)。用一行 gz stats 加简单统计就能生成这份证据:
# 每秒采样一次 RTF 与仿真时间,落盘备查 gz stats -p | stdbuf -oL grep real_time_factor >> rtf_log.csv
# 事后分析:找出 RTF 掉到 0.8 以下的时段及其仿真时刻 import re, statistics rows = [] for line in open("rtf_log.csv"): m = re.search(r"real_time_factor: ([0-9.]+).*sim_time: ([0-9.]+)", line) if m: rows.append((float(m.group(2)), float(m.group(1)))) drops = [(t, r) for t, r in rows if r < 0.8] print(f"样本 {len(rows)} 个,RTF 中位数 {statistics.median(r for _, r in rows):.2f}") if drops: t0 = drops[0][0] print(f"首次掉速发生在仿真时刻 {t0:.1f}s —— 对照当时的场景事件定位诱因")
这段分析的价值在最后一行输出:首次掉速的仿真时刻是定位诱因的锚点——把它与测试脚本的动作日志对齐,往往正好对应"生成第 N 个物体""相机进入高纹理区域""插件开始写盘"中的某一项,比笼统的"仿真有点卡"能省掉一半排查时间。配合 2.1 节的进程隔离实验(同一时刻用 -s 复跑),渲染账与物理账当场分账。
use_sim_time 不是可选项:桥接 /clock + 全节点启用,是仿真-ROS 协作的地基。时间这条暗线铺好了,剩下的一个架构问题是:如何在不改 Gazebo 源码的前提下,把自定义行为注入这条流水线?答案是插件——2.3 的主角。