STM32 SPI — 读写串行 FLASH(W25Q64)

野火 EBF STM32F103 Mini HAL 库:用 SPI1 驱动 W25Q64 串行 Flash,实现读 ID、扇区擦除、页写、缓冲区读写全流程。

STM32 SPI — 读写串行 FLASH(W25Q64)

开发板:野火 EBF STM32F103 Mini
固件库:HAL 库
对应例程16-SPI—读写串行FLASH(W25Q64)
参考源码User/main.cUser/flash/bsp_spi_flash.hUser/flash/bsp_spi_flash.c

SPI(Serial Peripheral Interface)是一种高速、全双工、同步的硬件总线,常用于连接 Flash、LCD、传感器等。本文用 SPI1 驱动一颗 8MB 的 W25Q64 串行 Flash,完整演示"读 ID → 擦除 → 写 → 读 → 校验"的存储流程。


一、功能简介

W25Q64 通过 SPI 接收命令码 + 地址 + 数据来工作。本例实现:

  1. 初始化 SPI1(模式 0/3 兼容,本例用 CPOL=High、CPHA=2Edge,即模式 3);
  2. 读取 Device IDJEDEC ID 验证芯片;
  3. 扇区擦除(Flash 只能把 1 写成 0,擦除才把 0 还原成 1);
  4. 页写 + 缓冲区写(自动处理跨页);
  5. 读数据,并与写入内容比对,成功则点亮 LED2。

核心知识点:

  • SPI 四线(SCK/MOSI/MISO/CS)与时序(CPOL/CPHA);
  • 片选 CS 的软件控制;
  • Flash “先擦后写” 与 “” 概念;
  • 一个字节发送即同步接收的全双工本质。

二、硬件准备

项目 说明
开发板 野火 EBF STM32F103 Mini
SPI 外设 SPI1
引脚 SCK → PA5,MISO → PA6,MOSI → PA7,CS → PA4
器件 W25Q64(8MB,SPI 接口串行 NOR Flash)
调试输出 USART1(115200)打印 ID 与读写结果

接线要点:SPI 主机的 MOSI 接从机 MOSI,MISO 接 MISO,SCK 接 SCK,CS 由主机用任意一个 GPIO 软件拉低/拉高来选通从机。


三、完整代码(带中文注释)

1. bsp_spi_flash.h(引脚、指令、接口声明)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#ifndef __SPI_FLASH_H
#define __SPI_FLASH_H

#include "stm32f1xx.h"
#include <stdio.h>

/* W25Q64 的 JEDEC ID = 0xEF4017(厂商 0xEF / 类型 0x40 / 容量 0x17=8MB) */
#define  sFLASH_ID             0XEF4017

#define SPI_FLASH_PageSize     256        // 一页 256 字节
#define SPI_FLASH_PerWritePageSize  256

/* ---- W25Q64 指令集(部分常用) ---- */
#define W25X_WriteEnable       0x06      // 写使能
#define W25X_WriteDisable      0x04
#define W25X_ReadStatusReg     0x05      // 读状态寄存器
#define W25X_ReadData          0x03      // 读数据
#define W25X_PageProgram       0x02      // 页编程(写)
#define W25X_SectorErase       0x20      // 扇区擦除(4KB)
#define W25X_ChipErase         0xC7      // 整片擦除
#define W25X_PowerDown         0xB9      // 掉电
#define W25X_ReleasePowerDown  0xAB
#define W25X_DeviceID          0xAB      // 读 Device ID
#define W25X_JedecDeviceID     0x9F      // 读 JEDEC ID

#define WIP_Flag               0x01       // 状态寄存器 bit0:Write In Progress
#define Dummy_Byte             0xFF       // 读操作时发送的无意义填充字节

/* ---- SPI 与 GPIO 引脚定义 ---- */
#define SPIx                   SPI1
#define SPIx_CLK_ENABLE()      __HAL_RCC_SPI1_CLK_ENABLE()

