ws2812-rmt 0.1.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.1.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 (jcmendez/fall_sensor-rust) 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.

Full debugging writeup: jcmendez/alerter_o1space_rust's doc/learnings.md (search "WS2812").

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

Every Timing field is a count of RMT clock ticks, not nanoseconds. The tick period depends on your chip's RMT clock source (which varies by chip family, and isn't always what esp-hal's own metadata suggests — this crate exists partly because that assumption was wrong once already) and on Rmt::new's requested frequency.

Timing::WS2812_AT_12_5NS_TICK is confirmed correct on an ESP32-C6 with:

let rmt = Rmt::new(peripherals.RMT, Rate::from_mhz(80))?;

in your own setup code (this crate fixes the channel's additional clock divider internally to reproduce the same recipe). 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,
)?;
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::WS2812_AT_12_5NS_TICK)?;
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.