FPGA驱动AD9910 DDS:SPI配置、寄存器设计与扫频实现全解析

发布时间:2026/9/7 5:25:57
FPGA驱动AD9910 DDS:SPI配置、寄存器设计与扫频实现全解析 简介基于FPGA的AD9910驱动源码包用Verilog实现DDS芯片控制面向信号发生器、频率合成类项目开发者帮助解决器件配置与波形生成问题。驱动内部包含寄存器读写、状态机调度、时钟同步、错误检测等机制代码注释清晰可作为学习FPGA与外设SPI交互的参考模板。压缩包共4个文件包括2个Verilog文件、1个XML组件描述和1个TCL脚本整体仅8KB结构紧凑便于快速部署和修改。已有3417人学习借助该源码可深入理解AD9910的时序操作并能直接迁移到无线通信、雷达、测试测量等系统中提升开发效率。对初学者而言状态机设计和SPI时序控制部分尤其值得反复研读能有效缩短FPGA驱动开发的学习曲线。 以前我调试射频板卡最怕的不是算法而是把一个高性能DDS芯片从休眠状态唤醒。AD9910这颗1 GSPS采样率的DDS集成14位DAC理论上只要你把40位频率字写进去它就能输出一路干净的连续波信号。可实际上从FPGA到AD9910之间有一层厚厚的寄存器、时序和状态机要处理很多项目就卡在这一步。下面这篇是我在一个信号源项目里基于FPGA写AD9910驱动模块的完整复盘包括SPI指令字设计、状态机实现、单音和扫频配置流程以及调试过程中踩过的坑适合正在写DDS驱动或者准备把FPGA和射频芯片打通的朋友。1. AD9910到底是什么芯片FPGA为什么非要掺一脚1.1 一颗自带DAC的DDS核心是相位累加器DDS的原理可以用一句话概括用一个相位累加器在数轴上均匀迈步步长越大输出频率越高。AD9910内部有一个40位的相位累加器每个SYSCLK周期累加一次FTWFrequency Tuning Word累加器的高位把完整正弦波的一个周期切分成很多相位点再通过查找表转成幅度码最后交给内部的14位DAC输出模拟信号。正因为这个架构AD9910的调频速度和相位连续性是传统锁相环方案比不了的。锁相环换频率往往要几10微秒甚至毫秒级而且涉及环路锁定和稳定时间DDS只要改一次FTW并完成I/O_UPDATE输出频率立刻切换相位还是连续的。这在跳频通信、雷达目标模拟、接收机本振扫频这类场景里非常关键。1.2 单片机能写寄存器但场景复杂时真不行很多人会问控制AD9910不就是往SPI寄存器里写值吗单片机不行吗行但如果你的系统里还有其他高速逻辑比如FPGA要做并行调制、要跟着外部触发信号实时切Profile、要同步多个DDS芯片单片机的串行执行和定时抖动就很要命。我这次选择FPGA的原因很简单AD9910的Profile切换、并口调制、RAMP扫频方向控制这些信号都是纳秒到微秒级的硬件管脚操作FPGA天然适合。而且FPGA和AD9910之间除了SPI还有I/O_UPDATE、RESET、外部触发、并行数据口这些协作信号放在一个时钟域里管理比单片机干净得多。1.3 硬件连线清单先摆清楚再写代码我在FPGA和AD9910之间的连接是这样规划的控制类信号全部从FPGA的3.3V bank引出信号方向以FPGA为参考功能说明SCLK输出SPI时钟建议先跑慢一点比如5MHzSDIO双向数据线写模式用输出读模式切输入SDO输入是否使用取决于你想不想做读回校验CSB输出片选低有效整帧传输期间保持低I/O_UPDATE输出寄存器更新脉冲写完配置后必须给一下RESET输出上电复位具体极性以板级设计为准PROFILE0~3输出Profile选择脚多配置场景使用DIRECTION输出RAMP扫频方向控制配合DRG使用除了控制脚供电和输出端也要注意。AD9910的模拟电源和数字电源最好分开走DAC输出是差分电流型我一般通过宽带变压器转成单端再接一个低通滤波器否则你会在示波器上看到一堆谐波和时钟杂散。参考时钟输入如果是单端另一脚记得按手册要求接地或加电容别悬空。2. SPI通信协议驱动好不好写全看指令字理解得透不透2.1 AD9910的16位指令字和普通SPI外设不一样AD9910的SPI指令字不是常见的8位而是16位。最高位W/R表示读写方向紧接着两位N[1:0]表示本次要传输的数据字节数然后是5位寄存器地址剩下位是保留位。这一点刚上手时很容易踩坑你用惯了8位指令的Flash或者温度传感器看到AD9910会下意识按8位去发寄存器地址全错位芯片根本没反应。多字节传输时数据部分MSB在前一次最多可以携带几个字节取决于N字段的编码具体对应关系要在手册里查。要特别提醒的是AD9910的40位FTW在寄存器映射表里是被拆到两个寄存器区域里的不是一条命令能全部写完的。所以写频率字前先把手册里的寄存器映射表打印出来把FTW高字节、FTW低字节、相位字、幅度字的地址圈出来再设计发送顺序。2.2 SPI模式和时钟模式0起步频率宁低勿高AD9910支持SPI模式0和模式3我统一用模式0也就是SCLK空闲为低、数据在上升沿采样、下降沿切换。只要主机从机一致用哪个模式都行但整个工程里不要混用否则调试时你会怀疑人生。SPI时钟频率方面AD9910确实能跑挺快但FPGA这边如果从100MHz系统时钟分频直接除20得到5MHz既稳定又留足裕量。我建议第一次上板先把SPI时钟降到1MHz左右跑通配置再用逻辑分析仪确认时序最后再提速。很多时候调试不出来不是逻辑写错而是SPI频率太高线缆或PCB走线寄生电容把沿搞缓了。CSB和SCLK的关系也值得强调CSB要在SCLK空闲状态下拉低整帧传完后再拉高拉高后至少保持一个SCLK周期的空闲时间。如果CSB提前拉高芯片可能只收到半个字节寄存器数据错乱而且这种错乱往往是“偶尔发生一次”特别难排查。2.3 读回校验给自己留一条后路很多DDS驱动只写不读配置完看输出能出波形就完事。但我的建议是只要硬件上把SDO引到FPGA就一定实现读回校验。读回能帮你区分“寄存器没写进去”和“写进去了但配置本身不对”这两个问题的排查路径完全不一样。读操作的时序和写操作有个区别有些SPI从机在指令字发完后数据线上会先出现一个或两个dont care位然后再输出有效数据。AD9910读回时也有类似的机制具体多少个空闲周期一定要参考手册里的时序图别想当然。我的做法是先发一个16位读指令然后连续采回预定个数的字节前几拍丢弃从有效位开始拼成寄存器值再和写入的期望值做比较。3. FPGA驱动架构一个主控状态机加三个可复用模块3.1 模块划分SPI主机和配置控制器分开写AD9910驱动逻辑我不建议全堆在一个文件里最少拆成两个模块一个底层的spi_master负责SCLK产生、字节收发、片选时序一个高层的ad9910_ctrl负责上电复位流程、寄存器配置序列、I/O_UPDATE触发逻辑。将来如果要把同一套SPI主机拿过去控制AD9959或者其他SPI射频芯片只要改高层控制器就行底层完全不用动。spi_master的对外接口大概长这样clk、rst_n、start、wr_data、bit_num、cs_n、sclk、sdo、done。它只做一件事把start拉高后传入的数据按指定比特数发出去发完拉done。状态机越简单越好一定不要在底层模块里塞业务逻辑否则换芯片时你还要小心翼翼拆逻辑。3.2 主控状态机建议至少包含这四个状态配置控制器的状态机我设计为IDLE、WR_CMD、WR_DATA、IO_UPDATE。状态转移逻辑是这样的IDLE状态下等待配置命令收到写寄存器请求后进入WR_CMD此刻把16位指令字交给spi_master发送发送完成进入WR_DATA把数据字节逐个发出去发完回到IDLE或者进入下一条命令。如果整个配置序列执行完毕状态机进入IO_UPDATE拉出一个宽度约为几百纳秒的更新脉冲再等待固定延时后回到IDLE。这个状态很有必要单独拉出来因为AD9910的缓冲寄存器和有效寄存器是分开的配置值先写到缓冲寄存器必须收到I/O_UPDATE信号后才生效。我第一次写驱动时忽略了这个细节SPI波形完全正确寄存器读回也对就是没有任何射频输出折腾了半天才想起来I/O_UPDATE没发。3.3 I/O_UPDATE和状态机的联动时机I/O_UPDATE脉冲的宽度和触发时机都要注意。脉冲太窄芯片可能采不到太宽又会把后面紧跟的配置冲掉。我一般用系统时钟产生一个约500ns的高脉冲然后进入一个延时状态等上至少1us再开始下一条命令。还要考虑一个工程细节如果I/O_UPDATE和SPI时钟不在同一个时钟域建议做一次打拍同步避免亚稳态。我习惯把SPI的SCLK专门用一个时钟使能信号来生成而不是另起一个时钟域这样整个状态机仍然是单时钟设计时序约束好做仿真也方便。3.4 把配置序列做成表代码看起来才像工程AD9910的初始化配置往往有十几条寄存器命令我建议把这些命令组织成一张配置表每条记录包含寄存器地址、数据值、字节数。状态机每执行完一条就从表里取下一条。想改输出频率就改表里的对应项不需要改状态机逻辑。配置表还可以配合一个简单的解析模块实现“动态更新”比如上电把Profile0的FTW配置成A频率运行过程中根据上位机指令重新计算FTW再插入一条写命令最后补一个I/O_UPDATE。这样FPGA端不会因为改频率而重新综合灵活性高很多。4. 单载波输出配置从寄存器到波形完整走一遍4.1 上电复位流程顺序不能乱AD9910的上电配置顺序我是这样安排的先给FPGA和AD9910上电然后拉RESET信号保持几个毫秒再释放让芯片内部寄存器回到默认状态等待一段时间等参考时钟稳定再开始SPI初始化。初始化时优先处理几件事关闭DAC掉电模式确认DAC处于正常工作状态配置系统时钟来源如果使用内部PLL倍频要设置好倍频系数和参考时钟分频并预留PLL锁定时间把芯片工作模式设置为需要的模式比如单音Profile模式或者RAMP模式最后才是写Profile0的频率、相位、幅度寄存器。配置序列的先后顺序不能乱。如果先写Profile寄存器后配置系统时钟芯片可能按错误的SYSCLK频率去解释FTW输出频率就会整体偏移。而且PLL未锁定之前SPI寄存器写操作本身也可能受参考时钟不稳定影响尽量等锁定后再写数据类寄存器。4.2 频率字、相位字、幅度字的换算公式AD9910的频率输出公式是f_out SYSCLK × FTW / 2^40反过来算FTWFTW round(f_out × 2^40 / SYSCLK)这个公式的本质就是相位累加器每周期步进多少个最小相位间隔最小频率分辨率等于SYSCLK除以2^40。SYSCLK越高分辨率越高但功耗也越大。相位字POW是16位对应360度相位范围POW phase × 2^16 / 360幅度字ASF是14位对应满量程幅度ASF amplitude_ratio × 2^14这几个换算我以前在调试时分不清该用2^40还是2^32后来直接用Python脚本算再手动核对一遍十六进制结果就很少出错了。def calc_ftw(f_out, sysclk): return round(f_out * (2**40) / sysclk) def calc_pow(phase_deg): return int(round(phase_deg * (2**16) / 360)) def calc_asf(ratio): return int(round(ratio * (2**14))) sysclk 1_000_000_000 f_out 100_000_000 ftw calc_ftw(f_out, sysclk) print(hex(ftw)) # 0x199999999a算出来的FTW是0x199999999A40位写进AD9910时要按寄存器映射表拆成高、低部分分别发送不能直接当普通32位数据一次写完。4.3 配置Profile0让芯片输出100MHz单音模式下芯片的输出频率主要由Profile0决定。我的配置流程是先写Profile0的幅度字和相位字如果不需要调制幅度可以设满量程相位设0再写频率字高字节和低字节最后检查Profile选择管脚的状态确保当前选中的就是Profile0。写完后发一次I/O_UPDATE把缓冲寄存器里的配置全部生效。我说的“全部”是真的全部包括幅度、相位、频率、工作模式所以建议把所有初始化参数都排好顺序一次性写完再更新不要在配置过程中反复更新容易把还没写完整的参数提前生效输出会在一瞬间蹦出不想要的频率。配置完成后用示波器探头接DAC输出端正常情况下应该能看到正弦波频率用示波器的频率测量功能读一下。如果探头直接接在DAC差分输出上看到信号畸变大概率是没加变压器或没做单端转换或者滤波网络没接。4.4 输出验证别只看波形好不好看有波形只是第一步还要验证频率准不准。普通示波器测频率本身有误差如果要较真得用频谱仪看峰值频率和杂散。我调试时发现示波器显示99.91MHz频谱仪显示100.0001MHz这种差异是正常的不要自己吓自己。如果频谱仪测出来的频率和设定值差了一个固定比例比如设定100MHz实际输出200MHz或者50MHz基本可以断定SYSCLK不是你预期值要么是PLL倍频系数配置错了要么是参考时钟本身频率不对。这时候优先查时钟树配置而不是反复检查FTW。5. 从单音到扫频RAMP模式扩展思路和实测踩坑5.1 RAMP模式怎么用FPGA控制单音模式只是AD9910的基本功它真正好用的是RAMP扫频模式也就是数字斜坡发生器DRG。工作方式是把Profile0作为扫频起始频率Profile1作为扫频结束频率DRG模块按你设定的步进和速率从起始往结束方向走走到底后可以回扫、可以暂停、也可以停在目标频率。FPGA端主要做三件事通过SPI配置Profile0和Profile1的FTW配置DRG的正向步长、逆向步长、更新速率等参数通过DIRECTION管脚控制扫频方向需要扫频时拉一个使能信号。扫频步长和速率怎么配取决于你要的扫频时间和频率间隔。比如要从100MHz扫到200MHz总共需要多少毫秒芯片内部每个DRG时钟周期更新一次频率把这个时间对应到DRG寄存器设置值上。这块公式比较绕我的建议是先配一个大步长快速验证功能确认扫频方向和控制信号都正常后再切换到目标参数。5.2 实测中三个高频坑按排查难度排序第一个坑是I/O_UPDATE缺失或者更新脉冲时序不对。这个问题最常见也最隐蔽SPI波形完美、寄存器读回无误但输出没有任何变化。我在调试时加了逻辑分析仪抓I/O_UPDATE引脚才看到更新脉冲宽度只有几十纳秒芯片没采到加宽到几百纳秒后一切正常。第二个坑是FTW拆分和拼接的顺序错了。40位FTW拆成两部分写入必须严格按照寄存器映射来高位部分写到高地址还是低地址不能凭感觉。我犯过一次错把高16位和低24位写反了输出频率等于完全随机的值而且用示波器看还稳定得很很难意识到是字节序问题。第三个坑是SYSCLK和FTW失配。比如外部参考时钟实际只有99MHzPLL倍频配置是按100MHz算的最后SYSCLK和预期差了1%输出频率也跟着差1%。这种误差示波器不容易发现频谱仪上一眼就能看出来。建议通过控制寄存器里的锁定状态位或者参考时钟监视功能确认时钟状态再优化FTW。5.3 调试手段组合拳状态机内部加探针外部抓总线RAMP模式比单音模式复杂我调试时习惯同时在FPGA内部和外部抓信号。FPGA内部用Vivado的ILA或者Quartus的SignalTap抓状态机的当前状态、SPI移位计数器的值、配置表地址、I/O_UPDATE脉冲的位置。芯片外部用逻辑分析仪抓SCLK、CSB、SDIO、I/O_UPDATE、DIRECTION这些管脚。抓完后把两边的波形做时间对齐看状态机的每一条命令是否完整走到了芯片引脚上。这个习惯帮我解决了很多“看起来SPI发了但芯片没收全”的问题。比如有一次我发现CSB在最后一字节的最后一个SCLK边沿之前就拉高了检查代码发现是状态机提前退出了DATA状态少了两个周期的保持时间。这类问题如果只盯着输出信号看永远都发现不了。逻辑分析仪的采样率至少要达到SPI时钟的4倍以上建议直接上20MHz采样率把整个配置过程完整录下来慢慢缩放看。一旦把SPI帧结构、I/O_UPDATE位置、寄存器刷新时机三者对照清楚AD9910对你来说基本就不算黑盒了。我后来把这套SPI主机模块直接拿过去控制AD9959只改了指令字长度和寄存器映射表就正常工作了。所以别把精力全花在“调通某一颗芯片”上底层通信模块做扎实上层配置逻辑做到可读可改以后再遇到其他DDS或者SPI类型的射频芯片起点会高很多。本文还有配套的精品资源点击获取