💾 SPI 实战:与 Flash 芯片通信,把数据真正“存起来”
“数据能算出来不算本事,能断电还在,才叫工程。”
——嵌入式工程师的尊严时刻
🧠 一、为什么要学 SPI + Flash?
很多新手做项目时会遇到一个现实问题:
- MCU 掉电后,数据全没了 ❌
- 想保存参数、日志、历史数据怎么办?
这时候你就会遇到非易失性存储器,而 SPI Flash 是性价比最高、最常见的一种。
它几乎是嵌入式项目的“标配外挂存储”。

🧩 二、SPI Flash 是什么?
SPI Flash 本质上是一个通过 SPI 总线访问的存储芯片。
常见型号包括:
| 型号 | 容量 | 厂商 |
|---|---|---|
| W25Q16 | 2MB | Winbond |
| W25Q64 | 8MB | Winbond |
| GD25Q32 | 4MB | GigaDevice |
| MX25L64 | 8MB | Macronix |
特点:
- 掉电数据不丢失
- 通过 SPI 读写
- 支持页写入、扇区擦除
- 常用于:参数存储、日志、字库、固件升级(OTA)
🔌 三、SPI Flash 硬件连接(以 W25Q64 为例)
| Flash 引脚 | MCU | 说明 |
|---|---|---|
| CS | SPI_NSS | 片选,低有效 |
| CLK | SPI_SCK | 时钟 |
| MOSI | SPI_MOSI | 主机发数据 |
| MISO | SPI_MISO | 从机回数据 |
| VCC | 3.3V | 大多数 Flash 不耐 5V |
| GND | GND | 共地 |
⚠️ 注意:
- SPI Flash 一般不耐 5V
- CS 引脚一定要独立控制
⚙️ 四、CubeMX 配置 SPI(推荐)
-
选择 SPI1 → Full-Duplex Master
-
NSS 选择 GPIO 控制
-
Clock Polarity / Phase:
- Mode 0(CPOL=0, CPHA=0)最常见
-
Data Size:8-bit
-
BaudRate Prescaler:先选慢一点(如 8 / 16)
CubeMX 会生成:
HAL_SPI_Init(&hspi1);
📜 五、SPI Flash 通信协议核心概念
常用指令表(必须记住)
| 功能 | 指令 |
|---|---|
| 读 JEDEC ID | 0x9F |
| 写使能 | 0x06 |
| 读状态寄存器 | 0x05 |
| 扇区擦除(4KB) | 0x20 |
| 页编程(写) | 0x02 |
| 读取数据 | 0x03 |
SPI Flash 是指令驱动型设备:
不发指令,它什么都不干。
🧪 六、基础通信:读取 Flash ID
这是验证 SPI 是否连通的第一步。
uint32_t SPI_Flash_ReadID(void)
{
uint8_t cmd = 0x9F;
uint8_t id[3] = {0};
FLASH_CS_LOW();
HAL_SPI_Transmit(&hspi1, &cmd, 1, 100);
HAL_SPI_Receive(&hspi1, id, 3, 100);
FLASH_CS_HIGH();
return (id[0] << 16) | (id[1] << 8) | id[2];
}
如果你读到类似:
EF 40 17
恭喜你,Flash 已经“开口说话”了。
✍️ 七、写 Flash 的正确流程(非常重要)
❗ 写 Flash ≠ 直接写!
正确流程是:
写使能 → 擦除 → 等待完成 → 页写 → 等待完成
1️⃣ 写使能
void SPI_Flash_WriteEnable(void)
{
uint8_t cmd = 0x06;
FLASH_CS_LOW();
HAL_SPI_Transmit(&hspi1, &cmd, 1, 100);
FLASH_CS_HIGH();
}
2️⃣ 等待 Flash 空闲
void SPI_Flash_WaitBusy(void)
{
uint8_t cmd = 0x05;
uint8_t status;
do {
FLASH_CS_LOW();
HAL_SPI_Transmit(&hspi1, &cmd, 1, 100);
HAL_SPI_Receive(&hspi1, &status, 1, 100);
FLASH_CS_HIGH();
} while (status & 0x01); // BUSY 位
}
3️⃣ 扇区擦除(4KB)
void SPI_Flash_EraseSector(uint32_t addr)
{
uint8_t cmd[4] = {
0x20,
(addr >> 16) & 0xFF,
(addr >> 8) & 0xFF,
addr & 0xFF
};
SPI_Flash_WriteEnable();
FLASH_CS_LOW();
HAL_SPI_Transmit(&hspi1, cmd, 4, 100);
FLASH_CS_HIGH();
SPI_Flash_WaitBusy();
}
4️⃣ 页写(最多 256 字节)
void SPI_Flash_PageWrite(uint32_t addr, uint8_t *buf, uint16_t len)
{
uint8_t cmd[4] = {
0x02,
(addr >> 16) & 0xFF,
(addr >> 8) & 0xFF,
addr & 0xFF
};
SPI_Flash_WriteEnable();
FLASH_CS_LOW();
HAL_SPI_Transmit(&hspi1, cmd, 4, 100);
HAL_SPI_Transmit(&hspi1, buf, len, 100);
FLASH_CS_HIGH();
SPI_Flash_WaitBusy();
}
⚠️ 注意:
len <= 256- 不能跨页写(否则数据会回绕)
📖 八、读取 Flash 数据
void SPI_Flash_Read(uint32_t addr, uint8_t *buf, uint16_t len)
{
uint8_t cmd[4] = {
0x03,
(addr >> 16) & 0xFF,
(addr >> 8) & 0xFF,
addr & 0xFF
};
FLASH_CS_LOW();
HAL_SPI_Transmit(&hspi1, cmd, 4, 100);
HAL_SPI_Receive(&hspi1, buf, len, 100);
FLASH_CS_HIGH();
}
🧪 九、完整测试:写入 + 读出验证
uint8_t tx_data[] = "Hello SPI Flash!";
uint8_t rx_data[32];
SPI_Flash_EraseSector(0x000000);
SPI_Flash_PageWrite(0x000000, tx_data, sizeof(tx_data));
SPI_Flash_Read(0x000000, rx_data, sizeof(tx_data));
如果串口打印出的 rx_data 正确,
说明你已经掌握了 SPI Flash 的基本驱动!
🧯 十、常见踩坑合集
| 问题 | 原因 |
|---|---|
| 写不进去 | 忘了写使能 |
| 读全是 0xFF | 没擦除 |
| 写完立刻读不对 | 没等 BUSY 清零 |
| 写跨页数据错乱 | 超过 256 字节 |
| SPI 通信异常 | 模式不对(CPOL/CPHA) |
🧠 十一、工程级建议(进阶)
- 抽象 Flash 驱动接口(
flash_read/write) - 做参数区 / 日志区地址规划
- 增加 CRC 校验防止数据损坏
- 使用环形日志结构
- 为 OTA 升级预留空间
✅ 十二、总结
SPI Flash 是嵌入式项目里非常关键的一环,
它让 MCU 具备了:
- 掉电保存能力
- 参数记忆能力
- 数据记录能力
学会 SPI Flash,你的项目就不再是“断电即失忆”。



1357

被折叠的 条评论
为什么被折叠?



