FPGA驱动ADF4351实现3GHz射频信号源:SPI时序与Verilog代码详解
1. 为什么是Spartan-6 ADF4351项目背景与选型思路1.1 这个组合能解决什么问题如果你最近在捣鼓射频信号源大概率会碰到ADF4351这颗芯片。它的输出范围覆盖35MHz到4.4GHz内部集成了VCO、电荷泵、鉴相器和可编程分频器一颗芯片加少量外围电路就能做一个宽带信号源淘宝上几十块的ADF4351模块满天飞。但真把它用起来你会发现一个尴尬的问题ADF4351的控制接口虽然叫SPI但和常规的Flash、ADC、OLED那种SPI完全不一样它每次要写入的是32位寄存器帧而且不是写完一帧就完事至少要连续写6个寄存器才能让芯片正常工作。我手上正好有一块Alinx黑金开发板主控是Xilinx Spartan-6 XC6SLX16。这个板子当年是入门FPGA的经典选择虽然现在看起来资源不算多但做ADF4351控制绰绰有余。相比于用STM32或者ESP8266软件模拟SPIFPGA的好处是时序完全可控SPI时钟快慢自己定而且方便后续扩展扫频、跳频、甚至配合DAC做模拟调制。这个项目做完之后你可以得到一个非常干净的32位SPI发送器以后驱动其他类似的寄存器型射频芯片比如ADF4002、LMX2594的SPI模式都能直接套用。1.2 为什么不是STM32不是树莓派而是FPGA很多人的第一反应是一个SPI控制而已用单片机不香吗确实香STM32的硬件SPI跑几MHz很轻松但如果只用硬件SPI你有可能直接被ADF4351的LE信号坑到。普通SPI器件的CS是低电平有效整个传输期间保持低而ADF4351的LE是一个锁存使能脉冲必须在32位数据全部移位完成后再拉高一个高脉冲把这个帧锁存到目标寄存器里。硬件SPI的CS逻辑没法直接做到这一点通常还是得用GPIO手动控制等于半软半硬。FPGA这边就简单了一个状态机搞定所有时序想怎么拉LE就怎么拉。另外这个项目的潜台词往往是“后面要做扫频信号源”。如果用FPGA可以把一组频率对应的寄存器值存在ROM或者FIFO里按固定时间间隔逐个发送跳频间隔可以做到微秒级甚至更低。这是MCU很难做到的。Spartan-6虽然老但这种低速控制型任务完全够用而且ISE 14.7对Spartan-6支持非常成熟不会像新器件那样逼着你用Vivado。1.3 项目最终目标这篇文章不是给你讲一堆空泛的原理而是直接落地。我会从ADF4351的寄存器结构和频率计算开始理清SPI时序波形然后给出完整的Verilog SPI驱动代码和顶层例化方法最后以输出3GHz信号为例讲清楚初始化顺序、寄存器配置值以及上板调试时最容易踩的几个坑。不管你用的是黑金AX4010还是别的Spartan-6板子代码稍微改一下引脚约束就能用。2. ADF4351寄存器与频率计算动手写驱动前必须搞清的事2.1 频率公式一次把每个参数吃透ADF4351的频率关系用下面这个公式表达RF_OUT fPFD × (INT FRAC / MOD) / RF_Divider其中fPFD是鉴相频率也就是电荷泵比较的参考频率单位MHz。INT是16位整数分频系数范围75到65535。FRAC是12位小数分子。MOD是12位小数分母这个值必须大于等于2。RF_Divider是输出分频器可以选择1、2、4、8、16、32、64。而fPFD本身由参考时钟REFIN经过处理得到fPFD REFIN × (1 D) / (R × (1 T))D是参考倍频器0或1。R是10位参考分频系数范围1到1023。T是参考二分频使能0或1。初看可能有点绕但你可以把小数分频想象成切披萨MOD决定把整个披萨切成多少块FRAC决定取其中几块。如果FRAC是0那就退化成纯整数分频。这颗芯片的厉害之处在于它能通过FRAC/MOD把任意不能被整数分频整除的频率用极高精度表达出来这就是小数N分频的基本思想。2.2 手算一个3GHz配置实例假设你的ADF4351模块板载参考时钟是10MHz TCXO这是最常见的配置。我们想让输出频率为3GHz。先算fPFD。选D0R1T0那么fPFD 10 × (1 0) / (1 × (1 0)) 10MHz再看3GHz落在哪个频段。ADF4351内部VCO的调谐范围大约是2.2GHz到4.4GHz3GHz正好在这个范围内所以RF_Divider直接选1不需要用分频器降频。于是RF_OUT 10 × (INT FRAC / MOD) / 1 3GHz解出来就是 INT FRAC/MOD 300。最简单的情况下FRAC0INT300MOD随便取一个大于等于2的数比如2。注意MOD不能为1这是数据手册明确要求的否则小数分频器工作不正常。这个配置写成寄存器值就是R0 (地址0000) FRAC(12bit) INT(16bit) 0x0000012CR0的二进制展开是高4位控制字0000接着12位FRAC全0低16位INT300十六进制0x012C。所以R0的32位值就是0x0000012C。R1用来设置MOD、相位字和预分频器。MOD2放在DB27-DB16相位字全0放在DB15-DB4预分频器选择DB3。3GHz工作频率下预分频器选8/9更合适所以DB31。于是R1 0x100200082.3 一共要写6个寄存器地址别搞错ADF4351总共需要配置6个寄存器从R0到R5每个都是32位。很多中文教程把帧长写成30位这是不准确的写驱动的时候一定以数据手册为准。帧格式是先发送最高位DB31最后发送DB0。高4位DB31-DB28是寄存器地址剩下的28位是数据。寄存器控制字主要功能R50101锁定检测方式、VCO倍频控制、LD引脚功能R40100RF输出通道使能、输出功率、RF分频器、VCO选择R30011时钟分频、周期测量、复位控制R20010参考分频R、参考倍频D、参考二分频T、电荷泵电流R10001MOD、相位字、预分频器、相位调整R00000FRAC、INT、相位字初始化顺序建议按照R5、R4、R3、R2、R1、R0的顺序写。为什么最后写R0因为R0里面的INT和FRAC一旦更新PLL就会立即开始锁定新的频率。先配置好其他寄存器最后写R0相当于按下了“开始锁定”按钮这样可以避免在初始化过程中PLL反复尝试锁定无效频率花费多余时间。2.4 其他寄存器不是手算出来的R1和R0可以由频率公式直接算出来但R2、R3、R4、R5里面还包含大量控制位比如电荷泵电流、MUXOUT模式、输出功率、锁定检测配置。手算这些字段极其容易出错我强烈建议先去ADI官网下载ADF4351的评估软件软件里输入参考频率和期望输出频率它会自动生成完整的6个寄存器值。这个软件做出来的值可以直接抄进Verilog代码里比自己一个个翻数据手册拼位靠谱得多。3. 引脚连接与SPI时序最容易翻车的地方3.1 Alinx黑金开发板与ADF4351模块的接线先看一下你手里的模块。市面上的ADF4351模块引脚一般有模块引脚方向说明VCC电源输入通常接3.3V部分模块板上带LDOGND地与FPGA共地SCLK输入SPI时钟SDATA输入SPI数据LE输入锁存使能CE输入芯片使能高有效LD输出锁定指示可接LEDRF_OUT输出射频输出REFIN输入外部参考输入如果模块不带板载TCXO黑金开发板上有扩展排针把FPGA的普通IO接出来。建议至少用4根线连SCLK、SDATA、LE、CE。Spartan-6的IO是3.3V LVCMOSADF4351的控制输入也是3.3V逻辑可以直接连接不需要电平转换。需要注意CE不能悬空必须拉高否则芯片直接关断。很多人焊好了板子发现没输出一查就是CE没接。我习惯把CE直接接到FPGA的3.3V电源轨或者用FPGA一个GPIO控制这样还能实现软件断电。REFIN也要格外注意。有些模块自带TCXO引脚上会标注REFIN但内部已经接好了不用你额外供时钟有些模块必须自己从外部给参考时钟。拿到模块先看卖家资料别默认它自带晶振。3.2 ADF4351的“SPI”其实不是标准SPI这是整个项目最核心的坑。ADF4351的时序要求是SCLK空闲时为低电平。数据在SCLK的上升沿被移入芯片内部移位寄存器。每个时钟周期移入1位先送最高位。32位数据全部移完后把LE拉高产生一个高脉冲这个上升沿才把32位数据从移位寄存器锁存到目标寄存器。LE拉高后要保持一小段时间数据手册要求至少几十纳秒具体以手册为准。注意LE在整个32位传输过程中都必须保持低电平这和SPI Flash的CS完全不同。如果你用MCU的硬件SPI硬件CS会在传输开始时拉低、结束时拉高这样LE上升沿出现在第32位时钟的结束位置似乎也能用其实不对因为普通SPI的CS拉高的同时器件在CS上升沿锁存数据看起来和ADF4351的LE很像。但实际上ADF4351对LE上升沿和最后一个SCLK上升沿之间的最小间隔有要求而且很多MCU硬件SPI会在最后一个时钟采样完成后立刻释放CS间隔太短会不可靠。所以最稳妥的做法仍然是GPIO单独控制LE。3.3 电源和地是相位噪声的隐形杀手ADF4351的VCO对电源噪声非常敏感。模块的VCC最好单独走线并在靠近模块引脚的地方加一个10μF钽电容和100nF陶瓷电容滤波。开发板上的3.3V开关电源噪声可能偏大如果发现锁定正常但信号相位噪声很差考虑用低压差线性稳压器单独给模块供电。另外FPGA的IO翻转会在地线上产生电流尖峰所以模块的地线尽量短粗直接接到开发板的GND排针不要绕远路。4. Verilog SPI主控实现32位帧发送的状态机设计4.1 整体架构一个模块解决六个寄存器的顺序发送SPI主控模块的设计目标很简单给它一个start脉冲它自动把R5到R0六个寄存器按顺序写完完成后拉高done。这样顶层逻辑非常简单任何时候想重新初始化或者切换频率只需把寄存器配置表修改一下再触发一次start即可。模块内部需要一个状态机大致包含四个状态IDLE空闲等待start。SHIFT32位数据的移出过程同时产生SCLK。LATCH32位全部移完后拉高LE产生锁存脉冲。WAIT_END全部6个寄存器写完拉高done。4.2 SCLK和SDATA的时序关系下降沿换数上升沿采样ADF4351在SCLK上升沿采样数据所以SDATA必须在上升沿之前稳定下来。我采用的策略是在SCLK的下降沿更新SDATA这样有整整半个SPI时钟周期让数据稳定上升沿采样时余量充足。如果你在上升沿那拍同时改数据和翻时钟极容易出现建立时间不足尤其SPI时钟跑得比较高的时候。SCLK本身由系统时钟分频产生。比如系统时钟50MHz想要1MHz的SPI时钟就需要每25个系统时钟翻转一次SCLK。DIV_CNT这个参数用系统时钟频率除以SPI时钟频率再除以2得到。4.3 完整的Verilog代码下面这个模块是SPI主控的核心没有依赖任何厂商IP直接拷贝到ISE里就可以综合。module adf4351_spi #( parameter integer SYS_CLK_HZ 50_000_000, parameter integer SPI_CLK_HZ 1_000_000 )( input wire clk, input wire rst_n, input wire start, output reg sclk, output reg sdata, output reg le, output reg done, output reg [2:0] reg_idx ); localparam integer DIV_CNT SYS_CLK_HZ / SPI_CLK_HZ / 2; localparam IDLE 2d0; localparam SHIFT 2d1; localparam LATCH 2d2; localparam WAIT_END 2d3; reg [1:0] state; reg [31:0] shift_reg; reg [5:0] bit_cnt; reg [15:0] clk_cnt; // 根据当前寄存器索引取配置值顺序是R5,R4,R3,R2,R1,R0 reg [31:0] cur_cfg; always (*) begin case (reg_idx) 3d0: cur_cfg 32h5000000C; // R5 3d1: cur_cfg 32h40802083; // R4 3d2: cur_cfg 32h30000004; // R3 3d3: cur_cfg 32h20000440; // R2 3d4: cur_cfg 32h10020008; // R1 3d5: cur_cfg 32h0000012C; // R0 default: cur_cfg 32h00000000; endcase end always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; sclk 1b0; sdata 1b0; le 1b0; done 1b0; reg_idx 3d0; bit_cnt 6d0; clk_cnt 16d0; shift_reg 32h0; end else begin case (state) IDLE: begin sclk 1b0; le 1b0; done 1b0; reg_idx 3d0; if (start) begin shift_reg cur_cfg; bit_cnt 6d31; clk_cnt 16d0; state SHIFT; end end SHIFT: begin if (clk_cnt DIV_CNT - 1) begin clk_cnt 16d0; if (sclk 1b0) begin sclk 1b1; // SCLK上升沿ADF4351采样当前SDATA end else begin sclk 1b0; // SCLK下降沿更新下一位数据 if (bit_cnt 6d0) begin state LATCH; end else begin bit_cnt bit_cnt - 1b1; shift_reg {shift_reg[30:0], 1b0}; end end end else begin clk_cnt clk_cnt 1b1; end end LATCH: begin le 1b1; if (clk_cnt DIV_CNT - 1) begin le 1b0; clk_cnt 16d0; if (reg_idx 3d5) begin state WAIT_END; end else begin reg_idx reg_idx 1b1; shift_reg cur_cfg; bit_cnt 6d31; state SHIFT; end end else begin clk_cnt clk_cnt 1b1; end end WAIT_END: begin le 1b0; done 1b1; if (!start) begin state IDLE; end end default: state IDLE; endcase end end assign sdata shift_reg[31]; endmodule这段代码有一个细节值得讲sdata是用连续赋值从shift_reg[31]取出来的。在SHIFT状态SCLK下降沿时shift_reg左移一位新的最高位立即出现在sdata上此时SCLK处于低电平之后SCLK再拉高时数据已经完全稳定。所以ADF4351在每个SCLK上升沿看到的数据其实已经保持了半个SPI周期建立时间充足。4.4 为什么不用现成的SPI IPXilinx有SPI IP核但那个IP主要面向标准的8位、16位传输要让它发32位ADF4351帧你还得自己拼寄存器地址和数据而且LE的脉冲时序依然要自己控制。绕了一圈下来代码量和直接写状态机差不多还多了一层IP配置的麻烦。对于这种固定帧长、固定寄存器顺序的器件手写状态机反而是最简洁、最可控的方案。5. 寄存器序列与顶层整合让3GHz信号真正跑起来5.1 初始化顺序与CE时序ADF4351上电后先把CE拉高然后等待几毫秒让内部电路稳定。接着按R5、R4、R3、R2、R1、R0的顺序写入寄存器。注意在每次切换寄存器时LE需要拉低足够时间至少保证SCLK没有任何活动。我代码里LE在LATCH状态结束后直接拉低然后马上进入下一次SHIFT中间有DIV_CNT个系统时钟的低电平间隔实际测试没有问题。如果你想让FPGA重新配置频率最稳妥的做法是把CE先拉低等几十微秒再拉高然后重新走一遍初始化流程。这样能保证PLL内部状态完全复位不会出现上一个频率的残留影响。5.2 针对10MHz参考、3GHz输出的示例寄存器值下面的寄存器序列是我以10MHz参考频率、整数模式、输出3GHz为例整理的一组可用值。注意R2、R3、R4、R5这些值跟你的具体模块和参考频率有关强烈建议用ADI官方软件重新生成后再填入代码。写入顺序寄存器32位数值说明第1个R50x5000000C锁定检测等配置第2个R40x40802083使能RF_OUT_A输出功率配置第3个R30x30000004时钟分频器关闭等第4个R20x20000440参考路径配置R1第5个R10x10020008MOD2预分频器8/9第6个R00x0000012CFRAC0INT300锁定3GHz在SPI主控模块里我使用一个case语句根据reg_idx返回cur_cfg正好对应这个顺序。如果你想改成输出其他频率只需要把这个case语句里的6个值替换掉。5.3 顶层模块例化与引脚约束顶层模块非常简单把系统时钟、复位、启动按钮接进来输出三根SPI信号和状态指示即可。module adf4351_top ( input wire clk_50m, input wire rst_n, input wire btn_start, output wire spi_sclk, output wire spi_sdata, output wire spi_le, output wire led_done, output wire led_lock ); reg start_p; reg btn_d; // 简单的按键单脉冲按下一次产生一个高电平脉冲 always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin btn_d 1b0; end else begin btn_d btn_start; end end assign start_p btn_start ~btn_d; adf4351_spi #( .SYS_CLK_HZ(50_000_000), .SPI_CLK_HZ(1_000_000) ) u_spi ( .clk (clk_50m), .rst_n (rst_n), .start (start_p), .sclk (spi_sclk), .sdata (spi_sdata), .le (spi_le), .done (led_done), .reg_idx () ); // led_lock 可以接到ADF4351的LD引脚这里只作引出示意 // 实际需要再加一个输入端口读LD电平此处省略 endmoduleSpartan-6使用UCF约束文件。引脚位置要查你自己的开发板原理图我给出格式示例不要直接照抄NET clk_50m LOC V10 | IOSTANDARD LVCMOS33; NET rst_n LOC T10 | IOSTANDARD LVCMOS33; NET btn_start LOC R10 | IOSTANDARD LVCMOS33; NET spi_sclk LOC M4 | IOSTANDARD LVCMOS33; NET spi_sdata LOC M5 | IOSTANDARD LVCMOS33; NET spi_le LOC M6 | IOSTANDARD LVCMOS33;5.4 一次上电自动初始化的小改进按代码里的模式每次复位后需要按一下按钮才会写寄存器。如果想让FPGA上电后自动初始化可以把顶层里start_p改成reg init_done; wire start_auto ~init_done;然后在adf4351_spi的done信号拉高时把init_done置1。这样FPGA上电复位结束后自动开始写ADF4351整个过程不需要人工干预。实际调试时我一般两种都保留上电自动初始化一次按钮用于强制重新初始化。6. 上板实测、常见问题与扩展方向6.1 没有频谱仪怎么看信号3GHz信号示波器是看不出来的普通示波器带宽不够就算看到了也是衰减后的正弦波。最简单的验证方法是把RF分频器加大输出一个500MHz以下的信号用示波器直接观察。改动方法很简单R0和R1不变只改R4里的RF分频器位把分频系数设为16或者32输出就到187.5MHz或者93.75MHz。这个频段用示波器看得很清楚。还有一种更快的验证方式把ADF4351的LD引脚接一个LEDLD输出数字锁定指示。初始化完成后如果PLL锁定成功LD引脚输出高电平LED常亮如果PLL失锁LD会输出低电平或者周期性脉冲。我建议把LD直接接到FPGA的一个输入引脚在ModelSim仿真时看LED上板后也方便定位问题。调试阶段用LD指示灯判断锁定状态比每次翻频谱仪效率高得多。6.2 常见问题排查表现象可能原因解决办法完全没有输出CE被拉低或悬空将CE接3.3V或FPGA输出高电平没有输出寄存器地址写错检查每个帧的高4位是否和寄存器序号一致没有输出RF输出通道未使能检查R4中的输出使能位输出频率偏差大R0里INT计算错误重新核对参考频率和输出分频器PLL失锁参考频率和R2配置不匹配检查R2中的R分频、倍频和二分频配置PLL失锁电荷泵电流不合适在R2中调整电荷泵电流档位输出功率远低于预期输出功率档位设为最低调整R4中的功率设置位SPI写不进数据SDATA在上升沿不稳定检查SPI时钟频率降低到500kHz以下试试SPI写不进数据LE高脉冲太短增加LATCH状态的保持时间输出信号相位噪声大电源滤波不足模块VCC处增加LC滤波或改用LDO供电6.3 从固定频率走向扫频信号源这个SPI主控模块做完之后距离一个完整的扫频信号源只差一步把case语句里的寄存器配置表换成一个ROM或者RAMROM里存放多组频率对应的寄存器值用计数器或者FIFO按顺序读出并触发SPI发送模块。如果你做的是跳频通信测试可以在FPGA内部用异步FIFO做频率指令缓冲外部MCU通过串口下发频率表FPGA以精确时间间隔依次执行跳频。整个框架和本文的SPI主控完全兼容只需要把cur_cfg的获取逻辑从单个case改成ROM[addr]。6.4 另外提一句ESP8266能不能接ADF4351经常有人问ESP8266能不能直接连ADF4351这类SPI接口芯片。芯片本身是可以连的ESP8266的GPIO支持软件模拟SPI3.3V逻辑电平也匹配。但你要注意两点一是ESP8266的GPIO翻转速度有限SPI时钟最好控制在几百kHz以内二是ESP8266的WiFi协议栈会抢占CPU如果一边开WiFi一边软件模拟SPI时序抖动可能很大导致寄存器写入不可靠。所以如果对时序稳定性有要求还是FPGA或者简单单片机更靠谱。这个问题本质上不是能不能连而是你的时序可靠性要求有多高。最后分享一个我现在一直在用的小技巧调试的时候把SPI时钟设低比如100kHz然后用逻辑分析仪抓SCLK、SDATA、LE三根线对照数据手册逐位核对发送的每一帧。第一次上板时用这个方法五分钟就能发现是不是地址位放错了。等确认发送逻辑完全正确之后再把SPI时钟提到1MHz以上这样能把“逻辑问题”和“时序问题”分开排查效率非常高。