当前位置: 首页 > news >正文

【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、引脚定义

从数据手册中我们可以看到相关的引脚设置

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

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,我们不超过即可。
  • 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、配置时钟

  1. 选择 HSE​
  1. 选择 Enable CSS​
  2. 填写 480​
  3. 回车,直接让 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​ 进行写入或擦除操作前,必须先执行 “ 写使能 ” 操作。写使能的流程如下:

  1. 发送写使能指令 0x06​ 向 Flash 发送 0x06​ 指令,使能后续的写/擦除操作。
  2. 轮询状态寄存器 轮询状态寄存器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)被置位,确保写使能命令已经生效。

  • 实现流程

    1. 构造 QSPI 命令结构体,设置为一线指令模式,指令为读取状态寄存器1(0x05),无地址,一线数据模式。
    2. 构造自动轮询配置结构体,只关心 WEL 位(Mask=0x02),匹配条件为 WEL=1(Match=0x02),其它参数如轮询间隔、匹配模式等根据实际设置。
    3. 调用 HAL_QSPI_AutoPolling​ 自动轮询芯片状态,直到 WEL 位被置位或超时。
    4. 成功返回 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)被置位,以允许后续的写入或擦除操作。

  • 实现流程

    1. 构造 QSPI 命令结构体,设置为一线指令模式,指令为写使能命令(0x06),无地址、无数据模式。
    2. 调用 HAL_QSPI_Command​ 发送写使能命令。
    3. 命令发送成功后,调用 QSPI_PollingWEL​ 轮询状态寄存器,等待 WEL 位被置位(即写使能生效)。
    4. 若整个过程成功,返回 0,否则返回 1。
  • 典型用法: 写入或擦除 Flash 前,需先调用此函数以保证芯片处于可写状态。

2、使能四线模式

Flash芯片上电默认是普通SPI模式,也没有使能四线读写,所以我们需要设置下Flash的模式。

从Flash的数据手册中可知:若需使用QSPI四线读写,须先使能QE(Quad Enable)位。

从状态寄存器中可以得知:QE 的使能位在 24 个 Bit 中的 第 9 位(S9)。

而且,我们看到 Volatile/Non-Volatile Writable​ 这个标志,说明 QE 位可通过非易失性(OTP)和易失性两种方式写入。通常使用非易失性写入(即掉电不丢失)。

所以我们可以将流程这样设计:

  1. 读取 SR2,检查 QE 是否已置位。
  2. 如果未置位,先写使能(WREN)。
  3. 写 SR2,把 QE 位置 1。
  4. 轮询 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!

至此读写测试就全部完成了。

工程代码

http://www.dtcms.com/a/412017.html

相关文章:

  • 成都青羊网站建设网页升级防问广大
  • Altium Designer 21 (十)DRC验证和设计文件输出
  • dw手机销售网站制作seo 网站制作
  • 【精品资料鉴赏】104页银行核心业务流程梳理和优化方案
  • 磁共振成像原理(理论)13:选层 (Slice Select) -布洛赫方法
  • wordpress 文章新窗口打开专业关键词排名优化软件
  • 【JAVA高级】接口响应慢?用 CompletableFuture 优化。
  • springboot大学校园旧物捐赠网站(代码+数据库+LW)
  • 廊坊网站建设小公司有必要买财务软件吗
  • 北京网站排名公司志愿海南网站
  • 【Android之路】图片无障碍化、文本易翻译初步和R类
  • 解决Compile Run插件运行c/c++中文乱码问题
  • 深圳做营销网站公司简介网站口碑推广
  • 网站流量是如何计算的wordpress资讯站
  • 做网站的范本深圳58同城招聘网
  • 深入浅出高并发内存池:原理、设计与实现
  • 0926第一个口头OC——快手主站前端
  • 网站职业技术培训学校广告设计公司深圳策划设计公司
  • A股大盘数据-20250926分析
  • 振动力学|01 单自由度系统的振动分析
  • 【Luogu_P2184】 贪婪大陆【树状数组】
  • 太原网站制作网站建设相关岗位名称
  • phpstorm content.dat.storageData 文件解析:作用、风险与处理建议
  • 做网站要学一些什么一学一做教育视频网站有哪些内容
  • 基于华为openEuler部署Blog轻量级博客系统
  • 西安企业建站费用互联网营销外包推广
  • 怎样做平台网站制作表白网页
  • 【系统架构设计(37)】数据库体系结构
  • 八爪鱼网络网站建设哪里有做商城的网站
  • 网站历史记录怎么恢复百度推广服务