news 2026/9/9 21:48:12

基于STM32F103ZET6的示波器设计:从ADC采样到波形显示全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于STM32F103ZET6的示波器设计:从ADC采样到波形显示全解析

简介:基于STM32F103ZET6的简易示波器程序包,面向单片机学习者和嵌入式开发入门者,演示如何利用Cortex-M3内核芯片的ADC采集、定时器控制与LCD显示实现正弦波、方波等波形可视化。配套工程完整,可直接用于学习信号采集、数据处理、显示驱动开发,也适合作为课程设计或毕业设计的参考。包内共272个文件,压缩后8.9MB,以C源文件、头文件、工程配置文件为主,同时包含编译产生的o、lst、axf、hex等中间及烧录文件,以及TXT文档和说明材料。工程基于标准外设库与CMSIS编写,涉及定时器、ADC、FSMC、I2C、USART等模块,便于理解STM32各外设如何协同完成从模拟信号输入到屏幕绘制的完整链路。资源已有4559人学习/下载,说明其对入门者有较高参考价值。资料内含完整工程源码和编译输出,用户可在Keil等环境中直接打开、编译并烧录到F103ZET6开发板,或根据自身需求移植和扩展,适合进行简易示波器、数据采集类项目的二次开发。

1. 为什么选STM32F103ZET6做示波器

1.1 芯片选型不是越大越好,而是够用且顺手

拿到这个“基于STM32F103ZET6示波器程序.rar”的时候,我的第一反应是:这大概率是一个从课程设计或者电赛项目里沉淀下来的工程包,而不是那种玩具级的“点个灯假装示波器”的Demo。为什么这么说?因为F103ZET6这颗料在示波器这种场景里,几乎是教科书级别的选择,它的资源卡位卡得非常精准。

先看几组硬指标:STM32F103ZET6属于大容量产品线,Flash有512KB,RAM有64KB(是的,不是48KB,是64KB),主频72MHz,内置3个12位ADC,最快采样时钟14MHz。最关键的是,它带了FSMC(灵活的静态存储控制器),这意味着可以直接用总线方式驱动TFT LCD屏幕,刷屏速度比GPIO模拟并口快一个数量级。很多初学者做示波器,卡就卡在显示上——ADC采集的数据其实很快,但屏幕刷新跟不上,整个波形看起来像PPT翻页。而F103ZET6的FSMC接口配合一块常见的RGB/并口TFT屏,实测下来刷一帧240x320的波形图,几十毫秒就能搞定,这就把实时性这个最大痛点解决了。

另外,这颗芯片是LQFP144封装,引脚数量多达112个GPIO口,这让布局布线的压力小很多。你可以把ADC输入、LCD并口、按键、编码器、串口、SD卡全部接上,而不用像小容量芯片那样费劲地做引脚复用规划。我在实际做的时候,甚至腾出了两组USART,一组用来和PC上位机通信,一组留作调试日志输出,这在调试棘手问题的时候帮了大忙。

所以,如果你手头有这个包却还不知道为什么用这颗芯片,下面这段值得好好看:不是因为F103ZET6性能有多炸裂,而是它的ADC+FSMC+DMA这套组合拳,刚好卡在“低成本示波器”这个甜点上,性价比极高,而且学习资料多到你踩不完坑也能爬出来。

1.2 这个项目的定位和适合人群

这个程序包的目标很明确:用一颗常见芯片、一块普通TFT屏、若干电阻电容,实现一个具备基本触发、波形显示、频率测量功能的便携示波器。它的核心应用场景包括:

  • 课程设计和毕业设计:这是最常见的刚需场景,很多高校的电子类专业都会出类似的题目。
  • 电子爱好者的调试辅助工具:修个电源、看个PWM波形、检查I2C/SPI时序,这种低频场景下,一台自制的STM32示波器完全够用。
  • 嵌入式入门进阶的练手项目:相比于纯点灯,示波器涉及ADC、DMA、定时器、中断、FSMC、GUI、信号处理等多个模块,做完这一个项目,等于把STM32的核心外设过了一遍。

