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