AT32 SPI — 读写 W25Q Flash(外部存储芯片)

AT32F403A SPI 接口读写 W25Q 系列 Flash 芯片,学会 SPI 通信和外部存储操作

SPI — 读写 W25Q Flash

难度: ⭐⭐(需要了解 SPI 基本概念)
固件库: AT32F403A_407_Firmware_Library V2.2.2
源文件: project/at_start_f403a/examples/spi/w25q_flash/src/main.c


这个实验是干嘛的?

MCU 内部的 Flash 空间有限(几百 KB),存不下大文件(字库、图片、音频)。这时候就需要外部 Flash 芯片

W25Q 系列是最常用的 SPI Flash,容量从 1MB 到 256MB 都有,价格便宜(几毛钱),接口简单(4 根线)。

本实验做的事情

  1. 通过 SPI 读取 Flash 芯片的 ID(确认芯片存在且型号正确)
  2. 擦除一个扇区(Flash 必须先擦后写)
  3. 写入 512 字节测试数据
  4. 读回数据并与原始数据比对
  5. 数据一致 → LED 亮(成功),不一致 → LED 灭(失败)

硬件准备

你需要一个 W25Q Flash 模块(淘宝几块钱):

1
2
3
4
5
6
7
开发板          W25Q 模块
  PA4 (CS)  ──→  CS
  PA5 (SCK) ──→  CLK
  PA6 (MISO)←──  DO
  PA7 (MOSI)──→  DI
  3.3V      ──→  VCC
  GND       ──→  GND

注意

  • CS 是片选信号,拉低选中芯片
  • SPI 通信必须共地
  • W25Q 供电 3.3V,不要接 5V

SPI 基础知识

SPI 是一种同步、全双工的串行通信接口,只需要 4 根线:

信号 全称 方向 说明
SCK Serial Clock 主→从 时钟信号,主机产生
MOSI Master Out Slave In 主→从 主机发数据给从机
MISO Master In Slave Out 从→主 从机发数据给主机
CS Chip Select 主→从 片选,拉低选中从机

SPI 的特点

  • 速度比 I2C 快得多(可达几十 MHz)
  • 全双工:主机和从机可以同时收发
  • 每个从机需要一根独立的 CS 线

完整代码

  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
#include "at32f403a_407_board.h"
#include "at32f403a_407_clock.h"
#include "spi_flash.h"    // W25Q 驱动(官方封装好的)

/* 测试地址和缓冲区大小 */
#define FLASH_TEST_ADDR    0x1000    // 测试起始地址
#define BUF_SIZE           0x256     // 512 字节

uint8_t tx_buffer[BUF_SIZE];   // 发送缓冲区
uint8_t rx_buffer[BUF_SIZE];   // 接收缓冲区
volatile error_status transfer_status = ERROR;

/* 填充测试数据:0x00, 0x01, 0x02, ... 0xFF */
void tx_data_fill(void)
{
  uint32_t i;
  for(i = 0; i < BUF_SIZE; i++)
  {
    tx_buffer[i] = i;
  }
}

/* 缓冲区比对 */
error_status buffer_compare(uint8_t* p1, uint8_t* p2, uint16_t len)
{
  while(len--)
  {
    if(*p1 != *p2) return ERROR;
    p1++;
    p2++;
  }
  return SUCCESS;
}

int main(void)
{
  __IO uint32_t flash_id = 0;

  system_clock_config();
  at32_board_init();
  tx_data_fill();
  uart_print_init(115200);

  /* ===== 第一步:初始化 SPI Flash ===== */
  spiflash_init();

  /* ===== 第二步:读取芯片 ID ===== */
  flash_id = spiflash_read_id();

  /* 验证 ID 是否是 W25Q 系列 */
  if(flash_id != W25Q128 && flash_id != W25Q64 &&
     flash_id != W25Q32  && flash_id != W25Q16 && flash_id != W25Q80)
  {
    printf("flash id check error! id: %x\r\n", flash_id);
    // LED 快闪报错
    for(int i = 0; i < 50; i++)
    {
      at32_led_toggle(LED2);
      at32_led_toggle(LED3);
      delay_ms(200);
    }
    return 1;
  }
  else
  {
    printf("flash id check success! id: %x\r\n", flash_id);
  }

  /* ===== 第三步:擦除扇区 ===== */
  // Flash 必须先擦后写!擦除后所有字节变成 0xFF
  spiflash_sector_erase(FLASH_TEST_ADDR / SPIF_SECTOR_SIZE);

  /* ===== 第四步:写入数据 ===== */
  spiflash_write(tx_buffer, FLASH_TEST_ADDR, BUF_SIZE);

  /* ===== 第五步:读回数据 ===== */
  spiflash_read(rx_buffer, FLASH_TEST_ADDR, BUF_SIZE);

  /* 打印读到的数据 */
  printf("Read Data: ");
  for(int i = 0; i < BUF_SIZE; i++)
  {
    printf("%x ", rx_buffer[i]);
  }

  /* ===== 第六步:比对验证 ===== */
  transfer_status = buffer_compare(rx_buffer, tx_buffer, BUF_SIZE);

  if(transfer_status == SUCCESS)
  {
    printf("\r\nflash data read write success!\r\n");
    at32_led_on(LED2);   // LED2 亮 = 成功
  }
  else
  {
    printf("\r\nflash data read write ERROR!\r\n");
    at32_led_off(LED2);  // LED2 灭 = 失败
  }

  while(1) {}
}