#define SPIx_SCK_PIN           GPIO_PIN_5
#define SPIx_SCK_GPIO_PORT     GPIOA
#define SPIx_MISO_PIN          GPIO_PIN_6
#define SPIx_MISO_GPIO_PORT    GPIOA
#define SPIx_MOSI_PIN          GPIO_PIN_7
#define SPIx_MOSI_GPIO_PORT    GPIOA
#define FLASH_CS_PIN           GPIO_PIN_4
#define FLASH_CS_GPIO_PORT     GPIOA

/* 用寄存器直接操作 CS,速度最快 */
#define digitalHi(p,i)  {p->BSRR = i;}
#define digitalLo(p,i)  {p->BSRR = (uint32_t)i << 16;}
#define SPI_FLASH_CS_LOW()   digitalLo(FLASH_CS_GPIO_PORT, FLASH_CS_PIN)   // 选通
#define SPI_FLASH_CS_HIGH()  digitalHi(FLASH_CS_GPIO_PORT, FLASH_CS_PIN)   // 取消选通

void SPI_FLASH_Init(void);
void SPI_FLASH_SectorErase(uint32_t SectorAddr);
void SPI_FLASH_BufferWrite(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite);
void SPI_FLASH_BufferRead(uint8_t* pBuffer, uint32_t ReadAddr, uint16_t NumByteToRead);
uint32_t SPI_FLASH_ReadID(void);
uint32_t SPI_FLASH_ReadDeviceID(void);
uint8_t SPI_FLASH_SendByte(uint8_t byte);
void SPI_FLASH_WriteEnable(void);
void SPI_FLASH_WaitForWriteEnd(void);
void SPI_Flash_PowerDown(void);

#endif /* __SPI_FLASH_H */

2. bsp_spi_flash.c(SPI 初始化与 Flash 驱动,节选核心)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
#include "./flash/bsp_spi_flash.h"

SPI_HandleTypeDef SpiHandle;

/* MSP:开时钟 + 配 GPIO(SCK/MOSI/MISO 复用,CS 普通推挽输出) */
void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi)
{
  GPIO_InitTypeDef GPIO_InitStruct;

  SPIx_SCK_GPIO_CLK_ENABLE();
  SPIx_MISO_GPIO_CLK_ENABLE();
  SPIx_MOSI_GPIO_CLK_ENABLE();
  SPIx_CS_GPIO_CLK_ENABLE();
  SPIx_CLK_ENABLE();

  GPIO_InitStruct.Pin   = SPIx_SCK_PIN;
  GPIO_InitStruct.Mode  = GPIO_MODE_AF_PP;       // 复用推挽
  GPIO_InitStruct.Pull  = GPIO_PULLUP;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
  HAL_GPIO_Init(SPIx_SCK_GPIO_PORT, &GPIO_InitStruct);

  GPIO_InitStruct.Pin = SPIx_MISO_PIN;           // MISO 同样复用
  HAL_GPIO_Init(SPIx_MISO_GPIO_PORT, &GPIO_InitStruct);

  GPIO_InitStruct.Pin = SPIx_MOSI_PIN;
  HAL_GPIO_Init(SPIx_MOSI_GPIO_PORT, &GPIO_InitStruct);

  GPIO_InitStruct.Pin  = FLASH_CS_PIN;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;     // CS 用软件普通 GPIO 控制
  HAL_GPIO_Init(FLASH_CS_GPIO_PORT, &GPIO_InitStruct);
}

/* SPI 参数配置:主模式、8 位、MSB 先行、模式 3、软件 NSS、4 分频 */
void SPI_FLASH_Init(void)
{
  SpiHandle.Instance               = SPIx;
  SpiHandle.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4;  // 72M/4=18MHz
  SpiHandle.Init.Direction         = SPI_DIRECTION_2LINES;     // 全双工
  SpiHandle.Init.CLKPhase          = SPI_PHASE_2EDGE;          // CPHA=1
  SpiHandle.Init.CLKPolarity       = SPI_POLARITY_HIGH;        // CPOL=1(模式3)
  SpiHandle.Init.CRCCalculation    = SPI_CRCCALCULATION_DISABLE;
  SpiHandle.Init.CRCPolynomial     = 7;
  SpiHandle.Init.DataSize          = SPI_DATASIZE_8BIT;
  SpiHandle.Init.FirstBit          = SPI_FIRSTBIT_MSB;
  SpiHandle.Init.NSS               = SPI_NSS_SOFT;             // 软件管理 CS
  SpiHandle.Init.TIMode            = SPI_TIMODE_DISABLE;
  SpiHandle.Init.Mode              = SPI_MODE_MASTER;

  HAL_SPI_Init(&SpiHandle);
  __HAL_SPI_ENABLE(&SpiHandle);
}

