4. QSPI Flash 存储器结构:页、扇区、块的划分

做嵌入式这些年,我见过不少新手栽在 Flash 存储结构上。他们以为 Flash 就是个“大数组”,想写哪就写哪。结果呢?要么写错了地址,要么擦除把数据全弄丢了。你想想看,一个 NOR Flash 芯片动辄几十 MB,内部要是没个层次,管起来非得疯掉不可。

今天就聊聊 QSPI Flash 的内部架构。说白了,它就像一座仓库——最小的单元是“页”,然后“扇区”,再往上“块”,最后是整个芯片。每一层都有自己的一组规则。记住这些规则,你写驱动、做启动时就能少踩很多坑。

4.1 基础单元:页 (Page)

页是 Flash 编程(写入)的最小单位。你可以往一页里写数据,但不能只写一个字节——想写必须写一整页,或者页的一部分。常见的 QSPI Flash 页大小是 256 字节或 512 字节。

  • 页写入时序:发送页编程命令(0x02),随后 3 字节地址、数据字节流。芯片内部会把数据锁存到页缓冲区,然后自动烧写。
  • 页边界限制:一次编程不能跨页。比如你从地址 0x00FF 开始写 2 字节,第三字节就落到下一页。很多驱动没处理好这问题,结果数据错位。我早些年在 STM32H7 上做 QSPI 驱动时就掉过这个坑。
我个人习惯:页写入前先检查剩余长度,若超过页边界则分多次发送。用 page_size - (addr % page_size) 算当前页剩余容量,特别实用。

4.2 擦除最小单元:扇区 (Sector)

页只能写,不能直接覆盖。想更新一页里的几个字节?得先把整个扇区擦除,再写回去。为什么这样?因为 Flash 物理特性决定了“写 1 变 0 容易,写 0 变 1 只能靠擦除”。

扇区(Sector)是擦除的最小单位。常见大小有 4 KB(小扇区)和 64 KB(大扇区)。拿 Winbond W25Q64 来说,它就有 4 KB 小扇区,刚好适配文件系统(如 FATFS)的簇大小,做日志存储特别方便。

扇区类型典型大小常见芯片用途
小扇区4 KBW25Q64, MX25L6435文件系统、配置参数
大扇区64 KBN25Q512, S25FL128大块固件、启动镜像

擦除一个扇区大概需要几十到几百毫秒。使用时还得注意:擦除前必须确保该扇区没有正在执行的操作,否则状态寄存器会一直显示“忙碌”。

曾经有一次,我在调试 Bootloader 时连续擦除两个扇区,没等第一个擦完就发了第二个命令。结果 Flash 状态机乱掉,整个芯片锁死。后来只好复位重新擦除。教训就是:每次擦除后都得轮询状态寄存器的 BUSY 位。

4.3 更大粒度:块 (Block)

把多个扇区组合,就成了块(Block)。块大小通常是 32 KB(8 个小扇区)或 64 KB(16 个小扇区)。很多 Flash 提供“块擦除”命令,能一次擦除整块,比逐个扇区擦快得多。

  • 块擦除指令:命令码 0xD8(64KB 块)或 0x52(32KB 块)。
  • 芯片擦除:命令 0xC7,擦除整个 Flash。耗时几秒,慎用。

嗯,这里要注意:不同厂商、不同型号的 Flash,块定义可能不一样。比如 Cypress S25FL系列里还有“参数块”和“主块”之分。拿到芯片后第一件事就是读 datasheet 里的 memory map 表,别想当然。

4.4 整芯片结构层次

把上面三层串起来,就是 Flash 的完整分级:

芯片 (Chip)
  └─ 块 (Block) 64KB
       └─ 扇区 (Sector) 4KB  (16个扇区组成一个块)
            └─ 页 (Page) 256B  (16页组成一个扇区)
                 └─ 字节 (Byte)

还是拿 W25Q64 举例:总容量 8 MB,内有 128 个 64KB 的块,每个块又分 16 个 4KB 扇区,每个扇区 16 页。这样你算算:8 MB = 128 × 64 KB = 2048 × 4 KB = 32768 × 256 B。

4.5 划分背后的设计思想

为什么不直接用“字节擦除”呢?因为在 Flash 内部,擦除操作需要给整个字线(word line)加高电压,颗粒度越小,控制逻辑越复杂,芯片成本越高。折中下来,业界选了 4 KB 扇区作为最小擦除单元,既照顾了文件系统需求,又控制了 die 面积。

我记得做过一个产品,Bootloader 和应用程序分别放不同块,参数区用一个小扇区。升级时只需擦除应用程序所在块,Bootloader 不动,很安全。

核心原则:
  • 写 → 以页为单位(页编程)
  • 擦除 → 以扇区或块为单位(扇区/块擦除)
  • 读 → 任意字节(无限制)
这条一定要刻在脑子里,写驱动时每步都要问自己:现在发的是什么命令?目标地址对齐了吗?
QSPI Flash 存储层次结构 芯片 (Chip) — 例如 8 MB (W25Q64) 块 0 (64KB) 块 1 (64KB) 块 127 (64KB) 扇区 0 (4KB) 扇区 1 (4KB) ... ... 扇区 15 (4KB) 一个 64KB 块 = 16 个 4KB 扇区 页0 页1 ... ... 页15 一个扇区 = 16 页 (每页256B) 写粒度: 页 (Page) | 擦除粒度: 扇区/块 (Sector/Block) | 读粒度: 任意字节

4.6 实际应用中的选择策略

做启动代码或存储方案时,怎么分配这些层级?我一般这么干:

  1. Bootloader:放在最前面 1~2 个块,用 64KB 块擦除,反正不常更新。
  2. 应用程序:按功能分区,每个模块放一个或几个连续块。
  3. 参数 / 配置数据:用 4KB 小扇区,每次更新擦除一个扇区。如果参数很小(比如几十字节),我会在驱动里做成“备份扇区”轮流使用,避免频繁擦写同一个扇区导致磨损——Flash 的擦写寿命大概 10 万次,别忽视。
  4. 日志 / 数据记录:用一整个块,按页追加写。写满后擦除整个块重新开始,效率最高。
我常用一个技巧:在启动时读取芯片 ID 和容量寄存器,自动算出页大小、扇区大小、块大小。这样代码能兼容不同型号的 Flash,换芯片只需改头文件里的几个宏。别硬编码 64KB 块大小,万一遇到 32KB 块的芯片就翻车了。

总之,了解页、扇区、块的划分,不光是为了看懂 datasheet,更是为了写出高效、可靠的 Flash 驱动。下一级一级的擦写关系理顺了,你就能在项目里灵活地规划存储布局。嗯,这块内容就先到这里,后面用到具体操作时会反复提到这些基本概念。