ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

CIC滤波器通带凹陷补偿:firceqrip invsinc设计与定点落地

CIC滤波器通带凹陷补偿:firceqrip invsinc设计与定点落地

数字前端链路里,CIC 滤波器几乎是免不了的一环:结构简单、不需要乘法器、抽取比可以做得很大,这些都是它的好。但它有个先天毛病——通带不平,越往通带边缘走衰减越重。我最近做的一版链路,CIC 抽完之后通带边缘掉了将近 3 dB,后面直接接信道滤波器,结果整个带内的幅频响应是斜的,测下来指标一直卡在那儿上不去。这个问题最后是靠一级 FIR 通带补偿解决的,用的就是 MATLAB 里firceqrip的invsinc通道。这篇就把整条路子完整走一遍:droop 是怎么算出来的、补偿目标曲线怎么定、firceqrip的参数怎么填、系数怎么定点落地、联调时踩了哪几个坑。适合正在做抽取/插值链路、被 CIC 通带凹陷困扰的同行参考,也适合刚接触多速率信号处理、想搞清楚"反 sinc 补偿"到底在补什么的朋友。

1. 通带边缘那 3 dB 是怎么掉的:CIC 幅频响应的固有凹陷

1.1 从积分-抽取-梳状的零极点抵消说起

CIC 滤波器的全称是级联积分梳状滤波器,它的结构本质上是 N 级积分器、一个抽取器、N 级梳状器串起来。单级的传递函数写成 Z 域是H(z) = (1 - z^(-R·M)) / (1 - z^(-1)),其中 R 是抽取因子,M 是差分延迟,通常取 1。这个式子的分子在单位圆上有 R·M 个等间隔零点,分母在 z=1 处有一个极点,两者恰好抵消,剩下的就是一个梳状的幅频响应。

从频域角度看,单级 CIC 的幅度响应可以写成|sin(π·f·R·M/fs)| / |R·M·sin(π·f/fs)|的形式,其中 fs 是抽取前的采样率。这个形状看起来像 sinc 函数,但严格说它是"离散 sinc"——分子分母都是正弦,原点处极限为 1,随着频率升高单调下降。N 级级联就是把这条曲线自乘 N 次,凹陷被放大 N 倍。

这里有个容易被忽略的点:很多人会把 CIC 的响应直接当成连续 sinc 来处理,用sinc(x) = sin(πx)/(πx)去近似。在小带宽、大抽取比的场景下这个近似误差不大,但当通带取到输出奈奎斯特的 40% 以上时,近似误差会开始显现,补偿曲线就会对不上。我后面所有的计算都用精确的离散形式,不用近似。

把频率归一化到抽取后的采样率,记x = f / fs_out,x 的取值范围是 0 到 0.5,那么 N 级 CIC 的归一化幅度响应是:

H(x) = [ sin(π·M·x) / (R·M·sin(π·x/R)) ]^N

这个式子在 x 趋近 0 时,分子约等于 π·M·x,分母里的 sin(π·x/R) 约等于 π·x/R,两者相乘正好也是 π·M·x,比值为 1,符合直流增益归一化的预期。这个形式比用绝对频率写起来清爽得多,后面所有的推导和代码都基于它。

1.2 把 droop 算出来:一组真实参数下的定量结果

光说"会掉"没用,得知道掉多少,才能决定补偿滤波器要多大力气。我拿手上正在调的一组参数做个完整演算:抽取前采样率 100 MHz,抽取因子 R=8,差分延迟 M=1,级数 N=5,抽取后采样率 12.5 MHz。

代入上面的公式,取几个关键频点算一下:

归一化频率 x(相对 fs_out)对应绝对频率CIC 幅度(dB)累计衰减
0.000 MHz0.000.00 dB
0.101.25 MHz-0.700.70 dB
0.202.50 MHz-2.852.85 dB
0.253.125 MHz-4.494.49 dB
0.303.75 MHz-6.526.52 dB
0.405.00 MHz-11.9411.94 dB
0.506.25 MHz-19.3119.31 dB

