STM32简易示波器/逻辑分析仪设计指南
核心模块深度剖析:
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) 伪代码:
void DMA2_Stream0_IRQHandler(void) { // 假设 DMA2 Stream0 用于 ADC1if (__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}
}
主循环处理伪代码:
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_READYcurrent_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
时间内)。否则会发生缓冲区覆盖,数据丢失。这是实时性的核心挑战!需要仔细优化算法。
软件触发实现细节 (边沿触发为例):
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 voltageint32_t position; // Found trigger position in buffer (-1 if not found)
} TriggerConfig;
TriggerConfig g_trigger;
触发搜索函数伪代码 (SearchTrigger
):
int32_t SearchTrigger(uint16_t *buffer, uint32_t size) {if (!g_trigger.enabled) return -1; // Trigger disabled, always "trigger" at start or use free-runuint16_t prev_sample, curr_sample;uint32_t start_idx = 0; // Could start from a safe offsetfor (uint32_t i = start_idx + 1; i < size; i++) {prev_sample = buffer[i - 1]; // Previous sample for chosen channelcurr_sample = buffer[i]; // Current sample for chosen channelswitch (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 ireturn 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 commonif (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 bufferreturn -1;
}
显示数据段确定 (假设屏幕宽度 DISP_WIDTH
像素):
-
目标: 在双缓冲区的当前处理块中找到
DISP_WIDTH
个点,以触发点为中心 (或按预触发比例偏移)。 -
预触发: 假设我们希望在触发点前显示
PRE_TRIGGER_POINTS
个点 (如占总显示点数的 20%)。
int32_t trigger_pos = g_trigger.position; // Result from SearchTrigger
int32_t start_index;if (trigger_pos >= 0) { // Trigger foundstart_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 pointsif (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
):
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 integeruint32_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 valueuint16_t pixel_max = 0x0000; // Initialize to min ADC valueuint32_t points_this_pixel = points_per_pixel;// Distribute remainder to avoid cumulative errorif (remainder > 0) {points_this_pixel++;remainder--;}// Find min and max within this group of pointsfor (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 columnmin_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 或标记位置,极大减少软件触发延迟。
-
模拟比较器触发 (示例):
-
配置 COMPx (如 COMP1):
-
反相端 (
INM
):连接 ADC 输入引脚 (经过调理的信号)。 -
同相端 (
INP
):连接 DAC 输出 (设置触发电平)。 -
输出极性:根据边沿选择。
-
使能窗口模式 (如果需要)。
-
-
配置 COMPx 输出路由到定时器 (
TIMx
) 的刹车输入 (BKIN
) 或作为 ADC 的触发源。 -
配置定时器 (
TIMx
) 工作在 One-Pulse 模式 或使用 COMP 输出作为门控。 -
当比较器输出跳变时,硬件立即响应:
-
停止 ADC 转换 (通过定时器刹车)。
-
或触发一个 DMA 请求将当前地址/状态保存到特定寄存器。
-
或产生一个精确的中断。
-
-
软件在中断中读取保存的状态,精确知道触发发生的时刻 (在 DMA 缓冲区中的位置)。
-
-
优点: 延迟极低 (ns 到 us 级)。
-
缺点: 配置复杂,资源有限 (COMP 数量少),灵活性不如软件触发 (难以实现复杂条件)。
总结与行动路线:
-
选型定板: 确定 STM32 (F4/H7), LCD, 决定模拟通道数、数字通道数、目标带宽/采样率。设计或购买前端调理板。
-
搭建基础工程 (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).
-
-
实现数据流:
-
验证 ADC DMA 双缓冲填充正常 (用 debugger 看 buffer 或串口打印)。
-
实现基本点绘制/滚动显示到 LCD。
-
-
实现触发:
-
添加触发条件设置 (菜单/UI)。
-
实现
SearchTrigger
函数。 -
实现基于触发点的数据显示定位。
-
-
优化显示:
-
实现 Min-Max 压缩算法。
-
优化 LCD 绘图 (局部刷新, DMA2D)。
-
-
添加数字通道: 配置并实现数字采集和逻辑波形显示。
-
完善 UI & 功能: 菜单系统,量程切换 (控制继电器/DAC),测量功能 (Vpp, Freq)。
-
(可选) 高级功能: USB 传输,SD 卡存储,硬件触发,FFT。