STM32 进阶封神之路(三十二):SPI 通信深度实战 ------ 硬件 SPI 驱动 W25Q64 闪存(底层时序 + 寄存器配置 + 读写封装)
上一篇我们掌握了 STM32 硬件 IIC 的底层原理与 BH1750 驱动,这一篇聚焦嵌入式高速同步通信协议 ------SPI(Serial Peripheral Interface)。SPI 以 "全双工、高速率、主从架构" 的优势,广泛应用于闪存、触摸屏、ADC/DAC、无线模块等外设,而 W25Q64 作为 SPI 接口的 NOR Flash,是存储日志、固件、配置参数的核心器件。
本文基于参考文档中的 SPI 协议与 W25Q64 核心知识点,从 SPI 协议底层原理、STM32 硬件 SPI 配置、W25Q64 存储架构、指令集解析,到完整的读写擦除实战,全程超详细拆解,帮你彻底掌握 SPI 通信与 Flash 存储的工业级应用!
一、SPI 协议核心认知:为什么它是高速通信首选?
1. SPI 协议核心特性与应用场景
SPI 是 Motorola 提出的同步串行通信协议,核心特点的是全双工、高速率、主从架构、三线 / 四线通信,相比 IIC 和 UART,优势极为突出:
- 速率高:支持 Mbps 级传输(W25Q64 最高支持 80MHz),远超 IIC 的 400Kbps;
- 全双工:同一时钟周期内可同时发送和接收数据,通信效率高;
- 结构简单:仅需 4 根线(NSS、SCK、MOSI、MISO),支持一主多从;
- 稳定性强:同步时钟驱动,时序精度高,抗干扰能力优于异步通信(如 UART)。
典型应用场景:
- 存储设备:SPI Flash(W25Q64、W25Q128)、EEPROM;
- 显示设备:OLED 屏幕(SSD1306 的 SPI 模式)、触摸屏(XPT2046);
- 传感器:加速度传感器(MPU6050 的 SPI 模式)、ADC 芯片(ADS1115);
- 无线模块:WiFi(ESP8266 的 SPI 模式)、蓝牙模块。
2. SPI 协议核心概念(必掌握)
(1)总线组成(四线制,参考文档物理层知识点)
表格
| 引脚名称 | 功能描述 | 主机配置 | 从机配置 |
|---|---|---|---|
| NSS(CS) | 片选信号,低电平选中从设备 | 输出模式(控制选中哪个从机) | 输入模式(接收主机片选信号) |
| SCK(Serial Clock) | 同步时钟,由主机生成 | 输出模式 | 输入模式 |
| MOSI(Master Out Slave In) | 主机输出,从机输入 | 输出模式 | 输入模式 |
| MISO(Master In Slave Out) | 主机输入,从机输出 | 输入模式 | 输出模式 |
关键说明:
- NSS 为低电平时,从设备被选中,仅选中的从设备会响应 SPI 通信;
- 一主多从场景下,主机通过不同 NSS 引脚控制多个从设备,SCK、MOSI、MISO 三线共用;
- 部分场景可省略 NSS 引脚(软件片选),但硬件片选更稳定,推荐使用。
(2)四种工作模式(核心时序差异)
SPI 的工作模式由时钟极性(CPOL) 和时钟相位(CPHA) 决定,共四种组合,参考文档时序知识点:
表格
| 模式 | CPOL(时钟极性) | CPHA(时钟相位) | 核心时序 | 适用场景 |
|---|---|---|---|---|
| 模式 0(默认) | 0(空闲时 SCK 为低电平) | 0(第一个时钟跳变沿采样) | 上升沿采样,下降沿发送 | 多数 SPI 设备(W25Q64 默认支持) |
| 模式 1 | 0(空闲时 SCK 为低电平) | 1(第二个时钟跳变沿采样) | 下降沿采样,上升沿发送 | 部分传感器 |
| 模式 2 | 1(空闲时 SCK 为高电平) | 0(第一个时钟跳变沿采样) | 下降沿采样,上升沿发送 | 工业控制设备 |
| 模式 3 | 1(空闲时 SCK 为高电平) | 1(第二个时钟跳变沿采样) | 上升沿采样,下降沿发送 | 高速通信场景 |
时序核心:
- 采样:接收方读取数据的时刻(需与发送方的发送时刻同步);
- 发送:发送方更新数据到数据线的时刻;
- W25Q64 支持模式 0 和模式 3,实战中推荐使用模式 0(兼容性最好)。
(3)数据传输规则
- 高位优先(MSB First):默认传输顺序,数据从最高位开始逐位传输;
- 低位优先(LSB First):需手动配置,部分设备支持;
- 同步传输:每一个 SCK 时钟周期传输 1 位数据,MOSI 和 MISO 的数据同步变化。
3. STM32F103 SPI 外设资源(参考文档 STM32 SPI 知识点)
STM32F103 系列内置 2 个硬件 SPI 外设(SPI1、SPI2),核心参数如下:
- SPI1:挂载在 APB2 总线(最高 72MHz),支持高速传输,引脚为 PA4(NSS)、PA5(SCK)、PA6(MISO)、PA7(MOSI);
- SPI2:挂载在 APB1 总线(最高 36MHz),引脚为 PB12(NSS)、PB13(SCK)、PB14(MISO)、PB15(MOSI);
- 支持主模式、从模式;
- 支持 8 位 / 16 位数据宽度;
- 支持软件片选(NSS 引脚不用,由软件控制)和硬件片选;
- 支持中断模式和 DMA 模式传输。
二、STM32 硬件 SPI 外设配置(底层寄存器 + 库函数)
本节基于 SPI1(PA4~PA7)配置主机模式,驱动 W25Q64,核心流程为 "时钟使能→GPIO 复用配置→SPI 参数配置→SPI 使能"。
1. 硬件接线(SPI1 + W25Q64)
表格
| STM32 引脚 | SPI 功能 | W25Q64 引脚 | 备注 |
|---|---|---|---|
| PA4(SPI1_NSS) | NSS(片选) | CS | 低电平选中,硬件片选 |
| PA5(SPI1_SCK) | SCK(时钟) | SCK | 主机生成时钟 |
| PA6(SPI1_MISO) | MISO(主机输入) | SO | 从机输出数据 |
| PA7(SPI1_MOSI) | MOSI(主机输出) | SI | 主机输出数据 |
| 3.3V | VCC | VCC | W25Q64 工作电压 3.3V |
| GND | GND | GND | 共地 |
| 3.3V | WP | WP | 写保护引脚,接高电平禁用写保护 |
| 3.3V | HOLD | HOLD | 保持引脚,接高电平正常工作 |
2. 库函数配置步骤
c
运行
#include "stm32f10x.h"
#include "delay.h"
// SPI1引脚定义
#define SPI1_NSS_PIN GPIO_Pin_4
#define SPI1_SCK_PIN GPIO_Pin_5
#define SPI1_MISO_PIN GPIO_Pin_6
#define SPI1_MOSI_PIN GPIO_Pin_7
#define SPI1_GPIO_PORT GPIOA
#define SPI1_GPIO_CLK RCC_APB2Periph_GPIOA
#define SPI1_CLK RCC_APB2Periph_SPI1
// W25Q64片选控制宏
#define W25Q64_CS_HIGH() GPIO_SetBits(SPI1_GPIO_PORT, SPI1_NSS_PIN) // 取消选中
#define W25Q64_CS_LOW() GPIO_ResetBits(SPI1_GPIO_PORT, SPI1_NSS_PIN) // 选中
// SPI1初始化(主机模式,模式0,8位数据,10MHz速率)
void SPI1_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct;
SPI_InitTypeDef SPI_InitStruct;
// 1. 使能GPIO和SPI1时钟
RCC_APB2PeriphClockCmd(SPI1_GPIO_CLK | SPI1_CLK, ENABLE);
// 2. 配置GPIO为复用功能
// NSS(PA4):推挽输出(硬件片选,主机控制)
GPIO_InitStruct.GPIO_Pin = SPI1_NSS_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(SPI1_GPIO_PORT, &GPIO_InitStruct);
// SCK(PA5)、MOSI(PA7):复用推挽输出
GPIO_InitStruct.GPIO_Pin = SPI1_SCK_PIN | SPI1_MOSI_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出
GPIO_Init(SPI1_GPIO_PORT, &GPIO_InitStruct);
// MISO(PA6):浮空输入
GPIO_InitStruct.GPIO_Pin = SPI1_MISO_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(SPI1_GPIO_PORT, &GPIO_InitStruct);
// 3. 配置SPI1参数
SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 全双工模式
SPI_InitStruct.SPI_Mode = SPI_Mode_Master; // 主机模式
SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b; // 8位数据宽度
SPI_InitStruct.SPI_CPOL = SPI_CPOL_Low; // 时钟极性0(空闲低电平)
SPI_InitStruct.SPI_CPHA = SPI_CPHA_1Edge; // 时钟相位0(第一个跳变沿采样)
SPI_InitStruct.SPI_NSS = SPI_NSS_Soft; // 软件片选(或SPI_NSS_Hard硬件片选)
SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8; // 波特率分频系数8
// APB2时钟72MHz,分频后速率=72MHz/8=9MHz(W25Q64支持最高80MHz)
SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB; // 高位优先
SPI_InitStruct.SPI_CRCPolynomial = 7; // CRC校验多项式(默认7,禁用CRC时无影响)
// 4. 初始化SPI1
SPI_Init(SPI1, &SPI_InitStruct);
// 5. 禁用CRC校验(多数场景无需CRC)
SPI_Cmd(SPI1, DISABLE);
SPI_CRCConfig(SPI1, DISABLE);
SPI_Cmd(SPI1, ENABLE);
// 6. 初始状态:取消选中W25Q64
W25Q64_CS_HIGH();
}
3. 关键配置说明(参考文档 STM32 SPI 知识点)
- GPIO 模式 :
- NSS:硬件片选时配置为推挽输出,软件片选时可配置为普通 GPIO;
- SCK/MOSI:复用推挽输出(AF_PP),因为需要 SPI 外设控制引脚电平;
- MISO:浮空输入(IN_FLOATING),避免外部电平干扰。
- 波特率分频 :
- SPI1 挂载在 APB2 总线(72MHz),分频系数可选 2、4、8、16 等,速率 = APB2 时钟 / 分频系数;
- W25Q64 支持最高 80MHz,实战中推荐 9MHz(稳定)或 18MHz(高速)。
- 片选模式 :
- 硬件片选(SPI_NSS_Hard):NSS 引脚由 SPI 外设自动控制(主模式下发送数据时自动拉低);
- 软件片选(SPI_NSS_Soft):NSS 引脚由用户手动控制(推荐,灵活性更高)。
- 工作模式:W25Q64 默认支持模式 0,因此 CPOL=Low,CPHA=1Edge。
4. SPI 核心通信函数(发送 / 接收 1 字节)
c
运行
// SPI1发送1字节数据,返回接收的字节(全双工同步传输)
uint8_t SPI1_Send_Byte(uint8_t data) {
// 等待发送缓冲区为空
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
// 发送数据
SPI_I2S_SendData(SPI1, data);
// 等待接收缓冲区非空
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);
// 返回接收的数据(全双工,发送的同时会接收数据)
return SPI_I2S_ReceiveData(SPI1);
}
// SPI1接收1字节数据(通过发送0xFF触发接收)
uint8_t SPI1_Receive_Byte(void) {
return SPI1_Send_Byte(0xFF); // 发送0xFF,接收从机返回的数据
}
// SPI1发送多字节数据
void SPI1_Send_Buffer(uint8_t *buf, uint16_t len) {
if (buf == NULL || len == 0) return;
for (uint16_t i = 0; i < len; i++) {
SPI1_Send_Byte(buf[i]);
}
}
// SPI1接收多字节数据
void SPI1_Receive_Buffer(uint8_t *buf, uint16_t len) {
if (buf == NULL || len == 0) return;
for (uint16_t i = 0; i < len; i++) {
buf[i] = SPI1_Receive_Byte();
}
}
关键说明:SPI 是全双工通信,发送数据的同时必然会接收数据,接收数据时也需要发送一个 "dummy 数据"(如 0xFF)触发时钟,这是 SPI 的核心特性。
三、W25Q64 闪存核心解析(参考文档 W25Q64 知识点)
W25Q64 是 Winbond(华邦)推出的 8MB 容量 SPI NOR Flash,支持高速 SPI 通信,是嵌入式系统中存储数据的常用器件。
1. W25Q64 核心参数
- 容量:8MB(64Mbit),分为 128 个扇区(每个扇区 64KB),每个扇区分为 16 个页(每个页 4KB);
- 通信接口:SPI(支持模式 0/3),最高速率 80MHz;
- 擦除方式:扇区擦除(64KB)、块擦除(32KB/64KB)、全片擦除;
- 编程方式:页编程(最多 4KB / 页);
- 使用寿命:10 万次擦写,数据保留 100 年;
- 工作电压:2.7V~3.6V(推荐 3.3V)。
2. W25Q64 存储布局(参考文档容量布局)
表格
| 存储单元 | 大小 | 数量 | 地址范围(示例) | 用途 |
|---|---|---|---|---|
| 页(Page) | 4KB | 2048 | 0x00000~0x00FFF(第 0 页) | 单次编程最小单元 |
| 扇区(Sector) | 64KB | 128 | 0x00000~0x0FFFF(第 0 扇区) | 单次擦除最小单元 |
| 块(Block) | 32KB/64KB | 256/128 | 0x00000~0x07FFF(32KB 块) | 批量擦除单元 |
| 全片(Chip) | 8MB | 1 | 0x000000~0x7FFFFF | 整个闪存 |
关键规则:
- 编程前必须先擦除(Flash 特性:只能将 1 改为 0,擦除可将 0 改为 1);
- 页编程时,超过 4KB 的数据会循环覆盖当前页的起始地址;
- 扇区擦除会将该扇区内的所有数据置为 0xFF。
3. W25Q64 核心指令集(参考文档指令操作)
W25Q64 通过 SPI 接收指令实现读写擦除,常用核心指令如下:
表格
| 指令代码 | 指令名称 | 功能描述 | 指令长度 | 备注 |
|---|---|---|---|---|
| 0x9F | 读取 ID | 读取制造商 ID、设备 ID | 1 字节指令 + 3 字节数据 | 验证通信是否正常 |
| 0x06 | 写使能 | 允许后续的编程 / 擦除操作 | 1 字节指令 | 编程 / 擦除前必须执行 |
| 0x05 | 读状态寄存器 1 | 读取闪存状态(如是否忙) | 1 字节指令 + 1 字节数据 | 0x00 = 空闲,0x01 = 忙 |
| 0x02 | 页编程 | 向指定地址写入数据(≤4KB) | 1 字节指令 + 3 字节地址 + N 字节数据 | 地址需对齐到页起始 |
| 0x20 | 扇区擦除 | 擦除指定扇区(64KB) | 1 字节指令 + 3 字节地址 | 擦除时间约 40ms |
| 0x03 | 读数据 | 从指定地址读取数据 | 1 字节指令 + 3 字节地址 + N 字节数据 | 支持任意长度读取 |
| 0xC7 | 全片擦除 | 擦除整个闪存 | 1 字节指令 | 擦除时间约 10 秒,慎用 |
四、W25Q64 驱动实现(硬件 SPI + 指令封装)
基于 STM32 SPI1 和 W25Q64 指令集,封装初始化、读 ID、擦除、编程、读取等核心函数,实现完整的 Flash 操作。
1. W25Q64 驱动头文件定义
c
运行
#ifndef __W25Q64_H__
#define __W25Q64_H__
#include "stm32f10x.h"
// W25Q64容量定义(8MB=64Mbit)
#define W25Q64_FLASH_SIZE 0x800000UL // 8MB
#define W25Q64_SECTOR_SIZE 0x10000UL // 64KB/扇区
#define W25Q64_PAGE_SIZE 0x1000UL // 4KB/页
// W25Q64 ID定义(制造商ID:0xEF,设备ID:0x4017)
#define W25Q64_MANUFACTURER_ID 0xEF
#define W25Q64_DEVICE_ID 0x4017
// W25Q64指令定义
#define W25Q64_CMD_READ_ID 0x9F
#define W25Q64_CMD_WRITE_EN 0x06
#define W25Q64_CMD_READ_SR1 0x05
#define W25Q64_CMD_PAGE_PROG 0x02
#define W25Q64_CMD_SECTOR_ERASE 0x20
#define W25Q64_CMD_READ_DATA 0x03
#define W25Q64_CMD_CHIP_ERASE 0xC7
// 函数声明
void W25Q64_Init(void);
uint32_t W25Q64_ReadID(void);
void W25Q64_WaitBusy(void);
void W25Q64_WriteEnable(void);
void W25Q64_SectorErase(uint32_t addr);
void W25Q64_PageWrite(uint32_t addr, uint8_t *buf, uint16_t len);
void W25Q64_ReadData(uint32_t addr, uint8_t *buf, uint32_t len);
void W25Q64_ChipErase(void);
#endif
2. W25Q64 核心驱动函数实现
c
运行
#include "w25q64.h"
#include "spi.h"
#include "delay.h"
// W25Q64初始化(初始化SPI1)
void W25Q64_Init(void) {
SPI1_Init();
W25Q64_CS_HIGH(); // 初始取消选中
delay_ms(10);
}
// 读取W25Q64 ID(制造商ID+设备ID)
uint32_t W25Q64_ReadID(void) {
uint32_t id = 0;
W25Q64_CS_LOW(); // 选中W25Q64
delay_us(10);
// 发送读ID指令(0x9F)
SPI1_Send_Byte(W25Q64_CMD_READ_ID);
// 读取3字节ID(制造商ID(1字节)+ 设备ID(2字节))
id |= (uint32_t)SPI1_Receive_Byte() << 16; // 制造商ID(0xEF)
id |= (uint32_t)SPI1_Receive_Byte() << 8; // 设备ID高字节(0x40)
id |= (uint32_t)SPI1_Receive_Byte(); // 设备ID低字节(0x17)
W25Q64_CS_HIGH(); // 取消选中
delay_us(10);
return id;
}
// 等待W25Q64空闲(擦除/编程完成)
void W25Q64_WaitBusy(void) {
uint8_t sr1 = 0;
W25Q64_CS_LOW();
delay_us(10);
// 发送读状态寄存器1指令
SPI1_Send_Byte(W25Q64_CMD_READ_SR1);
// 循环读取状态,直到Bit0为0(空闲)
do {
sr1 = SPI1_Receive_Byte();
} while ((sr1 & 0x01) == 0x01); // 0x01=忙,0x00=空闲
W25Q64_CS_HIGH();
delay_us(10);
}
// 写使能(编程/擦除前必须执行)
void W25Q64_WriteEnable(void) {
W25Q64_CS_LOW();
delay_us(10);
SPI1_Send_Byte(W25Q64_CMD_WRITE_EN);
W25Q64_CS_HIGH();
delay_us(10);
}
// 扇区擦除(64KB)
void W25Q64_SectorErase(uint32_t addr) {
// 地址校验(不能超过Flash容量)
if (addr >= W25Q64_FLASH_SIZE) return;
// 写使能
W25Q64_WriteEnable();
W25Q64_CS_LOW();
delay_us(10);
// 发送扇区擦除指令
SPI1_Send_Byte(W25Q64_CMD_SECTOR_ERASE);
// 发送3字节地址(A23~A0)
SPI1_Send_Byte((addr >> 16) & 0xFF); // 高字节
SPI1_Send_Byte((addr >> 8) & 0xFF); // 中字节
SPI1_Send_Byte(addr & 0xFF); // 低字节
W25Q64_CS_HIGH();
delay_us(10);
// 等待擦除完成(约40ms)
W25Q64_WaitBusy();
}
// 页编程(最多4KB)
void W25Q64_PageWrite(uint32_t addr, uint8_t *buf, uint16_t len) {
if (addr >= W25Q64_FLASH_SIZE || buf == NULL || len == 0) return;
// 限制单次编程长度≤4KB
if (len > W25Q64_PAGE_SIZE) len = W25Q64_PAGE_SIZE;
// 写使能
W25Q64_WriteEnable();
W25Q64_CS_LOW();
delay_us(10);
// 发送页编程指令
SPI1_Send_Byte(W25Q64_CMD_PAGE_PROG);
// 发送3字节地址
SPI1_Send_Byte((addr >> 16) & 0xFF);
SPI1_Send_Byte((addr >> 8) & 0xFF);
SPI1_Send_Byte(addr & 0xFF);
// 发送数据
SPI1_Send_Buffer(buf, len);
W25Q64_CS_HIGH();
delay_us(10);
// 等待编程完成(约1ms)
W25Q64_WaitBusy();
}
// 读数据(任意长度)
void W25Q64_ReadData(uint32_t addr, uint8_t *buf, uint32_t len) {
if (addr >= W25Q64_FLASH_SIZE || buf == NULL || len == 0) return;
W25Q64_CS_LOW();
delay_us(10);
// 发送读数据指令
SPI1_Send_Byte(W25Q64_CMD_READ_DATA);
// 发送3字节地址
SPI1_Send_Byte((addr >> 16) & 0xFF);
SPI1_Send_Byte((addr >> 8) & 0xFF);
SPI1_Send_Byte(addr & 0xFF);
// 接收数据
SPI1_Receive_Buffer(buf, len);
W25Q64_CS_HIGH();
delay_us(10);
}
// 全片擦除(慎用,约10秒)
void W25Q64_ChipErase(void) {
// 写使能
W25Q64_WriteEnable();
W25Q64_CS_LOW();
delay_us(10);
// 发送全片擦除指令
SPI1_Send_Byte(W25Q64_CMD_CHIP_ERASE);
W25Q64_CS_HIGH();
delay_us(10);
// 等待擦除完成(约10秒)
W25Q64_WaitBusy();
}
3. 多页数据写入封装(跨页处理)
W25Q64 的页编程限制单次写入≤4KB,若要写入超过 4KB 的数据,需手动处理跨页逻辑:
c
运行
// 多页写入(自动处理跨页)
void W25Q64_MultiPageWrite(uint32_t addr, uint8_t *buf, uint32_t len) {
uint16_t page_remain = 0; // 当前页剩余空间
uint32_t write_len = 0; // 本次写入长度
uint32_t current_addr = addr; // 当前写入地址
uint32_t current_len = len; // 当前剩余长度
uint8_t *current_buf = buf; // 当前写入缓冲区指针
while (current_len > 0) {
// 计算当前页剩余空间(地址对齐到页起始)
page_remain = W25Q64_PAGE_SIZE - (current_addr % W25Q64_PAGE_SIZE);
// 确定本次写入长度(取剩余长度和页剩余空间的最小值)
write_len = (current_len > page_remain) ? page_remain : current_len;
// 页编程
W25Q64_PageWrite(current_addr, current_buf, write_len);
// 更新参数
current_addr += write_len;
current_buf += write_len;
current_len -= write_len;
}
}
五、实战整合:W25Q64 读写擦除测试
1. 主函数实现
c
运行
#include "stm32f10x.h"
#include "usart.h"
#include "w25q64.h"
#include "delay.h"
// 测试数据定义
#define TEST_ADDR 0x000000UL // 测试地址(第0扇区第0页)
#define TEST_LEN 5000UL // 测试数据长度(5KB,跨1个页)
uint8_t write_buf[TEST_LEN]; // 写入缓冲区
uint8_t read_buf[TEST_LEN]; // 读取缓冲区
int main(void) {
uint32_t id = 0;
uint8_t err_flag = 0;
// 初始化系统时钟(72MHz)
SystemInit();
// 初始化串口1(115200bps,打印调试信息)
USART1_Init(115200);
// 初始化W25Q64
W25Q64_Init();
printf("STM32硬件SPI+W25Q64实战测试\r\n");
printf("=======================================\r\n\r\n");
// 1. 读取W25Q64 ID
id = W25Q64_ReadID();
printf("W25Q64 ID:0x%06X\r\n", id);
printf("制造商ID:0x%02X(预期0xEF)\r\n", (id >> 16) & 0xFF);
printf("设备ID:0x%04X(预期0x4017)\r\n", id & 0xFFFF);
if (id == ((W25Q64_MANUFACTURER_ID << 16) | W25Q64_DEVICE_ID)) {
printf("W25Q64通信正常!\r\n");
} else {
printf("W25Q64通信异常!\r\n");
while (1);
}
printf("---------------------------------------\r\n\r\n");
// 2. 填充测试数据(0x00~0x1387)
for (uint32_t i = 0; i < TEST_LEN; i++) {
write_buf[i] = i & 0xFF; // 填充0x00~0xFF循环数据
}
// 3. 扇区擦除(测试地址所在扇区)
printf("开始擦除扇区(地址:0x%06X)...\r\n", TEST_ADDR);
W25Q64_SectorErase(TEST_ADDR);
printf("扇区擦除完成!\r\n");
printf("---------------------------------------\r\n\r\n");
// 4. 多页写入测试数据(5KB,跨2个页)
printf("开始写入数据(长度:%d字节)...\r\n", TEST_LEN);
W25Q64_MultiPageWrite(TEST_ADDR, write_buf, TEST_LEN);
printf("数据写入完成!\r\n");
printf("---------------------------------------\r\n\r\n");
// 5. 读取数据
printf("开始读取数据(地址:0x%06X,长度:%d字节)...\r\n", TEST_ADDR, TEST_LEN);
W25Q64_ReadData(TEST_ADDR, read_buf, TEST_LEN);
printf("数据读取完成!\r\n");
printf("---------------------------------------\r\n\r\n");
// 6. 校验数据(对比写入和读取的数据)
printf("开始校验数据...\r\n");
for (uint32_t i = 0; i < TEST_LEN; i++) {
if (read_buf[i] != write_buf[i]) {
printf("数据校验失败!地址0x%06X,预期0x%02X,实际0x%02X\r\n",
TEST_ADDR + i, write_buf[i], read_buf[i]);
err_flag = 1;
break;
}
}
if (err_flag == 0) {
printf("数据校验成功!所有数据一致!\r\n");
}
printf("=======================================\r\n");
printf("W25Q64读写擦除测试完成!\r\n");
while (1) {
delay_ms(1000);
}
}
2. 串口打印效果
plaintext
STM32硬件SPI+W25Q64实战测试
=======================================
W25Q64 ID:0xEF4017
制造商ID:0xEF(预期0xEF)
设备ID:0x4017(预期0x4017)
W25Q64通信正常!
---------------------------------------
开始擦除扇区(地址:0x000000)...
扇区擦除完成!
---------------------------------------
开始写入数据(长度:5000字节)...
数据写入完成!
---------------------------------------
开始读取数据(地址:0x000000,长度:5000字节)...
数据读取完成!
---------------------------------------
开始校验数据...
数据校验成功!所有数据一致!
=======================================
W25Q64读写擦除测试完成!
六、SPI+W25Q64 常见问题与避坑指南(参考文档注意事项)
1. 读取 ID 失败(返回 0x000000 或 0xFFFFFF)
- 原因 1:硬件接线错误(SCK/MOSI/MISO 接反、未共地);解决:重新检查接线,确保 SCK 接 PA5、MOSI 接 PA7、MISO 接 PA6,必须共地;
- 原因 2:SPI 工作模式不匹配(W25Q64 默认模式 0,代码配置为其他模式);解决:确认 SPI 配置为 CPOL=Low、CPHA=1Edge(模式 0);
- 原因 3:片选信号未正确控制(始终选中或未选中);解决:初始化时 W25Q64_CS_HIGH(取消选中),通信时拉低选中;
- 原因 4:SPI 速率过高(超过 W25Q64 支持的 80MHz);解决:降低 SPI 速率(如分频系数 8,9MHz)。
2. 编程 / 擦除失败(状态寄存器始终为忙)
- 原因 1:未执行写使能指令(0x06);解决:编程 / 擦除前必须调用
W25Q64_WriteEnable(); - 原因 2:地址超出 Flash 容量;解决:确认地址≤0x7FFFFF(8MB);
- 原因 3:写保护引脚(WP)接低电平(启用写保护);解决:WP 引脚接 3.3V,禁用写保护;
- 原因 4:SPI 通信异常(数据传输错误);解决:先通过读 ID 验证通信,确保 SPI 发送 / 接收函数正常。
3. 数据校验失败(写入与读取的数据不一致)
- 原因 1:编程前未擦除扇区(Flash 只能写 0,不能写 1);解决:编程前必须擦除对应的扇区;
- 原因 2:页编程超过 4KB,导致数据覆盖;解决:使用
W25Q64_MultiPageWrite函数,自动处理跨页; - 原因 3:SPI 数据宽度配置错误(如配置为 16 位);解决:确保
SPI_DataSize = SPI_DataSize_8b; - 原因 4:地址发送顺序错误(应为高字节→低字节);解决:确认地址发送顺序为
(addr >> 16) & 0xFF(高)、(addr >> 8) & 0xFF(中)、addr & 0xFF(低)。
4. SPI 通信不稳定(偶尔成功偶尔失败)
- 原因 1:GPIO 速率配置过低(如 GPIO_Speed_2MHz);解决:将 SPI 引脚的 GPIO 速率配置为
GPIO_Speed_50MHz; - 原因 2:未添加延时(片选切换后未稳定);解决:片选拉低 / 拉高后添加 10~20μs 延时;
- 原因 3:电源纹波干扰(W25Q64 供电不稳定);解决:在 W25Q64 的 VCC 和 GND 之间并联 0.1μF 陶瓷电容;
- 原因 4:SPI 时钟极性 / 相位配置错误(接近工作临界点);解决:严格配置为模式 0(CPOL=Low,CPHA=1Edge)。
七、SPI 协议进阶:软件 SPI 与硬件 SPI 选型指南
1. 软件 SPI 实现(参考文档软件 SPI 知识点)
软件 SPI 通过普通 GPIO 模拟 SPI 时序,无需依赖硬件 SPI 外设,灵活性更高,核心代码示例:
c
运行
// 软件SPI引脚定义(任意GPIO)
#define SOFT_SPI_SCK_PIN GPIO_Pin_0
#define SOFT_SPI_MOSI_PIN GPIO_Pin_1
#define SOFT_SPI_MISO_PIN GPIO_Pin_2
#define SOFT_SPI_NSS_PIN GPIO_Pin_3
#define SOFT_SPI_GPIO_PORT GPIOB
// 软件SPI引脚操作宏
#define SOFT_SPI_SCK_HIGH() GPIO_SetBits(SOFT_SPI_GPIO_PORT, SOFT_SPI_SCK_PIN)
#define SOFT_SPI_SCK_LOW() GPIO_ResetBits(SOFT_SPI_GPIO_PORT, SOFT_SPI_SCK_PIN)
#define SOFT_SPI_MOSI_HIGH() GPIO_SetBits(SOFT_SPI_GPIO_PORT, SOFT_SPI_MOSI_PIN)
#define SOFT_SPI_MOSI_LOW() GPIO_ResetBits(SOFT_SPI_GPIO_PORT, SOFT_SPI_MOSI_PIN)
#define SOFT_SPI_MISO_READ() GPIO_ReadInputDataBit(SOFT_SPI_GPIO_PORT, SOFT_SPI_MISO_PIN)
// 软件SPI发送1字节(模式0)
uint8_t Soft_SPI_SendByte(uint8_t data) {
uint8_t i, recv_data = 0;
for (i = 0; i < 8; i++) {
// 高位优先
if (data & 0x80) {
SOFT_SPI_MOSI_HIGH();
} else {
SOFT_SPI_MOSI_LOW();
}
data <<= 1;
// 上升沿采样
SOFT_SPI_SCK_HIGH();
recv_data <<= 1;
if (SOFT_SPI_MISO_READ()) {
recv_data |= 0x01;
}
delay_us(1);
// 下降沿准备下一位
SOFT_SPI_SCK_LOW();
delay_us(1);
}
return recv_data;
}
2. 软件 SPI 与硬件 SPI 对比(参考文档 2.9 节)
表格
| 对比维度 | 软件 SPI | 硬件 SPI |
|---|---|---|
| 实现方式 | 普通 GPIO 模拟时序 | 硬件外设自动生成时序 |
| CPU 占用 | 高(需 CPU 全程参与) | 低(配置后自动传输) |
| 速率 | 低(最高约 1MHz) | 高(最高 80MHz) |
| 引脚限制 | 无(任意 GPIO) | 有(必须使用指定 SPI 引脚) |
| 代码复杂度 | 低(手动编写时序) | 中(配置外设寄存器) |
| 稳定性 | 一般(依赖延时精度) | 高(硬件时序精准) |
| 适用场景 | 低速、简单场景 | 高速、批量数据传输 |
3. 选型建议
- 优先选择硬件 SPI:W25Q64、OLED 屏幕、高速传感器等需要批量传输数据的场景;
- 优先选择软件 SPI:引脚资源紧张、低速通信、快速原型开发场景;
- 兼容性考虑:硬件 SPI 的引脚可能与其他外设冲突(如 JTAG),需注意引脚复用。
八、总结:SPI+W25Q64 核心要点与进阶方向
1. 核心要点回顾
- SPI 协议核心:四线制、全双工、同步时钟、四种工作模式,模式 0 为默认;
- W25Q64 核心:8MB 容量、扇区擦除 + 页编程、编程前必须写使能、Flash 只能写 0 擦 1;
- 驱动关键:SPI 参数配置匹配(模式 0、高位优先、8 位数据)、指令时序正确、地址发送顺序(高→低);
- 避坑核心:通信前读 ID 验证、编程前擦除、写使能不可少、跨页处理、电源稳定。
2. 进阶学习方向
- DMA 模式:硬件 SPI 支持 DMA 传输,进一步降低 CPU 占用,适合大批量数据读写;
- 中断模式:实现 SPI 通信中断,避免查询模式阻塞主循环;
- 多从设备:在同一 SPI 总线上挂载多个设备(如 W25Q64+OLED),通过 NSS 引脚切换;
- 低功耗优化:W25Q64 支持掉电模式,结合 STM32 低功耗,延长电池续航;
- 应用扩展:实现固件升级(将新固件存储到 W25Q64,再写入 STM32 Flash)、日志存储、配置参数保存。
掌握 SPI+W25Q64 后,你已具备嵌入式高速通信与数据存储的核心能力,可应用于工业控制、物联网设备、智能硬件等场景。下一篇我们将学习 STM32 的 CAN 总线通信,聚焦工业级设备间的高可靠性、实时性数据传输!