news 2026/11/4 13:23:16

STM32F103+标准库打造简易示波器:定时器触发ADC与TFT实时波形显示

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F103+标准库打造简易示波器:定时器触发ADC与TFT实时波形显示

简介:这份资源是面向嵌入式初学者与电子竞赛备赛者的STM32F103C8T6简易示波器实战Demo,围绕ADC采样、定时器触发、串口通信与波形显示等核心环节展开,帮助读者理解如何用Cortex-M3内核微控制器完成模拟信号的采集与可视化,适合已掌握基础GPIO与中断、希望进阶到综合项目的中级学习者。压缩包共298个文件,约8.56MB,以c与h源码、Keil工程文件(uvprojx、uvoptx)、编译中间产物(o、d、crf)及hex烧录文件为主,另含少量sct分散加载、lst列表与bat批处理脚本,完整保留了从源码到可执行固件的工程结构。目前已有2916人学习下载。资源涵盖ADC多通道配置、定时器触发采样、UART数据传输、主机端滤波与峰值检测等知识点,并附硬件电路与调试思路,读者可据此复现采样链路、修改采样率与显示逻辑,是理解示波器数据通路的实用参考。

1. 一块十几块钱的蓝板子,怎么就成了桌面上的简易示波器

手头只有一块 STM32F103C8T6 最小系统板,想测个 PWM 占空比、看个传感器输出的毛刺,又不想搬出几千块的台式示波器——这个 demo 就是为这种场景准备的。它把 ADC 采样、定时器触发、TFT 屏刷新和按键交互串成一条完整链路,跑起来之后屏幕上能实时画出电压波形,带网格、带量程标注,还能按键切换时基和触发方式。适合谁?适合刚学完 GPIO 和 ADC、想找一个“能看见结果”的练手项目的嵌入式新手,也适合需要给教学演示或产线快速排查做一个小工具的开发者。整份工程基于标准外设库组织,Keil 工程直接打开就能编译,硬件接线不复杂,核心代码量不大,但把“采样—缓存—绘制”这条数据通路讲得很清楚,拿来改造成自己的数据采集面板也顺手。

2. 技术选型与整体架构:为什么是 F103 + 标准库 + 并口屏

2.1 选型逻辑:够用、好买、资料多

这个 demo 的定位决定了选型不会往高性能走。STM32F103C8T6 是 72MHz 的 Cortex-M3,64KB Flash、20KB SRAM,内置 12 位 ADC,采样率在规则通道单次转换下能到 1Msps 量级,做音频以下频段的波形观察完全够用。选标准外设库而不是 HAL,原因很实际:标准库的 ADC 和 TIM 寄存器操作路径短,中断里干活的代码更可控,对于示波器这种对时序敏感的场景,少一层抽象就少一分不确定性。显示部分用 8 位并口的 ILI9341 类 TFT 模块,刷屏速度快,320×240 的分辨率画 200 多个采样点不挤。

常见做法是把采样和绘制放在同一个主循环里轮询,但这个 demo 里我建议(工程也是这么组织的)用定时器触发 ADC,采样节奏由硬件保证,主循环只负责从缓冲区取数据画图。这样即使刷屏偶尔慢一拍,采样间隔也不会被软件抖动带偏,波形不会出现“玄学”式的横向拉伸。

2.2 模块划分与数据流

整个工程按职责切成四块,彼此通过一个全局的采样缓冲区解耦:

模块文件职责
采样触发tim.c / adc.cTIM 定时触发 ADC 规则转换,转换完成进中断存值
数据缓存scope.c环形缓冲区,保存最近 N 个采样点,供绘制读取
显示驱动lcd.c / ili9341.c并口初始化、画点、画线、画网格、刷字符
交互控制key.c / main.c按键扫描,切换时基、触发边沿、暂停/继续

数据流是单向的:TIM 溢出 → 启动 ADC → 转换完成中断 → 写入环形缓冲区 → 主循环判断缓冲区有新数据 → 按当前时基换算横坐标 → 画线。这个结构的好处是采样频率和刷新频率互不绑定,改时基只需要改 TIM 的预分频和重装载值,绘制侧不用动。

2.3 时钟与引脚分配

