lib/timeutil: add utilities to manage local/reference clock alignment

Provide data structures to capture a timestamp in two different
clocks, monitor the drift between those clocks, and using a base
instant with estimated drift convert between the clocks.

This provides the core technology to convert between system uptime and
an external continuous time scale like TAI (UTC without applying leap
seconds).

Signed-off-by: Peter A. Bigot <pab@pabigot.com>
This commit is contained in:
Peter A. Bigot 2020-08-01 13:25:58 -05:00 committed by Anas Nashif
commit f6d7595d4e
7 changed files with 742 additions and 5 deletions

View file

@ -53,6 +53,236 @@ int64_t timeutil_timegm64(const struct tm *tm);
*/
time_t timeutil_timegm(const struct tm *tm);
/**
* @brief Immutable state for synchronizing two clocks.
*
* Values required to convert durations between two time scales.
*
* @note The accuracy of the translation and calculated skew between sources
* depends on the resolution of these frequencies. A reference frequency with
* microsecond or nanosecond resolution would produce the most accurate
* tracking when the local reference is the Zephyr tick counter. A reference
* source like an RTC chip with 1 Hz resolution requires a much larger
* interval between sampled instants to detect relative clock drift.
*/
struct timeutil_sync_config {
/** The nominal instance counter rate in Hz.
*
* This value is assumed to be precise, but may drift depending on
* the reference clock source.
*
* The value must be positive.
*/
uint32_t ref_Hz;
/** The nominal local counter rate in Hz.
*
* This value is assumed to be inaccurate but reasonably stable. For
* a local clock driven by a crystal oscillator an error of 25 ppm is
* common; for an RC oscillator larger errors should be expected. The
* timeutil_sync infrastructure can calculate the skew between the
* local and reference clocks and apply it when converting between
* time scales.
*
* The value must be positive.
*/
uint32_t local_Hz;
};
/**
* @brief Representation of an instant in two time scales.
*
* Capturing the same instant in two time scales provides a
* registration point that can be used to convert between those time
* scales.
*/
struct timeutil_sync_instant {
/** An instant in the reference time scale.
*
* This must never be zero in an initialized timeutil_sync_instant
* object.
*/
uint64_t ref;
/** The corresponding instance in the local time scale.
*
* This may be zero in a valid timeutil_sync_instant object.
*/
uint64_t local;
};
/**
* @brief State required to convert instants between time scales.
*
* This state in conjunction with functions that manipulate it capture
* the offset information necessary to convert between two timescales
* along with information that corrects for skew due to inaccuracies
* in clock rates.
*
* State objects should be zero-initialized before use.
*/
struct timeutil_sync_state {
/** Pointer to reference and local rate information. */
const struct timeutil_sync_config *cfg;
/** The base instant in both time scales. */
struct timeutil_sync_instant base;
/** The most recent instant in both time scales.
*
* This is captured here to provide data for skew calculation.
*/
struct timeutil_sync_instant latest;
/** The scale factor used to correct for clock skew.
*
* The nominal rate for the local counter is assumed to be
* inaccurate but stable, i.e. it will generally be some
* parts-per-million faster or slower than specified.
*
* A duration in observed local clock ticks must be multiplied by
* this value to produce a duration in ticks of a clock operating at
* the nominal local rate.
*
* A zero value indicates that the skew has not been initialized.
* If the value is zero when #base is initialized the skew will be
* set to 1. Otherwise the skew is assigned through
* timeutil_sync_state_set_skew().
*/
float skew;
};
/**
* @brief Record a new instant in the time synchronization state.
*
* Note that this updates only the latest persisted instant. The skew
* is not adjusted automatically.
*
* @param tsp pointer to a timeutil_sync_state object.
*
* @param inst the new instant to be recorded. This becomes the base
* instant if there is no base instant, otherwise the value must be
* strictly after the base instant in both the reference and local
* time scales.
*
* @retval 0 if installation succeeded in providing a new base
* @retval 1 if installation provided a new latest instant
* @retval -EINVAL if the new instant is not compatible with the base instant
*/
int timeutil_sync_state_update(struct timeutil_sync_state *tsp,
const struct timeutil_sync_instant *inst);
/**
* @brief Update the state with a new skew and possibly base value.
*
* Set the skew from a value retrieved from persistent storage, or
* calculated based on recent skew estimations including from
* timeutil_sync_estimate_skew().
*
* Optionally update the base timestamp. If the base is replaced the
* latest instant will be cleared until timeutil_sync_state_update() is
* invoked.
*
* @param tsp pointer to a time synchronization state.
*
* @param skew the skew to be used. The value must be positive and
* shouldn't be too far away from 1.
*
* @param base optional new base to be set. If provided this becomes
* the base timestamp that will be used along with skew to convert
* between reference and local timescale instants. Setting the base
* clears the captured latest value.
*
* @return 0 if skew was updated
* @return -EINVAL if skew was not valid
*/
int timeutil_sync_state_set_skew(struct timeutil_sync_state *tsp, float skew,
const struct timeutil_sync_instant *base);
/**
* @brief Estimate the skew based on current state.
*
* Using the base and latest syncpoints from the state determine the
* skew of the local clock relative to the reference clock. See
* timeutil_sync_state::skew.
*
* @param tsp pointer to a time synchronization state. The base and latest
* syncpoints must be present and the latest syncpoint must be after
* the base point in the local time scale.
*
* @return the estimated skew, or zero if skew could not be estimated.
*/
float timeutil_sync_estimate_skew(const struct timeutil_sync_state *tsp);
/**
* @brief Interpolate a reference timescale instant from a local
* instant.
*
* @param tsp pointer to a time synchronization state. This must have a base
* and a skew installed.
*
* @param local an instant measured in the local timescale. This may
* be before or after the base instant.
*
* @param refp where the corresponding instant in the reference
* timescale should be stored. A negative interpolated reference time
* produces an error. If interpolation fails the referenced object is
* not modified.
*
* @retval 0 if interpolated using a skew of 1
* @retval 1 if interpolated using a skew not equal to 1
* @retval -EINVAL
* * the times synchronization state is not adequately initialized
* * @p refp is null
* @retval -ERANGE the interpolated reference time would be negative
*/
int timeutil_sync_ref_from_local(const struct timeutil_sync_state *tsp,
uint64_t local, uint64_t *refp);
/**
* @brief Interpolate a local timescale instant from a reference
* instant.
*
* @param tsp pointer to a time synchronization state. This must have a base
* and a skew installed.
*
* @param ref an instant measured in the reference timescale. This
* may be before or after the base instant.
*
* @param localp where the corresponding instant in the local
* timescale should be stored. An interpolated value before local
* time 0 is provided without error. If interpolation fails the
* referenced object is not modified.
*
* @retval 0 if successful with a skew of 1
* @retval 1 if successful with a skew not equal to 1
* @retval -EINVAL
* * the time synchronization state is not adequately initialized
* * @p refp is null
*/
int timeutil_sync_local_from_ref(const struct timeutil_sync_state *tsp,
uint64_t ref, int64_t *localp);
/**
* @brief Convert from a skew to an error in parts-per-billion.
*
* A skew of 1.0 has zero error. A skew less than 1 has a positive
* error (clock is faster than it should be). A skew greater than one
* has a negative error (clock is slower than it should be).
*
* Note that due to the limited precision of @c float compared with @c
* double the smallest error that can be represented is about 120 ppb.
* A "precise" time source may have error on the order of 2000 ppb.
*
* A skew greater than 3.14748 may underflow the 32-bit
* representation; this represents a clock running at less than 1/3
* its nominal rate.
*
* @return skew error represented as parts-per-billion, or INT32_MIN
* if the skew cannot be represented in the return type.
*/
int32_t timeutil_sync_skew_to_ppb(float skew);
#ifdef __cplusplus
}
#endif

