3. 时间同步问题:仿真时间与真实时间的偏差、时间戳丢失、使用 time.time() 的陷阱
时间,在 SITL 仿真里是个特别容易翻车的东西。
你可能觉得,不就是打个时间戳嘛,有什么难的?
嗯,我以前也这么想。直到有一次,我采集了一整晚的仿真数据,第二天分析时发现时间轴完全是乱的——有的数据点时间戳比前一个还早,有的直接是 0。那感觉,就像熬夜写好的报告被猫踩了一脚。
这一节,我们就来聊聊仿真时间同步的三个核心坑:仿真时间 vs 真实时间的偏差、时间戳丢失、以及 time.time() 的陷阱。
核心观点:在 SITL 中,你面对的是两套时钟——仿真世界里的逻辑时钟,和操作系统里的真实时钟。搞混它们,数据就废了。
3.1 仿真时间 vs 真实时间:两个世界的时间流
SITL 跑起来后,仿真引擎会维护一个自己的时钟。这个时钟可以跑得比真实时间快,也可以慢,甚至可以暂停。比如你设置 --speed 2.0,仿真时间就比真实时间快一倍。
问题来了:你的数据采集程序,用的是哪个时间?
我个人习惯,在采集数据时,必须明确区分两种时间戳:
- 仿真时间(Sim Time):从仿真引擎内部获取,代表模型世界里的时刻。用于分析飞行器行为、控制逻辑、物理量变化。
- 真实时间(Wall Clock Time):从操作系统获取,代表数据被记录的真实时刻。用于分析采集延迟、系统负载、网络抖动。
如果你把真实时间当成仿真时间去分析飞行轨迹,那速度一变化,所有时序分析全错。我在项目中遇到过有人用真实时间画飞行曲线,结果仿真加速跑时,曲线被压成了一团。
| 时间类型 | 来源 | 用途 | 典型坑 |
|---|---|---|---|
| 仿真时间 | 仿真引擎(如 Gazebo 的 /clock topic) | 分析飞行器行为、控制逻辑 | 加速/减速时与真实时间不同步 |
| 真实时间 | 操作系统(time.time()、datetime.now()) | 分析采集延迟、系统负载 | 受系统时间调整、NTP 同步影响 |
我的建议:在数据记录中同时保留两种时间戳。字段名可以叫 sim_time_sec 和 wall_time_sec。这样后期分析时,你随时可以切换视角。
3.2 时间戳丢失:数据来了,时间没来
另一个常见问题:数据包到了,但时间戳字段是空的,或者全是 0。
为什么会这样?
说白了,就是采集线程和仿真引擎之间的时序竞争。仿真引擎发送数据时,时间戳字段可能还没填好,就被采集程序读走了。或者,采集程序在初始化阶段先收到了数据,但时间同步机制还没就绪。
我曾经在调试一个多旋翼的 SITL 时,发现前 200 条数据的时间戳全是 0。排查了半天,发现是采集程序在仿真引擎的 /clock 话题还没发布时就开始了订阅。解决方案很简单:等待第一个有效时间戳到来后再开始记录。
避坑指南:我曾经在采集代码里加了一个简单的等待循环,直到收到第一个非零时间戳才打开数据文件。这个习惯救了我很多次。
具体做法:
- 在采集循环中,检查时间戳是否大于 0。
- 如果连续 N 次收到无效时间戳,记录警告日志,但不写入数据文件。
- 如果时间戳突然回退(比上一个还小),丢弃该数据包并记录错误。
# 伪代码示例:安全的时间戳采集
last_timestamp = 0
valid_started = False
while running:
data = receive_sitl_data()
ts = data.timestamp
if ts <= 0:
log.warning(f"无效时间戳: {ts},跳过")
continue
if not valid_started:
if ts > 0:
valid_started = True
log.info("收到第一个有效时间戳,开始记录")
else:
continue
if ts < last_timestamp:
log.error(f"时间戳回退: last={last_timestamp}, current={ts},丢弃")
continue
last_timestamp = ts
write_to_file(data)
3.3 time.time() 的陷阱:你以为的「现在」不是现在
很多人在采集程序里直接用 time.time() 打时间戳。看起来没问题,对吧?
嗯,这里要注意。 time.time() 返回的是系统时钟的秒数。但系统时钟会受 NTP 同步、手动调整、甚至闰秒的影响。你想想看,如果采集过程中 NTP 把时间往回调了 0.5 秒,你的时间戳就出现了「倒流」。
更隐蔽的问题是:time.time() 的精度和分辨率在不同平台上不一样。在 Windows 上,它的精度可能只有 15 毫秒左右。对于高速仿真(比如 1000 Hz 的数据采集),这个精度完全不够用。
| 平台 | time.time() 典型精度 | 建议替代方案 |
|---|---|---|
| Linux | 微秒级(~1 µs) | time.monotonic() 或 clock_gettime() |
| Windows | 毫秒级(~15 ms) | time.perf_counter() 或 QPC |
| macOS | 微秒级(~1 µs) | time.monotonic() 或 mach_absolute_time() |
我的建议:在 Linux 上做 SITL 采集时,用 time.monotonic() 替代 time.time()。它不受系统时间调整影响,只增不减,适合做时间差计算。如果你需要绝对时间戳,可以额外记录一个 time.time() 作为参考,但不要用它做时序分析。
3.4 一张图看懂时间同步的核心逻辑
下面这张图,是我自己总结的 SITL 时间同步处理流程。每次写采集程序前,我都会对照着检查一遍。
3.5 实战建议:一个健壮的时间采集方案
说了这么多坑,那到底该怎么写采集代码?
我分享一个自己用了很久的方案,核心就三条:
- 双时间戳策略:每条数据同时记录仿真时间和真实时间(用 monotonic 时钟)。
- 前置等待:采集开始前,等待第一个有效仿真时间戳到来。
- 后置校验:写入数据前,检查时间戳是否单调递增。如果不是,直接丢弃。
总结一下:时间同步问题,本质上是「你以为的时间」和「仿真世界的时间」之间的鸿沟。填平这个鸿沟的方法,就是明确区分、严格校验、绝不妥协。
嗯,这一节的内容就到这里。时间问题说复杂也复杂,说简单也简单——只要你记住:永远不要相信一个没有校验过的时间戳。