Home | History | Annotate | Line # | Download | only in libntp
      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 (at) 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
     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
     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
     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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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