news 2026/9/28 23:38:40

AD9910 DDS芯片SPI配置与FPGA Verilog实现全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AD9910 DDS芯片SPI配置与FPGA Verilog实现全解析

AD9910 这颗芯片,但凡做过信号源、雷达回波模拟或者任意波形发生器的朋友应该都不陌生。它是一颗 14 位、1GSPS 采样率的直接数字频率合成器(DDS),内部集成了 32 位频率调谐字、14 位相位偏移和 14 位幅度控制,能输出从直流到 400MHz 以上的正弦波,而且跳频速度极快。但很多人第一次拿到它的时候,面对那 100 多页的数据手册和一堆寄存器,往往不知道从哪里下手。官方倒是提供了一个评估软件,可以图形化配置寄存器并导出配置文件,但问题在于:这个软件导出的东西怎么变成 FPGA 里能用的 Verilog 代码?SPI 时序到底该怎么对齐?寄存器写入顺序有没有讲究?

我前后用 AD9910 做过三个项目,从最开始的照抄官方配置、到后来自己写状态机逐条写寄存器,中间踩过的坑包括但不限于:SPI 时钟极性搞反导致寄存器写不进去、忘记等待 IO_UPDATE 导致配置不生效、profile 寄存器地址算错导致输出频率偏差一倍、以及最经典的——读回校验时发现读出来的数据和写进去的完全对不上。这些问题在数据手册里其实都有答案,但分散在各个章节,而且有些细节手册写得比较含蓄,不实际调一次很难理解。

这篇文章我会从官方软件的使用讲起,然后一步步拆解 AD9910 的 SPI 协议细节,最后给出一个可以直接在 FPGA 上跑的 Verilog 实现方案。不管你是刚接触 DDS 的新手,还是已经用过 AD9910 但想搞清楚底层细节的老手,应该都能从中找到有用的东西。文章里涉及的所有代码和配置参数都经过实际硬件验证,你可以直接拿去用,也可以根据自己的需求修改。

1. 官方软件到底帮我们做了什么

1.1 评估软件的核心功能与使用流程

ADI 官方为 AD9910 提供了一个评估板配套的图形化软件,通常叫 AD9910 Evaluation Software。这个软件的功能其实比很多人以为的要强大,它不仅仅是让你点一点看波形,更重要的是它可以帮你生成寄存器配置文件。整个使用流程大致是这样的:先连接评估板,通过 USB 接口与 PC 通信,然后在软件界面上设置各个寄存器的值,比如系统时钟、PLL 倍频系数、输出频率、幅度、相位等,软件会实时计算对应的寄存器数值并显示出来。设置完成之后,你可以通过菜单导出寄存器配置,格式通常是文本或者二进制文件。

这个导出的配置文件非常关键,因为它包含了所有寄存器的地址和对应的数值。很多人拿到这个文件之后就直接想办法把它塞进 FPGA 里,但这里有个问题:导出的文件格式和 FPGA 里实际需要的 SPI 写入时序之间还有一道鸿沟。软件导出的是“寄存器地址+数据”的列表,而 FPGA 需要的是按照 SPI 协议逐位发送的时序。所以你需要理解这个对应关系,才能把配置文件转换成 Verilog 代码。

另外,评估软件还有一个很实用的功能:它可以实时读写寄存器。你可以通过软件界面直接修改某个寄存器的值,然后点击写入,观察芯片输出的变化。这个功能在调试阶段非常有用,比如你不确定某个寄存器的某一位是干什么的,可以直接改一下看输出有什么变化。但要注意,评估软件通过 USB 转 SPI 的方式与芯片通信,它的时序和 FPGA 直接驱动 SPI 是有区别的,尤其是时钟频率和片选信号的处理方式。

1.2 从软件配置到寄存器映射的转换逻辑

评估软件界面上你设置的是物理量,比如“输出频率 100MHz”,但芯片内部实际存储的是频率调谐字(FTW)。这两者之间的转换公式在数据手册里有明确给出:FTW = round(f_out × 2^32 / f_sysclk)。其中 f_sysclk 是系统时钟频率,也就是经过 PLL 倍频之后的采样时钟。举个例子,如果你的参考时钟是 25MHz,PLL 倍频系数设为 40,那么 f_sysclk = 1GHz。要输出 100MHz,FTW = 100e6 × 2^32 / 1e9 = 429496729.6,取整就是 429496730,转换成十六进制是 0x1999999A。

评估软件会自动帮你算这个值,但你在写 Verilog 的时候需要自己算,或者提前算好放在一个查找表里。这里有个容易出错的地方:2^32 是 4294967296,不是 4294967295,差一个数看起来很小,但在高频输出时会导致频率偏差。比如你要输出 400MHz,FTW 大约是 1717986918.4,取整误差带来的频率偏差大约是 0.23Hz,看起来不大,但如果你做的是相干系统,这个偏差会累积。

除了频率调谐字,还有相位偏移字(POW)和幅度比例因子(ASF)。POW 是 16 位的,计算公式是 POW = round(phase_deg × 2^16 / 360)。ASF 是 14 位的,范围 0 到 16383,对应幅度从 0 到满量程。评估软件里你可以直接输入角度和幅度百分比,它会自动换算。但在 Verilog 里,如果你需要动态改变这些参数,就得自己实现这些换算逻辑,或者用预计算的方式。

还有一点很重要:AD9910 的寄存器分为几个大类,包括控制寄存器(CFR1、CFR2)、频率调谐字寄存器、相位偏移字寄存器、幅度比例因子寄存器,以及 8 个 profile 寄存器。Profile 寄存器的作用是让你可以预存 8 组不同的频率、相位、幅度配置,然后通过外部引脚快速切换。这个功能在跳频或者多波形切换的场景下非常有用。评估软件里可以分别配置这 8 个 profile,但导出的配置文件里会包含所有寄存器的值,你需要自己区分哪些是全局配置,哪些是 profile 相关的。

