首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
STM32F4定时器触发ADC与DMA实现FFT测频全攻略
📅 2026/9/9 0:15:28
✍️ 爱科研究院
👁 阅读 3,247
简介这是一套基于STM32F4的完整信号采集与频率分析工程面向嵌入式开发者和电子爱好者解决定时器触发ADC双通道采样、DMA高效传输以及FFT频谱测量与可变采样率波形显示等实践需求。压缩包共341个文件以h/c源码文件为主同时包含uvprojx/uvoptx工程配置、axf/hex编译产物、bat辅助脚本、md说明文档及map/lst映射列表等整体约40.78MB目录结构完整可直接导入Keil编译调试。项目结合时钟触发ADC实现精确同步采样通过DMA减轻CPU负载并利用FFT对时域信号进行频域分析配合可调采样频率的波形显示形成一套可参考的嵌入式信号处理方案。目前已有630人学习下载适合正在学习STM32 HAL库、深入研究ADC与DMA协同工作或需要快速搭建频谱分析功能的开发者参考复用。 做了那么多单片机项目最后发现数字测频这事儿绕不开“采样”和“算得快”这两道坎。用STM32F4做FFT测频率已经算是性价比很高的方案了但很多人第一版做出来不是频率抖得像心电图就是波形看起来总有点别扭。问题多半不在FFT本身而是前面ADC采样那一路没理顺。这篇文章我把定时器触发ADC、双通道采样、DMA搬运、可变采样率、FFT算频率这一整套流程拆开讲清楚每步都附上配置思路和踩坑记录适合正在调STM32F4采集频率、做示波器或者频谱分析的朋友参考。1. 方案设计与关键链路拆解1.1 为什么非得用时钟触发ADC而不是软件连续采样写第一版时很多人习惯在主循环里直接“ADC启动→等待转换完成→读数据”这么做低频下凑合能用但到了几千赫兹往上就露馅了。主循环里有中断、有显示刷新、有串口输出每次循环的时间都不一样ADC采样的时间间隔也就忽长忽短。光这一条就够让FFT结果面目全非的。FFT的核心假设是“等间隔采样”。哪怕间隔只抖动几个微秒体现在频谱上就是噪声底抬高、谱线变宽测出来的频率值会在真实值附近来回蹦。所以工程上必须引入硬件定时触发让定时器的更新事件TRGO作为ADC的启动信号这样采样时刻完全由硬件决定CPU再忙也不影响采样间隔。ST官方应用笔记里也建议这种做法它不是“推荐”而是“想测准就得这么干”。1.2 双通道同步采样的用意双通道不是非加不可但加了以后能做的事多很多可以同时采集两路不同频率的信号做比对也可以一路接被测信号、一路接参考源测试相位差甚至第二路直接悬空或者接基准电压用来检测系统的直流漂移。需要注意的是STM32F4的ADC是单核顺序转换结构双通道并不是真正的“同步保持”只是扫描模式下按顺序快速切换采集。所以两通道之间会有极小的相位差具体差多少取决于采样时间和通道切换开销。对音频范围内的测频而言这点差别可以忽略但要测严格相位关系时就要算一笔账。1.3 系统链路从模拟信号到频率数值整条链路可以画成外部信号 → 前端调理 → ADC采样 → DMA搬运 → 内存缓冲区 → 数据处理抽取/加窗/FFT → 频率计算 → 显示/串口输出。采样率可变这一步本质上是修改定时器的分频系数和重装载值。定下来一个原则采样率变但缓冲长度不变。比如统一设FFT点数N1024采样率从10kHz切到100kHz时频率分辨率也跟着变这个是物理规律逃不掉。稍后会写清楚怎么算。1.4 采样率可变方案怎么权衡可变采样率最大的好处是兼顾不同频段的信号——低频信号用低采样率频率分辨率做得细高频信号用高采样率把奈奎斯特频率撑上去。但是可变采样率也给设计和调试增加了不少工作量主要是两个问题定时器配置切换时的无缝衔接。不能一边在采集一边突然改参数否则数据缓冲里会出现混着两段不同采样率的波形FFT算出来就是一团乱麻。ADC模拟部分需要稳定时间。采样率切到高频后ADC内部的采样电容充放电节奏变了最好留出几个毫秒的稳定期再开始采有效数据否则前几十个点经常是飘的。2. 定时器触发ADC的配置要点2.1 用定时器的Update事件做TRGO触发源以STM32F407为例可以先选一个通用定时器TIM2、TIM3这类配置成向上计数模式把送定时器时钟设置为内部时钟源比如84MHz或系统时钟的APB1定时器时钟。让它工作在PWM模式之外的一种“主模式”——即更新事件产生TRGO信号。关键是这一步在定时器的CR2寄存器里设置MMS主模式选择 001表示“更新事件被作为TRGO输出”。然后让ADC的外部触发源选择这个定时器的TRGO。这样每当定时器计数溢出更新事件ADC就会自动启动一次转换。推一个常用公式采样频率 定时器时钟 / ((分频系数PSC 1) * (自动重装载值ARR 1))。举个例子定时器时钟84MHz目标采样率42kHz则84M / (PSC1) / (ARR1) 42000可以取PSC1ARR999因为84M/2/100042000正好。如果要100kHz那就是84M / (PSC1) / (ARR1) 100000取PSC1、ARR419即84M/2/420100000。注意ARR要写在“重装载值”里而不是“比较值”里。2.2 ADC规则组多通道配置与采样周期选择ADC1打开两个规则通道比如Channel 0和Channel 1。规则组的扫描模式Scan Mode必须使能这样ADC收到一次触发后会按顺序依次转换两个通道。转换结果一个接一个地放到ADC_DR数据寄存器里。这里有个容易踩的坑ADC的采样时间Sample Time不是越小越好。每个通道有独立的采样时间选择位范围从3个周期到480个周期。如果信号源输出阻抗偏高比如是几十千欧级别的电阻分压网络直接进ADC采样周期太短会导致采样电容吃不饱测出来的电压偏低波形幅度会无辜地“缩水”。实际做音频带宽的信号一般选15或28个周期精度和速度比较均衡。如果信号源是高阻抗类型的建议还是加一级运放电压跟随器否则调采样时间到头了也救不回来。转换周期计算公式ADC转换总时间 采样周期 12个周期12位分辨率时。比如采样周期15个ADC时钟周期那么每个通道一次转换时间是27个ADC周期。要注意多通道扫描时总时间要乘以通道数。2.3 DMA配置与传输模式选择STM32F4的ADC1要配上DMA2的Stream 0通道是ADC1的专用DMA通道。传输模式一定选Circular循环模式数据宽度半字16位因为ADC是12位结果存放在低16位。在DMA控制寄存器里有关键的一项叫“Continuous Requests”连续请求F4里这一位必须使能否则即使配置了循环模式传输一轮以后DMA也可能停住表现就是采一段就断了。再用一个接收缓冲区比如定义uint16_t adc_buf[1024 * 2]两个通道各1024点DMA自动循环往这里填数据。逻辑上用手动方式管理开一个“半传输完成中断”当DMA写满一半缓冲区时触发中断说明前一半已经稳定了可以即时处理这块数据。DMA还有一个“传输完成中断”表示全满可以处理后半块。这样数据一进一出互不干扰不会出现同一块数据又被DMA改写又被FFT读取的竞争状态。3. 数据预处理与FFT测频实战3.1 从DMA缓冲里把双通道数据拆出来DMA搬运到内存的数据格式是ch0[0], ch1[0], ch0[1], ch1[1], ...依次交替存储。也就是说每个偶数索引是通道0的数据每个奇数索引是通道1的数据。处理时就要拆开重排分别放入两个独立数组送入FFT。以ST的CMSIS-DSP库为例。arm_cfft_f32要求输入数据是浮点数且按照“实部、虚部、实部、虚部”交替排列。所以每个通道点的FFT数组是float fft_input[2048]; // 1024个点含实部和虚部 for (int i 0; i 1024; i) { fft_input[2 * i] (float)adc_buf[2 * i] - 2048.0f; // 通道0减去直流偏置 fft_input[2 * i 1] 0.0f; // 虚部清零 }为什么减2048STM32F4的ADC是12位无符号模式范围是0~4095中点约2048。FFT适合处理“正负交替”的信号如果直接灌入一个平均值2048的直流信号频谱的0Hz处会出现一个巨高无比的直流谱线不但影响视觉还会影响其他谱峰的相对判断。3.2 测频算法峰值谱线定位用512点复数FFT算1024点实信号时输出结果在0~511的范围内关于256对称实信号共轭对称所以只需要看前N/2个点的幅值。把每个点的幅值算出来找最大幅值对应的索引k则信号频率近似f_estimated k * Fs / N这里Fs是采样率N是FFT点数。例如Fs102400HzN1024频率分辨率是100Hz。最大谱线落在k47那么估算频率就是4700Hz。这是最粗的测法精度受频率分辨率和栅栏效应限制实际误差大致在±50Hz以内。想要提高精度可以在FFT前加汉宁窗Hanning Window窗函数能显著降低频谱泄漏的影响谱峰也更容易分辨。然后做单峰插值——比较峰值谱线和相邻两条谱线的幅值关系估算出“真实峰值”落在两个谱线之间的具体位置。这样原来只能精确到分辨率100Hz的测量可以提升到几个赫兹甚至更好。实测下来配汉宁窗抛物线插值在信噪比不错的情况下测1kHz~20kHz正弦波误差可以压在3Hz以内。3.3 CMSIS-DSP库使用踩坑记录每次调FFT之前确认四件事工程里是否加了arm_cortex-m4f_math.lib而且是否在“Define”里添加了ARM_MATH_CM4和__FPU_PRESENT1。浮点硬件加速是否打开。STM32F4有FPUKeil里要在Options for Target中把FPU设置为Single Precision。FFT计算函数内部需要临时缓冲区如果需要重复调用请确保调用前没有在中断里同时访问同一个缓冲区否则结果就是随机数。输入数据做完FFT后原位保存了计算结果如果下一轮还需要原始时域波形一定要另行备份。3.4 幅值谱和频率值如何对应CMSIS的arm_cmplx_mag_f32函数可以直接从复数结果里算出幅值。注意幅值峰值在几万是很正常的那是ADC原始值直接参与的不需要或者说不可以直接拿“FFT幅值”和电压值对标。想让纵轴显示成“mV”或“dBFS”要换算幅值电压 FFT幅值 / (N/2) * (参考电压 / 4096)这类换算建议放在上位机做嵌入式MCU算起来意义不大还浪费周期。4. 采样率动态切换与波形显示实现4.1 运行时修改采样率的正确姿势采样率切换的本质是改定时器的预分频PSC和重装载ARR。直接改是可以的但要按顺序来关闭触发和DMA或者至少置一个“忙标志”确保当前一轮采集已经停止。清空或者“忽略”当前DMA缓冲区中未处理完的数据。比较稳妥的做法是连续读两次标志位等DMA停了再做下一步。重新计算PSC和ARR写入定时器寄存器然后重新初始化DMA传输计数。延时几个毫秒至少等ADC采样电容稳定再打开触发信号。实际操作时我习惯把“配置定时器频率”封装成一个函数void TIM_SetSampleRate(uint32_t sample_rate) { TIM_TimeBaseInitTypeDef TIM_BaseInitStructure; uint32_t psc, arr; // 选择PSC1然后反算ARR psc 1; arr (TIM_CLOCK_HZ / (psc 1) / sample_rate) - 1; // 停DMA停ADC触发 DMA_Cmd(DMA2_Stream0, DISABLE); TIM_Cmd(TIM2, DISABLE); // 修改定时器分频和重装载 TIM_TimeBaseStructure.TIM_Prescaler psc; TIM_TimeBaseStructure.TIM_Period arr; TIM_TimeBaseInit(TIM2, TIM_TimeBaseInitStructure); // 清状态位重新使能 DMA_SetCurrDataCounter(DMA2_Stream0, BUFFER_SIZE); DMA_Cmd(DMA2_Stream0, ENABLE); TIM_Cmd(TIM2, ENABLE); }核心思路切采样率的过程要像“掉电重启”一样干脆不要省掉停DMA这一步。我见过有人只改定时器DMA还开着结果缓冲区被新老两种采样率的数据拼出一帧频谱图上直接出现两个篮子里的信号。4.2 采样率的上下限可不是随便给的STM32F407的ADC最高可以跑到约36MHzPCLK2为45MHz时除以2得到22.5MHz ADC时钟实际常配置为21MHz或者30MHz。但FFT测频还想精确采样率别顶到ADC上限最好留20%以上的余量。上限更多受到ADC转换时间的限制2个通道扫描、每个通道15个采样周期时一次完整扫描需要约2*(1512)54个ADC时钟周期。此时采样率如果超过ADC时钟除以54触发就会“掉队”表现为DMA缓冲区里数据不是严格等间隔的最麻烦的是这种错误不是一眼能看出来的。下限则取决于信号本身是否能被采样完整。按工程经验采样率至少要是被测信号频率的5到10倍才能让FFT后还有足够多的谱线做插值。比如测50Hz市电工频信号1000Hz的采样率已经足够但测20kHz超声信号就得把采样率调整到200kHz以上。4.3 波形显示串口加上位机最简单STM32F4本地挂屏幕显示当然是最终方案但在开发阶段我强烈建议先走串口上位机。用串口把DMA缓冲区里的原始ADC值发给PCPC上装个Python小脚本用pyqtgraph或者matplotlib实时刷新波形效率高太多。一种常见的帧格式void Send_Waveform_Data(void) { uint8_t header[2] {0xAA, 0x55}; serial_send(header, 2); serial_send((uint8_t*)adc_buf, sizeof(adc_buf)); }上位机读满两字节帧头后按16位数据连续读取就行。注意sprintf格式化输出非常拖时间1000点数据一帧用printf文本发送会直接拖慢采样率。要发就发二进制解析在电脑那边做。如果想直接在单片机上显示波形用TFT屏的FSMC接口DMA2D刷图也行但代码量会明显增加。我的建议是先把串口方案跑通确认FFT测频逻辑正确后再上屏幕不迟。屏幕显示和测频是两回事别把它们搅在一起调。5. 常见问题与排查技巧实录5.1 采样数据时好时坏频谱上出现乱七八糟的谱峰这个现象十有八九是定时器没正确触发。定时器虽然配置了但主模式没有设成“更新事件作为TRGO”或者ADC的外部触发源没选对配置。排查时可以先不用DMA把ADC每次转换结果直接拿串口打印看数据变化是否规律。再用逻辑分析仪抓一下定时器TRGO引脚上的输出波形如果引出来了确认触发频率。最快的检查方法还是对照参考手册把TRGO相关位置从头捋一遍。5.2 两个通道的数据相互串扰表现是通道0接正弦波通道1什么都没接但FFT结果里也能看到相同的频率分量。原因大多是ADC两个通道之间切换时间不够采样电容上的电荷还没完全恢复就开始下一次采集。解决办法就是把采样周期加长比如从15个周期改到84或144周期串扰会明显下降。模拟信号源的内阻也要尽量低内阻越高串扰越重。5.3 测低频信号频率值漂移得很厉害比如测1kHz信号FFT结果在998~1003之间乱跳。此时先看频率分辨率Fs400kHzN1024分辨率是390Hz这就不对劲1kHz信号被一堆谱线糊住测不准是正常的。要么降低采样率比如降到64kHz分辨率到62.5Hz要么加大FFT点数到4096分辨率也能做到几十赫兹。另外低频段受信号本身抖动的干扰更大加窗函数也会有一定改善。低频测频本质上拼的是分辨率和信噪比。5.4 FFT结果总是偶数位置正常奇数位置全是零或者错位这是典型的数据重排方向反了。CMSIS的实数FFT要求输入是“浮点实部和浮点虚部交替”很多第一次写的人把偶数下标填成了真实数据奇数下标填了0但是雷的是数组长度又没给对导致库内部读到了一半的错位数据。检查一下输入数组长度是不是N*2以及循环里下标是不是2*i和2*i1。5.5 DMA数据大方向正确但前几十个点一直是同一个值这多半是上电瞬间DMA还没来得及填充缓冲区还停在复位状态。通常的做法是开启ADCDMA后等一段时间不做任何处理让数据跑上几轮再启动“半传输中断”处理逻辑。也可以在初始化时先把缓冲全部填充0这样即使早期数据不真实也不影响后续处理。5.6 数据总位数和缓冲类型不匹配STM32F4的ADC寄存器是12位但ADC_DR寄存器的格式是32位寄存器中低16位有效。DMA搬运时配置成半字16位搬运接收缓冲类型要用uint16_t而不是uint32_t。有人图省事直接申请了uint32_t数组结果每次搬运的数据位宽和内存布局完全错位所有值都翻倍错乱。这点如果卡住了最优先检查。结尾一点个人实践心得这套方案做下来最大的体会就是FFT测频这件事功夫在FFT之外前面的ADC触发、DMA协调、数据排列要花掉大半调试时间。尤其是“等间隔采样”这条底线只要守住后面FFT怎么算都顺。采样率可变的实现并不复杂真正考验人的地方在切换时机和数据衔接的处理。我个人的建议是先把固定采样率跑通确认FFT结果准确之后再改成动态可调采样率版本逐步加功能这样即使后面出了问题也能快速定位在新增代码里。另外双通道带来的数据拆分问题容易被低估一定先把单通道跑成熟了再扩展到双通道不要一上来就双通道并行调否则两个通道的数据混在一起出错时排查成本会成倍增加。希望这套经过实际验证的方案能帮你少走点弯路。本文还有配套的精品资源点击获取
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/9/9 0:15:28
DS1302 RTC芯片实战:时序、寄存器与PCB布线避坑指南
2026/9/9 0:15:28
医疗小程序跨端开发:Vue3+TypeScript+Uniapp全流程实践
2026/9/9 0:15:28
Delphi/C++ Builder 下 PDFium 黑图排查与源码实战
2026/9/9 0:45:30
低内存STM32上的SM2国密算法实现与优化
2026/9/9 0:45:30
CPU流水线设计实战:Verilog实现插入气泡、重定向与多级嵌套中断
2026/9/9 0:45:30
基于STM32的GPS导航系统设计与NMEA解析实战
2026/9/9 0:45:30
车辆精准搜索实战:Python+ElasticSearch亿级数据检索优化
2026/9/9 0:45:30
FAST-LIVO2解析:直接法紧耦合多传感器融合里程计
2026/9/9 0:40:29
深度解析mattpocock/skills:用指令集让AI编程助手省Token提效
2026/9/9 0:00:26
MHS模型硬件标准:让大模型像调用软件一样控制物理设备
2026/9/9 0:00:27
AI五大核心方向详解:从机器学习到大模型,零基础转行选哪条?
2026/9/9 0:00:27
从50行最小循环到生产级AI引擎:工程化改造全解析
2026/9/8 0:43:11
超人会飞不算本事:系统稳定依赖清晰规则与边界设计
2026/9/8 1:13:27
超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论
2026/9/8 2:18:22
基于CNN的调制信号识别:MATLAB实现时频图分类实战