ws2812-rmt 0.2.0

Minimal WS2812/NeoPixel driver over esp-hal's RMT peripheral -- no third-party smart-leds abstraction
# 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`](https://crates.io/crates/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`](https://github.com/espressif/esp-idf/blob/master/examples/peripherals/rmt/led_strip_simple_encoder/main/led_strip_example_main.c))
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

```rust
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:

```rust
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

- Apache License, Version 2.0 ([LICENSE-APACHE]LICENSE-APACHE)
- MIT license ([LICENSE-MIT]LICENSE-MIT)

at your option.