你所不知道的GPS周数轮转

背景

GPS的时间是基于GPS时间起点(UTC 1980-01-06 00:00:00 ),用经历过一个周数和周内秒数定义的。表述这个周数使用的是一个10bit的字段,因此每过1024周就会出现一次翻转事件。GPS在1999年8月21日出现过周数翻转,今年是在2019年4月6日出现周数翻转。针对此类重点的事件,如果出现问题,会导致公司的整个授时体系的时间回退到1980年,存在所有业务受影响可能性。因此我在前期做了比较多的分析工作。一方面同多家GPS授时服务器的厂商确认是否会有影响,另外一方面自己从相关的文献和代码上确认实际受影响的概率。最终确认这个事件对现在的授时体系100%无影响。

技术原理分析

GPS授时服务器,俗称原子钟。实际的结构是由一个x86服务器(主板上接有铷原子钟或恒温晶振),外接一个可接收GPS/北斗/Galileo信号的天线组成。设备就放在机房的工机具区域,蘑菇头天线置于机房外(绝大部分是置于房顶)。设备上电后,一般几分钟就可以收到4个以上的卫星信号,完成位置锁定和时间同步。开机一段时间后内置的原子钟会被GPS信号驯服,最终趋于一个稳定频率,使得当GPS信号断掉后,还能保持非常高的授时精度。当前世面上设备的常用原子钟的主流精度是年偏差不超过±3ms。
GPS的授时服务器内的天线模块,通过解析GPS的L1NAV报文,计算出对应的位置&时间等信息。GPS接收器把原始报文经过一系列的计算后,使用NMEA-0183协议把相应的信息通过串口输出。针对串口输出报文中的GAGGA和GARMC数据做解析,就可以计算出当前的时间和日期。具体的报文是如下格式:

$GBGGA,105804.00,3016.36015,N,12006.34368,E,1,10,1.53,14.3,M,7.1,M,,*4A
$GBRMC,105805.00,A,3016.36016,N,12006.34352,E,0.120,,210419,,,A,V*1D

其中GBGGA的第二列表示现在是10:58:04,GBRMC的210419表示现在是19年4月21日。
整条链路实际上是
GPS–(广播)–>接收天线模块—-(芯片处理输出NMEA-0183报文)—-> NTPD处理
我们逐个分析可能出问题的环境:
1. 接收天线不能正确处理周数轮转,这个只能找厂商确认。
2. NTPD在根据报文做处理时,处理错误,这个可以自己从代码里去判断(实际上已经经历过一次周数轮转了,应该不会有问题)。

针对NTPD本身的时间处理,因为NMEA的报文里只有2位的年数和时间,那么当原子钟第一次启动时,其实是需要做判断才能知道这个时间到到底是2019年还是3019年的。NTPD在启动初始化NMEA设备时(也就是GPS接收天线在linux系统中注册的串口设备),会获取到软件编译的时间。而这个时间被NTPD作为额外一个参数传入做关键的判断(如果把编译的时间设置为1980年之前,则会使用1980)。具体的转换函数代码如下

int32_t
ntpcal_periodic_extend(
int32_t pivot,
int32_t value,
int32_t cycle
)
{
uint32_t diff;
char     cpl = 0; /* modulo complement flag */
char     neg = 0; /* sign change flag       */

/* make the cycle positive and adjust the flags */
if (cycle < 0) {
        cycle = - cycle;
        neg ^= 1;
        cpl ^= 1;
    }
    /* guard against div by zero or one */
    if (cycle > 1) {
/*
* Get absolute difference as unsigned quantity and
* the complement flag. This is done by always
* subtracting the smaller value from the bigger
* one.
*/
if (value >= pivot) {
diff = int32_to_uint32_2cpl(value)
- int32_to_uint32_2cpl(pivot);
} else {
diff = int32_to_uint32_2cpl(pivot)
- int32_to_uint32_2cpl(value);
cpl ^= 1;
}
diff %= (uint32_t)cycle;
if (diff) {
if (cpl)
diff = (uint32_t)cycle - diff;
if (neg)
diff = ~diff + 1;
pivot += uint32_2cpl_to_int32(diff);
}
}
return pivot;
}

