资讯动态

STM32F103ZET6示波器程序:ADC+DMA+TFT全流程实战

发布时间:2026/10/16 10:04:09 来源:尧图企业网站定制
简介这份资源是基于STM32F103ZET6的简易示波器程序压缩包面向学习嵌入式开发、单片机编程及信号采集的初学者与进阶开发者。项目利用该芯片72MHz主频、多通道ADC与丰富外设实现正弦波、方波等模拟信号的采集、处理与LCD波形显示是理解微控制器工作原理与ADC、定时器、SPI/I²C接口协同应用的典型实践。压缩包共272个文件约8.9MB以C源码与头文件为核心辅以工程配置、编译中间文件、说明文档及烧录固件涵盖从外设初始化到波形绘制的完整固件流程并附有Keil工程文件便于直接编译烧录。目前已有4570人学习下载适合作为嵌入式课程设计、电子竞赛或自学项目的参考范例帮助读者掌握信号采集、采样率控制与屏幕显示等关键环节快速搭建可运行的示波器原型。1. 从一块闲置的 F103 开发板说起这个示波器程序到底能跑出什么手头有块 STM32F103ZET6 的开发板屏幕、按键、ADC 都焊好了却一直吃灰——这大概是很多单片机玩家的真实写照。这份「基于 STM32F103ZET6 示波器程序」就是冲着这个场景来的它把一块通用开发板变成一台能看波形的简易示波器采样、触发、显示、按键调节全在片内跑完不依赖上位机。适合谁正在学 STM32 外设、想找一个「能把 ADC 定时器 DMA TFT 屏串起来」的完整练手项目的人以及需要一台低成本现场看波形的嵌入式开发者。它不追求台式示波器的带宽和精度胜在代码全、结构清晰、能直接烧进板子看效果拿来改采样率、改触发方式、改界面都很顺手。2. 技术选型与整体架构为什么是 F103 ADC DMA TFT 这套组合2.1 主控与外设的搭配逻辑STM32F103ZET6 属于 F1 系列里的高配型号144 引脚、512KB Flash、64KB SRAM最关键的是它带 3 个 12 位 ADC、多个通用定时器和 FSMC 总线。示波器这个场景对资源的需求其实很明确ADC 负责把模拟信号数字化定时器负责给出稳定的采样节拍DMA 负责把 ADC 结果搬进内存而不占用 CPUTFT 屏负责把波形画出来。F103ZET6 的 SRAM 有 64KB足够开一块几千点的采样缓冲区FSMC 又能直接驱动并口 TFT刷屏速度比软件模拟 SPI 快一个量级。这套组合不是随便凑的是「采样要稳、搬运要快、显示要跟得上」三个约束倒推出来的结果。常见做法是 ADC1 配 TIM2 触发DMA1 通道 1 搬运TFT 走 FSMC 的 NE 片选。为什么不用 ADC 连续转换模式因为连续模式下采样率由 ADC 自身时钟决定不好精确控制而定时器触发能给出确定的采样间隔这对示波器的时间轴刻度至关重要。DMA 用循环模式缓冲区填满一半或全部时产生中断主循环在中断里处理数据、更新显示CPU 大部分时间其实在干别的事。2.2 采样、触发与显示的数据流整个程序的数据流可以拆成三段采集段、处理段、显示段。采集段由 TIM2 的更新事件触发 ADCADC 每次转换完由 DMA 搬到adc_buffer[]处理段在 DMA 传输完成中断里跑做触发判断和波形对齐显示段在主循环里把对齐后的数据映射成屏幕坐标用 TFT 画点连线。触发是示波器的灵魂。这份程序里常见的是上升沿触发在缓冲区里从前往后找第一个「前一点低于触发电平、当前点高于触发电平」的位置把这个位置当作波形起点这样每次刷屏波形都从同一个相位开始看起来才稳定。如果不做触发对齐波形会在屏幕上左右乱跑这就是很多人第一次做示波器时遇到的「波形抖动」问题。// 触发查找在采样缓冲区中定位上升沿起点 // buf: 采样数据, len: 数据长度, level: 触发电平(ADC 码值) int find_trigger_index(uint16_t *buf, uint16_t len, uint16_t level) { for (int i 1; i len; i) { // 前一点低于电平当前点高于电平 上升沿 if (buf[i-1] level buf[i] level) { return i; // 返回触发点下标作为波形绘制起点 } } return 0; // 没找到触发点就从头画避免死等 }这段代码里level是触发电平对应的 ADC 码值12 位 ADC 满量程 4095如果参考电压 3.3V想触发在 1.65V 就传 2048。find_trigger_index返回的下标会作为绘制起点后面的数据循环回绕到缓冲区头部继续取形成连续波形。注意这里没做迟滞信号噪声大时可能误触发二次开发时可以加一个回差判断。2.3 目录结构与模块划分拿到源码包解压后典型的目录会分成这几块CMSIS和STM32F10x_StdPeriph_Driver是库文件User下放main.c、adc.c、timer.c、dma.c、tft.c、key.cProject下是 Keil 工程文件。核心逻辑集中在User目录库文件基本不用动。目录/文件作用User/main.c主循环、初始化调用、显示刷新调度User/adc.cADC 初始化、通道配置、校准User/timer.cTIM2 采样定时器配置User/dma.cDMA 通道配置与中断服务函数User/tft.cTFT 底层驱动、画点画线、坐标映射User/key.c按键扫描、时基/幅度档位切换这种划分的好处是每个外设一个文件改采样率只动timer.c改显示只动tft.c互不干扰。新手最容易犯的错是把所有初始化堆在main.c里几百行下来自己都找不到哪句配了哪个寄存器。3. 跑起来从解压到屏幕上出现第一条波形3.1 环境准备与工程打开这份程序是标准库工程不是 HAL 库所以需要 Keil MDK建议 5.x 版本加上 STM32F1 的器件支持包。解压后找到Project目录下的.uvprojx文件双击打开如果提示找不到器件去 Pack Installer 里装一下 F1 系列的 DFP。编译前确认两件事一是Options for Target里的晶振频率设成 8MHz外部晶振二是调试器选对常见的是 ST-Link 或 J-Link。注意标准库工程对 Keil 版本比较敏感用太新的 AC6 编译器可能报一堆警告建议切回 AC5或者把警告等级调低。编译通过后直接下载。如果板子上电后屏幕白屏或花屏先别怀疑程序八成是 TFT 的驱动芯片型号对不上——常见的有 ILI9341、ILI9325、ST7789程序里tft.c开头一般有个宏定义选型号改成你板子上的那颗再编译。3.2 关键初始化代码逐段看初始化的顺序有讲究先时钟再 GPIO再外设最后中断。顺序错了会出现「配置写了但不生效」的玄学问题。下面这段是典型的初始化骨架。int main(void) { // 1. 系统时钟配置到 72MHz SystemInit(); // 2. 延时函数初始化基于 SysTick delay_init(); // 3. 中断优先级分组F1 常用 2 位抢占 2 位响应 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 4. TFT 屏初始化内部会配 FSMC 和 GPIO TFT_Init(); // 5. 按键 GPIO 初始化 KEY_Init(); // 6. ADC DMA TIM 采样链路初始化 ADC_DMA_Init(); TIM2_Sample_Init(); // 7. 开定时器采样开始 TIM_Cmd(TIM2, ENABLE); while (1) { // 主循环检测按键、刷新波形、更新参数显示 KEY_Scan(); Wave_Display(); } }SystemInit()在标准库里默认把时钟拉到 72MHz但前提是外部晶振真的在起振。如果板子用的是内部 RC得改system_stm32f10x.c里的宏。NVIC_PriorityGroupConfig这行很多人忽略结果 DMA 中断和按键中断优先级打架波形刷新一顿一顿的。ADC_DMA_Init()里会把 ADC 校准也做掉校准不做的话采样值会偏几十个码值看直流电平尤其明显。3.3 采样链路的配置细节采样链路是 TIM2 触发 ADC、ADC 触发 DMA三个外设要串起来任何一环没配对都采不到数。TIM2 的配置决定了采样率公式是采样率 72MHz / ((Prescaler1) × (Period1))。比如预分频 0、周期 719就是 72MHz / 720 100kHz 采样率。void TIM2_Sample_Init(void) { TIM_TimeBaseInitTypeDef TIM_InitStruct; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // 预分频 0计数周期 719 100kHz 更新频率 TIM_InitStruct.TIM_Prescaler 0; TIM_InitStruct.TIM_Period 719; TIM_InitStruct.TIM_CounterMode TIM_CounterMode_Up; TIM_InitStruct.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM2, TIM_InitStruct); // 更新事件作为 ADC 的外部触发源 TIM_SelectOutputTrigger(TIM2, TIM_TRGO_Update); // 先不启动等 ADC 配好再开 TIM_Cmd(TIM2, DISABLE); }TIM_SelectOutputTrigger这行是关键它把 TIM2 的更新事件输出成 TRGO 信号ADC 配成外部触发时就能收到这个节拍。改采样率就改TIM_Period值越小采样越快但太快了 DMA 搬运和显示会跟不上缓冲区会溢出。经验值是 F103 上跑到 200kHz 左右还比较稳再高就要考虑降低显示刷新频率或者用双缓冲。ADC 那边要配成外部触发、扫描模式关、连续转换关DMA 配成循环模式、半字宽度、内存地址自增。这几个参数错一个要么采不到数要么数据错位。配完之后调ADC_ExternalTrigConvCmd(ADC1, ENABLE)和ADC_SoftwareStartConvCmd让 ADC 进入等待触发状态最后TIM_Cmd(TIM2, ENABLE)一开数据就开始往缓冲区里灌了。4. 避坑与排查波形不对时先查这几处4.1 屏幕白屏或花屏现象是上电后屏幕全白、全黑或者彩色条纹。原因通常是 TFT 驱动芯片型号不匹配或者 FSMC 的时序参数太激进。解决方法是打开tft.c找到型号宏改成实际芯片如果型号对但还是花屏把 FSMC 的FSMC_DataSetupTime从 2 调到 5 甚至 10给并口留足建立时间。杜邦线接的板子尤其容易因为线太长导致时序问题。4.2 波形左右乱跑不锁定现象是能看到波形但一直在屏幕上横向漂移。原因是没做触发对齐或者触发电平设在了信号范围之外。先确认find_trigger_index里的level落在信号的最大最小值之间再看触发查找是不是每次都返回 0——如果信号幅度太小够不到电平就会一直返回 0波形自然不稳。把触发电平调到信号中间值或者加个自动电平跟踪。4.3 采样值全是 0 或全是 4095现象是画出来的波形是一条直线贴在底部或顶部。全是 0 一般是 ADC 通道选错了或者 GPIO 没配成模拟输入。F103 的 ADC 引脚必须配成GPIO_Mode_AIN配成浮空输入读出来就是乱的。全是 4095 通常是输入电压超过了参考电压检查一下信号有没有加偏置示波器前端一般要有个分压和偏置电路直接把负电压或超过 3.3V 的信号怼进引脚会烧 ADC。4.4 波形刷新卡顿、一顿一顿现象是波形能显示但刷新不流畅按键反应也慢。原因是显示函数在中断里跑或者 DMA 中断太频繁。常见做法是把Wave_Display()放在主循环DMA 中断里只置个标志位主循环看到标志再刷。另外画线别一个点一个点地写用 TFT 的连续写或者开个行缓冲能快不少。4.5 编译报错找不到头文件现象是打开工程一编译就一堆cannot open source input file。原因是工程里的头文件路径是作者机器上的绝对路径换台电脑就失效。解决方法是进Options for Target→C/C→Include Paths把路径重新指到你解压后的User、CMSIS、StdPeriph_Driver/inc这几个目录。这是换机器必踩的坑跟代码本身没关系。5. 二次开发把采样率、触发方式和界面改成自己的5.1 改采样率与时间轴刻度采样率由 TIM2 的TIM_Period决定但改了采样率之后屏幕上的时间轴刻度也得跟着改否则显示的「每格多少微秒」就是错的。时间轴换算公式是每点时间 1 / 采样率每格时间 每点时间 × 每格点数。比如采样率 100kHz每点 10μs屏幕横向 240 像素画 240 个点那一屏就是 2.4ms。想看得更细就把采样率提上去想看得更宽就降采样率或者做抽取。// 根据采样率计算并显示时间轴刻度 // sample_rate: 当前采样率(Hz), points_per_div: 每格点数 void update_time_scale(uint32_t sample_rate, uint16_t points_per_div) { // 每点时间(纳秒) 1e9 / 采样率 uint32_t ns_per_point 1000000000UL / sample_rate; uint32_t ns_per_div ns_per_point * points_per_div; // 自动选单位显示ns / us / ms if (ns_per_div 1000) { TFT_ShowString(0, 0, T:%dns/div, ns_per_div); } else if (ns_per_div 1000000) { TFT_ShowString(0, 0, T:%dus/div, ns_per_div / 1000); } else { TFT_ShowString(0, 0, T:%dms/div, ns_per_div / 1000000); } }这段代码把采样率换算成人能读的时间刻度points_per_div是屏幕上横向一格画多少个采样点一般取 30 或 40。改采样率时记得同步调这个函数不然刻度显示会骗人。5.2 加一个自动量程手动调幅度档位很烦加个自动量程能省不少事。思路是在一屏数据里找最大值和最小值算出峰峰值然后选一个最接近的档位。档位本质上是前端分压比或者软件里的放大系数软件实现就是乘一个系数把波形缩放到屏幕高度内。// 自动量程根据峰峰值选择最合适的显示增益 // buf: 采样数据, len: 长度, screen_h: 屏幕可用高度 uint16_t auto_range(uint16_t *buf, uint16_t len, uint16_t screen_h) { uint16_t vmax 0, vmin 4095; for (int i 0; i len; i) { if (buf[i] vmax) vmax buf[i]; if (buf[i] vmin) vmin buf[i]; } uint16_t vpp vmax - vmin; if (vpp 0) return 1; // 直流信号给个默认增益 // 增益 屏幕高度 / 峰峰值留 10% 余量 return (uint16_t)(screen_h * 10 / (vpp * 11 / 10)); }auto_range返回的增益拿去乘每个采样点再映射到屏幕坐标。注意别让增益太大导致波形顶到屏幕外所以留了 10% 余量。这个函数放在每屏刷新前跑一次就能实现「波形自动缩放到合适大小」。5.3 触发方式扩展与验证方法现在只有上升沿触发可以加下降沿、双沿、甚至单次触发。下降沿就是把判断条件反过来前一点高于电平、当前点低于电平。单次触发是找到触发点后停掉 TIM2适合抓瞬态信号。改完触发逻辑后怎么验证我的习惯是拿信号发生器给一个 1kHz 方波先把触发电平调到 1.65V看波形是不是稳定不动再把电平调到 0V 和 3.3V波形应该开始漂移说明触发判断生效了。然后用示波器或者另一块板子对比一下频率读数误差在几个百分点内就算对。从那以后我每次改采样相关的代码都强制走一遍「方波验证 电平边界测试」不然很容易在触发判断上翻车。希望这份拆解能帮你把这块吃灰的 F103 板子用起来跑通之后你会发现示波器这个项目把 STM32 的 ADC、DMA、定时器、FSMC 全串了一遍是块很好的练兵场。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