news 2026/9/9 0:33:46

STM32F4定时器触发ADC双通道采样+DMA+FFT测频,动态调采样率波形显示

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F4定时器触发ADC双通道采样+DMA+FFT测频,动态调采样率波形显示

简介:一套基于STM32F4的时钟触发ADC双通道采样、DMA传输与FFT测频综合工程,面向需要实现高精度同步采集与实时频谱分析的嵌入式开发者。方案利用定时器触发ADC完成双通道模拟信号采集,经DMA搬运至内存,再通过FFT变换测量频率,同时支持采样频率切换与波形显示,适合实验室信号分析、设备故障诊断等场景。压缩包共341个文件,大小约40.78MB。主体为HAL库驱动的h/c源码、Keil工程文件及hex/axf烧录文件,附带编译过程产生的o/d等中间文件与少量说明文档。源码层级清晰,便于在此基础上改造成自定义采集显示系统。已有630人学习下载,适合有一定STM32基础、希望快速掌握触发采样+DMA+FFT组合使用方法的开发者。从资源中可梳理定时器配置、ADC双通道触发、DMA数据搬运、FFT频谱计算和LCD/上位机波形显示的完整流程;也可直接烧录hex/axf观察运行效果,再结合源码逐模块复现,显著缩短项目开发周期。 玩转STM32F4:定时器触发ADC双通道采样+DMA搬数+FFT测频,还能动态调采样率显示波形

搞嵌入式信号处理的朋友,应该都遇到过这种场景:想测一个未知信号的频率,手头只有一个MCU,没有专门的电平测量仪器。之前有位做设备运维的同行跟我聊,说他们用示波器看现场波形太麻烦,设备装好了不好接线,就想用板子上的STM32直接采集电压信号、算出频率,把波形和频率值实时显示出来。聊到后来,方案就收敛成了这个组合:STM32F4 + 定时器触发ADC双通道采样 + DMA传输 + FFT测频率 + 可变采样频率 + 波形显示。

这套方案听起来链条挺长,但拆开来看,每一步都是嵌入式开发里的高频基本功。我把它完整跑通之后,把里面的关键细节和踩过的坑整理了一下。不管你是要做一个简易频谱分析仪、设备振动监测,还是想把手里的F407、F427开发板用起来学FFT,这篇都能给你一套能直接落地的参考。

1. 整体设计思路:为什么是“定时器触发+DMA+FFT”这个组合

1.1 这个方案解决了什么问题

先捋一下需求链条。我们要做的是把一个连续的模拟电压信号变成数字序列,然后通过FFT算出它的频率。这个过程里最核心的瓶颈不是计算,而是采样。

很多人一开始会用软件触发ADC,也就是在主循环里来回切换通道、启动转换、等标志位、读数据。这样做的第一个问题是抖动。主循环里哪怕每条指令的时间是确定的,中断一进来、优先级一抢,采样间隔就变了。而FFT对采样间隔的一致性极其敏感,间隔抖一抖,频谱上就会出现一堆莫名其妙的旁瓣和杂散。第二个问题是浪费CPU。在1024点采样期间,CPU几乎全耗在等待和搬运上,后续的FFT计算、液晶刷新就没时间做了。

定时器触发ADC,本质上就是把“什么时候采样”这件事交给硬件定时器去管,让ADC的启动时钟和定时器的更新事件严格同步。配置成连续采样之后,ADC转换完一批数据,DMA自动把它们搬到内存数组里,全程不需要CPU干预。这样CPU只在DMA传完一批数据后收到一个完成中断,进去取数、做FFT、刷新显示,处理完了再回主循环干别的。整个数据采集链路里,CPU是被解放出来的。

1.2 为什么选STM32F4而不是F1或G0系列

