ParallelLedDriver
Source:
ParallelLedDriver.h
ParallelLedDriver¶
src/light/drivers/ParallelLedDriver.h:59Inherits:
DriverBase
Base for the parallel WS2812B LED-output drivers — the S3's LCD_CAM i80 bus (MultiPinLedDriver) and the P4's Parlio peripheral (ParlioLedDriver).
Both drive up to 16 strands that clock out SIMULTANEOUSLY, one GPIO lane each, fed consecutive slices of the source buffer (see kMaxLanes). (Vocabulary — strand / lane / slot / row — under More info → Terminology.)
How it works: encode the whole frame, then hand it to the DMA and walk away. Every WS2812 bit of every strand is written into one buffer up front — including a zeroed ≥300 µs pad at the end, the reset that tells the strands the frame is over — and then shipped as a single autonomous transfer. The encode is a fused correct + transpose, per row ([ParallelSlots.h]; the transpose mechanism is under More info → the frame transpose).
Why single-shot matters: there is NO CPU deadline while a frame is on the wire. A driver that refills buffers as the DMA drains them must beat the clock on every refill, and a WiFi interrupt at the wrong moment slips a bit and garbles the rest of the frame. Encoding first makes that impossible by construction. (The streaming ring in MoonLedDriver deliberately gives this up — it is the price of a frame too big to hold.)
Slicing: the strands are fed consecutive slices of this driver's window, in pins order, sized by ledsPerPin with the remainder split evenly — the same rule (and the same parser) as RmtLedDriver, so a strand count means the same thing on every LED driver.
CRTP, not virtual calls. The base calls into the derived through static_cast<Derived*>(this)->busX(), so the shared body costs no vtable and no runtime indirection on the hot path, and the module tree stays the project's one deliberate class hierarchy (the only virtual boundary is MoonModule -> DriverBase). A derived driver supplies only the peripheral-specific pieces: the bus* platform wrappers, lanesAvailable() (which makes it inert on the wrong chip), kPowerOfTwoBus (the i80 bus rounds to 8 or 16; Parlio's width IS its pin count), the slot rate kClockHz, and any extra bus pins it owns. The two drivers were ~250 of ~370 lines byte-for-byte identical before this base existed.
Public Attributes¶
char pins = ""
: Comma-separated GPIO list, one parallel lane per pin — up to kMaxLanes strands clocked out SIMULTANEOUSLY, fed consecutive slices of this driver's window.
char ledsPerPin = ""
: Comma-separated lights-per-lane; the unassigned remainder splits evenly over the remaining lanes.
bool doubleBuffer =
: Async double-buffer transmit (default ON).
bool ringSnapshot =
: Streaming-ring source snapshot on/off — an A/B measurement knob, ring path only.
bool loopbackTest =
: On-device loopback self-test — jumper a lane's TX to loopbackRxPin, tick to transmit a known WS2812 pattern and bit-verify the capture, proving the peripheral emits correct bytes on real silicon.
int8_t loopbackTxPin = -1
: Optional TX override for the self-test: when set (>= 0), the loopback transmits on THIS pin in place of lane 0 (laneList_[0]); other lanes are unchanged.
uint8_t loopbackStrand = 0
: Which STRAND carries the test pattern (shift mode only; ignored in direct mode, where the pattern always rides lane 0).
bool loopbackIntrusive =
: Loopback mode: OFF (default) tears the output down and transmits a PRIVATE test frame on a rebuilt bus — proves the peripheral in isolation, but on the ring path it must re-allocate the whole pool, which fails on a fragmented heap, and it tests a replica.
int8_t loopbackRxPin = -1
: Jumper this to the TX lane for the self-test (unset = -1 by default).
bool pinExpander =
: Is a 74HCT595 shift-register expander board fitted? OFF (the default) = strands wired straight to the GPIOs; ON = each data pin feeds one '595, so the driver drives pins × 8 strands.
int8_t latchPin = -1
: The 74HCT595 LATCH (RCLK) line — pulsed once the shifted byte is in, presenting it on the '595 outputs.
Public Methods¶
inline ParallelLedDriver()
: WS2812/SK6812 strips are GRB-wired, so a fresh parallel LED driver (and its MultiPinLedDriver subclass) references the "GRB" preset by default.
inline bool pinExpanderMode() const
: Is the shift-register expander engaged? (Reads the control; the alias keeps the intent readable at the call sites that ask "am I encoding through a register?")
inline uint8_t outputsPerPin() const
: Strands driven per physical data pin: 1 direct, or the '595's width through the expander.
virtual inline void defineDriverControls() override
: Bind the driver's controls: the window (start/count), the pins and ledsPerPin text lists, any derived-supplied bus controls (i80 adds clockPin/dcPin, Parlio none), and the loopback self-test controls (TX/RX pin overrides always bound but shown only in test mode).
virtual inline bool affectsPrepare(constchar * name) const override
: A change to the pins, per-lane counts, the window, a derived bus control (clockPin/dcPin on i80), or doubleBuffer re-parses and re-inits the bus live via the prepare sweep.
virtual inline void onControlChanged(constchar * name) override
: React to a control change off the render loop.
virtual inline void setup() override
: One-time wiring only (parse the lane lists into members); the bus acquire lives in [prepare()], the sole resource gate.
virtual inline void release() override
: Deinit the bus, then clear the shared fail/config-error state (DriverBase::release()).
inline void freeSnapshot()
: Free the ring snapshot buffer + clear the encode source and refresh the memory readout.
virtual inline void prepare() override
: Pure build (see MoonModule::prepare): re-parse the lanes and (re)init the bus off the hot path.
virtual inline void onCorrectionChanged() override
: RGB<->RGBW changes the bytes-per-light and therefore the frame size, so re-parse and re-init the bus — but ONLY when the frame size actually changed.
virtual inline void setSourceBuffer(Buffer * buf) override
: Point the driver at the source frame buffer and re-parse the lane config.
virtual inline void tick() override
: Per-tick output (deferred-wait double-buffer): a fused per-ROW pass corrects the same light index of every active lane and transposes it into 3-slot bus words in the DMA buffer, then ships it as one autonomous transfer.
inline void tickSync(uint8_t outCh)
: Blocking path (doubleBuffer OFF): encode the frame, send it, and wait out the wire before returning — so a tick costs encode + wire.
inline void tickAsync(uint8_t outCh)
: Deferred-wait path (doubleBuffer ON, the default): encode frame N+1 into the back buffer while frame N clocks out of the front, so a tick costs max(encode, wire) instead of encode + wire.
inline void tickRing(uint8_t)
: Streaming-ring path: wait for the previous frame, arm the next, and RETURN — the wire and its refills run in the background.
virtual inline void tick1s() override
: Refresh the read-only frameTime KPI once a second (off the hot path): the last measured DMA wire time and the fps ceiling it implies (1e6 / frameTime).
inline void refreshBusKpi()
: CRTP hook: refresh any backend-specific read-only KPI once a second (off the hot path).
inline bool busWaitIfBusy(uint8_t i)
: Wait for buffer i's transfer if one is still in flight; false means it wedged (the caller then skips the frame rather than writing into memory the DMA may still be reading).
inline bool busGaveUp()
: How long a transfer gets before we call it dead.
inline uint32_t waitBudgetMs() const
: Computed from the WS2812 wire contract, which is the same on every backend and needs no platform constant: one light clocks channels × 8 bits, each bit is 3 slots, and a slot is ~375 ns of WIRE time — the expander changes how many bus words fill a slot, not how long the strand takes.
inline void drainInFlight()
: Block until nothing is reading the DMA buffers or the source snapshot — the barrier that makes a live resize safe (a grid change or an RGB->RGBW switch must not free memory a transfer is reading).
inline void prefillShiftConstantsIfNeeded()
: Prefill both DMA buffers' shift-mode constants (a no-op in direct mode, where every slot word already carries data or is a zero the buffer's memset provides).
template<classSlot> inline void prefillShiftFrame(uint8_t outCh, uint8_t * dst)
: Write the shift-mode frame CONSTANTS into every DMA buffer, once, off the hot path.
template<classSlot> inline voidMM_RAMFUNC prefillShiftRows(uint8_t outCh, uint8_t * dst, nrOfLightsType firstRow, nrOfLightsType rowCount)
: Prefill the shift constants for rows [firstRow, firstRow + rowCount) into dst, written dst-RELATIVE (row firstRow lands at dst+0) — so a ring buffer holding one slice gets its constants laid down exactly as the whole-frame buffer does for the same rows.
template<classSlot> inline voidMM_RAMFUNC encodeRows(uint8_t outCh, uint8_t * dst, nrOfLightsType firstRow = 0, nrOfLightsType rowCount = 0, bool closeFrame = true)
: Encode rows [firstRow, firstRow + rowCount) into dst — the whole frame in one call for the direct paths, one slice per call for the ring (rowCount == 0 means "to the end").
inline voidMM_RAMFUNC encodeFrameClose(uint8_t * dst)
: Write ONLY the frame-closing latch word at dst — the shift expander's one-more-latch that presents the register's final slot on the strand (see encodeWs2812ShiftLatchPad).
template<classSlot> inline void encodeLoopbackFrame(uint8_t * frame, constuint8_t * wire, uint8_t outCh, nrOfLightsType lights)
: Build the self-test frame for DIRECT mode: the known pattern on lane 0, every other lane idle.
template<classSlot> inline void encodeLoopbackFrameShift(uint8_t * frame, constuint8_t * wire, uint8_t outCh, nrOfLightsType lights)
: Build the self-test frame for EXPANDER mode: the pattern on one '595 output (loopbackStrand), every other strand dark, the latch on its own bus bit exactly as at runtime — so the capture proves the expander's real wire, not a simplified one.
inline uint8_t laneCount() const
: Test-only accessors — pin the lane slicing and frame-size arithmetic on the host (unit_{I80,Parlio}LedDriver.cpp); the hardware half is proven on device.
inline nrOfLightsType laneLightCount(uint8_t i) const
: Lights on lane i (0 if out of range). Test-only.
inline boolMM_RAMFUNC uniformLaneCounts() const
inline nrOfLightsType laneStart(uint8_t i) const
: First light index of lane i's slice (0 if out of range). Test-only.
inline nrOfLightsType maxLaneLights() const
: Length of the longest lane — the frame's row count. Test-only.
inline size_t frameBytes() const
: Total DMA frame size in bytes (rows + latch pad). Test-only.
Public Static Attributes¶
constexpruint8_t kMaxLanes = 16
: Max parallel lanes = the peripheral's full 16 data lines.
constexpruint8_t kMaxStrands = 64
: Max strands the driver can drive: every data line fanned out through its '595 chain.
constexpr nrOfLightsType kLoopbackTestLights = 256
: Light count the loopback self-test drives (or maxLaneLights_ if the strip is smaller).
volatileuint32_t dbgTickWaitUs = 0
volatileuint32_t dbgTickSnapUs = 0
volatileuint32_t dbgTickPrimeUs = 0
uint32_t dbgSegGatherCy = 0
: All POPULATED strands the same length? Gates the ring's prefill-skip: what the skip actually requires is a per-row active mask that is constant across the frame — then a recycled buffer's prefilled constants stay valid across slices.
uint32_t dbgSegEmitCy = 0
uint32_t dbgSegRows = 0
Public Static Methods¶
static inline bool isTestParamControl(constchar * name)
: Loopback PARAMETER controls: not pins (no bus rebuild), but a change must re-run a running test so the verdict tracks the new setting (e.g.
More info¶
Terminology¶
A strand is one chain of LEDs. A lane is one bus data line — in the normal case, one GPIO driving one strand. A slot is one WS2812 bit on the wire, and a row is one light across every strand at once (so a frame is maxLaneLights rows).
The frame transpose (correct + transpose, per row)¶
The source has each light's bytes together, but the wire needs each bus WORD to carry one bit of EVERY strand at the same instant (bit L of the word = data line L). So the encoder turns 8 lights' bytes on their side — an 8x8 bit matrix transpose — and writes one word per slot, fused with the per-light correction in one pass ([ParallelSlots.h]). Both peripherals take the identical bytes: a Parlio bus word and an i80 bus word have the same meaning.