Expand description
WS2812 (NeoPixel) LED driver using esp-hal RMT peripheral (bare-metal, no_std).
This crate provides a bare-metal driver for WS2812/NeoPixel addressable LEDs
using the esp-hal RMT peripheral.
It is the no_std counterpart to rustyfarian-esp-idf-ws2812.
Pure color utilities are available in the bunting crate for testing.
§Buffer Sizing
The driver uses a const-generic buffer [PulseCode; N] where N = num_leds * 24 + 1.
Use buffer_size to compute N at compile time:
use rustyfarian_esp_hal_ws2812::buffer_size;
const N: usize = buffer_size(8); // 8-LED ring§RMT Clock Configuration
Configure the RMT channel with RMT_CLK_DIV to achieve the required 10 MHz clock.
Using a different divider will produce incorrect LED timing.
§Blocking Example
use esp_hal::{
gpio::Level,
rmt::{Rmt, TxChannelConfig, TxChannelCreator},
time::Rate,
};
use rgb::RGB8;
use rustyfarian_esp_hal_ws2812::{Ws2812Rmt, buffer_size, RMT_CLK_DIV};
const N: usize = buffer_size(1);
let rmt = Rmt::new(peripherals.RMT, Rate::from_mhz(80)).unwrap();
let config = TxChannelConfig::default()
.with_clk_divider(RMT_CLK_DIV)
.with_idle_output_level(Level::Low)
.with_idle_output(true)
.with_carrier_modulation(false);
let channel = rmt.channel0.configure_tx(&config).unwrap().with_pin(peripherals.GPIO8);
let mut led = Ws2812Rmt::<_, N>::new(channel);
led.set_pixel(RGB8::new(255, 0, 0)).unwrap();
let colors = [RGB8::new(255, 0, 0), RGB8::new(0, 255, 0), RGB8::new(0, 0, 255)];
led.set_pixels_slice(&colors).unwrap();§Async Example (feature async)
use embassy_time::Timer;
use esp_hal::{
gpio::Level,
rmt::{Rmt, TxChannelConfig, TxChannelCreator},
time::Rate,
};
use rgb::RGB8;
use rustyfarian_esp_hal_ws2812::{Ws2812Rmt, buffer_size, RMT_CLK_DIV};
const NUM_LEDS: usize = 12;
const N: usize = buffer_size(NUM_LEDS);
let rmt = Rmt::new(peripherals.RMT, Rate::from_mhz(80))
.unwrap()
.into_async();
let config = TxChannelConfig::default()
.with_clk_divider(RMT_CLK_DIV)
.with_idle_output_level(Level::Low)
.with_idle_output(true)
.with_carrier_modulation(false);
let channel = rmt.channel0.configure_tx(&config).unwrap().with_pin(peripherals.GPIO18);
let mut ws = Ws2812Rmt::<_, N>::new(channel);
let colors = [RGB8::new(255, 0, 0); NUM_LEDS];
loop {
ws.set_pixels_slice(&colors).await.unwrap();
Timer::after_millis(30).await;
}§When does async help?
WS2812 transmission is fast: approximately 30 µs per LED, or ~360 µs for a 12-LED ring. In a bare-metal system with no RTOS threads, even that small window matters — a blocking transmit prevents the executor from servicing any other tasks during that time.
The larger gain comes from inter-frame delays.
A typical animation loop waits 16–50 ms between frames.
With a blocking delay_ms(), the CPU is spinning the whole time.
With Timer::after_millis(16).await, the executor is free to handle Wi-Fi events,
button presses, sensor reads, or any other spawned task during that delay.
§Migration from the pre-0.4 API
Before async support was added, Ws2812Rmt had two type parameters: <'d, N>.
It now has three: <'d, Dm, N> where Dm is the driver mode (Blocking or Async).
| Before | After |
|---|---|
Ws2812Rmt<'d, N> | Ws2812Rmt<'d, Blocking, N> or Ws2812RmtBlocking<'d, N> |
Ws2812Rmt::<N>::new(channel) | Ws2812Rmt::<_, N>::new(channel) (infers Blocking from channel type) |
The simplest migration is to use the Ws2812RmtBlocking type alias — no other code changes
are required:
use rustyfarian_esp_hal_ws2812::{Ws2812RmtBlocking, buffer_size, RMT_CLK_DIV};
const N: usize = buffer_size(12);
let mut led: Ws2812RmtBlocking<N> = Ws2812RmtBlocking::new(channel);Alternatively, let the compiler infer the driver mode:
let mut led = Ws2812Rmt::<_, N>::new(channel); // Dm inferred from channel type§Future: SmartLedsWriteAsync
The smart-leds-trait ecosystem defines a SmartLedsWriteAsync trait for async LED writers.
Implementing it on Ws2812Rmt<'d, Async, N> is a planned follow-on once the trait
stabilises in the ecosystem.
See ADR 006 for details.
§Other async runtimes
This crate’s async support is built on esp-hal’s native async RMT channel and the
esp-rtos Embassy executor, which is the standard async runtime for esp-hal 1.0+.
Other Embassy-compatible executors (e.g., embassy-executor with a custom time driver)
are theoretically possible but untested; the RmtTxFuture in esp-hal is executor-agnostic
(it uses core::task::Waker), but the embassy-time timer support requires the
esp-rtos time driver to be initialised via esp_rtos::start().
esp-idf-hal (the std path) does not have async RMT support as of esp-idf-hal 0.46.
rustyfarian-esp-idf-ws2812 therefore remains blocking-only.
If esp-idf-hal gains async RMT in a future release, async support can be added there
under a separate feature flag without affecting this crate.
Structs§
- Ws2812
Rmt - WS2812 LED driver using the
esp-halRMT peripheral (bare-metal,no_std).
Enums§
- Error
- Errors that can occur during WS2812 RMT operations.
Constants§
- RMT_
CLK_ DIV - Clock divider for the RMT peripheral to achieve the required 10 MHz timing clock.
Functions§
- buffer_
size - Returns the required buffer size (in
PulseCodes) fornum_ledsWS2812 LEDs.
Type Aliases§
- Ws2812
RmtBlocking - Type alias for the blocking variant of
Ws2812Rmt.