STM32F4在这个场景里有几个先天优势。首先是主频高,F407默认跑到168MHz,做1024点FFT,用官方DSP库的arm_cfft_f32函数,也就几百微秒的事。F1系列跑72MHz,同样点数要慢一倍以上。其次是F4的ADC支持12位分辨率、多通道扫描模式和DMA连续请求,采样速率理论上能到2.4Msps,常规音频和传感器信号绰绰有余。再一个就是F4集成了硬件浮点单元FPU,做浮点FFT比F1的软件浮点快很多。

当然,如果只是采一个50Hz的工频信号,F1也能跑。但如果你后面想往上扩展,比如做音频频谱显示、振动分析,F4的余量会舒服很多。这套方案的代码移植性也比较好,F4系列之间基本可以平推。

1.3 系统整体框图与数据流向

整个系统的数据流向是这样的:

  • 外部信号经过调理电路(如果需要偏置、衰减、限幅)后,送入ADC的两个输入引脚。
  • 定时器(我用的是TIM2)产生更新事件,作为ADC的外部触发源。每次触发,ADC启动转换,按扫描顺序依次完成两个通道的采样。
  • ADC转换完成后,数据通过DMA自动搬运到一个内存缓冲区,DMA配置成循环模式。
  • 当DMA搬完设定的点数后,触发传输完成中断,在中断服务函数里设置一个标志位。
  • 主循环检测到标志位后,对缓冲区数据做FFT,计算幅度谱,搜索峰值得到频率值,再把原始波形和频谱信息送到LCD显示。

整个过程的关键词就是“硬同步、零拷贝、异步处理”。定时器保证了采样间隔的一致性,DMA保证了数据传输不影响CPU,主循环里的FFT和显示则是一个独立的消费环节。

2. 硬件连接与CubeMX关键配置

2.1 引脚分配和硬件注意事项

我这里用的是STM32F407VET6核心板,信号源接PA0和PA1,对应ADC1的通道0和通道1。如果你用F429或者F401,引脚基本一样,只要是ADC1的通道就行。

在硬件上有一个很重要的点:输入信号的电压范围必须在0到3.3V之间。STM32的ADC不能承受负压,也不能超过VDDA。如果你要测量的是双极性信号(比如交流波形),必须先经过调理电路把信号抬升到1.65V中位附近,否则ADC采到的低半部分会被截掉。没有示波器的时候,最简单的验证方式是先用万用表量一下输入引脚的静态电压,确保在合理范围内。

由于FFT对输入信号的质量比较敏感,建议在ADC引脚对地接一个0.1uF的滤波电容,可以有效减小高频噪声对采样结果的干扰。如果信号源内阻很高,还要考虑加一级运放跟随器做阻抗变换,否则ADC的内部采样电容会在采样的瞬间拉低信号电压,导致采样结果偏小。

2.2 定时器触发源的配置方法

定时器这部分是“可变采样频率”的关键所在。我用的是TIM2,配置成PWM模式或者直接使用更新事件都行。更新的频率决定了ADC的采样率,这个频率由定时器时钟、预分频器和自动重载值三者决定。

F4定时器的时钟来自APB1定时器时钟,通常为84MHz。采样率的计算公式是:

f_s = 84MHz / (PSC + 1) / (ARR + 1)

比如我要采样率32kHz,可以选PSC=0,ARR=2624,即84,000,000 / 2625 ≈ 32000Hz。这里有一点要注意:PSC和ARR都是寄存器值,实际计数值要加1,很多新手第一次算出来采样率差一点,就是因为忘了这个。

要让定时器输出触发信号给ADC,还需要在定时器配置里使能Trigger Output (TRGO),并把触发事件选择为Update Event。CubeMX里的对应选项是Trigger Event Selection,选Update Event,然后在ADC配置里把外部触发源选为Timer 2 Trigger Out event。

2.3 ADC双通道+DMA的关键配置选项

ADC1配置成Scan Conversion Mode,通道数量设为2。转换顺序无所谓,但要记录清楚顺序,后面代码里取数据时要对应上。分辨率选12位,数据对齐方式选右对齐。采样周期我一般设成84MHz下的15个周期,这个值在信号源内阻不高的情况下,对精度影响很小,但能降低整体转换时间,提高可支持的最大采样率。

