【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 ID2. 擦除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 ID2. 擦除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!
至此读写测试就全部完成了。