系统时钟走 HSE 8MHz 经 PLL 倍频到 72MHz,ADC 时钟由 PCLK2 六分频得到 12MHz,落在 14MHz 上限以内。引脚分配上,ADC 用 PA0(通道 0)作为信号输入,TFT 的数据口用 PB0~PB7,控制线 RS、WR、RD、CS、RST 分别落在 PA1~PA5,按键用 PA6、PA7 和 PB12 三个普通输入。这套分配避开了 JTAG 的 PA13~PA15 和 PB3~PB4,烧录调试不会打架。

提示:如果你手上的屏是 SPI 接口而不是并口,驱动层要换,但采样和缓存这两层完全不用动,这也是分层的价值。

3. 核心代码拆解:从定时器触发到屏幕画线

3.1 定时器触发 ADC 的配置

采样节奏的源头在 TIM2。下面这段是初始化代码,重点是预分频和重装载值的算法——它们直接决定采样率:

// tim.c:TIM2 作为 ADC 的外部触发源,更新事件触发规则转换 void TIM2_Scope_Init(uint32_t sample_rate) { TIM_TimeBaseInitTypeDef tim; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // 72MHz / (prescaler+1) / (period+1) = sample_rate // 先固定预分频为 71,得到 1MHz 计数时钟,再算重装载值 tim.TIM_Prescaler = 71; tim.TIM_Period = (1000000 / sample_rate) - 1; tim.TIM_CounterMode = TIM_CounterMode_Up; tim.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM2, &tim); // 更新事件作为 TRGO 输出,去触发 ADC TIM_SelectOutputTrigger(TIM2, TIM_TRGO_Update); TIM_Cmd(TIM2, ENABLE); }

sample_rate传进来的是每秒采样点数,比如 10000 表示 10kSa/s。预分频固定 71 是为了让计数时钟稳定在 1MHz,这样重装载值的计算就是简单的除法,改采样率只改一个参数。TIM_SelectOutputTrigger把更新事件映射到 TRGO 线上,ADC 配置成外部触发源选 TIM2_TRGO,就能做到“定时器每溢出一次,ADC 转换一次”,采样间隔由硬件锁定。

ADC 侧的配置要注意两点:一是规则通道的触发源必须设成ADC_ExternalTrigConv_T2_TRGO,二是转换完成中断里只做存值,不做任何耗时操作:

// adc.c:转换完成中断,把结果塞进环形缓冲区 void ADC1_2_IRQHandler(void) { if (ADC_GetITStatus(ADC1, ADC_IT_EOC) != RESET) { uint16_t v = ADC_GetConversionValue(ADC1); // 12 位,0~4095 Scope_PushSample(v); // 写入环形缓冲区 ADC_ClearITPendingBit(ADC1, ADC_IT_EOC); } }

ADC_GetConversionValue读的是 DR 寄存器,读动作本身会清 EOC 标志,但保险起见还是显式清一次。Scope_PushSample内部只做一次数组下标自增和边界回绕,执行时间在几十个时钟周期内,不会拖慢采样。这里有个血泪经验:中断里千万别顺手做浮点换算或者调printf,一旦中断执行时间超过采样间隔,采样点就会丢,波形上表现为周期性的缺口。

3.2 环形缓冲区与触发对齐

缓冲区用定长数组加读写指针实现,长度取 512,正好是屏幕横向像素的两倍多,方便做抽取显示:

// scope.c:环形缓冲区,写指针由中断推进,读指针由主循环推进 #define SCOPE_BUF_LEN 512 static uint16_t s_buf[SCOPE_BUF_LEN]; static volatile uint16_t s_wr = 0; void Scope_PushSample(uint16_t v) { s_buf[s_wr] = v; s_wr = (s_wr + 1) & (SCOPE_BUF_LEN - 1); // 长度取 2 的幂,用位与代替取模 } // 从触发点开始取 count 个点,触发点由边沿检测确定 uint16_t Scope_ReadAligned(uint16_t *out, uint16_t count, uint16_t level, uint8_t rising) { uint16_t start = 0, i; // 在缓冲区里找第一个跨越 level 的边沿作为起点 for (i = 1; i < SCOPE_BUF_LEN; i++) { uint16_t a = s_buf[(s_wr + i - 1) & (SCOPE_BUF_LEN - 1)]; uint16_t b = s_buf[(s_wr + i) & (SCOPE_BUF_LEN - 1)]; if (rising ? (a < level && b >= level) : (a > level && b <= level)) { start = i; break; } } for (i = 0; i < count; i++) out[i] = s_buf[(s_wr + start + i) & (SCOPE_BUF_LEN - 1)]; return start; }

