
前几年做嵌入式产品遇到过一件让我印象很深的事一台设备在用户手里跑了大半年突然出现开机卡死最后定位到是NAND Flash的一个坏块刚好落在文件系统关键元数据区域而我们的坏块管理策略只做了写入阶段的跳过没有处理块在使用过程中新增坏块的情况。那段时间查了不少资料、啃了几份芯片手册、反复做了几百次断电测试才把这块彻底理清。从那以后我就觉得搞懂NAND Flash物理结构和坏块管理不只是做存储驱动或SSD固件的人才需要。任何一个用NAND做存储介质的开发者——不管是做嵌入式设备、工业控制板卡、还是消费电子——都应该把原理吃透否则迟早会被坏块、比特翻转、擦写寿命这些问题教做人。这篇文章我就从物理结构讲起一路聊到坏块产生的机制、管理策略最后落到实际应用中的避坑经验。全程用“做过、踩过、改过”的视角来写不堆概念尽量把每个结论背后的原因说清楚。1. NAND Flash物理结构与工作原理1.1 浮栅晶体管一个“电子仓库”怎么存下1bit数据NAND Flash存储单元的本质是一个浮栅晶体管Floating Gate Transistor跟普通MOS管最大的区别在于栅极和沟道之间多了一层由多晶硅制作的浮栅外面被氧化层包裹着。这层浮栅就是“电子仓库”有没有电子、电子有多少决定了这个单元里存的是“0”还是“1”。写入数据编程时在控制栅上加较高的正电压沟道里的电子会通过隧穿氧化层进入浮栅这个过程叫Fowler-Nordheim隧穿。电子进入浮栅后就被困在里面了——因为氧化层是绝缘体电子想跑出来很困难这就是掉电后数据不丢失的根本原因。擦除时反过来在衬底加正电压、控制栅接地把浮栅里的电子“吸出来”。所以NAND Flash的基本特性就出来了写操作是把电子塞进去擦除是把电子抽出来读操作则是通过阈值电压的变化来判断浮栅里有没有电子。用生活里的例子类比电容充电比喻不太准更像一个带锁的小仓库编程就是往里放货物擦除就是把货物搬空。问题是货物搬进搬出是有磨损的——隧穿氧化层每一次搬货都会受到应力损伤这个层越来越薄、越来越漏电寿命就是这么一点点耗掉的。1.2 SLC、MLC、TLC、QLC同一个仓库四种存货方式如果只区分“有没有电子”一个单元只能存1bit这就是SLCSingle-Level Cell。后来人们想能不能根据电子数量的多少来区分多档状态于是MLC用4档电压存2bitTLC用8档电压存3bitQLC用16档电压存4bit。但要明白一个关键点电压档位越多相邻档位之间的电压差就越小抗干扰能力就越差。TLC的两个相邻状态之间可能只差几百毫伏氧化层稍微漏点电或者受到相邻单元读操作的干扰阈值电压就可能漂移到相邻档位去你的“0”就变成了“1”。模拟一下在一个2V的电压窗口里切8份做TLC每个档位区间只有250mV左右如果做QLC切16份每个区间只剩125mV。而NAND的物理噪声、读干扰、编程干扰本身就很容易产生几十到上百毫伏的扰动所以QLC必须依赖更强的ECC纠错码才能保证数据可靠。选型时也别只看单颗芯片价格。SLC虽然单位成本最高但耐久度擦写次数P/E Cycle通常是TLC的十倍甚至更多。工业级产品偏好SLC或pSLC把MLC/TLC当SLC用不是因为有钱烧的而是省掉后面数据管理、坏块替换的无数麻烦。1.3 从单元到Page再到BlockNAND的三级组织结构单个浮栅晶体管太小了实际使用时要把它们组织起来。NAND Flash的基本组织结构是Page页读写的最小单位大小一般是2KB、4KB、8KB、16KB不含备用区Spare AreaBlock块擦除的最小单位一般由128个或256个Page组成Plane平面多个Block组成通常一个Die有2个或4个PlaneDie裸片一颗独立的芯片核心往往一颗封装里含多个Die这个结构带来的第一个重要结论就是NAND Flash的擦除和读写是不同粒度的。你可以只读一个Page也可以只写一个Page但想在某个Block里擦掉一部分数据是不行的——哪怕只想清空里面1个Page的数据也得把整个Block所有数据先搬到别处再完整擦除整个Block。类比成仓库管理Page是货架上的一个托盘Block是一整个仓库。你只能拿掉托盘上的一件商品读/写Page但想清理货架得把整个仓库存货挪出去、全部清空、再重新摆回来。如果仓库里还有其他有价值的货物搬家就得小心翼翼。这个特性直接影响文件系统和FTL闪存转换层的设计。如果文件系统直接按页操作而不考虑块擦除对齐频繁更新小文件会触发大量的“读-改-写”搬运擦写放大系数爆表性能和寿命都会崩。1.4 P/E Cycle耗尽为什么闪存越用越“脆”每一个Block都有擦写次数上限这个数值称为P/E CycleProgram/Erase Cycle。SLC一般10万次左右MLC在3000到10000次TLC在1000到3000次QLC普遍在500到1000次。一旦超过这个次数Block的漏电和编程错误会急剧增加ECC越来越难纠回来最终变成坏块。从物理上看原因很简单每次擦写隧穿氧化层都在经历“电子穿梭”的冲击氧化层内部会积累陷阱电荷绝缘性能和结构完整性逐步下降。这就像一个气球反复吹气放气次数多了橡胶老化要么吹不动了要么漏气了。但P/E Cycle只是个统计数字不代表到了这个次数块一定坏也不是没到就一定不坏。不同批次、不同温度、不同写入模式实际寿命都可能有很大偏差。所以坏块管理不能只依赖厂商给的标称寿命值必须在运行时持续检测和处理。2. 坏块是怎么产生的从物理机制到类型划分2.1 出厂坏块与使用坏块先天缺陷 vs 后天损伤坏块分两类处理逻辑完全不同。出厂坏块Initial Bad Block / Factory Bad Block芯片出厂时就存在的坏块是晶圆制造和封装过程中的瑕疵。原厂会在Block的特定位置通常是第一个Page的Spare Area做标记比如写入非0xFF的特定值。出厂坏块只占很小的比例但管理时必须留意。使用坏块Runtime Bad Block / Acquired Bad Block芯片在生命周期内因P/E磨损、干扰、温湿度应力、静电损伤等原因新产生的坏块。这类坏块是运行过程中慢慢出现的必须靠主控/FTL实时扫描和记录。这两种坏块我都见过不少。有一次做老化测试一批在常温下很正常的芯片搬到高温箱里跑了72小时坏块数量直接翻了几倍。后来查数据手册才发现高温会加速电子泄漏同时氧化层缺陷的失效率也会显著提升。做车载或工控产品的朋友选型和设计时一定要把温度因素考虑进去。判断一个块是否损坏的方法尝试擦除然后读回看是否全部为0xFF或者尝试写入再读回用ECC校验结果判断。擦除后仍有位无法归零、写入后读回与写入内容不一致、ECC无法纠错——出现其中任意一种基本就可以判定该块不可用了。2.2 干扰效应读操作也能搞坏周围邻居干扰效应是NAND Flash里特别隐蔽的坑。很多人以为只有写入才影响数据其实读写操作都会对周围的浮栅晶体管产生干扰最典型的是Read Disturb读干扰反复读取某个Page时同一Block里其他Page的单元会受到影响。因为读取时字线上加的电压虽然没有编程那么高但次数多了还是会有电子慢慢注入浮栅导致阈值电压漂移。尤其是读密集型的应用一个Block反复读同一块区域旁边Page的数据可能悄悄变了而不自知。Program Disturb编程干扰对某个Page写入时同一WordLine上相邻Page或相邻Block的单元可能被部分编程阈值电压被抬高。Data Retention数据保持写入后长时间不上电浮栅里的电子会慢慢泄漏低温-40度以下和高温85度以上都会加速这个进程。它不产生新的坏块但会让已有坏块周围的数据更容易翻车。读干扰带来的直接后果就是你以为“只读不写”很安全结果某些Page的ECC错误逐渐累积直到某天读出来已经是不可纠错的错误。这也是为什么好的FTL会做“数据刷新”定期把块里的数据搬到新块然后擦除原块相当于“重启”一次把阈值电压拉回正常状态。2.3 数据保持与ECC纠错一块“护身符”能做到什么程度ECCError Correction Code纠错码是NAND可靠性的最后一道防线。每次写入时主控把数据算出一段校验码和业务数据一起存进Page的Spare Area读取时用校验码检测数据是否出错出错且在校验能力内时自动纠正。SLC时代ECC要求并不高1bit/512B的纠错能力基本够用但TLC和QLC时代LDPC低密度奇偶校验码成为主流方案纠错能力动辄涨到几十bit/1KB甚至上百bit/1KB。用BCH纠1bit时代的思路看今天完全不够用了。ECC的纠错能力是有上限的。如果错误位数超过纠错能力读出来的数据就是错的而且代码根本不知道哪里错了。这时候要么靠上层CRC/MD5之类再做一次完整性校验发现异常要么数据就真的丢了。所以一个好的存储架构是分层的NAND本身有物理错误主控用ECC管一层文件系统/应用再管一层完整性校验。把全部希望寄托在任何单一层面终有一天会被现实教育。3. 坏块管理核心策略与实操3.1 坏块表出厂坏块记录和运行时坏块记录要分开坏块管理的第一步是建表、查表、更新表。实际工程里我习惯分成两张表出厂坏块表Initial Bad Block TableIBBT芯片出厂时扫描一遍记录原始出厂坏块的地址。这张表基本不变只在初始化时加载一次作为基础底册。运行时坏块表Runtime Bad Block TableRBBT程序运行过程中新检测到的坏块实时追加记录。为什么必须分开因为出厂坏块和使用坏块的分布特征不同。出厂坏块是随机散布的本身有人统计过用的是类似于泊松分布的模型而使用坏块往往集中在高擦写区域。分开记录做磨损均衡和调度时可以更精准。坏块表本身也要存到NAND里。通常的做法是每个Block的特定Page保存坏块标记同时系统在启动时扫描若干保留Block作为坏块表的持久化存储区并做多副本冗余。试想一下如果坏块表本身所在的Block也坏了而你又没有冗余备份那整个存储系统就变成无头苍蝇了。3.2 块替换与坏块跳过两种常见的管理思路坏块管理在系统层面有两种典型思路。块跳过Skip在逻辑地址映射到物理块时遇到坏块就跳过映射到下一个好块。实现简单、开销小但物理块地址和逻辑地址不是连续对应的碎片比较多对地址映射表的管理要求更高。块替换Replace保留一组好块作为替换池。当监测到某个块接近寿命或出现故障时把数据搬到替换池里的好块更新映射表然后把这个坏块标记为坏。这个方案更复杂需要有足够多的预留空间但管理更灵活SSD基本都是用这种方式。实际产品中很少只用单一策略。我在嵌入式项目里常用“块跳过作为底线块替换作为主动调节”的混合策略初始化时用跳过策略避开出厂坏块运行时检测到坏块再触发替换操作。这里有个很容易忽略的点替换时机。替换太频繁会导致很多块被过早报废替换太晚数据可能已经损坏。我在项目中加入了“坏块预警”机制即ECC纠错后剩余纠错能力低于某个阈值时提前把该块数据搬到健康块而不等到完全写不进去才处理。这套机制上线后卡死问题基本绝迹。3.3 磨损均衡为什么要让每个块“雨露均沾”磨损均衡Wear Leveling解决的是“部分块频繁擦写、部分块常年闲置”的问题。最理想的情况是所有块的擦写次数大致相同这样整颗芯片的总寿命可以最大化。有两种实现粒度动态磨损均衡只在写入新数据时选择擦写次数最少的块实时调整。静态磨损均衡还会把那些长期不变的数据比如文件系统的系统文件搬到擦写次数少的块里旧块释放出来供写入使用。只做动态均衡是最常见的坑。像固件存储、日志文件这类频繁更新的数据会集中在少数块上做磨损而存放只读资源的那些块几乎不擦写。没有静态磨损均衡钱包再厚的芯片也经不起这种“局部过劳死”。4. 从原理到落地应用层避坑指南4.1 谨慎假设“写之前一定是干净的”新手最容易犯的错误是申请一个块直接写入认为里面是干净的0xFF。但NAND的擦除并不能保证100%干净尤其是老化的颗粒擦除后可能残留一些位刷不回来读出来不是全0xFF。此时直接写入残留数据会和业务数据混杂在一起后患无穷。正确操作是写入前先做状态判断必要时先擦除再写。比如从坏块表拿一个块先擦除读回检查是否全0xFF符合要求再写。如果不符合说明这个块大概率已经异常直接标记坏块换下一个。每次初始化和上电扫描时把所有块擦除一遍再使用能让后续管理简单很多。别嫌弃这多出来的时间开销它换来的是一整颗芯片的稳定性。4.2 读回校验写入成功只是开始NAND的写入操作返回“成功”并不代表数据一定正确。因为编程完成后主控手上并没有实际数据只是知道指令执行完毕了。有的芯片内部带有编程校验但那是针对“能否编程成功”的检查不是针对“写入的数据是否与期望一致”的校验。我见过几次诡异的问题写成功、读出来的数据却是错的ECC能纠但每次都纠说明错误已经存在了。最后定位发现是主控和NAND之间有信号质量问题走线太长、端接不匹配导致传输层就出错而NAND和主控都以为写对了。所以重要数据写入后强制做一次读回校验用ECC验证再返回成功。性能要求高的场景也要至少做一次全量校验扫描宁可在启动或升级时多花时间不要等到用户数据坏了再来救。4.3 掉电保护与日志坏块来时不能慌坏块出现往往伴随着掉电、重启、写入中断这些恶劣工况。如果在掉电瞬间正好在擦除或编程某个块这个块很可能当场报废。更麻烦的是如果坏块表刚好也处于更新过程中掉电整个映射关系可能错乱。常规做法是引入日志型记录。坏块信息和映射表更新前先把事务日志写到NAND固定区域多个备份轮流覆盖启动恢复时先回放日志再校验坏块表。就好比记账前先写备忘录账本出了问题还可以凭备忘录恢复。掉电是存储系统最大的敌人之一所有组织数据更新顺序的逻辑都应该围绕“掉电瞬间发生了什么来设计”。4.4 预留空间与分区给替换池留一条后路做产品设计时别把NAND容量用满。一定要预留一部分空间作为替换池存放运行期坏块的替换块损耗均衡池给静态磨损均衡搬运数据用坏块表多副本存储区预留比例取决于你的产品形态。消费级产品可能预留5%~10%工业级产品我觉得至少15%~20%比较稳。有一次我在一个容量很紧的项目里把预留空间压到3%结果跑了大半年坏块一多替换块不够用系统直接进入只读模式用户那边炸了锅。从那以后我宁可产品容量少标一点也绝不压缩预留。分区也很关键。启动代码、根文件系统、用户数据分开存。启动代码区域最好用SLC模式或选寿命令更高的颗粒并且单独一个分区跑静态磨损均衡防止启动区出现坏块导致设备直接变砖。不少方案把boot区做到带ECC保护的NOR Flash或者单独一个SLC NAND上不是没道理的。4.5 监控与预警在坏块彻底报废前行动除了被动处理已经产生的坏块更好的做法是主动监控。具体来说定期读取各Block的擦写次数通过命令或内部计数器分析磨损分布监控ECC纠错次数某块ECC纠错次数突然增加是非常明显的劣化信号坏块数量增长速率统计单位时间内新坏块的数量是否异常把这三类数据持续记录到独立的监控分区初期可以做离线分析后期可以做成实时预警。比如连续N个小时内坏块数量超过M个系统自动上报。这在远程维护的场景里省了我不少力气——很多故障在用户感知前就被提前处理掉了。5. 常见问题排查与经验总结5.1 坏块问题排查速查表现象可能原因排查方向处理方式擦除后读不是全0xFF块老化或损坏确认时间、坏块标记标记坏块替换读回错误但ECC可纠读干扰、数据保持检查读写频率、温度数据刷新搬移ECC不可纠严重劣化或传输问题检查时序、信号完整性但失败复位重试仍失败标记坏块断电重启后丢数据写入中断未落盘检查掉电保护设计日志回放重新映射坏块表更新掉电崩溃坏块表冗余不足检查坏块表存储策略增加多副本和日志系统运行慢擦写放大严重看GC频率、磨损均衡算法优化FTL增加预留空间5.2 实测中最容易踩的三个隐形坑坑一把块擦除次数当作唯一寿命指标。实际项目中数据保持时间往往先于P/E寿命耗尽。一个只写了一次但放了三年高温区的数据错误率可能比写了1000次但经常刷新的数据还高。温度与数据保持的关系多做实验才有感觉。坑二坏块表只存一份。坏块表是系统的生命线只存一份风险极高。我在一个方案里只做了双备份结果两块都因为同一个干扰效应同时损坏系统直接无法挂载存储。从那以后必须三份以上、分布在不同Plane/Die上。坑三开机扫描只扫Block的标记位。出厂坏块标记是原厂在Spare Area里写的但运行过程中产生的坏块如果只靠运行时检测掉电瞬间的坏块可能来不及被记录。所以上电后不仅要查坏块标记还要对应用区的块做抽样校验测试尤其是上次异常断电之后。5.3 最后给新手的实操建议如果你首次接触NAND存储开发我的建议是不要急着调FTL或自己写坏块管理。先拿一颗Datasheet详细一点的NAND配合一个带ECC能力的主控或开发板做以下几件基础功手动扫一遍全部Block记录出厂坏块分布写一个脚本连续擦写同一块观察ECC错误率变化曲线把一块芯片放到高温箱里做数据保持实验测试不同温度下位翻转率模拟掉电场景验证你的重启恢复逻辑是否可靠这些基本功做好了比你看十遍理论文章都管用。NAND存储这东西理论是地图实验才是真正的路。我自己也是踩了无数坑才把这块搞明白真要说有什么诀窍就是动手前务必把物理层特性吃透设计方案时多留余量处理故障时永远先怀疑自己——怀疑你的设计、你的时序、你的策略然后再怀疑颗粒本身。