☰
STM32 FFT官方库使用指南:从CMSIS-DSP移植到频谱分析实战
2026/10/20 12:52:25 网站建设 项目流程

简介:STM32 FFT官方库压缩包面向嵌入式开发者,尤其适合需要在Cortex-M内核上实现实时信号分析、频谱测量与振动监测等场景的工程师。资源共4个文件,包含2个头文件与2个汇编源文件,压缩包仅17KB,结构精简,可无缝集成到Keil或IAR工程中;汇编文件针对Cortex-M内核做优化,头文件则提供接口声明与FFT数据表,直接调用即可完成256点或1024点FFT运算。已有804人学习下载,说明其在STM32数字信号处理入门与项目中具有较高参考价值。压缩包内提供了官方汇编优化核心与配套接口,既可用于快速验证FFT算法,也能帮助开发者理解底层实现、数据组织规则及调用流程,为后续基于STM32的频谱分析、电机控制、振动检测等应用节省底层开发时间。

1. 什么是STM32 FFT官方库:用它之前先想清楚这三件事

做嵌入式信号处理的都知道,STM32 FFT官方库是很多项目起步的钥匙,一个zip拿回来,解压后却经常不知道从哪个文件夹开始,点开的“Reference Manual”几十页,真正用得上的函数就两三个。这个库本质上是一组被封装好的傅里叶变换函数,覆盖浮点和定点两种路径,背后替你把蝶形运算、旋转因子表和位逆序都做了,目的是让MCU也能跑出实时频谱。但它并是不一个“黑板匣子”——你至少要回答三个问题:用哪个版本,数据怎么填,结果怎么缩放。适合正在做振动监测、音频分析、电机电流谐波提取或电力系统测量的开发者,也适合那些项目DDL已经很近,但还不想从零手写FFT的硬件工程师。

2. 库内部结构与算法细节:决定调用方式的两个关键点

拿到任何“STM32 FFT官方库”类的压缩包,不要把下载的文件当黑匣子直接扔进工程。你得先搞清楚里面是老的查表式FFT实现,还是按CMSIS-DSP规范整理的函数集。这一步决定你后续所有代码的写法,也决定你能不能套用网上的那些例子。这一章把结构和原理说透,往后才能少走弯路。

2.1 老库和 CMSIS-DSP 两条路径怎么选

早期在某芯片厂商的官方例程包里,常见的是以cr4_fft_1024_stm32为代表的旧版FFT函数,核心思路是基4蝶形运算加查表,输入是Q15定点格式。这类函数的优点是代码量小、不需要浮点单元,跑在当时的Cortex-M3内核上很稳定,但缺点也明显:点数被固化成128、256、1024等几个档位,缩放因子的处理靠函数内部移位完成,使用者很难改精度。

后来的工程基本都切到 CMSIS-DSP 规范下的 FFT API,函数名以arm_cfft_f32、arm_cfft_q15、arm_rfft_f32等开头。这套接口在“新代码”里更通用,而且同时提供浮点和定点版本,Cortex-M4、Cortex-M7这类带FPU的内核直接跑浮点很顺手,不带FPU的F1内核也能用Q15定点。判断压缩包属于哪一类最简单的方法:看文件名和函数名。如果里面是stm32_dsp.h加stm32_fft.c,那大概率是旧版;如果出现arm_math.h和一组以arm_开头的源码文件,那就是新版标准库。

我的建议是,新项目别再用旧版查表函数,除非你是要在非常老的固件上打补丁。原因一是CMSIS-DSP持续更新,bug修复和速度优化都在新库;二是新库的API在F1、F4、F7之间基本一致,换芯片不用重写算法。旧版函数的手册和宏配置比较繁琐,点数一多,位逆序和旋转因子的细节就容易出问题。如果你在某个项目里确实看到别人用cr4_fft_1024_stm32,别急着改成新库,先把数据和输出量纲摸清楚再说。

2.2 蝶形运算与数据缓冲布局

