Skip to content

Commit f6d7595

Browse files
pabigotnashif
authored andcommitted
lib/timeutil: add utilities to manage local/reference clock alignment
Provide data structures to capture a timestamp in two different clocks, monitor the drift between those clocks, and using a base instant with estimated drift convert between the clocks. This provides the core technology to convert between system uptime and an external continuous time scale like TAI (UTC without applying leap seconds). Signed-off-by: Peter A. Bigot <[email protected]>
1 parent 513b9f5 commit f6d7595

File tree

7 files changed

+742
-5
lines changed

7 files changed

+742
-5
lines changed

include/sys/timeutil.h

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,236 @@ int64_t timeutil_timegm64(const struct tm *tm);
5353
*/
5454
time_t timeutil_timegm(const struct tm *tm);
5555

56+
/**
57+
* @brief Immutable state for synchronizing two clocks.
58+
*
59+
* Values required to convert durations between two time scales.
60+
*
61+
* @note The accuracy of the translation and calculated skew between sources
62+
* depends on the resolution of these frequencies. A reference frequency with
63+
* microsecond or nanosecond resolution would produce the most accurate
64+
* tracking when the local reference is the Zephyr tick counter. A reference
65+
* source like an RTC chip with 1 Hz resolution requires a much larger
66+
* interval between sampled instants to detect relative clock drift.
67+
*/
68+
struct timeutil_sync_config {
69+
/** The nominal instance counter rate in Hz.
70+
*
71+
* This value is assumed to be precise, but may drift depending on
72+
* the reference clock source.
73+
*
74+
* The value must be positive.
75+
*/
76+
uint32_t ref_Hz;
77+
78+
/** The nominal local counter rate in Hz.
79+
*
80+
* This value is assumed to be inaccurate but reasonably stable. For
81+
* a local clock driven by a crystal oscillator an error of 25 ppm is
82+
* common; for an RC oscillator larger errors should be expected. The
83+
* timeutil_sync infrastructure can calculate the skew between the
84+
* local and reference clocks and apply it when converting between
85+
* time scales.
86+
*
87+
* The value must be positive.
88+
*/
89+
uint32_t local_Hz;
90+
};
91+
92+
/**
93+
* @brief Representation of an instant in two time scales.
94+
*
95+
* Capturing the same instant in two time scales provides a
96+
* registration point that can be used to convert between those time
97+
* scales.
98+
*/
99+
struct timeutil_sync_instant {
100+
/** An instant in the reference time scale.
101+
*
102+
* This must never be zero in an initialized timeutil_sync_instant
103+
* object.
104+
*/
105+
uint64_t ref;
106+
107+
/** The corresponding instance in the local time scale.
108+
*
109+
* This may be zero in a valid timeutil_sync_instant object.
110+
*/
111+
uint64_t local;
112+
};
113+
114+
/**
115+
* @brief State required to convert instants between time scales.
116+
*
117+
* This state in conjunction with functions that manipulate it capture
118+
* the offset information necessary to convert between two timescales
119+
* along with information that corrects for skew due to inaccuracies
120+
* in clock rates.
121+
*
122+
* State objects should be zero-initialized before use.
123+
*/
124+
struct timeutil_sync_state {
125+
/** Pointer to reference and local rate information. */
126+
const struct timeutil_sync_config *cfg;
127+
128+
/** The base instant in both time scales. */
129+
struct timeutil_sync_instant base;
130+
131+
/** The most recent instant in both time scales.
132+
*
133+
* This is captured here to provide data for skew calculation.
134+
*/
135+
struct timeutil_sync_instant latest;
136+
137+
/** The scale factor used to correct for clock skew.
138+
*
139+
* The nominal rate for the local counter is assumed to be
140+
* inaccurate but stable, i.e. it will generally be some
141+
* parts-per-million faster or slower than specified.
142+
*
143+
* A duration in observed local clock ticks must be multiplied by
144+
* this value to produce a duration in ticks of a clock operating at
145+
* the nominal local rate.
146+
*
147+
* A zero value indicates that the skew has not been initialized.
148+
* If the value is zero when #base is initialized the skew will be
149+
* set to 1. Otherwise the skew is assigned through
150+
* timeutil_sync_state_set_skew().
151+
*/
152+
float skew;
153+
};
154+
155+
/**
156+
* @brief Record a new instant in the time synchronization state.
157+
*
158+
* Note that this updates only the latest persisted instant. The skew
159+
* is not adjusted automatically.
160+
*
161+
* @param tsp pointer to a timeutil_sync_state object.
162+
*
163+
* @param inst the new instant to be recorded. This becomes the base
164+
* instant if there is no base instant, otherwise the value must be
165+
* strictly after the base instant in both the reference and local
166+
* time scales.
167+
*
168+
* @retval 0 if installation succeeded in providing a new base
169+
* @retval 1 if installation provided a new latest instant
170+
* @retval -EINVAL if the new instant is not compatible with the base instant
171+
*/
172+
int timeutil_sync_state_update(struct timeutil_sync_state *tsp,
173+
const struct timeutil_sync_instant *inst);
174+
175+
/**
176+
* @brief Update the state with a new skew and possibly base value.
177+
*
178+
* Set the skew from a value retrieved from persistent storage, or
179+
* calculated based on recent skew estimations including from
180+
* timeutil_sync_estimate_skew().
181+
*
182+
* Optionally update the base timestamp. If the base is replaced the
183+
* latest instant will be cleared until timeutil_sync_state_update() is
184+
* invoked.
185+
*
186+
* @param tsp pointer to a time synchronization state.
187+
*
188+
* @param skew the skew to be used. The value must be positive and
189+
* shouldn't be too far away from 1.
190+
*
191+
* @param base optional new base to be set. If provided this becomes
192+
* the base timestamp that will be used along with skew to convert
193+
* between reference and local timescale instants. Setting the base
194+
* clears the captured latest value.
195+
*
196+
* @return 0 if skew was updated
197+
* @return -EINVAL if skew was not valid
198+
*/
199+
int timeutil_sync_state_set_skew(struct timeutil_sync_state *tsp, float skew,
200+
const struct timeutil_sync_instant *base);
201+
202+
/**
203+
* @brief Estimate the skew based on current state.
204+
*
205+
* Using the base and latest syncpoints from the state determine the
206+
* skew of the local clock relative to the reference clock. See
207+
* timeutil_sync_state::skew.
208+
*
209+
* @param tsp pointer to a time synchronization state. The base and latest
210+
* syncpoints must be present and the latest syncpoint must be after
211+
* the base point in the local time scale.
212+
*
213+
* @return the estimated skew, or zero if skew could not be estimated.
214+
*/
215+
float timeutil_sync_estimate_skew(const struct timeutil_sync_state *tsp);
216+
217+
/**
218+
* @brief Interpolate a reference timescale instant from a local
219+
* instant.
220+
*
221+
* @param tsp pointer to a time synchronization state. This must have a base
222+
* and a skew installed.
223+
*
224+
* @param local an instant measured in the local timescale. This may
225+
* be before or after the base instant.
226+
*
227+
* @param refp where the corresponding instant in the reference
228+
* timescale should be stored. A negative interpolated reference time
229+
* produces an error. If interpolation fails the referenced object is
230+
* not modified.
231+
*
232+
* @retval 0 if interpolated using a skew of 1
233+
* @retval 1 if interpolated using a skew not equal to 1
234+
* @retval -EINVAL
235+
* * the times synchronization state is not adequately initialized
236+
* * @p refp is null
237+
* @retval -ERANGE the interpolated reference time would be negative
238+
*/
239+
int timeutil_sync_ref_from_local(const struct timeutil_sync_state *tsp,
240+
uint64_t local, uint64_t *refp);
241+
242+
/**
243+
* @brief Interpolate a local timescale instant from a reference
244+
* instant.
245+
*
246+
* @param tsp pointer to a time synchronization state. This must have a base
247+
* and a skew installed.
248+
*
249+
* @param ref an instant measured in the reference timescale. This
250+
* may be before or after the base instant.
251+
*
252+
* @param localp where the corresponding instant in the local
253+
* timescale should be stored. An interpolated value before local
254+
* time 0 is provided without error. If interpolation fails the
255+
* referenced object is not modified.
256+
*
257+
* @retval 0 if successful with a skew of 1
258+
* @retval 1 if successful with a skew not equal to 1
259+
* @retval -EINVAL
260+
* * the time synchronization state is not adequately initialized
261+
* * @p refp is null
262+
*/
263+
int timeutil_sync_local_from_ref(const struct timeutil_sync_state *tsp,
264+
uint64_t ref, int64_t *localp);
265+
266+
/**
267+
* @brief Convert from a skew to an error in parts-per-billion.
268+
*
269+
* A skew of 1.0 has zero error. A skew less than 1 has a positive
270+
* error (clock is faster than it should be). A skew greater than one
271+
* has a negative error (clock is slower than it should be).
272+
*
273+
* Note that due to the limited precision of @c float compared with @c
274+
* double the smallest error that can be represented is about 120 ppb.
275+
* A "precise" time source may have error on the order of 2000 ppb.
276+
*
277+
* A skew greater than 3.14748 may underflow the 32-bit
278+
* representation; this represents a clock running at less than 1/3
279+
* its nominal rate.
280+
*
281+
* @return skew error represented as parts-per-billion, or INT32_MIN
282+
* if the skew cannot be represented in the return type.
283+
*/
284+
int32_t timeutil_sync_skew_to_ppb(float skew);
285+
56286
#ifdef __cplusplus
57287
}
58288
#endif

