FPGA驱动AD9910:从寄存器配置到频谱仪干净单音输出
简介这份资源面向FPGA开发者与数字信号处理学习者提供基于Verilog实现的AD9910高精度DDS芯片驱动程序可用于信号发生器、测试测量、无线通信及雷达系统等场景的波形控制开发。包内共4个文件以2个Verilog源码文件为核心分别承担AD9910底层驱动与顶层模块逻辑另含1个XML组件描述文件与1个TCL脚本压缩包整体约8KB结构精简便于快速移植。驱动设计覆盖SPI接口时序控制、配置寄存器读写、状态机管理、跨时钟域同步以及频率相位幅度参数更新等关键环节并预留错误检测思路帮助读者理解FPGA与AD9910之间的完整通信流程。目前已有3430人学习下载适合作为Verilog编程与DDS芯片交互的入门参考也可在此基础上扩展自定义波形生成与系统集成方案。1. 从一块空板到频谱仪上的干净单音这套 AD9910 驱动到底能帮你省掉哪几步如果你手上有一块 FPGA 开发板外面挂了一颗 AD9910上电后频谱仪上却只有底噪或者一堆杂散那大概率不是芯片坏了而是驱动时序没吃透。AD9910 是 ADI 的一款 1 GSPS 直接数字频率合成器内部有 14 位 DAC、32 位频率调谐字、16 位相位偏移和 14 位幅度控制能输出最高 400 MHz 的模拟正弦波。它的寄存器映射、SPI 时序、IO_UPDATE 脉冲宽度、PLL 锁定判断每一项都有明确的时序要求少一个条件就出不来波形。这套基于 FPGA 的 AD9910 驱动程序核心就是用 Verilog 把串行配置、并行调制和状态回读三条通路串起来让新手能直接跑通单音输出让熟手能在此基础上做扫频、调制和相位同步。适合正在做信号源、雷达模拟、通信本振或者任意波形发生的从业者也适合刚接触高速 DAC 配置、想找一个能上板验证的 Verilog 驱动参考的 FPGA 工程师。2. 先把 AD9910 的寄存器模型和 SPI 时序吃透为什么直接抄时序图会翻车2.1 寄存器映射不是一张平表而是一组带副寄存器的影子结构AD9910 的寄存器空间分两级一级是 SPI 直接访问的缓冲寄存器二级是真正控制内核的副寄存器。你写进去的 CFR1、CFR2、FTW、POW、ASF 这些值在 IO_UPDATE 上升沿之前只停在缓冲层不会立刻生效。这个设计的好处是你可以一次性更新多个参数让频率、相位、幅度在同一时刻切换避免中间态产生杂散。但坏处是如果你只写了寄存器却忘了拉 IO_UPDATE频谱仪上什么都不会变新手很容易在这里卡半天。常见做法是上电后先做一次全局复位然后按固定顺序写 CFR1、CFR2、CFR3、FTW、POW、ASF最后统一发一个 IO_UPDATE 脉冲。顺序不能乱因为 CFR1 里的 SDIO 输入输出方向、LSB 优先还是 MSB 优先、以及是否使能自动清除会影响后续所有 SPI 传输的解析方式。我一般会把 CFR1 放在最前面并且在上电配置里固定用 MSB 优先、SDIO 双向模式这样后面读状态寄存器时不用再切方向。下面这段 Verilog 是寄存器写入任务的核心骨架参数含义我写在注释里// AD9910 SPI 写寄存器任务 // addr: 7 位寄存器地址data: 32 位数据高位对齐 // 时序SCLK 空闲低SDIO 在 SCLK 上升沿采样 task ad9910_write_reg; input [6:0] addr; input [31:0] data; integer i; begin spi_cs_n 1b0; // 片选拉低开始传输 #SPI_HALF_PERIOD; // 先发 7 位地址MSB 优先 for (i 6; i 0; i i - 1) begin spi_sclk 1b0; spi_sdio addr[i]; #SPI_HALF_PERIOD; spi_sclk 1b1; // 上升沿写入 #SPI_HALF_PERIOD; end // 再发 32 位数据MSB 优先 for (i 31; i 0; i i - 1) begin spi_sclk 1b0; spi_sdio data[i]; #SPI_HALF_PERIOD; spi_sclk 1b1; #SPI_HALF_PERIOD; end spi_sclk 1b0; spi_cs_n 1b1; // 片选拉高结束传输 #SPI_CS_HOLD; end endtask这段代码里最关键的两个参数是SPI_HALF_PERIOD和SPI_CS_HOLD。AD9910 的 SCLK 最高能到 70 MHz但上电初期我建议先降到 10 MHz 以下等 PLL 锁定、电源稳定后再提速。SPI_CS_HOLD是片选拉高后的保持时间手册里要求最小 1 个 SCLK 周期实际我一般给 20 ns 以上避免状态机还没回到 IDLE 就被下一次传输打断。2.2 IO_UPDATE 不是普通 GPIO它的脉冲宽度和同步方式决定输出是否干净IO_UPDATE 是 AD9910 里最容易被低估的信号。它不是一个随便拉一下的使能脚而是一个要求最小脉冲宽度、并且和 SYNC_CLK 有建立保持关系的同步输入。手册里给的典型值是高电平至少保持一个 SYNC_CLK 周期SYNC_CLK 是系统时钟的四分之一。如果你用 1 GHz 系统时钟SYNC_CLK 就是 250 MHz周期 4 ns那 IO_UPDATE 高电平至少 4 ns。但实际板子上如果走线长、驱动弱我建议给到 20 ns 以上并且用 FPGA 内部寄存器直接输出不要经过组合逻辑否则毛刺会让内部状态机误触发。另一个坑是 IO_UPDATE 和 SPI 传输的间隔。如果你在 SPI 最后一个 SCLK 上升沿之后立刻拉 IO_UPDATEAD9910 可能还没把数据从移位寄存器搬到缓冲寄存器结果更新的是旧值。常见做法是 SPI 片选拉高后至少等 10 ns 再发 IO_UPDATE我一般会等 50 ns留足余量。2.3 单音输出的最小配置路径从复位到频谱仪出峰要让 AD9910 输出一个干净单音最少需要配置这几个寄存器CFR1 设置 SPI 模式和复位行为CFR2 设置 PLL 倍频和输出使能FTW 设置频率调谐字ASF 设置输出幅度。POW 可以保持零相位。下面是一个可上板的配置序列我用伪代码加参数说明的方式列出来// 上电配置序列系统时钟 1 GHz目标输出 100 MHz // 1. 全局复位 ad9910_write_reg(7h00, 32h00000000); // CFR1 先写零清状态 ad9910_write_reg(7h00, 32h00000002); // CFR1 bit11软件复位 #1000; // 等待复位完成 // 2. 配置 CFR1MSB 优先SDIO 双向禁用自动清除 ad9910_write_reg(7h00, 32h00000000); // 3. 配置 CFR2PLL 使能倍频 20 倍输出使能 ad9910_write_reg(7h01, 32h00000000 | (20 8) | (1 4)); // 4. 写 FTW100 MHz / (1 GHz / 2^32) 429496729.6取 429496730 ad9910_write_reg(7h07, 32d429496730); // 5. 写 ASF幅度满量程 ad9910_write_reg(7h09, 32h00003FFF); // 6. 发 IO_UPDATE io_update 1b1; #50; io_update 1b0;这里 FTW 的计算公式是FTW f_out * 2^32 / f_sys。如果系统时钟是 1 GHz目标 100 MHz算出来是 429496729.6取整后实际输出会有微小频偏这是 DDS 的固有量化误差不是驱动问题。ASF 是 14 位幅度控制满量程写 0x3FFF对应最大输出。CFR2 里的 PLL 倍频系数要和你板上的参考时钟匹配如果参考时钟是 50 MHz倍频 20 倍就是 1 GHz写错这个值要么不出波要么输出频率整体偏移。3. 用 Verilog 把 SPI 状态机、IO_UPDATE 和回读通路串起来一份能上板的驱动结构3.1 状态机划分IDLE、ADDR、DATA、HOLD、UPDATE 五段式最稳AD9910 的 SPI 写时序本身不复杂但加上 IO_UPDATE 和回读之后状态机容易写乱。我一般用五段式IDLE 等触发ADDR 发 7 位地址DATA 发 32 位数据HOLD 等片选保持UPDATE 发 IO_UPDATE 脉冲。读操作则多一个 TURNAROUND 状态用来切换 SDIO 方向。下面是一个简化的状态机框架localparam S_IDLE 3d0; localparam S_ADDR 3d1; localparam S_DATA 3d2; localparam S_HOLD 3d3; localparam S_UPDATE 3d4; localparam S_READ 3d5; reg [2:0] state, next_state; reg [5:0] bit_cnt; reg [6:0] addr_reg; reg [31:0] data_reg; always (posedge clk or negedge rst_n) begin if (!rst_n) state S_IDLE; else state next_state; end always (*) begin next_state state; case (state) S_IDLE: if (start) next_state S_ADDR; S_ADDR: if (bit_cnt 6d6) next_state S_DATA; S_DATA: if (bit_cnt 6d31) next_state S_HOLD; S_HOLD: if (hold_cnt 6d3) next_state S_UPDATE; S_UPDATE: if (update_cnt 6d7) next_state S_IDLE; default: next_state S_IDLE; endcase end这个状态机的关键点是bit_cnt和hold_cnt的位宽要够别用 4 位计数器去数 32 位数据否则会提前回绕。S_HOLD里我给了 3 个时钟周期的片选保持对应 50 ns 左右足够覆盖手册要求。S_UPDATE里 update_cnt 数到 7对应 100 ns 的 IO_UPDATE 高电平比最小要求宽很多但能兼容大多数板子。3.2 回读通路为什么你读回来的状态寄存器总是全零AD9910 支持回读但回读不是默认打开的。你需要在 CFR1 里把 SDIO 方向切到输出然后发读命令再在 TURNAROUND 状态里把 SDIO 从输出切成输入。很多新手读回来全是零原因有三个一是 CFR1 没配置成双向模式SDIO 一直是输入二是读命令的地址没带读标志位三是 TURNAROUND 时间不够SDIO 方向切换还没稳定就开始采样。常见做法是读之前先写一次 CFR1把 bit7 设成 1 表示使能 SDIO 输出然后发地址时最高位带 1 表示读操作。TURNAROUND 我一般给 2 个 SCLK 周期如果板子上有串联电阻或者走线电容大给到 4 个周期更稳。下面是一个读寄存器的任务片段task ad9910_read_reg; input [6:0] addr; output [31:0] data_out; integer i; begin spi_cs_n 1b0; // 发读地址最高位为 1 for (i 6; i 0; i i - 1) begin spi_sclk 1b0; spi_sdio (i 6) ? 1b1 : addr[i]; #SPI_HALF_PERIOD; spi_sclk 1b1; #SPI_HALF_PERIOD; end // TURNAROUND切换 SDIO 为输入 spi_sdio 1bz; #(SPI_HALF_PERIOD * 4); // 读 32 位数据 for (i 31; i 0; i i - 1) begin spi_sclk 1b0; #SPI_HALF_PERIOD; spi_sclk 1b1; data_out[i] spi_sdio; #SPI_HALF_PERIOD; end spi_cs_n 1b1; end endtask注意spi_sdio 1bz这一句它把 FPGA 引脚切成高阻让 AD9910 驱动 SDIO。如果你的 FPGA 引脚没有三态控制或者顶层把 SDIO 固定成了输出这里读回来永远是 FPGA 自己输出的值不是芯片的状态。这个坑我在早期项目里踩过查了两天才发现是引脚方向没切。3.3 时钟域处理SPI 时钟和系统时钟之间的握手AD9910 的 SPI 时钟是由 FPGA 产生的但配置数据往往来自另一个时钟域比如 50 MHz 的配置时钟和 1 GHz 的 DDS 系统时钟。如果直接把 50 MHz 域的信号拉到 1 GHz 域做状态机建立保持时间不够状态机会跑飞。常见做法是SPI 状态机全部跑在低速时钟域配置完成后产生一个跨时钟域的脉冲同步到高速域再去触发 IO_UPDATE。或者反过来IO_UPDATE 也放在低速域因为它的最小宽度要求是 4 ns低速域一个周期就够。我一般会把 SPI 状态机和 IO_UPDATE 都放在同一个 50 MHz 时钟域这样时序约束简单不用做复杂的跨时钟域握手。DDS 内核的 SYNC_CLK 和系统时钟由 AD9910 内部 PLL 产生FPGA 不需要管。唯一要注意的是如果你用 FPGA 给 AD9910 提供参考时钟参考时钟的抖动会直接影响输出频谱的相噪所以参考时钟最好从专用时钟引脚进走全局时钟网络。4. 避坑与排查AD9910 驱动调试中最容易翻车的五个现场4.1 现象SPI 写完了IO_UPDATE 也发了频谱仪上只有底噪原因CFR2 里的 PLL 使能位没打开或者倍频系数写错导致内部系统时钟没起来。AD9910 的 DAC 需要系统时钟才能工作如果 PLL 没锁定DAC 输出就是零。另一个可能是输出使能位没置位CFR2 里有一个 bit 控制 DAC 使能漏掉它同样不出波。解决先读回 CFR2 确认写入值再用示波器测 SYNC_CLK 引脚如果有 250 MHz 左右的方波说明 PLL 锁定了。如果没有检查参考时钟是否送到、PLL 倍频系数是否在允许范围内。AD9910 的 PLL 倍频范围是 4 到 20 倍超出这个范围不会锁定。4.2 现象输出频率对但幅度很小频谱上杂散很多原因ASF 寄存器没写或者写成了默认值。AD9910 上电后 ASF 默认是零DAC 输出幅度为零。另一个可能是 CFR1 里的自动清除功能没关导致每次 IO_UPDATE 后幅度被清零。解决确认 ASF 写入了非零值满量程是 0x3FFF。同时检查 CFR1 的 bit5 和 bit6这两个位控制自动清除和幅度缩放如果使能了自动清除每次更新后幅度会回到零。我一般会在初始化时把这两个位清零手动控制幅度。4.3 现象读回的状态寄存器全是 0xFF 或者全 0原因SDIO 方向没切换或者 TURNAROUND 时间不够。全 0xFF 通常是 SDIO 被外部上拉FPGA 没驱动全 0 通常是 SDIO 被 FPGA 一直驱动低芯片没机会输出。解决检查 CFR1 的 SDIO 方向配置确认读操作前把 SDIO 切成输入。用示波器看 SDIO 在 TURNAROUND 期间是否有中间电平如果有说明方向切换了但时间不够加大 TURNAROUND 周期。另外读地址的最高位要置 1否则芯片不会进入读模式。4.4 现象输出频谱上有规律的杂散间隔等于某个时钟频率原因SPI 时钟和 DDS 系统时钟发生了耦合或者 IO_UPDATE 的脉冲宽度不够导致内部状态机部分更新。另一个常见原因是电源去耦不足AD9910 的模拟电源和数字电源如果共用一路 LDODAC 输出会带电源纹波。解决先降低 SPI 时钟频率看杂散是否变化。如果杂散间隔跟着 SPI 时钟走说明是数字耦合需要在 PCB 上把 SPI 走线远离模拟输出。如果杂散间隔固定检查 IO_UPDATE 宽度用示波器确认高电平时间大于 20 ns。电源方面AD9910 的 AVDD 和 DVDD 最好分开供电至少加 10 uF 和 0.1 uF 的组合去耦。4.5 现象上电后第一次配置正常第二次配置就失效原因状态机没有正确复位或者 IO_UPDATE 的计数器没有清零。有些写法里状态机回到 IDLE 后计数器还保留着上次的值下一次传输直接从中间开始地址和数据错位。解决在 IDLE 状态里把所有计数器清零包括 bit_cnt、hold_cnt、update_cnt。另外每次传输前把 addr_reg 和 data_reg 重新加载不要依赖上次的残留值。我一般会在状态机的 IDLE 分支里写一段默认赋值确保每次新传输都是干净起点。5. 进阶玩法用 RAM 做扫频和脉冲调制以及一个验证输出是否干净的小技巧5.1 用 FPGA 内部 RAM 做线性扫频把 FTW 序列预存进去单音跑通之后下一步通常是扫频。AD9910 支持数字斜坡发生器但用 FPGA 内部 RAM 预存 FTW 序列更灵活扫频曲线可以任意定义。做法是在 FPGA 里开一块双口 RAM深度 1024位宽 32 位存好每个频点的 FTW 值。然后用一个地址计数器在 IO_UPDATE 的驱动下递增每个更新周期从 RAM 读一个 FTW 写到 AD9910。更新速率由 IO_UPDATE 的周期决定最快可以到 SYNC_CLK 的四分之一也就是 4 ns 一个点但实际受 SPI 写入速度限制我一般做到 1 us 一个点扫频范围 10 MHz 到 200 MHz步进 1 MHz扫完一轮 190 us。// 扫频控制每个 update_tick 从 RAM 读一个 FTW 并写入 AD9910 reg [9:0] ram_addr; reg [31:0] ftw_data; always (posedge clk) begin if (update_tick) begin ram_addr ram_addr 1b1; ftw_data ram_q; // 从 RAM 读出预存的 FTW ad9910_write_reg(7h07, ftw_data); // 写频率调谐字 io_update 1b1; // 发更新脉冲 end else begin io_update 1b0; end end这里update_tick的周期决定了扫频速度ram_addr的位宽决定了扫频点数。注意每次写 FTW 之后都要发 IO_UPDATE否则频率不会切换。如果扫频速度要求很高可以把 SPI 时钟提到 50 MHz但前提是板子走线短、信号质量好。5.2 脉冲调制用 ASF 做幅度键控注意上升沿和下降沿的过渡AD9910 的 ASF 寄存器可以实时改幅度配合 IO_UPDATE 就能做 ASK 调制。但直接切 ASF 会有突变频谱上会出现键控边带。如果要做干净的脉冲调制常见做法是在 FPGA 里做一个幅度斜坡每个更新周期把 ASF 从零逐步加到目标值再逐步降到零。斜坡长度取决于你想要的过渡带宽我一般用 16 个点做上升沿16 个点做下降沿更新周期 1 us过渡时间 16 us对应的边带抑制能到 40 dB 以上。5.3 验证输出是否干净用示波器的 FFT 功能做快速检查频谱仪当然最准但调试现场不一定有。一个退而求其次的办法是用高带宽示波器的 FFT 功能把探头接到 AD9910 的输出滤波之后看基波和主要杂散。示波器 FFT 的动态范围不如频谱仪但能快速判断有没有明显杂散、幅度是否正常、频率是否对。我一般会先看时域波形是否干净的正弦再看 FFT 里有没有非谐波杂散。如果时域波形上有台阶或者毛刺说明 IO_UPDATE 或者 SPI 时序有问题先解决时域再去看频域。从那以后我每次上电配置 AD9910都强制走一遍「读回 CFR2 确认 PLL 锁定、示波器看 SYNC_CLK、频谱仪看单音」这三步不再凭感觉判断。希望帮到你。本文还有配套的精品资源点击获取