简介:这份资源面向FPGA与数字信号处理方向的开发者,提供基于Verilog的AD9954 DDS驱动程序及配套PDF文档,帮助解决高频信号合成中芯片初始化、参数配置与接口时序控制等实际问题。压缩包共95个文件,约707KB,以41个h头文件与36个c源文件为主体,辅以8个汇编文件、Keil工程文件(uvprojx、uvoptx)及一份AD9954.pdf技术文档,覆盖驱动分层、寄存器定义与工程编译配置。内容围绕相位累加器、查找表与DA转换链路展开,涉及SPI或并行接口编程、频率相位增益设置及状态读取等关键环节,V2.0版本对早期缺陷做了修复与优化。已有589人学习下载,适合希望掌握DDS技术、Verilog建模与AD9954工程落地的读者参考借鉴。
1. AD9954 驱动为什么值得自己写:从一颗 DDS 芯片到可复现的 Verilog 方案
手上有一块 AD9954 的板子,想让它输出一个干净的正弦波,最省事的做法是找现成驱动直接烧进去。但真到调试阶段就会发现,频率分辨率、相位连续、扫频斜率这些参数一旦要按自己的需求改,现成驱动往往改不动,寄存器写错一位输出就是一片噪声。AD9954 是 ADI 的一款 400MSPS、14 位 DAC 的直接数字频率合成(DDS)芯片,内部有 32 位频率控制字、14 位相位偏移和幅度控制,支持线性扫频、单频、RAM 调制等多种模式。它的驱动本质就是一套 Verilog 状态机,把配置字按 SPI 时序写进寄存器,再配合 IO_UPDATE 把数据同步到内部。这套东西自己写一遍,参数怎么算、时序怎么卡、扫频怎么起停,全都握在手里。适合做信号源、雷达中频、锁相环参考、任意波形发生器的硬件工程师和 FPGA 开发者,尤其是需要 Verilog 参数递增、模块例化这类基础操作落地的人。
2. AD9954 的寄存器模型与 SPI 写时序:先把黑匣子拆开
2.1 寄存器地址映射与关键控制字
AD9954 内部有 20 多个寄存器,驱动里真正天天打交道的是这几个:CFR1(0x00)控制工作模式和扫频使能,CFR2(0x01)管扫频步进和 DAC 满量程,FTW0(0x04)是频率调谐字,POW0(0x05)是相位偏移字,ASF(0x06)是幅度比例因子,还有扫频相关的 NLSCW、PLSCW、RDW、FDW。每个寄存器宽度不一样,CFR1 是 32 位,FTW0 是 32 位,POW0 是 14 位有效,ASF 是 16 位。写的时候要先发寄存器地址字节,再发数据字节,地址字节的最高位决定是读还是写,写操作该位为 0。
频率控制字的计算公式是 FTW = (f_out × 2^32) / f_sysclk。假设系统时钟 400MHz,想输出 10MHz,FTW = 10e6 × 4294967296 / 400e6 ≈ 107374182,转成十六进制是 0x06666666。这个数算错一位,输出频率就偏得离谱。相位偏移字 POW = (phase_deg / 360) × 2^14,14 位对应 0 到 16383。幅度 ASF 是 16 位,满幅 0x3FFF 左右,具体看 DAC 满量程设置。
| 寄存器 | 地址 | 位宽 | 作用 | 常用值示例 |
|---|---|---|---|---|
| CFR1 | 0x00 | 32 | 工作模式、扫频使能 | 0x00000002 |
| CFR2 | 0x01 | 32 | 扫频步进、DAC 满量程 | 0x00000000 |
| FTW0 | 0x04 | 32 | 频率调谐字 | 0x06666666 |
| POW0 | 0x05 | 14 | 相位偏移 | 0x0000 |
| ASF | 0x06 | 16 | 幅度比例 | 0x3FFF |
2.2 SPI 写时序的 Verilog 状态机实现
AD9954 的 SPI 是四线制:SCLK、SDIO、CS_N、IO_UPDATE。数据在 SCLK 上升沿采样,SDIO 是双向的,写的时候只做输出。一次完整的写操作是:拉低 CS_N,按位发送地址字节(8 位),再发送数据字节(从高位到低位),发完拉高 CS_N,然后给一个 IO_UPDATE 脉冲把数据从缓冲写到内部寄存器。SCLK 频率不能超过 25MHz,实际跑 10MHz 比较稳。
下面是一个可综合的写寄存器任务,用状态机实现,参数化位宽方便复用:
// AD9954 单寄存器写任务,addr 为寄存器地址,data 为数据,len 为数据位宽 task ad9954_write_reg; input [7:0] addr; input [31:0] data; input [5:0] len; // 数据有效位宽,如 FTW0 为 32,POW0 为 14 integer i; begin cs_n = 0; // 发送地址字节,最高位为 0 表示写 for (i = 7; i >= 0; i = i - 1) begin sclk = 0; sdio = addr[i]; #5 sclk = 1; // 上升沿采样 #5; end // 发送数据,从最高有效位开始 for (i = len - 1; i >= 0; i = i - 1) begin sclk = 0; sdio = data[i]; #5 sclk = 1; #5; end sclk = 0; cs_n = 1; // IO_UPDATE 脉冲,至少 1 个 SCLK 周期宽度 io_update = 1; #20 io_update = 0; end endtask这段代码里#5是半周期延时,对应 10MHz 的 SCLK。len参数让同一个任务能写不同位宽的寄存器,避免为每个寄存器写一遍。IO_UPDATE 的宽度我给了 20ns,实际手册要求最小 1 个系统时钟周期,保守一点没坏处。注意 SDIO 在 SCLK 上升沿必须稳定,所以先设数据再拉高 SCLK,这个顺序反了就是经典翻车点。
2.3 上电初始化序列与 IO_UPDATE 的配合
AD9954 上电后不能直接写 FTW0 就完事,必须先做一轮初始化。典型顺序是:先给 CFR2 写 DAC 满量程和扫频步进,再写 CFR1 使能所需模式,然后写 FTW0 和 POW0,最后拉 IO_UPDATE。如果用到扫频,还要配 NLSCW、PLSCW、RDW、FDW 这四个寄存器。初始化里最容易漏的是 CFR1 的 bit1(LSB 第一位)——这个位控制是否自动清除相位累加器,单频输出时一般置 0 保持相位连续,扫频时按需设置。
IO_UPDATE 不是每次写寄存器都要给,可以攒几个寄存器一起更新。但 FTW0 和 POW0 这种直接影响输出的,写完最好立刻给脉冲。我一般会在初始化完成后统一给一次 IO_UPDATE,之后改频率再单独给。这里有个细节:IO_UPDATE 拉高期间 SDIO 和 SCLK 要保持空闲,否则可能被误采样。
3. 从零搭一个 AD9954 驱动模块:端口、参数与例化
3.1 模块端口定义与时钟域处理
驱动模块的端口分三组:系统侧是 clk、rst_n、频率/相位/幅度输入;SPI 侧是 sclk、sdio、cs_n、io_update;状态侧可以加一个 ready 或 busy 指示。系统时钟和 SPI 时钟可以是同一个,也可以分频。如果系统时钟是 50MHz,SPI 想跑 10MHz,就做一个 5 分频的使能信号来驱动状态机,而不是直接分频出新的时钟——FPGA 里少用衍生时钟,用时钟使能更稳。
module ad9954_driver ( input wire clk, // 系统时钟,如 50MHz input wire rst_n, input wire start, // 启动一次配置更新 input wire [31:0] ftw, // 频率控制字 input wire [13:0] pow, // 相位偏移 input wire [15:0] asf, // 幅度比例 output reg sclk, output reg sdio, output reg cs_n, output reg io_update, output reg ready ); // 时钟使能:每 5 个 clk 产生一个 spi_en reg [2:0] div_cnt; wire spi_en = (div_cnt == 3'd4); always @(posedge clk or negedge rst_n) begin if (!rst_n) div_cnt <= 0; else if (div_cnt == 3'd4) div_cnt <= 0; else div_cnt <= div_cnt + 1; end // 状态机与任务调用略,核心是 spi_en 有效时才推进 SCLK endmodulediv_cnt从 0 数到 4,每 5 个系统时钟周期产生一个spi_en,SPI 状态机只在spi_en有效时翻转 SCLK,等效 10MHz。这样整个设计只有一个时钟域,时序约束简单,不会出现跨时钟域的亚稳态。ready信号在初始化完成后拉高,外部逻辑看到 ready 再发 start 更新频率。
3.2 参数递增与扫频模式的实现思路
扫频是 AD9954 的强项,靠的是内部自动步进,不需要 FPGA 反复写 FTW0。配置扫频要设四个寄存器:NLSCW 是下限频率字,PLSCW 是上限频率字,RDW 是上升步长,FDW 是下降步长。扫频方向由 CFR1 的 bit 控制,扫频使能也在 CFR1。上升一步的时间 = RDW × 系统时钟周期,比如 RDW = 1000,系统时钟 400MHz,一步就是 2.5μs。
如果不想用内部扫频,也可以在 FPGA 里做参数递增,每隔固定时间给 FTW0 加一个步进值。这种做法灵活,能实现非线性扫频,但要注意每次写 FTW0 后都要给 IO_UPDATE,更新率受 SPI 速度限制。10MHz 的 SCLK 写一次 32 位 FTW0 大概 4μs,加上 IO_UPDATE,更新率上限约 200kHz。内部扫频能到几 MHz 的步进率,这是硬件扫频的优势。
3.3 顶层例化与引脚约束要点
顶层例化时把驱动模块和你的控制逻辑连起来,注意 SDIO 如果是双向引脚,在只写模式下可以约束成输出,省掉三态控制。引脚约束里 SCLK 和 SDIO 尽量走同一 bank,长度匹配不用太苛刻,10MHz 下问题不大。IO_UPDATE 建议用一个独立的普通 IO,不要和 SPI 信号共用。CS_N 如果板上只有一个 AD9954,可以直接接地常使能,但保留控制更规范。
ad9954_driver u_ad9954 ( .clk (sys_clk), .rst_n (rst_n), .start (cfg_start), .ftw (32'h06666666), // 10MHz @ 400MHz 系统时钟 .pow (14'd0), .asf (16'h3FFF), .sclk (ad_sclk), .sdio (ad_sdio), .cs_n (ad_cs_n), .io_update (ad_io_update), .ready (ad_ready) );例化时参数直接写死成常量没问题,但更推荐用 parameter 或 localparam 定义,方便换频率时只改一处。ftw的 32 位常量在 Verilog 里要写全,32'h06666666不能省成'h6666666,位宽不匹配会隐式扩展出意外值。
4. 避坑与排查:AD9954 驱动调试中最容易翻车的 5 个点
4.1 现象:输出频率是设定值的一半或两倍
原因通常是 FTW 计算时系统时钟频率搞错了。AD9954 的系统时钟可以是外部晶振直接输入,也可以是内部 PLL 倍频后的结果。如果板上晶振是 25MHz,PLL 倍频到 400MHz,但代码里按 25MHz 算 FTW,输出就会差 16 倍。先确认 REFCLK 的实际频率和 CFR2 里的 PLL 倍频系数,再代公式。另一个可能是 FTW 字节序发反了,AD9954 是高位先发,如果状态机从低位开始发,频率就乱了。
4.2 现象:SPI 写完后输出没变化
先量 IO_UPDATE 有没有脉冲。很多人写完寄存器忘了给 IO_UPDATE,数据停在缓冲里没进内部寄存器。用示波器抓 IO_UPDATE 引脚,确认写操作结束后有高脉冲。如果 IO_UPDATE 有脉冲但输出还是不变,检查 CS_N 是否在写完后正确拉高,CS_N 一直拉低可能导致后续写操作被忽略。还有一种情况是 CFR1 的 bit1 设成了自动清除相位,每次更新相位归零,波形看起来像没输出。
4.3 现象:输出波形有杂散或幅度不对
幅度不对先查 ASF 寄存器,16 位里有效位和 DAC 满量程设置有关。CFR2 里的 DAC 满量程电流如果设得太小,输出幅度就上不去。杂散问题多半是 SCLK 太快或者 SDIO 在 SCLK 上升沿不稳定。把 SCLK 降到 5MHz 试试,如果杂散消失就是时序余量不够。另外 AD9954 的 DAC 输出是电流型,外部 I/V 转换电路的运放带宽和反馈电阻也会影响波形质量,这部分是模拟侧的事,但驱动调试时容易被误判成数字问题。
4.4 现象:扫频时频率跳变不连续
扫频模式下频率跳变,先检查 NLSCW 和 PLSCW 是否设反了,下限大于上限会导致扫频行为异常。RDW 和 FDW 如果设成 0,步进为 0,频率不动。扫频步进时间 = RDW × 系统时钟周期,RDW 太小步进太快,DAC 输出可能跟不上。还有 CFR1 里的扫频使能位和扫频方向位要配对设置,只使能没设方向,扫频不会启动。
4.5 现象:上电后芯片发热或电流异常
AD9954 正常工作时电流在几百 mA 量级,如果明显发热,先断电量 DAC 输出端的 I/V 电路有没有短路。数字侧常见原因是 SDIO 和 SCLK 配置成了推挽输出但外部有强上拉或下拉,导致静态电流过大。还有一种情况是 CFR2 里 DAC 满量程设到了最大值,而外部 I/V 电阻又小,DAC 输出电流过大。上电初始化时先把 DAC 满量程设到中间值,确认波形正常再往上调。
5. 进阶技巧:用 Icarus Verilog 做 AD9954 驱动的仿真验证
写完驱动直接上板调试,出了问题只能靠示波器猜,效率低。我一般先用 Icarus Verilog 跑一遍仿真,把 SPI 波形和寄存器值都打出来,确认时序对了再烧板子。Icarus Verilog 是开源工具,装好之后用iverilog编译,vvp跑仿真,配合 GTKWave 看波形,整套流程不依赖商业工具。
先写一个 testbench,模拟 AD9954 的 SPI 从机行为,把收到的地址和数据存下来:
`timescale 1ns/1ps module tb_ad9954; reg clk = 0; reg rst_n = 0; reg start = 0; wire sclk, sdio, cs_n, io_update, ready; // 例化驱动 ad9954_driver u_dut ( .clk(clk), .rst_n(rst_n), .start(start), .ftw(32'h06666666), .pow(14'd0), .asf(16'h3FFF), .sclk(sclk), .sdio(sdio), .cs_n(cs_n), .io_update(io_update), .ready(ready) ); // 50MHz 时钟 always #10 clk = ~clk; // 模拟 AD9954 接收:在 sclk 上升沿采样 sdio reg [7:0] recv_addr; reg [31:0] recv_data; integer bit_cnt; always @(posedge sclk) begin if (!cs_n) begin if (bit_cnt < 8) recv_addr <= {recv_addr[6:0], sdio}; else recv_data <= {recv_data[30:0], sdio}; bit_cnt <= bit_cnt + 1; end end initial begin $dumpfile("ad9954.vcd"); $dumpvars(0, tb_ad9954); #100 rst_n = 1; #100 start = 1; #20 start = 0; #5000 $finish; end endmodulebit_cnt数到 8 之前收的是地址,之后收的是数据。跑完仿真在 GTKWave 里看recv_addr和recv_data,确认地址是 0x04、数据是 0x06666666,就说明 SPI 时序和字节序都对了。$dumpfile和$dumpvars把波形存成 VCD,GTKWave 直接打开。这个 testbench 不模拟 AD9954 内部行为,只验证驱动发出来的波形对不对,对调试 SPI 时序足够用。
仿真通过后再上板,先用示波器量 SCLK 频率是不是 10MHz,再量 IO_UPDATE 有没有脉冲,最后看输出波形。这套流程走下来,大部分驱动问题在仿真阶段就能暴露,省掉反复烧板子的时间。我自己的习惯是每改一次状态机就跑一遍仿真,波形存下来和上一版对比,确认没有意外变化再上板。AD9954 这颗芯片不算复杂,但寄存器多、时序要求细,仿真验证是性价比最高的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取