基础3

信号与频谱:傅里叶级数、傅里叶变换与 FFT

学完这一章你应该能

  • 说清楚「频谱」是什么:一个信号可以看成许多正弦波的叠加;
  • 分清傅里叶级数、傅里叶变换、DFT/FFT 各自用在什么信号上,得到的频谱有什么不同;
  • 算出方波这类常见周期信号的谐波;
  • 知道傅里叶变换和拉氏变换、频率特性 \(G(j\omega)\) 的关系,理解「滤波就是频谱相乘」;
  • 正确读 FFT 的结果:频率分辨率、幅值换算、频谱泄漏、加窗;
  • 用 numpy 写一个靠得住的幅值谱函数,分析实测数据。

前置知识:复数与欧拉公式、正弦与指数、分贝、拉氏变换的定义。积分只用到最简单的 \(\int\sin\) 和 \(\int\cos\)。

为什么要学?示波器上看到的是信号随时间怎么变(时域)。很多问题在时域里看不清:电机电流里叠着 PWM 纹波和 50 Hz 干扰,机械结构在某个频率上共振,传感器噪声铺满了所有频率……换个角度,把信号拆成不同频率的正弦波,看每个频率上有多大(频域),这些东西就一目了然。FFT 就是计算机做这件事的工具。这一章把它背后的道理讲清楚:先讲周期信号(傅里叶级数),再推广到任意信号(傅里叶变换),最后落到计算机能算的有限个采样点(DFT 和 FFT)。

1. 时域与频域

先看一个信号:

$$x(t)=0.2+1.0\sin(2\pi\cdot50t)+0.3\sin(2\pi\cdot120t)$$

画成波形是一条弯弯曲曲的线,很难一眼看出它由什么组成;换成频谱,只有三根竖线:0 Hz(直流)处 0.2,50 Hz 处 1.0,120 Hz 处 0.3。

时域:波形 x(t) t:0~0.1 s 频域:幅值谱 0.21.00.3 050120 f / Hz

频谱:信号里每个频率的正弦分量有多大(幅值谱),以及它的初相位(相位谱)。工程上最常看的是幅值谱。

时域和频域描述的是同一个信号,可以互相转换,信息一点不少。只是有些特征在时域里明显(什么时候出事、上升多快),有些在频域里明显(含哪些频率、噪声在哪个频段)。

为什么偏偏拆成正弦波?因为线性系统对正弦输入的稳态输出还是同频率的正弦,只是幅值和相位变了(第5章 5.1 节会证明)。把输入拆成一堆正弦,分别算出每个正弦经过系统后变成什么,再加起来,就是输出。这就是频域方法的出发点。

2. 傅里叶级数:周期信号 = 直流 + 基波 + 谐波

周期为 \(T\) 的信号(基波频率 \(f_0=1/T\),角频率 \(\omega_0=2\pi f_0\))可以写成

$$x(t)=a_0+\sum_{n=1}^{\infty}\big(a_n\cos n\omega_0t+b_n\sin n\omega_0t\big)$$

\(a_0\) 是直流分量(平均值),\(n=1\) 的项叫基波,\(n\ge2\) 的项叫 \(n\) 次谐波,频率是 \(nf_0\)。系数这样算:

$$a_0=\frac1T\int_0^Tx(t)\,dt,\qquad a_n=\frac2T\int_0^Tx(t)\cos n\omega_0t\,dt,\qquad b_n=\frac2T\int_0^Tx(t)\sin n\omega_0t\,dt$$

系数公式的直觉:拿某个频率的正弦去「称」信号。两个不同频率的谐波相乘,在一个周期内的平均值是 0;同频率的 \(\sin^2\) 平均值是 \(\frac12\)(这叫正交性)。所以把 \(x(t)\) 乘上 \(\sin n\omega_0t\) 再求平均,其他频率的成分全部变成 0,只剩下 \(\frac12b_n\),乘 2 就是 \(b_n\)。拉氏变换用 \(e^{-st}\) 去「称量」\(f(t)\)(基础2),是同一个思路。

同频率的 cos 和 sin 可以合成一个带初相位的正弦:\(a_n\cos n\omega_0t+b_n\sin n\omega_0t=A_n\sin(n\omega_0t+\varphi_n)\),幅值 \(A_n=\sqrt{a_n^2+b_n^2}\)。把 \(A_n\) 画在频率 \(nf_0\) 处,就是幅值谱:周期信号的频谱只在基波的整数倍上有值,是一根根离散的谱线。

