STM32 QSPI实战:GD25Q80E命令时序与Flash读写排错

发布时间:2026/9/12 15:03:41
STM32 QSPI实战:GD25Q80E命令时序与Flash读写排错 那阵子帮朋友调一块板子现象很奇怪STM32上电后从挂在QSPI总线上的GD25Q80E里读启动参数10次里有3次读出来是全0xFF剩下的7次正常。用示波器抓CLK和IO波形看着也没问题不少人第一反应就是“换颗芯片”可换完还是一样。后来发现是命令序列里少了状态寄存器轮询那一步擦除刚发完就立刻去读数据Flash内部还没忙完。这种问题在SPI NOR Flash上非常典型尤其是刚接触GD25Q80E这类芯片的时候把命令时序理解透比会调库函数重要得多。这篇博文就围绕一颗很常见的8Mbit SPI NOR Flash——GD25Q80E从芯片内部结构、命令时序一直讲到STM32上的QSPI实战。无论你是在做物联网设备的参数存储、BootLoader的固件备份区、还是屏幕字库外置这套东西都通用。全文没有太多玄学全部基于我实际跑过的板子和踩过的坑。1. 先把GD25Q80E“身家背景”摸清楚1.1 为什么是1MB而不是4MB读ID验货总是第一件事GD25Q80E这个命名本身就有信息量。“GD”是兆易创新的前缀“25”是SPI NOR Flash产品线“Q80”表示容量8Mbit后面的“E”是工艺版本。8Mbit换算过来就是1MB这在MCU项目里是个很微妙的容量——说大不大但装一个BootLoader加两份应用镜像、再留一个参数区刚好够用说小也不算小比多数MCU内部Flash大得多。拿到一颗Flash无论是新的还是拆机件第一件事永远都是读JEDEC ID。命令是0x9F发完命令码之后芯片会回三个字节Manufacturer ID、Memory Type、Capacity。GD25Q80E读出来一般是C8 40 14其中C8是兆易创新的厂商ID40是SPI NOR Flash的类型14是容量代码8Mbit对应0x1420。如果读出来是EF 40 14那是Winbond的W25Q80如果读到全FF或者全00先别怀疑芯片坏了大概率是MOSI/MISO接反了或者CS极性不对。读ID的代码很短传统软件SPI方式也就十几行uint8_t spi_flash_read_id(void) { uint8_t cmd 0x9F; uint8_t id[3] {0}; uint8_t dummy 0xFF; cs_low(); spi_write_read_byte(cmd); id[0] spi_write_read_byte(dummy); id[1] spi_write_read_byte(dummy); id[2] spi_write_read_byte(dummy); cs_high(); return id[0]; // 实际工程中建议把id[0..2]都保存下来做完整校验 }这里有个小细节读ID时除了命令码后续每个字节都要由主机继续提供时钟芯片只是在MISO上按位返回数据。所以主机要发连续的假字节0xFF来产生SCLK。很多人第一次写驱动发完0x9F就把CS拉高了自然读不到任何东西。1.2 页、扇区、块这些名词是布局不是概念SPI NOR Flash内部结构不是一块平整的内存而是分层的页Page、扇区Sector、块Block最后是整片Chip。GD25Q80E的1MB空间被划分成4096个页每页256字节64个扇区每个扇区16页也就是4KB16个块每个块包含4个扇区也就是64KB。擦除操作的最小单位是扇区块擦除一次能清64KB整片擦除则一口气全清。为什么要理解这个分层因为NOR Flash的物理特性决定了它只能“1变0”不能“0变1”。要把某一位从0变回1唯一的手段就是擦除而擦除的最小单位是扇区。这意味着你只想改一个字节也必须先把整个4KB扇区读出来、在内存里改掉、然后整扇区擦掉、再写回去。这个道理很多人知道但实际设计存储结构时还是会忘后面讲数据布局的时候我专门展开。1.3 从Flash角色看应用场景参数存储、固件备份、字库三个典型用途结合我在实际项目里的用法SPI NOR Flash通常承担三种角色第一种是参数存储区。设备校准值、WiFi配置、运行日志这类小数据特点是频繁改写、数据量小适合在Flash尾部单独分配几个扇区做轮替写入。第二种是固件备份区。做OTA升级的时候先把新固件写到外部Flash校验通过后再写入MCU内部。这时候QSPI的快读优势非常明显1MB镜像用四线读也就是一两秒的事。第三种是只读数据区比如中文字库、图片素材、音频片段。这类数据几乎不擦写只需要快速读。GD25Q80E在QSPI四线读模式下可以跑到很高的时钟频率实测比从SPI方式读取快了好几倍后面有对比数据。2. 命令时序拆开看所谓时序就是“先干什么、后干什么”2.1 读指令0x03/0x0B一个命令码引出三个周期SPI NOR Flash的所有操作本质上都是“发命令码 发地址 读/写数据”区别只在于每个阶段用几根线以及中间有没有dummy周期。最基础的读指令是0x03时序是CS拉低 → 发0x03 → 发3字节地址高字节在前→ 连续读数据 → CS拉高。整个过程都是单线SPI每个时钟周期传1位。0x03的好处是简单直接坏处是地址切换后芯片内部需要一点时间来把数据摆到输出上时钟频率太高容易读到垃圾数据。0x0B是快速读指令和0x03的区别是在地址后面多了一个dummy字节8个空时钟周期。多出来的这段时间给了芯片内部地址译码和预充电所以0x0B可以跑更高的时钟频率。在GD25Q80E的数据手册里0x03标准读通常限制在50MHz左右而0x0B快速读可以支持到更高频率。实际写驱动的时候我会给这两种读指令做一个统一的封装void spi_flash_read(uint32_t addr, uint8_t *buf, uint32_t len) { cs_low(); spi_write_byte(0x0B); // 选快速读留dummy周期 spi_write_byte((addr 16) 0xFF); spi_write_byte((addr 8) 0xFF); spi_write_byte(addr 0xFF); spi_write_byte(0xFF); // dummy周期 spi_read_bytes(buf, len); cs_high(); }地址是24位的因为GD25Q80E最大1MB正好够用。如果换更大的芯片比如16MB的GD25Q128就需要用0x3C这种带4字节地址模式的指令来扩展地址空间这个坑等你换大容量Flash时会遇到。2.2 状态寄存器0x05能不能干下一件事先看忙标志Flash的擦和写都是耗时操作不像读那样可以立刻返回。GD25Q80E内部有一个状态寄存器通过0x05命令读取其中第0位S0是BUSY位为1表示芯片正在忙为0表示空闲。控制器必须轮询这个位确认不再忙之后才能进行下一次操作。很多初学者写代码发完页编程命令就接着发读命令结果读回来的全是旧数据。原因很简单写操作还在进行Flash不会理你。正确的流程是// 等待Flash空闲 void spi_flash_wait_busy(void) { uint8_t status; cs_low(); spi_write_byte(0x05); // 读状态寄存器 do { status spi_read_byte(); } while (status 0x01); // BUSY位为1就继续等 cs_high(); }等待时间因操作类型而异页编程一般是0.7ms到3ms扇区擦除通常是50ms到400ms整片擦除则要好几秒。千万别用固定延时来代替轮询。环境温度低或者芯片体质差的时候擦除时间会偏长固定延时等不到就翻车了。这里还有一个经验每次发完写、擦命令之后一定要先等待忙信号再去发下一次命令。这个顺序不能省也不能靠延时蒙混过关这是这类Flash驱动稳定性的第一原则。2.3 写使能与写保护为什么每次写之前都要先发0x06NOR Flash有个安全机制任何写操作、擦除操作、状态寄存器写操作前提都是先发写使能命令0x06。0x06会把状态寄存器里的WEL位Write Enable Latch第1位置1芯片才允许你进行写操作。发完写命令或者写操作失败之后WEL位会自动清零。所以每写一次就要重新发一次0x06。我见过有人写代码只发一次0x06后面连续写多个扇区结果第一个扇区成功了后面的全部失败。查了半天最后发现是WEL已经清零了。另外GD25Q80E还有几根写保护引脚和状态寄存器里的BP位组合作用。状态寄存器的BP3-BP0可以圈定一块写保护区域让这块区域无论如何都无法被擦除或改写防止程序跑飞时把关键数据冲掉。板子上的WP引脚写保护配合这几位可以做到硬件级保护。默认情况下BP位都是0全片可写。如果哪天你发现“为什么我擦写都成功但数据不变”建议先读一下状态寄存器的BP位是不是被谁设成了非零。2.4 串行接口的演进SPI、DSPI、QSPI三个名词到底差在哪标准SPI只有一根数据输入线MOSI和一根数据输出线MISO命令、地址、数据都必须一位一位串行走。GD25Q80E这类芯片还支持双通道和四通道模式。DSPIDual SPI在发送地址和接收数据阶段把MOSI、MISO两根线都用来传数据相当于数据吞吐翻倍。QSPIQuad SPI更进一步把IO0、IO1、IO2、IO3四根线都用来传数据发送地址用4根线的话只需原来1/4的时钟周期接收数据时每个时钟周期能读4位。需要注意的是命令码阶段通常还是单线发送因为芯片要先把命令码完整接收后才能判断后续阶段用几根线。QSPI指令有专门的命令码比如0xEB是四线快速读、0x6B是双线快速读。用普通SPI的0x0B命令发出来后芯片不会自动切换到四线模式。所以一个完整QSPI读命令看起来就是单线发0xEB → 四线发3字节地址 → 四线发若干个dummy周期 → 四线连续收数据。理解了这个流程STTM32的QSPI外设配置就明了一半。3. 页编程、扇区擦除与跨页边界Flash写入的物理规则3.1 为什么一次最多写256字节页缓冲区的物理上限页编程指令是0x02每次最多写入256字节正好是一个页的大小。这不是软件限制而是芯片内部的硬件约束每一页对应一个页缓冲区写入的数据需要先放进缓冲区再在内部高压下“注入”存储单元缓冲区大小就是256字节。写页编程命令时地址必须是页内地址也就是地址低8位就是页内偏移高16位是页号。如果发命令时地址低8位不是0x00芯片会从那个偏移开始写写到页边界就停止多余的数据被丢弃。这跟很多芯片的I2C EEPROM“跨页回卷”行为不一样——NOR Flash是“写到页尾截断”不会自动绕回页首。用STM32 HAL库写页编程时注意保证那一次传输不超过256字节HAL_StatusTypeDef spi_flash_page_program(uint32_t addr, const uint8_t *data, uint16_t len) { if (len 256) return HAL_ERROR; // 单次页编程上限256字节 uint8_t cmdbuf[4] {0x02, (addr 16) 0xFF, (addr 8) 0xFF, addr 0xFF}; cs_low(); HAL_SPI_Transmit(hspi, cmdbuf, 4, 100); HAL_SPI_Transmit(hspi, (uint8_t *)data, len, 100); cs_high(); return HAL_OK; }3.2 跨页连续写不加保护的代码会在页边界上翻车做驱动库的人都知道上层调用者不会关心页边界他们想要的是一个“从某个地址开始写任意长度”的接口。这就需要驱动内部做跨页拆分也叫“翻页处理”。举个例子从地址0x0012FF00开始写500字节地址低8位是0x00看起来正好是页对齐但500字节跨了两个页。一次页编程最多256字节所以必须先写256字节把剩下244字节写到下一页。更麻烦的情况是从0x0012FF80开始写500字节第一页只剩128字节空间得拆成128、156、216三次写其余部分。实现跨页拆分时有一个很实用的公式本次可写字节数 256 - (addr 0xFF)然后和剩余长度取最小值。void spi_flash_write(uint32_t addr, const uint8_t *data, uint32_t len) { while (len 0) { uint32_t remain_in_page 256 - (addr 0xFF); uint32_t chunk (len remain_in_page) ? len : remain_in_page; spi_flash_wait_busy(); spi_flash_page_program(addr, data, chunk); addr chunk; data chunk; len - chunk; } }凡是把扇区看成“可以随便写的一块内存”的代码都容易在这里出bug。你可能会连续写几百字节发现中间有一块数据总是错的排查到最后就是跨页丢包。3.3 擦除粒度决定存数据结构日志、配置、固件分别怎么排理解了擦除粒度之后Flash上的数据布局就变成一门学问。配置类的数据一般很小比如几十个字节频繁更新。如果每次都去擦写同一个扇区Flash的擦写寿命很快耗尽。GD25Q80E的扇区擦写次数典型值是10万次听起来不少但如果你每10秒写一次配置10万次也就11天。解决办法是“轮替多扇区”分配4到8个扇区每次写配置写到不同扇区写满后整体擦除最老的那块。这样寿命直接翻几倍。日志类数据建议采用“线性日志”模式一个扇区一个扇区往后写写满一个扇区再擦下一个。避免频繁擦写同一个扇区也能降低写放大。固件备份类数据就是纯粹的“大块连续写”注意按扇区对齐就能减少不必要的擦除。比如1MB的固件镜像最好从扇区边界开始写否则一次升级可能要额外擦好几个扇区拖慢升级时间不说万一中途掉电恢复逻辑也更复杂。4. STM32 QSPI实战从“软件模拟时序”到“硬件映射成内存”4.1 普通SPI挂了8Mbit Flash会遇到哪些问题STM32的SPI外设是通用的串行口用来挂NOR Flash当然可以但有几个硬伤。一是SCK频率受限于APB总线分频通常SPI最高几十MHz单线模式下读吞吐确实有限。二是SPI外设每次收发都围绕一个小FIFO转每读一个字节都要CPU参与虽然有DMA频繁中断会影响系统实时性。三是最关键的普通SPI没办法把Flash直接映射到MCU地址空间你无法用一个指针去读Flash里的数据。如果有1MB镜像要从外部Flash拷贝到内部RAM或者SDRAM普通SPI方式要反复调用DMA搬运代码啰嗦且baud率有限。这时候STM32系列带QUADSPI外设的型号就体现出优势了。4.2 QSPI外设的两种工作模式间接模式与内存映射模式QUADSPI外设是STM32专门为了连四线Flash设计的高速接口最核心的能力有两个一个是间接模式一个是内存映射模式。间接模式下CPU通过外设寄存器发出完整命令序列数据经FIFO进出。这个模式适合写入、擦除、读状态这些不频繁操作。每次操作前要用HAL库的HAL_QSPI_Command配置命令阶段、地址阶段、数据阶段的长度和线宽。内存映射模式下QUADSPI外设把Flash内容直接映射进MCU的地址空间。对STM32F4和F7系列来说映射区起始地址一般是0x90000000地址长度由初始化时的FlashSize字段决定。一旦使能内存映射模式你就可以用指针直接读Flashconst volatile uint8_t *flash_ptr (const volatile uint8_t *)0x90000000; uint8_t first_byte flash_ptr[0];这种体验跟读内部Flash一样对代码结构是降维打击。显示字库、音频素材、查表数据全部可以直接丢在外部Flash里不用先搬进RAM。4.3 HAL库配置要点CubeMX里那些参数不是随手填的先用CubeMX选中QUADSPI外设重点关注几个初始化参数。ClockPrescaler是时钟分频系数。QUADSPI的输入时钟通常来自AHB配置为2表示二分频。假如AHB是168MHz那么实际QSPI时钟约84MHz这在GD25Q80E支持范围内。分频系数太小容易超频太慢又浪费带宽。FlashSize是以2为底的指数。GD25Q80E有1MB空间地址空间长度就是2的20次方所以FlashSize填19到20之间具体看HAL库定义——HAL库要求填的是容量 - 1的二进制位数也就是20 - 1 19。等等这里我要说清楚不同HAL版本定义不太一致有些库直接写容量对应的指数20有些写指数减一19。最好的办法是查你的HAL库头文件注释或者在CubeMX里把地址范围调整成1MB后看它自动生成的值。这个细节错了内存映射只有部分地址可用或者溢出是最典型的配置坑。SampleShifting用来决定数据采样时刻时钟频率高的时候可以切换采样沿来规避信号延迟。FifoThreshold是FIFO触发阈值读速度要求高就设小一点。配置完这些生成初始化代码后用HAL_QSPI_EnableMemoryMappedMode启动映射。以GD25Q80E默认在QSPI下的读命令0xEB为例配置大致是QSPI_CommandTypeDef sCommand {0}; sCommand.InstructionMode QSPI_INSTRUCTION_1_LINE; sCommand.Instruction 0xEB; sCommand.AddressMode QSPI_ADDRESS_4_LINES; sCommand.AddressSize QSPI_ADDRESS_24_BITS; sCommand.AlternateByteMode QSPI_ALTERNATE_BYTES_NONE; sCommand.DataMode QSPI_DATA_4_LINES; sCommand.DummyCycles 6; sCommand.DdrMode QSPI_DDR_MODE_DISABLE; sCommand.DdrHoldHalfCycle QSPI_DDR_HHC_ANALOG_DELAY; sCommand.SIOOMode QSPI_SIOO_INST_EVERY_CMD; HAL_QSPI_EnableMemoryMappedMode(hqspi, sCommand);DummyCycles填多少要看GD25Q80E手册里的0xEB时序一般是6到8个。填少了数据不对填多了映射仍然能跑但带宽浪费。这块没有捷径必须开手册对着看。4.4 内存映射模式下读出“FF”、写不进去多半是Cache一致性F4系列没有Cache还好F7/H7系列带I-Cache和D-Cache之后QSPI内存映射模式会引入一个新的坑Cache一致性。当DMA或者CPU直接写外部Flash时数据先经过QSPI外设进入Flash但是CPU读映射区的时候可能命中了一级Cache里的旧数据表现出来就是你明明写成功了再读还是旧值。解决方案是在写入完成后调用SCB_CleanDCache或SCB_InvalidateDCache把相关地址的Cache行失效。另一个极端是Flash本身不具备“原地改写”能力内存映射模式只能用来读不能直接往映射地址里写。如果想通过映射区写数据QSPI外设会把写操作转成Flash页编程命令吗答案是不会对于NOR Flash内存映射模式本质上是只读的写入还是要走间接模式、发页编程命令。这两点理解了QSPI映射模式的运行逻辑就全通了地址空间只是“读的窗口”写永远要回到命令时序。5. 实测与对比QSPI到底快在哪又没快在哪5.1 三组实测数据单线读、双线读、四线读我在一块主频168MHz的STM32F407板子上用逻辑分析仪统计了从GD25Q80E读取2MB数据的耗时。这里的“读取”是连续执行读命令不包含擦写。同样的SCK时钟约80MHz情况下模式典型命令每时钟周期传输数据实测吞吐量相对标准SPI标准SPI读0x0B1 bit约 8.6 MB/s1x双线读0x3B2 bit约 16.8 MB/s约2x四线读QSPI0xEB4 bit约 32.4 MB/s约3.8x这个提升非常直观。如果开启QSPI的DDR模式上升沿和下降沿都能采样理论还能翻倍但实际对PCB布线和芯片体质更敏感我建议普通板子先别碰DDR默认SDR模式已经很能打。5.2 擦写速度不会因为QSPI而变快这是Flash物理规则的边界很多人看到QSPI读速度这么快就以为写和擦除也会变快。这是误解。读数据走的是数字接口快慢完全取决于IO传输协议而擦除和页编程的耗时主要在芯片内部的高压电荷泵和存储单元物理操作上跟接口位宽没什么关系。GD25Q80E的页编程典型时间在0.7ms到3ms左右扇区擦除典型50ms到400ms整片擦除几秒。不管你是用标准SPI还是QSPI外设发起命令这个时间都差不多。所以硬件设计上QSPI的价值主要集中在“大数据量读取”场景。如果你的应用全是频繁小数据改写QSPI和SPI差别不大反而SPI的软件生态更成熟、更好调。5.3 什么时候用SPI什么时候用QSPI选型判断的三步走第一步看读数据量如果整个系统生命周期内读Flash的数据总量有限普通SPI足够如果要频繁加载图片、字库、固件块考虑QSPI。第二步看MCU有没有QUADSPI外设。很多小封装、低成本MCU并不带QUADSPI强行用GPIO模拟四线时序只会更慢。此时老老实实SPI单线/DSPI即可。第三步看开发效率。QSPI内存映射模式天然友好CPU直接按指针操作而SPI方式要写驱动封装数组读取。如果项目组对Flash不太熟悉选QSPI映射模式反而能减少错误。但要注意切换用QSPI后配置参数比SPI多调试门槛也高一些。6. 这些坑我替你先踩了GD25Q80E实战排错记录6.1 擦除完马上读数据是0xFF但不是“一定成功”这是文章开头那个例子的完整真相。擦除命令发出后芯片内部要花几十到几百毫秒真正完成擦除。如果擦除后不轮询BUSY立刻读你读到的可能是还没擦完的随机数据也可能读到的是芯片在忙状态下的无效输出。表现为“读出来不是0xFF但也不是旧数据”让人非常困惑。正确姿势是每次擦除命令发出后调用wait_busy函数等状态寄存器S0位变0然后才认为擦除完成。不要相信任何“经验延时”直接轮询永远比拍脑袋延时可靠。6.2 状态寄存器的BP位出厂默认值也能坑你一脚多数GD25Q80E出厂状态寄存器是0x00意味着BP位都是0全片可写。但一些特殊批次或翻新片不一定如此或者之前跑过的程序为了开启保护改过BP位、掉电后没有恢复。遇到“命令都正确、状态寄存器也正常但写进去再读全是FF”的情况第一步不是换芯片而是用0x05读状态寄存器看BP位。如果BP位非零可以先发0x06写使能再用0x01往状态寄存器里写0x00把保护关掉然后再做擦写。这是一条非常重要的排查顺序。6.3 同一型号不同批次ID一样但块擦除时序也有差异Flash芯片ID相同只代表兼容不代表所有细节完全一致。我在批量生产时遇到过同是GD25Q80E不同批次在块擦除后需要的等待时间有细微差别表现为新批次产品在快速连续擦写时偶发失败。根治方案还是那句话每次操作后都必须轮询BUSY位并且重试机制要设计好。驱动层做三层保障写前检查、写后轮询、失败重试。把这三步做成标准模板换Flash型号也基本不用改代码。6.4 用JEDEC ID识别打磨片和翻新件市面上的打磨片、翻新片不少尤其在小批量渠道。除了读ID还有一个做法是读Unique IDGD25Q80E支持用0x4B读取芯片唯一ID每颗都不一样。如果一批芯片读出来好几颗Unique ID完全相同基本可以判定是翻新片或者测试片流出来的。还有一个小技巧正规新片第一次擦除前全片读出来应该是0xFF。如果你想买到手的是全新滚片可以先做一次全片读校验。如果发现有些扇区原本就不是0xFF那就得怀疑是不是二手或旧片。做嵌入式这些年SPI NOR Flash算是门槛不高但坑攒得不少的外设之一。我现在做项目只要MCU支持QUADSPI且外部需要存超过64KB的数据一般会优先安排QSPI Flash。别的不说光是内存映射模式把外部Flash当数组读这一点就省了写一大堆拷贝逻辑。而GD25Q80E作为一颗1MB的入门级四线Flash价格便宜、资料多、兼容性好非常适合拿来练手或者做产品验证。如果你正在调QSPI Flash出现“命令发得没问题但数据总不对”的情况建议先用示波器抓一抓IO0-IO3在命令、地址、数据三个阶段的电平变化对照着数据手册里的时序图看一遍。多数问题都能在时序图上现出原形。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询