基于 W25N01KV 的 MTD/BBT/BMT/UBI 框架与坏块导致系统挂死问题剖析

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->_readmtd->_writemtd->_erasemtd->_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->_readspi_nand_readnandflash_read(from, len, retlen, buf, &status);

mtd->_block_isbadspi_nand_block_isbaden75122_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 BP3: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 个备用块)。

  1. 开启 ECC 后逻辑块大小仍然是 128 KB,ECC 数据存储在 OOB 区,不占用用户数据空间。

  2. 逻辑块地址是连续的,物理块地址可能不连续,由 BMT 负责映射。

  3. 分区配置应基于逻辑块地址,烧录和运行时访问都是逻辑地址。

  4. 坏块跳过由 BMT 在初始化时完成,将好块顺序映射为逻辑块,坏块不参与映射。

  5. 备用块池用于动态替换运行时新产生的坏块,确保逻辑容量稳定。

相关推荐
m0_547486662 小时前
《ARM Cortex-M4嵌入式应用技术——基于STM32F407、STM32CubeMX与Proteus》全套PPT课件
arm开发·stm32·proteus
Lanceli_van2 小时前
SQLite 3.45.2(sqlite-autoconf-3450200)ARM 交叉编译完整步骤
arm开发·sqlite
暮云星影3 小时前
全志linux开发屏幕适配(二)`HDMI`驱动适配说明
linux·arm开发·驱动开发
暮云星影4 小时前
瑞芯微rk3566开发FIT Secure Boot
linux·arm开发·驱动开发·安全
zlinear数据采集卡6 小时前
双核架构深度解析:ARM+FPGA如何让数据采集卡实现500Ksps高性能?
arm开发·fpga开发·架构
暮云星影7 小时前
全志linux开发 USB接口设置
linux·arm开发·驱动开发
暮云星影1 天前
全志开发环境搭建及编译构建
linux·arm开发·驱动开发
振南的单片机世界1 天前
PWM调压调速,H桥换向:直流电机四象限控制
arm开发·stm32·单片机·嵌入式硬件
暮云星影1 天前
全志T507sdk结构梳理及开发步骤总结
linux·arm开发
振南的单片机世界2 天前
RS485组网三要素:负载、距离、终端电阻
arm开发·stm32·单片机·嵌入式硬件