例:方波

幅值 ±1 的方波:前半个周期为 +1,后半个周期为 −1。平均值为 0,所以 \(a_0=0\);它是奇函数(\(x(-t)=-x(t)\)),所以所有 \(a_n=0\)。只需要算 \(b_n\):

$$b_n=\frac2T\left[\int_0^{T/2}\sin n\omega_0t\,dt-\int_{T/2}^{T}\sin n\omega_0t\,dt\right]=\frac{2}{n\pi}\big(1-\cos n\pi\big)=\begin{cases}\dfrac{4}{n\pi}, & n\ \text{为奇数}\\[2mm] 0, & n\ \text{为偶数}\end{cases}$$
$$x(t)=\frac4\pi\left(\sin\omega_0t+\frac13\sin3\omega_0t+\frac15\sin5\omega_0t+\cdots\right)$$

基波幅值 \(4/\pi\approx1.273\),比方波本身的幅值 1 还大;3 次谐波 0.424,5 次 0.255,7 次 0.182。在下面的图里把谐波一项一项加上去:

从这个例子能看出的规律

  • 信号越陡,频谱伸得越宽。有跳变的信号(方波、锯齿波)谐波按 \(1/n\) 衰减,高次谐波很多;连续但有尖角的(三角波)按 \(1/n^2\) 衰减;越光滑衰减越快。PWM 的陡峭边沿会产生很高频率的干扰(EMI),就是这个原因。
  • 对称性决定哪些谐波为 0。后半个周期是前半个周期取负(半波对称)的信号,只有奇次谐波。
  • 吉布斯现象:在跳变附近,有限项的和总会冲出约 9%,加再多项也去不掉,只是越来越窄。

两个和工作有关的例子:

复指数形式(为 FFT 做准备)

由欧拉公式 \(\cos\theta=\frac12(e^{j\theta}+e^{-j\theta})\)、\(\sin\theta=\frac1{2j}(e^{j\theta}-e^{-j\theta})\),级数可以改写成更紧凑的形式:

$$x(t)=\sum_{n=-\infty}^{\infty}c_ne^{jn\omega_0t},\qquad c_n=\frac1T\int_0^Tx(t)\,e^{-jn\omega_0t}\,dt$$

这里出现了「负频率」\(n<0\)。它不是什么物理现象,只是记账方式:一个实正弦被拆成 \(+nf_0\) 和 \(-nf_0\) 两个复指数,各分走一半幅值。对实信号,\(c_{-n}\) 和 \(c_n\) 共轭,\(|c_n|=\frac12A_n\)(\(n\ne0\)),\(c_0=a_0\)。记住这个「一半」,后面 FFT 的幅值换算要用。

3. 傅里叶变换:不是周期的信号

一个脉冲、一次冲击、一段阶跃响应都不是周期信号。办法是:把它看成周期 \(T\to\infty\) 的周期信号。

以宽度 \(\tau\)、高度 1 的矩形脉冲串为例,周期为 \(T\)。按上一节的公式算出(\(\operatorname{sinc}x=\dfrac{\sin\pi x}{\pi x}\)):

$$c_n=\frac{\tau}{T}\operatorname{sinc}\frac{n\tau}{T}$$

谱线间隔 \(1/T\)。周期越长,谱线越密,每根也越矮。但如果把纵轴乘以 \(T\),\(Tc_n=\tau\operatorname{sinc}(f\tau)\big|_{f=n/T}\),所有谱线都落在同一条曲线上,和 \(T\) 无关。拖动周期试试:

\(T\to\infty\) 时只剩下一个脉冲,谱线连成那条曲线,求和变成积分,这就是傅里叶变换:

$$X(j\omega)=\int_{-\infty}^{\infty}x(t)\,e^{-j\omega t}\,dt,\qquad x(t)=\frac1{2\pi}\int_{-\infty}^{\infty}X(j\omega)\,e^{j\omega t}\,d\omega$$

用频率 \(f\) 写也一样:\(X(f)=\int x(t)e^{-j2\pi ft}dt\)。

