news 2026/9/11 2:29:15

卫星导航中时间系统到底如何维持?如何输出?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
卫星导航中时间系统到底如何维持?如何输出?

卫星导航中的时间系统主要由各大卫星系统建立,分为中国北斗导航系统的BDT、美国GPS系统的GPST、欧洲GALILEO系统的GST、俄罗斯GLONASS系统的GLONASST,以及将各大系统统一化的协调世界时(UTC)时间系统,所以在整个GNSS导航模组内部,需要实现各大系统向UTC时间系统转换函数,个别模组以GPS作为主系统,主要依靠GPST来维持整个代码的时间算法逻辑。

首先了解下UTC时间的定义和来源。世界时(UT)是以地球自转形成的时间系统,其并不是一个严格均匀的时间系统,由于地球自转速度变慢,经过极移修正的世界时UT1在以每年1s的时间变慢。基于此,引入高精度时间侧标标准国际原子时(TAI),其基于全球数百个高精度原子钟(主要是铯原子钟)的读数加权平均。原子钟的振荡频率极其稳定,因此TAI是一个连续、均匀、高度稳定的时间尺度。它的秒长是固定不变的。将精密计数和真实观测结合在一起,即将UT1和TAI结合在一起使用,形成协调世界时UTC其起始历元为1972 年1 月1 日00 时00 分00 秒TAI,当UTC和UT1差距超过±0.9s后,UTC采用闰秒的方式加插1s,使UTC尽量接近UT1,这也是我们在GPST/BDT/GST中导航电文中会遇到的闰秒操作基础。

闰秒区分正闰秒和负闰秒,只不过当前发生的调整均为正闰秒,闰秒的调整总是被安排在UTC时间6月30日或12月31日的的最后一分钟的最后一秒之后,即在23:59:59后的时间是23:59:60,该时间则为调整的闰秒,也就是该1分钟包含61s。从1972年建立UTC时间起到2025年11月,一共发生过27次闰秒,具体时间如下:

表1 闰秒发生时间

闰秒序号闰秒年份闰秒日期 (UTC)闰秒时分秒 (UTC)
11972年6月30日23:59:60
21972年12月31日23:59:60
31973年12月31日23:59:60
41974年12月31日23:59:60
51975年12月31日23:59:60
61976年12月31日23:59:60
71977年12月31日23:59:60
81978年12月31日23:59:60
91979年12月31日23:59:60
101981年6月30日23:59:60
111982年6月30日23:59:60
121983年6月30日23:59:60
131985年6月30日23:59:60
141987年12月31日23:59:60
151989年12月31日23:59:60
161990年12月31日23:59:60
171992年6月30日23:59:60
181993年6月30日23:59:60
191994年6月30日23:59:60
201995年12月31日23:59:60
211997年6月30日23:59:60
221998年12月31日23:59:60
232005年12月31日23:59:60
242008年12月31日23:59:60
252012年6月30日23:59:60
262015年6月30日23:59:60
272016年12月31日23:59:60

上一次闰秒发生在2016年年底,到现在2025年已经8年多没有宣布调整闰秒的时间节点,2022年曾有国际消息传出最晚于2035年停止引入闰秒的操作,但未来具体闰秒如何处理,还很未知。

1. 各大时间系统介绍

各大系统虽然初始时间点UTC不一致,但是原理基本一致,所以在此选择最熟悉的BDT做介绍和延伸。

BDT 采用国际单位制(SI)秒为基本单位连续累计,不闰秒,起始历元为UTC时2006 年1 月1 日00 时00 分00 秒,记为BDT0,北斗系统的所有时间都是以此做为基准,BDS的周计数bds_week以该时间所在周开始计数。

BDT与UTC之间的闰秒信息在导航电文中播报,自2006年到2015年,发生过4次闰秒,可以从表1中看到,分别为:

闰秒序号2006年1月1日00:00:00
12008年12月31日23:59:60
22012年6月30日23:59:60
32015年6月30日23:59:60
42016年12月31日23:59:60

即2017年1月1日后:BDT=UTC+4s,当前代码中在没有获取到BDS导航电文中的UTCmodel信息时,直接采用的方式就是使用4s作为BDT和UTC的时间转换函数;获取到有效的UTCmodel后,则使用UTCmodel中的闰秒信息进行时间转换。

1.1 BDS UTCmodel 解码

BDS导航电文的UTCmodel参数主要包括:

