简介面向STM32F4嵌入式开发者提供一套完整的PWM方波FFT谐波分析工程解决信号采样与1至5次谐波幅值计算问题支持蓝牙无线调频。工程基于定时器3实现280kHz可调采样通过FFT分析得到基波及高次谐波数据串口打印结果并配合串口3与蓝牙实现手机控制PWM输出频率适合数字信号处理、电力电子测量及嵌入式调参场景。包体共201个文件压缩包仅1.38MB以h/c源文件、uvprojx工程配置、hex/axf可执行文件、lib库及bat辅助脚本为主可直接打开编译或烧录验证。已有2676人学习下载。配套工程包含完整源码与构建产物读者可据此复现实验低频信号可捕获完整5次谐波高频约100kHz时仍能计算1次与3次谐波若继续提高采样频率性能可进一步提升是研究采样定理与FFT分析边界的实用范例。 去年我在调一台无刷电机的驱动器整机一上电示波器接在PWM输出管脚上波形倒是方方正正但电机就是嗡嗡叫旁边的开关电源EMI也压不下去。我第一反应是换驱动管、调死区折腾了一晚上没效果。后来把波形抓下来离线做FFT才意识到问题根本不在上升沿和下降沿而是PWM方波本身携带的高次谐波能量在作响。从那以后我习惯用STM32F4直接对PWM方波做FFT谐波分析在板子上实时算出1次到5次谐波幅值。这篇文章就是把这套做法完整拆开从采样架构、DSP库调用到窗函数选择和幅值标定踩过的坑也都记录下来给做电机驱动、开关电源、逆变器这类嵌入式开发的朋友一个能直接复用的参考。1. PWM谐波从哪来为什么要上FFT分析PWM信号本质上不是一个“干净”的信号。理想情况下一个周期为T、占空比50%的方波用傅里叶级数展开除了直流分量之外主要包含1、3、5、7次等奇次谐波谐波幅值按1/n的规律衰减占空比一旦偏离50%偶次谐波也会冒出来。这些谐波分量在电机驱动里会转化为铁损和噪声在开关电源里会变成传导干扰在音频设备里则是刺耳的啸叫源。所以做谐波分析并不是为了学术好看而是为了量化“波形到底有多脏”。常规做法是用示波器的FFT功能或频谱分析仪。但示波器自带的FFT点数通常只有几千个点在低频频段分辨率勉强够用想精细观察低频谐波就力不从心了频谱仪精度高但一台不少钱也不可能塞进量产设备里。F4系列自带FPU和DSP指令跑1024点浮点FFT大概百微秒量级完全能做到周期性实时分析。这也是我认为这个项目最有价值的地方不依赖外部仪器单颗MCU就能在闭环系统里持续监测输出波形质量。在做之前目标要定清楚。这里不是要画一个漂亮的频谱图而是要稳定计算基波和2到5次谐波的幅值。这意味着需要关心采样率、采样点数、窗函数和幅值恢复系数每一环都会直接影响最终数据的可信度。2. 采样架构先把信号“洗干净”后面FFT才有意义谐波分析不准很多问题不是出在FFT本身而是信号进ADC之前就已经污染了。这个环节要同时解决三件事采样率够不够、信号电平匹配不当、采样间隔是否严格等时。2.1 采样率和采样点数的选择逻辑假设PWM频率是10kHz要看到5次谐波那就是50kHz。根据奈奎斯特定理采样率至少100kHz但工程上为了给抗混叠滤波留出过渡带常规做法是取谐波上限的3到5倍也就是200kHz到250kHz左右。采样点数决定了频率分辨率df fs / N。比如fs 200kHz、N 1024df约等于195Hz。10kHz的基波和第2次谐波之间相隔10kHz是df的五十多倍完全分得开。但如果PWM频率只有1kHz同样条件下谐波间距只有1kHzdf约195Hz依然能分辨只不过已经逼近窗函数主瓣宽度的影响区域了这时建议增加到2048点。还有一点要提醒采样时间窗要尽量覆盖多个PWM周期而不是只采一个周期。比如1024点200kHz采样时间只有5.12ms对10kHz PWM来说覆盖了51个周期这就为后续使用窗函数或相干采样留下了余地。2.2 电平匹配与抗混叠滤波器F4的ADC满量程是3.3V而PWM信号往往是0到3.3V逻辑电平直接接入ADC貌似没问题。但要注意如果方波幅度直接占满量程1次谐波幅值可能接近2V再叠加电源纹波和噪声很容易触及ADC顶部或底部造成削顶失真。我的做法是用运放做一级缩放和偏置把PWM调整成幅度约1V、中心在1.65V的信号再送入ADC。这样既保证信号在量程中间区域又能留出余量给谐波峰值。抗混叠滤波器很多人会忽略。如果采样率是200kHz理论上高于100kHz的频率分量会折叠回带内而方波的高次谐波频谱是很丰富的所以必须在ADC前加一阶RC低通或者二阶有源低通截止频率设置在100kHz附近。否则高频能量折叠回低频会直接抬高频谱底座让谐波幅值测不准。2.3 定时器触发采样与DMA缓冲区组织这是整个项目里最容易被“图省事”毁掉的一环。不要用主循环里调用ADC转换函数来采样因为循环里任何一条指令的耗时波动都会让采样间隔抖动等效于给信号叠加了相位噪声FFT结果出来频谱底噪会明显抬高。正确做法是用定时器输出触发ADC转换再用DMA把结果搬运到内存缓冲区。ADC配置成扫描模式、定时器更新事件触发、DMA循环模式。缓冲区可以开成两倍长度用DMA半传输中断和传输完成中断来区分前半段和后半段实现“一边采集一边处理”的双缓冲结构。这样CPU不需要关心每个采样点什么时候来只需要在缓冲区切换时启动一次FFT。3. 用CMSIS-DSP库做FFT调用容易解码才见功夫F4跑FFT不需要自己写蝶形运算。ST的CMSIS-DSP库是官方维护的Cortex-M4的FPU和SIMD指令都被充分利用了比自己写的版本代码量少、性能好、可维护性高。Keil环境下通过RTE图形化界面勾选DSP库即可也可以用Pack Manager安装。记得在工程里定义宏ARM_MATH_CM4并且包含arm_math.h头文件。3.1 核心函数调用流程这里我直接给出基于浮点实时FFT的调用代码它以实序列为输入输出压缩格式的频谱数据#include arm_math.h #define FFT_SIZE 1024 arm_rfft_fast_instance_f32 fft_instance; float32_t input[FFT_SIZE]; // 采样缓冲区定时器触发DMA填满 float32_t fft_output[FFT_SIZE]; // FFT输出格式见下文 float32_t mag_output[FFT_SIZE / 2]; arm_rfft_fast_init_f32(fft_instance, FFT_SIZE); // 每采集完一轮调用一次 arm_rfft_fast_f32(fft_instance, input, fft_output, 0); // 0 表示正变换arm_rfft_fast_f32的第四个参数0是正变换1是逆变换。用浮点版本是因为F4带FPU直接用float32_t计算最简单精度也足够虽然q15定点版本更快但处理增益和溢出控制都要自己操心没必要一开始就碰。3.2 输出格式解码最容易踩的坑arm_rfft_fast_f32的输出不是常规的“实部数组虚部数组”而是压缩格式。数据排列是这样的fft_output[0]直流分量实部fft_output[1]1次谐波实部fft_output[2]1次谐波虚部fft_output[3]2次谐波实部fft_output[4]2次谐波虚部以此类推直到奈奎斯特频率分量也就是第k个频谱分量的实部在fft_output[2k - 1]k从1开始虚部在fft_output[2k]而直流分量单独占fft_output[0]。我最初没看仔细直接把输出当成连续复数数组处理算出来的谱线全错位了排查了好久才发现这个格式问题。幅值计算有个固定的换算关系。对k次谐波uint32_t idx 2 * k; // 实际对应fft_output中的位置 float re fft_output[idx - 1]; float im fft_output[idx]; float amplitude 2.0f * sqrtf(re * re im * im) / FFT_SIZE;直流分量的幅值则是fft_output[0] / FFT_SIZE不需要乘2。4. 窗函数不是可选项谐波幅值准不准大半看它FFT成立的隐含前提是采样时间窗刚好是信号周期的整数倍。但在实际系统里PWM频率可能随负载变化或者本身就带载波偏移很难保证每个采样窗口都恰好覆盖整数个周期。一旦不满足能量就会从真实谱线“泄漏”到相邻频点谐波幅值偏小、底噪抬高测出来的数据根本没法看。解决泄漏有两条路。第一条是相干采样动态调整采样率或点数让采样窗长度始终等于基波周期的整数倍。这个方法最理想但实现复杂需要实时测量基波频率再做时钟调整。第二条是加窗函数工程上更常见。我常用汉宁窗。它在主瓣宽度和旁瓣抑制之间比较均衡衰减速度也够看。加窗后信号在时域被切了一刀频谱中峰值幅值会降低所以必须乘一个幅值恢复系数。汉宁窗的恢复系数是2也就是说计算完幅度后再乘以2即可得到接近真实幅值的结果。// 对输入数据加汉宁窗 for (uint16_t i 0; i FFT_SIZE; i) { float32_t win 0.5f * (1.0f - arm_cos_f32(2.0f * PI * i / (FFT_SIZE - 1))); input[i] * win; }不过窗函数不是万能的。它拓宽了主瓣导致相邻谐波间距很近时可能出现谱线交叠。比如采样率200kHz、FFT点数1024时汉宁窗主瓣宽度约为8个bin对应约1.5kHz。如果PWM基波低于1kHz且谐波间距只有1kHz2次和3次谐波就会互相干扰这时需要增大FFT点数或者降低采样率而不是继续依赖加窗。5. 从频谱图到1至5次谐波幅值的完整计算流程频谱数据到手后提取谐波幅值有个标准的操作顺序处理不当容易把相邻谱线的能量也算进来导致结果偏大或偏小。5.1 先定位基波再推谐波位置第一步是找到基波频率对应的bin位置。在频谱中从第2个bin开始避开直流搜索幅度最大值。这个最大值对应的频率就是基波频率记为f1。然后第n次谐波的理论位置就在n倍的基波bin附近。因为存在频谱泄漏和频率偏移我建议在理论位置左右各搜索一个bin取局部最大值作为该次谐波的幅度。uint16_t fund_bin 1; float max_mag 0; for (uint16_t i 1; i FFT_SIZE / 2; i) { // 计算幅度 float mag 2.0f * sqrtf(fft_output[2*i-1]*fft_output[2*i-1] fft_output[2*i]*fft_output[2*i]) / FFT_SIZE; if (mag max_mag) { max_mag mag; fund_bin i; } } for (uint16_t n 1; n 5; n) { uint16_t target_bin (uint16_t)(fund_bin * n); // 在target_bin附近±2个bin内搜索局部峰值 float peak_mag 0; for (int16_t offset -2; offset 2; offset) { int16_t idx target_bin offset; if (idx 0 || idx FFT_SIZE / 2) continue; float mag 2.0f * sqrtf(fft_output[2*idx-1]*fft_output[2*idx-1] fft_output[2*idx]*fft_output[2*idx]) / FFT_SIZE; if (mag peak_mag) peak_mag mag; } // 记录第n次谐波幅值 }如果你的信号加过窗上面计算出的幅度需要再乘以恢复系数。汉宁窗就是乘以2。我个人建议把窗函数的恢复系数和FFT的2/N合并处理写成宏定义避免每个通道都去乘一遍导致混乱。5.2 理论值对照一个50%占空比方波的验证案例一个0到3.3V、占空比50%的方波傅里叶级数理论值是可以手算的。直流分量为1.65V1次谐波幅值约2.10V3次谐波约0.70V5次谐波约0.42V偶次谐波理论上接近0。我把这个信号通过运放调理到ADC可测范围采集1024点采样率200kHz加汉宁窗实测结果和理论值对比如下谐波次数理论幅值V实测幅值V直流1.651.631次2.102.052次00.083次0.700.694次00.055次0.420.41基波和奇次谐波基本吻合误差主要来自ADC量化噪声、窗函数主瓣泄漏和运放本身的非线性。偶次谐波测出来有几十毫伏一方面是因为占空比不是精确的50%另一方面是频谱泄漏的残余这个结果属于正常范围不必过度解读。6. 实测中我踩过的四个坑每一个都让数据难看到怀疑人生6.1 偶次谐波“凭空出现”其实是占空比和泄漏的合谋有段时间我测出来的2次谐波总是达到0.2V以上怎么调窗函数都压不下去。后来用示波器高精度测量PWM占空比发现实际是50.3%而不是理论上的50%。占空比哪怕偏离一点点偶次谐波就会显著抬升。做谐波分析前先确认PWM占空比的真实值如果追求偶次谐波精度占空比误差必须控制在千分之几以内否则所有对偶次谐波的理论分析都是空中楼阁。另外如果采样窗口和PWM周期没有精确对齐泄漏也会把奇次谐波能量扩散到偶次位置这两者要区分开。6.2 ADC采样间隔抖动导致频谱底座整体抬高我最早的一版代码是在定时器中断里启动ADC转换中断服务函数里再把数据读出来放到数组。表面看起来采样率是对的但每次中断进出栈、读取寄存器的时间差异会造成几十纳秒到几百纳秒的采样抖动。FFT对周期性采样抖动很敏感反应到频谱上就是整个底噪抬高甚至出现杂散谱线。后来改成定时器直接触发ADC、DMA自动搬运CPU完全不参与采样过程频谱底噪立刻降了一截。这个坑在数据上最容易识别如果信号是干净方波但频谱底噪超过了-60dBc先查采样链路不要急着怀疑算法。6.3 DMA缓冲区与FFT长度不匹配数据被切开了一半双缓冲模式下DMA半传输中断和传输完成中断各处理半个缓冲区。如果这两个中断里的标志位初始化不对或者FFT读取缓冲区时DMA还在写入就会碰到“这一帧前半段是新的、后半段是旧的”的错位数据频谱上表现为完全混乱的无效谱线。解决办法是在进入FFT计算前检查DMA当前计数寄存器确认数据已经被完整搬运或者干脆在切换缓冲区的时刻禁止DMA传输一个周期把数据锁定住。我最终的做法是开三缓冲区一个用于当前DMA写入一个用于上一轮FFT运算一个用于空闲切换彻底杜绝了读写冲突。6.4 采样率无脑拉满频率分辨率反而崩了有人觉得采样率越高越好直接把ADC跑到2.4Msps结果1024点FFT的频率分辨率高达2343HzPWM基波才10kHz2次谐波和3次谐波之间只有10kHz的间距一个泄漏就能让相邻谐波叠在一起完全没法用。采样率的选取要从谐波上限反推而不是ADC上限。我做10kHz PWM时采样率稳定在200kHz既覆盖了5次谐波又保留了足够的频率分辨率当PWM频率降到1kHz时会把采样率降到40kHz左右目的就是让谐波间距在频谱上拉开倍数距离。7. 调试期的小技巧和后续扩展思路最后分享两个我在调试中觉得特别省事的经验。第一先用一个已知频率和占空比的信号发生器输出方波通过这套代码完整跑一遍把理论值和实测值的偏差范围摸清楚再接入真实的PWM信号。这样可以隔离“信号源本身脏”和“算法有问题”两类因素。第二把FFT计算的基波频率打印到串口和示波器测出来的频率对比如果频率对得上说明采样链路和FFT配置都没问题后面的谐波幅值才有可信度。这套思路稍微改一改就能扩展成更有用的工具把1到5次谐波幅值通过串口或LCD实时显示就能作为电机驱动系统的在线监测模块把谐波幅值超过设定阈值作为故障标志就能做成简单的PWM质量告警。F4的算力富余实测跑1024点浮点FFT加幅值计算一轮只占CPU几个百分比完全不影响主控制逻辑运行。如果你也在做类似的需求建议先把采样链路做扎实再谈FFT和窗函数这个顺序千万别搞反。本文还有配套的精品资源点击获取
