FastLED 3.9.15
Loading...
Searching...
No Matches
channels Directory Reference
+ 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
 

Detailed Description

Overview

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:

Architecture

The system consists of two layers:

  1. Channel - High-level LED strip controller with explicit configuration API
  2. ChannelEngine - Low-level hardware driver (RMT, PARLIO, SPI, I2S, UART, FLEX_IO, OBJECT_FLED, BIT_BANG, ...)

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):


Basic Usage

Channel API (Recommended)

The Channel API provides a clean, explicit interface for creating and configuring LED strips:

#include "FastLED.h"
#define NUM_LEDS 60
#define PIN1 16
#define PIN2 17
void setup() {
// Create channel configurations with names
fl::ChannelConfig config1("left_strip", fl::ClocklessChipset(PIN1, timing),
fl::ChannelConfig config2("right_strip", fl::ClocklessChipset(PIN2, timing),
// Register channels with FastLED
auto ch1 = FastLED.add(config1);
auto ch2 = FastLED.add(config2);
Serial.printf("Created: %s and %s\n", ch1->name().c_str(), ch2->name().c_str());
}
void loop() {
FastLED.show();
}
void setup()
void loop()
#define NUM_LEDS
CRGB leds2[lengths[RedStrip]]
CRGB leds1[lengths[GreenStrip]]
FL_DISABLE_WARNING_PUSH FL_DISABLE_WARNING_GLOBAL_CONSTRUCTORS CFastLED FastLED
Global LED strip management instance.
ESP32-P4 Parallel IO (PARLIO) LED channel.
void fill_solid(CRGB *targetArray, int numToFill, const CRGB &color) FL_NOEXCEPT
Fill a range of LEDs with a solid color.
Definition fill.cpp.hpp:9
constexpr EOrder RGB
Definition eorder.h:17
fl::CRGB CRGB
Definition crgb.h:25
constexpr ChipsetTimingConfig makeTimingConfig() FL_NOEXCEPT
Convert compile-time CHIPSET type to runtime timing config.
@ Red
<div style='background:#FF0000;width:4em;height:4em;'></div>
Definition crgb.h:622
@ Blue
<div style='background:#0000FF;width:4em;height:4em;'></div>
Definition crgb.h:512
Configuration for a single LED channel.
Definition config.h:163
Clockless chipset configuration (single data pin)
Definition config.h:32
#define Serial
Definition serial.h:304

Benefits:

Template addLeds<> API

The 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.

#include "FastLED.h"
CRGB leds1[60];
CRGB leds2[60];
void setup() {
// Each addLeds<> internally constructs a ChannelConfig.
FastLED.addLeds<WS2812, 16>(leds1, 60);
FastLED.addLeds<WS2812, 17>(leds2, 60);
}
void loop() {
FastLED.show();
}

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.

Compile-Time Bus Selection (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").

#include "FastLED.h"
// Including the per-driver bus_traits.h is the explicit opt-in that links
// the driver translation unit. Without it the templated call fails with
// "implicit instantiation of undefined template BusTraits<Bus::RMT>".
#include "platforms/esp/32/drivers/rmt/rmt_5/bus_traits.h"
CRGB leds[60];
void setup() {
fl::ClocklessChipset(16, timing),
// Compile-time bus pinning via TypedChannel — the single template entry
// point. Typos like fl::Bus::RTM are compile errors, and the only driver
// TU linked is RMT.
FastLED.add(channel);
}
fl::CRGB leds[NUM_LEDS]
Compile-time identifier for an LED channel transmission bus.
static ChannelPtr create(const ChannelConfigOf< Chipset > &cfg) FL_NOEXCEPT
Construct a runtime Channel from a typed configuration.
Strongly-typed channel configuration with compile-time chipset family.
Definition config.h:303

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.

// Clockless: pin to RMT at compile time.
FastLED.addLeds<WS2812, 4, GRB, fl::Bus::RMT>(leds, 60);
// SPI: pin to SPI at compile time.
FastLED.addLeds<APA102, 23, 18, RGB, DATA_RATE_MHZ(12), fl::Bus::SPI>(leds, 60);
@ APA102
APA102 LED chipset.
Definition FastLED.h:262
#define DATA_RATE_MHZ
Definition fastspi.h:95
@ SPI
Generic SPI clockless driver.
Definition bus.h:64
@ RMT
ESP32 RMT peripheral (all ESP32 variants).
Definition bus.h:62

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.

Runtime Bus Selection (non-template FastLED.add(cfg))

FastLED.add(cfg) is non-template (#2459). Pick the driver via cfg.options.mBus:

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.

#include "FastLED.h"
CRGB leds[60];
fl::Bus userPreferredBus(); // declared elsewhere -- reads config / UI
void setup() {
cfg.options.mBus = userPreferredBus(); // data-driven choice
FastLED.add(cfg); // auto-enables all drivers, dispatches by mBus
}
Bus
Driver identifier for compile-time bus selection.
Definition bus.h:60

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.

Opt-In Driver Registration (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:

// 1. Selective opt-in: only RMT and PARLIO end up linked AND registered.
#include "platforms/esp/32/drivers/rmt/rmt_5/bus_traits.h"
#include "platforms/esp/32/drivers/parlio/bus_traits.h"
void setup() {
}
void enableDrivers() FL_NOEXCEPT
Register the named drivers with ChannelManager for runtime selection.
Definition bus_traits.h:80
// 2. Universal opt-in: 3.10.3-style "every driver available at runtime".
#include "FastLED.h"
void setup() {
FastLED.enableAllDrivers(); // forwards to fl::enableAllDrivers()
// any cfg.options.mBus value now resolves at runtime via the manager
}

FastLED.enableAllDrivers() is defined in libfastled (no extra include needed). -Wl,--gc-sections drops 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.

// 3. Single-driver override: link the named driver AND set it at priority
// above the platform default so it wins ChannelManager dispatch. Must be
// called BEFORE addLeds<> / FastLED.add() so the override is visible
// when channels resolve their drivers.
#include "FastLED.h"
#include "platforms/esp/32/drivers/lcd_spi/bus_traits.h"
void setup() {
FastLED.setExclusiveDriver<fl::Bus::LCD_SPI>();
FastLED.addLeds<APA102, 23, 18, RGB>(leds, 60);
}
@ LCD_SPI
ESP32-S3 LCD_CAM SPI driver (true SPI chipsets).
Definition bus.h:68

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.


Hardware Engine Selection

The system automatically selects the best hardware driver based on platform capabilities:

Engine Priority

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

Overriding Engine Selection

For testing or performance tuning, you can control driver selection:

#include "FastLED.h"
#include "fl/channels/manager.h" // for fl::ChannelManager::instance().setDriverPriority
CRGB leds[60];
void setup() {
fl::ChannelConfig config(16, timing, fl::span<CRGB>(leds, 60), RGB);
FastLED.add(config);
// The three methods below are independent alternatives -- pick one
// strategy per program. They are shown together for reference only;
// calling them all in sequence (as written here) makes the later
// calls override the earlier ones.
// Method 1a: Link + register a specific driver at priority above the
// platform default (compile-time template form — production
// opt-in path). Must be called BEFORE the addLeds<>/FastLED.add()
// calls that should pick it up.
// #include "platforms/esp/32/drivers/rmt/rmt_5/bus_traits.h" (at file top)
FastLED.setExclusiveDriver<fl::Bus::RMT>();
// Method 1b: Same as 1a, but runtime-typed (no TU-link side effect).
// Use when the driver is already registered (mocks, custom,
// or after FastLED.enableAllDrivers()). Typed, typo-safe —
// fl::Bus::RTM is a compile error.
FastLED.setExclusiveDriver(fl::Bus::RMT);
// Method 1c: For custom/mock drivers (names not in the fl::Bus enum),
// use the by-name escape hatch on the manager directly:
// fl::ChannelManager::instance().setExclusiveDriverByName("MOCK_NAME");
// Method 2: Enable/disable specific already-registered drivers (string
// form -- works for drivers that have already been registered
// via enableDrivers<> / enableAllDrivers() / a mock test).
FastLED.setDriverEnabled("PARLIO", true);
FastLED.setDriverEnabled("SPI", false);
// Method 3: Adjust driver priority (higher = preferred)
// Engines are sorted by priority - changing priority triggers re-sort.
// Note: priority editing lives on the ChannelManager directly -- FastLED
// does NOT expose a setDriverPriority() forwarder.
fl::ChannelManager::instance().setDriverPriority("RMT", 9000); // Increase priority
fl::ChannelManager::instance().setDriverPriority("PARLIO", 8000); // Set below RMT
// Query available drivers (sorted by priority, high to low)
for (size_t i = 0; i < FastLED.getDriverCount(); i++) {
auto info = FastLED.getDriverInfos()[i];
Serial.printf("%s: priority=%d, enabled=%s\n",
info.name.c_str(), info.priority,
info.enabled ? "yes" : "no");
}
}
bool setDriverPriority(const fl::string &name, int priority) FL_NOEXCEPT
Change the priority of a registered driver.
static ChannelManager & instance() FL_NOEXCEPT
Get the global singleton instance.
Unified manager for channel drivers with priority-based fallback.

Control methods:

When to override:

Default behavior is recommended - automatic selection provides optimal performance and reliability.


Advanced Features

Channel Lifecycle Events

Register callbacks for channel lifecycle events:

#include "FastLED.h"
CRGB leds[60];
void setup() {
// Register event listeners via FastLED
auto& events = FastLED.channelEvents();
// Called when channel is created
events.onChannelCreated.add([](const fl::IChannel& ch) {
Serial.printf("Channel created: %s\n", ch.name().c_str());
});
// Called when channel data is enqueued to driver
events.onChannelEnqueued.add([](const fl::IChannel& ch, const fl::string& driver) {
Serial.printf("%s -> %s\n", ch.name().c_str(), driver.c_str());
});
// Create channel (triggers onChannelCreated)
fl::ChannelConfig config("my_strip", fl::ClocklessChipset(5, timing),
FastLED.add(config);
}
void loop() {
fill_rainbow(leds, 60, 0, 255 / 60);
FastLED.show(); // Triggers onChannelEnqueued for "my_strip"
}
virtual const fl::string & name() const =0
User-specified or auto-generated name (e.g. "Channel_3").
Polymorphic identification base for any channel in the system.
Definition ichannel.h:23
const char * c_str() const FL_NOEXCEPT
void fill_rainbow(CRGB *targetArray, int numToFill, fl::u8 initialhue, fl::u8 deltahue=5) FL_NOEXCEPT
Fill a range of LEDs with a rainbow of colors.
Definition fill.cpp.hpp:29

Available events:

Use cases:

Gamma Correction (UCS7604 16-bit)

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:

#include <FastLED.h>
#define NUM_LEDS 60
void setup() {
// makeClockless<>() carries both bit-period timing AND the UCS7604 encoder
// selector through to the channel. Use this one-liner for any non-WS2812
// clockless chipset — the 2-arg ClocklessChipset(pin, timing) form would
// default the encoder to WS2812.
auto channel = FastLED.add(config);
channel->setGamma(3.2f); // Override gamma (default is 2.8)
FastLED.setBrightness(128);
}
void loop() {
static uint8_t hue = 0;
FastLED.show();
delay(20);
}
uint8_t hue
Definition advanced.h:94
Centralized LED chipset timing definitions with nanosecond precision.
constexpr ClocklessChipset makeClockless(int pin) FL_NOEXCEPT
Build a ClocklessChipset from a compile-time TIMING trait.
Definition config.h:94

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):

CRGB warm_leds[60];
CRGB cool_leds[60];
void setup() {
// makeClockless<>() carries the UCS7604 encoder selector with the timing.
auto warm = FastLED.add(fl::ChannelConfig(
warm->setGamma(2.2f); // Gentle curve for warm ambiance
auto cool = FastLED.add(fl::ChannelConfig(
cool->setGamma(3.2f); // Steep curve for high contrast
}

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.

Runtime Reconfiguration

Change LED settings at runtime without recreating channels using applyConfig():

fl::ChannelPtr channel;
void setup() {
fl::ChannelConfig config(16, timing, leds, GRB);
channel = fl::Channel::create(config);
FastLED.add(channel);
}
// Called from UI/network handler
void updateSettings(CRGB* newLeds, int count, EOrder order) {
fl::ChannelConfig newConfig(16, timing, fl::span<CRGB>(newLeds, count), order, opts);
channel->applyConfig(newConfig);
}
#define DISABLE_DITHER
Disable dithering.
Definition dither_mode.h:10
fl::EOrder EOrder
Definition eorder.h:12
@ Tungsten100W
2850 Kelvin
Definition color.h:50
@ TypicalSMD5050
Typical values for SMD5050 LEDs.
Definition color.h:13
fl::u8 mDitherMode
Definition options.h:46
Optional channel configuration parameters All fields have sensible defaults and can be overridden as ...
Definition options.h:43

What changes:

What stays the same:

Use cases:

Per-Channel Bus Pinning (Mixed-Timing Parallel Output)

Bind specific channels to specific drivers via the typed mBus field — useful for transmitting different chipset timings in parallel across distinct hardware peripherals:

#include "FastLED.h"
#define NUM_LEDS 100
// Two strips with different chipset timings
CRGB ws2812_strip[NUM_LEDS];
CRGB ws2816_strip[NUM_LEDS];
void setup() {
// WS2812 strips bound to RMT driver
fl::ChannelOptions ws2812_opts;
ws2812_opts.mBus = fl::Bus::RMT; // typed, preferred (#2459)
FastLED.add(fl::ChannelConfig(16, timing_ws2812,
fl::span<CRGB>(ws2812_strip, NUM_LEDS), RGB, ws2812_opts));
// WS2816 strips bound to SPI driver (transmits in parallel with RMT)
fl::ChannelOptions ws2816_opts;
ws2816_opts.mBus = fl::Bus::SPI;
FastLED.add(fl::ChannelConfig(18, timing_ws2816,
fl::span<CRGB>(ws2816_strip, NUM_LEDS), RGB, ws2816_opts));
}
void loop() {
// Different effects on each strip
fill_rainbow(ws2812_strip, NUM_LEDS, 0, 255 / NUM_LEDS);
fill_solid(ws2816_strip, NUM_LEDS, CRGB::Blue);
FastLED.show(); // Both drivers transmit simultaneously
delay(20);
}

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:

Use ChannelManager::findDriverByName(name) directly when you want to probe the registry without triggering the log; getDriverByName(name) is the noisy variant.


Which API to Use

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)):

Template addLeds<> API (FastLED.addLeds<Chipset, PIN, ...>(...)):

Prefer the Channel API when:


Low-Level Engine API

⚠️ Advanced users only - Most users don't need direct driver access. FastLED.show() handles everything automatically.

Mixing Chipset Timings

Engines handle different chipset timings in two modes:

Sequential (Default) - Single driver transmits different timings one after another:

// Automatic - no configuration needed
FastLED.addLeds<WS2812, 16>(leds1, 60); // 800kHz timing
FastLED.addLeds<WS2816, 17>(leds2, 60); // Different timing
FastLED.show(); // Sequential transmission through same driver

Parallel (Explicit) - Multiple drivers transmit different timings simultaneously (see "Per-Channel Bus Pinning" above).

Engine States

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

Non-Blocking API

For advanced CPU/DMA parallelism (e.g., computing next frame while DMA transmits):

#include "FastLED.h"
CRGB leds[300];
void setup() {
fl::ChannelConfig config(16, timing, fl::span<CRGB>(leds, 300), RGB);
FastLED.add(config);
}
void computeNextFrame() {
// Do CPU-intensive work while DMA transmits
static uint8_t hue = 0;
fill_rainbow(leds, 300, hue++, 255 / 300);
}
void loop() {
// Get driver from ChannelManager
auto& manager = fl::ChannelManager::instance();
// Check if driver is ready for new data
// Hardware is idle - safe to show next frame
FastLED.show();
// DMA transmission finishing - no more poll() needed this frame
// Do useful work while waiting
computeNextFrame();
Serial.println(state.error.c_str());
}
// BUSY state: Keep polling until DRAINING or READY
delay(20);
}
TestState state
void delay(u32 ms, bool run_async=true) FL_NOEXCEPT
Public delay wrapper that keeps bare Arduino delay() preferred after using fl::delay; while still all...
Definition delay.h:98
@ READY
Hardware idle; ready to accept new transmissions.
Definition driver.h:167
@ DRAINING
All channels submitted; still transmitting.
Definition driver.h:169
@ ERROR
Driver encountered an error.
Definition driver.h:170
Driver state with optional error message.
Definition driver.h:165

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.


Implementing a Custom Channel Engine

Third-party developers can create custom channel drivers to support new hardware peripherals or transmission protocols. This section covers the requirements and best practices.

Overview

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:

Required Interface: IChannelDriver

Inherit from fl::IChannelDriver and implement these methods:

class MyCustomEngine : public fl::IChannelDriver {
public:
bool canHandle(const ChannelDataPtr& data) const override {
// Example: Only accept clockless chipsets
return data && data->isClockless();
}
void enqueue(ChannelDataPtr channelData) override {
if (channelData) {
mEnqueuedChannels.push_back(channelData);
}
}
void show() override {
if (mEnqueuedChannels.empty()) {
return;
}
// CRITICAL: Mark all channels as in-use BEFORE transmission
for (auto& channel : mEnqueuedChannels) {
channel->setInUse(true);
}
// Move pending queue to in-flight queue
mTransmittingChannels = fl::move(mEnqueuedChannels);
mEnqueuedChannels.clear();
// Start hardware transmission
beginTransmission(fl::span<const ChannelDataPtr>(
mTransmittingChannels.data(),
mTransmittingChannels.size()));
}
DriverState poll() override {
// Check hardware status
if (isHardwareBusy()) {
return DriverState::BUSY;
}
if (isTransmitting()) {
return DriverState::DRAINING;
}
// Transmission complete - CRITICAL: Clear isInUse flags
if (!mTransmittingChannels.empty()) {
for (auto& channel : mTransmittingChannels) {
channel->setInUse(false);
}
mTransmittingChannels.clear();
}
return DriverState::READY;
}
fl::string getName() const override {
return fl::string::from_literal("MY_ENGINE");
}
Capabilities getCapabilities() const override {
return Capabilities(true, false); // Clockless only
}
private:
void beginTransmission(fl::span<const ChannelDataPtr> channels);
bool isHardwareBusy() const;
bool isTransmitting() const;
// Two-queue architecture (required)
fl::vector<ChannelDataPtr> mEnqueuedChannels; // Pending queue
fl::vector<ChannelDataPtr> mTransmittingChannels; // In-flight queue
};
virtual void enqueue(ChannelDataPtr channelData) FL_NOEXCEPT=0
Enqueue channel data for transmission.
virtual fl::string getName() const FL_NOEXCEPT
Get the driver name for affinity binding.
Definition driver.h:214
virtual void show() FL_NOEXCEPT=0
Trigger transmission of enqueued data.
virtual bool canHandle(const ChannelDataPtr &data) const FL_NOEXCEPT=0
Check if this driver can handle the given channel data.
virtual Capabilities getCapabilities() const FL_NOEXCEPT=0
Get driver capabilities (clockless, SPI, or both)
virtual DriverState poll() FL_NOEXCEPT=0
Query driver state and perform maintenance.
Minimal interface for LED channel transmission drivers.
Definition driver.h:49
static string from_literal(const char *literal) FL_NOEXCEPT
Channel transmission data - lightweight DTO for driver transmission.
constexpr remove_reference< T >::type && move(T &&t) FL_NOEXCEPT
Definition s16x16x4.h:28

Critical: isInUse Flag Management

The isInUse flag prevents channels from modifying their data while the driver is transmitting. All drivers MUST manage this flag correctly.

Rules:

  1. Set isInUse(true) in show() - Before starting transmission
  2. Clear isInUse(false) in poll() - When transmission completes (READY state)
  3. Clear isInUse(false) on errors - When returning ERROR state

Why it matters:

Example (correct pattern):

void show() override {
// Mark in-use BEFORE transmission
for (auto& channel : mEnqueuedChannels) {
channel->setInUse(true); // ✅ Prevent modification
}
mTransmittingChannels = fl::move(mEnqueuedChannels);
mEnqueuedChannels.clear();
startHardware();
}
DriverState poll() override {
if (hardwareComplete()) {
// Clear in-use AFTER transmission
for (auto& channel : mTransmittingChannels) {
channel->setInUse(false); // ✅ Allow modification
}
mTransmittingChannels.clear();
return DriverState::READY;
}
return DriverState::DRAINING;
}
FastLED show()
constexpr remove_reference< T >::type && move(T &&t) FL_NOEXCEPT
Definition move.h:28

Two-Queue Architecture

Engines use a dual-queue system to separate pending data from in-flight data:

Pending Queue (mEnqueuedChannels):

In-Flight Queue (mTransmittingChannels):

Lifecycle flow:

Channel::showPixels()
driver->enqueue(data) → mEnqueuedChannels.push_back(data)
driver->show() → Move to mTransmittingChannels, clear mEnqueuedChannels
driver->poll() → Check hardware status
DriverState::READY → Clear mTransmittingChannels, ready for next frame

State Machine

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:

READY → show() → BUSY → (all queued) → DRAINING → (hardware complete) → READY
(error) → ERROR

Implementation notes:

Registration with ChannelManager

Register your driver with the bus manager to make it available:

// In your platform initialization code
void setupCustomEngine() {
// Register with priority (higher = preferred). Built-in priorities are
// defined in `fl/channels/bus_priorities.h` — see the Engine Priority
// section above. Custom drivers can use any integer priority value.
//
// The driver name is obtained via driver->getName() — addDriver() is a
// 2-arg call. If getName() returns an empty string, addDriver() emits an
// FL_WARN and rejects the driver.
}
void addDriver(int priority, fl::shared_ptr< IChannelDriver > driver) FL_NOEXCEPT
Add a driver with priority (higher priority = preferred)
shared_ptr< T > make_shared(Args &&... args) FL_NOEXCEPT
Definition shared_ptr.h:414

Priority guidelines for custom drivers:

Driver selection (ChannelManager::selectDriverForChannel):

  1. 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.
    • On hit: that driver is returned (no priority iteration).
    • On miss: Channel::showPixels() emits a one-shot FL_ERROR (see "Bus-miss diagnostic" above) and falls through to priority dispatch.
  2. Otherwise (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.
  3. Callers can override via 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:

DMA Wait Pattern

show() must wait for READY before starting a new frame. The correct pattern is a simple spin on poll():

void show() override {
// Wait for previous frame to finish.
while (poll() != DriverState::READY) {
// poll() drives the state machine and clears in-use flags.
}
// Now safe to start new frame...
}

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.

Best Practices

Memory Management:

Thread Safety:

Error Handling:

Performance:

Compatibility:

Example Engines

Reference implementations in the codebase:

Simple (good starting point):

Advanced (full-featured):

Key differences:

Testing Your Engine

Create unit tests following the existing patterns:

#include "test.h"
FL_TEST_CASE("MyEngine: Basic enqueue and transmission") {
// Create test data
auto data = fl::ChannelData::create(5, timing, fl::move(encodedData));
// Enqueue
driver->enqueue(data);
// Verify isInUse flag lifecycle
FL_CHECK_FALSE(data->isInUse()); // Not in use before show()
driver->show();
FL_CHECK(data->isInUse()); // In use during transmission
// Poll until complete
while (driver->poll() != fl::IChannelDriver::DriverState::READY) {
}
FL_CHECK_FALSE(data->isInUse()); // Not in use after transmission
}
static ChannelDataPtr create(const ChipsetVariant &chipset, fl::vector_psram< u8 > &&encodedData=fl::vector_psram< u8 >()) FL_NOEXCEPT
Create channel transmission data (modern variant-based API)
Definition data.cpp.hpp:12
#define FL_TEST_CASE(name)
Definition fltest.h:713
#define FL_CHECK(expr)
Definition fltest.h:725
#define FL_CHECK_FALSE(expr)
Definition fltest.h:737
void delayMicroseconds(u32 us)
Delay for a given number of microseconds.

See tests/fl/channels/driver.cpp for more test examples.


Reference

Headers:

Examples:

Reducing Binary Size

The 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:

build_flags =
-fno-asynchronous-unwind-tables
-fno-unwind-tables

Note: Earlier releases shipped FL_NO_UNWIND / FL_NO_UNWIND_BEGIN / FL_NO_UNWIND_END / FASTLED_FORCE_NO_UNWIND_TABLES / FASTLED_FORCE_UNWIND_TABLES macros 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 the build_flags form above instead — it actually shrinks the binary and also covers libstdc++.a and user TUs, which the macros could not reach. fbuild#243 will eventually apply these flags automatically per-architecture.