Verilog 实现 FPGA 四线 SPI Flash 读写设计
四线 SPI Flash 是 FPGA 项目中常见的外设存储器接口。本文将基于 Verilog 语言,介绍如何在 Altera 或 Xilinx FPGA 平台上实现一个简易的四线 SPI Flash 读写模块,涵盖读取和写入操作的核心逻辑。
总体思路
四线 SPI(Serial Peripheral Interface)是一种同步串行通信接口,主要由四条信号线组成:
- SCLK:时钟信号,由主设备(FPGA)提供。
- MOSI:主输出从输入,用于发送数据。
- MISO:主输入从输出,用于接收数据。
- CS#:片选信号,低电平有效,用于选择目标设备。
在 FPGA 中实现该模块,核心任务是生成 SCLK 信号并控制频率,根据读/写操作生成 MOSI 数据流,通过 MISO 捕获 Flash 返回的数据,并管理 CS# 的时序以完成一次完整的通信事务。
设计分析
状态机设计
状态机是控制 SPI 通信流程的关键。这里采用一个四态状态机来保证流程清晰可靠:
- 空闲态 (IDLE):等待启动信号,保持 CS# 为高。
- 发送头态 (SEND_HEADER):拉低 CS#,发送命令字节(如读命令或写命令)。
- 数据传输态 (DATA_XFER):逐位发送地址和数据,同时移位接收 MISO 数据。
- 完成态 (DONE):拉高 CS#,结束本次操作,返回空闲态。
状态跳转需严格匹配时钟沿,避免误触发。
时序匹配
FPGA 系统时钟通常较高(如 100MHz),而 SPI Flash 的工作频率往往受限(例如 20MHz 以下)。因此,必须使用分频器将系统时钟降频至合适的 SCLK 频率,确保满足 Flash 的时序要求。
数据传输
在数据传输阶段,利用移位寄存器逐位处理 MOSI 发送和 MISO 接收。标准 SPI Flash 通常以 8 位为单位传输,因此移位寄存器位宽设为 8 位即可。
Verilog 代码实现
代码分为三个模块:时钟分频、状态机控制和顶层集成。这样拆分便于调试和复用。
1. 时钟分频器模块
module clk_divider(
input wire sys_clk,
input wire rst_n,
output wire sclk
);
parameter DIV = 20'd50000000; // 100MHz 系统时钟,分频后约 1Hz
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:
if (start) begin
state <= SEND_HEADER;
cs_n <= 1'b0;
mosi <= write ? 1'b1 : 1'b0;
busy <= 1'b1;
end
SEND_HEADER:
if (sclk_edge) begin
state <= DATA_XFER;
end
DATA_XFER:
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
DONE:
begin
state <= IDLE;
busy <= 1'b0;
cs_n <= 1'b1;
end
default:
state <= IDLE;
endcase
end
end
assign data_out = data_out_reg;
assign done = (state == DONE) ? 1'b1 : 1'b0;
endmodule


