news 2026/9/15 12:12:43

CIC数字下变频抽取滤波器:从MATLAB建模到FPGA实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CIC数字下变频抽取滤波器:从MATLAB建模到FPGA实现

简介:面向数字信号处理学习者的DDC(数字下变频)CIC滤波器实现资料,聚焦利用CIC积分梳状滤波器完成射频/中频信号到基带的变频处理,涵盖MATLAB脚本、Simulink模型及VHDL源码,适合通信、雷达等方向学生与工程师对照学习。压缩包共6个文件,约26KB,包含2个VHDL文件、1个Simulink模型、1个MATLAB脚本、1个Verilog文件及1个TXT说明文本,覆盖从CIC滤波器设计、下变频仿真到FPGA实现的完整链路。已有386人学习,资源虽小但结构完整,既能用于理解CIC下变频原理与参数配置,也可作为DSPBuilder协同仿真或硬件综合的参考模板。通过分析源码与模型,读者可掌握混频、积分梳状滤波、降采样等关键步骤,并借鉴其工程化实现思路。

1. 数字中频信号落地前的那一级 CIC

接手一块 140MHz 中频采样的采集板,第一件事不是直接上几百阶 FIR,而是先把数据率降下来。之前做过一版纯 FIR 抽取,4 倍抽取就吃掉十几个 DSP48E,板子温度直接上去了。后来换成 CIC 抽取级,逻辑单元只占原来三分之一,性能却够用。这个工程里的fdcoeffR4N8M1L110.txtciccomp.m,其实就把一套典型链路从参数到补偿系数都配好了:R=4 的抽取率、8 级级联、M=1 差分延迟,加上 110 点补偿 FIR。对做数字中频接收、软件无线电或雷达信号处理的人来说,这是一套能直接从 MATLAB 仿真挪到 RTL 实现的模板。

2. CIC 频率响应从哪里来:零极点位置决定抽取安全性

2.1 积分器与梳状器的级联模型

CIC 滤波器的结构看起来只是“累加 + 延迟相减”的组合,但它的频响完全由零极点分布决定,理解这一点才能解释为什么它适合放在抽取器前面。

单级积分器在 z 域的系统函数是:

H_I(z) = 1 / (1 - z^{-1})

在单位圆 z=1 处有一个极点,对应零频增益无穷大(实际数字系统里就是直流增益为 R)。梳状器的系统函数是:

H_C(z) = 1 - z^{-RM}

其中 R 是抽取倍数,M 是差分延迟,通常取 1 或 2,它的零点均匀分布在单位圆上,角度间隔是 2π/(RM)。把 N 级积分器和 N 级梳状器级联后,系统函数写成:

H(z) = [ (1 - z^{-RM}) / (1 - z^{-1}) ]^N

这个式子约掉零极点之后,等价于一个长度为 RM 的移动平均滤波器迭代 N 次。也就是说,CIC 本质上是一种递归结构的 FIR 等效实现,不需要乘法器,只有加法器和寄存器。之前调过的另一个工程里把 M 从 1 改成 2,零点就减半,滚降变缓,但混叠泄漏明显加重,所以 M 不是随便拍的。

2.2 为什么抽取前用 CIC 而不是直接抗混叠 FIR

抽取操作最怕的是带外信号折返进通带。理论上抽取前需要一个低通滤波器,截止频率在折叠频率以下。如果直接用 FIR,阻带衰减做到 60dB 以上需要上百个抽头,系数复杂不说,时钟一高乘法器就吃紧。CIC 的优势在于系统函数已经保证了在抽取倍数整数倍频点上有零点,也就是说,混叠源所在的频点恰好被陷掉一部分。

以 R=4、M=1 为例,梳状零点在 0、fs/4、fs/2、3fs/4 处。抽取后折叠发生在 0、fs/4、fs/2 这些频点附近,零点和折叠点重合,所以只要不追求极高的阻带衰减,CIC 的抗混叠能力是够用的。如果对带外抑制要求到 70dB 以上,才需要在 CIC 后面再补一级 FIR 或者把 N 提高。注意 N 提高会带来两个副作用:通带滚降加深,以及增益按 R^N 指数增长,后者直接决定位宽。

