【STM32】 SPI Flash 驱动开发实战:阻塞模式驱动 W25Q64(含工程代码)

【STM32】SPI Flash 驱动开发实战:阻塞模式驱动 W25Q64

一、前言

前面我们用极简 demo 的方式介绍完了 SPI 的阻塞通信接口,完成了理论与实践结合的练习。本文把重点放在纯实践层面:使用 STM32 的 SPI1 主机,以阻塞模式驱动 W25Q64 Flash 芯片,完成从 CubeMX 配置、工程搭建到驱动代码编写、功能测试的全流程。

编写该驱动之前需要掌握三件事:

  1. SPI 主机阻塞模式配置过程:CubeMX 软件相关配置以及生成代码的理解;
  2. SPI 主机阻塞模式数据发送、接收相关函数接口的使用方法(HAL_SPI_Transmit / HAL_SPI_Receive);
  3. W25Qxx Flash 的操作方法------本次的核心目标。

二、需求分析

完成 W25Q64 驱动开发,必须实现以下功能:

序号 功能 说明
1 读取芯片 ID JEDEC ID,用于识别厂商、类型、容量
2 读取 Flash 状态信息 状态寄存器 1,关注 BUSY / WEL 位
3 使能 Flash 写操作 写使能指令 06h,置 WEL=1
4 Flash 扇区擦除 擦除最小单位 4KB
5 Flash 整片擦除 擦除整个芯片
6 Flash 页编程 / 跨页写入 写入最大单位 256 字节,封装跨页
7 等待 Flash 写完成 轮询 BUSY 位
8 读 Flash 数据 任意地址连续读取

驱动完成后,对功能进行测试,并通过串口打印测试结果。


三、硬件准备

3.1 W25Q64 的 SPI1 接线关系

开发板上 SPI1 的信号线(SCK / MOSI / MISO)是被 LCD 触摸屏和 W25Q64 两个从设备统一共享的,它们的引脚功能固定:

MCU 引脚 SPI1 功能 连接设备
PA5 SPI1_SCK 时钟,两从设备并联
PA7 SPI1_MOSI 主机输出,两从设备并联
PA6 SPI1_MISO 主机输入,两从设备并联
PE6 FLASH_CS(软件片选) 片选 W25Q64
PE4 TC_CS(软件片选) 片选 LCD 触摸屏

⚠️ 注意(这里有个大坑):

  • 如果不把 PE4 配置并拉高,LCD 可能被误片选,导致片选出错,进而引发未知错误(默认电平不确定,必须保证确定性);
  • 所以必须开启 GPIOE,PE4 和 PE6 默认都拉高,未来片选时只需拉低 PE6 即可选中 W25Q64。

3.2 选择 USART1 进行信息打印

  • 串口复用 GPIOA:PA9 (USART1_TX)、PA10 (USART1_RX);
  • 核心配置:异步模式、波特率 115200、9 位数据(含校验)、奇校验(Odd)、1 位停止位;
  • 上位机串口助手需保持相同配置才能正确收发。

3.3 选择 LED 灯

使用两个 LED 指示状态:

LED1(灵犀板 LED4)指示数据正常通信,LED2(LED5)指示通信异常。

选择 PF8 (LED4)、PF9 (LED5),外部有上拉电源,

配置为开漏输出:导通 N-MOS → LED 点亮,关闭 N-MOS → LED 熄灭。

3.4 选择串口助手

默认使用 comtool 上位机软件。


四、CubeMX 软件设计

4.1 配置仿真器接口(SWD)

开发板预留的调试接口是 SWD(Serial Wire Debug),在 SYS → Debug 中选择 Serial Wire。

⚠️ 如果在 CubeMX 中错误配置或禁止了 debug 接口(如 JTAG/SWD),烧录完程序后将无法通过调试器连接目标 STM32。

4.2 配置晶振和时钟

  • RCC → HSE 选择 Crystal / Ceramic Resonator(外部晶振);
  • 开发板是 8MHz HSE 晶振,将系统运行最大频率配置到 72MHz(HSE ×9 → SYSCLK 72MHz)。

4.3 SPI1 接口配置

cpp 复制代码
Mode: Full Duplex Master
Hardware NSS Signal: Disable        ← 不用硬件NSS,采用软件NSS片选
参数
Frame Format Motorola(标准 SPI)
Data Size 8 Bits
First Bit MSB First
Prescaler 64(波特率 1.125 MBits/s)
Clock Polarity (CPOL) Low
Clock Phase (CPHA) 1 Edge(即 Mode 0)
NSS Signal Type Software

引脚自动分配:

PA5 = SPI1_SCK、PA6 = SPI1_MISO、

PA7 = SPI1_MOSI(复用功能、高速、无上下拉)。

片选引脚:

开启 GPIOE,PE4 常拉高(防 LCD 误选),

PE6 供片选 W25Q64,默认输出高,后续通过软件拉低进行选通。

4.4 配置打印串口(USART1)

异步模式、波特率 115200、9 位数据(含奇校验)、1 位停止位。

使能串口后会自动生成 PA9/PA10 的引脚配置,保持默认即可。

4.5 配置指示 LED 灯

开启 PF8、PF9,配置为通用输出开漏模式,默认输出电平 LOW(默认让 LED 点亮)。

4.6 配置项目管理 & 4.7 生成代码

  • 工程名:ISP_Flash_test,Toolchain/IDE:MDK-ARM V5.32;
  • 点击 GENERATE CODE 生成代码(生成的代码结构之前已看过,此处不再赘述)。

五、代码编写

5.1 代码结构与准备工作

  1. 构建驱动文件结构:在工程目录下新建 BSP_Drivers 文件夹,内含 inc(头文件)、src(源文件)两个子目录,然后添加到 Keil 工程;
  2. 注意事项:Keil 中勾选 Use MicroLIB(配合 printf 重定向使用);
  3. 导入 LED 模块(操作略)。

细节:

BSP 全称 板级支持包(Board Support Package)。

文件命名带 BSP,表示访问的设备都是板子上外设的驱动功能。


5.2 初始化与驱动兼容性设计

我们希望驱动能够驱动基于 STM32 为主控的 W25Qxx 全系芯片,影响驱动兼容性的因素:

  1. W25Qxx 型号不同 → 页大小、扇区大小、块大小、容量大小的差异;
  2. 硬件电路板不同 → 控制片选的 IO 管脚可能不同;
  3. 硬件电路板不同 → 使用的 SPI 接口可能不同。

解决方案:通过结构体在初始化时传递差异性参数。

cpp 复制代码
/* Bsp_W25Q64.h */
typedef struct {
    uint32_t page_size;                  // 页大小
    uint32_t sector_size;                // 扇区大小
    uint32_t block_size;                 // 块大小
    uint32_t mem_size;                   // 总容量大小
    uint32_t chip_id;                    // 芯片ID
    SPI_HandleTypeDef *spi_handler;      // SPI句柄指针
    GPIO_TypeDef *nss_port;              // 软件片选引脚端口
    uint16_t nss_pin;                    // 软件片选针脚
} flash_t;
cpp 复制代码
/* Bsp_W25Q64.c */
static flash_t w25qxx;          // W25Qxx 对象
static bool w25qxx_init = false; // 初始化完成标志

bool BSP_W25Qxx_Init(flash_t *init)
{
    if (init == NULL)
        return false;

    // 注意:读取ID依赖初始化状态,所以先置true,失败再改回false
    w25qxx_init = true;
    w25qxx.page_size   = init->page_size;
    w25qxx.sector_size = init->sector_size;
    w25qxx.block_size  = init->block_size;
    w25qxx.mem_size    = init->mem_size;
    w25qxx.chip_id     = init->chip_id;
    w25qxx.spi_handler = init->spi_handler;
    w25qxx.nss_port    = init->nss_port;
    w25qxx.nss_pin     = init->nss_pin;

    // 为了保证兼容性,只处理ID相同的芯片
    uint32_t id = BSP_W25Qxx_Read_Id();
    if (id != w25qxx.chip_id) {
        w25qxx_init = false;
        printf("w25qxx init failed, ID error, current id is : 0x%08x, your id is : 0x%08x\n",
               id, w25qxx.chip_id);
        return false;
    }
    printf("w25qxx init success, ID=0x%08x\n", id);
    return w25qxx_init;
}

main 中的初始化参数(W25Q64):

cpp 复制代码
flash_t w25qxx_init = {
    .page_size   = 256,              // 页大小:256字节
    .sector_size = 4096,             // 扇区大小:4KB
    .block_size  = 64 * 1024,        // 块大小:64KB
    .mem_size    = 8 * 1024 * 1024,  // 内存大小:8MB
    .chip_id     = 0xEF4017,         // 芯片ID:W25Q64 = 0xEF4017
    .spi_handler = &hspi1,           // SPI句柄
    .nss_port    = GPIOE,            // CS端口
    .nss_pin     = GPIO_PIN_6        // CS引脚
};
BSP_W25Qxx_Init(&w25qxx_init);

灵犀板选择 GPIOE 端口位的 PE6 对 W25Q64 进行片选,默认输出高电平,拉低电平即选中从设备。


5.3 指令码定义

对 W25Qxx 芯片操作时,需要先发送一个字节的指令码,用于表示本次操作的作用(读数据、擦除扇区等)。常用标准 SPI 指令码(摘自 W25Q64JV 规格书 8.1.2):

指令 指令码 字节1 字节2 字节3 字节4 后续
Write Enable 06h --- --- --- --- ---
Write Disable 04h --- --- --- --- ---
Read Status Register-1 05h (S7-S0) 连续输出 --- --- ---
Write Status Register-1 01h (S7-S0) --- --- --- ---
Read Data 03h A23-A16 A15-A8 A7-A0 (D7-D0) 连续输出
Fast Read 0Bh A23-A16 A15-A8 A7-A0 Dummy (D7-D0)
Page Program 02h A23-A16 A15-A8 A7-A0 D7-D0 D7-D0(n)
Sector Erase (4KB) 20h A23-A16 A15-A8 A7-A0 --- ---
Block Erase (32KB) 52h A23-A16 A15-A8 A7-A0 --- ---
Block Erase (64KB) D8h A23-A16 A15-A8 A7-A0 --- ---
Chip Erase C7h/60h --- --- --- --- ---
JEDEC ID 9Fh (MF7-MF0) (ID15-ID8) (ID7-ID0) --- ---
Manufacturer/Device ID 90h Dummy Dummy Dummy (MF7-MF0) (ID7-ID0)
Read Unique ID 4Bh Dummy Dummy Dummy Dummy (UID63-0)
Power down B9h --- --- --- --- ---
Release Power-down ABh Dummy Dummy Dummy (ID7-ID0) ---
Enable Reset 66h --- --- --- --- ---
Reset Device 99h --- --- --- --- ---

5.4 从设备使能(片选控制)

操作 SPI 从机前,必须先通过片选信号选择设备。

我们选择软件使能 W25Q64,用完后再通过片选信号禁用。

再次强调:

当 SPI 片选信号(NSS)使用硬件模式时,SPI 外设自动控制 NSS;

使用软件模式时,需要用户通过 IO 口独立实现,灵活性更大。

cpp 复制代码
/* 使能:拉低CS,激活通信 */
static void BSP_W25Qxx_CS_Enable(void)
{
    HAL_GPIO_WritePin(w25qxx.nss_port, w25qxx.nss_pin, GPIO_PIN_RESET);
}

/* 禁用:拉高CS,结束通信 */
static void BSP_W25Qxx_CS_Disable(void)
{
    HAL_GPIO_WritePin(w25qxx.nss_port, w25qxx.nss_pin, GPIO_PIN_SET);
}

5.5 读取芯片 ID(JEDEC ID)

获取步骤:

  1. 将 /CS 引脚拉低,选中设备;
  2. 发送指令码 0x9F 启动 ID 读取操作;
  3. 制造商 ID(0xEF)+ 两个设备 ID 字节(存储器类型 ID15-8、容量 ID7-0)在 CLK 下降沿移出,MSB 优先;
  4. 将 /CS 引脚拉高,禁止设备。
cpp 复制代码
uint32_t BSP_W25Qxx_Read_Id(void)
{
    if (!w25qxx_init)
        return 0;

    uint8_t cmd = W25QXX_JEDEC_ID;      // 0x9F
    uint8_t id[3] = {0};
    HAL_StatusTypeDef status = HAL_OK;

    BSP_W25Qxx_CS_Enable();

    status = HAL_SPI_Transmit(w25qxx.spi_handler, &cmd, 1, HAL_MAX_DELAY);
    if (status != HAL_OK) {
        printf("w25qxx_read_id: write cmd(0x%02X) failed, status=%d\r\n", cmd, status);
        goto END;
    }

    status = HAL_SPI_Receive(w25qxx.spi_handler, id, 3, HAL_MAX_DELAY);
    if (status != HAL_OK) {
        memset(id, 0, sizeof(id));
        printf("w25qxx_read_id: read id failed, status=%d\r\n", status);
        goto END;
    }

END:
    BSP_W25Qxx_CS_Disable();
    // 组合3字节为24位返回值:高8位制造商 + 中8位类型 + 低8位容量
    return (id[0] << 16) | (id[1] << 8) | id[2];
}

测试结果:W25Q64 JEDEC ID : 0x00ef4017

✅(EF = Winbond 制造商 ID,40 = 存储器类型,17 = 8Mbit→8MB 容量)


5.6 读取芯片状态

5.6.1 两个关键标志位

W25Q64 共有 3 个状态寄存器,我们只需关注 Status Register-1 的 BUSY 和 WEL 两个标志位:

名称 含义
S0 BUSY 1 = 芯片正在忙(页编程/擦除/写状态寄存器中),完成后自动清 0
S1 WEL 1 = 写使能锁存(执行 06h 后置 1),操作完成或上电后自动清 0
S2-S4 BP0-BP2 块保护位
S5 TB 顶部/底部保护
S6 SEC 扇区保护
S7 SRP 状态寄存器保护

高度总结:

  • WEL(写使能)是"入场券"****:芯片默认锁定(WEL=0),防止意外误抹除。每次写/擦前必须手动"领票"(执行写使能指令),操作完票据自动作废;
  • BUSY(忙碌位)是"进度条":芯片内部擦除/写入需要物理时间(电荷充放电),BUSY 就像门口挂的"施工中"牌子,告诉主控先等等。

开发中的逻辑:

写入前必须调一次 Write Enable(WEL=1);

写入后必须循环读状态寄存器,等 BUSY=0 才能进行下一步。


5.6.2 读取状态寄存器时序

指令码 05h:

/CS 拉低 → CLK 上升沿移入指令码 → 状态寄存器各位在 CLK 下降沿从 DO 移出,MSB 优先 → /CS 拉高结束。

细节:状态寄存器不是 8 位吗?为什么返回 16 位?

如果在输入 05h 后时钟没有停止,芯片会不断重复输出这 8 位数据。

这方便主控轮询(Polling):

不需要反复发 05h,拉低 CS 后一直读,直到 BUSY 位变为 0 为止。

所以我们只读 register1 的 8 位、关注 2 位就够了。

cpp 复制代码
uint8_t BSP_W25Qxx_Status1_Reg(void)
{
    if (!w25qxx_init)
        return 0xFF;

    uint8_t cmd = W25QXX_READ_STATUS_REG1;   // 0x05
    uint8_t status = 0xFF;
    HAL_StatusTypeDef state = HAL_OK;

    BSP_W25Qxx_CS_Enable();

    state = HAL_SPI_Transmit(w25qxx.spi_handler, &cmd, 1, HAL_MAX_DELAY);
    if (state != HAL_OK) {
        printf("HAL_SPI_Transmit cmd 0x%02x error, state is: %d\n", cmd, state);
        goto END;
    }

    state = HAL_SPI_Receive(w25qxx.spi_handler, &status, 1, HAL_MAX_DELAY);
    if (state != HAL_OK) {
        status = 0xFF;
        printf("HAL_SPI_Receive cmd 0x%02x error, state is: %d\n", cmd, state);
        goto END;
    }

END:
    BSP_W25Qxx_CS_Disable();
    return status;
}

测试结果:

register1 status: 0x00 → 即默认 BUSY=0(不忙)、WEL=0(写保护)。


5.7 使能写操作

W25Q64 默认写保护。

每次执行页编程、扇区擦除、块擦除、芯片擦除和写状态寄存器指令之前,都必须先进行写使能(设置 WEL=1)。

cpp 复制代码
void BSP_W25Qxx_Write_Enable(void)
{
    if (!w25qxx_init)
        return;

    uint8_t cmd = W25QXX_WRITE_ENABLE;       // 0x06
    BSP_W25Qxx_CS_Enable();
    HAL_StatusTypeDef state = HAL_SPI_Transmit(w25qxx.spi_handler, &cmd, 1, HAL_MAX_DELAY);
    if (state != HAL_OK) {
        printf("HAL_SPI_Transmit cmd 0x%02x error, state is: %d\n", cmd, state);
    }
    BSP_W25Qxx_CS_Disable();
}

测试结果:

cpp 复制代码
Write_Enable before register status: 0x00
Write_Enable after register status: 0x02   ← WEL=1,关闭写保护

5.8 禁止写操作

指令码 04h。两种应用场景:

  • 自动锁定(最常用):实际编程中很少手动发 04h,因为 W25Q64 非常"谨慎",每次完成写入或擦除任务都会自动执行写禁止,防止误操作;
  • 手动锁定:发了写使能(06h)但突然不想写了,可以发 04h 手动"把门锁上"。
cpp 复制代码
void BSP_W25Qxx_Write_Disable(void)
{
    if (!w25qxx_init)
        return;

    uint8_t cmd = W25QXX_WRITE_DISABLE;      // 0x04
    BSP_W25Qxx_CS_Enable();
    HAL_StatusTypeDef state = HAL_SPI_Transmit(w25qxx.spi_handler, &cmd, 1, HAL_MAX_DELAY);
    if (state != HAL_OK) {
        printf("HAL_SPI_Transmit cmd 0x%02x error, state is: %d\n", cmd, state);
    }
    BSP_W25Qxx_CS_Disable();
}

上电保护:

芯片刚通电时默认就是"写禁止"状态,保证电源不稳时不会意外破坏数据。

断电重启后 Flash 默认恢复为"写禁止"。


5.9 等待写完成

对 W25Q64 进行编程或擦除时,操作需要几 ms ~ 几十 s 不等,必须等待操作顺利完成,本质就是检测 BUSY 标志位。

cpp 复制代码
/* 状态寄存器位定义 */
#define W25QXX_STATUS_BUSY  0x01   // 1 = 芯片正在忙(擦除或写入中)
#define W25QXX_STATUS_WR_EN 0x02   // 1 = 芯片处于写使能状态

void BSP_W25Qxx_Waitfor_Write_Complete(void)
{
    if (!w25qxx_init)
        return;
    while (BSP_W25Qxx_Status1_Reg() & W25QXX_STATUS_BUSY);
}
  • 读取状态标志位需要等待吗?不需要------返回是及时性的,而且"读 BUSY 还要等 BUSY"就是鸡生蛋蛋生鸡的问题了;
  • 读取正常数据时一般也不需要等,但最好避开设备忙的时间。

补充两个检测函数:

cpp 复制代码
bool BSP_W25Qxx_Is_Busy(void)
{
    if (!w25qxx_init)
        return false;
    return BSP_W25Qxx_Status1_Reg() & W25QXX_STATUS_BUSY;
}

bool BSP_W25Qxx_Is_Writeable(void)
{
    if (!w25qxx_init)
        return false;
    return BSP_W25Qxx_Status1_Reg() & W25QXX_STATUS_WR_EN;
}

5.10 扇区擦除

单位 大小 W25Q64 参数 指令 特点
页 Page 256 Byte 8MB / 256B = 32768 页 02h Page Program 页编程 写入单位 不能跨页写; 单次最多写 256 字节; 不能擦页!没有擦页指令
扇区 Sector 4 KB(4096B) 8MB / 4KB = 2048 个扇区 20h Sector Erase 扇区擦除 最小擦除单位 擦除后内容变成 0xFF; 地址必须 4K 对齐; 典型擦除时间 45ms
块 Block (64KB) 64 KB 8MB / 64KB = 128 个块 D8h Block Erase 64K 块擦除 一次性擦除 16 个扇区; 擦除速度比逐个扇区快; 适合大批量擦除
整片 Chip 8 MB 1 颗 W25Q64 C7h Chip Erase 擦全部存储; ⚠️时间几十秒,谨慎使用

编程(写入)之前必须先擦除,再写入**。**

指令码 20h:

/CS 拉低 → 移入指令码 + 24 位扇区地址 → /CS 拉高,自定时擦除周期开始。

参数 典型值 最大值
Sector Erase Time (4KB) tSE 45 ms 400 ms
cpp 复制代码
void BSP_W25Qxx_Sector_Erase(uint32_t sector_address)
{
    if (!w25qxx_init)
        return;

    // 强制地址对齐:扇区地址低12位必须为0(4096的整数倍)
    if (sector_address & 0xFFF) {
        printf("BSP_W25Qxx_Sector_Erase: address 0x%x not sector aligned\n", sector_address);
        return;
    }

    // 地址不能超过总容量
    if (sector_address >= w25qxx.mem_size) {
        printf("BSP_W25Qxx_Sector_Erase: address 0x%x out of range max memory size\n", sector_address);
        return;
    }

    // 构造擦除指令:指令码 + 24位地址(顺序来自时序图)
    uint8_t cmd[4] = {
        W25QXX_SECTOR_ERASE,                // 0x20
        (uint8_t)(sector_address >> 16),
        (uint8_t)(sector_address >> 8),
        (uint8_t)(sector_address)
    };

    // ⚠️ 坑:擦除操作一定要先写使能,否则擦除不会执行
    BSP_W25Qxx_Write_Enable();
    BSP_W25Qxx_CS_Enable();

    HAL_StatusTypeDef state = HAL_SPI_Transmit(w25qxx.spi_handler, cmd, 4, HAL_MAX_DELAY);
    if (state != HAL_OK) {
        printf("BSP_W25Qxx_Sector_Erase: sector erase cmd 0x%02x failed, status = %d\n", cmd[0], state);
        BSP_W25Qxx_CS_Disable();
        return;
    }
    printf("BSP_W25Qxx_Sector_Erase: sector erase cmd 0x%02x done, status = %d\n", cmd[0], state);

    // 拉高CS才开始擦除,然后等待完成
    BSP_W25Qxx_CS_Disable();
    BSP_W25Qxx_Waitfor_Write_Complete();
}

细节要点:

  • 芯片内部擦除逻辑在 CS 上升沿触发:下发擦除指令后不会立即擦除,必须取消片选(拉高 CS),擦除才开始;
  • 虽然是 24 位地址,但扇区擦除时芯片会自动忽略低 12 位,最佳实践:传入地址必须是 4096 的整数倍;
  • 擦除一旦完成,WEL 自动变回 0。连续擦除两个扇区必须遵循:写使能 → 擦除扇区1 → 等待BUSY → 写使能 → 擦除扇区2 → 等待BUSY 的循环;
  • TYP 就是 Typical(典型值),通常是统计上的平均值。

⚠️ 格外注意:

除非特殊要求,千万不要把扇区擦除、块擦除、整片擦除写在 while 循环中。

Flash 是有寿命的,如果忘了,可能会导致 Flash 一直被擦除。


5.11 整片擦除

指令码 C7h / 60h,无地址,单字节指令。

参数 典型值 最大值
Chip Erase Time tCE 20 s 100 s

⚠️ 注意单位是秒(S)!

整片擦除细节和扇区擦除差不多,唯一要特别注意的是擦除时间单位。

cpp 复制代码
void BSP_W25Qxx_Chip_Erase(void)
{
    if (!w25qxx_init)
        return;

    uint8_t cmd = W25QXX_CHIP_ERASE;         // 0xC7
    // ⚠️ 坑:擦除前必须写使能
    BSP_W25Qxx_Write_Enable();
    BSP_W25Qxx_CS_Enable();

    HAL_StatusTypeDef state = HAL_SPI_Transmit(w25qxx.spi_handler, &cmd, 1, HAL_MAX_DELAY);
    if (state != HAL_OK) {
        printf("BSP_W25Qxx_Chip_Erase: chip erase cmd 0x%02x failed, status = %d\n", cmd, state);
        BSP_W25Qxx_CS_Disable();
        return;
    }
    printf("BSP_W25Qxx_Chip_Erase: chip erase 0x%02x done, status = %d\n", cmd, state);

    // 关闭片选,开始整片擦除
    BSP_W25Qxx_CS_Disable();
    BSP_W25Qxx_Waitfor_Write_Complete();
}

测试时把等待函数改成带打印的版本,可以看到擦除过程约 19~20 秒:

cpp 复制代码
Erase ing ..., cnt is : 1
Erase ing ..., cnt is : 2
...
Erase ing ..., cnt is : 19
Erase Chip Done

5.12 页编程(写入的本质)

编程本质就是写入**。**

指令码 02h:

/CS 拉低 → 移入指令码 + 24 位地址 + 至少一个数据字节,

发送数据期间 /CS 必须全程保持低电平 → /CS 拉高,自定时页编程开始。

参数 典型值 最大值
Page Program Time tPP 0.4 ms 3 ms

关于页编程必须要知道的细节:

  • Flash 的写入基本单位随类型不同而有差异:NOR Flash 可以按字节写入,单次不能超过 Page;NAND Flash 必须是整页写入。W25Q64 是 NOR Flash:
    • W25Q64(NOR):1 ≤ 写入长度 ≤ Page(灵活,上限受限);
    • NAND:写入长度 = Page(刚性,必须对齐);
  • 地址回卷(Wrap):如果地址最后一个字节不为 0(即起始地址不是 256 对齐),且发送数据超过了该页剩余长度,地址将回卷到该页开头。可以把页理解为一个容量 256 的漏斗:可以只滴一滴水(1 字节),也可以倒满,但一次倒超过 256 就会溢出(卷绕);
  • 为什么叫"页编程":SPI 协议中发送地址后持续发数据,芯片内部地址指针自动 +1,但自动 +1 的逻辑被限制在页边界内(低 8 位地址自增)。如果叫"随机编程",理论上能从 0x00FF 连续写到 0x0100;但因为是"页编程",它会在 0x00FF 之后跳回 0x0000。这种物理地址指针不进位的特性,决定了它必须以"页"为边界进行逻辑划分;
  • 如果发送数据超过 256 字节,地址回卷到页起始位置并覆盖之前发送的数据。
cpp 复制代码
void BSP_W25Qxx_Write_Page(uint32_t page_address, uint16_t page_inner_offset,
                           uint8_t *wbuffer, uint16_t num_byte)
{
    if (!w25qxx_init) return;
    if (wbuffer == NULL || num_byte == 0) return;
    // 参数检查:确保写入的数据不会超出页面边界(不能跨页)
    if (page_inner_offset + num_byte > w25qxx.page_size)
        return;

    // 目标地址 = 页号 * 页大小 + 页内偏移
    uint32_t write_address = page_address * w25qxx.page_size + page_inner_offset;

    // 命令格式: PAGE_PROGRAM + 3字节地址(高位在前)
    uint8_t cmd[] = {
        W25QXX_PAGE_PROGRAM,                // 0x02
        (uint8_t)(write_address >> 16),
        (uint8_t)(write_address >> 8),
        (uint8_t)(write_address)
    };

    HAL_StatusTypeDef state = HAL_OK;

    // 发送写使能命令,允许后续写入操作
    BSP_W25Qxx_Write_Enable();
    BSP_W25Qxx_CS_Enable();

    // 发送页编程命令及地址信息
    state = HAL_SPI_Transmit(w25qxx.spi_handler, cmd, 4, HAL_MAX_DELAY);
    if (state != HAL_OK) {
        printf("BSP_W25Qxx_Write_Page: write cmd 0x%02x failed, status = %d\n", cmd[0], state);
        BSP_W25Qxx_CS_Disable();
        return;
    }
    printf("BSP_W25Qxx_Write_Page: write cmd 0x%02x done, status = %d\n", cmd[0], state);

    // 通过SPI发送要写入的数据
    state = HAL_SPI_Transmit(w25qxx.spi_handler, wbuffer, num_byte, HAL_MAX_DELAY);
    if (state != HAL_OK) {
        printf("BSP_W25Qxx_Write_Page: write data 0x%02x failed, status = %d\n", cmd[0], state);
        BSP_W25Qxx_CS_Disable();
        return;
    }
    printf("BSP_W25Qxx_Write_Page: write data 0x%02x done, status = %d\n", cmd[0], state);

    // 禁用片选信号并启动写入过程
    BSP_W25Qxx_CS_Disable();
    // 等待写入操作完成
    BSP_W25Qxx_Waitfor_Write_Complete();
}

5.13 读数据

指令码 03h:

/CS 拉低 → 移入指令码 + 24 位地址 → 数据在 CLK 下降沿从 DO 移出(MSB 优先),每移出一个字节地址自动 +1,可实现连续读取 → /CS 拉高结束。

读取操作的三大特性:

  1. 不用考虑 page / sector / block,在容量范围内没有大小限制;
  2. 读取具有实时性,不用阻塞等待,没有擦除/页编程"操作完成时长"的概念;
  3. 读取数据指令不需要使用 WEL 标志。

若在擦除、编程或写入周期中(BUSY=1)发送读取指令,该指令会被忽略,且不会对当前周期产生影响。另外 03h 仅支持标准 SPI 模式。

cpp 复制代码
void BSP_W25Qxx_Read_Data(uint32_t read_address, uint8_t *rbuffer, uint32_t rsize)
{
    if (!w25qxx_init) return;
    if (rbuffer == NULL || rsize == 0) return;
    // 检查读取地址+长度是否超出Flash容量范围
    if (read_address + rsize > w25qxx.mem_size)
        return;

    // 如果设备正在执行其他操作(如擦除或写入),等待其完成
    if (BSP_W25Qxx_Is_Busy())
        BSP_W25Qxx_Waitfor_Write_Complete();

    // 构建读取命令序列:命令码 + 3字节地址
    uint8_t cmd[] = {
        W25QXX_READ_DATA,                   // 0x03
        (uint8_t)(read_address >> 16),
        (uint8_t)(read_address >> 8),
        (uint8_t)(read_address)
    };
    HAL_StatusTypeDef state = HAL_OK;

    BSP_W25Qxx_CS_Enable();

    state = HAL_SPI_Transmit(w25qxx.spi_handler, cmd, 4, HAL_MAX_DELAY);
    if (state != HAL_OK) {
        printf("BSP_W25Qxx_Read_Data: send read cmd 0x%02x failed, status = %d\n", cmd[0], state);
        goto END;
    }
    printf("BSP_W25Qxx_Read_Data: send read cmd 0x%02x done, status = %d\n", cmd[0], state);

    state = HAL_SPI_Receive(w25qxx.spi_handler, rbuffer, rsize, HAL_MAX_DELAY);
    if (state != HAL_OK) {
        printf("BSP_W25Qxx_Read_Data: read data 0x%02x failed, status = %d\n", cmd[0], state);
        goto END;
    }
    printf("BSP_W25Qxx_Read_Data: read data 0x%02x done, status = %d\n", cmd[0], state);

END:
    BSP_W25Qxx_CS_Disable();
}

擦除 + 写入 + 读取的完整测试流程:

cpp 复制代码
// 1. 先擦除,才能后续写入
printf("----------------Erase Begin----------------\n");
BSP_W25Qxx_Sector_Erase(4096);
printf("erase 4096 address done.\n");
HAL_Delay(2000);
printf("----------------Erase Done----------------\r\n\r\n");

// 2. 读取确认(擦除后应为全0xFF)
uint8_t rbuffer[256];
BSP_W25Qxx_Read_Data(4096, rbuffer, 256);
// ... 打印 ...

// 3. 写入字符串(写入第二个扇区的第一个page,页内偏移0)
uint8_t *message = (uint8_t*)"hello world, hello bit!";
uint32_t len = strlen((const char*)message) + 1;   // 把\0也带上
BSP_W25Qxx_Write_Page(16, 0, message, len);
printf("Write message: %s\n", message);

// 4. 再读出来验证
BSP_W25Qxx_Read_Data(4096, rbuffer, len);
printf("read message: %s\n", rbuffer);

Led4_On();
Led5_On();

串口输出:

cpp 复制代码
w25qxx init success, ID=00ef4017
Bsp_W25Qxx_Sector_Erase: Sector Erase cmd 0x20 done, status = 0
erase 4096 address done.
Erase Done
----------------Read Begin
Bsp_W25Qxx_Read_Data: send read cmd 0x03 done, status = 0
read data: 00000000000000000000000000000000000000000000...
----------------Write Begin
Bsp_W25Qxx_Write_Page: write cmd 0x02 done, status = 0
Bsp_W25Qxx_Write_Page: write data 0x02 done, status = 0
write message: hello world, hello bit!
Write Done
----------------Read Begin
read message: hello world, hello bit!     ← 写入-读取验证通过 ✅
Read Done

5.14 写数据(跨页写入封装)

测试通过了读取和页写入,但使用上存在一个痛点:写入时必须指明写到哪个 page(256 字节),且最多写一个 page

我们想要的是在任意起始地址处写入任意长度的数据。

于是基于底层页编程函数 BSP_W25Qxx_Write_Page 封装**BSP_W25Qxx_Write_Data,内部自动处理分页逻辑:**

cpp 复制代码
void BSP_W25Qxx_Write_Data(uint32_t address, uint8_t *wbuffer, uint32_t wsize)
{
    if (!w25qxx_init) return;

    if (wbuffer == NULL || wsize == 0) {
        printf("BSP_W25Qxx_Write_Data: Invalid params, wbuffer=%p, wsize=%d\n", wbuffer, wsize);
        return;
    }

    if (address + wsize >= w25qxx.mem_size) {
        printf("BSP_W25Qxx_Write_Data: address+wsize 0x%X out of range\r\n", address + wsize);
        return;
    }

    uint32_t bytes_written = 0;      // 已写入字节计数器
    while (bytes_written < wsize) {  // 循环写入直到全部写完
        uint32_t remaining_data = wsize - bytes_written;   // 剩余未写入的数据量
        uint16_t page_number  = address / w25qxx.page_size; // 当前页号
        uint16_t page_offset  = address % w25qxx.page_size; // 页内偏移
        uint16_t remaining_space = w25qxx.page_size - page_offset; // 当前页剩余空间

        // 本次写入量 = min(剩余数据量, 当前页剩余空间)
        // 保证绝对不会跨页;需要跨页就下一次循环再写
        uint16_t to_writen = remaining_data > remaining_space ? remaining_space : remaining_data;

        BSP_W25Qxx_Write_Page(page_number, page_offset, &wbuffer[bytes_written], to_writen);

        bytes_written += to_writen;
        address += to_writen;
    }
}

至此,驱动所需的全部函数已完成。


六、Flash 测试

实际写代码的时候,最好边写边测。

6.1 功能测试项

序号 测试项
W25Q64 整片擦除测试
W25Q64 随机写入测试
W25Q64 随机读取测试
W25Q64 扇区擦除测试
W25Q64 页写入测试
W25Q64 页读取测试
W25Q64 周期性单字节读取测试

测试代码要点(main.c):

cpp 复制代码
/* USER CODE BEGIN PD */
#define W25QXX_PAGE_SIZE 256          // 为了测试
/* USER CODE END PD */

/* USER CODE BEGIN PV */
uint8_t rx_buffer[W25QXX_PAGE_SIZE]; // 读取缓存
uint8_t tx_buffer[W25QXX_PAGE_SIZE]; // 发送缓存
uint32_t first_page_address  = 0;      // 第一个page起始地址
uint32_t last_page_address   = 0x7FFF00; // 最后一个page起始地址
uint32_t first_sector_address = 0;     // 第一个sector起始地址
uint32_t last_sector_address  = 0x7FF000; // 最后一个sector起始地址
/* USER CODE END PV */
  • 整片擦除测试:BSP_W25Qxx_Chip_Erase() → 读首末页,应全为 0xff
  • 随机写测试:BSP_W25Qxx_Write_Data(first_page_address, "hello, bit!", ...)Write_Data(last_page_address, "w25q64 flash.", ...)
  • 随机读测试:分别读回首末页内容验证;
  • 扇区擦除测试:Sector_Erase(first_sector_address)Sector_Erase(last_sector_address),擦除后读回全 0xff
  • 页写入测试:tx_buffer[idx] = idx(首页 00FF)和 tx_buffer[idx] = 255 - idx(末页 FF00),写满一页;
  • 页读取测试:按 16 字节一行十六进制打印,首页应为 00 01 02 ... FF,末页应为 FF FE ... 00
  • 周期性单字节读取:while(1) 中 Read_Data(addr0, &data0, 1),offset 自增,验证任意地址单字节读取。

6.2 下载验证(串口输出节选)

cpp 复制代码
w25qxx init success, ID=0x00ef4017
1. W25Qxx Test Begin:
2. W25Qxx EraseChip Begin:
Read Page First: 0xff,0xff | 0xff, 0xff      ← 整片擦除成功
Read Page Last: 0xff,0xff | 0xff, 0xff
3. W25Qxx Random Write Begin:
Write First Page: hello, bit!
Write Last Page: w25q64 flash.
4. W25Qxx Random Read Begin:
Read First Page: hello, bit!                ← 随机读写一致
Read Last Page: w25q64 flash.
5.W25Qxx PageErase Begin:
Erase First Sector
Read Page First: 0xff,0xff | 0xff, 0xff      ← 扇区擦除成功
...
6.W25Qxx PageWrite Begin:
Write Page First: 0x00,0x01 | 0xfe, 0xff
Write Page Last: 0xff,0xfe | 0x01, 0x00
7.W25Qxx PageRead Begin:
Read Page First: 00 01 02 ... FD FE FF        ← 页写入读取一致
Read Page Last: FF FE FD ... 02 01 00
Read: first(00000000)=00, last(000001ff)=ff   ← 周期单字节读取
Read: first(00000001)=01, last(000001ff)=fe
...

6.3 回卷(Wrap Around)验证

验证"地址超过页边界后回卷到页开头"的特性。测试前需临时去掉 BSP_W25Qxx_Write_Page 中的页边界安全检查(page_inner_offset + num_byte > page_size 的检测)。

cpp 复制代码
// 先擦除,然后才能写入
BSP_W25Qxx_Sector_Erase(0);

// 欺骗W25Q64:前256字节全写0xFF(写入0xFF等于没写),再追加4字节
uint8_t wrap_around[260] = {0};
for (uint32_t idx = 0; idx < 256; idx++)
    wrap_around[idx] = 0xFF;
wrap_around[256] = 0x1;
wrap_around[257] = 0x2;
wrap_around[258] = 0x3;
wrap_around[259] = 0x4;

// 一次写入260字节 → 超过页容量,发生回卷
BSP_W25Qxx_Write_Page(0, 0, wrap_around, 260);

// 读取该页验证
uint8_t wrap_around_test[256] = {0};
BSP_W25Qxx_Read_Data(0, wrap_around_test, 256);
for (uint32_t idx = 0; idx < 256; idx++) {
    printf("%03d ", wrap_around_test[idx]);
    if ((idx + 1) % 16 == 0)
        printf("\n");
}

串口结果:

复制代码
001 002 003 004 255 255 255 255 255 255 255 255 255 255 255 255
255 255 255 255 ...

现象很明显:

第 257~260 字节(01 02 03 04)被回卷写到了页的开头位置,覆盖了原来的 0xFF。这就是"地址回卷"的直观证据。


七、附录:完整指令码定义(Bsp_W25Q64.h)

cpp 复制代码
/**
 * @brief W25Qxx系列Flash芯片命令定义
 */
#define W25QXX_WRITE_ENABLE     0x06   // 写使能命令:允许对Flash进行写入操作
#define W25QXX_WRITE_DISABLE    0x04   // 写禁用命令:禁止对Flash进行写入操作
#define W25QXX_READ_STATUS_REG1 0x05   // 读取状态寄存器1命令
#define W25QXX_READ_STATUS_REG2 0x35   // 读取状态寄存器2命令
#define W25QXX_WRITE_STATUS_REG 0x01   // 写入状态寄存器命令
#define W25QXX_PAGE_PROGRAM     0x02   // 页编程命令:写入一页数据(最大256字节)
#define W25QXX_QUAD_PAGE_PROGRAM 0x32  // 四通道页编程命令
#define W25QXX_BLOCK_ERASE      0xD8   // 块擦除命令:擦除一个块(通常为64KB)
#define W25QXX_SECTOR_ERASE     0x20   // 扇区擦除命令:擦除一个扇区(通常为4KB)
#define W25QXX_CHIP_ERASE       0xC7   // 整片擦除命令:擦除整个Flash芯片
#define W25QXX_ERASE_SUSPEND    0x75   // 擦除暂停命令
#define W25QXX_ERASE_RESUME     0x7A   // 擦除恢复命令
#define W25QXX_POWER_DOWN       0xB9   // 掉电模式命令
#define W25QXX_HIGH_PERF_MODE   0xA3   // 高性能模式命令
#define W25QXX_CONTINUOUS_READ  0x03   // 连续读取命令
#define W25QXX_FAST_READ        0x0B   // 快速读取命令
#define W25QXX_READ_DATA        0x03   // 读取数据命令(同连续读取)
#define W25QXX_FAST_READ_DUAL   0x3B   // 双输出快速读取命令
#define W25QXX_JEDEC_ID         0x9F   // 读取JEDEC ID命令
#define W25QXX_RELEASE_PDWN     0xAB   // 退出掉电模式命令

/* 状态寄存器位定义 */
#define W25QXX_STATUS_BUSY  0x01       // 1 = 芯片正在忙(擦除或写入操作中)
#define W25QXX_STATUS_WR_EN 0x02       // 1 = 芯片已处于写使能状态

八、总结

一次完整的 SPI Flash 驱动开发,核心套路其实很固定:

cpp 复制代码
flowchart TD
    A["写使能 06h(WEL=1,入场券)"] --> B["拉低 CS 选中设备"]
    B --> C["发送 指令码 + 24位地址 + 数据"]
    C --> D["拉高 CS(操作在 CS 上升沿触发)"]
    D --> E["轮询读状态寄存器 05h"]
    E --> F{"BUSY = 0 ?"}
    F -- "否" --> E
    F -- "是" --> G["操作完成,WEL 自动清零"]

要点回顾:

  1. 先使能后操作:任何写/擦操作前必须 Write_Enable(),操作完 WEL 自动清零;
  2. 等 BUSY:擦除/编程后必须 Waitfor_Write_Complete() 轮询等待;
  3. 擦除单位:扇区 4KB(低 12 位对齐)、块 64KB、整片约 20s;
  4. 写入单位:页 256 字节,不能跨页,跨页写入由 Write_Data 分页封装;
  5. 地址回卷:单次写入超过页剩余空间会回卷到页开头并覆盖;
  6. 擦除是写入前提:NOR Flash 只能把 1 写为 0,想写数据必须先把目标区域擦成全 1(0xFF);
  7. 别把擦除写进循环:Flash 有寿命,防止误擦除导致 Flash 报废。

工程文件结构

bash 复制代码
SPI_Flash_test/
├── BSP_Drivers/
│   ├── inc/BSP_W25Q64.h          ← 驱动头文件
│   └── src/BSP_W25Q64.c          ← 驱动源文件
├── APP_Test/
│   ├── App_Test.h                ← 测试函数声明
│   └── App_Test.c                ← 测试函数实现
└── Core/
    ├── Inc/main.h                ← CubeMX 生成,无需修改
    └── Src/main.c                ← 主函数(含外设初始化)

BSP_Drivers/inc/BSP_W25Q64.h

cpp 复制代码
#ifndef __BSP_W25Q64_H
#define __BSP_W25Q64_H


#ifdef __cplusplus
    extern "C" {
#endif


#include "stm32f1xx_hal.h"
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>

#define CMD_W25QXX_JEDEC_ID 0x9F
#define CMD_W25QXX_READ_STATUS_REG1 0x05
#define CMD_W25QXX_WRITE_ENABLE 0x06
#define CMD_W25QXX_WRITE_DISABLE 0x04
#define CMD_W25QXX_SECTOR_ERASE 0x20
#define CMD_W25QXX_READ_DATA 0x03
#define CMD_W25QXX_PAGE_PROGRAM 0x02
#define CMD_W25QXX_CHIP_ERASE 0xC7

// 设置BUSY检测位
#define W25Qxx_STATUS_BUSY 0x01 // 0000 0001
#define W25Qxx_STATUS_WEL  0x02 // 0000 0010
#define SECTOR_ADDRESS_MASK 0xFFF

typedef struct 
{
    uint32_t page_size;      // 页大小
    uint32_t sector_size;    // 扇区大小
    uint32_t block_size;     // 块大小
    uint32_t mem_total_size; // 总大小
    uint32_t chip_id;        // 芯片ID
    SPI_HandleTypeDef *hspi; // spi句柄指针
    GPIO_TypeDef *gpio_port; // 从设备选择引脚端口GPIOE
    uint16_t nss_pin;        // 片选针脚
}spi_flash_t;


bool BSP_W25Qxx_Init(spi_flash_t *init);
uint32_t BSP_W25Qxx_ID(void);
uint8_t BSP_W25Qxx_Statue1_Reg(void);
void BSP_W25Qxx_Write_Enable(void);
void BSP_W25Qxx_Write_Disable(void);
void BSP_W25Qxx_Waitfor_Write_Complete(void);
void BSP_W25Qxx_Waitfor_Write_Complete_V2(void);
void BSP_W25Qxx_Sector_Erase(uint32_t sector_address);
void BSP_W25Qxx_Chip_Erase(void);
// void BSP_W25Qxx_Block_Erase(void);
bool BSP_W25Qxx_Page_Program(uint32_t page_address, uint16_t page_inner_offset, uint8_t *wbuffer, uint16_t wsize);
bool BSP_W25Qxx_Read_Data(uint32_t read_address, uint8_t *rbuffer, uint32_t rsize);
bool BSP_W25Qxx_Write_Data(uint32_t write_address, uint8_t *wbuffer, uint32_t wsize);

#ifdef __cplusplus
}
#endif

#endif

BSP_Drivers/src/BSP_W25Q64.c

cpp 复制代码
#include "BSP_W25Q64.h"
#include "BSP_LED.h"

spi_flash_t g_w25qxx;
static bool g_is_init = false;
const uint8_t dummy = 0x00;

// GPIOE, PIN6, 拉低片选
static void BSP_W25Qxx_CS_Enable(void)
{
    if (!g_is_init)
        return;
    HAL_GPIO_WritePin(g_w25qxx.gpio_port, g_w25qxx.nss_pin, GPIO_PIN_RESET);
}

static void BSP_W25Qxx_CS_Disable(void)
{
    if (!g_is_init)
        return;
    HAL_GPIO_WritePin(g_w25qxx.gpio_port, g_w25qxx.nss_pin, GPIO_PIN_SET);
}

static bool BSP_W25Qxx_Is_busy(void)
{
    return BSP_W25Qxx_Statue1_Reg() & W25Qxx_STATUS_BUSY;
}

static bool BSP_W25Qxx_Is_Wel_Enable(void)
{
    return BSP_W25Qxx_Statue1_Reg() & W25Qxx_STATUS_WEL;
}

bool BSP_W25Qxx_Init(spi_flash_t *init)
{
    if (init == NULL)
        return g_is_init;
    g_w25qxx.page_size = init->page_size;
    g_w25qxx.sector_size = init->sector_size;
    g_w25qxx.block_size = init->block_size;
    g_w25qxx.mem_total_size = init->mem_total_size;
    g_w25qxx.chip_id = init->chip_id;
    g_w25qxx.hspi = init->hspi;
    g_w25qxx.gpio_port = init->gpio_port;
    g_w25qxx.nss_pin = init->nss_pin;
    g_is_init = true;

    // TODO
    uint32_t id = BSP_W25Qxx_ID(); // 获取SPI flash内部的id
    if (g_w25qxx.chip_id != id)
    {
        g_is_init = false;
        printf(">> BSP_W25Qxx_Init error, ID error, set ID is : 0x%x, current ID is: 0x%x\n", g_w25qxx.chip_id, id);
        return g_is_init;
    }
    printf(">> BSP_W25Qxx_Init success! current ID is: 0x%x\n", id);
    return g_is_init;
}

uint32_t BSP_W25Qxx_ID(void)
{
    if (!g_is_init)
        return 0x0;
    BSP_W25Qxx_CS_Enable();

    uint8_t cmd = CMD_W25QXX_JEDEC_ID;
    uint8_t id[3] = {0};
    HAL_StatusTypeDef state = HAL_SPI_Transmit(g_w25qxx.hspi, &cmd, sizeof(cmd), HAL_MAX_DELAY);
    if (state != HAL_OK)
    {
        printf(">> BSP_W25Qxx_ID, HAL_SPI_Transmit error, cmd is : 0x%x failed, state: 0x%x\n", cmd, state);
        memset(id, 0, sizeof(id));
        goto END;
    }
    state = HAL_SPI_Receive(g_w25qxx.hspi, id, sizeof(id), HAL_MAX_DELAY);
    if (state != HAL_OK)
    {
        printf(">> BSP_W25Qxx_ID, HAL_SPI_Receive error, cmd is : 0x%x failed, state: 0x%x\n", cmd, state);
        memset(id, 0, sizeof(id));
        goto END;
    }
END:
    BSP_W25Qxx_CS_Disable();
    return ((id[0] << 16) | (id[1] << 8) | (id[2])); // 0000 0000 0000 0000 0000 0000 0000 0000
}

uint8_t BSP_W25Qxx_Statue1_Reg(void)
{
    if (!g_is_init)
        return 0xFF;
    uint8_t cmd = CMD_W25QXX_READ_STATUS_REG1;
    uint8_t status_reg = 0xFF;
    BSP_W25Qxx_CS_Enable();
    HAL_StatusTypeDef state = HAL_SPI_Transmit(g_w25qxx.hspi, &cmd, sizeof(cmd), HAL_MAX_DELAY);
    if (state != HAL_OK)
    {
        printf(">> BSP_W25Qxx_Statue1_Reg HAL_SPI_Transmit error, cmd is : 0x%x failed, state: 0x%x\n", cmd, state);
        goto END;
    }
    state = HAL_SPI_Receive(g_w25qxx.hspi, &status_reg, sizeof(status_reg), HAL_MAX_DELAY);
    if (state != HAL_OK)
    {
        status_reg = 0xFF;
        printf(">> BSP_W25Qxx_Statue1_Reg HAL_SPI_Receive error, cmd is : 0x%x failed, state: 0x%x\n", cmd, state);
        goto END;
    }

END:
    BSP_W25Qxx_CS_Disable();
    return status_reg;
}

void BSP_W25Qxx_Write_Enable(void)
{
    if (!g_is_init)
        return;
    uint8_t cmd = CMD_W25QXX_WRITE_ENABLE;
    BSP_W25Qxx_CS_Enable();
    HAL_StatusTypeDef state = HAL_SPI_Transmit(g_w25qxx.hspi, &cmd, sizeof(cmd), HAL_MAX_DELAY);
    if (state != HAL_OK)
    {
        printf(">> BSP_W25Qxx_Write_Enable HAL_SPI_Transmit error, cmd is : 0x%x failed, state: 0x%x\n", cmd, state);
    }
    BSP_W25Qxx_CS_Disable();
}

void BSP_W25Qxx_Write_Disable(void)
{
    if (!g_is_init)
        return;
    uint8_t cmd = CMD_W25QXX_WRITE_DISABLE;
    BSP_W25Qxx_CS_Enable();
    HAL_StatusTypeDef state = HAL_SPI_Transmit(g_w25qxx.hspi, &cmd, sizeof(cmd), HAL_MAX_DELAY);
    if (state != HAL_OK)
    {
        printf(">> BSP_W25Qxx_Write_Enable HAL_SPI_Transmit error, cmd is : 0x%x failed, state: 0x%x\n", cmd, state);
    }
    BSP_W25Qxx_CS_Disable();
}

void BSP_W25Qxx_Waitfor_Write_Complete(void)
{
    if (!g_is_init)
        return;
    // while (BSP_W25Qxx_Statue1_Reg() & W25Qxx_STATUS_BUSY)
    //     ;

    int cnt  = 0;
    printf(">> 整盘擦除中\n");
    while (BSP_W25Qxx_Statue1_Reg() & W25Qxx_STATUS_BUSY)
    {
        printf(".");
        cnt++;
        LED_Blink(LED1, 1, 500);
    }
    printf("\n>> 整盘擦除完成, 花费时间: %dS\n", cnt);
}

void BSP_W25Qxx_Waitfor_Write_Complete_V2(void)
{
    if (!g_is_init)
        return;
    uint8_t cmd = CMD_W25QXX_READ_STATUS_REG1;
    uint8_t reg1 = 0xFF;
    BSP_W25Qxx_CS_Enable();
    HAL_StatusTypeDef state = HAL_SPI_Transmit(g_w25qxx.hspi, &cmd, sizeof(cmd), HAL_MAX_DELAY);
    if (state != HAL_OK)
    {
        printf(">> BSP_W25Qxx_Waitfor_Write_Complete_V2 HAL_SPI_Transmit error, cmd is : 0x%x failed, state: 0x%x\n", cmd, state);
        goto END;
    }
    do
    {
        HAL_SPI_TransmitReceive(g_w25qxx.hspi, &dummy, &reg1, sizeof(dummy), HAL_MAX_DELAY);
    } while (reg1 & W25Qxx_STATUS_BUSY);
END:
    BSP_W25Qxx_CS_Disable();
}

void BSP_W25Qxx_Sector_Erase(uint32_t sector_address)
{
    if (!g_is_init)
        return;
    if (sector_address & SECTOR_ADDRESS_MASK)
    {
        printf(">>> BSP_W25Qxx_Sector_Erase error, address 0x%x not sector aligned\n", sector_address);
        return;
    }
    if (sector_address >= g_w25qxx.mem_total_size)
    {
        printf(">>> BSP_W25Qxx_Sector_Erase error, address 0x%x out of range max flash size\n", sector_address);
        return;
    }
    // cmd[0] = CMD_W25QXX_SECTOR_ERASE;
    // cmd[1] = (sector_address>>16)
    // 1. 检测WEL位是否被设置1,如果为0才需要设置 2. 设置
    if (!BSP_W25Qxx_Is_Wel_Enable())
    {
        BSP_W25Qxx_Write_Enable();
    }

    uint8_t cmd[4] = {CMD_W25QXX_SECTOR_ERASE, (sector_address >> 16), (sector_address >> 8), sector_address};
    BSP_W25Qxx_CS_Enable();
    HAL_StatusTypeDef state = HAL_SPI_Transmit(g_w25qxx.hspi, cmd, sizeof(cmd) / sizeof(cmd[0]), HAL_MAX_DELAY);
    if (state != HAL_OK)
    {
        printf(">> BSP_W25Qxx_Sector_Erase HAL_SPI_Transmit error, cmd is : 0x%x failed, state: 0x%x\n", cmd[0], state);
        goto END;
    }
    printf(">> BSP_W25Qxx_Sector_Erase success\n"); // 擦除其实没有成功!!!!
END:
    BSP_W25Qxx_CS_Disable();

    if (state == HAL_OK)
        BSP_W25Qxx_Waitfor_Write_Complete_V2();
}

void BSP_W25Qxx_Chip_Erase(void)
{
    if (!g_is_init)
        return;

    // 1. 检测WEL位是否被设置1,如果为0才需要设置 2. 设置
    if (!BSP_W25Qxx_Is_Wel_Enable())
    {
        BSP_W25Qxx_Write_Enable();
    }

    // 正片擦除的逻辑
    uint8_t cmd = CMD_W25QXX_CHIP_ERASE;
    BSP_W25Qxx_CS_Enable();
    HAL_StatusTypeDef state = HAL_SPI_Transmit(g_w25qxx.hspi, &cmd, sizeof(cmd), HAL_MAX_DELAY);
    if (state != HAL_OK)
    {
        printf(">> BSP_W25Qxx_Chip_Erase HAL_SPI_Transmit error, cmd is : 0x%x failed, state: 0x%x\n", cmd, state);
    }
    BSP_W25Qxx_CS_Disable();
    if (state == HAL_OK)
        BSP_W25Qxx_Waitfor_Write_Complete();
}

bool BSP_W25Qxx_Read_Data(uint32_t read_address, uint8_t *rbuffer, uint32_t rsize)
{
    if (!g_is_init)
        return false;
    if (rbuffer == NULL || rsize == 0)
        return false;
    if (read_address + rsize > g_w25qxx.mem_total_size)
        return false;

    bool ret = true;
    // 1. 检测W25qxx是否在忙 2. 等待忙完
    if (BSP_W25Qxx_Is_busy())
        BSP_W25Qxx_Waitfor_Write_Complete_V2();

    uint8_t cmd[] = {CMD_W25QXX_READ_DATA, (uint8_t)(read_address >> 16), (uint8_t)(read_address >> 8), (uint8_t)(read_address)};
    uint16_t cmdlen = sizeof(cmd) / sizeof(cmd[0]);

    // 2. 发送
    BSP_W25Qxx_CS_Enable();
    HAL_StatusTypeDef state = HAL_SPI_Transmit(g_w25qxx.hspi, cmd, cmdlen, HAL_MAX_DELAY);
    if (state != HAL_OK)
    {
        ret = false;
        printf(">> BSP_W25Qxx_Read_Data HAL_SPI_Transmit error, cmd is : 0x%x failed, state: 0x%x\n", cmd[0], state);
        goto END;
    }

    state = HAL_SPI_Receive(g_w25qxx.hspi, rbuffer, (uint16_t)rsize, HAL_MAX_DELAY);
    if (state != HAL_OK)
    {
        ret = false;
        printf(">> BSP_W25Qxx_Read_Data HAL_SPI_Receive error, cmd is : 0x%x failed, state: 0x%x\n", cmd[0], state);
        goto END;
    }
    printf(">> BSP_W25Qxx_Read_Data success, cmd is : 0x%x, state: 0x%x\n", cmd[0], state);

END:
    BSP_W25Qxx_CS_Disable();
    return ret;
}

// 这个是对页进行写入的函数
bool BSP_W25Qxx_Page_Program(uint32_t page_address, uint16_t page_inner_offset, uint8_t *wbuffer, uint16_t wsize)
{
    if (!g_is_init)
        return false;
    if (wbuffer == NULL || wsize == 0)
        return false;
    // 不能超过一页大小
    if (page_inner_offset + wsize > g_w25qxx.page_size)
        return false;
    // 真实的写入地址
    uint32_t write_address = page_address * g_w25qxx.page_size + page_inner_offset;

    bool ret = true;

    // 构建指令
    uint8_t cmd[] = {CMD_W25QXX_PAGE_PROGRAM, (uint8_t)(write_address >> 16), (uint8_t)(write_address >> 8), (uint8_t)(write_address)};
    uint16_t cmdlen = sizeof(cmd) / sizeof(cmd[0]);

    // 发送指令,首选需要关闭禁止写入
    // 1. 检测WEL位是否被设置1,如果为0才需要设置 2. 设置
    if (!BSP_W25Qxx_Is_Wel_Enable())
    {
        BSP_W25Qxx_Write_Enable();
    }

    BSP_W25Qxx_CS_Enable();

    HAL_StatusTypeDef state = HAL_SPI_Transmit(g_w25qxx.hspi, cmd, cmdlen, HAL_MAX_DELAY);
    if (state != HAL_OK)
    {
        ret = false;
        printf(">> BSP_W25Qxx_Page_Program HAL_SPI_Transmit cmd error, cmd is : 0x%x failed, state: 0x%x\n", cmd[0], state);
        goto END;
    }

    // 发送数据
    state = HAL_SPI_Transmit(g_w25qxx.hspi, wbuffer, wsize, HAL_MAX_DELAY);
    if (state != HAL_OK)
    {
        ret = false;
        printf(">> BSP_W25Qxx_Page_Program HAL_SPI_Transmit data error, cmd is : 0x%x failed, state: 0x%x\n", cmd[0], state);
        goto END;
    }
    printf(">> BSP_W25Qxx_Page_Program HAL_SPI_Transmit cmd && data success, cmd is : 0x%x, state: 0x%x\n", cmd[0], state);

END:
    BSP_W25Qxx_CS_Disable();
    if (state == HAL_OK)
        BSP_W25Qxx_Waitfor_Write_Complete_V2();
    return ret;
}

bool BSP_W25Qxx_Write_Data(uint32_t write_address, uint8_t *wbuffer, uint32_t wsize)
{
    if (!g_is_init)
        return false;
    if (wbuffer == NULL || wsize == 0)
        return false;
    if (write_address + wsize > g_w25qxx.mem_total_size)
    {
        printf("BSP_W25Qxx_Write_Data error, address out range!\n");
        return false;
    }

    // 已经写入的字节数
    uint32_t written_bytes = 0;

    while (written_bytes < wsize)
    {
        // 计算剩余数据
        uint32_t remaining_data = wsize - written_bytes;

        // 计算写入的页号
        uint16_t page_number = write_address / g_w25qxx.page_size;
        // 计算页内偏移
        uint16_t page_inner_offset = write_address % g_w25qxx.page_size;
        // 当前页的剩余空间
        uint16_t remaining_space = g_w25qxx.page_size - page_inner_offset;

        // 计算本次写入的数据量
        uint16_t to_written = remaining_data > remaining_space ? remaining_space : remaining_data;

        // 进行页编程
        bool ret = BSP_W25Qxx_Page_Program(page_number, page_inner_offset, &wbuffer[written_bytes], to_written);
        (void)ret;

        // 更新已经写入的数据量
        written_bytes += to_written;
        // 更新当前写入地址
        write_address += to_written;
    }
    return true;
}

APP_Test/App_Test.h

cpp 复制代码
#ifndef ___APP_TEST_H
#define ___APP_TEST_H

#ifdef __cplusplus
extern "C"
{
#endif

#include "BSP_W25Q64.h"

    void Test_W25Q64_Driver_Init(SPI_HandleTypeDef *hspi, GPIO_TypeDef *port, uint16_t pin);
    void Test_W25Q64_Driver_Reg1(void);
    void Test_W25Q64_Driver_Write_Enable_Disable(void);
    void Test_W25Q64_Driver_Erase_Read(void);
    void Test_W25Q64_Driver_ChipErase_Any_Read_Write(void);
    void For_SPI_Test(SPI_HandleTypeDef *hspi, GPIO_TypeDef *port, uint16_t pin);
#ifdef __cplusplus
}
#endif

#endif

APP_Test/App_Test.c

cpp 复制代码
#include "App_Test.h"

void Test_W25Q64_Driver_Init(SPI_HandleTypeDef *hspi, GPIO_TypeDef *port, uint16_t pin)
{
    printf("--------------Test_W25Q64_Driver_Init-----------\n");
    spi_flash_t init = {
        .page_size = 256,
        .sector_size = 256 * 16,
        .block_size = 256 * 16 * 16,
        .mem_total_size = 256 * 16 * 16 * 128,
        .chip_id = 0xEF4017,
        .hspi = hspi,
        .gpio_port = port,
        .nss_pin = pin};

    BSP_W25Qxx_Init(&init);
}

void Test_W25Q64_Driver_Reg1(void)
{
    printf("--------------Test_W25Q64_Driver_Reg1-----------\n");
    uint8_t reg1 = BSP_W25Qxx_Statue1_Reg();
    printf(">> register1 value is : 0x%x\n", reg1);
}

void Test_W25Q64_Driver_Write_Enable_Disable(void)
{
    printf("--------------Test_W25Q64_Driver_Write_Enable-----------\n");
    uint8_t reg1 = BSP_W25Qxx_Statue1_Reg();
    printf(">> register1 value is : 0x%x\n", reg1);

    BSP_W25Qxx_Write_Enable();

    reg1 = BSP_W25Qxx_Statue1_Reg();
    printf(">> register1 value is : 0x%x\n", reg1);

    BSP_W25Qxx_Write_Disable();

    reg1 = BSP_W25Qxx_Statue1_Reg();
    printf(">> register1 value is : 0x%x\n", reg1);
}

void Test_W25Q64_Driver_Erase_Read(void)
{
    printf("--------------Test_W25Q64_Driver_Erase_Read-----------\n");

    // 1. 先读取数据 && 打印
    uint32_t start_address = 0;
    uint32_t end_address = 4096 - 4;
    uint32_t rx_buffer_first = 0;
    uint32_t rx_buffer_last = 0;

    // 1.1 把page0前4个字节清零
    uint32_t data = 0;
    BSP_W25Qxx_Page_Program(0, 0, (uint8_t *)&data, sizeof(data));

    BSP_W25Qxx_Read_Data(start_address, (uint8_t *)&rx_buffer_first, sizeof(rx_buffer_first));
    BSP_W25Qxx_Read_Data(end_address, (uint8_t *)&rx_buffer_last, sizeof(rx_buffer_last));
    printf(">> 擦除前: rx_buffer_first: 0x%x, rx_buffer_last: 0x%x\n", rx_buffer_first, rx_buffer_last);

    // 2. 擦除
    BSP_W25Qxx_Sector_Erase(start_address);

    // 3. 先读取数据 && 打印
    rx_buffer_first = 0;
    rx_buffer_last = 0;
    BSP_W25Qxx_Read_Data(start_address, (uint8_t *)&rx_buffer_first, sizeof(rx_buffer_first));
    BSP_W25Qxx_Read_Data(end_address, (uint8_t *)&rx_buffer_last, sizeof(rx_buffer_last));
    printf(">> 擦除后: rx_buffer_first: 0x%x, rx_buffer_last: 0x%x\n", rx_buffer_first, rx_buffer_last);
}

uint8_t buffer[4096 * 2];

static void PrintData(int step)
{
    printf("------------------Step: %d-------------\n", step);
    // 先打印前8个字节
    for (int i = 0; i < 8; i++)
        printf("0x%x ", buffer[i]);
    printf("\n");
    // 先打印后8个字节
    for (int i = 4096 * 2 - 8; i < 4096 * 2; i++)
        printf("0x%x ", buffer[i]);
    printf("\n");
}

void Test_W25Q64_Driver_ChipErase_Any_Read_Write(void)
{
    // 1. 确定两个sector
    uint32_t start_address = 0x0;
    uint16_t size = 4096 * 2;
    // 2. 先读取
    BSP_W25Qxx_Read_Data(start_address, buffer, size);

    // 3. 挑选若干数据
    PrintData(1);

    // 4. 擦除两页
    BSP_W25Qxx_Sector_Erase(0);
    BSP_W25Qxx_Sector_Erase(4096);
    BSP_W25Qxx_Read_Data(start_address, buffer, size);
    PrintData(2);

    // 5. 写入数据
    memset(buffer, 0, sizeof(buffer));
    BSP_W25Qxx_Write_Data(start_address, buffer, size);
    BSP_W25Qxx_Read_Data(start_address, buffer, size);
    PrintData(3);

    // 6. 整盘擦除
    BSP_W25Qxx_Chip_Erase();

    // 7. 读取
    BSP_W25Qxx_Read_Data(start_address, buffer, size);
    PrintData(3); //
}

void For_SPI_Test(SPI_HandleTypeDef *hspi, GPIO_TypeDef *port, uint16_t pin)
{
    Test_W25Q64_Driver_Init(hspi, port, pin);
    Test_W25Q64_Driver_Reg1();
    Test_W25Q64_Driver_Write_Enable_Disable();
    Test_W25Q64_Driver_Erase_Read();
    Test_W25Q64_Driver_ChipErase_Any_Read_Write();
}

相关推荐
纳祥科技1 小时前
拆机拆到芯片级:ARC功放机里的NX7026、NX8420、NX6805
单片机·音视频·智能音箱
wuyk5551 小时前
【Socket 进阶之路】第 9 章 Linux 网络服务量产稳定性优化|心跳保活、TIME_WAIT、SO_LINGER、内存池、断线重连、完整异常防护框架
linux·服务器·开发语言·网络·物联网
邪修king1 小时前
Re:Linux 系统篇(三十四):进程间通信开篇Chapter1 —— 匿名管道 pipe 深度拆解与代码实战
android·linux·运维·开发语言
孙启超2 小时前
【AI开发之Rust】第 13 课:async/await 与 tokio 异步运行时
开发语言·后端·rust
Am-Chestnuts2 小时前
DeepSeek内容转Word有哪几种方法?用DS随心转对比6条主流路径
开发语言·人工智能
大黄说说2 小时前
App 启动速度优化:冷启动从 3s 降到 800ms 完整实操
开发语言
Lazionr2 小时前
多态:从多种形态到运行时绑定
开发语言·c++