1. 课程导入
在嵌入式系统中,SPI Flash 几乎是“标配”存储器件:它承载启动镜像、文件系统、参数区、日志区与固件升级包。很多开发者第一次接触它时,会把它当成“能读能写的黑盒”,直到遇到读写失败、数据错位、掉电丢数据、启动异常,才意识到驱动层的重要性。本课程的目标不是只让你会调用几个读写函数,而是让你能独立完成一套可移植、可验证、可排障的 SPI Flash 驱动。
一、学习目标:从“能读到数据”到“能交付驱动”
本课程围绕四个层次展开。第一层是硬件认知:理解 NOR Flash 的存储阵列、扇区/块结构、页编程限制、擦除特性与寿命约束。第二层是协议与控制器:掌握 SPI 模式、命令集、状态寄存器、时序参数,以及 MCU 侧 SPI 控制器与 DMA 的配合。第三层是驱动实现:完成初始化、读 ID、读/写/擦除、页缓存管理、地址映射与错误处理。第四层是工程化验证:通过回环测试、边界测试、掉电测试与性能测试,证明驱动在真实场景下可靠。
二、核心概念:先建立正确的“存储观”
SPI Flash 驱动开发中,最容易出错的地方往往不是代码,而是对器件行为的误解。以下概念必须清晰:
- NOR 与 NAND:SPI Flash 多为 NOR 结构,支持随机读取,但写入前必须先擦除。
- 扇区与块:擦除最小单位通常是 4KB 扇区,也有 32KB、64KB 块;不能按字节擦除。
- 页编程:写入以页为单位,常见 256 字节;跨页写入必须拆分。
- 状态寄存器:BUSY 位表示内部操作未完成,WEL 位表示写使能锁存。
- 写使能:每次写、擦除前都必须发送 WREN,否则操作被忽略。
- 地址模式:3 字节地址最大 16MB,超过后需 4 字节地址或进入 4B 模式。
- SFDP:部分器件支持 SFDP 表,可自动读取容量与命令支持信息。
这些概念之间不是孤立的,而是共同决定驱动状态机。下图给出 SPI Flash 驱动开发的核心知识框架与实战路线,从硬件层到应用层逐级递进。
三、方法流程:驱动开发的六步闭环
为了避免“边写边猜”,建议按固定流程推进。每一步都有明确输入与输出,便于定位问题。
- 读数据手册:提取容量、扇区大小、页大小、命令集、时序、状态位定义。
- 确认硬件连接:SCK/MOSI/MISO/CS 引脚、上拉、供电、去耦、地回路。
- 打通 SPI 基础通信:先读 JEDEC ID,确认模式、时钟极性、位序正确。
- 实现擦写读:按“写使能→擦除→等待 BUSY→页编程→等待 BUSY→读取校验”闭环。
- 加入缓存与地址映射:处理跨页、跨扇区,提供统一读写接口。
- 测试与复盘:覆盖边界、异常、掉电与性能,记录日志并优化。
其中第 3 步是最关键的“分水岭”。如果 JEDEC ID 读不到,后面所有工作都无从谈起。下面是一段典型的读 ID 命令序列,可用于快速验证链路:
// 读 JEDEC ID:0x9F,返回 3 字节
uint8_t cmd = 0x9F;
uint8_t id[3] = {0};
spi_cs_low();
spi_transfer(&cmd, 1);
spi_transfer(id, 3);
spi_cs_high();
printf("JEDEC ID: %02X %02X %02X\n", id[0], id[1], id[2]);
// 例如:EF 40 18 表示 Winbond W25Q128 (16MB)
四、案例与场景:从启动到 OTA 的真实链路
假设一个典型场景:MCU 上电后从 SPI Flash 读取启动镜像,运行后通过 OTA 将新固件写入 Flash 的备份区,校验通过后切换启动地址。这个场景对驱动提出三个硬性要求:
- 读可靠性:启动阶段必须稳定读取,不能因时序或 CS 抖动导致镜像校验失败。
- 写原子性:OTA 写入过程中若掉电,备份区不能破坏原固件区。
- 性能可控:大块写入需使用 Quad 模式或 DMA,否则升级时间不可接受。
因此,驱动不能只提供 flash_read() 和 flash_write(),还要提供擦除粒度管理、写保护控制、状态轮询与超时机制。下表列出常见操作与关键约束,便于开发时对照检查。
| 操作 | 典型命令 | 最小单位 | 关键约束 |
|---|---|---|---|
| 读数据 | 0x03 / 0x0B | 字节 | 支持随机读,高速读需 dummy 周期 |
| 页编程 | 0x02 | 256 字节页 | 写前 WREN,跨页需拆分,等待 BUSY |
| 扇区擦除 | 0x20 | 4KB | 写前 WREN,擦除后数据为 0xFF |
| 块擦除 | 0xD8 | 64KB | 适合大区域快速擦除 |
| 读状态 | 0x05 | 1 字节 | 轮询 BUSY 位,判断操作完成 |
| 写使能 | 0x06 | — | 每次写/擦前必须发送 |