//! 四路 WS2812 灯带:一个 RMT TX 通道 + 官方 copy encoder,靠切 GPIO 轮流驱动。 //! //! # 为什么不用 `esp-idf-hal` 的 `TxChannelDriver` //! //! 它把 `rmt_channel_handle_t` 藏在结构体里(`as_ptr()` 是 `pub(crate)`),而 //! "一个通道依次驱动四路灯带"必须用 `rmt_tx_switch_gpio()` 换脚。ESP32-C3 的 RMT //! 只有 **2 个 TX 通道**,四路灯带不可能各占一个,所以这里直接用 `esp-idf-svc` 的 //! sys 绑定:通道、encoder、transmit 都是 IDF 原生的那几个函数,用法与参考实现的 //! `led_strip_encoder.c` 一一对应。 //! //! # ESP32-C3 的 RMT 内存限制(照抄参考实现的踩坑记录) //! //! C3 的 RMT 只有一个 block,总计 **96 个 symbol 的通道内存**,按 //! `SOC_RMT_MEM_WORDS_PER_CHANNEL = 48` 为单位分配。`mem_block_symbols = 64` 会被 //! 向上取整到 96,也就是一个通道吃光全部内存,第二个 `rmt_new_tx_channel()` 会以 //! `no free tx channels` 失败: //! //! ```text //! E rmt: rmt_tx_register_to_group(168): no free tx channels //! ``` //! //! 所以这里只建**一个**通道、让它独占 96 个 symbol(一帧 = 3 珠 × 24 bit + reset //! = 73 个 symbol,装得下),再用 `rmt_tx_switch_gpio()` 依次切到四个 GPIO 上发送。 //! 每路约 90 µs,10 ms 周期里的开销可以忽略。 //! //! # 与参考实现的差异 //! //! 参考实现里四条灯带永远同色,所以每 10 ms 无条件刷四遍。这里四路颜色各自独立 //! (被打的那一路要单独变暗),于是: //! //! - 内容没变的路不发(`refresh` 里比较上一次的 [`Strip`]),省掉每次 //! `disable → switch → enable → transmit` 的四轮开销; //! - 每秒仍强制全刷一次:灯带可能因为上电次序、掉电、静电而丢帧,缓存不能当真相。 use core::ffi::c_void; use core::time::Duration; use anyhow::Result; use esp_idf_svc::hal::rmt::{PinState, Pulse, Symbol}; use esp_idf_svc::hal::units::Hertz; use esp_idf_svc::sys as ffi; use log::{info, warn}; use armor_protocol::{Plate, Rgb, Strip}; use crate::esp; /// RMT 计时分辨率:10 MHz,即 1 tick = 0.1 µs。 const RMT_RESOLUTION_HZ: u32 = 10_000_000; /// 每条灯带 3 颗灯珠(与参考实现一致)。 const LEDS_PER_STRIP: usize = 3; /// 一帧的符号数:24 bit/珠 × 3 珠 + 1 个 reset 符号。 const FRAME_SYMBOLS: usize = LEDS_PER_STRIP * 24 + 1; /// 独占 C3 全部 RMT 通道内存,只建一个通道(见模块文档)。 const MEM_BLOCK_SYMBOLS: usize = 96; /// WS2812 的位时序:T0H / T0L / T1H / T1L。 const T0H_NS: u64 = 300; const T0L_NS: u64 = 900; const T1H_NS: u64 = 900; const T1L_NS: u64 = 300; /// 帧尾低电平时长;WS2812 要 >50 µs 才锁存。 const RESET_US: u64 = 50; /// 灯带数据脚(GPIO 号,按 [`Plate`] 的 index 排列):前 10 / 后 5 / 左 6 / 右 7。 const STRIP_GPIO: [i32; 4] = [10, 5, 6, 7]; /// `rmt_tx_wait_all_done` 的超时(毫秒)。一帧只要 ~90 µs,1 s 是"卡死就报错"的量级。 const TX_TIMEOUT_MS: i32 = 1000; /// 强制全刷的周期:缓存不可信(掉电、静电都可能让灯带丢帧)。 const FULL_REFRESH_MS: u64 = 1000; /// 四路 WS2812。 pub struct Leds { channel: ffi::rmt_channel_handle_t, encoder: ffi::rmt_encoder_handle_t, /// 通道当前是否处于 enable 状态;切 GPIO 只能在 INIT 状态下做。 enabled: bool, bit0: Symbol, bit1: Symbol, reset: Symbol, /// 每路的预编码帧。放在 `Box` 里:`rmt_transmit` 只借用指针,堆上的地址不会 /// 因为结构体搬动而失效。 frames: Box<[[ffi::rmt_symbol_word_t; FRAME_SYMBOLS]; 4]>, /// 上一次真正发出去的内容,用来跳过没变化的刷新。 current: [Option; 4], last_full_refresh_ms: u64, } impl Leds { /// 建通道与 encoder,把四路灯带全刷成黑色(上电先灭)。 pub fn new() -> Result { let mut config = ffi::rmt_tx_channel_config_t::default(); // 先落在前装甲板那一路;其余三路靠 switch_gpio 换过去。 config.gpio_num = STRIP_GPIO[Plate::Front.index() as usize] as _; config.clk_src = ffi::soc_periph_rmt_clk_src_t_RMT_CLK_SRC_DEFAULT; config.resolution_hz = RMT_RESOLUTION_HZ; config.mem_block_symbols = MEM_BLOCK_SYMBOLS; config.trans_queue_depth = 4; let mut channel: ffi::rmt_channel_handle_t = core::ptr::null_mut(); esp::check( "rmt_new_tx_channel", // SAFETY: config 与 channel 都是本函数的局部量,调用期间有效。 unsafe { ffi::rmt_new_tx_channel(&config, &mut channel) }, )?; let mut encoder: ffi::rmt_encoder_handle_t = core::ptr::null_mut(); let copy_config = ffi::rmt_copy_encoder_config_t::default(); if let Err(err) = esp::check( "rmt_new_copy_encoder", // SAFETY: 同上,copy encoder 只是把用户给的 symbol 原样搬进硬件。 unsafe { ffi::rmt_new_copy_encoder(©_config, &mut encoder) }, ) { // SAFETY: 通道刚建好且未使用,可以直接删。 unsafe { ffi::rmt_del_channel(channel) }; return Err(err); } let resolution = Hertz(RMT_RESOLUTION_HZ); let bit0 = Symbol::new_with( resolution, PinState::High, Duration::from_nanos(T0H_NS), PinState::Low, Duration::from_nanos(T0L_NS), )?; let bit1 = Symbol::new_with( resolution, PinState::High, Duration::from_nanos(T1H_NS), PinState::Low, Duration::from_nanos(T1L_NS), )?; let reset_low = Pulse::new_with_duration(resolution, PinState::Low, Duration::from_micros(RESET_US))?; // reset = 一整段(两半都是低电平)的符号。 let reset = Symbol::new(reset_low, reset_low); // `rmt_symbol_word_t` 是 union,bindgen 不给它 `Default`,只能自己写一个全零符号。 let blank = ffi::rmt_symbol_word_t { val: 0 }; let mut leds = Self { channel, encoder, enabled: false, bit0, bit1, reset, frames: Box::new([[blank; FRAME_SYMBOLS]; 4]), current: [None; 4], last_full_refresh_ms: 0, }; info!( "ws2812: 1 RMT channel x {MEM_BLOCK_SYMBOLS} symbols, {LEDS_PER_STRIP} leds/strip on GPIO {:?}", STRIP_GPIO ); // 上电先灭:不知道灯带上一次显示的是什么。 for plate in Plate::ALL { leds.write(plate, Strip::Black)?; } Ok(leds) } /// 把预编码的帧发到指定那一路。 fn write(&mut self, plate: Plate, strip: Strip) -> Result<()> { let slot = plate.index() as usize; self.encode(slot, strip.color()); // 切 GPIO 只能在 INIT 状态下做,所以先 disable。 if self.enabled { esp::check("rmt_disable", unsafe { ffi::rmt_disable(self.channel) })?; self.enabled = false; } esp::check( "rmt_tx_switch_gpio", // SAFETY: 通道处于 INIT 状态;gpio 是 C3 上的合法输出脚。 unsafe { ffi::rmt_tx_switch_gpio(self.channel, STRIP_GPIO[slot] as _, false) }, )?; esp::check("rmt_enable", unsafe { ffi::rmt_enable(self.channel) })?; self.enabled = true; let tx_config = ffi::rmt_transmit_config_t { loop_count: 0, // 不循环 ..Default::default() }; esp::check( "rmt_transmit", // SAFETY: frames[slot] 在 Box 上,地址稳定;调用期间不会被移动。 unsafe { ffi::rmt_transmit( self.channel, self.encoder, self.frames[slot].as_ptr() as *const c_void, core::mem::size_of_val(&self.frames[slot]), &tx_config, ) }, )?; esp::check( "rmt_tx_wait_all_done", // SAFETY: 纯等待,超时给 1 s。 unsafe { ffi::rmt_tx_wait_all_done(self.channel, TX_TIMEOUT_MS) }, )?; self.current[slot] = Some(strip); Ok(()) } /// 把 `rgb` 编成一条灯带的整帧(GRB、MSB first,3 珠同色)。 fn encode(&mut self, slot: usize, rgb: Rgb) { let grb = rgb.to_grb(); let frame = &mut self.frames[slot]; // 一颗灯珠的 24 个符号只算一遍,再复制给另外两颗 —— 四条灯带都是 3 珠, // 中间那两步没必要重复算。 let mut one_led = [ffi::rmt_symbol_word_t { val: 0 }; 24]; for (byte_index, byte) in grb.iter().enumerate() { for bit in 0..8 { let symbol = if (byte >> (7 - bit)) & 1 == 1 { self.bit1 } else { self.bit0 }; one_led[byte_index * 8 + bit] = symbol.into(); } } for led in 0..LEDS_PER_STRIP { frame[led * 24..(led + 1) * 24].copy_from_slice(&one_led); } // 帧尾:>50 µs 低电平,灯带才会锁存这一帧。 frame[FRAME_SYMBOLS - 1] = self.reset.into(); } /// 按 [`Plate`] 的 index 刷四路灯带。 /// /// 内容与上一次相同的路直接跳过(除非到了每秒一次的强制全刷),所以稳态下 /// 这个函数几乎不花时间。 pub fn refresh(&mut self, strips: [Strip; 4], now_ms: u64) -> Result<()> { let full = now_ms.saturating_sub(self.last_full_refresh_ms) >= FULL_REFRESH_MS; for plate in Plate::ALL { let strip = strips[plate.index() as usize]; if !full && self.current[plate.index() as usize] == Some(strip) { continue; } self.write(plate, strip)?; } if full { self.last_full_refresh_ms = now_ms; } Ok(()) } } impl Drop for Leds { fn drop(&mut self) { // 固件里 main 永不返回,这段只在异常路径(比如 new() 之后的早期返回)跑到。 unsafe { if self.enabled { ffi::rmt_disable(self.channel); } if !self.encoder.is_null() { ffi::rmt_del_encoder(self.encoder); } if !self.channel.is_null() { ffi::rmt_del_channel(self.channel); } } } } /// 供 main 打印一次用:把四路输出拼成一行,便于对照。 pub fn describe(strips: [Strip; 4]) -> String { Plate::ALL .iter() .map(|plate| { let strip = strips[plate.index() as usize]; let text = if strip.is_black() { "off".to_string() } else { format!("#{:06X}", strip.color().to_u32()) }; format!("{}={text}", plate.name()) }) .collect::>() .join(" ") } /// 记一次刷新失败:诊断信息比重启有用,现场灯带接错线也不该把固件打挂。 pub fn log_failure(err: &anyhow::Error) { warn!("ws2812 refresh failed: {err:#}"); }