DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
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 * DETWS_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. det_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 det_dma_hw_* hooks a real silicon driver overrides.
14 */
15
16#include "services/dma/dma.h"
17
18#if DETWS_ENABLE_DMA
19
20#include <string.h> // memcpy
21
22#ifdef ARDUINO
23#include "services/clock.h" // detws_millis()
24#endif
25
26namespace
27{
28uint32_t dma_now()
29{
30#ifdef ARDUINO
31 return detws_millis();
32#else
33 return 0; // host builds have no clock dependency; t_ms is informational
34#endif
35}
36} // namespace
37
38#if DETWS_DMA_SIMULATE
39
40// Ingress/egress staging holds a few buffers' worth so a single feed can span more than
41// one RX transfer (exercising the ping-pong flip) and several TX submits can accumulate
42// before capture.
43#define DMA_STAGE_CAP (DETWS_DMA_BUF_SIZE * 3)
44
45namespace
46{
47// Fixed-capacity byte FIFO (no heap): the simulator's ingress and egress staging.
48struct byte_ring
49{
50 uint8_t buf[DMA_STAGE_CAP];
51 uint16_t head; // read cursor
52 uint16_t len; // bytes queued
53
54 void reset()
55 {
56 head = 0;
57 len = 0;
58 }
59 uint16_t space() const
60 {
61 return (uint16_t)(DMA_STAGE_CAP - len);
62 }
63 // Append n bytes; fail-closed (append nothing) if they would not all fit.
64 bool push(const uint8_t *p, uint16_t n)
65 {
66 if (n > space())
67 return false;
68 for (uint16_t i = 0; i < n; i++)
69 {
70 buf[(head + len) % DMA_STAGE_CAP] = p[i];
71 len++;
72 }
73 return true;
74 }
75 // Pop up to max bytes into out; returns how many.
76 uint16_t pop(uint8_t *out, uint16_t max)
77 {
78 uint16_t n = (len < max) ? len : max;
79 for (uint16_t i = 0; i < n; i++)
80 {
81 out[i] = buf[head];
82 head = (head + 1) % DMA_STAGE_CAP;
83 len--;
84 }
85 return n;
86 }
87};
88
89struct dma_channel
90{
91 uint8_t rx_buf[2][DETWS_DMA_BUF_SIZE]; // ping-pong RX
92 uint8_t tx_buf[DETWS_DMA_BUF_SIZE]; // egress staging
93 byte_ring ingress; // sim: bytes arriving on the RX line
94 byte_ring egress; // sim: bytes transmitted via egress DMA
95 det_dma_cb cb;
96 void *ctx;
97 uint16_t rx_fill; // bytes in the active RX buffer since the last completion
98 uint16_t tx_len; // bytes pending egress (0 = idle)
99 uint16_t seq; // completion sequence
100 uint8_t rx_active; // which ping-pong buffer the engine is filling
101 det_dma_periph periph;
102 bool loopback;
103 bool tx_busy;
104 bool open;
105};
106
107// All DMA simulator state, owned by one instance (internal linkage): the channel table,
108// so it is one named owner, unreachable from any other translation unit.
109struct DmaCtx
110{
111 dma_channel ch[DETWS_DMA_CHANNELS];
112};
113DmaCtx s_dma;
114
115void emit(dma_channel &c, uint8_t id, det_dma_dir dir, const uint8_t *data, uint16_t len)
116{
117 det_dma_event ev;
118 ev.data = data;
119 ev.t_ms = dma_now();
120 ev.len = len;
121 ev.seq = c.seq++;
122 ev.channel = id;
123 ev.periph = c.periph;
124 ev.dir = dir;
125 ev._pad = 0;
126 if (c.cb)
127 c.cb(&ev, c.ctx);
128}
129
130// Complete whatever is queued for the channel: drain egress (TX), route loopback back
131// into ingress, then feed ingress into the ping-pong RX buffers, emitting one event per
132// full buffer and a final partial event (models the UART idle-line flush) so every poll
133// delivers all fed bytes.
134void pump(dma_channel &c, uint8_t id)
135{
136 if (c.tx_busy)
137 {
138 if (c.loopback)
139 c.ingress.push(c.tx_buf, c.tx_len); // internal TX->RX jumper
140 c.egress.push(c.tx_buf, c.tx_len); // capture (best-effort)
141 uint16_t sent = c.tx_len;
142 c.tx_busy = false;
143 c.tx_len = 0;
144 emit(c, id, det_dma_dir::DET_DMA_TX, nullptr, sent);
145 }
146
147 while (c.ingress.len > 0)
148 {
149 uint16_t room = (uint16_t)(DETWS_DMA_BUF_SIZE - c.rx_fill);
150 uint16_t got = c.ingress.pop(c.rx_buf[c.rx_active] + c.rx_fill, room);
151 c.rx_fill += got;
152 if (c.rx_fill == DETWS_DMA_BUF_SIZE) // buffer full -> complete + ping-pong flip
153 {
154 emit(c, id, det_dma_dir::DET_DMA_RX, c.rx_buf[c.rx_active], DETWS_DMA_BUF_SIZE);
155 c.rx_active ^= 1;
156 c.rx_fill = 0;
157 }
158 }
159 if (c.rx_fill > 0) // idle-line flush of the trailing partial buffer
160 {
161 emit(c, id, det_dma_dir::DET_DMA_RX, c.rx_buf[c.rx_active], c.rx_fill);
162 c.rx_active ^= 1;
163 c.rx_fill = 0;
164 }
165}
166} // namespace
167
168bool det_dma_open(const det_dma_config *cfg)
169{
170 if (!cfg || !cfg->on_complete || cfg->channel >= DETWS_DMA_CHANNELS)
171 return false;
172 dma_channel &c = s_dma.ch[cfg->channel];
173 if (c.open)
174 return false;
175 c.ingress.reset();
176 c.egress.reset();
177 c.cb = cfg->on_complete;
178 c.ctx = cfg->ctx;
179 c.rx_fill = 0;
180 c.tx_len = 0;
181 c.seq = 0;
182 c.rx_active = 0;
183 c.periph = cfg->periph;
184 c.loopback = cfg->loopback;
185 c.tx_busy = false;
186 c.open = true;
187 return true;
188}
189
190bool det_dma_tx_submit(uint8_t ch, const uint8_t *buf, uint16_t len)
191{
192 if (ch >= DETWS_DMA_CHANNELS || !buf || len == 0 || len > DETWS_DMA_BUF_SIZE)
193 return false;
194 dma_channel &c = s_dma.ch[ch];
195 if (!c.open || c.tx_busy) // one transfer in flight at a time (fail-closed)
196 return false;
197 memcpy(c.tx_buf, buf, len);
198 c.tx_len = len;
199 c.tx_busy = true;
200 return true;
201}
202
203void det_dma_close(uint8_t ch)
204{
205 if (ch >= DETWS_DMA_CHANNELS)
206 return;
207 s_dma.ch[ch].open = false;
208}
209
210void det_dma_poll(void)
211{
212 for (uint8_t i = 0; i < DETWS_DMA_CHANNELS; i++)
213 if (s_dma.ch[i].open)
214 pump(s_dma.ch[i], i);
215}
216
217bool det_dma_sim_feed(uint8_t ch, const uint8_t *bytes, uint16_t len)
218{
219 if (ch >= DETWS_DMA_CHANNELS || !bytes)
220 return false;
221 dma_channel &c = s_dma.ch[ch];
222 if (!c.open)
223 return false;
224 return c.ingress.push(bytes, len);
225}
226
227uint16_t det_dma_sim_capture(uint8_t ch, uint8_t *out, uint16_t max)
228{
229 if (ch >= DETWS_DMA_CHANNELS || !out)
230 return 0;
231 dma_channel &c = s_dma.ch[ch];
232 if (!c.open)
233 return 0;
234 return c.egress.pop(out, max);
235}
236
237#else // real silicon backend: dispatch to weak hooks a driver overrides (fail-closed).
238
239extern "C"
240{
241 __attribute__((weak)) bool det_dma_hw_open(const det_dma_config *cfg)
242 {
243 (void)cfg;
244 return false;
245 }
246 __attribute__((weak)) bool det_dma_hw_tx_submit(uint8_t ch, const uint8_t *buf, uint16_t len)
247 {
248 (void)ch;
249 (void)buf;
250 (void)len;
251 return false;
252 }
253 __attribute__((weak)) void det_dma_hw_close(uint8_t ch)
254 {
255 (void)ch;
256 }
257 __attribute__((weak)) void det_dma_hw_poll(void)
258 {
259 }
260}
261
262bool det_dma_open(const det_dma_config *cfg)
263{
264 if (!cfg || !cfg->on_complete || cfg->channel >= DETWS_DMA_CHANNELS)
265 return false;
266 return det_dma_hw_open(cfg);
267}
268
269bool det_dma_tx_submit(uint8_t ch, const uint8_t *buf, uint16_t len)
270{
271 if (ch >= DETWS_DMA_CHANNELS || !buf || len == 0 || len > DETWS_DMA_BUF_SIZE)
272 return false;
273 return det_dma_hw_tx_submit(ch, buf, len);
274}
275
276void det_dma_close(uint8_t ch)
277{
278 if (ch < DETWS_DMA_CHANNELS)
279 det_dma_hw_close(ch);
280}
281
282void det_dma_poll(void)
283{
284 det_dma_hw_poll();
285}
286
287#endif // DETWS_DMA_SIMULATE
288
289#endif // DETWS_ENABLE_DMA
#define DETWS_DMA_BUF_SIZE
Bytes per DMA transfer buffer (RX is double-buffered at this size).
#define DETWS_DMA_CHANNELS
Number of DMA channels (static-allocated; each is one peripheral link).
Pluggable monotonic clock for all library timing.
uint32_t detws_millis(void)
The library's monotonic time at 1000 Hz (milliseconds).
Definition clock.h:71
DMA peripheral ingest / egress (DETWS_ENABLE_DMA) - the v5 high-throughput hardware-ingest path.