简介:面向FPGA开发初学者与嵌入式设计人员,这份基于FPGA的SPI通信接口设计资源提供完整的Quartus II工程与硬件描述语言源码。设计围绕SPI主从通信协议,涵盖四个信号线的时序控制、状态机编写、引脚约束与开发板原理图,能够帮助读者在FPGA平台上实现并调试SPI接口,用于与传感器、存储器、显示模块等外设进行高速数据交换。压缩包共428个文件,大小5.47MB,主要文件类型包括qpf/qsf工程文件、tdf/vhd设计源码、vwf/bsf仿真与原理图文件、sof/pof配置下载文件,以及pdf/docx说明文档;工程目录完整,便于逐步对照与复用。目前已有1160人学习下载,适合希望系统掌握FPGA串行通信接口设计、理解SPI读写时序和硬件描述语言编程的学习者。通过该资源,读者可以走通设计、仿真、综合、管脚分配与下载的完整FPGA开发流程,并获得可直接引用的工程模板、管脚约束参考和排错思路,实践价值较高。
1. 用 FPGA 重造 SPI 接口,到底在解决什么问题
SPI 大概是数字接口里最“不像协议”的协议:四根线、一根时钟、一根使能,数据沿着边沿走。正因为它简单,几乎所有板级芯片都愿意给它留几个引脚,从 Flash、ADC、传感器到显示屏,无一例外。但简单只是表象,一旦把它放进 FPGA 工程里,事情立刻变味:ARM 上调用库函数三行代码解决的问题,到 FPGA 这边要自己设计移位寄存器、自己分频、自己处理跨时钟域。很多刚接触 FPGA 的工程师第一次被问“SPI 你怎么做”时,第一反应是“这不就是一个 shift register 吗”——然后就在时序约束和片选毛刺上栽了跟头。
这篇文章要做的,就是把这套“看起来简单、做起来全是细节”的 SPI 接口完整拆开:从物理层时序模型讲起,手写一个可综合的 SPI Master 模块,配上状态机设计、时钟分频策略、时序约束方法,最后落到一个真实的 W25Q64 Flash 读写例子上。你会发现,SPI 在 FPGA 里的难点从来不在协议本身,而在三大问题上:跨时钟域的可靠性、时序约束的完备性、多从机场景下的片选与三态控制。这些问题不解决,仿真再漂亮,上板就是随机的。
适合谁读?如果你写过 I2C、UART,但从没在 FPGA 里独立完成过 SPI 接口;或者你已经在用 Xilinx 的 AXI Quad SPI IP,但想知道 IP 内部到底发生了什么;再或者你即将面对 AD7124 这类高速 ADC 的 SPI 采集任务——这篇文章能让你少走至少两周弯路。下面所有代码均基于 Verilog 编写,可在 Xilinx 或 Intel 主流器件上直接综合,不依赖任何厂商原语。
2. SPI 协议层:时序模型、四种模式与主从机职责划分
2.1 四线机制里最容易忽略的“第三根线”
SPI 的全称是 Serial Peripheral Interface,四根线分别是:SCLK(串行时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)。从机靠 CS 的下降沿知道自己被选中,靠 SCLK 的边沿采样数据,靠 MOSI/MISO 完成双向传输。这里有三个细节,初学者几乎必踩:
第一,CS 是低有效还是高有效取决于从机手册,W25Q64 是低有效,但有些传感器是高有效,设计时不能写死。第二,MOSI 和 MISO 在标准 SPI 中是独立的,这意味着全双工传输是天然支持的——主机在发送一个字节的同时可以接收一个字节,这跟 I2C 的半双工有本质区别。第三,也是最容易被忽略的:从机在 CS 拉高(释放)时必须把 MISO 置为高阻态,否则多个从机共用一个 MISO 总线时会发生冲突。这就是为什么 MISO 必须用三态门控制,而不是单纯地拉高拉低。
CPI(Clock Polarity)和 CPHA(Clock Phase)共同定义了四种 SPI 模式。CPOL 决定空闲时 SCLK 的电平,CPHA 决定数据在哪个边沿被采样。模式 0(CPOL=0, CPHA=0)是最常见的,它在 SCLK 上升沿采样数据、下降沿切换数据。模式 1 是上升沿切换、下降沿采样。模式 2 和模式 3 把空闲电平拉高,其余逻辑类推。在 FPGA 设计里,模式选择不是写一个 case 语句就完了,它直接影响你采样时刻的裕量计算,后面讲时序约束时会具体展开。
2.1.1 从时序图推导采样时刻,比背模式表格可靠
与其背四种模式的表格,不如学会读时序图。以模式 0 为例:SCLK 空闲为低,第一个上升沿到来时,主机已经把 MOSI 上的第一位数据准备好,从机在当前上升沿采样;接下来 SCLK 下降沿时,主机切换下一位数据。这个“上升沿采样、下降沿切换”的节奏,定义了数据的建立时间和保持时间需求。
在 FPGA 代码里,模式 0 的直观写法是:在 SCLK 上升沿打一拍mosi_reg,在 SCLK 下降沿采样miso。但这里有一个工程陷阱——如果你在 FPGA 内部自己生成 SCLK(分频得到),那么用 SCLK 作为敏感信号去采样 MISO 是不可靠的。原因在于,FPGA 内部的时钟网络(BUFG)才是全局时钟,分频后的 SCLK 走的是普通布线资源,到达各寄存器的延迟不一致,你可能会采样到毛刺或亚稳态。所以严谨的做法是:FPGA 内部用一个高频时钟作为系统时钟,SPI 的 SCLK 由系统时钟分频产生,但所有寄存器的敏感信号都使用系统时钟,SCLK 只作为数据信号参与组合逻辑和判断。
2.2 主从职责:为什么“主”要负责提供一切节奏
SPI 没有应答机制,主机发时钟,数据就跟着走;主机不发时钟,从机就原地等待。这个特性是双刃剑:优点是协议开销极小,吞吐率可以做到很高(W25Q64 支持 133MHz 的时钟);缺点是从机没有时钟就不能发送数据,主机必须在发送请求帧的同时“顺手”把从机的响应数据接收出来。最常见的典型场景就是读 Flash:主机发送“读指令 + 地址”,同时接收到的 MISO 数据是无效的;等到主机继续发送哑数据(dummy byte)时,MISO 上才是真正从 Flash 返回的数据。
因此设计 SPI Master 时,必须定义清楚一条基本规则:发送一字节与接收一字节是捆绑在一起的,无论主机是否需要发送有效数据,SCLK 都要继续产生,MISO 都要被采样。代码层面的含义是:Master 模块内部始终同时驱动 MOSI 和采样 MISO,不存在“只发送不接收”或“只接收不发送”的模式。要发送时在 MOSI 上放数据,要接收时在 MOSI 上放 0xFF(或任意哑数据),从机自然会配合时钟给出响应数据。
3. 手写 SPI Master:Verilog 实现、状态机设计与参数化配置
3.1 模块端口定义与参数设计思路
现在开始写可复用的 SPI Master 模块。端口设计上要遵循“控制与数据分离”的原则:对外提供简单的读/写接口,内部封装时序细节,让上层模块不需要关心 SCLK 是几分频、CS 什么时候拉低什么时候拉高。参数化设计的关键是时钟分频系数CLK_DIV、数据位宽DATA_WIDTH和 SPI 模式SPI_MODE。这三个参数必须在顶层用parameter定义,不能写死在代码里。
在 Xilinx 的 Vivado 或 Intel 的 Quartus 里,代码写完后需要重新设置语言版本。这里要特别说明:推荐使用 SystemVerilog 的logic类型来简化端口声明,但如果你要兼容老工程或第三方工具链,用reg和wire更稳妥。下面代码兼容 Verilog-2001,可直接使用。
module spi_master #( parameter CLK_DIV = 4, // 系统时钟分频系数,SCLK = 系统时钟 / (2*CLK_DIV) parameter DATA_WIDTH = 8, // 数据位宽,默认 8 位 parameter SPI_MODE = 0 // 0~3,对应四种 SPI 模式 )( input wire clk, // 系统时钟,频率必须大于 SCLK*2*CLK_DIV 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 busy, // 忙信号,高电平表示传输中 output reg sclk, // SPI 时钟输出 output reg cs_n, // 片选输出,低有效 output reg mosi, // 主出从入 input wire miso // 主入从出 );参数说明:CLK_DIV决定 SCLK 频率,如果系统时钟是 50MHz,CLK_DIV=4时 SCLK 就是 50/(2×4) = 6.25MHz。这个频率对于大多数 Flash 和 ADC 都足够,但如果你要驱动高速器件(比如 50MHz 以上的 SPI NOR Flash),可以调小CLK_DIV,但要注意 FPGA 内部布线延迟和从机的最高时钟限制。SPI_MODE同样是可综合的 parameter,通过在 generate 块或 always 块里根据参数选择边沿逻辑。
3.2 内部状态机:用“半位周期”划分时间槽
SPI 传输最麻烦的是边沿对齐。我的做法是把一个 SCLK 周期切成两个半周期:前半周期是“切换数据”阶段,后半周期是“采样数据”阶段。这个“半周期”概念可以统一处理四种模式:模式 0 在上升沿采样、下降沿切换,那么切换发生在半周期的起始处,采样发生在半周期的中点。用系统时钟计数到这个中点位置,就产生了采样时刻。
localparam IDLE = 3'd0; localparam SEND_BIT = 3'd1; localparam SAMPLE_BIT= 3'd2; localparam FINISH = 3'd3; reg [2:0] state; reg [DATA_WIDTH-1:0] tx_shift; reg [DATA_WIDTH-1:0] rx_shift; reg [7:0] clk_cnt; reg [3:0] bit_cnt; // CPOL: 空闲时钟极性 wire cpol = SPI_MODE[1]; // CPHA: 采样相位,1 表示第二个边沿采样 wire cpha = SPI_MODE[0]; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; sclk <= cpol; // 空闲状态时 SCLK 等于 CPOL 值 cs_n <= 1'b1; mosi <= 1'b0; busy <= 1'b0; clk_cnt <= 0; bit_cnt <= 0; end else begin case (state) IDLE: begin if (start) begin state <= SEND_BIT; cs_n <= 1'b0; // 拉低片选 tx_shift <= tx_data; busy <= 1'b1; clk_cnt <= 0; bit_cnt <= 0; // 根据 CPHA 决定首 bit 何时放上 MOSI if (cpha == 1'b0) mosi <= tx_data[DATA_WIDTH-1]; end end SEND_BIT: begin // 半周期计数到达 CLK_DIV 时翻转 SCLK if (clk_cnt == CLK_DIV - 1) begin clk_cnt <= 0; sclk <= ~sclk; // 在“切换边沿”将下一位数据准备好 if (sclk == (cpha ? cpol : ~cpol)) begin tx_shift <= tx_shift << 1; mosi <= tx_shift[DATA_WIDTH-2]; end end else begin clk_cnt <= clk_cnt + 1; end end // 更多状态... endcase end end代码逻辑说明:state在 IDLE 收到start信号后跳转,cs_n拉低表示从机被选中。clk_cnt负责分频:当计数到CLK_DIV-1时翻转sclk,从而实现将系统时钟分频为 SCLK。关键逻辑是if (sclk == (cpha ? cpol : ~cpol))这一行——它判断当前 SCLK 的电平是否等于“数据切换边沿”的电平,如果是,就切换下一个 bit 的数据。为什么要这样判断?因为当sclk被翻转后,它的当前值就是刚切换后的电平,而这个电平所对应的边沿正是 CPHA 定义的数据切换时刻。
采样逻辑与发送逻辑在不同模式下是对称的:模式 0 和模式 1 在上升沿采样,模式 2 和模式 3 在下降沿采样。仔细分析会发现,采样边沿实际上就是 SCLK 从cpol变为~cpol的那一下。所以采样的判断条件是:
// 在采样边沿,锁存 MISO 数据 if (sclk == (cpha ? ~cpol : cpol)) begin rx_shift <= {rx_shift[DATA_WIDTH-2:0], miso}; end当bit_cnt计数到DATA_WIDTH-1且sclk又完成一次翻转后,状态机进入 FINISH 状态,拉高cs_n,把rx_shift赋值给rx_data,释放busy。
3.3 参数化拆解:CLK_DIV 与 SPI_MODE 的数学关系
关于CLK_DIV,有一个容易算错的点。SPI 的 SCLK 频率计算公式是:
f_sclk = f_sysclk / (2 * CLK_DIV)注意这里有系数 2,因为 SCLK 翻转一次需要CLK_DIV个系统时钟周期,而一个完整的 SCLK 周期包含两次翻转(低到高、高到低),所以总共是2 * CLK_DIV个系统时钟周期。如果你在时序报告里看到 SCLK 的实际频率和自己预期不符,先检查是不是忘记了乘 2。
SPI_MODE参数需要同时影响空闲电平、采样边沿和数据切换边沿三个位置。在代码里,我让它同时控制初始化时的sclk值(sclk <= cpol)、采样边沿的条件表达式和发送边沿的条件表达式。这样写的好处是,上层模块只需传递SPI_MODE数值,接口逻辑全部分离,不需要外部加转换逻辑。
4. 从理论到上板:时序约束、硬件片选与跨时钟域的工程化落地
4.1 约束 SCLK 与数据线的 set_input_delay / set_output_delay
这一步很多 FPGA 工程师会跳过去。仿真能过、上板测试能工作,就觉得约束是走形式。但当你把 SCLK 频率提高、或者从机芯片换成更苛刻的器件时,没约束的工程会以非常妖异的方式失败——有时好用有时不好用,换一片 FPGA 就崩。SPI 接口跨出 FPGA 芯片后,走的是器件引脚到外部芯片引脚的物理路径,时序延迟必须显式告知工具,否则工具的静态时序分析(STA)会把外部延迟当零处理。
以 Xilinx 为例,在约束文件 XDC 中需要为 SPI 引脚建三类约束:时钟约束、输出延迟约束、输入延迟约束。时钟约束先创建虚拟时钟代表 SCLK:
create_clock -name spi_sclk -period 160.0 [get_ports {sclk}]这个命令要求 FPGA 把sclk引脚当作输出时钟来对待。假如系统时钟是 50MHz(周期 20ns),分频后 SCLK 周期是 160ns,那么在约束里就应该写 160ns。如果你不创建这个虚拟时钟,工具会把 SCLK 当成普通数据端口,不会分析它与外部器件之间的建立时间关系。
输出延迟约束描述 FPGA 发送数据到外部器件输入端的路径延迟,包括 PCB 走线延迟和器件自身的建立时间要求:
set_output_delay -clock [get_clocks spi_sclk] -max 20.0 [get_ports {mosi cs_n}] set_output_delay -clock [get_clocks spi_sclk] -min 5.0 [get_ports {mosi cs_n}]参数说明:max 值 = PCB 最大走线延迟 + 外部器件建立时间,min 值 = PCB 最小走线延迟 - 外部器件保持时间,实际数值要根据你的板卡和从机数据手册计算。输入延迟同理,描述从机 MISO 输出的数据到达 FPGA 引脚的时间:
set_input_delay -clock [get_clocks spi_sclk] -max 15.0 [get_ports {miso}] set_input_delay -clock [get_clocks spi_sclk] -min 2.0 [get_ports {miso}]提示:如果从机手册只给了 t_valid(时钟沿到数据有效的延迟),那么输入延迟的 max 值直接取 t_valid + PCB 走线延迟。很多工程师把这里写成 0,等于假设从机数据瞬间到达,这在低速下运气好能过,高速下必然出错。
4.2 硬件片选与软件片选的取舍,以及多从机三态控制
片选信号有两种控制方式:硬件自动片选(每发一字节自动拉低拉高)和软件手动控制(CS 由上层模块控制,可以连续拉低多个字节)。在 FPGA 里,这两种方式都可以实现。硬件片选适合“单字节读/写”的场景,代码简单,但从机在每字节之间会看到 CS 拉高再拉低,有些器件不支持这种字节间隔,会要求 CS 在整个命令序列期间必须保持低电平。
软件片选的做法是把cs_n的控制权从 Master 模块里拿出来,由上层状态机负责:先拉低 CS,然后连续调用 Master 的发送接口,全部字节传输完成后再拉高。实现上只需要把 Master 模块中的cs_n信号改为输入(由上层控制)或增加一个cs_ctrl引脚来旁路内部控制。W25Q64 的读操作必须使用软件片选——因为读命令、地址和哑字节之间不允许 CS 拉高。
多从机场景下,每个从机各占一条 CS,但共享 SCLK、MOSI、MISO。这时 FPGA 的三个输出引脚(SCLK、MOSI、CS_0、CS_1...)可以同时驱动,但 MISO 是输入汇聚的,从机的 MISO 在未被选中时必须高阻。许多从机在 CS 拉高后会自动释放 MISO,但如果你遇到一个 CS 释放不及时的器件,需要在 FPGA 内部对 MISO 做上拉处理,或者串接一个电阻到电源。还有一个 FPGA 特有的小技巧:未选中的从机不仅 CS 要拉高,MOSI 上的电平也最好保持为 CPOL 电平,有些器件对 MOSI 的空闲状态敏感。
4.3 跨时钟域分析与 baud 生成器的边界情况
如果你的 FPGA 系统中,SPI Master 工作在一个时钟域,而数据源(比如 CPU 软核或 DMA 控制器)工作在另一个时钟域,那么数据在进入 Master 前必须做同步处理。最简单可靠的做法是多级寄存器同步(打两拍),但这只解决电平同步问题,不解决多比特数据一致性问题。
reg [1:0] start_sync; always @(posedge clk or negedge rst_n) begin if (!rst_n) start_sync <= 2'b00; else begin start_sync <= {start_sync[0], start_raw}; end end wire start_pulse = start_sync[1] & ~start_sync[0];这个代码把一个来自异步时钟域的脉冲信号start_raw打两拍,再通过边沿检测生成安全的使能脉冲。对于tx_data这类多比特数据,最稳妥的做法是先用握手信号(valid/ready)或异步 FIFO 把数据转到 Master 时钟域,再发起传输。不要侥幸地认为数据能稳定读出来,亚稳态一旦发生,数据错误是间歇性的,定位会非常痛苦。
分频系数的边界情况也一样值得注意。假设系统时钟 50MHz,你希望生成 12.5MHz 的 SCLK,那么CLK_DIV应该等于 2——此时 SCLK 每个半周期占 1 个系统时钟,寄存器可以在每个周期翻转一次,代码里clk_cnt == CLK_DIV - 1即clk_cnt == 1的条件会成立,但这会导致clk_cnt只有 0 和 1 两个状态,实际上每一拍都在翻转。这不是错误,但要注意此时 master 内部没有任何“准备窗口”,数据在翻转边沿的同一拍被切换,如果你在外部的采样正好打在这一拍上,会有竞争风险。我一般会保证CLK_DIV >= 3,给数据留出至少一拍的稳定窗口。
5. 实战:用 SPI Master 读写 W25Q64,附 ModelSim 仿真脚本
5.1 W25Q64 的命令序列与状态机设计
W25Q64 是华邦的 SPI NOR Flash,容量 64Mbit(8MB),SPI 最高时钟 133MHz。它支持标准 SPI、Dual SPI、Quad SPI,本节只涉及标准 SPI。以读数据(Read Data, 0x03)为例,需要主机依次发送:命令字节 0x03、三个地址字节(高字节在前)、若干哑字节(读模式下就是 0xFF 或任意值)。从机从第四个字节开始,每个 SCLK 返回一个数据字节,直到 CS 拉高。
写使能(Write Enable, 0x06)与写数据(Page Program, 0x02)的命令序列类似,但要注意 Flash 要求先发 0x06 再发 0x02,而且必须检查状态寄存器中的 WIP(Write In Progress)位确认写操作完成。这个“先命令后检查状态”的流程,是一个典型的上层状态机应用场景:
localparam SYS_IDLE = 4'd0; localparam SYS_CMD = 4'd1; localparam SYS_ADDR = 4'd2; localparam SYS_DUMMY = 4'd3; localparam SYS_READ = 4'd4; localparam SYS_WAIT = 4'd5; localparam SYS_DONE = 4'd6;状态转换规则:SYS_IDLE收到读请求后进入SYS_CMD,发送 0x03;完成后进入SYS_ADDR,分三次发送 24 位地址;接着进入SYS_DUMMY,发送一个哑字节后进入SYS_READ,连续读取 N 个字节,每个字节从 Master 的rx_data获取;全部读取完毕后进入SYS_DONE,拉高 CS。每个状态内部都调用第 3 节的 Master 模块,通过start信号触发一次单字节传输,等待busy拉低后获取数据。
5.2 验证波形:跑通仿真、检查关键信号
在 ModelSim 或 Vivado Simulator 中跑仿真,需要准备一个模拟的 Flash 从机模型。W25Q64 官方有 Verilog 模型,可以直接下载替换成测试模块。没有官方模型时,也可以只验证 Master 本身的时序:用一个简单的sram_like从机模块,根据地址返回不同数据即可。
`timescale 1ns/1ps module tb_spi_w25q64; reg clk = 0; reg rst_n = 0; reg start = 0; reg [7:0] tx_data = 0; wire [7:0] rx_data; wire busy; wire sclk, cs_n, mosi, miso; // 实例化 DUT spi_master #( .CLK_DIV(4), .DATA_WIDTH(8), .SPI_MODE(0) ) u_master ( .clk(clk), .rst_n(rst_n), .start(start), .tx_data(tx_data), .rx_data(rx_data), .busy(busy), .sclk(sclk), .cs_n(cs_n), .mosi(mosi), .miso(miso) ); // 50MHz 系统时钟 always #10 clk = ~clk; // 模拟一个最简单的从机响应 reg [7:0] slave_reg = 8'hA5; assign miso = slave_reg[7]; initial begin #20 rst_n = 1; #30 start = 1; tx_data = 8'h03; // 发送读命令 #20 start = 0; wait (busy == 1'b0); // 发送地址 start = 1; tx_data = 8'h00; #20 start = 0; wait (busy == 1'b0); start = 1; tx_data = 8'h00; #20 start = 0; wait (busy == 1'b0); start = 1; tx_data = 8'h00; #20 start = 0; wait (busy == 1'b0); // 哑字节 start = 1; tx_data = 8'hFF; #20 start = 0; wait (busy == 1'b0); $finish; end endmodule仿真时的观察重点有三个位置。第一,cs_n在start拉高后的下一个周期拉低,在传输完成后的下一个周期拉高,不能出现毛刺。第二,sclk占空比应该是 50%,即高电平持续CLK_DIV个时钟周期,低电平也持续CLK_DIV个时钟周期。第三,mosi在 SCLK 上升沿前已经稳定,也就是说在“切换边沿”时切换数据,在“采样边沿”时数据已经稳定。如果仿真波形和描述不符,优先核对SPI_MODE参数的位序与 CPOL/CPHA 的映射关系。
6. 三根探针:逻辑分析仪抓到的真实陷阱与解决手法
SPI 调试最有效的工具不是仿真波形,而是把逻辑分析仪(或 FPGA 内置的 ILA)接在 SCLK、MOSI/MISO、CS 上实测。以下三个坑是我在工程里反复踩过的,每一个都值得在上板前预判。
第一个是 MOSI 在 CS 拉低之前的电平毛刺。FPGA 引脚默认状态下 MOSI 可能是高阻态或不定态,当 CS 拉低、从机上电使能的一瞬间,MOSI 上的毛刺可能被当成有效数据采样。解决方法是把 MOSI 在空闲时驱动为确定的电平(通常是与 CPOL 无关的固定值,或者直接拉低),并保证 CS 比 MOSI 先稳定至少一个系统时钟周期。更保险的做法是在 Master 模块中,当busy=0时强制mosi = 1'b0或mosi = 1'b1,不让它悬空。
第二个坑是 MISO 的上拉/下拉与三态冲突。当你使用多个从机并联 MISO 时,用万用表测量 MISO 引脚电压,正常空闲态应该被上拉到高电平(因为从机释放总线)。如果你测到 0V 或中间电平,说明某个从机的 MISO 输出没有释放,或者上拉电阻阻值过大,会把信号拉不到逻辑高。FPGA 内部可配置的上拉PULLUP在这里很有用,但要注意它只能提供微弱的电流驱动,若 MISO 走线较长,建议外部加 4.7k 上拉。
第三个坑是 Flash 写操作之后的“忙等待”超时。W25Q64 的 Page Program 需要最长 3ms 的写入时间,这段时间内 Flash 不响应任何命令,只会在状态寄存器里把 WIP 位置 1。如果你在写完数据后立刻发读状态命令,读到的可能是 0xFF 或随机值,因为 Flash 正在忙。正确的做法是:发 0x05(Read Status Register-1)命令,读取状态字节,判断 bit0(WIP)是否为 0,为 0 才继续下一步。把这个忙等待逻辑做成一个带超时计数器的函数,超时值设为 10ms,避免极端情况下软件卡死在 while 循环里。以下代码展示了超时等待的实现方式:
// 等待 WIP 位清零,带超时保护 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin timeout_cnt <= 0; wip_done <= 1'b0; end else if (wip_done) begin // 已完成,不做任何操作 end else if (timeout_cnt >= TIMEOUT_MAX) begin wip_done <= 1'b1; // 超时退出,避免卡死 end else if (status_bit == 1'b0) begin wip_done <= 1'b1; // WIP 清零,写入完成 end else begin timeout_cnt <= timeout_cnt + 1; end end最后再提一个调试技巧:把 FPGA 内部的 ILA 探针触发条件设为cs_n下降沿,这样每次 SPI 传输开始时抓一拍,能最直观地看到整条命令序列是否按协议排列。特别是检查地址字节的字节序(Big-Endian 还是 Little-Endian)是否与 Flash 手册一致,这个问题一旦搞错,读回来的数据会呈现出混乱但规律性的错位,非常让人费解。
本文还有配套的精品资源,点击获取