ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
clock.h
Go to the documentation of this file.
1// 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 * `pc_millis()` is that single time source; by default it is the platform
11 * `millis()`.
12 *
13 * To drive the library from your own clock (a hardware timer, an external RTC, a
14 * simulation clock), call:
15 *
16 * pc_set_clock(my_clock_fn, my_ticks_per_second);
17 *
18 * Your clock reports a free-running tick count at `ticks_per_second`. The library
19 * **divides it down to its internal 1000 Hz**, so timeouts and polling keep the
20 * exact 1 ms granularity the tests verify regardless of how fast your clock runs.
21 * Pass a rate >= 1000, ideally a multiple of 1000 for exact division (e.g. a
22 * 1 MHz timer -> ticks_per_second = 1000000, divided by 1000). Pass `nullptr` to
23 * revert to the platform default. One source covers everything - swap it once and
24 * every subsystem follows.
25 *
26 * The worker poll cadence is fixed at 1000 Hz (the tested default); a build can
27 * trade latency for idle power with PC_WORKER_POLL_TICKS - see protocore_config.h.
28 *
29 * Header-only (the override state lives in inline-function-local statics, a single
30 * instance across the whole program), so there is nothing extra to compile or link.
31 *
32 * @author Douglas Quigg (dstroy0)
33 * @date 2026
34 */
35
36#ifndef PROTOCORE_CLOCK_H
37#define PROTOCORE_CLOCK_H
38
39#include <Arduino.h> // platform millis()
40#include <stdint.h>
41
42/** @brief User clock: returns a free-running monotonic tick count. */
43typedef uint32_t (*pc_clock_fn)(void);
44
45// Override state. Inline functions with local statics resolve to a single shared
46// instance across all translation units (ODR), so a set from anywhere is seen
47// everywhere - no .cpp, no globals to define.
49{
50 static pc_clock_fn fn = nullptr;
51 return fn;
52}
53inline uint32_t &_pc_clock_div_ref()
54{
55 static uint32_t div = 1;
56 return div;
57}
58
59/**
60 * @brief Install a custom clock running at @p ticks_per_second; the library divides
61 * it down to its internal 1000 Hz. Pass (nullptr, 0) to revert to the
62 * platform default.
63 */
64inline void pc_set_clock(pc_clock_fn fn, uint32_t ticks_per_second)
65{
66 _pc_clock_fn_ref() = fn;
67 _pc_clock_div_ref() = (ticks_per_second >= 1000) ? (ticks_per_second / 1000) : 1;
68}
69
70/** @brief The library's monotonic time at 1000 Hz (milliseconds). */
71inline uint32_t pc_millis(void)
72{
74 if (fn)
75 {
76 return fn() / _pc_clock_div_ref();
77 }
78 return millis();
79}
80
81/**
82 * @brief Block for at least @p ms milliseconds - the library's single delay primitive.
83 *
84 * `src/` never calls the platform `delay()` directly; every wait goes through this so timing stays
85 * centralized and portable. On device it yields to the RTOS one tick at a time until @p ms has elapsed on
86 * the monotonic clock (so it sleeps the task and feeds the watchdog, never starving the scheduler); on host
87 * it spins on the same clock. Measured against ::pc_millis, so a custom clock governs it too.
88 */
89inline void pcdelay(uint32_t ms)
90{
91#ifdef ARDUINO
92 if (ms == 0)
93 {
94 vTaskDelay(0); // a bare cooperative yield
95 return;
96 }
97 uint32_t start = pc_millis();
98 while (pc_millis() - start < ms)
99 {
100 vTaskDelay(1); // one RTOS tick: sleeps the task (the core can idle) and feeds the watchdog
101 }
102#else
103 uint32_t start = pc_millis();
104 while (pc_millis() - start < ms)
105 {
106 // host: spin on the monotonic clock (device-only code paths call this; host tests do not sleep here)
107 }
108#endif
109}
110
111// ---------------------------------------------------------------------------
112// Microsecond time base (v5 clock-awareness): ISR timestamps + sub-ms latency
113// ---------------------------------------------------------------------------
114//
115// A second, higher-resolution source for real-time work: timestamping a hardware
116// event in an ISR and budgeting how long a piece of work takes. Pluggable like the
117// millisecond clock; the default is the platform micros() on device, or
118// pc_millis() * 1000 on host (override for sub-ms precision in tests).
119
121{
122 static pc_clock_fn fn = nullptr;
123 return fn;
124}
125inline uint32_t &_pc_micros_div_ref()
126{
127 static uint32_t div = 1;
128 return div;
129}
130
131/**
132 * @brief Install a custom microsecond clock running at @p ticks_per_second; the
133 * library divides it down to 1 MHz. Pass (nullptr, 0) for the platform
134 * default.
135 */
136inline void pc_set_micros_clock(pc_clock_fn fn, uint32_t ticks_per_second)
137{
138 _pc_micros_fn_ref() = fn;
139 _pc_micros_div_ref() = (ticks_per_second >= 1000000u) ? (ticks_per_second / 1000000u) : 1u;
140}
141
142/**
143 * @brief Monotonic microseconds - the high-resolution time base for ISR
144 * timestamps and sub-millisecond latency. Safe to call from an ISR. Wraps
145 * roughly every 71 minutes, so use it only for short deltas (unsigned
146 * subtraction is wrap-safe).
147 */
148inline uint32_t pc_micros(void)
149{
151 if (fn)
152 {
153 return fn() / _pc_micros_div_ref();
154 }
155#ifdef ARDUINO
156 return micros();
157#else
158 return pc_millis() * 1000u; // host fallback (no platform micros); override for precision
159#endif
160}
161
162// ---------------------------------------------------------------------------
163// Latency budgeting: measure an operation against a microsecond budget
164// ---------------------------------------------------------------------------
165
166/**
167 * @brief Rolling latency statistics in microseconds: sample count, min / max /
168 * mean, and how many samples blew a budget. Fixed size, no heap; a
169 * subsystem (the preempting queue, a DMA path, a forwarding rule) keeps one
170 * and reports it for real-time visibility.
171 */
173{
174 uint32_t count;
175 uint32_t over_budget; ///< samples whose latency exceeded the budget
176 uint32_t min_us;
177 uint32_t max_us;
178 uint64_t sum_us;
179};
180
181/** @brief Zero a stat (min seeded high so the first sample sets it). */
183{
184 s->count = 0;
185 s->over_budget = 0;
186 s->min_us = 0xFFFFFFFFu;
187 s->max_us = 0;
188 s->sum_us = 0;
189}
190
191/** @brief Start of a measured span: capture the current microsecond time. */
192inline uint32_t pc_lat_begin(void)
193{
194 return pc_micros();
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 */
201inline void pc_lat_end(pc_latency_stat *s, uint32_t start_us, uint32_t budget_us)
202{
203 uint32_t lat = pc_micros() - start_us; // wrap-safe unsigned delta
204 s->count++;
205 s->sum_us += lat;
206 if (lat < s->min_us)
207 {
208 s->min_us = lat;
209 }
210 if (lat > s->max_us)
211 {
212 s->max_us = lat;
213 }
214 if (budget_us && lat > budget_us)
215 {
216 s->over_budget++;
217 }
218}
219
220/** @brief Mean latency (us) over the recorded samples, 0 if none. */
221inline uint32_t pc_lat_avg_us(const pc_latency_stat *s)
222{
223 return s->count ? (uint32_t)(s->sum_us / s->count) : 0u;
224}
225
226// ---------------------------------------------------------------------------
227// CPU cycle counter (v5 clock-awareness): sub-microsecond jitter measurement
228// ---------------------------------------------------------------------------
229//
230// pc_micros() wraps the platform micros(), which on ESP32 is itself derived from
231// a 1 MHz-divided cycle counter - roughly 1 us of quantization noise. That is too
232// coarse to characterize a single SPI-DMA transaction: at a 20 MHz SPI clock one
233// byte is 400 ns, and a fast external DAQ/scope can complete several DMA transfers
234// within one microsecond tick. pc_cycles() reads the CPU cycle counter directly
235// (CCOUNT on Xtensa ESP32 / ESP32-S3) for nanosecond-grade deltas - trigger-to-first-
236// -sample latency, inter-transfer jitter - the same primitive services/system/dma and the
237// pentesting rig's cryptobench already use ad hoc via ESP.getCycleCount(); this
238// gives every subsystem one named, documented entry point instead. Like pc_micros,
239// it wraps (roughly every 18 s at 240 MHz) - use it only for short deltas via
240// wrap-safe unsigned subtraction.
241
242/**
243 * @brief Free-running CPU cycle count. ISR-safe. On ARDUINO/ESP32 this is the
244 * hardware cycle counter (CCOUNT); on host it falls back to pc_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 */
248inline uint32_t pc_cycles(void)
249{
250#ifdef ARDUINO
251 return ESP.getCycleCount();
252#else
253 const uint32_t host_fallback_mhz = 240; // arbitrary stand-in; deltas only, never absolute
254 return pc_micros() * host_fallback_mhz;
255#endif
256}
257
258/**
259 * @brief Convert a cycle-count delta to nanoseconds at @p cpu_mhz (the running CPU
260 * frequency, e.g. getCpuFrequencyMhz() on ESP32). @p delta_cycles must come
261 * from a wrap-safe unsigned subtraction of two pc_cycles() reads.
262 */
263inline uint32_t pc_cycles_to_ns(uint32_t delta_cycles, uint32_t cpu_mhz)
264{
265 return cpu_mhz ? (uint32_t)(((uint64_t)delta_cycles * 1000u) / cpu_mhz) : 0u;
266}
267
268#endif // PROTOCORE_CLOCK_H
void pc_lat_end(pc_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
pc_clock_fn & _pc_clock_fn_ref()
Definition clock.h:48
pc_clock_fn & _pc_micros_fn_ref()
Definition clock.h:120
void pc_set_clock(pc_clock_fn fn, uint32_t ticks_per_second)
Install a custom clock running at ticks_per_second; the library divides it down to its internal 1000 ...
Definition clock.h:64
uint32_t pc_lat_begin(void)
Start of a measured span: capture the current microsecond time.
Definition clock.h:192
uint32_t(* pc_clock_fn)(void)
User clock: returns a free-running monotonic tick count.
Definition clock.h:43
void pcdelay(uint32_t ms)
Block for at least ms milliseconds - the library's single delay primitive.
Definition clock.h:89
uint32_t pc_lat_avg_us(const pc_latency_stat *s)
Mean latency (us) over the recorded samples, 0 if none.
Definition clock.h:221
uint32_t pc_cycles(void)
Free-running CPU cycle count. ISR-safe. On ARDUINO/ESP32 this is the hardware cycle counter (CCOUNT);...
Definition clock.h:248
uint32_t & _pc_micros_div_ref()
Definition clock.h:125
void pc_lat_reset(pc_latency_stat *s)
Zero a stat (min seeded high so the first sample sets it).
Definition clock.h:182
uint32_t pc_millis(void)
The library's monotonic time at 1000 Hz (milliseconds).
Definition clock.h:71
uint32_t & _pc_clock_div_ref()
Definition clock.h:53
uint32_t pc_cycles_to_ns(uint32_t delta_cycles, uint32_t cpu_mhz)
Convert a cycle-count delta to nanoseconds at cpu_mhz (the running CPU frequency, e....
Definition clock.h:263
void pc_set_micros_clock(pc_clock_fn fn, uint32_t ticks_per_second)
Install a custom microsecond clock running at ticks_per_second; the library divides it down to 1 MHz....
Definition clock.h:136
uint32_t pc_micros(void)
Monotonic microseconds - the high-resolution time base for ISR timestamps and sub-millisecond latency...
Definition clock.h:148
Rolling latency statistics in microseconds: sample count, min / max / mean, and how many samples blew...
Definition clock.h:173
uint32_t count
Definition clock.h:174
uint64_t sum_us
Definition clock.h:178
uint32_t max_us
Definition clock.h:177
uint32_t over_budget
samples whose latency exceeded the budget
Definition clock.h:175
uint32_t min_us
Definition clock.h:176