FPGA实战:SPI协议Verilog实现与时序约束全解析
发布时间:2026/9/5 1:45:49 作者:尧图编辑部 阅读量:1,286

作为一个常年跟 FPGA 打交道的工程师SPI 这个协议我估计闭着眼都能写出来。但每次帮新人看代码发现大家栽跟头的地方翻来覆去就那么几个时序不满足、跨时钟域处理出错、片选信号乱拉。这篇就把 SPI 在 FPGA 里的实现从头到尾捋一遍从协议本身到 Verilog 代码再到上板调试的坑一次性讲透。1. 先把 SPI 协议本身啃透1.1 四根线背后的设计哲学SPI 全称 Serial Peripheral Interface摩托罗拉在上世纪 80 年代搞出来的。它只有四根线SCLK串行时钟由主机产生从机只能被动接收MOSIMaster Output Slave Input主机输出、从机输入MISOMaster Input Slave Output主机输入、从机输出CS片选信号低电平有效由主机控制决定跟哪个从机通信这四根线的设计其实非常巧妙。SCLK 由主机产生意味着时钟源只有一个天然避免了像 I2C 那样需要时钟同步和仲裁的麻烦。CS 信号的出现让总线上可以挂多个从机只要 CS 拉低对应的设备其他设备就把 MISO 置为高阻态不干扰通信。在实际项目中我经常用一句大白话给新人解释SPI 就是主机说一句从机回一句一来一回靠时钟对齐。这句“一来一回”特别重要——SPI 是全双工的也就是说主机发数据的同时也在收数据这在很多场景下能省一半时间。1.2 CPOL 和 CPHA 到底怎么选这是新手最容易懵的地方。CPOLClock Polarity决定时钟空闲时的电平CPHAClock Phase决定数据在哪个边沿采样。组合起来就是四种模式模式CPOLCPHA时钟空闲电平采样边沿Mode 000低电平上升沿Mode 101低电平下降沿Mode 210高电平下降沿Mode 311高电平上升沿这里有个特别容易踩的坑很多芯片文档里写的“数据在上升沿稳定、下降沿变化”并不是绝对真理一定要看具体器件手册。比如 W25Q系列 Flash 默认支持 Mode 0 和 Mode 3但 ADC 芯片 ADS1256 只支持 Mode 1。你如果不管三七二十一用 Mode 0 去读 ADS1256读回来的数据全是乱的。从FPGA的角度看我建议优先用 Mode 0因为上升沿采样、下降沿变化这种“边沿对称”的模式在时序约束里最好做时钟余量也比较宽裕。但前提是片外器件必须支持。判断方法也简单看数据手册里的时序图重点关注 SCLK 空闲时是高还是低以及数据在哪个边沿被锁存。1.3 SPI 和 I2C、UART 的选型差异项目中经常遇到有人问为什么不用 I2C这里我按自己的工程经验做一个对比维度SPII2CUART线数4根可减少2根SDASCL2根TXRX速率最高可达几十~上百Mbps标准100k/400k快速1M典型115200~几Mbps通信方式全双工半双工全双工多设备靠CS一主多从靠地址寻址点对点时序复杂度简单、灵活有ACK、仲裁最简单适用场景高速、大数据量低速、多设备、节省引脚远距离、简单互联SPI 在 FPGA 项目中的地位说白了就是**“速度不够用、引脚不够省、协议不够复杂”的最佳折中点**。像 ADC 采样数据回传、Flash 读写、LCD 屏幕刷屏、传感器寄存器配置SPI 都是主力。I2C 在 FPGA 里做的话还得自己搞一个开漏输出和三态门麻烦不少。1.4 硬件片选和软件片选的区别这个细节很多人不太在意但我遇到过不止一次因为片选处理不当导致 Flash 读写偶发失败的情况。硬件片选就是用 FPGA 的一个 GPIO 直接连到从机的 CS 引脚。好处是时序完全可控——CS 拉低后你可以在任意时刻开始产生 SCLK时钟结束后再拉高 CS。大多数场景都推荐这种方式尤其是在速率较高或时序要求严格的时候。软件片选则是把 CS 信号通过协议“编”进数据流里常见于一些特殊的串行器件比如单线协议或者需要发送特定命令头才能唤醒的芯片。这种模式下FPGA 的 CS 引脚可能一直拉低靠数据帧格式来区分通信对象。实操中我的建议是能用硬件片选就别用软件片选。硬件片选的时序干净利落调试时用示波器一眼就能看出通信窗口软件片选虽然省一根线但排查问题时会多一层不确定性。2. FPGA 实现 SPI 的整体设计思路2.1 自己写 Verilog 还是用 IP 核这是设计开始前必须决策的问题。Xilinx 提供了 AXI Quad SPI IP 核用起来确实省事但说实话大部分项目里我还是倾向于自己写 RTL。原因有三个SPI 协议本身不复杂状态机加移位寄存器就能搞定写一遍对协议的理解深度完全不一样IP核的灵活性有时候反而是负担改一个参数就要重新配置和综合调试周期拉长自己写的代码可控性强比如你可以精确控制 CS 拉低到 SCLK 第一个边沿的时间间隔这在对接某些“脾气怪”的芯片时很关键当然如果项目时间紧、SPI 只是辅助功能而且对时序要求不高用 IP核也完全合理。我刚入行时也用过但后来发现调试IP核内部信号远不如调试自己的状态机直观。2.2 设计目标与模块划分假设我们要实现一个通用的 SPI 控制器既能做主又能做从支持四种模式可配数据位宽 8 位最高时钟 50MHz。模块划分如下spi_master.vSPI 主机状态机移位寄存器spi_slave.vSPI 从机逻辑spi_controller.v顶层控制器管理读写请求、数据缓冲clock_divider.v时钟分频模块产生 SCLK这种结构的好处是职责单一、可复用性强。我最早写的时候把主从逻辑写在一个文件里结果改一处就要测半天后来拆开后清爽多了。2.3 为什么推荐状态机的方式SPI 的时序本质上是一个有限状态机空闲→启动→传输→结束。用状态机实现的好处是代码逻辑和协议时序一 一对应易于理解和维护可以方便地插入超时、错误处理等逻辑后续如果要扩展成多字节读写、DMA 接口只需要增加状态即可我之前试过用纯计数器移位的方式写代码是短了但出问题后根本不知道哪拍的时序错了。后来全部改为状态机写法出错时一眼就能定位到是启动阶段还是传输阶段。对于FPGA工程来说优雅的代码远没有好调试的代码重要。3. SPI 通信 FPGA 实现的代码与关键细节3.1 跨时钟域处理所有问题的根源在设计 SPI 控制器之前必须先想清楚跨时钟域CDC问题。SPI外围器件的输入和 FPGA 内部逻辑往往是异步的典型场景是FPGA 的主时钟比如 100MHz而 SPI 从机的 SCLK 可能只有几 MHz外部 ADC 输出的 SPI 数据与 FPGA 内部逻辑时钟完全没有相位关系处理不好 CDC轻则偶发数据错误重则系统死机。我见过最典型的错误是直接拿 SCLK 当触发时钟去采样 MISO 数据这在 SCLK 频率较低、寄存器时序余量足够时勉强能跑但一旦温度变化或者芯片版本更换马上就冒问题。我的处理方案是尽量把外部信号用两级触发器同步到 FPGA 系统时钟域再用系统时钟去采样。具体到 SPI 从机模式我会把 SCLK 和 CS 都用系统时钟同步一遍然后检测 SCLK 的边沿事件再用这个边沿事件作为移位寄存器的使能信号而不是直接把 SCLK 当时钟。这样做的好处是整个设计只有一个时钟域时序约束简单也不容易出现亚稳态。3.2 SPI Master 核心代码逐段解析这里给出一段经过工程验证的 SPI Master 核心代码框架module spi_master #( parameter CPOL 1b0, parameter CPHA 1b0, parameter DATA_WIDTH 8 )( input wire clk, // 系统时钟例如 50MHz input wire rst_n, // 低电平复位 input wire start, // 启动一次传输 input wire [DATA_WIDTH-1:0] tx_data, // 待发送数据 output wire [DATA_WIDTH-1:0] rx_data, // 接收数据 output reg sclk, // 串行时钟 output reg cs_n, // 片选低有效 output reg mosi, // 主机输出 input wire miso, // 主机输入 output reg busy // 忙标志 ); localparam IDLE 3d0; localparam SETUP 3d1; localparam TRANSFER 3d2; localparam FINISH 3d3; reg [2:0] state; reg [DATA_WIDTH-1:0] shift_reg; reg [$clog2(DATA_WIDTH):0] bit_cnt; reg miso_d1, miso_d2; // 两级同步 always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; cs_n 1b1; sclk CPOL; mosi 1b0; busy 1b0; bit_cnt 0; end else begin // 默认情况保持当前状态 case (state) IDLE: begin cs_n 1b1; if (start) begin cs_n 1b0; // 拉低片选 shift_reg tx_data; bit_cnt 0; busy 1b1; state SETUP; end end SETUP: begin // 等待半个SCLK周期让从机准备好 sclk CPOL; // 仍保持空闲电平 state TRANSFER; end TRANSFER: begin // 根据 CPOL/CPHA 产生 SCLK 并在正确边沿采样/移出数据 // 这里演示 Mode 0空闲低电平上升沿采样 sclk ~sclk; // 产生时钟翻转 // 具体的数据移位逻辑需要配合边沿条件 if (sclk 1b0) begin // 上升沿即将到来 // 在上升沿之前把下一bit放到MOSI上 mosi shift_reg[DATA_WIDTH-1]; // 同步采样MISO miso_d1 miso; miso_d2 miso_d1; shift_reg {shift_reg[DATA_WIDTH-2:0], miso_d2}; bit_cnt bit_cnt 1; if (bit_cnt DATA_WIDTH-1) begin state FINISH; end end end FINISH: begin cs_n 1b1; // 拉高片选结束通信 sclk CPOL; busy 1b0; state IDLE; end endcase end end assign rx_data shift_reg; endmodule这个框架关键点有三个片选时序IDLE 状态下 CS 保持高检测到 start 后立即拉低并转移到 SETUP 状态。这样 CS 拉低到第一个 SCLK 沿之间有了充足的建立时间SCLK 生成通过状态机内部的时钟翻转实现而不是直接用高频时钟分频。这样做的好处是SCLK 的占空比和相位与状态机紧密相关数据变化和采样点精确可控MISO 同步采样miso_d1 和 miso_d2 是两级同步寄存器能有效消除亚稳态。实际工作时在 SCLK 的采样沿前一点把 MISO 打拍确保采到稳定值3.3 SPI Slave 设计的关键点作为从机时FPGA 需要接收外部主机发来的 SCLK此时绝对不能把 SCLK 直接作为设计的主时钟。我的做法是// 用系统时钟同步外部 SCLK 和 CS reg sclk_sync1, sclk_sync2; reg cs_n_sync1, cs_n_sync2; wire sclk_rising sclk_sync2 ~sclk_sync1; // 检测上升沿 wire sclk_falling ~sclk_sync2 sclk_sync1; // 检测下降沿 always (posedge clk or negedge rst_n) begin if (!rst_n) begin sclk_sync1 1b0; sclk_sync2 1b0; end else begin sclk_sync1 sclk; sclk_sync2 sclk_sync1; end end这里的核心思想是将异步的 SCLK 同步到系统时钟域然后检测边沿标志。这样外部 SCLK 频率只要远低于系统时钟频率经验法则是系统时钟至少是 SCLK 的 5 倍以上就能稳定采样。从机的数据逻辑不需要太复杂检测到 SCLK 边沿时根据模式把一个 bit 移入或移出。CS 拉高时表示一帧传输结束把接收到的 8bit 数据锁存到输出寄存器。3.4 一个完整案例FPGA 与 STM32H743 的 FMC-SPI 桥接在实际项目中FPGA 很少单独作为 SPI 传输的端点。我去年做过一个项目用 STM32H743 通过 FMC 总线并行访问 FPGAFPGA 再通过 SPI 控制四个 ADC 芯片采集数据。整个链路是STM32H743 的 FMC 并行接口向 FPGA 写入控制字和数据FPGA 内部把并行数据转成 SPI 时序分时轮询四个从机采集结果通过 FPGA 的寄存器接口返回给 STM32这里的难点在于STM32 的 FMC 时序和 SPI 时序是两种完全不同的协议中间需要一个数据缓冲和状态协调机制。我的做法是在 FPGA 内部开一组双口 RAM主机写入的数据先存进 FIFOSPI Master 状态机从 FIFO 取数据发送收到的数据再写回另一个 FIFO。这样两边速率失配的问题就自然解决了。这个案例也说明SPI 在 FPGA 里从来不是孤立存在的它往往是一长串数据通路中的一环设计时要考虑前后模块的接口和时序配合。4. 时序约束与上板调试实战4.1 必须写好的时序约束很多初学者写 SPI 代码后直接上板发现有数据错误也不知道从哪查。其实很多时候问题出在没有定义好跨时钟域约束。对于 SPI 这种低速接口至少需要set_input_delay约束 MISO 输入数据的到达时间set_output_delay约束 MOSI 和 SCLK 输出数据的输出时间set_false_path对于 CS 信号与 SCLK 之间的路径如果确定不需要严格分析可以设为 false path以 Xilinx Vivado 为例SPI 接口的约束可以这样写set_input_delay -clock [get_clocks sclk] -max 10 [get_ports miso] set_input_delay -clock [get_clocks sclk] -min 2 [get_ports miso] set_output_delay -clock [get_clocks sclk] -max 8 [get_ports mosi] set_output_delay -clock [get_clocks sclk] -min 1 [get_ports mosi]如果你用的是系统时钟直接采 MISO建议把 MISO 输入约束设为相对系统时钟的输入延迟。当然最稳妥的办法是把 SPI 接口的信号都约束在同一个时钟域下——即用系统时钟作为唯一的时序参考。4.2 如何用 ILA 和示波器定位问题上板调试最大的痛苦是“看起来好像通了但数据不对”。我的调试套路是第一步用 ILA集成逻辑分析仪抓内部信号。把 CS、SCLK、MOSI、MISO、状态机 state 寄存器全抓出来看一帧数据从启动到结束是否完整。这一步能排除 80% 的逻辑错误。比如你发现 CS 拉低后 SCLK 一直不变那大概率是状态机卡住了如果 SCLK 有波形但 MOSI 数据不对检查移位寄存器使能条件。第二步示波器看物理层波形。重点观察 SCLK 的占空比是否异常、MISO 数据的建立时间和保持时间是否满足要求。我曾经遇到过一个“偶发错误”用 ILA 看内部完全正常示波器一测才发现 MISO 数据变化沿和 SCLK 采样沿太接近余量只有 1~2ns。这种问题不是逻辑错误是时序余量不够解决办法要么降低 SCLK 频率要么调整采样点位置。第三步逻辑分析仪抓整帧通信。如果你同时用到主从两端的 SPI用一个支持协议解析的逻辑分析仪是最快的。它能直接标出每一 bit 是 0 还是 1一眼看出哪一位错了。4.3 常见的五类故障与排查速查表故障现象可能原因排查方法与解决CS 拉低后无响应状态机未启动或 SCLK 分频参数错误ILA 观察 state 是否进入 TRANSFER检查 start 信号是否满足时序要求数据全对但多/少一位数据位宽不匹配或 CPOL/CPHA 模式选错核对从机手册的时序图用逻辑分析仪看位计数偶发数据错误跨时钟域亚稳态或者时序余量不足增加两级同步寄存器降低 SCLK 频率调整 ILA 采样点MISO 一直高/低从机未正确响应可能片选地址错误或从机没上电示波器量 MISO 引脚电平确认从机状态检查电源和复位高速通信时波形失真PCB 走线过长或信号完整性差降低 SCLK 频率调整输出驱动强度考虑串联终端电阻4.4 关于仿真ModelSim 和 Vivado Simulator 的取舍上板之前一定要做仿真这是铁律。我自己习惯用 Vivado Simulator 做功能性仿真因为它集成在 Vivado 里不需要额外配置波形查看也方便。SPI 仿真的关键是要写一个模拟从机行为的 testbench。最简单的做法是用行为级代码模拟一个从机检测 SCLK 边沿把接收到的 bit 存入寄存器同时把预设的数据从 MISO 发出去。这样仿真跑一轮就能同时验证主机的发送和接收通路。有个小技巧仿真时钟频率可以适当调高一点比如实际 SCLK 是 10MHz仿真可以设成 100MHz这样跑一轮完整 8bit 传输只花仿真上的很短时间方便快速迭代验证。5. 实操心得与经验总结说实话SPI 在 FPGA 里的实现难度是“会者不难难者不会”。我把过去几年踩过的坑浓缩成几条第一一切要以器件手册为准。同样的 SPI不同芯片的时序细节差异巨大。有的芯片要求 CS 拉低后至少等 100ns 才能产生时钟有的芯片要求数据在 SCLK 下降沿前就稳定。这些数字只有手册能告诉你猜是猜不出来的。第二调不通不是代码问题是执行顺序问题。我的调试顺序永远是先看物理层波形 → 再看内部逻辑 → 最后怀疑代码。很多新人一上来就盯着 Verilog 代码猛看其实如果示波器上连波形都没有那肯定是工程配置的问题而不是逻辑的问题。第三客制化需求要提前封装。SPI 虽然协议简单但每个器件总有一些“特殊要求”比如某寄存器写入后需要等待、某指令需要不同的位宽。我会把这些特殊逻辑封装成单独的模块而不是散落在主状态机里否则后期维护起来会非常痛苦。第四通用性值得多花一点时间。如果项目里 SPI 会对接多个外设参数化设计非常划算。把数据位宽、CPOL/CPHA、SCLK 分频系数做成参数后期一个新器件接入可能只需要改两三行配置不用动核心代码。第五留足时序余量是王道。不要卡着上限跑。比如器件最高支持 50MHz SPI我通常只配到 25MHz 左右。高速数字设计里余量就是稳定性和后续调试空间省出来的那点时间在量产后会变成十倍百倍的坑。SPI 通信的 FPGA 实现看似简单但真正在工程中稳定跑起来需要理解协议本质、处理好跨时钟域、写对时序约束再配上一套靠谱的调试手段。把这五块吃透了基本上 SPI 相关的项目都能稳稳拿下。