下面这段 MATLAB 代码画出不同级数下的幅频响应,可以直观看到 N 从 1 到 8 的变化趋势:

% cic_spectrum.m CIC频响对比,R=4,M=1,N=1/4/8 R = 4; M = 1; Nlist = [1 4 8]; f = linspace(0, 0.5, 2048); % 归一化频率 for N = Nlist % CIC频响公式:等价的sinc^N形式 H = (sin(pi * f * R * M) ./ (R * M * sin(pi * f))) .^ N; H(1) = (R * M) ^ N; % 直流处的极限值 plot(f, 20*log10(abs(H) / abs(H(1)))); hold on; end hold off; xlabel('归一化频率'); ylabel('幅度 (dB)'); grid on; legend('N=1', 'N=4', 'N=8');

这段脚本里sin(pi * f * R * M) ./ (R * M * sin(pi * f))是 CIC 频响的闭式表达,直接用 MATLAB 的freqz按差分方程算也可以,但容易遇到大数溢出的警告。H(1)单独赋值为(R*M)^N是因为 0/0 在数值上算不出来,这个值对应直流增益。从图里能看出 N=8 时通带边缘已经明显下垂,所以后面必须补一个 CIC 补偿滤波器。

2.3 增益、位宽与截断策略

CIC 的直流增益是(R*M)^N。以这套参数 R=4、N=8、M=1 计算,增益是 4^8 = 65536,相当于 16bit 的增益。如果 ADC 输出是 16bit,全精度输出要 32bit 才不丢信息。实际工程里没人会把 32bit 全往下传,通常只保留高有效位,截断会带来量化噪声,但噪声电平的绝对值远低于 CIC 自身通带滚降带来的信号失真。

参数组合RMN直流增益3dB 带宽(归一化)硬件成本
配置 A(本工程)41865536约 0.0816 级寄存器 + 16 个加法器
配置 B4151024约 0.1410 级寄存器 + 10 个加法器
配置 C82465536约 0.058 级寄存器 + 8 个加法器,但混叠点较少

配置 B 适合对通带回平要求不高、逻辑紧张的场合;配置 C 虽然零点和配置 A 一样多,但 M=2 导致第一零点频率减半,对低通性能要求高的场景不建议。选择时先用freqz把滚降点落到信号带宽的 1.5 倍以上,再回头定级数,这是我处理这类问题时一贯的顺序。

提示:CIC 不是“下变频器”,它本身不做频谱搬移。真正把中频搬到基带的是坐标旋转数字计算(CORDIC)或者查表法本地振荡器(NCO)完成的混频。CIC 负责的是混频之后的抽取滤波,两者职责别混。

3. 从 fdcoeff 到 TDMDDC:MATLAB 建模与补偿系数生成

3.1 先读懂 fdcoeffR4N8M1L110.txt 里存的是什么

这组文件里fdcoeffR4N8M1L110.txt的名字已经给出了大部分信息:R=4 表示抽取率,N=8 表示 CIC 级数,M=1 表示差分延迟,L=110 对应的是一组长度为 110 的 FIR 补偿系数。我一般先把它读进来做归一化检查,避免后面级联时幅度对不上:

head -n 5 fdcoeffR4N8M1L110.txt

如果文件开头是这样的格式(每一行一个十进制浮点数):

0.001352081234 0.001491172345 ...

那就是纯系数文本。如果只有一行且用空格分隔,用load('fdcoeffR4N8M1L110.txt')也能读,MATLAB 会把它按矩阵解析。读进来后先确认length(coeff)是不是 110,以及sum(coeff)是否接近 1。补偿滤波器通常归一化到直流增益为 1,这样级联后整体增益只由 CIC 部分决定。

3.2 ciccomp.m 到底做了什么

ciccomp.m这名字在国内不少教科书工程里出现过,常见实现是用fdesign.ciccomp设计等波纹补偿 FIR,它针对 CIC 的sinc形状取倒数匹配。我通常这样写:

