news 2026/9/20 11:12:34

SPI NOR Flash实战:GD25Q80E命令时序与STM32 QSPI配置详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SPI NOR Flash实战:GD25Q80E命令时序与STM32 QSPI配置详解

1. 为什么我要认真聊聊 SPI NOR Flash 这件事

搞嵌入式的人,迟早会跟 SPI NOR Flash 打交道。你可能只是想存个配置参数、放几张图片、做个字库,或者跑个 OTA 升级,结果一翻手册,几十条命令、一堆时序图、四种工作模式,直接劝退。更别提 STM32 的 QSPI 外设寄存器又多又绕,很多人调了几天连 JEDEC ID 都读不出来。

我自己第一次用 GD25Q80E 的时候,就踩了个经典坑:命令发出去,数据读回来全是 0xFF,查了半天以为是焊接问题,最后发现是 QSPI 的 dummy cycles 没配对。这种问题手册上写得明明白白,但你不踩一次根本不会去注意。

这篇内容就是把我这些年用 SPI NOR Flash 的经验完整梳理一遍。从 GD25Q80E 这颗具体芯片的命令时序讲起,到 STM32 QSPI 外设的配置逻辑,再到实际读写擦除的完整代码实现,最后把常见问题和排查思路整理成速查表。不管你是刚接触 Flash 的新手,还是已经用过但总觉得没吃透的老手,应该都能从里面找到有用的东西。

GD25Q80E 是兆易创新的一款 8Mbit(1MB)SPI NOR Flash,支持标准 SPI、Dual SPI、Quad SPI 三种模式,工作电压 2.7V~3.6V,100000 次擦写寿命,数据保存 20 年。这个规格在嵌入式项目里非常典型,学会了它,换到 W25Q 系列、IS25 系列基本是平移的事。

2. 先搞懂 SPI NOR Flash 的底层逻辑

2.1 NOR Flash 和 NAND Flash 到底差在哪

很多人知道 Flash 分 NOR 和 NAND,但说不清楚区别。我用一个生活化的类比来解释:NOR Flash 像图书馆的书架,每本书(每个字节)都有固定位置,你可以直接走到任何一本书前面拿下来看,不需要从头翻。NAND Flash 像一卷胶卷,你要看第 500 帧,得从头快进过去。

这个差异来自它们的内部结构。NOR Flash 的每个存储单元并联在 bit line 上,支持随机访问,所以可以直接按地址读取任意字节。NAND Flash 的存储单元串联在一起,必须按页(page)读取、按块(block)擦除。

实际项目中这意味着什么?NOR Flash 适合存代码、存配置、做 XIP(Execute In Place),因为读取速度快、随机访问方便。NAND Flash 适合存大容量数据,比如文件系统、音视频,因为单位成本低、容量大。你不可能用 NOR Flash 存几个 GB 的视频,也不应该用 NAND Flash 直接跑代码。

还有一个关键区别是可靠性。NOR Flash 的坏块率极低,基本出厂就是全好块,不需要做坏块管理。NAND Flash 出厂就可能有坏块,用久了还会新增坏块,必须做 ECC 校验和坏块管理。这也是为什么小容量存储场景大家更愿意用 NOR Flash,省心。

2.2 GD25Q80E 的存储组织结构

GD25Q80E 的 8Mbit 容量是这样组织的:

层级大小数量说明
页(Page)256 字节4096 页最小写入单位
扇区(Sector)4KB256 个扇区最小擦除单位(4KB擦除)
块(Block)64KB16 个块支持 32KB/64KB 块擦除
整片1MB1 片支持整片擦除

这里有个非常重要的概念:Flash 的写入只能把 1 变成 0,不能把 0 变成 1。擦除操作才是把整个扇区恢复成 0xFF(全 1)。所以你要修改一个字节,流程必须是:读出整个扇区到 RAM → 在 RAM 里修改 → 擦除扇区 → 把修改后的数据写回去。这个"读-改-写"流程是 Flash 操作的核心模式,后面讲代码的时候会反复用到。

注意:擦除操作的最小单位是扇区(4KB),不是字节。你不能只擦一个字节。这是新手最容易犯的错误之一。

