目录
GD25Q64E介绍
1、基本介绍
GD25Q64E是由兆易创新(GigaDevice)推出的一款64M-bit(8MB)容量的串行NOR Flash存储器。该芯片采用SPI接口,广泛应用于嵌入式系统、消费电子、通信设备等需要非易失性存储的场景。
GD官方资料获取:https://www.gigadevice.com.cn/product/flash/spi-nor-flash/gd25q64e
2、主要特性
-
容量:64M-bit(8MB)
-
接口类型:支持标准SPI(Serial Peripheral Interface),兼容双/四线(Dual/Quad SPI)模式
-
工作电压:2.7V ~ 3.6V
-
时钟频率:最高支持133MHz
-
页面大小:256字节
-
扇区大小:4KB
-
块大小:32KB / 64KB
-
擦写寿命:每个扇区/块10万次擦写
-
数据保持时间:20年以上
-
封装形式:SOP8、USON8、WSON8等多种封装
-
工作温度范围:-40°C ~ +85°C
典型的应用:
-
程序代码存储
-
FPGA 配置文件保存
-
数据记录与参数存储
-
固件升级
-
物联网设备
3、引脚定义(SOP8)
从数据手册中我们可以看到相关的引脚设置

引脚编号 | 名称 | 功能说明 |
---|---|---|
1 | CS# | 片选 |
2 | SO(IO1) | 数据输出(数据输入输出引脚1) |
3 | WP#(IO2) | 写保护(数据输入输出引脚2) |
4 | VSS | 地 |
5 | SI(IO0) | 数据输入(数据输入输出引脚0) |
6 | CLK | 时钟输入 |
7 | HOLD#(IO3) | 暂停信号(数据输入输出引脚3) |
8 | VCC | 电源 |
4、功能说明
-
快速读写:支持高速 SPI/QSPI 读写操作
-
页编程:每次最多编程 256 字节
-
灵活擦除:支持扇区(4KB)、块(32KB/64KB)、整片擦除
-
掉电数据保护:关电后数据不丢失
-
硬件/软件写保护:支持部分区域写保护,防止误操作
-
深度掉电/待机模式:降低功耗
5、QuadSPI通讯简述
-
读操作:主控拉低 CS#,发送四线快速读指令(如0x6B/0xEB等)和地址,GD25Q64E 在四线(IO0~IO3)模式下返回数据,数据吞吐量更高。
-
写操作:先发送写使能(WREN)指令,随后发送四线页编程指令(如0x32),地址和数据均通过四线(IO0~IO3)传输,提高写入速度。
-
擦除操作:同样先发送写使能指令,再发送四线擦除指令(如4KB扇区擦除0x20),地址用四线传输,可实现更快的数据擦除。
-
模式切换:设备上电默认SPI模式,需通过设置状态寄存器切换到QSPI(四线)模式。
QSPI(Quad SPI)模式下,数据线数量从1线扩展到4线(IO0、IO1、IO2、IO3),极大提升数据传输速度,适用于高速固件加载和大数据量存储场景。
CubeMX配置
1、新建项目
我们直接利用CubeMX软件初始化一个工程

搜索 STM32H750VBT6
这个芯片,双击之后进入具体配置界面:


2、基础配置
使用外部高速/低速晶振:

我们先关闭MPU,这样的话我们就可以专注于QSPI-Flash的BSP代码编写。

打开串口,我们可以用这个串口查看调试信息。
因为我们原理图上的USART1的接口是PA9和PA10,所以我们修改相关的引脚接口

SWD调试接口配置:

3、QuadSPI接口配置
首先查看原理图的引脚:

我们可以很清晰的看到相关的引脚分配情况,我们直接开始配置引脚和我们的硬件原理图保持一致:
特别注意 :这里我们的引脚输出速度一定要设定为最高!!!

接下来进行QuadSPI的常规

-
Clock Prescaler :
1
240MHz/(1+1)=120MHz
(内部会自动+1)QSPI时钟最快,看GD25Q64E的数据手册,最大的时钟速度为133MHz,我们不超过即可。
-
Fifo Threshold :
1
- FIFO的阈值范围,直接默认为1即可。
-
Sample Shifting :
Sample Shifting Half Cycle
- 设定为半个时钟周期之后才开始数据采集,相当于加了一些延迟,读取数据的时候更有容错。
-
Flash Size :
23
(GD25Q64是8MByte,23代表2\^23\=8MByte)- 特别说明 :内部计算的时候计算这个Flash Size是会自动+1的,常规填写22即可,但涉及到内存映射的时候还是按照23填写(使其空间扩大一倍),否则使用内存映射的时候最后的空间映射会出问题!
-
Chip Select High Time :
5 Cycles
- CS的片选使用的时候,高电平时间至少保持
5 Cycles
- CS的片选使用的时候,高电平时间至少保持
-
Clock Mode :
LOW
- 片选的信号空闲时,时钟信号设定为低电平。
-
Flash ID :
Flash ID 1
-
Dual Flash :
Disabled
4、配置时钟
-
选择
HSE
-
选择
Enable CSS
-
填写
480
-
回车,直接让
CubeMX
软件自己计算配置即可!

我们可以看到QuadSPI的时钟是240MHz:

之前配置 QuadSPI 的时候 Clock Prescaler 设定为了 1
,因为内部分频会自动+1
,所以 QuadSPI 的时钟速度就是:
-
240MHz / (Clock Prescaler + 1)
-
= 240MHz / (1 + 1)
-
= 120MHz
-
5、生成工程
-
工程名字设定为:
QuadSPI-Flash-Poll_Project
。 -
Project Location
设定工程的位置。 -
Toolchain/IDE
设定生成的工程是Keil-MDK
的。


点击 GENERATE CODE
生成代码:

我们就能看到在相关的位置生成了工程目录:

KeilMDK工程设置
1、创建BSP
在工程目录中创建 BSP
文件夹,在 BSP
文件夹中再次创建一个 QuadSPI-Flash
的文件夹。

QuadSPI-Flash
的文件夹中创建 bsp_qspi_gd25q64e.c
和 bsp_qspi_gd25q64e.h

打开项目,将我们的创建的 .h
和 .c
文件添加到工程中

2、基础设置
使用KeilMDK
的微库,使用AC6
编译器:

优化等级降至 0
:

编写BSP驱动
0、QuadSPI接口结构体说明
我们在开始编写BSP之前,首先来了解下在STM32H7中要用的一些结构体。
1)QSPI_HandleTypeDef
QSPI_HandleTypeDef
是 STM32 HAL 库用于管理 QuadSPI 外设和数据传输状态的核心结构体。它包含了 QSPI 外设实例、初始化参数、收发缓冲区指针、DMA句柄、锁定状态、错误码以及超时等信息。常用于 BSP 驱动中的 QSPI 操作。
这个结构体一般定义在工程中:
\Libraries\STM32H7xx_HAL_Driver\Inc\stm32h7xx_hal_qspi.h

