简介:数字上变频(DUC)与数字下变频(DDC)是通信系统中实现采样率转换、频谱搬移的关键技术。这份基于FPGA的完整工程包面向通信/FPGA开发者和数字信号处理学习者,既覆盖基础原理,也给出可运行的硬件实现。工程围绕混频器、NCO数控振荡器、半带/FIR/CIC滤波器、增益级和复数/实数转换等模块展开,并对比了WiMAX系统中8~10阶低转换率采用FIR、更高转换率采用CIC的取舍;各处理模块均有独立控制线路,可单独使能,便于分步调试。资源共519个文件、21.1MB,源码以Verilog/VHDL为主,同时携带Vivado工程配置(.xpr/.xci/.bd)、约束(.xdc)、仿真脚本(.tcl/.do)、滤波器系数与初始化文件(.coe/.mif)以及综合网表、日志、批处理,覆盖从RTL设计、仿真验证到综合布局的完整流程。结合作者博文,以余弦信号上下变频为例演示DDC/DUC链路恢复原始信号,可在Vivado中直接打开工程,逐模块观察NCO本振、滤波抽取和增益控制。目前已有3548人学习,适合需要从算法到代码一体化掌握DDC/DUC的工程师与学生。 做通信或者信号处理的FPGA工程师,对DUC(数字上变频)和DDC(数字下变频)这两个词应该都不陌生。我自己第一次接触这两个概念时,被一堆采样率、抽取倍数、滤波器阶数绕得晕头转向,后来真正在Xilinx Artix-7上把一整条DUC/DDC链路跑通,才算把原理到实现彻底串起来。这篇东西就是基于那次项目经历的完整复盘,从原理讲到架构选型,再到每个核心模块的FPGA实现细节和调试踩坑,既适合刚接触通信物理层的FPGA开发者,也适合做DSP算法但想了解硬件落地方式的工程师。
1. 项目核心:数字变频到底在解决什么问题
1.1 为什么发射机和接收机都要“搬频率”
在无线通信系统里,基带信号通常只有几百kHz到几MHz的带宽,频率太低了,既没法通过天线有效辐射,也容易被各种低频噪声干扰。所以射频链路里必须有一级频率搬移:发射端把基带信号搬到高频载波上,接收端再把高频信号搬回低频。这个搬移动作,模拟域早就有了,就是模拟混频器加本振。
而数字上变频(DUC)和数字下变频(DDC)就是把这套搬移动作放到数字域来做。DUC负责把基带信号插值、滤波、搬移到中频,DDC则把ADC采进来的中频信号混频、滤波、抽取回基带。放在软件无线电、雷达、电子对抗这些场景里,DDC/DUC几乎是标配级的模块,没有它们,后面所有的数字信号处理都无从谈起。我这次做的项目,就是在一个中频信号上同时实现收发两条链路:发端是DUC,从基带2.048MHz插值到65.536MHz采样率,再搬移到16.384MHz中频;收端是DDC,反着来一遍。
1.2 DUC和DDC是同一面镜子的两面
从FPGA实现的角度看,DUC和DDC的模块是完全对称的。DDC核心三步:混频(乘以NCO产生的本振)、低通滤波、抽取。DUC则是:插值、滤波、混频。为什么DDC先混频再滤波抽取?因为抽取会带来频谱混叠,必须在抽取之前先把通带外信号滤干净。为什么DUC先插值后滤波?因为插值会在原频谱旁边产生镜像,必须用滤波器压掉才能出去。这个顺序是硬性的,不能反过来,这是整个设计里最关键的逻辑。
数学上,混频的本质就是乘以一个复指数exp(j2πf_c·t),在频域上把信号频谱整体搬移f_c。抽取是每隔R个点取一个点,对应频域上频谱做R倍周期延展,如果通带外噪声没滤干净,就会被折叠到带内来。插值是相邻点间插零,同样会让频谱产生周期性镜像。搞明白这两个动作的频域行为,后面看滤波器选型就轻松了。
1.3 数字方案对比模拟方案的本质优势
模拟变频最大的痛点是本振泄漏、载波漂移、温漂和镜像抑制差。做接收机的人应该都对镜像抑制校准深有体会,那真的是模拟域里很磨人的问题。数字变频把这些全部换成了代码:NCO(数控振荡器)的频率由相位累加器控制,频率分辨率轻松做到Hz级别,而且不受温度影响,也不会出现本振泄漏。换个频点就是更新一个频率控制字(FCW)的事,这在多频段、多模式通信设备里,简直是降维打击。
另外数字变频把原来一整套射频模拟器件变成几十行Verilog,体积、功耗、调试成本都大幅下降。ADC后面直接跟FPGA做DDC,DAC前面用FPGA做DUC,这是当前软件无线电最常见的架构,也验证了这套方案在工程上的可靠性和可维护性。
2. FPGA整体架构与选型思路
2.1 先把链路参数定下来
做FPGA实现之前,我习惯先把链路指标列成一张表,免得做一半发现指标互相冲突。我这次项目的实测参数如下:
- 基带采样率:2.048MHz,I/Q位宽16bit
- 中频频率:16.384MHz
- 最终采样率:65.536MHz(2.048的32倍)
- 信号带宽:100kHz
- DUC链路:三级HBF(每级2倍插值) + CIC(8倍插值) + 补偿FIR
- DDC链路:对应结构反过来,先CIC抽取8倍,再三级HBF各抽取2倍
为什么选这组数?关键在于2.048乘以32正好是65.536,而中频16.384MHz正好是最终采样率的四分之一。fs/4这个比例在数字变频里非常舒服,因为NCO输出cos(2π·16.384e6/65.536e6·n)就是cos(πn/2),序列值只有1、0、-1、0,混频器可以直接退化成信号选择、取反、置零操作,实现极其省资源。
2.2 FPGA选型与资源预算
我用的平台是Xilinx Artix-7系列(XC7A35T),只做单通道DUC和DDC的话,资源压力很小。最终实现下来,整条链路加起来不到1000个LUT,DSP48E1用了8个左右,块RAM用了两块做NCO查找表,组合下来资源占用非常轻松。如果换逻辑量更大的芯片当然也没问题,但我要说的是:别小看DUC/DDC,如果架构选错,比如用单级FIR做32倍插值,那DSP48的资源立马爆炸,所以选型之前先把架构定下来。
2.3 多级抽取/插值为什么是标配
这是整个项目里我觉得最值得讲清楚的一个点。如果想把32倍抽取在单级FIR里一步完成,按100kHz带宽和过渡带要求推算,FIR阶数可能得上千阶,工作频率还要跑在65.536MHz,乘法器用量直接爆炸。而拆成多级之后,每一级只处理一小步的采样率变换,滤波器阶数大幅下降。
拆分的逻辑是:抗混叠/抗镜像的要求取决于当前级的过渡带相对宽度,前端采样率高,通带相对较窄或者较宽,对应的滤波器要求反而松,后端采样率低,滤波器的计算压力也低。多级化小为大,总计算量通常能降到单级的四分之一甚至更低,这也是DDC/DUC链路里几乎都采用“HBF+CIC+补偿FIR”组合结构的根本原因。
3. 核心模块实现细节
3.1 NCO:数字本振的工程实现
NCO本质就是一个DDS(直接数字频率合成器):一个相位累加器每个时钟累加一个频率控制字(FCW),用累加器的高位作为查找表地址,输出正弦和余弦。记住下面这个公式就行:
FCW = f_out * 2^N / f_clkN是相位累加器位宽,我用的32位。以f_clk=65.536MHz、f_out=16.384MHz为例,FCW算出来是2^31,正好是满幅度的一半。相位累加器的高12位拿去查表,4096点正弦表用分布式RAM存,查表输出再经过一个乘法器完成混频,整个NCO模块不需要DSP资源。
这里有个工程细节必须提醒:相位截断(只用高12位而丢弃低20位)会带来很小的杂散,如果系统对杂散要求很高,两个办法,一是做相位抖动(dither),在累加器低位加一个随机序列来打散杂散;二是在查找表后面加一点滤波处理。正常情况下,12位相位分辨率带来的杂散已经低于-70dBc,多数通信系统够用了。
3.2 CIC滤波器参数计算和位宽设计
CIC(积分梳状滤波器)是全数字变频里最核心的模块,全部由加法器和寄存器组成,乘法器一个都不用,特别适合做大幅度抽取/插值。CIC做抽取时,N级积分器工作在高速端,N级微分器工作在低速端;CIC做插值时正好反过来,微分器在前、积分器在后。我这次项目用的CIC参数是:
- 抽取/插值倍数R = 8
- 微分延迟M = 1
- 级联数N = 4
CIC有一个绕不开的坑:增益很大,而且积分器是无限增益结构,位宽必须提前算足。增益公式是G = (R×M)^N,我这里的G = 8^4 = 4096。输出位宽的增量按下面的公式计算:
BitGrowth = ceil(N × log2(R × M))代入数值就是ceil(4×log2(8))=12位。输入是16bit,那么CIC内部至少需要16+12=28bit,实际实现中我还多留了1bit余量,用29bit跑。算到这里我要特别强调一句:输出位宽这个红线绝对不能省,省了CIC必然溢出,信号直接变成噪声。我见过不止一个新手在这个地方翻车。
CIC的幅频响应是sinc形状,通带内会有滚降,100kHz带宽内下降大概1dB左右,必须用一级FIR做补偿。补偿FIR我放在CIC之后,工作在低速端,这样乘法器压力小,还能顺便承担部分抗镜像滤波。
3.3 半带滤波器HBF与多相FIR
半带滤波器是做2倍抽取/插值最划算的FIR:近一半系数严格为零,乘法次数直接减半。我链路里三个2倍级都用了HBF,设计时直接用Matlab的filterDesigner生成系数,再导入Xilinx FIR Compiler IP。这里有几个实用经验:
先把通带边界设置为信号带宽,比如100kHz,把阻带起始频率设置在fs_new/2 - fpass这个位置,这样抽取后不会产生混叠。生成系数时记得加窗,用汉明窗或凯塞窗都可以,能有效压低阻带纹波。
如果采样率实在太高、FIR跑到时序吃紧,可以改成多相结构。多相分解会把一个N阶FIR拆成R个并行分支,每个分支只处理1/R的数据率,乘法器的时钟频率压力降下来,代价是控制逻辑复杂一点。这个项目里HBF的阶数不高,普通结构就够,但如果做宽带信号、采样率上到几百MHz,就必须认真考虑多相方案了。
3.4 链路逐级位宽与截位策略
我这条链路的位宽增长路径是:输入16bit → CIC输出28bit → 补偿FIR输出经过截位到20bit → 三级HBF逐级截位到16bit。截位不能一次砍太多,每级保留1~2bit余量,最后一级输出再重量化到16bit。这样逐级截位的策略,可以把每一级的量化噪声均匀地摊开,而不是最后一次性截断导致小信号直接淹没在噪声里。
实际验证时我会用Matlab对每个截位点做定点仿真,观察频谱噪声底是否升高太多,如果噪声底升高超过1~2dB就不合适,需要调整截位位置或保留更多位宽。这个习惯帮我在后期联调时省了很多排查时间。
4. 实操过程与验证方法
4.1 开发环境和工程组织
工具方面用的Vivado 2019.1,仿真用自带的xsim,后期大流量数据比对时也用过ModelSim。工程模块按功能拆得很清楚:nco.v、cic_dec.v、cic_interp.v、hbf_dec.v、hbf_interp.v、comp_fir.v,每个模块独立仿真通过后再做顶层联调。
这里分享一点模块划分的心得:CIC模块我写成了参数化形式,预留了R_CFG、N_CFG这些参数,后面想调整抽取倍数、改级联数都是改参数的事。HBF和FIR调用Xilinx IP核,系数文件独立管理。这样整个工程维护起来非常顺手。
4.2 仿真验证看时域更要看频谱
仿真验证阶段,我强烈建议别只盯着波形,把数据导出来做频谱分析。具体做法是在Testbench里生成单音正弦、扫频信号和调制信号三种激励,分别跑完链路后把各级输出写到txt文件,再用Matlab读进去做FFT。要核对三件事:
- 输入单音时,输出频谱里是否只有期望的那个频率分量,有没有多余的杂散
- 抽取后带外噪声是否被折叠进带内
- 插值后镜像频率是否压到目标水平
在这个阶段我发现过一个典型问题:第一版NCO的相位累加器在频率字切换时会复位,结果每次切换频率都出现一个毛刺,频谱上表现为一个宽带突刺,后来把复位逻辑去掉,只做相位连续累加,问题立即消失。这就是典型的“仿真发现、频谱定位”的案例。
4.3 上板调试环节的ILA经验
上板调试用的是Vivado的ILA(集成逻辑分析仪),这里有几个实测下来的经验:
先抓ADC/DAC接口的数据,确认接口时序没问题,再逐级抓CIC、HBF的输入输出。触发条件最好设成数据等于某个特征值,比如同步头或幅度峰值,比用外部引脚触发可靠得多。抓取深度不用太长,4096个点足够定位绝大多数问题,设太深反而浪费BRAM。
我遇到过一个非常经典的坑:仿真完全正常,上板后带外杂散明显偏高。排查了很久,最后发现是异步复位的释放时序问题,复位释放不同步导致前端模块偶尔出现毛刺。这个案例给我的教训是:FPGA工程里跨时钟域和复位同步的地位,远比想象中高。
5. 常见问题速查与避坑经验
下面这张表是我做这个项目过程里遇到的最典型的几个问题和对应的排查思路,整理出来方便大家直接对照:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| CIC输出数据溢出,信号失真 | 内部位宽没按增益公式预留 | 按BitGrowth = ceil(N×log2(R×M))补齐位宽 |
| 频谱出现非预期的杂散 | NCO相位截断 | 加dither噪声或增加查找表相位位宽 |
| 接收灵敏度差,带内噪声底高 | 抽取前滤波不够导致混叠 | 增加CIC级联数或前置FIR阶数 |
| 上板后出现周期性毛刺 | 跨时钟域或复位不同步 | 统一异步复位同步释放,接口处加FIFO/寄存器同步 |
| 时序违例严重跑不过 | CIC积分器链过长组合逻辑深 | 在积分器链中插入流水线寄存器 |
| 带外镜像抑制不达标 | 插值后FIR阶数不够 | 提高FIR阶数或换更陡的窗函数 |
再补几个独家实操心得:
CIC模块务必做成参数化,因为后级联调时大概率要调抽取倍数。每个模块都要带valid信号,形成一个握手机制,方便调试时通过数据有效标志判断卡在哪里。频率控制字更新要做成寄存器接口,能由上层软件在线改频。设计初期就用Matlab生成一组参考数据,上板后用ILA抓下来和参考比对,这是系统联调阶段最高效的验证方式。
6. 写在最后的一点个人体会
做完这个DUC/DDC项目再回头看,感触最深的一点是:数字变频的原理并不高深,但真正落地到FPGA上,到处都是“差一点就翻车”的细节。位宽计算多留1bit还是少1bit,决定了CIC会不会溢出;复位信号是同步还是异步、释放是否统一,决定了上板会不会出现莫名其妙的毛刺;滤波器是单级还是多级、抽取放在滤波前还是后,决定了整个系统的资源和性能。原理解决的是“能不能做”的问题,工程经验解决的才是“做出来能不能稳定跑”的问题。希望这篇文章能把我在这个项目里攒下来的细节和经验完整传递出来,帮后来的人少踩几个坑。这个链路后续扩展也顺理成章,做成多通道、接上ADC/DAC跑完整射频前端、甚至在前面加数字预失真,都是在同一套架构上叠加功能而已。
本文还有配套的精品资源,点击获取