这一块代码本来就写好了、根 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。
214 lines
8.0 KiB
Rust
214 lines
8.0 KiB
Rust
//! 四路压电片的 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()
|
||
}
|