简易数字示波器:STM32+ADC+TFT屏的波形显示——采样率、触发、时基全链路实战

文章目录


每日一句正能量

人生没有绝对的圆满,过于执着外界的评价容易丢失真正的自己。

圆满是一种理想状态,现实中总有遗憾、缺漏或意外。如果你把"得到所有人认可"当作幸福的前提,就会永远活在别人的嘴里。外界评价就像海浪,执着于它的人会像浮标一样起伏不定。而真正的自己,是在安静时、独处时、做热爱的事时,那个不假思索就出现的状态。

命运的变化不是突然降临的奇迹,而是当你从"被推着走"切换到"自己定规则"时,每一天微小选择中累积的偏移。

导读

上一篇我们完成了基于STM32的智能小车系统,本篇将转向测量仪器领域,从零构建一台简易数字示波器。通过STM32的ADC高速采样、DMA数据传输、触发控制与TFT波形显示,深入理解数字示波器的核心原理,并特别关注采样率、触发机制和时基控制这三个决定示波器性能的关键要素。


一、项目概述与系统架构

数字示波器是电子工程师的"眼睛",其本质是将模拟信号转换为数字信号并可视化显示。本项目基于STM32F103C8T6构建一台简易数字示波器,实现以下核心功能:

  • 双通道信号采集:0-3.3V输入,12位分辨率
  • 可调采样率:最高1Msps(每秒百万次采样)
  • 多种触发模式:上升沿/下降沿/电平触发
  • 10档时基控制:100μs/div ~ 100ms/div
  • 实时波形显示:2.4寸TFT彩屏,320×240分辨率
  • 自动参数测量:频率、峰峰值、最大值、最小值

1.1 系统总体架构

本系统采用经典的数字示波器信号链路

信号调理链路:BNC输入 → 衰减网络(1x/10x) → 运放缓冲 → 分压限幅 → STM32 ADC

数字处理链路:ADC采样 → DMA传输 → RAM缓冲区 → 触发检测 → 波形处理 → TFT显示

控制链路:4个按键(+/-/OK/MODE)→ GPIO输入 → 菜单系统 → 参数调节


二、硬件选型与电路设计

2.1 核心器件选型

器件 型号 关键参数 选型理由
主控 STM32F103C8T6 72MHz, 12bit ADC 内置ADC支持1Msps,DMA传输
TFT屏 2.4寸ILI9341 320×240, SPI接口 性价比高,驱动成熟
运放 LM358 双通道, 1MHz带宽 低成本电压跟随器
稳压管 BZX55C3V3 3.3V稳压 ADC输入保护

2.2 信号调理电路

衰减网络设计

10x衰减采用900KΩ + 100KΩ电阻分压,将0-10V输入衰减为0-1V,再经运放缓冲后送入ADC。1x模式下通过继电器/模拟开关直通。

输入保护

  • 稳压管钳位:3.3V稳压管并联在ADC输入端,防止过压损坏
  • 限流电阻:100Ω串联电阻限制瞬态电流
  • 运放缓冲:LM358电压跟随器提供高输入阻抗(约1MΩ),减少对被测电路的影响

关键设计要点

  • 输入阻抗需≥1MΩ,避免对被测电路产生负载效应
  • 运放带宽需≥信号最高频率的3倍,保证信号不失真
  • 分压电阻精度选用1%,确保衰减比准确

2.3 TFT屏接口

ILI9341驱动芯片支持8/16位并行和SPI串行接口。本项目采用SPI模式以节省IO口:

TFT引脚 STM32引脚 功能说明
CS PA4 片选,低电平有效
DC PA3 数据/命令选择
RESET PA2 硬件复位
MOSI PA7 SPI数据输出
SCK PA5 SPI时钟
LED 3.3V 背光供电

SPI配置为模式0(CPOL=0, CPHA=0),时钟分频后约18MHz,刷新率可达30-60fps。


三、ADC采样与DMA传输

3.1 STM32 ADC工作原理

STM32F103的ADC为逐次逼近型(SAR),12位分辨率,最大采样率1Msps。单次转换时序:

转换时间 = 采样保持时间 + 逐次逼近时间

  • 采样保持:1.5个ADC时钟周期(可配置为1.5/7.5/13.5/28.5/41.5/55.5/71.5/239.5)
  • 逐次逼近:固定的12.5个周期

