STM32 QSPI驱动GD25Q80E实战:从单线到四线高速读写与OTA分区设计
2026/9/19 12:09:07 网站建设 项目流程

1. 搞懂 SPI NOR Flash 到底在系统里扮演什么角色

1.1 为什么嵌入式项目绕不开 SPI NOR Flash

做过 STM32 项目的人大概率都碰过这样一个场景:板子上的 MCU 内部 Flash 只有 512KB 甚至 128KB,跑个裸机程序绰绰有余,可一旦要存字库、图片、音频、日志、OTA 升级包,内部空间立刻捉襟见肘。这时候最省事、最便宜、最通用的扩容方案,就是外挂一颗 SPI NOR Flash。

SPI NOR Flash 在嵌入式系统里的定位非常清晰:它是非易失性存储,掉电不丢数据,读写接口简单,封装小,价格低,容量从 1Mbit 到 512Mbit 都有。像 GD25Q80E 这颗 8Mbit(也就是 1MB)的芯片,几乎成了很多开发板的标配,正点原子、野火、安富莱的板子上都能见到它的身影。

它解决的问题其实就三类:

  • 代码存储:把一部分不常改的代码或资源放到外部 Flash,MCU 通过 XIP 或者搬运到 RAM 执行。
  • 数据存储:存配置参数、字库、图片、音频、日志等。
  • OTA 升级:新固件先写到外部 Flash,校验通过后再搬运到内部 Flash 或者直接在外部执行。

适合读这篇文章的人,我大致分三类:一是刚学完 STM32 基础外设,想进阶搞存储的;二是项目里要用外部 Flash 但被时序和命令搞得一头雾水的;三是想从普通 SPI 升级到 QSPI 四线模式提升读写速度的。这三类人看下去都会有收获。

1.2 GD25Q80E 这颗芯片的基本盘

GD25Q80E 是兆易创新(GigaDevice)出的 8Mbit SPI NOR Flash,标准 SPI 接口,支持单线、双线、四线模式。它的关键参数如下:

参数项数值说明
容量8Mbit / 1MB128 个 4KB 扇区,或 16 个 64KB 块
电压2.7V ~ 3.6V典型 3.3V 系统
接口SPI / Dual SPI / Quad SPI支持模式 0 和模式 3
最高时钟单线 120MHz,四线 120MHz实际受 MCU 和布线限制
页大小256 字节一次最多写 256 字节
扇区大小4KB最小擦除单位
块大小64KB大块擦除单位
擦写寿命10 万次每个扇区
数据保持20 年常温

这里有几个点必须提前说清楚,不然实操一定踩坑:

注意:NOR Flash 的写入规则是“先擦后写”,而且擦除的最小单位是 4KB 扇区,不是你想改哪个字节就改哪个字节。写之前对应区域必须是 0xFF 状态,否则写进去的数据是错的。

注意:一次写操作不能跨页。页大小 256 字节,如果你从地址 0x0000FF 开始写 10 个字节,会写到 0x000100 去,但地址 0x000100 属于下一页,实际会回卷到本页开头覆盖数据。这是新手最容易翻车的地方。

1.3 单线、双线、四线的本质区别

很多人搞不清 SPI、Dual SPI、Quad SPI 到底差在哪。我用一句话概括:区别只在数据线的数量,命令和地址的传输方式也相应变化,但底层还是 SPI 协议那一套。

  • 标准 SPI(单线):MOSI 发数据,MISO 收数据,一个时钟周期传 1 位。
  • Dual SPI(双线):把 MOSI 和 MISO 都当成双向数据线用,一个时钟周期传 2 位。
  • Quad SPI(四线):再加上 WP# 和 HOLD# 两个引脚当数据线,一个时钟周期传 4 位。

理论上四线比单线快 4 倍,但实际提升没那么夸张,因为命令阶段和地址阶段可能还是单线,只有数据阶段是四线。真正全四线的模式叫 QPI,命令、地址、数据全走四线,那才是速度拉满的玩法。

STM32 的 QSPI 外设就是专门为这种场景设计的,它硬件上支持命令阶段、地址阶段、数据阶段、交替字节阶段、空周期阶段分别配置线宽,非常灵活。后面我会详细讲怎么配。

2. GD25Q80E 命令时序拆解:别只会发 0x03

2.1 常用命令一览与使用场景