/* 向 Flash 发送 1 字节,同时读出 1 字节(SPI 全双工特性) */
uint8_t SPI_FLASH_SendByte(uint8_t byte)
{
  /* 等待发送缓冲区空 */
  while (__HAL_SPI_GET_FLAG(&SpiHandle, SPI_FLAG_TXE) == RESET);
  WRITE_REG(SpiHandle.Instance->DR, byte);        // 写入数据寄存器

  /* 等待接收缓冲区非空 */
  while (__HAL_SPI_GET_FLAG(&SpiHandle, SPI_FLAG_RXNE) == RESET);
  return READ_REG(SpiHandle.Instance->DR);        // 读出收到的数据
}

/* 读 JEDEC ID:发 0x9F 后连续收 3 字节 */
uint32_t SPI_FLASH_ReadID(void)
{
  uint32_t Temp = 0, Temp0 = 0, Temp1 = 0, Temp2 = 0;
  SPI_FLASH_CS_LOW();
  SPI_FLASH_SendByte(W25X_JedecDeviceID);
  Temp0 = SPI_FLASH_SendByte(Dummy_Byte);
  Temp1 = SPI_FLASH_SendByte(Dummy_Byte);
  Temp2 = SPI_FLASH_SendByte(Dummy_Byte);
  SPI_FLASH_CS_HIGH();
  Temp = (Temp0 << 16) | (Temp1 << 8) | Temp2;   // 组合成 24 位 ID
  return Temp;
}

/* 扇区擦除(4KB):写使能 -> 发指令+3字节地址 -> 等待写完 */
void SPI_FLASH_SectorErase(uint32_t SectorAddr)
{
  SPI_FLASH_WriteEnable();
  SPI_FLASH_WaitForWriteEnd();

  SPI_FLASH_CS_LOW();
  SPI_FLASH_SendByte(W25X_SectorErase);
  SPI_FLASH_SendByte((SectorAddr & 0xFF0000) >> 16);  // 地址高字节
  SPI_FLASH_SendByte((SectorAddr & 0xFF00) >> 8);     // 地址中字节
  SPI_FLASH_SendByte(SectorAddr & 0xFF);              // 地址低字节
  SPI_FLASH_CS_HIGH();
  SPI_FLASH_WaitForWriteEnd();
}

