☰
MATLAB FFT频谱分析完全指南:从归一化到窗函数的实战避坑
2026/10/2 3:26:20 网站建设 项目流程

做信号处理的十有八九都用MATLAB做过频谱分析,但"用过fft"和"会做频谱分析"之间隔着一条不小的沟。我见过太多人把fft当成黑盒子:数据一进来就Y = fft(x),然后直接plot(abs(Y)),出来的图说不清是什么,振幅大得离谱,横轴还是点序号。这篇内容就是把广义的傅里叶变换到频谱分析这一段路完整拆开讲一遍:fft返回的数据到底长什么样、为什么要除N、频率轴怎么构造、单边谱和双边谱什么关系、窗函数什么时候用,以及实测数据里最常见的几个坑。我会直接用实际跑过的代码和踩过的坑来讲,适合刚接触信号处理仿真、想搞懂FFT原理的学生,也适合拿MATLAB处理传感器实测数据的工程师对照排查。

1. 为什么"直接plot(abs(fft(x)))"的频谱总不对劲

1.1 fft的输出不是"现成的频谱",而是一堆需要解释的复数

MATLAB的fft本质是DFT的快速实现。给定N点序列x(n),它算出来的是N个复数Y(k),每个Y(k)对应一个数字频率2πk/N。严格写出来就是:

Y(k) = sum_{n=0}^{N-1} x(n) * exp(-j * 2π * k * n / N)

问题在于,fft函数本身不做任何归一化。它只是机械地完成累加求和,至于这个结果怎么换算成和时域波形一致的幅值,是使用者自己的责任。这就导致如果你直接plot(abs(Y)),最直观的翻车现象出现了:一个幅值为1的正弦信号,频谱峰值根本不是1,而是N/2。N=1000时峰值就是500。

很多同学在这时候会去翻各种帖子,看到有人说"要除N",有人又说"要乘2/N",到底听谁的?其实取决于你想看单边谱还是双边谱。如果你做的是实信号频谱分析,标准流程是:先abs(Y/N)得到归一化幅度,再取单边(只保留正频率部分),然后把除了直流和奈奎斯特频点之外的所有点幅值乘2。为什么是乘2不是乘别的?因为实信号FFT的结果具有共轭对称性,正频率和负频率各占一半能量,你只取正频率那一半,自然要把能量加回来。不理解这个对称性,后面画图就会一直糊涂。

1.2 三种"翻车"现象与各自的根源

第一个典型:幅度对不上。输入x = sin(2*pi*50*t),幅值1,但频谱峰值是N/2。解决就是Y = fft(x); P = abs(Y/N)。这一步是新手最容易漏的,我当年第一次跑出来看到500这个数字还以为自己信号写错了。

第二个典型:出现两条一模一样的"山"。原因是实信号的频谱关于N/2共轭对称,正频率部分和负频率部分各占一份。很多新手看到两条一样的谱线以为存在两个频率,实际上只有一个。处理办法是取单边谱,或者用fftshift把零频移到中间画双边谱。

第三个典型:横轴是点序号0到N-1,不是Hz。这也不是改个xlabel就能蒙混过去的,因为频率轴必须结合采样率重新构造:第k个点对应的实际频率是k*Fs/N。如果采样率是1000Hz、N=1000,那第50个点才是50Hz;但如果你采样率是2000Hz,同样的第50个点对应的就是100Hz。频率轴错位是频谱分析里最隐蔽的错误来源,因为它不影响图的形状,只影响坐标的读数,形状看起来挺正常,读数完全不对。

1.3 一个必须建立的观念:fft只能做数学变换,怎么解释它取决于你

我把话放这里:MATLAB的fft几乎不会出错,会出错的都是我们怎么定义和理解它的输出。它不知道你的采样率是多少、不知道你想看单边还是双边、不知道你有没有加窗、要不要做幅度归一化。这些都归你管。

