【工具使用】STM32CubeMX-QSPI配置-实现NorFlash读写

一、概述

无论是新手还是大佬,基于STM32单片机的开发,使用STM32CubeMX都是可以极大提升开发效率的,并且其界面化的开发,也大大降低了新手对STM32单片机的开发门槛。

本文主要讲述STM32芯片QSPI功能的配置及其相关知识。

二、软件说明

STM32CubeMX是ST官方出的一款针对ST的MCU/MPU跨平台的图形化工具,支持在Linux、MacOS、Window系统下开发,其对接的底层接口是HAL库,另外习惯于寄存器开发的同学们,也可以使用LL库。STM32CubeMX除了集成MCU/MPU的硬件抽象层,另外还集成了像RTOS,文件系统,USB,网络,显示,嵌入式AI等中间件,这样开发者就能够很轻松的完成MCU/MPU的底层驱动的配置,留出更多精力开发上层功能逻辑,能够更进一步提高了嵌入式开发效率。

演示版本 6.9.0

三、QSPI简介

QSPI(Quad SPI,四线串行外设接口)是一种常用的串行外部存储器接口,用于连接微控制器和外部闪存存储器。它是SPI(Serial Peripheral Interface,串行外设接口)的一种变种,通过增加数据线的数量,提高了数据传输速度。

QSPI使用四根线进行全双工的数据传输,包括一个时钟线、一个数据输入线、一个数据输出线和一个片选线。通过在每个时钟周期内同时传输四个数据位,QSPI能够提供更高的数据传输速度和带宽,相比传统的SPI接口,具有更高的性能。

QSPI广泛应用于存储器扩展、快速数据存取以及固件更新等领域。它可以连接各种外部闪存存储器,如串行闪存(Serial Flash),用于存储程序代码、配置数据等。

QSPI是SPI的一种变体,除了标准SPI和QSPI外,还有DSPI和OSPI两种SPI总线变体,下面表格列出了这四种接口的特点及差异。

这里来看下STM32H750中QSPI的外设描述。

四、QSPI配置

这是一段 嵌入式 / 硬件配置参数(General Parameters) ,常见于 MCU(微控制器单元)或 Flash 存储控制器的配置,各参数含义如下:

  1. Clock Prescaler(时钟预分频器)
    值:255
    作用:对时钟信号分频,降低时钟频率,用于控制通信速率(如 SPI 总线时钟),数值越大,实际工作时钟越低,通信越慢但可能更稳定。
  2. Fifo Threshold(FIFO 阈值)
    值:1
    作用:FIFO(先入先出队列)的触发阈值,当数据量达到 1 时,可能触发中断 / 数据传输,用于控制数据缓冲的触发条件。
  3. Sample Shifting(采样移位)
    值:No Sample Shifting
    作用:控制数据采样的时序偏移,"No Sample Shifting" 表示不偏移,直接按默认时序采样,确保数据对齐。
  4. Flash Size(闪存容量)
    值:1
    作用:定义 Flash 存储芯片的容量配置(需结合实际芯片,可能是按 "块 / 分区" 或逻辑容量设置 ),影响地址映射、读写范围。
  5. Chip Select High Time(片选高电平时间)
    值:1 Cycle
    作用:控制 "片选信号(CS)" 高电平持续周期,确保 Flash 芯片在指令 / 数据传输前后有稳定的电平时序,避免误操作。
  6. Clock Mode(时钟模式)
    值:Low
    作用:定义时钟信号的极性 / 相位(如 SPI 时钟模式),"Low" 通常指空闲时钟电平为低,配合采样边沿(如上升沿 / 下降沿)决定通信时序。
  7. Flash ID(闪存标识)
    值:Flash ID 1
    作用:区分不同 Flash 芯片(多芯片系统中),通过 ID 识别硬件,加载对应驱动 / 配置,确保适配芯片型号。
  8. Dual Flash(双闪存)
    值:Disabled
    作用:关闭 "双闪存模式",仅单 Flash 工作;使能后可支持双芯片并行 / 冗余,提升存储容量或可靠性。
    这些参数共同控制 Flash 存储与 MCU 的通信时序、数据缓冲、硬件适配 ,需结合具体芯片手册(如 SPI 控制器、Flash 型号)调整,确保读写稳定。

五、应用配置

5.1 端口配置

这里用的是正点原子的ALIENTEK北极星STM32H750开发板,查看QSPI相关的原理图如下,可以看到这里需要用到6个引脚,分别是QSPI_BK1_NCS(PB6)QSPI_BK1_CLK(PB2)QSPI_BK1_IO1(PF9)QSPI_BK1_IO2(PF8)QSPI_BK1_IO3(PF7)QSPI_BK1_IO4(PF6)。要注意原理图这里虽然写的是W25Q32,但实际读出来的FlashID表示这是一个W25Q64的芯片,大家要根据实际情况调整。


  • CubeMX端口配置

5.2 QSPI配置

Clock Prescaler: 查了下W25Q64的芯片,标准SPI模式最高频率可以到80M,这里配置成4分频,即50M。
Fifo Threshold: 4 字节刚好和指令包对齐,DMA 一次搬运刚好完成 1 次指令收发,搬运效率最高。
Sample Shifting: 当前SPI频率配置是50M,因为W25Q64数据输出延迟约 6~8ns,加上走线延迟,默认采样点会采到不稳定的跳变沿数据,所以这里配置Sample Shifting Half Cycle是为了延时半个时钟周期采样(10ns),让采样的位置处在稳定状态下,不至于刚好采到跳变时的值。建议频率高于40M的要开启这个功能。
Flash Size: 因为用的是W25Q64,其容量为8M,即最大地址位需要23位,这里配置为24位。
Chip Select High Time: 手册里还要求两次 SPI 指令之间,片选 CS 拉高后必须维持的最小时长为50ns,根据当前50M的频率计算,Chip Select High Time设置5个时钟周期即为100ns,大于手册要求的50ns。

因为GPIO端口是操作Flash芯片的,按照配置频率可以达到50M,所以GPIO速率得调成Very High

六、接口及应用

w25qxx芯片驱动

c 复制代码
/**
  ******************************************************************************
  * @file    w25qxx.c
  * @brief   W25Q64 QSPI Flash 驱动实现
  * @note    基于 STM32H750 HAL QSPI,Quad SPI 模式
  *
  *  数据线模式说明:
  *   1-1-1:  标准 SPI 模式  (指令1线 / 地址1线 / 数据1线)
  *   1-1-4:  Quad Output    (指令1线 / 地址1线 / 数据4线)
  *   1-4-4:  Quad I/O       (指令1线 / 地址4线 / 数据4线)
  *
  *  W25Q64 时序 (QSPI_CLK = 200MHz / Prescaler8 = 25MHz):
  *   扇区擦除: 典型 45ms, 最大 400ms
  *   页编程:   典型 0.7ms, 最大 3ms
  *   Quad读:   6B cmd, 8 dummy cycles
  ******************************************************************************
  */