(1)A0UTC:32bit,BDT相对于UTC的钟差;
(2)A1UTC:24bit,BDT相对于UTC的钟速;
(3)ΔtLS:8bit,新的闰秒生效前BDT相对于UTC的累积闰秒改正数;
(4)WNLSF:8bit,新的闰秒生效的周计数,占8比特,为DN对应的整周计数模256。WNLSF在模256之前和WN之差的绝对值不超过127。
(6)DN:8bit,新的闰秒生效的周内日计数;
(7)ΔtLSF:8bit,新的闰秒生效后BDT相对于UTC的累积闰秒改正数。

考虑到导航电文中存在误码情况,并且UTCmodel直接影响时间维持,所以在存储计算的时候需要判断闰秒信息是否有效?

(1)判断DN是否在0~6的范围内?

(2)判断ΔtLS和ΔtLS,是否都为0?并且差值是否满足≤1?(当前闰秒一次只会调整1s)

仿照RTKLIB中的UBLOX.c,300bit frame存储在uint32_t bds_frame[10]里面,基于BDS D1电文解码:

#define P2_30 9.313225746154785E-10 /* 2^-30 */ #define P2_50 8.881784197001252E-16 /* 2^-50 */ typedef struct { /* utc struct */ double a0; double a1; int8_t ls,lsf,dn,wnlsf; unsigned char check; } utc_model_t, *putc_model_t; void decode_bdsd1_utc(uint8_t *bds_frame, putc_model_t pbds_utc){ unsigned char fraid = 0, pageid = 0; uint16_t i = 0; utc_model_t utc_temp; memset(utc_temp, 0, sizeof(utc_temp)); fraid = getbitu (bds_frame,i+ 15, 3); pageid = getbitu (bds_frame,i+ 43, 7); if(5 == fraid && 10 == pageid ){ utc_temp.ls = getbitu2(buff, i+50, 2, i+60,6); utc_temp.lsf = getbitu (bds_frame, i+66, 8); utc_temp.wnlsf = getbitu (bds_frame, i+74, 8); utc_temp.a0 = getbits2(bds_frame, i+90, 22, i+120, 10)*P2_30 ; utc_temp.a1 = getbits2(bds_frame, i+130, 12, i+150, 12)*P2_50; utc_temp.dn = getbitu (bds_frame, i+162, 8); if((0 <= utc_temp.dn) && (utc_temp.dn <= 6)){ if((0 != utc_temp.ls) && (0 != utc_temp.lsf)){ if(abs(utc_temp.ls - utc_temp.lsf) <= 1){ utc_temp.check = 1; memcpy(pbds_utc, utc_temp, sieeof(utc_temp)); } } } } }

1.2 BDS UTCmodel 使用

UTCmodel的使用,可以具体参见BDS ICD文件中的解释说明。

假设当前时间的北斗周计数和周内秒分别为week_bds和tow_bds,首先计算tow_bds在一周内的天计数dow=t_bds[模86400];

(1) 当指示闰秒生效的周计数WNLSF和周内天计数DN还没到来时,而且用户当前时刻tE处在DN+2/3之前,则UTC与BDT之间的变换关系为:

(2)若用户当前的系统时刻tE处在指示闰秒生效的周计数WNLSF和周内天计数DN+2/3到DN+5/4之间,则UTC与BDT之间的变换关系为:

(3)当指示闰秒生效的周计数WNLSF和周内天计数DN已经过去,且用户当前的系统时刻tE处在DN+5/4之后,则UTC与BDT之间的变换关系为:

代码实现方面,需要考虑以下几个方面:

(1)和解码部分一样,也需要判断重点参数的有效性;

(2)考虑bds_week%256的mod_week和wnlsf之间关系的判断;

(3)考虑到dn和跨周同时发生的情况,所以在衡量DN+2/3和DN+5/4数值的时候,需要将当前时间week和周内天综合形成bds_day来判断;

(4)考虑只有wnls的第dn天的23:59:59之后的那一秒才能闰秒,形成235960,所以需要给出闰秒标志。

假设开发人员现在拿不到UTCmodel的相关参数,这个时候需要原生GNSS模组给出闰秒信息,比如上篇文章提到的UBX、和芯星通或者市面上可以遇到的其他GNSS模组,均需要特定语句拿到闰秒,但是有时候原生结果会晚一点时间输出,所以本来闰秒为UTC235959~235960~000000,就变成了UTC235959~000000~000000,假设高频率输出的时候,有可能也会出现闰秒时刻加载在000000中间,该措施需要注意和衡量。最好的解决方法就是需要GNSS模组直接输出子帧信息,算法内部解码获得UTCmodel,再做时间处理。

1.3 其他系统

与BDT规定类似,其他系统也有着时间初始零点的设置,BDS导航电文中week计数有13bit,最多可以计数到8192周,当前时间范围内暂时可以不用考虑周计数翻转的问题。

