【STM32H7】QuadSPI读写W25Q64JV
W25Q64JV介绍
1、基本介绍
W25Q64JV是一款由华邦电子(Winbond)推出的64兆位(8MB)串行闪存(Serial Flash)芯片,采用SPI(Serial Peripheral Interface)接口。它支持高速读写操作,具有低功耗、宽工作电压范围(2.7V至3.6V),以及多种擦写模式(如页面擦除、块擦除、整片擦除),并具备高可靠性和数据保持能力。W25Q64JV广泛应用于嵌入式系统、物联网设备、消费电子等领域,用于存储程序代码、配置数据、日志等。
STM32H750 系列微控制器通过 SPI 接口可以方便地与 W25Q64JV 等外部 Flash 芯片进行连接。这样不仅可以扩展系统的存储容量,还能实现程序代码、配置参数、数据日志等的高效存储与读取。利用 STM32H750 强大的片上资源(如高速 SPI、灵活的内存映射功能),外部 Flash 可被映射到 MCU 的地址空间,实现类似片内 Flash 的访问体验。
官方资料获取:点击跳转🚀
2、主要特性
- 容量:64M-bit(8MB)
- 接口类型:支持标准 SPI(Serial Peripheral Interface),兼容双线(Dual SPI)和四线(Quad SPI)模式
- 工作电压:2.7V ~ 3.6V
- 时钟频率:最高支持 133MHz
- 页面大小:256 字节
- 扇区大小:4KB
- 块大小:32KB / 64KB
- 擦写寿命:每个扇区/块可达 100,000 次擦写
- 数据保持时间:20 年以上
- 封装形式:SOP8、USON8、WSON8 等多种封装选择
- 工作温度范围:-40°C ~ +85°C
3、引脚定义
从数据手册中我们可以看到相关的引脚设置

| 引脚编号 | 名称 | 功能说明 |
|---|---|---|
| 1 | CS | 片选 |
| 2 | SO(IO1) | 数据输出(数据输入输出引脚1) |
| 3 | WP(IO2) | 写保护(数据输入输出引脚2) |
| 4 | GND | 地 |
| 5 | SI(IO0) | 数据输入(数据输入输出引脚0) |
| 6 | CLK | 时钟输入 |
| 7 | HOLD / RESET(IO3) | 暂停信号(数据输入输出引脚3) |
| 8 | VCC | 电源 |
4、框图

1)存储结构与分区
- 主存储区:
右侧显示整个W25Q64JV的地址空间,分为128个Block(每个Block 64KB),每个Block内部又分为若干Sector(每个Sector 4KB)。
存储空间从0x000000到0x7FFFFF,共8MB。 - Block Segmentation:
左上角详细列出了Sector的分布,每个Sector 4KB,Sector的首地址和末地址也标注出来,便于寻址和擦写操作。 - SFDP寄存器与安全寄存器:
顶部有SFDP寄存器,用于设备特性自描述;Security Register用于安全存储区,支持三组安全寄存器。
2)控制逻辑模块
- SPI命令与控制逻辑:
该模块是芯片的核心控制单元,负责与主控MCU进行通信,接收SPI协议下的命令(如读、写、擦除等),并控制数据流向。 - 输入输出引脚:
包含/CS(片选)、CLK(时钟)、DI(输入)、DO(输出)、/WP(写保护)、/HOLD(总线挂起),用于与外部设备通信和控制。 - 写控制逻辑与状态寄存器:
写控制逻辑结合/WP、状态寄存器,实现对写操作的保护和状态监控(如忙标志、错误标志等)。 - 高压发生器:
用于擦除和编程操作时产生所需的高电压。
3)地址与数据处理
- 页地址锁存/计数器、字节地址锁存/计数器:
用于定位和缓冲目标页或字节的数据地址,实现高速写入和读取。 - 列解码与256字节页缓冲区:
存储数据以页(256字节)为单位,任何写入操作都先经过页缓冲再写入闪存。
4)保护与擦写机制
- 写保护逻辑:
支持多级保护,包括硬件写保护(/WP引脚)和软件写保护(寄存器或命令控制),确保数据安全。 - 擦写分区:
支持按Sector(4KB)、Block(32KB/64KB)以及整片擦除,灵活满足不同应用场景的数据管理需求。
CubeMX配置
1、新建项目
我们直接利用CubeMX软件初始化一个工程

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


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

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

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

