坏块管理策略:坏块表(BBT)设计、坏块跳过算法、坏块替换策略

好,咱们接着聊坏块管理。说实话,这是NAND Flash开发中最让人头疼,但也最体现功底的部分。你想想看,一块芯片出厂就有坏块,用着用着还会新出坏块,这活儿怎么干?

我早年做第一个NAND项目时,就吃过亏。当时觉得坏块管理嘛,不就是跳过坏块吗?结果产品跑了三个月,数据全乱了。后来才明白,坏块管理是个系统工程,从表怎么建、怎么查,到坏块来了怎么处理,每一步都有讲究。

一、坏块表(BBT)设计:你的“黑名单”怎么建?

坏块表,说白了就是一张“黑名单”。芯片里哪些块是坏的,全记在上面。但怎么记、记在哪、什么时候更新,这里头门道不少。

1. BBT的存储位置

我个人习惯把BBT放在两个地方:

  • 芯片内部:通常是每个块的第一个页(Page 0)的OOB区域,或者专门的系统块。出厂时厂商会标记初始坏块。
  • 系统内存:运行时加载到RAM里,方便快速查询。掉电后需要重新从Flash加载。

这里有个坑——BBT本身所在的块也可能坏掉。所以我建议至少保留两份BBT副本,一份在主区域,一份在备份区域。我曾经遇到过BBT所在块突然坏掉,结果整个系统找不到坏块信息,那叫一个惨。

2. BBT的数据结构

最简单的BBT就是一个位图(Bitmap)。每个块对应一个bit:0表示好块,1表示坏块。比如一块128MB的NAND,有1024个块,BBT只需要128字节。

// 坏块表位图示例
#define BLOCK_COUNT 1024
uint8_t bbt[BLOCK_COUNT / 8];  // 128字节

// 标记块N为坏块
void mark_bad_block(uint16_t block_num) {
    bbt[block_num / 8] |= (1 << (block_num % 8));
}

// 查询块N是否坏块
int is_bad_block(uint16_t block_num) {
    return (bbt[block_num / 8] >> (block_num % 8)) & 0x01;
}

嗯,这里要注意:位图虽然省空间,但扩展性差。如果芯片容量升级,块数变了,位图大小也得跟着改。我后来更喜欢用链表结构,每个坏块记录一个条目,灵活多了。

3. BBT的更新时机

BBT不是一成不变的。什么时候更新?我总结三个时机:

  • 初始化时:从Flash加载出厂坏块标记,生成初始BBT。
  • 擦除/写入失败时:如果操作返回错误,立即标记该块为坏块。
  • 定期扫描时:有些系统会定期检查块的健康状态,提前标记即将坏掉的块。
警告:千万不要在每次写入失败时都立刻写回Flash!频繁写BBT会加速Flash磨损。我一般先在内存里更新,等系统空闲时再批量写回。

二、坏块跳过算法:怎么绕过去?

有了BBT,接下来就是怎么用。坏块跳过算法,说白了就是“遇到坏块怎么办”。常见的策略有三种:

1. 线性跳过(Linear Skip)

这是最直接的方法。逻辑地址映射到物理地址时,如果遇到坏块,就跳到下一个好块。

// 线性跳过示例
uint16_t logical_to_physical(uint16_t logical_block) {
    uint16_t physical = logical_block;
    while (is_bad_block(physical)) {
        physical++;  // 跳过坏块
    }
    return physical;
}

优点:实现简单。缺点:地址映射关系会漂移。比如逻辑块0可能映射到物理块0,也可能映射到物理块5(如果前5个都是坏块)。这在文件系统层会造成混乱。

2. 保留块替换(Reserve Block Replacement)

这个方法更优雅。系统预留一部分好块作为“备胎”。遇到坏块时,从保留区拿一个好块替换。

核心思想:逻辑地址到物理地址的映射是固定的,坏块被替换到保留区,上层完全感知不到坏块的存在。

// 保留块替换示例
#define RESERVE_START 1000  // 保留区起始块号
uint16_t replace_table[RESERVE_COUNT];  // 替换映射表

uint16_t get_physical_block(uint16_t logical_block) {
    if (is_bad_block(logical_block)) {
        // 查找替换表
        for (int i = 0; i < RESERVE_COUNT; i++) {
            if (replace_table[i] == logical_block) {
                return RESERVE_START + i;
            }
        }
        // 分配新的保留块
        uint16_t reserve = allocate_reserve_block();
        replace_table[reserve - RESERVE_START] = logical_block;
        return reserve;
    }
    return logical_block;
}

