4、快速傅里叶变换:DFT与FFT原理、频谱分析、窗函数的作用

各位同学,今天我们聊一个硬核话题——快速傅里叶变换。

说实话,我刚入行那会儿,看到“傅里叶”三个字就头疼。大学课本里全是积分、级数,看得人眼花。但后来做航空电台项目,天天跟频谱打交道,才真正明白这东西有多重要。

你想想看,电台信号在空中飞,你总得知道它里面藏着什么频率吧?是语音、是数据、还是干扰?傅里叶变换就是干这个的。

4.1 从DFT到FFT:一个“笨办法”的进化

先说说DFT,离散傅里叶变换。

它的数学公式长这样:

X(k) = Σ x(n) * e^(-j*2π*k*n/N)   (n=0 to N-1)

说白了,就是把时域信号跟一堆不同频率的复指数相乘,再累加。每个频率点算一次,N个点就要算N²次乘法。

我在项目中遇到过一个问题:采样率48kHz,做4096点的DFT。你猜要算多久?大概几秒钟。这在实时系统里根本没法用。

所以就有了FFT,快速傅里叶变换。

FFT的核心思想就四个字:分而治之

它把一个大DFT拆成两个小DFT,再拆成四个更小的DFT……一直拆到只剩2点DFT。计算量从N²降到了N*log₂(N)。

举个例子:

  • N=1024时,DFT需要约100万次运算
  • FFT只需要约1万次运算

差了整整100倍!

关键点:FFT不是新的变换,只是DFT的一种高效算法。结果一模一样,只是算得更快。

4.2 基2时间抽取FFT:我常用的套路

最常见的FFT实现是基2时间抽取法。

它的思路是这样的:

  1. 把输入序列按奇偶分成两组
  2. 分别对两组做N/2点DFT
  3. 用旋转因子W_N^k把结果组合起来

嗯,这里要注意:旋转因子W_N^k = e^(-j*2π*k/N),它是有周期性的。利用这个周期性,可以进一步减少计算量。

我习惯用C语言实现一个简单的FFT:

void fft(complex* x, int N) {
    // 位反转排序
    for (int i = 0; i < N; i++) {
        int j = bit_reverse(i, log2(N));
        if (i < j) swap(&x[i], &x[j]);
    }
    
    // 蝶形运算
    for (int len = 2; len <= N; len <<= 1) {
        complex w = exp(-j * 2 * PI / len);
        for (int i = 0; i < N; i += len) {
            complex wk = 1;
            for (int k = 0; k < len/2; k++) {
                complex u = x[i + k];
                complex v = x[i + k + len/2] * wk;
                x[i + k] = u + v;
                x[i + k + len/2] = u - v;
                wk *= w;
            }
        }
    }
}

这段代码我用了十几年了。核心就是那个蝶形运算,你看它像不像蝴蝶的两只翅膀?

小技巧:实际项目中,别自己写FFT。用现成的库,比如FFTW、CMSIS-DSP。它们针对特定处理器做了优化,比自己写的快得多。

4.3 频谱分析:从时域到频域的“翻译”

有了FFT,频谱分析就简单了。

基本步骤:

  1. 采集N点信号
  2. 做N点FFT
  3. 计算幅度谱:|X(k)| = sqrt(Re² + Im²)
  4. 换算频率轴:f(k) = k * fs / N

举个例子,你采集了1024点,采样率48kHz:

  • 频率分辨率 = 48000 / 1024 ≈ 46.875 Hz
  • 能分析的频率范围 = 0 ~ 24kHz(奈奎斯特频率)

我曾经调试一个航空电台,发现接收到的语音信号有奇怪的“嗡嗡”声。用FFT一看,在50Hz处有个尖峰。原来是电源工频干扰。加个陷波滤波器就解决了。

这就是频谱分析的价值——让你“看见”信号里有什么。

4.4 窗函数:为什么需要它?

好,问题来了。

你采集信号时,只能采有限长的一段。这相当于在无限长的信号上,加了一个矩形窗。

矩形窗有什么问题?

它会在频域产生“频谱泄漏”。

说白了,就是一个频率的能量,泄漏到了其他频率上。你看到的频谱不再是干净的谱线,而是带“裙边”的。

为什么会这样?

因为矩形窗在边界处突然截断,产生了高频分量。这些高频分量在频域表现为旁瓣。

解决办法就是——用其他形状的窗函数。

常用的窗函数有:

窗函数 主瓣宽度 旁瓣衰减 适用场景
矩形窗 最窄 -13dB 瞬态信号、频率分辨率要求高
汉宁窗 较宽 -32dB 一般频谱分析
海明窗 较宽 -43dB 语音信号处理
布莱克曼窗 最宽 -58dB 需要极低旁瓣

我个人的习惯是:

  • 做频率分辨率要求高的分析,用矩形窗
  • 做一般频谱分析,用汉宁窗
  • 做语音信号处理,用海明窗

注意:加窗会损失信号能量。因为窗函数在两端衰减了信号。所以加窗后,幅度谱的幅值会变小。需要做幅度校正。

校正系数很简单:

幅度校正因子 = N / Σ w(n)

其中w(n)是窗函数序列。

4.5 避坑指南:我踩过的几个坑

做频谱分析这么多年,我踩过不少坑。分享几个给你:

坑一:忘记去直流

我曾经分析一个信号,发现0Hz处有个巨大的尖峰。查了半天,原来是信号有直流偏置。后来我习惯先减去均值再做FFT。

坑二:频率轴搞错

FFT输出的频率顺序是:0, fs/N, 2fs/N, ..., (N/2-1)fs/N, -fs/2, ..., -fs/N。很多人直接画图,结果频谱是镜像的。记得用fftshift函数把负频率移到左边。

坑三:窗函数选错

有一次分析一个微弱信号,用了矩形窗。结果信号被旁瓣淹没了。换成布莱克曼窗,信号就出来了。选窗函数要看你的需求——是要频率分辨率,还是要动态范围。

总结一下:

  • DFT是理论,FFT是实践
  • 频谱分析让你“看见”信号
  • 窗函数是必备工具,选对了事半功倍

好了,这一章就到这里。下一章我们聊聊数字滤波器的设计,那也是个有意思的话题。