SWD调试接口配置:

设定 LED 控制引脚,使我们在出错的时候有相关的提示,在原理图中我们看到 LED 引脚是 PE3
- 输出高电平点亮LED
- 输出低电平则熄灭LED

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

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

接下来进行QuadSPI的常规

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

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

之前配置 QuadSPI 的时候 Clock Prescaler 设定为了 1 ,因为内部分频会自动+1 ,所以 QuadSPI 的时钟速度就是:
-
240MHz / (Clock Prescaler + 1)
- = 240MHz / (1 + 1)
- = 120MHz
只要不超过规定的 133MHz 的上线即可:

5、生成工程
- 工程名字设定为:QuadSPI-Flash-Poll_W25Q64JV_Project 。
- Project Location设定工程的位置。
- Toolchain/IDE 设定生成的工程是 Keil-MDK 的。


点击 GENERATE CODE 生成代码:

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

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

QuadSPI-W25Q64JV 的文件夹中创建 bsp_qspi_w25q64jv.c 和 bsp_qspi_w25q64jv.h

打开项目,将我们的创建的 .h 和 .c 文件添加到工程中
我创建了一个组 BSP/QSPI-W25Q64JV,将相关的 .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 位,需要根据 Flash 手册中的寻址空间设置。
- 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 返回的状态字节进行掩码处理,只比较关心的位(如 “ 忙 ” 位)。
- Interval:自动轮询间隔时钟周期数。决定每次状态寄存器轮询之间的等待时间,单位为 QSPI 时钟周期。
- StatusBytesSize:状态字节数。设置每次读回的状态寄存器长度。
- MatchMode:匹配方式。指定比较的判定方法,如全部匹配或部分匹配。
- AutomaticStop:自动停止。轮询到匹配状态后自动停止。
应用场景:
- 在 BSP 驱动中,自动轮询模式通常用于等待 Flash 完成写入或擦除操作。比如,先发起擦除命令,再配置 QSPI_AutoPollingTypeDef,调用 HAL_QSPI_AutoPolling() 轮询状态寄存器的 “ 忙 ” 位(BUSY),直到 Flash 空闲后再进行后续操作。
1、写使能
在 W25Q64JV 进行写入或擦除操作前,必须先执行 “ 写使能 ” 操作。写使能的流程如下:
- 发送写使能指令 0x06 向 Flash 发送 0x06 指令,使能后续的写/擦除操作。
- 轮询状态寄存器 轮询状态寄存器1的 WEL(写使能锁存)位(位1)是否被置位,确保写使能成功。
这是最终要的一个操作,奠定了之后的一切写入和擦除操作。

我们根据这个 Flash 数据手册中 8.2.4 的 Read Status Register-1 章节,可知 0x05 0x35 0x15 每个命令可以读取一个字节(8Bits)的数据,所以24bits的状态信息需要3个命令才能完全读取:
| 命令 | 对应的Bit位数 |
|---|---|
| 0x05 | 第0位到第7位 (bit 0 ~ bit 7) |
| 0x35 | 第8位到第15位 (bit 8 ~ bit 15) |
| 0x15 | 第16位到第24位(bit 16 ~ bit 23) |

我们需要的是 WEL(写使能锁存) 这个状态,所以我们查询 7.1 章节 Status Registers 表格可以看到 WEL 状态位于 S1 也就是位 1