当ADC时钟为12MHz时,最短转换时间 = (1.5 + 12.5) / 12MHz = 1.17μs ,对应采样率约857ksps

要达到1Msps,需将ADC时钟提升至14MHz(接近最大允许值),或使用更短的采样时间。

3.2 DMA双缓冲采样机制

采用DMA双缓冲机制实现零等待采样:

c 复制代码
#define SAMPLE_BUFFER_SIZE  1024

uint16_t adc_buffer_a[SAMPLE_BUFFER_SIZE];
uint16_t adc_buffer_b[SAMPLE_BUFFER_SIZE];
uint16_t *adc_ready_buffer = NULL;
volatile uint8_t buffer_ready_flag = 0;

/* ADC+DMA初始化 */
void ADC_DMA_Init(void) {
    GPIO_InitTypeDef GPIO_InitStruct;
    ADC_InitTypeDef ADC_InitStruct;
    DMA_InitTypeDef DMA_InitStruct;
    
    // 使能时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE);
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
    
    // PA0: ADC输入
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN;  // 模拟输入
    GPIO_Init(GPIOA, &GPIO_InitStruct);
    
    // DMA配置: 外设→内存,循环模式
    DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
    DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)adc_buffer_a;
    DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;
    DMA_InitStruct.DMA_BufferSize = SAMPLE_BUFFER_SIZE;
    DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
    DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
    DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;  // 循环模式
    DMA_InitStruct.DMA_Priority = DMA_Priority_High;
    DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;
    DMA_Init(DMA1_Channel1, &DMA_InitStruct);
    
    // ADC配置
    ADC_InitStruct.ADC_Mode = ADC_Mode_Independent;
    ADC_InitStruct.ADC_ScanConvMode = DISABLE;      // 单通道
    ADC_InitStruct.ADC_ContinuousConvMode = ENABLE;  // 连续转换
    ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
    ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;
    ADC_InitStruct.ADC_NbrOfChannel = 1;
    ADC_Init(ADC1, &ADC_InitStruct);
    
    // 配置采样时间: 1.5 cycles(最快速度)
    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_1Cycles5);
    
    // 使能ADC DMA请求
    ADC_DMACmd(ADC1, ENABLE);
    DMA_Cmd(DMA1_Channel1, ENABLE);
    ADC_Cmd(ADC1, ENABLE);
    
    // ADC校准
    ADC_ResetCalibration(ADC1);
    while(ADC_GetResetCalibrationStatus(ADC1));
    ADC_StartCalibration(ADC1);
    while(ADC_GetCalibrationStatus(ADC1));
    
    // 启动ADC转换
    ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}

/* DMA中断:半传输/传输完成 */
void DMA1_Channel1_IRQHandler(void) {
    if(DMA_GetITStatus(DMA1_IT_HT1)) {  // 半传输中断
        adc_ready_buffer = adc_buffer_a;
        buffer_ready_flag = 1;
        DMA_ClearITPendingBit(DMA1_IT_HT1);
    }
    if(DMA_GetITStatus(DMA1_IT_TC1)) {  // 传输完成中断
        adc_ready_buffer = adc_buffer_b;
        buffer_ready_flag = 1;
        DMA_ClearITPendingBit(DMA1_IT_TC1);
    }
}

双缓冲机制优势

  • ADC持续填充Buffer A时,CPU处理Buffer B
  • 半传输中断和传输完成中断交替触发,实现零拷贝数据交换
  • 避免CPU等待ADC,最大化系统吞吐量

3.3 采样率与时基控制

采样率与时基档位的关系:

复制代码
采样率 = 显示点数 / 屏幕总时间
屏幕总时间 = 每格时间 × 水平格数(10格)

例如,时基为1ms/div时:

  • 屏幕总时间 = 1ms × 10 = 10ms
  • 采样率 = 1024点 / 10ms = 102.4ksps

通过调整ADC的预分频器和采样时间,实现不同档位的采样率:

c 复制代码
typedef struct {
    uint32_t time_per_div;   // 每格时间(μs)
    uint32_t sample_rate;    // 采样率(Hz)
    uint32_t adc_prescaler;  // ADC预分频
    uint32_t sample_time;    // 采样时间配置
} TimeBase_Config;

