ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
clock.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file clock.h
6 * @brief Pluggable monotonic clock for all library timing.
7 *
8 * The library's internal timing runs at **1000 Hz** - one tick is one millisecond,
9 * the cadence the test suite asserts and every timeout / poll is expressed in.
10 * `Clock.millis()` is that single time source, leaving the reading in `Clock.ms`; by
11 * default it is the platform `millis()`.
12 *
13 * To drive the library from your own clock (a hardware timer, an external RTC, a
14 * simulation clock), call:
15 *
16 * Clock.src.fn = my_clock_fn;
17 * Clock.src.ticks_per_second = my_ticks_per_second;
18 * Clock.set_ms(Clock.internal);
19 *
20 * Your clock reports a free-running tick count at `ticks_per_second`. The library
21 * **divides it down to its internal 1000 Hz**, so timeouts and polling keep the
22 * exact 1 ms granularity the tests verify regardless of how fast your clock runs.
23 * Pass a rate >= 1000, ideally a multiple of 1000 for exact division (e.g. a
24 * 1 MHz timer -> ticks_per_second = 1000000, divided by 1000). Pass `NULL` to
25 * revert to the platform default. One source covers everything - swap it once and
26 * every subsystem follows.
27 *
28 * The worker poll cadence is fixed at 1000 Hz (the tested default); a build can
29 * trade latency for idle power with PROTOCORE_WORKER_POLL_TICKS - see protocore_config.h.
30 *
31 * The installed clocks live in clock.c, one instance for the whole program, so a
32 * build that reads the clock links that translation unit.
33 *
34 * @author Douglas Quigg (dstroy0)
35 * @date 2026
36 */
37
38#ifndef PROTOCORE_CLOCK_H
39#define PROTOCORE_CLOCK_H
40
41#include "protocore_config.h" // the entry point: PROTOCORE_INLINE, protocore_types.h, and the platform time base
42
44
45/** @brief User clock: returns a free-running monotonic tick count. */
46typedef uint32_t (*protocore_clock_fn)(void);
47
48/**
49 * @brief Install a custom clock running at @p ticks_per_second; the library divides
50 * it down to its internal 1000 Hz. Pass (NULL, 0) to revert to the
51 * platform default.
52 */
53/** @brief A time source and the rate it counts at. */
54typedef struct
55{
56 protocore_clock_fn fn; ///< the source; NULL falls back to the platform's own
57 uint32_t ticks_per_second; ///< its rate, divided down to the library's
59
60/** @brief The installed clocks and the calls that read them, described only in clock.c. */
61struct ClockInternal;
62
63/**
64 * @brief The pluggable time source.
65 *
66 * A caller sets the members a call takes, invokes it through ::Clock, and reads the outcome off the
67 * same handle. The installed sources are behind @ref internal.
68 *
69 * @var ClockNs::src a time source and the rate it counts at
70 * @var ClockNs::ms milliseconds since boot
71 * @var ClockNs::us microseconds since boot
72 * @var ClockNs::cyc the free-running CPU cycle count
73 * @var ClockNs::set_ms install the millisecond source
74 * @var ClockNs::millis read it, or the platform's when none is installed
75 * @var ClockNs::set_us install the microsecond source
76 * @var ClockNs::micros read it, or the platform's when none is installed
77 * @var ClockNs::cycles read the cycle counter
78 * @var ClockNs::internal the installed sources and the calls that read them
79 *
80 * The sources live behind the handle rather than in this header because a caller installing a clock
81 * and the library reading it are different translation units and have to see the same value.
82 */
83typedef struct
84{
86
87 uint32_t ms;
88 uint32_t us;
89 uint32_t cyc;
90
91 void (*set_ms)(struct ClockInternal *ctx);
92 void (*millis)(struct ClockInternal *ctx);
93 void (*set_us)(struct ClockInternal *ctx);
94 void (*micros)(struct ClockInternal *ctx);
95 void (*cycles)(struct ClockInternal *ctx);
96
97 struct ClockInternal *internal;
98} ClockNs;
99
100/** @brief The one symbol this module exports. */
101extern ClockNs Clock;
102
103/** @brief The library's monotonic time at 1000 Hz (milliseconds). */
104
105/**
106 * @brief Block for @p ms milliseconds - the library's single delay primitive.
107 *
108 * Hands the core back for @p ms ticks. A tick is a millisecond, and zero is a bare yield.
109 */
110PROTOCORE_INLINE void pcdelay(uint32_t ms)
111{
112 protocore_platform_task_delay(ms);
113}
114
115// ---------------------------------------------------------------------------
116// Microsecond time base (v5 clock-awareness): ISR timestamps + sub-ms latency
117// ---------------------------------------------------------------------------
118//
119// A second, higher-resolution source for real-time work: timestamping a hardware
120// event in an ISR and budgeting how long a piece of work takes. Pluggable like the
121// millisecond clock; the default is the platform micros() on device, or
122// protocore_millis() * 1000 on host (override for sub-ms precision in tests).
123
124/**
125 * @brief Install a custom microsecond clock running at @p ticks_per_second; the
126 * library divides it down to 1 MHz. Pass (NULL, 0) for the platform
127 * default.
128 */
129
130/**
131 * @brief Monotonic microseconds - the high-resolution time base for ISR
132 * timestamps and sub-millisecond latency. Safe to call from an ISR. Wraps
133 * roughly every 71 minutes, so use it only for short deltas (unsigned
134 * subtraction is wrap-safe).
135 */
136
137/**
138 * @brief Block for at least @p us microseconds of REAL time - a hardware settle.
139 *
140 * ::pcdelay sleeps the task one RTOS tick at a time and a tick is a millisecond, so it cannot
141 * express a shorter wait: asking it for 500 us waits 1 ms.
142 *
143 * This reads ::protocore_platform_micros, the raw counter, and NOT ::protocore_micros. The library clock is
144 * pluggable: an application can install one that runs at its own rate, and a test can install one
145 * it steps by hand. A part that needs 500 us to settle needs 500 us of real time, so a clock the
146 * application controls cannot be what decides when the wait ends - against a stepped clock this
147 * returns at once or never returns. The subtraction is unsigned, so the counter's wrap is safe.
148 *
149 * This SPINS: it does not yield, and nothing else on the core runs while it does. That holds only
150 * where the wait is part of bringing a device up. On the request path it stalls handle() for its
151 * whole duration, which the pump's latency budget then records.
152 */
154{
155 uint32_t start = protocore_platform_micros();
156 while (protocore_platform_micros() - start < us)
157 {
158 }
159}
160
161// ---------------------------------------------------------------------------
162// Latency budgeting: measure an operation against a microsecond budget
163// ---------------------------------------------------------------------------
164
165/**
166 * @brief Rolling latency statistics in microseconds: sample count, min / max /
167 * mean, and how many samples blew a budget. Fixed size, no heap; a
168 * subsystem (the preempting queue, a DMA path, a forwarding rule) keeps one
169 * and reports it for real-time visibility.
170 */
171typedef struct
172{
173 uint32_t count;
174 uint32_t over_budget; ///< samples whose latency exceeded the budget
175 uint32_t min_us;
176 uint32_t max_us;
177 uint64_t sum_us;
179
180/** @brief Zero a stat (min seeded high so the first sample sets it). */
182{
183 s->count = 0;
184 s->over_budget = 0;
185 s->min_us = 0xFFFFFFFFu;
186 s->max_us = 0;
187 s->sum_us = 0;
188}
189
190/** @brief Start of a measured span: capture the current microsecond time. */
192{
194 return Clock.us;
195}
196
197/**
198 * @brief End of a span started at @p start_us: record its latency, counting it as
199 * over-budget when @p budget_us is non-zero and exceeded. Wrap-safe.
200 */
201PROTOCORE_INLINE void protocore_lat_end(protocore_latency_stat *s, uint32_t start_us, uint32_t budget_us)
202{
204 uint32_t lat = Clock.us - start_us; // wrap-safe unsigned delta
205 s->count++;
206 s->sum_us += lat;
207 if (lat < s->min_us)
208 {
209 s->min_us = lat;
210 }
211 if (lat > s->max_us)
212 {
213 s->max_us = lat;
214 }
215 if (budget_us && lat > budget_us)
216 {
217 s->over_budget++;
218 }
219}
220
221/** @brief Mean latency (us) over the recorded samples, 0 if none. */
223{
224 return s->count ? (uint32_t)(s->sum_us / s->count) : 0u;
225}
226
227// ---------------------------------------------------------------------------
228// CPU cycle counter (v5 clock-awareness): sub-microsecond jitter measurement
229// ---------------------------------------------------------------------------
230//
231// protocore_micros() wraps the platform micros(), which is itself commonly derived from
232// a 1 MHz-divided cycle counter - roughly 1 us of quantization noise. That is too
233// coarse to characterize a single SPI-DMA transaction: at a 20 MHz SPI clock one
234// byte is 400 ns, and a fast external DAQ/scope can complete several DMA transfers
235// within one microsecond tick. protocore_cycles() reads the CPU cycle counter directly
236// for nanosecond-grade deltas - trigger-to-first-sample latency, inter-transfer jitter -
237// the same primitive mmgr/dma and the pentesting rig's cryptobench already reach for ad
238// hoc; this gives every subsystem one named, documented entry point instead. Like
239// protocore_micros, it wraps (roughly every 18 s at 240 MHz) - use it only for short
240// deltas via wrap-safe unsigned subtraction.
241
242/**
243 * @brief Free-running CPU cycle count. ISR-safe. On a part that has one this is the
244 * hardware cycle counter; on host it falls back to protocore_micros()
245 * scaled by @p host_fallback_mhz (a coarse stand-in - override with a real
246 * cycle source in a host test that needs nanosecond precision).
247 */
248
249/**
250 * @brief Convert a cycle-count delta to nanoseconds at @p cpu_mhz (the running CPU
251 * frequency, as the platform reports it). @p delta_cycles must come
252 * from a wrap-safe unsigned subtraction of two protocore_cycles() reads.
253 */
254PROTOCORE_INLINE uint32_t protocore_cycles_to_ns(uint32_t delta_cycles, uint32_t cpu_mhz)
255{
256 return cpu_mhz ? (uint32_t)(((uint64_t)delta_cycles * 1000u) / cpu_mhz) : 0u;
257}
258
260
261#endif // PROTOCORE_CLOCK_H
PROTOCORE_INLINE uint32_t protocore_lat_avg_us(const protocore_latency_stat *s)
Mean latency (us) over the recorded samples, 0 if none.
Definition clock.h:222
PROTOCORE_INLINE void protocore_lat_reset(protocore_latency_stat *s)
Zero a stat (min seeded high so the first sample sets it).
Definition clock.h:181
PROTOCORE_INLINE uint32_t protocore_cycles_to_ns(uint32_t delta_cycles, uint32_t cpu_mhz)
Free-running CPU cycle count. ISR-safe. On a part that has one this is the hardware cycle counter; on...
Definition clock.h:254
PROTOCORE_INLINE void protocore_lat_end(protocore_latency_stat *s, uint32_t start_us, uint32_t budget_us)
End of a span started at start_us: record its latency, counting it as over-budget when budget_us is n...
Definition clock.h:201
PROTOCORE_BEGIN_DECLS typedef uint32_t(* protocore_clock_fn)(void)
User clock: returns a free-running monotonic tick count.
Definition clock.h:46
PROTOCORE_INLINE void protocore_delay_us(uint32_t us)
Install a custom microsecond clock running at ticks_per_second; the library divides it down to 1 MHz....
Definition clock.h:153
ClockNs Clock
The one symbol this module exports.
PROTOCORE_INLINE uint32_t protocore_lat_begin(void)
Start of a measured span: capture the current microsecond time.
Definition clock.h:191
PROTOCORE_INLINE void pcdelay(uint32_t ms)
The library's monotonic time at 1000 Hz (milliseconds).
Definition clock.h:110
#define PROTOCORE_INLINE
Linkage for a leaf primitive whose body is cheaper than the call that reaches it.
uint32_t ms
Definition clock.h:87
struct ClockInternal * internal
Definition clock.h:97
uint32_t us
Definition clock.h:88
ClockSrcArgs src
Definition clock.h:85
void(* micros)(struct ClockInternal *ctx)
Definition clock.h:94
uint32_t cyc
Definition clock.h:89
Install a custom clock running at ticks_per_second; the library divides it down to its internal 1000 ...
Definition clock.h:55
uint32_t ticks_per_second
its rate, divided down to the library's
Definition clock.h:57
protocore_clock_fn fn
the source; NULL falls back to the platform's own
Definition clock.h:56
Rolling latency statistics in microseconds: sample count, min / max / mean, and how many samples blew...
Definition clock.h:172
uint32_t over_budget
samples whose latency exceeded the budget
Definition clock.h:174
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
#define PROTOCORE_END_DECLS
Definition types.h:97