1. 为什么W25Q128在STM32H7上值得认真对待
W25Q128这颗Flash芯片,搞嵌入式的基本都见过。128Mbit容量、SPI接口、便宜、好买、资料全,很多人拿它存个字库、放个图片、记点日志就完事了。但如果你手里正好有一块STM32H7的板子,还只用普通SPI模式去读写它,那说实话有点浪费。STM32H7系列带QSPI控制器,配合W25Q128的Quad模式,理论带宽能拉到普通SPI的四倍左右。这个提升不是纸面数字,在实际项目里,比如你要做GUI刷图、音频采样缓存、或者跑个轻量文件系统,差距非常明显。
我最早接触这个组合是在一个工业HMI项目上,屏幕刷新时要从Flash读大量图片数据,普通SPI模式下刷一屏有明显撕裂感,换成QSPI之后流畅度直接上了一个台阶。后来陆续在几个数据采集项目里也用了这套方案,踩了不少坑,也总结了一些经验。这篇文章就把W25Q128在STM32H7上跑QSPI的完整思路、关键配置、实操步骤和避坑经验都梳理一遍。
适合谁看?如果你已经会用STM32的普通SPI读写W25Q128,想进一步榨干硬件性能,那这篇正好。如果你刚接触QSPI,也没关系,我会从最基本的原理讲起,把每个关键寄存器配置的逻辑都说清楚。前提是你对STM32的HAL库或者寄存器操作有基本概念,C语言要能看懂。
注意:本文所有内容基于公开的芯片数据手册和常见工程实践,具体参数以你手头芯片的最新手册为准。
2. QSPI和普通SPI到底差在哪
2.1 从四根线说起
普通SPI大家都很熟,四根线:SCK、MOSI、MISO、CS。数据一位一位地传,全双工。W25Q128支持标准SPI模式,最高时钟频率大概在80MHz到104MHz之间(具体看型号后缀和电压)。算一下,80MHz下理论带宽是10MB/s,实际因为命令开销、等待周期,能跑到5MB/s就算不错了。
QSPI的核心变化在于数据线。它把MOSI拆成IO0到IO3四根双向数据线,SCK和CS保留。在Quad模式下,每个时钟周期可以传4位数据,而不是1位。同样是80MHz时钟,理论带宽直接变成40MB/s。当然实际也达不到理论值,但跑到20MB/s以上是很正常的。
这里有个概念要区分清楚:QSPI是STM32H7外设的名字,Quad SPI是W25Q128的工作模式。STM32H7的QSPI控制器支持单线、双线、四线模式,也支持内存映射模式。W25Q128这边,你需要发特定命令让它进入Quad模式,之后才能用四根线传数据。
2.2 STM32H7的QSPI外设有什么特别的
STM32H7的QSPI控制器和F4、F7上的有点不一样。它支持双闪存模式,可以同时接两颗Flash;支持内存映射模式,映射后可以直接用指针读Flash数据,像读内部Flash一样方便;还有独立的FIFO和DMA请求线。
最关键的是内存映射模式。配置好之后,W25Q128的地址空间会映射到STM32H7的0x90000000起始的区域。你定义一个指针指向这个地址,直接解引用就能读数据,CPU不需要干预传输过程。这对执行代码或者快速读大量数据来说太方便了。
但内存映射模式只支持读,写操作还是得走间接模式。而且进入内存映射模式之前,必须先配置好Flash的Quad使能位,否则映射过去读出来的数据全是错的。
2.3 四倍速是怎么算出来的
很多人说四倍速,其实是个粗略说法。准确讲,在相同SCK频率下,Quad模式每个周期传4位,标准SPI每个周期传1位,理论上是4倍。但实际有效带宽还受几个因素影响:
- 命令阶段的开销:每次传输都要发命令、地址、模式位,这些在Quad模式下也可以优化
- 等待周期:Flash内部有读取延迟,需要配置Dummy Cycles
- DMA效率:如果不用DMA,CPU搬运数据也会成为瓶颈
- 总线仲裁:STM32H7的总线矩阵很复杂,QSPI的优先级需要合理配置
所以实际测下来,读性能提升通常在3到3.5倍之间,写性能提升更少一些,因为写操作本身有页编程时间限制。但即便如此,这个提升也足够让很多应用场景发生质变。
3. 硬件连接与引脚配置要点
3.1 引脚映射不是随便选的
STM32H7的QSPI引脚是复用在特定GPIO上的,不是任意引脚都能用。以常见的STM32H743为例,QSPI的引脚分布在PORTB和PORTF上:
| 信号 | 引脚 | 复用功能 |
|---|---|---|
| QSPI_CLK | PB2 | AF9 |
| QSPI_CS | PB6 | AF10 |
| QSPI_IO0 | PF8 | AF10 |
| QSPI_IO1 | PF9 | AF10 |
| QSPI_IO2 | PF7 | AF9 |
| QSPI_IO3 | PF6 | AF9 |
注意IO2和IO3的复用功能编号和IO0、IO1不一样,这个在配置GPIO的时候很容易搞错。我见过有人把IO2配成AF10,结果Quad模式下读数据高四位全是0,查了半天才发现是复用功能选错了。
另外,PB2作为QSPI_CLK,它的复用功能是AF9,不是AF10。这些细节在STM32H7的参考手册GPIO复用表里都能查到,但表格很大,容易看串行。建议配置的时候直接对着CubeMX生成的代码检查一遍。
3.2 上拉电阻和走线
W25Q128在Quad模式下,IO0到IO3都是双向的。当Flash没有驱动这些线的时候,它们处于高阻态。如果这时候STM32H7的对应引脚也处于输入模式且没有上拉,线路上就会有浮空风险,可能导致误触发或者功耗增加。
常见做法是在IO0到IO3上各加一个10K到47K的上拉电阻到VCC。CS线也建议加上拉,保证在空闲时Flash不被选中。SCK线一般不需要上拉,但如果你走线比较长,可以考虑加一个串联电阻来抑制振铃。
走线方面,QSPI的时钟频率可以跑到100MHz以上,这时候PCB走线就不能太随意了。几条数据线尽量等长,误差控制在几个毫米以内。SCK线要远离其他高速信号,避免串扰。如果板子空间允许,QSPI的走线最好走在同一层,参考平面完整。
实操心得:我在一个项目里因为QSPI走线跨了分割地平面,导致高速读取时偶尔出现位错误。后来在Flash电源引脚旁边加了0.1uF和1uF的退耦电容,问题才解决。退耦电容一定要靠近Flash的VCC引脚,越近越好。
3.3 电源和电压匹配
W25Q128的供电范围是2.7V到3.6V,STM32H7的IO电压一般是3.3V,两者可以直接对接。但要注意,如果你用的是W25Q128JV系列,它的IO电压和核心电压是分开的,VCC和VIO都要接。有些封装把这两个引脚合并了,有些没有,看具体型号。
STM32H7的QSPI引脚输出能力有限,如果走线很长或者挂了多个从设备,可能需要加缓冲器。但一般单颗Flash、走线在10厘米以内,直接连就行。
4. CubeMX配置与底层驱动搭建
4.1 时钟树配置
QSPI的时钟来源是PLL1Q或者PLL2R,具体取决于你的时钟树设计。以STM32H743为例,假设系统时钟跑到400MHz,PLL1Q可以配置到200MHz,然后QSPI控制器内部再分频。
W25Q128在Quad模式下的最高时钟频率,普通型号是104MHz,但那是理想条件下的。实际跑的时候,受PCB走线、电源质量、温度影响,能稳定跑到80MHz到100MHz就不错了。我一般先配置到80MHz,跑通了再往上试。
CubeMX里QSPI的时钟分频系数是1到256,实际时钟等于输入时钟除以分频系数。比如输入200MHz,分频系数设为2,QSPI时钟就是100MHz。但注意,这个100MHz是SCK的输出频率,在Quad模式下每个周期传4位,所以数据速率是400Mbit/s,也就是50MB/s。当然实际达不到,因为有命令开销和等待周期。
4.2 QSPI参数配置
CubeMX里QSPI的配置项比较多,我挑几个关键的讲:
Clock Prescaler:分频系数,决定了SCK频率。先设大一点,跑通再减小。
FIFO Threshold:FIFO阈值,影响中断和DMA请求的触发时机。一般设为4或者8,看你的传输粒度。
Sample Shifting:采样偏移,有None和Half Cycle两个选项。如果高速下读数据不稳定,可以试试Half Cycle,让采样点往后移半个周期。
Flash Size:Flash容量,W25Q128是128Mbit,也就是16MB。这个值影响内存映射模式的地址范围。
CS High Time:片选高电平保持时间,单位是时钟周期。W25Q128要求CS拉高后至少保持50ns,按100MHz算就是5个周期。设成8比较保险。
Flash ID:这个不是必填的,但填上之后CubeMX会自动帮你计算一些参数。
4.3 命令配置表
QSPI控制器需要你告诉它,读、写、擦除分别用什么命令,地址是几位,数据是几位,Dummy Cycles要几个。W25Q128的Quad读命令是0xEB,Quad页编程是0x32,扇区擦除是0x20。
以Quad读为例,配置如下:
| 参数 | 值 | 说明 |
|---|---|---|
| Instruction | 0xEB | Quad I/O Fast Read |
| Address | 24位 | 3字节地址 |
| Alternate Bytes | 0 | 不用 |
| Dummy Cycles | 6 | 80MHz下需要6个 |
| Data | 4线 | Quad模式 |
| Instruction Mode | 1线 | 命令阶段还是单线 |
Dummy Cycles的数量和SCK频率有关。W25Q128手册里有个表格,80MHz下Quad读需要6个Dummy Cycles,100MHz下需要8个。这个值设少了会读到错误数据,设多了会浪费带宽。我一般按手册推荐值加1来设,留点余量。
4.4 GPIO初始化代码
CubeMX生成的GPIO初始化代码大概长这样:
GPIO_InitStruct.Pin = GPIO_PIN_2|GPIO_PIN_6; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; GPIO_InitStruct.Alternate = GPIO_AF9_QSPI; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7; GPIO_InitStruct.Alternate = GPIO_AF10_QSPI; HAL_GPIO_Init(GPIOF, &GPIO_InitStruct); GPIO_InitStruct.Pin = GPIO_PIN_8|GPIO_PIN_9; GPIO_InitStruct.Alternate = GPIO_AF10_QSPI; HAL_GPIO_Init(GPIOF, &GPIO_InitStruct);注意PB6同时出现在两个配置里,因为CS是PB6,但它的复用功能是AF10,而PB2是AF9。这段代码里我把PB6和PB2分开配置了,实际写的时候要仔细核对。
常见坑:CubeMX有时候会把QSPI的GPIO配置生成得比较乱,特别是当你同时用了其他复用功能的时候。建议生成代码后手动检查一遍每个引脚的AF编号,别嫌麻烦。
5. W25Q128的Quad模式使能流程
5.1 上电后的默认状态
W25Q128上电后默认是标准SPI模式,IO0到IO3中只有IO0和IO1参与数据传输,IO2和IO3是高阻态。要进入Quad模式,需要先发命令设置状态寄存器2的QE位。
这个QE位是Non-Volatile的,也就是说设置一次之后,下次上电还是Quad模式。但如果你换了一颗新的Flash,或者Flash被擦除过,QE位可能是0。所以稳妥的做法是每次初始化的时候都检查并设置一次。
5.2 写使能加写状态寄存器
设置QE位的流程是:
- 发Write Enable命令(0x06)
- 发Write Status Register 2命令(0x31)
- 发一个字节,bit1设为1
- 等待写完成
用HAL库的代码大概是这样:
QSPI_CommandTypeDef cmd; // 写使能 cmd.Instruction = 0x06; cmd.InstructionMode = QSPI_INSTRUCTION_1_LINE; cmd.AddressMode = QSPI_ADDRESS_NONE; cmd.DataMode = QSPI_DATA_NONE; cmd.DummyCycles = 0; HAL_QSPI_Command(&hqspi, &cmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); // 写状态寄存器2 cmd.Instruction = 0x31; cmd.InstructionMode = QSPI_INSTRUCTION_1_LINE; cmd.AddressMode = QSPI_ADDRESS_NONE; cmd.DataMode = QSPI_DATA_1_LINE; cmd.NbData = 1; uint8_t reg2 = 0x02; // QE位置1 HAL_QSPI_Command(&hqspi, &cmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Transmit(&hqspi, ®2, HAL_QPSI_TIMEOUT_DEFAULT_VALUE);写完之后要等一会儿,让Flash内部完成写入。可以读状态寄存器1的bit0(BUSY位)来判断是否完成。
5.3 验证Quad模式是否生效
设置完QE位之后,可以发一个Quad读命令试试。如果读出来的数据正确,说明Quad模式已经生效。如果读出来全是0xFF或者0x00,那可能是QE位没设上,或者Dummy Cycles不对。
我一般会先读Flash的JEDEC ID(命令0x9F)来确认通信正常,然后再试Quad读。JEDEC ID是固定的,W25Q128应该是0xEF4018。如果这个都读不对,那说明基础SPI通信就有问题,得先查硬件。
实操心得:有些批次的W25Q128在出厂时QE位就是1,有些是0。不要假设,每次都检查。另外,如果你在代码里频繁切换单线和四线模式,记得每次切换后都要重新配置QSPI控制器,因为STM32H7的QSPI外设不会自动记住这些状态。
6. 内存映射模式配置与使用
6.1 进入内存映射模式
内存映射模式是STM32H7 QSPI最实用的功能之一。配置好之后,W25Q128的整个16MB空间会映射到0x90000000开始的地址。你可以直接定义指针来读数据:
#define QSPI_BASE_ADDR 0x90000000 uint8_t *flash_ptr = (uint8_t *)QSPI_BASE_ADDR; uint8_t data = flash_ptr[0x1000]; // 读偏移0x1000处的数据进入内存映射模式的代码:
QSPI_CommandTypeDef cmd; QSPI_MemoryMappedTypeDef mem_mapped; cmd.Instruction = 0xEB; // Quad读 cmd.InstructionMode = QSPI_INSTRUCTION_1_LINE; cmd.AddressMode = QSPI_ADDRESS_4_LINES; cmd.AddressSize = QSPI_ADDRESS_24_BITS; cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE; cmd.DummyCycles = 6; cmd.DataMode = QSPI_DATA_4_LINES; cmd.NbData = 0; cmd.DdrMode = QSPI_DDR_MODE_DISABLE; cmd.SIOOMode = QSPI_SIOO_INST_EVERY_CMD; mem_mapped.TimeOutActivation = QSPI_TIMEOUT_COUNTER_DISABLE; mem_mapped.TimeOutPeriod = 0; HAL_QSPI_MemoryMapped(&hqspi, &cmd, &mem_mapped);配置好之后,CPU访问0x90000000区域时,QSPI控制器会自动发起读操作,不需要软件干预。这对执行代码或者快速读大量数据来说非常方便。
6.2 内存映射模式下的性能优化
内存映射模式虽然方便,但默认配置下性能不一定最优。有几个地方可以调:
Cache配置:STM32H7有L1 Cache,如果QSPI映射区域配置成Cacheable,重复读同一块数据时可以直接从Cache取,速度极快。但要注意Cache一致性问题,如果Flash内容被外部修改了,Cache里的数据可能不是最新的。对于只读的Flash来说,这个问题不大。
预取使能:QSPI控制器支持预取,可以在CPU还没请求下一块数据的时候提前读。这个功能在顺序读场景下很有用,能显著提升有效带宽。
DMA配合:虽然内存映射模式下CPU可以直接读,但如果要搬运大量数据到其他内存区域,用DMA更高效。STM32H7的MDMA可以配置成从QSPI映射区域搬到SRAM,完全不占CPU。
我实测过,在400MHz的STM32H743上,从QSPI映射区域用DMA搬16KB数据到SRAM,耗时大概在30微秒左右,算下来有效带宽超过500MB/s。当然这是有Cache命中的情况,如果Cache没命中,带宽会降到50MB/s左右。
6.3 退出内存映射模式
内存映射模式不是永久的,你可以随时退出,回到间接模式。退出之后,0x90000000区域就不再映射到Flash了,访问会出错。
退出内存映射模式的方法是调用HAL_QSPI_Abort,或者重新配置QSPI控制器。我一般会在需要写Flash之前退出映射模式,写完再重新进入。
注意:内存映射模式下不能直接写Flash。W25Q128的写操作需要发命令、发地址、发数据,这些在映射模式下是做不到的。所以写操作必须走间接模式。
7. 读写性能实测与对比
7.1 测试方法
为了量化QSPI带来的提升,我设计了一个简单的测试:从Flash的0地址开始,连续读1MB数据到SRAM,分别用标准SPI模式和Quad模式跑,记录耗时。
测试平台是STM32H743IIT6,系统时钟400MHz,QSPI时钟80MHz。标准SPI模式下,SCK也是80MHz。测试代码用DWT计数器计时,精度到CPU周期。
7.2 实测数据
| 模式 | 数据量 | 耗时 | 有效带宽 |
|---|---|---|---|
| 标准SPI | 1MB | 210ms | 4.76MB/s |
| Quad SPI | 1MB | 62ms | 16.1MB/s |
| Quad SPI + DMA | 1MB | 58ms | 17.2MB/s |
| Quad SPI + 内存映射 | 1MB | 55ms | 18.2MB/s |
从数据看,Quad模式比标准SPI快了大约3.4倍。加上DMA和内存映射之后,提升到3.8倍左右。虽然没有到理论上的4倍,但考虑到命令开销和等待周期,这个结果已经很不错了。
写性能方面,因为W25Q128的页编程时间固定(典型值0.7ms,最大3ms),所以Quad模式对写性能的提升主要体现在数据传输阶段。对于大量小数据写入,提升不明显;对于连续大块写入,提升大概在2倍左右。
7.3 影响性能的关键因素
Dummy Cycles:这个参数对读性能影响很大。设多了浪费时钟周期,设少了读数据出错。80MHz下6个Dummy Cycles是手册推荐值,我实测下来6个和7个都能稳定工作,但6个的带宽高一点。
FIFO阈值:FIFO阈值设得太小会导致频繁中断,设得太大又可能溢出。我一般设成8,配合DMA使用。
总线优先级:STM32H7的总线矩阵里,QSPI的优先级可以配置。如果QSPI和其他高带宽外设(比如LTDC、DMA2D)同时工作,需要合理分配优先级,避免QSPI被饿死。
Cache策略:内存映射模式下,Cache命中率直接影响读性能。对于顺序读,预取能显著提高命中率;对于随机读,Cache效果有限。
实操心得:我在一个GUI项目里,把图片数据放在QSPI Flash里,用内存映射模式直接读。刚开始没配Cache,刷图很慢。后来把QSPI映射区域配成Write-Through模式,刷图速度直接翻倍。但要注意,Write-Through模式下写操作会直接写穿到Flash,而Flash写很慢,所以只适合只读区域。
8. 常见问题与排查技巧
8.1 读出来全是0xFF
这是最常见的问题。可能的原因有:
- QE位没设置成功,Flash还在单线模式
- Dummy Cycles设少了,采样点落在无效数据区
- GPIO复用功能配错了,IO2或IO3没配成QSPI功能
- Flash的CS没拉低,或者拉低时间不够
- 电源电压不够,Flash没正常工作
排查顺序:先读JEDEC ID,确认基础SPI通信正常;再读状态寄存器2,确认QE位是1;然后检查GPIO配置;最后用示波器看波形。
8.2 高速下数据偶尔出错
低速能跑,高速就出错,一般是信号完整性问题。可以尝试:
- 降低SCK频率,看是否稳定
- 在IO0到IO3上加小电容(10pF到22pF)到地,滤除高频噪声
- 调整Sample Shifting,改成Half Cycle
- 增加CS High Time
- 检查电源退耦电容是否足够
我在一个项目里遇到过100MHz下每读几MB就错一个字节的情况,后来把SCK降到80MHz就完全稳定了。所以不要盲目追求最高频率,稳定第一。
8.3 内存映射模式读数据不对
内存映射模式配置对了,但读出来的数据和预期不符。可能的原因:
- 进入映射模式之前没有正确配置Quad使能
- 映射的地址范围超过了Flash实际容量
- Cache一致性问题,读到了旧数据
- 映射模式配置里的Dummy Cycles和间接模式不一致
解决方法:先用间接模式读一块数据作为基准,再用映射模式读同一块数据,对比是否一致。如果不一致,逐步排查配置。
8.4 写操作失败
W25Q128的写操作有几个限制:
- 写入地址必须按页对齐(256字节一页)
- 写入数据不能跨页
- 写之前必须先擦除(擦除单位是扇区,4KB)
- 每次写之前都要发Write Enable命令
如果写失败,先检查这些条件是否满足。另外,写操作完成后要等待BUSY位清零,不能立即发下一条命令。
8.5 常见问题速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 读全0xFF | QE位未设 | 检查状态寄存器2 |
| 读全0x00 | CS未拉低 | 检查CS引脚配置 |
| 高速出错 | 信号完整性 | 降频、加电容、调采样 |
| 映射读错 | Dummy Cycles不对 | 与间接模式对比 |
| 写失败 | 未擦除或未写使能 | 检查写流程 |
| 偶尔死机 | 总线冲突 | 调整QSPI优先级 |
9. 进阶技巧与项目经验
9.1 双闪存模式
STM32H7的QSPI支持双闪存模式,可以同时接两颗W25Q128,容量翻倍到32MB,带宽也能翻倍。配置上需要把两颗Flash的CS分别接到QSPI的CS和另一个GPIO上,然后配置QSPI控制器为双闪存模式。
双闪存模式下,两颗Flash的地址空间是交替映射的。比如0x90000000对应Flash1的0地址,0x90000001对应Flash2的0地址,以此类推。读的时候两颗Flash并行工作,带宽翻倍。
这个模式适合需要大容量高带宽的场景,比如视频缓存、大型字库。但硬件设计上要注意两颗Flash的走线等长,否则并行读写时可能出错。
9.2 配合DMA和DMAMUX
STM32H7的DMAMUX可以把QSPI的DMA请求路由到任意DMA通道。配合双缓冲技术,可以实现QSPI读和CPU处理并行。比如你在做音频播放,DMA从QSPI读下一段音频数据到缓冲区A,同时CPU处理缓冲区B的数据,两个缓冲区交替使用,实现无缝播放。
配置DMAMUX的时候要注意,QSPI的DMA请求线是固定的,需要通过DMAMUX的请求发生器来路由。具体配置在参考手册的DMAMUX章节有详细说明。
9.3 在QSPI Flash上跑代码
STM32H7支持从外部QSPI Flash执行代码,这叫XIP(Execute In Place)。配置好内存映射模式后,把代码链接到0x90000000区域,CPU就可以直接从Flash取指令执行。
但要注意,QSPI Flash的读取速度比内部Flash慢,而且有Cache才能跑得流畅。对于性能敏感的代码,还是放到内部RAM或者ITCM里跑比较好。XIP适合放一些不常执行的代码,比如配置函数、字符串常量。
我在一个项目里把UI资源文件放在QSPI Flash,代码放在内部Flash,通过内存映射读资源,效果很好。既节省了内部Flash空间,又保证了代码执行速度。
9.4 低功耗考虑
W25Q128支持深度掉电模式,电流可以降到1微安以下。在电池供电的设备里,不访问Flash的时候可以发命令让它进入掉电模式,需要的时候再唤醒。
但要注意,掉电模式下Flash不响应任何命令,唤醒需要一定时间。而且STM32H7的QSPI控制器在掉电模式下也要相应配置,避免产生不必要的总线活动。
实操心得:我在一个低功耗数据记录仪项目里,用QSPI Flash存数据,平时Flash处于掉电模式,MCU定时唤醒写一批数据然后继续睡。整体功耗控制在50微安以下,用一节纽扣电池能跑好几个月。关键是要确保写数据之前Flash已经完全唤醒,否则会写失败。
10. 完整初始化代码参考
把前面讲的串起来,一个完整的QSPI初始化流程大概是这样的:
// 1. 配置GPIO MX_GPIO_Init(); // 2. 配置QSPI外设 hqspi.Instance = QUADSPI; hqspi.Init.ClockPrescaler = 2; // 200MHz/2 = 100MHz hqspi.Init.FifoThreshold = 8; hqspi.Init.SampleShifting = QSPI_SAMPLE_SHIFTING_HALFCYCLE; hqspi.Init.FlashSize = 23; // 2^23 = 8MB? 不对,W25Q128是16MB // 更正:FlashSize = 24, 2^24 = 16MB hqspi.Init.ChipSelectHighTime = QSPI_CS_HIGH_TIME_8_CYCLE; hqspi.Init.ClockMode = QSPI_CLOCK_MODE_0; hqspi.Init.FlashID = QSPI_FLASH_ID_1; hqspi.Init.DualFlash = QSPI_DUALFLASH_DISABLE; HAL_QSPI_Init(&hqspi); // 3. 复位Flash QSPI_CommandTypeDef cmd; cmd.Instruction = 0x66; // Enable Reset cmd.InstructionMode = QSPI_INSTRUCTION_1_LINE; cmd.AddressMode = QSPI_ADDRESS_NONE; cmd.DataMode = QSPI_DATA_NONE; cmd.DummyCycles = 0; HAL_QSPI_Command(&hqspi, &cmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); cmd.Instruction = 0x99; // Reset HAL_QSPI_Command(&hqspi, &cmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); HAL_Delay(1); // 4. 设置QE位 cmd.Instruction = 0x06; // Write Enable HAL_QSPI_Command(&hqspi, &cmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); cmd.Instruction = 0x31; // Write Status Register 2 cmd.DataMode = QSPI_DATA_1_LINE; cmd.NbData = 1; uint8_t qe = 0x02; HAL_QSPI_Command(&hqspi, &cmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Transmit(&hqspi, &qe, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); // 5. 等待写完成 do { cmd.Instruction = 0x05; // Read Status Register 1 cmd.DataMode = QSPI_DATA_1_LINE; cmd.NbData = 1; uint8_t status; HAL_QSPI_Command(&hqspi, &cmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Receive(&hqspi, &status, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); if ((status & 0x01) == 0) break; } while(1); // 6. 进入内存映射模式 QSPI_MemoryMappedTypeDef mem_mapped; cmd.Instruction = 0xEB; cmd.AddressMode = QSPI_ADDRESS_4_LINES; cmd.AddressSize = QSPI_ADDRESS_24_BITS; cmd.DataMode = QSPI_DATA_4_LINES; cmd.DummyCycles = 6; cmd.NbData = 0; mem_mapped.TimeOutActivation = QSPI_TIMEOUT_COUNTER_DISABLE; HAL_QSPI_MemoryMapped(&hqspi, &cmd, &mem_mapped);这段代码里有个地方要注意:FlashSize的计算。W25Q128是128Mbit,等于16MB,2的24次方是16MB,所以FlashSize应该设为24。我上面写23是错的,后来更正了。这个参数影响内存映射的地址范围,设错了会导致访问越界。
另外,复位Flash的步骤不是必须的,但建议加上。有些Flash在上电后可能处于不确定状态,复位一下更稳妥。
11. 我踩过的几个坑
第一个坑是GPIO复用功能配错。前面提过,IO2和IO3的AF编号和IO0、IO1不一样。我当时没仔细看手册,全配成AF10,结果Quad模式下读数据高四位全是0。查了两天才发现是这个问题。
第二个坑是Dummy Cycles设少了。80MHz下我设了4个,低速能读,高速就出错。后来查手册发现80MHz需要6个,改成6之后稳定了。这个参数不能凭感觉,一定要查手册。
第三个坑是内存映射模式下Cache一致性问题。我在一个项目里通过QSPI写入了新数据,然后立即用映射模式读,读出来还是旧数据。后来发现是Cache没刷新。解决方法是在写操作之后调用SCB_InvalidateDCache_by_Addr刷新对应的Cache行。
第四个坑是电源退耦不足。Flash在高速读写时电流波动比较大,如果退耦电容不够,电源上会有噪声,导致读写错误。我在Flash的VCC引脚旁边加了0.1uF和1uF的电容之后,问题就解决了。
第五个坑是CS High Time设得太小。W25Q128要求CS拉高后至少保持50ns,我一开始设了1个周期,100MHz下才10ns,不够。改成8个周期之后就没再出过问题。
这些坑说起来都是细节,但每一个都可能导致项目卡壳好几天。希望看到这篇文章的人能少走点弯路。
12. 性能还能再压榨吗
如果你已经把QSPI跑到100MHz,DMA和内存映射都配好了,还想再快,可以考虑几个方向:
提高SCK频率:W25Q128的极限是104MHz,但实际能不能跑到取决于你的PCB和电源。我试过跑到120MHz,大部分芯片能工作,但个别批次会出错。如果产品要过认证,建议留足余量。
用双闪存模式:两颗Flash并行,带宽翻倍。但硬件成本增加,PCB面积也增加。
优化Cache策略:把QSPI映射区域配成Cacheable,并且合理设置预取,能显著提高顺序读性能。
减少命令开销:在内存映射模式下,每次读都要发命令和地址。如果读的是连续地址,QSPI控制器会自动合并传输,减少开销。但如果读的地址跳跃很大,开销就上去了。
用DDR模式:STM32H7的QSPI支持DDR模式,在时钟的上升沿和下降沿都采样数据,理论带宽再翻倍。但W25Q128不支持DDR,所以这个用不上。如果你用的是支持DDR的Flash,可以试试。
实际项目里,我一般把SCK定在80MHz到100MHz之间,配合DMA和内存映射,性能已经足够应付大多数场景了。再往上压榨,收益递减,风险递增,不太划算。
13. 几个实用的小技巧
快速验证Quad模式是否生效:读JEDEC ID用单线模式,读Device ID用Quad模式,对比结果。如果Quad模式下读出来不对,说明QE位没设上。
用示波器看波形:如果条件允许,用示波器看SCK和IO0的波形。正常工作时,IO0上应该有明显的四线传输特征。如果IO0一直是高电平或者低电平,说明Quad模式没生效。
写操作前先擦除:W25Q128只能把1写成0,不能把0写成1。所以写之前必须先擦除,擦除后所有位都是1。这个和EEPROM不一样,别搞混了。
注意扇区边界:擦除的最小单位是4KB扇区,写入不能跨页(256字节)。如果你要写的数据跨越了页边界,需要分多次写。
保留一份黄金配置:调通之后,把CubeMX的配置文件或者初始化代码保存一份。下次做新项目直接复制,省得重新踩坑。
定期读状态寄存器:在长时间连续读写的时候,定期读一下状态寄存器,确认BUSY位和WEL位状态正常。如果发现异常,及时处理。
温度影响:W25Q128的读写速度受温度影响。高温下可能需要降低SCK频率才能稳定工作。如果你的产品要在宽温范围内工作,建议留足余量。
批次差异:不同批次的W25Q128在性能上可能有差异。我遇到过同一型号不同批次的芯片,一批能跑100MHz,另一批只能跑80MHz。所以量产的时候要留余量,不能按实验室的最优值来设计。
这些技巧都是实际项目中积累的,书本上不一定有。希望能帮到正在折腾QSPI的你。