核心模块深度剖析:
1. 模拟前端信号调理电路 (关键!性能瓶颈往往在这里):
- 架构示例 (单通道简化版):
**Input BNC/J ---> [R_protect 1K] ---+---> [TVS Diodes to GND and VDD]
|
+---> [Relay or Analog Switch (e.g., ADG1419) for 1x/10x] ---+--> [1x Path: Direct] ---+
| |
+------------------------------------------------------------+
|
V
Precision Resistor Divider Network
|
V
High-Speed OpAmp Buffer/Amplifier (e.g., ADA4807)\] ---\> \[DC Offset Adjust (OpAmp Summing)\] ---\> \[Anti-Aliasing LPF (RC)\] ---\> ADC_INx \^ \| \[DAC Output (for Offset Control)\] \<--- STM32 DAC** * **量程切换 (1x/10x):** * **继电器:** 机械寿命有限,切换慢,但导通电阻小,寄生电容小,隔离好。适合低频、高精度。 * **精密模拟开关 (ADG1419):** 切换快 (ns级),寿命长,体积小。**关键参数:** * `R_on` (导通电阻): 需足够小 (几Ω到几十Ω),且平坦,避免引入非线性误差。计算在最低量程下的衰减误差。 * `C_on/C_off` (导通/关断电容): 影响高频响应。`C_on` 会与输入电阻形成低通,`C_off` 会引入容性负载。 * **电荷注入:** 开关切换瞬间注入的电荷会引起电压毛刺。选择低电荷注入型号,布局时尽量靠近运放输入。 * **电阻网络:** 使用高精度 (0.1%或0.01%)、低温漂 (\<10ppm/°C) 的金属膜电阻。计算分压比时考虑 `R_on` 的影响。并联小电容补偿高频衰减。 * **直流偏置调整:** * **目的:** 将双极性信号 (如 ±5V) 或地参考信号偏移到 ADC 输入范围 (0-3.3V) 中间 (如 1.65V)。 * **实现:** 使用运放**求和电路** 。一路输入是调理后的信号,另一路输入由 **STM32 DAC** 提供可调的偏置电压。公式: `V_out = - ( (R_f / R_signal) * V_signal + (R_f / R_dac) * V_dac )` (反相求和)。调整 `V_dac` 即可移动信号基线。 * **抗混叠滤波器 (AAF):** * **必要性:** 根据奈奎斯特采样定理,采样率 `fs` 必须 \> 2 \* `f_max` (信号最高频率分量)。AAF 在采样前滤除高于 `fs/2` 的频率分量,防止混叠失真。 * **设计:** 简单的 **1阶或2阶 RC/Active LPF** 。截止频率 `fc` 通常设置为目标最大显示频率 `f_disp_max` 的 2-5 倍 (因为 Min-Max 显示算法需要更高采样率来保留细节),但必须 `< fs/2`。例如:目标显示 1MHz 信号,采样率 10MSPS,`fc` 可设在 2.5MHz - 5MHz。 * **元件:** 电阻需稳定,电容需 **NP0/C0G** 类型 (低损耗、低介电吸收、温漂小)。 **2. ADC 配置与极限性能压榨 (以 STM32H743 为例,目标 3.6MSPS+):** * **时钟配置:** * `ADCCLK` 来源:通常选择 `per_ck` (由 PLL 提供)。H743 允许 `ADCCLK` 最高 50MHz (Vcore=1.3V)。 * 在 RCC 中配置 PLL 输出 `per_ck` 为所需频率 (如 100MHz)。 * 设置 ADC 预分频器 `ADCx_CCR[CKMODE]` 或 `ADCx_CCR[PRESC]` 使 `ADCCLK` \<= 50MHz (如 `per_ck=100MHz` / 2 = 50MHz)。 * **ADC 模式配置 (CubeMX/寄存器):** * `ADCx_CFGR`: * `RES`: `00` (12-bit resolution) * `DMNGT`: `11` (DMA Circular mode) **必须!** * `CONT`: `1` (Continuous conversion mode) - 由定时器触发启动序列。 * `OVRMOD`: `0` (Overrun overwrites) - 或 `1` (产生中断),结合双缓冲处理。 * `EXTEN`: `01` (Hardware trigger detection on the rising edge) **关键!** * `EXTSEL`: 选择触发源对应的定时器 TRGO (如 `TIM1_TRGO`). * `ADCx_SMPR1/2`: **采样时间 (`t_samp`)** 是速度关键!**最短采样时间** 由信号源阻抗 (`R_s`) 和 ADC 输入电容 (`C_adc`, \~pF) 决定。`t_samp >= 5 * R_s * C_adc` (粗略)。对于前端缓冲后的低阻抗源 (`R_s < 100Ω`),可使用最短采样时间 (e.g., `SMP=000` = 1.5 cycles @ `ADCCLK`)。**计算转换时间 `t_conv` = `t_samp` + `t_conv12bit` (通常 8.5 cycles @12-bit)** 。`t_conv` 决定 **理论最大采样率 `f_s_max = ADCCLK / t_conv`** 。例如:`ADCCLK=50MHz`, `t_samp=1.5cyc`, `t_conv12bit=8.5cyc`, `t_conv=10cyc`, `f_s_max=5MSPS` (但 STM32H743 ADC1 在 12-bit 下标称 3.6MSPS,需实测)。 * **多通道扫描:** * `ADCx_SQR1`: `L[3:0]` 设置序列长度 (通道数)。 * `ADCx_SQR1/2/3/4`: `SQx[4:0]` 设置序列中第 `x` 个转换的通道号。 * **采样率代价:** 总采样率 `f_s_total = f_s_max / N_channels`。例如单通道可达 3.6MSPS,双通道扫描则每通道最大约 1.8MSPS。 * **校准:** * 上电后执行 `HAL_ADCEx_Calibration_Start()` (或寄存器操作 `ADCAL=1`)。校准偏移和线性度误差,存储在内部。**必须做!** **3. 定时器 (TIM) 配置 - 采样时钟源:** * **目的:** 产生精确的、可调的 PWM 或更新事件 (`UEV`) 来触发 ADC 采样。 * **配置 (以 TIM1 高级定时器为例):** * **时钟源:** 内部时钟 (`CK_INT`),通常来自高速 PLL (如 400MHz)。 * **分频器:** `PSC` (预分频器寄存器)。`Timer_CLK = CK_INT / (PSC + 1)` * **计数器:** `ARR` (自动重载寄存器)。计数器从 0 计数到 `ARR`。 * **触发输出 (TRGO):** * 模式 (`TIMx_CR2[MMS]`): 选择 `010` (更新事件 `UEV` 作为 TRGO) 或 `011` (OC1REF 作为 TRGO)。 * 如果选择 PWM 模式 (`MMS=011`): * `TIMx_CCMR1[OC1M]`: `110` (PWM 模式 1) 或 `111` (PWM 模式 2) * `TIMx_CCR1`: 设置 PWM 占空比 (通常设成 `ARR/2` 产生方波)。触发发生在 OC1REF 上升沿或下降沿 (取决于 PWM 模式)。 * **更新频率 (采样率 `fs`):** * 更新事件频率 `f_update = Timer_CLK / (ARR + 1)` * 当使用 `UEV` 触发时: **`fs = f_update`** * 当使用 PWM 触发时: **`fs = f_update`** (触发频率等于更新频率,与占空比无关) * **动态调整采样率:** 在运行时修改 `PSC` 或 `ARR` (通常通过用户旋转编码器事件触发)。**注意:** 修改 `ARR` 时,为避免计数不连续,可使用 `TIMx_EGR(UG)` 位产生一次更新事件,或使用预加载寄存器 (`TIMx_CR1[ARPE]=1`, 修改 `ARR` 后在下一次更新事件生效)。 **4. DMA 双缓冲模式实现 (核心数据搬运):** * **配置 (CubeMX/寄存器):** * **外设地址:** `ADC1->DR` (或 `ADCx_COMMON->CDR` for dual ADC) * **内存地址:** 指向两个缓冲区的指针 `adc_bufferA[]` 和 `adc_bufferB[]` (类型 `uint16_t`)。**大小:** `BUFFER_SIZE` (每个缓冲区能容纳的样本数)。 * **数据宽度:** 外设:半字 (16-bit, 对应 `DR`),内存:半字。 * **方向:** 外设到内存。 * **模式:** 循环模式 (`CIRC=1`)。 * **内存增量:** 开启 (`MINC=1`)。 * **外设增量:** 关闭 (`PINC=0`)。 * **双缓冲模式:** 开启 (`DBM=1`)。 * **传输长度:** `NDTR = 2 * BUFFER_SIZE` (总长度 = BufferA + BufferB)。 * **内存地址 0 (M0AR):** `&adc_bufferA[0]` * **内存地址 1 (M1AR):** `&adc_bufferB[0]` * **当前目标内存 (CT):** 由 DMA 自动管理。`CT=0` 表示当前正在填充 M0AR (BufferA),`CT=1` 表示正在填充 M1AR (BufferB)。 * **中断:** 开启传输完成中断 (`TCIE`) 和半传输完成中断 (`HTIE`)。**优先级:** 设置为高优先级 **中断服务程序 (ISR) 伪代码:** ```cs void DMA2_Stream0_IRQHandler(void) { // 假设 DMA2 Stream0 用于 ADC1 if (__HAL_DMA_GET_FLAG(&hdma_adc1, DMA_FLAG_HTIF0)) { // Half-Transfer (BufferA full) __HAL_DMA_CLEAR_FLAG(&hdma_adc1, DMA_FLAG_HTIF0); g_adc_buf_ready = BUFFER_A_READY; // 设置全局标志通知主循环处理 BufferA } if (__HAL_DMA_GET_FLAG(&hdma_adc1, DMA_FLAG_TCIF0)) { // Transfer-Complete (BufferB full) __HAL_DMA_CLEAR_FLAG(&hdma_adc1, DMA_FLAG_TCIF0); g_adc_buf_ready = BUFFER_B_READY; // 设置全局标志通知主循环处理 BufferB } } ``` **主循环处理伪代码:** ```cs while (1) { if (g_adc_buf_ready != BUFFER_NONE) { uint16_t *current_buf; if (g_adc_buf_ready == BUFFER_A_READY) { current_buf = adc_bufferA; } else { // BUFFER_B_READY current_buf = adc_bufferB; } g_adc_buf_ready = BUFFER_NONE; // 快速清除标志 // --- 这里是数据处理核心区 --- // 1. 触发检测 (SearchTrigger(current_buf, BUFFER_SIZE)) // 2. 找到触发点后,确定要显示的数据段 (考虑预触发点) // 3. 对显示段的数据应用波形压缩算法 (MinMaxCompress()) // 4. 将压缩后的点数据转换为屏幕坐标 // 5. 更新LCD波形显示 (DrawWaveform()) // ------------------------- } // ... 处理UI、菜单等其他任务 (非阻塞) } ``` **关键点:** * ISR 必须**极其精简**!只设置标志,不做复杂计算。 * 主循环中的数据处理部分 (`// --- 这里是数据处理核心区 ---`) 必须在下一个 DMA 中断到来前完成 (`BUFFER_SIZE / fs` 时间内)。否则会发生缓冲区覆盖,数据丢失。这是实时性的核心挑战!需要仔细优化算法。 **软件触发实现细节 (边沿触发为例):** ```cs typedef enum { TRIGGER_RISING, TRIGGER_FALLING, TRIGGER_HIGH, TRIGGER_LOW } TriggerType; typedef struct { uint8_t enabled; TriggerType type; uint8_t channel; // ADC channel index (0-based in buffer) uint16_t level; // ADC raw value corresponding to trigger voltage int32_t position; // Found trigger position in buffer (-1 if not found) } TriggerConfig; TriggerConfig g_trigger; ``` **触发搜索函数伪代码 (`SearchTrigger`):** ```cs int32_t SearchTrigger(uint16_t *buffer, uint32_t size) { if (!g_trigger.enabled) return -1; // Trigger disabled, always "trigger" at start or use free-run uint16_t prev_sample, curr_sample; uint32_t start_idx = 0; // Could start from a safe offset for (uint32_t i = start_idx + 1; i < size; i++) { prev_sample = buffer[i - 1]; // Previous sample for chosen channel curr_sample = buffer[i]; // Current sample for chosen channel switch (g_trigger.type) { case TRIGGER_RISING: if (prev_sample < g_trigger.level && curr_sample >= g_trigger.level) { g_trigger.position = i; // Trigger found at index i return i; } break; case TRIGGER_FALLING: if (prev_sample > g_trigger.level && curr_sample <= g_trigger.level) { g_trigger.position = i; return i; } break; case TRIGGER_HIGH: // Simpler, less common if (curr_sample >= g_trigger.level) { g_trigger.position = i; return i; } break; case TRIGGER_LOW: if (curr_sample <= g_trigger.level) { g_trigger.position = i; return i; } break; } } g_trigger.position = -1; // Trigger not found in this buffer return -1; } ``` **显示数据段确定 (假设屏幕宽度 `DISP_WIDTH` 像素):** * **目标:** 在双缓冲区的当前处理块中找到 `DISP_WIDTH` 个点,以触发点为中心 (或按预触发比例偏移)。 * **预触发:** 假设我们希望在触发点前显示 `PRE_TRIGGER_POINTS` 个点 (如占总显示点数的 20%)。 ```cs int32_t trigger_pos = g_trigger.position; // Result from SearchTrigger int32_t start_index; if (trigger_pos >= 0) { // Trigger found start_index = trigger_pos - PRE_TRIGGER_POINTS; if (start_index < 0) { // Not enough pre-trigger data? Pad with what we have. start_index = 0; } } else { // No trigger found (or disabled). Use tail of buffer (free-run mode) start_index = size - DISP_WIDTH; // Show the latest points if (start_index < 0) start_index = 0; } // Ensure we have DISP_WIDTH points available from start_index uint32_t points_to_display = MIN(DISP_WIDTH, size - start_index); ``` * **双缓冲区与预触发:** 双缓冲区 (`BUFFER_SIZE`) 必须设置得足够大 (`> DISP_WIDTH`),才能存储触发点*之前* 的数据。`BUFFER_SIZE` 决定了最大可捕获的预触发时间 (`PRE_TRIGGER_TIME = BUFFER_SIZE / fs`)。 **6. Min-Max 波形压缩算法 (高效显示的核心):** * **目的:** 将 `points_to_display` 个原始采样点 (可能上千) 压缩到 `DISP_WIDTH` 个像素列上 (320列)。 * **算法伪代码 (`MinMaxCompress`):** ```cs void MinMaxCompress(uint16_t *input, uint32_t input_len, uint16_t *min_buf, uint16_t *max_buf, uint32_t output_len) { uint32_t points_per_pixel = input_len / output_len; // May not be integer uint32_t remainder = input_len % output_len; uint32_t idx_in = 0; for (uint32_t pixel = 0; pixel < output_len; pixel++) { uint16_t pixel_min = 0xFFFF; // Initialize to max ADC value uint16_t pixel_max = 0x0000; // Initialize to min ADC value uint32_t points_this_pixel = points_per_pixel; // Distribute remainder to avoid cumulative error if (remainder > 0) { points_this_pixel++; remainder--; } // Find min and max within this group of points for (uint32_t p = 0; p < points_this_pixel; p++) { uint16_t val = input[idx_in++]; if (val < pixel_min) pixel_min = val; if (val > pixel_max) pixel_max = val; } // Store results for this pixel column min_buf[pixel] = pixel_min; max_buf[pixel] = pixel_max; } } ``` * **绘制:** 对于屏幕上的每一列 `x` (0 到 `DISP_WIDTH-1`): * 计算该列对应的最小点 `y_min` 和最大点 `y_max` (需将 ADC 值转换为屏幕 Y 坐标)。 * 在 `(x, y_min)` 和 `(x, y_max)` 之间画一条**垂直线**。这是保留峰值信息最有效的方式。 * **优化:** * 使用查表法 (`LUT`) 将 ADC 值 (`uint16_t`) 直接转换为屏幕 Y 坐标 (`uint8_t`),避免浮点运算 `y = (ADC_val - v_offset) * v_gain`。 * 内层循环 (`for (uint32_t p = ...`) 是热点,用指针遍历,确保编译器优化良好。 **7. 数字通道采集 (逻辑分析仪):** * **原理:** 使用另一个定时器 (`TIMx`) 触发,在固定时间间隔读取一组 GPIO 的状态。 * **配置:** * **GPIO:** 配置所需数量的 GPIO 为**输入** (带上拉/下拉或浮空,根据需求)。 * **定时器:** 配置一个定时器 (`TIMx`) 产生更新事件 (`UEV`) 或 PWM (频率 = 数字采样率 `f_digital`)。`f_digital` 通常远高于模拟采样率 (`f_analog`),但受限于 GPIO 读取和存储速度。 * **DMA:** * **外设地址:** `GPIOx->IDR` (输入数据寄存器)。 * **内存地址:** `uint16_t digi_bufferA[]`, `uint16_t digi_bufferB[]` (双缓冲)。 * **数据宽度:** 字 (32-bit) 或半字 (16-bit),取决于 `IDR` 宽度和需要的通道数。 * **配置:** 类似 ADC DMA (循环、双缓冲、定时器触发传输 `TIMx_TRGO` -\> `DMA_REQ`)。触发源选择定时器更新事件。 * **数据处理:** * 每个 `uint16_t/uint32_t` 样本代表所有数字通道在采样时刻的电平 (bit0=ch0, bit1=ch1, ...)。 * 在显示时,对每个通道,遍历显示时间窗口内的样本,检查对应 bit 是 1 还是 0。 * 在对应像素列 `x` 上,如果 bit 为 1,从 `Y_high` 到 `Y_high - height` 画线;如果为 0,从 `Y_low` 到 `Y_low + height` 画线 (形成方波)。不同通道用不同颜色/高度错开显示。 **8. 性能优化锦囊 (生死攸关):** * **编译器优化:** `-O2` 或 `-O3`,`-flto` (链接时优化)。检查生成的汇编。 * **数据局部性:** 确保处理的数据在 Cache 中。使用 `__attribute__((section(".ram_d2")))` 或 `MPU` 配置将 DMA 缓冲区和显示缓冲区放在最快的 RAM (如 DTCM on H7)。 * **指令选择:** * **整数运算:** 优先使用 `int32_t`/`uint32_t`。避免浮点。 * **位操作:** 用位掩码和移位代替乘除2的幂。 * **查表 (LUT):** 对于重复计算 (ADC-\>Voltage-\>Y, Sin/Cos for FFT)。 * **内联函数:** 对关键小函数使用 `__STATIC_INLINE`。 * **汇编:** 对绝对热点 (如 Min-Max 内层循环) 考虑手写汇编或 CMSIS-DSP 函数。 * **外设加速:** * **DMA2D (图形加速器):** 用于快速填充网格背景、绘制垂直线段、复制波形图像块。大幅提升 LCD 刷新率。 * **FPU (如果可用):** 如果必须做浮点 (如 FFT),确保开启 FPU,用单精度 (`float`),向量化运算。 * **CRC:** 校验数据传输 (可选)。 * **LCD 优化:** * **局部刷新:** 只更新波形区域,而非全屏。使用 `DMA2D` 区域填充/复制。 * **直接写 GRAM:** 使用 FSMC/FMC 的存储器映射模式直接操作 LCD GRAM 地址,比 SPI 命令快几个数量级。 * **优化画点/线函数:** 避免函数调用开销,直接操作内存或使用 `DMA2D`。 * **调试技巧:** * **GPIO 翻转:** 在 ISR 入口/出口、处理开始/结束处翻转 GPIO,用示波器测量执行时间。 * **DWT 计数器:** 使用 `DWT->CYCCNT` 进行 CPU 周期级精度的代码段计时。 * **串口输出统计:** 输出每个缓冲区处理耗时、最大耗时、触发成功率等。 **9. 模拟前端设计计算示例 (10x衰减档位):** * **目标:** 输入 ±10V,输出到 ADC 范围 0-3.3V。 * **衰减网络:** `R1` (输入电阻), `R2` (对地电阻)。衰减比 `Att = R2 / (R1 + R2) = 1/11` (10x probe is 1/10, but scope input usually 1M // \~20pF, so effective ratio is \~1/10.1 or similar. We use 1/11 for calculation simplicity). * **计算:** 输入 +10V -\> 输出 `10V * (1/11) ≈ 0.909V`。输入 -10V -\> 输出 `-10V * (1/11) ≈ -0.909V`。 * **电平移位:** 需要将 -0.909V \~ +0.909V 移位到 0V \~ 3.3V。中心点偏移 `Offset = (0.909V - (-0.909V)) / 2 + (-0.909V) = 0.909V - 0.909V = 0V`? 不对。 * 当前范围:`V_min_in = -0.909V`, `V_max_in = +0.909V`。中心点是 `0V`。 * 目标范围:`V_min_out = 0V`, `V_max_out = 3.3V`。中心点是 `1.65V`。 * 需要增益 `G` 和偏移 `V_os` 满足: `V_out = G * V_in + V_os` `@ V_in = -0.909V, V_out = 0V: 0 = G * (-0.909) + V_os` `@ V_in = +0.909V, V_out = 3.3V: 3.3 = G * (0.909) + V_os` 解方程: `(2) - (1): 3.3 = G * 1.818 => G = 3.3 / 1.818 ≈ 1.815` 代入 `(1): 0 = 1.815 * (-0.909) + V_os => V_os = 1.815 * 0.909 ≈ 1.65V` * **结论:** 需要一个增益 `G ≈ 1.815`,偏移 `V_os = 1.65V` 的**同相求和放大器**。 * **运放选择:** 信号带宽要求。假设目标示波器带宽 `BW_target = 5MHz`。运放所需增益带宽积 `GBW >= G * BW_target * Gain_Margin (e.g., 5) ≈ 1.815 * 5MHz * 5 ≈ 45.375MHz`。选择 GBW \> 50MHz 的运放 (如 ADA4807: 80MHz GBW)。 **10. 硬件触发 (进阶):** * **原理:** 利用 STM32 内部的**模拟比较器 (COMP)** 或**定时器输入捕获**直接产生硬件信号停止 DMA 或标记位置,极大减少软件触发延迟。 * **模拟比较器触发 (示例):** 1. 配置 COMPx (如 COMP1): * 反相端 (`INM`):连接 ADC 输入引脚 (经过调理的信号)。 * 同相端 (`INP`):连接 DAC 输出 (设置触发电平)。 * 输出极性:根据边沿选择。 * 使能窗口模式 (如果需要)。 2. 配置 COMPx 输出路由到定时器 (`TIMx`) 的刹车输入 (`BKIN`) 或作为 ADC 的触发源。 3. 配置定时器 (`TIMx`) 工作在 **One-Pulse 模式** 或使用 COMP 输出作为门控。 4. 当比较器输出跳变时,硬件立即响应: * 停止 ADC 转换 (通过定时器刹车)。 * 或触发一个 DMA 请求将当前地址/状态保存到特定寄存器。 * 或产生一个精确的中断。 5. 软件在中断中读取保存的状态,精确知道触发发生的时刻 (在 DMA 缓冲区中的位置)。 * **优点:** 延迟极低 (ns 到 us 级)。 * **缺点:** 配置复杂,资源有限 (COMP 数量少),灵活性不如软件触发 (难以实现复杂条件)。 **总结与行动路线:** 1. **选型定板:** 确定 STM32 (F4/H7), LCD, 决定模拟通道数、数字通道数、目标带宽/采样率。设计或购买前端调理板。 2. **搭建基础工程 (CubeMX):** * 配置时钟树 (PLL -\> High Speed Clocks)。 * 配置定时器 (TIM_ADC, TIM_DIGITAL) 产生 TRGO。 * 配置 ADC (规则组,定时器触发, DMA 双缓冲)。 * 配置 GPIO for Digital Inputs (if used) and TIM_DIGITAL DMA. * 配置 USART/USB for debug/output. * 配置 FSMC/FMC/SPI for LCD. * 配置 NVIC (DMA, TIM interrupts high priority). 3. **实现数据流:** * 验证 ADC DMA 双缓冲填充正常 (用 debugger 看 buffer 或串口打印)。 * 实现基本点绘制/滚动显示到 LCD。 4. **实现触发:** * 添加触发条件设置 (菜单/UI)。 * 实现 `SearchTrigger` 函数。 * 实现基于触发点的数据显示定位。 5. **优化显示:** * 实现 Min-Max 压缩算法。 * 优化 LCD 绘图 (局部刷新, DMA2D)。 6. **添加数字通道:** 配置并实现数字采集和逻辑波形显示。 7. **完善 UI \& 功能:** 菜单系统,量程切换 (控制继电器/DAC),测量功能 (Vpp, Freq)。 8. **(可选) 高级功能:** USB 传输,SD 卡存储,硬件触发,FFT。