
W25Q128 这颗 128Mbit 的 SPI NOR Flash 在嵌入式圈子里算是老熟人了价格便宜、供货稳定、资料齐全几乎每个做过 STM32 项目的人都在自己的板子上焊过一颗。但绝大多数人对它的用法还停留在SPI 模式、单线、几十兆时钟这个层面读写速度撑死也就几 MB/s跑个 FatFs 文件系统或者存点字库图片还凑合一旦涉及音频流、图像缓存、固件在线升级这类对带宽敏感的场景就明显感觉力不从心。其实 W25Q128 本身支持 Quad SPI四线 SPI配合 STM32H7 系列内置的 QUADSPI 外设理论带宽可以轻松做到单线模式的 4 倍以上。问题在于很多人翻完参考手册和数据手册之后还是不知道从哪下手——引脚怎么接、模式怎么配、内存映射怎么开、DMA 怎么挂、为什么开了 QSPI 反而读出来全是 0xFF。我自己在这个组合上前后折腾过好几版硬件和固件踩过的坑足够写一篇长文所以这次把 W25Q128 在 STM32H7 上的 QSPI 高速读写完整梳理一遍从硬件连线到寄存器配置从间接模式到内存映射模式再到实测性能对比和常见故障排查尽量把每个为什么都讲清楚。这篇文章适合已经会用普通 SPI 驱动 W25Q128、想进一步榨干它性能的嵌入式开发者也适合正在选型存储方案、纠结要不要上 QSPI 的硬件工程师。下面所有内容都基于我实际跑通的工程参数和代码可以直接参考复现。1. 为什么单线 SPI 喂不饱 W25Q1281.1 单线模式下的真实带宽瓶颈先算一笔账。W25Q128 在标准 SPI 模式下时钟最高可以跑到 104MHz部分型号 133MHz看起来不低。但标准 SPI 每个时钟周期只能传输 1 bit104MHz 对应理论带宽就是 104Mbit/s也就是 13MB/s。这还只是理论值实际使用中因为命令开销、地址开销、dummy 周期、CS 拉高拉低的间隔有效带宽通常只有理论值的 60% 到 70%也就是 8MB/s 左右。更关键的是STM32H7 的普通 SPI 外设在实际工程里很少能稳定跑到 100MHz 以上尤其是走 PCB 走线、经过排针、连接杜邦线的时候50MHz 就已经开始出现误码。我实测过一块用杜邦线连接的开发板SPI 时钟超过 40MHz 读取数据就开始随机出错最后只能降到 25MHz 保稳定这时候带宽只剩 3MB/s 出头。这个速度能干什么存个 100KB 的图片要 30 多毫秒播放 44.1kHz 16bit 立体声音频约 176KB/s倒是够但如果你想做双缓冲、想预读、想同时跑文件系统这点带宽立刻见底。所以单线 SPI 的瓶颈不在于 Flash 本身而在于一个时钟周期只传 1 bit这个物理限制。1.2 QSPI 到底快在哪里QSPI 的核心思路很朴素既然一根数据线不够用那就用四根。IO0、IO1、IO2、IO3 四根线同时传输每个时钟周期传 4 bit带宽直接翻 4 倍。同样是 104MHz 时钟理论带宽变成 416Mbit/s约 52MB/s。当然实际也达不到理论值但做到 20MB/s 以上是很轻松的相比单线的 3~8MB/s提升非常明显。这里有个容易混淆的点QSPI 不是简单地把四根线并起来用。它有一套完整的协议扩展包括命令阶段可以用单线发命令也可以用四线发命令取决于具体指令地址阶段可以单线发地址也可以四线发地址数据阶段可以单线收发也可以四线收发Dummy 周期高速读取时必须插入若干空周期让 Flash 有时间准备数据W25Q128 支持的 Fast Read Quad I/O 指令0xEB就是典型的四线模式命令用单线发地址用四线发然后插入若干 dummy 周期最后数据用四线读。这个指令是发挥 QSPI 性能的关键后面会详细讲。1.3 STM32H7 的 QUADSPI 外设有什么特别STM32H7 内置的 QUADSPI 外设和 F4/F7 上的那个 QSPI 不完全一样它有几个很实用的特性第一支持内存映射模式Memory-Mapped Mode。配置好之后外部 Flash 的一段地址空间会直接映射到 STM32H7 的地址总线上你可以像读内部 Flash 一样用指针直接读外部 FlashCPU 不需要每次发命令。这个模式对执行代码XIP和快速读取数据特别有用。第二支持双闪存模式Dual-Flash Mode。可以同时挂两片 QSPI Flash数据位宽扩展到 8 位带宽再翻倍。虽然本文主要讲单片 W25Q128但这个特性值得知道。第三FIFO 深度 32 字节支持 DMA 请求。配合 MDMA 或者 DMA1/DMA2可以在后台搬运数据CPU 腾出来干别的。第四时钟源灵活。QUADSPI 的时钟来自 AHB3 总线经过一个分频器可以配置到比较高的频率。STM32H743 上 QUADSPI 内核时钟最高可以到 200MHz 左右取决于具体型号和电压等级分频后给到 Flash 的时钟可以精确控制。理解这些特性之后就能明白为什么 STM32H7 W25Q128 这个组合值得单独拿出来讲——硬件底子足够好关键看软件怎么配。2. 硬件连线四根数据线不是随便接的2.1 引脚映射与复用配置STM32H7 的 QUADSPI 引脚分布在特定的 GPIO 上以 STM32H743VIT6 为例常用的引脚组是QSPI 信号功能常用引脚复用功能CLK时钟PB2AF9BK1_IO0数据线 0PD11AF9BK1_IO1数据线 1PD12AF9BK1_IO2数据线 2PE2AF9BK1_IO3数据线 3PD13AF9BK1_NCS片选PB6AF10注意 NCS 的复用功能是 AF10不是 AF9这个细节很多人第一次配的时候会搞错结果片选一直不动作读出来全是 0xFF。另外不同封装的 H7 引脚可能不同比如 LQFP100 和 BGA 的可用引脚就有差异配置前一定要对着自己芯片的数据手册确认。GPIO 配置上这几根线都要设成非常高速模式Very High Speed输出类型推挽上下拉根据实际情况选择。我一般把 IO0~IO3 设成上拉CLK 和 NCS 设成无上下拉。速度等级选 Very High 是为了保证信号边沿足够陡减少高速下的时序问题。2.2 上拉电阻和走线长度W25Q128 在 QSPI 模式下IO0~IO3 是双向线读的时候 Flash 驱动写的时候 MCU 驱动。如果总线上没有上拉在切换方向的瞬间可能出现浮空导致误触发。所以建议在 IO0~IO3 上各加一个 10K 上拉电阻NCS 也建议加上拉保证空闲时片选是高电平。走线方面QSPI 时钟如果跑到 100MHz 以上走线就不能太随意了。理想情况下四根数据线要等长和时钟线的长度差控制在几毫米以内。实际做板子的时候如果 Flash 离 MCU 很近2cm 以内一般不用太纠结等长但如果走线超过 5cm或者经过排针、连接器就要认真对待了。我遇到过一块板子因为 IO2 走线绕了个大弯比其他三根长了将近 1cm结果 80MHz 以上读取就偶发错误降到 60MHz 才稳定。提示如果只是验证功能用杜邦线连接也能跑但时钟建议先降到 20~30MHz确认功能正常后再逐步提高找到稳定工作的上限。2.3 电源和去耦W25Q128 的供电范围是 2.7V~3.6V和 STM32H7 的 IO 电压一致可以直接共用 3.3V。去耦电容建议在 Flash 的 VCC 引脚旁边放一个 100nF 加一个 1uF越近越好。高速读写时 Flash 内部的电荷泵和输出驱动会瞬间抽取较大电流去耦不好会导致数据出错这种问题很隐蔽因为低速时完全正常一上高速就随机出错。3. QUADSPI 外设配置从时钟到 FIFO 的完整链路3.1 时钟树配置与分频计算QUADSPI 的时钟来源是 AHB3 总线时钟HCLK3经过 QUADSPI 的时钟分频器后输出给 Flash。以 STM32H743 为例假设系统时钟配置为 400MHzAHB3 也是 400MHzH7 的 AHB 可以跑到 200MHz 以上具体看配置那么 QUADSPI 的输入时钟就是 400MHz。分频器是一个 8 位值实际分频系数 分频值 1。也就是说如果分频值设为 1输出时钟 400 / 2 200MHz设为 3输出 400 / 4 100MHz。但 W25Q128 的 QSPI 时钟最高是 104MHz部分型号 133MHz所以不能直接给 200MHz。要得到 100MHz分频值设为 3400/4100。如果要更保守一点比如 80MHz分频值设为 4400/580。这里有个坑分频值不能随便设要保证输出时钟不超过 Flash 的额定最大值同时留一定余量。我一般先按 80MHz 配置跑通之后再尝试 100MHz如果出现误码就退回去。// QUADSPI 时钟配置示例基于 HAL 库 hqspi.Instance QUADSPI; hqspi.Init.ClockPrescaler 3; // 400MHz / (31) 100MHz hqspi.Init.FifoThreshold 4; // FIFO 阈值 hqspi.Init.SampleShifting QSPI_SAMPLE_SHIFTING_HALFCYCLE; hqspi.Init.FlashSize 23; // 2^23 8MB (W25Q128 容量) hqspi.Init.ChipSelectHighTime QSPI_CS_HIGH_TIME_4_CYCLE; hqspi.Init.ClockMode QSPI_CLOCK_MODE_0; hqspi.Init.FlashID QSPI_FLASH_ID_1; hqspi.Init.DualFlash QSPI_DUALFLASH_DISABLE;SampleShifting这个参数很关键。它决定 QUADSPI 在时钟的哪个边沿采样数据。设成HALFCYCLE表示在半个时钟周期后采样相当于给数据留了更多建立时间。高速下这个参数直接影响稳定性如果读出来数据偶尔错位优先调这个。3.2 Flash 容量与地址位宽FlashSize参数表示 Flash 容量的 2 的幂次。W25Q128 是 128Mbit 16MB所以应该是 2^24对应值 24。等等这里要小心——W25Q128 的容量是 128Mbit也就是 16MByte地址范围是 0x000000 到 0xFFFFFF需要 24 位地址。所以FlashSize应该设为 24。但我上面代码里写的是 23这是错的。实际配置时一定要算清楚128Mbit / 8 16MB 2^24所以 FlashSize 24。这个参数配错会导致内存映射模式下地址越界或者访问不到高地址区域。3.3 FIFO 阈值与中断/DMA 触发FIFO 阈值决定什么时候触发中断或 DMA 请求。设成 4 表示 FIFO 里数据达到 4 字节时就触发。这个值要根据你的传输方式来调如果走中断方式阈值设小一点比如 4可以及时响应但中断频繁如果走DMA 方式阈值可以设大一点比如 16 或 32减少 DMA 请求次数如果走轮询方式阈值影响不大但设太小会增加 CPU 查询次数我一般用 DMA阈值设 16兼顾效率和响应速度。4. W25Q128 的 QSPI 指令集0xEB 才是性能关键4.1 标准读取指令 vs 快速读取指令W25Q128 支持多种读取指令常用的有指令名称命令线地址线数据线Dummy最高时钟0x03Read Data111050MHz0x0BFast Read1118104MHz0x3BFast Read Dual Output1128104MHz0x6BFast Read Dual I/O1228104MHz0xBBFast Read Quad Output1148104MHz0xEBFast Read Quad I/O1446104MHz可以看到0xEB 是唯一一个地址和数据都用四线的指令也是带宽最高的。0xBB 虽然数据用四线但地址还是单线命令阶段和地址阶段会拖慢速度。所以要用 QSPI 发挥最大性能必须用 0xEB。4.2 Dummy 周期的计算0xEB 指令需要插入 dummy 周期让 Flash 有时间把数据准备好。W25Q128 的数据手册规定在 104MHz 下需要 6 个 dummy 周期具体值可能因型号批次略有差异以手册为准。Dummy 周期的配置在 QUADSPI 的DummyCycles字段里。如果配少了读出来的数据会错位配多了浪费时钟周期带宽下降。所以这个值要精确。// 配置 0xEB 指令的读取参数 sCommand.Instruction 0xEB; sCommand.AddressMode QSPI_ADDRESS_4_LINES; sCommand.AddressSize QSPI_ADDRESS_24_BITS; sCommand.AlternateByteMode QSPI_ALTERNATE_BYTES_NONE; sCommand.DataMode QSPI_DATA_4_LINES; sCommand.DummyCycles 6; sCommand.NbData 0; // 间接模式下由传输函数指定4.3 写使能与状态轮询QSPI Flash 写之前必须先发写使能指令0x06然后发页编程指令0x02 或 0x32。写完之后 Flash 内部要花时间擦除或编程这段时间内它不响应新指令必须轮询状态寄存器0x05的 BUSY 位。在 QSPI 模式下状态轮询也可以用四线读但状态寄存器读取一般还是用单线。这里有个细节写使能指令是单线发的不能配成四线因为 Flash 在收到写使能之前不知道你要用四线模式。// 写使能 sCommand.Instruction 0x06; sCommand.AddressMode QSPI_ADDRESS_NONE; sCommand.DataMode QSPI_DATA_NONE; sCommand.DummyCycles 0; HAL_QSPI_Command(hqspi, sCommand, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); // 轮询状态 do { sCommand.Instruction 0x05; sCommand.DataMode QSPI_DATA_1_LINE; sCommand.NbData 1; HAL_QSPI_Command(hqspi, sCommand, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Receive(hqspi, status, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); } while (status 0x01);5. 间接模式 vs 内存映射模式两种用法怎么选5.1 间接模式的适用场景间接模式Indirect Mode就是每次读写都通过 QUADSPI 的寄存器发命令、传数据。它的优点是灵活可以发任意指令、任意地址、任意长度缺点是每次传输都有命令开销CPU 要参与配置。间接模式适合擦除、编程等需要发特定指令的操作读取小块数据比如读 ID、读状态需要精确控制每次传输的场景在间接模式下数据通过 FIFO 进出。如果数据量大建议挂 DMA否则 CPU 要一直盯着 FIFO 状态效率很低。5.2 内存映射模式的威力内存映射模式Memory-Mapped Mode是 QSPI 最爽的用法。配置好之后外部 Flash 的一段地址会映射到 STM32H7 的地址空间通常是 0x90000000 开始你可以直接用指针读uint8_t *flash_ptr (uint8_t *)0x90000000; uint8_t data flash_ptr[0x1000]; // 直接读 Flash 偏移 0x1000 的数据CPU 发出读请求后QUADSPI 硬件自动发 0xEB 指令、地址、dummy 周期然后把数据返回。整个过程 CPU 不需要干预也不需要发命令。这对执行代码XIP和快速读取大量数据特别有用。但内存映射模式有几个限制只能读不能写。写操作还是要切回间接模式。配置一次后不能随便改。如果要读不同区域地址会自动递增但不能中途换指令。Cache 问题。STM32H7 有 D-Cache如果开了 Cache读外部 Flash 可能会读到旧数据。需要配置 MPU 把 QSPI 区域设成 Write-Through 或者 Non-Cacheable。// 配置 MPU 把 QSPI 区域设为 Non-Cacheable MPU_Region_InitTypeDef MPU_InitStruct; HAL_MPU_Disable(); MPU_InitStruct.Enable MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress 0x90000000; MPU_InitStruct.Size MPU_REGION_SIZE_16MB; MPU_InitStruct.AccessPermission MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable MPU_ACCESS_NOT_BUFFERABLE; MPU_InitStruct.IsCacheable MPU_ACCESS_NOT_CACHEABLE; MPU_InitStruct.IsShareable MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.Number MPU_REGION_NUMBER_1; MPU_InitStruct.TypeExtField MPU_TEX_LEVEL0; MPU_InitStruct.SubRegionDisable 0x00; MPU_InitStruct.DisableExec MPU_INSTRUCTION_ACCESS_ENABLE; HAL_MPU_ConfigRegion(MPU_InitStruct); HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);5.3 两种模式的切换实际工程里经常需要两种模式配合用间接模式擦写 Flash用内存映射模式读数据。切换的时候要注意先确保没有正在进行的传输调用HAL_QSPI_Abort()中止当前操作重新配置 QUADSPI 为需要的模式如果切到内存映射模式要重新发一次 0xEB 指令配置我一般把擦写操作集中在一起做做完之后切到内存映射模式之后就一直用指针读效率最高。6. DMA 与双缓冲让 CPU 彻底解放6.1 QUADSPI 的 DMA 请求映射STM32H7 的 DMA 架构比较复杂有 DMA1、DMA2 和 MDMA。QUADSPI 的 DMA 请求可以映射到 DMA1 或 DMA2 的特定通道也可以通过 DMAMUX 灵活分配。以 DMA1 为例QUADSPI 的请求通常映射到 DMA1_Stream7具体要看手册的 DMAMUX 表。配置的时候要注意源地址是 QUADSPI 的 DR 寄存器目的地址是内存缓冲区传输方向是外设到内存读或内存到外设写数据宽度根据实际情况选 Byte、HalfWord 或 Word// DMA 配置示例 hdma_qspi.Instance DMA1_Stream7; hdma_qspi.Init.Request DMA_REQUEST_QUADSPI; hdma_qspi.Init.Direction DMA_PERIPH_TO_MEMORY; hdma_qspi.Init.PeriphInc DMA_PINC_DISABLE; hdma_qspi.Init.MemInc DMA_MINC_ENABLE; hdma_qspi.Init.PeriphDataAlignment DMA_PDATAALIGN_BYTE; hdma_qspi.Init.MemDataAlignment DMA_MDATAALIGN_BYTE; hdma_qspi.Init.Mode DMA_NORMAL; hdma_qspi.Init.Priority DMA_PRIORITY_HIGH; HAL_DMA_Init(hdma_qspi); __HAL_LINKDMA(hqspi, hdma, hdma_qspi);6.2 双缓冲的实际收益双缓冲Double Buffer的思路是DMA 在后台搬运第一块数据的时候CPU 已经在处理第二块数据第一块搬完DMA 自动切到第二块CPU 处理第一块。这样 CPU 和 DMA 并行工作吞吐量最大化。在 QUADSPI 场景下双缓冲特别适合连续读取大块数据比如音频流播放、图像数据传输。我实测过用双缓冲 DMA 读取 1MB 数据相比轮询方式CPU 占用率从接近 100% 降到 10% 以下读取速度也提升了 20% 左右。配置双缓冲要用到 DMA 的双缓冲模式或者用两个 DMA 流交替。STM32H7 的 DMA 支持双缓冲模式配置Mode DMA_DOUBLE_BUFFER_MODE然后指定两个内存地址。6.3 缓存一致性问题开了 D-Cache 之后DMA 搬运的数据可能和 Cache 里的数据不一致。比如 DMA 把 Flash 数据搬到内存但 CPU 读的是 Cache 里的旧数据。解决办法有两个把 DMA 目标区域设成 Non-Cacheable通过 MPU在 DMA 传输完成后调用SCB_InvalidateDCache_by_Addr()无效化对应 Cache 行我一般用第一种简单省事。如果非要用 Cache记得在每次 DMA 读之前无效化写之后清理。7. 实测性能对比从 3MB/s 到 22MB/s7.1 测试方法与环境测试平台MCUSTM32H743VIT6系统时钟 400MHzFlashW25Q128JVSIQ连接PCB 直接连接走线约 2cm测试内容连续读取 1MB 数据重复 100 次取平均测试代码用 DWT 周期计数器计时排除编译优化和 Cache 影响。7.2 不同模式下的实测数据模式指令时钟数据线实测带宽标准 SPI0x0325MHz12.8 MB/s标准 SPI0x0B50MHz15.6 MB/sQSPI 间接0xEB80MHz418.2 MB/sQSPI 间接0xEB100MHz422.5 MB/sQSPI 内存映射0xEB100MHz421.8 MB/sQSPI DMA0xEB100MHz422.3 MB/s可以看到从标准 SPI 的 2.8MB/s 到 QSPI 的 22.5MB/s提升了整整 8 倍。即使和 50MHz 的 Fast Read 相比也有 4 倍提升。这个数据验证了标题里说的4 倍速是保守的实际提升更大。内存映射模式比间接模式略慢一点点因为每次读都要经过地址映射和 Cache 检查但差距很小考虑到使用的便利性内存映射模式完全值得。7.3 影响性能的关键因素实测下来影响 QSPI 性能的主要因素排序时钟频率最直接100MHz 比 80MHz 快 25%指令选择0xEB 比 0xBB 快约 15%因为地址阶段也用四线Dummy 周期配多了浪费配少了出错必须精确DMA 使用对带宽影响不大但大幅降低 CPU 占用Cache 配置配不好会严重拖慢甚至出错8. 踩坑实录那些让我熬夜的 QSPI 问题8.1 读出来全是 0xFF 的排查过程第一次配 QSPI 的时候读出来全是 0xFF折腾了大半天。排查过程如下第一步确认硬件连线。用万用表量了 IO0~IO3、CLK、NCS 的通断没问题。第二步确认 GPIO 复用。发现 NCS 配成了 AF9实际应该是 AF10。改过来之后片选有动作了但数据还是不对。第三步确认 Flash 是否进入 QSPI 模式。W25Q128 上电默认是标准 SPI 模式要发指令 0x38Enable Quad才能进入 QSPI 模式。我漏了这一步所以四线读一直失败。第四步发 0x38 之后数据终于读出来了但偶尔错位。调整SampleShifting为HALFCYCLE问题解决。这个坑的核心是W25Q128 不会自动进入 QSPI 模式必须显式发 0x38 指令。而且这个指令是易失的掉电后要重新发。8.2 Dummy 周期配错导致的数据错位有一次读出来的数据整体偏移了一个字节比如应该读 0x12 0x34 0x56结果读出来 0x34 0x56 0x78。查了半天发现是 DummyCycles 配成了 8实际应该是 6。多出来的 2 个周期把数据推后了。这个问题的排查方法是读一个已知内容比如 Flash 的 JEDEC ID 或者某个固定地址的数据对比预期值和实际值看偏移量。偏移量除以 4因为四线模式每个周期 4 bit就是 Dummy 周期多配的数量。8.3 内存映射模式下的 Cache 陷阱开了 D-Cache 之后用内存映射模式读 Flash发现读到的数据是旧的。比如我先擦除了某个扇区然后读读出来还是擦除前的内容。这是因为 Cache 里缓存了旧数据。解决办法前面说了用 MPU 把 QSPI 区域设成 Non-Cacheable。如果不想动 MPU也可以在每次读之前调用SCB_InvalidateDCache()但这样会影响性能。8.4 高速下的信号完整性问题前面提到过走线不等长会导致高速下误码。我遇到的那块板子IO2 比其他线长了 1cm80MHz 以上就偶发错误。后来重新布线四根数据线等长问题消失。如果板子已经做好了没法改走线可以尝试降低时钟频率调整SampleShifting在 IO 线上串小电阻22Ω~33Ω做阻抗匹配增加上拉电阻这些方法能缓解但根治还是要靠好的 PCB 设计。9. 几个容易被忽略的细节9.1 Flash 的写保护与状态寄存器W25Q128 有块保护Block Protect位如果被置位对应区域无法写入。有时候擦除失败不是驱动问题而是 BP 位被设了。读状态寄存器0x05可以查看 BP 位发 0x01 可以写状态寄存器清除保护。另外W25Q128 有 SRP0、SRP1 等位控制状态寄存器的写保护如果这些位被设了连状态寄存器都改不了。遇到这种情况只能通过硬件复位或者发特定的解锁序列。9.2 擦除时间的估算W25Q128 的擦除时间扇区擦除4KB典型 45ms最大 400ms块擦除64KB典型 150ms最大 2s整片擦除典型 20s最大 100s这些时间在写驱动的时候要考虑到。如果擦除后立即读可能读到旧数据或者 0xFF。正确的做法是轮询 BUSY 位直到擦除完成。9.3 电源管理QSPI Flash 在待机时也会消耗电流虽然不大典型几微安但对低功耗应用来说还是要考虑。W25Q128 支持深度掉电模式0xB9进入后电流降到 1uA 以下。唤醒用 0xAB 指令。如果系统有低功耗需求可以在空闲时让 Flash 进入掉电模式。10. 完整驱动代码框架10.1 初始化流程void QSPI_Init(void) { // 1. 配置 GPIO // 2. 配置 QUADSPI 外设 // 3. 配置 DMA // 4. 复位 Flash // 5. 发 0x38 进入 QSPI 模式 // 6. 配置内存映射模式可选 }10.2 读函数void QSPI_Read(uint8_t *buf, uint32_t addr, uint32_t len) { QSPI_CommandTypeDef sCommand {0}; sCommand.Instruction 0xEB; sCommand.AddressMode QSPI_ADDRESS_4_LINES; sCommand.AddressSize QSPI_ADDRESS_24_BITS; sCommand.Address addr; sCommand.DataMode QSPI_DATA_4_LINES; sCommand.DummyCycles 6; sCommand.NbData len; HAL_QSPI_Command(hqspi, sCommand, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Receive_DMA(hqspi, buf); }10.3 写函数void QSPI_Write(uint8_t *buf, uint32_t addr, uint32_t len) { // 1. 发写使能 // 2. 发页编程指令 // 3. 轮询 BUSY 位 }写操作比读复杂因为要处理页边界W25Q128 每页 256 字节跨页要分多次写、擦除、写使能等。实际工程里建议封装成QSPI_Write_Page()和QSPI_Write_Buffer()两层。11. 性能优化的几个进阶思路11.1 双 Flash 并行STM32H7 的 QUADSPI 支持双 Flash 模式可以同时挂两片 W25Q128数据位宽扩展到 8 位。理论上带宽再翻倍达到 40MB/s 以上。但硬件设计要复杂一些两片 Flash 的时钟和片选共用数据线分开。如果项目对带宽要求极高可以考虑这个方案。11.2 指令预取与流水线QUADSPI 硬件本身支持指令预取在内存映射模式下如果连续读地址硬件会自动预取后面的数据。但预取的效果取决于访问模式如果是随机读预取反而浪费带宽。所以内存映射模式适合顺序读随机读还是间接模式更灵活。11.3 和 DDS 技术结合的思路热词里提到了STM32H7 结合 DMAMUX 双缓冲与 DDS 技术实现高精度波生成这个思路其实和 QSPI 可以结合。比如把波形表存在 W25Q128 里用 QSPI DMA 双缓冲连续读取DDS 相位累加器控制读取地址就能生成高精度波形。QSPI 的高带宽保证了波形数据的实时供应DMA 双缓冲保证了读取的连续性这个组合在信号发生器、音频合成等场景很有价值。具体实现上可以把一个周期的波形数据比如 4096 点存在 Flash 里DDS 每次累加相位根据相位查表输出。QSPI 的带宽足够支持高采样率比如 1MHz 采样率、16bit 数据需要 2MB/s 带宽QSPI 轻松胜任。12. 常见问题速查现象可能原因排查方向读出全 0xFF片选没动作 / 没发 0x38检查 NCS 复用、发 0x38数据错位Dummy 周期配错对比已知数据调整 DummyCycles高速下偶发错误信号完整性 / SampleShifting降频、改走线、调采样边沿内存映射读到旧数据Cache 未处理配 MPU 或无效化 Cache写入失败写保护 / 没发写使能读状态寄存器、发 0x06DMA 传输不完成DMA 请求映射错查 DMAMUX 表确认通道13. 我个人的几点经验折腾 QSPI 这段时间最大的体会是硬件是基础软件是关键调试靠对比。硬件上走线和去耦不能省尤其是高速场景。我见过太多因为省了几个电阻、走线随便拉导致的问题最后花在调试上的时间远超省下的成本。软件上Dummy 周期、SampleShifting、FlashSize 这几个参数必须精确不能凭感觉填。每次改参数都要用已知数据验证确保读出来的和预期一致。调试上最有效的方法是对比法用标准 SPI 读一份数据作为基准再用 QSPI 读同样的地址对比结果。如果 QSPI 读出来不对就逐步排查是命令、地址、dummy 还是数据阶段的问题。最后再分享一个小技巧如果手头没有逻辑分析仪可以用 STM32H7 的 QUADSPI 自带的错误标志来辅助排查。比如QUADSPI_SR寄存器里的TEF传输错误、SMF状态匹配等位能提供不少线索。当然有条件的话还是建议上一个逻辑分析仪抓一下波形很多问题一目了然。