这也是为什么同样一套代码,懂的人能画出一张漂亮的频谱图,不懂的人只能得到一条莫名其妙的曲线。fft输出的是"DFT系数",而频谱图是你在这些系数之上做的"解释"。你先建立起这个观念,再去碰代码,后面每一步操作你都能知道自己在干什么,而不是在试参数。

2. 搭一条标准的频谱分析流水线:采样率、频率轴与单边谱

2.1 采样率、点数与频率分辨率的底层关系

频谱分析的第一步,先把三个参数的关系刻在脑子里:

df = Fs / N

这里的N指的是参与变换的数据长度,不是你在fft(x, N)里写的那第二个参数。为什么?因为DFT每个频点(bin)之间的间隔是Fs/N,N越大,相邻频点间距越小,越能区分靠得很近的频率成分。而这个式子换个写法就是df = 1/T,T是数据的总时长。也就是说,频率分辨率由观测时长决定,不是由FFT点数决定。

举个例子:Fs=1000Hz,采集1秒得到N=1000点,df=1Hz。这时候频谱上50Hz和50.5Hz会被分到同一个bin里,你根本分不开。想达到0.5Hz的分辨率,需要N=2000,也就是采集2秒。这是物理规律,不是软件能突破的。

这里必须强调一个浓度极高的误区:很多人觉得fft(x, 65536)比fft(x)更"精细",因为点数多了几十倍。确实,补零会让曲线看起来更平滑,但这只是插值,并不会把两个靠得很近的真实频率分量"顶开"。信息量没有增加,只是把同一个频谱画得更圆润了。我实测过,1秒数据里50Hz和53Hz两个分量,直接FFT和补零到65536点,两个峰都还是糊在一起,没有任何改善。提高频率分辨率只能靠采集更长时间的数据,补零救不了你。

2.2 频率轴怎么构造:从索引到Hz

MATLAB里fft的输出Y(k),k=0对应直流,k=1对应频率Fs/N,k=n对应n*Fs/N。所以频率轴的构造方式是:

f = (0:N-1) * Fs / N; % 完整频率轴,从0到接近Fs

如果只想看单边谱,取前一半即可:

f = (0:N/2) * Fs / N; % 从0到Fs/2(奈奎斯特频率)

为什么是N/2+1个点?因为0频(直流)和Fs/2(奈奎斯特频率)各占一个特殊位置,中间部分才是成对的频点。取单边的时候,幅度也要做相应修正:取abs(Y/N)后,除了直流分量和奈奎斯特分量不乘2,其余所有频点幅度乘2。

Fs = 1000; t = (0:999) / Fs; x = sin(2*pi*50*t) + 0.5*sin(2*pi*120*t); N = length(x); Y = fft(x); P2 = abs(Y / N); P1 = P2(1:N/2+1); P1(2:end-1) = 2 * P1(2:end-1); f = (0:N/2) * Fs / N; plot(f, P1); grid on;

这段代码跑完,你会看到50Hz处峰值正好是1,120Hz处峰值正好是0.5,横轴精确到Hz。这就是一条标准的频谱分析流水线。我建议你把这段存成自己的模板,每次做分析前先跑一遍,确认频率轴正确再换自己的数据。

2.3 双边谱与fftshift的适用场景

如果你分析的是复信号(比如基带IQ数据)、调制信号或者需要观察负频率的场合,那就得画双边谱。这时候用fftshift把零频移到横轴正中央:

f_shift = (-N/2:N/2-1) * Fs / N; Y_shift = fftshift(Y); plot(f_shift, abs(Y_shift / N));

fftshift的原理是把Y数组的前后两半交换,让直流分量落在中间位置。负频率在左边,正频率在右边。这个操作在处理OFDM、射频信号分析时几乎是标配,但在普通实信号分析里容易让人头晕,所以我一般建议:**先明确你处理的是实信号还是复信号,再决定画单边还是双边。**实信号默认单边,复信号才需要双边。

3. 窗函数:频谱泄漏的根源与对症下药

3.1 泄漏是怎么发生的:非整周期截断

