简介:本资源是面向FPGA开发初学者与嵌入式硬件工程师的MT25QL系列SPI Flash读写实战工程,聚焦于高速可靠存储接口的底层驱动实现与验证。工程基于Xilinx Vivado 2018.3平台构建,完整实现了50MHz输入时钟经PLL倍频至80MHz后,以115200波特率通过UART交互完成Flash扇区级写入与回读验证——发送单字节即自动写入起始地址连续256字节并原样回传,便于快速闭环调试。压缩包含329个文件(10.56MB),涵盖28个Verilog核心逻辑文件、7个XDC管脚约束文件、27个说明文本、14个Tcl自动化脚本及ILA调试配置等,关键代码均附中文注释;目录结构清晰,FLASH驱动模块已封装为独立IP,读/写数据端口标准化,可直接对接用户自定义控制逻辑,移植性强。目前已有164人学习下载,适合用于课程设计、毕业设计或工业现场存储模块快速原型开发。 做FPGA的工程师,迟早要和FLASH打交道。我这次用Verilog在Vivado里写了一个读写MT25QL FLASH的完整示例工程,从SPI控制器到命令状态机全部自己实现,没有用厂商现成的IP核,把读写擦除三个操作在工程里全部跑通。这篇文章就把这个工程从设计思路到具体实现、再到调试踩坑的过程完整拆解出来,给正在被NOR FLASH时序折腾的同行一个直接能抄作业的参考。
先说下为什么选MT25QL。这芯片是Micron的QSPI NOR FLASH,常见型号有MT25QL128、MT25QL256,容量从128Mb到256Mb不等,3.3V供电,单芯片16MB的容量用来存FPGA配置、存固件、存数据记录都够用。相比I2C EEPROM,它容量大得多;相比NAND,它接口简单、读写随机访问友好,而且Xilinx和Intel的FPGA配置芯片标准方案里到处能看到它的影子。这个工程我以MT25QL128为例,地址24位,页大小256字节,擦除最小单位是4KB扇区。
1. 项目背景与整体设计思路
1.1 为什么FPGA要和NOR FLASH打交道
FPGA本身是SRAM工艺,掉电就丢配置,这决定了它必须外挂一颗非易失存储来放bit文件。上电后FPGA通过SPI接口把配置文件读进来,完成加载,这是NOR FLASH最典型的应用场景。但实际项目里,FLASH的用途远不止存配置:有些系统需要在线升级固件,上位机把新版本bit文件下发到FLASH,下次启动加载新版本;有些信号处理板需要FPGA本地记录一些关键数据,比如参数表、校准系数、运行日志;还有些系统用FLASH做数据缓存,断电后数据不丢。
正是因为这些需求,FPGA和FLASH之间的读写操作就成了一个绕不开的基础能力。你可以靠厂商的配置功能被动地烧写,但要做在线升级、数据记录这种事,必须自己设计一套主动读写FLASH的逻辑。
1.2 方案选型:为什么选MT25QL而不是其他FLASH
选芯片的时候我对比过几个方案。NAND FLASH容量大价格低,但管理机制复杂,坏块、ECC、块擦除这些处理逻辑在FPGA里实现起来非常痛苦,除非是做SSD或者大容量存储卡这种场景,否则杀鸡用牛刀。I2C EEPROM倒是简单,但容量做到几Mbit就到头了,存个配置文件都紧巴巴,更别说存数据日志。SPI NOR FLASH正好卡在中间:容量从几Mbit到几百Mbit都有,接口只有6根线,随机读快,擦写逻辑也直观,最适合FPGA工程里做配置存储、固件备份、数据记录这些任务。
选了SPI NOR之后具体挑哪颗,我最终定的MT25QL128,理由是:第一,Micron这颗芯片的命令集和时序都非常规范,数据手册写的清晰,网上参考资料也多;第二,它被大量Xilinx开发板用作配置芯片,硬件上可以直接复用配置电路,原理图都不用改;第三,它兼容JEDEC标准命令集,如果哪天要换国产替代或者换其他品牌,Verilog代码里改个命令码就行,迁移成本低。当然如果你用的是Intel平台,对应的W25Q系列命令集也类似,这段代码稍加修改也能用。
1.3 系统架构和接口定义
整个工程我按三个层次来设计:最底层是SPI控制器,负责把字节变成SCK/SI/SO时序,这是所有操作的基础;中间层是FLASH命令层,把读、写、擦除、状态查询这些命令按照数据手册的时序组织成SPI字节序列;最上层是用户访问接口,给外部逻辑提供类似读寄存器、写缓冲区、启动操作这样的调用方式。
这样的分层架构有个明显好处:SPI控制器不关心你发的是什么命令,它只管按时钟把字节发出去、收回来;命令层不关心数据从哪来、到哪去,它只管按协议把命令码、地址、数据串起来;用户接口层不关心FLASH内部状态,它只管接收指令、反馈状态。哪一层出错直接看哪一层,调试的时候思路非常清楚。
用户接口我设计得很简单,一组请求信号加上一组数据线:
// 用户接口信号 input wire user_cmd_valid // 命令请求有效 input wire [2:0] user_cmd_type // 命令类型:1-读 2-写 3-擦除 input wire [23:0] user_cmd_addr // 目标地址 input wire [7:0] user_cmd_data_in // 写入数据 output wire [7:0] user_cmd_data_out // 读出数据 output wire user_cmd_busy // 忙标志 output wire user_cmd_done // 命令完成这套接口做出来之后,上层逻辑要读数据就拉高valid、给地址和命令类型,然后等done信号;要写数据就往data_in上按节拍送数据。接口宽度可以根据实际需求扩展成8位、16位或者32位,我这里是8位匹配FLASH的字节结构。
2. 关键模块拆解与设计细节
2.1 SPI控制器的设计:模式选型与字节传输
MT25QL支持标准SPI、Dual SPI和Quad SPI三种模式,我这个工程先以标准SPI模式为基础,把所有命令跑通。SPI的四种工作模式通过CPOL(时钟极性)和CPHA(时钟相位)区分,MT25QL的数据手册明确要求工作在模式0,也就是CPOL=0、CPHA=0:空闲时SCK为低电平,数据在SCK上升沿被采样,下降沿更新。
这个细节值得多说一句,因为我见过不少人在FPGA里写SPI控制器时想当然地以为上升沿发送、下降沿接收,结果时序对不上,读回来的数据全是乱的。MT25QL的约定是:FPGA作为主机,在SCK的下降沿把数据放到SI线上,FLASH在SCK的上升沿采样;反过来FLASH在SCK下降沿把数据放到SO线上,FPGA在SCK上升沿采样。所以统一用上升沿采样、下降沿更新的做法就不会错。
SCK的频率我这里设成25MHz,这对MT25QL来说是绰绰有余的,标准Read命令最高支持50MHz,Fast Read可以到133MHz,但FPGA内部逻辑跑25MHz意味着SPI控制器只需要一个简单的计数器分频就能产生SCK,时序约束也宽松。后面如果要提速换用更高频率,直接调分频系数就行,控制器结构不用改。
字节传输状态机我把每个字节的8个bit拆成独立状态,这样每一拍干了什么看得清清楚楚:
// 发送一字节 SPI控制器核心状态 localparam S_IDLE = 0, S_BIT = 1, S_DONE = 2; always @(posedge clk) begin case (spi_state) S_IDLE: if (spi_start) begin spi_bit_cnt <= 3'd7; spi_state <= S_BIT; end S_BIT: begin // 下降沿更新数据 if (!sck_reg) begin spi_data_out <= spi_buffer[spi_bit_cnt]; spi_bit_cnt <= spi_bit_cnt - 1'b1; if (spi_bit_cnt == 0) spi_state <= S_DONE; end end S_DONE: begin sck_en <= 1'b0; // 拉高片选前停止时钟 spi_done <= 1'b1; spi_state <= S_IDLE; end endcase end这个状态机里的数据移位逻辑我调整了几次才稳定,核心在于必须保证SCK的下降沿和数据更新严格同步。很多人在FPGA里做SPI都是直接在系统时钟沿更新数据,SCK从同一个寄存器打一拍出来,这样做虽然能跑,但时序上会引入不确定的延迟。我后来改成了先产生SCK的使能信号、再做数据移位的方案,实际上是把SCK的每个有效沿当作独立的节拍来处理,整个时序就干净多了。
2.2 FLASH命令层设计:为什么写操作必须先发写使能
MT25QL的命令集看起来一大堆,但基础操作总结下来就几个核心命令。我在命令层统一维护一个命令寄存器,根据用户请求自动生成完整的命令序列,包括命令码、地址字节和数据字节。
常用的命令码需要直接背下来:
| 命令功能 | 命令码 | 命令格式 |
|---|---|---|
| Read Data | 0x03 | 命令码 + 3字节地址 + 数据输出 |
| Fast Read | 0x0B | 命令码 + 3字节地址 + 1字节Dummy + 数据输出 |
| Page Program | 0x02 | 命令码 + 3字节地址 + 数据输入 |
| Sector Erase (4KB) | 0x20 | 命令码 + 3字节地址 |
| Block Erase (64KB) | 0xD8 | 命令码 + 3字节地址 |
| Write Enable | 0x06 | 命令码 |
| Read Status Register | 0x05 | 命令码 + 数据输出 |
这里必须重点强调Write Enable命令。MT25QL有个写保护机制:每次成功执行编程或者擦除命令之后,内部的状态寄存器写使能锁存位WEL会自动清零,下一次操作之前必须重新发Write Enable命令,否则FLASH会直接忽略编程和擦除请求。也就是说,每次写操作都得按这个固定套路来:Write Enable(0x06)→ Page Program(0x02)→ 轮询状态寄存器直到WIP清零,三步缺一不可。
初次做这个工程的时候,我写了第一版代码后给FLASH写数据,读回来全是对应的值,没任何错误,但就是写不进去。排查了大半天,最后打印状态寄存器发现WEL位一直是0,才意识到少发了写使能。这个坑非常典型,凡是用了SPI NOR FLASH的人几乎都踩过。
2.3 用户访问接口:让上层逻辑能干净地调用
用户访问接口的设计直接决定了这个工程好不好用。我遇到过一些参考设计,把SPI控制器和FLASH命令揉在一起,上层逻辑直接对着SPI信号操作,读一个字节要自己拉低片选、自己拼地址、自己数时钟,用起来极其痛苦。这个工程我特意把用户接口做得干净一点,核心就是一个命令状态机加一组寄存器。
接口时序约定成类似简单总线的风格:用户拉高valid表示发起操作,给出命令类型和地址,然后等busy拉低、done拉高。对于写操作,需要额外把待写数据按字节送进来;对于读操作,FLASH返回的数据在done的时候同时出现在data_out总线上。
命令状态机内部把整个操作拆成几个阶段:
localparam IDLE = 4'd0; localparam WREN = 4'd1; // 写使能 localparam SEND_ADDR = 4'd2; // 发送地址 localparam PROGRAM = 4'd3; // 编程阶段 localparam ERASE = 4'd4; // 擦除阶段 localparam READ_DATA = 4'd5; // 读数据阶段 localparam READ_STAT = 4'd6; // 读状态寄存器 localparam CHECK_WIP = 4'd7; // 等待WIP清零 localparam DONE = 4'd8; // 完成这套接口设计好之后,上层逻辑只需要关心三个信号:发起命令、等待完成、取数据,完全不感知底层的SPI波形和FLASH时序。我后来在这套接口基础上做了DMA式的连续读、连续写,只需要在接口层加一个计数器自动递增地址,外部逻辑甚至不用管每个页的边界问题。
3. 核心状态机与读写时序实现
3.1 全局状态机如何把多个命令串起来
FLASH操作和简单的SPI字节收发最大的区别在于操作是有“阶段”的,每个阶段持续的时间还不一样。比如读操作是命令码加地址后马上就有数据流返回;页编程是命令码加地址加数据发完之后还要等内部写入完成,这个时间可能长达几毫秒;扇区擦除更夸张,发送命令之后要等几十毫秒甚至几百毫秒。全局状态机的作用就是把这些阶段有序地组织起来,保证每个阶段的时序都符合数据手册要求。
全局状态机我设计成上图那种结构:每一条命令路径都是一个有限状态集合,不同路径之间通过IDLE状态切换。以编程操作为例,完整的路径是IDLE→WREN→SEND_ADDR→PROGRAM→READ_STAT→CHECK_WIP→DONE,其中CHECK_WIP状态会一直停留,直到FLASH状态寄存器的WIP位清零。
这个状态机在实现上有个细节很考验人:状态跳转的时机必须严格对齐SPI字节传输结束事件。我刚开始做的时候用了两个always块,一个管状态跳转,一个管SPI字节计数,结果总是出现状态已经跳走了但SPI还没收完当前字节的情况。后来统一改成事件驱动的方式,SPI控制器在每完成一个字节传输时产生一个done脉冲,全局状态机只在这个脉冲到来时跳转,整个时序就稳定了。
3.2 读操作时序:0x03和0x0B和0x6B怎么选
读操作是FLASH用到最频繁的功能,MT25QL提供了多个读命令,最主要的区别在于频率和引脚数。0x03标准读,最高支持50MHz,一个数据引脚输出;0x0B快速读,最高支持133MHz,需要额外的8个Dummy时钟周期;0x6B是Quad Output快速读,最高支持133MHz,四个数据引脚同时输出,吞吐量提升四倍。
我这个示例工程先实现了0x03标准读,因为它的时序最简单,适合把框架跑通。读操作的具体波形是:主机拉低CS,发送命令码0x03,紧接着发送24位地址,之后FLASH会在每个SCK时钟沿从SO引脚输出对应地址的数据,地址自动递增。注意地址发送是高字节在前,也就是A23..A16、A15..A8、A7..A0的顺序,这个顺序写反了读回来的数据位置全对不上。
如果你要做高速读,0x0B必须加Dummy周期。Dummy周期的含义是命令码和地址发完之后,FLASH需要几个时钟周期来准备数据,这段时间主机只给时钟不收数据。0x6B的Quad模式则要把单向输出的SO改成双向的IO0到IO3,FPGA侧的引脚方向要跟着切换,这个比标准SPI复杂不少,我会在后面专门讲。
3.3 页编程操作与页边界问题的处理
写操作是让无数新手栽跟头的地方。MT25QL的Page Program命令一次最多写入256字节,而且是按页对齐的:如果你跨越页边界连续写,数据不会自动换页,而是回卷覆盖到当前页起始地址,这可能把之前写好的数据冲掉。
避免这个问题的办法很简单:写数据之前先检查当前地址加上本次写入长度是否跨页,如果跨了,就拆成两次编程,或者限制每次单次编程的字节数不超过当前页剩余空间。我工程里实现了一个自动拆分逻辑,用户只需要给出起始地址和缓冲区,命令状态机自动判断是否跨页,跨页就分两次操作,对上层完全透明。
页编程的完整时序流程是:
- 发送Write Enable命令,置位WEL
- 发送Page Program命令码0x02
- 发送24位起始地址
- 连续发送数据字节,最多256字节
- 拉高CS,触发内部编程
- 轮询状态寄存器直到WIP清零
- 检查WEL是否已经自动清零,确认本次编程完成
编程过程中如果CS拉高得太早,数据没有完全写入;拉得太晚,可能多写进了额外的字节。这个时序窗口在数据手册里叫tPP,典型值0.4ms,但在FPGA实现里不需要精确计时,轮询WIP才是可靠的完成判断方式。
3.4 扇区擦除与状态轮询的实现原理
NOR FLASH的特性决定了写操作只能把1写成0,要把0恢复成1只能靠擦除。MT25QL支持4KB扇区擦除、64KB块擦除和整片擦除,分别对应命令码0x20、0xD8和0xC7。我这个工程把4KB扇区擦除作为标准操作来实现,因为它是批量擦除的最小粒度,做数据记录的场合最常见。
擦除的流程也是先写使能、再发擦除命令、最后等WIP清零,但等待时间比编程长得多:4KB扇区擦除典型时间是40ms,最大可达400ms。这就带来了一个设计问题:如果FPGA在擦除期间干等着,整个系统就卡住了。我在接口层增加了一个busy状态和中断标志,擦除期间上层逻辑可以去干别的事,等busy拉低再发下一条命令。
状态轮询的实现方式也有讲究。最基本的做法是发Read Status Register命令,然后把状态寄存器的bit0(WIP)拉回来,为1说明还在忙,为0说明空闲。注意状态寄存器的数据是在命令码发完之后的下一个字节,而且FLASH会持续输出状态寄存器的值,所以如果你一直给时钟、不拉高CS,就可以连续读状态。我的实现是一次轮询只读一个字节就拉高CS,然后重新发命令,虽然效率不是最高,但逻辑清晰、不容易出错。
4. 时序约束与上板调试要点
4.1 FPGA工程里的时钟和引脚约束
FPGA做SPI控制器最大的坑是SCK产生方式导致的时序违例。如果SCK不是从系统时钟分频来的,而是用组合逻辑直接生成的,那它在FPGA内部走的是通用布线资源,延迟不确定,在高速SPI下很容易产生毛刺。正确的做法有两种:一是用ODDR原语,把SCK映射到FPGA的专用输出时钟引脚;二是在逻辑内部产生很高的SCK使能信号,用IOB寄存器输出。我工程里选的是第一种方式,用ODDR产生SCK,这样SCK的质量有保障。
引脚约束方面,要注意FLASH的WP_N和HOLD_N两个引脚。在标准SPI模式下,这两个引脚必须接高电平,否则FLASH会处于写保护或者暂停状态。FPGA侧如果你没有用这两个引脚,记得在约束文件里把它们设置成输出高电平,或者外部电路直接上拉,我遇到过几次因为HOLD_N引脚悬空导致FLASH偶发不响应的情况,排查到最后都是这个原因。
时钟约束在Vivado里我是这样写的:
create_clock -name system_clk -period 10.0 [get_ports clk] set_output_delay -clock [get_clocks system_clk] -max 4.0 [get_ports {spi_sck spi_si spi_cs}] set_output_delay -clock [get_clocks system_clk] -min 1.0 [get_ports {spi_sck spi_si spi_cs}] set_input_delay -clock [get_clocks system_clk] -max 4.0 [get_ports spi_so] set_input_delay -clock [get_clocks system_clk] -min 1.0 [get_ports spi_so]这些delay值不是拍脑袋写的,要参考MT25QL数据手册里的时序参数:SCK到数据输出的时钟沿偏斜、数据建立保持时间要求。算的时候把FPGA芯片内部走线延迟、PCB走线延迟都估算进去,留出余量。实际调试时如果发现SPI在某些温度频率下不稳定,优先回头检查这些约束是不是给得太极限了。
4.2 仿真验证怎么搭:模型仿真还是直接看波形
FPGA逻辑写完一定先在仿真环境里验证,再上板。MT25QL在Xilinx Vivado里有现成的仿真模型,但我不太喜欢直接调库,而是自己写了一个精简的FLASH行为模型,只模拟了本例程用到的几个命令:Read、Page Program、Sector Erase和Read Status。好处是仿真速度极快,而且模型里可以加打印信息,方便观察每一条命令的状态。
仿真验证的核心是覆盖各种边界情况:跨页边界的写操作、地址对齐的读操作、擦除超时、状态寄存器轮询次数等等。我专门构造了地址从页末尾开始写8字节的场景,验证自动拆分逻辑是否正确;又构造了相邻扇区连续擦除的场景,验证状态机不会互相干扰。
仿真通过之后再上板,上板调试要养成看信号的习惯。FPGA内部逻辑的信号可以在ILA(Integrated Logic Analyzer)里抓,我一般抓CS、SCK、命令状态机状态和数据总线这几个信号,配合SystemILA的触发功能,在写操作发起时抓一段完整的波形,对照数据手册逐拍检查。用这种方法我定位过芯片选错、地址错位、时序不足等好几个问题。
4.3 板上实测结果和性能评估
整个工程在我手头一块Artix-7开发板上跑通了,FLASH型号是MT25QL128ABA。实测下来几个数据我心里有数:标准读模式下,25MHz时钟,吞吐率大约3.1MB/s;页编程256字节,实测耗时约0.5ms,吞吐率大约0.5MB/s;扇区擦除4KB,实测耗时约60ms,符合数据手册典型参数。这些数字对配置存储和数据记录完全够用,但如果要做大量数据吞吐,建议升级到Quad读和Quad写模式,吞吐率能再翻几倍。
从写完代码到稳定运行,期间大大小小修了十几个问题,最大的感悟是:SPI FLASH看起来简单,但真正要把读写擦除都做到可靠,需要逐个命令地抠时序细节。接下来我把调试过程中遇到的高频问题整理出来,这些几乎都是新手必踩的坑。
5. 常见问题与排查技巧实录
5.1 写数据失败:WEL位没有置位
现象是写完数据后读回全是0xFF,或者直接读状态寄存器发现WEL一直是0。这个问题的根源必然是漏发了Write Enable命令。FLASH编程和擦除命令执行完成之后,内部会自动清除WEL位,如果你提前把WEL当成常态条件,下一次操作就会失败。
排查技巧:在每一条写路径上都检查一下WEL位,如果WEL为0就补发Write Enable,再执行编程。更稳健的做法是在编程前无条件发一次Write Enable,不要依赖上一次的状态。我最终的代码就采用了这种方式,省心很多。
5.2 读回来数据错位:地址字节顺序搞反
现象是读数据本身能读出来,但数据的位置跟预期不一致,比如读0x000000地址却出0x000001的内容。这种问题几乎都是地址字节发送顺序的问题。MT25QL是高位地址先发,也就是A23..A16在最前面,这和很多工程师从低字节开始发的直觉正好相反。
排查技巧:用逻辑分析仪抓SCK和SI,把发送的字节逐个解码出来,跟命令格式对照,一眼就能看出地址字节顺序对不对。另外常见的混淆是把命令码和地址拼在一个字节流里时位序搞反,SPI本身是MSB first,这个也容易在移位逻辑中写错。
5.3 擦除完成判断不准确:WIP轮询逻辑有缺陷
现象是擦除命令发完后立即读FLASH,返回旧数据,过一会儿又变成新数据,看起来好像“慢半拍”。这是没有做WIP轮询导致的。擦除操作是异步的,命令发完不等于擦除完成,只有状态寄存器的WIP位从1变成0才是真正的完成标志。
正确做法是擦除命令发完之后,循环执行“发送0x05命令→读回状态字节→检查bit0”,直到bit0为0才返回完成。而且轮询时要保证每次读状态寄存器的CS拉低和拉高时序完整,否则FLASH状态不对,读出来的数据没意义。
5.4 QSPI模式下IO方向切换导致的数据冲突
这个工程标准SPI跑通之后,我升级到Quad模式时碰到了IO冲突的问题。Quad模式下IO0到IO3都是双向引脚,命令和地址阶段是输入方向,数据阶段要切换成输出方向。如果方向切换时序没处理好,数据引脚上会出现驱动冲突,读回来的数据全是乱的。
解决办法是在命令状态机里增加方向控制信号,严格按“命令发送完成后的第一个时钟沿切换方向”的规则来。因为FPGA内部逻辑和引脚之间还有延迟,方向切换要提前一拍,否则切换完成后第一个数据采样可能是无效的。这个坑在Xilinx官方文档里也有提到,做QSPI绕不开。
5.5 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 写数据后读回全FF | 没发Write Enable | 检查WEL位,编程前补发0x06 |
| 读数据位置错位 | 地址字节顺序发反 | 抓SI波形,核对高字节在前 |
| 写数据丢失后半段 | 跨页边界未处理 | 判断地址加长度是否超页,拆分编程 |
| 擦除后数据还是旧的 | 没有轮询WIP位 | 发送0x05循环读状态直到bit0为0 |
| Quad读数据乱 | IO方向切换冲突 | 检查方向信号时序,预留切换延迟 |
| FLASH偶发不响应 | WP/HOLD引脚电平不对 | 确认两引脚接高或者FPGA输出高 |
6. 一些经验总结和扩展方向
6.1 自己写FLASH控制器和用IP核怎么选
关于SPI FLASH控制器到底应该自己写还是用厂商IP核,我的看法是这样:如果你的项目时间紧、只求能用,直接用Xilinx和Intel的Quad SPI IP核,他们帮你处理了绝大多数时序细节,配置界面里勾一下就行;但如果你想深入理解FLASH的工作原理,或者你的FLASH型号和IP核的默认配置差异比较大,又或者你需要对时序做特别优化,那自己写一遍是非常值得的。
自己写的最大的好处是,出了任何问题你都能从原理层面去分析,而不是面对一个黑盒。比如IP核报错了你不知道是命令发错了还是时序不满足,但自己写的控制器,每个状态的波形都是自己设计的,问题定位快得多。这个工程花了一个周末从零写完,换来的经验在后续项目里反复用得上。
6.2 如何从标准SPI扩展成QSPI模式
如果要在现有工程上做QSPI扩展,我的建议是先把标准SPI模式下的读写擦除全部跑稳定,再一项项加能力。第一步改成Dual模式,把SO变成双向的IO1;第二步改成Quad模式,把SI也解放出来变成IO0,再补上IO2和IO3。每一步都是增量改动,出问题时能很快定位是新加的引脚还是时序有问题。
在现有工程上做QSPI扩展时,重点改动三个地方:命令层要能发送0x6B和0x32这类Quad命令;SPI控制器要增加引脚方向切换逻辑;顶层模块的引脚约束要重新定义双向总线。其他部分不用大动,全局状态机的框架完全可以复用。
6.3 这个工程后续还能怎么玩
工程写好之后,我在几个地方做了扩展。第一个是用它做在线升级,上位机通过串口把新的bit文件分包发下来,FPGA收到后写入一个专门的分区,写完后设置启动标志,下次上电从新分区加载。第二个是用它做数据记录,因为MT25QL的随机读很快,可以把采样数据先缓存到DDR,然后按页批量写入FLASH,实现类似数据记录仪的方案。第三个是把FLASH当作大容量参数存储,把一堆标定参数按地址索引组织存储,上电时一次性读入内部寄存器。
这些扩展方向本质上都是在用户访问接口上做文章,底层的FLASH控制器不需要改。所以我把这层接口设计得越干净,后面做扩展就越省力。这也是我对这个工程最满意的地方。
本文还有配套的精品资源,点击获取