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

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 总线通信,聚焦工业级设备间的高可靠性、实时性数据传输!

相关推荐
不做无法实现的梦~2 小时前
clion配置stm32(调试,烧录的详细教程)
stm32·单片机·嵌入式硬件
RestCloud2 小时前
API网关 vs iPaaS:企业集成架构选型的本质差异与2026年选型指南
架构·数据处理·数据传输·ipaas·ai网关·集成平台
好大哥呀2 小时前
C++ Web 编程
开发语言·前端·c++
Mr_Xuhhh3 小时前
LeetCode hot 100(C++版本)(上)
c++·leetcode·哈希算法
漫随流水3 小时前
c++编程:反转字符串(leetcode344)
数据结构·c++·算法
南境十里·墨染春水3 小时前
C++ 笔记 友元(面向对象)
开发语言·c++·笔记
笨笨饿4 小时前
20_Git 仓库使用手册 - 初学者指南
c语言·开发语言·嵌入式硬件·mcu·学习
C++ 老炮儿的技术栈4 小时前
分享一个安全的CString
c语言·c++·windows·git·安全·visual studio
freshman_y4 小时前
STM32工程模板如何配置
stm32·单片机·嵌入式硬件