% ciccomp.m 设计CIC补偿滤波器并保存系数 R = 4; M = 1; N = 8; % 与fdcoeff文件名对应 Fp = 0.05; % 通带截止频率(归一化) Fst = 0.0625; % 阻带起始频率,取1/(2R)-一点余量 Ap = 0.01; % 通带纹波 0.01dB Ast = 60; % 阻带衰减 60dB d = fdesign.ciccomp('decimator', N, M, R, Fp, Fst, Ap, Ast); Hd = design(d, 'equiripple'); coeff = Hd.Numerator; fprintf('长度: %d\n', length(coeff)); % 保存系数,供Simulink或RTL使用 fid = fopen('fdcoeffR4N8M1L110.txt', 'w'); fprintf(fid, '%.12f\n', coeff); fclose(fid);

这里fdesign.ciccomp('decimator', N, M, R, ...)的关键参数前三项是 CIC 的级数 N、差分延迟 M、抽取率 R,这几个值必须和实际用的 CIC 完全一致,差一个都会导致补偿曲线错位。实际验证时,如果发现补偿后通带仍有 0.5dB 以上的起伏,多半是 Fp 设置得太接近通带边缘,导致补偿 FIR 在边缘处过度增益,放大了带外噪声。

3.3 Simulink 模型 TDMDDC.mdl 的链路组织

打开TDMDDC.mdl或者DSPBuilder_TDMDDC_import对应的工程,典型的 DDC 链路是这个顺序:

ADC输入(或仿真源) → NCO混频 → CIC抽取级(R=4,N=8,M=1) → 增益归一化(右移或乘法) → ciccomp FIR补偿 → 基带输出

注意 NCO 混频不只是乘一个余弦,实信号输入时通常用两路正交本地振荡器输出 I/Q 两路,分别对应余弦和正弦。混频后频谱被搬到 0 频附近,但也会有一半分量落在两倍中频处,这部分靠后面的 CIC 和 FIR 滤除;如果中频落在 fs/4 附近,经典的取法是用 [1, 0, -1, 0] 和 [0, 1, 0, -1] 的序列做混频,整个混频级就不需要查表乘法器,DSP Builder 里可以直接用常数序列实现。

链路里 CIC 级联之后别忘了做一次位宽压缩。这里给出用 MATLAB System object 搭建等效链路的脚本:

% ddclink_sim.m 数字下变频链路仿真:NCO混频 + CIC抽取 + 补偿 fs = 100e6; % 采样率 R = 4; t = (0:4095).' / fs; f_if = 20e6; % 中频 x = cos(2*pi*f_if*t) + 0.1*randn(size(t)); % 中频信号+噪声 nco_i = cos(2*pi*f_if*t); % 正交本振 nco_q = -sin(2*pi*f_if*t); % 负频率搬移 i_bb = x .* nco_i; q_bb = x .* nco_q; % CIC 抽取:R=4, N=8, M=1 cic = dsp.CICDecimator(R, M, 8); % 第三个参数是级数 i_dec = cic(i_bb); q_dec = cic(q_bb); % 归一化并送入补偿滤波器 i_comp = filter(comp_coeff, 1, i_dec); q_comp = filter(comp_coeff, 1, q_dec);

这段脚本中dsp.CICDecimator的前两个参数分别是抽取率和差分延迟,第三个参数是级数,默认是 2 级,这里必须显式写 8。如果仿真结果发现 I/Q 两路幅度不一致,检查 NCO 初始相位是否对齐,CIC 本身是不分相位的,问题几乎总出在混频器上。

提示:dsp.CICDecimator默认内部采用双精度浮点,仿出来的波形不会暴露位宽截断问题。真要评估硬件行为,需要在tdmddc.mdl里将 CIC 的滤波结构设成定点,并把输出位宽设成和 RTL 一致,否则最后上板时 SNR 会掉 3~5dB。

4. 到 RTL 的最后一跳:DSP Builder 导入与 VHDL 接口对照

4.1 DSPBuilder_TDMDDC_import 的定位

DSPBuilder_TDMDDC_import是把 Simulink 定点模型转换为 VHDL/Verilog 的入口工程。DSP Builder 导出的代码默认是 Avalon-ST 接口风格,也就是说数据通路带validready握手信号。你如果过去只写过普通 FIFO 接口的滤波器,这一节要重点看握手时序,它的ready信号并不是每一拍都为高。

