简介:基于STM32CubeMX的STM32F777 QSPI驱动完整工程,面向需要高速四线串行通信、外接SPI Flash或FRAM等外部存储器的嵌入式开发者。压缩包内含581个文件,以365个C源文件和135个头文件为主,辅以汇编启动文件、ICF链接脚本和IOC工程配置,整体约4.95MB,目录结构完整,便于直接移植;IOC文件可通过STM32CubeMX重新生成代码,方便按实际需求调整引脚与参数。目前已有847人学习下载,作者标注已通过实际测试验证。工程利用STM32CubeMX自动生成HAL/LL库代码,覆盖QSPI初始化、四线与单线模式选择、时钟频率设定、CPOL/CPHA极性和相位调整,以及地址模式和数据宽度配置;同时提供HAL_QSPI_Init、HAL_QSPI_Transmit、HAL_QSPI_Receive等读写接口,并给出DMA传输与中断处理的参考实现。这些代码能帮助开发者快速搭建外部存储器通信链路,减少手动配置寄存器的工作量,提升驱动开发和调试效率。 用STM32CubeMX调QSPI驱动外挂NOR Flash这事,我在H750板子上折腾了不少时间,今天把配置、代码和踩坑记录一次说清楚。很多人问QSPI和普通SPI到底怎么选、地址映射怎么开、HAL库命令怎么配合,这篇文章就从CubeMX操作讲到内存映射读取数据,适合正在用STM32外挂W25Q系列Flash、或者准备用QSPI做代码存储和OTA升级的开发者。
1. 项目背景与方案选择
1.1 为什么用QSPI而不是普通SPI
QSPI全称Quad SPI,本质上是在标准SPI基础上扩展了数据线,把原来的MOSI和MISO扩充成四根双向IO线,所以在同一时钟频率下,四线模式的理论带宽是普通SPI的四倍。比如标准SPI每个时钟周期只能传1bit,QSPI四线模式可以一次传4bit,这个差距在读取大容量Flash时非常明显。
我做H750项目时,片内Flash只有128KB,Bootloader加基础功能就快塞满了,后来外挂一颗W25Q128(16MB)做应用存储和XIP执行。如果用普通SPI读Flash,只能一页页发命令把数据搬到RAM,启动时间会让人崩溃。QSPI配合内存映射模式后,CPU直接访问0x90000000起始的地址段,跟读内部Flash一样简单,而且支持直接从外部Flash取指执行代码,这是标准SPI没法做到的。
| 维度 | 标准SPI | QSPI四线模式 |
|---|---|---|
| 数据线数量 | MOSI、MISO两根 | IO0~IO3四根 |
| 带宽(同频) | 1bit/时钟 | 4bit/时钟 |
| 内存映射/XIP | 基本不支持 | 主流MCU支持 |
| 典型用途 | 传感器、EEPROM | NOR Flash、代码存储、GUI资源 |
选QSPI还有一个隐性优势:很多中高端MCU的QSPI外设内部带FIFO和命令队列,能减轻CPU负担。虽然HAL库封了一层,但底层是硬件状态机在工作,比GPIO模拟SPI高效太多了。
1.2 用CubeMX生成QSPI驱动的三个理由
第一,时钟树不会配错。QSPI的时钟来源于AHB总线,在H7系列上还要经过分频器。手动配置时经常出现两个问题:忘了使能QUADSPI外设时钟,或者分频系数设置不当导致Flash读写不稳定。CubeMX图形化界面里能看到时钟链路,分频后到底是多少MHz一目了然。
第二,引脚冲突提前暴露。H750的QSPI引脚经常和FMC、SDMMC复用,手动对着数据手册一个个查很痛苦。CubeMX勾选Quad SPI后,如果引脚有冲突它会直接红色报错,省去了后面焊板子再改线的麻烦。
第三,HAL库底层代码由ST官方维护。QSPI涉及的命令模式、间接模式、内存映射模式切换,寄存器配置链非常长,手写标准外设库版本不仅代码量大,还容易漏掉某个状态位。CubeMX生成的基础代码虽然也要自己补Flash命令,但外设初始化、中断时钟这些不会出错。对刚接触QSPI的开发者来说,先用CubeMX跑通流程,再回来看参考手册,效率最高。
2. 上手前的关键细节
2.1 QSPI与NOR Flash的基础认知
QSPI外设只是负责把命令、地址、数据按照约定的时序发送给Flash,真正决定能不能正确读写的是你用的那颗Flash芯片。以最常见的W25Q128JV为例,它接受标准的命令码,比如0x9F读ID、0x03标准读、0x6B四线快速读、0x02页编程、0x20扇区擦除。QSPI控制器和Flash之间是主从关系,所有操作都从主控拉低CS开始,发完命令后按需切换数据线方向。
初学者要分清两个概念:命令模式和内存映射模式。命令模式是你主动调用HAL_QSPI_Command,指定这次操作的命令、地址、数据长度和线数,然后调用HAL_QSPI_Transmit或HAL_QSPI_Receive完成一次具体的数据交换。内存映射模式则完全不同,主控配置好之后,CPU对外部Flash地址的普通内存访问会被QSPI外设自动转成Flash读操作,你不用再手动发命令,直接读指针就行。写操作、擦除操作不能在内存映射模式下进行,必须先退出映射。
Flash内部还有一个状态寄存器,其中WIP位(Bit0)代表是否忙。每次页编程、扇区擦除、芯片擦除之后都可能置位,所以写或擦除后要轮询这个位,直到它变回0。因为QSPI协议本身没有“就绪”信号,只能通过读状态寄存器来判断,这几乎是所有QSPI驱动代码里必须有的函数。
2.2 CubeMX中QSPI配置面板逐个解释
CubeMX里QUADSPI的配置参数看起来不多,但每个都会影响时序。我通常关注这几个:
- Clock Prescaler:分频系数。假设H750的AHB时钟是240MHz,这里填4,最终QSPI时钟就是60MHz。W25Q128的标准读命令最高支持50MHz,四线快速读可以到133MHz,但上线还是要看布线和Flash型号,建议先填大一点留余量。
- Flash Size:这个参数填的是“地址位数减1”,而不是容量数值。比如16MB的Flash需要24位地址,这里就填23。填错了会导致地址映射错位,读写偏移全部混乱。
- Sample Shift:采样点移位。当Flash高速回传数据时,主控在哪个时钟沿采样很关键。如果读ID正常但大批量读取偶发错位,切换这个参数往往能救回来。
- Clock Polarity / Phase:W25Q系列一般支持Mode 0,对应CPOL=0、CPHA=0,保持默认即可。
- 发送指令和地址的线数模式:CubeMX里没有完全给出来,很多需要在自写命令结构体里设置。比如快速读用单线发命令、四线发数据和地址,需要把InstructionMode设为QSPI_INSTRUCTION_1_LINE,AddressMode设为QSPI_ADDRESS_4_LINES。
我建议在配置之前先去Flash数据手册里查出你要用的命令码、Dummy Cycles和线数模式。很多人的问题不是出在CubeMX的默认参数,而是出在自写命令时Dummy Cycles填错。比如四线快速读0x6B通常需要8个Dummy Cycles,你填成0,Flash根本不会返回正确数据。
3. 实操:从工程创建到内存映射读取
3.1 新建工程并配置CubeMX
下面以STM32H750VBT6为例跑一遍完整流程。打开STM32CubeMX,新建工程,选择芯片型号。左侧分类里找到Connectivity -> QUADSPI,勾选Quad SPI模式。打开Clock Configuration页面,找到QUADSPI时钟链路,确认AHB分频后的时钟频率。我习惯把QSPI时钟先设在40MHz左右,等所有功能调通了再往上提。
Parameter Settings里,Clock Prescaler填6(如果AHB=240MHz,则QSPI=40MHz),Flash Size填23,Sample Shift选Half(如果后面高速读取有问题再切换)。生成工程前,Project Manager里把Toolchain选成MDK-ARM或STM32CubeIDE,生成后打开工程。
这里有一个关键点:CubeMX生成的代码只包含QSPI外设的初始化,并不会自动配置Flash的命令。一切Flash操作都要你自己组织命令结构体,这是HAL库QSPI驱动和普通SPI驱动最大的不同。你可以在qspi.c里添加几个自用函数,把常用的读ID、读写、擦除封装起来。
3.2 用HAL库完成Flash ID读取和读写
读取JEDEC ID是验证接线和时序最直接的方法。命令码0x9F,返回三个字节,分别是厂商ID(W25Q系列是0xEF)、内存类型和容量代码。参考代码:
uint8_t QSPI_ReadID(uint32_t *id) { QSPI_CommandTypeDef cmd = {0}; uint8_t data[3] = {0}; cmd.Instruction = 0x9F; cmd.DummyCycles = 0; cmd.NbData = 3; cmd.DataMode = QSPI_DATA_1_LINE; if (HAL_QSPI_Command(&hqspi, &cmd, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK) { return 1; } if (HAL_QSPI_Receive(&hqspi, data, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK) { return 2; } *id = (data[0] << 16) | (data[1] << 8) | data[2]; return 0; }这段代码的逻辑是:先通过HAL_QSPI_Command告诉外设“我要发一个命令,命令码是0x9F,读取3个字节”,再通过HAL_QSPI_Receive把数据从数据线收回来。命令模式和数据模式都可以单独指定。对于0x9F这种标准读ID命令,全程单线即可,所以DataMode用QSPI_DATA_1_LINE。
写操作比读操作多一个步骤:写使能。发送页编程命令0x02之前,必须先发0x06命令把Flash内部的WEL位置位。最后还要等待WIP位清零。封装一个等待忙函数:
uint8_t QSPI_WaitBusy(uint32_t timeout_ms) { QSPI_CommandTypeDef cmd = {0}; uint8_t status = 0; uint32_t tick = HAL_GetTick(); do { memset(&cmd, 0, sizeof(cmd)); cmd.Instruction = 0x05; cmd.NbData = 1; cmd.DataMode = QSPI_DATA_1_LINE; if (HAL_QSPI_Command(&hqspi, &cmd, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK) { return 1; } if (HAL_QSPI_Receive(&hqspi, &status, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK) { return 1; } if ((status & 0x01) == 0) { return 0; } } while (HAL_GetTick() - tick < timeout_ms); return 1; }这个函数是写擦操作的生命线。擦除一个4KB扇区通常耗时30~100ms,所以timeout_ms我建议给1000ms,避免环境差异导致偶尔超时。另外,每次填充命令结构体后,都要检查一下上一个命令残留的Address和DummyCycles是否被清了。用memset清零整个结构体是最保险的。
页写入时要注意页边界问题。W25Q系列页大小一般是256字节,一页只能写256字节,超过页边界会自动回绕到页开头。如果你要写入1000字节,就得拆成4页,每页的起始地址都要对齐到页边界。如果没有做跨页处理,数据会出现“换行”覆盖,这是写入不稳定最典型的原因。
3.3 开启内存映射模式直接寻址
内存映射模式是QSPI的高级用法,也是H750这类芯片托管外部Flash的底气所在。开启方式不复杂,配置好映射结构和Flash读取模式后,调用HAL_QSPI_MemoryMapped即可。下面是一段开启映射并读取数据的示例:
QSPI_MemoryMappedTypeDef memCfg = {0}; memCfg.TimeOutActivation = QSPI_TIMEOUT_COUNTER_DISABLE; memCfg.TimeOutPeriod = 0; memCfg.ISSMode = QSPI_ISS_DISABLE; memCfg.FifoThreshold = 4; if (HAL_QSPI_MemoryMapped(&hqspi, &memCfg) != HAL_OK) { Error_Handler(); } uint8_t *flash_ptr = (uint8_t *)0x90000000; uint32_t val = flash_ptr[0x1000];开启映射前,要先通过命令模式配置好Flash的读取命令。我一般用四线快速读命令0x6B,地址模式设为四线,Dummy Cycles设为8。这样内存映射后每次CPU访问都会走四线快速读,性能比标准读命令高很多。具体代码是在调用HAL_QSPI_MemoryMapped之前,先HAL_QSPI_Command发送一个0x6B命令,把Flash内部状态切换到快速读模式,然后设置QSPI外设进入内存映射模式。如果省略这一步,CubeMX默认的映射可能使用标准读命令,虽然也能读,但速度慢不少。
H750的外部QSPI Bank1映射地址是0x90000000,Bank2是0x70000000。不同系列地址可能不同,使用前务必查参考手册。映射地址对应的空间大小是你在CubeMX里配置的Flash Size参数决定的,如果地址范围超过Flash实际容量,硬件上会访问异常。
映射模式下还有一个“退出映射”的坑。如果你要执行擦除或写入操作,一定要先调用HAL_QSPI_DeInit或者发送退出映射命令,让QSPI外设回到间接模式。否则你以为在发命令,实际数据被当成了映射地址访问,轻则操作无效,重则触发硬件错误。我在做OTA升级时,就因为没有退出映射而卡了很久。
4. 排查记录:我踩过的QSPI坑
4.1 读ID失败和时序不稳
第一次调QSPI时,JEDEC ID读出来是0xFFFFFF,排查了很久。先把分频系数调大,让QSPI时钟降到10MHz左右,然后读ID就正常了。这说明问题出在初始频率太高,Flash返回的数据主控采不到。后来我把Prescaler重新算好,又微调了Sample Shift,60MHz下也能稳定读ID。
还有一个被忽略的点是命令之间的CS高电平时间。如果两个命令间隔太短,Flash内部状态机还没准备好,下一轮命令就会被忽略。遇到偶发读取失败时,可以在每条命令之间加一点微秒级延时,或者确保HAL库的Timeout参数足够大。逻辑分析仪在这种场景下非常有用,能直接看到CS拉低是否过早、CLK频率是否超标。
4.2 写入不生效和写保护陷阱
写操作不生效,最容易被忽视的原因有两个:一是没有发写使能命令0x06,二是Flash状态寄存器里的BP位(Block Protection)被置位。W25Q128在新出厂时BP位的默认状态不一定为0,如果处于保护状态,擦除和写入都会被忽略。解决办法是发送Write Status Register命令(0x01),把状态寄存器值写成0x00,注意这之前也必须先发写使能命令。
另外,页写入时跨页回绕的问题前面提到过。很多调试“写进去的数据不对”其实不是QSPI驱动的问题,而是数据在Flash页边界处被回绕覆盖了。写一个通用写函数时,建议增加跨页拆分逻辑,比如先算出当前地址到页末尾还剩多少字节,大于剩余长度就分两次写。这个细节能避免后续在项目里反复踩坑。
4.3 内存映射模式下数据错乱与Cache问题
内存映射模式跑通后,我在烧写新固件后遇到了“有时读到旧数据”的诡异情况。这个不是QSPI本身的问题,而是H7的DCache缓存了映射地址的数据。Flash内容更新了,但Cache里还是旧值,CPU读到的就是老数据。解决思路有两个:最简单的是把0x90000000这段映射区域配置成不缓存,或者在读取关键区域前主动SCB_InvalidateDCache_by_Addr。
如果你的项目需要高性能,可以把这段区域配成Write-Back并定期做Cache维护;如果只是跑代码和存配置,我建议直接用MPU把QSPI映射区设为Device或Non-cacheable,省心不容易出错。CubeMX默认不一定帮你配置MPU,需要手动增加一个MPU Region,基地址0x90000000,大小和Flash容量匹配,属性选“b Non-cacheable”即可。
另一个常见错误是在启动阶段做XIP执行时,没有关闭Cache or没有配置MPU,导致代码跳转到外部Flash后随机死机。H750的XIP性能和Cache配置强相关,调的时候建议一步步来,先在非映射模式下验证读写,再开映射,最后开Cache,千万不要一上来全开,出了问题不好定位。
老实说,QSPI驱动本身并不难,真正耗时间的都是时序细节和缓存一致性。我最后再分享一个实际经验:在CubeMX生成工程后,先不做业务逻辑,先写一个自测序列——读ID、擦除一个扇区、页写入、回读校验、退出映射、再进入内存映射读取。每个步骤打印一个状态码。这样以后无论换板子还是换Flash型号,都能快速定位问题出在硬件还是驱动。等你把这套驱动跑顺了,再去看H750的XIP、Bootloader跳转那些进阶玩法,就会顺手很多。
本文还有配套的精品资源,点击获取