ARTICLE DETAIL

资讯详情

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

FPGA实现DDS方波信号发生器:从相位累加器到时序优化

FPGA实现DDS方波信号发生器:从相位累加器到时序优化 简介基于 FPGA 的 DDS 信号发生器设计资源面向学习 FPGA 与数字信号处理的工程师、高校学生适合课程设计或电子竞赛备赛场景。工程基于 Quartus 搭建利用相位累加器、频率控制字等核心机制生成方波、正弦波、三角波、锯齿波四种波形并支持通过按键实时调节输出频率帮助理解直接数字合成的完整工作流程。资源包共 449 个文件、约 15.57MB除核心 v 源码外还包含 qpf/qsf 工程配置、sof/pof 下载文件、仿真报告rpt/qmsg及波形初始化 mif 等同时保留大量 Quartus 综合产生的 hdb/rdb/ddb 中间数据便于逐级分析工程构建与布局布线过程。目前已吸引 498 人学习下载。对希望掌握 Verilog 编写、DDS 原理及 FPGA 信号发生器实现的读者而言这份资源既能作为课程设计参考也能作为工程实践的入门模板实用价值较高。1. 用FPGA做DDS信号发生器为什么方波比正弦波更考验时序在很多工程师的印象里DDSDirect Digital Synthesis直接数字频率合成是生成正弦波的首选方案——查表、累加、乘加输出经过DAC平滑滤波就能得到干净波形。但回到FPGA实现层面方波反而更考验设计功底方波的频谱含量丰富边沿质量直接由时钟抖动和输出驱动决定而DDS的相位截断误差会在方波边沿附近转化为可见的定时抖动。换句话说正弦波看幅度精度方波看时间精度两者在资源占用、参数设计和验证方法上有本质区别。这篇文章围绕“DDS FPGA 方波信号发生器”这条主线从DDS的相位累加器原理讲起给出可直接落地的Verilog实现再深入到方波特有的参数权衡、抗抖动设计和硬件验证方法。适合已经能写基础FPGA逻辑、想真正把信号发生器做到可用的工程师也适合从零开始做DDS项目的同学作为参考基线。2. DDS在FPGA上的核心参数核算频率控制字、相位累加器与初步选型2.1 从相位增量到输出频率频率控制字到底怎么算DDS的核心是一个相位累加器每个时钟周期累加一次频率控制字Frequency Control WordFCW累加结果的高位作为查表地址或直接作为方波输出的相位判据。输出频率的计算公式是f_out (FCW * f_clk) / 2^N其中N是相位累加器的位宽f_clk是系统时钟频率。这个公式是全部设计的基础所有参数调整都围绕它展开。举个例子如果N32f_clk100MHz希望输出1MHz方波那么FCW (1,000,000 * 2^32) / 100,000,000 42,949,673十六进制就是0x028F5C29。这个值可以直接写入寄存器不需要浮点运算。实际工程中我一般会把FCW的计算做成一个Python脚本避免手工换算出错。下面这段代码可以直接在命令行里用#!/usr/bin/env python3 def calculate_fcw(f_out, f_clk, n_bits32): fcw int((f_out * 2**n_bits) / f_clk) return fcw # 参数说明 # f_out 目标输出频率单位Hz # f_clk FPGA系统时钟频率单位Hz # n_bits 相位累加器位宽常见值为32或48 # 示例100MHz时钟下输出1MHz方波 fcw calculate_fcw(1_000_000, 100_000_000, 32) # 输出0x028F5C29 42949673 print(fFCW 0x{fcw:08X} {fcw}) # 验证反向计算 f_actual (fcw * 100_000_000) / 2**32 print(f实际输出频率: {f_actual:.6f} Hz)累加器位宽N的选择直接影响频率分辨率。32位在100MHz时钟下频率分辨率约为0.023Hz对绝大多数测试场景已经足够。48位累加器多用于对频率精度有极端要求的场合比如通信系统的载波恢复但FPGA里会多占约16个触发器普通信号发生器没必要。2.2 方波输出与正弦波输出在DDS架构上的路径分歧DDS查表方案生成正弦波时相位累加器的高位作为ROM地址ROM存储量化后的正弦值。生成方波则有两条不同路径。第一条路径是把ROM替换成比较器累加器的最高位MSB就是方波信号——MSB为1时输出高电平为0时输出低电平。这条路径最省资源只要一个寄存器位但只能产生50%占空比的方波。第二条路径是保留完整相位信息用相位值的比较阈值控制翻转位置通过调节阈值实现任意占空比。这个方案需要把N位相位值与可配置阈值比较多消耗一个比较器但灵活性大幅提升。我一般推荐第二条路径原因在于FPGA中LUT资源相对充裕而信号发生器最怕的是功能缺项。能调占空比的方波发生器在电机驱动、LED调光、数字电源等场景远比固定50%方案实用。2.3 相位截断的量化影响输出方波时误差在哪里正弦波DDS做相位截断后输出频谱会出现杂散这是很多文献反复讲的问题。方波DDS的相位截断影响不同主要体现为边沿位置的量化误差。假设累加器为32位只用最高8位作为比较器输入那么相位量化的步进是360°/256 1.40625°。折算到时间上在100MHz时钟下每个LUT输出周期约为39.06ns相位量化导致的边沿跳动就是这个值的整数倍。这个跳动在方波上直接表现为周期抖动Period Jitter不是正弦波那种频谱杂散测试方法和容忍标准完全不同。如果项目要求低抖动输出相位截断位宽要选大。完整做32位比较器在7系列FPGA上只需要一个DSP48E1或者约30个LUT6资源代价远小于预期不必刻意截断。3. 在Xilinx FPGA上实现DDS方波并驱动DAC完整的Verilog设计3.1 顶层模块架构和接口定义下面给出一个可在Xilinx Artix-7系列上直接综合的设计。实现的功能是通过UART配置频率控制字和占空比参数输出两路方波至DAC的I通道和Q通道其中Q通道固定相移90度。// dds_square_wave_top.v // DDS方波信号发生器顶层模块 // 目标器件Xilinx Artix-7 XC7A35T // 时钟100MHz module dds_square_wave_top ( input wire clk_100m, // 系统时钟100MHz input wire rst_n, // 异步复位低有效 input wire uart_rx, // UART接收波特率921600 output wire [11:0] dac_i_data, // DAC I通道数据直通方波 output wire [11:0] dac_q_data, // DAC Q通道数据正交方波 output wire dac_clk, // DAC时钟输出 output wire sq_wave_out // 未经过DAC的方波输出用于示波器验证 ); // 内部信号声明 reg [31:0] fcw_reg; // 频率控制字寄存器 reg [31:0] phase_acc_i; // I通道相位累加器 reg [31:0] phase_acc_q; // Q通道相位累加器 reg [11:0] dac_i_buf; // DAC I通道输出缓冲 reg [11:0] dac_q_buf; // DAC Q通道输出缓冲 reg [7:0] duty_cycle; // 占空比参数0-255高电平比例 // 方波生成比较器方案相位值高于阈值输出高电平 // 阈值由占空比参数控制实现1%-99%可调占空比 wire [31:0] threshold; assign threshold duty_cycle * 32h1000000; // 等效于duty_cycle * (2^24) always (posedge clk_100m or negedge rst_n) begin if (!rst_n) begin phase_acc_i 32d0; phase_acc_q 32d0; dac_i_buf 12h000; dac_q_buf 12h000; end else begin // 相位累加器按照FCW递增 phase_acc_i phase_acc_i fcw_reg; // Q通道相位滞后90度等价于在相位上偏移2^30 phase_acc_q phase_acc_i 32h40000000; // I通道方波生成和DAC输出 if (phase_acc_i threshold) begin dac_i_buf 12hFFF; // DAC满幅输出 end else begin dac_i_buf 12h000; // DAC零输出 end // Q通道方波生成 if (phase_acc_q threshold) begin dac_q_buf 12hFFF; end else begin dac_q_buf 12h000; end end end // 输出驱动 assign dac_i_data dac_i_buf; assign dac_q_data dac_q_buf; assign dac_clk clk_100m; // 直接方波输出经过输出寄存器的IOB寄存减少引脚抖动 reg sq_wave_reg; always (posedge clk_100m or negedge rst_n) begin if (!rst_n) begin sq_wave_reg 1b0; end else begin sq_wave_reg phase_acc_i[31]; // 直接取MSB固定50%占空比 end end assign sq_wave_out sq_wave_reg; // UART配置接口 uart_config #( .CLK_FREQ (100_000_000), .BAUD_RATE(921600) ) uart_cfg_inst ( .clk_100m (clk_100m), .rst_n (rst_n), .uart_rx (uart_rx), .fcw_out (fcw_reg), .duty_out (duty_cycle) ); endmodule这段代码注意几个关键设计点。相位累加器采用32位宽度不需要额外截断比较器直接使用完整32位值。Q通道相位偏移用了固定常量0x40000000等价于90度相移这个写法比单独用另一个累加器省一半硬件。DAC输出用了12位宽度全幅输出12hFFF实际使用时要根据DAC的接口格式调整某些DAC要求二进制补码格式。3.2 相位累加器加法和时序收敛一个容易忽视的细节上面的代码中Q通道直接取了phase_acc_i 32h40000000这是一个组合逻辑表达式。在FPGA实现中这会产生一个32位加法器位宽较大且不在时序路径的起点寄存器上可能成为关键路径。更好的做法是在模块内部额外注册一级reg [31:0] phase_acc_q_reg; always (posedge clk_100m or negedge rst_n) begin if (!rst_n) begin phase_acc_q_reg 32d0; end else begin phase_acc_q_reg phase_acc_i 32h40000000; end end这样加法器由时序逻辑完成综合工具可以在时钟周期内合理布局避免产生组合逻辑路径过长的问题。如果发现时序违例也可以考虑把Q通道做成独立的累加器每次同步加上同一个FCW但相位差需要通过复位时序来校正实现复杂度更高。3.3 UART配置模块实现寄存器读写与频率切换时机配置接口采用UART是为了方便在没有JTAG的板卡上调试。模块设计为接收4字节数据包结构是帧头0xA5然后频率控制字高16位频率控制字低16位最后是占空比字节。// uart_config.v // UART接收和寄存器配置模块 // 数据帧格式0xA5 FCW[31:16] FCW[15:0] DUTY_CYCLE module uart_config #( parameter CLK_FREQ 100_000_000, parameter BAUD_RATE 921600 )( input wire clk_100m, input wire rst_n, input wire uart_rx, output reg [31:0] fcw_out, output reg [7:0] duty_out ); localparam BIT_PERIOD CLK_FREQ / BAUD_RATE; // 状态机定义 localparam IDLE 3d0; localparam RECV_DATA 3d1; localparam RECV_PAYLOAD 3d2; reg [2:0] state; reg [15:0] cycle_cnt; reg [3:0] bit_cnt; reg [7:0] rx_byte; reg rx_done; reg [7:0] frame_buf [0:4]; // 5字节缓冲区 always (posedge clk_100m or negedge rst_n) begin if (!rst_n) begin state IDLE; cycle_cnt 16d0; bit_cnt 4d0; rx_done 1b0; end else begin case (state) IDLE: begin if (uart_rx 1b0) begin state RECV_DATA; cycle_cnt BIT_PERIOD / 2; // 采样到起始位中心 bit_cnt 4d0; end end RECV_DATA: begin if (cycle_cnt 0) begin if (bit_cnt 8d8) begin // 停止位采样 bit_cnt 4d0; rx_done 1b1; state RECV_PAYLOAD; end else begin rx_byte[bit_cnt] uart_rx; bit_cnt bit_cnt 1; cycle_cnt BIT_PERIOD - 1; end end else begin cycle_cnt cycle_cnt - 1; end end RECV_PAYLOAD: begin // 接收数据帧处理 rx_done 1b0; state IDLE; end endcase end end // 帧解析每5字节为一帧 reg [2:0] byte_index; always (posedge clk_100m or negedge rst_n) begin if (!rst_n) begin byte_index 3d0; fcw_out 32d0; duty_out 8d128; end else if (rx_done) begin frame_buf[byte_index] rx_byte; if (byte_index 3d4) begin // 完整帧已接收 if (frame_buf[0] 8hA5) begin fcw_out {frame_buf[1], frame_buf[2], frame_buf[3], frame_buf[4]}; duty_out frame_buf[4]; // 也接收第5字节作为占空比 end byte_index 3d0; end else begin byte_index byte_index 1b1; end end end endmodule这个UART模块采用了简单的过采样方式没有使用FIFO和双缓冲。它的主要作用是验证配置通路实际产品中如果要求无缝切频需要在写入FCW时做乒乓操作——先在影子寄存器里准备新值然后在时钟边沿统一加载避免配置过程中产生不可预知的输出频率。上面代码里FCW在每次完整帧解析后直接赋值存在毫秒级的频率跳变这一点在低抖动应用中需要额外处理。3.4 用Vivado创建工程并约束时钟和引脚在Vivado中创建工程时选择目标FPGA器件后需要添加几项核心约束。首先创建时钟约束文件XDC# clock.xdc # 系统时钟100MHz主时钟端口 create_clock -name sys_clk -period 10.000 -waveform {0.000 5.000} [get_ports clk_100m] # 输出延迟约束DAC接口为并行数据建立时间和保持时间由DAC数据手册决定 # 假设DAC的建立时间要求为2ns数据保持时间要求为1.5ns set_output_delay -clock [get_clocks sys_clk] -max 8.0 [get_ports {dac_i_data[*]}] set_output_delay -clock [get_clocks sys_clk] -min 1.5 [get_ports {dac_i_data[*]}] # 方波直接输出引脚IOB寄存器 set_property IOB TRUE [get_ports sq_wave_out]时钟周期设置为10ns是因为系统时钟100MHz。set_output_delay -max的值是时钟周期减去DAC建立时间再减去PCB走线延迟即10 - 2 8ns这是一个估算值实际要以PCB走线长度和DAC芯片手册为准。set_property IOB TRUE非常关键这个约束会把方波输出寄存器的flip-flop放到位I/O的专用寄存器位置缩短输出路径减少引脚上的时钟到输出延迟偏差。3.5 上板验证的第一步用示波器测量DDS方波频率精度综合、实现并生成比特流后将设计下载到FPGA开发板。连接示波器至sq_wave_out引脚设置触发电平为1.65V3.3V LVCMOS电平的中间值。测量要点用示波器的频率测量功能读取输出频率与理论频率进行比对。示波器的时基设置为与输出频率匹配例如1MHz方波可以用500ns/div。如果测得的频率偏差超过0.01%优先检查系统时钟实际频率——许多开发板上的晶振标称值与实际值有偏差部分板子甚至使用PLL倍频后的时钟实际频率可能与预期不符。另一个测量指标是上升沿的抖动。示波器开启余辉模式Infinite Persistence观察边沿叠加的情况。理想情况下边沿的模糊范围应小于1ns。如果抖动明显大于这个值需要查看时钟约束是否生效以及DAC芯片如果方波经由DAC输出的建立时间是否满足要求。4. 从方波到信号发生器DAC驱动、占空比调节和方波输出电路设计4.1 驱动DAC直接输出方波与正弦波的性能差异上面的设计中方波从FPGA引脚直接输出没有经过DAC。这种方式的好处是上升沿陡峭3.3V LVCMOS输出可以达到1ns以内。但直接引脚输出有局限幅值固定为FPGA I/O电压标准无法调节输出阻抗不匹配时长走线会产生反射造成振铃。实际产品中方波信号发生器通常在FPGA后级加一级DAC用全幅值代码0x000和0xFFF产生方波这样输出幅值由DAC的参考电压决定灵活性更高。但DAC输出有个不可忽视的问题——压摆率受限。比如12位DAC采用电流舵架构满量程建立时间为10ns则最大压摆率为满幅/10ns。此时输出的方波边沿已经不陡峭高频方波会变成梯形波。因此如果设计中方波只是一个辅助输出用DAC无妨如果方波是主力输出通道建议保留独立的GPIO直出方案或者用比较器把DAC输出整形回数字电平。4.2 占空比调节实现从固定阈值到可编程阈值占空比调节的原理是利用比较器相位值高于阈值输出高低于阈值输出低。阈值为相位值范围的某个比例这个比例决定了高电平持续的时间。上面代码中占空比控制字是8位范围0到255。阈值计算为duty_cycle * 32h1000000也就是duty_cycle * 2^24。这个换算的原因是32位相位累加器的满刻度是2^328位占空比控制字满刻度是2^8 256把8位扩展到32位需要左移24位。占空比控制字的有效范围需要注意0和255分别对应0%和100%占空比这两种情况下输出是恒定电平不是方波。因此控制字范围最好限制在1到254之间即理论占空比范围是0.39%到99.61%。实际使用中部分负载对极端占空比敏感建议软件层面限制到5%到95%之间。下面给出占空比阈值换算的Python脚本方便上位机计算#!/usr/bin/env python3 def duty_to_threshold(duty_percent, acc_width32): 将占空比百分比转换为DDS相位累加器比较阈值 duty_percent: 0.0 - 100.0 duty_cycle_byte int(duty_percent * 255 / 100) threshold duty_cycle_byte (acc_width - 8) return threshold, duty_cycle_byte # 示例生成25%占空比方波的阈值 th, duty_byte duty_to_threshold(25.0) print(f占空比控制字: 0x{duty_byte:02X}, 相位阈值: 0x{th:08X})上位机调用这个函数计算后通过UART把占空比字节写入FPGA即可。占空比参数一般放在一个专门的寄存器中配置实时生效不需要停止输出。需要注意的坑是如果工程中还使用DAC输出正弦波占空比参数不应影响正弦波通路需要在DDS核内做两套独立的输出链路。4.3 外围电路FPGA直出方波加一级驱动器和阻抗匹配FPGA引脚输出能力有限高电平驱动电流一般只有4到16mA。如果方波需要驱动50欧姆负载或者较长同轴电缆必须加一级驱动缓冲。典型做法是使用74LVC1T45电平转换芯片或专用时钟缓冲器如NB3N551。以一个常见的设计为例FPGA输出3.3V方波后级接74LVC2G17施密特触发缓冲器再通过50欧姆串联匹配电阻输出到SMA接口。这样的电路能把上升沿控制在1.2ns左右在100MHz时钟下约12%周期同时保护FPGA引脚免受外部反射的伤害。关键参数在驱动器选择时要注意驱动器的传播延迟Propagation Delay和通道间偏斜Skew。多路输出场景下通道间偏斜直接决定了方波相位的精度普通逻辑门的偏斜在0.5ns到1ns之间时钟专用缓冲器可以做到20ps以下。电路连接时串联匹配电阻的阻值应等于传输线特征阻抗减去驱动器输出阻抗。比如驱动器输出阻抗约17欧姆50欧姆传输线应串联33欧姆电阻。这个电阻的作用是吸收传输线上的反射波防止多次反射造成振铃。4.4 四类常见输出电路的对比与选择边界输出方案上升时间幅值灵活性输出阻抗典型应用FPGA引脚直出0.5-1ns固定I/O电平约50欧逻辑测试、示波器参考74LVC缓冲后出1-2ns固定3.3V/5V匹配后50欧常规实验室信号源DAC全幅输出5-20ns可调由DAC决定波形发生器一体方案比较器整形后出200ps-1ns可调通常50欧差分低抖动时钟测试选择方案的关键看应用指标的侧重点。如果关心上升沿和抖动用FPGA直出或专用时钟缓冲器如果关心幅值调节范围用DAC加比较器整形。比较器整形方案中要特别注意比较器的滞后电压Hysteresis设置过小的滞后会在边沿附近产生反复翻转形成振荡过大的滞后又会导致占空比偏移。对于几MHz的方波滞后设到5mV到10mV比较合适。5. 提高方波质量的三个实用技巧相位抖动抑制、电源滤波和验证方法5.1 消除累加器复位导致的相位跳变DDS设计中常见的一个问题是系统复位或频率切换时累加器清零或重新装载输出波形产生一个不完整的脉冲。解决思路是让频率切换发生在相位连续的时刻。具体做法是把FCW寄存器做成双缓冲配置接口先写入影子寄存器然后在某个约定的时钟沿统一载入。这个约定沿最好由外部触发信号决定或者选择在累加器过零时同步加载。// 相位连续切换的FCW更新逻辑片段 wire fcw_update_en; reg [31:0] fcw_shadow; reg [31:0] fcw_active; always (posedge clk_100m or negedge rst_n) begin if (!rst_n) begin fcw_active 32h028F5C29; // 默认1MHz end else if (fcw_update_en) begin // 只在相位累加器最高位翻转沿更新 fcw_active fcw_shadow; end endfcw_update_en的生成逻辑检测相位累加器最高位的上升沿在此刻更新FCW。因为此时方波正好处于低到高的翻转点输出电平不会产生毛刺。这个技巧在FMCW雷达或要求频率切换无相位突变的测试系统中是必需的。5.2 电源和时钟的滤波细节方波抖动与芯片工作状态强相关FPGA内部数字逻辑的大量翻转会通过电源网络引入噪声这个噪声会调制到输出方波的边沿位置表现为随机抖动Random Jitter。降低这个抖动的手段有三个。第一在原理图设计阶段给FPGA内核电源通常是1.0V增加磁珠和10uF钽电容加0.1uF瓷片电容的π型滤波。第二输出方波与FPGA内部逻辑使用不同的时钟域即使用独立时钟缓冲器驱动的干净时钟作为DDS的参考时钟。第三对于特别敏感的场合把方波输出通路放到FPGA的差分引脚上用LVDS标准输出外部用差分转单端电路恢复方波这样共模噪声被抑制。5.3 验证方波质量的完整测试流程用一台带宽至少500MHz的示波器进行验证。设置示波器的采样率为5GSa/s以上打开参数测量功能测量频率周期、上升时间、下降时间、正脉宽、负脉宽和周期抖动。具体流程是先把示波器时基调到方波周期的10倍左右稳定触发后测量频率然后切换到较小时基一个周期充满屏幕测量上升时间最后开启余辉模式并叠加超过1000个波形查看边沿的模糊分布范围。周期抖动的均方根值应小于时钟周期的1%例如100MHz时钟对应10ns周期抖动RMS应小于100ps。如果超过这个值优先检查DAC或驱动器的电源去耦电容是否足够以及驱动器的输入是否有阻抗失配。5.4 用信号发生器反过来验证FPGA板卡的时钟质量这里提一个工程上常用的辅助手段如果手上有一台高精度信号发生器可以把它输出的同频方波送入FPGA的时钟输入引脚用FPGA内部计数器测量两边方波的频率差。FPGA逻辑实现一个简单的频率计用内部时钟计数输入方波上升沿个数再通过UART上报。这个方法的精密度远高于示波器频率测量可以辅助定位系统时钟的初始误差。// 简易频率计测量外部方波在1s内的脉冲数 reg [31:0] pulse_cnt; reg [31:0] pulse_cnt_reg; reg [26:0] time_base_cnt; reg time_base_tick; always (posedge ext_clk or negedge rst_n) begin if (!rst_n) begin pulse_cnt 32d0; end else begin pulse_cnt pulse_cnt 1b1; end end always (posedge clk_100m or negedge rst_n) begin if (!rst_n) begin time_base_cnt 27d0; time_base_tick 1b0; end else if (time_base_cnt 27d99_999_999) begin time_base_cnt 27d0; time_base_tick 1b1; end else begin time_base_cnt time_base_cnt 1b1; time_base_tick 1b0; end end这段代码用两个时钟域分别计数和产生门控信号实际使用需要同步器和格雷码处理才能保证跨时钟域采样的正确性这里展示的是核心思路。本文还有配套的精品资源点击获取
返回列表