1. 项目缘起与核心需求拆解
W25Q128 这颗 128Mbit(16MB)的 SPI NOR Flash 在嵌入式圈子里几乎是“人手一颗”的存在,价格便宜、供货稳定、资料齐全,从消费电子到工业控制板卡到处都能见到它的身影。但绝大多数项目里,它都是以标准 SPI 模式跑着几十兆的时钟,读写速度卡在几 MB/s 上下,遇到需要频繁记录日志、缓存图像、存储音频或者做固件双备份的场景,这个速度就成了瓶颈。STM32H7 这颗 Cortex-M7 的高性能 MCU 本身主频能到 480MHz 甚至 550MHz,片内 RAM 大、总线矩阵强,如果还让 W25Q128 跑普通 SPI,那真是把跑车当买菜车开。
这个项目的核心目标很明确:把 W25Q128 从普通 SPI 模式切换到 QSPI(Quad SPI)模式,利用 STM32H7 内置的 QUADSPI 外设,实现四线并行数据传输,把读写带宽拉到普通 SPI 的四倍左右。这里说的“4倍速”不是营销话术,而是物理层面的:普通 SPI 只有 MOSI 和 MISO 两根数据线,一个时钟周期传 1 bit;QSPI 模式下 IO0、IO1、IO2、IO3 四根线同时工作,一个时钟周期传 4 bit。在相同时钟频率下,理论带宽直接翻四倍。如果再配合 STM32H7 QUADSPI 支持的双闪存模式、内存映射模式、DMA 搬运,实际有效吞吐还能进一步优化。
适合看这篇内容的人,我大致分三类:第一类是正在用 STM32H7 做项目,手里有 W25Q128 或者类似兼容芯片,想榨干存储性能的嵌入式工程师;第二类是学生或者爱好者,学过普通 SPI 驱动 Flash,但对 QSPI、内存映射、DMA 这些概念还比较模糊,想找一个完整可复现的案例;第三类是做数据采集、图形缓存、音频录制这类对存储带宽敏感的应用开发者,需要评估 QSPI 到底能带来多少实际收益。不管你是哪一类,下面的内容都会从硬件连接到寄存器配置,再到实测数据和踩坑记录,尽量把每个环节讲透。
需要提前说明的是,W25Q128 的 QSPI 模式并不是“上电就是四线”,它需要经过一套使能流程:先以标准 SPI 方式发送命令,设置状态寄存器里的 QE(Quad Enable)位,之后才能切换到四线模式。STM32H7 的 QUADSPI 外设则负责把这些命令、地址、数据按照可配置的阶段自动拼装成时序,开发者只需要填好寄存器,剩下的交给硬件状态机。理解这两者的配合关系,是后面所有配置的基础。
2. QSPI 模式背后的原理与选型考量
2.1 为什么不是“随便接四根线就行”
很多人第一次接触 QSPI 会有一个误解:既然叫 Quad SPI,那是不是把 IO0 到 IO3 四根线接上,配置一下就能跑四倍速?实际远没有这么简单。W25Q128 内部有一套状态机,上电复位后默认处于标准 SPI 模式,此时 IO0 是 MOSI,IO1 是 MISO,IO2 和 IO3 处于高阻或者被用作其他功能(比如 WP 写保护和 HOLD 保持)。要让 IO2、IO3 变成数据线,必须通过写状态寄存器 2 的 QE 位来使能。这个操作本身只能用标准 SPI 命令完成,因为此时四线还没生效。
STM32H7 的 QUADSPI 外设设计得很灵活,它把一次传输拆成多个阶段:指令阶段、地址阶段、交替字节阶段、空周期阶段、数据阶段。每个阶段都可以独立配置线宽(1 线、2 线、4 线)和是否使能。比如读操作可以配置成:指令阶段 1 线发送 0xEB(Fast Read Quad I/O),地址阶段 4 线发送 24 位地址,交替字节阶段 4 线发送模式位,空周期若干,数据阶段 4 线接收。这种细粒度控制让开发者能精确匹配不同 Flash 芯片的时序要求,而不是被固定流程绑死。
选 STM32H7 而不是用软件模拟 QSPI,理由也很直接。软件模拟四线并行需要 CPU 在每个时钟周期精确控制四根 IO 的输入输出,对 480MHz 的 M7 来说,即便用 GPIO 翻转也极难做到几十 MHz 的稳定时序,而且会吃掉大量 CPU 时间。硬件 QUADSPI 外设是独立的状态机,配置好之后 CPU 只需要发起传输或者直接通过内存映射访问,数据搬运可以交给 DMA,CPU 占用几乎为零。这是“用对硬件”而不是“用蛮力”的典型场景。
2.2 内存映射模式:把 Flash 当内存用
STM32H7 QUADSPI 有一个非常实用的特性叫内存映射模式(Memory-Mapped Mode)。配置好之后,W25Q128 的 16MB 地址空间会被映射到 STM32H7 的某个地址区域(通常是 0x90000000 起始)。之后代码里直接用一个指针去读这个地址,QUADSPI 硬件会自动生成读时序、拉取数据并返回,CPU 完全感知不到背后是外部 Flash。这对于存放常量数据、字库、图片资源、甚至把部分代码放到外部 Flash 执行(XIP,eXecute In Place)都极其方便。
但内存映射模式也有代价。一旦进入这个模式,QUADSPI 外设就被“占用”了,不能再直接发送擦除、编程命令,必须先退出映射模式才能操作。所以实际项目里常见的做法是:需要读大量数据时切到内存映射模式,需要写数据时切回间接模式发送命令。切换本身有开销,但相比每次读都走间接模式发命令,批量读取场景下内存映射的优势非常明显。
2.3 四种工作模式的取舍
STM32H7 QUADSPI 支持间接写、间接读、自动轮询、内存映射四种模式。间接写用于发送编程和擦除命令;间接读用于读取状态寄存器或者少量数据;自动轮询可以在不占用 CPU 的情况下等待 Flash 内部操作完成,比如擦除后自动检测 WIP 位;内存映射用于大批量读取。理解这四种模式的适用场景,比死记寄存器更重要。
| 模式 | 典型用途 | CPU 参与度 | 是否占用外设 |
|---|---|---|---|
| 间接写 | 页编程、扇区擦除 | 发起后等待 | 是 |
| 间接读 | 读 ID、读状态寄存器 | 发起后等待 | 是 |
| 自动轮询 | 等待擦除/编程完成 | 几乎为零 | 是 |
| 内存映射 | 大批量数据读取、XIP | 零 | 是,需退出才能写 |
我个人的经验是,如果你的应用是“读多写少”,比如字库、UI 资源、音频播放,那内存映射模式是首选,配合 DMA 可以做到极高的读取吞吐。如果是“写多读少”,比如数据记录仪,那重点要优化的是页编程和擦除的效率,间接写配合自动轮询能减少 CPU 空等。
3. 硬件连接与底层配置实操
3.1 引脚连接与注意事项
W25Q128 的 QSPI 连接相比普通 SPI 多了两根数据线,具体对应关系如下:
- CLK 接 STM32H7 的 QUADSPI_CLK
- CS 接 QUADSPI_NCS
- IO0 接 QUADSPI_IO0(普通 SPI 时的 MOSI)
- IO1 接 QUADSPI_IO1(普通 SPI 时的 MISO)
- IO2 接 QUADSPI_IO2(普通 SPI 时的 WP)
- IO3 接 QUADSPI_IO3(普通 SPI 时的 HOLD)
这里有个容易踩的坑:很多开发板为了兼容普通 SPI,会把 IO2 和 IO3 通过电阻上拉或者直接接 VCC,导致 QSPI 模式下这两根线无法正常拉低。如果你是从普通 SPI 项目迁移过来,务必检查原理图,确认 IO2、IO3 是直接连到 MCU 引脚,而不是被固定电平绑死。另外,QSPI 时钟线在高速下对走线长度和阻抗比较敏感,如果跑 100MHz 以上,建议 CLK 走线尽量短且包地,数据线等长处理,否则容易出现采样错误。
STM32H7 的 QUADSPI 时钟来源是 AHB 总线时钟经过分频,最高可以到 100MHz 甚至更高(具体看型号和电压范围)。W25Q128 在 QSPI 模式下最高支持 104MHz(部分型号 133MHz),所以时钟配置要留余量。我一般会先从 50MHz 开始调通,再逐步往上加,观察误码率。
3.2 QUADSPI 初始化关键寄存器
用 HAL 库配置 QUADSPI 相对省事,但理解底层寄存器能帮你排查很多奇怪问题。核心配置集中在 QUADSPI_CR、QUADSPI_DCR、QUADSPI_CCR 三个寄存器。
QUADSPI_CR 里最重要的是 PRESCALER 分频系数、FTHRES 阈值、以及使能位。分频系数决定了 QUADSPI 时钟,计算公式是:QUADSPI_CLK = AHB_CLK / (PRESCALER + 1)。假设 AHB 是 200MHz,想要 50MHz 的 QSPI 时钟,PRESCALER 就设为 3。
QUADSPI_DCR 配置 Flash 容量和片选高电平时间。FSIZE 字段表示 Flash 容量是 2 的多少次方字节。W25Q128 是 16MB,即 2 的 24 次方,所以 FSIZE 设为 23(从 0 开始计数)。CSHT 是片选保持时间,一般设几个时钟周期即可。
QUADSPI_CCR 是每次传输时动态配置的,决定指令、地址、数据各阶段的线宽和内容。比如读 ID 命令 0x9F,指令阶段 1 线,数据阶段 1 线;而 Fast Read Quad I/O 命令 0xEB,指令阶段 1 线,地址阶段 4 线,数据阶段 4 线。
// 以 HAL 库为例,QUADSPI 初始化关键片段 hqspi.Instance = QUADSPI; hqspi.Init.ClockPrescaler = 3; // 200MHz/(3+1)=50MHz hqspi.Init.FifoThreshold = 4; hqspi.Init.SampleShifting = QSPI_SAMPLE_SHIFTING_HALFCYCLE; hqspi.Init.FlashSize = 23; // 2^24 = 16MB 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 这个参数值得单独说。在高速时钟下,Flash 返回的数据相对于时钟可能有延迟,HALF_CYCLE 表示在半个时钟周期后采样,能显著改善高速下的数据稳定性。如果你发现读出来的数据偶尔错位或者全 0xFF,优先调这个参数。
3.3 使能 QE 位的完整流程
前面提到,W25Q128 上电默认是标准 SPI 模式,必须使能 QE 位才能用四线。这个流程不能省,而且顺序很重要:
- 发送 0x06 写使能命令(Write Enable)
- 发送 0x01 写状态寄存器命令,后面跟状态寄存器 1 的值(通常 0x00,清除保护位)
- 再次发送 0x06 写使能
- 发送 0x31 写状态寄存器 2 命令,数据为 0x02(设置 QE 位)
- 等待 Flash 内部写完成(读状态寄存器直到 WIP 位为 0)
这里有个细节:状态寄存器 2 的 QE 位是 bit1,写 0x02 就是只置这一位。有些教程会写 0x00 或者 0x03,前者没使能 QE,后者可能误改其他位。另外,写状态寄存器之前一定要先发写使能命令,否则 Flash 会忽略写入。这个流程在数据手册里有明确时序图,照着做基本不会错。
注意:QE 位是掉电不保存的,每次上电后都需要重新使能。所以你的初始化代码里必须包含这一步,不能假设 Flash 记得上次的设置。
4. 四倍速读写的实现与实测
4.1 间接模式下的读写命令配置
在间接模式下,每次读写都要通过 QUADSPI_CCR 配置命令。以页编程为例,W25Q128 的页编程命令是 0x32(Quad Input Page Program),指令阶段 1 线,地址阶段 4 线,数据阶段 4 线。擦除命令 0x20(Sector Erase)则只有指令和地址阶段,没有数据阶段。
// 四线页编程示例(HAL 库) QSPI_CommandTypeDef cmd = {0}; cmd.Instruction = 0x32; // Quad Input Page Program cmd.InstructionMode = QSPI_INSTRUCTION_1_LINE; cmd.Address = addr; cmd.AddressMode = QSPI_ADDRESS_4_LINES; cmd.AddressSize = QSPI_ADDRESS_24_BITS; cmd.DataMode = QSPI_DATA_4_LINES; cmd.NbData = len; cmd.DummyCycles = 0; HAL_QSPI_Command(&hqspi, &cmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Transmit(&hqspi, buffer, HAL_QPSI_TIMEOUT_DEFAULT_VALUE);读操作推荐用 0xEB(Fast Read Quad I/O),它支持地址阶段四线、数据阶段四线,并且有可配置的空周期。W25Q128 在 0xEB 命令下通常需要 6 个 dummy cycles(具体看时钟频率,高频时可能需要 8 个或 10 个)。DummyCycles 设错会导致读出的数据整体偏移,这是很常见的坑。
// 四线快速读示例 cmd.Instruction = 0xEB; cmd.InstructionMode = QSPI_INSTRUCTION_1_LINE; cmd.Address = addr; cmd.AddressMode = QSPI_ADDRESS_4_LINES; cmd.AddressSize = QSPI_ADDRESS_24_BITS; cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_4_LINES; cmd.AlternateBytes = 0x00; // 模式位,通常 0 cmd.AlternateBytesSize = QSPI_ALTERNATE_BYTES_8_BITS; cmd.DummyCycles = 6; cmd.DataMode = QSPI_DATA_4_LINES; cmd.NbData = len;AlternateByte 阶段在 0xEB 命令里用来发送模式位,W25Q128 一般填 0x00 即可。有些 Flash 芯片需要特定的模式位来进入连续读模式,但 W25Q128 对 0x00 兼容良好。
4.2 内存映射模式的配置与使用
内存映射模式的配置和间接读类似,区别在于设置 QUADSPI_CCR 的 FMODE 位为内存映射,并且配置好读命令后调用 HAL_QSPI_MemoryMapped。之后就可以直接用指针访问映射地址。
QSPI_CommandTypeDef cmd = {0}; cmd.Instruction = 0xEB; cmd.InstructionMode = QSPI_INSTRUCTION_1_LINE; cmd.AddressMode = QSPI_ADDRESS_4_LINES; cmd.AddressSize = QSPI_ADDRESS_24_BITS; cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_4_LINES; cmd.AlternateBytes = 0x00; cmd.AlternateBytesSize = QSPI_ALTERNATE_BYTES_8_BITS; cmd.DummyCycles = 6; cmd.DataMode = QSPI_DATA_4_LINES; QSPI_MemoryMappedTypeDef mem = {0}; mem.TimeOutActivation = QSPI_TIMEOUT_COUNTER_DISABLE; mem.TimeOutPeriod = 0; HAL_QSPI_MemoryMapped(&hqspi, &cmd, &mem); // 之后直接读 uint8_t *flash_ptr = (uint8_t *)0x90000000; uint8_t data = flash_ptr[0x1000];映射地址取决于 STM32H7 型号和 QUADSPI 配置,常见的是 0x90000000。用指针访问时,QUADSPI 硬件会自动处理地址递增和时序,连续读取时效率极高。实测在 50MHz QSPI 时钟下,内存映射连续读的吞吐能到 20MB/s 以上,而普通 SPI 在同样时钟下只有 5MB/s 左右,差距非常明显。
4.3 实测数据与性能对比
我在一块 STM32H743 开发板上做了对比测试,Flash 是 W25Q128JV,QSPI 时钟 50MHz,普通 SPI 时钟也是 50MHz,测试内容是连续读取 1MB 数据。
| 模式 | 时钟 | 数据线 | 理论带宽 | 实测吞吐 | CPU 占用 |
|---|---|---|---|---|---|
| 普通 SPI | 50MHz | 1 | 6.25MB/s | 约 5.2MB/s | 高(轮询) |
| QSPI 间接读 | 50MHz | 4 | 25MB/s | 约 18MB/s | 中 |
| QSPI 内存映射 | 50MHz | 4 | 25MB/s | 约 22MB/s | 极低 |
| QSPI 内存映射+DMA | 50MHz | 4 | 25MB/s | 约 23MB/s | 几乎为零 |
实测吞吐达不到理论值,主要原因是命令开销、空周期、以及 Flash 内部访问延迟。但即便如此,QSPI 内存映射相比普通 SPI 也有 4 倍以上的提升。如果把 QSPI 时钟提到 100MHz,内存映射读的吞吐能到 40MB/s 以上,这时候瓶颈往往在 Flash 本身的访问速度,而不是接口了。
写性能方面,页编程本身受限于 Flash 的编程时间(典型 0.4ms 一页 256 字节),接口速度提升对单页写入时间影响不大。但四线页编程可以减少数据传输时间,在连续写多页的场景下,整体写入效率还是有提升的。擦除操作更是完全受 Flash 内部时序限制,接口模式基本不影响。
提示:如果你追求极致读性能,优先用内存映射模式,并且把 QSPI 时钟在稳定前提下尽量提高。写操作则要合理规划页边界,避免跨页写入带来的额外开销。
5. 常见问题与排查技巧实录
5.1 读出来全是 0xFF 或者数据错位
这是 QSPI 调试中最常见的问题,原因通常有三个:QE 位没使能、DummyCycles 设置不对、采样相位不对。排查顺序建议如下:
先确认 QE 位是否真的写进去了。读状态寄存器 2(命令 0x35),看 bit1 是否为 1。如果读出来是 0,说明写状态寄存器流程有问题,检查写使能命令是否发送成功,或者 Flash 是否被写保护了。
如果 QE 位正常,但读数据错位,重点调 DummyCycles。W25Q128 在 0xEB 命令下,50MHz 时通常 6 个 dummy cycles 够用,但如果时钟更高,可能需要 8 个。你可以写一个测试:往 Flash 某个地址写已知数据,然后读回来对比,逐步增加 DummyCycles 直到数据正确。
采样相位问题则通过 QUADSPI_CR 的 SampleShifting 位调整。HALF_CYCLE 在多数情况下更稳,但如果你的 PCB 走线延迟很小,NO_SHIFT 也可能正常工作。这个需要结合具体硬件实测。
5.2 内存映射模式下无法写入
这是设计使然,不是 bug。内存映射模式一旦进入,QUADSPI 外设就专注于自动读时序,无法再发送编程或擦除命令。要写入必须先调用 HAL_QSPI_Abort 或者重新初始化 QUADSPI 退出映射模式,然后走间接写流程。写完后再重新配置内存映射。
实际项目里,我通常会把写操作集中处理:需要写的时候退出映射,批量写完再重新进入。频繁切换会有开销,所以尽量把写操作合并,减少切换次数。
5.3 高速时钟下数据不稳定
当 QSPI 时钟超过 80MHz 时,很多开发板会出现偶发数据错误。除了前面说的采样相位,还要检查硬件层面:QSPI 时钟线是否做了阻抗匹配,数据线是否等长,电源是否干净。W25Q128 的 VCC 建议加 100nF 和 10uF 去耦电容,靠近芯片放置。
软件层面可以尝试降低一点时钟,或者增加 CS 高电平时间(CSHT)。有些 Flash 在片选拉高后需要一定时间才能完成内部操作,CSHT 太短会导致下一次访问出错。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 读全 0xFF | QE 位未使能 | 读状态寄存器 2 确认 bit1 |
| 数据整体偏移 | DummyCycles 不对 | 逐步增加 dummy 数测试 |
| 偶发位错误 | 采样相位或硬件问题 | 调 SampleShifting,检查走线 |
| 映射模式写失败 | 模式未退出 | 先 Abort 再发写命令 |
| 高速下不稳定 | 时钟过高或电源噪声 | 降频测试,加去耦电容 |
| 擦除后读仍为旧数据 | 擦除未完成 | 用自动轮询等 WIP 清零 |
5.5 几个容易被忽略的细节
第一,W25Q128 的扇区是 4KB,块是 64KB,擦除操作最小单位是扇区。写之前必须先擦除,这是 NOR Flash 的特性,不像 EEPROM 可以字节覆盖。第二,页编程一次最多 256 字节,跨页写入会回卷到页首,导致数据覆盖。第三,状态寄存器的保护位(BP0-BP3)如果被设置,会导致部分区域无法写入,调试时可以先全部清零。
我在实际项目中还遇到过一个情况:Flash 的 HOLD 引脚在普通 SPI 模式下被拉低,切换到 QSPI 后 IO3 被占用,但硬件上还接着下拉电阻,导致四线模式下 IO3 始终为低,数据线少了一根。这种硬件遗留问题很隐蔽,查原理图时一定要确认 IO2、IO3 没有被外部电路强制拉死。
6. 性能压榨与进阶优化思路
6.1 双闪存模式与交错访问
STM32H7 的 QUADSPI 支持双闪存模式,可以同时挂两颗 Flash,IO0-IO3 共用,通过额外的片选区分。在双闪存模式下,两颗 Flash 可以交替访问,理论上把带宽再翻一倍。但这个模式对硬件要求高,两颗 Flash 的走线要尽量对称,而且软件上要处理地址映射和片选切换。对于大多数项目,单颗 W25Q128 的 QSPI 性能已经够用,双闪存更适合对存储带宽有极端需求的场景。
6.2 DMA 与双缓冲配合
虽然内存映射模式下 CPU 占用已经很低,但如果你的应用需要把 Flash 数据搬运到特定 RAM 区域做处理,用 DMA 可以进一步解放 CPU。STM32H7 的 DMA 支持外设到内存、内存到外设的搬运,配合 QUADSPI 的 FIFO 阈值中断,可以实现“读一块、处理一块”的流水线。这里要注意 DMA 的源地址在内存映射模式下是外部 Flash 映射地址,DMA 控制器需要能访问这个区域,STM32H7 的 DMA 一般都可以。
6.3 指令缓存与数据缓存的影响
STM32H7 有 L1 指令缓存和数据缓存,当你在内存映射模式下执行代码(XIP)时,缓存命中率直接影响性能。如果代码有大量顺序执行,缓存能显著提升效率;如果频繁跳转,缓存命中率低,性能会下降。对于数据读取,如果开启了数据缓存,重复读取同一区域会很快,但要注意缓存一致性——Flash 内容变了之后,缓存里的旧数据不会自动失效,需要手动清理缓存或者用非缓存地址访问。
6.4 实际项目中的取舍建议
不是所有场景都值得上 QSPI。如果你的项目只是偶尔读几个字节的参数,普通 SPI 完全够用,还省两根线。但如果涉及以下场景,QSPI 的收益就很明显:UI 资源加载(图片、字库)、音频数据流播放、高速数据记录、固件双备份切换、XIP 执行。我的一般建议是,只要你的 Flash 容量超过 4MB 且读操作频繁,就优先考虑 QSPI,硬件上多接两根线的成本远低于软件优化的代价。
最后分享一个我在调试时的小技巧:用 QUADSPI 的自动轮询模式等待擦除完成,比用软件循环读状态寄存器省心得多。配置好轮询命令和匹配值后,硬件会自动检测 WIP 位,完成后触发中断或者置标志位,CPU 在这期间可以去做别的事。这个模式在批量擦除场景下特别有用,能把 CPU 从空等中解放出来。