尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32F103ZET6示波器实战:从ADC采样到DMA与触发定位全解析

STM32F103ZET6示波器实战:从ADC采样到DMA与触发定位全解析 简介基于STM32F103ZET6的简易示波器程序定位为单片机学习者与电子工程师的参考例程。它演示了如何利用芯片内置ADC对正弦波、方波等模拟信号进行实时采样并经SPI/I2C或FSMC接口驱动LCD完成波形绘制覆盖了定时器采样控制、DMA传输、中断与数据处理等关键知识点。压缩包共272个文件、8.9MB主要包含C/.H源码、STM32启动文件(.s)、Keil工程文件(.uvproj)、编译中间文件(.o/.lst)、烧录文件(.hex/.axf)以及说明文档结构完整可直接编译或烧录到开发板运行。已有4559人学习下载。通过研读代码既能理解STM32外设初始化与库函数调用方式也可学习信号采集、缓存处理、LCD刷屏的实现思路在此基础上可进一步扩展多通道采集、频率测量或FFT频谱分析适合课程设计、毕设预研和项目原型验证。 玩过STM32F103ZET6的人都明白示波器程序这个压缩包在网上一抓一大把但十份里有八份是能编译、上板子就是不出波形的状态。我自己也踩过这个坑从最初在裸机上用延时死等ADC采样到后来用定时器触发起泵、DMA搬运、触发定位一条链路调通前前后后折腾了一个多星期。作为过来人我建议刚接触这个项目的朋友别急着双击.rar看keil工程先把整个系统拆开看明白否则你连波形抖动、削顶、混叠这些问题出在哪里都判断不了。这篇文章的内容我会从标题里的STM32F103ZET6示波器这两个关键词展开讲清楚一块Cortex-M3内核的单片机为什么能当示波器用、能到什么性能档次、硬件上哪些细节不能省、软件上哪几个逻辑是核心以及我实测过程里遇到的坑。不管你拿到的压缩包是哪个版本这篇文章都能帮你把它调出稳定可用的波形显示。1. 这个示波器项目到底是什么拆一下标题里的信息量很多人看到STM32F103ZET6示波器会误以为这是个玩具级的东西实际上这个组合是嵌入式学习里相当经典的性能压榨型项目。之所以选ZET6而不是C8T6或者G0系列核心原因是这个芯片的几个硬指标刚好卡在入门示波器的门槛上。1.1 STM32F103ZET6能做到什么程度ZET6的关键资源是72MHz主频、512KB Flash、64KB SRAM、3个12位ADC、2个DAC、5个串口、1个DMA控制器LQFP144封装下有多达112个可用的GPIO。仪器底子主要是ADC部分3个12位ADC理论上可以完成交叉采样让等效采样率冲到数Msps级别。但在实际工程里我不会建议你一开始就上三重ADC交叉采样。交叉采样的相位误差、通道一致性在没有高精度时钟源和前端调理电路的情况下很难控制好。单个ADC配合定时器触发稳定跑出150kHz到1MHz的采样率对于看PWM呼吸灯波形、电源纹波、串口波特率信号、音频信号这类日常调试完全够用。这也是绝大多数ST官方和开源社区的方案采用的路线。另外一个容易被忽略的参数是SRAM。64KB的SRAM意味着你可以开很大的采样缓冲区比如做10KB、20KB的深度采样把一段长波形一次性抓到内存里再慢慢分析。相比之下那些小容量芯片做示波器缓冲区只有几百个点看波形经常只能看到一段一段的画面很难受。1.2 示波器程序的核心模块划分一个完整的STM32示波器固件不管压缩包里代码写得再乱最终跑起来一定是这几个模块在配合工作信号调理如果不外接模拟前端直接用ADC引脚采样那就至少要有分压电阻和钳位保护电路。ADC采样控制定时器产生触发事件ADC在固定的时间点启动转换保证采样时间间隔均匀。数据搬运ADC转换完成由DMA搬运进SRAM缓冲区CPU不参与每次搬运。触发定位在一段采样数据中查找满足电平条件的位置把波形对齐到你预设的触发电平上。数据处理与显示把原始ADC值映射成屏幕坐标画网格、画波形、叠加刻度文字。按键与交互调整电压档位、时间档位、触发电平这些参数必须能实时改。把模块拆开之后你就会发现调试的顺序也有了先保证ADC能连续采到数再验证DMA搬运数据完整接着做触发定位最后才折腾显示。很多人拿到程序一上来就炫屏结果波形根本不稳定问题往往出在前面两步而且是最无聊的配置细节上。2. 硬件要点仪器级信号链路是软件救不了的STM32F103ZET6的内部ADC再好信号从探头进来的时候被弄坏了后面软件再怎么滤波、插值都没有意义。所以在电路层面这是一条信号链路不是你平时焊个最小系统板那么简单。2.1 前端信号调理探头的衰减与偏置最简单的示波器前端至少要考虑两个问题量程和保护。如果你直接用3.3V供电的ADC引脚去测一个12V的电平信号芯片很可能直接烧掉这一点做过的人都懂。正规做法是用精密电阻分压把被测信号等比衰减到ADC可接受的范围。比如测0~24V的直流电平可以用一对100kΩ和10kΩ的电阻分压衰减11倍然后再用运放跟随器做阻抗匹配。给ADC提供一个高输入阻抗、低输出阻抗的信号源这是保证采样精度的一个重要前提我实测下来没有跟随器和高阻分压时ADC读数受采样保持电路的充放电影响误差能超过十几毫伏低频下还能接受高频时波形幅度明显偏低。交流耦合和偏置调整也不能漏。测音频、测纹波这种叠加在直流上的交流信号如果直接把信号接到ADC直流分量会占掉很大一部分量程你关心的纹波细节反而看不到。所以前端需要一个隔直电容把直流分量滤掉再叠加一个可调的直流偏置让交流信号中心处于ADC量程的中部。这个偏置通常用电位器加运放实现软件里再做一个中值校准。另外两颗背靠背的钳位二极管或者齐纳二极管把输入电压限制在0~3.3V以内是保护ADC输入的最基本手段成本几毛钱但能省掉一整块板子。强烈建议无论多简单的版本都加上。2.2 ADC采样电路与参考电压处理STM32F103的ADC是12位逐次逼近型它需要一个稳定的参考电压。板子上如果VREF和VDDA只是简单接到3.3V电源ADC低位的数据跳动会非常明显特别是测小信号时。哪怕不做独立的基准源也应该在VDDA引脚并联0.1μF和1μF以上的滤波电容并且把模拟电源和数字电源用电感或者磁珠分割开。我自己第一次调的时候ADC采集的噪声一直是4到6个LSB跳动后来在VDDA和VSSA引脚上加强滤波、缩短地回路之后跳动降到了1个LSB以内。此外ADC的时钟频率不能乱配。STM32F103的ADC工作在12MHz以上时极限14MHz采样非线性会变差。标准做法是PCLK272MHzADC分频系数选择6让ADC时钟稳定在12MHz。在这个前提下单个通道的最短转换时间可以做到1μs左右也就是大概1Msps。如果你把分频配成4ADC时钟到18MHz采集速度看似快了不少但很多芯片跑在这个配置下线性度明显下降数据曲线在零点附近会出现台阶这个我用可调电源实测验证过。2.3 显示屏幕与按键接口选型显示部分大多数压缩包默认驱动的是3.2英寸或者4.3英寸的TFT-LCD常见控制器有ILI9341、ILI9325、SSD1963等。选择屏幕时除了分辨率更要关注刷新接口。ILI9341用SPI接口时240x320全屏刷新需要几百毫秒画波形还好但如果做菜单、波形缩放体验会非常卡。SSD1963这类支持RGB并口直插的控制器配合FSMC接口刷新速度快一个量级。ZET6刚好有FSMC外设可以像操作SRAM一样操作LCD这是这个芯片做示波器显示的又一大优势。按键至少需要5个上、下、左、右、确认如果愿意再加一个复位。触发电平调节和电压/时间档位切换都靠这组按键。用带ADC按键分压的方案可以省IO口但多个按键同时按下时会互相干扰。更稳的做法是直接接GPIOZET6最不缺的就是引脚。3. 软件逻辑拆解采样、触发、画点三个循环软件是这个项目的灵魂。很多人卡住的点在触发但我得先提醒一句如果采样节奏都没控制好触发做出来也是白做。所以下面按数据流的顺序来讲。3.1 定时器触发采样的节奏控制要让ADC以固定间隔采样最理想的方式是用定时器的TRGO事件作为ADC的触发源。以TIM3为例配置TIM3的主频为某个分频后频率设置自动重装载值ARR来控制周期。例如要得到100kHz的采样率可以这样配置72MHz时钟预分频PSC设为9即10分频得到7.2MHz的计数频率ARR设为71那么触发频率是7200000/72100kHz。代码逻辑如下void ADC_TIM_Init(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); TIM_TimeBaseStructure.TIM_Period 71; // ARR TIM_TimeBaseStructure.TIM_Prescaler 9; // PSC TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); TIM_SelectOutputTrigger(TIM3, TIM_TRGOSource_Update); // TRGO TIM_Cmd(TIM3, ENABLE); // 使能前建议先关闭确认配置完成再开启 }然后配置ADC的外部触发方式为TIM3的TRGO。这里有个细节ADC的外部触发在STM32F103上需要等待一个稳定时间官方库函数里有ADC_ExternalTrigConvCmd(ADC1, ENABLE)这个接口用于启动外部触发模式不少老代码漏了这句导致ADC不转换。这个函数在标准外设库中是控制EXTSEL位的使能开关如果漏掉你会发现定时器跑了但ADC没有任何数据。为什么不推荐直接在ADC中断里读数据然后软件发起下一次转换因为ADC转换完成中断响应有延迟中断里又要处理数值、启动下一次转换时间一长采样间隔就会抖动。示波器最忌讳的就是横轴时间不均匀。定时器硬件触发DMA搬运是唯一能在入门级MCU上做到数据时序稳定的办法。3.2 DMA双缓冲不要让CPU搬数据ADC采集的数据需要送到内存如果ADC中断每次去读CPU会被大量打断导致波形显示刷新变慢。正确做法是DMA循环模式把ADC1-DR地址作为源指定数组作为目标传输长度设为缓冲区的容量比如2048或4096个点。DMA有一个很有用的功能是半传输中断DMA_IT_HT和传输完成中断DMA_IT_TC。利用这两个中断可以实现双缓冲当DMA搬运完前半段数据时CPU可以处理前半段比如触发判定、波形绘制同时DMA还在继续往后半段搬运当后半段完成时CPU再处理后半段。这种乒乓结构能极大提高数据处理的并行度。配置要点DMA_InitTypeDef DMA_InitStructure; DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)SampleBuffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize 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(ADC1_DMA_Channel, DMA_InitStructure);这里有两个坑一是数据宽度必须用HalfWord也就是16位因为ADC是12位的放在寄存器低12位如果用Byte宽度你会得到交替错乱的字节数据完全没法看二是DMA要选择正确的通道ADC1的DMA请求对应的是DMA1_Channel1ADC2对应DMA1_Channel2ADC3对应DMA2_Channel5配错通道看起来程序正常但数据永远不更新。3.3 触发判断从乱跑到锁定的关键波形在屏幕上左右乱飞是示波器项目里最常见的现象。原因是采样数据出来了但没有做触发定位每次开始画的点都不一样。触发的本质很简单在数据缓冲区里找到第一个满足电压跨过触发电平并且方向是设定的边沿的位置从那个位置开始取一屏数据来显示。以上升沿触发为例算法可以这样写int16_t FindTriggerPoint(uint16_t *buf, uint32_t len, uint16_t threshold) { uint32_t i; for (i 1; i len; i) { if (buf[i-1] threshold buf[i] threshold) { return i; // 找到跨过阈值的点 } } return -1; // 未找到 }这个逻辑不难但要实用还需要处理几个细节触发电平需要一个滞回区间否则信号在阈值附近上下抖动时一次扫描可能触发多次画面还是会晃。我常用的做法是只有当前一个点低于threshold - HYSTERESIS且后一个点高于threshold HYSTERESIS才认为触发。HYSTERESIS可以取信号幅度的3%到5%。触发模式要有自动和正常两种。如果没有触发条件比如信号幅度小于触发电平自动模式下就周期性刷新最近的一段数据保证屏幕不黑正常模式则要等触发条件满足才刷新一帧。软件实现时用一个标志位区分就行。触发点的数据寻址是这样的触发点不一定要落在缓冲区开头。如果找到触发点在位置N那么显示起点是buf N - DISPLAY_POINTS / 2让触发点位于屏幕水平方向大约1/3到1/2的位置这是很多示波器的默认习惯。如果N小于DISPLAY_POINTS/2说明触发电平附近可用的数据不够这时候可以等下一次DMA传输完成或者做数据块移位处理。3.4 波形绘制与坐标标定波形绘制我建议采用画线连接而不是打点。打点模式在采样点稀疏、波形斜率大的地方会出现断点看起来像虚线。画线的做法是在相邻两个显示坐标之间做线性插值。LCD如果是320像素宽显示波形区域放在左边320x200的范围右侧留一部分给菜单。每两个采样点之间映射到X轴可能只有零点几个像素到几个像素具体映射要根据当前时间档位计算。电压标度的核心公式是uint16_t y (uint16_t)( (4095L - adc_value) / 4096.0f * DISPLAY_HEIGHT );为什么用4095L - adc_value因为屏幕Y轴一般是上小下大而ADC值越大电压越高如果不反相波形会上下颠倒。初学的人经常看到正弦波相位对不上才意识到这个问题。Y轴标尺显示也要换算已知ADC参考电压是3.3V分压衰减倍数是比如10倍那么一个LSB对应实际电压是3.3/4096*10≈8.06mV。显示每格1V还是每格100mV取决于你当前设置的电压档位。这些标定稀疏乘法计算量不大可以用整数运算避免浮点提高显示速度。4. 实操落地把程序烧进板子的完整步骤这里我假定你手上的压缩包是标准的STM32库函数版工程编译环境是Keil MDK。如果你的压缩包是寄存器版本也没有问题核心步骤是一样的只是底层写入方式不同。4.1 引脚初始化的几个关键位置时钟初始化是第一个大坑。ZET6的ADC挂在APB2总线上你的启动代码里必须显式开启ADC1时钟、DMA1时钟、GPIOA时钟、TIM3时钟如果用TIM3缺一个ADC数据就为零。GPIO配置ADC输入引脚时必须配置为模拟输入模式即GPIO_Mode_AIN。配置成浮空输入或者上拉下拉输入都不对会让ADC读数产生偏差。LCD的FSMC时序要对照屏幕控制器手册查参数。常见错误是地址建立时间和数据建立时间配得太快导致字节错位、花屏。ILI9341这类屏幕配合FSMC时我用过一组比较稳的参数地址建立时间16个HCLK、数据建立时间16个HCLK写操作时序偏慢但是显示绝对稳定。如果你想追求刷新率再逐步把这些参数调小然后观察是否有花屏。4.2 采样率、电压档位与时间轴的对应关系采样率决定了你能看到的最高信号频率。按照奈奎斯特定理理论上采样率至少是信号频率的2倍才不会混叠但实际显示时上升沿、下降沿的细节需要更高的倍数工程上至少5到10倍才能看出波形的形状。比如采样率100kHz时显示10kHz方波就已经比较吃力上升沿会变得很平缓显示1kHz方波就非常清晰。时间档位和采样率之间的关系由每屏点数决定。如果缓冲区有2048个点每屏显示2048点采样率100kHz时一屏对应的实际时间是2048/10000020.48ms也就是屏上X轴左侧到右侧总时长约20ms。你切到200kHz采样率一屏时间就变成10ms左右。按键左/右调整时间档位时其实改的是定时器ARR值和显示点数映射参数两者要联动修改只改一个会导致时间轴显示不对。4.3 性能优化怎么把刷新率提上去一块240x320的LCD如果整屏全部重画就算FSMC并口也可能要30到50ms示波器刷新率就上不去波形会闪。我的优化经验有三个第一个是波形区域采用局部刷新。波形只画在固定的窗口内背景网格和坐标文字不随每次波形变化重刷可以用一块显存区域保存固定的背景刷新时只更新波形像素。这个方法操作量少效果明显。第二个是采用显存偏移的方式画波形。IL9325系列屏幕可以设置窗口坐标然后连续写入像素数据避免每画一个点切换一次RGB地址。STM32库的LCD驱动里如果你看到每次打点都有写地址的IO操作那刷新速度肯定慢。正确做法是设置好窗口后用连续的写数据操作把一帧波形刷完。第三个是双缓冲到SRAM里的帧缓冲。ZET6有64KB SRAM可以用大概320x200x2128KB的1/4大小做双缓冲或者用压缩格式。如果显存不够可以用幽灵缓冲的方式先在内存数组里画好波形再一次DMA刷到LCD。实测下来加了区域刷新和连续写窗口后300x200波形区域刷新能在10ms以内跑完一帧波形已经不太闪了。5. 调试验收拿信号源说话程序写完不是看点灯式地没崩溃就行示波器项目必须用标准信号源验证精度。手头没有函数发生器的话用STM32的DAC生成正弦波或者方波再用杜邦线接到ADC输入也是一个自测方案。5.1 用方波/正弦波校准的步骤先把输入信号接到ADC引脚设为自动触发模式时间档位调到信号的几个周期内。此时你应该能看到波形大致成型。接下来逐项验证检查电压读数用万用表测ADC引脚对地电压和屏上显示的电压值对比。比如DAC输出1.65V屏上显示应该在1.64V到1.66V之间超过这个误差就要检查分压电阻精度和参考电压。检查频率读数用已知频率的信号源比如1kHz正弦波检查屏上每个完整周期占据的横轴格数。一屏20ms显示1kHz的信号应该有20个周期如果只显示10个周期说明采样率比设定值慢了一倍多半是定时器分频配错或者主频不对。检查上升沿用方波信号看上升沿如果看到明显倾斜甚至振铃一是信号源本身的问题二是前端带宽不足三是采样率太低。把采样率提高再对比就能定位原因。5.2 常见故障与排查速查表我整理了这几个月里最常遇到的问题和对应的排查方案你也可能在调这个项目时遇到完全一样的情况。现象可能原因排查方法ADC数据一直为0模拟输入模式未配置ADC未开启检查GPIO_Mode_AIN配置和ADC_Cmd是否启用波形上下颠倒Y坐标映射未反相核对Y坐标计算是否用了4095 - adc_value波形左右乱跑触发定位没做或触发电平超出信号范围把触发阈值设在信号幅度中间再测试波形显示缓慢且闪烁全屏刷新、逐点写屏改局部刷新、窗口连续写、减少菜单刷新电压读数整体偏高/偏低参考电压不准、分压电阻有误差用万用表校准再用电位器校准增益系数高频信号变形采样率不够或ADC时钟超频检查定时器触发频率把ADC时钟配到12MHz波形上有固定毛刺数字地干扰模拟地模拟地和数字地单点连接加强VDDA滤波遇到波形显示不正常不要一上来就怀疑代码逻辑先拿一个已知的稳定信号比如电池电压分压得到的1.5V直流去验证ADC采集数值是否准确。如果直流都测不准波形显示问题大概率是硬件问题。6. 写在最后这个项目还能怎么玩当我第一次在这个板子上看到稳定的1kHz正弦波流下来的时候那种成就感真的无可替代。这个项目最迷人的地方在于它把ADC、DMA、定时器、中断、显示驱动这些零散的知识点全部串成了一条完整的数据流。你只要完整走一遍对STM32整个体系的理解能上一个台阶。调完基础版本之后我建议你往几个方向扩展一下给信号前端加一个真正意义上的程控增益放大用运放和模拟开关实现多档量程自动切换把采样数据通过串口或者USB发给PC在上位机做FFT频谱分析这样就能把STM32示波器扩展成简易频谱仪还可以把触发方式从简单的边沿触发升级成脉冲宽度触发用来抓串口通信的毛刺。这些扩展每一个都不简单但是每一个都能让你对示波器和嵌入式系统有更深的理解。如果你身边有别的同学也在调这个压缩包记得提醒他先把硬件前端处理好再动软件不然很容易出现代码改到头了波形还是坏的的尴尬局面。祝折腾愉快。本文还有配套的精品资源点击获取
返回列表