DMA配置是这个项目里最容易出错的一环。一定要把DMA模式设成Circular(循环模式),数据宽度和外设都是Half Word(16位),内存地址递增。内存缓冲区大小,我建议配置成FFT点数N的两倍。比如FFT是1024点,那么缓冲区至少是2048个半字。为什么要两倍?因为双通道采样,每个采样周期会产生两个数据,分别是通道0和通道1,它们会交替写入缓冲区。缓冲区里数据排列是:ch0[0], ch1[0], ch0[1], ch1[1]……一直到ch0[N-1], ch1[N-1]。

这里有个非常经典的坑:DMA的传输完成中断触发时机。在循环模式下,如果数据长度设为2048(2×1024),那么每次搬完2048个半字才触发一次中断,这正好是双通道各1024点。如果你图省事把DMA长度设成1024,那传输完成时只采了一半数据,FFT点数就不够了。

我个人的习惯是:DMA缓冲区长度BufferSize直接设为1024*2,对应一次完整FFT输入。然后FFT前把偶数下标的数据抽出来作为通道0序列,奇数下标作为通道1序列。当然你也可以用两个独立缓冲区+半传输中断来实现流水线,复杂度高一些,但基本思路一致。

3. 采样频率可变与FFT测频的核心实现

3.1 可变采样频率的设计思路

很多方案里采样频率是写死的,改一次还得重新编译烧录,调试时极其痛苦。我这里做成了运行时可调,通过两个按键实时调整定时器的ARR值,实现采样频率在1kHz到100kHz之间的连续调节。

具体做法是,在按键中断里修改TIM2->ARR寄存器的值,同时更新一个全局变量current_sample_rate,这个变量在FFT计算频率时会用到。这里有一个重要细节:修改ARR后,定时器的更新事件频率立刻改变,但ADC的采样率不会瞬间随之变化,而是对频率值做了延迟更新。实际使用中这个延迟无伤大雅,但如果你在做精确测量,建议修改ARR后等待一个采样周期再开始新的FFT计算。

这里还有一个更安全的做法:在修改ARR之前先停止定时器,改完再启动。因为直接在运行中改ARR,有可能碰到定时器计数到一半改值的情况,导致第一个周期的采样间隔异常。虽然这个异常只影响一个周期,但你要是做高精度测量,这点误差是不能接受的。

3.2 FFT点数的选择与频率分辨率计算

FFT点数选取,本质是频率分辨率和实时性之间的权衡。频率分辨率由公式决定:

Δf = f_s / N

其中f_s是采样率,N是FFT点数。其他条件不变时,采样率越低、点数越大,分辨能力越强。比如采样率16kHz、点数1024,分辨率就是15.625Hz。这意味着两个频率差小于15.625Hz的信号,在频谱上是分不开的。

但点数不能无限增大。首先内存限制,STM32F4的内部RAM虽然不小,但1024点浮点FFT,输入和输出各占1024×4字节,加上中间变量,也还吃得消。到4096点就有点紧了,尤其是在开了LCD缓冲和多个缓冲区的情况下。其次,点数越大,需要的采样时间越长,对于频率快速变化的信号,可能导致测量滞后。

我这个项目里用1024点,因为要显示波形,也需要一定的数据量。如果你只测频率不显示波形,512点就够了,计算更快。频率分辨率不够时,先用高采样率粗测,再用低采样率精测,这种“两步测频法”在工程上很实用。

3.3 FFT计算与峰值搜索的细节

FFT计算用的是ARM官方的CMSIS-DSP库。先做位反转,再调用浮点FFT函数:

arm_status status = arm_cfft_f32(&arm_cfft_sR_f32_len1024, fft_input, 0, 1); arm_cmplx_mag_f32(fft_input, fft_output, 1024);

