ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

用FPGA手写DDS信号发生器:从相位累加器到完整RTL设计

用FPGA手写DDS信号发生器:从相位累加器到完整RTL设计 简介基于FPGA的DDS信号发生器设计资料合集面向FPGA初学者与进阶开发者帮助理解直接数字频率合成DDS原理并完成工程落地。资源以Verilog实现为核心包含完整源码工程、ModelSim仿真脚本与波形记录同时提供原理图与PCB图便于从算法设计到硬件验证全流程对照学习。压缩包共2808个文件主要类型包括Quartus工程与Verilog源码文件、mif/hex初始化数据文件、sdc时序约束文件、rpt/qmsg编译报告、wlf仿真波形记录以及wmv视频教程等。其中初始化数据文件用于构建波形查找表仿真波形记录可对照查看时序结果视频教程则演示各阶段操作整体约700MB目录结构按功能模块划分便于检索定位。目前已有1441人学习下载是FPGA经典案例系列博客的配套资源之一。通过这套资料可掌握DDS波形生成、查找表设计、DAC接口控制等关键技术并借助操作录屏和说明文档快速复现实验、排查常见问题适合作为课程设计、毕业设计或竞赛备战的参考资料。 如果你在大学实验室或者电子设计竞赛的现场待过你一定见过那种“一机难求”的信号源。两三个人围着一台老旧的DDS函数信号发生器排队调参数谁先用还得看脸色。DDS信号发生器不是什么新概念市面上成品也多得是但当你需要把它集成进自己的系统或者竞赛时经费有限又或者你就是想彻底搞明白“相位累加器到底怎么工作”的时候——用FPGA自己写一个DDS就是最靠谱的路径。这也是DDS_Verilog这个工程存在的全部意义不依赖厂商IP核不靠成品模块纯Verilog手写RTL在FPGA里实现一个可控频率、可控相位、可控波形的完整DDS信号发生器。这篇文章就是我基于这个工程的实际设计思路做的一次完整复盘。从原理拆解到代码细节从仿真验收到电路联调再到我踩过的那些坑一次说清楚。适合刚接触FPGA但有一定Verilog基础的学生也适合想赶竞赛进度、直接“抄作业”的工程师。1. 为什么是FPGA做DDS从定时器翻转到相位累加器先说个最常见的误解。很多人刚开始做信号发生器第一反应是用单片机。单片机做方波很简单IO口翻转就是了做任意波形呢查表加DAC也勉强能做。但你一旦把频率分辨率、相位连续性和实时调频这三个要求同时摆上桌面单片机的瓶颈立刻暴露无遗。举一个真实场景。你用STM32的定时器做PWM输出正弦波假设主频168MHz想要一个1kHz的正弦波定时器计数周期需要做到168000个时钟。这没问题。但当你从1kHz切到1.001kHz的时候周期变化是168个时钟的整数倍吗不是。于是你只能取整频率就偏了。偏一点点无所谓如果这个信号是用来做锁相放大器的参考源偏0.1Hz输出的解调结果都是错的。DDS的核心思路完全绕开了这个死结。它不追求“一个周期内尽可能多输出点”而是用相位累加器去描述波形在时间轴上的推进过程。相位做加法的粒度越细频率分辨率就越高而这个过程在FPGA里就是一个无比纯粹的二进制加法器一个时钟周期做一次加法没有中断响应延迟没有指令流水线阻塞完全确定性的时序。再往下说一层。DDS为什么能实现相位连续调制因为频率字的变化只影响累加器的步长它不打断当前正在进行的累加过程。也就是说你在任意一个时钟节拍把频率字从A改成B输出的波形相位不会跳变只会在当前位置以新的步长继续往前走。这在通信系统里叫“相位连续”在测试信号源里意味着你扫频的时候不会出现毛刺或者断裂。这一点用锁相环做不到用单片机定时器也做不到只有DDS从架构上天然支持。所以结论很直接用FPGA做DDS不是炫技是DDS对实时性、精确性和并行性的要求在倒逼计算平台。FPGA提供的恰恰就是这三个东西。这也是为什么几乎所有商用射频信号源内部算法最终都落到一片FPGA或者ASIC的DDS核上。2. DDS核心原理拆解相位累加器、查找表与相位截断DDS的数学基础其实特别简单。一个正弦波数学表达是sin(2πft)。如果我们把相位φ看成时间的线性函数那么输出波形就是在以固定的角度增量一次次查表。DDS要干的事情就是每个时钟节拍把当前相位往前推一个角度步长然后用这个相位值去查一个正弦表查出来的幅度值经过DAC变成模拟电压。但工程实现上有一个关键问题存储资源是有限的。你不能真的把360度每个细小的角度都存一张表。所以就有了相位累加器加查找表这个经典组合。相位累加器本质上是一个N位二进制加法器每个时钟周期加上一个频率控制字FCW。累加器位宽N决定了相位分辨率的精度。查找表的深度M决定了你存了多少个采样点通常远小于2^N。这时候就出现一个工程上的核心概念——相位截断。累加器输出的N位相位我们只取高M位去寻址查找表低N-M位直接丢掉。这个截断操作带来的是相位误差但在合理配置下截断误差折算到幅度上的信噪比是可以接受的。具体推导一下量化关系。设系统时钟频率为Fclk累加器位宽为N频率控制字为FCW那么输出频率Fout的精确表达式为Fout Fclk × FCW / 2^N反过来如果要输出一个指定频率FoutFCW的计算公式就是FCW Fout × 2^N / Fclk注意这里的FCW往往是非整数而寄存器只能存整数所以实际输出频率和理论值之间的偏差就是频率分辨率等于Fclk / 2^N。举个实际例子系统时钟50MHz累加器32位频率分辨率就是50000000 / 4294967296 ≈ 0.0116Hz。也就是说你可以输出任意频率误差不超过0.012Hz这个精度在绝大多数测试场景下都够用了。查找表位宽的问题也很容易理解。你用的DAC如果是10位ROM存储的幅度值就需要10位宽甚至建议略微超过DAC位宽给后级的数字滤波和增益控制留出余量。查找表深度建议至少256点。深度越低波形越粗糙查表输出的是一个阶梯状近似谐波会明显抬升。深度太高多消耗BRAM但频率响应改善有限因为最终的平滑还是要靠后级的模拟低通滤波器来做。有一点必须提醒相位截断产生的杂散信号幅度和截断位数之间有一个经典公式理论上每截断1位最坏情况下的杂散抑制恶化约6dB。所以如果你的应用对频谱纯度要求很高就不要吝啬相位累加器的位宽。这也是为什么我宁可用32位累加器、实际寻址只取高10位也不直接用一个10位累加器的原因。3. Verilog代码实现从RTL到仿真的完整拆解我写的DDS_Verilog工程代码结构不复杂核心模块就是三个相位累加器、波形ROM查找表、顶层接口控制。但每一段代码背后都有取舍逻辑不是随便拼一拼就能出正弦波的摘几个关键片段说说。3.1 相位累加器所有的调频调相都发生在这里相位累加器模块的写法非常固定本质上就是一个带复位、有时钟使能的累加寄存器。但有一点需要特别说明频率控制字的位宽必须和累加器位宽严格一致否则赋值的时候很容易出现隐式截断的问题。我习惯的做法是把频率字和相位字都在顶层定义好作为模块参数传入。代码示例如下module phase_accumulator #( parameter N 32 )( input wire clk, input wire rst_n, input wire [N-1:0] freq_control_word, input wire [N-1:0] phase_offset, output reg [N-1:0] phase_out ); always (posedge clk or negedge rst_n) begin if (!rst_n) phase_out 32d0; else phase_out phase_out freq_control_word; end这里有一个非常容易忽略的细节。实际做相位调制的时候phase_offset不能直接改累加器的状态寄存器因为这样会破坏相位累加的连续性。正确做法是把相位偏移量累加到累加器输出之后用一个单独的加法器完成wire [N-1:0] phase_modulated; assign phase_modulated phase_out phase_offset;这样频率字影响累加的速率偏移量只改变查表的起点两者互不干扰PSK调制的时候就能做到相位跳变而不产生频率毛刺。还有一个重要参数起始相位。如果系统上电后要求输出从指定相位开始除了设置相位偏移量还要注意复位逻辑。我习惯在复位释放后的第一个周期写入初值否则波形起始点不确定在示波器上看起来像是随机相位。3.2 波形ROM查找表ROM和Case两种实现方式的取舍查找表在FPGA里有两种常见实现方式用IP核生成ROM或者直接写一个case语句。两者各有适用场景。用IP核生成ROM的优点是底层会映射到BRAM资源上读取延迟固定存储深度可以做得很大适合波形数据比较长超过1024点的场景。缺点是生成IP核需要配置流程而且如果要在工程里移植到不同的FPGA型号或厂商平台IP核就得重新生成一遍。用case语句实现的查找表更“轻量”直接作为组合逻辑嵌入不占用专用的存储块适合波形深度不大的情况256点或512点而且代码可移植性极好。缺点是当查找表深度变大case语句会消耗大量LUT资源时序也可能恶化。DDS_Verilog工程里我用的方案是256点正弦查找表用case实现。原因有三第一256点深度足够普通波形输出第二不依赖IP核代码拿到任意FPGA平台都能直接综合第三后续想扩展成方波、三角波、任意波形表时改case分支就行不用重新生成IP。但如果你要输出复杂的调制波形我建议用IP核方式直接把波形数据存进.coe或者.mif文件工程化程度更高。典型的case查找表片段长这样reg [9:0] wave_data; always (*) begin case (phase_addr) 8d0: wave_data 10sd511; 8d1: wave_data 10sd524; // 查表数据... default: wave_data 10sd511; endcase end这里有一个技术细节输出的幅度数据我特意设置了符号位。很多初学者直接用无符号数存正弦表结果波形输出从0到满幅叠加了一个直流偏置后级滤波出来信号带直流分量接喇叭就是“砰砰”的冲击声。正确的做法是使用二进制补码表示让正弦波以0为中点正负对称。3.3 顶层控制频率字计算与按键扫描顶层模块把累加器和查找表串起来同时要处理频率字的动态更新。频率字从哪里来如果是板载按键控制我建议先把按键消抖和脉冲化模块写好如果是串口或SPI下发频率字那就直接接通信模块的解析输出。DDS_Verilog工程默认采用按键方式每次按键改变步进值步进值步进大小和累加器位宽的关系需要先算好。假设你要求按键每按一次输出频率改变100Hz系统时钟50MHz累加器32位那么按键对应的频率字增量就是delta_fcw 100 × 2^32 / 50000000 ≈ 8589.9 ≈ 8590这个数值看起来不起眼但如果不提前算清楚直接在代码里写freq_control_word freq_control_word 32d100;那实际频率变化就变成了100×50000000/2^32 ≈ 1.16Hz按键按到你手酸频率也拧不上去完全是两码事。顶层代码还需要把相位累加器输出的高位和ROM地址位宽对齐。我习惯在顶层模块里单独定义一个相位截断的连线信号避免在子模块里混用位宽引起告警。wire [9:0] rom_addr; assign rom_addr phase_modulated[31:22]; // 取高10位3.4 仿真验证Modelsim下验证频率和相位写代码不仿真等于白写。DDS这种模块仿真起来特别直观你只要看波形输出的周期对不对就知道累加器有没有算错。我的仿真流程是这样的写一个简单的testbench给一个固定的频率字比如让DDS输出10kHz跑一段足够长的时间然后在Modelsim里用游标量出两个波峰之间的时间差换算成频率。如果你看到的周期不是100微秒那就要仔细检查频率字的计算或者累加器位宽是不是被综合工具优化掉了。仿真时还有一个常见的坑Modelsim默认会显示模拟量的信号但你把ROM输出的10位数字信号拉成模拟波形看时别忘了它是补码格式。选“Format - Analog - Signed”才能看到正确的正弦形状否则波形会看起来像被削了半边可能是细节问题但很容易吓到第一次做DDS的新手。4. 硬件联调中的实际问题DAC选型、滤波器设计与噪声信号排查软件仿真通过只是第一步信号发生器最终要输出真实的模拟信号这一段的坑比RTL设计要多得多。4.1 DAC选型与接口设计并行接口永远是第一优先级DDS系统后端必须接DAC选DAC时最核心的参数有三个分辨率、转换速率、接口类型。分辨率方面。10位DAC是入门级适合常规教学实验如果对波形质量有要求建议选12位以上的。转换速率必须大于DDS系统时钟的两倍以上留出足够裕量实际上建议五倍以上因为DAC的建立时间和数字接口的时序裕量都要算进去。接口类型方面。这个坑我踩得最重。很多开发板上用SPI接口的DAC芯片SPI接口节省引脚没错但SPI是串行协议低速还好一旦碰到50MHz以上的数据更新率串行接口的时序根本来不及。DDS的输出频率再高如果DAC的串行传输速率跟不上数据就会丢或者出错波形直接变成噪声。所以做DDS联调时接口类型直接选并行接口的DAC芯片一个时钟周期更新一次数据没有瓶颈。常用的经济型选择是AD97088位或者TLV561812位之类的芯片。我手头这块板子用的是10位并行DAC刚好和查找表数据位宽对齐一个时钟一拍数据干净利落。4.2 低通滤波器不要相信DAC直接出来的波形DAC直接输出的信号是阶梯波里面有大量的高频镜像分量。频谱上以采样时钟为周期做镜像基频上叠加的谐波杂散可以轻松达到-30dBc以上。要得到干净的模拟波形一级低通滤波器必不可少。滤波器这里我建议直接采用一个简单的一阶或二阶RC低通截止频率设置在输出最高频率的三到五倍。有人会问为什么不用高阶滤波器因为作为通用信号源通常希望带宽是可调节的固定截止频率的高阶滤波器反而限制了灵活性。你可以把LPF设计成可切换的几组不同截止频率通过跳线或者模拟开关来切换。最基本的RC滤波器参数计算假设输出最高频率5MHzRC截止频率设定15MHz可取R330ΩC33pF。Fc 1 / (2πRC) 1 / (2 × 3.14 × 330 × 33×10^-12) ≈ 14.6MHz这个滤波器的效果是能让5MHz以内信号比较平滑但无法完全压制镜像频率。讲究一点的话可以在DAC输出端再加一个运放做二阶有源滤波顺便把幅度调整到合适范围。这个工程能做到的就是给你预留出滤波器的接口位置具体参数按你的输出频率往上调。4.3 信号噪声排查地弹、电源去耦和时钟质量联调过程中最容易出现的问题是波形毛刺。很多人上来就怀疑FPGA逻辑写错了但实际上大部分毛刺问题来源于硬件设计而不是RTL逻辑。地弹问题。DAC的数字输入和模拟输出共地时数字管脚在时钟边沿瞬间切换会产生很大的电流尖峰这个尖峰通过地平面耦合到模拟输出端表现出来就是正弦波上的一串细密毛刺。解决办法是模拟地和数字地单点连接DAC芯片下方保证完整的地平面不给高频返回电流增加阻抗。电源去耦。FPGA和DAC的电源都需要在电源引脚旁边就近放置0.1uF陶瓷电容如果没有这个电容电源电压在高速翻转时波动大DAC的内置基准电压同步抖动输出波形就有周期性的幅度噪声。这个噪声很难从逻辑层面消除只能靠硬件解决。时钟质量。DDS对系统时钟的抖动极其敏感。用FPGA内部PLL生成时钟时如果PLL的带宽配置不当输出时钟的眼图很差累加器的采样时刻不确定最终表现在输出上就是相位噪声增加。我个人习惯是PLL输出时钟之后再经过一个BUFG全局时钟网络尽量保证所有时序逻辑使用同一个时钟域。5. 波形质量问题与频谱纯度优化杂散抑制与相位噪声改善很多初学者做到DDS能出正弦波就收工了但一个真正可用的信号发生器需要面对的是水平垂直的频谱质量问题。以下是几个在实践中提升频谱纯度的可行方向。5.1 相位截断杂散的机理与对策相位累加器截断的过程相当于给理论相位加了一个周期性的锯齿状误差信号这个误差信号会在频谱上产生额外的杂散分量。要减小截断杂散最直接的办法是增加查找表的寻址位宽——把输出到ROM的地址从高10位扩到高12位波形存储器深度从1024扩到4096最坏情况下的杂散抑制可以提升约18dB。但存储深度增加会占用BRAM资源。折中方案是在累加器与查找表之间插入一个相位抖动模块给截断掉的低比特位上加一个小幅度随机数打散相位误差的周期性把离散的杂散变成平摊的宽带噪声底。这等于牺牲一部分信噪比换取更高的无杂散动态范围在通信系统里这个技巧非常常用。DDS_Verilog工程里我没有默认开启这个功能因为抖动引入的宽带底噪在某些低噪声测试里反而更讨厌但如果你的竞争对手的频谱比你的干净试着打开它就不会错。5.2 幅度非平坦度与校准DAC输出幅度随频率上升会自然滚降。这个滚降曲线理论上是sinc函数形状频率越高衰减越大在接近奈奎斯特频率时衰减达到最大。如果你需要信号源在频带内保持幅度一致就需要一个EMAC幅度均衡电路。在FPGA内部实现就是在ROM输出之后接一个ROM增益补偿表根据频率控制字查表自动调整不同频率点的输出幅度增益。这个方法适合频率跨度大的信号源对单一频段输出意义不大。一般的做法是在输出端用可变增益放大器做模拟AGC补偿但精度和代价都比较高。如果你只是入门级别的应用把这个放大增益做成手动档位就够用了。5.3 相位噪声与时钟源的关系DDS的相位噪声理论上主要取决于系统时钟源的相位噪声而与DDS本身的逻辑无关。也就是说你给DDS配一个高质量的晶体振荡器输出的信号相位噪声就好你给它一个满是抖动和毛刺的时钟输出的信号就没办法干净。实操建议做DDS板卡时系统时钟不要从FPGA开发板上那种普通贴片晶振直接取最好用低相噪的温补晶振或者恒温晶振作为PLL参考源。对于1GHz以下的输出频段普通温补晶振的相噪水平就足够应付绝大多数测试场景了不用上恒温晶振那种发烧级的配置。6. 工程文件结构与其他波形扩展思路DDS_Verilog.rar里的文件组织我按功能分得很清晰方便你们拿到工程后直接对照。DDS_Verilog/ ├── rtl/ # RTL源码 │ ├── top_dds.v # 顶层模块频率字、累加器、查找表例化 │ ├── phase_accumulator.v # 相位累加器 │ ├── wave_rom_lut.v # 正弦波查找表case实现 │ └── debounce.v # 按键消抖模块可选项 ├── sim/ # 仿真目录 │ ├── tb_dds_top.v # testbench │ └── run_modelsim.tcl # Modelsim脚本 ├── doc/ # 设计说明文档 └── README.md拿到工程之后建议你们先跑仿真确认输出频率符合预期后再上板。不要一开始就想着扩展功能先把基础正弦波跑通再考虑往里面加东西。扩展思路其实很多我列几个比较实用的方向。方波输出不需要改DDS核心只要在查找表里把正弦数据替换成方波数据即可。需要注意的是方波高频分量极多对后端低通滤波器要求高否则波形边缘会有严重的振铃。三角波输出把查找表替换成对称的线性上升下降数据同样原理。扫频信号在顶层加一个频率控制字的自动调制逻辑让FCW随时间线性变化。注意扫频速率要合适太快了相位累加器还没稳定波形就变了太慢了没有扫频效果通常每毫秒变化一个步进比较合适。FSK/PSK调制这就是前面讲phase_offset的用武之地。FSK就是切换不同的频率控制字PSK就是在累加器输出端叠加不同的相位偏移量。这两个功能在通信原理实验里非常常用也完全可以在现有DDS框架内直接扩展。任意波形输出把波形ROM从case语句换成真正的ROM IP核外部把波形数据通过串口或者SD卡加载进FPGA的存储空间再配合DDS寻址逻辑就变成一个简易的任意波形发生器了。7. 关于Phase这一块再啰嗦几句最后想说一个很多人没注意的点DDS和“让FPGA输出若干路PWM”看起来很像但本质完全不同。PWM的占空比改变的是脉冲宽度DDS改变的是相位推进速度这个区别决定了DDS天然适合做精密频率合成而PWM更适合做功率控制。如果你的应用只是简单的呼吸灯或者电机调速不要绕这么大一圈用DDS但如果你要的是精确频率输出和相位连续调谐那DDS这条路才是正解。我在最初做这个工程的时候也走过一段弯路——上来就想着加各种调制功能结果基础波形都调不通。后来把心静下来从相位累加器一遍一遍查datasheet用示波器一针一针看波形发现大部分问题根本不是代码逻辑问题而是时序约束没写、时钟域没理顺、DAC的时序没满足。你把这三样弄扎实了DDS其实是个很老实的电路它不会刻意刁难你你给它什么时钟和频率字它就还你什么波形。如果看完这篇你自己动手写了一遍对照这个工程跑通了仿真和上板那DDS对你来说就不再是教科书上的一个名词而是你手里一个随时能调出任意频率正弦波的工具。这个东西后续能扩展出来的实验项目和竞赛方案真的会超出你的预期。本文还有配套的精品资源点击获取
返回列表