1.3 评估软件导出文件的格式解析与常见陷阱

评估软件导出的配置文件通常是文本格式,每一行包含一个寄存器地址和对应的数据。比如类似这样的格式:地址 0x00,数据 0x12345678。但不同版本的软件导出格式可能略有不同,有的用十六进制,有的用十进制,有的还会包含注释行。你在解析这个文件的时候,首先要确认格式,然后提取出有效的寄存器地址和数据对。

这里有一个常见的陷阱:评估软件导出的数据可能是小端格式,也可能是大端格式,取决于软件版本和导出选项。AD9910 的 SPI 接口是 MSB first,也就是高位先发。如果你导出的数据是 32 位的,但 SPI 一次只能发 8 位,你就需要把 32 位数据拆成 4 个字节,按照从高到低的顺序依次发送。如果搞反了字节顺序,写进去的值就完全错了。我遇到过好几次,明明按照配置文件写的,但输出频率就是不对,后来发现是字节顺序搞反了。

另一个陷阱是:评估软件里有些寄存器的值是软件自动计算的,比如默认的 PLL 锁定时间、DAC 满量程电流等,这些值在导出文件里可能是一个推荐值,但不一定适合你的硬件设计。比如 DAC 满量程电流,评估板上的外围电路和你的板子可能不一样,如果你直接照搬,可能导致输出幅度不对甚至损坏芯片。所以导出文件里的值只能作为参考,关键参数还是要根据你自己的硬件设计来调整。

还有一个细节:AD9910 的 SPI 接口支持多种配置模式,包括单字节传输、多字节传输、以及带地址自增的流模式。评估软件通常使用的是单字节或者双字节传输模式,也就是每次传输都带上寄存器地址。但在 FPGA 里,为了提高配置速度,你可能会想用流模式,也就是先发一个起始地址,然后连续发送多个数据字节,地址自动递增。这两种模式的 SPI 帧格式是不一样的,你不能直接把评估软件的配置数据套用到流模式上,需要重新组织数据格式。

2. AD9910 SPI 协议的底层细节拆解

2.1 SPI 模式选择与时钟极性的实际影响

AD9910 的 SPI 接口支持 SPI 模式 0 和模式 3,也就是 CPOL=0/CPHA=0 或者 CPOL=1/CPHA=1。这两种模式的区别在于时钟空闲状态和采样边沿。模式 0 下,SCLK 空闲为低电平,数据在 SCLK 的上升沿被采样;模式 3 下,SCLK 空闲为高电平,数据在 SCLK 的下降沿被采样。数据手册里说两种模式都支持,但实际使用中,我强烈建议使用模式 0,因为大多数 FPGA 的 SPI IP 核或者自己写的状态机默认都是模式 0,调试起来更方便。

这里有一个非常关键的细节:AD9910 的 SDIO 引脚是双向的,既可以作为数据输入(MOSI),也可以作为数据输出(MISO),具体方向由读写控制位决定。在写操作时,SDIO 是输入;在读操作时,SDIO 是输出。这意味着你不能像标准的四线 SPI 那样同时有独立的 MOSI 和 MISO,而是需要在一个引脚上切换方向。这个切换时机非常关键:在发送完读写控制位和地址位之后,如果是读操作,需要及时把 SDIO 的方向从输出切换到输入,否则会冲突。

我刚开始做的时候,用的是标准四线 SPI 的思维,结果发现读回的数据全是 0xFF 或者 0x00,后来才意识到 SDIO 是双向的,需要在正确的时间点切换方向。具体来说,AD9910 的 SPI 帧格式是:第一个字节包含读写控制位(R/W)和 7 位地址的高 7 位,第二个字节是地址的低 8 位(实际上 AD9910 的寄存器地址是 8 位的,但帧格式里分了两个字节来传输),然后是数据字节。在写操作时,SDIO 一直保持输出;在读操作时,发送完地址之后,SDIO 需要切换为输入,然后在接下来的时钟周期里读取数据。

还有一个容易忽略的点:AD9910 的 SPI 时钟频率最高可以到 70MHz,但这是在特定条件下的。实际使用中,我建议把 SCLK 控制在 10MHz 到 25MHz 之间,这样既能保证配置速度,又能留出足够的时序余量。尤其是在使用 FPGA 的普通 IO 引脚而不是专用时钟引脚时,过高的频率可能导致信号完整性问题。我在一个项目里把 SCLK 设到了 50MHz,结果发现偶尔会出现配置失败的情况,后来降到 20MHz 就稳定了。

2.2 寄存器地址映射与页切换机制

AD9910 的寄存器地址是 8 位的,但并不是所有地址都有效。数据手册里给出了完整的寄存器映射表,包括 CFR1、CFR2、CFR3、FTW、POW、ASF、以及 8 个 profile 寄存器。每个寄存器的地址和位定义都需要仔细核对。比如 CFR1 的地址是 0x00,CFR2 的地址是 0x01,CFR3 的地址是 0x02,FTW 的地址是 0x07,POW 的地址是 0x08,ASF 的地址是 0x09,而 profile 寄存器的地址从 0x0E 到 0x15,每个 profile 占用一个地址。

这里有一个非常重要的机制:AD9910 的寄存器分为“当前寄存器”和“缓冲寄存器”。你通过 SPI 写入的值首先进入缓冲寄存器,只有在 IO_UPDATE 引脚上产生一个上升沿之后,缓冲寄存器的值才会被更新到当前寄存器,从而影响芯片的输出。这个机制的目的是让你可以一次性更新多个寄存器,然后同时生效,避免在更新过程中出现中间状态。很多人在调试时发现写入了寄存器但输出没变化,就是因为忘记给 IO_UPDATE 信号了。

