FastLED 3.10.6
Loading...
Searching...
No Matches
AutoResearchSpiDma.h
Go to the documentation of this file.
1// AutoResearchSpiDma.h — LPC845 SPI + DMA async driver AutoResearch
2// validation harness (FastLED #3456, Phase 1 of #3453 bench bring-up).
3//
4// Device-side RPC handlers that exercise the LPC845 SPI-with-DMA0
5// async driver (`ARMHardwareSPIOutputDMA<>` from
6// `platforms/arm/lpc/spi_arm_lpc_dma.h`, PR #3454) on real silicon.
7//
8// Gated on `FL_IS_ARM_LPC_845 && FASTLED_LPC_SPI_DMA`. The earlier
9// LPC804 widening (#3505) was reverted after empirical evidence
10// confirmed LPC804 silicon has no DMA peripheral — see
11// `agents/docs/peripheral-existence.md` "Historical anti-example".
12// LPC845 F_CPU = 24 MHz → default harness divider 6 gives ~4 MHz SCK.
13// The whole file compiles out on any other build target.
14//
15// **Sibling of `AutoResearchPwmDmaClockless.h`** (#3468) — same
16// architectural shape, different peripheral. The two must not be
17// compiled into the same build (both claim DMA0 channels and the
18// LowMemory flash budget doesn't fit both). Enforced host-side by
19// `bash autoresearch` refusing `--dma-spi --pwm-dma-cl` together.
20//
21// Handlers registered (call `autoresearch::dma_spi::bind(remote)` from
22// AutoResearchLowMemory.h when `FASTLED_LPC_SPI_DMA` is defined):
23//
24// `dmaSpiTransferOnce(byte_count, byte_pattern)`
25// Fill an `byte_count`-byte buffer with `byte_pattern`, call
26// `kickDmaStream()`, wait for `done()`, return CSV
27// `"success,started_us,done_us,cpu_busy_bit"`. `cpu_busy_bit` is
28// always 0 in this handler (we wait on the CPU); the async proof
29// is via `dmaSpiTransferOverlap` below.
30//
31// `dmaSpiTransferOverlap(byte_count, byte_pattern)`
32// Kick the same stream but run a tight beacon-toggle counter
33// between `kickDmaStream()` and `done() == true`. Reports CSV
34// `"success,total_us,toggle_count"`. Non-zero `toggle_count`
35// under DMA load is the affirmative async claim — the CPU was
36// free to run application code while the DMA0 channel drove
37// SPI TX.
38//
39// `dmaSpiMeasureSck(divider)`
40// Emits a short burst at the passed divider; returns
41// `"success,total_us,byte_count"` so the host can compute the
42// effective SCK rate via `SCK_hz = byte_count * 8 / total_s`.
43// No SCT input capture on the SCK pin (that's issue #3015 Phase 3
44// territory — the SCT capture backend is opt-in and doesn't ship
45// the mux configuration for arbitrary SPI pins yet); the host
46// compares the measured effective rate against the requested
47// divider band as a first-order sanity check.
48//
49// Buffer sizing: the harness reuses the driver's static encode buffer
50// (`FASTLED_LPC_SPI_DMA_MAX_BYTES`, default 2048). Requests larger
51// than that are clamped by the driver.
52
53#pragma once
54
55#include <FastLED.h>
56
57#if defined(FL_IS_ARM_LPC_845) && defined(FASTLED_LPC_SPI_DMA)
58
59#include "fl/remote/remote.h"
60#include "fl/stl/noexcept.h"
61#include "fl/stl/singleton.h"
62#include "fl/stl/sstream.h"
63#include "platforms/arm/lpc/spi_arm_lpc_dma.h" // ok platform headers — AutoResearch driver-specific test needs the concrete SPI DMA driver
64
65namespace autoresearch {
66namespace dma_spi {
67
68// Default pin assignment matches the LPC845-BRK SPI0 typical routing.
69// Since #3580 the driver's init() owns the full peripheral plumbing —
70// SPI clock enable, FCLKSEL function-clock select, reset release, and
71// SWM routing of SCK/MOSI to these pins. No startup-code cooperation
72// is required.
73//
74// If the user needs to move pins, override at compile time via
75// `-DFASTLED_LPC_SPI_DMA_HARNESS_DATA_PIN=<N>` /
76// `-DFASTLED_LPC_SPI_DMA_HARNESS_CLOCK_PIN=<N>` /
77// `-DFASTLED_LPC_SPI_DMA_HARNESS_DIVIDER=<N>`.
78#ifndef FASTLED_LPC_SPI_DMA_HARNESS_DATA_PIN
79#define FASTLED_LPC_SPI_DMA_HARNESS_DATA_PIN 17
80#endif
81#ifndef FASTLED_LPC_SPI_DMA_HARNESS_CLOCK_PIN
82#define FASTLED_LPC_SPI_DMA_HARNESS_CLOCK_PIN 13
83#endif
84#ifndef FASTLED_LPC_SPI_DMA_HARNESS_DIVIDER
85// 24 MHz core / 6 → 4 MHz SCK. Fast enough that a 256-byte transfer
86// completes in ~0.5 ms — well inside the RPC response window.
87#define FASTLED_LPC_SPI_DMA_HARNESS_DIVIDER 6
88#endif
89
90using HarnessDriver = fl::ARMHardwareSPIOutputDMA<
91 static_cast<fl::u8>(FASTLED_LPC_SPI_DMA_HARNESS_DATA_PIN),
92 static_cast<fl::u8>(FASTLED_LPC_SPI_DMA_HARNESS_CLOCK_PIN),
93 static_cast<fl::u32>(FASTLED_LPC_SPI_DMA_HARNESS_DIVIDER)>;
94
95struct SpiDmaHarnessState {
96 fl::u8 buffer[FASTLED_LPC_SPI_DMA_MAX_BYTES];
97 HarnessDriver driver;
98 bool initialized;
99
100 SpiDmaHarnessState() : initialized(false) {}
101};
102
103#if FASTLED_LPC_DMA_ISR
104struct SpiDmaStreamState {
105 fl::u8 source[FASTLED_LPC_SPI_DMA_STREAM_BYTES];
106};
107
108inline SpiDmaStreamState& spiDmaStreamState() FL_NO_EXCEPT {
110}
111#endif
112
113inline SpiDmaHarnessState& spiDmaHarnessState() FL_NO_EXCEPT {
115}
116
117// Bench buffer: sized to the driver's encode-buffer capacity so callers
118// can hit the widest DMA descriptor the driver will accept.
119inline fl::u8* harnessBuffer() FL_NO_EXCEPT {
120 return spiDmaHarnessState().buffer;
121}
122
123// Lazy driver instantiation — first RPC call configures SPI+DMA once.
124inline HarnessDriver& harnessDriver() FL_NO_EXCEPT {
125 SpiDmaHarnessState& state = spiDmaHarnessState();
126 if (!state.initialized) {
127 state.driver.init();
128 state.initialized = true;
129 }
130 return state.driver;
131}
132
133// Bounds guard for host-supplied lengths — matches
134// FASTLED_LPC_SPI_DMA_MAX_BYTES so we can't overflow the static
135// encode buffer even if the host misbehaves.
136inline int clampByteCount(int n) FL_NO_EXCEPT {
137 if (n <= 0) return 0;
138 const int cap = static_cast<int>(FASTLED_LPC_SPI_DMA_MAX_BYTES);
139 return (n > cap) ? cap : n;
140}
141
142// One-shot DMA transfer with CPU wait. CSV: "success,started_us,done_us,cpu_busy".
143inline fl::string transferOnceHandler(int byte_count, int byte_pattern) FL_NO_EXCEPT {
144 const int len = clampByteCount(byte_count);
145 if (len == 0) return fl::string("0,0,0,0");
146 fl::u8* buf = harnessBuffer();
147 const fl::u8 pat = static_cast<fl::u8>(byte_pattern & 0xFF);
148 for (int i = 0; i < len; ++i) buf[i] = pat;
149
150 HarnessDriver& drv = harnessDriver();
151 // Ensure prior stream has drained before we time this one.
152 drv.waitFully();
153
154 const fl::u32 t_start = micros();
155 HarnessDriver::kickDmaStream(buf, static_cast<fl::u32>(len));
156 // Poll for done() without WFI so the elapsed measurement matches
157 // the CPU-busy-wait cost path. (Callers wanting the WFI sleep path
158 // use `waitDma()` — that's not what we're measuring here.)
159 //
160 // Bounded spin (#3580 diagnosis): a healthy 512-byte stream at 4 MHz
161 // SCK completes in ~1 ms; 2M iterations ≈ 1 s at 24 MHz. On timeout,
162 // return a register snapshot instead of wedging into the watchdog:
163 // "0,dma_active,chan_xfercfg,chan_ctlstat,spi_stat,dma_errint"
164 fl::u32 spins = 0;
165 while (!HarnessDriver::done()) {
166 if (++spins > 2000000u) {
167 fl::sstream d;
168 d << 0 << ','
169 << DMA0->COMMON[0].ACTIVE << ','
170 << DMA0->CHANNEL[FASTLED_LPC_SPI_DMA_CHANNEL].XFERCFG << ','
171 << DMA0->CHANNEL[FASTLED_LPC_SPI_DMA_CHANNEL].CTLSTAT << ','
172 << SPI0->STAT << ','
173 << DMA0->COMMON[0].ERRINT;
174 return d.str();
175 }
176 }
177 const fl::u32 t_end = micros();
178
179 fl::sstream s;
180 s << 1 << ',' << t_start << ',' << t_end << ',' << 0;
181 return s.str();
182}
183
184// Async proof — kick and beacon-toggle a counter while DMA runs.
185// CSV: "success,total_us,toggle_count". Non-zero toggle_count is the
186// affirmative async claim.
187inline fl::string transferOverlapHandler(int byte_count, int byte_pattern) FL_NO_EXCEPT {
188 const int len = clampByteCount(byte_count);
189 if (len == 0) return fl::string("0,0,0");
190 fl::u8* buf = harnessBuffer();
191 const fl::u8 pat = static_cast<fl::u8>(byte_pattern & 0xFF);
192 for (int i = 0; i < len; ++i) buf[i] = pat;
193
194 HarnessDriver& drv = harnessDriver();
195 drv.waitFully();
196
197 const fl::u32 t_start = micros();
198 HarnessDriver::kickDmaStream(buf, static_cast<fl::u32>(len));
199
200 // Beacon-toggle loop. Volatile counter forces the compiler to
201 // emit real memory ops each iteration — otherwise the loop
202 // collapses and the "async proof" becomes an over-optimized nop.
203 volatile fl::u32 toggle_count = 0;
204 while (!HarnessDriver::done()) {
205 ++toggle_count;
206 }
207 const fl::u32 t_end = micros();
208 const fl::u32 total_us = t_end - t_start;
209
210 fl::sstream s;
211 s << 1 << ',' << total_us << ',' << toggle_count;
212 return s.str();
213}
214
215// SCK measurement via wall-clock timing. CSV: "success,total_us,byte_count".
216// Host computes effective SCK from `byte_count * 8 / total_us`. The
217// `divider_hint` parameter is echoed back for the log — the compile-time
218// driver divider is what actually runs (`FASTLED_LPC_SPI_DMA_HARNESS_DIVIDER`).
219inline fl::string measureSckHandler(int divider_hint) FL_NO_EXCEPT {
220 (void)divider_hint;
221 // 512 bytes @ 4 MHz SCK ≈ 1.024 ms — small enough for RPC latency,
222 // big enough that the timer resolution isn't the dominant term.
223 constexpr int kBenchBytes = 512;
224 fl::u8* buf = harnessBuffer();
225 for (int i = 0; i < kBenchBytes; ++i) buf[i] = 0xA5;
226
227 HarnessDriver& drv = harnessDriver();
228 drv.waitFully();
229
230 const fl::u32 t_start = micros();
231 HarnessDriver::kickDmaStream(buf, static_cast<fl::u32>(kBenchBytes));
232 while (!HarnessDriver::done()) {
233 // spin
234 }
235 // Include the SPI master shift-register drain so the wire time
236 // covers full byte delivery (kickDmaStream returns when DMA is
237 // drained, but the SPI master can still be clocking out the
238 // final byte in the shift register).
239 drv.waitFully();
240 const fl::u32 t_end = micros();
241 const fl::u32 total_us = t_end - t_start;
242
243 fl::sstream s;
244 s << 1 << ',' << total_us << ',' << kBenchBytes;
245 return s.str();
246}
247
248// Bind the three handlers on the passed-in `Remote`. Call from
249// AutoResearchLowMemory.h under `#if defined(FASTLED_LPC_SPI_DMA)`.
250#if FASTLED_LPC_DMA_ISR
251// ISR-refilled streaming proof (#3453 follow-up). Streams a frame larger
252// than the whole encode buffer (multi-chunk, ping-pong ISR refill) while
253// beacon-toggling on the main thread. CSV: "success,total_us,toggle_count".
254// A stalled refill chain times out into a register-snapshot CSV.
255//
256// RAM budget (#3585): the LPC845-BRK AutoResearch build's JSON-RPC stack
257// leaves only ~3 KB shared between heap and stack on the 16 KB part. The
258// DMA completion ISR (encode + descriptor arm) preempts that stack at its
259// deepest point; a large `stream_src` static shrank the gap until the ISR
260// pushed the stack into the heap and corrupted a return address (wild-PC
261// HardFault, not a driver logic bug — the ping-pong is correct). 512 bytes
262// is a genuine multi-chunk stream (2 chunks + 1 ISR refill at kHalfCap=256)
263// and keeps enough headroom. Do NOT raise this on the 16 KB bench build.
264#ifndef FASTLED_LPC_SPI_DMA_STREAM_BYTES
265#define FASTLED_LPC_SPI_DMA_STREAM_BYTES 512
266#endif
267inline fl::string streamOverlapHandler(int byte_count, int byte_pattern) FL_NO_EXCEPT {
268 fl::u8* stream_src = spiDmaStreamState().source;
269 int len = byte_count;
270 if (len <= 0) return fl::string("0,0,0");
271 if (len > FASTLED_LPC_SPI_DMA_STREAM_BYTES)
272 len = FASTLED_LPC_SPI_DMA_STREAM_BYTES;
273 const fl::u8 pat = static_cast<fl::u8>(byte_pattern & 0xFF);
274 for (int i = 0; i < len; ++i) stream_src[i] = pat;
275
276 HarnessDriver& drv = harnessDriver();
277 drv.waitFully();
278
279 const fl::u32 t_start = micros();
280 if (!HarnessDriver::kickDmaStreamAsync(stream_src,
281 static_cast<fl::u32>(len))) {
282 return fl::string("0,kick_refused,0");
283 }
284 volatile fl::u32 toggle_count = 0;
285 fl::u32 spins = 0;
286 while (!HarnessDriver::streamDone()) {
287 ++toggle_count;
288 if (++spins > 4000000u) {
289 fl::sstream d;
290 d << 0 << ',' << DMA0->COMMON[0].ACTIVE << ','
291 << DMA0->CHANNEL[FASTLED_LPC_SPI_DMA_CHANNEL].CTLSTAT << ','
292 << SPI0->STAT;
293 return d.str();
294 }
295 }
296 const fl::u32 total_us = micros() - t_start;
297
298 fl::sstream s;
299 s << 1 << ',' << total_us << ',' << toggle_count;
300 return s.str();
301}
302#endif // FASTLED_LPC_DMA_ISR
303
304// Args arrive as a JSON array (the house RpcClient convention — a single
305// `const fl::json&` param that IS the positional-arg array). Read
306// `args[N]` positionally. This is what lets the bench run over fbuild's
307// Rust serial monitor (ci.rpc_client.RpcClient) instead of raw pyserial;
308// see agents/docs/hardware-autoresearch.md "Device serial".
309inline int argInt(const fl::json& args, fl::size i, int fallback) FL_NO_EXCEPT {
310 return static_cast<int>(args[i].as_int().value_or(fallback));
311}
312
313inline void bind(fl::Remote& remote) FL_NO_EXCEPT {
314 remote.bind("dmaSpiTransferOnce",
315 [](const fl::json& args) -> fl::string {
316 return transferOnceHandler(argInt(args, 0, 0), argInt(args, 1, 0));
317 });
318#if FASTLED_LPC_DMA_ISR
319 remote.bind("dmaSpiStreamOverlap",
320 [](const fl::json& args) -> fl::string {
321 return streamOverlapHandler(argInt(args, 0, 0), argInt(args, 1, 0));
322 });
323#endif
324 remote.bind("dmaSpiTransferOverlap",
325 [](const fl::json& args) -> fl::string {
326 return transferOverlapHandler(argInt(args, 0, 0), argInt(args, 1, 0));
327 });
328 remote.bind("dmaSpiMeasureSck",
329 [](const fl::json& args) -> fl::string {
330 return measureSckHandler(argInt(args, 0, 6));
331 });
332}
333
334} // namespace dma_spi
335} // namespace autoresearch
336
337#endif // FL_IS_ARM_LPC_845 && FASTLED_LPC_SPI_DMA
TestState state
fl::unique_ptr< fl::Remote > remote
Definition RpcClient.ino:43
static T & instance() FL_NO_EXCEPT
Definition singleton.h:81
string str() const FL_NO_EXCEPT
Definition strstream.h:43
unsigned char u8
Definition stdint.h:131
fl::u32 micros()
Universal microsecond timer - returns microseconds since system startup.
corkscrew_args args
Definition old.h:149
#define FL_NO_EXCEPT