例 宽 \(\tau\)、高 1、中心在 0 的单个矩形脉冲:

$$X(f)=\int_{-\tau/2}^{\tau/2}e^{-j2\pi ft}\,dt=\frac{\sin\pi f\tau}{\pi f}=\tau\operatorname{sinc}(f\tau)$$

第一个零点在 \(f=1/\tau\):脉冲越窄,频谱越宽。1 ms 的脉冲,能量主要在 1 kHz 以内;1 µs 的脉冲,频谱一直铺到 1 MHz。极限情况是冲激 \(\delta(t)\),它的傅里叶变换在所有频率上都等于 1。所以用小锤敲一下机械结构(近似冲激),就能一次激励出它所有的共振频率,这是测机械共振的常用办法。

单位不一样:级数的系数 \(c_n\) 和信号的单位相同(比如 V);\(X(f)\) 的单位是「信号 × 秒」(V·s,也就是 V/Hz),它是密度。连续谱上单独一个频率没有「幅值」可言,一段频带才有能量。噪声就是这种连续谱,所以要用功率谱密度描述(第5节)。

傅里叶级数傅里叶变换DFT / FFT
用在什么信号上周期信号(连续时间)不是周期的信号(连续时间)有限个采样点 \(x[0]\dots x[N-1]\)
频谱长什么样离散的谱线,只在 \(nf_0\) 处连续的曲线离散的频点,间隔 \(f_s/N\)
频谱的单位和信号相同(如 V)信号 × 秒(V/Hz),是密度是 \(N\) 个点的和,除以 \(N\) 才换算成幅值
怎么得到积分(手算、查表)积分(手算、查表)计算机直接算,FFT 是快速算法

口头上「傅里叶变换」常被用来泛指这几种。工作中说「做个傅里叶变换看看」,几乎都是指对采样数据做 FFT。

4. 和拉氏变换、频率特性的关系

拉氏变换 \(F(s)=\int_0^\infty f(t)e^{-st}dt\),\(s=\sigma+j\omega\)。对 \(t<0\) 时为 0 的信号,令 \(\sigma=0\),它就变成了傅里叶变换。拉氏变换多出来的 \(e^{-\sigma t}\) 是一个衰减因子,让阶跃、斜坡甚至发散的信号也能积分;它还能自然地带上初始条件。所以分析控制系统(关心过渡过程和稳定性)用拉氏变换,分析信号(关心含有哪些频率)用傅里叶变换。

信号经过系统:时域里输出是输入和脉冲响应的卷积(基础2 的卷积定理),频域里就是相乘:

$$Y(j\omega)=G(j\omega)\,X(j\omega)$$

\(G(j\omega)=G(s)\big|_{s=j\omega}\) 就是第5章的频率特性:输入里频率为 \(\omega\) 的分量,幅值被放大 \(|G(j\omega)|\) 倍,相位移动 \(\angle G(j\omega)\)。

滤波 = 把信号的频谱乘上滤波器的频率特性。想去掉哪个频率,就让 \(|G(j\omega)|\) 在那里很小;想保留哪个频率,就让它在那里接近 1。滤波器专题讲怎么设计 \(G(j\omega)\) 的形状。

例 100 Hz、±1 的方波,经过截止频率 100 Hz 的一阶低通 \(G(s)=\dfrac{1}{\tau s+1}\)(\(\tau=\frac{1}{2\pi\cdot100}\))。第 \(n\) 次谐波的频率是 \(n\times100\) Hz,被乘上 \(|G|=\dfrac{1}{\sqrt{1+n^2}}\),相位滞后 \(\arctan n\):

谐波输入幅值\(|G|\)输出幅值相位
基波 100 Hz1.2730.7070.900−45°
3 次 300 Hz0.4240.3160.134−71.6°
5 次 500 Hz0.2550.1960.050−78.7°

高次谐波被压得比基波厉害得多,输出的方波被「磨圆」了。用时域的微分方程算这个输出要费不少功夫,在频域里几行就算完了。

反过来,同时记录一个系统的输入和输出,分别做 FFT,\(Y/X\) 就是 \(G(j\omega)\) 的估计。这是实测频率特性的基础(第10章 10.5 节)。

5. 计算机里的傅里叶:DFT 与 FFT