主要字段说明如下:
-
Instance :QSPI 寄存器基地址,通常为
QUADSPI
外设的指针。 -
Init :QSPI 初始化参数结构体(
QSPI_InitTypeDef
),包括数据宽度、时钟分频等设置。 -
pTxBuffPtr/pRxBuffPtr:发送/接收缓冲区指针,指向待发送/接收的数据。
-
TxXferSize/RxXferSize:发送/接收的数据大小(字节数)。
-
TxXferCount/RxXferCount:剩余待发送/接收的数据计数。
-
hmdma:MDMA(内存到外设 DMA)句柄指针,用于加速数据传输。
-
Lock:用于实现多线程/中断安全的数据访问。
-
State:QSPI 当前通信状态(空闲、忙、错误等),用于流程控制。
-
ErrorCode:错误码,标识 QSPI 通信过程中出现的错误类型。
-
Timeout:QSPI 内存访问的超时时间设置。
-
回调函数(如果使能了注册回调) :包括错误/中止/完成/超时等事件的回调处理函数,便于异步通信和事件响应。
使用方式:
- 在 BSP 驱动中,通常会定义一个全局
QSPI_HandleTypeDef
变量(如QSPI_HandleTypeDef hqspi
),在初始化、读写、擦除等操作时传递给 HAL QSPI API(如HAL_QSPI_Command
,HAL_QSPI_Transmit
,HAL_QSPI_Receive
等),以实现控制和数据收发。
2)QSPI_CommandTypeDef
QSPI_CommandTypeDef
是 STM32 HAL 库中用于配置 QuadSPI 命令的结构体。它定义了通过 QSPI 总线与外部 Flash通信时所需的各种参数,包括指令、地址、模式、数据长度等。
这个结构体一般定义在工程中:
\Libraries\STM32H7xx_HAL_Driver\Inc\stm32h7xx_hal_qspi.h

主要字段说明如下:
-
Instruction:要发送到 Flash 的操作指令(如读/写/擦除等),通常为 8 位。
-
Address:目标地址,通常为 24 位或 32 位,需要根据 GD25Q64E 的寻址空间设置。
-
AlternateBytes:备用字节,可用于某些特殊命令。
-
AddressSize :地址大小,可以为 8/16/24/32 位,通常小于
128MBit
的Flash选用 24 位,而大于则选用32位。 -
AlternateBytesSize:备用字节大小,通常不用时设为无。
-
DummyCycles:虚拟周期数,QSPI 快速读时常需设定,需要根据实际调整。
-
InstructionMode:指令阶段使用的线数(单线、双线、四线),QSPI 操作通常选用四线。
-
AddressMode:地址阶段线数,支持单/双/四线。
-
AlternateByteMode:备用字节阶段线数。
-
DataMode:数据阶段线数,QSPI 快速读/写一般用四线。
-
NbData:数据传输的字节数,读写操作时设置为实际长度。
-
DdrMode:是否启用 DDR(双倍速)。
-
DdrHoldHalfCycle:DDR 半周期保持。
-
SIOOMode:单次指令发送模式。
一般应用场景:
- 使用
HAL_QSPI_Command
、HAL_QSPI_Receive
、HAL_QSPI_Transmit
和HAL_QSPI_AutoPolling
时进行必要的命令模式配置。
3)QSPI_AutoPollingTypeDef
QSPI_AutoPollingTypeDef
是 STM32 HAL 库中用于配置 QSPI 自动轮询模式的结构体。自动轮询模式常用于等待外部 Flash完成擦除或编程操作,通过自动读取状态寄存器判断操作是否结束,提高效率和响应速度。
这个结构体一般定义在工程中:
\Libraries\STM32H7xx_HAL_Driver\Inc\stm32h7xx_hal_qspi.h

主要字段说明如下:
-
Match:目标匹配值。用于与读取到的状态寄存器(经过掩码 Mask)进行比较,判断是否达到指定状态(如空闲)。
-
Mask:掩码值。对 Flash 返回的状态字节进行掩码处理,只比较关心的位(如 GD25Q64E 的"忙"位)。
-
Interval:自动轮询间隔时钟周期数。决定每次状态寄存器轮询之间的等待时间,单位为 QSPI 时钟周期。
-
StatusBytesSize:状态字节数。设置每次读回的状态寄存器长度。
-
MatchMode:匹配方式。指定比较的判定方法,如全部匹配或部分匹配。
-
AutomaticStop:自动停止。轮询到匹配状态后自动停止。
应用场景:
- 在 BSP 驱动中,自动轮询模式通常用于等待 Flash 完成写入或擦除操作。比如,先发起擦除命令,再配置
QSPI_AutoPollingTypeDef
,调用HAL_QSPI_AutoPolling()
轮询状态寄存器的"忙"位(BUSY),直到 Flash 空闲后再进行后续操作。
1、写使能
在 GD25Q64E 进行写入或擦除操作前,必须先执行"写使能"操作。写使能的流程如下:
-
发送写使能指令
0x06
向 Flash 发送0x06
指令(WREN),使能后续的写/擦除操作。 -
轮询状态寄存器 可选步骤:轮询状态寄存器1的 WEL(写使能锁存)位(位1)是否被置位,确保写使能成功。
这是最终要的一个操作,奠定了之后的一切写入和擦除操作。


我们根据这个Flash数据手册中 Read Status Register (RDSR)
章节,可知 0x05
0x35
0x15
每个命令可以读取一个字节(8Bits)的数据,所以24bits的状态信息需要3个命令才能完全读取:
命令 | 对应的Bit位数 |
---|---|
0x05 | 第0位到第7位 (S0 ~ S7) |
0x35 | 第8位到第15位 (S8 ~ S15) |
0x15 | 第16位到第24位(S16 ~ S23) |
我们需要的是 WEL(写使能锁存)
这个状态,所以我们查询 STATUS REGISER
表格可以看到 WEL
状态位于 S1
也就是位 1

