FastLED 3.10.6
Loading...
Searching...
No Matches
AutoResearchRpPioParallel.cpp
Go to the documentation of this file.
1// FastLED#3899: "PIO0 + PIO1 simultaneous execution passes without resource
2// collision, stale state machine, or DMA ownership leak."
3//
4// runParallelTest drives both blocks at once, but for the PIO0+PIO1 pair it
5// deliberately skips RX validation (AutoResearchRemoteRunParallelTest.cpp) and
6// makes no resource assertions at all. A PASS there therefore proves only that
7// show() completed. Each of the three failure modes named in the criterion is
8// invisible to it.
9//
10// This asks the SDK claim system directly, which is ground truth regardless of
11// how the channel engine is implemented:
12//
13// collision -> the two engines must never hold overlapping resources,
14// and must never take more than one state machine each.
15// stale SM -> between frames the engine must be back to baseline; a
16// state machine kept or abandoned across a frame boundary
17// shows up as residue.
18// ownership leak -> after teardown every count must return to baseline.
19//
20// Note ChannelEngineRpPio claims per transmission and calls deinitialize()
21// when the transfer drains, so the interesting sample is *between*
22// FastLED.show() and FastLED.wait(), not after. Sampling after the wait sees
23// an idle engine and proves nothing.
24//
25// What this does NOT assert: that PIO0 and PIO1 are ever claimed at the *same
26// instant*. FastLED.show() drains the frame before returning on this path, so
27// the observable in-flight window is roughly one sample per frame -- far too
28// coarse to tell genuine serialization apart from an unlucky sample. The
29// `pio0HeldOne`/`pio1HeldOne`/`bothHeldAtOnce` fields are reported as
30// diagnostics for that open question and deliberately do not gate `success`.
31// Claiming serialization on this evidence would be unfounded.
32
34
35#include "platforms/arm/rp/is_rp.h"
36
37#if defined(FL_IS_RP2040) || defined(FL_IS_RP2350)
38
39#include <Arduino.h>
40
41#include "FastLED.h"
42#include "fl/channels/channel.h"
43#include "fl/channels/config.h"
45#include "fl/stl/vector.h"
46
47// IWYU pragma: begin_keep
48#include "hardware/dma.h"
49#include "hardware/pio.h"
50// IWYU pragma: end_keep
51
52namespace {
53
54constexpr int kDefaultPinA = 2;
55constexpr int kDefaultPinB = 4;
56constexpr int kDefaultLeds = 256;
57constexpr int kDefaultIterations = 8;
58// Two CRGB buffers come out of the RPC arguments. 2 * 2000 * 3 B is 12 KB,
59// comfortable on an RP2350 and far beyond any useful test size.
60constexpr int kMaxLeds = 2000;
61// Each iteration is a full show() plus a bounded wait, so this keeps the probe
62// well inside AutoResearch's 5 s loop watchdog.
63constexpr int kMaxIterations = 256;
64// AutoResearch's loop watchdog is 5 s; leave room for the JSON response.
65constexpr uint32_t kShowTimeoutMs = 2000;
66
67PIO pioForIndex(int index) {
68 if (index == 0) return pio0;
69 if (index == 1) return pio1;
70#if defined(FL_IS_RP2350)
71 if (index == 2) return pio2;
72#endif
73 return nullptr;
74}
75
77int freeSmsInBlock(int pio_index) {
78 PIO pio = pioForIndex(pio_index);
79 if (pio == nullptr) return 0;
80 int free_count = 0;
81 for (uint sm = 0; sm < 4; ++sm) {
82 if (!pio_sm_is_claimed(pio, sm)) ++free_count;
83 }
84 return free_count;
85}
86
87int freeDmaChannels() {
88 int free_count = 0;
89 for (uint ch = 0; ch < NUM_DMA_CHANNELS; ++ch) {
90 if (!dma_channel_is_claimed(ch)) ++free_count;
91 }
92 return free_count;
93}
94
96struct ResourceSnapshot {
97 int free_sms[NUM_PIOS] = {};
98 int free_dma = 0;
99
100 void capture() {
101 for (int i = 0; i < NUM_PIOS; ++i) free_sms[i] = freeSmsInBlock(i);
102 free_dma = freeDmaChannels();
103 }
104
105 int totalFreeSms() const {
106 int total = 0;
107 for (int i = 0; i < NUM_PIOS; ++i) total += free_sms[i];
108 return total;
109 }
110
111 bool operator==(const ResourceSnapshot& other) const {
112 if (free_dma != other.free_dma) return false;
113 for (int i = 0; i < NUM_PIOS; ++i) {
114 if (free_sms[i] != other.free_sms[i]) return false;
115 }
116 return true;
117 }
118
119 fl::json toJson() const {
120 fl::json out = fl::json::object();
121 fl::json blocks = fl::json::array();
122 for (int i = 0; i < NUM_PIOS; ++i) {
123 blocks.push_back(static_cast<int64_t>(free_sms[i]));
124 }
125 out.set("freeSmsPerBlock", blocks);
126 out.set("freeDma", static_cast<int64_t>(free_dma));
127 return out;
128 }
129};
130
131fl::ChannelPtr addPioChannel(int pio_which, int pin, fl::vector<CRGB>& leds) {
133 opts.mBus = fl::Bus::FLEX_IO;
134 opts.mBusWhich = pio_which;
137 opts);
138 return FastLED.add(config);
139}
140
141} // namespace
142
143namespace autoresearch {
144
145fl::json runRpPioParallelResourceTest(const fl::json& args) {
146 fl::json response = fl::json::object();
147
148 int pin_a = kDefaultPinA;
149 int pin_b = kDefaultPinB;
150 int num_leds = kDefaultLeds;
151 int iterations = kDefaultIterations;
152 if (args.is_object()) {
153 auto read_int = [&args](const char* key, int fallback) -> int {
154 if (args.contains(key) && args[key].is_int()) {
155 return static_cast<int>(args[key].as_int().value());
156 }
157 return fallback;
158 };
159 pin_a = read_int("pinA", pin_a);
160 pin_b = read_int("pinB", pin_b);
161 num_leds = read_int("numLeds", num_leds);
162 iterations = read_int("iterations", iterations);
163 }
164 // Upper bounds matter more than the lower ones here: both LED buffers are
165 // allocated from this RPC's arguments, and two unbounded CRGB vectors will
166 // exhaust an RP2350's RAM long before anything reports a problem. Reject
167 // rather than clamp, so a caller asking for something impossible learns
168 // that instead of silently measuring a different run.
169 if (num_leds < 1 || num_leds > kMaxLeds || iterations < 1 ||
170 iterations > kMaxIterations) {
171 response.set("success", false);
172 response.set("error", "InvalidArgs");
173 response.set("maxNumLeds", static_cast<int64_t>(kMaxLeds));
174 response.set("maxIterations", static_cast<int64_t>(kMaxIterations));
175 return response;
176 }
177
178 response.set("pinA", static_cast<int64_t>(pin_a));
179 response.set("pinB", static_cast<int64_t>(pin_b));
180 response.set("numLeds", static_cast<int64_t>(num_leds));
181 response.set("iterations", static_cast<int64_t>(iterations));
182
183 ResourceSnapshot baseline;
184 baseline.capture();
185 response.set("baseline", baseline.toJson());
186
187 // Start from a known driver state so neither engine is picked by priority.
189 FastLED.setDriverEnabled("PIO0", true);
190 FastLED.setDriverEnabled("PIO1", true);
191
192 fl::vector<CRGB> leds_a(num_leds);
193 fl::vector<CRGB> leds_b(num_leds);
194
195 bool collision_free = false;
196 bool no_residue = true;
197 bool shows_completed = false;
198 {
199 fl::ChannelPtr channel_a = addPioChannel(0, pin_a, leds_a);
200 fl::ChannelPtr channel_b = addPioChannel(1, pin_b, leds_b);
201 response.set("channelACreated", static_cast<bool>(channel_a));
202 response.set("channelBCreated", static_cast<bool>(channel_b));
203
204 if (!channel_a || !channel_b) {
206 response.set("success", false);
207 response.set("error", "ChannelCreationFailed");
208 return response;
209 }
210
211 for (int i = 0; i < num_leds; ++i) {
212 leds_a[i] = CRGB(0xFF, 0x00, 0x00);
213 leds_b[i] = CRGB(0x00, 0xFF, 0x00);
214 }
215
216 // Best in-flight sample across all frames: the one where the most
217 // resources were held at once.
218 ResourceSnapshot busiest;
219 busiest.capture();
220 int fewest_free = busiest.totalFreeSms() + busiest.free_dma;
221 int frames_sampled = 0;
222
223 // Sample continuously for the whole transmission rather than once per
224 // frame. A single sample cannot tell "the two engines are never
225 // concurrent" apart from "the sample landed between them".
226 int samples_taken = 0;
227 shows_completed = true;
228 for (int iter = 0; iter < iterations && shows_completed; ++iter) {
229 FastLED.show();
230
231 const u32 deadline = millis() + kShowTimeoutMs;
232 bool done = false;
233 while (!done && static_cast<i32>(millis() - deadline) < 0) {
234 ResourceSnapshot in_flight;
235 in_flight.capture();
236 const int held = in_flight.totalFreeSms() + in_flight.free_dma;
237 if (held < fewest_free) {
238 fewest_free = held;
239 busiest = in_flight;
240 }
241 ++samples_taken;
242 // 1 ms slices so the loop keeps sampling while DMA drains.
243 done = FastLED.wait(1);
244 }
245 shows_completed = done;
246 ++frames_sampled;
247
248 // Between frames the engine must have given everything back.
249 ResourceSnapshot between;
250 between.capture();
251 if (!(between == baseline)) no_residue = false;
252 }
253 response.set("samplesTaken", static_cast<int64_t>(samples_taken));
254 response.set("showsCompleted", shows_completed);
255 response.set("framesSampled", static_cast<int64_t>(frames_sampled));
256 response.set("busiestInFlight", busiest.toJson());
257 response.set("noResidueBetweenFrames", no_residue);
258
259 // Diagnostics for the open simultaneity question (see header comment).
260 const bool pio0_held_one = busiest.free_sms[0] == baseline.free_sms[0] - 1;
261 const bool pio1_held_one = busiest.free_sms[1] == baseline.free_sms[1] - 1;
262 response.set("pio0HeldOne", pio0_held_one);
263 response.set("pio1HeldOne", pio1_held_one);
264 response.set("bothHeldAtOnce", pio0_held_one && pio1_held_one);
265
266 // What is actually asserted: neither engine ever over-claims, and the
267 // probe genuinely observed a transmission in flight (otherwise a clean
268 // baseline would pass vacuously).
269 bool no_over_claim = busiest.free_dma >= baseline.free_dma - 2;
270 for (int i = 0; i < NUM_PIOS; ++i) {
271 if (busiest.free_sms[i] < baseline.free_sms[i] - 1) no_over_claim = false;
272 }
273 const bool observed_in_flight =
274 busiest.totalFreeSms() + busiest.free_dma <
275 baseline.totalFreeSms() + baseline.free_dma;
276 collision_free = no_over_claim && observed_in_flight;
277 response.set("noOverClaim", no_over_claim);
278 response.set("observedInFlight", observed_in_flight);
279 response.set("collisionFree", collision_free);
280 }
281
283
284 ResourceSnapshot released;
285 released.capture();
286 response.set("released", released.toJson());
287 const bool no_leak = released == baseline;
288 response.set("noLeak", no_leak);
289
290 response.set("success", collision_free && shows_completed && no_residue &&
291 no_leak);
292 return response;
293}
294
295} // namespace autoresearch
296
297#else
298
299namespace autoresearch {
300
302 (void)args;
303 fl::json response = fl::json::object();
304 response.set("success", false);
305 response.set("error", "RP PIO parallel resource test requires an RP2040/RP2350");
306 return response;
307}
308
309} // namespace autoresearch
310
311#endif
CRGB leds[1]
size_t capture(fl::shared_ptr< fl::RxChannel > rx_channel, fl::span< uint8_t > rx_buffer, size_t expected_data_bytes, int tx_pin, const fl::ChipsetTimingConfig &timing, const char *driver_name, fl::RunResult *diagnostics=nullptr)
bool done
FL_DISABLE_WARNING_PUSH FL_DISABLE_WARNING_GLOBAL_CONSTRUCTORS CFastLED FastLED
Global LED strip management instance.
ESP32-P4 Parallel IO (PARLIO) LED channel.
void setDriverEnabled(const char *name, bool enabled)
Enable or disable a channel driver by name at runtime.
void show(fl::u8 scale)
Update all our controllers with the current led colors, using the passed in brightness.
void wait()
Wait for all channel bus transmissions to complete.
void clear(bool writeData=false)
Clear the leds, wiping the local array of data.
void set(const fl::string &key, const json &value) FL_NO_EXCEPT
Definition json.h:866
void push_back(const json &value) FL_NO_EXCEPT
Definition json.h:918
static json object() FL_NO_EXCEPT
Definition json.h:857
static json array() FL_NO_EXCEPT
Definition json.h:853
constexpr EOrder RGB
Definition eorder.h:17
@ CHANNELS
Remove all channels from controller list.
Definition FastLED.h:668
fl::CRGB CRGB
Definition crgb.h:25
fl::json runRpPioParallelResourceTest(const fl::json &args)
Prove PIO0 + PIO1 simultaneous operation at the resource level (#3899).
constexpr ChipsetTimingConfig makeTimingConfig() FL_NO_EXCEPT
Convert compile-time CHIPSET type to runtime timing config.
fl::u32 millis()
Universal millisecond timer - returns milliseconds since system startup.
FASTLED_FORCE_INLINE bool operator==(const CRGB &lhs, const CRGB &rhs) FL_NO_EXCEPT
Check if two CRGB objects have the same color data.
Definition crgb.h:740
@ FLEX_IO
Definition bus.h:36
@ PIO
PIO receiver (RP2040/RP2350)
Definition rx.h:164
corkscrew_args args
Definition old.h:149
fl::u32 uint32_t
Definition stdint.h:218
Configuration for a single LED channel.
Definition config.h:167
Optional channel configuration parameters All fields have sensible defaults and can be overridden as ...
Definition options.h:85
Compile-time timing extraction from ChipsetTiming structs.