QSPI协议 - 超越XIP:在内存映射、四线模式与DMA协同中压榨极致性能

该文章同步至OneChan

当四线并行、内存映射与DMA交织,我们如何平衡带宽、延迟与系统资源,实现极致吞吐?

导火索:一个QSPI Flash的"高速"读取性能瓶颈

在一个高性能嵌入式系统中,使用QSPI Flash存储程序代码和数据。系统设计时预期通过内存映射模式实现快速的XIP执行,但实际测试发现:

  1. 在内存映射模式下,CPU直接读取QSPI Flash的速度仅为理论带宽的30%
  2. 启用缓存后性能有所提升,但偶尔会出现数据一致性问题
  3. 使用DMA从QSPI Flash拷贝数据到RAM时,带宽利用率也不到50%

逻辑分析仪捕获的波形显示,在连续读取数据时,QSPI接口在每读取256字节后会有大约2μs的停顿。进一步分析发现,这是因为Flash内部页编程和读取的页边界限制,以及QSPI控制器在跨页时需要重新发送命令序列。

矛盾点在于:QSPI硬件提供了四线并行和高达133MHz的时钟频率,理论上可以提供532Mbps的带宽(4线×133MHz)。但实际性能受限于Flash本身的特性、QSPI控制器的配置、系统总线的竞争以及软件访问模式。要榨取极致性能,必须深入理解每一层的细节。

第一性原理:重新审视四线SPI与内存映射

设计的演进:从单线SPI到四线QSPI

传统SPI使用两条数据线(MOSI和MISO)实现全双工,但实际读操作中,MOSI线只用于发送命令和地址,大部分时间处于未充分利用状态。QSPI通过增加数据线数量,将读数据的带宽提高4倍。

复制代码
单线SPI读取序列:
命令(8位) + 地址(24位) + 哑元周期 + 数据(多位,每时钟1位)

四线QSPI读取序列(标准):
命令(1线, 8位) + 地址(4线, 24位) + 哑元周期 + 数据(4线, 每时钟4位)

四线QSPI读取序列(快速):
命令(4线, 8位) + 地址(4线, 24位) + 哑元周期 + 数据(4线, 每时钟4位)

关键洞察

  1. 命令阶段:可以单线发送,也可以四线发送。四线发送命令需要Flash支持,且命令本身需要设计为四线兼容(通常命令字节被重复4次)。

  2. 地址阶段:通常使用四线发送,每个时钟周期传输4位地址,24位地址只需6个时钟。

  3. 哑元周期:由于Flash从接收到地址到准备好数据需要一定时间(tACC),这段时间用哑元时钟填充。哑元周期数取决于Flash的延迟和QSPI时钟频率。

  4. 数据阶段:四线传输,每个时钟周期传输4位数据。

内存映射模式的实现原理

内存映射模式是QSPI最吸引人的特性之一。它将外部Flash映射到处理器的地址空间,使得CPU可以直接通过加载指令(如LDR)读取Flash内容,而无需软件干预。

硬件支持:QSPI控制器内部有一个地址转换器,当CPU访问特定的内存区域(如0x9000_0000)时,QSPI控制器自动将访问转换为QSPI读事务。

转换过程

复制代码
CPU发起读访问(地址0x9000_1234)
↓
QSPI控制器截获该访问
↓
将地址0x9000_1234转换为Flash偏移0x1234
↓
构造QSPI读命令序列:命令 + 地址(0x1234) + 哑元 + 数据
↓
通过QSPI接口从Flash读取数据
↓
将数据返回给CPU

优势:代码可以像在内部Flash一样在外部Flash中运行(XIP),无需先拷贝到RAM。

挑战:每次访问都有固定的开销(命令、地址、哑元),并且访问是随机的,无法利用突发读取的优化。此外,缓存对性能影响巨大。

性能模型:理论带宽 vs. 实际带宽

理论带宽计算

假设QSPI时钟为100MHz,四线数据,则理论带宽为:

复制代码
100MHz × 4位/时钟 = 400Mbps = 50MB/s

实际带宽模型

实际带宽受限于每次读事务的开销和数据传输时间。

