ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

FPGA数字下变频(DDC)实现:DDS混频、滤波抽取与频率校正

2026/10/3 10:09:43 拓冰建站 浏览量
FPGA数字下变频(DDC)实现:DDS混频、滤波抽取与频率校正 最近在调一个中频数字化接收机项目完整走了一遍FPGA数字下变频DDC流程ADC采样进来的中频信号先在FPGA内部通过Vivado的DDS IP核生成正交本振cos/sin做IQ混频把中频搬移到基带再用低通滤波加抽取把数据率降下来最后利用DDS可编程频率字完成频率校正。整个过程配合MATLAB仿真对比能提前把定点误差、位宽截位问题给暴露出来。这篇文章就把这套思路、配置步骤、代码结构和踩过的坑一次性说清楚适合正在用Xilinx系列FPGA做软件无线电、信号处理或者刚入门DDC的开发者参考。1. 数字下变频的整体设计与思路拆解1.1 数字下变频到底在解决什么问题数字下变频DDC是软件无线电接收链路里的核心环节。射频前端把天线信号搬移到中频ADC以很高的采样率对中频信号采样如果直接把这一大堆高速数据送给后级解调处理器的算力、FPGA内部的资源、存储器带宽都会不堪重负。DDC的作用有两个一是把感兴趣的信号从载波附近搬到基带二是通过滤波和抽取把数据率降到正好满足信号带宽的程度。采集到基带IQ数据后后面的符号同步、解调、测频就方便多了。关键在于为什么要用IQ采样。中频采样得到的实信号频谱是正负对称的单独一路实信号无法区分载波以上的信号和载波以下的信号。用正交本振cos和sin分别混频得到I路和Q路构成复信号正频率和负频率被分开这样就能完整保留信号的相位信息。GPS、北斗接收机、雷达、频谱监测设备基本都是这个思路。1.2 DDC链路的基本结构混频、滤波、抽取一个标准数字下变频模块由三部分组成数字混频器、低通滤波器、抽取器。高密度集成在FPGA里数据流是流式的非常适合硬件实现。假设ADC采样时钟为fs输入中频信号中心频率为f_if我们在FPGA内部用DDS产生频率f_lo的正弦和余弦本振信号。混频器完成一次复数乘法I x(n) * cos(2π f_lo n)Q -x(n) * sin(2π f_lo n)混频输出包含差频分量f_if - f_lo和和频分量f_if f_lo。低通滤波器把和频分量滤掉保留差频分量也就完成了频谱搬移。抽取器再把采样率从fs降到fs / DD为抽取因子。抽取前低通是必须的否则高频分量折叠回基带信号就再也救不回来了。这里最容易被忽视的是本振频率的精度。工程中发射端和接收端的晶振不是理想同步的接收机本振和发射机本振之间会有偏差这个偏差直接进入差频分量导致基带信号存在残余频偏。所以DDC链路中需要频率校正能力这也是我后面专门用DDS可编程频率字来实现的原因。1.3 方案选型为什么用Vivado DDS IP核而不是自己写NCO很多人问直接用Verilog写一个DDS也不难为什么非要用Xilinx的DDS Compiler IP核我自己早期也手写过NCO功能确实能跑但有两个问题很难受。一是杂散性能不好控制。NCO本质是相位累加器加查找表相位截断会产生杂散查找表深度太浅杂散更大要想达到好的SFDR需要做抖动注入和ROM优化这些都是前人反复踩坑总结出来的自己重新做一遍成本很高。二是时序收敛问题。手写ROM查找表在高频时钟下容易出现时序违例尤其当输出位宽扩展到16 bit甚至24 bit时ROM规模增大路径延迟变长。Xilinx DDS Compiler在底层做了优化支持多种架构选择时序和资源都能得到保证。另外DDS IP核支持Phase Increment Programmable模式也就是说频率控制字可以通过AXI4-Stream动态写入这正好用来做频率校正。如果自己写NCO动态改频率字得自己处理跨时钟域和毛刺问题。所以综合考虑在常规DDC场景下直接用Vivado的DDS IP核是性价比最高的选择。2. 用Vivado DDS IP核生成正交本振信号2.1 DDS IP核的工作原理和核心参数计算DDSDirect Digital Synthesizer的基本结构是相位累加器、相位加法器和波形映射。简单的理解就是一个N位的累加器每个时钟周期累加一次频率控制字Phase Increment累加结果作为相位值通过查找表或者CORDIC算法映射成正弦或余弦值。输出信号频率由下面这个公式决定f_out (Δphase * f_clk) / 2^N其中Δphase是频率控制字f_clk是系统时钟N是相位累加器位数。频率分辨率就是f_clk / 2^NN越大频率步进越小。举一个实际例子。工程里系统时钟f_clk 100 MHz需要产生f_lo 20 MHz 的本振信号相位累加器位宽取32 bit频率控制字计算如下Δphase round(20e6 * 2^32 / 100e6) 858993459这个数写成16进制是0x33333333。配置到DDS IP核后实际输出频率为f_actual 858993459 * 100e6 / 2^32 ≈ 19.999999999 MHz频率误差小到完全可以忽略。这就是为什么DDS频率分辨率极高特别适合做精细频率校正。注意在Vivado的DDS Compiler IP核配置界面里如果选了Phase Increment Programmable还会多一个s_axis_phase_tvalid和s_axis_phase_tdata端口频率控制字就是从这个AXI4-Stream接口灌进去的。这样以后想改频率不需要重新综合工程运行中直接写控制字即可。2.2 Vivado DDS IP核的配置要点下面是我在Vivado 2021.2里的配置过程这些参数在2019.1到2023.x版本基本通用。打开IP Catalog搜索DDS Compiler双击进入配置界面。Configuration选项卡Implementation选择Phase Increment Programmable或者Phase Increment and Phase Offset Programmable。如果只做DDC选前者就够如果后续还要做相位调制选后者。时钟设置输入时钟频率填100 MHz和工程实际时钟一致这个会影响IP的时序分析。Output Frequency不一定要在这里填因为可编程模式下频率由外部控制字决定这里的设置只是一个初始值。通道数单通道即可。如果做多通道DDC可以选多通道但要注意AXI-Stream总线的TUSER信号分配。输出选择选Sine and Cosine这样一次性生成两路正交本振不需要例化两个DDS。输出位宽16 bit比较均衡。如果ADC数据位宽只有12 bitDDS输出16 bit已经足够如果对带外抑制和SNR要求高可以选18 bit或24 bit占用LUT和DSP资源会上升。相位位宽32 bit频率分辨率足够高。配置完成后生成IP核心端口如下aclkDDS工作时钟s_axis_phase_tvalid频率控制字有效信号s_axis_phase_tdata频率控制字数据位宽与相位位宽一致m_axis_data_tvalid输出数据有效信号m_axis_data_tdata该总线上包含了正弦和余弦两个分量低位是cos高位是sin不同版本可能相反需要查看产品指南一定记得查一下Xilinx PG141DDS Compiler Product Guide确认tdata分配顺序我最早在这里吃过亏连续两天数据完全对不上最后发现是cos和sin接反了。2.3 用DDS输出与ADC数据做IQ混频混频在FPGA里实现起来很直接就是乘法。ADC采样数据进入FPGA后先做必要的位宽对齐然后分别与DDS输出的cos和sin相乘得到I路和Q路。下面是一段简化后的Verilog示例这里假设ADC输出为12 bit有符号数adc_dataDDS输出为16 bit有符号数module ddc_mixer #( parameter ADC_WIDTH 12, parameter NCO_WIDTH 16, parameter MIX_WIDTH 28 )( input wire clk, input wire rst_n, input wire [ADC_WIDTH-1:0] adc_data, input wire adc_valid, output wire [MIX_WIDTH-1:0] i_out, output wire [MIX_WIDTH-1:0] q_out, output wire mix_valid ); wire [NCO_WIDTH-1:0] nco_sin; wire [NCO_WIDTH-1:0] nco_cos; wire nco_valid; wire [31:0] phase_inc; // 频率控制字100MHz时钟下约20MHz本振 assign phase_inc 32h33333333; dds_compiler_0 u_dds ( .aclk (clk), .s_axis_phase_tvalid (adc_valid), .s_axis_phase_tdata (phase_inc), .m_axis_data_tvalid (nco_valid), .m_axis_data_tdata ({nco_sin, nco_cos}) ); // 将ADC数据扩展为16bit有符号数再乘法 wire signed [NCO_WIDTH-1:0] adc_ext; assign adc_ext {{(NCO_WIDTH-ADC_WIDTH){adc_data[ADC_WIDTH-1]}}, adc_data}; wire signed [MIX_WIDTH-1:0] mult_i; wire signed [MIX_WIDTH-1:0] mult_q; assign mult_i adc_ext * $signed(nco_cos); assign mult_q adc_ext * $signed(nco_sin); // 输出寄存一拍保持与valid对齐 reg [MIX_WIDTH-1:0] i_r, q_r; reg valid_r; always (posedge clk or negedge rst_n) begin if (!rst_n) begin i_r 0; q_r 0; valid_r 1b0; end else begin i_r mult_i; q_r mult_q; valid_r nco_valid; end end assign i_out i_r; assign q_out q_r; assign mix_valid valid_r; endmodule注意这里没有做饱和处理如果乘积超过目标位宽应该在下一级滤波前截位或者绕回。混频乘法之后立即截位会损失一些低比特但对后续滤波影响不大如果对动态范围要求高建议把完整乘积送入FIR IP核让滤波器的输入输出位宽配置来自动处理。3. IQ采样与滤波抽取实现3.1 低通滤波器在DDC里的作用混频后的I/Q信号同时包含差频分量和和频分量。比如中频20 MHz本振20 MHz差频在0 Hz附近和频在40 MHz附近。如果不对和频分量滤波后面一旦抽取40 MHz分量就会混叠到基带等于白干。在DDC链路中低通滤波器的作用是抗混叠加信道选择。滤波器的通带要覆盖信号带宽阻带要覆盖和频分量所在频段。假设信号带宽2 MHz采样率100 MHz那么低通截止频率大约1 MHz在40 MHz处要有足够衰减。这个滤波器阶数不会低直接用FIR滤波器实现窗函数法或者等波纹法都能设计。我用MATLAB的filterDesigner工具来设计滤波器设定通带截止频率1 MHz阻带截止频率2 MHz通带纹波0.1 dB阻带衰减60 dB采样率100 MHz算出来阶数在100阶左右。生成系数后导出为.coe文件供Vivado FIR Compiler使用。3.2 FIR Compiler IP核配置与系数导入在Vivado里使用FIR Compiler IP核配置要点如下滤波类型Single Rate如果抽取率不大可以单级FIR加直接抽取抽取率大可以选Decimation模式。通道数1因为I路和Q路分别例化一个FIR或者选择通道数2让IP内部时分复用处理I/Q两路。通道数2更省DSP资源但复杂度高一些新手建议先例化两个单通道FIR。输入数据位宽取决于混频后截位宽度一般取16 bit或20 bit。滤波器系数把MATLAB导出的.coe文件加载进去IP内部会自动量化系数。建议选择Coefficient Type为Signed 16 bit。输出位宽通常自动计算保证不溢出但输出位宽太宽会占用更多资源可以手动指定配合Scale Factor。举个例子我设计的100阶低通FIR系数16 bit量化后输入16 bit输出位宽系统自动算下来是32 bit左右。直接拿32 bit数据去做抽取可以但数据率很高时总线位宽大后续模块负担重。实际工程中会在FIR输出端做截位或饱和把I/Q数据保留到18 bit这时需要观察信号幅度避免截位把有效位截掉。3.3 抽取率选择与CICFIR组合DDC的抽取率不是随便定的。根据带通采样定理抽取后的采样率至少要大于信号带宽的两倍。假设基带信号带宽2 MHz那抽取后采样率最低也要4 MHz。工程上一般留余量取5 MHz到10 MHz。如果把100 MHz降到10 MHz抽取率D10直接用100阶FIR抗混叠也可以但高阶FIR在100 MHz时钟下每个时钟都要计算DSP资源消耗很大。更高效的做法是采用CIC加FIR两级结构先用CIC滤波器做10倍抽取CIC不需要乘法器只做积分和梳状滤波资源占用极低但CIC通带会有滚降所以抽取后再用一个低频FIR做补偿和整形。CIC滤波器在Vivado里也有CIC Compiler IP核配置阶数和抽取率非常方便。需要注意的是CIC的增益较大位宽要按公式计算扩展防止溢出。这套结构和直接FIR抽取相比资源能省一半以上性能也足够。如果系统里只有一路DDC直接FIR抽取更省事如果做多通道、多频点扫描CICFIR的优势就体现出来了。4. 频率校正与载波偏移补偿4.1 频偏从哪来频率校正不是可有可无的。实际系统中接收机本振与发射机本振之间的频率偏差主要有三个来源收发晶振频率容差。普通TCXO的精度在±1 ppm到±10 ppm之间。如果是2.4 GHz载波10 ppm就是24 kHz的频偏。这个频偏叠加到中频信号上会让基带信号的频谱整体偏移。多普勒频移。运动场景下载波越高多普勒越明显。比如卫星通信中低轨卫星过顶时多普勒频移可能达到几十kHz必须实时校正。链路非理想因素回波或本振相位噪声造成的慢变化。如果不校正QAM信号的星座图会旋转OFDM子载波之间的正交性被破坏误码率急剧上升。所以在DDC链路中频率校正往往是接收机能不能正常工作的关键一环。4.2 用DDS可编程频率字做数字频率校正之前选择DDS IP核的Phase Increment Programmable模式就是为了在这里派上用场。校正的基本思路是先估计输入信号的残余频偏Δf得到对应的频率控制字修正常量再叠加到DDS基础频率控制字上。计算公式很简单。假设DDS期望输出频率为f_lo现在需要额外修正Δf则新的频率控制字为Δphase_new round((f_lo Δf) * 2^N / f_clk)用一个加法器把基频控制字和校正控制字相加再送入DDS的s_axis_phase_tdata端口就实现了动态频率校正。这个过程可以做成一个闭环基带I/Q数据经过FFT或叉积鉴频器估计出残余频偏转换成频率字再反馈给DDS。我在工程里用的是简化锁频环基于I/Q信号相邻符号的叉积cross(n) I(n-1) * Q(n) - I(n) * Q(n-1)该值经过环路滤波器后正比于残余频偏。把滤波器输出映射成DDS频率校正字反复迭代直到频偏收敛到几十赫兹以内。这个方案实现简单不需要占用FFT资源适合窄带信号。如果信号模式复杂或者频偏范围大可以先用FFT做粗估计再用叉积鉴频做细估计两者结合效果更好。4.3 MATLAB仿真对比频偏校正效果验证在联调FPGA之前我习惯先在MATLAB里把算法仿真一遍。这次我构造了一个带100 kHz频偏的中频信号采样率100 MHz中频20 MHz本振20 MHz加上校正偏置模拟收发频偏场景。fs 100e6; % 采样率 N 10000; % 采样点数 t (0:N-1) / fs; f_if 20e6; % 中频 f_lo 20.1e6; % 本振故意偏100kHz x exp(1j*2*pi*f_if*t); % 理想中频信号 s real(x) 0.01*randn(1,N); % 加噪声并取实部 nco_i cos(2*pi*f_lo*t); nco_q -sin(2*pi*f_lo*t); i_mix s .* nco_i; q_mix s .* nco_q; % 低通滤波 [b, a] butter(4, 2e6/(fs/2)); i_f filter(b, a, i_mix); q_f filter(b, a, q_mix); % 频率估计瞬时相位差分 phi atan2(q_f, i_f); cross phi(2:end) - phi(1:end-1); angle_est mean(cross) * fs; fprintf(估计频偏 %.2f Hz\n, angle_est); % 补偿调整本振频率 f_lo_new f_lo - angle_est; nco_i2 cos(2*pi*f_lo_new*t); nco_q2 -sin(2*pi*f_lo_new*t); i_c s .* nco_i2; q_c s .* nco_q2; i_cf filter(b, a, i_c); q_cf filter(b, a, q_c); phi_c atan2(q_cf, i_cf);这段代码里最核心的是中间那一步频率估计。atan2得到瞬时相位相位差就是频率差。实际FPGA里我不会直接用atan2而是用叉积近似在一个小角度范围内sin(Δφ)约等于Δφ所以用Q乘以I_prev减去I乘以Q_prev就能估计频偏实现成本低很多。MATLAB里面精度高是为了先验证算法思路。通过仿真能看到校正后相位轨迹基本不再旋转星座图聚拢。这个方法移植到FPGA后实际测试也能达到同样的效果。5. MATLAB仿真与FPGA验证对比5.1 用MATLAB搭建DDC浮点参考模型每做一版FPGA代码我都会先搭建一个MATLAB浮点参考模型。这个模型的意义是提供一个标准答案FPGA定点仿真的结果要和它对齐误差在允许范围内才能去上板。参考模型包括三个部分ADC采样数据生成、数字混频、低通滤波抽取。ADC采样数据可以用一个正弦波叠加噪声来模拟也可以直接使用MATLAB生成的中频信号量化成int12。最好把FPGA里用到的DDS频率控制字、FIR系数都导进MATLAB保证输入条件一致。比如DDS输出频率是19.999999999 MHz而不是20 MHz这个微小差异会在长时间仿真中累积成相位偏差如果参考模型直接用20 MHz算对比时就会出问题。5.2 定点仿真数据和浮点模型怎么对齐FPGA仿真数据导出到MATLAB一般用testbench的fwrite把I/Q输出写到文本文件或者使用Vivado的ILA在板上抓数后导出CSV。我常用方法是在testbench里写文件integer fid_i, fid_q; initial begin fid_i $fopen(i_out.txt, w); fid_q $fopen(q_out.txt, w); end always (posedge clk) begin if (mix_valid out_valid) begin $fwrite(fid_i, %d\n, $signed(i_out)); $fwrite(fid_q, %d\n, $signed(q_out)); end end然后在MATLAB里load这两个文件和浮点模型输出做对比。注意FPGA输出有流水线延迟所以对比前要把浮点参考模型也延迟同样拍数或者先找一个明显跳变沿做对齐否则相关系数可能会很低。对准后计算IQ误差矢量幅度EVM或信噪比。我遇到过的最典型情况是DDS输出正确、乘法正确但FIR滤波器系数导入时符号位反了导致输出全错MATLAB对比一眼就能看出来。5.3 量化误差来源与控制方法定点模型和浮点之间存在误差这一点不用慌关键是要知道误差从哪来、控制在什么范围。第一个误差源是DDS相位截断。DDS内部相位累加器32 bit但查找表地址位宽可能只有15 bit或16 bit低位被截断会引入相位杂散。Xilinx DDS IP核自带抖动处理一般不需要手动处理。第二个误差源是乘法截位。混频乘法后如果直接截取高16位会损失低比特信息。建议至少保留高位18到20 bit或者保留完整结果到FIR让FIR内部的精度处理来消化。第三个误差源是FIR系数量化。MATLAB设计的浮点系数量化成16 bit定点后滤波器的频响会略有改变特别是阻带衰减可能下降几个dB。如果对带外抑制要求高可以把系数位宽扩展到24 bit。第四个误差源是滤波器输出截位。如果输出保留位太少信号峰值被削波会带来严重失真。可以用AXI接口的tuser信号监测溢出或者在IP配置中选择饱和处理模式。综合下来在我的工程里16 bit DDS、16 bit FIR系数、18 bit混频输出最终EVM能控制在1%以内这对一般数字接收机来说足够了。6. 常见问题与调试心得6.1 DDS输出数据与混频数据没有对齐DDS IP核从输入频率控制字到输出正弦/余弦有一定的流水线延迟。FIR Compiler IP核也有固定延迟。如果设计中要求I/Q数据与其他控制信号严格对齐不能简单用同一个valid打天下而应该让valid跟着数据流走。最稳妥的做法是用AXI-Stream的tvalid/tready握手机制。一级模块的tvalid拉高后数据经过一个周期的处理下一级tvalid也拉高这样逐级传递。如果某些中间模块不支持背压可以简化成valid打拍延迟方案用移位寄存器延迟一个周期模拟数据流水延迟。我自己第一次调的时候没注意DDS输出延迟结果I路和Q路各自延迟不一致星座图变成一个椭圆。排查了很久才发现I路和Q路分别用了不同的中间寄存器但复位时刻不同延迟差了一拍。后来统一用一个状态机生成valid问题立刻消失。6.2 Vivado综合后比特流生成失败与时序收敛很多人在写完DDC逻辑后跑综合和实现会发现时序违例尤其是DDS、乘法器、FIR串联起来组合逻辑路径变长。常见的解决思路是在IP核配置里启用Output Register把DDS输出寄存一拍乘法器也开启内部流水线。这会让延迟变大但时序更好收敛。在混频乘法器后面插入额外寄存器。乘法器本身有DSP48E1硬核支持组合逻辑延迟很小但数据位宽很大时建议在乘法结果后再打一拍分割路径。使用约束文件正确约束时钟不要只用默认的create_clock。例如主时钟100 MHz通过MMCM/PLL驱动DDS、FIR那么约束应该基于MMCM输出时钟避免工具乱推。如果你遇到Vivado生成比特流失败类问题先看Timing Summary把最差路径找到再用上述方法拆流水线。时序问题在DDC链路里很常见但只要每级都打一拍基本上都能解决。6.3 板级ILA抓信号不如预期仿真全通过上板后发现信号不对这种情况下我最先做的就是三个检查时钟是否正确。用ILA同时抓clk和rst_n确认复位释放时钟跑起来了。IP核的复位策略。DDS和FIR的复位是高有效还是低有效不同IP不一样和顶层复位反了输出就一直为0。数据位宽和符号位。ADC数据如果是无符号需要根据ADC手册转换成补码否则混频输出会整体翻转。还有一点容易被忽略ILA本身也会消耗大量BRAM和布线资源抓取信号过多会导致时序变差所以只抓关键信号深度1024就够。ILA触发条件用tvalid上升沿而不是数据值方便快速定位第一个有效数据。6.4 关于FPGA里IQ采样和DDC扩展的一点心得做完这个DDC链路后我发现这套结构稍微改一改就能复用到很多场景。比如把DDS的频率字做成多组用状态机切换就能实现频点快速扫描这在频谱监测和跳频通信里非常实用。再比如做多通道DDC只需要把DDS IP核配置成多通道FIR Compiler也配置成多通道资源可以共享成本远低于例化多份。如果后续要进一步提高集成度可以考虑用Vivado的IP Integrator把DDS、乘法器、FIR、CIC用Block Design搭起来做成一个完整的DDC子系统再用HDL包装。这样在工程中可读性高也方便后期移植到Zynq平台用ARM核通过AXI-Lite动态配置频率字和滤波器参数。最后再分享一个小技巧做任何IP核配置前先把Xilinx的Product Guide对应章节翻一翻。DDS看PG141FIR看PG149CIC看PG140。这些文档里包含了端口时序、位宽计算、延迟周期表很多网上资料都没讲清楚。照着文档配一次比盲目试错快得多。