View file

@ -12,6 +12,7 @@
#include <zephyr/types.h>
#include <errno.h>
#include <stddef.h>
#include <sys/timeutil.h>
/** Convert a civil (proleptic Gregorian) date to days relative to
@ -66,5 +67,112 @@ time_t timeutil_timegm(const struct tm *tm)
errno = ERANGE;
rv = -1;
}
return rv;
}
int timeutil_sync_state_update(struct timeutil_sync_state *tsp,
const struct timeutil_sync_instant *inst)
{
int rv = -EINVAL;
if (((tsp->base.ref == 0) && (inst->ref > 0))
|| ((inst->ref > tsp->base.ref)
&& (inst->local > tsp->base.local))) {
if (tsp->base.ref == 0) {
tsp->base = *inst;
tsp->latest = (struct timeutil_sync_instant){};
tsp->skew = 1.0;
rv = 0;
} else {
tsp->latest = *inst;
rv = 1;
}
}
return rv;
}
int timeutil_sync_state_set_skew(struct timeutil_sync_state *tsp, float skew,
const struct timeutil_sync_instant *base)
{
int rv = -EINVAL;
if (skew > 0) {
tsp->skew = skew;
if (base != NULL) {
tsp->base = *base;
tsp->latest = (struct timeutil_sync_instant){};
}
rv = 0;
}
return rv;
}
float timeutil_sync_estimate_skew(const struct timeutil_sync_state *tsp)
{
float rv = 0;
if ((tsp->base.ref != 0) && (tsp->latest.ref != 0)
&& (tsp->latest.local > tsp->base.local)) {
const struct timeutil_sync_config *cfg = tsp->cfg;
double ref_delta = tsp->latest.ref - tsp->base.ref;
double local_delta = tsp->latest.local - tsp->base.local;
rv = ref_delta * cfg->local_Hz / local_delta / cfg->ref_Hz;
}
return rv;
}
int timeutil_sync_ref_from_local(const struct timeutil_sync_state *tsp,
uint64_t local, uint64_t *refp)
{
int rv = -EINVAL;
if ((tsp->skew > 0) && (tsp->base.ref > 0) && (refp != NULL)) {
const struct timeutil_sync_config *cfg = tsp->cfg;
int64_t local_delta = local - tsp->base.local;
int64_t ref_delta = (int64_t)(tsp->skew * local_delta) *
cfg->ref_Hz / cfg->local_Hz;
int64_t ref_abs = (int64_t)tsp->base.ref + ref_delta;
if (ref_abs < 0) {
rv = -ERANGE;
} else {
*refp = ref_abs;
rv = (int)(tsp->skew != 1.0);
}
}
return rv;
}
int timeutil_sync_local_from_ref(const struct timeutil_sync_state *tsp,
uint64_t ref, int64_t *localp)
{
int rv = -EINVAL;
if ((tsp->skew > 0) && (tsp->base.ref > 0) && (localp != NULL)) {
const struct timeutil_sync_config *cfg = tsp->cfg;
int64_t ref_delta = (int64_t)(ref - tsp->base.ref);
double local_delta = (ref_delta * cfg->local_Hz) / cfg->ref_Hz
/ tsp->skew;
int64_t local_abs = (int64_t)tsp->base.local
+ (int64_t)local_delta;
*localp = local_abs;
rv = (int)(tsp->skew != 1.0);
}
return rv;
}
int32_t timeutil_sync_skew_to_ppb(float skew)
{
int64_t ppb64 = (int64_t)((1.0 - skew) * 1E9);
int32_t ppb32 = (int32_t)ppb64;
return (ppb64 == ppb32) ? ppb32 : INT32_MIN;
}

View file

@ -1,5 +1,6 @@
# Copyright 2019-2020 Peter Bigot Consulting
# SPDX-License-Identifier: Apache-2.0
project(timeutil)
set(SOURCES main.c test_gmtime.c test_s32.c test_s64.c)
set(SOURCES main.c test_gmtime.c test_s32.c test_s64.c test_sync.c)
find_package(ZephyrUnittest REQUIRED HINTS $ENV{ZEPHYR_BASE})

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Peter Bigot Consulting
* Copyright 2019-2020 Peter Bigot Consulting
*
* SPDX-License-Identifier: Apache-2.0
*/
@ -58,7 +58,8 @@ void test_main(void)
ztest_test_suite(test_timeutil_api,
ztest_unit_test(test_gmtime),
ztest_unit_test(test_s32),
ztest_unit_test(test_s64)
ztest_unit_test(test_s64),
ztest_unit_test(test_sync)
);
ztest_run_test_suite(test_timeutil_api);
}