lib/os/timeutil.c

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
#include <zephyr/types.h>
1414
#include <errno.h>
15+
#include <stddef.h>
1516
#include <sys/timeutil.h>
1617

1718
/** Convert a civil (proleptic Gregorian) date to days relative to
@@ -66,5 +67,112 @@ time_t timeutil_timegm(const struct tm *tm)
6667
errno = ERANGE;
6768
rv = -1;
6869
}
70+
71+
return rv;
72+
}
73+
74+
int timeutil_sync_state_update(struct timeutil_sync_state *tsp,
75+
const struct timeutil_sync_instant *inst)
76+
{
77+
int rv = -EINVAL;
78+
79+
if (((tsp->base.ref == 0) && (inst->ref > 0))
80+
|| ((inst->ref > tsp->base.ref)
81+
&& (inst->local > tsp->base.local))) {
82+
if (tsp->base.ref == 0) {
83+
tsp->base = *inst;
84+
tsp->latest = (struct timeutil_sync_instant){};
85+
tsp->skew = 1.0;
86+
rv = 0;
87+
} else {
88+
tsp->latest = *inst;
89+
rv = 1;
90+
}
91+
}
92+
93+
return rv;
94+
}
95+
96+
int timeutil_sync_state_set_skew(struct timeutil_sync_state *tsp, float skew,
97+
const struct timeutil_sync_instant *base)
98+
{
99+
int rv = -EINVAL;
100+
101+
if (skew > 0) {
102+
tsp->skew = skew;
103+
if (base != NULL) {
104+
tsp->base = *base;
105+
tsp->latest = (struct timeutil_sync_instant){};
106+
}
107+
rv = 0;
108+
}
109+
69110
return rv;
70111
}
112+
113+
float timeutil_sync_estimate_skew(const struct timeutil_sync_state *tsp)
114+
{
115+
float rv = 0;
116+
117+
if ((tsp->base.ref != 0) && (tsp->latest.ref != 0)
118+
&& (tsp->latest.local > tsp->base.local)) {
119+
const struct timeutil_sync_config *cfg = tsp->cfg;
120+
double ref_delta = tsp->latest.ref - tsp->base.ref;
121+
double local_delta = tsp->latest.local - tsp->base.local;
122+
123+
rv = ref_delta * cfg->local_Hz / local_delta / cfg->ref_Hz;
124+
}
125+
126+
return rv;
127+
}
128+
129+
int timeutil_sync_ref_from_local(const struct timeutil_sync_state *tsp,
130+
uint64_t local, uint64_t *refp)
131+
{
132+
int rv = -EINVAL;
133+
134+
if ((tsp->skew > 0) && (tsp->base.ref > 0) && (refp != NULL)) {
135+
const struct timeutil_sync_config *cfg = tsp->cfg;
136+
int64_t local_delta = local - tsp->base.local;
137+
int64_t ref_delta = (int64_t)(tsp->skew * local_delta) *
138+
cfg->ref_Hz / cfg->local_Hz;
139+
int64_t ref_abs = (int64_t)tsp->base.ref + ref_delta;
140+
141+
if (ref_abs < 0) {
142+
rv = -ERANGE;
143+
} else {
144+
*refp = ref_abs;
145+
rv = (int)(tsp->skew != 1.0);
146+
}
147+
}
148+
149+
return rv;
150+
}
151+
152+
int timeutil_sync_local_from_ref(const struct timeutil_sync_state *tsp,
153+
uint64_t ref, int64_t *localp)
154+
{
155+
int rv = -EINVAL;
156+
157+
if ((tsp->skew > 0) && (tsp->base.ref > 0) && (localp != NULL)) {
158+
const struct timeutil_sync_config *cfg = tsp->cfg;
159+
int64_t ref_delta = (int64_t)(ref - tsp->base.ref);
160+
double local_delta = (ref_delta * cfg->local_Hz) / cfg->ref_Hz
161+
/ tsp->skew;
162+
int64_t local_abs = (int64_t)tsp->base.local
163+
+ (int64_t)local_delta;
164+
165+
*localp = local_abs;
166+
rv = (int)(tsp->skew != 1.0);
167+
}
168+
169+
return rv;
170+
}
171+
172+
int32_t timeutil_sync_skew_to_ppb(float skew)
173+
{
174+
int64_t ppb64 = (int64_t)((1.0 - skew) * 1E9);
175+
int32_t ppb32 = (int32_t)ppb64;
176+
177+
return (ppb64 == ppb32) ? ppb32 : INT32_MIN;
178+
}

tests/unit/timeutil/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
# Copyright 2019-2020 Peter Bigot Consulting
12
# SPDX-License-Identifier: Apache-2.0
23

34
project(timeutil)
4-
set(SOURCES main.c test_gmtime.c test_s32.c test_s64.c)
5+
set(SOURCES main.c test_gmtime.c test_s32.c test_s64.c test_sync.c)
56
find_package(ZephyrUnittest REQUIRED HINTS $ENV{ZEPHYR_BASE})

tests/unit/timeutil/main.c

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2019 Peter Bigot Consulting
2+
* Copyright 2019-2020 Peter Bigot Consulting
33
*
44
* SPDX-License-Identifier: Apache-2.0
55
*/
@@ -58,7 +58,8 @@ void test_main(void)
5858
ztest_test_suite(test_timeutil_api,
5959
ztest_unit_test(test_gmtime),
6060
ztest_unit_test(test_s32),
61-
ztest_unit_test(test_s64)
61+
ztest_unit_test(test_s64),
62+
ztest_unit_test(test_sync)
6263
);
6364
ztest_run_test_suite(test_timeutil_api);
6465
}

0 commit comments

Comments
 (0)