1. 从一个采样率翻车的夜晚说起
三年前调试一套电机振动监测系统,加速度传感器输出接进采集卡,采样率设定为 10 kHz,理论上能覆盖到 5 kHz 以下的信号。可抓出来的频谱图在 2.8 kHz 附近总有一个莫名其妙的尖峰,把轴承内圈故障频率的边带完全淹没了。我们换了传感器、换了采集卡、甚至怀疑是机械共振,折腾到凌晨两点才发现:真正的问题出在没做抗混叠滤波,4.8 kHz 附近的一个高频谐波被折叠到了 2.8 kHz 处。
这件事让我意识到一个很朴素的道理——离散傅里叶变换和频谱分析,这两个词在教科书里通常是连着出现的,但真正做过实际信号处理的人都知道,DFT 本身并不可怕,可怕的是你在把连续信号喂给 DFT 之前忽略的那些环节。采样定理、频谱泄露、窗函数选择、频率分辨率、幅度谱归一化,任何一个环节没想清楚,你看到的频谱都可能是一张"谎言地图"。
这篇博文想写给那些已经在用 NumPy、MATLAB 或者嵌入式 DSP 库做频谱分析,但总觉得结果"看着对、用着虚"的同学。我会从 DFT 的数学骨架讲起,一步步过渡到工程实现,再把踩过的坑整理成可直接查的排查表。如果你正在做振动分析、音频处理、电力谐波检测、雷达信号处理或者任何需要"把时间波形变成频率柱状图"的活儿,下面的内容应该能帮你省掉几个通宵。
2. 为什么连续傅里叶变换不够用
2.1 计算机不认识连续函数
连续傅里叶变换的定义是一个积分:
$$X(f) = \int_{-\infty}^{+\infty} x(t) e^{-j2\pi ft} dt$$
这个公式在数学上很漂亮,但计算机做不了两件事:第一,它无法存储无限长的时间信号;第二,它无法计算连续积分。所以工程上必须做两步离散化:把时间轴切成 $N$ 个等间隔的点,把频率轴也切成 $N$ 个等间隔的点。这一刀切下去,就有了离散傅里叶变换。
理解 DFT 最直观的方式,是把它当成"对连续频谱的一次等间隔采样"。假设原始信号 $x(t)$ 的频谱是 $X(f)$,采样周期 $T_s$,采样点数 $N$,那么 DFT 输出的第 $k$ 个频率点,对应的物理频率是 $k \cdot f_s / N$,其中 $f_s = 1/T_s$。这个 $f_s / N$ 就是频率分辨率,它决定了你能否把两个靠得很近的谱峰分开。
很多人第一次看到 DFT 公式会觉得别扭,因为它不是对称的:
$$X[k] = \sum_{n=0}^{N-1} x[n] e^{-j2\pi kn/N}$$
其实可以这样理解:$e^{-j2\pi kn/N}$ 是一组正交基,DFT 就是在问"信号 $x[n]$ 里含有多少这个频率的成分"。正交性保证了每个频率点的投影互不干扰,这是 DFT 可逆、能无损重构的前提。
2.2 DFT 和 FFT 的关系:别把两者混为一谈
这是我带新人的时候必问的一个问题:DFT 和 FFT 有什么区别?正确答案是——DFT 是变换的定义,FFT 是计算 DFT 的一种快速算法。DFT 的暴力计算复杂度是 $O(N^2)$,FFT 通过分治把复杂度降到 $O(N \log N)$。当 $N = 1024$ 时,FFT 大约比暴力 DFT 快 100 倍;当 $N = 1$ 048 576 时,差距超过 5 万倍。
这意味着什么?意味着你在实际工程中调用np.fft.fft()的时候,底层走的是 FFT 算法,但输出的结果在数学上与 DFT 定义完全一致。你不需要自己写蝶形运算,但你必须知道 FFT 对点数有偏好——绝大多数实现要求点数是 2 的幂或者可以分解为小素数的乘积。如果你的数据长度是质数,FFT 库可能会退化成慢速算法,甚至直接报错。
实操提示:做 FFT 之前,先把数据补齐到最接近的 2 的幂长度。补零不会增加真实频率分辨率,但能让 FFT 跑得快、让频谱曲线更平滑。
2.3 频率分辨率的真相:物理分辨率 vs 视觉分辨率
这是最容易混淆的一个点。假设采样率 1000 Hz,采集 1 秒数据,得到 1000 个点,做 1000 点 FFT,频率分辨率是 1 Hz。如果你把这 1000 个点补零到 8192 点再做 FFT,频率轴上的点距变成 0.122 Hz,看起来"分辨率提高了",但实际上两个原本间隔 1 Hz 以内的信号依然无法被分辨。
原因很简单:物理频率分辨率只取决于实际观测时长 T,即 $\Delta f = 1/T$。补零只是在原有频谱上做插值,让曲线更光滑,不会凭空变出新的频率信息。我把这个规律总结成一句话给团队新人:采样率决定你能看到多高的频率,观测时长决定你能分清多近的频率。
| 参数 | 决定因素 | 典型影响 |
|---|---|---|
| 最高分析频率 | 采样率 $f_s$ | 只能看到 $f_s/2$ 以下的信号 |
| 物理频率分辨率 | 观测时长 $T$ | 间隔小于 $1/T$ 的谱峰无法分开 |
| 频率轴点距 | FFT 点数 $N_{\text{fft}}$ | 补零可减小点距,但不提升物理分辨率 |
| 幅度精度 | 窗函数与归一化方式 | 直接影响谱峰高度的可信度 |
3. 从采样到频谱:工程实现的关键环节
3.1 采样定理不是"大于两倍"就万事大吉
奈奎斯特采样定理说采样率要大于信号最高频率的两倍,但实际工程中我建议至少取 2.5 倍甚至 5 倍。为什么?因为真实信号几乎不可能是严格带限的,总会有高频噪声、谐波或者瞬态成分。如果你的采样率刚好卡在 2 倍,任何高于 $f_s/2$ 的成分都会折叠回低频,造成混叠。
抗混叠滤波器是必须的。模拟前端要放一个截止频率略低于 $f_s/2$ 的低通滤波器,把高频成分在采样之前就砍掉。我见过太多项目为了省一个运放和几个电容,结果在频谱上花了十倍时间做后处理,得不偿失。
判断是否发生混叠有一个简单方法:改变采样率,看频谱峰的位置是否跟着变。如果某个峰的位置固定不变,那很可能是真实信号;如果峰的位置随采样率变化而移动,那基本可以确定是混叠产物。
3.2 窗函数:不是可选项,是必选项
对有限长数据做 FFT,本质上是对无限长信号乘了一个矩形窗。矩形窗的频谱有较大的旁瓣,会导致强信号的旁瓣淹没附近的弱信号,这就是频谱泄露。
不同的窗函数在"主瓣宽度"和"旁瓣衰减"之间做取舍:
| 窗类型 | 主瓣宽度(bin) | 旁瓣衰减(dB) | 适用场景 |
|---|---|---|---|
| 矩形窗 | 0.89 | -13 | 瞬态信号、整周期采样 |
| 汉宁窗 | 1.44 | -31 | 连续信号通用分析 |
| 汉明窗 | 1.30 | -43 | 音频、语音分析 |
| 布莱克曼窗 | 1.68 | -58 | 强弱信号共存场景 |
| 平顶窗 | 2.94 | -70 | 幅度精确测量 |
选窗的核心逻辑是:如果你关心频率定位精度,选主瓣窄的;如果你关心幅度精度,选旁瓣低的。汉宁窗是最通用的默认选择,我大概 70% 的场合都用它。
注意:加窗之后,信号的幅度会被衰减,必须在频域做补偿。常见的补偿系数是窗函数时域序列的均值,汉宁窗的幅度补偿系数约为 2.0,汉明窗约为 1.85。
3.3 单边谱与双边谱:为什么你看到的幅度是两倍
FFT 输出的是双边谱,频率范围从 $-f_s/2$ 到 $+f_s/2$。对于实信号,正负频率的幅度是对称的,所以我们通常只看正半轴,把负半轴的能量合并过来,这就是单边谱。
单边谱的幅度换算规则很简单:直流分量($k=0$)和奈奎斯特分量($k=N/2$)保持不变,其余频率点的幅度乘以 2。如果你用np.abs(fft_result)直接画图,看到的幅度会比真实物理幅度小一半(对非直流分量而言)。
归一化还要除以 FFT 点数 $N$。完整的单边幅度谱计算公式:
$$A[k] = \frac{2}{N \cdot C} \left| X[k] \right|, \quad k = 1, 2, \ldots, N/2-1$$
其中 $C$ 是窗函数的幅度补偿系数。这个公式我建议每个做频谱分析的人都亲手推导一遍,否则你永远不知道自己画的谱对不对。
3.4 频率轴的构建:别再手算 bin 号了
频率轴第 $k$ 个点对应的物理频率:
$$f_k = k \cdot \frac{f_s}{N_{\text{fft}}}$$
其中 $N_{\text{fft}}$ 是实际做 FFT 的点数(补零后的点数),不是原始数据长度。这一点很容易搞混。如果你用 1000 个原始点补零到 8192 点做 FFT,频率轴要按 8192 来算,但物理分辨率仍然由 1000 个点的观测时长决定。
代码层面,我通常这样构建频率轴:
import numpy as np fs = 10000 # 采样率 N_original = 5000 # 原始点数 N_fft = 8192 # 补零后 FFT 点数 freq_axis = np.fft.rfftfreq(N_fft, d=1/fs) # 或者手动构建 freq_axis_manual = np.arange(N_fft // 2 + 1) * fs / N_fftnp.fft.rfftfreq返回的是单边谱的频率轴,长度是 $N/2+1$,直接对应np.fft.rfft的输出。用这个组合可以避免拼接负频率的麻烦。
4. 频谱分析实操全流程
4.1 完整代码框架:从原始数据到可读频谱
下面这套流程是我在实际项目中反复使用并逐步固化的。它包含去均值、加窗、FFT、幅度归一化、单边谱提取五个步骤,适用于大多数振动、音频、电力信号分析场景。
import numpy as np import matplotlib.pyplot as plt def spectrum_analyze(x, fs, window='hann', n_fft=None): """ 输入: x: 一维时域信号 fs: 采样率 window: 窗函数类型 n_fft: FFT 点数,默认为最接近数据长度的 2 的幂 输出: freq: 频率轴 (Hz) amp: 单边幅度谱 """ N = len(x) if n_fft is None: n_fft = 2 ** int(np.ceil(np.log2(N))) # 1. 去均值,消除直流偏置 x = x - np.mean(x) # 2. 加窗 if window == 'hann': win = np.hanning(N) cg = 0.5 # 汉宁窗幅度补偿系数 elif window == 'hamming': win = np.hamming(N) cg = 0.54 else: win = np.ones(N) cg = 1.0 x_win = x * win # 3. FFT X = np.fft.rfft(x_win, n=n_fft) # 4. 幅度归一化 amp = np.abs(X) / (N * cg) amp[1:-1] *= 2 # 单边谱非直流分量乘 2 # 5. 频率轴 freq = np.fft.rfftfreq(n_fft, d=1/fs) return freq, amp这段代码里有几个细节值得单独拎出来说。x = x - np.mean(x)这一步很多人会忽略,但直流分量在频谱上就是一个巨大的 0 Hz 峰,它的旁瓣可能污染低频段。归一化除以的是 $N$ 而不是 $n_{\text{fft}}$,因为窗函数只加在原始数据上,补零部分没有信号能量。amp[1:-1] *= 2跳过了直流和奈奎斯特点,这两个频率点不应该乘 2。
4.2 参数选择的计算过程
假设你要分析一个电机振动信号,关注的故障频率在 100 Hz 到 2000 Hz 之间,其中有两个特征频率间隔约 8 Hz,需要把它们分开。
第一步,确定采样率。关注最高频率 2000 Hz,考虑 2.5 倍以上余量,选 $f_s = 10000$ Hz。这样最高分析频率 5000 Hz,有足够余量。
第二步,确定观测时长。要分辨 8 Hz 间隔,物理分辨率 $\Delta f \leq 8$ Hz,所以观测时长 $T \geq 1/8 = 0.125$ 秒。实际工程中取 1 秒更稳妥,于是原始点数 $N = f_s \times T = 10000$ 点。
第三步,确定 FFT 点数。10000 点不是 2 的幂,最接近的是 16384。补零到 16384 点后,频率轴点距变成 $10000 / 16384 \approx 0.61$ Hz,曲线更光滑。但物理分辨率仍然是 $1/1 = 1$ Hz。
第四步,确定窗函数。间隔 8 Hz、分辨率 1 Hz,谱峰间隔 8 个 bin,汉宁窗主瓣宽度 1.44 bin,不会造成明显遮蔽。同时汉宁窗旁瓣衰减足够,选它。
这套参数验证下来,100 Hz 到 2000 Hz 范围内的谱峰应该都能清晰呈现,8 Hz 间隔的两个峰也能分开。
4.3 频谱平均:降低方差的有效手段
单次 FFT 得到的频谱方差很大,谱线看起来毛刺很多。工程上常用的做法是分段平均,也就是把长数据切成若干段,每段分别做 FFT,然后对幅度谱求平均。
这里有一个经典权衡:段数越多,方差越小,但每段越短,频率分辨率越差。假设你有 10 秒数据,采样率 10 kHz,总共 100000 点。
- 分 10 段,每段 10000 点,分辨率 1 Hz,平均 10 次
- 分 50 段,每段 2000 点,分辨率 5 Hz,平均 50 次
如果你的目标是检测间隔较远的谱峰,选第二种;如果要分辨靠得很近的谱峰,只能牺牲平均次数选第一种。
分段时还有一个细节:段与段之间通常设置 50% 重叠,这样可以在不增加数据总长的前提下增加平均次数,同时减少因分段边界造成的信息丢失。这就是 Welch 方法的雏形。
4.4 对数谱与线性谱:别只会看一种
线性幅度谱适合观察谱峰的绝对幅度,比如判断某个频率成分是否超标。但线性谱的缺点是动态范围有限,弱信号容易被强信号的旁瓣淹没。
对数谱(dB 谱)把幅度取 20 倍对数,动态范围一下子拉开。计算公式:
$$A_{\text{dB}}[k] = 20 \log_{10} \left( \frac{A[k]}{A_{\text{ref}}} \right)$$
$A_{\text{ref}}$ 通常取 1 或者信号的最大幅度。对数谱适合观察谐波结构、噪声底、弱边带。我自己的习惯是:先看线性谱确认主峰位置和幅度,再看对数谱找谐波和边带。
提示:对数谱不能有零值,计算前加一个小常数如 $10^{-12}$ 避免 $\log(0)$。
5. 常见问题与排查技巧实录
5.1 频谱峰位置不对:先查频率轴再查信号
这是出现频率最高的问题。频谱上的峰位置与理论值对不上,通常有三类原因。
第一类是频率轴算错了。最常见的是把补零前的点数当成 FFT 点数来算频率轴,或者用了双边谱的频率轴去对应单边谱的数据。排查方法很简单:输入一个已知频率的正弦波,看峰是否落在正确位置。
第二类是采样率设置错误。采集卡的实际采样率与代码里写的 $f_s$ 不一致,可能是硬件分频、时钟源配置或者驱动层做了重采样。排查方法是采集一个标准信号源输出,比如 1 kHz 正弦,看频谱峰是否在 1 kHz。
第三类是信号本身就不是你以为的那个频率。比如电机转速波动导致基频漂移,或者传感器安装方式改变了共振频率。这时候要结合时域波形一起看。
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 峰位置整体偏移 | 采样率设置错误 | 采集已知频率标准信号验证 |
| 峰位置成倍数偏差 | 频率轴点数计算错误 | 检查是否用补零点数构建频率轴 |
| 低频出现大峰 | 直流偏置未去除 | 检查去均值步骤 |
| 高频出现鬼峰 | 混叠 | 改变采样率观察峰是否移动 |
| 峰两侧不对称 | 窗函数选择不当 | 换用旁瓣更低的窗 |
5.2 幅度不对:归一化系数逐项核对
幅度出错的原因比频率出错更隐蔽,因为频率轴一眼就能看出对错,幅度却需要跟理论值仔细比对。我见过最多的错误是忘记除以 $N$,导致幅度大得离谱;其次是忘记做单边谱乘 2,导致幅度小一半;第三是窗函数补偿系数没加,幅度小了约一倍。
正确的归一化流程是:先除以原始数据长度 $N$,再除以窗函数补偿系数 $C$,最后对非直流分量乘 2。三者的顺序不影响结果,但缺一不可。验证方法:生成一个幅度为 1 的正弦波,做完整流程,看输出谱峰是否接近 1。
5.3 频谱泄露:如何判断和抑制
泄露的典型表现是谱峰底部变宽,像一座山的"裙边"拖得很长。如果裙边淹没了旁边的弱信号,说明泄露已经影响到分析结果。
判断泄露是否严重,可以看谱峰两侧的衰减速度。理想情况下,远离主瓣后幅度应该快速下降到噪声底。如果下降很慢,或者有明显的周期性起伏,说明窗函数旁瓣太高或者信号没有整周期截断。
抑制泄露的方法有三条:选旁瓣更低的窗(汉宁换布莱克曼)、增加观测时长让信号更接近整周期、如果信号周期已知,直接按整周期截断。第三条最彻底,但需要知道信号基频,适合转速稳定的旋转机械分析。
5.4 频率分辨率不够:补零救不了你
新手最常见的误解是"补零能提高分辨率"。补零只能让频率轴更密,让曲线更平滑,不能让两个原本分不开的谱峰分开。真正的解决方法是增加观测时长。
假设你要分辨间隔 5 Hz 的两个信号,采样率 10 kHz。最少需要观测 0.2 秒,即 2000 个点。如果你只有 1000 个点(0.1 秒),分辨率只有 10 Hz,无论怎么补零都分不开。
遇到这种情况,要么延长采集时间,要么用参数估计方法(如 MUSIC、ESPRIT)做超分辨率分析。后者属于进阶话题,适合信噪比高、信号模型明确的场景。
5.5 实时频谱分析中的帧长与刷新率权衡
做实时频谱显示的时候,帧长和刷新率是一对矛盾。帧长越长,频率分辨率越高,但每帧计算耗时越长,刷新率越低。帧长越短,刷新快,但分辨率粗糙。
我的经验值是:刷新率保持在 10 到 20 帧每秒比较舒适,人眼不会觉得卡顿。基于这个刷新率,单帧处理时间要控制在 50 到 100 毫秒以内。对于 10 kHz 采样率、16 位精度、单通道信号,这个时间预算足够处理 8192 点左右的 FFT。
如果信号采样率很高,比如 1 MHz,单帧 8192 点只覆盖 8 毫秒,分辨率只有 125 Hz,很多细节看不到。这时候要么降低刷新率,要么用多分辨率分析策略:高频段用短帧快速刷新,低频段用长帧慢速刷新。
6. 进阶话题与个人实践体会
6.1 相位谱的价值被严重低估
绝大多数人只关注幅度谱,把相位谱当成可有可无的附属品。但在某些场景下,相位信息才是关键。比如判断两个通道信号的时延,用互谱的相位斜率可以精确到采样点以内;再比如做模态分析,相位关系能区分同频不同振型的信号。
相位谱的计算要注意:相位是相对于 FFT 起点的,如果每次 FFT 的起点没有对齐,相位会随机跳变。做相位相关分析时,必须保证各次采集的触发时刻一致。
6.2 功率谱密度与功率谱的区别
功率谱(PSD)和功率谱密度(PSD,Power Spectral Density)是两个不同的量。功率谱的单位是幅度的平方,表示某个频率 bin 内的功率;功率谱密度的单位是功率每赫兹,表示单位带宽内的功率密度。两者只在频率分辨率归一化上有差异,但混用会导致量纲错误。
做随机信号分析、噪声测量、振动总级值计算时,必须用功率谱密度,否则改变 FFT 点数会改变总功率计算结果,这是不对的。功率谱密度的归一化系数要除以频率分辨率 $\Delta f$。
6.3 我的个人实践清单
做了几年频谱分析,我总结了一张"上电前必查清单",每次开始一个新项目都会过一遍:
- 传感器带宽是否覆盖关注频率范围
- 抗混叠滤波器截止频率是否低于 $f_s/2$
- 采样率与关注最高频率的比例是否大于 2.5
- 观测时长是否满足频率分辨率要求
- 窗函数类型是否匹配信号特征
- 归一化系数是否逐项核对过
- 是否用已知信号源验证过整条链路
- 对数谱和线性谱是否都看过
这八条看起来基础,但每一条我都至少踩过一次坑。尤其是最后一条,很多隐蔽的谐波和边带只有在对数谱上才看得清楚。
6.4 关于学习路径的一点建议
如果你刚接触 DFT 和频谱分析,我的建议是先不要急着上代码。找一本信号处理教材,把 DFT 的定义、性质、帕塞瓦尔定理、循环卷积这几块手推一遍。然后在纸上画一个 8 点 DFT 的蝶形图,亲手算一遍输入输出。这些看起来"低效"的练习,会在你后面调参的时候变成直觉。
真正开始做工程的时候,从正弦波加白噪声这种最简单的信号入手,逐步增加复杂度:单频加谐波、多频加噪声、调幅信号、调频信号、瞬态冲击。每增加一种信号类型,就回头看一下频谱是否符合预期。这个过程大概需要两三个月,但走完之后,你看到任何频谱图都不会发怵。
频谱分析这件事,工具和方法都是公开的,差距就在对细节的把控上。频率轴多一点少一点、窗函数换一个、归一化差一项,结果可能完全不同。这些细节没法从教科书上直接学到,只能在反复实践和排查中积累。希望这篇整理能帮你少走几段弯路。