2 * ntp_calgps.c - calendar for GPS/GNSS based clocks
4 * Written by Juergen Perlinger (perlinger@ntp.org) for the NTP project.
5 * The contents of 'html/copyright.html' apply.
7 * --------------------------------------------------------------------
9 * This module implements stuff often used with GPS/GNSS receivers
13 #include <sys/types.h>
15 #include "ntp_types.h"
16 #include "ntp_calendar.h"
17 #include "ntp_calgps.h"
18 #include "ntp_stdlib.h"
19 #include "ntp_unixtime.h"
23 #include "vint64ops.h"
25 /* ====================================================================
26 * misc. helpers -- might go elsewhere sometime?
27 * ====================================================================
37 /* calculate 'lfp - ofs' as '(l_fp)(-ofs) + lfp': negating a
38 * double is cheap, as it only flips one bit...
47 /* ====================================================================
48 * GPS calendar functions
49 * ====================================================================
52 /* --------------------------------------------------------------------
53 * normalization functions for day/time and week/time representations.
54 * Since we only use moderate offsets (leap second corrections and
55 * alike) it does not really pay off to do a floor-corrected division
56 * here. We use compare/decrement/increment loops instead.
57 * --------------------------------------------------------------------
64 static const int32_t limit = SECSPERDAY;
66 if (datum->secs >= limit) {
69 while ((datum->secs -= limit) >= limit);
70 } else if (datum->secs < 0) {
73 while ((datum->secs += limit) < 0);
82 static const int32_t limit = 7 * SECSPERDAY;
84 if (datum->wsecs >= limit) {
87 while ((datum->wsecs -= limit) >= limit);
88 } else if (datum->wsecs < 0) {
91 while ((datum->wsecs += limit) < 0);
95 /* --------------------------------------------------------------------
96 * Add an offset to a day/time and week/time representation.
98 * !!Attention!! the offset should be small, compared to the time period
99 * (either a day or a week).
100 * --------------------------------------------------------------------
108 /* fraction can be added easily */
109 datum->frac += offset.l_uf;
110 datum->secs += (datum->frac < offset.l_uf);
112 /* avoid integer overflow on the seconds */
113 if (offset.l_ui >= INT32_MAX)
114 datum->secs -= (int32_t)~offset.l_ui + 1;
116 datum->secs += (int32_t)offset.l_ui;
117 _norm_ntp_datum(datum);
126 /* fraction can be added easily */
127 datum->frac += offset.l_uf;
128 datum->wsecs += (datum->frac < offset.l_uf);
131 /* avoid integer overflow on the seconds */
132 if (offset.l_ui >= INT32_MAX)
133 datum->wsecs -= (int32_t)~offset.l_ui + 1;
135 datum->wsecs += (int32_t)offset.l_ui;
136 _norm_gps_datum(datum);
139 /* -------------------------------------------------------------------
140 * API functions civil calendar and NTP datum
141 * -------------------------------------------------------------------
149 /* force result in basedate era
151 * When calculating this directly in days, we have to execute a
152 * real modulus calculation, since we're obviously not doing a
153 * modulus by a power of 2. Executing this as true floor mod
154 * needs some care and is done under explicit usage of one's
155 * complement and masking to get mostly branchless code.
157 static uint32_t const clen = 7*1024;
159 uint32_t base, days, sign;
162 /* Get base in NTP day scale. No overflows here. */
163 base = (basedate_get_gpsweek() + GPSNTP_WSHIFT) * 7
167 sign = (uint32_t)-(days < base);
168 days = sign ^ (days - base);
170 days = base + (sign & clen) + (sign ^ days);
182 _norm_ntp_datum(&out);
183 return _gpsntp_fix_gps_era(&out);
186 /* ----------------------------------------------------------------- */
188 _gpsntp_from_daytime(
195 static const int32_t shift = SECSPERDAY / 2;
199 /* set result based on pivot -- ops order is important here */
201 retv.secs = ntpcal_date_to_daysec(jd);
202 gpsntp_add_offset(&retv, fofs); /* result is normalized */
203 retv.days = pivot->days;
205 /* Manual periodic extension without division: */
206 if (pivot->secs < shift) {
207 int32_t lim = pivot->secs + shift;
208 retv.days -= (retv.secs > lim ||
209 (retv.secs == lim && retv.frac >= pivot->frac));
211 int32_t lim = pivot->secs - shift;
212 retv.days += (retv.secs < lim ||
213 (retv.secs == lim && retv.frac < pivot->frac));
215 return warp ? _gpsntp_fix_gps_era(&retv) : retv;
218 /* -----------------------------------------------------------------
219 * Given the time-of-day part of a civil datum and an additional
220 * (fractional) offset, calculate a full time stamp around a given pivot
221 * time so that the difference between the pivot and the resulting time
222 * stamp is less or equal to 12 hours absolute.
225 gpsntp_from_daytime2_ex(
232 TNtpDatum dpiv = *pivot;
233 _norm_ntp_datum(&dpiv);
234 return _gpsntp_from_daytime(jd, fofs, &dpiv, warp);
237 /* -----------------------------------------------------------------
238 * This works similar to 'gpsntp_from_daytime1()' and actually even uses
239 * it, but the pivot is calculated from the pivot given as 'l_fp' in NTP
240 * time scale. This is in turn expanded around the current system time,
241 * and the resulting absolute pivot is then used to calculate the full
245 gpsntp_from_daytime1_ex(
256 pvi64 = ntpcal_ntp_to_ntp(pivot.l_ui, NULL);
257 split = ntpcal_daysplit(&pvi64);
258 dpiv.days = split.hi;
259 dpiv.secs = split.lo;
260 dpiv.frac = pivot.l_uf;
261 return _gpsntp_from_daytime(jd, fofs, &dpiv, warp);
264 /* -----------------------------------------------------------------
265 * Given a calendar date, zap it into a GPS time format and then convert
266 * that one into the NTP time scale.
269 gpsntp_from_calendar_ex(
276 gps = gpscal_from_calendar_ex(jd, fofs, warp);
277 return gpsntp_from_gpscal_ex(&gps, FALSE);
280 /* -----------------------------------------------------------------
281 * create a civil calendar datum from a NTP date representation
289 memset(cd, 0, sizeof(*cd));
292 nd->days + DAY_NTP_STARTS + ntpcal_daysec_to_date(
296 /* -----------------------------------------------------------------
297 * get day/tod representation from week/tow datum
300 gpsntp_from_gpscal_ex(
308 TGpsDatum date = *gd;
311 uint32_t base = basedate_get_gpsweek() + GPSNTP_WSHIFT;
312 _norm_gps_datum(&date);
313 date.weeks = ((date.weeks - base) & 1023u) + base;
316 ts64 = ntpcal_weekjoin(date.weeks, date.wsecs);
317 ts64 = subv64u32(&ts64, (GPSNTP_DSHIFT * SECSPERDAY));
318 split = ntpcal_daysplit(&ts64);
320 retv.frac = gd->frac;
321 retv.secs = split.lo;
322 retv.days = split.hi;
326 /* -----------------------------------------------------------------
327 * get LFP from ntp datum
336 retv.l_uf = nd->frac;
337 retv.l_ui = nd->days * (uint32_t)SECSPERDAY
342 /* -------------------------------------------------------------------
343 * API functions GPS week calendar
345 * Here we use a calendar base of 1899-12-31, so the NTP epoch has
346 * { 0, 86400.0 } in this representation.
347 * -------------------------------------------------------------------
355 /* force result in basedate era
357 * This is based on calculating the modulus to a power of two,
358 * so signed integer overflow does not affect the result. Which
359 * in turn makes for a very compact calculation...
365 base = basedate_get_gpsweek() + GPSNTP_WSHIFT;
366 week = base + ((week - base) & (GPSWEEKS - 1));
377 _norm_gps_datum(&out);
378 return _gpscal_fix_gps_era(&out);
381 /* -----------------------------------------------------------------
382 * Given a calendar date, zap it into a GPS time format and the do a
383 * proper era mapping in the GPS time scale, based on the GPS base date,
386 * This function also augments the century if just a 2-digit year
387 * (0..99) is provided on input.
389 * This is a fail-safe against GPS receivers with an unknown starting
390 * point for their internal calendar calculation and therefore
391 * unpredictable (but reproducible!) rollover behavior. While there
392 * *are* receivers that create a full date in the proper way, many
393 * others just don't. The overall damage is minimized by simply not
394 * trusting the era mapping of the receiver and doing the era assignment
395 * with a configurable base date *inside* ntpd.
398 gpscal_from_calendar_ex(
404 /* (-DAY_GPS_STARTS) (mod 7*1024) -- complement of cycle shift */
405 static const uint32_t s_compl_shift =
406 (7 * 1024) - DAY_GPS_STARTS % (7 * 1024);
412 /* if needed, convert from 2-digit year to full year
413 * !!NOTE!! works only between 1980 and 2079!
418 else if (cal.year < 100)
421 /* get RDN from date, possibly adjusting the century */
422 again: if (cal.month && cal.monthday) { /* use Y/M/D civil date */
423 days = ntpcal_date_to_rd(&cal);
424 } else { /* using Y/DoY date */
425 days = ntpcal_year_to_ystart(cal.year)
426 + (int32_t)cal.yearday
427 - 1; /* both RDN and yearday start with '1'. */
430 /* Rebase to days after the GPS epoch. 'days' is positive here,
431 * but it might be less than the GPS epoch start. Depending on
432 * the input, we have to do different things to get the desired
433 * result. (Since we want to remap the era anyway, we only have
434 * to retain congruential identities....)
437 if (days >= DAY_GPS_STARTS) {
438 /* simply shift to days since GPS epoch */
439 days -= DAY_GPS_STARTS;
440 } else if (jd->year < 100) {
441 /* Two-digit year on input: add another century and
442 * retry. This can happen only if the century expansion
443 * yielded a date between 1980-01-01 and 1980-01-05,
444 * both inclusive. We have at most one retry here.
449 /* A very bad date before the GPS epoch. There's not
450 * much we can do, except to add the complement of
451 * DAY_GPS_STARTS % (7 * 1024) here, that is, use a
452 * congruential identity: Add the complement instead of
453 * subtracting the value gives a value with the same
454 * modulus. But of course, now we MUST to go through a
455 * cycle fix... because the date was obviously wrong!
458 days += s_compl_shift;
461 /* Splitting to weeks is simple now: */
465 /* re-base on start of NTP with weeks mapped to 1024 weeks
466 * starting with the GPS base day set in the calendar.
468 gps.weeks = week + GPSNTP_WSHIFT;
469 gps.wsecs = days * SECSPERDAY + ntpcal_date_to_daysec(&cal);
471 gpscal_add_offset(&gps, fofs);
472 return warp ? _gpscal_fix_gps_era(&gps) : gps;
475 /* -----------------------------------------------------------------
476 * get civil date from week/tow representation
486 memset(cd, 0, sizeof(*cd));
487 nd = gpsntp_from_gpscal_ex(wd, FALSE);
488 gpsntp_to_calendar(cd, &nd);
491 /* -----------------------------------------------------------------
492 * Given the week and seconds in week, as well as the fraction/offset
493 * (which should/could include the leap seconds offset), unfold the
494 * weeks (which are assumed to have just 10 bits) into expanded weeks
495 * based on the GPS base date derived from the build date (default) or
496 * set by the configuration.
498 * !NOTE! This function takes RAW GPS weeks, aligned to the GPS start
499 * (1980-01-06) on input. The output weeks will be aligned to NTPD's
500 * week calendar start (1899-12-31)!
513 retv.weeks = week + GPSNTP_WSHIFT;
514 gpscal_add_offset(&retv, fofs);
515 return _gpscal_fix_gps_era(&retv);
518 /* -----------------------------------------------------------------
519 * internal work horse for time-of-week expansion
522 _gpscal_from_weektime(
528 static const int32_t shift = SECSPERWEEK / 2;
532 /* set result based on pivot -- ops order is important here */
535 gpscal_add_offset(&retv, fofs); /* result is normalized */
536 retv.weeks = pivot->weeks;
538 /* Manual periodic extension without division: */
539 if (pivot->wsecs < shift) {
540 int32_t lim = pivot->wsecs + shift;
541 retv.weeks -= (retv.wsecs > lim ||
542 (retv.wsecs == lim && retv.frac >= pivot->frac));
544 int32_t lim = pivot->wsecs - shift;
545 retv.weeks += (retv.wsecs < lim ||
546 (retv.wsecs == lim && retv.frac < pivot->frac));
548 return _gpscal_fix_gps_era(&retv);
551 /* -----------------------------------------------------------------
552 * expand a time-of-week around a pivot given as week datum
555 gpscal_from_weektime2(
561 TGpsDatum wpiv = * pivot;
562 _norm_gps_datum(&wpiv);
563 return _gpscal_from_weektime(wsecs, fofs, &wpiv);
566 /* -----------------------------------------------------------------
567 * epand a time-of-week around an pivot given as LFP, which in turn
568 * is expanded around the current system time and then converted
572 gpscal_from_weektime1(
582 /* get 64-bit pivot in NTP epoch */
583 pvi64 = ntpcal_ntp_to_ntp(pivot.l_ui, NULL);
585 /* convert to weeks since 1899-12-31 and seconds in week */
586 pvi64 = addv64u32(&pvi64, (GPSNTP_DSHIFT * SECSPERDAY));
587 split = ntpcal_weeksplit(&pvi64);
589 wpiv.weeks = split.hi;
590 wpiv.wsecs = split.lo;
591 wpiv.frac = pivot.l_uf;
592 return _gpscal_from_weektime(wsecs, fofs, &wpiv);
595 /* -----------------------------------------------------------------
596 * get week/tow representation from day/tod datum
607 ts64 = ntpcal_dayjoin(gd->days, gd->secs);
608 ts64 = addv64u32(&ts64, (GPSNTP_DSHIFT * SECSPERDAY));
609 split = ntpcal_weeksplit(&ts64);
611 retv.frac = gd->frac;
612 retv.wsecs = split.lo;
613 retv.weeks = split.hi;
617 /* -----------------------------------------------------------------
618 * convert week/tow to LFP stamp
627 retv.l_uf = gd->frac;
628 retv.l_ui = gd->weeks * (uint32_t)SECSPERWEEK
629 + (uint32_t)gd->wsecs
630 - (uint32_t)SECSPERDAY * GPSNTP_DSHIFT;