深入解析NAND Flash:从物理原理到嵌入式驱动实战
1. 从存储芯片到系统基石:为什么NAND Flash值得深究
如果你从事嵌入式开发、存储系统设计,或者仅仅是好奇手机、固态硬盘(SSD)里的数据是怎么存下来的,那么“NAND Flash”这个词你一定不陌生。它早已不是实验室里的前沿科技,而是渗透到我们数字生活每一个角落的基石。从手机里的一张照片,到数据中心里PB级的数据,背后都离不开这片小小的硅片。但很多时候,我们只是把它当作一个“黑盒”——知道它能存东西,知道它比机械硬盘快,但对其内部运作、设计考量以及那些让人头疼的“坏块”、“磨损均衡”知之甚少。
我最初接触NAND Flash是在一个嵌入式项目里,需要在一块T113开发板上适配SPI NAND Flash的裸机驱动。当时对着数据手册和一堆陌生的术语(Page, Block, FTL…)一头雾水,踩了不少坑。后来发现,无论是做底层驱动、设计存储控制器,还是进行系统级优化,对NAND Flash基本概念的清晰理解,是避开那些深坑、做出合理设计决策的前提。这篇文章,我就结合自己的实践和踩坑经历,把NAND Flash那些核心的、容易混淆的概念掰开揉碎了讲清楚。我们不止看“是什么”,更要弄明白“为什么这么设计”,以及在实际项目中“需要注意什么”。
2. NAND Flash的物理结构:从晶体管到存储阵列
要理解NAND Flash的行为,必须从它的物理本质开始。这就像理解一座建筑,得先看它的砖块和结构。
2.1 核心存储单元:浮栅晶体管
NAND Flash的每一个存储比特,都依赖于一个叫做“浮栅晶体管”的器件。你可以把它想象成一个带有特殊“水桶”的开关。这个晶体管有源极(Source)、漏极(Drain)和控制栅(Control Gate),这和一个普通MOSFET类似。但关键在于,在控制栅和沟道之间,多了一个被绝缘层(通常是二氧化硅)完全包围起来的“浮栅”。这个浮栅是电学上孤立的,电荷一旦注入进去,在没有外部能量干预的情况下,可以保存很多年。
存储原理:
- 编程(写1变0):在控制栅施加一个较高的正电压(比如15V-20V),同时在漏极施加一个中等电压。这会产生一个强大的电场,使得沟道中的电子获得足够能量,穿越绝缘层(这个过程叫“F-N隧穿”),被注入到浮栅中。浮栅带了负电荷,相当于提高了这个晶体管的阈值电压。当我们用正常的读取电压去检测时,这个晶体管就不会导通,我们将其状态解读为“0”。
- 擦除(写0变1):在晶体管的源极(或衬底)施加一个高的正电压,同时控制栅接地。这个电场方向与编程时相反,它会把浮栅中的电子“吸”出来,排空浮栅的电荷。这样,晶体管的阈值电压恢复到较低水平,正常读取电压下晶体管导通,状态被解读为“1”。
- 读取:施加一个介于“已擦除”和“已编程”状态阈值电压之间的电压到控制栅。如果晶体管导通,说明浮栅无电荷(状态为1);如果不导通,说明浮栅有电荷(状态为0)。
注意:这里有一个关键且反直觉的点:在NAND Flash的世界里,擦除(Erase)才是将存储单元置为“1”的操作,而编程(Program)是将“1”变成“0”。一块刚出厂或刚被完整擦除的NAND Flash,其所有比特都是“1”。我们常说的“写入数据”,实际上是进行“编程”操作,把相应的“1”变成“0”。这个设定直接影响了很多上层逻辑的设计。
2.2 组织架构:Page, Block, Plane, Die, LUN
单个晶体管构不成大容量存储。它们被组织成一个严谨的层次结构,这个结构直接决定了NAND Flash的所有操作特性。
- 页:这是NAND Flash读取和编程(写入)的最小操作单位。你不能只改写一个字节,必须整页地写。一页通常包含几个部分:
- 主数据区:存放用户数据,大小通常是4KB, 8KB, 16KB等。
- 备用区/空闲区:通常为每512字节主数据对应16-128字节,用于存放ECC校验码、坏块标记、逻辑到物理地址映射表等元数据。在文件系统(如YAFFS2, JFFS2)或FTL中,这个区域至关重要。
- 块:这是NAND Flash擦除的最小操作单位。一个块由几十到几百个连续的页组成(例如,一个4KB页的NAND,一个块可能包含64、128或256个页)。这意味着,如果你想改写某一页里的一个字节,你不能直接覆盖它。你必须:
- 将整个块的数据(除了要改的那一页)读出来,暂存到其他地方(如RAM)。
- 擦除整个块(这是一个耗时较长的操作,通常是毫秒级,而编程是百微秒级)。
- 将修改后的数据(包含新数据和从旧块读出的其他数据)重新编程写入到该块(或另一个新块)的页中。 这就是NAND Flash最著名的特性:“异地更新”。它引出了垃圾回收、磨损均衡等一系列复杂的管理需求。
- 平面、晶圆、逻辑单元:在更高密度的NAND中,多个块会组成一个平面。多个平面可以并行操作以提升吞吐量,它们组成一个Die。一个封装内可以有一个或多个Die。逻辑单元是主机(如SSD主控)能够独立执行命令(如读、写、擦除)的最小单元,通常对应一个Die或多个能并行操作的Die的集合。
理解这个“页写块擦”的层次结构,是理解后续所有高级话题的基石。它解释了为什么NAND Flash需要专门的文件系统或FTL,也解释了随机写性能差的内在原因。
3. NAND Flash的类型演进与技术分野
NAND Flash不是一成不变的,它的技术路线一直在演进,主要围绕如何在一个存储单元里存放更多比特,以及接口如何简化。
3.1 按每个单元存储的比特数分类
这是最核心的分类方式,直接决定了密度、成本、性能和可靠性。
- SLC:每个存储单元存放1个比特(2种状态:1或0)。它的阈值电压窗口宽,状态区分容易,因此具有极高的可靠性、超长的擦写寿命(通常10万次以上)和最快的读写速度。但成本也最高,单位容量价格昂贵。主要用于对可靠性要求极端苛刻的工业、军事、企业级高端存储领域。
- MLC:每个存储单元存放2个比特(4种状态:11, 10, 00, 01)。通过精确控制浮栅电荷量来区分4个不同的电压状态。密度是SLC的两倍,成本大幅下降,但代价是寿命缩短(典型值3000-10000次)、读写速度变慢、对读写电压精度和控制要求更高。曾是消费级SSD和高端U盘的主流。
- TLC:每个存储单元存放3个比特(8种状态)。密度再提升50%,成本进一步降低,成为当前消费级SSD、大容量U盘和手机存储的绝对主力。但其寿命更短(典型值500-1500次)、速度更慢、误码率更高,需要更强大的ECC纠错和FTL算法来弥补。
- QLC:每个存储单元存放4个比特(16种状态)。追求极致的存储密度和低成本,主要用于大容量、低成本的近线存储或读取密集型应用。擦写寿命非常有限(可能低于200次),性能(尤其是写入性能)和可靠性挑战巨大,严重依赖主控和固件的优化。
选型心得:不要盲目追求新技术。对于频繁写入的日志系统、数据库重做日志、交换分区,SLC或MLC是更稳妥的选择。对于大部分桌面应用和资料存储,TLC性价比最高。QLC只适合“写入一次,读取多次”的冷数据仓库场景。我曾在一个工业数据记录仪项目中,因成本压力选了TLC颗粒,结果在高频小数据写入场景下,不到一年就出现了大量坏块,后来换用工业级MLC才稳定下来。这个教训很深刻:寿命指标一定要结合你的实际写入负载来评估。
3.2 按接口和封装分类
这决定了你如何与NAND Flash芯片通信。
- 并行接口:传统的接口,有独立的地址、数据和控制总线(如CE#, CLE, ALE, WE#, RE#, R/B#, I/Ox)。优点是接口简单直观,初期速度尚可。缺点是引脚多,PCB走线复杂,难以提升频率,已逐渐被淘汰。
- 串行接口:
- SPI NAND Flash:使用标准的SPI总线(CLK, CS#, MOSI, MISO)进行通信。引脚数极少(通常4-6根),封装小,接口简单,非常适合空间受限、对容量要求不高(几十Mb到几Gb)的嵌入式场景。驱动开发相对容易,很多MCU都有硬件SPI控制器。我在T113上适配的就是这种,需要仔细处理上电时序、命令集和坏块管理。
- ONFI / Toggle模式:这是现代大容量NAND的主流高速接口。采用源同步时钟(DQS)和差分信号,速率可达几百MT/s甚至更高。需要专用的NAND Flash控制器来处理复杂的时序协议。
- eMMC / UFS:这已经不是裸片了,而是将NAND Flash晶圆、控制器和标准接口封装在一起的一体化存储解决方案。对于手机、平板等消费电子设备,直接使用eMMC/UFS是最佳选择,主机无需关心NAND的物理特性,像访问一个块设备一样简单。但这也意味着你失去了底层的控制权和优化可能性。
- Raw NAND vs. Managed NAND:
- Raw NAND:就是裸片,你需要自己实现或集成FTL、ECC、坏块管理等所有功能。灵活性最高,挑战也最大。
- Managed NAND(如eMMC, 或带有内置控制器的SPI NAND):芯片内部集成了控制器,帮你处理了所有底层管理,对外提供简单的块设备接口。易用性最好,是大多数应用的推荐选择。
4. NAND Flash的“阿喀琉斯之踵”:固有特性与挑战
NAND Flash的物理特性带来了一系列必须面对的挑战,优秀的存储设计正是为了妥善应对这些挑战。
4.1 坏块管理:与不完美共舞
NAND Flash芯片在生产过程中和正常使用中都会产生坏块。出厂时就会标记出初始坏块,通常是在每个块的第一页或第二页的备用区(Spare Area)的特定位置写入非0xFF的标记。在使用过程中,随着擦写次数的增加,也会产生新的坏块。
BBM策略:
- 坏块发现:在擦除、编程操作后,通过状态寄存器或读取操作验证来检测失败。一旦一个块在擦除或编程时失败多次(根据芯片规格),就应将其标记为坏块。
- 坏块标记:在坏块的特定位置(通常与出厂标记位置相同)写入坏块标记。注意:这个标记不能通过常规擦除操作清除,防止标记丢失。
- 地址重映射:这是FTL的核心功能之一。系统维护一个“逻辑块地址”到“物理块地址”的映射表。当主机要访问一个逻辑块时,如果它映射的物理块是坏的,FTL会自动将其重映射到一个预留的“备用好块”上。这个过程对主机完全透明。
实操避坑:在编写裸机驱动或简单FTL时,上电初始化时必须扫描整个芯片,建立坏块表。忽略这一步,可能会导致数据写入坏块而丢失。对于SPI NAND,坏块标记的位置可能因厂商而异,务必仔细查阅数据手册。
4.2 纠错码:数据的守护神
随着MLC/TLC/QLC的普及,NAND Flash的原始误码率越来越高。一个页在读取时,可能个别比特会出错(称为“位翻转”)。ECC就是用来检测和纠正这些错误的。
- 必要性:没有ECC,数据完整性根本无法保证。SLC可能只需要简单的汉明码,而TLC/QLC必须使用强大的BCH码或LDPC码。
- 工作流程:
- 写入时:控制器根据要写入一页主数据区的内容,计算出一组ECC校验码,将其写入该页对应的备用区。
- 读取时:控制器读出主数据区和ECC校验码,重新计算ECC。将新计算的ECC与存储的ECC比较,如果不同,则说明数据有误,并尝试根据算法纠正错误比特。如果错误比特数超过ECC的纠错能力,则报告不可纠正错误。
- 强度选择:ECC纠错能力越强,所需的校验位越多(占用更多备用区),计算开销也越大。需要在可靠性、空间开销和性能之间权衡。现代高端SSD主控的LDPC引擎是非常复杂的。
4.3 磨损均衡:让每个块“雨露均沾”
由于NAND Flash每个块的擦写次数有限,如果频繁更新同一逻辑地址的数据(比如文件系统的FAT表或日志区),会导致对应的物理块很快磨损报废,而其他块却几乎全新。WL算法的目标就是将写操作均匀地分布到所有物理块上,延长整体设备寿命。
常见策略:
- 动态磨损均衡:在每次写入新数据时,优先选择当前擦写计数最小的块。这需要记录每个块的擦写次数。
- 静态磨损均衡:更进一步,当检测到某些块数据长期不更新(静态数据)时,主动将这些数据迁移到擦写次数较多的“年轻”块上,而把“年老”的块腾出来接收新的写入。这能更彻底地均衡磨损,但会增加额外的写操作(写放大)。
经验之谈:在资源受限的嵌入式系统中,实现一个完整的动态WL可能都很有挑战。一个简化但有效的做法是:采用“循环写入”策略。例如,将存储空间视为一个环形的日志,总是写入下一个可用的块,写满一圈后覆盖最老的块。这虽然不是最优的WL,但能避免热点问题。在T113的SPI NAND驱动中,我结合坏块表,实现了一个简单的基于块队列的循环分配器,在实践中效果不错。
4.4 垃圾回收:为“新写”腾出空间
由于NAND Flash的“异地更新”特性,当一份数据被更新后,旧版本的数据所在的页就变成了“无效数据”。这些无效数据占据着空间,但它们所在的块不能直接用于写入新数据,因为块内可能还混合着有效数据。GC的任务就是回收这些包含无效数据的块,将其擦除变为可用状态。
GC过程:
- 选择候选块:FTL会选择一个“脏页”(无效数据)比例较高的块作为回收候选。
- 数据搬迁:读取该块中所有仍然有效的页,将它们写入到其他空闲块中。
- 擦除块:搬迁完成后,这个候选块中的所有数据都无效了,此时可以安全地擦除整个块,将其加入空闲块池。
写放大:GC是写放大的主要来源。例如,为了写入4KB新数据,GC可能需要搬迁16KB有效数据,然后擦除一个块,最后写入20KB数据(4KB新+16KB旧)。写放大因子达到了5。WAF直接影响SSD的实际寿命和性能。优秀的FTL算法会尽量减少GC的触发频率和搬迁数据量。
5. 闪存转换层:连接物理与逻辑的桥梁
FTL是管理Raw NAND所有复杂性的软件/固件层。它向上层(文件系统或操作系统)呈现一个简单的、可随机寻址的块设备接口,向下管理NAND的所有物理特性。
5.1 FTL的核心功能
- 地址映射:这是FTL最核心的功能。将主机发出的逻辑扇区地址转换为NAND的物理地址(Die, Plane, Block, Page)。映射关系需要持久化地存储在NAND上(通常放在备用区或特定块中),并在上电时加载到RAM。
- 坏块管理:如前所述,实现坏块的发现、标记和透明重映射。
- 磨损均衡:记录块擦写次数,并引导写入流量,均衡磨损。
- 垃圾回收:在后台回收无效数据占用的空间。
- ECC管理:在写入时生成校验码,在读取时进行校验和纠错。
- 数据缓冲与调度:合并小的随机写请求,优化写入模式,提升性能。
5.2 映射粒度与策略
映射策略是FTL设计的精髓,直接影响性能、内存开销和复杂度。
- 块级映射:一个逻辑块映射到一个物理块。映射表小(块数量远少于页数量),内存占用少。但任何一个小页的更新,都需要搬移整个逻辑块的所有有效页到新块,写放大极大,性能差。现已很少使用。
- 页级映射:一个逻辑页映射到一个物理页。非常灵活,写放大最小,性能好。但映射表巨大(页数量非常多),需要大量RAM来缓存映射表,且映射表本身的管理开销大。常用于高性能SSD主控。
- 混合映射:折中方案。最主流的是FTL。它将逻辑空间划分为多个固定大小的“区域”。在区域内部使用页级映射,区域之间使用块级映射。同时,它引入一个“日志块”来吸收小的随机写:随机写先进入日志块(页级映射),当日志块写满后,再与对应的数据块合并,转换成顺序写。这是一种在资源有限情况下取得良好平衡的策略,被很多嵌入式FTL(如U-Boot的DFU, Linux的UBI)所采用。
在嵌入式场景下的选择:对于SPI NAND这类资源受限的环境,实现一个完整的页级映射FTL通常不现实。更常见的做法是:
- 使用现成的、经过优化的中间件,如LittleFS、SPIFFS等,它们内部实现了适合Flash的FTL逻辑。
- 如果必须自己管理,采用基于FTL思想的简化版:将NAND划分为几个大的“段”,在段内使用顺序追加写,并定期进行段合并。这需要精心设计元数据存储格式和恢复机制。
6. 文件系统与NAND Flash的适配
通用文件系统(如FAT32, ext4)是为磁盘设计的,假设存储介质可以原地覆盖写入,没有擦除寿命限制。直接用在Raw NAND上会导致灾难性后果(频繁擦写固定位置元数据,快速损坏设备)。因此,需要专为Flash设计的文件系统。
- JFFS2 / YAFFS2:早期Linux上流行的Flash文件系统。它们在文件系统层实现了日志结构、磨损均衡和垃圾回收。但挂载时需要扫描整个介质建立索引,耗时很长,且内存占用与介质大小相关,不适合大容量存储。
- UBI + UBIFS:在Linux上更现代的方案。UBI是一个位于MTD设备之上的卷管理层,负责坏块管理、磨损均衡和逻辑擦除块分配。UBIFS则是建立在UBI之上的一个功能完整的日志文件系统。它解决了JFFS2挂载慢的问题,性能更好,是嵌入式Linux大容量NAND存储的推荐选择。
- LittleFS / SPIFFS:为资源极度受限的微控制器设计的轻量级文件系统。它们设计精巧,抗掉电能力强,内存占用小。LittleFS采用写时复制和日志结构,SPIFFS则类似一个大型的键值存储。对于单片机上的SPI NAND,它们是绝佳选择。
- FTL + 传统文件系统:这是SSD和eMMC/UFS的方式。FTL在硬件或固件层完成了所有Flash管理,向上提供一个完美的“块设备”。在这个块设备上,你可以毫无顾忌地使用FAT32、exFAT、NTFS、ext4等任何传统文件系统。这是对主机最友好的方式。
选型建议:
- 无OS的MCU + SPI NAND:优先考虑LittleFS。它比SPIFFS更健壮,目录支持更好。
- 嵌入式Linux + 大容量Raw NAND:使用UBI/UBIFS组合。
- 嵌入式Linux + eMMC:直接使用ext4或F2FS。F2FS是三星为Flash设计的文件系统,对SSD/eMMC有额外优化。
- 需要与PC交换数据:在FTL提供的块设备上使用exFAT或FAT32。
7. 实战:以SPI NAND裸机驱动为例的深度解析
最后,我们结合“SPI NAND裸机驱动”和“T113适配”这两个热点,把理论落地。假设我们要在Allwinner T113这颗ARM Cortex-A7芯片上,为一块Winbond的SPI NAND(如W25N01GV)编写驱动。
7.1 驱动框架设计
一个完整的SPI NAND驱动至少需要实现以下层次:
- SPI控制器底层驱动:初始化T113的SPI控制器,配置时钟、模式(模式0或模式3)、位宽、速率等。确保能进行基本的读、写字节操作。
- SPI NAND命令层:封装SPI NAND的专用命令。这些命令不是简单的SPI传输,通常有固定的格式:先写命令码(Opcode),可能跟地址,再读写数据。关键命令包括:
READ_ID:读取芯片的制造商ID和设备ID,用于识别芯片。READ_STATUS_REG:读取状态寄存器,获取忙状态、写使能状态、ECC状态等。WRITE_ENABLE/WRITE_DISABLE:在进行编程或擦除前,必须使能写操作。PAGE_DATA_READ/READ_DATA:启动页读取并读取数据。PROGRAM_LOAD/PROGRAM_EXECUTE:加载数据到内部缓存并执行编程。BLOCK_ERASE:擦除一个块。
- 基础操作函数:
nand_init():初始化SPI,读取ID,检查芯片,读取坏块表。nand_read_page():读取一页数据(主数据+备用区)。需要处理ECC校验。nand_write_page():写入一页数据。需要先擦除所在块(或确保块是空的),然后编程,并验证。nand_erase_block():擦除一个块。擦除前必须确保块内所有页都是无效的或已备份。
- 坏块管理与ECC层:
- 在
init时,遍历所有块,读取备用区中的坏块标记,建立内存中的坏块映射表。 - 在
read_page时,读取备用区中的ECC校验码,对主数据进行校验和纠错。 - 在
write_page时,计算主数据的ECC校验码,将其写入备用区。
- 在
- FTL/管理层(可选但推荐):基于上述基础函数,实现一个简单的逻辑到物理地址映射、磨损均衡和垃圾回收。即使是一个简单的“顺序写+循环回收”策略,也能极大地改善易用性和寿命。
7.2 T113适配中的关键点与坑
- 时钟与时序:T113的SPI时钟可能较高,需要根据SPI NAND芯片数据手册的最高频率来设置。过高的频率会导致通信失败。务必在初始化后通过
READ_ID命令验证通信是否正常。 - GPIO模拟与硬件SPI:T113有硬件SPI控制器,通常应优先使用。配置时注意引脚复用功能的选择。如果使用GPIO模拟SPI,要特别注意延时,确保满足芯片的时序要求(如
tCH,tCL,tCS等)。 - 忙状态等待:编程和擦除是耗时操作(毫秒级)。发送命令后,必须循环读取状态寄存器,等待
BUSY位清零,绝对不能使用死等延时,会严重影响系统实时性。可以使用超时机制,避免芯片故障导致驱动卡死。 - 坏块标记位置:不同厂商、不同容量的SPI NAND,坏块标记在备用区的位置可能不同。W25N系列通常在第一页的备用区第一个字节。必须依据你所使用芯片的具体数据手册。
- ECC的硬件支持:一些较新的SPI NAND芯片(如某些GD系列)内部集成了ECC引擎,可以通过状态寄存器读取ECC状态。而像W25N01GV这类老芯片,可能需要驱动软件实现BCH纠错,或者依赖主控的硬件ECC(如果T113的SPI控制器支持)。这是一个巨大的开发量差异。
- 上电复位与稳定时间:SPI NAND上电后需要一小段稳定时间才能接受命令。驱动中应在初始化开始时添加一个几毫秒的延时。
- 四线模式:一些SPI NAND支持四线(Quad I/O)模式,可以大幅提升读取速度。但这需要SPI控制器和驱动都支持。在T113上,需要确认硬件SPI是否支持Quad模式,并正确配置。
编写这个驱动的过程,实际上就是把前面所有理论串联起来实践一遍。从识别芯片、正确读写一页数据、处理坏块、到最终构建一个可靠的可读写存储层,每一步都需要对NAND Flash的物理特性有清晰的认识。当你成功驱动起来,并能在上面稳定运行一个文件系统时,你对NAND Flash的理解就不再停留在纸面了。