View file

@ -0,0 +1,396 @@
/*
* Copyright 2020 Peter Bigot Consulting
*
* SPDX-License-Identifier: Apache-2.0
*/
/* Tests for the time_sync data structures */
#include <string.h>
#include <ztest.h>
#include "timeutil_test.h"
static const struct timeutil_sync_config cfg1 = {
.ref_Hz = USEC_PER_SEC,
.local_Hz = 32768,
};
static const struct timeutil_sync_config cfg2 = {
.ref_Hz = NSEC_PER_SEC,
.local_Hz = 100,
};
static inline uint64_t scale_ref(uint32_t factor,
const struct timeutil_sync_config *cfg)
{
return (uint64_t)factor * (uint64_t)cfg->ref_Hz;
}
static inline uint64_t scale_local(uint32_t factor,
const struct timeutil_sync_config *cfg)
{
return (uint64_t)factor * (uint64_t)cfg->local_Hz;
}
static inline int64_t scale_local_signed(int32_t factor,
const struct timeutil_sync_config *cfg)
{
return (int64_t)factor * (int64_t)cfg->local_Hz;
}
static void test_state_update(void)
{
struct timeutil_sync_instant si = { 0 };
struct timeutil_sync_state ss = { 0 };
int rv = timeutil_sync_state_update(&ss, &si);
zassert_equal(rv, -EINVAL,
"invalid init got: %d", rv);
zassert_equal(ss.base.ref, 0,
"unexpected base ref");
zassert_equal(ss.skew, 0,
"unexpected skew");
si.ref = 1;
rv = timeutil_sync_state_update(&ss, &si);
zassert_equal(rv, 0,
"valid first init got: %d", rv);
zassert_equal(ss.base.ref, 1,
"base not updated");
zassert_equal(ss.latest.ref, 0,
"unexpected latest ref");
zassert_equal(ss.skew, 1.0,
"unexpected skew");
rv = timeutil_sync_state_update(&ss, &si);
zassert_equal(rv, -EINVAL,
"non-increasing ref got: %d", rv);
zassert_equal(ss.base.ref, 1,
"unexpected base ref");
zassert_equal(ss.base.local, 0,
"unexpected base local");
zassert_equal(ss.latest.ref, 0,
"unexpected latest ref");
si.ref += 1;
rv = timeutil_sync_state_update(&ss, &si);
zassert_equal(rv, -EINVAL,
"non-increasing local got: %d", rv);
zassert_equal(ss.latest.ref, 0,
"unexpected latest ref");
si.local += 20;
rv = timeutil_sync_state_update(&ss, &si);
zassert_equal(rv, 1,
"increasing got: %d", rv);
zassert_equal(ss.base.ref, 1,
"unexpected base ref");
zassert_equal(ss.base.local, 0,
"unexpected base local");
zassert_equal(ss.latest.ref, si.ref,
"unexpected latest ref");
zassert_equal(ss.latest.local, si.local,
"unexpected latest local");
}
static void test_state_set_skew(void)
{
struct timeutil_sync_instant si = {
.ref = 1,
};
struct timeutil_sync_state ss = {
.cfg = &cfg1,
};
float skew = 0.99;
int rv = timeutil_sync_state_update(&ss, &si);
zassert_equal(rv, 0,
"valid first init got: %d", rv);
zassert_equal(ss.skew, 1.0,
"unexpected skew");
rv = timeutil_sync_state_set_skew(&ss, -1.0, NULL);
zassert_equal(rv, -EINVAL,
"negative skew set got: %d", rv);
zassert_equal(ss.skew, 1.0,
"unexpected skew");
rv = timeutil_sync_state_set_skew(&ss, 0.0, NULL);
zassert_equal(rv, -EINVAL,
"zero skew set got: %d", rv);
zassert_equal(ss.skew, 1.0,
"unexpected skew");
rv = timeutil_sync_state_set_skew(&ss, skew, NULL);
zassert_equal(rv, 0,
"valid skew set got: %d", rv);
zassert_equal(ss.skew, skew,
"unexpected skew");
zassert_equal(ss.base.ref, si.ref,
"unexpected base ref");
zassert_equal(ss.base.local, si.local,
"unexpected base ref");
skew = 1.01;
si.ref += 5;
si.local += 3;
rv = timeutil_sync_state_set_skew(&ss, skew, &si);
zassert_equal(rv, 0,
"valid skew set got: %d", rv);
zassert_equal(ss.skew, skew,
"unexpected skew");
zassert_equal(ss.base.ref, si.ref,
"unexpected base ref");
zassert_equal(ss.base.local, si.local,
"unexpected base ref");
zassert_equal(ss.latest.ref, 0,
"uncleared latest ref");
zassert_equal(ss.latest.local, 0,
"uncleared latest local");
}
static void test_estimate_skew(void)
{
struct timeutil_sync_state ss = {
.cfg = &cfg1,
};
struct timeutil_sync_instant si0 = {
.ref = cfg1.ref_Hz,
};
struct timeutil_sync_instant si1 = {
.ref = si0.ref + cfg1.ref_Hz,
.local = si0.local + cfg1.local_Hz,
};
float skew = 0.0;
skew = timeutil_sync_estimate_skew(&ss);
zassert_equal(skew, 0,
"unexpected uninit skew: %f", skew);
int rv = timeutil_sync_state_update(&ss, &si0);
zassert_equal(rv, 0,
"valid init got: %d", rv);
skew = timeutil_sync_estimate_skew(&ss);
zassert_equal(skew, 0,
"unexpected base-only skew: %f", skew);
rv = timeutil_sync_state_update(&ss, &si1);
zassert_equal(rv, 1,
"valid update got: %d", rv);
zassert_equal(ss.base.ref, si0.ref,
"unexpected base ref");
zassert_equal(ss.base.local, si0.local,
"unexpected base local");
zassert_equal(ss.latest.ref, si1.ref,
"unexpected latest ref");
zassert_equal(ss.latest.local, si1.local,
"unexpected latest local");
skew = timeutil_sync_estimate_skew(&ss);
zassert_equal(skew, 1.0,
"unexpected linear skew: %f", skew);
/* Local advanced half as far as it should: scale by 2 to
* correct.
*/
ss.latest.local = scale_local(1, ss.cfg) / 2;
skew = timeutil_sync_estimate_skew(&ss);
zassert_equal(skew, 2.0,
"unexpected half skew: %f", skew);
/* Local advanced twice as far as it should: scale by 1/2 to
* correct.
*/
ss.latest.local = scale_local(2, ss.cfg);
skew = timeutil_sync_estimate_skew(&ss);
zassert_equal(skew, 0.5,
"unexpected double skew: %f", skew);
}
static void tref_from_local(const char *tag,
const struct timeutil_sync_config *cfg)
{
struct timeutil_sync_state ss = {
.cfg = cfg,
};
struct timeutil_sync_instant si0 = {
/* Absolute local 0 is 5 s ref */
.ref = scale_ref(10, cfg),
.local = scale_local(5, cfg),
};
uint64_t ref = 0;
int rv = timeutil_sync_ref_from_local(&ss, 0, &ref);
zassert_equal(rv, -EINVAL,
"%s: unexpected uninit convert: %d", tag, rv);
rv = timeutil_sync_state_update(&ss, &si0);
zassert_equal(rv, 0,
"%s: unexpected init: %d", tag, rv);
zassert_equal(ss.skew, 1.0,
"%s: unexpected skew");
rv = timeutil_sync_ref_from_local(&ss, ss.base.local, NULL);
zassert_equal(rv, -EINVAL,
"%s: unexpected missing dest: %d", tag, rv);
rv = timeutil_sync_ref_from_local(&ss, ss.base.local, &ref);
zassert_equal(rv, 0,
"%s: unexpected fail", tag, rv);
zassert_equal(ref, ss.base.ref,
"%s: unexpected base convert", tag);
rv = timeutil_sync_ref_from_local(&ss, 0, &ref);
zassert_equal(rv, 0,
"%s: unexpected local=0 fail", tag, rv);
zassert_equal(ref, scale_ref(5, cfg),
"%s: unexpected local=0 ref", tag);
rv = timeutil_sync_ref_from_local(&ss, ss.base.local, &ref);
zassert_equal(rv, 0,
"%s: unexpected local=base fail", tag, rv);
zassert_equal(ref, ss.base.ref,
"%s: unexpected local=base ref", tag);
rv = timeutil_sync_ref_from_local(&ss, ss.base.local
+ scale_local(2, cfg), &ref);
zassert_equal(rv, 0,
"%s: unexpected local=base+2s fail %d", tag, rv);
zassert_equal(ref, ss.base.ref + scale_ref(2, cfg),
"%s: unexpected local=base+2s ref", tag);
rv = timeutil_sync_ref_from_local(&ss, (int64_t)ss.base.local
- scale_local(12, cfg), &ref);
zassert_equal(rv, -ERANGE,
"%s: unexpected local=base-12s res %u", tag, rv);
/* Skew of 0.5 means local runs at double speed */
rv = timeutil_sync_state_set_skew(&ss, 0.5, NULL);
zassert_equal(rv, 0,
"%s: failed set skew", tag);
/* Local at double speed corresponds to half advance in ref */
rv = timeutil_sync_ref_from_local(&ss, ss.base.local
+ scale_local(2, cfg), &ref);
zassert_equal(rv, 1,
"%s: unexpected skew adj fail", tag);
zassert_equal(ref, ss.base.ref + cfg->ref_Hz,
"%s: unexpected skew adj convert", tag);
}
static void test_ref_from_local(void)
{
tref_from_local("std", &cfg1);
tref_from_local("ext", &cfg2);
}
static void tlocal_from_ref(const char *tag,
const struct timeutil_sync_config *cfg)
{
struct timeutil_sync_state ss = {
.cfg = cfg,
};
struct timeutil_sync_instant si0 = {
/* Absolute local 0 is 5 s ref */
.ref = scale_ref(10, cfg),
.local = scale_local(5, cfg),
};
int64_t local = 0;
int rv = timeutil_sync_local_from_ref(&ss, 0, &local);
zassert_equal(rv, -EINVAL,
"%s: unexpected uninit convert: %d", tag, rv);
rv = timeutil_sync_state_update(&ss, &si0);
zassert_equal(rv, 0,
"%s: unexpected init: %d", tag, rv);
zassert_equal(ss.skew, 1.0,
"%s: unexpected skew", tag);
rv = timeutil_sync_local_from_ref(&ss, ss.base.ref, NULL);
zassert_equal(rv, -EINVAL,
"%s: unexpected missing dest", tag, rv);
rv = timeutil_sync_local_from_ref(&ss, ss.base.ref, &local);
zassert_equal(rv, 0,
"%s: unexpected fail", tag, rv);
zassert_equal(local, ss.base.local,
"%s: unexpected base convert", tag);
rv = timeutil_sync_local_from_ref(&ss, ss.base.ref
+ scale_ref(2, cfg), &local);
zassert_equal(rv, 0,
"%s: unexpected base+2s fail", tag);
zassert_equal(local, ss.base.local + scale_local(2, cfg),
"%s: unexpected base+2s convert", tag);
rv = timeutil_sync_local_from_ref(&ss, ss.base.ref
- scale_ref(7, cfg), &local);
zassert_equal(rv, 0,
"%s: unexpected base-7s fail", tag);
zassert_equal(local, scale_local_signed(-2, cfg),
"%s: unexpected base-7s convert", tag);
/* Skew of 0.5 means local runs at double speed */
rv = timeutil_sync_state_set_skew(&ss, 0.5, NULL);
zassert_equal(rv, 0,
"%s: failed set skew", tag);
/* Local at double speed corresponds to half advance in ref */
rv = timeutil_sync_local_from_ref(&ss, ss.base.ref
+ scale_ref(1, cfg) / 2, &local);
zassert_equal(rv, 1,
"%s: unexpected skew adj fail", tag);
zassert_equal(local, ss.base.local + scale_local(1, cfg),
"%s: unexpected skew adj convert", tag);
}
static void test_local_from_ref(void)
{
tlocal_from_ref("std", &cfg1);
tlocal_from_ref("ext", &cfg2);
}
static void test_skew_to_ppb(void)
{
float skew = 1.0;
int32_t ppb = timeutil_sync_skew_to_ppb(skew);
zassert_equal(ppb, 0,
"unexpected perfect: %d", ppb);
skew = 0.999976;
ppb = timeutil_sync_skew_to_ppb(skew);
zassert_equal(ppb, 24020,
"unexpected fast: %d", ppb);
skew = 1.000022;
ppb = timeutil_sync_skew_to_ppb(skew);
zassert_equal(ppb, -22053,
"unexpected slow: %d", ppb);
skew = 3.147483587;
ppb = timeutil_sync_skew_to_ppb(skew);
zassert_equal(ppb, -2147483587,
"unexpected near limit: %.10g %d", skew, ppb);
skew = 3.147483826;
ppb = timeutil_sync_skew_to_ppb(skew);
zassert_equal(ppb, INT32_MIN,
"unexpected above limit: %.10g %d", skew, ppb);
}
void test_sync(void)
{
test_state_update();
test_state_set_skew();
test_estimate_skew();
test_ref_from_local();
test_local_from_ref();
test_skew_to_ppb();
}

View file

@ -1,5 +1,5 @@
common:
tags: timeutils
tags: timeutil
type: unit
tests:

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Peter Bigot Consulting
* Copyright 2019-2020 Peter Bigot Consulting
*
* SPDX-License-Identifier: Apache-2.0
*/
@ -22,5 +22,6 @@ void timeutil_check(const struct timeutil_test_data *tp,
void test_gmtime(void);
void test_s32(void);
void test_s64(void);
void test_sync(void);
#endif /* TIMEUTIL_TEST_H */