这个函数的第一个参数是FFT实例结构体,在工程初始化时通过arm_cfft_init_f32函数初始化。转换完成后的fft_input数组里,0号元素是直流分量(实数),第1号元素是频率为Δf的实部和虚部,以此类推。通过arm_cmplx_mag_f32计算幅值后,fft_output[i]就对应频率i×Δf处的幅度。

峰值搜索的大致思路是:从第2个点开始遍历到第N/2-1个点,找到幅度最大的那个下标,频率就是下标乘以Δf。找到之后,我通常还会做一次插值修正,减小栅栏效应带来的误差。最简单的线性插值法是取峰值点及其左右两点,用抛物线拟合出更精确的峰值位置。我实测下来,加了这个修正后,测量误差可以从十几个Hz缩小到1Hz以内。

直流分量那个点(index 0)要排除,因为大多数信号都有直流偏置,它会占据很大的幅度值,但对测频没有意义。如果你用FFT直接用原始波形数据,且波形有直流偏置,你会在0Hz处看到一个巨大的峰值,有时甚至会掩盖附近的低频信号。这一步是新手经常忽略导致测频失败的典型原因。

3.4 波形显示的数据处理技巧

LCD显示波形需要把ADC采样值映射到屏幕坐标上。假设屏幕宽度是240像素,要在屏幕上画1024个点,就必须降采样。不能简单只取前240个点,那样会丢掉中间的大部分波形细节。我一般先把1024个点按每4个点一组取最大值和最小值,得到一个包络序列,再画竖线连接最大值和最小值,这样显示出来的波形更接近示波器的效果。

这里有个小技巧:CPU在画波形时,如果直接用画点函数一个点一个点地画,速度会非常慢。LCD控制器(比如ILI9341)支持画线函数,你可以在每列上,从包络的最小值画到最大值,只需要一次画线操作。这样240列的刷新速度比240×2个单点快了将近一倍。

LCD刷屏也有讲究。整个波形区域大约240×200像素,如果你每次都全屏刷新,即使MCU主频再高,也会有明显的闪烁。更合适的做法是只开一个窗口(Window Address Set),把刷新区域限制在波形显示区域范围内,其他区域不动。用DMA2D传输数据还能进一步提速,但普通应用不开DMA2D也够。

4. 常见问题与排查技巧实录

4.1 ADC采到的数据全是0或最大值

这个问题最常见的诱因有三个:通道配置错误、GPIO模式不对、输入电压超范围。

GPIO必须配置为模拟输入模式,这一点很多新手容易忽略——配置成复用推挽输出,ADC是采不到信号的。其次是确认通道号与引脚对应关系,ADC1_IN0是PA0,ADC1_IN1是PA1,搞反了会采到完全不相关的引脚电平。第三个是输入电压范围,这一点前面讲过,超过3.3V或者低于0V都会导致ADC读数为满量程或零。可以用万用表量引脚电压来排除。

我之前遇到过一个比较隐蔽的情况:PB0和PA0挨在一起,杜邦线插错了一根,导致通道0一直读到通道1的信号。后来我是在读到的序列里发现两个通道波形完全相同才察觉的。建议调试初期先只使能一个通道,确认无输入时读到的是零附近,再接信号。

4.2 FFT测出的频率总是偏大或偏小

频率偏大或偏小,几乎都是采样率设置与实际不符造成的。比如你期望32kHz采样,但定时器实际配置成了32.5kHz,FFT算出来频率自然偏高约1.5%。

排查方法是先看定时器参数。回头核对APB1定时器时钟是否真的是84MHz,有的库函数会自动配置系统时钟,但如果你手动改了时钟树,APB1分频系数没对上,定时器时钟就会差很多。还有一个特别容易忽略的点:CubeMX生成的代码中,定时器初始化时直接写入了PSC和ARR寄存器,但如果你在运行中改了ARR,库函数的时基变量(比如htim2.Init.Period)并不会自动更新,导致之后你读这个变量算采样率时用的是旧值。