2.3 SPI 通信模式:Mode 0/1/2/3 的选择

SPI 有四种通信模式,由 CPOL(时钟极性)和 CPHA(时钟相位)组合决定:

模式CPOLCPHA空闲时钟电平采样边沿
Mode 000上升沿
Mode 101下降沿
Mode 210下降沿
Mode 311上升沿

GD25Q80E 支持 Mode 0 和 Mode 3。实际用的时候选哪个?我一般用 Mode 0,因为大部分 MCU 的 SPI 外设默认就是 Mode 0,配置简单。但如果你发现读出来的数据不稳定,可以试试 Mode 3,有些板子的信号完整性在 Mode 3 下更好。

这里有个经验:SPI 时钟频率越高,信号完整性越重要。GD25Q80E 标准 SPI 模式下最高支持 133MHz(快速读命令),但实际能跑多快取决于你的 PCB 走线、上拉电阻、电源质量。我一般先在低速(比如 1MHz)下调通功能,再逐步提高频率测试稳定性。

3. GD25Q80E 命令时序深度拆解

3.1 命令集概览:哪些命令必须掌握

GD25Q80E 的命令有几十条,但常用的就那么十来条。我把它们分成几类:

识别类命令:

  • 0x9F:读 JEDEC ID,返回 3 字节(厂商 ID + 存储类型 + 容量)
  • 0x90:读制造商 ID 和设备 ID
  • 0xAB:释放掉电/读设备 ID

状态类命令:

  • 0x05:读状态寄存器 1(BUSY 位、WEL 位、BP 位)
  • 0x35:读状态寄存器 2(QE 位、SRP 位)
  • 0x01:写状态寄存器
  • 0x06:写使能(Write Enable)
  • 0x04:写禁止(Write Disable)

读取类命令:

  • 0x03:标准读(最高 50MHz)
  • 0x0B:快速读(最高 133MHz,需要 dummy cycle)
  • 0x3B:双线快速读
  • 0x6B:四线快速读
  • 0xEB:四线快速读(需要 6 个 dummy cycle)

编程类命令:

  • 0x02:页编程(最多 256 字节)
  • 0x32:四线页编程
  • 0x20:4KB 扇区擦除
  • 0x52:32KB 块擦除
  • 0xD8:64KB 块擦除
  • 0xC7/0x60:整片擦除

配置类命令:

  • 0x31:写状态寄存器 2(设置 QE 位)
  • 0x15:读状态寄存器 3

这些命令里,0x06(写使能)是最容易被忽略但最关键的。每次写操作(页编程、擦除、写状态寄存器)之前都必须先发 0x06,否则操作不会执行。我见过太多人调了半天写不进去,最后发现是忘了发写使能。

3.2 读 JEDEC ID:验证通信是否正常的第一步

读 JEDEC ID 是调试 SPI Flash 的第一步,因为它不需要擦除、不需要写使能,只要通信正常就能读到固定值。

时序是这样的:

  1. CS 拉低
  2. 发送 0x9F(1 字节)
  3. 读取 3 字节数据
  4. CS 拉高

GD25Q80E 的 JEDEC ID 是 0xC8 0x40 0x14:

  • 0xC8:厂商 ID(兆易创新)
  • 0x40:存储类型(SPI NOR)
  • 0x14:容量代码(8Mbit = 1MB)

如果你读出来是 0x00 0x00 0x00 或者 0xFF 0xFF 0xFF,说明通信有问题。0xFF 通常是 MISO 线没接好或者 Flash 没供电,0x00 通常是 MOSI 或 CLK 有问题。

实操心得:读 JEDEC ID 的时候,SPI 时钟先设到 1MHz 以下,确保信号稳定。等确认能读到了,再逐步提高频率。

3.3 读状态寄存器:判断忙闲和写使能状态

状态寄存器 1 的位定义:

Bit名称说明
7SRP0状态寄存器保护位
6BP2块保护位 2
5BP1块保护位 1
4BP0块保护位 0
3TB顶部/底部保护
2SEC扇区/块保护
1WEL写使能锁存
0BUSY忙标志

BUSY 位(Bit 0)是最常用的。擦除和编程操作需要时间,4KB 扇区擦除典型 45ms,页编程典型 0.7ms。在这段时间内,Flash 不响应新命令,你必须轮询 BUSY 位直到它变成 0。

