四线 SPI Flash 是一种常见的外设存储器接口,广泛应用于 FPGA 项目中。本文将基于 Verilog 语言,介绍如何在 Altera 或 Xilinx FPGA 平台上实现一个简易的四线 SPI Flash 读写模块,包括读取和写入操作。
一、总体思路
四线 SPI(Serial Peripheral Interface)是一种同步串行通信接口,通常由以下四条线组成:
- SCLK:时钟信号,由主设备(FPGA)提供。
- MOSI:主输出从输入,用于发送数据。
- MISO:主输入从输出,用于接收数据。
- CS#:片选信号,用于选择目标设备。
对于 FPGA 实现的四线 SPI Flash 读写模块,需要完成以下任务:
- 生成 SCLK 信号,并控制其频率。
- 根据操作类型(读或写)生成 MOSI 信号。
- 通过 MISO 信号接收 Flash 返回的数据。
- 控制 CS# 信号,以选择或取消选择 Flash 设备。
二、设计分析
1. 状态机设计
状态机是控制 SPI 通信流程的核心模块。设计一个简单但可靠的四态状态机:
- 空闲态:等待开始信号。
- 发送头态:发送命令字节(如读命令或写命令)。
- 数据传输态:发送或接收数据。
- 完成态:等待新的操作。
状态机的转移逻辑需要考虑每个状态的持续时间,以及如何处理可能的中断或错误。
2. 时序匹配
在 SPI 通信中,时钟信号 SCLK 的时序匹配非常重要。FPGA 的时钟信号通常较高频(如 100MHz),但我们可能需要 SCLK 的频率低于某一阈值(如 20MHz),以确保与 Flash 的兼容性。
为此,可以在 FPGA 内部使用分频器模块对系统时钟进行分频处理,得到所需的 SCLK 信号。
3. 数据传输
在数据传输阶段,需要实现一个移位寄存器,用于逐位发送 MOSI 信号或逐位接收 MISO 信号。移位寄存器的位宽通常为 8 位(适用于标准 SPI Flash)。
三、Verilog 代码实现
以下是一个基于 Verilog 的四线 SPI Flash 控制器的设计代码。代码分为几个模块,方便调试和复用。
1. 时钟分频器模块
module clk_divider( input wire sys_clk, input wire rst_n, output wire sclk ); parameter DIV = 20'd50000000; // 100MHz 系统时钟,分频参数需根据实际需求调整 integer cnt; always @(posedge sys_clk or negedge rst_n) begin if (!rst_n) begin cnt <= 0; sclk <= 0; end else begin if (cnt == DIV - 1) begin cnt <= 0; sclk <= !sclk; end else begin cnt <= cnt + 1; end end end endmodule
2. 状态机模块
module spi_state_machine( input wire sclk, input wire rst_n, input wire start, input wire write, input wire miso, output wire busy, output wire cs_n, output wire mosi, output wire done, output wire data_out ); reg [7:0] data_out_reg; reg [2:0] state; reg sclk_edge; // 状态定义 localparam IDLE = 3'd0; localparam SEND_HEADER = 3'd1; localparam DATA_XFER = 3'd2; localparam DONE = 3'd3; always @(posedge sclk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; busy <= 1'b0; cs_n <= 1'b1; mosi <= 1'b0; sclk_edge <= 1'b0; data_out_reg <= 8'd0; end else begin case (state) IDLE: begin if (start) begin state <= SEND_HEADER; cs_n <= 1'b0; mosi <= (write) ? 1'b1 : 1'b0; busy <= 1'b1; end end SEND_HEADER: begin if (sclk_edge) begin state <= DATA_XFER; end end DATA_XFER: begin if (sclk_edge) begin if (!write) begin data_out_reg <= {miso, data_out_reg[7:1]}; end if (data_out_reg[0] == 1'b1) begin state <= DONE; end end end DONE: begin state <= IDLE; busy <= 1'b0; cs_n <= 1'b1; end default: begin state <= IDLE; end endcase end end assign data_out = data_out_reg; assign done = (state == DONE) ? 1'b1 : 1'b0; endmodule


