xref: /netbsd/external/bsd/ntp/dist/libntp/ntp_calgps.c (revision 9034ec65)
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