资讯详情

资讯详情

STM32上实现FFT频谱分析:从CMSIS-DSP库到采样参数配置详解

简介面向 STM32F1 系列嵌入式开发者提供一套基于 Cortex-M3 内核的快速傅里叶变换FFT实现工程解决单片机数字信号处理中时域到频域转换、算法落地与工程调试等实际问题。压缩包内共有 225 个文件约 4.12MB核心为 56 个 C 语言源文件与 60 个头文件覆盖 ADC 信号采集、定时器中断、TFTLCD 显示、I2C 与 CAN 通信等常用外设驱动同时保留 Keil 工程、编译中间文件、烧录文件、链接脚本与批处理工具目录结构清晰方便直接打开工程对照学习或进行二次开发。内容包含 FFT 蝶形运算与位反转实现、CMSIS-DSP 库调用、内存布局、中断触发采样、误差分析和性能优化等关键知识点并配有标准外设库例程能够让读者在真实硬件上完成信号采样、FFT 计算与频谱显示的完整流程。已有 10147 人浏览学习适合希望入门数字信号处理或提升嵌入式算法能力的单片机开发者也可作为课程设计、毕业设计与项目实践的参考资料。 最近带毕设的学弟问我FFT能不能在单片机上跑他手里是一块STM32F103希望采样一段电压信号不连电脑、不上位机直接在板子上算出“信号里有哪些频率分量”。我说能而且核心代码不超过十行——难的不是FFT本身而是采样怎么采、参数怎么配、结果怎么解释。这也是这篇文章想讲清楚的事从为什么优先用ARM官方DSP库到采样率、FFT点数和频率分辨率的配合再到代码级调用、串口传回PC验证结果最后聊聊F1/F4不同芯片的实测表现与选型建议。1. 在STM32上做FFT先分清三条路线和一个核心约束1.1 三条路线怎么选想给STM32加频谱分析能力无非三条路路线实现方式优点难点官方CMSIS-DSP库直接调用arm_cfft_f32等API速度快、稳定性高、API统一需要正确配置宏定义和库文件手写FFT用C语言实现基2蝶形运算对FFT原理理解最透彻开发调试成本高性能难保证硬件FFT加速利用部分MCU内置FFT外设或FPGA IP核计算速度快STM32F1/F4等大多数型号没有通用性差我给学弟的建议很直接工程上优先用CMSIS-DSP库。这不是说手写FFT没意义——想真正理解“蝶形运算”“旋转因子”“位反转”自己写一遍是很好的学习路径。但做实际产品时官方库经过ARM体系结构级别的汇编优化比编译器自动优化出来的C代码快得多而且它的API在F1、F4、F7、H7各系列上完全一致。1.2 核心约束RAM空间和CPU算力很多人在电脑上运行FFT毫无压力一移植到STM32就报内存不足原因在于FFT的空间复杂度是O(N)不假但它需要2倍点数的float数组来存放复数数据。举个例子1024点FFT的输入缓冲是float32_t fft_input[1024 * 2]一个float占4字节光是这个数组就需要1024 * 2 * 4 8192字节也就是8KB。做2048点就是16KB4096点就是32KB。STM32F103C8T6的RAM只有20KB如果是4096点浮点FFT光输入缓冲就超了再加上ADC缓冲、其他全局变量基本跑不起来。因此选FFT点数之前先看一眼芯片的RAM容量这是一个很多新手容易忽略的硬约束。2. 采样参数怎么定采样率、点数与频率分辨率的黄金三角2.1 两条公式决定一切在单片机上做FFT最终算出的频谱能“看清”什么取决于采样率fs和点数N这两个参数它们直接决定频率分辨率频率分辨率 Δf fs / N假设你设置采样率fs 4096Hz做1024点FFT那么Δf 4096 / 1024 4Hz。这意味着频谱上相邻两根谱线之间的频率间隔是4Hz两个频率相差不到4Hz的信号在频谱上会混成一个峰分不开。如果你的信号是100Hz和103Hz两个正弦波叠加用这个配置测出来就是“一个很宽的峰”而不是两个清晰尖峰。另一个铁律是采样定理采样率必须大于信号最高频率的2倍否则会发生频谱混叠。这里要特别强调“大于”而不是“等于”工程上一般取信号最高频率的5到10倍。比如信号最高频率1kHz采样率选5kHz到10kHz比较稳妥。2.2 为什么必须做“等间隔采样”FFT算法的前提是采样点之间的时间间隔完全一致。如果主循环里用软件延时去采ADC中断一多、任务一乱时间间隔就会抖动频谱上会出现很多“莫名其妙的毛刺”。正确的做法是定时器触发ADC DMA搬运定时器产生固定频率的触发事件送给ADC的触发输入。ADC每次触发完成一次转换结果通过DMA自动搬运到内存。DMA搬运满N个点后触发中断在中断里置一个标志主循环检测到标志后开始做FFT。CubeMX里的配置要点是ADC的触发源选择定时器TRGO而不是软件触发DMA模式选正常模式非循环传输长度设为N这样采完一轮后DMA会停止避免主循环还在算FFTDMA又把新数据写进来把缓冲覆盖掉。定时器的分频系数和自动重载值按目标采样率反推即可。假设定时器输入时钟是84MHzF4的APB1定时器时钟想得到比较接近4096Hz的触发频率可以这样配分频PSC 83计数频率 84MHz / 84 1MHz自动重载ARR 243则触发频率 1MHz / 244 ≈ 4098Hz和4096Hz差了不到0.05%对频谱分析影响可以忽略。2.3 别忘了ADC量程问题STM32的ADC输入范围是0到VREF通常3.3V。如果你的被测信号是双极性的比如-1.5V到1.5V的正弦波直接接进ADC引脚会把负半周削掉频谱里会出现很多高次谐波。此时需要加一级偏置电路把信号抬到0到3.3V范围内同时保证幅度不超过ADC量程。这个环节在硬件调试时特别容易踩我曾见过有人调了半天频谱不对最后发现是运放供电电压不对导致偏置点漂移信号削波了。3. 用ARM DSP库跑通第一个FFT代码级拆解3.1 环境准备CubeMX和Keil的配置CMSIS-DSP库的使用方式有两种方式一在STM32CubeMX的Software Packs列表中勾选ARM CMSIS-DSP生成工程时会自动加入源码路径和宏定义。方式二手动在Keil工程里添加库文件。方式二的手动配置需要三步在C/C编译选项的Define里加宏定义。F1系列加ARM_MATH_CM3F4系列加ARM_MATH_CM4如果芯片带FPU还要加ARM_MATH_MATRIX_CHECK可选的这类检查和__FPU_PRESENT1。把CMSIS-DSP的Include头文件路径添加到工程。在工程中加库文件。F1用libarm_cortexM3l_math.libF4带FPU用libarm_cortexM4lf_math.lib末尾的f代表float单精度浮点。3.2 核心调用arm_cfft_f32和arm_cmplx_mag_f32DSP库做FFT的核心调用只有两个函数。下面以1024点为例给出完整逻辑#include arm_math.h #define FFT_SIZE 1024 #define PI 3.14159265358979f float32_t fft_input[FFT_SIZE * 2]; // 复数缓冲偶数下标存实部奇数下标存虚部 float32_t fft_mag[FFT_SIZE]; // 幅值谱 extern volatile uint16_t adc_buffer[FFT_SIZE]; extern volatile uint8_t adc_done_flag; void process_fft(void) { if (!adc_done_flag) { return; } adc_done_flag 0; // 1. 把ADC采样值构造成DSP库要求的复数格式 for (int i 0; i FFT_SIZE; i) { // 假设ADC为12位0~4095对应0~3.3V转成以1.65V为中心的双极性波形 float voltage (float)adc_buffer[i] * 3.3f / 4095.0f - 1.65f; fft_input[2 * i] voltage; fft_input[2 * i 1] 0.0f; } // 2. 执行FFT第4个参数bitReverseFlag传1库内部处理位反转 arm_cfft_f32(arm_cfft_sR_f32_len1024, fft_input, 0, 1); // 3. 计算幅值谱 arm_cmplx_mag_f32(fft_input, fft_mag, FFT_SIZE); // 4. 找峰值对应的频率 uint32_t max_index 0; float max_val 0.0f; arm_max_f32(fft_mag, FFT_SIZE, max_val, max_index); float sample_rate 4098.0f; // 由定时器配置决定 float peak_freq (float)max_index * sample_rate / FFT_SIZE; // 这里可以用串口把peak_freq和谱线发送出去 }这段代码有几个关键点值得说明arm_cfft_f32的第3个参数是方向0表示正变换时域到频域1表示反变换频域到时域。一般我们只用正变换。第4个参数bitReverseFlag传1意思是由库函数帮你做位反转。如果传0输出顺序是错乱的需要自己再处理。arm_cmplx_mag_f32计算的是复数模值也就是sqrt(实部^2 虚部^2)这是窄带信号的幅值谱。3.3 从频域数据到“看得懂”的频率和幅值FFT做完后输出数组的第k个元素对应频率freq(k) k * fs / N比如前面例子中fs 4098Hz、N 1024那么第25根谱线对应频率25 * 4098 / 1024 ≈ 100.05Hz。这是解释频谱最基本的换算关系。幅值的校正要看k取值第0根谱线是直流分量它的真实幅值 fft_mag[0] / N。第1到第N/2 - 1根谱线是正频率部分真实幅值 fft_mag[k] / (N/2)。第N/2到第N-1根谱线是负频率镜像工程上一般不看。如果你对加窗有要求比如测量真实信号时加了汉宁窗交流分量的幅值还需要乘一个恢复系数汉宁窗大约是2倍。这个细节直接关系到你测到的幅度准不准但很多人只关注峰值位置对不对忽略了幅值标定。4. 频谱到手之后怎么验证你的FFT结果是正确的4.1 先用“干净的合成信号”验证算法本身我调试FFT有个习惯先不用真实ADC数据只在代码里构造一个已知的测试信号验证算法链路是否正常。方法很简单在fft_input里填充两个正弦波叠加例如for (int i 0; i FFT_SIZE; i) { float t (float)i / 4098.0f; float sig 1.0f * arm_sin_f32(2.0f * PI * 100.0f * t) 0.5f * arm_sin_f32(2.0f * PI * 300.0f * t); fft_input[2 * i] sig; fft_input[2 * i 1] 0.0f; }这里用了100Hz、1V幅值的正弦波叠加300Hz、0.5V幅值的正弦波。如果FFT正常频谱上应该在100Hz处出现一个幅值约为1.0的峰在300Hz处出现一个幅值约为0.5的峰。为什么这步很有价值因为如果算法代码有bug合成信号就能直接暴露问题而不需要去怀疑外围电路。我自己遇到过两种典型情况一是第0根谱线直流分量特别大其他峰都很矮后来发现是忘了减均值二是峰的位置偏了一两根谱线检查后确认是采样率参数写错。用合成信号快速定位能节省大量时间。4.2 再走实际链路用DAC生成信号ADC采集后做FFT如果条件允许我推荐做一个“板级自测”方案用STM32的DAC输出一个指定频率的正弦波同时用ADC采回来做FFT。这个方法需要芯片带DAC外设比如STM32F103RET6、F401、F407等都有但注意F103C8T6没有DAC。具体做法是用一个正弦查找表通过DMA定时触发DAC输出让它产生一个1kHz的正弦波。DAC输出接到ADC输入引脚ADC按照前面说的定时器触发方式采样再做FFT理想情况下频谱主峰应该在1kHz附近。这样做最大的好处是不依赖外部信号发生器整条链路都是板级验证。如果DAC生成的幅值和FFT测出的幅值对不上经验上先检查DAC的参考电压和ADC的参考电压是否一致再检查是否有偏置误差导致小信号被量化噪声淹没。4.3 串口输出频谱到PC端画图核对单片机上的串口调试信息输出也很重要。我一般会让STM32把谱线以文本形式打出来格式简单即可printf(%d %.2f %.2f\r\n, (int)k, freq, fft_mag[k]);串口助手把这些数据保存成txt文件再用Python或者MATLAB读进来画图就能快速核对频谱形状。需要注意printf的浮点输出在无FPU的F103上比较慢会占用不少CPU时间。如果FFT处理周期要求高建议用microlib或者自己实现一个简单的浮点转字符串函数或者直接只输出整数部分的幅值减少格式化耗时。5. 实测耗时、常见坑与工程选型建议5.1 不同芯片和点数的耗时量级很多读者最关心的问题FFT在单片机上跑一次到底要多久这个问题和主频、Flash等待周期、编译器优化等级、库版本都有关系。以下是我项目实测的经验量级供参考芯片主频是否有FPU1024点浮点FFT耗时STM32F103C8T672MHz无约5ms级别STM32F401CCU684MHz有约0.5ms级别STM32F407VET6168MHz有约0.1ms级别如果你用的是F103且对实时性要求比较高可以考虑改用定点FFT也就是arm_cfft_q15函数。Q15格式是把数据限制在-1到0.9999之间的定点数计算速度比浮点快很多在F103上1024点Q15定点FFT通常能压到0.5ms以内。代价是幅值处理逻辑要绕一点因为Q15的定标关系需要额外换算。我的建议是如果只是做一个低频信号的频谱分析比如电力信号50Hz、振动信号几百Hz浮点版完全够用如果是音频或者需要跑到几kHz甚至几十kHz的信号再考虑定点版。5.2 新手最容易踩的五个坑数组长度搞错。1024点FFT的输入数组长度必须是2048很多人在代码里定义了float fft_input[1024]结果越界写坏其他变量程序跑飞。记住是2N不是N。忘了加DSP库的宏定义。Keil里如果没加ARM_MATH_CM4这类宏编译时会出现大量未定义标识符的报错或者能编译过但链接时找不到库函数。ADC输入负压削波。前面提到过双极性信号直接进ADC会削掉负半周频谱里出现大量谐波第一反应不一定能想到是电路问题。采样间隔不均匀。用了软件延时循环采ADC或者DMA缓冲和FFT缓冲共用导致数据被覆盖结果就是频谱底部抬高出现连续噪声地板。只看峰值不看频率坐标换算。有时调试时看到频谱峰位置对但始终差一点多半是把fs和N的对应关系算错了。检查一下freq k * fs / N这条公式是不是用了正确的fs。5.3 给工程选型的建议做完这个项目后我个人的体会是STM32做FFT难在“怎么把系统搭对”而不是“怎么把FFT算出来”。先画一张参数关系表明确你的信号最高频率、需要的频率分辨率反推出采样率和点数再回过来看芯片RAM能不能承受。算法验证阶段先用合成信号跑通FFT再接真实ADC链路最后再考虑优化实时性问题这个顺序最省事。如果你的项目需要的是功率谱密度而不是幅值谱可以分两步走STM32只管算出幅值谱功率谱密度到PC端再用Python的scipy.signal.welch这类工具继续处理没必要把复杂统计计算全部堆到单片机上。MCU的定位应该是可靠的数据采集和初步频谱计算真正复杂的分析交给上位机这个分工在实际工程里最顺手。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →