目录

便携式示波器实战:STM32 + TFT 显示,口袋里的测量神器

作为一名嵌入式工程师,示波器绝对是使用频率最高的工具之一。但台式示波器动辄几千上万,还笨重得很。今天我们就来 DIY 一台便携式示波器,用 STM32 做主控,配上 TFT 彩屏,成本不到 200 块,却能应付日常 80% 的测量需求。

这个项目源于我的一次踩坑经历:有次在客户现场调试电路,发现信号有问题,但没带示波器。等借到设备时,问题又复现不了了。从那以后,我就决心搞个能塞进工具包的小示波器。

需要准备什么?

物品 型号/规格 价格
主控板 STM32F407VET6 (168MHz) ¥45
显示屏 3.5 寸 TFT LCD (320×480) Â¥35
ADC 模块 外部 12 位 ADC (可选) ¥25
衰减电路 电阻分压网络 + 运放 ¥15
电池 18650 锂电池 2000mAh ¥20
充电模块 TP4056 充电板 ¥5
外壳 3D 打印或亚克力 ¥30
按钮开关 轻触开关 x6 ¥5
BNC 接口 示波器标准接口 ¥8
**总计** **¥188**

如果你手头有现成的 STM32 开发板(比如正点原子、野火),成本还能再降。显示屏也可以用更便宜的 2.4 寸,看个人需求。

步骤 1:硬件设计

核心原理图

示波器的核心就三件事:信号采集数据处理波形显示

信号输入 → 衰减/放大 → ADC 采集 → STM32 处理 → TFT 显示
           ↓              ↓
        量程切换       DMA 传输

输入衰减电路

STM32F4 的 ADC 输入范围是 0-3.3V,但我们要测的信号可能是 0-20V 甚至更高。所以需要衰减电路:

// 分压电阻计算:R1=30k, R2=10k → 衰减比 4:1
// 输入 0-20V → ADC 看到 0-5V (再加钳位保护到 3.3V)

void configure_attenuation() {
    // 使用模拟开关切换不同衰减比
    // 1X: 0-3.3V 量程
    // 10X: 0-33V 量程  
    HAL_GPIO_WritePin(ATTEN_GPIO, ATTEN_PIN, GPIO_PIN_SET);
}

⚠️ 踩坑提醒: 一开始我没加钳位二极管,结果一次误测 12V 直接把 ADC 引脚烧了。后来加了两个 BAT54S 做钳位,安全多了。

ADC 采样配置

STM32F407 自带 3 个 12 位 ADC,最高采样率 2.4MSPS。我们用定时器触发 + DMA 传输,实现连续采样:

// main.c - ADC 配置
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;

void MX_ADC1_Init(void) {
    ADC_ChannelConfTypeDef sConfig = {0};

    hadc1.Instance = ADC1;
    hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; // 42MHz
    hadc1.Init.Resolution = ADC_RESOLUTION_12B;
    hadc1.Init.ScanConvMode = DISABLE;
    hadc1.Init.ContinuousConvMode = ENABLE;  // 连续模式
    hadc1.Init.DiscontinuousConvMode = DISABLE;
    hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
    hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
    hadc1.Init.NbrOfConversion = 1;
    hadc1.Init.DMAContinuousRequests = ENABLE;  // DMA 连续请求
    hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV;

    if (HAL_ADC_Init(&hadc1) != HAL_OK) {
        Error_Handler();
    }

    // 配置通道:PA1 (ADC1_IN1)
    sConfig.Channel = ADC_CHANNEL_1;
    sConfig.Rank = 1;
    sConfig.SamplingTime = ADC_SAMPLETIME_3CYCLES;  // 最快采样
    if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) {
        Error_Handler();
    }
}

// DMA 配置:传输到缓冲区
#define ADC_BUFFER_SIZE 1024
uint32_t adc_buffer[ADC_BUFFER_SIZE];

void MX_DMA_Init(void) {
    hdma_adc1.Instance = DMA2_Stream0;
    hdma_adc1.Init.Channel = DMA_CHANNEL_0;
    hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;
    hdma_adc1.Init.Mode = DMA_CIRCULAR;  // 循环模式
    hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH;
    hdma_adc1.Init.FIFOMode = DMA_FIFOMODE_DISABLE;

    if (HAL_DMA_Init(&hdma_adc1) != HAL_OK) {
        Error_Handler();
    }

    __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1);
}

启动采样:

// 开始 ADC 采集(DMA 自动传输)
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, ADC_BUFFER_SIZE);

步骤 2:波形显示实现