GPST的初始时间点是UTC时间1980年1月6日00 时00 分00 秒,记为GPST0,在GPS 导航电文中播发的week计数只有10bit,其最多只能表示1024周,所以在代码代码要做好GPS周翻转计数次数的存储,防止在week翻转的时候出现时间维持异常的问题。

GST的初始时间点是UTC时间1998年8月22日00 时00 分00 秒,记为GST0,Galileo导航电文中播发的week计数只有12bit,可表示范围到4096周。

GLONASS的时间跟随UTC时间,相差3hour,可以写成:

并且GLONSS的时间维持和其他系统都不一样,可以直接参见TRTKLIB中的解码函数:

tow=time2gpst(gpst2utc(geph->tof),&week); tod=fmod(tow,86400.0); tow-=tod; tof=tk_h*3600.0+tk_m*60.0+tk_s-10800.0; /* lt->utc */ if (tof<tod-43200.0) tof+=86400.0; else if (tof>tod+43200.0) tof-=86400.0; geph->tof=utc2gpst(gpst2time(week,tow+tof)); toe=tb*900.0-10800.0; /* lt->utc */ if (toe<tod-43200.0) toe+=86400.0; else if (toe>tod+43200.0) toe-=86400.0; geph->toe=utc2gpst(gpst2time(week,tow+toe)); /* utc->gpst */

1.4 RTKLIB 中相关时间转换

1.4.1 统一时间格式

在RTKLIB或者实际代码中,会比较bdt1和bdt0的时间大小,当week和tow都已知后,会直接将其转换到以UTC0点对应的数值轴上,即UTC0为0s,后续分别基于该数值进行直接累加,结构体形式如下:

//来源:RTKLIB typedef struct { /* time struct */ time_t time; /* time (s) expressed by standard time_t */ double sec; /* fraction of second under 1 s */ } gtime_t; /* beidou time (bdt) to time --------------------------------------------------- * convert week and tow in beidou time (bdt) to gtime_t struct * args : int week I week number in bdt * double sec I time of week in bdt (s) * return : gtime_t struct *-----------------------------------------------------------------------------*/ extern gtime_t bdt2time(int week, double sec) { gtime_t t=epoch2time(bdt0); if (sec<-1E9||1E9<sec) sec=0.0; t.time+=86400*7*week+(int)sec; t.sec=sec-(int)sec; return t; }

1.4.2 各大系统时间转换

由于从2016年起闰秒信息再未更新, 所以大部分GPS和UTC之间的闰秒转换直接使用18s,RTKLIB中有根据闰秒表和年月日判断选择闰秒的操作:

const static double gpst0[]={1980,1, 6,0,0,0}; /* gps time reference */ const static double gst0 []={1999,8,22,0,0,0}; /* galileo system time reference */ const static double bdt0 []={2006,1, 1,0,0,0}; /* beidou time reference */ static double leaps[MAXLEAPS+1][7]={ /* leap seconds (y,m,d,h,m,s,utc-gpst) */ {2017,1,1,0,0,0,-18}, {2015,7,1,0,0,0,-17}, {2012,7,1,0,0,0,-16}, {2009,1,1,0,0,0,-15}, {2006,1,1,0,0,0,-14}, {1999,1,1,0,0,0,-13}, {1997,7,1,0,0,0,-12}, {1996,1,1,0,0,0,-11}, {1994,7,1,0,0,0,-10}, {1993,7,1,0,0,0, -9}, {1992,7,1,0,0,0, -8}, {1991,1,1,0,0,0, -7}, {1990,1,1,0,0,0, -6}, {1988,1,1,0,0,0, -5}, {1985,7,1,0,0,0, -4}, {1983,7,1,0,0,0, -3}, {1982,7,1,0,0,0, -2}, {1981,7,1,0,0,0, -1}, {0} }; extern gtime_t gpst2utc(gtime_t t) { gtime_t tu; int i; for (i=0;leaps[i][0]>0;i++) { tu=timeadd(t,leaps[i][6]); if (timediff(tu,epoch2time(leaps[i]))>=0.0) return tu; } return t; } /* utc to gpstime -------------------------------------------------------------- * convert utc to gpstime considering leap seconds * args : gtime_t t I time expressed in utc * return : time expressed in gpstime * notes : ignore slight time offset under 100 ns *-----------------------------------------------------------------------------*/ extern gtime_t utc2gpst(gtime_t t) { int i; for (i=0;leaps[i][0]>0;i++) { if (timediff(t,epoch2time(leaps[i]))>=0.0) return timeadd(t,-leaps[i][6]); } return t; } /* gpstime to bdt -------------------------------------------------------------- * convert gpstime to bdt (beidou navigation satellite system time) * args : gtime_t t I time expressed in gpstime * return : time expressed in bdt * notes : ref [8] 3.3, 2006/1/1 00:00 BDT = 2006/1/1 00:00 UTC * no leap seconds in BDT * ignore slight time offset under 100 ns *-----------------------------------------------------------------------------*/ extern gtime_t gpst2bdt(gtime_t t) { return timeadd(t,-14.0); } /* bdt to gpstime -------------------------------------------------------------- * convert bdt (beidou navigation satellite system time) to gpstime * args : gtime_t t I time expressed in bdt * return : time expressed in gpstime * notes : see gpst2bdt() *-----------------------------------------------------------------------------*/ extern gtime_t bdt2gpst(gtime_t t) { return timeadd(t,14.0); } /* galileo system time to time ------------------------------------------------- * convert week and tow in galileo system time (gst) to gtime_t struct * args : int week I week number in gst * double sec I time of week in gst (s) * return : gtime_t struct *-----------------------------------------------------------------------------*/ extern gtime_t gst2time(int week, double sec) { gtime_t t=epoch2time(gst0); if (sec<-1E9||1E9<sec) sec=0.0; t.time+=86400*7*week+(int)sec; t.sec=sec-(int)sec; return t; } /* time to galileo system time ------------------------------------------------- * convert gtime_t struct to week and tow in galileo system time (gst) * args : gtime_t t I gtime_t struct * int *week IO week number in gst (NULL: no output) * return : time of week in gst (s) *-----------------------------------------------------------------------------*/ extern double time2gst(gtime_t t, int *week) { gtime_t t0=epoch2time(gst0); time_t sec=t.time-t0.time; int w=(int)(sec/(86400*7)); if (week) *week=w; return (double)(sec-w*86400*7)+t.sec; }

