前言

  以W25Q64芯片为例,将从芯片概述、引脚描述、时序介绍、存储框图、案例代码等几个方向展开介绍。部分图片及内容摘自芯片手册。驱动代码方面提供了W25Q64和W25Q128两个版本,后者为笔者后续重新构建,版本较新,可供参考。STM32CubeMX的相关配置只会展示关键部分,建议具备一定基础后再阅读

芯片概述

  W25Q64是Winbond 25Q系列的一款串行NOR闪存,适用于代码映射至RAM、直接从双/四路SPI执行代码,以及存储语音、文本与数据。工作电压2.7V~3.6V,待机电流低至1µA。存储阵列包含32768个可编程页,每页256字节,单次最多可编程256字节。擦除按组进行:16页为1个扇区即4KB,128页为32KB块,256页为64KB块,也可整片擦除。通信上支持标准SPI、双I/O及四I/O SPI。此外,该芯片支持JEDEC标准制造商ID和设备ID,带64位唯一序列号,以及3个256字节的安全寄存器。在容量方面,25Q系列中Q后面的数字代表容量大小,如W25Q128表示128Mbit即16MByte,而W25Q64即为64Mbit即8MByte。该芯片容量8MB,地址线占用24位,因此16MB及以下容量的W25Q系列24位寻址代码基本通用;超过此容量则需开启4字节寻址模式

引脚描述

在这里插入图片描述

引脚名称 I/O 功能
/CS I 片选输入
DO I/O 数据输出
/WP I/O 写保护输入
GND
DI I/O 数据输入
CLK I 串行时钟输入
/HOLD I/O 保持输入
VCC 电源
  • /CS必须从高电平转换为低电平,才能接收新的指令
  • /HOLD可以理解成设备选中引脚,通常在多设备使用同一SPI总线时使用

时序概要

芯片支持标准SPI协议,以常用的获取芯片ID的时序为例:命令"9Fh"即十六进制9F,根据手册指令集表或时序注释可知,该命令下返回的三个字节分别代表制造商、内容类型和容量。笔者使用的W25Q64JV执行后返回EF 40 17,表示华邦制造、SPI类型、64Mbit即8M字节容量,得到这一信息即证明时序通畅

在这里插入图片描述

存储框图

在写入操作上,每次最多只能写1页,即256字节;擦除则必须按组进行,可选粒度为16页即一个扇区4KB、128页即32KB块、256页即64KB块,或整片擦除,这些分别对应着扇区擦除、32KB块擦除、块擦除、全片擦除等常见术语。NOR Flash的特性决定了擦除操作会将目标区域所有位强制置1,擦除后每个字节读取为0xFF,而编程即写入只能将已为1的位改写为0,因此覆盖旧数据前必须先擦除

在这里插入图片描述

256字节=1页=116扇区=1256块 256字节 = 1页 = \frac{1}{16}扇区 = \frac{1}{256}块 256字节=1=161扇区=2561

驱动代码

  两份驱动代码。W25Q64的驱动代码库是从一个小项目剥离出来的,无关部分已尽量抽象为接口函数,方便替换。后续笔者又为W25Q128重新编写了驱动,同样提供了抽象接口函数。以下内容将围绕原理图与配置、代码文件、调试结果三个部分展开,展示芯片的硬件连接以及STM32CubeMX上的配置
  SPI协议根据CPOL时钟极性与CPHA时钟相位的组合,可划分为4种模式,分别为模式0、模式1、模式2、模式3。W25QXX系列芯片仅支持模式0即CPOL=0、CPHA=0,以及模式3即CPOL=1、CPHA=1,在构建SPI配置时务必留意,选错模式将导致通信失败

W25Q64

原理图以及配置

在这里插入图片描述

在这里插入图片描述

程序文件

ZQXY_W25Q64.h

#pragma once
// #include "ZQXY_Core.h"
#include <stdint.h>
#include <string.h>
#include "spi.h"


/*** 寄存器 ***/
// 写操作控制命令
#define ZQXY_W25Q64_WRITE_ENABLE 0x06   // 写使能命令,允许写入和擦除操作
#define ZQXY_W25Q64_WRITE_DISABLE 0x04  // 写禁用命令,禁止写入和擦除操作

