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,然后选Plane,接着定位到Block,最后找到Page内部的某个字节。
3.8 实际项目中的注意事项
嗯,最后聊几个我在项目中踩过的坑:
- 坏块管理:NAND Flash出厂就有坏块,而且使用过程中还会产生新坏块。我建议你在驱动层维护一个坏块表,每次擦除前先检查一下。
- 读写干扰:频繁读写同一个Block,会导致相邻Block的数据出错。解决办法是使用磨损均衡算法,把数据分散到不同的Block。
- 数据保持:NAND Flash的数据保持时间有限,尤其是TLC和QLC。我一般会在产品中加一个定期刷新的机制,比如每3个月重写一次数据。
- 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(闪存转换层)和磨损均衡的时候,你就不会觉得吃力了。