简介:以FPGA为平台实现直接数字频率合成(DDS)的完整工程资料包,适合数字电路、通信电子线路课程设计,以及备战电子设计竞赛的本科生和研究生。内容不仅包含Verilog源码与仿真工程,还配有视频教学和原理图、PCB图,帮助从算法原理到硬件落地全面理解。压缩包共收录2808个文件,涵盖.v/.vhd源码、Quartus工程配置、.do仿真脚本、.rpt报告、.hex/.mif波形初始化数据、.wmv视频及PDF/XLSX说明文档等,整体大小约700MB,目录结构清晰,按模块区分便于检索。目前已有1441人学习下载。通过该资料可快速复现DDS波形发生器,学习相位累加器、查找表、波形存储与D/A转换等关键环节;视频与文档能辅助排错,适合FPGA入门到进阶的实践训练,也可移植到信号源、调制解调等项目中复用。 做FPGA开发的人,几乎绕不开DDS(Direct Digital Synthesis,直接数字频率合成)这个东西。不管是信号发生器、通信系统中的调制解调,还是电机驱动里的SPWM生成,DDS都是最常用的信号产生方案之一。我这次抽空整理了一套基于FPGA的DDS信号发生器Verilog代码,工程打包在DDS_Verilog.rar里,正好把整个设计思路、代码结构和调试过程完整梳理一遍,分享给正在入门FPGA或者需要做信号源的朋友。
这套东西适合谁用?如果你正在学Verilog,想找一个有实际意义、能上板跑的综合项目;或者你在做仪器仪表、通信基带、音频处理,需要一个频率和相位都可以灵活配置的正弦波/方波/三角波发生器,那这份代码可以直接参考甚至移植。代码我用了纯Verilog编写,不依赖Xilinx或Altera的特殊IP核(ROM用了distributed RAM原语,替换也容易),综合性和可移植性都不错。
1. 项目概述与整体方案选型
1.1 DDS基本原理:一个加法和一张表
DDS的核心思想可以概括为“相位累加 + 查表映射”。具体说,就是一个N位的相位累加器,每个时钟周期累加一个频率控制字(Frequency Control Word,FCW),累加器的高位作为地址去查一张预先存好波形数据的查找表(Look-Up Table,LUT),输出的数据经过DAC或者PWM就得到模拟波形。
这里最关键的一个公式是输出频率与系统时钟、累加器位宽、频率控制字的关系:
f_out = f_clk * FCW / 2^N其中N是相位累加器位宽,f_clk是系统工作时钟。从这个公式可以看出,输出频率的分辨率是由累加器位宽N决定的,N越大,频率分辨率越高。比如系统时钟100MHz,N=32位,那么频率分辨率就是100MHz / 2^32 ≈ 0.023Hz,能做到非常精细的频率步进。
为什么用FPGA做DDS而不是用单片机?原因很简单:DDS的相位累加和查表是纯粹的并行流水线操作,FPGA天然适合这种结构。用STM32等MCU虽然也能做,但受限于主频和指令周期,输出频率上不去,而且占用的CPU资源太高。FPGA可以把累加器跑到几百兆,同时信号更新延迟是确定的时钟周期数,这个确定性在闭环控制系统里至关重要。
1.2 为什么把累加器位宽做这么大
很多初学者会问,我只要产生一个1kHz的正弦波,需要32位的累加器吗?用16位甚至8位不行吗?
这里要区分两个概念:相位分辨率和幅度分辨率。累加器位宽N决定了相位分辨率,也就是频率步进精度;而查找表的地址位宽(也就是截断后的位数)决定了一次查表能找到多少个相位点。这两个数在实际工程里往往是分开的——累加器全精度累加,查表时只取高M位作为地址。
举个例子,如果N=32、M=12,意味着相位累加点数是2^32个,但ROM里只存了2^12=4096个点。那么每次查表相当于用高12位去索引,低20位相当于相位细分,虽然丢弃了,但输出的平均频率仍然是精准的。这种“全精度累加、高位截断查表”的做法,是DDS工程实现的标准技巧,既能保证频率精度,又不会让ROM大得离谱。
做一个4096点深、12位宽的查找表,用FPGA片内的Block RAM实现只需要一个BRAM,资源开销非常小。如果你想把谐波和杂散做得更低,可以增加M的位数,比如用14位或16位地址,代价是ROM容量翻倍,但对现代FPGA来说依然微不足道。
2. 核心模块分解与Verilog代码剖析
2.1 顶层架构:信号发生器应该拆成几个模块
整套设计我拆成了四个模块:时钟管理模块(clk_gen)、频率控制字生成模块(freq_ctrl)、DDS核心模块(dds_core)、波形输出接口模块(wave_out)。模块划分的原则是“每个模块只干一件事”,这样后期维护和复用都方便。
顶层模块例化这四个子模块,对外接口只有系统时钟、复位、频率设置信号(并行接口或SPI接口),以及最终的波形输出总线。如果你想做多通道波形输出,例化多个dds_core,各自给不同的频率控制字就行,这是FPGA并行优势的天然体现。
下面给出dds_core模块的完整代码,这是整个工程的核心,值得逐行看明白:
module dds_core #( parameter N = 32, // 相位累加器位宽 parameter M = 12, // ROM查找表地址位宽 parameter DATA_W = 12 // 输出波形数据位宽 )( input wire clk, input wire rst_n, input wire [N-1:0] fcw, // 频率控制字 output reg [DATA_W-1:0] wave_out ); reg [N-1:0] phase_acc; // 相位累加器 always @(posedge clk or negedge rst_n) begin if (!rst_n) phase_acc <= 32'd0; else phase_acc <= phase_acc + fcw; end // 查找表例化 wire [M-1:0] rom_addr = phase_acc[N-1:N-M]; wire [DATA_W-1:0] rom_data; rom_sin u_rom ( .clk (clk), .addr (rom_addr), .data (rom_data) ); // 输出寄存器,锁存一层,避免组合路径过长 always @(posedge clk or negedge rst_n) begin if (!rst_n) wave_out <= {DATA_W{1'b0}}; else wave_out <= rom_data; end endmodule2.2 波形查找表:正弦波数据生成的两种方式
ROM查找表是DDS的心脏,里面的波形数据怎么来,直接影响最终信号质量。常见做法有两种:
第一种,用Matlab或Python生成.coe文件或.mif文件,然后在Vivado/Quartus里用IP核例化ROM,把波形数据初始化进去。这种方式流程成熟、可控性强,正弦数据可以自己量化,适合对信号质量有要求的场景。
第二种,用FPGA片内的分布式RAM(Distributed RAM)配合initial块直接初始化数据,适合数据量不大、不想走IP核流程的情况。下面是一个用initial块生成32点正弦查找表的例子:
module rom_sin #( parameter ADDR_W = 5, parameter DATA_W = 8 )( input wire clk, input wire [ADDR_W-1:0] addr, output reg [DATA_W-1:0] data ); reg [DATA_W-1:0] rom [0:2**ADDR_W-1]; initial begin rom[0] = 8'd128; rom[1] = 8'd153; rom[2] = 8'd177; rom[3] = 8'd198; // 这里省略中间数据,实际工程可用脚本自动生成 rom[31] = 8'd103; end always @(posedge clk) begin data <= rom[addr]; end endmodule实际项目里,我不会手写这些数据,而是用一段Python脚本直接生成.coe文件,再把文件加载到Vivado的Block Memory Generator IP核里。生成正弦查找表的Python代码很简单,关键是把正弦浮点值量化为整数,并且注意数据位宽和直流偏置的处理。
关于波形数据量化,有个细节要提醒:单极性输出(比如0~4095)适合直接接DAC,双极性输出(比如-2048~+2047)适合后续做正交调制或叠加处理。如果你要拿波形数据去做乘法或混频,建议用有符号数格式存储,避免后面反复做无符号到有符号的转换。这块处理不好,后续代码写起来非常别扭。
2.3 频率控制字的计算:从公式到寄存器
有了前面的公式 f_out = f_clk * FCW / 2^N,反过来求FCW:
FCW = f_out * 2^N / f_clk以一个具体例子算一下:系统时钟100MHz,累加器32位,想输出10kHz正弦波:
FCW = 10000 * 2^32 / 100000000 = 429496.7296取整为429497,那么实际输出频率为 100000000 * 429497 / 2^32 ≈ 10000.000007Hz,误差可以忽略不计。
这里有个实际工程中的经验:频率控制字建议做成可配置寄存器,而不是固化在代码里。我用过SPI接口和并行总线两种方式做过配置,简单系统直接用拨码开关加按键也行。但凡是涉及在线调频的系统,建议至少留一个串行配置接口,方便上位机实时改频率。我在工程里加了一个简单的UART接收模块,上位机发4字节,低32位作为FCW,实测调频响应在一个时钟周期内生效,非常跟手。
3. 仿真验证与FPGA上板实测
3.1 Testbench搭建:还没上板就先把问题都暴露出来
写FPGA代码不仿真等于裸奔。DDS这种数字信号处理模块,仿真的重要性甚至高于上板测试。仿真能让你直接观察内部相位累加器的行为、ROM地址的变化、输出时序是否对齐,这些在示波器上是看不完整的。
我习惯用Vivado自带的XSim做仿真,不需要单独装ModelSim,流程简单,波形查看也够用。Testbench的核心逻辑是产生时钟、给复位、设置FCW,然后看输出波形。下面是一个最小可用的testbench:
module tb_dds_core; reg clk = 0; reg rst_n = 0; reg [31:0] fcw = 32'd429497; // 对应10kHz@100MHz系统时钟 wire [11:0] wave_out; // 100MHz时钟 always #5 clk = ~clk; dds_core #( .N(32), .M(12), .DATA_W(12) ) u_dut ( .clk(clk), .rst_n(rst_n), .fcw(fcw), .wave_out(wave_out) ); initial begin #20 rst_n = 1; #10000; // 中间可以动态修改频率控制字,观察频率变化 fcw = 32'd4294970; // 对应100kHz #10000; $finish; end endmodule仿真波形出来以后,你会看到wave_out呈现一个连续的正弦包络,而不是离散的阶梯状跳变。这是因为相位累加器的数值在每个时钟周期都在递增,查表地址也一直在走,数据在时间轴上非常平滑。如果看到波形有明显断裂或者突跳,优先查相位累加器有没有被意外复位,或者ROM地址位宽有没有接错。
3.2 上板调试:DAC选型和模拟端的坑
仿真通过之后就是上板验证。DDS数字端的输出是并行的数字码,必须经过DAC才能变成模拟信号。我用过ADI的AD9767(14位、125MSPS)、TI的DAC904,以及最简单的R-2R电阻网络DAC,各有各的坑。
如果是高速DAC,时钟和数据的时序约束一定要做对。FPGA输出数据总线经过引脚延迟后,DAC的建立时间和保持时间必须满足要求。这里的一个实用技巧是:DAC的数据输出建议用ODDR原语来对齐时序,或者在FPGA内部打一拍再输出,确保数据稳定后再被DAC采样。
如果是R-2R电阻网络DAC,那就要注意电阻精度和输出驱动能力。12位的R-2R网络对电阻精度要求很高,普通5%精度的贴片电阻根本不够用,至少要用0.1%精度的精密电阻。否则输出波形的高位会有明显的非线性失真,示波器上看起来像波形上叠加了锯齿状的误差。另外一个坑是,R-2R网络后必须加运放做缓冲,否则DAC的负载效应会直接影响输出电压精度。
我在调试中还遇到过一个问题:正弦波形看起来正常,但频谱上有一个很大的固定频率杂散。排查了半天发现是FPGA板上的开关电源纹波通过DAC的参考电压引脚窜入了模拟链路。解决办法很简单,在DAC参考电压引脚旁边加一个10uF钽电容和0.1uF瓷片电容并联去耦,杂散立刻降了20dB以上。模拟电路的坑就是这样,看起来不起眼的细节,往往决定全局。
3.3 输出波形的后处理:低通滤波和幅度控制
DDS直接输出的阶梯状波形包含丰富的镜像频率分量(DAC的sin(x)/x保持效应带来的高频镜像),这些高频分量如果不过滤掉,直接看频谱会非常难看。实际信号发生器产品里,DAC输出后面都会跟一个椭圆低通滤波器,用来压制镜像频率。
滤波器的截止频率选择,取决于你要求的最高输出频率。比如你只需输出1MHz以内的信号,那滤波器截止频率设在1~2MHz左右就够了。在调试时我经常用示波器的FFT功能直接看输出频谱,这样可以快速判断滤波效果。
幅度控制方面,最简单的做法是调整DDS查找表的满量程值,但这样会影响相位累加器的精度。更好的方案是在DDS输出后面加一级数字乘法器,用另一个寄存器控制增益,实现数字AGC或者手动幅度调节。这样频率和幅度就解耦了,互不干扰。工程里我加了一个16位的乘法器(用于带符号乘法)和一个直流偏置加法器,效果非常好。
4. 常见问题排查与性能优化记录
4.1 问题速查表:这些坑我几乎每次都会遇到
下面这张表整理我在做DDS过程中遇到最多的问题和对应解法,照着排查能省很多时间。
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 输出频率是预期的两倍 | 相位累加器位宽N定义错误,或者FCW算错 | 核对公式f_out = f_clk * FCW / 2^N,重新计算 |
| 波形呈阶梯状,毛刺严重 | DAC建立时间不满足,时钟和数据线等长没做好 | 用ODDR寄存输出,调整IOB约束,检查PCB走线 |
| 输出信号叠加了缓慢漂移 | DAC参考电压不稳,或者电源去耦不足 | 检查参考电压源,加强电源去耦电容 |
| 修改FCW后频率不生效 | 寄存器没有同步到DDS时钟域 | 用两级同步器打两拍,或者把FCW锁存在系统时钟域 |
| 正弦波形顶部被削平 | 查找表数据溢出或DA满量程设置问题 | 检查量化范围和DAC输出摆幅设置 |
| 频率分辨率不够,调一个最小步进频率跳变明显 | 累加器位宽N太小 | 把N扩到32位甚至40位 |
4.2 关于SFDR的几点优化心得
对于信号发生器来说,SFDR(无杂散动态范围)是一个核心指标。DDS输出信号的杂散来源主要有两个:相位截断产生的杂散和幅度量化误差。
相位截断杂散是DDS特有的问题。前面说了,累加器N位全精度累加,查表只用高M位。被截断的低N-M位实际上相当于给相位引入了量化误差,这个误差会周期性地产生杂散。抑制方法有两条路:一是增加M,提高相位分辨率;二是引入抖动技术。给查找表地址加上一点随机扰动(dither),可以打散杂散的周期规律,把集中的杂散能量分散到整个频带,SFDR能得到明显改善。代价是噪声底抬高一点,但在很多应用场景下是值得的。
幅度量化误差,就是查找表数据位宽有限造成的量化噪声。这个主要靠增加ROM数据位宽来降低。从12位提升到14位,量化噪声功率大约降12dB。再往上提升,对FPGA的资源消耗和DAC精度来说性价比就不高了。
如果项目对信号纯净度有很高的要求,我建议DDS的查找表用16位数据宽度,然后配合外部14位或16位DAC,系统性能基本就能满足实验室级信号源的需求。
4.3 这套代码后续能扩展的方向
DDS这个项目做熟之后,可以顺理成章往几个方向扩展:
一是做多通道任意波形发生器。把dds_core例化多份,共享同一个时钟和配置总线,每个通道独立输出,相位差可以通过给相位累加器赋初始值来实现,这样就能做正交信号源,直接用于QAM调制教学或者矢量网络分析仪的激励源。
二是把查找表内容换成任意波形。DDS的ROM里存的不只是正弦波,只要你能把波形离散化并写入ROM,它就能输出任意周期波形。方波、三角波、锯齿波只是一步之遥,更复杂的任意波形需要上位机动态更新ROM内容。
三是把DDS和反馈控制结合,做成一个扫频源。通过状态机控制FCW按对数或者线性规律扫频,配合功率检测模块,就能做成一个简易的频谱分析激励源,这在射频调试中非常有价值。
我在实际调试中最大的体会是:DDS看起来是一个简单的数字模块,但真正把它做出好的指标,牵涉到数字电路设计、FPGA时序约束、模拟电路布局、电源完整性等多个领域的知识。正因为如此,DDS非常适合作为FPGA入门到进阶的过渡项目——它能让你从单纯的“写逻辑”跨到“做系统”的思维层面。工程代码打包在DDS_Verilog.rar里,里面的顶层模块、仿真脚本和ROM生成脚本都是可以直接用的,有需要的朋友拿去参考,遇到问题咱们可以多交流。
本文还有配套的精品资源,点击获取