1. 为什么OOB这块“边角料”值得单独拎出来讲
搞嵌入式存储的朋友多半有过这种体验:明明主存储区读写都正常,文件系统却时不时报错,或者设备跑久了数据莫名其妙对不上。排查半天,最后发现问题出在NAND Flash的OOB区域上。OOB,全称Out-Of-Band,中文一般叫“带外区域”或“冗余区”,是NAND Flash每个页(Page)旁边附带的一小块额外存储空间。以常见的SLC NAND为例,一个2KB的页通常配64字节OOB;MLC/TLC的页更大,OOB也相应增加到几百字节。这块空间不在主数据区里,用户正常读写数据时不会碰到它,但它在底层承担着纠错码存储、坏块标记、文件系统元数据备份等关键职责。
很多人第一次接触OOB是在调试UBI或者YAFFS文件系统的时候,发现挂载失败,日志里冒出一堆ECC相关的报错,才意识到这块“边角料”不是可有可无的。实际上,OOB区域的设计直接关系到NAND Flash的可靠性、寿命和文件系统的稳定性。你把它理解成每个页的“病历本”就对了——主数据区是病人的身体,OOB记录着这个页的健康状况、有没有坏道、纠错信息够不够用。病历本写乱了,医生就没法正确诊断。
这篇内容适合几类人看:一是刚接触NAND Flash驱动开发的工程师,需要搞清楚OOB到底怎么用;二是在做嵌入式文件系统移植的开发者,被UBI/ YAFFS的OOB配置坑过;三是对存储底层感兴趣、想弄明白ECC和坏块管理机制的爱好者。我会从OOB的物理布局讲起,拆解ECC纠错码在OOB里的排布方式,再落到文件系统层面怎么利用这块区域做优化,中间穿插实操中踩过的坑和参数选择的逻辑。不堆砌术语,尽量用实际场景把原理讲透。
2. OOB区域的物理布局与核心职责拆解
2.1 一个页的“户型图”:主区与OOB怎么分
要理解OOB,先得看清NAND Flash一个页的内部结构。以一款典型的2Gb SLC NAND为例,页大小2048字节,OOB 64字节。这64字节不是随便用的,厂商在数据手册里会给出推荐的划分方式。常见的布局是这样的:前一部分留给ECC校验码,中间几个字节做坏块标记,剩下的预留给文件系统或厂商自定义使用。
具体来说,如果采用BCH纠错算法、每512字节数据需要一定长度的ECC码,那么2048字节主数据分成4个扇区,每个扇区配一段ECC码,加起来可能占掉OOB的一半甚至更多。剩下的字节里,第一个字节的特定比特位通常被用作坏块标记——注意,坏块标记不是每个页都写,而是每个块(Block)的第一个页或第二个页的OOB里写。这个细节很关键,后面讲坏块管理时会展开。
我用一个表格把典型SLC NAND的OOB布局列出来,方便对照:
| OOB字节偏移 | 用途 | 说明 |
|---|---|---|
| 0-1 | 坏块标记 | 非0xFF表示坏块,通常写在块内第一个页 |
| 2-3 | 文件系统元数据 | 部分文件系统用来存逻辑地址映射 |
| 4-39 | ECC校验码 | 按扇区划分,每512字节配一段 |
| 40-63 | 预留/厂商自定义 | 可用于掉电保护标记、版本号等 |
这个表不是标准答案,不同厂商、不同ECC强度下划分会变。但逻辑是通的:ECC码占大头,坏块标记占固定位置,剩下的才是文件系统和用户可用的。你在写驱动的时候,第一件事就是翻数据手册确认这个布局,别自己拍脑袋定。
2.2 ECC纠错码为什么必须住在OOB里
有人会问,ECC码为什么不放在主数据区,非要塞到OOB?这涉及NAND的读写粒度问题。NAND的读操作是以页为单位整页读出的,ECC校验需要在数据离开芯片、进入控制器的那一刻就完成。如果把ECC码和用户数据混在主区,控制器没法区分哪部分是数据、哪部分是校验码,校验逻辑会乱套。OOB的设计让主区和校验区物理分离,控制器读一页时,主区数据走数据通道,OOB里的ECC码走校验通道,各司其职。
更深一层的原因是坏块管理和ECC的独立性。坏块标记必须在块的生命周期内稳定存在,哪怕主数据区被反复擦写,坏块标记也不能丢。OOB区域在擦除操作时是否被一并擦除,取决于芯片设计——多数NAND在擦除块时会把OOB一起擦掉,所以坏块标记需要在驱动层做保护,擦除前先备份、擦除后回写。这个机制如果没处理好,坏块标记丢了,整个坏块管理就崩了。
实际调试中我遇到过一种情况:某款NAND在擦除后OOB全变成0xFF,驱动没有做坏块标记的回写,结果跑了一段时间后坏块表全乱了,文件系统开始往坏块里写数据,最终数据损坏。这个坑的根源就是对OOB擦除行为的理解不到位。所以你在选型阶段就要确认:这颗NAND擦除时OOB是否保留?如果不保留,驱动里必须加备份回写逻辑。
2.3 坏块标记的存放规则与读取时机
坏块标记的存放有个约定俗成的规则:一般写在每个块第一个页的OOB里,偏移位置由厂商指定,常见的是OOB的第0字节。判断逻辑是:读出该字节,如果值不是0xFF,就认为这个块是坏块。为什么用0xFF作为“好块”的默认值?因为NAND擦除后的初始状态就是全1,0xFF代表未被写过。厂商在出厂测试时发现坏块,会把对应位置写成非0xFF值做标记。
读取坏块标记的时机很讲究。上电初始化时,驱动需要扫描整个NAND的坏块表,这个扫描过程只读每个块第一个页的OOB,不读主数据,速度相对快。但扫描本身也有开销,大容量NAND全盘扫描可能要几百毫秒甚至更久。有些方案会把坏块表缓存在内存里,后续操作查内存表,只有遇到新坏块才更新。这里有个经验:扫描时不要只信第一个页的标记,有些老化块可能在第二个页也做了标记,稳妥的做法是前两个页都检查一遍,任一标记为非0xFF就判坏。
还有一个容易忽略的点:坏块标记的写入是一次性的,不能反复擦写。厂商出厂标记的坏块,用户层不应该去清除或改写这个标记。我见过有人为了“修复”坏块,把标记改回0xFF,结果那个块确实能用一阵子,但很快又坏了,而且因为标记被清掉,坏块管理逻辑不再跳过它,数据写进去就丢。坏块就是坏块,标记是保护机制,不是障碍。
3. ECC纠错机制与OOB的深度绑定关系
3.1 从汉明码到BCH:纠错强度决定OOB占用
ECC算法的选择直接决定OOB里要留多少字节给校验码。早期SLC NAND用汉明码,纠错能力弱,每512字节只需要几个字节的ECC码,OOB压力小。但随着工艺制程进步,NAND的存储单元越来越小,每个单元能存的电子数减少,误码率上升,汉明码不够用了,BCH码成为主流。BCH码的纠错位数可以配置,常见的有4bit、8bit、16bit甚至24bit纠错。
纠错位数越高,需要的ECC码越长。以BCH为例,每512字节数据,4bit纠错大约需要7字节ECC码,8bit纠错需要13字节左右,16bit纠错则要26字节上下。你算一下:2KB页分成4个512字节扇区,如果每个扇区配16bit纠错的ECC码,光ECC就占掉104字节,而OOB总共才64字节——根本放不下。这就是为什么大页NAND的OOB也会相应增大,或者采用更高效的LDPC码。
实际选型时有个简单的估算方法:先确定主数据页大小和扇区划分,再根据NAND厂商推荐的ECC强度算出每扇区ECC码长度,乘以扇区数,看是否超过OOB容量。如果超了,要么降低纠错位数(牺牲可靠性),要么换OOB更大的芯片,要么用LDPC这种编码效率更高的方案。这个计算在硬件设计阶段就要做,等板子回来发现ECC放不下就麻烦了。
3.2 ECC码在OOB里的排布方式与读写对齐
ECC码在OOB里的排布不是简单拼接,而是和主数据的扇区一一对应。控制器读一页时,按扇区粒度分别计算ECC,再和OOB里对应位置的ECC码比对。这就要求OOB的读写必须和主数据同步,不能只读主数据不读OOB,否则校验无从谈起。
这里有个实操细节:很多NAND控制器支持“带OOB读”和“不带OOB读”两种模式。带OOB读时,控制器一次性把主数据和OOB都读出来,硬件自动完成ECC校验,如果纠错成功就把纠正后的数据放到缓冲区,同时报告纠错状态(比如纠正了几位、是否不可纠)。不带OOB读则跳过校验,直接拿原始数据。调试阶段建议始终用带OOB读,哪怕性能稍低,也要确保数据可信。等系统稳定了,再针对特定场景评估是否关闭校验换性能。
写操作同理,带OOB写时控制器会自动生成ECC码并填入OOB对应位置,用户不需要手动算ECC。但要注意,如果你在文件系统层往OOB里写了自定义数据(比如YAFFS的标签),要确认这些数据不会和ECC码的位置冲突。有些控制器允许用户指定OOB中哪些字节由硬件管理、哪些留给软件,这个配置在驱动初始化时就要定好,中途改会出乱子。
3.3 纠错失败时的OOB现场信息价值
ECC纠错不是万能的,当误码位数超过纠错能力时,校验会失败。这时候OOB里的信息就成了排查线索。好的控制器会报告“不可纠错误”发生在哪个扇区,结合OOB里该扇区的ECC码,可以判断是数据真的坏了还是ECC码本身被写坏了。
我处理过一次批量数据异常,日志显示某个块的页频繁报不可纠错误。把出问题的页OOB dump出来看,发现ECC码区域的值和正常页明显不同,进一步查发现是驱动在写这个页时OOB缓冲区没清零,残留了上一页的ECC码,导致写入的校验码是错的。这种问题光看主数据根本发现不了,必须把OOB一起抓出来分析。所以调试工具里一定要有OOB dump功能,出问题时第一时间保存现场。
另外,有些高级NAND支持“读取重试”(Read Retry)机制,当ECC纠错失败时,控制器会调整读参考电压重新读,多次重试仍失败才判死。这个过程中OOB里的ECC码会被反复比对,如果某次重试后校验通过了,说明数据还在,只是读窗口偏了。这个机制对延长NAND寿命很有帮助,但前提是OOB里的ECC码本身是完好的。
4. 文件系统层如何榨干OOB的剩余价值
4.1 UBI/UBIFS对OOB的依赖与配置要点
UBI(Unsorted Block Images)是嵌入式领域常用的NAND管理层的,它在MTD层之上做坏块管理和磨损均衡,UBIFS则是建立在UBI之上的文件系统。UBI对OOB的使用有明确要求:它需要知道OOB里哪些字节可用、ECC占了多少、坏块标记在哪。这些信息通过MTD设备的参数传进去,配置错了UBI直接挂载失败。
关键参数是vid_hdr_offset和data_offset。UBI会在每个页的OOB里存一个VID头(Volume Identifier Header),记录这个页属于哪个逻辑卷、什么类型。这个VID头必须放在OOB中不被ECC和坏块标记占用的区域。配置时你要根据芯片的OOB布局算出偏移量,填到内核参数或设备树里。填错了,UBI读出来的VID头是乱的,挂载时报“bad VID header”之类的错误。
我的经验是:先在驱动层把OOB的可用区域摸清楚,画一张字节地图,标出坏块标记、ECC、预留区各占哪段,然后从空闲段里划一块给UBI的VID头。划的时候留点余量,别紧贴着ECC区,防止不同ECC配置下越界。UBI的VID头一般需要几十字节,具体看版本,留64字节以上的空闲OOB比较稳妥。
4.2 YAFFS2的标签存储与OOB的紧密耦合
YAFFS2是另一种常见的NAND文件系统,它的设计哲学和UBI不同——YAFFS2直接把文件系统的元数据(称为tag)存在OOB里。每个页的OOB中有一段专门放tag,记录这个页对应的文件ID、页内偏移、序列号等信息。这样设计的好处是挂载时只读OOB就能重建文件系统树,不用扫描主数据区,速度快。
但这也意味着YAFFS2对OOB布局极其敏感。如果OOB里tag的位置和ECC码冲突,或者坏块标记占了tag要用的字节,YAFFS2就没法正常工作。移植YAFFS2时,必须根据具体NAND的OOB布局修改它的yaffs_oobinfo结构体,把tag的偏移和长度配对。配错了的表现是挂载后文件列表为空或者文件内容错乱,因为tag读出来是垃圾值。
我移植YAFFS2时踩过一个坑:某款NAND的OOB第0字节是坏块标记,但YAFFS2默认配置把tag从第0字节开始放,结果tag的第一个字节被坏块标记覆盖,导致部分块的tag解析错误。解决办法是把tag起始偏移往后挪,跳过坏块标记区。这个调整在YAFFS2的配置头文件里改一个宏就行,但前提是你得知道坏块标记在哪——又回到那句话,先看数据手册。
4.3 利用OOB做掉电保护与版本标记的实践
除了文件系统自带的用法,OOB的预留区还能做不少文章。一个典型场景是掉电保护:设备在写数据过程中突然断电,可能导致页写了一半,主数据和ECC不匹配。如果在OOB的预留字节里写一个“写完成标记”,上电时检查这个标记,就能判断该页是否写完。标记的写法可以是:写数据前先把标记清成特定值,写完后再改成完成值。上电扫描时发现标记是“未完成”状态,就知道这页数据不可信,需要从备份恢复或丢弃。
另一个用法是版本标记。固件升级场景下,新固件写入NAND后,可以在OOB预留区写一个版本号,启动时比对版本号决定加载哪份固件。这样比单独划一个存储区存版本号更省空间,而且和固件数据绑定在同一个页上,不会出现版本号和固件内容不一致的情况。
不过这些自定义用法有个前提:你得确保文件系统或管理层的OOB使用不会覆盖你的预留区。最稳妥的做法是在整个软件栈里维护一份OOB布局约定文档,驱动层、文件系统层、应用层都按这个约定来,谁也别越界。我见过项目里两个团队各自往OOB预留区写东西,互不知情,最后数据互相踩踏,查了一周才定位到。
5. 实操全流程:从裸片测试到文件系统挂载
5.1 裸片OOB读写测试与布局验证
拿到一颗新NAND,别急着上文件系统,先用裸片测试把OOB的脾气摸清楚。测试分几步走。第一步,擦除一个块,然后把整个块的OOB读出来,确认擦除后OOB是否全0xFF。如果不是,说明芯片有特殊行为,得查手册。第二步,往主数据区写一页已知数据,读回时带OOB,检查ECC码区域是否被硬件正确填充,坏块标记位置是否保持0xFF。第三步,故意往OOB的ECC区写错误值,再读主数据,确认控制器报ECC错误——这验证了ECC校验确实在工作。
测试代码用MTD字符设备的ioctl就能做,核心是MEMREADOOB和MEMWRITEOOB两个命令。下面是一段简化的测试逻辑,展示怎么读一个页的OOB:
#include <mtd/mtd-abi.h> #include <fcntl.h> #include <sys/ioctl.h> int fd = open("/dev/mtd0", O_RDWR); struct mtd_oob_buf oob; unsigned char oob_data[64]; oob.start = page_addr; // 目标页的起始地址 oob.length = 64; // OOB长度 oob.ptr = oob_data; if (ioctl(fd, MEMREADOOB, &oob) < 0) { perror("read oob failed"); } // oob_data 里就是该页的OOB内容跑完测试,把OOB的字节分布画出来,和手册对照,确认坏块标记、ECC、空闲区的位置。这一步花半小时,能省掉后面几天的调试。
5.2 驱动层OOB缓冲区管理与ECC配置
驱动层要管好两块东西:OOB缓冲区的大小和ECC引擎的配置。OOB缓冲区按芯片实际OOB大小分配,别抠门,多留几个字节做对齐。ECC配置包括算法选择(BCH还是LDPC)、纠错位数、扇区划分。这些参数在驱动 probe 阶段根据设备树或平台数据设定,设定后不要动态改,改了容易和已写入的数据不兼容。
一个容易忽略的点是OOB缓冲区的初始化。每次读写OOB前,把缓冲区清零或填0xFF,避免残留数据干扰。前面提到的ECC码写错问题,根源就是缓冲区没清。另外,如果驱动支持DMA,OOB缓冲区的物理地址要满足DMA对齐要求,否则传输可能出错。这些细节在驱动模板里通常有注释,但新手容易跳过。
ECC配置还要和文件系统对齐。比如UBI要求ECC至少能纠4bit,你配了2bit,UBI可能拒绝工作。YAFFS2对ECC的要求相对宽松,但tag区域的读写必须绕过ECC或者保证ECC不覆盖tag。这些约束在选文件系统时就要确认,别等驱动写完了才发现不匹配。
5.3 文件系统挂载参数调优与OOB空间核算
文件系统挂载时,OOB空间的核算是最后一道关。以UBI为例,挂载前用ubiformat工具格式化时,它会根据MTD参数计算可用空间。如果OOB配置不对,ubiformat会报错或者格式化出来的UBI卷容量异常。核算方法是:总OOB减去坏块标记、ECC码、VID头占用的字节,剩下的才是可用空间。这个值要和UBI的预期对上。
挂载参数里有个mtd_oobavail的概念,表示OOB中可供文件系统自由使用的字节数。这个值填小了浪费空间,填大了越界踩ECC。我的做法是先用保守值跑通,再用ubiattach的调试输出确认实际用量,逐步调到准确值。调的时候每次改一个参数,改完做一次完整的写入-断电-重启-校验流程,确保掉电后数据可恢复。
6. 常见问题排查与避坑经验实录
6.1 OOB相关故障速查表
下面这张表是我这些年遇到过的OOB典型问题,按现象、可能原因、排查手段整理,方便对照:
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
| 文件系统挂载失败,报ECC错误 | OOB中ECC码位置与驱动配置不符 | dump OOB,对照手册检查ECC区 |
| 坏块表异常,好块被跳过 | 擦除后坏块标记丢失,未回写 | 检查擦除流程是否备份回写OOB |
| YAFFS2挂载后文件为空 | tag偏移与坏块标记冲突 | 调整tag起始偏移,避开标记区 |
| 写入数据读回校验失败 | OOB缓冲区残留旧ECC码 | 写前清零OOB缓冲区 |
| UBI报bad VID header | VID头偏移配置错误 | 核算OOB空闲区,修正偏移参数 |
| 掉电后部分页数据损坏 | 无写完成标记,半写页被误用 | 在OOB预留区加写完成标记 |
这张表建议存下来,下次遇到类似现象先对照一遍,能省不少时间。
6.2 三个血泪教训换来的避坑技巧
第一个教训关于坏块标记的保护。前面提过,擦除会清掉OOB,坏块标记必须备份回写。但回写时机有讲究:不能在擦除后立刻回写,因为如果回写过程中断电,标记可能写坏。稳妥的做法是擦除后先不回写,等这个块真正要被写入数据时,在写数据前的准备阶段回写标记。这样即使回写时断电,块里也没有有效数据,损失可控。
第二个教训关于ECC强度与OOB容量的平衡。有个项目为了追求可靠性,把ECC纠错位数拉到最高,结果OOB放不下,驱动自动截断了部分ECC码,导致高扇区的纠错能力实际下降,反而更不可靠。后来降到中等强度,ECC完整放入OOB,整体可靠性反而更好。ECC不是越强越好,要算清楚OOB能不能装下。
第三个教训关于OOB布局的文档化。团队里驱动、文件系统、应用三个层面都碰OOB,没有统一文档,各写各的,最后冲突。后来强制要求每次改OOB用法必须更新布局文档,代码评审时对照检查。这个习惯看起来笨,但确实避免了多次潜在事故。
6.3 上电扫描与坏块表重建的实操建议
设备长期运行后,坏块表可能因为各种原因丢失或不一致,需要重建。重建流程是:全盘扫描每个块第一个页的OOB,读坏块标记,生成新表。扫描时要注意两点:一是扫描期间不要进行写操作,避免边扫边写导致状态不一致;二是扫描结果和旧表比对,如果新表比旧表多出大量坏块,说明NAND可能整体老化,要考虑更换芯片而不是简单更新表。
扫描的耗时和容量成正比,大容量NAND全盘扫描可能影响启动时间。优化办法是只扫描有变化的区域,或者把坏块表定期持久化到可靠存储里,上电时先加载持久化表,再做增量扫描。增量扫描的实现需要在每次发现新坏块时更新持久化表,这个逻辑要小心处理掉电场景,避免表写了一半。
7. 一些关于OOB的个人体会
折腾NAND这些年,我越来越觉得OOB区域像是一个系统的“暗物质”——平时看不见摸不着,但它的状态决定了整个存储系统能不能稳住。很多数据损坏的根因不在主数据路径上,而在OOB的某个字节被写错、某个标记被覆盖、某段ECC码没对齐。排查这类问题,光看应用层日志没用,得往下钻到OOB字节级别。
我的习惯是:每接触一款新NAND,第一件事就是把OOB布局画出来贴在工位上,驱动、文件系统、应用各用哪段标得清清楚楚。这个习惯帮我躲过了好几次潜在的冲突。另外,调试工具里一定要有OOB dump和对比功能,出问题时先存现场再分析,别急着重启把现场冲掉。
最后分享一个小技巧:如果你不确定某个OOB字节的用途,可以做一个对照实验——写一页数据,dump OOB;再写另一页不同数据,再dump;对比两次OOB的差异,差异位置大概率是ECC码(随数据变化),不变的位置可能是坏块标记或预留区。这个方法虽然土,但在没有完整文档的时候特别好用。