ARTICLE DETAIL

资讯详情

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

Verilog级CIC补偿滤波器仿真:从原理到实战全解析

Verilog级CIC补偿滤波器仿真:从原理到实战全解析 Verilog级CIC补偿滤波器仿真演示从原理到仿真的完整实战很多做数字中频、软件无线电或者音频处理的FPGA工程师早晚都会撞上CIC滤波器这道坎。CICCascaded Integrator-Comb级联积分梳状滤波器在抽取和内插场景里简直是神器不需要乘法器纯加法器和延迟单元就能实现资源省得感人。但它的通带衰减droop也让人头疼尤其是抽取倍数大了以后信号高频分量被压得明显变形。这时候就需要补偿滤波器CIC Compensation Filter来把通带拉平。我这次就把一个完整的“Verilog级CIC补偿滤波器仿真演示”项目拆开来讲从数学原理到Verilog实现再到Modelsim/Vivado仿真验证把每一步背后的为什么都说清楚。这篇文章既适合刚接触FPGA数字信号处理的同学做入门参考也适合老工程师快速回顾关键细节。先说清楚这个项目能干什么输入一个采样率较高的信号经过CIC抽取后再用一个补偿FIR滤波器纠正CIC引入的通带失真。整个过程用Verilog实现在仿真环境里跑通直接观察波形和频谱结果。我用的参数是抽取因子R8、差分延迟M1、CIC级数N3补偿滤波器采用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 [求和_{k0}^{M-1} z^(-k)]^N当M1时其实就是N个移动平均滤波器的级联。这个结构最漂亮的地方在于它只有加法和延迟没有乘法器。在FPGA里面乘法的代价远高于加法所以CIC特别适合处理高数据率的场景比如ADC前端采样率100MHz以上的信号抽取。但是天下没有免费的午餐。CIC的幅度响应有一个明显的问题|H(e^jw)| |sin(wM/2) / sin(w/2)|^N这个响应在通带内不是平的而是随着频率上升呈现下垂droop。N越大、M越大下垂越剧烈。举个例子R8、M2、N3时通带边缘的信号增益可能衰减好几个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到底怎么定这个项目的核心参数我定了R8、M1、N3这里说明一下我的选择逻辑。抽取因子R8这是一个比较典型的中间值既能体现抽取的过程又不会让仿真数据量爆炸。R再大比如64仿真时间会很长且补偿滤波器的频响设计要特别小心R再小比如2CIC的优势体现不明显。差分延迟M1M1时CIC的频率响应是sin(x)/x形状严格说是sin(wR/2)/sin(w/2)的N次方实现最简单延迟单元最少。M2可以增加零点数量和混叠抑制能力但通带下垂更严重。作为教学演示M1最合适计算也直观。级数N3N越大阻带衰减越大但通带下垂也越厉害而且寄存器位宽增长更快。N3时阻带衰减大约在30-40dB量级足够演示效果同时补偿FIR的阶数不用太高。如果用N5或更高补偿滤波器的抽头数可能要到60以上才能拉平通带仿真和实现都要更费劲。位宽增长的问题这里也提一下。CIC滤波器内部寄存器的位宽增长遵循公式B_out ceil(N * log2(R * M)) B_in我输入用16bit数据R8、M1、N3那 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{1b0}}; 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; // 这里简化为M1的情况每级只延迟一拍 reg signed [DATA_WIDTH-1:0] dff; always (posedge clk or negedge rst_n) begin if (!rst_n) dff {DATA_WIDTH{1b0}}; 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 1MHz31阶FIR的乘累加用流水线方式很轻松。对称系数的利用是重点优化方向。31阶FIR系数对称实际需要乘法器的数量是 ceil((311)/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{1b0}}; 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_WIDTHCOEF_WIDTH-1:0] acc; reg signed [DATA_WIDTHCOEF_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输出发散我在实际调试中遇到过两种根因。第一种是积分器位宽不够前面反复提到了N3、R8、M1位宽需要增加9bit。如果你在RTL里没按公式扩展位宽直流分量累积几次之后就会溢出翻转看起来就是波形“彻底失控”。第二种是Testbench里的激励信号位宽和数据通路位宽不匹配比如激励生成的是无符号数但CIC输入声明的是signed这会产生一个巨大的直流偏置积分器迟早饱和。排查发散问题我推荐一个非常实用的方法先用直流输入测试CIC。给一个固定常数理论上CIC收敛后各节点电压数值应该保持恒定。如果积分器输出一直线性增长那肯定是位宽或反馈环路问题。这种方法比直接上正弦波调试快得多。5.3 补偿后带外噪声放大这是补偿滤波器设计里的经典陷阱。补偿滤波器在通带内增益大于1如果阻带没有约束它在带外也会放大噪声。我在design阶段用fdesign.ciccomp时会把阻带衰减Av设到60dB以上并仔细检查补偿滤波器在0.5/R以外的频响曲线。如果发现带外增益超过0dB就需要提高阻带设计要求或者增加阶数。另外还有一点补偿滤波器对CIC阻带零点位置附近的响应特别敏感如果系数量化导致零点偏移可能在某个频点形成尖峰。这种问题在频域视角下非常明显在时域波形里几乎看不到。所以我的习惯是无论做多简单的CIC项目最后一定做频谱分析不能只看时域波形。5.4 仿真速度慢到怀疑人生R8的抽取仿真还好数据量不大。但如果你把R提到64或128再叠加一个64阶的补偿FIR仿真时间会变得非常可观。我的经验是分层验证先在MATLAB里完成全链路算法验证再用RTL做小数据量验证。所谓小数据量不是减少采样点数而是降低输入信号频率、缩短仿真总时间只要能覆盖几十个抽取周期就足够证明功能。如果要跑大量数据可以在Testbench里加一个“数据源从文件读取”的逻辑用Python或MATLAB提前生成上万个点的激励数据然后交给仿真器跑。这比在Testbench里用Verilog算三角函数高效得多。6. 后续扩展与实际应用思考CIC补偿滤波器做完了往哪个方向扩展都会遇到对应的新问题。后端做上变频反向对称工具和思路可以完全复用把补偿滤波器换成IIR阶数更低但稳定性验证要花额外精力把定点仿真结果接入FPGA在线调试则涉及ILA逻辑分析仪的具体触发条件和采样深度设置。我在实际应用中最常遇到的是把R8的演示参数换成真实系统的非整数抽取或宽带信号场景。这时MATLAB设计工具里的设置会复杂一些比如通带频率不再是简单的0.5/R而是根据倍率实时计算的相对带宽。但整体思路不变——先算CIC频响再设计反向补偿最后在RTL里落地。最后分享一个小技巧在RTL里调试CIC、FIR这类数据通路时除了看波形还可以用$display在关键节点直接打印数据流。配合$fwrite写文件能让你在几秒钟内定位是截位错误、位宽错误还是时序错误。很多问题藏在大量波形里肉眼看不出来数据打印反而最直接。这个项目做完之后我对CIC的认识变了。以前觉得它就是个简单的抽取滤波器代码几十行写完收工。但真正把它和补偿滤波器串成完整链路再去啃通带下垂、位宽增长、量化误差这些问题才意识到CIC的“简单”背后全是数字信号处理的底层逻辑。推荐每个做FPGA信号处理的工程师都亲手搭一遍这个链路收获比看十篇文章都大。
返回列表