ARTICLE DETAIL

资讯详情

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

FPGA驱动ADF4351输出3GHz信号:完整Verilog SPI实现与避坑指南

FPGA驱动ADF4351输出3GHz信号:完整Verilog SPI实现与避坑指南 手里压着一块Alinx黑金Spartan-6开发板想让它输出3GHz甚至更高频率的射频信号板载资源肯定做不到这时候ADF4351这种自带VCO的小芯片就是最省事的方案。这个活的核心其实就是用FPGA写一段SPI驱动把寄存器配置正确灌进去信号就出来了。听起来简单但我在做的时候踩了不少坑从SPI时序到寄存器配置走了好几轮弯路所以这篇把完整流程、Verilog代码、仿真和上板实测都整理出来给后面准备拿FPGA驱动ADF4351的人省点时间。这篇内容适合两类朋友一类是刚入门FPGA想找个真实外设练练SPI通信的另一类是已经在用单片机或FPGA做射频项目需要让ADF4351快速输出目标频率信号的。不管你是看懂了数据手册但不知道代码怎么写还是抄了网上的例程却不明白为什么要加那么多延时这篇都能给你一个能直接落地的答案。1. 项目整体设计与思路拆解1.1 ADF4351是什么为什么选它ADF4351是ADI推出的一款宽带频率合成器输出范围从35MHz到4400MHz内部集成了VCO压控振荡器和锁相环只需要外部提供一个低频参考时钟比如10MHz或25MHz就能通过寄存器配置输出任意频率的射频信号。跟用DDS比如AD9850相比ADF4351的优势在于频率高、相位噪声低、输出功率可调非常适合做本振源、信号发生器的前端、无线收发测试等场景。很多人会把ADF4350和ADF4351搞混简单区分一下ADF4350支持整数分频和小数分频输出频率上限到4.4GHzADF4351在4350的基础上优化了指标并且多了对输出分频器的改进支持更低频率的输出噪声性能也更好。两者的寄存器配置方式基本一致SPI时序也一样所以下面的Verilog驱动代码两种芯片都能用。1.2 为什么用黑金Spartan-6开发板黑金开发板用的是Xilinx Spartan-6系列FPGA我手上这块是XC6SLX16逻辑资源虽然不算多但驱动ADF4351这种纯SPI从机绰绰有余。选择这块板子还有几个实际原因一个是Spartan-6的IO电平配置灵活ADF4351的数字接口最高可以工作在3.3V逻辑电平黑金板子的普通IO口默认就是3.3V不需要额外的电平转换电路直接连就能用。另一个是黑金板子上的系统时钟通常是50MHz这个频率对驱动SPI来说很方便。ADF4351的SCLK最大可以跑到20MHz以上我们用50MHz系统时钟分频出5MHz或10MHz的SCLK时序裕量非常充足稳定性也有保证。还有一个现实问题是手边正好就这块板子而且Spartan-6的支持软件ISE 14.7很成熟网上教程也多。如果你手头是Vivado下的Artix-7或者Zynq代码稍微改下时钟频率就能移植因为SPI驱动本来就是纯逻辑跟具体FPGA型号关系不大。1.3 为什么非要自己写Verilog SPI驱动ADF4351的寄存器配置接口是标准的3线SPI分别是SCLK时钟、SDATA数据、LE锁存使能。之所以不推荐用现成的IP核虽然Xilinx ISE里也有AXI Quad SPI这类IP但Spartan-6的SPI IP往往是针对挂接Flash、ADC等器件的通用设计接口复杂、配置繁琐为了驱动一个只有3根线的芯片去调IP核参数性价比太低了。更关键的是ADF4351的写入时序有个特点数据是32位一帧MSB先行这跟很多8位、16位的SPI从机不一样。通用SPI IP通常需要配置成32位模式或者自己拼字节读写反而容易出问题。自己用Verilog写一个状态机好处是完全掌控时序细节想发多少位就发多少位想加延时就在哪里加延时出了问题也好定位。2. 硬件连接与工程准备2.1 引脚分配与关键连接ADF4351模块我买的是常见的成品小板引脚排针直接引出SCLK、SDATA、LE、CE、RF_OUT、REF_IN等。跟黑金开发板连接只需要三根信号线加两根电源线SPI时钟频率不高所以不需要考虑高速信号完整性杜邦线就能跑得很稳。ADF4351模块引脚开发板FPGA引脚说明SCLKP11SPI时钟FPGA输出SDATAP12数据线FPGA输出LEN12锁存使能FPGA输出CE3.3V芯片使能直接拉高REF_IN外部参考源10MHz或25MHz正弦波/TTLVCC3.3V模块供电GNDGND共地引脚分配看起来简单但有个细节要注意LE这根线在ADF4351里扮演的角色是数据锁存相当于其他SPI芯片的CS片选。数据在SCLK上升沿逐位移入芯片内部的移位寄存器当32位全部移完之后给LE一个上升沿数据才会真正写入对应寄存器。所以LE不能用普通IO默认状态必须由FPGA的SPI状态机精确控制。2.2 硬件连接避坑点我第一次连接的时候直接把CE悬空结果芯片完全没反应后来查数据手册才知道CE内部虽然有下拉电阻但悬空时处于关断状态必须拉高才能工作。这个型号CE是高有效跟有些芯片正好相反很容易踩坑。参考时钟REF_IN也很讲究。ADF4351内部有参考频率检测电路输入参考时钟的幅度太大会损坏输入引脚太小又可能导致锁定失败。如果用有源晶振输出建议幅度控制在0.5V到VDD之间并且最好串一个50Ω电阻做匹配。如果手头只有无源晶振需要设计振荡电路这个复杂度就上去了我的经验是直接买一个10MHz或者25MHz的有源贴片晶振输出接个小电容隔直再并一个到地的偏置电阻就可以稳定馈给REF_IN。供电方面ADF4351的VCO供电要求比较干净模块板载一般已经做好了LDO或磁珠滤波但如果用杜邦线从开发板的3.3V排针取电要注意开发板稳压器的电流能力。Spartan-6本身功耗不低再加上ADF4351的输出驱动级如果选的是电脑USB供电的板子可能出现电压跌落导致锁相环失锁。2.3 开发环境准备Spartan-6必须用ISE 14.7Vivado不支持Spartan-6这一点先确认清楚。ISE 14.7在Windows 10以上系统安装时会有一些兼容性问题常见做法是在虚拟机里装XP或者Win7环境或者直接找已经处理好的绿色版。仿真工具我用的是ModelSim SE也可以用Questasim。如果不想折腾正版授权的Icarus Verilogiverilog加GTKWave也能跑仿真只是波形查看体验不如ModelSim顺手。ICARUS Verilog虽然对SystemVerilog支持一般但这段代码全部是经典Verilog语法跑起来完全没问题。这里提醒一个很常见的报错“Failure to obtain a Verilog simulation license”这是ModelSim的license没有配置好导致的跟代码无关。解决办法是确认环境变量“LM_LICENSE_FILE”指向了有效的license文件如果用的是破解版还要保证安装路径没有中文字符。3. ADF4351寄存器配置与3GHz信号生成原理3.1 频率合成公式与参数计算ADF4351输出频率的核心公式是F_VCO f_PFD × (INT FRAC / MOD)其中f_PFD是鉴相频率phase frequency detector由参考时钟REF_IN经过R分频器得到f_PFD REF_IN × (1 D) / (R × (1 T))公式里的几个参数都可以通过寄存器配置D是参考时钟倍频器参考频率倍增器T是参考时钟二分频R是参考分频系数。INT是整数分频系数FRAC是小数分频的分子MOD是小数分频的分母。以你搜索最多的“ADF4351如何产生3GHz频率的信号”为例假设参考时钟是25MHz先确定VCO频率。ADF4351的VCO覆盖范围是2200MHz到4400MHz3GHz正好落在VCO直接输出范围内所以RF输出分频器可以设为1也就是F_RF F_VCO。令R分频器为1D为0T为0那么f_PFD 25MHz。要想F_VCO 3000MHz则N 3000 / 25 120。取整数分频INT 120小数分频FRAC 0MOD 1也就是整数N模式下输出正好就是3GHz。如果把VCO频率设在3000MHz以下比如想输出900MHz就可以让VCO工作在3600MHz然后用输出分频器除以4得到900MHz。这种做法的好处是尽量让VCO工作在较高频率相位噪声性能更好也能避开VCO低频段的性能下降区。3.2 寄存器映射与示例值ADF4351的寄存器用4位地址区分一共6个寄存器R0到R5每次SPI写入一个32位数据其中高28位是数据内容低4位是寄存器地址。下面是驱动3GHz输出时的一组示例值对应25MHz参考时钟寄存器地址32位写入值主要作用R000000x003C0000设置INT120FRAC0R100010x00008001设置MOD1R200100x00004002R分频1电荷泵电流R300110x00000003频带选择时钟等R401000x00808004输出分频1RF输出功率R501010x580005MUXOUT与锁定检测设置注意这组数值只是示例不是万能公式。实际项目里参考时钟频率、输出频率、分频器组合、电荷泵电流、输出功率档位都会变用ADIsimPLL计算出来的值可能跟这组差异很大。上板前请务必按你的应用场景重新生成。3.3 用ADIsimPLL快速生成寄存器值不要自己手算这些寄存器位ADI官方提供免费的ADIsimPLL软件输入参考频率、目标频率、环路带宽等参数就能自动生成整套寄存器配置包括这里R0到R5的完整32位值还会给出预期的相位噪声曲线。这个工具是驱动ADF4351最省心的途径。用ADIsimPLL生成值之后有几个参数要人工关注电荷泵电流Charge Pump Current设置太大可能引起环路不稳定太小则锁定时间变长RF输出功率选择ADF4351有四档可调最大档位约5dBm模块板载通常还有一级放大器或者衰减网络最终输出功率要按模块手册为准输出分频器RF Divider如果设成分频模式记得把VCO频率设成目标频率乘以分频比。4. Verilog SPI驱动设计与完整代码4.1 写时序核心要点ADF4351的SPI写时序有三个关键约束理解这几个约束后代码就很好写了。第一数据MSB先行。每个32位寄存器值先发送最高位bit31最后发送最低位bit0bit0到bit3就是寄存器地址。第二SDATA在SCLK上升沿被采样。这意味着FPGA必须在SCLK上升沿到来之前就把数据放到SDATA上保持稳定。实际实现中最稳妥的方式是让SDATA在SCLK的下降沿改变这样经过半个SCLK周期的建立时间上升沿采样时数据一定稳定。第三LE的上升沿锁存数据。LE可以在所有32位数据移完后拉高但要保证LE上升沿和最后一个SCLK上升沿之间留出足够的时间。数据手册里对LE建立时间的要求很宽裕但为了保险我在最后一个SCLK下降沿之后再加了几个系统时钟周期才拉高LE。还有个细节是SCLK空闲电平。ADF4351没有强制要求SCLK空闲状态但按常规做法SCLK在无数据传输时保持低电平比较稳妥。有些SPI从机要求时钟极性高电平空闲ADF4351没有这个硬性要求不用过于纠结。4.2 SPI主控模块32位单次写入下面这个Verilog模块是整篇的核心负责向ADF4351写入一个32位寄存器值。模块通过参数HALF计算SCLK半周期时长比如系统时钟50MHz、目标SCLK频率5MHz时HALF就是50_000_000 / 5_000_000 / 2 5。timescale 1ns / 1ps // // ADF4351 SPI主控模块 // 功能向 ADF4351 写入一个 32bit 寄存器值MSB first // 时序sdata 在 SCLK 下降沿更新ADF4351 在 SCLK 上升沿采样 // 全部移完后 LE 拉高锁存再拉低结束本次事务 // module adf4351_spi #( parameter CLK_HZ 50_000_000, parameter SCLK_HZ 5_000_000 )( input wire clk, // 系统时钟 input wire rst_n, // 异步复位低有效 input wire start, // 启动一次写入建议单周期脉冲 input wire [31:0] tx_data, // 待发送的 32bit 数据 output reg sclk, // SPI 时钟 output reg sdata, // SPI 数据 output reg le, // 锁存使能高有效 output reg busy, // 忙标志 output reg done // 完成标志单周期脉冲 ); localparam HALF CLK_HZ / SCLK_HZ / 2; localparam S_IDLE 3d0; localparam S_SHIFT 3d1; localparam S_LE_HI 3d2; localparam S_LE_LO 3d3; reg [2:0] state; reg [31:0] sr; reg [4:0] bit_cnt; reg [15:0] half_cnt; reg [3:0] le_cnt; reg last_bit; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state S_IDLE; sclk 1b0; sdata 1b0; le 1b0; busy 1b0; done 1b0; sr 32d0; bit_cnt 5d31; half_cnt 16d0; le_cnt 4d0; last_bit 1b0; end else begin done 1b0; case (state) S_IDLE: begin if (start) begin sr tx_data; bit_cnt 5d31; half_cnt 16d0; sclk 1b0; sdata tx_data[31]; le 1b0; busy 1b1; last_bit 1b0; state S_SHIFT; end end S_SHIFT: begin if (half_cnt HALF - 1) begin half_cnt 16d0; sclk ~sclk; if (sclk) begin // 下降沿更新 sdata 并移位 if (last_bit) begin state S_LE_HI; end else begin sdata sr[30]; sr {sr[30:0], 1b0}; if (bit_cnt 0) last_bit 1b1; else bit_cnt bit_cnt - 1b1; end end end else begin half_cnt half_cnt 1b1; end end S_LE_HI: begin le 1b1; if (le_cnt 4d9) begin le_cnt 4d0; state S_LE_LO; end else begin le_cnt le_cnt 1b1; end end S_LE_LO: begin le 1b0; busy 1b0; done 1b1; state S_IDLE; end endcase end end endmodule这段代码的核心是S_SHIFT状态里的下降沿处理逻辑。half_cnt在每一个系统时钟周期加一当计数到HALF减一时翻转SCLK这样SCLK的频率就是目标SCLK频率的两倍周期。需要注意的是翻转SCLK时判断的是翻转前的值如果翻转前sclk是1那么翻转后变成0这就是下降沿在这个时刻更新sdata并移位如果翻转前sclk是0翻转后变成1是上升沿ADF4351在这个沿采样数据。为什么要在下降沿更新sdata因为这样能保证数据在SCLK上升沿前至少有半个SCLK周期的稳定时间。比如SCLK是5MHz半个周期是100ns这个建立时间对ADF4351来说绰绰有余。如果你把SCLK提高到20MHz建立时间变成25ns仍然满足数据手册要求。4.3 顶层配置模块连续写多个寄存器SPI主控模块一次只处理一个寄存器但ADF4351初始化需要写6个寄存器。所以顶层模块需要按顺序把R0到R5依次喂给SPI主控模块这也就是热词里说的“多字节收发”在FPGA里的实际做法本质就是在状态机里排队一个事务完成后自动发起下一个事务。timescale 1ns / 1ps // // ADF4351 顶层配置模块 // 功能上电后按顺序写入 R0~R5 六个寄存器 // 说明REG 值仅为 25MHz 参考、3000MHz 输出示例实用请用 ADIsimPLL 重算 // module adf4351_top ( input wire clk, // 50MHz 系统时钟 input wire rst_n, // 复位 output wire sclk, // 到 ADF4351 的 SPI 时钟 output wire sdata, // 到 ADF4351 的 SPI 数据 output wire le // 到 ADF4351 的 LE 锁存 ); localparam [31:0] REG0 32h003C0000; localparam [31:0] REG1 32h00008001; localparam [31:0] REG2 32h00004002; localparam [31:0] REG3 32h00000003; localparam [31:0] REG4 32h00808004; localparam [31:0] REG5 32h580005; reg [2:0] idx; reg [31:0] tx_data; reg start; reg start_en; wire busy; wire done; // 根据当前寄存器序号选择发送内容 always (*) begin case (idx) 3d0: tx_data REG0; 3d1: tx_data REG1; 3d2: tx_data REG2; 3d3: tx_data REG3; 3d4: tx_data REG4; 3d5: tx_data REG5; endcase end // 发送调度逻辑空闲时发起一次写入写完切换下一个寄存器 always (posedge clk or negedge rst_n) begin if (!rst_n) begin idx 3d0; start 1b0; start_en 1b1; end else begin start 1b0; if (done) begin idx (idx 3d5) ? 3d0 : idx 1b1; start_en 1b1; end if (start_en !busy) begin start 1b1; start_en 1b0; end end end adf4351_spi #( .CLK_HZ (50_000_000), .SCLK_HZ (5_000_000) ) u_spi ( .clk (clk), .rst_n (rst_n), .start (start), .tx_data (tx_data), .sclk (sclk), .sdata (sdata), .le (le), .busy (busy), .done (done) ); endmodule这里start_en的设计是为了避免SPI模块还在忙状态下重复触发start。上电复位后start_en为1当检测到busy为0时立刻拉高start一个周期同时拉低start_en。等done信号回来后说明当前寄存器数据已经锁存进ADF4351此时递增idx并重新允许发起下一次写入。这样六个寄存器会按顺序循环下发即使你是想把输出频率切换到另一个频点这个顶层逻辑也可以直接复用只需替换寄存器值。4.4 仿真Testbench与task调用有了SPI主控模块和顶层配置模块写Testbench时正好可以用上Verilog的task语法。把“发送一个32位数据”封装成一个task在initial块里调用六次代码非常干净。timescale 1ns / 1ps module tb_adf4351_top; reg clk 0; reg rst_n 0; wire sclk, sdata, le; always #10 clk ~clk; // 50MHz adf4351_top u_top ( .clk (clk), .rst_n (rst_n), .sclk (sclk), .sdata (sdata), .le (le) ); // 也可以直接用 task 操作 spi_master测试单次 32bit 收发 reg [31:0] test_data; reg start; wire busy, done; reg [31:0] spi_rx_shadow; task spi_write_32(input [31:0] data); begin (posedge clk); test_data data; start 1; (posedge clk); start 0; wait (done); end endtask initial begin rst_n 0; #100; rst_n 1; #200; spi_write_32(32h003C0000); spi_write_32(32h00008001); spi_write_32(32h00004002); spi_write_32(32h00000003); spi_write_32(32h00808004); spi_write_32(32h580005); #500; $finish; end initial begin $dumpfile(tb_adf4351_top.vcd); $dumpvars(0, tb_adf4351_top); end endmodule用iverilog跑这个Testbench命令如下iverilog -o tb_out tb_adf4351_top.v adf4351_top.v adf4351_spi.v vvp tb_out生成的VCD波形可以用GTKWave打开重点观察以下几点SCLK是不是规则的5MHz方波SDATA每次变化是否都发生在SCLK下降沿LE上升沿是否出现在最后一个SCLK周期之后六个寄存器是否按R0到R5的顺序依次写完。5. 仿真与上板调试实录5.1 仿真波形怎么看仿真阶段最容易发现的问题就是时序错位。我第一次跑仿真时LE拉高的时机太早最后一个bit还没被ADF4351采样就锁存了波形上表现为LE上升沿和最后一个SCLK脉冲离得太近。后来在S_SHIFT状态里加了last_bit标志确保在最后一个数据bit被上升沿采样之后的下降沿才进入S_LE_HI状态问题就解决了。查看波形时还有个技巧把sdata的显示模式改成十六进制这样能够直接看到发送的32位数据是不是预期的寄存器值。我的做法是把sdata按照SCLK上升沿采样后拼成一个32位的shadow寄存器仿真结束前打印出来对比这个办法在验证数据顺序时特别好用。5.2 上板实测3GHz输出与验证代码综合、生成比特流下载到黑金开发板之后ADF4351模块的RF_OUT接频谱仪可以看到3GHz附近出现明显的频谱分量。如果没有频谱仪也可以用高带宽的示波器大概确认信号存在但示波器探头输入电容会对高频信号造成衰减测到的幅度仅供参考。上板后最常遇到的现象是输出完全没有信号。这时候先量ADF4351的LD锁定检测引脚正常锁定时LD输出高电平如果寄存器5里配置成锁定检测模式。如果LD一直低说明PLL没有锁定排查顺序是先查SPI时序是否有问题、寄存器是否真的写进去了再查参考时钟是否稳定最后查电荷泵电流设置是否合理。还有一种情况是锁定指示灯亮了但输出频率不对。这种通常是寄存器配置数据算错了比如INT设置错误、R分频设置错误。我的建议是不要凭感觉猜直接把ADIsimPLL生成的值抓下来和SPI总线上的写入值一一比对很多时候是漏了某个寄存器或者把地址位写错了。5.3 信号质量与优化建议PLL锁定后频谱不是一根干净的谱线而是带有一定的相位噪声和杂散。对于一般应用ADF4351的性能已经足够但如果追求更好指标有几个地方值得优化。环路滤波器是关键。很多成品模块用的是默认环路带宽比如几十kHz如果输出信号的相位噪声在偏离载波较近处表现不佳可能是环路带宽设置和电荷泵电流不匹配。通过ADIsimPLL可以仿真不同环路带宽下的噪声曲线然后相应修改模块上的贴片电阻电容值。电源去耦也很重要。VCO对电源噪声极敏感我实测过把ADF4351模块的供电从开发板3.3V换成一个低噪声LDO单独供电后带外相噪大约改善了3到5dB。如果你后续想让输出信号做更高要求的测试给ADF4351单独一路电源是性价比最高的投资。6. 常见问题速查与独家避坑心得6.1 问题速查表现象可能原因解决方案完全没有输出LD不亮CE引脚被拉低或悬空将CE直接接3.3V完全没有输出LD常亮RF输出分频器/输出使能位没配好检查R4中的输出使能位输出频率不对寄存器值算错或地址位写错用ADIsimPLL重新生成并逐位比对SCLK无波形SPI状态机没有启动确认start信号是否被拉高busy是否一直为低LE始终为高状态机卡在S_LE_HI检查le_cnt是否正常递增复位是否释放仿真报ModelSim license错误license环境变量未配置检查LM_LICENSE_FILE仿真数据混乱SDATA与SCLK相位不对时序设计错误确认sdata只在SCLK下降沿更新6.2 避坑经验总结这轮实操下来印象最深的几个点值得单独说。第一ADF4351的寄存器写入顺序是有讲究的。数据手册建议先写R5、R4等非关键寄存器最后写R0因为R0通常包含对频率切换的触发位。顶层模块里如果按R0到R5顺序写也不是不行但可能引起一次不必要的频率跳变。稳妥的做法是把R0放到最后写。第二SPI的“片选”概念在这里要转换成LE。热词里经常有人问SPI硬件片选和软件片选的区别ADF4351没有真正的CS引脚LE就是软件片选由FPGA用IO控制。如果你用的是带硬件SPI外设的单片机千万别把LE接到硬件片选引脚上去否则每次字节传输LE都会翻转一次而ADF4351要求32位全部移完后才能锁存。第三我做完这个项目后的体会是Verilog代码本身不复杂真正卡人的是时序边界和寄存器配置。写SPI驱动时养成“在所有状态转移前先想清楚波形”的习惯比直接抄代码有用得多。比如上一节提到的LE锁存时机搞懂之后你再看任何SPI从机的数据手册都能一眼看出该往状态机里加什么。这趟用黑金Spartan-6驱动ADF4351的折腾前前后后花了大半个周末。回头看看最值钱的不是那几行代码而是踩坑过程中对SPI时序和PLL配置的理解。代码部分你直接拿去用就行但寄存器值如果需要出别的频率一定记得用ADIsimPLL重新生成别在我给的示例值上硬套。后面如果想把输出频率做成可动态切换的可以在顶层模块里把寄存器值组放到RAM里通过串口或按键切换地址SPI驱动部分一行都不用改。
返回列表