这一块代码本来就写好了、根 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。
313 lines
11 KiB
Rust
313 lines
11 KiB
Rust
//! 四路 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(©_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:#}");
|
||
}
|