
把NEXYS A7上Microblaze软核的板载QSPI Flash读写速度拉起来这件事我折腾了差不多两天。最开始用的是Vitis里现成的BSP驱动读1MB数据竟然要500多毫秒写1MB更是夸张一条写链路下来能等出急性子。后来从硬件IP配置一路查到驱动层把SPI时钟、Quad模式、DMA通道以及页编程的流程策略逐个调了一遍最终读性能提升了一个数量级写性能也翻了几倍。这篇就把整个优化过程、踩坑和最终能直接参考的驱动设计完整还原出来给在FPGA上跑软核并且需要频繁读写NOR Flash的朋友做个参考。1. 项目背景与优化思路拆解1.1 先看懂NEXYS A7的Flash硬件底细NEXYS A7开发板配套的FPGA根据版本不同常见是xc7a100t或者xc7a35t二者都挂了一片128Mb16MB的Quad-SPI NOR Flash具体型号大多是Micron N25Q128部分批次是ISSI IS25LP128。这片Flash通过SPI接口连接FPGA板级原理图上可以看到SCK、CSn、DQ0-DQ3共6根核心信号和Artix-7的配置Bank直接相连。N25Q128这类QSPI NOR Flash有几个和性能强相关的参数做优化前必须搞清楚参数典型值说明Page大小256B页编程的最小单位不能跨页扇区擦除4KBsub-sector erase粒度小块擦除64KBsector/block erase时间较长页编程典型时间0.3~0.7ms受工艺和VCC影响64KB块擦除典型时间0.3~1s再快的代码也得等它最大SPI时钟50~80MHz取决于型号和VCCQuad模式下通常50MHz这里的核心结论是NOR Flash的读性能受限于SPI时钟和线宽写性能受限于内部编程和擦除的物理时间。写性能那个限制是Flash器件本身的物理客观瓶颈再怎么写代码也不可能绕过而读性能则可以通过提高时钟、使用Quad模式、加DMA等手段实打实地提上去。我为什么要强调这点因为很多人一开始就把优化重点放在“修改驱动代码里的循环延时”上结果读还是慢、写还是慢。真正的原因早就不在软件层而在传输通道的配置上。NEXYS A7的Flash虽然叫“板载Flash”但默认工程里并没有把它配置到高性能状态硬件工程师如果不动IP参数Vitis自动生成的BSP也只会按最保守的路径跑。1.2 默认读写慢在哪从软核到Flash的一条完整链路Microblaze访问板载Flash的路径大致是CPU通过AXI总线访问AXI Quad SPI控制器控制器把AXI操作翻译成SPI时序再经过DQ线到达Flash。这条路里的每一个环节都可能成为性能瓶颈而且它们之间是串联关系任何一个卡住整条链路就快不了。默认Vivado创建工程时如果只是拖了个AXI Quad SPI IP并让它跑在Standard模式下SPI时钟分频通常按比较保守的配置来比如100MHz系统时钟分到十几兆甚至更低同时走的是单bit的Standard SPI协议。这个组合下的理论瓶颈就是SPI时钟本身和CPU主频完全没关系。另一个容易被忽略的点是BSP自带的xilflash和xilisf库为了移植方便大量使用寄存器轮询模式一次页编程发起后CPU就在那边等状态寄存器里的WIP位等到花都谢了。所以性能优化的全局思路我总结成四个动作把SPI时钟拉到Flash规格允许的范围内。把协议从单bit的Standard切到4bit的Quad模式。用DMA把CPU从逐字节搬运中解放出来。在软件流程上消除无谓的轮询等待和重复擦除。这四个动作从硬件到软件依次展开也对应本文后面几个章节。如果你手上不是NEXYS A7而是其他Artix-7板卡只要板载Flash是QSPI NOR这套思路同样适用只是引脚约束和Flash型号需要按实际板卡调整。2. 基线测试先把当前性能摸清楚2.1 搭建Microblaze最小测试工程在动手优化之前我搭了一个尽量简单的硬件平台做基准测试避免其他外设干扰结论。我用的是Vivado 2024.2硬件工程组成如下Microblaze软核主频100MHz开启I-Cache和D-Cache各16KB。AXI Interconnect用于连接各外设。AXI Quad SPI IP选择Quad模式基础时钟连接100MHz使能FIFO预留DMA接口。AXI UART Lite串口打印测试数据。AXI Timer作为计时基准。Processor System Reset模块和Clock模块。这里有一个特别值得注意的点AXI Quad SPI IP在配置时有两种模式选项Standard SPI和Quad SPI它们对应的IP引脚数量不同。如果一开始选了Standard后面即使软件里发Quad命令也是白搭因为DQ2/DQ3引脚根本没引出来。所以从一开始就要选Quad模式不要图省事。约束文件方面参考NEXYS A7的官方master XDC即可重点是把QSPI的SCK、CS、DQ0到DQ3约束到固定引脚上并且添加适当的IOSTANDARD。如果不清楚具体引脚去Digilent官方的约束文件里搜QSPI全部抄过来就好。这里的小坑是有些板卡Flash的CS引脚命名和XDC里的名字不完全一致遇到编译报错就逐个对照原理图。2.2 用C代码测出优化前的真实数据硬件工程导出到Vitis之后我写了一段计时测试代码。核心逻辑很直接先发RDID0x9F确认Flash在位然后分别做四件测量读4KB、写4KB、擦除64KB块、擦除后再读回校验。计时用XTime精度换算成毫秒输出。这里给出一个简化的测试函数骨架#include xil_printf.h #include xil_time.h #include xqspips.h #include xqspips_hw.h static XQspiPs QspiInstance; int FlashReadID(u8 *id) { u8 cmd[4] {0x9F, 0x00, 0x00, 0x00}; u8 rx[4] {0}; XQspiPs_Transfer(QspiInstance, cmd, rx, 4); id[0] rx[1]; id[1] rx[2]; id[2] rx[3]; return 0; } u32 ReadTest(u32 addr, u8 *buf, u32 len) { // 通过读命令将Flash内容读到buf // 包括CS拉低、发送Read命令、地址、连续读len字节、CS拉高 return len; } u32 WriteTest(u32 addr, u8 *buf, u32 len) { // 按256B页循环Write Enable - Page Program - Wait WIP return len; } void BaselineTest(void) { u32 tStart, tEnd, diff; u8 rd[256], wr[256]; memset(wr, 0xA5, sizeof(wr)); XTime_GetTime(tStart); WriteTest(0x000000, wr, 4096); XTime_GetTime(tEnd); diff (tEnd - tStart) / (COUNTS_PER_SECOND / 1000); xil_printf(Write 4KB: %d ms\r\n, diff); XTime_GetTime(tStart); ReadTest(0x000000, rd, 4096); XTime_GetTime(tEnd); diff (tEnd - tStart) / (COUNTS_PER_SECOND / 1000); xil_printf(Read 4KB: %d ms\r\n, diff); }注意这里的ReadTest和WriteTest是示意性伪代码真实工程里还需要把XQspiPs的配置、CS控制、命令字以及状态等待全部补全。重点是计时方法用同一个时钟源在不同优化阶段跑同一段数据量这样对比才有说服力。我实际测得基线数据大概如下操作数据量实测耗时折合吞吐顺序读1MB约350ms约2.9MB/s页编程写1MB约7.8s约130KB/s64KB块擦除64KB约0.9s-这组数据就是后续所有优化的起点。看到这里大家应该能明白当时读1MB数据要300多毫秒确实不符合对“板载Flash”的直觉预期所以必须动手改。3. 瓶颈定位一条链路逐个环节查3.1 Flash控制器端时钟、位宽与FIFO的连锁影响先看控制器侧。在AXI Quad SPI IP里SCK时钟由AXI Full时钟我这里设的是100MHz经过分频寄存器得到。当BSP默认把分频设置成8分频甚至16分频时SCK就只有6.25MHz到12.5MHz。如果你再配合Standard模式那每个SCK周期只在DQ0上传1bit数据1MB数据需要约800万个SCK周期仅传输状态这一层的开销就决定了它不可能快。换成Quad模式之后每个SCK周期最多可以传4bit如果同时把SCK提高到50MHz理论上读吞吐就是25MB/s量级。这是一个巨大的变化也是我后来实测读性能能够从2.9MB/s拉到20MB/s以上的直接原因。FIFO深度的影响也不容小觑。AXI Quad SPI IP默认的FIFO是16字节当CPU不使用DMA、只靠PIO模式读写时每填满16字节就要做一次AXI总线访问如果还要在命令模式下逐字节处理CPU会被频繁打断。这时候访问8KB甚至更大的数据块总线上的交互次数非常可观驱动层哪怕只是多几行循环判断累计起来都是肉眼可见的延时。关键计算以25MB/s目标反推50MHz SCK、Quad模式下每个字节需要2个SCK周期4bit/周期即传输1字节耗时0.04us要喂饱这个速度CPU至少每0.64us要往FIFO塞16字节。如果CPU一边在等状态位一边还要搬运数据很容易就掉速。所以控制器侧的优化组合拳必须是Quad模式加高SCK再加DMA。3.2 软件驱动端命令模式下的轮询与等待再来看软件侧。默认BSP的Flash驱动整体上是面向通用性的它要确保各种Flash型号都能跑于是命令处理上非常保守每个页编程操作都严格经历一遍发送Write Enable0x06。发送Page Program0x02带地址和数据。不断读取Status Register0x05检查WIP位直到编程完成。这个流程本身没有错但问题出在第三步。Flash页编程的典型时间是0.3到0.7ms在等待期间CPU没有任何其他可做的工作只能死等。写1MB数据至少要组合2018次页编程假设每次等待平均0.5ms那光是等WIP就要1秒。加上每页之间的命令帧开销和CS翻转最终测到的130KB/s也就说得通了。类似的问题在擦除时更明显。64KB块擦除的典型时间是0.3到1s驱动在等待擦除完成时会把整个CPU挂住。如果业务里需要频繁擦写这种同步等待的设计在响应性上完全不可接受。软件侧优化其实只有几个方向一是减少等待点比如把多个页编程命令连续下发后用一次长轮询来代替每页轮询二是把等待时间藏到后台去比如一边擦除一边让CPU去做别的事三是用DMA直接搬运数据块减少CPU的逐字节介入。这三者在实际工程里可以组合使用但不建议一上来就全上先量化再改出了问题也好定位。3.3 各因素影响量化为了让优化顺序更清晰我单独隔离了不同因素做了对比测试配置组合读速率说明Standard 12.5MHz1.5MB/s最保守配置Standard 50MHz6.2MB/s频率提升4倍Quad 50MHz22MB/s位宽提升4倍Quad DMA 50MHz21~22MB/s接近物理上限注意Quad加DMA和Quad不加DMA的读速率接近是因为SPI通道已经接近物理极限DMA的主要优势其实是释放CPU占用。如果是做状态机复杂的实时系统CPU占用率降低带来的价值比那零点几MB/s的吞吐提升重要得多。写操作方面DMA带来的差异更多体现在命令帧间隙的利用效率上后面会展开。4. 核心优化方案与实操实现4.1 第一步把SPI时钟和Quad模式打开这个改动在硬件层和驱动代码层都要做。硬件层在Block Design里双击AXI Quad SPI IP把Mode改成Quad SPI并且把参考时钟选成100MHz。片选数保持默认1个即可。注意如果IP配置界面里没有显示DMA相关选项需要勾选Enable DMA Interface这会暴露一个AXI Stream接口用来和AXI DMA连接。如果后续想用DMA方式做大批量读写这一步不能省。如果是纯PIO方式也可以不连DMA但改完Quad模式后需要重新生成Bitstream并导出硬件配置到Vitis。驱动层初始化时用XQspiPs_SetClkPrescaler把分频系数设置到合适档位。比如想让SCK接近50MHz在100MHz输入下分频2即可得到50MHz。不同驱动库对分频寄存器的叫法略有差异关键是理解这个换算关系实际SCK等于AXI时钟除以分频系数。初始化核心代码int QspiInit(void) { XQspiPs_Config *cfg; cfg XQspiPs_LookupConfig(XPAR_AXI_QUAD_SPI_0_DEVICE_ID); if (!cfg) return -1; XQspiPs_CfgInitialize(QspiInstance, cfg, cfg-BaseAddress); // 设置为主模式、8bit帧长度 u32 options XQSPIPS_MASTER_MODE | XQSPIPS_FRAME_SIZE_8_BITS; XQspiPs_SetOptions(QspiInstance, options); // SCK分频100MHz / 2 50MHz XQspiPs_SetClkPrescaler(QspiInstance, XQSPIPS_CLK_PRESCALE_2); // 使能 XQspiPs_Enable(QspiInstance); return 0; }改完这一项后顺序读性能已经能从2.9MB/s跳到10MB/s左右。Quad模式下数据线从1根变成4根读吞吐理论上翻4倍。这里唯一的坑是Flash本身是否支持Quad读命令N25Q128和IS25LP128都是支持的所以不需要额外担心。但如果你用的是老款Flash或者兼容型号建议先查一下数据手册里的命令表。4.2 第二步用DMA把CPU从传输中解放出来当数据量超过几十KB级别PIO模式的瓶颈就从SPI时钟转移到了CPU搬运速度。这时候需要启用DMA。在Block Design中AXI Quad SPI的QSPI_DMA接口连接到一个AXI DMA IPMM2S通道往SPI FIFO喂数据S2MM通道从SPI FIFO取数据。接着在Vitis中同时使用XQspiPs和XQspiDma两个驱动。需要说明的是AXI Quad SPI内部集成的DMA接口本质上是AXI Stream必须经由外部AXI DMA做Stream到Memory Map的转换所以Block Design里不能少这个IP。DMA读的大致流程int QspiDmaRead(u32 addr, u8 *buf, u32 len) { // 1. 构造Flash读命令和地址先通过XQspiPs发送到FIFO // 2. 启动S2MM通道让AXI DMA接收来自SPI FIFO的数据 // 3. 等待DMA完成中断或轮询完成标志 // 4. 返回前对buf执行Cache Invalidate确保CPU读到最新的DMA数据 }DMA写的大致流程int QspiDmaWrite(u32 addr, u8 *buf, u32 len) { // 1. 对buf执行Cache Flush把DDR里的数据刷到内存一致域 // 2. 以256B为单位构造页编程命令每页一次Write Enable // 3. MM2S通道把数据送入SPI FIFO控制器负责发出 // 4. 等待页编程完成再处理下一页 }注意DMA本身不会让Flash页编程物理时间变短但它能把CPU死等WIP和数据搬运的阻塞感降下来。在一个处理多任务的系统里DMA完成后用回调通知应用层应用层就可以利用等待时间去处理别的事情。另外Microblaze带有D-Cache时DMA和CPU之间的cache一致性问题容易被忽视。DMA写入内存后CPU缓存的旧数据未必自动失效CPU写好DMA源缓冲后也要主动把脏数据刷出去。对应的接口分别是Xil_DCacheInvalidateRange和Xil_DCacheFlushRange这两个调用在Vitis的xil_cache.h里都有务必在每次DMA传输前后调用。启用DMA和Cache维护之后大块顺序读可以稳定跑到18到22MB/s顺序写也能到300KB/s以上。这个阶段已经比基线强了非常多。4.3 第三步重排擦除与写入流程NOR Flash有一个特性任何改写都需要先把目标位置擦除成全0xFF。如果业务逻辑动不动就先全片擦除再重新写入整份数据性能一定很难看。我的做法是根据实际使用模式做三类优化。第一类是写前检查。每次写数据前先把目标区域读回来判断是否已经是0xFF。如果本来就是0xFF就直接页编程跳过擦除。这个检查在读性能已经优化到位后成本很低但能省下大量不必要的擦除时间尤其适用配置参数频繁更新的场景。第二类是扇区轮转。对于日志型或者配置型数据不要每次都在固定地址写而是设计一个轮转队列写入新版本时追加到新的扇区旧扇区标记为废弃等到整个队列用完了再集中擦除最旧的一批扇区。这种设计可以把擦除操作摊薄到极低的频率上代价是多写一点管理逻辑。简单说就是让Flash的各个扇区轮流被使用避免一直擦同一个扇区也能稍微延长Flash寿命。第三类是合并大块擦除。如果需要更新的是连续的多块64KB区域尽量一次性发完擦除命令再等避免擦一下、等一下的碎步操作。每条擦除命令之间的CS翻转和命令字开销虽然不大但累计起来也相当可观。写流程重组的核心逻辑可以简化如下int FlashWriteOptimized(u32 addr, u8 *buf, u32 len) { // 每256B页循环处理 for (u32 off 0; off len; off 256) { u32 pageAddr addr off; // 读回当前页判断是否需要擦除 u8 tmp[256]; ReadPage(pageAddr, tmp); if (!IsAllFF(tmp)) { EraseSectorContaining(pageAddr); // 按页所在扇区擦除4KB } // 写使能 页编程 WriteEnable(); PageProgram(pageAddr, buf off, 256); WaitWipClear(); } return 0; }实际工程里还会遇到一个问题数据长度不一定是256B的整数倍最后一页不足256B时必须保持该页剩余字节为0xFF再写入否则会把旧数据搞坏。这也是页编程里最容易踩的坑之一。处理办法是先读回目标页到临时缓冲修改对应偏移处的数据再把整个256B写回去。4.4 优化后的驱动接口与实测结果经过以上三步改造我把驱动封装成了四个接口FlashRead、FlashWrite、FlashErase、FlashEraseAndWrite。上层业务只需要关心地址和数据不再关心SPI模式、DMA和擦除策略。接口设计上注意几点所有接口的地址参数都按字节地址传递底层自己换算成Flash命令地址缓冲指针要按4字节对齐方便DMA搬运长度参数尽量按256B对齐避免内部做太多拆分。实测结果如下操作优化前优化后提升倍数顺序读 1MB约350ms约45ms约8倍页编程写 1MB约7.8s约2.4s约3倍64KB块擦除约0.9s约0.65s约1.4倍1MB连续写入(含擦除)约15s约4.5s约3.3倍读性能的提升主要来自Quad模式和50MHz SCK这部分直接逼近了N25Q128在SDR模式下的读能力。写性能的提升则来自DMA减少了命令帧之间的CPU空转加上写前检查避免了大量无谓擦除。而擦除本身的时间是由Flash物理特性决定的再怎么优化也只是把等待和开销藏起来不可能从物理层面超越。这里的数字不同板卡会有出入但提升趋势是确定的。如果你的读性能还在个位数MB/s徘徊优先查SPI时钟和模式如果写性能在100KB/s附近挣扎优先查页编程等待和擦除策略。5. 常见问题与排查技巧实录5.1 Flash ID读取失败或识别为错误颗粒优化过程中最先发作的问题就是上电后RDID读回来全0xFF或者乱码。排查路径一般是从易到难检查SPI初始时钟是否过高。部分Flash在刚上电时无法承受高频时钟驱动初始化阶段要先以较低频率发RDID等识别出型号再切换高频。我的代码里有一段是从分频16开始发RDID成功读回ID后再切换分频2这个细节很关键。检查CS引脚控制时序。Flash的命令帧对CS的电平翻转有严格时序要求比如命令字节后面接高阻态数据读回期间CS必须持续拉低任何多余的CS抖动都可能导致命令被丢弃。检查板级连接。NEXYS A7的Flash走线在板内基本不用怀疑信号质量但如果你改过外围电路或者Flash插座接触不良RDID偶发失败就是第一个信号。另外N25Q128的RDID通常返回0x20 0xBA 0x18这类格式而IS25LP128返回0x9D 0x60 0x18如果代码里硬编码了某一种型号的判断识别为错误颗粒很正常。处理办法是只检测容量字节和厂商ID对具体型号做兼容而不是强区分。5.2 时钟频率提升后读写不稳定、偶发数据错位把SCK从25MHz拉到50MHz甚至更高后会遇到两类问题。一类是信号质量问题NEXYS A7板载走线本身设计良好问题不大但如果你改了外部负载或者测试飞线就可能出现偶发bit错误另一类是Flash规格超限比如某些老型号N25Q128在3.3V VCC下最高只能跑50MHz硬拉到80MHz导致时序裕量不足。排查时建议先降回25MHz确认问题消失再用逻辑分析仪抓一下SCK和DQ线上的时序重点看建立时间和保持时间是否达标。如果条件限制没有分析仪就保守一点读操作跑到50MHz写操作控制在25MHz因为写时序对噪声更敏感。实测下来大部分不稳定读错位问题把SCK降到45MHz左右就消失了没必要非在50MHz的极限边缘试探。5.3 Vitis固化Flash时报error: flash download failed - target dll has been cancelled这个问题做Microblaze固化时经常遇到。Vitis通过JTAG把ELF或MCS写到Flash期间如果JTAG链路被打断或者进程异常就会弹出这个错误。我遇到的主要原因有三种硬件服务器hw_server与Vitis之间的连接被上一次未关闭的调试会话占用。解决办法是把Vitis里的Run Configurations全部结束必要时从系统进程里杀掉残留的hw_server再重新连接设备。Flash写保护了。部分板卡的QSPI Flash有非易失的WP位第一次用时需要先读状态寄存器确认Block Protection。如果不小心被保护下载就会失败。可以用状态寄存器操作解除保护后再试。启动镜像或存储格式不匹配。Vitis 2024.2中对固化文件的格式、地址和boot header都有要求如果指定了错误的分区表或者镜像地址也会触发同样报错。建议先由Vitis自动生成cfgmem再手动微调地址。固化前还有个小技巧先擦除整颗Flash再写。如果你的板子Flash容量不大16MB全片擦除一次不过几秒能避免不少写入时遇到旧数据的怪问题。尤其是从旧镜像切换到新镜像时不擦干净往往会出现校验失败。5.4 常见问题速查表现象可能原因解决思路RDID返回全FFSPI时钟过高/CS异常降低初始频率检查CS读数据偶发错位SCK超频、信号质量差降频、检查走线、加延时写后读回不一致页编程跨页或剩余字节未填FF按256B拆页处理擦除时间明显变长电压不足或Flash老化测量供电交叉验证型号固化失败target dll cancelledJTAG占用、Flash保护、镜像格式不对清理进程、解除保护、检查地址性能始终上不去仍是Standard模式/低频重查IP配置和分频6. 优化效果总结与扩展建议6.1 优化前后完整回顾回顾整个项目优化核心其实并不是某一段奇技淫巧的代码而是把SPI时钟、Quad位宽、DMA搬运、擦除策略这四个环节逐个扶正。这个过程也让我意识到嵌入式系统的性能优化永远要先找到物理瓶颈再决定软件怎么改。盲目优化轮询代码往往费劲不讨好。从我个人的体会来说几个改动里性价比最高的是SPI时钟和Quad模式因为它们直接改变了传输通道的上限DMA次之它解决的是CPU介入问题擦除策略是最需要和业务绑定的但它带来的系统体验提升反而是最大的。6.2 后续扩展方向如果项目对Flash性能还有更高要求可以考虑两个方向。一是启用Flash的XIPExecute In Place能力让Microblaze从Flash直接取指令执行省去上电搬运到DDR的过程这也会让顺序读性能有质的飞跃。二是叠加一个轻量文件系统比如LittleFS或SPIFFS把裸的地址读写转换为文件操作让扇区管理、磨损均衡和掉电保护都交给成熟的文件系统库。对于需要做OTA升级或者长期存日志的朋友还可以考虑把双bank Flash的切换考虑进去后台写新程序到另一个bank等校验完毕再原子切换启动标志这个方案比单纯追求单次写速度更能提升整体可用性。最后再分享一个小经验如果在你的项目里Flash读性能优化到位后依然不够用别急着继续压榨SPI时钟回头看看业务是不是把太多高频数据放在NOR Flash上了。NOR Flash的定位是代码存储和低频配置真正的高频数据应该走片外DDR或者SD卡盲目用Flash硬扛高频读写再优化也扛不住物理天花板。把合适的数据放到合适的存储介质里才是性能优化的终极答案。