// 状态寄存器读写命令
#define ZQXY_W25Q64_READ_STATUS_REGISTER_1 0x05  // 读状态寄存器1(包含忙标志、写使能状态等)
#define ZQXY_W25Q64_READ_STATUS_REGISTER_2 0x35  // 读状态寄存器2(包含四线模式等状态)
#define ZQXY_W25Q64_WRITE_STATUS_REGISTER 0x01   // 写状态寄存器命令

// 编程(写入)命令
#define ZQXY_W25Q64_PAGE_PROGRAM 0x02       // 页编程命令(最多256字节)
#define ZQXY_W25Q64_QUAD_PAGE_PROGRAM 0x32  // 四线页编程命令(提高写入速度)

// 擦除命令
#define ZQXY_W25Q64_BLOCK_ERASE_64KB 0xD8  // 64KB块擦除命令
#define ZQXY_W25Q64_BLOCK_ERASE_32KB 0x52  // 32KB块擦除命令
#define ZQXY_W25Q64_SECTOR_ERASE_4KB 0x20  // 4KB扇区擦除命令(最小擦除单元)
#define ZQXY_W25Q64_CHIP_ERASE 0xC7        // 整片擦除命令(擦除整个芯片)

// 擦除控制命令
#define ZQXY_W25Q64_ERASE_SUSPEND 0x75  // 暂停擦除操作
#define ZQXY_W25Q64_ERASE_RESUME 0x7A   // 恢复擦除操作

// 电源管理命令
#define ZQXY_W25Q64_POWER_DOWN 0xB9                        // 进入深度断电模式(低功耗)
#define ZQXY_W25Q64_HIGH_PERFORMANCE_MODE 0xA3             // 进入高性能模式
#define ZQXY_W25Q64_CONTINUOUS_READ_MODE_RESET 0xFF        // 连续读模式复位命令
#define ZQXY_W25Q64_RELEASE_POWER_DOWN_HPM_DEVICE_ID 0xAB  // 退出断电模式并读取设备ID

// ID读取命令
#define ZQXY_W25Q64_MANUFACTURER_DEVICE_ID 0x90  // 读取制造商和设备ID
#define ZQXY_W25Q64_JEDEC_ID 0x9F                // 读取JEDEC ID(制造商+存储器类型+容量)

// 读取数据命令
#define ZQXY_W25Q64_READ_DATA 0x03                // 普通读取命令(最高25MHz)
#define ZQXY_W25Q64_FAST_READ 0x0B                // 快速读取命令(需要虚拟字节)
#define ZQXY_W25Q64_FAST_READ_DUAL_OUTPUT 0x3B    // 双线输出快速读取
#define ZQXY_W25Q64_FAST_READ_DUAL_IO 0xBB        // 双线IO快速读取
#define ZQXY_W25Q64_FAST_READ_QUAD_OUTPUT 0x6B    // 四线输出快速读取
#define ZQXY_W25Q64_FAST_READ_QUAD_IO 0xEB        // 四线IO快速读取
#define ZQXY_W25Q64_OCTAL_WORD_READ_QUAD_IO 0xE3  // 八字节四线IO读取
#define ZQXY_W25Q64_DUMMY_BYTE 0xFF               // 虚拟字节(用于时序对齐)

/*** 接口函数 ***/
void ZQXY_W25Q64_DelayMs(uint32_t ms);                       // 延时函数
void ZQXY_W25Q64_CS_Control(uint8_t state);                  // CS控制函数
void ZQXY_W25Q64_Transmit(uint8_t* tx_data, uint16_t size);  // SPI发送函数
void ZQXY_W25Q64_Receive(uint8_t* rx_data, uint16_t size);   // SPI接收函数

/*** 基本函数 ***/
void ZQXY_W25Q64_Init(void);           // 初始化W25Q64
void ZQXY_W25Q64_ReadID(uint8_t* id);  // 读取W25Q64 ID
void ZQXY_W25Q64_WriteEnable(void);    // 写使能
void ZQXY_W25Q64_WaitBusy(void);       // 等待设备空闲

void ZQXY_W25Q64_ChipErase(void);                // 整片擦除
void ZQXY_W25Q64_BlockErase(uint32_t address);   // 块擦除
void ZQXY_W25Q64_SectorErase(uint32_t address);  // 扇区擦除