FFT官方库没有改变傅里叶变换本身,只是把标准DIT(按时间抽取)或DIF(按频率抽取)的蝶形过程用结构化的循环和查表实现。使用者通常不需要关心内部蝶形展开,但必须明白数据缓冲layout,否则填数据时就会错位。

以最常用的arm_cfft_f32为例,输入输出共用一个交错数组:

float32_t fftBuffer[2 * FFT_SIZE]; // 下标: 0 = real[0], 1 = imag[0], 2 = real[1], 3 = imag[1] ...

每个“样本点”占用两个float32,一个实部一个虚部。一般采集信号是纯实数,所以虚部全部填0,实部放ADC采样值。官方库内部是原位运算,也就是说运行完arm_cfft_f32后,原数组里存的是频域结果,实部和虚部仍然交错存放。如果你在代码里还保留了原时域buffer,那么“时域一备份,频域一备份”的拷贝需要自己负责。

蝶形运算的另一个关键概念是旋转因子。官方库会初始化一个旋转因子表,arm_cfft_init_f32做的事情之一就是计算这些因子。所以不要跳过初始化直接调FFT。很多第一次用的人把arm_cfft_f32当作“一切自动搞定”,结果因为没调用init版本,输出全乱。内存布局上,旋转因子表也会占用一些静态空间,点数越大,表和翻转索引越占RAM,后面做参数评估时要算进去。

一旦理解了交错布局,你自然能明白为什么要用arm_abs_f32或手写sqrt(re*re + im*im)来求幅值,以及为什么数组长度不是FFT_SIZE而是2 * FFT_SIZE。如果看到计算结果所有频率都变成噪声,先回去检查缓存布局,这是最常见的低级错误之一。

3. 快速移植到工程:从添加文件到看到正确频谱

原理清楚了,动手就顺理成章。这章用一个实际例程来演示:把官方库接到一个Cortex-M4内核的工程里,采集N点数据,跑一次FFT,把主峰对应的频率和幅值打印出来。整个过程分三步:加库文件、配置编译宏、写调用代码。新手照着敲一遍就能看到正确的频谱。

3.1 添加库文件和配置编译宏

官方库的代码不是简单地“把zip里的文件丢进工程根目录”就完事。你至少需要把arm_math.h和所有arm_cfft_*相关源文件加进编译路径。我的习惯是把整个CMSIS-DSP/Source目录按需引入,而不是全量添加,因为全部编译会拖慢构建时间,还会让Flash空间接近爆炸。

组织工程时,可以先建一个DSP/文件夹,把需要的源码放进去,然后在IDE的Include Path里添加头文件路径。以常见的MDK或STM32CubeIDE为例,Core/DSP的include路径类似:

DSP/Include DSP/Source/TransformFunctions DSP/Source/SupportFunctions DSP/Source/ComplexMathFunctions

这里不包括MathFunctions,因为FFT通常只依赖TransformFunctions和SupportFunctions。实际需要哪几个文件取决于你的代码,最稳妥是先全量添加,编译后再卸载用不上的文件。

宏配置是另一个频率很高的“坑”。不同内核需要定义不同的宏,否则库会按默认处理器调用错误的指令,轻则编译警告,重则计算乱掉。以Cortex-M4或多边形Cortex-M7内核为例,C/C++ Compiler Preprocessor通常要加:

ARM_MATH_CM4 __FPU_PRESENT=1 ARM_MATH_LOOPUNROLL ABB_MATH_ATAN_FAST

__FPU_PRESENT=1是告诉库硬件浮点单元存在,这样内部会走硬件浮点指令。ARM_MATH_CM4则把库的分支刀到带FPU的优化路径。如果你的芯片是Cortex-M3且不带FPU,通常定义ARM_MATH_CM3,使用Q15版本。宏定义错掉后最常见的现象是arm_cfft_f32的耗时变长好几倍,或者返回结果极其不规律——这不是算法问题,是库被客编译时的CPU分支搞混了每次我看到这种问题,都会先从这些宏重新查一遍。

