ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

FPGA SPI主接口设计:Verilog状态机、CPOL/CPHA与调试实战

FPGA SPI主接口设计:Verilog状态机、CPOL/CPHA与调试实战 简介基于FPGA的SPI通信接口设计是一份面向FPGA开发者和电子工程学习者的完整工程包聚焦SPI总线协议在FPGA中的落地实现涵盖协议原理、状态机设计、管脚分配及Quartus II开发流程。压缩包共428个文件大小5.47MB核心为VHDL源码、Quartus II工程文件qpf/qsf、仿真波形vwf及开发板原理图PDF等工程中间文件一并保留便于直接打开、复现与二次修改。已有1160人学习下载。借助这份工程包读者可以拿到可综合的SPI接口代码与完整工程框架理解MISO/MOSI/SCLK/CS四线时序控制掌握主从设备通信的状态机编写思路并参考管脚约束与仿真结果进行硬件调试适合正在学习数字系统设计或需要快速集成SPI外设的开发人员。1. 打开工程包最先看到什么综合映射文件里的FPGA SPI设计打开这份工程包第一眼看到的不是README而是一串spi.autos_3e92...map.atm和spi_out.vhd.bak。前者是Quartus II综合后留下的映射文件后者是设计者保留的一份VHDL源文件备份说明它不是把教程代码塞进新建工程的演示品而是从真实开发流程里打包出来的。整套设计围绕FPGA上的SPI主接口展开用Verilog状态机实现MISO/MOSI/SCLK/CS_n四线协议配套管脚分配、开发板原理图和可编译工程目标是直接跑通低速外设——W25Q64读ID、AD7124采样这类应用都属于这个范围。适合两类人刚学FPGA开发、想找一个不依赖IP核的纯逻辑串行口的人以及已经在用MCU做SPI、想弄懂FPGA实现时采样沿、管脚约束和调试方法的老手。2. SPI时序的本质与FPGA里的选型从四根线到CPOL/CPHA2.1 四根线完成全双工其实就是两个移位寄存器SPI的物理层只有四根线SCLK、MOSI、MISO、CS_n。主设备产生SCLKCS_n低有效选中从设备MOSI/MISO构成一组全双工移位路径。理解SPI最简单的方式是把它看作两个首尾相接的移位寄存器发送时主设备在某个时钟沿把tx_data的最高位移到MOSI从设备在同一个或被移相的时钟沿把MISO上的一位移入自己的移位寄存器一轮8拍下来主机发送的8位进入从机从机返回的8位被主机捕获。因此在FPGA里实现SPI本质上不是实现什么复杂总线而是管理好两个数据位流和一个可编程时钟。在FPGA里用逻辑门实现UART需要波特率发生器加双沿判断SPI省去帧格式和起始位代价是多带4根线并且主设备对上升沿、下降沿都要有明确控制。这也是为什么很多FPGA项目里SPI比UART先跑通——它是同步协议只要管脚约束正确调试难度低一个数量级。接一个具体从设备前我一般先打开数据手册翻到SPI时序图做两件事第一看SCLK空闲时是低还是高记为CPOL第二看数据在哪个边沿被锁存记为CPHA。以W25Q64为例手册给出Mode 0和Mode 3我习惯固定用Mode 0AD7124则只能用Mode 0配置错会一直读到噪声。2.2 CPOL/CPHA四个模式别凭经验猜很多MCU工程里习惯把SPI固化为Mode 0切到FPGA里也跟着填CPOL0、CPHA0结果遇到Mode 1/2的设备就抓瞎。正确的判定规则是CPOL看空闲电平CPHA看采样沿是第几个边沿。CPHA0意味着数据在第一个边沿稳定CPHA1时第一个边沿只是翻转时钟数据在第二个边沿才有效。四个模式对照如下模式CPOLCPHASCLK空闲采样沿典型器件Mode 000低上升沿W25Q64、OLED、AD7124Mode 101低下降沿少量音频ADCMode 210高下降沿部分SD卡Mode 311高上升沿W25Q64、ENC28J60FPGA实现里的坑在于CPOL和CPHA不是两个独立旋钮它们共同决定MOSI该在哪个系统时钟沿更新。代码里如果只改空闲电平不改采样沿会出现每帧错一位的现象而且用逻辑分析仪看波形很难发现因为SCLK和MOSI看起来都有输出。确定模式的可执行步骤是先在时序图里找DONT CARE区域看SCLK空闲保持的是高还是低再假设一组数据分别用四个模式跑一次能稳定读回预期值的那个模式就是对的。实际项目中我更信任手册而不是猜猜模式只用来做上板后的快速验证。2.3 与I2C、UART对比什么场景下非FPGA不可选型时还要知道SPI为什么在某些场景下比I2C、UART更合适。三者不是替代关系而是应用范围不同特性SPII2CUART时钟源主设备提供SCLK主设备提供SCL双方各自波特率数据线MOSI/MISO分开SDA半双工TX/RX分开地址机制靠CS_n片选7/10位地址无典型速率几十MHz几MHz几Mbps从设备数量每设备一根CS_n同总线多地址一对一如果FPGA上挂一个W25Q64SPI明显比I2C合适因为器件本身没有I2C版本如果要挂多个温度传感器I2C一根数据线能省管脚UART的优势是线最少但需要双方对波特率FPGA里做UART还要处理亚稳态。在FPGA开发中当外设是SPI且从设备数量多时可以用一个FPGA把所有片选管理起来外部MCU通过FMC或普通GPIO写几个寄存器就能切换通道典型场景就是STM32H743和FPGA实现FMC通信FPGA再转成多路SPI去驱动外部设备。3. 参数化SPI主接口的Verilog实现状态机、采样沿和testbench3.1 先定接口再写状态机下面这段代码是我在工程里用的SPI主接口核心支持CPOL/CPHA参数化可以灵活接W25Q64这类Mode 0/3器件也能切到Mode 1/2。代码里用一个半周期计数器和一个phase标志区分每个SCLK周期的第一个边沿和第二个边沿从而统一判断采样沿。module spi_master #( parameter SCLK_DIV 8, // 系统时钟与SCLK的分频比必须为偶数 parameter CPOL 0, // 空闲电平0为低1为高 parameter CPHA 0, // 采样相位0为第一沿1为第二沿 parameter DATA_WIDTH 8 // 一次传输的位宽≤16 )( input wire clk, input wire rst_n, input wire start, input wire [DATA_WIDTH-1:0] tx_data, output reg [DATA_WIDTH-1:0] rx_data, output reg sclk, output reg mosi, input wire miso, output reg cs_n, output reg busy ); localparam IDLE 2d0; localparam RUN 2d1; localparam POST 2d2; localparam HALF SCLK_DIV / 2; reg [1:0] state; reg [15:0] clk_cnt; reg [3:0] bit_cnt; reg phase; wire half_done (clk_cnt HALF - 1); always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; sclk CPOL; cs_n 1b1; busy 1b0; clk_cnt 16d0; bit_cnt 4d0; phase 1b0; mosi 1b0; end else begin case (state) IDLE: begin sclk CPOL; cs_n 1b1; busy 1b0; if (start) begin cs_n 1b0; busy 1b1; bit_cnt 4d0; clk_cnt 16d0; phase 1b0; mosi tx_data[DATA_WIDTH-1]; state RUN; end end RUN: begin if (half_done) begin clk_cnt 16d0; phase ~phase; if (phase CPHA) begin // 采样沿接收当前位 rx_data[bit_cnt] miso; if (bit_cnt DATA_WIDTH-1) begin // 最后一位采完进入POST补完整最后一个时钟周期 sclk ~sclk; state POST; end else begin bit_cnt bit_cnt 1b1; sclk ~sclk; end end else begin // 驱动沿更新下一位 mosi tx_data[DATA_WIDTH-1 - bit_cnt]; sclk ~sclk; end end else begin clk_cnt clk_cnt 1b1; end end POST: begin // 让SCLK保持半个周期再回到空闲电平并释放CS_n if (half_done) begin sclk CPOL; cs_n 1b1; busy 1b0; state IDLE; end else begin clk_cnt clk_cnt 1b1; end end endcase end end endmodule核心逻辑是半周期计数器。HALF SCLK_DIV/2clk_cnt每数到HALF-1就产生一次SCLK边沿phase在边沿翻转0表示当前是第一个边沿1表示第二个边沿。因为CPHA参数本身就是采样沿所在相位所以用phase CPHA判断采样沿这一个判断同时兼容四个模式避免了在状态机里复制四个分支。参数使用时注意系统时钟50MHz、SCLK想跑1MHz时SCLK_DIV设50SCLK_DIV必须是偶数否则HALF取整会破坏占空比。DATA_WIDTH通常为8如果直接接24位ADC把参数改成24同时把bit_cnt位宽从reg [3:0]加宽到reg [7:0]。start拉高一个时钟周期即可cs_n在下一拍拉低MOSI预置最高位之后每两个半周期完成一位最后一个采样沿进入POST状态补完时钟周期再回到IDLE。3.2 为什么用单always状态机而不是三段式三段式状态机适合逻辑复杂、输出多的情况SPI主接口的协议本身IDLE/RUN/POST三个状态输出cs_n、sclk、mosi都依赖边沿相位用一个always块写反而容易保证非阻塞赋值顺序避免组合输出毛刺。对于老玩家我的边界建议是如果这个模块要挂到AXI4-Lite总线上就把协议引擎和总线接口分开SPI核心仍用单always用户侧再加独立的总线握手状态机。工程里保留的spi_out.vhd.bak是用VHDL写的另一版。如果你更熟悉VHDL可以对照同一组状态变量去读要注意VHDL里signal和variable的更新时机尤其是bit_cnt在采样沿递增时用variable会引发综合结果不一致。3.3 testbench至少要覆盖正常收发和CPOL/CPHA切换仿真里我把SCLK_DIV设成8这样50MHz时钟下SCLK约6.25MHz仿真时间短同时把外部miso手动置1这样rx_data能直观看出每一位都采到了1如果混进0就说明采样沿配置错位。testbench最小结构如下module tb_spi_master; reg clk 0; reg rst_n 0; reg start 0; reg [7:0] tx_data 8h9F; wire [7:0] rx_data; reg miso 1b1; wire sclk, mosi, cs_n, busy; spi_master #( .SCLK_DIV(8), .CPOL(0), .CPHA(0) ) dut ( .clk(clk), .rst_n(rst_n), .start(start), .tx_data(tx_data), .rx_data(rx_data), .sclk(sclk), .mosi(mosi), .miso(miso), .cs_n(cs_n), .busy(busy) ); always #10 clk ~clk; initial begin #30 rst_n 1; (posedge clk); start 1b1; (posedge clk); start 1b0; #2000; $display(rx %02h, rx_data); $finish; end endmodule仿真里把miso手动置1既验证了采样路径也暴露位序问题。把CPOL/CPHA在四组参数里各跑一遍观察sclk与mosi的相对位置比上板后再抓波形快得多。综合时再把SCLK_DIV改成实际分频比。4. 从代码到板卡Quartus II管脚分配、SDC约束与W25Q64联调4.1 先看原理图再进Pin PlannerSPI接错管脚不会烧板但会浪费一下午。拿到开发板原理图先确认三件事IO电平是3.3V还是1.8VMOSI/MISO有没有串联电阻CS_n有没有被外部上拉。很多板子为了让器件默认不被选中会在CS_n上接10k上拉FPGA输出端配置为强驱动时上拉电阻不影响逻辑但I/O标准必须一致。然后打开Quartus II的Pin Planner把逻辑信号分配到物理引脚。以一块Cyclone IV开发板为例映射如下信号FPGA管脚I/O标准开发板位置clkPIN_T93.3-V LVTTL50MHz晶振rst_nPIN_B33.3-V LVTTLKEY0sclkPIN_E13.3-V LVTTL扩展口J9_18mosiPIN_E23.3-V LVTTL扩展口J9_19misoPIN_E33.3-V LVTTL扩展口J9_20cs_nPIN_E43.3-V LVTTL扩展口J9_21操作路径是Assignments → Pin Planner在All Pins窗口选择信号双击Location下拉引脚I/O Standard选3.3-V LVTTL。改完保存到.qsf重新编译。更可控的方式是用Tcl命令在Quartus Tcl Console里执行set_location_assignment PIN_E1 -to sclk set_location_assignment PIN_E2 -to mosi set_location_assignment PIN_E3 -to miso set_location_assignment PIN_E4 -to cs_n set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to sclk基于脚本的管脚分配方便版本管理换板卡型号时只改映射文件不用在GUI里重新拖一遍。要特别注意如果原理图上CS_n所在bank是3.3V电源域就不要把它强行分配到1.8V bank否则即使编译通过上电后IO状态也可能不对。4.2 把SCLK当成虚拟时钟来做时序约束很多FPGA开发者在低速SPI项目里不写时序约束觉得1MHz频率跑得动直到外设换成AD7124或者SPI从设备要求MISO建立时间只有25ns才开始怀疑综合器布局。其实低速设计也该写SDC至少把外部路径的input/output delay注进来让时序分析器看到真实路径。因为SCLK是FPGA内部逻辑分频产生的Quartus不会自动把它当时钟树看所以要先为它建立一个虚拟时钟再约束与SCLK同步输出的MOSI、CS_n以及外部输入的MISO。在.sdc文件里加入create_clock -name sys_clk -period 20.000 [get_ports clk] create_clock -name v_sclk -period 200.000 set_output_delay -clock v_sclk -max 80.000 [get_ports {sclk mosi cs_n}] set_output_delay -clock v_sclk -min 10.000 [get_ports {sclk mosi cs_n}] set_input_delay -clock v_sclk -max 90.000 [get_ports miso] set_input_delay -clock v_sclk -min 20.000 [get_ports miso]这里的period 200ns对应SCLK5MHz适合把SCLK_DIV设为10的场合。output delay max/min表示从虚拟SCLK沿到FPGA引脚的数据到达时间窗口数值来自从设备手册中SCLK到MISO的Tco以及PCB走线时延估算。更精确的做法是在原理图阶段查从设备手册把Tco_max写进约束而不是用宽松示例值。约束写完还要在Settings里勾选SDC参与分析再重新编译看时序报告。如果不想深入研究最低限度也要确保.sdc被工程加载在.qsf里加set_global_assignment -name SDC_ENTRY_FILE spi.sdc否则约束文件不会生效时序分析器以为外部路径不存在。4.3 用W25Q64读ID验证整条链路把SPI主模块实例化在顶层分配好管脚后下载到板卡。用手动按键触发一次8位传输发送命令0x9F。W25Q64的读ID命令由CS_n拉低、发送0x9F、再连续读3字节组成返回Manufacturer ID、Memory Type和Capacity。由于我前面的模块每传输一帧都自动拉高CS_n做不了连续读所以顶层需要用一个小状态机控制“发命令再读数据”的过程或者临时改模块把CS_n释放出来。一次读ID的流程可以拆成四步拉低cs_n发送0x9F连续发三帧任意数据在每帧结束后从rx_data取1字节拉高cs_n。比对返回值正常是0xEF 0x40 0x18。如果读到0xFF先量SCLK有没有输出如果读到全0检查MISO和MOSI有没有接反如果只有第一字节正确、后面错多半是CS_n在最后一字节前被提前拉高导致从设备终止了后续读取。这个现象很常见也正好引到最后一个话题你怎么管理CS_n决定了SPI能不能做连续突发传输。5. 把CS_n从状态机里拆出来软件片选、连续读与SignalTap实测5.1 软件片选和硬件片选FPGA里应该怎么选前文模块里的cs_n由SPI状态机自动管理每传输一帧自动拉低、结束后自动拉高这种叫硬件片选。它适合单次写寄存器、读状态这类短事务遇到W25Q64连续读、OLED连续刷显存这类需求就必须把CS_n从协议状态机里拆出来改成软件片选——由更上层逻辑甚至外部MCU控制CS_n引脚SPI状态机只在CS_n有效时产生SCLK和移位数据。场景硬件片选软件片选单次8位访问适合可以但多写一层连续读/多字节需要扩展适合多从设备切换每设备一套状态机GPIO控制不同CS_n时序可控性内部确定受上层逻辑影响我的推荐做法是把SPI状态机的CS_n改成输入信号由外部总线控制片选时序。这样在上层状态机里就能实现“先拉低CS_n发命令再连续读多个字节最后拉高CS_n”的完整事务。这与Linux SPI驱动里software chip select的思路一致主控端用GPIO模拟片选硬件只保证字节流在SCLK上的相位正确。5.2 用SignalTap抓SPI波形一个晚上搞定时序问题Quartus II自带SignalTap II逻辑分析仪不需要示波器也能看内部信号。设置路径Assignments → Settings → SignalTap II Logic Analyzer采样时钟用系统时钟50MHz保证能抓到SCLK的每一个沿把sclk、mosi、miso、cs_n和state加入观察列表触发条件设为cs_n下降沿采样深度1024。重新综合下载运行一次读操作打开SignalTap看波形。SignalTap会占用片上RAM资源紧张时只抓sclk和cs_n。看波形时重点确认两件事一是在采样沿MISO是否已经稳定二是MOSI在驱动沿附近有没有发生跳变跳变位置是否与采样沿重叠。如果采样沿处MISO刚好在翻转说明SCLK相位没对齐。最简单可靠的修复是把采样点向后推半个系统时钟周期在采样逻辑里多打一拍reg miso_sync; always (posedge clk) begin miso_sync miso; end然后在采样沿用miso_sync替代miso。这个寄存器延迟能把采样点从边沿附近挪到数据稳定区低速SPI项目里大部分随机漏位问题都能这样解决。如果看到SCLK上有毛刺回到管脚约束检查I/O标准是否匹配、有没有开启Slew Rate。本文还有配套的精品资源点击获取
返回列表