GD25Q80E 的命令集和大多数 SPI NOR Flash 兼容,我整理了一张常用命令表,这些是实操中真正会用到的:

命令十六进制功能常用场景
Write Enable0x06写使能每次擦写前必须发
Write Disable0x04写禁止可选
Read Status Register 10x05读状态寄存器轮询 BUSY 位
Read Data0x03普通读低速读,单线
Fast Read0x0B快速读带空周期,支持高速
Fast Read Dual0x3B双线快速读双线模式
Fast Read Quad0x6B四线快速读QSPI 常用
Page Program0x02页编程写数据,最多 256 字节
Quad Page Program0x32四线页编程四线写
Sector Erase0x204KB 扇区擦除小范围擦除
Block Erase 32KB0x5232KB 块擦除中等范围
Block Erase 64KB0xD864KB 块擦除大范围擦除
Chip Erase0xC7/0x60全片擦除整片清空
Read ID0x9F读 JEDEC ID识别芯片
Enable QPI0x38进入 QPI 模式全四线
Exit QPI0xFF退出 QPI恢复单线
Enable Reset0x66复位使能软复位前
Reset0x99复位软复位

这张表建议直接收藏,调试的时候对着看,比翻数据手册快得多。

2.2 读操作时序:0x03 和 0x0B 差在哪

先看最简单的读命令 0x03。它的时序是:

  1. CS 拉低
  2. 发送 0x03(8 个时钟)
  3. 发送 24 位地址(3 字节)
  4. 连续读取数据,每个时钟一位
  5. CS 拉高结束

这个命令的优点是简单,缺点是速度上不去。因为数据在时钟下降沿输出,MCU 需要在上升沿采样,留给信号建立的时间很短,一般时钟不能超过 50MHz 左右。

0x0B(Fast Read)多了一个东西:空周期(Dummy Cycles)。时序变成:

  1. CS 拉低
  2. 发送 0x0B
  3. 发送 24 位地址
  4. 发送 8 个空周期(时钟照跑,数据线不管)
  5. 连续读取数据
  6. CS 拉高

这 8 个空周期是给 Flash 内部准备数据用的,有了它,时钟可以拉到 100MHz 以上。你可以这样理解:0x03 是“你问了我立刻答”,0x0B 是“你问了我喘口气再答”,喘这口气的时间让芯片能跑得更快。

实操心得:如果你用普通 SPI 读,时钟低于 30MHz,用 0x03 就行,简单可靠。一旦超过 50MHz,必须换 0x0B,否则读出来的数据会随机出错,而且这种错误很难查,因为低速测试时完全正常。

2.3 写操作时序:Write Enable 是绕不过去的门槛

NOR Flash 的写操作有个硬性规定:每次擦除或编程之前,必须先发 Write Enable(0x06)。这是芯片的安全机制,防止误写。

完整的写流程是这样的:

  1. 发 0x06(Write Enable)
  2. 读状态寄存器(0x05),确认 WEL 位(bit 1)为 1
  3. 发擦除或编程命令
  4. 轮询状态寄存器,等 BUSY 位(bit 0)变 0
  5. 完成

这里有个细节很多人忽略:Write Enable 是“一次性”的。你发一次 0x06,只能执行一次擦除或编程操作。如果你要连续写多个页,每一页之前都要重新发 0x06。我见过有人写了一个循环,只发一次 0x06,结果只有第一页写成功,后面全是 0xFF,查了半天以为是硬件问题。

状态寄存器的 BUSY 位轮询也有讲究。擦除一个 4KB 扇区典型时间是 45ms,最大 400ms;写一页 256 字节典型 0.7ms,最大 3ms。你不能发完命令立刻读数据,必须等 BUSY 变 0。

// 等待 Flash 空闲的典型实现 void Flash_WaitBusy(void) { uint8_t status; do { Flash_CS_Low(); SPI_TransferByte(0x05); // 读状态寄存器1 status = SPI_TransferByte(0xFF); Flash_CS_High(); } while (status & 0x01); // bit0 是 BUSY 位 }

这段代码看起来简单,但有个坑:轮询频率不能太高。如果你在 100MHz SPI 下疯狂轮询,CS 频繁翻转,反而会影响 Flash 内部操作。实际项目中我一般加个 1ms 延时再轮询,既不影响速度,又稳定。

2.4 擦除操作时序:4KB、32KB、64KB 怎么选

