☰
用Verilog配置AD9516:从寄存器表到SPI时序的完整实战指南
2026/10/28 15:59:14 网站建设 项目流程

简介:这是一份面向FPGA开发者的AD9516时钟芯片驱动示例代码,基于Verilog HDL实现,重点演示如何通过SPI接口完成芯片配置。资源适用于通信、数据中心等对时钟精度要求较高的项目,也适合正在学习FPGA外设驱动开发的工程师参考。包内共409个文件,压缩后约21.4MB,主要包含Verilog/VHDL源码、Xilinx工程文件(xpr/xdc/bit)、仿真脚本(do/wcfg)以及大量txt/rpt等辅助文档,便于直接加载工程或按模块查阅。目前已有1011人学习,代码结构完整,包含顶层模块、SPI主控制器、寄存器配置模块等关键部分,可直接复用并依据项目调整时钟频率、相位偏移等参数,能有效缩短驱动开发周期。

1. 用 Verilog 把 AD9516 从黑匣子变成可控时钟源:案例代码的读法

新板卡第一次上电,FPGA 逻辑跑起来了,示波器却看不到 ADC 的采样时钟,这种情况十有八九不是晶振没起振,而是板上的 AD9516 时钟芯片根本没被初始化。AD9516 这颗芯片在高速数据采集、多通道同步、DDR 接口的板卡上太常见了,它内部有 PLL、VCO、分频器和输出驱动器,但所有配置都藏在寄存器里,上电默认状态往往不是你要的频率。很多工程师习惯用 SPI 脚本或者 ADI 的评估软件先把寄存器值算好,再移植到工程里;但到了 FPGA 平台上,没有 CPU 帮你跑脚本,只能用 Verilog HDL 自己写一个配置逻辑,在复位释放后按时序把一串寄存器值写进去。这份 AD9516-cfg-demo 案例代码,解决的就是“FPGA 上电后谁来配 AD9516、怎么配、怎么确认配成功”这个落地问题,适合正在做数据采集或时钟分配、不想依赖外部 MCU 的工程师。

2. AD9516 配置到底配什么:寄存器模型与操作时序

2.1 为什么 AD9516 需要“编程”而不是上电就能用

AD9516 不是简单的时钟 buffer,它内部有一条完整的时钟链路:外部参考时钟进入 PLL 鉴相器,经过电荷泵和 VCO(或者外部 VCO),再经过多级分频器,最终从 OUT0 到 OUT7 输出多路时钟。每一级的分频比、输出电平标准(LVDS/LVPECL/CMOS)、延迟粗调细调、上电顺序,都是靠寄存器位来控制的。上电后芯片有一个默认寄存器状态,但默认状态通常只是“能输出一个参考频率”,不是你的 ADC 需要的 125 MHz 或者 FPGA 需要的 62.5 MHz。更关键的是,AD9516 的 VCO 频率范围、电荷泵电流、环路滤波器参数必须匹配你板上的实际电路,这些没有一组现成的默认值能套用,必须根据你的参考时钟频率和目标输出频率现算现配。

配置 AD9516 就是把“频率规划表”翻译成寄存器值,再通过芯片的串行接口(SPI 或 I2C)写进去。AD9516 的寄存器数量在 0x000 到 0x1FF 这个量级,其中大部分是初始化和分频器配置,还有一部分是更新寄存器(0x232 附近是软复位和更新时序相关控制)。实际用下来,真正要写的关键寄存器大约几十个,其余保持默认就行。这也是读案例代码时的第一个重点:不要试图看懂每一个寄存器,要抓住“哪些寄存器决定输出频率、哪些决定电平标准、哪些是更新使能”。

2.2 AD9516 的 SPI 写时序与寄存器更新机制

