资讯动态

基于FPGA读写MT25QL SPI NOR Flash的工程实现与验证

发布时间:2026/9/1 2:08:57 来源:尧图企业网站定制
简介本资源是一套基于Xilinx FPGA平台实现MT25QL系列SPI NOR Flash读写控制的完整Verilog工程面向数字电路初学者、FPGA开发工程师及嵌入式硬件开发者解决Flash芯片底层驱动开发与验证难题。工程以Vivado 2018.3为开发环境通过串口交互115200波特率触发对Flash起始地址连续256字节的写入与回读操作具备即连即验的闭环验证能力便于快速掌握SPI Flash时序控制、状态寄存器解析及扇区擦写管理等核心知识点。压缩包共329个文件含28个Verilog源码含关键注释、7个XDC约束文件需按实际原理图适配、14个Tcl脚本支持综合/实现/仿真全流程、19个RST报告及多个bit/DCP/IP核文件总大小10.56MB目录结构清晰模块划分明确。已有1141人学习下载配套代码全部可读可调FLASH驱动模块已封装为独立IP读写端口标准化可直接对接用户逻辑同时提供时钟管理50MHz输入→80MHz工作频率、ILA调试配置及基础仿真脚本显著降低移植与调试门槛。 做FPGA开发的朋友迟早都会碰到一件事给板子配一颗SPI NOR Flash。无论是存配置、存固件、存采集数据这颗小芯片都是绕不开的。我最近在项目中用FPGA驱动MT25QL系列FLASH芯片从指令时序到状态机设计再到板级回环验证完整走了一遍踩了不少坑也沉淀了一些值得分享的经验。这篇文章就把这个“基于FPGA读写MT25QL FLASH芯片详细示例工程”的整体思路、Verilog实现要点、验证流程和排查技巧一次性讲清楚希望对正在做存储相关开发或者正准备入门FPGA存储控制器的同学有帮助。这个工程解决的核心问题很直接让FPGA通过SPI接口对MT25QL FLASH完成擦除、写入、读取操作并提供可靠的数据校验机制。适合正在学习FPGA的开发者、需要在自己的板卡上使用SPI NOR Flash的硬件工程师以及想搞清楚存储控制器内部状态机设计逻辑的人参考。下面我开始讲正题。1. 项目整体设计与思路拆解1.1 为什么要用MT25QL而不是常见的W25Q128先说芯片选型。MT25QL是美光Micron的工业级SPI NOR Flash系列常见型号包括MT25QL128ABA128Mbit16MB、MT25QL256ABA256Mbit32MB、MT25QL512ABA512Mbit64MB等。很多朋友可能更熟悉Winbond的W25Q128因为国产开发板、ESP32模块上用得极多。那为什么这个工程选MT25QL原因有三第一Xilinx官方评估板大量使用MT25QL系列。比如Zynq和Ultrascale系列的开发板上板载QSPI Flash很多就是MT25QL256FPGA工程师如果能直接驱动它在调试官方板卡或做原型验证时会非常顺手不用再去换芯片。第二MT25QL的指令集完全兼容JEDEC SPI NOR标准和W25Q系列的核心指令高度一致像READ0x03、PAGE PROGRAM0x02、SECTOR ERASE0x20、READ ID0x9F这些指令两边通用。也就是说只要把这个控制器写通换到W25Q128上也基本不用改逻辑顶多改改容量、页大小之类的参数。这个工程涉及的方案具有天然的延展性。第三MT25QL支持单线、双线、四线SPI模式最高工作频率可以达到133MHz。虽然单线模式跑不到那么高但如果后续项目有提速需求同一个控制器可以平滑扩展到Quad SPI模式代码架构不用推倒重来。1.2 控制器架构两级结构的设计取舍这个工程没有用Xilinx自带的SPI IP核而是完全自己写Verilog实现这是有意的设计选择。为什么不用现成IP核一是IP核内部逻辑黑盒化出问题不好定位二是不同Vivado版本生成的IP核接口有差异换个版本工程要重新配置三是自己写控制器可以清楚掌控每一个时钟周期、每一个延时这对理解SPI协议本身非常有帮助。整体架构上我采用的是“SPI协议层 Flash应用层”的两级结构跟软件分层的思想类似上层逻辑不关心底层SPI波形怎么产生底层逻辑也不知道上层在发什么指令只管按协议收发数据。这个解耦思路在调试和复用上都很好用。具体划分为三个模块spi_masterSPI主控制器负责时钟分频、字节级收发、片选信号控制对外提供简单的字节发送/接收接口。flash_ctrlFlash应用层状态机负责拼装指令、地址、数据管理擦除、编程、读取的完整流程还要轮询状态寄存器判断WIP位。top顶层模块完成两个模块的例化并对外引出引脚约束同时可以挂LED或串口用于状态指示。这个分层的核心价值在于如果你想换一颗支持不同指令集的Flash只需要改flash_ctrl这一层SPI物理层完全不用动如果你想换FPGA平台只需要改top层的引脚约束和spi_master的时钟分频参数。这个结构我在多个项目里复用下来非常实用。2. SPI协议基础与MT25QL指令集详解2.1 SPI接口信号与工作模式选择SPI接口一共4根线比I2C多一根但传输速率和简单程度都更有优势。MT25QL的SPI接口信号如下CS_N片选低有效。整个指令传输过程中必须一直拉低指令结束后拉高。这里有一个容易踩坑的点某些人对“低有效”不够敏感把片选极性搞反导致FPGA发出去的指令Flash完全收不到。SCK串行时钟空闲状态的电平由CPOL决定。DIMOSI主机发送数据线数据在时钟沿被Flash采样。DOMISO主机接收数据线Flash在读操作中输出数据。MT25QL支持SPI Mode 0CPOL0CPHA0和SPI Mode 3CPOL1CPHA1。FPGA作为主机时工程里最常用的是Mode 0即空闲时SCK为低电平数据在SCK上升沿被采样。选择Mode 0的好处是逻辑实现简单FPGA内部时序分析也直观。需要特别注意的是MT25QL的DI信号在Mode 0下要求数据在SCK上升沿之前稳定换句话说FPGA发送数据时要在SCK下降沿前后更新数据这样才能保证在上升沿被可靠采样。实际代码里我会用SCK的下降沿来更新发送数据SCK的上升沿来采样接收数据这个习惯帮我避免了很多时序毛刺问题。2.2 核心指令集与状态寄存器这个示例工程用到的指令不多但每条都很关键指令操作码功能说明备注READ ID0x9F读取JEDEC ID返回3字节调试首选最快确认通信是否正常READ0x03从指定地址读出数据可以连续读无需页边界限制WRITE ENABLE0x06置位WEL写/擦除前必发忘发这步是写失败的No.1原因READ STATUS0x05读取状态寄存器判断WIP位必须轮询确保上一次写/擦完成PAGE PROGRAM0x02以页为单位写入数据单次最多256字节SECTOR ERASE0x20擦除4KB扇区全部置0xFF写数据前必须先擦除BLOCK ERASE0xD8擦除64KB块大容量擦除时比扇区擦除快状态寄存器重点关注两个位Bit0是WIPWrite In Progress为1表示Flash正在执行内部擦除或编程操作此时不响应其他指令Bit1是WELWrite Enable Latch写完0x06之后该位变1表示允许执行写/擦除指令。工程里最稳妥的流程是每次发指令之前先发0x06然后等WIP为0。MT25QL不同工艺版本的JEDEC ID字节定义稍有差异但通常第一个字节是厂商ID 0x20Micron第二个字节是Memory Type 0xBA第三个字节代表容量。比如MT25QL128的容量字节是0x18MT25QL256可能是0x19具体以datasheet为准。上板时用这个ID做回读如果读到0x20开头基本可以判定SPI底层的收发通路是好的。2.3 关键时序写入和擦除到底要多久NOR Flash的写入和擦除是慢操作这和SRAM、DDR完全是两个物种。理解时序参数是状态机设计的前提。以MT25QL128ABA为例几个关键参数据我实测和datasheet对照如下页编程256字节典型0.3ms最大3ms扇区擦除4KB典型90ms最大400ms块擦除64KB典型0.4s最大2.5s这就引出一个设计原则FPGA发完PAGE PROGRAM或SECTOR ERASE指令后必须持续轮询状态寄存器的WIP位直到WIP清零才能进行下一步操作。绝对不能靠固定延时简单凑合因为Flash的擦除时间随温度和芯片批次差异极大固定延时太短会出错太长会拖慢整体性能。轮询WIP的标准做法是发0x05指令接8个SCK时钟从DO线上读回状态寄存器检查Bit0。如果为1拉高CS_N、再拉低CS_N重新发送0x05循环。发送0x05读取状态寄存器不需要先发WRITE ENABLE它任何时候都可以执行。芯片进入深度掉电模式需要先发送WAKE UP0xAB指令才能恢复我用了一个示例来留作扩展提醒。3. 核心模块Verilog实现3.1 SPI主控制器字节级收发基础spi_master模块是整个工程的地基。它的工作核心是根据分频系数产生SCK在SCK下降沿更新发送数据在SCK上升沿采样接收数据。每次传输一个字节需要8个SCK周期。模块接口设计如下module spi_master #( parameter CLK_FREQ 50_000_000, // 系统时钟频率 parameter SPI_FREQ 10_000_000 // SPI时钟频率MT25QL单线模式建议不超过50MHz )( input wire clk, input wire rst_n, input wire start, // 启动一次单字节传输 input wire [7:0] tx_data, // 要发送的数据 output reg [7:0] rx_data, // 接收到的数据 output wire done, // 传输完成标志 // SPI物理接口 output wire sck_out, output reg cs_n_out, // 片选控制由上层逻辑接管 output reg mosi_out, input wire miso_in );这里需要注意一个细节cs_n_out为什么由上层控制而不是模块内部自动拉低拉高因为Flash指令的传输长度不同比如READ ID是1个命令字节加3个字节回读需要连续传输4个字节片选必须在这4个字节期间全程保持低电平。如果由底层模块每传一个字节就拉高拉低一次片选那指令就被切碎了Flash会认为传输中断返回的数据全是无效的。因此片选信号由flash_ctrl应用层统一控制底层模块只负责纯字节收发。SCK分频的计算方式如下SPI_FREQ SPI时钟频率分频系数 CLK_FREQ / (2 × SPI_FREQ)。工程例化时如果系统时钟50MHz、SPI时钟10MHz分频系数就是2.5需要向上取整为3也就是每6个系统时钟产生1个SCK周期实际SCK约8.33MHz在Flash允许的范围内。3.2 Flash应用层状态机设计flash_ctrl模块是工程的核心状态机负责把底层字节传输组合成真正的Flash操作。状态机划分如下IDLE空闲状态等待上位机触发指令SEND_CMD发送指令字节如0x9F、0x02、0x20SEND_ADDR发送3字节地址高位在前按地址从高字节到低字节排列WRITE_DATA逐字节发送页数据每发送一个字节检查是否到底WAIT_WIP等待Flash内部操作完成轮询状态寄存器READ_DATA读取数据每读一个字节保存到FIFO或寄存器DONE完成标志拉高一个周期的脉冲页编程状态机的关键代码如下我做了简化但保留了核心思路// 以PAGE PROGRAM为例的简化状态机片段 always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; byte_cnt 0; end else begin case (state) IDLE: begin if (start_write) begin // 先发写使能然后在使能完成后进入SEND_CMD spi_cmd 8h06; // WRITE ENABLE state ISSUE_WE; end end ISSUE_WE: begin if (spi_done) state SEND_CMD; end SEND_CMD: begin // 发送0x02随后依次发送addr[23:16]、addr[15:8]、addr[7:0] if (byte_cnt 3) begin byte_cnt 0; state WRITE_DATA; end end WRITE_DATA: begin // 每拍写入一字节建议在最后字节时触发spi_start if (byte_cnt PAGE_SIZE) state WAIT_WIP; end WAIT_WIP: begin // 读状态寄存器判断Bit0是否清零 // 未清零则继续轮询清零则去DONE end DONE: begin state IDLE; end endcase end end这个状态机最大的特点是任何写、擦操作都循着“先发写使能→再发命令→轮询WIP→结束”这个固定模板。READ操作不涉及写使能直接发命令和地址就能读数据。建议大家在实现时把状态机模板抽象出来每条指令复用相同的状态逻辑只替换命令字和数据处理分支这样代码密度会大幅降低结构也更清晰。另外页编程有一个必须注意的边界一次PAGE PROGRAM最多写入256字节而且不能跨页写。如果起始地址是0x0105那么这一页剩下的字节数只有0xFF - 0x05 1 251字节超过251字节就写到下一页去了。MT25QL对跨页写有两种处理策略但工程上最安全的做法是从应用层严格切分每次写前计算当前地址在页内偏移确保单次编程不超过页边界。3.3 顶层例化与仿真验证顶层模块的工作就是把spi_master和flash_ctrl接好同时做引脚约束。例化时注意以下事项spi_master的SPI_FREQ参数不要超过Flash单线模式的最高时钟保守起见设为10MHz足够应付绝大多数应用场景。flash_ctrl的地址位宽要针对MT25QL128设置24位如果换到1Gb容量128MB则地址需要扩展到32位。在Vivado里需要在XDC约束文件中正确分配引脚注意引脚所在的BANK电压必须是1.8V或3.3V取决于板卡上Flash的实际供电否则上电后引脚电平不匹配数据读回来全是乱码。仿真建议用ModelSim或Vivado自带的XSim搭建一个简单的testbench把Flash的SPI回环模型替换为理想模型或者直接用逻辑分析仪抓仿真波形。先跑一个最简单的场景发起一次READ ID检查回读的0x20、0xBA、0x18是否出现在DO线上。如果仿真能过说明SPI层时序逻辑基本正确再上板验证不至于瞎子摸象。4. 板级实操与验证流程4.1 硬件连接与初始化检查上板之前先做三件基本功检查别急着烧bitstream否则出了问题很难区分是逻辑问题还是硬件问题。第一确认Flash的供电电压。MT25QL支持3.3V VCC部分工业级版本还支持1.8V具体看芯片丝印的后缀。FPGA引脚所在的BANK电压必须与之匹配。之前我踩过一次坑板卡上Flash是3.3V供电但FPGA BANK设成了1.8V结果SPI时钟、片选这些信号电平都不对每次读ID都返回0x00。第二确认CS_N引脚在空闲状态被拉高。可以用万用表量一下FPGA引脚在未配置时的状态如果FPGA的IO默认是下拉而Flash的CS_N又没有外部上拉电阻上电瞬间可能导致Flash误进入编程模式。工程里建议在XDC中将CS_N引脚设置为内置上拉PULLUP保险起见板级最好还有一颗10kΩ上拉。第三确认地线连接。这个说起来很基础但SPI高速翻转时如果FPGA和Flash的地电位不一致MISO线上会出现严重的噪声干扰轻则数据移位重则整整比DDR内存条线的小身板都难过。4.2 完整读写回环测试步骤上板验证建议按以下顺序一步步跑每一步都确认通过后再进入下一步读ID测试烧写完工程后通过UART或LED观察READ ID返回的数据。如果正确读到0x20、0xBA、0x18说明SPI链路、片选控制、时钟极性全部正确这时候可以认为底层已经通了。擦除扇区测试选择一个不重要的扇区比如地址0x000000开始的4KB扇区发SECTOR ERASE 0x20命令。擦除后从该扇区首地址连读4KB确认数据全部是0xFF。页编程测试向擦除后的扇区写入一页256字节的测试数据建议使用递增序列0x00到0xFF而非全0或全1这样万一有地址错位或位翻转对比时能一眼看出来。读回比较再读取该页数据和写入数据进行逐字节比对。如果全部匹配说明写路径和读路径都正确。跨页测试从页尾地址比如0x0000FF写入一页数据验证应用层是否正确处理了跨页切分逻辑。掉电保持测试完整写入一批数据后断电重新上电再次读取数据确认数据没有丢失。这里有一个非常重要的实测心得测试数据一定不要用全0x00或全0xFF。因为Flash擦除后是0xFF如果写全0xFF读回来也是0xFF根本分辨不出是“写成功了”还是“压根没写”。用递增序列或者伪随机序列才能真实反映数据通路的正确性。4.3 使用ILA抓取时序的调试方法如果回环测试失败最直接的定位手段是Vivado的ILAIntegrated Logic Analyzer核。挂上ILA后抓取cs_n_out、sck_out、mosi_out、miso_in这四根线以及状态机的state信号触发条件设为start_write上升沿。抓到数据后重点看三个方面一是看片选时序。一个完整的指令过程CS_N必须从指令开始一直拉低到最后一个比特结束期间不能出现毛刺。如果在指令中间CS_N意外跳高了Flash会立即终止当前传输。二是看SCK与MOSI的相位关系。SPI Mode 0下SCK空闲为低MOSI数据必须在SCK上升沿前稳定。如果波形里看到MOSI在上升沿附近才跳变说明发送数据的更新时刻偏晚了需要检查spi_master里发送数据是在SCK的哪个沿更新的。三是看MISO采样窗口。读数据时FPGA在SCK上升沿采样MISO。如果MISO在上升沿附近有回踢噪声或翻转太慢说明前面分析时序或走线存在问题可能需要降低SPI的通信速率或者给MISO信号加一个简单的一级寄存器打拍消除亚稳态风险。ILA采样深度建议设置到4096以上因为页编程整个流程会产生大量跳变太浅抓不住完整过程。触发位置设为“middle”可以同时看到触发前的命令发送过程和触发后的WIP轮询过程。5. 常见问题速查与避坑指南5.1 典型问题与排查思路根据我调试这个工程的经验把最容易碰到的几个问题整理成了一张排查表方便大家在现场快速定位现象可能原因定位方法解决办法读ID全FFCS_N极性接反或未拉低用ILA抓CS_N波形检查CS_N逻辑确认低有效读ID全00FPGA IO电压与Flash不匹配万用表量VCC和IO电平调整BANK电压至匹配值读ID正确写入后读回全FF忘记发WRITE ENABLE查看波形确认0x06是否发送在擦除/编程前补发0x06写入后读回部分正确部分错误跨页写入未处理检查写入地址和页边界应用层计算页内剩余字节并拆分擦除后读回不是全FFBLOCK ERASE和SECTOR ERASE搞混核对命令字0x20是4KB扇区擦除0xD8是64KB块擦除数据总是移位错位SPI采样沿不对ILA看SCK和MISO相位确认Mode 0上升沿采样状态机卡在WAIT_WIP不动Flash已进入保护状态或命令未生效波形分析检查WP引脚和状态寄存器其他位5.2 我踩过的坑和总结的经验第一个坑是写保护。MT25QL有WP#引脚和状态寄存器里的块保护位BP0-BP3默认情况下BP位为0没有启用保护这在绝大多数新片子是安全的。但有些板载Flash在出厂时可能被配置了保护或者上一个程序改过状态寄存器导致你发PAGE PROGRAM后WIP一直为1状态机卡死。遇到这种情况排查思路要拓宽先发WRITE STATUS0x01把状态寄存器配置为0x00解除块保护再执行擦写。第二个坑是上电后Flash状态不确定。MT25QL上电后可能处于标准SPI模式也可能因为之前的供电时序问题进入了一个非预期的状态。上电后建议先发送一条WRITE DISABLE0x04或在读ID之前多发几条0x06让Flash的状态机恢复到已知状态再执行正式操作。第三个坑是仿真环境里忽略时序延时。很多初学者在仿真时把Flash模型换成理想模型WIP位在仿真里瞬间清零导致状态机设计时低估了真实芯片的擦除等待时间。真实芯片上擦除一个扇区可能需要几百毫秒而且温度越低擦除越慢。因此我强烈建议在实际调试时预留足够长的WIP超时阈值比如1000ms并用一个计数器在轮询超过阈值后报错避免死等。第四个坑比较隐晦是关于FPGA引脚推挽输出的。SPI的MOSI、SCK这两个输出信号在FPGA中如果用默认的推挽输出本身没有问题。但MISO是输入信号如果板级设计没有做串联匹配电阻在长走线下会出现过冲采样到错误的电平。如果读数据偶尔出错、复位后又正常优先怀疑信号完整性可以尝试在MISO的FPGA输入引脚上设置内部延迟单元IDELAY或者降低SPI速率以换取稳定。第五个心得是关于命令字节的拼接顺序。MT25QL这类SPI NOR Flash指令长度不固定同样是指令有的需要1字节命令外加3字节地址有的只需要命令无地址。我见过不少人在状态机里把地址字节顺序搞反先发低字节后发高字节导致读写地址偏移。Flash地址发送顺序是高位先行即addr[23:16]、addr[15:8]、addr[7:0]这一点务必记牢。第六个经验是工程落地时建议加上CRC校验或简单和校验。虽然这个示例工程验证阶段只用逐字节比较但实际产品中数据从Flash读回后最好通过CRC32或和校验来做完整性验证。特别是做固件升级场景时Flash中的数据损坏可能是致命的。我后来在这颗MT25QL工程上增加了一个简单的异或校验模块写入时计算校验值读取时重新计算不匹配就直接报错并重擦重写在产线测试里显著提升了问题定位效率。另外关于时钟频率很多人一开始就把SPI拉到50MHz甚至更高结果调试时各种问题叠加很难定位。我的建议是先把SPI降到5MHz甚至1MHz跑通整个流程确认逻辑正确后再逐步提速。FPGA和Flash之间的信号完整性问题往往在高频下才会暴露但绝大多数应用场景单线SPI 10MHz到20MHz已经足够没必要为了跑速度给自己找麻烦。最后再分享一个小技巧在正常读写验证通过后可以把写入的数据换成随机数连续写多个扇区再做完整读回比较这个压力测试能发现一些边角情况下的地址错位问题。我通常在验收工程前会跑一轮全地址空间的“写-读-比”循环发现过两次页边界处理不严谨导致的偶发错误这类问题在只做单页测试时是根本复现不出来的。这个工程的后续扩展方向也很明确一是将单线模式扩展为Quad SPI读写速率可以翻几倍二是增加Flash坏块管理逻辑把存储区域划分为数据区和备份区三是对接FIFO或DMA接口让Flash读写不占用CPU这里指FPGA内部软核处理器时间。如果你正在做类似的存储控制器建议先把单线模式的读写流程吃透再逐步扩展底子打牢了后面就快很多了。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价