void ZQXY_W25Q64_PageProgram(uint32_t address, const uint8_t* data, uint16_t size);  // 页编程
void ZQXY_W25Q64_ReadData(uint32_t address, uint8_t* data, uint16_t size);           // 读取数据

ZQXY_W25Q64.c

该文件中,记得替换自己的延时函数控制片选引脚函数初始化函数。以及选用的串口调试。如果使用的不是硬件SPI1,那么收发函数也要修改下

#include "ZQXY_W25Q64.h"

static const char* TAG = "[ZQXY_W25Q64]";

// 如果没有串口打印功能,可以取消下面这行注释来禁用日志输出
// #define ZQXY_LOGI(tag, format, ...)

/*** 接口函数 ***/
/**
 * @brief 延时函数
 * @param ms 延时毫秒数
 */
void ZQXY_W25Q64_DelayMs(uint32_t ms) { ZQXY_DelayMs(ms); }

/**
 * @brief 控制片选引脚
 * @param state 1表示片选高电平,0表示片选低电平
 */
void ZQXY_W25Q64_CS_Control(uint8_t state)
{
    if (state == 1) 
    {
        HAL_GPIO_WritePin(W25Q64_CS_GPIO_Port, W25Q64_CS_Pin, GPIO_PIN_SET);
    } 
    else 
    {
        HAL_GPIO_WritePin(W25Q64_CS_GPIO_Port, W25Q64_CS_Pin, GPIO_PIN_RESET);
    }
}

/**
 * @brief SPI发送数据
 * @param tx_data 要发送的数据缓冲区
 * @param size 发送数据的大小
 */
void ZQXY_W25Q64_Transmit(uint8_t* tx_data, uint16_t size)
{
    HAL_SPI_Transmit(&hspi1, tx_data, size, HAL_MAX_DELAY);
}

/**
 * @brief SPI接收数据
 * @param rx_data 接收数据的缓冲区
 * @param size 接收数据的大小
 */
void ZQXY_W25Q64_Receive(uint8_t* rx_data, uint16_t size)
{
    HAL_SPI_Receive(&hspi1, rx_data, size, HAL_MAX_DELAY);
}

/*** 基本函数 ***/
/**
 * @brief 初始化W25Q64
 */
void ZQXY_W25Q64_Init(void)
{
    // 初始化SPI等相关配置
    ZQXY_W25Q64_DelayMs(10);
    HAL_GPIO_WritePin(W25Q64_WP_GPIO_Port, W25Q64_WP_Pin, GPIO_PIN_SET);
    ZQXY_LOGI(TAG, "W25Q64 Initialized");
}

/**
 * @brief 读取W25Q64 ID
 * @param re_id 存储读取ID的缓冲区
 * @note 读取90h返回厂商+ID(2位字节),读取ABh返回ID(1位字节),读取9Fh返回厂商+内存类型+ID(3位字节)
 * @note 厂商:EFh 华邦。ID:14h 16M、15h 32M、16h 64M、17 128M。内存类型:30h 旧版、40h SPI、60h QPI
 * @note 若是没有片选则读不出任何数据,必须先拉低片选引脚
 * 
 */
void ZQXY_W25Q64_ReadID(uint8_t* id)
{
    uint8_t cmd;
    uint8_t rx_data[3];
    memset(rx_data, 0, sizeof(rx_data));

    // 读取JEDEC ID
    cmd = ZQXY_W25Q64_JEDEC_ID;
    ZQXY_W25Q64_CS_Control(0);
    ZQXY_W25Q64_Transmit(&cmd, 1);
    ZQXY_W25Q64_Receive(rx_data, 3);
    ZQXY_W25Q64_CS_Control(1);
    ZQXY_LOGI(TAG, "W25Q64 JEDEC ID: %02X %02X %02X", rx_data[0], rx_data[1], rx_data[2]);

    // 如果提供了ID缓冲区,则将读取的ID存储到该缓冲区
    if (id != NULL) 
    {
        id[0] = rx_data[0];
        id[1] = rx_data[1];
        id[2] = rx_data[2];
    }
}

/**
 * @brief 写使能
 */
