ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
preempt_queue.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 preempt_queue.h
6 * @brief User-configurable preempting work queues + high-priority processing tasks
7 * (PROTOCORE_ENABLE_PREEMPT_QUEUE) - the v5 real-time ingest primitive.
8 *
9 * Fixed-capacity queues, each feeding one dedicated, core-pinned task. A producer posts
10 * a fixed-size item; the scheduler **preempts** the lower-priority producer the instant
11 * the item lands so it is processed immediately instead of on the next tick. Producers
12 * post from a task (back or front, with a wait timeout) or from an ISR (interrupt-safe,
13 * with an immediate context-switch request). Each processing task pops items in order
14 * and hands each to a user handler.
15 *
16 * **Named lanes.** There are several queues, addressed by @ref protocore_pq_lane:
17 * - `PROTOCORE_PQ_LANE_USER` - the single lane exposed to the application. The no-lane
18 * `protocore_pq_*` API drives it. Lowest priority.
19 * - `PROTOCORE_PQ_LANE_DMA` / `_FORWARD` / `_DEVICE` - internal lanes for the library's own
20 * real-time work (DMA peripheral transfers, interface forwarding, device access).
21 * They run **above** the user lane (base `PROTOCORE_PQ_INTERNAL_PRIORITY`, DMA highest),
22 * so internal ingest always preempts user work; and below the network stack's own
23 * tasks so networking is never starved.
24 *
25 * This is the single normalized pipe for "hardware event -> process now": a DMA-complete
26 * / GPIO / bus ISR posts a descriptor onto its lane, the lane's task drains it. Zero-heap
27 * queue storage (static, compile-time PROTOCORE_PQ_DEPTH x PROTOCORE_PQ_ITEM_SIZE per lane; a
28 * task's stack is created only when its lane starts, so unused lanes cost only their queue
29 * storage), fail-closed on a full queue, no hot-path locks - so latency stays bounded.
30 *
31 * A build with no task backend posts into the same fixed per-lane ring, and
32 * protocore_pq_drain[_lane]() runs the handler over what is queued, so the logic is
33 * host-testable and behaves identically to the device's draining task.
34 *
35 * @author Douglas Quigg (dstroy0)
36 * @date 2026
37 */
38
39#ifndef PROTOCORE_PREEMPT_QUEUE_H
40#define PROTOCORE_PREEMPT_QUEUE_H
41
42#include "protocore_config.h" // the entry point: protocore_types.h for the widths
43
44#if PROTOCORE_ENABLE_PREEMPT_QUEUE
45
47
48/**
49 * @brief The preempting lanes, ordered by role. The USER lane is exposed to the
50 * application; the internal lanes run at a higher priority (DMA highest) so
51 * internal ingest preempts user work.
52 */
53typedef enum PROTO_ENUM_PACKED
54{
55 PROTOCORE_PQ_LANE_USER = 0, ///< exposed to the app (no-lane API); lowest priority
56 PROTOCORE_PQ_LANE_DMA, ///< internal: DMA peripheral transfers (highest)
57 PROTOCORE_PQ_LANE_FORWARD, ///< internal: interface forwarding
58 PROTOCORE_PQ_LANE_DEVICE, ///< internal: device access
59 PROTOCORE_PQ_LANE_COUNT
60} protocore_pq_lane;
61
62/**
63 * @brief Handler a lane's processing task invokes for each dequeued item.
64 * @param item pointer to PROTOCORE_PQ_ITEM_SIZE bytes (the posted item).
65 * @param ctx the opaque pointer passed to protocore_pq_start[_lane]().
66 */
67typedef void (*protocore_pq_handler)(const void *item, void *ctx);
68
69/**
70 * @brief What a lane does with an item, and where it ranks.
71 *
72 * The priority is the QoS: it is what makes a post preempt lower-ranked work instead of waiting
73 * for it, so it belongs to the lane rather than to whichever task happens to drain it.
74 */
75typedef struct
76{
77 protocore_pq_handler handler; ///< Called once per dequeued item (required).
78 void *ctx; ///< Opaque, forwarded to @ref handler.
79 uint8_t priority; ///< Lane priority; 0 = the lane's default (internal ranks above user).
80 uint8_t core; ///< Core to pin the lane's worker to; ignored where there is one.
81 const char *name; ///< Lane name (debug); may be NULL.
82} protocore_pq_config;
83
84/** @brief What one post carries, and how long it may wait for room. */
85typedef struct
86{
87 const void *item; ///< PROTOCORE_PQ_ITEM_SIZE bytes to copy onto the lane
88 uint32_t timeout_ticks; ///< how long a post may block; 0 returns at once when the lane is full
89} PqPostArgs;
90
91/**
92 * @brief The preempting work queues.
93 *
94 * A caller names the lane, sets the members a call takes, invokes it through ::PreemptQueue, and
95 * reads the outcome off the same handle. The queue storage and the tasks are behind @ref internal.
96 *
97 * @var PreemptQueueNs::lane the lane every call names
98 * @var PreemptQueueNs::cfg what starting a lane installs
99 * @var PreemptQueueNs::post_args what a post carries
100 * @var PreemptQueueNs::ok a call's true/false outcome
101 * @var PreemptQueueNs::n the peak depth a high-water read reports
102 * @var PreemptQueueNs::u8 the priority a lookup reports
103 * @var PreemptQueueNs::post_from_isr post from an ISR, then hand the CPU to the lane if it outranks us
104 * @var PreemptQueueNs::post_urgent post to the front of the lane, ahead of what is already queued
105 * @var PreemptQueueNs::high_water the most items ever queued at once, as a sizing aid
106 * @var PreemptQueueNs::priority the lane's default task priority
107 * @var PreemptQueueNs::running the lane's task is up
108 * @var PreemptQueueNs::start create the queue and spawn the lane's task
109 * @var PreemptQueueNs::post post to the back of the lane
110 * @var PreemptQueueNs::drain run the handler over what is queued, where no task does it
111 * @var PreemptQueueNs::stop stop the lane's task
112 */
113typedef struct
114{
115 protocore_pq_lane lane;
116 const protocore_pq_config *cfg;
117 PqPostArgs post_args;
118 proto_bool ok;
119 size_t n;
120 uint8_t u8;
121} PreemptQueueVars;
122
123/** @brief The operands and the outcome. */
124extern PreemptQueueVars PreemptQueueV;
125
126/** @brief The entries. */
127typedef struct
128{
129 void (*const post_from_isr)(uint8_t *work);
130 void (*const post_urgent)(uint8_t *work);
131 void (*const high_water)(uint8_t *work);
132 void (*const priority)(uint8_t *work);
133 void (*const running)(uint8_t *work);
134 void (*const start)(uint8_t *work);
135 void (*const post)(uint8_t *work);
136 void (*const drain)(uint8_t *work);
137 void (*const stop)(uint8_t *work);
138} PreemptQueueNs;
139
140// What the table binds, defined once in the .c and taking one parameter each: everything
141// else an entry needs is an operand in PreemptQueueV or a region of the borrow at a fixed offset.
142void protocore_preempt_queue_post_from_isr(uint8_t *work);
143void protocore_preempt_queue_post_urgent(uint8_t *work);
144void protocore_preempt_queue_high_water(uint8_t *work);
145void protocore_preempt_queue_priority(uint8_t *work);
146void protocore_preempt_queue_running(uint8_t *work);
147void protocore_preempt_queue_start(uint8_t *work);
148void protocore_preempt_queue_post(uint8_t *work);
149void protocore_preempt_queue_drain(uint8_t *work);
150void protocore_preempt_queue_stop(uint8_t *work);
151
152// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
153// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
154// `PreemptQueue.post_from_isr(work)` resolves to a named function and becomes a DIRECT call. An extern table
155// leaves the call indirect and the symbol live at every level, -O2 -flto included.
156static const PreemptQueueNs PreemptQueue __attribute__((unused)) = {
157 .post_from_isr = protocore_preempt_queue_post_from_isr,
158 .post_urgent = protocore_preempt_queue_post_urgent,
159 .high_water = protocore_preempt_queue_high_water,
160 .priority = protocore_preempt_queue_priority,
161 .running = protocore_preempt_queue_running,
162 .start = protocore_preempt_queue_start,
163 .post = protocore_preempt_queue_post,
164 .drain = protocore_preempt_queue_drain,
165 .stop = protocore_preempt_queue_stop,
166};
167
168/**
169 * @brief The PROTOCORE_PREEMPT_QUEUE_BORROW bytes this module's state lives in.
170 *
171 * Stated beside the namespace rather than on it: an entry takes a borrow, and this is where
172 * that borrow comes from. Taken once from the end of the pool, which no mark and no release
173 * walks, so the state lasts the life of the program.
174 *
175 * @return the span.
176 */
177uint8_t *protocore_preempt_queue_span(void);
178
179// --- User-lane API (drives PROTOCORE_PQ_LANE_USER) --------------------------------------------
180
181/** @brief Start the USER lane. */
182PROTOCORE_INLINE proto_bool protocore_pq_start(const protocore_pq_config *cfg)
183{
184 PreemptQueueV.lane = PROTOCORE_PQ_LANE_USER;
185 PreemptQueueV.cfg = cfg;
186 PreemptQueue.start(protocore_preempt_queue_span());
187 return PreemptQueueV.ok;
188}
189/** @brief Post to the back of the USER lane. */
190PROTOCORE_INLINE proto_bool protocore_pq_post(const void *item, uint32_t timeout_ticks)
191{
192 PreemptQueueV.lane = PROTOCORE_PQ_LANE_USER;
193 PreemptQueueV.post_args.item = item;
194 PreemptQueueV.post_args.timeout_ticks = timeout_ticks;
195 PreemptQueue.post(protocore_preempt_queue_span());
196 return PreemptQueueV.ok;
197}
198/** @brief Post to the front of the USER lane (urgent). */
199PROTOCORE_INLINE proto_bool protocore_pq_post_urgent(const void *item, uint32_t timeout_ticks)
200{
201 PreemptQueueV.lane = PROTOCORE_PQ_LANE_USER;
202 PreemptQueueV.post_args.item = item;
203 PreemptQueueV.post_args.timeout_ticks = timeout_ticks;
204 PreemptQueue.post_urgent(protocore_preempt_queue_span());
205 return PreemptQueueV.ok;
206}
207/** @brief Post to the USER lane from an ISR. */
208PROTOCORE_INLINE proto_bool protocore_pq_post_from_isr(const void *item)
209{
210 PreemptQueueV.lane = PROTOCORE_PQ_LANE_USER;
211 PreemptQueueV.post_args.item = item;
212 PreemptQueue.post_from_isr(protocore_preempt_queue_span());
213 return PreemptQueueV.ok;
214}
215/** @brief Drain the USER lane (host / inline drive). */
216PROTOCORE_INLINE void protocore_pq_drain(void)
217{
218 PreemptQueueV.lane = PROTOCORE_PQ_LANE_USER;
219 PreemptQueue.drain(protocore_preempt_queue_span());
220}
221/** @brief Stop the USER lane's task. */
222PROTOCORE_INLINE void protocore_pq_stop(void)
223{
224 PreemptQueueV.lane = PROTOCORE_PQ_LANE_USER;
225 PreemptQueue.stop(protocore_preempt_queue_span());
226}
227/** @brief True while the USER lane's task is running. */
228PROTOCORE_INLINE proto_bool protocore_pq_running(void)
229{
230 PreemptQueueV.lane = PROTOCORE_PQ_LANE_USER;
231 PreemptQueue.running(protocore_preempt_queue_span());
232 return PreemptQueueV.ok;
233}
234/** @brief Peak items ever queued on the USER lane. */
235PROTOCORE_INLINE size_t protocore_pq_high_water(void)
236{
237 PreemptQueueV.lane = PROTOCORE_PQ_LANE_USER;
238 PreemptQueue.high_water(protocore_preempt_queue_span());
239 return PreemptQueueV.n;
240}
241
243
244#endif // PROTOCORE_ENABLE_PREEMPT_QUEUE
245
246#endif // PROTOCORE_PREEMPT_QUEUE_H
PROTO_ENUM_PACKED
Application protocol spoken on a listener port or connection slot.
#define PROTOCORE_INLINE
Linkage for a leaf primitive whose body is cheaper than the call that reaches it.
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
_Bool proto_bool
The truth value.
Definition types.h:64
#define PROTOCORE_END_DECLS
Definition types.h:97