ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

FPGA实现DDS信号发生器:从相位累加器到Verilog实战

FPGA实现DDS信号发生器:从相位累加器到Verilog实战 简介FPGA DDS信号发生器工程基于Verilog实现直接数字频率合成面向数字逻辑学习者、FPGA工程师以及电子类课程设计人群是理解DDS原理与硬件实现的完整工程样例。设计包含相位累加器、分频器、波形查表及DAC接口四大核心模块利用MIF文件存储正弦波数据通过频率控制字累加和分频系数调整实现输出频率、相位可调可广泛应用于通信系统、测试测量设备、雷达与信号源等领域。压缩包内共315个文件既有核心v/tdf设计源码、mif波形数据、qsf/qpf工程配置、sof比特流也包含大量Quartus工程数据库cdb、hdb、ddb等并附带readme说明整体大小仅5.7MB。已有895人学习下载工程目录结构清晰既可直接打开工程进行仿真和上板验证也可参照源码梳理DDS参数计算、分频器设计、MIF生成以及DAC输出等完整流程对二次开发和教学演示都非常友好。 FPGA开发板上一个普通晶振加上几百行Verilog就能输出一个频率精确到小数点后好几位、稳定性让很多模拟电路工程师都意外的正弦波。这个看起来很反直觉的事情靠的是一个叫DDSDirect Digital Synthesis直接数字合成的技术。DDS说白了就是“用纯数字方式合成模拟波形”通信、测量、雷达、音频这些领域里都是基础中的基础。很多朋友学FPGA学到一定阶段都会拿DDS信号发生器练手因为它麻雀虽小五脏俱全——里面既有纯数字的时序逻辑又有数字到模拟的边界还牵扯频率计算、相位截断、DAC时序这些硬核问题。如果你正在做FPGA入门后的第一个“像样项目”或者想给后续的信号处理、通信系统打基础这个项目都很难绕开。这篇文章我会从DDS的底层原理讲起把相位累加器、查找表、DAC这条链路彻底拆清楚给出可以直接参考的Verilog代码和查找表生成脚本最后聊聊我在调试过程中踩过的坑。不用怕看不懂下面每个概念我都会用最直白的大白话解释。1. 为什么DDS是FPGA入门到进阶的“黄金项目”1.1 先看一个反直觉的事实FPGA内部没有正弦波全是一堆寄存器和查找表里的数字。但通过DDS这些数字能被精确地“翻译”成一个频率可控的正弦信号稳定度远超很多模拟振荡器。原因在于数字系统的时钟来自石英晶振频率极度稳定相位累加器是纯数字运算不随温度、电压漂移。这是DDS和传统模拟振荡器最本质的区别模拟电路里的RC、LC元件会因温度、老化产生漂移数字系统里的加法器不会。只要晶振够准DDS输出频率就够准。拿环境温度变化这个场景来说模拟振荡器可能漂移几百赫兹DDS几乎纹丝不动。1.2 为什么不直接买DDS芯片市面上有很多专用DDS芯片比如AD9833、AD9910用起来确实方便。但我的观点很明确学习阶段用FPGA自己实现一遍DDS收获远大于直接用芯片。理由有三个第一专用芯片是黑盒你只知道配置寄存器内部相位累加器到底怎么工作、杂散怎么来的完全没概念第二芯片的输出波形被固定想输出任意波形通常做不到第三FPGA实现DDS后可以把信号源和后续的数字信号处理逻辑DDC、滤波、调制放在同一颗芯片里端到端完全可控。这也解释了为什么热词列表里“FPGA”“DDS”“DDC”总是成群出现——它们处在同一条数字信号处理链路里DDS就是那个起点。1.3 这个项目能让你学到什么做完DDS信号发生器下面几个能力是可以直接迁移到后续项目里的频率控制字与位宽计算能力所有数字信号处理系统都涉及“参数到硬件精度”的换算逻辑。查找表LUT技术查表法不只是DDS用图像处理里的LUT调色、通信里的星座映射都用到了。DAC接口时序设计让你第一次认真思考“数字逻辑输出到模拟芯片”之间的时序边界。后面无论你做DDC、做卡尔曼滤波硬件化、做图像处理这套底层能力都能用上。2. DDS工作链路相位累加器、查找表与DAC如何协同我学习和记忆DDS靠的是把它拆成三个环节相位累加器负责“算相位”查找表负责“查幅度”DAC负责“转模拟”。三个环节各司其职缺一个都不完整。2.1 相位累加器一个永远在“转圈”的计数器相位累加器就是一个N位宽的寄存器每个时钟周期累加一个频率控制字Frequency Control Word简称FCW。核心理解在于“溢出即周期”寄存器累加到满量程后自然溢出溢出的那一刻就代表正弦波走过了一个完整的360度周期。可以把它想成一个车轮上的刻度盘频率控制字大车轮每步转过的角度就大转一圈需要的时间短输出频率就高频率控制字小每步转得慢输出频率就低。DDS能精确控制频率靠的就是控制“每步转多少角度”。这个类比我每次讲给新同事听对方基本几句话就懂。2.2 查找表把相位翻译成幅度相位累加器的高M位对应一个周期内的相位点。查找表里存着一个周期的正弦波采样值。以1024点为例相位累加器高10位每变化一次就从ROM里读出一个对应的幅度值。这里有个关键细节为什么不把32位相位全部用来查表因为ROM的容量不可能做到2的32次方那么大。实际做法是只取高10位或12位舍弃低位。被舍弃的低位叫“相位截断”它不影响频率精度但会在频谱上引入少量杂散这一点我放到第3章详细展开。2.3 DAC与低通滤波数字世界的最后一道转换ROM输出的数字幅度经过DAC变成模拟电压。因为每个时钟周期电压会跳一个台阶理想情况下波形是阶梯状的。阶梯里的高频分量靠一级低通滤波器LPF滤掉就能得到平滑的正弦波。这里要特别提醒很多新手只关注FPGA和DAC忽略低通滤波结果示波器上看到一台阶波还以为是代码写错了。其实FPGA侧完全正确是模拟端少了一级滤波而已。我最早做DDS时就犯过这个错白白折腾了两天。2.4 频率控制字的计算拿具体数字算一遍假设时钟50MHz相位累加器32位想要输出1kHz正弦波FCW 1kHz × 2^32 / 50MHz 1000 × 4294967296 / 50000000 85899.34592取整85900实际输出频率f_out 85900 × 50MHz / 2^32 ≈ 999.99Hz误差微乎其微。而频率分辨率Δf 50MHz / 2^32 ≈ 0.0116Hz这个精度是传统模拟振荡器几乎不可能做到的。如果嫌分辨率不够把N改成48位就行代价只是加法器多占一点逻辑资源在现代FPGA里根本不叫事。3. 参数设计位宽、深度和精度的取舍DDS的参数设计是最容易把人绕晕的地方我把它们拆开说清楚。3.1 相位累加器位宽决定频率分辨率频率分辨率公式Δf f_clk / 2^N。N越大分辨率越高。100MHz时钟加32位累加器分辨率约0.023Hz如果做到48位分辨率能到10的负7次方赫兹级别适合做高精度频率源。所以常规设计直接上32位不用犹豫。有些人会纠结“位数少一点是不是省资源”但在FPGA里32位加法器占的资源几乎可以忽略。省这一个加法器的面积远不如省一块DAC调理电路的面积来得实在。3.2 查找表深度相位截断带来的杂散查找表地址位宽直接决定ROM深度10位就是1024点12位是4096点。点数越多相位量化噪声越低。需要说明的是相位截断会在频谱上产生杂散杂散电平大约按 6.02×(地址位宽) dBc 的规律变化10位地址约-60dBc12位约-72dBc。如果你的信号要用于要求比较高的测量仪器直接4096点起步如果只是学习演示1024点完全够用。我的判断标准很简单看最终DAC的位数。12位DAC配1024点14位以上配4096点匹配起来比较合理。3.3 幅度位宽与DAC分辨率必须匹配查找表输出的数据位宽要跟DAC的输入位宽一致。常见DAC有8位、12位、14位、16位。位数越高量化噪声越低但DAC芯片成本和PCB布局复杂度也随之上升。大多数FPGA开发板上的并行DAC是8位或12位。我建议学习阶段选12位既能看清量化噪声的影响又不至于被16位复杂设计拖累。如果你是买现成开发板优先选那种板载并行DAC的型号因为能直接看波形验证学习效率高得多。3.4 一份可以直接抄的参数表下面这张表我整理了很久做方案选型时可以直接对照参数入门方案标准方案高配方案时钟频率50MHz100MHz200MHz以上相位累加器24位32位48位查找表地址8位256点10位1024点12位4096点数据位宽8位12位14/16位频率分辨率约3Hz约0.023Hz极高精度适合场景入门学习通用信号源高精度测量仪器初学者直接抄中间那列也就是“标准方案”跑通后再根据需求调整。4. Verilog实现从顶层到子模块的完整落地理论说完直接上代码。下面是Vivado环境下的实现DAC部分按常见的12位并行输入处理。4.1 顶层模块接口module dds_top ( input wire clk, // 系统时钟100MHz input wire rst_n, // 低电平复位 input wire [31:0] fcw, // 频率控制字 output reg [11:0] dac_data // 12位DAC数据 );接口设计的关键是FCW从外部输入。这样后续无论是拨码开关还是STM32的FMC总线都能灵活地改频率不需要每次改代码重新综合。4.2 相位累加器代码累加器本身非常简单几行代码搞定reg [31:0] phase_acc; always (posedge clk or negedge rst_n) begin if (!rst_n) phase_acc 32d0; else phase_acc phase_acc fcw; end如果你需要输出相位信息比如做PSK调制时改初相位可以把累加器再加上一个相位偏移输入wire [31:0] phase_shifted phase_acc phase_offset;相位偏移可以在上位机或拨码开关里配置这样信号发生器的相位调节功能就有了。这个只是加法操作不会破坏DDS整体架构。4.3 查找表的三种实现方式我见过新手手写几百行case语句来存正弦数据维护起来非常痛苦。三种主流方案对比一下case语句直接枚举适合点数很少的场景比如64点以内逻辑简单但代码冗长改波形要改一大段。ROM IP核加载COE文件最推荐Vivado的Block Memory Generator直接支持适合1024点以上而且综合后占用的是块内存而不是查找表逻辑。Verilog二维数组加initial块初始化适合中等点数不用建IP但要写初始化代码综合工具支持度不如ROM IP稳定。实际项目里我几乎都用ROM IP配合Python脚本生成COE文件改波形只改脚本参数重新编译一遍就行。4.4 DAC输出寄存消除毛刺的关键DAC数据总线上的毛刺多半来自组合逻辑直通。我的经验是所有送DAC的数据都必须打一拍寄存器让数据在时钟边沿后稳定更新不要在数据总线上做组合逻辑。always (posedge clk) begin dac_data rom_data; // 寄存一拍再送DAC end有些DAC芯片还需要一个DAC时钟信号来锁存数据这个时钟通常可以取系统时钟具体参考芯片手册。我最初做的时候漏接了DAC_CLK波形半天出不来后来翻了原理图才发现是电源和时钟引脚没处理好这类低级错误在新手阶段非常常见。5. 查找表生成与板级实测从仿真到示波器的完整验证5.1 用Python一键产出COE文件手写正弦数据不现实连复制粘贴都容易出错。最省事的方案是用Python脚本生成COE文件直接给Vivado的ROM IP加载。import math depth 1024 # 查找表深度 width 12 # 数据位宽 with open(sine_lut.coe, w) as f: f.write(memory_initialization_radix10;\n) f.write(memory_initialization_vector\n) for i in range(depth): # 正弦波转成12位无符号表示带直流偏移 val int((2**(width-1) - 1) * (1 math.sin(2 * math.pi * i / depth)) / 2) f.write(str(val)) if i depth - 1: f.write(;) else: f.write(,\n)这里有个非常容易踩的坑DAC输入如果是单极性的比如0到3.3V范围那你的正弦波必须整体抬升到中间电平附近否则负半周直接被削掉。上面的脚本已经做了偏移处理复制即用。5.2 Vivado中ROM IP的配置细节在Vivado里建ROM IP的路径是IP Catalog → Block Memory Generator。需要注意几个点Memory Type选Simple Dual Port RAM或Single Port ROM都可以学习阶段Single Port够用。Read Width填12Read Depth填1024和COE文件保持一致。在Other Options里勾选Load Init File选择生成的sine_lut.coe。输出寄存器Output Register建议勾上时序会更好。如果综合后发现ROM占用资源远超预期大概率是IP配置成了RAM而不是ROM或者把复位和输出寄存器同时打开了。5.3 仿真验证先确认数字部分没问题上板之前先写一个简单的testbench给一个固定FCW跑仿真。注意观察输出是否为连续的正弦阶梯波相位累加器是否在正常累加。仿真时建议把FCW设成一个容易观察的值比如2^24这样在1024点的LUT下每隔16个时钟周期读一个点波形周期很好数。如果仿真波形不对先查是不是ROM没有正常初始化在Vivado的仿真里直接查看ROM内部变量的初始值即可。这一步能帮你把数字部分的逻辑问题拦在板级调试之前。5.4 上板实测波形出来之前和之后的事仿真过了不等于上板没问题。我把自己调试时踩过的坑整理一下完全没有输出先查DAC芯片的电源、使能引脚再查FPGA到DAC的数据位有没有接反。开发板原理图一定要先看我见过有人把数据总线接反了还查了一下午代码。输出频率和计算值有偏差先确认系统时钟频率对不对再确认FCW有没有被截断。比如寄存器是32位但信号声明成了16位那高16位就丢了。波形台阶明显多半是低通滤波没加或者截止频率太高。我用一阶RC滤波R取100Ω、C取100nF截止频率约16kHz对几kHz的正弦波够用。波形上有毛刺DAC数据总线上加输出寄存器重新布局布线基本能消除。注意用示波器测的时候探头的接地线不要太长地线夹子悬空会引入很大的噪声波形看起来会毛刺很多我一开始就误会成代码问题了。6. 进阶玩法从信号发生器到通信系统的基石6.1 任意波形发生器把查找表里的正弦数据换成三角波、方波、自定义噪声序列DDS框架完全兼容只需要改COE文件硬件不用动。用Python生成任意波形采样的COE文件时注意采样点数要足够多否则波形会失真。这个扩展门类让你瞬间从“只能发正弦波”变成“能发任意波”。6.2 FSK/PSK调制FSK调制就是切换两个不同的频率控制字发“1”用f1发“0”用f0。在代码里用MUX选一下FCW输入就行几乎不加额外逻辑。PSK调制则是切换查找表的相位起点本质上就是给相位累加器加一个相位偏移。这两类调制在通信系统仿真里非常常见做了之后你对“信号源是调制的基础”这句话会有非常直观的感受。6.3 与STM32的FMC联动想要频率随时可调常见的方案就是STM32通过FMC接口写FPGA内部的FCW寄存器。热词里的“stm32h743和fpga实现fmc通信”就是这个方向。FMC写FPGA时FPGA侧只需要做一个简单的寄存器桥reg [31:0] fcw_reg; always (posedge clk) begin if (fmc_cs_n 1b0 fmc_we_n 1b0) fcw_reg fmc_data; endFMC的接口速率远高于SPI做动态扫频、跳频都很轻松。这个桥接逻辑本身不难难点在于FPGA侧要处理FMC的读写时序建议先跑通一个单寄存器读写再扩展到多个配置寄存器。这样你日后做任意波形发生器的上位机控制面板也只是多写几个寄存器的事。6.4 继续向DDC、滤波等方向延伸做完DDS之后信号源已经就位下一步就是数字信号处理。做DDC数字下变频时可以用DDS做NCO数控振荡器作为本振把中频信号混频到基带。这是DDS非常高频的应用场景也是热词里“dds和ddc”经常成对出现的原因。再往后可以做数字滤波器、卡尔曼滤波算法硬件化甚至把DDS的NCO直接嵌入到图像处理流水线里产生扫描时钟。整个FPGA开发的知识版图很多分支都能从DDS这个项目上生长出去。我个人做FPGA项目的经验是不要急着同时上多个复杂模块。DDS信号发生器这个项目先把固定频率的正弦波跑通再用按键或拨码切换频率最后才联上位机。每步都稳定之后再往前走一步很多奇怪的坑会在过程中自然消失。本文还有配套的精品资源点击获取
返回列表