处理这类导入模型的步骤是这样的:

  1. 在 Simulink 里双击DSPBuilder_TDMDDC_import模型,确认信号采样率标注正确;
  2. 用 DSP Builder 的Signal Compiler配置目标器件和时钟频率,比如 EP4CE115 或 Cyclone V;
  3. 生成 VHDL 之后,在 Quartus 工程里把生成目录下的所有.vhd加入工程,替换掉手写的cic.vhdfir.vhd之间的手工连接。

4.2 cic.vhd 端口与时序关系

手写工程里cic.vhd的顶层接口一般是这样的默认结构:

-- cic.vhd R=4, N=8, M=1 抽取滤波器顶层 entity cic is port ( clk : in std_logic; rst_n : in std_logic; data_in : in std_logic_vector(15 downto 0); data_out : out std_logic_vector(23 downto 0); -- 截断后16bit常用,这里预留高位 valid_in : in std_logic; valid_out: out std_logic ); end entity;

数据流内部是两级循环:先跑 8 级积分器,每级是一个累加器;随后进入抽取器,每抽 4 个点输出一个点;最后进入 8 级梳状器,梳状器是y[n] = x[n] - x[n - 1]的延迟相减。这里最容易出错的是抽取使能的生成,常见做法是:

-- 抽取使能:每R拍产生一个高电平 process (clk, rst_n) begin if rst_n = '0' then count <= (others => '0'); clk_en <= '0'; elsif rising_edge(clk) then if count = R_M1 then -- R-1 = 3 count <= (others => '0'); clk_en <= '1'; else count <= count + 1; clk_en <= '0'; end if; end if; end process;

这段代码里的R_M1是常量R - 1 = 3,抽取使能clk_en每 4 个输入时钟产生一个高电平。逻辑上没问题,但有一个隐蔽坑:当data_invalid_in不是持续为高时,这个自由运行的计数器会把无效数据也计入抽取周期。正确做法是把valid_in与计数器自增条件做与:

if count = R_M1 then count <= (others => '0'); clk_en <= '1'; elsif valid_in = '1' then count <= count + 1; else clk_en <= '0'; end if;

这样即使前端丢数据,抽取节奏仍然和数据帧对齐。之前在一个卫星接收机项目里就因为这个漏了valid_in条件,导致解出的符号出现周期性误码,排查了两天才定位。

4.3 Avalon-ST 接口与 alt_avalonst_pfc_0.v 的衔接

alt_avalonst_pfc_0.v在工程里起的是 Avalon-ST 数据打包/解包的作用,它把来自 AD 的数据流规整成 DSP Builder 后端方便处理的时序格式。Avalon-ST 最重要的四个信号是validreadysopeopsopeop分别是包起始和包结束标志,滤波器本身不关心包边界,但下游解调模块会把它们作为帧同步信号用。

信号方向含义注意点
avst_sink_valid输入上游数据有效必须和data一起变化
avst_sink_ready输出本模块可接收数据为低时上游要停流
avst_sink_sop输入包起始每个包的第一个有效数据拉高
avst_sink_eop输入包结束包内可背靠背传输
avst_source_valid/ready输出/输入下游接口握手与 sink 侧可以完全异步

fir.vhd就是那个 110 阶补偿 FIR,它接收cic.vhddata_out,在内部按系数串行或半并行乘累加。如果直接沿用 MATLAB 生成的浮点系数,在 VHDL 里要先量化到 16bit 定点。量化后的系数和fdcoeffR4N8M1L110.txt里的原始值会有微小偏差,建议在仿真里把量化后频率响应打印出来看看阻带是否还能维持 50dB 以上。

提示:DSP Builder 生成的时序不会自动把 CIC 的高位宽输出截断到 FIR 能接受的范围。如果你在 RTL 里看到data_out(23 downto 8)这种写法,别惊讶,这是把 24bit CIC 输出右移 8 位取高 16bit,等效于除以 256。它对应 MATLAB 模型里的增益归一化。若复位后第一包数据幅度异常,检查这个移位是否设置在梳状器之后,放在积分器之前会导致直流漂移。

5. 验证 CIC 下变频的三种手段和一次资源优化

5.1 双音测试验证抽取后混叠位置