计算机拿到的不是连续的 \(x(t)\),而是每隔 \(T_s=1/f_s\) 采一次、一共 \(N\) 个点的数据,记录长度 \(T=N/f_s\)。采样以后只能看到 \(f_s/2\)(奈奎斯特频率)以下的频率,更高的会混叠成假的低频(第8章 8.2 节),所以 A/D 之前要有抗混叠滤波。

对这 \(N\) 个点做离散傅里叶变换(DFT):

$$X[k]=\sum_{n=0}^{N-1}x[n]\,e^{-j2\pi kn/N},\qquad k=0,1,\dots,N-1$$

第 \(k\) 个频点对应的频率是 \(f_k=k\,f_s/N\)。FFT(快速傅里叶变换)是算 DFT 的快速算法,算出来的数和按定义算完全一样,只是快得多:按定义算要约 \(N^2\) 次乘法,FFT 约 \(N\log_2N\) 次;\(N=1024\) 时是一百万次和一万次的差别。

DFT 把这 \(N\) 个点当成一个周期,假设信号在记录之外无限重复。所以它算的其实是这个「重复信号」的傅里叶级数(把第2节的积分换成求和),由此得到两个最重要的结论:

  • 频率分辨率 \(\Delta f=\dfrac{1}{T}=\dfrac{f_s}{N}\):谱线间隔等于记录长度的倒数。想分得更细,只能采得更久。提高采样频率没用,\(N\) 和 \(f_s\) 一起变大,\(\Delta f\) 不变。
  • 频谱泄漏:如果记录里不是整数个周期,首尾接起来的地方有跳变。跳变的频谱很宽,一个频率的能量就「漏」到附近很多频点上。

怎么读 FFT 的结果

在实验里改一改频率和点数:

泄漏与加窗

把 50 Hz 改成 50.5 Hz,1 s 里就有 50.5 个周期。不加窗时,峰值只剩 0.640(实际是 1.0,低了 36%),而且能量漏得到处都是,离它 10 Hz 远的 60 Hz 处还有 0.031。旁边如果有一个 0.02 的小信号,就被淹没了。

解决办法是加窗:做 FFT 之前,把数据乘上一个两头为 0、中间为 1 的窗函数,让首尾平滑地接起来,没有跳变。最常用的是汉宁窗 \(w[n]=0.5-0.5\cos\dfrac{2\pi n}{N-1}\)。加窗后,50.5 Hz 的峰值是 0.849,60 Hz 处的泄漏降到 0.0004,小了约 80 倍。

代价有两个:

窗什么时候用频率不在频点上时,幅值最多偏低
不加窗(矩形窗)能保证记录里是整数个周期:同步采样,或者激励信号是自己产生的36%(0.640)
汉宁窗一般情况下的默认选择15%(0.849)
平顶窗要准确读幅值:校准、测谐波含量约 0.1%,但主峰更宽

分辨率:两个靠得很近的频率

两个频率要相差约 3 个 \(\Delta f\) 以上,才能可靠地看出两个峰。例:50 Hz(幅值 1.0)和 53 Hz(幅值 0.5),加汉宁窗:

补零不能提高分辨率。把 1000 个点后面补上 3000 个 0 再做 FFT,频点间隔变成 0.25 Hz,曲线画得更细了,但 50 Hz 和 50.5 Hz 还是只有一个峰(50 Hz 处 1.087)。补零只是在原来的频点之间插值,数据里的信息还是那 1 s 的。

噪声

白噪声的能量分摊在所有频点上,\(N\) 越大,每个频点分到的越少,噪声底按 \(1/\sqrt N\) 下降;正弦分量的幅值不随 \(N\) 变。所以想从噪声里找出一个小的振动,就采长一点。第6节有一个例子:记录长度变成 16 倍,噪声底降到约 1/4。

反过来,正因为噪声的 FFT 幅值随 \(N\) 变,要描述噪声本身有多大,不能用幅值谱,要用功率谱密度(PSD,单位 V²/Hz),它不随 \(N\) 变。scipy.signal.welch 把长数据切成很多段,每段做 FFT 后再平均,得到的 PSD 曲线平滑得多。

6. 用 numpy 做 FFT

代码在 numpy 2.5.4、scipy 1.18.1 上运行过,注释里写的是实际输出,按页面顺序运行。

import numpy as np

