简介:面向FPGA开发者的DDS(直接数字频率合成器)IP核应用资料,重点讲解如何基于Xilinx DDS Compiler IP生成单频信号与线性调频(LFM)信号。文档深入解析DDS工作原理,涵盖输出频率、频率分辨率与相位增量的计算公式,并讨论相位累加器位宽对频率分辨率的影响。配置方面,逐项说明系统参数与硬件参数的选择差异,详解Configuration、Implementation和Detailed Implementation标签下的设置要点,包括SFDR、相位可编程模式(Fixed、Programmable、Streaming)以及输出信号形式等。同时,通过单频和线性调频的FPGA设计示例,展示从参数计算到IP核配置、波形验证的完整流程,可直接用于雷达、通信、测试测量等领域的信号生成开发。资源为单个PDF文件,大小3.03MB,目前已有2763人学习下载,适合具备一定FPGA基础、希望系统掌握DDS IP核配置与LFM信号实现的工程师。 FPGA工程师在项目里遇到“用DDS IP产生信号”这种需求太常见了,尤其是在雷达、通信测试设备里。单频信号好办,DDS IP配好固定频点直接输出就行;但“线性调频”这四个字一出来,好多新手就卡住了——LFM不是改一个参数就能出来的,它要求瞬时频率随时间线性变化,而且不是线性改频率控制字那么简单。这篇就围绕Xilinx的DDS Compiler IP核,完整讲清楚单频和LFM两种模式怎么实现、参数怎么算、仿真怎么验证,以及我实际调板子时踩过哪些坑。
这篇内容适合三类人:刚入门FPGA、第一次接触DDS IP的;用DDS做信号源但只会配单频的;以及做雷达/通信系统,需要自己生成Chirp信号的。整个过程基于Vivado环境展开,但原理同样适用于其他厂家的DDS IP。
1. 开局先理思路:FPGA里用DDS IP做信号发生器的核心逻辑
1.1 DDS的工作原理:就是数字域“赶表盘”
直接数字频率合成(Direct Digital Synthesizer,DDS)的核心思想其实特别朴素。你想象一个圆形的表盘,表盘边缘刻着正弦波一个周期的幅值,每来一个时钟脉冲,指针就走一步,指针指向的数值就是这一时刻的输出。这个“走一步”的角度增量,就是频率控制字(Frequency Tuning Word,FTW)。增量越大,指针转一圈的时间越短,输出频率越高;增量越小,输出频率越低。这个“表盘”在FPGA里就是一块ROM查找表,存放一个周期正弦波的采样值;那个“走指针”的模块叫相位累加器。输出频率和参数的关系就一条公式:
[ f_{out} = \frac{f_{clk} \times FTW}{2^N} ]
其中N是相位累加器的位宽,f_clk是工作时钟。这个N直接决定了频率分辨率:
[ \Delta f = \frac{f_{clk}}{2^N} ]
举个例子,100MHz时钟,32位累加器,分辨率大约0.023Hz,这对于绝大多数应用来说已经完全够用了,这也是DDS在数字频率合成里优势明显的原因——频率切换极快、分辨率高、相位连续。
1.2 单频和线性调频的数学本质差异
单频信号在DDS里的实现就是让FTW保持一个固定值,相位累加器匀速向前走,输出的正弦波频率恒定。这时的相位随时间线性增加:
[ \varphi(t) = 2\pi f_0 t ]
而线性调频信号(Chirp)的瞬时频率是随时间线性变化的,设起始频率为f0,终止频率为f1,扫频时间为T,则:
[ f(t) = f_0 + \frac{f_1 - f_0}{T}t ]
瞬时频率的积分就是瞬时相位:
[ \varphi(t) = 2\pi\left(f_0 t + \frac{f_1 - f_0}{2T}t^2\right) ]
注意这个二次项,相位随时间按平方规律增长。对应到DDS的数字域,就是相位累加器的步进——频率控制字——需要随时间线性增加。也就是说:单频是FTW固定不变,LFM是FTW按固定斜率线性递增。明白了这一点,后面配置IP和写代码的方向就清晰了。
1.3 为什么直接选用Xilinx的DDS Compiler IP核
有朋友可能会说,DDS原理这么简单,我自己用Verilog写一个相位累加器加ROM查找表不就完了?当然可以,两三百行代码的事。但实际工程里我还是强烈推荐用Xilinx的DDS Compiler IP核,原因有几点:
第一,查找表优化过。自己做ROM查找表,如果只用相位的高16位做地址,ROM深度就是65536,占用大量Block RAM资源;IP核内部会利用正弦波的对称性把存储量压到四分之一,还能加上抖动(dithering)技术来提升无杂散动态范围(SFDR),这些细节自己写代码很难做到位。
第二,接口现成。DDS Compiler内置了AXI4-Stream接口,支持频率控制字和相位偏移的动态配置,后续做LFM、扫频、相位调制都非常方便。
第三,仿真模型完善。IP核生成的时候会附带行为级仿真模型,可以直接配合AXI总线做功能验证,不用自己搭复杂的测试模型。
2. 关键参数怎么配:影响输出质量的几个硬指标
用DDS IP前,一定要先把几个核心参数想清楚,否则写代码写到一半才发现配置不对,返工很痛苦。
2.1 相位累加器位宽和频率分辨率的关系
打开DDS Compiler配置界面,第一个要设置的就是System Clock,即DDS的工作时钟频率。接着是Number of Channels,一般选1。最关键的是在“Parameter Selection”里选择系统参数还是硬件参数。我习惯选“Hardware Parameters”,直接指定Phase Width(相位累加器位宽)和Output Width(输出数据位宽)。
提示:相位累加器位宽决定了频率分辨率,不是越高越好。32位在100MHz时钟下能到0.02Hz级别,对于绝大多数信号源需求已经绰绰有余。位宽越大虽然精度越高,但动态配置频率控制字时数据位宽也变大,接口占用资源变多,综合频率也会受影响。
在实际工程里,除非要做极低频率、极高精度的信号源,否则32位相位累加器是通用选择,兼顾精度和资源。
2.2 SFDR和无杂散动态范围那些事
SFDR(Spurious-Free Dynamic Range)是DDS最重要的性能指标之一,它衡量的是输出频谱中最大杂散分量与主信号之间的功率差。FPGA里没有实际DAC,这里的SFDR主要受相位截断和幅度量化误差影响。理想情况下,查找表地址位宽每增加1位,SFDR提升大约6dB。Xilinx DDS Compiler配置界面里会让你选择“Output Frequency”或者“SFDR”,一般选目标SFDR,比如80dB或90dB,IP会自动算出需要多少位的相位截断。需要注意:配置成90dB以上时ROM资源会明显增大,如果后面接了外部DAC芯片,还要综合看DAC的实际性能,不必盲目追求过高SFDR。
2.3 输出位宽与数据格式的选择
Output Width指输出正弦波的幅度量化位数,常见选择16位。输出位数越高,幅度量化噪声越小,但也就意味着后级的处理位宽都要跟着提。DDS Compiler的数据输出格式有两种:有符号数和无符号数。一般建议选有符号数(Two's complement),这样后级做数字下变频、混频、FFT分析时不需要再做符号转换。默认情况下IP在输出端会加TLAST、TVALID等AXI信号,如果只是一路连续数据流,这些信号可以不用管,直接把TDATA引脚接出去就行。
2.4 动态调频的关键:S_AXIS_CONFIG配置接口
要实现LFM,绕不开动态配置频率控制字。DDS Compiler的配置方式有三种:固定模式(Fixed)、可编程模式(Programmable)、以及流式模式(Streaming)。固定模式在编译前就定死了FTW,无法动态改频,只适合单频输出;可编程和流式模式都会引出S_AXIS_CONFIG接口,这才是LFM的正确打开方式。
S_AXIS_CONFIG接口的数据格式要注意,TDATA的位宽等于相位累加器位宽加相位偏移位宽,具体排列是高位是相位偏移(Phase Offset),低位是频率控制字(Phase Increment)。比如32位累加器、16位相位偏移,那TDATA就是48位,高16位是相位偏移,低32位是频率字。这个排列顺序搞反了,输出波形就会变得奇奇怪怪——最常见的就是频率对不上、波形乱跳。
3. 实操:Vivado里把DDS IP跑起来
配置参数说清楚了,直接进Vivado操作。我用的版本是Vivado 2020.2,DDS Compiler对应的版本是6.0,界面可能有细微差异,但总体逻辑一致。
3.1 工程创建与IP例化
在IP Catalog里搜“DDS Compiler”,双击打开。配置界面有几个关键地方:
- System Clock:填100(MHz),代表DDS工作时钟频率。
- Parameter Selection:选“Hardware Parameters”。
- Phase Width:32。
- Output Width:16。
- Output Selection:选“Sine”或者“Sine and Cosine”,看需求。雷达系统经常需要正交双路信号,直接选同时输出正余弦最方便。
- Phase Increment Programmability:选“Programmable”,这样S_AXIS_CONFIG接口才会出现。
在“Detailed Implementation”页里,还可以勾选“Dither”选项,对SFDR有好处,建议开启。另外要留意IP核给你算出来的Spurious Free Dynamic Range值,如果发现比预期低,说明相位截断位数不够,需要返回调整。
生成IP后,在Sources窗口可以看到例化模板,复制到自己的顶层模块即可。
3.2 单频信号生成的Verilog例程
先写最简单的单频。假设100MHz时钟,需要输出10MHz正弦波,频率控制字计算如下:
[ FTW = \frac{10\text{MHz} \times 2^{32}}{100\text{MHz}} = 429496730 \approx \text{0x1999999A} ]
需要注意的是,这个公式算出来的结果要取整。如果取整误差导致的频率偏差无法接受,可以通过提高相位累加器位宽来改善。
单频模式下如果用的“Programmable”配置接口,有一个很隐蔽的坑:S_AXIS_CONFIG的TVALID信号不能永远拉低,否则IP不会采样你给过去的FTW。正确做法是上电复位后,把配置数据放在TDATA上,然后把TVALID拉高至少一个周期,再拉低,让IP完成一次配置握手。
module dds_single_tone( input wire clk, input wire rst_n, output wire [15:0] sine_out, output wire [15:0] cosine_out ); localparam [31:0] FTW = 32'h1999_999A; // 10MHz @ 100MHz reg [47:0] config_r; reg valid_r; reg send_done; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin config_r <= 48'd0; valid_r <= 1'b0; send_done <= 1'b0; end else if (!send_done) begin config_r <= {16'd0, FTW}; // 高16位相位偏移=0,低32位频率控制字 valid_r <= 1'b1; send_done <= 1'b1; end else begin valid_r <= 1'b0; end end dds_compiler_0 u_dds( .aclk (clk), .s_axis_config_tvalid (valid_r), .s_axis_config_tdata (config_r), .m_axis_data_tdata ({cosine_out, sine_out}), // 取决于IP配置顺序 .m_axis_data_tvalid (), .m_axis_phase_tvalid (), .m_axis_phase_tdata () ); endmodule有一点要注意:当Output Selection同时选“Sine and Cosine”时,M_AXIS_DATA的TDATA是“高16位余弦,低16位正弦”还是相反,要看IP核生成时的引脚定义。Vivado的DDS Compiler一般是“高通道在前”,多通道时尤其明显。如果仿真发现正弦和余弦信号对调了,直接改一下取位顺序就行,不影响功能。
3.3 LFM线性调频信号的三种实现方式
LFM的实现方式我梳理下来,基本就三种,各有适用场景:
第一种:用S_AXIS_CONFIG动态累加FTW。
这是最直观的方式,在FPGA里写一个累加器,每个时钟周期(或每个采样周期)让FTW加上一个固定步进STEP,然后送给DDS的配置接口,模拟FTW线性增长。注意,这里控制的是FTW的变化斜率,即频率变化率。
第二种:把DDS配成“Phase Increment”可编程模式,利用内部Phase Accumulator自动做相位累加。
这种方式是通过改变“Phase Increment”寄存器的方式工作的,本质上和第一种是一个意思,只是把累加器的活交给了IP核内部。
第三种:直接计算瞬时频率字,每隔N个时钟周期更新一次FTW。
这种方式适合扫频时间比较长、单点频率字更新不需要每周期都变的场景。把扫频时间分成M段,每段计算好对应FTW,用状态机或者ROM存起来,按顺序喂给配置接口。这种方式最可控,但灵活性差一点,适合固定指标的LFM发生器。
实际做雷达Chirp信号时,我几乎都用第一种方式,理由就一个——扫频连续、相位连续,频谱干净。
FTW步进STEP的计算是关键。假设需要从f0扫到f1,扫频时间为T,系统时钟为f_clk,相位累加器位宽为N,那么:
[ STEP = \frac{(f_1 - f_0) \times 2^N}{f_{clk}^2 \times T} ]
注意这里分子分母的量纲,f_clk²是时钟频率的平方,单位是Hz²,而T是秒,乘起来才能和分子的Hz×2^N约掉。
如果STEP是小数怎么办?这种情况在实际工程中常见。比如100MHz时钟、32位累加器,扫频10MHz到20MHz,扫频时间1ms,算出来STEP大约是一千多左右的小数。这时候要先把STEP取整,再把余数的累积误差通过一个小累加器(小数累加器)来消除,原理类似DDS的频率分辨率,只是这个累加器是给FTW补偿的。如果不做这一步,扫频终点频率会有固定偏移,有时差个几十kHz,在雷达测距里误差就大了。
module dds_lfm( input wire clk, input wire rst_n, output wire [15:0] sine_out, output wire [15:0] cosine_out ); localparam [31:0] F0_WORD = 32'h1999_999A; // 起始频率10MHz localparam [31:0] F1_WORD = 32'h3333_3333; // 终止频率20MHz localparam [31:0] STEP = (F1_WORD - F0_WORD) / 32'd1024; // 扫频段数 reg [31:0] fword; reg [15:0] scan_cnt; reg [47:0] config_r; reg cfg_valid; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin fword <= F0_WORD; scan_cnt <= 16'd0; config_r <= 48'd0; cfg_valid <= 1'b0; end else begin if (scan_cnt < 16'd1024) begin scan_cnt <= scan_cnt + 16'd1; fword <= fword + STEP; config_r <= {16'd0, fword + STEP}; cfg_valid <= 1'b1; // 保持持续更新,实际中可加tdest握手 end else begin // 扫频结束,保持末频输出 config_r <= {16'd0, F1_WORD}; cfg_valid <= 1'b1; end end end dds_compiler_0 u_dds( .aclk (clk), .s_axis_config_tvalid (cfg_valid), .s_axis_config_tdata (config_r), .m_axis_data_tdata ({cosine_out, sine_out}), .m_axis_data_tvalid (), .m_axis_phase_tvalid (), .m_axis_phase_tdata () ); endmodule上面这个例子有点粗糙,实际里STEP的计算不是简单除整这么简单。最好的做法是用上面的公式算完STEP后,把STEP设成整数,再用一个余数累加器做补偿,扫频曲线才精确。另外这里扫频结束之后我让FTW保持不变,也就是输出停在20MHz。实际项目中扫频结束后很多时候要关断输出,这个可以根据项目具体需求来处理。
3.4 仿真验证与结果判读
代码写完,直接跑行为仿真。先说单频的仿真:testbench里产生100MHz时钟,复位释放后观察M_AXIS_DATA的波形。如果IP配置的是“Sine and Cosine”双通道输出,你会看到sine_out和cosine_out两条相位差90度的正弦波。用光标量一下周期,10MHz信号的周期应该是100ns。
LFM仿真里,肉眼观察波形只能看到频率逐渐变密,但具体扫频斜率是否准确很难判断。更严谨的做法是:把输出数据写进文本文件,再导入MATLAB或Python做时频分析。Vivado的仿真模型里用$fopen和$fwrite可以方便地导出。短时傅里叶变换(STFT)能看到一条斜线,斜线的斜率就是扫频斜率,对照设计指标就能判断是否正确。
4. 踩坑实录:常见问题与排查技巧
最后这部分是我实际调板子、调仿真过程中积累的教训,可以说每一条都是真金白银换来的。
4.1 频率控制字算对了但输出频率不对
这个问题出现过好几次。第一反应就是检查位宽。DDS Compiler的TDATA位宽是相位偏移位宽加上频率控制字位宽,如果相位偏移是16位、频率控制字是32位,TDATA总位宽就是48位,可TDATA位宽写成了32位,导致频率字被截断,输出频率自然不对。
另一个高频原因:s_axis_config_tvalid时序不正确。配置接口是AXI4-Stream握手机制,如果TVALID在TDATA变化前就提前拉高,IP会在拿握手的那个时钟上升沿抓到不是最新数据的值。强烈建议手动拉高TVALID的同时确认TDATA已经稳定至少一个周期。
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 输出全0 | 复位一直有效或TVALID从未拉高 | 检查复位信号和TVALID波形 |
| 输出频率与预期差很多 | TDATA位宽/拼接顺序错误 | 核对IP接口定义的TDATA排列 |
| 频率不稳定、偶尔跳变 | 配置握手时序不满足 | 检查TVALID/TDATA是否建立保持足够 |
| 输出波形幅度异常 | 输出位宽截断错误 | 检查M_AXIS_DATA取位范围 |
4.2 输出波形杂散严重
SFDR不达标的常见原因有三个:第一,Dither没有打开,相位截断的周期性误差导致明显的杂散峰;第二,后级处理数据位宽不够,比如DDS自己输出了16位数据,你后级只取了高8位,量化噪声会被放大好几dB;第三,查找表地址位宽不足,这就要回到IP配置里增大“Phase Width”或目标SFDR参数。
不过没必要在FPGA里追求太夸张的SFDR,实际信号链里DAC、放大器、阻抗不匹配等因素对SFDR的影响往往比DDS本身大得多。我一般的判断标准是:DDS输出的SFDR比系统需求高出10dB左右就够了,把资源留给后级。
4.3 仿真正常但上板后LFM频率变化曲线不对
仿真跑得好好的,下载到板子上就不对了,这个坑踩得非常多。首先查DDS的输入时钟是否有抖动。DDS对时钟频率非常敏感,如果你用MMCM/PLL生成的时钟,要确认时钟频率设置和实际一致。更隐蔽的问题是复位信号——很多开发板的复位是异步的,且复位释放时如果恰好赶上DDS内部状态机在握手的边沿,可能导致IP初始化失败。
4.4 扫频曲线的“阶梯感”
用第一种动态累加FTW的方式做LFM时,如果每次配置的STEP太大,瞬时频率的变化就会呈现明显的阶梯状,表现出来就是时频图上不是一条平滑斜线,而是一级一级的台阶。这是因为FTW更新是离散的,每个更新点之间的瞬时频率是恒定的。
要减小这个台阶效应,方法有两个:一是减小FTW步进,让频率变化更细腻,这要求STEP尽量小,但太小会导致扫频时间变长;二是提高DDS的工作时钟,让频率字的更新周期更短。实在不行就用第二种方式,让IP内部的相位累加器自己处理相位连续累加,比外部手写累加更平滑。
4.5 仿真数据量太大,卡到怀疑人生
LFM扫频时间动辄几百微秒甚至毫秒级,行为仿真按1ns时间精度去跑,数据量是天文数字。我踩过这个坑之后,现在的做法是:先缩短扫频时间做原理验证。比如实际需求是1ms的Chirp,仿真时缩短到100us,先把功能逻辑调通,确认波形趋势正确,再跑一次完整时长的仿真做最终验证。这样既保证验证充分,又不会把时间全耗在仿真上。
另外,仿真过程中把DDS输出数据用$fwrite实时写入文本,在仿真的同时生成数据文件,等仿真结束再做STFT分析,比直接在波形窗口里肉眼看波形要高效得多。
最后再分享一个小技巧
做LFM的DDS时,很多人忽略了一个细节:DDS输出的是数字域正弦波的采样值,它本身不含“扫频起始时刻”的信息。如果你想把LFM的扫频零点精确对齐到外部触发信号上,建议用IP核的S_AXIS_PHASE端口做一个相位同步。在触发信号到来时,把相位偏移清零,同时让FTW从初始值开始递增,这样每次触发的扫频起点都是同一个相位,多帧累加/平均处理时才不会产生相位跳变。这一点在雷达回波处理和相参积累里非常关键,不少工程师就是栽在这个不起眼的“起始相位对齐”上。
DDS IP做单频和LFM,本质上就是把一个数学公式映射到数字电路上。公式弄明白了、配置对上了、握手信号别搞错,剩下就是水磨工夫的调试。上面这些内容是我在实际项目里踩坑踩出来的经验,完整走完一遍流程后,你会发现FPGA做信号源这条路其实很宽——遇到不会的就翻Xilinx的PG141文档,再不行就跑仿真加看波形,问题总能定位出来的。
本文还有配套的精品资源,点击获取