看这张表就能明白问题有多严重。如果我的有用信号带宽占输出奈奎斯特的 40%(也就是 x 从 0 到 0.2),边缘处就已经掉了 2.85 dB;如果带宽占到 60%,边缘掉 6.5 dB,这个量级已经不是"稍微不平",而是会直接影响后端解调门限的。

这里还有个经验值可以记一下:同样的 R 和 M,级数 N 每增加 1,通带边缘的 droop 大致按比例放大。N=4 时边缘 droop 约 2.28 dB,N=5 时 2.85 dB,N=6 时 3.42 dB,基本是线性关系(因为取对数之后是乘法变加法)。所以如果链路里 CIC 级数选得比较高(为了更好的混叠抑制),通带补偿的必要性就更强。

算 droop 这一步我强烈建议用脚本做,不要手算,也不要凭经验估。手算容易在归一化上出错,而一旦算错,补偿滤波器的目标曲线就整个偏了,后面怎么调都调不平。写个十来行的脚本,把整个通带扫一遍,输出最大值、最小值和平坦度,是最省事的做法。

1.3 为什么不能指望在 CIC 内部把曲线拉平

有人会想:既然 CIC 的通带不平,那能不能在 CIC 结构内部动手脚,比如给积分器加个反馈系数、或者调整梳状器的延迟?这条路我也试过,结论是不划算。

CIC 的核心优势就在于它没有乘法器——整个结构只有加法器、减法器和寄存器。一旦引入任何可调系数,乘法器就回来了,而且是在抽取前的高速侧工作,资源和功耗的代价非常大。更麻烦的是,CIC 的 droop 形状是 sinc 幂次的凸曲线,用简单的反馈结构根本拟合不出来,最多只能做点粗糙的预畸变。

另一条思路是在 CIC 前面做预加重,也就是在高速侧先把信号的高频部分抬起来,让 CIC 的凹陷正好抵消。听起来很巧妙,实际上更糟:预加重是在抽取前做的,那时候采样率还是 100 MHz,任何 FIR 都要在这个速率下跑,资源翻好几倍;而且预加重会把带外噪声一起抬起来,经过 CIC 抽取后混叠回带内,反而恶化信噪比。

所以业界的标准做法就是老老实实在 CIC 后面跟一级补偿 FIR。这一级工作在抽取后的低采样率上(本例 12.5 MHz),资源开销可以接受,而且它的目标响应非常明确——就是把1/|H_cic(x)|这条曲线拟合出来。补偿器的增益曲线只在整个通带内有几个 dB 的起伏,属于"小信号整形",用定点实现也很友好。

2. 补偿滤波器的目标曲线:1/|H_cic| 该怎么定边界

2.1 目标幅度响应的表达式与采样点

补偿滤波器的理想幅度响应就是 CIC 响应的倒数,把上面那个公式翻过来即可:

|H_comp(x)| = [ R·M·sin(π·x/R) / sin(π·M·x) ]^N

需要注意,这个式子在 x=0 处是 0/0 型,要用极限处理:x 趋近 0 时,整个式子趋近于 1。写代码的时候要么避开 x=0 这个点,要么显式地把第一个点置为 1。我第一次写的时候就忘了这一步,结果firls直接返回了一堆 NaN,查了半天才发现是原点除零。

另外,这个目标曲线是相对于"已经做过增益归一化的 CIC"定义的。也就是说,补偿滤波器的直流增益应该是 1,而不是 1/(R·M)^N。关于这个增益归一化放在哪一级,后面 2.3 小节专门讲,这里先记住结论。

目标响应的采样密度也有讲究。如果用firls或者自己写最小二乘,采样点太少会导致曲线拟合出现"折线感",尤其在通带边缘曲率最大的地方,采样稀疏会直接让拟合误差超标。我的做法是在 0 到通带边缘之间至少取 200 个点,并且在靠近边缘的 20% 区间内加密一倍。这个细节在文档里基本不会提,但实测下来对最终平坦度的影响挺明显的。