/* Includes ------------------------------------------------------------------*/
#include "w25qxx.h"

/* ======================== 外部变量 ======================================== */
extern QSPI_HandleTypeDef hqspi;

/* ======================== 调试变量 =========================================
 * 在 debugger 中查看这些变量快速定位问题
 *
 * g_debug_step:       最后执行的操作步骤代码
 * g_debug_hal_status: HAL 函数返回的 HAL_StatusTypeDef
 * g_debug_error_code: hqspi.ErrorCode (HAL QSPI 错误码)
 *
 * 关键步骤码:
 *   0    = 空闲/成功
 *   1-9  = WritePage 流程步骤
 *   10   = QSPI_WriteEnable 入口
 *   20   = EraseSector: 地址越界
 *   21   = EraseSector: 写使能失败
 *   22   = EraseSector: 擦除命令失败
 *   23   = EraseSector: 等待完成超时
 *   50   = WritePage: 页边界越界
 *   51   = WritePage: 写使能失败
 *   52   = WritePage: 命令发送失败
 *   53   = WritePage: 数据发送失败
 *   54   = WritePage: 轮询超时
 *   90   = QSPI_WriteEnable 内部: 命令失败
 *   100  = Init: 复位失败
 *   101  = Init: Quad模式配置失败
 *   102  = Init: ID读取失败
 *   103  = Init: ID不匹配
 */
volatile uint32_t g_debug_step        = 0;
volatile uint32_t g_debug_hal_status   = 0;
volatile uint32_t g_debug_error_code   = 0;

/* 调试辅助宏 */
#define DBG_SET(s)        do { g_debug_step = (s); } while(0)
#define DBG_HAL(s, r)     do { g_debug_step = (s); g_debug_hal_status = (uint32_t)(r); } while(0)
#define DBG_ERR(s)        do { g_debug_step = (s); g_debug_error_code = hqspi.ErrorCode; } while(0)

/* ======================== 芯片信息 ======================================== */
W25Qxx_InfoTypeDef w25qxx_info = {
    .FlashSize           = W25QXX_FLASH_SIZE,
    .EraseSectorSize     = W25QXX_SECTOR_SIZE,
    .EraseSectorNumber   = W25QXX_FLASH_SIZE / W25QXX_SECTOR_SIZE,
    .EraseBlock32KSize   = W25QXX_BLOCK_SIZE_32K,
    .EraseBlock32KNumber = W25QXX_FLASH_SIZE / W25QXX_BLOCK_SIZE_32K,
    .EraseBlock64KSize   = W25QXX_BLOCK_SIZE_64K,
    .EraseBlock64KNumber = W25QXX_FLASH_SIZE / W25QXX_BLOCK_SIZE_64K,
    .PageSize            = W25QXX_PAGE_SIZE,
    .PageNumber          = W25QXX_FLASH_SIZE / W25QXX_PAGE_SIZE,
};

/* ======================== 私有函数声明 ==================================== */
static uint8_t  QSPI_WriteEnableCmd(void);
static uint8_t  QSPI_AutoPollBusy(uint32_t TimeoutMs);
static uint8_t  QSPI_ResetMemory(void);
static uint8_t  QSPI_EnableQuadMode(void);
static void     QSPI_EnsureReady(void);

/* ======================================================================== */
/*                        公共 API 实现                                      */
/* ======================================================================== */

/**
  * @brief  初始化 W25Q64 Flash
  * @note   流程: 复位 -> Quad模式使能 -> JEDEC ID验证
  * @retval 0: 成功  |  1: 复位失败  |  2: ID不匹配
  *         3: ID读取失败  |  4: Quad配置失败
  */
uint8_t W25Qxx_Init(void)
{
    uint32_t jedec_id;

    /* 1. 中止任何挂起操作 */
    QSPI_EnsureReady();

    /* 2. 复位芯片 */
    if (QSPI_ResetMemory() != 0)
    {
        DBG_SET(100);
        return 1;
    }
    HAL_Delay(10);

    /* 3. 使能 Quad 模式 (写入须先于 Quad 操作) */
    if (QSPI_EnableQuadMode() != 0)
    {
        DBG_SET(101);
        return 4;
    }

    /* 4. 读取 JEDEC ID 验证 */
    jedec_id = W25Qxx_ReadID();
    if (jedec_id == 0)
    {
        DBG_SET(102);
        return 3;
    }
    if (jedec_id != W25Q64_JEDEC_ID)
    {
        DBG_SET(103);
        return 2;
    }

    DBG_SET(0);
    return 0;
}

/**
  * @brief  读取 JEDEC ID
  * @retval 32位 ID (0xEF4017 = W25Q64),失败返回 0
  */
uint32_t W25Qxx_ReadID(void)
{
    QSPI_CommandTypeDef cmd;
    uint8_t buf[3] = {0};
    HAL_StatusTypeDef ret;

    QSPI_EnsureReady();

    memset(&cmd, 0, sizeof(cmd));
    cmd.InstructionMode   = QSPI_INSTRUCTION_1_LINE;
    cmd.Instruction       = W25QXX_CMD_READ_JEDEC_ID;
    cmd.AddressMode       = QSPI_ADDRESS_NONE;
    cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
    cmd.DataMode          = QSPI_DATA_1_LINE;
    cmd.DummyCycles       = 0;
    cmd.DdrMode           = QSPI_DDR_MODE_DISABLE;
    cmd.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;
    cmd.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;
    cmd.NbData            = 3;

    ret = HAL_QSPI_Command(&hqspi, &cmd, W25QXX_CMD_TIMEOUT);
    if (ret != HAL_OK)
    {
        return 0;
    }

    ret = HAL_QSPI_Receive(&hqspi, buf, W25QXX_CMD_TIMEOUT);
    if (ret != HAL_OK)
    {
        return 0;
    }

    return ((uint32_t)buf[0] << 16) | ((uint32_t)buf[1] << 8) | (uint32_t)buf[2];
}

/**
  * @brief  读取唯一 ID (8字节)
  */
void W25Qxx_ReadUID(uint8_t *pUID)
{
    QSPI_CommandTypeDef cmd;

    if (pUID == NULL) return;

    QSPI_EnsureReady();

    memset(&cmd, 0, sizeof(cmd));
    cmd.InstructionMode   = QSPI_INSTRUCTION_1_LINE;
    cmd.Instruction       = W25QXX_CMD_READ_UNIQUE_ID;
    cmd.AddressMode       = QSPI_ADDRESS_NONE;
    cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
    cmd.DataMode          = QSPI_DATA_1_LINE;
    cmd.DummyCycles       = 32;  /* W25Q64 UID 需要 4 字节 dummy */
    cmd.DdrMode           = QSPI_DDR_MODE_DISABLE;
    cmd.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;
    cmd.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;
    cmd.NbData            = 8;

    if (HAL_QSPI_Command(&hqspi, &cmd, W25QXX_CMD_TIMEOUT) != HAL_OK)
        return;

    HAL_QSPI_Receive(&hqspi, pUID, W25QXX_CMD_TIMEOUT);
}