AD9516 的 SPI 接口是 4 线制:SCLK、SDIO、CSB、SDO(回读时用,也可以把 SDIO 配成双向)。协议本身不复杂,但有两个细节容易翻车。第一个是默认的指令字节格式:bit7 表示读写方向,bit6 表示是单字节还是多字节连续读写,bit5 到 bit0 是寄存器地址高 6 位。第二个是寄存器地址本身超过 6 位,所以地址低 8 位在下一个字节里给出。也就是说,一次写操作至少要发两个字节的地址再加一个字节的数据。好多第一次写的工程师只发了单字节地址,发现寄存器写不进去,查半天手册才发现 AD9516 的指令字是两段式。

AD9516 SPI 操作指令字节地址字节数据字节
单字节写(读标志 0)bit7=0,bit6=0,addr[13:8]addr[7:0]data[7:0]
多字节写bit7=0,bit6=1,addr[13:8]addr[7:0]data[0],data[1]…
单字节回读bit7=1,bit6=0,addr[13:8]addr[7:0]输出 data

另外一个关键机制在寄存器 0x232 的 Bit0,叫作“更新所有活动的寄存器”。AD9516 内部有很多寄存器是缓冲型的,你写进去的值不会立刻生效,要等这个更新位产生一个上升沿,配置才会一次性同步到内部。案例代码里一般会把它设计成一个独立的写操作,放在所有配置结束之后。漏掉这一步的现象非常典型:回读寄存器值是新的,但输出频率还是默认值,因为配置只在缓冲区内,没有真正切到工作寄存器组。

2.3 案例代码里配置流程的典型顺序

我习惯把 AD9516 的配置流程拆成四段。第一段是复位和初始化:上电后先对 0x000 的软复位位操作,或者直接等 FPGA 复位释放后开始写。第二段是 PLL 相关:参考分频 R、反馈分频 N、电荷泵电流、VCO 频率范围选择。第三段是输出通道配置:每一个 OUT 通道的分频器、延迟、电平标准、是否上电。第四段是更新寄存器触发。这个顺序不能乱,比如先更新再写分频器,输出可能瞬间出现错误的频率尖峰,对后级 ADC 的输入保护电路不友好。

AD9516-cfg-demo 这类案例代码的核心价值就是把上面这个流程固化成了 Verilog 状态机和一张寄存器数据表。你拿到代码后,第一件事不是看代码风格,而是找到那张寄存器表,把它和你的频率规划对照一遍,确认每一行都和你的设计目标匹配。

3. 从案例代码看懂 Verilog 驱动架构:模块划分与状态机

3.1 模块划分:table、控制状态机、SPI 时序发生器

一个好的 AD9516 配置代码,至少应该拆成三个模块,而不是把所有逻辑揉在一个 always 块里。第一个模块是寄存器配置表,本质上是一个 ROM 或者一组参数,用 Verilog 的 localparam 定义二维数组,每个元素包含地址和数据。第二个模块是配置控制器,一个状态机,负责按顺序从表里取数据、发起写操作、等待写完成、计数。第三个模块是 SPI 时序发生器,负责把“写某地址某数据”这个请求转成 SCLK、SDIO、CSB 的物理时序。三个模块之间用简单的握手信号连接:控制器给时序发生器一个 start 脉冲和地址数据,时序发生器写完回一个 done。

为什么这样拆?因为后面你要改频率时,只需改配置表,完全不用碰时序逻辑。大多数案例代码也遵循这个结构,但有的会把配置表和控制器合并,这在一次性配置的场景里问题不大,可一旦你要做动态切换频率,或者调试时反复改寄存器,合并结构的维护成本就上来了。我自己在做类似的接口时,宁可多写几十行,也要把表独立出来。

// reg_cfg_table.v —— 寄存器配置表模块 module reg_cfg_table #( parameter integer CFG_DEPTH = 64 // 配置条目数 )( input wire clk, input wire [7:0] cfg_addr_rd, // 读取地址(条目索引) output reg [15:0] cfg_data // {addr[7:0], data[7:0]} 可扩展成16位 ); // 用二维数组保存配置条目,每个条目低8位是寄存器地址,高8位是数据 reg [15:0] cfg_mem [0:CFG_DEPTH-1]; always @(posedge clk) begin cfg_data <= cfg_mem[cfg_addr_rd]; end // 初始化块:按顺序列出关键寄存器配置 initial begin cfg_mem[0] = 16'h0000; // 寄存器0x00 = 0x10(软复位使能) cfg_mem[1] = 16'h0110; // 由芯片配置决定,示例值 // ... 省略中间条目 ... // 最后一条:更新寄存器 0x232 = 0x01 cfg_mem[CFG_DEPTH-1] = 16'h2321; end endmodule