前面说df = Fs / N,DFT默认你的数据是一个周期信号在时间轴上无限重复。如果信号频率恰好是df的整数倍(比如df=1Hz,信号正好50Hz),截断片段刚好构成整数个完整周期,FFT结果就完美集中在50Hz这一个bin,其他位置几乎为零。

但实际采集的信号很少这么听话。如果信号是50.3Hz,在1秒的窗口里它不是一个整数周期,DFT强行按周期延拓时,边界处就会产生跳变。表现出来的后果是:能量从真实的50.3Hz这个位置"漏"到邻近所有频率点,形成一条逐渐衰减的裙边。这就是频谱泄漏。

用个生活化的比喻:你拍照,取景框正好卡在拍摄对象中间,照片干净利落;取景框稍微偏一点,主体的边缘就会糊掉,还带进来旁边乱七八糟的东西。窗函数的作用,就是弱化取景框边界的存在感。

3.2 常用窗函数与选择场景

矩形窗(rectwin)就是不处理,泄漏最严重。汉宁窗(hann)是通用首选,主瓣略宽,但旁瓣衰减很漂亮。汉明窗(hamming)与汉宁很接近,最近旁瓣更小,但稍远旁瓣会有回升。布莱克曼窗(blackman)旁瓣抑制最强,代价是主瓣最宽。平顶窗(flattopwin)幅度测量误差极小,专门用于精确校准幅值的场合。

窗函数主瓣宽度旁瓣衰减最适用场景
rectwin2df-13 dB整周期截断,或分辨率优先
hann4df-31 dB通用首选,分辨与泄漏折中
hamming4df-43 dB最近旁瓣最小,但远旁瓣回升
blackman6df-58 dB要求极端抑制旁瓣
flattopwin约5.8df较低精确幅值测量、校准

MATLAB里加窗就是一乘:

w = hann(N, 'periodic'); xw = x .* w; Y = fft(xw);

注意:hann默认输出列向量,如果x是行向量,得转置一下。另一个细节是'periodic'和symmetric两种模式,做频谱分析基本都用'periodic',它让窗函数首尾不重复,更符合DFT的周期延拓假设。

3.3 加窗后的幅度修正不能忘

窗函数本质是对信号做幅度加权,这会降低信号总能量,导致频谱幅度偏小。我踩过一次坑:给信号加汉宁窗后,幅值1的正弦峰值变成了0.5,当时还以为哪个步骤写错了,排查了半天才发现是窗函数增益没补偿。

修正常见做法是除以窗函数的平均增益:

w = hann(N, 'periodic'); xw = x .* w; Y = fft(xw) / sum(w) * 2; % 单边谱,除直流外乘2,且已补偿窗增益

记住这个顺序:先加窗,再FFT,再除以窗的和(或mean(w)),再取单边乘2。如果你用periodogram或者pwelch,MATLAB会自动处理窗增益,不用手动操心,但自己手写FFT时必须记得。

加窗的代价是主瓣变宽。也就是说,两个频率成分的距离小于主瓣宽度时,它们依然可能糊在一起。这时候光靠加窗解决不了,必须回到采集端增加观测时长。窗是"抑制泄漏的代价",代价就是分辨率下降。没有免费的午餐。

4. 三个实战案例:从叠加正弦到实测信号

4.1 案例一:两个频率只差3 Hz,怎么用观测时长把它们分开

假设信号是50Hz幅值1、53Hz幅值0.5,采样率Fs=1000。

先采集1秒(N=1000),df=1Hz,频率分辨率虽然数值上小于3Hz,但加上窗函数的主瓣展宽和泄漏,两个峰在频谱上几乎融合成一个宽宽的包络,分不出是两个频率。这让人很头疼,因为理论上"1Hz分辨率"应该能分3Hz间隔,实际上就是糊的。

