坏块管理策略:坏块表(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。
- 擦除/写入失败时:如果操作返回错误,立即标记该块为坏块。
- 定期扫描时:有些系统会定期检查块的健康状态,提前标记即将坏掉的块。
二、坏块跳过算法:怎么绕过去?
有了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,以此类推。窗口内的块数不变,但地址偏移了。
三、坏块替换策略:换上去之后怎么办?
坏块替换不是简单的“换一个块”就完事了。替换之后,数据怎么迁移?映射怎么更新?这些都是问题。
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的建立,到坏块发现后的处理,再到替换策略的选择,每一步都有对应的技术方案。
嗯,坏块管理这块内容确实不少。但说白了,核心就三件事:建表、查表、换块。建表要可靠,查表要快,换块要稳。把这三点做好了,NAND Flash的可靠性就有保障了。
我最后再啰嗦一句:不要迷信某个特定的策略。不同的芯片、不同的应用场景,适合的方案都不一样。多测试、多验证,找到最适合你项目的方案,这才是正道。
公众号:蓝海资料掘金营,微信deep3321