3.2 初始化、填缓冲和单次FFT例程

下面是一段可自行复制的浮点FFT调用代码,以FFT_SIZE=1024为例。你需要把adc_value换成自己采集到的数据,我用一个模拟正弦波来验证逻辑。

#include "arm_math.h" #include <math.h> #define FFT_SIZE 1024 float32_t fftInput[2 * FFT_SIZE]; // 实部和虚部交织 float32_t fftOutput[FFT_SIZE]; // 幅值结果 void demo_fft(void) { uint32_t i; // 准备一段正弦波:幅值1.0,频率50Hz,采样率1000Hz for (i = 0; i < FFT_SIZE; i++) { fftInput[2 * i] = 1.0f * sinf(2.0f * PI * 50.0f * i / 1000.0f); fftInput[2 * i + 1] = 0.0f; // 虚部为0 } // 初始化旋转因子,只需要调用一次 arm_cfft_instance_f32 fftInstance; arm_cfft_init_f32(&fftInstance, FFT_SIZE); // 执行复数FFT,输入输出共用缓冲 arm_cfft_f32(&fftInstance, fftInput, 0, 1); // 计算每个bin的幅模 arm_abs_f32(fftInput, fftOutput, FFT_SIZE); // 打印前5个bin的模值 for (i = 0; i < 5; i++) { printf("bin[%d]: %.2f\r\n", i, fftOutput[i]); } }

这段代码里有一个在用官方库时容易忽略的细节:arm_cfft_f32的第三个参数是构建形式标志,0表示按正常时间顺序,1表示按逆序。通常用0就可以;如果你想直接弹出一个“物理上频率递增”的频谱,就要理解它其实是在控制输出顺序。如果你用ifftFlag=1做逆变换,第四个参数把“bitReverseFlag”也设为1,否则结果会乱序。

arm_cfft_init_f32的fftInstance只是存了旋转因子和位逆序索引,不是把输入数据搬到新缓冲。所以在多次调用FFT时,只需初始化一次,不必每次重复。数据填进去之后,运行库函数,原来fftInput的时域序列就变成了频域实部虚部。通过arm_abs_f32把复数模提取到fftOutput中,里面第0个bins是直流分量,后面每个bins对应一种频率。

这段代码的幅值和真实幅值之间还需要乘一个折算系数,一般是非直流分量要乘2/FFT_SIZE,直流分量乘1/FFT_SIZE。为什么要这么做,下一章会专门讲。

3.3 用DMA接好AD采集,避免拷贝开销

实际项目里的数据不是用手工填正弦波,而是来自ADC。ADC的DMA连续传输会产生一个环形缓冲,问题在于FFT函数要求数据按“实部虚部交错”布局,且需要一次性传入连续内存。常见做法是让DMA只填实数缓冲区,然后在FFT调用前把实数数据复制到fftInput的交错位置。

#define BUFFER_SIZE 1024 uint16_t dmaBuffer[BUFFER_SIZE]; float32_t fftInput[2 * BUFFER_SIZE]; void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { uint32_t i; // 把DMA转换结果转成浮点,并填充实部,虚部写0 for (i = 0; i < BUFFER_SIZE; i++) { fftInput[2 * i] = (float32_t)dmaBuffer[i] - 2048.0f; // 中心化 fftInput[2 * i + 1] = 0.0f; } run_fft(); }

每个采样点先减去2048(若ADC是12位),是为了把数据居中到0附近,否则直流分量很大,FFT结果的第0个bin会异常高,低幅值谐波容易被淹没。这个拷贝过程看着简单,但在高采样率下会很耗时。为了把开销压下去,下一章的“双缓冲DMA”是很通用的解决方案:两块DMA缓冲,A块在计算时B块继续采集,避免原地等待。需要注意的是,这并不能削减拷贝本身,但可以把它从采集周期里“藏起来”,代价是FFT结果会延迟一个缓冲周期。某些实时性要求特别严的场合,还有人直接把DMA缓冲做成浮点数组,并留一个交错位,让ADC数据直接落到FFT数组里,但这要求DMA外设能输出交错帧,具体看芯片的DMA和定时器的能力。

