QSPI Flash 存储原理:页、扇区、块结构
做 QSPI Flash 开发,第一件事就是把它的“地盘”搞清楚。说白了,Flash 内部的存储单元不是一盘散沙,而是按层级组织的。每一层对应不同的操作粒度。如果搞混了,轻则写不进去,重则把整片数据洗掉。我刚开始踩过这个坑,后来才明白为什么手册里要把页、扇区、块写得那么清楚。
1. 存储层级概览
一个典型的 QSPI Flash (NOR 型),内部结构大致如下:
- 字节 (Byte) —— 最小的可寻址单元,8 bit。
- 页 (Page) —— 最小的 写(编程)单位。常见大小:256 字节、512 字节。
- 扇区 (Sector) —— 最小的 擦除单位。常见大小:4 KB。
- 块 (Block) —— 更大的擦除单位,常由多个扇区组成。常见大小:32 KB、64 KB。
- 芯片 (Chip) —— 整个 Flash 芯片,可能有多个 Bank。
我的习惯:设计驱动时,先把页大小、扇区大小、擦除块大小用宏定义写好。这样后面所有地址计算都基于宏,不怕改型号。
2. 页、扇区、块的关系与差异
这里最容易混淆的就是“擦除”和“写”的粒度。你看,NOR Flash 有个物理特性:只能把 1 写成 0,不能把 0 写成 1。想要恢复成 1,就必须擦除。擦除的最小单位比写大得多。所以“先擦除再写”是铁律。
| 层级 | 常见大小 | 操作类型 | 特点 |
|---|---|---|---|
| 页 (Page) | 256 B / 512 B | 写(编程) | 一次写一整页,不能跨页写 |
| 扇区 (Sector) | 4 KB | 擦除 | 擦除后全为 1,写操作前必须扇区已擦除 |
| 块 (Block) | 32 KB / 64 KB | 擦除 | 通常保护边界更粗,擦除时间更长 |
| 芯片 (Chip) | 按型号 | 全片擦除 | 擦除整片,一般只在初始化时用 |
我曾经踩过的坑:对着 4 KB 扇区只写了一个字节,就以为其他字节保持原来的数据。结果读回来全是乱码。为什么?因为写页之前必须擦除扇区,擦除后扇区全为 1,而我只写了那一页,其他页还是全 1。如果你在写前没擦除,那旧数据里的 0 根本写不进去。
3. 页写操作:不能偷懒
QSPI Flash 内部有一个 页缓存 (Page Buffer),大小等于页大小。你发送写命令时,数据先塞进这个缓存,然后 Flash 自己把缓存内容写入存储阵列。这个过程叫“编程”。注意:
- 如果发送的数据不足一页,剩下的字节会被自动填成 0xFF。
- 如果你往同一个页连续发两次写命令,第二次会把第一次覆盖掉,因为页缓存只保留最后一次。
- 禁止跨页写:比如页大小 256 字节,你想从地址 0x00FF 开始写两个字节,第二字节会跑到下一页的地址 0x0100。看起来没问题?但很多 Flash 会直接忽略跨页部分,或者产生不可预料的结果。我建议每次写之前检查地址是否对齐到页边界。
// 伪代码:写入一页数据
uint8_t page_data[256];
uint32_t addr = 0x000000; // 页起始地址,必须对齐到页大小
// 1. 写使能 (WREN)
qspi_send_cmd(CMD_WREN);
// 2. 发送页编程命令 (PP) + 地址 + 数据
qspi_start(CMD_PP);
qspi_send_addr(addr);
qspi_send_bytes(page_data, 256);
qspi_stop();
// 3. 等待忙状态
while (qspi_read_status1() & BIT_BUSY) {
// 等待
}
4. 擦除操作:扇区 vs 块
一般来说,扇区擦除 (Sector Erase, 4 KB) 是最常用的擦除粒度。块擦除 (Block Erase) 只在大批量更新时用,因为擦除时间更长。例如:
- 扇区擦除:典型 45 ms
- 块擦除(64 KB):典型 200 ms
- 全片擦除:几秒到十几秒
嗯,这里要注意:不同型号的 Flash,最小擦除单位可能不同。有的芯片只有扇区擦除没有块擦除,或者反过来。务必看数据手册的“Memory Organization”章节。
5. 一张图理清层级
下面我把页、扇区、块的关系画成一张结构图。你可以看到数据是如何从芯片一层层拆到最小的存储单元。
6. 实操中的避坑指南
讲完原理,分享几条我这些年总结的实战经验:
- 写之前一定要检查擦除状态。我曾经在批量产测时省掉了这一步,结果因为上一轮擦除还没完成,直接写数据,导致整批产品数据校验失败。后来加了轮询 BUSY 标志,问题消失。
- 不要用块擦除代替扇区擦除。有的工程师嫌扇区擦除太慢,直接擦除 64 KB 块。但如果你的数据只改了一个扇区,块擦除会把整个块清空,其他扇区的数据就丢了。
- 页写地址必须对齐页起始。比如页大小 256 字节,地址 0x100 是对齐的,0x110 就是不对齐。很多 Flash 会报错或者忽略多余字节,我建议在驱动中用
addr & (PAGE_SIZE - 1)检查。 - 如果跨页写怎么办? 没错,没有那个命令支持跨页写。你要自己拆分成两个页写操作:先写第一页的剩余部分,再写第二页的开头部分。
最后说一句:所有的页、扇区、块地址计算,都基于一个公式:
扇区号 = 地址 / 扇区大小
页内偏移 = 地址 % 页大小
把这些写在驱动里,比手动算快得多,也不容易犯错。