尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

FPGA实现DDS信号发生器:从原理到Vivado工程实践

FPGA实现DDS信号发生器:从原理到Vivado工程实践 1. 项目缘起从需求到FPGA实现的思考最近在整理一些信号处理相关的项目发现很多场景都离不开一个基础但核心的模块信号发生器。无论是通信系统的基带仿真、音频算法的验证还是传感器激励信号的产生一个稳定、灵活、可编程的信号源都是不可或缺的。过去我们可能习惯用现成的函数发生器或者用单片机配合DAC来生成但这些方案要么灵活性不足要么在频率分辨率、切换速度和相位连续性上存在局限。这时直接数字频率合成技术就进入了视野。它是一种从相位概念出发通过查表法直接合成所需波形的技术。相比于模拟振荡器DDS方案具有频率分辨率极高、频率切换速度快、相位连续以及输出相位噪声低等优点。而FPGA凭借其并行处理和可重构的特性无疑是实现DDS的理想平台。它内部丰富的逻辑资源和Block RAM可以非常高效地实现相位累加器和波形查找表这两个核心部件。所以这个系列的第一篇我就想从一个最经典、也最基础的案例入手在FPGA上实现一个DDS正弦信号发生器。这不仅是理解DDS原理的绝佳实践也是掌握FPGA中ROM IP核使用、数据位宽处理、时序设计等基础技能的敲门砖。无论你是刚接触FPGA的新手还是想巩固数字信号处理基础的工程师跟着这个流程走一遍都会有不少收获。我们将使用Vivado工具链从原理分析、IP核配置、代码编写到仿真验证一步步构建出一个参数可调的正弦波发生器。2. DDS核心原理相位累加与波形查找的舞蹈要理解如何在FPGA中实现DDS我们必须先吃透它的工作原理。DDS的核心思想可以用一个非常直观的模型来理解想象一个指针在单位圆上匀速旋转指针在X轴或Y轴上的投影其变化规律就是一个标准的正弦或余弦波。DDS就是对这个过程的数字化模拟。这个模拟过程主要由三个核心部件完成相位累加器、相位-幅度转换器通常是一个ROM查找表以及数模转换器。在纯数字域的FPGA设计中我们主要关注前两者DAC部分通常由板载的外接芯片完成。2.1 相位累加器决定信号的“节奏”相位累加器是DDS的“心脏”它是一个N位的加法器与一个N位的相位寄存器相连。在每个系统时钟的上升沿相位寄存器会将当前值加上一个固定的值——频率控制字。这里就引出了DDS中第一个关键公式输出频率Fout与系统时钟Fclk、频率控制字FTW以及相位累加器位宽N的关系。Fout (FTW * Fclk) / (2^N)怎么理解这个公式呢相位累加器的最大值是2^N这对应着相位从0到2π的一个完整周期。FTW可以看作是每个时钟周期相位前进的“步长”。步长越大走完一个完整周期2π所需的时钟周期数就越少即输出信号的周期越短频率也就越高。当FTW为1时输出频率最低为Fclk / 2^N这就是DDS的频率分辨率。N通常取值较大如24、32、48位以获得极高的频率分辨率。例如Fclk100MHzN32则频率分辨率高达约0.023Hz这是传统模拟电路难以企及的。2.2 波形查找表将相位“翻译”成幅度相位累加器的输出是一个不断增长的相位值0 到2^N-1。我们需要将这个相位值转换为对应的正弦波幅度值。最直接的方法就是使用查找表。我们预先将一个周期的正弦波幅度数据按照一定的精度幅度位宽M计算好并存入一个ROM中。这里有一个重要的工程折衷我们不可能把2^N个相位点对应的幅度值都存下来因为N往往很大如32位那将需要海量的存储空间。实际上我们只存储一个周期正弦波的2^K个采样点K通常远小于N如10~14位。那么相位累加器输出的高K位就被用作ROM的查找地址。这意味着我们实际上对相位进行了“截断”或“量化”只保留了高K位作为ROM地址。这种做法的本质是用一个2^K点的离散正弦表来近似连续的正弦波。K越大存储的点数越多波形的精度就越高但消耗的Block RAM资源也越多。同时由于丢弃了相位累加器的低(N-K)位会引入相位截断误差这是DDS输出信号中相位噪声的主要来源之一。在实际工程中我们需要根据系统对杂散和噪声的要求来合理选择K的值。2.3 整体数据流与关键参数让我们把整个数据流串起来看。在每一个时钟周期相位寄存器将其当前值加上FTW得到一个新的相位值。取新相位值的高K位作为地址输入到ROM查找表。ROM输出该地址对应的正弦波幅度数据M位宽。幅度数据被送到FPGA的IO引脚驱动外部的DAC芯片如果板载有的话最终转化为模拟电压信号。在这个过程中有几个关键的设计参数需要我们权衡系统时钟频率Fclk决定了DDS能输出的最高理论频率通常遵循奈奎斯特采样定理Fout_max Fclk/2也影响了频率分辨率。相位累加器位宽N决定了频率分辨率。N越大分辨率越高但也会略微增加逻辑资源消耗。ROM地址位宽K决定了波形一个周期的采样点数直接影响输出波形的精度和谐波失真。ROM数据位宽M决定了输出幅度的量化精度影响信号的动态范围和无杂散动态范围。频率控制字FTW用户可通过改变此值实时、无缝地调整输出频率这是DDS的一大优势。理解了这些我们就知道在FPGA中设计DDS其实就是用硬件描述语言去构建相位累加器和控制ROM并处理好这些参数之间的关系。3. Vivado工程实战构建你的第一个DDS模块理论分析完毕我们进入动手环节。我使用的环境是Vivado 2022.1FPGA开发板是带有Basys3或类似配置的板子主要需要有外部DAC或PMOD接口。我们的目标是生成一个1MHz的正弦波系统时钟为100MHz。3.1 创建工程与ROM IP核配置首先在Vivado中创建一个新的RTL工程。工程创建好后我们第一个要配置的就是存储正弦波的ROM IP核。打开IP Catalog在Flow Navigator中找到并点击 “IP Catalog”。搜索并选择Block Memory Generator在IP Catalog的搜索框输入 “block memory”选择 “Block Memory Generator”。配置ROMBasic标签页Component Name命名为sin_rom。Memory Type选择Single Port ROM。我们只需要读。Port A Options标签页Write Width设置为12。这意味着我们输出的幅度数据是12位宽M12。选择12位是为了匹配很多入门级开发板上的DAC芯片如ADI的AD9707或LTC的LTC2624。Write Depth设置为1024。这意味着我们的ROM有1024个存储单元即2^KK10。存储一个正弦波的1024个采样点对于入门学习精度足够了。Operating Mode选择No Change。标准ROM模式。勾选Primitives Output Register。这个选项会在ROM输出端添加一级寄存器有助于改善时序但会引入一个时钟周期的延迟。对于DDS来说这个延迟是固定且可预测的通常可以接受。Other Options标签页这里最关键的是加载初始化文件。勾选Load Init File。点击Edit创建一个新的Coe File。COE文件是Vivado识别的一种初始化数据格式。在打开的对话框中Memory Initialization选择Custom Format。在下方的大文本框中我们需要填入1024个数据。数据范围是0到4095因为12位宽最大值是2^12-14095。正弦波的公式为data round(2047.5 2047.5 * sin(2*pi*i/1024))。这里2047.5是中间值对应模拟0V2047.5是幅度。i从0到1023。你可以用MATLAB、Python甚至Excel生成这个数据列表。格式是每行一个十进制数。例如文件开头是memory_initialization_radix10; memory_initialization_vector 2048, 2072, 2096, ... (中间数据) 2048;生成文件后在Vivado中浏览并选择这个COE文件。生成IP核点击OKVivado会综合并生成这个ROM的封装文件.xci和实例化模板。注意ROM的深度Depth和位宽Width需要仔细权衡。深度102410位地址对于演示和音频范围信号通常足够但如果需要极高纯度的射频信号可能需要更大的深度如12位地址4096点来降低由查表引入的谐波失真。这直接消耗更多的Block RAM资源。3.2 编写DDS核心Verilog代码有了ROM我们就可以编写DDS的核心模块了。新建一个Verilog文件命名为dds_sine_generator.v。module dds_sine_generator #( parameter PHASE_WIDTH 32, // 相位累加器位宽 N parameter ROM_ADDR_WIDTH 10, // ROM地址位宽 K parameter ROM_DATA_WIDTH 12, // ROM数据位宽 M parameter CLK_FREQ 100_000_000 // 系统时钟频率单位Hz )( input wire clk, // 系统时钟 input wire rst_n, // 低电平复位 input wire [PHASE_WIDTH-1:0] freq_word, // 频率控制字 FTW output wire [ROM_DATA_WIDTH-1:0] sine_data // 正弦波数据输出 ); // 相位累加寄存器 reg [PHASE_WIDTH-1:0] phase_acc_reg; // ROM地址线取相位累加器的高ROM_ADDR_WIDTH位 wire [ROM_ADDR_WIDTH-1:0] rom_addr; // ROM数据线 wire [ROM_DATA_WIDTH-1:0] rom_data; // 相位累加过程 always (posedge clk or negedge rst_n) begin if (!rst_n) begin phase_acc_reg 0; end else begin phase_acc_reg phase_acc_reg freq_word; end end // 将相位累加器的高位赋给ROM地址 // 注意这里取的是 phase_acc_reg 的最高位MSB开始向下ROM_ADDR_WIDTH位。 // 一种更清晰的写法是取 phase_acc_reg[PHASE_WIDTH-1 -: ROM_ADDR_WIDTH] assign rom_addr phase_acc_reg[PHASE_WIDTH-1:PHASE_WIDTH-ROM_ADDR_WIDTH]; // 实例化ROM IP核 sin_rom your_rom_instance ( .clka(clk), // 时钟 .addra(rom_addr), // 地址输入 .douta(rom_data) // 数据输出 ); // 将ROM输出直接赋值给模块输出 assign sine_data rom_data; endmodule代码关键点解析参数化设计使用parameter定义了关键位宽和时钟频率。这使得模块非常灵活只需在实例化时修改参数就能适配不同的需求而不需要修改内部代码。这是可复用IP设计的好习惯。相位累加在always块中每个时钟上升沿phase_acc_reg寄存器都会加上输入的freq_word。这是一个典型的累加器模型。复位时清零。地址生成assign rom_addr phase_acc_reg[PHASE_WIDTH-1:PHASE_WIDTH-ROM_ADDR_WIDTH];这行代码是精髓。它截取了相位累加器最高位的ROM_ADDR_WIDTH位作为ROM地址。为什么取高位因为相位累加器的低(PHASE_WIDTH - ROM_ADDR_WIDTH)位代表的是相位的小数部分在查表时被舍去了。取高位保证了地址随着相位增长而单调连续变化从而正确遍历整个正弦表。ROM实例化sin_rom是之前通过IP Catalog生成的模块名。你需要确保这个名称与IP核配置时的Component Name一致。端口连接很简单时钟、地址、数据输出。输出直接将ROM的输出rom_data连接到模块的输出端口sine_data。3.3 计算频率控制字与顶层模块设计我们的目标是产生1MHz的信号系统时钟100MHz相位累加器位宽N32。根据公式FTW Fout * 2^N / FclkFTW 1,000,000 * (2^32) / 100,000,000 1e6 * 4294967296 / 1e8 42949672.96 ≈ 42949673 (四舍五入取整)在Verilog中我们可以用一个常数来表示这个值。通常我们会创建一个顶层模块将DDS核心模块实例化并分配这个常数值给freq_word。同时顶层模块还需要处理时钟、复位信号并将sine_data输出到FPGA的物理引脚。假设我们的开发板通过PMOD接口连接了一个12位DAC那么我们需要在顶层模块中将sine_data分配到正确的FPGA引脚上。这涉及到约束文件XDC的编写。顶层模块示例 (top.v):module top ( input wire sys_clk, // 系统时钟输入假设接在板载100MHz晶振 input wire rst_n_in, // 复位按钮输入低有效 output wire [11:0] dac_data // 连接到外部DAC的12位数据线 ); // 内部复位信号可能需要对按键输入进行去抖和同步处理这里简化 wire rst_n; assign rst_n rst_n_in; // 实际项目建议使用复位同步器 // 频率控制字对应1MHz输出 localparam FREQ_WORD_1MHZ 32d42949673; // 实例化DDS发生器 dds_sine_generator #( .PHASE_WIDTH(32), .ROM_ADDR_WIDTH(10), .ROM_DATA_WIDTH(12), .CLK_FREQ(100_000_000) ) u_dds_sine_gen ( .clk(sys_clk), .rst_n(rst_n), .freq_word(FREQ_WORD_1MHZ), .sine_data(dac_data) ); endmodule约束文件关键内容 (top.xdc):# 时钟约束 create_clock -period 10.000 -name sys_clk [get_ports sys_clk] # 复位引脚约束 set_property PACKAGE_PIN [板载复位按键的引脚号] [get_ports rst_n_in] set_property IOSTANDARD LVCMOS33 [get_ports rst_n_in] # DAC数据引脚约束假设使用PMOD JA口的12个引脚 set_property PACKAGE_PIN [引脚号1] [get_ports {dac_data[0]}] set_property IOSTANDARD LVCMOS33 [get_ports {dac_data[0]}] ... # 为 dac_data[1] 到 dac_data[11] 重复类似约束4. 仿真验证与上板调试让波形“动”起来代码写好了约束也加了但在烧录到板子之前必须进行充分的仿真验证。这是保证设计功能正确的关键一步。4.1 编写Testbench进行功能仿真我们使用Verilog编写一个简单的测试平台来观察DDS输出的数据是否正确。timescale 1ns / 1ps module tb_dds_sine_generator(); reg clk; reg rst_n; reg [31:0] freq_word; wire [11:0] sine_data; // 实例化被测模块 dds_sine_generator #( .PHASE_WIDTH(32), .ROM_ADDR_WIDTH(10), .ROM_DATA_WIDTH(12), .CLK_FREQ(100_000_000) ) u_dut ( .clk(clk), .rst_n(rst_n), .freq_word(freq_word), .sine_data(sine_data) ); // 生成100MHz时钟 initial begin clk 0; forever #5 clk ~clk; // 周期10ns频率100MHz end // 测试过程 initial begin // 初始化 rst_n 0; freq_word 32d42949673; // 1MHz对应的FTW #100; rst_n 1; // 释放复位 #20000; // 运行20us可以观察到多个正弦波周期 $finish; end // 将信号变化记录到VCD文件便于在GTKWave等工具中查看 initial begin $dumpfile(dds_sim.vcd); $dumpvars(0, tb_dds_sine_generator); end endmodule在Vivado中运行行为仿真后我们可以在仿真波形窗口中查看sine_data信号。你应该能看到一个以2048为中心对应模拟0V在0到4095之间变化的数字信号。将其设置为“模拟波形”显示模式就能看到一个清晰的正弦波图形。你可以测量波形的周期理论上应该是1us对应1MHz由于仿真步长和显示精度可能会稍有偏差但应非常接近。仿真分析要点复位后启动观察释放复位后sine_data是否从ROM的第一个值开始输出并且输出是否连续。频率验证计算两个相邻波峰或过零点之间的时间差看是否等于1/Fout。改变FTW你可以在Testbench中动态改变freq_word的值例如减半观察输出频率是否也随之减半。这是验证DDS频率可调性的关键。4.2 上板实测与调试技巧仿真通过后就可以进行综合、实现、生成比特流并下载到FPGA开发板了。上板后你需要一个示波器来观察实际的模拟波形。硬件连接将FPGA板上分配了dac_data信号的引脚通常是PMOD接口通过杜邦线连接到一块DAC模块如基于AD9707的模块的数字输入口。再将DAC模块的模拟输出连接到示波器的探头。上电与下载给FPGA板和DAC模块供电通过Vivado Hardware Manager将比特流文件下载到FPGA中。示波器观测调整示波器时基使屏幕上能显示数个周期的波形。你应该能看到一个频率为1MHz的正弦波。测量其频率与理论值对比。观察波形的幅度和直流偏置。由于我们ROM中的数据是围绕2048对称的经过一个典型的偏移二进制编码DAC后输出应该是围绕一个中间电压如1.65V如果DAC参考电压是3.3V变化的正弦波。常见问题与调试没有波形首先检查电源和接地。用示波器测量DAC的参考电压和输出引脚电压是否有变化。回到Vivado确保引脚约束正确比特流下载成功。可以在代码中增加一个LED闪烁的测试逻辑先确保FPGA基本功能正常。波形频率不对最常见的原因是频率控制字FTW计算错误或者系统时钟Fclk与实际板载晶振频率不符。仔细核对计算过程和约束文件中的时钟周期定义。波形畸变非正弦可能是ROM的COE文件数据错误或者DAC模块的电路如运放滤波电路有问题。可以先用Vivado的ILA集成逻辑分析仪IP核抓取FPGA输出给DAC的数字信号sine_data将其与仿真波形对比如果数字信号正确则问题出在DAC之后的模拟电路部分。波形毛刺多DDS的输出在频率控制字不能被2^N整除时其相位累加过程是周期性的但输出序列可能不是绝对周期的这会在频谱上产生杂散。此外数字信号输出到引脚时如果未加时序约束或存在同步问题也可能产生毛刺。确保对dac_data输出寄存器进行时序约束在XDC中设置set_output_delay并考虑在最终输出前用时钟寄存器打一拍以消除毛刺。实操心得第一次上板很可能看不到完美波形。不要慌采用“分治法”排查。先确保数字部分正确用ILA抓取sine_data和rom_addr看其变化规律是否符合预期。如果数字部分正确问题就缩小到了DAC模块、电路连接或示波器设置上。另外对于高速信号PCB布局布线、电源去耦都非常重要入门级开发板在较高频率如几十MHz下输出波形可能 already 会变差这是正常现象。5. 性能优化与扩展思考一个基础的DDS跑通了但作为一个工程项目我们还需要考虑更多。5.1 优化频谱纯度相位抖动与幅度压缩基础的DDS由于相位截断只用高K位寻址会在输出频谱中引入杂散信号。一种常见的优化技术是相位抖动。思路是在对相位累加器输出进行截断前加入一个较小的随机数噪声这个噪声可以将截断误差的周期性破坏掉将其能量转化为宽带的底噪从而抑制集中的杂散谱线。在FPGA中可以用一个线性反馈移位寄存器来生成伪随机序列作为抖动噪声。另一种技术是幅度压缩主要用于节省ROM资源。对于正弦波我们可以只存储0°到90°第一象限的数据然后通过相位高两位来判断当前相位属于哪个象限并对查表结果进行相应的取反、取补等操作来还原出整个周期的波形。这样可以将ROM大小减少到原来的1/4。5.2 扩展功能调制与多通道DDS的灵活性使得它很容易扩展出各种调制功能。调频直接动态改变输入到模块的freq_word值即可实现FM。调相在将相位累加器输出送给ROM前加上一个相位偏移量phase_offset即可实现PM。调幅将ROM输出的幅度数据sine_data与一个幅度控制字相乘需要用到FPGA内的乘法器IP核或DSP Slice即可实现AM。多通道/正交输出如果需要同时产生正弦和余弦信号即I/Q两路可以配置两个ROM分别存储正弦和余弦表共用同一个相位累加器。这在通信系统中非常常见。5.3 资源评估与选型建议在更大型的设计中需要关注DDS模块消耗的FPGA资源逻辑资源相位累加器是一个加法器消耗LUT和寄存器。位宽越大消耗越多但通常不是主要矛盾。存储资源ROM消耗Block RAM。深度和位宽直接决定了消耗的BRAM大小。例如一个1024x12bit的ROM会消耗1个36Kb的BRAM因为每个BRAM通常可配置为多种宽度和深度组合。DSP资源如果实现了幅度调制则会消耗乘法器或DSP Slice。对于Xilinx的FPGAVivado本身就提供了非常成熟且高度优化的DDS Compiler IP核。这个IP核不仅封装了相位累加和查找表逻辑还直接集成了可选的泰勒级数展开校正、抖动注入、多通道输出等高级功能并且能够自动推断使用DSP48单元来实现高性能的相位累加。在大多数生产项目中除非有极其特殊的定制需求否则直接使用官方的DDS Compiler IP核是更高效、更可靠的选择。我们此处的自主实现根本目的是为了深入理解其原理这是灵活运用和调试高级IP核的基础。通过这个从零开始的DDS正弦信号发生器项目我们不仅完成了一个可用的信号源更重要的是走完了FPGA数字系统设计的完整流程从算法原理分析、参数计算、IP核配置、HDL编码、仿真验证到上板调试。过程中遇到的每一个问题和解决方案都是宝贵的经验积累。当你下次需要使用更复杂的DDS Compiler IP核时你会清楚地知道每一个配置参数背后的含义以及如何根据频谱、资源需求去权衡和优化。
返回列表