FPGA实战:从零构建SPI驱动W25Q64 Flash的完整状态机方案
当第一次尝试用FPGA控制W25Q64 Flash时,我对着数据手册里密密麻麻的时序图发呆了半小时。SPI协议看似简单,但要把文档中的波形图转化为可工作的Verilog代码,就像在迷宫里寻找出口——特别是当状态机设计不合理时,调试过程简直是一场噩梦。本文将分享一套经过实际项目验证的解决方案,包含可直接复用的状态机架构和关键代码片段。
1. 理解W25Q64 Flash的操作特性
W25Q64JV是Winbond推出的64Mbit串行Flash存储器,采用标准的SPI接口。与EEPROM不同,Flash存储器的操作有其独特之处:
- 页编程限制:最小写入单位为页(256字节),且必须先擦除后写入
- 擦除粒度:支持扇区(4KB)、块(32KB/64KB)和整片擦除
- 状态寄存器:BUSY和WEL标志位决定操作可行性
// 常用指令定义 localparam CMD_WRITE_ENABLE = 8'h06, CMD_PAGE_PROGRAM = 8'h02, CMD_SECTOR_ERASE = 8'h20, CMD_READ_DATA = 8'h03, CMD_READ_STATUS = 8'h05;关键点:Flash只能将bit从1改为0,不能单独将0改回1。这就是为什么写入前必须执行擦除操作——擦除会将整个区域恢复为全1状态。
2. SPI模式选择与时序实现
W25Q64支持SPI模式0和模式3,两种模式的主要区别在于时钟极性:
| 特性 | 模式0 (CPOL=0, CPHA=0) | 模式3 (CPOL=1, CPHA=1) |
|---|---|---|
| 时钟空闲电平 | 低 | 高 |
| 数据采样边沿 | 上升沿 | 下降沿 |
| 数据变化边沿 | 下降沿 | 上升沿 |
// SPI时钟生成逻辑(模式0) always @(posedge i_clk or posedge i_rst) begin if(i_rst) begin r_spi_clk <= 1'b0; r_spi_cnt <= 0; end else if(i_spi_active) begin r_spi_clk <= ~r_spi_clk; // 时钟翻转 r_spi_cnt <= (r_spi_clk) ? r_spi_cnt + 1 : r_spi_cnt; end else begin r_spi_clk <= 1'b0; // 空闲状态保持低电平 end end注意:实际项目中建议在模块参数中设计可配置的CPOL和CPHA,以增强代码复用性。本文示例为简化起见采用固定模式0。
3. 状态机设计与实现
合理的状态机设计是SPI驱动稳定的核心。我们采用11状态的设计方案,覆盖所有Flash操作场景:
3.1 状态定义与跳转逻辑
localparam ST_IDLE = 0, ST_WR_EN = 1, ST_ERASE = 2, ST_PAGE_PROG = 3, ST_READ = 4, ST_WAIT_BUSY = 5; // 状态转移示例 always @(*) begin case(r_current_state) ST_IDLE: if(i_start_erase) next_state = ST_WR_EN; else if(i_start_write) next_state = ST_WR_EN; else if(i_start_read) next_state = ST_READ; ST_WR_EN: if(spi_done) next_state = (pending_op == OP_ERASE) ? ST_ERASE : ST_PAGE_PROG; ST_ERASE: if(spi_done) next_state = ST_WAIT_BUSY; // 其他状态转移... endcase end3.2 关键状态处理技巧
写使能(WRITE_ENABLE)处理:
- 必须在每个写操作(编程/擦除)前执行
- 执行后WEL位自动置1
- 完成操作后自动清零
忙状态检测:
// 忙状态检查状态机片段 ST_WAIT_BUSY: begin o_spi_start <= 1'b1; o_spi_data <= {CMD_READ_STATUS, 8'h00}; if(spi_done && !i_spi_rdata[0]) // BUSY位为0 next_state = ST_IDLE; else if(spi_done) next_state = ST_WAIT_BUSY; // 继续等待 end4. 完整驱动架构设计
推荐的三层模块化设计:
顶层接口层(Flash_drive)
- 提供用户友好的并行接口
- 处理数据缓冲和流控制
控制逻辑层(Flash_ctrl)
- 实现核心状态机
- 生成SPI操作序列
- 管理忙等待和错误处理
SPI物理层(spi_drive)
- 实现精确的SPI时序
- 处理时钟相位和采样
- 支持可配置的CPOL/CPHA
// 典型用户接口时序示例 initial begin // 擦除扇区 flash_erase(24'h001000); // 写入数据 flash_write(24'h001000, write_data); // 读取验证 flash_read(24'h001000, read_data); end5. 调试技巧与常见问题
典型问题1:写操作失败
- 检查写使能指令是否成功执行(可通过读状态寄存器验证WEL位)
- 确认在页编程前已执行擦除操作
- 检查地址是否对齐到页边界(256字节)
典型问题2:读取数据异常
- 确认CS信号在传输期间保持低电平
- 检查SPI时钟相位是否符合Flash要求
- 验证读取指令后的地址传输顺序(MSB先发)
示波器调试建议:
- 先捕获SPI时钟和CS信号,确认基本时序正确
- 对照数据手册检查命令序列(特别是第一个字节)
- 注意MOSI/MISO的建立保持时间(通常需要>5ns)
6. 性能优化策略
对于高速应用场景,可以考虑以下优化:
- 双缓冲设计:在写入当前页时准备下一页数据
- 批量操作:合并连续地址的擦除/编程操作
- QSPI模式:W25Q64支持四线模式,可提升4倍带宽
// 双缓冲示例 reg [7:0] buffer[0:255]; reg [7:0] shadow_buffer[0:255]; reg buffer_sel; // 写入流程 always @(posedge i_clk) begin if(i_write_strobe) begin if(!buffer_sel) buffer[i_write_addr] <= i_write_data; else shadow_buffer[i_write_addr] <= i_write_data; end if(page_write_done) begin buffer_sel <= ~buffer_sel; start_write <= 1'b1; end end在完成第一个状态机版本后,建议添加以下增强功能:
- 写保护检测机制
- 坏块管理支持
- 自动重试和错误计数
实际项目中,我发现最耗时的不是代码编写而是调试——特别是当状态机跳转条件考虑不周全时。建议在仿真阶段构建完整的测试序列,覆盖所有异常分支。