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 0x170xEF: 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数据
然后读取它


