资讯动态

FPGA数字下变频(DDC)实战:DDS+IQ混频+抽取滤波全解析

发布时间:2026/10/3 4:15:51 来源:尧图企业网站定制
把20MHz的中频信号搬到零频同时把100M的采样率降到25M这件事放在FPGA里做最顺手的组合就是Vivado的DDS IP核加上自己写的I/Q混频和抽取滤波。数字下变频DDC在软件无线电、雷达回波处理、通信接收机里几乎天天用我刚跑完一个完整的实战工程趁热把细节整理出来DDS频率字怎么算、IQ采样链路怎么搭、滤波器怎么配以及怎么用MATLAB先把链路验证一遍再写FPGA逻辑。这篇文章不是纯原理讲解更像一份完整的踩坑记录和实操手册。整个过程围绕一个明确目标在Xilinx FPGA里用DDS IP核生成正交本振和ADC送进来的14位中频采样数据做正交混频输出I/Q两路基带信号再低通滤波、抽取降速。如果你正好在做类似的项目照着这个思路走一遍能少走不少弯路。我会尽量把参数计算过程、例化代码、仿真对比方法都写得具体方便你直接在Vivado里复现。1. 从需求到方案DDC为何非做不可以及我为什么选DDS IP核1.1 数字下变频到底完成了哪三件事数字下变频说白了就是三件事频谱搬移、低通滤波、数据抽取。频谱搬移是核心动作。ADC采样出来的中频信号比如20MHz本身还是高频载波后级解调算法不希望在高频上进行。用一个本地数字本振信号比如20MHz的复指数信号去乘ADC数据频谱整体就会从20MHz搬到零频附近。这一步在数学上就是乘以一个复指数实际硬件实现时拆成实部虚部就是分别乘以cos和sin。搬完之后的频谱里除了想要的基带信号还会残留下一个以两倍本振频率为中心的高频分量。比如本振20MHz、信号也在20MHz混频后不仅有基带还有40MHz左右的镜像分量。这个分量不滤掉后面的抽取就会发生严重的频谱混叠。所以必须加低通滤波器把带宽外的分量清干净。滤波之后才是抽取。基带信号只要保留有用带宽就够了。比如信号带宽2MHz原始采样率100MHz数据率完全可以在低通滤波之后降到25M甚至10M对后级DSP或ARM来说处理压力一下子小了很多。抽取这一步本身不复杂就是每隔N个点取一个但前提是前面低通滤波要到位否则混叠会直接污染基带信号。这三个动作配合起来就是DDC的全部内容。理解了它后面每一步选型和参数调整都有了判断依据。1.2 DDS IP核 vs 手写NCO我为什么偏向IP核本振信号怎么产生最常见的方案是一个数字控制振荡器NCO。手写一个NCO其实不算难一个相位累加器加一个ROM查找表几十行Verilog就能跑出一个正弦波来。我在学校做课程设计时就这么干过相位累加器在每个时钟周期累加一个频率控制字累加结果的高位作为ROM地址ROM里存一个周期的正弦波形查表输出就是正弦波。但在实际工程项目里我更推荐直接用Xilinx的DDS Compiler IP核原因很实在。第一IP核内部做了ROM压缩和输出优化。SIN/COS查找表有对称性IP核会自动利用这个对称性把ROM容量压缩到四分之一以下。自己写查找表地址映射、极性翻转、补码处理都要自己处理稍不注意就出bug。第二IP核的输出频谱质量更好。DDS Compiler内部对相位截断误差做了处理用户配置SFDR指标后IP核会选择合适的ROM深度和宽度保证无杂散动态范围达到要求。在DDC链路里本振的频谱纯度直接影响最终解调质量这个优化很值。第三IP核支持可编程相位增量。硬件上电后我可以动态修改频率控制字这对频率校正至关重要。后面我会详细讲在工程里晶振频偏、多普勒频移都会让信号偏离理想频点能在线微调本振频率是刚需。第四接口统一都是AXI4-Stream。DDS输出的数据可以直接对接FIR Compiler等IP核时序和握手机制都是现成的省去自己写valid握手逻辑的麻烦。那什么时候用手写NCO我的判断是资源特别紧张、只需要方波或简单扫频波形、或者纯粹为了学习原理时手写完全没问题。但做工程追求的是稳定和效率所以我选择了DDS IP核方案。2. 核心参数与频率控制字DDS IP核配置前必须算清楚2.1 DDS IP核关键参数速览Vivado里打开IP Catalog搜索DDS Compiler双击进入配置界面会看到一堆参数。我和大家一样第一次看这些参数时也懵所以先列一张表把最关键的几个讲清楚。参数作用我的配置Configuration选择输出波形和结构Phase Generator and SIN COS LUTPhase Width相位累加器位宽决定频率分辨率32位Output Width正弦/余弦输出数据位宽16位SFDR无杂散动态范围和输出位宽强相关90 dB 左右跟着输出位宽走Phase Increment Programmability是否允许动态改频率字ProgrammableOutput Selection选择输出 SIN、COS 还是两者都出SIN and COSPhase Width是个关键参数。频率分辨率由它决定频率分辨率等于系统时钟除以2的相位位宽次方。32位累加器配100MHz时钟分辨率大约是0.023Hz。这个精度对绝大多数通信和雷达场景都足够所以直接选32位不用纠结。Output Width则要跟着ADC位宽和后级处理精度走。我用的ADC是14位DDS输出16位混频之后乘积位宽在30位左右后级再截位到需要的精度。这里没选特定位数是因为DDS输出位宽太低会让本振的量化噪声变大太高又白白浪费DSP乘法器资源16位是综合性能和资源后比较合适的选择。Phase Increment Programmability这个选项一定要勾上。勾选后IP核会多出一个s_axis_phase接口运行时往这个接口写频率控制字就可以实时改变输出频率。我后面做频率校正全靠这个接口。2.2 频率控制字手把手计算DDS输出频率和频率控制字FTWPhase Increment之间的关系公式非常简单f_out FTW * Fs / 2^N其中Fs是系统时钟N是相位累加器位宽。反过来如果要产生某个目标频率f_out频率控制字就是FTW round(f_out * 2^N / Fs)用一个具体例子走一遍。系统时钟Fs100MHzN32目标本振频率20MHzFTW round(20e6 * 2^32 / 100e6) round(858993459.2) 858993459 0x33333333那实际输出频率是f_out 858993459 * 100e6 / 2^32 19.99999997 MHz误差约0.03Hz完全可以忽略。这说明只要选32位相位累加器大部分整数频率基本都能算到足够准。但注意如果你的目标频率不是整数兆赫兹比如20.001MHz结果就不是整数。FTW算出来是859036447.5四舍五入取整之后实际输出频率和期望值会有零点几赫兹的偏差。在多数解调系统里这个偏差没什么影响可如果对频率精度要求极高比如长时间相参积累的雷达就得加大相位累加器位宽或者想办法在外部做残余频偏补偿。实操中我一般直接在MATLAB里把FTW算好生成一个常量文件给Testbench和Verilog用。这样不容易手算出错回头写调试报告时也方便追溯。计算时注意数据格式FTW超过32位会溢出一旦溢出输出频率完全不对这个低级错误我踩过一次。2.3 Vivado实际操作IP核配置与例化在Vivado里的实际操作流程我按步骤写在下面方便你照着点。打开工程在Flow Navigator里点IP Catalog。搜索框输入DDS找到DDS Compiler双击打开配置界面。第一页Configuration选Phase Generator and SIN COS LUTPhase Increment Programmability选Programmable。第二页设置系统时钟我这填100MHz、Phase Width为32、Output Width为16。如果选了Programmable这里还能看到s_axis_phase接口位宽信息。生成IP核后打开例化模板把例化代码复制到自己的顶层模块里。例化代码看起来大概是这样我做了精简wire dds_tvalid; wire [31:0] dds_tdata; wire [15:0] dds_cos dds_tdata[31:16]; wire [15:0] dds_sin dds_tdata[15:0]; dds_compiler_0 u_dds ( .aclk(clk_100m), .s_axis_phase_tvalid(1b1), .s_axis_phase_tdata({ftw, 8d0}), .m_axis_data_tvalid(dds_tvalid), .m_axis_data_tdata(dds_tdata) );有个细节要特别提醒当Phase Width是32位时因为AXI4-Stream要做字节对齐s_axis_phase_tdata的位宽不一定是32位而可能是40位。数据具体放在高32位还是低32位不同配置、不同Vivado版本可能不一样不要照抄别人的代码一定要以你自己工程里IP核生成的例化模板为准。例化模板里实际引脚名、位宽都写得明明白白照着改就行。DDS输出的m_axis_data_tdata在我这个配置下是32位高16位是cos低16位是sin。这个顺序在IP核配置页里会标明拿testbench仿真一下看到输出波形再对接下来的逻辑也不迟。3. IQ混频、低通滤波与抽取的Verilog实现3.1 I/Q正交采样为什么要用sin和cos两路本振DDS IP核同时输出sin和cos两路正交本振正好用来做I/Q采样。为什么要这么麻烦用两路信号而不是直接乘一个正弦波原因在于实信号的频谱是共轭对称的。单路混频只能把信号整个搬到低频但你想区分原始信号是高于本振还是低于本振单路做不到因为正频率和负频率在实信号下混在一起了。用I/Q两路混频本质上是在构造一个复信号。I路乘以cosQ路乘以sin然后I和Q合起来就是复信号IjQ。这个复信号在频域里不再对称正频率和负频率是分开的搬移之后就能保留完整的频谱信息。如果只做单路混频信号在上边带还是下边带解调端根本分不清。举一个生活化的例子坐电梯时感觉到的上下感。单路混频就像只知道坐了电梯不知道是上去还是下来I/Q正交混频则通过两路正交信息把方向这个信息保留下来。DDC里的方向对应的就是信号相对本振的频偏方向。所以DDS输出两路正交本振不是多余设计而是正交解调的内在要求。对IQ采样来说两路本振的相位差必须是严格的90度。DDS IP核从同一个相位累加器查表生成sin和cos相位同步性天然有保障这也是数字方案优于模拟正交混频器的地方。3.2 混频器代码与位宽处理混频器的Verilog实现核心就是一个带符号乘法器。我把代码写在这里module ddc_mixer #( parameter ADC_WIDTH 14, parameter DDS_WIDTH 16, parameter MIX_WIDTH 32 )( input wire clk, input wire rst_n, input wire [ADC_WIDTH-1:0] adc_data, input wire [DDS_WIDTH-1:0] dds_cos, input wire [DDS_WIDTH-1:0] dds_sin, output reg [MIX_WIDTH-1:0] i_mixed, output reg [MIX_WIDTH-1:0] q_mixed ); always (posedge clk or negedge rst_n) begin if (!rst_n) begin i_mixed {(MIX_WIDTH){1b0}}; q_mixed {(MIX_WIDTH){1b0}}; end else begin i_mixed $signed(adc_data) * $signed(dds_cos); q_mixed $signed(adc_data) * $signed(dds_sin); end end endmodule这里面有两个容易被新手忽视的坑。第一个是符号问题。ADC数据在常规配置下是补码格式DDS的输出默认也是补码但两者未必一定能直接用$signed。关键是确认你的数据最高位是不是符号位。有些ADC模块或自己做数据采集时输出的格式可能是无符号数进了FPGA就被当成正数处理混频结果完全不对。遇到这种要先做格式转换比如无符号数据减一个直流偏置转成有符号格式。第二个是位宽问题。14位乘16位乘积位宽应该是30位。我在模块里把输出设成了32位多出的两位是符号扩展没问题。但如果后级FIR滤波器输入位宽是固定的比如32位这里就得把混频结果手动截位否则综合工具会报位宽不匹配或者数据溢出把有效信号截没了。截位也有讲究。直接拿掉低几位会引入量化误差但为了接口对齐只能这样。我的经验是截位时先看信号的幅度范围如果乘积最大可能到2^29那输出32位就没必要截到28位左右更合理具体要结合后级滤波器动态范围来调。3.3 低通滤波与抽取的实操选择混频之后紧接着就是低通滤波。为什么必须滤还是用刚才的例子本振20MHz信号20MHz混频后基带分量在0MHz附近高频分量在40MHz附近。如果直接抽取4倍40MHz附近的分量会折叠回10MHz以内干扰基带信号这就是混叠一旦混进去神仙难救。低通滤波器的截止频率怎么定看信号带宽。我的信号带宽约2MHz低通截止频率我给到3MHz留了一点过渡带余量。实现上我用了Xilinx的FIR Compiler IP核系数在MATLAB里用filterDesigner设计好导出.coe文件直接加载进IP核。FIR Compiler里可以直接配置抽取倍数这样滤波和抽取一步完成省去单独写抽取逻辑的麻烦。抽取倍数我这里选4100M降到25M。要注意的是抽取倍数和滤波器多相分解有关FIR Compiler会自动把滤波器拆成多相结构抽取倍数设计在IP核内部时序上它会输出valid信号后级按valid采样就行。如果你的工程不想依赖IP核也可以手写一个简单的移动平均加抽取但性能差点。对带宽较宽的信号移动平均的频谱响应不够陡过渡带会出现混叠隐患。所以我建议有条件直接上FIR IP核资源占用不大效果稳定。抽取后的数据流向哪里看你后级是什么。我这边抽取之后送进一个FIFO再由ARM通过AXI总线读走。这样DDC部分和处理器解耦数据率匹配交给FIFO来协调。4. MATLAB仿真先行FPGA结果再对齐4.1 先用MATLAB把DDC链路跑通我个人的习惯是任何数字信号处理链路上FPGA之前先在MATLAB里建一个模型跑通。这样做的好处是算法层的验证完全不受Verilog语法、时序细节干扰能快速确认思路本身对不对。MATLAB里DDC的模型很直接核心代码就几行fs 100e6; t (0:4095)/fs; f_if 20e6; % 生成一个中频测试信号带宽1MHz左右叠加噪声 x sin(2*pi*f_if*t) 0.1*randn(size(t)); % 理想本振复指数I路取实部Q路取虚部 nco exp(-1j*2*pi*f_if*t); y x .* nco; % 低通滤波截止频率3MHz [b, a] butter(4, 3e6/(fs/2)); y_f filter(b, a, y); % 4倍抽取 y_d y_f(1:4:end);这个模型里I/Q信号是复数的实部和虚部对应FPGA里I路和Q路两路实数信号。跑完看看y_d的频谱确认信号已经搬移到0频附近、带宽正确模型就成立。有一点要说清楚MATLAB里用的是理想本振频率完全等于f_if不存在量化误差、相位截断误差。而FPGA里的DDS是有限位宽的字输出频率和理想值会有微小偏差。这个偏差在对比结果时要心里有数。4.2 从Vivado仿真导出数据到MATLABMATLAB模型验证通过后再写Verilog和Testbench最后把FPGA仿真结果导出来和MATLAB结果对比。从Vivado仿真导出数据我在Testbench里常用$fwrite。代码大概这样integer fp; initial begin fp $fopen(ddc_out.txt, w); end always (posedge clk) begin if (dds_tvalid) begin $fwrite(fp, %d %d %d\n, adc_data, i_mixed, q_mixed); end end这里输出的三列分别是ADC原始数据、混频后的I路、混频后的Q路。要注意$fwrite的%格式符在不同仿真器里可能略有差异Vivado Simulator里%d输出的是十进制补码负值会正确显示为负数MATLAB用fscanf读取后不需要额外转换。但如果你的数据格式比较特殊或者想保留完整位宽也可以按十六进制%x输出在MATLAB里再用typecast转换成有符号数。这个方式更通用因为补码的负数和十六进制字符串之间的转换不同进制规则容易搞混。导出的数据量别太大。仿真10万个点就够对比了全链路跑几百万个点仿真时间成倍增加对验证没什么额外帮助。我一般仿真4096个点或者16384个点足够看清频谱和时域波形。4.3 两边的结果对不上怎么办MATLAB模型和FPGA仿真相差多大才算正常我的标准是频谱形状基本一致峰值频点一致幅度在量化误差范围内接近就算对齐。实际对比中我最常遇到的问题有几个。第一时域波形相位对不上。原因是FPGA链路各级IP核都有固定延迟。DDS IP核有latency混频器有一级流水FIR滤波器有几十拍延迟。MATLAB是理想零延迟滤波当然对不上。解决办法很简单在Testbench里用一个计数器记录从输入开始到输出有效数据的cycle数然后在MATLAB对比时把对应点平移对齐。第二幅度对不上。FPGA里混频结果位宽32位但后级FIR输入位宽可能只有16位截位会损失低几位。MATLAB是全精度浮点运算幅度自然有差异。这个属于量化误差正常关键看差异是否在可接受范围内。如果截位后信噪比降得厉害就回去调截位策略多留几位。第三低频有毛刺。这往往不是逻辑错误而是DDS输出的本振不是理想单频旁边有杂散。DDS的SFDR指标一般在90dB左右时域波形是干净的FFT放大后看到细小的杂散是正常的。如果杂散幅度异常高就要回查DDS配置重点看SFDR参数是否设得太低。第四完全对不上一个在0频一个在20MHz。这种情况最常见的原因就是本振频率字写错了。用第2节的公式重新算一遍FTW再用Vivado仿真直接看dds_tdata的波形频率对不对这一步先单独验证。5. 调试实录我踩过的坑和补救办法5.1 问题速查表直接对着排查调试DDC链路时遇到的问题我整理成了一张速查表后面再碰到类似问题一条条排查就行。现象可能原因解决办法DDS输出始终为0s_axis_phase_tvalid没有拉高或aclk未正确连接检查时钟和valid信号仿真时先单独看DDS输出混频后每周期出现符号翻转异常符号位处理错误确认ADC和DDS输出格式统一用补码并加$signed基带信号有固定直流分量DDS输出可能是无符号格式或AD数据直流偏置未去除检查DDS输出格式配置输入数据先做直流消除或偏置校正频谱峰值不在零频而是在某固定偏移本振频率和信号频率不一致按公式重新计算FTW检查频率字必要时用可编程相位增量在线微调抽取后频谱混叠低通截止频率太高或阶数不够降低截止频率增加FIR阶数确认抽取倍数与滤波器设计匹配FPGA仿真和MATLAB结果幅度差异大位宽截断或IP核latency未对齐检查各级位宽截位要合理对比时先做延迟补偿DDS能以较低资源实现扫频需要每个周期更新相位增量利用s_axis_phase接口用一个小逻辑实时更新FTW看起来都是简单的表格但每一条背后都是花过时间查问题的。特别是符号位和位宽十有八九的DDC问题都能归结到这两个地方。5.2 几个真正管用的调试习惯调试FPGA信号处理链路我有一个雷打不动的原则逐级验证绝不一条链路跑到底再排查。具体怎么做DDS出来后先单独仿真确认正弦波形正确、频率正确、cos/sin相位差是90度。下一步再把混频器接上输入一段已知信号比如常数或单频正弦看混频输出是不是预期的和差频。最后再接FIR滤波器观测滤波后的波形。这样做的原因很朴素一旦全链路出问题你能根据现象快速定位到某一级。否则几十个模块一起跑波形对不上排查范围太大浪费时间。利用好Vivado的Mark Debug功能。我习惯把DDS输出、混频输出、FIR输出这几个关键节点在综合前设置成Mark Debug上板后在Vivado Logic Analyzer里看实际波形。仿真没问题不代表上板没问题有时钟约束问题、有外部信号质量问题只有跑在真实硬件上才能暴露。Logic Analyzer里可以直接看到频谱大概形状判断DDC有没有真正工作。频率校正这块我多说一句。实际项目中晶振频偏、多普勒频移都会让信号偏离设计频点。比如本振用的晶振实际频率比标称值低几百赫兹导致信号并没精确落在零频。这时候利用DDS可编程相位增量在线微调FTW把信号拉回零频。调整手法是先看频谱峰值偏多少算出差值频率按公式换算成FTW调整量写入s_axis_phase接口即可。这里面有个细节调整频率控制字时最好在信号链路空闲时进行或者做一个平滑过渡避免瞬间的相位跳变造成频谱杂散。我在工程里用了一个简单状态机只在数据帧间隙更新FTW实测对信号影响很小。多级联调时别忘了把每一级的valid信号捋一遍。AXI4-Stream接口最重要的就是valid和ready的握手关系。DDS输出的tvalid, FIR输出的tvalid, 以及FIFO写入使能链路对不上通常就是valid信号链断了。最后再分享一点个人的体会DDC这套链路做下来我最大的感受是FPGA算法工程的核心一半在算力规划另一半在细节。DDS IP核、FIR IP核本身都很成熟配置好基本不会出错真正考验人的是位宽匹配、符号处理、延迟对齐这些细活。尤其是位宽从ADC到混频再到滤波器每一级都在变化稍不留神就让信号失真。建议新接触DDC的朋友先别急着上板把MATLAB模型、位宽规划、仿真对比这三件事做扎实后面会顺畅很多。如果你也在做类似的中频采集解调项目希望这篇能帮你把路走顺。DDS IP核的频率控制字计算、IQ混频的符号位处理、MATLAB和FPGA结果对齐这三块是我觉得最值得投入时间的地方吃透了之后不管是改频点、调带宽、加校正都只是在这些基础上做参数修改而已。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