
简介SPI作为微控制器与外部设备间广泛应用的串行接口具有简单高效的特点针对其Verilog实现该压缩包提供了完整的工程文件与源码面向FPGA开发者和数字逻辑设计学习者帮助解决SPI通信中的时钟同步、数据收发与片选控制等问题。资源共包含167个文件压缩包整体大小仅649KB核心内容为Verilog源码.v文件同时包含ISE工程文件、UCF引脚约束、仿真波形记录.wlf、工程日志及综合中间文件等内容目录结构清晰完整覆盖源码、约束与仿真输出便于按流程查阅。目前已有482人学习下载。设计提供SPI主模块与从模块的完整可用代码支持通过CPOL和CPHA设定四种通信模式可适应不同外设的时序要求关键逻辑包括分频计数产生SCK、边沿检测同步、移位寄存器收发数据、SS片选控制等采用参数化写法可灵活调整数据位宽与传输速率适合在FPGA上快速验证或作为学习模板复用。工程资料中还保留了综合与仿真全过程记录有助于理解从RTL到FPGA实现的整个流程对课程设计、毕业设计和产品开发均有参考价值对于需要学习数字接口设计、完成课程作业或进行FPGA项目开发的读者这套资料能提供从代码到仿真验证的闭环参考。1. SPI Verilog 实现前必须想清楚的三个问题一个很常见的现象照着网上例程写了个 SPI 主控仿真波形漂亮接到 W25Q64 上读 JEDEC ID读回来全是 0。多数时候问题不在协议本身而在采样沿选错、第一位数据的建立时序不对或者片选释放得太早。SPI 看起来只有四根线但 CPOL/CPHA 的组合、MSB/LSB 顺序、多字节传输时片选保持策略任何一个环节出错表现都是“数据不对但波形又很像”。这篇内容把 SPI Verilog 实现拆成可落地的路径先从时序图反推模块接口再给出一份可综合、可调模式的主控代码接着用 Testbench 和真实 Flash 验证最后落到多字节收发和软硬件片选的取舍上。适合正在写 FPGA SPI 接口、被时序细节卡住的工程师也适合想把 SPI 彻底吃透的 Verilog 初学者。2. 从 SPI 时序图反推 Verilog 模块划分2.1 对照数据手册确定 CPOL/CPHA 与采样沿SPI 主控最核心的参数不是波特率而是 CPOL 和 CPHA。CPOL 决定 SCLK 空闲电平CPHA 决定数据是在第一个边沿还是第二个边沿被采样。这两个参数组合出四种模式很多器件手册直接写“支持 Mode 0/3”但有些只给时序图需要自己判断。模式CPOLCPHASCLK 空闲电平数据采样沿常见器件Mode 000低上升沿第一个沿W25Q64、MAX31865Mode 101低下降沿第二个沿部分 AD 采集芯片Mode 210高下降沿第一个沿部分传感器Mode 311高上升沿第二个沿W25Q64、SD Card判断方法很简单看数据在哪个边沿变化在哪个边沿稳定。如果数据在 SCLK 下降沿变化、上升沿稳定那就是 Mode 0反过来数据在上升沿变化、下降沿稳定就是 Mode 1。拿示波器抓一下现有驱动的 SCLK 和 MOSI比对着手册猜更快。提示不要凭“Flash 一般用 Mode 0”去套所有器件。同一颗芯片的不同指令、不同工作模式片选时序可能完全不一样。2.2 主控接口定义四线制端口与参数化设计确定了模式之后下一步是定义模块端口。一个典型的 SPI 主控需要两类接口一类接系统总线时钟、复位、启动、数据另一类接外部器件SCLK、MOSI、MISO、CS。数据位宽做成参数方便后续扩展成 16 位、32 位传输。module spi_master #( parameter DATA_WIDTH 8, // 单次传输位宽 parameter CPOL 0, // 时钟极性 parameter CPHA 0, // 时钟相位 parameter CLK_DIV 10 // 分频系数sclk clk / CLK_DIV )( 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 reg done, // 传输完成脉冲 output reg sclk, // SPI 时钟 output reg mosi, // 主出从入 input wire miso, // 主入从出 output reg cs_n // 片选低有效 );MISO 必须是 inputMOSI 和 SCLK 是 output这是 SPI 规范定的方向不能反。done设计成单周期脉冲调用方看到done为高就可以读取rx_data并启动下一拍。片选cs_n放在主控内部控制还是由上层控制会影响多字节传输的写法后面第 5 章专门说。2.2.1 状态机与计数器两种写法的适用边界SPI 主控有两种常见写法纯计数器和三段式状态机。纯计数器适合固定单字节回环测试代码量小但扩展多字节连续传输时要加额外逻辑。三段式状态机可读性和可维护性更好一般把传输拆成 IDLE、XFER、HOLD 三个状态。IDLE等待 start拉低 cs_n准备数据。XFER按位收发数据驱动 SCLK 翻转。HOLD结束后拉高 cs_n输出 done 脉冲。这里用状态机还有一个好处可以在 IDLE 和 XFER 之间插入等待周期满足从机的 CS 建立时间这在连接 AD7124 这类对时序敏感的器件时非常有用。3. 用 Verilog 写一个支持四模式的 SPI 主控3.1 可直接综合的主控模块代码下面这份代码是我在实际项目里常用的结构支持四种 SPI 模式位宽和分频系数都是参数化配置。核心思路是用 SCLK 翻转前的电平判断即将出现的是第一个沿还是第二个沿从而决定是采样还是移位。module spi_master #( parameter DATA_WIDTH 8, parameter CPOL 0, parameter CPHA 0, parameter CLK_DIV 10 )( 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 reg done, output reg sclk, output reg mosi, input wire miso, output reg cs_n ); localparam IDLE 2d0; localparam XFER 2d1; localparam HOLD 2d2; reg [1:0] state; reg [7:0] clk_cnt; reg [3:0] bit_cnt; reg [DATA_WIDTH-1:0] tx_buf; reg [DATA_WIDTH-1:0] rx_buf; // 在 clk_cnt 溢出翻转 SCLK 的那一拍sclk 还是旧值 // CPHA0第一个沿采样CPHA1第二个沿采样 wire sample_here (CPHA 1b0) ? (sclk CPOL) : (sclk ! CPOL); wire shift_here (CPHA 1b0) ? (sclk ! CPOL) : (sclk CPOL); always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; sclk CPOL; cs_n 1b1; mosi 1b0; done 1b0; clk_cnt 8d0; bit_cnt 4d0; end else begin case (state) IDLE: begin done 1b0; if (start) begin cs_n 1b0; bit_cnt 4d0; // CPHA0第一位数据在 CS 拉低后就要就绪 if (CPHA 1b0) begin mosi tx_data[DATA_WIDTH-1]; tx_buf {tx_data[DATA_WIDTH-2:0], 1b0}; end else begin tx_buf tx_data; end state XFER; end end XFER: begin if (clk_cnt (CLK_DIV/2 - 1)) begin clk_cnt 8d0; // 先判断沿类型再翻转 SCLK if (shift_here) begin mosi tx_buf[DATA_WIDTH-1]; tx_buf {tx_buf[DATA_WIDTH-2:0], 1b0}; end if (sample_here) begin rx_buf {rx_buf[DATA_WIDTH-2:0], miso}; if (bit_cnt DATA_WIDTH-1) begin rx_data {rx_buf[DATA_WIDTH-2:0], miso}; state HOLD; end else begin bit_cnt bit_cnt 1; end end sclk ~sclk; end else begin clk_cnt clk_cnt 1; end end HOLD: begin cs_n 1b1; done 1b1; state IDLE; end endcase end end endmodule3.1.1 关键逻辑说明sample_here和shift_here是整个设计的核心。以 Mode 0 为例CPOL0、CPHA0SCLK 空闲为低。当clk_cnt溢出准备翻转时如果当前 SCLK 还是低说明即将产生上升沿这是第一个沿sample_here为真主机在这个沿采样 MISO如果当前 SCLK 是高即将产生下降沿这是第二个沿shift_here为真主机在这个沿把下一位数据放到 MOSI。CPHA1 时正好反过来第一个沿是移位沿第二个沿是采样沿所以sample_here的条件取反。用这种方式判断边沿比打一拍检测sclk跳变少一个时钟周期延迟避免采样点整体后移。IDLE 状态里对tx_buf的处理也需要特别注意。CPHA0 时从机在第一个采样沿就要读到第一位数据所以 CS 拉低后必须立刻把tx_data的最高位放到 MOSI 上同时把tx_buf左移一位让移位寄存器最高位变成第二位数据。CPHA1 时第一位在第一个移位沿才给出不需要预置。3.1.2 参数设置与时钟分频CLK_DIV必须是偶数因为 SCLK 每CLK_DIV/2个系统时钟翻转一次一个完整 SCLK 周期需要CLK_DIV个系统时钟。比如系统时钟 50MHz想要 5MHz 的 SCLKCLK_DIV取 10。CLK_DIV系统时钟 50MHz 时 SCLK 频率适用场景412.5MHzW25Q64 快速读短走线105MHz常规 Flash 读写202.5MHz传感器采集抗干扰501MHz长线连接、低速 AD 芯片SCLK 频率不是越高越好。连接线超过 10cm、没有阻抗匹配时建议把频率降到 5MHz 以下。MOSI 数据在 SCLK 边沿前需要建立时间CLK_DIV越大建立时间越充裕这是低速场景下优先调大分频系数的原因。3.2 和软件模拟 SPI 的差异MCU 上经常用 GPIO 软件模拟 SPI每个 bit 靠延时翻转引脚。FPGA 里写 SPI 主控本质上也是“软件模拟”只不过这个“软件”变成了状态机运行在硬件时钟上。软件模拟的优势是灵活引脚任意选Verilog 实现的优势是时序精确、不占 CPU、可以同时跑多个 SPI 实例。但 Verilog 实现有一个容易踩的坑软件模拟时可以每 bit 之间延时半周期而 Verilog 主控的 SCLK 必须连续翻转不能中途停顿。如果从机要求字节之间有间隔需要在状态机里显式插入等待周期否则从机可能把两个字节当成一个连续流。4. 仿真验证与真实器件调试4.1 Testbench用回环从机验证时序写一个简单的回环 Testbench让 MISO 在 SCLK 下降沿输出移位寄存器中的值主机在上升沿采样。这样能同时验证发送和接收两条路径。timescale 1ns/1ps module tb_spi_master; reg clk 0; reg rst_n 0; reg start 0; reg [7:0] tx_data 8hA5; wire [7:0] rx_data; wire done, sclk, mosi, cs_n; reg miso 0; always #10 clk ~clk; // 50MHz spi_master #( .DATA_WIDTH(8), .CPOL(0), .CPHA(0), .CLK_DIV(10) ) u_spi ( .clk(clk), .rst_n(rst_n), .start(start), .tx_data(tx_data), .rx_data(rx_data), .done(done), .sclk(sclk), .mosi(mosi), .miso(miso), .cs_n(cs_n) ); // 回环从机每个 SCLK 下降沿送出最高位同时移入 MOSI reg [7:0] loop_buf; always (negedge sclk or negedge cs_n) begin if (!cs_n) begin miso loop_buf[7]; loop_buf {loop_buf[6:0], mosi}; end end initial begin #20 rst_n 1; #30 start 1; #20 start 0; wait (done 1); $display(rx_data %02h, rx_data); #100 $finish; end endmodule这段代码里loop_buf初始值是 0所以第一次回环读到的rx_data会是 0这正常。测试的重点是观察 SCLK 的翻转频率、CS 的拉低时机、MOSI 数据是否在每个移位沿提前就绪。把loop_buf初始化为8hC3就能验证主机能不能正确采到固定数据。4.1.1 仿真常见的两个问题仿真时最容易发现的两类问题一是done信号一直不拉高说明bit_cnt没有计数到边界检查sample_here的极性是否反了二是rx_data整体移位一位通常是 CPHA 参数和器件时序不匹配把 CPHA 取反试试。4.2 连接 W25Q64 读 JEDEC ID 的验证流程W25Q64 是验证 SPI 主控最常用的器件支持 Mode 0 和 Mode 3。读 JEDEC ID 的指令是0x9F发送完指令后从机连续返回 3 个字节 ID整个过程 CS 必须保持低电平。// 伪代码用主控模块连续三次调用读指令 // 第 0 次tx_data 8h9F忽略 rx_data // 第 1 次tx_data 8h00rx_data 为 ID 第 1 字节 // 第 2 次tx_data 8h00rx_data 为 ID 第 2 字节 // 第 3 次tx_data 8h00rx_data 为 ID 第 3 字节调用方式是在每次done拉高后立即给下一次start拉高。注意 W25Q64 在发送指令期间 MISO 还在输出无意义数据所以第一次的rx_data要丢弃。正常读出来的 ID 前两个字节是0xEF 0x40第三字节是容量码例如0x17对应 8Mb。如果读回全是0xFF先看 CS 是否真的在整个传输期间保持低电平。用逻辑分析仪抓波形确认指令字节准确出现在 MOSI 上再检查 MISO 有没有接错引脚。4.3 调试 SPI Verilog 代码的故障定位方法现象可能原因排查方向读回全 0采样沿选错、MISO 未连接、CS 没拉低抓 SCLK 和 MISO 波形对比数据错位 1 bitCPHA 参数与器件不匹配反转 CPHA 重新仿真数据反序MSB/LSB 顺序搞反检查移位方向和寄存器索引仿真正常、上板失败引脚约束错误、三态处理不当检查 XDC 约束和 IO 标准MISO 偶尔跳变干扰严重、建立时间不足降低 SCLK 频率加滤波MISO 毛刺在高频模式下容易出现常见做法是在主时钟域对 MISO 打两拍再用三点滑动窗口滤波。这个技巧对长线传输特别有效。reg miso_d1, miso_d2; always (posedge clk) begin miso_d1 miso; miso_d2 miso_d1; end wire miso_clean (miso miso_d1) | (miso miso_d2) | (miso_d1 miso_d2);注意滑动窗口滤波会引入至少两个时钟周期延迟对 SCLK 高于 10MHz 的场景要慎用否则建立时间会变得非常紧张。5. SPI Verilog 多字节收发与软硬件片选的取舍5.1 多字节连续读的状态机扩展单个字节的主控模块改成多字节模式核心改动是把 HOLD 状态变成“判断是否还有下一字节”。读 W25Q64 这类操作要求 CS 在整个指令和数据传输期间保持低电平所以不能每读完一个字节就拉高 CS。常见做法是增加一个byte_total参数和一个byte_index计数器在 HOLD 状态判断// HOLD 状态扩展示意 HOLD: begin if (byte_index byte_total - 1) begin cs_n 1b1; done 1b1; state IDLE; end else begin byte_index byte_index 1; state XFER; // 保持 CS 为低继续下一字节 end end这里cs_n在 HOLD 状态不能无条件拉高否则多字节读会被截断。这也是软件片选比硬件片选灵活的地方硬件片选由外设自动控制很多 MCU 的 SPI 外设会在每个字节之间自动拉高片选做 Flash 连续读时就非常别扭。在 FPGA 里自己控制cs_n想保持多久就保持多久。5.2 软件片选与硬件片选的实际选择在纯 FPGA 设计中我一般直接选择软件片选也就是用状态机里的寄存器控制 CS。好处有两个一是多字节传输时 CS 的保持时间完全可控二是可以通过调整状态机自由插入等待周期适配慢速从机。代价是自己要保证 CS 拉低、拉高的建立和保持时间通常至少留出半个 SCLK 周期。硬件片选更适合 SoC 场景比如 ARM 处理器自带的 SPI 控制器片选由硬件模块管理CPU 只需要写寄存器。Linux 下的 SPI 驱动也支持软件拉片选通过设备树配置cs-gpios把片选映射到 GPIO本质上和 FPGA 里的软件片选一个思路——把控制权交给驱动代码。关于 DMA独立编写的 SPI Verilog 主控不需要考虑 DMA主控本身就在硬件里。只有把 SPI 挂到 CPU 总线上、需要搬运大块数据时才涉及 DMA 请求FPGA 侧用 FIFO 缓存收发数据就足够了。最后提一个实用细节无论单字节还是多字节cs_n的拉低动作一定要和start信号在同一拍内用非阻塞赋值完成。这样从机看到的第一个 SCLK 边沿一定在 CS 稳定之后不会出现片选还没拉低、时钟先跳的违例。把这一点写进状态机比事后调时序约束省事得多。本文还有配套的精品资源点击获取