4. 参数调节与精度取舍:采样率、点数、窗口和归一化

工程里“能跑”不等于“跑得对”。FFT的精度主要受四件事影响:采样率、FFT点数、窗函数和结果归一化。很多人实现了一个能出频谱的程序,但频率偏移、幅值量纲对不上,问题就出在参数搭配。这一章把参数关系拆开说清楚,并给出可以直接抄用的经验值。

4.1 采样率与FFT点数匹配,算清楚带宽和分辨率

频率分辨率delta_f = fs / N,也就是频谱上每个bin之间的间隔。如果你的目标是分辨两个相隔2Hz的分量,那delta_f至少小于2Hz,通常要小于间隔的一半。例如采样率fs=1000Hz,N=512,分辨率就是约1.95Hz;想要0.5Hz分辨率,要么把N撑到2048,要么把采样率降到500Hz。注意:FFT并不是“点数越多越好”,点数翻倍意味着内存翻倍,计算时间也近似翻倍,实时系统的CPU占用会猛增。

下表是常见的几个组合,适用于振动和音频分析:

采样率 fs (Hz)FFT 点数 N频率分辨率 (Hz)最高可分析频率 (Hz)单次采集时长 (s)
10005121.9535000.512
100010240.9775001.024
800010247.81240000.128
800040961.95340000.512
44100102443.066220500.023
44100409610.766220500.093

注意“最高可分析频率”是奈奎斯特频率,也就是fs/2,它并不等于最大“可见”频率。如果信号里有超过fs/2的分量,它们会折叠回低频频谱,形成假峰。所以在采样前必须加模拟或数字抗混叠滤波器,这不是FFT库能帮你补救的。很多振动监测项目把采样率设为最高关心频率的5到10倍,给抗混叠滤波留过渡带。

在设置FFT点数时,还要考虑位逆序和旋转因子表的RAM占用。一个更直接的经验是:先用最常用的1024点数把整条链路跑通,再按频率分辨率的需求去调点数,别一步到位上4096,否则调试时一个普通断点就可能打爆“内存不足”的警告。

4.2 窗函数与幅值校正:不要直接算完就画曲线

FFT是一个有限长度变换,默认的矩形窗会把非整数周期信号截断成“刀切”的形状,在频域表现为主瓣旁边多了很多旁瓣,即频谱泄漏。减小泄漏的通用方法是加窗,而STM32官方库自带的是“把输入点逐点乘上窗系数”,不是像某些库那样在FFT内部自动加窗。所以你的代码要额外准备一张窗表。

汉宁窗是嵌入式最常用的默认窗,公式是:

for (i = 0; i < N; i++) { window[i] = 0.5f * (1.0f - cosf(2.0f * PI * i / (N - 1))); fftInput[2 * i] *= window[i]; }

加窗之后,信号能量会折损一部分,所以直接算出来的幅值会小于真实值。如果不关心绝对幅值,只关心相对峰高,可以不做幅值校正。但如果要做定量测量,比如电机电流谐波有效值,必须修正。常见做法是在计算幅值后乘以一个窗函数的恢复系数:汉宁窗系数大约是1.5到1.6,根据你的定义不同,最稳妥的方法是拿一个已知正旋波测试,反推系数。我一般不用绝对恢复系数,而是用峰值比较法:把一个标准的1V正弦波跑一遍FFT,记录测量峰值,然后用这个测量值去归一化之后的每次结果。这种方法比公式更贴合实际,因为ADC基准电压、增益误差也一并被折进去了。

加窗的隐含代价是“主瓣宽度增大”,两个频率太接近的峰可能合并成一个。取舍就很明显:想要高分辨率,就提高N;想要低旁瓣,就加窗。两者要一起调。