复制代码
总时间 = 命令时间 + 地址时间 + 哑元时间 + 数据时间
命令时间:命令位数 / (线数 × 时钟频率)
地址时间:地址位数 / (线数 × 时钟频率)
哑元时间:哑元周期数 / 时钟频率
数据时间:数据位数 / (线数 × 时钟频率)

示例:四线模式,100MHz,命令8位(单线),地址24位(四线),哑元周期8,读取256字节(2048位)数据。

复制代码
命令时间 = 8位 / (1线 × 100MHz) = 80ns
地址时间 = 24位 / (4线 × 100MHz) = 60ns
哑元时间 = 8周期 / 100MHz = 80ns
数据时间 = 2048位 / (4线 × 100MHz) = 5120ns
总时间 = 80+60+80+5120 = 5340ns
有效数据 = 256字节
实际带宽 = 256字节 / 5340ns ≈ 47.9MB/s
效率 = 47.9 / 50 = 95.8%

但上述计算假设了连续读取,且没有考虑跨页边界、缓存未命中、总线竞争等因素。实际效率会低得多。

性能陷阱:QSPI系统的五大瓶颈

瓶颈一:Flash内部的页边界限制

大多数QSPI Flash内部被划分为页(通常256或512字节)。连续读操作不能跨越页边界,否则需要额外的延迟。

问题:当读取操作跨越页边界时,Flash内部需要重新定位到下一页,可能导致额外的延迟(通常几微秒)。

解决方案

  1. 软件避免跨页访问,或者将跨页访问拆分为两次。
  2. 使用QSPI控制器的自动递增地址功能,让硬件处理页边界。

瓶颈二:命令序列的配置不当

QSPI控制器需要正确配置命令序列,包括命令、地址宽度、数据模式、哑元周期等。配置不当会导致性能下降或通信失败。

常见错误

  • 哑元周期设置过小,数据尚未准备好就采样,导致读取错误。
  • 哑元周期设置过大,增加不必要的延迟。
  • 没有使能四线模式,仍然使用单线模式传输数据。

优化方法:根据Flash数据手册和实际时钟频率计算最佳哑元周期。有些QSPI控制器支持自动检测Flash所需哑元周期。

瓶颈三:内存映射模式下的缓存效应

内存映射模式下,CPU通过缓存访问QSPI Flash。缓存行的大小通常为32字节。每次缓存未命中,QSPI控制器会发起一次读事务。但缓存行读事务可能不是最优的。

示例:缓存行32字节,但QSPI控制器可能只支持最大8字节的突发读取。为了读取32字节,需要4次8字节的读取,每次都有命令、地址、哑元的开销。

优化

  1. 配置QSPI控制器支持更大的突发长度(如64字节)。
  2. 使用预取指功能,提前读取后续数据。

瓶颈四:系统总线竞争

QSPI控制器通过总线(如AXI)连接到内存系统,与CPU、DMA等共享带宽。当多个主机同时访问时,QSPI的访问可能被阻塞。

影响:在内存映射模式下,CPU读取QSPI Flash时,如果总线被其他主机占用,则读取延迟增加,导致CPU停顿。

优化

  1. 提高QSPI控制器的总线优先级。
  2. 使用缓存减少对QSPI的访问次数。

瓶颈五:软件访问模式

软件以何种方式访问QSPI Flash对性能影响巨大。随机访问和顺序访问的性能差异可达10倍以上。

顺序访问:可以利用Flash的连续读模式,只需发送一次命令和地址,然后连续读取数据。

随机访问:每次读取都需要完整的命令序列,开销巨大。

优化:尽量将数据顺序存储,并顺序访问。使用DMA进行大块数据搬运,减少CPU干预。

超越XIP:高级优化技巧

技巧一:精确计算哑元周期

哑元周期必须足够长,以覆盖Flash的访问时间(tACC)。tACC通常与电压、温度有关,且在不同的时钟频率下,所需的哑元周期数不同。

计算公式

复制代码
所需哑元周期数 = ceil( tACC × 时钟频率 ) - 固定延迟

其中固定延迟包括命令和地址的传输时间。

示例 :tACC = 40ns,时钟频率 = 100MHz,命令8位单线(80ns),地址24位四线(60ns)。