2.2 通带边界、过渡带与阶数的三方博弈

补偿滤波器不是凭空设计的,它要和前后的滤波器配合。这里有几个边界必须一起定:

通带边界 wo:这是补偿有效的范围。一般取最终有用信号带宽再留 10%~20% 的余量,因为后面的信道滤波器也有过渡带,两者要衔接上。本例里我把 wo 定在 x=0.2,也就是输出奈奎斯特的 40%。

过渡带起点:补偿滤波器在 wo 之外就不能再往上抬了,否则会把带外噪声放大。如果后面紧跟半带滤波器,过渡带可以放到 x=0.25 开始;如果补偿器是链路的最后一级,那过渡带必须收得更紧,从 x=0.22 就要开始往下压。

阶数 n:这是最直接的资源杠杆。用 Kaiser 的经验公式估算,等波纹 FIR 的阶数大致是n ≈ (A - 8) / (2.285 · Δω),其中 A 是阻带衰减(dB),Δω 是过渡带宽度(弧度,相对采样率)。本例中过渡带从 x=0.2 到 x=0.3,宽度 0.1,对应 Δω=0.628 rad。代入:

目标阻带衰减估算阶数实际取用延迟(@12.5 MHz)
40 dB222211 样本 / 0.88 μs
50 dB293015 样本 / 1.20 μs
60 dB363618 样本 / 1.44 μs
70 dB434422 样本 / 1.76 μs

我最后选了 30 阶。理由是这个链路的补偿器后面还跟着两级半带滤波器,本身就有 50 dB 以上的阻带抑制,补偿器的阻带不需要做得太狠,50 dB 足够;而 30 阶带来的 1.2 μs 延迟在整体延迟预算里还扛得住。

这里有个常被忽略的约束:firceqrip要求阶数 n 为偶数。原因是它设计的是第一类线性相位滤波器,长度 n+1 必须是奇数,这样系数才严格对称、中点系数不为零。如果你填了个奇数进去,一般会直接报错或者被自动调整,别以为是工具箱出问题了。

2.3 归一化放在哪一级:CIC 增益 2^15 的处理

这一节是我认为整个设计里最容易出错、也最值得单独拎出来讲的地方。

CIC 的直流增益是(R·M)^N。本例里 R=8、M=1、N=5,增益是 8^5 = 32768,正好是 2 的 15 次方。这个数字不是巧合——很多工程链路会刻意挑选 R 和 N,让(R·M)^N落在 2 的整数次幂附近,这样归一化只需要一个右移,不需要乘法器,也不引入舍入误差。

如果补偿滤波器把归一化也包进来,它的系数就得以 1/32768 为基准,通带增益在 3e-5 这个量级。用 16 位定点根本表示不了,必须用 32 位甚至浮点,完全失去工程意义。

所以正确的分工是:CIC 输出先做移位归一化,把直流增益拉回 1 附近;补偿 FIR 只负责几个 dB 的曲线整形,直流增益约等于 1。具体做法是在 CIC 的最后一级梳状器输出上右移 15 位。如果(R·M)^N不是 2 的幂(比如 R=12、N=4,增益 20736),那就得用乘法或者"移位相加"近似,这时候要么接受一点点增益误差,要么在补偿 FIR 的系数里顺手把这个常数吸收掉——因为补偿 FIR 本身就有乘法器,多乘一个常数不增加成本。

我见过一个挺典型的翻车案例:某版设计里 CIC 输出没做归一化,补偿 FIR 的系数又是按"直流增益 1"设计的,结果整条链路的增益是 32768 倍,一上电就饱和。查了两天才发现是两级之间的增益账没对上。所以这一步一定要在系统层面把增益链路画清楚,每一级的直流增益标出来,乘一遍看是不是 1。

3. firceqrip 的 invsinc 通道:参数填法与我踩过的偏差

3.1 firceqrip 的等波纹思路与 invsinc 选项的语义

