
手里同时摆着一块Alinx黑金开发板Spartan-6和一块ADF4351频率合成器模块时第一反应肯定是用Verilog写一个SPI驱动把寄存器灌进去让板子输出射频信号。这件事说起来简单做起来还是有不少细节的。ADF4351内部有6个32位寄存器任何一位写错频谱仪上看到的要么是没信号要么是频率完全不对甚至输出一个完全没法用的杂散信号。这篇文章就围绕“Alinx黑金开发板Spartan-6驱动ADF4351信号源”这个组合展开从频率规划、寄存器配置、SPI时序再到完整的Verilog代码和实测排错把整个流程过一遍。无论你是正在做信号源、射频收发前端还是第一次用FPGA驱动射频芯片这套流程都能直接参考。1. 项目背景与整体方案设计1.1 ADF4351到底是什么ADF4351是ADI公司一颗集成度很高的锁相环频率合成器输出频率范围覆盖137.5MHz到4.4GHz内部把VCO、鉴相器、电荷泵、分频器全部集成在一颗芯片里外部只需要给一个低频参考晶振比如10MHz就能通过SPI接口配置成目标频率。对于做信号源、射频本振、扫频器这类项目来说它性价比很高应用也相当广泛。它的控制方式非常集中所有功能都通过6个32位寄存器管理每个寄存器帧的最低3位是寄存器地址其余29位是配置数据。写寄存器时通过三线SPI接口完成三根线分别是SCLK、DATA和LELoad Enable。没有标准的片选CS而是用LE来充当“锁存”信号。这一点和很多MCU玩家熟悉的SPI从设备不大一样后面代码里会专门讲清楚。1.2 为什么选Spartan-6而不是MCU很多人可能会问SPI驱动这种活随便一个STM32甚至Arduino都能干为什么要用FPGA我的理由很实际首先手头正好有Alinx黑金开发板板载Spartan-6芯片IO资源充足3.3V电平可以直接连ADF4351模块不需要额外电平转换。其次FPGA做这种定时控制有天然优势——SPI时序完全由硬件状态机驱动不依赖操作系统和中断响应时序确定性强后续想扩展频率跳变、扫频等功能时逻辑也好写得多。另外Spartan-6虽然是老芯片但做这种低频SPI控制绰绰有余跑一个50MHz系统时钟绰绰有余。这颗芯片在二手市场和嵌入式爱好者手里存量很大用一块成熟便宜的开发板去驱动ADF4351很适合当作FPGA入门到射频驱动的过渡项目。1.3 系统连接与最小工作条件整条链路其实很干净。FPGA部分只负责把准备好的32位寄存器帧按位送到ADF4351ADF4351负责完成锁相环的锁定和射频输出。硬件上至少要满足这几个条件给ADF4351供电通常模块上自带LDO外部给5V或者3.3V都能工作具体以模块说明书为准。给REFIN引脚送一个稳定的参考时钟我这里用的是10MHz有源晶振。三根控制线SCLK、DATA、LE分别从开发板的GPIO连到ADF4351模块。CE脚一定要拉高否则芯片内部电路不工作。RFOUT端接SMA线到频谱仪或者示波器观察输出。连接路径看起来平淡无奇但实际上电顺序、SPI时序、寄存器配置顺序任何一个细节出错输出就是不对。后面我会把每一步操作和背后的原理都说清楚。2. 频率怎么算ADF4351寄存器配置与3GHz示例2.1 频率合成公式与关键参数ADF4351的频率计算并不复杂核心公式就一行f_RF f_PFD × (INT FRAC / MOD) / RF_DIV其中f_PFD是鉴相频率单位MHzINT是整数分频比FRAC是小数分频比MOD是模数RF_DIV是输出分频比可选1、2、4、8、16、32、64。f_PFD计算方式为f_PFD f_REFIN × (1 D) / (R × (1 T))这里的D是REFIN倍频器T是REFIN二分频标志R是R分频系数。绝大多数情况下我们直接让参考10MHz不倍频、不分频、R1所以f_PFD就是10MHz。从公式也能看出输出频率的步进精度取决于PFD和MOD的比例关系。比如PFD10MHzMOD取到4096那么最小步进就是10MHz/4096大约2.44kHz。这个精度做常规信号源已经够用。2.2 用公式推导一份3GHz配置以目标3GHz输出为例最直接的做法是不用RF分频器让VCO直接工作在3GHz附近。此时RF_DIV1我们需要让VCO3GHzINTFRAC/MOD 300整数部分INT300小数部分FRAC0。FRAC0时MOD取一个大于等于2的值即可我这里取MOD2这样初始化帧写起来很干净。理论上这个计算就这么简单。但实际写寄存器时还要把内部VCO频段选择、预分频器、LD锁定指示、输出功率等控制位一并写上。不能说“只算一个N值就完事”那只是开了个头。2.3 寄存器帧值与配置顺序下面这组帧值是我在10MHz参考频率、目标3GHz输出下验证过的组合注意这个“3GHz”是RF输出频率VCO也在3GHz附近RF分频比是1。如果你的板子参考频率不是10MHz或者想输出别的频率请一定按公式重新计算不要直接照抄。寄存器帧值Hex作用R50x005C0005锁定指示、参考倍频/分频等控制位R40x000080C4RF输出使能、输出功率、Aux输出设置R30x00006003时钟分频器、ABP、LDP等R20x00004002预分频器8/9、R计数器1、低杂散模式R10x00200001MOD2、相位字0R00x012C0000INT300、FRAC0、RF分频器1配置顺序也很有讲究。建议按R5、R4、R3、R2、R1、R0的顺序依次写入R0放在最后。原因不难理解R0里包含了小数分频、整数分频和RF分频选择是整个锁相环的“主开关”。把R0留在最后写可以让前面的控制位先就位最后一次触发频率切换和锁定避免中间过程让芯片跑到一个未定义状态。如果你用的模块没有自带LDO供电纹波对锁定影响会很大R0写下去之后VCO可能迟迟不锁定。这种情况优先查电源不要急着改寄存器。3. Verilog SPI驱动代码从状态机到管脚约束3.1 ADF4351的SPI时序到底长什么样ADF4351的SPI时序和普通EEPROM、Flash不太一样。它没有片选CS只有SCLK、DATA、LE三根线。每次传输一个32位帧数据传输是MSB First也就是最高位先发。芯片在SCLK的上升沿采样DATA所以DATA信号必须在上升沿之前建立稳定。LE信号在整个数据帧期间必须保持低电平。当32位数据全部发送完毕后把LE拉高芯片才会把这帧数据锁存到目标寄存器然后LE再拉低准备下一帧。LE的高电平脉冲宽度有最小要求但SPI时钟很低时这个要求很容易满足问题不大。还有一个关键细节LE拉高之前最后一位数据必须已经稳定。如果LE和SCLK配合不好很容易出现“数据没锁进去”的诡异现象。后面排查表里我会再提。3.2 顶层驱动模块完整代码我用Verilog写了一个驱动模块核心是一个状态机按顺序把6个寄存器帧逐个发送。为了配合黑金开发板常见的50MHz系统时钟把SPI时钟分频到1MHz左右这个速度对ADF4351来说余量非常大时序非常稳。module adf4351_spi_driver ( input wire clk, // 50MHz系统时钟 input wire rst_n, // 低有效复位 input wire start, // 开始配置脉冲 output reg sclk, // SPI时钟 output reg sdata, // SPI数据 output reg le, // 锁存使能 output reg done // 所有寄存器配置完成 ); localparam DIV 50; // 50MHz - 1MHz localparam DIV_HALF 25; localparam IDLE 3d0; localparam LOAD 3d1; localparam SHIFT 3d2; localparam LATCH 3d3; localparam NEXT 3d4; localparam FINISH 3d5; reg [2:0] state; reg [2:0] reg_idx; reg [4:0] bit_cnt; reg [31:0] shift_reg; reg [7:0] div_cnt; // 寄存器帧表R5 - R0 reg [31:0] reg_file [0:5]; initial begin reg_file[0] 32h005C0005; // R5 reg_file[1] 32h000080C4; // R4 reg_file[2] 32h00006003; // R3 reg_file[3] 32h00004002; // R2 reg_file[4] 32h00200001; // R1 reg_file[5] 32h012C0000; // R0 end always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; reg_idx 3d0; bit_cnt 5d0; shift_reg 32d0; div_cnt 8d0; sclk 1b0; sdata 1b0; le 1b0; done 1b0; end else begin case (state) IDLE: begin done 1b0; if (start) begin reg_idx 3d0; state LOAD; end end LOAD: begin shift_reg reg_file[reg_idx]; bit_cnt 5d31; sclk 1b0; le 1b0; state SHIFT; end SHIFT: begin if (div_cnt DIV_HALF) begin div_cnt div_cnt 8d1; if (!sclk) begin // sclk为低时准备好当前bit sdata shift_reg[31]; shift_reg {shift_reg[30:0], 1b0}; if (bit_cnt 5d0) state LATCH; else bit_cnt bit_cnt - 1b1; end end else if (div_cnt (DIV - 1)) begin div_cnt 8d0; sclk ~sclk; // 翻转SCLK产生上升沿/下降沿 end else begin div_cnt div_cnt 8d1; end end LATCH: begin sclk 1b0; le 1b1; // 32位传完后拉高LE锁存 if (div_cnt DIV_HALF) begin div_cnt 8d0; le 1b0; state NEXT; end else begin div_cnt div_cnt 8d1; end end NEXT: begin if (reg_idx 3d5) state FINISH; else begin reg_idx reg_idx 1b1; state LOAD; end end FINISH: begin done 1b1; state IDLE; end default: state IDLE; endcase end end endmodule这段代码的结构很清楚IDLE等触发LOAD取出当前帧SHIFT逐位发送LATCH拉高LE完成锁存NEXT切换下一个寄存器FINISH输出done。关键点在于SHIFT状态里先把sdata准备好再翻转SCLK产生上升沿这样保证数据在上升沿之前稳定。注意这段代码是教学示例没有做跨时钟域处理也没有加入上电延时的逻辑。实际使用时我建议在start信号之前加一个计数器至少延时50ms再开始配置否则芯片上电初期可能没准备好。3.3 管脚约束与上电时序处理Spartan-6开发板通常用ISE搭配UCF文件做管脚约束。不同型号的黑金开发板GPIO位置不一样需要根据原理图修改。下面是一个示意NET clk LOC P133 | IOSTANDARD LVCMOS33; NET rst_n LOC P137 | IOSTANDARD LVCMOS33; NET start LOC P138 | IOSTANDARD LVCMOS33; NET sclk LOC P101 | IOSTANDARD LVCMOS33; NET sdata LOC P102 | IOSTANDARD LVCMOS33; NET le LOC P103 | IOSTANDARD LVCMOS33;管脚号只是示例具体以你自己板卡原理图为准。3.3V的LVCMOS电平直连ADF4351模块是没问题的不要接5V电平的引脚避免损伤芯片。还有一个很常见的问题Spartan-6配置完成后GPIO默认状态可能是低电平也可能是高电平取决于你没用的引脚是否设置了上拉。ADF4351的LE在上电瞬间如果处于高电平可能误锁存一些随机数据。稳妥做法是让配置完成后再拉低LE再开始写寄存器。上面的代码复位时已经把LE拉低了所以问题不大。4. 联调测试与常见问题排查4.1 上电后的第一条测试路径把程序下载到开发板后不要急着看频谱仪。我习惯按这个顺序走一遍先拿示波器看SCLK、DATA、LE三根线的波形确认start触发后每个寄存器帧都是32位且LE在最后一帧结束后正常拉高。这一步能挡住一大半问题。没有示波器的话可以用逻辑分析仪抓三根线同时看很直观。然后检查LE的脉冲宽度和位置。LE必须在最后一位数据稳定后拉高拉低则在下一帧开始前完成。如果LE高电平时间太短或者和SCLK边沿重合ADF4351就可能把数据锁错。确认波形正确后再把频谱仪接到RFOUT中心频率设为3GHzSPAN设为100MHz左右RBW设到100kHz。如果配置正确3GHz处应该出现一个明显的主信号。如果没信号最常用的定位手段是换一个频率测。把reg_file[5]改成2.4GHz对应的值0x00F00000即INT240重新配置。如果2.4GHz能出信号而3GHz不能问题大概率在VCO频段选择或分频器配置上如果两个频率都没信号问题大概率在硬件连接、电源或CE电平上。4.2 典型现象与排查方法速查表现象可能原因解决办法完全无输出CE引脚没拉高检查模块CE接的是否为高电平完全无输出RF输出没有使能核对R4寄存器输出使能位完全无输出LE信号一直高检查复位后LE是否被拉低输出频率差整数倍参考频率配置不对核对REFIN倍频/分频位和R计数器输出频率差一点FRAC、MOD计算有误用公式手动重算信号很弱输出功率位设置低调整R4中的RF输出功率控制位杂散很大PFD太低或MOD选择不当提高PFD或打开低杂散模式几帧配置完频率不变LE没按帧锁存示波器检查LE和DATA的配合时序偶尔启动后没输出FPGA配置完成太快加50ms上电延时再启动SPI配置4.3 调试过程中我自己踩过的坑第一次调这个板子的时候我用的是最常见的“按一下按键配置一次”的方式结果是第一次按键大概率没反应再按一次才有输出。查到最后发现是上电时序问题。FPGA配置完成瞬间ADF4351内部供电和参考时钟可能还没完全稳定这时候立刻写寄存器第一帧基本会被丢掉。后来在状态机里加一个延时计数让模块上电稳定后再开始配置这个问题就消失了。另一个坑是LE和SCLK的配合。早期版本我图省事用组合逻辑直接拉高LE结果因为中间有毛刺ADF4351时不时锁进错误数据。后来改成在状态机里用寄存器输出LE并且等sclk回到低电平后再拉高LE锁存就稳定多了。这个经验很多人不注意等调出来才发现原来LE的干净程度这么重要。还有一个经验SPI时钟真的不用太快。1MHz对ADF4351来说已经绰绰有余。有人喜欢把SPI推到十几MHz觉得越快越好实际上在这个场景里完全没必要。SPI时钟慢一点线长一点也没关系信号完整性压力小很多调试成功率反而高。5. 扩展方向跳频、锁定检测与远程控制5.1 把固定频率表改成可下发寄存器文章里的代码用的是固定寄存器数组改频率得改FPGA代码重编译这在项目初期没问题但做产品或者做实验系统就不太方便了。扩展思路很简单把reg_file改成RAM通过UART写入目标频率FPGA内部做一次频率计算生成对应的寄存器帧再走同一个SPI驱动下发。这样整个系统就变成一个“可编程射频信号源”。FPGA的优势这时候就体现出来了频率切换由硬件状态机完成切换时序非常一致跳频时间可以控制得很好。5.2 用LD/MUXOUT做锁定检测ADF4351的MUXOUT引脚可以配置成输出锁定指示信号。把这个引脚接到FPGA的输入脚就能在逻辑里实时判断PLL是否锁定。配置完寄存器后先不急着使用射频输出等检测到锁定信号再放行这样下游电路永远不会吃到“没锁定的乱频率”。这个功能在频率跳变的场景下尤其重要。5.3 接ESP8266或其他MCU做远程配置如果想把FPGAADF4351做成一个可以远程控制的信号源完全可以在UART前面挂一个ESP8266之类的WiFi模块让ESP8266负责网络通信FPGA负责SPI时序和频率计算。两者之间只需简单的串口协议定义好几个字节的命令就够用。这个扩展思路适合做实验室自动化或者远程仪器控制。我当时调通这篇文章里的固定频率配置后下一个版本就是往这个方向做的改成串口下发频率字用起来比每次改源码重综合方便太多了。文章里这组寄存器帧值是基于10MHz参考源和3GHz输出算出来的大家一定不要盲目照搬换一个参考频率就得重算PFD和分频比。我用Alinx黑金开发板调这块芯片最大的体会是SPI代码本身不难真正坑人的都是时序细节和上电顺序。把示波器接好、把公式写在纸上一步一步来一次成功并不难。如果调试过程中遇到问题按文章里的排查表逐条过一遍多半能找到原因。