void ZQXY_W25Q64_WriteEnable(void)
{
    uint8_t cmd = ZQXY_W25Q64_WRITE_ENABLE;
    
    // 发送命令
    ZQXY_W25Q64_CS_Control(0);  
    ZQXY_W25Q64_Transmit(&cmd, 1);  
    ZQXY_W25Q64_CS_Control(1);
}

/**
 * @brief 等待设备空闲
 */
void ZQXY_W25Q64_WaitBusy(void)
{
    uint8_t cmd = ZQXY_W25Q64_READ_STATUS_REGISTER_1;
    uint8_t status = 0;

    // 轮询状态寄存器,直到忙标志位清零
    do
    {
        ZQXY_W25Q64_CS_Control(0);
        ZQXY_W25Q64_Transmit(&cmd, 1);
        ZQXY_W25Q64_Receive(&status, 1);
        ZQXY_W25Q64_CS_Control(1);
        ZQXY_W25Q64_DelayMs(1);
    } while (status & 0x01);  
}

/**
 * @brief 块擦除
 * @param address 要擦除的地址(64KB对齐)
 */
void ZQXY_W25Q64_BlockErase(uint32_t address)
{
    // 检查地址是否对齐
    if (address % 65536 != 0) 
    {
        ZQXY_LOGI(TAG, "Block erase address must be 64KB aligned");
        return;
    }

    // 写使能
    ZQXY_W25Q64_WriteEnable();

    // 构建块擦除命令
    uint8_t cmd[4];
    cmd[0] = ZQXY_W25Q64_BLOCK_ERASE_64KB;  
    cmd[1] = (address >> 16) & 0xFF;        
    cmd[2] = (address >> 8) & 0xFF;         
    cmd[3] = address & 0xFF;                

    // 发送擦除命令
    ZQXY_W25Q64_CS_Control(0);    
    ZQXY_W25Q64_Transmit(cmd, 4); 
    ZQXY_W25Q64_CS_Control(1);    
    
    // 等待擦除完成
    ZQXY_W25Q64_WaitBusy();  
}

/**
 * @brief 扇区擦除
 * @param address 要擦除的地址(4KB对齐)
 */
void ZQXY_W25Q64_SectorErase(uint32_t address) 
{
    // 检查地址是否对齐
    if (address % 4096 != 0) 
    {
        ZQXY_LOGI(TAG, "Sector erase address must be 4KB aligned");
        return;
    }

    // 写使能
    ZQXY_W25Q64_WriteEnable();
 
    // 构建块擦除命令
    uint8_t cmd[4];
    cmd[0] = ZQXY_W25Q64_SECTOR_ERASE_4KB;
    cmd[1] = (address >> 16) & 0xFF;
    cmd[2] = (address >> 8) & 0xFF;
    cmd[3] = address & 0xFF;
 
    // 发送扇区擦除命令
    ZQXY_W25Q64_CS_Control(0);
    ZQXY_W25Q64_Transmit(cmd, 4);
    ZQXY_W25Q64_CS_Control(1);
    
    // 等待擦除完成
    ZQXY_W25Q64_WaitBusy();  
}
 
/**
 * @brief 整片擦除
 */
void ZQXY_W25Q64_ChipErase(void) 
{
    // 写使能
    ZQXY_W25Q64_WriteEnable();
 
    // 构建整片擦除命令
    uint8_t cmd = ZQXY_W25Q64_CHIP_ERASE;

    // 发送整片擦除命令
    ZQXY_W25Q64_CS_Control(0);
    ZQXY_W25Q64_Transmit(&cmd, 1);
    ZQXY_W25Q64_CS_Control(1);
 
    // 等待擦除完成
    ZQXY_W25Q64_WaitBusy();
}

/**
 * @brief 页编程
 * @param address 编程起始地址
 * @param data 要写入的数据
 * @param size 写入数据的大小(最大256字节)
 */