/**
  * @brief  读取状态寄存器
  * @param  reg_num: 1 = SR1, 2 = SR2
  * @retval 寄存器值
  */
uint8_t W25Qxx_ReadStatusReg(uint8_t reg_num)
{
    QSPI_CommandTypeDef cmd;
    uint8_t val = 0;

    QSPI_EnsureReady();

    memset(&cmd, 0, sizeof(cmd));
    cmd.InstructionMode   = QSPI_INSTRUCTION_1_LINE;
    cmd.Instruction       = (reg_num == 2) ? W25QXX_CMD_READ_STATUS_REG2
                                           : W25QXX_CMD_READ_STATUS_REG;
    cmd.AddressMode       = QSPI_ADDRESS_NONE;
    cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
    cmd.DataMode          = QSPI_DATA_1_LINE;
    cmd.DummyCycles       = 0;
    cmd.DdrMode           = QSPI_DDR_MODE_DISABLE;
    cmd.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;
    cmd.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;
    cmd.NbData            = 1;

    if (HAL_QSPI_Command(&hqspi, &cmd, W25QXX_CMD_TIMEOUT) != HAL_OK)
        return 0;

    HAL_QSPI_Receive(&hqspi, &val, W25QXX_CMD_TIMEOUT);
    return val;
}

/* ======================================================================== */
/*                          擦除操作                                         */
/* ======================================================================== */

/**
  * @brief  擦除单个扇区 (4KB)
  * @param  SectorAddr: 扇区编号 [0, 2047]
  */
void W25Qxx_EraseSector(uint32_t SectorAddr)
{
    QSPI_CommandTypeDef cmd;
    uint32_t addr;

    /* 边界检查 */
    if (SectorAddr >= w25qxx_info.EraseSectorNumber)
    {
        DBG_SET(20);
        return;
    }

    addr = SectorAddr * W25QXX_SECTOR_SIZE;

    QSPI_EnsureReady();

    /* 写使能 */
    if (QSPI_WriteEnableCmd() != 0)
    {
        DBG_SET(21);
        return;
    }

    /* 发送擦除命令 */
    memset(&cmd, 0, sizeof(cmd));
    cmd.InstructionMode   = QSPI_INSTRUCTION_1_LINE;
    cmd.Instruction       = W25QXX_CMD_SECTOR_ERASE;
    cmd.AddressMode       = QSPI_ADDRESS_1_LINE;
    cmd.AddressSize       = QSPI_ADDRESS_24_BITS;
    cmd.Address           = addr;
    cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
    cmd.DataMode          = QSPI_DATA_NONE;
    cmd.DummyCycles       = 0;
    cmd.DdrMode           = QSPI_DDR_MODE_DISABLE;
    cmd.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;
    cmd.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;
    cmd.NbData            = 0;

    if (HAL_QSPI_Command(&hqspi, &cmd, W25QXX_CMD_TIMEOUT) != HAL_OK)
    {
        DBG_ERR(22);
        return;
    }

    /* 等待擦除完成 */
    if (QSPI_AutoPollBusy(W25QXX_SECTOR_ERASE_TIMEOUT) != 0)
    {
        DBG_SET(23);
        return;
    }
}

/**
  * @brief  擦除 32KB 块
  */
void W25Qxx_EraseBlock32K(uint32_t BlockAddr)
{
    QSPI_CommandTypeDef cmd;
    uint32_t addr;

    if (BlockAddr >= w25qxx_info.EraseBlock32KNumber) return;
    addr = BlockAddr * W25QXX_BLOCK_SIZE_32K;

    QSPI_EnsureReady();

    if (QSPI_WriteEnableCmd() != 0) return;

    memset(&cmd, 0, sizeof(cmd));
    cmd.InstructionMode   = QSPI_INSTRUCTION_1_LINE;
    cmd.Instruction       = W25QXX_CMD_BLOCK_ERASE_32KB;
    cmd.AddressMode       = QSPI_ADDRESS_1_LINE;
    cmd.AddressSize       = QSPI_ADDRESS_24_BITS;
    cmd.Address           = addr;
    cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
    cmd.DataMode          = QSPI_DATA_NONE;
    cmd.DummyCycles       = 0;
    cmd.DdrMode           = QSPI_DDR_MODE_DISABLE;
    cmd.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;
    cmd.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;
    cmd.NbData            = 0;

    if (HAL_QSPI_Command(&hqspi, &cmd, W25QXX_CMD_TIMEOUT) != HAL_OK) return;

    QSPI_AutoPollBusy(W25QXX_BLOCK32K_ERASE_TIMEOUT);
}

/**
  * @brief  擦除 64KB 块
  */
void W25Qxx_EraseBlock64K(uint32_t BlockAddr)
{
    QSPI_CommandTypeDef cmd;
    uint32_t addr;

    if (BlockAddr >= w25qxx_info.EraseBlock64KNumber) return;
    addr = BlockAddr * W25QXX_BLOCK_SIZE_64K;

    QSPI_EnsureReady();

    if (QSPI_WriteEnableCmd() != 0) return;

    memset(&cmd, 0, sizeof(cmd));
    cmd.InstructionMode   = QSPI_INSTRUCTION_1_LINE;
    cmd.Instruction       = W25QXX_CMD_BLOCK_ERASE_64KB;
    cmd.AddressMode       = QSPI_ADDRESS_1_LINE;
    cmd.AddressSize       = QSPI_ADDRESS_24_BITS;
    cmd.Address           = addr;
    cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
    cmd.DataMode          = QSPI_DATA_NONE;
    cmd.DummyCycles       = 0;
    cmd.DdrMode           = QSPI_DDR_MODE_DISABLE;
    cmd.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;
    cmd.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;
    cmd.NbData            = 0;

    if (HAL_QSPI_Command(&hqspi, &cmd, W25QXX_CMD_TIMEOUT) != HAL_OK) return;

    QSPI_AutoPollBusy(W25QXX_BLOCK64K_ERASE_TIMEOUT);
}

/**
  * @brief  整片擦除 (耗时约 40 秒)
  */
