NEXYS A7这块板子在学生项目和小型科研验证里几乎是“标配”,Artix-7的资源对Microblaze软核来说相当宽裕,跑一个带Cache的处理器加各种外设都不吃力。但很多人第一次在上面用Microblaze操作板载QSPI Flash时都会皱眉:往Flash里写几百KB数据,能等到人发困;读取看起来还行,可一旦数据量大、地址不连续,吞吐就掉得离谱。这次的实战项目就是围绕这个问题展开的——把NEXYS A7开发板(Xilinx Artix-7)上Microblaze核访问板载FLASH的读写性能从“能用”优化到“尽可能快”,并顺带把固化流程、启动时间一起捋顺。
如果你刚好在用Vivado/Vitis搭Microblaze,或者正被Flash下载失败、固化后不启动这类问题卡住,这篇文章的思路和代码可以直接抄作业。我会把性能瓶颈拆开讲清楚,再给出一套从IP配置、时钟调优、DMA搬运到XIP模式逐层递进的优化方案,连踩坑记录也一并附上。
1. 项目概述:为什么软核读写Flash会卡成瓶颈
1.1 这块板子上的Flash与Microblaze分工
NEXYS A7板载一颗Spansion/Infineon S25FL128S,容量128Mb(16MB),走的就是标准SPI/QSPI接口。它同时承担两件事:一是作为FPGA的配置存储器,上电时把bitstream从Flash加载进Artix-7;二是在Microblaze启动后,作为应用程序和数据的非易失存储介质。
这意味着Flash的访问路径不是专属于你的逻辑的——它挂在FPGA的专用配置引脚上,和系统启动强相关。Microblaze想读它,需要通过AXI Quad SPI控制器;想在掉电后继续运行程序,也得靠它。这个双重身份决定了你优化Flash读写时,必须清楚自己在动哪一块区域,否则一不小心把配置文件覆盖掉,板子就会“变砖”(严格说是配置失效,得重新烧录)。
1.2 优化前必须明确的三个性能指标
做性能优化第一件事不是改代码,而是先把指标定义清楚。我当时给自己定了三个可量化目标:
- 读吞吐:连续读1MB数据,统计从Flash到内存的有效带宽,单位MB/s。
- 写吞吐:写入256KB数据,包含必要的擦除等待,统计整段的平均写速度。
- 启动时间:DONE信号拉高后,从SPI Flash加载配置和应用程序到Microblaze开始执行main函数的总耗时。
这三个指标看起来独立,实际会互相影响。程序固化在Flash里,启动时就等于做一次“巨额读取”,读得快,启动自然快;而写Flash时页编程和擦除是物理上绕不开的慢操作,优化空间和数据组织方式强相关。先把指标立住,后面每次改动才有对照。
2. 瓶颈分析:你的时间到底花在哪了
2.1 从SPI总线到Flash制造工艺,先排一排链路
要优化性能,先得知道一条读写请求从CPU发出到数据落地,中间经过哪些环节。Microblaze往Flash写一个字节,数据流大致是:CPU执行写寄存器的指令 → AXI总线传输 → AXI Quad SPI控制器内部的发送FIFO → SPI移位寄存器逐位输出 → Flash芯片内部完成页编程。读则是反方向,但多一个“发命令、发地址、等Dummy周期、数据逐位返回”的过程。
这条链路里,每一级都可能成为瓶颈:
- AXI总线频率:如果AXI时钟只有50MHz,32位数据总线每次传输至少一拍,理论上限就是200MB/s,看似足够,但实际AXI交易有延迟、有等待周期,尤其是Lite接口的握手开销不小。
- SPI时钟频率:这是最直观的限制。QSPI工作在标准SPI模式时一位一位传,50MHz下理论峰值只有6.25MB/s;切到Quad模式后四线并行,同样50MHz时钟下理论峰值到25MB/s。
- Flash内部操作时间:读命令有Dummy周期和输出延迟,页编程一条命令最多写256字节,之后还要等芯片把数据写进存储阵列,这个时间在毫秒级。
- 软件搬运开销:如果每次读写都由CPU在中断里搬数据,再加上循环判断状态位,实际吞吐往往会比总线理论值再打对折。
我最初跑基准测试时,用默认的AXI Quad SPI配置、轮询方式、标准SPI模式,连续读1MB大约耗时450ms,算下来2.2MB/s;写入256KB更是花了6秒多,因为每次页编程都同步等待WIP位清0,再加上每512KB/64KB区段的擦除等待,整个流程慢得让人怀疑人生。
2.2 实测现状:轮询方式到底能跑多快
写一个简单的性能测试程序并不复杂,关键在于分别测“纯总线传输时间”和“包含Flash内部等待的总时间”。我用Xilinx的XSpi驱动直接操作SPI控制器,读的时候发一个Read命令,然后连续读数据;写的时候发Page Program命令,然后死等状态寄存器的WIP位。这也是很多入门教程的写法,代码简单、逻辑直白,但性能确实一般。
实测数据如下(SPI时钟配置为50MHz,AXI时钟100MHz):
| 操作 | 模式 | 实测速度 | 备注 |
|---|---|---|---|
| 连续读1MB | Standard SPI | 约2.2 MB/s | 命令/地址/dummy开销占比高 |
| 连续读1MB | Quad Output Fast Read | 约6.8 MB/s | 数据线4位并行,但dummy周期仍拖慢 |
| 写256KB含擦除 | Page Program | 约35 KB/s | 受页编程等待时间和擦除时间拖累 |
| 固化后启动 | - | 约800ms | 其中Flash读取bitstream占了大头 |
这个结果说明了三件事:第一,标准SPI模式读确实浪费带宽,Quad模式必须开;第二,写性能的主要瓶颈在Flash内部编程/擦除时间,而不是SPI线速;第三,软件轮询带来的额外开销同样不可忽视,需要把CPU从逐字节搬运中解放出来。
2.3 优化策略:从SPI协议、控制器配置与数据搬运三层下手
既然明确了瓶颈是“线速未用满、CPU搬运低效、Flash内部等待无法消除只能规避”,优化策略自然分成三层:
- 协议层:把标准SPI读写命令换成Quad命令,让四根数据线全部参与传输,读性能立刻翻倍。
- 工程配置层:调整AXI Quad SPI IP的FIFO深度、参考时钟频率、AXI数据位宽,让控制器本身不要成为瓶颈。
- 架构层:使用DMA做数据搬运,减少CPU介入;对于适合只读的场景,直接把QSPI控制器配成AXI4内存映射模式(XIP),让Microblaze像访问内存一样读Flash,这是读性能最彻底的提升方案。
每一层优化都建立在前面一步的基础上。物理线速没跑满时,上DMA也只是把慢速的SPI数据流搬到更快的地方,治标不治本。
3. 工具链与硬件准备:NEXYS A7环境速写
3.1 板载Flash颗粒与引脚连接
NEXYS A7-100T的核心芯片是XC7A100T-1CSG324C,板载Flash挂在一组专用引脚上,原理图里的网络名一般是FLASH_CS、FLASK_SCK、FLASH_DQ0~DQ3。这颗S25FL128S支持Standard SPI、Dual SPI和Quad SPI,命令集兼容主流QSPI Flash。
提一个很多新手容易忽略的点:Flash的IO电压和FPGA Bank电压必须匹配。NEXYS A7上这部分工作在3.3V,所以你配置引脚约束时不要随意改IO Standard,否则轻则读写异常,重则损坏器件。
3.2 Vivado/Vitis环境与Microblaze工程搭建要点
我用的工具链是Vivado 2024.2 + Vitis 2024.2。新版的Vitis把嵌入式开发整合进了统一IDE,流程稍微变了一些,但核心思路没变:Vivado里搭硬件、导出XSA,再到Vitis里建平台和应用工程。
搭建Microblaze系统时,我的习惯是先在Vivado里加一个Clocking Wizard,把板载12MHz晶振转出三路时钟:100MHz作为Microblaze和AXI总线时钟,200MHz留给DDR(如果用DDR的话),50MHz作为QSPI的参考时钟。这里有个原则:SPI参考时钟不要直接用AXI 100MHz,因为SPI控制器的分频比是固定几个档位,参考时钟太高反而难以得到合适的SPI频率,而且跨时钟域的数据同步也会引入不确定性。
Microblaze本身我建议打开指令Cache和数据Cache。对Flash性能测试来说,Cache可能会“掩盖”一部分真实Flash读速度,但实际应用就是靠Cache扛性能的,所以打开更贴近真实场景。另外,Microblaze的本地存储器大小要够放测试代码和数据缓冲,我一般给64KB以上。
3.3 把工程从“能跑”改成“方便做性能分析”
默认生成的Microblaze工程,如果只是拿来点个灯、打印串口,完全没问题。但要做性能分析,需要提前做三件事:
第一,串口打印务必加时间戳。Microblaze没有内置高精度计时器,但有一个AXI Timer,把它配成64位周期计数,频率设为CPU时钟,就能精确测出每段代码的时钟周期数。用这个做性能基准比用秒表按手机靠谱得多。
第二,测试数据和目标地址要避开程序本身使用的区域。如果你要从Flash读数据到DDR,或者往Flash写数据,务必分清楚哪个地址段是bitstream、哪个是应用代码、哪个是数据区。我的习惯是:bitstream放在Flash最前面,应用代码紧随其后,数据区单独划一个偏移段。数据区的读测试只访问数据区,写测试也只写数据区。
第三,把“擦除”和“编程”拆开计时。很多人只测一个总的写时间,发现瓶颈后不知道是擦除慢还是编程慢。我的做法是分别写3个函数:整扇区擦除、单页编程、擦除+编程连续操作,分别计时,这样才能定位到具体是哪一步慢。
4. 第一层优化:AXI Quad SPI IP配置与时钟调优
4.1 IP配置表:Standard/Quad/Quad IO模式怎么选
在Vivado里添加AXI Quad SPI IP时,配置界面有几个关键项,直接决定你能用什么命令:
| 配置项 | 可选项 | 推荐值 | 理由 |
|---|---|---|---|
| Interface Type | AXI4-Lite / AXI4(Memory-Mapped) | 先选AXI4-Lite做基准 | AXI4-Lite寄存器访问简单,适合先跑通 |
| SPI Mode | Standard / Dual / Quad | Quad | 四线同时收发,基础带宽翻倍 |
| Data Width | 2/4/8/16/32 | 32 | 与AXI总线位宽对齐,减少读写寄存器次数 |
| FIFO Depth | 16/32/64/128/256 | 256 | 减少CPU等待FIFO的空满频繁切换 |
| XIP Mode | Enable/Disable | 按需(后续章节展开) | XIP模式用于存配置、字库等只读场景 |
| Flash Model | 厂商下拉框 | Spansion S25FL128S | 驱动初始化时序更匹配 |
这里要特别解释Mode选择。很多教程让选Quad SPI,指的是数据线四根全部用于读写;但如果你继续用Read命令(0x03),数据仍然是一位一位回的,Quad只是修改了写命令的传输格式。真正让读速度翻倍的是“Quad Output Fast Read”(0x6B)或“Quad I/O Fast Read”(0xEB)这类命令。IP配置里的Quad模式,更多是告诉控制器“你的Flash支持四线操作,驱动层可以用对应命令”。
4.2 时钟树设计:AXI频率和SPI参考时钟的关系
SPI控制器的内部架构里,SPI时钟由参考时钟分频得到。参考时钟越高,能选的高SPI频率档位越多,但也不是越高越好。AXI Quad SPI IP的数据手册里有个约束:参考时钟和SPI时钟的比例必须在某个范围内,确保发送FIFO的数据能及时补上。如果比例不合适,SPI高速模式下FIFO会空转,白白浪费带宽。
我实测下来的一组稳定组合是:AXI时钟100MHz,QSPI参考时钟50MHz,SPI时钟设为参考时钟的半分频即25MHz,或者干脆让SPI时钟等于50MHz(如果参考时钟和SPI时钟比例允许)。S25FL128S在Quad模式下跑到50MHz很稳定,再高也不是不行,但板级信号质量开始吃紧,尤其要注意DQ线之间的串扰。对于追求稳定的项目,50MHz是一个性价比很高的点。
4.3 实测对比:只改配置,不改代码,性能变化
当我把AXI Quad SPI IP从默认配置调整为Quad模式、32位数据宽度、256深度FIFO,并且SPI时钟设为50MHz后,其他代码一字未动,重新跑基准测试:
- 采用Quad Output Fast Read(0x6B)命令连续读1MB,速度从2.2MB/s升到6.8MB/s。
- 采用Quad Page Program命令写数据,配合更长的FIFO,写速度从35KB/s提到48KB/s。
- 读性能提升明显,写性能提升有限,原因还是擦除和页编程等待占据了大量时间。
这一轮优化几乎不费吹灰之力,纯粹是把控制器配置用对。但它也暴露了一个问题:CPU在循环里不断读写FIFO,这种“搬运工”工作占用了大量执行周期,SPI再快也会被CPU的轮询节奏拖累。于是下一步开始动数据搬运架构。
5. 第二层优化:DMA搬运、缓存一致性与写入策略
5.1 用AXI DMA把CPU从数据搬运中解放出来
如果QSPI控制器是AXI4-Lite接口,数据搬运是典型的“CPU读寄存器→写寄存器”流程,效率确实低。更合理的做法是在系统里加一个AXI DMA,让DMA直接搬数据。这里要说明一下,AXI DMA本质是“内存到内存”或“内存到外设”的搬运器,和SPI控制器配合时,通常的做法是:CPU配置好DMA的源地址、目的地址和长度,DMA负责把数据从内存搬到SPI控制器发送FIFO,或者从SPI控制器接收FIFO搬到内存。
我用的结构是:Microblaze的AXI接口分别连接AXI DMA的控制寄存器和AXI Quad SPI,DMA的MM2S(Memory to Stream)和S2MM(Stream to Memory)通道再通过AXI Stream接到SPI控制器的数据接口。注意,这个场景下QSPI IP一般要选择AXI4(Memory-Mapped)接口类型,因为它要能响应来自DMA的读请求。
驱动层的调用也简单:
#include "xaxidma.h" #include "xil_cache.h" XAxiDma dma; // 配置DMA,源地址srcAddr,目的地址dstAddr,长度len // 发送前刷新数据缓存,确保DMA看到的不是陈旧数据 Xil_DCacheFlushRange((UINTPTR)srcAddr, len); XDma_SimpleTransfer(&dma, (UINTPTR)srcAddr, len, XDMA_MM2S_CHANNEL); XDma_SimpleTransfer(&dma, (UINTPTR)dstAddr, len, XDMA_S2MM_CHANNEL); // 等待DMA完成 while (!XDma_IsS2mmIdle(&dma)); // 接收后使无效,防止CPU读到缓存的旧值 Xil_DCacheInvalidateRange((UINTPTR)dstAddr, len);这段代码是我在实际项目里的核心骨架。第一次跑的时候我忽略了两行Cache操作,结果数据错乱,后面会专门讲这个坑。
5.2 Microblaze数据缓存的坑:为什么偶尔读到旧数据
Microblaze开启数据Cache后,CPU读内存会先查Cache,写内存也会经过Cache。这本来是为了加速,但如果DMA直接读写内存,CPU和DMA之间就存在“看到的数据不一致”的问题。
具体场景是:CPU把数据写好放在内存里,然后让DMA把这段数据搬到SPI控制器。如果数据还在CPU的Cache里没有回写到内存,DMA实际搬运的可能是旧数据,Flash里写进去的自然就是乱的。反过来,DMA从内存某地址读了一段数据放好,CPU再去读这个地址时,可能命中Cache里的旧内容,而不是DMA刚写入的新数据,于是你读回来的Flash数据永远是上一轮的。
解决办法就是前面代码里的两个API:DMA发送前用Xil_DCacheFlushRange确保数据落回内存,DMA接收完成后用Xil_DCacheInvalidateRange让Cache失效,强制从内存重新加载。这是嵌入式系统里DMA和Cache共存的通用准则,不只Microblaze,Zynq、RISC-V软核都一样,一定要记住。
5.3 写入性能优化:页编程、擦除调度与减少命令开销
Flash写入慢,很大一部分“慢”来自Flash内部操作时间。S25FL128S的页编程一次最多256字节,典型编程时间在毫秒级;擦除按扇区(4KB)或块(64KB)进行,一次扇区擦除典型时间数百毫秒。这是物理特性,没法消除,但可以通过策略优化:
第一,数据尽量按扇区边界对齐,减少跨扇区擦除次数。如果一个扇区只写了几百字节,下次写另一个区域又要整扇区擦除,白白浪费大量时间。
第二,把“擦除”和“编程”流水化。不要擦一个扇区写一个扇区,同步等待到天荒地老;可以维护一个工作队列,先擦除多个扇区,再连续编程。虽然Flash芯片不支持擦写并行,但软件层面可以减少命令切换和状态轮询的开销。
第三,页编程命令本身尽量用Quad Page Program。S25FL128S支持0x32/0x34这类Quad写命令,同样是四线并行传输,虽然页编程的等待时间不变,但命令和数据的传输时间大幅缩短。如果总数据量大,这个节省很可观。
优化后的写流程是:先发Write Enable(0x06),再发Quad Page Program命令、3字节地址、最多256字节数据,然后轮询状态寄存器等WIP位清0。这里有一个很实用的检查点:在编程之前先读一次状态寄存器,确认上一条命令已经完成;如果前一条命令还在进行中,新命令会被Flash忽略,导致“写失败但代码不报错”的隐藏问题。
6. 第三层优化:XIP模式,让Flash变成内存映射设备
6.1 XIP的工作原理与适用场景
XIP(Execute In Place)不是一个新概念,但在Softcore系统里很多人没用过。它的核心是:把AXI Quad SPI控制器配置为AXI4 Memory-Mapped接口,控制器会把Flash映射到一段连续的AXI地址空间,CPU直接通过普通的Load指令读这段地址,硬件自动帮你发Fast Read命令、处理Dummy周期、返回数据。
这意味着,读Flash不再需要你手动发起SPI传输、等FIFO、读数据寄存器,而是像读内存一样自然。配合Microblaze的数据Cache,重复访问某些数据时甚至会直接命中Cache,性能提升非常明显。
XIP模式最适合的场景是:字库、配置表、启动代码、查表数据等只读内容。我项目里把一套开机自检配置表放进了Flash映射区,Microblaze启动后不经过任何驱动,直接用指针访问,读性能几乎追平AXI内存读。
6.2 在AXI Quad SPI中开启XIP及代码切换
在Vivado里启用XIP,主要动作是把AXI Quad SPI IP的Interface Type选成AXI4(Memory-Mapped),并勾上Enable XIP。生成的地址映射里会多出一段例如0x80000000开头的区域,这就是Flash在总线上的“透明访问窗口”。
不过要注意,XIP模式并不是所有Flash命令都能自动处理,它通常固化使用Fast Read类型的读命令。如果需要写Flash,建议先把控制器切回普通模式,或者通过另一段寄存器接口发送Page Program命令。我在驱动里做了两层抽象:普通模式负责擦除和编程,XIP模式负责高速读取。二者通过同一把片选和命令逻辑切换,互不干扰。
一个容易踩的坑:XIP模式下,如果Microblaze Cache使能,第一次读到的是Flash数据没错,但如果这段时间里你用普通模式改写了同一块Flash区域,Cache里可能还是旧数据。所以,凡是涉及“用普通模式写Flash→用XIP读回新数据”的场景,记得在读之前调用Xil_DCacheInvalidateRange,把对应Cache行失效。
6.3 XIP实测结果补充:真正的“读性能天花板”
开启XIP后,读性能测试数据发生了质变。连续读1MB,只是用一个指针循环累加数据,实测速度从6.8MB/s跳到11.3MB/s。如果数据在Cache里反复命中,这个数字还能更高。
这时候读性能的瓶颈已经不再是SPI带宽,而是Flash返回数据的Dummy周期和AXI读延迟。继续往上提,需要优化的是AXI总线突发长度、Cache命中率这类系统级因素。对绝大多数应用来说,11MB/s的读取速度已经足够支撑字库加载、配置文件读取、远程升级时的Flash数据回读等场景。
到这里,Microblaze读Flash的优化基本到头了,除非你换用并行 NOR Flash 或者把数据搬到DDR里,但那已经不是“优化SPI Flash性能”的范畴。下面说说和性能强相关的固化与启动问题。
7. 固化流程与启动时间优化
7.1 Vitis 2024.2 下生成Bootimage并固化
性能优化得再好,程序固化不了等于白做。Vitis 2024.2的固化流程相比老版本有些变化,但核心三步不变:
第一步,在Vivado里导出硬件,生成XSA。注意勾选“包含bitstream”,否则后面Bootimage里缺少FPGA配置文件。
第二步,在Vitis里右键Application工程,选择生成Boot Image。Boot Image格式里通常包含两部分:FPGA配置数据(bitstream)和应用可执行文件(elf)。如果系统简单,可以不单独做FSBL,直接用Microblaze的默认启动流程。
第三步,使用Vitis的“Program Flash Memory”功能,Target选择QSPI,Image File选择生成的boot.bin,地址偏移一般设为0。烧录前确认板卡的启动模式跳线是SPI模式。
NEXYS A7上,FPGA从Flash加载配置的过程在硬件层面是由Artix-7内部的上电配置逻辑完成的,不需要Microblaze参与;Microblaze的应用代码则需要一个极小的启动引导,通常是MIT Bootloader或者直接在链接脚本里把程序放在Flash高地址,然后把可执行段加载到本地存储器或DDR运行。推荐的做法是编译时把代码段放在可执行内存,通过链接脚本控制,让固化镜像更小、启动更快。
7.2 减小启动时间的手段
启动时间由两部分组成:FPGA从Flash读取配置的硬件加载时间,以及Microblaze加载并运行应用的时间。前者和bitstream大小、配置时钟有关,通常改不了太多;后者是可以用性能优化直接受益的。
我在这个项目里试过两种缩小启动时间的手段。第一种是压缩bitstream,Vivado里勾选“Compress Bitstream”,配置数据可以缩小一半左右,FPGA配置时间明显下降。第二种是精简Microblaze应用本身,把不必要的初始化模块disable掉,例如用不到的UART、GPIO外设初始化,不链接对应的库函数;程序体积小了,从Flash加载进内存的耗时自然变短。
更激进的方案是让Microblaze直接在Flash里运行只读代码,也就是整段代码跑XIP。这样启动时完全不需要把代码从Flash拷贝到RAM,Microblaze复位后直接跳到Flash映射地址开始执行。不过这个方案对链接脚本要求较高,且代码执行速度受SPI读带宽限制——如果SPI时钟配置得不够高,反而比“拷贝到DDR跑”更慢。我最终采用了折中方案:只读数据和调用频率高的纯函数放XIP,主程序和变量区放DDR,启动时间和运行性能都兼顾。
8. 常见问题排查与避坑实录
8.1 Flash下载失败 - target dll has been cancelled 排查
这个报错在Vitis烧Flash时非常经典,尤其在你从Vivado切到Vitis、或者同时开了多个硬件服务器时容易遇到。第一次见到它,我一度以为是板子坏了,后来梳理出几个常见原因:
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| 打开Program Flash Memory后立即报该错 | JTAG链路被另一个Vivado/Vitis占用 | 关闭多余开发环境,只保留一个连接;任务管理器结束无关hw_server进程 |
| 烧录中途报该错 | USB线供电不足/接触不良 | 换双头USB线或外接电源;重新插拔JTAG线 |
| 换板子后报该错 | 板卡上电时序问题 | 先给板子上电,等FPGA DONE亮后再连Vitis烧Flash |
| 一直报该错且ID读不到 | Flash型号或配置不匹配 | 检查Target Device中的Flash型号是否选成S25FL128S,确认Quad Mode打开 |
还有一次特别坑:我把QSPI控制器的参考时钟从50MHz改成80MHz,结果烧录时“Flash download failed”。原因不是时钟频率本身,而是Vitis的Flash下载驱动对特定参考时钟下的分频配置不兼容。降到50MHz后一切正常。所以遇到诡异问题,先往“非默认配置”方向怀疑。
8.2 启动失败类:固化后不运行的几个隐藏原因
程序固化后上电不运行,是最打击人信心的环节。我的经验是按下“复位”和“掉电重启”是两回事,Microblaze的复位路径不同,需要分开排查。
最常见的问题是链接脚本错误。Microblaze应用默认链接到本地存储器的起始地址,如果你把程序固化到Flash地址偏移0x000000,而上电后Microblaze去本地存储器地址读代码,自然什么都读不到。解决办法是在链接脚本里把应用的可执行段放到启动引导约定的地址,或者用生成的Bootimage,让启动逻辑把代码从Flash搬运到内存。
第二个常见问题是启动模式跳线没拨对。NEXYS A7上电默认从QSPI Flash配置,但如果你之前用JTAG调试过,板卡可能停留在JTAG模式,掉电重启时要确认跳线位置。
第三个隐藏问题:bitstream和应用在Flash里位置重叠。如果你用了Bootimage,它内部会打包bitstream和elf,写入地址有固定约定;如果你手动分段烧录,务必确认应用地址没有覆盖bitstream。覆盖后FPGA配置加载会失败,表现出来就是上电后DONE灯不亮,连Microblaze的影子都看不到。
8.3 性能上不去的隐藏原因:不止是驱动问题
如果按前面的优化步骤做完,性能还是上不去,问题往往不在驱动代码,而在系统层面:
第一,检查是否真的用了Quad命令。很多人配置了IP的Quad模式,但驱动代码里还是用0x03标准Read命令,数据线只有一根在工作,速度自然上不去。可以在示波器上测DQ1~DQ3是否有翻转,或者直接看驱动发送的命令字节。
第二,检查SPI时钟是否真的到了设定频率。IP配置里的参考时钟和分频参数可能被综合过程优化掉,也可能因为时钟约束没加对,导致实际工作频率远低于预期。在Vivado里用Clock Interaction Report过一遍时钟域,确认没有未约束的跨时钟域路径。
第三,检查FIFO的深度和中断优先级。如果DMA方案里FIFO太浅,SPI高速连续传输时可能会因为FIFO下溢插入等待周期,吞吐掉得厉害。我一般把FIFO深度调到最大,并且把DMA中断优先级设为高于其他外设,避免高频率中断打断数据传输。
第四,也可能FLASH颗粒本身状态不佳。S25FL128S在频繁擦写后会出现坏块,虽然QSPI NOR的坏块管理比NAND简单,但强烈建议在量产项目里做一次全片检查,至少对数据区做坏块标记和地址跳过。否则某个扇区擦写时间异常长,会让整个写的平均速度看起来很难看。
最后分享一个我个人的体会:FPGA上的软核性能优化,多数时候不是靠某一个“大招”解决的,而是把协议、IP配置、数据搬运、缓存一致性这几个环节一点点抠到位。读性能从2.2MB/s到11MB/s,写性能从35KB/s到接近100KB/s,每一步优化拆开看都不复杂,组合起来效果却非常明显。如果你也在做Microblaze读写Flash相关的项目,希望这篇文章能帮你少走几段弯路。