十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FPGA读写SPI Flash完整实战:基于MT25QL的控制器设计与调试

FPGA读写SPI Flash完整实战:基于MT25QL的控制器设计与调试 简介本资源是一套基于FPGA实现MT25QL系列SPI FLASH芯片读写控制的完整Verilog工程面向数字电路初学者、嵌入式硬件开发者及FPGA工程师解决高速非易失存储器底层驱动开发与验证难题。工程以Xilinx Vivado 2018.3平台构建含329个文件涵盖28个核心Verilog模块含FLASH控制器、串口收发、PLL时钟管理、7个XDC管脚约束文件、14个Tcl自动化脚本、13个综合/实现报告rpt及多个bit位流与ILA调试文件总大小10.56MB关键代码均附中文注释驱动模块已封装为独立IP读写端口清晰可复用。已有1141人学习下载工程支持快速移植仅需按实际原理图修改XDC约束并重编译即可适配不同FPGA型号实测通过50MHz输入经PLL倍频至80MHz系统时钟配合115200波特率串口交互发送单字节即自动完成256地址连续写入与回读验证是理解SPI Flash协议、时序控制与FPGA外设集成的高实用性参考设计。 写这个工程之前我其实犹豫了两天。FPGA读SPI Flash听起来是个再普通不过的活儿网上随便搜都能找到一堆参考代码但真正动手去做还是踩了一串坑。MT25QL这种美光的NOR Flash命令集和Winbond的W25Q系列不完全一样时序细节、状态寄存器定义、擦除时间也都是有讲究的照搬别人的代码很容易翻车。这篇文章就基于我实际调通的示例工程把FPGA读写MT25QL的完整思路、关键代码、仿真验证和板级调试过程都摊开来讲希望能让想用FPGA接NOR Flash做配置存储、参数保存或日志记录的读者少走点弯路。1. 工程定位与芯片选型1.1 为什么是MT25QL而不是其他FlashMT25QL是Micron美光的串行NOR Flash家族覆盖128Mb到2Gb的容量范围支持标准SPI、双通道SPI和四通道SPIQSPI读速度最高可以跑到133MHz甚至更高。在FPGA工程里它最常见的角色有两个一个是充当FPGA的配置芯片上电时由FPGA主动加载bitstream另一个是作为通用存储介质用来保存用户数据、运行日志、校准参数等。相比NAND FlashNOR Flash的优点是读接口简单、随机读性能好、可以直接按字节寻址缺点是容量密度低、写速度慢但用作FPGA配置和中小容量数据存储完全够用。我在设计这个示例工程时选的是MT25QL128ABA也就是128Mb容量1.8V供电的型号。选它的原因很简单市面上容易买到大多数开发板上用的也是这一颗或它的3.3V版本MT25QL128ABB两者命令集一致只是电气特性不同。需要注意区分的是MT25QL虽然是JEDEC标准兼容的NOR Flash但它的命令定义和W25Q系列并非所有都一致比如状态寄存器位定义、擦除命令代码就有差别。所以写代码时一定要以对应型号的datasheet为准不要想当然地拿其他芯片的驱动直接套。1.2 接口与关键信号MT25QL的接口信号非常精简典型的就是六根线CS_N片选、CLK时钟、DI/DQ0数据输入用于命令和写入数据、DO/DQ1数据输出用于读取数据在QSPI模式下还有WP_N/DQ2和RESET_N/DQ3。标准SPI模式下FPGA侧只需要分配四个IO即可完成全部操作这比并行的NOR Flash或NAND Flash省引脚得多。接线上有一个特别容易忽略的点WP_N引脚在标准SPI模式下不是单纯用来做写保护的它还兼职DQ2数据线。如果你的FPGA设计只工作在标准SPI模式建议在硬件上把WP_N和HOLD_NRESET_N都拉高避免它们在命令执行过程中因为电平抖动导致未知错误。在FPGA内部约束时也要注意把SPI相关的引脚放在同一个BANK并且尽量靠近这样能减少PCB走线偏差特别是后续要升级到QSPI模式时四根数据线的等长约束很重要。我实测过同样的控制器在QSPI模式下如果DQ0到DQ3的走线长度差超过200mil高频时读回来的数据偶尔会错位。2. SPI控制器核心设计2.1 四种SPI模式怎么选SPI接口有四种工作模式由CPOL时钟极性和CPHA时钟相位组合决定。MT25QL的datasheet里给出了完整的时序图支持Mode 0和Mode 3两种常用模式。Mode 0对应CPOL0、CPHA0即空闲时时钟为低电平数据在时钟上升沿采样Mode 3对应CPOL1、CPHA1空闲时时钟为高电平数据也是在上升沿采样。两者的区别只在时钟空闲时的电平状态命令时序和数据采样边沿是一致的。大部分开源代码默认使用Mode 0我在示例工程里也沿用了这个配置。但有一个需要特别留意的地方如果你的FPGA和Flash之间的时钟路径上存在反相器或经过某个缓冲实际到达Flash的时钟可能与控制器内部产生的时钟相位相反这时Mode 0会变成Mode 2导致时序完全错乱。调试时如果读ID读出来的值不对第一个要查的就是CPOL和CPHA有没有被外部电路改动过。建议在硬件设计时CLK线尽量不要经过任何会翻转相位的器件。2.2 命令帧与状态机架构MT25QL的命令帧结构可以总结为三种类型只有操作码的短命令、操作码加地址的命令、操作码加地址加数据的传输命令。例如读JEDEC ID0x9F是只有操作码的短命令读数据0x03是操作码加三字节地址再加连续数据页编程0x02是操作码加三字节地址再加最多256字节数据。控制器架构我分成两层底层是位级SPI主机负责按照时钟对协议收发每一位数据上层是命令状态机负责组装命令帧、管理超时和错误状态。这种分层的好处是底层协议基本固定后续如果从标准SPI升级到Dual SPI或Quad SPI只需要改底层发送接收逻辑上层命令层改动很小。状态机的核心跳转包括空闲、发送操作码、发送地址、发送数据或接收数据、等待状态寄存器、完成等几个状态。代码结构清晰以后排查问题能省很多时间。以下是一个简化版的SPI Master核心状态机框架实际工程中我会再把位计数、字节计数和超时计数加进去localparam IDLE 4d0; localparam SEND_OP 4d1; localparam SEND_ADDR 4d2; localparam SEND_DATA 4d3; localparam RECV_DATA 4d4; localparam WAIT_DONE 4d5; reg [3:0] spi_state; always (posedge clk or negedge rst_n) begin if (!rst_n) spi_state IDLE; else case (spi_state) IDLE: if (start_cmd) spi_state SEND_OP; SEND_OP: if (op_done) spi_state ...; // ... 根据命令类型决定是否发送地址、发送数据、接收数据 endcase end2.3 关键时序参数MT25QL的datasheet里有很多时序参数做控制器时用得到的主要有tCH时钟高电平时间、tCL时钟低电平时间、tSU数据建立时间、tH数据保持时间、tCS片选高电平最小时间。这些参数决定了SPI时钟的最高频率和命令之间的最小间隔。在简化计算时FPGA内部生成SPI时钟通常是把系统时钟分频得到。我的示例工程系统时钟是100MHzSPI时钟设置为25MHz分频系数为4。这样能在读写速度和时序稳定性之间取得平衡也方便时序约束。tCS这个参数容易被忽略它要求两次命令之间CS_N必须保持高电平至少一段时间通常是几十纳秒。如果你的状态机在连续命令之间CS_N拉高时间过短Flash可能直接不响应后续命令。解决方法是每次CS_N拉高后插入几个空时钟周期再开始下一次传输我在控制器里固定插入16个空周期完全覆盖tCS要求。3. 读写擦除的完整流程3.1 初始化与ID识别FPGA上电后第一步不是直接读数据而是先对Flash做一次JEDEC ID识别。原因很简单板子上的Flash型号可能和工程预设的不一致或者焊接有问题通过ID识别可以尽早暴露硬件问题。MT25QL的读ID命令是0x9F发送命令后Flash会返回三个字节厂商ID、存储器类型、容量代码。美光的厂商ID是0x20MT25QL128的设备ID是0xBB或0xBA不同版本略有差异容量代码表示128Mb。读ID的完整时序是拉低CS_N发送0x9F然后连续读取三个字节数据最后拉高CS_N。由于是标准SPI发送命令和读取数据都发生在同一个时钟线上FPGA侧需要在发送完命令后把数据线方向从输出切换到输入。这个切换动作在FPGA内部用三态门实现assign spi_dq0 (spi_out_en) ? spi_mosi : 1bz; assign spi_miso spi_dq1;切换时机很关键。如果切换早了可能会把命令字节的最后一个位冲掉切换晚了Flash返回的第一个数据位会被漏采。我的做法是在发送完操作码的最后一位后比正常时序晚半个SPI时钟周期再切换方向实测在25MHz下非常稳定。3.2 写使能与页编程NOR Flash的写操作和RAM完全不同它只能在把位从1变成0的方向上修改也就是说如果没有先擦除写入操作无法把0改回1。所以完整的写流程一定是“擦除”加“编程”的组合并且每次编程之前必须先发送写使能命令0x06。这是很多人第一次写Flash驱动时最不理解的地方。页编程Page Program命令是0x02最大一次可以写入256字节。这里有个大坑页编程的地址边界。如果从地址0x100开始连续写入256字节正好填满一页没问题但如果从地址0x100开始写入300字节超出当前页边界后地址会自动回绕到当前页的页首也就是说数据会写在0x100到0x1FF的范围内超出的部分覆盖掉页首已有数据。这是NOR Flash的硬性行为不是芯片的Bug但确实很容易让初学者写进去的数据和读出来的对不上。因此控制器的上层必须做跨页切分。我的实现逻辑是每次发送的字节数不超过“当前地址到下一页边界的距离”写完一部分后更新地址和剩余长度循环执行页编程命令。示例代码如下// 跨页切分伪代码 remaining width; current_addr addr; while (remaining 0) begin page_left 256 - (current_addr[7:0]); this_len (remaining page_left) ? page_left : remaining; send_page_program(current_addr, this_len, data_buf); wait_wip_clear(); current_addr current_addr this_len; remaining remaining - this_len; end这里还有一个容易忽视的点每执行一次页编程命令之后都必须等待WIP位清零才能发起下一次页编程。如果连续发送命令而不等待Flash会直接忽略后续命令。等待时间取决于芯片的页编程时间MT25QL典型值在0.4ms到3ms之间。3.3 擦除操作与状态轮询擦除命令分三种粒度4KB扇区擦除0x20、64KB块擦除0xD8、整片擦除0xC7。每次擦除之前同样需要发送写使能命令0x06。擦除完成时间的差异非常大扇区擦除典型值在100ms到400ms级别块擦除可能到1秒以上整片擦除可以达到几十秒。这些时间对于状态机来说都是“长时间等待”不能死等必须设计超时机制。等待擦除完成的方法是轮询状态寄存器。发送读状态寄存器命令0x05然后读一个字节检查bit0WIPWrite In Progress。WIP为1表示Flash还在忙为0表示空闲。轮询不能做得太频繁否则会占用大量SPI带宽也不能隔太久否则会影响写入速度。我在设计中采用“查一次等待1ms再查一次”的策略并且设置一个总超时时间比如扇区擦除超过2秒就报错。这个超时值是从Flash数据手册上的最大擦除时间加一倍余量来的。状态寄存器的另一个重要位是bit1WEL写使能锁存每次执行完页编程或擦除命令后WEL位会自动清零。如果发现命令发出去但Flash没有动作可以读一下状态寄存器确认WEL是否已经置1这是排查问题的一个有效手段。3.4 地址映射与规划工程里我单独规划了一片地址映射表把Flash分成配置区、日志区、用户数据区三个区块。配置区固定从0x000000开始大小为64KB存储设备参数日志区从0x010000开始循环写入记录运行日志用户数据区从0x100000开始给上层应用使用。地址分区的目的很简单防止不同功能模块互相覆盖数据。特别是日志区采用循环覆盖模式时如果不限制边界很容易把配置区冲掉。这个分区表虽然看起来简单但对实际产品来说至关重要。我遇到过不止一次开发阶段由于没有分区调试程序反复读写同一片区域结果固件参数和测试数据混在一起导致排查了半天才发现是数据被覆盖了。4. 工程搭建与仿真验证4.1 RTL文件组织工程目录我习惯这样组织rtl目录存放所有源代码sim目录存放仿真文件和testbench约束目录存放XDC或SDC约束文件doc目录存放datasheet和笔记。示例工程的rtl目录下主要有这几个文件spi_master.v、mt25ql_ctrl.v、flash_test_top.v。其中spi_master.v是底层SPI主机mt25ql_ctrl.v是命令状态机flash_test_top.v是顶层模块例化控制器并集成一个简单的测试逻辑。顶层模块对外接口包括系统时钟、复位、启动信号、操作类型选择、地址、写数据、读数据、忙信号、完成信号、错误标志。所有内部信号都不应该暴露到顶层这样后续集成到别的大工程时非常干净。接口数量控制在20个以内比较合理如果太多说明控制器的封装出了问题。4.2 Testbench与Flash行为模型仿真是这个工程开发效率的关键。FPGA的IO操作在仿真里通过testbench直接驱动SPI信号然后需要一个模拟MT25QL行为模型。我一开始想找现成的仿真模型后来发现Micron官网的Verilog模型用起来有点重而且接口和我的控制器不完全匹配索性自己写了一个精简版的行为模型用一个大的reg数组模拟存储空间在testbench里用task实现命令解析识别0x9F、0x03、0x02、0x20、0x05等命令并维护WIP位和擦除状态。写这个行为模型的核心价值在于它可以在没有硬件的情况下把跨页编程、状态轮询、超时处理这些逻辑全部跑通。实际测试中我通过仿真发现了一个典型问题页编程循环里没有先发写使能命令导致连续写多页时第二页开始写不进去。这个问题在仿真阶段就被抓出来省了很多上板调试时间。如果只用开发板硬调这种问题要查半天才能定位到是代码逻辑问题。4.3 约束设计与上板验证上板验证的第一步是检查时钟约束。SPI时钟25MHz由100MHz系统时钟分频产生这里有两个约束思路如果SPI时钟是内部生成的不需要为它单独建时钟约束但最好加上伪路径约束避免工具把SPI时钟当作真正的时钟树来分析如果后续用MMCM/PLL专门产生SPI时钟则必须建立正式时钟约束并且分析FPGA内部到IO引脚的路径延迟。引脚约束方面SPI四根线加片选线共五根电平标准根据硬件原理图设置常见的是LVCMOS33或LVCMOS18。这里有个细节在Xilinx Vivado的XDC文件里IO标准必须和原理图一致否则上板时IO会不工作。我吃过一次亏原理图上Flash接的是1.8V但XDC里写了LVCMOS33结果数据完全读不出来还以为是代码问题查了一整天才发现是IO电平配置错了。5. 板级调试与坑点排查5.1 读ID异常排查拿到板子后最先做的验证操作就是读ID。如果读回来的ID和0x20BB不匹配排查顺序是先看SPI时钟有没有正常翻转再看CS_N时序是否正确然后检查MOSI/MISO是否接反最后查IO电平标准。我的板级调试过程中遇到过读ID全0xFF的情况排查下来发现是MISO引脚上的上拉电阻出了问题导致FPGA侧看到的都是高电平。换成全0x00的情况则是MISO被下拉到地。如果读ID值稳定但就是不对多半是命令字节发送出错或CPOL/CPHA配置错误建议用逻辑分析仪抓一下SPI波形。逻辑分析仪在这个项目里可以说是排查利器。普通SPI信号频率不高几十块钱的逻辑分析仪就能抓得很清楚。我调试时习惯把CS_N、CLK、MOSI、MISO四根线全部接上然后按触发条件抓取一次完整的命令交互过程对比datasheet里的时序图。这个方法定位问题非常高效很多时候代码逻辑对错一眼就能看出来。5.2 写数据后读回全FF或全00这个问题的成因通常是三个方向。第一是没擦除就直接写NOR Flash在未擦除状态下读出来是0xFF写入时无法把0变成1所以读回的数据会被旧数据干扰表现为部分位不对或全FF第二是擦除命令没有正确执行比如擦除前没发写使能或者地址选错第三是页编程过程中WIP轮询失败控制器在Flash还忙时就发了下一个命令导致编程被忽略。如果写读不一致第一件事是把问题缩小先单独测试擦除擦除后全片读回来是不是全0xFF再单独测试写写固定模式如0x5A后读回来对不对。这样分步骤排查比对着代码猜要快得多。还要注意在调试擦除功能时千万不要对整片执行擦除除非你确认这块Flash上没有需要保留的数据尤其是一些开发板出厂时Flash里面放了测试程序或配置数据整片擦除之后再想恢复会比较麻烦。5.3 状态机超时与复位设计状态机长时间等待WIP清零是常见卡死点。我遇到过的情况是Flash在前一次命令超时后进入了一个未定义状态后续所有命令都不响应状态机一直轮询WIP永远等不到清零。解决方法是给状态机加超时计数超时后自动回到IDLE状态并且给出错误标志。FLASH控制器复位之后建议先执行一次软件复位命令0xF0把Flash的状态恢复到已知初始状态再做后续操作。这个命令在MT25QL的datasheet里有但很多参考代码里都没有写算是一个隐藏很深的细节。5.4 常见问题速查表现象可能原因优先排查方向读ID全0xFFMISO被上拉、Flash未上电、CS_N时序不对逻辑分析仪检查IO读ID全0x00MISO被下拉、引脚约束错误查硬件原理图读ID有值但不对CPOL/CPHA配置错、命令字节不对对照datasheet时序图写完读回全FF没有擦除或擦除失败单独测试擦除命令写完读回数据错位跨页边界没有切分检查页编程地址边界状态机卡死等待WIP超时后无处理加超时和软复位QSPI模式下数据错乱四根数据线不等长检查PCB走线和时序约束6. 进一步优化与场景扩展6.1 QSPI与更高吞吐率标准SPI模式下命令、地址、数据都在一根线上传输读数据的有效带宽很低。比如25MHz时钟下标准SPI的原始速率只有25Mbps扣掉命令和地址开销后实际吞吐会更低。MT25QL支持Quad Output Read命令0x6B在命令和地址发送完成后四条数据线全部变为输出模式理论上可以把读速度提升到四倍。FPGA实现QSPI读的难点在于数据线的方向控制。命令阶段DQ0是输入从FPGA到Flash地址阶段DQ0到DQ3都是输入数据阶段四根线全部变成输出。这个方向切换如果设计不好会出现总线冲突或数据丢失。我自己的经验是不要试图在每个字节内频繁切换方向而是把方向控制放到命令、地址、数据三个大阶段的边界上统一处理这样时序收敛容易得多。如果只是为了提高配置加载速度也可以用Xilinx的AXI Quad SPI IP它内部已经处理好了QSPI协议细节。6.2 FPGA配置与远程升级MT25QL在FPGA系统里最常见的用法是作为配置芯片。以Xilinx 7系列为例可以设置SPI x1或SPI x4模式上电时FPGA自动从片外Flash加载配置数据。这里要特别注意一个逻辑问题配置模式下Flash由FPGA硬件逻辑自动读取用户逻辑通常还没有加载所以如果同一个Flash同时存放配置文件和用户数据必须把配置文件放在Flash的前面用户数据放在后面并且严格按照厂商指定的地址范围使用避免破坏配置文件。远程升级的场景还可以利用FPGA的Multiboot功能在配置Flash的特定地址烧录一个多功能启动镜像固件运行时把新版本镜像写入Flash然后触发重新配置FPGA加载新镜像。这就对Flash的驱动提出了更高要求不仅要能读写还得保证写入过程的断电安全不能因为写入一半掉电导致整片Flash数据失效。分区设计和双备份机制在这里就显得尤为重要。6.3 与成熟IP的取舍做FPGA Flash控制器可以完全自己写RTL也可以直接用FPGA厂商提供的IP核。两者怎么选我的建议是如果是系统集成、时间紧张直接用厂商IP比如Xilinx的AXI Quad SPI它覆盖了标准SPI、Dual、Quad多种模式并且有完整的AXI接口可以和处理器总线无缝对接如果是学习目的、或者需要深度定制的协议流程自研控制器更有价值因为你完全掌控每个时钟周期的行为出错时可以精确追踪到每一条命令。自研控制器还有一个额外好处代码完全可移植。厂商IP一般绑定特定FPGA平台换到另一家FPGA厂商就要重新换IP。而自研的SPI Master和Flash命令状态机无论是Xilinx、Altera还是国产FPGA只要Verilog语法兼容代码搬过去就能用。我在项目里就靠这套自研控制器把同一份Flash驱动代码在不同FPGA平台上反复使用省了不少重复开发时间。这个工程做完之后我最大的感受是FPGA读写Flash这件事本身难点不在代码量而在细节。命令集差异、页边界处理、WIP轮询、超时复位、跨页切分每一个细节不到位最终都会以数据错误或系统卡死的方式暴露出来。如果你正在做类似功能建议先从读ID和单页读写开始把基础链路跑通再逐步加入擦除、跨页、QSPI等复杂功能。等基础功能稳定了后续加远程升级、日志存储这些业务功能都会顺很多。本文还有配套的精品资源点击获取
返回列表