代码分段讲解

1. 初始化 SPI Flash

1
spiflash_init();

这行做了什么:

  • 配置 SPI1 为主机模式
  • 配置时钟极性(CPOL)和相位(CPHA)—— W25Q 要求 CPOL=0, CPHA=0
  • 配置 CS 引脚(PA4)为 GPIO 输出
  • 拉高 CS(不选中芯片)

2. 读取芯片 ID

1
flash_id = spiflash_read_id();

每个 W25Q 芯片都有一个唯一的制造商 ID + 设备 ID:

  • W25Q80 → 0xEF14
  • W25Q16 → 0xEF15
  • W25Q32 → 0xEF16
  • W25Q64 → 0xEF17
  • W25Q128 → 0xEF18

读 ID 的命令是 0x90,SPI 时序:

1
2
主机发送: [0x90] [0x00] [0x00] [0x00] [dummy] [dummy]
从机返回: [dummy] [dummy] [dummy] [dummy] [制造商ID] [设备ID]

3. 擦除扇区

1
spiflash_sector_erase(FLASH_TEST_ADDR / SPIF_SECTOR_SIZE);

Flash 的铁律:必须先擦后写!

Flash 只能把 1 写成 0,不能把 0 写成 1。所以写之前必须先擦除(把所有位变成 1 = 0xFF)。

W25Q 的最小擦除单位是扇区(4KB)。擦除命令是 0x20

4. 写入数据

1
spiflash_write(tx_buffer, FLASH_TEST_ADDR, BUF_SIZE);

写入流程:

  1. 发送写使能命令(0x06)— 必须先使能才能写
  2. 发送页编程命令(0x02)+ 地址 + 数据
  3. 等待写入完成(读状态寄存器,等 BUSY 位清零)

注意:W25Q 一次最多写 256 字节(一页),跨页需要分多次写。

5. 读取数据

1
spiflash_read(rx_buffer, FLASH_TEST_ADDR, BUF_SIZE);

读取最简单:发送读命令(0x03)+ 地址,然后持续读取数据。没有长度限制。


实验现象

串口输出:

1
2
3
flash id check success! id: ef16
Read Data: 0 1 2 3 4 5 6 7 8 9 a b c d e f 10 11 ...
flash data read write success!
  • LED2 亮起表示读写成功
  • 如果 LED 快闪,说明芯片 ID 读取失败(接线问题)

常见问题

Q: 读到的 ID 是 0x0000 或 0xFFFF?

  • 检查接线:CS、SCK、MOSI、MISO 是否正确
  • 检查供电:W25Q 需要 3.3V
  • 检查 SPI 模式:CPOL=0, CPHA=0

Q: 写入后读出来的数据不对?

  • 是否先擦除了?Flash 必须先擦后写
  • 擦除后检查是否全为 0xFF
  • 写入地址是否对齐(扇区边界 4KB 对齐)

Q: 想存储大文件怎么办?

  • Flash 文件系统:FatFs + SPI Flash
  • 或者简单方案:固定地址存储,自己管理地址分配

小结

概念 说明
SPI 高速同步串行接口,4 线制
W25Q 最常用的 SPI Flash 芯片
先擦后写 Flash 的铁律,擦除后全为 0xFF
扇区 最小擦除单位,W25Q 是 4KB
页写入 一次最多写 256 字节

学了 SPI 能做什么?

SPI 是连接"高速外设"的主力接口:

存储与显示

项目 原理 难度
字库存储 中文字库烧录到 W25Q,LCD 显示中文 ⭐⭐⭐
图片存储 BMP/JPG 图片存 Flash,LCD 显示 ⭐⭐⭐
数据记录 传感器数据存 Flash,掉电不丢失 ⭐⭐
固件升级 新固件存 Flash,IAP 从 Flash 加载运行 ⭐⭐⭐⭐
LCD 驱动 SPI 接口的 TFT LCD(ST7735、ILI9341) ⭐⭐⭐
OLED 显示 SPI 接口的 OLED 屏(SSD1306) ⭐⭐

传感器与通信

项目 原理 难度
SD 卡读写 SPI 模式访问 SD 卡,实现大容量存储 ⭐⭐⭐
加速度计 SPI 接口的 MEMS 传感器(MPU6050) ⭐⭐⭐
LoRa 通信 SPI 接口的 LoRa 模块(SX1278) ⭐⭐⭐⭐
以太网 SPI 接口的以太网芯片(W5500) ⭐⭐⭐⭐
CAN 控制器 SPI 接口的 CAN 芯片(MCP2515) ⭐⭐⭐

核心思路:SPI 的核心优势是速度快。Flash、LCD、传感器这些需要大量数据传输的外设,几乎都用 SPI。掌握了 SPI,你就打开了连接各种高速外设的大门。


下一步:学会了 SPI,可以学 I2C 读写 EEPROM,了解另一种常用的低速通信总线。

世界是你们
使用 Hugo 构建
主题 StackJimmy 设计