TFT 屏幕我们用 SPI 接口驱动,刷新率能达到 30fps 以上。显示部分的核心是把 ADC 采样的数据点画到屏幕上:

// display.c - 波形绘制
#include "lcd.h"
#include "gui.h"

#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 480
#define WAVEFORM_COLOR 0x001F  // 蓝色
#define GRID_COLOR 0x8430      // 灰色

void draw_grid(void) {
    // 画网格线
    GUI_SetColor(GRID_COLOR);

    // 垂直线(时间刻度)
    for (int i = 0; i < SCREEN_WIDTH; i += 40) {
        GUI_DrawLine(i, 40, i, SCREEN_HEIGHT - 20);
    }

    // 水平线(电压刻度)
    for (int i = 40; i < SCREEN_HEIGHT; i += 40) {
        GUI_DrawLine(0, i, SCREEN_WIDTH, i);
    }

    // 画零电平基准线
    GUI_SetColor(0xF800);  // 红色
    GUI_DrawLine(0, SCREEN_HEIGHT/2, SCREEN_WIDTH, SCREEN_HEIGHT/2);
}

void draw_waveform(uint32_t* data, uint16_t length) {
    int16_t last_y = -1;

    GUI_SetColor(WAVEFORM_COLOR);

    for (int i = 0; i < length && i < SCREEN_WIDTH; i++) {
        // ADC 值 (0-4095) → 屏幕 Y 坐标
        uint16_t adc_value = data[i] & 0xFFF;
        int16_t y = SCREEN_HEIGHT - 20 - (adc_value * (SCREEN_HEIGHT - 60)) / 4095;

        // 限制 Y 坐标在显示区域内
        if (y < 40) y = 40;
        if (y > SCREEN_HEIGHT - 20) y = SCREEN_HEIGHT - 20;

        // 画线段(抗锯齿可以后续优化)
        if (last_y >= 0) {
            GUI_DrawLine(i, last_y, i, y);
        }
        last_y = y;
    }
}

void update_display(void) {
    // 清屏
    GUI_Clear(0x0000);  // 黑色背景

    // 画网格
    draw_grid();

    // 画波形
    draw_waveform(adc_buffer, ADC_BUFFER_SIZE);

    // 显示参数
    GUI_ShowString(10, 10, "CH1: 5V/div", 12, 0xFFFF);
    GUI_ShowString(10, 25, "Time: 1ms/div", 12, 0xFFFF);

    // 刷新屏幕
    LCD_Refresh();
}

步骤 3:触发功能实现

没有触发的示波器就像没有对焦的相机——根本看不清波形。我们实现一个简单的边沿触发:

// trigger.c - 触发控制
typedef enum {
    TRIG_MODE_AUTO,
    TRIG_MODE_NORMAL,
    TRIG_MODE_SINGLE
} TrigMode_t;

typedef enum {
    TRIG_EDGE_RISING,
    TRIG_EDGE_FALLING
} TrigEdge_t;

typedef struct {
    TrigMode_t mode;
    TrigEdge_t edge;
    uint16_t level;  // 触发电平 (0-4095)
    uint16_t timeout_ms;
} TriggerConfig_t;

TriggerConfig_t trig_config = {
    .mode = TRIG_MODE_AUTO,
    .edge = TRIG_EDGE_RISING,
    .level = 2048,  // 中间电平
    .timeout_ms = 100
};

// 检测触发点
int find_trigger_point(uint32_t* buffer, uint16_t length) {
    for (int i = 0; i < length - 1; i++) {
        uint16_t curr = buffer[i] & 0xFFF;
        uint16_t next = buffer[i + 1] & 0xFFF;

        if (trig_config.edge == TRIG_EDGE_RISING) {
            // 上升沿:从低于阈值到高于阈值
            if (curr < trig_config.level && next >= trig_config.level) {
                return i;
            }
        } else {
            // 下降沿
            if (curr >= trig_config.level && next < trig_config.level) {
                return i;
            }
        }
    }
    return -1;  // 未找到触发点
}

// 主循环中的触发处理
void oscilloscope_main_loop(void) {
    static uint32_t last_trig_time = 0;

    // 等待触发
    int trig_pos = find_trigger_point(adc_buffer, ADC_BUFFER_SIZE);

    if (trig_pos >= 0) {
        // 找到触发点,更新显示
        update_display();
        last_trig_time = HAL_GetTick();
    } else {
        // 自动模式下,超时后强制刷新
        if (trig_config.mode == TRIG_MODE_AUTO) {
            if (HAL_GetTick() - last_trig_time > trig_config.timeout_ms) {
                update_display();
                last_trig_time = HAL_GetTick();
            }
        }
    }
}

