ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
dma.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 dma.h
6 * @brief DMA peripheral ingest / egress (PC_ENABLE_DMA) - the v5 high-throughput
7 * hardware-ingest path.
8 *
9 * A DMA channel moves bytes between a peripheral (UART / I2C / SPI) and a static
10 * buffer while the CPU is free, then a DMA-complete event carries the result up.
11 * Two directions:
12 *
13 * - RX (ingress): peripheral -> DMA -> buffer. On completion the channel emits a
14 * @ref pc_dma_event whose `data`/`len` point at the just-filled buffer. RX is
15 * **double-buffered (ping-pong)**: the completed buffer is handed to the callback
16 * while the engine fills the other, so there is a full transfer of headroom to
17 * consume it before it is reused.
18 * - TX (egress): buffer -> DMA -> peripheral. The caller submits bytes; on
19 * completion the channel emits a TX event (data == nullptr) so the producer knows
20 * the buffer is free to refill.
21 *
22 * The completion callback runs in ISR context on real silicon, so keep it tiny - the
23 * intended pattern is to post the event into the preempting work queue
24 * (services/system/preempt_queue) with pc_pq_post_from_isr(), letting a high-priority task
25 * do the real work off the interrupt. pc_dma stays decoupled from the queue: it just
26 * hands you the event.
27 *
28 * **Simulator (PC_DMA_SIMULATE, default on).** With no physical loopback wire the
29 * transfers run through an in-memory model of the peripheral: pc_dma_sim_feed() injects
30 * bytes as if they arrived on the RX line, pc_dma_sim_capture() reads back what egress
31 * DMA "transmitted", and a channel opened with `loopback` feeds its own TX egress into
32 * its RX ingress (an internal jumper). pc_dma_poll() advances the engine and fires the
33 * completions. This exercises the entire ingress -> event -> (queue) -> handler and
34 * produce -> egress pipeline identically on the host bench and on-device. It is the
35 * shipped, tested backend; a real silicon driver plugs into pc_dma_hw_* when the flag
36 * is 0 (see docs/KNOWN_LIMITATIONS.md).
37 *
38 * Zero-heap: PC_DMA_CHANNELS channels, each with 2x PC_DMA_BUF_SIZE RX + one TX
39 * buffer + the simulator's ingress/egress staging, all static and compile-time sized.
40 * Fail-closed: a submit onto a busy channel or a feed past the staging capacity returns
41 * false rather than blocking or overrunning.
42 *
43 * @author Douglas Quigg (dstroy0)
44 * @date 2026
45 */
46
47#ifndef PROTOCORE_DMA_H
48#define PROTOCORE_DMA_H
49
50#include "protocore_config.h"
51
52#if PC_ENABLE_DMA
53
54#include <stddef.h>
55#include <stdint.h>
56
57/** @brief Peripheral a channel is bound to (informational; selects the real backend). */
58enum class pc_dma_periph : uint8_t
59{
60 PC_DMA_UART = 0,
61 PC_DMA_I2C = 1,
62 PC_DMA_SPI = 2,
63};
64
65/** @brief Transfer direction carried on a completion event. */
66enum class pc_dma_dir : uint8_t
67{
68 PC_DMA_RX = 0, ///< ingress: peripheral -> buffer
69 PC_DMA_TX = 1, ///< egress: buffer -> peripheral
70};
71
72/**
73 * @brief A DMA-complete event handed to the channel callback.
74 *
75 * For RX, @ref data / @ref len point at the completed ping-pong buffer - valid only
76 * until that buffer is reused (a transfer or two later). Consume it inside the callback,
77 * or, if you hand the work to another task (e.g. post it to the preempting queue), copy
78 * the @ref len bytes into the posted item: a deferred consumer can lag past the buffer's
79 * reuse under load, so post the bytes, not this pointer. For TX, @ref data is nullptr and
80 * @ref len is the number of bytes drained.
81 */
82struct pc_dma_event
83{
84 const uint8_t *data; ///< RX: received bytes (into a ping-pong buffer); TX: nullptr
85 uint32_t t_ms; ///< pc_millis() at completion (0 on host builds)
86 uint32_t t_us; ///< pc_micros() at completion (0 on host builds) - a high-rate
87 ///< peripheral (SPI slave DMA off a fast external DAQ/scope) can
88 ///< complete several transfers inside one t_ms tick; use t_us to
89 ///< measure inter-transfer jitter / trigger latency at that rate.
90 uint16_t len; ///< bytes transferred
91 uint16_t seq; ///< per-channel completion sequence (wraps)
92 uint8_t channel; ///< channel id [0, PC_DMA_CHANNELS)
93 pc_dma_periph periph; ///< pc_dma_periph
94 pc_dma_dir dir; ///< pc_dma_dir
95 uint8_t _pad;
96};
97
98/**
99 * @brief Called once per completed transfer (RX and TX). ISR context on real silicon,
100 * so keep it tiny (the canonical body posts @p ev to the preempting queue).
101 * @param ev the completion event (owned by the caller; copy what you need).
102 * @param ctx the opaque pointer from @ref pc_dma_config.
103 */
104typedef void (*pc_dma_cb)(const pc_dma_event *ev, void *ctx);
105
106/** @brief Channel configuration passed to pc_dma_open(). */
107struct pc_dma_config
108{
109 uint8_t channel; ///< channel id [0, PC_DMA_CHANNELS).
110 pc_dma_periph periph; ///< pc_dma_periph the channel drives.
111 bool loopback; ///< simulator: this channel's TX egress feeds its own RX ingress.
112 pc_dma_cb on_complete; ///< completion callback (required).
113 void *ctx; ///< opaque, forwarded to @ref on_complete.
114};
115
116/**
117 * @brief Configure a channel and arm its RX transfer.
118 * @return true if opened; false on a bad channel id / null callback / already open.
119 */
120bool pc_dma_open(const pc_dma_config *cfg);
121
122/**
123 * @brief Submit bytes for egress DMA on channel @p ch (copies up to PC_DMA_BUF_SIZE).
124 *
125 * Fail-closed: returns false if a TX is still in flight on the channel (wait for its
126 * TX-complete event), if the channel is closed, or on a null / oversize buffer.
127 * @return true if the transfer was accepted.
128 */
129bool pc_dma_tx_submit(uint8_t ch, const uint8_t *buf, uint16_t len);
130
131/** @brief Close a channel and drop any in-flight transfer / staged simulator bytes. */
132void pc_dma_close(uint8_t ch);
133
134/**
135 * @brief Advance the simulator engine: drain egress, run loopback, complete RX/TX and
136 * fire the callbacks. No-op on the real silicon backend (ISRs drive completion).
137 * This is how the host and the on-device self-test step the pipeline.
138 */
139void pc_dma_poll(void);
140
141#if PC_DMA_SIMULATE
142
143/**
144 * @brief Simulator: inject @p len bytes as if they arrived on channel @p ch's RX line.
145 * Delivered as one or more RX events on the next pc_dma_poll().
146 * @return true if staged; false if the ingress staging is full (fail-closed) or the
147 * channel is closed.
148 */
149bool pc_dma_sim_feed(uint8_t ch, const uint8_t *bytes, uint16_t len);
150
151/**
152 * @brief Simulator: read back up to @p max bytes that channel @p ch transmitted via
153 * egress DMA. Consumes them from the capture buffer.
154 * @return number of bytes copied into @p out.
155 */
156uint16_t pc_dma_sim_capture(uint8_t ch, uint8_t *out, uint16_t max);
157
158#endif // PC_DMA_SIMULATE
159
160#endif // PC_ENABLE_DMA
161
162#endif // PROTOCORE_DMA_H
User-facing configuration for ProtoCore.