4.3 定点与浮点库的取舍

STM32系列里,有的内核带硬件浮点单元FPU,有的不带。arm_cfft_f32在带FPU的芯片上用起来很快,但放在F1这类内核上会大量调用软件浮点,速度很慢。这时要用arm_cfft_q15或arm_cfft_q31这类定点库。定点库输入的数据格式是Q15,也就是用32768表示整数1.0,原始ADC值需要左移一位再填入实部。

一个典型Q15版本填数写法:

q15_t fftInput[2 * N]; for (i = 0; i < N; i++) { fftInput[2 * i] = (q15_t)((__SSAT(dmaBuffer[i] - 2048, 12)) << 4); fftInput[2 * i + 1] = 0; } arm_cfft_instance_q15 initQ15; arm_cfft_init_q15(&initQ15, N); arm_cfft_q15(&initQ15, fftInput, 0, 1);

左移4位是把12位ADC值提升到16位有效范围,避免精度浪费。定点库计算完毕后的结果也是Q15格式,取出模时仍需做饱和和还原。定点库的优势是计算时间可预测,适合没有FPU且对实时性要求很高的场景;缺点是动态范围小,如果信号中直流很大而目标谐波很小,定点库容易把小分量淹没。遇到这种情况,优先考虑时域上去掉直流,而不是继续加大Q15比例。

浮点库适合带FPU的Cortex-M4/M7芯片,开发方便,精度好,代码易读。如果你在用F4系列,除非你对每个周期的耗时有苛刻要求,否则直接选用arm_cfft_f32是最省心的路径。不过要注意浮点运算中间过程的溢出通常不会立即报错,而是给出Inf或NaN,所以打印结果时先看看有没有奇怪值。我习惯在调试阶段打印fftOutput[0]到fftOutput[5],确认数值范围正常后才开始分析频谱。

5. 避坑与常见问题:线上项目里最容易翻车的五个点

这一章是我把几个项目里的FFT问题集中整理出来的记录,每条都是“现象→原因→解决”的结构,适用于你刚把STM32 FFT官方库接进工程时的排错。很多坑第一次遇到时特折磨,回头想其实就是一句话的事。

5.1 编译链接期的坑:找不到函数和头文件

现象:工程编译时报error: L6218E: Undefined symbol arm_cfft_f32,或者cannot open include file 'arm_math.h'。有时候在某个编译宏下没报错,换个芯片型号就报错,让人很抓狂。

原因:库源文件没有被加入编译列表,仅加了头文件路径;或者arm_math.h里面按ARM_MATH_CM4、ARM_MATH_CM3之类的宏来裁剪函数,宏没定义或定义了多个,导致某些函数没被编译进来。

解决:确认工程中已加入CMSIS-DSP的TransformFunctions、SupportFunctions和ComplexMathFunctions源文件,并且Include Path指向了真正的头文件目录;只定义一个设备相关宏,比如ARM_MATH_CM4,不要同时定义ARM_MATH_CM3。还有一个容易忽略的点:arm_math.h会依赖cmsis_gcc.h等编译器适配头文件,这些文件也要在include路径里。

如果你用CubeMX生成工程,默认可能没有把CMSIS-DSP源文件加入编译,需要在IDE里手动添加或把分组设入“C工程”中。别相信模板直接就有,很多实战项目都要自己加这一步。添加后先编译一个空库测试,再写FFT调用函数,可以避免一堆错误混在一起。

5.2 数据处理精度坑:旁瓣、泄漏和量纲

现象:算出来的频谱,明明输入是单个正弦波,却主峰两侧出来好几个小峰,峰值大小也和输入信号的幅值对不上。有人会怀疑是库的bug,实际是窗和数据处理的bug。

