ws2812-rmt 0.2.0

Minimal WS2812/NeoPixel driver over esp-hal's RMT peripheral -- no third-party smart-leds abstraction
#![no_std]
//! Minimal WS2812/NeoPixel driver over `esp_hal`'s RMT peripheral.
//!
//! # Why this exists instead of `esp-hal-smartled2`
//!
//! 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 reliably corrupted, either
//! sticking on a stale frame or bleeding a previously-sent channel's value
//! into later frames. 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 reset/latch pulse (at least 50us), 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.
//! 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, on an ESP32-C6, where the RMT source clock is a fixed 80MHz PLL
//! independent of the APB clock. ESP32-C3 (and classic ESP32/S3) default the
//! RMT source clock to the APB clock instead, which is a different situation
//! with a different confirmed-working [`Timing`] constant -- see each
//! constant's own doc comment for the exact `Rmt::new`/[`Ws2812::new`]
//! `clk_divider` recipe it was confirmed against. If you're on a chip or
//! clock configuration this crate hasn't been confirmed on, verify on real
//! hardware (a logic analyzer is the reliable way; empirically, wrong tick
//! periods here don't fail loudly -- they render as anything from "stuck on
//! one color" to "completely dark").
//!
//! # Example
//!
//! ```rust,ignore
//! let rmt = Rmt::new(peripherals.RMT, Rate::from_mhz(80))?;
//! let mut led = Ws2812::<{ ws2812_rmt::buffer_len(1) }>::new(
//!     rmt.channel0,
//!     peripherals.GPIO1,
//!     Timing::WS2812_AT_12_5NS_TICK_ESP32C6,
//!     2, // clk_divider -- see that constant's doc comment
//! )?;
//! led.write(&[RGB8::new(0, 30, 0)])?;
//! ```

use esp_hal::Blocking;
use esp_hal::gpio::Level;
use esp_hal::gpio::interconnect::PeripheralOutput;
use esp_hal::rmt::{
    Channel, ConfigError, Error as RmtError, PulseCode, Tx, TxChannelConfig, TxChannelCreator,
};
pub use smart_leds_trait::RGB8;

/// Per-bit RMT pulse timings, in RMT clock ticks -- see the crate-level
/// docs for why the tick period isn't something this crate can determine
/// for you.
#[derive(Clone, Copy, Debug)]
pub struct Timing {
    pub t0h: u16,
    pub t0l: u16,
    pub t1h: u16,
    pub t1l: u16,
    /// Length of *each half* of the trailing reset/latch pulse (split into
    /// two phases the same way Espressif's own reference encoder does;
    /// total reset length is `2 * reset_half`).
    pub reset_half: u16,
}

impl Timing {
    /// WS2812 (6-pin variant) datasheet timings -- 350/800/700/600ns, 50us
    /// reset -- converted at a 12.5ns RMT tick period. Confirmed working on
    /// an ESP32-C6 with `Rmt::new(peripherals.RMT, Rate::from_mhz(80))` and
    /// [`Ws2812::new`]'s `clk_divider` set to `2` (that chip's RMT source
    /// clock is a fixed 80MHz PLL, independent of the requested frequency
    /// used to pick it, so `clk_divider` does the real work of reaching a
    /// 12.5ns tick here).
    pub const WS2812_AT_12_5NS_TICK_ESP32C6: Timing = Timing {
        t0h: 28,
        t0l: 64,
        t1h: 56,
        t1l: 48,
        reset_half: 2000,
    };

    /// WS2812B (4-pin variant) datasheet timings -- 400/850/800/450ns, 50us
    /// reset -- converted at a 12.5ns RMT tick period. Confirmed working on
    /// an ESP32-C3 with `Rmt::new(peripherals.RMT, Rate::from_mhz(80))` and
    /// [`Ws2812::new`]'s `clk_divider` set to `1` (this chip's RMT source
    /// clock defaults to APB, which is fixed at 80MHz on ESP32-C3/classic
    /// ESP32/S3 -- `Rmt::new`'s requested frequency already lands on
    /// 12.5ns/tick with no additional channel-level division needed).
    pub const WS2812B_AT_12_5NS_TICK_ESP32C3: Timing = Timing {
        t0h: 32,
        t0l: 68,
        t1h: 64,
        t1l: 36,
        reset_half: 2000,
    };
}

/// Number of `PulseCode`s needed to drive `pixel_count` WS2812 pixels: 24
/// bits/pixel (8 bits x 3 channels, MSB first), plus one explicit reset
/// pulse, plus the RMT end marker. Use this to size [`Ws2812`]'s
/// `BUFFER_LEN` const parameter.
pub const fn buffer_len(pixel_count: usize) -> usize {
    pixel_count * 24 + 2
}

