FPGA驱动SPI Flash的模块化设计与实现:从全擦除到连续写入的实战解析

张开发
2026/4/16 17:01:07 15 分钟阅读

分享文章

FPGA驱动SPI Flash的模块化设计与实现:从全擦除到连续写入的实战解析
1. SPI Flash驱动开发的核心挑战第一次接触FPGA驱动SPI Flash时我对着开发板愣了半天——明明时序图都看懂了代码也照着手册写了可Flash就是不理我。后来才发现这种看似简单的四线接口藏着不少坑。SPI Flash作为嵌入式系统中最常用的存储方案其驱动开发需要同时考虑硬件时序、协议规范和实际应用场景。常见的Winbond、Micron等品牌的SPI Flash芯片虽然都遵循标准SPI协议但在细节上各有差异。比如全擦除Chip Erase指令有的型号是0x60有的是0xC7。更麻烦的是时序参数页编程Page Program后的写入等待时间tBP在不同容量芯片上可能从3ms到10ms不等。我在野火征途Pro开发板上实测时就因为没注意这个参数导致连续写入失败。2. 模块化设计架构解析2.1 状态机核心设计在Verilog中实现SPI驱动最核心的就是状态机设计。以全擦除功能为例完整流程需要经历写使能WREN→ 全擦除指令 → 等待擦除完成。我的实现方案是这样的parameter IDLE 3b000; parameter WREN 3b001; parameter ERASE 3b010; parameter WAIT 3b011; always (posedge clk or negedge rst_n) begin if(!rst_n) begin state IDLE; end else begin case(state) IDLE: if(start_erase) state WREN; WREN: if(wren_done) state ERASE; ERASE: if(erase_cmd_sent) state WAIT; WAIT: if(erase_done) state IDLE; endcase end end每个状态都要精确控制CS片选信号的拉低和拉高时机。实测发现CS信号在指令发送完毕后必须保持拉低至少50ns否则某些Flash芯片会丢弃最后几位数据。2.2 时钟分频策略SPI时钟频率需要根据Flash规格选择。对于支持104MHz的Flash在FPGA上我通常这样分频reg [7:0] clk_div; reg spi_clk; always (posedge clk) begin if(clk_div DIVIDER - 1) begin clk_div 0; spi_clk ~spi_clk; // 50%占空比 end else begin clk_div clk_div 1; end end但在实际项目中当FPGA主频达到100MHz时直接使用系统时钟会产生毛刺。我的解决方案是插入BUFG时钟缓冲器并用ODDR2原语输出时钟信号BUFG spi_clk_bufg (.I(spi_clk_gen), .O(spi_clk_buf)); ODDR2 #( .DDR_ALIGNMENT(NONE), .INIT(1b0), .SRTYPE(SYNC) ) ODDR2_inst ( .Q(SPI_SCK), .C0(spi_clk_buf), .C1(~spi_clk_buf), .CE(1b1), .D0(1b1), .D1(1b0), .R(1b0), .S(1b0) );3. 关键功能实现细节3.1 全擦除功能优化全擦除Chip Erase会清空整个Flash芯片典型耗时3-10秒。在早期版本中我采用简单轮询方式while(1) begin read_status_reg(status); if(!(status 0x01)) break; // 等待BUSY位清零 delay_ms(100); }这种方案会阻塞整个系统。改进后的方案使用状态机超时检测parameter TIMEOUT 32d100_000_000; // 10s 100MHz always (posedge clk) begin if(state WAIT_ERASE) begin timeout_cnt timeout_cnt 1; if(timeout_cnt TIMEOUT) begin erase_timeout 1b1; state IDLE; end end else begin timeout_cnt 0; end end3.2 连续写入性能提升标准SPI Flash页编程Page Program每次最多写入256字节。要实现连续写入需要处理页边界对齐问题。我的解决方案是计算当前地址所在页的剩余空间如果剩余空间不足先写满当前页自动切换到下一页继续写入关键代码实现function [7:0] calc_remaining; input [15:0] addr; begin calc_remaining 8hFF - addr[7:0]; end endfunction always (*) begin remaining calc_remaining(current_addr); if(wr_len remaining) begin chunk_size remaining; need_wrap 1b1; end else begin chunk_size wr_len; need_wrap 1b0; end end实测在Winbond W25Q128JV上通过预取地址和流水线操作连续写入速度可以从标准的1.5MB/s提升到3.2MB/s。4. 调试技巧与实战经验4.1 信号完整性排查在调试SPI通信时最头疼的就是信号质量问题。有一次遇到数据错位问题最终发现是PCB走线过长导致的。现在我的调试流程是先用示波器检查SCK时钟质量确保上升/下降时间符合规格测量CS到SCK的建立/保持时间tSU/ tH检查MOSI/MISO数据线与时钟的相位关系建议在FPGA端加入可调延迟模块方便补偿时序// 可配置IO延迟模块 IDELAYE2 #( .IDELAY_TYPE(FIXED), .DELAY_SRC(IDATAIN), .IDELAY_VALUE(10) ) delay_miso ( .IDATAIN(MISO_pin), .DATAOUT(MISO_delayed), ... );4.2 跨平台兼容性处理不同厂商的SPI Flash存在细微差异我的驱动中会通过JEDEC ID自动适配case(jedec_id[15:0]) 16h4018: begin // Winbond W25Q128 PAGE_SIZE 256; SECTOR_SIZE 4096; DUAL_READ 1; end 16hBA20: begin // Micron N25Q128 PAGE_SIZE 256; SECTOR_SIZE 65536; // 64KB sectors QUAD_READ 1; end endcase对于特殊指令如Micron的4KB扇区擦除会动态调整指令集if(FLASH_TYPE MICRON) erase_cmd 8h20; // 4KB sector erase else erase_cmd 8hD8; // 通常的32KB/64KB擦除在野火征途Pro开发板上验证时建议先用逻辑分析仪抓取原始SPI波形再对照芯片手册逐条指令检查。遇到问题时可以尝试降低时钟频率到1MHz以下进行基础功能验证待基本读写正常后再逐步提高频率。

更多文章