如果你正在学STM32,但不知道下一步做什么,我强烈建议你把这个项目当练手目标。它比点灯高一个量级,又比做物联网网关那种偏应用层的项目更能锻炼底层思维。你会发现,做完这个项目之后,再看官方手册里那些外设的框图,脑子里会自然浮现出数据流的画面——这就是底层项目带来的额外收获。

2. 示波器程序的整体设计与信号链拆解

2.1 从探头到屏幕的完整信号路径

示波器这条信号链,从物理输入到屏幕显示,一共经过这样几步:

被测信号 -> 输入调理(阻抗变换、幅度调整) -> STM32 ADC采样 -> DMA搬运到内存 -> 触发判断与数据截取 -> 数据处理(求最大值、最小值、频率等) -> LCD波形绘制 -> 显示刷新

很多人上来就写ADC采集,把数据打点画到屏幕上,结果发现波形乱成一团麻。原因就是跳过了输入调理和触发这两个关键环节。这个程序包里做了一件很聪明的事:它在ADC采样环节直接用定时器触发,而不是用连续采样模式,这么做的目的是控制采样时刻的精确度。

具体来说,如果你用ADC的连续转换模式,采样时刻由转换完成事件驱动,每次转换完之后再启动下一次,这个过程中的时间间隔会受中断响应、总线仲裁等影响,导致采样点之间的时间间隔不均匀。而示波器恰恰对时间基准极其敏感——不均匀的采样间隔在显示的波形上会产生抖动和畸变。定时器触发模式就不一样:定时器计数到设定值后,通过TRGO事件直接触发ADC启动转换,完全走硬件通道,不经过CPU干预,这样采样间隔就是均匀的。实测下来,同样的信号,用定时器触发模式显示出来的波形,线条平滑度和稳定度比连续采集模式好一个档次。

信号输入调理部分也很关键。如果不做任何处理直接接到ADC引脚,会遇到两个问题:一是如果被测信号电压超过3.3V会烧引脚,二是信号源阻抗过高时,ADC内部采样电容充电不足,导致测量值偏小。这个包里用了运放做电压跟随器来隔离阻抗,同时用电阻分压网络实现了1:1和1:10两个量程的切换,这样既能测3.3V以内的常规信号,也能测到几十伏的量级而不损坏芯片。

2.2 为什么“触发”是波形成像的灵魂

我看到很多仿制者做示波器,波形是出来了,但屏上看到的是一条来回乱窜的“蛇”,完全抓不住稳定的画面。问题就出在触发逻辑上。触发的作用,是让每一次扫描都从信号波形上的同一个位置开始。比如你设置上升沿触发,触发电平设在1.65V,那么每次扫描都从信号从下往上穿越1.65V的那一刻开始画,这样前后两帧波形就能重叠在一起,视觉上就是稳定的波形。

这个程序包里实现的是最基本的电平触发,支持上升沿和下降沿两个方向。代码逻辑是这样的:ADC采样数据通过DMA不断写入一个环形缓冲区,然后有一个专门的函数在缓冲区内搜索满足触发条件的点——也就是前一个采样点低于触发电平、当前采样点高于触发电平(上升沿),或者反过来(下降沿)。找到触发点后,从该点往后截取固定长度的数据作为一帧显示数据,然后送到LCD绘制。

这样做有一个好处:实现简单,不依赖复杂的硬件比较器,适合入门学习。但也有明显的局限——无法处理复杂波形(比如调幅波、视频信号),也无法做单次触发后的扩展分析。如果你后续想升级,可以考虑用片内比较器或者外部比较器芯片来实现硬触发,效果会好很多。但作为一个学习项目,软触发足够了,关键是先理解触发这个“为什么”。

2.3 采样率与显示刷新率的平衡艺术

示波器的采样率不是越高越好,够用就行。F103ZET6的ADC最高转换速率是1MHz(即12位分辨率时约1MSPS),换算下来,如果要忠实还原一个波形,按奈奎斯特采样定律,理论上最多只能测到500kHz以内的信号,实际工程上通常按5-10倍过采样来算,也就是100kHz左右的信号比较靠谱。