firceqrip是 MATLAB 信号处理工具箱里一个比较老但很好用的函数,它设计的是等波纹线性相位 FIR 滤波器——通带内波纹幅度相等、阻带内波纹幅度也相等,本质上和firpm是同一类,但参数化方式不同:firpm用权重向量控制各频段的相对误差,firceqrip直接指定通带波纹 rp 和阻带衰减 rs,用起来更直观。

它的基本调用形式是b = firceqrip(n, wo, [rp rs]),n 是阶数,wo 是通带边缘(归一化到奈奎斯特频率,1 对应 fs/2),rp 和 rs 分别是通带波纹和阻带衰减。

关键在于它的第四个参数——滤波器类型选项。常见的有'high'(高通)、'stop'(带阻)、'passedge'(把 wo 解释为通带边缘而非截止点),以及本文要用的'invsinc'。'invsinc'的作用是:在低通包络的基础上,把通带内的幅度响应塑造成反 sinc 形状,也就是(sin(x)/x)^(-k)这种往上翘的曲线。这正好就是 CIC 补偿需要的形状。

参数形式上,'invsinc'后面跟的是一个描述形状的向量。我这边的工具箱版本里,写成[N, R*M]就能得到和1/|H_cic|几乎重合的结果——第一个元素对应反 sinc 的幂次(正好是 CIC 的级数 N),第二个元素对应 sinc 的形状尺度(对应 R·M)。

注意:不同版本的工具箱对这个向量的两位参数解释并不完全一致,有的版本把第二位的含义定义成倒数或者缩放因子。第一次用的时候,务必用fvtool或者freqz把设计结果的幅频响应和理论1/|H_cic|叠在一张图上比对,对不上就调第二个元素,试R*M和1/(R*M)两种,一般五分钟内就能确定。

这个"先叠图再定参"的习惯,比死记文档参数含义靠谱得多。我现在的做法是,任何一次补偿器设计,第一件事都是把理论曲线和设计曲线画在一起,肉眼扫一眼形状对不对,再做定量误差统计。

3.2 一次完整的设计脚本与结果核对

把前面的东西串起来,完整的设计脚本大概是这样:

% ---- 链路参数 ---- R = 8; % 抽取因子 M = 1; % 差分延迟 N = 5; % CIC 级数 fs_out = 12.5e6; % 抽取后采样率 % ---- 补偿滤波器参数 ---- n = 30; % 阶数,必须为偶数 wo = 0.4; % 通带边缘,归一化到 Nyquist(=0.2 相对 fs_out) rp = 0.02; % 通带波纹(dB) rs = 50; % 阻带衰减(dB) % ---- 用 firceqrip 的 invsinc 模式设计 ---- b = firceqrip(n, wo, [rp rs], 'invsinc', [N, R*M]); % ---- 理论目标曲线 ---- x = linspace(1e-6, 0.5, 1024); % 归一化到 fs_out Hcic = (sin(pi*M*x) ./ (R*M*sin(pi*x/R))).^N; Hideal = 1 ./ Hcic; % ---- 实际响应 ---- [Hd, w] = freqz(b, 1, 1024, 2); % w 归一化到 1 = Nyquist w = w / 2; % 换成相对 fs_out 的归一化 % ---- 误差统计(只在通带内)---- idx = w <= 0.2; err = 20*log10(abs(Hd(idx))) - 20*log10(Hideal(idx)); fprintf('通带内最大偏差 %.3f dB,最小 %.3f dB\n', max(err), min(err)); % ---- 级联验证 ---- Htot = abs(Hd) .* Hcic; fprintf('级联后通带边缘 %.3f dB\n', 20*log10(Htot(find(idx,1,'last'))));

跑出来的结果,通带内最大偏差在 ±0.05 dB 以内,级联后的通带边缘从原来的 -2.85 dB 拉回到 -0.02 dB 附近。这个精度对绝大多数通信链路都够了。

