ws2812-rmt 0.2.0

Minimal WS2812/NeoPixel driver over esp-hal's RMT peripheral -- no third-party smart-leds abstraction
docs.rs failed to build ws2812-rmt-0.2.0
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.

ws2812-rmt

A minimal, no_std WS2812/NeoPixel driver over esp-hal's RMT peripheral — no third-party smart-leds/RMT abstraction in between.

Why this exists

Extracted from real-hardware debugging on an ESP32-C6 after esp-hal-smartled2 proved unable to reliably update a WS2812 pixel to a changing value on repeated writes — a single steady color always rendered correctly, but blink/breathe patterns (i.e. anything other than "set it once and leave it") reliably corrupted: either sticking on a stale frame, or bleeding a previously-sent channel's value into later frames (a once-used blue value would visibly tint every later color — red became pink, green became cyan).

Root cause: that crate — like most WS2812/RMT drivers, including an earlier version of this one — relies on the RMT channel's idle-low state between separate transmissions to serve as the WS2812's required ≥50µs reset/latch pulse, rather than encoding that reset explicitly in the transmitted buffer the way Espressif's own reference encoder (esp-idf/examples/peripherals/rmt/led_strip_simple_encoder) does. This crate always appends an explicit reset pulse, and transmits in blocking mode (Channel::transmit(..).wait(), which busy-polls hardware status registers — no interrupt/async-waker path at all) so a bug in an async completion path can't be a variable either.

RMT clock tick period is chip- and configuration-specific — verify it

Every Timing field is a count of RMT clock ticks, not nanoseconds, and Ws2812::new takes an explicit clk_divider alongside it. The resulting tick period depends on your chip's RMT clock source, which varies by chip family (this crate exists partly because that assumption was wrong once already, on an ESP32-C6) and on Rmt::new's requested frequency combined with that clk_divider.

Two presets are confirmed on real hardware so far:

Constant Chip Rmt::new frequency clk_divider Datasheet variant
Timing::WS2812_AT_12_5NS_TICK_ESP32C6 ESP32-C6 80MHz 2 WS2812 (6-pin)
Timing::WS2812B_AT_12_5NS_TICK_ESP32C3 ESP32-C3 80MHz 1 WS2812B (4-pin)

Both land on the same 12.5ns tick period, reached differently because the two chips default their RMT clock source differently (a fixed 80MHz PLL on C6, the APB clock — itself fixed at 80MHz on C3 — on C3). If you're on a different chip or clock configuration, verify on real hardware — a logic analyzer is the reliable way. Empirically, a wrong tick period here doesn't fail loudly; it renders as anything from "stuck on one color" to "completely dark".

Usage

use ws2812_rmt::{buffer_len, Timing, Ws2812, RGB8};

let rmt = Rmt::new(peripherals.RMT, Rate::from_mhz(80))?;
let mut led = Ws2812::<{ buffer_len(1) }>::new(
    rmt.channel0,
    peripherals.GPIO1,
    Timing::WS2812_AT_12_5NS_TICK_ESP32C6,
    2, // clk_divider -- must match the constant's own doc comment
)?;
led.write(&[RGB8::new(0, 30, 0)])?;

For more than one pixel, size the buffer for however many you have:

const PIXELS: usize = 25;
let mut leds = Ws2812::<{ buffer_len(PIXELS) }>::new(
    rmt.channel0,
    peripherals.GPIO8,
    Timing::WS2812B_AT_12_5NS_TICK_ESP32C3,
    1,
)?;
leds.write(&[RGB8::new(0, 30, 0); PIXELS])?;

Chip support

Gated behind Cargo features matching esp-hal's own chip features — enable the one matching your target:

esp32, esp32c2, esp32c3, esp32c5, esp32c6, esp32c61, esp32h2, esp32s2, esp32s3.

CI only exercises esp32c3/esp32c6 (RISC-V targets installable via plain rustup, no Xtensa toolchain needed) — the others should work by the same esp-hal feature-forwarding mechanism, but haven't been build-tested here.

License

Licensed under either of

at your option.