Files
ESP32S3_vt/armor/armor-firmware/src/leds.rs
T
Horldsence a30eb3200a armor: ESP32-C3 装甲板固件(一主三从)+ 烧录脚本,之前一直没提交
这一块代码本来就写好了、根 README 里也按"已实现"描述,但一直没有进仓库,
只存在于工作区里。同一个提交里带上 `.env.example` 的两个新变量(ARMOR_TEAM、
ARMOR_THRESHOLD)——它们的说明文字就是为这套固件写的。

内容:
- armor-protocol:规则层(血量、每板独立 50ms 受击冷却、受击后只有那一路灯带全黑、
  按键 FSM、上报编码)。纯计算,主机上 22 个测试,已跑通。
- armor-firmware:四路压电片 ADC 连续采样、四路 WS2812(C3 只有一个 RMT 通道可用,
  靠 rmt_tx_switch_gpio 轮流驱动)、NVS、BOOT 键、WiFi + UDP 广播。
- scripts:build_armor.sh / flash_armor.sh / armor_probe.py(不开界面单看广播)。

armor/ 是独立 workspace(目标 RISC-V 的 ESP32-C3),与外层 Xtensa 的图传固件互不相干,
各自 .cargo/config.toml 就是这条边界。构建产物由根 .gitignore 的 `/armor/target/` 覆盖,
本次没有把任何 target/ 或 build/ 下的东西加进来。

顺带把 `.DS_Store` 加进 .gitignore。
2026-10-14 19:43:14 +08:00

313 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 四路 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<Strip>; 4],
last_full_refresh_ms: u64,
}
impl Leds {
/// 建通道与 encoder,把四路灯带全刷成黑色(上电先灭)。
pub fn new() -> Result<Self> {
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(&copy_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::<Vec<_>>()
.join(" ")
}
/// 记一次刷新失败:诊断信息比重启有用,现场灯带接错线也不该把固件打挂。
pub fn log_failure(err: &anyhow::Error) {
warn!("ws2812 refresh failed: {err:#}");
}