FastLED 3.10.6
Loading...
Searching...
No Matches
AutoResearchRpPioContention.cpp
Go to the documentation of this file.
1// FastLED#1471 hardware reproduction: PIO state-machine contention.
2//
3// The reported symptom was random-coloured LEDs when Adafruit TinyUSB was
4// linked alongside FastLED on an RP2040. TinyUSB claims PIO state machines;
5// so does FastLED's clockless driver. The claim is that FastLED now routes
6// every acquisition through RpPioDmaResourceManager (non-panicking
7// pio_claim_unused_sm) and therefore never takes a state machine another
8// library owns.
9//
10// That claim had never been executed on silicon. This diagnostic starves the
11// PIO block directly -- no TinyUSB needed, since what matters is that the
12// state machines are claimed by someone else -- and then asks FastLED to build
13// a clockless controller on top of the starved block.
14
16
17#include "platforms/arm/rp/is_rp.h"
18
19#if defined(FL_IS_RP2040) || defined(FL_IS_RP2350)
20
21#include <Arduino.h>
22
23#include "FastLED.h"
25
26// IWYU pragma: begin_keep
27#include "hardware/pio.h"
28// IWYU pragma: end_keep
29
30namespace {
31
32// Pin 2 is inside LegacyClocklessProxy's dispatch table on every RP board and
33// is not committed to UART0 (0/1) or the SPI loopback wiring (3, 8, 11).
34constexpr int kContentionPin = 2;
35constexpr int kContentionLeds = 8;
36
37// Every state machine in every block: 2 blocks on RP2040, 3 on RP2350.
38constexpr int kMaxStateMachines = NUM_PIOS * 4;
39
40PIO pioForIndex(int index) {
41 if (index == 0) return pio0;
42 if (index == 1) return pio1;
43#if defined(FL_IS_RP2350)
44 if (index == 2) return pio2;
45#endif
46 return nullptr;
47}
48
49int countFreeStateMachines() {
50 int free_count = 0;
51 for (int pio_index = 0; pio_index < NUM_PIOS; ++pio_index) {
52 PIO pio = pioForIndex(pio_index);
53 if (pio == nullptr) continue;
54 for (uint sm = 0; sm < 4; ++sm) {
55 if (!pio_sm_is_claimed(pio, sm)) ++free_count;
56 }
57 }
58 return free_count;
59}
60
62class PioStarver {
63 public:
64 ~PioStarver() { release(); }
65
66 int starve() {
67 for (int pio_index = 0; pio_index < NUM_PIOS; ++pio_index) {
68 PIO pio = pioForIndex(pio_index);
69 if (pio == nullptr) continue;
70 for (;;) {
71 const int sm = pio_claim_unused_sm(pio, false);
72 if (sm < 0) break;
73 mPio[mCount] = pio;
74 mSm[mCount] = sm;
75 ++mCount;
76 }
77 }
78 return mCount;
79 }
80
82 bool claimsIntact() const {
83 for (int i = 0; i < mCount; ++i) {
84 if (!pio_sm_is_claimed(mPio[i], static_cast<uint>(mSm[i]))) {
85 return false;
86 }
87 }
88 return true;
89 }
90
91 void release() {
92 while (mCount > 0) {
93 --mCount;
94 pio_sm_unclaim(mPio[mCount], static_cast<uint>(mSm[mCount]));
95 }
96 }
97
98 private:
99 PIO mPio[kMaxStateMachines] = {};
100 int mSm[kMaxStateMachines] = {};
101 int mCount = 0;
102};
103
104} // namespace
105
106namespace autoresearch {
107
108fl::json runRpPioContentionTest() {
109 fl::json response = fl::json::object();
110 response.set("numPios", static_cast<int64_t>(NUM_PIOS));
111 response.set("pin", static_cast<int64_t>(kContentionPin));
112
113 // Both proxies below are destroyed before this frame returns, so the
114 // buffer can live on the stack.
115 CRGB leds[kContentionLeds] = {};
116
117 const int baseline_free = countFreeStateMachines();
118 response.set("baselineFreeSms", static_cast<int64_t>(baseline_free));
119
120 // Leg 1 (control): with state machines available, FastLED must take one.
121 // Without this leg a "FastLED claimed nothing" result under starvation
122 // would be indistinguishable from the driver being compiled out.
123 fl::json control = fl::json::object();
124 bool control_proxy_valid = false;
125 bool control_claimed_one = false;
126 {
127 LegacyClocklessProxy proxy(kContentionPin, leds, kContentionLeds);
128 const int free_during = countFreeStateMachines();
129 control_proxy_valid = proxy.valid();
130 control_claimed_one = free_during == baseline_free - 1;
131 control.set("proxyValid", control_proxy_valid);
132 control.set("freeDuring", static_cast<int64_t>(free_during));
133 control.set("claimedOne", control_claimed_one);
134 FastLED.show();
135 }
136 const int control_free_after = countFreeStateMachines();
137 control.set("freeAfter", static_cast<int64_t>(control_free_after));
138 control.set("released", control_free_after == baseline_free);
139 response.set("control", control);
140
141 // Leg 2: starve the block first, exactly as TinyUSB does at boot.
142 fl::json contention = fl::json::object();
143 bool no_theft = false;
144 bool survived_show = false;
145 {
146 PioStarver starver;
147 const int starved = starver.starve();
148 contention.set("starvedSms", static_cast<int64_t>(starved));
149 contention.set("freeAfterStarve",
150 static_cast<int64_t>(countFreeStateMachines()));
151
152 LegacyClocklessProxy proxy(kContentionPin, leds, kContentionLeds);
153 contention.set("proxyValid", proxy.valid());
154
155 // The load-bearing assertion for #1471: the starving owner's claims
156 // are all still its own, and nothing new was handed out.
157 no_theft = starver.claimsIntact() && countFreeStateMachines() == 0;
158 contention.set("claimsIntact", starver.claimsIntact());
159 contention.set("noTheft", no_theft);
160
161 // A disabled controller must still be safe to draw through.
162 FastLED.show();
163 survived_show = true;
164 contention.set("survivedShow", survived_show);
165 }
166 const int contention_free_after = countFreeStateMachines();
167 contention.set("freeAfter", static_cast<int64_t>(contention_free_after));
168 response.set("contention", contention);
169
170 // A failed acquisition must not strand the partial claims it made.
171 const bool no_leak = contention_free_after == baseline_free;
172 response.set("noLeak", no_leak);
173 // The control leg is part of the verdict, not just a reported field: if
174 // the clockless backend is compiled out, the control claims nothing and
175 // every contention assertion below holds vacuously. Without these terms a
176 // driver that does not exist reports the same success as one that
177 // correctly declines.
178 response.set("success", control_proxy_valid && control_claimed_one &&
179 no_theft && survived_show && no_leak &&
180 control_free_after == baseline_free);
181 return response;
182}
183
184} // namespace autoresearch
185
186#else
187
188namespace autoresearch {
189
191 fl::json response = fl::json::object();
192 response.set("success", false);
193 response.set("error", "RP PIO contention test requires an RP2040/RP2350");
194 return response;
195}
196
197} // namespace autoresearch
198
199#endif
CRGB leds[1]
FL_DISABLE_WARNING_PUSH FL_DISABLE_WARNING_GLOBAL_CONSTRUCTORS CFastLED FastLED
Global LED strip management instance.
void show(fl::u8 scale)
Update all our controllers with the current led colors, using the passed in brightness.
void set(const fl::string &key, const json &value) FL_NO_EXCEPT
Definition json.h:866
static json object() FL_NO_EXCEPT
Definition json.h:857
fl::CRGB CRGB
Definition crgb.h:25
fl::json runRpPioContentionTest()
Reproduce FastLED#1471's PIO state-machine contention on real RP silicon.
unsigned int uint
Definition stdint.h:132
@ PIO
PIO receiver (RP2040/RP2350)
Definition rx.h:164