我自己的做法是,不用库函数里那些状态变量来算采样率,而是在修改ARR时同步维护一个全局变量cur_sample_rate,所有后续计算都以它为准。这样即使CubeMX生成的代码状态管理有延迟,也不影响实际计算。再用一个已知频率信号(比如信号发生器输出1kHz)做验证,确认读数偏差在可接受范围内。

4.3 DMA中断不触发或数据错乱

DMA中断不触发,第一步检查的应该是DMA长度配置。在双通道扫描模式下,一次采两个数据,但DMA只知道数据长度,不知道通道业务。比如你要每个通道各采1024点,DMA的总长度就必须是2048。如果只设了1024,数据传到一半就触发完成中断了,后续数据根本不会搬入缓冲区。

另一个常见问题是中断优先级配置。DMA中断优先级如果比定时器中断低,且定时器更新事件频繁,DMA中断可能被无限推迟,导致主循环永远等不到标志位。我一般把DMA中断优先级设为最高,至少也要高于可能阻塞的串口中断。

数据错乱还有一种情况是缓冲区越界。DMA在循环模式下,搬完一批后地址自动回到起始位置,这没有问题。但如果主循环在FFT计算期间,DMA又搬了一批新数据进来,缓冲区中后一半的数据就已经被覆盖了。所以你看到频谱“乱跳”,不一定是算法错了,很可能是拿到的数据本身就是新旧混合的。解决办法是保证FFT计算时间小于一次完整采集时间,或者用双缓冲机制:DMA在搬缓冲区A时,CPU处理缓冲区B,两个缓冲区交替使用。这个机制在实时性要求高的场合几乎是必须的,缺点就是内存多花一倍。

4.4 LCD显示波形波动太大,看起来“发毛”

LCD显示不稳定,本质上是大动态信号上叠加了高频噪声。FFT峰值可能没受影响,但波形显示特别难看。解决思路有两个方向:一是数据层面做软件滤波,二是显示层面做余辉效果。

软件滤波我推荐的是滑动平均加中值滤波组合:先做中值滤波去除尖峰脉冲,再做滑动平均平滑波形。代价是会有几个采样点的相位延迟,显示波形会略有“钝化”,但测频结果基本不受影响。

如果想达到数字示波器那种“余辉”效果,实现起来也不难。把历史波形按较低透明度叠加刷新,或者维护一个最大值保持缓冲区,在计算当前帧波形时和上一帧的包络做一个最大值合并。这样既能看到细节波动,又能让画面稳定下来。实际做下来,观感会比单纯画一条实时曲线好得多。

4.5 定时器改频率后采样率跳变异常

这个问题出现在运行时动态修改PSC或ARR的场景。如果你只改了ARR,PSC不动,频率能连续变化。但如果你改了PSC,通常会因为实时值还没生效,出现一次或几次异常采样周期,反映到频谱上就是基频旁边多出一堆毛刺。

我调试时发现的一个规律是:修改ARR相对安全,因为ARR小范围变化时,定时器溢出点随之下一次更新;但修改PSC时,定时器不会立即重启计数器,可能导致当前计数周期长度异常。所以这里我推荐修改采样率时,先调用HAL_TIM_Base_Stop,修改完分频和自动重载值后,再清除更新标志位并重新启动定时器。虽然丢几个采样点,但换来的频率准确度和稳定性是值得的。如果非要无缝切换,那就要组合使用定时器同步和从模式重置功能,复杂度会上一个台阶,一般的嵌入式项目没必要。

5. 实测效果与扩展想法

这套系统搭好之后,我用信号发生器从50Hz到10kHz做过一轮扫频测试。50Hz时因为频率分辨率限制,读数在49~51Hz之间浮动;500Hz以上,读数误差能稳定在1Hz以内。测方波和正弦波都正常,但方波的高频谐波会在频谱上显示出一串峰,如果你只是单纯取幅度最大值,有可能误判为谐波频率。所以后面我加了一个限制条件,只在20Hz到采样率一半范围内搜索基频峰值,并优先选择幅度最大的低频峰。

