
简介这是一份针对基于FPGA的SPI通信接口设计的工程实践包适合FPGA开发者、电子相关专业学生以及希望掌握串行通信协议和硬件描述语言的入门者旨在解决FPGA与传感器、存储器、显示模块等外部SPI外设之间的高速数据交换问题。资源共428个文件压缩包约5.47MB内部包含Quartus II工程与编译生成文件qpf、qsf、bdf、cdb、hdbVerilog/VHDL源码、仿真波形vwf、下载配置文件sof/pof以及pdf、docx设计说明工程文件用于打开和配置项目源码负责状态机与收发逻辑仿真文件用于验证时序设计文档适合对照阅读、快速上手。目前已有1160人学习下载。内容从SPI四线原理及时序关系讲起覆盖MISO、MOSI、SCLK、CS信号在状态机中的调度以及Pin Planner管脚分配、外部设备连接和硬件调试要点其中还包含开发板原理图和关键报告读者既能深入理解协议底层机制也能掌握在Quartus II中完成设计、综合、仿真与下载的完整开发流程。整体体量紧凑、结构清晰适合作为FPGA课程设计或自学串行接口开发的有力参考。1. 从SPI协议到FPGA接口为什么工程师常卡在时序而不在逻辑把一块SPI接口的ADC挂在FPGA上大多数人第一反应是照着数据手册写个状态机但真正调起来才发现问题总出在采样沿和建立时间上。SPI只有四根线协议本身简单到一眼能看完难点在于FPGA侧的时钟域规划、IO时序约束和信号完整性这三件事做不好逻辑再漂亮也是白搭。本文按照从协议到实现、从仿真到板级的路径把基于FPGA的SPI通信接口设计完整拆开覆盖状态机怎么写、时钟怎么分、约束怎么加、波形怎么抓并在最后用W25Q64的JEDEC ID读取作为闭环验证。适合正在做FPGA与EEPROM、ADC、传感器或无线芯片通信的工程师也适合刚入门FPGA、被SPI时序折腾得想换方案的人。2. SPI协议的四线本质与模式选择FPGA侧先解决的两个问题2.1 MOSI、MISO、SCLK、CS的信号方向与主从约定SPI是同步全双工总线主机产生时钟和片选从机在时钟驱动下收发数据。四条线里SCLK和CS永远由主机控制MOSI是主机输出从机输入MISO是主机输入从机输出。FPGA作为主机时MOSI是输出引脚MISO是输入引脚SCLK和CS是输出引脚FPGA作为从机时方向正好反过来。写代码前先把引脚方向定清楚这在FPGA里直接对应IO Bank的约束。很多人习惯用inout双边脚但SPI除了个别三线模式外不需要inoutMISO和MOSI是独立的单方向引脚用inout反而会把简单问题复杂化。方向确认后下一个问题是从机数量。一个SPI主机可以挂多个从机每个从机一根CSSCLK和MOSI/MISO共享。FPGA作为主机时CS信号用普通IO引脚拉低即可不用像MCU那样单独配置硬件片选外设。这里涉及一个热搜里经常提到的概念——硬件片选与软件片选的区别硬件片选由外设控制器自动控制时序软件片选则是在状态机里手动拉低拉高CS引脚。FPGA做SPI主机的常见做法是软件片选因为状态机本身就能精确控制CS拉低的时刻和持续周期。2.2 CPOL和CPHA怎么选从机的采样沿决定你的寄存器写法SPI的四种工作模式由CPOL时钟极性和CPHA时钟相位组合决定。CPOL决定空闲时SCLK是高电平还是低电平CPHA决定数据是采样在第一个边沿还是第二个边沿。对FPGA设计者来说这两个参数直接决定状态机里移位寄存器的触发沿写错一个沿读回来的数据整体错一位而且是那种仿真看不出、上板才发现的错误。判断模式的方法很简单看从机数据手册里时序图上的SAMPLE点。手册里通常会把采样沿标成向上的箭头或直接用虚线标出采样时刻。最常见的组合是模式0CPOL0CPHA0即空闲时SCLK为低数据在上升沿采样、下降沿切换。但不要默认所有芯片都是模式0像某些ADC默认是模式1或模式3必须逐颗确认。模式CPOLCPHA空闲电平采样沿数据切换沿000低上升沿下降沿101低下降沿上升沿210高下降沿上升沿311高上升沿下降沿FPGA侧实现时推荐在采样沿的前半个周期准备好数据也就是在数据切换沿更新移位寄存器在采样沿采集MISO。这样能让数据稳定时间最长对信号完整性最有利。这一点在SPI时钟频率较高时尤其重要后面章节会再展开。2.3 SPI与IIC的区别一句话讲清为什么FPGA工程师偏爱SPI热词里频繁出现“iic和spi的区别”这里用一句话说透IIC只有两根线但要应答机制和地址帧SPI四根线但全双工、速度快、无需应答。IIC适合低速、线数受限的板内通信SPI适合需要吞吐量或低延迟的场景比如FPGA与高速ADC、DAC、Flash之间的数据传输。对FPGA实现而言SPI的优势在于时序完全由主机掌控不必像IIC那样处理开漏上拉和仲裁。SPI从机不需要地址CS拉低即为选中这省去了解析地址帧的逻辑。SPI的劣势是线数多每多一个从机就要多一根CS。此外SPI没有流控机制主机发多少从机就得收多少所以FPGA侧通常需要自己规划设计FIFO深度防止数据溢出。3. 用Verilog在FPGA上实现SPI主机状态机、移位寄存器与时钟分频3.1 SPI主机模块的端口定义与内部结构划分在FPGA上实现SPI主机常见的做法是把模块拆成四个部分时钟分频器、发送移位寄存器、接收移位寄存器和主状态机。时钟分频器从系统时钟产生SCLK发送移位寄存器把并行数据转成串行输出到MOSI接收移位寄存器把MISO上的串行数据转成并行输出主状态机负责协调整个传输流程。端口定义上我一般会这样设计module spi_master #( parameter DATA_WIDTH 8, parameter SCLK_DIV 4 )( input wire clk, input wire rst_n, // 用户接口 input wire start, input wire [DATA_WIDTH-1:0] tx_data, output wire [DATA_WIDTH-1:0] rx_data, output wire busy, output wire done, // SPI物理接口 output wire sclk, output wire mosi, input wire miso, output wire cs_n );SCLK_DIV参数控制分频系数SCLK频率等于系统时钟频率除以SCLK_DIV的平方具体推导在下一节展开。busy信号用于告知上层模块当前正在传输done在传输完成时拉高一个周期。这里的cs_n是低有效片选与大多数SPI从机的约定一致。3.2 状态机设计一次SPI传输从头到尾的五个周期SPI主状态机可以简化为五个状态空闲、启动、传输、结束和完成。空闲状态下CS保持高电平一旦捕获到start信号就进入启动状态拉低CS并准备发送第一位数据。传输状态中移位寄存器在数据切换沿更新数据同时在采样沿从MISO采入数据。结束状态拉高CS完成状态输出done信号。状态机的核心代码如下采用三段式写法便于后期维护和时序收敛localparam IDLE 3d0; localparam START 3d1; localparam TRANSFER 3d2; localparam FINISH 3d3; localparam COMPLETE 3d4; reg [2:0] state, next_state; reg [DATA_WIDTH-1:0] tx_shift; reg [DATA_WIDTH-1:0] rx_shift; reg [3:0] bit_cnt; reg sclk_reg; reg cs_n_reg; // 状态转移逻辑 always (posedge clk or negedge rst_n) begin if (!rst_n) state IDLE; else state next_state; end // 次态组合逻辑 always (*) begin next_state state; case (state) IDLE: if (start) next_state START; START: next_state TRANSFER; TRANSFER: if (bit_cnt DATA_WIDTH) next_state FINISH; FINISH: next_state COMPLETE; COMPLETE: if (!start) next_state IDLE; default: next_state IDLE; endcase end位计数的逻辑是bit_cnt从0累加到DATA_WIDTH-1当计数等于DATA_WIDTH时说明所有位都已发送完毕。这里有一个细节bit_cnt的递增必须发生在采样沿而不是数据切换沿否则最后一位的采样可能在状态机跳转到FINISH之后才完成导致丢位。处理方式是让bit_cnt在采样沿判断是否等于DATA_WIDTH-1等于则下一周期回到FINISH状态。3.3 SCLK生成与分频系数计算SCLK由系统时钟分频产生。最简单可靠的方法是用计数器产生对称方波计数器从0计数到SCLK_DIV-1翻转一次SCLK再计数到SCLK_DIV-1再翻转一次这样SCLK的周期是系统时钟周期的2*SCLK_DIV倍。reg [7:0] clk_cnt; reg sclk_en; always (posedge clk or negedge rst_n) begin if (!rst_n) clk_cnt 0; else if (state TRANSFER) begin if (clk_cnt SCLK_DIV-1) clk_cnt 0; else clk_cnt clk_cnt 1b1; end else clk_cnt 0; end always (posedge clk or negedge rst_n) begin if (!rst_n) sclk_reg 1b0; else if (state TRANSFER) begin if (clk_cnt SCLK_DIV-1) sclk_reg ~sclk_reg; end else sclk_reg 1b0; // 空闲时拉低适配CPOL0 end分频系数怎么定取决于从机支持的SCLK上限和FPGA系统时钟频率。比如系统时钟100MHz从机最高支持20MHz那么SCLK_DIV取3SCLK约为16.7MHz。这个值留了约20%的余量覆盖了PCB走线延迟和IO输出延迟。注意这里SCLK不是严格50%占空比因为计数器从0到SCLK_DIV-1才翻转一次实际占空比近似50%对SPI协议完全够用。3.4 数据切换与采样时刻MOSI在半周期前更新MISO在边沿捕获模式0下数据在SCLK上升沿采样在SCLK下降沿切换。对应到Verilog实现就是在检测到SCLK下降沿的时刻更新tx_shift在SCLK上升沿的时刻采样miso存入rx_shift。但直接用posedge sclk做时钟域会产生跨时钟域问题更稳妥的方式是用系统时钟检测SCLK的边沿。reg sclk_prev; wire sclk_rising sclk_reg ~sclk_prev; wire sclk_falling ~sclk_reg sclk_prev; always (posedge clk or negedge rst_n) begin if (!rst_n) sclk_prev 1b0; else sclk_prev sclk_reg; end // 下降沿切换MOSI数据 always (posedge clk or negedge rst_n) begin if (!rst_n) tx_shift 0; else if (state START) tx_shift tx_data; else if (sclk_falling (state TRANSFER)) tx_shift {tx_shift[DATA_WIDTH-2:0], 1b0}; end // 上升沿采样MISO数据 always (posedge clk or negedge rst_n) begin if (!rst_n) rx_shift 0; else if (sclk_rising (state TRANSFER)) rx_shift {rx_shift[DATA_WIDTH-2:0], miso}; endtx_shift在下降沿左移一位最低位补0同时把最高位送到MOSI引脚rx_shift在上升沿把MISO采样进来并左移。采样时刻比数据切换时刻晚半个SCLK周期这就是建立时间余量的来源。从机在上升沿采样时MOSI上的数据已经稳定了半个周期足够满足绝大多数从机的建立时间要求。4. 时序约束与板级调试set_input_delay、逻辑分析仪和数据采样窗口4.1 为什么FPGA的SPI接口需要时序约束不约束也能跑但频率上不去很多人在工程里写完SPI逻辑就直接跑板子不写任何SDC约束因为几百kHz的SPI确实不约束也能出波形。但一旦SCLK提到10MHz以上不约束的影响就暴露出来仅靠内部路径延迟综合器无法知道MISO的数据相对于SCLK什么时候有效采样得到的数据可能在不同位之间出现错误。时序约束的本质是告诉综合器数据相对于时钟的到达时间和要求时间。对SPI接口来说时钟是由FPGA内部产生的数据既有输出到从机的MOSI也有从机返回来的MISO。输出的MOSI受set_output_delay约束输入的MISO受set_input_delay约束。SPI的SCLK频率不高通常不需要做复杂的多周期约束但基本的IO约束必须写。4.2 set_input_delay与set_output_delay的参数设定方法这里以外接从机、SCLK由FPGA产生为例给出一个可以直接套用的约束模板# 指定SCLK为生成时钟 create_clock -name sys_clk -period 10.0 [get_ports clk] create_generated_clock -name spi_sclk -source [get_ports clk] \ -divide_by 6 [get_ports sclk] # 输入延迟约束MISO相对SCLK set_input_delay -clock spi_sclk -max 2.0 [get_ports miso] set_input_delay -clock spi_sclk -min 0.5 [get_ports miso] # 输出延迟约束MOSI和CS相对SCLK set_output_delay -clock spi_sclk -max 3.0 [get_ports {mosi cs_n}] set_output_delay -clock spi_sclk -min 0.5 [get_ports {mosi cs_n}]参数怎么定依据是从机数据手册里的建立时间tSU和保持时间tH。set_input_delay max表示数据在SCLK边沿之前多久到达值由PCB走线延迟和从机输出延迟之和决定set_input_delay min表示数据在SCLK边沿之后还要保持多久。maxmin的值应小于一个SCLK周期否则采样窗口会变成负的。如果你的从机手册没给具体数值常见做法是先按max0.4*周期min0.1*周期估算跑完时序报告再调整。4.3 用ILA抓SPI波形触发条件设为CS下降沿最常见约束做完后板级验证靠逻辑分析仪。Vivado里的ILAIntegrated Logic Analyzer是最常用的手段ILA探针直接连接内部信号无需额外引脚。抓取SPI波形时的探针建议包含cs_n_reg、sclk_reg、mosi、miso、state。触发条件设置为cs_n_reg下降沿因为一次传输只有一个下降沿不会像SCLK那样触发几百次。ila_spi u_ila ( .clk(clk), .probe0(state), .probe1(cs_n_reg), .probe2(sclk_reg), .probe3(mosi), .probe4(miso) );ILA的采样时钟建议直接用系统时钟而不是SCLK因为系统时钟频率更高能同时看到SCLK中间时刻的MISO毛刺和稳定电平。抓完波形后重点检查两点MOSI的数据切换是否在SCLK下降沿附近MISO的采样点是否落在数据稳定区。如果采样点落在了数据跳变沿附近说明set_input_delay的max值设置过大或过小需要按实物波形反推调整。4.4 片上示波器之外误码率测试与边缘检测除了看波形还有一个更实用的验证方法——回环测试。把FPGA的MOSI直接短接到MISO引脚发送一个预设的8位数据接收到的数据与发送的逐位比较。回环测试能验证链路本身是否完好但不能验证从机接口时序因为回环没有真实从机的输出延迟。更接近真实情况的验证是接一个已知ID的SPI Flash读取它的JEDEC ID寄存器ID读错则说明时序有问题。调试中发现的最典型误区是把仿真波形和ILA波形混为一谈。仿真无延迟ILA反映的是实际IO延迟。同一段代码仿真通过但上板失败优先排查时序约束里有没有写IO延迟其次排查PCB走线长度是否导致SCLK和MISO之间存在过大偏差。走线超过5厘米且SPI时钟超过20MHz时建议把SCLK频率降一半再测试通过后再逐步升频。5. 实战FPGA通过SPI读取W25Q64的JEDEC ID与四个坑5.1 W25Q64的读ID指令格式与通用SPI读操作模板W25Q64是华邦的SPI NOR Flash也是FPGA开发板上最常见的SPI从机之一。读取JEDEC ID的指令是0x9F主机发送0x9F后从机依次返回厂商ID和器件ID共3个字节。对W25Q64来说返回的应该是0xEF、0x40、0x17。这个流程只用到一个SPI读操作模板先发送指令字节然后继续发送任意数据通常全0同时从MISO接收数据。在SPI主机模块之上需要再包一层Flash控制状态机localparam SEND_CMD 2d0; localparam READ_ID 2d1; localparam DONE_ST 2d2; reg [1:0] flash_state; reg [DATA_WIDTH-1:0] cmd_data; reg spi_start; wire spi_busy; wire [DATA_WIDTH-1:0] spi_rx; always (posedge clk or negedge rst_n) begin if (!rst_n) begin flash_state SEND_CMD; spi_start 1b0; end else begin case (flash_state) SEND_CMD: begin if (!spi_busy) begin spi_start 1b1; cmd_data 8h9F; end if (spi_done) begin spi_start 1b0; flash_state READ_ID; end end READ_ID: begin // 连续读取3个字节 end endcase end end5.2 读取状态寄存器轮询判断Flash是否忙的关键命令写入或擦除操作需要轮询状态寄存器。W25Q64的状态寄存器地址是0x05主机发送0x05后连续读取数据bit0为1表示忙为0表示空闲。轮询状态寄存器的频率不能太高常见做法是每轮询一次间隔10到100微秒避免占用SPI总线太久。5.3 Flash读写中的4个常见坑坑一是写操作前忘了发写使能指令0x06W25Q64在每次写页、擦除操作前必须先发写使能否则操作被忽略。坑二是读取操作时发送的读取长度不够比如读ID需要连续发送3个字节的哑数据很多第一次写的人只发一个字节结果只收到一个字节的ID后面全是0xFF。坑三是SCLK频率超过从机上限W25Q64标准模式最高支持50MHz但实际在FPGA板上往往因信号完整性达不到理论值测试时先降到10MHz验证逻辑再升频。坑四是CS拉高的时刻不对SPI Flash要求CS拉高时SCLK必须处于空闲电平否则从机可能误判下一个SPI操作导致后续指令全部失效。这些坑的特点是仿真全通过上板全失效所以板级调试时建议先用ILA把一次完整读操作的CS、SCLK、MOSI、MISO都抓下来再用波形对照指令时序图。把一次读ID的波形对照完后再验证写使能、读状态、页编程三个流程这时你的SPI主机接口才算真正打通这套状态机加约束的组合可以复用到FPGA tdc直方图读取、图像处理前的传感器配置、以及任何需要和SPI从机打交道的项目里。本文还有配套的精品资源点击获取