fs, N = 1000, 1000                        # 采样频率 1 kHz,采 1000 点,记录长度 1 s
t = np.arange(N) / fs
x = 0.2 + 1.0 * np.sin(2 * np.pi * 50 * t) + 0.3 * np.sin(2 * np.pi * 120 * t)


def amp_spectrum(x, fs, window="hann"):
    """单边幅值谱:返回频率和每个频率上正弦分量的幅值(单位和 x 相同)"""
    n = len(x)
    w = np.hanning(n) if window == "hann" else np.ones(n)
    X = np.fft.rfft(x * w)                # 只算 0 ~ fs/2 这一半
    A = 2 * np.abs(X) / w.sum()           # 除以 N(加窗时除以窗的和),再乘 2 把负频率的一半加回来
    A[0] /= 2                             # 直流没有负频率,不乘 2
    if n % 2 == 0:
        A[-1] /= 2                        # fs/2 处也不乘 2
    return np.fft.rfftfreq(n, 1 / fs), A


f, A = amp_spectrum(x, fs, window=None)
print(f[1] - f[0])                        # 1.0:频率分辨率 fs/N
for k in (0, 50, 120):
    print(f"{f[k]:.0f} Hz: {A[k]:.3f}")   # 0 Hz: 0.200 / 50 Hz: 1.000 / 120 Hz: 0.300

泄漏和加窗:

x2 = np.sin(2 * np.pi * 50.5 * t)         # 1 s 里有 50.5 个周期,不是整数
for win in (None, "hann"):
    f, A = amp_spectrum(x2, fs, window=win)
    k = A.argmax()
    print(f"{win}: 峰在 {f[k]:.0f} Hz,幅值 {A[k]:.3f};60 Hz 处 {A[60]:.4f}")
# None: 峰在 50 Hz,幅值 0.640;60 Hz 处 0.0307
# hann: 峰在 51 Hz,幅值 0.849;60 Hz 处 0.0004

要准确读幅值,换平顶窗:

from scipy.signal.windows import flattop

w = flattop(N)
A = 2 * np.abs(np.fft.rfft(x2 * w)) / w.sum()
print(f"{A.max():.3f}")                   # 0.999

噪声里找小振动:直流 3.0、噪声标准差 0.1,里面藏着一个幅值只有 0.02 的 180 Hz 振动:

rng = np.random.default_rng(0)
for n in (1000, 16000):
    tn = np.arange(n) / fs
    y = 3.0 + 0.02 * np.sin(2 * np.pi * 180 * tn) + 0.1 * rng.standard_normal(n)
    f, A = amp_spectrum(y - y.mean(), fs)  # 先去掉均值
    k = np.argmin(abs(f - 180))
    print(f"N = {n}: 180 Hz 处 {A[k]:.4f},噪声底(中位数){np.median(A):.4f}")
# N = 1000: 180 Hz 处 0.0140,噪声底(中位数)0.0063
# N = 16000: 180 Hz 处 0.0213,噪声底(中位数)0.0016

\(N=1000\) 时,振动只比噪声底高 2 倍,读数也被噪声带偏了(0.0140,实际是 0.02);\(N=16000\) 时高出 13 倍,读数也准了。

分析实测数据的步骤

  1. 确认采样频率:用时间戳算实际的 \(f_s\),它不准,所有频率都会跟着错;
  2. 定记录长度:要分开相差 \(\delta f\) 的两个频率,记录至少要 \(3/\delta f\) 长;
  3. 去均值(有缓慢漂移就用 scipy.signal.detrend 去趋势);
  4. 加汉宁窗,做 rfft,按上面的公式换算幅值;
  5. 看峰:读频率和幅值;要准确的幅值换平顶窗;要比较噪声大小用 PSD;
  6. 在单片机上做 FFT 时,常用的库(比如 ARM 的 CMSIS-DSP)一般要求 \(N\) 是 2 的幂;numpy 没有这个限制。