验证 CIC 下变频最实用的方法是双音测试:把两个频率相差很小的信号同时送入链路,观察输出频谱里是否有第三个或第四个互调峰。我常用的设置是:中频 20MHz,一个信号在 20.1MHz,另一个在 20.2MHz,经过 4 倍抽取后,输出采样率 25MHz,两个信号分别落在 0.1MHz 和 0.2MHz 处。如果抽取前滤波不到位,0.2MHz 信号经过折叠会出现在 0.05MHz 附近,频谱上会出现一个不属于原信号的峰。

pwelch看输出功率谱,正常情况下两个主峰相差应在 40dB 以上。若出现多余谱线,先调 N 或 M,N=8 还不够就把补偿 FIR 的阻带起始频率降低到1/(2R) * 0.8,也就是 0.0625 缩到 0.05。

5.2 检查时序和溢出

上板调试时,把cic.vhd的积分器输出用逻辑分析仪抓下来,观察最高位是否频繁翻转。CIC 积分器内部是 32bit 全精度累加,若你截断了低 8 位,在高信号幅度下溢出表现为直流电平跳变,频谱会在低频出现“毛刺裙”。一个简单检查法:在 Simulink 里把定点仿真位宽设成与 RTL 一致,然后把最大正弦幅度从满幅降到 -6dBFS,看输出 SNR 是否随之线性下降。SNR 不是线性下降而是突变,说明截断点位置不对,或梳状器之前存在中间截断。

5.3 逻辑紧张时的降阶策略

资源不够时我不会优先改抽取率,那会牵动整个后续处理。先试着把 N=8 降到 N=5,代价是阻带衰减变差约 12dB,但通带更平。此时要重新执行ciccomp.m,把 N 参数同步改掉,并让补偿 FIR 的阶数从 110 降到 80 左右,省下的 DSP 块给后端复数混频。换完参数后,把上一节的双音测试重新跑一遍,重点比较通带边缘频率点上的增益平坦度,只要带内纹波不超过 0.5dB,这个降阶方案就可以继续往下走,直到资源约束与性能共同找到那个平衡点。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 12:12:37

微电网多目标优化调度与NSDBO算法Matlab实现

1. 微电网多目标优化调度问题解析微电网作为分布式能源系统的重要实现形式&#xff0c;其优化调度直接关系到系统运行的经济性和可靠性。在实际工程中&#xff0c;我们需要同时考虑多个相互冲突的目标函数&#xff0c;这就形成了典型的多目标优化问题。微电网调度中最常见的三个…

作者头像 李华
网站建设 2026/9/15 12:09:50

静态代码分析工具横向对比:从ESLint到SonarQube,如何选型与落地

很多团队的代码质量管控&#xff0c;其实一直卡在一个很尴尬的位置&#xff1a;代码评审靠人眼盯&#xff0c;低级错误靠运行时炸出来&#xff0c;线上出问题再回头补测试。我在经历过几次“本地运行得好好的&#xff0c;一上线就被空指针打脸”之后&#xff0c;彻底意识到&…

作者头像 李华
网站建设 2026/9/15 12:07:56

小程序毕设项目:大学生课堂签到考勤信息化管理平台的设计与实现 基于微信小程序的教学考勤数据可视化系统 (源码+文档,讲解、调试运行,定制等)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/9/15 12:07:28

AI能自己变强吗:一场让大模型自己考试、自己判卷、自己复习实验

你有没有想过一个问题&#xff1a;一个每天都在跟外部世界打交道的AI&#xff0c;到底有没有从这些经历里学到东西&#xff1f;现在的大语言模型早就不只是聊天机器人了。它们会调用工具、执行代码、跟各种环境反复交互&#xff0c;产生大量的行为数据。听起来这应该是个宝库,毕…

作者头像 李华
网站建设 2026/9/15 12:06:18

C#泛型编程:核心原理与高效实践指南

1. 为什么C#泛型是每个开发者必须掌握的利器2005年随着.NET 2.0发布的泛型功能&#xff0c;彻底改变了C#开发者的编程方式。泛型允许我们创建类型安全的集合类和方法&#xff0c;避免了装箱拆箱的性能损耗。在实际项目中&#xff0c;泛型的使用场景无处不在——从简单的List集合…

作者头像 李华