1 /* $NetBSD: ntp_calgps.c,v 1.2 2020/05/25 20:47:24 christos Exp $ */
2
3 /*
4 * ntp_calgps.c - calendar for GPS/GNSS based clocks
5 *
6 * Written by Juergen Perlinger (perlinger@ntp.org) for the NTP project.
7 * The contents of 'html/copyright.html' apply.
8 *
9 * --------------------------------------------------------------------
10 *
11 * This module implements stuff often used with GPS/GNSS receivers
12 */
13
14 #include <config.h>
15 #include <sys/types.h>
16
17 #include "ntp_types.h"
18 #include "ntp_calendar.h"
19 #include "ntp_calgps.h"
20 #include "ntp_stdlib.h"
21 #include "ntp_unixtime.h"
22
23 #include "ntp_fp.h"
24 #include "ntpd.h"
25 #include "vint64ops.h"
26
27 /* ====================================================================
28 * misc. helpers -- might go elsewhere sometime?
29 * ====================================================================
30 */
31
32 l_fp
ntpfp_with_fudge(l_fp lfp,double ofs)33 ntpfp_with_fudge(
34 l_fp lfp,
35 double ofs
36 )
37 {
38 l_fp fpo;
39 /* calculate 'lfp - ofs' as '(l_fp)(-ofs) + lfp': negating a
40 * double is cheap, as it only flips one bit...
41 */
42 ofs = -ofs;
43 DTOLFP(ofs, &fpo);
44 L_ADD(&fpo, &lfp);
45 return fpo;
46 }
47
48
49 /* ====================================================================
50 * GPS calendar functions
51 * ====================================================================
52 */
53
54 /* --------------------------------------------------------------------
55 * normalization functions for day/time and week/time representations.
56 * Since we only use moderate offsets (leap second corrections and
57 * alike) it does not really pay off to do a floor-corrected division
58 * here. We use compare/decrement/increment loops instead.
59 * --------------------------------------------------------------------
60 */
61 static void
_norm_ntp_datum(TNtpDatum * datum)62 _norm_ntp_datum(
63 TNtpDatum * datum
64 )
65 {
66 static const int32_t limit = SECSPERDAY;
67
68 if (datum->secs >= limit) {
69 do
70 ++datum->days;
71 while ((datum->secs -= limit) >= limit);
72 } else if (datum->secs < 0) {
73 do
74 --datum->days;
75 while ((datum->secs += limit) < 0);
76 }
77 }
78
79 static void
_norm_gps_datum(TGpsDatum * datum)80 _norm_gps_datum(
81 TGpsDatum * datum
82 )
83 {
84 static const int32_t limit = 7 * SECSPERDAY;
85
86 if (datum->wsecs >= limit) {
87 do
88 ++datum->weeks;
89 while ((datum->wsecs -= limit) >= limit);
90 } else if (datum->wsecs < 0) {
91 do
92 --datum->weeks;
93 while ((datum->wsecs += limit) < 0);
94 }
95 }
96
97 /* --------------------------------------------------------------------
98 * Add an offset to a day/time and week/time representation.
99 *
100 * !!Attention!! the offset should be small, compared to the time period
101 * (either a day or a week).
102 * --------------------------------------------------------------------
103 */
104 void
gpsntp_add_offset(TNtpDatum * datum,l_fp offset)105 gpsntp_add_offset(
106 TNtpDatum * datum,
107 l_fp offset
108 )
109 {
110 /* fraction can be added easily */
111 datum->frac += offset.l_uf;
112 datum->secs += (datum->frac < offset.l_uf);
113
114 /* avoid integer overflow on the seconds */
115 if (offset.l_ui >= INT32_MAX)
116 datum->secs -= (int32_t)~offset.l_ui + 1;
117 else
118 datum->secs += (int32_t)offset.l_ui;
119 _norm_ntp_datum(datum);
120 }
121
122 void
gpscal_add_offset(TGpsDatum * datum,l_fp offset)123 gpscal_add_offset(
124 TGpsDatum * datum,
125 l_fp offset
126 )
127 {
128 /* fraction can be added easily */
129 datum->frac += offset.l_uf;
130 datum->wsecs += (datum->frac < offset.l_uf);
131
132
133 /* avoid integer overflow on the seconds */
134 if (offset.l_ui >= INT32_MAX)
135 datum->wsecs -= (int32_t)~offset.l_ui + 1;
136 else
137 datum->wsecs += (int32_t)offset.l_ui;
138 _norm_gps_datum(datum);
139 }
140
141 /* -------------------------------------------------------------------
142 * API functions civil calendar and NTP datum
143 * -------------------------------------------------------------------
144 */
145
146 static TNtpDatum
_gpsntp_fix_gps_era(TcNtpDatum * in)147 _gpsntp_fix_gps_era(
148 TcNtpDatum * in
149 )
150 {
151 /* force result in basedate era
152 *
153 * When calculating this directly in days, we have to execute a
154 * real modulus calculation, since we're obviously not doing a
155 * modulus by a power of 2. Executing this as true floor mod
156 * needs some care and is done under explicit usage of one's
157 * complement and masking to get mostly branchless code.
158 */
159 static uint32_t const clen = 7*1024;
160
161 uint32_t base, days, sign;
162 TNtpDatum out = *in;
163
164 /* Get base in NTP day scale. No overflows here. */
165 base = (basedate_get_gpsweek() + GPSNTP_WSHIFT) * 7
166 - GPSNTP_DSHIFT;
167 days = out.days;
168
169 sign = (uint32_t)-(days < base);
170 days = sign ^ (days - base);
171 days %= clen;
172 days = base + (sign & clen) + (sign ^ days);
173
174 out.days = days;
175 return out;
176 }
177
178 TNtpDatum
gpsntp_fix_gps_era(TcNtpDatum * in)179 gpsntp_fix_gps_era(
180 TcNtpDatum * in
181 )
182 {
183 TNtpDatum out = *in;
184 _norm_ntp_datum(&out);
185 return _gpsntp_fix_gps_era(&out);
186 }
187
188 /* ----------------------------------------------------------------- */
189 static TNtpDatum
_gpsntp_from_daytime(TcCivilDate * jd,l_fp fofs,TcNtpDatum * pivot,int warp)190 _gpsntp_from_daytime(
191 TcCivilDate * jd,
192 l_fp fofs,
193 TcNtpDatum * pivot,
194 int warp
195 )
196 {
197 static const int32_t shift = SECSPERDAY / 2;
198
199 TNtpDatum retv;
200
201 /* set result based on pivot -- ops order is important here */
202 ZERO(retv);
203 retv.secs = ntpcal_date_to_daysec(jd);
204 gpsntp_add_offset(&retv, fofs); /* result is normalized */
205 retv.days = pivot->days;
206
207 /* Manual periodic extension without division: */
208 if (pivot->secs < shift) {
209 int32_t lim = pivot->secs + shift;
210 retv.days -= (retv.secs > lim ||
211 (retv.secs == lim && retv.frac >= pivot->frac));
212 } else {
213 int32_t lim = pivot->secs - shift;
214 retv.days += (retv.secs < lim ||
215 (retv.secs == lim && retv.frac < pivot->frac));
216 }
217 return warp ? _gpsntp_fix_gps_era(&retv) : retv;
218 }
219
220 /* -----------------------------------------------------------------
221 * Given the time-of-day part of a civil datum and an additional
222 * (fractional) offset, calculate a full time stamp around a given pivot
223 * time so that the difference between the pivot and the resulting time
224 * stamp is less or equal to 12 hours absolute.
225 */
226 TNtpDatum
gpsntp_from_daytime2_ex(TcCivilDate * jd,l_fp fofs,TcNtpDatum * pivot,int warp)227 gpsntp_from_daytime2_ex(
228 TcCivilDate * jd,
229 l_fp fofs,
230 TcNtpDatum * pivot,
231 int/*BOOL*/ warp
232 )
233 {
234 TNtpDatum dpiv = *pivot;
235 _norm_ntp_datum(&dpiv);
236 return _gpsntp_from_daytime(jd, fofs, &dpiv, warp);
237 }
238
239 /* -----------------------------------------------------------------
240 * This works similar to 'gpsntp_from_daytime1()' and actually even uses
241 * it, but the pivot is calculated from the pivot given as 'l_fp' in NTP
242 * time scale. This is in turn expanded around the current system time,
243 * and the resulting absolute pivot is then used to calculate the full
244 * NTP time stamp.
245 */
246 TNtpDatum
gpsntp_from_daytime1_ex(TcCivilDate * jd,l_fp fofs,l_fp pivot,int warp)247 gpsntp_from_daytime1_ex(
248 TcCivilDate * jd,
249 l_fp fofs,
250 l_fp pivot,
251 int/*BOOL*/ warp
252 )
253 {
254 vint64 pvi64;
255 TNtpDatum dpiv;
256 ntpcal_split split;
257
258 pvi64 = ntpcal_ntp_to_ntp(pivot.l_ui, NULL);
259 split = ntpcal_daysplit(&pvi64);
260 dpiv.days = split.hi;
261 dpiv.secs = split.lo;
262 dpiv.frac = pivot.l_uf;
263 return _gpsntp_from_daytime(jd, fofs, &dpiv, warp);
264 }
265
266 /* -----------------------------------------------------------------
267 * Given a calendar date, zap it into a GPS time format and then convert
268 * that one into the NTP time scale.
269 */
270 TNtpDatum
gpsntp_from_calendar_ex(TcCivilDate * jd,l_fp fofs,int warp)271 gpsntp_from_calendar_ex(
272 TcCivilDate * jd,
273 l_fp fofs,
274 int/*BOOL*/ warp
275 )
276 {
277 TGpsDatum gps;
278 gps = gpscal_from_calendar_ex(jd, fofs, warp);
279 return gpsntp_from_gpscal_ex(&gps, FALSE);
280 }
281
282 /* -----------------------------------------------------------------
283 * create a civil calendar datum from a NTP date representation
284 */
285 void
gpsntp_to_calendar(TCivilDate * cd,TcNtpDatum * nd)286 gpsntp_to_calendar(
287 TCivilDate * cd,
288 TcNtpDatum * nd
289 )
290 {
291 memset(cd, 0, sizeof(*cd));
292 ntpcal_rd_to_date(
293 cd,
294 nd->days + DAY_NTP_STARTS + ntpcal_daysec_to_date(
295 cd, nd->secs));
296 }
297
298 /* -----------------------------------------------------------------
299 * get day/tod representation from week/tow datum
300 */
301 TNtpDatum
gpsntp_from_gpscal_ex(TcGpsDatum * gd,int warp)302 gpsntp_from_gpscal_ex(
303 TcGpsDatum * gd,
304 int/*BOOL*/ warp
305 )
306 {
307 TNtpDatum retv;
308 vint64 ts64;
309 ntpcal_split split;
310 TGpsDatum date = *gd;
311
312 if (warp) {
313 uint32_t base = basedate_get_gpsweek() + GPSNTP_WSHIFT;
314 _norm_gps_datum(&date);
315 date.weeks = ((date.weeks - base) & 1023u) + base;
316 }
317
318 ts64 = ntpcal_weekjoin(date.weeks, date.wsecs);
319 ts64 = subv64u32(&ts64, (GPSNTP_DSHIFT * SECSPERDAY));
320 split = ntpcal_daysplit(&ts64);
321
322 retv.frac = gd->frac;
323 retv.secs = split.lo;
324 retv.days = split.hi;
325 return retv;
326 }
327
328 /* -----------------------------------------------------------------
329 * get LFP from ntp datum
330 */
331 l_fp
ntpfp_from_ntpdatum(TcNtpDatum * nd)332 ntpfp_from_ntpdatum(
333 TcNtpDatum * nd
334 )
335 {
336 l_fp retv;
337
338 retv.l_uf = nd->frac;
339 retv.l_ui = nd->days * (uint32_t)SECSPERDAY
340 + nd->secs;
341 return retv;
342 }
343
344 /* -------------------------------------------------------------------
345 * API functions GPS week calendar
346 *
347 * Here we use a calendar base of 1899-12-31, so the NTP epoch has
348 * { 0, 86400.0 } in this representation.
349 * -------------------------------------------------------------------
350 */
351
352 static TGpsDatum
_gpscal_fix_gps_era(TcGpsDatum * in)353 _gpscal_fix_gps_era(
354 TcGpsDatum * in
355 )
356 {
357 /* force result in basedate era
358 *
359 * This is based on calculating the modulus to a power of two,
360 * so signed integer overflow does not affect the result. Which
361 * in turn makes for a very compact calculation...
362 */
363 uint32_t base, week;
364 TGpsDatum out = *in;
365
366 week = out.weeks;
367 base = basedate_get_gpsweek() + GPSNTP_WSHIFT;
368 week = base + ((week - base) & (GPSWEEKS - 1));
369 out.weeks = week;
370 return out;
371 }
372
373 TGpsDatum
gpscal_fix_gps_era(TcGpsDatum * in)374 gpscal_fix_gps_era(
375 TcGpsDatum * in
376 )
377 {
378 TGpsDatum out = *in;
379 _norm_gps_datum(&out);
380 return _gpscal_fix_gps_era(&out);
381 }
382
383 /* -----------------------------------------------------------------
384 * Given a calendar date, zap it into a GPS time format and the do a
385 * proper era mapping in the GPS time scale, based on the GPS base date,
386 * if so requested.
387 *
388 * This function also augments the century if just a 2-digit year
389 * (0..99) is provided on input.
390 *
391 * This is a fail-safe against GPS receivers with an unknown starting
392 * point for their internal calendar calculation and therefore
393 * unpredictable (but reproducible!) rollover behavior. While there
394 * *are* receivers that create a full date in the proper way, many
395 * others just don't. The overall damage is minimized by simply not
396 * trusting the era mapping of the receiver and doing the era assignment
397 * with a configurable base date *inside* ntpd.
398 */
399 TGpsDatum
gpscal_from_calendar_ex(TcCivilDate * jd,l_fp fofs,int warp)400 gpscal_from_calendar_ex(
401 TcCivilDate * jd,
402 l_fp fofs,
403 int/*BOOL*/ warp
404 )
405 {
406 /* (-DAY_GPS_STARTS) (mod 7*1024) -- complement of cycle shift */
407 static const uint32_t s_compl_shift =
408 (7 * 1024) - DAY_GPS_STARTS % (7 * 1024);
409
410 TGpsDatum gps;
411 TCivilDate cal;
412 int32_t days, week;
413
414 /* if needed, convert from 2-digit year to full year
415 * !!NOTE!! works only between 1980 and 2079!
416 */
417 cal = *jd;
418 if (cal.year < 80)
419 cal.year += 2000;
420 else if (cal.year < 100)
421 cal.year += 1900;
422
423 /* get RDN from date, possibly adjusting the century */
424 again: if (cal.month && cal.monthday) { /* use Y/M/D civil date */
425 days = ntpcal_date_to_rd(&cal);
426 } else { /* using Y/DoY date */
427 days = ntpcal_year_to_ystart(cal.year)
428 + (int32_t)cal.yearday
429 - 1; /* both RDN and yearday start with '1'. */
430 }
431
432 /* Rebase to days after the GPS epoch. 'days' is positive here,
433 * but it might be less than the GPS epoch start. Depending on
434 * the input, we have to do different things to get the desired
435 * result. (Since we want to remap the era anyway, we only have
436 * to retain congruential identities....)
437 */
438
439 if (days >= DAY_GPS_STARTS) {
440 /* simply shift to days since GPS epoch */
441 days -= DAY_GPS_STARTS;
442 } else if (jd->year < 100) {
443 /* Two-digit year on input: add another century and
444 * retry. This can happen only if the century expansion
445 * yielded a date between 1980-01-01 and 1980-01-05,
446 * both inclusive. We have at most one retry here.
447 */
448 cal.year += 100;
449 goto again;
450 } else {
451 /* A very bad date before the GPS epoch. There's not
452 * much we can do, except to add the complement of
453 * DAY_GPS_STARTS % (7 * 1024) here, that is, use a
454 * congruential identity: Add the complement instead of
455 * subtracting the value gives a value with the same
456 * modulus. But of course, now we MUST to go through a
457 * cycle fix... because the date was obviously wrong!
458 */
459 warp = TRUE;
460 days += s_compl_shift;
461 }
462
463 /* Splitting to weeks is simple now: */
464 week = days / 7;
465 days -= week * 7;
466
467 /* re-base on start of NTP with weeks mapped to 1024 weeks
468 * starting with the GPS base day set in the calendar.
469 */
470 gps.weeks = week + GPSNTP_WSHIFT;
471 gps.wsecs = days * SECSPERDAY + ntpcal_date_to_daysec(&cal);
472 gps.frac = 0;
473 gpscal_add_offset(&gps, fofs);
474 return warp ? _gpscal_fix_gps_era(&gps) : gps;
475 }
476
477 /* -----------------------------------------------------------------
478 * get civil date from week/tow representation
479 */
480 void
gpscal_to_calendar(TCivilDate * cd,TcGpsDatum * wd)481 gpscal_to_calendar(
482 TCivilDate * cd,
483 TcGpsDatum * wd
484 )
485 {
486 TNtpDatum nd;
487
488 memset(cd, 0, sizeof(*cd));
489 nd = gpsntp_from_gpscal_ex(wd, FALSE);
490 gpsntp_to_calendar(cd, &nd);
491 }
492
493 /* -----------------------------------------------------------------
494 * Given the week and seconds in week, as well as the fraction/offset
495 * (which should/could include the leap seconds offset), unfold the
496 * weeks (which are assumed to have just 10 bits) into expanded weeks
497 * based on the GPS base date derived from the build date (default) or
498 * set by the configuration.
499 *
500 * !NOTE! This function takes RAW GPS weeks, aligned to the GPS start
501 * (1980-01-06) on input. The output weeks will be aligned to NTPD's
502 * week calendar start (1899-12-31)!
503 */
504 TGpsDatum
gpscal_from_gpsweek(uint16_t week,int32_t secs,l_fp fofs)505 gpscal_from_gpsweek(
506 uint16_t week,
507 int32_t secs,
508 l_fp fofs
509 )
510 {
511 TGpsDatum retv;
512
513 retv.frac = 0;
514 retv.wsecs = secs;
515 retv.weeks = week + GPSNTP_WSHIFT;
516 gpscal_add_offset(&retv, fofs);
517 return _gpscal_fix_gps_era(&retv);
518 }
519
520 /* -----------------------------------------------------------------
521 * internal work horse for time-of-week expansion
522 */
523 static TGpsDatum
_gpscal_from_weektime(int32_t wsecs,l_fp fofs,TcGpsDatum * pivot)524 _gpscal_from_weektime(
525 int32_t wsecs,
526 l_fp fofs,
527 TcGpsDatum * pivot
528 )
529 {
530 static const int32_t shift = SECSPERWEEK / 2;
531
532 TGpsDatum retv;
533
534 /* set result based on pivot -- ops order is important here */
535 ZERO(retv);
536 retv.wsecs = wsecs;
537 gpscal_add_offset(&retv, fofs); /* result is normalized */
538 retv.weeks = pivot->weeks;
539
540 /* Manual periodic extension without division: */
541 if (pivot->wsecs < shift) {
542 int32_t lim = pivot->wsecs + shift;
543 retv.weeks -= (retv.wsecs > lim ||
544 (retv.wsecs == lim && retv.frac >= pivot->frac));
545 } else {
546 int32_t lim = pivot->wsecs - shift;
547 retv.weeks += (retv.wsecs < lim ||
548 (retv.wsecs == lim && retv.frac < pivot->frac));
549 }
550 return _gpscal_fix_gps_era(&retv);
551 }
552
553 /* -----------------------------------------------------------------
554 * expand a time-of-week around a pivot given as week datum
555 */
556 TGpsDatum
gpscal_from_weektime2(int32_t wsecs,l_fp fofs,TcGpsDatum * pivot)557 gpscal_from_weektime2(
558 int32_t wsecs,
559 l_fp fofs,
560 TcGpsDatum * pivot
561 )
562 {
563 TGpsDatum wpiv = * pivot;
564 _norm_gps_datum(&wpiv);
565 return _gpscal_from_weektime(wsecs, fofs, &wpiv);
566 }
567
568 /* -----------------------------------------------------------------
569 * epand a time-of-week around an pivot given as LFP, which in turn
570 * is expanded around the current system time and then converted
571 * into a week datum.
572 */
573 TGpsDatum
gpscal_from_weektime1(int32_t wsecs,l_fp fofs,l_fp pivot)574 gpscal_from_weektime1(
575 int32_t wsecs,
576 l_fp fofs,
577 l_fp pivot
578 )
579 {
580 vint64 pvi64;
581 TGpsDatum wpiv;
582 ntpcal_split split;
583
584 /* get 64-bit pivot in NTP epoch */
585 pvi64 = ntpcal_ntp_to_ntp(pivot.l_ui, NULL);
586
587 /* convert to weeks since 1899-12-31 and seconds in week */
588 pvi64 = addv64u32(&pvi64, (GPSNTP_DSHIFT * SECSPERDAY));
589 split = ntpcal_weeksplit(&pvi64);
590
591 wpiv.weeks = split.hi;
592 wpiv.wsecs = split.lo;
593 wpiv.frac = pivot.l_uf;
594 return _gpscal_from_weektime(wsecs, fofs, &wpiv);
595 }
596
597 /* -----------------------------------------------------------------
598 * get week/tow representation from day/tod datum
599 */
600 TGpsDatum
gpscal_from_gpsntp(TcNtpDatum * gd)601 gpscal_from_gpsntp(
602 TcNtpDatum * gd
603 )
604 {
605 TGpsDatum retv;
606 vint64 ts64;
607 ntpcal_split split;
608
609 ts64 = ntpcal_dayjoin(gd->days, gd->secs);
610 ts64 = addv64u32(&ts64, (GPSNTP_DSHIFT * SECSPERDAY));
611 split = ntpcal_weeksplit(&ts64);
612
613 retv.frac = gd->frac;
614 retv.wsecs = split.lo;
615 retv.weeks = split.hi;
616 return retv;
617 }
618
619 /* -----------------------------------------------------------------
620 * convert week/tow to LFP stamp
621 */
622 l_fp
ntpfp_from_gpsdatum(TcGpsDatum * gd)623 ntpfp_from_gpsdatum(
624 TcGpsDatum * gd
625 )
626 {
627 l_fp retv;
628
629 retv.l_uf = gd->frac;
630 retv.l_ui = gd->weeks * (uint32_t)SECSPERWEEK
631 + (uint32_t)gd->wsecs
632 - (uint32_t)SECSPERDAY * GPSNTP_DSHIFT;
633 return retv;
634 }
635
636 /* -*-EOF-*- */
637