做嵌入式存储相关的开发,不管是写裸NAND驱动、搭文件系统,还是选型eMMC芯片,迟早都会撞上一个绕不开的话题——坏块管理。这个词听起来像给数据存储做“安检”,但它实际上是贯穿NAND Flash整个生命周期的基本功。我刚开始接触的时候,也以为坏块管理就是单纯把坏块找出来做个标记,后来在轻视它这件事上栽了不少跟头。这篇内容就围绕BBT(坏块表)和坏块管理的核心逻辑展开,用贴近实操的方式说清楚:NAND为什么必然会产生坏块、BBT是怎么运作的、不同方案下坏块管理到底该怎么落地。适合刚入行的嵌入式软件工程师,也适合准备把存储方案做到产品级的开发者。
1. NAND的物理宿命:为什么坏块是躲不掉的
1.1 一个数据单元是怎么“累死”的
NAND Flash的存储单元本质上是一个浮栅晶体管,数据是靠电荷存在浮栅里来表达的。写入时,电子被强电场穿过隧穿氧化层注入浮栅;擦除时,又用相反的电压把这些电子拉出来。问题就出在这个“穿来穿去”的过程上。
每做一次编程/擦除循环,隧穿氧化层就会被“搓”掉一层。次数多了,氧化层变薄、漏电、锁不住电荷,这个单元的可靠性就越来越差,直到彻底无法判断里面存的是0还是1。这就像一张纸反复铅笔写、又反复擦掉重写,擦到最后纸面起毛、破洞,再怎么写也写不成型了。
这个过程不是说整个芯片一起坏,而是按块(Block)为单位逐步恶化的。NAND的最小读单元是页(Page),最小擦除单元是块(Block),一块通常包含64、128甚至几百个页。只要一个块里的关键单元坏了,导致这个块没法被可靠擦除或写入,那这个块基本上就宣告报废。
1.2 坏块的正式判定:不是你想坏就能坏
工程上判断一个块是不是坏块,靠的是操作结果和ECC校验,而不是靠肉眼观察:
- 编程/写入失败:往某个页写数据时,状态寄存器回报编程失败。这类失败通常意味着整个块已经处于不可用状态。
- 擦除失败:擦除操作回报失败,这个块基本无药可救。
- ECC纠错无法容忍:读取时数据错误比特数超过ECC能力上限,说明这个块的电荷保持能力已经崩了。
坏块管理的核心对象,就是以块为单位的这些东西。有不少人第一次接触NAND时习惯把坏块理解成“硬盘的坏道”,这个类比其实不太准。坏道往往是一个扇区坏了,还能隔离掉;而NAND的坏块是物理上“一整块”不可用,而且坏掉的块数量会随着使用时间持续增长,不是一个恒定值。
2. 出厂坏块与运行期坏块:两种语境下的同一件事
2.1 出厂坏块:原厂已经帮我们做了标记
NAND出厂时就不是完美的。原厂在晶圆制造、切割、封测阶段,就能检测出一部分不合格的块。这不是成品没做好,而是工艺上允许有一定比例的不良品存在。比较有意思的是,随着制程不断微缩,出厂坏块的比例反而在上升。以前SLC时代,出厂坏块可能也就千分之几;到了TLC、QLC时代,1%~2%甚至更高的出厂坏块比例都很常见,原厂会在芯片出厂前把这些坏块状态“烧”到一个固定位置。
最常见的标记方式,是在块内第一个页的OOB区域(带外区域)固定偏移位置写入非0xFF的值。比如某个行业约定俗成的规则,把OOB里特定字节写成0x00代表坏块,保留0xFF代表好块。原厂还会多写几处重复标记,防止标记位置本身被破坏后读不出来。
2.2 运行期坏块:系统运行中的拦截信号
出厂坏块是“天生的”,运行期坏块则是“用坏的”。系统运行中主要靠两类信号发现新坏块:
第一类是操作层回报的硬错误。比如文件系统往某个块发擦除命令,返回擦除失败;往某个页发写命令,返回编程失败。这些都是硬性的物理故障信号,基本不用怀疑,直接标记坏块就行。
第二类要靠ECC状态位来判断。NAND的页读取通常会返回ECC纠错情况,如果当前页的错误比特数已经逼近甚至超过ECC阈值,这通常是一个预警信号。行业里一般不会马上把它当成坏块处理,而是尝试把这个块的数据搬走、做一次擦除,如果擦除还失败再确定坏块。这个“二次确认”流程能有效减少误判,毕竟有些时候只是电荷发生暂时性偏移,重新擦写后还能继续用。
2.3 两种坏块的管理权重差异
从管理策略上看,出厂坏块属于静态信息,产品出厂前处理好就行;运行期坏块则是动态事件,整个生命周期都要盯。下面这个表格可以把两者的差异看得很清楚:
| 对比维度 | 出厂坏块 | 运行期坏块 |
|---|---|---|
| 来源 | 制造环节的工艺缺陷 | 擦写磨损、电荷泄漏、环境干扰 |
| 特点 | 数量固定,出厂时已确定 | 数量随P/E次数增长而增加 |
| 获取方式 | 读OOB区域的原厂标记 | 操作失败、ECC阈值告警 |
| 处理阶段 | 设备初始化阶段扫描并建立BBT | 运行中实时监测并动态更新BBT |
| 误判风险 | 低,标记固定 | 较高,需要二次确认 |
很多嵌入式工程师只重视出厂坏块的扫描,对运行期坏块的动态更新不够上心,这是后面会踩大坑的地方。
3. BBT的内核逻辑:一张表如何撑起整片存储的可靠性
3.1 BBT到底记了什么
BBT的全称是Bad Block Table,坏块表。它的本质很简单:用一张结构化的记录,标记出哪几个块是坏的,哪几个块是好的。
常见的表项记录内容有这么几类:
- 块编号或物理地址:让人能定位到具体哪个块。
- 坏块状态:0表示好块,1表示坏块,也可以用枚举值细分坏的原因。
- 发现时间或原因标识:像是出厂坏块还是运行期新损坏,方便后期做统计分析和寿命预测。
- 保留字段:为以后扩展留位置,比如记录被替换的备用块映射关系。
BBT可以有不同形态。最简单的是位图形式,一个bit对应一个块;稍微复杂一点的是引导区形式,记录每个块的详细状态。位图形式省空间、查询快,但信息量少;引导区形式占用大一点,但能承载更多现场信息。实际产品里两者经常结合使用,位图做索引、详细记录做兜底。
3.2 BBT自己放在哪里才安全
这里的核心矛盾是:BBT是用来记录坏块的,但存放BBT的区域自身也存在坏块风险。如果把BBT放在固定的一个块里,这个块坏了,BBT就全丢了。行业里通用的做法是冗余副本加分散放置:
- 多个副本:通常存放2到4份,分散在不同Plane、不同Die或不同分区。
- 写入仲裁:每次更新时按序写入副本,并带序列号或CRC校验。读取时取校验通过且序列号最新的那份。
- 动态搬迁:如果发现某个BBT存放块开始出现ECC告警,就主动把BBT搬到一个健康的保留块里,并更新指针。
这些思路和数据库的WAL(预写式日志)有点相似,核心思想都是“不能把鸡蛋放在一个篮子里”。有些方案甚至会把BBT管理器体本身做成一个小文件系统,专门管理这些系统块,这样可扩展性更强,但复杂度也高不少。
3.3 BBT什么时候更新、怎么防止更新丢一半
BBT不是启动时读一次就完事,它在运行中是持续演变的。触发更新的典型时机包括:
- 某个块在擦除、编程时失败,确认坏块后立即更新。
- 后台巡检发现ECC错误率异常升高,完成了坏块替换后更新。
- 磨损均衡模块切换活跃块,更新映射关系。
这里最容易出问题的是更新过程中的掉电。假如正在往BBT块里写新数据,写到一半突然断电,老数据被覆写了一部分,新数据也没写完,BBT直接就“残废”了。行业里的通用做法是双页交替写:保留上一版有效BBT,把新版本写在另一个位置,写完后再做一个切换标记。下次启动时先读切换标记,再决定用哪个版本。如果两个版本都校验失败,那就只能回到出厂扫描状态——意味着所有运行期的坏块记录全部丢失,系统不得不重新全盘扫描,这在量产设备里属于重大事件。
4. 坏块管理的三种主流策略与选择权衡
4.1 跳过坏块:最朴素也最常见
跳过策略很好理解,就是通过BBT查询到坏块地址后,在寻址时直接跳到下一个可用块。这种方案不需要维护复杂的映射表,逻辑简单、开销低,对工程量有限的产品很友好。
但它的缺点也很明显。因为物理地址的连续性破坏了,逻辑块号和物理块号之间无法一一对应。如果上层文件系统直接按连续物理地址计算偏移,就会踩到坏块上。所以跳过策略一般得配合某种地址转换层,至少是一个简单的“逻辑块号到物理块号”的重排序表。
4.2 重映射与保留块池:FTL层的基本功
更专业的方案是引入FTL(闪存转换层),把逻辑块号映射到物理块号,并在芯片出厂时就划出一块保留块池。当某个活跃块损坏时,FTL从保留块池里选一个新块,把旧块里的有效数据搬过去,然后更新映射表。这个过程对上层来说是透明的。
这套机制最大的优势是分布更均衡,不会因为个别坏块导致空间利用率断崖式下降。但代价是FTL本身复杂度高,映射表也要占用一部分NAND空间,还得时刻处理映射表的持久化问题。尤其到了掉电恢复场景,如果映射表丢了,后果比BBT丢失还严重,整个用户文件系统都可能无法挂载。
4.3 策略选择的现实权衡
我做过几次选型对比,简单总结一下三种策略的适用场景:
| 策略 | 实现难度 | NAND利用率 | 运行开销 | 适合场景 |
|---|---|---|---|---|
| 跳过坏块 | 低 | 中等 | 低 | 裸NAND+自主文件系统、数据可重建场景 |
| 地址重映射 | 中 | 较高 | 中 | 需要随机读写性能的小型设备 |
| 保留池+磨损均衡 | 高 | 高 | 较高 | 追求长寿命、高可靠性的产品级方案 |
需要注意的是,选择策略时不能只盯着“坏块怎么办”这一个问题,还得和磨损均衡、掉电保护、垃圾回收这些功能综合起来考虑。比如一个产品的数据更新频率很高,那磨损均衡的重要性可能远大于坏块替换;反过来,如果产品生命周期短、写入量少,复杂的FTL层反而可能因为映射表写入放大而加速磨损。
5. 裸NAND与带主控存储:玩法的分水岭
5.1 裸NAND:从启动阶段就要自己扛
如果方案用的是裸NAND(没有内置主控的Flash芯片),那坏块管理就是嵌入式软件自己躲不开的工作。难点在于,从Boot阶段开始就得处理坏块,因为引导代码本身也放在NAND里。
常见做法是把引导代码分成多份冗余存放,BootROM加载第一份失败后尝试第二份。同时,Boot阶段的代码要尽量小,能塞进几个块中不跨坏块。等主系统运行起来,再加载完整BBT进入工作模式。曾有团队在某个项目里把Boot分区紧挨着一块出厂坏块放置,反复测试几十次没问题,结果量产某批次芯片因为坏块位置偏移,直接导致一批设备刷机后起不来。这就是跳过策略下没做冗余引导的典型事故。
5.2 eMMC/UFS:主控替你扛,但你仍要懂
对于eMMC和UFS这类带主控的存储,坏块管理和磨损均衡大多数已经由内部FTL处理了。上层开发者不用再去扫描出厂坏块,也不需要维护BBT,看起来很省心。但省心不意味着可以无知。
带主控的存储依然有寿命上限,而且内部容量里面还藏着一块保留空间,专门给坏块替换和磨损均衡用。当主控内部保留块消耗殆尽,芯片就会出现大量写入失败或强制只读。所以就算用eMMC,我也建议在产品里保留SMART数据的读取通道,监控平均擦写次数和重映射块数。不然设备突然间批量变砖,排查起来很痛苦。
5.3 几个容易混淆的判断
我经常看到有人把“裸NAND”和“eMMC”混为一谈,这里有几个判断上的关键分歧点:
第一,裸NAND没有统一的状态汇报接口,坏块信息只能靠上层扫描或原厂标记;eMMC有标准状态寄存器,可以查询寿命和重映射情况。第二,裸NAND的坏块管理策略完全取决于自己代码的健壮性,换了主控方案后代码基本可以复用;eMMC的方案则依赖于主控厂商实现质量,换供应商后行为可能有细微差异。第三,裸NAND对掉电非常敏感,映射表丢失风险高;eMMC由于有内置电容和固件保护机制,掉电容错性要好得多,但也不是100%免疫。
6. 实战里最容易踩的四个坑
6.1 坏块表更新的掉电瞬间
我们在某个模拟项目X里调试了很久,最后发现问题出在BBT更新流程。当时为了图省事,BBT只有一个副本,每次更新都是直接擦除原块再写新内容。结果设备在频繁掉电测试下,大概每几百次就会遇到一次BBT校验失败。原因很简单,擦除原块后新数据还没写完,电就断了,整个表变成了空白。
后来改成双副本轮流写,并增加“当前有效版本”标志位,这个问题基本清零。经验是:坏块表的更新流程里必须有一个原子切换动作,不能存在“中间状态可被识别为有效”的窗口。
6.2 上电全盘扫描的正确打开方式
每次上电都做全盘扫描最稳妥,但代价很大。一片中等容量的MLC NAND,全盘读一遍可能要好几十秒,很多产品根本接受不了这个启动时间。
更实用的办法是把扫描拆成两级:启动阶段只扫描出厂坏块标记区和最近记录的BBT,快速进入工作状态;后台再慢慢做全量巡检,发现新坏块再动态更新。当然,前提是文件系统访问NAND的路径本来就要走统一的坏块查询接口,否则后台扫描还没跑完,某个坏块可能已经被读过了。
6.3 出厂坏块标记的重复判断陷阱
原厂标记一般在OOB区域重复写好几份,目的是防止某个标记位置被意外改写。但我见过一些代码只判断第一个标记位置,一旦那个位置损坏,明明是坏块却被当成好块,写入后就出现数据打水漂的情况。
正确做法是检查多个标记位置,并设定判定规则:比如“任意一个标记为非0xFF即视为坏块”或者“多数标记一致才判定”。后一种更稳健,但要注意有些原厂在擦除后会把标记区一起擦成0xFF,这会导致“重复擦除后标记消失”的假象。所以这块逻辑一定要基于原厂手册仔细核对,不能想当然。
6.4 放久了不读的盘,坏块会“变多”
这是我在售后数据里发现的一个反直觉现象。一批设备存放了几个月后开机,发现坏块数量比出厂时多了不少。起初怀疑是器件批量问题,后来排查才发现是电荷泄漏。长时间断电状态下,NAND单元里的电荷会慢慢衰减,导致读取时ECC错误变多。如果错误率超过阈值,巡检程序就会把这类块标记成坏块。
处理办法有两条:一是产品在长期存储前,尽量把数据重新刷写一遍,给电荷“续个命”;二是巡检判坏时不要把一次性读取错误直接定性为永久坏块,可以加一个“擦除-重写-再验证”的流程。很多时候,擦写一次之后块又能正常用了。
从我个人的实际经验看,坏块管理做得好的项目,不一定代码写得有多花哨,而是把每一条边界条件都想清楚了。比如掉电时序、扫描时机、误判保护、冗余策略,这些环节单独看都不难,难的是一起发生时系统依然能自洽。刚开始做嵌入式存储时,我也犯过“一张表打天下”的毛病,总觉得坏块管理就是启动时扫一次、查表跳过去就行,后来面对现场设备才明白,真正的可靠性是靠一层层冗余和兜底逻辑堆出来的。如果你正在设计存储方案,不妨先把这篇文章里提到的边界场景列成一份检查清单,逐条对照自己的实现,大概率能省下不少返工的功夫。