Scope_ReadAligned做的是软件触发:在缓冲区里从最新数据往回找第一个符合设定边沿的跳变点,以它为起点连续取count个点。这样每次刷屏的波形起点都对齐在同一个相位上,静止信号看起来就是稳定的,不会左右乱跑。level是触发电平(0~4095),rising为 1 表示上升沿触发。缓冲区长度取 512 是 2 的幂,所以回绕用& (LEN-1)而不是%,在中断里能省几个周期。

3.3 屏幕绘制:网格、波形与量程标注

绘制分两步,先画静态网格,再画动态波形。网格只在初始化时画一次,之后每帧只清波形区域,避免整屏刷新拖慢帧率:

// lcd.c:画 8×10 的网格,横线对应电压刻度,竖线对应时间刻度 void LCD_DrawGrid(void) { uint8_t i; for (i = 0; i <= 8; i++) // 8 条横线,把 3.3V 分成 8 格 LCD_DrawHLine(0, 240 - i * 30, 320, GRAY); for (i = 0; i <= 10; i++) // 10 条竖线,对应 10 个时间格 LCD_DrawVLine(i * 32, 0, 240, GRAY); } // 把 0~4095 的采样值映射到屏幕纵坐标并连线 void Scope_DrawWave(uint16_t *pts, uint16_t n) { uint16_t i, x, y, py; py = 240 - (pts[0] * 240 / 4096); // 12 位满量程映射到 240 像素高 for (i = 1; i < n; i++) { x = i * 320 / n; y = 240 - (pts[i] * 240 / 4096); LCD_DrawLine(x - 320 / n, py, x, y, GREEN); py = y; } }

纵坐标映射用的是pts[i] * 240 / 4096,把 0~4095 线性压到 0~240 像素,再翻转成屏幕坐标(屏幕原点在左上角)。横坐标i * 320 / n把 n 个采样点均匀铺满 320 像素宽。这里用整数运算而不是浮点,是因为 F103 没有 FPU,浮点除法在每帧几百次调用下会明显拖慢刷新。量程标注在网格画完后用LCD_ShowString在左侧写“0V”“1.65V”“3.3V”,时基标注在底部写“1ms/div”之类的文字,切换时基时只重写这几个字符区域。

4. 编译、烧录与上电验证:一次跑通的完整步骤

4.1 工程导入与编译配置

工程是 Keil MDK 的.uvprojx,打开后先确认三件事:Device 选的是 STM32F103C8,Output 里勾选了 Create HEX File,Debug 里选的是 ST-Link 或 DAP-Link。标准库的路径一般放在工程目录下的Libraries/里,如果编译报找不到stm32f10x.h,去 Options → C/C++ → Include Paths 把CMSIS、FWlib/inc、User三个目录补进去。C99 模式建议打开,因为代码里用了//注释和变量中途声明。

编译前把system_stm32f10x.c里的时钟宏确认为SYSCLK_FREQ_72MHz,否则 TIM2 的预分频算出来的采样率会整体偏掉。这一步翻车的人不少——时钟没配对,波形频率看着就是错的,还以为是 ADC 的问题。

4.2 接线清单与上电顺序

接线按下面这张表来,接错一根屏就不亮或者花屏:

信号STM32 引脚说明
信号输入PA0ADC 通道 0,输入电压 0~3.3V
TFT 数据 D0~D7PB0~PB78 位并口
RS / WR / RDPA1 / PA2 / PA3命令数据选择、写、读
CS / RSTPA4 / PA5片选、复位
按键 K1~K3PA6 / PA7 / PB12时基切换、触发切换、暂停

上电顺序建议先给屏复位再初始化,代码里LCD_Init内部已经做了拉低 RST 延时再拉高的动作。信号输入一定要先确认幅度在 0~3.3V 以内,超压会直接打穿 ADC 引脚,这是硬件层面没有后悔药的地方。测未知信号前先用万用表量一下直流偏置,必要时串一个 10k 电阻加两个肖特基钳位到 3.3V 和地。

4.3 上电后该看到什么

烧录完成后复位,屏幕先白屏一下然后出现灰色网格,左侧有电压刻度,底部有时基文字。PA0 悬空时波形是一条在中间抖动的线(工频干扰耦合),用手指碰一下 PA0 会看到明显的 50Hz 正弦轮廓,这其实是个很好的自检手段——能看到这个,说明采样链路、缓冲、绘制三段都通了。按 K1 切换时基,底部文字变化,波形横向疏密跟着变;按 K2 切换触发边沿,波形起点的相位会跳一下然后稳定;按 K3 暂停,波形定格。

