1,W25N01KV (1Gbit QSPI NAND) 为例,其物理特性如下(摘自数据手册):
| 参数 | 值 |
|---|---|
| 总容量 | 1 Gbit = 128 MByte |
| 页大小 (Page) | 2,048 字节主区 + 64 字节备用区 (OOB) |
| 每块页数 | 64 页 → 128 KB(主区) + 4 KB(OOB) |
| 块大小 (Block) | 128 KByte (2KB × 64)+4k |
| 总块数 | 1024 块 |
| ECC 能力 | 片上 4-bit ECC (可启用/禁用),内置 4-bit ECC,每512字节扇区独立 |
| 出厂坏块标记 | 第1页主区第1字节或备用区第1字节 ≠ 0xFF |
简单的ubi文件系统框架

-
MTD (Memory Technology Device) :内核抽象层,向上提供
mtd->_read、mtd->_write、mtd->_erase、mtd->_block_isbad等接口。 -
BBT (Bad Block Table) :存放物理坏块信息,来源包括出厂标记(每个块的第1页的 spare 区第1字节非 0xFF)以及运行时检测到的坏块。
-
BMT (Block Management Table) :可选层,实现逻辑块到物理块的动态映射,主要功能:
-
当某个物理块被标记为坏块时,从预留的备用块池中分配一个好块,映射给原来的逻辑地址。
-
上层 UBI 看到的是连续的逻辑块,不感知底层物理坏块。
-
-
UBI (Unsorted Block Images) :在 MTD 之上提供卷管理和磨损均衡,本身也维护自己的坏块表(通过 MTD 的
_block_isbad接口)。
2,以 W25N01KV 为例:
-
块大小 = 128 KB = 0x20000 字节
-
镜像文件总大小假设为 32 MB,对应 256 个逻辑块(0~255)。
问题块 :逻辑块 134(即 UBI 内部编号)。
其在镜像文件中的起始偏移地址(假设无坏块时)为:
Offset = 逻辑块号 × 块大小 = 134 × 0x20000 = 0x10C0000 (17,563,648 字节)
实际情况:
-
物理块 134 是坏块(出厂或使用中产生)。
-
BMT 将逻辑块 134 映射到物理块 135。
-
烧录器在写入镜像时,物理块 134 被跳过,原本属于逻辑块 134 的数据被写入了物理块 135。
-
但是
_block_isbad(134)查询物理块 134 → 返回坏块 → UBI 废弃逻辑块 134 → 偏移 0x10C0000 处的数据在 UBI 看来"丢失"。
如果坏块检查函数 mtd->_block_isbad 没有查询 BMT,而是直接检查物理块 134,就会返回"坏块"给 UBI。UBI 会标记该逻辑块为坏块,不再使用其中的数据
mtd->_read→spi_nand_read→nandflash_read(from, len, retlen, buf, &status);
mtd->_block_isbad→spi_nand_block_isbad→en75122_nand_check_block_bad(offs, 0);
逻辑块 134 的数据实际位于物理块 135(因为物理块 134 是坏块,被 BMT 跳过)。
-
当 UBI 要读取逻辑块 134 时,调用
_read→ BMT 映射 → 读物理块 135 → 成功读到数据。 -
当 UBI 要检查逻辑块 134 是否为坏块时,调用
_block_isbad(134)→ 直接查物理块 134 的状态 → 物理块 134 是坏块 → 返回"是坏块"。// 读操作:使用了 BMT 映射
static int nandflash_read(loff_t from, size_t len, ...)
{
uint32_t logical_block = from / BLOCK_SIZE;
uint32_t physical_block = bmt_logical_to_physical(logical_block);
// 从 physical_block 读取数据
}// 坏块检查:未使用 BMT 映射
static int w25n01kv_block_isbad(struct mtd_info *mtd, loff_t ofs)
{
struct spi_nand *spinand = mtd_to_spinand(mtd);
u32 block = ofs >> mtd->erasesize_shift;// **错误实现**:直接检查物理块,没有经过 BMT // 正确应该: phys_block = bmt_logical_to_physical(spinand, block); return spi_nand_check_block_bad(spinand, block); // block 是物理地址}
BMT 映射表示例(内存结构)
struct bmt_entry {
u16 logical; // 逻辑块号 (0~1023)
u16 physical; // 物理块号 (0~1023)
u8 status; // 0: good, 1: bad, 2: reserved
};
// 启动时建立映射:扫描物理块,跳过坏块,顺序映射到逻辑块
static int bmt_build_table(struct spi_nand *spinand)
{
u16 logical = 0;
for (u16 phys = 0; phys < total_blocks; phys++) {
if (spi_nand_check_block_bad(spinand, phys)) {
// 坏块:不分配逻辑块号,将物理块加入坏块池
bmt_add_bad_block(phys);
} else {
bmt_map[logical].logical = logical;
bmt_map[logical].physical = phys;
logical++;
}
}
// 剩余好块数量应等于逻辑块数
}
(1)bmt的实现
BMT 映射 :只负责将逻辑访问重定向到好块,但坏块标记本身仍需要被读取 (例如判断是否坏块时,可能仍会读取该物理块的标记区域)。在本次问题中,_block_isbad 直接查询了物理坏块,而没
BMT 初始化与配置
struct bmt_config {
bool enable; // 是否启用 BMT
u16 reserved_blocks; // 备用块数量(用于替换坏块)
u16 start_logical_block; // 起始逻辑块号(通常为 0)
// 坏块替换策略:顺序分配、磨损均衡等
};
开启 ECC 与坏块管理相关寄存器配置
W25N01KV 内部 ECC 默认使能(ECC-E=1)。相关配置寄存器:
| 寄存器 | 地址 | 位 | 设置 | 作用 |
|---|---|---|---|---|
| Configuration Register (SR-2) | Bxh | ECC-E (bit 4) | 1 (默认) | 启用内部 4-bit ECC |
| Protection Register (SR-1) | Axh | BP[3:0], TB | 0 | 禁用写保护,允许全片擦写 |
| Status Register-3 | Cxh | ECC status | 只读 | 检测 ECC 校正情况 |
坏块跳过过程
假设物理块 134 是坏块,物理块 135 是好块:
| 逻辑块号 | 映射物理块号 |
|---|---|
| 0 | 0 |
| ... | ... |
| 133 | 133 |
| 134 | 135 |
| 135 | 136 |
| ... | ... |
关键 :逻辑块数量 = 总好块数。坏块不占用逻辑块号,因此逻辑块空间是连续的,但背后物理位置有空洞。
// 写操作前检查
int spi_nand_write(struct spi_nand *spinand, loff_t to, size_t len, const u_char *buf)
{
u32 phys_block = to >> mtd->erasesize_shift;
// 1. 查询 BBT
if (bbt_is_bad(phys_block)) {
// 2. 如有 BMT,尝试重映射
if (bmt_enabled) {
phys_block = bmt_get_alternative(spinand, phys_block);
} else {
return -EIO;
}
}
// 3. 执行写入
int ret = do_write(spinand, phys_block, ...);
// 4. 写入失败则标记坏块并触发重映射
if (ret == -EIO) {
bbt_mark_bad(phys_block);
bmt_replace_block(spinand, phys_block);
}
return ret;
}
static int w25n01kv_block_isbad(struct mtd_info *mtd, loff_t ofs)
{
struct spi_nand *spinand = mtd_to_spinand(mtd);
u32 logical_block = ofs >> mtd->erasesize_shift;
u32 phys_block = bmt_logical_to_physical(spinand, logical_block);
if (phys_block == BMT_INVALID)
return 1; // 无有效物理块,视为坏
return spi_nand_check_block_bad(spinand, phys_block);
}
total_logical_blocks 的计算方法
total_logical_blocks 表示可供上层文件系统使用的逻辑块数量。其计算公式为:
total_logical_blocks = 总物理块数 - 预留备用块数 - 出厂坏块数(动态处理)
但更常用的设计是:
total_logical_blocks = 总物理块数 - 预留备用块数
如果预留备用块数 reserved_blocks = 20(经验值,可根据产品寿命调整则 total_logical_blocks = 1024 - 20 = 1004
g_bmt->logical_to_phys = kmalloc_array(total_logical_blocks, sizeof(u16), GFP_KERNEL);
动态坏块替换(写入失败时的处理)
当物理块在写入过程中失败(如擦除或编程超时),需要将该块标记为坏,并从空闲池中分配一个新块,更新映射表,并可能将原逻辑块数据迁移过去。
/**
* bmt_replace_block - 替换坏物理块,并更新映射
* @logical: 受影响的逻辑块号
* @bad_phys: 发生错误的物理块号
*
* 返回值: 新分配的物理块号,失败返回 BMT_INVALID
*/
u16 bmt_replace_block(u16 logical, u16 bad_phys)
{
u16 new_phys;
if (!g_bmt || g_bmt->free_count == 0) {
pr_err("BMT: no free block for replacement!\n");
return BMT_INVALID;
}
/* 从空闲池取一个新块 */
new_phys = g_bmt->free_pool[--g_bmt->free_count];
/* 更新映射表:逻辑块现在指向新物理块 */
g_bmt->logical_to_phys[logical] = new_phys;
/* 标记坏物理块 */
set_bit(bad_phys, (unsigned long *)g_bmt->phys_bad);
/* 可选:将坏块标记写入硬件(如写 OOB 标记) */
spi_nand_mark_bad_block(g_bmt->spinand, bad_phys);
pr_info("BMT: logical %u remapped from phys %u to phys %u\n",
logical, bad_phys, new_phys);
return new_phys;
}
分区表(逻辑地址)
| 分区名 | 起始逻辑块 | 逻辑块数 | 大小 (KB) | 用途 |
|---|---|---|---|---|
| bootloader | 0 | 8 | 1024 | 1MB 启动代码 |
| env | 8 | 4 | 512 | U-Boot 环境变量 |
| kernel | 12 | 32 | 4096 | Linux 内核 |
| dtb | 44 | 2 | 256 | 设备树 |
| rootfs | 46 | 300 | 38400 | UBIFS 根文件系统 |
| data | 346 | 剩余 | 可变 | 用户数据 |
物理块状态表(简化)
| 物理块号 | 状态 | 映射逻辑块号 |
|---|---|---|
| 0 ~ 132 | 好块 | 0 ~ 132 |
| 133 | 坏块 | 无 |
| 134 ~ 1003 | 好块 | 133 ~ 1002 |
| 1004 ~ 1023 | 好块(备用池) | 无(空闲) |
逻辑块到物理块的映射
| 逻辑块号 | 物理块号 |
|---|---|
| 0 | 0 |
| 1 | 1 |
| ... | ... |
| 132 | 132 |
| 133 | 134 ← 跳过了物理块 133 |
| 134 | 135 |
| ... | ... |
| 1002 | 1003 |
OOB分析和说明:
总逻辑块数 = 1003(本例中只用了 1003 个好块,剩余 20 个备用块)。
-
开启 ECC 后逻辑块大小仍然是 128 KB,ECC 数据存储在 OOB 区,不占用用户数据空间。
-
逻辑块地址是连续的,物理块地址可能不连续,由 BMT 负责映射。
-
分区配置应基于逻辑块地址,烧录和运行时访问都是逻辑地址。
-
坏块跳过由 BMT 在初始化时完成,将好块顺序映射为逻辑块,坏块不参与映射。
-
备用块池用于动态替换运行时新产生的坏块,确保逻辑容量稳定。