改成采集2秒(N=2000),df=0.5Hz,同样的信号,两个峰清晰分离。这个案例给了两条硬经验:

  1. 要分辨间隔为Δf的两个频率成分,观测时长必须满足T > 1/Δf,工程上往往要留出2倍以上余量。
  2. 补零没有用。我把同一个1秒数据用fft(x, 4096)试过,曲线变光滑了,但两个峰还是糊着的。补零改善的是插值密度,不是物理分辨率。
Fs = 1000; t1 = (0:999) / Fs; x1 = sin(2*pi*50*t1) + 0.5*sin(2*pi*53*t1); % 1秒:分离困难,包络糊在一起 t2 = (0:1999) / Fs; x2 = sin(2*pi*50*t2) + 0.5*sin(2*pi*53*t2); % 2秒:df=0.5Hz,两个峰清晰可辨

这条经验在振动监测、雷达测距、音频音高分析里极其常用。你一旦发现频谱上两个峰值黏在一起,第一反应应该是"数据采够时间了吗",而不是去调什么窗函数和算法。

4.2 案例二:被噪声淹没的微弱信号,用幅度谱还是功率谱密度

实际信号没有不加噪的。假设目标是一个幅值0.05的50Hz正弦,叠加大方差高斯白噪声。直接画归一化幅度谱,那个0.05的小峰很可能已经被噪声底的随机起伏盖住了,肉眼根本找不到。

这时候换韦尔奇法(pwelch)做功率谱密度估计,效果会好很多。pwelch把数据分段、加窗、做FFT、多段平均,能有效压低噪声方差,让埋在噪声里的线谱露出来。MATLAB一句话:

[pxx, f] = pwelch(x, hann(256), 128, 1024, Fs); plot(f, 10*log10(pxx));

这段代码里,256是窗长,128是重叠点数(50%重叠),1024是FFT点数(可以大于窗长做插值显示),Fs是采样率。纵轴是dB/Hz,单位是功率谱密度。多段平均之后,噪声的随机波动被压平,周期性小信号反而更突出。

我的经验是:**定量测幅值用幅度谱,探测微弱周期分量用PSD。**幅度谱的单位和物理量对应,但噪声大时不好看;PSD虽然不能直接读出幅值,但在检测"有没有这个频率成分"这件事上效果碾压幅度谱。做pwelch时还有个参数细节:窗长越短,分段越多,方差越小,但分辨率越差;重叠越多,数据利用越充分。工程上常用50%重叠,窗长取数据长度的一半左右,再根据实际效果微调。

4.3 案例三:实测数据里的直流偏置和脉冲毛刺

实测数据比仿真脏得多,最常见的两个干扰是直流偏置和脉冲毛刺。

直流偏置:传感器输出往往带着一个固定静态偏置,比如加速度计静止时输出1.5V而不是0V。这个直流分量在FFT的0Hz处形成巨大尖峰,而且由于直流分量相当于一个极端的非周期分量,它的泄漏会横跨全频段,把低频信号整个抬高。处理方案很朴素:

x = x - mean(x);

先去均值再做频谱分析。如果不去,低频段那条"抬高的基线"会误导你做出一堆错误判断。我之前处理IMU静止数据时,FFT后低频总有一条很高的基底,查了半天发现是温漂趋势项,不是任何真实频率成分。去均值只能去常数偏置,温漂这类线性趋势要用polyfit拟合去趋势,或者高通滤波。

脉冲毛刺:数据采集偶尔蹦出一个尖峰(电源干扰、接触抖动等),在频谱里表现为全频段抬高,因为脉冲的频谱本来就是宽频的。处理办法是在时域用中值滤波剔除:

x_clean = medfilt1(x, 5);

中值滤波对孤立毛刺非常有效,但它对真实信号也有轻微平滑,所以只对明显毛刺做,不要全数据无差别过一遍。做完预处理再看频谱,很多"莫名其妙噪底抬高"都能解释清楚。我强烈建议频谱分析前先画一张时域波形全览图,肉眼扫一遍有没有趋势项、毛刺、削顶。这步花30秒,能省后面排查好几个小时。

