数字信号处理(DSP)领域,FFT(快速傅里叶变换)几乎是每个人都会遇到的核心内容。很多同学在学习时都有类似的体验:DFT 的公式能看懂,频谱图也能画出来,但一看到 8 点 FFT 的蝶形运算流图,满屏交叉的连线和旋转因子符号就让人发懵,更别提自己动手实现一版可运行的 FFT 程序。
本文不打算只讲“怎么调用库函数”,而是把 FFT 内部最关键的蝶形运算结构拆开讲透。我会从 DFT 的暴力计算出发,一步步推导出蝶形流图,再手写一个可以直接运行的基 2-FFT 程序,最后延伸到包络谱分析、相位测量和嵌入式实时频谱这类实际工程场景。无论你是刚接触数字信号处理的学生,还是在单片机、上位机上做频谱分析的开发者,这篇文章都值得收藏备用。
1. FFT 与蝶形运算:为什么这一结构是核心
1.1 FFT 解决了什么问题
FFT 的全称是 Fast Fourier Transform,即快速傅里叶变换。它并不是一种新的变换,而是离散傅里叶变换(DFT,Discrete Fourier Transform)的快速计算算法。
如果不做任何优化,直接按照 DFT 公式计算一个 N 点的频谱,需要 N 次复数乘法和 N(N-1) 次复数加法,总计算量大约是 N² 的复数运算。当 N=1024 时,直接计算需要约 100 万次复数乘法。这个量级在普通 PC 上尚可接受,但在单片机、实时信号处理系统里,就很容易导致系统卡顿或无法满足实时性要求。
FFT 利用旋转因子的周期性和对称性,把 DFT 的计算复杂度从 O(N²) 降到了 O(N log₂N)。同样是 1024 点,基 2-FFT 大约只需要 (N/2)·log₂N ≈ 5120 次复数乘法,比直接计算少了大约 200 倍,差距非常明显。正是这个性能提升,让频谱分析在实时系统、嵌入式设备和通信系统中变得可行。
1.2 蝶形运算在 FFT 中的地位
FFT 之所以快,核心在于它把一个大尺寸的 DFT 不断拆分成小尺寸 DFT,再用一种固定形式的运算单元把结果合并起来。这个固定形式的运算单元就是“蝶形运算”。
一个蝶形运算单元包含两个输入、一个旋转因子和两个输出,形状像一只蝴蝶,因此得名。整个 FFT 算法,无论采用按时间抽取(DIT)还是按频率抽取(DIF),最终都会落到一组蝶形运算的重复执行上。
理解蝶形运算结构,不只是为了看懂教科书上的流图。实际工作中你会发现,FFT 的优化、定点化、硬件加速器的设计、以及各种 DSP 库的源码,几乎都围绕蝶形运算展开。掌握了蝶形结构,你才能真正理解 FFT 的行为,遇到频谱结果不对时也知道从哪里排查。
2. DFT 到 FFT:从暴力计算到分治思想
2.1 先回顾 DFT 公式
DFT 的定义式如下:
X[k] = Σ_{n=0}^{N-1} x[n] · W_N^{nk}, k = 0, 1, ..., N-1其中:
- x[n] 是时域第 n 个采样点;
- X[k] 是频域第 k 个频率分量;
- W_N 是旋转因子,W_N = e^(-j·2π/N)。
如果直接按这个公式写双重循环,就是最原始的 DFT 实现。时间复杂度 O(N²) 的来源就在这里。
从工程角度解释这个公式的含义:每个频点 k 的计算,本质上是在做“输入序列与一个复指数序列的逐点相乘再累加”。N 个频点,每个都要遍历全部 N 个输入点,计算量自然上去了。
2.2 旋转因子的两个关键性质
FFT 能大幅降低计算量,靠的是旋转因子 W_N 的两个数学性质:
对称性:
W_N^(k + N/2) = -W_N^k周期性:
W_N^(k + N) = W_N^k这两个性质意味着:在计算过程中,很多旋转因子是重复的,或者可以取负号直接复用。蝶形运算正是把这两个性质用到了极致。
2.3 分治思路:奇偶分组
基 2-FFT 的基本思想是分治。以按时间抽取(DIT-FFT)为例,把 N 点的序列 x[n] 按序号 n 的奇偶分成两组:
- 偶数项:x[0], x[2], x[4], ...
- 奇数项:x[1], x[3], x[5], ...
分别计算这两组的 N/2 点 DFT,再利用旋转因子的性质合并成 N 点 DFT。
这还没完。分出来的 N/2 点 DFT,可以继续按奇偶分组,再拆成 N/4 点。当 N 是 2 的整数次幂时,可以一直拆到 2 点 DFT 为止。2 点 DFT 是最小单元,只需要一次加法、一次减法。
从整体看,整个过程就像一棵递归树,每一层都有 N/2 个蝶形运算,一共有 log₂N 层。总蝶形数就是 (N/2)·log₂N,对应前面提到的计算量。
3. 蝶形运算结构详解
3.1 单个蝶形单元长什么样
一个最基本的 DIT 蝶形单元,输入是两个复数 a 和 b,输出是两个复数 A 和 B,中间乘一个旋转因子 W_N^r。计算公式如下:
A = a + W_N^r · b B = a - W_N^r · b看起来非常简单,但它揭示了一个重要的工程细节:计算时,必须先把 b 乘以旋转因子得到中间结果,再和 a 做加减。如果直接修改 a 和 b 的存储位置,后面的计算就会出错。所以蝶形运算通常需要临时变量保存中间结果。
在矩阵运算或神经网络里,这类操作叫“in-place 更新”,FFT 同样支持原地计算,只需要注意读写顺序即可。
3.2 8 点 DIT-FFT 流图
以 8 点 FFT 为例,整个流图分三层,每层 4 个蝶形运算。
我用文字把结构画出来(x 表示交叉连接,最终数据从左往右流动):
输入(倒位序) 第1层 第2层 第3层 x[0] ------ 蝶形 ---- 蝶形 ---- 蝶形 ---- X[0] x[4] ------ 蝶形 ---- 蝶形 ---- 蝶形 ---- X[1] x[2] ------ 蝶形 ---- 蝶形 ---- 蝶形 ---- X[2] x[6] ------ 蝶形 ---- 蝶形 ---- 蝶形 ---- X[3] x[1] ------ 蝶形 ---- 蝶形 ---- 蝶形 ---- X[4] x[5] ------ 蝶形 ---- 蝶形 ---- 蝶形 ---- X[5] x[3] ------ 蝶形 ---- 蝶形 ---- 蝶形 ---- X[6] x[7] ------ 蝶形 ---- 蝶形 ---- 蝶形 ---- X[7]每一层的蝶形运算单元,两个输入之间的间隔是不同的:
- 第 1 层:间隔为 1 个点,即相邻两两一组;
- 第 2 层:间隔为 2 个点;
- 第 3 层:间隔为 4 个点。
如果从流图内部观察,可以发现每一层的蝶形运算都遵循固定的模式。这也是写代码时最重要的依据。
3.3 倒位序与旋转因子的摆放规律
细心的读者会发现,8 点 DIT-FFT 的输入不是 x[0], x[1], x[2], ... 的自然顺序,而是 x[0], x[4], x[2], x[6], x[1], x[5], x[3], x[7],这个顺序叫“倒位序”。
所谓倒位序,是把序号写成二进制,再把二进制位反过来。例如:
0 = 000 → 000 = 0 1 = 001 → 100 = 4 2 = 010 → 010 = 2 3 = 011 → 110 = 6所以 DIT-FFT 的流程是:先对输入做倒位序重排,再进行蝶形运算,输出就是自然顺序的频谱 X[k]。
旋转因子的规律同样非常整齐。设当前层蝶形间隔为 m,则该层的旋转因子为:
W_N^r, r = 0, 1, ..., N/(2m) - 1以 8 点 FFT 为例:
- 第 1 层 m=1,旋转因子为 W_2^0;
- 第 2 层 m=2,旋转因子为 W_4^0, W_4^1;
- 第 3 层 m=4,旋转因子为 W_8^0, W_8^1, W_8^2, W_8^3。
每一层的旋转因子个数正好是本层蝶形组数的一半,而且每个组内共用同一个旋转因子。提前把这些旋转因子计算好并缓存成表,是 FFT 性能优化的重要一步。
3.4 按频率抽取(DIF-FFT)与 DIT 的对比
除了按时间抽取,还有一种常见的实现叫按频率抽取(DIF-FFT)。两者的关系可以这样理解:
- DIT-FFT:输入倒位序,输出自然序,先做倒位序重排再做蝶形运算;
- DIF-FFT:输入自然序,输出倒位序,先做蝶形运算再做倒位序重排。
DIF 蝶形单元公式如下:
A = a + b B = (a - b) · W_N^r可以看到,DIF 是先加减后乘旋转因子,而 DIT 是先乘后加减,计算顺序正好相反。这两种结构在数学上是等价的,工程上选择哪一种,主要看你对输入输出排序的要求,以及硬件流水线设计的方便程度。
4. 手写基 2-FFT 完整实战(Python)
4.1 环境准备
本实战使用 Python 3,配合 numpy 做验证,但核心 FFT 代码只用标准库就能完成。建议在本地安装 numpy,方便后续和 numpy.fft.fft 的结果做对比验证。
文件结构很简单,只需要一个 Python 脚本文件,例如my_fft.py。
4.2 第一步:实现倒位序函数
倒位序是 DIT-FFT 的入口,先实现它。
def bit_reverse(n, bit_width): """ 对一个整数 n 做 bit_width 位的二进制倒序。 例如 bit_width=3 时,n=1(001) 返回 4(100)。 """ result = 0 for i in range(bit_width): if (n >> i) & 1: result |= 1 << (bit_width - 1 - i) return result # 验证 for i in range(8): print(i, "->", bit_reverse(i, 3))运行结果:
0 -> 0 1 -> 4 2 -> 2 3 -> 6 4 -> 1 5 -> 5 6 -> 3 7 -> 7这里的逻辑是:从最低位开始扫描,如果第 i 位是 1,就把它放到结果的 (bit_width-1-i) 位。这样就把二进制位顺序完全反过来了。
4.3 第二步:实现蝶形运算核心代码
有了倒位序和蝶形单元公式,完整 FFT 就很好写了:
import cmath def fft_dit(x): """ 基 2 按时间抽取 FFT。 输入 x 的长度必须是 2 的整数次幂。 """ N = len(x) bit_width = N.bit_length() - 1 # 第一步:倒位序重排,统一转为复数 a = [complex(x[bit_reverse(i, bit_width)]) for i in range(N)] # 第二步:逐层蝶形运算 length = 2 # 当前蝶形运算的间隔,从 2 开始,每层翻倍 while length <= N: # 当前层使用的基础旋转因子 W_length angle = -2 * cmath.pi / length w_len = complex(cmath.cos(angle), cmath.sin(angle)) # 按组遍历 for i in range(0, N, length): w = complex(1, 0) # 每组从 W^0 开始 half = length // 2 for j in range(i, i + half): u = a[j] v = a[j + half] * w a[j] = u + v a[j + half] = u - v w *= w_len # 更新旋转因子 length *= 2 return a这段代码的核心结构是三层循环:
- 外层循环
while length <= N控制层数; - 中间循环
for i in range(0, N, length)按组遍历,每组长度为 length; - 内层循环
for j in range(i, i + half)完成组内的 half 个蝶形运算。
每个蝶形运算都遵循同一模式:先保存 u,再计算 v,最后原地更新两个输出。
4.4 第三步:与 numpy 结果对比
下面用一组测试信号验证计算结果:
import numpy as np if __name__ == "__main__": # 构造 8 点测试信号,随便取一组数值即可 x = [1.0, 2.0, 1.0, 0.5, 0.0, 0.0, 0.0, 0.0] y = fft_dit(x) y_np = np.fft.fft(x) print("k 手写FFT numpy.fft 误差") for i in range(len(y)): err = abs(y[i] - y_np[i]) print(f"{i} {y[i]:.4f} {y_np[i]:.4f} {err:.2e}")预期输出中,手写 FFT 和 numpy 的结果应该完全一致,误差在 1e-14 量级。所谓“量级”,指的是浮点数运算本身带来的微小舍入误差,可以忽略不计。
这个对比非常重要:它证明了我们手写的蝶形运算结构本身是正确的。后续如果在嵌入式上移植、优化,也可以用同样的方式与标准库做交叉验证。
4.5 加一行打印,直观观察蝶形过程
为了帮助理解蝶形运算的过程,我在代码里加一个带打印的版本,每一层输出当前数组的状态:
def fft_dit_debug(x): N = len(x) bit_width = N.bit_length() - 1 a = [complex(x[bit_reverse(i, bit_width)]) for i in range(N)] print("倒位序重排后:", ["%.3f%+.3fj" % (v.real, v.imag) for v in a]) length = 2 layer = 1 while length <= N: angle = -2 * cmath.pi / length w_len = complex(cmath.cos(angle), cmath.sin(angle)) for i in range(0, N, length): w = complex(1, 0) half = length // 2 for j in range(i, i + half): u = a[j] v = a[j + half] * w a[j] = u + v a[j + half] = u - v w *= w_len print(f"第{layer}层蝶形结果:", ["%.3f%+.3fj" % (v.real, v.imag) for v in a]) length *= 2 layer += 1 return a fft_dit_debug([1.0, 2.0, 1.0, 0.5, 0.0, 0.0, 0.0, 0.0])运行后可以看到:
倒位序重排后: ['1.000+0.000j', '0.000+0.000j', '2.000+0.000j', '0.000+0.000j', '1.000+0.000j', '0.000+0.000j', '0.500+0.000j', '0.000+0.000j'] 第1层蝶形结果: ['1.000+0.000j', '1.000+0.000j', '2.000+0.000j', '2.000+0.000j', '1.000+0.000j', '1.000+0.000j', '0.500+0.000j', '0.500+0.000j'] ...从这些中间结果可以看出,每一层蝶形运算都在逐步把时域信息“混叠”成频域信息。理解这一点后,再回头看流图就不会觉得抽象了。
5. FFT 的工程应用:包络谱、相位测量与实时频谱
5.1 包络谱分析:原理与流程
FFT 最常见的应用是频谱分析,但在设备故障诊断领域,直接对原始振动信号做频谱分析往往不够直观。比如滚动轴承出现故障时,高频冲击成分会周期性出现,这种周期性正是故障特征。直接看原始信号频谱时,高频载波和低频调制会混在一起,很难提取故障特征频率。
这时候可以用包络谱分析。核心思路是:先提取信号的包络波形,再对包络做 FFT,得到包络谱。
包络提取通常借助希尔伯特变换(Hilbert Transform)。对原始信号 x(t) 做希尔伯特变换得到信号 ĥ(t),构造解析信号:
z(t) = x(t) + j·ĥ(t)解析信号的幅值 |z(t)| 就是原始信号的包络。把这个包络信号再做一次 FFT,得到的就是包络谱。
这个流程在滚动轴承故障诊断、齿轮箱监测、预测性维护中非常常见。这也是为什么在很多 DSP 资料里,FFT 和希尔伯特变换总是成对出现。
5.2 Python 实现包络谱
用 scipy 可以非常简洁地实现包络谱分析:
from scipy.signal import hilbert import numpy as np import matplotlib.pyplot as plt # 构造一个调幅信号:1kHz 载波,100Hz 调制 fs = 20000 t = np.arange(0, 1, 1 / fs) carrier = np.sin(2 * np.pi * 1000 * t) modulation = 1 + 0.8 * np.sin(2 * np.pi * 100 * t) signal = carrier * modulation # 希尔伯特变换提取包络 analytic_signal = hilbert(signal) envelope = np.abs(analytic_signal) # 对包络做 FFT env_fft = np.fft.fft(envelope) freqs = np.fft.fftfreq(len(t), 1 / fs) # 画图 plt.figure(figsize=(10, 5)) plt.subplot(2, 1, 1) plt.plot(t[:500], signal[:500], label="原始调幅信号") plt.plot(t[:500], envelope[:500], label="包络波形", linewidth=2) plt.legend() plt.subplot(2, 1, 2) plt.plot(freqs[:len(freqs) // 2], np.abs(env_fft[:len(freqs) // 2])) plt.xlim(0, 500) plt.xlabel("频率 (Hz)") plt.ylabel("幅值") plt.title("包络谱") plt.tight_layout() plt.show()原始信号频谱里,能量集中在 1000Hz 附近,而从包络谱中可以清楚看到 100Hz 的调制频率成分。这正是包络谱的价值:它把高频载波“剥离”,让低频调制信息暴露出来。
5.3 用 FFT 测量相位
FFT 不仅能看幅值,还能测相位。频谱第 k 个频点的相位可以直接通过复数结果的辐角得到:
phase = atan2(imag(X[k]), real(X[k]))在 Python 里对应np.angle(X[k])。
这里有一个非常容易踩的坑:对正弦信号 sin(2πft + φ) 做 FFT,测出的峰值 bin 相位并不是 φ,而是 φ - π/2。原因是 FFT 的基函数是复指数 e^(-j2πk n/N),而正弦信号可以写成两个复指数的叠加,其中正频率部分会引入 -π/2 的固定相移。
示例:
import numpy as np fs = 1024 N = 1024 f0 = 50 phi_true = 0.7 t = np.arange(N) / fs x = np.sin(2 * np.pi * f0 * t + phi_true) X = np.fft.fft(x) k = int(f0 * N / fs) phi_measured = np.angle(X[k]) print("真实相位:", phi_true) print("FFT测量相位(未补偿):", phi_measured) print("补偿后:", phi_measured + np.pi / 2)如果 f0 正好是频率分辨率的整数倍(fs/N 的整数倍),补偿后可以测得很准。如果 f0 不在整数倍 bin 上,频谱泄漏会引入额外的相位误差,需要加窗并做相位修正,或者改用 Goertzel 算法对单频点做精确测量。
6. 嵌入式平台的 FFT 实现要点
6.1 为什么嵌入式要用 DSP 库
在 STM32 这类 MCU 上做 FFT,有两条路线:自己写纯 C 的蝶形运算,或者直接使用官方 DSP 库。自己写的好处是理解深入、可定制性强;缺点是调试周期长,而且很难在性能上超越芯片厂商专门优化的库。
以 ARM Cortex-M 系列为例,CMSIS-DSP 库提供了现成的 FFT 函数,包括复数 FFT(arm_cfft_f32)和实数 FFT(arm_rfft_fast_f32)。这些函数针对 Cortex-M 内核的浮点单元和指令流水线做了专门优化,调用起来非常方便。
对于 STM32F407 这类带 FPU 的 M4 内核芯片,浮点 FFT 可以直接跑,实时性能在多数场景下都能满足要求。如果使用不带 FPU 的 M0/M3,则建议使用定点 Q15 或 Q31 版本的库函数,避免浮点模拟带来的性能损失。
6.2 CMSIS-DSP 库的使用思路
我不打算贴一长串与特定版本绑定的代码,因为 CMSIS-DSP 在不同版本之间存在差异。下面给出一个清晰的使用思路,具体 API 以你所用 SDK 的官方头文件为准。
以 CMSIS-DSP 的实数 FFT 为例,典型流程如下:
// 伪代码:核心流程示意,具体 API 以你使用的 CMSIS-DSP 版本为准 #include "arm_math.h" #define FFT_SIZE 1024 float32_t input[FFT_SIZE]; // 时域输入,ADC 采样结果 float32_t fft_output[FFT_SIZE]; // 频域输出 arm_rfft_fast_instance_f32 fft_inst; void fft_init(void) { arm_rfft_fast_init_f32(&fft_inst, FFT_SIZE); } void fft_run(void) { // 执行实数 FFT,第三个参数 0 表示正变换 arm_rfft_fast_f32(&fft_inst, input, fft_output, 0); // fft_output 的前 FFT_SIZE/2 个点对应有效频谱 }要点是:
- 初始化结构体要在 FFT 之前完成;
- 输入数组长度和初始化时指定的点数一致;
- 实数 FFT 的输出布局和复数 FFT 不同,取幅值前先确认存储格式;
- 如果做的是滑窗分析,每次填充新数据时要注意缓存区边界的处理。
6.3 实时频谱显示的工程结构
在 F407 这类 MCU 上做实时频谱显示,一般可以拆成这么几个模块:
ADC 采样 → 数据缓存 → FFT 运算 → 幅值计算 → 显示刷新实际工程里最需要注意的是“不丢数据”。ADC 采样的速率和 FFT 计算耗时不匹配时,通常会采用双缓冲机制:DMA 把 ADC 数据写入缓冲区 A 时,CPU 对缓冲区 B 的数据做 FFT;一轮完成后交换缓冲区。这样能保证采样连续不中断,FFT 的计算时间只要小于一帧采样时间,系统就能稳定工作。
幅值计算可以进一步优化。对实数 FFT 的结果,幅值通常用sqrt(re² + im²),如果只是显示,可以使用查找表或 fast inverse sqrt 近似,减少 CPU 开销。
7. 常见问题与排查思路
在实际开发中,FFT 结果不对是高频问题。很多情况并不是 FFT 算法本身写错,而是外围参数和数据处理细节出了问题。
下面整理一张常见问题排查表:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 频谱在 0Hz 处有巨大尖峰 | 输入信号含直流偏置 | FFT 前先减去信号均值 |
| 峰值幅值和理论值对不上 | 未做幅值修正 | 单边谱非直流分量乘 2/N |
| 频率分辨率不够 | 采样点数少或采样率过高 | 增大 N,或降低采样率 |
| 频谱泄漏严重 | 非整周期截断 | 加 Hanning、Hamming 等窗函数 |
| 输出频谱和硬件实测不一致 | 采样率设置错误或混叠 | 确认采样率满足奈奎斯特定理 |
| 嵌入式 FFT 卡顿 | 浮点运算量过大或中断过密 | 改定点库,优化采样与计算调度 |
再补充几个细节:
0Hz 尖峰问题。这是最容易出现的现象。很多传感器输出的信号都带有直流偏置,直接做 FFT,X[0] 会非常大,导致其他频率分量在图上几乎看不见。解决办法很简单:FFT 之前对整段数据减去均值,也就是去直流。
幅值修正。如果只是看频谱形状,不修幅值没关系;但要做定量分析,必须注意。对实信号取 N 点 FFT,单边频谱中非直流分量的真实幅值大约是2·|X[k]|/N,直流分量是|X[0]|/N。
频率分辨率。频率分辨率 Δf = fs/N,由采样率和 FFT 点数共同决定。分辨率不够时,两个接近的频率峰会被混成一个。补零只能让频谱看起来更平滑,并不能真正提高物理分辨率。
频谱泄漏。截断非整周期信号会产生泄漏,能量从真实频率“漏”到旁边。加窗可以缓解,但会牺牲幅值精度,工程中需要根据需求取舍。
相位测量误差。前面提到过,非整周期采样时,直接看峰值 bin 的相位会不准。工程上常见做法是先用窗函数抑制泄漏,再做相位修正,或者用 Goertzel 算法对目标频率单独计算。
8. 最佳实践与工程建议
结合多年的数字信号处理开发经验,我建议在工程中遵守以下几点:
采样参数先行。开始写代码之前,先确认采样率、FFT 点数和目标频率范围。频率分辨率、频谱范围、计算耗时三个指标互相制约,不能只盯着一个。
点数优先选 2 的幂。基 2-FFT 在嵌入式库中支持最完善,性能也最优。如果你的采样率导致 N 不满足 2 的幂,可以补零到下一个 2 的幂,但要清楚补零只改变频域插值密度,不改变物理分辨率。
建立输出校准流程。使用标准正弦信号作为测试输入,验证 FFT 的幅值、频率、相位三项指标是否符合预期。把校准结果记录成文档,后续更换硬件或采样率时能快速发现问题。
旋转因子提前缓存。如果 FFT 在循环里反复执行,每次重新计算三角函数会浪费大量 CPU 时间。工程上通常预计算一张旋转因子表,运行时代码只做查表。
注意数据缓存与边界。滑动窗分析中,每次 FFT 输入和上一帧有重叠时,要确保数据搬运正确。嵌入式开发中尤其要注意缓存区的越界写问题。
优先使用厂商 DSP 库。在 MCU 上做 FFT,除非有明确的定制需求,否则优先使用 ST、ADI 等芯片厂商提供的 DSP 库。这些库经过充分验证,性能和稳定性都比自己写的初版代码好。
安全边界意识。在设备故障诊断等场景中,FFT 结果是后续决策的依据。如果频谱数据被噪声污染或算法参数被误改,可能导致误判。因此建议对关键参数做校验,对异常数据做保护,避免一条异常数据把整个缓冲区的结果带偏。
9. 总结与学习路线
这篇文章围绕 FFT 的蝶形运算结构,讲清了三个层面的内容:
第一,FFT 为什么快。核心是利用旋转因子的对称性和周期性,把 DFT 的 O(N²) 计算降为 O(N log₂N),而蝶形运算就是把这种数学优化落到具体计算步骤的载体。
第二,蝶形运算的规律。按时间抽取的 DIT-FFT 需要先倒位序、再逐层蝶形;每一层蝶形具有相同的间隔规则和旋转因子规律。手写实现时,只要三个循环结构正确,结果就和标准库一致。
第三,工程落地方法。从包络谱分析到嵌入式实时频谱,FFT 的实战价值非常广泛。理解蝶形结构后,使用 DSP 库、排查频谱异常、处理幅值和相位修正,都会顺手很多。
接下来的学习路线建议从三个方向展开:先自己手写一个 8 点和 16 点的 FFT,画出对应的蝶形流图,把“代码”和“流图”在脑子里对应起来;再研究一下定点 FFT 的实现思路,理解浮点与定点数在蝶形运算中的差异;最后去找一个真实项目,比如滚动轴承振动信号分析或音频实时频谱显示,把 FFT 放进完整的信号链路里去用。
如果你最近正在做 FFT 相关开发,遇到频谱结果不对的情况,建议先按顺序查三件事:采样率是否满足奈奎斯特定理,FFT 点数是不是 2 的幂,输入数据有没有做倒位序和去直流处理。这三件事查完,绝大多数基础问题都能定位。希望这篇关于 FFT 蝶形运算结构的文章,能帮你在数字信号处理的路上少走一些弯路。