十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

CIC抽取滤波器与补偿FIR设计:原理、Verilog实现及Modelsim仿真排错

CIC抽取滤波器与补偿FIR设计:原理、Verilog实现及Modelsim仿真排错 写这篇博文前先交代个背景前阵子做一块高速数据采集的信号链路ADC输出直接进FPGA采样率不低后端急着需要降速第一反应当然是CIC抽取滤波器。CIC这东西结构清爽、不需要乘法器资源开销极小但真正联调仿真的时候才发现光有CIC还不够——抽取后的通带滚降像一把钝刀把有用信号的边缘砍得惨不忍睹。后面再补上一级补偿FIR波形才算是真正可用。整个过程踩了不少坑尤其是Modelsim里一仿真就出红线、偶尔还出现发散迹象今天把整条链路从原理到仿真好好捋一遍希望能帮到正在跟CIC较劲的朋友。1. 为什么CIC抽取滤波器必须配补偿级先把这笔滚降账算清楚1.1 CIC的级联结构与其频响特征CIC滤波器由积分器Integrator和梳状器Comb交替级联而成抽取型CIC的标准拓扑是N级积分器在前、D倍抽取开关居中、N级梳状器在后。讲原理的时候一般会给出这个传递函数H(z) [(1 - z^(-MD)) / (1 - z^(-1))]^N其中M是微分时延绝大多数工程场景直接取1N是级联数D是抽取倍数。这个公式看着复杂拆开看其实很直白分子梳状器负责在特定频率上制造零点分母积分器在零频处有极点两者相除得到的是一个经典的“近似sinc”响应主瓣低通、旁瓣快速衰减换来的优势是结构里没有一个乘法器全是加法器和寄存器级联在FPGA里占用的DSP资源几乎为零。但“零乘法器”不是白拿的。CIC的频响在主瓣内并不是平坦的越靠近通带边缘滚降越明显。以三级级联、抽取倍数D8为例在输出采样率fs_out的归一化频率f00.2处通带增益已经掉到约-1.7dB到f00.4处滚降达到-7.15dB。这个衰减放在工程里是非常可观的直接导致信号的高频分量严重变软波形失真肉眼可见。1.2 补偿前后的对比数据要理解补偿的必要性最直接的办法是拿数据说话。假设输入信号是一个跨通带的线性调频信号带宽一直延伸到这个CIC输出通带的边缘抽完以后你会在频谱仪或者仿真FFT里看到一条明显下坠的“坡”信号高频段几乎是被埋进底噪里。这时就需要在CIC后面再级联一个补偿FIR滤波器它的频响设计成正向sinc形状即与CIC滚降曲线相反在通带内“顶”回去让级联后的总响应恢复平坦。我实际设计用的参数是D8N3M1输入位宽16bit输出采样率从原始62.5MHz降到7.8125MHz。用MATLAB的fdesign.ciccomp工具一键生成补偿FIR系数设定通带边缘为输出采样率的40%阻带边缘为50%带内纹波0.1dB最终得到24阶对称FIR。补偿前的CIC在通带边缘附近滚降约7.2dB加上补偿级后总波动控制在±0.2dB以内这个效果对大多数基带信号处理链路都算合格。提示计算CIC滚降时容易把频率坐标搞混。CIC响应公式里的频率f是相对输入采样率归一化的而补偿滤波器工作在抽取后的采样率上换算时一定要把输出采样率对应回去否则补偿曲线会整体错位。1.3 为什么不用FIR直接替代CIC也有朋友问过既然CIC通带这么不平坦为什么不直接用FIR做抽取省掉补偿这级事答案是资源账划不来。CIC做D倍抽取时积分器和梳状器工作在输入高速时钟域但每一级都只是加法器而等效的FIR抽取滤波器需要先把整个抽取滤波器跑在高速时钟上然后再降采样抽头数一多乘法器数量立刻爆炸。以我上面这个D8的工程为例如果直接用FIR实现等效滤波至少需要50~60个抽头每个抽头一个乘法器DSP资源消耗是CIC加补偿FIR方案的十几倍。CIC砍掉高速侧的乘法器数量补偿FIR则工作在低速时钟域同样一组乘法器能覆盖更多数据样本两者配合才是成本最优解。2. Verilog写CIC抽取级积分器和梳状器的寄存器时序细节2.1 积分器级联的寄存器排布硬件代码里最核心的问题就是位宽。CIC积分器是无条件累加每级增加log2(D)位才能保证不溢出。三级级联、D8时每级增加3位输入16bit中间累加位宽至少25bit。很多新手在这里犯迷糊直接把中间寄存器设成16bit仿真波形一放大全是削顶失真还误以为是自己输入激励的问题。积分器的Verilog实现很直接三级积分器在输入时钟域下逐级累加reg signed [ACC_W-1:0] int1_r, int2_r, int3_r; always (posedge clk or negedge rst_n) begin if (!rst_n) begin int1_r 25sd0; int2_r 25sd0; int3_r 25sd0; end else begin int1_r int1_r din; int2_r int2_r int1_r; int3_r int3_r int2_r; end end这里有个容易被忽略的操作输入数据din是16bit有符号数与25bit累加器相加前必须先做符号扩展否则Verilog在运算时会把高位补零正数没问题负数直接算错。我在代码里用的是{{(ACC_W-DATA_W){din[DATA_W-1]}}, din}这类写法或者干脆声明一个wire型的signed扩展信号简单可靠。2.2 梳状器差分与抽取时钟的对齐梳状器工作在哪一侧是写RTL时最容易搞糊涂的地方。经典结构是积分器全速运行到抽取开关之后才进入梳状器。也就是说梳状器不需要每个输入时钟都计算而是每D个时钟采样一次做一阶差分。实现上有两种风格一种是让梳状器也跑全速时钟但是用使能信号控制寄存器更新另一种是直接用抽取后的低速时钟驱动梳状器。前者在FPGA时序上更好收敛因为单时钟域我推荐用使能方式。reg [2:0] cnt; wire decim_pulse (cnt 3d7); always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 3d0; end else begin cnt cnt 3d1; end end always (posedge clk or negedge rst_n) begin if (!rst_n) begin comb1_d 25sd0; comb1_r 25sd0; comb2_d 25sd0; comb2_r 25sd0; comb3_d 25sd0; comb3_r 25sd0; dout 16sd0; vld_out 1b0; end else begin vld_out 1b0; if (decim_pulse) begin comb1_d int3_r; comb1_r comb1_d; comb2_d comb1_r; comb2_r comb2_d; comb3_d comb2_r; comb3_r comb3_d; dout comb3_r[15:0]; vld_out 1b1; end end end注意最后一句话截位要从梳状器输出取高16位而不是低16位。CIC的DC增益等于D^ND8、N3时放大512倍所以有效数据在高位。取comb3_r[24:9]等同于除以512但直接拼接截断会引入直流偏置对无符号输入不明显对有符号数就可能造成负半轴不对称。稳妥做法是做带符号舍入先看被舍掉的低9位最高位即原第9位再加到高16位的最低位上。细化一点wire round_bit comb3_r[8]; wire [15:0] dout_raw comb3_r[24:9]; dout dout_raw {{15{1b0}}, round_bit};这样比直接截断多了0.5LSB的补偿长时间累加不会偏。别小看这个细节后级如果接的是真正的DAC直流偏置会在频谱中心留下一条可见的谱线。2.3 插值方向SINC补偿同样适用热词里还有CIC插值滤波器这里顺带说一句。插值型CIC是抽取型的镜像结构先是梳状器再是零值插入最后积分器。补偿思路一模一样只是补偿FIR放在插值之前工作在低速时钟域。所以这篇里讲的所有仿真验证方法、位宽计算和红线排查思路换成插值方向依然适用只需要把抽取使能换成插值使能。3. 补偿FIR滤波器设计从MATLAB系数到Verilog乘法器映射3.1 fdesign.ciccomp设计流程补偿滤波器设计我直接推荐MATLAB的fdesign.ciccomp对象几行代码就能得到经过优化后的逆sinc系数不推荐自己手写理想反sinc再加窗。原因是CIC滚降曲线本身就是个高次幂正弦函数手动近似误差大而fdesign内部用一种带权重的最小二乘算法在通带内逼近逆响应过渡带和阻带的行为也能单独约束。设计命令我实际用的参数如下clear; close all; D 8; % 抽取倍数 N 3; % CIC级数 M 1; % 微分延迟 Fs 7.8125e6; % 抽取后的输出采样率 Fpass 0.40 * Fs/2; % 通带边缘(对应归一化0.4) Fstop 0.50 * Fs/2; % 阻带边缘 % 24阶有符号系数, 阻带衰减要求80dB f fdesign.ciccomp(D, M, N, Fpass, Fstop, 24, 80); Hd design(f, equiripple); coef Hd.Numerator;这里有个设计边界必须注意CIC补偿只能在主瓣范围内进行当f0超过输出采样率的一半时CIC的频响已经跌入深层旁瓣任何线性滤波器都救不回来了。所以Fpass不能设太高一般取输出采样率的0.4~0.45比较保险再往上就是跟物理极限较劲补偿出来的系数会非常怪异还会引入高频噪声放大。3.2 系数定点化直接截断还是舍入MATLAB设计出来的是浮点系数直接扔给Verilog是跑不动的。以24阶FIR为例系数范围通常在±1.5左右我把它量化成18bit有符号定点数。具体做法是统一按Q17格式即1位符号位、17位小数位浮点系数乘以2^17后四舍五入取整然后检查量化前后的频响误差coef_q round(coef * 2^17); % 回读频响跟浮点版本对比 [Hf, f] freqz(coef, 1, 4096, Fs); [Hq, ~] freqz(coef_q/2^17, 1, 4096, Fs); plot(f, 20*log10(abs(Hf)), f, 20*log10(abs(Hq)));量化位数选择取决于通带纹波要求。18bit量化系数带内误差通常在0.01dB量级完全够用如果系数位宽降到12bit带内纹波可能恶化到0.1dB以上这时候总链路平坦度就被FIR拖累了。综合下来补偿FIR用18bit是性价比较好的取值。3.3 Verilog乘法器排布与对称复用24阶线性相位FIR是对称系数24个抽头可以复用为12个乘法器两个对称位置各取一个数据相加后共用一个系数。这个优化在FPGA里很关键因为补偿FIR虽然工作在低速时钟域但乘法器能省则省。reg signed [17:0] tap0, tap1, tap2, tap3; wire signed [17:0] sum_sym; assign sum_sym tap0 tap23; // 对称位置相加 // 每个时钟周期乘一个对称系数 wire signed [35:0] mul_out; assign mul_out sum_sym * coef_rom[0];滤波器数据链路的乘法结果累加是常见时序瓶颈。低速时钟域下全展开累加即可如果时钟频率高可以做多级加法树把一次累加拆成3~4级每级之间插寄存器换来更好的时序收敛。实际项目中我习惯在输入、乘法输出、累加输出各打一拍流水线延迟多一点无所谓稳定性最重要。4. 仿真环境搭建把Modelsim波形红线问题扼杀在Testbench里4.1 测试向量生成与参考模型对照仿真这一步最容易“一看波形全是红线”几乎每个初学者都会撞一次。先把测试平台构建思路理清。Testbench里我主要做两件事产生有意义的输入数据和生成一份可比的参考输出。输入信号我建议用定点正弦叠加一个小频偏的线性调频这样既能看到时域幅值变化又能用FFT观察频响补偿前后的差异。Verilog里产生正弦波可以用$sin系统函数但注意它返回的是实数写入寄存器前要手动定点化reg signed [15:0] din; real phase 0.0; real step 2.0 * 3.14159265 * 0.02; // 归一化频率0.02*fs always (posedge clk) begin phase phase step; din $signed($rtoi(32767.0 * $sin(phase))); end这里还有一个参考模型的做法把同一份CIC抽取加补偿滤波的算法在MATLAB里用浮点跑一遍再把结果导出成hex文本在Testbench里用$readmemh加载跟RTL仿真输出逐拍比对。这个双轨对照能极大提高调试效率比人肉盯波形可靠得多。比对时允许的误差设为1~2LSB因为定点截断和舍入顺序不同会造成轻微差别只要不超出容限就算通过。4.2 红线波形到底是哪来的Modelsim里信号显示成红色线含义很明确这个信号的值是X未知态或Z高阻态。最常见的三个原因按排查优先级排第一寄存器没有复位。CIC积分器和梳状器都必须在rst_n有效后清零如果Testbench里复位信号没拉低就释放或者复位只持续了一个时钟周期但中间寄存器复位不完整仿真起点就会带上X态。排查方法是看波形里红色是不是只集中在起始段后面恢复正常如果是这种情况十有八九是复位释放和首个有效数据之间的时序没对齐。第二输入端口没驱动。Testbench里只例化了DUT却忘了给din赋初值或者din是reg型但初值没有设置成0仿真一开始就会是X态。这个平时隐蔽但在进位链长长的CIC里X会沿着加法器一路传播最终所有输出都变成红色不看仿真日志很难定位。第三无符号与有符号混用。CIC中间寄存器一旦声明成无符号reg而输入数据带符号仿真时负数会以补码形式被无符号解释波形表面上看是满幅度翻转但FFT频谱完全对不上。这种不是红线但更容易误导人。给个通用排查清单我每次遇到红线就按这个顺序过一遍检查rst_n是否在t0时先拉低至少保持5个时钟周期再释放检查所有待测模块输入端口的驱动信号是否有初值检查DPI/文件读取类仿真里测试向量文件路径是否正确文件没读到数据读出的内容就是X检查有没有多个驱动源同时驱动同一信号Verilog里两个always对同一reg赋值会直接产生X态4.3 把“仿真发散”当成一个独立问题看热词里出现“仿真发散”很多人第一反应是算法不稳定。实际上在纯RTL级仿真里真正的数学发散极少见常见的是两种假象。一种是截位振荡中间寄存器的位宽不够累加器反复溢出又回绕产生貌似指数增长的尖峰波形看起来就像发散。另一种是响应时间不足CIC是极慢的滤波器群延迟很大仿真只跑了几百个周期就下结论说“输出不对”其实积分器还没建立稳定稳态输出远没到。处理发散类问题我的习惯是先做静态位宽审计。把每条数据通路的位宽增长逐级列出来16bit输入经过3级积分器增长到25bit在梳状器阶段至少保持25bit截位只允许发生在最后一级输出。中间任何一级擅自截位CIC在数学上的零极点抵消关系就会破坏轻则频响变形重则累加器不稳定。5. 仿真发散现象的根因与定点化处理5.1 发散现象在波形和FFT上的典型表现真碰到发散的时候波形上的特征是输出幅度随仿真时间线性或指数增长达到饱和后不再回落或者周期性地出现巨大毛刺。在Modelsim里你会看到幅值一路冲高然后被位宽截断“折叠”下来出现锯齿状包络。FFT上则表现为整个频谱噪声地板抬升尤其是低频段异常隆起。这种表现十有八九来自积分器溢出后回绕。CIC数学上允许回绕但前提是回绕不能导致信息丢失也就是说中间累加位宽必须大于理论最大值的跨距否则回绕之后的高位被丢弃等最后截位时数据已经残缺输出自然崩掉。我见过一个D16、N5的极端案例理论上至少需要20bit增长设计者只给了16bit中间位宽结果仿真出来的输出像一堆随机数怎么调都没用最后把中间位宽补到32bit才恢复正常。5.2 字长设计与寄存器饱和策略字长计算的公式要烂熟于心每级积分器增加log2(D)位N级总共增加N*log2(D)位再加上输入位宽。比如D8、N3、M1时增长位就是339位16bit输入对应25bit中间字长。如果M不等于1增长位是Nlog2(D*M)推导方式是每隔M个采样差分一次直流增益更大。在最终输出截位处有两种策略可选。一种是回绕截位只保留高OW位丢弃低AW-OW位这种做法在CIC链内部是被允许的因为只要中间位宽足够丢弃的是低阶冗余位不会影响高位信息。另一种是饱和截位在截位前判断符号和溢出位溢出时报正/负饱和。两种策略各有适用场景后级有AGC控制的系统饱和会带来非线性失真但能防止瞬时大信号把后级打坏后级没有AGC、全靠数字尺度管理的系统回绕截位加少量舍入更干净。我通常选择回绕截位加round因为FPGA里实现饱和判断要额外做符号扩展判断逻辑时序压力更大。5.3 一个完整的验证与回归流程防发散不能只靠仿真看波形我的流程是先用一个单频信号跑几百个输出周期确认输出幅度符合理论增益再用带内扫频信号跑一遍把RTL的输出文件导回MATLAB做功率谱跟浮点参考模型对比每根谱线的偏差最后用白噪声激励跑一次长仿真确认底噪没有异常隆起。三步都过了这个CIC补偿链路才算真正可信。白噪声激励这一步很多团队会省但恰恰是它最容易暴露定点化导致的杂散我建议无论如何都补上。6. 实测波形对比与性能数据补偿前与补偿后的差异6.1 时域波形和频域频谱的对照仿真环境搭好以后用前面提到的那组参数实际跑一遍。输入是一个归一化频率0.02fs的正弦波先经过三级CICD8抽取再进入24阶补偿FIR。在Modelsim里观察CIC裸输出会发现波形包络出现明显的“削角”现象幅度比输入小了一截这就是通带边缘滚降在时域上的投影。接入补偿滤波器后同一频率点的输出幅度恢复了约6.8dB大约从原来的-7.2dB抬到-0.3dB波形边缘重新变圆润。频域数据更有说服力。把CIC输出做1024点FFT和补偿后输出做同样点数FFT补偿前频点在归一化频率0.4处幅度已经明显坠下去补偿后同一条谱线被顶回平坦区与0频率处的参考点差不超过0.4dB。整个过程验证了补偿滤波器“逆sinc”设计的正确性也说明前面位宽和截位处理没有引入额外污染。6.2 资源占用与性能对比仿真通过后我顺手在Xilinx Artix-7上做了一次综合资源数据分享给大家参考模块LUTFFDSP48E工作时钟三级CIC抽取D8286212062.5MHz24阶补偿FIR412356127.8125MHz合计69856812-对比一下直接用FIR做D8抽取的等效方案大约需要56阶高速滤波器DSP48E至少占用56个LUT和FF也会多出三倍以上。CIC加补偿的组合在资源上是碾压级的。当然代价是补偿FIR的群延迟会比纯FIR方案大一些但对大多数通信基带和控制系统场景这个延迟完全在可接受范围。6.3 仿真到上板的最后一步检查仿真全部通过之后从上板实测的角度再提醒几个容易被遗漏的点。首先是时钟域CIC输入时钟域和补偿FIR输出时钟域如果在系统里实际上是同一个PLL的不同输出务必检查两个时钟是否同源同步否则抽取时刻抖动会在输出端表现为噪声。其次是复位释放时序FPGA上电后PLL锁定需要时间但CIC积分器复位不能跟着PLL locked信号走否则PLL锁定瞬间的毛刺会直接打入积分器造成首个样本错误。还有一点是我自己交过学费的地方补偿FIR的系数ROM初始化。Xilinx FPGA里ROM可以用分布式RAM或者BRAM但初始化文件.coe里系数格式和位宽必须跟RTL里声明的完全一致差一个bit上板后输出频谱就会多出一堆不该有的杂散。最直接的检查办法是在仿真阶段用$readmemh加载同一个系数文件确认RTL仿真输出和MATLAB参考一致再生成.coe做到文件级统一。最后分享一个调试技巧当CIC和补偿FIR链路级联后出现幅度对不上时不要急着怀疑滤波器设计先把两个级的vld_out时序拍出来看一下。CIC输出的valid脉冲和补偿FIR的输入握手是否对齐决定了数据链路上有没有样本被丢掉或重复。我做过不止一次这种问题排查最后发现都是使能信号晚了一拍导致整体幅值偏小跟滤波系数一点关系都没有。
返回列表