FPGA读写MT25QL QSPI Flash完整工程与Verilog实现详解 简介本资源是面向FPGA初学者与嵌入式硬件开发者的MT25QL系列SPI Flash读写实战工程聚焦于解决FPGA与高速串行FLASH芯片的底层驱动集成难题适用于数据存储、固件升级、配置加载等典型应用场景。压缩包共329个文件涵盖28个Verilog源码含完整FLASH控制器、UART接口、PLL时钟管理模块、7个XDC约束文件已标注管脚映射逻辑、27个文本说明与注释文档以及bit位流、仿真脚本.bat/.tcl、综合报告.rpt和调试波形.wdb/.ila等关键开发产物整体大小10.56MB。已有164人学习下载工程基于Vivado 2018.3构建输入时钟50MHz经PLL倍频至80MHzUART波特率115200支持一键发送单字节触发256地址连续写入回读验证所有模块端口清晰分离FLASH驱动已封装为可复用IP仅需对接数据/控制信号即可快速集成全部Verilog代码关键路径均附中文注释便于理解时序逻辑、状态机流转与SPI协议交互细节。 FPGA和MT25QL这俩词放一起八成是碰到实际项目了。MT25QL是Micron家的QSPI NOR Flash在工业控制、通信板卡、测试测量设备里出现频率极高很多FPGA工程都要靠它存配置数据、固件镜像或者运行日志。我之前在几个项目里都跟这颗芯片打过交道从最开始照着数据手册写SPI控制器到后来调通四线QSPI模式、搞定擦写均衡和掉电保护中间踩了不少坑。这篇就把整个读写示例工程的细节拆开讲从芯片特性到Verilog实现从时序参数到在线调试一次性说清楚。1. 先认识MT25QL它和普通SPI Flash有什么区别很多人第一次接触MT25QL会下意识把它当成W25Q系列那样“拿来就用”的芯片。实际上MT25QL虽然兼容经典SPI协议但它真正的设计目标是高性能QSPI接口和市面上常见的GD25Q、W25Q在指令集和时序细节上有不少差异。1.1 选型之前需要搞清楚的几个关键参数MT25QL家族最常见的型号是MT25QL128ABA和MT25QL256ABA分别对应128Mb16MB和256Mb32MB容量。选型时第一件事是确认封装和电压等级MT25QL有1.8V和3.3V两个版本后缀不同比如MT25QL128ABA8ESF是3.3V的SOP-16封装MT25QL128ABA1EW9则是1.8V的WPDFN封装。FPGA的IO bank电压必须和芯片匹配否则轻则通信不稳定重则直接损伤芯片。再看接口MT25QL支持三种SPI模式单线SPI1-1-1、双线SPI1-1-2和1-2-2、四线QSPI1-1-4和1-4-4。这里的数字含义是“命令线数-地址线数-数据线数”比如1-4-4表示命令用单线发地址和数据都用四线。工程里常说的“QSPI模式”通常指1-4-4这是MT25QL吞吐率最高的传输方式。如果只是简单存个配置文件单线SPI也够用但如果你要频繁更新固件或者存储采集数据QSPI模式能把传输时间缩短到原来的四分之一。还要注意MT25QL支持DTRDouble Transfer Rate模式也就是双沿采样。在DTR模式下时钟的上升沿和下降沿都传输数据理论上能把吞吐率再翻一倍。但DTR对FPGA侧的时序约束要求比较高我建议除非对带宽有硬性要求否则第一版工程先用STRSingle Transfer Rate模式调通再考虑要不要上DTR否则到时候时序收敛和调试会一起压过来很容易焦头烂额。1.2 指令集和状态机思维别把它当普通SPI从机MT25QL的指令集比普通SPI Flash丰富不少基础操作包括Read0x03、Fast Read0x0B、Page Program0x02、Sector Erase0x20、Block Erase0xD8、Write Enable0x06、Read Status Register0x05等。四线模式下还有对应的Quad Output Fast Read0x6B、Quad Input Fast Program0x32等指令。这里有个容易忽略的点MT25QL的地址支持3字节和4字节两种模式默认是3字节地址最大寻址128Mb如果你用的是256Mb的芯片或者开启了4字节模式必须通过命令比如0xB7进入4字节模式切换否则高位地址会丢失读写数据错位。FPGA复位后一般要检查一下当前地址模式确保和配置一致。写指令的时候还有个细节很多Flash芯片在Page Program之前会自动写使能但MT25QL要求必须先发Write Enable0x06命令把状态寄存器里的WEL位置1然后才能发Program命令。如果跳过这一步芯片会直接忽略写入请求状态寄存器里的WEL位还是0。这个问题我们在后面代码实现时会展开这里先记住MT25QL的写流程是“写使能→写指令→轮询状态寄存器”三步走缺一不可。从FPGA侧看MT25QL就是一个受命令驱动的状态机。你给它发命令字节它根据命令切换到对应的内部状态比如读状态寄存器、执行擦除、接收页数据等。FPGA端不需要关心芯片内部怎么管理存储阵列只需要严格按照时序把命令、地址、数据、等待周期组织好即可。这就像去银行办业务你不需要知道银行柜台后面怎么记账只需要按流程填单、排队、递材料窗口工作人员自然替你办完。2. FPGA端架构设计模块划分与顶层连线写FPGA工程最忌讳一上来就写RTL。先把架构理清楚后面写代码才能少返工。基于MT25QL的读写示例工程我建议分成三个层次顶层接口层、命令控制层、物理传输层。2.1 顶层模块先把三个接口通道想明白顶层模块需要对外提供三类接口第一类是FPGA内部的用户接口也就是给其他逻辑模块用的信号。我习惯把它设计成一个简单的Memory Mapped接口包括时钟、复位、读请求、写请求、地址、写数据、读数据、忙标志。这样的接口对上层业务逻辑很友好无论是软核CPU、状态机还是DMA控制器都能快速对接。第二类是MT25QL物理接口也就是直接连到芯片管脚上的信号。单线SPI模式下只需要4根线SCK、CS_N、MOSIDQ0、MISODQ1。QSPI模式下需要6根线SCK、CS_N、DQ0、DQ1、DQ2、DQ3其中DQ0在写方向作为MOSI读方向作为MISODQ1-DQ3在QSPI模式下都是双向数据线。这里要注意FPGA管脚的三态控制QSPI模式下DQ方向随时要切换处理不好就会出现总线冲突。第三类是可选的状态/调试接口。比如LED指示当前状态、ILA探针信号等。把调试信号在顶层显式引出来在线调试时直接添加ILA核不用翻代码找内部信号能省不少事。三态IO在FPGA里的写法有固定套路我习惯用下面的代码结构// QSPI模式下DQ双向控制 assign DQ0 (dq0_oe) ? dq0_out : 1bz; assign DQ1 (dq1_oe) ? dq1_out : 1bz; assign DQ2 (dq2_oe) ? dq2_out : 1bz; assign DQ3 (dq3_oe) ? dq3_out : 1bz; // 读回输入 assign dq0_in DQ0; assign dq1_in DQ1; assign dq2_in DQ2; assign dq3_in DQ3;dq0_oe为高时FPGA驱动DQ0为低时释放总线让芯片驱动。QSPI模式读数据阶段FPGA必须把DQ1-DQ3全部释放否则会跟芯片的输出打架。2.2 状态机划分命令层、传输层、应用层各司其职整个读写控制逻辑我习惯拆成三个状态机而不是一个大状态机搞定所有事。这样做的原因是职责清晰调试时只需要定位具体是哪个状态机出了问题。命令层状态机负责维护当前的命令上下文比如正在执行的是读、写还是擦除。它接收用户接口的请求解析命令类型生成对应序列。传输层状态机负责把命令、地址、数据字节按顺序发送出去处理位宽转换和时钟边沿。应用层状态机则处理上层业务逻辑比如“收到写请求后先发写使能再发页编程然后轮询状态寄存器”这些多步操作的编排逻辑放在应用层。这里有个设计原则物理传输层只做“按字节收发”不关心字节内容命令层只做“指令序列编排”不关心物理时序。这样某层改动时不会影响其他层。比如从单线SPI升级到QSPI模式时只需要改物理传输层指令序列和数据编排不用大变。我第一版工程就是靠这个架构后来把单线模式升级到QSPI模式只花了两天。3. 核心代码实现从“发出一条命令”到“完整读写”这个章节是整篇的核心我会按“单线SPI控制器→读流程→写流程→擦除流程”的顺序展开每段都给出关键代码和背后考量。3.1 先写一个干净的单线SPI控制器很多人写SPI控制器喜欢用计数器产生分频时钟这个方法能跑但可维护性差。我推荐用状态机加移位寄存器的方式主时钟直接作为SPI时钟源通过分频计数器产生SCK的边沿事件这样代码更清晰也方便加时序约束。一个最小可用的SPI发送状态机是这样的localparam IDLE 3d0; localparam SHIFT 3d1; localparam DONE 3d2; reg [2:0] spi_state; reg [7:0] shift_reg; reg [3:0] bit_cnt; reg sck_reg; reg cs_n_reg; always (posedge clk or negedge rst_n) begin if (!rst_n) begin spi_state IDLE; cs_n_reg 1b1; sck_reg 1b0; shift_reg 8d0; bit_cnt 4d0; end else begin case (spi_state) IDLE: begin cs_n_reg 1b1; if (tx_valid) begin cs_n_reg 1b0; // 拉低片选开始传输 shift_reg tx_data; bit_cnt 4d8; spi_state SHIFT; end end SHIFT: begin if (spi_clk_rise) begin // 上升沿发送数据 DQ0 shift_reg[7]; shift_reg {shift_reg[6:0], 1b0}; end if (spi_clk_fall) begin if (bit_cnt 0) bit_cnt bit_cnt - 1b1; if (bit_cnt 1) spi_state DONE; end end DONE: begin cs_n_reg 1b1; tx_done 1b1; spi_state IDLE; end endcase end end这里要注意MIT25QL在SPI模式下要求SCK空闲为低电平CPOL0数据在上升沿发送、下降沿采样CPHA0也就是SPI Mode 0。在SHIFT状态里要分别生成上升沿和下降沿事件不要直接操作sck引脚而是通过sck_reg在输出端产生时钟。为了生成spi_clk_rise和spi_clk_fall可以用一个计数器// SPI时钟分频 reg [7:0] clk_div_cnt; reg spi_clk_en; always (posedge clk or negedge rst_n) begin if (!rst_n) begin clk_div_cnt 8d0; spi_clk_en 1b0; end else begin if (clk_div_cnt (SPI_DIV/2 - 1)) begin clk_div_cnt 8d0; spi_clk_en 1b1; // 产生一个主时钟周期的使能脉冲 end else begin clk_div_cnt clk_div_cnt 1b1; spi_clk_en 1b0; end end end这个使能脉冲每半个SPI时钟周期出现一次。当spi_clk_en为高时翻转sck_reg就得到分频后的SCK。在sck_reg为低时采集数据、为高时更新输出的逻辑需要根据使能脉冲的序号来判断。用这种“时钟使能”而不是“门控时钟”的做法有几个好处第一sck_reg是寄存器输出不会产生毛刺第二对vivado或Quartus的时序分析友好第三后续如果要支持DTR模式只需要改使能脉冲和采样沿的位置不用推翻重写。3.2 读操作完整流程命令、地址、等待周期、数据以最常见的单线Fast Read0x0B为例完整的读序列是CS_N拉低发送命令字节0x0B发送3字节地址先高后低发送1字节dummy在Fast Read模式下必须有一个等待周期数据手册里叫dummy cycle连续接收数据字节内部地址自动递增CS_N拉高结束传输这里有一个很多人会掉进去的坑Fast Read和普通Read0x03的区别就在dummy cycle。普通Read没有等待周期命令和地址发完直接输出数据Fast Read需要一个dummy字节。如果你用的是0x0B但忘了dummy读出来的数据永远是第一个字节循环或者整体偏移8位。我第一次调这个的时候数据总是错位一个字节查了半天数据手册才意识到是dummy cycle没发。读操作状态机关键片段// 读操作序列 READ_CMD: begin // 发送0x0B spi_tx_data 8h0B; spi_tx_valid 1b1; end READ_ADDR: begin // 发送3字节地址addr[23:16], addr[15:8], addr[7:0] spi_tx_data addr_byte; end READ_DUMMY: begin // 发送dummy字节内容任意 spi_tx_data 8h00; end READ_DATA: begin // 接收数据字节 data_out spi_rx_data; end读数据时FPGA要向芯片提供时钟同时释放MOSI线使DQ0输出为高阻否则MOSI会一直拉在低电平导致芯片的数据输出被干扰。在单线模式下读阶段DQ0方向必须从输出切换到输入否则数据读不回来。这个方向切换的时机要精确建议在最后一个地址字节发出的下降沿切换方向这样可以保证下一个时钟沿采样到的第一个数据是有效的。读操作还有一个细节地址递增是自动的。当你连续拉低CS_N并持续提供时钟时MT25QL的内部地址指针会自动加1所以可以一次读出整片内容而不需要每读一个字节就发一次命令。这在读取日志、固件镜像时非常有用可以大幅度提高读取速度。如果只需要周期性地读同一个固定地址那每次都要重新发命令。不过在嵌入式系统里顺序读的场景远多于随机读所以FPGA设计时把读状态机设计成“连续读直到收到停止信号”是更合理的方案。3.3 写操作与擦除流程轮询状态寄存器才是关键MT25QL的写操作按最小单位分为页编程Page Program和擦除Erase。页编程最小单位是1字节擦除最小单位是4KB的扇区擦除Sector Erase 0x20或64KB的块擦除Block Erase 0xD8。芯片还支持整片擦除0xC7但实际工程中很少用。Page Program的流程是发送Write Enable0x06把WEL位置1发送Page Program0x02发送3字节地址必须在一个页内页大小通常是256字节发送要写入的数据最多256字节CS_N拉高内部编程开始轮询Read Status Register0x05直到WIP位位0从1变为0页编程的“页边界限制”是个大坑。MT25QL规定页编程最多写到当前页的边界如果地址跨越页边界数据会回卷到页起始位置覆盖已经写好的数据。假如你从地址0x00FF开始连续写10个字节前1个字节写到0x00FF后面的字节会写到0x0000开始的地址把页开头的字节覆盖掉。写驱动里必须做页边界检查把跨页的数据拆分两次编程操作。页编程状态机片段// 页编程操作 PAGE_PROG_CMD: begin spi_tx_data 8h02; // Page Program指令 end PAGE_PROG_ADDR: begin spi_tx_data addr_byte; end PAGE_PROG_DATA: begin spi_tx_data data_byte; end PAGE_PROG_WAIT: begin // 轮询状态寄存器 spi_tx_data 8h05; // Read Status Register // 读取状态字节检查bit0WIP if (status_reg[0] 1b0) prog_done 1b1; end轮询状态寄存器本质上是一个“读→判断→是否继续”的循环。发送0x05命令后芯片会在下一个字节输出状态寄存器内容。FPGA需要不断重复这个操作直到WIP位清零才认为编程结束。这个循环里必须给CS_N做正确的拉高拉低操作每轮询一次都要完整地执行“CS_N拉低→发命令→读字节→CS_N拉高”不能一直把CS_N拉低连续读状态寄存器。我见过有人一直拉着CS_N不放结果状态值读出来一直是0xFF怎么都等不到编程完成问题就出在这里。擦除操作流程类似只是命令不同。Sector Erase是0x20Block Erase是0xD8地址只关注高位扇区地址或块地址低地址位会被忽略。擦除时间比编程时间长很多一个扇区擦除大约需要几百毫秒64KB块擦除可能要1~2秒。这段时间内FPGA必须保持对状态寄存器的轮询不能认为“命令发完就结束了”。如果轮询超时要检查是不是地址模式配置错误或者CS_N时序不对。4. 时序参数与运行速率理论计算和实测调优FPGA读写Flash最容易忽略的是时序分析。很多人写完代码发现偶尔读写失败抓波形又看不出明显问题最后发现是SPI时钟频率太高不满足芯片的时序要求。4.1 几个必须知道的时序参数MT25QL数据手册里有几个关键时序参数写驱动前最好记下来参数符号典型值说明SCK最高频率单线fC133MHz普通SPI模式极限频率SCK最高频率QSPIfC133MHz四线模式同样受限于内部时钟输出有效时间tV8ns时钟沿之后数据有效延迟输出保持时间tHO3ns数据保持时间CS_N拉高最小宽度tSHSL20ns两次传输之间的最小间隔页编程时间tPP1.5ms典型值最大3ms扇区擦除时间tSE400ms典型值最大3s状态寄存器轮询周期tW—无固定值通过WIP位判断有了这些参数就能算出极限时钟频率。假设FPGA主频50MHz单线模式下不约束的话SPI时钟直接跑20MHz是没问题的。但如果跑到40MHz以上就要检查FPGA IO的建立时间和保持时间是不是满足。实际工程里除非是高速固件更新场景否则SPI时钟跑10~25MHz就足够了降低速率换来的是稳定性和调试效率这笔账怎么算都划算。4.2 通过ILA实测调整时钟相位FPGA驱动Flash不能只靠理论计算实际板上波形才是最终依据。推荐在Xilinx Vivado里把下面三个信号加入ILA在线调试SPI_SCK、SPI_CS_N、SPI_DQ0。触发条件设置为CS_N下降沿然后观察一次完整命令序列。如果是Xilinx 7系列或UltraScale系列ILA采样时钟至少要达到SPI时钟的4倍。如果SPI时钟是20MHzILA采样时钟至少80MHz。如果你的逻辑主频只有50MHz可以直接把100MHz或200MHz的独立时钟引入ILA。这个在创建ILA核时要手动配置采样时钟不能直接用系统逻辑时钟否则采到的波形会一塌糊涂。实际调时序时重点看三个位置第一命令字节发送完毕后CS_N是否保持低电平足够长的时间出现在下一个命令或数据阶段。如果CS_N提前拉高芯片会认为传输结束后续字节被丢弃。第二读数据阶段的采样点是否正确。SPI Mode 0下FPGA应该在SCK下降沿采样数据也就是芯片在上升沿更新数据之后。如果你在上升沿采样会采到旧值读回来的数据全是乱的。这个在ILA波形上一眼就能看出来数据变化发生在上升沿你的采样点在上升沿必然采到过渡状态。第三dummy cycle是否存在且长度正确。如果用Fast Read0x0B数据手册要求dummy cycle是8个时钟周期1字节。如果用QSPI模式dummy cycle可能变成6个或4个时钟具体看数据手册里的Mode Bits配置。ILA波形上如果命令和地址发完后第一个数据还没出现而你又少发了一个字节的dummy读出的数据会整体偏移一位或一个字节。根据我个人经验最稳妥的做法是在正式工程里把SPI时钟控制在10~20MHz附近然后用ILA验证一次完整的“读ID→读数据→写数据→回读”流程。验证通过后再根据实际需求提高时钟。不要一上来就追求最高速率先把功能跑通再谈优化。5. 问题排查与调试技巧在线调试中的常见坑FPGA调试Flash最大的难点是“看起来时序对了但数据就是不对”。这里我把常见的坑整理成表格方便对照排查。5.1 常见问题速查表现象可能原因排查方法读回的数据全为0xFFSPI空闲态不对或CS_N没有正确拉低检查CS_N时序确认CPOL/CPHA配置读回的数据整体偏移1字节Fast Read漏发dummy cycle确认命令和dummy周期数量读写大文件时偶发失败SPI时钟频率过高建立时间不足降低SPI时钟频率检查IO时序约束写数据后回读不一致页边界跨越数据回卷检查页编程地址是否越界拆分跨页写入擦除后状态寄存器WIP一直为1CS_N在整个轮询周期未拉高确认每次轮询命令完整的CS_N拉高拉低流程芯片能读但不能写缺少Write Enable命令确认写流程第一步是发送0x06QSPI模式下数据错乱DQ方向切换时序不对确认读阶段数据线全部置为高阻第一行“读回全0xFF”是最常见的看起来像芯片没有响应其实往往是把CS_N信号方向搞反了或者时钟极性不对。MT25QL要求CS_N高有效SCK空闲低电平。如果FPGA复位后SCK是常高那芯片可能一直处于无效状态读出来的数据自然全是0xFF。建议在顶层代码里把SPI信号初始值显式写清楚复位时CS_N拉高、SCK拉低、DQ方向切到输出。第二行“数据偏移1字节”也特别常见。我排查这个问题的经验是先在ILA里抓取整个读序列数一下从命令发出到第一个有效数据出现之间到底过了多少个时钟周期。然后把数据手册里的时序图拿出来逐格对比重点看dummy cycle的位置。5.2 调试时的几个取巧技巧在线调试除了用ILA还有几个小技巧能显著提高效率。技巧一用JEDEC ID验证通信链路。MT25QL支持Read JEDEC ID0x9F命令会返回3字节的厂商ID和设备ID。MT25QL128的JEDEC ID是0x20 0xBA 0x18MT25QL256是0x20 0xBB 0x19。上电后先读一次ID如果ID正确说明SPI物理链路、命令时序、地址模式都基本正常再往下调试就有的放矢了。如果ID读不对先别急着调读写优先排查物理层问题。技巧二写一个固定模式的递增数据比如从0x00写到0xFF循环写入一片区域后再读回逐字节比对同时把第一个不匹配的地址反馈到调试信息里。这样能快速定位是“偏移错位”还是“随机翻转”问题。如果是偏移错位往往是命令或dummy问题如果是随机翻转大概率是时序余量不足要降频。技巧三在FPGA逻辑里设计一个简单的“写后读校验”状态机调试完成后可以保留在工程里量产测试时也很有用。这个状态机自动执行“擦除扇区→写入固定图案→回读比对→输出结果”的完整流程省去手动操作逻辑分析仪的时间。不管是开发阶段还是产测阶段这个功能都能帮你快速定位Flash和FPGA之间的通信问题。6. 后续扩展方向从能用到好用整个工程实现完成后还可以考虑几个扩展方向让这套FPGA读写MT25QL的代码具备更强的复用性。第一个方向是把单线SPI升级到QSPI模式。QSPI模式下Page Program可以使用0x32指令Fast Read可以使用0x6B指令地址和数据都走四线吞吐率理论上是单线的4倍。前提是物理层要把DQ0-DQ3的方向切换逻辑处理好并且FPGA侧的IO约束需要调整。升级之后最好重新跑一遍文中的读写校验流程因为四线模式下的时序和单线模式完全不同之前没问题的地方可能在QSPI下暴露出来。第二个方向是增加多片Flash的支持。通过片选信号扩展或者把多片Flash并联使用。MT25QL支持标准的片选级联每片占用一个CS_N引脚。读数据时可以多片并发写数据时要注意各片的写使能状态。多片架构适合做固件AB备份或者RAID级别的数据冗余。第三个方向是结合软核CPU或状态机实现文件系统。比如在FPGA内部跑一个轻量级文件系统如LittleFS或SPIFFS的软件栈把MT25QL当作块设备来管理。这样上层逻辑可以通过文件路径操作Flash而不是直接操作裸地址。这个方案适合需要管理大量配置参数或日志数据的应用场景代码复用性和可维护性都会高很多。最后再分享一个从实际项目里总结出来的建议在正式工程里MT25QL的时钟频率不要追求极限稳定压倒一切。如果项目要求高吞吐率优先考虑QSPI模式并把时钟控制在50MHz以下如果只是配置存储和日志记录20MHz单线模式完全够用。我见过太多人为了省几十毫秒把时钟拉到极限结果产品在高温或低电压环境下偶发读写失败回归测试时焦头烂额。硬件产品最重要的是可预测性和稳定性这不是一句空话而是每一个决策背后都要权衡的准则。本文还有配套的精品资源点击获取