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

📅 发布时间:2026/9/9 21:48:19
基于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有512KBRAM有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性能有多炸裂而是它的ADCFSMCDMA这套组合拳刚好卡在“低成本示波器”这个甜点上性价比极高而且学习资料多到你踩不完坑也能爬出来。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模块可以通过设置BOOT01、BOOT10进入串口下载模式用FlyMcu或者STM32CubeProgrammer烧录。但这种方式下载完之后还要手动把BOOT0跳回0才能运行程序比较繁琐。条件允许的话强烈建议花几十块钱配个ST-Link或者DAP-Link调试体验完全不是一个级别。说句实在话示波器这种程序一次跑通的可能性很低总得反复下程序、打断点看变量没个调试器等于盲人摸象。3.3 硬件初始化顺序决定你开机黑屏还是亮屏老规矩先看main函数的执行流程。这个程序的启动顺序一般是这样系统时钟初始化SysTick、延时函数初始化、GPIO初始化、LCD初始化、ADCDMA初始化、定时器初始化、按键和编码器初始化、最后进入主循环。为什么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的采样率可以设psc0arr719那么采样频率就是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数据就会不断覆盖写到内存缓冲区中形成一个可持续读取的环形数据池。二是数据宽度选择HalfWord16位因为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能够“看”到完整的波形。这也是我在前面强调输入调理电路重要的原因——如果直接把负半周的信号送进ADCADC会直接把负电压当作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坐标通常是从上往下增长的也就是屏幕顶部是Y0底部是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.02kHzPWM20kHz, 占空比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计算大概耗时几百微秒到一毫秒在低速信号下完全来得及。工程包如果能集成一个简易频谱显示模式你就等于同时拥有了一台简易频谱仪虽然分辨率带宽和动态范围比不上专业仪器但在音频分析、振动检测这类低频场景下绰绰有余。我个人在实际操作中的体会是示波器这个项目最大的价值不是那台能用的仪器本身而是从中学到的那一套“数据采集—搬运—处理—显示”的完整思维链路。这条链路在以后的每一个嵌入式项目里都会反复用到比如电机控制里的电流采样反馈、工业设备里的传感器数据采集、音频产品里的麦克风数据流处理……底层逻辑一模一样只是换了具体外设。所以如果你刚拿到这个程序包别只盯着它“能用”把每一段代码的“为什么”都读懂这才是这个压缩包里最值钱的东西。本文还有配套的精品资源点击获取