基于 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 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 个备用块)。

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

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

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

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

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

相关推荐
szxinmai主板定制专家1 天前
基于ZYNQ MPSOC图像采集与压缩系统总体设计方案
linux·arm开发·人工智能·嵌入式硬件·fpga开发
底层开发智库1 天前
学习ARM新姿势,理论实践的结合
arm开发
szxinmai主板定制专家2 天前
基于ZYNQ MPSOC ARM+FPGA的超高清实时图像采集与压缩系统设计
linux·运维·服务器·arm开发·人工智能·嵌入式硬件·fpga开发
加油20192 天前
嵌入式软件技术栈和学习路线详解
linux·arm开发·数据结构·mqtt·设计模式·嵌入式
虹科汽车电子4 天前
高效传感器与ECU研发测试方案:虹科PSI5模拟器加速智能安全系统落地
arm开发·安全·seskion
szxinmai主板定制专家4 天前
RK3568 + CODESYS+实时系统运动控制器PLC,支持 AI 视觉目标检测,预测性维护,混合多系统部署,多路模拟量采集
arm开发·人工智能·嵌入式硬件·fpga开发
XMAIPC_Robot5 天前
深度无人机自动驾驶仪,中小型无人机硬件在环仿真飞行
运维·arm开发·人工智能·fpga开发·无人机·边缘计算
番茄灭世神5 天前
Vscode开发/调试ARM单片机最新教程
c语言·arm开发·vscode·stm32·嵌入式·gd32
猫猫的小茶馆5 天前
【Python】函数与模块化编程
linux·开发语言·arm开发·驱动开发·python·stm32