Verilog级CIC补偿滤波器仿真演示:从原理到仿真的完整实战
很多做数字中频、软件无线电或者音频处理的FPGA工程师,早晚都会撞上CIC滤波器这道坎。CIC(Cascaded Integrator-Comb,级联积分梳状)滤波器在抽取和内插场景里简直是神器,不需要乘法器,纯加法器和延迟单元就能实现,资源省得感人。但它的通带衰减(droop)也让人头疼,尤其是抽取倍数大了以后,信号高频分量被压得明显变形。这时候就需要补偿滤波器(CIC Compensation Filter)来把通带拉平。我这次就把一个完整的“Verilog级CIC补偿滤波器仿真演示”项目拆开来讲,从数学原理到Verilog实现,再到Modelsim/Vivado仿真验证,把每一步背后的为什么都说清楚。这篇文章既适合刚接触FPGA数字信号处理的同学做入门参考,也适合老工程师快速回顾关键细节。
先说清楚这个项目能干什么:输入一个采样率较高的信号,经过CIC抽取后,再用一个补偿FIR滤波器纠正CIC引入的通带失真。整个过程用Verilog实现,在仿真环境里跑通,直接观察波形和频谱结果。我用的参数是抽取因子R=8、差分延迟M=1、CIC级数N=3,补偿滤波器采用31阶FIR。下面我会完整演示从滤波器系数设计到RTL编码、再到仿真的全链路,包括我踩过的坑和排查思路。
1. 整体设计与思路拆解:为什么需要CIC,又为什么需要补偿
1.1 CIC滤波器的结构与核心特性
CIC滤波器之所以在抽取/内插场景里被广泛使用,因为它是个“穷人的低通滤波器”。它由两部分组成:积分器(Integrator)和梳状器(Comb)。积分器的传递函数是 H_I(z) = 1 / (1 - z^-1),梳状器的传递函数是 H_C(z) = 1 - z^(-M)。把N级积分器和N级梳状器级联起来,整体传递函数就是:
H(z) = [(1 - z^(-M)) / (1 - z^-1)]^N = [求和_{k=0}^{M-1} z^(-k)]^N
当M=1时,其实就是N个移动平均滤波器的级联。这个结构最漂亮的地方在于:它只有加法和延迟,没有乘法器。在FPGA里面,乘法的代价远高于加法,所以CIC特别适合处理高数据率的场景,比如ADC前端采样率100MHz以上的信号抽取。
但是天下没有免费的午餐。CIC的幅度响应有一个明显的问题:
|H(e^jw)| = |sin(wM/2) / sin(w/2)|^N
这个响应在通带内不是平的,而是随着频率上升呈现下垂(droop)。N越大、M越大,下垂越剧烈。举个例子,R=8、M=2、N=3时,通带边缘的信号增益可能衰减好几个dB。对于通信系统来说,这个衰减会导致信号失真,尤其是宽带信号,星座图会明显收缩。
1.2 抽取系统为什么要放在CIC后面加补偿
要理解补偿滤波器的作用,得先搞清楚整个抽取链路的信号流。典型的CIC抽取器结构是:CIC滤波器先做抗混叠滤波,然后抽取,抽取后再接补偿滤波器。问题来了:补偿滤波器放在抽取前还是抽取后?
放在抽取前,意味着补偿滤波器工作在高速率时钟下,实现代价高;但好处是算法简单,可以直接按照输入频谱设计。放在抽取后,工作时钟降低了R倍,资源更省,但补偿滤波器的频响需要进行频率轴缩放。实际工程中,绝大多数设计把补偿滤波器放在抽取之后,因为FPGA里的时钟频率往往吃紧,把高开销的FIR放到低速率端是常规做法。
频率轴缩放怎么理解?假设原始信号在抽取前,频率范围是0到Fs/2。抽取R倍后,输出端的频率范围仍然是0到Fs_out/2 = Fs/(2R)。CIC的通带下垂在归一化频率轴上是固定的,但在抽取后的频率轴上,信号频谱被“展宽”了R倍。所以补偿滤波器设计时,要把CIC在[0, 0.5/R]区间内的响应反转过来。
1.3 参数选择的考量:R、M、N到底怎么定
这个项目的核心参数我定了R=8、M=1、N=3,这里说明一下我的选择逻辑。
抽取因子R=8:这是一个比较典型的中间值,既能体现抽取的过程,又不会让仿真数据量爆炸。R再大(比如64),仿真时间会很长,且补偿滤波器的频响设计要特别小心;R再小(比如2),CIC的优势体现不明显。
差分延迟M=1:M=1时CIC的频率响应是sin(x)/x形状(严格说是sin(wR/2)/sin(w/2)的N次方),实现最简单,延迟单元最少。M=2可以增加零点数量和混叠抑制能力,但通带下垂更严重。作为教学演示,M=1最合适,计算也直观。
级数N=3:N越大阻带衰减越大,但通带下垂也越厉害,而且寄存器位宽增长更快。N=3时阻带衰减大约在30-40dB量级,足够演示效果,同时补偿FIR的阶数不用太高。如果用N=5或更高,补偿滤波器的抽头数可能要到60以上才能拉平通带,仿真和实现都要更费劲。
位宽增长的问题这里也提一下。CIC滤波器内部寄存器的位宽增长遵循公式:
B_out = ceil(N * log2(R * M)) + B_in
我输入用16bit数据,R=8、M=1、N=3,那 B_out = ceil(3 * log2(8)) + 16 = 9 + 16 = 25,也就是内部最大位宽25bit。如果你把位宽设小了,仿真波形就会看到溢出产生的非线性失真,这个后面在问题排查里我会详细说。
2. 补偿滤波器系数设计与预验证
2.1 基于Matlab的系数求解过程
知道了CIC的频响,补偿滤波器设计理论上就是求它的逆响应。工程上最常用的方法是利用fdatool或者Designer,选择“CIC Compensator”作为响应类型,输入CIC参数,然后让工具自动生成系数。
我用的是代码方式,因为代码可复现性更好。核心思路是:先用freqz算出CIC在目标频段的响应,再反向设计一个FIR滤波器逼近它的倒数。
% 参数定义 R = 8; M = 1; N = 3; Fs = 1; % 归一化频率 % CIC频率响应 [hic, w] = freqz(1, 1, 8192, Fs); % 占位,实际用CIC级联模型 % 实际CIC频响直接用公式计算更准确 cic_freq = (sin(w * M / 2) ./ sin(w / 2)).^N; % 修正直流附近的除零问题 cic_freq(isnan(cic_freq)) = 1; % 目标:补偿CIC在[0, 0.5/R]范围内的衰减 % 注意频率轴范围:抽取后关心的信号带宽范围 passband_edge = 0.5 / R; % 设计补偿FIR,使用最小二乘法 comp_order = 30; f = [0, passband_edge, passband_edge * 1.2, 0.5]; a = [1.0 / max(cic_freq(1:round(passband_edge*8192))), ... 1.0 / min(cic_freq(1:round(passband_edge*8192))), ... 0, 0]; % 更规范的方法是用firls或firpm comp_coeff = firls(comp_order, [0 1], [1 0], 'hilbert'); % 这个不对,先占位都说到这了,坦诚讲,这段代码我故意留了不严谨的地方,因为在真实项目里我不会用这种“反向求逆”的方式去手算补偿系数。最稳的做法是直接在MATLAB的Filter Designer里选“CIC Compensator”:
% 推荐方式:使用fdesign cic_comp = fdesign.ciccomp(M, N, R, ... 'Fp,Ast,Ap', 0.4, 60, 0.1); % 通带频率、阻带衰减、通带纹波 comp_filter = design(cic_comp, 'equiripple'); coeffs = comp_filter.Numerator;这样设计出来的补偿滤波器,在通带内能精准抵消CIC的下垂,阻带衰减也满足要求。我的经验是,通带范围不要卡到理论边界0.5/R,要留出10%-20%的余量,否则边界处补偿效果会剧烈恶化,仿真里会出现“振铃”式的过冲。
2.2 系数量化与性能评估
设计出来的浮点系数不能直接用进FPGA,需要量化成定点数。这里有个经典矛盾:系数位宽越大,滤波器频响越接近理想值,但乘法器资源也越多。我用的补偿滤波器是31阶FIR,系数位宽18bit,输出截位到16bit。
量化后一定要对比浮点和定点的幅频响应:
quant_coeff = round(coeffs / max(abs(coeffs)) * (2^17 - 1)); quant_coeff = quant_coeff / (2^17 - 1); [h_float, f] = freqz(coeffs, 1, 8192, Fs); [h_fixed, ~] = freqz(quant_coeff, 1, 8192, Fs); plot(f, 20*log10(abs(h_float ./ h_fixed)));如果量化后通带纹波超过0.1dB,就得加宽系数位宽,这是我在实际项目里反复验证过的经验:18bit系数对31阶滤波器足够了,但如果补偿滤波器阶数到了64阶以上,系数位宽最好升到20位。另外要注意数据位宽和系数位宽的乘积,做乘法时结果位宽是两个位宽之和,后续截位时要考虑好舍入方式。我一般用“先加偏置再截位”而不是直接截断,能显著降低直流偏移。
2.3 仿真前的理想结果参考
设计完系数,先用MATLAB做一次全链路的浮点仿真,得到“理想输出”,这个结果后面就是RTL仿真的对照基准。我生成一个含多音信号的激励源(比如300Hz、1.2kHz两个正弦叠加,采样率8kHz),经过CIC抽取和补偿滤波后,理论上能看到两个谱峰幅度差异很小。
这段预验证非常关键。如果浮点结果都不对,RTL仿真必然错,先排查算法层面。我见过太多人直接跳进Verilog编码,结果最后波形不对,排查半天发现是MATLAB系数本身就有问题。
3. Verilog实现:CIC抽取器与补偿FIR的RTL编码
3.1 工程整体结构与模块划分
整个工程我分成四个模块:cic_decimator负责CIC抽取,comp_fir负责补偿滤波,clk_div或直接用使能信号解决抽取时钟,顶层cic_comp_top做例化连接。另外写了一个tb_cic_comp测试平台。
模块划分的原则是单一职责。CIC和FIR不要混在一个模块里写,否则调试时波形追踪很痛苦。我习惯在每个模块的接口上把输入输出数据位宽写清楚,并在RTL注释里标注对应的位宽变化依据,这不是给谁看的,是为了自己三个月后回来看代码还能想起来为什么这里有25位信号。
3.2 CIC抽取器核心代码结构与要点
CIC的RTL实现核心是三段:积分器级联、抽取、梳状器级联。注意抽取是放在积分器和梳状器之间的,因为CIC的数学结构就是先积分再抽取再差分。
积分器部分的核心代码:
module cic_integrator #( parameter DATA_WIDTH = 16, parameter INT_WIDTH = 25, parameter STAGES = 3 )( input wire clk, input wire rst_n, input wire signed [DATA_WIDTH-1:0] din, output wire signed [INT_WIDTH-1:0] dout ); reg signed [INT_WIDTH-1:0] int_reg [0:STAGES-1]; integer i; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin for (i = 0; i < STAGES; i = i + 1) int_reg[i] <= {INT_WIDTH{1'b0}}; end else begin int_reg[0] <= int_reg[0] + din; for (i = 1; i < STAGES; i = i + 1) int_reg[i] <= int_reg[i] + int_reg[i-1]; end end assign dout = int_reg[STAGES-1]; endmodule这里有个细节:积分器的位宽必须按前面说的公式预留足够,否则在直流输入时,积分器会一直累加,有限的位宽导致溢出翻转。有些资料说CIC积分器溢出没问题因为后级差分会抵消——那是基于二进制补码的模运算特性,但前提是所有级联模块的位宽严格按照增益公式计算。如果你中间截位了,模运算性质被破坏,差分就不能正确恢复信号,输出就会出现严重失真。
梳状器部分:
module cic_comb #( parameter DATA_WIDTH = 25, parameter DIFF_DELAY = 1, parameter STAGES = 3 )( input wire clk, input wire rst_n, input wire signed [DATA_WIDTH-1:0] din, output wire signed [DATA_WIDTH-1:0] dout ); reg signed [DATA_WIDTH-1:0] delay_reg [0:STAGES-1][0:DIFF_DELAY-1]; // 每一级:dout = din - din[DIFF_DELAY延迟] // 用generate循环实现三级梳状 genvar i; generate for (i = 0; i < STAGES; i = i + 1) begin : comb_stage wire signed [DATA_WIDTH-1:0] delayed; // 这里简化为M=1的情况,每级只延迟一拍 reg signed [DATA_WIDTH-1:0] dff; always @(posedge clk or negedge rst_n) begin if (!rst_n) dff <= {DATA_WIDTH{1'b0}}; else dff <= din; end end endgenerate endmodule为了简洁,上面的梳状器代码做了简化,实际工程中多级梳状器的中间信号连接需要逐级处理。我还是建议把CIC写成单模块,用数组和generate搞定,信号连接更清晰。
抽取部分用输出使能信号实现,而不是真正分频产生慢时钟。我的做法是:在积分器和梳状器之间,每R个输入时钟产生一个数据的有效标志sample_en,梳状器只在sample_en有效时计算。这样整个系统仍然跑在同一个时钟域,避免跨时钟域带来的头疼问题,也符合FPGA设计的良好实践。
3.3 补偿FIR的Verilog实现
补偿FIR我采用串行MAC(乘累加)结构。31阶滤波器,系数对称,可以先把输入数据存入延迟链,然后乘累加。串行结构在一个时钟周期内完成所有MAC不太现实,所以需要多个时钟周期。但对于抽取后的数据率(8MHz/8 = 1MHz),31阶FIR的乘累加用流水线方式很轻松。
对称系数的利用是重点优化方向。31阶FIR,系数对称,实际需要乘法器的数量是 ceil((31+1)/2) = 16个。如果不用对称性,需要31个乘法器,资源差了一倍。
module comp_fir #( parameter DATA_WIDTH = 16, parameter COEF_WIDTH = 18, parameter OUTPUT_WIDTH = 16, parameter TAPS = 31 )( input wire clk, input wire rst_n, input wire signed [DATA_WIDTH-1:0] din, input wire din_valid, output reg signed [OUTPUT_WIDTH-1:0] dout, output reg dout_valid ); // 系数存储 reg signed [COEF_WIDTH-1:0] coeff [0:TAPS-1]; // 数据延迟链 reg signed [DATA_WIDTH-1:0] data_line [0:TAPS-1]; integer i; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin for (i = 0; i < TAPS; i = i + 1) begin data_line[i] <= {DATA_WIDTH{1'b0}}; end end else if (din_valid) begin data_line[0] <= din; for (i = 1; i < TAPS; i = i + 1) data_line[i] <= data_line[i-1]; end end // 乘累加逻辑 reg signed [DATA_WIDTH+COEF_WIDTH-1:0] acc; reg signed [DATA_WIDTH+COEF_WIDTH-1:0] mac_result; reg [5:0] mac_cnt; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin acc <= 0; mac_cnt <= 0; end else if (din_valid) begin // 启动乘累加,多个周期完成 if (mac_cnt < TAPS) begin acc <= acc + data_line[mac_cnt] * coeff[mac_cnt]; mac_cnt <= mac_cnt + 1; end end end // 实际工程中还需状态机控制输出 endmodule这段代码示意了串行MAC的结构,但不是完整可综合版本。真正的实现需要增加一个状态机:检测到din_valid后,依次累加16个对称系数的乘积,然后输出结果。有一个经验值得分享:FIR输出的截位不只是简单右移,需要根据ZCIC得到的增益做精细的缩放对齐。CIC本身有增益,补偿滤波器也有增益,级联后增益可能达到数百倍,仿真时一眼看去输出全是满幅,就容易误判成溢出。最好的办法是把增益用移位量化成2的幂次,在数据通路里显式控制,一眼能看清缩放关系。
3.4 顶层连接与数据位宽规划
顶层模块就是把两个子模块接起来,这里最关键的是CIC输出位宽和FIR输入位宽的衔接。CIC输出25bit,但FIR输入只需要16bit,这里必须截位。怎么截?根据仿真结果确定有效数据所在位。我通常会先做一次全精度的仿真,然后把数据导入MATLAB分析有效位分布,再确定截位方案。直接用高16位截断是最粗暴的,很容易把CIC增益缩放失误导致的饱和当作“正常信号”。
我的建议是在CIC和FIR之间加一个可配置的移位寄存器(shifter),并用一个状态寄存器和仿真中的实际结果对照,确认位宽缩放正确。
4. 仿真验证全流程:从Testbench到波形分析
4.1 Testbench设计:激励生成与参考模型
仿真的核心不只是看波形“动起来”,而是要验证RTL输出和MATLAB理想模型一致。我在Testbench里做的事情有三件:生成多音正弦激励、运行RTL仿真、把RTL输出导出到文件供MATLAB对比。
激励生成我直接用Verilog生成两个正弦波叠加,采样率设为8MHz(也就是输入时钟频率8MHz,抽取后输出1MHz)。用查找表或者直接循环产生,注意正弦波频率要落在补偿滤波器的通带内,比如200kHz和1.2MHz两个音,这样经过CIC后高频音的衰减可以被补偿滤波器恢复出来。
Testbench示例:
module tb_cic_comp; reg clk; reg rst_n; reg signed [15:0] stimulus; wire signed [15:0] cic_out; wire signed [15:0] comp_out; // 生成时钟 125MHz always #4 clk = ~clk; // 8MHz? // 激励信号 reg [31:0] phase1, phase2; wire signed [15:0] sin1 = $signed(...); // 省略查找表细节 always @(posedge clk) begin stimulus <= sin1 + sin2; end // 例化DUT cic_comp_top dut (...); // 输出到文件 integer fd; initial begin fd = $fopen("rtl_output.txt", "w"); end always @(posedge clk) begin if (dout_valid) $fwrite(fd, "%d\n", comp_out); end endmodule有一个非常重要的细节:在Testbench中没有初始化clk和rst_n时,仿真就会给出红线或X态,这是新手最常遇到的情况。我的习惯是rst_n先拉低20个时钟周期,再拉高,确保所有寄存器完成复位。仿真时间设置也要够长,至少让输入信号经过整个滤波链路的建立时间,比如跑2ms的仿真时间,采出2000个输出点,这样数据量对频谱分析足够。
4.2 三段式仿真流程:前仿真、后仿真与Matlab单点对比
前仿真(RTL仿真)验证功能,后仿真(综合后门级仿真)验证时序。我在这两个阶段都投入了精力,因为它们暴露的问题完全不同。
前仿真阶段最典型的坑是“仿真发散”。CIC积分器在复位不干净或者位宽不够时,输出就会不断累加直到饱和,反映在仿真波形上就是阶梯状“爬坡”然后翻转。我排查过一次,最后发现是rst_n释放的时序和时钟沿对齐问题——复位释放瞬间如果时钟沿刚好到来,积分器会多累加一个不确定值,表现出来就是输出有个直流偏置。解决办法是复位释放用异步复位、同步释放,并且把复位释放时刻放在时钟低电平区间(在Testbench中控制rst_n的撤除时间)。
后仿真阶段一般用综合工具生成的网表加标准单元库延迟,跑完后主要看两个指标:最大时钟频率是否达标、是否有毛刺导致的数据采错。CIC和FIR的时序路径主要在乘法器和加法器链上,如果时钟频率上不去,就要在FIR内部插入流水线寄存器。我在补偿FIR中加了2级流水线,把乘累加路径拆开,时序很快就收敛了。
Matlab对比这一步,就是把rtl_output.txt读进MATLAB,和理想模型输出ideal_output.txt放在同一个图上画。不需要每一点完全一致,但要检查误差是否在你预先设定的范围内。因为我做了定点量化,RTL输出和浮点理想输出存在截位误差,误差幅度应该在LSB的几倍以内。如果误差很大,优先检查截位位置和缩放因子。
4.3 波形与频谱分析:如何判断补偿是否生效
波形分析的重点在三个信号上:输入信号stimulus、CIC抽取后的输出cic_out、补偿后的输出comp_out。
输入是双音的时域波形,CIC输出因为抽取和滤波,波形明显变得平滑,但高频分量被CIC压低了。这个压制在时域波形上并不好直观判断,所以一定要做频谱分析。我的操作方法是把仿真输出数据存成文本,导入MATLAB或Python做FFT,观察各频点幅度。
拿我的参数举例,输入的两个音分别是200kHz和1.2MHz,采样率8MHz,抽取因子8,输出数据率1MHz。CIC的归一化频响在1.2MHz处相对于200kHz大约有4-5dB的衰减。补偿滤波器生效后,这个衰减应被抬回到0.1dB以内。我看频谱时主要看两点:一是两个谱峰的相对幅度是否恢复,二是带外噪声是否被抑制而不是被放大。有些设计补偿滤波器没做好,通带拉平了但带外噪声也被放大,提取的信噪比反而更差。
还有一个曲线指标,就是直接对比CIC输出频谱和补偿后频谱,把两者放在同一张图上。这时候你能直观看到补偿滤波器像一个“反斜坡”,把CIC压下去的频段抬回来。我每次做这类设计,都会把这张频谱对比图截出来放在设计文档里,比任何文字都更有说服力。
5. 常见问题与排查技巧实录
5.1 仿真输出是红线/X态
Modelsim或Vivado仿真中波形显示红线,本质是信号没有初始化或没有被驱动。最常见的原因是Testbench中忘记初始化clk或rst_n,导致后续所有时序逻辑输出都是X态蔓延。排查顺序:先看clk和rst_n波形是否正常,再看第一个寄存器输出,逐个向后追踪。我在Vivado里习惯用“Force Clock”功能快速生成时钟,而不是手写always块,省心不少。
另一种红线是复位释放后第一个有效数据前的空窗期,这个不是问题,数据流建立后过了几个时钟周期自然消失。新手容易误判,其实只要把波形放大到数据有效区间看即可。
5.2 CIC输出发散或不收敛
“仿真发散”这个词在热搜词里出现得很高频。CIC输出发散,我在实际调试中遇到过两种根因。第一种是积分器位宽不够,前面反复提到了:N=3、R=8、M=1,位宽需要增加9bit。如果你在RTL里没按公式扩展位宽,直流分量累积几次之后就会溢出翻转,看起来就是波形“彻底失控”。第二种是Testbench里的激励信号位宽和数据通路位宽不匹配,比如激励生成的是无符号数,但CIC输入声明的是signed,这会产生一个巨大的直流偏置,积分器迟早饱和。
排查发散问题,我推荐一个非常实用的方法:先用直流输入测试CIC。给一个固定常数,理论上CIC收敛后各节点电压(数值)应该保持恒定。如果积分器输出一直线性增长,那肯定是位宽或反馈环路问题。这种方法比直接上正弦波调试快得多。
5.3 补偿后带外噪声放大
这是补偿滤波器设计里的经典陷阱。补偿滤波器在通带内增益大于1,如果阻带没有约束,它在带外也会放大噪声。我在design阶段用fdesign.ciccomp时,会把阻带衰减Av设到60dB以上,并仔细检查补偿滤波器在0.5/R以外的频响曲线。如果发现带外增益超过0dB,就需要提高阻带设计要求或者增加阶数。
另外还有一点,补偿滤波器对CIC阻带零点位置附近的响应特别敏感,如果系数量化导致零点偏移,可能在某个频点形成尖峰。这种问题在频域视角下非常明显,在时域波形里几乎看不到。所以我的习惯是无论做多简单的CIC项目,最后一定做频谱分析,不能只看时域波形。
5.4 仿真速度慢到怀疑人生
R=8的抽取仿真还好,数据量不大。但如果你把R提到64或128,再叠加一个64阶的补偿FIR,仿真时间会变得非常可观。我的经验是分层验证:先在MATLAB里完成全链路算法验证,再用RTL做小数据量验证。所谓小数据量,不是减少采样点数,而是降低输入信号频率、缩短仿真总时间,只要能覆盖几十个抽取周期就足够证明功能。
如果要跑大量数据,可以在Testbench里加一个“数据源从文件读取”的逻辑,用Python或MATLAB提前生成上万个点的激励数据,然后交给仿真器跑。这比在Testbench里用Verilog算三角函数高效得多。
6. 后续扩展与实际应用思考
CIC补偿滤波器做完了,往哪个方向扩展都会遇到对应的新问题。后端做上变频,反向对称,工具和思路可以完全复用;把补偿滤波器换成IIR,阶数更低但稳定性验证要花额外精力;把定点仿真结果接入FPGA在线调试,则涉及ILA逻辑分析仪的具体触发条件和采样深度设置。
我在实际应用中最常遇到的是把R=8的演示参数换成真实系统的非整数抽取或宽带信号场景。这时MATLAB设计工具里的设置会复杂一些,比如通带频率不再是简单的0.5/R,而是根据倍率实时计算的相对带宽。但整体思路不变——先算CIC频响,再设计反向补偿,最后在RTL里落地。
最后分享一个小技巧:在RTL里调试CIC、FIR这类数据通路时,除了看波形,还可以用$display在关键节点直接打印数据流。配合$fwrite写文件,能让你在几秒钟内定位是截位错误、位宽错误还是时序错误。很多问题藏在大量波形里肉眼看不出来,数据打印反而最直接。
这个项目做完之后,我对CIC的认识变了。以前觉得它就是个简单的抽取滤波器,代码几十行写完收工。但真正把它和补偿滤波器串成完整链路,再去啃通带下垂、位宽增长、量化误差这些问题,才意识到CIC的“简单”背后全是数字信号处理的底层逻辑。推荐每个做FPGA信号处理的工程师都亲手搭一遍这个链路,收获比看十篇文章都大。