Skip to main content

Crate rustyfarian_esp_hal_ws2812

Crate rustyfarian_esp_hal_ws2812 

Source
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).

BeforeAfter
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§

Ws2812Rmt
WS2812 LED driver using the esp-hal RMT 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) for num_leds WS2812 LEDs.

Type Aliases§

Ws2812RmtBlocking
Type alias for the blocking variant of Ws2812Rmt.