FPGA实现DDS:AD9767双通道14位DAC与Cyclone IV E的Verilog设计
简介这是一套基于AD9767高速双通道DAC与Cyclone IV E FPGA的DDS实验例程面向FPGA开发者、数字信号处理学习者及需要快速上手AD9767驱动与DDS信号生成的工程师。工程基于Quartus 13.0提供完整的Verilog源码包含顶层AD9767_DDS模块、PLL时钟管理、按键控制与LED指示等逻辑可直接编译下载到开发板验证。资源包共148个文件以.v源码、.tdf描述文件、.qpf/.qsf工程文件、.sof配置文件及.mif初始化文件等为主附带完整的Quartus编译工程数据和readme说明压缩包仅4.18MB结构紧凑便于参考。已有517人学习下载适合在FPGA课程设计或毕业设计中借鉴AD9767双通道波形发生、DDS频率控制等关键实现。1. 为什么是 AD9767 Cyclone IV E这套 DDS 方案的定位与整条链路用 FPGA 做 DDS瓶颈很少在逻辑本身多半出在 DAC 那一侧位宽够不够、更新率撑不撑得住、双通道相位对不对得齐。标题这个组合——AD9767 高速双通道 14 位 DAC配 Cyclone IV E FPGA在 Quartus 13.0 里用 Verilog 写例程——正好把从相位累加器到模拟输出的整条链路串齐。适合正在做信号源、扫频源、I/Q 正交波形或者第一次要把 DDS 从仿真搬到板子上的人。文章按搭板级 DDS 的真实顺序写先算清频率控制字、累加器位宽、查找表深度这些参数再落到 Quartus 13.0 工程和可综合 Verilog最后讲验证输出和快速定位问题的办法。DDS 的难点从来不在背公式而在参数之间怎么互相牵制、数字侧和模拟侧怎么接上。2. DDS 参数模型与 AD9767 双通道数字接口2.1 相位累加器怎么算频率控制字、位宽与频率分辨率DDS 的核心是用累加代替正弦计算。每个时钟周期相位累加器把自身值加上频率控制字 FCW累加器溢出一次就代表正弦走完一个周期。输出频率满足f_out FCW × f_clk / 2^N其中 N 是相位累加器位宽。频率分辨率就是 FCW 加 1 时输出频率的变化量Δf f_clk / 2^N。同样跑在 100 MHzN20 时分辨率约 95 HzN32 时约 0.023 Hz。做信号源或者扫频源32 位累加器是常规选择代价不过几十个寄存器没必要省。累加器位宽 N频率分辨率f_clk100 MHz典型场景320.023 Hz信号源、扫频源、需要细步进的仪器280.373 Hz通信调制器、波形发生器245.96 Hz快速原型、对频率步进不敏感的功能演示注意频率分辨率只由 N 决定和查找表深度、DAC 位宽没有关系。查找表深度影响的是相位截断杂散和波形纯度DAC 位宽影响的是幅度量化噪声这两个参数都不改变步进细度。2.1.1 频率控制字反算公式与 Python 脚本知道目标频率 f_out反算 FCW 的常用公式是 FCW round(f_out × 2^N / f_clk)。比如 f_clk100 MHz、N32、想要 1 MHz1e6 × 2^32 / 1e8 42,949,672.96四舍五入后是 42,949,673十六进制 0x028F5C29。#!/usr/bin/env python3 # 给定目标频率计算 32 位 DDS 频率控制字 fclk 100e6 # DAC 实际锁存数据的时钟单位 Hz N 32 for fout in (1e6, 1.5e6, 10e6): fcw int(round(fout * 2**N / fclk)) f_actual fcw * fclk / 2**N print(f目标 {fout/1e6:6} MHz | FCW0x{fcw:08X} | 实际 {f_actual/1e6:.6f} MHz)脚本输出 0x028F5C29、0x03D70A3D 这类值直接填进 Verilog 的 parameter 或寄存器。两个地方容易错fclk 必须是 AD9767 实际锁存数据的时钟不是板载晶振频率FCW 取整在高频时仍有小于 1 ppm 的误差仪器级应用要按实际算出的频率重新校准。扫频或跳频时每次重算 FCW 太慢可以预生成一张查找表把频率点和 FCW 一一对应存进 ROM。2.2 AD9767 双通道数字接口的数据组织方式与 I/O 电平AD9767 是双通道 14 位电流输出 DAC最大更新率在 125 MSPS 这个量级具体以数据手册为准。数字输入按偏移二进制理解数据 0x0000 对应一端的满量程电流0x20008192对应差分输出为零0x3FFF 对应另一端满量程。DDS 查找表直接输出以 8192 为中心的正弦序列即可省掉符号转换和补码运算。两个通道各自独立的 14 位数据总线外加各自的 WRT 锁存引脚数据在 WRT 上升沿被采进 DAC 内部寄存器。FPGA 侧要做三件事把数据引脚约束成 3.3-V LVCMOS 推挽输出把 WRT 当作随路采样时钟处理而不是普通使能信号数据时序按采样时钟来约束。信号方向说明DB13..DB0A/BFPGA 到 DAC14 位并行数据偏移二进制WRTA/BFPGA 到 DAC上升沿锁存数据即采样时钟IOUTA / IOUTBDAC 到外部电流输出需电阻或运放转电压FS ADJ外接电阻按手册推荐范围设定满量程电流2.2.1 建立保持时间窗口与推挽、开漏的认知差AD9767 的数字输入端是标准 LVCMOS 接收器不是开漏结构FPGA I/O 配置成普通推挽输出就行不需要外接上拉。这里有个常见误解做过 I2C、EEPROM 这类开漏接口的人容易把“输出结构”和“输出电平标准”混在一起给 DAC 数据口配成开漏结果高电平拉不上去。Cyclone IV E 的 I/O 没有 ARM 那种完整的上拉、推挽模式表按 LVCMOS 设置驱动强度和压摆率即可这也是 FPGA 做并行接口比单片机省心的地方。时序上真正要算的是 WRT 上升沿那一瞬间数据还在不在。数据随 FPGA 时钟翻转经过输出延迟 tco 之后才有效WRT 采样沿必须落在数据不再变动的区间内。低速时随便写都能跑到了 100 MHz 这个量级就要显式处理具体做法放到第 4 章核心思路是输出寄存器加时钟相移。提示AD9767 数据手册里的 tSU、tH 是针对固定测试负载给出的板级布线长短、FPGA 输出压摆率都会改变实际裕量约束时按手册典型值再留 1 ns 余量。3. 在 Quartus 13.0 里搭建 DDS 工程并写出可综合的 Verilog 核心3.1 新建工程Cyclone IV E 器件选择与引脚约束打开 Quartus II 13.0sp113.0 版本线File → New Project Wizard芯片型号按板子选常见的是 EP4CE15F17C8 或 EP4CE22F17C8器件族选 Cyclone IV E。Cyclone IV 器件库在 13.0 里是自带的不需要额外安装。时钟输入默认用板上 50 MHz 晶振后面用 PLL 倍频出 100 MHzDDS 逻辑和 DAC 接口共用这一个时钟。工程建好后先写引脚约束。下面这段 QSF 把时钟、复位和 DAC 数据总线指到具体引脚并把 I/O 标准统一设成 3.3-V LVCMOSset_location_assignment PIN_M9 -to clk_50m set_location_assignment PIN_R17 -to rst_n set_location_assignment PIN_L14 -to dac_a_data[0] set_location_assignment PIN_L16 -to dac_a_data[1] set_instance_assignment -name IO_STANDARD 3.3-V LVCMOS -to clk_50m set_instance_assignment -name IO_STANDARD 3.3-V LVCMOS -to dac_a_data[*] set_instance_assignment -name IO_STANDARD 3.3-V LVCMOS -to dac_b_data[*] set_instance_assignment -name IO_STANDARD 3.3-V LVCMOS -to dac_wrt_a set_instance_assignment -name IO_STANDARD 3.3-V LVCMOS -to dac_wrt_b引脚号必须按自己板子的原理图改这里只是示例。带 [*] 的总线赋值一次覆盖整组信号比逐位写省事。DAC 数据口离 FPGA 远、布线长时把驱动强度调高一档或者在 Assignment Editor 里打开 fast output register让输出路径短一些、tco 散布小一些。3.1.1 PLL 例化与时钟域说明用 MegaWizard Plug-In Manager 生成 altpllinclk0 接 50 MHzc0 输出 100 MHzc1 输出供 AD9767 WRT 使用的相移时钟。PLL 输出相对输入是确定的相位关系整个设计里只有一个时钟域不存在异步跨时钟域问题也就不需要异步 FIFO 或者脉冲同步器。有人把 DDS 逻辑放在 50 MHz、DAC 接口放在 100 MHz 才需要做跨时钟域处理这里不要画蛇添足。3.2 DDS 核心 Verilog相位累加器与查找表例化下面 dds_channel 模块是一个通道的完整 DDS 核心参数化位宽可以直接综合module dds_channel #( parameter N 32, // 相位累加器位宽 parameter ADDR_W 12, // 查找表地址位宽 parameter DATA_W 14 // 查找表数据位宽与 DAC 一致 )( input wire clk, input wire rst_n, input wire [N-1:0] fcw, // 频率控制字 input wire [N-1:0] psk, // 相位偏移字单位与累加器一致 output wire [DATA_W-1:0] dout // 正弦幅度偏移二进制 ); reg [N-1:0] phase_acc; always (posedge clk or negedge rst_n) begin if (!rst_n) phase_acc {N{1b0}}; else phase_acc phase_acc fcw; end // 相位偏移加在累加器全宽上低位不参与寻址但保留相位精度 wire [N-1:0] phase_sum phase_acc psk; wire [ADDR_W-1:0] addr phase_sum[N-1:N-ADDR_W]; sin_rom u_rom ( .clock (clk), .address (addr), .q (dout) ); endmodule逻辑说明复位时累加器清零每个时钟沿加一次 FCW查找表地址取累加器高 12 位低 20 位自然截断。addr 取高位而不是低位这个细节决定了频率对不对不少例程写错成 phase_acc[11:0]波形频率差了 2^20 倍还不自知。psk 加到全宽上做精细相移时低位没有白白丢掉。复位在这里很重要。上电瞬间 FCW 和 psk 可能还没就绪累加器从随机状态起跑输出会有一段不确定相位做相干解调或相位测量时影响明显所以顶层复位必须可靠释放。3.3 14 位正弦查找表MegaWizard ROM 与 MIF 生成查找表存 4096 点、14 位正弦地址 12 位。两种做法各有用途方式存储资源修改频率适用场景MegaWizard 生成 ROM: 1-PORTM9K 块改 MIF 后重编译正式工程、需要预编译初始化initial $readmemh 的 reg 数组M9K 块或逻辑改 .hex 即可仿真优先、快速验证正式工程推荐第一种。Tools → MegaWizard Plug-In Manager → ROM: 1-PORT数据宽 14深度 4096输出寄存器选项勾上初值文件选下面脚本生成的 sin_4096x14.mif。生成后的 sin_rom.v 和 qip 文件会加入工程顶层直接例化。#!/usr/bin/env python3 # 生成 4096 x 14 偏移二进制正弦 MIF 文件 import math W, D 14, 4096 with open(sin_4096x14.mif, w) as f: f.write(fWIDTH{W};\nDEPTH{D};\n) f.write(ADDRESS_RADIXHEX;\nDATA_RADIXHEX;\n) f.write(CONTENT BEGIN\n) for i in range(D): v int(round(8192 8191 * math.sin(2 * math.pi * i / D))) f.write(f{i:03X} : {v:04X};\n) f.write(END;\n)MIF 内容是以 8192 为中心的偏移二进制正弦0 度对应 0x200090 度对应 0x3FFF270 度对应 0x0001。幅度用 8191 而不是 8192是为了输出对称、不削顶。MIF 放进工程目录后直接全编译。看编译报告里查找表占用的 M9K 数量如果显示 0 个却占了几百个逻辑单元多半是综合器没把存储推断成 ROM检查 qip 文件有没有被排除出工程。注意$readmemh 方式在 Quartus 13.0 里也能综合成 RAM但 .hex 文件必须作为 Memory Initialization File 加入工程否则仿真波形一片空白综合出来的 ROM 也没有初值。4. 双通道顶层相位控制、输出对齐与 SignalTap II 验证4.1 双通道独立频控与 90 度相位差的顶层 Verilog双通道的意义不只是省一颗 DAC。两个相位累加器驱动同一个查找表能保证两路幅度、频率完全同源只差一个可编程相位偏移这是 I/Q 调制和相干检测的硬需求。下面是顶层代码module dds_top ( input wire clk_50m, input wire rst_n, output wire [13:0] dac_a_data, output wire [13:0] dac_b_data, output wire dac_wrt_a, output wire dac_wrt_b ); wire clk_dac; wire [13:0] sin_a, sin_b; wire [13:0] sin_a_r, sin_b_r; pll u_pll ( .inclk0 (clk_50m), .c0 (clk_dac) // 100 MHz ); dds_channel #(.N(32), .ADDR_W(12), .DATA_W(14)) u_ch_a ( .clk (clk_dac), .rst_n (rst_n), .fcw (32h028F5C29), // 1 MHz .psk (32h00000000), // 0 度 .dout (sin_a) ); dds_channel #(.N(32), .ADDR_W(12), .DATA_W(14)) u_ch_b ( .clk (clk_dac), .rst_n (rst_n), .fcw (32h028F5C29), // 1 MHz .psk (32h40000000), // 90 度 .dout (sin_b) ); always (posedge clk_dac or negedge rst_n) begin if (!rst_n) begin sin_a_r 14d0; sin_b_r 14d0; end else begin sin_a_r sin_a; sin_b_r sin_b; end end assign dac_a_data sin_a_r; assign dac_b_data sin_b_r; assign dac_wrt_a ~clk_dac; assign dac_wrt_b ~clk_dac; endmodule两个通道各自例化一套 dds_channel挂在同一颗 PLL 时钟下相位累加器在同一时钟沿更新通道间不会累积相位偏移。通道 B 的 psk 写成 0x40000000 就是 90 度相移。顶层输出又加了一级寄存器这是为 AD9767 的采样窗口准备的原因在 4.2 节展开。4.1.1 相位偏移量对照与换算psk 的单位是 2^N 分之一周期N32 时常用角度对照如下相位差psk 值0°0x0000000090°0x40000000180°0x80000000270°0xC0000000任意角度用 角度 / 360 × 2^32 换算后取整。单独调 90 度相移不用改逻辑改 psk 参数重编译即可比在查找表里放两套数据省一半 M9K。4.2 输出寄存器与 WRT 时钟相移给 AD9767 留出采样窗口查找表输出本身带一级内部寄存器顶层再加一级目的是让数据停留在 I/O 单元附近。没有这级输出寄存器14 位数据路径长短不一到达 DAC 引脚的时间可能差出 1 到 2 ns高速下直接表现为波形毛刺。WRT 在顶层用反相时钟驱动数据在 clk_dac 上升沿变化WRT 在下降沿翻转、上升沿采样给数据留出约半个周期的建立时间。100 MHz 下这是 5 ns减去 FPGA 的 tco 和 DAC 的建立时间后余量一般属于“能用但不算宽裕”的做法。把采样率推到手册上限附近时改用 PLL 的 c1 输出相移时钟专门驱动 WRTwire clk_wrt; pll u_pll ( .inclk0 (clk_50m), .c0 (clk_dac), // 0 度数据时钟 .c1 (clk_wrt) // -90 度相移采样时钟 ); // assign dac_wrt_a clk_wrt;相移值要结合 TimeQuest 的输出延迟约束确定。SDC 里类似这样写set_output_delay -clock [get_clocks clk_wrt] -max 3.5 [get_ports {dac_a_data[*] dac_b_data[*]}] set_output_delay -clock [get_clocks clk_wrt] -min -0.5 [get_ports {dac_a_data[*] dac_b_data[*]}]其中 max 对应外部 DAC 要求在采样沿之前多久数据必须有效min 对应采样沿之后数据要保持多久。先按手册典型值填跑完时序报告再微调。很多人忽略这一步结果低速功能正常、一上高速就时好时坏问题往往不在逻辑而在约束。4.3 用 SignalTap II 抓相位累加器与正弦输出先把验证做进 FPGA 里再谈示波器。Quartus 13.0 自带 SignalTap IITools → SignalTap II Logic Analyzer新建 .stp 文件选器件把 phase_acc[31:24]、sin_a、sin_b 加进信号列表采样深度设 2048采样时钟选 clk_dac。重新全编译后 Program Device触发条件设成 sin_a 跨越 0x2000 中点。这时能看到三件事相位累加器是否按 FCW 步进、查找表输出是否连续正弦、两路之间是否固定差 90 度。SignalTap 会占用 M9K 做采样缓冲EP4CE15 上这点深度完全够用。先在逻辑分析仪里确认数字侧正常再接示波器看模拟输出能把模拟端的问题单独隔离出来。提示SignalTap 的采样时钟必须等于被观察信号的实际更新时钟否则抓到的 phase_acc 看起来像随机数这不是逻辑 bug是采样时钟没选对。5. 仿真与板级校准验证频率准确度、排掉三个高频问题5.1 用 ModelSim-Altera 跑 Testbench数周期验证频率Quartus 13.0 配套的 ModelSim-Altera 可以直接跑验证。写一个极简 testbench 例化顶层50 MHz 时钟、20 ns 半周期复位拉低 100 ns 后释放跑 200 μs。目标 1 MHz 输出在 200 μs 里应该有 200 个完整正弦周期把游标放在第 1 个和第 201 个过零点之间时间差就是 200 μs反推频率与目标一致。验证双通道相位差更直接把 db 波形和 da 叠在一起两路 90 度点的时间差应该是 0.25 μs也就是 1 MHz 的四分之一周期。用高 12 位寻址频率误差来源只剩 FCW 取整1 MHz 时误差在 10^-7 量级仿真里看不出来。仿真波形正确后再烧到板子上顺序不要颠倒。5.2 板级最常见的三个位置第一波形固定在中间电平不动。先看 PLL 锁定信号和 rst_n 有没有释放再看 SignalTap 里 phase_acc 有没有累加。FCW 赋成 0、顶层忘了连复位是这里最常见的两个原因。第二频率是期望值的整数倍或几分之一。检查 addr 是不是取了 phase_acc 的高位。写成 phase_acc[11:0] 会让查找表跳过大部分数据输出频率完全乱这类错误在仿真里数周期就能发现。第三波形有台阶或毛刺。数字侧先怀疑数据线序和 WRT 相位模拟侧先怀疑电流输出端的 I-V 转换电路。AD9767 输出是电流必须经过合适阻值的电阻或运放转换成电压再接示波器直接量引脚看不到完整波形。数字地和模拟地单点连接避免开关噪声灌进模拟地。提示改任何参数后先在 SignalTap 里确认相位累加器在动、LUT 输出是正弦再碰模拟端。把验证顺序固定成“时钟 → 相位累加器 → 查找表 → DAC 数据 → 模拟输出”绝大多数 DDS 问题能在前四步里定位完。最后留一句把 dout 接到示波器之前先在逻辑分析仪里看几屏波形这事比任何理论计算都快。本文还有配套的精品资源点击获取