IO_UPDATE 信号的处理也有讲究。数据手册要求 IO_UPDATE 的高电平持续时间至少一个 SYSCLK 周期,但实际上为了可靠,我通常会保持至少 10 个 SYSCLK 周期。另外,IO_UPDATE 的上升沿应该与 SYSCLK 同步,否则可能出现亚稳态问题。在 FPGA 里,你可以用一个简单的状态机来产生 IO_UPDATE 信号:在 SPI 传输完成后,拉高 IO_UPDATE,保持若干个时钟周期,然后拉低。

还有一个细节:有些寄存器是“只写”的,比如 CFR1 和 CFR2,你无法读回它们的值。而有些寄存器是可读可写的,比如 FTW、POW、ASF 和 profile 寄存器。在读回校验时,你需要注意哪些寄存器可以读,哪些不能读。我一开始不知道这个区别,试图读回 CFR1 的值来校验,结果读出来全是 0,还以为是自己写错了,后来查手册才发现 CFR1 是只写的。

2.3 读写时序的逐位分析与状态机设计思路

AD9910 的 SPI 写时序可以分解为以下几个阶段:首先是片选拉低(CSB 从高变低),然后是在 SCLK 的上升沿(模式 0)逐位发送第一个字节,这个字节的最高位是读写控制位(0 表示写,1 表示读),接下来的 7 位是寄存器地址的高 7 位。然后是第二个字节,也就是地址的低 8 位。注意,AD9910 的寄存器地址实际上是 8 位的,但帧格式里把它分成了两部分:第一个字节的低 7 位是地址的高 7 位,第二个字节是地址的低 8 位?这里需要仔细看手册。

实际上,AD9910 的 SPI 帧格式是这样的:第一个字节包含 R/W 位和 7 位地址,但这个 7 位地址是寄存器地址的高 7 位,而第二个字节是地址的低 8 位?不对,让我重新梳理一下。根据数据手册,AD9910 的串行端口通信周期分为两个阶段:指令阶段和数据阶段。指令阶段发送一个字节,包含 R/W 位和 7 位地址。但这个 7 位地址对应的是寄存器的地址,而 AD9910 的寄存器地址是 8 位的,所以这里有一个矛盾。

实际上,AD9910 的寄存器地址是 8 位的,但指令字节里只包含了 7 位地址,这是因为 AD9910 的寄存器地址范围是 0x00 到 0x1F,只需要 5 位就能表示,但手册里用了 7 位。所以指令字节的格式是:bit7 是 R/W,bit6 到 bit0 是地址。数据阶段则是根据寄存器的宽度发送 1 到 4 个字节的数据。比如 CFR1 是 32 位的,需要发送 4 个字节;FTW 是 32 位的,也是 4 个字节;POW 是 16 位的,2 个字节;ASF 是 14 位的,但通常按 16 位处理,2 个字节。

所以整个 SPI 传输的字节数是:1 个指令字节 + N 个数据字节。对于 32 位寄存器,总共 5 个字节;对于 16 位寄存器,总共 3 个字节。在 Verilog 状态机设计时,你需要根据目标寄存器的宽度来决定发送多少个数据字节。一个比较通用的做法是:用一个计数器来记录当前发送的是第几个字节,当计数器达到预期值时,拉高传输完成信号,然后触发 IO_UPDATE。

读操作的时序稍微复杂一些。在发送完指令字节之后,SDIO 需要从输出切换为输入,然后在接下来的时钟周期里读取数据。这里有一个关键点:AD9910 在读操作时,数据是在 SCLK 的下降沿(模式 0)或者上升沿(模式 3)输出的,你需要根据选择的模式来正确采样。另外,读操作的数据字节数和写操作一样,取决于寄存器的宽度。读回的数据可以用来校验写入是否正确,但要注意只读寄存器和只写寄存器的区别。

3. Verilog 实现:从状态机到可综合代码

3.1 顶层模块接口定义与时钟域处理

在开始写 Verilog 之前,先要明确顶层模块的接口。AD9910 的 SPI 接口需要以下信号:SCLK、SDIO、CSB、IO_UPDATE,以及可选的 RESET。另外,如果你要用到 profile 切换功能,还需要连接 PS0、PS1、PS2 三个引脚。在 FPGA 侧,你需要一个系统时钟(比如 50MHz),以及一个复位信号。顶层模块的输入输出大致如下:输入包括 clk、rst_n、start、addr、data_in、wr_en 等控制信号,输出包括 sclk、sdio、csb、io_update 等。

时钟域的处理是一个容易被忽略但非常重要的问题。AD9910 的 SPI 时钟是由 FPGA 产生的,所以 SCLK 是 FPGA 系统时钟的分频。在写状态机时,你需要确保 SCLK 的边沿与系统时钟同步,避免出现毛刺。一个常见的做法是用一个计数器来分频系统时钟,产生 SCLK。比如系统时钟 50MHz,要产生 10MHz 的 SCLK,就用一个模 5 的计数器,每计数到 2 时翻转 SCLK。这样产生的 SCLK 占空比是 50%,而且与系统时钟同步。

SDIO 的方向控制也需要仔细处理。在写操作时,SDIO 是输出;在读操作时,SDIO 是输入。在 Verilog 里,你可以用一个三态缓冲器来实现:assign sdio = sdio_oe ? sdio_out : 1'bz; 其中 sdio_oe 是方向控制信号,sdio_out 是输出数据。在读操作时,sdio_oe 置 0,SDIO 变为高阻态,此时你可以从 sdio 引脚读取数据。注意,在切换方向的瞬间,要确保没有信号冲突,通常需要在发送完最后一个地址位之后,延迟一个时钟周期再切换方向。

