1. 从150MHz到1MHz,为什么不能一步到位
先把场景摆清楚。假设你手上有一路ADC采样数据,采样率150MHz,信号带宽大概几百kHz,你现在需要把它降到1MHz附近做后续处理——可能是解调、可能是音频分析、也可能是某种低速控制环路。很多人第一反应是:直接每150个点抽1个不就行了?
理论上,如果原始信号里只有你想要的低频成分,抽取确实没问题。但现实是,ADC前端进来的信号里混着大量高频噪声、杂散、甚至邻近频道的干扰。直接抽取会把所有这些高频成分混叠到低频带内,一旦混进来就再也分不开了。这就是为什么降采样之前必须先抗混叠滤波。
那为什么不用一个150MHz采样率下直接跑一个陡峭的低通FIR?可以,但代价极大。要在150MHz时钟下实现一个过渡带极窄、阻带衰减80dB以上的FIR,阶数可能要到几百甚至上千阶。这意味着需要几百个乘法器在150MHz下同时工作,FPGA里的DSP资源根本扛不住。即使资源够,功耗和时序收敛也是噩梦。
所以工程上的标准做法是多级级联:先用CIC滤波器做粗降采样,把采样率大幅拉低,再用FIR做精细的整形和补偿。CIC的优势在于不需要乘法器,只用加减法和寄存器就能实现,在高速率下极其省资源。它的缺点也明显——通带不平坦,有droop(下垂),阻带衰减有限。所以CIC后面跟一级FIR,既做补偿又做最终的选择性滤波。
这套CIC+FIR的组合,就是从150MHz降到1MHz最经典、最务实的方案。下面我把整个流程从参数计算到Vivado实现到仿真验证,一步步拆开讲。
2. 降采样链路的分级设计与参数计算
2.1 为什么选CIC做第一级粗降采样
CIC(Cascaded Integrator-Comb)滤波器的核心结构是积分器+抽取+梳状器的级联。它之所以适合做第一级,原因有三个:
第一,无乘法器。CIC只用加法器和延迟寄存器,在150MHz这种高速时钟下,加法器的时序压力远小于乘法器。你不需要占用宝贵的DSP48资源。
第二,抽取因子可以很大。CIC的抽取因子R可以做到几十甚至上百,一级就能把150MHz拉到几MHz。这为后续FIR争取了巨大的资源空间。
第三,结构规整,参数化容易。CIC的性能由三个参数决定:抽取因子R、差分延迟M、级数N。改变这三个参数就能调整频率响应,非常适合在Vivado里用IP核快速配置。
但CIC有个硬伤:通带下垂。它的频率响应是sinc函数的形状,在通带边缘会有明显的幅度衰减。级数N越多,阻带衰减越好,但通带下垂也越严重。所以CIC后面必须跟一级补偿FIR。
2.2 分级方案的具体计算
我的方案是这样的:150MHz先经过CIC做R=25的抽取,降到6MHz。然后再经过一级FIR做2倍抽取,降到3MHz。最后再经过一级FIR做3倍抽取,降到1MHz。
等等,为什么不直接CIC做R=150一步到位?因为CIC的抽取因子越大,通带下垂越严重,而且CIC的工作时钟必须至少是输入采样率,150MHz下R=150意味着梳状器部分要在150MHz下处理,积分器部分在1MHz下处理,中间的位宽会膨胀得很厉害。位宽膨胀是CIC的另一个坑——每经过一级积分器,位宽就要增加log2(R)位。R=150的话,log2(150)≈7.23,N级就是7.23N位的膨胀。如果N=5,位宽要增加36位,这对寄存器资源是很大的浪费。
所以更合理的做法是多级CIC+FIR混合。我选择的分级是:
| 级数 | 滤波器类型 | 输入速率 | 输出速率 | 抽取因子 | 主要作用 |
|---|---|---|---|---|---|
| 第一级 | CIC | 150MHz | 6MHz | 25 | 粗降采样,抗混叠 |
| 第二级 | FIR | 6MHz | 3MHz | 2 | 补偿CIC下垂,进一步滤波 |
| 第三级 | FIR | 3MHz | 1MHz | 3 | 最终整形,精确控制带宽 |
这个方案的好处是每一级的抽取因子都不大,CIC的位宽膨胀可控,FIR的阶数也不需要太高。整体资源消耗和时序压力都在合理范围内。
2.3 CIC参数的确定
CIC的三个参数需要仔细选。抽取因子R=25已经定了。差分延迟M通常取1,这是最常用的值,取大了会让频率响应的零点位置偏移,一般没必要。级数N决定了阻带衰减,CIC的阻带衰减近似为:
阻带衰减(dB) ≈ 20 × N × log10(R)
对于R=25,如果N=4,阻带衰减≈20×4×log10(25)≈20×4×1.4≈112dB。这已经足够了。N=5的话约140dB,但通带下垂也更严重。我选N=4,在阻带衰减和通带平坦度之间取平衡。
CIC的输出位宽计算公式是:
B_out = B_in + ceil(N × log2(R × M))
假设输入位宽B_in=12位(典型ADC位宽),N=4,R=25,M=1:
B_out = 12 + ceil(4 × log2(25)) = 12 + ceil(4 × 4.64) = 12 + ceil(18.56) = 12 + 19 = 31位
所以CIC输出需要31位。这个位宽在后续FIR里可以适当截断,因为有效信号只占低位的一部分。
2.4 补偿FIR的设计要点
CIC在6MHz输出速率下,通带边缘(假设你关心的信号带宽是500kHz)的下垂量需要计算。CIC的幅度响应为:
|H(f)| = |sin(π × R × M × f / fs) / (R × M × sin(π × f / fs))|^N
在f=500kHz,fs=150MHz,R=25,M=1,N=4时,代入计算会发现通带边缘大约有3-4dB的下垂。补偿FIR的目标就是在通带内提供相反的增益曲线,把下垂补回来。
补偿FIR的阶数不需要太高,因为CIC的下垂曲线是平滑的,用20-30阶就够。我一般用Vivado的FIR Compiler IP核,选择“Inverse Sinc”响应类型,它会自动生成补偿系数。但要注意,IP核生成的系数是浮点的,需要量化成定点数。量化位宽建议至少16位,否则补偿精度不够。
第二级FIR做2倍抽取,工作在6MHz输入速率下,30阶的话需要30个乘法器在6MHz下工作,资源完全不是问题。第三级FIR做3倍抽取,工作在3MHz下,同样30阶,更轻松。
3. Vivado里CIC IP核的配置与位宽处理
3.1 CIC Compiler的配置界面详解
在Vivado里,CIC滤波器通过“CIC Compiler”IP核实现。打开IP Catalog,搜索“CIC”,找到“CIC Compiler”。双击打开配置界面,主要参数有:
- Filter Type:选Decimation(抽取)
- Number of Stages:填4
- Differential Delay:填1
- Decimation Rate:填25
- Input Sample Frequency:填150
- Input Data Width:填12
- Quantization:选Full Precision(全精度输出)
这里有个关键点:Quantization选项。如果选Full Precision,输出位宽就是前面算的31位。如果选Truncation,你可以指定输出位宽,IP核会自动截断。我建议第一级CIC选Full Precision,把截断留给后面的FIR去做,这样灵活性更高。
配置完成后,IP核会显示频率响应曲线。你可以直观地看到通带下垂和阻带衰减。如果下垂太严重,可以适当降低N或者减小R。
3.2 位宽膨胀的实际处理
CIC输出31位,但后续FIR的输入不需要这么宽。怎么截?不是简单地把低位丢掉,而是要考虑有效信号的位置。
CIC的增益是(R×M)^N = 25^4 = 390625,约等于2^18.6。也就是说,输入信号经过CIC后,幅度放大了约390625倍。如果输入是12位有符号数,满量程是±2048,输出满量程就是±2048×390625≈±8×10^8,需要30位才能表示。所以31位输出里,真正有效的信号在高位,低位主要是量化噪声和运算误差。
截断策略是:保留高18-20位,丢掉低11-13位。具体丢多少要看你的信噪比要求。丢得越多,资源越省,但量化噪声越大。我一般保留20位,丢11位。这样第二级FIR的输入位宽是20位,输出可以截到16位。
注意:截断时一定要做饱和处理(Saturation),而不是简单的截位。否则大信号时会从正最大值翻转到负最大值,产生严重的失真。Vivado的FIR Compiler IP核里有Output Rounding和Saturation选项,记得勾上。
3.3 时钟域的处理
CIC的输入是150MHz,输出是6MHz。在Vivado里,CIC IP核的时钟使能信号(CE)需要按照抽取率来产生。具体来说,你需要一个计数器,每25个150MHz时钟周期产生一个6MHz的使能脉冲。
这个使能脉冲的产生很简单:
reg [4:0] cnt = 0; reg ce_6m = 0; always @(posedge clk_150m) begin if (cnt == 24) begin cnt <= 0; ce_6m <= 1; end else begin cnt <= cnt + 1; ce_6m <= 0; end endCIC IP核的CE端口接这个ce_6m信号,IP核内部就会在使能有效时输出一个抽取后的样本。注意CIC IP核的输入数据端口是在150MHz下每个周期都采样的,但只有CE有效时才真正处理。所以你的ADC数据要一直保持在数据线上,不能只在CE有效时才给。
3.4 多级级联的接口衔接
CIC输出6MHz数据,第二级FIR也工作在6MHz。这里不需要再做时钟域转换,直接把CIC的输出接到FIR的输入,FIR的CE也接ce_6m即可。但要注意,CIC IP核的输出有效信号(RDY)需要正确连接到FIR的输入有效信号。
Vivado的FIR Compiler IP核有“Input Valid”和“Output Valid”信号,CIC Compiler也有类似的握手信号。级联时要把上一级的输出有效接到下一级的输入有效,确保数据流正确同步。
第二级FIR做2倍抽取,输出3MHz。它的CE需要每2个6MHz周期产生一个脉冲。第三级FIR做3倍抽取,输出1MHz,CE每3个3MHz周期产生一个脉冲。这些使能信号的产生逻辑类似,只是计数器的模值不同。
4. FIR Compiler的系数生成与定点量化
4.1 用MATLAB生成补偿系数
虽然Vivado的FIR Compiler IP核可以自动生成Inverse Sinc系数,但自动生成的系数有时候不够精确。我习惯用MATLAB自己算,这样可控性更强。
补偿FIR的目标响应是CIC响应的倒数。在MATLAB里可以这样写:
fs = 6e6; % 采样率 f_pass = 500e3; % 通带边缘 R = 25; M = 1; N = 4; f = 0:1e3:f_pass; H_cic = abs(sin(pi*R*M*f/150e6)./(R*M*sin(pi*f/150e6))).^N; H_comp = 1./H_cic; H_comp = H_comp / max(H_comp); % 归一化然后用firls或fir2设计一个FIR,使其频率响应逼近H_comp。阶数取30,用firls:
order = 30; b = firls(order, [0 f_pass fs/2]/(fs/2), [H_comp(1) H_comp(end) 0]);这样得到的b就是补偿FIR的系数。注意这个FIR同时要完成2倍抽取的抗混叠,所以它的截止频率要设在fs/4=1.5MHz附近,而不是500kHz。因为2倍抽取后采样率变成3MHz,奈奎斯特频率是1.5MHz,所以FIR的阻带要从1.5MHz开始。
4.2 定点量化的位宽选择
MATLAB算出来的系数是浮点数,需要量化成定点数。量化位宽直接影响滤波器的性能。一般来说:
- 系数位宽16位:阻带衰减约80-90dB
- 系数位宽18位:阻带衰减约90-100dB
- 系数位宽20位:阻带衰减约100-110dB
对于大多数应用,16位就够了。但如果你的系统对阻带衰减要求很高(比如超过100dB),建议用18位或20位。
量化时要注意系数的动态范围。如果系数最大值和最小值相差很大,量化误差会集中在小的系数上。可以用归一化的方法:先把系数除以最大值,量化后再在FPGA里通过移位恢复增益。
在MATLAB里量化:
b_max = max(abs(b)); b_norm = b / b_max; b_fix = round(b_norm * (2^15 - 1)); % 16位有符号这样b_fix就是16位定点系数,范围在[-32767, 32767]。在FPGA里,FIR的输出需要乘以b_max的倒数来恢复增益。这个增益恢复可以在FIR之后用移位实现,如果b_max接近2的幂次,移位就很方便。
4.3 FIR Compiler IP核的配置
在Vivado里打开FIR Compiler IP核,配置如下:
- Filter Type:Decimation
- Decimation Rate:2(第二级)或3(第三级)
- Coefficient Source:COE File
- Coefficient File:选择MATLAB导出的.coe文件
- Input Data Width:20(第二级)或16(第三级)
- Coefficient Width:16
- Output Width:16
- Output Rounding:Round to Nearest
- Saturation:勾选
.coe文件的格式很简单:
radix=10; coefdata= -123, 456, -789, ...;把MATLAB量化的系数按这个格式写进去就行。
4.4 多相滤波的考虑
如果你的FIR阶数很高(比如超过100阶),在高速时钟下可能需要用多相结构来降低乘法器的数量。Vivado的FIR Compiler IP核支持“Multi-Phase”模式,可以把一个高采样率的FIR分解成多个低采样率的子滤波器。
但在我们这个场景里,第二级FIR工作在6MHz,30阶,根本不需要多相。第三级工作在3MHz,更不需要。所以直接用普通的Decimation模式就行。多相滤波是给那些采样率很高、阶数很大的场景用的,比如直接对150MHz信号做FIR。
5. 仿真验证:从MATLAB到Vivado的联合调试
5.1 在MATLAB里先跑通链路
在写Verilog之前,我强烈建议先在MATLAB里把整个链路仿真一遍。这样可以快速验证参数是否合理,避免在Vivado里反复调试。
MATLAB仿真脚本的大致流程:
% 生成测试信号:500kHz正弦 + 高频噪声 fs = 150e6; t = 0:1/fs:0.001; f_sig = 500e3; sig = sin(2*pi*f_sig*t) + 0.1*sin(2*pi*10e6*t) + 0.05*randn(size(t)); % CIC抽取 R = 25; N = 4; M = 1; cic_out = cic_decimate(sig, R, N, M); % 第二级FIR b2 = load('fir2_coeff.txt'); fir2_out = filter(b2, 1, cic_out); fir2_out = fir2_out(1:2:end); % 2倍抽取 % 第三级FIR b3 = load('fir3_coeff.txt'); fir3_out = filter(b3, 1, fir2_out); fir3_out = fir3_out(1:3:end); % 3倍抽取 % 画频谱 pwelch(fir3_out, [], [], [], 1e6);跑完这个脚本,你能看到最终的频谱。如果500kHz的信号清晰,10MHz的干扰被压到-80dB以下,说明参数没问题。如果干扰没压下去,就要调整CIC的N或者FIR的阶数。
5.2 Vivado仿真环境的搭建
在Vivado里建一个Testbench,把ADC数据用文件读入的方式送给CIC。Testbench的基本结构:
module tb_top; reg clk_150m = 0; reg rst_n = 0; reg [11:0] adc_data; wire [15:0] dout; wire dout_valid; // 时钟生成 always #3.33 clk_150m = ~clk_150m; // 150MHz // 读取ADC数据 reg [11:0] adc_mem [0:14999]; integer i = 0; initial begin $readmemh("adc_data.hex", adc_mem); rst_n = 0; #100 rst_n = 1; for (i = 0; i < 15000; i = i + 1) begin @(posedge clk_150m); adc_data <= adc_mem[i]; end #10000 $finish; end // 例化顶层 top u_top ( .clk_150m(clk_150m), .rst_n(rst_n), .adc_data(adc_data), .dout(dout), .dout_valid(dout_valid) ); // 把输出写入文件 integer fout; initial fout = $fopen("dac_out.txt", "w"); always @(posedge clk_150m) begin if (dout_valid) $fwrite(fout, "%d\n", dout); end endmoduleADC数据可以用MATLAB生成,存成十六进制文件。仿真跑完后,把dac_out.txt读回MATLAB做频谱分析,和MATLAB仿真结果对比。
5.3 仿真中常见的坑
第一个坑:CIC输出延迟。CIC IP核有内部延迟,输出有效信号比输入晚若干个时钟周期。如果你在Testbench里没有正确处理这个延迟,可能会把无效数据当成有效数据。解决办法是严格使用IP核的RDY信号作为数据有效标志。
第二个坑:FIR的Group Delay。FIR滤波器有群延迟,30阶FIR的群延迟是15个采样周期。在6MHz下就是2.5微秒。这个延迟在仿真里会影响你对齐输入输出,做频谱分析时要记得去掉前面的瞬态。
第三个坑:定点溢出的仿真。在MATLAB里仿真是浮点的,不会溢出。但在FPGA里是定点的,如果中间某一级的位宽不够,会溢出。仿真时要把Vivado的波形导出来,检查每一级的最大值是否接近满量程。如果接近,就要增加位宽或者调整增益。
第四个坑:仿真速度。150MHz时钟下仿真1毫秒就是150000个周期,Vivado仿真会跑得很慢。我一般只仿真100微秒左右,足够看到稳态响应了。如果非要跑长仿真,可以用Vivado的“Behavioral Simulation”加上“-testplusarg”来加速,或者用第三方仿真器。
6. 时序收敛与资源优化的实战经验
6.1 150MHz下的时序压力
150MHz时钟周期是6.67纳秒。在Artix-7或Zynq-7000这种主流器件上,这个频率不算高,但也不低。CIC的积分器部分在150MHz下工作,每级积分器就是一个加法器加一个寄存器。加法器的组合逻辑延迟加上布线延迟,在6.67纳秒内完成一般没问题。但如果你的器件速度等级较低(比如-1),或者布线拥塞严重,可能会时序不收敛。
我的经验是:CIC的积分器部分不要加额外的逻辑。有些人在积分器后面加了饱和处理或者舍入逻辑,这会增加组合逻辑延迟,导致时序失败。正确的做法是让CIC IP核输出全精度结果,把舍入和饱和留给后面的FIR。
6.2 资源占用的估算与优化
整个链路的资源占用大致如下(以Artix-7 XC7A100T为例):
| 资源类型 | CIC (N=4, R=25) | FIR2 (30阶, 2倍抽取) | FIR3 (30阶, 3倍抽取) | 合计 |
|---|---|---|---|---|
| DSP48 | 0 | 15 | 10 | 25 |
| 寄存器 | ~200 | ~500 | ~400 | ~1100 |
| LUT | ~300 | ~600 | ~500 | ~1400 |
| BRAM | 0 | 0 | 0 | 0 |
这个资源占用非常小,XC7A100T有240个DSP48和超过60000个LUT,完全绰绰有余。如果你用的是更小的器件,比如XC7A35T,也完全放得下。
优化方向:如果DSP资源紧张,可以把FIR的系数位宽从16位降到12位,这样DSP48可以从25个降到20个左右。但阻带衰减会降到60-70dB。如果LUT紧张,可以复用乘法器,用时分复用的方式让一个乘法器处理多个抽头,但这会降低吞吐率,在6MHz下可能没问题,在150MHz下就不行了。
6.3 布局布线的注意事项
Vivado的布局布线器有时候会把CIC和FIR放在相距很远的区域,导致布线延迟增加。如果时序不收敛,可以尝试以下方法:
- 给CIC和FIR分别加Pblock约束,把它们约束在相邻的区域
- 使用MAX_FANOUT属性限制高扇出信号的扇出数
- 对时钟使能信号加BUFG,确保时钟质量
另外,150MHz的时钟建议走全局时钟网络,不要用普通IO引脚直接驱动。如果时钟是从外部晶振进来的,要经过IBUFG和BUFG。
6.4 实测中的意外情况
我在实际项目里遇到过一个问题:CIC输出在特定输入模式下会出现极限环振荡。原因是CIC的积分器在输入为常数时,输出会周期性波动。这是CIC的固有特性,不是bug。解决办法是在CIC后面加一个抖动注入(dither),或者把CIC的级数降低。
还有一个问题是FIR的系数溢出。如果MATLAB量化的系数超过了16位有符号数的范围(-32768到32767),Vivado会报错。检查方法是看MATLAB里max(abs(b_fix))是否小于32767。如果超了,要么降低量化位宽,要么重新归一化。
7. 从仿真到上板的最后一步
7.1 生成比特流前的检查清单
在点“Generate Bitstream”之前,我一般会检查这几项:
- 时序报告:确保WNS(Worst Negative Slack)为正,最好大于0.5纳秒
- DRC报告:确保没有严重的DRC错误,特别是RTSTAT-2这种和复位相关的
- 功耗报告:确保功耗在器件和散热能力范围内
- IO约束:确保ADC数据引脚和时钟引脚都正确约束了
如果DRC报RTSTAT-2错误,通常是复位信号没有正确约束。检查你的复位信号是否加了ASYNC_REG属性,或者是否用了正确的复位同步器。
7.2 上板调试的实用技巧
上板后,第一件事是用**ILA(Integrated Logic Analyzer)**抓CIC和FIR的输出波形。在Vivado里例化ILA IP核,把CIC的输出、FIR的输入输出、有效信号都接上。触发条件设成“dout_valid == 1”,采样深度设成1024或2048。
抓到的数据可以导出成CSV,在MATLAB里做FFT。如果频谱和仿真一致,说明链路工作正常。如果不一致,检查以下几点:
- ADC数据是否真的在150MHz下稳定
- CIC的CE信号是否正确产生
- FIR的系数是否和MATLAB一致
- 时钟是否有抖动或偏斜
7.3 性能实测数据
我在一块Artix-7开发板上实测过这个链路。输入是12位ADC,150MHz采样,信号是500kHz正弦加10MHz干扰。经过CIC+FIR后,输出1MHz采样率,500kHz信号的信噪比约75dB,10MHz干扰被压到-85dB以下。整个链路的延迟约5微秒,功耗增加约120毫瓦。
这个性能对于大多数低速信号处理应用已经足够了。如果你需要更高的信噪比,可以增加FIR的阶数或者提高系数位宽。如果你需要更低的延迟,可以减少FIR的阶数,但阻带衰减会变差。
7.4 后续扩展的方向
这个链路还可以进一步扩展。比如在CIC前面加一级HB(Half-Band)滤波器,先做2倍抽取,把150MHz降到75MHz,再进CIC。这样CIC的工作时钟可以降到75MHz,时序压力更小。HB滤波器的系数很规整,资源占用也很少。
或者把第三级FIR换成多相滤波器,实现更灵活的抽取率。比如你需要从3MHz降到1.2MHz,抽取因子是2.5,不是整数。这时候可以用多相滤波器实现分数倍抽取。
再或者,如果你需要正交下变频,可以在CIC后面加一个NCO(Numerically Controlled Oscillator)和混频器,把信号搬到基带。这整套流程在Vivado里都有对应的IP核,配置起来很方便。
我个人在实际操作中的体会是:CIC+FIR这套组合看起来简单,但参数计算和位宽处理是最容易出问题的地方。尤其是CIC的位宽膨胀和FIR的系数量化,一定要在MATLAB里先验证清楚再写Verilog。仿真阶段多花一个小时,上板调试就能少花一天。另外,ILA抓波形的时候,不要只看输出,要把每一级的中间结果都抓出来对比,这样才能快速定位问题出在哪一级。