这段代码的要点在于“条目寻址”和“初始化块”。配置控制器只需要维护一个计数器作为 cfg_addr_rd,每完成一次 SPI 写就加一,计数器到 CFG_DEPTH-1 时说明配置流程结束。初始化块里的具体数值请对照你自己的频率规划表替换,不要直接抄示例值。另外,CFG_DEPTH 必须等于你实际条目数,否则状态机会多做一轮空操作。

3.2 配置状态机的四个状态与跳转条件

控制器状态机我一般只设四个状态:IDLE、READ_CFG、SPI_WRITE、WAIT_SETTLE。IDLE 等待外部启动信号(通常是 FPGA 复位释放后的一个脉冲,或者软件触发);READ_CFG 里用当前计数器去查配置表,把地址和数据锁存到输出寄存器;SPI_WRITE 里拉高 start 信号,等待 SPI 模块的 done;WAIT_SETTLE 是给芯片内部逻辑留出稳定时间,特别是 PLL 锁定判断之前,这一步不能省。

// cfg_controller.v —— 配置主状态机 reg [1:0] state; reg [6:0] cfg_cnt; reg wr_start; reg [7:0] reg_addr; reg [7:0] reg_data; wire wr_done; wire [15:0] cfg_entry; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; cfg_cnt <= 0; wr_start <= 0; end else begin case (state) IDLE: begin if (cfg_start) begin state <= READ_CFG; end end READ_CFG: begin reg_addr <= cfg_entry[7:0]; // 取出地址 reg_data <= cfg_entry[15:8]; // 取出数据 state <= SPI_WRITE; end SPI_WRITE: begin wr_start <= 1'b1; if (wr_done) begin wr_start <= 1'b0; cfg_cnt <= cfg_cnt + 1'b1; if (cfg_cnt == CFG_DEPTH - 1) begin state <= WAIT_SETTLE; // 全部写完 end else begin state <= READ_CFG; end end end WAIT_SETTLE: begin // 等待PLL锁定或定时结束,然后输出cfg_finish脉冲 if (settle_timer == SETTLE_CYCLES) begin cfg_finish <= 1'b1; state <= IDLE; end end endcase end end

这里最容易写错的地方是 cfg_cnt 与 WRITE 状态的配合:wr_start 必须只拉高一个周期,否则 SPI 模块会重复触发写操作。我见过不少案例代码用“拉高后等待”的方式,导致一次配置写了三四遍同一寄存器。解决办法是让 SPI 模块在 start 上升沿锁存数据,同时控制器在拉高后的下一个周期立即拉低。另外 cfg_cnt 在 SPI_WRITE 里递增,不要在 READ_CFG 里提前加,否则最后一条配置完成后计数会多一跳。

3.3 SPI 时序发生器的关键参数:SCLK 极性与建立保持时间

SPI 时序发生器是最贴近物理层的模块。AD9516 的数据手册会给出 SCLK 最高频率和建立保持时间要求。FPGA 的做法通常是用系统时钟分频产生 SCLK,再用移位寄存器把数据一位一位送出去。常见问题是:SCLK 空闲电平极性、数据在哪个沿变化。AD9516 支持 SPI Mode 0(CPOL=0,数据在上升沿采样)和 Mode 1、2,但不同型号的 SPI 模式定义有差异,一定要以数据手册时序图为准,不能想当然用 CPU 的 SPI 主机默认配置。

