在嵌入式音频、软件无线电、传感器信号调理这些场景里,FIR 滤波器几乎是绕不开的一环,而只要动手设计 FIR 滤波器,第一个撞上的岔路口就是窗函数怎么选。滤波器设计软件里那个下拉框——矩形、汉宁、汉明、布莱克曼、凯泽——看起来只是个选项,实际上它直接决定了过渡带有多少宽、阻带能压多深、阶数要堆到多少、后期定点化会不会翻车。这篇内容不讲教科书式的推导,专讲在真实项目里,怎么从一句“阻带 80dB、过渡带 2kHz”的需求,一步步反推出该用哪种窗、长度取多少、参数怎么定,以及哪些坑是手册上不会写、只有自己调过才知道的。无论你是刚开始接触数字滤波的学生,还是已经在做音频或射频链路的工程师,下面这些手算流程和参数陷阱都能直接拿去用。
1. 窗函数在 FIR 滤波器里到底修的是什么
1.1 理想低通截断后的振铃,是绕不过去的
理想低通的冲激响应是 sinc 函数,它在时间轴上是无限长的,向两侧缓慢衰减。要把它变成可以写进代码、跑进 FPGA 的有限长系数,最直觉的做法就是直接从中间截一段出来,两侧全部置零,这在数学上等价于乘了一个矩形窗。问题也随之而来:时域的突然截断在频域就是与矩形窗的频谱做卷积,把原本干净的砖墙响应抹成了一片带波纹的形状,通带内出现起伏,阻带内出现一长串旁瓣,第一旁瓣只比主瓣低 13dB 左右。
这就是吉布斯现象。它的本质不是程序写错了,也不是采样率不够,而是“有限长度”这个约束本身带来的代价。工程上没法消除它,只能压制它,窗函数就是用来压制这件事的工具。所谓选窗,本质上是在挑一条衰减曲线:让 sinc 两端平滑地衰减到零,而不是一刀切下去,振铃就会被削下去。
我见过不少刚上手的人,看到阻带压不下去第一反应是加大阶数,实际上阶数只影响过渡带的陡峭程度,对阻带最低能到多深几乎没帮助。阻带能压多深,主要由窗的形状决定,这个认知上的区分非常关键,后面所有选型判断都建立在这上面。
1.2 主瓣管过渡带,旁瓣管阻带
任何窗函数的频谱都可以拆成两部分来看:中间那个又高又胖的主瓣,以及两侧一排排逐渐降低的旁瓣。这个拆法不是学术游戏,它直接对应两个工程指标。
主瓣的宽度决定了卷积之后响应被抹开的范围,换句话说就是过渡带有多宽。主瓣越窄,从通带到阻带的切换越干脆。矩形窗的主瓣最窄,所以同样阶数下它的过渡带最陡,这也是它唯一的优势。
旁瓣的高度决定了阻带能被压到多低。旁瓣越高,泄漏进阻带的能量越多,阻带衰减就越差。矩形窗的第一旁瓣只有 -13dB 的相对高度,设计出来的滤波器阻带衰减也就卡在 -21dB 左右,这个数字在绝大多数场景里都不够看。汉宁窗把旁瓣压到 -31dB,汉明窗压到 -43dB,布莱克曼窗直接压到 -57dB,代价是主瓣被撑宽,过渡带跟着变宽。
这两者是一对天然的矛盾,压旁瓣必然撑主瓣,撑主瓣必然拖宽过渡带,拖宽过渡带就必须加阶数来补。所有的窗函数设计,都是在这条权衡曲线上选一个自己舒服的点,没有哪个窗能同时做到过渡带最窄和阻带最深。谁要是跟你说某个窗“全面最优”,那基本是不了解这条曲线。
1.3 选窗之前先摆清四个指标
很多选型讨论之所以吵不出结果,是因为大家手里的指标不一样。动手之前,先把这四个数字白纸黑字写下来,选型会变得非常机械。
第一是过渡带宽度,也就是从通带边缘到阻带边缘差多少赫兹。这个数字决定阶数,是最贵的一个指标。过渡带收窄一半,阶数基本翻倍,运算量和存储都跟着翻。
第二是阻带最小衰减,单位 dB。这个数字决定窗型,80dB 以下汉明窗勉强,再深就得上布莱克曼或凯泽。
第三是通带允许的纹波。窗法有个先天特点:通带纹波和阻带衰减近似对称,阻带压到 80dB,通带纹波也就大致在 0.01dB 这个量级。如果你的指标是通带纹波 0.001dB、阻带只要 40dB,那窗法就浪费了,应该换等波纹设计。
第四是采样率和截止频率的位置。所有公式都是归一化频率下的,实际算的时候必须换算,换算错了整个设计就白做。
把这四条写在纸上,再去看窗函数对照表,选择范围通常只剩一两个。我个人的习惯是先按阻带要求筛窗口类型,再按过渡带要求算长度,最后仿真验证,不达标就加 5% 到 10% 的长度重来,比反复纠结理论公式省事得多。
2. 六大常用窗横向评测:把数字摊开看
2.1 一张对照表看清主瓣、旁瓣与过渡带
不同资料给出的过渡带系数常常对不上,原因是它们对“过渡带”的定义不同:有的按主瓣宽度算,有的按 -6dB 到 -60dB 的实际跌落宽度算,有的按满足特定阻带指标的等效宽度算。下面这张表我用的是最常见的归一化表述,过渡带以 Δω = kπ/N 给出,N 为滤波器长度,这样换算到实际频率时最不容易出错。
| 窗类型 | 主瓣宽度(双侧) | 最高旁瓣 | 可达阻带衰减 | 过渡带系数 k | 适用场景 |
|---|---|---|---|---|---|
| 矩形窗 | 4π/N | -13dB | 约 -21dB | 1.8 | 频谱分析、瞬态检测,滤波器里极少用 |
| 巴特利特窗 | 8π/N | -27dB | 约 -25dB | 6.1 | 教学演示,实际几乎被汉宁替代 |
| 汉宁窗 | 8π/N | -31dB | 约 -44dB | 3.1 | 通用低通、抗混叠、频谱分析 |
| 汉明窗 | 8π/N | -43dB | 约 -53dB | 3.3 | 语音通信、对第一旁瓣敏感的场合 |
| 布莱克曼窗 | 12π/N | -57dB | 约 -74dB | 5.5 | 高阻带要求、测量仪器前端 |
| 凯泽窗 | 可调 | 可调 | 可调至 -100dB 以上 | 由 β 决定 | 指标明确的工程实现首选 |
看这张表要抓两个要点。第一,过渡带系数和阻带衰减是绑在一起动的,从汉宁到布莱克曼,阻带深了 30dB,过渡带系数从 3.1 涨到 5.5,等于同样过渡带要求下阶数要多花 77%。第二,汉宁和汉明的过渡带系数几乎一样,但阻带差了 9dB,这 9dB 是白捡的,所以只要不是对远端旁瓣有特殊忌讳,汉明在多数场景里比汉宁更划算。
2.2 矩形窗:唯一能给出最陡过渡带的窗
矩形窗在所有窗里是异类。它的主瓣最窄,同样长度下过渡带最陡,理论上没有任何一个窗能在这项指标上赢过它。但它的第一旁瓣高达 -13dB,设计出来的滤波器阻带衰减卡在 -21dB 附近,实际能用的场合少得可怜。
那它什么时候还有价值?我遇到过两类。一类是本身阻带要求就极低的场景,比如某些只做粗略平滑的预处理,20dB 衰减足够;另一类是频谱分析用的窗,这里矩形窗反而是首选,因为它的频率分辨率最高,能把靠得很近的两个谱线分开,代价是泄漏大,需要用别的手段补偿。
还有一个容易忽略的场合:当滤波器长度已经由硬件资源锁死,而过渡带又实在做不到时,有人会退而求其次用矩形窗换陡度,然后在后面串一级模拟滤波或者用自适应算法补阻带。这种做法我不推荐,因为 -21dB 的阻带意味着高频分量会原封不动地漏进来,数字域后面怎么补都补不干净。真要做这种妥协,至少也要用汉宁,把阻带先拉到 -44dB 再谈其他。
顺带提一句巴特利特窗,也就是三角形窗。它的旁瓣衰减比矩形好,但主瓣宽了一倍,阻带只改善 4dB,性价比极差,现在基本只在教学里出现,工程实现里直接跳过就行。
2.3 汉宁窗与汉明窗:一个字的差别在阻带
这两个窗名字只差一个字,公式也只差一个系数,但频域表现差别不小,值得单独拎出来讲。
汉宁窗的时域表达式是 0.5 - 0.5cos(2πn/(N-1)),两端严格归零,频谱上旁瓣以 18dB/倍频程的速度快速衰减,第一旁瓣 -31dB,远端旁瓣掉得很快。这个特性让它在抗宽带噪声时表现很好,因为远端泄漏少。
汉明窗的表达式是 0.54 - 0.46cos(2πn/(N-1)),两端不归零,停在 0.08 的位置。这个不归零的设计让它的第一旁瓣被压到 -43dB,但代价是远端旁瓣不再快速衰减,形成一个大约 -43dB 的平台,一直延伸到很远。
所以选型逻辑很清晰:干扰主要集中在靠近过渡带的位置,用汉明;干扰是宽带的、遍布高频,用汉宁。语音通信里常见的情况是邻近信道干扰,靠得比较近,汉明更合适;而抗混叠滤波器面对的是整个高频段的折叠能量,汉宁的快速远端衰减反而更有优势。
我自己的默认选择是汉明,理由是 -53dB 的阻带在实际项目里刚好卡在很多指标的及格线上,而且过渡带系数只比汉宁大 0.2,几乎不花额外阶数。如果指标要求 60dB 以上,两个都不够,直接跳到布莱克曼或凯泽,不要在汉宁汉明之间反复试。
2.4 布莱克曼和平顶:深阻带与幅度精度的两种取向
布莱克曼窗由三个余弦项叠加而成,主瓣宽度达到 12π/N,是汉宁的 1.5 倍,但阻带能压到 -74dB 左右。这个数字让它成为高阻带要求场景的默认答案:测量仪器前端、高精度 ADC 的抗混叠、需要压制强邻道干扰的接收链路,基本都在这个量级。
布莱克曼的另一个好处是旁瓣衰减速度快,远端泄漏很低,适合处理那种“近端干扰不强但远端杂散很多”的场合。缺点是过渡带太宽,同样指标下阶数比汉明多六七成,运算量吃紧的嵌入式平台要掂量一下。
平顶窗则是另一个方向的极端。它的通带极其平坦,幅度误差可以压到 0.01dB 以内,专门用于幅度测量,比如校准信号源、功率测量。但它的主瓣极宽,过渡带是布莱克曼的两三倍,用在滤波器设计里基本等于自杀。我见过有人把平顶窗误用到低通设计上,结果两千阶都做不出想要的过渡带,排查了半天才发现窗选错了。记住一句话:平顶窗是给频谱分析用的,不是给滤波用的。
介于两者之间还有 Blackman-Harris、Nuttall 这些四项、五项窗,阻带能到 -90dB 以上,过渡带系数在 7 到 9 之间。它们的存在意义是:当你需要 90dB 阻带但又不想用凯泽窗那套数值求解时,直接调库里的现成窗更省事。实际项目里我用得不多,因为凯泽窗在同等指标下过渡带更窄,参数也更直观。
2.5 凯泽窗与切比雪夫窗:把旁瓣电平变成可调旋钮
前面那些窗都是固定形状,选定了阻带就定了。凯泽窗不一样,它有一个参数 β,可以连续调节主瓣和旁瓣的分配比例,等于把“选窗”变成了“调参”。
β 越大,旁瓣压得越低,主瓣越宽;β 等于 0 时退化成矩形窗。这个连续可调的特性让凯泽窗成为工程实现的万能解:指标说 62dB,就调 β 到刚好 62dB 多一点,不多花一点阶数,也不留太多余量。配合经验公式,从指标到阶数和 β 可以一步算出来,不需要试错。
切比雪夫窗(又叫 Dolph-Chebyshev 窗)走的是另一条路:它让所有旁瓣高度完全相等,形成等波纹。这个特性在需要精确控制最大旁瓣电平时很有用,比如某些雷达脉冲压缩场景。但对普通滤波器设计来说,等波纹旁瓣并不比凯泽的递减旁瓣更有优势,而且参数只有“旁瓣电平”一个,过渡带的对应关系不如凯泽直观,所以我用得更少。
实际操作中,如果项目里已经有成熟的凯泽窗设计流程,就别折腾别的了;如果指标里有明确的“最大旁瓣不得高于 X dB”这种表述,再考虑切比雪夫。
3. 从指标反推窗型与阶数的手算流程
3.1 完整案例:48kHz 采样率下 80dB 抗混叠滤波器
拿一个真实需求走一遍完整流程。某音频采集链路,采样率 48kHz,要求通带 0 到 8kHz 内纹波不超过 0.01dB,10kHz 以上衰减不低于 80dB。
第一步,确定过渡带。通带边缘 8kHz,阻带起点 10kHz,过渡带宽度 Δf = 2kHz。归一化到采样率是 0.04167,换算成角频率 Δω = 2π × 2000 / 48000 = 0.2618 rad/sample。
第二步,选窗型。80dB 这个数字直接排除汉宁(-44dB)和汉明(-53dB),布莱克曼的 -74dB 也不够,只剩凯泽窗(或者 Blackman-Harris 这类高阶窗)。选凯泽。
第三步,算阶数。凯泽窗的经验公式是 N ≈ (A - 8) / (2.285 × Δω),其中 A 是阻带衰减的 dB 数。代入 A = 80,Δω = 0.2618:
N ≈ (80 - 8) / (2.285 × 0.2618) = 72 / 0.5982 ≈ 120.4
取整到 121 个系数(奇数长度,保证 Type I 线性相位,低通设计用奇数长度最省心)。阶数是 120 阶。
第四步,算 β。A = 80 大于 50,用公式 β = 0.1102 × (A - 8.7) = 0.1102 × 71.3 ≈ 7.86。
第五步,定截止频率。这里有个关键细节:截止频率要放在过渡带的正中间,也就是 (8000 + 10000) / 2 = 9000Hz。原因在于窗法设计的低通响应在截止频率处的幅度大约是 -6dB,如果直接把截止频率设在 8kHz,那么 8kHz 处的衰减就已经是 -6dB 了,通带边缘指标立刻失败。
第六步,生成并验证。用 Python 的 scipy 一行就能出系数:
import numpy as np from scipy import signal fs = 48000 numtaps = 121 fc = 9000 beta = 7.86 taps = signal.firwin(numtaps, fc, window=('kaiser', beta), fs=fs, scale=True)这段代码跑出来的系数,实测阻带通常在 -80dB 到 -85dB 之间,通带偏差在 1e-4 量级,指标达标。如果实测差一点点,把长度从 121 加到 127 或 129 重跑,一般就够了。
3.2 窄过渡带的代价:1kHz 到 1.2kHz 要多少阶
换个更极端的场景,感受一下过渡带宽度对阶数的影响有多剧烈。同样是 48kHz 采样率,通带 0 到 1kHz,阻带从 1.2kHz 开始,要求衰减 60dB。
过渡带 Δf = 200Hz,归一化后是 0.004167,Δω = 0.02618。A = 60,落在 21 到 50 之上,用第一个 β 公式。先算阶数:
N ≈ (60 - 8) / (2.285 × 0.02618) = 52 / 0.05982 ≈ 869
取 871 个系数。β = 0.1102 × (60 - 8.7) = 5.65。
从 121 个系数到 871 个,翻了三倍多,只因为过渡带从 2kHz 收到了 200Hz。这就是为什么工程师总在跟系统那边争过渡带指标:过渡带每收窄一半,乘法器资源基本翻倍。如果这个滤波器要跑在 48kHz 实时链路上,871 阶意味着每秒约 4200 万次乘加,对小型 MCU 已经不轻松,得上 DSP 或者降采样后再滤。
所以实际项目里有一个常用套路:先降采样,再滤波。如果信号带宽只有 1kHz,先在 48kHz 下用几十阶的粗滤波器滤一遍,抽取到 4.8kHz,再在低采样率下做窄过渡带滤波。同样的绝对过渡带宽度,在低采样率下归一化后宽了十倍,阶数直接降到十分之一。这个思路比死磕窗函数选型有用得多。
3.3 凯泽窗 β 的三种取值方法与我的习惯
β 的取值有三种常用途径,适用场合不太一样。
第一种是公式法,就是上面用的两个分段公式。A 大于 50 时用 0.1102(A - 8.7),A 在 21 到 50 之间用 0.5842(A - 21)^0.4 + 0.07886(A - 21),A 小于 21 时 β 取 0。这个公式误差在 0.1dB 量级,工程上完全够用。
第二种是查表法,经典文献里给了一张 β 和阻带衰减的对照表,21dB 对应 0,30dB 对应 1.5,40dB 对应 2.5,50dB 对应 3.4,60dB 对应 4.5,70dB 对应 5.7,80dB 对应 7.0,90dB 对应 8.5,100dB 对应 10.0。查表比公式快,但两点之间还是要插值,实际精度不如公式。
第三种是仿真迭代法,先按公式估一个 β,跑一次频响,看实测阻带,差多少就微调。这个方法最笨但最可靠,尤其是当你的指标定义比较特殊(比如阻带从某一段开始算、或者要求某一段相对衰减)时,公式的假设可能不成立,只能靠仿真兜底。
我的习惯是:先用公式算,然后故意把 β 加大 3% 到 5%,宁可阻带深一点、过渡带宽一点点,也不要在量产批次的边角料上卡线。因为实际器件的频率响应会随温度、时钟抖动变化,设计时刚好卡在 80dB,实测掉到 78dB 是很常见的事,留点余量比返工便宜得多。
3.4 窗法做不到的事:独立控制通带与阻带
这一点必须单独说清楚,因为它决定了什么时候该放弃窗函数,改用等波纹设计。
窗法设计出来的滤波器,通带纹波和阻带衰减是近似对称的,都是 δ 的量级。阻带压到 80dB,意味着 δ ≈ 1e-4,通带纹波也就在 ±0.0001 左右。这个特性对大多数场景是好事,通带白送一个高平坦度。
但如果你的指标是通带纹波 0.01dB、阻带只要 45dB,窗法就亏了。0.01dB 对应 δ ≈ 0.001,同样的 δ 给出 60dB 阻带,多出来的 15dB 全浪费在阻带上,而这些余量本来可以换成更窄的过渡带。这种“通带严、阻带松”的指标,用 Parks-McClellan 等波纹算法能省下 30% 以上的阶数。
反过来,“通带松、阻带严”的指标,窗法也不划算,因为通带精度被强行拉到和阻带一样高。
所以判断标准很简单:**通带纹波要求和阻带衰减要求相差在 10dB 以内,用窗法;相差超过 20dB,考虑等波纹。**这个经验值不是理论推导出来的,是我在几个项目里对比实测结果之后总结的,比较粗糙但好用。
等波纹算法也有自己的问题:设计过程是迭代的,某些极端指标下会不收敛;通带和阻带都是等波纹,某些场合(比如音频)听起来不如窗法的平滑滚降自然;系数的解析性差,出了问题不好解释。所以我的一般顺序是:先试窗法,不满足再上等波纹,而不是一上来就用最优算法。
4. 从系数到验证:一条可复现的实现链路
4.1 生成系数的三种写法与参数陷阱
同一个滤波器,用 scipy 有三种常见写法,每种都有坑。
第一种用signal.firwin,这是最常用的:
taps = signal.firwin(numtaps=121, cutoff=9000, window='hamming', fs=48000)这里第一个坑是截止频率的归一化方式。老版本 scipy 用nyq参数传奈奎斯特频率,新版本用fs传采样率,两者混用会导致频率差一倍。如果你接手的是别人的老代码,看到firwin(N, 0.1875, nyq=0.5)这种写法,说明它用的是归一化到奈奎斯特频率的老接口,0.1875 对应的实际频率是 0.1875 × 24000 = 4500Hz。新版要写成firwin(N, 4500, fs=48000)。这个换算错一次,滤波器中心频率整个偏掉,而且频响图看起来还挺正常,特别难发现。
第二个坑是scale参数,默认是 True,会把系数归一化到直流增益为 1。低通设计这没问题,但如果做的是高通或者带阻,直流增益本来就该是 0,这个归一化会做出奇怪的结果。做高通时务必设scale=False,自己按通带中心频率归一化。
第三个坑是window参数传元组的形式:window=('kaiser', beta),注意 beta 是位置参数,不能写成关键字。写成window='kaiser'会用默认 β,出来的阻带跟算好的对不上。
第二种写法是直接构造窗再乘 sinc:
n = np.arange(numtaps) m = n - (numtaps - 1) / 2 h_ideal = np.sinc(2 * fc / fs * m) * 2 * fc / fs w = np.kaiser(numtaps, beta) taps = h_ideal * w这种写法透明,每一步都能改,适合做变体设计,比如变截止频率、加窗后再做微调。缺点是容易漏掉归一化,或者把 sinc 的定义搞错(numpy 的 sinc 是 sin(πx)/(πx),不是 sin(x)/x,代入时要乘 π)。
第三种是手写查表系数,用于极低资源的定点平台。这种情况一般只在系数固定、代码空间紧张时用,把系数存成常量数组,配合移位加法实现乘法。要注意的是量化后的系数必须保持对称,否则线性相位就没了。
4.2 频响验证:把主瓣旁瓣量出来
系数生成之后必须验证,不能只信公式。验证的核心是三件事:通带偏差、阻带最低点、过渡带实际宽度。
w, h = signal.freqz(taps, worN=1 << 16, fs=fs) mag_db = 20 * np.log10(np.abs(h) + 1e-15) # 通带最大偏差 passband = mag_db[w <= 8000] print("通带偏差:", np.abs(passband).max(), "dB") # 阻带最强点 stopband = mag_db[w >= 10000] print("阻带衰减:", -stopband.max(), "dB") # 找 -3dB 和 -60dB 点,看实际过渡带 idx_3db = np.argmax(mag_db <= -3) idx_60db = np.argmax(mag_db <= -60) print("实际过渡带:", w[idx_60db] - w[idx_3db], "Hz")这段代码里worN取 2 的 16 次方是有讲究的。**FFT 点数不够会导致频率采样太粗,阻带的尖峰可能被跳过,量出来的衰减值偏乐观。**我见过有人用 512 点 FFT 验证一个 800 阶滤波器,看到阻带 -90dB 就交付了,实际上用细网格一测只有 -70dB。经验做法是取至少 8 倍于滤波器长度的点数,再向上取到 2 的幂。
还有一个细节:np.argmax(mag_db <= -60)找的是第一个满足条件的点,如果通带里就有低于 -60dB 的凹陷(比如设计出错),这个结果会完全不对。严谨一点的做法是先定位到通带外,再在阻带区间内搜索。
4.3 系数量化与线性相位对称性
浮点系数直接搬到定点硬件上,必须过量化这一关。这里最容易出的问题是对称性被破坏。
理想情况下,线性相位 FIR 的系数满足 h[n] = h[N-1-n],严格对称。但浮点转定点时,如果两个对称位置的系数分别做四舍五入,有可能一个进位一个舍去,得到一个不对称的系数对。这种不对称在频域表现为相位响应出现轻微弯曲,音视频里可能表现为轻微的相位失真,在通信解调里可能直接导致误码率上升。
稳妥的做法是量化后强制对称:
q = 15 # Q15 格式 taps_q = np.round(taps * (1 << q)).astype(np.int64) # 强制镜像对称 taps_q = (taps_q + taps_q[::-1]) // 2 # 再检查一遍 assert np.array_equal(taps_q, taps_q[::-1])这里用整除 2 而不是取平均后取整,是为了避免再引入一次舍入。做完之后把系数还原成浮点,重新跑一次频响,看指标掉了多少。Q15 量化通常会让阻带衰减恶化 3 到 6dB,如果原始设计只留了 2dB 余量,量化后就不达标了。这就是前面强调留 5% 余量的原因。
量化位宽怎么选?我的经验是:阻带要求 60dB 以下用 Q12,60 到 80dB 用 Q15,80dB 以上用 Q17 或者干脆保留浮点。Q15 理论动态范围约 96dB,扣掉滤波器自身的增益余量,实际能保证 80dB 左右,再往上就吃紧了。
4.4 群延迟补偿与定点实现的溢出检查
线性相位 FIR 的群延迟是 (N-1)/2 个采样点。121 个系数对应 60 个采样点,48kHz 下是 1.25ms。这个延迟在很多场景里必须补偿,比如多通道同步采集,几个通道的滤波器长度不一致,不补偿就会出现通道间相位差;又比如闭环控制系统,1.25ms 的延迟可能直接影响稳定性。
补偿方法很简单:在数据流前面补相应数量的零,或者在时间戳上做偏移。麻烦的是当滤波器长度动态变化时(比如自适应滤波),延迟也跟着变,这时候要么固定长度,要么在输出端做可变延迟线。
定点实现的溢出检查是另一件常被忽略的事。滤波器输出的最大绝对值不会超过输入最大值乘以系数绝对值之和,也就是 sum(|h[n]|)。低通滤波器的这个值在 1 到 1.1 之间,看起来不大,但中间累加过程是逐项相加的,如果累加器位宽不够,中间结果会溢出。
计算方法:输入 16 位有符号数,最大值 32767,累加器至少需要 16 + log2(sum|h| × 2^Q) 位。以 Q15 系数、121 阶为例,sum|h| × 2^15 ≈ 1.05 × 32768 ≈ 34406,log2 约等于 15.07,所以累加器需要 16 + 15 + 1 = 32 位。用 32 位累加器是安全的,用 24 位就要小心,得先算清楚最坏情况。
还有一个坑:系数的直流增益归一化之后,sum(h) 等于 1,但 sum(|h|) 不等于 1。对于某些窗(比如旁瓣很高的),sum(|h|) 可能达到 1.3 甚至更高,溢出余量要按这个算,不能按 1 算。
5. 踩过的坑与排查速查
5.1 过渡带估算偏差的三个常见来源
公式算出来的阶数,实测往往不够,差个 10% 到 20% 是常态。原因主要有三个。
第一是公式本身的定义差异。前面说过,凯泽窗的 N 公式里,Δω 的定义是阻带边缘减通带边缘的角频率差,但有些资料里用的是主瓣宽度,有些用的是 -6dB 到 -60dB 的宽度,这几个定义之间能差 30%。混用不同来源的公式,结果自然对不上。
第二是截止频率的位置。把截止频率设在过渡带中点,是理论最优;但实际指标往往只约束通带边缘和阻带起点,中点位置稍微偏一点,实测指标就会在某一侧富余、另一侧不够。我的做法是中点定位之后,分别检查两侧,哪侧不够就往哪侧挪一点截止频率,通常挪 5% 就能补齐。
第三是指标的定义方式。有些指标说“10kHz 以上衰减 80dB”,实际是指从 10kHz 开始算;有些是说“10kHz 处衰减 80dB,之后不限”。前者宽松,后者严格,按后者设计出来的滤波器要比前者长不少。跟提需求的人确认清楚这几个字,能省下几百阶。
5.2 通带边缘掉下去的那些情况
通带边缘衰减超标,是我遇到过最多的问题。典型表现是:设计要求 8kHz 处衰减不超过 0.1dB,实测 8kHz 处掉了 0.5dB,怎么调都不行。
原因通常是截止频率设错了。窗法设计的响应在截止频率处是 -6dB,从截止频率往通带方向走,衰减逐渐减小,但减小的速度取决于过渡带的陡峭程度。过渡带越窄,通带边缘离 -6dB 点越近,掉得越多。
解决办法有两个。一是把截止频率往阻带方向挪,让通带边缘离 -6dB 点远一些,代价是过渡带实际变窄,需要加阶数。二是直接加阶数,让整个过渡带变陡,同样的频率偏移对应更小的衰减。
还有一个隐蔽的情况:多级滤波器级联。单级设计时每级都刚好达标,级联之后通带边缘的衰减是累加的,两级各掉 0.05dB,加起来 0.1dB 就超标了。级联设计时每级都要留够余量,我一般按总指标除以级数再打个八折来分配。
5.3 多带、半带与多相结构里的窗选型
不是所有 FIR 都是简单的低通。多带滤波器、半带滤波器、多相抽取滤波器,窗的选择逻辑都不太一样。
多带滤波器(比如同时通过两个频段)用窗法设计时,各个带的指标是耦合的。如果两个带的阻带要求不一样,窗型要按最严的那个选,结果就是要求松的那个带浪费了阶数。这种情况我更倾向用等波纹设计,能独立分配各带的误差权重。
半带滤波器是个特例,它的系数有一半是零(除中心点外偶数位置全为零),运算量减半,非常适合 2 倍抽取。设计时通常直接用专门的半带设计函数,不用窗法。但如果非要手算,要注意半带滤波器对系数的对称性和零值分布有额外约束,加窗之后可能破坏这些约束,得手动清理。
多相抽取滤波器的每个支路都是原滤波器的子集,窗型选择跟原型滤波器一样,但要注意各支路的系数动态范围可能差别很大,定点化时不能共用一套缩放因子,否则小系数支路的量化噪声会很大。稳妥做法是每个支路单独评估,取最保守的那个缩放。
5.4 常见问题速查表
下面这张表是我这些年攒下来的排查清单,遇到问题先对一遍,能省不少时间。
| 现象 | 最可能的原因 | 排查动作 | 解决方式 |
|---|---|---|---|
| 阻带只到 -21dB 左右 | 用了矩形窗 | 检查 window 参数是否为默认 | 换成汉明或凯泽 |
| 阻带比预期浅 10dB 以上 | FFT 验证点数不足 | 把 worN 提到 2 的 16 次方以上 | 提高验证精度后重测 |
| 通带边缘衰减超标 | 截止频率设在通带边缘 | 检查 fc 是否在过渡带中点 | fc 向阻带方向偏移 |
| 截止频率整体偏一倍 | 归一化方式混用 | 查 nyq 与 fs 参数 | 统一到 fs 参数 |
| 高通滤波器直流不为零 | scale 默认归一化 | 检查 scale 参数 | 设 scale=False 后手动归一化 |
| 相位响应不是直线 | 量化破坏对称性 | 检查 h[n] 与 h[N-1-n] | 量化后强制镜像对称 |
| 实测阻带比仿真差 5dB | 系数量化位宽不足 | 核对 Q 格式与指标 | 升到 Q15 或 Q17 |
| 时域输出有直流偏移 | 系数和不为 1 或定点溢出 | 算 sum(h) 和累加器位宽 | 重新归一化、加宽累加器 |
| 级联后指标不达标 | 各级误差累加 | 单独测每级指标 | 按级数分配余量 |
| 过渡带比算的宽 20% | 公式定义不一致 | 核对 Δω 的定义 | 统一用角频率差重算 |
表里最后一条特别值得说。不同资料给的过渡带系数差异极大,我见过同一款窗在三个来源里分别标注 3.1、3.3、3.5 的。遇到这种情况,不要纠结哪个对,直接用仿真验证:按某个系数算一个长度,跑频响测实际过渡带,反推出真实系数,再按这个系数重算。一轮迭代就能校准到你自己的工具链上,比查十份资料都快。
我个人在实际操作中的体会是,窗函数选型这件事,理论公式只能给你一个起点,真正决定成败的是“算完就测、测完再调”这个循环跑得够不够快。我现在的习惯是把整个流程写成一个脚本,输入通带边缘、阻带起点、衰减要求三个数,自动算阶数、生成系数、跑频响、打印实测指标,不达标就自动加长重跑。这个脚本不到一百行,但省下来的时间比读十篇论文都多。另外提醒一句,别在项目后期才想起来优化滤波器长度,那时候架构已经定死,改一个参数牵动一大片,前期多花半天把指标和阶数算清楚,后面能少加很多班。