void ZQXY_W25Q64_PageProgram(uint32_t address, const uint8_t* data, uint16_t size)
{
    // 检查参数
    if (size > 256) 
    {
        ZQXY_LOGI(TAG, "Page program size exceeds 256 bytes");
        return;
    }
    else if (data == NULL || size == 0) 
    {
        ZQXY_LOGI(TAG, "Invalid page program parameters");
        return;
    }

    // 写使能
    ZQXY_W25Q64_WriteEnable();  

    // 构建页编程命令
    uint8_t tx_cmd[4];
    tx_cmd[0] = ZQXY_W25Q64_PAGE_PROGRAM;
    tx_cmd[1] = (address >> 16) & 0xFF;
    tx_cmd[2] = (address >> 8) & 0xFF; 
    tx_cmd[3] = address & 0xFF;        

    // 发送页编程命令和数据
    ZQXY_W25Q64_CS_Control(0);
    ZQXY_W25Q64_Transmit(tx_cmd, 4); 
    ZQXY_W25Q64_Transmit((uint8_t*)data, size);
    ZQXY_W25Q64_CS_Control(1);
    
    // 等待设备空闲
    ZQXY_W25Q64_WaitBusy();  

    ZQXY_LOGI(TAG, "Page program completed at address: 0x%06X", address);
}

/**
 * @brief 读取数据
 * @param address 读取起始地址
 * @param data 存储读取数据的缓冲区
 * @param size 读取数据的大小
 */
void ZQXY_W25Q64_ReadData(uint32_t address, uint8_t* data, uint16_t size)
{
    // 检查参数
    if (size == 0 || data == NULL)
    {
        ZQXY_LOGI(TAG, "Invalid read parameters");
        return;
    }

    // 等待设备空闲
    ZQXY_W25Q64_WaitBusy();  

    // 构建读取命令
    uint8_t rx_cmd[4];
    rx_cmd[0] = ZQXY_W25Q64_READ_DATA;  
    rx_cmd[1] = (address >> 16) & 0xFF; 
    rx_cmd[2] = (address >> 8) & 0xFF;  
    rx_cmd[3] = address & 0xFF;         

    // 发送读取命令和接收数据
    ZQXY_W25Q64_CS_Control(0);
    ZQXY_W25Q64_Transmit(rx_cmd, 4);
    ZQXY_W25Q64_Receive(data, size);
    ZQXY_W25Q64_CS_Control(1);

    ZQXY_LOGI(TAG, "Read %d bytes from address 0x%06X", size, address);
}


main.c文件,仅展示相关代码

int mian()
{
    static const char* TAG = "[ZQXY_W25Q64]";

    // 初始化W25Q64
    ZQXY_W25Q64_Init();  
    // 读取W25Q64 ID
    ZQXY_W25Q64_ReadID();       
    // 擦除整片 + 读取3字节
    ZQXY_W25Q64_ChipErase();    
    ZQXY_W25Q64_ReadData(0x000000, RxDataBuffer, 3);
    ZQXY_LOGI(TAG, "ReadData: %d %d %d", RxDataBuffer[0], RxDataBuffer[1], RxDataBuffer[2]);
    ZQXY_DelayMs(3000);

    // 写入3字节数据 + 读取3字节
    RxDataBuffer[0] = 0x01;
    RxDataBuffer[1] = 0x02;
    RxDataBuffer[2] = 0x03;
    ZQXY_W25Q64_PageProgram(0x000000, RxDataBuffer, 3);
    memset(RxDataBuffer, 0, sizeof(RxDataBuffer));
    ZQXY_W25Q64_ReadData(0x000000, RxDataBuffer, 3);
    ZQXY_LOGI(TAG, "ReadData: %d %d %d", RxDataBuffer[0], RxDataBuffer[1], RxDataBuffer[2]);
    ZQXY_DelayMs(3000);
    
}

调试信息

上述代码在串口中打印出的信息

在这里插入图片描述

W25Q128

该驱动程序文件是基于正点原子开发板上的W25Q128进行编写测试的,其并非华邦的芯片,所以在读取ID的厂家位上有所不同

原理图以及配置

诸如CS引脚这种的并非SPI协议中的标准引脚配置,所以要在STM32CubeMX上,单独配置为GPIO的输出模式

在这里插入图片描述

在这里插入图片描述

程序文件

W25Q128_RES.h

#pragma once

#define W25Q128_WRITE_ENABLE                     0x06
#define W25Q128_WRITE_DISABLE                    0x04
#define W25Q128_READ_STATUS_REGISTER_1           0x05   
#define W25Q128_READ_STATUS_REGISTER_2           0x35
#define W25Q128_WRITE_STATUS_REGISTER            0x01
#define W25Q128_PAGE_PROGRAM                     0x02   // 页编程
#define W25Q128_QUAD_PAGE_PROGRAM                0x32
#define W25Q128_BLOCK_ERASE_64KB                 0xD8
#define W25Q128_BLOCK_ERASE_32KB                 0x52
#define W25Q128_SECTOR_ERASE_4KB                 0x20   // 扇区擦除
#define W25Q128_CHIP_ERASE                       0xC7
#define W25Q128_ERASE_SUSPEND                    0x75
#define W25Q128_ERASE_RESUME                     0x7A
#define W25Q128_POWER_DOWN                       0xB9
#define W25Q128_HIGH_PERFORMANCE_MODE            0xA3
#define W25Q128_CONTINUOUS_READ_MODE_RESET       0xFF
#define W25Q128_RELEASE_POWER_DOWN_HPM_DEVICE_ID 0xAB
#define W25Q128_MANUFACTURER_DEVICE_ID           0x90
#define W25Q128_READ_UNIQUE_ID                   0x4B
#define W25Q128_JEDEC_ID                         0x9F
#define W25Q128_READ_DATA                        0x03
#define W25Q128_FAST_READ                        0x0B
#define W25Q128_FAST_READ_DUAL_OUTPUT            0x3B
#define W25Q128_FAST_READ_DUAL_IO                0xBB
#define W25Q128_FAST_READ_QUAD_OUTPUT            0x6B
#define W25Q128_FAST_READ_QUAD_IO                0xEB
#define W25Q128_OCTAL_WORD_READ_QUAD_IO          0xE3

#define W25Q128_DUMMY_BYTE 0xFF

W25Q128.h

#pragma once

#define W25Q128_SPI_TIMEOUT_MS 100

/* 操作状态枚举 */
typedef enum {
    W25Q128_OK       = 0x00U,
    W25Q128_ERROR    = 0x01U,
    W25Q128_BUSY     = 0x02U,
    W25Q128_TIMEOUT  = 0x03U
} W25Q128_Status_e;

/**
 * @brief  初始化 W25Q128 芯片
 * @note   配置 SPI 外设及 GPIO 引脚, 并唤醒芯片使其进入待命状态
 */
void W25Q128_Init(void);

/**
 * @brief  读取 W25Q128 的制造商 ID 与设备 ID
 * @param  MID 输出参数, 接收 Manufacturer ID (Winbond 为 0xEF)
 * @param  DID 输出参数, 接收 Device ID (W25Q128 为 0x4018)
 */
W25Q128_Status_e W25Q128_ReadID(uint8_t *MID, uint16_t *DID);

/**
 * @brief  按页编程 W25Q128 (最大 256 字节/页)
 * @param  Address   写入的起始地址 (0x000000 ~ 0xFFFFFF)
 * @param  DataArray 指向待写入数据的缓冲区指针
 * @param  Count     待写入的字节数 (不可跨页, 单次 ≤ 256)
 * @note   该函数不会自动处理跨页, 调用者需确保写入不跨越页边界
 */
W25Q128_Status_e W25Q128_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count);

/**
 * @brief  擦除 W25Q128 的一个扇区 (4KB)
 * @param  Address 目标扇区内的任意地址 (自动对齐至扇区首地址)
 * @note   擦除后该扇区所有字节变为 0xFF, 耗时约 45~400ms (典型值)
 */
W25Q128_Status_e W25Q128_SectorErase(uint32_t Address);

/**
 * @brief  从 W25Q128 连续读取数据
 * @param  Address   读取的起始地址 (0x000000 ~ 0xFFFFFF)
 * @param  DataArray 指向接收数据缓冲区的指针
 * @param  Count     待读取的字节数
 */
W25Q128_Status_e W25Q128_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count);



W25Q128.c

#include "main.h"
#include "spi.h"
#include "gpio.h"

#include "W25Q128.h"
#include "W25Q128_RES.h"

// SPI 句柄
#define W25Q128_SPI_HANDLE (&hspi1)

/**
 * @brief CS 引脚控制
 * 
 * @return W25Q128_Status_e 
 */
static void _W25Q128_GPIO_CS(uint8_t value)
{
	HAL_GPIO_WritePin(F_CS_GPIO_Port, F_CS_Pin,value == 0 ? GPIO_PIN_RESET : GPIO_PIN_SET);
}

/**
 * @brief  通过 SPI 发送数据(底层 HAL 接口化封装)
 * @param[in] pData 待发送数据的缓冲区指针
 * @param[in] Size  发送字节数
 * @return W25Q128_Status_e 操作状态
 */
static W25Q128_Status_e _W25Q128_SPI_Transmit(const uint8_t *pData, uint16_t Size)
{
	return HAL_SPI_Transmit(W25Q128_SPI_HANDLE, pData, Size, W25Q128_SPI_TIMEOUT_MS);
}

/**
 * @brief  通过 SPI 接收数据(底层 HAL 接口化封装)
 * @param[out] pData 接收数据缓冲区的指针
 * @param[in]  Size  接收字节数
 * @return W25Q128_Status_e 操作状态
 */
static W25Q128_Status_e _W25Q128_SPI_Receive(uint8_t *pData, uint16_t Size)
{
	return HAL_SPI_Receive(W25Q128_SPI_HANDLE, pData, Size, W25Q128_SPI_TIMEOUT_MS);
}

/**
 * @brief  通过 SPI 全双工收发数据(底层 HAL 接口化封装)
 * @param[in]  pTxData 待发送数据的缓冲区指针
 * @param[out] pRxData 接收数据缓冲区的指针
 * @param[in]  Size    收发字节数
 * @return W25Q128_Status_e 操作状态
 */
static W25Q128_Status_e _W25Q128_SPI_TransmitReceive(const uint8_t *pTxData, uint8_t *pRxData, uint16_t Size)
{
	return HAL_SPI_TransmitReceive(W25Q128_SPI_HANDLE, pTxData, pRxData, Size, W25Q128_SPI_TIMEOUT_MS);
}


/**
 * @brief 临界区锁 (关中断), 保护 WriteEnable → 写/擦命令 之间的 WEL 位
 */
static void _W25Q128_Lock(void)
{
	__disable_irq();
}

/**
 * @brief 临界区解锁 (开中断)
 */
static void _W25Q128_Unlock(void)
{
	__enable_irq();
}

//写入先擦除
//读取能跨页,写入不能,只能回到页首。
void W25Q128_Init()
{
	_W25Q128_GPIO_CS(1);
}

/**
 * @brief 读取 ID
 * 
 * @param MID 
 * @param DID 
 * @return W25Q128_Status_e 
 */
W25Q128_Status_e W25Q128_ReadID(uint8_t *MID,uint16_t *DID)
{
	uint8_t tx_data[4] = {W25Q128_JEDEC_ID, W25Q128_DUMMY_BYTE, W25Q128_DUMMY_BYTE, W25Q128_DUMMY_BYTE};
	uint8_t rx_data[4] = {W25Q128_DUMMY_BYTE, W25Q128_DUMMY_BYTE, W25Q128_DUMMY_BYTE, W25Q128_DUMMY_BYTE};
	W25Q128_Status_e status = W25Q128_ERROR;

	_W25Q128_GPIO_CS(1);
	HAL_Delay(1);
	_W25Q128_GPIO_CS(0);
	HAL_Delay(1);
	status = _W25Q128_SPI_TransmitReceive(tx_data, rx_data, 4);
	HAL_Delay(1);
	_W25Q128_GPIO_CS(1);

	*MID = rx_data[1];
	*DID = rx_data[2] << 8;
	*DID |= rx_data[3];
	
	return status;
}

/**
 * @brief 写使能
 */
static W25Q128_Status_e _W25Q128_WriteEnable(void)
{
	W25Q128_Status_e status = W25Q128_ERROR;
	uint8_t tx_data[] = {W25Q128_WRITE_ENABLE};

	_W25Q128_GPIO_CS(0);
	status = _W25Q128_SPI_Transmit(tx_data, 1);
	_W25Q128_GPIO_CS(1);

	return status;
}

/**
 * @brief 等待完成
 */