原因:一是没有加窗,信号被矩形窗截断,产生频谱泄漏;二是没做幅值折算,把FFT结果直接当成真值;三是输入数据起点不固定,导致相位跳变,也产生额外旁瓣。还有一个隐蔽原因是ADC数据没有中心化,直流分量很大,频谱的0号bin出息非常高,旁边的1号bin被直流泄漏抬升。

解决:第一步先减均值,把输入信号中心化到0。第二步加汉宁窗,降低截断造成的旁瓣。第三步做幅值校正:对非直流分量,幅值等于sqrt(re*re+im*im) * 2 / N;直流分量则是sqrt(re*re+im*im) * 1 / N。如果有加窗,再乘一个恢复系数。建议用已知幅值和频率的标准正弦波做一次系统标定,记下“代码显示峰值/实际输入幅值”的比值,之后所有测量都除这个比值。量纲问题用这种方法处理最直接,也最靠谱。

5.3 实时性与稳定性坑:卡顿、溢出

现象:采样率不高,但每次进FFT后CPU占用率飙到高位,中间穿插其他任务时频繁发生延时;或者FFT计算中途进入HardFault。

原因:常见的是选了浮点库却忘了开启FPU;或者每次FFT都以超长方式复制数据、调用printf,比如用单次循环memcpy或者把DMA缓冲整个搬来搬去;还有可能是arm_cfft_init_f32传入的实例结构体是栈上的,在中断里初始化,结果栈对齐不足或者生命周期结束后被其他函数覆盖。

解决:FPU要检查启动文件里是否有__FPU_Present宏和FPU_Init()调用,并且在IDE中开启“硬件单精度浮点”。DMA数据传递尽量用双缓冲设计:DMA在A缓冲时跑FFT,下周期DMA写到B缓冲,轮流切换。FFT实例fftInstance建议定义成全局静态变量,并且用ALIGN_HEAD(4)或__attribute__((aligned(4)))对齐到至少4字节,有些内核要求8字节对齐。还有一个小技巧:把printf从高频周期里挪到低优先级任务,否则串口阻塞会把实时性彻底拖垮。如果还慢,就调低N,或在允许的失真前提下改用定点库。

5.4 幅值正确但频率偏移:分辨率低导致频点对不上

现象:正弦波频率是47Hz,FFT输出却在45.5Hz位置有峰,幅值也不稳定。原因是频率分辨率不够,47Hz落在两个bin之间,能量被分到相邻bin,显示出“频谱峰摆错了位置”。这种现象不是库能解决的,纯粹是参数设置问题。

解决:提高N降低delta_f。比如fs=1000Hz、N=1024时分辨率约0.977Hz,47Hz会落在第48个bin附近,仍然不是精确的47Hz。要让频谱峰接近真实频率,需要 N 足够大,或使用“频谱插值”算法,例如对峰值附近三个点做二次插值来估计真实频率。官方库不提供插值,你需要手写一小段抛物线插值,代码很短,能显著改善频偏问题。

5.5 小信号被大直流淹没:堆叠在0号bin的直流泄漏

现象:输入信号里有一个很大的平均分量,比如ADC满码值为4095,而交流幅值只有20码,FFT后所有小信号都被0号bin的旁瓣盖住了,尤其是在加了窗后,0号bin附近的泄漏依旧明显。原因是最初没有对有均值的数据做归一化。解决办法是预处理时减去一个“零点校准值”——通常采集一小段无信号时的ADC均值,然后每次采样都减去该均值。信号中心化后,直流分量及它带来的泄漏就会大大减弱。

如果你不想多花RAM存均值,可以在FFT入口处直接把fftInput[0]对应的时域项减去mean,但这需要知道全局平均值,所以还是得先存一个。更稳妥的方案是硬件上把ADC前端做成偏置到1/2VREF,软件再用一个校准周期来动态修正,这样不同温漂下的直流点都能被削掉。最好的做法是在项目初始化阶段记录一组安静信号的平均值,之后每次采样都减掉它。这一条对电流和电压谐波测量尤为重要。

6. 进阶技巧:用双缓冲和滑动FFT逼近实时频谱监测