擦除命令有三个粒度:

  • 0x20 扇区擦除:4KB,典型 45ms
  • 0x52 块擦除:32KB,典型 120ms
  • 0xD8 块擦除:64KB,典型 150ms
  • 0xC7 全片擦除:整片,典型 10s(8Mbit)

选择原则很简单:擦除范围越小,时间越短,但命令开销占比越高;范围越大,单字节擦除成本越低,但灵活性差。

举个例子,你要存 10KB 的数据,分布在 3 个 4KB 扇区里。用扇区擦除,发 3 次命令,总时间约 135ms。用 64KB 块擦除,发 1 次命令,时间 150ms,但会把块内其他数据也擦掉。所以如果你只是更新一小块数据,千万别图省事用大块擦除,会把旁边的数据一起干掉。

踩过的坑:有一次做 OTA,新固件 60KB,我图快用了 64KB 块擦除,结果把紧挨着的配置参数区也擦了。配置区在 64KB 边界之后,我以为没事,实际上块擦除是按地址对齐的,0x10000 开始的 64KB 块覆盖 0x10000~0x1FFFF,配置区在 0x1F000,正好在里面。这个教训告诉我,擦除前一定要算清楚地址范围。

2.5 状态寄存器:BUSY 和 WEL 两位必须盯紧

状态寄存器 1 的位定义如下:

名称含义
bit 0BUSY1 表示芯片忙,0 表示空闲
bit 1WEL1 表示写使能已置位
bit 2BP0块保护位 0
bit 3BP1块保护位 1
bit 4BP2块保护位 2
bit 5TB顶部/底部保护选择
bit 6SEC扇区/块保护选择
bit 7SRP0状态寄存器保护

BP0~BP2 和 TB、SEC 组合起来可以保护部分区域不被写入,这个功能在量产时很有用,可以防止程序跑飞误擦 bootloader 区。但调试阶段建议全部清零,不然你会遇到“明明发了写命令却写不进去”的诡异问题。

实操心得:如果你发现写操作一直失败,先读状态寄存器,看看 BP 位是不是被置位了。有些 Flash 出厂默认保护一部分区域,或者上一次程序设置了保护没清除,都会导致写失败。发 0x01 写状态寄存器可以把 BP 位清零。

3. STM32 QSPI 外设配置实战

3.1 为什么用 QSPI 而不是普通 SPI

普通 SPI 外设也能驱动 NOR Flash,但有几个硬伤:

  • 速度上限低:STM32 的 SPI 最高时钟一般 50MHz 左右(F4 系列),F7/H7 能到 100MHz,但单线模式下实际有效带宽有限。
  • CPU 占用高:每次读写都要 CPU 参与搬运数据,大块数据传输时 CPU 基本被占满。
  • 不支持内存映射:没法像访问内存一样直接读 Flash,XIP 执行代码很麻烦。

QSPI 外设解决了这些问题:

  • 四线模式:理论带宽是单线的 4 倍。
  • DMA 支持:大块数据传输不占 CPU。
  • 内存映射模式:可以把外部 Flash 映射到地址空间,像读内存一样读 Flash,直接 XIP 执行代码。
  • 硬件自动处理命令阶段:命令、地址、空周期、数据阶段的线宽和长度都能配,不用软件一位一位拼。

以 STM32F767 为例,QSPI 时钟最高可以到 108MHz(AHB 216MHz 的二分频),四线模式下理论带宽 54MB/s,实际能跑到 30MB/s 以上,比普通 SPI 快一个数量级。

3.2 硬件连接:引脚映射和注意事项

GD25Q80E 和 STM32 QSPI 的典型连接如下(以 STM32F767 的 QUADSPI 为例):

GD25Q80E 引脚STM32 引脚功能
CS#PB6片选
CLKPB2时钟
DI (MOSI)PD11数据线 0
DO (MISO)PD12数据线 1
WP#PD13数据线 2
HOLD#/RESET#PB10数据线 3
VCC3.3V电源
GNDGND

几个硬件上的注意点:

注意:WP# 和 HOLD# 在四线模式下变成数据线 2 和数据线 3,所以这两个引脚不能再做其他用途。如果你用的是普通 SPI 模式,WP# 和 HOLD# 要上拉到 VCC,否则芯片可能进入保护或保持状态。

