diff --git a/doc/reference/index.rst b/doc/reference/index.rst index 29bb60fc9738..bd8bf98b5f85 100644 --- a/doc/reference/index.rst +++ b/doc/reference/index.rst @@ -29,6 +29,7 @@ API Reference resource_management/index.rst shell/index.rst storage/index.rst + misc/timeutil.rst usb/index.rst usermode/index.rst util/index.rst diff --git a/doc/reference/kernel/timing/clocks.rst b/doc/reference/kernel/timing/clocks.rst index f5ec87938fbf..8a28d02f4d01 100644 --- a/doc/reference/kernel/timing/clocks.rst +++ b/doc/reference/kernel/timing/clocks.rst @@ -66,6 +66,8 @@ multiples of each other and where the output fits within a single word, these conversions expand to a 2-4 operation sequence, requiring full precision only where actually required and requested. +.. _kernel_timing_uptime: + Uptime ====== diff --git a/doc/reference/misc/timeutil.rst b/doc/reference/misc/timeutil.rst new file mode 100644 index 000000000000..4ed3c7178b98 --- /dev/null +++ b/doc/reference/misc/timeutil.rst @@ -0,0 +1,240 @@ +.. _timeutil_api: + +Time Utilities +############## + +Overview +******** + +:ref:`kernel_timing_uptime` in Zephyr is based on the a tick counter. With +the default :option:`CONFIG_TICKLESS_KERNEL` this counter advances at a +nominally constant rate from zero at the instant the system started. The POSIX +equivalent to this counter is something like ``CLOCK_MONOTONIC`` or, in Linux, +``CLOCK_MONOTONIC_RAW``. :c:func:`k_uptime_get()` provides a millisecond +representation of this time. + +Applications often need to correlate the Zephyr internal time with external +time scales used in daily life, such as local time or Coordinated Universal +Time. These systems interpret time in different ways and may have +discontinuities due to `leap seconds `__ and +local time offsets like daylight saving time. + +Because of these discontinuities, as well as significant inaccuracies in the +clocks underlying the cycle counter, the offset between time estimated from +the Zephyr clock and the actual time in a "real" civil time scale is not +constant and can vary widely over the runtime of a Zephyr application. + +The time utilities API supports: + +* :ref:`converting between time representations ` +* :ref:`synchronizing and aligning time scales ` + +For terminology and concepts that support these functions see +:ref:`timeutil_concepts`. + +Time Utility APIs +***************** + +.. _timeutil_repr: + +Representation Transformation +============================= + +Time scale instants can be represented in multiple ways including: + +* Seconds since an epoch. POSIX representations of time in this form include + ``time_t`` and ``struct timespec``, which are generally interpreted as a + representation of `"UNIX Time" + `__. + +* Calendar time as a year, month, day, hour, minutes, and seconds relative to + an epoch. POSIX representations of time in this form include ``struct tm``. + +Keep in mind that these are simply time representations that must be +interpreted relative to a time scale which may be local time, UTC, or some +other continuous or discontinuous scale. + +Some necessary transformations are available in standard C library +routines. For example, ``time_t`` measuring seconds since the POSIX EPOCH is +converted to ``struct tm`` representing calendar time with `gmtime() +`__. +Sub-second timestamps like ``struct timespec`` can also use this to produce +the calendar time representation and deal with sub-second offsets separately. + +The inverse transformation is not standardized: APIs like ``mktime()`` expect +information about time zones. Zephyr provides this transformation with +:c:func:`timeutil_timegm` and :c:func:`timeutil_timegm64`. + +.. doxygengroup:: timeutil_repr_apis + :project: Zephyr + +.. _timeutil_sync: + +Time Scale Synchronization +========================== + +There are several factors that affect synchronizing time scales: + +* The rate of discrete instant representation change. For example Zephyr + uptime is tracked in ticks which advance at events that nominally occur at + :option:`CONFIG_SYS_CLOCK_TICKS_PER_SEC` Hertz, while an external time + source may provide data in whole or fractional seconds (e.g. microseconds). +* The absolute offset required to align the two scales at a single instant. +* The relative error between observable instants in each scale, required to + align multiple instants consistently. For example a reference clock that's + conditioned by a 1-pulse-per-second GPS signal will be much more accurate + than a Zephyr system clock driven by a RC oscillator with a +/- 250 ppm + error. + +Synchronization or alignment between time scales is done with a multi-step +process: + +* An instant in a time scale is represented by an (unsigned) 64-bit integer, + assumed to advance at a fixed nominal rate. +* :c:struct:`timeutil_sync_config` records the nominal rates of a reference + time scale/source (e.g. TAI) and a local time source + (e.g. :c:func:`k_uptime_ticks`). +* :c:struct:`timeutil_sync_instant` records the representation of a single + instant in both the reference and local time scales. +* :c:struct:`timeutil_sync_state` provides storage for an initial instant, a + recently received second observation, and a skew that can adjust for + relative errors in the actual rate of each time scale. +* :c:func:`timeutil_sync_ref_from_local()` and + :c:func:`timeutil_sync_local_from_ref()` convert instants in one time scale + to another taking into account skew that can be estimated from the two + instances stored in the state structure by + :c:func:`timeutil_sync_estimate_skew`. + +.. doxygengroup:: timeutil_sync_apis + :project: Zephyr + +.. _timeutil_concepts: + +Concepts Underlying Time Support in Zephyr +****************************************** + +Terms from `ISO/TC 154/WG 5 N0038 +`__ +(ISO/WD 8601-1) and elsewhere: + +* A *time axis* is a representation of time as an ordered sequence of + instants. +* A *time scale* is a way of representing an instant relative to an origin + that serves as the epoch. +* A time scale is *monotonic* (increasing) if the representation of successive + time instants never decreases in value. +* A time scale is *continuous* if the representation has no abrupt changes in + value, e.g. jumping forward or back when going between successive instants. +* `Civil time `__ generally refers + to time scales that legally defined by civil authorities, like local + governments, often to align local midnight to solar time. + +Relevant Time Scales +==================== + +`International Atomic Time +`__ (TAI) is a time +scale based on averaging clocks that count in SI seconds. TAI is a montonic +and continuous time scale. + +`Universal Time `__ (UT) is a +time scale based on Earth’s rotation. UT is a discontinuous time scale as it +requires occasional adjustments (`leap seconds +`__) to maintain alignment to +changes in Earth’s rotation. Thus the difference between TAI and UT varies +over time. There are several variants of UT, with `UTC +`__ being the most +common. + +UT times are independent of location. UT is the basis for Standard Time +(or "local time") which is the time at a particular location. Standard +time has a fixed offset from UT at any given instant, primarily +influenced by longitude, but the offset may be adjusted ("daylight +saving time") to align standard time to the local solar time. In a sense +local time is "more discontinuous" than UT. + +`POSIX Time `__ is a time scale +that counts seconds since the "POSIX epoch" at 1970-01-01T00:00:00Z (i.e. the +start of 1970 UTC). `UNIX Time +`__ is an extension of POSIX +time using negative values to represent times before the POSIX epoch. Both of +these scales assume that every day has exactly 86400 seconds. In normal use +instants in these scales correspond to times in the UTC scale, so they inherit +the discontinuity. + +The continuous analogue is `UNIX Leap Time +`__ which is UNIX time plus all +leap-second corrections added after the POSIX epoch (when TAI-UTC was 8 s). + +Example of Time Scale Differences +--------------------------------- + +A positive leap second was introduced at the end of 2016, increasing the +difference between TAI and UTC from 36 seconds to 37 seconds. There was +no leap second introduced at the end of 1999, when the difference +between TAI and UTC was only 32 seconds. The following table shows +relevant civil and epoch times in several scales: + +==================== ========== =================== ======= ============== +UTC Date UNIX time TAI Date TAI-UTC UNIX Leap Time +==================== ========== =================== ======= ============== +1970-01-01T00:00:00Z 0 1970-01-01T00:00:08 +8 0 +1999-12-31T23:59:28Z 946684768 2000-01-01T00:00:00 +32 946684792 +1999-12-31T23:59:59Z 946684799 2000-01-01T00:00:31 +32 946684823 +2000-01-01T00:00:00Z 946684800 2000-01-01T00:00:32 +32 946684824 +2016-12-31T23:59:59Z 1483228799 2017-01-01T00:00:35 +36 1483228827 +2016-12-31T23:59:60Z undefined 2017-01-01T00:00:36 +36 1483228828 +2017-01-01T00:00:00Z 1483228800 2017-01-01T00:00:37 +37 1483228829 +==================== ========== =================== ======= ============== + +Functional Requirements +----------------------- + +The Zephyr tick counter has no concept of leap seconds or standard time +offsets and is a continuous time scale. However it can be relatively +inaccurate, with drifts as much as three minutes per hour (assuming an RC +timer with 5% tolerance). + +There are two stages required to support conversion between Zephyr time and +common human time scales: + +* Translation between the continuous but inaccurate Zephyr time scale and an + accurate external stable time scale; +* Translation between the stable time scale and the (possibly discontinuous) + civil time scale. + +The API around :c:func:`timeutil_sync_state_update()` supports the first step +of converting between continuous time scales. + +The second step requires external information including schedules of leap +seconds and local time offset changes. This may be best provided by an +external library, and is not currently part of the time utility APIs. + +Selecting an External Source and Time Scale +------------------------------------------- + +If an application requires civil time accuracy within several seconds then UTC +could be used as the stable time source. However, if the external source +adjusts to a leap second there will be a discontinuity: the elapsed time +between two observations taken at 1 Hz is not equal to the numeric difference +between their timestamps. + +For precise activities a continuous scale that is independent of local and +solar adjustments simplifies things considerably. Suitable continuous scales +include: + +- GPS time: epoch of 1980-01-06T00:00:00Z, continuous following TAI with an + offset of TAI-GPS=19 s. +- Bluetooth mesh time: epoch of 2000-01-01T00:00:00Z, continuous following TAI + with an offset of -32. +- UNIX Leap Time: epoch of 1970-01-01T00:00:00Z, continuous following TAI with + an offset of -8. + +Because C and Zephyr library functions support conversion between integral and +calendar time representations using the UNIX epoch, UNIX Leap Time is an ideal +choice for the external time scale. + +The mechanism used to populate synchronization points is not relevant: it may +involve reading from a local high-precision RTC peripheral, exchanging packets +over a network using a protocol like NTP or PTP, or processing NMEA messages +received a GPS with or without a 1pps signal. diff --git a/include/sys/timeutil.h b/include/sys/timeutil.h index ab7a422771c9..f6e1f96c86b8 100644 --- a/include/sys/timeutil.h +++ b/include/sys/timeutil.h @@ -29,6 +29,13 @@ extern "C" { #endif +/** + * @defgroup timeutil_apis Time Utility APIs + * @defgroup timeutil_repr_apis Time Representation APIs + * @ingroup timeutil_apis + * @{ + */ + /** * @brief Convert broken-down time to a POSIX epoch offset in seconds. * @@ -53,8 +60,249 @@ int64_t timeutil_timegm64(const struct tm *tm); */ time_t timeutil_timegm(const struct tm *tm); +/** + * @} + * @defgroup timeutil_sync_apis Time Synchronization APIs + * @ingroup timeutil_apis + * @{ + */ + +/** + * @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 +/** + * @} + */ + #endif /* ZEPHYR_INCLUDE_SYS_TIMEUTIL_H_ */ diff --git a/lib/os/timeutil.c b/lib/os/timeutil.c index ab9feadd446e..948f98bc37cb 100644 --- a/lib/os/timeutil.c +++ b/lib/os/timeutil.c @@ -12,6 +12,7 @@ #include #include +#include #include /** 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; +} diff --git a/samples/boards/nrf/clock_skew/CMakeLists.txt b/samples/boards/nrf/clock_skew/CMakeLists.txt new file mode 100644 index 000000000000..535cb5bd0ece --- /dev/null +++ b/samples/boards/nrf/clock_skew/CMakeLists.txt @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.13.1) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(nrf_clock_skew) + +FILE(GLOB app_sources src/*.c) +target_sources(app PRIVATE ${app_sources}) diff --git a/samples/boards/nrf/clock_skew/Kconfig b/samples/boards/nrf/clock_skew/Kconfig new file mode 100644 index 000000000000..cf029898acf6 --- /dev/null +++ b/samples/boards/nrf/clock_skew/Kconfig @@ -0,0 +1,10 @@ +# Copyright (c) 2020, Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +config APP_ENABLE_HFXO + bool "Use HFXO as HFCLK source" + help + Turn on the high-frequency clock, which means switching to the + crystal as a source rather than the internal oscillator. + +source "Kconfig.zephyr" diff --git a/samples/boards/nrf/clock_skew/README.rst b/samples/boards/nrf/clock_skew/README.rst new file mode 100644 index 000000000000..393ff2a09f72 --- /dev/null +++ b/samples/boards/nrf/clock_skew/README.rst @@ -0,0 +1,116 @@ +.. _nrf-clock-skew-sample: + +nRF5x Clock Skew Demo +##################### + +Overview +******** + +This sample uses the API for correlating time sources to measure the +skew between HFCLK (used for the CPU) and LFCLK (used for system time). + +The ``CONFIG_APP_ENABLE_HFXO`` Kconfig option can be select to configure +the high frequency clock to use a crystal oscillator rather than the +default RC oscillator. (Capabilities like Bluetooth that require an +accurate high-frequency clock generally enable this source +automatically.) The relative error is significantly lower when HFXO is +enabled. + +Requirements +************ + +This application uses any nRF51 DK or nRF52 DK board for the demo. + +Building, Flashing and Running +****************************** + +.. zephyr-app-commands:: + :zephyr-app: samples/boards/nrf/clock_skew + :board: nrf52dk_nrf52840 + :goals: build flash + :compact: + +Running: + + +Sample Output +============= + +.. code-block:: console + + *** Booting Zephyr OS build zephyr-v2.4.0-693-g4a3275faf567 *** + Power-up clocks: LFCLK[ON]: Running LFXO ; HFCLK[OFF]: Running HFINT + Start TIMER_0: 0 + Timer-running clocks: LFCLK[ON]: Running LFXO ; HFCLK[OFF]: Running HFINT + Checking TIMER_0 at 16000000 Hz against ticks at 32768 Hz + Timer wraps every 268 s + + Ty Latest Base Span Err + HF 00:00:00.015667 + LF 00:00:00.404296 + Started sync: 0 + + Ty Latest Base Span Err + HF 00:00:10.001151 00:00:00.015667 00:00:09.985483 + LF 00:00:10.413818 00:00:00.404296 00:00:10.009521 00:00:00.024038 + Skew 0.997599 ; err 2401411 ppb + + Ty Latest Base Span Err + HF 00:00:19.997456 00:00:00.015667 00:00:19.981788 + LF 00:00:20.434265 00:00:00.404296 00:00:20.029968 00:00:00.048180 + Skew 0.997595 ; err 2405464 ppb + + Ty Latest Base Span Err + HF 00:00:29.993845 00:00:00.015667 00:00:29.978178 + LF 00:00:30.454650 00:00:00.404296 00:00:30.050354 00:00:00.072176 + Skew 0.997598 ; err 2401828 ppb + + Ty Latest Base Span Err + HF 00:00:39.986181 00:00:00.015667 00:00:39.970514 + LF 00:00:40.475036 00:00:00.404296 00:00:40.070739 00:00:00.100225 + Skew 0.997499 ; err 2501189 ppb + + Ty Latest Base Span Err + HF 00:00:49.981516 00:00:00.015667 00:00:49.965848 + LF 00:00:50.495422 00:00:00.404296 00:00:50.091125 00:00:00.125277 + Skew 0.997499 ; err 2501010 ppb + + Ty Latest Base Span Err + HF 00:00:59.976042 00:00:00.015667 00:00:59.960375 + LF 00:01:00.515808 00:00:00.404296 00:01:00.111511 00:00:00.151136 + Skew 0.997486 ; err 2514243 ppb + ... + Ty Latest Base Span Err + HF 00:01:59.935661 00:00:00.015667 00:01:59.919994 + LF 00:02:00.638153 00:00:00.404296 00:02:00.233856 00:00:00.313862 + Skew 0.997390 ; err 2610445 ppb + ... + Ty Latest Base Span Err + HF 00:04:59.769166 00:00:00.015667 00:04:59.753498 + LF 00:05:01.005279 00:00:00.404296 00:05:00.600982 00:00:00.847484 + Skew 0.997181 ; err 2819240 ppb + ... + Ty Latest Base Span Err + HF 00:09:59.513787 00:00:00.015667 00:09:59.498119 + LF 00:10:01.617156 00:00:00.404296 00:10:01.212860 00:00:01.714741 + Skew 0.997148 ; err 2852201 ppb + ... + Ty Latest Base Span Err + HF 00:30:08.384536 00:00:00.015667 00:30:08.368868 + LF 00:30:14.084594 00:00:00.404296 00:30:13.680297 00:00:05.311429 + Skew 0.997072 ; err 2928495 ppb + ... + Ty Latest Base Span Err + HF 00:59:57.353602 00:00:00.015667 00:59:57.337934 + LF 01:00:07.734375 00:00:00.404296 01:00:07.330078 00:00:09.992144 + Skew 0.997230 ; err 2770006 ppb + ... + Ty Latest Base Span Err + HF 02:59:33.181323 00:00:00.015667 02:59:33.165656 + LF 03:00:03.434265 00:00:00.404296 03:00:03.029968 00:00:29.864312 + Skew 0.997236 ; err 2764463 ppb + ... + Ty Latest Base Span Err + HF 05:59:55.031709 00:00:00.015667 05:59:55.016042 + LF 06:00:57.120941 00:00:00.404296 06:00:56.716644 00:01:01.700602 + Skew 0.997151 ; err 2849042 ppb diff --git a/samples/boards/nrf/clock_skew/prj.conf b/samples/boards/nrf/clock_skew/prj.conf new file mode 100644 index 000000000000..db7c52c95bc8 --- /dev/null +++ b/samples/boards/nrf/clock_skew/prj.conf @@ -0,0 +1,5 @@ +#CONFIG_CLOCK_CONTROL=y +CONFIG_COUNTER=y +CONFIG_COUNTER_TIMER0=y +CONFIG_NEWLIB_LIBC=y +CONFIG_NEWLIB_LIBC_FLOAT_PRINTF=y diff --git a/samples/boards/nrf/clock_skew/sample.yaml b/samples/boards/nrf/clock_skew/sample.yaml new file mode 100644 index 000000000000..15b340d52c3d --- /dev/null +++ b/samples/boards/nrf/clock_skew/sample.yaml @@ -0,0 +1,7 @@ +sample: + name: Clock Skew Sample for nRF +tests: + sample.boards.nrf.clock_skew: + build_only: true + platform_allow: nrf52840dk_nrf52840 nrf52dk_nrf52832 nrf51dk_nrf51422 + tags: power diff --git a/samples/boards/nrf/clock_skew/src/main.c b/samples/boards/nrf/clock_skew/src/main.c new file mode 100644 index 000000000000..ac8cc89895b6 --- /dev/null +++ b/samples/boards/nrf/clock_skew/src/main.c @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2020 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include + +#define TIMER_NODE DT_NODELABEL(timer0) +#define CLOCK_NODE DT_INST(0, nordic_nrf_clock) +#define UPDATE_INTERVAL_S 10 + +static const struct device *clock0; +static const struct device *timer0; +static struct timeutil_sync_config sync_config; +static uint64_t counter_ref; +static struct timeutil_sync_state sync_state; +static struct k_delayed_work sync_work; + +/* Convert local time in ticks to microseconds. */ +uint64_t local_to_us(uint64_t local) +{ + return z_tmcvt(local, sync_config.local_Hz, USEC_PER_SEC, false, + false, false, false); +} + +/* Convert HFCLK reference to microseconds. */ +uint64_t ref_to_us(uint64_t ref) +{ + return z_tmcvt(ref, sync_config.ref_Hz, USEC_PER_SEC, false, + false, false, false); +} + +/* Format a microsecond timestamp to text as D d HH:MM:SS.SSSSSS. */ +static const char *us_to_text_r(uint64_t rem, char *buf, size_t len) +{ + char *bp = buf; + char *bpe = bp + len; + uint32_t us; + uint32_t s; + uint32_t min; + uint32_t hr; + uint32_t d; + + us = rem % USEC_PER_SEC; + rem /= USEC_PER_SEC; + s = rem % 60; + rem /= 60; + min = rem % 60; + rem /= 60; + hr = rem % 24; + rem /= 24; + d = rem; + + if (d > 0) { + bp += snprintf(bp, bpe - bp, "%u d ", d); + } + bp += snprintf(bp, bpe - bp, "%02u:%02u:%02u.%06u", + hr, min, s, us); + return buf; +} + +static const char *us_to_text(uint64_t rem) +{ + static char ts_buf[32]; + + return us_to_text_r(rem, ts_buf, sizeof(ts_buf)); +} + +/* Show status of various clocks */ +static void show_clocks(const char *tag) +{ + static const char *const lfsrc_s[] = { +#if defined(CLOCK_LFCLKSRC_SRC_LFULP) + [NRF_CLOCK_LFCLK_LFULP] = "LFULP", +#endif + [NRF_CLOCK_LFCLK_RC] = "LFRC", + [NRF_CLOCK_LFCLK_Xtal] = "LFXO", + [NRF_CLOCK_LFCLK_Synth] = "LFSYNT", + }; + static const char *const hfsrc_s[] = { + [NRF_CLOCK_HFCLK_LOW_ACCURACY] = "HFINT", + [NRF_CLOCK_HFCLK_HIGH_ACCURACY] = "HFXO", + }; + static const char *const clkstat_s[] = { + [CLOCK_CONTROL_STATUS_STARTING] = "STARTING", + [CLOCK_CONTROL_STATUS_OFF] = "OFF", + [CLOCK_CONTROL_STATUS_ON] = "ON", + [CLOCK_CONTROL_STATUS_UNKNOWN] = "UNKNOWN", + }; + union { + unsigned int raw; + nrf_clock_lfclk_t lf; + nrf_clock_hfclk_t hf; + } src; + enum clock_control_status clkstat; + bool running; + + clkstat = clock_control_get_status(clock0, CLOCK_CONTROL_NRF_SUBSYS_LF); + running = nrf_clock_is_running(NRF_CLOCK, NRF_CLOCK_DOMAIN_LFCLK, + &src.lf); + printk("%s: LFCLK[%s]: %s %s ; ", tag, clkstat_s[clkstat], + running ? "Running" : "Off", lfsrc_s[src.lf]); + clkstat = clock_control_get_status(clock0, CLOCK_CONTROL_NRF_SUBSYS_HF); + running = nrf_clock_is_running(NRF_CLOCK, NRF_CLOCK_DOMAIN_HFCLK, + &src.hf); + printk("HFCLK[%s]: %s %s\n", clkstat_s[clkstat], + running ? "Running" : "Off", hfsrc_s[src.hf]); +} + +static void sync_work_handler(struct k_work *work) +{ + uint32_t ctr; + int rc = counter_get_value(timer0, &ctr); + const struct timeutil_sync_instant *base = &sync_state.base; + const struct timeutil_sync_instant *latest = &sync_state.latest; + + if (rc == 0) { + struct timeutil_sync_instant inst; + uint64_t ref_span_us; + + counter_ref += ctr - (uint32_t)counter_ref; + inst.ref = counter_ref; + inst.local = k_uptime_ticks(); + + rc = timeutil_sync_state_update(&sync_state, &inst); + printf("\nTy Latest Base Span Err\n"); + printf("HF %s", us_to_text(ref_to_us(inst.ref))); + if (rc > 0) { + printf(" %s", us_to_text(ref_to_us(base->ref))); + ref_span_us = ref_to_us(latest->ref - base->ref); + printf(" %s", us_to_text(ref_span_us)); + } + printf("\nLF %s", us_to_text(local_to_us(inst.local))); + if (rc > 0) { + uint64_t err_us; + uint64_t local_span_us; + char err_sign = ' '; + + printf(" %s", us_to_text(local_to_us(base->local))); + + local_span_us = local_to_us(latest->local - base->local); + printf(" %s", us_to_text(local_span_us)); + + if (ref_span_us >= local_span_us) { + err_us = ref_span_us - local_span_us; + err_sign = '-'; + } else { + err_us = local_span_us - ref_span_us; + } + printf(" %c%s", err_sign, us_to_text(err_us)); + } + printf("\n"); + if (rc > 0) { + float skew = timeutil_sync_estimate_skew(&sync_state); + + printf("Skew %f ; err %d ppb\n", skew, + timeutil_sync_skew_to_ppb(skew)); + } else if (rc < 0) { + printf("Sync update error: %d\n", rc); + } + } + k_delayed_work_submit(&sync_work, K_SECONDS(UPDATE_INTERVAL_S)); +} + +void main(void) +{ + const char *clock_label = DT_LABEL(CLOCK_NODE); + const char *timer0_label = DT_LABEL(TIMER_NODE); + uint32_t top; + int rc; + + /* Grab the clock driver */ + clock0 = device_get_binding(clock_label); + if (clock0 == NULL) { + printk("Failed to fetch clock %s\n", clock_label); + } + + show_clocks("Power-up clocks"); + + if (IS_ENABLED(CONFIG_APP_ENABLE_HFXO)) { + rc = clock_control_on(clock0, CLOCK_CONTROL_NRF_SUBSYS_HF); + printk("Enable HFXO got %d\n", rc); + } + + /* Grab the timer. */ + timer0 = device_get_binding(timer0_label); + if (timer0 == NULL) { + printk("Failed to fetch timer0 %s\n", timer0_label); + return; + } + + /* Apparently there's no API to configure a frequency at + * runtime, so live with whatever we get. + */ + sync_config.ref_Hz = counter_get_frequency(timer0); + if (sync_config.ref_Hz == 0) { + printk("Timer %s has no fixed frequency\n", + timer0_label); + return; + } + + top = counter_get_top_value(timer0); + if (top != UINT32_MAX) { + printk("Timer %s wraps at %u (0x%08x) not at 32 bits\n", + timer0_label, top, top); + return; + } + + rc = counter_start(timer0); + printk("Start %s: %d\n", timer0_label, rc); + + show_clocks("Timer-running clocks"); + + sync_config.local_Hz = CONFIG_SYS_CLOCK_TICKS_PER_SEC; + + sync_state.cfg = &sync_config; + + printf("Checking %s at %u Hz against ticks at %u Hz\n", + timer0_label, sync_config.ref_Hz, sync_config.local_Hz); + printf("Timer wraps every %u s\n", + (uint32_t)(BIT64(32) / sync_config.ref_Hz)); + + k_delayed_work_init(&sync_work, sync_work_handler); + rc = k_delayed_work_submit(&sync_work, K_NO_WAIT); + + printk("Started sync: %d\n", rc); +} diff --git a/tests/unit/timeutil/CMakeLists.txt b/tests/unit/timeutil/CMakeLists.txt index 29af778ecc3f..5a60184a0c34 100644 --- a/tests/unit/timeutil/CMakeLists.txt +++ b/tests/unit/timeutil/CMakeLists.txt @@ -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}) diff --git a/tests/unit/timeutil/main.c b/tests/unit/timeutil/main.c index 832701da9a78..95b4944c250e 100644 --- a/tests/unit/timeutil/main.c +++ b/tests/unit/timeutil/main.c @@ -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); } diff --git a/tests/unit/timeutil/test_sync.c b/tests/unit/timeutil/test_sync.c new file mode 100644 index 000000000000..3bbdaff1fd41 --- /dev/null +++ b/tests/unit/timeutil/test_sync.c @@ -0,0 +1,396 @@ +/* + * Copyright 2020 Peter Bigot Consulting + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* Tests for the time_sync data structures */ + +#include +#include +#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(); +} diff --git a/tests/unit/timeutil/testcase.yaml b/tests/unit/timeutil/testcase.yaml index 3d7b05fdba8b..eac190d82adf 100644 --- a/tests/unit/timeutil/testcase.yaml +++ b/tests/unit/timeutil/testcase.yaml @@ -1,5 +1,5 @@ common: - tags: timeutils + tags: timeutil type: unit tests: diff --git a/tests/unit/timeutil/timeutil_test.h b/tests/unit/timeutil/timeutil_test.h index 97559d77370f..dc74953e4100 100644 --- a/tests/unit/timeutil/timeutil_test.h +++ b/tests/unit/timeutil/timeutil_test.h @@ -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 */