多相滤波器组原理与MATLAB实现:别硬算FFT
2026/9/24 13:01:04 网站建设 项目流程

在做频谱监测项目之前,我对信道化的理解就是“滑窗 FFT,按 bin 拿子带”。直到真把多路射频采样数据丢进去,才发现直接 FFT 这条路在工程上有多难走:窗口泄漏、信道串扰、帧间相位不连续,每一个都能让你的弱信号直接淹死在底噪里。后来换成了多相滤波器组(Polyphase Filter Bank,PFB),同样是用 MATLAB,同样的 M 个信道,计算量降了差不多一个数量级,边缘信道也干净很多。这篇文章我会把原理、完整可跑的 MATLAB 代码、参数到底怎么选,还有我自己调了三天才发现的坑,全部摊开来讲。

如果你是做软件无线电、雷达回波宽带记录、频谱监测,或者正在给某一个接收链路做数字信道化,这篇可以直接拿来当参考。懂一点 FFT,但不想每次都用“先 FFT 再硬抠 bin”这种土办法的人,读下去应该会有收获。

1. 为什么“别硬算 FFT”

1.1 直接 FFT 分信道到底差在哪

很多人一开始的思路都是这样:输入信号按 N 点分帧,加窗,做 N 点 FFT,然后把频谱上对应的几个 bin 当成一个信道,输出给后端解调。代码一气呵成,MATLAB 里几行就能跑通。

但工程上这么干有几个绕不过去的问题。

第一,FFT 本身的频率选择性很有限。它等价于一排中心频率均匀分布的窄带滤波器组,但每个“滤波器”的主瓣宽度、旁瓣高度完全由窗函数决定。矩形窗旁瓣只有 -13 dB 左右,也就是相邻信道里一个强信号,能在旁边信道漏出将近 20% 的幅度;就算换汉宁窗,旁瓣能压到 -30 多 dB,但主瓣变宽,弱信号和强信号频率离得稍近一点,照样被吃掉。对频谱监测、雷达侦收这种动态范围要求 60 dB 以上的场景,直接 FFT 的 bin 结果根本不够看。

第二,帧间相位不连续。直接 FFT 滑窗输出的是每一帧的频谱,而信道化本质上要输出的是“多路时域窄带信号”,不是“一堆频谱快照”。前端做解调、测向、测频时,相位连续性非常关键。你可以通过重叠加窗来改善,但重叠越多,计算冗余越大,最后等于变相把计算量抬上去了。

第三,FFT 输出的每一个 bin 实际上只代表“该频点附近一个频带的积分能量”,并没有做完整的带通滤波和下变频。真正意义上的信道化,应该是先把宽带信号分成一个个窄带子信道,再把每个子信道搬到基带,同时降低采样率。FFT 只完成了“频带划分”里最粗糙的一步,后面的滤波和抽取它一概不负责。

1.2 信道化的正解:滤波 + 下变频 + 抽取

一个规范的数字信道化器,每个信道应该长这样:先有一个中心频率对准该信道的带通滤波器,滤出信道内信号;然后混频到基带;最后按抽取因子 D 降低采样率。也就是说,信道化输出的是 D 倍降采样后的复数基带序列,而不是一帧一帧的频谱数据。

那为什么不直接对 M 个信道分别写 M 个 FIR 滤波器?可以,但计算量你算一下就明白了。

假设每个信道滤波器阶数为 L(也就是 L 个抽头),M 个信道,每输出 M 个样本,就得做 M × L 次乘加。L 为了把邻道抑制度做到 60 dB 以上,通常不会低于 256 阶。M 取 32 的话,M × L = 8192 次乘加,还只是输出 32 个样本。要是 M 取 256,这个数字直接爆炸。

所以才有了多相滤波器组:它把“M 个独立滤波器”的运算,重构成“一组短得多的小滤波器 + 一个 M 点 FFT”,让计算量从 M × L 量级降到 L + M·log₂M 量级。这就是标题里“别硬算 FFT”的真正含义——FFT 不是不能用,而是别让 M 个独立滤波各自为战,要把它和多相分解结合起来用。

