
简介STM32示波器HAL项目源码包面向具备一定单片机基础的嵌入式学习者和STM32开发者围绕用HAL库搭建自制数字示波器展开可用于理解定时器触发采样、ADC多通道采集、串口传输、中断回调与波形数据处理等环节。压缩包共1261个文件约40.33MB以C源文件和H头文件为主体配合汇编启动文件、工程编译中间文件、链接脚本、静态库及Keil MDK工程文件完整保留了从外设驱动到应用逻辑的组织结构。已有597人学习下载说明其在STM32实践项目中具有一定参考价值。读者可从中借鉴HAL初始化、定时器与ADC配置、UART通信、缓冲区管理以及Core与Drivers目录的分层方式也可结合显示屏和图形界面相关代码梳理示波器的数据展示思路适合作为课程设计、毕业设计或嵌入式练手的对照工程。1. STM32示波器不是“采到点就能画”先给采样链路算三笔账把 stm32 单片机做成一台能看波形的示波器最容易被低估的不是屏驱而是采样、触发、显示三段时间常数互相打架。这份资源是一套基于 STM32 HAL 的示波器源码工程核心链路是 ADC 采样、定时器触发、DMA 搬运、环形缓冲找触发点再把码值换算成电压画到屏上。它适合手里有开发板、想从“能采数”走到“波形稳定可读”的嵌入式从业者也适合课程设计场景新手可以照参数表复现熟手重点看触发和前端边界。2. 采样链路落地ADCDMA定时器触发的参数账与最小代码示波器工程最先崩的地方往往不是画线函数而是采样链路没有算账。ADC 采多快、定时器多久触发一次、DMA 搬多少个点、屏幕一次画多少个点这四个数只要有一个拍脑袋后面就会看到“波形能出来但没法看”的结果。我一般先把采样率、存储深度和显示窗口对齐再进图形化配置工具点参数这样返工少。2.1 采样率、带宽和存储深度先对齐采样率不是越高越好。ADC 每个点都要占用转换时间、DMA 带宽和内存屏幕又不可能把几万个点全画出来。比较稳妥的做法是先定目标观察频段再按每周期 10 到 20 个点反推采样率。比如想看 1 kHz 以内的信号采样率放到 20 kS/s 到 50 kS/s 已经够用如果目标到 10 kHz采样率至少要到 100 kS/s 以上同时要确认前端运放和 ADC 采样保持电路跟得上。存储深度决定你一次能看多长的窗口。环形缓冲开 2048 点、采样率 20 kS/s一屏大约 102 ms同样 2048 点把采样率提到 200 kS/s窗口只剩 10 ms。很多人调时基时只改显示缩放不改采样率结果就是“时基变了但波形细节没变”这是把显示窗口和采样窗口混为一谈了。目标场景建议采样率每周期点数需要先确认的边界音频段、电源纹波20 kS/s 到 50 kS/s10 到 20前端偏置是否稳定ADC 采样时间是否足够电机电流、PWM 观察100 kS/s 到 500 kS/s10 到 16定时器触发是否抖动DMA 是否跟得上开关节点、窄脉冲1 MS/s 以上看脉宽而定模拟前端带宽、探头地线、ADC 有效位还有一笔账是 ADC 采样时间。STM32 的 ADC 前面通常有采样保持电容源阻抗高、采样时间短读出来的码值会发飘。分压电阻用大了省电但采样时间不够就会“电压对不上”我一般把分压网络阻抗压到几十千欧以内必要时加一级运放缓冲再适当加长 ADC 采样周期。这个改动比在软件里加滤波更根本。注意采样率提高后DMA 中断频率也会提高。中断里只置标志位不要在回调里做浮点换算和画屏否则主循环会被拖死。2.2 HAL初始化里必须盯住的五个参数HAL 库把很多寄存器操作封装成结构体方便是方便但示波器场景最怕默认值。下面五个参数我每次都会逐项核对尤其是连续转换和 DMA 模式配错后现象很隐蔽程序能跑波形却只更新一次。参数示波器场景建议配错后的典型现象ADC 分辨率12 位起步够用再考虑过采样码值跳变大小信号看不清连续转换关闭交给定时器触发采样率不受控时基对不上外部触发源选定时器 TRGO 或对应事件波形更新节奏飘触发点乱DMA 模式循环模式只采一轮就停屏幕冻结ADC 采样时间按源阻抗加长同一电压读数偏低或跳动图形化配置工具里把 ADC 的 Continuous Conversion Mode 关掉External Trigger 选定时器触发DMA 请求开循环模式。不同 STM32 系列的 HAL 形参类型可能不一样但配置思路一致定时器负责“什么时候采”ADC 负责“采一个点”DMA 负责“把点搬走”CPU 只处理搬完的窗口。这个分工定下来后面触发和显示才有稳定的数据源。2.3 定时器触发与DMA半满中断的代码骨架下面这段是采样链路的骨架重点看 DMA 长度、半满回调和满回调。实际工程里要把hadc1、hdma_adc1换成自己的句柄定时器周期按目标采样率算。#include main.h #define ADC_BUF_LEN 2048U static uint16_t adc_dma_buf[ADC_BUF_LEN]; volatile uint8_t adc_half_ready 0; volatile uint8_t adc_full_ready 0; /* 启动 ADC 定时器触发 DMA 循环搬运 */ void Scope_Start(void) { /* 长度单位是“转换次数”不是字节数 */ if (HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_dma_buf, ADC_BUF_LEN) ! HAL_OK) { Error_Handler(); } /* 定时器先停再启动避免相位不确定 */ __HAL_TIM_SET_COUNTER(htim3, 0); HAL_TIM_Base_Start(htim3); } /* DMA 半满前一半数据可读 */ void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { adc_half_ready 1; } } /* DMA 全满后一半数据可读 */ void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { adc_full_ready 1; } }这段代码的逻辑很直白Scope_Start()只负责把 ADC 和定时器拉起来真正的数据搬运由 DMA 完成。半满回调和满回调分别标记前 1024 点和后 1024 点可读主循环看到标志后再做触发查找和显示避免在中断里做重活。参数上ADC_BUF_LEN不建议小于 1024否则触发窗口太短预触发深度不够也不建议一上来开 8192 以上先看 RAM 和刷新节奏再决定要不要加深。定时器周期按触发频率 定时器时钟 / ((PSC 1) * (ARR 1))算。PSC 和 ARR 不要直接抄别人的数字先把芯片主频、定时器挂载总线、ADC 触发来源确认清楚。常见翻车是定时器时钟算错一倍屏幕时基整体偏一半还以为是 ADC 转换时间问题。我的习惯是把采样率打到调试口用示波器或逻辑分析仪量一下触发脉冲确认频率后再继续调触发算法。3. 触发稳定波形环形缓冲、边沿判断和预触发深度没有触发的示波器本质上是一个高速滚动条。你会看到正弦波左右跑调触发旋钮像在碰运气。触发的作用不是“让波形好看”而是给每一帧找一个固定参考点让屏幕上的波形和真实信号相位对齐。这套工程里触发放在软件层做优点是参数好调缺点是会占用一点主循环时间所以缓冲结构和查找范围要设计清楚。3.1 自由采样为什么会让波形左右跑ADC 和定时器一直在跑但屏幕每次取的窗口起点是随机的。假设信号周期是 1 ms你每次从缓冲里取 2048 点起点可能落在波峰也可能落在波谷画出来自然左右跳。触发要解决的问题就是在这一帧数据里找到一个满足条件的边沿把边沿前面的预触发点当作屏幕左边界后面的点依次画过去。边沿判断不能只写“大于阈值就算触发”。真实信号有噪声阈值附近会来回抖结果是触发点一帧在前、一帧在后波形看起来像在抖。常见做法是加迟滞上升沿要求前一个点低于阈值 - 迟滞当前点高于阈值下降沿反过来。迟滞量不用太大12 位 ADC 下先取 16 到 64 个码值再根据噪声调整。3.2 用环形缓冲找触发点的代码写法下面这段代码在已经搬完的一窗数据里找上升沿或下降沿返回触发点在窗口中的起始索引。pre_samples是预触发深度决定触发点左边留多少点。实际工程里可以把返回值映射到屏幕横坐标找不到触发点时走自动触发。#include stdint.h #define TRIG_NONE 0xFFFFFFFFU static int16_t trig_level 2048; /* 12 位 ADC 中间值附近 */ static uint16_t trig_hyst 32; /* 迟滞码值按噪声调 */ static uint8_t trig_edge 1; /* 1 上升沿0 下降沿 */ static uint16_t pre_samples 256; /* 触发点左侧保留点数 */ uint32_t Scope_FindTrigger(const uint16_t *buf, uint32_t len) { if (len (uint32_t)pre_samples 1U) { return TRIG_NONE; } for (uint32_t i pre_samples; i len; i) { int16_t prev (int16_t)buf[i - 1U]; int16_t cur (int16_t)buf[i]; if (trig_edge) { /* 上升沿先低于下门限再穿过上门限 */ if (prev (trig_level - (int16_t)trig_hyst) cur trig_level) { return i - pre_samples; } } else { /* 下降沿先高于上门限再穿过下门限 */ if (prev (trig_level (int16_t)trig_hyst) cur trig_level) { return i - pre_samples; } } } return TRIG_NONE; }trig_level是触发阈值按 ADC 码值给不要直接写成电压否则每次改量程都要重算。trig_hyst是迟滞噪声大就加信号边沿很缓就减但不要减到 0。pre_samples决定触发点左边能看到多少内容调大后能看边沿前的建立过程但会压缩右侧窗口2048 点缓冲下先取 256 到 512 比较稳。返回TRIG_NONE时不要死等自动触发逻辑应该在一段时间后强制刷新一帧否则用户会以为死机。3.3 预触发、迟滞和单次捕获的调参边界预触发深度和采样率要一起看。预触发 512 点、采样率 20 kS/s触发点左边只有 25.6 ms如果信号是 50 Hz 交流这个窗口连一个完整周期都不够。我的习惯是让预触发深度至少覆盖目标信号的两个周期再根据屏幕宽度决定右边留多少点。单次捕获模式则要把自动触发关掉触发到一次就停适合抓上电瞬间或偶发脉冲。自动触发的时间不要设得太短。设 10 ms 时低频信号还没等到边沿就被强制刷新波形会来回跳设 500 ms 又会让用户觉得响应慢。常见做法是给一个和时基相关的超时比如屏幕窗口时长的 2 到 3 倍再留一个手动强制刷新入口。触发模式切换时记得清掉旧标志位否则上一帧的trig_found会污染下一帧表现出来就是“换了触发边沿但波形没变”。4. 码值到屏幕前端偏置、电压换算和刷新节流ADC 读回来的是 0 到 4095 的码值屏幕要的是电压和像素坐标。中间这段换算看起来只是乘系数实际最容易出问题的是前端偏置和校准。很多“波形削顶”“负电压测不到”的现象不是算法错而是前端根本没有把信号抬到 ADC 的输入范围内。软件能补的是比例和偏移补不了运放供电轨和输入保护。4.1 前端调理决定量程软件只能补一小段STM32 的 ADC 输入范围通常在 0 到 VDDA 之间直接测负电压或高于 VDDA 的信号会翻车。常见做法是先用电阻分压把电压降到安全范围再用运放跟随或同相放大做缓冲最后加一个偏置网络把静态工作点抬到 VDDA 的一半左右。这样正负信号都能落在 ADC 范围内软件再减掉偏置电压。分压比不要只按理论算。电阻公差、运放输入偏置电流、ADC 输入漏电流都会让实际比例偏一点。我一般把分压网络阻抗控制在几十千欧以内输入端加限流电阻和钳位保护运放选轨到轨输入输出型。前端带宽也要看分压电阻和输入电容会形成低通电阻取太大时高频幅度会掉屏幕上看就是“正弦波变圆了”这时候加软件滤波只会越滤越糊。前端项常见取值方向影响分压比按最大输入电压留 1.2 倍余量决定量程和 ADC 安全性偏置电压约 VDDA/2决定负电压能否完整落入范围输入阻抗几十千欧以内太高会拖慢采样建立保护串阻加钳位防止过压和负压损坏引脚4.2 ADC码值换算与两点校准换算不要只写raw * 3.3 / 4095。VDDA 实际值、分压比、偏置偏移都有误差直接算出来的电压可能差几十毫伏。更稳的做法是两点校准给两个已知电压记录 ADC 码值算出斜率和截距后面所有码值都走这条直线。校准点要跨过常用量程不要都在同一个电压附近取。typedef struct { float k; float b; } Scope_Cal_t; static Scope_Cal_t g_cal {1.0f, 0.0f}; /* raw0/raw1 是两次校准的 ADC 码值v0/v1 是万用表读到的实际电压 */ void Scope_CalTwoPoint(uint16_t raw0, float v0, uint16_t raw1, float v1) { if (raw0 raw1) { return; } g_cal.k (v1 - v0) / ((float)raw1 - (float)raw0); g_cal.b v0 - g_cal.k * (float)raw0; } float Scope_RawToVolt(uint16_t raw) { return g_cal.k * (float)raw g_cal.b; }Scope_CalTwoPoint()只做一件事把码值和电压拟合成一条直线。k是每码值多少伏b是零码值对应的偏移。两个校准点不要隔得太近否则斜率对噪声很敏感也不要用超过 ADC 输入范围的点否则前端已经饱和校准出来的直线没有意义。校准完把k、b存到 Flash 或备份寄存器里下次上电直接加载别每次开机都让用户重新对。4.3 屏幕绘制与刷新节流的实现画屏不要在 DMA 回调里做。回调频率高画一条线可能几十微秒几行下来主循环就被拖慢ADC 缓冲还会溢出。常见做法是主循环检查adc_half_ready或adc_full_ready找到触发点后先做抽点再把抽点后的数组画成折线。刷新频率控制在 20 到 30 帧每秒人手看起来已经连续CPU 也有时间做按键和测量。#define DRAW_PERIOD_MS 33U #define DRAW_POINTS 256U static uint32_t last_draw_ms 0; /* LCD_DrawLine 按实际屏驱替换这里只表达抽点和节流逻辑 */ void Scope_Draw(uint32_t now_ms, const uint16_t *win, uint32_t start) { if ((now_ms - last_draw_ms) DRAW_PERIOD_MS) { return; } last_draw_ms now_ms; uint16_t x0 0; uint16_t y0 Scope_VoltToY(Scope_RawToVolt(win[start])); for (uint16_t i 1; i DRAW_POINTS; i) { /* 把 2048 点窗口抽成 256 点避免像素级重复绘制 */ uint32_t idx start ((uint32_t)i * 8U); uint16_t x1 (uint16_t)((uint32_t)i * 320U / DRAW_POINTS); uint16_t y1 Scope_VoltToY(Scope_RawToVolt(win[idx])); LCD_DrawLine(x0, y0, x1, y1); x0 x1; y0 y1; } }DRAW_PERIOD_MS是刷新节流33 ms 约等于 30 帧每秒屏幕小、SPI 慢就加到 50 ms不要为了流畅把主循环堵死。DRAW_POINTS是实际画线点数2048 点窗口抽成 256 点既保留包络又减少重复像素。抽点前最好做一点峰值保持或均值否则窄脉冲可能被抽没如果目标就是抓窄脉冲宁可减少显示点数也不要盲目抽点。Scope_VoltToY()里做电压到像素的映射注意屏幕坐标方向别把正电压画到屏幕下方。5. 避坑与排查毛刺、死机、波形撕裂的现场记录这套工程跑通之后最容易出现的不是编译错误而是“看起来能用但测不准”。下面几条是调试现场最常见的现象每条按现象、原因、解决拆开写。遇到问题时先别改算法先确认采样链路和供电很多玄学问题最后都落在模拟前端和中断优先级上。5.1 采样与触发类故障现象波形每帧左右跳触发指示灯偶尔亮偶尔灭。原因触发阈值落在噪声带里或者迟滞太小边沿判断在阈值附近来回翻。另一个常见原因是 ADC 采样时间不够码值本身在抖。解决先把触发源切到直流耦合观察静态码值跳动范围把迟滞设到噪声峰峰值的 1.5 倍左右再检查 ADC 采样时间是否匹配前端阻抗。如果信号本身有高频毛刺前端加 RC 低通别只靠软件触发迟滞硬压。现象高频正弦幅度明显变小波形像阶梯。原因采样率不够或者前端运放带宽、ADC 采样保持建立时间不够。还有一种情况是探头地线太长测高频时引入了振铃看起来像幅度异常。解决先降输入频率验证如果低频正常、高频掉幅优先查前端带宽和采样率。采样率按目标频率的 10 倍以上留余量探头地线尽量短测开关节点时不要用长地线夹。软件上不要用平均滤波掩盖带宽不足平均会把窄脉冲和真实高频一起抹掉。现象DMA 只进一次回调屏幕波形冻结。原因DMA 配成了普通模式搬完一轮就停或者回调里做了耗时操作主循环来不及清标志下一轮覆盖了旧数据。解决把 DMA 改成循环模式回调里只置位标志。主循环处理完一窗数据后再清标志处理期间可以丢帧但不能让标志无限堆积。调试时在主循环里翻转一个空闲 GPIO用逻辑分析仪看翻转周期能快速判断是采样停了还是显示卡了。5.2 显示与供电类故障现象屏幕花屏、波形撕裂按键反应变慢。原因在 DMA 中断或 ADC 回调里直接画屏SPI 刷屏时间超过采样窗口或者屏幕和 ADC 共用总线DMA 搬运时抢总线。解决画屏搬到主循环固定 20 到 30 帧每秒刷新画线前先关局部中断或做好缓冲区切换。SPI 屏用 DMA 刷屏时注意和 ADC DMA 的优先级别让屏幕把采样总线长期占住。屏幕刷新和采样缓冲最好双缓冲前台画、后台采避免画到一半数据被覆盖。现象上电偶发死机复位后又能跑测负电压或较大信号时芯片发热。原因输入电压超过 ADC 引脚绝对最大范围或负压通过分压网络灌进引脚内部保护二极管长时间导通。解决输入端串限流电阻加肖特基钳位到地和 VDDA分压网络确保任何输入条件下引脚电压都在 0 到 VDDA 之间。负压测量必须靠偏置抬升不能指望 ADC 自己扛。上电顺序也要查先给运放供电再给 ADC 引脚信号避免运放未建立时输出异常电压。6. 从“能看”到“能测”校准、FFT和带宽验证的收口动作波形稳定之后下一步不是继续加花哨功能而是做可重复的验证。我一般先用万用表给两个直流点做一次两点校准把k、b写进参数区再用信号发生器给 100 Hz、1 kHz、10 kHz 正弦记录屏幕幅度和频率读数偏差超过 5% 就回头查分压比和采样率。方波用来看看上升沿有没有过冲FFT 用来确认高频分量有没有混叠如果输入 10 kHz、采样率 20 kS/s频谱上出现不该有的低频包络基本就是欠采样别怀疑算法。带宽验证不要只看屏幕上的正弦好不好看要看幅度掉到 0.707 时的频率点同时确认探头和前端分压网络没有先限带。校准数据建议存两份一份出厂默认一份现场校准现场数据异常时能快速回退。从那以后我每次改量程或换前端电阻都强制走一遍直流两点校准加 1 kHz 正弦复核不再凭眼睛判断“差不多”。这套 HAL 工程和调参记录我放在资源包里需要的按项目页取用先跑通采样链路再动前端和触发参数顺序反了很容易把问题搅成一锅粥。希望帮到你。本文还有配套的精品资源点击获取