步骤 4:用户交互

用 6 个按钮实现基本控制:

// buttons.c - 按钮控制
// 按钮定义:UP, DOWN, LEFT, RIGHT, ENTER, BACK

void button_handler(uint8_t btn_id, ButtonEvent_t event) {
    static uint8_t menu_selection = 0;

    if (event == BUTTON_PRESS_SHORT) {
        switch (btn_id) {
            case BTN_UP:
                // 增加垂直档位
                adjust_vertical_scale(1);
                break;
            case BTN_DOWN:
                // 减少垂直档位
                adjust_vertical_scale(-1);
                break;
            case BTN_LEFT:
                // 减小时基
                adjust_timebase(-1);
                break;
            case BTN_RIGHT:
                // 增大时基
                adjust_timebase(1);
                break;
            case BTN_ENTER:
                // 切换触发模式
                toggle_trigger_mode();
                break;
            case BTN_BACK:
                // 返回菜单
                show_menu();
                break;
        }
    }
}

常见问题排查

问题 1:波形显示不稳定,一直在抖动

  • 原因: 触发设置不当或信号噪声太大
  • 解决:
    1. 检查触发电平是否在信号幅度范围内
    2. 尝试切换触发边沿(上升/下降)
    3. 在输入端加 100nF 电容滤波
    4. 如果是低频信号,用自动触发模式

问题 2:高频信号显示失真

  • 原因: 采样率不够或带宽限制
  • 解决:
    1. 降低时基(提高采样率)
    2. 检查 ADC 采样时间配置(用 3 周期最快)
    3. 确认信号频率不超过奈奎斯特频率(采样率/2)
    4. STM32F4 的 ADC 最高 2.4MSPS,理论带宽 1.2MHz,实际建议测 500kHz 以内

问题 3:屏幕刷新太慢

  • 原因: SPI 时钟太低或绘图算法效率低
  • 解决:
    1. 提高 SPI 时钟(最高 42MHz)
    2. 使用 DMA 传输显示数据
    3. 只刷新变化区域(局部刷新)
    4. 降低波形点数(用抽取算法)

问题 4:测量值不准确

  • 原因: ADC 参考电压漂移或分压电阻误差
  • 解决:
    1. 用精密万用表校准输入电压
    2. 在软件中加入校准系数
    3. 使用外部基准电压源(如 REF3033)
    4. 选择 1% 精度电阻做分压网络

性能指标

做完后的实测数据:

参数 指标
带宽 DC ~ 500kHz (-3dB)
采样率 最高 2MSPS
垂直分辨率 12 位 (4096 级)
垂直档位 1V/div ~ 10V/div
时基范围 10μs/div ~ 1s/div
输入阻抗 1MΩ (并联 20pF)
最大输入 ±35V (DC+AC)
屏幕刷新 30fps
电池续航 约 4 小时

这个性能对付数字电路调试、传感器信号分析、电源纹波测量都够用了。当然,跟几千块的台式示波器比还是有差距,但考虑到 1/10 不到的价格,性价比还是很香的。

扩展建议

如果你想继续升级这个项目:

  1. 增加第二通道 – 用 STM32 的第二个 ADC,实现双踪显示
  2. 添加 FFT 功能 – 用 DSP 库做频谱分析
  3. 支持 U 盘存储 – 把波形数据存成 CSV 格式
  4. 蓝牙/WiFi 连接 – 手机 APP 远程查看波形
  5. 自动测量 – 频率、周期、峰峰值、占空比等参数
  6. 李萨如图形 – X-Y 模式观察相位关系

代码我已经放到 GitHub 上了,欢迎 star 和 PR:

总结

这台便携式示波器从设计到调试,我大概花了两个周末。最大的收获不是省了多少钱,而是真正理解了示波器的工作原理。以前用台式示波器时,那些触发、采样、带宽的概念都很抽象,现在自己实现一遍,瞬间就通了。

对于经常在外面跑现场的工程师,或者预算有限的学生党,这个项目非常值得尝试。就算不买配件,光看代码学习 ADC、DMA、定时器这些外设的使用,也是很好的实践。

最后提醒一句:测量高压信号时一定要注意安全!这台示波器的输入是浮地的,不能直接测市电。如果需要测 220V,请配合隔离变压器或高压差分探头使用。

希望这篇博客文章对您有所帮助!


相关资源:

更多关于 的文章
关注创客出手公众号

关注创客出手