2. 多相滤波器组的原理:把推导说人话

2.1 一套看得懂的最小数学版本

先约定几个符号:

  • M:子信道数,也是最终 FFT 的点数。
  • D:抽取因子,通常 D ≤ M。D = M 叫临界抽取,D < M 叫过采样。
  • h[n]:原型低通滤波器,长度 L = M × K。这里 K 是每个多相子滤波器的抽头数,也是我们后面调参时最重要的变量之一。

信道化的核心思想是:先用一个低通原型滤波器把基带信号限制在单个信道带宽内,再通过复指数调制得到 M 个带通滤波器。利用 DFT 调制的周期性,这 M 个带通滤波器可以统一写成一组多相结构。

具体推导简化成这样:

把 h[n] 按相位拆分,定义第 m 个多相子滤波器为

pₘ[n] = h[m + n·M],其中 m = 0, 1, …, M-1,n = 0, 1, …, K-1。

对输入序列 x[k],在第 k 个输出帧,先做相位对齐累加:

uₘ[k] = Σₙ pₘ[n] · x[k·D - n·M - m]

然后对 u 向量做 M 点 FFT,得到 M 个信道的输出:

Y[c, k] = FFT(u₀[k], u₁[k], …, u_{M-1}[k]) 的第 c 个分量。

这个式子看着抽象,但拆开看就很直白:原来的 M 个信道、每个信道 L 阶滤波,变成了 M 个小滤波器并行滤波,最后用一次 FFT 把“调制到不同中心频率”这件事统一做完。滤波运算总量从 M×L 变成了 L,FFT 只是额外附加的成本。

2.2 一个流水线的比喻

打个比方:你有 M 家餐厅窗口,每份套餐原本都要从洗菜切菜开始单独做,那就是 M×L 的工作量。多相分解相当于把厨房改成中央厨房,先把所有食材按“相位”切好、分门别类装进 M 个盒子,然后每个窗口只需要从对应盒子里取料,最后统一按固定配方装盘。这个“装盘”动作就是 FFT。

这里的“相位”其实就是输入样本相对抽取时刻的时间偏移。D 决定每帧推进多少样本,M 决定每个窗口覆盖哪种偏移类型,K 决定每个盒子里能存多少历史食材。

2.3 为什么计算量能降下来,算笔账

以 M = 32、K = 8 为例。原型滤波器长度 L = M × K = 256。

直接法:每个信道一个 256 阶 FIR,输出 32 个样本需要 32 × 256 = 8192 次乘加。

多相法:M 个子滤波器各 8 阶,共 256 次乘加;再加一个 32 点 FFT,大约 32×log₂32 / 2 = 80 次复数乘法(按蝶形运算粗略估)。就算乘 4 算成实数开销,加起来也就 500 多次实数乘加,跟 8192 差了一个数量级以上。

如果 M 增大到 256,直接法每输出 256 点要做 256×2048 = 52 万次乘加;多相法做 2048 次滤波乘加 + 256 点 FFT(约 1024 次复数乘),差距能到两个数量级。这就是多相滤波器组在宽带信道化里几乎成为标配的原因。

3. MATLAB 完整实现与仿真验证

3.1 最核心的函数:pfb_channelizer

我直接给出一个可运行的核心函数。为了减少不同 MATLAB 版本的中文注释乱码问题,函数内部注释我建议用英文,调用示例里我给你中文解释。

