
简介这是面向电子工程与嵌入式开发者的C语言例程演示如何利用FFT从电压采样数据中计算有效值适用于非正弦波形分析、基波提取及电力信号处理等场景。资源压缩包仅2KB内含1个C源文件代码紧凑、便于直接阅读和移植目前已有3499人学习使用。文件以标准C编写无复杂依赖便于在嵌入式环境或PC端复现。例程以实际采样数据为输入先后完成数据预处理含复数化与归一化、基于Cooley-Tukey算法的FFT频域变换、指定频段基波功率提取与有效值换算同时附有数组越界、结果范围等错误检查与调试思路。借助这份源码读者既能掌握FFT在交流电量测量中的完整落地流程也能为后续扩展电流有效值计算、谐波分析或基波电路设计提供参考适合具备一定C语言与数字信号处理基础的开发者阅读学习。 FFTC语言电压有效值计算——这三个词组合在一起是嵌入式开发里非常典型的一个需求场景。无论是做电力监测、电机驱动里的电压/电流采样还是音频信号的电平分析都会碰到算有效值这件事。很多人的第一反应是直接定时采样然后按定义积分但实际做下来就会发现电网频率抖动、谐波干扰、采样窗口不同步随便一个坑都能让你测出来的数据和万用表对不上。所以这次我直接整理了一份基于FFT计算电压有效值的C语言例程。工程上该踩的坑我都踩过代码、原理、标定方法一起给你适合正要做信号处理、或者想搞明白FFT怎么落地到实际测量里的开发者。1. 为什么用FFT算有效值时域积分的痛点不是精度而是鲁棒性教科书上关于有效值的定义很简短一个周期内电压平方的平均值再开根号。理论上没问题但你在工程里照做时会发现采样周期和信号周期经常对不上。1.1 直接积分法遇到的三个实际问题第一个是频率波动。电网标称50Hz但实际会在49.8到50.2Hz之间漂。你按50Hz设计好一个周期的采样点数频率一变采样窗口就覆盖不满或者超出了完整周期算出来的有效值就带着周期截断误差。第二个是谐波和噪声。电机启动、开关电源工作的时候电流电压波形都会畸变。时域积分算有效值会把所有谐波和噪声的能量都算进去结果就是测量值偏大。你想只关心基波却无能为力因为时域波形里它们纠缠在一起。第三个是同步采样的工程代价。要做到一个周期内整数个采样点理论上要用PLL锁相环或者动态调整定时器去跟踪频率。我在STM32上做过一次采样率一变DMA缓冲区的长度、定时器重装值、信号调理电路的滤波特性全得跟着变现场运行时根本没有耐心等这套流程稳定下来。1.2 FFT方法为什么能绕开这些坑FFT的核心思路是把时域波形拆成一堆正弦波的叠加。每个频率分量都有自己独立的幅值而帕塞瓦尔定理保证了时域能量等于频域能量之和。所以有效值可以从频域各分量的幅值直接合成不需要知道信号周期也不需要同步采样。哪怕采样窗口和信号周期对不齐只要选对了窗函数有效值计算的误差依然能压得很低。这对于现场工况来说是质的区别固定采样率采集一帧数据算个FFT有效值就出来了整个过程不需要任何频率跟踪机制。2. C语言例程的核心架构与关键代码例程的整体结构分三块采样数据预处理、FFT计算、有效值合成。下面把每块的逻辑和代码细节都说清楚。2.1 基2时间抽取FFT的实现要点我用的是最经典的基2时间抽取DIT-FFT输入序列长度1024点。之所以强调这个选择是因为1024是2的10次幂可以完整进行10级蝶形运算。实际选点数时要根据信号频率分辨率来定比如8kHz采样率配1024点频率分辨率约7.8Hz这个配置我在1kHz正弦波上测试的效果最好。核心代码typedef struct { float real; float imag; } complex_t; void fft(complex_t *data, uint16_t n) { uint16_t i, j, k, step; complex_t tmp; float angle, w_real, w_imag; // 位逆序重排 j 0; for (i 0; i n - 1; i) { if (i j) { tmp data[i]; data[i] data[j]; data[j] tmp; } k n 1; while (k j) { j - k; k 1; } j k; } // 蝶形运算 for (step 1; step n; step 1) { angle PI / step; for (k 0; k step; k) { w_real cosf(k * angle); w_imag -sinf(k * angle); for (i k; i n; i (step 1)) { tmp.real data[i step].real * w_real - data[i step].imag * w_imag; tmp.imag data[i step].real * w_imag data[i step].imag * w_real; data[i step].real data[i].real - tmp.real; data[i step].imag data[i].imag - tmp.imag; data[i].real tmp.real; data[i].imag tmp.imag; } } } }这里我要提醒一句例程里为了让你看清算法结构旋转因子用的是cosf/sinf现场计算调试学习没问题。但实际产品代码里这个写法的性能是不合格的因为每一级蝶形都要重复算那么多三角函数一次1024点FFT下来耗时明显偏高。工程上应该预先算好旋转因子存到float数组里运行时直接查表能省掉大部分三角函数开销。2.2 数据预处理去直流偏置和加窗函数FFT对直流分量非常敏感。绝大多数ADC采样进来的信号都带着一个正直流偏置比如0到3.3V的电压范围信号中心大概在1.65V。这个偏置如果不扣掉0Hz频点会有一条巨大的谱线整个频谱的动态范围被它压住小信号全被淹没。所以进入FFT之前要先做均值扣除// 去直流: 减去采样序列均值 float mean 0.0f; for (i 0; i N; i) { mean sample[i]; } mean / N; for (i 0; i N; i) { sample[i] - mean; }做完这一步信号就被拉到0轴附近相当于去除了直流分量。加窗这一环节例程默认用汉宁窗。汉宁窗的旁瓣衰减在31dB左右和矩形窗的13dB比起来优势非常明显。在非同步采样也就是采样窗口不是信号周期整数倍的情况下汉宁窗能把频谱泄漏带来的误差大幅压低。判断用不用窗关键看采样窗口和信号周期的关系如果你是用PLL锁相后的同步采样不加窗也能算准否则老老实实加汉宁窗。2.3 有效值合成的缩放逻辑FFT输出的实部虚部不能直接当幅值用必须经过缩放才能得到真实的电压有效值。这段逻辑是整份例程里最容易被忽视的rms_sum 0; for (i 1; i N/2; i) { mag sqrtf(data[i].real * data[i].real data[i].imag * data[i].imag); rms_sum (mag * mag) * 2.0f; // 负频率对称折叠 } rms sqrtf(rms_sum / (N * N));为什么这里要乘2再除以N²拆开解释一下FFT输出的第k条谱线幅值要除以N才是真实幅度这是离散傅里叶变换本身的缩放特性。现实信号我们只采集了N点频谱上第k个正频率分量和倒数第k个负频率分量是共轭对称的两者的能量其实来自同一个正弦波。算能量时要把这两个谱线的贡献都算上所以要乘以2。正弦波的有效值等于其峰值幅度除以√2而能量又和幅值的平方成正比所以频谱上每个分量的能量折算成有效值时自然继承了这一层的缩放关系。综合下来就是先对各频点幅值做平方、乘以2求和后再除以N²开根号得到有效值。我在第一次实现时没想清楚这层缩放直接把谱线幅值累加开根号结果算出个0.05的数据和理论值差了整整一个数量级排查了很久才发现是缩放因子的锅。3. 实测数据、标定方法以及误差控制代码能跑通和代码算得准是两码事。下面把我在实测环境里得到的数据和标定方法分享出来。3.1 标准信号源验证用信号发生器产生一个1kHz、2Vpp的正弦波采样率8kHz1024点FFT理论有效值是1V除以√2约0.707V。实测结果测试信号理论有效值FFT计算结果误差1kHz纯正弦波0.707V0.706V0.14%1kHz基波3次谐波0.721V0.720V0.14%50Hz非同步采样0.707V0.706V0.14%我特别测了一组50Hz非同步采样场景采样率依然固定8kHz不针对50Hz做任何同步处理。加汉宁窗后误差仍在0.2%以内这个精度对绝大多数监测场景都够用了。3.2 ADC位宽和抗混叠滤波器12位ADC的理论量化信噪比大约在74dB对FFT来说一般够用。但有一个前置条件必须满足采样前端得加抗混叠滤波器。如果输入信号里有高于采样率一半的频率分量这些分量会折叠回低频段直接污染你关心的频点而且一旦混叠任何数字算法都救不回来。我在例程里建议前端加一阶RC低通截止频率设为最高分析频率的1.5倍。比如你想分析到4kHzRC截止频率就放在6kHz左右。这样既不会衰减带内信号又能把高频折叠的风险压下去。3.3 不加窗的误差到底有多大做个直观对比。固定8kHz采样率采一个50Hz正弦波1024点恰好覆盖6.4个信号周期属于典型的非同步采样。这时如果不加窗FFT频谱上基波周围会散开一大片旁瓣基波谱线的幅值直接偏低算出来的有效值误差在0.8%左右。如果加上汉宁窗再算误差会降到0.2%以下。可以说在未知信号频率、无法同步采样的嵌入式场景里窗函数不是可选项而是必选项。4. 适配Cortex-M系列MCU的落地经验4.1 浮点型和定点型的选择例程里的代码是标准的float实现。在Cortex-M4或者Cortex-M7这种带FPU的核心上float运算几乎不占用额外周期直接跑就行。实测在Cortex-M4上开FPU跑1024点FFT大约1.2ms这个速度对大多数100Hz级别的实时反馈控制来说绰绰有余。但如果你的MCU是Cortex-M0或者M3这种不带FPU的型号浮点运算会被编译器转换成软件模拟速度慢得感人。这种情况我建议改用定点FFT数据用int16_t或int32_t旋转因子用Q15格式存储。精度稍微损失一点但换来的是Cortex-M0上大约8ms跑完1024点的性能比浮点模拟快了一个数量级。4.2 基于DMA双缓冲的无缝采集如果ADC传输采用DMA循环模式建议把采样缓冲区设计成双缓冲结构buffer A和buffer B各1024点。ADC DMA先填满A填B的同时CPU可以处理A里的数据做FFT。这样采样和处理完全并行不会出现处理数据时漏采样的尴尬。伪代码逻辑if (dma_irq dma_buff_index A) { fft_process(bufferA); } else if (dma_irq dma_buff_index B) { fft_process(bufferB); }4.3 位逆序自检一个百试百灵的调试方法DIT-FFT要求输入序列做位逆序重排这一步出错的话频谱输出完全是乱的。我调试时用的自检方法特别简单构造一个冲激信号data[0]1其余全部为0如果FFT代码正确输出频谱的所有频点幅值都应该正好是1。这个测试一旦通过说明FFT本体没问题后面再出误差就只会在预处理和缩放逻辑里找。我在不同平台移植FFT时都用这个自检节省了大量定位时间。5. 还能往哪个方向优化5.1 频谱插值测频率如果你需要跟踪信号的真实频率而不是只算有效值可以对FFT谱峰附近的几条谱线做抛物线插值估算出真实谱峰的位置。插值精度足够的话频率测量误差可以做到0.1Hz以内电网那种缓慢漂移完全不在话下。5.2 多相系统复用同一个FFT三相电压要分别算有效值、相位和负序分量时千万别写三个FFT函数。三个通道的采样数据复用同一个FFT函数顺序处理即可。算完频谱后从基波分量提取幅值和相位就能进一步计算三相不平衡度、负序/零序分量这在电能质量分析这类应用里是标准的扩展路径。5.3 滑动窗口FFT做实时更新实时性要求更高的场景可以考虑滑动窗口FFT每次采集到一个新样本丢掉最老的一个样本通过增量更新算法把FFT计算量从N log2 N降到N。代价是计算逻辑和内存都变得复杂对初学者来说不建议一上来就上这个等标准FFT流程跑通之后再做优化更稳妥。最后分享一个实战中调过的数据坑整个例程里我最后悔没早做的一步是ADC原始值到浮点数的格式转换。ADC采出来是0到4095的uint16_t而FFT输入要的是以0为中心的float。如果直接拿原始值去做均值扣除和FFT直流偏置就会变成频域里0Hz处一个巨大的尖峰后面的所有计算全被带偏。我当时排查这个问题花了半天最后定位到是转换缺失。现在的做法是进了预处理就先做一步转换float v ((float)adc_val * 3.3f / 4096.0f) - 1.65f;把ADC值先变成真实电压以0V为中心再做去直流、加窗、FFT。这一步虽然只是多写一行代码但从根本上保证了整个信号链路的正确性。希望这份例程能让你少走这段弯路。本文还有配套的精品资源点击获取