Skip to content

Commit a79627d

Browse files
committed
lib: os: timespec_util: reimplement and test with 32768 ticks/s
Originally, the timespec_to_timeout() and timespec_from_timeout() tests assumed that tv_sec and tv_nsec values divided evenly with the number of ticks per second (CONFIG_SYS_CLOCK_TICKS_PER_SEC). However, those assumptions broke down when testing with 32768 ticks/s . Upon further investigation, there were additional corner cases discovered that were not handled in an ideal or safe way. Part of this fix involved identifying several "domains" and special values that have semantic meaning that must be handled particularly carefully. Additional hidden-API constants were made to simplify the solution. K_TICK_MIN (1) K_TICK_MAX (UINT32_MAX-1 in 32-bit, INT64_MAX in 64-bit) K_TS_NO_WAIT (like K_NO_WAIT) K_TS_FOREVER (like K_FOREVER) 1. Converting a negative k_timeout_t These only exist in 64-bit representation and actually encode absolute timepoints via ticks. Since the stated purpose of the conversion functions is to convert between durations, we must reject absolute time points in timespec_from_timeout(). 2. Converting a negative timespec We assume that this duration means a timeout has already expired, and round these up to K_NO_WAIT in timespec_to_timeout(). 3. Due to the larger numeric space in the timespec representation, the reverse mapping of timespec to k_timeout_t is "fuzzy". However, K_NO_WAIT (K_TS_NO_WAIT) and K_FOREVER (K_TS_FOREVER) must remain semantically equivalent. The previous implementation also held this to be true, but the test cases are a bit clearer about it now. 4. Also, due to the larger numeric space in timespec representation, there was a special requirement to round up to the nearest tick boundary for any timespec in the strictly exclusive range (K_TS_NO_WAIT, K_TS_MAX). We must round up, since a) rounding down to K_NO_WAIT is most certainly not representative of a non-zero finite duration delay b) the kernel operates on tick boundaries However, since it's possible that rounding up might not _always_ be the right thing to do, in order to allow the application to make more informed decisions, we created a modified timespec_to_timeout_rem() that also returns the remainder (or difference) between the requested time to convert and resulting k_timeout_t. The difference is expressed as a timespec object. 5. Above the K_TS_MAX boundary, which is the highest possible timespec that can be represented by a tick, and specifically in the 64-bit timeout representation, there is a domain that cannot be rounded up to K_TS_FOREVER and should always be rounded down to K_TS_MAX. This is to ensure that finite durations remain finite (even if they are beyond the heat death of the universe). Signed-off-by: Chris Friedt <cfriedt@tenstorrent.com>
1 parent b3ccee4 commit a79627d

File tree

8 files changed

+431
-206
lines changed

8 files changed

+431
-206
lines changed