显示刷新率方面,1024点FFT加上波形绘制,整个周期大概在20ms左右,也就是一秒钟能刷新50帧画面,肉眼看起来完全流畅。如果把FFT点数提高到2048,刷新率会掉到30帧左右,但测频分辨率会提升一倍。这个可以根据场景需要灵活选择。

这套方案后续还可以往几个方向扩展。比如把两个通道分别接到麦克风前置放大器和振动传感器输出端,就能同时做声音和振动分析;也可以通过串口把频谱数据发送到上位机,用Python的matplotlib或者QtChart绘制实时频谱瀑布图。如果对采样率有更高的要求,可以把定时器触发ADC换成高级定时器的中心对齐模式,配合DAC产生扫频激励信号,就能搭出一个低频阻抗分析仪的原型。

对我来说,这个项目最大的收获不是FFT本身,而是把“定时器硬件触发ADC、DMA逐个搬数据、CPU只在关键时刻介入”这种异步流水线思路彻底吃透了。工程里那种一采样CPU就卡死的困境,很大程度上就是没用好DMA循环模式加双缓冲这套组合拳。希望这篇文章也能帮你把这套东西真正跑起来。

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

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

终端AI编程Agent opencode:从安装配置到实战应用全解析

前段时间我把主流的AI编程工具挨个试了一圈,最终日常主力落在了opencode上。这个工具最吸引我的地方,是它把“终端里的AI编程Agent”这件事做得很彻底:开源、Go语言写的单文件二进制、能接任意模型、自带Skills和Memory机制,还可以…

作者头像 李华
网站建设 2026/9/9 0:31:31

DeepSeek Harness插件生态全解析:16个热插件的安装、配置与开发实战

这几天 DeepSeek Harness 的插件仓库更新速度快得离谱,我隔两天去翻一次 release notes 都有种跟不上趟的感觉。好几个技术群里还在传那份“16 个超火插件”的截图,但底下评论第一条永远是:大肥鱼已经落后 N 个版本了。这话听起来像调侃&…

作者头像 李华
网站建设 2026/9/9 0:26:24

内网离线部署Claude Code与Superpowers:从npm包到技能包的完整指南

1. 先把需求看清楚:内网安装的真正链路内网环境安装 Claude Code 和 Superpowers,不是“下载个安装包双击下一步”能解决的。最近医院信息科的同事给了一台 Windows Server 2016 标准版服务器,要求“把 Claude Code 装好,再把 Sup…

作者头像 李华
网站建设 2026/9/9 0:25:51

腾讯开源HY-World 2.0:一句话生成3D游戏场景的实战指南

1. 一句话造世界,HY-World 2.0 到底做了什么 如果你还没听说过 HY-World 2.0,我建议先把这个名字记下来。它是腾讯在 3D AIGC 方向开源的一个项目,核心能力是:输入一句话或者一张图,直接生成一个完整的、可交互的 3D 游…

作者头像 李华
网站建设 2026/9/9 0:22:25

深入理解UVM组件树:构建原理、核心机制与调试技巧

1. 先搞明白:UVM的Hierarchy树到底是什么 接触UVM验证平台的人,几乎每天都会和"层次""树"打交道,但真正把整棵树的来龙去脉说清楚的人并不多。很多初学者搭环境靠的是照抄模板,顶层叫啥、env里挂几个agent、s…

作者头像 李华
网站建设 2026/9/9 0:20:18

手性BIC超表面复现指南:COMSOL仿真全流程与避坑经验

手性BIC超表面这个方向,算是最近几年光子学社区里热度最高的几个话题之一了。原因也简单:BIC能把Q因子做到极高,手性结构又能带来强烈的圆二色性(CD)响应,两个特性组合在一起,在传感、非线性、偏…

作者头像 李华