STM32 进阶封神之路(三十二):SPI 通信深度实战 —— 硬件 SPI 驱动 W25Q64 闪存(底层时序 + 寄存器配置 + 读写封装)

0 阅读25分钟

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 默认支持)
模式 10(空闲时 SCK 为低电平)1(第二个时钟跳变沿采样)下降沿采样,上升沿发送部分传感器
模式 21(空闲时 SCK 为高电平)0(第一个时钟跳变沿采样)下降沿采样,上升沿发送工业控制设备
模式 31(空闲时 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.3VVCCVCCW25Q64 工作电压 3.3V
GNDGNDGND共地
3.3VWPWP写保护引脚,接高电平禁用写保护
3.3VHOLDHOLD保持引脚,接高电平正常工作

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)4KB20480x00000~0x00FFF(第 0 页)单次编程最小单元
扇区(Sector)64KB1280x00000~0x0FFFF(第 0 扇区)单次擦除最小单元
块(Block)32KB/64KB256/1280x00000~0x07FFF(32KB 块)批量擦除单元
全片(Chip)8MB10x000000~0x7FFFFF整个闪存

关键规则

  • 编程前必须先擦除(Flash 特性:只能将 1 改为 0,擦除可将 0 改为 1);
  • 页编程时,超过 4KB 的数据会循环覆盖当前页的起始地址;
  • 扇区擦除会将该扇区内的所有数据置为 0xFF。

3. W25Q64 核心指令集(参考文档指令操作)

W25Q64 通过 SPI 接收指令实现读写擦除,常用核心指令如下:

表格

指令代码指令名称功能描述指令长度备注
0x9F读取 ID读取制造商 ID、设备 ID1 字节指令 + 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 总线通信,聚焦工业级设备间的高可靠性、实时性数据传输!