做嵌入式这几年,NAND Flash算是我又爱又恨的器件。前年做一台工业采集设备的存储模块,日志数据写在一颗512MB的SLC NAND上,设备跑了大概两个月,客户反馈偶发卡死、日志凭空消失。查到最后,问题出在一个出厂时完全正常的块上——它在一个高P/E循环之后悄悄变成了坏块,而我们的驱动只做了启动时扫描,没有做运行时动态坏块管理。那次排查让我从物理结构到应用层把NAND Flash存储原理和坏块管理整个重新过了一遍,很多之前没想透的地方,全都串起来了。
这篇文章就是那轮排查后的完整总结。不打算讲太多纸上谈兵的概念,重点放在:NAND Flash内部的存储细胞怎么工作、读写擦除为什么有那么多限制、坏块到底怎么产生、怎么标记、驱动层怎么管理,以及我在真实项目里踩过哪些坑。不管你是刚接触NAND的工程师,还是已经在用文件系统但被坏块问题困扰过的人,这篇应该都能帮你省下不少排查时间。
1. 从一颗存储芯片看NAND Flash的物理结构
1.1 存储细胞长什么样:浮栅晶体管与电荷俘获
NAND Flash的最小存储单元,本质上是一个特殊的MOSFET晶体管。普通晶体管只有控制栅,而NAND的存储管在控制栅和沟道之间多了一层浮栅(Floating Gate),这层浮栅被氧化物完全包裹,像一个“蓄电瓶”悬浮在沟道上方。
写入数据时,控制栅加上十几伏的高压,电子会穿过氧化层“隧道”进入浮栅;撤掉电压后,氧化层把电子困在里面,电荷就留住了。因为有电荷,这个晶体管的阈值电压会升高,读数据时通过给控制栅加一个中间电压,,测这个cell通还是不通,就能判断里面存的是0还是1。
现在的3D NAND很多改用了Charge Trap结构,把浮栅换成一整圈氮化硅电荷俘获层。原理类似,但对漏电更耐受,更适合堆叠层数。但不管哪种结构,都绕不开一个本质:电子存储是物理现象,氧化层在反复“充电、放电”中会被磨损,这直接决定了NAND的寿命上限。
1.2 页、块、平面的组织方式
NAND不是一个个cell独立使用的,内部组织方式非常关键。所有cell按word line和bit line排列,共享周边电路。读和写的最小单位是页(Page),擦除的最小单位是块(Block),一个块里通常包含几十到上百个页。
举个例子,常见的256MB SLC NAND,页大小是2KB数据加64B备用区,一个块由64个页组成,那么块大小就是128KB,整片大概有1024个块。新一些的NAND页大小做到4KB或8KB,一个块里的页数也更多。
这里要特别聊一下备用区(Spare Area / OOB)。每页末尾会预留一小块区域,专门放ECC校验码、逻辑块映射信息、文件系统元数据,还有我们后面重点要说的坏块标记。原来很多新手以为OOB就是给用户随便用的,其实不是,它在整个坏块管理和数据纠错体系里是核心区域,用得不对会出大问题。
1.3 SLC、MLC、TLC与QLC:一个存储单元塞几位数据
SLC是每个cell只存1bit,浮栅里只有“有电/没电”两种状态,阈值电压分成两档,误判概率极低。MLC每个cell存2bit,需要区分四档阈值电压;TLC存3bit,八档;QLC存4bit,十六档。
档位越多,相邻档位之间的电压间距就越小,电荷泄漏、读干扰、温度变化带来的电压漂移都更容易引起误判,寿命也会成倍下降。我用一个表把关键差异列清楚:
| 类型 | 每cell位数 | 阈值电压档数 | 典型P/E寿命 | 常见场景 |
|---|---|---|---|---|
| SLC | 1 | 2 | 5万~10万次 | 工业控制、军工、高可靠存储 |
| MLC | 2 | 4 | 3000~10000次 | 消费级SSD、U盘、企业级SLC替代 |
| TLC | 3 | 8 | 1000~3000次 | 大容量消费级SSD、TF卡 |
| QLC | 4 | 16 | 500~1000次 | 大容量低频写入SSD |
选NAND类型不只是容量问题,而是把“数据可靠性”和“寿命余量”放在什么位置的问题。我们项目里全部用SLC,虽然贵,但省心。
2. 读写擦除的底层原理与关键参数
2.1 为什么只能先擦后写:编程与擦除的本质
NAND Flash的编程操作有一个很反直觉的特点:它只能把“1”变成“0”,不能把“0”变回“1”。擦除后的存储单元处于“1”状态,编程时真正被写进去的其实是那些要变成0的bit。
这就解释了为什么NAND必须按块擦除:擦除是把一个块里所有cell的浮栅电荷全部拉出来,恢复成清一色的“1”状态。如果只擦一个页,那些不需要擦的cell会跟着受损,所以物理上只能块级擦除。
在驱动开发中最直接的后果就是:如果你要覆盖写入某个页的数据,必须先保证这个页所在的块是已擦除状态。很多程序崩溃和数据错乱,根源就是在已经写过数据的页上直接再次编程,结果越写越乱。这个小细节,就是“先擦后写”四个字背后真正的物理原因。
2.2 寿命从哪来:P/E次数与氧化层磨损
每次擦除操作,浮栅和沟道之间的氧化层都要经历一次高电压下的电荷迁移,称为Program/Erase Cycle,也就是P/E次数。氧化层耐不住无限次的折腾,随着P/E次数增加,氧化层内部会积累损伤,电子更容易被困在陷阱里,阈值电压窗口变小,最终导致cell无法可靠编程或擦除。
SLC一般在5万到10万次,MLC在一万次左右,好的能到两万,TLC普遍一两千到三千次。具体数值Datasheet里都会给,不看就直接做产品设计是在给自己埋雷。我做项目时都会把寿命预算算一遍,比如设备每天写100MB,选一颗128GB TLC,看似寿命很够,但如果文件系统不做磨损均衡,天天往同一个块上写,实际情况和理论寿命差距会非常大。
2.3 两个容易被忽略的“隐形杀手”:读干扰与数据保持
读操作不会直接写数据,但也不是完全无副作用。读取一个页时,同一条bit line上的其他cell会被加上较高的导通电压,长期频繁读同一个区域,那些非目标cell里的电荷会发生偏移,积累到一定程度就变成误判,这就是读干扰。
另外一个是数据保持,浮栅里的电荷会慢慢泄漏,温度越高漏得越快。常温下数据放个几年通常没问题,但工业设备常在高温环境工作,五六十度甚至更高温下,数据保持时间会急剧缩短。我们有一台设备在南方机房放了两年没开机,再上电后一部分历史数据就出现了ECC警告,还好当时留了足够的纠错余量,否则数据直接不可读。
对付这两个问题都需要主动维护:读干扰靠定期搬移,读次数多的块如果ECC错误率上升,就把数据复制到新块;数据保持问题则要求驱动知道“这块数据放了多久”,必要时刷新重写。
3. 坏块是怎么产生的:分类与定位
3.1 出厂坏块 vs 使用坏块
先明确一点:NAND Flash出厂时就不保证所有块都是好的。由于制造工艺的杂质、光刻缺陷等问题,每颗NAND芯片出厂时就会有一定数量的块无法正常使用,这是原厂在生产测试阶段就确认并标记好的,属于出厂坏块。
使用过程中产生的坏块,则是寿命耗尽或者遭遇极端工况的结果。比如P/E次数逼近极限、长时间高温工作、编程或擦除过程中掉电、强烈干扰导致的电压毛刺,都可能让原本正常的块变成坏块。两类坏块的共同点是:都不可修复,只能绕开。区别在于,出厂坏块从一开始就能扫描出来,使用时只需要避开;使用坏块来得不可预期,必须在运行过程中动态检测和应对。
3.2 坏块标记:原厂写在备用区的“身份信息”
出厂坏块不是随机遗漏的,原厂会在坏块的第一个页或前两个页的备用区写入标记值,通常是0x00,而正常擦除状态是0xFF。所以拿到一颗NAND,只需要遍历所有块,读第一个页的OOB区域,看有没有非0xFF的字节,就能把出厂坏块找出来。
不过有一个坑:不同厂商的标记位置可能不一样。有些厂商标记在OOB第一个字节,有些标记在特定偏移位,有些要求检查前两页而非第一页。我在项目里专门吃过这个亏,用三星的扫描规则去扫一颗镁光的芯片,扫出来的“坏块表”完全不靠谱。拿到一颗新料,第一件事就是翻Datasheet的Bad Block Information章节,按厂商规则来。
另外,现在有些主控会自动跳过坏块标记,如果你用的是原厂提供的擦除工具,工具可能已经把坏块信息处理过了。但作为驱动工程师,我始终建议自己重新做一次全盘扫描,别偷懒。
3.3 坏块为什么需要动态管理
出厂坏块是静态的,出厂时就固定了,但使用坏块是动态的。今天正常的块,跑了几个月后可能编程失败,也可能擦除失败,如果不做动态管理,就会遇到我开头说的那种问题:文件系统写到一个之前正常的块,结果写不进去,数据直接丢。
动态坏块管理要解决的核心是两件事:第一,怎么及时发现坏块;第二,发现之后怎么把数据搬走,并且以后不再用这个块。第二点尤其重要,很多精简驱动只在启动时做一次全盘坏块扫描,运行期间完全不做监测,这在SLC上可能还能扛一阵,在TLC/QLC上基本等于撞运气。
4. 坏块管理实战:从扫描到替换的完整方案
4.1 坏块扫描的三个层次
完整的坏块检测分成三个层次。第一层是出厂扫描,读OOB标记,把原厂标记的坏块找出来,这个在系统启动时做一次就够了。第二层是整盘测试,对空盘执行擦除、编程、回读校验,能发现虽然出厂标记正常,但实际已无法可靠工作的块。第三层是运行时监控,每次编程、擦除、ECC纠错时记录失败信息,把出问题的块及时加入坏块表。
第一层和第三层是必须做的,第二层看场景:如果产品出厂的固件里有初始化功能,建议全盘扫一遍再交付使用,避免把“带病块”留给用户。注意,整盘测试会擦掉所有数据,只能对空盘做,别在生产后段对已经写了固件的设备执行。
下面是一个简易出厂坏块扫描伪代码,用的是裸NAND读取OOB的思路:
#define PAGE_SIZE 2048 #define OOB_SIZE 64 int nand_scan_bad_blocks(struct nand_info *nand) { int block, bad = 0; uint8_t oob[OOB_SIZE]; for (block = 0; block < nand->num_blocks; block++) { // 读块内第一个page的OOB区域 memset(oob, 0xFF, sizeof(oob)); nand_read_oob(block * nand->pages_per_block, 0, oob); // 出厂坏块标记:OOB[0] != 0xFF,部分厂商检查前两页 if (oob[0] != 0xFF) { bad++; nand->bbt[block] = BBT_BAD; } else { nand->bbt[block] = BBT_GOOD; } } return bad; }注意这里的OOB[0]只是最常见的检测规则,实际产品里要根据厂商Datasheet调整偏移和检查页数。
4.2 坏块表的保存与恢复
有了坏块表,不能只存在内存里,掉电就没了,那每次启动都重扫,不但慢,而且运行时新标记的坏块信息会丢失。坏块表必须持久化存储,通常放在NAND的固定区域,比如最开头的几个块或者最末尾的块。
坏块表常见的保存策略是存多份副本,至少两份,轮流写入。每次更新时先写备份,再写主表,避免更新过程中掉电导致表损坏。启动时先读主表,校验失败再用备份表,如果备份也不可信,就退回全盘重新扫描。
我习惯用位图方式保存坏块表,每bit代表一个物理块,bit为1表示正常,0表示坏块。一颗1024块的NAND,坏块表只需要128字节,加上校验和和序列号,一个页就放得下,写起来也快。
4.3 ECC校验与坏块判断的配合
很多入门级文章把坏块管理简单理解成“扫描标记-避开”,但真正可靠的坏块判断离不开ECC。ECC校验码存在每个页的OOB区域,写入时由硬件或软件生成,读取时重新计算并比对,能纠正一定比特数的错误。
ECC先发现问题,坏块管理再做后续处理。典型判断逻辑是:如果一次读回操作ECC纠正了较多bit错误,说明这个块正在劣化,虽然本次数据还能恢复,但应该启动搬迁流程,把该页数据复制到好的块,并把这个块标记为“待观测”甚至直接判坏。如果ECC彻底校验失败,说明数据已经不可恢复,这个块必须立即标记坏块。
在现代大页NAND上,BCH码和LDPC码用得最多。LDPC纠错能力更强,但需要主控有软判决能力,普通单片机做起来吃力。SLC NAND用BCH就够,TLC以上强烈建议选配带LDPC硬件引擎的SoC或主控。
4.4 写失败、擦除失败后的现场处理
运行中真正的坏块“案发现场”,往往是一次写页操作或者擦除块操作之后的状态寄存器读取。NAND在编程或擦除命令发出后,会让你等待Ready/Busy信号,然后再读状态寄存器,里面有编程/擦除失败标志位。
正确流程是:发写页命令,等待Ready,读状态寄存器,如果失败,不要马上重试覆盖,而是先把这个块标记为坏块,然后把数据写到分配好的替换块里。有些工程师习惯失败后立刻原地重试,觉得是偶发问题,这种做法很危险,因为NAND的编程失败通常不是偶发,而是块已经劣化到临界点了,这次侥幸写进去,下次读出来大概率还是错的。
擦除失败同理。擦除操作失败往往比编程失败更严重,说明整个块的电荷状态已经失控,必须直接判坏,绝不能继续用。
5. 实际应用避坑指南:来自项目现场的教训
5.1 用NAND前必须确认的四个问题
拿到一颗NAND Flash,先别急着写驱动,花半个小时确认四件事,能免掉后面几周的排查时间。
第一,这颗NAND需要多大ECC?Datasheet或应用手册里通常会写,比如每512字节需要至少4字节BCH,或者每1KB需要8字节等。主控的硬件ECC引擎是否支持?不支持的话,你得评估软件ECC会不会吃太多CPU。第二,读ID是否正常?时序参数是否配对了?市面上很多“兼容料”实际上时序并不完全一致,初始化不对会出现各种诡异数据错乱。第三,块大小、页大小、OOB大小是多少?这决定了你驱动里的数据结构怎么设计。第四,你用的文件系统本身是否已经做了坏块管理?Linux下用UBI/UBIFS、JFFS2这类专门为NAND设计的文件系统,坏块处理和磨损均衡都有了,但如果直接上FAT或者裸读裸写,那就全部得自己来。
这四个问题分别对应四个深坑,任何一个没想清楚,后续都可能在现场出问题。
5.2 不经文件系统直接操作NAND时的坏块策略
裸NAND直接操作在低端单片机和嵌入式Linux中都很常见。没有文件系统帮忙时,坏块管理必须完全自己维护。我的建议是至少实现四块基础能力:出厂坏块扫描、运行时写/擦失败检测、坏块表持久化、数据搬移重映射函数。
设计逻辑块映射表是常见做法。每个逻辑块映射到一个物理块,启动时加载坏块表,分配写入块时跳过坏块。每次写操作返回前都要做ECC校验或状态寄存器检查,发现失败立即搬移数据。这个方案写起来不复杂,但一定要预留足够的替换块,不要把坏块表中所有正常块都分给文件系统,保留3%到5%的物理块作为运行时替换池,否则用着用着没有替换块了,系统只能死机。
Linux下如果你不想自己造轮子,强烈建议使用UBI层。UBI自带坏块管理、磨损均衡,配合UBIFS使用非常成熟。我在一个Cortex-A7方案上把裸NAND分区从自研驱动切到UBI/UBIFS之后,坏块相关问题基本清零,投入产出比非常高。
5.3 掉电与并发场景下的数据安全
NAND最怕的工况之一是编程或擦除过程中掉电。写页操作需要一定时间,如果写到一半掉电,这个页可能处于半编程状态,读出来会是一堆无法用ECC纠正的错误数据。更糟的是,掉电如果在块擦除过程中发生,这个块可能进入“半擦除半编程”的异常状态,哪怕重新上电也无法恢复正常。
处理思路是在数据写入路径上增加日志机制。每次写数据之前,先把“我要把逻辑块X的数据写到物理块Y的页Z”这个意图记到单独的日志区,写操作完成后再更新状态。上电恢复时检查日志区,如果发现记录说“应该写完但状态没更新”,就可以把备份数据重新安排到新块,避免半截数据污染主数据区。
并发访问方面,很多主控的NAND控制器同一时刻只允许一个操作,如果家里有裸驱动,务必对整个NAND操作加互斥锁。我见过一个项目,两个任务同时读写不同块的页,结果串扰导致大量数据错乱,加了锁之后再没出现过。
5.4 小容量NAND设备更容易踩的坑
很多低成本的物联网设备用的是64MB、128MB这类小容量SLC,整片只有几百个块。坏块一旦多几个,坏块比例很容易超过10%,可用容量就非常紧张了。这种情况下,坏块表、替换池、文件系统元数据这三部分的预留一定要算清楚,别把可用块全塞给业务数据,否则设备运行后期会面临“有空间但写不进去”的局面。
还有一类典型的坑是来源不明的散新料或者被清过标记的芯片。市面上有些低价NAND是原厂打下来已标记坏块的次品,被重新封装后坏块标记被清除或部分清除。如果直接信原厂标记直接跳过扫描,后果可想而知。所有物料,无论渠道多正规,上线前都统一全盘扫描一遍再入库,这已经成了我们项目的铁律。
最后再分享一个小习惯:每次做NAND相关项目,我都会在调试阶段写一个长时间加压测试工具,循环执行擦除、写入、回读校验,同时定期检查ECC错误率曲线。很多坏块并不是突然坏的,ECC错误率会有一个从低到高的爬坡过程,提前观察到爬坡趋势,比事后被坏块坑一轮要有价值得多。希望这篇总结能帮你在存储设计上少交一点学费。