注意:CLK 走线尽量短,最好等长处理。四线模式下 100MHz 时钟,走线长了信号质量会明显下降,读数据出错率飙升。我见过一块板子 CLK 走了 8cm,四线模式死活跑不起来,降到 50MHz 才稳定。

注意:CS# 上拉一个 10K 电阻到 VCC,防止 MCU 复位期间引脚浮空导致 Flash 误动作。

3.3 CubeMX 配置:参数怎么填

用 CubeMX 配置 QSPI 相对省事,但有几个参数必须理解清楚,不然生成的代码跑不起来。

Clock Prescaler:分频系数,QSPI 时钟 = AHB 时钟 / (Prescaler + 1)。比如 AHB 216MHz,Prescaler 设 1,QSPI 时钟 108MHz。调试阶段建议先设大一点,比如 8 分频,跑通了再往上加。

FIFO Threshold:FIFO 阈值,一般设 4 或 8。这个影响 DMA 触发时机,设太小会频繁中断,设太大可能溢出。

Sample Shifting:采样偏移,可选 None 或 Half Cycle。高速时钟下建议选 Half Cycle,给数据信号更多建立时间。

Flash Size:Flash 容量,GD25Q80E 是 8Mbit,这里填 0x14(表示 2^20 = 1MB,即 8Mbit)。这个值影响内存映射模式的地址范围。

CSHT(Chip Select High Time):片选高电平保持时间,可选 1~8 个时钟周期。一般设 1 或 2 就行,太短可能导致命令之间间隔不够。

Clock Mode:CPOL 和 CPHA。GD25Q80E 支持模式 0(CPOL=0, CPHA=0)和模式 3(CPOL=1, CPHA=1)。CubeMX 里选 Mode 0 即可。

3.4 命令配置结构体:每个字段都要搞明白

STM32 HAL 库的 QSPI 命令配置结构体QSPI_CommandTypeDef是核心,字段比较多,我逐个解释:

QSPI_CommandTypeDef sCommand = {0}; sCommand.Instruction = 0x6B; // 命令码,四线快速读 sCommand.InstructionMode = QSPI_INSTRUCTION_1_LINE; // 命令阶段单线 sCommand.Address = 0x000000; // 读起始地址 sCommand.AddressMode = QSPI_ADDRESS_1_LINE; // 地址阶段单线 sCommand.AddressSize = QSPI_ADDRESS_24_BITS; // 24位地址 sCommand.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE; // 无交替字节 sCommand.AlternateBytes = 0; sCommand.AlternateBytesSize = QSPI_ALTERNATE_BYTES_8_BITS; sCommand.DummyCycles = 8; // 8个空周期 sCommand.DataMode = QSPI_DATA_4_LINES; // 数据阶段四线 sCommand.NbData = 4096; // 读4096字节 sCommand.DdrMode = QSPI_DDR_MODE_DISABLE; // 关闭DDR sCommand.DdrHoldHalfCycle = QSPI_DDR_HHC_ANALOG_DELAY; sCommand.SIOOMode = QSPI_SIOO_INST_EVERY_CMD; // 每次命令都发指令

这里最容易搞错的是DummyCycles。GD25Q80E 在四线快速读(0x6B)时,数据手册规定是 8 个空周期。但如果你用的是 0xEB(Quad I/O Fast Read),空周期可能是 6 个或 10 个,具体看模式配置。填错了读出来全是 0xFF 或者乱码。

实操心得:DummyCycles 不对是 QSPI 调试最常见的坑。我的经验是,先用单线 0x03 读 ID,确认硬件连接没问题;再切到 0x0B 单线快速读,确认空周期配置;最后才上 0x6B 四线。一步一步来,比一上来就四线好查问题。

3.5 内存映射模式:像读内存一样读 Flash

QSPI 最爽的功能是内存映射模式。配置好之后,外部 Flash 的内容会映射到 STM32 的地址空间(通常是 0x90000000 开始),你可以直接用指针读:

