ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32H750 QSPI轮询读写GD25Q64E:从CubeMX到代码全解析

STM32H750 QSPI轮询读写GD25Q64E:从CubeMX到代码全解析 先说清楚我为什么要写这个项目。最近在STM32H750VBT6上做一个小型HMI片内Flash只有128KB放了UI框架和字库之后空间非常紧张于是必须外挂一颗8MB的NOR Flash。选来选去用了GD25Q64E64Mbit8MB3.3V供电四线QSPI接口价格上比W25Q64便宜不少而且GD25系列和Winbond的命令集基本兼容很多现有代码都能直接用。网上SPI方式读写GD25Q64E的例子很多但STM32H750x7系列把QSPI外设跑起来、用最朴素的轮询方式完整读写GD25Q64E的资料反而零零散散很多人卡在读ID不对、擦除超时、写进去再读全是0xFF这几个经典环节。这篇文章我用自己的实际代码和调试记录讲透这件事。整个思路是先用CubeMX把QUADSPI外设配置出来然后基于HAL库的QSPI接口用轮询方式实现初始化、读JEDEC ID、扇区擦除、页编程、读取数据最后把常见的坑列一遍。适合刚接触STM32H750 QSPI、准备用GD25Q64E这类8MB NOR Flash的小伙伴参考。1. 整体设计思路为什么是QSPI为什么是轮询1.1 这颗Flash为什么要用QSPI而不是普通SPI普通SPI只能使用一根数据输入线、一根数据输出线跑满也就是1-bit串行传输。如果只是存几十KB的配置参数普通SPI完全够用甚至用I2C都行。但我要在8MB的Flash里存GUI字库和图片资源后续还想用内存映射方式让CPU直接执行外部Flash里的代码这时候就必须上QSPI。STM32H750的QUADSPI外设支持1线、2线和4线数据总线和GD25Q64E的四根IO引脚一一对应。读数据时可以让4根线同时工作同样时钟频率下吞吐量直接是普通SPI的4倍。GD25Q64E本身也支持Quad Output Fast Read这类命令地址和命令仍然走单线但数据阶段用IO0~IO3并行输出效果立竿见影。这颗Flash的容量是8MB地址空间是0x000000~0x7FFFFF也就是0x800000字节。QSPI的FlashSize字段按2的次数填8MB就是2^23所以在CubeMX里填23代码里也要把hqspi.Init.FlashSize 23写对。如果这个值填错地址会错位读出来的数据全是乱的。1.2 轮询方式定位不是性能最优但一定是最稳的起步方式STM32H750的QSPI支持三种操作方式轮询、中断、DMA。标题里既然点名“轮询方式”我们就先把这种方式讲透。轮询方式的本质是CPU一直主动查看QSPI外设的状态寄存器判断当前传输是否完成。HAL库的HAL_QSPI_Command、HAL_QSPI_Transmit、HAL_QSPI_Receive都带一个timeout参数传进去之后内部会不断循环检查状态直到传输完成或者超时返回错误。这种方式听上去不高级但对项目起步阶段极其友好。你不需要处理中断服务函数里的状态机也不需要关心DMA的描述符和回调代码是一条直线走下来的遇到问题好定位。等整个读写流程验证通过了再根据实际吞吐需求切换成中断或DMA也不迟。所以我的建议是第一版驱动先老老实实用轮询把Flash命令时序、WIP等待、页边界这些问题全部跑通。轮询方式的性能损失在大部分场景下是可以接受的尤其是配置参数读写、OTA升级包存储这种低频操作512字节一页的数据量轮询和DMA的差别根本感觉不出来。1.3 硬件连接和准备工作QSPI的硬件连接要看板子原理图不同开发板引脚不一样。我自己手头这块板子上QSPI Flash接的是功能引脚QSPI_CLKPF10QSPI_BK1_NCSPG6QSPI_BK1_IO0PF8QSPI_BK1_IO1PF9QSPI_BK1_IO2PF7QSPI_BK1_IO3PF6这个引脚组合是STM32H750-DK等常见板子的接法但你自己画板子一定要以原理图为准然后到CubeMX的QUADSPI配置界面里重新分配。四根IO线不要接反IO0不一定是物理上的第一个数据位接错之后读ID会读到0xC8参数乱掉甚至读回全0xFF。另外注意GD25Q64E是3.3V器件STM32H750的VDD如果是3.3V就不用加电平转换。如果是双电压域设计QSPI引脚供电域必须和Flash电平匹配这点在低功耗项目里更容易踩。2. 必须先搞懂的GD25Q64E命令和时序细节2.1 GD25Q64E基础参数速记GD25Q64E是一颗8MB串行NOR Flash页大小256字节扇区大小4KB块大小64KB。每次写操作前要先把目标区域擦除成0xFF写操作只能把1变成0所以“先擦后写”是铁律。几个关键参数参数数值说明容量64Mbit / 8MB地址0x000000~0x7FFFFF页编程大小256字节超过256字节要拆页扇区擦除大小4KB最小可擦除单位是扇区WIP位状态寄存器bit0置1表示Flash内部正在忙JEDEC ID0xC84017制造商0xC8类型0x40容量0x17状态寄存器里的WIP位是调试时最重要的一个位。擦除、编程、写状态寄存器这些操作都不是瞬间完成的Flash内部有一个忙于执行内部操作的时间窗口在这段时间里你只能读状态寄存器不能发其他敏感操作。轮询WIP位就是等待Flash内部的这个窗口结束。2.2 QSPI和“四个脚全上”的关系很多刚接触QSPI的人有个误区认为QSPI就是四根线同时传输所有东西。实际上不是。GD25Q64E支持两种工作模式SPI模式命令、地址、数据都可能只走1根线或者命令地址走1根线、数据阶段走4根线这取决于具体命令。QPI模式连命令本身都通过4根线传输需要发送0x38进入QPI模式0xFF退出。标题里说的“QSPI读写”通常是指在SPI模式下使用Quad Input/Quad Output命令让数据阶段走4根线而不是强制进入QPI模式。所以我下面的代码不会发0x38进入QPI而是直接用命令0x6B做Quad Output Fast Read用命令0x32或0x02做页编程。这样最稳兼容性也最好。在STM32H750的HAL库里每条QSPI命令用QSPI_CommandTypeDef描述里面有几个关键字段InstructionMode命令阶段走几根线一般是1线。AddressMode地址阶段走几根线可以是1线或4线。DataMode数据阶段走几根线这是我们最常改的。DummyCyclesDummy周期数某些读命令需要。NbData本次数据阶段期望传输的字节数。把这几个字段理解透后面代码就是填表。2.3 本次用到的命令速查表命令操作码说明DummyCycles数据线数Write Enable0x06写使能写/擦除前必须发0无Read Status Register-10x05读状态寄存器1检查WIP01线读1字节Read JEDEC ID0x9F读3字节ID01线读3字节Sector Erase0x204KB扇区擦除0无Page Program0x02单线页编程最多256字节01线写数据Quad Input Page Program0x32四线页编程最多256字节04线写数据Quad Output Fast Read0x6B四线快速读数据81线地址4线数据读写数据时我推荐使用0x6B和0x32这样项目才真正用上了“QSPI”的四线能力。如果你只想先用单线把流程跑通把指令改成0x03读、0x02写就行代码结构完全不用动。2.4 轮询过程中最容易被忽略的WIP状态轮询HAL库自带的HAL_QSPI_Transmit和HAL_QSPI_Receive只负责QSPI总线层面的数据传输它们从来不关心Flash内部是否完成了擦除或编程。举个例子你通过QSPI总线把256字节数据发给了FlashHAL_QSPI_Transmit立即返回HAL_OK。但这时候Flash内部可能还在把数据往存储单元里写如果你的程序紧接着就去读刚写入的地址大概率读回来的是旧数据或者0xFF然后你会误以为写失败了。这就是为什么必须自己写一个“轮询WIP位”的函数。擦除或者页编程之后通过命令0x05读状态寄存器1检查bit0如果为1就继续等直到变成0。这个等待过程本质上也是“轮询方式”的一部分并且这才是GD25Q64E真正意义上的忙状态等待和QSPI外设的传输完成标志是两回事。3. 从CubeMX配置到完整代码轮询方式读写GD25Q64E3.1 CubeMX里的QSPI参数配置打开STM32CubeMX在Connectivity里找到QUADSPI把Mode选成Single Bank。如果板子上只有一颗QSPI Flash用单bank模式就够了没必要开双bank。关键参数我建议这样设置参数值说明Clock Prescaler2~4先保守一点让QSPI时钟在几十MHzClock ModeMode 0CPOL0CPHA0和GD25Q64E默认匹配Sample ShiftingHalf Clock高频下提高采样可靠性Flash Size238MB 2^23Fifo Threshold4按默认即可如果你不确定QSPI时钟源是多少先不要追求极限速度。GD25Q64E在3.3V下最高能跑120MHz左右但这个“最高”往往需要更严格的PCB布线、更短的走线和合理的上下拉。调通的第一个版本建议把分频调高让时钟控制在50MHz以内能省掉后面一大半莫名其妙的问题。CubeMX生成工程的时候记得把QUADSPI对应的GPIO初始化代码一起勾选。之后生成的HAL_QSPI_MspInit里会自动把引脚复用成QSPI功能。3.2 初始化自检把读ID放在所有操作之前新板子拿回来我强烈建议第一步只做一件事读JEDEC ID。如果ID能读出0xC84017说明QSPI时钟、命令模式、引脚连接、供电全部正常后面写擦读才有意义。uint8_t GD25Q64E_ReadID(uint8_t *id) { QSPI_CommandTypeDef cmd {0}; cmd.InstructionMode HAL_QSPI_INSTRUCTION_1_LINE; cmd.Instruction 0x9F; cmd.AddressMode HAL_QSPI_ADDRESS_NONE; cmd.AlternateByteMode HAL_QSPI_ALTERNATE_BYTE_NONE; cmd.DataMode HAL_QSPI_DATA_1_LINE; cmd.DummyCycles 0; cmd.NbData 3; cmd.SIOOMode HAL_QSPI_SIOO_INST_EVERY_CMD; if (HAL_QSPI_Command(hqspi, cmd, HAL_QSPI_TIMEOUT_DEFAULT) ! HAL_OK) { return 1; } if (HAL_QSPI_Receive(hqspi, id, HAL_QSPI_TIMEOUT_DEFAULT) ! HAL_OK) { return 2; } return 0; }调用之后检查id[0] 0xC8、id[1] 0x40、id[2] 0x17。如果读出来的东西不对先别急着查代码大概率是引脚复用、时钟配置或Flash供电这三个方向的问题。3.3 写使能必须在写和擦除之前GD25Q64E的擦除、页编程、写状态寄存器操作都需要先发送Write Enable命令也就是0x06。这个步骤极其容易漏一旦漏了后续命令会被Flash忽略擦除超时、写数据无效都是这么来的。static void GD25Q64E_WriteEnable(void) { QSPI_CommandTypeDef cmd {0}; cmd.InstructionMode HAL_QSPI_INSTRUCTION_1_LINE; cmd.Instruction 0x06; cmd.AddressMode HAL_QSPI_ADDRESS_NONE; cmd.AlternateByteMode HAL_QSPI_ALTERNATE_BYTE_NONE; cmd.DataMode HAL_QSPI_DATA_NONE; cmd.DummyCycles 0; cmd.SIOOMode HAL_QSPI_SIOO_INST_EVERY_CMD; HAL_QSPI_Command(hqspi, cmd, HAL_QSPI_TIMEOUT_DEFAULT); }再配合一个轮询WIP的函数static uint8_t GD25Q64E_ReadStatusReg1(void) { QSPI_CommandTypeDef cmd {0}; uint8_t status 0; cmd.InstructionMode HAL_QSPI_INSTRUCTION_1_LINE; cmd.Instruction 0x05; cmd.AddressMode HAL_QSPI_ADDRESS_NONE; cmd.AlternateByteMode HAL_QSPI_ALTERNATE_BYTE_NONE; cmd.DataMode HAL_QSPI_DATA_1_LINE; cmd.DummyCycles 0; cmd.NbData 1; cmd.SIOOMode HAL_QSPI_SIOO_INST_EVERY_CMD; HAL_QSPI_Command(hqspi, cmd, HAL_QSPI_TIMEOUT_DEFAULT); HAL_QSPI_Receive(hqspi, status, HAL_QSPI_TIMEOUT_DEFAULT); return status; } static void GD25Q64E_WaitBusy(uint32_t timeout_ms) { uint32_t start HAL_GetTick(); while (GD25Q64E_ReadStatusReg1() 0x01) { if ((HAL_GetTick() - start) timeout_ms) { Error_Handler(); } } }这里0x01就是WIP位。擦除一个扇区几百毫秒都很正常页编程则很快通常几毫秒之内。3.4 扇区擦除GD25Q64E的最小擦除单位是4KB扇区。有些Flash还支持32KB块擦除、64KB块擦除和整片擦除但日常使用我建议全部用扇区擦除灵活、不容易误伤数据。void GD25Q64E_SectorErase(uint32_t address) { QSPI_CommandTypeDef cmd {0}; GD25Q64E_WriteEnable(); cmd.InstructionMode HAL_QSPI_INSTRUCTION_1_LINE; cmd.Instruction 0x20; cmd.AddressMode HAL_QSPI_ADDRESS_1_LINE; cmd.AddressSize HAL_QSPI_ADDRESS_24_BITS; cmd.Address address; cmd.AlternateByteMode HAL_QSPI_ALTERNATE_BYTE_NONE; cmd.DataMode HAL_QSPI_DATA_NONE; cmd.DummyCycles 0; cmd.SIOOMode HAL_QSPI_SIOO_INST_EVERY_CMD; HAL_QSPI_Command(hqspi, cmd, HAL_QSPI_TIMEOUT_DEFAULT); GD25Q64E_WaitBusy(1000); }调用时要保证address是4KB对齐的也就是低12位为0。如果传了一个不是扇区边界的地址不同Flash行为不一样有些会直接忽略有些会擦除所在扇区调试时容易把人搞糊涂。我们代码里最好加一个if ((address 0xFFF) ! 0)的检查。3.5 页编程写入数据页编程一次最多写256字节而且原则上是不能跨页的。也就是说如果你要从地址0x100开始写200字节目标地址加长度不能越过0x1FF这个页边界。最省心的做法是让调用方保证这一点。void GD25Q64E_PageProgram(uint32_t address, const uint8_t *buf, uint16_t len) { QSPI_CommandTypeDef cmd {0}; if (len 0 || len 256) { Error_Handler(); } if ((address 0xFF) len 256) { Error_Handler(); } GD25Q64E_WriteEnable(); cmd.InstructionMode HAL_QSPI_INSTRUCTION_1_LINE; cmd.Instruction 0x32; /* Quad Input Page Program */ cmd.AddressMode HAL_QSPI_ADDRESS_1_LINE; cmd.AddressSize HAL_QSPI_ADDRESS_24_BITS; cmd.Address address; cmd.AlternateByteMode HAL_QSPI_ALTERNATE_BYTE_NONE; cmd.DataMode HAL_QSPI_DATA_4_LINES; /* 数据阶段走4根线 */ cmd.DummyCycles 0; cmd.NbData len; cmd.SIOOMode HAL_QSPI_SIOO_INST_EVERY_CMD; HAL_QSPI_Command(hqspi, cmd, HAL_QSPI_TIMEOUT_DEFAULT); HAL_QSPI_Transmit(hqspi, (uint8_t *)buf, HAL_QSPI_TIMEOUT_DEFAULT); GD25Q64E_WaitBusy(1000); }我一直用0x32这个Quad Input命令因为这才是名副其实的QSPI写入。如果你担心兼容性或者还没验证四线写模式可以先临时把cmd.Instruction改成0x02把cmd.DataMode改成HAL_QSPI_DATA_1_LINE其他代码不变。3.6 读取数据读数据没有边界限制只要地址不超出8MB想读多少就读多少。我推荐使用0x6B Quad Output Fast Read数据阶段走四线DummyCycles要填8。void GD25Q64E_Read(uint32_t address, uint8_t *buf, uint32_t len) { QSPI_CommandTypeDef cmd {0}; cmd.InstructionMode HAL_QSPI_INSTRUCTION_1_LINE; cmd.Instruction 0x6B; /* Quad Output Fast Read */ cmd.AddressMode HAL_QSPI_ADDRESS_1_LINE; cmd.AddressSize HAL_QSPI_ADDRESS_24_BITS; cmd.Address address; cmd.AlternateByteMode HAL_QSPI_ALTERNATE_BYTE_NONE; cmd.DataMode HAL_QSPI_DATA_4_LINES; /* 数据阶段走4根线 */ cmd.DummyCycles 8; cmd.NbData len; cmd.SIOOMode HAL_QSPI_SIOO_INST_EVERY_CMD; HAL_QSPI_Command(hqspi, cmd, HAL_QSPI_TIMEOUT_DEFAULT); HAL_QSPI_Receive(hqspi, buf, HAL_QSPI_TIMEOUT_DEFAULT); }如果只是验证数据也可以把0x6B改成0x03普通读DummyCycles改成0数据模式改成1线这样能排除四线模式导致的硬件连接问题。3.7 一次完整的写读回环测试把上面的函数串起来做一个512字节的写读回环测试。因为一次只能页编程256字节所以要分两次写并且先把目标扇区擦掉。#define TEST_ADDR 0x1000 void GD25Q64E_Test(void) { uint8_t write_buf[512]; uint8_t read_buf[512]; uint8_t id[3]; uint32_t i; if (GD25Q64E_ReadID(id) ! 0) { Error_Handler(); } for (i 0; i 512; i) { write_buf[i] (uint8_t)(i 0x10); } /* 擦除TEST_ADDR所在扇区 */ GD25Q64E_SectorErase(TEST_ADDR); /* 分两次页编程 */ GD25Q64E_PageProgram(TEST_ADDR, write_buf, 256); GD25Q64E_PageProgram(TEST_ADDR 256, write_buf 256, 256); /* 回读 */ GD25Q64E_Read(TEST_ADDR, read_buf, 512); /* 校验 */ for (i 0; i 512; i) { if (write_buf[i] ! read_buf[i]) { Error_Handler(); } } }这个测试跑通之后你的轮询方式读写GD25Q64E基础驱动就齐了。后面要接入文件系统或者OTA升级都是在这个基础上封装更上层的接口底层命令不用再动。4. 调试过程实录常见问题和排查方法4.1 问题排查速查表我在这颗Flash上调过的板子不算少把最常见的现象、原因和解决办法整理成了一张表大家可以直接对着查。现象可能原因解决办法读ID返回全0xFFFlash供电异常、引脚复用错、NCS没拉低、QSPI时钟没起来先用万用表量VDD和VSS核对CubeMX引脚分配读ID返回全0x00QSPI时钟极性不对、上拉电阻有问题、Flash焊接异常检查Clock Mode是否为Mode 0检查IO上拉读ID能读到0xC8但后两位不对Flash型号不是GD25Q64E或FlashSize字段填错确认器件型号和容量核对FlashSize23写数据后读回来全是0xFF擦除不完整、写使能没发、页编程长度超过256字节、跨页了先调读ID再查擦除后的数据是否全0xFF最后拆分页编程擦除超时写使能没发、QSPI时钟过快、Flash内部忙死擦除前必须发0x06降低QSPI时钟频率检查WIP轮询四线读正常但四线写不行0x32命令不受支持、数据模式配错、硬件IO3断线先改用0x02单线写验证再排查IO3地址超过0x7FFFFF读出异常8MB地址越界检查地址范围8MB最大地址就是0x7FFFFF读回全0xFF这个现象很多新手第一反应是“写失败”其实更常见的是“根本没擦除”或者“写命令没真正执行”。NOR Flash擦除后的状态是1也就是每个字节都是0xFF所以看到0xFF不一定是错误可能是这个扇区本来就是干净的。4.2 页编程跨页问题必须前置处理我调试时第一次写入就踩了跨页的坑。当时想从地址0x110开始连续写入300字节页大小是256字节这个操作实际上跨了两个页。GD25Q64E的页编程命令虽然最多可以写256字节但并不保证自动跨页处理很多同型号Flash一旦检测到跨页要么忽略后面的数据要么从页头开始覆盖行为不可控。所以在上层封装里一定要自己做跨页拆分。最简单的办法是判断本次写入的起始地址低8位加上长度是否超过256uint32_t page_remain 256 - (address 0xFF); uint32_t write_len len; if (write_len page_remain) { write_len page_remain; } GD25Q64E_PageProgram(address, buf, write_len); address write_len; buf write_len; len - write_len;这样循环下去直到所有数据写完。这个逻辑在任何NOR Flash驱动里都是必需品不要指望命令本身帮你处理。4.3 轮询等待的timeout要留足HAL库的timeout单位是毫秒但注意不要把timeout设置成“必须在这个时间内完成”的意思它只是死等时的保护上限。GD25Q64E的扇区擦除时间典型值可能在几十毫秒到几百毫秒之间我见过有些环境温度低、电压偏低的时候擦除时间明显变长。所以等待WIP的timeout我建议给到1000ms以上页编程给到100ms左右而不是用5ms这种理论值去卡。反过来轮询WIP的循环里也不要傻等尤其是跑RTOS的时候最好在等待过程中加一点延时或者让出CPU。纯裸机环境下如果对实时性要求不高直接在GD25Q64E_WaitBusy里加一个HAL_Delay(1)也能减少对QSPI总线的无效占用。4.4 关于内存映射方式的一点点扩展QSPI除了轮询方式读写还有Memory-Mapped内存映射模式。这种模式下QSPI Flash会被映射到STM32H750的地址空间比如Bank1的起始地址0x90000000CPU可以直接用指针访问外部Flash不需要手动发命令。最典型的使用场景就是XIP直接从外部Flash执行代码。但内存映射模式下Flash内部依然有忙状态的概念擦除和编程还是要回到命令模式不能一边映射一边写同一个bank。所以我建议先把轮询方式彻底跑通再考虑内存映射。否则两个概念混在一起出了问题你都不知道是总线时序不对还是Flash状态没处理好。4.5 时钟频率别一上来就跑极限GD25Q64E标称支持120MHz但这是理想情况。实际板子上QSPI信号走线、地平面、Flash引脚上拉、STM32H750的I/O驱动能力都会影响最高稳定频率。我在测试中遇到过QSPI时钟调到100MHz后四线读偶尔会出现数据错位降到80MHz以下就完全稳定。遇到这种问题先不要怀疑Flash芯片最有效的排查方法是把Clock Prescaler调大让时钟频率降一半再测试。如果降频后一切正常说明不是配置错误而是信号完整性不够需要从PCB层面积优化而不是继续在代码里硬调SampleShifting去碰运气。最后说一句我自己的体会QSPI调试最怕的就是在硬件没确认的情况下疯狂刷代码。只要你老老实实先读IDID能读对后面所有命令基本都能顺下来读ID读不对后面写再多代码都是白搭。把GD25Q64E的0xC84017印在脑子里这块8MB Flash对你来说就没有秘密了。
返回列表