有一点要提醒:freqz的第四个参数设成 2,返回的w就归一化到 1 对应 Nyquist。而我在算理论曲线时用的x是归一化到采样率的(x=0.5 对应 Nyquist)。这两套归一化差了一倍,写脚本时必须手动除以 2,否则两条曲线会错开一半,看起来完全不重合。这个坑我在 5.1 里还会再展开说。

3.3 invsinc 对不上时的替代路线:firls 与 fir2

'invsinc'模式虽然方便,但它有个局限:它只保证通带内是等波纹的反 sinc 形状,对阻带的控制相对粗放,而且反 sinc 的幂次只能取整数(对应 CIC 级数)。如果遇到 CIC 级数是分数的情况(比如插值链路里的组合),或者需要更精细的通带/阻带权重分配,就得换方法。

路线一:firls最小二乘拟合。直接把目标幅度响应喂进去:

f = linspace(0, 0.5, 400); A = [1 ./ (sin(pi*M*f(2:end)) ./ (R*M*sin(pi*f(2:end)/R))).^N, zeros(1,100)]; ff = [f, linspace(0.5, 1, 100)]; b2 = firls(30, ff, A, [ones(1,400), 10*ones(1,100)]);

第二行末尾的10*ones(...)是阻带权重,把它调大能换来更深的阻带抑制,代价是通带拟合精度下降。这个权重调参的过程有点磨人,但换来的是对通带、阻带两边都能精确控制。

路线二:fir2频率采样加插值。对光滑的1/H曲线效果很好,因为反 sinc 曲线本身没有突变,频率采样加插值能很好地还原。我用 32 阶的fir2做过对比,通带拟合误差大概 0.08 dB,略差于firceqrip的 0.05 dB,但胜在参数直观、调试方便。

路线三:解析方法直接算系数。反 sinc 曲线可以用泰勒展开近似,把1/(sin(x)/x)展开成1 + x²/6 + 7x⁴/360 + ...的形式,然后对多项式做逆 Z 变换,得到近似系数。这个方法在算力受限、只能用很小的滤波器时有用,但阶数一高误差就上来了,现在基本不用。

我把三条路线的特点整理成表:

方法通带拟合精度阻带可控性参数直观度适用场景
firceqrip+ invsinc最好(±0.05 dB)中等高CIC 级数为整数、标准补偿
firls好(±0.1 dB)最好低(要调权重)需要精确控制阻带
fir2中(±0.1 dB)中等高快速原型、曲线光滑
解析近似差差中极低阶数、算力受限

实际工程里我的默认选择是firceqrip,只有在阻带指标特别严的时候才切到firls。

4. 系数落地:定点量化、对称折叠与延迟预算

4.1 系数位宽与累加器位宽的估算表

MATLAB 里算出来的系数是双精度,落到 FPGA 或 ASIC 里必须定点化。这一步的位宽选择有个常见的误区:很多人凭感觉给个 16 位,结果通带波纹一量化就反弹,回头怀疑是算法有问题。

正确的估算逻辑是这样的。系数位宽取决于两个因素:一是补偿滤波器通带内的最大增益(本例约 1.39,即 +2.85 dB,需要 1 位整数位),二是系数的最小有效值(决定了需要多少小数位才能保住精度)。我的经验是:

补偿增益(通带峰值)建议系数位宽量化后通带波纹恶化
< 1 dB12 bit< 0.01 dB
1 ~ 3 dB14 ~ 16 bit0.02 ~ 0.05 dB
3 ~ 6 dB16 ~ 18 bit0.05 ~ 0.10 dB
> 6 dB20 bit 以上明显,建议重新分配通带

本例补偿增益 2.85 dB,落在第二档,16 位系数(1 符号 + 1 整数 + 14 小数)够用。

累加器位宽的估算公式是:Acc ≥ B_coef + ceil(log2(L)) + 1,其中 L 是滤波器长度(n+1),加 1 是保护位。本例 B_coef=16,L=31,ceil(log2(31))=5,所以16+5+1=22,取 24 位留足余量。