/* 按页写:最多写一页 256 字节,写完等待内部编程结束 */
void SPI_FLASH_PageWrite(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{
  SPI_FLASH_WriteEnable();
  SPI_FLASH_CS_LOW();
  SPI_FLASH_SendByte(W25X_PageProgram);
  SPI_FLASH_SendByte((WriteAddr & 0xFF0000) >> 16);
  SPI_FLASH_SendByte((WriteAddr & 0xFF00) >> 8);
  SPI_FLASH_SendByte(WriteAddr & 0xFF);

  if (NumByteToWrite > SPI_FLASH_PerWritePageSize)
    NumByteToWrite = SPI_FLASH_PerWritePageSize;   // 超页则截断

  while (NumByteToWrite--)
    SPI_FLASH_SendByte(*pBuffer++);               // 逐字节写入

  SPI_FLASH_CS_HIGH();
  SPI_FLASH_WaitForWriteEnd();
}

/* 缓冲区写:自动拆分跨页,内部调用 PageWrite */
void SPI_FLASH_BufferWrite(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{
  uint8_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0;
  Addr = WriteAddr % SPI_FLASH_PageSize;          // 起始地址在页内的偏移
  count = SPI_FLASH_PageSize - Addr;              // 当前页还能写多少
  NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;
  NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;

  if (Addr == 0)                                 // 地址正好页对齐
  {
    if (NumOfPage == 0)                          // 不到一页
      SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);
    else
    {
      while (NumOfPage--)                         // 整页挨个写
      {
        SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);
        WriteAddr += SPI_FLASH_PageSize;
        pBuffer   += SPI_FLASH_PageSize;
      }
      if (NumOfSingle != 0)                      // 尾巴不足一页
        SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);
    }
  }
  else                                           // 地址未对齐
  {
    if (NumOfPage == 0)
    {
      if (NumOfSingle > count)                   // 当前页放不下,拆成两段
      {
        temp = NumOfSingle - count;
        SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);
        SPI_FLASH_PageWrite(pBuffer + count, WriteAddr + count, temp);
      }
      else
        SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);
    }
    else                                         // 多页情况,先填满当前页再整页写
    {
      NumByteToWrite -= count;
      NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;
      NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;
      SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);
      WriteAddr += count; pBuffer += count;
      while (NumOfPage--)
      {
        SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);
        WriteAddr += SPI_FLASH_PageSize; pBuffer += SPI_FLASH_PageSize;
      }
      if (NumOfSingle != 0)
        SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);
    }
  }
}

/* 读数据:发 0x03 + 3 字节地址,随后逐字节收 */
void SPI_FLASH_BufferRead(uint8_t* pBuffer, uint32_t ReadAddr, uint16_t NumByteToRead)
{
  SPI_FLASH_CS_LOW();
  SPI_FLASH_SendByte(W25X_ReadData);
  SPI_FLASH_SendByte((ReadAddr & 0xFF0000) >> 16);
  SPI_FLASH_SendByte((ReadAddr & 0xFF00) >> 8);
  SPI_FLASH_SendByte(ReadAddr & 0xFF);
  while (NumByteToRead--)
    *pBuffer++ = SPI_FLASH_SendByte(Dummy_Byte); // 每发 1 个哑字节,收 1 个真实数据
  SPI_FLASH_CS_HIGH();
}

3. main.c(演示流程)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include "main.h"
#include "stm32f1xx.h"
#include "./usart/bsp_debug_usart.h"
#include "./led/bsp_led.h"
#include "./flash/bsp_spi_flash.h"

uint8_t Tx_Buffer[] = "感谢您选用野火stm32开发板\r\nhttp://firestm32.taobao.com";
uint8_t Rx_Buffer[sizeof(Tx_Buffer)];

int main(void)
{
  SystemClock_Config();
  LED_GPIO_Config();
  DEBUG_USART_Config();

  printf("\r\n这是一个8M串行flash(W25Q64)实验(SPI驱动) \r\n");

  SPI_FLASH_Init();
  __IO uint32_t FlashID = SPI_FLASH_ReadID();
  printf("\r\nFlashID is 0x%X\r\n", FlashID);

  if (FlashID == sFLASH_ID)
  {
    printf("\r\n检测到SPI FLASH W25Q64 !\r\n");
    SPI_FLASH_SectorErase(0x00000);                       // 写前先擦除
    SPI_FLASH_BufferWrite(Tx_Buffer, 0x00000, sizeof(Tx_Buffer));   // 写入
    printf("\r\n写入的数据为:\r\n%s", Tx_Buffer);

    SPI_FLASH_BufferRead(Rx_Buffer, 0x00000, sizeof(Tx_Buffer));    // 读出
    printf("\r\n读出的数据为:\r\n%s", Rx_Buffer);

    if (memcmp(Tx_Buffer, Rx_Buffer, sizeof(Tx_Buffer)) == 0)
    { LED2_ON;  printf("\r\n8M串行flash(W25Q64)测试成功!\n\r"); }
    else
    { LED1_ON;  printf("\r\n8M串行flash(W25Q64)测试失败!\n\r"); }
  }
  else
  { LED1_ON;  printf("\r\n获取不到 W25Q64 ID!\n\r"); }

  SPI_Flash_PowerDown();
  while(1);
}

四、代码分段讲解