// spi_writer.v —— 单字节 SPI 写时序 module spi_writer ( input wire clk, // 系统时钟,如 50MHz input wire rst_n, input wire start, // 单周期脉冲 input wire [13:0] addr, // 14位寄存器地址 input wire [7:0] data, // 8位写数据 output wire sclk, output wire csb, output wire sdi, output reg done ); localparam IDLE=1'b0, SHIFT=1'b1; reg [4:0] bit_cnt; // 22位:2字节指令+地址+数据 reg [21:0] shift_reg; reg state; reg sclk_r; assign csb = (state == IDLE) ? 1'b1 : 1'b0; assign sdi = shift_reg[21]; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; done <= 1'b0; end else if (start && state == IDLE) begin state <= SHIFT; done <= 1'b0; shift_reg <= {1'b0, 1'b0, addr[13:8], addr[7:0], data}; // 指令字节+地址+数据 bit_cnt <= 0; end else if (state == SHIFT) begin // 时钟分频与移位逻辑省略,核心是每个SCLK沿移出一位 // 发送完22位后回到IDLE并拉高done end end endmodule

SCLK 的频率选择:AD9516 的 SPI 最高支持 25 MHz 到 30 MHz,但 FPGA 里配置代码跑在 50 MHz 或 100 MHz 系统时钟下,最简单的做法是直接把系统时钟当作串行时钟,把 SCK 设计成 clk 的占空比非 50% 的脉冲,或者用计数器分频成 10 MHz 左右。分频系数不用太纠结,配置只有几百个字节,慢一点也就多几毫秒,但对 PCB 走线和信号完整性更友好。真正要关心的是 CSB 低电平期间 SCLK 的边沿个数是否严格等于 22(指令 8 位 + 地址 8 位 + 数据 8 位),多一个少一个都会导致最后一位移位错位。

4. 把寄存器表从 ADI 评估软件导成 Verilog:计算方法与实用脚本

4.1 频率规划先算后写:参考分频、反馈分频与 VCO 频率约束

AD9516 的 PLL 配置不是随意写分频值的。它的鉴相频率由参考时钟除以 R 分频得到,反馈分频 N 决定 VCO 频率与鉴相频率的比例关系。VCO 频率必须落在芯片规定的范围内(不同型号有差异,比如 1.5 GHz 到 3 GHz 这个量级),输出频率则是 VCO 分别除以各通道分频器的结果。所以在写代码前,先在 Excel 或者 ADI 的评估软件里验证一组分频组合:参考时钟 10 MHz,想要 125 MHz 输出,那么 VCO 选 2.5 GHz,N 分频就是 250,输出分频选 20,这是一个最简单的正向推算。

# 用 Python 脚本辅助计算分频组合,避免手算翻车 ref = 10e6 # 参考时钟 10 MHz vco_freq = 2.5e9 # VCO 目标频率,需在芯片范围内 r_div = 1 # 参考分频 n_div = int(vco_freq / (ref / r_div)) # 反馈分频 out_freq = 125e6 # 期望输出频率 out_div = int(vco_freq / out_freq) # 输出分频器 print(f"R分频={r_div}, N分频={n_div}, 输出分频={out_div}") print(f"鉴相频率={ref/r_div/1e6:.2f} MHz, VCO={r_div*n_div*ref/1e9:.3f} GHz")

这个脚本输出的分频组合可以用来交叉验证寄存器表里的值。N 分频在 AD9516 里不是直接写十进制的,而是拆成 N 高字节和 N 低字节,不同位宽的分频器对应不同的寄存器地址,案例代码里通常会把这些计算好的结果以注释形式附在表旁边。拿到新板子时,我建议先用 ADI 的官方评估软件跑一遍同样的参数,把生成的寄存器文件存底,再和 demo 代码里的表逐项核对,能发现相当多笔误。

4.2 把寄存器表翻译成 Verilog 数组的两种做法