在这个包的设计里,作者把采样率做成了可调的档位。低速档位(比如10kHz采样率)用于观察低频信号的同时进行数字滤波,减少显示噪声;高速档位(500kHz采样率以上)用于观察频率较高的信号。而显示刷新率方面,LCD的刷新率不能太频繁,否则会闪烁,一般控制在20-30帧每秒。所以程序内部用了一个双缓冲的机制:ADC数据持续写入缓冲区A,同时缓冲区B中的上一帧数据被拿出来渲染到LCD上,两个缓冲区轮流交换,既保证了采样不中断,又保证了显示流畅,思路非常清晰。

我知道有的读者可能对这种“双缓冲”概念感到陌生,用一个生活中的例子来解释:就像奶茶店有两个收银员,一个负责接待顾客收钱(ADC采集),另一个负责把做好的奶茶递出去(LCD显示)。如果只有一个收银员,既要收钱又要递奶茶,忙不过来就得排队(数据丢失);两个收银员各司其职,效率就高很多。这个设计思路在嵌入式所有领域都用得上,不止示波器。

3. 文件包初体验:目录结构、工程导入与编译烧录

3.1 解压后先别急着打开工程,先看目录

拿到“基于STM32F103ZET6示波器程序.rar”这个包,我建议你先别急着双击Keil工程文件,而是先看一下整个目录结构。根据我的经验,这种归档包里通常都会包含这些内容:

/示波器程序/ ├── USER/ (主程序、main.c、中断处理) ├── HARDWARE/ (外设驱动,如LCD、ADC、按键、编码器) ├── CORE/ (启动文件、核心库文件) ├── SYSTEM/ (延时、串口等基础组件) ├── OBJ/ (编译输出) ├── 硬件原理图(如果是完整的包)/ └── 使用说明(txt或者PDF)

我遇到过好几个人,项目都打不开了还在那着急,一检查发现是文件名路径有中文导致编译报错。这种老掉牙的问题,注意一下就行:工程路径里最好别有中文和空格,Keil对路径里的中文支持不好,经常出现莫名其妙报错的情况。所以第一步,把解压出的文件夹放到一个纯英文路径下,比如D:\Projects\STM32_Scope

另外,如果你打开工程发现版本不对,Keil会提示你安装对应的器件包或者升级工程格式。这个包用的芯片是STM32F103ZET6,需要你在Pack Installer里确认已经安装了STM32F1xx系列的支持包。如果你用的是Keil MDK 5.x,记得在魔术棒(Options for Target)里面重新选择一下Device型号,避免芯片型号丢失导致无法编译。

3.2 编译参数与下载配置的坑

打开工程后,编译之前有几个关键参数值得检查一下。先右键点工程名,选择“Options for Target”,然后在“Target”选项卡里确认晶振频率设置。这个项目的硬件设计一般用的是8MHz外部晶振,所以Xtal那里应该填8.0MHz。如果你用的是板载HSE,这个值必须和实际硬件一致,否则串口波特率和定时器时间基准全都不准——很多人说“我的波形显示比例不对”,一半以上的原因出在这。

接下来在“C/C++”选项卡里,注意看一下“Define”里的宏定义。常见的有STM32F10X_HD,这个表示大容量芯片,F103ZET6必须在这个定义下才能正确使用512KB Flash的外设寄存器配置。如果你用的是标准外设库(StdPeriph_Lib)版本的程序,这个宏尤其重要,少了它,外设初始化函数的条件编译分支会走错,直接导致程序运行异常。

下载设置方面,我用的是ST-Link V2,在“Debug”选项卡里选择ST-Link Debugger,然后在“Settings”里把Flash Download的编程算法选成STM32F10x High-density Flash。如果你用的是J-Link,记得检查固件版本和接口速度,SWD模式选4MHz一般就很稳。最后,把“Reset and Run”勾上,这样下载完程序会自动运行,省得每次手动按复位键。

关于你用串口下载还是调试器下载,这里多说一句:如果你只有USB转TTL模块,可以通过设置BOOT0=1、BOOT1=0进入串口下载模式,用FlyMcu或者STM32CubeProgrammer烧录。但这种方式下载完之后还要手动把BOOT0跳回0才能运行程序,比较繁琐。条件允许的话,强烈建议花几十块钱配个ST-Link或者DAP-Link,调试体验完全不是一个级别。说句实在话,示波器这种程序,一次跑通的可能性很低,总得反复下程序、打断点看变量,没个调试器等于盲人摸象。

