频谱仪上那条基本平坦的底噪里,有时会冒出一个不到两格宽的小鼓包,占整个分析带宽连百分之一都不到,可就是它把链路误码率从 1e-6 拉到 1e-3;电机振动谱上那几根细如发丝的谱线,决定了轴承到底要不要拆;水下听音记录里那条频率只漂了几赫兹的线谱,就是判断目标有没有靠近的唯一线索。这些场景的主角都是同一个东西——窄带信号。说白一点,窄带信号就是"带宽相对于它的中心频率小到可以忽略"的一类信号,正是这个"小到可以忽略",让一大堆原本要解微分方程、要处理分数延迟、要做高阶卷积的问题,退化成了乘一个复指数、加一个相移、做一次低通滤波。它解决的从来不是"信号怎么发"的问题,而是"信号怎么被高效地表示、被低成本地采集、被高增益地检测出来"的问题。这篇内容适合三类人:刚接触数字信号处理、搞不清复包络为什么能替代实信号的同学;在做射频采集、频谱监测、振动诊断、水声检测,需要真正把窄带特性用起来的工程师;以及被"窄带干扰"折磨过、想知道陷波器参数到底怎么算的实践者。下面我按判据、表示、采集、检测、抑制、排查这条线,把能直接抄作业的东西全摊开。
1. 把"窄带"这两个字钉死:判据、直觉与常见误解
这个概念最容易被含糊带过。很多教材上来就是一句"窄带信号是指带宽远小于中心频率的信号",然后直接跳到复包络公式,中间的判据、边界、失效条件全省了。实际做工程,你必须知道"远小于"到底是多小、"小于"是相对谁说的,否则滤波器阶数算错、采样率选错、阵列模型用错,全都是从这里埋下的雷。
1.1 三种判据:相对带宽、绝对带宽、时间尺度
工程上真正在用的判据其实有三套,它们回答的是不同的问题,混用就会出事。
第一套是相对带宽判据,也是最常被引用的:定义分数带宽 $FBW = B / f_c$,其中 $B$ 是信号占据的带宽,$f_c$ 是中心频率。习惯上 $FBW \le 1%$ 叫窄带,$1% \sim 10%$(有的行业取到 25%)叫宽带,再往上就是超宽带。雷达和频谱监测领域基本沿用这套口径,因为超宽带的门槛常被定在 20%~25% 的相对带宽或 500 MHz 的绝对带宽。这套判据关心的是"信号在频域上占的格子有多窄"。
第二套是绝对带宽判据,取决于你所处的信道环境。比如在 2.4 GHz 附近,200 kHz 的信号毫无疑问是窄带;但在几十赫兹的超低频水声或地震波采集里,200 kHz 根本就不存在。这时候"窄带"是相对于"邻近有多少个同类的信号、信道间隔多大"来说的。一个 180 kHz 的蜂窝物联网制式,在整个频段规划里就是标准的窄带业务单元。
第三套是时间尺度判据,最容易被忽略但最好用:窄带意味着包络和相位的变化速度远慢于载波。换句话说,载波周期 $1/f_c$ 相比于包络变化的特征时间 $1/B$ 是个小量。这个判据直接决定了你能不能在一个载波周期内把幅度当成常数、能不能用前后两个采样点的相位差去近似瞬时频率——而这正是所有解调算法的地基。
我在实际项目里通常这么用:设计滤波器、选采样率时看第一套,做频谱共存分析时看第二套,写解调代码、判断"我这个差分近似还成不成立"时看第三套。三者不冲突,各管一段。
1.2 窄带假设到底省掉了什么:从数学到工程
窄带假设真正的价值,是它把一个"宽带问题"降维成"准静态问题"。我列几个最典型的收益点,你对照自己的场景就知道值不值。
收益一:复包络可以低通处理。实窄带信号可以写成 $x(t) = a(t)\cos(2\pi f_c t + \varphi(t))$,其中 $a(t)$、$\varphi(t)$ 的最高变化频率不超过 $B/2$。这意味着你把它搬到基带之后,所需处理带宽从 $f_c + B/2$ 一下子掉到 $B/2$,降幅是几个数量级。一个 70 MHz 中频、2 MHz 带宽的信号,采样率从 142 MSPS 直接降到 2.5 MSPS 就够,后端算力、存储、功耗全线下降。
收益二:阵列时延退化成相移。天线阵或水听器阵里,两个阵元之间的传播时延 $\tau$ 在窄带下可以用一个相移 $e^{-j2\pi f_c \tau}$ 近似,前提是 $B\tau \ll 1$。这一条是整个窄带波束形成理论的地基。反过来,一旦 $B\tau$ 接近 1,时延不能再用相移代替,必须上分数延迟滤波器,运算量翻好几倍。我曾经在一个 40% 相对带宽的阵列项目上直接套用相移模型,波束指向偏了两度多,查了三天才发现是模型选错了,不是校准没做好。
收益三:微分近似成立。瞬时频率定义为相位的导数,离散实现就是相邻点相位差除以采样间隔。窄带条件下相位变化缓慢,差分引入的近似误差小,噪声也不会被差分放得太大。宽带信号做同样操作,相位一步跨过好几个 2π,解卷绕都救不回来。
收益四:系统可视为准静态。放大器、混频器、滤波器的幅频特性在 $B$ 范围内变化很小,可以当常数处理;增益压缩、相位非线性这些指标只需要在中心频点标定。窄带系统的指标表通常比宽带系统短一半,原因就在这里。
1.3 容易踩的三个认知误区
注意:下面这几条,是我见过最频繁的误用,几乎每个新手都会至少中一条。
误区一:以为"窄带"是一个信号的固有属性。不是。同一个物理信号,在 100 MHz 载波上看是窄带,在零中频直接基带处理时就变成了宽带(相对带宽 100%)。窄带是"信号 + 观察视角"的联合属性,脱离参考频率谈窄带是没有意义的。
误区二:以为窄带就一定好处理。窄带带来的最大麻烦恰恰是分辨率要求。为了把间隔 50 Hz 的两根谱线分开,你需要的观测时间至少 20 ms;为了在 100 dB 动态范围下看到旁边那个弱 80 dB 的小信号,窗函数、位宽、相位噪声一个都不能将就。窄带检测本质上是拿时间换频率分辨率,这笔账后面第 4 章要细算。
误区三:以为窄带信号在时域上"看起来像正弦"。只有单频纯音才是正弦。一个带宽 200 Hz、中心 6 kHz 的窄带信号,时域上包络是随机起伏的,只是起伏得比载波慢。很多人拿示波器看,觉得"这不挺乱的吗,怎么就窄带了",然后怀疑自己信号源有问题。看频谱,别看波形。
2. 窄带信号的表示:实信号、解析信号与复包络
表示方法是整个窄带处理的核心技能。同一段数据,你可以在实信号域、解析信号域、复包络域三个层面上看它,每一层能做的事不一样。搞混这三个域,代码就会写出"用实信号的相位去做频率估计"这种必然出错的写法。
2.1 希尔伯特变换与解析信号
实信号 $x(t)$ 的频谱是共轭对称的,正负频率各一半,信息冗余一倍。解析信号$z(t) = x(t) + j\hat{x}(t)$ 把负频率那一半砍掉,只保留正频率分量,$\hat{x}(t)$ 就是 $x(t)$ 的希尔伯特变换。用起来最直观的好处是:$z(t)$ 的模就是瞬时包络,$z(t)$ 的辐角就是瞬时相位,两者都能直接取出来。
离散实现上,标准做法是 FFT 加单边谱再 IFFT,或者直接调用成熟库。但这里有两个必须知道的坑。
第一个坑是边界效应。希尔伯特变换本质是一个非因果的全通滤波器(在频域上乘以 $-j,\text{sgn}(f)$),用 FFT 实现等价于假设信号是周期延拓的。如果你的信号在两端幅度不为零,周期延拓处就会形成一个阶跃,产生沿着整段数据传播的振铃,包络在首尾各有一小段是错的。我的处理习惯是:两端各镜像延拓约 10% 的长度,变换完再裁掉,成本极低但效果立竿见影。
第二个坑是纯直流和极低频。希尔伯特变换在零频处的响应是不连续的,如果信号里混了直流偏置或者极低频漂移,解析信号的相位会被带偏,包络也会鼓起来。所以进希尔伯特之前先做一次高通或者去均值,是常规动作。
import numpy as np from scipy.signal import hilbert def analytic_signal(x, pad_ratio=0.1): n = x.size p = int(n * pad_ratio) # 镜像延拓,压低边界振铃 xp = np.concatenate([x[p:0:-1], x, x[-2:-p-2:-1]]) z = hilbert(xp) return z[p:p + n] fs = 48000.0 t = np.arange(int(fs * 0.5)) / fs fc, b = 6000.0, 200.0 rng = np.random.default_rng(7) # 造一个窄带信号:缓慢起伏的包络 + 缓慢漂移的相位 slow = np.cumsum(rng.normal(size=t.size)) slow = slow - slow.mean() slow = slow / np.max(np.abs(slow)) bb = (1 + 0.3 * slow) * np.exp(1j * 0.6 * np.sin(2 * np.pi * 3 * t)) x = np.real(bb * np.exp(1j * 2 * np.pi * fc * t)) z = analytic_signal(x) env = np.abs(z) phase = np.unwrap(np.angle(z))这段代码生成的就是一个典型的窄带信号:中心 6 kHz,包络带宽大约在几赫兹量级,相对带宽不到 5%。用np.abs(z)拿到包络,np.angle(z)拿到相位,后续所有分析都从这两个量出发。
2.2 复包络与载波搬移的关系
解析信号里还带着 $f_c$ 这个高速旋转的相位因子,真正有用的信息全在它的慢变部分里。把载波因子除掉,得到的就是复包络(也叫基带等效信号):
$$x(t) = \mathrm{Re}\left{ \tilde{x}(t), e^{j2\pi f_c t} \right}$$
$\tilde{x}(t)$ 就是复包络,它的带宽只有 $B/2$,采样率可以降到略高于 $B$。这一步在工程上叫数字下变频(DDC),流程是:乘一个 $e^{-j2\pi f_c n/f_s}$ 把中心频点搬到零频,然后低通滤掉搬移出来的高频镜像,最后抽取降采样。第 3 章会给完整实现。
这里有个细节值得单独说:复包络的相位参考系。$\tilde{x}(t)$ 的相位是相对于你选定的 $f_c$ 而言的。如果你的 $f_c$ 和信号真实载波差了 100 Hz,那么复包络的相位就会以 100 Hz 的速度匀速旋转,看起来像一个缓慢的线性相位斜坡。这在做相干积累的时候是致命的——积累时间一长,相位转了大半圈,累加直接抵消。所以做长时间相干处理前,必须先把残余频偏估出来补掉,这一步叫载波同步,粗估常用 FFT 找峰,精估常用相位差或锁相环。
我还想强调一点:复包络是复数,它的实部和虚部分别叫 I 路和 Q 路。很多人第一次见 IQ 数据会问"虚部是干什么用的,物理上不存在啊"。虚部不是物理量,它是数学工具,作用是让"相位"这个维度能被线性地表示出来。一旦你把它当成"另一路独立的实信号"去处理,就会写出很多莫名其妙的错误,比如对 I、Q 分别做滤波然后奇怪为什么相位失真了。
2.3 瞬时幅度、相位与频率的离散实现
窄带信号最有用的三个导出量是瞬时幅度、瞬时相位、瞬时频率,公式都很简单,但离散实现全是细节。
瞬时幅度就是 $|z[n]|$,没什么好说的。瞬时相位是 $\arg z[n]$,取值范围 $(-\pi, \pi]$,必须做解卷绕(np.unwrap),否则每次过 $\pm\pi$ 就跳变 $2\pi$,算出来的频率会出现巨大的尖刺。
瞬时频率是相位的数值微分:
$$f[n] = \frac{f_s}{2\pi}\left(\varphi[n] - \varphi[n-1]\right)$$
这个式子在窄带下成立的前提是相邻两点的相位差远小于 $\pi$,也就是 $f_c \ll f_s/2$。如果你的信号中心频率接近奈奎斯特频率,这个近似就崩了,必须先下变频到基带再用。
还有一个实际经验:相位差分对噪声极其敏感。差分相当于一个高通,噪声在高频段被放大,算出来的瞬时频率会毛刺满天飞。所以直接差分几乎没有实用价值,通常要先对解析信号做轻度平滑(比如滑动平均或者低阶低通),或者改用多点相位回归的方法估斜率。我在做振动包络分析时,习惯先对复包络做 3~5 点的滑动平均再差分,毛刺能压下去十几 dB,代价只是损失一点点时间分辨率。
def inst_freq(z, fs, smooth=5): # 先对复包络平滑,抑制差分噪声放大 k = np.ones(smooth) / smooth zs = np.convolve(z, k, mode='same') ph = np.unwrap(np.angle(zs)) f = np.diff(ph) * fs / (2 * np.pi) return f这段代码拿到的就是逐点的频率偏移量,配合包络一起画出来,窄带信号在时频上的行为一目了然。
3. 采集链路:带通采样怎么算,数字下变频怎么做
前面都是表示层面的事,这一章落到硬件。窄带信号最大的好处就是能用远低于奈奎斯特频率的采样率采下来,前提是你会算带通采样的允许区间。这一步算错,采下来的数据看起来"有信号",但频率位置全错,后面的分析全部作废。
3.1 带通采样的允许区间与时钟抖动预算
带通采样的约束条件是这样的:设信号占据 $[f_L, f_H]$,带宽 $B = f_H - f_L$,采样率 $f_s$ 必须落在
$$\frac{2f_H}{n} \le f_s \le \frac{2f_L}{n-1}, \quad n = 1, 2, \ldots, \left\lfloor \frac{f_H}{B} \right\rfloor$$
这些区间在频率轴上是一段一段的,$n$ 越大区间越窄。新手常犯的错是随手取一个满足条件的 $f_s$ 就用,结果发现时钟稍有偏差就混叠。真正稳妥的做法是把允许区间算出来,挑最宽的那一段,采样率取它的中点。
import numpy as np def bp_zones(f_low, f_high, fs_max, fs_min=1.0): bw = f_high - f_low zones = [] n_max = int(np.floor(f_high / bw)) for n in range(1, n_max + 1): lo = 2 * f_high / n hi = 2 * f_low / (n - 1) if n > 1 else fs_max lo, hi = min(lo, hi), max(lo, hi) lo, hi = max(lo, fs_min), min(hi, fs_max) if hi > lo: zones.append((n, lo, hi, (lo + hi) / 2)) return zones # 例:70 MHz 中频,2 MHz 带宽 for n, lo, hi, mid in bp_zones(69e6, 71e6, 200e6): print(f"n={n:2d} [{lo/1e6:8.3f}, {hi/1e6:8.3f}] MHz 中点 {mid/1e6:8.3f} MHz")跑出来的结果会很直观:$n=35$ 时允许区间是 [4.057, 4.059] MHz,宽度只有 2 kHz 左右,任何一点时钟偏差都会让你掉出窗口;而 $n=5$ 时区间是 [28.4, 34.5] MHz,宽度超过 6 MHz,随便挑都稳。所以我的原则是——优先选小的 $n$,除非后端的处理带宽确实撑不住。宁可多采一些数据,也不要让采样时钟的稳定性成为系统瓶颈。
注意:带通采样对时钟抖动的容忍度比基带采样差得多,因为采样时刻的误差是按射频中心频率换算成相位误差的。
抖动引起的信噪比上限有个简单公式:
$$\mathrm{SNR}{j} = -20\log{10}\left(2\pi f_{in}\sigma_t\right)$$
举例:$f_{in} = 70$ MHz,时钟抖动 $\sigma_t = 1$ ps,则 $2\pi \times 7\times10^{7} \times 10^{-12} \approx 4.4\times10^{-4}$,对应大约 $-67$ dB。这时候哪怕你用 16 位 ADC,实际有效位数也就 11 位左右。想做 80 dB 以上的动态范围,抖动必须压到 100 fs 量级,这就是为什么高端采集卡的时钟部分比 ADC 还贵。
采样之后,信号在数字域的等效频率是:
$$f_{dig} = \left| f_c - \mathrm{round}(f_c / f_s) \cdot f_s \right|$$
这个式子最好写到代码注释里,因为每一次换采样率,你都要靠它重新定位信号在数字谱上的位置,凭感觉估一定会错。
3.2 数字下变频与多级抽取
拿到带通采样数据后,第一件事是把它搬到零频,第二件事是把采样率降下来。这两个动作合称数字下变频加抽取,标准流程是:混频 → 低通 → CIC 抽取 → 补偿滤波 → 半带抽取 → 精细 FIR。
from scipy.signal import firwin, lfilter, resample_poly def ddc(x, fs, fc, bw, out_rate): n = np.arange(x.size) lo = x * np.exp(-1j * 2 * np.pi * fc / fs * n) # 低通:截止频率取带宽的一半再多留 20% 过渡带 cutoff = (bw / 2 * 1.2) / (fs / 2) taps = firwin(129, cutoff, window='hamming') bb = lfilter(taps, 1.0, lo) # 按有理数比例重采样,内部自带抗混叠 up = int(round(out_rate)) down = int(round(fs)) from math import gcd g = gcd(up, down) return resample_poly(bb, up // g, down // g)这段代码里最能体现经验的是cutoff那一行。很多人直接把截止频率设成 $B/2$,结果主瓣边缘被削掉,信号的带边畸变。我的习惯是多留 20% 的过渡带,牺牲一点点带宽换取平坦的通带响应。另外firwin的阶数不能省,低通过渡带越窄,阶数越高,这是绕不过去的账。
抽取过程有个铁律:每一级抽取前必须先滤波。CIC 滤波器的通带下垂很明显(在通带边缘能掉 3~4 dB),所以 CIC 后面通常要跟一个反 sinc 的补偿 FIR,否则你的窄带信号通带内会呈现一个倾斜的"溜肩",做幅度测量时直接偏掉。这个问题在多级抽取链路里特别隐蔽,因为每一级都"看起来对",但累积起来就是几个 dB 的误差。
3.3 位宽、增益与动态范围的账
窄带处理有一个特别爽的地方:抽取带来的处理增益。ADC 的量化噪声均匀分布在 $[0, f_s/2]$,当你只保留带宽 $B$ 时,噪声功率按比例下降了 $f_s/(2B)$ 倍,折算成分贝就是
$$G_p = 10\log_{10}\frac{f_s}{2B}$$
举个实在的例子:采样率 100 MSPS,信号带宽 200 kHz,则 $G_p = 10\log_{10}(250) \approx 24$ dB。一个 12 位 ADC 的标称信噪比约 74 dB,加上这 24 dB 的处理增益,等效信噪比可以到 98 dB 上下,相当于 16 位以上的水平。这就是为什么很多窄带接收机能用中低端 ADC 做出很高的动态范围——窄带本身就是一种增益。
但也别高兴太早,处理增益只能压量化噪声,压不了相位噪声和杂散。本振的相位噪声在窄带里会直接落到你的通带内,处理增益对它完全无效。做宽带采集的时候,相噪被分散到整个奈奎斯特区,看起来不高;一旦你做窄带抽取,原本在别处的相噪被搬进通带,底噪就抬起来了。我见过一个系统,换了个便宜的时钟源,宽带谱上看不出差别,做窄带抽取后信噪比掉了 15 dB,查了两天才定位到相噪。
4. 窄带信号的检测与分析:分辨率、窗函数与单频提取
到了这一章就是窄带信号真正的战场——在噪声里把一个宽度只有几十赫兹的信号找出来。这里所有的取舍都围绕一句话:频率分辨率和幅度精度是矛盾的,你得先想清楚自己到底要哪个。
4.1 频率分辨率、相干增益与噪底换算
FFT 的频率分辨率是
$$\Delta f = \frac{f_s}{N} = \frac{1}{T_{obs}}$$
其中 $T_{obs}$ 是观测时长。想分辨间隔 50 Hz 的两根谱线,观测时间至少要 20 ms,实际上留两倍余量更稳,取 40 ms。这个式子还告诉你一件事:采样率本身不影响分辨率,只有观测时长影响。很多人以为提高采样率就能看得更细,其实只是把同一段观测时间分得更碎而已。
噪底和分辨率的关系也要算清楚。噪声功率与带宽成正比,所以当分辨率带宽(RBW)从 $BW_1$ 变到 $BW_2$ 时,噪底变化量是
$$\Delta L = 10\log_{10}\frac{BW_1}{BW_2}$$
扫频式频谱仪上显示的"平均噪声电平"就是这个量,很多人在看 FFT 结果时忘记换算,把一个 RBW 下的噪底直接和另一个 RBW 下的噪底比较,得出"换了个分析方法噪声变大了 20 dB"这种荒谬结论。
相干积累的增益是 $10\log_{10}N$,也就是 1000 点 FFT 相对单点有 30 dB 增益。这个增益的来源是信号在频点上同相相加,而噪声随机相加只增长 $\sqrt{N}$。所以做微弱窄带信号检测,最有效的手段永远是拉长观测时间、做相干积累,而不是换更贵的 ADC。
举一个具体的账:一个 6 kHz 中心、200 Hz 带宽的信号,采样率 48 kSPS,观测 1 秒,$N = 48000$,$\Delta f = 1$ Hz。信号功率集中在约 200 个频率格子里,如果知道确切频率,可以只对这 200 个格子求和,相比整个人带宽的能量,噪声被压制了 $48000/200 = 240$ 倍,即 24 dB。这就是为什么窄带检测的门限能做到远低于宽带检测。
4.2 窗函数的实际取舍
不加窗就是矩形窗,等于对信号做了硬截断,频谱泄漏极其严重,第一旁瓣只有 $-13$ dB,旁边的弱信号会被淹掉。加窗就是用一个两端平滑衰减的包络去乘信号,代价是主瓣变宽。
| 窗类型 | 主瓣宽度(约) | 最高旁瓣 | 幅度测量误差 | 适用场景 |
|---|---|---|---|---|
| 矩形窗 | 2 bin | $-13$ dB | 大 | 信号整周期截断、要求极限分辨率 |
| 汉宁窗 | 4 bin | $-31$ dB | 中 | 通用频谱分析,默认选它 |
| 汉明窗 | 4 bin | $-43$ dB | 中 | 主瓣附近有弱信号需要看清 |
| 布莱克曼窗 | 6 bin | $-58$ dB | 较大 | 大动态范围、强干扰旁边的弱线 |
| 平顶窗 | 8~10 bin | $-70$ dB | 极小 | 做幅度标定、电平测量 |
这张表里最容易误用的是矩形窗。它的主瓣最窄,看起来分辨率最高,但旁瓣衰减只有 13 dB,意味着一个 -20 dBm 的强信号,会在旁边 5 个格子外留下 -33 dBm 的泄漏,把一个 -60 dBm 的弱窄带信号完全盖住。我见过太多人拿矩形窗去"提高分辨率",结果是在强干扰旁边什么也看不见。
选择逻辑其实很简单:先问自己是要测频率还是测幅度。测频率、要分辨相邻的两根线,用汉宁甚至矩形;测幅度、要做电平定标,用平顶窗;强干扰旁边找弱信号,用布莱克曼。选错了再怎么调参数都白搭。
4.3 Goertzel 与 ZOOM-FFT:单频检测的两把刀
如果你只关心几个固定频点,比如检测一个已知频率的导频或者标准信号,跑全长度 FFT 是极大的浪费。这时候有两个更省的工具。
第一个是 Goertzel 算法。它本质上是一个二阶 IIR 谐振器,对每个目标频点单独递推,运算量是 $O(N)$,比 FFT 的 $O(N\log N)$ 更省,而且不要求 $N$ 是 2 的幂。
$$s[n] = x[n] + 2\cos(\omega_0)s[n-1] - s[n-2]$$
$$\omega_0 = \frac{2\pi k}{N}, \quad |X(k)|^2 = s[N-1]^2 + s[N-2]^2 - 2\cos(\omega_0)s[N-1]s[N-2]$$
def goertzel(x, k, n_total): w = 2 * np.pi * k / n_total c = 2 * np.cos(w) s1 = s2 = 0.0 for v in x: s0 = v + c * s1 - s2 s2, s1 = s1, s0 return s1 * s1 + s2 * s2 - c * s1 * s2这个算法有个隐藏优势:它对 $N$ 没有限制。FFT 想要分辨率高,$N$ 必须大;Goertzel 可以每来一个新样本就更新一次判决,做成滑动版本,实时性极好。做窄带告警、频率监测的时候非常好用。
第二个是 ZOOM-FFT,也叫选带细化。思路是先把感兴趣的窄带搬移到零频、低通、抽取,再对降采样后的数据做短 FFT,等效于在原信号上获得了极高的分辨率。这和数字下变频是同一套逻辑,好处是运算量远低于对原始长数据做全谱 FFT。比如你要在 1 MHz 带宽里看一个 100 Hz 宽的细节,全谱 FFT 需要 2000 万点,ZOOM-FFT 先抽取 1000 倍再算 2 万点就够,运算量差两个数量级。
这两个工具我一般是这么分工的:频点已知、要实时盯梢,用 Goertzel;频点未知或者要看周边的谱形,用 ZOOM-FFT。
5. 窄带干扰抑制:陷波器设计与频域处理的边界
处理窄带信号时,最常见的实际需求反过来了——不是检测窄带信号,而是要干掉它。工频干扰、本振泄漏、邻近信道的强载波,全都是窄带干扰。抑制窄带干扰的核心矛盾是:既要挖得深,又不能把旁边有用的信号一起挖掉。
5.1 二阶 IIR 陷波器的参数计算
最经典的是二阶格型陷波器,传递函数:
$$H(z) = \frac{1 - 2\cos(\omega_0)z^{-1} + z^{-2}}{1 - 2r\cos(\omega_0)z^{-1} + r^2 z^{-2}}$$
分子是单位圆上的两个零点,正好在 $\omega_0$ 处,形成无限深的陷波;分母是一对位于半径 $r$ 处的极点,用来控制陷波宽度。$/r$ 越接近 1,陷波越窄、越尖锐。两者的关系和带宽的关系是:
$$BW \approx \frac{(1-r)f_s}{\pi} \quad \Rightarrow \quad r \approx 1 - \frac{\pi \cdot BW}{f_s}$$
举个实际算例:采样率 48 kHz,要滤掉 50 Hz 工频,希望陷波宽度 2 Hz(既能让 50 Hz 彻底下去,又不伤 48 Hz 和 52 Hz 附近的分量):
$$1 - r = \frac{\pi \times 2}{48000} \approx 1.309\times10^{-4} \Rightarrow r \approx 0.99987$$
import numpy as np from scipy.signal import filtfilt, freqz def notch(f0, bw, fs): r = 1 - np.pi * bw / fs w0 = 2 * np.pi * f0 / fs b = np.array([1.0, -2 * np.cos(w0), 1.0]) a = np.array([1.0, -2 * r * np.cos(w0), r * r]) # 归一化直流增益,避免整体电平漂移 g = (1 + r * r - 2 * r * np.cos(2 * w0)) / (2 - 2 * np.cos(2 * w0)) return b / g, a注意:$r$ 太接近 1 会带来两个问题。一是系数量化误差,用单精度浮点甚至定点实现时 $r$ 舍入后可能落到 1,滤波器直接失稳;二是极点靠近单位圆会让瞬态响应拖得很长,开机后几十毫秒内输出都是错的。我在 16 位定点平台上做这个滤波器,$r$ 一般不超过 0.999,需要更窄的陷波就提高采样率或者改用频域方法。
还有一个实操技巧:用零相位滤波。filtfilt做正反两遍,把相位响应完全抵消,陷波前后信号的相位关系保持不变,这对后续做相干分析特别重要。代价是等效阶数翻倍、边界效应也要额外处理。
5.2 FIR 带阻与频域置零的取舍
IIR 陷波器省资源,但有相位失真和非线性相位的问题。如果后端对相位敏感,就得用 FIR。
FIR 带阻的阶数估算可以用经验公式:
$$N \approx \frac{A_s - 7.95}{2.285 \cdot \Delta\omega}, \quad \Delta\omega = \frac{2\pi \Delta f}{f_s}$$
其中 $A_s$ 是阻带衰减(dB),$\Delta f$ 是过渡带宽度。代入一组真实参数:阻带 80 dB,过渡带 200 Hz,采样率 48 kHz,则 $\Delta\omega = 0.02618$,$N \approx 1204$。一千多阶,实时跑起来相当吃力。
这个数字很能说明问题:窄带滤波的代价是阶数暴涨。过渡带越窄,阶数越高,没有捷径。所以工程上的通用做法是先降采样再精细滤波——先把数据抽到刚好覆盖目标 band 的速率,此时相对过渡带变宽了,同样的绝对带宽只需要很少的阶数。我做过一个对比:直接在 48 kSPS 上做 2 Hz 过渡带的带阻要 1200 阶,先抽到 200 SPS 再做同样绝对宽度的带阻只要 5 阶,运算量差了 200 倍以上。
至于频域置零,也就是把 FFT 结果里干扰所在的几个 bin 直接清零再 IFFT,看起来最省事,但坑很多。直接对整段数据做 FFT 清零,等价于假设信号是周期的,边界处会产生严重的振铃,听起来像"回声"。正确做法是重叠相加:分段、加窗、FFT、置零、IFFT、重叠相加,段间重叠至少 50%。即便如此,被挖掉的那几个 bin 里原本的信号也一并没了,所以这个方法只适合"干扰频点固定且有用信号不占那个频点"的情况。
6. 典型问题排查与速查表
前面讲的都是"怎么设计",这一章讲"设计完了不work怎么办"。窄带系统的排查有个特点:现象往往很相似(信噪比不够、谱线位置不对、包络失真),但根因可能分布在天线、时钟、ADC、滤波器、算法五个层面上,必须有顺序地查。
6.1 频谱上的"鼓包"未必是信号
这是我最常提醒新人的一句话。FFT 上看到一个窄窄的凸起,第一反应应该是"它是不是真的",而不是"它是什么"。
排查顺序我一般这么走:先把采样率减半再采一次。真信号的位置会按新的采样率重新映射(用 3.1 节那个折叠公式算),而很多伪像——比如时钟馈通、电源纹波耦合、ADC 内部杂散——会跟着采样率一起变,或者干脆消失。第二步,把输入断开(接匹配负载),看这个鼓包还在不在。在,就是系统内部产生的;不在,才是外部信号。第三步,改变观测时长,看鼓包的宽度是否跟着变。真窄带信号的谱线宽度由它自身的相位噪声决定,不随你的 FFT 长度变化;而泄漏产生的"假鼓包"会随着窗函数和长度明显变形。
还有一种特别隐蔽的情况:鼓包是信号的谐波混叠。一个强信号的高次谐波被采样折叠到你关心的频段里,看起来像一个独立的窄带信号。判断方法是改变采样率,看它的频率是否按折叠规律跳变。这个坑我在做低电平监测时踩过,白白分析了两天,最后发现是前端放大器二次谐波没滤干净。
6.2 解调失真的几个高频原因
包络解调出来波形"肩部塌陷"、"顶部不平",或者瞬时频率一直在缓慢漂移,原因通常集中在下面几处。
一是希尔伯特变换的边界效应。表现是首尾各有一段包络明显异常,中间正常。解决办法就是 2.1 节的镜像延拓,或者干脆丢掉首尾各 5% 的数据。
二是复包络的残余频偏。表现是包络上叠加了一个周期性的起伏,频率正好等于频偏量。这在下变频时本振频率和信号载波不完全一致时必然出现。解决办法是在复包络上估一次频偏(对相位做线性拟合取斜率),补掉之后再取包络。
三是滤波器群时延不匹配。I 路和 Q 路用了不同阶数或者不同结构的滤波器,两路时延差了个把采样点,复包络的相位就会歪,解调出来的星座图会呈现明显的"旋转拖尾"。这个在 FPGA 实现里特别常见,因为 I、Q 两路经常是分时复用的,很容易在时序上差一拍。
四是抽取时的混叠。抗混叠滤波器阻带衰减不够,带外的大信号折叠进通带。表现是解调输出里混进了与信号无关的周期性成分。判断方法是把抽取倍数减半再看,混叠成分的频率会跟着变。
6.3 常见问题速查表
| 现象 | 最可能的原因 | 快速验证方法 | 处理方式 |
|---|---|---|---|
| 谱线位置与预期差一个固定值 | 下变频频率算错或采样率取整误差 | 用折叠公式重算数字频率 | 校准 $f_c$ 与 $f_s$ 的比值,用有理数精确表示 |
| 噪底比理论值高 10 dB 以上 | 时钟相噪大或前端增益过大 | 断开输入看底噪是否下降 | 换低相噪时钟,重排增益分配 |
| 弱信号被强信号旁的"裙边"淹没 | 窗函数旁瓣太高 | 换布莱克曼窗重算 | 改用低旁瓣窗,或先做干扰对消 |
| 包络首尾异常、中间正常 | 希尔伯特变换边界效应 | 截掉首尾再看 | 镜像延拓后变换,或丢弃边界数据 |
| 陷波后信号整体电平漂移 | 陷波器未做增益归一化 | 看陷波前后直流分量 | 归一化滤波器系数 |
| 抽取后出现莫名的周期性干扰 | 抗混叠滤波不足导致混叠 | 减小抽取倍数复测 | 提高抗混叠滤波器阶数或改多级抽取 |
| 长时间相干积累后信号消失 | 残余频偏导致相位旋转抵消 | 缩短积累时间看是否恢复 | 先做载波同步再积累 |
| 瞬时频率毛刺严重 | 相位差分放大噪声 | 平滑后再差分 | 对复包络先做轻度低通 |
这张表里我最想强调的是最后两行。长时间相干积累失败和瞬时频率毛刺,是窄带处理里最典型的两类"看起来是算法问题、其实是基本物理约束"的故障。前者提醒你相位是有参考系的,后者提醒你微分会放大噪声,两个都是绕不过去的。
7. 一点个人体会
做窄带相关的东西这些年,我越来越觉得这个领域的门槛不在公式,而在对量级的敏感。同样一个"窄带信号",相对带宽是 0.1% 还是 5%,决定了你是用简单的相移近似还是要上分数延迟滤波器;观测时间是 1 ms 还是 1 s,决定了你能分辨 1 kHz 还是 1 Hz;时钟抖动是 1 ps 还是 100 fs,决定了你的动态范围天花板是 67 dB 还是 87 dB。这些数字看起来琐碎,但它们才是方案能不能落地的分水岭。
如果让我给刚上手的人一条建议,那就是:先把允许区间算出来再动手写代码。不管是采样率、滤波器阶数、还是观测时长,先拿笔把区间和边界算清楚,比调参调一天有效得多。我见过太多项目,卡了整整一周,最后发现是采样率选在了允许区间的边缘上,改个数字就通了。