TimeBase_Config timebase_table[] = {
    {100,   1024000, RCC_PCLK2_Div2, ADC_SampleTime_1Cycles5},   // 100μs/div
    {200,    512000, RCC_PCLK2_Div2, ADC_SampleTime_1Cycles5},   // 200μs/div
    {500,    204800, RCC_PCLK2_Div4, ADC_SampleTime_7Cycles5},   // 500μs/div
    {1000,   102400, RCC_PCLK2_Div4, ADC_SampleTime_7Cycles5},   // 1ms/div
    {2000,    51200, RCC_PCLK2_Div6, ADC_SampleTime_13Cycles5},  // 2ms/div
    {5000,    20480, RCC_PCLK2_Div6, ADC_SampleTime_28Cycles5},  // 5ms/div
    {10000,   10240, RCC_PCLK2_Div8, ADC_SampleTime_41Cycles5},  // 10ms/div
    {20000,    5120, RCC_PCLK2_Div8, ADC_SampleTime_55Cycles5},  // 20ms/div
    {50000,    2048, RCC_PCLK2_Div8, ADC_SampleTime_71Cycles5},  // 50ms/div
    {100000,   1024, RCC_PCLK2_Div8, ADC_SampleTime_239Cycles5}, // 100ms/div
};

四、触发系统实现

4.1 触发原理

触发是示波器的灵魂功能,其作用是稳定显示周期性波形。没有触发时,波形会在屏幕上左右滚动;触发后,每次从信号的同一位置开始显示,实现波形"冻结"。

触发过程

  1. 设定触发电平 (如2.5V)和触发边沿(上升沿/下降沿)
  2. ADC持续采样,软件扫描采样数据
  3. 当检测到信号从低于触发电平变为高于触发电平时,判定为触发条件满足
  4. 以触发点为基准,截取显示窗口的数据
  5. 将截取的数据送往TFT显示

4.2 触发检测算法

c 复制代码
typedef enum {
    TRIGGER_RISING,   // 上升沿触发
    TRIGGER_FALLING,  // 下降沿触发
    TRIGGER_LEVEL,    // 电平触发
    TRIGGER_AUTO,     // 自动触发(无信号时强制刷新)
    TRIGGER_SINGLE    // 单次触发
} Trigger_Mode;

typedef struct {
    Trigger_Mode mode;
    uint16_t level;       // 触发电平(ADC值 0-4095)
    uint16_t pre_trigger; // 预触发点数(触发点左侧显示点数)
} Trigger_Config;

Trigger_Config g_trigger = {TRIGGER_RISING, 2048, 100};

/* 触发检测 */
int16_t Trigger_FindPoint(uint16_t *buffer, uint16_t size) {
    uint16_t trigger_adc = g_trigger.level;
    
    switch(g_trigger.mode) {
        case TRIGGER_RISING:
            for(int i = 1; i < size; i++) {
                if(buffer[i-1] < trigger_adc && buffer[i] >= trigger_adc) {
                    return i;  // 找到上升沿触发点
                }
            }
            break;
            
        case TRIGGER_FALLING:
            for(int i = 1; i < size; i++) {
                if(buffer[i-1] > trigger_adc && buffer[i] <= trigger_adc) {
                    return i;  // 找到下降沿触发点
                }
            }
            break;
            
        case TRIGGER_AUTO:
            // 强制返回中心位置
            return size / 2;
            
        case TRIGGER_SINGLE:
            // 仅触发一次,触发后停止采样
            for(int i = 1; i < size; i++) {
                if(buffer[i-1] < trigger_adc && buffer[i] >= trigger_adc) {
                    ADC_Stop();  // 停止ADC
                    return i;
                }
            }
            break;
            
        default: break;
    }
    
    return -1;  // 未找到触发点
}

4.3 预触发与显示窗口

实际示波器中,触发点通常不在屏幕最左侧,而是预留预触发深度(如20%),这样可以看到触发前的信号状态:

c 复制代码
/* 截取显示窗口 */
void Waveform_Extract(uint16_t *src, uint16_t src_size, 
                      uint16_t *dst, uint16_t dst_size,
                      int16_t trigger_point) {
    int16_t start_idx = trigger_point - g_trigger.pre_trigger;
    
    // 边界检查
    if(start_idx < 0) start_idx = 0;
    if(start_idx + dst_size > src_size) start_idx = src_size - dst_size;
    
    // 复制数据
    for(int i = 0; i < dst_size; i++) {
        dst[i] = src[start_idx + i];
    }
}