如果白屏,先查背光和 RST 时序;如果花屏,查数据口 PB0~PB7 有没有接反;如果波形是一条直线不动,查 ADC 的触发源是不是配成了软件触发而不是 TIM2_TRGO。

5. 避坑与排查:五个真实踩过的坑

5.1 波形左右乱跑,像在“漂”

现象是静止信号画出来起点每次都不一样,波形整体横向滑动。原因多半是没做触发对齐,直接按写指针当前位置取数,每次取的起点相位随机。解决办法就是 3.2 节里的Scope_ReadAligned,先找边沿再取数。如果做了对齐还漂,检查触发电平是不是设在了信号幅度之外,那样永远找不到边沿,函数会退化成从 0 开始取。

5.2 采样率一高波形就缺一块

现象是把时基调到最快档,波形出现周期性缺口或者台阶。原因是 ADC 中断里执行时间超过了采样间隔,或者主循环里关中断时间太长(比如刷屏时用了临界区)。排查方法是把中断里的代码精简到只剩存值,刷屏函数里不要包__disable_irq()。另外 ADC 采样时间寄存器ADC_SampleTime设得太长也会拉低实际转换速率,10kSa/s 以上建议用 41.5 或 55.5 个周期。

5.3 屏幕刷新慢,波形一顿一顿

现象是波形更新肉眼可见地卡。原因通常是每帧整屏重绘,320×240 个点全刷一遍,并口再快也扛不住。解决办法是只清波形区域再画新线,网格和文字不重画。还有一个隐蔽原因是LCD_DrawLine里用了浮点插值,换成 Bresenham 整数算法后帧率能翻倍。

5.4 编译过了但屏不亮

现象是程序跑起来了(按键有反应)但屏幕全黑。先量 RST 引脚有没有复位脉冲,再量 CS 有没有拉低。常见原因是并口数据线的方向配置错了,PB0~PB7 必须配成推挽输出而不是浮空输入;另一个原因是 FSMC 没开——如果用的是 FSMC 并口方案,记得使能RCC_AHBPeriph_FSMC,只配 GPIO 是不够的。

5.5 测小信号时噪声大得没法看

现象是测几十毫伏的信号,波形上全是毛刺。原因是 F103 的 ADC 在低幅度段量化噪声占比高,加上 PA0 走线长容易耦合。改善办法:输入前加一级运放缓冲并做 RC 低通(截止频率按信号带宽定),软件上对连续 4 个采样点做滑动平均再入缓冲区。注意平均会降低有效采样率,时基标注要相应修正,不然读出来的频率是错的。

6. 二次开发:把 demo 改成自己的采集面板

拿到这份工程之后,最值得动的三个地方是采样深度、触发方式和显示布局。采样深度上,现在缓冲区是 512 点,SRAM 还有富余,可以扩到 2048 点做更长的波形记录,但要注意Scope_ReadAligned里的回绕逻辑依赖长度是 2 的幂,扩的时候保持这个约束。触发方式上,现在只有上升沿和下降沿,加一个“自动/正常”模式切换不难:正常模式下等不到触发就一直保持上一帧,自动模式下超时没触发就强制刷新,逻辑加在Scope_ReadAligned的返回值判断上即可。

显示布局的改造空间最大。当前是固定 8×10 网格,如果要做双通道,可以把屏幕上下分成两个 120 像素高的区域,ADC 用 PA0 和 PA1 两个通道,用 ADC 的规则序列同时转换两个通道,DMA 搬到两个缓冲区。下面是一个双通道 DMA 搬运的骨架,改的时候可以直接套:

// 双通道 + DMA:ADC 规则序列扫 PA0、PA1,DMA 循环搬运到数组 #define DUAL_LEN 256 uint16_t adc_dual[DUAL_LEN * 2]; // 前一半通道0,后一半通道1,交替存放 void ADC_DualDMA_Init(void) { DMA_InitTypeDef dma; ADC_InitTypeDef adc; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel1); dma.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; dma.DMA_MemoryBaseAddr = (uint32_t)adc_dual; dma.DMA_DIR = DMA_DIR_PeripheralSRC; dma.DMA_BufferSize = DUAL_LEN * 2; dma.DMA_PeripheralInc = DMA_PeripheralInc_Disable; dma.DMA_MemoryInc = DMA_MemoryInc_Enable; dma.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; dma.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; dma.DMA_Mode = DMA_Mode_Circular; // 循环模式,缓冲区自动回绕 DMA_Init(DMA1_Channel1, &dma); DMA_Cmd(DMA1_Channel1, ENABLE); ADC_Init(ADC1, &adc); // 省略部分字段,重点在规则序列 ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_ExternalTrigConvCmd(ADC1, ENABLE); // 仍由 TIM2_TRGO 触发 }

DMA_Mode_Circular是关键,它让 DMA 在搬满DUAL_LEN * 2个半字后自动回到起点,主循环随时读数组都不会越界。规则序列里通道 0 排第 1、通道 1 排第 2,所以数组里是交替存放的,画图时按奇偶下标拆成两路。ADC_ExternalTrigConvCmd保持使能,采样节奏还是由 TIM2 控制,DMA 只是把“读 DR”这个动作从 CPU 手里接过去,中断开销直接归零。

验证改造是否成功,我一般用三步:先用信号发生器给一个 1kHz、1Vpp 的正弦,看两路波形相位和幅度是否一致;再把一路接 3.3V 直流、一路接地,看两条线是否分别贴顶和贴底;最后把时基从最慢切到最快,观察波形有没有跳变或丢帧。三步都过,基本可以认为采样链路是干净的。

从那以后我每次改采样相关的代码,都强制先用直流和已知频率正弦各跑一遍再上真实信号——这个习惯帮我省掉了好几次把量化噪声当成信号特征去调电路的冤枉路。希望帮到你。

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

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

Windows Sandbox中CreateProcessAsUserW失败原因与修复方案

1. 这个错误不是权限问题&#xff0c;而是沙盒环境的“身份错位” 第一次在某跨平台系统开发中遇到 CreateProcessAsUserW failed: 2 报错时&#xff0c;我下意识去查 Windows 权限策略、UAC 设置、管理员组成员关系——整整花了三小时翻文档、改组策略、重装沙盒功能&#x…

作者头像 李华
网站建设 2026/11/4 13:21:36

Ubuntu 22.04 自建企业知识库:Halogen + pgvector 部署与调优实战

1. 为什么要在 Ubuntu 上自建企业知识库 1.1 从 6.4 万份文档说起&#xff1a;一个真实的需求场景 我所在团队维护着一个积累了六年多的企业知识库&#xff0c;里面沉淀了产品文档、技术方案、会议纪要、客户案例、运维手册等各类资料&#xff0c;总数大概在 6.4 万份左右。这…

作者头像 李华
网站建设 2026/11/4 13:20:19

三步搞定高画质视频下载:音视频分离与流复制合并实战

视频下载这件事&#xff0c;说大不大&#xff0c;说小也不小。我自己平时做素材归档、离线剪辑、课程备份&#xff0c;经常需要把一些公开的视频保存到本地&#xff0c;时间一长就攒出了一套相对顺手的流程。这次聊的这套“三步走”方案&#xff0c;核心目标很明确&#xff1a;…

作者头像 李华
网站建设 2026/11/4 13:16:29

SSL证书一键部署工具:简化HTTPS配置

1. 项目概述&#xff1a;SSL证书部署的平民化革命 最近在给朋友的小型电商网站部署HTTPS时&#xff0c;发现市面上缺少一款真正适合非技术人员的SSL证书工具。大多数方案要么需要命令行操作&#xff0c;要么要求服务器权限&#xff0c;对普通站长极不友好。于是我开始寻找一种&…

作者头像 李华
网站建设 2026/11/4 13:15:58

并行草稿机制解析:从P-EAGLE到DSpark的投机采样加速

做推理加速的人&#xff0c;几乎都会碰到同一个尴尬&#xff1a;GPU 很贵、显存很大&#xff0c;但生成 token 的速度就是上不去。自回归解码一步只出一个字&#xff0c;剩下的时间基本都耗在把几百亿参数从显存搬到计算单元的路上。投机采样&#xff08;Speculative Decoding&…

作者头像 李华
网站建设 2026/11/4 13:14:41

金融行业大文件上传解决方案:Vue+WebUploader实战

1. 金融行业大文件上传的痛点与解决方案 在金融行业的日常业务中&#xff0c;Excel文件的上传处理是一个高频需求场景。从财务报表到交易记录&#xff0c;从业者每天都需要处理大量包含敏感数据的电子表格。但实际工作中我们常遇到这样的困境&#xff1a;当用户尝试上传超过50M…

作者头像 李华