尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

FPGA实现DDS信号发生器:Verilog代码与调试要点

FPGA实现DDS信号发生器:Verilog代码与调试要点 简介以FPGA为平台实现直接数字频率合成DDS的完整工程资料包适合数字电路、通信电子线路课程设计以及备战电子设计竞赛的本科生和研究生。内容不仅包含Verilog源码与仿真工程还配有视频教学和原理图、PCB图帮助从算法原理到硬件落地全面理解。压缩包共收录2808个文件涵盖.v/.vhd源码、Quartus工程配置、.do仿真脚本、.rpt报告、.hex/.mif波形初始化数据、.wmv视频及PDF/XLSX说明文档等整体大小约700MB目录结构清晰按模块区分便于检索。目前已有1441人学习下载。通过该资料可快速复现DDS波形发生器学习相位累加器、查找表、波形存储与D/A转换等关键环节视频与文档能辅助排错适合FPGA入门到进阶的实践训练也可移植到信号源、调制解调等项目中复用。 做FPGA开发的人几乎绕不开DDSDirect 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 WordFCW累加器的高位作为地址去查一张预先存好波形数据的查找表Look-Up TableLUT输出的数据经过DAC或者PWM就得到模拟波形。这里最关键的一个公式是输出频率与系统时钟、累加器位宽、频率控制字的关系f_out f_clk * FCW / 2^N其中N是相位累加器位宽f_clk是系统工作时钟。从这个公式可以看出输出频率的分辨率是由累加器位宽N决定的N越大频率分辨率越高。比如系统时钟100MHzN32位那么频率分辨率就是100MHz / 2^32 ≈ 0.023Hz能做到非常精细的频率步进。为什么用FPGA做DDS而不是用单片机原因很简单DDS的相位累加和查表是纯粹的并行流水线操作FPGA天然适合这种结构。用STM32等MCU虽然也能做但受限于主频和指令周期输出频率上不去而且占用的CPU资源太高。FPGA可以把累加器跑到几百兆同时信号更新延迟是确定的时钟周期数这个确定性在闭环控制系统里至关重要。1.2 为什么把累加器位宽做这么大很多初学者会问我只要产生一个1kHz的正弦波需要32位的累加器吗用16位甚至8位不行吗这里要区分两个概念相位分辨率和幅度分辨率。累加器位宽N决定了相位分辨率也就是频率步进精度而查找表的地址位宽也就是截断后的位数决定了一次查表能找到多少个相位点。这两个数在实际工程里往往是分开的——累加器全精度累加查表时只取高M位作为地址。举个例子如果N32、M12意味着相位累加点数是2^32个但ROM里只存了2^124096个点。那么每次查表相当于用高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 32d0; 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{1b0}}; else wave_out rom_data; end endmodule2.2 波形查找表正弦波数据生成的两种方式ROM查找表是DDS的心脏里面的波形数据怎么来直接影响最终信号质量。常见做法有两种第一种用Matlab或Python生成.coe文件或.mif文件然后在Vivado/Quartus里用IP核例化ROM把波形数据初始化进去。这种方式流程成熟、可控性强正弦数据可以自己量化适合对信号质量有要求的场景。第二种用FPGA片内的分布式RAMDistributed 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] 8d128; rom[1] 8d153; rom[2] 8d177; rom[3] 8d198; // 这里省略中间数据实际工程可用脚本自动生成 rom[31] 8d103; 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反过来求FCWFCW 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然后看输出波形。下面是一个最小可用的testbenchmodule tb_dds_core; reg clk 0; reg rst_n 0; reg [31:0] fcw 32d429497; // 对应10kHz100MHz系统时钟 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 32d4294970; // 对应100kHz #10000; $finish; end endmodule仿真波形出来以后你会看到wave_out呈现一个连续的正弦包络而不是离散的阶梯状跳变。这是因为相位累加器的数值在每个时钟周期都在递增查表地址也一直在走数据在时间轴上非常平滑。如果看到波形有明显断裂或者突跳优先查相位累加器有没有被意外复位或者ROM地址位宽有没有接错。3.2 上板调试DAC选型和模拟端的坑仿真通过之后就是上板验证。DDS数字端的输出是并行的数字码必须经过DAC才能变成模拟信号。我用过ADI的AD976714位、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生成脚本都是可以直接用的有需要的朋友拿去参考遇到问题咱们可以多交流。本文还有配套的精品资源点击获取
返回列表