ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
preempt_queue.cpp
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 preempt_queue.cpp
6 * @brief Preempting work queues + high-priority processing tasks - implementation.
7 *
8 * One queue + one task per lane (pc_pq_lane::PC_PQ_LANE_COUNT lanes). The no-lane pc_pq_* API
9 * lives in the header and forwards to the USER lane. Internal lanes default to a higher
10 * priority than the user lane so internal ingest preempts user work.
11 */
12
14
15#if PC_ENABLE_PREEMPT_QUEUE
16
17#include <string.h>
18
19#if defined(ARDUINO)
20#include "freertos/FreeRTOS.h"
21#include "freertos/queue.h"
22#include "freertos/task.h"
23#endif
24namespace
25{
26// Common preempt-queue state (both host + device), owned by one instance (internal linkage):
27// the per-lane handler, its context, and the high-water mark. The backend-specific state (the
28// FreeRTOS queue/task on device, the ring on host) lives in its own owner where its types are
29// in scope. One named owner, unreachable from any other translation unit.
30struct PqCtx
31{
32 pc_pq_handler handler[(size_t)pc_pq_lane::PC_PQ_LANE_COUNT] = {};
33 void *ctx[(size_t)pc_pq_lane::PC_PQ_LANE_COUNT] = {};
34 size_t high_water[(size_t)pc_pq_lane::PC_PQ_LANE_COUNT] = {}; // peak items queued at once (sizing aid)
35};
36PqCtx s_pq;
37
38// GCOVR_EXCL_START ESP32-only: only used to name the FreeRTOS task in the ARDUINO pc_pq_start_lane;
39// the coverage host never starts tasks, and this is internal-linkage (anon namespace) so not callable.
40const char *lane_name(pc_pq_lane lane)
41{
42 switch (lane)
43 {
44 case pc_pq_lane::PC_PQ_LANE_DMA:
45 return "pc_pq_dma";
46 case pc_pq_lane::PC_PQ_LANE_FORWARD:
47 return "pc_pq_fwd";
48 case pc_pq_lane::PC_PQ_LANE_DEVICE:
49 return "pc_pq_dev";
50 default:
51 return "pc_pq_user";
52 }
53}
54// GCOVR_EXCL_STOP
55
56bool lane_ok(pc_pq_lane lane)
57{
58 return (unsigned)lane < (unsigned)pc_pq_lane::PC_PQ_LANE_COUNT;
59}
60} // namespace
61
62// Default task priority per lane: internal lanes rank above the user lane (DMA highest),
63// staying below the lwIP tcpip (18) / WiFi tasks so networking is never starved.
64uint8_t pc_pq_lane_priority(pc_pq_lane lane)
65{
66 switch (lane)
67 {
68 case pc_pq_lane::PC_PQ_LANE_DMA:
69 return (uint8_t)(PC_PQ_INTERNAL_PRIORITY + 2);
70 case pc_pq_lane::PC_PQ_LANE_FORWARD:
71 return (uint8_t)(PC_PQ_INTERNAL_PRIORITY + 1);
72 case pc_pq_lane::PC_PQ_LANE_DEVICE:
73 return (uint8_t)(PC_PQ_INTERNAL_PRIORITY);
74 case pc_pq_lane::PC_PQ_LANE_USER:
75 default:
76 return 5; // used only when a config passes priority 0; kept below the internal lanes
77 }
78}
79
80#ifdef ARDUINO
81
82namespace
83{
84// All FreeRTOS-backend state, owned by one instance (internal linkage): the static queue
85// storage/control blocks, the queue + task handles, and the per-lane run flag. One named
86// owner, unreachable from any other translation unit.
87struct PqQueueCtx
88{
89 StaticQueue_t q_struct[(size_t)pc_pq_lane::PC_PQ_LANE_COUNT];
90 uint8_t q_storage[(size_t)pc_pq_lane::PC_PQ_LANE_COUNT][PC_PQ_DEPTH * PC_PQ_ITEM_SIZE];
91 QueueHandle_t q[(size_t)pc_pq_lane::PC_PQ_LANE_COUNT] = {};
92 TaskHandle_t task[(size_t)pc_pq_lane::PC_PQ_LANE_COUNT] = {};
93 volatile bool run[(size_t)pc_pq_lane::PC_PQ_LANE_COUNT] = {};
94};
95PqQueueCtx s_pqq;
96
97void note_depth(pc_pq_lane lane, UBaseType_t waiting)
98{
99 if ((size_t)waiting > s_pq.high_water[(size_t)lane])
100 {
101 s_pq.high_water[(size_t)lane] = (size_t)waiting;
102 }
103}
104
105// The dedicated processing task for one lane (its id is the task parameter): block until
106// an item lands (so a post preempts straight into here), then run the handler for each
107// item in order. It blocks forever between items (zero idle wakeups).
108void pq_task(void *arg)
109{
110 pc_pq_lane lane = static_cast<pc_pq_lane>(reinterpret_cast<uintptr_t>(arg));
111 uint8_t item[PC_PQ_ITEM_SIZE];
112 for (;;)
113 {
114 if (xQueueReceive(s_pqq.q[(size_t)lane], item, portMAX_DELAY) == pdTRUE && s_pq.handler[(size_t)lane])
115 {
116 s_pq.handler[(size_t)lane](item, s_pq.ctx[(size_t)lane]);
117 }
118 }
119}
120} // namespace
121
122bool pc_pq_start_lane(pc_pq_lane lane, const pc_pq_config *cfg)
123{
124 if (!lane_ok(lane) || s_pqq.run[(size_t)lane] || !cfg || !cfg->handler)
125 {
126 return false;
127 }
128 s_pq.handler[(size_t)lane] = cfg->handler;
129 s_pq.ctx[(size_t)lane] = cfg->ctx;
130 s_pq.high_water[(size_t)lane] = 0;
131 if (!s_pqq.q[(size_t)lane])
132 {
133 s_pqq.q[(size_t)lane] = xQueueCreateStatic(PC_PQ_DEPTH, PC_PQ_ITEM_SIZE, s_pqq.q_storage[(size_t)lane],
134 &s_pqq.q_struct[(size_t)lane]);
135 }
136 if (!s_pqq.q[(size_t)lane])
137 {
138 return false;
139 }
140 s_pqq.run[(size_t)lane] = true;
141 uint8_t prio = cfg->priority ? cfg->priority : pc_pq_lane_priority(lane);
142 int core = cfg->core % portNUM_PROCESSORS;
143 if (xTaskCreatePinnedToCore(pq_task, cfg->name ? cfg->name : lane_name(lane), PC_PQ_STACK, (void *)(uintptr_t)lane,
144 prio, &s_pqq.task[(size_t)lane], core) != pdPASS)
145 {
146 s_pqq.run[(size_t)lane] = false;
147 return false;
148 }
149 return true;
150}
151
152bool pc_pq_post_lane(pc_pq_lane lane, const void *item, uint32_t timeout_ticks)
153{
154 if (!lane_ok(lane) || !s_pqq.q[(size_t)lane] || !item)
155 {
156 return false;
157 }
158 if (xQueueSendToBack(s_pqq.q[(size_t)lane], item, (TickType_t)timeout_ticks) != pdTRUE)
159 {
160 return false;
161 }
162 note_depth(lane, uxQueueMessagesWaiting(s_pqq.q[(size_t)lane]));
163 return true;
164}
165
166bool pc_pq_post_lane_urgent(pc_pq_lane lane, const void *item, uint32_t timeout_ticks)
167{
168 if (!lane_ok(lane) || !s_pqq.q[(size_t)lane] || !item)
169 {
170 return false;
171 }
172 if (xQueueSendToFront(s_pqq.q[(size_t)lane], item, (TickType_t)timeout_ticks) != pdTRUE)
173 {
174 return false;
175 }
176 note_depth(lane, uxQueueMessagesWaiting(s_pqq.q[(size_t)lane]));
177 return true;
178}
179
180bool pc_pq_post_lane_from_isr(pc_pq_lane lane, const void *item)
181{
182 if (!lane_ok(lane) || !s_pqq.q[(size_t)lane] || !item)
183 {
184 return false;
185 }
186 BaseType_t woke = pdFALSE;
187 if (xQueueSendToBackFromISR(s_pqq.q[(size_t)lane], item, &woke) != pdTRUE)
188 {
189 return false;
190 }
191 note_depth(lane, uxQueueMessagesWaitingFromISR(s_pqq.q[(size_t)lane]));
192 portYIELD_FROM_ISR(woke); // switch to the processing task now if it outranks us
193 return true;
194}
195
196void pc_pq_drain_lane(pc_pq_lane)
197{
198 // The lane's task drains on device; nothing to do here.
199}
200
201void pc_pq_stop_lane(pc_pq_lane lane)
202{
203 if (!lane_ok(lane))
204 {
205 return;
206 }
207 s_pqq.run[(size_t)lane] = false;
208 if (s_pqq.task[(size_t)lane]) // the task blocks on the queue (portMAX_DELAY), so delete it directly
209 {
210 vTaskDelete(s_pqq.task[(size_t)lane]);
211 s_pqq.task[(size_t)lane] = nullptr;
212 }
213}
214
215bool pc_pq_running_lane(pc_pq_lane lane)
216{
217 return lane_ok(lane) && s_pqq.run[(size_t)lane];
218}
219
220size_t pc_pq_high_water_lane(pc_pq_lane lane)
221{
222 return lane_ok(lane) ? s_pq.high_water[(size_t)lane] : 0;
223}
224
225#else // host build - fixed per-lane rings, no tasks; pc_pq_drain_lane() runs the handler
226
227namespace
228{
229// All host-backend state, owned by one instance (internal linkage): the per-lane ring buffer,
230// its head/tail/count cursors, and the started flag. One named owner, unreachable cross-TU.
231struct PqRingCtx
232{
233 uint8_t buf[(size_t)pc_pq_lane::PC_PQ_LANE_COUNT][PC_PQ_DEPTH * PC_PQ_ITEM_SIZE];
234 size_t head[(size_t)pc_pq_lane::PC_PQ_LANE_COUNT] = {}; // next write slot
235 size_t tail[(size_t)pc_pq_lane::PC_PQ_LANE_COUNT] = {}; // next read slot
236 size_t count[(size_t)pc_pq_lane::PC_PQ_LANE_COUNT] = {};
237 bool started[(size_t)pc_pq_lane::PC_PQ_LANE_COUNT] = {};
238};
239PqRingCtx s_pqr;
240
241void note_count(pc_pq_lane lane)
242{
243 if (s_pqr.count[(size_t)lane] > s_pq.high_water[(size_t)lane])
244 {
245 s_pq.high_water[(size_t)lane] = s_pqr.count[(size_t)lane];
246 }
247}
248} // namespace
249
250bool pc_pq_start_lane(pc_pq_lane lane, const pc_pq_config *cfg)
251{
252 if (!lane_ok(lane) || s_pqr.started[(size_t)lane] || !cfg || !cfg->handler)
253 {
254 return false;
255 }
256 s_pq.handler[(size_t)lane] = cfg->handler;
257 s_pq.ctx[(size_t)lane] = cfg->ctx;
258 s_pqr.head[(size_t)lane] = 0;
259 s_pqr.tail[(size_t)lane] = 0;
260 s_pqr.count[(size_t)lane] = 0;
261 s_pq.high_water[(size_t)lane] = 0;
262 s_pqr.started[(size_t)lane] = true;
263 return true;
264}
265
266bool pc_pq_post_lane(pc_pq_lane lane, const void *item, uint32_t)
267{
268 if (!lane_ok(lane) || !item || s_pqr.count[(size_t)lane] >= PC_PQ_DEPTH)
269 {
270 return false; // fail closed when full
271 }
272 memcpy(s_pqr.buf[(size_t)lane] + s_pqr.head[(size_t)lane] * PC_PQ_ITEM_SIZE, item, PC_PQ_ITEM_SIZE);
273 s_pqr.head[(size_t)lane] = (s_pqr.head[(size_t)lane] + 1) % PC_PQ_DEPTH;
274 s_pqr.count[(size_t)lane]++;
275 note_count(lane);
276 return true;
277}
278
279bool pc_pq_post_lane_urgent(pc_pq_lane lane, const void *item, uint32_t)
280{
281 if (!lane_ok(lane) || !item || s_pqr.count[(size_t)lane] >= PC_PQ_DEPTH)
282 {
283 return false;
284 }
285 s_pqr.tail[(size_t)lane] =
286 (s_pqr.tail[(size_t)lane] + PC_PQ_DEPTH - 1) % PC_PQ_DEPTH; // step the read cursor back, write there
287 memcpy(s_pqr.buf[(size_t)lane] + s_pqr.tail[(size_t)lane] * PC_PQ_ITEM_SIZE, item, PC_PQ_ITEM_SIZE);
288 s_pqr.count[(size_t)lane]++;
289 note_count(lane);
290 return true;
291}
292
293bool pc_pq_post_lane_from_isr(pc_pq_lane lane, const void *item)
294{
295 return pc_pq_post_lane(lane, item, 0); // no ISRs on host
296}
297
298void pc_pq_drain_lane(pc_pq_lane lane)
299{
300 if (!lane_ok(lane))
301 {
302 return;
303 }
304 while (s_pqr.count[(size_t)lane] > 0)
305 {
306 if (s_pq.handler[(size_t)lane])
307 {
308 s_pq.handler[(size_t)lane](s_pqr.buf[(size_t)lane] + s_pqr.tail[(size_t)lane] * PC_PQ_ITEM_SIZE,
309 s_pq.ctx[(size_t)lane]);
310 }
311 s_pqr.tail[(size_t)lane] = (s_pqr.tail[(size_t)lane] + 1) % PC_PQ_DEPTH;
312 s_pqr.count[(size_t)lane]--;
313 }
314}
315
316void pc_pq_stop_lane(pc_pq_lane lane)
317{
318 if (lane_ok(lane))
319 {
320 s_pqr.started[(size_t)lane] = false;
321 }
322}
323
324bool pc_pq_running_lane(pc_pq_lane lane)
325{
326 return lane_ok(lane) && s_pqr.started[(size_t)lane];
327}
328
329size_t pc_pq_high_water_lane(pc_pq_lane lane)
330{
331 return lane_ok(lane) ? s_pq.high_water[(size_t)lane] : 0;
332}
333
334#endif // ARDUINO
335
336#endif // PC_ENABLE_PREEMPT_QUEUE
User-configurable preempting work queues + high-priority processing tasks (PC_ENABLE_PREEMPT_QUEUE) -...
#define PC_PQ_ITEM_SIZE
Bytes per preempting-queue item (the posted item must fit).
#define PC_PQ_INTERNAL_PRIORITY
Base FreeRTOS priority for the internal preempting lanes (DMA / forwarding / device access)....
#define PC_PQ_DEPTH
Capacity of the preempting queue in items (static-allocated).
#define PC_PQ_STACK
Stack (bytes) for each preempting-queue processing task (ESP32).