Skip to content

MultiPinLedDriver

Source: MultiPinLedDriver.h

I80Peripheral

class I80Peripheral
src/light/drivers/MultiPinLedDriver.h:41

Inherits: LedPeripheral

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 backend serves every chip:

  • ESP32-S3 / -P4 / -S31: 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 sibling backend is named Parlio (its own API).

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 backend 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 1 is HIGH 750 ns and a 0 375 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 longer 0 on a direct 3.3 V line gets misread as 1 (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 silicon paths 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 nbDmaBuffer cushion 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 = platform::i2sLanes > 0 ? -1 : 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 = platform::i2sLanes > 0 ? 33 : 11

Public Methods

virtual inline uint8_t lanesAvailable() const override : The number of i80 lanes this chip provides (0 = no i80 bus on this chip); the orchestrator's inert-on-wrong-chip guards key off it.

virtual inline bool powerOfTwoBus() const override : Does the bus width round up to a power of two (8/16), or is it the exact pin count?

virtual inline size_t dmaBudgetBytes() const override : Whole-frame DMA byte budget.

virtual inline bool loopbackFullWidth() const override : Must the loopback self-test build a full-width bus (true) or can it run on a private 1-lane unit (false)? esp_lcd i80 / MoonI80 need the full width; Parlio can do a single lane.

virtual inline LedHwBlock hwBlock() const override : The classic ESP32's esp_lcd-i80 backend IS the I2S peripheral; on the LCD_CAM chips (S3/P4/S31) it is LcdCam (shared with MoonI80, which is why the two conflict).

virtual inline bool busContentionCleared() const override : The classic ESP32's i80 IS an I2S peripheral, and this bus always drives from instance 1, leaving instance 0 (the only one with a PDM converter) for audio.

virtual inline const char * initFailMsg() const override : The status message when bus init fails on this peripheral.

virtual inline uint16_t clockPinForBus() const override : 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.

virtual inline void addBusControls(ControlList & controls) override : Bind the i80-specific bus controls: the sacrificial WR (clockPin) and DC pins the peripheral mandates.

virtual inline bool busControlTriggersBuild(const char * name) const override : A clockPin or dcPin change triggers a bus rebuild via the prepare sweep.

virtual inline const char * validateBusFatal() const override : FATAL bus-pin check → routed to the ERROR path (idles the driver), unlike validateBusPins' per-lane WARNINGS.

virtual inline const char * validateBusPins(const uint16_t * lanes, uint8_t n) const override : Reject a data lane that collides with the WR (clockPin) or DC pin.

virtual inline bool supportsPinExpander() const override : 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).

virtual inline bool busInit(size_t frameBytes, bool wantSecondBuffer) override : The bus pin list comes from the orchestrator: in shift mode it appends the latch to the data pins (the latch is a bus lane), so the peripheral drives it.

virtual inline uint8_t * busBuffer(uint8_t i) override : DMA buffer i (0/1) the orchestrator encodes into; buffer 1 is null when the second buffer didn't fit (single-buffer mode).

virtual inline size_t busCapacity() const override : The per-buffer byte capacity (fixed at bus creation; both buffers equal).

virtual inline bool busTransmit(uint8_t i, size_t bytes) override : Kick off the autonomous transfer of the first bytes of DMA buffer i; returns whether it started.

virtual inline bool busWait(uint8_t i, uint32_t ms) override : Block up to ms for buffer i's in-flight transfer to complete.

virtual inline uint32_t busLastTransmitUs() const override : The most recent DMA transfer's wire time (µs) — the WS2812 output floor.

virtual inline void busDeinit() override : Tear down the i80 bus and its DMA buffer.

virtual inline platform::RmtLoopbackResult busLoopback(const uint8_t * frame, size_t frameBytes, size_t dataBytes, uint8_t rowBits) override : Run the loopback self-test.

virtual inline void recordBusPins() override : Store WR/DC alongside the data pins, so a clockPin/dcPin edit rebuilds the bus too (not just a data-pin change).

virtual inline bool extraBusPinsCurrent() const override : Whether the live bus's WR/DC pins still match the current clockPin/dcPin (so the orchestrator can skip a rebuild).