3. NAND Flash阵列组织:Page、Block、Plane、Die、Chip层级关系与寻址方式

好,咱们今天聊聊NAND Flash的物理结构。说白了,你手里拿的那颗芯片,里面可不是一锅粥,而是有非常清晰的层级关系。我刚开始接触这个的时候,也被Page、Block、Die这些词搞得晕头转向。后来自己画了一张图,才彻底搞明白。

你想想看,一颗NAND Flash芯片,从大到小,可以拆成:Chip → Die → Plane → Block → Page。每一层都有自己的职责,寻址的时候也是一层一层往下找。

3.1 最小操作单元:Page(页)

Page是读写操作的最小单位。什么意思?就是你哪怕只想改一个字节,也得把整个Page读出来,改完再写回去。嗯,这里要注意,Page不能单独擦除

常见的Page大小有:

  • MLC(2-bit/cell):通常 16KB + 1KB(备用区)
  • TLC(3-bit/cell):通常 16KB + 2KB(备用区)
  • QLC(4-bit/cell):可能更大,比如 32KB + 4KB

关键点:Page的备用区(Spare Area / OOB)是用来存ECC校验码、坏块标记、逻辑地址映射表的。我见过不少新手把备用区当普通存储用,结果ECC校验位没地方放,数据读出来全是错的。

3.2 擦除操作单元:Block(块)

Block是擦除操作的最小单位。一个Block包含多个Page。为什么擦除不能按Page来?这是NAND的物理特性决定的——浮栅晶体管的电荷只能整块释放。

常见的Block大小:

  • MLC:256个Page/Block → 约 4MB
  • TLC:384个Page/Block → 约 6MB
  • QLC:512个Page/Block → 约 16MB

避坑指南:我曾经在一个项目里,因为没注意Block大小,写了一个循环擦除程序。结果擦除到一半,发现Block大小是4MB,而我分配的缓冲区只有2MB。嗯,后果就是数据被截断了,整个分区都挂了。从那以后,我每次写擦除代码前,都会先读一下芯片的ID,确认Block大小。

3.3 并行操作单元:Plane(平面)

Plane是并行操作的基本单位。一个Die内部通常包含2个或4个Plane。每个Plane有自己的缓存寄存器(Cache Register)和数据寄存器(Data Register)。

为什么要有Plane?说白了就是为了提速。你可以同时向两个Plane发命令,比如同时读两个Page,或者同时写两个Page。这样带宽直接翻倍。

我个人的习惯是,在做高性能存储方案时,优先考虑多Plane的芯片。比如一个Die有4个Plane,你就能同时操作4个Page,吞吐量非常可观。

3.4 独立执行单元:Die(裸片)