// 配置内存映射模式 QSPI_CommandTypeDef sCommand = {0}; sCommand.Instruction = 0x6B; sCommand.InstructionMode = QSPI_INSTRUCTION_1_LINE; sCommand.AddressMode = QSPI_ADDRESS_1_LINE; sCommand.AddressSize = QSPI_ADDRESS_24_BITS; sCommand.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE; sCommand.DummyCycles = 8; sCommand.DataMode = QSPI_DATA_4_LINES; sCommand.NbData = 0; // 内存映射模式必须为0 QSPI_MemoryMappedTypeDef sMemMappedCfg = {0}; sMemMappedCfg.TimeOutActivation = QSPI_TIMEOUT_COUNTER_DISABLE; sMemMappedCfg.TimeOutPeriod = 0; HAL_QSPI_MemoryMapped(&hqspi, &sCommand, &sMemMappedCfg); // 之后就可以直接读 uint8_t *flash_ptr = (uint8_t *)0x90000000; uint8_t data = flash_ptr[0x1000]; // 读地址0x1000的数据

内存映射模式下,读操作完全由硬件完成,CPU 只管取数据,速度非常快。如果你要把代码放到外部 Flash 执行(XIP),就必须用这个模式。

但内存映射模式有个限制:只能读,不能写。写操作还是要退出映射模式,用间接模式发命令。所以典型流程是:上电配好映射模式,运行时代码直接读;需要写的时候切回间接模式,写完再切回映射模式。

4. 完整驱动实现与常见问题排查

4.1 从零写一个可用的驱动框架

我把驱动分成三层:底层 SPI 收发、中间命令封装、上层应用接口。这样分层的好处是换 MCU 或换 Flash 型号时,只需要改底层或中间层。

底层:负责 CS 控制、字节收发、延时。如果用 QSPI 外设,这层基本由 HAL 库代劳。

中间层:封装读 ID、读状态、写使能、擦除、页写、读数据等函数。

上层:提供Flash_ReadFlash_WriteFlash_Erase这种通用接口,内部处理跨页、擦除对齐等逻辑。

先看读 ID 函数,这是验证硬件连接的第一步:

uint32_t Flash_ReadID(void) { uint8_t id[3]; QSPI_CommandTypeDef cmd = {0}; cmd.Instruction = 0x9F; cmd.InstructionMode = QSPI_INSTRUCTION_1_LINE; cmd.AddressMode = QSPI_ADDRESS_NONE; cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE; cmd.DummyCycles = 0; cmd.DataMode = QSPI_DATA_1_LINE; cmd.NbData = 3; HAL_QSPI_Command(&hqspi, &cmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Receive(&hqspi, id, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); return (id[0] << 16) | (id[1] << 8) | id[2]; }

GD25Q80E 的 JEDEC ID 是 0xC84014。如果你读出来是 0xFFFFFF 或 0x000000,说明硬件连接有问题;如果是其他值,可能是兼容型号或者命令模式不对。

再看页写函数,这里要处理跨页问题:

void Flash_PageWrite(uint32_t addr, uint8_t *buf, uint16_t len) { // 计算本页剩余空间 uint16_t page_remain = 256 - (addr % 256); if (len > page_remain) { len = page_remain; // 截断到本页末尾 } Flash_WriteEnable(); QSPI_CommandTypeDef cmd = {0}; cmd.Instruction = 0x02; cmd.InstructionMode = QSPI_INSTRUCTION_1_LINE; cmd.Address = addr; cmd.AddressMode = QSPI_ADDRESS_1_LINE; cmd.AddressSize = QSPI_ADDRESS_24_BITS; cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE; cmd.DummyCycles = 0; cmd.DataMode = QSPI_DATA_1_LINE; cmd.NbData = len; HAL_QSPI_Command(&hqspi, &cmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Transmit(&hqspi, buf, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); Flash_WaitBusy(); }

上层写函数负责拆分跨页和擦除:

void Flash_Write(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t sector_start = addr & ~(0xFFF); // 4KB对齐 uint32_t sector_end = (addr + len - 1) & ~(0xFFF); // 先擦除涉及的扇区 for (uint32_t s = sector_start; s <= sector_end; s += 4096) { Flash_SectorErase(s); } // 再按页写入 while (len > 0) { uint16_t page_len = 256 - (addr % 256); if (page_len > len) page_len = len; Flash_PageWrite(addr, buf, page_len); addr += page_len; buf += page_len; len -= page_len; } }

这个框架虽然简单,但覆盖了 90% 的使用场景。实际项目中你可能还需要加缓存、加磨损均衡、加掉电保护,那就是另一个话题了。

4.2 常见问题速查表

调试 SPI NOR Flash 时遇到的问题,八成集中在下面这张表里:

现象可能原因排查方法
读 ID 全是 0xFFCS 没拉低、时钟没输出、电源没接示波器看 CS 和 CLK
读 ID 全是 0x00MISO 没接、引脚配置错检查引脚复用和上拉
读 ID 正确但读数据错空周期配置不对、时钟太快降时钟、调 DummyCycles
写不进去没发 Write Enable、区域被保护读状态寄存器看 WEL 和 BP
写进去读出来是 0xFF没擦除、擦除没等完成确认擦除后 BUSY 变 0
只有第一页写成功Write Enable 只发了一次每页写前都发 0x06
数据错位跨页写没处理检查页边界
四线模式读乱码DummyCycles 不对、线序错先用单线验证
内存映射读崩溃Flash Size 配置错、地址越界检查 CubeMX 配置
高速下随机出错信号完整性差、采样点不对降时钟、改 Sample Shifting

4.3 几个真实踩坑记录

坑一:CS 高电平时间太短导致命令丢失。有一次用 QSPI 连续发命令,两条命令之间 CS 只拉高了 1 个时钟周期,结果第二条命令偶尔不执行。后来把 CSHT 改成 4 个时钟周期,问题消失。原因是 Flash 内部需要时间识别 CS 上升沿,太快了它反应不过来。

坑二:DMA 传输长度不是 4 的倍数导致卡死。STM32 的 QSPI DMA 在某些系列上要求传输长度是 4 的倍数,我传了 4095 字节,结果 DMA 卡住不动。改成 4096 或者分两次传就好了。这个坑在参考手册里写得很隐蔽,不看勘误表根本发现不了。

坑三:内存映射模式下写操作没退出映射。我一开始想省事,在映射模式下直接发写命令,结果写命令被当成读数据处理,完全没效果。后来老老实实先HAL_QSPI_Abort退出映射,写完再重新配置映射模式。

坑四:擦除和写入之间没等 BUSY。擦除命令发完立刻发写命令,写命令被忽略,因为芯片还在忙。这个错误在低速下偶尔能成功,高速下必失败,非常具有迷惑性。

4.4 性能优化:从 1MB/s 到 30MB/s

如果你只是存点配置参数,单线 10MHz 足够了。但如果你要跑 XIP 或者传大文件,性能优化就很重要。我实测下来,优化路径是这样的:

第一步,从 0x03 换成 0x0B,时钟从 30MHz 提到 80MHz,读速度从 3MB/s 提到 8MB/s。

第二步,从单线换成四线 0x6B,时钟保持 80MHz,读速度提到 25MB/s。

第三步,开 DMA,CPU 占用从 100% 降到 5% 以下,读速度稳定在 28MB/s。

第四步,如果支持 DDR 模式,还能再翻倍,但 GD25Q80E 不支持 DDR,所以到 28MB/s 就是极限了。

写速度方面,页写 256 字节典型 0.7ms,算下来约 0.36MB/s,这是 Flash 物理特性决定的,优化空间不大。擦除 4KB 典型 45ms,约 0.09MB/s。所以写密集场景要考虑磨损均衡和缓存策略。

实操心得:如果你发现四线模式速度没比单线快多少,检查一下命令阶段和地址阶段是不是还在单线。真正的四线加速需要命令、地址、数据全四线(QPI 模式),但 QPI 模式下不能再用普通 SPI 命令,需要专门的进入和退出命令。这个取舍要看你的具体场景。

5. 进阶玩法与项目落地建议

5.1 OTA 升级中的 Flash 分区设计

外部 Flash 做 OTA 是很常见的需求,分区设计直接影响升级可靠性和回滚能力。我一般这样分:

区域大小用途
Bootloader64KB内部 Flash,负责搬运和校验
App A512KB外部 Flash,当前运行固件
App B512KB外部 Flash,新固件暂存
Config4KB配置参数
Log剩余空间运行日志

升级流程是:新固件写到 App B,校验 CRC 通过后写一个标志位到 Config,重启后 Bootloader 根据标志位决定搬运哪个 App。这样即使升级失败,也能回滚到旧版本。

这里的关键点是校验。我一般用 CRC32,对整个固件算一遍,和固件头里的 CRC 比对。不要用简单的累加和,碰撞概率太高。

5.2 磨损均衡:什么时候需要,怎么做

NOR Flash 每个扇区擦写寿命 10 万次。如果你每秒写一次配置参数,同一个扇区一年就写废了。所以写频繁的场景必须做磨损均衡。

最简单的做法是双扇区交替:准备两个 4KB 扇区,轮流写,写之前先擦另一个。这样寿命翻倍。复杂一点的是日志式存储:把扇区当成环形缓冲区,每次写新记录,写满了擦一次。这样寿命能提高几十倍。

但说实话,大部分嵌入式项目根本写不到 10 万次。我做过一个工业设备,每分钟写一次日志,一年也就 50 万次,但分布在 128 个扇区上,每个扇区才 4000 次,远没到寿命。所以磨损均衡要不要做,先算一下实际写入频率再说。

5.3 选型建议:GD25Q80E 之外还有什么选择

GD25Q80E 是 8Mbit 的入门款,如果你的项目需要更大容量或更高速度,可以考虑:

  • GD25Q16E / GD25Q32E:16Mbit / 32Mbit,引脚兼容,代码基本不用改。
  • GD25Q64E / GD25Q128E:64Mbit / 128Mbit,适合存大量资源。
  • W25Q 系列:华邦的,兼容性好,资料多。
  • IS25LP 系列:ISSI 的,低功耗做得好。

选型时重点看三个参数:容量、电压、最高时钟。容量按实际需求留 30% 余量;电压要和 MCU 系统匹配;最高时钟要满足你的速度要求。另外注意封装,SOP8 和 WSON8 的焊接难度不一样,手焊建议选 SOP8。

5.4 调试工具与技巧

调试 SPI NOR Flash,光靠 printf 效率太低。我常用的工具组合是:

  • 逻辑分析仪:抓 SPI 波形,看命令、地址、数据是否符合预期。Saleae 或者国产的 DSLogic 都行,采样率至少 100MHz。
  • 示波器:看信号完整性,特别是高速下的过冲和振铃。
  • Flash 编程器:单独把 Flash 拆下来读写,验证芯片本身是否正常。
  • STM32CubeProgrammer:支持外部 Flash 烧录,可以配合 QSPI 直接下载固件。

逻辑分析仪抓波形时,重点看几个地方:CS 拉低到第一个时钟的间隔、命令码是否正确、地址是否对齐、空周期数量是否匹配、数据阶段线宽是否正确。这几个点对了,基本就没问题。

实操心得:我习惯在驱动里加一个Flash_Dump函数,把 Flash 前 256 字节打印出来。每次调试新板子,先读 ID,再 Dump,确认基本读写正常,再跑业务逻辑。这个习惯帮我省了大量时间,因为很多问题在 Dump 阶段就能发现。

5.5 一个完整的读写测试用例

最后给一个可以直接用的测试用例,覆盖擦除、写入、读取、校验全流程:

#define TEST_ADDR 0x000000 #define TEST_SIZE 4096 void Flash_Test(void) { uint8_t write_buf[TEST_SIZE]; uint8_t read_buf[TEST_SIZE]; // 准备测试数据 for (int i = 0; i < TEST_SIZE; i++) { write_buf[i] = i & 0xFF; } // 擦除 printf("Erasing sector...\n"); Flash_SectorErase(TEST_ADDR); // 写入 printf("Writing data...\n"); Flash_Write(TEST_ADDR, write_buf, TEST_SIZE); // 读取 printf("Reading data...\n"); Flash_Read(TEST_ADDR, read_buf, TEST_SIZE); // 校验 if (memcmp(write_buf, read_buf, TEST_SIZE) == 0) { printf("Test PASS!\n"); } else { printf("Test FAIL!\n"); for (int i = 0; i < TEST_SIZE; i++) { if (write_buf[i] != read_buf[i]) { printf("Mismatch at %d: write=0x%02X read=0x%02X\n", i, write_buf[i], read_buf[i]); break; } } } }

这个用例虽然简单,但能覆盖 90% 的硬件和驱动问题。每次换新板子或新 Flash 型号,先跑这个用例,通过了再上业务代码。

我个人在实际操作中的体会是,SPI NOR Flash 这东西入门不难,但细节特别多,每一个细节都可能让你卡半天。最好的学习方式就是拿一块板子,从读 ID 开始,一步一步把命令都试一遍,用逻辑分析仪看波形,把数据手册上的时序图和实际波形对上。这个过程走一遍,以后遇到任何 SPI Flash 都能快速上手。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询