第一种做法是把寄存器表展开成上面第 3 章那样的二维数组,每行{地址, 数据}。优点是直观,缺点是表很长时行数多、容易看花眼。第二种做法是只在代码里写“必须修改的寄存器”,其余靠芯片默认值。案例代码一般会选第一种,因为它是演示性质,希望你看清每一个被配置的位。第二种做法更适合量产,因为可以显著缩短配置时间,也不容易被默认值漂移影响。这两种做法在 0x232 更新寄存器上的处理是一样的:都必须放在最后。

不管是哪种做法,都要注意 AD9516 有些寄存器同时包含多个功能位,修改其中一位不能简单覆盖整个字节。例如一个寄存器里既有一位控制输出极性,又有一位控制分频器复位模式,如果你只是想把极性翻转,直接对整个字节写一个新值,可能把另一位改坏。案例代码中大量出现8'hXX这样的整字节写入,这在寄存器位不冲突时没问题;一旦发现输出行为不对,排查方向之一就是“有没有不必要的位被覆盖”。

4.3 配置时长估算:为什么 PLL 锁定等待不能省

写完最后一个寄存器并触发更新后,AD9516 内部的 PLL 需要一段时间来锁定。锁定时间取决于环路滤波器带宽和 VCO 频率跳变幅度,短则几百微秒,长则几毫秒。案例代码里的 WAIT_SETTLE 状态就是在兜这件事。FPGA 侧不能天真地在配置完立刻认为时钟有效,也不能只按固定延时等待。更稳妥的做法是:用 AD9516 的锁定检测输出引脚(Locked)作为 FPGA 输入,等它拉高再往下走流程;如果板子上没有引出这个引脚,再用定时器等待。

// 等待PLL锁定的两种方式,二选一 // 方式1:读锁定引脚(更可靠) always @(posedge clk) begin if (cfg_finish && !pll_locked) begin status_led <= 1'b1; // 指示未锁定 end end // 方式2:定时等待,常见做法是计数 100_000 个系统时钟周期 localparam SETTLE_CYCLES = 100_000; reg [16:0] settle_timer; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin settle_timer <= 0; end else if (state == WAIT_SETTLE) begin if (settle_timer < SETTLE_CYCLES) settle_timer <= settle_timer + 1'b1; end else begin settle_timer <= 0; end end

这里有一个测试时的技巧:如果你用 PLL 锁定引脚来判断,仿真的时候记得在 testbench 里人为拉高 pll_locked,否则仿真会卡在 WAIT_SETTLE 出不来。如果只用定时器,定时长度要给足,尤其第一次上电调试时,可以在代码里留一个调试参数,把 SETTLE_CYCLES 改成很大的值,配合示波器观察实际锁定时间,再回填一个合理值,而不是一开始就拍脑袋定 100000。

5. 避坑与排查:AD9516 配置最常见的 5 个问题

5.1 现象一:所有寄存器都写了,输出频率还是默认值

原因:漏掉了 0x232 的更新操作,或者更新位没有产生有效的上升沿。AD9516 的很多寄存器是双缓冲的,写入后停留在缓冲区,必须通过写更新寄存器触发一次内部同步。案例代码里如果在表格末尾有 0x232 = 0x01 这一行,但在状态机里因为最后一条提前跳出循环,没发出去,也会出现这个现象。

解决:在配置流程中单独检查最后一条是否发送完成。如果确认表格有这一条,就查状态机计数器的结束条件,确保更新寄存器那一行确实被执行。另一个验证技巧:回读 0x232 的值为 0x01 是没用的,因为写操作已经把值写进去了,真正要做的是回读一个受控的寄存器,比如某个分频器寄存器的值,确认和写入一致。

5.2 现象二:第一次上电能出时钟,复位后再配置就失败

原因:FPGA 复位后重新配置 AD9516,但芯片可能还停留在上一次配置的工作状态中,没有先做软复位。AD9516 的软复位位在 0x000 寄存器,配置流程开头必须先把这一位置 1 再清 0(或者按手册写入复位命令),把芯片恢复到上电默认态,然后再开始配置。缺少软复位时,寄存器表里那些“保持默认值”的位不会回到默认,而是保留上一次的值。

