
做嵌入式这几年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寿命常见场景SLC125万~10万次工业控制、军工、高可靠存储MLC243000~10000次消费级SSD、U盘、企业级SLC替代TLC381000~3000次大容量消费级SSD、TF卡QLC416500~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需要多大ECCDatasheet或应用手册里通常会写比如每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错误率会有一个从低到高的爬坡过程提前观察到爬坡趋势比事后被坏块坑一轮要有价值得多。希望这篇总结能帮你在存储设计上少交一点学费。