简介:基于FPGA的DDS信号发生器设计工程,面向数字电路与信号处理学习者,可生成方波、正弦波、三角波、锯齿波四种基本波形,通过按键调整频率控制字(FCW)实现频率精细调节。工程采用相位累加器产生数字相位,方波通过相位范围比较输出高低电平,正弦波借助查找表实现,三角波与锯齿波利用累加翻转逻辑,完整呈现DDS核心架构。压缩包共449个文件,以Quartus工程数据库文件(hdb、cdb、rdb等)为主,含Verilog源码、mif波形存储表、sof/pof配置文件、pin引脚分配、仿真报告及readme说明,整体15.57MB。已有498人学习下载。同时附带完整工程备份与分层目录,可快速定位各模块并直接打开修改,适合课堂实验、课程设计及竞赛准备。通过该工程可深入理解FPGA并行计算与直接数字合成技术的结合方式,为通信测试、音频处理等实际应用提供设计参考。
1. 用FPGA把方波做到MHz级:DDS的工程化选择
方波谁都会生成,一个定时器翻转IO就能跑出几百kHz。但当你需要区分1.000000 MHz和1.000001 MHz、需要16路相位严格同步、需要频率切换时波形不产生相位跳变,计数器和PLL方案就都不成立了。直接数字合成(DDS)通过相位累加器把频率分辨率做到系统时钟的2的N次方分之一,FPGA负责把累加、查表和波形映射放进并行流水线。这套基于FPGA的DDS信号发生器可同时输出正弦波、方波、三角波和锯齿波,方波只是其中实现成本最低的一路:一个比较器逻辑即可。它适合FPGA入门阶段想弄清楚DDS机制的人,也适合测试测量、电机驱动等场景需要自研廉价信号源的工程师。
2. DDS相位累加器原理与FPGA位宽选型
2.1 相位累加器决定频率分辨率
DDS的本质是把相位随时间做线性增长,再将当前相位映射成波形幅度。相位累加器是一个N位寄存器,每个时钟脉冲增加一个频率控制字FCW,累加到2^N后自然溢出回绕,完成一次完整相位扫描。输出频率由f_out = FCW × f_clk / 2^N决定。以50 MHz系统时钟、32位累加器为例,频率分辨率是50e6 / 4294967296 ≈ 0.0116 Hz,也就是说FCW可以无级分辨到毫赫兹级别,这是计数器分频方案难以达到的。
在FPGA里,相位累加器退化成一条加法链,综合后就是一组级联的加法器和寄存器,Xilinx的DDS IP核内部实现完全相同。自己写累加器最大的好处是每一级都可以通过仿真窗口观察,行为模型与硬件一一对应。对信号发生器仿真或FPGA入门学习来说,这种方式能同时把RTL设计和DDS数学联系起来。DDS相位输出连续增长,因此频率切换过程不会产生毛刺或相位跳变,这也是DDS适合做扫频信号源的根本原因。相比之下,AD9834/AD9850等DDS芯片遵循同样原理,只是把累加器做进专用硅片;FPGA方案的优势在于波形种类和位宽都能自定义,改一段ROM就能出任意非标准波形。
2.2 相位截断与查表深度取舍
用完整32位相位去索引波形ROM需要2^32个存储单元,即使改用BRAM也需要上百块,因此工程上只取相位累加器的高位作为地址。截断会带来相位噪声,正弦波表现为杂散分量;但方波这类二值波形几乎不受截断影响,ROM深度只影响多种波形复用一个地址总线时的拾取范围。
| 累加器位宽 | 查表地址位宽 | 频率分辨率@50MHz | 典型场景 |
|---|---|---|---|
| 24 | 10 | 2.98 Hz | 教学演示 |
| 32 | 12 | 0.0116 Hz | 通用信号发生器 |
| 32 | 14 | 0.0116 Hz | 低杂散正弦输出 |
| 48 | 16 | 1.78e-7 Hz | 精密仪器 |
从表格可见,32位累加器配合12位地址是最均衡组合。计算FCW时可直接用Python脚本批量生成,省去手工换算:
f_clk = 50e6 # 系统时钟50MHz N = 32 # 累加器位宽 for f_out in (100e3, 1e6, 5e6): fcw = int(round(f_out * 2**N / f_clk)) print(f"f_out={f_out/1e6:.3f} MHz, FCW={fcw}")脚本输出结果就是后面要写进RTL的常数或按键步进的基准值,浮点累积误差不会渗入FCW。这里需要注意,f_out不能超过奈奎斯特上限f_clk/2,DDS理论上限虽然可以到f_clk/2,但实际工程建议不超过f_clk/3,否则镜像分量会明显干扰方波边沿质量。
2.3 四种波形的相位映射差异
| 波形 | 实现方式 | 额外资源 | 相位连续性 |
|---|---|---|---|
| 正弦波 | ROM查表 | 1块BRAM | 连续 |
| 方波 | 取累加器最高位 | 无 | 连续 |
| 三角波 | 高位异或加取反 | 2个LUT | 连续但有尖角 |
| 锯齿波 | 直接截取高位 | 无 | 回绕处瞬跳 |
方波的映射最简单:相位累加器最高位为0时输出低电平,为1时输出高电平。这个判断在Verilog里就是一行连续赋值,综合后只有一个LUT和一个输出寄存器:
assign square_raw = phase_acc[PHASE_WIDTH-1]; // 50%占空比方波三角波要利用方向位。取高12位phase_hi后,方向位为0直接输出,为1取反输出,得到从0升到满幅再降回0的对称波形。锯齿波不需要方向判断,直接把phase_hi当作幅度,溢出回绕即完成重置。对比四种映射可以发现,方波和锯齿波是零成本波形,三角波消耗2个LUT,只有正弦波必须依赖存储器,这也是很多低成本信号发生器优先保证方波和锯齿波精度的原因。对FPGA入门学习者来说,建议先用方波打通整个链路,因为方波不依赖ROM,仿真时任何位宽变化都能通过dac_data的0/1跳变直接观察,调通了再添加正弦ROM和三角波逻辑。
3. Verilog实现四波形DDS信号发生器
3.1 顶层模块与端口规划
顶层模块把DDS拆成相位累加器、方波生成、波形选址三个独立部分。端口设计上保留12位DAC数据总线,波形选择用2位拨码,频率控制字由按键模块给出。参数化设计让同一套代码可以适配不同时钟和DAC位宽,改动仅限顶层参数。
| 端口名 | 方向 | 位宽 | 功能说明 |
|---|---|---|---|
| clk | input | 1 | 50 MHz 系统时钟 |
| rst_n | input | 1 | 低电平复位 |
| wave_sel | input | 2 | 00正弦 01方波 10三角 11锯齿 |
| btn_freq | input | 4 | 频率调整按键输入 |
| dac_data | output | 12 | 当前波形采样值输出 |
module dds_top #( parameter PHASE_WIDTH = 32, parameter ADDR_WIDTH = 12, parameter DAC_WIDTH = 12 )( input wire clk, input wire rst_n, input wire [1:0] wave_sel, input wire [3:0] btn_freq, output wire [DAC_WIDTH-1:0] dac_data ); wire [PHASE_WIDTH-1:0] freq_ctrl; wire [PHASE_WIDTH-1:0] phase_acc; wire [ADDR_WIDTH-1:0] phase_hi = phase_acc[PHASE_WIDTH-1 -: ADDR_WIDTH]; wire [DAC_WIDTH-1:0] squ_data, sin_data, tri_data, saw_data; dds_phase_accum #(.PHASE_WIDTH(PHASE_WIDTH)) u_phase ( .clk(clk), .rst_n(rst_n), .freq_ctrl(freq_ctrl), .phase_acc(phase_acc) ); dds_square_wave #(.DAC_WIDTH(DAC_WIDTH)) u_square ( .clk(clk), .phase_acc(phase_acc), .dac_data(squ_data) ); assign dac_data = (wave_sel == 2'b01) ? squ_data : (wave_sel == 2'b10) ? tri_data : (wave_sel == 2'b11) ? saw_data : sin_data; endmodule逻辑说明:顶层把所有子模块通过相位总线串起来,dac_data用多路选择器在四种波形间切换。phase_acc是全局唯一的相位基准,四个波形模块共享同一个相位累加器输出,因此切换波形时相位不会重置,周期间连续。wave_sel切换瞬间dac_data就切换到目标波形,不需要额外的握手信号。
参数说明:PHASE_WIDTH和DAC_WIDTH是核心位宽参数,修改后内部所有总线自动对齐。ADDR_WIDTH只影响ROM地址截取,对方波无影响。被例化的两个子模块都使用同名参数,Vivado综合时会检查参数传递是否一致,不匹配会直接报错提示。
3.2 相位累加器与频率控制字加载
module dds_phase_accum #( parameter PHASE_WIDTH = 32 )( input wire clk, input wire rst_n, input wire [PHASE_WIDTH-1:0] freq_ctrl, output reg [PHASE_WIDTH-1:0] phase_acc ); always @(posedge clk or negedge rst_n) begin if (!rst_n) phase_acc <= {PHASE_WIDTH{1'b0}}; else phase_acc <= phase_acc + freq_ctrl; end endmodule逻辑说明:这是整套信号发生器的频率基准。每个时钟上升沿累加一次FCW,FCW固定时相位斜率恒定,输出频率稳定;FCW变化时相位从当前值继续走,不产生相位跳变。复位后相位归零,确保首拍从相位0开始输出。累加器自然溢出,不需要显式清零,这也是DDS能够覆盖超宽频率范围的原因。
参数说明:freq_ctrl位宽必须与PHASE_WIDTH一致,否则综合时发生位宽截断,高频段频率会突然跳变。复位优先级高于累加,上电后建议保持复位至少一个时钟周期再释放,防止相位累加器进入未知态。若需要多通道同步输出,只需复制多个累加器并共用一个复位和时钟,相位天然同步。
3.3 方波、三角波与锯齿波的并行生成
module dds_square_wave #( parameter DAC_WIDTH = 12 )( input wire clk, input wire [31:0] phase_acc, output reg [DAC_WIDTH-1:0] dac_data ); always @(posedge clk) begin if (phase_acc[31]) dac_data <= {DAC_WIDTH{1'b1}}; // 高电平满幅 else dac_data <= {DAC_WIDTH{1'b0}}; // 低电平零 end endmodule逻辑说明:方波利用相位累加器最高位做幅度映射,输出只有满幅和零两种状态。每个时钟周期最多翻转一次,边沿严格对齐相位0和π位置,不会出现计数器方案那种比较器建立时间带来的抖动。dac_data在时钟沿后更新,和DAC接口的建立时间天然匹配。
参数说明:DAC_WIDTH为12时满幅输出4095,接10位DAC则改为10位全1,多出的低位直接截断。如果只想要1位CMOS逻辑方波,可以不经过DAC模块,直接把最高位引到FPGA引脚,驱动能力取决于该引脚的IO标准设置。若要调整占空比,把判断位换成相位累加器高位的某个阈值比较即可。
三角波和锯齿波从相同的高位相位直接构造:
wire tri_dir = phase_hi[ADDR_WIDTH-1]; wire [ADDR_WIDTH-1:0] tri_amp = tri_dir ? (~phase_hi) : phase_hi; assign tri_data = {tri_amp, {(DAC_WIDTH-ADDR_WIDTH){1'b0}}}; assign saw_data = {phase_hi, {(DAC_WIDTH-ADDR_WIDTH){1'b0}}};逻辑说明:phase_hi取自相位累加器高12位。tri_dir为1时位取反,波形开始下降,构成峰值为满幅的三角波;位取反不产生进位链,综合后延迟比减法小。锯齿波直接截取高位,回绕瞬间从满幅跌到0,对应相位累加器的自然溢出点。这两路数据在顶层多路选择器中与方波和正弦波汇合。
参数说明:如果三角波需要偏置到非对称范围,可以加一个直流偏置寄存器,在拼接前与tri_amp做加法。这里用拼接操作把12位相位扩展到12位DAC总线,若DAC_WIDTH大于ADDR_WIDTH,剩余低位补零,量化噪声由地址位宽决定。
正弦波模块只需例化ROM:用phase_hi索引预先用Python生成的coe文件,coe中每个地址存放一个12位正弦样本。ROM在Vivado中会推断为BRAM,数据文件用Xilinx ROM IP核加载。四路波形共用同一个phase_hi总线,切换不会造成相位错位。
4. 频率控制字计算与按键调频接口实战
4.1 FCW取值与频率换算
DDS输出频率由f_out = FCW × f_clk / 2^N决定,反向推导FCW = f_out × 2^N / f_clk。以50MHz时钟和32位累加器为例,目标1MHz时FCW = 1e6 × 4294967296 ÷ 50e6 = 85899345.92,取整85899346,实际输出频率偏差小于0.000005 Hz,远小于晶振ppm级别误差。
| 目标频率 | FCW | 实际频率误差 |
|---|---|---|
| 10 kHz | 858993 | <0.0000004 Hz |
| 100 kHz | 8589934 | <0.000004 Hz |
| 1 MHz | 85899345 | <0.000005 Hz |
| 5 MHz | 429496729 | <0.000023 Hz |
| 12.5 MHz | 1073741824 | <0.00006 Hz |
如果使用24位累加器,1MHz对应FCW = 335544.32,只能取整335544,实际频率相差约2.98 Hz,这就是24位方案只适合教学的原因。设计时把上表直接固化成ROM查找表或按键步进数组,能避免FPGA运行时做定点除法,节省大量逻辑资源。实际产品中,频率和FCW的对应关系通常由上位机算好后下发,FPGA只负责加载和锁定。
按键切换频率时,freq_ctrl寄存器根据步进更新:
reg [PHASE_WIDTH-1:0] freq_ctrl; always @(posedge clk or negedge rst_n) begin if (!rst_n) freq_ctrl <= 32'd85899345; // 默认1 MHz else if (key_freq_up) freq_ctrl <= freq_ctrl + 32'd85899; // 步进约1 kHz else if (key_freq_dn) freq_ctrl <= freq_ctrl - 32'd85899; end逻辑说明:FCW更新与相位累加器解耦,按键触发只修改freq_ctrl寄存器。相位累加器下一拍就按新FCW累加,频率切换立即生效且不打断当前相位,这是DDS相对于锁相环方案的核心优势。递增步进85899对应1kHz的FCW增量,实际偏差约0.0004 Hz。
参数说明:步进值要防止累加越界,freq_ctrl最高不超过f_clk/2对应FCW,否则输出频率超过奈奎斯特限制,方波会严重失真。上限保护可以用饱和比较实现,到达上限后保持不再累加。步进值在不同频段可切换,低频段用细步进,高频段用粗步进。
4.2 按键消抖与脉冲生成
机械按键直接接在逻辑上会产生5-20ms抖动,一次按压可能被判定多次。常规做法是用20ms窗口采样,配合两级同步触发器消除亚稳态:
reg [1:0] btn_sync; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin btn_sync <= 2'b11; cnt_20ms <= 0; btn_prev <= 1'b1; key_freq_up <= 1'b0; end else begin btn_sync <= {btn_sync[0], btn_freq[0]}; if (cnt_20ms == 20'd999_999) begin cnt_20ms <= 0; key_freq_up <= (btn_sync[1] == 1'b0) && (btn_prev == 1'b1); btn_prev <= btn_sync[1]; end else begin cnt_20ms <= cnt_20ms + 1'b1; end end end逻辑说明:btn_sync是两级触发器同步链,把异步按键输入转变到系统时钟域。每20ms对同步后的信号采样一次,检测到电平从高变低时输出一个周期宽度的key_freq_up脉冲。这个脉冲直接送给freq_ctrl更新逻辑,一次按压只产生一次触发。
参数说明:50MHz下计数到999999正好是20ms,想加快响应可以缩短窗口到5ms,但消抖效果会变差。这里以btn_freq[0]单键示例,多按键时每个按键独立复制一套同步采样逻辑,或用状态机轮询减少寄存器开销。按键低有效和高有效板的差异可以在同步链前加一个反相器统一。
4.3 频率范围与步进精度的工程约束
32位累加器频率分辨率极高,但按键步进必须和应用匹配。100Hz步进对应FCW增量约86个单位,适合低频精细调节;MHz级扫频则需要大步进快速遍历。可以设计两个按键分别控制粗调细调,粗调步进为1MHz对应FCW,细调步进为1kHz对应FCW,两个按键同时按下时归零复位。
如果完全不想自己写累加器,Xilinx的DDS Compiler IP核可以配置相位累加器位宽、输出通道数和相位偏移,但IP核生成的正弦查表是固定结构,方波输出仍需自己从phase_out里取最高位。自己写RTL与IP核混用是常见做法:用IP核处理正弦,用自定义逻辑处理方波和锯齿波,既保留IP核的时序优化,又拿到二值波形的零成本优势。
DDS输出方波在接近f_clk/2时,每周期只有两三个采样点,边沿位置只能落在相位累积序列上,方波的上升沿间隔会出现量化抖动。仪器级方波源通常把DDS频率上限限制在f_clk/8以下,比如50MHz时钟只输出到6.25MHz,这样每个周期至少16个采样点,抖动肉眼不可见。
5. 仿真验证与板级调试技巧
5.1 用Vivado行为仿真验证FCW与相位翻转
testbench把顶层模块实例化,设置固定FCW运行足够周期数:
module tb_dds_top; reg clk = 1'b0; reg rst_n = 1'b0; reg [1:0] wave_sel = 2'b01; wire [11:0] dac_data; always #10 clk = ~clk; // 50 MHz initial begin repeat (5) @(posedge clk); rst_n = 1'b1; repeat (10000) @(posedge clk); $finish; end dds_top #( .PHASE_WIDTH(32), .ADDR_WIDTH(12), .DAC_WIDTH(12) ) u_dds_top ( .clk(clk), .rst_n(rst_n), .wave_sel(wave_sel), .btn_freq(4'b0000), .dac_data(dac_data) ); endmodule逻辑说明:复位释放后跑10000个时钟周期,足够看到多个完整方波周期。仿真结束后在波形窗口添加phase_acc和dac_data,检查phase_acc[31]的翻转周期是否为FCW整倍。10000周期只是起点,若要验证1MHz方波在50MHz时钟下有50个周期,建议仿真5倍数量观察频率漂移。
参数说明:波形的切换验证可以在仿真的中段改变wave_sel,观察dac_data是否从当前相位点连续过渡到目标波形,不应出现从0重新开始的现象。仿真的时间步长默认1ns,对50MHz时钟足够精确。
5.2 方波上升沿与DAC建立时间的匹配
FPGA并行数据到DAC,通常要求DAC_CLK比数据晚半个周期采样,以避开数据翻转的非稳态区。在Vivado约束里对DAC数据线设置set_output_delay,把数据驱动延迟约束到DAC建立时间窗口内。若DAC数据是输入到FPGA,则用set_input_delay约束外部数据相对DAC_CLK的到达时间。常见做法是在顶层用ODDR原语输出DAC_CLK,确保时钟和数据边沿对齐。
方波整形的工程建议:在DAC后加一级高速比较器把模拟沿转换为陡峭CMOS沿,阈值设在DAC满幅一半处。方波经过比较器后,边沿抖动由比较器传播延迟抖动决定,不再受相位量化噪声影响。这个做法在输出给电机预驱或通信同步信号时非常关键。
5.3 频率计实测与晶振精度判断
上板后用频率计设定1MHz输出,测量10秒周期数,配合信号发生器仿真结果对照。如果测得频率稳定在999999.8到1000000.2之间,说明DDS链路正常,误差主要来自板载晶振偏差。此时用高精度外部时钟源替换板载晶振,偏差会直接回落,借此可以区分是时钟问题还是逻辑问题。若偏差超过0.1%,优先检查时钟树约束和晶振负载电容。
最后一个可落地的技巧:在Vivado里用set_false_path约束按键同步链路径,因为按键信号没有固定周期要求,加上约束能减少时序报告中的噪音。在PCB改版或升级DAC芯片时,这个约束能帮你快速定位真正的时序失效点,而不是被按键路径的假违规带偏方向。
本文还有配套的精品资源,点击获取