WEL 位(Bit 1)用来确认写使能是否生效。发完 0x06 之后读状态寄存器,如果 WEL 是 1,说明写使能成功;如果是 0,说明写使能没生效,后面的写操作肯定失败。

读状态寄存器的时序:

  1. CS 拉低
  2. 发送 0x05
  3. 读取 1 字节(可以连续读,但一般读一次就够)
  4. CS 拉高

3.4 页编程时序:256 字节的边界陷阱

页编程命令 0x02 的时序:

  1. CS 拉低
  2. 发送 0x02
  3. 发送 3 字节地址(24 位)
  4. 发送最多 256 字节数据
  5. CS 拉高
  6. 等待 BUSY 位变 0

这里有个大坑:页编程不能跨页。如果你从地址 0x0000F0 开始写 32 字节,会写到 0x0000F0~0x00010F,但 0x000100 是下一页的起始地址,数据会回卷到 0x000000 开始覆盖。这不是 bug,是 Flash 的设计特性。

所以写数据的时候必须做页对齐处理。我的做法是:把要写的数据按页边界切分,每段不超过 256 字节且不跨页。

void flash_write(uint32_t addr, uint8_t *data, uint32_t len) { uint32_t page_remain; while (len > 0) { page_remain = 256 - (addr % 256); uint32_t write_len = (len < page_remain) ? len : page_remain; flash_page_program(addr, data, write_len); addr += write_len; data += write_len; len -= write_len; } }

3.5 扇区擦除时序:4KB 对齐是硬性要求

扇区擦除命令 0x20 的时序:

  1. 发送 0x06(写使能)
  2. CS 拉低
  3. 发送 0x20
  4. 发送 3 字节地址
  5. CS 拉高
  6. 等待 BUSY 位变 0(典型 45ms,最大 400ms)

地址必须是 4KB 对齐的,也就是说地址的低 12 位必须是 0。如果你传了个 0x00001000,那没问题;如果传 0x00001001,芯片会忽略低 12 位,实际擦除的还是 0x00001000 开始的 4KB。

注意:擦除时间比较长,轮询 BUSY 位的时候要加超时机制。我一般设 1 秒超时,超过就报错。正常不会超过 400ms,但如果电源不稳或者芯片有问题,可能会卡住。

4. STM32 QSPI 外设配置实战

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

普通 SPI 外设只能单线传输(MOSI 和 MISO 各一根线),而 QSPI 支持四线并行传输(IO0~IO3 四根数据线)。理论上 QSPI 的吞吐量是普通 SPI 的四倍。

但 QSPI 的优势不只是速度。STM32 的 QSPI 外设支持内存映射模式(Memory-Mapped Mode),可以把 Flash 映射到 MCU 的地址空间,像读内存一样直接读 Flash 数据。这对 XIP 执行代码或者直接读取字库、图片非常方便。

STM32 的 QSPI 外设还支持自动轮询、命令序列自定义等功能,可以灵活适配各种 Flash 芯片。不过灵活性也带来了配置复杂度,寄存器多、参数多,第一次配容易懵。

4.2 硬件连接:哪些引脚必须接

以 STM32F4/F7/H7 系列的 QSPI 为例,典型连接:

QSPI 信号STM32 引脚Flash 引脚说明
CLKPB2CLK时钟
CSPB6CS#片选
IO0PC9DI/IO0数据线 0
IO1PC10DO/IO1数据线 1
IO2PE2WP#/IO2数据线 2 / 写保护
IO3PD13HOLD#/IO3数据线 3 / 保持

注意 IO2 和 IO3 在标准 SPI 模式下是 WP# 和 HOLD# 功能,在 Quad 模式下才作为数据线使用。如果你只用标准 SPI 模式,这两个引脚可以不接,但建议接上并上拉到 VCC,防止误触发写保护。

实操心得:CLK 和 CS 线上建议串 22Ω~33Ω 的电阻,可以改善信号完整性。特别是 CLK 频率超过 50MHz 的时候,这个电阻能明显减少过冲和振铃。

4.3 QSPI 初始化:从时钟到参数