所以我们只需要读状态寄存器的第一个字节即可,用 0x05 这个命令。
首先在 BSP\QuadSPI-W25Q64JV\bsp_qspi_w25q64jv.h 中定义相关的寄存器:
#define W25Q64JV_CMD_WRITE_ENABLE 0x06 /* 写使能 */
#define W25Q64JV_CMD_READ_STATUS_1 0x05 /* 读取状态寄存器第0位到第7位(S0 ~ S7) */
#define W25Q64JV_CMD_READ_STATUS_2 0x35 /* 读取状态寄存器第8位到第15位(S8 ~ S15) */
#define W25Q64JV_CMD_READ_STATUS_3 0x15 /* 读取状态寄存器第16位到第23位(S16 ~ S23) */
在 BSP\QuadSPI-W25Q64JV\bsp_qspi_w25q64jv.c 中开始编写函数:
/******************************************************************
* 函 数 名 称:QSPI_PollingWEL
* 函 数 说 明:等待写使能位(WEL)被置位
* 函 数 形 参:无
* 函 数 返 回:0:成功 1:失败
* 备 注:无
******************************************************************/
static int QSPI_PollingWEL(void)
{
QSPI_CommandTypeDef FlashCMD = {0};
QSPI_AutoPollingTypeDef FlashCFG = {0};
FlashCMD.InstructionMode = QSPI_INSTRUCTION_1_LINE; /* 一线指令模式 */
FlashCMD.Instruction = W25Q64JV_CMD_READ_STATUS_1; /* 读取状态寄存器1命令 */
FlashCMD.AddressMode = QSPI_ADDRESS_NONE; /* 无地址模式 */
FlashCMD.DataMode = QSPI_DATA_1_LINE; /* 一线数据模式 */
FlashCFG.Mask = 0x02; /* 0000 0010 */ /* 只关心WEL位,关注位数为1的那一位 */
FlashCFG.Match = 0x02; /* 0000 0010 */ /* WEL位为1的时候退出轮询 */
FlashCFG.MatchMode = QSPI_MATCH_MODE_AND; /* AND匹配模式 */
FlashCFG.StatusBytesSize = 1; /* 状态寄存器1有1个字节 */
FlashCFG.Interval = 0x10; /* 轮询间隔为16个QSPI时钟周期 */
FlashCFG.AutomaticStop = QSPI_AUTOMATIC_STOP_ENABLE; /* 自动停止轮询 */
if (HAL_QSPI_AutoPolling(&hqspi, &FlashCMD, &FlashCFG, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK) {
return 1;
}
return 0;
}
-
功能: 用于自动轮询的状态寄存器1(SR1,指令 0x05),等待 WEL(Write Enable Latch,位1)被置位,确保写使能命令已经生效。
-
实现流程
- 构造 QSPI 命令结构体,设置为一线指令模式,指令为读取状态寄存器1(0x05),无地址,一线数据模式。
- 构造自动轮询配置结构体,只关心 WEL 位(Mask=0x02),匹配条件为 WEL=1(Match=0x02),其它参数如轮询间隔、匹配模式等根据实际设置。
- 调用 HAL_QSPI_AutoPolling 自动轮询芯片状态,直到 WEL 位被置位或超时。
- 成功返回 0,失败返回 1。
-
典型用法: 作为写使能命令后的状态确认步骤,确保芯片状态正确,避免后续写/擦除失败。
/******************************************************************
* 函 数 名 称:QSPI_WriteEnable
* 函 数 说 明:发送写使能命令,并等待WEL位被置位
* 函 数 形 参:无
* 函 数 返 回:0:成功 1:失败
* 备 注:无
******************************************************************/
static int QSPI_WriteEnable(void)
{
QSPI_CommandTypeDef FlashCMD = {0};
FlashCMD.InstructionMode = QSPI_INSTRUCTION_1_LINE; /* 一线指令模式 */
FlashCMD.Instruction = W25Q64JV_CMD_WRITE_ENABLE; /* 写使能命令 */
FlashCMD.AddressMode = QSPI_ADDRESS_NONE; /* 无地址模式 */
FlashCMD.DataMode = QSPI_DATA_NONE; /* 无数据模式 */
/* 发送写使能命令 */
if (HAL_QSPI_Command(&hqspi, &FlashCMD, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK) {
return 1;
}
/* 等待WEL位被置位 */
return QSPI_PollingWEL();
}
-
函数说明:用于向 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 位(S9)。

而且,我们看到 Volatile/Non-Volatile Writable 这个标志,说明 QE 位可通过非易失性(OTP)和易失性两种方式写入。通常使用非易失性写入(即掉电不丢失)。
所以我们可以将流程这样设计:
- 读取 SR2,检查 QE 是否已置位。
- 如果未置位,先写使能(WREN)。
- 写 SR2,把 QE 位置 1。
- 轮询 BUSY,等待操作完成。

我们查看 8.2.5 章节的 Write Status Register-1 (01h), Status Register-2 (31h) & Status Register-3 (11h) 看到,
写状态寄存器使用 0x01/0x31/0x11 这三个命令,分别对应的状态寄存器的位数是:
| 命令 | 对应的Bit位数 |
|---|---|
| 0x01 | 第0位到第7位 (S0 ~ S7) |
| 0x31 | 第8位到第15位 (S8 ~ S15) |
| 0x11 | 第16位到第24位(S16 ~ S23) |

同时我们从这段文字中提取到了两个很重要的信息:
- 发送指令码 “ 01h / 31h / 11h ”必须事先执行一个标准的写使能(0x06)指令,设备才能接受写状态寄存器指令(状态寄存器位 WEL 必须等于1)。
- 易失性写入(掉电丢失)需要先执行 0x50 命令,执行易失性的写使能。
- 非易失性写入(掉电不丢失)需要先执行标准写使能 0x06 命令。
- BUSY 状态在写状态寄存器的时候状态是 1,结束为 0。
我们继续查看状态寄存器表,发现了Flash擦除和页编程的时候都会使能这个寄存器

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

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

其中读取最常使用 0x9F 命令,直接读取三个字节:厂家编号+设备编号+容量信息

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

而 BP0、BP1和 BP2 在状态寄存器的 S2 ~ S4 位,而且是在第一个字节中。
小括号里提示了它可以掉电不丢失写入或掉电丢失写入。

我们这里直接掉电不丢失写入(非易失性写入)
在 BSP\QuadSPI-W25Q64JV\bsp_qspi_w25q64jv.c 中开始编写函数,用于检测并解除保护:
/******************************************************************
* 函 数 名 称:QSPI_FlashUnprotect
* 函 数 说 明:解除写保护
* 函 数 形 参:无
* 函 数 返 回:0:成功 1:失败
* 备 注:无
******************************************************************/
static int QSPI_FlashUnprotect(void)
{
uint8_t ReadSR1 = 0;
uint8_t WriteSR1 = 0;
QSPI_CommandTypeDef FlashCMD = {0};
/* 读 SR1 */
FlashCMD.InstructionMode = QSPI_INSTRUCTION_1_LINE; /* 一线指令模式 */
FlashCMD.AddressMode = QSPI_ADDRESS_NONE; /* 无地址模式 */
FlashCMD.DataMode = QSPI_DATA_1_LINE; /* 一线数据模式 */
FlashCMD.Instruction = W25Q64JV_CMD_READ_STATUS_1; /* 读取状态寄存器1命令 */
FlashCMD.NbData = 1; /* 读取1个字节 */
if (HAL_QSPI_Command(&hqspi, &FlashCMD, 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置0(分别是SR1的S2/S3/S4位)*/
WriteSR1 = (ReadSR1 & 0xE3); // 0xE3: 11100011
/* 写使能 */
if (QSPI_WriteEnable() != 0) return 1;
/* 写SR1 */
FlashCMD.Instruction = W25Q64JV_CMD_WRITE_STATUS_1; /* 写状态寄存器1命令 */
FlashCMD.DataMode = QSPI_DATA_1_LINE; /* 一线数据模式 */
FlashCMD.NbData = 1; /* 发送1个字节 */
if (HAL_QSPI_Command(&hqspi, &FlashCMD, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK) return 1;
if (HAL_QSPI_Transmit(&hqspi, &WriteSR1, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK) return 1;
/* 等待BUSY位被清0 */
return QSPI_WaitingBUSY();
}
用于解除 Flash 的写保护,具体做法是将状态寄存器1 (SR1) 的 Block Protect 位 BP0、BP1、BP2(分别对应 SR1 的 S2、S3、S4 位)全部置为 0,从而实现解锁或取消保护指定的存储区域。这个函数可以在BSP初始化的时候运行一下!
5、擦除命令
我们从Flash手册的开头 FEATURES 中可以了解到扇区的大小是 4KB,块是 32KB 或者 64KB 的,也就是我们操作扇区的时候,擦除起始地址必须是被目标4KB扇区整除的。

我们有这三个命令可以用,随便打开一个,继续查看具体的命令解释:

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

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

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

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

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

手册中有这样的几个命令,我们只关注快速读取(fast read),这样确保读取的速度最快!
| 官方命名 | 命令 | 解释 |
|---|---|---|
| Read Data | 0x03 | 最基本的读命令,直接用 SPI 一线模式读取数据。速度较慢,不带 Dummy 时钟。适合初始化、慢速场合。 |
| Fast Read | 0x0B | 使用标准SPI模式(单线),但比标准“Read Data (03h)”多了一个“dummy cycle”(空时钟周期),以提高数据输出速度。 |
| Fast Read Dual Output | 0x3B | 地址阶段仍然用单线SPI,但数据输出阶段用双线(Dual Output),即Flash的两个数据管脚(IO0/IO1)同时输出数据,数据传输速度加倍。 |
| Fast Read Quad Output | 0x6B | 地址阶段用单线SPI,数据输出阶段用四线(Quad Output),即Flash的四个数据管脚(IO0~IO3)同时输出数据,速度提升到四倍。 |
| Fast Read Dual I/O | 0xBB | 地址和数据阶段都用双线(IO0/IO1),不仅数据传输快,地址发送也快。 |
| Fast Read Quad I/O | 0xEB | 地址和数据阶段都使用4根数据线(IO0~IO3)并行收发。速度最快! |
我们直接使用 【Fast Read Quad I/O】 接口命令。
关于 四线快速输入输出读取(0xEB) 我们读取数据手册:

这都是 0xEB 这个命令下的语句,从中我们可以读出几个非常重要的信息:
- 需要 使能QE位 才能使用四线模式,使能QE之后用的是 1‑4‑4 Quad I/O:指令单线 - 地址四线 - 数据四线。
- 注意:QE = 1 只是允许Quad I/O,不等于直接使用 4-4-4 模式,也就是 4线指令-4线命令-4线数据
经过实际的代码测试其中有一个关键的参数 Dummy ,这是指令空闲时间,之前我们使用单线模式发送一些简单的命令,写入的速度没有达到读取的速度这么高,所以我们不怎么关注于这个参数,现在我们直接把速度拉满,任何一点小小的时序错误都不行。
我们使用的是 0xEB 这个命令,查看这个命令的时许图:

-
对于 Fast Read Quad I/O (EBh) 指令,时序图清楚地显示:
- 在发送完指令和 24 位地址后,主控需要再发送 4个Dummy时钟周期,之后才能开始接收数据。
- 这 4 个 Dummy 周期是标准要求,无论SPI时钟多少,Dummy周期数量不变。
按照手册来说一般情况下需要选择的是 4
在 .h 中定义:
注意:QSPI_DUMMY_CYCLES_READ_QUAD 需要我们最后测试的时候进行调整。目前先设定默认 6。
#define DUMMY_CYCLES_FAST_READ_QUAD_IO 4 /* 四线快速I/O读取时的指令空闲周期数 */
#define W25Q64JV_CMD_READ_DATA 0x03 /* 读取数据 */
#define W25Q64JV_CMD_FAST_READ 0x0B /* 快速读取 */
#define W25Q64JV_CMD_FAST_READ_DUAL 0x3B /* 双线快速读取 1-1-2 */
#define W25Q64JV_CMD_FAST_READ_QUAD 0x6B /* 四线快速读取 1-1-4 */
#define W25Q64JV_CMD_FAST_READ_DUAL_IO 0xBB /* 双线快速I/O读取 1-2-2 */
#define W25Q64JV_CMD_FAST_READ_QUAD_IO 0xEB /* 四线快速I/O读取 1-4-4 */
在 .c 中编写函数:
/******************************************************************
* 函 数 名 称: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 FlashCMD = {0};
FlashCMD.DdrMode = QSPI_DDR_MODE_DISABLE; /* 关闭DDR模式 */
FlashCMD.DdrHoldHalfCycle = QSPI_DDR_HHC_ANALOG_DELAY; /* DDR模式下的时钟延迟,因为关闭了DDR模式所以不管 */
FlashCMD.InstructionMode = QSPI_INSTRUCTION_1_LINE; /* 一线指令模式 */
FlashCMD.AddressMode = QSPI_ADDRESS_4_LINES; /* 四线地址模式 */
FlashCMD.DataMode = QSPI_DATA_4_LINES; /* 四线数据模式 */
FlashCMD.AlternateByteMode = QSPI_ALTERNATE_BYTES_4_LINES; /* 四线交替字节模式 */
FlashCMD.AlternateBytesSize = QSPI_ALTERNATE_BYTES_8_BITS; /* 8位交替字节 */
FlashCMD.AlternateBytes = 0x00; /* 模式字节 */
FlashCMD.AddressSize = QSPI_ADDRESS_24_BITS; /* 24位地址 */
FlashCMD.SIOOMode = QSPI_SIOO_INST_EVERY_CMD; /* 每次传输时发送指令 */
FlashCMD.Instruction = W25Q64JV_CMD_FAST_READ_QUAD_IO; /* 四线快速I/O读取命令 1-4-4 */
FlashCMD.Address = startAddr; /* 读取数据的起始地址 */
FlashCMD.NbData = totalSize; /* 读取数据的大小 */
/* 这个需要根据实际测试结果进行调整 */
FlashCMD.DummyCycles = DUMMY_CYCLES_FAST_READ_QUAD_IO; /* 四线快速I/O读取时指令空闲周期数 */
/* 发送命令 */
if (HAL_QSPI_Command(&hqspi, &FlashCMD, 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;
}
用于快速读取 Flash 数据,支持一次读取任意长度(可超过256字节页长),只要不超过芯片总容量。利用 QSPI 四线模式和“快速读”命令,大幅提升读取速度。
- 使用QSPI四线模式,速度远高于普通SPI读取(单线模式)。
- 支持跨页读取,不受256字节页长限制,适合大量数据一次性读取。
- DummyCycles需要根据主控和Flash速度调整,否则容易读错数据。
- 适合固件升级、资源加载等对速度和批量数据有要求的场景。
测试程序编写
1、Debug串口重定向
/* retarget the C library printf function to the USART */
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
return ch;
}
2、读写测试
/* Demo:测试 QSPI-Flash的读写
步骤:
1. 读取 JEDEC ID
2. 擦除64K块 -> 写一页 (32字节)
3. 直接读取数据查看相关的结果
*/
static void QSPI_TestReadWrite(void)
{
#define QSPI_TEST_ADDR 0x000000 /* 测试地址: 40KB处 (4KB对齐) */
#define QSPI_TEST_LEN 32 /* 测试数据长度 */
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) {
printf("Read ID Fail\r\n");
return;
}
printf("JEDEC ID: %02X %02X %02X\r\n", mid,did,uid);
/* 准备写入数据模式递增 */
for (i = 0; i < QSPI_TEST_LEN; i++) {
tx[i] = (uint8_t)(0xA0 + i);
}
printf("Erase 4K Sector 0x%06X ...\r\n", QSPI_TEST_ADDR);
if (QSPI_EraseFlashSector_4KB(QSPI_TEST_ADDR) != 0) { printf("Erase Fail\r\n"); return; }
printf("Page Program...\r\n");
printf("Writing %d bytes to address 0x%06X\r\n", QSPI_TEST_LEN, QSPI_TEST_ADDR);
if (QSPI_WriteFlash(QSPI_TEST_ADDR,tx,QSPI_TEST_LEN) != 0) { printf("Program Fail\r\n"); return; }
if (QSPI_ReadFlash(QSPI_TEST_ADDR, rx, QSPI_TEST_LEN) != 0) {
printf("Read Flash Fail\r\n");
return;
}
printf("TX:\r\n"); for(i = 0; i < QSPI_TEST_LEN; i++) { printf("%02X ", tx[i]); }
printf("\r\n");
printf("RX:\r\n"); for(i = 0; i < QSPI_TEST_LEN; i++) { printf("%02X ", rx[i]); }
printf("\r\n");
int diff = 0; for (i = 0; i < QSPI_TEST_LEN; i++) { if (tx[i] != rx[i]) { diff = 1; break; } }
printf(diff?"COMPARE: FAIL\r\n":"COMPARE: OK\r\n");
}
main.c的代码如下:
/* 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 "bsp_qspi_w25q64jv.h"
#include <stdio.h>
#include <string.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 */
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
static void QSPI_TestReadWrite(void);
void Simple_Delay_Ms(uint32_t ms);
/* 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 */
/* 初始化QSPI Flash */
if (BSP_QSPI_W25Q64JV_Init() == 0) {
printf("QSPI Flash initialized successfully!\r\n");
/* 运行QSPI测试演示 */
QSPI_TestReadWrite();
} else {
printf("QSPI Flash initialization failed!\r\n");
}
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
// HAL_Delay(1000);
HAL_GPIO_WritePin(USER_LED_GPIO_Port, USER_LED_Pin, GPIO_PIN_SET);
}
/* 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 */
/* Demo:测试 QSPI-Flash的读写
步骤:
1. 读取 JEDEC ID
2. 擦除64K块 -> 写一页 (32字节)
3. 直接读取数据查看相关的结果
*/
static void QSPI_TestReadWrite(void)
{
#define QSPI_TEST_ADDR 0x000000 /* 测试地址: 40KB处 (4KB对齐) */
#define QSPI_TEST_LEN 32 /* 测试数据长度 */
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) {
printf("Read ID Fail\r\n");
return;
}
printf("JEDEC ID: %02X %02X %02X\r\n", mid,did,uid);
/* 准备写入数据模式递增 */
for (i = 0; i < QSPI_TEST_LEN; i++) {
tx[i] = (uint8_t)(0xA0 + i);
}
printf("Erase 4K Sector 0x%06X ...\r\n", QSPI_TEST_ADDR);
if (QSPI_EraseFlashSector_4KB(QSPI_TEST_ADDR) != 0) { printf("Erase Fail\r\n"); return; }
printf("Page Program...\r\n");
printf("Writing %d bytes to address 0x%06X\r\n", QSPI_TEST_LEN, QSPI_TEST_ADDR);
if (QSPI_WriteFlash(QSPI_TEST_ADDR,tx,QSPI_TEST_LEN) != 0) { printf("Program Fail\r\n"); return; }
if (QSPI_ReadFlash(QSPI_TEST_ADDR, rx, QSPI_TEST_LEN) != 0) {
printf("Read Flash Fail\r\n");
return;
}
printf("TX:\r\n"); for(i = 0; i < QSPI_TEST_LEN; i++) { printf("%02X ", tx[i]); }
printf("\r\n");
printf("RX:\r\n"); for(i = 0; i < QSPI_TEST_LEN; i++) { printf("%02X ", rx[i]); }
printf("\r\n");
int diff = 0; for (i = 0; i < QSPI_TEST_LEN; i++) { if (tx[i] != rx[i]) { diff = 1; break; } }
printf(diff?"COMPARE: FAIL\r\n":"COMPARE: OK\r\n");
}
/* retarget the C library printf function to the USART */
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
return ch;
}
/* 简易延时函数 */
void Simple_Delay_Ms(uint32_t ms)
{
/* 系统时钟为480MHz,每个循环大约消耗几个时钟周期 */
/* 这个值需要根据实际系统频率和编译器优化级别调整 */
volatile uint32_t delay_count = ms * 120000; /* 大概1ms的延时 */
while (delay_count--) {
__NOP();
}
}
/* 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输出的数据我们可以发现,TX 和 RX 的数据是一致的,也就是说明读取和写入的内容完全OK!

至此读写测试就全部完成了。
工程代码
更多推荐

所有评论(0)