再往上一层,如果 CIC 用的是位增长(bit growth)结构,累加器位宽 = 输入位宽 +ceil(log2((R·M)^N))。本例输入 12 位,增益 2^15,所以 CIC 内部要 27 位。这个数字直接决定了 CIC 那部分寄存器的规模,比补偿滤波器那边重要得多。

4.2 对称结构、乘法器数量与放置位置

由于firceqrip设计出的系数严格对称(第一类和第二类线性相位滤波器的特性),硬件实现时一定要用折叠结构:把b[k]和b[n-k]先加再乘,乘法器数量直接砍半。31 阶滤波器本来是 31 个乘法器,折叠后是 16 个,这个节省很实在。

放置位置也有讲究。补偿 FIR 应该放在CIC 抽取之后,也就是工作在低采样率侧。原因很直接:在 12.5 MHz 下跑 31 阶 FIR,每秒 31×12.5M = 387.5M 次乘加;如果放在抽取前的 100 MHz 侧,就是 3.1G 次乘加,差八倍。而且低采样率侧的时序余量宽松得多,布线压力小。

有人会问:放在抽取后会不会影响补偿效果?不会。因为 CIC 的 droop 是相对于输出采样率定义的幅频特性,抽取之后看到的频响就是那条 droop 曲线,在哪个速率上补都是一样的。唯一的区别是延迟——放在低速侧,同样阶数对应的绝对时间延迟反而更小。

还有一种情况是补偿器需要放在抽取前,那就是补偿带宽接近输入奈奎斯特、需要多相分解的时候。但那种场景很少见,一般意味着 CIC 抽取比选得太小了,应该先重新审视链路参数。

4.3 延迟与增益的一点账

线性相位 FIR 的群延迟是n/2个采样点。30 阶对应 15 个输出样本,在 12.5 MHz 下是 1.2 μs。这个数字要计入整条链路的延迟预算,尤其对闭环控制、实时同步这类应用,几个微秒都可能影响环路的稳定性。

增益账也要算清楚。补偿滤波器的直流增益应该精确等于 1(因为 CIC 已经归一化过了),但它的通带峰值增益是 1.39。这意味着如果输入信号是一个满幅的低频正弦,经过补偿器后峰值会超出满幅,定点实现时必须预留 1.5 dB 以上的余量,否则会在通带边缘削顶。这个余量要么在补偿器输出右移一位(代价是损失 6 dB 动态范围),要么在上游链路把信号压下来一点。

我一般会把补偿器的输出位宽做宽 2 位,保留小数部分,把右移放到后级滤波器里一起做,这样不会损失精度,也不会有削顶风险。

5. 联调阶段翻车的四种典型形态

5.1 补偿带宽只有预期的一半:wo 的归一化对象搞错了

这是最高频的错误,我自己也犯过。firceqrip的wo参数归一化到奈奎斯特频率,也就是 1 对应 fs/2。但我算理论曲线的时候,习惯用归一化到采样率的形式(0.5 对应 fs/2)。两套坐标差了整整一倍。

具体的翻车过程是这样的:我想让通带覆盖到输出采样率的 20%(x=0.2),于是填了wo=0.2。结果设计出来的补偿滤波器在 x=0.1 就开始往下掉了,补偿带宽只有预期的一半,通带边缘 0.2 处完全没被抬起。当时第一反应是"firceqrip 的 invsinc 参数填错了",调了半天第二个元素,越调越乱。

正确的填法是wo = 0.4(因为 0.2 相对采样率 = 0.4 相对奈奎斯特)。判断有没有搞错,最简单的办法是看补偿曲线的"拐点"位置对不对——理论上的 droop 曲线在哪个频率开始明显下弯,设计出来的补偿曲线就应该在同一个位置开始上翘,两者错开就是归一化错了。

排查路径我总结成三步:先画理论1/|H_cic|曲线,标出通带边缘;再画设计结果的响应,看它的"上翘终点"落在哪;两个位置对不上,先检查归一化,再看 invsinc 参数,最后才怀疑阶数不够。这个顺序很重要,因为前两个问题的概率远高于

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进