解决:把软复位写进表格的前两个条目,或者用状态机在 READ_CFG 之前单独执行一次复位写入。注意软复位后要等一小段恢复时间,不能紧跟着写其他寄存器,一般加 10 到 100 个系统时钟周期即可。

5.3 现象三:SPI 写操作波形异常,CSB 拉低后 SCLK 就乱了

原因:SCLK 的产生方式不正确。在 FPGA 里用计数器分频产生 SCLK 时,如果分频后的 SCLK 和 CSB 的拉低时序不在同一个时钟域里同步,会出现 CSB 刚拉低、SCLK 的第一个边沿已经过去,导致时序错位。另外,如果 SCLK 用门控时钟(由组合逻辑生成)驱动 SDI,在 FPGA 布线后容易出现毛刺。

解决:SCLK 最好由 always 块在系统时钟上升沿翻转得到,不要用组合逻辑门控;CSB 也由寄存器输出,并保证它和 SCLK 的相对相位满足手册要求。动手改之前先在示波器上测量一下 SCLK 空闲电平是否和代码里设置的 CPOL 一致。

5.4 现象四:回读的值有时正确有时错误,还有时候读到 0xFF

原因:使用了半双工 SDIO 却没有正确切换方向。AD9516 回读时,SDIO 在指令和地址阶段是输入,数据阶段要切换成输出。如果 FPGA 侧没有三态缓冲,或者切换时机晚了一个周期,读到的就会是总线冲突后的未知值。案例代码如果只实现了写,那你做回读验证时就得自己补上这一块。

解决:回读代码里用高阻态控制 SDIO,在发送完地址字节后的下一个系统时钟周期把 SDIO 设为输入。推荐一个更省事的做法:如果 PCB 上有 SDO 引脚和 SDIO 同时引出,把 SDO 作为独立回读引脚,这样数据方向切换就没有了冲突危险。回读只在调试阶段用,量产的配置可以不接 SDO。

5.5 现象五:配置逻辑移植到新板卡后,某些输出通道无时钟

原因:寄存器表里的输出通道配置是按原板卡的 PCB 拓扑写的,新板卡把 OUT2 接到了 ADC,但代码里 OUT2 的寄存器值仍写着“掉电”。AD9516 的输出通道默认状态是什么、哪些通道上电、哪些掉电,直接决定你的 ADC 能否工作。

解决:拿到案例代码后,逐个核对输出通道对应的寄存器条目,重点是“输出上电/掉电控制”和“输出电平标准”这两类寄存器。最好在表格里给每条配置加注释,标明“这是 OUT0、LVDS、上电”,这样换板卡时只改注释对应的行即可,也方便 code review 的人检查。

6. 进阶:加一个非阻塞回读状态机,让配置结果可验证

配置代码写完、时钟也能出来了,但过两个月你自己都怀疑寄存器表有没有被改坏。我的习惯是在 demo 代码的基础上做一个“读回再校验”的小状态机:配置结束后,把所有关键寄存器的值读回来,和期望值逐个比对,任何一个不匹配就在调试引脚上输出一个错误脉冲。这个逻辑用不了多少资源,但能把“时钟没输出”从一个黑匣子问题变成一个可定位的寄存器级问题。回读状态机也可以用回读的数据做动态校准:比如通过 I2C 或 UART 把回读值送到上位机,结合 ADI 评估软件导出的寄存器文件做一次全量比对。这样既验证了 Verilog 配置代码的正确性,也意外地给产线测试提供了一个快速筛选手段。代码层面要注意:回读和写操作共用 SPI 时序发生器时,状态机要增加“读指令”分支;回读的 SDI 数据要在最后一个下降沿之后采样,否则会采到未稳定的旧值。遇到时钟输出不稳定时,不要急着改代码,先在频谱仪上确认输出频率是否精确,再怀疑配置问题;如果是频率偏差大,多半是参考分频或反馈分频算错,而不是输出通道的问题。这个回读校验的思路我也用在了其他时钟芯片的驱动上,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询