我个人比较推荐这种方法。它保持了逻辑地址的连续性,上层软件不用做特殊处理。但代价是需要额外的保留空间和映射表。

3. 滑动窗口(Sliding Window)

这个方法介于两者之间。把整个Flash分成多个窗口,每个窗口内如果有坏块,就在窗口内滑动跳过。

比如每个窗口64个块,逻辑块0-63映射到物理块0-63。如果物理块5是坏块,那么逻辑块5映射到物理块6,逻辑块6映射到物理块7,以此类推。窗口内的块数不变,但地址偏移了。

小技巧:滑动窗口的窗口大小要选好。太小了,坏块多时窗口内好块不够用;太大了,地址漂移范围大。我一般选32或64,兼顾灵活性和稳定性。

三、坏块替换策略:换上去之后怎么办?

坏块替换不是简单的“换一个块”就完事了。替换之后,数据怎么迁移?映射怎么更新?这些都是问题。

1. 替换粒度

替换可以按块替换,也可以按页替换。块替换简单,但浪费空间(一个坏页导致整个块被替换)。页替换更精细,但管理复杂。

替换粒度 优点 缺点
块替换 实现简单,管理开销小 空间利用率低,一个坏页浪费整个块
页替换 空间利用率高 管理复杂,需要页级映射表

我做过一个项目,要求高可靠性,用了页替换。结果映射表占了大量RAM,得不偿失。后来改回块替换,虽然浪费了点空间,但系统稳定多了。所以不要盲目追求精细,要看实际需求。

2. 数据迁移

替换坏块时,如果坏块里还有有效数据,需要先把数据读出来,写到替换块里。这个过程叫数据迁移。

// 坏块替换与数据迁移
int replace_bad_block(uint16_t bad_block) {
    uint16_t reserve = allocate_reserve_block();
    if (reserve == INVALID) return -1;  // 没有可用保留块
    
    // 读取坏块中的有效数据
    uint8_t buffer[PAGE_SIZE];
    for (int page = 0; page < PAGES_PER_BLOCK; page++) {
        if (is_page_valid(bad_block, page)) {
            read_page(bad_block, page, buffer);
            write_page(reserve, page, buffer);
        }
    }
    
    // 更新映射表
    update_mapping(bad_block, reserve);
    
    // 标记坏块
    mark_bad_block(bad_block);
    
    return 0;
}

这里有个细节:数据迁移过程中如果掉电怎么办?我一般会加一个“迁移中”标志位。如果掉电后重启发现标志位被置位,就重新执行迁移。保证数据不丢。

3. 替换策略的选择

不同的应用场景,替换策略也不同:

  • 消费级产品:用线性跳过就够了,简单省资源。坏块率低,偶尔丢点数据也能接受。
  • 工业级产品:用保留块替换。可靠性优先,多花点成本也值得。
  • 企业级存储:用页替换+RAID-like保护。数据安全第一,性能和管理复杂度都是次要的。

我的经验:不要试图用一个策略解决所有问题。我做过一个车载项目,用了混合策略——大部分区域用线性跳过,关键数据区用保留块替换。既保证了性能,又保证了关键数据的可靠性。

四、知识体系总览

下面这张图,把坏块管理的核心逻辑串起来了。从BBT的建立,到坏块发现后的处理,再到替换策略的选择,每一步都有对应的技术方案。

坏块管理核心流程 坏块表(BBT)建立 加载出厂标记 + 运行时检测 坏块发现 擦除失败 / 写入失败 / 读取ECC错误 坏块跳过算法 线性跳过 / 滑动窗口 简单快速,地址会漂移 坏块替换策略 保留块替换 / 页替换 地址不变,需要映射表 数据迁移 读取旧块 → 写入新块 → 更新映射 映射更新 BBT更新 + 地址映射表更新 坏块管理完成

嗯,坏块管理这块内容确实不少。但说白了,核心就三件事:建表、查表、换块。建表要可靠,查表要快,换块要稳。把这三点做好了,NAND Flash的可靠性就有保障了。

我最后再啰嗦一句:不要迷信某个特定的策略。不同的芯片、不同的应用场景,适合的方案都不一样。多测试、多验证,找到最适合你项目的方案,这才是正道。


公众号:蓝海资料掘金营,微信deep3321