五、TFT波形显示

5.1 显示界面设计

示波器显示界面包含以下元素:

波形区域:黑色背景,白色/彩色网格线,绿色波形轨迹

状态栏(顶部):

  • CH1/CH2垂直灵敏度(V/div)
  • 时基档位(s/div)
  • 触发状态(TRIG'D/READY/AUTO)

测量值区域(左侧/底部):

  • 频率(Freq)
  • 峰峰值(Vpp)
  • 最大值(Vmax)
  • 最小值(Vmin)
  • 平均值(Vavg)

5.2 波形绘制算法

c 复制代码
#define SCREEN_WIDTH   320
#define SCREEN_HEIGHT  240
#define GRID_DIV_X     10
#define GRID_DIV_Y     8
#define WAVE_Y_CENTER  120

/* 坐标映射:ADC值 → 屏幕Y坐标 */
uint16_t ADC_To_ScreenY(uint16_t adc_value, float v_scale) {
    // ADC: 0-4095 对应 0-3.3V
    float voltage = (adc_value * 3.3f) / 4095.0f;
    float v_offset = 1.65f;  // 中心电压
    
    // 像素/伏 = 屏幕高度/8格 / v_scale
    float pixel_per_volt = (SCREEN_HEIGHT / GRID_DIV_Y) / v_scale;
    
    int16_t y = WAVE_Y_CENTER - (int16_t)((voltage - v_offset) * pixel_per_volt);
    
    // 限幅
    if(y < 0) y = 0;
    if(y >= SCREEN_HEIGHT) y = SCREEN_HEIGHT - 1;
    
    return (uint16_t)y;
}

/* 绘制波形 */
void Waveform_Draw(uint16_t *wave_data, uint16_t points, float v_scale) {
    uint16_t x_step = SCREEN_WIDTH / points;
    uint16_t last_x = 0, last_y = WAVE_Y_CENTER;
    
    // 清除旧波形(使用背景色重绘)
    TFT_FillRect(0, 20, SCREEN_WIDTH, SCREEN_HEIGHT - 40, COLOR_BLACK);
    
    // 绘制网格
    Draw_Grid();
    
    // 绘制波形
    TFT_SetColor(COLOR_GREEN);
    for(int i = 0; i < points - 1; i++) {
        uint16_t x1 = i * x_step;
        uint16_t y1 = ADC_To_ScreenY(wave_data[i], v_scale);
        uint16_t x2 = (i + 1) * x_step;
        uint16_t y2 = ADC_To_ScreenY(wave_data[i + 1], v_scale);
        
        // 使用线段连接相邻点
        TFT_DrawLine(x1, y1, x2, y2);
        
        last_x = x2;
        last_y = y2;
    }
}

/* 绘制网格 */
void Draw_Grid(void) {
    TFT_SetColor(COLOR_DARKGRAY);
    
    // 垂直线(10格)
    for(int i = 0; i <= GRID_DIV_X; i++) {
        uint16_t x = i * (SCREEN_WIDTH / GRID_DIV_X);
        TFT_DrawLine(x, 20, x, SCREEN_HEIGHT - 20);
    }
    
    // 水平线(8格)
    for(int i = 0; i <= GRID_DIV_Y; i++) {
        uint16_t y = 20 + i * ((SCREEN_HEIGHT - 40) / GRID_DIV_Y);
        TFT_DrawLine(0, y, SCREEN_WIDTH, y);
    }
    
    // 中心十字线(加粗)
    TFT_SetColor(COLOR_GRAY);
    TFT_DrawLine(SCREEN_WIDTH/2, 20, SCREEN_WIDTH/2, SCREEN_HEIGHT-20);
    TFT_DrawLine(0, WAVE_Y_CENTER, SCREEN_WIDTH, WAVE_Y_CENTER);
}

5.3 波形插值优化

当采样点数较少时,直接连线会产生锯齿。采用线性插值使波形更平滑:

c 复制代码
/* 线性插值:在相邻采样点之间插入中间点 */
void Waveform_Interpolate(uint16_t *src, uint16_t src_size,
                          uint16_t *dst, uint16_t dst_size) {
    float ratio = (float)(src_size - 1) / (dst_size - 1);
    
    for(int i = 0; i < dst_size; i++) {
        float src_idx = i * ratio;
        int idx_low = (int)src_idx;
        int idx_high = idx_low + 1;
        float frac = src_idx - idx_low;
        
        if(idx_high >= src_size) idx_high = src_size - 1;
        
        // 线性插值
        dst[i] = (uint16_t)(src[idx_low] * (1 - frac) + src[idx_high] * frac);
    }
}

六、奈奎斯特采样定理与混叠

6.1 采样定理核心

奈奎斯特采样定理 :为了无失真地恢复原始信号,采样频率必须大于信号最高频率的2倍

f s > 2 × f m a x f_s > 2 \times f_{max} fs>2×fmax

实际工程建议 :采样率 ≥ 5倍信号频率,以保证波形还原质量。

6.2 混叠效应

当采样率不足时,高频信号会被"伪装"成低频信号,这种现象称为混叠(Aliasing)

混叠频率计算

f a l i a s = ∣ f s i g n a l − n × f s a m p l e ∣ f_{alias} = |f_{signal} - n \times f_{sample}| falias=∣fsignal−n×fsample∣

其中n为使f_alias落在0~f_sample/2范围内的整数。

抗混叠措施

  1. 硬件低通滤波:在ADC输入端加入RC低通滤波器,截止频率设为f_sample/2
  2. 软件过采样:以更高采样率采集后数字降采样
  3. 合理选择时基:确保当前时基下的采样率满足奈奎斯特准则
c 复制代码
/* 检查当前设置是否满足奈奎斯特准则 */
uint8_t Check_Nyquist(float signal_freq) {
    float nyquist_freq = g_sample_rate / 2.0f;
    
    if(signal_freq > nyquist_freq) {
        // 显示警告
        TFT_DrawString(10, 5, \"ALIAS WARNING!\", COLOR_RED);
        return 0;  // 不满足
    }
    return 1;  // 满足
}

七、参数测量与光标功能

7.1 自动参数测量

c 复制代码
typedef struct {
    float frequency;   // 频率
    float vpp;         // 峰峰值
    float vmax;        // 最大值
    float vmin;        // 最小值
    float vavg;        // 平均值
    float duty_cycle;  // 占空比
} Waveform_Measure;

/* 参数测量 */
void Waveform_Measure(uint16_t *data, uint16_t size, Waveform_Measure *result) {
    uint32_t sum = 0;
    uint16_t max_val = 0, min_val = 4095;
    
    // 最大值、最小值、平均值
    for(int i = 0; i < size; i++) {
        if(data[i] > max_val) max_val = data[i];
        if(data[i] < min_val) min_val = data[i];
        sum += data[i];
    }
    
    result->vmax = (max_val * 3.3f) / 4095.0f;
    result->vmin = (min_val * 3.3f) / 4095.0f;
    result->vpp = result->vmax - result->vmin;
    result->vavg = ((sum / size) * 3.3f) / 4095.0f;
    
    // 频率测量:过零检测法
    uint16_t cross_count = 0;
    uint16_t first_cross = 0, last_cross = 0;
    uint16_t threshold = (max_val + min_val) / 2;
    
    for(int i = 1; i < size; i++) {
        if(data[i-1] < threshold && data[i] >= threshold) {
            if(cross_count == 0) first_cross = i;
            last_cross = i;
            cross_count++;
        }
    }
    
    if(cross_count >= 2) {
        float period_samples = (float)(last_cross - first_cross) / (cross_count - 1);
        result->frequency = g_sample_rate / period_samples;
    } else {
        result->frequency = 0;
    }
}

7.2 光标测量功能

光标功能允许用户手动测量波形上任意两点的时间差和电压差:

c 复制代码
typedef struct {
    uint16_t x1, y1;  // 光标A位置
    uint16_t x2, y2;  // 光标B位置
    uint8_t active;   // 光标是否启用
} Cursor_Measure;

/* 绘制光标 */
void Cursor_Draw(Cursor_Measure *cursor, float v_scale, uint32_t time_per_div) {
    if(!cursor->active) return;
    
    // 光标A(虚线十字)\n    TFT_SetColor(COLOR_YELLOW);\n    Draw_DashedLine(cursor->x1, 20, cursor->x1, SCREEN_HEIGHT-20);\n    Draw_DashedLine(0, cursor->y1, SCREEN_WIDTH, cursor->y1);\n    \n    // 光标B(虚线十字)\n    TFT_SetColor(COLOR_CYAN);\n    Draw_DashedLine(cursor->x2, 20, cursor->x2, SCREEN_HEIGHT-20);\n    Draw_DashedLine(0, cursor->y2, SCREEN_WIDTH, cursor->y2);\n    \n    // 计算差值\n    float dx = (cursor->x2 - cursor->x1) * time_per_div / (SCREEN_WIDTH / GRID_DIV_X);\n    float dy = (cursor->y2 - cursor->y1) * v_scale / (SCREEN_HEIGHT / GRID_DIV_Y);\n    \n    // 显示测量结果\n    char buf[64];\n    sprintf(buf, \"dT=%.2fus dV=%.2fV\", dx, dy);\n    TFT_DrawString(200, 5, buf, COLOR_WHITE);\n}

八、性能测试与优化

8.1 实测性能指标

指标 设计值 实测值 备注
最大采样率 1Msps 857ksps 受限于ADC时钟
模拟带宽 100kHz ~80kHz -3dB点
垂直分辨率 12bit 12bit 理论值
存储深度 1024点 1024点 可扩展至2048
刷新率 30fps 25-40fps 取决于时基档位
输入阻抗 1MΩ ~1.1MΩ 含运放偏置
频率测量精度 ±1% ±2% 受噪声影响

8.2 性能优化策略

1. 采样率优化

  • 使用双ADC交错采样(STM32部分型号支持),理论采样率翻倍
  • 降低ADC分辨率至10bit或8bit,缩短转换时间

2. 显示刷新优化

  • 仅刷新波形区域,不重复绘制静态UI元素
  • 使用DMA传输向TFT发送数据,减少CPU占用
  • 实现局部刷新:只更新变化的像素

3. 触发稳定性优化

  • 加入触发滞后(Hysteresis):设置两个阈值(上升沿触发需跨越 upper_threshold,下降沿需低于 lower_threshold)
  • 使用数字滤波消除噪声导致的误触发
c 复制代码
/* 带滞后的触发检测 */
int16_t Trigger_Find_Hysteresis(uint16_t *buffer, uint16_t size) {

    uint16_t upper = g_trigger.level + 50;   // 上阈值 
    uint16_t lower = g_trigger.level - 50;   // 下阈值
    for(int i = 2; i < size; i++) {
    if(buffer[i-2] < lower && buffer[i-1] < upper && buffer[i] >= upper) {           
    return i;  // 严格上升沿
       }    
        }    
        return -1;
        }

九、常见问题与调试技巧

问题现象 可能原因 解决方案
波形抖动 触发不稳定/噪声干扰 增加触发滞后,启用平均滤波
显示花屏 SPI速率过高/接线不良 降低SPI速率,检查接线
采样率低 ADC时钟配置错误 检查RCC_ADCCLKConfig
波形失真 输入信号超量程 切换衰减档位,调整垂直灵敏度
频率测量不准 信号噪声大/过零点抖动 增加采样点数,使用插值算法
屏幕刷新慢 全屏重绘效率低 实现局部刷新,优化绘制算法

十、总结与展望

本项目从硬件电路到软件算法,完整实现了一台基于STM32的简易数字示波器。核心要点总结:

  1. 采样率是基础:理解奈奎斯特定理,合理配置ADC时钟和采样时间
  2. 触发是灵魂:实现稳定的触发机制是波形显示的关键
  3. 时基决定视野:通过调整采样率实现不同时间尺度的信号观察
  4. DMA解放CPU:双缓冲机制实现高效的数据流传输
  5. 显示优化体验:合理的UI布局和绘制算法提升使用体验

后续升级方向

  • 增加FFT频谱分析功能,实现频域观察
  • 支持双通道同时显示,对比两个信号
  • 加入USB数据传输,连接PC进行深度分析
  • 升级为彩色波形显示,不同通道用不同颜色
  • 增加波形存储与回放功能,保存重要信号

转载自:https://blog.csdn.net/u014727709/article/details/162450016

欢迎 👍点赞✍评论⭐收藏,欢迎指正