手里同时摆着一块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和LE(Load 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位寄存器帧按位送到ADF4351,ADF4351负责完成锁相环的锁定和射频输出。硬件上至少要满足这几个条件:
- 给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是鉴相频率,单位MHz;INT是整数分频比;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不倍频、不分频、R=1,所以f_PFD就是10MHz。
从公式也能看出,输出频率的步进精度取决于PFD和MOD的比例关系。比如PFD=10MHz,MOD取到4096,那么最小步进就是10MHz/4096,大约2.44kHz。这个精度做常规信号源已经够用。
2.2 用公式推导一份3GHz配置
以目标3GHz输出为例,最直接的做法是不用RF分频器,让VCO直接工作在3GHz附近。此时RF_DIV=1,我们需要让VCO=3GHz,INT+FRAC/MOD = 300,整数部分INT=300,小数部分FRAC=0。
FRAC=0时MOD取一个大于等于2的值即可,我这里取MOD=2,这样初始化帧写起来很干净。
理论上这个计算就这么简单。但实际写寄存器时,还要把内部VCO频段选择、预分频器、LD锁定指示、输出功率等控制位一并写上。不能说“只算一个N值就完事”,那只是开了个头。
2.3 寄存器帧值与配置顺序
下面这组帧值是我在10MHz参考频率、目标3GHz输出下验证过的组合,注意这个“3GHz”是RF输出频率,VCO也在3GHz附近,RF分频比是1。如果你的板子参考频率不是10MHz,或者想输出别的频率,请一定按公式重新计算,不要直接照抄。
| 寄存器 | 帧值(Hex) | 作用 |
|---|---|---|
| R5 | 0x005C0005 | 锁定指示、参考倍频/分频等控制位 |
| R4 | 0x000080C4 | RF输出使能、输出功率、Aux输出设置 |
| R3 | 0x00006003 | 时钟分频器、ABP、LDP等 |
| R2 | 0x00004002 | 预分频器8/9、R计数器=1、低杂散模式 |
| R1 | 0x00200001 | MOD=2、相位字=0 |
| R0 | 0x012C0000 | INT=300、FRAC=0、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 = 3'd0; localparam LOAD = 3'd1; localparam SHIFT = 3'd2; localparam LATCH = 3'd3; localparam NEXT = 3'd4; localparam FINISH = 3'd5; 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] = 32'h005C0005; // R5 reg_file[1] = 32'h000080C4; // R4 reg_file[2] = 32'h00006003; // R3 reg_file[3] = 32'h00004002; // R2 reg_file[4] = 32'h00200001; // R1 reg_file[5] = 32'h012C0000; // R0 end always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; reg_idx <= 3'd0; bit_cnt <= 5'd0; shift_reg <= 32'd0; div_cnt <= 8'd0; sclk <= 1'b0; sdata <= 1'b0; le <= 1'b0; done <= 1'b0; end else begin case (state) IDLE: begin done <= 1'b0; if (start) begin reg_idx <= 3'd0; state <= LOAD; end end LOAD: begin shift_reg <= reg_file[reg_idx]; bit_cnt <= 5'd31; sclk <= 1'b0; le <= 1'b0; state <= SHIFT; end SHIFT: begin if (div_cnt == DIV_HALF) begin div_cnt <= div_cnt + 8'd1; if (!sclk) begin // sclk为低时准备好当前bit sdata <= shift_reg[31]; shift_reg <= {shift_reg[30:0], 1'b0}; if (bit_cnt == 5'd0) state <= LATCH; else bit_cnt <= bit_cnt - 1'b1; end end else if (div_cnt == (DIV - 1)) begin div_cnt <= 8'd0; sclk <= ~sclk; // 翻转SCLK,产生上升沿/下降沿 end else begin div_cnt <= div_cnt + 8'd1; end end LATCH: begin sclk <= 1'b0; le <= 1'b1; // 32位传完后拉高LE锁存 if (div_cnt == DIV_HALF) begin div_cnt <= 8'd0; le <= 1'b0; state <= NEXT; end else begin div_cnt <= div_cnt + 8'd1; end end NEXT: begin if (reg_idx == 3'd5) state <= FINISH; else begin reg_idx <= reg_idx + 1'b1; state <= LOAD; end end FINISH: begin done <= 1'b1; 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,中心频率设为3GHz,SPAN设为100MHz左右,RBW设到100kHz。如果配置正确,3GHz处应该出现一个明显的主信号。
如果没信号,最常用的定位手段是换一个频率测。把reg_file[5]改成2.4GHz对应的值(0x00F00000,即INT=240),重新配置。如果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做远程配置
如果想把FPGA+ADF4351做成一个可以远程控制的信号源,完全可以在UART前面挂一个ESP8266之类的WiFi模块,让ESP8266负责网络通信,FPGA负责SPI时序和频率计算。两者之间只需简单的串口协议,定义好几个字节的命令就够用。这个扩展思路适合做实验室自动化或者远程仪器控制。
我当时调通这篇文章里的固定频率配置后,下一个版本就是往这个方向做的,改成串口下发频率字,用起来比每次改源码重综合方便太多了。
文章里这组寄存器帧值是基于10MHz参考源和3GHz输出算出来的,大家一定不要盲目照搬,换一个参考频率就得重算PFD和分频比。我用Alinx黑金开发板调这块芯片最大的体会是:SPI代码本身不难,真正坑人的都是时序细节和上电顺序。把示波器接好、把公式写在纸上,一步一步来,一次成功并不难。如果调试过程中遇到问题,按文章里的排查表逐条过一遍,多半能找到原因。