【模块系列】STM32&W25Q64

以W25Q64JV芯片为例。从芯片概述、引脚描述、时序介绍、存储框图、案例代码等几个方向进行介绍。部分图片以及内容摘抄至芯片手册

芯片概述

W25Q64JV串行闪存,隶属于25Q系列,适用于代码映射至RAM、直接从双/四路SPI执行代码以及存储语音、文本与数据。工作电压范围在2.7~3.6V,待机状态下低至1µA电流。

以W25Q64JV为例,其阵列被组织成32768个可编程页面,每个页面256字节。一直最多可编程256字节。页面可以以16、128、256个为一组或整个芯片进行擦除,也就是4KB、32KB、64KB、全部。

支持标准串行外设接口SPI,双/四引脚SPI。此外,该设备支持JEDEC标准制造商和设备ID,以及64位唯一序列号和三个256字节的安全寄存器。

省略多路SPI下的引脚功能介绍,以标准SPI操作为主:

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

时序介绍

以常用的获取芯片ID的时序流程为例

  • "9Fh"表示,十六进制的9F

通过手册中8.1.2 指令集表1(标准SPI指令)或者时序中的注释可以知道,在该指令下,接收到的三个字节,分别是制造商内容类型容量 ,像笔者使用的是++W25Q64JV++ ,执行指令后返回EF 40 17,华邦制造SPI类型64Mbit(8M),得到这样的信息就证明,时序是通的。

存储框图

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

这里在对芯片概述里的写入和擦除部分做描述,简单的来说,就是在写入时,最大不能超过1页也就是256字节,擦除时只能选择16页、128页、256页或者整个芯片都擦除的方式,对应的就是扇区(16页-4KB)、半块(128页-32KB)、(256页-64KB),等名词常提到的擦除,也表示不同颗粒度。

擦除操作后的数据读取值为0xFF。擦除操作:是将目标区域的所有位强制置为1,即每个的字节变为0xFF。编程操作:将已擦除的1改写为0

代码案例

STM32F103C8T6+硬件SPI1。因为代码库是笔者从一个小项目剥离出来的,不相关的方向已经尽量给出替换成接口函数,如果还有什么问题可以提出来!

原理图以及配置

笔者使用W25Q64的硬件连接,以及STM32CubeMX上的配置

程序文件

ZQXY_W25Q64.h

c 复制代码
#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,那么收发函数也要修改下。

c 复制代码
#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(仅展示相关代码)

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);
}

调试信息

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

相关推荐
llilian_1620 小时前
总线授时卡 CPCI总线授时卡的工作原理及应用场景介绍 CPCI总线校时卡
运维·单片机·其他·自动化
禾仔仔21 小时前
USB MSC从理论到实践(模拟U盘为例)——从零开始学习USB2.0协议(六)
嵌入式硬件·mcu·计算机外设
The Electronic Cat1 天前
树莓派使用串口启动死机
单片机·嵌入式硬件·树莓派
先知后行。1 天前
常见元器件
单片机·嵌入式硬件
恒锐丰小吕1 天前
屹晶微 EG2302 600V耐压、低压启动、带SD关断功能的高性价比半桥栅极驱动器技术解析
嵌入式硬件·硬件工程
Dillon Dong1 天前
按位或(|=)的核心魔力:用宏定义优雅管理嵌入式故障字
c语言·stm32
Free丶Chan1 天前
dsPIC系列-1:dsPIC33点灯 [I/O、RCC、定时器]
单片机·嵌入式硬件
v先v关v住v获v取1 天前
塔式立体车库5张cad+设计说明书+三维图
科技·单片机·51单片机
恒锐丰小吕1 天前
屹晶微 EG2106D 600V耐压、半桥MOS/IGBT驱动芯片技术解析
嵌入式硬件·硬件工程