Die是芯片内部的一个独立单元。每个Die有自己的控制逻辑、寄存器阵列和ECC引擎。多个Die可以封装在同一颗Chip里,通过片选信号(CE#)来区分。

常见的配置:

  • 单Die封装:比如一颗芯片里只有1个Die
  • 多Die堆叠:比如4个Die堆叠在一起,共用一组IO引脚

我记得有一次调试一个SSD主控,发现读写速度上不去。查了半天,原来是主控只支持单Die操作,而芯片是4 Die堆叠的。后来改成了多Die交错操作,速度直接翻了3倍多。

3.5 物理封装单元:Chip(芯片)

Chip就是你焊在PCB上的那颗黑色芯片。它可能包含1个或多个Die。Chip通过一组IO引脚(DQ0-DQ7)、控制引脚(CE#, CLE, ALE, WE#, RE#等)与主控通信。

寻址的时候,主控通过发送命令序列来指定要操作的Chip、Die、Plane、Block和Page。典型的命令序列如下:

// 以读取一个Page为例(假设是ONFI 4.0协议)
// 1. 发送读命令 0x00
// 2. 发送5个地址周期(Row Address + Column Address)
//    - 第1周期:Column Address (低8位)
//    - 第2周期:Column Address (高8位)
//    - 第3周期:Row Address (低8位) → 包含Page号
//    - 第4周期:Row Address (中8位) → 包含Block号
//    - 第5周期:Row Address (高8位) → 包含Die/Plane选择
// 3. 发送读确认命令 0x30
// 4. 等待tR(读操作时间)
// 5. 读取数据

// 伪代码示例
void nand_read_page(uint32_t row_addr, uint16_t col_addr) {
    nand_send_cmd(0x00);           // 读命令
    nand_send_addr(col_addr & 0xFF); // 列地址低8位
    nand_send_addr((col_addr >> 8) & 0xFF); // 列地址高8位
    nand_send_addr(row_addr & 0xFF); // 行地址低8位
    nand_send_addr((row_addr >> 8) & 0xFF); // 行地址中8位
    nand_send_addr((row_addr >> 16) & 0xFF); // 行地址高8位
    nand_send_cmd(0x30);           // 读确认
    nand_wait_ready();             // 等待tR
    nand_read_data(buffer, page_size); // 读取数据
}

3.6 寻址方式详解

NAND Flash的寻址分为行地址列地址两部分:

  • 行地址(Row Address):用来定位Block和Page。通常占用3个字节(24位),可以寻址到Die内部的任意一个Page。
  • 列地址(Column Address):用来定位Page内部的字节偏移。通常占用2个字节(16位),可以寻址到Page内部的任意一个字节。

举个例子,假设一个芯片的配置如下:

  • 每个Page:16KB(16384字节)
  • 每个Block:256个Page
  • 每个Die:2048个Block
  • 每个Chip:4个Die

那么寻址一个字节需要:

  • 列地址:14位(16384 = 2^14)
  • 行地址:18位(256 * 2048 = 524288 = 2^19,但Die选择需要额外2位)

总共需要32位地址。但实际传输时,列地址用2个字节(16位),行地址用3个字节(24位),总共5个字节。

小技巧:我建议你在设计驱动时,把地址计算封装成宏或内联函数。比如:

#define PAGE_OFFSET(page) ((page) & 0xFF)  // Page号在行地址的低8位
#define BLOCK_OFFSET(block) (((block) & 0xFF) << 8)  // Block号在行地址的中8位
#define DIE_OFFSET(die) (((die) & 0x03) << 16)  // Die选择在高2位

这样代码可读性高,也不容易出错。

3.7 层级关系总结

为了让你更直观地理解,我画了一张层级关系图:

Chip(芯片) Die 0 Die 1 Plane 0 Plane 1 Plane 0 Plane 1 Block 0 Block 1 Block ... Page 结构 数据区(Data Area) 备用区(Spare Area / OOB) ECC校验位 Chip Die Plane Block Page

从这张图你可以看到,整个层级关系就像俄罗斯套娃。寻址的时候,主控先选Chip,再选Die,然后选Plane,接着定位到Block,最后找到Page内部的某个字节。

3.8 实际项目中的注意事项

嗯,最后聊几个我在项目中踩过的坑:

  1. 坏块管理:NAND Flash出厂就有坏块,而且使用过程中还会产生新坏块。我建议你在驱动层维护一个坏块表,每次擦除前先检查一下。
  2. 读写干扰:频繁读写同一个Block,会导致相邻Block的数据出错。解决办法是使用磨损均衡算法,把数据分散到不同的Block。
  3. 数据保持:NAND Flash的数据保持时间有限,尤其是TLC和QLC。我一般会在产品中加一个定期刷新的机制,比如每3个月重写一次数据。
  4. ECC强度:不同层级的NAND Flash需要的ECC强度不同。MLC一般需要4-bit/512字节,TLC需要8-bit/512字节,QLC可能需要16-bit/512字节甚至更高。

总结一下:理解NAND Flash的层级关系,是写好驱动和文件系统的基础。你只要记住:Page是读写单位,Block是擦除单位,Plane是并行单位,Die是独立单位,Chip是封装单位。寻址的时候,行地址找Block和Page,列地址找Page内的字节。把这些搞清楚了,后面讲FTL(闪存转换层)和磨损均衡的时候,你就不会觉得吃力了。

专注资料整理