简介:这份资源面向FPGA开发者与数字电路学习者,提供基于Verilog实现的AD9910高精度DDS芯片驱动程序,可用于信号发生器、测试测量、无线通信及雷达系统等需要高质量模拟波形输出的场景。压缩包共4个文件,约8KB,包含2个Verilog源文件、1个XML元数据文件与1个TCL脚本,分别承担逻辑实现、组件描述与工程配置等用途。驱动程序围绕SPI接口配置、状态机管理、时钟同步、波形参数设置及错误检测等环节展开,读者可据此理解FPGA与AD9910之间的寄存器读写时序与通信机制。目前已有3429人学习下载,适合作为Verilog编程与DDS芯片控制的实践参考,也可在此基础上扩展频率、相位与幅度控制功能,适配更多自定义应用需求。
1. 基于FPGA的AD9910驱动程序:从时序黑匣子到可复现的DDS控制链路
如果你手里有一块FPGA开发板,又恰好需要一路频率、相位、幅度都能数控的正弦波或方波,AD9910几乎是绕不开的选型。它是一颗内置14位DAC的直接数字频率合成器(DDS),最高采样率1 GSPS,支持单频、RAM调制、数字斜坡等多种模式。但很多人第一次上手时会卡在同一个地方:芯片手册时序图看得懂,可写出来的Verilog就是不出波形,读回寄存器全是0或者0xFF。问题往往不在DDS本身,而在“基于FPGA的AD9910驱动程序”这条串行配置链路上——SPI时序、上电复位、IO更新、寄存器映射,任何一环出错,芯片就安静得像没通电。这篇笔记面向已经会用FPGA做基本逻辑、但还没把AD9910真正跑起来的工程师,把驱动从时序到参数逐层拆开,目标是让你照着能复现出一路可控正弦波。
2. AD9910驱动到底在驱动什么:寄存器、时序与三种写入路径
2.1 先分清AD9910的SPI不是标准SPI
AD9910的串行端口支持SPI兼容模式,但它和你在STM32上用的标准SPI有本质区别。标准SPI是CPOL/CPHA四模式,而AD9910的时序由SCLK、SDIO、SDO、CSB四根线构成,数据在SCLK上升沿采样,但它的“指令字节”结构是固定的:一个字节的指令 + 若干字节的数据。指令字节的最高位是R/W(1读0写),接着两位是x(无关位),低5位是寄存器地址。也就是说,你一次传输至少是1字节指令加N字节数据,N由目标寄存器的宽度决定。
很多新手直接拿FPGA的SPI IP核去接,结果发现IP核只发数据不发指令字节,或者CSB拉低时机不对。AD9910要求CSB在整帧传输期间保持低电平,SCLK空闲状态可以是高或低,但数据必须在上升沿稳定。我一般会自己写一个状态机,而不是用硬核SPI,因为AD9910的帧结构太特殊,硬核反而绕。
2.2 三种写入路径:单次写、IO更新、RAM回放
AD9910的寄存器写入后并不会立即生效,这是最容易翻车的地方。芯片内部有一个“IO更新”机制,只有IO_UPDATE引脚收到一个上升沿,暂存寄存器的值才会被搬到活动寄存器。所以你的驱动必须包含两个动作:先通过SPI把数据写进暂存区,再给IO_UPDATE一个脉冲。
对于单频输出,流程是:写CFR1、CFR2配置功能,写FTW(频率调谐字)、POW(相位偏移字)、ASF(幅度比例因子),然后拉高IO_UPDATE。对于需要扫频或调制,可以用数字斜坡模式,配置DRL、DRR等寄存器。对于任意波形回放,则要把波形数据写进RAM,再配置RAM播放控制。三种路径的寄存器映射不同,但SPI写入时序完全一致。
2.3 上电顺序与复位:别让芯片带着未知状态工作
AD9910上电后,内部寄存器是未定义的。手册要求先给RESET引脚一个高脉冲(至少1个SCLK周期),然后等待至少1ms让内部LDO稳定,再开始SPI写入。我见过有人直接上电就写寄存器,结果输出频率是设定值的两倍——因为FTW寄存器的高位残留了随机值。
正确的上电序列是:电源稳定 → 拉高RESET至少10ns → 拉低 → 延时1ms → 写CFR1关闭不必要的功能 → 写其他寄存器 → IO_UPDATE。这个顺序在Verilog里可以用一个简单的复位状态机实现,不需要软核。
3. 用Verilog写一个可综合的AD9910 SPI驱动状态机
3.1 状态机设计:IDLE、START、SHIFT、STOP四段式
下面是一个我反复用过的SPI写入状态机,支持任意长度数据,指令字节和数据字节分开传入。代码里用了一个移位寄存器,SCLK由系统时钟分频产生,分频系数可调。
// AD9910 SPI写驱动,参数化分频与数据宽度 module ad9910_spi_wr #( parameter CLK_DIV = 8, // SCLK分频系数,SCLK = sys_clk / (2*CLK_DIV) parameter DATA_W = 32 // 最大数据位宽,按寄存器实际宽度截取 )( input wire sys_clk, input wire rst_n, input wire start, // 启动一次写传输 input wire [7:0] instr, // 指令字节:R/W + 地址 input wire [DATA_W-1:0] data, // 待写数据,高位在前 input wire [5:0] data_len, // 数据字节数,1~4 output reg sclk, output reg sdio, output reg csb, output reg done ); localparam IDLE = 2'd0; localparam START = 2'd1; localparam SHIFT = 2'd2; localparam STOP = 2'd3; reg [1:0] state; reg [7:0] div_cnt; reg [4:0] bit_cnt; // 当前已发送位数 reg [39:0] shift_reg; // 指令+数据拼接,最多40位 reg [5:0] total_bits; always @(posedge sys_clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; sclk <= 0; sdio <= 0; csb <= 1; done <= 0; div_cnt <= 0; bit_cnt <= 0; shift_reg <= 0; total_bits<= 0; end else begin case (state) IDLE: begin sclk <= 0; csb <= 1; done <= 0; if (start) begin // 拼接指令字节和数据,数据按字节对齐到高位 shift_reg <= {instr, data, {(40-DATA_W-8){1'b0}}}; total_bits <= 8 + data_len * 8; bit_cnt <= 0; div_cnt <= 0; csb <= 0; state <= START; end end START: begin // 产生SCLK,上升沿输出数据,下降沿移位 if (div_cnt == CLK_DIV-1) begin div_cnt <= 0; sclk <= ~sclk; if (sclk == 0) begin // 上升沿:输出当前最高位 sdio <= shift_reg[39]; end else begin // 下降沿:移位 shift_reg <= {shift_reg[38:0], 1'b0}; bit_cnt <= bit_cnt + 1; if (bit_cnt == total_bits-1) begin state <= STOP; end end end else begin div_cnt <= div_cnt + 1; end end STOP: begin sclk <= 0; csb <= 1; done <= 1; state<= IDLE; end endcase end end endmodule这段代码的关键点有三个。第一,shift_reg拼接时把指令字节放在最高8位,数据放在紧接着的高位,低位补零,这样移位时总是先发指令再发数据。第二,total_bits由data_len决定,支持1到4字节数据,覆盖AD9910所有寄存器宽度。第三,SCLK在div_cnt计数到一半时翻转,保证数据在上升沿稳定。CLK_DIV参数决定SCLK频率,AD9910的SCLK最高可到70MHz左右,但实际建议不超过25MHz,留足建立保持余量。
3.2 指令字节怎么算:以FTW寄存器为例
FTW寄存器地址是0x07,宽度32位。写操作R/W=0,所以指令字节是0_00_00111,即0x07。如果你要读,指令字节是0x87。数据就是32位频率调谐字。假设系统时钟100MHz,想输出10MHz正弦波,FTW = round(10e6 * 2^32 / 1e9) = 42949673,十六进制0x028F5C29。
在顶层模块里,你可以用一个简单的序列器依次写CFR1、CFR2、FTW、ASF,最后发IO_UPDATE脉冲。IO_UPDATE就是一个至少一个SCLK周期的高脉冲,直接用一个计数器产生即可。
3.3 读回验证:SDO引脚不能悬空
写完寄存器后,最好读回验证。AD9910的SDO引脚在读操作时输出数据,但很多开发板把SDO悬空了,导致读回全是0。读时序和写类似,只是SDIO在指令阶段后变为高阻,SDO开始驱动。FPGA端要把SDO接一个输入引脚,并在SCLK下降沿采样。读回的数据可以用来确认FTW是否写对,这是排查“波形频率不对”最快的手段。
4. 参数计算与模式配置:FTW、POW、ASF和数字斜坡
4.1 频率调谐字FTW:一个除法搞定,但别用浮点
FTW的计算公式是:FTW = round(f_out * 2^32 / f_sys)。其中f_sys是DAC采样率,通常由外部时钟决定,比如1GHz。在FPGA里不要用浮点,直接整数运算:先算f_out * 2^32,再除以f_sys。如果f_out和f_sys都是整数Hz,可以用64位中间变量。比如f_out=10MHz,f_sys=1GHz,FTW = 10e6 * 4294967296 / 1e9 = 42949672.96,取整42949673。
注意,FTW是32位无符号数,写入时高位在前。如果你写反了字节序,输出频率会变成完全错误的值。我一般会在代码里加一个字节序转换函数,确保MSB先发。
4.2 相位偏移字POW和幅度比例因子ASF
POW是16位,公式:POW = round(phase_deg / 360 * 65536)。ASF是14位,范围0到16383,对应幅度0到满量程。ASF=16383时输出最大幅度,但实际DAC输出还受外部滤波和负载影响。如果你发现幅度上不去,先检查ASF是否写满,再检查DAC输出端的匹配电阻。
4.3 数字斜坡模式:扫频和扫幅的寄存器配置
数字斜坡模式用于产生线性扫频或扫幅。需要配置DRL(斜坡下限)、DRU(斜坡上限)、DRR(斜坡步长)、DRS(斜坡速率)等寄存器。以扫频为例,DRL和DRU都是32位FTW值,DRR是32位步长,DRS是16位速率。每次IO_UPDATE后,DDS会按照DRS设定的速率自动累加FTW,直到达到DRU。这个模式适合做雷达或测试信号源,但要注意斜坡步长和速率要匹配,否则输出会跳变。
配置顺序:先写CFR2使能数字斜坡,再写DRL、DRU、DRR、DRS,最后IO_UPDATE启动。停止时写CFR2关闭使能。
5. 避坑与排查:AD9910驱动最常见的5个翻车现场
5.1 现象:SPI写完后读回全是0xFF
原因:SDO引脚在FPGA端没有正确配置为输入,或者读时序中SDIO方向切换时机不对。AD9910在读操作时,指令字节的最后一位是R/W=1,之后SDIO变为高阻,SDO驱动数据。如果FPGA没有在正确时刻释放SDIO,总线冲突会导致读回错误。
解决:在SPI状态机里增加一个方向控制信号,读操作时在指令字节发送完毕后立即把SDIO设为输入,并在SCLK下降沿采样SDO。如果开发板SDO没接,直接放弃读回,用示波器看输出。
5.2 现象:输出频率是设定值的两倍或一半
原因:FTW字节序错误,或者系统时钟频率与预期不符。比如你按1GHz算FTW,但实际DAC采样率是500MHz,输出频率就会翻倍。
解决:先用示波器测量DAC输出频率,反推实际f_sys。然后检查FTW写入的字节序,确保MSB在前。可以在代码里加一个已知频率的测试模式,比如固定输出1MHz,验证链路。
5.3 现象:IO_UPDATE后输出无变化
原因:IO_UPDATE脉冲太窄,或者CSB在IO_UPDATE时没有拉高。AD9910要求IO_UPDATE上升沿时CSB为高,否则更新被忽略。
解决:在状态机里确保IO_UPDATE脉冲宽度至少一个SCLK周期,并且在发脉冲前拉高CSB。我一般用计数器产生一个1us宽的高脉冲,足够可靠。
5.4 现象:输出波形幅度很小或失真
原因:ASF没有写满,或者DAC输出端没有加合适的重建滤波器。AD9910输出是电流型,需要外部运放或电阻转换成电压。如果直接接高阻负载,幅度会很小。
解决:检查ASF寄存器是否设为16383。检查DAC输出端的50欧姆电阻和低通滤波器。如果输出高频信号,滤波器截止频率要足够高,否则幅度衰减。
5.5 现象:SPI通信完全无响应
原因:RESET引脚没有正确复位,或者电源电压不足。AD9910需要1.8V和3.3V多路电源,任何一路缺失都会导致芯片不工作。
解决:用万用表测量所有电源引脚电压,确认在手册范围内。检查RESET引脚上电时是否有高脉冲。如果FPGA和AD9910电平不匹配,加电平转换芯片。
6. 进阶技巧:用状态机实现多寄存器自动序列与时序余量验证
当你已经能稳定输出单频信号后,下一步通常是让AD9910按预设序列自动切换频率或幅度。手动写多个SPI传输再逐个触发IO_UPDATE,代码会变得冗长且容易出错。我一般会做一个“寄存器序列表”,把要写的寄存器地址、数据、数据长度预先存在FPGA的Block RAM里,然后用一个序列器自动遍历。这样切换模式只需要换一张表,不用改状态机。
具体做法是:定义一个位宽足够的ROM,每个条目包含指令字节、数据、数据长度。序列器从地址0开始读,每读完一条就启动一次SPI传输,传输完成后地址加1,直到遇到结束标志。所有条目写完后,再统一发一个IO_UPDATE脉冲。这样能保证所有寄存器在同一时刻生效,避免中间状态导致输出毛刺。
时序余量验证是另一个容易被忽略的环节。AD9910的SCLK最高频率在手册里是70MHz,但实际PCB走线、FPGA IO延迟、电平转换都会吃掉余量。我习惯在初期把SCLK降到10MHz以下,确保功能正确,然后再逐步提高,用示波器看SCLK上升沿和SDIO数据的关系。如果发现数据在上升沿附近抖动,就加大分频系数。另外,CSB到SCLK的建立时间也要留够,至少5ns。
还有一个实用技巧:用FPGA内部的逻辑分析仪(比如集成在开发环境里的在线调试工具)抓SPI波形。把SCLK、SDIO、CSB、IO_UPDATE四个信号接到调试核上,触发条件设为CSB下降沿,就能看到完整的传输过程。对比手册时序图,一眼就能看出是建立时间不够还是字节序错了。这个手段比反复改代码快得多。
最后说一个我自己的教训:早期做AD9910驱动时,我总想一次把代码写完美,结果调了两周没出波形。后来改成“最小系统法”——先只写一个FTW寄存器,固定输出10MHz,用示波器确认有信号;再加ASF调幅度;再加POW调相位;最后才上数字斜坡。每一步都验证通过再往下走。这个习惯让我后来做任何DDS芯片都能在半天内出第一版驱动。希望帮到你。
本文还有配套的精品资源,点击获取