SPI协议软件实现 W25QXX flash 存储器
SPI 协议知识点
- SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线
- 四根通信线:SCK(Serial Clock)、MOSI(Master Output Slave Input)、MISO(Master Input Slave Output)、SS(Slave Select)
- 同步,全双工
- 支持总线挂载多设备(一主多从)
- 所有SPI设备的SCK、MOSI、MISO分别连在一起
- 主机另外引出多条SS控制线,分别接到各从机的SS引脚
- 输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入
通讯过程:
- 主设备拉低目标从设备的 CS 引脚。
- 主设备开始输出 SCLK 时钟信号。
- 主设备通过 MOSI 发送数据,同时从设备可以通过 MISO 发送数据(全双工)。
- 数据按位在时钟边沿传输,常见有四种模式。
- 主设备通信完毕后,拉高 CS 引脚,结束传输。
优点:
- 简单、高速(常见速度几十 Mbps)
- 全双工通信
- 支持多个从设备(每个一个 CS)
缺点:
- 每个从设备需要一个独立 CS 引脚,不适合大规模设备扩展
- 无协议层,软件必须自己处理数据完整性
- 通信距离短,一般用于板级通信
✅ 七、典型应用
- SD 卡模块
- Flash 存储芯片(如 W25Qxx)
- OLED 屏幕
- 各类传感器(如加速度计)
SPI 时序基本单元
- 起始条件:SS从高电平切换到低电平
- 终止条件:SS从低电平切换到高电平
SPI 的时序由两个参数控制:
- CPOL(时钟极性):SCLK 空闲时是高电平还是低电平。
- CPHA(时钟相位):在哪个边沿采样数据(第一个还是第二个边沿)。
模式 | CPOL | CPHA | 描述 |
---|---|---|---|
Mode 0 | 0 | 0 | 空闲低电平,第一上升沿采样 |
Mode 1 | 0 | 1 | 空闲低电平,第二下降沿采样 |
Mode 2 | 1 | 0 | 空闲高电平,第一下降沿采样 |
Mode 3 | 1 | 1 | 空闲高电平,第二上升沿采样 |
主从必须使用相同的模式才能正常通信。
- 交换一个字节(模式0)
最多使用
- CPOL=0:空闲状态时,SCK为低电平
- CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据
- 交换一个字节(模式1)
- CPOL=0:空闲状态时,SCK为低电平
- CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据
- 交换一个字节(模式2)
- CPOL=1:空闲状态时,SCK为高电平
- CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据
- 交换一个字节(模式3)
- CPOL=1:空闲状态时,SCK为高电平
- CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据
W25QXX flash 存储器
W25QXX 是 Winbond 公司生产的一系列串行 NOR Flash 存储器芯片,广泛应用于嵌入式系统中,用于存储代码、配置、用户数据、图像、语音等信息。
🧠 一、W25QXX 是什么?
-
类型:串行 NOR Flash(非易失性存储器)
-
接口:支持 SPI 协议(支持标准 SPI、Dual SPI、Quad SPI)
-
容量范围:
- 常见型号有:W25Q16 (2MB)、W25Q32 (4MB)、W25Q64 (8MB)、W25Q128 (16MB)、W25Q256 (32MB)
-
供电电压:2.7V ~ 3.6V(通常为 3.3V)
🧱 二、存储结构(以 W25Q64 为例)
单位 | 大小 | 数量 | 说明 |
---|---|---|---|
芯片总容量 | 64Mbit = 8MB | — | — |
块(Block) | 64KB | 128 个 | 可擦除单元 |
扇区(Sector) | 4KB | 2048 个 | 最小可擦除单元 |
页(Page) | 256 字节 | 32,768 页 | 写入数据的基本单位 |
❗ 注意:写入单位是页(Page),擦除单位是扇区(Sector)或块(Block)
⚙️ 三、基本功能命令
功能 | 指令码(Hex) | 描述 |
---|---|---|
写使能 | 0x06 | 必须先发送,才可写入/擦除 |
写禁止 | 0x04 | 禁止后续写入/擦除操作 |
读数据 | 0x03 | 读取 Flash 数据 |
快速读 | 0x0B | 加上 dummy byte,提高读取速度 |
页写入 | 0x02 | 写入 1~256 字节数据 |
扇区擦除(4KB) | 0x20 | 擦除一个扇区 |
块擦除(32K/64K) | 0x52 /0xD8 | 擦除大范围数据 |
全片擦除 | 0xC7 /0x60 | 擦除整个 Flash |
读状态寄存器 | 0x05 | 检查 BUSY、写保护位等 |
写状态寄存器 | 0x01 | 配置状态寄存器位 |
JEDEC ID | 0x9F | 读取芯片厂商和型号信息 |
🔁 四、使用流程(典型写入)
- 写使能(0x06)
- 擦除扇区(0x20 + 地址)
- 写使能(0x06)
- 页编程(0x02 + 地址 + 数据)
- 轮询状态寄存器(0x05),等待 BUSY 清除
📌 五、特性总结
特性 | 说明 |
---|---|
速度 | 最高可达 104MHz(SPI),可选 Quad SPI 模式 |
擦除寿命 | 每个扇区支持约 10 万次擦写 |
掉电保存 | 非易失性,断电数据仍然保留 |
尺寸小 | 常用 SOP-8、WSON、USON 等封装 |
广泛使用 | 常见于 STM32、ESP32、FPGA、WiFi 模块、路由器等产品中 |
🔍 六、常见应用场景
- 存储系统启动代码(Bootloader)
- 存储图像、字库、音频文件(适合大容量 W25Q128、256)
- 数据记录与配置参数存储
- 替代 EEPROM,提供更大容量和更快速度
🧪 七、识别芯片(JEDEC ID)
你可以通过发送 0x9F
命令读取芯片 ID,例如:
-
W25Q64 的返回值是:
0xEF 0x40 0x17
0xEF
: Winbond 厂商 ID0x40
: 存储类型0x17
: 容量(0x17 表示 64Mbit = 8MB)
利用 SPI 写W25QXX 示例代码
- 写一个字节 / 读取一个字节
- 写使能
- 扇区擦除
- 写入数据
- 读取数据
- 分文件结构清晰
🗂️ 文件结构说明
W25QXX_Driver/
├── w25qxx.h // 头文件
├── w25qxx.c // 实现文件
├── soft_spi.h // 软件 SPI 接口头文件
├── soft_spi.c // 软件 SPI 实现
✅ soft_spi.h
:软件 SPI 接口头文件
#ifndef __SOFT_SPI_H__
#define __SOFT_SPI_H__#include <stdint.h>void MySPI_Init(void);
void MySPI_CS_L(void);
void MySPI_CS_H(void);
void MySPI_W_MOSI(uint8_t bit);
void MySPI_W_SCK(uint8_t bit);
uint8_t MySPI_R_MISO(void);
uint8_t MySPI_SwapByte(uint8_t byte_send);#endif
✅ soft_spi.c
:软件 SPI 实现(需你根据硬件定义填 GPIO 操作)
#include "soft_spi.h"
#include <your_gpio_driver.h> // 替换为实际的GPIO操作头文件void MySPI_Init(void) {// 初始化 GPIO 方向// 设置 SCK, MOSI 为输出,MISO 为输入
}void MySPI_CS_L(void) {// 拉低 CS
}void MySPI_CS_H(void) {// 拉高 CS
}void MySPI_W_MOSI(uint8_t bit) {// 设置 MOSI 引脚电平
}void MySPI_W_SCK(uint8_t bit) {// 设置 SCK 引脚电平
}uint8_t MySPI_R_MISO(void) {// 读取 MISO 电平return 0; // 实际应返回 MISO 电平
}uint8_t MySPI_SwapByte(uint8_t byte_send) {uint8_t i, byte_recv = 0x00;for (i = 0; i < 8; i++) {MySPI_W_MOSI((byte_send & 0x80) != 0);byte_send <<= 1;MySPI_W_SCK(1); // 上升沿采样if (MySPI_R_MISO()) {byte_recv |= (0x80 >> i);}MySPI_W_SCK(0); // 下降沿传输}return byte_recv;
}
✅ w25qxx.h
:W25QXX 驱动接口
#ifndef __W25QXX_H__
#define __W25QXX_H__#include <stdint.h>void W25QXX_Init(void);
void W25QXX_WriteEnable(void);
void W25QXX_WaitBusy(void);
void W25QXX_EraseSector(uint32_t addr);
void W25QXX_WriteByte(uint32_t addr, uint8_t data);
uint8_t W25QXX_ReadByte(uint32_t addr);#endif
✅ w25qxx.c
:W25QXX 驱动实现
#include "w25qxx.h"
#include "soft_spi.h"void W25QXX_Init(void) {MySPI_Init();MySPI_CS_H(); // 默认拉高 CS
}void W25QXX_WriteEnable(void) {MySPI_CS_L();MySPI_SwapByte(0x06); // Write EnableMySPI_CS_H();
}void W25QXX_WaitBusy(void) {uint8_t status;do {MySPI_CS_L();MySPI_SwapByte(0x05); // Read Status Registerstatus = MySPI_SwapByte(0xFF);MySPI_CS_H();} while (status & 0x01); // 等待 BUSY 清除
}void W25QXX_EraseSector(uint32_t addr) {W25QXX_WriteEnable();MySPI_CS_L();MySPI_SwapByte(0x20); // Sector EraseMySPI_SwapByte((addr >> 16) & 0xFF);MySPI_SwapByte((addr >> 8) & 0xFF);MySPI_SwapByte(addr & 0xFF);MySPI_CS_H();W25QXX_WaitBusy();
}void W25QXX_WriteByte(uint32_t addr, uint8_t data) {W25QXX_WriteEnable();MySPI_CS_L();MySPI_SwapByte(0x02); // Page ProgramMySPI_SwapByte((addr >> 16) & 0xFF);MySPI_SwapByte((addr >> 8) & 0xFF);MySPI_SwapByte(addr & 0xFF);MySPI_SwapByte(data);MySPI_CS_H();W25QXX_WaitBusy();
}uint8_t W25QXX_ReadByte(uint32_t addr) {uint8_t data;MySPI_CS_L();MySPI_SwapByte(0x03); // Read DataMySPI_SwapByte((addr >> 16) & 0xFF);MySPI_SwapByte((addr >> 8) & 0xFF);MySPI_SwapByte(addr & 0xFF);data = MySPI_SwapByte(0xFF);MySPI_CS_H();return data;
}
✅ 示例用法(main.c)
#include "w25qxx.h"int main(void) {W25QXX_Init();// 擦除地址 0x000000 所在扇区W25QXX_EraseSector(0x000000);// 写入一个字节 0xABW25QXX_WriteByte(0x000000, 0xAB);// 读取验证uint8_t data = W25QXX_ReadByte(0x000000);if (data == 0xAB) {// 写入成功}while (1);
}
效果
我遇到的问题:我的协议和通讯都写好了,但是呢一直对W25QXX的读写特性不了解,导致一直无法进行写和读操作,经过一顿排错以后才知道,如果要进行写,那么一定进行写使能,把要写的区域进行擦除,然后在进行写使能,然后再去写目标地址,最后能读!
Flash 操作注意事项 坑
:
写入操作时:
- 写入操作前,必须先进行写使能
- 每个数据位只能由1改写为0,不能由0改写为1
- 写入数据前必须先擦除,擦除后,所有数据位变为1
- 擦除必须按最小擦除单元进行
- 连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入
- 写入操作结束后,芯片进入忙状态,不响应新的读写操作
读取操作时: - 直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取
代码仓库
发送0xAB
数据
然后读取它