FPGA驱动SPI Flash的读写时序与Verilog实现
1. SPI Flash基础与FPGA控制原理
SPI Flash是一种采用串行外设接口(SPI)的闪存芯片,在嵌入式系统中广泛用于存储固件、配置参数等数据。与并行Flash相比,SPI Flash引脚数量少(通常4-6线)、封装尺寸小,特别适合空间受限的应用场景。FPGA作为可编程逻辑器件,通过硬件描述语言(Verilog/VHDL)可以灵活实现SPI控制器,直接操作Flash存储器。
以常见的M25P16为例,这款16Mbit容量的SPI Flash支持最高50MHz时钟频率。实际项目中我发现,要稳定驱动这类芯片,关键在于两点:一是正确配置SPI通信模式,二是准确理解Flash的操作指令集。这就像与人交流,既要说对方听得懂的语言(SPI时序),又要用正确的词汇表达意图(操作指令)。
2. SPI模式选择与时序设计
2.1 SPI工作模式解析
SPI有四种工作模式,由CPOL(时钟极性)和CPHA(时钟相位)两个参数决定:
- CPOL=0:时钟空闲时为低电平
- CPOL=1:时钟空闲时为高电平
- CPHA=0:数据在时钟第一个边沿采样
- CPHA=1:数据在时钟第二个边沿采样
M25P16支持模式0(CPOL=0,CPHA=0)和模式3(CPOL=1,CPHA=1)。实测中模式3稳定性更好,其特点是:
- 时钟空闲时为高电平
- 数据在下降沿输出(FPGA发送)
- 数据在上升沿采样(FPGA接收)
2.2 Verilog时序实现技巧
下面是用Verilog实现SPI主机的核心代码段:
// SPI模式3:CPOL=1, CPHA=1 always @(posedge sys_clk) begin if(state == SPI_DATA) begin // 时钟信号翻转 spi_clk_reg <= ~spi_clk_reg; // 下降沿发送数据 if(spi_clk_reg == 1'b1) spi_mosi_reg <= write_data_reg[7]; // 上升沿接收数据 if(spi_clk_reg == 1'b0) read_data_reg <= {read_data_reg[6:0], spi_miso}; end end特别要注意的是第一个数据位的处理。在模式3下,由于第一个下降沿前没有时钟边沿,需要在初始状态就设置好MOSI电平。我曾在项目中因为这个细节导致首字节数据错误,调试了整整一天才发现问题。
3. Flash指令集详解与操作流程
3.1 关键指令解析
M25P16的指令集包含约20条命令,但实际常用的是以下几个:
- WREN (06h):写使能。任何写入或擦除操作前必须执行,相当于"解锁"开关。
- READ (03h):读取数据。发送24位地址后可连续读取,地址自动递增。
- PP (02h):页编程。每次写入最多256字节,需先擦除对应区域。
- SE (D8h):扇区擦除。以64KB为单位擦除,耗时约0.5-1秒。
- RDID (9Fh):读取厂商ID。常用于硬件调试和SPI通路验证。
3.2 典型操作时序
读取数据流程:
- 拉低CS片选信号
- 发送READ指令(03h)
- 发送24位地址(3字节)
- 连续读取数据
- 拉高CS结束传输
写入数据流程:
- 发送WREN指令
- 发送PP指令(02h)
- 发送24位地址
- 发送1-256字节数据
- 等待TPP时间(典型值0.7ms)
这里有个易错点:PP指令只能将bit从1改为0,若要写入新数据,必须先执行擦除操作将目标区域恢复为全1状态。我在早期项目中就犯过直接写入未擦除区域的错误,导致数据异常。
4. Verilog实现完整架构
4.1 模块划分建议
完整的Flash控制器建议分为两个模块:
- SPI主机模块:处理底层时序,提供字节级读写接口
- Flash控制模块:解析高层指令,管理操作流程
module flash_control( input sys_clk, input sys_rstn, // 用户接口 input read_req, input [23:0] read_addr, output [7:0] read_data, // SPI接口 output spi_clk, output spi_mosi, input spi_miso ); // 状态机定义 localparam IDLE = 3'd0; localparam CMD = 3'd1; localparam ADDR = 3'd2; localparam DATA = 3'd3; reg [2:0] state; reg [7:0] cmd_reg; reg [23:0] addr_reg;4.2 状态机设计要点
Flash操作本质上是状态机驱动的过程。以页编程为例:
- IDLE状态:等待用户请求
- WREN状态:发送写使能指令
- PP_CMD状态:发送页编程指令
- ADDR状态:发送目标地址
- DATA状态:传输数据字节
- WAIT状态:等待编程完成
实际项目中,建议为每个重要操作添加超时检测。我曾遇到Flash芯片异常导致永久卡死的情况,后来加入超时机制后系统稳定性大幅提升。
5. 工程实践中的经验分享
5.1 时序约束关键点
在FPGA工程中,必须添加正确的时序约束:
# SPI时钟约束(假设系统时钟50MHz) create_clock -period 20 [get_ports sys_clk] set_output_delay -clock [get_clocks sys_clk] -max 2 [get_ports {spi_mosi spi_clk}]特别要注意跨时钟域信号的处理。如果SPI时钟分频自系统时钟,需要使用适当的同步器处理控制信号。
5.2 常见问题排查
读取全FF或00:
- 检查CS信号是否正常
- 确认SPI模式设置正确
- 测量电源电压是否稳定
写入失败:
- 是否先执行了WREN
- 目标区域是否已擦除
- 等待时间是否足够(TPP/TSE)
间歇性错误:
- 检查PCB布线(SPI走线要短且等长)
- 添加去耦电容(0.1uF靠近VCC引脚)
- 降低时钟频率测试
记得第一次调试时,我遇到随机数据错误,最后发现是PCB上SPI走线过长(>10cm)导致信号完整性问题。将Flash芯片移近FPGA后问题立即解决。
6. 性能优化技巧
6.1 双缓冲读取技术
对于大数据量读取,可采用双缓冲机制:
// 双缓冲实现 reg [7:0] buffer0[0:255]; reg [7:0] buffer1[0:255]; reg buffer_sel; always @(posedge sys_clk) begin if(read_ack) begin if(!buffer_sel) buffer0[addr_lsb] <= read_data; else buffer1[addr_lsb] <= read_data; if(addr_lsb == 8'hFF) buffer_sel <= ~buffer_sel; end end这种方法允许在读取一个缓冲区的数据时,后台同时填充另一个缓冲区,显著提高吞吐量。
6.2 批量擦除策略
Flash擦除耗时较长(扇区擦除约0.5s,整片擦除可达10s),建议:
- 上电时检查是否需要全局擦除
- 运行时采用"脏块标记"管理
- 空闲时执行后台擦除操作
在某个数据采集项目中,我通过预擦除空闲扇区的方法,将关键写入延迟从毫秒级降低到微秒级,满足了实时性要求。
