MultiPinLedDriver
Source:
MultiPinLedDriver.h
MultiPinLedDriver¶
src/light/drivers/MultiPinLedDriver.h:40Inherits:
ParallelLedDriver< MultiPinLedDriver >
Output driver: parallel 8-or-16-lane WS2812B over the ESP-IDF esp_lcd i80 bus — the parallel scale path on all three i80-capable ESP32 families.
RMT gives a chip 4-8 channels; this gives 8-16 lanes for the wall time of one. The magic is that ESP-IDF exposes ONE public i80 API (esp_i80_new_i80_bus / esp_i80_panel_io_tx_color) and routes it to whichever peripheral the silicon has — so this single driver serves every chip:
-
ESP32-S3 / -P4: backed by the dedicated LCD_CAM peripheral.
-
classic ESP32: backed by the I2S peripheral in i80/LCD mode (the classic has no LCD_CAM; I2S-i80 is its only >8-lane route). IDF's own CMake picks the backend by chip; the two are mutually exclusive per silicon, so
[lanesAvailable()]reads whichever lane-count constant is non-zero. Named for the i80 bus (the shared API), not a peripheral, since it isn't one peripheral — same reason its siblings are namedRmt/Parlio(their APIs).
The shared body (slicing, the whole-frame async double-buffer DMA, the fused encode, the loopback self-test, the frameTime KPI) lives in ParallelLedDriver; this class adds only the i80-specific pieces:
-
The sacrificial WR (pixel clock) + DC GPIOs the i80 bus mandates even though WS2812 ignores both, and the "exactly 8 or 16 pins" rule (the i80 layer rejects a partial bus). A sub-16 board parks unused lanes + WR/DC on one spare GPIO (the ghost-pin trick).
-
The 3-slot-per-bit wire contract: each WS2812 bit becomes three bus slots at 2.67 MHz (slot = 375 ns): all-active-lanes HIGH, the data bits, then all LOW — so a
1is HIGH 750 ns and a0375 ns, approximating RMT's 700/350. The slot is deliberately NOT the lineage's ~416 ns: newer WS2812B revisions spec T0H max ≈ 380 ns, and a longer0on a direct 3.3 V line gets misread as1(the strip washes white). One bus word per slot (bus bit L = the L-th pin); unequal strands idle LOW once exhausted. Slot layout: [ParallelSlots.h]. -
Both backends do whole-frame chained DMA (autonomous, CPU out of the timing loop), so the classic I2S path is WiFi-underrun-immune by construction — it does NOT need the ISR-refilled ring / large
nbDmaBuffercushion the raw-register I2S-clockless lineage requires. -
The platform::i80Ws2812* calls (ESP-IDF's esp_lcd i80 bus + GDMA).
Prior art: Adafruit's LCD_CAM discovery, hpwit's I2SClockless lineage (classic-ESP32 I2S parallel), FastLED's S3 driver — architecture studied, never copied. We build on IDF's maintained esp_lcd i80 abstraction rather than tracing the raw-register I2S driver (Industry standards, our own code).
Public Attributes¶
int8_t clockPin = 10
: WR (pixel clock) and DC: the IDF i80 bus requires both on real GPIOs (esp_i80_panel_io_i80.c: wr_gpio_num >= 0 && dc_gpio_num >= 0), yet the WS2812 strands ignore both — they are peripheral-fixed, not user-strand wiring, so a sensible overridable default cannot do harm (same class as the chip-fixed Ethernet pins).
int8_t dcPin = 11
Public Methods¶
inline uint16_t clockPinForBus() const
: Spare bus lanes (shift mode, when the board has fewer data pins than the bus is wide) park on WR: the peripheral already drives it and the board already wires it, so the lane is inert.
inline void addBusControls()
: Bind the i80-specific bus controls: the sacrificial WR (clockPin) and DC pins the peripheral mandates.
inline bool busControlTriggersBuild(constchar * name) const
: A clockPin or dcPin change triggers a bus rebuild via the prepare sweep.
inline constchar * validateBusFatal() const
: Reject a data lane that collides with the WR (clockPin) or DC pin.
inline constchar * validateBusPins(constuint16_t * lanes, uint8_t n) const
inline bool busInit(size_t frameBytes, bool wantSecondBuffer)
: The bus pin list comes from the base: in shift mode it appends the latch to the data pins (the latch is a bus lane), so the peripheral drives it.
inline uint8_t * busBuffer(uint8_t i)
: DMA buffer i (0/1) the base encodes into; buffer 1 is null when the second buffer didn't fit (single-buffer mode).
inline size_t busCapacity() const
: The per-buffer byte capacity (fixed at bus creation; both buffers equal).
inline bool busTransmit(uint8_t i, size_t bytes)
: Kick off the autonomous transfer of the first bytes of DMA buffer i; returns whether it started.
inline bool busWait(uint8_t i, uint32_t ms)
: Block up to ms for buffer i's in-flight transfer to complete.
inline uint32_t busLastTransmitUs() const
: The most recent DMA transfer's wire time (µs) — the WS2812 output floor.
inline void busDeinit()
: Tear down the i80 bus and its DMA buffer.
inline platform::RmtLoopbackResult busLoopback(constuint8_t * frame, size_t frameBytes, size_t dataBytes, uint8_t rowBits)
: Run the loopback self-test.
inline void recordBusPins()
: Store WR/DC alongside the data pins, so a clockPin/dcPin edit rebuilds the bus too (not just a data-pin change).
inline bool extraBusPinsCurrent() const
: Whether the live bus's WR/DC pins still match the current clockPin/dcPin (so the base can skip a rebuild).
Public Static Attributes¶
constexprbool kPowerOfTwoBus =
constexprbool kLoopbackFullWidth =
constexprconstchar * kInitFailMsg = "i80 init /
constexprbool kSupportsPinExpander = platform::lcdLanes > 0
: Create the i80 bus + its DMA buffer(s) sized for frameBytes on the current data lanes plus the WR/DC pins; wantSecondBuffer requests the async double-buffer's second frame buffer (allocated only if it fits — else single-buffer).
Public Static Methods¶
constexpr static inline constexpruint8_t lanesAvailable()
: The number of i80 lanes this chip provides (0 = no i80 bus on this chip); the base's inert-on-wrong-chip guards key off it.