则从命令开始到数据准备好需要40ns,但命令和地址已经用了140ns,所以理论上不需要哑元周期。但实际上,由于内部流水线,可能仍需要少量哑元周期。

实践:通过实验确定最小哑元周期。从最大值开始递减,直到读取错误,然后增加1-2个周期作为余量。

技巧二:配置QSPI控制器的寄存器

以STM32的QSPI为例,需要配置多个寄存器以实现最佳性能。

c 复制代码
// 示例:STM32 QSPI初始化代码片段
void QSPI_Init(void) {
    // 1. 配置时钟分频,得到100MHz时钟
    QUADSPI->DCR |= (0 << QUADSPI_DCR_CKMODE_Pos); // 模式0,时钟空闲为低
    QUADSPI->CR |= (0 << QUADSPI_CR_PRESCALER_Pos); // 不分频,输入时钟为100MHz则输出100MHz
    
    // 2. 配置Flash大小(地址位数)
    QUADSPI->DCR |= (23 << QUADSPI_DCR_FSIZE_Pos); // 24位地址,2^24=16MB
    
    // 3. 配置采样边缘
    QUADSPI->CR |= (1 << QUADSPI_CR_SAMPLE_SHIFTING_Pos); // 在时钟后半周期采样,提高时序余量
    
    // 4. 配置双闪存模式(如果使用两个Flash并行)
    // QUADSPI->CR |= (1 << QUADSPI_CR_DFM_Pos); // 使能双闪存模式,数据线变为8条
    
    // 5. 配置中断和DMA
    QUADSPI->CR |= (1 << QUADSPI_CR_TCIE_Pos); // 使能传输完成中断
    // 使能DMA
    QUADSPI->CR |= (1 << QUADSPI_CR_DMAEN_Pos);
}

技巧三:内存映射模式的配置与优化

内存映射模式需要正确配置存储器映射的区域和访问属性。

c 复制代码
// 配置内存映射模式
void QSPI_EnableMemoryMappedMode(void) {
    // 1. 配置读命令序列
    QUADSPI->CCR =
        (0xEB << QUADSPI_CCR_INSTRUCTION_Pos) | // 快速读四线命令,0xEB
        (3 << QUADSPI_CCR_ADDRESSMODE_Pos) |    // 24位地址
        (3 << QUADSPI_CCR_ADDRESSIZE_Pos) |     // 四线地址
        (0 << QUADSPI_CCR_ABMODE_Pos) |         // 无交替字节
        (3 << QUADSPI_CCR_ABMODE_Pos) |         // 四线数据
        (6 << QUADSPI_CCR_DCYC_Pos) |           // 哑元周期=6
        (1 << QUADSPI_CCR_DMODE_Pos);           // 四线数据
    
    // 2. 设置内存映射模式
    QUADSPI->CCR |= (1 << QUADSPI_CCR_FMODE_Pos); // 内存映射模式
    
    // 3. 使能QSPI
    QUADSPI->CR |= (1 << QUADSPI_CR_EN_Pos);
    
    // 4. 在MPU中配置该区域为可缓存、可预取
    MPU_Config(0x90000000, 16*1024*1024, MPU_REGION_ENABLE | MPU_REGION_CACHEABLE | MPU_REGION_BUFFERABLE);
}

注意:内存映射区域通常配置为可缓存(Cacheable)和可缓冲(Bufferable),以提高性能。但需要处理缓存一致性问题。当QSPI Flash的内容被DMA或另一个处理器更新时,必须清洗缓存。

技巧四:DMA与QSPI的协同

DMA可以用于在QSPI Flash和RAM之间搬运数据,减轻CPU负担。但需要仔细配置DMA和QSPI的握手。

