4、GPS/RTK时间同步:基于PPS脉冲与GPRMC语句的授时机制

时间同步这件事,在自动驾驶里有多重要?

我打个比方你就明白了。你开车时,眼睛看到前方有障碍物,耳朵听到旁边有喇叭声,大脑需要把这些信息在同一个时间点上融合起来。如果眼睛看到的是0.1秒前的画面,耳朵听到的是0.2秒前的声音,那你判断出来的结果肯定不准。

多传感器融合也是这个道理。激光雷达、摄像头、毫米波雷达、IMU,每个传感器都有自己的时钟。如果不做时间同步,你拿到的数据就是「各说各话」——明明是一个时刻的场景,却被贴上了不同的时间戳。

那GPS/RTK在这里扮演什么角色?说白了,它就是整个系统的「标准时钟源」。我习惯把GPS比作一个「时间裁判」——它从天上接收卫星信号,然后通过PPS脉冲和GPRMC语句,把精确的时间告诉其他传感器。

4.1 PPS脉冲:那个「啪」的一下

PPS,全称Pulse Per Second,每秒一个脉冲。你想想看,GPS卫星上搭载的是原子钟,精度在纳秒级别。GPS接收机解算出时间后,会在每个整秒时刻输出一个高电平脉冲——这就是PPS。

这个脉冲有多准?

我实测过,在RTK固定解状态下,PPS的上升沿抖动通常在±20纳秒以内。什么概念?光速是3×10⁸米/秒,20纳秒对应的距离误差只有6米。对于自动驾驶来说,这个精度完全够用。

核心要点:PPS脉冲只提供「时刻基准」,不提供「时间数值」。它告诉你「现在就是整秒」,但没告诉你「现在是哪一秒」。

那「哪一秒」这个信息谁来给?GPRMC语句。

4.2 GPRMC语句:时间数值的载体

GPRMC是NMEA 0183协议里的一条标准语句。它包含了UTC时间、日期、经纬度、速度、航向等信息。我截取一条典型的GPRMC语句给你看:

$GPRMC,123519.00,A,3150.7760,N,11712.4560,E,0.0,0.0,220324,0.0,E*6A

这条语句里,123519.00 就是UTC时间——12点35分19秒00毫秒。后面的 220324 是日期——2024年3月22日。

你可能会问:那PPS和GPRMC怎么配合?

嗯,这里有个关键点。GPRMC语句的发送时间,和PPS脉冲的上升沿,是有固定时序关系的。大多数GPS接收机在PPS上升沿之后的几十毫秒内,会输出对应的GPRMC语句。也就是说:

  1. PPS上升沿:告诉你「整秒到了」
  2. GPRMC语句:告诉你「刚才那个整秒是几点几分几秒」

两者结合,你就得到了一个带绝对时间戳的基准时刻。

4.3 授时机制:从GPS到传感器

我在实际项目中,通常采用两种方式来做时间同步:

方式一:硬件同步(推荐)

把GPS的PPS脉冲直接接到每个传感器的硬件触发引脚上。比如激光雷达,它支持外部触发模式。PPS一来,激光雷达就立刻开始扫描一圈。这样所有传感器的数据采集时刻,都被硬生生地「对齐」到了同一个时间基准上。

我的经验:硬件同步的精度最高,抖动通常在微秒级。但缺点是硬件布线复杂,而且不是所有传感器都支持外部触发。我遇到过一款摄像头,它的触发输入有5毫秒的延迟——嗯,这个坑我踩过。

方式二:软件同步(折中方案)

如果传感器不支持外部触发,那就只能用软件方式。具体做法是:

  1. 系统维护一个全局时钟,定期用GPS的PPS和GPRMC来校准
  2. 每个传感器数据到达时,打上当前全局时钟的时间戳
  3. 后处理时,根据时间戳做插值对齐

软件同步的精度取决于系统时钟的稳定性和中断响应速度。在Linux系统上,我一般用adjtimex来微调系统时钟,配合PPS的GPIO中断,可以把时钟漂移控制在几十微秒以内。

4.4 避坑指南:我踩过的那些雷

我曾经犯过一个低级错误:把PPS脉冲的极性搞反了。有些GPS模块输出的是高电平有效,有些是低电平有效。我那次用的是低电平有效的模块,但代码里配置的是上升沿触发——结果PPS来了,中断没响应,时间同步完全失效。查了两天才找到原因。

还有几个常见问题,我列出来供你参考:

  • PPS信号抖动:GPS信号弱的时候,PPS的抖动会变大。我建议在代码里加一个滤波逻辑,连续几个PPS的间隔如果超过1秒±100微秒,就认为当前信号不可靠。
  • GPRMC语句丢失:偶尔会出现GPRMC语句没收到的情况。我的做法是:用上一个有效的GPRMC时间,加上PPS的计数,推算当前时间。但要注意,如果GPS长时间失锁,这个推算会累积误差。
  • RTK和普通GPS的区别:RTK模式下,PPS的精度会更高。普通GPS的PPS抖动可能在±100纳秒,而RTK固定解可以做到±10纳秒。如果你的系统对时间精度要求极高(比如高速场景下的多传感器融合),建议用RTK。

4.5 代码示例:一个简单的授时模块

下面是我在项目中用过的伪代码框架,你可以参考一下:

// PPS中断服务函数
void pps_interrupt_handler() {
    pps_count++;  // PPS计数加1
    pps_flag = 1; // 标记PPS到达
}

// GPRMC解析线程
void gprmc_parse_thread() {
    while(1) {
        if(收到GPRMC语句) {
            utc_time = 解析时间字段();
            utc_date = 解析日期字段();
            last_valid_time = utc_time;
            last_valid_date = utc_date;
            pps_count_at_gprmc = pps_count; // 记录当前PPS计数
        }
    }
}

// 获取当前绝对时间
uint64_t get_absolute_time() {
    if(pps_flag) {
        pps_flag = 0;
        // 当前绝对时间 = 上次GPRMC时间 + (当前PPS计数 - 上次PPS计数)秒
        current_time = last_valid_time + (pps_count - pps_count_at_gprmc);
    }
    return current_time;
}

这段代码的核心逻辑就是:用PPS计数来「补足」GPRMC语句之间的时间间隔。只要GPS信号不丢,时间精度就能维持在微秒级。

4.6 小结

GPS/RTK时间同步,说白了就是两件事:

  • PPS脉冲:提供高精度的「时刻基准」
  • GPRMC语句:提供完整的「时间数值」

两者配合,就能为整个传感器系统建立一个统一的时间坐标系。我个人建议,只要条件允许,优先用硬件同步方式。如果不行,软件同步也能凑合,但一定要做好时钟漂移的监控和补偿。

下一章,我会讲激光雷达和摄像头的时间同步——那个更复杂,因为涉及到不同数据帧率的对齐。到时候我会分享一个我在实际项目中用到的「双缓冲对齐法」,挺实用的。