
做信号处理久了你会发现 DFT 是个非常有意思的工具你把一段时域数据扔进 FFT它却把频谱、幅值、相位、泄漏、噪声全部揉在一起还给你。处理得顺的时候它像一把趁手的刀处理不顺的时候你会怀疑自己拿到的不是频谱而是一堆坏掉的巧合。最近又帮人调了一轮基于 FFT 的频谱测量正好把问答里反复出现的问题再整理一篇。这篇不列教材式的推导只讲实际工作中绕不开的几个坎频率分辨率到底怎么算、为什么总泄漏、窗函数怎么配合幅值校准、以及现场排查时最容易忽略的小细节。熟悉这个系列的朋友应该知道我写的基本都是实战向的内容。DFT 这个话题虽然老但每次帮人排查问题我都能发现一些文档里不会写的坑。这一篇继续用问答的形式把那些“看着简单、上手就错”的点拆开讲清楚。1. 频率分辨率到底由什么决定1.1 相邻谱线的间隔不是 FFT 点数说了算很多人一上来就盯着“FFT 点数”看总觉得点数越大频率分辨率越高。这个说法只对了一半。DFT 相邻谱线的间距是 Δf Fs / N其中 Fs 是采样率N 是参与变换的采样点数。但这里有个容易被忽略的细节N 不是你想填多少就填多少的变换长度而是你实际采集到的、真实信号持续的时间长度。假如采样率 Fs 8000 Hz采样点数 N 8000那么数据时长 T 1 秒Δf 1 Hz。如果你只采集了 0.5 秒也就是 N 4000 点就算你把 FFT 长度设成 4096、8192 甚至 16384原始 DFT 结果中相邻谱线的间距依然由真实采样时长决定大约是 2 Hz。补零只是让显示出来的谱线更密并不会把 2 Hz 变成 1 Hz。所以设计测量系统时一定要先问自己一个问题我需要分辨多小的频率间隔这个间隔决定了最小采样时长。比如要测量电网谐波50 Hz 基波、100 Hz 二次谐波中间间隔 50 Hz理论上只要 T ≥ 20 ms 就能把相邻谐波分开。但如果你要分辨 1000 Hz 和 1001 Hz 两个音频分量T 至少要 1 秒。采样率再高也没用采得再密数据时长不够两个峰还是会糊在一起。1.2 零填充只是插值不是外推“那我多补一些零频率分辨率不就上去了吗”这是数据时长相关的问题里最经典的一个误解。零填充确实会让频谱曲线的形状变得更平滑峰值位置看起来更准确但本质上是把原来的 DFT 结果在频域做了插值。原来两个真峰之间没有分离补零之后依然不会分离原来只有一个峰补零之后也不会凭空多出一个峰。举个例子两个正弦信号一个 1000 Hz一个 1001 Hz采样率 8000 Hz数据长度 0.5 秒。此时 Δf 2 Hz1000 Hz 和 1001 Hz 会落在同一个谱线附近两个分量基本叠在一起。你不管在后面补多少零DFT 长度拉到 65536这两个峰还是分不开。真正要分开它们只能把采样时间拉长到 1 秒以上。零填充真正有用的场景是找峰位。当你已经确认频谱里只有一个主峰希望通过插值获得更精确的频率估计时补零比直接找最大谱线更稳。因为它不是改变分辨率而是让峰值附近的曲线更细腻配合抛物线插值可以估到十分之一甚至百分之一个谱线间隔。记住这句话零填充用来“找位置”可以用来“分双峰”不行。1.3 数据越长越好吗非平稳信号要换思路既然数据时长决定分辨率那是不是无脑拉长窗口就行了不是。DFT 的前提是信号在观察窗口内相对平稳。如果你把一段频率一直在变化的信号硬塞进一个很长的窗口得到的结果不是“高频谱分辨率”而是一段被时间平均过的模糊频谱。我处理过一台振动监测设备的实时频谱电机启动瞬间频率从 20 Hz 快速爬升到 50 Hz。用 1 秒窗口做 FFT频谱里能看到一片“频率云雾”改用 0.1 秒窗口才能看频率随时间变化的趋势。所以做监测类系统时要在时间分辨率和频率分辨率之间取舍。想捕捉瞬态变化窗口要短想分辨稳定信号的微小频率差窗口才需要长。这个道理在做短时傅里叶变换时尤其明显窗口短则时间定位好、频率定位差窗口长则反过来。从工程角度说最实用的选择方法是先把信号特点摸清楚信号在目标时长内是不是稳定的如果是尽量拉长时间如果不是宁可牺牲一点分辨率也要保证频谱反映的是当下状态。2. 频谱泄漏与加窗很多人第一步就错了2.1 泄漏的本质DFT 在“假装”信号是周期的DFT 数学上默认你给的这段数据是一个无限长周期信号的一个周期。可现实里你截出来的信号起点和终点往往不连续。比如一个 50 Hz 正弦波你用 1000 Hz 采样率采了 25 个点这 25 个点并不是整数个周期起点和终点幅值完全对不上。DFT 把这个波形当成周期信号来展开时两个周期之间就会出现一个巨大的跳变频谱上就表现为本不该有的能量散布到各个频率上这就是频谱泄漏。拿生活里的场景类比切蛋糕时如果刀口没有对齐蛋糕上的花纹切出来的边缘一定是花的。泄漏就是这个“没对齐”的刀口。如果你的采样时长恰好是信号周期的整数倍信号起点和终点能接上泄漏就很小。可实际频率千变万化采样率往往是固定的整数周期这种理想情况很少发生。2.2 常见窗函数的特点与选择既然做不到整周期采样就得通过加窗来降低起点和终点的跳变幅度。窗函数的作用是把数据两端逐渐衰减到零让截断处的“不连续”变得不明显。代价是主瓣变宽本来很尖锐的峰被展宽靠近的两个峰更难分开。不同窗函数的选择本质上是“主瓣宽度、旁瓣衰减、幅值精度”三者的权衡。我整理了一个非常实用的对照表窗函数主瓣宽度旁瓣衰减幅值误差最适合的场景矩形窗1 个谱线-13 dB最大约 3.9 dB整周期采样或瞬态信号汉宁窗2 个谱线-31 dB约 1.4 dB通用频谱分析兼顾分辨率和泄漏海明窗2 个谱线-43 dB约 1.8 dB窄带随机信号旁瓣要求稍高布莱克曼-哈里斯窗4 个谱线-92 dB约 0.8 dB需要极低旁瓣弱信号检测平顶窗5 个谱线-90 dB 左右小于 0.01 dB精确测量幅值不关心频率分辨率实际项目里最常用的组合是汉宁窗加幅值校准。它主瓣不算太宽旁瓣衰减也够用适合绝大多数连续频谱测量。如果是为了精确读幅值比如校准传感器灵敏度我一般用平顶窗。如果是在一堆强信号旁边找微弱信号比如通信系统里检测邻道泄露就优先用布莱克曼-哈里斯窗。2.3 加窗后的幅度校准加窗不是乘一下就完事了。窗函数把数据两端压小以后信号总能量会发生变化直接对频谱取幅值结果一定偏小。很多人第一次用汉宁窗发现“信号明明是 1 VFFT 峰值却只有 0.5 V”就是因为没做幅度校准。校准的办法是计算窗函数的“相干增益”也就是窗序列的平均值。汉宁窗的相干增益大约是 0.5所以校准后的幅值要除以 0.5也就是乘 2。更通用的公式是校准幅值 |X_windowed[k]| / N * 2 / (sum(w) / N)其中 w 是窗函数序列。sum(w) / N 就是相干增益。注意这里的 2 是单边谱转换用的后面还会细说。我在实际代码里一般这样写import numpy as np fs 8000 t np.arange(8000) / fs x 2.0 * np.sin(2 * np.pi * 1001 * t) w np.hanning(len(x)) xw x * w X np.fft.rfft(xw) / len(xw) * 2 / np.mean(w) freqs np.fft.rfftfreq(len(xw), 1 / fs) idx np.argmax(np.abs(X)) print(f峰值频率: {freqs[idx]:.3f} Hz, 幅值: {np.abs(X[idx]):.3f})这段代码最关键的就是“除以相干增益”。如果不除校准后幅值会只有真实值的一半左右。之前有不少朋友抄了别人的代码最后发现幅值对不上基本都是漏了这一步。2.4 做实验前先确认采样时钟是不是“真干净”在实际工程里比窗函数更隐蔽的泄漏源是采样时钟抖动。如果你的 ADC 触发时钟不稳定采样点的时间间隔不是严格均匀的那么即便你加再好的窗频谱旁边还是会拖出一条“毛边”。这个现象在水电、风电等强干扰环境里尤其常见。排查方法很直接给 ADC 输入一个标准正弦信号用已知的好时钟采样看频谱主峰旁边有没有异常展宽或对称毛刺。如果有先怀疑供电和时钟电路没有的话再怀疑算法。这个顺序比一上来就换窗函数有效得多。3. 幅值、相位与单边谱别让结果带着“伪装”3.1 单边谱幅度换算为什么要乘 2哪里不能乘很多刚接触 FFT 的人都会遇到一个困惑幅值明明应该是 1FFT 结果却只有 0.5。这里除了窗函数的影响还涉及单边谱和双边谱的换算。实数信号的频谱具有共轭对称性正负频率各占一半能量。如果你只用正频率来显示频谱就得把幅值乘 2才能补回负频率那部分能量。所以标准的单边幅值谱是单边幅度谱 2 * |X(k)| / N但有两个特殊位置不能乘 2直流分量和奈奎斯特频率。直流分量只有一个不存在正负频率配对奈奎斯特频率在实数 FFT 输出里也只出现一次。在这两个点上乘 2幅值会直接翻倍导致结果错误。这个坑的典型表现是FFT 结果里 DC 那一根谱线总是比预期大一倍。如果你看到直流分量一直“虚高”先检查是不是 DC 处也机械地乘了 2。3.2 加窗之后幅值越修越乱加窗和乘 2 这两个操作经常混在一起顺序一错结果全乱。正确流程是先给时域数据乘窗函数然后做 FFT再除以 N再乘以单边谱的 2最后除以窗函数的相干增益。用公式写就是完整校准后幅值 |FFT(x * w)| / N * 2 / (sum(w) / N)不少代码只做了一半有人乘了 2 忘了除相干增益有人记得相干增益但忘了乘 2。这两个操作看起来都是“乘一个数”含义却不同。我的习惯是先写注释再写代码把每一步的物理含义标注清楚否则过两周自己看都容易糊涂。另外做多通道采集时每个通道的窗函数要和数据长度完全一致。假如一个通道 N 2048另一个通道 N 4096两边窗函数的相干增益不同校准系数就不能混用。3.3 相位的读取与整周期采样DFT 的相位信息可以直接从频点的复数结果里取相位 arctan(虚部 / 实部)。但工程测量里相位经常读不准主要原因是截断位置不理想。如果信号包含多个频率分量而你的采样时长不是各分量的整数周期泄漏会同时污染幅度和相位。越是偏旁瓣严重的窗函数相位受到的污染越明显。所以要求相位精度时建议专门为被测频率设计采样时长让信号尽量满足整周期截断如果做不到就选择一个相位特性较好的窗函数比如汉宁窗并利用峰值谱线的实部和虚部来计算相位。还要注意FFT 输出相位默认是以时间原点为参考的。如果你做的是滑动窗口窗口每滑动一次原点位置就变了相位读数必须做对应的补偿。否则你会发现 FFT 谱线频率对、幅值对但相位一会正一会负完全没法用。4. 参数选型与工程实现采样率、点数、窗长的配合4.1 先定最小频率间隔再倒推采样时长正确设计一套频谱测量参数顺序一定是从需求倒推。第一步根据信号最高频率确定采样率。采样率至少要大于最高频率的两倍工程上还要留出抗混叠滤波器的过渡带余量一般取 2.5 到 4 倍。第二步根据你要分辨的最小频率间隔确定采样时长。比如要分辨 1 Hz就需要至少 1 秒的数据。这一步很多人会忽略结果采样率定得挺高数据时长却只有几十毫秒两个靠近的频率根本分不开。第三步把采样时长乘采样率得到采样点数 N。为了 FFT 计算效率以及后续处理方便我会把 N 圆整到接近的 2 的幂比如 2400 点圆整到 20488000 点圆整到 8192。圆整之后真实频率间隔会有一点点变化不影响大多数工程判断。举个例子做音频频谱分析关心 20 Hz 到 20 kHz要求在 100 Hz 处能分辨 1 Hz 的间隔。采样率至少取 48 kHz数据时长 1 秒N 取 48000 点。直接做 65536 点 FFT频率间隔约 0.73 Hz余量充足。如果信号是瞬态的只有 200 ms 有效时间那么 1 Hz 分辨率就达不到只能接受 5 Hz 分辨率。这是物理规律算法救不了。4.2 加窗会让“有效分辨率”变差设计时要留余量有些工程师学会了加窗却忘了窗函数会把主瓣去掉足够大。汉宁窗的主瓣宽度是 2 个谱线间隔意味着两个需要分开的频率至少要有 2 个 Δf 的距离否则主瓣会互相重叠看不出两个峰。比如你用汉宁窗做频谱理论分辨率 Δf 1 Hz但两个真实频率分别位于 1000 Hz 和 1001 Hz 时DPO 已经显示为一个宽峰。并不是 DFT 公式算错了而是窗函数主瓣把两个峰“粘”在了一起。因此工程上要区分“谱线间隔”和“可分辨频率间隔”。谱线间隔由数据时长决定可分辨频率间隔还要加上窗函数主瓣宽度这一项。我的建议是方案设计阶段就把目标分辨率先除以窗函数的有效主瓣系数。汉宁窗按 2 算布莱克曼-哈里斯窗按 4 算。也就是说如果你希望用汉宁窗分辨 1 Hz采样时长至少按 2 Hz 备用期待用 2 到 3 倍的时长来换余量。这样在实际调试时不至于因为窗函数展宽而被迫返工。4.3 平均、重叠与实时实现现场做频谱测量不管窗函数选得再好总存在随机噪声。这时候光靠一次 FFT 远远不够通常要做频谱平均。平均方式分为线性平均和峰值保持。线性平均能压低随机噪声适合稳定信号的幅值估计峰值保持能捕捉瞬态最大包络适合故障监测这类场景。在实时系统中滑动窗口更常见。窗口每次滑动一个很小步长然后重新做 FFT。为了减少相邻帧之间幅值跳变的“抖晃感”一般会让窗口重叠 50% 到 75%。重叠比例越高时间上的观察越细腻但计算量也随之上升。我见过不少实现直接放弃重叠每 N 个点才做一次 FFT结果实时性还行但频谱图看起来像“阶梯状”跳变非常生硬。叠加 50% 重叠后视觉和数值都稳定很多。代价只是计算量提升一倍现代处理器基本都能扛住。4.4 一个可以套用的简易流程实际项目里我经常用下面这几步来搭建一个频谱测量模块确认信号带宽确定采样率和抗混叠滤波器指标。根据最小可分辨频率间隔计算最小采样时长和点数 N。选择窗函数通用分析用汉宁精确幅值用平顶弱信号检测用布莱克曼-哈里斯。确定 FFT 长度优先填到 2 的幂小于采集点数时说明需要补零大于采集点数时则丢弃尾部或分段处理。校准幅值除以 N、乘单边谱系数 2、再除以窗函数相干增益。选择平均方式稳定信号用线性平均对抗噪声监测异常用峰值保持。这套流程看起来简单但每一步都可能出错。尤其是第 4 步FFT 长度和采样点数不一致时很多人会把频率轴也算错。记住频率轴刻度按 FFT 长度和采样率计算而不是按采样点数计算。5. 现场排查与实操记录几个让工程师头疼的问题5.1 明明信号是 50 Hz谱峰却出现在 50.3 Hz这个现象非常常见。原因多半是频谱分辨率不够真实峰值落在了两个谱线之间FFT 结果里最高谱线自然偏移到距离最近的整数谱线上。假设 Δf 1 Hz50 Hz 信号出现在 50.3 Hz那你在 50 Hz 和 51 Hz 两个谱线上都能看到能量只是 50 Hz 这一根略高结果读出来就变成 50 Hz。处理办法是补零加插值。先用零填充把频谱曲线拉平滑再在峰值附近取三根谱线做抛物线插值就能把峰值位置估算到小数位。注意这个估算值仍然不是物理分辨率提升只是对真实峰位的最佳估计。对 50.3 Hz 这种“不准”的场合插值足够用。还有一种情况是采样时钟本身有偏差比如晶振不准也会导致谱峰整体偏移。这时候要把参考信号加进来做修正或者使用锁相环同步采样时钟。5.2 明明有低频分量加窗后却“消失”了加窗会削弱两端的信号。如果低频分量在窗口内只出现了很少几个周期比如 10 Hz 信号只采集了 0.2 秒等于只有 2 个周期加汉宁窗后两端被压到接近零剩下的有效能量非常有限频谱里自然看不出来。这种时候不要死磕窗函数要先判断“数据长度是否足够包含足够多个周期”。要测量 10 Hz 信号至少留 0.5 到 1 秒数据如果硬件条件不允许就放弃用 DFT 测这个低频分量改用锁相放大或包络分析等方法。低频测量的本质是长时间观察任何窗函数都无法改变这个物理约束。5.3 幅度公式全部正确结果还是偏小公式对了代码也按顺序写了幅度还偏小我会优先怀疑三件事。第一波形是否包含直流偏置。如果信号叠加了直流会影响动态范围和整个频谱的底噪但通常不影响交流幅值除非信号已经削顶。第二ADC 是否真正满量程。很多采集系统标称 16 位实际有效位数只有 12 位。输入信号没有足够大时量化噪声会拉高底噪同时让幅值估计偏小。加大到接近满量程再测结果通常会回归正常。第三是否是有效值还是峰值的问题。FFT 校准算出来的是正弦波的峰值。用万用表或示波器测量时读到的往往是有效值二者相差 1.414 倍。如果拿有效值直接跟频谱幅值比较当然对不上。这个问题不算算法错误但却是现场最常见的一个“乌龙”。5.4 用滑动 DFT 做实时频谱需要注意的问题滑动 DFT 比整块 FFT 复杂但它特别适合实时频谱显示尤其是示波器式的频谱瀑布图。需要注意的主要有三点窗函数必须逐样本乘进数据不能在滑动后只对新增样本乘窗频谱刷新率必须和窗口重叠率匹配否则画面会跳变相位读数要补偿窗口滑动距离否则相位永远不稳定。实际调试时我把窗口步长设为 FFT 长度的四分之一也就是 75% 重叠这样时间分辨率和计算量的平衡比较合适。打开瀑布图后能明显看到时域瞬态对应频率曲线变化的时间轴一致性。这个经验适合大多数实时频谱分析场景但如果你的处理器很紧张可以降到 50% 重叠牺牲一点平滑度换性能。我自己的习惯是每改一次窗函数或采样率就用一个标准信号源做一次全流程校准确认幅值、频率、相位三个指标都对上了再继续下一步。这是最高效的排错手段远远好过靠肉眼盯着波形猜问题。