|
FastLED 3.9.15
|
Directory dependency graph for channels:Directories | |
| adapters | |
| detail | |
| rx | |
| spi | |
Files | |
| _build.cpp.hpp | |
| all_drivers.h | |
Declaration of fl::enableAllDrivers() — enrolls every channel driver available on the current platform into ChannelManager. | |
| bus.h | |
| Compile-time identifier for an LED channel transmission bus. | |
| bus_priorities.h | |
| Per-Bus priority constants for ChannelManager registration. | |
| bus_traits.h | |
Per-driver traits keyed on fl::Bus. | |
| channel.cpp.hpp | |
| channel.h | |
| ESP32-P4 Parallel IO (PARLIO) LED channel. | |
| channel_events.cpp.hpp | |
| channel_events.h | |
| channel_typed.h | |
Phase 3b templated channel facade – Channel<Bus, Chipset> with compile-time bus/chipset enforcement. | |
| chipset_helpers.h | |
| cled_controller.h | |
| base definitions used by led controllers for writing out led data | |
| config.cpp.hpp | |
| config.h | |
| data.cpp.hpp | |
| data.h | |
| Channel transmission data - lightweight DTO for driver transmission. | |
| driver.cpp.hpp | |
| driver.h | |
| ichannel.h | |
Type-erased base for the templated Channel<Bus, Chipset> family. | |
| id_tracker.cpp.hpp | |
| id_tracker.h | |
| manager.cpp.hpp | |
| manager.h | |
| Unified manager for channel drivers with priority-based fallback. | |
| options.h | |
| rx.cpp.hpp | |
| Implementation of RxDevice factory. | |
| rx.h | |
| Common RX interfaces and shared types. | |
| rx_sct_capture.h | |
| Public re-export of the LPC SCT-capture RX driver (FastLED#3015). | |
| spi.h | |
| Multi-lane SPI interface for LED output Hardware (SPI_HW): 1-8 parallel data lanes Software (SPI_BITBANG, SPI_ISR): up to 32 parallel data lanes See examples/Spi/Spi.ino for usage example. | |
| validation.cpp.hpp | |
| validation.h | |
| wave3.cpp.hpp | |
| Wave3 waveform generation and transposition implementation. | |
| wave3.h | |
| wave8.cpp.hpp | |
| wave8.h | |
The Channels API provides a modern, hardware-accelerated interface for driving multiple LED strips in parallel with DMA-based timing. It abstracts platform-specific hardware (PARLIO, RMT, SPI, I2S) into a unified interface that works across ESP32 variants and other microcontrollers.
Key benefits:
The system consists of two layers:
Users create Channel objects using the Channel API (FastLED.add(cfg) / Channel::create(cfg)) or the template-based FastLED.addLeds<>() API. Both route through the same ChannelManager and driver layer; pick by call-site shape, not by maturity. The driver layer is managed automatically based on platform capabilities and priorities.
Two complementary dispatch modes are available (introduced by issue #2428, refined by #2459 / #2460):
Compile-time fl::Bus binding — two equivalent entry points pin the driver at compile time:
fl::TypedChannel<fl::Bus::RMT, fl::ClocklessChipset>::create(cfg) — strongly-typed factory with static_assert for bus/chipset compatibility.FastLED.addLeds<WS2812, 4, GRB, fl::Bus::RMT>(leds, NUM) — every addLeds<> variant takes an optional trailing fl::Bus B = fl::Bus::AUTO template parameter (#2460).In either case, naming Bus::X at the call site is what links the driver's translation unit, so --gc-sections drops every driver the sketch doesn't reference. Bus/chipset mismatches become static_assert errors rather than runtime warnings.
FastLED.add(cfg) is non-template. Pick the driver by setting cfg.options.mBus = fl::Bus::RMT (typed enum class). The non-template path auto-enrolls every driver on the platform via fl::enableAllDrivers() and emits a one-time FL_WARN_ONCE explaining the binary-size trade-off (suppress with -DFASTLED_SUPPRESS_RUNTIME_DRIVER_WARNING). For minimum binary size, use the compile-time path instead. Custom/mock drivers (whose names aren't in the fl::Bus enum) bind via priority dispatch — register the mock with manager.addDriver() and either let it win by priority, or use manager.setExclusiveDriver(name) to force-select.The Channel API provides a clean, explicit interface for creating and configuring LED strips:
Benefits:
addLeds<> APIThe familiar template-based FastLED.addLeds<>() form is a one-line convenience over the Channel API. It's the right pick for short sketches that don't need to reconfigure at runtime.
Every addLeds<> variant also accepts an optional trailing fl::Bus B = fl::Bus::AUTO template parameter — see "Compile-Time Bus Selection" below for how to pin the driver from the call site.
fl::Bus)The fl::Bus enum (in fl/channels/bus.h) is the single identifier that flows through both the templated APIs and the runtime registry overrides. Each value names exactly one concrete driver:
fl::Bus::X | Driver string (busName(X) / IChannelDriver::getName()) |
|---|---|
RMT | "RMT" |
PARLIO | "PARLIO" |
SPI | "SPI" |
I2S | "I2S" |
I2S_SPI | "I2S_SPI" |
LCD_RGB | "LCD_RGB" |
LCD_SPI | "LCD_SPI" |
LCD_CLOCKLESS | "LCD_CLOCKLESS" |
UART | "UART" |
FLEX_IO | "FLEX_IO" |
OBJECT_FLED | "OBJECT_FLED" |
BIT_BANG | "BIT_BANG" |
STUB | "STUB" |
AUTO | sentinel - resolves to DefaultBus<Chipset>::value for the platform |
busName(Bus) returns the canonical string literal. This is what ChannelManager::findDriverByName matches against each driver's getName(). Driver names match the enumerator exactly, including underscores ("BIT_BANG", "FLEX_IO", "OBJECT_FLED").
Strongly-typed ChannelConfigOf<Chipset> (Phase 3b, #2428): the TypedChannel<Bus, Chipset>::create(cfg) template accepts a ChannelConfigOf<ClocklessChipset> or ChannelConfigOf<SpiChipsetConfig> and static_asserts via BusSupports<B, Chipset>::value that the chosen bus actually handles the chipset family:
TypedChannel<Bus, Chipset> lives in fl/channels/channel_typed.h. It returns a ChannelPtr to the regular non-template runtime Channel so callbacks, the draw list, and ChannelManager see one channel type.
addLeds<> Bus pinning (#2460): every FastLED.addLeds<> variant accepts an optional trailing fl::Bus B = fl::Bus::AUTO template parameter. B = AUTO (the default) leaves call sites byte-for-byte unchanged; B != AUTO ODR-uses fl::BusTraits<B>::instance via fl::busKeepAlive<B>() so --gc-sections retains the named driver TU.
The Bus parameter triggers linker keep-alive in every variant. For the SPI variants on the FASTLED_SPI_USES_CHANNEL_API branch, the parameter also populates cfg.options.mBus = B so the channel routes through the named driver at runtime. Non-Channel-API controllers (older ClocklessController subclasses that pre-date the Channel API) keep their platform-default routing and rely on the linker keep-alive alone — for full runtime routing through a specific Bus, prefer FastLED.add(cfg) with cfg.options.mBus = B.
FastLED.add(cfg))FastLED.add(cfg) is non-template (#2459). Pick the driver via cfg.options.mBus:
cfg.options.mBus = fl::Bus::RMT — pin to a specific driver. The dispatch looks up busName(mBus) in ChannelManager.cfg.options.mBus = fl::Bus::AUTO (the default) — ChannelManager picks the highest-priority driver that canHandle the chipset.For custom / third-party / mock drivers whose names aren't in the fl::Bus enum, register the driver with manager.addDriver(priority, driver) and either (a) clear competing drivers first so priority dispatch picks it, or (b) call manager.setExclusiveDriverByName(name) for process-wide binding (the by-name escape hatch — manager.setExclusiveDriver(fl::Bus) is the typed form for built-in drivers).
The non-template path auto-enrolls every driver on the platform (fl::enableAllDrivers() runs inside) so any mBus value can be dispatched at runtime. A one-time FL_WARN_ONCE explains the binary-size trade-off; suppress it with -DFASTLED_SUPPRESS_RUNTIME_DRIVER_WARNING. For minimum binary size, use the compile-time TypedChannel<...>::create() path above.
If cfg.options.mBus names a driver that — for whatever reason — isn't in the manager's registry, Channel::showPixels emits a one-shot FL_ERROR listing the resolution options (fl::enableDrivers<fl::Bus::X>(), FastLED.enableAllDrivers(), or the FastLED.addLeds<..., fl::Bus::X>(...) shape) and falls back to AUTO/priority dispatch (#2455, #2460).
Passing fl::Bus::AUTO (the default) skips the pinning step and lets ChannelManager pick by priority — identical to constructing the config without touching cfg.options.mBus.
enableDrivers<> / enableAllDrivers / setExclusiveDriver<>)Default behaviour: no driver auto-registration. Only the platform-default driver TU (named by the legacy clockless controller's Phase 5b pre-bind via BusTraits<DefaultBus<Chipset>>::instancePtr()) is linked into the binary; every other driver is --gc-sections-eligible until something names its BusTraits<Bus::X>::instancePtr(). This is the binary-size fix for #2420 / #2421 — the old FASTLED_DISABLE_LEGACY_DRIVER_REGISTRY macro has been removed; the default IS the opt-in path.
To register additional drivers at runtime, sketches pick one of three opt-in calls:
FastLED.enableAllDrivers()is defined inlibfastled(no extra include needed).-Wl,--gc-sectionsdrops the call graph — every driver TU it references — when no sketch calls it, so the opt-in remains zero-cost for sketches that don't need it.
Including the matching per-driver bus_traits.h is the explicit opt-in that makes the BusTraits<Bus::X> specialization visible at the call site — without that include the call fails to link and --gc-sections stays free to drop the driver TU.
The system automatically selects the best hardware driver based on platform capabilities:
Engines are tried in priority order (highest first) until one accepts the channel. Default priorities live in fl/channels/bus_priorities.h; setDriverPriority(name, n) overrides them at runtime.
ESP32 family:
| Engine | Priority | Platforms | Notes |
|---|---|---|---|
| I2S_SPI | 10 | ESP32-dev (original) | Native I2S parallel SPI for true SPI chipsets |
| LCD_SPI | 10 | ESP32-S3 | LCD_CAM SPI driver for true SPI chipsets |
| PARLIO | 4 | ESP32-P4, C6, H2, C5 | Parallel I/O with hardware timing |
| LCD_RGB | 3 | ESP32-P4 | LCD RGB peripheral (parallel clockless) |
| RMT | 2 (Recommended default) | All ESP32 variants | Reliable, broad chipset support |
| LCD_CLOCKLESS | 2 | ESP32-S3 | LCD_CAM clockless (replaces the misnamed I2S) |
| I2S | 1 | ESP32-S3 | LCD_CAM via legacy I80 bus (experimental) |
| SPI | 0 | ESP32, S2, S3 | DMA-based, deprioritized due to reliability |
| UART | -1 | All ESP32 variants | Wave8 encoding (experimental, not recommended) |
Teensy 4.x:
| Engine | Priority | Notes |
|---|---|---|
| FLEX_IO | 1 | FlexIO2 driver |
| OBJECT_FLED | 1 | ObjectFLED driver |
Portable fallbacks:
| Engine | Priority | Notes |
|---|---|---|
| BIT_BANG | 0 | Cycle-counted GPIO toggling fallback |
| STUB | 0 | Native/host/test stub driver |
For testing or performance tuning, you can control driver selection:
Control methods:
FastLED.setExclusiveDriver<fl::Bus::X>() - Link the named driver TU and register it at priority above the platform default (production opt-in path; compile-time TU-link). Must be called before addLeds<> / FastLED.add().FastLED.setExclusiveDriver(fl::Bus) - Runtime-typed: disable all drivers except the named one. Typed, typo-safe (fl::Bus::RTM is a compile error). Does NOT link a driver TU — use the template form above for that.fl::ChannelManager::instance().setExclusiveDriverByName(name) - By-name escape hatch for mocks / custom drivers not in fl::Bus. Does NOT link a driver TU.FastLED.setDriverEnabled(name, enabled) - Enable/disable a specific already-registered driver.fl::ChannelManager::instance().setDriverPriority(name, priority) - Change priority (triggers automatic re-sort). No FastLED.* forwarder is provided for this.When to override:
Default behavior is recommended - automatic selection provides optimal performance and reliability.
Register callbacks for channel lifecycle events:
Available events:
onChannelCreated - After channel constructiononChannelAdded - After adding to FastLED controller listonChannelEnqueued - After data enqueued to driveronChannelConfigured - After applyConfig() calledonChannelRemoved - After removing from controller listonChannelBeginDestroy - Before channel destructionUse cases:
The UCS7604 chipset supports 16-bit color depth, which benefits from gamma correction to produce perceptually smooth brightness gradients. The Channels API provides per-channel gamma control:
How gamma resolution works:
| Method | Scope | Precedence |
|---|---|---|
channel->setGamma(3.2f) | Per-channel | Highest - overrides built-in default |
| (no call) | Built-in default | 2.8 (matches UCS7604 datasheet recommendation) |
Common gamma values:
Per-channel example (two strips, different gamma):
Note: Gamma correction only affects 16-bit UCS7604 modes (TIMING_UCS7604_800KHZ with 16-bit, TIMING_UCS7604_1600KHZ). 8-bit mode passes values through unchanged.
Change LED settings at runtime without recreating channels using applyConfig():
What changes:
What stays the same:
Use cases:
Bind specific channels to specific drivers via the typed mBus field — useful for transmitting different chipset timings in parallel across distinct hardware peripherals:
Use cases:
mBus is typed. cfg.options.mBus is an enum class (#2459), so typos like fl::Bus::RTM are compile errors. The canonical driver name is derived via busName(B) — string literals never appear at the call site.
Bus-miss diagnostic (one-shot, from #2456 / #2459 / #2460): when cfg.options.mBus != fl::Bus::AUTO resolves to a driver that isn't registered with ChannelManager, the first Channel::showPixels() call emits a single FL_ERROR and falls back to AUTO/priority dispatch. Subsequent shows on the same channel suppress the warning via mBusWarned. The diagnostic uses ChannelManager::findDriverByName() (silent lookup) to distinguish two cases:
fl::enableDrivers<fl::Bus::X>(), FastLED.enableAllDrivers(), or FastLED.addLeds<..., fl::Bus::X>(...) (pins Bus + triggers linker keep-alive).canHandle() rejected the chipset (bus/chipset mismatch) — message suggests picking a different Bus.Use ChannelManager::findDriverByName(name) directly when you want to probe the registry without triggering the log; getDriverByName(name) is the noisy variant.
Both APIs are first-class and route through the same ChannelManager / driver layer. Pick by call-site shape, not by maturity.
Channel API (FastLED.add(cfg) / Channel::create(cfg)):
ChannelConfig — chipset, span, RGB order, and ChannelOptions are all visible at the call site.Channel::applyConfig() — good fit for web UIs, MQTT, or any sketch that reconfigures LEDs after setup().ChannelPtr you can hold and re-apply.Template addLeds<> API (FastLED.addLeds<Chipset, PIN, ...>(...)):
ChannelConfig; no behavioral difference from the Channel API.fl::Bus B template parameter pins the driver and triggers linker keep-alive.Prefer the Channel API when:
applyConfig).mBus).⚠️ Advanced users only - Most users don't need direct driver access. FastLED.show() handles everything automatically.
Engines handle different chipset timings in two modes:
Sequential (Default) - Single driver transmits different timings one after another:
Parallel (Explicit) - Multiple drivers transmit different timings simultaneously (see "Per-Channel Bus Pinning" above).
Hardware drivers use a 4-state machine for non-blocking DMA transmission:
| State | Description | poll() return value meaning |
|---|---|---|
| READY | Idle, ready to accept new data | Hardware is idle, safe to call show() |
| BUSY | Actively transmitting or queuing channels | Transmission in progress, driver is working |
| DRAINING | All channels enqueued, DMA still transmitting | Transmission finishing, no more data needed |
| ERROR | Hardware error occurred | Error state, check error message |
State flow: READY → show() → BUSY → DRAINING → poll() → READY
For advanced CPU/DMA parallelism (e.g., computing next frame while DMA transmits):
When to use:
Key insight: DRAINING state signals that the driver doesn't need more poll() calls - all channels are enqueued and DMA is finishing transmission. This is the optimal time to compute the next frame.
Third-party developers can create custom channel drivers to support new hardware peripherals or transmission protocols. This section covers the requirements and best practices.
A channel driver bridges the gap between high-level Channel objects and low-level hardware. Channels pass their encoded data to drivers via an ephemeral enqueue - drivers manage transmission, not channel registration.
Key responsibilities:
enqueue() (temporary, per-frame)isInUse flag during transmissionInherit from fl::IChannelDriver and implement these methods:
The isInUse flag prevents channels from modifying their data while the driver is transmitting. All drivers MUST manage this flag correctly.
Rules:
isInUse(true) in show() - Before starting transmissionisInUse(false) in poll() - When transmission completes (READY state)isInUse(false) on errors - When returning ERROR stateWhy it matters:
Channel::showPixels() prevents this: Example (correct pattern):
Engines use a dual-queue system to separate pending data from in-flight data:
Pending Queue (mEnqueuedChannels):
enqueue() callsshow() is calledshow() after moving to in-flight queueIn-Flight Queue (mTransmittingChannels):
show(), cleared by poll() when READYisInUse flagLifecycle flow:
Engines implement a 4-state machine for non-blocking transmission:
| State | Description | When poll() returns this |
|---|---|---|
| READY | Idle, ready for new data | Hardware idle, no transmissions in progress |
| BUSY | Actively transmitting channels | Hardware actively working, still accepting data |
| DRAINING | All channels enqueued, DMA finishing | All data submitted, no more poll() needed |
| ERROR | Hardware error occurred | Error state, check error message |
State flow:
Implementation notes:
show())Register your driver with the bus manager to make it available:
Priority guidelines for custom drivers:
bus_priorities.h).Driver selection (ChannelManager::selectDriverForChannel):
Channel::showPixels() derives a bus key from cfg.options.mBus via busName(mBus) and passes it to selectDriverForChannel. When mBus != Bus::AUTO, the manager does a silent findDriverByName(busKey) lookup first.Channel::showPixels() emits a one-shot FL_ERROR (see "Bus-miss diagnostic" above) and falls through to priority dispatch.mBus == Bus::AUTO or bus-miss fallback): the manager iterates drivers by priority (high to low) and returns the first that canHandle()s the channel data.cfg.options.mBus (per-channel, runtime), fl::TypedChannel<Bus, Chipset>::create() or FastLED.addLeds<..., fl::Bus::X> (compile-time, links only the named driver), or FastLED.setExclusiveDriver<fl::Bus::X>() (process-wide, compile-time TU-link; must be called before addLeds<>).Priority modification:
addDriver())setDriverPriority(name, priority)show() must wait for READY before starting a new frame. The correct pattern is a simple spin on poll():
Do NOT branch on DRAINING or other intermediate states inside show()'s wait loop. The poll() method is responsible for driving the state machine to READY — show() just needs to wait for it. Branching on intermediate states (e.g., breaking early on DRAINING) splits the "wait for previous frame" logic across multiple places and makes the code harder to reason about.
Memory Management:
fl::vector for dynamic arrays (not std::vector)fl::shared_ptr<ChannelData> (not raw pointers)Thread Safety:
enqueue(), show(), poll() are called from main threadFL_IRAM attributesrc/platforms/esp/32/drivers/parlio/parlio_engine.h for ISR patternsError Handling:
DriverState::ERROR on hardware failuresisInUse flags before returning ERRORFL_WARN() or FL_DBG()Performance:
show() and poll() (hot paths)Compatibility:
canHandle() conservatively (reject unsupported chipsets)canHandle() if hardware has limitsReference implementations in the codebase:
Simple (good starting point):
src/platforms/esp/32/drivers/uart/channel_engine_uart.cpp.hpp - UART Wave8 encodingsrc/platforms/stub/clockless_channel_stub.h - Stub driver for testingAdvanced (full-featured):
src/platforms/esp/32/drivers/rmt/rmt_5/channel_engine_rmt.cpp.hpp - RMT with ISR callbackssrc/platforms/esp/32/drivers/parlio/channel_engine_parlio.cpp.hpp - PARLIO with chipset groupingKey differences:
Create unit tests following the existing patterns:
See tests/fl/channels/driver.cpp for more test examples.
Headers:
fl/channels/channel.h — Channel class and the non-template Channel::create(cfg) factory.fl/channels/channel_typed.h — TypedChannel<Bus, Chipset> (compile-time bus/chipset enforcement, returns a ChannelPtr to the same Channel).fl/channels/ichannel.h — IChannel ABC (callback-facing identification base).fl/channels/config.h — ChannelConfig, ChannelConfigOf<Chipset>, ClocklessChipset, SpiChipsetConfig.fl/channels/options.h — ChannelOptions (correction, temperature, dither, rgbw, mBus driver selection, gamma).fl/channels/bus.h — fl::Bus enum, busName(), DefaultBus<Chipset>.fl/channels/bus_traits.h — BusTraits<B>, BusSupports<B, Chipset>, enableDrivers<Bus...>(), busKeepAlive<B>().fl/channels/bus_priorities.h — default_bus_priority(Bus) table consumed by enableDrivers<>().fl/channels/all_drivers.h — declaration header for fl::enableAllDrivers() / FastLED.enableAllDrivers(). The body lives in platforms/channel_drivers.impl.cpp.hpp, linked into libfastled; --gc-sections handles the tree-shaking.fl/channels/manager.h — ChannelManager (addDriver, getDriverByName, findDriverByName, selectDriverForChannel, setDriverPriority, setDriverEnabled, setExclusiveDriver, setExclusiveDriverByName, clearAllDrivers, ...).fl/channels/channel_events.h — Lifecycle event callbacks.fl/channels/driver.h — IChannelDriver interface and DriverState machine.Examples:
examples/BlinkParallel.ino - Parallel LED strip exampleThe compiler emits DWARF .eh_frame / FDE unwind tables by default on most toolchains. On ESP32-S3 release builds this can add ~180 KB even when -fno-exceptions is set. To strip it, add the codegen flags to your platformio.ini:
Note: Earlier releases shipped
FL_NO_UNWIND/FL_NO_UNWIND_BEGIN/FL_NO_UNWIND_END/FASTLED_FORCE_NO_UNWIND_TABLES/FASTLED_FORCE_UNWIND_TABLESmacros that attempted to do this via#pragma GCC optimize("no-unwind-tables"). A byte-level audit on GCC 14.2.0 / xtensa-esp-elf (issue #2473) proved the pragma is a no-op: wrapped TUs still shipped the full.eh_frame. The macros have been removed (issue #2474). Use thebuild_flagsform above instead — it actually shrinks the binary and also coverslibstdc++.aand user TUs, which the macros could not reach. fbuild#243 will eventually apply these flags automatically per-architecture.