c 复制代码
// 使用DMA从QSPI Flash读取数据到RAM
void QSPI_DMA_Read(uint32_t flash_addr, uint8_t *ram_addr, uint32_t size) {
    // 1. 配置DMA
    DMA_InitTypeDef dma_init;
    dma_init.Channel = DMA_CHANNEL_3;
    dma_init.Direction = DMA_PERIPH_TO_MEMORY;
    dma_init.PeriphInc = DMA_PINC_DISABLE;
    dma_init.MemInc = DMA_MINC_ENABLE;
    dma_init.PeriphDataAlignment = DMA_PDATAALIGN_WORD; // QSPI数据寄存器为32位
    dma_init.MemDataAlignment = DMA_MDATAALIGN_WORD;
    dma_init.Mode = DMA_NORMAL;
    dma_init.Priority = DMA_PRIORITY_HIGH;
    HAL_DMA_Init(&dma_init);
    
    // 2. 配置QSPI为间接读模式
    QUADSPI->CCR =
        (0xEB << QUADSPI_CCR_INSTRUCTION_Pos) | // 快速读四线命令
        (3 << QUADSPI_CCR_ADDRESSMODE_Pos) |    // 24位地址
        (3 << QUADSPI_CCR_ADDRESSIZE_Pos) |     // 四线地址
        (3 << QUADSPI_CCR_DMODE_Pos) |          // 四线数据
        (6 << QUADSPI_CCR_DCYC_Pos);            // 哑元周期
    QUADSPI->DLR = size - 1; // 设置数据长度(字节数-1)
    QUADSPI->AR = flash_addr; // 设置地址
    
    // 3. 启动DMA
    HAL_DMA_Start(&hdma, (uint32_t)&QUADSPI->DR, (uint32_t)ram_addr, size/4);
    
    // 4. 启动QSPI传输
    QUADSPI->CCR |= (1 << QUADSPI_CCR_FMODE_Pos); // 间接读模式
    QUADSPI->CR |= (1 << QUADSPI_CR_EN_Pos);
    
    // 5. 等待DMA完成
    HAL_DMA_PollForTransfer(&hdma, HAL_DMA_FULL_TRANSFER, 1000);
}

DMA配置要点

  • QSPI的数据寄存器通常是32位的,因此DMA的传输宽度应设置为字(32位)。
  • 数据长度需要根据字节数换算为字数。
  • 在启动DMA后再启动QSPI传输,避免数据丢失。

技巧五:双闪存模式(Dual Flash)的配置

对于更大的存储需求,可以使用两个QSPI Flash,分别连接到QSPI控制器的两组数据线。在双闪存模式下,数据线扩展到8条,每个时钟周期可以传输8位数据。

接线方式

复制代码
Flash1: IO0, IO1, IO2, IO3
Flash2: IO4, IO5, IO6, IO7

配置

c 复制代码
// 使能双闪存模式
QUADSPI->CR |= (1 << QUADSPI_CR_DFM_Pos);

// 配置Flash1和Flash2的片选
// 通常通过地址的最高位来选择:A24=0选择Flash1,A24=1选择Flash2
// 因此,两个Flash的地址空间是连续的,总容量为32MB(每个16MB)

性能提升:理论上,双闪存模式可以将带宽提高一倍。但需要注意,两个Flash必须具有相同的型号和特性,且需要同时进行相同的操作。

QSPI系统设计检查清单(10条)

1. 时钟配置检查

问题 :QSPI时钟频率是否在Flash支持范围内?是否考虑了信号完整性?
验证 :测量SCK信号,确保边沿陡峭,过冲小。
检查点:时钟频率 ≤ Flash最大频率,走线长度匹配,终端电阻适当。

2. 命令序列配置

问题 :命令序列(命令、地址、哑元、数据)的线数和时序是否正确?
验证 :用逻辑分析仪捕获波形,对比Flash数据手册。
检查点:命令码正确,地址位数匹配,哑元周期足够,数据线数正确。

3. 内存映射配置

问题 :内存映射区域是否配置了正确的缓存和缓冲属性?
验证 :测试随机访问和顺序访问的性能,对比缓存开启和关闭的情况。
检查点:MPU/MMU配置正确,缓存策略合理(通常Write-Back, Read-Allocate)。

4. 哑元周期优化

问题 :哑元周期是否针对当前时钟频率和电压温度优化?
验证 :在不同温度和电压下测试读取稳定性。
检查点:哑元周期在极端条件下仍能稳定工作,且不过大。

5. 跨页处理

问题 :软件或硬件是否处理了Flash的页边界?
验证 :测试跨页读取,检查是否有性能下降或错误。
检查点:QSPI控制器支持自动递增地址,或软件将跨页访问拆分。

6. DMA配置

