FPGA实现SPI通信:从协议原理到Verilog实战与调试 直接啃过SPI协议的人应该都有这种感觉看芯片手册时觉得SPI简单得不行四根线、一个主从关系、几种模式感觉比UART还省事。可真到了要在FPGA上自己实现一套SPI通信的时候各种问题噼里啪啦全冒出来了——模式配不对、时钟沿采错、跨时钟域丢数据、片选信号处理不当、板子上一堆Noise……当初我在一个项目里用FPGA驱动一颗SPI接口的ADC调试花了整整两个晚上最后发现居然是主模式时钟极性设反了。那种“明明逻辑没问题可波形就是不对”的感觉做过的人应该都懂。这篇文章我就结合自己在FPGA上实现SPI通信的实际经历把从协议细节、模块设计、Verilog编码到时序约束、板级调试的完整链路拆开来讲。不整虚的直接上能落地的思路和代码片段还会把我踩过的坑、排查问题的套路一并交代清楚。适合正在学FPGA的入门工程师也适合需要快速把SPI从设备或主设备跑通的在职开发者参考。1. SPI协议本质与FPGA实现的核心思路1.1 四线通信背后的时序本质SPI全称Serial Peripheral Interface是一种同步串行通信接口。所谓同步指的是收发双方共用同一个时钟源数据在时钟沿上完成采样或更新不需要额外约定波特率因此比UART更容易实现高速传输。它本质上是一个移位寄存器在时钟驱动下不断循环的过程。标准SPI最少需要四根线SCLK串行时钟由主机产生并提供给从机MOSI主出从入主机向从机发送数据的线路MISO主入从出从机向主机回传数据的线路CS/SS片选信号低电平有效由主机控制用来选中目标从机。虽然只有四根线但SPI有四种模式组合通过CPOL时钟极性和CPHA时钟相位两个参数决定。CPOL决定空闲时SCLK的电平状态CPHA决定数据是在SCLK的上升沿还是下降沿被采样。这四种模式初看没什么实际接从设备时却是最容易翻车的地方。我见过不止一个同事Flash和ADC挂在同一条SPI总线上一个要模式0一个要模式3结果他统一配了个模式0ADC死活读不出正确数据。用FPGA实现SPI核心工作其实就两件事一是精确产生符合从机要求的SCLK时序二是在正确的边沿把数据移出或采入。其他什么协议状态机、FIFO缓冲、中断标志都是围绕这两个目标展开的辅助结构。1.2 为什么用FPGA而不是单片机不少人对FPGA实现SPI有疑问STM32、ESP32这些MCU不是自带硬件SPI外设吗为什么要费劲写Verilog自己实现我的回答是分场景的。如果只是接个传感器、控制个显示屏MCU硬件SPI确实又快又省心。但FPGA的优势在于几个特殊场景需要多个SPI主站同时工作而且每个从设备的速率、模式都不同MCU的SPI外设数量通常不够对时延有硬性要求比如数据从收到响应必须在几百纳秒内完成MCU的中断响应做不到SPI只是整个系统的一小块旁边还有ADC采集、图像处理、PCIe等逻辑需要协同工作用FPGA统一做数据通路更自然SPI时序需要灵活定制比如三线制复用双向数据线、非标准的时钟频率等协议栈完全握在自己手里。而且从学习角度说在FPGA上实现SPI是理解时序逻辑和状态机的极佳训练项目。它不像UART那样还需要处理波特率分频和异步采样SPI的同步特性让初学者能更聚焦地理解时钟、数据、移位寄存器这三者的关系。有了这个底子再去碰IIC、PCIe、DDR时序会轻松不少。顺便说一句热词里提到的“IIC和SPI的区别”我简单总结一下。IIC只需要两根线SDA和SCL有设备地址概念支持多主多从可以通过应答机制确认通信成功SPI则是四线全双工速率更高协议开销更小但是没有标准的应答机制适合高速、大数据量的场合。日常选型低速且挂载设备多选IIC高速且结构简单选SPI。FPGA实现IIC比SPI麻烦一些核心在于时序不能回溯、应答和仲裁机制复杂。不过如果SPI都没搞明白IIC建议也别急着碰。1.3 软件片选与硬件片选怎么选这是一个很容易被忽略但实际影响系统稳定性的点。FPGA里实现片选有两种方式软件片选就是用一个普通GPIO输出控制CS信号在发送前拉低、结束后拉高。这种方式灵活想什么时候拉就什么时候拉也容易配合自定义时序。缺点是CS的拉低和SCLK的产生之间可能存在不确定的延迟因为是由代码逻辑控制的。硬件片选则是在FPGA内部通过移位逻辑自动控制CS信号CS的拉低和SCLK的启动由同一个状态机在同一个时钟周期附近完成时序确定性更好。缺点是灵活性稍差想加入一些特殊时序要求时会受约束。在实际工程里我的建议是如果你驱动的从设备对CS和SCLK之间的建立时间敏感几乎所有SPI Flash和ADC都有这个参数那就老老实实用硬件片选让CS在SCLK之前固定提前几个时钟周期拉低这样才能满足数据手册要求。如果只是测试、调试阶段软件片选更方便配合逻辑分析仪随时改逻辑。之前在某篇帖子里看到“lattice FPGA SPI Flash硬件片选”的讨论说的就是这个问题。实际上FPGA厂商给的SPI控制器IP核里CS的控制逻辑都已经帮你做好了确定性的时序但这不代表你可以不关心——很多从设备要求CS在传输结束后有一个最短的拉高时间比如W25Q系列要求tSHSL至少几十纳秒IP核默认配置不一定满足还是需要你自己评估。2. SPI模块的Verilog实现细节2.1 架构设计状态机计数器移位寄存器的铁三角一个标准FPGA SPI主机模块通常由三个部分构成协议状态机负责管理IDLE、传输中、结束这三个大的阶段时钟分频/边沿生成器从系统时钟产生SCLK并提供发送/接收采样的时间基准移位寄存器负责把数据按位发送到MOSI并把MISO采到的数据逐位移入。这三个部分不算复杂但实现时的细节决定了模块是能跑还是能稳定跑。先说时钟分频。SPI的SCLK不一定要用独立时钟源常见做法是把FPGA系统时钟通过计数器分频得到。比如系统时钟100MHz要产生10MHz的SCLK就让计数器每5个周期翻转一次电平。这里有一个容易忽略的问题SCLK本身是一个分频时钟如果你直接在always块里用它作为触发时钟会产生两个时钟域给后续时序约束和跨时钟域处理带来麻烦。我的推荐做法是始终使用系统时钟作为唯一的触发时钟SCLK作为普通信号通过组合逻辑或时序逻辑生成所有状态的跳变、寄存器的移位都在系统时钟的边沿完成。这样整个模块是单时钟域设计时序收敛容易得多。再说状态机的设计。这里给出一个典型的SPI主机状态机跳转思路IDLE等待发送启动信号如tx_start拉低CS或先不拉由外部控制PRE准备阶段一般在启动SCLK之前提前拉低CS等待tlead时间SHIFT启动SCLK按照设定的位宽8位/16位/32位逐位移位发送/接收POST停止SCLK等待trail时间DONE拉高CS或交给外部输出接收完成标志回到IDLE。这个状态机最大的优势在于把SPI时序中的建立时间、保持时间、CS延迟时间都显式地用状态周期来表达而不是靠碰运气。2.2 模式0/模式2主模式核心代码示例下面这段是我在一个项目里实际用过的SPI主机写逻辑核心实现的是模式0CPOL0CPHA0或者模式2CPOL1CPHA0在FPGA主模式下的行为。注意这里我用的是“数据在SCLK下降沿时更新上升沿时被采样”的时序正好对应模式0从机的采样期望。当然具体还是得对着从机手册确认这里只是演示结构。module spi_master #( parameter DATA_WIDTH 8, parameter CLK_DIV 8 // 系统时钟分频系数SCLK sys_clk / (2*CLK_DIV) )( input wire sys_clk, input wire rst_n, input wire tx_start, input wire [DATA_WIDTH-1:0] tx_data, output reg [DATA_WIDTH-1:0] rx_data, output reg spi_cs_n, output reg spi_sclk, output reg spi_mosi, input wire spi_miso, output reg tx_done ); localparam IDLE 2d0; localparam PRE 2d1; localparam SHIFT 2d2; localparam POST 2d3; reg [1:0] state; reg [DATA_WIDTH-1:0] shift_reg; reg [7:0] clk_cnt; reg [3:0] bit_cnt; always (posedge sys_clk or negedge rst_n) begin if (!rst_n) begin state IDLE; spi_cs_n 1b1; spi_sclk 1b0; spi_mosi 1b0; tx_done 1b0; clk_cnt 0; bit_cnt 0; end else begin case (state) IDLE: begin spi_cs_n 1b1; tx_done 1b0; if (tx_start) begin shift_reg tx_data; state PRE; spi_cs_n 1b0; // 先拉低片选 end end PRE: begin // 等待几个系统时钟周期满足CS建立时间 if (clk_cnt CLK_DIV/2 - 1) begin clk_cnt 0; state SHIFT; end else begin clk_cnt clk_cnt 1b1; end end SHIFT: begin // SCLK翻转逻辑 if (clk_cnt CLK_DIV/2 - 1) begin clk_cnt 0; spi_sclk ~spi_sclk; if (spi_sclk 1b0) begin // SCLK从低变高锁存MISO数据 shift_reg {shift_reg[DATA_WIDTH-2:0], spi_miso}; if (bit_cnt DATA_WIDTH-1) begin state POST; end else begin bit_cnt bit_cnt 1b1; end end else begin // SCLK从高变低更新MOSI数据下一bit spi_mosi shift_reg[DATA_WIDTH-1]; end end else begin clk_cnt clk_cnt 1b1; end end POST: begin if (clk_cnt CLK_DIV/2 - 1) begin clk_cnt 0; spi_cs_n 1b1; // 拉高片选 rx_data shift_reg; tx_done 1b1; state IDLE; end else begin clk_cnt clk_cnt 1b1; end end endcase end end endmodule这段代码的细节值得说两句。SCLK的翻转是用系统时钟驱动的普通寄存器不是用PLL生成真正的独立时钟。这样做的好处是全模块单时钟域综合、布局布线、时序分析都简单。坏处是SCLK的高低电平宽度只能按系统时钟整数倍控制精度有限。如果你需要非常精细的SCLK占空比可以用MMCM/PLL锁相环来产生精确时钟但那种情况下跨时钟域FIFO或握手处理就是必须考虑的了。还有一点上面代码里的spi_mosi更新时机是在SCLK下降沿发生的同时。如果你拿FPGA去驱动一个在上升沿采样的从机这个时序刚好完美。但如果换成一个在下降沿采样的从机比如某些SPI DAC就需要把MOSI的更新时机调整到SCLK上升沿之前。这个灵活性正是自己写SPI模块的价值规则完全由你定而不是被别人的IP核限制死。2.3 接收数据时的采样点怎么定SPI主机接收数据时最容易犯的错误就是“在MISO变化的同时去采数据”。数字电路里采样必须满足建立时间和保持时间如果采样沿和数据变化沿靠得太近亚稳态就会找上门。如果用上面的实现结构当spi_sclk从低变高时从机在这个边沿把数据驱动到MISO线上你如果还在同一个边沿去采样采到的可能是”上一bit的值“也可能是“毛刺中间的值”完全不确定。正确做法是让采样点落在数据稳定区间内比如在SCLK的下降沿如果数据是在上升沿更新的或者干脆用系统时钟在SCLK高电平的中间阶段采样一次。我调试时更喜欢用“接收侧多打一拍”的策略MISO信号先用两级触发器同步消除亚稳态然后采样时用SCLK的反相位沿来采样。假如从机在SCLK上升沿更新MISO那我们就在SCLK下降沿采样数据铁定是稳定的。如果从机规划更复杂就干脆把采样点固定在SCLK高电平正中间的某个系统时钟周期上这样容错空间更大。3. 从工程角度谈SPI跨时钟域与多从机扩展3.1 跨时钟域处理SPI并不是隔离的很多人在FPGA里写SPI模块时只盯着SPI协议本身忘了思考一个问题SPI控制器所在时钟域和其他逻辑比如一个PCIe接口、一个DMA控制器、一个软核处理器的时钟域之间数据是异步的。比如你有一个Zynq平台PS端通过AXI总线给PL侧的SPI控制器发送数据PS时钟可能是100MHzPL侧SPI模块用的是50MHz两者没有确定的相位关系。如果你直接在SPI模块内部用50MHz时钟去采来自AXI总线的信号很可能采到中间态。正确的做法是控制信号跨时钟域用两级同步器打两拍处理脉冲型控制信号比如tx_start、tx_done数据信号跨时钟域用异步FIFO如Xilinx的FIFO IP核缓冲数据FIFO读写时钟分别接两个时钟域寄存器配置跨时钟域采用握手协议或者寄存器桥确保配置值稳定后再使用。这里有个很常见的错误有人为了省事直接用系统时钟采外部输入的SPI信号比如来自MCU的SCLK和MOSI这在小项目里可能能跑但在时序比较紧的系统中就是定时炸弹。因为SPI从设备的SCLK和MOSI是异步信号没有配合同步器就进入FPGA内部逻辑一旦出现亚稳态可能导致整个状态机跳飞。最好的方案是把FPGA作为SPI从机时对外来的SCLK做边沿检测和双触发器同步。虽然会引入一个时钟周期的延迟但换来的稳定性非常值。3.2 多从机扩展是挂多根CS还是菊花链实际接口设计中经常遇到一个SPI控制器要接多个从设备的情况。这时候有几种方案多CS独立控制每个从设备一根CSSCLK、MOSI、MISO共享。FPGA实现起来很简单就是多输出几个CS信号发送前选中其中一个拉低。注意同一时刻只能有一个CS为低否则多个从机同时驱动MISO总线打架。菊花链Daisy Chain从设备的MISO接到下一个从设备的MOSI形成链路。适合从设备数量多但每个CS需要独立的场景本质上用移位寄存器链做广播或数据透传。FPGA实现菊花链需要对每个从设备的位延迟有精确认识调试比独立CS复杂。GPIO扩展片选当CS数量实在太多FPGA引脚紧张时可以用一个3-8译码器或I2C GPIO扩展芯片来产生CS。我在实际项目中用得最多的是多CS独立控制。FPGA引脚资源通常不是瓶颈而独立CS协议最简单、排查问题也容易。唯一要注意的是不同从设备可能有不同的SPI模式和数据位宽这要求你的模块在每次传输开始时最多用一套配置参数通过寄存器提前配置好或者每次传输都附送参数。我更倾向于“配置寄存器启动传输”的方式在状态机进入SHIFT之前把模式和位宽锁存下来防止中途被修改。3.3 SPI与DMAFPGA侧怎么做数据搬运热词里提到了“SPI DMA”这个在MCU语境下很容易理解就是让DMA控制器把数据从内存搬到SPI发送寄存器或者从SPI接收寄存器搬到内存CPU只负责启动和收尾。但FPGA上实现DMA的思路略有不同。FPGA里的“DMA”其实就是一个内部数据流引擎比如在Zynq平台你可以用AXI DMA IP把PS内存数据通过AXI Stream搬给SPI模块在纯FPGA平台你可以写一个简单的FIFO读状态机配合一个写FIFO状态机实现“外部数据进来→FIFO缓冲→SPI发送”的流水线。我的经验是FPGA做SPI的高速数据搬运核心不是DMA本身而是FIFO深度和背压Backpressure设计。比如SPI发送速率20Mbps相当于每秒2.5MB而FIFO写比如来自ADC速率是50Mbps如果FIFO深度不够一旦SPI端暂停FIFO就会溢出。最简单的方案是FIFO几乎满的时候暂停上游写入但这样做对实时性要求高的场景不友好。更好的方案是做一个水位线监测提前降低上游写入速率或者把SPI速率提升到高于上游平均速率。还有一点很多新手会忽略SPI发送一个字节的间隙SCLK会停止并保持空闲状态这段间隙用来满足从设备对CS或SCLK间隔的要求。在DMA连续传输中这个间隙会让瞬时吞吐率下降所以你要算的是“包含间隙的等效速率”而不是纯SCLK频率。之前我调试一块屏标称SPI速率40MHz但因为每帧之间需要等待几百微秒的延迟最后实际刷新率远远低于理论值所以热词里“spi屏幕刷新率是多少”这类问题答案往往是“量出来才知道”。4. 时序约束与板级调试的实战经验4.1 SDC约束怎么写FPGA里SPI模块的时序约束主要涉及两组SCLK信号与系统时钟的关系如果SCLK是用系统时钟分频得到的普通信号综合工具通常无法自动推断出它和系统时钟之间的周期关系。这时需要在SDC里用create_generated_clock把SCLK定义出来否则时序分析工具可能错误地认为SCLK不受约束导致布线结果在硅片上出现意外时序违规。I/O约束如果你把SPI引脚放在FPGA引脚上要设置set_output_delay和set_input_delay描述外部从设备相对SCLK的建立/保持时间要求。这套约束是很多FPGA工程师不爱写的因为麻烦。但如果不写综合工具对IOB里面的寄存器布局就会很随意高速SPI比如50MHz以上可能出现采样失败。以Xilinx Vivado为例一个简单的主模式模式下输出数据时序约束思路先根据从机的数据手册查到它要求的建立时间tSU然后在SDC里设置output delay为(SCLK周期 - tSU)的近似值。这些值不需要极其精确你的目标是让工具有一个合理的“参考”从而正确约束IOB寄存器的位置和延迟。4.2 调试工具与信号观测技巧FPGA调试SPI我的第一建议是优先用FPGA内部的逻辑分析仪比如Xilinx ILA而不是示波器。ILA可以在系统时钟域下精确采样内部信号方便你同时观察状态机的状态、SCLK、MOSI、MISO、CS以及数据寄存器的值。示波器只能看外部引脚电平看不到内部状态机。但ILA有一个坑它插入探针会改变综合布线结果有时会引入额外延迟导致原本刚刚满足时序的SPI信号变得不稳定。所以在用ILA调试通之后务必把ILA去掉重新综合跑一次确认最终版代码没问题。第二个技巧是在MISO入口打一拍形成同步采样。调试时加两个触发器把MISO同步进来然后观察同步后的MISO你会发现比直接看原始信号“干净”很多。这也是为什么我总建议不要把外部异步信号直接接进状态机。第三个技巧是用计数器控制SCLK留出观测窗口。如果你在ILA里看SCLK和MISO变化总觉得它们在同一个沿附近打架不妨在状态机的SHIFT阶段把SCLK的下降沿和MOSI更新沿强行拉开一段时间比如让MOSI在下降沿前半个系统时钟周期更新再看波形会清晰很多。4.3 真机调试中的典型波形病灶我在多个项目里遇到过背锅级SPI波形问题总结起来就这几种情况CS和SCLK同时变化很多从设备对CS下降沿到第一个SCLK上升沿的时间有要求如果CS变化和SCLK启动在同一个系统时钟周期内实现就会违规。解决方法是状态机里预留PRE周期用两个时钟周期专门给CS建立时间。MISO在MOSI改变之后瞬间改变然后在采样点又变回来这其实是主模式正常现象因为从机可能是上升沿驱动、下降沿采样点位可能会交叉。不要被波形吓到严格按照从机手册的分析来决定采样点。SCLK上有多余毛刺几乎可以肯定是分频逻辑产生了竞争-冒险。检查SCLK是不是来自组合逻辑如assign SCLK cnt DIV ? 1 : 0这种代码容易出毛刺。正确做法是SCLK在always块中通过寄存器赋值。同一SPI总线上多个从设备的MISO互相打架低概率但发生了就麻烦。用万用表量CS的电平确认只有一个CS被拉低。如果软件设计没问题可能是FPGA的上电时序问题导致某个CS没有初始化到默认高电平记得给相关寄存器加初始值或复位值。4.4 “SPI under”是什么问题热词里出现了“spi under”的联想词这大概率是指SPI传输速率低于预期SPI underutilized或者某个IP核报的欠载underflow错误。具体到不同语境含义不同。比如在Xilinx AXI Quad SPI IP中可能上报“TX FIFO Underflow”表示硬件试图从一个空的发送FIFO中取数据。原因通常是软件没有在启动传输前把数据填好或者中断使能配置有问题。处理思路很简单确保发送前FIFO非空、接收中断清理逻辑没问题、背压机制正确部署。如果你遇到的是“SPI速率跑不满”那就要做链路整体分析从数据源到FIFO到SPI引脚的瓶颈在哪。我之前遇到过一块FPGA把SPI SCLK分频设成了偶数分频但分频计数器的时钟频率太高导致SCLK实际频率比预期少了很多。多查几步问题往往在不起眼的地方。5. 典型应用场景与模块扩展思路5.1 SPI Flash读取与启动引导FPGA最常见的SPI应用之一就是驱动板载SPI NOR Flash比如W25Q系列。这类芯片的主要操作包括读ID、读数据、写使能、扇区擦除、页编程等命令每条命令都有固定的指令编码和地址格式。FPGA驱动SPI Flash有几个注意点读操作需要先发命令字节和地址然后切换方向把MOSI从输出转为输入模式对于FPGA引脚来说需要三态控制。有些新手在FPGA里实现读Flash时只发了地址忘了切换方向结果MISO一直在读MOSI的延后数据根本不对。写Flash需要先发写使能命令WREN0x06每页编程之前都要确认状态寄存器里的WEL位已经置1否则写入直接失败。状态寄存器轮询页编程和扇区擦除都需要时间FPGA端要持续读状态寄存器等到BUSY位清零才能进行下一步操作。如果用固定延时效率低不说延时不够还会出错。这个场景下FPGA的优势是可以用一个简单的状态机把整条“擦除-写入-校验”流水线实现出来不需要CPU参与。很多FPGA板卡上电启动时配置逻辑就是通过SPI从Flash加载比特流的这和用户设计的SPI控制器是两回事但底层的SPI协议是一样的。5.2 高速ADC数据采集与SPI传输另一类常见应用是用FPGA配置并读取SPI接口的高速ADC。比如AFE4490这类模拟前端内部寄存器需要通过SPI配置配置完成后FPGA的工作重心转为读取转换结果如热词里提到的“afe4490 spi”这类芯片的SPI接口往往要求主模式默认是模式0但有的寄存器配置需要连续写入中间不能拉高CS。我在之前的项目里遇到过一个情况ADC手册说寄存器的读写需要CS保持低电平至少多少纳秒但手册前后章节写得含糊。我用FPGA实现了寄存器读写后发现寄存器读出来全是0xFF后来用逻辑分析仪逐拍观察发现CS在写地址和写数据之间被拉高了一下而后续地址的锁存依赖于CS持续低电平。修正状态机逻辑以后问题解决。所以调试SPI设备时一定要养成习惯先看手册的时序图数清楚CS拉低到SCLK启动的周期数、两字节之间的CS状态要求、以及传输结束后CS拉高的最短时间。有空用逻辑分析仪抓一下波形把手册时序图和数据手册的电气参数对比确认能省很多时间。5.3 STM32H743与FPGA的FMC通信与SPI对比热词里“stm32h743和fpga实现fmc通信”这种场景其实是FPGA和MCU之间高速通信的经典方案。FMC总线是并行的地址数据线很多带宽远高于SPI适合ARM核和FPGA之间大量寄存器读写或缓冲区搬移。SPI则适合引脚少、速率要求低的场景。拿STM32H743和FPGA协作来说常见的架构是低速控制通路IIC或SPI用于配置寄存器、查询状态高速数据通路FMC或PCIe用于批量数据搬运。FPGA端实现FMC接口时要仔细处理地址译码和数据总线的时序FMC对建立/保持时间要求比较严格比SPI复杂得多。如果你第一次调FMC和FPGA互连建议先用SPI把基础功能跑通再切换到FMC。这样可以把问题域隔离不会两个接口同时出问题时不知道改哪边。另外如果你是用STM32H743的硬件SPI外设又不想被硬件引脚绑定限制可以用GPIO模拟SPI。不过GPIO模拟SPI在STM32H7这种高性能核上其实不划算软件翻转GPIO的开销很大更高频率下的时序抖动也大。FPGA上“IO口模拟三线SPI”则完全是另一回事因为硬件并行执行用Verilog写一个IO口模拟SPI模块很自然。6. 实际工程中容易踩的坑和排查技巧6.1 模式配置错误是最隐蔽的敌人如果说有一个SPI调试问题值得单独开一节那就是模式配置错误。这类问题往往不会导致接口完全不工作而是导致“偶尔读对、偶尔读错”的诡异现象非常折磨人。我做过的典型case是这样的一个SPI温度传感器手册上说支持模式0和模式3。我用FPGA主模式配置成模式0结果读回来的温度值在25℃和26℃之间随机跳动一看就是采样点不稳定。后来怀疑是CPOL或CPHA配置错误导致采样边沿正好落在数据翻转边沿附近把模式改成模式3问题立即消失。排查模式问题的思路很简单用逻辑分析仪同时抓SCLK和MOSI/MISO对比手册时序图。如果发现“主机发送的时候从机正好也在变化”那就说明采样沿选错了。如果你没有一个额外的参考设备也可以用ILA观察MISO在SCLK上升沿和下降沿前后的值是否稳定。如果在两个沿附近数据都不稳定那还要再查线路质量和信号完整性。6.2 从机MISO驱动的三态问题因为MISO是多个从机共享的所以每个从机必须在没有被选中的时候把MISO驱动为高阻态释放总线。FPGA如果同时接了多个从机GPIO输入引脚是接收总线信号的但FPGA内部逻辑如果误把某个从机的MISO识别为“一直驱动低”的信号那就是三态控制有问题。在FPGA中实现三态总线需要在输出端使用IOBUF原语或带有三态控制的OBUFT比如// 三态控制示例 assign spi_miso_in spi_miso_en ? spi_miso_pad : 1bz;不过如果你只是做SPI主机MISO引脚通常只需要配置成输入不需要三态控制。只有当你用同一个引脚既做MOSI又做MISO半双工模式或实现三线SPI总线时才需要IOBUF。半双工三线SPI在数字传感器里比较常见这种情况下主机的MOSI和MISO会短接到一条线上主机的数据引脚必须是双向的。FPGA实现时需要做方向控制在发送阶段把引脚配置为输出接收阶段切换为输入。切换方向要留出足够的死区时间否则总线竞争会导致信号毛刺和数据损坏。6.3 调试时对采样判断的思考方式最后分享一个调试思维不算代码层面的技巧但很实用。调SPI时遇到错误数据先别急着改代码先用排除法判断错误的来源。是主机时钟没产生是主机数据没移出去是从机没响应还是从机响应了但采样点不对这四个问题对应的调试手段完全不同主机时钟没产生看ILA或示波器检查状态机是否从IDLE正常跳出主机数据没移出去看MOSI波形检查移位寄存器装载和移位的位序MSB先出还是LSB先出这个经常错比如W25Q是MSB先出从机没响应检查CS是否被正常拉低、供电和复位是否正常从机响应了但主机采错点调整采样点位置找个数据手册上没有明确说“不要采”的位置把采样放在该边上。工作中还有一个经验是SPI时钟频率不是一开始就直接拉到最高。先把时钟频率设低比如几百kHz把协议跑通再逐步提高频率用二分法找最大稳定频率。如果低频率下也出错那基本可以排除信号完整性问题是逻辑本身的问题如果低频率正常、高频率出错那大概率是采样点和PCB布线走线长度引起的延时问题。7. SPI模块设计的最后几个建议在SPI通信FPGA实现这个题目下代码写出来其实不难难的是把模块做得稳定、可复用、可维护。我在做过的几个项目中最实用的经验放在最后分享。第一个建议先设计验证计划再写代码。在写Verilog之前先列出要支持的从设备清单、每个设备的SPI模式、最大SCLK频率、数据位宽、CS时序要求。有了签名之后写模块时就能针对性地做参数化、预留配置寄存器而不是每个新的从设备都推倒重写。好的SPI模块应该能通过参数例化不同位宽、不同分频系数、不同模式最好还能支持外部寄存器动态切换。第二个建议内部信号分层清晰。把协议状态机、数据通路、跨时钟域同步这三个层次在模块接口上就分开。如果以后要接不同的从设备改的是协议状态机一层数据通路和同步逻辑不用动。模块之间的接口尽量用简单的AXI4-Lite或者简单的寄存器读写接口方便挂到SoC总线上。第三个建议把“时序验证”当作标配。实现完成后至少用形式验证工具检查一遍状态机的所有分支。仿真时的Testbench不要只仿真“正常情况”要把CS拉高但SCLK还在跑、MISO悬空、连续发送之间间隔很短等异常情况都覆盖到。这些边界情况才是真正在板子上会坑你的。第四个建议不管多急都要留出一版“慢速调试模式”。就是可以通过寄存器把分频系数调大、把SCLK降到非常低方便你用廉价逻辑分析仪观察协议正确性。等到协议没问题了再切回高速模式跑性能。这个习惯帮我避免了多次“波形看不清导致误判”的尴尬。SPI通信的FPGA实现不算高深但每个细节都值得认真对待。协议只有四根线可牵扯到的却是状态机设计、跨时钟域、时序约束、总线竞争、信号完整性一系列问题。希望这篇文章里的结构和踩坑记录能帮你少走一些弯路。你手头如果正在做相关的项目建议先把模式、采样点、CS时序这三件事确认清楚再动手写代码你会发现进展会顺利很多。