//! 四路压电片的 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` —— `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 { 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 { 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 { let slot = PLATE_CHANNEL.iter().position(|c| *c == channel)?; Plate::ALL.get(slot).copied() }