FPGA零乘法器CIC抽取滤波器:原理、Verilog实现与仿真验证全记录

发布时间:2026/10/22 11:39:30
FPGA零乘法器CIC抽取滤波器:原理、Verilog实现与仿真验证全记录 先说结论CIC抽取滤波器是FPGA数字信号处理里少见的“零乘法器”滤波器特别适合ADC过采样数据的抽取、数字下变频前端、以及音频采样率转换这类场景。这次我完整走了一遍“原理推导 → Verilog建模 → 仿真验证 → MATLAB Simulink建模仿真 → 结果交叉比对”的流程把中间遇到的位宽溢出、抽取时序、量化误差问题全部记录下来直接照着做就能复用。这个项目适合正在接触FPGA数字信号处理、需要实现数据抽取或者降采样功能的人也适合被各种滤波器系数量化搞到头大的同学。如果你只想调个现成IP核这篇文章也能帮你把后端参数、位宽配置这些东西弄清楚。1. 先搞清楚CIC抽取滤波器到底在解决什么问题1.1 抽取场景里的“笨办法”反而是最优解数据抽取其实就是把高采样率的数据按整数倍丢掉一部分只保留需要的点。比如ADC输出100MHz的采样流你只需要25MHz采样率的数据那每4个点取1个就行。看起来简单但直接抽点会出问题频带里混入了超出新采样率一半的高频分量这就是混叠。解决办法是先滤波再抽取把不需要的高频滤掉。CIC滤波器全称Cascade Integrator Comb级联积分梳状滤波器。它的典型结构是一串积分器加上一串梳状差分器中间夹一个抽取器。最核心的优势是没有乘法器只有加法、减法、寄存器这在FPGA里意味着极低的逻辑资源消耗和很高的吞吐能力尤其适合处理100MHz以上的高速数据流。代价是通带响应不够平直衰减也一般但作为抽取前端的抗混叠滤波器绰绰有余。1.2 为什么不直接用FIR非要用CICFIR滤波器性能确实好通带纹波小、线性相位但是多抽头、高系数的FIR在FPGA里要用大量DSP乘法单元。如果你只是做32倍、64倍抽取FIR的系数数量会非常可观而CIC只用R、M、N三个参数就能控制抽取倍数、延迟和滤波性能结构极其规整。我用过四级级联CIC做64倍抽取整套逻辑消耗只有几百个LUT这在资源紧张的工程里太重要了。CIC的劣势也明显通带内有频率倾斜droop。如果后续对信号平坦度要求高就得再接一个补偿滤波器一般用FIR。所以工程上常见的做法是“CIC粗滤抽取 FIR补偿整形”把高性能和低成本两个优势同时拿到。1.3 为什么要把Verilog仿真和Simulink仿真两条路都走一遍开发FPGA数字信号处理最怕RTL写完了才发现算法理解错了到时候在板子上调试非常痛苦。我的习惯是先用MATLAB/Simulink把算法模型跑一遍验证滤波效果、确认参数范围再写Verilog RTL让RTL仿真结果和Simulink结果对齐。把两条验证路径都做完整的好处有三个一是可以互相验证数学实现是否一致二是Simulink能快速扫描参数比如抽取值、阶数找到最佳设计点三是Verilog仿真能暴露位宽、时序、符号处理这些Simulink里根本看不到的问题。这套“算法模型左膀、RTL实现右臂”的验证方法实际项目中几乎是标准动作。2. 设计参数靠数学推不能拍脑袋乱定2.1 把传递函数拆成积分器加梳状器两级看CIC滤波器的系统函数长这样H(z) ((1 - z^(-R*M)) / (1 - z^(-1)))^N其中R是抽取倍数M是差分延迟通常取1或2N是级联阶数。这个式子看起来复杂但拆开就清楚了分子部分(1 - z^(-R*M))对应梳状滤波器分母部分1/(1 - z^(-1))^N对应N级积分器。整个CIC就是“积分器 → 抽取 → 梳状器”的结构。为什么积分器在前、梳状器在后因为积分器有反馈回路工作频率必须和输入数据率一致放在抽取之前它还能继续在每个输入时钟周期工作梳状器没有反馈放在抽取之后只处理抽取后的低速率数据。如果反过来梳状器工作在高速率下浪费资源而且积分器接收的是抽过的数据误差会很大。2.2 增益和输出位宽的计算这一节最容易被忽视CIC是直流增益很高的滤波器增益就是(R*M)^N。比如R8、M1、N3理论增益是512也就是输入信号幅度会被放大512倍。如果不知道这个增益直接接后面模块数据早就溢出了。所以工程上FIFO之后一般要么做右移截位要么做饱和截位把增益缩回去。位宽增长的估算公式是B_out B_in N * ceil(log2(R*M))举个例子输入16位有符号数R8、M1、N3log2(8)3所以内部位宽至少是16 33 25位。考虑到中间累加可能再要冗余我通常再留1到2位余量最终内部位宽设到26位或27位。注意Verilog里$clog2函数计算的是2的幂次数RM的结果要提前算清楚。2.3 CIC阶数和差分延迟选择的经验法则N1到2适合简单抽取保护通带比较宽混叠抑制差N3到4是常用折中抗混叠够了通带倾斜可以用补偿器救回来N5以上除非极端抑制需求否则不建议随便上因为位宽和资源都会涨而且通带倾斜会非常严重。差分延迟M的取值也影响响应形状。M1是最常用的主瓣宽度和旁瓣高度均衡。M2时旁瓣衰减更好但主瓣会变窄。对于窄带信号或者需要更强阻带衰减的场景可以用M2代价是要多做一些位宽预算。注意CIC级数N≠“积分器级数N级梳状器”实际结构里积分器有N级、梳状器也有N级级联阶数指的是同时级联的积分/梳状对不要混淆。3. Verilog写出来和仿起来是两回事3.1 先写一个单级CIC抽取核弄懂数据流下面是我写的单级CIC抽取器Verilog模块R8输入16位内部累加位宽24位。考虑到这是演示代码重点看结构多级扩展逻辑是在它基础上循环复制的。module cic_single #( parameter DIN_WIDTH 16, parameter ACC_WIDTH 24, parameter R 8 )( input wire clk, input wire rst_n, input wire ce_in, // 输入采样率时钟使能 input wire ce_dec, // 抽取后时钟使能 input wire signed [DIN_WIDTH-1:0] din, output wire signed [ACC_WIDTH-1:0] dout ); // 积分器每个输入时钟都累加一次 reg signed [ACC_WIDTH-1:0] acc; always (posedge clk or negedge rst_n) begin if (!rst_n) acc {ACC_WIDTH{1b0}}; else if (ce_in) acc acc din; end // 抽取寄存器抽取点的瞬间锁存积分器输出 reg signed [ACC_WIDTH-1:0] dec_reg; always (posedge clk or negedge rst_n) begin if (!rst_n) dec_reg {ACC_WIDTH{1b0}}; else if (ce_dec) dec_reg acc; end // 梳状差分器相邻两个抽取点做差 reg signed [ACC_WIDTH-1:0] comb_dly; always (posedge clk or negedge rst_n) begin if (!rst_n) comb_dly {ACC_WIDTH{1b0}}; else if (ce_dec) comb_dly dec_reg; end assign dout dec_reg - comb_dly; endmodule这段代码的关键点是积分器一直在跑ce_in是输入采样使能每个有效输入周期累加一次ce_dec则是抽取点的位置每R个输入周期来一个脉冲。comb_dly保存的是上一个抽取点的值和当前的dec_reg做差就是“积分器上连续两个抽取值之差”对应系统函数的差分部分。用这种方式写读代码的人能一下子理解CIC数据流的节奏。实际工程中ce_in、ce_dec这两个使能一般由一个采样率分频器产生也可以用AXI Stream的tvalid信号驱动。3.2 多级CIC的Verilog扩展看到位宽增长就对了三级级联的代码并不复杂本质上就是把积分器、梳状器各复制三份。我用generate批量生成这样参数N改起来很方便module cic_dec #( parameter DIN_WIDTH 16, parameter N 3, parameter R 8, parameter M 1, parameter ACC_WIDTH 26 )( input wire clk, input wire rst_n, input wire ce_in, input wire ce_dec, input wire signed [DIN_WIDTH-1:0] din, output wire signed [ACC_WIDTH-1:0] dout ); genvar i; wire signed [ACC_WIDTH-1:0] int_chain [0:N-1]; wire signed [ACC_WIDTH-1:0] comb_chain [0:N-1]; // 积分器级联 assign int_chain[0] int_data(0); generate for (i 0; i N; i i 1) begin : gen_int reg signed [ACC_WIDTH-1:0] acc_r; always (posedge clk or negedge rst_n) begin if (!rst_n) acc_r 0; else if (ce_in) begin if (i 0) acc_r acc_r din; else acc_r acc_r int_chain[i-1]; end end end endgenerate // 抽取寄存器、梳状器同样用generate批量展开 endmodule写多级代码时最容易犯的错误是忘记逐级位宽增长。我的做法是直接把ACC_WIDTH设足够大内部所有积分器、梳状器统一用ACC_WIDTH位省去每级截位。虽然有点浪费寄存器但仿真调试非常清楚——每一级的波形宽度一致出了异常一眼就能定位。等资源紧张需要优化时再逐级缩位也不迟。3.3 testbench里那些容易翻车的细节仿真必须包含完整的时钟和复位时序还有参考数据。我给CIC写testbench时习惯先把输入数据用MATLAB生成好保存成十六进制的txt文件再用$readmemh读进仿真这样后续和Simulink对比就有统一的数据源。module tb_cic_single; reg clk, rst_n; reg ce_in, ce_dec; reg signed [15:0] din; wire signed [23:0] dout; cic_single #( .DIN_WIDTH(16), .ACC_WIDTH(24), .R(8) ) uut (.*); integer file_id; reg [15:0] mem [0:2047]; initial begin clk 0; forever #5 clk ~clk; end initial begin $readmemh(input_data.txt, mem); rst_n 1b0; ce_in 1b0; ce_dec 1b0; #30 rst_n 1b1; repeat (2048) begin (posedge clk); ce_in 1b1; din mem[test_idx]; // 每8拍给一次抽取脉冲 ce_dec (test_idx % 8 7) ? 1b1 : 1b0; if (ce_dec) $fdisplay(file_id, %d, dout); test_idx test_idx 1; end $finish; end endmodule这里有个我踩过的坑如果ce_dec和ce_in在同一个时钟周期拉高抽取寄存器采样到的是积分器当前累加后的新值还是旧值这和代码里积分器用阻塞赋值还是非阻塞赋值、抽取寄存器在always块中的相对位置都有关系。为避免歧义我统一用非阻塞赋值并且让极性使能打一拍抽取寄存器直接锁存上一个时钟周期积分器已经稳定下来的结果。换句话说抽取点比累加点晚一个时钟周期采样这样时序余量更宽也不会出现读值不确定的情况。3.4 仿真波形怎么观察才能确认功能正确不要一上来就看输出值大小先把三个关键信号放进去观察acc、dec_reg、comb_dly。acc应该呈现单调递增/递减的阶梯状波形输入为正弦波时它大致是“积分后的曲线”幅度随直流漂移变化。dec_reg是每R个周期更新一次的采样值波形比acc稀疏但趋势一致。dout是dec_reg与comb_dly的差值输出波形此时应该非常接近低通滤波后的正弦波。观察dout时如果看到明显的直流偏置别慌CIC本身是低通结构对直流和低频信号增益很大直流分量会被放大是正常的。真正的异常是dout数据量比以前更大导致波形严重上下削顶那就是位宽不够。4. MATLAB/Simulink做真值表和参数扫描4.1 Simulink里搭CIC抽取模型只要四步我用的MATLAB版本自带DSP System Toolbox它提供了CIC Decimation模块可以直接配置R、M、N三个参数。搭建步骤如下新建Simulink模型从“DSP System Toolbox → Filtering → Multirate Filters”里拖出CIC Decimation。打开模块参数Decimation Factor设8Differential Delay设1Number of Sections设3Input/output bit widths按你的设计设好。信号源用Sine Wave加Quantizer模块量化成16位整数保证和Verilog输入格式一致。用Scope或者To Workspace模块把输入输出保存到工作区备查。这一步的坑在于Simulink默认的CIC滤波输出位宽计算和你的Verilog位宽可能对不上。Simulink有“FPGA-oriented bit growth”选项勾选后它会按公式自动配置内部位宽如果你不勾选输出位宽可能会按双精度浮点运算结果和定点RTL对不上。对比的时候一定要把输出量化方式改成Fixed-point。4.2 MATLAB脚本生成激励数据并读取仿真结果我在做交叉验证时用了一套脚本流程极其有效。先写一个MATLAB脚本生成输入txt和参考输出数据再让Verilog仿真读取输入txt、生成输出txt最后回到MATLAB里把两组输出画在同一个图上对比。% 生成16位整数正弦激励 fs 1e6; f0 20e3; N 2048; n 0:N-1; x round(2000 * sin(2*pi*f0/fs * n)); % 写入txtVerilog用$readmemh读取 fid fopen(input_data.txt, w); for i 1:N if x(i) 0 fprintf(fid, %04X\n, x(i) 65536); else fprintf(fid, %04X\n, x(i)); end end fclose(fid); % 读取Verilog仿真输出 y_verilog load(verilog_out.txt); y_matlab simout; % Simulink导出的结果 % 统一缩放并绘图 figure; plot(y_matlab(1:256), r-o); hold on; plot(y_verilog(1:256), b--x); legend(MATLAB/Simulink, Verilog仿真);输入数据格式这里有个细节Verilog的signed reg配合$readmemh读文件时文件里的16进制如果超过了32767会按有符号数解释吗实测下来不太稳定所以我干脆把负数映射成补码形式写入读取后仿真内部再用signed转换这样两边数据就对齐了。另外在读取使用16位十六进制值时要小心如果超过7FFF要自动减65536再转有符号这是MATLAB脚本里要补写的转换。4.3 怎么判断两路仿真结果是否“一致”对比两个仿真结果时你会发现波形形状很接近但幅度未必完全相等。原因很简单MATLAB/Simulink的CIC Decimation模块输出默认可能带内部缩放或者已经帮你做了增益归一化而Verilog输出的是原始增益放大结果。我的做法是先把Simulink的输出也按(R*M)^N的增益做一次“反缩放”也就是右移9位512再看误差。如果误差在量化步长允许范围内比如±1个LSB就认为两路结果一致。如果差很多优先检查三个地方输入量化是否都是16位有符号定点、Simulink是否开了FPGA定点模式、Verilog的位宽设置是否合理。5. 我在调试中踩过的一些坑5.1 位宽截断与符号扩展问题这个坑几乎是每个做CIC的新手必踩的。我在单级测试时为了省寄存器把ACC_WIDTH直接设成20位结果输出波形在信号幅度较大的地方出现削顶像是被无理截断了。原因是输入16位有符号数R8的单级增益是8输出峰值理论上需要19位16ceil(log2(8))19我用20位应该刚好够但忽略了输入本身可能是负满幅再加上多级累加的一点点余量瞬间溢出。解决方式很简单要么内部统一用理论位宽加2位红色余量要么在输出端做截位时加饱和逻辑。饱和逻辑的Verilog写法就是用比较器判断是否溢出如果溢出就把输出拉满幅值。这个处理对信号链路的健壮性非常重要尤其后面还有FIFO或者DSP模块的时候。5.2 抽取时钟与数据同步问题如果ce_dec和ce_in同时拉高抽取寄存器锁存的值可能是积分器正在更新的值也可能是一拍前的旧值这取决于代码里积分器和抽取寄存器的顺序关系、非阻塞赋值的时间戳。我在仿真里遇到过“输出偶尔多跳一个数”的奇怪问题排查半天发现是testbench的ce_dec逻辑写得太随意和积分器更新时钟同沿了。正确做法是让抽取寄存器的使能延后一拍。具体来说计数到R的最后一个周期时先让积分器累加完成下一个时钟边沿再做抽取寄存器锁存。这样时序上天然晚一拍但不会出现采样不确定。这个“晚一拍”的延迟是CIC标准结构允许的因为梳状部分本身有延迟不会影响最终功能。5.3 量化误差和仿真结果对不齐的问题有一回我把Verilog输出和Simulink输出对到小数点后结果怎么都对不齐差了一个固定偏移量。后来发现是Simulink的CIC Decimation模块里默认勾选了“Normalize Gain”它自动把输出除以增益了而我的Verilog是完全原始增益。把那个选项取消或者两边都统一摆到归一化尺度问题立刻消失。还有一个容易忽略的点是输入数据类型。Simulink里如果信号源是double类型再接Quantizer量化的舍入方式是四舍五入而Verilog仿真里如果直接截位就是向下取整。这两种舍入方式在正弦信号上的差异会导致波形幅值最大相差1个LSB但不是系统性大误差。对比时接受1个LSB以内的差值是工程上的合理容差。5.4 多级结构下差分延迟M理解错误第三级梳状器如果写成“两个连续抽取点差值”但M2时就不是相邻抽取点差而是相隔两个抽取点差。这个细小差别我一开始搞错了导致输出响应和Matlab参考曲线差异非常明显。记住差分延迟M代表的是梳状器延迟R*M个输入周期也就是M个抽取周期。Verilog实现时M2需要两个延迟寄存器串起来即延迟两个节拍再做差。5.5 资源占用和时序收敛的小经验虽然CIC没有乘法器但它的关键路径是积分器的加法链。数据率高的时候多级积分器串行加法可能会导致时序收敛困难。我的做法是给关键加法路径加流水寄存器每级积分器输出打一拍再进入下一级。这会带来固定延迟但换取的是更高时钟频率实测下来用这种方法在200MHz时钟下跑3级CIC抽取64倍毫无压力。6. 想进一步扩展这个设计的方向做完基础的CIC抽取滤波往后的路就很明显了。第一个方向是加补偿FIR滤波器把CIC通带的droop拉平这时候可以把补偿器系数用MATLAB设计好转成Verilog乘法器结构并跟原有的CIC输出级联。第二个方向是把抽取和插值都做出来形成完整的采样率转换模块可复用于变速调制解调器或音频接口设计。第三个方向是尝试多通道并行CIC结构。当输入数据率极高时单路逻辑跑不动就需要用时分复用或并行多相分解的思路实现CIC结构这是相对进阶的话题但有了单路CIC的基础理解会顺畅很多。我在实际项目里把上面这套CIC抽取核直接接到了一个DDC前端配合NCO混频器和后端FIR补偿滤波器整个链路下来效果很稳。CIC这种结构适合“简单直接”地解决问题但前提是前面把位宽、增益、时序这些基本功全部算清楚。希望这篇记录能帮你一步到位少走我踩过的那几条弯路。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询