news 2026/9/10 1:28:49

嵌入式FFT谐波分析实战:从采样率到THD计算的完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
嵌入式FFT谐波分析实战:从采样率到THD计算的完整实现

简介:这份资源以C语言实现FFT快速傅里叶变换,可用于电力系统、音频处理与通信领域的谐波分析,能够计算从基波到第51次谐波的含量,帮助评估非线性负载导致的波形失真。压缩包内共3个文件,包括C源码、配套头文件以及一份Word格式的FFT说明文档,整体仅62KB,轻量易读;源码体现DIT蝶形运算结构,文档则详细解释输入数据准备、参数设置、函数调用方式与结果解读,便于学习者对照源码理解每个环节。已有2827人学习下载。借助该工具,既能掌握FFT算法的C语言实现原理,也能直接用于谐波含量计算与信号频谱分析,同时可作为数字信号处理课程的实验参考,适合嵌入式开发者、电气工程师及高校学生阅读研究。 FFT谐波分析这活儿,玩嵌入式或者电力电子的人早晚都得碰。我前阵子做一台变频器输出端的电能质量评估,甲方上来就问“谐波到几次”“THD多少”,我手里的手持式电能分析仪又刚好不在现场,最后干脆用板子上的MCU直接跑FFT,把51次以内的谐波含量全部算了出来,实测结果和送检报告对得上。今天就把这套做法完整拆开讲一遍。

1. 需求分析与整体方案设计

1.1 为什么必须算到51次谐波

国标GB/T 14549-1993对公用电网谐波的规定是算到50次,IEC 61000-4-7标准也是建议测量到50次谐波。我在实际工程里习惯把上限做到51次,主要原因是想覆盖奇数次的统计便利性:50Hz工频下51次谐波频率是2550Hz,60Hz工频下是3060Hz,这个范围足够覆盖大多数变频器、UPS、开关电源产生的低频传导谐波。

更关键的是,现在的PWM整流器和变频器开关频率普遍在2kHz到20kHz之间,开关频率附近的边带谐波往往落在第40次到第60次频段。如果只算到50次,正好把这些边带漏掉一半,THD(总谐波畸变率)的评估就会有明显偏差。51次不是一个理论推导出来的数,是工程实践中摸出来的保险值。

对于电力电子工程师来说,谐波分析的目标通常就三类:

  • 评估并网电流/电压是否符合标准——需要各次谐波的含有率,核心是幅值精度
  • 排查设备之间干扰——关心谐波频率分布,峰值位置找对了就行
  • 计算THD和功率因数校正——需要从基波到高次谐波的全谱积分

这三种需求决定了FFT实现时的取舍方向:是重精度还是重速度,是算幅值谱还是算功率谱。我在这次项目里三种都要覆盖,所以直接用浮点FFT,避免定点实现带来的动态范围问题。

1.2 FFT方案选型:库函数、IP核还是手写

做FFT谐波分析,代码不是难点,真正的难点在于选对实现途径。目前主流的做法有三条路:

第一,MCU专用DSP库。比如ARM的CMSIS-DSP库,或者TI的DSP库,直接调arm_cfft_f32这类接口,底层已经是优化过的汇编或高度优化的C代码。这是嵌入式项目里性价比最高的方案,我是用了STM32F4系列,主频168MHz,1024点浮点FFT单次跑下来不到1ms,完全满足实时显示的需求。

第二,FPGA的FFT IP核。Xilinx Vivado里的FFT IP核,如果采样点数固定,计算延迟可以压到微秒级,通过AXI-Stream接口输入数据、输出频谱。这个方案适合需要极低延迟或超大点数(8192点以上)的场景,逻辑开发周期比MCU方案长很多,但性能上限完全不同。

第三,纯手写FFT。教学用可以,量产项目不建议。理由是:手写FFT的bug排查成本极高,而且性能很难超过芯片厂商或者MathWorks等专业机构多年的优化成果。我在大学的课程设计里写过一次基2时间抽取FFT,跑通没问题,放到产品里对比CMSIS库函数,同样的1024点,性能差了3倍以上,精度也有差距。

如果要在上位机或者Matlab里做离线分析,那就更简单了,fft()一行的活,但要注意Matlab的FFT输出规则和嵌入式的完全一致,核心是处理好归一化和频谱搬移。

2. 关键参数计算与原理解析

2.1 采样率与FFT点数的匹配计算

FFT谐波分析最容易被忽视、又最决定成败的环节,是采样参数的确定。这里有个标准的计算链条:

第一步:确定最高分析频率。基波50Hz,算到51次谐波,最高分析频率fmax = 51 × 50 = 2550Hz。

第二步:确定采样率。按奈奎斯特采样定理,采样率必须大于2倍最高频率,即fs > 5100Hz。实际工程中我习惯留20%到30%的裕量,同时兼顾ADC的时钟分频便利性,所以取了fs = 12800Hz。这个数值是128的整数倍,对后续整周期采样特别友好。

第三步:确定FFT点数。这里有一个决定频谱分辨率的公式:Δf = fs / N,其中N是FFT点数。频谱分辨率决定了你能不能分清相邻的两个频率分量。对于50Hz基波系统,理想的Δf正好等于50Hz(每个谐波恰好占一根谱线)或者50的整数分之一,这样各次谐波才能落到整数的谱线位置上,避免栅栏效应。

我做过的选型对比:

FFT点数N采样率fs频率分辨率Δf51次谐波所在谱线特点
1286400Hz50Hz第51根最省资源,每根谱线恰为一次谐波
25612800Hz50Hz第102根多4倍频谱细节,分辨率仍对准50Hz
102412800Hz12.5Hz第204根频谱细节丰富,能分辨基频附近的间谐波

我最终选了1024点、12800Hz采样率。原因很简单:工业现场的电网上不只有50Hz基波,还有大量的间谐波和噪声。如果分辨率是50Hz,那基波附近的49Hz、51Hz分量就会被直接抹平,看不出来。用12.5Hz分辨率,至少能看到这些异常分量的轮廓。代价是计算量增大了8倍,但前面说过,CMSIS-DSP跑1024点FFT不到1ms,这个代价完全可以接受。

2.2 频谱泄漏与窗函数选择

FFT的本质是假设被分析的信号是一个无限周期信号的一个周期切片。如果采样窗口内不是整数个基波周期,频谱就会向四周“泄漏”,原本应该在某一根谱线上的能量,散到了旁边很多根谱线上。

解决泄漏问题有两条路:一是做到整周期采样,二是加窗函数。

整周期采样的做法是让采样持续时间正好等于基波周期的整数倍。我上面的参数就是为此设计的:12800Hz采样率,1024点数据,采样持续时间为1024/12800 = 0.08秒,正好是50Hz周期的4倍。这种情况下,不加窗也不会泄漏。

但现实是电网频率会波动,49.8Hz、50.2Hz都是合法的。频率一旦偏离50Hz,整周期就破坏了,还是会泄漏。所以工程上必须加窗。窗函数的选型有个经典对比:

窗函数主瓣宽度旁瓣衰减频率分辨率适用场景
矩形窗-13dB最高整周期采样严格成立时
汉宁窗中等-31dB中等通用电力谐波分析,我最常用
布莱克曼窗-58dB较低强干扰下的弱信号检测

我测试过:在电网频率偏移0.5Hz的情况下,矩形窗测出的基波幅值误差可达2%以上,汉宁窗可以控制在0.5%以内。对谐波分析来说,这个差距已经足够决定THD是否超标了。所以我默认给所有数据加汉宁窗,除非用户明确要求矩形窗做频谱细看。

窗函数不是免费的:它会让主瓣变宽,导致相邻很近的分量区分度下降。好在谐波信号彼此间隔50Hz,汉宁窗的3dB主瓣宽度约为2个谱线间隔(在这个例子里是25Hz),不足以吞并相邻谐波。

3. 嵌入式FFT核心代码与谐波提取实现

3.1 基于CMSIS-DSP的FFT完整流程

我用的STM32F4,采样由ADC+DMA自动完成,ADC采样率和定时器触发精确到12800Hz。外设配置这里不展开,重点讲数据从DMA缓冲区到谐波结果输出的完整链路。

第一步,把ADC的原始数据填入FFT输入缓冲。CMSIS-DSP的arm_cfft_f32要求数据按实部、虚部交替排列,所以我们把采样值放在偶数位,奇数位填0:

#define FFT_SIZE 1024 #define SAMPLING_FS 12800.0f #define FUND_FREQ 50.0f #define HARM_MAX 51 float32_t fft_input[FFT_SIZE * 2]; float32_t fft_mag[FFT_SIZE / 2]; uint16_t adc_buffer[FFT_SIZE]; // DMA自动填充 for (uint16_t i = 0; i < FFT_SIZE; i++) { // 先做去直流:减去一个周期的平均值,避免直流分量淹没低频细节 fft_input[2 * i] = (float32_t)adc_buffer[i] - dc_offset; fft_input[2 * i + 1] = 0.0f; }