function Y = pfb_channelizer(x, M, D, K) % PFB_CHANNELIZER Polyphase filter bank channelizer. % Y = pfb_channelizer(x, M, D, K) % x: input signal, row or column vector. % M: number of sub-channels / FFT size. % D: decimation factor, recommended D <= M. % K: number of taps in each polyphase sub-filter. % Y: M x nFrames complex matrix, channel-by-time output. L = M * K; % prototype filter length h = fir1(L - 1, 1 / M, kaiser(L, 10)); % lowpass prototype % Polyphase decomposition: p_m[n] = h(m + n*M) hp = reshape(h, M, K); % M x K matrix x = x(:).'; N = numel(x); nFrames = floor(N / D); % Prepend L zeros for safe negative index access xp = [zeros(1, L), x]; Y = zeros(M, nFrames); for k = 1:nFrames u = zeros(M, 1); for m = 0:M-1 acc = 0; for p = 0:K-1 % 0-based sample index: (k-1)*D - p*M - m idx = (k - 1) * D - p * M - m; acc = acc + hp(m + 1, p + 1) * xp(L + idx + 1); end u(m + 1) = acc; end Y(:, k) = fft(u); % M-point FFT to form M channels end end

代码逻辑不复杂,但有几个细节你必须注意:

  • fir1(L-1, 1/M, kaiser(L, 10))返回的是 L 个系数,不是 L+1 个。L-1 才是阶数。
  • reshape(h, M, K)是按列填充的,第一列放 h(1)~h(M),第二列放 h(M+1)~h(2M)。这样hp(m+1, p+1)正好等于 h(m + p×M),也就是多相分解定义。
  • 前面补 L 个零是为了统一处理负索引。输入信号最开头那几个样本,在物理上是“没有历史数据”,补零等效于假设滤波器初始状态为零,这是正确的。

3.2 仿真脚本:看信道化结果对不对

下面给一个测试脚本,输入三个固定频率的复正弦,频率分别设计在信道中心,方便对照输出:

fs = 100e6; % 100 MHz sample rate N = 4096; t = (0:N-1) / fs; % Three tones at channel center frequencies % Channel spacing = fs / M = 3.125 MHz x = exp(1j*2*pi*6.25e6*t) + ... % channel 2 (0-based) exp(1j*2*pi*18.75e6*t) + ... % channel 6 (0-based) exp(1j*2*pi*(-6.25e6)*t); % channel 30 (0-based, alias at 93.75 MHz) M = 32; D = 32; K = 8; Y = pfb_channelizer(x, M, D, K); % Plot time-frequency image nFrames = size(Y, 2); figure; imagesc((0:nFrames-1)*D/fs, (0:M-1)*fs/M, 20*log10(abs(Y) + eps)); xlabel('Time (s)'); ylabel('Frequency (Hz)'); colorbar; title('PFB Channelizer Output');

理想情况下,abs(Y)的三个峰值应该分别落在第 3、第 7、第 31 行(MATLAB 是 1-based,所以比 0-based 索引多 1),也就是对应 6.25 MHz、18.75 MHz、93.75 MHz 这三根谱线。

再画一个通道平均功率谱,更直观:

Pavg = 20*log10(mean(abs(Y).^2, 2)); figure; stem((0:M-1)*fs/M, Pavg, 'filled'); xlabel('Channel center frequency (Hz)'); ylabel('Average power (dB)'); title('Per-channel average power');

你应该会看到三个明显的尖峰,其他通道基本贴在底噪上。如果你用的是 D = M = 32 的临界抽取,边缘通道(靠近 0 Hz 和 Nyquist 那两个)可能会有轻微泄漏,这是正常现象,后面第 4 节会讲怎么用 D < M 来改善。

3.3 再验证一下:跟“直接下变频 + 滤波 + 抽取”对比

如果你心里还是没底,可以写一个直白的基准函数:对每个信道单独做下变频、低通滤波、M 倍抽取。这个基准实现绝对正确,只是慢。然后用它和pfb_channelizer对比:

function Y_ref = ref_channelizer(x, M, D, K) L = M*K; h = fir1(L-1, 1/M, kaiser(L, 10)); x = x(:).'; N = numel(x); nFrames = floor(N/D); Y_ref = zeros(M, nFrames); for c = 0:M-1 % down-convert to baseband fc = c*fs?; % 注意:这个示例需要传入fs,略作示意 end end

