From c78e2ceaa9e8af4b803606e8765b2da789c8a5e1 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Tue, 7 Apr 2026 11:50:32 -0600 Subject: [PATCH] fix: switch WS2812 driver to blocking RMT transmit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Async RMT had peripheral lifetime issues causing silent failures. Blocking transmit is ~31µs per update (negligible) and avoids the async complexity. Also removes the ManuallyDrop/unsafe workaround. --- firmware/src/board/heltec_v4.rs | 16 ++++------------ firmware/src/driver/ws2812.rs | 28 +++++++++++++++++++++------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/firmware/src/board/heltec_v4.rs b/firmware/src/board/heltec_v4.rs index ae52f316..d63f71d8 100644 --- a/firmware/src/board/heltec_v4.rs +++ b/firmware/src/board/heltec_v4.rs @@ -77,18 +77,10 @@ impl LoRaBoard for Board { let i2c = mcu::init_i2c(p.I2C0, p.GPIO17, p.GPIO18); let display = Some(DisplayParts { i2c }); - // WS2812B RGB LED on GPIO38 via RMT channel 0. - // Wrap in ManuallyDrop so the Rmt peripheral isn't disabled when - // the struct is dropped after we take channel0. - let rmt = core::mem::ManuallyDrop::new( - Rmt::new(p.RMT, Rate::from_mhz(80)) - .expect("RMT init") - .into_async(), - ); - // SAFETY: we're reading channel0 out of the ManuallyDrop wrapper. - // The Rmt won't be dropped, so the peripheral stays enabled. - let channel0 = unsafe { core::ptr::read(&raw const rmt.channel0) }; - let led_channel = channel0 + // WS2812B RGB LED on GPIO38 via RMT channel 0 (blocking, ~31µs per update) + let rmt = Rmt::new(p.RMT, Rate::from_mhz(80)).expect("RMT init"); + let led_channel = rmt + .channel0 .configure_tx( p.GPIO38, TxChannelConfig::default() diff --git a/firmware/src/driver/ws2812.rs b/firmware/src/driver/ws2812.rs index 840c2a2f..d86e7dcf 100644 --- a/firmware/src/driver/ws2812.rs +++ b/firmware/src/driver/ws2812.rs @@ -2,6 +2,7 @@ //! //! Encodes RGB values as GRB PulseCodes and transmits via RMT. //! At 80 MHz with divider=1, each tick is 12.5 ns. +//! Uses blocking transmit (~31µs per LED update). use esp_hal::gpio::Level; use esp_hal::rmt::{Channel, PulseCode, Tx}; @@ -14,14 +15,16 @@ const BIT_0: PulseCode = PulseCode::new(Level::High, 32, Level::Low, 68); /// WS2812 1-bit: High 800ns (64 ticks), Low 450ns (36 ticks). const BIT_1: PulseCode = PulseCode::new(Level::High, 64, Level::Low, 36); -/// WS2812B single-LED driver over RMT. +/// WS2812B single-LED driver over RMT (blocking). pub struct Ws2812 { - channel: Channel<'static, esp_hal::Async, Tx>, + channel: Option>, } impl Ws2812 { - pub fn new(channel: Channel<'static, esp_hal::Async, Tx>) -> Self { - Self { channel } + pub fn new(channel: Channel<'static, esp_hal::Blocking, Tx>) -> Self { + Self { + channel: Some(channel), + } } /// Encode 3 bytes (GRB order) + end marker into PulseCodes. @@ -35,7 +38,6 @@ impl Ws2812 { idx += 1; } } - // data[24] is already end_marker data } } @@ -43,8 +45,20 @@ impl Ws2812 { impl RgbLed for Ws2812 { async fn set_rgb(&mut self, r: u8, g: u8, b: u8) { let data = Self::encode(r, g, b); - if let Err(e) = self.channel.transmit(&data).await { - defmt::warn!("WS2812 transmit error: {}", e); + if let Some(ch) = self.channel.take() { + match ch.transmit(&data) { + Ok(tx) => match tx.wait() { + Ok(ch) => self.channel = Some(ch), + Err((e, ch)) => { + defmt::warn!("WS2812 wait error: {}", e); + self.channel = Some(ch); + } + }, + Err(e) => { + defmt::warn!("WS2812 transmit error: {}", e); + // Channel consumed on transmit error — LED is lost + } + } } } }