前几章已经把单次FFT的用法讲透,真正的工程现场却往往是“持续监测”。这时的核心不是把单次FFT跑得多快,而是怎么让FFT不阻塞采样,同时还能保持频域结果连续。我常用的方案是双缓冲DMA加滑动窗口,整个结构只有三个角色:DMA在乒乓缓冲里填数据,主循环或中断在非总线占用时段执行FFT,结果用于显示或告警。

先看一个简化的状态机:

#define BLOCK_SIZE 1024 static uint16_t adcBuf[2][BLOCK_SIZE]; static int activeBuf = 0; // 当前正在被DMA填充的缓冲 static volatile int readyBuf = -1; // 已完成一个块的缓冲 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (activeBuf == 0) { HAL_ADC_Start_DMA(&hadc, (uint32_t *)adcBuf[1], BLOCK_SIZE); } // 注意:DMA应预先配置为循环模式 activeBuf = 1 - activeBuf; if (readyBuf == -1) { readyBuf = activeBuf ? 0 : 1; } }

这个回调里没有真正计算FFT,只设置readyBuf。主循环会检查readyBuf,把相应缓冲转换为浮点数组并执行FFT。滑动FFT的实现要点是:DMA缓冲区是线性两分块的,每块长度为N,采完一个块后立即开始下一块采集,而上一块的数据在后台处理。每次块首的时间点连续相接,所以结果是“连续块上的频率监视”,但相邻两块的相位可能突变,因为块间没有重叠。如果你需要更平滑的谱图,可以用50%重叠的滑动窗,让相邻块共享N/2点数据,代价是FFT次数翻倍。

void main_loop(void) { if (readyBuf >= 0) { int buf = readyBuf; // 把12位ADC数据转成浮点并中心化 for (uint32_t i = 0; i < BLOCK_SIZE; i++) { fftInput[2*i] = ((float32_t)adcBuf[buf][i] - 2048.0f) * 3.3f / 4096.0f; fftInput[2*i + 1] = 0.0f; } arm_cfft_init_f32(&fftInst, BLOCK_SIZE); arm_cfft_f32(&fftInst, fftInput, 0, 1); arm_abs_f32(fftInput, fftMag, BLOCK_SIZE); // 找到主峰bin uint32_t maxInd; arm_max_f32(fftMag + 1, BLOCK_SIZE - 1, &maxVal, &maxInd); maxInd += 1; // 跳过直流 float32_t freq = (float32_t)maxInd * ADC_FS_HZ / BLOCK_SIZE; readyBuf = -1; } }

上面代码中arm_max_f32我传的是fftMag + 1,也就是不把直流点考虑进去,避免大量直流导致的峰值误判。找到最大值下标后再加1回原索引,然后乘以频率分辨率得到主频。这种简单方法只适合“单频占优”的信号,比如电机转频测量。对于谐波复杂的情况,应该改成遍历fftMag并累积超过阈值的峰,再逐个频率段分析。

最后一个常被问的技巧是有效值(RMS)提取。如果你想得到第k个频率分量的有效值,先用幅值校正得到峰值Vpeak,再除以sqrt(2)得到该频率分量的有效值。如果FFT结果里的幅值本身已经做了2/N校正,那么Vrms = Vpeak / 1.414。我在实际项目中会把这段写成一个函数,传入FFT数组和bin范围,返回该频段的有效值能量。

作为一个每次调试FFT都要强制先跑一遍标准正弦波自检的人,这已经成了我的习惯:先输入幅度已知、频率已知的信号,看FFT输出是否落在预期bin上,幅值偏差是否在1%以内。通过自检后再接真实传感器,那个“幅值对不上却找不到原因”的玄学就会少很多。从那以后我每次换芯片或更新官方库,都强制走一遍单音自检,再把调试串口的打印频率降低,避免打印拖慢采集节奏。希望这些经验能帮你在用STM32 FFT官方库时少走几步弯路。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询