简介:一套完整的基于FPGA的SPI通信接口设计实例,面向FPGA开发初学者及需要快速实现串行外设通信的工程师。资源覆盖SPI协议基础、接口逻辑实现、硬件连线与仿真验证全流程,内含时钟生成、状态机、数据移位寄存器、片选逻辑等关键模块,适合作为课程设计或项目前期的参考模板。压缩包共428个文件,约6.42MB,核心包括Quartus工程源码(tdf、vhd、bdf、qsf)、仿真波形文件(vwf)、配置与下载文件(sof、pof),以及说明文档(pdf、docx)和原理图截图,目录结构完整,便于按模块查阅和复用。目前已有299人学习下载,可帮助使用者快速理解SPI通信的状态机设计与验证方法,缩短开发调试周期。
1. 基于FPGA的SPI通信接口设计:为什么你需要这一套代码、原理图与仿真
不少FPGA开发者第一次对接ADC或Flash芯片时,会照着MCU的SPI代码思路直接写Verilog,结果仿真波形正常,上板后从设备就是不工作。问题往往不在协议本身,而在时钟分频、片选时序、输入约束和PCB引脚连接这些“外围”环节。一个完整的SPI通信接口设计,应该包含可综合的RTL代码、准确的原理图、可读的说明文档以及可复现的仿真环境。本文把这四部分串成一套可落地的方案,适合需要与SPI从设备通信、正在从入门转向独立调试的FPGA工程师。你能从中拿到可直接修改的Verilog模块、Testbench写法、原理图注意事项和上板验证方法。
2. SPI协议核心与FPGA通信接口的架构选型
2.1 SPI的4种工作模式与时序参数
SPI是一个同步串行接口,通常由SCLK、MOSI、MISO、CS_N四根线组成。FPGA作为主机时,必须根据从设备数据手册确定CPOL(时钟极性)和CPHA(时钟相位)。CPOL决定SCLK空闲电平,CPHA决定数据采样沿。四种模式定义如下所示。
| 模式 | CPOL | CPHA | 空闲电平 | 采样沿 | 移位沿 |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 低 | 上升沿 | 下降沿 |
| 1 | 0 | 1 | 低 | 下降沿 | 上升沿 |
| 2 | 1 | 0 | 高 | 下降沿 | 上升沿 |
| 3 | 1 | 1 | 高 | 上升沿 | 下降沿 |
FPGA实现时,不能把这些参数硬编码在时序逻辑里,而应该作为模块参数传入。比如CPOL决定片选拉低后、传输开始前SCLK的初始化电平;CPHA决定相对片选沿是立刻采样还是延迟半拍采样。很多从设备虽然标称支持“SPI Mode 0”,但实际时序图里的建立时间和保持时间窗口很窄,这时需要重新阅读数据手册,而不是照抄网上模板。
2.2 时钟分频与最大速率设计
FPGA内部系统时钟通常远高于SCLK频率。常见做法是在系统时钟域用计数器分频产生SCLK。假设系统时钟频率为F_clk,目标SCLK频率为F_sclk,那么分频计数器需要计数到DIV = F_clk / (2 * F_sclk)。例如50MHz系统时钟产生10MHz SCLK,则DIV = 50e6 / (2 * 10e6) = 2.5,这不是整数,需要采用非整数分频或调整目标频率。实际中建议选用整数分频,或者把SCLK设计成由系统时钟通过BUFG驱动,确保相位一致。
分频决定速率上限,但真正限制最高SCLK的往往是FPGA I/O时序和从设备的建立保持时间。在Vivado或Quartus中需要为输入信号MISO添加约束,常用语句如下:
set_input_delay -clock sclk -max 8.0 [get_ports miso] set_input_delay -clock sclk -min 2.0 [get_ports miso]这里约束的意思是MISO相对于SCLK的输入路径最大延迟8ns、最小延迟2ns。如果不加这个约束,综合工具默认MISO与SCLK同源,实际PCB走线延迟和从设备输出延迟会被忽略,导致时序分析过于乐观。具体数值可从从设备datasheet和PCB布线长度估算。
2.3 硬件片选与软件片选在FPGA侧的取舍
FPGA片选线通常直接由GPIO控制,这属于硬件片选。硬件片选的优点是拉低、拉高的时机完全由RTL状态机控制,可以精确保证CS建立时间。软件片选则常见于Linux下的SPI驱动,由驱动在传输消息前手动拉CS,但FPGA内部做协议处理时很少用软件片选,因为软件可编程的延迟不可控。
这里需要注意,某些从设备要求CS_N在整个传输期间保持低电平,且CS_N上升沿出现时SCLK必须已经回到空闲电平。如果FPGA内部把CS_N和SCLK由不同逻辑产生,就可能出现CS_N提前拉高导致从设备截断数据。所以片选信号必须由主状态机统一控制,不能由计数器独立翻转。
2.4 顶层架构:状态机加移位寄存器
FPGA实现SPI主机的顶层结构通常包括四个部分:分频器、发送/接收移位寄存器、SCLK发生器、状态机。分频器产生SCLK使能脉冲;移位寄存器在SCLK边沿移出MOSI并移入MISO;状态机控制CS_N拉低、传输开始、位计数、CS_N拉高。一个简洁的状态机可以只包含IDLE、TRANSFER、DONE三态。在TRANSFER状态中,每产生一个SCLK沿就移位一次,计数器到DATA_WIDTH时跳回IDLE并拉高CS_N。这个架构的好处是SCLK和CS_N在同一时钟域内生成,综合后能准确分析时序。
实现时最干净的方法是:所有信号在系统时钟clk的上升沿改变,SCLK由分频使能信号sclk_tick驱动翻转,这样SCLK实际是系统时钟的派生时钟,而不是独立的时钟域。
3. FPGA SPI通信接口的Verilog代码实现(可综合)
3.1 模块接口与参数化定义
下面是一个可综合的SPI主机模块,参数包括数据位宽、分频系数、极性、相位。接口信号包含系统时钟、异步复位、启动握手以及四线SPI信号。
module spi_master #( parameter DATA_WIDTH = 8, parameter DIVIDER = 50, // clk频率 / SCLK频率 / 2 parameter CPOL = 0, parameter CPHA = 0 )( 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 wire sclk, // SPI时钟 output wire mosi, // 主出从入 input wire miso, // 主入从出 output wire cs_n, // 片选,低有效 output reg busy ); reg [7:0] div_cnt; reg sclk_en; // SCLK使能脉冲 reg sclk_reg; reg [DATA_WIDTH-1:0] tx_shift; reg [DATA_WIDTH-1:0] rx_shift; reg [4:0] bit_cnt; reg [1:0] state; localparam IDLE = 2'd0; localparam TRANSFER = 2'd1; localparam DONE = 2'd2; // SCLK分频 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin div_cnt <= 0; sclk_en <= 0; end else if (state == IDLE) begin div_cnt <= 0; sclk_en <= 0; end else begin if (div_cnt == DIVIDER - 1) begin div_cnt <= 0; sclk_en <= 1; end else begin div_cnt <= div_cnt + 1; sclk_en <= 0; end end end // SCLK反转与片选 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin sclk_reg <= CPOL; cs_n <= 1'b1; end else begin if (state == IDLE) begin sclk_reg <= CPOL; cs_n <= 1'b1; end else if (sclk_en) begin sclk_reg <= ~sclk_reg; cs_n <= 1'b0; end end end // 移位寄存器与状态机 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin tx_shift <= 0; rx_shift <= 0; rx_data <= 0; bit_cnt <= 0; state <= IDLE; busy <= 0; end else begin case (state) IDLE: begin busy <= 1'b0; if (start) begin tx_shift <= tx_data; bit_cnt <= DATA_WIDTH; state <= TRANSFER; busy <= 1'b1; end end TRANSFER: begin if (sclk_en && (sclk_reg == (CPOL ^ CPHA))) begin mosi_tmp <= tx_shift[DATA_WIDTH-1]; // 具体移位逻辑见下面说明 end end // 省略部分代码 endcase end end // 注意:上面省略了部分状态判断,完整实现见3.2节 endmodule上面的代码片段展示了参数定义和分频结构,但没有完成移位。原因在于CPHA不同的模式对应不同的采样沿,需要仔细设计状态机。参数CPOL ^ CPHA用于选择SCLK边沿:当CPOL和CPHA相同时(模式0、3),在SCLK为高时采样;不同时(模式1、2),在SCLK为低时采样。这里需要结合具体的边沿条件。
3.2 完整移位逻辑与状态机
为了让代码可综合且易读,这里给出一个经过简化的完整传输状态机。关键设计思路:在每个SCLK边沿时,MOSI输出当前最高位,MISO数据移入最低位。位计数递减到0表示传输完成。
// 传输过程中的移位与计数 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin tx_shift <= 0; rx_shift <= 0; rx_data <= 0; bit_cnt <= 0; state <= IDLE; busy <= 1'b0; end else begin case (state) IDLE: begin busy <= 1'b0; if (start) begin tx_shift <= tx_data; rx_shift <= 0; bit_cnt <= DATA_WIDTH; state <= TRANSFER; busy <= 1'b1; end end TRANSFER: begin // 在采样沿(由CPHA决定)增量位 if (sclk_en && (sclk_reg == (CPOL ^ CPHA))) begin rx_shift <= {rx_shift[DATA_WIDTH-2:0], miso}; bit_cnt <= bit_cnt - 1; end // 在移位沿(与采样沿相反)设置MOSI if (sclk_en && (sclk_reg != (CPOL ^ CPHA))) begin if (bit_cnt > 0) begin tx_shift <= {tx_shift[DATA_WIDTH-2:0], 1'b0}; end end if (bit_cnt == 0) begin rx_data <= rx_shift; state <= DONE; end end DONE: begin state <= IDLE; busy <= 1'b0; end endcase end end endmodule这段代码中,sclk_reg == (CPOL ^ CPHA)对应采样沿,sclk_reg != (CPOL ^ CPHA)对应移位沿。当CPHA=0时,采样发生在第一个边沿(上升沿),此时sclk_reg从CPOL变为非CPOL,条件为==?实际情况需要补充一个判断。更严谨的方法是使用sclk_en脉冲配合边沿方向检测,同时需要处理CPHA=0时首bit在CS拉低后立即送出。建议不直接依据sclk_reg电平,而用posedge_sclk或negedge_sclk脉冲。由于篇幅限制,上述代码可作为框架,实际使用时需配合Testbench修正边沿条件。
3.3 参数对行为的影响与常见错误
DIVIDER影响SCLK频率,但不会改变相位关系。CPOL改变空闲电平和输出有效电平,CPHA决定数据采样点。常见错误是把CPOL和CPHA同时当成时钟极性,没有考虑CPOL ^ CPHA的关系。还有一个高频错误:在always @(posedge sclk)中把SCLK当作时钟使用,导致综合出跨时钟域逻辑,时序难收敛。正确做法是始终使用系统时钟作为主时钟,SCLK作为普通信号由分频产生,并通过使能脉冲控制采样。这样写出来的代码在set_input_delay约束下才能准确分析。
3.4 用仿真快速验证参数配置
在写Testbench时,建议先固定CPOL=0, CPHA=0,确认基本传输功能,再切换到其他模式。通过对比mosi和miso波形,能够快速发现片选沿与采样沿的错位。后续章节给出Testbench代码和验证方法。
4. 原理图绘制与说明文档的组织
4.1 原理图中的SPI信号连接与引脚分配
原理图不只是把FPGA的引脚和从设备连接起来,还要考虑上电状态、驱动能力和信号完整性。FPGA作为主机,MOSI和SCLK是推挽输出,不需要上拉;MISO是从设备输出,如果从设备在未选中时输出高阻,则必须在MISO上加10kΩ上拉电阻,否则会造成浮空输入。CS_N通常有上拉电阻,确保FPGA在复位期间从设备不被误选中。若FPGA和从设备电平不同,需要加电平转换芯片。高SPI速率下,推荐使用支持方向自动判断的TXS0108E,但要注意其最高速率与容性负载限制。
绘制原理图时,网络标号要与Verilog端口名一致,比如mosi、miso、sclk、cs_n。不要用SDA或SCL这类I2C命名,容易混淆。同时标注FPGA引脚对应的Bank电压域,确保IO标准匹配。
4.2 说明文档应包含的接口规格表
说明文档是一份“给下一个开发者”的交接资料。除了模块功能介绍,必须包含一张完整的接口规格表。下面是一个常见模板。
| 信号名 | 方向 | 位宽 | 说明 |
|---|---|---|---|
| clk | 输入 | 1 | 系统时钟,100MHz,差分输入需转换 |
| rst_n | 输入 | 1 | 异步复位,低有效 |
| start | 输入 | 1 | 启动一次传输,高电平至少保持一个clk周期 |
| tx_data | 输入 | 8 | 待发送数据,在start拉高前稳定 |
| rx_data | 输出 | 8 | 接收数据,在busy拉低后有效 |
| sclk | 输出 | 1 | SPI时钟,频率 = clk/(2*DIVIDER) |
| mosi | 输出 | 1 | 主机输出数据 |
| miso | 输入 | 1 | 从机输出数据 |
| cs_n | 输出 | 1 | 片选,低有效 |
| busy | 输出 | 1 | 高电平表示正在传输 |
文档还要画出时序图,不必用专业EDA工具,用Wavedrom或简单的SVG即可。时序图中必须标注建立时间和保持时间窗口,说明数据在哪条边沿采样。另外,在“注意事项”一节写明本设计不支持全双工同时收发时的LSB先传,默认MSB先传。
4.3 PCB布局中的SPI信号走线建议
在原理图之后的PCB Layout阶段,SCLK和MOSI走线要避免过长,走线长度差控制在100mil以内,尤其高速SPI(>20MHz)时建议包地。CS_N走线尽量远离SCLK,防止开关噪声耦合导致误触发。FPGA引脚选择时,避免把SPI信号分配到差分对专用引脚,否则需要额外使用IOBUFDS,增加复杂度。同时,尽量让所有SPI信号位于同一个Bank,方便统一配置IO标准。
4.4 原理图与RTL一致性检查
板卡回来后最痛苦的问题是“原理图与代码不一致”。比如RTL中CS_N是低有效,原理图却连接到从设备的CS(高有效),导致片选逻辑反向。检查方法是:从模块端口列表中导出信号列表,与原理图网络表逐项比对。可以用Tcl脚本读取Vivado的open_schematic然后导出csv,再用Python脚本与原理图BOM比对。这一步虽然没有技术含量,但能省下数天调试时间。
5. 仿真Testbench设计与波形验证
5.1 模拟SPI从设备行为的Testbench框架
仿真需要构造一个虚拟SPI从设备,用来回放MISO数据并检查MOSI输出。下面是一个适用于模式0的极简Testbench。它用系统时钟驱动DUT,同时在SCLK边沿将MISO置为固定值。
`timescale 1ns/1ps module tb_spi_master; reg clk; reg rst_n; reg start; reg [7:0] tx_data; reg miso; wire sclk; wire mosi; wire cs_n; wire [7:0] rx_data; wire busy; spi_master #( .DIVIDER (50), .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; // 模拟从设备:上升沿采样MOSI,下降沿输出MISO reg [7:0] slave_data = 8'hF0; always @(posedge sclk or negedge cs_n) begin if (!cs_n) begin // 每个SCLK上升沿后,将slave_data左移并输出最高位 miso <= slave_data[7]; slave_data <= {slave_data[6:0], 1'b0}; end end initial begin clk = 0; rst_n = 0; start = 0; tx_data = 0; miso = 0; #100; rst_n = 1; #50; tx_data = 8'hA5; @(posedge clk); start = 1; #20; start = 0; // 等待传输完成 wait(!busy); #50; $display("rx_data = %h", rx_data); $finish; end endmoduleTestbench中always @(posedge sclk)模拟从设备行为,但要注意,实际从设备的输出延迟可能正好落在采样沿附近,导致仿真出现亚稳态。此时可以通过#1延迟让MISO在SCLK边沿后改变,更接近真实情况。上例为了简洁没有加延迟,实际应用中建议在posedge sclk后加#1。
5.2 仿真波形检查的三个关键指标
打开仿真波形后,不要只关心rx_data是否正确,还要检查以下几点。
| 检查项 | 位置 | 通过标准 |
|---|---|---|
| CS_N时序 | 传输开始和结束时 | CS_N拉低后至少一个SCLK半周期才出现第一个有效沿;结束前最后一个采样沿完成后再拉高 |
| SCLK频率与占空比 | 任意一段 | SCLK周期约为200ns(DIVIDER=50时),占空比接近50% |
| MOSI数据变化点 | 每个SCLK沿 | MOSI在移位沿改变,在采样沿稳定,变化沿与采样沿不重叠 |
如果发现CS_N与SCLK同时变化,说明状态机中片选信号的赋值存在时序竞争。这时需要把cs_n的产生放在另一个always块中,并且使用nonblocking赋值,避免在IDLE到TRANSFER切换时出现毛刺。
5.3 常见仿真陷阱:复位、X态与时钟使能
复位释放后,如果DUT内部寄存器没有初始化,仿真会出现很多X态。建议在DUT中为所有寄存器添加initial语句或者依赖复位信号给出确定值。另外,Testbench中避免使用#20这种绝对延时来释放复位,而应该用时钟边沿同步释放,如下所示:
initial begin @(negedge clk); rst_n = 1; end这样可以消除复位释放与时钟沿之间的建立/保持冲突。还有一个容易被忽略的陷阱:在initial块中给miso赋值时,如果与always @(posedge sclk)同时驱动同一个miso变量,仿真会警告“multiple drivers”。解决方法是只使用一个驱动源,或者将miso定义成wire类型,在always块中使用reg变量后通过连续赋值驱动。
5.4 用仿真验证多模式切换
在Testbench中可以通过参数重定义或多次初始化来验证四种SPI模式。由于DUT参数是编译期常量,验证不同模式需要例化多个DUT或者使用defparam。更实用的办法是把CPOL和CPHA作为仿真变量,但RTL代码中不能把参数换成变量。一种变通设计是把模式配置做成寄存器,但会增加复杂度。对于大多数场景,建议在Testbench中用generate块例化四个DUT,每个使用不同的参数组合,然后分别对比收发数据。这样做可以一次性确认四种模式下的时序都正确。
6. 进阶技巧:从接口验证到系统集成的几个实用方法
6.1 回环测试与逻辑分析仪验证
最有效的上板验证是回环测试:将FPGA的MOSI引脚与MISO引脚在PCB上直接短接,或者在顶层约束中把两者连接的IO短接。此时发送的数据应该原样接收。如果回环测试失败,先检查IO约束是否生效,再用内嵌逻辑分析仪(Vivado ILA或SignalTap)抓取SCLK、CS_N和MOSI波形。注意ILA采样率必须至少为SCLK的4倍,否则无法分辨边沿。
6.2 用AXI Quad SPI IP核替代自研模块的取舍
当SPI从设备数量增多或需要与MicroBlaze处理器交互时,Xilinx的AXI Quad SPI IP核是常见选择。它的优势是支持多从设备、自动配置寄存器,缺点是时序行为被封装成黑盒,遇到通信异常时难以调试。自研模块的优势在于完全可控,且代码量不大。如果要使用IP核,记得在配置界面中勾选FIFO选项,并在软件中通过AXI总线写入控制寄存器。对于纯FPGA逻辑,自研模块更轻量,也更容易集成到自定义状态机中。
6.3 用FIFO缓存实现批量传输
当需要连续发送大量数据时,单次SPI传输会产生间隔,浪费带宽。常见做法是在FPGA内部用FIFO做缓冲,主状态机自动从FIFO读取数据并连续发起SPI传输。FIFO深度位宽要根据从设备页大小设置,比如Flash页写入通常是256字节,FIFO至少256深度。注意FIFO空满信号与SPI忙信号配合,避免传输一半时FIFO为空。这种设计属于“FPGA IP核缓存索引重组”思路中的一种,但核心还是把SPI状态机封装成“带流控的发送通道”。
6.4 高性能模式:DSPI与QSPI的扩展
标准SPI每次传输1bit,DSPI在MOSI/MISO基础上增加一根IO0和IO1,实现双倍数据率;QSPI则是四根数据线。FPGA实现QSPI时,不能把数据线直接接到普通IO,必须使用IO状态机动态切换方向。这个进阶方向需要重新设计状态机,但基础仍是本文中的SPI主状态机。如果只是调试Flash,许多FPGA厂商的内存控制器已原生支持QSPI,可直接例化IP核。
在实际项目中,先把标准SPI接口调通,再用回环测试验证收发一致性,最后根据需求决定是否引入FIFO或IP核。整个设计闭环的核心在于:代码、原理图、文档、仿真互相印证,任何一环缺失,调试时都会多浪费数倍时间。
本文还有配套的精品资源,点击获取