RTKLIB中将GPS作为主系统,所有的时间系统都是使用GPST做转换,即其他系统转换为gtime格式,需要将BDT/GST先转gtime格式,GPST再转为UTC时间。

1.4.3 输出时间格式

从上述来看,基本还是各大系统的内部时间,对外输出的时候,一般需要转换为年月日时分秒的格式来看,比如NMEA数据中的GGA/RMC语句、RINEX数据中的观测量时间,可以直接使用RTKLIB中的转换函数time2epoch()。

/* convert calendar day/time to time ------------------------------------------- * convert calendar day/time to gtime_t struct * args : double *ep I day/time {year,month,day,hour,min,sec} * return : gtime_t struct * notes : proper in 1970-2037 or 1970-2099 (64bit time_t) *-----------------------------------------------------------------------------*/ extern gtime_t epoch2time(const double *ep) { const int doy[]={1,32,60,91,121,152,182,213,244,274,305,335}; gtime_t time={0}; int days,sec,year=(int)ep[0],mon=(int)ep[1],day=(int)ep[2]; if (year<1970||2099<year||mon<1||12<mon) return time; /* leap year if year%4==0 in 1901-2099 */ days=(year-1970)*365+(year-1969)/4+doy[mon-1]+day-2+(year%4==0&&mon>=3?1:0); sec=(int)floor(ep[5]); time.time=(time_t)days*86400+(int)ep[3]*3600+(int)ep[4]*60+sec; time.sec=ep[5]-sec; return time; } /* time to calendar day/time --------------------------------------------------- * convert gtime_t struct to calendar day/time * args : gtime_t t I gtime_t struct * double *ep O day/time {year,month,day,hour,min,sec} * return : none * notes : proper in 1970-2037 or 1970-2099 (64bit time_t) *-----------------------------------------------------------------------------*/ extern void time2epoch(gtime_t t, double *ep) { const int mday[]={ /* # of days in a month */ 31,28,31,30,31,30,31,31,30,31,30,31,31,28,31,30,31,30,31,31,30,31,30,31, 31,29,31,30,31,30,31,31,30,31,30,31,31,28,31,30,31,30,31,31,30,31,30,31 }; int days,sec,mon,day; /* leap year if year%4==0 in 1901-2099 */ days=(int)(t.time/86400); sec=(int)(t.time-(time_t)days*86400); for (day=days%1461,mon=0;mon<48;mon++) { if (day>=mday[mon]) day-=mday[mon]; else break; } ep[0]=1970+days/1461*4+mon/12; ep[1]=mon%12+1; ep[2]=day+1; ep[3]=sec/3600; ep[4]=sec%3600/60; ep[5]=sec%60+t.sec; }

2. 如何维持时间系统

