2. SPI Flash基础
在嵌入式系统中,SPI Flash 是最常见的非易失性存储器件之一。它通过 SPI 总线与 MCU 通信,用于存放 Bootloader、固件、配置参数、字库、日志等数据。很多开发者第一次接触 SPI Flash 时,会把它简单理解成“一个可以通过 SPI 读写的存储芯片”,但真正进入驱动开发后,必须回答几个基础问题:这颗芯片是 NOR 还是 NAND?内部存储单元如何组织?页、扇区、块之间是什么关系?容量如何换算?CPU 发出的地址如何映射到具体页和字节?只有把这些问题弄清楚,后续的擦除、写入、读取、坏块管理、文件系统移植才有稳定基础。
一、NOR 与 NAND:两种不同的存储技术路线
SPI Flash 从存储介质上主要分为 NOR Flash 和 NAND Flash。二者都基于浮栅晶体管存储电荷,但阵列结构、接口方式和适用场景差异很大。NOR Flash 的存储单元采用并行连接,支持随机访问,读取时可以直接按地址输出数据,因此常被称为“代码存储型 Flash”。NAND Flash 的存储单元采用串联结构,读取以页为单位,写入和擦除速度更快、单位容量成本更低,适合大容量数据存储。
在 SPI 接口封装下,NOR Flash 通常支持 03h 普通读、0Bh 快速读、页编程、扇区擦除、块擦除等命令;NAND Flash 则通过 SPI 控制器封装了复杂的 NAND 管理逻辑,对外仍表现为命令、地址、数据三阶段,但内部存在坏块、ECC、页缓冲等机制。对于驱动开发者来说,最直观的区别是:NOR 可以像内存一样“读”,NAND 必须按页读,且写前必须擦除。
二、存储单元结构:浮栅晶体管如何保存 0 和 1
无论是 NOR 还是 NAND,基本存储单元都是浮栅晶体管。它比普通 MOS 管多了一层浮栅,浮栅被绝缘层包围,可以长期保存电荷。当浮栅中注入电子时,晶体管的阈值电压升高,在读取电压下不导通,表示“0”;当浮栅中没有电子时,晶体管导通,表示“1”。擦除操作就是把浮栅中的电子移走,使单元回到“1”状态。因此 Flash 的物理特性是:擦除后为 0xFF,写入只能把 1 变成 0,不能把 0 变回 1。这就是“先擦后写”的根本原因。
NOR Flash 的单元并联在位线上,读取时可以通过地址直接选中某个单元,所以支持随机访问。NAND Flash 的单元串联成串,读取时必须把整页数据加载到页缓存,再串行输出。串联结构减少了接触孔和金属连线,因此 NAND 的存储密度更高、单位成本更低,但随机访问能力弱,且更容易出现坏块和位翻转。
三、页、扇区、块:SPI Flash 的层级组织
SPI Flash 内部不是按字节随意擦写的,而是按固定层级组织。以常见的 W25Q64 为例,容量为 64Mbit,即 8MB。它的组织关系如下:
- 页(Page):最小写入单位,通常为 256 字节。一次页编程命令最多写入一页,跨页写入需要拆分成多次。
- 扇区(Sector):最小擦除单位,通常为 4KB。擦除命令以扇区为粒度,不能只擦一个字节。
- 块(Block):由多个扇区组成,常见为 64KB,即 16 个 4KB 扇区。块擦除命令可一次擦除 64KB。
- 整片(Chip):整片擦除命令,擦除整个芯片,耗时最长。
地址映射方面,SPI Flash 使用 24 位地址线,可寻址 16MB。对于 8MB 的 W25Q64,地址范围是 0x000000 到 0x7FFFFF。地址计算关系为:扇区号 = 地址 / 4096,页号 = 地址 / 256,块号 = 地址 / 65536。驱动中常用宏定义完成这些换算。
#define FLASH_PAGE_SIZE 256
#define FLASH_SECTOR_SIZE 4096
#define FLASH_BLOCK_SIZE 65536
#define FLASH_TOTAL_SIZE (8 * 1024 * 1024)
#define ADDR_TO_PAGE(a) ((a) / FLASH_PAGE_SIZE)
#define ADDR_TO_SECTOR(a) ((a) / FLASH_SECTOR_SIZE)
#define ADDR_TO_BLOCK(a) ((a) / FLASH_BLOCK_SIZE)
#define PAGE_TO_ADDR(p) ((p) * FLASH_PAGE_SIZE)
#define SECTOR_TO_ADDR(s) ((s) * FLASH_SECTOR_SIZE)
四、容量与地址映射:从 24 位地址到具体单元
SPI Flash 的容量通常以 Mbit 或 MB 表示。换算关系是 1 Byte = 8 bit,1 MB = 8 Mbit。例如 W25Q64 是 64Mbit,等于 8MB;W25Q128 是 128Mbit,等于 16MB。24 位地址最大寻址 16MB,因此超过 16MB 的芯片需要 32 位地址模式或 4 字节地址命令。驱动开发中,读、写、擦除命令都需要发送 3 字节地址,高位在前。
| 层级 | 典型大小 | 作用 | 常用命令 |
|---|---|---|---|
| 页 Page | 256 Byte | 最小写入单位 | 02h Page Program |
| 扇区 Sector | 4 KB | 最小擦除单位 | 20h Sector Erase |
| 块 Block | 64 KB | 大块擦除单位 | D8h Block Erase |
| 整片 Chip | 8 MB / 16 MB | 整片擦除 | C7h Chip Erase |
五、案例与误区:为什么写入前必须擦除
假设我们要把 0x000100 开始的 300 字节数据写入 W25Q64。由于页大小是 256 字节,0x000100 位于第 1 页,页内偏移为 0。300 字节跨越了第 1 页和第 2 页,因此必须拆成两次页编程:第一次写 256 字节到 0x000100,第二次写剩余 44 字节到 0x000200。如果直接发一条 300 字节的页编程命令,芯片会在页边界回卷,覆盖第 1 页开头的数据,这是非常典型的错误。
另一个常见误区是“读改写”时忘记擦除。Flash 写入只能把 1 变成 0,如果目标区域之前写过数据,直接写入会导致新旧数据按位与,结果不可预期。正确流程是:读出整个扇区到 RAM,修改目标字节,擦除该扇区,再把整扇区写回。对于频繁修改的参数区,建议使用双备份或日志式存储,避免频繁擦除导致寿命下降。
注意:SPI Flash 的擦除次数有限,NOR 通常为 10 万次左右,NAND 更低。驱动中应避免频繁擦写同一扇区,必要时加入磨损均衡或缓存机制。
调试建议:读写前先用 9Fh 命令读取 JEDEC ID,确认芯片型号和容量;再用 05h 读状态寄存器,等待 BUSY 位清零后再发下一条命令。地址换算建议统一用宏,避免手算错误。
理解 NOR 与 NAND 的差异、浮栅存储原理、页/扇区/块层级以及容量与地址映射,是 SPI Flash 驱动开发的第一块基石。后续的擦除、写入、读取、ID 识别、状态轮询、坏块管理等操作,都建立在这些基础概念之上。只有把地址、页边界、擦除粒度三者的关系彻底理清,才能写出稳定可靠的 Flash 驱动。