ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32 FFT频谱分析与波形识别实战:采样、变换到特征提取

STM32 FFT频谱分析与波形识别实战:采样、变换到特征提取 简介基于STM32的FFT频谱分析与波形识别工程面向嵌入式开发者与信号处理学习者可完成50Hz~200Hz范围内基频及3、5、7次谐波峰值测量并自动区分正弦、方波、锯齿波、三角波。资源包含基础版与可触屏调采样频率的优化版后者能针对不同信号实时调整采样率以改善频率分辨率适合需要兼顾算法实现与界面交互的实战项目。压缩包共395个文件以c/h源码、o/d中间文件、crf编译产物、uvprojx工程配置为主另含hex、map、axf、sct等烧录与链接脚本整体约9.78MB。代码基于正点原子精英版3.5寸TFTLCD烧写即可运行已有3.3万余人学习下载。通过阅读工程可掌握STM32的ADC采样、FFT变换、LCD波形绘制及触屏交互设计同时附带Keil工程备份与批处理脚本便于不同环境下的编译与清理是学习数字信号处理与嵌入式显示的完整范例。 STM32做FFT频谱分析这个话题我前后调了一个多月才算真正跑顺。你要问这个项目到底有什么用简单说就一句话给单片机一双“眼睛”让它不仅能看波形长什么样还能拆出这个波形背后由哪些频率成分组成最后自动判断出这是什么类型的信号。往实际应用上靠音频频谱显示、信号发生器输出自检、电机振动监测、电力谐波分析底层都是这套“采样 FFT 特征识别”的组合。所以这不是一个只能用来交差毕业设计的玩具项目它更像一个随时能塞进各种仪器设备里的核心模块。这篇不打算从头推导傅里叶变换的数学公式那玩意教材里讲得够多了我重点讲从零把“STM32 FFT频谱分析 波形识别”跑起来的过程中真正影响成败的选型思路、参数计算、代码实现和那些文档里不会写的坑。1. 项目拆解这套系统到底由哪些环节构成1.1 三件事采样、变换、识别看标题好像就两个功能实际拆开是三件事采样要保证数据均匀且准确FFT要把时域数据变成频域数据波形识别则要综合时域和频域特征做判决。这三件事层层递进前一步不扎实后面全是错的。我见过不少人一上来就急着调FFT库结果ADC采样间隔乱跳FFT出来的频谱跟撒了一把芝麻一样根本没法看。反过来也有人把FFT结果打印出来就以为完事了完全没想怎么从一堆幅值数据里判定波形类型。其实整个项目的硬骨头不在FFT本身而在“采样链路的稳定性”和“特征怎么提取”这两块。1.2 我最终采用的系统框图整体架构不复杂模拟信号经过简单的偏置和限幅电路送入STM32内部ADC由定时器触发转换用DMA把结果搬到内存攒够一个FFT帧后做浮点变换再把幅值谱送去显示同时跑一个识别函数判断波形类型。软件方面我用的是STM32CubeMX HAL库 CMSIS-DSP这套组合是目前最主流的路径网上资料多出问题也好查。标准库新建工程那套流程我也走过但平心而论CubeMX生成初始化代码的效率要高得多适合把精力集中在核心算法上。开发环境用Keil MDK或者VSCode EIDE都可以关键是把CMSIS-DSP的库加进去这一步后面会细说。2. 选型对比芯片、FFT库和采样链路怎么选2.1 F103还是F407这不是纠结的问题如果你手里有F103的开发板说实话不推荐用在这个项目上。做FFT需要大量的乘加运算尤其是浮点运算。F103主频72MHz没有FPU软件浮点做512点FFT勉强能跑1024点就要几十毫秒加上显示和识别刷新率会掉到每秒几帧卡顿感非常明显。F407就不一样了168MHz主频带单精度硬件FPU和DSP指令跑1024点实数FFT在CMSIS-DSP库的加持下实测大概是百微秒级别完全不影响实时刷新。还有一个容易被忽略的点F407的SRAM有192KBF103只有20KB。做4096点FFT时浮点数组缓冲区就要占用两路N个float4096点大约是32KBF103不但性能跟不上内存空间也直接卡死。如果预算有限退一步选带FPU的G4系列或者F3系列也可以型号不同但思路完全一致。核心结论就一句这个项目尽量别用不带FPU的老型号硬扛。2.2 FFT库的选择与配置FFT实现方式有三个层次自己写基2算法、移植KissFFT等第三方库、直接用CMSIS-DSP。我的建议是直接用CMSIS-DSPST官方在Cortex-M4上做了大量指令级优化自己写的代码很难超过它。CMSIS-DSP里最常用的是arm_rfft_fast_f32专门处理实数序列因为ADC采出来的本来就是实数用这个函数比先构造复数再调arm_cfft_f32要省一半运算。配置时要在工程里添加CMSIS-DSP源码或预编译库用Keil的话在Manage Run-Time Environment里勾选DSP库的Transform和Statistics功能就能自动引入。注意一个版本差异CMSIS-DSP有旧版arm_math.h和新版arm_rfft_fast_instance_f32结构体定义位置有调整之分用新版时有些函数名一样但头文件路径和宏定义不同编译报错多为“找不到arm_math.h”或者“identifier undefined”优先检查是否把DSP库的Include路径加全了。2.3 采样链路为什么要用定时器触发ADCFFT对采样点的间隔均匀性非常敏感。如果靠main循环里的delay去采样循环里每条指令的执行时间不同会产生抖动这些抖动在频域里表现为噪声底座抬高小信号直接被淹没。所以正确的做法是定时器产生更新事件把这个事件接到ADC的外部触发引脚每次触发启动一次采样转换转换完成由DMA自动把结果搬到内存数组。整个过程中CPU不参与数据搬运只等DMA传输完成中断来通知“一帧数据齐了”。这种硬件自动化的方式采样间隔的抖动可以做到纳秒级FFT效果会干净很多。3. 参数计算与核心代码实现3.1 采样率、FFT点数和频率分辨率怎么定玩FFT必须理解三个参数的关系采样率Fs决定能分析的频率范围根据奈奎斯特定理最高分析频率是Fs/2FFT点数N决定频率分辨率相邻两根谱线的间隔是Fs/N采集一帧数据需要的时间就是N/Fs。举个例子我用来做音频频段的实验信号最高频率按20kHz算Fs取48kHz就有富余。N取1024时频率分辨率是48000/102446.875Hz这意味着50Hz和60Hz这种频率根本分不开。如果你要观察工频或低频信号就得把采样率降下来比如Fs2048HzN1024分辨率就是2Hz能很清楚地分辨50Hz和60Hz。内存占用也要提前算1024点FFT需要一个1024点的浮点输入数组和一个1024点的输出数组每个float占4字节总共8KB。再加上幅值数组和其他缓冲最好不要超过芯片SRAM的一半免得堆栈溢出。我用的是512点起步调试通后再升到1024点或者2048点循序渐进比较稳妥。3.2 定时器触发ADC DMA的配置要点CubeMX里的配置顺序有讲究。先把定时器时钟源设为内部时钟然后算预分频PSC和自动重装ARR。以F407主频168MHz、目标采样率48kHz为例168000000 / 48000 3500那么可以取PSC1ARR1749因为168MHz / (11) / (17491) 48000Hz刚好精确命中。之后把定时器的Trigger Output设为Update Event这个TRGO信号就是给ADC用的。ADC侧要关闭连续转换模式外部触发源选择对应的Timer Trigger Out事件采样周期我一般选较短的档位比如15个ADC时钟周期左右既能降低源阻抗要求又不会拖慢最大采样率。DMA配置为Circular循环模式数据宽度设为Half Word因为12位ADC结果存在16位半字里。这里要强调一个检查点ADC的触发方式、DMA的外设地址和内存地址不要搞反。外设地址要填ADC的数据寄存器地址内存地址填你自己定义的数组首地址。很多工程用DMA采出来全是0多半是地址配错或者宽度不一致。3.3 浮点FFT计算与幅值校准代码做完采样配置核心算法代码其实很简短。下面是我实际在用的FFT处理函数关键行都有注释。#include arm_math.h #define FFT_SIZE 1024 static float32_t fft_input[FFT_SIZE]; static float32_t fft_output[FFT_SIZE]; static float32_t fft_mag[FFT_SIZE / 2]; static arm_rfft_fast_instance_f32 fft_inst; void FFT_Init(void) { arm_rfft_fast_init_f32(fft_inst, FFT_SIZE); } void FFT_Process(uint16_t *adc_buf) { uint16_t i; float32_t avg 0.0f; // ADC值转电压并去直流 for (i 0; i FFT_SIZE; i) { fft_input[i] (float32_t)adc_buf[i] * 3.3f / 4095.0f; avg fft_input[i]; } avg / FFT_SIZE; for (i 0; i FFT_SIZE; i) { fft_input[i] - avg; } // 0表示正变换 arm_rfft_fast_f32(fft_inst, fft_input, fft_output, 0); // 输出排列: out[0]直流实部, out[1]Nyquist实部, // out[2]Re1, out[3]Im1, out[4]Re2, out[5]Im2 ... fft_mag[0] fft_output[0] / FFT_SIZE; // 直流 fft_mag[FFT_SIZE / 2 - 1] fft_output[1] / FFT_SIZE; // Nyquist for (i 1; i FFT_SIZE / 2; i) { float32_t re fft_output[2 * i]; float32_t im fft_output[2 * i 1]; // 单边谱除直流和Nyquist外要乘2 fft_mag[i] sqrtf(re * re im * im) * 2.0f / FFT_SIZE; } }这里最容易踩的坑就是输出数据排列格式。很多人把arm_rfft_fast_f32的输出直接丢给arm_cmplx_mag_f32去求幅值结果完全不对。因为这个函数的输出并不是普通复数交错排列而是把直流和Nyquist单独放在前两个位置从第三个元素开始才是Re1, Im1, Re2, Im2这种交错格式。要手动按上面的方式取数。去直流那一步也不能省。硬件上为了测交流信号经常把信号偏置到1.65V这个直流分量如果不减掉会在0Hz处形成一个大尖峰不仅占掉显示量程还可能掩盖低频信号。3.4 加窗处理频谱泄漏的救星在采样频率和信号频率不是整数倍关系时信号截断会产生频谱泄漏表现为真实谱线附近拖出一大片“裙边”。最常用的处理是加汉宁窗。static float32_t window[FFT_SIZE]; void Window_Init(void) { for (uint16_t i 0; i FFT_SIZE; i) { window[i] 0.5f - 0.5f * cosf(2.0f * PI * i / (FFT_SIZE - 1)); } } // 在FFT_Process里、去直流之后执行 for (i 0; i FFT_SIZE; i) { fft_input[i] * window[i]; }加窗是有代价的窗函数会摊宽主瓣降低频率分辨率而且幅度会乘一个系数。汉宁窗的幅度恢复因子是2所以加窗后如果要做精确的幅值测量要在前面的乘2基础上再乘2。不过如果只是看相对频谱形态或者做波形识别乘不乘恢复因子影响不大因为所有谱线被同等缩放。如果信号频率正好能对齐到bin中心比如Fs/N能整除信号频率那不加窗也问题不大。但实际信号总有漂移所以我默认都加窗省心。4. 波形识别算法从特征到判决4.1 先做时域特征峰值系数区分大类FFT结果搞定了接下来是重头戏——怎么识别波形类型。我的方法分两步时域粗判频域细判。时域最有效的特征之一是峰值系数也就是峰值除以有效值Crest Factor。理想情况下正弦波峰值系数是√2约1.414方波是1三角波是√3约1.732锯齿波也是√3左右。只靠这个指标就能把方波和正弦波分得比较干净。计算有效值需要在时域做不能直接用FFT幅值。我一般在ADC数据上算float CalcRMS(uint16_t *adc_buf, float dc_offset) { float sum 0.0f; for (uint16_t i 0; i FFT_SIZE; i) { float x (float32_t)adc_buf[i] * 3.3f / 4095.0f - dc_offset; sum x * x; } return sqrtf(sum / FFT_SIZE); } // 峰值就是整个数组里偏离直流最大的那个值 float peak GetPeak(adc_buf, dc_offset); float crest peak / CalcRMS(adc_buf, dc_offset);还要利用过零检测测出基波频率和FFT最大谱峰对应的频率互相对照。两者一致则说明FFT基频找对了不一致说明可能采到了噪声或者多频信号识别置信度要打折。4.2 频域谐波指纹奇次偶次和衰减规律时域粗判之后再用FFT结果看谐波结构。不同波形的谐波指纹差异很明显我把判断依据整理成一张表波形谐波特征幅度衰减规律峰值系数正弦波基本只有基波谐波很少无约1.414方波只有奇次谐波1/k约1三角波只有奇次谐波1/k²约1.732锯齿波奇次偶次都有1/k约1.732具体做法是找到最大谱峰作为基波记录幅值h1然后在2倍、3倍、4倍……频率处搜索对应谱峰记录h2、h3、h4等。由此计算总谐波失真THD和奇偶次谐波能量比。float fundamental fft_mag[max_index]; float thd 0.0f; float even_energy 0.0f, odd_energy 0.0f; for (int k 2; k 8; k) { int idx (int)(max_index * k); if (idx FFT_SIZE / 2) break; float h fft_mag[idx]; thd h * h; if (k % 2 0) even_energy h; else odd_energy h; } thd sqrtf(thd) / fundamental;4.3 判决流程与容错处理把时域和频域信息合并我用一个简单的决策树判断波形类型顺序是先看峰值系数。如果接近1优先判为方波即使频谱上奇次谐波不全也要信时域的大方向。如果峰值系数接近1.414且THD小判为正弦波。如果峰值系数接近1.732再看频域结构存在明显偶次谐波时判为锯齿波只有奇次谐波时比较相邻奇次谐波衰减速度接近1/k判方波接近1/k²判三角波。这里有个很现实的坑真实方波经过低通滤波或带宽限制后高阶谐波会被削掉看起来会越来越像三角波。所以不要只依赖频域衰减规律一定要结合时域峰值系数和波形包络的斜率判断。我实测中遇到过一次方波和三角波来回跳的情况后来增加了“连续3帧结果一致才更新识别结果”的滤波机制界面才稳定下来。5. 调试实录那些文档里没有的坑5.1 下载器连不上Error: No STM32 target found这个报错几乎是每个人都会被教育一次的经典问题。完整报错是“Error: No STM32 target found! If your product embeds Debug Authentication, please ...”。第一次遇到别慌按下面对照检查SWD两根线SWDIO和SWCLK有没有接反GND是否共地。目标板电压是否正常很多下载失败其实是板子没供电。按住板子复位键的同时点击下载能连上就说明程序里把SWD引脚复用掉了。实在不行就把BOOT0拉高再上电用烧录工具执行全片擦除然后BOOT0拉回低电平。检查一下Keil或CubeIDE里的Debug设置是不是选了Serial Wire有些新建工程默认没勾选管脚全变普通GPIO自然连不上。另外如果电脑上“STM32 Virtual COM Port”设备显示黄色感叹号那是ST-Link的VCP驱动没装好重新装一下STSW-LINK009驱动就解决了不会影响下载但串口打印数据时缺了它还真不行。5.2 频谱图全是噪点或大尖峰频谱一团糟的排查顺序很重要。先确认采样率是否和预期一致。用示波器看定时器触发引脚或者直接从一个已知频率的信号发生器灌1kHz正弦波看FFT最大谱峰是不是正好落在1000Hz附近偏了就是时钟树或者预分频算错。再看FFT结果是不是只显示前半部分了。很多人直接把全部N/2个点画出来结果看到“镜像频谱”。记住实数FFT的结果关于Fs/2对称只需显示0到Fs/2这一段。噪声底座过高的问题重点检查有没有用定时器硬件触发采样以及信号源阻抗是否太高导致ADC采样保持电容充放电不充分。后者可以加一级运放电压跟随器解决。5.3 波形识别误判的常见原因识别误判这一块我见过最多的情况是把带限方波识别成三角波。原因是方波经过前端抗混叠滤波器或者隔直电路后高次谐波衰减严重谱峰衰减规律接近1/k²。我的解决办法是给判定逻辑加一个先验条件先看时域的上升沿陡峭程度如果边沿变化率明显大于三角波的线性斜率即使谐波衰减快也优先判定为方波。还有一个容易被忽略的点ADC采样到的信号有直流偏置时波形识别会失败因为正负半周不再对称峰值系数完全失真。所以识别前一定要把直流偏置减掉而且要在偏置电压稳定的前提下进行。5.4 其他隐患延时卡死和系统稳定性HAL_Delay卡死这个问题在FFT项目里也遇到过。情况通常是在DMA传输完成中断里调用了HAL_Delay而HAL_Delay依赖SysTick中断SysTick优先级如果比DMA中断低程序就死在等待标志位上。解决办法很简单中断里不要调用HAL_Delay用状态机或者变量打时间戳代替。另外如果刷新频谱时发现画面闪烁明显多半是直接在显示函数里做了大量浮点运算导致主循环变慢。合理做法是让FFT和识别跑在主循环显示环节用DMA送显或者用双缓冲交替渲染。做到这一步整个系统才算真正能拿出去用。这个项目做完后我最大的体会是FFT本身不难难的是把采样链路做扎实、把特征阈值调到合适。调试波形识别的那个星期我把四种波形来回灌了上百遍慢慢才摸清不同容差对结果的影响。如果你也想做类似的东西建议先用电脑上的Python或Matlab把FFT和识别算法模拟一遍确认阈值靠谱后再往STM32上移植会省掉很多烧录调试的时间。照着这套方案走你差不多两三天就能看到自己的单片机上显示出一条干净的频谱再花一两天把波形识别调稳这个项目就算真正落地了。本文还有配套的精品资源点击获取
返回列表