资讯动态

STM32简易示波器工程解析:ADC+DMA+定时器触发完整链路

发布时间:2026/10/20 6:41:39 来源:尧图企业网站定制
简介基于STM32F103ZET6开发的简易示波器工程面向嵌入式初学者与电子工程师演示如何利用Cortex-M3处理器内置ADC、定时器及LCD显示接口完成模拟信号实时采集、波形数据处理与屏幕绘制可观测正弦波、方波等常见信号适用于电路调试、信号检测及单片机外设开发练习。资源包共272个文件压缩后大小约8.9MB其中C语言源码.c/.h为核心另含编译过程文件.lst/.o/.d、链接映射与烧录镜像.axf/.hex/.map以及Keil工程配置、文本说明和备份文档覆盖从工程打开、程序编译到固件下载的完整流程。该资源已有4570人学习下载。除可直接编译烧录的固件工程外资料中保留了中间输出文件与配置备份便于对照理解ADC采样时序、定时器触发、FSMC/SPI驱动LCD以及波形显示算法的实现细节同时附带使用文档和工程备份可帮助快速搭建开发环境、复用代码框架为设计便携式示波器或深入掌握STM32系列驱动开发提供有效参考。1. 一个单片机示波器的工程包值得你花一个下午拆开把信号发生器调成 1kHz 正弦波屏上却是一条带毛刺的直线这是我第一次跑通“STM32F103ZET6 示波器程序”的真实感受。后来发现问题出在触发方式和 DMA 搬运时序上而不是硬件坏了。这份基于 STM32F103ZET6 的简易示波器工程本质是用单片机的 ADC 连续采样、DMA 搬运、定时器触发外加一块 TFTLCD 屏幕把模拟信号变成可观察的波形。它适合两类人一类是刚学完单片机基础、想把手里的开发板变成测量工具的新手另一类是已经在做信号采集项目、需要参考采样链路和屏幕刷新方案的在职人员。这个工程不大但采样、搬运、显示、交互四件事都齐了拆完它你对 STM32 的 DMA 和定时器触发理解会上一个台阶。2. 整体架构从信号输入到屏幕刷新的四段链路2.1 硬件构成与引脚分配我拿到这份工程时先看的是引脚分配。主控是 STM32F103ZET6这是大容量型号512KB Flash、64KB RAM做示波器演示绰绰有余。输入信号从 PA0 进入对应 ADC1 的通道 0TFTLCD 屏走 FSMC 接口数据总线挂在 PB0~PB15 的十六位并口上控制线用 NE1 做片选、A16 做寄存器选择这类接法在正点原子和野火的板子上都是标配按键接在 PE2~PE4 上做时基切换、幅值切换和触发模式切换。屏的型号多数工程用的是 ILI9341 控制器也可以是 ILI9325代码里只要把底层写寄存器函数对上就行。如果你手里的屏不一样先改 LCD_WriteReg 和 LCD_WriteData 这两个函数。频率测量这一路工程里是抓 PA8 的输入捕获也就是 TIM1 的通道 1用来读信号频率。整个工程的信号流分三段信号从 PA0 进 ADCADC 由定时器触发转换结果交给 DMA 搬进内存主循环里把内存中的波形数据画到屏上。硬件参数上F103 的 ADC 是 12 位分辨率最快转换时间约 1.2us理论上 1Msps 左右。实际工程里常用定时器做外部触发把采样率控制在 100kHz 以内比较稳妥因为还要留时间给屏幕刷新和按键响应。输入带宽不用指望太高前端没有运放跟随的话输入阻抗基本就是 ADC 内部的采样电容测低频信号没问题测高频信号幅度会掉得厉害。这个工程的定位是“简易示波器”不是射频仪器别拿它去量高频晶振波形就行。功能模块使用的引脚/外设说明ADC 采样输入PA0 / ADC1_CH0默认单通道采样DMA 搬运DMA1_Channel1把 ADC 转换结果搬到内存缓存采样触发TIM2_TRGO通过定时器更新事件触发 ADCLCD 显示FSMC 接口 / PB 口16 位并口映射到外部 SRAM 地址频率测量PA8 / TIM1_CH1输入捕获模式测频按键交互PE2 / PE3 / PE4时基、幅值、触发模式切换2.2 数据流采样、存储、显示三环解耦这个工程让我觉得有价值的地方在于它把数据流拆成了三个独立环节。采样环节是 ADC 在定时器驱动下不断转换结果通过 DMA 写到指定的内存数组整个过程不占用 CPU主循环只负责把这块内存里的数据搬上 LCD。这其实是很多工业采集设备的通用套路采集由硬件完成处理由软件完成两者用一块环形缓冲或双缓冲衔接。工程里用的是双缓冲思路。两块缓冲区DMA 交替往里面写写完一块就置标志位主循环查到这个标志位后去读已经写完的那一块同时把另一块交给 DMA 继续填充。这样避免了一边 DMA 写、一边 CPU 读造成的数据错位。我第一次跑的时候没注意这个直接把 DMA 的缓冲区和 LCD 绘制的数组设成同一个结果波形在屏上像流水一样抖。后来改成双缓冲才稳定住。存储深度这块工程默认缓存 4096 个点12 位 ADC 结果存在 16 位数组里占 8KB RAM对 64KB RAM 来说很充裕。你可以改成 8192 点画面会更细腻但要注意 DMA 传输计数和绘制循环都要同步改。显示环节是这套系统里最耗时的一部分。ILI9341 刷一屏 320x240 的像素如果用画点函数一个一个点去写刷新率会低到没法看。工程里多数是先把波形算成坐标数组再批量把该亮的点一次性写到屏的 GRAM 里。更讲究的做法是用 LCD 的区域写功能把整个波形区域当作一个矩形窗口然后连续写像素值这样速度比逐点跳转快好几倍。2.3 参数与性能边界把工程的参数列出来方便你对照手里的板子评估效果。垂直灵敏度常见分档是 5mV/div 到 5V/div但这是把 ADC 的 0~3.3V 量程按满幅折算出来的没有硬件放大或衰减电路所以实际能精确测量的范围就是 0~3.3V测负半周信号会被 ADC 内部钳位削掉。时基分档从 10us/div 到 500ms/div对应采样率从 1MHz 档往下递减分档逻辑是改定时器的预分频系数和自动重载值。水平方向的采样率与显示屏的宽度有关。显示屏横坐标若取 320 像素一屏显示 320 个采样点时基 1ms/div 说明每个水平格 1ms10 格共 10ms采样率就是 32kHz 左右。工程里常见的做法是不做抽取直接画全部点但为配合时基得更合理。波形的水平位置和垂直偏移也能调垂直偏移通过软件给 ADC 值加一个直流偏置实现水平位置通过跳过数组前 N 个点再开始绘制实现这两块代码通常放在按键控制里。整体性能上实测最高不失真采样率大约在 500kHz 左右再往上信号幅度会因混叠而乱跳。ADC 的采样时间要留够工程里把采样时间设成了 239.5 个周期也就是最慢档用来保证输入阻抗高、采得准。这是很多新手容易忽略的点采样时间设短了源阻抗稍高采出来的值就偏低。3. 核心代码拆解定时器触发、ADC 转换、DMA 搬运的采样链路3.1 定时器触发与采样率生成这套采样链路的核心是“定时器触发 ADCADC 完成后通知 DMA 搬运”。用定时器而不是软件不断启动ADC是为了让采样间隔严格均匀这是波形不失真的底线。// tim2_trigger.c —— TIM2 更新事件作为 ADC 触发源 void TIM2_Init(uint16_t prescaler, uint16_t period) { TIM_TimeBaseInitTypeDef tim; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); tim.TIM_Prescaler prescaler; // 预分频72MHz / (prescaler1) tim.TIM_CounterMode TIM_CounterMode_Up; tim.TIM_Period period; // 自动重载值决定触发频率 tim.TIM_ClockDivision TIM_CKD_DIV1; tim.TIM_RepetitionCounter 0; // 高级定时器才用到TIM2 填 0 TIM_TimeBaseInit(TIM2, tim); TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update); // 更新事件作为 TRGO TIM_Cmd(TIM2, ENABLE); }逻辑说明TIM2 的更新事件通过 TRGO 引脚输出到 ADC 的外部触发输入每个更新事件到来时 ADC 自动启动一次转换不需要 CPU 干预。预分频器和自动重载值组合出采样率公式是采样率 72MHz / (prescaler1) / (period1)。比如要采 100kHz就设 prescaler7、period89得到 72M / 8 / 90 100kHz。注意 TIM2 的时钟源在 APB1 上如果 APB1 分频不是 1实际时钟要先乘倍频我一般在系统时钟配置里统一把 APB1 设为 36MHz然后代码里按 72MHz 算因为 TIM2 的时钟会自动乘 2 回到 72MHz。3.2 ADC 与 DMA 双缓冲配置ADC 部分要用 ADC1开启扫描模式还是单次模式取决于你是否要测多通道。单通道示波器把扫描模式关掉更关键的是把 DMA 请求打开。这里有个常见翻车点ADC 的 DMA 请求和 DMA 通道的传输方向、模式必须匹配循环模式和双缓冲模式要用对。// adc_dma.c —— ADC1 连续采样 DMA1 双缓冲搬运 #define SAMPLE_NUM 4096 uint16_t adc_buf[2][SAMPLE_NUM]; volatile uint8_t buf_ready[2] {0, 0}; // 双缓冲完成标志 void ADC1_DMA_Init(void) { GPIO_InitTypeDef gpio; ADC_InitTypeDef adc; DMA_InitTypeDef dma; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); GPIO_StructInit(gpio); gpio.GPIO_Pin GPIO_Pin_0; gpio.GPIO_Mode GPIO_Mode_AIN; // PA0 模拟输入 GPIO_Init(GPIOA, gpio); ADC_StructInit(adc); adc.ADC_ExternalTrigConv ADC_ExternalTrigConv_T2_TRGO; // 由 TIM2 触发 adc.ADC_ExternalTrigConvCmd ENABLE; // 外部触发使能 adc.ADC_DataAlign ADC_DataAlign_Right; adc.ADC_NbrOfChannel 1; adc.ADC_Resolution ADC_Resolution_12b; ADC_Init(ADC1, adc); ADC_DMACmd(ADC1, ENABLE); // 开启 ADC 的 DMA 请求 ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); // DMA1_Channel1 搬运到缓冲区0先启动第一块 DMA_StructInit(dma); dma.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; dma.DMA_MemoryBaseAddr (uint32_t)adc_buf[0]; dma.DMA_DIR DMA_DIR_PeripheralSRC; dma.DMA_BufferSize SAMPLE_NUM; 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.DMA_Priority DMA_Priority_High; DMA_Init(DMA1_Channel1, dma); DMA_ITConfig(DMA1_Channel1, DMA_IT_TC, ENABLE); // 传输完成中断 DMA_Cmd(DMA1_Channel1, ENABLE); }逻辑说明DMA 配置成循环模式后每次传满 SAMPLE_NUM 个点会触发一次传输完成中断然后地址自动绕回起点继续搬运。我在中断里切换缓冲区的索引和完成标志主循环轮询标志后把对应缓冲区的数据拿去绘制。这里有个值得注意的参数是 DMA_PeripheralDataSize 和 DMA_MemoryDataSize 都设为半字因为 ADC 的 12 位结果正好存在 16 位寄存器中。如果你把数据宽度设成字节高 8 位会被截掉波形会变成带阶梯的锯齿。双缓冲的切块我用了一个简单办法中断里不改变 DMA 的目标地址而是维护当前“正在被DMA写”的缓冲区和“闲置”的缓冲区编号CPU 永远读闲置那块。这样两块 4KB 的缓冲区轮流被填充CPU 和 DMA 不会同时访问同一块内存数据一致性有保证。代价是中断里要多做几次索引切换但对 100kHz 采样率来说没有压力。3.3 采样数据处理去极值和波形重组ADC 的值直接画出来通常会有一两个异常点要么是高到顶、要么是低到底原因是电源纹波或 ADC 偶发转换异常。工程里做了一个简单的去极值滤波处理方式是把相邻三次采样的中间值作为有效值这个做法比均值滤波保留波形细节更好。// process.c —— 去极值滤波 波形缓存分发 int16_t median3(int16_t a, int16_t b, int16_t c) { if (a b) { int16_t t a; a b; b t; } if (b c) { int16_t t b; b c; c t; } if (a b) { int16_t t a; a b; b t; } return b; // 三值取中 } uint16_t waveform_cache[SAMPLE_NUM]; void process_waveform(uint16_t *src, uint16_t *dst, uint16_t len) { for (uint16_t i 1; i len - 1; i) { dst[i] median3(src[i-1], src[i], src[i1]); } dst[0] src[0]; dst[len-1] src[len-1]; }逻辑说明median3 函数通过三次比较交换取中间值不产生额外内存分配适合在中断或主循环里高频调用。process_waveform 把 DMA 缓冲区的原始 4096 个点逐一遍历生成去极值后的新缓存。处理一个点大约十几个周期4096 个点在 72MHz 主频下不到 1ms不会影响实时性。去极值后波形数据被主循环拿去画屏幕你看不到那些偶发的毛刺了。若你嫌中值滤波削峰太厉害可以把 median3 换成“与上次值差超过阈值才更新”的死区滤波保留真正的高频脉冲。4. 波形绘制与交互控制LCD 刷屏与按键调参4.1 FSMC 接口与画点画线底层实现FSMC 是 F103 系列里很实用的外设它能把外部存储器接口映射到内核的地址空间对 LCD 的读写操作就变成了对某个内存地址的读写字。工程里把 LCD 当作一块外部 SRAM命令写入地址 0x60000000数据写入地址 0x60020000这个偏移由 A16 地址线决定。这样的好处是画点函数里不用 GPIO 逐位翻转时序而是直接给指针赋值CPU 开销极小。// lcd_fsmc.c —— FSMC 接口画点 #define LCD_REG_ADDR *(volatile uint16_t *)0x60000000 #define LCD_DATA_ADDR *(volatile uint16_t *)0x60020000 void LCD_WriteReg(uint16_t reg) { LCD_REG_ADDR reg; } void LCD_WriteData(uint16_t data) { LCD_DATA_ADDR data; } void LCD_SetCursor(uint16_t x, uint16_t y) { LCD_WriteReg(0x002A); // 列地址设置 LCD_WriteData(x); LCD_WriteReg(0x002B); // 行地址设置 LCD_WriteData(y); LCD_WriteReg(0x002C); // 开始写显存 } void LCD_DrawPoint(uint16_t x, uint16_t y, uint16_t color) { LCD_SetCursor(x, y); LCD_WriteData(color); }逻辑说明多数 ILI9341 的驱动流程都是先发命令地址、再发坐标参数、最后连续写像素颜色。0x002A、0x002B、0x002C 是 ILI9341 标准的列地址、行地址和显存写入命令换成 ILI9325 时命令码不同但流程一样。画点函数虽然写得简单但逐点画整屏波形就会很慢所以波形绘制会先用画直线函数把相邻采样点连起来减少点的数量。你需要特别关注 FSMC 的时序配置地址建立时间和数据建立时间设得太短高速刷屏时会出现偏色和错线。工程里一般把 EXTMOD 打开用独立的时序参数控制读和写。4.2 波形区绘制网格、坐标轴与动态刷新波形显示不是简单画点就完事。工程里先把整个波形区域分成 10x8 的网格用灰色细线画出来然后根据当前时基把 DMA 缓冲区里的 4096 个点抽取成 320 个点对应屏幕水平方向分辨率再做垂直映射到屏幕坐标最后把相邻两点连成一条折线。抽取的做法是每隔 12 个点取一个但因为时基在变抽取间隔也会变。// draw_wave.c —— 抽取 垂直映射 连线绘制 void Draw_Waveform(uint16_t *src, uint16_t points, uint16_t vref) { uint16_t i, x, y; uint16_t x_step points / 320; // 每屏水平像素数固定为 320 for (i 0; i 320; i) { // 每 x_step 个点取一个峰值点避免漏掉窄脉冲 uint16_t idx i * x_step x_step / 2; uint16_t val src[idx]; y WAVE_TOP (val * WAVE_HEIGHT) / 4096; x WAVE_LEFT i; if (i 0) LCD_DrawPoint(x, y, WAVE_COLOR); else LCD_DrawLine(prev_x, prev_y, x, y, WAVE_COLOR); prev_x x; prev_y y; } (void)vref; // 垂直偏移量在这里统一加到 val 上 }逻辑说明x_step 是抽取步长points 是 DMA 缓冲区里的有效点数。当 points4096 时x_step12相当于每 12 个原始采样点中取中间那一个来画。这里没做峰值抽取所以窄脉冲可能被漏掉若你测的是方波或脉冲信号建议改成在一个 x_step 窗口内取最大值和最小值分别画上下两根线渲染效果更像真实示波器。垂直映射用了一个线性公式y WAVE_TOP (val * WAVE_HEIGHT) / 4096把 12 位 ADC 值线性映射到波形区域的高度范围内。WAVE_TOP 和 WAVE_HEIGHT 是波形区域上边界和高度改这两个值就能调整波形在屏上的位置和缩放比例。每帧波形绘制之前先清掉旧波形区工程里是把这个区域填充为黑色再画网格和波形线。清屏操作如果整块区域用画点函数做会很慢用区域填充往 GRAM 里连续写入一个背景色值会快很多。4.3 按键交互与时基调整逻辑按键在这个工程里的核心职责是切换时基和幅值以及选择触发模式。工程里简单做了按键扫描消抖后识别短按和长按。短按时基步进到下一档长按时基连续加快方便快速从低频信号跳到高频信号。时基档位的数值存在一个常量数组里每次按键只是改变数组索引然后重新配置定时器。// control.c —— 时基切换 const uint16_t timebase_table[8][2] { {7, 89}, // 100kHz {71, 89}, // 10kHz {719, 89}, // 1kHz {719, 899}, // 100Hz }; void Timebase_Next(void) { timebase_index; if (timebase_index 4) timebase_index 0; TIM2_Init(timebase_table[timebase_index][0], timebase_table[timebase_index][1]); delay_ms(5); // 等待采样链路重新稳定 }逻辑说明timebase_table 里每个元素对应预分频和自动重载的组合。8 行 2 列是我举的例子工程里可能有更多档位。切换时基时定时器重新初始化ADC 触发频率随之变化但 DMA 和缓冲区不用动主循环自然按新采样率刷新画面。这里值得注意的是切换后不要立刻读波形数据因为缓冲区里还残留旧采样率下的数据等几个毫秒再绘制也就是 delay_ms(5) 的作用。触发电平的调整逻辑类似只不过它不是改定时器而是改垂直映射公式里的 vref 偏移量按一次按键 vref 加一个固定步长波形整体上移或下移通常还会提供一个“自动触发”模式由程序寻找波形的过零点来对齐波形。5. 避坑指南调 STM32 简易示波器最容易翻车的五个点5.1 波形整体跳动ADC 值像随机数现象输入一个稳定的直流电压屏上波形不是一条直线而是在某个范围内上下乱跳幅度很大。原因最常见的是 ADC 的采样时间设置太短外部信号源的内阻和 ADC 采样电容之间没充放电完全导致转换结果不准。另一个常见原因是 VREF 引脚悬空或滤波电容没接参考电压不稳定。解决把 ADC 采样时间设为最慢档 239.5 周期在 VREF 与 VREF- 之间加一个 1uF 陶瓷电容最好再并联一个 0.1uF 高频去耦电容。还有一条排查路径是检查 ADC 时钟ADC 的输入时钟最高 14MHz如果 PCLK2 分频设成了 1ADC 时钟 72MHz 直接超标转换结果就会严重非线性。5.2 信号频率一变高波形幅度就变矮现象输入 1kHz 正弦波时显示的 Vpp 是实际值输入 50kHz 时幅度明显缩水高频时几乎看不到。原因输入端没有做阻抗匹配和带宽限制。ADC 的采样电容在采样阶段会从外部吸收电荷等效输入阻抗随采样率升高而降低信号源内阻一高分压效应就拉低了实际进入 ADC 的电压。解决在输入前端加一个电压跟随器用单运放做同相放大输入阻抗做到 1M 欧姆级别。若手头没有运放就降低信号源内阻把信号发生器的输出阻抗调到最低档。另外输入串联电阻不要超过 100 欧姆否则会和采样电容形成 RC 低通进一步压高频。5.3 DMA 开启后程序跑飞或卡死现象加上 DMA 中断后主循环偶尔卡住或者跑一段时间进入 HardFault。原因常见是 DMA 中断标志没有及时清除重复进入中断另一种是配置了 DMA_Mode_Circular但中断服务函数里对缓冲区的长度越界操作把栈踩坏了。解决在 DMA 中断里先读 DMA_GetITStatus确认是传输完成中断后清 DMA_ClearITPendingBit再对缓冲区索引取模。同时检查 DMA 配置里的 MemoryBaseAddr 是否指向合法的 RAM 地址若指向 Flash 地址或者 0x10000000 之外的地址就会触发总线错误。我建议在 DMA 中断里所有数组访问都用 volatile 修饰防止编译器优化后出现索引错乱。5.4 屏幕刷新闪烁严重波形像在闪动现象波形会显示出来但整屏每隔几百毫秒闪一下看着眼睛很累。原因主循环里绘制波形前先整屏清成黑色清屏和绘制之间有较长时间的空白期人眼能感知到闪烁。另一个原因是 FSMC 时序配置过慢一帧波形画了 100ms。解决不要整屏清空只把波形区域那一条高度内的旧画面填充掉且填充和画线尽量在同一帧内完成。更彻底的做法是开启 LCD 的撕裂抑制功能在 VSYNC 信号到来前不写显存保证 DDRAM 更新中不会出现撕裂。工程里如果没接 VSYNC 引脚可以至少在写完一帧后强制等待两个 vsync 周期再开始下一帧。5.5 频率测量在小信号时读数乱跳现象用输入捕获量频率信号幅度正常时读数很准但信号调到 100mV 以下时读数突然变成随机数。原因输入捕获的触发沿检测没有回差信号叠加噪声后在阈值附近来回穿越产生无数个假沿。解决在捕获引脚前加一个史密特触发器电路或者用比较器把信号整形为方波。软件上可以开启捕获输入滤波STM32 的输入捕获滤波器本质上是一个数字低通用来去毛刺。TIM_ICInit 里的 TIM_ICFilter 寄存器设成 0x0F表示连续采样 8 个周期都为同一电平才确认边沿能滤掉大部分噪声。若噪声仍严重就在代码里对连续两次捕获的计数值求差差值小于阈值就丢弃。6. 二次开发把单通道示波器改成双通道与自动测量6.1 双通道采样的改造思路单通道改成双通道不需要换芯片把 ADC1 的规则通道序列从 1 个通道改成 2 个通道。具体做法是用 ADC_RegularChannelConfig 依次配置通道 0 和通道 1再设置 ADC_NbrOfChannel 为 2DMA 缓冲区长度保持 4096但每个周期写入两个采样值。这样 DMA 搬运回来的数组是两通道交织的处理时下标对 2 取模即可分离两路波形。代价是每个通道的实际采样率减半因为 ADC 完成一组规则序列需要一次转换通道 0、一次转换通道 1。工程里若原本采样率是 100kHz双通道后每通道实际只有 50kHz。这个数据流的变化很简单但绘制函数要注意把波形分离成上下两个显示区不要叠在一起。垂直方向高度的减半会让波形幅度看起来变小如果是两路不同幅度的信号最好各配一个独立的垂直灵敏度参数。改造时还需要注意触发源只能选一路。我一般做法是通道 0 作为触发源稳定显示后通道 1 的波形即使有抖动也能保持相对稳定的相位关系。如果两路信号频率不同双通道示波器没法同时稳定两路这是所有模拟前端采集方案的通病不用纠结。6.2 自动测量Vpp 和频率的软件实现自动测量是示波器的基本盘。工程里可以加一个简单的软件测量模块遍历缓冲区里的所有采样点找出最大值和最小值两者相减得到 Vpp再按当前时基换算成实际电压。频率测量则是扫描数组中的过零点记录连续两个上升沿之间的采样点数乘以采样周期得到周期取倒数就是频率。// measure.c —— 自动测量 Vpp 与频率 float Measure_Vpp(uint16_t *buf, uint16_t len, float vref_frac) { uint16_t min 4095, max 0; for (uint16_t i 0; i len; i) { if (buf[i] max) max buf[i]; if (buf[i] min) min buf[i]; } float vpp (max - min) * 3.3f / 4096.0f; return vpp / vref_frac; // 除垂直缩放系数还原真实电压 } uint32_t Measure_Freq(uint16_t *buf, uint16_t len, uint32_t sample_rate) { uint8_t last_edge 0; uint16_t cnt 0; uint32_t period 0; for (uint16_t i 1; i len; i) { uint8_t cur_edge (buf[i] 2048) (buf[i-1] 2048); if (cur_edge !last_edge) { period i - cnt; break; } if (cur_edge) cnt i; last_edge cur_edge; } if (period 0) return 0; return sample_rate / period; }逻辑说明Measure_Vpp 先遍历整段缓冲区找极值再用 12 位分辨率和 3.3V 基准计算实际电压。vref_frac 是垂直灵敏度系数若波形被垂直放大到了 2 倍真实电压就是显示值的 1/2。Measure_Freq 的做法是寻找从低于中值到高于中值的上升沿记录第一个上升沿和第二个上升沿之间的采样点个数除以采样率得到周期。这个算法不能处理直流信号和低于 0V 的负半周信号因为 ADC 只能采 0~3.3V所以测交流信号前要加直流偏置把信号垫到 1.65V 左右。6.3 验证方法用已知信号校准系统改完代码后不要直接相信测量结果先接一个已知幅值和频率的信号源做校准。我通常先输出一个 1kHz、1Vpp 的正弦波检查示波器显示的频率是否为 1kHzVpp 是否为 1V。如果频率偏了检查采样率计算公式里是否把预分频加 1 和自动重载加 1 都算对了这是最容易出错的地方。如果 Vpp 偏了先看直流偏置电压是否准确再检查幅值映射公式里的基准电压是不是 3.3V有些板子的 VDDA 实际只有 3.28V那样需要把 3.3f 换成实测值。校准完低频信号后再往高频方向推能看 50kHz 方波不畸变这套系统就有实用价值了。从那以后我每次调这类采集工程都强制自己先跑一遍信号源校准流程把每个档位的读数记下来再动代码。很多采样率错位、基准不准的问题在已知信号面前一照就原形毕露省得在屏上瞎猜。这个工程包里的代码逻辑本身并不复杂但足够把 STM32 的 ADC、DMA、定时器、FSMC 四块外设串成一条完整的实时信号链拆完再自己加一两个功能比你单独看外设例程要扎实得多。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