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 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