Files
ESP32S3_vt/armor/armor-firmware/src/piezo.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

214 lines
8.0 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.
//! 四路压电片的 ADC 连续采样(ESP32-C3)。
//!
//! # 为什么要连续(DMA)采样
//!
//! 压电片被敲一下的波形只有几十微秒宽。用 oneshot 轮询(每圈读一次)几乎必然从
//! 尖峰旁边擦过去,表现就是"有时候敲得响才算";参考实现因此用 40 kHz 的连续采样,
//! 这里保持一致:四个通道合计 40 kHz(每通道 10 kHz),一帧 10 ms = 400 个采样点。
//!
//! # 为什么也走 sys 绑定
//!
//! `esp-idf-hal` 的 `adc::continuous` 要用 `AdcChannels` 组合通道,而多通道组合的
//! 入口 `EmptyAdcChannels::chain` 在 0.47 里的约束是 `A: Adc` 且
//! `O: AdcChannels<AdcUnit = A>` —— `ADC1` 满足前者、`ADCU1` 满足后者,同一个类型
//! 参数凑不齐,四路 ADC1 通道拼不出来。直接照着 IDF 的 C API 建驱动反而更短,也
//! 与参考实现逐行对应。
//!
//! 采样点的通道号由硬件结果自带(`type2.channel`),所以映射不依赖"结果按 pattern
//! 顺序轮转"这个假设。
use anyhow::Result;
use esp_idf_svc::sys as ffi;
use log::info;
use armor_protocol::Plate;
use crate::esp;
/// 四个通道合计的采样率;每通道 10 kHz。
const SAMPLE_FREQ_HZ: u32 = 40_000;
/// 一帧的采样点数:10 ms × 40 kHz。
pub const FRAME_MEASUREMENTS: usize = 400;
/// 一帧的字节数(每个结果 4 字节)。
const FRAME_BYTES: u32 = FRAME_MEASUREMENTS as u32 * ffi::SOC_ADC_DIGI_RESULT_BYTES;
/// 连续读的超时:正常情况 10 ms 就凑满一帧,200 ms 只用来发现"驱动卡住了"。
const READ_TIMEOUT_MS: u32 = 200;
/// 每个板位对应的 ADC 通道号(ESP32-C3:GPIO0/4/3/1 = ADC1_CH0/CH4/CH3/CH1)。
///
/// 顺序与 [`Plate`] 的 index 一致;硬件结果里的通道号就是这些值。
const PLATE_CHANNEL: [u8; 4] = [0, 4, 3, 1];
/// 一帧的统计:读到多少采样点,以及每个板位的峰值。
///
/// 峰值是给**现场校准阈值**用的:压电片的输出幅度取决于贴装、线长和有没有放大,
/// 只报"有没有过阈值"没法判断该把阈值调到哪里。status 行每秒打印一次。
#[derive(Debug, Clone, Copy, Default)]
pub struct FrameStats {
/// 这一帧实际读到的采样点数。
pub samples: usize,
/// 每个板位的最大原始 ADC 值,按 [`Plate`] 的 index 排列。
pub peak: [u16; 4],
}
/// 连续采样的压电片输入。
pub struct Piezo {
handle: ffi::adc_continuous_handle_t,
/// 一帧的原始结果。`adc_digi_output_data_t` 里有 union,bindgen 不给 `Default`,
/// 所以用零值显式初始化(这个类型是纯 POD,零值是合法状态)。
frame: Box<[ffi::adc_digi_output_data_t; FRAME_MEASUREMENTS]>,
}
impl Piezo {
/// 建驱动并开始采样。
pub fn new() -> Result<Self> {
let handle_config = ffi::adc_continuous_handle_cfg_t {
// 两帧的余量:这一帧还没读完时,下一帧有地方落。
max_store_buf_size: FRAME_BYTES * 2,
conv_frame_size: FRAME_BYTES,
..Default::default()
};
let mut handle: ffi::adc_continuous_handle_t = core::ptr::null_mut();
esp::check(
"adc_continuous_new_handle",
// SAFETY: 两个指针都指向本函数的局部量,调用期间有效。
unsafe { ffi::adc_continuous_new_handle(&handle_config, &mut handle) },
)?;
// 四路通道的采样参数:0 dB 衰减、12 bit、都在 ADC1。
let mut patterns = [ffi::adc_digi_pattern_config_t {
atten: ffi::adc_atten_t_ADC_ATTEN_DB_0 as u8,
channel: 0,
unit: ffi::adc_unit_t_ADC_UNIT_1 as u8,
bit_width: 12,
}; 4];
for (pattern, channel) in patterns.iter_mut().zip(PLATE_CHANNEL) {
pattern.channel = channel;
}
let config = ffi::adc_continuous_config_t {
pattern_num: patterns.len() as u32,
adc_pattern: patterns.as_mut_ptr(),
sample_freq_hz: SAMPLE_FREQ_HZ,
conv_mode: ffi::adc_digi_convert_mode_t_ADC_CONV_SINGLE_UNIT_1,
// C3 的连续采样只支持 12 bit 的 TYPE2 结果布局,也正好对应下面读的
// `type2.data`(参考实现依赖了默认值 0,这里写明确)。
format: ffi::adc_digi_output_format_t_ADC_DIGI_OUTPUT_FORMAT_TYPE2,
..Default::default()
};
if let Err(err) = esp::check(
"adc_continuous_config",
// SAFETY: IDF 会把 patterns 复制进驱动,调用后不再持有这个指针。
unsafe { ffi::adc_continuous_config(handle, &config) },
) {
// SAFETY: 句柄刚建好、还没 start,可以直接删。
unsafe { ffi::adc_continuous_deinit(handle) };
return Err(err);
}
esp::check("adc_continuous_start", unsafe {
ffi::adc_continuous_start(handle)
})?;
let zero = ffi::adc_digi_output_data_t {
__bindgen_anon_1: ffi::adc_digi_output_data_t__bindgen_ty_1 { val: 0 },
};
info!(
"piezo: {SAMPLE_FREQ_HZ} Hz total over {} channels, {} measurements/frame (10 ms)",
PLATE_CHANNEL.len(),
FRAME_MEASUREMENTS
);
Ok(Self {
handle,
frame: Box::new([zero; FRAME_MEASUREMENTS]),
})
}
/// 读一帧(约 10 ms),把过阈值的采样点收集进 `hits`。
///
/// `hits` 会被清空后填充 `(板位, 原始值)`;返回值里带这一帧的采样点数与每路峰值。
/// **去抖与扣血不在这里做** —— 那是
/// [`armor_protocol::Armor::on_sample`] 的职责,一次击打的多个过阈值采样点由它
/// 的冷却窗口收敛成一次受击。
pub fn read_frame(
&mut self,
threshold: u16,
hits: &mut Vec<(Plate, u16)>,
) -> Result<FrameStats> {
hits.clear();
let mut stats = FrameStats::default();
let mut out_length: u32 = 0;
let rc = unsafe {
ffi::adc_continuous_read(
self.handle,
self.frame.as_mut_ptr() as *mut u8,
FRAME_BYTES,
&mut out_length,
READ_TIMEOUT_MS,
)
};
if rc == ffi::ESP_ERR_TIMEOUT {
// 没有数据不算错误:上一帧还没凑满,下一圈再来。
return Ok(stats);
}
esp::check("adc_continuous_read", rc)?;
let count = (out_length as usize / ffi::SOC_ADC_DIGI_RESULT_BYTES as usize)
.min(FRAME_MEASUREMENTS);
stats.samples = count;
for index in 0..count {
// SAFETY: 配置的是 TYPE2(12 bit)格式,`type2` 是有效成员;每个结果
// 4 字节且已 4 字节对齐。
let result = unsafe { self.frame[index].__bindgen_anon_1.type2 };
let raw = result.data() as u16;
let Some(plate) = plate_of_channel(result.channel() as u8) else {
continue;
};
// 峰值取自**所有**采样点,不只是过阈值的:阈值调得对不对,要靠没敲时
// 的底噪和敲击时的幅度两个数字一起看。
let slot = plate.index() as usize;
if raw > stats.peak[slot] {
stats.peak[slot] = raw;
}
if raw > threshold {
hits.push((plate, raw));
}
}
Ok(stats)
}
}
impl Drop for Piezo {
fn drop(&mut self) {
// 固件里 main 永不返回;这段只在启动失败路径上跑到。
unsafe {
if !self.handle.is_null() {
ffi::adc_continuous_stop(self.handle);
ffi::adc_continuous_deinit(self.handle);
}
}
}
}
/// 硬件结果里的通道号 → 板位。
#[must_use]
fn plate_of_channel(channel: u8) -> Option<Plate> {
let slot = PLATE_CHANNEL.iter().position(|c| *c == channel)?;
Plate::ALL.get(slot).copied()
}