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 根线)。
本实验做的事情:
- 通过 SPI 读取 Flash 芯片的 ID(确认芯片存在且型号正确)
- 擦除一个扇区(Flash 必须先擦后写)
- 写入 512 字节测试数据
- 读回数据并与原始数据比对
- 数据一致 → 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
这行做了什么:
- 配置 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);
|
写入流程:
- 发送写使能命令(
0x06)— 必须先使能才能写
- 发送页编程命令(
0x02)+ 地址 + 数据
- 等待写入完成(读状态寄存器,等 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,了解另一种常用的低速通信总线。