static W25Q128_Status_e _W25Q128_WaitBusy(void)
{
	W25Q128_Status_e status = W25Q128_ERROR;
	uint8_t tx_data[2] = {W25Q128_READ_STATUS_REGISTER_1, W25Q128_DUMMY_BYTE};
	uint8_t rx_data[2] = {W25Q128_DUMMY_BYTE, W25Q128_DUMMY_BYTE};
	uint32_t timeout = 100000;

	_W25Q128_GPIO_CS(0);
	do {
		_W25Q128_SPI_TransmitReceive(tx_data, rx_data, 2);
		timeout--;
		if(timeout == 0) {break;}

	} while((rx_data[1] & 0x01) == 0x01);
	_W25Q128_GPIO_CS(1);
	
	return status;
}

/**
 * @brief 按页编程 W25Q128 (最大 256 字节/页)
 */
W25Q128_Status_e W25Q128_PageProgram(uint32_t address, uint8_t *data_array, uint16_t len)
{
	W25Q128_Status_e status = W25Q128_ERROR;
	uint8_t tx_data[4] = {W25Q128_PAGE_PROGRAM, address >> 16, address >> 8, address};

	_W25Q128_Lock();
	_W25Q128_WriteEnable();

	_W25Q128_GPIO_CS(0);
	status = _W25Q128_SPI_Transmit(tx_data, 4);
	status |= _W25Q128_SPI_Transmit(data_array, len);
	_W25Q128_GPIO_CS(1);
	_W25Q128_Unlock();

	_W25Q128_WaitBusy();
	return status;
}

/**
 * @brief 擦除扇区
 * @note  4KB
 */
W25Q128_Status_e W25Q128_SectorErase(uint32_t address)
{
	W25Q128_Status_e status = W25Q128_ERROR;
	uint8_t tx_data[4] = {W25Q128_SECTOR_ERASE_4KB, address >> 16, address >> 8, address};

	_W25Q128_Lock();
	_W25Q128_WriteEnable();

	_W25Q128_GPIO_CS(0);
	status = _W25Q128_SPI_Transmit(tx_data, 4);
	_W25Q128_GPIO_CS(1);
	_W25Q128_Unlock();

	_W25Q128_WaitBusy();
	return status;
}

/**
 * @brief 读取指定长度数据
 */
W25Q128_Status_e W25Q128_ReadData(uint32_t address, uint8_t *data, uint32_t len)
{
	W25Q128_Status_e status = W25Q128_ERROR;
	uint8_t tx_data[4] = {W25Q128_READ_DATA, address >> 16, address >> 8, address};

	_W25Q128_GPIO_CS(0);
	status = _W25Q128_SPI_Transmit(tx_data, 4);
	status |= _W25Q128_SPI_Receive(data, len);
	_W25Q128_GPIO_CS(1);

	return status;
}

mian.c文件中,用于测试的函数

void W25Q128_Test()
{
	/* ====== W25Q128 简易测试 ====== */
	uint8_t MID = 0;
	uint16_t DID = 0;
	printf("--- W25Q128 Test ---\r\n");

	/* 1. 读 ID */
	if (W25Q128_ReadID(&MID, &DID) != W25Q128_OK) {
		printf("FAIL: ReadID\r\n");
		goto test_done;
	}
	printf("ID: MID=0x%02X DID=0x%04X\r\n", MID, DID);

	/* 2. 擦除扇区 0 */
	W25Q128_SectorErase(0x000000);
	printf("Erase OK\r\n");

	/* 3. 写一页 */
	{
		uint8_t buf[256];
		uint8_t i;
		for (i = 0; i < 255; i++) buf[i] = i + 1;
		buf[255] = 0;
		W25Q128_PageProgram(0x000000, buf, 256);
	}
	printf("Write OK\r\n");

	/* 4. 读回校验 */
	{
		uint8_t buf[256] = {0};
		uint8_t i, ok = 1;
		W25Q128_ReadData(0x000000, buf, 256);
		for (i = 0; i < 255; i++) { if (buf[i] != i + 1) ok = 0; }
		if (buf[255] != 0) ok = 0;
		printf("%s\r\n", ok ? "PASS" : "FAIL: Verify");
	}

	test_done:
	printf("--- Test End ---\r\n");
		
}

验证结果

在这里插入图片描述

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