Skip to content

RmtLedDriver

Source: RmtLedDriver.h

RmtLedDriver

class RmtLedDriver
src/light/drivers/RmtLedDriver.h:39

Inherits: DriverBase

Output driver: WS2812B-class addressable LEDs over the ESP32 RMT (Remote Control Transceiver) peripheral — one GPIO and one RMT TX channel per strand, fed consecutive slices of the source buffer (8-bit, GRB).

The default LED driver for classic-ESP32 and S3 board entries, and the readable EXAMPLE future LED drivers copy: a sibling of NetworkSendDriver (same DriverBase hooks, same per-light correction_.apply() guard, same once-allocated owned buffer sized off the hot path); only the emit differs — this fuses the correction + WS2812 symbol-encode into one pass (the encode is RmtSymbol.h, host-tested) then hands per-pin slices to the platform.

Wire contract — WS2812B: 1-wire NRZ at 800 kHz, no clock line. Each bit is a 1.25 µs cell that starts HIGH then drops LOW; the HIGH duration encodes the bit (0 = 350 ns high, 1 = 700 ns high), MSB-first per byte. Channel order (GRB, GRBW, …) is applied by Correction before the encode, so the encoder is order-agnostic. Frames latch on ≥300 µs idle-LOW. Timings live in LedDriverConfig, converted to RMT ticks from the granted resolution (~40 MHz), never hard-coded to one clock. The platform owns only the peripheral (platform::rmtWs2812*); on a chip without RMT TX channels those calls are inert stubs, so it compiles everywhere. The peripheral half uses the modern RMT driver (ESP-IDF 5.x "RMT v2": rmt_new_tx_channel / a copy encoder / rmt_transmit), not the legacy channel-numbered API — not a preference: the legacy driver was removed entirely in ESP-IDF v6 (the build IDF), so v2 is the only API that exists. On chips whose RMT has a DMA backend (SOC_RMT_SUPPORT_DMA — the P4; the classic ESP32 has none) the whole-frame loopback capture uses it.

Flicker on LEDs that should be off is almost always a data-line signal-integrity problem (3.3 V drive into a 5 V strip), not firmware — the "LED signal integrity" use-case guide has the confirm-firmware-innocent playbook (loopbackFrame, the TX-power sweep) and the electrical fixes.

RmtLedDriver card

Public Attributes

char pins = "" : Comma-separated GPIO list, one RMT TX channel per pin ("18,17,16").

char ledsPerPin = "" : Comma-separated lights-per-pin ("100,100,50"), matched to pins by position — each pin takes the next consecutive slice of the source buffer, in list order (pin 1 = [0,n₁), pin 2 = [n₁,n₁+n₂), …).

bool loopbackTest = : On-device loopback self-test — RMT is a transceiver, so the driver verifies its own output on real silicon (replaces the old standalone test firmware).

int8_t loopbackTxPin = -1 : Optional TX override for the test: when set (>= 0), the loopback transmits on THIS pin in place of pins[0], so the test can run on a dedicated jumper without re-typing the operational pins.

int8_t loopbackRxPin = -1 : Jumper this to the TX pin for the test (unset = -1 by default; bench used pin 5).

bool loopbackFrame = : Whole-frame stress variant: instead of a 24-bit burst, transmit a real frame the size of the first pin's slice, back to back, and bit-verify the WHOLE capture.

Public Methods

inline RmtLedDriver() : WS2812/SK6812 strips are physically GRB-wired, so a fresh RMT driver references the "GRB" preset by default (a strip attached to a freshly-flashed board shows correct colors).

virtual inline void defineDriverControls() override : Bind the driver's controls: the window (start/count), the pins and ledsPerPin text lists, and the loopback self-test controls (the TX/RX pin overrides and frame-stress flag are always bound but shown only in test mode).

virtual inline bool affectsPrepare(constchar * name) const override : Changing the pin list or the per-pin counts re-parses and re-inits the RMT channels (live, not reboot-to-apply), so the pipeline-wide prepare sweep runs and parseConfig()/reinit() pick up the new lists.

virtual inline void onControlChanged(constchar * name) override : React to a control change (runs off the render loop, in the HTTP/API handler context — a blocking self-test here is fine).

virtual inline void setup() override : Parse the config and (re)init the RMT channels.

virtual inline void release() override : Release the RMT channels and free the symbol buffer, then clear the shared fail/config-error state (DriverBase::release()).

virtual inline void prepare() override : Pure build (see MoonModule::prepare): re-parse, resize the symbol buffer, and (re)init the RMT channels off the hot path ([tick()] never allocates).

virtual inline void onCorrectionChanged() override : Preset toggle (RGB↔RGBW) changes outChannels without a structural rebuild — the per-pin symbol offsets scale with outChannels, so re-derive them too.

virtual inline void setSourceBuffer(Buffer * buf) override : Point the driver at the source frame buffer; re-parse (counts derive from its light count) and resize the symbol buffer to match.

virtual inline void tick() override : Per-tick output: fuse the correction and WS2812 symbol-encode in one pass over this driver's window, then start every pin's transmit before waiting on any, so the tick costs the longest strand rather than the sum.

inline constuint32_t * symbolBuffer() const : Test-only accessors.

inline size_t symbolCapacity() const : Words allocated in the symbol buffer. Test-only.

inline uint8_t pinCount() const : Number of parsed output pins (0 = idle). Test-only.

inline nrOfLightsType pinLightCount(uint8_t i) const : Lights on pin i (0 if out of range). Test-only.

inline size_t pinSymbolOffsetWords(uint8_t i) const : Word offset of pin i's slice in the symbol buffer (0 if out of range). Test-only.

Public Static Attributes

constexpruint8_t kMaxPins = 8 : Hard cap on the pin arrays: the largest RMT TX group of any supported chip (8 on classic ESP32; the S3 has 4 — enforced per target via maxPinsForTarget()).

constexpruint32_t kResolutionHz = 40'000'000