void W25Qxx_EraseChip(void)
{
    QSPI_CommandTypeDef cmd;

    QSPI_EnsureReady();

    if (QSPI_WriteEnableCmd() != 0) return;

    memset(&cmd, 0, sizeof(cmd));
    cmd.InstructionMode   = QSPI_INSTRUCTION_1_LINE;
    cmd.Instruction       = W25QXX_CMD_CHIP_ERASE;
    cmd.AddressMode       = QSPI_ADDRESS_NONE;
    cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
    cmd.DataMode          = QSPI_DATA_NONE;
    cmd.DummyCycles       = 0;
    cmd.DdrMode           = QSPI_DDR_MODE_DISABLE;
    cmd.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;
    cmd.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;
    cmd.NbData            = 0;

    if (HAL_QSPI_Command(&hqspi, &cmd, W25QXX_CMD_TIMEOUT) != HAL_OK) return;

    QSPI_AutoPollBusy(W25QXX_CHIP_ERASE_TIMEOUT);
}

/* ======================================================================== */
/*                          写入操作                                         */
/* ======================================================================== */

/**
  * @brief  四线页编程 (单页内写入)
  * @note   使用 Quad Input Page Program (0x32) 命令
  *         写完后自动等待 BUSY 位清除
  * @param  pData:    数据缓冲区
  * @param  PageAddr: 页编号 [0, 32767]
  * @param  Offset:   页内偏移 [0, 255]
  * @param  Size:     写入字节数 (Offset+Size <= 256)
  * @retval 0:成功  1:超出页边界  2:写使能失败
  *         3:命令失败  4:数据发送失败  5:等待超时
  */
uint8_t W25Qxx_WritePage(uint8_t *pData, uint32_t PageAddr, uint32_t Offset, uint32_t Size)
{
    QSPI_CommandTypeDef cmd;

    /* 参数校验 */
    if (pData == NULL || Size == 0) return 0;
    if ((Offset + Size) > W25QXX_PAGE_SIZE)
    {
        DBG_SET(50);
        return 1;
    }

    QSPI_EnsureReady();

    /* 写使能 */
    if (QSPI_WriteEnableCmd() != 0)
    {
        DBG_SET(51);
        return 2;
    }

    /* 配置 Quad Page Program 命令 */
    memset(&cmd, 0, sizeof(cmd));
    cmd.InstructionMode   = QSPI_INSTRUCTION_1_LINE;
    cmd.Instruction       = W25QXX_CMD_QUAD_PAGE_PROGRAM;  /* 0x32 */
    cmd.AddressMode       = QSPI_ADDRESS_1_LINE;
    cmd.AddressSize       = QSPI_ADDRESS_24_BITS;
    cmd.Address           = PageAddr * W25QXX_PAGE_SIZE + Offset;
    cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
    cmd.DataMode          = QSPI_DATA_4_LINES;             /* 四线数据 */
    cmd.DummyCycles       = 0;
    cmd.DdrMode           = QSPI_DDR_MODE_DISABLE;
    cmd.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;
    cmd.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;
    cmd.NbData            = Size;

    if (HAL_QSPI_Command(&hqspi, &cmd, W25QXX_CMD_TIMEOUT) != HAL_OK)
    {
        DBG_ERR(52);
        return 3;
    }

    /* 发送数据 (4线) */
    if (HAL_QSPI_Transmit(&hqspi, pData, W25QXX_CMD_TIMEOUT) != HAL_OK)
    {
        DBG_ERR(53);
        return 4;
    }

    /* 等待编程完成 */
    if (QSPI_AutoPollBusy(W25QXX_PAGE_PROGRAM_TIMEOUT) != 0)
    {
        DBG_SET(54);
        return 5;
    }

    DBG_SET(0);
    return 0;
}

/**
  * @brief  扇区内写入 (自动处理跨页)
  * @note   先擦除扇区再分页写入
  */
uint8_t W25Qxx_WriteSector(uint8_t *pData, uint32_t SectorAddr, uint32_t Offset, uint32_t Size)
{
    uint32_t addr;
    uint32_t remain;
    uint32_t page_offset;
    uint32_t page_remain;
    uint32_t write_size;
    uint8_t  *pCur;
    uint8_t  ret;

    if (pData == NULL || Size == 0) return 0;
    if ((Offset + Size) > W25QXX_SECTOR_SIZE) return 1;

    addr   = SectorAddr * W25QXX_SECTOR_SIZE + Offset;
    remain = Size;
    pCur   = pData;

    while (remain > 0)
    {
        page_offset = addr % W25QXX_PAGE_SIZE;
        page_remain = W25QXX_PAGE_SIZE - page_offset;
        write_size  = (remain < page_remain) ? remain : page_remain;

        ret = W25Qxx_WritePage(pCur, addr / W25QXX_PAGE_SIZE, page_offset, write_size);
        if (ret != 0) return ret;

        pCur   += write_size;
        addr   += write_size;
        remain -= write_size;
    }

    return 0;
}

/* ======================================================================== */
/*                          读取操作                                         */
/* ======================================================================== */

/**
  * @brief  标准读取 (1线 SPI, 命令 0x03)
  */
void W25Qxx_ReadData(uint8_t *pData, uint32_t ReadAddr, uint32_t Size)
{
    QSPI_CommandTypeDef cmd;

    if (pData == NULL || Size == 0) return;

    QSPI_EnsureReady();

    memset(&cmd, 0, sizeof(cmd));
    cmd.InstructionMode   = QSPI_INSTRUCTION_1_LINE;
    cmd.Instruction       = W25QXX_CMD_READ_DATA;
    cmd.AddressMode       = QSPI_ADDRESS_1_LINE;
    cmd.AddressSize       = QSPI_ADDRESS_24_BITS;
    cmd.Address           = ReadAddr;
    cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
    cmd.DataMode          = QSPI_DATA_1_LINE;
    cmd.DummyCycles       = 0;
    cmd.DdrMode           = QSPI_DDR_MODE_DISABLE;
    cmd.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;
    cmd.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;
    cmd.NbData            = Size;

    if (HAL_QSPI_Command(&hqspi, &cmd, W25QXX_CMD_TIMEOUT) != HAL_OK) return;

    HAL_QSPI_Receive(&hqspi, pData, W25QXX_CMD_TIMEOUT);
}

/**
  * @brief  四线快速读取 (命令 0x6B, Quad Output Fast Read)
  * @note   指令1线 / 地址1线 / 8个dummy周期 / 数据4线
  */
void W25Qxx_FastReadData(uint8_t *pData, uint32_t ReadAddr, uint32_t Size)
{
    QSPI_CommandTypeDef cmd;

    if (pData == NULL || Size == 0) return;

    QSPI_EnsureReady();

    memset(&cmd, 0, sizeof(cmd));
    cmd.InstructionMode   = QSPI_INSTRUCTION_1_LINE;
    cmd.Instruction       = W25QXX_CMD_FAST_READ_QUAD;   /* 0x6B */
    cmd.AddressMode       = QSPI_ADDRESS_1_LINE;
    cmd.AddressSize       = QSPI_ADDRESS_24_BITS;
    cmd.Address           = ReadAddr;
    cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
    cmd.DataMode          = QSPI_DATA_4_LINES;            /* 四线接收数据 */
    cmd.DummyCycles       = 8;                            /* 8个 dummy 周期 */
    cmd.DdrMode           = QSPI_DDR_MODE_DISABLE;
    cmd.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;
    cmd.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;
    cmd.NbData            = Size;

    if (HAL_QSPI_Command(&hqspi, &cmd, W25QXX_CMD_TIMEOUT) != HAL_OK) return;

    HAL_QSPI_Receive(&hqspi, pData, W25QXX_CMD_TIMEOUT);
}

/* ======================================================================== */
/*                          控制操作                                         */
/* ======================================================================== */

/**
  * @brief  写使能 (公共接口)
  */
uint8_t W25Qxx_WriteEnable(void)
{
    return QSPI_WriteEnableCmd();
}

/**
  * @brief  写禁止
  */
uint8_t W25Qxx_WriteDisable(void)
{
    QSPI_CommandTypeDef cmd;

    QSPI_EnsureReady();

    memset(&cmd, 0, sizeof(cmd));
    cmd.InstructionMode   = QSPI_INSTRUCTION_1_LINE;
    cmd.Instruction       = W25QXX_CMD_WRITE_DISABLE;
    cmd.AddressMode       = QSPI_ADDRESS_NONE;
    cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
    cmd.DataMode          = QSPI_DATA_NONE;
    cmd.DummyCycles       = 0;
    cmd.DdrMode           = QSPI_DDR_MODE_DISABLE;
    cmd.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;
    cmd.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;
    cmd.NbData            = 0;

    if (HAL_QSPI_Command(&hqspi, &cmd, W25QXX_CMD_TIMEOUT) != HAL_OK) return 1;

    return QSPI_AutoPollBusy(W25QXX_CMD_TIMEOUT);
}

/**
  * @brief  等待写操作完成 (轮询 BUSY 位)
  */
uint8_t W25Qxx_WaitForWriteEnd(void)
{
    return QSPI_AutoPollBusy(W25QXX_CMD_TIMEOUT);
}

/**
  * @brief  进入掉电模式
  */
void W25Qxx_EnterPowerDown(void)
{
    QSPI_CommandTypeDef cmd;

    QSPI_EnsureReady();

    memset(&cmd, 0, sizeof(cmd));
    cmd.InstructionMode   = QSPI_INSTRUCTION_1_LINE;
    cmd.Instruction       = W25QXX_CMD_ENTER_POWER_DOWN;
    cmd.AddressMode       = QSPI_ADDRESS_NONE;
    cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
    cmd.DataMode          = QSPI_DATA_NONE;
    cmd.DummyCycles       = 0;
    cmd.DdrMode           = QSPI_DDR_MODE_DISABLE;
    cmd.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;
    cmd.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;
    cmd.NbData            = 0;

    HAL_QSPI_Command(&hqspi, &cmd, W25QXX_CMD_TIMEOUT);
}

/**
  * @brief  退出掉电模式
  */
void W25Qxx_ExitPowerDown(void)
{
    QSPI_CommandTypeDef cmd;

    QSPI_EnsureReady();

    memset(&cmd, 0, sizeof(cmd));
    cmd.InstructionMode   = QSPI_INSTRUCTION_1_LINE;
    cmd.Instruction       = W25QXX_CMD_RELEASE_POWER_DOWN;
    cmd.AddressMode       = QSPI_ADDRESS_NONE;
    cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
    cmd.DataMode          = QSPI_DATA_NONE;
    cmd.DummyCycles       = 0;
    cmd.DdrMode           = QSPI_DDR_MODE_DISABLE;
    cmd.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;
    cmd.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;
    cmd.NbData            = 0;

    HAL_QSPI_Command(&hqspi, &cmd, W25QXX_CMD_TIMEOUT);
    HAL_Delay(1);
}

/**
  * @brief  使能内存映射模式 (Quad SPI, 只读)
  * @note   映射后 Flash 内容可直接通过指针访问
  *         地址 = 0x90000000 + Flash物理地址
  */
void W25Qxx_EnableMemoryMappedMode(void)
{
    QSPI_CommandTypeDef      cmd;
    QSPI_MemoryMappedTypeDef mem_cfg;

    QSPI_EnsureReady();

    memset(&cmd, 0, sizeof(cmd));
    cmd.InstructionMode   = QSPI_INSTRUCTION_1_LINE;
    cmd.Instruction       = W25QXX_CMD_FAST_READ_QUAD;
    cmd.AddressMode       = QSPI_ADDRESS_1_LINE;
    cmd.AddressSize       = QSPI_ADDRESS_24_BITS;
    cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
    cmd.DataMode          = QSPI_DATA_4_LINES;
    cmd.DummyCycles       = 8;
    cmd.DdrMode           = QSPI_DDR_MODE_DISABLE;
    cmd.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;
    cmd.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;

    mem_cfg.TimeOutPeriod       = 0x10;
    mem_cfg.TimeOutActivation   = QSPI_TIMEOUT_COUNTER_ENABLE;

    HAL_QSPI_MemoryMapped(&hqspi, &cmd, &mem_cfg);
}

/**
  * @brief  中止 QSPI 操作 (紧急恢复)
  */
void W25Qxx_Abort(void)
{
    HAL_QSPI_Abort(&hqspi);
}

/* ======================================================================== */
/*                        私有辅助函数                                       */
/* ======================================================================== */

/**
  * @brief  确保 QSPI 接口处于空闲状态
  */
static void QSPI_EnsureReady(void)
{
    if (HAL_QSPI_GetState(&hqspi) != HAL_QSPI_STATE_READY)
    {
        HAL_QSPI_Abort(&hqspi);
    }
}

/**
  * @brief  发送写使能命令 (0x06)
  * @retval 0:成功  1:失败
  */
static uint8_t QSPI_WriteEnableCmd(void)
{
    QSPI_CommandTypeDef cmd;
    HAL_StatusTypeDef   ret;

    DBG_SET(10);

    QSPI_EnsureReady();

    memset(&cmd, 0, sizeof(cmd));
    cmd.InstructionMode   = QSPI_INSTRUCTION_1_LINE;
    cmd.Instruction       = W25QXX_CMD_WRITE_ENABLE;
    cmd.AddressMode       = QSPI_ADDRESS_NONE;
    cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
    cmd.DataMode          = QSPI_DATA_NONE;
    cmd.DummyCycles       = 0;
    cmd.DdrMode           = QSPI_DDR_MODE_DISABLE;
    cmd.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;
    cmd.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;
    cmd.NbData            = 0;

    ret = HAL_QSPI_Command(&hqspi, &cmd, W25QXX_CMD_TIMEOUT);
    if (ret != HAL_OK)
    {
        DBG_HAL(90, ret);
        DBG_ERR(90);
        return 1;
    }

    return 0;
}

/**
  * @brief  硬件自动轮询 BUSY 位,等待 Flash 就绪
  * @param  TimeoutMs: 超时时间 (ms)
  * @retval 0:就绪  1:超时
  */
static uint8_t QSPI_AutoPollBusy(uint32_t TimeoutMs)
{
    QSPI_CommandTypeDef      cmd;
    QSPI_AutoPollingTypeDef  cfg;

    /* 配置状态寄存器读取 (1线模式,不受 QE 位影响) */
    memset(&cmd, 0, sizeof(cmd));
    cmd.InstructionMode   = QSPI_INSTRUCTION_1_LINE;
    cmd.Instruction       = W25QXX_CMD_READ_STATUS_REG;  /* 0x05 */
    cmd.AddressMode       = QSPI_ADDRESS_NONE;
    cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
    cmd.DataMode          = QSPI_DATA_1_LINE;             /* 1线读取 */
    cmd.DummyCycles       = 0;
    cmd.DdrMode           = QSPI_DDR_MODE_DISABLE;
    cmd.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;
    cmd.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;
    cmd.NbData            = 1;

    /* 轮询配置: 等待 BUSY 位 = 0 (与掩码匹配) */
    memset(&cfg, 0, sizeof(cfg));
    cfg.MatchMode      = QSPI_MATCH_MODE_AND;
    cfg.AutomaticStop  = QSPI_AUTOMATIC_STOP_ENABLE;
    cfg.Mask           = W25QXX_SR1_BUSY;   /* 只检查 BUSY 位 */
    cfg.Match          = 0x00;              /* 期望 BUSY = 0   */
    cfg.StatusBytesSize = 1;
    cfg.Interval        = 1;                /* 轮询间隔: 1个SCK周期 */

    if (HAL_QSPI_AutoPolling(&hqspi, &cmd, &cfg, TimeoutMs) != HAL_OK)
    {
        return 1;
    }

    return 0;
}

/**
  * @brief  复位 W25Q64
  * @note   先发使能复位(0x66),再发复位(0x99)
  */
static uint8_t QSPI_ResetMemory(void)
{
    QSPI_CommandTypeDef cmd;

    QSPI_EnsureReady();

    /* 使能复位命令 */
    memset(&cmd, 0, sizeof(cmd));
    cmd.InstructionMode   = QSPI_INSTRUCTION_1_LINE;
    cmd.Instruction       = W25QXX_CMD_ENABLE_RESET;
    cmd.AddressMode       = QSPI_ADDRESS_NONE;
    cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
    cmd.DataMode          = QSPI_DATA_NONE;
    cmd.DummyCycles       = 0;
    cmd.DdrMode           = QSPI_DDR_MODE_DISABLE;
    cmd.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;
    cmd.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;
    cmd.NbData            = 0;

    if (HAL_QSPI_Command(&hqspi, &cmd, W25QXX_CMD_TIMEOUT) != HAL_OK)
        return 1;

    /* 复位设备 */
    cmd.Instruction = W25QXX_CMD_RESET_DEVICE;
    if (HAL_QSPI_Command(&hqspi, &cmd, W25QXX_CMD_TIMEOUT) != HAL_OK)
        return 1;

    return 0;
}

/**
  * @brief  使能 Quad 模式 (设置状态寄存器2的 QE 位)
  * @note   W25Q64 需要 QE=1 才能使用 Quad SPI 操作
  *         Quad 操作包括: Quad Page Program(0x32), Quad Fast Read(0x6B)
  */
static uint8_t QSPI_EnableQuadMode(void)
{
    uint8_t sr1, sr2;
    uint8_t status_reg[2];
    QSPI_CommandTypeDef cmd;

    QSPI_EnsureReady();

    /* 1. 读取当前状态寄存器值 */
    sr1 = W25Qxx_ReadStatusReg(1);
    sr2 = W25Qxx_ReadStatusReg(2);

    /* 2. 检查 QE 位是否已设置 */
    if (sr2 & W25QXX_SR2_QE)
    {
        return 0;  /* 已经是 Quad 模式 */
    }

    /* 3. 写使能 (状态寄存器写入需要) */
    if (QSPI_WriteEnableCmd() != 0) return 1;

    /* 4. 写状态寄存器 (SR1 + SR2) */
    status_reg[0] = sr1;
    status_reg[1] = sr2 | W25QXX_SR2_QE;  /* 设置 QE 位 */

    memset(&cmd, 0, sizeof(cmd));
    cmd.InstructionMode   = QSPI_INSTRUCTION_1_LINE;
    cmd.Instruction       = W25QXX_CMD_WRITE_STATUS_REG;
    cmd.AddressMode       = QSPI_ADDRESS_NONE;
    cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
    cmd.DataMode          = QSPI_DATA_1_LINE;
    cmd.DummyCycles       = 0;
    cmd.DdrMode           = QSPI_DDR_MODE_DISABLE;
    cmd.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;
    cmd.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;
    cmd.NbData            = 2;

    if (HAL_QSPI_Command(&hqspi, &cmd, W25QXX_CMD_TIMEOUT) != HAL_OK)
        return 1;

    if (HAL_QSPI_Transmit(&hqspi, status_reg, W25QXX_CMD_TIMEOUT) != HAL_OK)
        return 1;

    /* 5. 等待写入完成 */
    if (QSPI_AutoPollBusy(W25QXX_WRITE_SR_TIMEOUT) != 0)
        return 1;

    /* 6. 验证 QE 位 */
    sr2 = W25Qxx_ReadStatusReg(2);
    if (!(sr2 & W25QXX_SR2_QE))
    {
        return 1;
    }

    return 0;
}
c 复制代码
/**
  ******************************************************************************
  * @file    w25qxx.h
  * @brief   W25Q64 QSPI Flash 驱动头文件
  * @note    适用于 STM32H750 + W25Q64 (8MB / 64Mbit)
  ******************************************************************************
  */

#ifndef __W25QXX_H
#define __W25QXX_H

#ifdef __cplusplus
extern "C" {
#endif

/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include <string.h>

/* ======================== W25Q64 Flash 参数 =============================== */
#define W25QXX_FLASH_SIZE              0x800000UL  /* 8MB = 64Mbit        */
#define W25QXX_SECTOR_SIZE             0x1000UL    /* 4KB 扇区            */
#define W25QXX_BLOCK_SIZE_32K          0x8000UL    /* 32KB 块             */
#define W25QXX_BLOCK_SIZE_64K          0x10000UL   /* 64KB 块             */
#define W25QXX_PAGE_SIZE               0x100UL     /* 256B 页             */

/* JEDEC Manufacturer & Device ID */
#define W25Q64_JEDEC_ID                0xEF4017UL

/* ======================== 命令定义 ======================================== */
#define W25QXX_CMD_WRITE_ENABLE        0x06  /* 写使能                  */
#define W25QXX_CMD_WRITE_DISABLE       0x04  /* 写禁止                  */
#define W25QXX_CMD_READ_STATUS_REG     0x05  /* 读状态寄存器1           */
#define W25QXX_CMD_READ_STATUS_REG2    0x35  /* 读状态寄存器2           */
#define W25QXX_CMD_WRITE_STATUS_REG    0x01  /* 写状态寄存器            */
#define W25QXX_CMD_READ_DATA           0x03  /* 标准读 (1线)            */
#define W25QXX_CMD_FAST_READ           0x0B  /* 快速读 (1线)            */
#define W25QXX_CMD_FAST_READ_QUAD      0x6B  /* 四线快速读              */
#define W25QXX_CMD_PAGE_PROGRAM        0x02  /* 页编程 (1线)            */
#define W25QXX_CMD_QUAD_PAGE_PROGRAM   0x32  /* 四线页编程              */
#define W25QXX_CMD_SECTOR_ERASE        0x20  /* 4KB 扇区擦除            */
#define W25QXX_CMD_BLOCK_ERASE_32KB    0x52  /* 32KB 块擦除             */
#define W25QXX_CMD_BLOCK_ERASE_64KB    0xD8  /* 64KB 块擦除             */
#define W25QXX_CMD_CHIP_ERASE          0xC7  /* 整片擦除 (0xC7/0x60)   */
#define W25QXX_CMD_READ_JEDEC_ID       0x9F  /* 读JEDEC ID              */
#define W25QXX_CMD_READ_UNIQUE_ID      0x4B  /* 读唯一ID                */
#define W25QXX_CMD_ENTER_POWER_DOWN    0xB9  /* 进入掉电模式            */
#define W25QXX_CMD_RELEASE_POWER_DOWN  0xAB  /* 退出掉电模式            */
#define W25QXX_CMD_ENABLE_RESET        0x66  /* 使能复位                */
#define W25QXX_CMD_RESET_DEVICE        0x99  /* 复位设备                */

/* ======================== 状态寄存器位定义 ================================= */
#define W25QXX_SR1_BUSY                (1U << 0)  /* 忙标志              */
#define W25QXX_SR1_WEL                 (1U << 1)  /* 写使能锁存          */
#define W25QXX_SR2_QE                  (1U << 1)  /* Quad使能位          */

/* ======================== 超时定义 (ms) =================================== */
#define W25QXX_CMD_TIMEOUT             100U       /* 命令超时             */
#define W25QXX_SECTOR_ERASE_TIMEOUT    500U       /* 扇区擦除超时         */
#define W25QXX_BLOCK32K_ERASE_TIMEOUT  1200U      /* 32KB块擦除超时       */
#define W25QXX_BLOCK64K_ERASE_TIMEOUT  2500U      /* 64KB块擦除超时       */
#define W25QXX_CHIP_ERASE_TIMEOUT      90000U     /* 整片擦除超时         */
#define W25QXX_PAGE_PROGRAM_TIMEOUT    10U        /* 页编程超时           */
#define W25QXX_WRITE_SR_TIMEOUT        20U        /* 写状态寄存器超时     */

/* ======================== 类型定义 ======================================== */
typedef struct {
    uint32_t FlashSize;           /* Flash总大小 (字节)                    */
    uint32_t EraseSectorSize;     /* 扇区大小 (字节)                       */
    uint32_t EraseSectorNumber;   /* 扇区数量                              */
    uint32_t EraseBlock32KSize;   /* 32KB块大小 (字节)                     */
    uint32_t EraseBlock32KNumber; /* 32KB块数量                            */
    uint32_t EraseBlock64KSize;   /* 64KB块大小 (字节)                     */
    uint32_t EraseBlock64KNumber; /* 64KB块数量                            */
    uint32_t PageSize;            /* 页大小 (字节)                         */
    uint32_t PageNumber;          /* 页数量                                */
} W25Qxx_InfoTypeDef;

/* ======================== 公共变量 ======================================== */
extern W25Qxx_InfoTypeDef w25qxx_info;

/* 调试变量 - 在 debugger 中查看定位问题 */
extern volatile uint32_t g_debug_step;
extern volatile uint32_t g_debug_hal_status;
extern volatile uint32_t g_debug_error_code;

/* ======================== API 函数声明 ==================================== */

/** @brief  初始化 QSPI Flash (复位 + Quad使能 + ID验证)
  * @retval 0:成功  1:复位失败  2:ID不匹配  3:ID读取失败  4:Quad配置失败
  */
uint8_t W25Qxx_Init(void);

/** @brief  读取 JEDEC ID (厂商ID + 设备ID)
  * @retval 32位ID值 (如 0xEF4017 表示 W25Q64),失败返回 0
  */
uint32_t W25Qxx_ReadID(void);

/** @brief  读取唯一ID (8字节)
  * @param  pUID: 缓冲区指针 (至少8字节)
  */
void W25Qxx_ReadUID(uint8_t *pUID);

/** @brief  读取指定状态寄存器
  * @param  reg_num: 寄存器编号 (1 或 2)
  * @retval 寄存器值
  */
uint8_t W25Qxx_ReadStatusReg(uint8_t reg_num);

/* ---- 擦除操作 ---- */

/** @brief  擦除指定扇区 (4KB)
  * @param  SectorAddr: 扇区编号 [0, FlashSize/SectorSize)
  */
void W25Qxx_EraseSector(uint32_t SectorAddr);

/** @brief  擦除 32KB 块
  * @param  BlockAddr: 块编号 [0, FlashSize/BlockSize32K)
  */
void W25Qxx_EraseBlock32K(uint32_t BlockAddr);

/** @brief  擦除 64KB 块
  * @param  BlockAddr: 块编号 [0, FlashSize/BlockSize64K)
  */
void W25Qxx_EraseBlock64K(uint32_t BlockAddr);

/** @brief  整片擦除 (耗时约 40 秒)
  */
void W25Qxx_EraseChip(void);

/* ---- 写入操作 ---- */

/** @brief  四线页编程 (单页内写入,不超过页边界)
  * @param  pData:    数据缓冲区
  * @param  PageAddr: 页编号
  * @param  Offset:   页内偏移 (字节)
  * @param  Size:     写入字节数 (Offset+Size <= 256)
  * @retval 0:成功  1:超出页边界  2:写使能失败  3:命令失败  4:数据发送失败  5:等待超时
  */
uint8_t W25Qxx_WritePage(uint8_t *pData, uint32_t PageAddr, uint32_t Offset, uint32_t Size);

/** @brief  扇区内写入 (自动跨页处理)
  * @param  pData:      数据缓冲区
  * @param  SectorAddr: 扇区编号
  * @param  Offset:     扇区内偏移 (字节)
  * @param  Size:       写入字节数 (Offset+Size <= 4096)
  * @retval 0:成功  非0:失败
  */
uint8_t W25Qxx_WriteSector(uint8_t *pData, uint32_t SectorAddr, uint32_t Offset, uint32_t Size);

/* ---- 读取操作 ---- */

/** @brief  标准读取 (1线 SPI 模式)
  * @param  pData:    数据缓冲区
  * @param  ReadAddr: 读取起始地址
  * @param  Size:     读取字节数
  */
void W25Qxx_ReadData(uint8_t *pData, uint32_t ReadAddr, uint32_t Size);

/** @brief  四线快速读取 (Quad Fast Read)
  * @param  pData:    数据缓冲区
  * @param  ReadAddr: 读取起始地址
  * @param  Size:     读取字节数
  */
void W25Qxx_FastReadData(uint8_t *pData, uint32_t ReadAddr, uint32_t Size);

/* ---- 控制操作 ---- */

/** @brief  写使能
  * @retval 0:成功  1:失败
  */
uint8_t W25Qxx_WriteEnable(void);

/** @brief  写禁止
  * @retval 0:成功  1:失败
  */
uint8_t W25Qxx_WriteDisable(void);

/** @brief  等待写操作完成 (轮询 BUSY 位)
  * @retval 0:成功  1:超时
  */
uint8_t W25Qxx_WaitForWriteEnd(void);

/** @brief  进入掉电模式
  */
void W25Qxx_EnterPowerDown(void);

/** @brief  退出掉电模式
  */
void W25Qxx_ExitPowerDown(void);

/** @brief  使能内存映射模式 (Quad SPI, 只读直接访问)
  */
void W25Qxx_EnableMemoryMappedMode(void);

/** @brief  中止 QSPI 传输 (紧急恢复)
  */
void W25Qxx_Abort(void);

#ifdef __cplusplus
}
#endif

#endif /* __W25QXX_H */

接口调用

c 复制代码
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "w25qxx.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 ---------------------------------------------------------*/

QSPI_HandleTypeDef hqspi;

/* USER CODE BEGIN PV */

  uint32_t flash_id;
  uint8_t write_data[256];
  uint8_t read_data[256];
  uint8_t status;
/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MPU_Config(void);
static void MX_GPIO_Init(void);
static void MX_QUADSPI_Init(void);
/* USER CODE BEGIN PFP */

/* 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();

  /* MPU Configuration--------------------------------------------------------*/
  MPU_Config();

  /* 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();
  /* USER CODE BEGIN 2 */

/* 调试变量声明 - 在调试器中查看这些变量定位问题 */
extern volatile uint32_t g_debug_step;
extern volatile uint32_t g_debug_hal_status;
extern volatile uint32_t g_debug_error_code;

  /* 读取Flash ID验证QSPI硬件 */
  flash_id = W25Qxx_ReadID();

  /* 初始化W25Q64 Flash */
  status = W25Qxx_Init();
  if (status != 0)
  {
    /* Flash初始化失败 */
    while(1);
  }

  /* 1. 先擦除扇区0 (必须先擦除才能正确写入) */
  W25Qxx_EraseSector(0);

  /* 等待擦除完成 (扇区擦除最多400ms) */
  HAL_Delay(500);

  /* 2. 准备要写入的数据 */
  for (int i = 0; i < 256; i++)
  {
    write_data[i] = i;
  }

  /* 3. 写入一页数据 */
  status = W25Qxx_WritePage(write_data, 0, 0, 256);
  if (status != 0)
  {
    /* 写入失败 - 在调试器中查看以下变量定位问题:
       g_debug_step:   最后执行的步骤
       g_debug_hal_status: HAL函数返回状态
       g_debug_error_code: hqspi.ErrorCode

       步骤对应:
       1=进入WritePage, 3=计算地址完成, 4=调用写使能
       5=写使能失败(看step=16, hal_status=1, error_code)
       6=写使能成功, 7=配置页编程命令, 8=HAL_QSPI_Command失败
       9=发送数据, 10=数据发送失败, 11=开始轮询, 12=轮询超时
       10~16=QSPI_WriteEnable内部步骤
    */
    while(1);
  }

  /* 等待页面编程完成 */
  HAL_Delay(5);

  /* 4. 读取数据验证 - 地址0 */
  W25Qxx_FastReadData(read_data, 0, 256);

  /* 写入和读取都成功!请在调试时查看 read_data 内容 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}
  • 效果演示

七、注意事项

1、擦除、写之前,都必须先发写使能指令,这是Flash芯片标注的事项。

2、为防止因为软件操作Flash芯片操作到一半复位,导致Flash芯片卡在某个异常状态,在Flash芯片初始化时,最好复位下芯片。

3、Flash芯片的QSPI模式配置是可以保存的,所以在开启芯片的QSPI模式之前,可以先读取相应的寄存器,如果已经是QSPI模式,则不需要再重新配置。

八、相关链接

对于刚入门的小伙伴可以先看下STM32CubeMX的基础使用及Keil的基础使用。
【工具使用】STM32CubeMX-基础使用篇
【工具使用】Keil5软件使用-基础使用篇

相关推荐
知识噬元兽12 小时前
【工具使用】STM32CubeMX-FATFS文件系统
stm32·单片机·嵌入式硬件
广州灵眸科技有限公司21 小时前
瑞芯微(EASY EAI)RV1126B 核心板供电电路
linux·运维·服务器·单片机·嵌入式硬件·电脑
浩浩测试一下1 天前
汇编 16位32位64位通用寄存器(逆向分析)
汇编·windows·stm32·单片机·嵌入式硬件·逆向·二进制
潜创微科技1 天前
IT68353:双 DP1.4a+HDMI2.0 转 HDMI2.0 单芯片 KVM 切换方案
嵌入式硬件·音视频
踏着七彩祥云的小丑1 天前
嵌入式测试学习第 17 天:常见接口:USB、Type-C、排针
单片机·嵌入式硬件
szxinmai主板定制专家1 天前
电力设备RK3568/RK3576+FPGA,多系统混合部署Linux+RTOS RT-THREAD,强实时性
linux·运维·服务器·人工智能·嵌入式硬件·fpga开发
振南的单片机世界1 天前
EXTI边沿触发:按键一按就通知CPU,不用轮询
stm32·单片机·嵌入式硬件
jllllyuz1 天前
STM32 BMP280 I2C通信驱动程序
stm32·单片机·嵌入式硬件
优信电子1 天前
基于STM32F103C8T6单片机驱动ACS712模块进行电流检测
stm32·单片机·嵌入式硬件·嵌入式·电流检测·acs712·电流采集