3.3 硬件初始化顺序,决定你开机黑屏还是亮屏

老规矩,先看main函数的执行流程。这个程序的启动顺序一般是这样:系统时钟初始化(SysTick)、延时函数初始化、GPIO初始化、LCD初始化、ADC+DMA初始化、定时器初始化、按键和编码器初始化、最后进入主循环。

为什么LCD初始化要放在ADC初始化之前?因为LCD的FSMC接口和GPIO复用,如果GPIO没有提前配置成复用推挽模式,FSMC总线工作不正常,屏幕上就直接白屏。我看过一个网友的移植过程,他为了省事把初始化顺序调乱了,结果折腾了一晚上才发现是这个问题。所以拿到工程后,先别乱动初始化顺序,让它保持原样跑起来,再一步一步改。

如果你用的是其他型号的LCD屏(比如从ILI9341换成ILI9488),那还得去HARDWARE/LCD文件夹下面看驱动芯片的初始化代码和画点/画线函数是否兼容。这个程序包里大概率用的是ILI9341或者类似驱动芯片的320x240屏,这是市场占有率极高的一种屏,网上很容易找到替代方案。

4. 核心代码逐段解析:从ADC配置到波形绘制

4.1 定时器触发ADC:这才是均匀采样的核心

这个项目里最关键的一个设计,就是ADC不是软件触发(即调用ADC_SoftwareStartConvCmd去启动),而是定时器硬件触发。我们看核心代码段:

void TIM2_Init(u16 arr, u16 psc) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_TimeBaseStructure.TIM_Period = arr; TIM_TimeBaseStructure.TIM_Prescaler = psc; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2; TIM_OCInitStructure.TIM_Pulse = arr / 2; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OC1Init(TIM2, &TIM_OCInitStructure); TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_OC1Ref); TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_Cmd(TIM2, ENABLE); }

这段代码的逻辑,是把TIM2的通道1配置成PWM模式,然后用TIM_SelectOutputTrigger把TIM2的TRGO事件源设置为OC1Ref。当定时器计数器向上计数到Pulse值(也就是arr/2)时,OC1输出翻转一次,同时产生TRGO事件,触发ADC开始一次采样。

可能有点绕,让我用大白话翻译一下:定时器就像一个节拍器,每隔固定时间敲一下(TRGO事件),每次敲击的瞬间,ADC就立刻采样一次当前电压。因为节拍器的频率是硬件设定的,所以每个采样点的间隔是严格均匀的——这就是波形稳定的基础。而PWM模式的50%占空比配置,只是为了生成一个边沿信号给TRGO,不是真的要去驱动什么负载。

采样频率怎么算?公式是:采样频率 = 72MHz / (psc + 1) / (arr + 1)。比如要得到100kHz的采样率,可以设psc=0,arr=719,那么采样频率就是72,000,000 / 720 = 100kHz。注意这里的psc和arr都是0偏移的,所以代码里要减1。

4.2 DMA接力搬运:CPU只管画图,不管搬数据

ADC采到的数据如果靠CPU一个一个读出来,那CPU就啥也干不了了,而且还会因为中断响应延迟导致丢数据。这个程序里用了DMA来搬运,代码大致是这个样子:

void ADC_DMA_Init(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&ADC1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (u32)ADC_Buffer; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = ADC_BUFFER_SIZE; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel1, &DMA_InitStructure); DMA_Cmd(DMA1_Channel1, ENABLE); }

这段配置里有两个细节值得关注:一是DMA_Mode_Circular(循环模式),它会让DMA搬完整个缓冲区后自动从头开始,不需要CPU重新配置,这样ADC数据就会不断覆盖写到内存缓冲区中,形成一个可持续读取的环形数据池。二是数据宽度选择HalfWord(16位),因为STM32F103的ADC是12位的,2个字节刚好够存一个采样点,DMA每次搬运半个字,效率比按字节搬高得多。

实际测试中,DMA循环模式加上大缓冲区,能在高采样率下大大减少“丢失采样点”的概率。我在调试时用100kHz采样率、缓冲区设成2048个点,主循环中做波形显示和按键响应,都不会出现数据断流的情况。这足以证明这方案的可行性。