STM32 QSPI 的初始化分几步:

第一步:配置 GPIO 复用功能

GPIO_InitTypeDef gpio = {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOC_CLK_ENABLE(); __HAL_RCC_GPIOD_CLK_ENABLE(); __HAL_RCC_GPIOE_CLK_ENABLE(); // CLK gpio.Pin = GPIO_PIN_2; gpio.Mode = GPIO_MODE_AF_PP; gpio.Pull = GPIO_NOPULL; gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH; gpio.Alternate = GPIO_AF9_QUADSPI; HAL_GPIO_Init(GPIOB, &gpio); // CS gpio.Pin = GPIO_PIN_6; HAL_GPIO_Init(GPIOB, &gpio); // IO0, IO1 gpio.Pin = GPIO_PIN_9 | GPIO_PIN_10; HAL_GPIO_Init(GPIOC, &gpio); // IO3 gpio.Pin = GPIO_PIN_13; HAL_GPIO_Init(GPIOD, &gpio); // IO2 gpio.Pin = GPIO_PIN_2; HAL_GPIO_Init(GPIOE, &gpio);

第二步:配置 QSPI 外设参数

QSPI_HandleTypeDef hqspi = {0}; hqspi.Instance = QUADSPI; hqspi.Init.ClockPrescaler = 1; // 时钟分频,1 表示 2 分频 hqspi.Init.FifoThreshold = 4; // FIFO 阈值 hqspi.Init.SampleShifting = QSPI_SAMPLE_SHIFTING_HALFCYCLE; hqspi.Init.FlashSize = 22; // 2^22 = 4MB 地址空间 hqspi.Init.ChipSelectHighTime = QSPI_CS_HIGH_TIME_4_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);

这里几个参数需要解释:

ClockPrescaler:QSPI 时钟 = AHB 时钟 / (Prescaler + 1)。如果 AHB 是 200MHz,Prescaler 设 1,QSPI 时钟就是 100MHz。GD25Q80E 在 Quad 模式下最高支持 133MHz,所以 100MHz 是安全的。

SampleShifting:采样偏移。QSPI_SAMPLE_SHIFTING_HALFCYCLE 表示在时钟边沿后半个周期采样,可以改善高速下的信号完整性。如果读数据不稳定,可以试试改成 QSPI_SAMPLE_SHIFTING_NONE。

FlashSize:Flash 容量对应的地址位数。8Mbit = 1MB = 2^20,所以应该设 20。但很多人设 22 也没问题,因为地址空间大一点不影响,只要不超过 32 位就行。

ChipSelectHighTime:CS 拉高后的保持时间。GD25Q80E 要求最小 50ns,QSPI_CS_HIGH_TIME_4_CYCLE 在 100MHz 下是 40ns,稍微不够,建议设 QSPI_CS_HIGH_TIME_6_CYCLE 或更高。

4.4 命令序列配置:QSPI 的核心机制

STM32 QSPI 最核心的概念是"命令序列"。每条操作(读 ID、读数据、写数据、擦除)都对应一个命令序列,包含以下阶段:

阶段说明可配置项
Instruction发送命令码模式(无/单线/双线/四线)、值
Address发送地址模式、大小(8/16/24/32位)、值
Alternate Bytes发送交替字节模式、大小、值
Dummy Cycles空周期数量
Data传输数据模式、长度

每个阶段的"模式"可以是:

  • QSPI_CCR_IMODE_NONE:跳过该阶段
  • QSPI_CCR_IMODE_1_LINE:单线
  • QSPI_CCR_IMODE_2_LINES:双线
  • QSPI_CCR_IMODE_4_LINES:四线

以读 JEDEC ID 为例,命令序列配置:

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; cmd.DdrMode = QSPI_DDR_MODE_DISABLE; cmd.SIOOMode = QSPI_SIOO_INST_EVERY_CMD; uint8_t id[3]; HAL_QSPI_Command(&hqspi, &cmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Receive(&hqspi, id, HAL_QPSI_TIMEOUT_DEFAULT_VALUE);

读数据(快速读,0x0B)的命令序列:

cmd.Instruction = 0x0B; 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 = 8; // 0x0B 需要 8 个 dummy cycles cmd.DataMode = QSPI_DATA_1_LINE; cmd.NbData = len;

Quad 读(0xEB)的命令序列:

cmd.Instruction = 0xEB; cmd.InstructionMode = QSPI_INSTRUCTION_1_LINE; cmd.Address = addr; cmd.AddressMode = QSPI_ADDRESS_4_LINES; // 地址四线传输 cmd.AddressSize = QSPI_ADDRESS_24_BITS; cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_4_LINES; cmd.AlternateBytes = 0x00; // 模式字节 cmd.AlternateBytesSize = QSPI_ALTERNATE_BYTES_8_BITS; cmd.DummyCycles = 6; // 0xEB 需要 6 个 dummy cycles cmd.DataMode = QSPI_DATA_4_LINES; // 数据四线传输 cmd.NbData = len;

注意:0xEB 命令的 dummy cycles 是 6 个,不是 8 个。这个参数如果设错,读出来的数据会整体偏移,表现为第一个字节丢失或者多出垃圾数据。我踩过这个坑,查了一整天才发现是 dummy cycles 的问题。

5. 完整读写擦除代码实现

5.1 写使能与等待忙闲

void qspi_write_enable(void) { QSPI_CommandTypeDef cmd = {0}; cmd.Instruction = 0x06; cmd.InstructionMode = QSPI_INSTRUCTION_1_LINE; cmd.AddressMode = QSPI_ADDRESS_NONE; cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE; cmd.DummyCycles = 0; cmd.DataMode = QSPI_DATA_NONE; cmd.DdrMode = QSPI_DDR_MODE_DISABLE; cmd.SIOOMode = QSPI_SIOO_INST_EVERY_CMD; HAL_QSPI_Command(&hqspi, &cmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); } uint8_t qspi_read_status(void) { QSPI_CommandTypeDef cmd = {0}; cmd.Instruction = 0x05; 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 = 1; cmd.DdrMode = QSPI_DDR_MODE_DISABLE; cmd.SIOOMode = QSPI_SIOO_INST_EVERY_CMD; uint8_t status = 0; HAL_QSPI_Command(&hqspi, &cmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Receive(&hqspi, &status, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); return status; } void qspi_wait_busy(void) { uint32_t timeout = 1000000; while ((qspi_read_status() & 0x01) && timeout--); if (timeout == 0) { // 超时处理 } }

5.2 扇区擦除实现

void qspi_sector_erase(uint32_t addr) { qspi_write_enable(); qspi_wait_busy(); QSPI_CommandTypeDef cmd = {0}; cmd.Instruction = 0x20; 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_NONE; cmd.DdrMode = QSPI_DDR_MODE_DISABLE; cmd.SIOOMode = QSPI_SIOO_INST_EVERY_CMD; HAL_QSPI_Command(&hqspi, &cmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); qspi_wait_busy(); }

5.3 页编程实现

void qspi_page_program(uint32_t addr, uint8_t *data, uint16_t len) { if (len > 256) len = 256; qspi_write_enable(); qspi_wait_busy(); 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; cmd.DdrMode = QSPI_DDR_MODE_DISABLE; cmd.SIOOMode = QSPI_SIOO_INST_EVERY_CMD; HAL_QSPI_Command(&hqspi, &cmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Transmit(&hqspi, data, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); qspi_wait_busy(); }

5.4 数据读取实现

void qspi_read(uint32_t addr, uint8_t *buf, uint32_t len) { QSPI_CommandTypeDef cmd = {0}; cmd.Instruction = 0x0B; 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 = 8; cmd.DataMode = QSPI_DATA_1_LINE; cmd.NbData = len; cmd.DdrMode = QSPI_DDR_MODE_DISABLE; cmd.SIOOMode = QSPI_SIOO_INST_EVERY_CMD; HAL_QSPI_Command(&hqspi, &cmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Receive(&hqspi, buf, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); }

5.5 使能 Quad 模式

GD25Q80E 默认是标准 SPI 模式,要用 Quad 模式必须先设置状态寄存器 2 的 QE 位(Bit 1)。

void qspi_enable_quad_mode(void) { qspi_write_enable(); qspi_wait_busy(); // 读状态寄存器 2 QSPI_CommandTypeDef cmd = {0}; cmd.Instruction = 0x35; 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 = 1; cmd.DdrMode = QSPI_DDR_MODE_DISABLE; cmd.SIOOMode = QSPI_SIOO_INST_EVERY_CMD; uint8_t sr2 = 0; HAL_QSPI_Command(&hqspi, &cmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Receive(&hqspi, &sr2, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); // 设置 QE 位 sr2 |= 0x02; // 写状态寄存器 2 qspi_write_enable(); cmd.Instruction = 0x31; cmd.DataMode = QSPI_DATA_1_LINE; cmd.NbData = 1; HAL_QSPI_Command(&hqspi, &cmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Transmit(&hqspi, &sr2, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); qspi_wait_busy(); }

6. 常见问题与排查技巧实录

6.1 读出来全是 0xFF 或 0x00

这是最常见的问题,排查思路如下:

现象可能原因排查方法
全 0xFFMISO 未连接、Flash 未供电、CS 未拉低万用表测电压、示波器看波形
全 0x00MOSI 或 CLK 未连接、GPIO 复用配置错误检查 GPIO 配置、示波器看 CLK
部分正确时序问题、dummy cycles 错误降低时钟频率、检查 dummy cycles
随机数据信号完整性问题、电源噪声加去耦电容、串匹配电阻

我的经验是:先用示波器看 CLK、CS、MOSI、MISO 四根线的波形。如果 CLK 没有输出,说明 QSPI 外设没使能或者 GPIO 复用没配对。如果 CS 一直是高,说明片选逻辑有问题。如果 MOSI 有数据但 MISO 没反应,说明 Flash 没工作或者 MISO 线断了。

6.2 写不进去数据

写操作失败通常有这几个原因:

忘记发写使能(0x06):这是最常见的。每次页编程或擦除之前都必须发 0x06,而且发完之后要确认 WEL 位是 1。

目标地址没擦除:Flash 只能把 1 写成 0,如果目标地址之前写过数据(有 0),直接写新数据会失败。必须先擦除。

状态寄存器保护位被设置:BP0~BP2 位如果被设置,对应区域的写操作会被禁止。读状态寄存器确认这些位是 0。

写使能后发了其他命令:写使能是一个锁存状态,发完 0x06 之后如果发了其他命令(除了写操作),WEL 位会被清除。所以 0x06 之后要紧接着发写命令。

6.3 Quad 模式读数据错位

用 0xEB 命令读数据时,如果 dummy cycles 设错,数据会整体偏移。GD25Q80E 的 0xEB 命令需要 6 个 dummy cycles,不是 8 个。如果你设成 8 个,读出来的数据会多两个字节的垃圾,后面的数据全部后移。

排查方法:读一个已知内容(比如 JEDEC ID 或者之前写入的固定数据),对比读出来的内容,看偏移量是多少。如果偏移了 2 个字节,说明 dummy cycles 多了 2 个。

6.4 擦除时间过长或超时

4KB 扇区擦除典型 45ms,最大 400ms。如果你发现擦除经常超时,可能是:

  • 电源电压偏低,导致擦除效率下降
  • Flash 芯片老化,擦写次数接近上限
  • 轮询 BUSY 位的代码有问题,比如读状态寄存器时没正确拉 CS

实操心得:擦除和编程操作期间,尽量不要频繁读状态寄存器。我一般每 1ms 读一次,既能及时检测到完成,又不会占用太多总线时间。

6.5 QSPI 内存映射模式读数据异常

内存映射模式(Memory-Mapped Mode)配置好之后,可以直接用指针读 Flash 数据。但有几个坑:

地址对齐:映射后的基地址是 0x90000000(对于 QSPI Bank 1),但 Flash 内部的地址偏移要加上去。比如读 Flash 地址 0x1000 的数据,指针应该是 0x90000000 + 0x1000。

Cache 问题:如果 MCU 有 Cache,映射区域的读数据可能被缓存。修改 Flash 内容后,需要失效对应的 Cache 行才能读到新数据。

Dummy cycles 配置:内存映射模式也需要配置 dummy cycles,而且这个配置在进入映射模式之前就要设好,进入之后不能改。

7. 性能优化与进阶技巧

7.1 提高读取速度的几种手段

用 Quad 模式:0xEB 命令比 0x0B 快四倍(数据线从 1 根变 4 根)。但需要先使能 QE 位。

提高时钟频率:GD25Q80E 在 Quad 模式下最高 133MHz。STM32 QSPI 的时钟源是 AHB,通过分频得到。如果 AHB 是 200MHz,分频系数设 1 得到 100MHz,设 0 得到 200MHz(可能超过 Flash 上限)。

用内存映射模式:省去每次发命令的开销,直接读地址。适合频繁读取的场景。

开启 QSPI 的 DDR 模式:双沿采样,理论速度翻倍。但 GD25Q80E 不支持 DDR,这个功能用不上。

7.2 擦写寿命管理与磨损均衡

GD25Q80E 的擦写寿命是 100000 次。如果你频繁写同一个扇区,很快就会坏。简单的磨损均衡思路:

  • 把配置数据分成两份,交替写入两个扇区
  • 每次写入时记录一个计数器,写到一定次数后切换到另一个扇区
  • 读取时比较两个扇区的计数器,取最新的那份

这个方案实现简单,能把寿命翻倍。如果数据量不大,还可以用更多扇区做轮询。

7.3 OTA 升级中的 Flash 分区设计

做 OTA 升级时,Flash 通常分成几个区域:

区域大小说明
Bootloader32KB引导程序,负责跳转和升级
App A200KB应用程序 A
App B200KB应用程序 B(备份/升级目标)
Config4KB配置参数
Flag4KB升级标志、版本号

升级流程:Bootloader 检查 Flag 区,如果有升级标志,把新固件从 App B 拷贝到 App A,清除标志,跳转到 App A。如果没有升级标志,直接跳转到 App A。

这个设计的关键是 Flag 区的读写要可靠。我一般用两个扇区做冗余,每个扇区存两份数据加 CRC 校验,确保掉电不会丢标志。

8. 我踩过的那些坑和最后的小技巧

调 SPI NOR Flash 这些年,踩过的坑真不少。有一次用 GD25Q80E 做字库存储,读出来的汉字全是乱码,查了两天发现是地址算错了——字库文件的偏移地址没加上 Flash 的基地址。还有一次做 OTA,升级完重启后程序跑飞,最后发现是 Bootloader 跳转前没关中断,中断向量表还指向旧程序。

几个我觉得比较实用的小技巧:

第一,调试初期一定要用逻辑分析仪或者示波器看波形。光靠读代码很难发现时序问题,看到实际波形就一目了然了。

第二,写操作之前先读一遍目标区域。如果读出来不是 0xFF,说明没擦除,直接写会失败。这个检查能省很多调试时间。

第三,状态寄存器的 BUSY 位轮询要加超时。我见过 Flash 因为电源问题卡在 BUSY 状态不出来的情况,没有超时机制的话程序就死在那里了。

第四,Quad 模式使能后最好读一次状态寄存器确认。有些批次的芯片 QE 位写入后需要额外的时间才能生效,读一次确认更稳妥。

第五,PCB 布局时 Flash 尽量靠近 MCU。SPI 时钟线走线过长会导致信号质量下降,特别是频率超过 50MHz 的时候。如果实在要拉远,记得加匹配电阻和去耦电容。

GD25Q80E 这颗芯片我用过很多项目,从简单的参数存储到复杂的 OTA 升级,稳定性一直不错。关键是命令时序要吃透,QSPI 配置要仔细,剩下的就是多调多试。希望这篇内容能帮你少走一些弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 11:07:12

Windows Server 2019 U盘裸装实战:从镜像校验到分区避坑全指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 11:04:19

芯片互连协议深度对比:CHI七态与PBR路由在Scale-up场景中的工程实践

做芯片互连的朋友应该都有一种体验&#xff1a;平时聊协议头头是道&#xff0c;一旦落到 RTL 里写状态机&#xff0c;或者在上板后抓 Deadlock&#xff0c;才发现那些“资历很老”的互连协议每个都有自己的脾气。尤其 Scale-up 场景&#xff0c;核数翻倍、内存距离拉长、缓存一…

作者头像 李华