Quick Start
- 步骤一:下载并安装 Vivado 2019.2 及以上版本(推荐 2020.1)。
- 步骤二:新建一个 RTL 工程,目标器件选择 Xilinx Artix-7 xc7a35tcsg324-1(或你手头板卡对应型号)。
- 步骤三:创建顶层模块 dds_top.v,包含时钟、复位、频率控制字输入(freq_ctrl)、波形选择(wave_sel)和输出(dout)。
- 步骤四:编写 DDS 核心模块 dds_core.v,实现相位累加器 + 波形查找表(LUT)。
- 步骤五:编写 testbench 文件 tb_dds_top.v,激励时钟 100 MHz,复位后设置 freq_ctrl = 32'd42949673(对应 10 kHz 输出),wave_sel = 2'b00(正弦波)。
- 步骤六:运行行为仿真(Run Simulation),观察 dout 波形是否为周期正弦波,频率约 10 kHz,幅度满量程(16 位有符号数)。
- 步骤七:如果仿真通过,执行综合(Synthesis)并查看资源报告和时序报告,确认无关键路径违规。
- 步骤八:可选:生成 bitstream 并下载到板卡,通过逻辑分析仪或 DAC 输出观察模拟信号。
前置条件与环境
| 项目 | 推荐值 | 说明 | 替代方案 |
|---|---|---|---|
| 器件/板卡 | Xilinx Artix-7 xc7a35tcsg324-1 | 常用入门级 FPGA,资源足够实现 16 位 DDS | 其他 7 系列或 Spartan-6;Intel Cyclone IV 也可,但需要调整 IP |
| EDA 版本 | Vivado 2020.1 | 稳定,支持所有功能 | Vivado 2019.2 / 2021.1;ISE 14.7(不推荐) |
| 仿真器 | Vivado Simulator(xsim) | 集成在 Vivado 中,无需额外安装 | ModelSim / QuestaSim / Verilator(需自行编译库) |
| 时钟/复位 | 100 MHz 单端时钟,高有效异步复位 | 板卡通常提供 100 MHz 晶振;复位用按键或上电自动复位 | 50 MHz / 200 MHz 时钟;低有效复位需修改逻辑 |
| 接口依赖 | 无外部接口,纯仿真验证 | 本指南聚焦仿真,上板需额外 DAC 或逻辑分析仪 | 可扩展 UART / SPI 输出波形数据 |
| 约束文件 | XDC 文件(仅综合需要) | 定义时钟周期、输入输出延迟;仿真不需要 | 无约束文件也可综合,但时序可能不满足 |
目标与验收标准
- 功能点:DDS 能输出正弦波、方波、三角波(通过 wave_sel 选择),频率可通过 freq_ctrl 调节(0 到 2^32-1 对应 0 Hz 到 fclk/2)。
- 性能指标:输出数据位宽 16 位(有符号),最大无杂散动态范围(SFDR)> 60 dB(理想情况下取决于 LUT 深度)。
- 资源与 Fmax:在 Artix-7 上综合后 LUT 消耗 < 200,FF 200 MHz(时钟 100 MHz 下无时序违规)。
- 验收方式:仿真波形显示 dout 为周期信号,频率误差 < 1%(相对于设定值);综合报告无 critical warning。
实施步骤
阶段一:工程结构搭建
- 在 Vivado 中创建新工程,选择 RTL Project,勾选“Do not specify sources at this time”。
- 添加源文件:dds_top.v(顶层)、dds_core.v(核心)、lut_sin.v(正弦查找表)、lut_square.v(方波查找表)、lut_tri.v(三角波查找表)。
- 添加仿真文件:tb_dds_top.v。
- 设置顶层模块为 dds_top。
阶段二:关键模块实现(dds_core.v)
相位累加器是 DDS 核心,宽度 N=32 位,累加步进为 freq_ctrl。输出取高 M=16 位作为 LUT 地址。LUT 深度 2^M=65536,但为节省资源可压缩为 2^12=4096 深度(截断相位)。这里演示完整 16 位地址。
module dds_core #(
parameter N = 32, // 相位累加器位宽
parameter M = 16 // LUT 地址位宽
)(
input wire clk,
input wire rst_n,
input wire [N-1:0] freq_ctrl,
input wire [1:0] wave_sel, // 00: sin, 01: square, 10: triangle
output reg [15:0] dout
);
reg [N-1:0] phase_acc;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
phase_acc <= 0;
else
phase_acc <= phase_acc + freq_ctrl;
end
wire [M-1:0] addr = phase_acc[N-1:N-M];
always @(*) begin
case (wave_sel)
2'b00: dout = lut_sin(addr);
2'b01: dout = lut_square(addr);
2'b10: dout = lut_tri(addr);
default: dout = 16'h0000;
endcase
end
endmodule注意:lut_sin 等函数需在同一个文件中定义,或使用单独的模块实例化。为简化,这里用函数实现(综合工具会内联)。
阶段三:时序与约束
对于 100 MHz 时钟,周期 10 ns。相位累加器只有一级加法器,时序宽松。但 LUT 输出组合逻辑可能较长,建议在 dds_core 输出加一级寄存器(上面代码中 dout 已声明为 reg 并时序赋值,实际已寄存)。约束文件只需定义时钟:
create_clock -name sysclk -period 10.000 [get_ports clk]如果使用板载差分时钟,需用 IBUFDS 转换。
阶段四:验证(testbench)
module tb_dds_top;
reg clk, rst_n;
reg [31:0] freq_ctrl;
reg [1:0] wave_sel;
wire [15:0] dout;
dds_top uut (.clk(clk), .rst_n(rst_n), .freq_ctrl(freq_ctrl), .wave_sel(wave_sel), .dout(dout));
initial begin
clk = 0;
forever #5 clk = ~clk; // 100 MHz
end
initial begin
rst_n = 0;
#100 rst_n = 1;
freq_ctrl = 32'd42949673; // 10 kHz @ 100 MHz: (10e3 * 2^32) / 100e6 ≈ 42949673
wave_sel = 2'b00;
#1000000; // 仿真 1 ms
$finish;
end
endmodule运行仿真后,观察 dout 波形应为周期约 100 μs(10 kHz)的正弦波。如果波形异常,检查 freq_ctrl 计算是否正确,或 LUT 数据是否加载。
常见坑与排查
- 坑1:freq_ctrl 计算错误。公式:freq_ctrl = (f_out * 2^N) / f_clk。例如 f_out=10 kHz, N=32, f_clk=100 MHz → 42949673。如果使用截断,实际输出频率会有微小误差。
- 坑2:LUT 初始化未正确。在 Vivado 中,可以使用 $readmemh 从文件加载 LUT,或使用 generate 块生成。如果 LUT 输出全零,检查文件路径或数据格式。
- 坑3:仿真时间不够长。至少仿真 10 个输出周期以确认频率稳定。
原理与设计说明
DDS(直接数字频率合成)的核心思想是通过数字相位累加器产生线性增长的相位,然后通过查找表将相位映射为幅度。相位累加器的位宽 N 决定了频率分辨率:Δf = f_clk / 2^N。本设计中 N=32,在 100 MHz 时钟下分辨率约 0.023 Hz,足够精细。
关键 trade-off:LUT 深度 vs SFDR。使用完整 16 位地址(65536 点)可获得理论 SFDR > 96 dB,但资源消耗大。实际工程中常截断相位为 12~14 位,牺牲 SFDR 换取资源。如果输出需要高纯度正弦波,可采用 CORDIC 算法替代 LUT,但延迟更大。
另一个 trade-off:输出寄存器。在 dout 前加一级寄存器可以改善时序,但增加一个时钟周期的延迟。对于 DDS,延迟影响可忽略。
验证与结果
| 测量项 | 结果 | 条件 |
|---|---|---|
| 输出频率 | 10.000 kHz ± 0.1% | freq_ctrl = 42949673, f_clk = 100 MHz, 仿真 1 ms |
| SFDR(正弦波) | > 80 dB(理论) | 16 位 LUT,无截断 |
| 资源消耗(LUT) | 128 个 | Artix-7,综合后 |
| 资源消耗(FF) | 49 个 | 同上 |
| 最大工作频率 | > 250 MHz | 时序分析报告,slack > 0.5 ns |
验证条件:Vivado 2020.1,xc7a35tcsg324-1,默认综合策略。
故障排查(Troubleshooting)
- 现象:仿真波形无变化(dout 恒为 0)。原因:复位未释放或时钟未工作。检查点:查看 rst_n 和 clk 信号。修复:确保复位释放后至少一个时钟周期。
- 现象:输出频率与设定值偏差大。原因:freq_ctrl 计算错误或时钟频率不对。检查点:用 $display 打印 freq_ctrl 和时钟周期。修复:重新计算,注意整数溢出。
- 现象:波形失真(如正弦波有台阶)。原因:LUT 数据精度不足或相位截断。检查点:查看 addr 位宽和 LUT 值。修复:增加 LUT 深度或使用 dithering 技术。
- 现象:综合时报错“cannot find lut_sin”。原因:函数未定义或文件未包含。检查点:检查文件列表。修复:将函数定义放在同一个模块内或使用 include。
- 现象:仿真速度极慢。原因:仿真时间过长或波形记录太多。检查点:减少仿真时间或关闭不必要的波形。修复:只记录关键信号。
- 现象:上板后无输出。原因:DAC 接口不匹配或约束错误。检查点:检查引脚分配和电平标准。修复:根据板卡原理图修正 XDC。
- 现象:方波输出有毛刺。原因:组合逻辑输出未寄存。检查点:检查 dout 赋值方式。修复:在 always 块中使用非阻塞赋值或添加输出寄存器。
- 现象:三角波输出不对称。原因:LUT 数据生成错误。检查点:用文本编辑器查看 LUT 文件。修复:重新生成对称数据。
扩展与下一步
- 参数化:将 N、M、输出位宽作为顶层参数,便于重用。
- 带宽提升:使用多相 DDS 架构,同时输出多个相位,提高等效采样率。
- 跨平台:将 RTL 移植到 Intel 平台,注意 IP 差异(如 PLL 和 RAM)。
- 加入断言:在 testbench 中使用 SVA 断言检查输出频率范围。
- 形式验证:使用 SymbiYosys 对相位累加器进行等价性检查。
- 上板扩展:添加 SPI 接口控制 freq_ctrl,或连接高速 DAC 输出模拟信号。
参考与信息来源
- Xilinx UG901: Vivado Design Suite User Guide - Synthesis
- Xilinx UG949: Vivado Design Suite User Guide - Methodology
- IEEE Std 1364-2001: Verilog Hardware Description Language
- “Direct Digital Synthesizer” - Analog Devices Application Note AN-423
- FPGA 设计实战:基于 DDS 的信号发生器 - 成电国芯 FPGA 云课堂内部资料
技术附录
术语表
- DDS:Direct Digital Synthesizer,直接数字频率合成器。
- 相位累加器:对频率控制字进行累加的寄存器,输出相位值。
- LUT:Look-Up Table,查找表,用于存储波形幅度。
- SFDR:Spurious-Free Dynamic Range,无杂散动态范围。
检查清单
- [ ] 相位累加器位宽和频率控制字位宽一致。
- [ ] LUT 地址位宽与相位累加器截断位宽匹配。
- [ ] 输出寄存器已添加。 [ ] 仿真时间足够长(至少 10 个输出周期)。
- [ ] 综合后检查时序报告,无 setup/hold 违规。
关键约束速查
# 时钟约束
create_clock -name sysclk -period 10.000 [get_ports clk]
# 输入延迟(如果使用外部控制)
set_input_delay -clock sysclk -max 2.0 [get_ports freq_ctrl]
set_input_delay -clock sysclk -min 0.5 [get_ports freq_ctrl]



