简介:面向STM32嵌入式开发者的FFT信号分析实践资源,以工程化方式展示如何在STM32上完成ADC采集、时域数据预处理、快速傅里叶变换及频域结果提取,适合需要实现频谱分析、噪声过滤或振动监测的软硬件工程师参考。资源包共999个文件,压缩后约6.21MB,包含C/H源码、汇编启动文件、链接脚本、Keil工程文件、hex/axf编译产物以及CMSIS-DSP数学库文件,可完整还原开发环境并直接对照学习。核心代码涵盖FFT初始化与实数/复数运算表,配合.lib和.a静态库,便于理解底层DSP实现。已有900余人浏览学习。对希望在资源受限MCU上落地FFT的开发者,这份资料提供了从外设配置到算法调用的完整链路,并能借助示例工程快速验证频域分析方案。 把示波器探头夹到传感器输出端,会看到一条不断跳动的时域曲线,但光靠眼睛很难判断这堆信号里到底藏了哪些频率成分。在STM32上做傅里叶变换,就是为了把这堆“毛线”拆开,把每个频率成分单独拉出来看清幅值和频率。这些年从F103到F407、H7,我自己在电机电流采集、音频频谱显示、振动检测这几个方向上都用FFT处理过实测信号,它的工程价值远比教科书里“频谱分析”四个字要大得多。
这篇文章我会从最核心的思路拆解开始,把采样率怎么选、FFT点数怎么定、CMSIS-DSP库怎么配、代码怎么写、幅值怎么标定,以及我实际踩过的坑全部梳理一遍。适合正在用STM32做信号采集、需要从时域波形进一步做频域分析的开发者参考,无论是刚接触数字信号处理的新手,还是已经在项目里用过FFT但觉得结果不理想的老手,都能从里面找到可以直接用的东西。
1. 思路拆解:为什么要在STM32上做傅里叶分析
1.1 从时域到频域,能解决哪些实际问题
时域波形能看出信号的大致形状、幅值和周期,但工程里很多时候问题恰恰出在“看不出”的地方。比如一台电机在额定转速下运行,电流波形看起来就是正弦波,可一旦负载突变或者驱动桥臂老化,电流里会叠加高次谐波,这些谐波在时域上可能只有很小幅度的畸变,肉眼很难发现,但放到频域里就能精确看到250Hz、350Hz这些频率点上出现了额外峰值。
另一个典型场景是测频率。用定时器捕获测量脉冲周期很方便,但面对含有噪声的模拟信号时,边沿抖动会直接影响测量精度。用FFT把信号变换到频域后,找到幅值最大的频率分量,就能得到基波频率,抗噪声能力比过零检测强得多。
STM32现在的主频普遍在几十到几百MHz,加上硬件FPU和DSP指令集,跑FFT已经不是吃力的事。F103做1024点FFT只需要几个毫秒,F407用硬件FPU加速后更快,完全可以在实时数据采集、周期性频谱刷新的需求下稳定工作。相比在示波器上手动看频谱,把FFT直接烧进固件里,意味着设备在现场就能自主完成频域分析和异常判断,这对嵌入式系统的价值非常大。
1.2 算法选型:FFT与DFT,以及STM32上的现成方案
离散傅里叶变换(DFT)的数学定义很直观,对N个采样点,每个频率点都要做N次复数乘加,整体复杂度是O(N^2)。N为256时计算量尚可接受,但N到1024后计算量成倍增长,MCU上跑起来实时性很差。
FFT(快速傅里叶变换)本质上是DFT的高效算法,利用旋转因子的周期性和对称性,把复杂度降到O(N log N)。以1024点为例,DFT大概需要一百万次复数运算,而基-2 FFT只需要约一万次,差距是两个数量级以上,所以工程上大家普遍直接用FFT。STM32上主流的实现方案有三种:
- ARM官方CMSIS-DSP库:针对Cortex-M内核做优化,支持实数FFT和复数FFT,调用简单,文档齐全,这是我最推荐的方式。
- STM32CubeF4/F7/H7固件包自带的DSP库:实际就是CMSIS-DSP的集合版本,与CubeMX联合使用非常方便。
- 开源KissFFT、FFTW(仅限带MMU的高端型号移植):KissFFT是纯C实现,不依赖特定芯片,可移植性好,适合非ARM核心或者不想引入CMSIS的场合。
选库的逻辑无非看三点:计算效率、RAM占用、集成成本。CMSIS-DSP针对Cortex-M的FPU和SIMD指令做了底层优化,性能比普通纯C实现高不少,而且CubeMX里勾选DSP库就能直接调用,所以我目前在STM32平台几乎都用它。
FFT输入数据有实部和虚部,工程中ADC采集到的信号是实数序列,因此可以使用实数FFT。CMSIS-DSP里对应arm_rfft_fast_init_f32和arm_rfft_fast_f32这两个函数,具体用法后面代码部分会说清楚。
2. 硬件与采样准备:数据质量决定分析结果
很多人学FFT时最关注变换过程本身,但实际项目里,频谱结果准不准,八成由采样环节决定,而不是由FFT算法决定。采样率、采样点数、触发方式、信号调理这些如果不到位,后面频谱图再漂亮都是假的。
2.1 采样率、FFT点数与频率分辨率怎么定
FFT输出的频率分辨率计算公式很简单:Δf = Fs / N,其中Fs是采样率,N是FFT点数。也就是说,想把两个靠得很近的频率成分区分开,要么提高N,要么降低Fs,但降低Fs又会压缩可分析的频率范围。
根据奈奎斯特采样定理,采样率必须大于信号最高频率的两倍,工程上一般取最高频率的5到10倍,留出抗混叠滤波器的过渡带裕量。以采集工频50Hz信号、想看5次谐波250Hz为例,最高频率取500Hz比较稳妥,采样率设为5kSps,FFT点数取1024,频率分辨率约4.88Hz,完全能区分50Hz的基波和各次谐波。
这里有一个硬件资源层面的取舍:N越大,RAM占用越高。让FFT输入缓冲为N点float数组,CMSIS-DSP的实数FFT内部还需要临时存储N + 2个float的位置,同时ADC的DMA缓冲至少还要N个点。以1024点为例,仅DMA缓冲加FFT缓冲大概就要12KB以上,F103这类RAM只有20KB的芯片会变得紧张,F407的192KB则非常从容。
我一般建议:如果对频率分辨率要求不高,N选256或512即可,计算快、RAM省;如果要做精细的谐波分析,N选1024或2048,配合F4以上型号的硬件FPU,实时性依然没问题。
2.2 用定时器触发ADC:稳定采样的关键
ADC连续采样然后主循环里Delay读取,是新手最容易踩的坑。问题在于主循环的执行时间受分支判断、中断影响,采样点间隔根本做不到严格相等。FFT要求输入序列是等间隔采样的,如果时间间隔抖动,频谱上会出现额外噪声,严重时会把真实的频率峰值淹掉。
正确做法是使用定时器触发ADC采样,让采样时钟完全独立于CPU。拿通用定时器TIM2举例:配置定时器周期为1/Fs,更新事件触发ADC转换,ADC通过DMA把结果搬运到内存缓冲区,DMA缓冲半满或全满时触发中断,在中断里对已采集的数据做FFT。这样采样间隔由硬件保证,CPU只需要在DMA中断里取数计算,稳定性远超软件延时方案。
定时器周期的计算方法是PSC和ARR的配合。假设APB1定时器时钟为72MHz,想让采样率为20kSps,则周期为50us,可以让PSC取72-1,ARR取50-1,这样定时器计数频率为1MHz,溢出周期正好50us。如果有多个定时器可供选择,优先选与ADC属于同一个时钟域的,避免跨时钟域产生细微偏差。
2.3 信号调理与抗混叠滤波
FFT分析结果要符合实际,输入信号的模拟链路必须先处理干净。首要问题是抗混叠。当信号中存在高于采样率一半的频率分量时,这些高频分量会被“折叠”到低频段,产生原本不存在的假峰值,这是频域分析中最隐蔽的问题之一。
我自己的习惯是,在ADC采样之前加一级RC低通滤波器,截止频率设为Fs/2附近。RC滤波的计算公式是f_c = 1 / (2πRC),以20kSps采样率为例,Fs/2是10kHz,可以选R=1.5kΩ、C=10nF,算下来截止频率约10.6kHz,能把采样范围外的高频分量压掉,也不会对目标频段造成太大衰减。
如果被测信号带有负压,比如交流信号经过电流传感器后是正负交变的,前置电路必须加上偏置,把信号整体抬升到ADC量程中间。常见方案是运放加法器电路,把信号叠加一个1.65V基准(3.3V供电时),再送入ADC输入引脚。量程匹配问题容易被忽略:信号如果超过ADC参考电压会被削顶,低于地会被钳位,这两种情况都会在频谱上产生大量谐波,看起来像是系统问题,实际是采集链路没做好。
3. 代码实现:从ADC数据到频谱图的完整链路
3.1 工程配置与CMSIS-DSP库
在STM32CubeMX中开启所需的ADC、定时器和DMA后,还需要在Middleware and Software Packs或者工具链中确认DSP库已启用。CMSIS-DSP库的添加方式有几种:CubeMX的Software Packs里勾选DSP,或者在Keil的Manage Run-Time Environment里选择CMSIS-DSP,也可以手动把libarm_cortexM4lf_math.a等库文件链接进工程。
配置完成后,代码里要包含的头文件主要是arm_math.h,并且要确保宏定义__FPU_USED和__ARM_FP有被系统正确设置。使用FPU的芯片(如F4、F7、H7)建议在编译器选项里开启单精度硬浮点,否则FFT浮点运算性能会大打折扣。F103这类没有硬件FPU的型号,浮点FFT也能跑,但耗时较长,对实时性要求高的建议改用定点Q15版本接口。
CMSIS-DSP关键的函数接口使用后面有完整示例。初始化FFT实例时,要传入FFT点数,一般只支持16、32、64、128、256、512、1024、2048、4096这些2的幂次值,这也是选用基-2 FFT带来的天然约束。用arm_rfft_fast_f32做实数FFT时,输出序列是复数交替存储格式,想得到幅值谱,需要再用arm_cmplx_mag_f32计算模值。
3.2 核心FFT代码与逐段说明
下面这段代码是我自己在项目里实际使用的结构,针对F407移植,核心逻辑和其他F系列一致。任务是这样的:定时器触发ADC以20kSps采样,DMA搬运1024个点后停止,主循环里对采样数据做FFT并打印前几个谱峰的频率。
#include "arm_math.h" #define FFT_SIZE 1024 #define SAMPLE_RATE 20000.0f // ADC DMA 缓冲,1024点 volatile float32_t adc_buf[FFT_SIZE]; // FFT 输入输出缓冲 float32_t fft_input[FFT_SIZE]; float32_t fft_output[FFT_SIZE * 2]; // 复数输出,实部虚部交替 float32_t fft_magnitude[FFT_SIZE / 2]; arm_rfft_fast_instance_f32 fft_instance; uint8_t new_data_ready = 0; // 假设DMA传输完成中断里调用此函数 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { if (hadc->Instance == ADC1) { // 将DC分量偏置的问题在采集端已经处理好,这里直接拷贝数据 for (int i = 0; i < FFT_SIZE; i++) { fft_input[i] = adc_buf[i]; } new_data_ready = 1; } } void FFT_Init(void) { arm_rfft_fast_init_f32(&fft_instance, FFT_SIZE); } void FFT_Process(void) { if (!new_data_ready) return; new_data_ready = 0; // 1. 实数FFT arm_rfft_fast_f32(&fft_instance, fft_input, fft_output, 0); // 2. 计算幅值(只取前FFT_SIZE/2个频点,正频部分) // fft_output内部格式为 [r0, i0, r1, i1, ...] arm_cmplx_mag_f32(fft_output, fft_magnitude, FFT_SIZE / 2); // 3. 找到最大幅值的频率点 uint32_t max_index = 0; float32_t max_value = 0; for (uint32_t i = 1; i < FFT_SIZE / 2; i++) { if (fft_magnitude[i] > max_value) { max_value = fft_magnitude[i]; max_index = i; } } float freq = (float)max_index * SAMPLE_RATE / FFT_SIZE; printf("Max freq: %.1f Hz, magnitude: %.2f\n", freq, max_value); }几点补充说明:
- arm_rfft_fast_f32的最后一个参数ifftFlag为0表示做正变换,为1做逆变换。
- arm_cmplx_mag_f32计算的是复数模值,输入为交替的实部和虚部,输出为幅值数组,长度是FFT_SIZE/2,对应0到Fs/2的正频谱部分,这部分就是常用的幅度谱。
- 这里没有加窗函数。对于纯正弦信号或者谐波分析需求,加不加窗对峰值位置影响不大;如果信号包含非整周期截断的复杂成分,建议在FFT前对数据加汉宁窗或布莱克曼窗,减少频谱泄漏。
3.3 幅值换算与标定,让频谱读数可信
FFT出来的幅值不能直接当真实电压幅值用。工程上最常用的换算方法有两种:对于单频正弦信号,频谱上该频点的模值经过arm_cmplx_mag_f32计算后,对应信号幅度为2倍,也就是真实峰值等于该点模值除以FFT点数之后乘以2。这个结论来源于FFT定义中幅度谱归一化的推导,可以类比成:能量分散到每个频率点上,总点数参与平均,所以还原真实幅值要把N除掉,又因为双边谱的能量折半到单边谱,所以要再乘2。
用公式表示为:真实幅值 = 2 * fft_magnitude[k] / N。此处N是FFT点数,fft_magnitude[k]是索引k处的模值。如果加了窗函数,还需要额外修正窗函数的幅度衰减系数。比如汉宁窗的幅度修正系数大约为1.6,也就是把上面的结果再乘以1.6,才能让幅值读数和示波器一致。
标定的方法也很直接:用一个已知幅值、已知频率的标准正弦波输入到ADC引脚,比如信号发生器输出1kHz、1V峰值的正弦波,跑FFT后调整系数,直到频谱峰值与1V对应。这样标定一次之后,后续分析的数据就比较可信了。
4. 典型应用实测:谐波分析与频率测量
4.1 电机电流谐波分析
我之前在一台无刷电机驱动项目里,用STM32F407的ADC采集了采样电阻两端的电压,采样率20kSps,FFT点数1024,观察电流频谱。正常运行时,频谱上只在几十Hz基波频率有一个明显峰。后来电机出现“嗡嗡”声,电流波形看起来只是略微变形,但FFT频谱显示基波之外增加了明显的3次和5次谐波分量。
这个现象在时域上很难量化,但频域里可以清楚地看到每次谐波的幅值比例,直接指向驱动MOS管的PWM死区设置和换向时序问题。后来调整了PWM补偿参数,再跑FFT,谐波幅值显著降低。这类诊断用示波器也可以做,但嵌入式端自己实现FFT后,设备可以在运行中持续监测谐波变化,做到异常预警,这是外接仪器不具备的优势。
4.2 音频频率计与频谱显示
另一个适合练手的项目是音频频率计。麦克风或驻极体拾音头经过放大后输出音频信号,ADC采样率通常设10kSps或20kSps,FFT点数512或1024,找到幅值最大频点即可显示声音基频。实测弹吉他单音时,测量误差主要来源于FFT频率分辨率,比如采样率10kHz、点数512,分辨率约19.5Hz,对于100Hz左右的低频音调,误差可能接近半个半音,可通过增大FFT点数来改善。
如果同时做LED频谱显示,可以把FFT幅值数组按频段分组,映射到几根LED柱状条上,效果直观,实时性也好。音频频谱显示用到的代码逻辑和上面的FFT_Process函数几乎一样,只是把最大幅值查找换成频带分组求平均。
这类应用的关键在于,信号输入要避开直流偏置过高的问题。麦克风输出带有较大的直流分量,如果不做隔直或偏置调整,频谱中0Hz处的直流峰值会非常突出,导致其他频点被动态范围压掉。解决方法是输入级加隔直电容,或者在FFT前对数据减去平均值,后一种方法在软件里更容易实现。
5. 常见问题与排查技巧实录
5.1 STM32调试器连不上,目标芯片没找到
这个错误提示很多新人都遇到过:no stm32 target found! 连ST-Link报这类错误时,我先按顺序排查:确认调试器与板子的SWD接口接线没有接反;确认板子有独立供电且电流足够;把BOOT0引脚临时拉高再上电,通常可以绕过用户程序里的低功耗设置或调试脚复用问题。
另一个容易被忽略的点是目标芯片的调试接口被程序复用了。如果代码里把SWD引脚配置成了GPIO用途,第一次烧录成功后,第二次就无法连接。解决办法是调整工程配置,不启用SWD引脚的复用功能,或者用ST-Link Utility里的Connect under reset模式,在芯片复位期间强行连接,再擦除Flash。
调试接口相关的复位电容也不能小看。SWDIO和SWCLK线过长引入的寄生电容,可能导致高频率调试时钟下信号不稳定,从而出现连接失败。此时把ST-Link的时钟频率从4MHz降到1MHz以下,通常就能恢复连接。这类问题看着像芯片坏了,实际往往只是硬件链路细节没处理好。
| 现象 | 可能原因 | 快速处理方式 |
|---|---|---|
| no target found | SWD接线松脱或接反 | 重新压紧杜邦线,检查TXD/RXD方向 |
| no target found | 程序复用了SWD引脚 | BOOT0拉高,用Connect under reset擦除 |
| no target found | 复位引脚电容过大 | 降低调试时钟频率,换短线 |
| 能连上但无法烧录 | 芯片Flash保护开启 | 用STM32CubeProgrammer解除读保护 |
5.2 频谱噪声基底高、毛刺多
FFT频谱上,噪声基底过高或者出现大量无规律毛刺时,第一反应不应该是换算法,而应该是检查采样链路。最常见的原因是采样时间间隔不均匀。确认定时器配置是不是真的触发了ADC,DMA是不是工作在循环模式,中断处理函数里是不是做了太多耗时操作,导致采样数据丢点。换一个思路验证:把采样率降到1kSps,用已知方波或者正弦波输入,观察频谱是否干净。
其次是电源噪声。STM32主板常用的开关电源芯片纹波较大,ADC参考电压如果直接取自给芯片供电的3.3V,频谱上会出现开关频率的杂散峰。解决方法是给ADC参考电压加RC滤波,或者使用内部参考电压源,条件允许时用低噪声LDO单独给模拟部分供电。这个问题在实验室用稳压源供电时很难暴露,一到现场就原形毕露。
最后是窗函数的问题。如果采集到的信号周期不是FFT点数的整数倍,泄漏效应会让主峰旁边出现大片旁瓣,看起来像是更高的噪声基底。对非定频信号或者未知频率信号,养成加汉宁窗的习惯能显著改善频谱可读性。汉宁窗的代价是主瓣变宽,频率分辨率略微下降,但对绝大多数MCU频谱分析场景,这个代价可以接受。
5.3 频率分辨率不够与后续扩展思路
当两个频率很接近的分量被FFT显示成一个峰时,说明分辨率不够。这时优先考虑增大FFT点数。从1024换成2048,分辨率提高一倍,RAM和计算量也会上升。F407这类带FPU的芯片还可以接受,F103就要掂量一下是否值得换平台。
如果无法增大点数,另一个办法是降低采样率。比如只关心0到1kHz频段,没必要用20kSps采样,改成2kSps,同样1024点,分辨率从19.5Hz变成约1.95Hz,提升显著。但前提是输入链路必须加好低通滤波器,把高于新奈奎斯特频率的成分滤掉,否则混叠会比分辨率不足更麻烦。
后续如果想做更复杂的分析,比如谐波总畸变率THD计算、功率谱密度估计、数字滤波配合FFT做频域滤波,STM32的资源也够用。CMSIS-DSP库还提供了很多其他DSP函数,比如定点FFT、FIR滤波、复数运算,可以按需组合。个人在做嵌入式信号处理这条路上,最核心的建议是:先保证采样链路可靠,再追求算法复杂度,因为频谱结果可信的前提永远是你的采样数据可信。最后分享一个自测方法:用固定频率的信号输入到ADC引脚,FFT读出的频率如果稳定在误差范围内,且幅值随输入调整线性变化,就说明整套链路从硬件到软件基本扎实。
本文还有配套的精品资源,点击获取