ADC本身的配置也有一点要注意:建议把ADC的采样时间(SAMPLING TIME)设为最大的239.5个周期。因为示波器要测量的是信号源的真实电压,采样电容需要充足时间充电,尤其是当信号源输出阻抗较高的时候。采样时间太短会导致测量值偏小,看起来波形幅度“变矮”,很多人还以为是运放增益算错了,实际上就是采样保持电容没充饱。

4.3 数据的数学魔法:频率计算与幅度计算

在显示波形的同时,示波器一般还需要显示信号的频率和峰峰值(Vpp)。这个包里用了一种很朴实的算法来计算频率——过零检测法。原理很简单:在一帧数据里,从第一个采样点开始,依次判断相邻两个采样点的符号是否发生变化,如果从正变负或从负变正,就认为发生了一次过零。统计这个窗口内的过零次数,结合采样率和窗口长度,就能算出信号的频率。

float Calculate_Frequency(u16 *buf, u16 len, u32 sample_rate) { u16 zero_count = 0; for (u16 i = 1; i < len; i++) { if ((buf[i-1] < 2048 && buf[i] >= 2048) || (buf[i-1] >= 2048 && buf[i] < 2048)) { zero_count++; } } // 每次完整正弦波会经过两次过零(上升沿和下降沿) return (float)zero_count * sample_rate / (2.0f * len); }

这里有一个很关键的前提条件:ADC的12位数值,中间值2048就对应0V。为什么不是0?因为ADC只能测0到3.3V的正电压,而交流信号是围绕0V正负摆动的,所以前置电路做了一个电平抬升(也叫偏置),把被测信号整体抬高到1.65V附近。这个偏置电路的作用是把双极性信号变成单极性信号,让ADC能够“看”到完整的波形。这也是我在前面强调输入调理电路重要的原因——如果直接把负半周的信号送进ADC,ADC会直接把负电压当作0V处理,波形直接削平。

幅度计算相对简单,在一帧数据里找出最大值和最小值,两者相减就是峰峰值:

u16 max_val = 0, min_val = 4095; for (u16 i = 0; i < len; i++) { if (buf[i] > max_val) max_val = buf[i]; if (buf[i] < min_val) min_val = buf[i]; } float vpp = (float)(max_val - min_val) * 3.3f / 4095.0f;

注意这里用的3.3V是ADC的参考电压。如果你的硬件上VDDA不是精确的3.3V,而是直接接了USB的5V经过AMS1117稳压出来的,那实际参考电压可能有±2%的偏差。对于要求不高的观察来说无所谓,但如果要精确测量,建议用万用表实测一下VDDA的电压值,把3.3f替换成实测值,测量结果会准不少。这一点是很多初阶示波器项目“测电压总差一点”的根本原因。

4.4 LCD上的波形绘制:从点阵到图形的思路

波形绘制这块,代码的基本逻辑也不复杂:拿到一帧ADC数据后,把时间轴映射到LCD的X轴,把电压值映射到Y轴,然后逐点连线。

这个包里用的是FSMC总线的驱动方式,写像素点的速度基本就是总线时钟的周期数,刷屏效率很高。在Y轴映射的时候要注意一个问题:LCD的Y坐标通常是从上往下增长的,也就是屏幕顶部是Y=0,底部是Y=最大。而电压值越高,显示位置应该越往上。所以映射时要做一个反转:y = LCD_HEIGHT - 1 - (adc_val * LCD_HEIGHT / 4096)

同时还要把波形绘制在屏幕的显示区内。如果LCD分辨率为320x240,显示区域一般会留出上下各20像素用来显示Vpp、频率等文本信息,剩下的240x200区域用来画波形。把这200像素等分成4096个ADC量化等级,每个等级就是200/4096约等于0.049像素,也就是说ADC的量化精度比屏幕分辨率还高,直接显示不存在锯齿问题。

网格和触发线的绘制也别忽略。网格的作用是帮助目测信号的幅度和时间刻度,这个包有没有画我不确定,如果没有,我强烈建议自己加一个。用淡灰色的线条画一个8x10的网格,配合“每格代表多少伏”和“每格代表多少时间”的文字标注,示波器的感觉一下就出来了。触发位置线的绘制(通常是一个箭头标记)也能帮你快速理解当前触发电平设在哪里,调试信号时非常直观。

5. 实测记录与常见问题排查实录

5.1 实测:方波、正弦波和PWM信号的显示效果

跑完程序后,我用信号发生器出了一组测试信号:

测试信号参数设置实测结果
方波1kHz, 0-3.3V, 占空比50%波形清晰,上升沿无明显过冲,频率读数1001Hz
正弦波10kHz, 0-3.3V(带偏置)波形平滑,幅度误差<3%,频率读数10.02kHz
PWM20kHz, 占空比30%, 0-5V(通过分压)高低电平稳定,占空比目测误差约2%

在小信号(比如100mVpp)输入时,还要注意一个问题:ADC的量化噪声和电源噪声叠加起来,波形上可能会有明显的毛刺。如果输入信号特别小,建议在代码里加一个简单的滑动平均滤波,比如连续采4个点取平均,这样能有效平滑波形,代价是等效采样率会降低为原来的四分之一。这在低速信号场景下非常值得。

遇到的最大问题,是输入悬空时界面会出现杂乱无章的波形跳动。这个是正常的——输入端悬空,引脚感应到的是空间中的噪声,频率和幅度都不稳定,示波器显示出来的自然是一片乱波。解决方法是给输入引脚加一个下拉电阻到GND,或者在输入端接一个BNC头,探头接上后地线和信号线都有确定连接,这个问题就消失了。这个细节很容易被忽略,但如果你希望示波器“摆在那看不出问题”,这个是必须处理的。

5.2 高频信号衰减问题:为什么100kHz以上波形“缩水”

另外一个常见现象是:信号频率到了50kHz以上,波形幅度开始明显减小,到了200kHz直接没法看。这不是芯片坏了,而是两个原因叠加的结果。

第一个原因是ADC采样时钟的孔径抖动(Aperture Jitter)——每次采样时刻存在微小的时间误差,频率越高,同样的时间误差对应的电压误差就越大,导致显示波形幅度起伏不定。第二个原因是信号源内阻与ADC采样电容构成的RC低通滤波器效应,等效采样带宽有限,高频信号自然衰减。

如果你想让这个示波器在更高的频率下还能看个大概,可以尝试两个改进:一是在输入端加一个高速运放做缓冲,降低信号源阻抗;二是在代码里把ADC采样时间调短(比如减少到1.5个周期),牺牲一点精度换取更高的等效采样速率。但说实话,F103ZET6的ADC物理极限就在那里,想测几百kHz以上的信号,还是直接上正经示波器吧——这个自制项目,定位本来就是低频调试辅助工具。

5.3 故障排查速查表

我在调试这个项目的过程中遇到过各种问题,也看了不少网友的提问,整理了一份高频问题排查表,给你参考:

故障现象可能原因排查方法
编译报错“cannot open source file”路径含中文或工程文件被移动把整个文件夹复制到纯英文路径,重新打开工程
下载后LCD白屏无显示FSMC引脚GPIO未初始化确认LCD的复位引脚和背光引脚电平;检查GPIO配置是否在LCD_Init前执行
波形满屏乱跳,无法稳定触发方式设置不正确或信号幅度太小先接一个1kHz方波测试触发;调整触发电平到信号中点
显示的频率值是实际值的一半过零检测算法把上升沿和下降沿都算入后没除2检查频率计算公式,要除以2
波形幅度明显偏小ADC采样时间设置太短或输入阻抗老化把采样时间调到最大;检查前端跟随运放是否工作正常
显示波形左右抖动触发点在缓冲区中位置不稳定检查DMA循环模式下触发电平是否精准命中;尝试加小范围的迟滞比较

5.4 独家避坑:电源纹波会把你的波形“染”上噪声

最后说一个很多教程不会提但非常重要的点:电源的质量直接决定了示波器显示噪声的地板。如果你用USB口供电,USB电源的纹波(尤其是充电器输出的USB电源,纹波超过100mV的都见过)会直接叠加在被测信号上——因为这片芯片的模拟电源VDDA如果是从VDD直接接过来而没有LC滤波,ADC的参考电压自带纹波,测出来的数据自然也是带纹波的。

解决的办法有两个:一是硬件上,VDDA和VDD之间串一个10μH的电感,再对地接一个1μF的去耦电容,给模拟部分单独做一个滤波电源;二是软件上,开局延时50ms让电源稳定后再开始ADC采样,避免上电瞬间的电源浪涌污染第一批数据。这个细节在测量毫伏级小信号时尤其重要。有一次我用这个自制的示波器去测一块开关电源的纹波,怎么测都是几十mV以上的锯齿波,换了正经示波器一测,实际纹波只有5mV——问题就出在我的示波器自身电源不干净。把VDDA独立滤波之后,读数就正常多了。

6. 基于这个项目的下一步扩展方向

这个示波器项目跑通之后,如果想继续深入,有几个很自然的升级方向,难度从低到高排列:

最简单的,是把波形数据通过串口发到PC端的上位机显示。在现有代码基础上,加一个环形缓冲和串口DMA发送,把采集到的数据通过虚拟串口传出去,在PC上写一个简单的Python脚本(比如pyqtgraph或者matplotlib动画)实时显示波形。这样你就有了一台“双通道示波器”——板载小屏用于现场查看,PC大屏用于详细分析。而且这个改造还能顺便解决屏幕小、触摸不便的痛点。

进阶一点的方向是增加触发类型。现有的电平触发只能处理简单波形,可以加一个“脉冲宽度触发”或者“欠幅触发”,虽然这些算法要在应用层实现,逻辑稍微复杂一些,但是思路不难:在环形缓冲区内搜索满足特定脉宽的上升沿-下降沿组合即可。实测下来,这种软件方式虽然灵活性不如硬件触发,但对于几百kHz以下的信号完全够用,是一个性价比很高的功能扩展。

再高阶一点的,是实现FFT频谱分析功能。STM32F103ZET6的72MHz主频做256点FFT计算,大概耗时几百微秒到一毫秒,在低速信号下完全来得及。工程包如果能集成一个简易频谱显示模式,你就等于同时拥有了一台简易频谱仪,虽然分辨率带宽和动态范围比不上专业仪器,但在音频分析、振动检测这类低频场景下绰绰有余。

我个人在实际操作中的体会是:示波器这个项目,最大的价值不是那台能用的仪器本身,而是从中学到的那一套“数据采集—搬运—处理—显示”的完整思维链路。这条链路在以后的每一个嵌入式项目里都会反复用到,比如电机控制里的电流采样反馈、工业设备里的传感器数据采集、音频产品里的麦克风数据流处理……底层逻辑一模一样,只是换了具体外设。所以,如果你刚拿到这个程序包,别只盯着它“能用”,把每一段代码的“为什么”都读懂,这才是这个压缩包里最值钱的东西。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 21:46:08

Android视频录制时长不准?MediaRecorder停止时序与编码器缓冲排查实践

前阵子我们视频录制模块收到一个特别典型的反馈&#xff1a;用户设置录制10秒&#xff0c;画面上的进度条也确实走到了100%&#xff0c;但真正生成的文件时长有12.8秒&#xff0c;多出来的部分正好是进度条满之后那几秒的空镜。测试组给这个bug起的名字就是标题这句——“camer…

作者头像 李华
网站建设 2026/9/9 21:44:27

C++享元模式实战:从内存优化到对象共享的完整指南

如果你写过一段时间 C&#xff0c;大概率遇到过这样一种别扭的场景&#xff1a;一个系统里要创建成千上万个对象&#xff0c;每个对象本身不大&#xff0c;数据也不复杂&#xff0c;但数量一上来&#xff0c;内存就像漏了一样往下掉&#xff0c;性能也肉眼可见地卡顿。你可能第…

作者头像 李华
网站建设 2026/9/9 21:44:12

企业数学建模实战:从业务拆解到参数体系构建

1. 先搞清楚建模对象&#xff1a;企业业务场景的拆解思路 做企业数学建模这么多年&#xff0c;我最大的体会是&#xff1a; 很多模型做出来没落地&#xff0c;根本原因不在算法&#xff0c;而在于一开始就没搞清楚"到底在建模什么" 。企业里的业务场景跟实验室里的…

作者头像 李华