资讯动态

CIC补偿滤波器Verilog实现与仿真:从参数计算到RTL验证

发布时间:2026/9/18 9:08:18 来源:尧图企业网站定制
做FPGA信号处理这些年我发现一个很有意思的现象很多同事拿到数字下变频或者抽取滤波的需求第一反应就是上FIR结果综合完一看乘法器资源吃紧时序还差点收不住。最后折腾一圈回过头来把第一级换成CIC滤波器问题立马缓解。这个免费的午餐指的是它在结构上完全不需要乘法器只用加法器和延时寄存器就能完成大倍率抽取或插值。但CIC也不是没有代价——它的通带不平坦带内容易出现明显的倾斜这就要在链路里加补偿滤波器。这篇文章我就以Verilog实现和仿真为主线把CIC补偿滤波器的参数设计、RTL实现、仿真验证从头到尾走一遍适合正在做通信基带、数据采集系统或者刚接触数字滤波的FPGA工程师参考。1. 为什么通信链路里总有CIC的身影先看清它的成本和收益1.1 CIC在信号链路里的真实位置CICCascaded Integrator-Comb级联积分梳状滤波器最常见的落位是ADC采样之后、常规FIR抽取滤波之前担任第一级抗混叠抽取器。以一个实际的基带接收链路为例ADC输出16bit、采样率1MHz解调需要的符号速率只有125kHz这意味着中间要完成8倍抽取。如果全部用FIR做8倍抽取按照过渡带要求阶数可能在128阶以上128个乘法器加上流水线寄存器面积和功耗都不小。而把第一级8倍抽取交给CIC后后续链路只需要处理125kHz的低速数据半带滤波器和成形滤波器都在低速率下工作资源占用成倍下降。这种高速率下用廉价滤波器先降速低速率下再用高精度滤波器整形的分层思路几乎贯穿所有数字中频接收机。CIC香在高速端FIR香在整形端两者是互补关系不存在谁完全替代谁。1.2 CIC的免费午餐到底免了什么费CIC结构由两部分组成积分器Integrator和梳状器Comb。积分器本质是一个累加器梳状器本质是一个延时相减器。整个滤波器在高速时钟下只需要做加法和减法没有任何乘法运算。这对FPGA来说非常友好LUT和寄存器就能堆出很高的数据吞吐率而且不占用DSP48乘法器资源。相比同功能的FIRCIC的资源开销往往能小一个数量级。但免费是有代价的主要体现在三方面增益随参数指数增长。级联N级、抽取因子R、微分延迟M直流增益是(R·M)^N。输入16bit、R8、M1、N3时增益就是512输出位宽必须相应扩展否则直接爆掉。通带不平坦。CIC幅频响应在通带内并不是平直的越靠近通带边缘衰减越大N越大衰减越严重。这部分倾斜必须由补偿滤波器拉平。阻带衰减有限。单级CIC的阻带衰减只有13dB左右需要靠增加级数来提升但级数增加又会加剧通带倾斜需要在两者之间找平衡。理解了这三点就能明白标题里为什么会有补偿二字——CIC和补偿滤波器从来是成对出现的单看CIC本身很多场景下指标是没法直接用的。2. 动手写代码之前先把R、M、N和位宽算清楚2.1 三个核心参数的选取逻辑CIC滤波器有三个关键参数抽取/插值因子R、微分延迟M、级数N。很多初学者上来就抄一段代码参数随便填仿真出来波形不对也不知道问题出在哪。实际上参数选取是有明确权衡的我来拆开说。抽取因子R由系统速率关系直接决定这个基本没有商量余地输入速率除以输出速率就是R。拿上面的例子1MHz输入、125kHz输出R就是8。微分延迟M的取值一般是1或2。M1时零点分布稀疏通带衰减相对小是大多数场景的默认选择M2时频率响应零点增加一倍对混叠的抑制更强但通带倾斜也更大通常只在需要额外抑制特定频段时才用。我个人的建议没有特殊要求一律M1。级数N直接影响阻带抑制和通带平坦度工程上3级最常见。N1时阻带特性很差N5以上实现代价和通带倾斜都变得难以接受。下面这张表列出了不同N下的典型特性方便参考级数N阻带衰减相对值通带倾斜程度典型应用场景1约13dB小对阻带要求很低的后级粗处理2约26dB中资源极度受限、带内要求宽松3约39dB较大多数通信基带抽取链路4-5约52-65dB很大需搭配强补偿滤波器使用2.2 位宽增长的计算这个不提前算仿真必炸CIC最容易被忽略的就是位宽。因为积分器是无限增长型结构如果位宽不够仿真中会看到输出波形突变甚至溢出翻转也就是很多人遇到的仿真发散现象。还有一种情况是波形整体趋势看着对但幅度明显不对这种往往是截断策略出了问题。我有位宽计算公式Bout Bin ceil(N · log2(R · M))代入例子Bin16、R8、M1、N3log2(8)3Bout16925位。也就是说整个CIC链路的积分器和梳状器中间节点至少要25位才能保证不溢出。还有一个概念容易混淆直流增益G(R·M)^N512与位宽扩展的9bit是同一个数量关系。增益是幅度上的倍数位宽是二进制上的位数2^9512两者一一对应。实际工程中我不会真的把25位全部输出给后级而是做合理截断。CIC输出的有效信息集中在高位低位主要是量化噪声和累计噪声通常截到18位或20位再配合补偿滤波器使用。截断时建议用四舍五入而不是直接丢弃避免产生直流偏置。FPGA里做四舍五入很简单加上0x2半LSB再右移代价极低。2.3 先肉眼看一下频率响应长什么样CIC的z域传递函数可以写成一个漂亮的闭合形式H(z) [(1 - z^(-R·M)) / (1 - z^(-1))]^N这个式子说明CIC本质上是一个长度为R·M的矩形窗卷积N次得到的滤波器的等效表达。理解这一点很多问题都好解释了矩形窗的频谱第一零点在ffs/(R·M)所以CIC天然在抽取后的混叠频带附近有陷波矩形窗卷积N次相当于频域sinc函数取N次幂所以级数越多阻带衰减越好同时通带顶部越往下塌直流增益等于R·M的N次方正好和位宽公式相互印证。看频率响应时我一般会先在MATLAB里用freqz画一下确认通带边缘衰减是否可接受再回到RTL动手。这一步能省掉大量仿真时间。3. Verilog RTL实现积分器链、梳状器链和时钟使能对齐3.1 整条链路的模块划分与数据流实现CIC抽取滤波器结构上是N级积分器串联然后做抽取再N级梳状器串联。这里有个关键点积分器工作在高速输入时钟域梳状器工作在抽取后的低速时钟域。实际工程中为了时钟域简单我通常让整个模块都跑输入时钟但梳状器用时钟使能控制只在抽取时刻进行一次运算和更新。以N3为例数据流如下输入din[15:0]依次经过积分器1、2、3在抽取时钟使能有效时锁存再经过梳状器1、2、3最终输出dout[24:0]。在多级串联结构中我建议把积分器和梳状器分别做成可参数化例化的子模块避免同一段代码复制三次也方便后续改成N4或N5。3.2 积分器子模块的RTL写法积分器就是一个带饱和约束的累加器。不考虑饱和的话代码非常简单module cic_integrator #( parameter DATA_WIDTH 25 )( input wire clk, input wire rst_n, input wire clk_en, input wire signed [DATA_WIDTH-1:0] din, output reg signed [DATA_WIDTH-1:0] dout ); always (posedge clk or negedge rst_n) begin if (!rst_n) begin dout {(DATA_WIDTH){1b0}}; end else if (clk_en) begin dout dout din; end end endmodule为什么积分器要用signed类型因为输入信号可能带有直流成分或者负半周如果把有符号数当成无符号数处理累加结果会完全错误。这一点在仿真波形上特别容易看出来一旦类型用错负半周会变成很大的正数输出波形直接乱掉。clk_en信号在积分器里通常一直有效因为积分器工作在输入速率每个时钟都要累加。3.3 梳状器子模块与抽取使能的配合梳状器要复杂一点它包含一个延时链和一个减法器。差分延迟为M时需要M拍延时。抽取后的每个有效时刻当前输入减去M拍前的输入得到输出。为了节省寄存器工程上常利用抽取机制把延时转移到低速侧。本文为了逻辑清晰直接在梳状器内部做延时module cic_comb #( parameter DATA_WIDTH 25, parameter DIFF_DELAY 1 )( input wire clk, input wire rst_n, input wire clk_en, // 抽取后的有效使能 input wire signed [DATA_WIDTH-1:0] din, output reg signed [DATA_WIDTH-1:0] dout ); reg signed [DATA_WIDTH-1:0] delay_line [DIFF_DELAY-1:0]; integer i; always (posedge clk or negedge rst_n) begin if (!rst_n) begin for (i 0; i DIFF_DELAY; i i 1) delay_line[i] {(DATA_WIDTH){1b0}}; dout {(DATA_WIDTH){1b0}}; end else if (clk_en) begin dout din - delay_line[DIFF_DELAY-1]; for (i DIFF_DELAY-1; i 0; i i - 1) delay_line[i] delay_line[i-1]; delay_line[0] din; end end endmodule这里有个容易踩坑的点梳状器必须在clk_en有效时才更新延时链。如果写成每个时钟都更新抽取后的符号率就对不上仿真里会出现波形毛刺和周期错乱。我早期就在这个细节上栽过跟头当时怎么看波形都不对最后逐拍对照才发现是延时链被高速时钟刷新了。3.4 顶层例化和截断策略顶层要做的事是例化N个积分器、生成抽取使能、例化N个梳状器、完成位宽截断。抽取使能实际上是一个分频计数器的输出reg [3:0] cnt; reg clk_en_div; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 4d0; clk_en_div 1b0; end else if (cnt R - 1) begin cnt 4d0; clk_en_div 1b1; end else begin cnt cnt 1b1; clk_en_div 1b0; end end完整例化代码就不全文贴了核心是每一级的位宽传入要一致。我习惯让整个CIC中间节点统一使用Bout25位最后输出再截断。输出截断用signed类型的算术右移并加半LSB做四舍五入wire signed [24:0] cic_raw; wire signed [17:0] cic_out; assign cic_out (cic_raw 18sd128) 7; // 25bit - 18bit为什么是右移7位25位比18位多出7位直接丢弃会引入最大1个LSB的量化误差并带直流偏置加上0x80128即7位空间的一半再做算术右移量化误差变成均匀分布在±0.5LSB内直流偏置基本消除。4. 仿真演示从Testbench搭建到波形检查再到排查红线和发散4.1 Testbench结构与激励生成CIC滤波器的仿真核心目的是验证三件事抽取时序是否正确、增益是否等于理论值、频域响应是否符合预期。为此Testbench需要产生一个已知频率的正弦波输入并记录CIC输出到文本文件供后用脚本分析。正弦激励可以用查找表生成也可以在仿真里直接调用$sin系统函数。后者更方便但要注意仿真时间步长必须足够小否则生成的正弦波本身就有台阶失真。我习惯把测试信号频率设在通带内比如输入采样率1MHz、信号频率20kHz时initial begin clk 0; rst_n 0; #100; rst_n 1; end always #500 clk ~clk; // 1MHz时钟 integer fid; initial begin fid $fopen(cic_out.txt, w); end always (posedge clk) begin if (rst_n) begin test_signal $signed(16d20000 * $sin(2 * 3.14159 * 20000.0 * $realtime * 1e-9)); // 也可以直接用计数器和查找表 end end always (posedge clk) begin if (clk_en_div rst_n) begin $fwrite(fid, %d\n, cic_out); end end注意这里$realtime*1e-9是把仿真时间从纳秒换算成秒。如果用查找表驱动记得初始化用readmemh或initial块赋值否则仿真一开始是未知态波形就是红线。4.2 波形检查的关键节点仿真跑完先不要急着看频谱按顺序检查这几个关键节点积分器各级输出在仿真波形里确认它们是连续累加的有符号值不存在突然翻转。clk_en_div信号确认每个周期高电平只持续一个时钟间隔正好是8拍。梳状器第一级输出这个信号只在clk_en_div有效时变化其他时间保持。最终输出应该是幅度稳定的正弦波频率仍是20kHz但采样率从1MHz降到了125kHz一个信号周期内大约有6.25个输出点。如果输出波形看起来像一个周期只有两三个点那是抽取比例算错了如果输出带有明显锯齿多半是截断位宽不够或者四舍五入逻辑写错。4.3 仿真波形是红线问题出在哪很多人问过ModelSim波形显示红线的问题这个在CIC仿真里尤其常见。红线本质是信号处于高阻态Z或者未知态X最常见的原因有三个复位没做初始化。积分器里的dout是reg类型仿真一开始如果不复位初始值就是X而累加器dout dout din会把X一直传播下去。例化时端口位宽不匹配。比如顶层声明24位子模块参数是25位仿真器高位补0看起来波形总是少半格严重时直接出现不定态。$fopen文件路径问题导致输出全X。这个比较隐蔽其实信号本身没有X是波形查看器的数据源出了问题。排查思路是先看复位之后第一个有效时钟沿所有reg是否都已经变成确定值再逐级跟踪积分器1的输出如果第一级就不是确定值问题在输入或复位如果第一级正常、第二级开始出现X大概率是位宽或类型不匹配。二进制按位X传播的路径是定位这套问题最有效的手段。4.4 仿真看起来发散了先别怀疑算法CIC仿真里还有一种现象输出波形整体趋势是正确的但幅度越来越大直到顶到边界然后掉下来看起来像发散振荡。这不是反馈回路不稳定而是整数溢出导致的翻转回绕。我在做位宽验证时曾故意把积分器位宽缩小到20位输入一个幅度到了3000的正弦仿真到一定时间后波形尾部急剧异常。逐个积分器看去第三个积分器的值在某拍已经超过2^20之后全部取模翻转。解决办法就是按第2节的公式把位宽留够再仿。一个经验是仿真时宁可把中间节点位宽设得比理论值大2bit先把功能跑通再根据峰值实际大小砍位宽这样调试速度快很多。5. CIC通带下垂的补偿设计与联合仿真验证5.1 先量化看看通带到底掉了多少CIC在通带边缘的衰减是客观存在的我们来算一个具体数值。对于抽取因子R8、M1、N3的CIC归一化幅频响应公式|H(f)| / G | sin(π · R · M · f / fs) / (R · M · sin(π · f / fs)) |^N带上具体参数在几个关键频点算一下信号频率 f / fs归一化增益衰减dB0 (直流)101/1280.995-0.041/640.981-0.171/320.925-0.683/1280.838-1.541/16 (即fs/(2R))0.640-3.88如果信号只占抽取后奈奎斯特带宽的一半比如f1/32 fs处只掉了0.68dB很多场景可以忍但如果信号一直延伸到f1/16 fs附近3.88dB的倾斜就非常明显了必须靠补偿滤波器拉平。5.2 补偿滤波器设计思路反sinc曲线既然CIC幅频响应对应的是幂次sinc形状补偿滤波器最直观的设计思路是做一条反sinc曲线。工程上两种做法直接计算理想补偿响应用MATLAB的fir2或自行构造频率采样点生成FIR系数用fdatool导入CIC幅频响应作为期望补偿曲线再反推出等波纹FIR。常用补偿滤波器的系数数量取决于通带范围和波动要求。文中这个例子通带内做到±0.1dB大约需要31到63阶的FIR。这个阶数是在抽取后的125kHz低速域下运行的31个乘法器并不算多完全可以接受。补偿后的联合链路是CIC抽取滤波器 - 截断到18bit - FIR补偿滤波器 - 输出16bit。这里有一个重要细节补偿滤波器的输入位宽应该和CIC截断后的位宽一致不要让25位直接塞进FIR否则内部乘法器位数膨胀资源白涨。5.3 联合仿真怎么验证补偿到位联合仿真时Testbench除了记录CIC原始输出还要记录补偿后的FIR输出。然后统一导入MATLAB做FFT观察通带内频响是否平坦。我在实际验证中习惯这么操作把CIC输出保存为cic_out.txt把补偿后输出保存为comp_out.txt在MATLAB里分别读入截掉前几百个点让滤波器进入稳态对两个序列分别做FFT画出归一化幅度对比信号频率处的幅度CIC原始输出会明显低于低频处的幅度而补偿后应该基本拉平。注意一个容易搞混的点补偿滤波器的DSP计算是在慢时钟域即125kHz下工作但为了Testbench简单也可以和CIC一样都用1MHz时钟加clk_en控制。如果补偿FIR没有做时钟使能直接每个时钟都启动MAC运算输出速率会变快跟CIC输出的对齐关系全部错乱频谱对比就会完全失真。这个坑我见过好几个同事踩过。5.4 补偿滤波器在RTL里的实现提示补偿FIR系数是有符号小数需要在RTL里做定点化。定点化策略直接决定补偿精度。系数先用浮点计算好比如一组31个系数然后统一乘上2^13并四舍五入取整。输入18bit系数13bitMAC累加器需要1813536bit留出余量log2(31)≈5输出时再截回16bit。为什么通常不直接用浮点或者Q格式因为FPGA里DSP48本身是定点的选择2的整数次幂放大系数后续截断就是简单移位硬件友好。事实上在补偿滤波器设计里更重要的是系数定标的一致性而不是追求系数数值的绝对精度。RTL实现时如果资源宽裕最简单的办法是直接用乘法器做对称FIR如果资源紧张可以只存一半系数利用对称性先加后乘这样乘法器数量直接减半。多相滤波器结构也能做但这是后话。6. 从这次仿真里沉淀下来的一些实操经验最后分享几个实际调试中的经验都是踩过坑之后才真正记住的。第一个经验CIC的参数一定要先算后写。R、M、N和位宽这四件事在前端设计阶段花半小时定下来比仿真阶段花三天排查溢出要划算得多。特别是Bout公式建议贴在工程注释里省得下次又来推导。第二个经验仿真验证一定要导出数据做频域分析不要只盯波形。时域波形只能看出有没有看不出对不对CIC的通带倾斜和补偿效果只有FFT拉出来才能定量判断。我建工程时习惯固定留一个$fwrite出口无论哪个模块测试完都能落盘这是保证仿真可复现的好习惯。第三个经验CIC和补偿FIR最好放在一起仿。只仿CIC会觉得通带倾斜无所谓只仿补偿FIR又不知道实际级联后的效果。联合仿真一次到位能同时验证抽取时序、截断误差和补偿平坦度效率是最高的。第四个经验遇到频谱异常先查时钟使能再查位宽最后查系数。这个排查顺序能覆盖绝大多数数字滤波仿真问题。跳过使能直接查系数往往绕远路。CIC补偿滤波器这个组合是数字信号处理里一块很实用的拼图掌握它之后很多速率转换场景都能快速拆解成CIC降速FIR整形的标准套路。这次的仿真演示只是个起点后面如果碰到更大倍率的抽取、多相结构的补偿、或者插值方向的CIC都可以在这个基础上扩展。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价