include/zephyr/sys/clock.h

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,44 @@ typedef struct {
166166
/* added tick needed to account for tick in progress */
167167
#define _TICK_ALIGN 1
168168

169+
/* Converts ticks to nanoseconds with minimal rounding error */
170+
#define K_TICKS_TO_NSECS(ticks) \
171+
MULDIV((uint64_t)(ticks), NSEC_PER_SEC, CONFIG_SYS_CLOCK_TICKS_PER_SEC)
172+
173+
/* The minimum duration in ticks strictly greater than that of K_NO_WAIT */
174+
#define K_TICK_MIN ((k_ticks_t)1)
175+
176+
/* The maximum duration in ticks strictly and semantically "less than" K_FOREVER */
177+
#define K_TICK_MAX ((k_ticks_t)(IS_ENABLED(CONFIG_TIMEOUT_64BIT) ? INT64_MAX : UINT32_MAX - 1))
178+
179+
/* The semantic equivalent of K_NO_WAIT but expressed as a timespec object*/
180+
#define K_TS_NO_WAIT \
181+
((struct timespec){ \
182+
.tv_sec = 0, \
183+
.tv_nsec = 0, \
184+
})
185+
186+
/* The semantic equivalent of K_FOREVER but expressed as a timespec object*/
187+
#define K_TS_FOREVER \
188+
((struct timespec){ \
189+
.tv_sec = (time_t)INT64_MAX, \
190+
.tv_nsec = (long)(NSEC_PER_SEC - 1), \
191+
})
192+
193+
/* The semantic equivalent of K_TICK_MIN but expressed as a timespec object */
194+
#define K_TS_MIN \
195+
((struct timespec){ \
196+
.tv_sec = 0, \
197+
.tv_nsec = (long)K_TICKS_TO_NSECS(K_TICK_MIN), \
198+
})
199+
200+
/* The semantic equivalent of K_TICK_MAX but expressed as a timespec object */
201+
#define K_TS_MAX \
202+
((struct timespec){ \
203+
.tv_sec = (time_t)((uint64_t)K_TICK_MAX / CONFIG_SYS_CLOCK_TICKS_PER_SEC), \
204+
.tv_nsec = (long)(K_TICKS_TO_NSECS(K_TICK_MAX) % NSEC_PER_SEC), \
205+
})
206+
169207
/** @endcond */
170208

171209
#ifndef CONFIG_TIMER_READS_ITS_FREQUENCY_AT_RUNTIME

include/zephyr/sys/timeutil.h

Lines changed: 41 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -381,25 +381,6 @@ static inline bool timespec_normalize(struct timespec *ts)
381381
{
382382
__ASSERT_NO_MSG(ts != NULL);
383383

384-
#if defined(CONFIG_SPEED_OPTIMIZATIONS) && HAS_BUILTIN(__builtin_add_overflow)
385-
386-
int sign = (ts->tv_nsec >= 0) - (ts->tv_nsec < 0);
387-
int64_t sec = (ts->tv_nsec >= (long)NSEC_PER_SEC) * (ts->tv_nsec / (long)NSEC_PER_SEC) +
388-
((ts->tv_nsec < 0) && (ts->tv_nsec != LONG_MIN)) *
389-
DIV_ROUND_UP((unsigned long)-ts->tv_nsec, (long)NSEC_PER_SEC) +
390-
(ts->tv_nsec == LONG_MIN) * ((LONG_MAX / NSEC_PER_SEC) + 1);
391-
bool overflow = __builtin_add_overflow(ts->tv_sec, sign * sec, &ts->tv_sec);
392-
393-
ts->tv_nsec -= sign * (long)NSEC_PER_SEC * sec;
394-
395-
if (!overflow) {
396-
__ASSERT_NO_MSG(timespec_is_valid(ts));
397-
}
398-
399-
return !overflow;
400-
401-
#else
402-
403384
long sec;
404385

405386
if (ts->tv_nsec >= (long)NSEC_PER_SEC) {
@@ -441,11 +422,10 @@ static inline bool timespec_normalize(struct timespec *ts)
441422
/* no change: SonarQube was complaining */
442423
}
443424

444-
__ASSERT_NO_MSG(timespec_is_valid(ts));
425+
__ASSERT(timespec_is_valid(ts), "{%lld, %ld} is not a valid timespec",
426+
(long long)ts->tv_sec, (long)ts->tv_nsec);
445427

446428
return true;
447-
448-
#endif
449429
}
450430

451431
/**
@@ -614,123 +594,60 @@ static inline bool timespec_equal(const struct timespec *a, const struct timespe
614594
* This function converts time durations expressed as Zephyr @ref k_timeout_t
615595
* objects to `struct timespec` objects.
616596
*
597+
* @note This function will assert if assertions are enabled and @p timeout is not relative,
598+
* (i.e. a timeout generated by `K_TIMEOUT_ABS_TICKS` or similar is used).
599+
*
617600
* @param timeout the kernel timeout to convert
618601
* @param[out] ts the timespec to store the result
619602
*/
620-
static inline void timespec_from_timeout(k_timeout_t timeout, struct timespec *ts)
621-
{
622-
__ASSERT_NO_MSG(ts != NULL);
623-
624-
#if defined(CONFIG_SPEED_OPTIMIZATIONS)
625-
626-
uint64_t ns = k_ticks_to_ns_ceil64(timeout.ticks);
627-
628-
*ts = (struct timespec){
629-
.tv_sec = (timeout.ticks == K_TICKS_FOREVER) * INT64_MAX +
630-
(timeout.ticks != K_TICKS_FOREVER) * (ns / NSEC_PER_SEC),
631-
.tv_nsec = (timeout.ticks == K_TICKS_FOREVER) * (NSEC_PER_SEC - 1) +
632-
(timeout.ticks != K_TICKS_FOREVER) * (ns % NSEC_PER_SEC),
633-
};
634-
635-
#else
636-
637-
if (timeout.ticks == 0) {
638-
/* This is equivalent to K_NO_WAIT, but without including <zephyr/kernel.h> */
639-
ts->tv_sec = 0;
640-
ts->tv_nsec = 0;
641-
} else if (timeout.ticks == K_TICKS_FOREVER) {
642-
/* This is roughly equivalent to K_FOREVER, but not including <zephyr/kernel.h> */
643-
ts->tv_sec = (time_t)INT64_MAX;
644-
ts->tv_nsec = NSEC_PER_SEC - 1;
645-
} else {
646-
uint64_t ns = k_ticks_to_ns_ceil64(timeout.ticks);
647-
648-
ts->tv_sec = ns / NSEC_PER_SEC;
649-
ts->tv_nsec = ns - ts->tv_sec * NSEC_PER_SEC;
650-
}
651-
652-
#endif
653-
654-
__ASSERT_NO_MSG(timespec_is_valid(ts));
655-
}
603+
void timespec_from_timeout(k_timeout_t timeout, struct timespec *ts);
656604

657605
/**
658606
* @brief Convert a timespec to a kernel timeout
659607
*
660-
* This function converts durations expressed as a `struct timespec` to Zephyr @ref k_timeout_t
661-
* objects.
608+
* This function converts a time duration, @p req, expressed as a `timespec` object, to a Zephyr
609+
* @ref k_timeout_t object.
610+
*
611+
* If @p req contains a negative duration or if both `tv_sec` and `tv_nsec` fields are zero, this
612+
* function will return @ref K_NO_WAIT.
613+
*
614+
* If @p req contains the maximum representable `timespec`, `{max(time_t), 999999999}`, then this
615+
* function will return @ref K_FOREVER.
616+
*
617+
* If @p req contains a value that is greater than the maximum equivalent tick duration, then this
618+
* function will return the maximum representable tick duration (i.e. @p req will be rounded-down).
662619
*
663-
* Given that the range of a `struct timespec` is much larger than the range of @ref k_timeout_t,
664-
* and also given that the functions are only intended to be used to convert time durations
665-
* (which are always positive), the function will saturate to @ref K_NO_WAIT if the `tv_sec` field
666-
* of @a ts is negative.
620+
* Otherwise, this function will return the `k_timeout_t` that is rounded-up to a tick boundary.
621+
*
622+
* If @p rem is not `NULL`, it will be set to the remainder of the conversion, i.e. the difference
623+
* between the requested duration and the converted duration as a `timespec` object, approximately
624+
* as shown below.
625+
*
626+
* ```python
627+
* rem = requested_duration - converted_duration
628+
* ```
629+
*
630+
* @param req the requested `timespec` to convert
631+
* @param[out] rem optional pointer to a `timespec` to store the remainder
632+
* @return the corresponding kernel timeout
633+
*/
634+
k_timeout_t timespec_to_timeout_rem(const struct timespec *req, struct timespec *rem);
635+
636+
/**
637+
* @brief Convert a timespec to a kernel timeout
667638
*
668-
* Similarly, if the duration is too large to fit in @ref k_timeout_t, the function will
669-
* saturate to @ref K_FOREVER.
639+
* This function converts a time duration, @p req, expressed as a `timespec` object, to a Zephyr
640+
* @ref k_timeout_t object exactly as @ref timespec_to_timeout_rem, but does not provide a
641+
* remainder.
670642
*
671643
* @param ts the timespec to convert
672644
* @return the kernel timeout
645+
*
646+
* @see timespec_to_timeout_rem()
673647
*/
674648
static inline k_timeout_t timespec_to_timeout(const struct timespec *ts)
675649
{
676-
__ASSERT_NO_MSG((ts != NULL) && timespec_is_valid(ts));
677-
678-
#if defined(CONFIG_SPEED_OPTIMIZATIONS)
679-
680-
return (k_timeout_t){
681-
/* note: must check for 32-bit size here until #90029 is resolved */
682-
.ticks = ((sizeof(ts->tv_sec) == sizeof(int32_t) && (ts->tv_sec == INT32_MAX) &&
683-
(ts->tv_nsec == NSEC_PER_SEC - 1)) ||
684-
((sizeof(ts->tv_sec) == sizeof(int64_t)) && (ts->tv_sec == INT64_MAX) &&
685-
(ts->tv_nsec == NSEC_PER_SEC - 1))) *
686-
K_TICKS_FOREVER +
687-
((sizeof(ts->tv_sec) == sizeof(int32_t) && (ts->tv_sec == INT32_MAX) &&
688-
(ts->tv_nsec == NSEC_PER_SEC - 1)) ||
689-
((sizeof(ts->tv_sec) == sizeof(int64_t)) && (ts->tv_sec != INT64_MAX) &&
690-
(ts->tv_sec >= 0))) *
691-
(IS_ENABLED(CONFIG_TIMEOUT_64BIT)
692-
? (int64_t)(CLAMP(
693-
k_sec_to_ticks_floor64(ts->tv_sec) +
694-
k_ns_to_ticks_floor64(ts->tv_nsec),
695-
0, (uint64_t)INT64_MAX))
696-
: (uint32_t)(CLAMP(
697-
k_sec_to_ticks_floor64(ts->tv_sec) +
698-
k_ns_to_ticks_floor64(ts->tv_nsec),
699-
0, (uint64_t)UINT32_MAX)))};
700-
701-
#else
702-
703-
if ((ts->tv_sec < 0) || (ts->tv_sec == 0 && ts->tv_nsec == 0)) {
704-
/* This is equivalent to K_NO_WAIT, but without including <zephyr/kernel.h> */
705-
return (k_timeout_t){
706-
.ticks = 0,
707-
};
708-
/* note: must check for 32-bit size here until #90029 is resolved */
709-
} else if (((sizeof(ts->tv_sec) == sizeof(int32_t)) && (ts->tv_sec == INT32_MAX) &&
710-
(ts->tv_nsec == NSEC_PER_SEC - 1)) ||
711-
((sizeof(ts->tv_sec) == sizeof(int64_t)) && (ts->tv_sec == INT64_MAX) &&
712-
(ts->tv_nsec == NSEC_PER_SEC - 1))) {
713-
/* This is equivalent to K_FOREVER, but not including <zephyr/kernel.h> */
714-
return (k_timeout_t){
715-
.ticks = K_TICKS_FOREVER,
716-
};
717-
} else {
718-
if (IS_ENABLED(CONFIG_TIMEOUT_64BIT)) {
719-
return (k_timeout_t){
720-
.ticks = (int64_t)CLAMP(k_sec_to_ticks_floor64(ts->tv_sec) +
721-
k_ns_to_ticks_floor64(ts->tv_nsec),
722-
0, (uint64_t)INT64_MAX),
723-
};
724-
} else {
725-
return (k_timeout_t){
726-
.ticks = (uint32_t)CLAMP(k_sec_to_ticks_floor64(ts->tv_sec) +
727-
k_ns_to_ticks_floor64(ts->tv_nsec),
728-
0, (uint64_t)UINT32_MAX),
729-
};
730-
}
731-
}
732-
733-
#endif
650+
return timespec_to_timeout_rem(ts, NULL);
734651
}
735652

736653
/**

include/zephyr/sys/util.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,13 @@ extern "C" {
372372
? ((n) - ((d) / 2)) / (d) \
373373
: ((n) + ((d) / 2)) / (d))
374374

375+
/**
376+
* @brief Calculate `a * (b / c)` as `(a * b) / c` to minimize rounding errors
377+
*
378+
* @return The result of @p a * @p b / @p c ordered to minimize rounding errors.
379+
*/
380+
#define MULDIV(a, b, c) (((a) * (b)) / (c))
381+
375382
#ifndef MAX
376383
/**
377384
* @brief Obtain the maximum of two values.
@@ -846,6 +853,8 @@ static inline bool util_eq(const void *m1, size_t len1, const void *m2, size_t l
846853
#define KHZ(x) ((x) * 1000)
847854
/** @brief Number of Hz in @p x MHz */
848855
#define MHZ(x) (KHZ(x) * 1000)
856+
/** @brief Number of Hz in @p x GHz */
857+
#define GHZ(x) (MHZ(x) * 1000ULL)
849858

850859
/**
851860
* @brief For the POSIX architecture add a minimal delay in a busy wait loop.

lib/os/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ zephyr_sources(
1111
printk.c
1212
sem.c
1313
thread_entry.c
14+
timeutil.c
1415
)
1516

1617
zephyr_sources_ifdef(CONFIG_FDTABLE fdtable.c)

lib/os/timeutil.c

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright (c) 2025 Tenstorrent AI ULC
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
#include <stdbool.h>
8+
#include <stddef.h>
9+
#include <stdint.h>
10+
#include <time.h>
11+
12+
#include <zephyr/kernel.h>
13+
#include <zephyr/sys/__assert.h>
14+
#include <zephyr/sys/timeutil.h>
15+
#include <zephyr/sys/time_units.h>
16+
#include <zephyr/sys/util.h>
17+
18+
void timespec_from_timeout(k_timeout_t timeout, struct timespec *ts)
19+
{
20+
__ASSERT_NO_MSG(ts != NULL);
21+
__ASSERT_NO_MSG(Z_IS_TIMEOUT_RELATIVE(timeout) ||
22+
(IS_ENABLED(CONFIG_TIMEOUT_64BIT) && K_TIMEOUT_EQ(timeout, K_FOREVER)));
23+
24+
if (K_TIMEOUT_EQ(timeout, K_FOREVER)) {
25+
/* duration == K_TICKS_FOREVER ticks */
26+
*ts = K_TS_FOREVER;
27+
} else if (K_TIMEOUT_EQ(timeout, K_NO_WAIT)) {
28+
/* duration <= 0 ticks */
29+
*ts = K_TS_NO_WAIT;
30+
} else {
31+
*ts = (struct timespec){
32+
.tv_sec =
33+
(time_t)((uint64_t)timeout.ticks / CONFIG_SYS_CLOCK_TICKS_PER_SEC),
34+
.tv_nsec = (long)(K_TICKS_TO_NSECS((uint64_t)timeout.ticks) % NSEC_PER_SEC),
35+
};
36+
}
37+
38+
__ASSERT_NO_MSG(timespec_is_valid(ts));
39+
}
40+
41+
static k_timeout_t timespec_timeout_rem(const struct timespec *ts, struct timespec *rem,
42+
k_timeout_t timeout)
43+
{
44+
if (rem != NULL) {
45+
timespec_from_timeout(timeout, rem);
46+
timespec_sub(rem, ts);
47+
timespec_negate(rem);
48+
}
49+
50+
return timeout;
51+
}
52+
53+
k_timeout_t timespec_to_timeout_rem(const struct timespec *ts, struct timespec *rem)
54+
{
55+
__ASSERT_NO_MSG((ts != NULL) && timespec_is_valid(ts));
56+
57+
if (timespec_compare(ts, &K_TS_NO_WAIT) <= 0) {
58+
if (rem != NULL) {
59+
*rem = *ts;
60+
}
61+
return K_NO_WAIT;
62+
}
63+
64+
if (timespec_compare(ts, &K_TS_FOREVER) == 0) {
65+
if (rem != NULL) {
66+
*rem = K_TS_NO_WAIT;
67+
}
68+
return K_FOREVER;
69+
}
70+
71+
if (timespec_compare(ts, &K_TS_MIN) <= 0) {
72+
/* round up to align to min ticks */
73+
return timespec_timeout_rem(ts, rem, K_TICKS(K_TICK_MIN));
74+
}
75+
76+
if (timespec_compare(ts, &K_TS_MAX) >= 0) {
77+
/* round down to align to max ticks */
78+
return timespec_timeout_rem(ts, rem, K_TICKS(K_TICK_MAX));
79+
}
80+
81+
uint64_t ticks_s = k_sec_to_ticks_ceil64(ts->tv_sec);
82+
uint64_t ticks_ns = k_ns_to_ticks_ceil64(ts->tv_nsec);
83+
84+
return timespec_timeout_rem(ts, rem, K_TICKS(ticks_s + ticks_ns));
85+
}

tests/lib/timespec_util/prj.conf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
CONFIG_ZTEST=y
2+
3+
# required to use native_sim for testing and have consistent time_t size across all platforms
4+
CONFIG_PICOLIBC=y

0 commit comments

Comments
 (0)