2d4e511ca2
Update ntp-4.2.8p13 --> 4.2.8p14. The advisory can be found at: http://support.ntp.org/bin/view/Main/SecurityNotice#\ March_2020_ntp_4_2_8p14_NTP_Rele No CVEs have been documented yet. MFC after: now Security: http://support.ntp.org/bin/view/Main/NtpBug3610 http://support.ntp.org/bin/view/Main/NtpBug3596 http://support.ntp.org/bin/view/Main/NtpBug3592
635 lines
16 KiB
C
635 lines
16 KiB
C
/*
|
|
* ntp_calgps.c - calendar for GPS/GNSS based clocks
|
|
*
|
|
* Written by Juergen Perlinger (perlinger@ntp.org) for the NTP project.
|
|
* The contents of 'html/copyright.html' apply.
|
|
*
|
|
* --------------------------------------------------------------------
|
|
*
|
|
* This module implements stuff often used with GPS/GNSS receivers
|
|
*/
|
|
|
|
#include <config.h>
|
|
#include <sys/types.h>
|
|
|
|
#include "ntp_types.h"
|
|
#include "ntp_calendar.h"
|
|
#include "ntp_calgps.h"
|
|
#include "ntp_stdlib.h"
|
|
#include "ntp_unixtime.h"
|
|
|
|
#include "ntp_fp.h"
|
|
#include "ntpd.h"
|
|
#include "vint64ops.h"
|
|
|
|
/* ====================================================================
|
|
* misc. helpers -- might go elsewhere sometime?
|
|
* ====================================================================
|
|
*/
|
|
|
|
l_fp
|
|
ntpfp_with_fudge(
|
|
l_fp lfp,
|
|
double ofs
|
|
)
|
|
{
|
|
l_fp fpo;
|
|
/* calculate 'lfp - ofs' as '(l_fp)(-ofs) + lfp': negating a
|
|
* double is cheap, as it only flips one bit...
|
|
*/
|
|
ofs = -ofs;
|
|
DTOLFP(ofs, &fpo);
|
|
L_ADD(&fpo, &lfp);
|
|
return fpo;
|
|
}
|
|
|
|
|
|
/* ====================================================================
|
|
* GPS calendar functions
|
|
* ====================================================================
|
|
*/
|
|
|
|
/* --------------------------------------------------------------------
|
|
* normalization functions for day/time and week/time representations.
|
|
* Since we only use moderate offsets (leap second corrections and
|
|
* alike) it does not really pay off to do a floor-corrected division
|
|
* here. We use compare/decrement/increment loops instead.
|
|
* --------------------------------------------------------------------
|
|
*/
|
|
static void
|
|
_norm_ntp_datum(
|
|
TNtpDatum * datum
|
|
)
|
|
{
|
|
static const int32_t limit = SECSPERDAY;
|
|
|
|
if (datum->secs >= limit) {
|
|
do
|
|
++datum->days;
|
|
while ((datum->secs -= limit) >= limit);
|
|
} else if (datum->secs < 0) {
|
|
do
|
|
--datum->days;
|
|
while ((datum->secs += limit) < 0);
|
|
}
|
|
}
|
|
|
|
static void
|
|
_norm_gps_datum(
|
|
TGpsDatum * datum
|
|
)
|
|
{
|
|
static const int32_t limit = 7 * SECSPERDAY;
|
|
|
|
if (datum->wsecs >= limit) {
|
|
do
|
|
++datum->weeks;
|
|
while ((datum->wsecs -= limit) >= limit);
|
|
} else if (datum->wsecs < 0) {
|
|
do
|
|
--datum->weeks;
|
|
while ((datum->wsecs += limit) < 0);
|
|
}
|
|
}
|
|
|
|
/* --------------------------------------------------------------------
|
|
* Add an offset to a day/time and week/time representation.
|
|
*
|
|
* !!Attention!! the offset should be small, compared to the time period
|
|
* (either a day or a week).
|
|
* --------------------------------------------------------------------
|
|
*/
|
|
void
|
|
gpsntp_add_offset(
|
|
TNtpDatum * datum,
|
|
l_fp offset
|
|
)
|
|
{
|
|
/* fraction can be added easily */
|
|
datum->frac += offset.l_uf;
|
|
datum->secs += (datum->frac < offset.l_uf);
|
|
|
|
/* avoid integer overflow on the seconds */
|
|
if (offset.l_ui >= INT32_MAX)
|
|
datum->secs -= (int32_t)~offset.l_ui + 1;
|
|
else
|
|
datum->secs += (int32_t)offset.l_ui;
|
|
_norm_ntp_datum(datum);
|
|
}
|
|
|
|
void
|
|
gpscal_add_offset(
|
|
TGpsDatum * datum,
|
|
l_fp offset
|
|
)
|
|
{
|
|
/* fraction can be added easily */
|
|
datum->frac += offset.l_uf;
|
|
datum->wsecs += (datum->frac < offset.l_uf);
|
|
|
|
|
|
/* avoid integer overflow on the seconds */
|
|
if (offset.l_ui >= INT32_MAX)
|
|
datum->wsecs -= (int32_t)~offset.l_ui + 1;
|
|
else
|
|
datum->wsecs += (int32_t)offset.l_ui;
|
|
_norm_gps_datum(datum);
|
|
}
|
|
|
|
/* -------------------------------------------------------------------
|
|
* API functions civil calendar and NTP datum
|
|
* -------------------------------------------------------------------
|
|
*/
|
|
|
|
static TNtpDatum
|
|
_gpsntp_fix_gps_era(
|
|
TcNtpDatum * in
|
|
)
|
|
{
|
|
/* force result in basedate era
|
|
*
|
|
* When calculating this directly in days, we have to execute a
|
|
* real modulus calculation, since we're obviously not doing a
|
|
* modulus by a power of 2. Executing this as true floor mod
|
|
* needs some care and is done under explicit usage of one's
|
|
* complement and masking to get mostly branchless code.
|
|
*/
|
|
static uint32_t const clen = 7*1024;
|
|
|
|
uint32_t base, days, sign;
|
|
TNtpDatum out = *in;
|
|
|
|
/* Get base in NTP day scale. No overflows here. */
|
|
base = (basedate_get_gpsweek() + GPSNTP_WSHIFT) * 7
|
|
- GPSNTP_DSHIFT;
|
|
days = out.days;
|
|
|
|
sign = (uint32_t)-(days < base);
|
|
days = sign ^ (days - base);
|
|
days %= clen;
|
|
days = base + (sign & clen) + (sign ^ days);
|
|
|
|
out.days = days;
|
|
return out;
|
|
}
|
|
|
|
TNtpDatum
|
|
gpsntp_fix_gps_era(
|
|
TcNtpDatum * in
|
|
)
|
|
{
|
|
TNtpDatum out = *in;
|
|
_norm_ntp_datum(&out);
|
|
return _gpsntp_fix_gps_era(&out);
|
|
}
|
|
|
|
/* ----------------------------------------------------------------- */
|
|
static TNtpDatum
|
|
_gpsntp_from_daytime(
|
|
TcCivilDate * jd,
|
|
l_fp fofs,
|
|
TcNtpDatum * pivot,
|
|
int warp
|
|
)
|
|
{
|
|
static const int32_t shift = SECSPERDAY / 2;
|
|
|
|
TNtpDatum retv;
|
|
|
|
/* set result based on pivot -- ops order is important here */
|
|
ZERO(retv);
|
|
retv.secs = ntpcal_date_to_daysec(jd);
|
|
gpsntp_add_offset(&retv, fofs); /* result is normalized */
|
|
retv.days = pivot->days;
|
|
|
|
/* Manual periodic extension without division: */
|
|
if (pivot->secs < shift) {
|
|
int32_t lim = pivot->secs + shift;
|
|
retv.days -= (retv.secs > lim ||
|
|
(retv.secs == lim && retv.frac >= pivot->frac));
|
|
} else {
|
|
int32_t lim = pivot->secs - shift;
|
|
retv.days += (retv.secs < lim ||
|
|
(retv.secs == lim && retv.frac < pivot->frac));
|
|
}
|
|
return warp ? _gpsntp_fix_gps_era(&retv) : retv;
|
|
}
|
|
|
|
/* -----------------------------------------------------------------
|
|
* Given the time-of-day part of a civil datum and an additional
|
|
* (fractional) offset, calculate a full time stamp around a given pivot
|
|
* time so that the difference between the pivot and the resulting time
|
|
* stamp is less or equal to 12 hours absolute.
|
|
*/
|
|
TNtpDatum
|
|
gpsntp_from_daytime2_ex(
|
|
TcCivilDate * jd,
|
|
l_fp fofs,
|
|
TcNtpDatum * pivot,
|
|
int/*BOOL*/ warp
|
|
)
|
|
{
|
|
TNtpDatum dpiv = *pivot;
|
|
_norm_ntp_datum(&dpiv);
|
|
return _gpsntp_from_daytime(jd, fofs, &dpiv, warp);
|
|
}
|
|
|
|
/* -----------------------------------------------------------------
|
|
* This works similar to 'gpsntp_from_daytime1()' and actually even uses
|
|
* it, but the pivot is calculated from the pivot given as 'l_fp' in NTP
|
|
* time scale. This is in turn expanded around the current system time,
|
|
* and the resulting absolute pivot is then used to calculate the full
|
|
* NTP time stamp.
|
|
*/
|
|
TNtpDatum
|
|
gpsntp_from_daytime1_ex(
|
|
TcCivilDate * jd,
|
|
l_fp fofs,
|
|
l_fp pivot,
|
|
int/*BOOL*/ warp
|
|
)
|
|
{
|
|
vint64 pvi64;
|
|
TNtpDatum dpiv;
|
|
ntpcal_split split;
|
|
|
|
pvi64 = ntpcal_ntp_to_ntp(pivot.l_ui, NULL);
|
|
split = ntpcal_daysplit(&pvi64);
|
|
dpiv.days = split.hi;
|
|
dpiv.secs = split.lo;
|
|
dpiv.frac = pivot.l_uf;
|
|
return _gpsntp_from_daytime(jd, fofs, &dpiv, warp);
|
|
}
|
|
|
|
/* -----------------------------------------------------------------
|
|
* Given a calendar date, zap it into a GPS time format and then convert
|
|
* that one into the NTP time scale.
|
|
*/
|
|
TNtpDatum
|
|
gpsntp_from_calendar_ex(
|
|
TcCivilDate * jd,
|
|
l_fp fofs,
|
|
int/*BOOL*/ warp
|
|
)
|
|
{
|
|
TGpsDatum gps;
|
|
gps = gpscal_from_calendar_ex(jd, fofs, warp);
|
|
return gpsntp_from_gpscal_ex(&gps, FALSE);
|
|
}
|
|
|
|
/* -----------------------------------------------------------------
|
|
* create a civil calendar datum from a NTP date representation
|
|
*/
|
|
void
|
|
gpsntp_to_calendar(
|
|
TCivilDate * cd,
|
|
TcNtpDatum * nd
|
|
)
|
|
{
|
|
memset(cd, 0, sizeof(*cd));
|
|
ntpcal_rd_to_date(
|
|
cd,
|
|
nd->days + DAY_NTP_STARTS + ntpcal_daysec_to_date(
|
|
cd, nd->secs));
|
|
}
|
|
|
|
/* -----------------------------------------------------------------
|
|
* get day/tod representation from week/tow datum
|
|
*/
|
|
TNtpDatum
|
|
gpsntp_from_gpscal_ex(
|
|
TcGpsDatum * gd,
|
|
int/*BOOL*/ warp
|
|
)
|
|
{
|
|
TNtpDatum retv;
|
|
vint64 ts64;
|
|
ntpcal_split split;
|
|
TGpsDatum date = *gd;
|
|
|
|
if (warp) {
|
|
uint32_t base = basedate_get_gpsweek() + GPSNTP_WSHIFT;
|
|
_norm_gps_datum(&date);
|
|
date.weeks = ((date.weeks - base) & 1023u) + base;
|
|
}
|
|
|
|
ts64 = ntpcal_weekjoin(date.weeks, date.wsecs);
|
|
ts64 = subv64u32(&ts64, (GPSNTP_DSHIFT * SECSPERDAY));
|
|
split = ntpcal_daysplit(&ts64);
|
|
|
|
retv.frac = gd->frac;
|
|
retv.secs = split.lo;
|
|
retv.days = split.hi;
|
|
return retv;
|
|
}
|
|
|
|
/* -----------------------------------------------------------------
|
|
* get LFP from ntp datum
|
|
*/
|
|
l_fp
|
|
ntpfp_from_ntpdatum(
|
|
TcNtpDatum * nd
|
|
)
|
|
{
|
|
l_fp retv;
|
|
|
|
retv.l_uf = nd->frac;
|
|
retv.l_ui = nd->days * (uint32_t)SECSPERDAY
|
|
+ nd->secs;
|
|
return retv;
|
|
}
|
|
|
|
/* -------------------------------------------------------------------
|
|
* API functions GPS week calendar
|
|
*
|
|
* Here we use a calendar base of 1899-12-31, so the NTP epoch has
|
|
* { 0, 86400.0 } in this representation.
|
|
* -------------------------------------------------------------------
|
|
*/
|
|
|
|
static TGpsDatum
|
|
_gpscal_fix_gps_era(
|
|
TcGpsDatum * in
|
|
)
|
|
{
|
|
/* force result in basedate era
|
|
*
|
|
* This is based on calculating the modulus to a power of two,
|
|
* so signed integer overflow does not affect the result. Which
|
|
* in turn makes for a very compact calculation...
|
|
*/
|
|
uint32_t base, week;
|
|
TGpsDatum out = *in;
|
|
|
|
week = out.weeks;
|
|
base = basedate_get_gpsweek() + GPSNTP_WSHIFT;
|
|
week = base + ((week - base) & (GPSWEEKS - 1));
|
|
out.weeks = week;
|
|
return out;
|
|
}
|
|
|
|
TGpsDatum
|
|
gpscal_fix_gps_era(
|
|
TcGpsDatum * in
|
|
)
|
|
{
|
|
TGpsDatum out = *in;
|
|
_norm_gps_datum(&out);
|
|
return _gpscal_fix_gps_era(&out);
|
|
}
|
|
|
|
/* -----------------------------------------------------------------
|
|
* Given a calendar date, zap it into a GPS time format and the do a
|
|
* proper era mapping in the GPS time scale, based on the GPS base date,
|
|
* if so requested.
|
|
*
|
|
* This function also augments the century if just a 2-digit year
|
|
* (0..99) is provided on input.
|
|
*
|
|
* This is a fail-safe against GPS receivers with an unknown starting
|
|
* point for their internal calendar calculation and therefore
|
|
* unpredictable (but reproducible!) rollover behavior. While there
|
|
* *are* receivers that create a full date in the proper way, many
|
|
* others just don't. The overall damage is minimized by simply not
|
|
* trusting the era mapping of the receiver and doing the era assignment
|
|
* with a configurable base date *inside* ntpd.
|
|
*/
|
|
TGpsDatum
|
|
gpscal_from_calendar_ex(
|
|
TcCivilDate * jd,
|
|
l_fp fofs,
|
|
int/*BOOL*/ warp
|
|
)
|
|
{
|
|
/* (-DAY_GPS_STARTS) (mod 7*1024) -- complement of cycle shift */
|
|
static const uint32_t s_compl_shift =
|
|
(7 * 1024) - DAY_GPS_STARTS % (7 * 1024);
|
|
|
|
TGpsDatum gps;
|
|
TCivilDate cal;
|
|
int32_t days, week;
|
|
|
|
/* if needed, convert from 2-digit year to full year
|
|
* !!NOTE!! works only between 1980 and 2079!
|
|
*/
|
|
cal = *jd;
|
|
if (cal.year < 80)
|
|
cal.year += 2000;
|
|
else if (cal.year < 100)
|
|
cal.year += 1900;
|
|
|
|
/* get RDN from date, possibly adjusting the century */
|
|
again: if (cal.month && cal.monthday) { /* use Y/M/D civil date */
|
|
days = ntpcal_date_to_rd(&cal);
|
|
} else { /* using Y/DoY date */
|
|
days = ntpcal_year_to_ystart(cal.year)
|
|
+ (int32_t)cal.yearday
|
|
- 1; /* both RDN and yearday start with '1'. */
|
|
}
|
|
|
|
/* Rebase to days after the GPS epoch. 'days' is positive here,
|
|
* but it might be less than the GPS epoch start. Depending on
|
|
* the input, we have to do different things to get the desired
|
|
* result. (Since we want to remap the era anyway, we only have
|
|
* to retain congruential identities....)
|
|
*/
|
|
|
|
if (days >= DAY_GPS_STARTS) {
|
|
/* simply shift to days since GPS epoch */
|
|
days -= DAY_GPS_STARTS;
|
|
} else if (jd->year < 100) {
|
|
/* Two-digit year on input: add another century and
|
|
* retry. This can happen only if the century expansion
|
|
* yielded a date between 1980-01-01 and 1980-01-05,
|
|
* both inclusive. We have at most one retry here.
|
|
*/
|
|
cal.year += 100;
|
|
goto again;
|
|
} else {
|
|
/* A very bad date before the GPS epoch. There's not
|
|
* much we can do, except to add the complement of
|
|
* DAY_GPS_STARTS % (7 * 1024) here, that is, use a
|
|
* congruential identity: Add the complement instead of
|
|
* subtracting the value gives a value with the same
|
|
* modulus. But of course, now we MUST to go through a
|
|
* cycle fix... because the date was obviously wrong!
|
|
*/
|
|
warp = TRUE;
|
|
days += s_compl_shift;
|
|
}
|
|
|
|
/* Splitting to weeks is simple now: */
|
|
week = days / 7;
|
|
days -= week * 7;
|
|
|
|
/* re-base on start of NTP with weeks mapped to 1024 weeks
|
|
* starting with the GPS base day set in the calendar.
|
|
*/
|
|
gps.weeks = week + GPSNTP_WSHIFT;
|
|
gps.wsecs = days * SECSPERDAY + ntpcal_date_to_daysec(&cal);
|
|
gps.frac = 0;
|
|
gpscal_add_offset(&gps, fofs);
|
|
return warp ? _gpscal_fix_gps_era(&gps) : gps;
|
|
}
|
|
|
|
/* -----------------------------------------------------------------
|
|
* get civil date from week/tow representation
|
|
*/
|
|
void
|
|
gpscal_to_calendar(
|
|
TCivilDate * cd,
|
|
TcGpsDatum * wd
|
|
)
|
|
{
|
|
TNtpDatum nd;
|
|
|
|
memset(cd, 0, sizeof(*cd));
|
|
nd = gpsntp_from_gpscal_ex(wd, FALSE);
|
|
gpsntp_to_calendar(cd, &nd);
|
|
}
|
|
|
|
/* -----------------------------------------------------------------
|
|
* Given the week and seconds in week, as well as the fraction/offset
|
|
* (which should/could include the leap seconds offset), unfold the
|
|
* weeks (which are assumed to have just 10 bits) into expanded weeks
|
|
* based on the GPS base date derived from the build date (default) or
|
|
* set by the configuration.
|
|
*
|
|
* !NOTE! This function takes RAW GPS weeks, aligned to the GPS start
|
|
* (1980-01-06) on input. The output weeks will be aligned to NTPD's
|
|
* week calendar start (1899-12-31)!
|
|
*/
|
|
TGpsDatum
|
|
gpscal_from_gpsweek(
|
|
uint16_t week,
|
|
int32_t secs,
|
|
l_fp fofs
|
|
)
|
|
{
|
|
TGpsDatum retv;
|
|
|
|
retv.frac = 0;
|
|
retv.wsecs = secs;
|
|
retv.weeks = week + GPSNTP_WSHIFT;
|
|
gpscal_add_offset(&retv, fofs);
|
|
return _gpscal_fix_gps_era(&retv);
|
|
}
|
|
|
|
/* -----------------------------------------------------------------
|
|
* internal work horse for time-of-week expansion
|
|
*/
|
|
static TGpsDatum
|
|
_gpscal_from_weektime(
|
|
int32_t wsecs,
|
|
l_fp fofs,
|
|
TcGpsDatum * pivot
|
|
)
|
|
{
|
|
static const int32_t shift = SECSPERWEEK / 2;
|
|
|
|
TGpsDatum retv;
|
|
|
|
/* set result based on pivot -- ops order is important here */
|
|
ZERO(retv);
|
|
retv.wsecs = wsecs;
|
|
gpscal_add_offset(&retv, fofs); /* result is normalized */
|
|
retv.weeks = pivot->weeks;
|
|
|
|
/* Manual periodic extension without division: */
|
|
if (pivot->wsecs < shift) {
|
|
int32_t lim = pivot->wsecs + shift;
|
|
retv.weeks -= (retv.wsecs > lim ||
|
|
(retv.wsecs == lim && retv.frac >= pivot->frac));
|
|
} else {
|
|
int32_t lim = pivot->wsecs - shift;
|
|
retv.weeks += (retv.wsecs < lim ||
|
|
(retv.wsecs == lim && retv.frac < pivot->frac));
|
|
}
|
|
return _gpscal_fix_gps_era(&retv);
|
|
}
|
|
|
|
/* -----------------------------------------------------------------
|
|
* expand a time-of-week around a pivot given as week datum
|
|
*/
|
|
TGpsDatum
|
|
gpscal_from_weektime2(
|
|
int32_t wsecs,
|
|
l_fp fofs,
|
|
TcGpsDatum * pivot
|
|
)
|
|
{
|
|
TGpsDatum wpiv = * pivot;
|
|
_norm_gps_datum(&wpiv);
|
|
return _gpscal_from_weektime(wsecs, fofs, &wpiv);
|
|
}
|
|
|
|
/* -----------------------------------------------------------------
|
|
* epand a time-of-week around an pivot given as LFP, which in turn
|
|
* is expanded around the current system time and then converted
|
|
* into a week datum.
|
|
*/
|
|
TGpsDatum
|
|
gpscal_from_weektime1(
|
|
int32_t wsecs,
|
|
l_fp fofs,
|
|
l_fp pivot
|
|
)
|
|
{
|
|
vint64 pvi64;
|
|
TGpsDatum wpiv;
|
|
ntpcal_split split;
|
|
|
|
/* get 64-bit pivot in NTP epoch */
|
|
pvi64 = ntpcal_ntp_to_ntp(pivot.l_ui, NULL);
|
|
|
|
/* convert to weeks since 1899-12-31 and seconds in week */
|
|
pvi64 = addv64u32(&pvi64, (GPSNTP_DSHIFT * SECSPERDAY));
|
|
split = ntpcal_weeksplit(&pvi64);
|
|
|
|
wpiv.weeks = split.hi;
|
|
wpiv.wsecs = split.lo;
|
|
wpiv.frac = pivot.l_uf;
|
|
return _gpscal_from_weektime(wsecs, fofs, &wpiv);
|
|
}
|
|
|
|
/* -----------------------------------------------------------------
|
|
* get week/tow representation from day/tod datum
|
|
*/
|
|
TGpsDatum
|
|
gpscal_from_gpsntp(
|
|
TcNtpDatum * gd
|
|
)
|
|
{
|
|
TGpsDatum retv;
|
|
vint64 ts64;
|
|
ntpcal_split split;
|
|
|
|
ts64 = ntpcal_dayjoin(gd->days, gd->secs);
|
|
ts64 = addv64u32(&ts64, (GPSNTP_DSHIFT * SECSPERDAY));
|
|
split = ntpcal_weeksplit(&ts64);
|
|
|
|
retv.frac = gd->frac;
|
|
retv.wsecs = split.lo;
|
|
retv.weeks = split.hi;
|
|
return retv;
|
|
}
|
|
|
|
/* -----------------------------------------------------------------
|
|
* convert week/tow to LFP stamp
|
|
*/
|
|
l_fp
|
|
ntpfp_from_gpsdatum(
|
|
TcGpsDatum * gd
|
|
)
|
|
{
|
|
l_fp retv;
|
|
|
|
retv.l_uf = gd->frac;
|
|
retv.l_ui = gd->weeks * (uint32_t)SECSPERWEEK
|
|
+ (uint32_t)gd->wsecs
|
|
- (uint32_t)SECSPERDAY * GPSNTP_DSHIFT;
|
|
return retv;
|
|
}
|
|
|
|
/* -*-EOF-*- */
|