简介:针对AD9764这款十四位高速数模转换器,该资源是一套完整的FPGA逻辑驱动与数据采集参考工程,主要面向FPGA开发工程师、数字系统学习者以及从事信号采集处理的工程人员,帮助解决DAC芯片与FPGA之间的接口时序设计、并行数据写入、时钟配合等实际问题。压缩包整体约6.93MB,内部包含AD9764技术手册以及基于Quartus Prime 18.0创建的Verilog工程文件,源码与配置可直接打开查看,省去自行搭建工程环境的麻烦。该资源已有1048人学习,可作为设计参考被反复翻阅。使用这套工程,可以减少底层时序调试时间,让开发者将精力集中在算法与系统功能上。通过学习其中驱动逻辑,读者能够理解DAC的数据锁存、输出建立时间、复位与使能控制等关键细节,进而迁移到波形发生器、任意信号源、高速数据采集系统等应用场景,也可作为课程设计、毕业设计或项目预研的起点。
1. 直接用并行数据线怼上 AD9764 就能出波形?想简单了
把 FPGA 的 14 根 IO 接到 AD9764 的 DB0~DB13,再给个时钟,看着像是最简单的 DAC 驱动。真正上板以后你会发现,示波器显示的波形毛刺满天飞,正弦波变成了一级一级的台阶,甚至数据手册标称的 125 MSPS 在自家板子上连 1 MHz 都跑不稳。问题基本出在两个地方:一是数据时序没满足手册的建立保持时间,二是时钟信号和电源噪声把 DAC 的积分非线性直接放大成了可见失真。
AD9764 是 ADI 公司经典的 14 位、125 MSPS 并行电流输出型 DAC,常见于波形发生、数字中频和闭环控制前端。FPGA + Verilog 驱动它的工作量不大,但需要把接口时序、时钟管理、约束文件和验证环境串起来,才能让这颗 DAC 跑出接近手册的性能。我这篇博文就按「数据手册读哪些参数 → Verilog 驱动怎么写 → Quartus Prime 18.0 工程怎么搭 → 板上怎么验证」这条完整落地的路径来讲,适合正在用 Cyclone IV / MAX 10 之类 Altera 器件做信号源或者调制器前端的人。
2. AD9764 数据手册里必看的接口参数与 FPGA 连接方式
2.1 引脚功能与并行数据通道
AD9764 的并行接口一眼看上去只有三组信号:14 位数据总线 DB0~DB13、一个时钟输入 CLOCK、一个模式选择 MODE。数据手册的时序图会告诉你,DAC 在 CLOCK 上升沿锁存数据,所以在 FPGA 内部必须把输出数据对齐到时钟沿,而不是用组合逻辑直接输出。
| 引脚 | 方向 | 功能说明 | FPGA 连接建议 |
|---|---|---|---|
| DB13~DB0 | 输入 | 14 位并行二进制数据 | 对应 FPGA 普通 IO |
| CLOCK | 输入 | 数据锁存时钟,上升沿有效 | 用 PLL 输出或全局时钟引脚 |
| MODE | 输入 | 1=双缓冲模式,0=直通模式 | 普通 IO,按速率需求接固定电平 |
| REFIO | 输入/输出 | 内部 1.2V 参考电压 | 接 0.1uF 去耦电容到地 |
| FSADJ | 输入 | 满量程电流调整引脚 | 通过 RSET 电阻接地 |
| IOUT / IOUTN | 输出 | 互补电流输出 | 接运放或电阻负载 |
这里有一个新手必踩的坑:MODE 引脚。很多例程直接把 MODE 接地,让 DAC 工作在直通模式,数据一进总线就立刻转换。直通模式下,只要 FPGA 的 IO 翻转稍微不同步,毛刺就会直接反映到输出电流上。我一般会把 MODE 拉高,用双缓冲模式,让数据先进入第一级锁存器,再靠 LOAD 信号统一送入 DAC 寄存器,时序可控性明显更好。
FSADJ 引脚决定了满量程输出电流,公式是 IFS = 32 × VREFIO / RSET。VREFIO 典型值 1.2V,RSET 用 2kΩ 电阻时满量程电流约 19.2mA,需要在 IOUT 引脚接运放转换成电压。如果 RSET 选得太大,电流太小会牺牲信噪比,选太小又可能超出运放线性范围,2kΩ 作为起步值是稳妥的。
2.2 FPGA IO 标准与驱动能力设置
AD9764 的数据输入兼容 3.3V 和 5V 逻辑,Altera FPGA 常见的是 3.3V LVCMOS 或 2.5V LVCMOS。Quartus 里需要用 Assignment Editor 显式指定每个引脚的电平标准,默认的 3.3V 不一定能驱动较长的走线。
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to dac_data[0] set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to dac_clk set_instance_assignment -name CURRENT_STRENGTH_NEW "8MA" -to dac_data[0] set_instance_assignment -name SLEW_RATE "2" -to dac_data[0]注意SLEW_RATE选项。默认的慢摆率在走线过长时会让数据位的跳变时间不一致,建立时间和保持时间都会恶化;但摆率太快会带来更大的地弹噪声。对于 AD9764 这种 125 MSPS 级别的 DAC,8 mA 驱动电流加中低摆率通常是比较平衡的组合。IO 内部也不要配置成开漏模式,DAC 的并行数据总线是推挽输入,开漏输出反而会导致上升沿缓慢。
2.3 时钟抖动对 SNR 的影响与 PCB 布局
AD9764 的 CLOCK 引脚可以接收单端 CMOS 时钟,也可以接收差分 PECL 时钟。对 14 位分辨率而言,时钟抖动控制在几十皮秒量级以内才不会把 SNR 压下去。FPGA 内部 PLL 输出的时钟抖动在大多数情况下够用,前提是你别从普通 IO 引一根长线再绕回来,而是复用 Altera 的专用时钟输出引脚或者将时钟信号从靠近 DAC 的引脚引出。
PCB 布局方面有三个要点直接决定 DAC 的底线性能。第一,每个电源引脚都要就近放置 100nF 和 1uF 去耦电容,AVDD 和 DVDD 分开走线,模拟地和数字地在 DAC 下方单点相连。第二,数据总线和时钟线尽量等长,特别是时钟线要走阻抗受控的微带线,避免在 DAC 输入端产生反射。第三,IOUT 和 IOUTN 是电流输出,走线尽量短,负载电阻和运放靠近 DAC 放置,反馈电阻不要离运放太远。
3. Verilog 驱动逻辑实现:直通、双缓冲与 FIFO 对接
3.1 最基础的寄存器直通驱动
如果只是想让 DAC 先动起来,一个简单的寄存器锁存就能满足需求。把 FPGA 内部待发送的数据先用 D 触发器打一拍,再连接到 DAC 数据总线,避免组合逻辑冒险产生的毛刺被 DAC 锁存。
module dac14_direct #( parameter DATA_WIDTH = 14 )( input wire clk, input wire rst_n, input wire [DATA_WIDTH-1:0] data_in, output reg [DATA_WIDTH-1:0] dac_data, output reg dac_clk ); always @(posedge clk or negedge rst_n) begin if (!rst_n) begin dac_data <= {DATA_WIDTH{1'b0}}; dac_clk <= 1'b0; end else begin dac_data <= data_in; dac_clk <= ~dac_clk; end end endmodule这段代码里dac_clk是对输入时钟直接取反生成的。这样的优点是 DAC 在输入时钟的下降沿看到数据变化,在上升沿锁存,某种程度上弥补了输出延时不固定的问题。但这只是一个简化方案,实际工程里我建议直接让 PLL 把同一时钟分别送到 FPGA 寄存器和 DAC 时钟引脚,尽量减少时钟偏斜。clk用 FPGA 内部 PLL 输出,dac_clk用相同频率的 IO 输出,数据在时钟上升沿前后保持稳定即可。
3.2 双缓冲模式下的写时序状态机
MODE 接高电平后,DAC 内部变成一个双寄存器结构:数据先写入输入寄存器,LOAD 信号上升沿再把数据传送到 DAC 寄存器并开始转换。这种结构在数据需要并行更新时特别有用,例如同时驱动多片 DAC 做正交调制。
module dac14_double_buf #( parameter DATA_WIDTH = 14 )( input wire clk, input wire rst_n, input wire wr_en, input wire [DATA_WIDTH-1:0] data_in, output reg [DATA_WIDTH-1:0] dac_data, output reg load ); localparam IDLE = 2'b00; localparam STORE = 2'b01; localparam PULSE = 2'b10; reg [1:0] state; reg [DATA_WIDTH-1:0] buf_data; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; buf_data <= {DATA_WIDTH{1'b0}}; dac_data <= {DATA_WIDTH{1'b0}}; load <= 1'b0; end else begin case (state) IDLE: begin if (wr_en) begin buf_data <= data_in; state <= STORE; end end STORE: begin load <= 1'b1; state <= PULSE; end PULSE: begin dac_data <= buf_data; load <= 1'b0; state <= IDLE; end default: state <= IDLE; endcase end end endmodule这个状态机的逻辑是:外部把 14 位数据写入buf_data,第二个周期把load拉高一个时钟宽度,第三个周期在load下降沿把buf_data送入dac_data并输出给 DAC。注意load的宽度保持恰好一个时钟周期,不要用组合逻辑产生,否则在高速时钟下容易产生毛刺。如果系统需要流水线吞吐,可以把 IDLE 状态去掉,让写入操作每个周期都可以接受新数据,代价是 DAC 的更新频率会有两个周期延迟。
3.3 用异步 FIFO 把前端数据接到 DAC 时钟域
很多实际工程里,DAC 的数据不是直接从寄存器来,而是来自 ADC 采集、滤波运算或者以太网/串口下发的波形点。这些数据的产生时钟和 DAC 的更新时钟往往不在同一个域,直接跨时钟域打拍子会出现数据错位。常见做法是在中间插入一个异步 FIFO,把写侧时钟域的数据缓存下来,读侧用 DAC 时钟读出。
module dac14_fifo_bridge #( parameter DATA_WIDTH = 14, parameter FIFO_DEPTH = 1024 )( input wire wr_clk, input wire [DATA_WIDTH-1:0] wr_data, input wire wr_en, output wire fifo_full, input wire dac_clk, input wire rst_n, input wire rd_en, output wire [DATA_WIDTH-1:0] dac_data, output wire fifo_empty ); async_fifo #( .DATA_WIDTH(DATA_WIDTH), .FIFO_DEPTH(FIFO_DEPTH) ) u_fifo ( .wr_clk (wr_clk), .wr_en (wr_en), .wr_data (wr_data), .wr_full (fifo_full), .rd_clk (dac_clk), .rd_en (rd_en), .rd_data (dac_data), .rd_empty (fifo_empty) ); endmodule使用异步 FIFO 时有个细节:FIFO 的空满标志本身存在跨时钟域同步延迟,读侧判断fifo_empty之后再去读,数据要晚一拍才有效。所以 DAC 侧的读控制一般是组合逻辑判断,比如assign rd_en = ~fifo_empty;,然后再在dac_clk上升沿打拍输出。如果省略这条打拍,DAC 在最后一个数据读取时可能会读到无效数据,输出波形尾部会多出一个台阶或者毛刺。
3.4 代码组织中值得参数化的点
| 参数 | 推荐值 | 作用 |
|---|---|---|
| DATA_WIDTH | 14 | 匹配 AD9764 分辨率,改其他 DAC 时调整 |
| FIFO_DEPTH | 1024 或 4096 | 缓存深度,取决于写突发长度 |
| CLK_DIV | 1、2、4 | 需要对 DAC 降频时用的分频系数 |
| UPDATE_MODE | 直通/双缓冲 | 用一个参数控制 MODE 引脚逻辑 |
把这些参数提到模块顶部,换用其他 14 位 DAC(比如 AD9744、AD9760)时只需要改端口名称和时序参数,不用重新整理整个逻辑。如果后续要升级到 16 位 DAC,DATA_WIDTH改成 16,FPGA 引脚多分配两位,代码主体不需要重写。
4. Quartus Prime 18.0 完整工程:建工程、引脚约束与 ModelSim 仿真
4.1 创建工程并组织源文件
Quartus Prime 18.0 装载 Altera 全系列器件的工程,File → New Project Wizard 选择目标器件型号,比如 Cyclone IV EP4CE10F17C8。工程里至少包含三个文件:顶层 Verilog 文件、DAC 驱动模块、时序约束 SDC 文件。如果用了 PLL 或异步 FIFO,还需要添加对应的 IP 文件(.qip)。
Altera 的 FPGA 用什么软件开发,答案就是 Quartus Prime。18.0 版本对 Cyclone V 之前的器件支持最稳定,不要为了用新版本而去装 22.x,18.0 对 AD9764 这类简单逻辑完全足够。ModelSim 建议用随 Quartus 一起安装的 Intel FPGA Starter Edition 10.5b,免费的 ModelSim-Intel FPGA Edition 可以完成仿真验证。
4.2 引脚约束与 QSF 配置
引脚分配有两种方式:图形界面的 Assignment Editor,或者直接编辑 .qsf 文件。后者更适合版本管理,这里给出一个片段:
set_location_assignment PIN_C12 -to dac_clk set_location_assignment PIN_D12 -to dac_data[0] set_location_assignment PIN_E12 -to dac_data[1] set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to dac_clk set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to dac_data[*] set_instance_assignment -name SLEW_RATE "2" -to dac_clk set_instance_assignment -name OUTPUT_ENABLE_GROUP "GROUP_A" -to dac_data[*]OUTPUT_ENABLE_GROUP保证所有 DAC 数据引脚在配置完成后同时使能,避免上电瞬间某些引脚处于未定义状态把 DAC 输入端拉出异常电平。这里提醒一句,DAC 数据总线的引脚不要分散到 FPGA 的不同 BANK,尽量集中在一个 BANK 内。Altera 的每个 GPIO BANK 电压不同,混用 BANK 容易出现电平标准不匹配,极端情况下导致 DAC 数据位翻转时间差异上百皮秒。
时序约束里面有几行不可或缺的内容。时钟定义必须存在,否则 TimeQuest 会报未约束时钟;输出延时是为了让综合器知道 DAC 的数据建立时间要求。
create_clock -name dac_clk -period 8.000 [get_ports dac_clk] set_output_delay -clock [get_clocks dac_clk] -min -add_delay 1.000 [get_ports {dac_data[*]}] set_output_delay -clock [get_clocks dac_clk] -max -add_delay 2.000 [get_ports {dac_data[*]}]这里假设 DAC 时钟周期 8 ns(即 125 MHz),DAC 数据手册要求的建立时间约 2 ns、保持时间约 1 ns。set_output_delay的-min和-max分别对应保持时间和建立时间要求,具体数值要从 AD9764 数据手册的 AC 特性表查,不同参考设计给出的余量习惯不一样。
4.3 ModelSim 仿真验证驱动逻辑
仿真这一步可以在没有硬件的情况下先验证状态机的正确性。写 Testbench 时,给模块输入一个正弦波查表数据序列,观察 dac_data 输出是否符合预期。
module tb_dac14; reg clk_100m; reg rst_n; reg [13:0] sine_data; wire [13:0] dac_out; wire load; dac14_double_buf u_dac ( .clk (clk_100m), .rst_n (rst_n), .wr_en (1'b1), .data_in (sine_data), .dac_data (dac_out), .load (load) ); always #5 clk_100m = ~clk_100m; reg [13:0] rom [0:1023]; integer i; initial begin $readmemh("sine_table.hex", rom); rst_n = 0; clk_100m = 0; #100 rst_n = 1; for (i = 0; i < 2048; i = i + 1) begin @(posedge clk_100m); sine_data = rom[i & 1023]; end #1000 $finish; end endmodule$readmemh读取十六进制波形表文件,Testbench 再用每个时钟沿向模块写入新数据。仿真时重点看两件事:load 信号是否只有一个时钟周期宽度,dac_data 是否比数据输入延迟正好两个周期。
仿真遇到17.1 error: failure to obtain a verilog simulation license这类错误时,先查 ModelSim 的许可证路径LM_LICENSE_FILE环境变量是否指向有效的 license 文件。Quartus 免费版的 ModelSim Starter Edition 不支持所有器件库,如果器件是 Cyclone 10 或较新的 MAX 10,需要下载对应版本的仿真库并手动编译到 ModelSim 的目录下。常常是许可证问题,不是代码问题。
4.4 SignalTap II 板上抓到真实数据
仿真过了并不代表板子没问题。用 SignalTap II 加一个触发条件,抓取 DAC 数据总线和状态机的关键信号。这里要注意 SignalTap 占用的逻辑资源和存储器会在综合后改变布局,PLL 的相位偏移可能和没有插 IP 时不同,所以抓完信号后,如果 DAC 输出有所改善,最好重新编译一次再跑。
SignalTap 触发建议设在 FIFO 读侧,看rd_en和fifo_empty的时序关系;如果发现rd_en在 FIFO 快空的时候拉高,但数据已经停止更新,就要检查读侧逻辑是否多打了一拍。这个场景在异步 FIFO 里特别容易出现,听上去像是 FPGA 裂了,实际上只是空标志和读使能之间的握手差了周期。
5. 用 DDS 相位累加器生成扫频正弦波并量化输出质量
驱动的最终验证不能只看波形像不像正弦,还要看频率误差和频谱纯度。把 DAC 驱动模块接到一个直接用 DDS 相位累加器实现的信号源,就能在示波器上观察扫频结果,用频谱仪看杂散水平。
module dds_dac_sweep #( parameter PHASE_WIDTH = 24, parameter DATA_WIDTH = 14 )( input wire clk, input wire rst_n, input wire [PHASE_WIDTH-1:0] freq_word, output wire [DATA_WIDTH-1:0] dac_data ); reg [PHASE_WIDTH-1:0] phase_acc; always @(posedge clk or negedge rst_n) begin if (!rst_n) phase_acc <= 0; else phase_acc <= phase_acc + freq_word; end sine_lut u_sine ( .phase (phase_acc[PHASE_WIDTH-1:PHASE_WIDTH-10]), .data (dac_data) ); endmodule输出频率的计算公式是f_out = f_clk × freq_word / 2^PHASE_WIDTH。以 100 MHz 时钟、24 位相位累加器为例,freq_word = 1677721时约输出 100 kHz,扫频时用一个计数器分段改变freq_word即可。相位截断取高 10 位查表,引入的量化噪声大约在 -60 dB 以下,对 14 位 DAC 来说刚刚好;如果要求更高,把查表地址提高到 12 位或者改用分段线性插值。
扫频开始时先用低频 1 kHz 验证双缓冲模式下的波形是否平滑,再把频率往上抬。AD9764 在理想条件下 SFDR 能到 70 dBc 以上,但板上跑 20 MHz 以上时如果频谱出现明显的谐波簇,优先检查电源去耦而非 DAC 本身。用频谱仪测 10 MHz 正弦输出,比较基波与二次谐波差值,低于 55 dB 的时候基本可以断定时钟抖动或数字馈通是主要来源,回头调整 PCB 布局的接地比在代码里加滤波更有效。
这套驱动方案同样能直接移植到任意 14 位并行 DAC 上,AD9744 只需改参考电压配置,AD9764 的替代型号在时序上基本兼容,换器件后重新仿真一遍读写时序就能跑通。调通一个信号源以后,把 DAC 驱动的输出接到 LPF 和功放级,就能作为整块数据采集板的前端激励通道来使用。
本文还有配套的精品资源,点击获取