ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
dma.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 dma.cpp
6 * @brief DMA peripheral ingest / egress - implementation.
7 *
8 * PC_DMA_SIMULATE (default) runs an in-memory model of the peripheral: an ingress
9 * staging ring feeds the ping-pong RX buffers, egress DMA drains the TX buffer into a
10 * capture ring, and a loopback channel routes its own TX back into its RX. pc_dma_poll()
11 * advances that engine and fires the completion callbacks - so the whole pipeline is
12 * host- and device-testable with no physical loopback. When the flag is 0, the front end
13 * dispatches to the weak pc_dma_hw_* hooks a real silicon driver overrides.
14 */
15
17
18#if PC_ENABLE_DMA
19
20#include <string.h> // memcpy
21
22#ifdef ARDUINO
23#include "services/system/clock.h" // pc_millis(), pc_micros()
24#endif
25
26namespace
27{
28uint32_t dma_now()
29{
30#ifdef ARDUINO
31 return pc_millis();
32#else
33 return 0; // host builds have no clock dependency; t_ms is informational
34#endif
35}
36uint32_t dma_now_us()
37{
38#ifdef ARDUINO
39 return pc_micros();
40#else
41 return 0; // host builds have no clock dependency; t_us is informational
42#endif
43}
44} // namespace
45
46#if PC_DMA_SIMULATE
47
48// Ingress/egress staging holds a few buffers' worth so a single feed can span more than
49// one RX transfer (exercising the ping-pong flip) and several TX submits can accumulate
50// before capture.
51#define DMA_STAGE_CAP (PC_DMA_BUF_SIZE * 3)
52
53namespace
54{
55// Fixed-capacity byte FIFO (no heap): the simulator's ingress and egress staging.
56struct byte_ring
57{
58 uint8_t buf[DMA_STAGE_CAP];
59 uint16_t head; // read cursor
60 uint16_t len; // bytes queued
61
62 void reset()
63 {
64 head = 0;
65 len = 0;
66 }
67 uint16_t space() const
68 {
69 return (uint16_t)(DMA_STAGE_CAP - len);
70 }
71 // Append n bytes; fail-closed (append nothing) if they would not all fit.
72 bool push(const uint8_t *p, uint16_t n)
73 {
74 if (n > space())
75 {
76 return false;
77 }
78 for (uint16_t i = 0; i < n; i++)
79 {
80 buf[(head + len) % DMA_STAGE_CAP] = p[i];
81 len++;
82 }
83 return true;
84 }
85 // Pop up to max bytes into out; returns how many.
86 uint16_t pop(uint8_t *out, uint16_t max)
87 {
88 uint16_t n = (len < max) ? len : max;
89 for (uint16_t i = 0; i < n; i++)
90 {
91 out[i] = buf[head];
92 head = (head + 1) % DMA_STAGE_CAP;
93 len--;
94 }
95 return n;
96 }
97};
98
99struct dma_channel
100{
101 uint8_t rx_buf[2][PC_DMA_BUF_SIZE]; // ping-pong RX
102 uint8_t tx_buf[PC_DMA_BUF_SIZE]; // egress staging
103 byte_ring ingress; // sim: bytes arriving on the RX line
104 byte_ring egress; // sim: bytes transmitted via egress DMA
105 pc_dma_cb cb;
106 void *ctx;
107 uint16_t rx_fill; // bytes in the active RX buffer since the last completion
108 uint16_t tx_len; // bytes pending egress (0 = idle)
109 uint16_t seq; // completion sequence
110 uint8_t rx_active; // which ping-pong buffer the engine is filling
111 pc_dma_periph periph;
112 bool loopback;
113 bool tx_busy;
114 bool open;
115};
116
117// All DMA simulator state, owned by one instance (internal linkage): the channel table,
118// so it is one named owner, unreachable from any other translation unit.
119struct DmaCtx
120{
121 dma_channel ch[PC_DMA_CHANNELS];
122};
123DmaCtx s_dma;
124
125void emit(dma_channel &c, uint8_t id, pc_dma_dir dir, const uint8_t *data, uint16_t len)
126{
127 pc_dma_event ev;
128 ev.data = data;
129 ev.t_ms = dma_now();
130 ev.t_us = dma_now_us();
131 ev.len = len;
132 ev.seq = c.seq++;
133 ev.channel = id;
134 ev.periph = c.periph;
135 ev.dir = dir;
136 ev._pad = 0;
137 if (c.cb) // GCOVR_EXCL_BR_LINE cb is guaranteed non-null while a channel is open:
138 // pc_dma_open rejects a null on_complete, and emit() only runs via pump(),
139 // which pc_dma_poll() only calls for channels with open == true.
140 {
141 c.cb(&ev, c.ctx);
142 }
143}
144
145// Complete whatever is queued for the channel: drain egress (TX), route loopback back
146// into ingress, then feed ingress into the ping-pong RX buffers, emitting one event per
147// full buffer and a final partial event (models the UART idle-line flush) so every poll
148// delivers all fed bytes.
149void pump(dma_channel &c, uint8_t id)
150{
151 if (c.tx_busy)
152 {
153 if (c.loopback)
154 {
155 c.ingress.push(c.tx_buf, c.tx_len); // internal TX->RX jumper
156 }
157 c.egress.push(c.tx_buf, c.tx_len); // capture (best-effort)
158 uint16_t sent = c.tx_len;
159 c.tx_busy = false;
160 c.tx_len = 0;
161 emit(c, id, pc_dma_dir::PC_DMA_TX, nullptr, sent);
162 }
163
164 while (c.ingress.len > 0)
165 {
166 uint16_t room = (uint16_t)(PC_DMA_BUF_SIZE - c.rx_fill);
167 uint16_t got = c.ingress.pop(c.rx_buf[c.rx_active] + c.rx_fill, room);
168 c.rx_fill += got;
169 if (c.rx_fill == PC_DMA_BUF_SIZE) // buffer full -> complete + ping-pong flip
170 {
171 emit(c, id, pc_dma_dir::PC_DMA_RX, c.rx_buf[c.rx_active], PC_DMA_BUF_SIZE);
172 c.rx_active ^= 1;
173 c.rx_fill = 0;
174 }
175 }
176 if (c.rx_fill > 0) // idle-line flush of the trailing partial buffer
177 {
178 emit(c, id, pc_dma_dir::PC_DMA_RX, c.rx_buf[c.rx_active], c.rx_fill);
179 c.rx_active ^= 1;
180 c.rx_fill = 0;
181 }
182}
183} // namespace
184
185bool pc_dma_open(const pc_dma_config *cfg)
186{
187 if (!cfg || !cfg->on_complete || cfg->channel >= PC_DMA_CHANNELS)
188 {
189 return false;
190 }
191 dma_channel &c = s_dma.ch[cfg->channel];
192 if (c.open)
193 {
194 return false;
195 }
196 c.ingress.reset();
197 c.egress.reset();
198 c.cb = cfg->on_complete;
199 c.ctx = cfg->ctx;
200 c.rx_fill = 0;
201 c.tx_len = 0;
202 c.seq = 0;
203 c.rx_active = 0;
204 c.periph = cfg->periph;
205 c.loopback = cfg->loopback;
206 c.tx_busy = false;
207 c.open = true;
208 return true;
209}
210
211bool pc_dma_tx_submit(uint8_t ch, const uint8_t *buf, uint16_t len)
212{
213 if (ch >= PC_DMA_CHANNELS || !buf || len == 0 || len > PC_DMA_BUF_SIZE)
214 {
215 return false;
216 }
217 dma_channel &c = s_dma.ch[ch];
218 if (!c.open || c.tx_busy) // one transfer in flight at a time (fail-closed)
219 {
220 return false;
221 }
222 memcpy(c.tx_buf, buf, len);
223 c.tx_len = len;
224 c.tx_busy = true;
225 return true;
226}
227
228void pc_dma_close(uint8_t ch)
229{
230 if (ch >= PC_DMA_CHANNELS)
231 {
232 return;
233 }
234 s_dma.ch[ch].open = false;
235}
236
237void pc_dma_poll(void)
238{
239 for (uint8_t i = 0; i < PC_DMA_CHANNELS; i++)
240 {
241 if (s_dma.ch[i].open)
242 {
243 pump(s_dma.ch[i], i);
244 }
245 }
246}
247
248bool pc_dma_sim_feed(uint8_t ch, const uint8_t *bytes, uint16_t len)
249{
250 if (ch >= PC_DMA_CHANNELS || !bytes)
251 {
252 return false;
253 }
254 dma_channel &c = s_dma.ch[ch];
255 if (!c.open)
256 {
257 return false;
258 }
259 return c.ingress.push(bytes, len);
260}
261
262uint16_t pc_dma_sim_capture(uint8_t ch, uint8_t *out, uint16_t max)
263{
264 if (ch >= PC_DMA_CHANNELS || !out)
265 {
266 return 0;
267 }
268 dma_channel &c = s_dma.ch[ch];
269 if (!c.open)
270 {
271 return 0;
272 }
273 return c.egress.pop(out, max);
274}
275
276#else // real silicon backend: dispatch to weak hooks a driver overrides (fail-closed).
277
278extern "C"
279{
280 __attribute__((weak)) bool pc_dma_hw_open(const pc_dma_config *cfg)
281 {
282 (void)cfg;
283 return false;
284 }
285 __attribute__((weak)) bool pc_dma_hw_tx_submit(uint8_t ch, const uint8_t *buf, uint16_t len)
286 {
287 (void)ch;
288 (void)buf;
289 (void)len;
290 return false;
291 }
292 __attribute__((weak)) void pc_dma_hw_close(uint8_t ch)
293 {
294 (void)ch;
295 }
296 __attribute__((weak)) void pc_dma_hw_poll(void)
297 {
298 }
299}
300
301bool pc_dma_open(const pc_dma_config *cfg)
302{
303 if (!cfg || !cfg->on_complete || cfg->channel >= PC_DMA_CHANNELS)
304 {
305 return false;
306 }
307 return pc_dma_hw_open(cfg);
308}
309
310bool pc_dma_tx_submit(uint8_t ch, const uint8_t *buf, uint16_t len)
311{
312 if (ch >= PC_DMA_CHANNELS || !buf || len == 0 || len > PC_DMA_BUF_SIZE)
313 {
314 return false;
315 }
316 return pc_dma_hw_tx_submit(ch, buf, len);
317}
318
319void pc_dma_close(uint8_t ch)
320{
321 if (ch < PC_DMA_CHANNELS)
322 {
323 pc_dma_hw_close(ch);
324 }
325}
326
327void pc_dma_poll(void)
328{
329 pc_dma_hw_poll();
330}
331
332#endif // PC_DMA_SIMULATE
333
334#endif // PC_ENABLE_DMA
Pluggable monotonic clock for all library timing.
uint32_t pc_millis(void)
The library's monotonic time at 1000 Hz (milliseconds).
Definition clock.h:71
uint32_t pc_micros(void)
Monotonic microseconds - the high-resolution time base for ISR timestamps and sub-millisecond latency...
Definition clock.h:148
DMA peripheral ingest / egress (PC_ENABLE_DMA) - the v5 high-throughput hardware-ingest path.
#define PC_DMA_BUF_SIZE
Bytes per DMA transfer buffer (RX is double-buffered at this size).
#define PC_DMA_CHANNELS
Number of DMA channels (static-allocated; each is one peripheral link).