尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

FPGA SPI通信接口设计:从Verilog代码到仿真的完整实现方案

FPGA SPI通信接口设计:从Verilog代码到仿真的完整实现方案 简介一套完整的基于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决定数据采样沿。四种模式定义如下所示。模式CPOLCPHA空闲电平采样沿移位沿000低上升沿下降沿101低下降沿上升沿210高下降沿上升沿311高上升沿下降沿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 2d0; localparam TRANSFER 2d1; localparam DONE 2d2; // 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 1b1; end else begin if (state IDLE) begin sclk_reg CPOL; cs_n 1b1; end else if (sclk_en) begin sclk_reg ~sclk_reg; cs_n 1b0; 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 1b0; if (start) begin tx_shift tx_data; bit_cnt DATA_WIDTH; state TRANSFER; busy 1b1; 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 1b0; end else begin case (state) IDLE: begin busy 1b0; if (start) begin tx_shift tx_data; rx_shift 0; bit_cnt DATA_WIDTH; state TRANSFER; busy 1b1; 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], 1b0}; end end if (bit_cnt 0) begin rx_data rx_shift; state DONE; end end DONE: begin state IDLE; busy 1b0; end endcase end end endmodule这段代码中sclk_reg (CPOL ^ CPHA)对应采样沿sclk_reg ! (CPOL ^ CPHA)对应移位沿。当CPHA0时采样发生在第一个边沿上升沿此时sclk_reg从CPOL变为非CPOL条件为实际情况需要补充一个判断。更严谨的方法是使用sclk_en脉冲配合边沿方向检测同时需要处理CPHA0时首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时建议先固定CPOL0, CPHA0确认基本传输功能再切换到其他模式。通过对比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输出1SPI时钟频率 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以内尤其高速SPI20MHz时建议包地。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 8hF0; 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], 1b0}; end end initial begin clk 0; rst_n 0; start 0; tx_data 0; miso 0; #100; rst_n 1; #50; tx_data 8hA5; (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周期约为200nsDIVIDER50时占空比接近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每次传输1bitDSPI在MOSI/MISO基础上增加一根IO0和IO1实现双倍数据率QSPI则是四根数据线。FPGA实现QSPI时不能把数据线直接接到普通IO必须使用IO状态机动态切换方向。这个进阶方向需要重新设计状态机但基础仍是本文中的SPI主状态机。如果只是调试Flash许多FPGA厂商的内存控制器已原生支持QSPI可直接例化IP核。在实际项目中先把标准SPI接口调通再用回环测试验证收发一致性最后根据需求决定是否引入FIFO或IP核。整个设计闭环的核心在于代码、原理图、文档、仿真互相印证任何一环缺失调试时都会多浪费数倍时间。本文还有配套的精品资源点击获取
返回列表