FastLED 3.10.8
Loading...
Searching...
No Matches
spi_channel_adapter.cpp.hpp
Go to the documentation of this file.
1
3
5#include "fl/log/log.h"
6#include "fl/log/log.h"
7#include "fl/stl/cstring.h" // For memcpy
8#include "fl/channels/config.h" // For SpiChipsetConfig
9// IWYU pragma: begin_keep
10#include "platforms/shared/spi_hw_1.h" // For SpiHw1::Config // ok platform headers
11#include "fl/stl/noexcept.h"
12// IWYU pragma: end_keep
13
14namespace fl {
15
16// Static factory method (multi-controller version)
19 fl::vector<int> priorities,
21 const char* adapterName
23 // Validation
24 if (hwControllers.empty()) {
25 FL_WARN("SpiChannelEngineAdapter::create: No controllers provided");
26 return nullptr;
27 }
28
29 if (hwControllers.size() != priorities.size() || hwControllers.size() != names.size()) {
30 FL_WARN("SpiChannelEngineAdapter::create: Size mismatch in arguments");
31 return nullptr;
32 }
33
34 if (!adapterName || adapterName[0] == '\0') {
35 FL_WARN("SpiChannelEngineAdapter::create: Empty adapter name");
36 return nullptr;
37 }
38
39 // Create adapter
40 auto adapter = fl::make_shared<SpiChannelEngineAdapter>(adapterName);
41
42 // Register all controllers
43 for (size_t i = 0; i < hwControllers.size(); i++) {
44 if (!hwControllers[i]) {
45 FL_WARN("SpiChannelEngineAdapter: Null controller at index " << i);
46 continue;
47 }
48
49 adapter->mControllers.emplace_back(hwControllers[i], priorities[i], names[i]);
50
51 FL_DBG("SpiChannelEngineAdapter: Registered controller '" << names[i] << "' (priority " << priorities[i] << ", lanes: " << (static_cast<int>(hwControllers[i]->getLaneCount())) << ")");
52 }
53
54 if (adapter->mControllers.empty()) {
55 FL_WARN("SpiChannelEngineAdapter: No valid controllers registered");
56 return nullptr;
57 }
58
59 return adapter;
60}
61
63 FL_NO_EXCEPT : mName(name)
64{
65 // Controllers added via create() factory method
66}
67
69 FL_DBG("SpiChannelEngineAdapter: Destructor for '" << mName << "'");
70
71 // Clear any enqueued channels that were never transmitted
72 // This prevents infinite loop in poll() which returns DRAINING if enqueued channels exist
73 mEnqueuedChannels.clear();
74
75 // Poll until READY to ensure cleanup of any in-flight transmissions
76 while (poll() != DriverState::READY) {
77 // Busy-wait for transmission completion
78 // This is acceptable in destructor context
79 }
80
81 // Shutdown all initialized controllers
82 for (auto& ctrl : mControllers) {
83 if (ctrl.controller && ctrl.controller->isInitialized()) {
84 ctrl.controller->end();
85 }
86 }
87}
88
90 int maxPriority = -1;
91 for (const auto& ctrl : mControllers) {
92 if (ctrl.priority > maxPriority) {
93 maxPriority = ctrl.priority;
94 }
95 }
96 return maxPriority;
97}
98
100 // Check if clock pin already assigned
101 for (const auto& assignment : mClockPinAssignments) {
102 if (assignment.clockPin == clockPin && assignment.requestedBus == requestedBus) {
103 return assignment.controllerIndex;
104 }
105 }
106
107 // Find highest priority available controller
108 int bestIndex = -1;
109 int bestPriority = -1;
110
111 for (size_t i = 0; i < mControllers.size(); i++) {
112 if (requestedBus != Esp32SpiBus::AUTO &&
113 mControllers[i].controller->getBusId() != static_cast<int>(requestedBus)) {
114 continue;
115 }
116 if (!canControllerHandleClockPin(mControllers[i], clockPin)) {
117 continue;
118 }
119
120 if (mControllers[i].priority > bestPriority) {
121 bestIndex = i;
122 bestPriority = mControllers[i].priority;
123 }
124 }
125
126 // Record assignment
127 if (bestIndex >= 0) {
128 mClockPinAssignments.push_back({clockPin, requestedBus, static_cast<size_t>(bestIndex)});
129 }
130
131 return bestIndex;
132}
133
135 const ControllerInfo& ctrl, int clockPin) const FL_NO_EXCEPT {
136
137 // Uninitialized controllers can handle any pin
138 if (!ctrl.isInitialized) {
139 return true;
140 }
141
142 // Check if already handling this pin
143 for (int assignedPin : ctrl.assignedClockPins) {
144 if (assignedPin == clockPin) {
145 return true;
146 }
147 }
148
149 // ESP32 constraint: cannot re-initialize with different pins
150 return false;
151}
152
154 ControllerInfo& ctrl, int clockPin, int dataPin) FL_NO_EXCEPT {
155
156 if (ctrl.isInitialized) {
157 // Verify compatibility
158 for (int pin : ctrl.assignedClockPins) {
159 if (pin == clockPin) {
160 return true; // Already configured for this pin
161 }
162 }
163 FL_WARN("SpiChannelEngineAdapter: Controller " << ctrl.name << " already initialized with different clock pin");
164 return false;
165 }
166
167 // Initialize controller based on lane count
168 u8 lanes = ctrl.controller->getLaneCount();
169
170 if (lanes == 1) {
171 SpiHw1::Config config;
172 config.bus_num = static_cast<fl::u8>(ctrl.controller->getBusId());
173 config.clock_pin = clockPin;
174 config.data_pin = dataPin;
175 config.clock_speed_hz = 20000000; // 20 MHz for APA102
176 config.max_transfer_sz = 65536;
177
178 if (!ctrl.controller->begin(&config)) {
179 FL_WARN("SpiChannelEngineAdapter: Failed to initialize " << ctrl.name);
180 return false;
181 }
182 } else {
183 // TODO: Multi-lane initialization (SpiHw2/4/8/16)
184 FL_WARN("SpiChannelEngineAdapter: Multi-lane init not yet implemented");
185 return false;
186 }
187
188 ctrl.isInitialized = true;
189 ctrl.assignedClockPins.push_back(clockPin);
190
191 FL_DBG("SpiChannelEngineAdapter: Initialized " << ctrl.name << " with clock pin " << clockPin);
192
193 return true;
194}
195
196bool SpiChannelEngineAdapter::canHandle(const ChannelDataPtr& data) const FL_NO_EXCEPT {
197 if (!data) {
198 return false;
199 }
200
201 // ⚠️ CRITICAL: Accept ONLY true SPI chipsets (APA102, SK9822, HD108)
202 // Reject clockless chipsets (WS2812, SK6812) - those use ChannelEngineSpi or RMT
203 //
204 // This is the opposite of ChannelEngineSpi::canHandle(), which should be:
205 // return !data->isSpi(); // Accept clockless, reject true SPI
206 //
207 // Correct routing:
208 // APA102 → SpiChannelEngineAdapter (this adapter)
209 // WS2812 → ChannelEngineSpi (clockless-over-SPI) OR RMT/PARLIO
210 return data->isSpi();
211}
212
213void SpiChannelEngineAdapter::enqueue(ChannelDataPtr channelData) FL_NO_EXCEPT {
214 if (!channelData) {
215 FL_WARN("SpiChannelEngineAdapter: Null channel data passed to enqueue()");
216 return;
217 }
218
219 // Validate that we can handle this data
220 if (!canHandle(channelData)) {
221 FL_WARN("SpiChannelEngineAdapter: Cannot handle non-SPI channel data (chipset mismatch)");
222 return;
223 }
224
225 mEnqueuedChannels.push_back(channelData);
226 FL_DBG("SpiChannelEngineAdapter: Enqueued channel (total: " << mEnqueuedChannels.size() << ")");
227}
228
230 if (mEnqueuedChannels.empty()) {
231 return;
232 }
233
234 FL_DBG("SpiChannelEngineAdapter: show() called with " << mEnqueuedChannels.size() << " channels");
235
236 // Move enqueued channels to transmitting list
238 mEnqueuedChannels.clear();
239
240 // Group channels by clock pin
241 // Channels with the same clock pin can share SPI bus configuration
243
244 FL_DBG("SpiChannelEngineAdapter: Grouped into " << groups.size() << " clock pin groups");
245
246 // Transmit each group sequentially
247 for (size_t i = 0; i < groups.size(); i++) {
248 const ClockPinGroup& group = groups[i];
249
250 FL_DBG("SpiChannelEngineAdapter: Transmitting group with clock pin " << group.clockPin << " (" << group.channels.size() << " channels)");
251
252 if (!transmitBatch(group.channels)) {
253 FL_WARN("SpiChannelEngineAdapter: Failed to transmit batch for clock pin " << group.clockPin);
254 // Continue with other groups rather than aborting entirely
255 }
256 }
257
258 FL_DBG("SpiChannelEngineAdapter: show() complete");
259}
260
262 // Check if ANY controller is busy
263 bool anyBusy = false;
264 for (const auto& ctrl : mControllers) {
265 if (ctrl.controller && ctrl.controller->isBusy()) {
266 anyBusy = true;
267 break;
268 }
269 }
270
271 if (anyBusy) {
272 return DriverState::BUSY;
273 }
274
275 // All controllers idle - release transmitting channels
276 if (!mTransmittingChannels.empty()) {
277 FL_DBG("SpiChannelEngineAdapter: Releasing " << mTransmittingChannels.size() << " completed channels");
278
279 for (const auto& channel : mTransmittingChannels) {
280 if (channel) {
281 channel->setInUse(false);
282 }
283 }
284 mTransmittingChannels.clear();
285 }
286
287 // Check for draining state
288 if (!mEnqueuedChannels.empty()) {
289 return DriverState::DRAINING;
290 }
291
292 return DriverState::READY;
293}
294
297) FL_NO_EXCEPT {
299
300 for (const auto& channel : channels) {
301 if (!channel) {
302 continue;
303 }
304
305 // Extract clock pin from chipset configuration
306 const auto& chipset = channel->getChipset();
307 if (!chipset.is<SpiChipsetConfig>()) {
308 FL_WARN("SpiChannelEngineAdapter: Non-SPI chipset in groupByClockPin");
309 continue;
310 }
311
312 const SpiChipsetConfig& spiConfig = chipset.get<SpiChipsetConfig>();
313 int clockPin = spiConfig.clockPin;
314
315 // Find existing group for this clock pin
316 ClockPinGroup* existingGroup = nullptr;
317 for (ClockPinGroup& group : groups) {
318 if (group.clockPin == clockPin && group.requestedBus == spiConfig.spiBus) {
319 existingGroup = &group;
320 break;
321 }
322 }
323
324 // Add to existing group or create new group
325 if (existingGroup) {
326 existingGroup->channels.push_back(channel);
327 } else {
328 ClockPinGroup newGroup;
329 newGroup.clockPin = clockPin;
330 newGroup.requestedBus = spiConfig.spiBus;
331 newGroup.channels.push_back(channel);
332 groups.push_back(newGroup);
333 }
334 }
335
336 return groups;
337}
338
340 if (channels.empty()) {
341 return true;
342 }
343
344 // Extract clock pin and data pin from first channel (all same in batch)
345 const auto& chipset = channels[0]->getChipset();
346 if (!chipset.is<SpiChipsetConfig>()) {
347 FL_WARN("SpiChannelEngineAdapter: Non-SPI chipset in transmitBatch");
348 return false;
349 }
350
351 const SpiChipsetConfig& spiConfig = chipset.get<SpiChipsetConfig>();
352 int clockPin = spiConfig.clockPin;
353 int dataPin = channels[0]->getPin(); // MOSI pin from channel config
354
355 // Select controller for this clock pin
356 int controllerIndex = selectControllerForClockPin(clockPin, spiConfig.spiBus);
357 if (controllerIndex < 0) {
358 FL_WARN("SpiChannelEngineAdapter: No available controller for clock pin " << clockPin);
359 return false;
360 }
361
362 ControllerInfo& ctrl = mControllers[controllerIndex];
363
364 // Initialize controller if needed
365 if (!initializeControllerIfNeeded(ctrl, clockPin, dataPin)) {
366 return false;
367 }
368
369 // Transmit using selected controller
370 for (const auto& channel : channels) {
371 if (!channel) {
372 continue;
373 }
374
375 // Mark channel as in use
376 channel->setInUse(true);
377
378 // Get encoded data
379 const auto& data = channel->getData();
380 if (data.empty()) {
381 FL_WARN("SpiChannelEngineAdapter: Empty channel data");
382 channel->setInUse(false);
383 continue;
384 }
385
386 FL_DBG("SpiChannelEngineAdapter: Transmitting channel via " << ctrl.name << " (pin " << channel->getPin() << ", " << data.size() << " bytes)");
387
388 // Acquire DMA buffer
389 DMABuffer dmaBuffer = ctrl.controller->acquireDMABuffer(data.size());
390 if (!dmaBuffer.ok()) {
391 FL_WARN("SpiChannelEngineAdapter: Failed to acquire DMA buffer (error " << (static_cast<int>(dmaBuffer.error())) << ")");
392 channel->setInUse(false);
393 return false;
394 }
395
396 // Copy data to DMA buffer
397 fl::span<u8> buffer = dmaBuffer.data();
398 if (buffer.size() < data.size()) {
399 FL_WARN("SpiChannelEngineAdapter: DMA buffer too small (" << buffer.size() << " < " << data.size() << ")");
400 channel->setInUse(false);
401 return false;
402 }
403
404 fl::memcpy(buffer.data(), data.data(), data.size());
405
406 // Transmit (async mode for non-blocking operation)
407 if (!ctrl.controller->transmit(TransmitMode::ASYNC)) {
408 FL_WARN("SpiChannelEngineAdapter: Transmission failed");
409 channel->setInUse(false);
410 return false;
411 }
412
413 FL_DBG("SpiChannelEngineAdapter: Transmission queued successfully");
414 }
415
416 // Wait for transmission to complete (synchronous for now)
417 // TODO: Make fully async by returning BUSY state and polling in poll()
418 if (!ctrl.controller->waitComplete(1000)) { // 1 second timeout
419 FL_WARN("SpiChannelEngineAdapter: Transmission timeout");
420 return false;
421 }
422
423 FL_DBG("SpiChannelEngineAdapter: Batch transmission complete");
424 return true;
425}
426
427} // namespace fl
CLEDController * controller
bool transmitBatch(fl::span< const ChannelDataPtr > channels) FL_NO_EXCEPT
Transmit a batch of channels (all same clock pin)
fl::vector< ClockPinGroup > groupByClockPin(fl::span< const ChannelDataPtr > channels) FL_NO_EXCEPT
Group channels by clock pin for efficient transmission.
void enqueue(ChannelDataPtr channelData) FL_NO_EXCEPT override
Enqueue channel data for transmission.
fl::string mName
Engine name for debugging.
int getPriority() const FL_NO_EXCEPT
Get maximum priority among all controllers.
void show() FL_NO_EXCEPT override
Trigger transmission of enqueued data.
bool canHandle(const ChannelDataPtr &data) const FL_NO_EXCEPT override
Check if adapter can handle channel data.
fl::vector< ControllerInfo > mControllers
All managed controllers.
static fl::shared_ptr< SpiChannelEngineAdapter > create(fl::vector< fl::shared_ptr< SpiHwBase > > hwControllers, fl::vector< int > priorities, fl::vector< const char * > names, const char *adapterName) FL_NO_EXCEPT
Create unified adapter managing multiple controllers.
int selectControllerForClockPin(int clockPin, Esp32SpiBus requestedBus) FL_NO_EXCEPT
Select best controller for a given clock pin.
SpiChannelEngineAdapter(const char *name) FL_NO_EXCEPT
Private constructor - use create() factory method.
bool initializeControllerIfNeeded(ControllerInfo &ctrl, int clockPin, int dataPin) FL_NO_EXCEPT
Initialize controller if needed for this clock pin.
bool canControllerHandleClockPin(const ControllerInfo &ctrl, int clockPin) const FL_NO_EXCEPT
Check if controller can handle this clock pin.
DriverState poll() FL_NO_EXCEPT override
Query driver state and perform maintenance.
~SpiChannelEngineAdapter() FL_NO_EXCEPT override
Destructor.
fl::vector< ChannelDataPtr > mEnqueuedChannels
Channels waiting for show()
fl::vector< ClockPinAssignment > mClockPinAssignments
Clock pin → controller mapping.
fl::vector< ChannelDataPtr > mTransmittingChannels
Channels currently transmitting.
Group data structure for channels with same clock pin.
fl::size size() const FL_NO_EXCEPT
const T * data() const FL_NO_EXCEPT
Definition span.h:486
constexpr fl::size size() const FL_NO_EXCEPT
Definition span.h:483
fl::size size() const FL_NO_EXCEPT
void push_back(const T &value) FL_NO_EXCEPT
Definition vector.h:624
#define FL_WARN(...)
Definition log.h:437
#define FL_DBG
Definition log.h:553
Centralized logging categories for FastLED hardware interfaces and subsystems.
constexpr remove_reference< T >::type && move(T &&t) FL_NO_EXCEPT
Definition move.h:30
shared_ptr< T > make_shared(Args &&... args) FL_NO_EXCEPT
Definition shared_ptr.h:423
unsigned char u8
Definition stdint.h:131
Esp32SpiBus
ESP-IDF SPI host selection for an individual clocked LED controller.
Definition spi_bus.h:18
u8 u8 fract8 blur_amount FL_NO_EXCEPT
Definition blur.h:186
FASTLED_FORCE_INLINE void * memcpy(void *dest, const void *src, size_t n) FL_NO_EXCEPT
Definition cstring.h:136
Base definition for an LED controller.
Definition crgb.hpp:179
Adapter that wraps HW SPI controllers for ChannelManager.
Driver state with optional error message.
Definition driver.h:168
fl::shared_ptr< SpiHwBase > controller
Hardware instance.
fl::string name
Name (e.g., "SPI2", "SPI3", "I2S0")
Information about a registered SPI hardware controller.
Esp32SpiBus spiBus
ESP-IDF host override, or AUTO.
Definition config.h:107
int clockPin
GPIO clock pin (SCK)
Definition config.h:105
SPI chipset configuration (data + clock pins)
Definition config.h:103