这里我不展开完整代码,对比思路就是:对第 c 个信道,先用 exp(-1j2pifct) 下变频,再用 h 低通,最后每 D 个点取一个输出。两种实现的最大误差应该在 1e-6 量级,甚至更小。一旦误差对不上,优先检查多相索引方向。

3.4 如果有 DSP System Toolbox

如果你的环境里有 DSP System Toolbox,最省事的工程化对象是dsp.Channelizer

chn = dsp.Channelizer('NumFrequencyChannels', M, ... 'StopbandAttenuation', 80, ... 'DecimationFactor', D); Y = chn(x.');

这个系统对象内部帮你把多相分解和 FFT 都封装好了,而且支持流式处理。但说实话,手写一遍再换它,你对参数含义的理解会完全不同。我建议先用上面的pfb_channelizer跑通,再决定要不要切到系统对象。

4. 参数到底该怎么选

4.1 M:子信道数

M 的第一重身份是 FFT 点数,第二重身份是信道个数。在设计前端时,M 通常不是拍脑袋定的,而是由“目标子信道带宽”反推出来的:

M = 采样率 fs / 单信道带宽 B_ch

举例:你在做 100 MHz 采样率的宽带接收机,想每路窄带信号带宽大约 1 MHz,那 M 就取 100 左右;考虑到 FFT 效率,M 最好取 2 的幂,比如 128,再把采样率或者信道带宽稍微微调。M 越大,单信道带宽越窄,FFT 点数越高,但同步意味着 K 相同时原滤波器更长,处理延迟更大。不要盲目取大,够用就好。

4.2 D:抽取因子,工程上最值得花心思的地方

D = M 是临界抽取,输出数据量最小,后端存储和解调压力最小。但临界抽取有一个隐患:滤波器过渡带必然存在,过渡带里的能量会混叠进相邻信道,导致边缘信道失真。实际射频信号里,信道和信道之间往往还有相邻信道干扰,这个失真会被放大。

我的工程习惯是 D 取 M 的 75% 到 87.5%。例如 M = 32 时,D 取 24 或 28。这样每个信道留出 12.5% 到 25% 的保护间隔(guard band)。代价是输出帧率更高,数据量多了百分之十几到二十几,但边缘信道干净得多。对测向、测频、解调这种后续环节来说,这十几二十的冗余数据非常值。

4.3 K:多相子滤波器抽头数

K 决定原型滤波器的过渡带陡峭程度和阻带衰减能力。K 小,比如 K = 2,原型滤波器只有 M×2 阶,过渡带非常宽,相邻信道之间“你中有我”,信道隔离度基本没法看。K 大,比如 K = 16 或 32,滤波变得很陡,邻道抑制度能到 80 dB 以上,但滤波器群延迟变大,时域上“拖尾”更长,瞬态响应时间变长。

对我来说,K 的常用区间是 4 到 16。频谱监测这种弱信号检测场景,K 取 8 以上比较稳;如果只是把一个宽带信号粗分成几个子带,后端还有均衡补偿,K 取 4 也够。想用更小的 K 就接受更高的串扰,想用更大的 K 就接受更长的延迟和更多计算量,这是最基本的权衡。

4.4 原型滤波器的截止频率怎么设

很多新手会在fir1的截止频率参数上翻车。fir1(L-1, 1/M, ...)里的1/M指的是“截止频率位于 Nyquist 频率的 1/M”,也就是数字角频率 π/M。对应的物理截止频率是 fs/(2M),双边带通带总宽度是 fs/M,正好是一个信道带宽。

如果 D < M,可以适当调整这个截止频率,给过渡带留位置。我的经验是从1/M开始,跑一次带内信号和邻道强干扰的仿真,观察左右边缘信道输出;如果发现靠近保护带的信道幅度明显下凹,就把截止频率往上提一点,比如提到(D/M) * (1/M)之类,但一定要重新跑邻道抑制度指标。滤波器设计最终服务的是系统指标,不是某个公式。

5. 避坑指南与问题排查

5.1 我踩过的五个坑

第一个坑:reshape方向搞反。很多人会把多相分解写成hp = reshape(h, K, M),结果子滤波器的抽头顺序完全错位,输出全是乱码一样的频谱。记住,按列填充时,第一维必须是 M,后面才不会错。

第二个坑:补零不够导致索引越界。你在函数里写xp(L + idx + 1),如果前面只补了M*K/2个零,p取到最大、m取到最大时,idx会变成负数,MATLAB 直接报错或者给你一个错误结果。我推荐统一补L个零,多补无害。

第三个坑:fir1长度没算对。fir1(n, Wn)返回 n+1 个系数。如果你想总长度正好是 M×K,必须写fir1(L-1, ...)。这个错很隐蔽,因为 MATLAB 不报错,但reshape会直接因为元素个数不匹配而红灯。

第四个坑:FFT 通道顺序和频谱坐标。fft(u)的第 1 个通道是 0 Hz,不是负频率。画图时(0:M-1)*fs/M作为频率坐标是对的,但如果你习惯画fftshift后的双边谱,别把输出通道和物理频率对应关系弄混。调试时先输入单音信号,看峰值落在第几个通道,对应频率对不对,再往下调。

第五个坑:中文注释乱码。虽然不影响运行,但 MATLAB 在部分 Windows 中文环境下打开旧脚本,中文注释会变成乱码。我的建议是核心算法注释直接写英文,或者用纯 ASCII,省得换一台机器就一片乱。写笔记和博客再用中文仔细解释。

5.2 常见问题速查表

现象可能原因解决办法
输出全是 NaN 或 Inf输入信号含 NaN,或数据溢出检查输入信号;考虑转 single/double 类型
某个信道输出始终接近 0K 太小或滤波器截止设置过紧,信道通带太窄增大 K;检查原型滤波器截止频率
邻道串扰大,弱信号被强信号顶掉K 过小;临界抽取导致过渡带混叠增大 K;D 改为 0.75~0.875 M
边缘信道幅度明显低于内部信道D = M 时边缘信道过渡带被裁用 D < M 留保护带
输出帧数比预期少nFrames = floor(N/D)最后一个不完整帧被丢弃接受丢弃;或采用重叠处理保留尾部
reshape报维度错误原型滤波器长度不是 M 的整数倍检查fir1参数,确保总长度 = M×K

5.3 验证方法比代码更重要

我强烈建议你在正式接入信号之前,先跑固定单音测试。输入正弦频率放在第 c 个信道中心,理论输出应该只有第 c+1 行有能量(MATLAB 1-based)。然后移动频率到两个信道中间,观察能量是平均分配到两个信道,还是被某一边吃掉。这个实验能帮你快速确认 M、D、K 是否匹配。

另外一个好用的验证信号是扫频信号,比如从 0 扫到 fs/2。用 imagesc 看时频图,你应该看见一条倾斜的亮线依次穿过各信道。线附近如果有明显拖尾,说明滤波器旁瓣不够低或 K 不够大。

最后再分享一点个人习惯

我现在做信道化相关项目,第一反应已经很少是“直接 FFT”了,而是先问自己三个问题:需要多少个信道、能容忍多大邻道泄漏、后端要的是时域序列还是频谱快照。想清楚这三点,再决定用临界抽取还是过采样,用 K=8 还是 K=16。

另外,如果你最终要在 FPGA 或嵌入式平台落地,MATLAB 这边验证通过只是第一步。多相分解后的子滤波器系数可以直接导出成查找表,FFT 部分用现成 IP 核,整个数据结构非常规整,这也是这个算法在工程里特别受欢迎的原因之一。

这个多相滤波器组代码还能继续扩展的方向也很多,比如加一个自动增益控制、把输出改成正交解调格式、或者用两级级联做可变带宽信道化。你有具体场景的话,基于上面的代码改起来会很快。

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

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

立即咨询