从零开始用Verilog在EP4CE6 FPGA上驱动M25P16 Flash存储芯片1. 硬件准备与环境搭建拿到一块Cyclone IV EP4CE6开发板和M25P16 Flash芯片时首先要确保硬件连接正确。这款FPGA开发板通常采用3.3V逻辑电平与M25P16完全兼容。建议使用杜邦线将Flash的SPI接口与FPGA的GPIO引脚相连M25P16引脚CS# (片选) → FPGA GPIODO (数据输出) → FPGA GPIODI (数据输入) → FPGA GPIOCLK (时钟) → FPGA GPIOVCC → 3.3VGND → 共地在Quartus II中新建工程时选择正确的器件型号EP4CE6F17C8。时钟方面虽然M25P16最高支持50MHz但为简化设计我们将FPGA的50MHz时钟四分频至12.5MHz使用。// 时钟分频模块示例 module clk_div( input clk_50m, output reg clk_12_5m 0 ); reg [1:0] counter 0; always (posedge clk_50m) begin counter counter 1; if(counter 3) begin clk_12_5m ~clk_12_5m; counter 0; end end endmodule2. SPI协议深度解析与实现SPI(Serial Peripheral Interface)是一种同步串行通信协议在Flash控制中需要特别注意其工作模式。M25P16支持模式0(CPOL0, CPHA0)和模式3(CPOL1, CPHA1)我们选择模式3进行通信。SPI主设备状态机设计localparam IDLE 3d0, DELAY 3d1, // 片选提前拉低 TRANS 3d2, // 数据传输 HOLD 3d3; // 片选保持 always (posedge clk or negedge rst_n) begin if(!rst_n) state IDLE; else case(state) IDLE: if(start) state DELAY; DELAY: if(delay_done) state TRANS; TRANS: if(trans_done) state HOLD; HOLD: if(hold_done) state IDLE; endcase end关键时序参数参数描述M25P16要求12.5MHz周期tSHSL片选建立时间≥5ns80nstSHWL片选保持时间≥100ns80nstPP页编程时间典型1.4ms实际等待5mstSE扇区擦除时间典型0.7s实际等待3s提示虽然手册给出了典型操作时间但实际编程时应按最大等待时间设计确保可靠性。3. Flash控制状态机设计与实现M25P16的操作需要严格遵循命令序列。我们设计一个多层状态机来处理各种操作3.1 顶层状态设计localparam IDLE 4d0, RDID 4d1, // 读ID READ 4d2, // 读数据 WEN_1 4d3, // 写使能(擦除前) SE 4d4, // 扇区擦除 DEL_SE 4d5, // 擦除等待 WEN_2 4d6, // 写使能(编程前) PP 4d7, // 页编程 DEL_PP 4d8; // 编程等待3.2 关键命令字节指令代码描述WREN06h写使能RDID9Fh读器件IDREAD03h读数据PP02h页编程SED8h扇区擦除3.3 数据流控制写入数据时需要使用FIFO缓冲防止数据丢失// FIFO控制逻辑示例 assign fifo_rd_req (state PP) (cnt_byte 3) tx_ready; assign write_ready ~fifo_full; always (posedge clk) begin if(write_req) begin write_addr_r write_addr; write_num_r write_num; end if(fifo_num write_num_r) fifo_wr_end 1; else if(pp2del_pp) fifo_wr_end 0; end4. 完整代码实现与测试4.1 顶层模块设计module top( input clk, input rst_n, input [2:0] key, // 按键输入 output sclk, output mosi, input miso, output cs_n ); // 时钟分频 wire clk_12_5m; clk_div u_clk_div(.clk_50m(clk), .clk_12_5m(clk_12_5m)); // 按键消抖 wire [2:0] key_flag; key #(.KEY_WIDTH(3)) u_key( .sys_clk(clk_12_5m), .sys_rst_n(rst_n), .key_in(key), .key_flag(key_flag) ); // Flash控制器 flash_control u_flash( .clk(clk_12_5m), .rst_n(rst_n), .rdid_req(key_flag[0]), .read_req(key_flag[1]), .read_addr(24h0), .read_num(8d1), .write_req(key_flag[2]), .write_addr(24h0), .write_num(8d1), .write_data(8hAC), .write_data_vld(key_flag[2]), .sclk(sclk), .mosi(mosi), .miso(miso), .cs_n(cs_n) ); endmodule4.2 测试方案设计使用ModelSim进行功能仿真时需要模拟Flash的行为// Flash行为模型 initial begin // 初始化 flash_mem new(); // 测试序列 #100; write_flash(0, 8hAC); // 写入数据 #100; read_flash(0); // 读取验证 #100; read_id(); // 读取ID #5000; $stop; end典型测试用例ID读取测试发送9Fh命令应返回厂商ID(20h)和器件ID写-读测试先发送WREN(06h)发送PP(02h)地址数据等待tPP时间发送READ(03h)地址读取验证扇区擦除测试发送WREN(06h)发送SE(D8h)扇区地址等待tSE时间读取验证是否全FFh5. 性能优化与实用技巧5.1 时序优化方案// 精确延时控制 always (posedge clk) begin if(state DEL_SE || state DEL_PP) begin if(cnt_delay delay_max-1) cnt_delay cnt_delay 1; else cnt_delay 0; end else cnt_delay 0; end assign end_delay (cnt_delay delay_max-1);5.2 状态寄存器利用实际项目中建议读取状态寄存器而非固定延时// 状态寄存器读取 localparam RDSR 8h05; // 读状态寄存器命令 // 状态寄存器位定义 define WIP 0 // 写进行中 define WEL 1 // 写使能锁存 // 状态检查逻辑 task check_status; begin send_cmd(RDSR); while(rx_data[WIP]) begin #100; send_cmd(RDSR); end end endtask5.3 错误处理机制增加超时检测和错误状态// 错误状态定义 localparam ERROR_TIMEOUT 2b01; localparam ERROR_WEL 2b10; // 超时计数器 always (posedge clk) begin if(state ! IDLE) begin if(timeout_cnt 32hFFFF_FFFF) timeout_cnt timeout_cnt 1; end else timeout_cnt 0; end // 错误检测 assign error_flag (timeout_cnt TIMEOUT_MAX) ? ERROR_TIMEOUT : (check_wel !status_wel) ? ERROR_WEL : 2b00;6. 实际项目中的应用扩展6.1 多扇区操作实现连续扇区编程功能// 多扇区写入控制 reg [7:0] sector_cnt; always (posedge clk) begin if(sector_write_start) begin sector_cnt 0; current_addr start_addr; end else if(sector_write_busy) begin if(one_sector_done) begin sector_cnt sector_cnt 1; current_addr current_addr 4096; // 4KB扇区 end end end assign sector_write_done (sector_cnt sector_total);6.2 掉电保护设计重要数据写入需要特殊处理// 安全写入流程 task safe_write; input [23:0] addr; input [7:0] data; begin // 1. 检查目标区域是否已擦除 if(!is_erased(addr)) begin // 2. 执行扇区擦除 send_wren(); send_se(addr); wait_erase_done(); end // 3. 执行编程 send_wren(); send_pp(addr, data); wait_program_done(); // 4. 验证数据 if(read_back(addr) ! data) error_handling(); end endtask6.3 文件系统集成简易文件系统设计思路// 文件系统头结构 typedef struct packed { logic [7:0] magic; // 文件标识 logic [23:0] file_size; // 文件大小 logic [23:0] next_file; // 下一个文件位置 logic [31:0] checksum; // 校验和 } file_header_t; // 文件写入流程 task write_file; input [23:0] start_addr; input [7:0] data[]; begin // 计算校验和 checksum calc_checksum(data); // 写入文件头 write_header(start_addr, data.size(), checksum); // 写入文件数据 for(int i0; idata.size(); i) begin write_byte(start_addr8i, data[i]); end end endtask