
做数字信号处理的人应该都跟CIC滤波器打过交道尤其是在FPGA里做数字下变频DDC或者数字上变频DUC的时候。CIC这东西最大的好处就是省资源积分器加梳状器拼起来一个乘法器都不用纯靠加法器和延迟寄存器就能完成大倍率抽取或插值。但省资源的代价也很明显通带不是平的会往下塌级数越多、抽取倍数越大塌得越狠。这就要求在后面或前面串一个补偿滤波器把这个下垂拉回来。这篇文章我主要想分享一下我最近做的一个完整链路用Verilog实现CIC抽取滤波器再加上一个FIR补偿滤波器然后用ModelSim/QuestaSim跑仿真最后把数据导出来用MATLAB做FFT验证补偿效果。整个过程中踩了不少坑尤其是截位、位宽、仿真发散、波形红X这些问题我会把排查思路一起写出来。适合刚接触多速率信号处理、想在FPGA里落地CIC补偿链路、以及被仿真问题折磨的同学参考。1. CIC滤波器特性回顾与补偿需求1.1 CIC滤波器的基本结构与增益计算CICCascaded Integrator-Comb级联积分器-梳状器滤波器在结构上就是前后两段前段是N个积分器级联后段是N个梳状器级联中间根据需要插入抽取器或插值器。抽取模式下积分器跑在高速时钟域梳状器跑在低速时钟域插值模式则反过来。每个积分器的传递函数是1/(1-z^-1)每个梳状器的传递函数是1-z^(-M*D)其中M是抽取或插值倍数D是微分延迟通常取1或2。整个CIC的传递函数可以写为H(z) [(1 - z^(-M*D)) / (1 - z^(-1))]^N把z换成e^(j2πf/fs)之后可以得到它的幅频响应|H(f)| |sin(πMDf/fs) / (MDsin(πD*f/fs))|^N这里需要注意很多资料会把分子分母同时归一化使得零频处增益为1。做FPGA实现的时候我们真正关心的是未归一化的整型增益也就是G (M*D)^N。比如M16、D1、N4增益就是16^465536。这个数字直接决定了内部寄存器的位宽我后面会专门算。CIC之所以能在高速时钟域里跑得很欢核心在于它没有乘法器。积分器就是“累加”两个字每个时钟周期把输入加到累加器上。梳状器更简单当前值减去延迟N拍前的值。全部是加法和减法这对FPGA的时序非常友好。从另一个角度看CIC其实就是一个移动平均滤波器滑动窗口滤波在多速率条件下的变体窗口长度等于M*D所以它的频率响应天然就是sinc形状必然存在通带下垂。1.2 通带下垂的定量分析下垂有多严重我习惯直接代公式估算。以我这次演示用的参数为例输入采样率fs_in80MHz抽取倍数M16输出采样率fs_out5MHzCIC级数N4微分延迟D1。输出端的奈奎斯特频率是2.5MHz。我先算1MHz处的衰减此时f/fs_in 1/80 0.0125sin(π160.0125) sin(0.6283) ≈ 0.5878 16 * sin(π*0.0125) 16 * sin(0.03927) ≈ 16 * 0.03926 0.6283单级幅度比为0.5878/0.6283 ≈ 0.93554级之后为0.9355^4 ≈ 0.766换算成dB就是20*log10(0.766) ≈ -2.3dB。再算1.5MHz处此时f/fs_in 1.5/80 0.01875sin(π160.01875) sin(0.9425) ≈ 0.8090 16 * sin(π*0.01875) 16 * sin(0.0589) ≈ 16 * 0.05887 0.9419单级幅度比为0.8090/0.9419 ≈ 0.85894级之后为0.8589^4 ≈ 0.544换算成dB是20*log10(0.544) ≈ -5.3dB。也就是说在输出通带的高频段CIC自己就吞掉了5dB以上的信号。如果你的系统对带内平坦度有要求比如通带纹波小于0.5dB这个下垂是绝对不能接受的。而且这个问题没办法靠加大N来解决级数越高sinc滚降越陡下垂只会更严重。注意一点这里的估算使用的是小角度近似之前先算了具体三角函数值实际设计时直接用MATLAB的freqz验证更稳妥。但手算的好处是能直观看到每个参数的影响尤其是M和N对通带边缘衰减的放大作用。1.3 补偿滤波器放在抽取前还是抽取后补偿的目的是把CIC在通带内的非平坦频响拉平所以补偿滤波器的幅频特性理论上应该是CIC频响的倒数。摆在面前的问题是放在抽取之前还是之后。如果放在抽取前也就是放在80MHz的高速时钟域那么为了在1.5MHz处给出需要的补偿增益FIR滤波器必须处理80MHz的数据率更麻烦的是补偿频段相对于80MHz只占极窄的一段要用高阶FIR才能做到窄带整形DSP资源消耗会非常大。如果放在抽取后也就是5MHz的低速时钟域情况就友好得多数据率只有原来的1/16同样一个FIR滤波器只需要1/16的运算量而且补偿频段0~1.5MHz在5MHz采样率下占到了0~0.6的归一化频率滤波器设计起来很自然阶数也不会特别高。所以我这个链路采用的结构是CIC抽取16倍→ 补偿FIR → 后端处理。如果是做插值比如DUC结构镜像一下补偿FIR放在CIC插值之前作用完全对称。2. 补偿滤波器设计从MATLAB到系数定点化2.1 用fdesign.ciccomp确定FIR系数设计CIC补偿滤波器MATLAB里有现成的工具不需要自己去反解sinc。我的做法是直接用fdesign.ciccomp指定CIC级数N、微分延迟D、通带截止频率、阻带起始频率、通带纹波和阻带衰减一步到位。设计指标定为通带0~1MHz阻带起始1.75MHz通带纹波0.25dB阻带衰减50dB采样率按输出端5MHz给。fs_in 80e6; M 16; N 4; D 1; fs_out fs_in / M; Fp 1e6; % 通带边缘 1MHz Fst 1.75e6; % 阻带起始 1.75MHz Ap 0.25; % 通带纹波 0.25dB Ast 50; % 阻带衰减 50dB Hd fdesign.ciccomp(N, D, Fp, Fst, Ap, Ast, fs_out); d design(Hd, equiripple); % 查看设计结果 fvtool(d); fprintf(滤波器阶数: %d\n, length(d.Numerator) - 1);用等波纹法设计出来阶数大概在三十几阶对称FIR实际硬件只需要一半左右乘法器。这里要注意fdesign.ciccomp的Fp、Fst是用输出端采样率fs_out归一化的实际频率不是归一化到奈奎斯特我给的是绝对频率函数内部会自行转换。设计完成之后建议立刻做一次级联验证确认CIC补偿后的整体响应真的平了。我会生成CIC的理想频响再乘上补偿FIR的频响n 0:4095; f n / 4096 * fs_out; % 输出端频率轴 % CIC理想频响用公式直接算 cic_resp abs(sin(pi*M*D*f/fs_in) ./ (M*D*sin(pi*D*f/fs_in))).^N; % 防除零 cic_resp(f0) 1; % 补偿FIR频响 [Hfir, w] freqz(d.Numerator, 1, 4096, fs_out); % 级联 total cic_resp .* abs(Hfir).; figure; plot(f, 20*log10(cic_resp), r); hold on; plot(f, 20*log10(abs(Hfir)), g); plot(f, 20*log10(total), b); legend(CIC,补偿FIR,级联); grid on; xlim([0 1.8e6]);级联后的响应在1MHz以内应该被压在0dB附近纹波基本在0.25dB以内。看到这个结果再往下做Verilog心里就有底了。2.2 系数定点化别把小系数直接量化成整数MATLAB设计出来的系数是浮点小数Verilog里要定点化。这里新手最容易踩坑直接把0.012这种小数乘以2^15再四舍五入如果忘了归一化系数会全部变成0或者只有几个非零值滤波器整个失效。正确做法是先把系数按最大绝对值归一化到1再量化到目标位宽coef d.Numerator; % 归一化到最大绝对值1 scale max(abs(coef)); coef_norm coef / scale; % 量化到16bit有符号 COEF_WIDTH 16; max_val 2^(COEF_WIDTH-1) - 1; coef_int round(coef_norm * max_val); % 检查量化后的频响 Hq freqz(coef_int, 1, 4096, fs_out);量化之后要再跑一次频响对比确认量化误差没有造成通带纹波超标。16bit系数对于这个应用场景基本够用。关于直流增益还要单独核对一下。如果补偿滤波器在0Hz附近的归一化增益是1定点化后可能变成0.9997或者1.0004问题不大。但如果你前面做了“先归一化再量化”的操作实际滤波器增益已经整体乘了一个scale系数也就是说输出信号会被整体放大。这个需要在链路里统一考虑否则你会看到补偿后输出幅度比预期大还以为补偿过度了。最简单的方法是把补偿FIR的输出再做一次右移截位把scale的影响吸收掉我后面详细说。3. Verilog实现CIC抽取链路与补偿FIR3.1 CIC抽取滤波器RTL实现CIC抽取的经典实现积分器跑在高速时钟抽取之后梳状器跑在低速时钟。但实际工程里我更推荐所有的寄存器仍然用同一套高速时钟只是用使能信号控制抽取和梳状器的工作节奏。这样避免了多时钟域带来的跨时钟处理问题代码也更好调试。参数化设计级数、抽取倍数、位宽都做成parametermodule cic_decimator #( parameter N 4, // CIC级数 parameter M 16, // 抽取倍数 parameter D 1, // 微分延迟 parameter DIN_WIDTH 16, parameter ACC_WIDTH 32 // 内部累加器位宽 )( input wire clk, input wire rst_n, input wire signed [DIN_WIDTH-1:0] din, input wire din_valid, output wire signed [DIN_WIDTH-1:0] dout, output wire dout_valid ); // 积分器链 reg signed [ACC_WIDTH-1:0] integ [0:N-1]; integer i; always (posedge clk or negedge rst_n) begin if (!rst_n) begin for (i 0; i N; i i 1) integ[i] d0; end else begin if (din_valid) begin integ[0] integ[0] din; for (i 1; i N; i i 1) integ[i] integ[i] integ[i-1]; end end end // 抽取计数 reg [$clog2(M)-1:0] cnt; wire dec_en; always (posedge clk or negedge rst_n) begin if (!rst_n) cnt d0; else if (din_valid) cnt cnt 1b1; end assign dec_en (cnt M-1) din_valid; // 梳状器链在抽取使能下工作 reg signed [ACC_WIDTH-1:0] comb_d [0:N-1]; reg signed [ACC_WIDTH-1:0] comb [0:N-1]; always (posedge clk or negedge rst_n) begin if (!rst_n) begin for (i 0; i N; i i 1) begin comb_d[i] d0; comb[i] d0; end end else if (dec_en) begin comb_d[0] integ[N-1]; comb[0] integ[N-1] - comb_d[0]; for (i 1; i N; i i 1) begin comb_d[i] comb[i-1]; comb[i] comb[i-1] - comb_d[i]; end end end // 输出截位与有效标志 localparam SHIFT $clog2((M*D)**N); // 等价于 N*$clog2(M*D) wire signed [ACC_WIDTH-1:0] trunc_out $signed(comb[N-1]); // 截位算术右移保留符号 reg signed [DIN_WIDTH-1:0] dout_r; reg dout_v_r; always (posedge clk or negedge rst_n) begin if (!rst_n) begin dout_r d0; dout_v_r 1b0; end else begin dout_v_r dec_en; if (dec_en) dout_r trunc_out[ACC_WIDTH-1 -: DIN_WIDTH]; end end assign dout dout_r; assign dout_valid dout_v_r; endmodule这里有几个关键点先划一下。内部位宽ACC_WIDTH必须按Hogenauer提出的“有限精度字长”公式来算ACC_WIDTH DIN_WIDTH N * ceil(log2(MD))。用我的参数就是16 44 32bit。少了不行累加器会溢出仿着仿着输出就开始跳变很多人以为是电路不稳定其实是位宽不够。输出截位我用了最简单的“直接取高16bit”也就是算术右移16位。这种方式实现简单但会带来直流偏置因为截断相当于向负无穷取整。如果对直流精度有要求正确的做法是加上一个常数偏置再做截位即“rounding”。演示场景我直接截断后面会看到低频分量会有一点点偏差但不会影响对补偿效果的判断。3.2 补偿FIR滤波器实现补偿FIR我放在CIC之后工作在5MHz数据率下。滤波器阶数三十多阶用全并行转置结构。FIR是对称系数理论上可以只算一半乘法器但代码会复杂一些。先给一个对新手友好的通用转置结构用generate循环例化乘加单元module fir_compensator #( parameter TAPS 35, parameter DIN_WIDTH 16, parameter COEF_WIDTH 16, parameter ACC_WIDTH 35 )( input wire clk, input wire rst_n, input wire signed [DIN_WIDTH-1:0] din, input wire din_valid, output wire signed [DIN_WIDTH-1:0] dout, output wire dout_valid ); wire signed [COEF_WIDTH-1:0] coef [0:TAPS-1]; wire signed [DIN_WIDTHCOEF_WIDTH-1:0] prod [0:TAPS-1]; reg signed [ACC_WIDTH-1:0] acc [0:TAPS-1]; // 系数例化用本地参数写入二分之一的系数这里为简化只留说明 // 实际工程中用 include 文件或者 readmemh 读系数表 genvar g; generate for (g 0; g TAPS; g g 1) begin : tap_gen if (g 0) begin always (posedge clk or negedge rst_n) begin if (!rst_n) acc[g] d0; else if (din_valid) acc[g] din * coef[g]; end end else begin always (posedge clk or negedge rst_n) begin if (!rst_n) acc[g] d0; else if (din_valid) acc[g] acc[g-1] din * coef[g]; end end end endgenerate // 取累加链最终值截位输出 reg signed [DIN_WIDTH-1:0] dout_r; reg dout_v_r; always (posedge clk or negedge rst_n) begin if (!rst_n) begin dout_r d0; dout_v_r 1b0; end else begin dout_v_r din_valid; if (din_valid) dout_r acc[TAPS-1][ACC_WIDTH-1 -: DIN_WIDTH]; end end assign dout dout_r; assign dout_valid dout_v_r; endmodule注意这个代码里我用了一个乘加单元串起来的结构其实每个tap都乘了系数。FPGA综合时会把它映射到DSP48里一个tap一个DSP48。对于35阶FIR来说35个DSP48看起来不少但工作在5MHz下完全是杀鸡用牛刀。如果你在高速率下做可以考虑串行MAC结构用一个DSP48分时复用省资源但控制逻辑会增加。低速率这里就直接全并行简单可靠。还有一个细节这个FIR代码中最后一个tap的acc是整个乘加累加链的最终结果它包含了所有35个乘加项的求和。转置结构天然适合流水线每一级的寄存器都插在乘法器之后时序压力很小。3.3 位宽与截位策略位宽是整个链路最容易翻车的地方。我分三段讲CIC内部、CIC到FIR的接口、FIR输出。CIC内部位宽用32bit前面说过。这32bit是“保命”的累加过程中不允许发生饱和溢出否则仿真波形就会出现异常跳变。CIC输出截位到多少给FIR一种选择是直接按公式截到16bit也就是把增益65536消掉。这样做的问题是固定增益完全恢复补偿FIR的16bit输入动态范围正好覆盖输入幅度看起来合理。但工程上我更推荐加一点余量CIC输出截到18bit故意保留2bit额外精度因为补偿滤波器在1.5MHz附近有5dB以上的通带增益如果CIC输出已经顶到满偏FIR里一放大就容易截顶失真。多留2bit相当于多了12dB动态范围可以有效避免这个问题。FIR输出的累加器位宽要仔细算输入18bit系数16bit两者乘积34bit35阶累加需要再留ceil(log2(35))≈6bit所以累加器至少40bit。我代码里写的是ACC_WIDTH35专门注明是简化版。真正做的时候建议直接取DIN_WIDTHCOEF_WIDTH$clog2(TAPS)2这个2是给中间峰值和舍入用的。最后再把这个40bit累加结果截到16bit输出。截的时候用一个保守的饱和逻辑如果超出16bit范围就钳到最大值或最小值避免回绕。4. 仿真演示从Testbench到频谱验证4.1 测试平台搭建与输入激励生成测试信号我用两个正弦叠加1MHz和1.5MHz幅度分别设置为0.3和0.316bit下大概9830采样率80MHz。之所以选这两个频率是为了覆盖补偿滤波器通带内的高低两端能一次性看出CIC下垂和补偿效果。Verilog里很难直接算正弦函数我的做法是先用MATLAB生成定点正弦采样值写到文本文件里testbench里用$readmemh读进ROM再按地址循环输出。这个方法通用性强后面换任意激励信号都方便。fs_in 80e6; N_samples 65536; % 2^16个点 t (0:N_samples-1)/fs_in; sig 0.3*sin(2*pi*1e6*t) 0.3*sin(2*pi*1.5e6*t); % 量化到16bit有符号 qsig round(sig * 32767); fid fopen(stim_hex.txt,w); for k 1:N_samples fprintf(fid, %04x\n, qsig(k) 16hFFFF); end fclose(fid);注意这里存的是16bit有符号数的二进制补码用%04x输出16进制testbench读进来之后要转成signed。如果直接把%04x读到reg里再赋给signed端口本身不会错但要保证高位符号bit被正确解释。Testbench的基本结构module tb_cic_comp; reg clk; reg rst_n; reg signed [15:0] din; reg din_valid; wire signed [15:0] cic_dout; wire cic_valid; wire signed [15:0] fir_dout; wire fir_valid; // 激励ROM reg [15:0] stim_rom [0:65535]; integer idx; initial begin $readmemh(stim_hex.txt, stim_rom); idx 0; end // 80MHz时钟 always #6.25 clk ~clk; // 输入激励 always (posedge clk or negedge rst_n) begin if (!rst_n) din_valid 1b0; else begin din_valid 1b1; din $signed(stim_rom[idx]); idx (idx 65535) ? 0 : idx 1; end end // CIC cic_decimator #(.N(4), .M(16), .D(1)) u_cic (.clk(clk), .rst_n(rst_n), .din(din), .din_valid(din_valid), .dout(cic_dout), .dout_valid(cic_valid)); // 补偿FIR fir_compensator #(.TAPS(35), .DIN_WIDTH(16), .COEF_WIDTH(16), .ACC_WIDTH(40)) u_fir (.clk(clk), .rst_n(rst_n), .din(cic_dout), .din_valid(cic_valid), .dout(fir_dout), .dout_valid(fir_valid)); // 导出输出数据 integer out_file; initial out_file $fopen(fir_out.txt,w); always (posedge clk) begin if (fir_valid) $fwrite(out_file, %d\n, fir_dout); end // 复位 initial begin clk 0; rst_n 0; #100 rst_n 1; #200000 $finish; end endmodule仿真时长怎么算输出端5MHz做4096个输出点才能得到足够频率分辨率的FFT对应输入时钟数4096*1665536个80MHz下是819.2us。加上复位时间跑到1ms完全够用。ModelSim里这个规模很快就跑完。4.2 仿真波形怎么看波形打开之后很多人看到CIC内部的integ信号是一堆往上爬的锯齿会吓一跳以为代码错了。实际上这是积分器的正常行为每个时钟把输入累加进去看起来像一个大三角波抽取时刻瞬间抓拍到积分值。这个“锯齿”只要没有突然跳变到正负满幅来回翻就说明位宽还够。再看CIC输出cic_dout应该是两个频率叠加的包络正弦在抽取有效时输出一个点非有效时保持上一个值。如果你把cic_dout接到普通wire上会看到每个输出点之间有一段“保持”这是正常的。dout_valid信号的脉冲位置就是有效输出点。补偿FIR输出fir_dout应该是更干净的正弦幅度比CIC输出略有变化1.5MHz分量会被明显放大。波形上的直观效果是包络的形状基本不变但高频分量的占比变大了。如果我能把两个输出的频谱并排放在一张图上效果一目了然CIC输出的频谱呈现低通特性1.5MHz分量比1MHz分量低约3dB补偿后两个谱线基本持平。4.3 用MATLAB做FFT验证ModelSim/QuestaSim里面看时域波形只能算初步验证要确认补偿效果必须把数据导出来做频谱分析。把上面写的fir_out.txt用MATLAB读进来% 读取CIC输出和FIR输出 cic_data load(cic_out.txt); fir_data load(fir_out.txt); % 先去掉前100个点等流水线填满再分析 Nfft 4096; window hann(Nfft); spec_cic fft(cic_data(100:Nfft99).*window, Nfft); spec_fir fft(fir_data(100:Nfft99).*window, Nfft); f (0:Nfft-1)/Nfft * 5e6; % 输出采样率5MHz figure; subplot(2,1,1); plot(f, 20*log10(abs(spec_cic(1:Nfft/2))1e-12)); title(CIC output); xlabel(Hz); ylabel(dB); xlim([0 2.5e6]); grid on; subplot(2,1,2); plot(f, 20*log10(abs(spec_fir(1:Nfft/2))1e-12)); title(CIC Compensation FIR output); xlabel(Hz); ylabel(dB); xlim([0 2.5e6]); grid on;注意MATLAB中20*log10里要加一个极小值防止对数取0。窗口最好加Hann窗否则频谱泄漏会让相邻谱线拖尾看起来不够干净。预期结果用一张表概括信号频率CIC理论衰减CIC仿真测得的相对幅度补偿后相对幅度1MHz-2.3 dB-1.9~-2.5 dB0 ± 0.25 dB1.5MHz-5.3 dB-4.8~-5.6 dB0 ± 0.5 dB这个相对幅度是拿该频率的谱线幅度和0.3幅度输入信号的理论值比较得到的。因为MATLAB生成的激励幅度准频谱归一化之后可以直接换算。如果补偿后两条谱线在1dB以内说明整个链路工作正常。如果偏差很大先回头检查定点系数是否加载正确再看截位是否引入了异常增益。5. 常见问题排查与工程建议5.1 仿真发散、输出越界是位宽问题“仿真发散”这个词在CIC这种纯数字电路里其实不太准确。CIC没有反馈型的递归环路带来的稳定性问题它只有一个积分器链极点都落在单位圆上理论上是临界稳定。但仿真里你会看到输出越来越大、最终翻掉本质上就是累加器位宽不够导致溢出回绕。排查思路很简单第一确认ACC_WIDTH算对没有Nlog2(MD)一定要取上整而且是按M*D的整数结果算第二看输入信号是不是包含直流直流会让积分器持续累加比正弦信号占位宽更快第三仿真跑长时间后输出突然跳变基本可以断定是位宽问题。如果输入是满幅正弦16bitM16N4ACC_WIDTH32只是刚好不溢出的底线实际建议再加1~2bit安全余量。FPGA资源对这点位宽根本不敏感多几bit换来的是调试时少掉很多头发。5.2 Modelsim波形红线、X态怎么查ModelSim/QuestaSim里看到波形是红线其实就是信号处于X态。新手最容易在这个问题上卡住。最常见的原因按概率排序复位没有正常释放、时钟没有真正翻转、端口连接错误、多驱动冲突。排查方法和顺序我建议固定下来看rst_n波形是不是在仿真开始后拉高了。看clk波形有没有正常翻转频率是否正确。把CIC输入端口din、din_valid拉出来看确认激励有没有进来。检查顶层例化时信号名有没有拼错这是低级错误但高频发生。如果有多个模块同时驱动同一个wire综合器会报warning仿真器会显示X。还有一个隐蔽问题我给的CIC代码中输出截位用了trunc_out[ACC_WIDTH-1 -: DIN_WIDTH]这种位选语法在旧版ModelSim里可能解析有问题。如果编译报错就改成trunc_out[ACC_WIDTH-1:ACC_WIDTH-DIN_WIDTH]。5.3 全链路性能不达预期先别怀疑代码如果仿真能跑通但补偿后频率响应还是不平先别急着改Verilog。我的建议是按“浮点算法→定点算法→RTL仿真”三层逐一排查。先回到MATLAB确认CIC理想频响和补偿FIR级联后的响应在通带内确实平了。如果浮点层面就不平那就是设计指标或系数问题跟Verilog无关。再用浮点系数量化后的系数在MATLAB里重新跑一遍频响确认16bit量化误差可接受。最后再回到RTL仿真结果做对比。很多时候我发现问题出在“系数加载顺序”。FIR系数的排列顺序是h[0], h[1], ..., h[N-1]对应输入信号延迟链从近到远。转置结构里系数顺序如果反了滤波器会变成另一个响应看起来也是“不平”但频响完全错误。还有一点经验我看频谱对比时不会只看单次FFT的谱线工程上会跑多次仿真、用不同随机相位测试或者输入扫频信号这样能逼出所有两边不靠的问题。5.4 关于插值CIC的一个补充这次做的是抽取链路但如果你做的是插值链路也就是CIC插值滤波器结构上几乎是对称的补偿FIR放在插值之前CIC梳状器在低速端、积分器在高速端。系数设计仍然可以用fdesign.ciccomp只是注意频率轴是插值前还是插值后别搞混。插值场景下我建议在补偿FIR后面加一个限幅模块防止CIC内插峰值过高导致输出clipping。这个原理跟抽取补偿基本相同。最后再分享一点个人体会CIC加补偿FIR这套链路原理不难代码量也不大真正费时间的往往是那些看起来不起眼的细节位宽、截位、系数顺序、仿真数据导出。我自己第一次做这个演示的时候卡在截位上卡了两天CIC输出直接截到16bit补偿后1.5MHz的信号失真严重当时还以为是补偿滤波器设计错了。后来把CIC输出改成18bit再进FIR所有问题迎刃而解。所以给看到这里的同学一个建议如果仿真结果和理论对不上先确认每一级输出的位宽和截位是否和你预期一致而不是一头扎进CIC原理里抠公式。好的工程师一半时间在写代码另一半时间在给位宽做体检。把这条链路完整做通一遍你对多速率信号处理的很多模糊感觉都会变得清晰很多。