做FPGA的人几乎都躲不开“造信号”这件事。通信系统里的本振、测试设备里的激励源、电机控制里的载波都需要一个频率稳定、相位可控的正弦或者余弦序列。我第一次遇到这个需求时第一反应是自己写一个查找表把正弦值存成一个ROM外面挂一个相位累加器去查表。听起来不复杂但真正做下去就会发现相位截断会带来杂散、查找表深度和资源占用难以平衡、不同位宽参数改起来要重新综合最后做到一半还是回头用官方IP。这篇文章就来说说怎么用Xilinx LogiCORE IP DDS Compiler v6.0从配置、例化到仿真把一块板子上最常用的信号源稳稳当当地做出来。内容偏实战适合手里有Vivado、想快速把DDS Compiler跑通的工程师也适合刚入门FPGA、想搞明白DDS原理的同学。1. DDS Compiler v6.0 凭什么比我手写的查找表好用1.1 原理本质相位累加器加查找表DDS的核心思想说穿了特别简单一个相位累加器一个正弦查找表。每个时钟周期相位累加器加上一个固定步长这个步长就是频率控制字累加器的值作为地址去查表查出来的结果就是当前时刻的正弦值。用生活里的例子来理解相位累加器就像钟表的秒针频率控制字相当于秒针每次跳动的角度固定步长越大走完一圈的时间越短输出信号的频率就越高。但手写这个结构和小项目里的难点在于细节全在方案权衡里。相位累加器的位宽决定频率分辨率N位累加器配合50MHz时钟理论上能做到的频率分辨率是50MHz除以2的N次方。N越大分辨率越高但查找表的地址如果也用N位ROM深度根本扛不住。常见做法是把累加器的高M位拿去查表低N-M位直接截断。这一截断相位上就引入了量化误差体现在频域上就是一堆杂散。想压低杂散就得加抖动、加噪声整形这些都是工程优化里最容易翻车的地方。1.2 IP核把脏活累活都干完了Xilinx的DDS Compiler IP核把这些事全部封装起来了。它内部根据你填的SFDR指标自动决定查找表深度和相位截断位置自动做相位抖动来改善频谱纯度还会自动把输出量化位宽和最终精度匹配好。你不用管ROM该初始化成多少点也不用纠结相位截断选多少位填几个目标参数工具全算好了。这套IP另一个值钱的地方是接口标准化。它挂的是标准的AXI4-Stream数据接口和AXI4-Lite配置接口跟其他Xilinx IP对接特别顺。比如后面接一个DAC的接口逻辑或者拿来给混频器做本振数据通路完全不用自己定义握手协议。如果自己写DDS光跟下游模块对齐数据格式就能耗半天。我还用过它的多通道模式。一个IP核配置出8路、16路正余弦信号每路频率相位独立可设通道之间天然对齐。这在相控阵、多通道信号发生器里非常实用手写的话要复制多份查找表还要自己处理通道复用工作量完全不是一个量级。2. 配置前必须搞懂的三个数频率分辨率、相位增量、SFDR2.1 公式与手算实例配置DDS Compiler之前我强烈建议先把三个数算明白频率分辨率、相位增量、SFDR。这三个数不搞清楚界面上填参数就是瞎填。频率分辨率由相位累加器位宽N和系统时钟fclk决定频率分辨率 Δf fclk / (2^N)目标输出频率 fout 对应的相位增量 Δθ (fout × 2^N) / fclk举个例子。系统时钟50MHz想输出1MHz正弦波假设相位累加器是32位。那么Δθ (1e6 × 2^32) / 50e6 ≈ 85,899,345.92取整数85,899,346实际输出频率就是85,899,346 / 2^32 × 50e6 ≈ 1.00000000093MHz。误差不到1Hz完全够用。如果累加器位宽只有16位同样的计算方式取整误差会把人逼疯所以实战中相位累加器位宽通常选24位以上32位是常态。SFDR这个指标直接决定查找表和输出位宽怎么做。简单经验公式是输出数据每增加1bit理论SFDR提升约6dB。16bit输出的理论极限大约是96dB但在有相位截断的情况下实际杂散水平取决于相位截断位数和抖动策略。DDS Compiler界面上直接有SFDR输入框你填目标值它自动反推内部参数。2.2 Vivado图形界面配置的每一项该怎么填在Vivado里打开IP Catalog搜索DDS Compiler选择v6.0版本双击打开配置界面。我按实际配置顺序逐个说。第一步是Configuration栏目。Operating Mode建议选Full Performance这是性能和资源兼顾的模式。如果你需要极低资源占用可以选Standard但输出频谱纯度会差一些。System Clock填实际输入时钟频率比如50MHz这个数非常关键后面所有频率计算都以它为基准。第二步是输出通道配置。Number of Channels填需要的通道数单路信号就填1。Output Type有三种选择Sine、Cosine、Sine and Cosine。只需要一路正弦就选Sine需要正交本振信号就选Sine and Cosine同时输出两路相位差固定90度。第三步是输出位宽和频率参数。Output Width决定输出数据的bit数我常用16bit既保证动态范围又不至于让后续处理数据量过大。Frequency per Channel这里可以直接填目标频率工具会自动算相位增量也可以切到Phase Increment手动填。我建议第一次用自动模式让工具把结果算好后面再手动复核。Phase Offset按需填比如想输出相位偏移45度的正弦波就填45。第四步是SFDR参数。界面里有个目标SFDR输入我一般填80到100dB。填的越高内部查找表深度越大资源占用越多。一个中等规模的FPGA选16bit输出、100dB SFDR完全没问题但如果资源紧张降到80dB能省不少资源。2.3 固定频率还是可编程频率这是个取舍问题DDS Compiler支持两种工作方式固定频率和可编程频率。固定频率模式下相位增量在综合时就写死了IP内部直接按这个频率输出不生成配置接口资源最省时序最简单。可编程模式下会多出一组AXI4-Lite配置端口运行过程中随时改写频率控制字和相位偏移。怎么选如果信号频率在整个系统生命周期里不变比如固定本振就用固定频率模式省掉一堆配置逻辑。如果需要跳频、扫频、或者不同工作模式下切换频率就必须用可编程模式比如通信系统里的跳频通信每次切换频率都需要CPU或者状态机通过AXI接口把新的频率控制字写进去。我在实际项目里还见过一种折中做法固定频率模式做底层本振外层挂一个混频器实现数字上变频。这种做法在信号频率切换不频繁、但需要考虑资源占用的场景下比用可编程DDS更节省逻辑资源。选型没有绝对对错关键看你自己的实时性要求和资源预算。3. 配置后的例化与最小工程接线3.1 例化模板端口逐一解析配置完成后点击Generate生成IP核再点Open IP Example Design可以查看官方例程。我这里直接给出固定频率模式下的最小例化代码这个模式不生成配置端口最干净。module dds_demo( input wire clk, // 50MHz系统时钟 input wire rst_n, // 低电平复位 output wire [15:0] sine, // 正弦输出 output wire [15:0] cosine // 余弦输出 ); wire tvalid; wire [31:0] tdata; // 同时输出sine和cosine时IP将两个数据打包成32bit // 约定的低位是sine高位是cosine assign sine tdata[15:0]; assign cosine tdata[31:16]; dds_compiler_0 u_dds ( .aclk (clk), .aresetn (rst_n), .m_axis_data_tvalid (tvalid), .m_axis_data_tdata (tdata) ); endmodule这里有几个关键点必须说清楚。aclk是IP的时钟输入频率必须和之前配置的System Clock一致。aresetn是低有效复位工程里最常见的坑就是复位信号没拉高导致IP一直处于复位状态输出始终是0。m_axis_data_tvalid是输出有效标志DDS Compiler在正常工作时每拍都输出有效数据tvalid会持续拉高。m_axis_data_tdata按配置打包了sine和cosine具体排列方式以IP的Summary界面说明和仿真波形为准。3.2 一个能直接跑通的最小设计把上面的模块放进一个工程再接上时钟和复位就是一个能用的信号源。如果暂时不想搭完整的Testbench也可以用Vivado自带的硬件管理器把ILA核挂在tdata上直接抓波形。不过正常做验证还是建议走仿真后面我详细说怎么搭仿真环境。要注意的是如果输出只选了Sine或者Cosinem_axis_data_tdata的位宽就等于Output Width不再是2倍。很多同学第一次直接用示例代码发现tdata位宽对不上就是这个原因。还想提醒一个细节DDS输出的数据和DAC的对接方式。DDS Compiler输出的是二进制补码格式也就是带符号数。很多DAC芯片的输入接口要求的是二进制偏移码两者之间差一个最高位取反的操作。第一次做数模转换的同学经常在这里栽跟头波形出来是反的或者中间有个大台阶往往就是编码格式没转换。4. 仿真验证从波形看DDS是否真的在工作4.1 编写一个能马上用的Testbench仿真是我最推荐的验证手段因为可以精确看到频率、相位、幅度还能直接在波形上做测量。下面这个Testbench直接可用时钟50MHz跑完约200微秒能观察到200个周期的1MHz正弦波。timescale 1ns/1ps module tb_dds_demo; reg clk; reg rst_n; wire [15:0] sine; wire [15:0] cosine; dds_demo u_dut( .clk (clk), .rst_n (rst_n), .sine (sine), .cosine (cosine) ); initial begin clk 1b0; forever #10 clk ~clk; // 20ns一个周期50MHz end initial begin rst_n 1b0; #100; // 释放复位 rst_n 1b1; #200_000; // 跑200us $finish; end endmodule在Vivado里把tb_dds_demo设置为仿真顶层运行Behavioral Simulation即可。DDS Compiler这样的官方IP在Vivado Simulator里仿真不需要额外配置库文件工具会自动识别IP的仿真模型。如果你用的是ModelSim或者QuestaSim需要把IP的仿真库编译好这个操作在Vivado的Tools菜单下有Compile Simulation Libraries向导选择对应工具和器件型号一键完成。4.2 波形分析与判断方法仿真跑完波形窗口里看到的是数字二进制补码一堆高低电平看久了眼睛疼。Vivado Simulator里可以把波形显示切换成模拟模式选中sine信号右键菜单选择Waveform Style然后选Analog。这样能看到连续的正弦曲线方便直接检查。判断DDS是否正常第一看波形形状正常的正弦波应该平滑无台阶如果看到明显阶梯说明输出位宽太低或者SFDR目标填小了。第二看周期1MHz正弦波的周期应该是1微秒。把两个游标放到波形的两个相邻峰值上查看时间差如果等于1000ns就说明频率正确。第三看幅度16bit输出的正弦峰值大约在32767附近这是二进制补码能表示的最大正数如果峰值非常小或者接近0基本可以确定有问题。我自己常用的一个辅助手段是用初始语句在仿真后期打印一段时间内的数据值配合Excel做快速FFT看一下主瓣和杂散水平。虽然麻烦一点但能直观看到SFDR的真实表现比只用眼睛看波形可靠得多。Vivado Simulator里也可以直接在波形窗口添加频谱视图在Waveform窗口右键添加Digital/FFT视图选择对应的信号就能看频谱分布。5. 在线调频通过AXI接口动态改写频率控制字5.1 动态调频的使用场景固定频率模式适合信号频率不变的场景可一旦功能需求变成多频点跳变就得上可编程模式。比如雷达系统的线性调频、通信系统的跳频本振、医疗超声里的扫频激励这些都需要在运行过程中快速改变输出频率。DDS Compiler v6.0在配置时如果勾选了Phase Increment Programmability为Programmable就会生成AXI4-Lite风格的配置接口通过它在线改写频率控制字。5.2 配置数据格式与写时序配置接口的关键信号是s_axis_config_tvalid、s_axis_config_tready和s_axis_config_tdata。其中s_axis_config_tdata的位宽取决于可编程项的总宽度如果同时支持相位增量和相位偏移可编程那么总线位宽等于两者之和。数据排列规则是低N位对应相位增量高M位对应相位偏移。有一个非常容易踩坑的地方就是相位增量的位数N并不一定等于你在界面里看到的输出位宽。这个N是相位累加器的位宽通常在IP的Summary界面里有显示。写配置数据的时候必须按照这个N来放频率控制字放错位数配置出来的频率会完全不对。写配置的时序不复杂核心是等tready握手。下面给一个简单的状态机示例演示如何把一个新的频率控制字写入DDSparameter N 32; // 相位累加器位宽以IP Summary为准 parameter NEW_FREQ_CTRL 32d85899346; // 对应1MHz50MHz reg [1:0] cfg_state; reg s_axis_config_tvalid; reg [N-1:0] s_axis_config_tdata; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cfg_state 2d0; s_axis_config_tvalid 1b0; s_axis_config_tdata d0; end else begin case (cfg_state) 2d0: begin s_axis_config_tvalid 1b1; s_axis_config_tdata NEW_FREQ_CTRL; cfg_state 2d1; end 2d1: begin if (s_axis_config_tready) begin s_axis_config_tvalid 1b0; cfg_state 2d2; end end endcase end end这个状态机写完后DDS会在几个时钟周期内完成频率切换之后的输出就是新的频率。要强调的是配置写入频率不能太高翻看PG141文档两次配置之间需要满足一定的间隔时间否则可能出现配置数据未被正确采到的偶发问题。我在项目里通常的做法是配置完成后等待一小段时间再发起下一次配置宁可慢一点也要稳。5.3 动态配置的实测现象与验证方法配置完成后怎么确认真的生效了最简单的方法看波形周期。把配置时刻标注在仿真波形上观察配置完成后第一个完整周期的长度对比目标频率的周期值。比如从2MHz切到1MHz波形周期应当从500ns变成1000ns。还有一种验证方式是观察m_axis_phase_tdata这个端口输出的是当前相位累加器值。配置变化后相位累加器的步长会改变体现在phase信号的斜率上。用两个游标量一下phase信号固定角度之间的时间间隔也可以反推当前频率。这个方法在多通道同时调频的场景特别有用可以快速确认所有通道是否都切换到了目标频率。6. 常见问题速查与避坑记录6.1 问题与排查速查表我把这些年用DDS Compiler遇到的高频问题整理成了一张表遇到类似现象可以直接对照排查。现象可能原因解决办法输出一直是0tvalid也不拉高aresetn没有释放或者复位信号极性接反确认复位信号拉高且释放后至少等一个时钟周期输出频率和目标频率差很多System Clock填错或者相位增量手动填错重新确认系统时钟频率复核Δθ计算输出波形有严重台阶Output Width太小量化噪声大提高输出位宽到12bit以上频谱杂散特别高SFDR目标填得太低或相位截断严重提高SFDR目标值重新生成IP同时输出sin/cos时两路数据对调sin/cos在tdata中的位置理解反了按IP Summary或仿真波形确认高半和低半的分配AXI配置写了没反应没有等待tready或配置数据位没放对按时序完成握手复查配置总线的位宽排列多通道模式下输出顺序错乱Channel Sequence配置和取数逻辑不一致检查通道顺序配置确认取数时按序读取仿真跑不起来报错提示缺库仿真工具缺少IP的仿真模型库用Compile Simulation Libraries编译对应仿真库6.2 我自己踩过的几个坑第一个坑是复位极性。DDS Compiler的复位端口是aresetn低有效。我第一版代码图省事直接接到了系统的低有效复位上但忽略了IP例化时又做了一次取反结果整个模块一直在复位状态波形全平。这个问题的隐蔽性在于综合报告里不会有任何告警只能靠仿真波形发现。第二个坑是固定频率模式下误以为可以动态改频。有一版项目把DDS配成了固定模式但代码里预留了配置端口例化时端口没连就放了空后面想在线调频时发现根本没有配置接口只能重新生成IP核。所以项目前期一定要把“频率是否可变”这个需求确认清楚再定配置模式。第三个坑是相位增量的取整误差。手动算频率控制字的时候如果N取得不够大取整误差会直接反映在输出频率上。我做过一次N16的配置50MHz时钟想输出3.3MHz理论相位增量为4325.376取整4325之后实际频率算出来是3.2997MHz偏差0.3kHz看着不大但对于要求频率精确到Hz级的高精度信号源这个误差就不可接受了。后来全部改用32位累加器这个问题彻底消失。第四个坑是关于相位偏移的符号问题。配置阶段填Phase Offset时不同版本的IP处理方式略有不同有的是度数有的是弧度。仿真时一旦发现相位偏移方向和预想相反先回去确认配置界面的单位不要急着改代码。7. 最后分享几个实用小技巧写到这里整个DDS Compiler v6.0的配置、例化和仿真流程就完整走了一遍。最后分享一个我平时开发特别常用的技巧如果只是想验证一个IP核功能不急着写复杂的Testbench直接在Vivado里右键IP核选择Open IP Example Design工具会自动生成一个包含官方例化代码和仿真工程的完整工程。先把官方的参考设计和仿真跑一遍确认现象正常以后再照着自己的需求改比对着文档啃效率高很多。还有个技巧是DDS输出数据的后处理。如果后面要接DAC记得做二进制补码到偏移码的转换如果要给混频器用可以直接把sine和cosine两路一起用省去自己做90度移相。多通道场景下即使暂时只用一个通道我也建议先按最终通道数配置IP核避免后面扩展时重新生成IP导致接口变化。信号生成是FPGA里非常基础也非常核心的一块能力。把DDS Compiler这类官方IP用熟练不仅省时间更重要的是能拿到官方调好的频谱纯度和稳定性。下次再有人问我“FPGA怎么产生一个干净的正弦波”我的回答永远是同一句先打开Vivado搜DDS Compiler把上面的流程走一遍。
阅读完成 · 觉得有帮助?