这里有一个细节值得注意:dc_offset必须先算出来,而不是等于ADC量程中点。工业现场的信号调理电路可能引入直流偏置,这个偏置会在频谱的0Hz处形成一个大峰值,严重时会影响第1、2根谱线的读数。我是在进入FFT之前维护一个滑动平均来估计直流分量,然后做差。

第二步,加窗。以汉宁窗为例,窗系数需要提前算好存成表格,避免运行时重复计算正弦函数:

static float32_t hanning_win[FFT_SIZE]; void window_init(void) { for (uint16_t i = 0; i < FFT_SIZE; i++) { hanning_win[i] = 0.5f * (1.0f - arm_cos_f32(2.0f * PI * i / (FFT_SIZE - 1))); } } void apply_window(void) { for (uint16_t i = 0; i < FFT_SIZE; i++) { fft_input[2 * i] *= hanning_win[i]; // 虚部为0,不用管 } }

第三步,调用FFT并计算幅值。CMSIS-DSP的FFT函数是原地操作,输入和输出共用同一个缓冲区:

arm_cfft_f32(&arm_cfft_sR_f32_len1024, fft_input, 0, 1); arm_cmplx_mag_f32(fft_input, fft_mag, FFT_SIZE / 2);

arm_cfft_f32最后一个参数1表示做正变换,如果是逆变换则填0。arm_cmplx_mag_f32是求复数模长,也就是对每个谱线计算sqrt(re^2 + im^2),但是注意它的输出长度是FFT_SIZE/2,因为我们只看0到奈奎斯特频率的正频率部分,负频率部分在实数输入的条件下是共轭对称的。

3.2 51次谐波分量的提取与THD计算

频谱算出来后,谐波提取反而是个细致活。由于我设置的采样参数,每个谐波峰值的谱线位置是可以直接计算出来的:

float harm_amp[HARM_MAX + 1]; // harm_amp[0]为直流,实际不使用 for (uint16_t k = 1; k <= HARM_MAX; k++) { // 换算谐波频率在频谱中的索引 float freq_k = k * FUND_FREQ; uint16_t idx = (uint16_t)(freq_k * FFT_SIZE / SAMPLING_FS + 0.5f); // 在该索引附近找局部最大值 uint16_t peak_idx = idx; float max_val = fft_mag[idx]; for (int16_t offset = -2; offset <= 2; offset++) { uint16_t ni = idx + offset; if (ni >= 1 && ni < FFT_SIZE / 2 && fft_mag[ni] > max_val) { max_val = fft_mag[ni]; peak_idx = ni; } } harm_amp[k] = max_val * 2.0f / FFT_SIZE; // 幅值还原 }

这里面有两个容易出错的地方。第一个是2.0f / FFT_SIZE这个归一化系数。FFT输出的幅值与真实幅值之间不是直接的等号关系:如果不归一化,一个1000点的正弦信号经过1024点FFT后,峰值谱线的幅值大约是500,等于信号幅值的N/2倍。所以要除以N/2,也就是乘上2/N。注意这是针对单频正弦信号的峰值幅值,不是RMS值。如果要算有效值,再除以1.414。

第二个是你搜索峰值时不能只取idx这一根谱线。工程电网频率有波动,谐波峰不会严格落在理论谱线上,可能偏了1到2根。我上面代码里在idx ± 2范围内找局部最大,这个方法简单粗暴但非常有效。如果你不想牺牲精度,可以做频域插值,我放到第四章讲。

最后是THD计算。按IEC的定义,THD是各次谐波有效值与基波有效值的比值(这里用幅值代替有效值,因为同一信号下的比例关系不变):

float thd = 0.0f; float sum_harm_sq = 0.0f; for (uint16_t k = 2; k <= HARM_MAX; k++) { sum_harm_sq += harm_amp[k] * harm_amp[k]; } thd = sqrtf(sum_harm_sq) / harm_amp[1] * 100.0f; // 单位为%

算到51次这一点特别重要:如果我按传统的只算到25次,THD可能显示为8%,但算到51次后可能变成11%。这个2到3个百分点的差距,会直接影响设备是“合格”还是“超标”的判定。

4. 实测常见问题与排查经验

4.1 频率偏移下的栅栏效应和插值校正

嵌入式设备测电网,最头疼的就是电网频率波动。我调试时遇到过这么个情况:基波实际是49.8Hz,采样参数是按50Hz设计的,结果频谱图上基波峰值不在第128根谱线(理论位置)上,而是落到128和129之间的某个位置。这时直接取第128根谱线的幅值,会明显偏低——因为能量分散到了相邻谱线上。这就是栅栏效应,只通过离散谱线“看”信号,总有看不见的角落。

解决办法是双谱线插值,原理很简单:取峰值谱线两侧两根谱线的幅值,按权重估算真实峰的位置和幅度。经典的汉宁窗双谱线插值校正公式如下:

设峰值谱线索引为k0,相邻两根谱线的幅值分别为y1和y2(其中y1 >= y2),定义参数β = y2 / y1,那么频率偏移量δ可以通过查表或近似公式求得。汉宁窗下,δ与β之间有近似关系:

δ ≈ (2β - 1) / (β + 1)

真实幅值A = y1 × (2 + δ) / 1.5(汉宁窗的幅值恢复系数略有不同),真实频率f = (k0 + δ) × fs / N。

我实测下来,用这个校正后,在49.5Hz到50.5Hz的偏移范围内,幅值误差从超过2%降到0.3%以内。如果你的系统对精度要求较高,这步不能省。

4.2 我踩过的实战避坑清单

谐波分析这个功能,看着简单,实际跑起来问题一堆。我把踩过的坑整理成了表格,按出现频率排序:

现象根因解决办法
频谱第0根谱线异常高大,后面低频段模糊输入信号含直流偏置先做去直流处理,减去滑动平均
各次谐波峰值偏小,基波误差尤其明显FFT幅值归一化系数不对检查系数是否为2/N,窗函数补偿系数是否引入
频谱出现宽大“裙边”,谐波峰连成一片采样非整周期且未加窗加汉宁窗,或在锁相环控制下进行同步采样
偶数次谐波也很大波形正负半周不对称检查变送器、运放供电和偏置,常见原因是信号调理电路单电源供电
51次以后仍有很多大分量采样率不足或信号本身含有高频振荡确认采样率大于2倍的目标频率,必要时提高采样率分析更高频段
MCU资源占用过高,频谱刷新率不足使用了大点数FFT场景却没用浮点加速使用带FPU的MCU,打开编译器的FPU优化选项,或者改用定点FFT
THD计算结果与商用测试仪对不上只算到某次就截断,或未计入间谐波统一到51次,确认旁瓣泄漏和底噪已经压低

还有一个容易被忽略的点:当作FFT的数据长度不是2的幂时,或者你在做循环缓冲的拼接时,数据不连续会导致频谱出现剧烈的“刺状”噪声。解决方法是确保送入FFT的1024个点,在时间上是严格连续的采样序列,不能有缺帧或重复。

用1024点、12800Hz采样做出来的频谱,如果你用它直接计算功率谱密度,可以用每根谱线的幅值平方除以频率分辨率(在功率谱密度定义为每Hz功率时)。不过对于谐波分析而言,我们通常更关心幅值和功率谱,而不是功率谱密度,因为标准要求的谐波指标以“百分比”为单位。只有做噪声分析时才需要用到PSD的积分概念。

最后再说一个实际调测的技巧:FFT的谐波结果虽然能准确反映电网质量,但网格线上的首个谱峰往往不是基波,而是直流附近的低频干扰。我建议在看频谱前先用一个20Hz的高通滤波器预处理数据,比如用简单的差分滤波器,可以极大改善低频段的信噪比。这个改动成本极低,但效果立竿见影。

上次项目交付后,甲方又拿来一台进口变频器的输出电流做同样分析,我只改了下采样率和基波频率参数,程序半小时内完成适配。这就是把FFT谐波分析做成通用模块的好处——参数可配置,算法不需要跟着硬件换。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 1:26:59

Unet系列分割模型对比实践:从Attention到R2U的科学训练

简介&#xff1a;面向计算机视觉与医学图像分析场景的深度学习资源包&#xff0c;提供Unet、AttentionUnet、R2Unet和R2AUet四种经典分割模型的可运行工程&#xff0c;并配有ISIC 2017皮肤病变数据集局部样本&#xff0c;零基础学习者可按照示例快速跑通&#xff0c;中高级研究…

作者头像 李华
网站建设 2026/9/10 1:26:57

c++ bug

报错&#xff1a; “D:\Build\gdal-3.10.2\INSTALL.vcxproj”(默认目标) (1) -> “D:\Build\gdal-3.10.2\ALL_BUILD.vcxproj”(默认目标) (3) -> “D:\Build\gdal-3.10.2\frmts\gif\gdal_GIF.vcxproj”(默认目标) (38) -> (ClCompile 目标) -> F:\Anaconda3\Librar…

作者头像 李华