ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AD9910 SPI寄存器配置实战:从官方软件到Verilog的完整指南

AD9910 SPI寄存器配置实战:从官方软件到Verilog的完整指南 1. 为什么AD9910的SPI配置值得单独写一篇实战总结AD9910这颗芯片搞射频信号生成、雷达回波模拟、任意波形输出的朋友应该都不陌生。它是一颗14位、1GSPS采样率的直接数字频率合成器DDS内部集成了32位频率调谐字、14位相位偏移、10位幅度缩放还有RAM播放、数字斜坡发生器、多芯片同步这些高级功能。但真正让很多人卡住的不是DDS原理而是它的SPI寄存器配置——寄存器数量多、位域分散、上电默认值不一定符合预期而且官方评估软件生成的配置和FPGA里手写的Verilog时序经常对不上。我自己前前后后做过四五个基于AD9910的项目从最开始的单频点输出到后来的线性调频、RAM调制、多片同步几乎每一种功能都踩过SPI配置的坑。最典型的一次是用官方软件生成了一组寄存器值直接搬到FPGA里结果输出频谱里出现了莫名其妙的杂散查了两天才发现是CFR2寄存器里某个使能位没配对导致内部PLL没有正确锁定。这种问题官方数据手册不会直接告诉你只有实际调过才知道。这篇内容适合三类人看第一类是用STM32或者FPGA驱动AD9910但SPI写进去没反应的第二类是想从官方评估软件过渡到自己写Verilog驱动不知道哪些寄存器必须配、哪些可以偷懒的第三类是做多片AD9910同步或者RAM模式需要精确控制寄存器写入顺序的。我会从SPI协议本身讲起然后拆解官方软件生成的配置再给出可直接复现的Verilog实现最后把常见问题和排查方法整理成速查表。2. AD9910的SPI协议到底特殊在哪里2.1 四线SPI的时序特征与普通SPI的差异AD9910的SPI接口支持三线和四线模式但实际项目里几乎都用四线SCLK、SDIO、SDO、CS。它和常见的SPI Flash、传感器SPI最大的区别在于AD9910的寄存器地址是7位数据是32位而且指令周期和数据周期是分开的。一次完整的写操作是先发一个字节的指令最高位是读写标志低7位是寄存器地址然后再发4个字节的数据。读操作类似但SDO会在数据周期输出寄存器内容。时序上AD9910的SCLK最高可以到70MHz左右但实际用FPGA驱动时我一般会把SCLK控制在10MHz到25MHz之间。原因很简单AD9910的SPI接口对建立时间和保持时间有要求尤其是SDIO在SCLK上升沿采样如果PCB走线较长或者有容性负载太高的时钟会导致数据眼图变差。我实测过在20MHz以下用普通IO口直接驱动误码率几乎为零超过40MHz就需要考虑阻抗匹配和走线等长了。还有一个容易忽略的点AD9910的CS信号在指令周期和数据周期之间不能拉高。也就是说一次写操作必须在一个CS低电平周期内完成中间不能断开。我见过有人用软件片选每发一个字节就拉高CS结果寄存器完全写不进去。这一点和某些SPI设备不同必须注意。2.2 指令字节的位域定义与读写标志指令字节的格式是这样的Bit7是R/W标志1表示读0表示写Bit6到Bit0是寄存器地址。比如要写寄存器0x00CFR1指令字节就是0x00要读寄存器0x00指令字节就是0x80。这里有个细节AD9910的寄存器地址是7位但实际可寻址范围是0x00到0x1F共32个寄存器。地址超过0x1F的芯片会忽略。数据周期是32位MSB先发。对于写操作SDIO在SCLK上升沿被采样对于读操作SDO在SCLK下降沿更新主机在上升沿采样。这个相位关系在Verilog里实现时需要根据FPGA的时钟极性来调整。我一般用SCLK的上升沿作为数据输出沿下降沿作为数据采样沿这样和AD9910的时序要求匹配。注意AD9910的SDO在非读操作时是高阻态如果总线上有多个从设备需要确保CS拉高时SDO不会冲突。实际项目中我通常只挂一片AD9910所以SDO直接接FPGA的输入引脚不需要外部上拉。2.3 寄存器映射概览哪些必须配哪些可以默认AD9910的32个寄存器里真正每次上电都必须配置的其实只有几个CFR1、CFR2、CFR3、FTW、POW、ASF。其他的如RAM寄存器、数字斜坡寄存器只有在使用对应功能时才需要配置。但这里有个坑CFR2里的PLL使能位和PLL倍频系数如果配错了整个芯片的时钟系统就不工作SPI读写也会异常。我整理了一个必须配置的寄存器清单寄存器地址名称关键位域典型值作用0x00CFR1Bit31-240x00控制RAM、数字斜坡、OSK等0x01CFR2Bit23-160x1FPLL倍频、SYNC、PDCLK等0x02CFR3Bit15-80x3FDAC满量程电流、VCO范围0x04FTWBit31-0计算值频率调谐字0x05POWBit31-160x0000相位偏移0x06ASFBit31-160x3FFF幅度缩放因子这个表里的典型值是基于100MHz参考时钟、内部PLL倍频到1GHz的配置。实际使用时FTW需要根据目标输出频率计算FTW round(f_out * 2^32 / f_sysclk)。比如要输出100MHzf_sysclk1GHzFTW 100e6 * 2^32 / 1e9 429496729.6取整就是0x1999999A。3. 官方评估软件生成的配置怎么读懂3.1 官方软件界面与寄存器导出功能ADI官方提供了一个叫AD9910 Evaluation Software的PC端工具可以通过USB转SPI板子直接配置芯片。这个软件最大的价值不是让你用PC控制而是它可以生成寄存器配置文件。在软件里设置好频率、幅度、扫描模式后点击“Export”或者“Save”按钮会生成一个文本文件里面列出了所有寄存器的地址和值。但很多人拿到这个文件后直接懵了为什么有些寄存器地址是0x00、0x01、0x02有些却是0x0E、0x0F而且同一个寄存器软件里显示的值和实际写入的值可能不一样。这是因为官方软件在导出时会把所有寄存器都列出来包括那些默认值不需要修改的。实际写入时只需要写你关心的那几个。我一般会这样做先在软件里配置好目标功能然后导出配置文件再用一个简单的Python脚本解析这个文件提取出非默认值的寄存器。这样就能得到一份精简的配置列表。比如配置一个100MHz单频输出导出的文件里可能有20多个寄存器但实际需要写的只有6个。3.2 从配置文件提取关键寄存器值的脚本方法下面这个Python脚本是我常用的用来解析官方软件导出的寄存器文件import re def parse_ad9910_config(file_path): registers {} with open(file_path, r) as f: for line in f: # 匹配类似 0x00 0x00000000 的行 match re.match(r(0x[0-9A-Fa-f])\s(0x[0-9A-Fa-f]), line.strip()) if match: addr int(match.group(1), 16) value int(match.group(2), 16) registers[addr] value return registers def filter_non_default(registers): # 默认值参考数据手册 defaults { 0x00: 0x00000000, 0x01: 0x00000000, 0x02: 0x00000000, 0x03: 0x00000000, 0x04: 0x00000000, 0x05: 0x00000000, 0x06: 0x00000000, 0x07: 0x00000000, } non_default {} for addr, value in registers.items(): if addr in defaults and value ! defaults[addr]: non_default[addr] value elif addr not in defaults: non_default[addr] value return non_default if __name__ __main__: regs parse_ad9910_config(ad9910_config.txt) filtered filter_non_default(regs) for addr, value in sorted(filtered.items()): print(f寄存器 0x{addr:02X}: 0x{value:08X})这个脚本跑完你就能得到一份精简的寄存器列表。但要注意官方软件导出的值有时候是“显示值”不是“写入值”。比如幅度缩放因子软件里显示的是0到1的浮点数但实际写入的是14位整数。这个转换需要你自己做。3.3 官方配置与手动配置的差异对比官方软件生成的配置和手动配置最大的差异在于CFR2寄存器。官方软件在初始化时会先写一个默认的CFR2值然后再根据你的设置修改。但手动配置时如果你只写了修改后的值可能会漏掉一些隐含的使能位。举个例子官方软件在配置PLL时会同时设置CFR2的Bit23PLL使能和Bit22-20PLL倍频系数。但如果你手动写CFR2只设置了倍频系数忘了使能PLL芯片就会用参考时钟直接作为系统时钟输出频率就完全不对了。我对比过官方软件和手动配置的差异整理成下表配置项官方软件行为手动配置常见错误正确做法PLL使能自动置位Bit23忘记置位写CFR2时确保Bit231DAC满量程根据幅度设置自动计算直接用默认值根据目标幅度计算CFR3的Bit15-8输出使能自动置位CFR1的Bit16忘记置位写CFR1时确保Bit161SYNC时序自动配置忽略多片同步时必须配提示如果你不确定某个寄存器的值最稳妥的方法是先用官方软件配置好导出文件然后逐条对比。不要凭感觉写。4. Verilog实现SPI写寄存器的完整流程4.1 顶层模块设计与状态机划分用Verilog驱动AD9910的SPI核心是一个状态机。我一般把状态机分成几个状态IDLE、SEND_INSTR、SEND_DATA、WAIT、DONE。每个状态对应一个时钟周期或者多个时钟周期具体取决于SCLK的分频系数。顶层模块的接口包括系统时钟clk、复位rst_n、启动信号start、寄存器地址addr、寄存器数据data、SPI物理接口sclk、sdio、cs_n。内部需要一个时钟分频器把系统时钟分频到合适的SCLK频率。比如系统时钟50MHzSCLK目标10MHz分频系数就是5。状态机的转移逻辑是这样的IDLE状态下当start信号拉高锁存addr和data进入SEND_INSTRSEND_INSTR状态下把指令字节{1b0, addr}逐位输出到SDIO同时产生SCLKSEND_INSTR完成后进入SEND_DATASEND_DATA状态下把32位数据逐位输出SEND_DATA完成后进入WAITWAIT状态下拉高CS等待几个时钟周期然后进入DONEDONE状态下产生一个完成脉冲回到IDLE。这里有个细节SCLK在IDLE状态下应该保持低电平CS保持高电平。在SEND_INSTR和SEND_DATA状态下CS保持低电平SCLK在数据稳定后产生上升沿。我一般用SCLK的下降沿更新SDIO上升沿让AD9910采样。这样SDIO在SCLK上升沿之前已经稳定了。4.2 时钟分频与SCLK相位控制时钟分频器我一般用一个计数器实现reg [7:0] clk_div_cnt; reg sclk_reg; always (posedge clk or negedge rst_n) begin if (!rst_n) begin clk_div_cnt 8d0; sclk_reg 1b0; end else if (spi_active) begin if (clk_div_cnt DIV_VALUE - 1) begin clk_div_cnt 8d0; sclk_reg ~sclk_reg; end else begin clk_div_cnt clk_div_cnt 1b1; end end else begin clk_div_cnt 8d0; sclk_reg 1b0; end endDIV_VALUE决定了SCLK的频率。如果系统时钟是50MHzDIV_VALUE5那么SCLK就是10MHz。但要注意SCLK的占空比不一定是50%因为计数器从0到DIV_VALUE-1翻转一次。如果DIV_VALUE是奇数占空比会略有偏差但AD9910对占空比要求不严格一般没问题。相位控制方面我一般让SDIO在SCLK的下降沿更新这样在上升沿时数据已经稳定了至少半个SCLK周期。实现方法是在SCLK下降沿对应的时钟周期更新SDIO的值。具体来说当clk_div_cnt等于DIV_VALUE-1且sclk_reg从1变0时更新SDIO。4.3 指令与数据发送的位操作细节发送指令和数据时需要一个位计数器。指令是8位数据是32位总共40位。我一般用一个6位计数器从0到39。当计数器小于8时发送指令位大于等于8时发送数据位。指令位的取值如果是写操作最高位是0低7位是地址。所以指令字节是{1b0, addr[6:0]}。数据位的取值从data[31]到data[0]MSB先发。这里有个容易出错的地方位计数器的递增时机。我一般让位计数器在SCLK的上升沿递增这样在下一个下降沿更新SDIO时位计数器已经指向下一位了。具体代码always (posedge clk or negedge rst_n) begin if (!rst_n) begin bit_cnt 6d0; sdio_reg 1b0; end else if (spi_active) begin if (sclk_reg 1b0 clk_div_cnt DIV_VALUE - 1) begin // SCLK下降沿更新SDIO if (bit_cnt 6d8) begin sdio_reg instr_byte[7 - bit_cnt]; end else begin sdio_reg data_reg[39 - bit_cnt]; end end if (sclk_reg 1b1 clk_div_cnt DIV_VALUE - 1) begin // SCLK上升沿递增位计数器 bit_cnt bit_cnt 1b1; end end end这段代码的逻辑是在SCLK下降沿更新SDIO在SCLK上升沿递增位计数器。这样AD9910在上升沿采样时SDIO已经稳定了。4.4 多寄存器连续写入的时序安排实际项目中上电后需要连续写多个寄存器。我一般用一个简单的ROM或者查找表存储寄存器地址和值的序列然后依次启动SPI写操作。每次写操作完成后等待几个时钟周期再启动下一次。这里有个关键点AD9910的寄存器写入后需要一定的时间才能生效。尤其是CFR2里的PLL配置写入后需要等待PLL锁定通常需要几十微秒。如果写完CFR2后立刻写FTW可能会因为PLL还没锁定而导致输出频率不对。我一般会在写完CFR2后插入一个延时计数器等待1ms左右再写其他寄存器。连续写入的Verilog实现可以用一个状态机状态包括LOAD、WRITE、WAIT、NEXT。LOAD状态下从ROM读取地址和值WRITE状态下启动SPI写WAIT状态下等待SPI完成NEXT状态下地址递增判断是否写完所有寄存器。5. 寄存器配置的典型场景与参数计算5.1 单频点输出FTW与ASF的计算单频点输出是最基础的功能。需要配置的寄存器CFR1、CFR2、CFR3、FTW、POW、ASF。其中FTW的计算公式是FTW round(f_out * 2^32 / f_sysclk)。f_sysclk是系统时钟由参考时钟和PLL倍频决定。举个例子参考时钟100MHzPLL倍频10倍f_sysclk1GHz。要输出100MHzFTW 100e6 * 2^32 / 1e9 429496729.6取整为429496730十六进制是0x1999999A。ASF是幅度缩放因子14位范围0到0x3FFF。如果要用满幅度ASF0x3FFF。如果要输出一半幅度ASF0x1FFF。注意ASF不是线性对应幅度的实际输出幅度和ASF的关系需要查数据手册的曲线。POW是相位偏移16位范围0到0xFFFF对应0到360度。如果不需要相位偏移POW0x0000。5.2 线性调频与数字斜坡模式的寄存器配置线性调频需要用到数字斜坡发生器。需要配置的寄存器CFR1的Bit21-20数字斜坡使能、CFR2的Bit14-12数字斜坡步进、数字斜坡限值寄存器0x0B到0x0E、数字斜坡步进寄存器0x0F到0x12。数字斜坡的工作原理是FTW从起始值开始每个时钟周期增加一个步进值直到达到上限值然后复位到起始值。步进值决定了调频斜率上限值决定了调频带宽。参数计算假设f_sysclk1GHz起始频率100MHz终止频率200MHz调频时间1ms。起始FTW 100e6 * 2^32 / 1e9 0x1999999A终止FTW 200e6 * 2^32 / 1e9 0x33333333。FTW差值 0x19999999。调频时间1ms对应1e6个时钟周期1GHz时钟。步进值 0x19999999 / 1e6 ≈ 429.5取整为430十六进制0x1AE。数字斜坡的步进值写入0x0F到0x12限值写入0x0B到0x0E。注意这些寄存器的写入顺序有要求一般先写限值再写步进最后使能数字斜坡。5.3 RAM模式下的波形播放配置RAM模式可以播放自定义波形。AD9910内部有1024x32位的RAM可以存储频率、相位、幅度或者极性数据。配置RAM模式需要CFR1的Bit31-24RAM使能、RAM目标、RAM播放模式、RAM限值寄存器0x16到0x19、RAM数据寄存器0x1A到0x1D。RAM播放模式有几种直接播放、连续循环、单次触发。我一般用连续循环模式配置CFR1的Bit23-22为01。RAM目标选择幅度、频率或相位配置CFR1的Bit21-20。RAM数据的写入比较特殊需要先写RAM限值然后写RAM数据。RAM数据是32位但实际有效位数取决于RAM目标。比如幅度是14位频率是32位相位是16位。注意RAM模式下的SPI写入速度会影响波形播放的连续性。如果RAM数据没写完就开始播放会出现波形断裂。我一般先把所有RAM数据写完再使能RAM播放。6. 常见问题与排查技巧实录6.1 SPI写不进去的几种典型原因SPI写不进去是最常见的问题。我遇到过的情况有CS信号在指令和数据之间拉高了、SCLK频率太高导致数据采样错误、SDIO方向控制不对、寄存器地址写错了。排查方法先用示波器或者逻辑分析仪抓SPI波形看指令字节和数据字节是否符合预期。如果波形没问题再读回寄存器值看是否和写入值一致。如果读回值不对可能是芯片没正常工作检查电源和参考时钟。还有一个隐蔽的问题AD9910的SPI接口在复位后需要一定的时间才能工作。如果上电后立刻写寄存器可能会失败。我一般会在上电后等待10ms再开始SPI写操作。6.2 输出频谱杂散的寄存器相关排查输出频谱有杂散很多时候是寄存器配置不当导致的。常见原因CFR2的PLL倍频系数设置不当导致系统时钟有抖动CFR3的DAC满量程电流设置过大导致DAC非线性ASF设置过小导致量化噪声增大。排查方法先用官方软件配置同样的参数看输出频谱是否正常。如果官方软件正常说明是手动配置的问题。然后逐条对比寄存器值找出差异。我遇到过CFR2的Bit19SYNC使能被误置位导致输出频谱出现周期性杂散。6.3 多片AD9910同步的配置要点多片AD9910同步需要配置SYNC信号。CFR2的Bit19是SYNC使能Bit18-16是SYNC时序。多片同步时所有芯片的SYNC引脚连在一起由主芯片或者FPGA产生同步脉冲。配置要点所有芯片的CFR2必须一致尤其是PLL倍频系数和SYNC时序。同步脉冲的宽度和间隔需要根据数据手册计算。我一般用FPGA产生一个周期性的同步脉冲宽度为几个时钟周期间隔为几十个时钟周期。提示多片同步时SPI写入顺序很重要。一般先写所有芯片的CFR2再写其他寄存器。如果先写其他寄存器再写CFR2可能会导致同步失败。6.4 常见问题速查表问题现象可能原因排查方法解决方法SPI写不进去CS在指令和数据之间拉高抓SPI波形确保CS在整个写周期保持低输出频率不对FTW计算错误重新计算FTW用公式FTW f_out * 2^32 / f_sysclk输出幅度不对ASF设置错误检查ASF值确保ASF在0到0x3FFF之间频谱杂散大PLL配置不当对比官方软件配置检查CFR2的PLL倍频系数多片不同步SYNC配置不一致检查所有芯片的CFR2确保CFR2完全一致读回值不对SDO方向控制错误检查FPGA的IO方向读操作时SDO设为输入上电无输出复位时间不足检查上电时序上电后等待10ms再配置7. 从官方软件到Verilog的迁移经验7.1 配置文件的自动化转换思路官方软件导出的配置文件是文本格式可以写脚本自动转换成Verilog的ROM初始化文件。我一般用Python脚本读取配置文件提取非默认值的寄存器然后生成一个Verilog的case语句或者ROM初始化文件。转换时要注意官方软件的值是32位十六进制Verilog的ROM也是32位。地址是7位但实际只用到0x00到0x1F。生成的ROM深度可以是32每个地址对应一个32位值。7.2 上电初始化序列的推荐顺序上电初始化序列我一般按这个顺序先写CFR3DAC满量程电流再写CFR2PLL配置等待PLL锁定再写CFR1功能使能最后写FTW、POW、ASF。这个顺序的原因是CFR3影响DAC的模拟性能先配置好CFR2影响系统时钟必须等PLL锁定后再配置其他寄存器CFR1影响功能使能最后配置。等待PLL锁定的时间我一般用1ms。如果参考时钟频率较低可能需要更长时间。可以用一个计数器实现计数到50e350MHz时钟1ms后继续。7.3 实测验证与调试手段实测验证时我一般先用示波器看AD9910的输出引脚确认有信号输出。然后用频谱分析仪看频谱确认频率和幅度正确。如果条件有限可以用一个简单的LC滤波器加检波二极管看输出幅度是否随ASF变化。调试时我习惯先配置一个简单的单频输出确认SPI和基本功能正常再逐步增加复杂度。不要一上来就配RAM模式或者数字斜坡那样出了问题很难定位。提示AD9910的输出是电流型DAC输出需要外接一个电阻到地把电流转换成电压。这个电阻的值会影响输出幅度一般用50欧姆或者100欧姆。8. 写在最后的一些实操体会AD9910的SPI配置说难不难说简单也不简单。核心就三点时序要对、寄存器值要算对、初始化顺序要合理。我刚开始做的时候总觉得官方软件生成的配置太复杂想自己简化结果踩了不少坑。后来发现官方软件的配置虽然冗余但至少是经过验证的先照搬再逐步精简是最稳妥的路径。Verilog实现方面状态机不要写得太复杂一个简单的40位发送状态机就够了。SCLK频率不要贪高10MHz到25MHz足够用稳定性比速度重要。多片同步的时候SYNC信号的走线要等长否则同步精度会受影响。最后分享一个小技巧如果你手头没有逻辑分析仪可以用FPGA的在线调试工具比如Xilinx的ILA或者Intel的SignalTap抓SPI波形。把SCLK、SDIO、CS都接到ILA上触发条件设为CS下降沿就能看到完整的写操作时序。这个方法我用了很多次比外接逻辑分析仪方便多了。
返回列表