STM32F4定时器触发ADC双通道采样+DMA+FFT测频,动态调采样率波形显示
简介一套基于STM32F4的时钟触发ADC双通道采样、DMA传输与FFT测频综合工程面向需要实现高精度同步采集与实时频谱分析的嵌入式开发者。方案利用定时器触发ADC完成双通道模拟信号采集经DMA搬运至内存再通过FFT变换测量频率同时支持采样频率切换与波形显示适合实验室信号分析、设备故障诊断等场景。压缩包共341个文件大小约40.78MB。主体为HAL库驱动的h/c源码、Keil工程文件及hex/axf烧录文件附带编译过程产生的o/d等中间文件与少量说明文档。源码层级清晰便于在此基础上改造成自定义采集显示系统。已有630人学习下载适合有一定STM32基础、希望快速掌握触发采样DMAFFT组合使用方法的开发者。从资源中可梳理定时器配置、ADC双通道触发、DMA数据搬运、FFT频谱计算和LCD/上位机波形显示的完整流程也可直接烧录hex/axf观察运行效果再结合源码逐模块复现显著缩短项目开发周期。 玩转STM32F4定时器触发ADC双通道采样DMA搬数FFT测频还能动态调采样率显示波形搞嵌入式信号处理的朋友应该都遇到过这种场景想测一个未知信号的频率手头只有一个MCU没有专门的电平测量仪器。之前有位做设备运维的同行跟我聊说他们用示波器看现场波形太麻烦设备装好了不好接线就想用板子上的STM32直接采集电压信号、算出频率把波形和频率值实时显示出来。聊到后来方案就收敛成了这个组合STM32F4 定时器触发ADC双通道采样 DMA传输 FFT测频率 可变采样频率 波形显示。这套方案听起来链条挺长但拆开来看每一步都是嵌入式开发里的高频基本功。我把它完整跑通之后把里面的关键细节和踩过的坑整理了一下。不管你是要做一个简易频谱分析仪、设备振动监测还是想把手里的F407、F427开发板用起来学FFT这篇都能给你一套能直接落地的参考。1. 整体设计思路为什么是“定时器触发DMAFFT”这个组合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可以选PSC0ARR2624即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 Word16位内存地址递增。内存缓冲区大小我建议配置成FFT点数N的两倍。比如FFT是1024点那么缓冲区至少是2048个半字。为什么要两倍因为双通道采样每个采样周期会产生两个数据分别是通道0和通道1它们会交替写入缓冲区。缓冲区里数据排列是ch0[0], ch1[0], ch0[1], ch1[1]……一直到ch0[N-1], ch1[N-1]。这里有个非常经典的坑DMA的传输完成中断触发时机。在循环模式下如果数据长度设为20482×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是PA0ADC1_IN1是PA1搞反了会采到完全不相关的引脚电平。第三个是输入电压范围这一点前面讲过超过3.3V或者低于0V都会导致ADC读数为满量程或零。可以用万用表量引脚电压来排除。我之前遇到过一个比较隐蔽的情况PB0和PA0挨在一起杜邦线插错了一根导致通道0一直读到通道1的信号。后来我是在读到的序列里发现两个通道波形完全相同才察觉的。建议调试初期先只使能一个通道确认无输入时读到的是零附近再接信号。4.2 FFT测出的频率总是偏大或偏小频率偏大或偏小几乎都是采样率设置与实际不符造成的。比如你期望32kHz采样但定时器实际配置成了32.5kHzFFT算出来频率自然偏高约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的场景。如果你只改了ARRPSC不动频率能连续变化。但如果你改了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循环模式加双缓冲这套组合拳。希望这篇文章也能帮你把这套东西真正跑起来。本文还有配套的精品资源点击获取