所以我们只需要读状态寄存器的第一个字节即可,用 0x05
这个命令。
首先在 BSP\QuadSPI-Flash\bsp_qspi_gd25q64e.h
中定义相关的寄存器:
cpp
#define QSPI_CMD_WRITE_ENABLE 0x06 /* 写使能 */
#define GD25Q64E_CMD_READ_STATUS_1 0x05 /* 读取状态寄存器第0位到第7位(S0 ~ S7) */
#define GD25Q64E_CMD_READ_STATUS_2 0x35 /* 读取状态寄存器第8位到第15位(S8 ~ S15) */
#define GD25Q64E_CMD_READ_STATUS_3 0x15 /* 读取状态寄存器第16位到第23位(S16 ~ S23) */
在 BSP\QuadSPI-Flash\bsp_qspi_gd25q64e.h
中开始编写函数:
cpp
/******************************************************************
* 函 数 名 称:QSPI_PollingWEL
* 函 数 说 明:等待写使能位(WEL)被置位
* 函 数 形 参:无
* 函 数 返 回:0:成功 1:失败
* 备 注:无
******************************************************************/
static int QSPI_PollingWEL(void)
{
QSPI_CommandTypeDef sCommand = {0};
QSPI_AutoPollingTypeDef sConfig = {0};
sCommand.InstructionMode = QSPI_INSTRUCTION_1_LINE; /* 一线指令模式 */
sCommand.Instruction = GD25Q64E_CMD_READ_STATUS_1; /* 读取状态寄存器1命令 */
sCommand.AddressMode = QSPI_ADDRESS_NONE; /* 无地址模式 */
sCommand.DataMode = QSPI_DATA_1_LINE; /* 一线数据模式 */
sConfig.Mask = 0x02; /* 只关心WEL位 */
sConfig.Match = 0x02; /* WEL位为1 */
sConfig.MatchMode = QSPI_MATCH_MODE_AND; /* AND匹配模式 */
sConfig.StatusBytesSize = 1; /* 状态寄存器1有1个字节 */
sConfig.Interval = 0x10; /* 轮询间隔为16个QSPI时钟周期 */
sConfig.AutomaticStop = QSPI_AUTOMATIC_STOP_ENABLE; /* 自动停止轮询 */
if (HAL_QSPI_AutoPolling(&hqspi, &sCommand, &sConfig, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
return 1;
return 0;
}
-
功能: 用于自动轮询 GD25Q64E 的状态寄存器1(SR1,指令 0x05),等待 WEL(Write Enable Latch,位1)被置位,确保写使能命令已经生效。
-
实现流程
-
构造 QSPI 命令结构体,设置为一线指令模式,指令为读取状态寄存器1(0x05),无地址,一线数据模式。
-
构造自动轮询配置结构体,只关心 WEL 位(Mask=0x02),匹配条件为 WEL=1(Match=0x02),其它参数如轮询间隔、匹配模式等根据实际设置。
-
调用
HAL_QSPI_AutoPolling
自动轮询芯片状态,直到 WEL 位被置位或超时。 -
成功返回 0,失败返回 1。
-
-
典型用法: 作为写使能命令后的状态确认步骤,确保芯片状态正确,避免后续写/擦除失败。
cpp
/******************************************************************
* 函 数 名 称:QSPI_WriteEnable
* 函 数 说 明:发送写使能命令,并等待WEL位被置位
* 函 数 形 参:无
* 函 数 返 回:0:成功 1:失败
* 备 注:无
******************************************************************/
static int QSPI_WriteEnable(void)
{
QSPI_CommandTypeDef sCommand = {0};
sCommand.InstructionMode = QSPI_INSTRUCTION_1_LINE; /* 一线指令模式 */
sCommand.Instruction = GD25Q64E_CMD_WRITE_ENABLE; /* 写使能命令 */
sCommand.AddressMode = QSPI_ADDRESS_NONE; /* 无地址模式 */
sCommand.DataMode = QSPI_DATA_NONE; /* 无数据模式 */
/* 发送写使能命令 */
if (HAL_QSPI_Command(hqspi, &sCommand, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
return 1;
/* 等待WEL位被置位 */
return QSPI_Polling_Wait_WEL();
}
-
函数说明:用于向 GD25Q64E Flash 芯片发送写使能命令(WREN,0x06),并等待写使能锁存位(WEL)被置位,以允许后续的写入或擦除操作。
-
实现流程
-
构造 QSPI 命令结构体,设置为一线指令模式,指令为写使能命令(0x06),无地址、无数据模式。
-
调用
HAL_QSPI_Command
发送写使能命令。 -
命令发送成功后,调用
QSPI_PollingWEL
轮询状态寄存器,等待 WEL 位被置位(即写使能生效)。 -
若整个过程成功,返回 0,否则返回 1。
-
-
典型用法: 写入或擦除 Flash 前,需先调用此函数以保证芯片处于可写状态。
2、使能四线模式
Flash芯片上电默认是普通SPI模式,也没有使能四线读写,所以我们需要设置下Flash的模式。
从Flash的数据手册中可知:若需使用QSPI四线读写,须先使能QE(Quad Enable)位。

从状态寄存器中可以得知:QE的使能位在 24 个Bit中的 第 9 位。
而且,在Note中看到的是 Non-volatile writable
掉电不丢失,所以我们可以将流程这样设计:
-
读取 SR2,检查 QE 是否已置位。
-
如果未置位,先写使能(WREN)。
-
写 SR2,把 QE 位置 1。
-
轮询 BUSY,等待操作完成。

我们查看命令列表看到,写状态寄存器使用 0x01/0x31/0x11
这三个命令,分别对应的状态寄存器的位数是:
命令 | 对应的Bit位数 |
---|---|
0x01 | 第0位到第7位 (S0 ~ S7) |
0x31 | 第8位到第15位 (S8 ~ S15) |
0x11 | 第16位到第24位(S16 ~ S23) |

同时我们从这段文字中提取到了两个很重要的信息:
-
写入的过程中会使能 WIP 的状态位,直到写入完成。
-
WIP位处于状态寄存器的第0位。
我们继续查看状态寄存器表,发现了Flash擦除和页编程的时候都会使能这个寄存器


首先在 BSP\QuadSPI-Flash\bsp_qspi_gd25q64e.h
中进行相关的定义:
cpp
#define GD25Q64E_CMD_WRITE_STATUS_1 0x01 /* 写状态寄存器第0位到第7位(S0 ~ S7) */
#define GD25Q64E_CMD_WRITE_STATUS_2 0x31 /* 写状态寄存器第8位到第15位(S8 ~ S15) */
#define GD25Q64E_CMD_WRITE_STATUS_3 0x11 /* 写状态寄存器第16位到第23位(S16 ~ S23) */
在 BSP\QuadSPI-Flash\bsp_qspi_gd25q64e.h
中开始编写函数:
cpp
/******************************************************************
* 函 数 名 称:QSPI_PollingWIP
* 函 数 说 明:等待状态位(WIP)被置0,擦除和页编程的时候WIP位会被置位1
* 函 数 形 参:无
* 函 数 返 回:0:成功 1:失败
* 备 注:无
******************************************************************/
static int QSPI_PollingWIP(void)
{
QSPI_CommandTypeDef FlashCommand = {0};
QSPI_AutoPollingTypeDef PollConfig = {0};
FlashCommand.InstructionMode = QSPI_INSTRUCTION_1_LINE; /* 一线指令模式 */
FlashCommand.AddressSize = QSPI_ADDRESS_24_BITS; /* 24位地址 */
FlashCommand.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE; /* 无交替字节 */
FlashCommand.Instruction = GD25Q64E_CMD_WRITE_STATUS_1; /* 读取状态寄存器1命令 */
FlashCommand.AddressMode = QSPI_ADDRESS_NONE; /* 无地址模式 */
FlashCommand.DataMode = QSPI_DATA_1_LINE; /* 一线数据模式 */
FlashCommand.SIOOMode = QSPI_SIOO_INST_EVERY_CMD; /* 每次传输都发指令 */
FlashCommand.DummyCycles = 0; /* 无需指令空闲周期 */
PollConfig.Mask = 0x01; /* 只关心WIP位 */
PollConfig.Match = 0x00; /* WIP位为0 */
PollConfig.MatchMode = QSPI_MATCH_MODE_AND; /* AND匹配模式 */
PollConfig.StatusBytesSize = 1; /* 状态寄存器1有1个字节 */
PollConfig.Interval = 0x10; /* 轮询间隔为16个QSPI时钟周期 */
PollConfig.AutomaticStop = QSPI_AUTOMATIC_STOP_ENABLE; /* 自动停止轮询 */
/* 轮询WIP位,会不断的查询目标寄存器的数值,直到WIP位被置位 */
if (HAL_QSPI_AutoPolling(&hqspi, &FlashCommand, &PollConfig, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
return 1;
return 0;
}
此函数的作用就是轮询等待状态寄存器中的WIP位被置0,也就是等待写入/擦除
操作结束。
cpp
/******************************************************************
* 函 数 名 称:QSPI_EnableQuadMode
* 函 数 说 明:使能四线模式
* 函 数 形 参:无
* 函 数 返 回:0:成功 1:失败
* 备 注:无
******************************************************************/
static int QSPI_EnableQuadMode(void)
{
uint8_t Sr2 = 0;
QSPI_CommandTypeDef FlashCommand = {0};
/* 读 SR2 */
FlashCommand.InstructionMode = QSPI_INSTRUCTION_1_LINE; /* 一线指令模式 */
FlashCommand.AddressMode = QSPI_ADDRESS_NONE; /* 无地址模式 */
FlashCommand.DataMode = QSPI_DATA_1_LINE; /* 一线数据模式 */
FlashCommand.Instruction = GD25Q64E_CMD_READ_STATUS_2; /* 读取状态寄存器2命令 */
FlashCommand.NbData = 1; /* 读取1个字节 */
if (HAL_QSPI_Command(&hqspi, &FlashCommand, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK) return 1;
if (HAL_QSPI_Receive(&hqspi, &Sr2, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK) return 1;
/* 如果已经是四线模式,则直接返回 */
if (Sr2 & 0x02) return 0;
/* 写使能 */
if (QSPI_WriteEnable() != 0) return 1;
/* 写SR2 */
uint8_t WriteSR2 = (Sr2 | 0x02);
FlashCommand.Instruction = GD25Q64E_CMD_WRITE_STATUS_2; /* 写状态寄存器2命令 */
FlashCommand.DataMode = QSPI_DATA_1_LINE; /* 一线数据模式 */
FlashCommand.NbData = 1; /* 发送1个字节 */
if (HAL_QSPI_Command(&hqspi, &FlashCommand, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK) return 1;
if (HAL_QSPI_Transmit(&hqspi, &WriteSR2, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK) return 1;
/* 等待WIP位被清0 */
return QSPI_PollingWIP();
}
此函数首先读取了 QE
的状态位判断是不是已经被置 1
,如果已经置1则直接返回,否则使用相关的写入命令对QE位进行修改,然后等待写入结束(WIP置0)。
3、读取ID
阅读 Flash
的数据手册,看到了ID定义表:

我们可以看到有好个类ID的读取命令:

其中 MID 是厂家ID,ID 是设备ID,我们读取使用 0x9F
命令即可,直接读取三个字节:厂家编号+设备编号+容量信息

首先在 BSP\QuadSPI-Flash\bsp_qspi_gd25q64e.h
中进行相关的定义:
cpp
#define GD25Q64E_CMD_READ_ID 0x9F /* 读取器件ID */
在 BSP\QuadSPI-Flash\bsp_qspi_gd25q64e.c
中开始编写函数:
cpp
/******************************************************************
* 函 数 名 称:QSPI_ReadFlashID
* 函 数 说 明:读取Flash ID
* 函 数 形 参:无
* 函 数 返 回:0:成功 1:失败
* 备 注:无
******************************************************************/
int QSPI_ReadFlashID(uint8_t *mid, uint8_t *did, uint8_t *uid)
{
QSPI_CommandTypeDef FlashCommand = {0};
uint8_t id[3] = {0};
FlashCommand.InstructionMode = QSPI_INSTRUCTION_1_LINE; /* 一线指令模式 */
FlashCommand.AddressSize = QSPI_ADDRESS_24_BITS; /* 24位地址 */
FlashCommand.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE; /* 无交替字节 */
FlashCommand.AddressMode = QSPI_ADDRESS_NONE; /* 无地址模式 */
FlashCommand.DdrMode = QSPI_DDR_MODE_DISABLE; /* 关闭DDR模式 */
FlashCommand.DdrHoldHalfCycle = QSPI_DDR_HHC_ANALOG_DELAY; /* DDR模式下的时钟延迟 */
FlashCommand.Instruction = GD25Q64E_CMD_READ_ID; /* 读取器件ID命令 */
FlashCommand.DataMode = QSPI_DATA_1_LINE; /* 一线数据模式 */
FlashCommand.SIOOMode = QSPI_SIOO_INST_EVERY_CMD; /* 每次传输都发指令 */
FlashCommand.NbData = 3; /* 读取3个字节 */
FlashCommand.DummyCycles = 0; /* 无需指令空闲周期 */
/* 发送读取ID命令 */
if (HAL_QSPI_Command(&hqspi, &FlashCommand, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
return 1;
/* 读取ID数据 */
if (HAL_QSPI_Receive(&hqspi, id, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
return 1;
/* 解析ID数据 */
if (mid != NULL) *mid = id[0];
if (did != NULL) *did = id[1];
if (uid != NULL) *uid = id[2];
return 0;
}
用于读取 GD25Q64E Flash 芯片的器件 ID。通过发送 读取ID命令(0x9F) ,获取芯片厂商、型号和容量信息,用于芯片识别和初始化自检。
-
mid :指向存放厂家ID的指针(Manufacturer ID),兆易创新为
0xC8
-
did :指向存放器件型号ID的指针(Device ID),GD25Q64E为
0x40
-
uid :指向存放容量ID的指针(Unique ID/Memory Capacity),GD25Q64E为
0x17
-
三者均可为
NULL
,若不需要某项ID可传NULL
4、解除写保护
这个其实不是非必要的,以防外一擦除或者写入的时候失败,我们设计的这个函数,每次在 Flash 的 Init 时运行一下即可。
从 Flash 数据手册的这个表里面我们可以看到,无论CMP
等于几,只要我们将BP0
、BP1
和BP2
置1
,那么所有的保护都会关闭。

而BP0
、BP1
和BP2
在状态寄存器的 S2 ~ S4
位,而且是在第一个字节中。
Note 提示了它是掉电不丢失的。

在 BSP\QuadSPI-Flash\bsp_qspi_gd25q64e.c
中开始编写函数,用于检测并解除保护:
cpp
/******************************************************************
* 函 数 名 称:QSPI_Unprotect
* 函 数 说 明:解除写保护
* 函 数 形 参:无
* 函 数 返 回:0:成功 1:失败
* 备 注:无
******************************************************************/
static int QSPI_Unprotect(void)
{
uint8_t ReadSR1 = 0;
uint8_t WriteSR1 = 0;
QSPI_CommandTypeDef FlashCommand = {0};
/* 读 SR1 */
FlashCommand.InstructionMode = QSPI_INSTRUCTION_1_LINE; /* 一线指令模式 */
FlashCommand.AddressMode = QSPI_ADDRESS_NONE; /* 无地址模式 */
FlashCommand.DataMode = QSPI_DATA_1_LINE; /* 一线数据模式 */
FlashCommand.Instruction = GD25Q64E_CMD_READ_STATUS_1; /* 读取状态寄存器1命令 */
FlashCommand.NbData = 1; /* 读取1个字节 */
if (HAL_QSPI_Command(&hqspi, &FlashCommand, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK) return 1;
if (HAL_QSPI_Receive(&hqspi, &ReadSR1, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK) return 1;
/* 将BP0、BP1和BP2置1(分别是SR1的S2/S3/S4位)*/
WriteSR1 = (ReadSR1 | 0x1C); // 0x1C: 00011100
/* 写使能 */
if (QSPI_WriteEnable() != 0) return 1;
/* 写SR1 */
FlashCommand.Instruction = GD25Q64E_CMD_WRITE_STATUS_1; /* 写状态寄存器1命令 */
FlashCommand.DataMode = QSPI_DATA_1_LINE; /* 一线数据模式 */
FlashCommand.NbData = 1; /* 发送1个字节 */
if (HAL_QSPI_Command(&hqspi, &FlashCommand, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK) return 1;
if (HAL_QSPI_Transmit(&hqspi, &WriteSR1, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK) return 1;
/* 等待WIP位被清0 */
return QSPI_PollingWIP();
}
用于解除 GD25Q64E Flash 的写保护 ,具体做法是将状态寄存器1 (SR1) 的 Block Protect 位 BP0、BP1、BP2(分别对应 SR1 的 S2、S3、S4 位)全部置为 1,从而实现解锁或取消保护指定的存储区域。这个函数可以在BSP初始化的时候运行一下!
5、擦除扇区(Sector)
我们从Flash手册的开头 FEATURES
中可以了解到扇区的大小是 4KB
,块是 32KB
或者 64KB
的,也就是我们操作扇区的时候,擦除起始地址必须是被目标扇区整除的。
查看相关的命令(0x20
):

其中提到了三个点
-
必须先写使能,然后等待
WEL
标志被置位。 -
在擦除期间,可以查看WIP标志位,当为1时还在擦除,为0时则完成擦除,可以用这个来判断擦除完成。
-
如果被
BP0 ~ BP4
所设置为保护区域,那么擦除操作将无效。
所以我们针对这三个点进行代码的编写,首先是写使能,这个之前已经实现过了,而且里面还包含了WEL
标志置位的等待函数。
接下来就可以编写具体的擦除函数了,首先在.h
定义命令:
cpp
#define GD25Q64E_CMD_ERASE_SECTOR_4KB 0x20 /* 扇区擦除,擦除大小为4KB */
#define GD25Q64E_CMD_ERASE_SECTOR_32KB 0x52 /* 扇区擦除,擦除大小为32KB */
#define GD25Q64E_CMD_ERASE_SECTOR_64KB 0xD8 /* 扇区擦除,擦除大小为64KB */
然后再 .c
中编写函数:
cpp
/******************************************************************
* 函 数 名 称:QSPI_EraseFlashSector
* 函 数 说 明:擦除扇区
* 函 数 形 参:无
* 函 数 返 回:0:成功 1:失败
* 备 注:擦除大小默认为64KB,SectorAddr必须能被64KB整除。
******************************************************************/
int QSPI_EraseFlashSector(uint32_t SectorAddr)
{
QSPI_CommandTypeDef FlashCommand = {0};
/* 写使能 */
if (QSPI_WriteEnable() != 0) return 1;
FlashCommand.InstructionMode = QSPI_INSTRUCTION_1_LINE; /* 一线指令模式 */
FlashCommand.AddressSize = QSPI_ADDRESS_24_BITS; /* 24位地址 */
FlashCommand.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE; /* 无交替字节 */
FlashCommand.DdrMode = QSPI_DDR_MODE_DISABLE; /* 关闭DDR模式 */
FlashCommand.DdrHoldHalfCycle = QSPI_DDR_HHC_ANALOG_DELAY; /* DDR模式下的时钟延迟,因为关闭了DDR模式所以不管 */
/*
* 注意:GD25Q64E的扇区擦除命令是0x20,擦除大小为4KB
* 如果需要擦除32KB或64KB,可以使用0x52或0xD8命令
* 这里为了速度快,统一使用64KB扇区擦除命令
* SectorAddr必须能被64KB整除
* 可选命令:
* GD25Q64E_CMD_ERASE_SECTOR_4KB (扇区擦除,擦除大小为4KB)
* GD25Q64E_CMD_ERASE_SECTOR_32KB (扇区擦除,擦除大小为32KB)
* GD25Q64E_CMD_ERASE_SECTOR_64KB (扇区擦除,擦除大小为64KB)
*/
FlashCommand.Instruction = GD25Q64E_CMD_ERASE_SECTOR_64KB; /* 64KB扇区擦除命令 */
FlashCommand.Address = SectorAddr; /* 扇区地址,要确保能被目标扇区大小整除 */
FlashCommand.AddressMode = QSPI_ADDRESS_1_LINE; /* 一线地址模式 */
FlashCommand.DataMode = QSPI_DATA_NONE; /* 不发送数据 */
FlashCommand.SIOOMode = QSPI_SIOO_INST_EVERY_CMD; /* 每次传输都发指令 */
FlashCommand.DummyCycles = 0; /* 无需指令空闲周期 */
/* 发送扇区擦除命令 */
if (HAL_QSPI_Command(&hqspi, &FlashCommand, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
return 1;
/* 等待WIP位被清0 */
return QSPI_PollingWIP();
}
用于擦除 GD25Q64E Flash 芯片的一个扇区(Sector),默认擦除大小为 64KB。用户只需传入待擦除的扇区首地址,函数会完成整个擦除流程。
-
GD25Q64E 支持 4KB、32KB 和 64KB 三种扇区擦除命令(0x20、0x52、0xD8),本函数选择速度较快的 64KB 擦除命令。
-
必须保证传入的 SectorAddr 为 64KB 对齐,否则可能擦除到错误的区域。
-
擦除操作不可逆,数据会被彻底清除。
具体的擦除时间可以参照 Flash 的数据手册末尾表格

6、擦除整个芯片
整个操作很慢,所以一般情况下我们不使用,作为储备函数使用。

我们直接在 .h
中定义:
cpp
#define GD25Q64E_CMD_ERASE_CHIP 0xC7 /* 芯片全擦除 */
在 .c
中编写函数:
cpp
/******************************************************************
* 函 数 名 称:QSPI_EraseFlashChip
* 函 数 说 明:擦除整个芯片
* 函 数 形 参:无
* 函 数 返 回:0:成功 1:失败
* 备 注:慎用,会擦除整个芯片,速度非常慢,大约需要几十秒!
******************************************************************/
int QSPI_EraseFlashChip(void)
{
QSPI_CommandTypeDef FlashCommand = {0};
/* 写使能 */
if (QSPI_WriteEnable() != 0) return 1;
FlashCommand.InstructionMode = QSPI_INSTRUCTION_1_LINE; /* 一线指令模式 */
FlashCommand.AddressSize = QSPI_ADDRESS_24_BITS; /* 24位地址 */
FlashCommand.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE; /* 无交替字节 */
FlashCommand.DdrMode = QSPI_DDR_MODE_DISABLE; /* 关闭DDR模式 */
FlashCommand.DdrHoldHalfCycle = QSPI_DDR_HHC_ANALOG_DELAY; /* DDR模式下的时钟延迟,因为关闭了DDR模式所以不管 */
FlashCommand.Instruction = GD25Q64E_CMD_ERASE_CHIP; /* 全芯片擦除命令 */
FlashCommand.Address = 0; /* 无需传入地址 */
FlashCommand.AddressMode = QSPI_ADDRESS_1_LINE; /* 一线地址模式 */
FlashCommand.DataMode = QSPI_DATA_NONE; /* 不发送数据 */
FlashCommand.SIOOMode = QSPI_SIOO_INST_EVERY_CMD; /* 每次传输都发指令 */
FlashCommand.DummyCycles = 0; /* 无需指令空闲周期 */
/* 发送扇区擦除命令 */
if (HAL_QSPI_Command(&hqspi, &FlashCommand, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
return 1;
/* 等待WIP位被清0 */
return QSPI_PollingWIP();
}
这里我们不需要发送地址,也无需发送数据等待即可。
具体的擦除时间可以参照 Flash 的数据手册:

7、页编程(写入数据)
首先我们解释下页编程这个概念:
-
页编程(Page Program)是 NOR Flash(GD25Q64E 属于 NOR)写入数据的基本单位。
-
Flash 芯片的存储空间被划分为若干"页"(page),每一页通常是 256 字节。
-
一次编程操作最多只能向一个页写入(即每次最多写 256 字节,从页的起始地址到页的结束地址)。
-
如果写入数据跨越两个页,会自动在下一次编程命令中继续写入下一页。
-
页编程的好处:
-
可以高效地管理写入,减少对存储单元的损耗。
-
避免写入超出页界限导致数据错乱。
-
我们可以直接理解成写入操作即可。
我们查看 Flash 的数据手册,可以看到有以下这些和页相关的命令:

0x02 (PP) 和 0x32 (Quad Page Program) 都是页编程命令,用于向 Flash 芯片写入(编程)数据。
-
0x02 (Page Program) :用普通 SPI 总线写数据。
-
0x32 (Quad Page Program) :用 QSPI 四线模式写数据(速度快)。
0x75(PES) 、0x7A(PER) 和 0x42 是一些其他的命令:
-
0x75 (PES) :暂停程序/擦除过程
-
0x7A (PER) :恢复程序/擦除过程
-
0x42:向安全寄存器写入数据
我们直接用快速四线写入命令(0x32)作为基本的写入操作,首先在 .h
定义:
cpp
#define GD25Q64E_CMD_PAGE_PROGRAM 0x02 /* 页编程 */
#define GD25Q64E_CMD_QUAD_PAGE_PROGRAM 0x32 /* 四线快速页编程(快速写入) */
然后在 .c
编写函数:
cpp
/******************************************************************
* 函 数 名 称:QSPI_WriteFlash
* 函 数 说 明:快速利用页编程写入Flash数据,标准页大小为256Byte。
* 函 数 形 参:PageAddr:页地址,要确保能被256Byte整除
* pData:数据缓冲区指针
* totalSize:写入数据大小,最大不能超过256Byte
* 函 数 返 回:0:成功 1:失败
* 备 注:
******************************************************************/
int QSPI_WriteFlash(uint32_t PageAddr, uint8_t *pData, uint32_t totalSize)
{
QSPI_CommandTypeDef FlashCommand = {0};
/* 写使能 */
if (QSPI_WriteEnable() != 0) return 1;
FlashCommand.InstructionMode = QSPI_INSTRUCTION_1_LINE; /* 一线指令模式 */
FlashCommand.AddressSize = QSPI_ADDRESS_24_BITS; /* 24位地址 */
FlashCommand.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE; /* 无交替字节 */
FlashCommand.DdrMode = QSPI_DDR_MODE_DISABLE; /* 关闭DDR模式 */
FlashCommand.DdrHoldHalfCycle = QSPI_DDR_HHC_ANALOG_DELAY; /* DDR模式下的时钟延迟,因为关闭了DDR模式所以不管 */
FlashCommand.AddressMode = QSPI_ADDRESS_1_LINE; /* 一线地址模式 */
FlashCommand.DataMode = QSPI_DATA_4_LINES; /* 四线数据模式 */
FlashCommand.SIOOMode = QSPI_SIOO_INST_ONLY_FIRST_CMD; /* 仅第一次传输时发送指令 */
FlashCommand.DummyCycles = 0; /* 无需指令空闲周期 */
FlashCommand.Instruction = GD25Q64E_CMD_QUAD_PAGE_PROGRAM; /* 四线快速页编程命令 */
FlashCommand.Address = PageAddr; /* 页地址,要确保能被256Byte整除 */
FlashCommand.NbData = totalSize; /* 写入数据大小 */
/* 发送页编程命令 */
if (HAL_QSPI_Command(&hqspi, &FlashCommand, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
return 1;
/* 发送数据 */
if (HAL_QSPI_Transmit(&hqspi, pData, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
return 1;
/* 等待WIP位被清0 */
return QSPI_PollingWIP();
}
8、读取数据
这算是整个 Flash 最重要的一个步骤,无论是后来的内存映射还是其他的操作,所有的操作都取决于整个读取的速度,我们直接开始看Flash的数据手册:

手册中有这样的几个命令,我们只关注快速读取(fast read),这样确保读取的速度最快!
官方命名 | 命令 | 解释 |
---|---|---|
READ DATA BYTES (READ) | 0x03 | 最基本的读命令,直接用 SPI 一线模式读取数据。<br />速度较慢,不带 Dummy 时钟。<br />适合初始化、慢速场合。<br /> |
FAST READ | 0x0B | 比 0x03 快,因为发完地址后加一个 Dummy 字节(8个时钟),紧接着高速输出数据。<br />还是单线 SPI,只是速度提升了。<br />用于有 Dummy 支持的主控,提高读取速度。<br /> |
DUAL OUTPUT FAST READ | 0x3B | 地址和指令用一线 SPI,数据输出用双线(2线)模式。<br />比普通 SPI 读快一倍,适合 MCU 支持 Dual SPI 的场合。<br /> |
QUAD OUTPUT FAST READ | 0x6B | 地址和指令用一线 SPI,数据输出用四线(4线)模式(QSPI)。<br />速度比普通 SPI 读快四倍,适合速度要求高的应用。<br /> |
DUAL I/O FAST READ | 0xBB | 地址、数据都用双线(2线)模式传输。<br />比 Dual Output 更快,因为地址也用双线模式。<br /> |
QUAD I/O FAST READ | 0xEB | 地址、数据都用四线(QSPI)模式传输。<br />是最快的读操作,适合高性能主控(支持 QSPI)。<br /> |
我们直接使用 【QUAD I/O FAST READ】 接口命令。
关于我们之后将要使用的 四线快速输入输出读取命令(0xEB)
我们读取数据手册:



这都是 0xEB
这个命令下的语句,从中我们可以读出几个非常重要的信息:
-
当前用的是 1‑4‑4 Quad I/O:指令单线→地址四线→(可选模式字节/Dummy)→数据四线。
-
4‑4‑4(QPI) 模式:指令本身也走 4 线并行(一次输出 4bit×2 个时钟\=1字节)。Flash 需要先执行 "Enter QPI Mode" 指令(绝大多数 GigaDevice 为 0x38,退出是 0xFF,务必查你的 Datasheet QPI 章节确认)。
-
QE\=1 只是允许 Quad I/O,不等于直接使用 4-4-4 模式,也就是 4线指令-4线命令-4线数据
-
QE 置位 1 ≠ 进入 QPI 模式。QE 只是允许使用 1-1-4 / 1-4-4 / 1-1-2 / 1-2-2 等扩展 I/O 操作。若要使用 4-4-4(即指令也 4 线)必须显式发送"进入 QPI 模式"指令
但是但是但是,在手册中没有明显的找到相关的进入4-4-4指令的命令,所以我们使用的是1-4-4模式。

经过实际的代码测试其中有一个关键的参数:Dummy
这是指令空闲时间,之前我们使用单线模式发送一些简单的命令,写入的速度没有达到读取的速度这么高,所以我们不怎么关注于这个参数,现在我们直接把速度拉满,任何一点小小的时序错误都不行,关于这个参数的问题,还是来读Flash的数据手册:

我们发现了这个表格,上面详细的列出了,在DC位置0和置1时速度分别有什么样的变化,我们使用的是 0xEB
这个命令,按照手册来说一般情况下需要选择的是 6
或者 10
,但是经过作者的测试,发现这个其实和板子的一些硬件参数也有关系,例如走线,和芯片引脚的配置等等。所以我们要根据实际的情况来进行设置。
下面我先设置为
6
进行测试,后面我们再进行一些调整和测试。
在 .h
中定义:
注意:
QSPI_DUMMY_CYCLES_READ_QUAD
需要我们最后测试的时候进行调整。目前先设定默认6
。
cpp
#define QSPI_DUMMY_CYCLES_READ_QUAD 6 /* 四线读取时的指令空闲周期数 */
#define GD25Q64E_CMD_READ_DATA 0x03 /* 读取数据 */
#define GD25Q64E_CMD_FAST_READ 0x0B /* 快速读取数据 */
#define GD25Q64E_CMD_FAST_READ_DUAL 0x3B /* 双线快速读取数据 */
#define GD25Q64E_CMD_FAST_READ_QUAD 0x6B /* 四线快速读取数据 */
#define GD25Q64E_CMD_FAST_READ_IO_DUAL 0xBB /* 双线输入输出快速读取数据 */
#define GD25Q64E_CMD_FAST_READ_IO_QUAD 0xEB /* 四线输入输出快速读取数据 */
在 .c
中编写函数:
cpp
/******************************************************************
* 函 数 名 称:QSPI_ReadFlash
* 函 数 说 明:快速读取Flash数据,可以超过标准页大小256Byte,不能超过Flash芯片的容量大小
* 函 数 形 参:startAddr:数据起始地址
* pData:数据缓冲区指针
* totalSize:读取数据大小,可以大于标准页256Byte,不能超过Flash芯片容量
* 函 数 返 回:0:成功 1:失败
* 备 注:
******************************************************************/
int QSPI_ReadFlash(uint32_t startAddr, uint8_t *pData, uint32_t totalSize)
{
QSPI_CommandTypeDef FlashCommand = {0};
FlashCommand.DdrMode = QSPI_DDR_MODE_DISABLE; /* 关闭DDR模式 */
FlashCommand.DdrHoldHalfCycle = QSPI_DDR_HHC_ANALOG_DELAY; /* DDR模式下的时钟延迟,因为关闭了DDR模式所以不管 */
FlashCommand.InstructionMode = QSPI_INSTRUCTION_1_LINE; /* 一线指令模式 */
FlashCommand.AddressMode = QSPI_ADDRESS_4_LINES; /* 四线地址模式 */
FlashCommand.DataMode = QSPI_DATA_4_LINES; /* 四线数据模式 */
FlashCommand.AlternateByteMode = QSPI_ALTERNATE_BYTES_4_LINES; /* 四线交替字节模式 */
FlashCommand.AlternateBytesSize = QSPI_ALTERNATE_BYTES_8_BITS; /* 8位交替字节 */
FlashCommand.AlternateBytes = 0x00; /* 模式字节 */
FlashCommand.AddressSize = QSPI_ADDRESS_24_BITS; /* 24位地址 */
FlashCommand.SIOOMode = QSPI_SIOO_INST_EVERY_CMD; /* 每次传输时发送指令 */
FlashCommand.Instruction = GD25Q64E_CMD_FAST_READ_IO_QUAD; /* 四线快速输入输出读取命令 */
FlashCommand.Address = startAddr; /* 读取数据的起始地址 */
FlashCommand.NbData = totalSize; /* 读取数据的大小 */
/* 这个需要根据实际测试结果进行调整 */
FlashCommand.DummyCycles = QSPI_DUMMY_CYCLES_READ_QUAD; /* 四线读取时的指令空闲周期数 */
/* 发送命令 */
if (HAL_QSPI_Command(&hqspi, &FlashCommand, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
return 1;
/* 读取数据 */
if (HAL_QSPI_Receive(&hqspi, pData, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
return 1;
return 0;
}
用于快速读取 GD25Q64E Flash 数据,支持一次读取任意长度(可超过256字节页长),只要不超过芯片总容量。利用 QSPI 四线模式和"快速读"命令,大幅提升读取速度。
-
使用QSPI四线模式,速度远高于普通SPI读取(单线模式)。
-
支持跨页读取,不受256字节页长限制,适合大量数据一次性读取。
-
DummyCycles需要根据主控和Flash速度调整,否则容易读错数据。
-
适合固件升级、资源加载等对速度和批量数据有要求的场景。
测试程序编写
1、Debug串口函数编写
cpp
/* 串口简单输出(阻塞方式) */
static void uart_puts(const char *s)
{
HAL_UART_Transmit(&huart1, (uint8_t*)s, (uint16_t)strlen(s), 1000);
}
static void uart_print_hex(const uint8_t *buf, uint32_t len)
{
char out[4];
for (uint32_t i = 0; i < len; i++) {
static const char HEX[] = "0123456789ABCDEF";
out[0] = HEX[(buf[i] >> 4) & 0x0F];
out[1] = HEX[ buf[i] & 0x0F];
out[2] = ( (i+1) % 16 == 0 ) ? '\n' : ' ';
out[3] = 0;
HAL_UART_Transmit(&huart1, (uint8_t*)out, (out[2]=='\n')?3:2, 1000);
}
if (len % 16 != 0) uart_puts("\r\n");
}
2、QuadSPI读写测试(重要)
这个很重要,我们要根据这个结果进行测试
QSPI_DUMMY_CYCLES_READ_QUAD
的数值,再根据实际的情况进行调整,因为这个数值不稳定容易数据移位而导致读取的数据是错误的。例如:
写入:A0A1A2A3A4A5A6A7A8A9AAABACADAEAF
读出:0A1A2A3A4A5A6A7A8A9AAABACADAEAF
我们可以看到开头的一些数据丢失了,所以这就是
DummyCycles
(指令空闲时间)的数值偏大,读取的数据不及时导致开头丢掉了,这时候我们就可以修改DummyCycles
的数字减小再次进行测试。
cpp
/* Demo:测试 QSPI-Flash的读写
步骤:
1. 读取 JEDEC ID
2. 擦除64K块 -> 写一页 (32字节)
3. 直接读取数据查看相关的结果
*/
static void QSPI_TestReadWrite(void)
{
uint8_t tx[QSPI_TEST_LEN];
uint8_t rx[QSPI_TEST_LEN];
uint32_t i;
uint8_t mid=0, did=0, uid=0;
/* 1. 读取 JEDEC ID */
if (QSPI_ReadFlashID(&mid,&did,&uid) != 0) {
uart_puts("Read ID Fail\r\n");
return;
}
char msg[80];
snprintf(msg, sizeof(msg), "JEDEC ID: %02X %02X %02X\r\n", mid,did,uid);
uart_puts(msg);
/* 准备写入数据模式递增 */
for (i = 0; i < QSPI_TEST_LEN; i++) {
tx[i] = (uint8_t)(0xA0 + i);
}
uart_puts("Erase 64K Block...\r\n");
if (QSPI_EraseFlashSector(QSPI_TEST_ADDR) != 0) { uart_puts("Erase Fail\r\n"); return; }
uart_puts("Page Program...\r\n");
if (QSPI_WriteFlash(QSPI_TEST_ADDR,tx,QSPI_TEST_LEN) != 0) { uart_puts("Program Fail\r\n"); return; }
/* 直接读取数据查看相关的结果 */
if (QSPI_ReadFlash(QSPI_TEST_ADDR, rx, QSPI_TEST_LEN) != 0) {
uart_puts("Read Flash Fail\r\n");
return;
}
uart_puts("TX:\r\n"); uart_print_hex(tx, QSPI_TEST_LEN);
uart_puts("RX:\r\n"); uart_print_hex(rx, QSPI_TEST_LEN);
int diff = 0; for (i = 0; i < QSPI_TEST_LEN; i++) { if (tx[i] != rx[i]) { diff = 1; break; } }
uart_puts(diff?"COMPARE: FAIL\r\n":"COMPARE: OK\r\n");
}
main.c
的代码如下:
cpp
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* Copyright (c) 2025 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "quadspi.h"
#include "usart.h"
#include "gpio.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "string.h"
#include "stdio.h"
#include "bsp_qspi_gd25q64e.h"
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
#define QSPI_TEST_ADDR 0x00000000u /* 64KB边界;确保 <= 8MB */
#define QSPI_TEST_LEN 32 /* 测试字节数(<=256, 一页) */
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
static void QSPI_TestReadWrite(void);
static void uart_puts(const char *s);
static void uart_print_hex(const uint8_t *buf, uint32_t len);
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_QUADSPI_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
if (QSPI_FlashInit() != 0) {
uart_puts("QSPI Init Fail\r\n");
Error_Handler();
}
uart_puts("STM32H750xx Init OK\r\n");
/* 运行Flash读写测试 */
QSPI_TestReadWrite();
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Supply configuration update enable
*/
HAL_PWREx_ConfigSupply(PWR_LDO_SUPPLY);
/** Configure the main internal regulator output voltage
*/
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE0);
while(!__HAL_PWR_GET_FLAG(PWR_FLAG_VOSRDY)) {}
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 5;
RCC_OscInitStruct.PLL.PLLN = 192;
RCC_OscInitStruct.PLL.PLLP = 2;
RCC_OscInitStruct.PLL.PLLQ = 2;
RCC_OscInitStruct.PLL.PLLR = 2;
RCC_OscInitStruct.PLL.PLLRGE = RCC_PLL1VCIRANGE_2;
RCC_OscInitStruct.PLL.PLLVCOSEL = RCC_PLL1VCOWIDE;
RCC_OscInitStruct.PLL.PLLFRACN = 0;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2
|RCC_CLOCKTYPE_D3PCLK1|RCC_CLOCKTYPE_D1PCLK1;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.SYSCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.AHBCLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB3CLKDivider = RCC_APB3_DIV2;
RCC_ClkInitStruct.APB1CLKDivider = RCC_APB1_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_APB2_DIV2;
RCC_ClkInitStruct.APB4CLKDivider = RCC_APB4_DIV2;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK)
{
Error_Handler();
}
}
/* USER CODE BEGIN 4 */
/* 串口简单输出(阻塞方式) */
static void uart_puts(const char *s)
{
HAL_UART_Transmit(&huart1, (uint8_t*)s, (uint16_t)strlen(s), 1000);
}
static void uart_print_hex(const uint8_t *buf, uint32_t len)
{
char out[4];
for (uint32_t i = 0; i < len; i++) {
static const char HEX[] = "0123456789ABCDEF";
out[0] = HEX[(buf[i] >> 4) & 0x0F];
out[1] = HEX[ buf[i] & 0x0F];
out[2] = ( (i+1) % 16 == 0 ) ? '\n' : ' ';
out[3] = 0;
HAL_UART_Transmit(&huart1, (uint8_t*)out, (out[2]=='\n')?3:2, 1000);
}
if (len % 16 != 0) uart_puts("\r\n");
}
/* Demo:测试 QSPI-Flash的读写
步骤:
1. 读取 JEDEC ID
2. 擦除64K块 -> 写一页 (32字节)
3. 直接读取数据查看相关的结果
*/
static void QSPI_TestReadWrite(void)
{
uint8_t tx[QSPI_TEST_LEN];
uint8_t rx[QSPI_TEST_LEN];
uint32_t i;
uint8_t mid=0, did=0, uid=0;
/* 1. 读取 JEDEC ID */
if (QSPI_ReadFlashID(&mid,&did,&uid) != 0) {
uart_puts("Read ID Fail\r\n");
return;
}
char msg[80];
snprintf(msg, sizeof(msg), "JEDEC ID: %02X %02X %02X\r\n", mid,did,uid);
uart_puts(msg);
/* 准备写入数据模式递增 */
for (i = 0; i < QSPI_TEST_LEN; i++) {
tx[i] = (uint8_t)(0xA0 + i);
}
uart_puts("Erase 64K Block...\r\n");
if (QSPI_EraseFlashSector(QSPI_TEST_ADDR) != 0) { uart_puts("Erase Fail\r\n"); return; }
uart_puts("Page Program...\r\n");
if (QSPI_WriteFlash(QSPI_TEST_ADDR,tx,QSPI_TEST_LEN) != 0) { uart_puts("Program Fail\r\n"); return; }
/* 直接读取数据查看相关的结果 */
if (QSPI_ReadFlash(QSPI_TEST_ADDR, rx, QSPI_TEST_LEN) != 0) {
uart_puts("Read Flash Fail\r\n");
return;
}
uart_puts("TX:\r\n"); uart_print_hex(tx, QSPI_TEST_LEN);
uart_puts("RX:\r\n"); uart_print_hex(rx, QSPI_TEST_LEN);
int diff = 0; for (i = 0; i < QSPI_TEST_LEN; i++) { if (tx[i] != rx[i]) { diff = 1; break; } }
uart_puts(diff?"COMPARE: FAIL\r\n":"COMPARE: OK\r\n");
}
/* USER CODE END 4 */
/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
__disable_irq();
while (1)
{
}
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
下载程序之后,查看串口Debug输出的数据我们可以发现:
-
ID读取成功,和预期的一致。
-
读取的信息出现了偏移,很明显的是
RX
(读取出来的数据)出现了最前面A0
这个数据的丢失,这就和我们的DummyCycles
数值过大有关,发送指令之后等待的时间过长,导致了数据读取不及时,丢掉了最前面的数据,所以我们需要修改DummyCycles
的数值,在前面的《编写BSP驱动/读取数据
》章节中我们定义的QSPI_DUMMY_CYCLES_READ_QUAD
是6
(使用的默认值),我们接下来开始将这个数字逐步-1
,直到最后的对别结果是完全正确的才可以。

经过两次测试,发现当QSPI_DUMMY_CYCLES_READ_QUAD
为4
的时候数据是正确的,所以我们可以将这个数字固定下来了,就直接使用4
。

至此读写测试就全部完成了。
工程文件