本篇文章区分自研GNSS模组以及GNSS算法/大融合算法对于时间系统的维持,两者是不一样的维持思路。

2.1 GNSS模组

对于GNSS模组而言,即就是在模组内部需要完成卫星信号的捕获跟踪、帧同步、计算观测量、维持时间系统的功能,在维持时间系统模块的具体流程为:

(1)帧同步获得frame_tow,基于frame_tow+estimate_path=tow来更新本地时间tow,如果是多个卫星在同时在一个历元均帧同步成功,则选择一个最高CNR的卫星做为初始化本地时间tow的参考卫星;

(2)解码星历获得周计数week,不同系统一定要做区分,week周计数出现的频率也要做部分。举例:假设BDS系统只有2个卫星获得week,判断week1和week2是否一致,一致则更新rcv_week,否则则不更新,假设当前有3个及以上的卫星获得week后,需要判断n个week中出现频率最多的week,并且出现频率>1/2的week才用来更新rcv_week;

(3)根据各大系统之间的week计数关系做相互校验,比如GPS_week和BDS_week相差1356,GPS_week和Galileo_week相差1024周,因为BDS_week的数据有效位比较长,所以常用来转换计算GPS的周翻转次数。

注意点:
(1)跨周的维持,个别模组直接采用的方式是tow采用累加方式,一直到604800.0s,将tow翻转为0,week+=1;

(2)接收机调钟,因为计算接收机钟差一直在累加,常用的方式是在钟差累积到一定数值的时候直接调整,此时baseband需要对于上报观测量间隔进行调整。

(3)观测量调整,在RINEX那一节也提到,在输出伪距、载波、多普勒的时候将钟差、钟漂调整量修正,可以参考RTCM协议中的计算公式:

2.2 GNSS 算法/融合算法

对于GNSS算法,即直接使用外部模组输出的RTCM/UBLOX数据做PVT/RTK计算/与INS融合等算法,不同GNSS模组输出的时间信息有差异,在此区分下RTCM和UBLOX数据流程。

对于RTCM数据,其观测量数据中只有tow时间,没有week时间,所以无法对齐时间,所以在使用RTKLIB中的rtkcov解码RTCM数据的时候需要输入时间信息,在代码中常用的方法是:

(1)基于RTCM星历获得week,首先基于tow和eph_toe的差值做星历有限期的判断,才能使用星历的week,后续判断和GNSS模组一致,不同系统一定要做区分,week周计数出现的频率也要做部分。举例:假设BDS系统只有2个卫星获得week,判断week1和week2是否一致,一致则更新rcv_week,否则则不更新,假设当前有3个及以上的卫星获得week后,需要判断n个week中出现频率最多的week,并且出现频率>1/2n的week才用来更新rcv_week;

(2)基于从GNSS模组定位信息中的时间信息week、tow和从星历中拿到的week做校验判断,防止跨周出现异常。

对于UBLOX数据,观测量中有week和tow信息,直接使用,所增加的步骤就是从GNSS模组定位信息中的时间信息week、tow和从星历中拿到的week做校验判断,防止跨周出现异常。

注意点:

(1)整点对齐功能,举例:个别模组在输出观测量数据的时候,并不是标准的5hz,在0.0、0.2、0.4、0.6、0.8的时候输出,而是在0.12、0.32、0.52、0.72、0.92的时刻输出,所以要做要整点观测量数据的归算;

(2)跨周功能非常重要,在代码内部维持的时候需要做重点关注!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 2:28:28

CMSIS-FreeRTOS深度审计:产线级RTOS工程避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 2:28:03

用QEMU模拟苹果芯片,在x86 Linux上调试Darwin内核

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 2:27:57

Node.js环境配置与优化全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 2:27:38

手把手构建 PCSX2:3 步把 PS2 模拟器从源码编译到能跑起来

手把手构建 PCSX2&#xff1a;3 步把 PS2 模拟器从源码编译到能跑起来 【免费下载链接】pcsx2 PCSX2 - The Playstation 2 Emulator 项目地址: https://gitcode.com/GitHub_Trending/pc/pcsx2 PCSX2 是一款免费开源的 PlayStation 2 模拟器&#xff0c;它用解释器、动态…

作者头像 李华
网站建设 2026/9/11 2:25:03

CNN特征提取与SVM/GentleBoost混合分类实战

简介&#xff1a;本资源是一份面向高校机器学习课程学习者与初学者的完整实践项目包&#xff0c;聚焦卷积神经网络&#xff08;CNN&#xff09;在图像场景分类任务中的Matlab实现。资源包含可直接运行的CNN训练与测试源码、覆盖15类典型室内外场景的标注图像数据集&#xff0c;…

作者头像 李华