/// WS2812/NeoPixel string driver over one RMT TX channel, in blocking mode
/// -- see the crate-level docs for why blocking rather than async.
///
/// `BUFFER_LEN` must be [`buffer_len`]`(pixel_count)` for however many
/// pixels you intend to pass to [`Ws2812::write`] -- there's no
/// `generic_const_exprs` on stable Rust to compute this from a plain pixel
/// count alone, so it has to be spelled out at the type level (the same
/// shape `esp-hal-smartled2`'s own `BUFFER_SIZE` parameter uses).
pub struct Ws2812<'d, const BUFFER_LEN: usize> {
    // `Some` except transiently inside `write()`: `Channel::transmit()`
    // consumes `self` and hands it back once the transaction completes, so
    // there's no way to hold a live `Channel` across that call without a
    // `take()`/put-back dance.
    channel: Option<Channel<'d, Blocking, Tx>>,
    timing: Timing,
}

impl<'d, const BUFFER_LEN: usize> Ws2812<'d, BUFFER_LEN> {
    /// Configure `channel` (typically `rmt.channelN`) to drive a WS2812
    /// string on `pin`, using `timing` for the pulse widths and
    /// `clk_divider` for this channel's own additional RMT clock divider.
    /// See `timing`'s own doc comment (e.g.
    /// [`Timing::WS2812_AT_12_5NS_TICK_ESP32C6`]) for the exact
    /// `Rmt::new`/`clk_divider` pairing it was confirmed against -- the two
    /// have to match, or the resulting tick period won't be what `timing`
    /// assumes.
    pub fn new<Ch>(
        channel: Ch,
        pin: impl PeripheralOutput<'d>,
        timing: Timing,
        clk_divider: u8,
    ) -> Result<Self, ConfigError>
    where
        Ch: TxChannelCreator<'d, Blocking>,
    {
        let config = TxChannelConfig::default()
            .with_clk_divider(clk_divider)
            .with_idle_output_level(Level::Low)
            .with_idle_output(true)
            .with_carrier_modulation(false);
        let channel = channel.configure_tx(&config)?.with_pin(pin);
        Ok(Self {
            channel: Some(channel),
            timing,
        })
    }

    /// Update every pixel. `pixels.len()` must equal the pixel count this
    /// `Ws2812` was sized for (`BUFFER_LEN == buffer_len(pixels.len())`) --
    /// panics otherwise, since that's a static call-site mismatch, not a
    /// runtime condition callers need to handle.
    ///
    /// Blocks for roughly the transmission time (~1.1us/bit plus the
    /// reset pulse -- e.g. ~55us for one pixel at the confirmed timing)
    /// while the RMT hardware shifts the bits out.
    pub fn write(&mut self, pixels: &[RGB8]) -> Result<(), RmtError> {
        assert_eq!(
            BUFFER_LEN,
            buffer_len(pixels.len()),
            "Ws2812::write: pixels.len() doesn't match this driver's BUFFER_LEN"
        );
        let buf = self.encode(pixels);
        let channel = self
            .channel
            .take()
            .expect("channel is always Some between write() calls");
        match channel.transmit(&buf) {
            Ok(txn) => match txn.wait() {
                Ok(channel) => {
                    self.channel = Some(channel);
                    Ok(())
                }
                Err((e, channel)) => {
                    self.channel = Some(channel);
                    Err(e)
                }
            },
            Err((e, channel)) => {
                self.channel = Some(channel);
                Err(e)
            }
        }
    }

    /// Bit -> `PulseCode`, MSB first per channel, GRB wire order (the wire
    /// order WS2812/WS2812B actually use, not the logical RGB order
    /// `RGB8`'s fields are named in).
    fn encode(&self, pixels: &[RGB8]) -> [PulseCode; BUFFER_LEN] {
        let zero = PulseCode::new(Level::High, self.timing.t0h, Level::Low, self.timing.t0l);
        let one = PulseCode::new(Level::High, self.timing.t1h, Level::Low, self.timing.t1l);

        let mut buf = [PulseCode::end_marker(); BUFFER_LEN];
        let mut i = 0;
        for pixel in pixels {
            for byte in [pixel.g, pixel.r, pixel.b] {
                for bit in (0..8).rev() {
                    buf[i] = if byte & (1 << bit) != 0 { one } else { zero };
                    i += 1;
                }
            }
        }
        buf[i] = PulseCode::new(
            Level::Low,
            self.timing.reset_half,
            Level::Low,
            self.timing.reset_half,
        );
        buf
    }
}