常见错误

  • 直接看 \(|X[k]|\),忘了除以 \(N/2\)(大了几百几千倍);或者加了窗还除以 \(N\)(小了一半);
  • 不去均值就加窗:直流会通过窗函数漏到最前面几个频点。比如 0.2 的直流,加汉宁窗后在 1 Hz 处冒出一个 0.200 的假峰;
  • 以为提高采样频率、或者补零就能提高分辨率(只有加长记录才行);
  • 不是整数个周期又不加窗,把泄漏出来的「裙边」当成真实的频率成分,峰值低了 36% 还以为测准了;
  • 采样前没有抗混叠滤波,高于 \(f_s/2\) 的成分混叠成假的低频峰(第8章);
  • 拿不同 \(N\) 下噪声的 FFT 幅值互相比较(噪声底随 \(N\) 变,要比较就用 PSD)。

本章小结

  • 频谱:信号里每个频率的正弦分量有多大。时域和频域是同一个信号的两种描述。
  • 傅里叶级数:周期信号 = 直流 + 基波 + 谐波,频谱是 \(nf_0\) 处的离散谱线;方波 \(b_n=\frac{4}{n\pi}\)(奇次)。信号越陡,频谱越宽。
  • 傅里叶变换:周期 \(T\to\infty\) 时谱线连成连续曲线,单位是密度;脉冲越窄,频谱越宽。
  • 拉氏变换取 \(s=j\omega\) 就是傅里叶变换;\(Y(j\omega)=G(j\omega)X(j\omega)\):滤波就是频谱相乘。
  • DFT 把 \(N\) 个点当成一个周期:分辨率 \(\Delta f=f_s/N=1/T\);不是整数个周期就会泄漏;FFT 只是 DFT 的快速算法。
  • 单边幅值 \(2|X[k]|/N\)(加窗时把 \(N\) 换成 \(\sum w\));默认去均值、加汉宁窗;分开两个频率要相差约 3 个 \(\Delta f\);补零不提高分辨率;噪声底按 \(1/\sqrt N\) 下降。

练习

1. 一个 ±5 V、1 kHz 的方波,3 次谐波的频率和幅值是多少?2 kHz 处有没有分量?

3 次谐波在 3 kHz,幅值 \(\dfrac{4\times5}{3\pi}\approx2.12\) V。方波半波对称,偶次谐波都是 0,所以 2 kHz、4 kHz 处没有分量。

2. 采样频率 10 kHz,做 2048 点 FFT。频率分辨率、能看到的最高频率、记录长度各是多少?

\(\Delta f=10000/2048\approx4.88\) Hz;最高 \(f_s/2=5\) kHz;记录长度 \(2048/10000=0.2048\) s。

3. 设备上有 49 Hz 和 50 Hz 两个振动,想用加汉宁窗的 FFT 把它们分开,至少要采多长的数据?

两个频率相差 1 Hz,至少要 3 个 \(\Delta f\),所以 \(\Delta f\le\frac13\) Hz,记录长度 \(T=1/\Delta f\ge3\) s。和采样频率无关。

4. 示波器量到一个 1 V 的正弦振动,FFT 上 50 Hz 附近最高只有 0.64 V。可能是什么原因?怎么办?

信号频率不在频点上(记录里不是整数个周期),又没有加窗,最坏情况下峰值只剩 0.64 倍,能量漏到了旁边的频点。办法:加汉宁窗(最多偏低 15%);要准确读幅值就用平顶窗;或者调整记录长度,让它正好包含整数个周期。

5. 1000 点的数据,补零到 4000 点再做 FFT,原来分不开的两个峰能分开吗?

不能。补零只是在原来的频点之间插值,让曲线看起来更细;分辨率由数据的实际长度决定,1000 点(1 kHz 采样时 1 s)对应 1 Hz,分不开的还是分不开。要分开只能采更长的数据。

6. 傅里叶级数的系数和傅里叶变换的值,单位一样吗?为什么 FFT 的结果要除以 N?

不一样。级数系数和信号同单位(V),变换是密度(V·s,即 V/Hz)。DFT 是 \(N\) 个采样点的求和,约等于 \(N\) 倍的级数系数,除以 \(N\) 才是级数系数 \(c_k\);再乘 2 把负频率那一半加回来,才是正弦分量的幅值。

7. 宽 1 ms 的方脉冲和宽 10 µs 的方脉冲,哪个的频谱更宽?它们频谱的第一个零点分别在哪里?

10 µs 的更宽。第一个零点在 \(1/\tau\):1 ms 的在 1 kHz,10 µs 的在 100 kHz。这也是为什么开关越快(边沿越陡)的电路,高频干扰越严重。