1. 从“点灯”到“通信”:为什么SPI是FPGA工程师的必修课
很多朋友刚接触FPGA时,都是从点亮一个LED灯开始的。看着自己写的Verilog代码,通过一个简单的计数器控制GPIO引脚的高低电平,让LED闪烁起来,那种成就感是实实在在的。但很快你就会发现,FPGA的世界远不止于此。当你想让FPGA读取一个温度传感器的数据,或者控制一块OLED屏幕显示字符时,你立刻就会撞上“通信协议”这堵墙。而SPI,往往是这堵墙上最容易打开的第一扇门。
我刚开始学FPGA那会儿,也卡在这里很久。看着数据手册里SPI的时序图,那些CS、SCLK、MOSI、MISO信号线,感觉比控制LED复杂了不止一个数量级。但后来我明白了,SPI(Serial Peripheral Interface,串行外设接口)之所以成为嵌入式领域最基础、最常用的通信协议之一,恰恰是因为它的“简单”。这种简单不是功能上的简陋,而是设计哲学上的清晰和直接。它不像I2C那样需要复杂的起始、停止条件和应答机制,也不像UART那样需要事先约定好波特率。SPI的核心就是“同步”和“全双工”,主设备提供一个时钟,从设备跟着这个时钟的节拍收发数据,你发一位,我同时收一位,效率高,实现起来逻辑也相对规整。
对于FPGA工程师来说,掌握SPI的意义远不止于驱动某一个具体的外设。它更像是一把钥匙,帮你理解数字系统中“主从”设备如何协同工作,如何通过几根线实现高效的数据交换。更重要的是,用Verilog或VHDL去实现一个SPI主控制器,是一个绝佳的、从组合逻辑/时序逻辑的简单应用,过渡到复杂状态机设计的实战项目。你会第一次真正需要去仔细阅读一份外设的数据手册,理解它的寄存器映射、命令格式和时序要求,然后把这些要求翻译成精确的时钟周期级的硬件行为。这个过程,是FPGA开发从“玩具”走向“工具”的关键一步。
所以,这篇笔记不会只给你一段能“跑通”的SPI代码。我更想和你一起,从硬件工程师的视角,拆解SPI协议的每一个细节,理解其背后的设计考量,然后用Verilog亲手搭建一个健壮、可配置的SPI主控制器。我们会以驱动一个常见的SPI Flash存储器(比如W25Q128)为例,但设计的核心模块将具备通用性,稍作修改就能适配各种SPI设备。准备好了吗?让我们从最根本的协议层开始。
2. 深入骨髓:SPI协议的四线制与工作模式详解
在开始写代码之前,我们必须把SPI的“规矩”吃透。很多人对SPI有个模糊的印象:四根线,主从通信。但具体是哪四根?它们怎么配合?为什么同样的设备,有的代码能通有的就不行?问题往往出在对工作模式的理解偏差上。
SPI通常需要四根信号线,这是它的物理基础:
- SCLK (Serial Clock,串行时钟):由主设备产生并输出给所有从设备。这是整个通信的节拍器,所有数据位的传输都严格对齐时钟边沿。这里有个关键点:SCLK在空闲状态(即没有数据传输时)的电平是可配置的,这直接引出了CPOL参数。
- MOSI (Master Out Slave In,主出从入):主设备的数据输出线,从设备的数据输入线。主设备通过这根线向从设备发送指令和数据。
- MISO (Master In Slave Out,主入从出):主设备的数据输入线,从设备的数据输出线。从设备通过这根线向主设备回传数据。注意:在单主多从的典型架构中,所有从设备的MISO线需要“线与”(通常通过三态门实现),主设备通过片选信号来选择哪一个从设备有权驱动MISO线,避免总线冲突。
- CS/SS (Chip Select / Slave Select,片选/从机选择):由主设备控制,低电平有效。当主设备想和某个从设备通信时,就将对应从设备的CS线拉低。这是SPI总线实现“一主多从”的关键。CS信号必须在整个数据传输周期内保持有效(低电平)。
理解了物理连接,我们来看最核心也最容易出错的部分:SPI的工作模式。这由两个参数共同决定:时钟极性(CPOL)和时钟相位(CPHA)。
- CPOL (Clock Polarity,时钟极性):定义SCLK线在空闲状态(CS无效,无数据传输)时的电平。
- CPOL=0:SCLK空闲时为低电平。
- CPOL=1:SCLK空闲时为高电平。
- CPHA (Clock Phase,时钟相位):定义数据在时钟的哪个边沿被采样(捕获)。
- CPHA=0:数据在时钟的第一个边沿(如果CPOL=0,则是上升沿;如果CPOL=1,则是下降沿)被采样,并在下一个边沿切换。
- CPHA=1:数据在时钟的第二个边沿被采样,并在同一个边沿切换。
这两个参数的组合,形成了四种SPI模式,通常用Mode 0到Mode 3表示:
- Mode 0 (CPOL=0, CPHA=0):空闲时SCLK为低,数据在SCLK的上升沿被采样,下降沿切换。这是最常用的模式,很多传感器、Flash芯片都默认使用此模式。
- Mode 1 (CPOL=0, CPHA=1):空闲时SCLK为低,数据在SCLK的下降沿被采样,上升沿切换。
- Mode 2 (CPOL=1, CPHA=0):空闲时SCLK为高,数据在SCLK的下降沿被采样,上升沿切换。
- Mode 3 (CPOL=1, CPHA=1):空闲时SCLK为高,数据在SCLK的上升沿被采样,下降沿切换。
一个必须牢记的实操要点:你所实现的SPI控制器模式,必须与外设数据手册中要求的工作模式完全一致,否则数据采样会错位,导致通信完全失败。在阅读数据手册时,要仔细查找关于“SPI Mode”或“CPOL/CPHA”的描述。例如,W25Q128 Flash芯片就支持Mode 0和Mode 3。
为了更直观地理解这四种模式下的时序差异,特别是数据采样(MOSI/MISO稳定)和切换的时机,我整理了下表。在设计状态机时,这张表能帮你清晰地确定在哪个时钟边沿需要做什么操作。
| SPI模式 | CPOL | CPHA | SCLK空闲电平 | 数据采样边沿 (主设备采样MISO) | 数据切换边沿 (主设备切换MOSI) | 常见应用设备举例 |
|---|---|---|---|---|---|---|
| Mode 0 | 0 | 0 | 低电平 | SCLK上升沿 | SCLK下降沿 | W25Q系列SPI Flash, BMP280传感器 |
| Mode 1 | 0 | 1 | 低电平 | SCLK下降沿 | SCLK上升沿 | 某些型号的SD卡(SPI模式) |
| Mode 2 | 1 | 0 | 高电平 | SCLK下降沿 | SCLK上升沿 | 较少见 |
| Mode 3 | 1 | 1 | 高电平 | SCLK上升沿 | SCLK下降沿 | MAX31855热电偶转换器 |
如何根据上表设计你的Verilog代码?以最常用的Mode 0为例。对于主设备来说,“数据采样边沿”意味着你需要在这个边沿到来之前,确保MISO线上的数据已经稳定(从设备已经准备好)。因此,你的状态机应该在SCLK的下降沿(采样边沿的前一个边沿)去锁存(采样)MISO的数据。而“数据切换边沿”意味着你需要在这个边沿将下一位要发送的数据放到MOSI线上。所以,你的状态机应该在SCLK的上升沿(切换边沿)去更新MOSI的输出。这个“提前半个周期准备数据”的思想,是保证SPI时序稳定的关键。
3. 架构与实现:设计一个可配置的SPI主控制器状态机
理解了协议,我们就可以动手用Verilog来塑造它了。我们的目标是设计一个通用的SPI主控制器模块,它应该具备以下特性:
- 可配置的工作模式:通过参数或输入端口,支持Mode 0-3。
- 可配置的时钟分频:能够根据FPGA的系统时钟(比如50MHz)和所需的SPI时钟速率(比如25MHz)进行分频。
- 易于集成的用户接口:提供类似“启动传输”、“传输完成”、“待发送数据”、“接收到的数据”这样的握手信号,方便上层状态机调用。
- 处理任意字节长度:虽然SPI通常以8位(一个字节)为单位,但我们的设计应该能通过计数器轻松扩展到16位、24位等,以支持某些特殊外设的命令格式。
首先,我们定义模块的输入输出端口。除了标准的时钟和复位,核心是用户控制接口和SPI物理接口。
module spi_master #( parameter CLK_DIV = 2, // 系统时钟分频系数,SCLK = sys_clk / (2 * CLK_DIV) parameter CPOL = 0, // 时钟极性 parameter CPHA = 0 // 时钟相位 )( input wire sys_clk, // 系统时钟 (e.g., 50MHz) input wire rst_n, // 低电平复位 // 用户控制接口 input wire start, // 启动一次传输,高电平脉冲有效 input wire [7:0] data_tx, // 待发送的数据(8位) output reg [7:0] data_rx, // 接收到的数据(8位) output reg busy, // 忙标志,传输过程中为高 output reg done, // 传输完成标志,高电平脉冲 // SPI物理接口 output reg sclk, // SPI串行时钟 output reg mosi, // 主设备数据输出 input wire miso, // 主设备数据输入 output reg cs // 片选信号,低电平有效 );接下来是核心的状态机设计。一个典型的SPI字节传输过程可以分为以下几个状态:
- IDLE(空闲):等待
start信号。CS为高(无效),SCLK处于空闲电平(由CPOL决定)。 - START(启动):收到
start脉冲后,拉低CS(选中从设备),并根据CPHA准备第一个数据位。这里有个细节:如果CPHA=0,第一个数据位需要在第一个SCLK边沿之前就出现在MOSI上。因此,在START状态就需要将data_tx的最高位(MSB)输出到MOSI。 - DATA(数据传输):在这个状态中,我们通过一个计数器控制,循环8次,每次循环完成一位数据的发送和接收。根据CPOL和CPHA,在SCLK的适当边沿切换MOSI和采样MISO。
- STOP(停止):8位数据传输完毕,将CS拉高,结束本次通信。产生
done完成脉冲。
状态机的状态转移和输出逻辑,需要严格对照我们之前分析的时序表。下面以Mode 0 (CPOL=0, CPHA=0)为例,给出关键部分的代码逻辑和注释。
// 状态定义 localparam S_IDLE = 2'b00; localparam S_START = 2'b01; localparam S_DATA = 2'b10; localparam S_STOP = 2'b11; reg [1:0] state, next_state; reg [3:0] bit_cnt; // 位计数器,0-7 reg [7:0] tx_reg; // 发送移位寄存器 reg [7:0] rx_reg; // 接收移位寄存器 reg sclk_en; // SCLK使能信号,高电平时才翻转SCLK reg [7:0] clk_div_cnt; // 时钟分频计数器 // 时钟分频逻辑:用于产生SCLK always @(posedge sys_clk or negedge rst_n) begin if (!rst_n) begin clk_div_cnt <= 8'd0; sclk <= CPOL; // 复位时SCLK置于空闲电平 end else if (sclk_en) begin if (clk_div_cnt == CLK_DIV - 1) begin clk_div_cnt <= 8'd0; sclk <= ~sclk; // 达到分频计数,翻转SCLK end else begin clk_div_cnt <= clk_div_cnt + 1; end end else begin // sclk_en为低时,保持SCLK为空闲电平 sclk <= CPOL; clk_div_cnt <= 8'd0; end end // 主状态机 always @(posedge sys_clk or negedge rst_n) begin if (!rst_n) begin state <= S_IDLE; cs <= 1'b1; mosi <= 1'b1; // 通常MOSI空闲时置高 busy <= 1'b0; done <= 1'b0; bit_cnt <= 4'd0; tx_reg <= 8'h00; rx_reg <= 8'h00; data_rx <= 8'h00; sclk_en <= 1'b0; end else begin done <= 1'b0; // 默认done信号为0,只在完成时产生一个周期脉冲 case (state) S_IDLE: begin cs <= 1'b1; mosi <= 1'b1; busy <= 1'b0; sclk_en <= 1'b0; if (start) begin state <= S_START; busy <= 1'b1; tx_reg <= data_tx; // 锁存要发送的数据 rx_reg <= 8'h00; // 清空接收寄存器 end end S_START: begin cs <= 1'b0; // 拉低片选,选中设备 // Mode 0 (CPHA=0): 在第一个SCLK边沿之前就准备好数据 if (CPHA == 0) begin mosi <= tx_reg[7]; // 输出最高位(MSB) tx_reg <= {tx_reg[6:0], 1'b0}; // 左移,为下一位准备 end // 等待一段时间建立时间(Tcs),这里用状态停留实现 // 实际中可根据需要插入计数器延时 state <= S_DATA; sclk_en <= 1'b1; // 使能SCLK生成 bit_cnt <= 4'd0; end S_DATA: begin // 关键:根据CPHA决定在SCLK的哪个边沿采样和切换 // 以下针对 Mode 0 (CPOL=0, CPHA=0) 的写法 // 假设sclk在分频逻辑中生成,我们检测其边沿 if (sclk_en && clk_div_cnt == CLK_DIV - 1) begin // 当SCLK即将翻转时(即clk_div_cnt计满),根据当前SCLK电平判断边沿 // 对于CPOL=0,sclk从0->1是上升沿,从1->0是下降沿 if (sclk == 1'b0) begin // 当前是低电平,下一个时钟是上升沿 // 在上升沿(采样边沿)采样MISO rx_reg <= {rx_reg[6:0], miso}; end else begin // 当前是高电平,下一个时钟是下降沿 // 在下降沿(切换边沿)切换MOSI输出 if (bit_cnt < 4'd7) begin // 前7位 mosi <= tx_reg[7]; tx_reg <= {tx_reg[6:0], 1'b0}; bit_cnt <= bit_cnt + 1; end else begin // 第8位已经处理完 // 第8位数据在START状态或上一个下降沿已输出 // 接收第8位数据已经在上升沿完成 state <= S_STOP; sclk_en <= 1'b0; // 停止SCLK end end end end S_STOP: begin // 保持CS低电平一段时间(Thold),这里用状态停留 // 将接收到的数据输出 data_rx <= rx_reg; done <= 1'b1; // 产生完成脉冲 state <= S_IDLE; end endcase end end注意:上面的代码是一个高度简化的示例,重点在于展示状态机和时序控制的思想。一个健壮的工业级设计还需要考虑更多细节,比如:精确的建立/保持时间(
Tsu/Thd)通过计数器来保证、CS信号的建立和保持时间、支持CPHA=1的模式、以及更优雅的边沿检测方法(通常使用寄存器打拍来检测sclk的上升沿和下降沿)。在S_DATA状态中,通过检测clk_div_cnt来判断SCLK边沿的方法在低速下可行,但在高速或要求严格时序的情况下,建议使用独立的边沿检测逻辑。
4. 实战驱动SPI Flash:以W25Q128为例的完整流程
有了通用的SPI主控制器,我们就可以用它来驱动具体的设备了。我们选择SPI Flash W25Q128作为例子,因为它命令集丰富,包含了读、写、擦除等典型操作,非常适合用来检验我们的SPI控制器。驱动一个外设,本质上是按照其数据手册的时序和命令格式,通过SPI总线发出一系列特定的字节序列。
4.1 理解W25Q128的命令帧与数据帧
W25Q128的通信遵循一个基本格式:先拉低CS片选信号,然后主设备发送一个指令字节,对于某些指令(如读数据0x03),后面还需要跟上地址字节(24位,3个字节),然后才是数据的读写。传输完成后拉高CS。
例如,读取数据的命令时序如下:
- CS拉低。
- 发送指令
0x03。 - 发送24位地址(A23-A0),高位在前。
- 之后,从设备会从该地址开始,通过MISO线持续输出数据,每个SCLK周期输出一位,直到CS被拉高。
- CS拉高,结束读取。
这里有一个非常重要的细节:W25Q128支持标准SPI(单线)、Dual SPI和Quad SPI模式。我们这里只使用最基础的标准SPI模式,即只用MOSI和MISO两根数据线。在标准SPI下,所有指令、地址、数据的传输都是MSB(最高位)在前。
4.2 构建上层控制状态机
我们的SPI主控制器(spi_master)模块负责最底层的字节传输。我们需要一个上层的控制模块(比如叫flash_controller)来组织这些字节,形成完整的命令帧。这个模块也是一个状态机,它负责:
- 在收到“读Flash”的请求时,依次将指令
0x03、地址字节1、地址字节2、地址字节3通过spi_master发送出去。 - 然后,连续循环调用
spi_master发送哑元数据(如0x00),同时从spi_master接收返回的数据,即为Flash中读出的数据。 - 管理CS信号。注意,对于W25Q128,一次读命令从CS拉低开始,到读完所需数据后再拉高CS,中间CS不能拉高,否则命令会终止。
下面是一个简化的flash_controller读取逻辑的状态描述:
- IDLE: 等待读请求。
- SEND_CMD: 启动
spi_master,发送指令0x03。等待spi_master的done信号。 - SEND_ADDR_H:
spi_master完成后,启动下一次传输,发送地址高字节。 - SEND_ADDR_M: 发送地址中字节。
- SEND_ADDR_L: 发送地址低字节。
- READ_DATA: 循环发送哑元数据
0x00,并读取spi_master返回的数据,存入缓冲区。每读完一个字节,地址加1,直到读完指定长度。 - FINISH: 拉高CS(通过控制
spi_master的start信号停止,或直接控制CS),产生读完成信号。
// flash_controller 模块的部分关键逻辑示例 reg [2:0] state; reg [23:0] rd_addr; // 读地址 reg [7:0] rd_len; // 要读取的字节数 reg [7:0] data_buffer [0:255]; // 数据缓冲区 reg [7:0] buffer_idx; always @(posedge sys_clk or negedge rst_n) begin if(!rst_n) begin state <= IDLE; spi_start <= 1'b0; spi_data_tx <= 8'h00; end else begin case(state) IDLE: if(rd_req) begin rd_addr <= rd_addr_in; rd_len <= rd_len_in; state <= SEND_CMD; end SEND_CMD: begin spi_data_tx <= 8'h03; // 读指令 spi_start <= 1'b1; if(spi_done) begin spi_start <= 1'b0; state <= SEND_ADDR_H; end end SEND_ADDR_H: begin spi_data_tx <= rd_addr[23:16]; spi_start <= 1'b1; if(spi_done) begin spi_start <= 1'b0; state <= SEND_ADDR_M; end end // ... 发送中地址和低地址状态类似 READ_DATA: begin spi_data_tx <= 8'h00; // 发送哑元数据以产生SCLK,读取MISO spi_start <= 1'b1; if(spi_done) begin spi_start <= 1'b0; data_buffer[buffer_idx] <= spi_data_rx; // 保存数据 buffer_idx <= buffer_idx + 1; rd_addr <= rd_addr + 1; // 地址递增,为连续读做准备 if(buffer_idx == rd_len - 1) begin state <= FINISH; end end end FINISH: begin // 产生完成中断或信号 state <= IDLE; end endcase end end4.3 上板调试与逻辑分析仪抓取波形
代码写好了,仿真也通过了,但上板后可能就是不工作。这时候,逻辑分析仪(Logic Analyzer)是你的救命稻草。我强烈建议每个FPGA开发者都学会使用它(像Saleae Logic这类USB逻辑分析仪就很好用)。
将逻辑分析仪的探头连接到FPGA板上的SCLK、MOSI、MISO、CS信号线。设置触发条件为CS下降沿。然后启动你的FPGA程序发起一次读操作。抓取到的波形,是你排查问题的黄金依据。
你需要对照W25Q128的数据手册,逐一检查:
- 时序模式:SCLK空闲电平对吗?数据是在正确的边沿采样和切换的吗?(对应CPOL/CPHA)
- 命令序列:CS拉低后,发送的第一个字节是不是
0x03?后面跟的三个地址字节是否正确?MSB在前吗? - 时序参数:CS拉低到第一个SCLK边沿的时间(
Tcss)够吗?数据建立时间(Tsu)和保持时间(Thd)满足要求吗?这些时间在数据手册里都有明确的最小值要求。 - 从设备响应:在发送完地址后的SCLK周期里,MISO线上是否有数据输出?数据是否稳定?
我遇到过最常见的问题就是模式不对。代码里默认用了Mode 0,但有些板子或Flash可能被配置成了其他模式。还有一次是地址字节序错了,我误发了LSB在前。通过逻辑分析仪,你能一眼看到实际发出的指令序列,从而快速定位是控制器代码问题,还是外设本身或硬件连接问题。
5. 性能优化与高级话题:从基础SPI到效率提升
当你成功驱动了SPI Flash,实现了基本的读写后,可以思考如何优化和扩展。这能让你对SPI的理解更深一层。
5.1 时钟分频与速度权衡
我们的spi_master模块有一个CLK_DIV参数。SPI的时钟频率(SCLK)并非越快越好。你需要考虑两个限制:
- 从设备支持的最高SCLK频率:W25Q128在标准SPI模式下最高支持104MHz。你的控制器必须能产生低于或等于这个频率的时钟。
- FPGA到从设备的PCB走线长度:频率越高,信号完整性越重要。过长的走线可能导致边沿畸变,产生通信错误。通常,在面包板或飞线环境下,建议先从低频(如1MHz以下)开始测试,稳定后再逐步提高。
在FPGA内部,更高的SCLK意味着你的状态机和控制逻辑需要运行在更高的系统时钟下,以确保能精确控制SCLK的边沿。例如,要产生50MHz的SCLK,你的系统时钟sys_clk可能需要达到100MHz或更高,才能进行可靠的边沿检测和信号生成。
5.2 引入FIFO缓冲与DMA思想
目前我们的设计是“请求-响应”式的:上层要一个字节,SPI控制器传一个字节。当需要连续读取大量数据(比如读取整个Flash扇区)时,这种频繁的握手会消耗大量FPGA逻辑资源和时间。
一个常见的优化是引入FIFO(First In First Out)缓冲区。
- 在发送端,上层模块可以提前将需要发送的多个字节(如一条完整的读命令+地址)写入发送FIFO。SPI控制器状态机则从发送FIFO中依次取出字节发送,无需等待上层干预。
- 在接收端,SPI控制器将收到的字节直接写入接收FIFO。上层模块可以从接收FIFO中连续读取数据。
这相当于为SPI控制器配备了一个“流水线”,大大提高了连续传输的效率和吞吐量。如果再进一步,配合一个简单的DMA(直接内存访问)控制器,让DMA自动从FPGA内部的Block RAM或外部存储器中取出要发送的数据填入发送FIFO,并将接收FIFO的数据搬移到目标存储器,那么FPGA的软核处理器(如NIOS II)甚至可以不参与具体的数据搬运过程,极大解放了CPU。
5.3 应对更复杂的SPI变种:Dual/Quad SPI与QPI
W25Q128等现代SPI Flash为了提升速度,支持了Dual SPI和Quad SPI。在Dual SPI模式下,MOSI和MISO线在指令和地址阶段后,可以同时用于数据传输(变成IO0和IO1),每个时钟周期传输2位数据。在Quad SPI模式下,四根数据线(IO0-IO3)全部用于数据传输,每个时钟周期传输4位数据。
要实现这些模式,你的SPI控制器就需要升级为支持可配置数据线宽度的控制器。在发送了特定的“使能Quad IO”命令后,后续的数据传输阶段,控制器需要同时驱动或采样2根或4根数据线。这涉及到更复杂的IO管理和时序控制,但对提升Flash的读取性能(尤其是执行代码的XIP功能)是质的飞跃。QPI(Quad Peripheral Interface)模式则更进一步,连指令和地址也通过4线传输。
从基础的单线SPI到Quad SPI,这个演进路径清晰地展示了如何通过增加数据线宽度来突破串行接口的带宽瓶颈。理解了这个,你也就理解了为什么高速存储器接口(如DDR)会是并行总线。
6. 避坑指南与调试心得:那些数据手册里不会写的细节
最后,分享几个我在调试SPI,特别是Flash这类设备时踩过的坑和总结的经验。这些在官方数据手册里往往一笔带过,但却是项目成败的关键。
坑一:Flash的写使能与写保护W25Q128上电后默认是写保护状态。在执行任何写操作(如页编程0x02、扇区擦除0x20)之前,必须先发送“写使能”指令(0x06)。而且,这个使能状态不是永久的,在一次写操作完成后,或者断电后,状态会自动回到写禁止。所以你的写操作流程必须是:0x06(写使能) -> 等待一小段时间(Twen) ->0x02(页编程) -> 等待编程完成(检查忙状态位或延时Tpp)。忘记发0x06是最常见的“写了半天没反应”的原因。
坑二:忙状态查询Flash在执行写或擦除操作时,需要一定时间(毫秒级),这段时间内它不响应任何指令。你有两种方法处理:
- 硬延时:发送写/擦除命令后,简单延时足够长的时间(如扇区擦除最多400ms)。简单粗暴,但效率低,会阻塞整个系统。
- 查询状态寄存器:发送“读状态寄存器1”指令(
0x05),检查返回值的第0位(BUSY位)。为1表示忙,为0表示空闲。这是推荐的做法。你需要在上层状态机中实现一个轮询循环,不断发送0x05并检查,直到BUSY位为0再继续后续操作。
坑三:地址对齐与跨页写Flash的页编程操作(0x02)一次最多写入256字节。但有一个重要限制:写入的起始地址和数据的结合,不能跨越页边界。一页的起始地址是256字节对齐的(如0x000000,0x000100)。如果你想从地址0x0000F0开始写入20个字节,这是允许的。但如果你想从0x0000F0开始写入30个字节,那么后10个字节就会“跨页”到0x000100开始的页,这是不允许的,会导致写入错误。安全的做法是,在写之前,由软件或硬件逻辑检查地址和长度,如果跨页,则拆分成两次页编程操作。
调试心得:善用仿真与虚拟模型在接触一块新的SPI芯片时,不要急于上板。先在仿真环境里搭建测试平台(Testbench)。你可以用Verilog编写一个简单的SPI从设备行为模型(Behavioral Model),这个模型不需要实现芯片的所有功能,只需要能正确响应你当前测试的指令即可(比如,对读指令0x03,返回一个预定义的数据序列)。用这个模型来验证你的SPI控制器状态机和命令序列是否正确,比上板调试高效得多。很多FPGA开发环境(如Vivado、Quartus)也自带一些IP核的仿真模型,可以加以利用。
实现一个SPI控制器,就像在FPGA内部搭建了一座与外部世界沟通的精确桥梁。从理解协议的四根线和四种模式,到用状态机将时序图转化为Verilog代码,再到驱动真实外设时处理各种边界情况和异常状态,每一步都是对数字逻辑设计能力的扎实锻炼。当你第一次通过自己写的代码,从Flash中正确读出制造商ID(0xEF,0x40)时,那种喜悦和信心是无可替代的。更重要的是,掌握了SPI的设计思路,你再去看I2C、UART甚至更复杂的协议,会发现它们核心的思想是相通的:定义好物理层、数据链路层的规则,然后用状态机忠实地实现它。希望这篇笔记,能帮你稳稳地跨过FPGA与外部器件通信的第一道门槛。