STM32中QSPI扩展Flash实战:从协议到代码的完整指南
你有没有遇到过这样的尴尬?——项目做到一半,发现MCU片内Flash快爆了。UI资源、语音文件、多套固件镜像全堆在一起,编译器报错“.text段溢出”,而你手里的STM32F407只剩1MB Flash……这时候,是换更大封装的芯片?还是砍功能?
其实,还有一条高性价比的出路:用QSPI外挂一颗NOR Flash。
今天我们就来手把手带你打通这条“扩容高速路”。不讲虚的,从硬件连接、寄存器配置到XIP就地执行,一步步教你如何在STM32上把外部Flash变成“第二内存”。
为什么是QSPI?不是SPI,也不是并口
先说结论:QSPI是在性能、引脚数和成本之间最平衡的选择。
传统SPI虽然简单,但带宽有限,最高也就30~50Mbps,读个图片都卡。而并行NOR Flash虽然快,却要占用14个以上IO口,PCB布线复杂,成本也高。
那QSPI呢?
- 只用6根线(CLK, CS, IO0~IO3)
- 理论速率320Mbps(80MHz × 4线)
- 支持XIP,代码可以直接运行
- 硬件自动管理时序,CPU几乎不参与
简直是为嵌入式系统量身定制的“外挂硬盘”。
我曾经在一个工业HMI项目里,主控用的是STM32F767,原本打算把所有UI图片打包进内部Flash,结果一算:光字体+图标就占了800KB,再加动画帧序列,直接破限。后来改用QSPI接了一颗W25Q128(16MB),不仅空间绰绰有余,还能实现动态加载页面资源,流畅度反而提升了。
这,就是QSPI的价值。
QSPI不只是“四线SPI”:它到底强在哪?
很多人以为QSPI就是“SPI接四根数据线”,其实远不止如此。
STM32内置的QUADSPI控制器是一个高度集成的DMA-capable外设,它能做的事情比你想象得多:
- 自动生成完整的通信帧(命令+地址+空周期+数据)
- 支持双闪切换(Dual Flash Mode)
- 可配置FIFO阈值触发中断或DMA请求
- 最关键的是:支持内存映射模式(Memory-Mapped Mode)
什么叫内存映射?
意思是你可以把外部Flash映射到CPU的地址空间,比如0x90000000开始的位置。之后,只要访问这个地址范围,硬件会自动发起QSPI读操作——就像读SRAM一样自然。
这就实现了真正的XIP(eXecute In Place):MCU可以直接从外部Flash取指执行,无需先把固件搬进内部Flash或RAM。
⚠️ 注意:写和擦除仍然需要切回间接模式,毕竟Flash不能边读边写。
芯片选型实战:W25Q128为何成为首选?
市面上支持QSPI的Flash不少,但我推荐新手从W25Q128JV入手,原因很实在:
- 容量够大:128M-bit = 16MB,足够塞下两套固件做A/B升级;
- 兼容性好:几乎所有STM32开发板例程都以它为例;
- 手册清晰:Winbond的数据手册连dummy cycle都标得明明白白;
- 封装友好:常见SOIC-8或WSON-8,手工焊接无压力。
它的内部结构也很规整:
- 每页 256 字节
- 每扇区 4KB(16页)
- 每块 64KB(16扇区)
- 总共 256 块 → 刚好 16MB
写操作必须按“页”进行(≤256字节),擦除则至少按“扇区”(4KB起)。所以你在设计文件系统时就得考虑这些物理限制。
另外,别忘了每次写之前要发一个Write Enable(0x06)命令,否则Flash会无视你的写入请求——这是新手最容易踩的坑之一。
硬件怎么连?6根线搞定一切
典型的连接方式如下:
| STM32引脚 | 外部Flash引脚 | 功能说明 |
|---|---|---|
| QSPI_CLK | SCK | 时钟信号 |
| QSPI_CS | /CS | 片选,低电平有效 |
| QSPI_IO0 | IO0 | 双向数据线0 |
| QSPI_IO1 | IO1 | 双向数据线1 |
| QSPI_IO2 | IO2 | 双向数据线2 |
| QSPI_IO3 | IO3 | 双向数据线3 |
电源部分别偷懒!一定要在VCC引脚靠近芯片处加一个100nF陶瓷电容,最好再并联一个1μF钽电容。如果供电不稳定,高速读取时极易出现数据错乱。
走线也有讲究:
- 尽量等长,总长度建议不超过10cm;
- 避免锐角拐弯,减少反射;
- 不要和其他高频信号(如USB、Ethernet)平行走线;
- 若板子较大,可在每根信号线上串联一个22Ω电阻抑制振铃。
GPIO选择上,优先使用MCU原生QSPI引脚,避免使用AF重映射,否则可能影响最大时钟频率。
软件驱动核心流程:HAL库下的QSPI初始化
我们以STM32H7系列为例,使用HAL库完成QSPI初始化。
第一步:时钟与GPIO配置
确保QSPI时钟源已使能(通常来自PLL),并通过CubeMX配置对应引脚为QUADSPI复用功能。
__HAL_RCC_QSPI_CLK_ENABLE(); // GPIO 初始化略(由CubeMX生成)第二步:QUADSPI参数设置
QSPI_HandleTypeDef hqspi; void MX_QUADSPI_Init(void) { hqspi.Instance = QUADSPI; hqspi.Init.ClockPrescaler = 1; // 分频=1 → SCK = 200MHz / 2 = 100MHz hqspi.Init.FifoThreshold = 4; // FIFO达4字节触发中断/DMA hqspi.Init.SampleShifting = QSPI_SAMPLE_SHIFTING_HALFCYCLE; // 半周期采样,提升稳定性 hqspi.Init.ChipSelectHighTime = QSPI_CS_HIGH_TIME_6_CYCLE; // CS高时间 ≥ 5 cycles hqspi.Init.ClockMode = QSPI_CLOCK_MODE_3; // CPOL=1, CPHA=1(匹配W25Q128) hqspi.Init.FlashSize = POSITION_VAL(0x1000000) - 1; // 2^24 = 16MB hqspi.Init.BurstLength = QSPI_BURSTLENGTH_SINGLE; // 默认单次传输 hqspi.Init.DualFlash = QSPI_DUALFLASH_DISABLE; if (HAL_QSPI_Init(&hqspi) != HAL_OK) { Error_Handler(); } }这里有几个关键点要特别注意:
- ClockPrescaler = 1:意味着SCK频率为100MHz(假设QSPI_CLK=200MHz)。初次调试建议先设为4(50MHz),稳定后再拉高。
- SampleShifting:由于信号传播延迟,在高速下建议启用半周期偏移采样。
- FlashSize:必须准确设置,否则超过边界访问会导致异常。
寄存器级操作详解:一次Quad Read是怎么发生的?
让我们深入看看一次“四线快速读”的全过程。
以读取W25Q128为例,使用的指令是0xEB(Fast Read Quad Output),其通信帧结构如下:
| 阶段 | 内容 | 数据线数 |
|---|---|---|
| Instruction | 0xEB | 1线 |
| Address (24bit) | 目标地址 A[23:0] | 4线 |
| Dummy Cycles | 6个空周期 | 4线 |
| Data Output | 连续输出数据 | 4线 |
注意:Dummy Cycles是必须的!因为Flash需要时间准备数据输出。W25Q128在104MHz下要求6个dummy cycles,若省略,前几个字节会错乱。
对应的HAL配置如下:
HAL_StatusTypeDef QSPI_Read(uint8_t *buf, uint32_t addr, uint32_t size) { QSPI_CommandTypeDef cmd = {0}; cmd.InstructionMode = QSPI_INSTRUCTION_1_LINE; cmd.Instruction = 0xEB; // Quad Fast Read cmd.AddressMode = QSPI_ADDRESS_4_LINES; cmd.AddressSize = QSPI_ADDRESS_24_BITS; cmd.Address = addr; cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE; cmd.DataMode = QSPI_DATA_4_LINES; cmd.NbData = size; cmd.DummyCycles = 6; cmd.DdrMode = QSPI_DDR_MODE_DISABLE; cmd.SIOOMode = QSPI_SIOO_INST_EVERY_CMD; if (HAL_QSPI_Command(&hqspi, &cmd, HAL_TIMEOUT_DEFAULT) != HAL_OK) return HAL_ERROR; return HAL_QSPI_Receive(&hqspi, buf, HAL_TIMEOUT_DEFAULT); }看到没?整个过程不需要手动发送每一位,全部由硬件自动完成。你只需要告诉控制器:“我要发什么命令、地址多少、要不要dummy、数据多长”,剩下的交给QUADSPI外设。
这就是专用控制器的优势。
如何进入内存映射模式?实现真正XIP
这才是QSPI最大的杀伤力所在。
一旦进入内存映射模式,外部Flash就会被映射到地址0x90000000开始的空间。你可以像访问数组一样读取其中的内容:
uint8_t *flash_ptr = (uint8_t*)0x90000000; printf("First byte: 0x%02X\n", flash_ptr[0]);甚至可以直接跳转过去执行代码!
启动流程示意:
- Bootloader检测是否有新固件在QSPI Flash中
- 配置QSPI为内存映射模式
- 关闭中断,设置栈指针,跳转至
0x90000000 + offset执行
具体切换代码如下:
HAL_StatusTypeDef QSPI_EnterMemoryMappedMode(void) { QSPI_CommandTypeDef cmd; QSPI_MemoryMappedTypeDef mm_cfg; // 先发送普通读命令模板 cmd.InstructionMode = QSPI_INSTRUCTION_1_LINE; cmd.Instruction = 0xEB; // Quad Read cmd.AddressMode = QSPI_ADDRESS_4_LINES; cmd.AddressSize = QSPI_ADDRESS_24_BITS; cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE; cmd.DataMode = QSPI_DATA_4_LINES; cmd.DummyCycles = 6; cmd.DdrMode = QSPI_DDR_MODE_DISABLE; cmd.SIOOMode = QSPI_SIOO_INST_EVERY_CMD; mm_cfg.TimeOutActivation = QSPI_TIMEOUT_COUNTER_DISABLE; mm_cfg.TimeOutPeriod = 0; return HAL_QSPI_MemoryMapped(&hqspi, &cmd, &mm_cfg); }调用成功后,任何对0x90000000 ~ 0x90FFFFFF范围的读访问都会自动转换为QSPI读操作。
✅ 提示:某些型号支持AHB-Lite缓存(如STM32H7的ART Accelerator),可进一步提升连续读取性能。
常见坑点与调试秘籍
❌ 问题1:读出来全是0xFF或0x00?
可能是以下原因:
- Flash未正确上电(检查VCC和电容)
- GPIO配置错误(是否用了正确的AF功能?)
- Clock Mode不匹配(W25Q128默认用Mode 3)
- Dummy Cycles设置不足
👉 解法:先降低时钟到40MHz,dummy设为8,确认能读出JEDEC ID(0xEF 0x40 0x18)再说其他。
❌ 问题2:XIP模式下程序跑飞?
记住:内存映射模式只支持读!如果你试图在该区域写Flash,或者执行了非法跳转,后果自负。
此外,某些Cortex-M内核会对指令预取做优化,建议在跳转前插入:
DSB ISB清空流水线。
❌ 问题3:DMA读取卡住?
检查FIFO Threshold设置是否合理。若设得太低(如1字节),中断太频繁;太高(如16字节)可能导致溢出。
推荐值:4~8字节,配合DMA双缓冲使用效果最佳。
实战应用场景推荐
场景1:Bootloader + A/B固件升级
将两套固件分别存于QSPI Flash的不同区域,Bootloader根据标志位决定加载哪一套。更新时只需下载到备用区,下次启动自动切换。
场景2:图形界面资源存储
把BMP/PNG解码后的像素数据、字体字模、动画帧序列全放进外部Flash,运行时按需加载,大幅节省内部Flash。
场景3:音频播报系统
预存多段语音提示(PCM或ADPCM格式),通过DMA持续读取播放,CPU零负担。
场景4:AI模型部署
轻量级神经网络模型(如TensorFlow Lite for Microcontrollers)常达几百KB,放在QSPI Flash中,推理时分块加载即可。
写在最后:QSPI不是终点,而是起点
当你熟练掌握QSPI后,你会发现很多以前不敢想的功能变得可行了:
- 固件热更新不用重启
- UI动画可以更丰富
- 设备能携带更多本地知识库
- 甚至可以尝试从外部Flash引导Linux(配合FSBL)
更重要的是,这种“软硬协同”的思维会让你在系统架构设计上更进一步。
所以,别再让Flash容量限制你的想象力了。
6根线,16MB空间,高达100MHz的访问速度——现在全都掌握在你手中。
如果你正在做一个需要大容量存储的项目,不妨试试QSPI。说不定,它就是那个让你项目“起飞”的转折点。
有什么问题欢迎留言讨论,我可以分享更多实际项目中的QSPI优化技巧,比如如何做坏块管理、怎样提升多设备兼容性等等。