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. 一张图理清层级

下面我把页、扇区、块的关系画成一张结构图。你可以看到数据是如何从芯片一层层拆到最小的存储单元。

QSPI Flash 存储层级结构 Flash 芯片 (Chip) Block 0 (64 KB) Sector 0 (4 KB) 共 16 个扇区/Block Page 0 (256 B) 共 16 页/Sector 字节 (Byte) × 256 / Page 说明 写最小单位:页 擦除最小单位:扇区 跨边界操作 会导致数据错误 擦除时间:扇区 ~45ms | 块 ~200ms | 写页 ~0.5ms

6. 实操中的避坑指南

讲完原理,分享几条我这些年总结的实战经验:

  1. 写之前一定要检查擦除状态。我曾经在批量产测时省掉了这一步,结果因为上一轮擦除还没完成,直接写数据,导致整批产品数据校验失败。后来加了轮询 BUSY 标志,问题消失。
  2. 不要用块擦除代替扇区擦除。有的工程师嫌扇区擦除太慢,直接擦除 64 KB 块。但如果你的数据只改了一个扇区,块擦除会把整个块清空,其他扇区的数据就丢了。
  3. 页写地址必须对齐页起始。比如页大小 256 字节,地址 0x100 是对齐的,0x110 就是不对齐。很多 Flash 会报错或者忽略多余字节,我建议在驱动中用 addr & (PAGE_SIZE - 1) 检查。
  4. 如果跨页写怎么办? 没错,没有那个命令支持跨页写。你要自己拆分成两个页写操作:先写第一页的剩余部分,再写第二页的开头部分。

最后说一句:所有的页、扇区、块地址计算,都基于一个公式:
扇区号 = 地址 / 扇区大小
页内偏移 = 地址 % 页大小
把这些写在驱动里,比手动算快得多,也不容易犯错。