1. 从“看懂公式”到“用起来”傅里叶变换到底在解决什么问题很多人学数字信号处理卡在傅里叶变换这一关不是因为公式太难而是因为教材一上来就给出一堆积分和求和符号却很少讲清楚一件事我们到底为什么要做这个变换我刚开始接触傅里叶变换的时候也有过同样的困惑。时域波形明明已经能看出信号的样子了为什么还要费劲把它变到频域去后来做实际项目才慢慢明白时域和频域不过是观察同一个信号的两种视角。有些问题在时域里看是一团乱麻换到频域里一眼就能看穿。举个最直观的例子。假设你有一段音频信号里面混杂着50Hz的工频干扰和一个200Hz的有用信号。在时域波形上这两个成分叠加在一起你很难直接把它们分开。但做完傅里叶变换之后频域图上会清清楚楚地出现两个峰值一个在50Hz一个在200Hz。你想滤掉哪个就在频域上把对应的分量压下去再变换回时域就行了。这就是傅里叶变换最核心的价值它把信号从“时间轴上的幅度变化”翻译成了“频率轴上的成分构成”。在数字信号处理这个领域里傅里叶变换不是一个孤立的数学工具而是一整套分析体系的地基。后面要学的滤波器设计、频谱分析、调制解调、语音处理、图像压缩几乎全都建立在傅里叶变换的思想之上。你如果这一块没吃透后面学数字滤波器的时候就会觉得那些系数来得莫名其妙学FFT的时候也只能死记流程而不知道每一步在干什么。这篇文章主要面向两类人一类是正在学数字信号处理课程、被傅里叶变换的公式绕晕的学生另一类是做嵌入式开发、音频处理或者通信相关工作的工程师需要用傅里叶变换解决实际问题但理论基础不够扎实。我会尽量避开纯数学推导把重点放在“这个概念在工程上意味着什么”“实际写代码时要注意什么”“哪些坑我踩过”这些方面。公式该有的还是会有但每一个公式我都会告诉你它在实际中对应的是什么操作。2. 四种傅里叶变换形式的工程含义与选择逻辑2.1 为什么会有四种形式而不是一种翻开任何一本数字信号处理的教材你都会看到四种傅里叶变换形式连续时间傅里叶变换CTFT、连续时间傅里叶级数CTFS、离散时间傅里叶变换DTFT、离散傅里叶变换DFT。初学的时候很容易懵为什么搞这么多种我到底该用哪个其实这四种形式的区别就来自两个维度信号在时域是连续的还是离散的以及信号在时域是周期的还是非周期的。这两个维度一交叉就产生了四种组合对应四种变换形式。时域信号特性频域特性变换形式典型应用场景连续、非周期连续、非周期CTFT模拟信号理论分析连续、周期离散、非周期CTFS周期模拟信号频谱分析离散、非周期连续、周期DTFT数字信号理论分析离散、周期离散、周期DFT计算机实际计算这张表建议你记住因为它解释了一个非常关键的问题为什么计算机上只能用DFT。计算机处理的数据必须是离散的、有限长的而DFT恰好是时域离散、频域也离散的形式天然适合计算机处理。你在MATLAB或者Python里调用fft()函数底层算的就是DFT。2.2 DTFT和DFT之间那道容易被忽略的鸿沟很多人在学DTFT的时候觉得挺顺利公式写出来也好看但一到DFT就卡住了。问题出在哪里出在频域采样这一步。DTFT的频域是连续的也就是说你需要无穷多个频率点才能完整描述一个离散时间信号的频谱。但计算机不可能存储无穷多个点所以我们必须对频域也进行采样把它变成有限个离散的频率点。这个采样操作带来的直接后果就是时域信号被隐式地周期延拓了。这句话是理解DFT的关键。DFT隐含了一个假设你给它的N个数据点实际上代表的是一个周期为N的周期信号的一个周期。如果你不管这个隐含假设直接拿DFT的结果去分析一个非周期信号就可能出现频谱泄漏和栅栏效应。注意DFT的周期性延拓假设不是bug而是feature。理解这一点你才能理解为什么后面要加窗、为什么要做补零、为什么频率分辨率等于采样率除以点数。2.3 实际工程中怎么选一个简单的判断流程面对一个实际问题怎么确定该用哪种变换我总结了一个简单的判断流程信号是模拟的还是数字的如果是模拟信号还没采样那理论上用CTFT或CTFS分析但实际中我们通常先采样变成数字信号再处理。信号是周期性的还是非周期性的如果是周期信号且你知道周期可以用DFT的一个周期来分析结果就是离散谱线。如果是非周期信号DFT会给出一个近似频谱。你需要连续频谱还是离散谱线如果只是想知道信号里有哪些频率成分DFT足够了。如果需要精确的连续频谱形状那得用DTFT的理论但实际计算还是靠DFT加补零来逼近。在嵌入式音频处理项目中我基本只用DFT通过FFT快速实现。因为麦克风采进来的就是离散信号我只需要知道各个频段的能量分布DFT给出的离散频谱完全够用。如果你要做的是理论推导或者算法证明那DTFT和CTFT的公式该推还是得推。3. 从DFT到FFT计算量是怎么被压下来的3.1 直接算DFT有多慢DFT的定义式是这样的$$X[k] \sum_{n0}^{N-1} x[n] \cdot e^{-j\frac{2\pi}{N}nk}, \quad k 0, 1, \ldots, N-1$$这个公式看起来不复杂就是一个求和。但如果直接按这个公式写代码计算量是惊人的。对于每一个k你需要做N次复数乘法和N-1次复数加法。总共有N个k所以总的复数乘法次数是$N^2$。假设N1024$N^2$大约是100万次复数乘法。每次复数乘法又包含4次实数乘法和2次实数加法所以实际运算量是400万次实数乘法。这在今天的电脑上当然不算什么但在实时性要求高的嵌入式场景里这个计算量是不可接受的。我早期做过一个音频频谱显示的项目一开始就是直接按DFT公式写的。在PC上跑没问题但移植到主频只有几十兆的MCU上之后刷新率惨不忍睹一帧频谱要算好几百毫秒。后来换成FFT同样的点数计算时间直接降到了几毫秒。3.2 FFT的核心思想分而治之FFT不是一种新的变换它只是DFT的一种快速算法。它的核心思想是利用旋转因子的周期性和对称性把一个大点数的DFT分解成若干个小点数的DFT然后逐层合并。最经典的基2-FFTRadix-2 FFT要求点数N是2的整数次幂。它把N点DFT分解成两个N/2点DFT一个处理偶数索引的样本一个处理奇数索引的样本。然后这两个N/2点DFT又可以继续分解直到只剩下2点DFT。这样分解之后总的复数乘法次数从$N^2$降到了$\frac{N}{2}\log_2 N$。还是以N1024为例$\frac{1024}{2} \times 10 5120$次复数乘法相比原来的100万次计算量降低了大约200倍。这就是为什么FFT是数字信号处理领域最重要的算法之一。3.3 蝶形运算FFT的基本单元FFT的分解过程最终会落到一种叫做蝶形运算的基本单元上。一个标准的基2蝶形运算接收两个复数输入输出两个复数中间涉及一次复数乘法和两次复数加减法。蝶形运算的名字来源于它的信号流图形状——两条线交叉像蝴蝶的翅膀。虽然我这里不能画图但你可以想象两个输入节点在上方两个输出节点在下方中间有交叉的连接线。在实际写FFT代码的时候蝶形运算是需要重点优化的部分。因为整个FFT就是大量蝶形运算的重复蝶形运算的效率直接决定了FFT的整体性能。常见的优化手段包括查表法旋转因子$W_N^k e^{-j\frac{2\pi}{N}k}$的值可以预先算好存成表避免在循环里反复调用三角函数。位反转FFT的输入需要按位反转的顺序排列这个操作可以在数据读入的时候顺便完成。原地运算蝶形运算的输入和输出可以共用同一块内存节省存储空间。3.4 自己写FFT还是用现成库这个问题我被问过很多次。我的建议是除非你的目标就是学习FFT算法本身否则不要自己从零写FFT。原因很简单。现成的FFT库经过了几十年的优化在精度、速度、稳定性方面都远超你自己手写的版本。比如在PC端Python的NumPy和SciPy都提供了高度优化的FFT实现底层调用的是经过汇编级优化的库。在嵌入式端ARM的CMSIS-DSP库提供了针对Cortex-M系列处理器优化的FFT函数充分利用了硬件特性。我自己只在两种情况下会手写FFT一是教学演示需要把每一步都展示出来二是目标平台没有可用的FFT库而且点数固定且很小比如只有8点或16点这时候手写一个简单的基2-FFT反而更省事。提示如果你用的是STM32系列MCU直接上CMSIS-DSP库里的arm_cfft_f32()或arm_rfft_fast_f32()。实数FFT比复数FFT快将近一倍因为实数序列的频谱具有共轭对称性有一半的数据是冗余的。4. 频谱分析实战从采样到频谱图的完整链路4.1 采样率、点数和频率分辨率的三角关系做频谱分析的时候有三个参数是绕不开的采样率$f_s$、采样点数N、频率分辨率$\Delta f$。它们之间的关系是$$\Delta f \frac{f_s}{N}$$这个公式看起来简单但它决定了你能在频谱图上看到多细的细节。举个例子如果你用48kHz的采样率采集了1024个点那么频率分辨率就是48000/1024 ≈ 46.9Hz。这意味着频谱图上相邻两个点之间相隔46.9Hz如果信号里有两个频率相差只有20Hz的成分你在这个频谱图上是分不清的。想要提高频率分辨率有两条路要么降低采样率要么增加采样点数。降低采样率受限于奈奎斯特采样定理你至少得保证采样率大于信号最高频率的两倍。所以更常用的方法是增加采样点数。但点数增加意味着计算量增加而且需要更长的采集时间。在实际项目中我通常会先估算需要的频率分辨率然后反推需要的点数。比如我想分辨间隔50Hz的两个频率成分采样率是48kHz那么N至少要是48000/50960取2的整数次幂就是1024。4.2 加窗为什么你的频谱图上会有“假峰”如果你直接对一个有限长的信号做FFT实际上相当于给信号乘了一个矩形窗。矩形窗的频谱旁瓣比较高会导致频谱泄漏——本来应该集中在某个频点的能量泄漏到了旁边的频点上。频谱泄漏的表现就是频谱图上某个频率附近出现了一片“裙摆”而不是一根干净的谱线。如果两个频率成分靠得比较近泄漏还可能把弱信号淹没掉。解决方法是加窗。常见的窗函数有汉宁窗、汉明窗、布莱克曼窗、凯泽窗等。不同的窗函数在主瓣宽度和旁瓣衰减之间有不同的取舍窗函数主瓣宽度旁瓣衰减适用场景矩形窗最窄最差-13dB瞬态信号、脉冲分析汉宁窗较宽较好-31dB通用频谱分析汉明窗较宽好-43dB语音信号处理布莱克曼窗宽很好-58dB需要低旁瓣的场合凯泽窗可调可调需要灵活权衡的场合选择窗函数的原则是如果你需要精确的幅度信息选旁瓣低的窗如果你需要精确的频率信息选主瓣窄的窗。大多数通用频谱分析场景下汉宁窗是一个比较稳妥的选择。加窗的代价是窗函数会衰减信号两端的幅度导致频谱幅度整体下降。所以做完FFT之后需要乘以一个窗函数增益补偿系数来恢复幅度。这个系数通常是窗函数所有系数的平均值。4.3 补零提高的是显示分辨率不是真实分辨率很多人以为在信号末尾补零再FFT可以提高频率分辨率这是一个常见的误解。补零确实会让频谱图上的点变多看起来更“平滑”但它不会增加任何新的频率信息。补零的本质是在频域进行插值。它让频谱曲线上的点更密但两个相邻真实频率成分之间的可分辨性并没有提高。真正的频率分辨率只取决于实际采集到的信号时长即$\Delta f 1/T$其中T是采集时间。那补零有什么用主要有两个一是让频谱曲线看起来更光滑便于观察峰值位置二是让点数变成2的整数次幂方便使用基2-FFT。我在实际项目中经常补零到2的整数次幂但从来不会指望补零能提高分辨率。4.4 一个完整的频谱分析代码示例下面是一个用Python做频谱分析的完整示例涵盖了采样、加窗、FFT、幅度归一化的全过程import numpy as np import matplotlib.pyplot as plt # 参数设置 fs 48000 # 采样率 N 1024 # 采样点数 t np.arange(N) / fs # 时间轴 # 生成测试信号1000Hz 3000Hz f1, f2 1000, 3000 x 0.7 * np.sin(2 * np.pi * f1 * t) 0.3 * np.sin(2 * np.pi * f2 * t) # 加汉宁窗 window np.hanning(N) x_windowed x * window # 计算窗函数增益补偿系数 gain np.sum(window) / N # FFT X np.fft.rfft(x_windowed) freq np.fft.rfftfreq(N, 1/fs) # 幅度归一化 amplitude np.abs(X) / (N * gain) * 2 # 乘以2是因为rfft只取了一半频谱 # 绘制频谱图 plt.figure(figsize(10, 4)) plt.plot(freq, amplitude) plt.xlabel(Frequency (Hz)) plt.ylabel(Amplitude) plt.title(Spectrum Analysis) plt.grid(True) plt.show()这段代码里有几个细节值得注意。np.fft.rfft()是专门针对实数输入的FFT它只返回正频率部分比完整的fft()快将近一倍。幅度归一化的时候除以N * gain是为了补偿窗函数带来的幅度衰减乘以2是因为实数FFT只取了一半的频谱能量需要折算回来。如果你把这段代码跑一遍会看到频谱图上在1000Hz和3000Hz处有两个清晰的峰值幅度分别接近0.7和0.3和原始信号一致。这说明加窗和归一化的处理是正确的。5. 相位谱被大多数人忽略的另一半信息5.1 为什么相位谱看起来像噪声做完FFT之后你会得到复数结果。取模得到幅度谱取角度得到相位谱。大多数人在做频谱分析的时候只看幅度谱相位谱直接忽略。这其实丢掉了一半的信息。相位谱之所以看起来像噪声是因为对于大多数信号来说相位在频率轴上的变化非常剧烈。相邻两个频点的相位可能相差很大画出来就是一团乱麻。但这并不意味着相位没有用。在有些应用场景里相位信息比幅度信息更重要。比如在音频合成中如果你只保留幅度谱而把相位全部置零再逆变换回时域声音会变得非常奇怪像金属敲击声。这是因为人耳对相位的变化虽然不如对幅度敏感但相位决定了波形的形状对音色有重要影响。5.2 相位解缠绕让相位谱变得可读相位谱看起来乱一个重要的原因是相位缠绕。np.angle()返回的相位范围是$(-\pi, \pi]$当真实相位超过这个范围时就会被折叠回来产生跳变。解决方法是做相位解缠绕phase unwrapping。它的基本思路是检测相邻两个相位值之间的跳变如果跳变超过$\pi$就认为发生了缠绕通过加减$2\pi$来修正。# 相位解缠绕 phase np.angle(X) phase_unwrapped np.unwrap(phase)np.unwrap()就是NumPy提供的解缠绕函数。解缠绕之后的相位谱通常是一条比较平滑的曲线更容易分析。5.3 群延迟相位谱的导数在滤波器设计和信号传输分析中群延迟是一个非常重要的概念。它定义为相位对频率的负导数$$\tau_g(\omega) -\frac{d\phi(\omega)}{d\omega}$$群延迟的物理含义是不同频率成分通过系统之后延迟时间是多少。如果群延迟在通带内是常数说明所有频率成分延迟相同信号波形不会失真。如果群延迟随频率变化就会产生相位失真表现为信号波形变形。我在做音频均衡器的时候就特别关注群延迟。一个设计不好的均衡器虽然幅度响应看起来没问题但群延迟在某些频段可能很大导致声音听起来“糊”。后来改用线性相位FIR滤波器群延迟在整个频带内都是常数声音就干净多了。6. 实际项目中踩过的坑与经验总结6.1 频谱泄漏导致的误判有一次做一个电机振动监测的项目需要在频谱图上找异常频率成分。一开始没加窗频谱图上到处都是“毛刺”我以为是电机里有什么异常振动排查了半天才发现是频谱泄漏造成的假象。后来加了汉宁窗频谱图立刻干净了很多真正的振动峰值清晰可见。这个经历让我养成了一个习惯只要做频谱分析先加窗再说。除非你分析的是整周期信号且采样长度恰好是信号周期的整数倍否则不加窗的频谱图基本没法看。6.2 采样率设置过低导致的混叠另一个常见的坑是采样率设置过低。根据奈奎斯特采样定理采样率必须大于信号最高频率的两倍。如果信号里有高于$f_s/2$的频率成分就会发生混叠高频成分会被“折叠”到低频区域在频谱图上出现在错误的位置。混叠的可怕之处在于你无法从采样后的数据中判断是否发生了混叠。一旦混叠发生高频成分和低频成分就混在一起了没有任何算法能把它们分开。唯一的解决办法是在采样之前加抗混叠滤波器把高于$f_s/2$的频率成分滤掉。我在做音频采集的时候一般会在ADC前面加一个截止频率略低于$f_s/2$的低通滤波器。虽然这会损失一点高频信息但总比混叠要好。6.3 浮点精度问题FFT涉及大量的浮点运算在嵌入式平台上如果使用单精度浮点累积误差可能会比较明显。特别是点数比较大的时候误差会随着运算层数增加而累积。我在一个项目中用单精度浮点做4096点FFT发现底噪比预期高了将近20dB。后来改用定点运算并且对每一步的缩放做了精细控制底噪才降下来。如果平台支持双精度浮点那当然最好但双精度的计算速度会慢不少需要根据实际需求权衡。6.4 实时频谱显示的帧率优化做实时频谱显示的时候帧率是一个关键指标。如果每帧都要做一次完整的FFT在低端MCU上可能跑不到30帧。我的优化经验是使用实数FFT比复数FFT快将近一倍。降低点数如果不需要很高的频率分辨率用256点或512点就够了。使用查表法旋转因子预先算好避免实时计算三角函数。利用DMAADC采集和FFT计算可以并行进行用DMA搬运数据CPU专心算FFT。重叠处理如果帧率要求很高可以采用重叠保留法每次只更新一部分数据。这些优化手段组合起来我在一块主频80MHz的MCU上做到了1024点FFT、30帧每秒的频谱显示效果相当流畅。6.5 关于窗函数选择的个人经验最后再聊一下窗函数的选择。我个人的经验是通用频谱分析汉宁窗主瓣和旁瓣的折中比较好。需要精确幅度平顶窗主瓣很宽但幅度精度极高。瞬态信号分析矩形窗不要加窗因为加窗会模糊瞬态的时间信息。语音处理汉明窗这是语音处理领域的标准选择。需要灵活调节凯泽窗通过调节参数可以在主瓣和旁瓣之间连续权衡。如果你不确定用哪个先用汉宁窗试一下大多数情况下都不会太差。等你有特殊需求了再根据上面的原则去选更合适的窗。傅里叶变换这个东西光看公式是学不会的。你得动手写代码拿真实信号去试看频谱图长什么样加窗和不加窗有什么区别补零和不补零有什么变化。试得多了自然就有感觉了。我在最开始学的时候也是反复折腾了好几个星期才把DTFT和DFT的关系搞清楚。现在回头看那些折腾都是值得的因为后面学滤波器、学调制解调的时候这些基础概念会反复出现。基础打牢了后面就是一马平川。