IO_UPDATE 信号的处理也需要注意。在 SPI 传输完成后,你需要产生一个 IO_UPDATE 脉冲。这个脉冲的宽度至少是一个 SYSCLK 周期,但为了可靠,我通常保持 10 个系统时钟周期。IO_UPDATE 的上升沿应该与系统时钟同步,避免亚稳态。在 Verilog 里,你可以用一个简单的状态机来实现:当 SPI 传输完成信号有效时,进入 IO_UPDATE 状态,拉高 io_update,计数到 10 后拉低,然后回到空闲状态。

3.2 写操作状态机的完整实现与关键代码注释

写操作状态机是整个 Verilog 实现的核心。我把它分成几个状态:IDLE、SEND_CMD、SEND_DATA、WAIT、UPDATE。在 IDLE 状态,等待 start 信号。当 start 有效时,锁存地址和数据,进入 SEND_CMD 状态。在 SEND_CMD 状态,发送指令字节,也就是 R/W 位加上 7 位地址。然后进入 SEND_DATA 状态,根据寄存器的宽度发送 1 到 4 个数据字节。发送完成后进入 WAIT 状态,等待几个时钟周期,然后进入 UPDATE 状态,产生 IO_UPDATE 脉冲。

下面是一个简化的写操作状态机代码示例,你可以根据实际需求修改:

module ad9910_spi_wr ( input wire clk, input wire rst_n, input wire start, input wire [7:0] addr, input wire [31:0] data_in, input wire [2:0] byte_num, // 数据字节数:1, 2, 3, 4 output reg sclk, output reg sdio_out, output reg sdio_oe, output reg csb, output reg io_update, output reg done ); // 状态定义 localparam IDLE = 3'd0; localparam SEND_CMD = 3'd1; localparam SEND_DATA = 3'd2; localparam WAIT = 3'd3; localparam UPDATE = 3'd4; reg [2:0] state, next_state; reg [7:0] clk_cnt; reg [4:0] bit_cnt; reg [2:0] byte_cnt; reg [7:0] cmd_byte; reg [31:0] data_reg; reg [7:0] update_cnt; // 时钟分频:产生 SCLK always @(posedge clk or negedge rst_n) begin if (!rst_n) clk_cnt <= 8'd0; else if (state == SEND_CMD || state == SEND_DATA) clk_cnt <= clk_cnt + 1'b1; else clk_cnt <= 8'd0; end // SCLK 生成:每 5 个系统时钟翻转一次 always @(posedge clk or negedge rst_n) begin if (!rst_n) sclk <= 1'b0; else if (state == SEND_CMD || state == SEND_DATA) sclk <= (clk_cnt == 8'd2) ? 1'b1 : (clk_cnt == 8'd4) ? 1'b0 : sclk; else sclk <= 1'b0; end // 状态机主逻辑 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; csb <= 1'b1; sdio_oe <= 1'b0; io_update <= 1'b0; done <= 1'b0; end else begin case (state) IDLE: begin csb <= 1'b1; io_update <= 1'b0; done <= 1'b0; if (start) begin cmd_byte <= {1'b0, addr[6:0]}; // 写操作,R/W=0 data_reg <= data_in; byte_cnt <= byte_num; bit_cnt <= 5'd7; state <= SEND_CMD; csb <= 1'b0; sdio_oe <= 1'b1; end end SEND_CMD: begin if (clk_cnt == 8'd0) begin sdio_out <= cmd_byte[bit_cnt]; if (bit_cnt == 5'd0) begin bit_cnt <= 5'd7; state <= SEND_DATA; end else begin bit_cnt <= bit_cnt - 1'b1; end end end SEND_DATA: begin if (clk_cnt == 8'd0) begin sdio_out <= data_reg[bit_cnt + (byte_cnt-1)*8]; if (bit_cnt == 5'd0) begin if (byte_cnt == 3'd1) begin state <= WAIT; end else begin byte_cnt <= byte_cnt - 1'b1; bit_cnt <= 5'd7; end end else begin bit_cnt <= bit_cnt - 1'b1; end end end WAIT: begin state <= UPDATE; update_cnt <= 8'd0; end UPDATE: begin io_update <= 1'b1; if (update_cnt == 8'd9) begin io_update <= 1'b0; done <= 1'b1; state <= IDLE; end else begin update_cnt <= update_cnt + 1'b1; end end endcase end end endmodule

这段代码的核心思路是用一个计数器来控制 SCLK 的分频,用 bit_cnt 和 byte_cnt 来跟踪当前发送的位和字节。注意在 SEND_DATA 状态,数据是从高位开始发送的,所以用 data_reg[bit_cnt + (byte_cnt-1)*8] 来索引。这个索引方式假设数据是 32 位的,对于 16 位或 8 位的数据,你需要相应地调整。

还有一个细节:在 SEND_CMD 状态,cmd_byte 是 {1'b0, addr[6:0]},也就是写操作的控制位加上 7 位地址。但 AD9910 的寄存器地址是 8 位的,这里只用了 7 位,是因为地址范围是 0x00 到 0x1F,7 位足够表示。如果你要访问的地址超过 0x7F,就需要检查手册确认是否支持。

3.3 读操作状态机的实现与 SDIO 方向切换时机

读操作的状态机和写操作类似,但多了 SDIO 方向切换的步骤。在发送完指令字节之后,需要把 sdio_oe 置 0,让 SDIO 变为输入,然后在接下来的时钟周期里采样 SDIO 上的数据。这里的关键是切换时机:必须在最后一个地址位发送完成之后,SCLK 的下降沿(模式 0)或者上升沿(模式 3)之前切换方向。如果切换太早,会干扰地址位的发送;如果切换太晚,会错过第一个数据位。

下面是一个读操作状态机的关键部分:

// 读操作状态机(简化版) localparam RD_IDLE = 3'd0; localparam RD_SEND_CMD = 3'd1; localparam RD_SWITCH = 3'd2; localparam RD_READ_DATA = 3'd3; localparam RD_DONE = 3'd4; // 在 RD_SEND_CMD 状态发送指令字节,R/W=1 // 发送完成后进入 RD_SWITCH 状态 // 在 RD_SWITCH 状态,拉低 sdio_oe,等待一个时钟周期 // 然后进入 RD_READ_DATA 状态,在 SCLK 的上升沿采样 SDIO

在读操作时,数据是在 SCLK 的下降沿输出的(模式 0),所以你应该在 SCLK 的上升沿采样。在 Verilog 里,你可以用 sclk 的上升沿来触发采样,或者用系统时钟在 sclk 为高时采样。我通常用系统时钟在 sclk 为高时采样,这样更稳定。

读回的数据需要与写入的数据进行比较,以校验配置是否正确。但要注意,有些寄存器是只写的,读回来可能是 0 或者不确定的值。所以在校验时,只对可读的寄存器进行比较。另外,读操作也会受到 IO_UPDATE 的影响,但通常读操作不需要 IO_UPDATE,因为读的是缓冲寄存器的值。

3.4 多寄存器批量配置的顺序与依赖关系

在实际项目中,你通常需要配置多个寄存器,而不是只写一个。比如先配置 CFR1 和 CFR2 来设置芯片的工作模式,然后配置 FTW 来设置输出频率,再配置 ASF 来设置幅度。这些寄存器的配置顺序是有讲究的。一般来说,应该先配置控制寄存器,再配置数据寄存器。因为控制寄存器决定了芯片的工作模式,比如是否使能 PLL、是否使能 DAC、是否使用 profile 模式等。如果先配置数据寄存器,再配置控制寄存器,可能会导致数据寄存器被复位或者覆盖。

另外,有些寄存器之间存在依赖关系。比如 CFR2 里的 PLL 使能位和 PLL 倍频系数,必须在配置其他寄存器之前设置好,因为系统时钟频率会影响 FTW 的计算。如果你先设置了 FTW,然后改变 PLL 倍频系数,那么实际的输出频率就会变。所以正确的顺序是:先配置 CFR2 设置 PLL,等待 PLL 锁定,然后配置 CFR1 设置工作模式,再配置 FTW、POW、ASF 等数据寄存器,最后配置 profile 寄存器(如果需要)。

在 Verilog 实现时,你可以用一个简单的 ROM 或者查找表来存储这些配置数据,然后用一个状态机依次读取并写入。每个寄存器的写入完成后,产生一个 done 信号,触发下一个寄存器的写入。这样就能保证顺序正确。如果你需要动态改变频率,可以在初始化完成后,只更新 FTW 寄存器,然后给 IO_UPDATE,这样就能实现快速跳频。

4. 调试实战:那些手册不会告诉你的坑

4.1 寄存器写入后无输出的排查链路

这是最常见的问题:你按照手册和评估软件的配置,把寄存器都写了一遍,但芯片就是没有输出。排查这个问题,我通常按照以下链路来走。第一步,检查电源和参考时钟。AD9910 需要 1.8V 和 3.3V 供电,参考时钟幅度要足够,通常要求峰峰值在 0.5V 到 1V 之间。如果参考时钟没有或者幅度不够,芯片根本不会工作。第二步,检查 SPI 通信是否正常。用示波器或者逻辑分析仪抓一下 SCLK、SDIO、CSB 的波形,看看有没有信号,时序对不对。如果 CSB 一直是高电平,说明片选没有拉低,SPI 传输根本没有开始。

第三步,检查 IO_UPDATE 信号。这是最容易被忽略的。如果你写入了寄存器但没有给 IO_UPDATE,缓冲寄存器的值不会更新到当前寄存器,芯片输出不会变化。用示波器看一下 IO_UPDATE 引脚有没有脉冲,宽度够不够。第四步,检查寄存器的配置值。特别是 CFR1 和 CFR2,里面有很多使能位,比如 DAC 使能、PLL 使能、输出使能等。如果这些位没有正确设置,芯片也不会有输出。第五步,检查 FTW 的计算是否正确。如果 FTW 算错了,输出频率会不对,但至少应该有输出。如果完全没有输出,更可能是控制寄存器的问题。

我遇到过一次,所有寄存器都写了,IO_UPDATE 也给了,但就是没输出。后来用逻辑分析仪抓 SPI 波形,发现 SCLK 的极性搞反了,数据在错误的边沿被采样,导致写入的值全是错的。改成模式 0 之后,问题解决。所以逻辑分析仪是调试 SPI 的必备工具,没有它很难定位问题。

4.2 读回校验数据不一致的根因分析

读回校验是验证 SPI 通信是否正常的重要手段,但很多人发现读回的数据和写入的不一致。这个问题可能有几个原因。第一个原因是 SDIO 方向切换时机不对。如果在读操作时没有及时把 SDIO 切换为输入,或者切换太晚,读回的数据就会错位。第二个原因是 SCLK 的采样边沿不对。AD9910 在模式 0 下是在 SCLK 的下降沿输出数据,你应该在上升沿采样。如果你在下降沿采样,读回的数据可能不稳定。

第三个原因是寄存器本身是只写的。比如 CFR1、CFR2、CFR3 都是只写寄存器,你读回来的值可能是 0 或者随机值,这不代表写入失败。只有可读的寄存器,比如 FTW、POW、ASF 和 profile 寄存器,才能用于校验。第四个原因是 IO_UPDATE 的影响。如果你在读操作之前给了 IO_UPDATE,缓冲寄存器的值可能已经被更新,读回来的值可能和你刚写入的不一样。所以读回校验最好在写入之后立即进行,不要插入 IO_UPDATE。

还有一个比较隐蔽的原因:SPI 时钟频率过高。如果 SCLK 频率超过了 AD9910 的最大允许频率,或者信号完整性不好,读回的数据可能出现位错误。我建议在调试阶段把 SCLK 降到 1MHz 到 5MHz,等通信稳定后再逐步提高。另外,SDIO 上的上拉电阻也很重要,通常需要 10kΩ 的上拉,确保在空闲时 SDIO 为高电平。

4.3 高频输出时的杂散与时钟馈通问题

当输出频率较高时,比如超过 200MHz,你可能会发现输出频谱上有杂散或者时钟馈通。这个问题不完全是 SPI 配置的问题,但和寄存器配置有关。首先,检查 DAC 满量程电流的设置。如果电流设置过大,输出幅度会饱和,产生谐波。如果电流设置过小,信噪比会下降。数据手册里给出了推荐值,通常是 20mA 左右,但具体要根据你的负载和输出滤波器来调整。

其次,检查 PLL 的配置。如果 PLL 倍频系数过高,相位噪声会恶化,导致杂散。比如参考时钟 25MHz,PLL 倍频到 1GHz,倍频系数是 40,这个值比较高,相位噪声可能不太好。如果可能,尽量降低倍频系数,或者使用更高频率的参考时钟。另外,PLL 的环路滤波器参数也需要根据参考时钟频率和倍频系数来调整,数据手册里有推荐值,但实际使用时可能需要微调。

还有一个常见问题是时钟馈通。AD9910 的系统时钟会通过芯片内部耦合到输出,在输出频谱上表现为系统时钟频率的杂散。要减少这个杂散,可以在输出端加一个低通滤波器,截止频率略高于你的输出频率。另外,确保 SYSCLK 的走线远离输出走线,减少耦合。在 PCB 布局时,模拟地和数字地要分开,通过磁珠或者 0 欧姆电阻连接。

4.4 多片 AD9910 同步的注意事项

在一些应用中,比如相控阵或者多通道信号源,需要多片 AD9910 同步工作。同步的关键是确保所有芯片的 SYSCLK 同源同相,以及 IO_UPDATE 信号同时到达。首先,所有 AD9910 的参考时钟应该来自同一个时钟源,经过时钟分配器后分别送到每片芯片。时钟分配器的输出 skew 要尽可能小,通常要求小于 100ps。其次,IO_UPDATE 信号也应该同源,最好用 FPGA 的同一个引脚驱动,经过缓冲后送到每片芯片。如果走线长度不同,IO_UPDATE 到达的时间会有差异,导致同步误差。

另外,AD9910 有一个 SYNC_CLK 引脚,可以输出 SYSCLK 的 1/4 分频,用于多片同步。你可以用这个信号来对齐各片芯片的相位。具体做法是:配置一片为主片,其他为从片,通过 SYNC_CLK 和 SYNC_IN 引脚来同步。数据手册里有详细的多片同步流程,但实际操作中,PCB 布局和走线等长是关键。我做过一个四片同步的项目,走线长度差异控制在 5mm 以内,同步误差在 100ps 左右,满足大多数应用需求。

5. 从评估软件到 Verilog 的完整工作流

5.1 配置数据的提取与格式转换脚本

评估软件导出的配置文件不能直接用于 Verilog,你需要一个转换脚本。我通常用 Python 写一个小脚本来完成这个工作。脚本的功能是:读取评估软件导出的文本文件,解析出寄存器地址和数据,然后生成一个 Verilog 的查找表或者 ROM 初始化文件。比如生成一个 case 语句或者一个 $readmemh 格式的文件。

下面是一个简单的 Python 脚本示例:

import re def parse_config(file_path): configs = [] with open(file_path, 'r') as f: for line in f: # 假设格式为:地址 数据 match = re.match(r'0x([0-9A-Fa-f]+)\s+0x([0-9A-Fa-f]+)', line) if match: addr = int(match.group(1), 16) data = int(match.group(2), 16) configs.append((addr, data)) return configs def generate_verilog_rom(configs, output_path): with open(output_path, 'w') as f: for addr, data in configs: f.write(f"{addr:02X} {data:08X}\n") if __name__ == "__main__": configs = parse_config("ad9910_config.txt") generate_verilog_rom(configs, "ad9910_rom.hex")

这个脚本假设导出文件的格式是“0x地址 0x数据”,如果你的格式不同,需要调整正则表达式。生成的 hex 文件可以用 $readmemh 在 Verilog 里读取。注意,AD9910 的寄存器宽度不同,有的是 32 位,有的是 16 位,所以在生成 ROM 时,要统一成 32 位或者根据实际宽度处理。

5.2 初始化流程的状态机设计与上电时序

上电之后,AD9910 需要一定的初始化时间。首先是电源稳定,然后参考时钟稳定,然后 PLL 锁定。数据手册里给出了 PLL 锁定时间的典型值,通常是几百微秒。在 FPGA 里,你需要一个上电复位状态机,等待足够的时间后再开始 SPI 配置。我通常的做法是:上电后等待 10ms,确保电源和时钟稳定,然后开始配置寄存器。配置完成后,再等待 1ms,让 PLL 锁定,然后给 IO_UPDATE。

初始化状态机的流程大致是:IDLE -> WAIT_POWER -> CONFIG_CFR2 -> WAIT_PLL -> CONFIG_CFR1 -> CONFIG_FTW -> CONFIG_ASF -> CONFIG_PROFILE -> UPDATE -> DONE。每个配置步骤调用一次写操作状态机,等待 done 信号后进入下一步。这样就能保证配置顺序正确,而且每一步都有足够的时间。

这里有一个细节:CFR2 里的 PLL 使能位和倍频系数需要在配置其他寄存器之前设置,因为 PLL 锁定需要时间。所以我在配置完 CFR2 之后,会等待一段时间(比如 1ms),然后再配置 CFR1 和其他寄存器。另外,CFR1 里的 DAC 使能位和输出使能位也要正确设置,否则芯片没有输出。

5.3 动态频率切换的 Verilog 实现方案

在很多应用中,你需要动态改变输出频率,比如跳频通信或者扫频。AD9910 支持通过 profile 引脚快速切换频率,也支持通过 SPI 更新 FTW 寄存器。如果切换速度要求不高(毫秒级),可以直接用 SPI 更新 FTW,然后给 IO_UPDATE。如果要求微秒级切换,就需要使用 profile 引脚。

使用 profile 引脚的方式是:预先在 8 个 profile 寄存器里存储不同的 FTW、POW、ASF 值,然后通过 PS0、PS1、PS2 三个引脚选择当前使用的 profile。切换时只需要改变 PS 引脚的电平,不需要 SPI 通信,切换速度非常快,通常在几十纳秒以内。在 Verilog 里,你只需要输出三个引脚的电平即可。

如果要用 SPI 动态更新 FTW,你需要实现一个简单的接口:当收到新的频率值时,计算 FTW,然后调用写操作状态机写入 FTW 寄存器,最后给 IO_UPDATE。FTW 的计算可以用 FPGA 内部的乘法器和除法器实现,也可以预先算好放在查找表里。对于大多数应用,预先算好放在 ROM 里更简单,也更节省资源。

5.4 资源占用与时序收敛的优化建议

在 FPGA 里实现 AD9910 的 SPI 配置,资源占用其实很小。一个写操作状态机大概只需要几十个寄存器和几百个 LUT。如果你用 ROM 存储配置数据,会额外占用一些 Block RAM。对于大多数 FPGA 来说,这些资源都是绰绰有余的。但如果你需要同时控制多片 AD9910,或者需要高速动态更新,就需要考虑资源优化。

时序收敛方面,主要关注 SCLK 的输出时序。如果 SCLK 频率较高,比如超过 25MHz,你需要确保 SCLK 的输出延迟和 SDIO 的输出延迟匹配。在 FPGA 里,可以通过设置输出延迟或者使用 ODDR 原语来调整。另外,如果 SDIO 是双向的,输入和输出的时序都需要约束。我通常会在 XDC 或者 SDC 文件里对 SCLK 和 SDIO 添加时序约束,确保建立时间和保持时间满足要求。

还有一个优化建议:如果 SPI 配置数据是固定的,可以把配置数据直接硬编码在 Verilog 里,而不是用 ROM。这样可以节省 Block RAM,而且综合工具可以做更多的优化。如果配置数据需要动态改变,再用 ROM 或者寄存器组。另外,状态机的编码方式也会影响资源占用,用独热码(one-hot)可能比二进制码占用更多寄存器,但组合逻辑更简单,时序更好。具体选择取决于你的 FPGA 型号和时序要求。

6. 几个实际项目中的配置案例

6.1 单频点正弦波输出的最小配置集

如果你只需要输出一个固定频率的正弦波,配置其实很简单。最少的寄存器配置包括:CFR2 设置 PLL 和参考时钟倍频,CFR1 设置 DAC 使能和输出使能,FTW 设置输出频率,ASF 设置输出幅度。如果不需要相位控制,POW 可以保持默认值 0。Profile 寄存器也不需要配置,因为默认使用的是单频模式。

具体的配置值取决于你的硬件。假设参考时钟是 25MHz,PLL 倍频系数设为 40,系统时钟就是 1GHz。要输出 100MHz,FTW = 100e6 × 2^32 / 1e9 = 429496730 = 0x1999999A。ASF 设为满量程,也就是 0x3FFF。CFR2 的值需要设置 PLL 使能位和倍频系数,具体位定义参考数据手册。CFR1 需要设置 DAC 使能位和输出使能位。

这个最小配置集我用了很多次,非常稳定。需要注意的是,CFR2 的配置要在 CFR1 之前,因为 PLL 需要时间锁定。另外,IO_UPDATE 要在所有寄存器写入之后给,确保所有配置同时生效。

6.2 扫频模式下的寄存器动态更新策略

扫频模式需要动态改变 FTW。有两种实现方式:一种是用 FPGA 内部逻辑实时计算 FTW 并通过 SPI 写入,另一种是预先计算好一系列 FTW 值存储在 ROM 里,然后依次写入。前者的灵活性更高,可以任意改变扫频范围和步进;后者的速度更快,适合高速扫频。

如果扫频速度要求不高,比如每秒几千个频点,用 SPI 动态更新就足够了。在 Verilog 里,你可以用一个计数器来产生频率索引,然后从 ROM 里读取对应的 FTW,写入 AD9910。每次写入后给 IO_UPDATE,然后等待一段时间,再写下一个频点。如果扫频速度要求很高,比如微秒级切换,就需要使用 profile 引脚。你可以把 8 个频点预存在 profile 寄存器里,然后快速切换 PS 引脚。但 8 个频点可能不够,这时候可以结合 SPI 和 profile,用 SPI 更新 profile 寄存器的值,然后用 PS 引脚切换。

我在一个雷达回波模拟项目里用过扫频模式,扫频范围是 50MHz 到 150MHz,步进 1MHz,每个频点停留 10us。用 SPI 动态更新 FTW,SCLK 设为 20MHz,每次更新大约需要 2us,加上 IO_UPDATE 和等待时间,总共大约 5us,满足 10us 的停留时间要求。

6.3 多片级联时的 SPI 片选与数据分发

多片 AD9910 级联时,SPI 总线的处理有两种方式:一种是每片芯片有独立的 CSB,SCLK 和 SDIO 共享;另一种是每片芯片有独立的 SPI 接口,完全独立控制。第一种方式节省引脚,但需要确保在切换 CSB 时,SCLK 和 SDIO 的状态正确。第二种方式更简单,但占用更多 FPGA 引脚。

我通常推荐第一种方式,因为 AD9910 的 SPI 接口支持多片共享总线。具体做法是:FPGA 产生 SCLK 和 SDIO,然后通过一个译码器产生多个 CSB 信号。在写某一片芯片时,只拉低对应的 CSB,其他芯片的 CSB 保持高电平。这样就能实现分时复用。需要注意的是,在切换 CSB 时,要确保 SCLK 和 SDIO 处于空闲状态,避免误触发。

如果多片芯片需要同步更新,可以在所有芯片的 SPI 传输完成后,同时给 IO_UPDATE。这样所有芯片的配置会同时生效。IO_UPDATE 信号可以共享,也可以独立控制。如果共享,需要确保 IO_UPDATE 的驱动能力足够,通常需要加缓冲器。

6.4 低功耗场景下的寄存器配置取舍

在一些低功耗应用中,比如便携式设备,需要降低 AD9910 的功耗。AD9910 的功耗主要来自 DAC 和 PLL。你可以通过配置 CFR1 和 CFR2 来关闭不需要的功能。比如,如果不需要 PLL,可以关闭 PLL,直接使用参考时钟作为系统时钟,这样可以节省 PLL 的功耗。如果输出幅度不需要满量程,可以降低 ASF 的值,减少 DAC 的功耗。

另外,AD9910 有一个省电模式,可以通过 CFR1 的相应位来使能。在省电模式下,芯片的功耗会大幅降低,但输出也会关闭。这个模式适合在待机时使用。唤醒时需要重新配置寄存器,并等待 PLL 锁定。

还有一个技巧:如果输出频率较低,可以降低系统时钟频率,从而降低功耗。比如输出 10MHz,系统时钟可以设为 100MHz,而不是 1GHz。这样 DAC 和数字部分的功耗都会降低。但要注意,系统时钟降低后,FTW 的计算公式也要相应调整。

7. 写在最后的一些个人体会

AD9910 这颗芯片我用了好几年,从最开始的一头雾水到后来能够熟练配置,中间走了不少弯路。最大的体会是:数据手册一定要仔细看,尤其是时序图和寄存器位定义,很多问题的答案都在里面,只是需要耐心去找。另外,逻辑分析仪是调试 SPI 的利器,没有它,很多问题只能靠猜。还有一点,不要迷信评估软件的配置,它只是一个参考,实际硬件可能需要调整。

如果你刚开始接触 AD9910,我建议先从评估板入手,用官方软件配置,观察输出,然后用逻辑分析仪抓 SPI 波形,理解时序。等对芯片有了直观认识之后,再自己写 Verilog 代码。这样循序渐进,比一上来就啃手册要高效得多。另外,多和其他做硬件的朋友交流,很多坑别人已经踩过了,问一下就能省很多时间。

最后分享一个小技巧:在调试阶段,可以把 SCLK 频率降到 1MHz,这样即使时序有点问题,也能通信成功。等确认配置正确后,再逐步提高 SCLK 频率,观察是否稳定。这个方法帮我定位过好几次时序问题。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 23:38:37

AI Agent企业级落地指南:2026全景、信任挑战与工程实践

2026 年开工没多久&#xff0c;我身边做 AI 的朋友几乎都在聊同一个体感&#xff1a;AI Agent 的钱是真的进来了&#xff0c;可信任还没跟上。融资新闻一条接一条&#xff0c;产品盘点文章满天飞&#xff0c;但真敢把 Agent 放进核心生产流程的团队&#xff0c;十个里面未必有三…

作者头像 李华
网站建设 2026/9/28 23:37:54

POE交换机电源架构设计:12V升压48V与PSE端口保护实战

1. POE交换机电源架构的核心设计逻辑1.1 为什么POE交换机的电源架构值得单独拿出来讲搞过网络硬件的人都知道&#xff0c;POE交换机跟普通交换机最大的区别不在交换芯片&#xff0c;而在电源。普通交换机只需要一颗12V适配器插上去就能跑&#xff0c;整机功耗撑死十几瓦。但POE…

作者头像 李华
网站建设 2026/9/28 23:35:22

MATLAB CNN实战:MNIST手写数字识别98%准确率全流程

简介&#xff1a;这份资源面向希望入门深度学习与计算机视觉的MATLAB用户&#xff0c;尤其是需要完成课程设计、毕业设计或算法验证的学生与工程师。它提供了一套完整可运行的手写数字识别方案&#xff0c;采用单层卷积网络提取MNIST图像特征&#xff0c;再通过双层全连接网络完…

作者头像 李华
网站建设 2026/9/28 23:35:17

合规机票比价自动化:Playwright与Skyscanner API实战

我理解您的要求&#xff0c;也完全认同内容安全与专业表达的重要性。但需要坦诚说明&#xff1a;当前输入中仅提供了项目标题和网络热词列表&#xff0c;未提供任何实质性的项目正文、技术细节、实现逻辑或可验证的上下文信息。标题“Browser Use结合Jev模型实现自动选飞机票&a…

作者头像 李华
网站建设 2026/9/28 23:32:45

7个AI论文写作助手,终结LaTeX排版与模板匹配难题

身边很多朋友写论文&#xff0c;最头疼的其实不是内容&#xff0c;而是 LaTeX 那套排版规则。标题要加什么命令&#xff0c;图片往左还是往右&#xff0c;参考文献格式怎么调&#xff0c;字号行距按哪个模板改——这些琐碎活儿占用大量时间&#xff0c;等论文真正写完&#xff…

作者头像 李华
网站建设 2026/9/28 23:30:31

统一命令行入口:用CLI-Anything封装散落脚本与操作

说实话&#xff0c;刚接触CLI-Anything的时候&#xff0c;我真没觉得它有多特别。平时工作里已经攒了一堆Shell脚本、一堆Python小工具、一堆alias&#xff0c;还有贴在工位上的便签——哪个命令对应哪个项目、哪个脚本要传什么参数&#xff0c;全靠脑子记。直到有一次我休假回…

作者头像 李华