5. 频谱分析排错清单:频率轴错位、直流泄漏与相位虚影

5.1 频率轴错位与混叠

症状:信号明明是50Hz,频谱峰值却出现在400Hz(假设Fs=1000),或者以Fs/2为轴对称折叠。

这类问题根源通常有两个。一是采样率记错:代码里实际采样率是500Hz,却用1000去构造频率轴,于是所有频率标都翻倍。二是真正的混叠:信号里有频率超过Fs/2的成分,采样后这些成分折叠到0~Fs/2区间,在原本不存在的位置出现"幽灵峰"。

区分方法很简单:改变采样率(或先用一个已知频率的正弦信号做校准)。如果峰值位置随Fs正确变化,说明是标定错误;如果峰值始终折在某条线上,说明是混叠。混叠必须从采集端解决:提高采样率,或者加抗混叠滤波器。事后靠软件是补不回来的,因为高频信息在采样那一刻已经永久丢失了。

5.2 直流泄漏与"大尖峰"

0Hz处巨大尖峰且旁边低频被抬高的现象,十有八九是没去均值。这个现象在加速度计、陀螺仪、温度传感这类带静态偏置的信号里特别常见。

排查顺序:

  1. 画时域图,看均值是否偏离0;
  2. x = x - mean(x)后再做频谱;
  3. 如果尖峰仍在,检查是否有趋势项,用多项式拟合或高通滤波去除。

我处理过一个真实案例:一段看似平稳的振动信号,FFT后低频段总有一条缓慢升高的基底,怎么调窗都没用。最后发现是传感器温漂造成的线性趋势项,去掉之后基线才恢复正常。先看时域再上频域,这条规矩能帮你避开大量假象。

5.3 相位谱为什么经常"乱跳"

很多人画相位谱时看到一团乱麻,就以为代码错了。其实相位谱只有在信号幅度足够大的频率点才有意义。如果某频率处幅度接近零,相位完全由噪声主导,自然乱跳。这也是为什么我一般不看全频段的相位谱,只看幅度谱峰值那几根线的相位。

读取相位时要用atan2而不是atan:

phase = atan2(imag(Y), real(Y));

atan2能正确处理四个象限,不会出现正负号反了的问题。而且phase默认范围是[-π, π],连续变化的相位会被"包裹"成锯齿状,想看真实相位变化要用unwrap:

phase_unwrapped = unwrap(phase);

相位谱的实际应用主要是滤波器相频特性测量、两路信号的相位差分析、时延估计这些。做相位分析前,我建议先做一个基准测试:用两个已知时延的信号测算相位差,验证自己的读取逻辑是否正常,再上真实数据。没有基准,测出来的相位差是0.3弧度还是0.8弧度,你根本不知道哪个对。

5.4 频谱分析自查表

现象最可能原因优先排查动作
峰值幅度对不上没除NY=fft(x); abs(Y/N)
左右对称两座山实信号双边谱取单边或fftshift
横轴不是Hz没构造频率轴f=(0:N/2)*Fs/N
两个峰糊在一起观测时长不足采集更长时间,而非补零
0Hz巨大尖峰直流偏置x=x-mean(x)
频谱整体抬高脉冲毛刺或趋势项时域查看、中值滤波、去趋势
相位乱跳在幅度接近0处读相位只在显著峰值处读相位并unwrap

我在实际做信号处理项目时最大的体会是:频谱分析90%的问题都出在使用fft之前和之后的那几步——采样时长够不够、要不要去均值、要不要加窗、幅度怎么归一化。fft本身几乎不会坏,真正会出错的都是我们怎么定义和理解它的输出。如果你今天只打算记住一件事,那就是先想清楚"我要回答的问题是什么",再决定画幅度谱、功率谱还是相位谱。不要一上来就跑图,先检查数据采集时长和采样率,再验证频率轴,最后读结果。这套流程我反反复复用了几十次,每次都能在几分钟内定位问题所在。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询