问题 :DMA与QSPI的握手是否正确?传输效率如何?
验证 :测量DMA传输大量数据的时间和CPU占用率。
检查点:DMA传输完成中断正常,无数据丢失,带宽利用率>80%。

7. 总线竞争管理

问题 :QSPI控制器在总线上的优先级是否足够?
验证 :在总线高负载下测试QSPI访问延迟。
检查点:QSPI控制器优先级高于其他主机,或使用带宽预留。

8. 双闪存同步

问题 :双闪存模式下,两个Flash的特性是否匹配?片选逻辑是否正确?
验证 :分别测试每个Flash,然后测试同时访问。
检查点:两个Flash型号相同,时序一致,片选信号正确。

9. 功耗管理

问题 :QSPI接口的功耗是否在预算内?是否在低功耗模式下正确关闭?
验证 :测量不同工作频率下的电流消耗。
检查点:在睡眠模式下禁用QSPI时钟,动态调整时钟频率。

10. 错误处理

问题 :是否检测和处理QSPI传输错误?是否有重试机制?
验证 :模拟传输错误(如断开数据线),检查系统响应。
检查点:错误标志被检测,有重试机制,重试次数可配置。

总结:在带宽、延迟与复杂度之间权衡

QSPI协议通过增加数据线数量、支持内存映射和DMA,为外部Flash提供了高性能的访问接口。然而,真正的极致性能来自于对每一层细节的深入理解和优化:

  1. 硬件层面:信号完整性、终端匹配、时钟频率。
  2. 配置层面:命令序列、哑元周期、缓存策略。
  3. 架构层面:内存映射、DMA使用、总线竞争。
  4. 软件层面:访问模式、错误处理、低功耗管理。

QSPI的性能优化是一个系统工程,需要硬件工程师、驱动开发者和系统架构师紧密合作。只有将Flash特性、控制器配置、系统架构和软件访问模式综合考虑,才能榨取出QSPI接口的每一分性能。

当您下次设计基于QSPI的系统时,请记住:四线并行带来的不仅是带宽的提升,还有时序复杂度的增加。内存映射提供了便利,但也引入了缓存一致性的挑战。DMA减轻了CPU负担,但需要仔细配置握手。在追求极致性能的路上,每一个环节都不可或缺。


思考题:在您的QSPI应用中,是否遇到了性能瓶颈?您是如何分析并解决的?是调整了哑元周期,还是优化了软件访问模式?

下篇预告:接下来我们将探讨eSPI协议。在《LPC的继承者:在引脚节约、虚拟外设与安全启动间重塑板级架构》中,我们将揭示:eSPI如何用更少的引脚实现更强的功能?虚拟外设如何改变主板设计?安全启动如何集成?以及eSPI在取代传统LPC接口时面临的兼容性挑战。

相关推荐
FreakStudio2 小时前
MicroPython对接大模型:uopenai + 火山方舟实现文字聊天和图片理解
python·单片机·ai·嵌入式·面向对象·电子diy
Sss_Ass2 小时前
跟着老师不迷路系列---跟着李述铜老师学习汇编语言之基本汇编程序符号绑定语句
学习·嵌入式·汇编语言·李述铜·符号绑定语句
Z文的博客7 小时前
SLCAN工程搭建与实现教程(下)
stm32·单片机·嵌入式·can
Jason_zhao_MR9 小时前
STM32MP135F安全芯引入!米尔MYD-YF13X系统、安全、功能三重升级
stm32·嵌入式硬件·安全·嵌入式
LN花开富贵21 小时前
【ROS】鱼香ROS2学习笔记二
linux·笔记·python·学习·嵌入式
济6171 天前
FreeRTOS 通信任务设计(3)---基于状态机的串口协议帧解析
stm32·嵌入式·freertos
CinzWS1 天前
电源管理(上):动态功耗管理与时钟门控——ARMv8的“省电魔法“
嵌入式·芯片验证·原型验证·a53
济6171 天前
FreeRTOS 通信任务设计(4终)----从字节流到有效帧的完美闭环
stm32·嵌入式·freertos
Hello_Embed1 天前
嵌入式上位机开发入门(二十四):Paho MQTT 嵌入式客户端源码分析
网络·单片机·网络协议·tcp/ip·嵌入式