比如如果软件的编译时间是2015年,从GPS报文获取到的时间是19,一个世纪100年,因此调用该函数的时候传入的参数如下:
ntpcal_periodic_extend(2015,19,100),最终可以计算出准确的年份是2019年。

在服务器运行的过程中,遇到周数轮转时是如何处理的呢,实际上NTPD自身的时间是以1900年1月1日0时为起点计算的,
反正就是从NMEA报文里获取的时间戳,和周数根本没关系。根本就不受所谓的轮转影响,只是内部有部分UTC/GPS时间转换的函数,在遇到GPS的周数轮转后,做了相应的±1024的操作。例如下面这个把GPS时间转换成NTP时间的函数:

void
gpstolfp(
u_int weeks,
u_int days,
unsigned long  seconds,
l_fp * lfp
)
{
if (weeks < GPSWRAP)
{
weeks += GPSWEEKS;
}

lfp->l_ui = (uint32_t)(weeks * SECSPERWEEK + days * SECSPERDAY + seconds + GPSORIGIN); /* convert to NTP time */
lfp->l_uf = 0;
}

那么接收模块能否支持周数轮转呢,厂商把设备放实验室,通过模拟器输出对应的信号,观察机器相应的反应即可。我们使用的设备厂商是做了相关的测试,因此确认这方面是没风险的。

针对此种轮转事件,唯一出问题的风险点在于20年前的设备,主板上电池坏掉了,关机后重启系统只能有一个BIOS的默认时间,这时即便时收取到卫星信号,按照前面说的算法,是无法计算出正确的时间的(接收模块不能计算出准确时间,而且NTPD也不能处理这种异常)。

北斗

当此次GPS周数轮转后,国内的媒体除了一个劲地转发一些毫无技术分析的文章时,也有把北斗单独做了赞扬。主要的原因是北斗使用13bit存储周数,需要100多年才会翻转一次。其实2004年GPS也有升级的CNAV报文是使用13bit存储周数(目前大部分设备还是使用L1NAV)。
不过NMEA协议是为了在不同的GPS(全球定位系统)导航设备中建立统一的RTCM(海事无线电技术委员会)标准,由美国国家海洋电子协会(NMEA-The National Marine Electronics Associa-tion)制定的一套通讯协议。市面上你能买到民用的设备,虽然可以接受北斗的信号,但是你从设备串口读取出来的都是NMEA-0183的报文,和GPS的没有差异,还是会存在初次转成4位年数的问题。唯一值得庆幸的时,北斗是在21世纪才开始做的导航系统,这2位数字还能用个80年。

北斗报文

$GBRMC,175829.00,A,3016.36276,N,12006.35248,E,0.271,,200419,,,A,V*1D
$GBGGA,175829.00,3016.36276,N,12006.35248,E,1,08,1.28,48.6,M,7.1,M,,*4E
$GBGGA,175830.00,3016.36276,N,12006.35247,E,1,08,1.28,48.8,M,7.1,M,,*47
$GBRMC,175831.00,A,3016.36281,N,12006.35248,E,0.088,,200419,,,A,V*18
$GBGGA,175831.00,3016.36281,N,12006.35248,E,1,08,1.28,49.0,M,7.1,M,,*48
$GBRMC,175832.00,A,3016.36283,N,12006.35256,E,0.080,,200419,,,A,V*1E
$GBGGA,175832.00,3016.36283,N,12006.35256,E,1,08,1.28,49.4,M,7.1,M,,*42

GPS报文

$GNGGA,060633.000,3119.3559,N,12135.9948,E,1,22,0.6,51.2,M,0.0,M,,*4A

$GNRMC,060633.000,A,3119.3559,N,12135.9948,E,0.00,203.12,160419,,,A*71

参考文档

  1. https://en.wikipedia.org/wiki/GPS_signals#Time
此条目发表在NTP分类目录。将固定链接加入收藏夹。