1. SPI 是"全双工移位寄存器"

主机每发送 1 个字节的同时,必然接收 1 个字节——这是 SPI 的硬件本质。SPI_FLASH_SendByte() 正是利用这一点:写 DR 发命令,读 DR 取回从机回应的数据(读操作里发的是 Dummy_Byte 0xFF,纯属为了产生时钟去"挤"出数据)。

2. 模式 3 是什么?

SPI 有 4 种模式,由 CPOL(时钟极性)和 CPHA(时钟相位)决定。W25Q64 支持模式 0 和模式 3。本例用 CPOL=High、CPHA=2Edge(模式 3):空闲时 SCK 为高,在第二个边沿(下降沿)采样。主从双方模式必须一致。

3. 为什么 CS 用软件 GPIO 控制?

SPI_NSS_SOFT 表示片选不交给硬件 NSS 引脚,而是我们手动拉低/拉高 FLASH_CS。这样时序最灵活——每个命令前后都能精确控制 CS 的脉冲,符合 Flash 的"命令-地址-数据"帧格式。

4. Flash “先擦后写"的铁律

NOR Flash 的存储单元只能把 1 写成 0,不能直接把 0 改回 1,只有"擦除"操作(按扇区/块/整片)才能整片恢复成 1。所以写数据前必须先擦除目标扇区(本例每扇区 4KB)。未擦除就写,会导致数据错误。

5. 为什么要分页写?

W25Q64 的"页编程"一次最多写 256 字节,且地址不能跨页自动回绕。因此 BufferWrite 要计算起始地址在页内的偏移,把超过本页边界的数据拆分到下一页写——这是 Flash 驱动里最易错、也最重要的细节。

6. 写后为什么要 WaitForWriteEnd

芯片内部把数据烧录进存储阵列需要时间(毫秒级)。状态寄存器 bit0(WIP)为 1 表示"正在写”。驱动每次写/擦后都轮询该位,直到归零才继续,避免下一条命令在忙时被忽略。


五、实验现象

  1. 串口打印 检测到SPI FLASH W25Q64 !,并显示 FlashID is 0xEF4017
  2. 串口回显写入的字符串与读出的字符串完全一致;
  3. 板载 LED2 点亮,表示读写校验成功;
  4. 若 ID 不符,LED1 点亮并打印"获取不到 W25Q64 ID"。

六、常见问题

Q1:读不到正确 ID(全 0 或全 FF)?

  • 检查 SPI 模式(CPOL/CPHA)与 Flash 是否匹配;
  • 检查 CS 时序:每条命令前后 CS 必须正确拉低/拉高;
  • 用逻辑分析仪抓 SCK/MOSI/MISO,确认命令码真的发出去了。

Q2:写入后读出数据不对?

  • 八成是没擦除就写了;写前务必 SPI_FLASH_SectorErase
  • 或写入长度跨页但没做分页处理。

Q3:SPI 时钟太快导致通信失败?

  • 先把 SPI_BAUDRATEPRESCALER 调大(分频更多、更慢),稳定后再提速;W25Q64 最高支持约 80MHz(双/四线更快,但本例单线 18MHz 已稳妥)。

七、小结

  • SPI 通信 = 配置主模式参数 + 软件控 CS + 命令/地址/数据帧;
  • Flash 驱动三大要点:先擦后写、分页处理、写后等忙
  • 利用 SPI 全双工"发即收"特性,一个 SendByte 就能完成收发。

八、拓展应用

  1. FatFs 文件系统:在 W25Q64 上挂载 FatFs,像 U 盘一样存文件(参考 Flash FatFs文件系统移植)。
  2. 存放字库/图片:把中文字模、BMP 图存进 Flash,LCD 显示时再读出来(见液晶显示系列例程)。
  3. SPI DMA 加速:用 DMA 代替逐字节 SendByte,大块数据搬运效率大幅提升。
  4. 四线 QSPI:换用支持 QSPI 的 MCU(如 F4/F7/H7),把 W25Q64 当运行内存(XIP)直接执行代码。
世界是你们
使用 Hugo 构建
主题 StackJimmy 设计