基于 Linux 内核源码(主线 master 分支)实际读取分析。 主要源文件:
include/linux/mtd/mtd.h、drivers/mtd/mtdcore.c、drivers/mtd/nand/raw/nand_base.c、include/linux/mtd/rawnand.h、include/linux/mtd/nand.h、drivers/mtd/ubi/ubi.h、include/linux/mtd/ubi.h、drivers/mtd/spi-nor/core.c、include/linux/mtd/spi-nor.h、fs/ubifs/ubifs.h
- MTD 子系统概述
- MTD 层次架构
- 核心数据结构 mtd_info
- MTD 操作接口
- OOB(Out-Of-Band)区域
- NAND Flash 驱动层
- nand_chip 结构体详解
- NAND 页/块/擦除单元结构
- ECC 纠错码机制
- 坏块管理(BBM)
- UBI 子系统
- UBIFS 文件系统
- SPI NOR Flash
- MTD 字符设备与块设备
- JFFS2:历史背景
- MTD 注册流程
- 典型使用场景与配置
MTD(Memory Technology Devices)是 Linux 内核对各类闪存介质的统一抽象层,由
David Woodhouse 于 1999 年创立(drivers/mtd/mtdcore.c 第 4 行版权声明
Copyright © 1999-2010 David Woodhouse),目的是屏蔽不同闪存硬件(NOR、NAND、
DataFlash 等)之间的差异,向上层文件系统和用户空间提供一致的接口。
MTD 与传统块设备(如 HDD/SSD)的核心区别:
| 特性 | 块设备(HDD/SSD) | MTD 设备 |
|---|---|---|
| 写入粒度 | 512 字节扇区 | 页(Page),通常 2KB~ |
| 擦除操作 | 无需单独擦除 | 必须先擦除再写入 |
| 坏块 | 由控制器透明处理 | 需要软件层显式管理 |
| 磨损均衡 | 内置(SSD FTL) | 需要 UBI 等上层实现 |
| OOB 区域 | 无 | 每页附带 OOB 字节 |
MTD 子系统在内核中的位置(Kconfig 路径:CONFIG_MTD):
drivers/mtd/
├── mtdcore.c # MTD 核心,设备注册/注销,字符设备
├── mtdchar.c # /dev/mtdX 字符设备实现
├── mtdblock.c # /dev/mtdblockX 块设备模拟
├── mtd_blkdevs.c # 块设备层适配
├── partitions/ # 分区解析(cmdlinepart、ofpart 等)
├── nand/
│ ├── raw/ # raw NAND 驱动(nand_base.c 等)
│ └── spi/ # SPI NAND 驱动
├── spi-nor/ # SPI NOR Flash 驱动
├── ubi/ # UBI(Unsorted Block Images)
└── chips/ # NOR Flash 芯片驱动(CFI 等)
用户空间应用
|
+-----------+ +-----------+ +-----------+
| /dev/mtdX | | UBIFS | | JFFS2 |
| 字符设备 | | 文件系统 | | 文件系统 |
+-----------+ +-----------+ +-----------+
| |
+-----------+ +-----------+
|/dev/mtdblk| | UBI | <--- 磨损均衡 / 坏块管理
| 块设备层 | | 卷管理层 |
+-----------+ +-----------+
\ /
+------------------+
| MTD 核心层 | <--- struct mtd_info
| (mtdcore.c) | 统一 read/write/erase 接口
+------------------+
|
+------------+------------+
| | |
+--------+ +---------+ +---------+
| NAND | | SPI NOR | | NOR |
| 驱动层 | | 驱动层 | | 驱动层 |
|nand_chip| | spi_nor | | ... |
+--------+ +---------+ +---------+
| |
硬件 Flash 芯片(通过 NAND 控制器 / SPI 控制器)
MTD 设备有两种角色,由 include/linux/mtd/mtd.h 第 213~236 行定义:
// mtd.h L213-L218
struct mtd_part {
struct list_head node; // 挂入父设备的分区链表
u64 offset; // 在父设备中的偏移
u64 size; // 分区大小
u32 flags; // 原始 flags(分区逻辑可能调整)
};
// mtd.h L232-L236
struct mtd_master {
struct mutex partitions_lock; // 保护分区链表的锁
struct mutex chrdev_lock;
unsigned int suspended : 1;
};- Master(主设备):直接对应物理闪存,
mtd->parent == NULL。 - Partition(分区):逻辑子设备,
mtd->parent指向父设备。
内联函数 mtd_is_partition()(mtd.h L421)判断当前设备是否为分区:
static inline bool mtd_is_partition(const struct mtd_info *mtd)
{
return mtd->parent;
}mtd_get_master()(mtd.h L403)沿 parent 链向上遍历,找到根设备:
static inline struct mtd_info *mtd_get_master(struct mtd_info *mtd)
{
while (mtd->parent)
mtd = mtd->parent;
return mtd;
}MTD 使用 IDR(DEFINE_IDR(mtd_idr),mtdcore.c L71)管理设备编号,字符设备号
通过宏 MTD_DEVT(index) = MKDEV(MTD_CHAR_MAJOR, (index)*2)(mtdcore.c L87)计算。
每个 MTD 设备占用两个次设备号:偶数为读写设备 /dev/mtdX,奇数为只读设备
/dev/mtdXro。
struct mtd_info(include/linux/mtd/mtd.h L238-L401)是整个 MTD 子系统的
核心,它同时描述 Master 设备和 Partition,嵌入了 mtd_part 和 mtd_master。
// mtd.h L238
struct mtd_info {
u_char type; // 设备类型:MTD_NANDFLASH、MTD_NORFLASH 等
uint32_t flags; // 标志位:MTD_WRITEABLE、MTD_NO_ERASE 等
uint64_t size; // 设备总大小(字节)
uint32_t erasesize; // 擦除块大小(主要粒度,如 128KB)
uint32_t writesize; // 最小写入单元(NAND 为页大小,NOR 为 1)
uint32_t writebufsize;// 写缓冲区大小(某些 NOR > writesize)
uint32_t oobsize; // 每个写单元的 OOB 字节数(如 64 字节)
uint32_t oobavail; // 可用 OOB 字节数(去掉 ECC 占用后)
// 位移和掩码,用于地址对齐快速计算
unsigned int erasesize_shift;
unsigned int writesize_shift;
unsigned int erasesize_mask;
unsigned int writesize_mask;
unsigned int bitflip_threshold; // 触发 -EUCLEAN 的 bitflip 阈值
const char *name; // 设备名称(如 "nand0")
int index; // 在 mtd_idr 中的编号
const struct mtd_ooblayout_ops *ooblayout; // OOB 布局描述
const struct mtd_pairing_scheme *pairing; // MLC/TLC 配对方案
unsigned int ecc_step_size; // ECC 步长(每步保护的字节数)
unsigned int ecc_strength; // 每步最多纠正的位翻转数
int numeraseregions;
struct mtd_erase_region_info *eraseregions; // 变长擦除区域描述
// 操作函数指针(不要直接调用,使用 mtd_*() 包装函数)
int (*_erase)(struct mtd_info *mtd, struct erase_info *instr);
int (*_read)(struct mtd_info *mtd, loff_t from, size_t len,
size_t *retlen, u_char *buf);
int (*_write)(struct mtd_info *mtd, loff_t to, size_t len,
size_t *retlen, const u_char *buf);
int (*_read_oob)(struct mtd_info *mtd, loff_t from,
struct mtd_oob_ops *ops);
int (*_write_oob)(struct mtd_info *mtd, loff_t to,
struct mtd_oob_ops *ops);
int (*_block_isbad)(struct mtd_info *mtd, loff_t ofs);
int (*_block_markbad)(struct mtd_info *mtd, loff_t ofs);
// ... 更多 ops
struct mtd_ecc_stats ecc_stats; // ECC 统计(corrected/uncorrectable)
int subpage_sft; // 子页移位(NAND)
void *priv; // 驱动私有数据指针
struct module *owner;
struct device dev;
struct kref refcnt;
struct nvmem_device *nvmem; // NVMEM 提供者
struct mtd_info *parent; // 父设备(为 NULL 则是 master)
struct list_head partitions; // 子分区链表
struct mtd_part part; // 分区字段(offset/size/flags)
struct mtd_master master; // master 字段(partitions_lock 等)
};mtdcore.c L142~L168 的 mtd_type_show() 函数揭示了所有设备类型:
| 类型常量 | 字符串 | 说明 |
|---|---|---|
MTD_ABSENT |
"absent" | 不存在 |
MTD_RAM |
"ram" | RAM 类型 |
MTD_ROM |
"rom" | 只读 ROM |
MTD_NORFLASH |
"nor" | NOR Flash |
MTD_NANDFLASH |
"nand" | SLC NAND Flash |
MTD_DATAFLASH |
"dataflash" | Atmel DataFlash |
MTD_UBIVOLUME |
"ubi" | UBI 卷 |
MTD_MLCNANDFLASH |
"mlc-nand" | MLC NAND Flash |
内核代码不应直接调用 mtd->_read(),而必须使用 mtd.h L499 起定义的包装函数:
// mtd.h L499
int mtd_erase(struct mtd_info *mtd, struct erase_info *instr);
// mtd.h L505
int mtd_read(struct mtd_info *mtd, loff_t from, size_t len,
size_t *retlen, u_char *buf);
// mtd.h L507
int mtd_write(struct mtd_info *mtd, loff_t to, size_t len,
size_t *retlen, const u_char *buf);
// mtd.h L512
int mtd_read_oob(struct mtd_info *mtd, loff_t from, struct mtd_oob_ops *ops);
int mtd_write_oob(struct mtd_info *mtd, loff_t to, struct mtd_oob_ops *ops);
// mtd.h L543
int mtd_block_isbad(struct mtd_info *mtd, loff_t ofs);
int mtd_block_markbad(struct mtd_info *mtd, loff_t ofs);这些包装函数会处理分区偏移转换(通过 mtd_get_master_ofs() 将分区内偏移换算
为物理设备偏移),保证分区视图的正确性。
struct erase_info(mtd.h L30-L34):
struct erase_info {
uint64_t addr; // 擦除起始地址(必须对齐 erasesize)
uint64_t len; // 擦除长度(必须是 erasesize 的整数倍)
uint64_t fail_addr; // 失败地址(MTD_FAIL_ADDR_UNKNOWN 表示未知)
};擦除操作是 Flash 中最危险的操作:一次擦除通常将整个块(128KB 或 256KB)全部 置 1(0xFF)。NAND Flash 的擦除次数有限(SLC 约 10 万次,MLC 约 1 万次)。
MTD 读取操作有特殊的返回值约定:
0:完全成功,无位翻转-EUCLEAN:有位翻转,但已被 ECC 纠正,且翻转数达到bitflip_threshold, 表示该块正在老化,建议上层(如 UBI)搬移数据-EBADMSG:ECC 无法纠正,数据不可靠- 其他负值:I/O 错误
mtdcore.c L288 处的 sysfs 属性 bitflip_threshold 可由用户态动态调整
(/sys/class/mtd/mtdX/bitflip_threshold)。
NAND Flash 的每个页(Page)除了主数据区域外,还附带一段"带外"(Out-Of-Band)区域。 典型布局:
+------------------------------------------+----------+
| 主数据区(Main Area) | OOB 区 |
| 2048 字节 | 64 字节 |
+------------------------------------------+----------+
OOB 区域用途:
- ECC 校验码:存储对应页数据的 ECC 字节,用于错误检测和纠正
- 坏块标记(BBM):出厂时坏块在特定位置标记为非 0xFF
- 文件系统元数据:JFFS2 等文件系统会在 OOB 存储额外信息
struct mtd_oob_ops(mtd.h L70-L80)描述一次读写 OOB 的操作参数:
struct mtd_oob_ops {
unsigned int mode; // MTD_OPS_PLACE_OOB / MTD_OPS_AUTO_OOB / MTD_OPS_RAW
size_t len; // 主数据区要读/写的字节数
size_t retlen; // 实际读/写的主数据字节数
size_t ooblen; // OOB 区要读/写的字节数
size_t oobretlen; // 实际读/写的 OOB 字节数
uint32_t ooboffs; // 在 OOB 区内的偏移(PLACE_OOB/RAW 模式有效)
uint8_t *datbuf; // 主数据缓冲区(NULL 则只操作 OOB)
uint8_t *oobbuf; // OOB 数据缓冲区
struct mtd_req_stats *stats; // 统计信息(bitflip 计数等)
};操作模式说明:
MTD_OPS_PLACE_OOB:在ooboffs处手动放置 OOB 数据MTD_OPS_AUTO_OOB:自动跳过 ECC 字节,只操作"free"区域MTD_OPS_RAW:原始模式,完整读写整个 OOB,不做任何 ECC 处理
struct mtd_ooblayout_ops(mtd.h L106-L111)定义如何在 OOB 中定位 ECC 和
空闲区域:
struct mtd_ooblayout_ops {
// 返回第 section 个 ECC 区域的偏移和长度
int (*ecc)(struct mtd_info *mtd, int section,
struct mtd_oob_region *oobecc);
// 返回第 section 个空闲区域的偏移和长度
int (*free)(struct mtd_info *mtd, int section,
struct mtd_oob_region *oobfree);
};struct mtd_oob_region(mtd.h L92-L95):
struct mtd_oob_region {
u32 offset; // 在 OOB 内的偏移
u32 length; // 区域长度
};典型的 2KB 页 NAND(64 字节 OOB),使用 BCH 8 位强度 ECC 时,OOB 布局可能为:
OOB 64 字节:
[0..1] 坏块标记(BBM)
[2..63] ECC 字节(13 字节/512字节步长,共 4 步 = 52 字节)
辅助函数(mtd.h L431-L447):
mtd_ooblayout_ecc():获取第 N 段 ECC 区域mtd_ooblayout_free():获取第 N 段空闲区域mtd_ooblayout_get_eccbytes():从 OOB 缓冲区中拷贝 ECC 字节mtd_ooblayout_count_freebytes():统计可用 OOB 空闲字节总数mtd_ooblayout_count_eccbytes():统计 ECC 字节总数
include/linux/mtd/nand.h 定义了与硬件无关的通用 NAND 抽象层。
struct nand_memory_organization(nand.h L29-L39)描述 NAND 的物理存储组织:
struct nand_memory_organization {
unsigned int bits_per_cell; // 每单元位数:1=SLC, 2=MLC, 3=TLC
unsigned int pagesize; // 页大小(字节)
unsigned int oobsize; // OOB 大小(字节/页)
unsigned int pages_per_eraseblock; // 每块的页数
unsigned int eraseblocks_per_lun; // 每 LUN 的块数
unsigned int max_bad_eraseblocks_per_lun; // 最大允许坏块数/LUN
unsigned int planes_per_lun; // 每 LUN 的 plane 数
unsigned int luns_per_target; // 每 target(die)的 LUN 数
unsigned int ntargets; // 总 target(die)数
};struct nand_device(nand.h L420-L427)是所有 NAND 类型的基类:
struct nand_device {
struct mtd_info mtd; // 内嵌的 MTD 设备
struct nand_memory_organization memorg; // 存储组织
struct nand_ecc ecc; // ECC 配置
struct nand_row_converter rowconv; // 行地址转换
struct nand_bbt bbt; // 坏块表(Bad Block Table)
const struct nand_ops *ops; // NAND 基本操作(erase/markbad/isbad)
};通过 container_of 实现的类型转换辅助函数(nand.h L453-L467):
// nand.h L453
static inline struct nand_device *mtd_to_nanddev(struct mtd_info *mtd)
{
return container_of(mtd, struct nand_device, mtd);
}
// nand.h L464
static inline struct mtd_info *nanddev_to_mtd(struct nand_device *nand)
{
return &nand->mtd;
}drivers/mtd/nand/raw/nand_base.c 是 Raw NAND 的核心实现,作者为
Steven J. Hill 和 Thomas Gleixner(nand_base.c L1-L24 注释)。
选片与取消选片(nand_base.c L171-L201):
// nand_base.c L171
void nand_select_target(struct nand_chip *chip, unsigned int cs)
{
chip->cur_cs = cs;
if (chip->legacy.select_chip)
chip->legacy.select_chip(chip, cs);
}
// nand_base.c L194
void nand_deselect_target(struct nand_chip *chip)
{
if (chip->legacy.select_chip)
chip->legacy.select_chip(chip, -1);
chip->cur_cs = -1;
}chip->controller->lock 和 chip->lock 两级锁(nand_base.c L212-L214)保证
了控制器级别和芯片级别的并发安全。
struct nand_chip(include/linux/mtd/rawnand.h L1288-L1346)是 Raw NAND
驱动的核心数据结构,继承自 nand_device:
struct nand_chip {
struct nand_device base; // 基类,内嵌 mtd_info
struct nand_id id; // NAND ID(最多 8 字节)
struct nand_parameters parameters; // ONFI/JEDEC 参数
struct nand_manufacturer manufacturer; // 厂商信息
struct nand_chip_ops ops; // 芯片级操作(suspend/resume/lock)
struct nand_legacy legacy; // 旧版接口(已废弃,勿用)
unsigned int options; // 芯片选项标志(NAND_BUSWIDTH_16 等)
/* 数据接口配置 */
const struct nand_interface_config *current_interface_config;
struct nand_interface_config *best_interface_config;
/* 坏块管理 */
unsigned int bbt_erase_shift; // BBT 条目地址位数
unsigned int bbt_options; // BBT 选项
unsigned int badblockpos; // 坏块标记在 OOB 中的位置
unsigned int badblockbits; // 判断好块需要的最少置 1 位数
struct nand_bbt_descr *bbt_td; // BBT 主描述符
struct nand_bbt_descr *bbt_md; // BBT 镜像描述符
u8 *bbt; // 内存中的 BBT 缓存
/* 地址布局(移位值加速计算) */
unsigned int page_shift; // 页内偏移位数(log2 pagesize)
unsigned int phys_erase_shift; // 块内偏移位数(log2 blocksize)
unsigned int chip_shift; // 芯片内偏移位数(log2 chipsize)
unsigned int pagemask; // 芯片内页号掩码
/* I/O 缓冲区 */
u8 *data_buf; // 数据缓冲区(pagesize + oobsize)
u8 *oob_poi; // OOB 缓冲区指针(指向 data_buf 末尾)
struct {
unsigned int bitflips;
int page; // 当前缓存的页号(-1 无缓存)
} pagecache;
/* 并发控制 */
struct mutex lock;
unsigned int suspended : 1;
wait_queue_head_t resume_wq;
int cur_cs; // 当前选中的 CS(-1 未选)
/* 安全区域(来自 DT) */
struct nand_secure_region *secure_regions;
u8 nr_secure_regions;
/* 连续读(Sequential Cache Read)*/
struct {
bool ongoing;
unsigned int first_page;
unsigned int pause_page;
unsigned int last_page;
} cont_read;
struct nand_controller *controller; // 所属控制器
struct nand_ecc_ctrl ecc; // ECC 控制结构
void *priv; // 驱动私有数据
};类型转换辅助(rawnand.h L1348-L1356):
static inline struct nand_chip *mtd_to_nand(struct mtd_info *mtd)
{
return container_of(mtd, struct nand_chip, base.mtd);
}
static inline struct mtd_info *nand_to_mtd(struct nand_chip *chip)
{
return &chip->base.mtd;
}struct nand_controller(rawnand.h L1120-L1128)描述共享控制器:
struct nand_controller {
struct mutex lock; // 序列化对控制器的访问
const struct nand_controller_ops *ops;
struct {
unsigned int data_only_read: 1; // 支持不重启就继续读数据
unsigned int cont_read: 1; // 支持连续缓存读
} supported_op;
bool controller_wp; // 控制器管理 WP 引脚
};struct nand_controller_ops(rawnand.h L1097-L1105)定义现代控制器的操作接口:
struct nand_controller_ops {
int (*attach_chip)(struct nand_chip *chip); // ECC 检测后调用,分配资源
void (*detach_chip)(struct nand_chip *chip); // 释放资源
int (*exec_op)(struct nand_chip *chip, // 执行 NAND 操作序列
const struct nand_operation *op,
bool check_only);
int (*setup_interface)(struct nand_chip *chip, int chipnr,
const struct nand_interface_config *conf);
};exec_op 接口替代了旧版的 cmdfunc/read_buf/write_buf,通过
struct nand_operation 描述一系列原子指令(CMD/ADDR/DATA_IN/DATA_OUT/WAITRDY),
让控制器驱动可以精确控制时序(见 rawnand.h L670-L1034 的指令定义体系)。
NAND Flash 的存储层次从小到大:
位(Bit)
|
字节(Byte)
|
页(Page)= 主数据区 + OOB
| 典型值:512B(旧)、2KB、4KB、16KB
|
块(Block / Eraseblock)= N 个页
| 典型值:64页/块(2KB页=128KB块)、256页/块
|
LUN(Logical Unit Number)= N 个块
|
Target(Die)= N 个 LUN
|
NAND 芯片 = N 个 Target(多 Die 封装)
以 Samsung K9F2G08 为例(nand_memory_organization 填充值):
bits_per_cell = 1(SLC)pagesize = 2048(2KB 主区 + 64 字节 OOB)pages_per_eraseblock = 64(=> 块大小 = 128KB)eraseblocks_per_lun = 2048(=> 总容量 = 256MB)
MLC(Multi-Level Cell)每个物理单元存储 2 位,TLC 存储 3 位。由于物理单元共享, 同一单元内的多个页(称为"pair")相互影响,写入顺序有严格要求。
struct mtd_pairing_scheme(mtd.h L181-L187)抽象了页配对关系:
struct mtd_pairing_scheme {
int ngroups; // 组数(MLC=2, TLC=3)
int (*get_info)(struct mtd_info *mtd, int wunit,
struct mtd_pairing_info *info); // wunit -> (pair, group)
int (*get_wunit)(struct mtd_info *mtd,
const struct mtd_pairing_info *info); // (pair, group) -> wunit
};nand_base.c L49~L97 实现了 dist3_pairing_scheme(针对 dist=3 距离的 MLC):
// nand_base.c L93
const struct mtd_pairing_scheme dist3_pairing_scheme = {
.ngroups = 2,
.get_info = nand_pairing_dist3_get_info,
.get_wunit = nand_pairing_dist3_get_wunit,
};H27UCG8T2BTR-BC 的配对方案示例(mtd.h L130~L145 注释):
group-0 group-1
pair-0 page-0 page-4
pair-1 page-1 page-5
pair-2 page-2 page-8
...
pair-127 page-251 page-255
check_offs_len()(nand_base.c L99-L116)检查地址和长度必须对齐到物理块边界:
// nand_base.c L104
if (ofs & ((1ULL << chip->phys_erase_shift) - 1)) {
pr_debug("%s: unaligned address\n", __func__);
ret = -EINVAL;
}chip->page_shift、chip->phys_erase_shift、chip->chip_shift 三个移位值
是频繁用于地址计算的加速字段。chip->pagemask 用于从绝对页号取芯片内页号。
NAND Flash 的存储单元由于物理特性会产生位翻转(bit flip),ECC 机制用于检测和
纠正这些错误。内核支持多种 ECC 方案,由 include/linux/mtd/nand.h L144~L197
定义的枚举类型描述:
// nand.h L145
enum nand_ecc_engine_type {
NAND_ECC_ENGINE_TYPE_INVALID,
NAND_ECC_ENGINE_TYPE_NONE, // 不做 ECC
NAND_ECC_ENGINE_TYPE_SOFT, // 软件 ECC
NAND_ECC_ENGINE_TYPE_ON_HOST, // 主机端硬件 ECC(NAND 控制器内置)
NAND_ECC_ENGINE_TYPE_ON_DIE, // 芯片内部 ECC(芯片固件处理)
};
// nand.h L174
enum nand_ecc_algo {
NAND_ECC_ALGO_UNKNOWN,
NAND_ECC_ALGO_HAMMING, // Hamming 码(1 位纠错)
NAND_ECC_ALGO_BCH, // BCH 码(多位纠错,常用)
NAND_ECC_ALGO_RS, // Reed-Solomon 码
};
// nand.h L161
enum nand_ecc_placement {
NAND_ECC_PLACEMENT_UNKNOWN,
NAND_ECC_PLACEMENT_OOB, // ECC 字节存放在 OOB 区
NAND_ECC_PLACEMENT_INTERLEAVED, // ECC 与数据交织存放(Syndrome 布局)
};struct nand_ecc_ctrl(rawnand.h L349-L385)是 Raw NAND ECC 的核心控制结构:
struct nand_ecc_ctrl {
enum nand_ecc_engine_type engine_type;
enum nand_ecc_placement placement;
enum nand_ecc_algo algo;
int steps; // 每页的 ECC 步数(通常 pagesize / step_size)
int size; // 每步保护的数据字节数(常见:256 或 512)
int bytes; // 每步产生的 ECC 字节数
int total; // 全页 ECC 字节总数 = steps * bytes
int strength; // 每步最大可纠正位翻转数
int prepad; // Syndrome 布局的前填充字节
int postpad; // Syndrome 布局的后填充字节
u8 *calc_buf; // 计算出的 ECC 缓冲区
u8 *code_buf; // 从 flash 读出的 ECC 缓冲区
// 操作函数指针
void (*hwctl)(struct nand_chip *chip, int mode);
int (*calculate)(struct nand_chip *chip, const uint8_t *dat,
uint8_t *ecc_code);
int (*correct)(struct nand_chip *chip, uint8_t *dat, uint8_t *read_ecc,
uint8_t *calc_ecc); // 返回纠正的位翻转数,或 -EBADMSG
int (*read_page)(struct nand_chip *chip, uint8_t *buf,
int oob_required, int page);
int (*write_page)(struct nand_chip *chip, const uint8_t *buf,
int oob_required, int page);
int (*read_oob)(struct nand_chip *chip, int page);
int (*write_oob)(struct nand_chip *chip, int page);
// ... 更多读写变体
};include/linux/mtd/nand.h L242-L267 定义了更新的 ECC 引擎抽象:
// nand.h L242
struct nand_ecc_context {
struct nand_ecc_props conf; // 引擎参数(algo/strength/step_size)
unsigned int nsteps; // 步数
unsigned int total; // ECC 总字节数
void *priv; // 引擎私有数据
};
// nand.h L260
struct nand_ecc_engine_ops {
int (*init_ctx)(struct nand_device *nand); // 初始化 ECC 上下文
void (*cleanup_ctx)(struct nand_device *nand); // 清理 ECC 上下文
int (*prepare_io_req)(struct nand_device *nand, // 读写前准备
struct nand_page_io_req *req);
int (*finish_io_req)(struct nand_device *nand, // 读写后完成
struct nand_page_io_req *req);
};软件 ECC 引擎的获取接口(nand.h L333-L348):
// Hamming 软件引擎(nand-ecc-sw-hamming.c)
struct nand_ecc_engine *nand_ecc_sw_hamming_get_engine(void);
// BCH 软件引擎(nand-ecc-sw-bch.c,依赖 lib/bch.c)
struct nand_ecc_engine *nand_ecc_sw_bch_get_engine(void);nand_base.c L38-L39 引入了这两个 ECC 模块的头文件:
#include <linux/mtd/nand-ecc-sw-hamming.h>
#include <linux/mtd/nand-ecc-sw-bch.h>Hamming 码是最简单的 ECC 方案,每 256 字节数据产生 3 字节(22 位)ECC:
- 可以检测 2 位错误,纠正 1 位错误
- 计算复杂度低,适合旧版 SLC NAND
- 现代 MLC/TLC NAND 通常需要更强的 ECC(4T~ 24T BCH)
BCH(Bose-Chaudhuri-Hocquenghem)码是更强的多位纠错码。内核 lib/bch.c 提供
了通用 BCH 实现(GF(2^m) 有限域运算):
- 强度(t):可纠正的最大位翻转数
- 每 512 字节步长,BCH-4 需要约 7 字节 ECC,BCH-8 需要约 13 字节
- 现代 NAND 控制器通常内置硬件 BCH 引擎(ON_HOST 类型)
ECC 强度要求来自芯片本身(nand_ecc_props.requirements),由驱动在检测阶段填入,
内核通过 nand_ecc_is_strong_enough()(nand.h L308)验证配置是否满足芯片要求。
NAND Flash 出厂时,厂商会在坏块的特定位置留下标记(通常为非 0xFF 值)。
rawnand.h 定义了三种可能的位置(L203-L205):
#define NAND_BBM_FIRSTPAGE BIT(24) // 块的第一页 OOB
#define NAND_BBM_SECONDPAGE BIT(25) // 块的第二页 OOB
#define NAND_BBM_LASTPAGE BIT(26) // 块的最后一页 OOBOOB 中的具体字节位置(rawnand.h L221-L222):
#define NAND_BBM_POS_SMALL 5 // 小页(512B)NAND
#define NAND_BBM_POS_LARGE 0 // 大页(2KB+)NAND(第 0 字节)nand_block_bad()(nand_base.c L252-L279)是默认的坏块检测实现:
static int nand_block_bad(struct nand_chip *chip, loff_t ofs)
{
int first_page, page_offset;
u8 bad;
first_page = (int)(ofs >> chip->page_shift) & chip->pagemask;
page_offset = nand_bbm_get_next_page(chip, 0);
while (page_offset >= 0) {
res = chip->ecc.read_oob(chip, first_page + page_offset);
bad = chip->oob_poi[chip->badblockpos];
if (likely(chip->badblockbits == 8))
res = bad != 0xFF; // 精确匹配
else
res = hweight8(bad) < chip->badblockbits; // 汉明重量判断
if (res)
return res; // 发现坏块标记
page_offset = nand_bbm_get_next_page(chip, page_offset + 1);
}
return 0;
}nand_bbm_get_next_page()(nand_base.c L225-L243)根据 NAND_BBM_* 标志遍历
需要检查的页偏移列表。
为避免每次操作都要读取 OOB 检查坏块,内核会在 NAND 启动时扫描坏块并建立内存
BBT 缓存(chip->bbt)。BBT 每个块使用 2 bit 编码:
| 值 | 含义 |
|---|---|
| 00 | 已知坏块 |
| 01 | 工厂坏块 |
| 10 | 保留 |
| 11 | 好块 |
chip->bbt_td 和 chip->bbt_md(rawnand.h L1306-L1307)分别指向 BBT 主描述符
和镜像描述符,内核还会将 BBT 持久化到闪存末尾的特定区域(通常占用 4 个预留块)。
NAND_SKIP_BBTSCAN(rawnand.h L155)标志可以跳过启动时的 BBT 扫描(由更上层
的 UBI 接管坏块管理时使用)。
UBI(Unsorted Block Images)是构建在 MTD 之上的卷管理层,提供:
- 磨损均衡(Wear Leveling):均匀分配擦写次数
- 坏块管理:透明地屏蔽坏块
- 卷管理:将物理擦除块(PEB)映射到逻辑擦除块(LEB),创建多个逻辑卷
+------------------+
| UBIFS / 用户态 |
+------------------+
| UBI 卷(Volume)| <-- struct ubi_volume
+------------------+
| UBI 设备(Device)| <-- struct ubi_device
| - EBA(LEB->PEB映射)
| - WL(磨损均衡)
| - I/O 子系统
+------------------+
| MTD 设备 |
+------------------+
struct ubi_device(drivers/mtd/ubi/ubi.h L555-L656)是 UBI 设备的核心描述符,
包含 700+ 行注释,以下列出关键字段:
struct ubi_device {
struct cdev cdev; // 字符设备(/dev/ubiX)
struct device dev;
int ubi_num; // UBI 设备编号
int vol_count; // 卷数量
struct ubi_volume *volumes[UBI_MAX_VOLUMES + UBI_INT_VOL_COUNT];
spinlock_t volumes_lock;
/* PEB 统计 */
int rsvd_pebs; // 保留 PEB 数
int avail_pebs; // 可用 PEB 数
int beb_rsvd_pebs; // 为坏块处理预留的 PEB 数
int bad_peb_count; // 坏 PEB 数量
int good_peb_count; // 好 PEB 数量
/* 卷表 */
int vtbl_slots;
struct ubi_vtbl_record *vtbl; // 内存中的卷表副本
struct mutex device_mutex;
/* 磨损均衡计数 */
int max_ec; // 最大擦写计数
int mean_ec; // 平均擦写计数
/* EBA 子系统 */
unsigned long long global_sqnum; // 全局序列号
spinlock_t ltree_lock;
struct rb_root ltree; // LEB 锁树(并发控制)
/* Fastmap 优化 */
int fm_disabled;
struct ubi_fastmap_layout *fm; // Fastmap 布局
struct ubi_fm_pool fm_pool; // Fastmap PEB 池
struct ubi_fm_pool fm_wl_pool; // WL 子系统用的池
/* 磨损均衡(WL)子系统 */
struct rb_root used; // 已使用 PEB 的红黑树(按 EC 排序)
struct rb_root free; // 空闲 PEB 的红黑树(按 EC 排序)
int free_count;
struct rb_root scrub; // 需要 scrubbing 的 PEB
struct list_head pq[UBI_PROT_QUEUE_LEN]; // 保护队列(10 个 slot)
int pq_head;
spinlock_t wl_lock;
struct ubi_wl_entry **lookuptbl; // PEB 号 -> wl_entry 快速查找表
struct ubi_wl_entry *move_from; // 正在搬移的源 PEB
struct ubi_wl_entry *move_to; // 正在搬移的目标 PEB
struct task_struct *bgt_thread; // 后台线程(ubi_bgt%dd)
char bgt_name[sizeof(UBI_BGT_NAME_PATTERN)+2];
/* 物理设备参数 */
long long flash_size; // 底层 MTD 设备大小
int peb_count; // PEB 总数
int peb_size; // PEB 大小(= MTD erasesize)
int leb_size; // LEB 大小(= peb_size - 头部开销)
int leb_start; // LEB 在 PEB 内的起始偏移
struct mtd_info *mtd; // 指向底层 MTD 设备
struct ubi_debug_info dbg;
};struct ubi_wl_entry(ubi.h L170-L177)是磨损均衡的基本单元:
struct ubi_wl_entry {
union {
struct rb_node rb; // 在 free/used 红黑树中的节点
struct list_head list; // 在保护队列中的节点
} u;
int ec; // 擦写计数(Erase Counter)
int pnum; // 物理擦除块号
};磨损均衡策略:
- 静态磨损均衡(Static WL):即使某个 PEB 没有被写入,也会定期将其数据 搬移到 EC 较高的块中,同时用低 EC 块取代高 EC 块
- 动态磨损均衡(Dynamic WL):每次写入新数据时,选择当前 EC 最小的空闲块
- 触发阈值:当 max_ec - min_ec 超过某个阈值(默认 UBI_WL_THRESHOLD=128)时, 后台线程触发数据搬移
free 红黑树按 EC 从小到大排列,写入时从树的最左节点取块;used 树存放已用块,
scrubbing 时遍历 EC 差异大的块对。
保护队列(Protection Queue,PQ):长度为 UBI_PROT_QUEUE_LEN=10(ubi.h L73),
刚被搬移的 PEB 先放入保护队列,经过足够多的全局擦除操作后才重新加入自由池,
防止同一块被反复搬移。
EBA 维护 LEB -> PEB 的映射表(ubi_volume->eba_tbl)。每个卷有独立的 EBA 表。
struct ubi_eba_leb_desc(ubi.h L269-L272):
struct ubi_eba_leb_desc {
int lnum; // 逻辑擦除块号
int pnum; // 物理擦除块号
};struct ubi_ltree_entry(ubi.h L193-L199)实现每个 LEB 的读写锁:
struct ubi_ltree_entry {
struct rb_node rb;
int vol_id;
int lnum;
int users; // 当前使用者数
struct rw_semaphore mutex; // 读写信号量
};struct ubi_volume(ubi.h L330-L371):
struct ubi_volume {
struct device dev;
struct cdev cdev; // /dev/ubiX_Y
struct ubi_device *ubi;
int vol_id;
int reserved_pebs; // 为此卷保留的 PEB 数
int vol_type; // UBI_DYNAMIC_VOLUME 或 UBI_STATIC_VOLUME
int usable_leb_size; // 可用 LEB 大小(考虑对齐后)
int used_ebs; // 实际含数据的 LEB 数
long long used_bytes; // 数据字节总量
int alignment; // 卷对齐要求
int data_pad; // 末尾填充字节数
char name[UBI_VOL_NAME_MAX + 1];
struct ubi_eba_table *eba_tbl; // LEB->PEB 映射表
unsigned int corrupted:1; // 静态卷是否损坏
unsigned int upd_marker:1; // 更新被中断标记
unsigned int updating:1; // 正在更新中
};卷分两种类型:
- Dynamic Volume:可变内容,适合文件系统(UBIFS)
- Static Volume:不可变内容,UBI 会对整个卷做 CRC 校验(适合内核/ramdisk 镜像)
include/linux/mtd/ubi.h L240-L261 定义了供内核模块使用的 UBI 操作接口:
struct ubi_volume_desc *ubi_open_volume(int ubi_num, int vol_id, int mode);
struct ubi_volume_desc *ubi_open_volume_nm(int ubi_num, const char *name, int mode);
void ubi_close_volume(struct ubi_volume_desc *desc);
int ubi_leb_read(struct ubi_volume_desc *desc, int lnum, char *buf,
int offset, int len, int check);
int ubi_leb_write(struct ubi_volume_desc *desc, int lnum, const void *buf,
int offset, int len);
int ubi_leb_change(struct ubi_volume_desc *desc, int lnum, const void *buf,
int len);
int ubi_leb_erase(struct ubi_volume_desc *desc, int lnum);
int ubi_leb_unmap(struct ubi_volume_desc *desc, int lnum);ubi_leb_change() 是原子 LEB 变更操作——先将新内容写入临时 PEB,验证无误后
再更新 EBA 映射,保证掉电安全性。
Fastmap(CONFIG_MTD_UBI_FASTMAP)是 UBI 的快速挂载优化。正常 UBI attach 需要
扫描所有 PEB 读取 EC/VID 头,对于大容量 NAND(如 8GB)可能需要数十秒。
Fastmap 将整个 UBI 状态(WL 树、EBA 表等)序列化后写入专用 PEB,下次 attach 只 需读取 Fastmap PEB 即可,将挂载时间降低到秒级以下。
struct ubi_fastmap_layout(ubi.h L232-L238):
struct ubi_fastmap_layout {
struct ubi_wl_entry *e[UBI_FM_MAX_BLOCKS]; // Fastmap 占用的 PEB
int to_be_tortured[UBI_FM_MAX_BLOCKS];
int used_blocks;
int max_pool_size;
int max_wl_pool_size;
};I/O 错误码(ubi.h L105-L111)体现了 UBI 对 MTD 错误的精细处理:
enum {
UBI_IO_FF = 1, // 读到全 0xFF
UBI_IO_FF_BITFLIPS, // 全 0xFF 但有位翻转
UBI_IO_BAD_HDR, // EC/VID 头损坏(CRC 错误)
UBI_IO_BAD_HDR_EBADMSG, // 头损坏且有 ECC 不可纠正错误
UBI_IO_BITFLIPS, // 有位翻转但已纠正
};UBIFS(Unsorted Block Images File System)是专为 UBI 设计的日志结构文件系统, 由 Nokia 的 Artem Bityutskiy 和 Adrian Hunter 开发(fs/ubifs/ubifs.h L1-L9)。
UBIFS 文件系统结构
+--------------------------------------------------+
| VFS 层接口 |
+--------------------------------------------------+
| |
| Journal(日志) Index(B-tree 索引) |
| +-----------+ +------------------+ |
| | GC Head | | TNC(Tree Node | |
| | Base Head | | Cache)内存 B树 | |
| | Data Head | +------------------+ |
| +-----------+ | |
| | Index Node |
| v (存储在 Flash 上) |
| LEB(Logical Eraseblock) |
+--------------------------------------------------+
| UBI 卷 |
+--------------------------------------------------+
fs/ubifs/ubifs.h 定义的核心常量:
// ubifs.h L41
#define UBIFS_VERSION 1
// ubifs.h L44
#define UBIFS_SUPER_MAGIC 0x24051905
// ubifs.h L60
#define MIN_INDEX_LEBS 2 // 最少保留 2 个 LEB 给索引
// ubifs.h L81
#define BGT_NAME_PATTERN "ubifs_bgt%d_%d" // 后台线程名
// ubifs.h L87
#define NONDATA_JHEADS_CNT 2 // 非数据 Journal Head 数(GC + Base)UBIFS 使用多个 Journal Head(日志头)来分离不同类型的写入流量:
| Journal Head | 宏名 | 用途 |
|---|---|---|
| GC Head | GCHD |
垃圾回收使用 |
| Base Head | BASEHD |
inode、目录项等元数据节点 |
| Data Head | DATAHD |
文件数据节点 |
每个 Journal Head 对应一个当前活跃的 LEB,新数据顺序追加写入,满后换新 LEB。 这种设计使写入成为顺序操作,避免了随机写入对闪存的伤害。
提交状态机(ubifs.h L189-L196):
enum {
COMMIT_RESTING = 0, // 无需提交
COMMIT_BACKGROUND, // 后台提交已请求
COMMIT_REQUIRED, // 必须提交
COMMIT_RUNNING_BACKGROUND, // 后台提交进行中
COMMIT_RUNNING_REQUIRED, // 提交进行中且必须完成
COMMIT_BROKEN, // 提交失败
};UBIFS 使用 B-tree 作为文件系统索引,在内存中维护 B-tree 的缓存节点(znode)。
union ubifs_key(ubifs.h L281-L286)是 B-tree 的键类型:
union ubifs_key {
uint8_t u8[UBIFS_SK_LEN];
uint32_t u32[UBIFS_SK_LEN/4];
uint64_t u64[UBIFS_SK_LEN/8];
__le32 j32[UBIFS_SK_LEN/4];
};键由 inode 号 + 键类型 + offset 组成,支持以下节点类型:
UBIFS_INO_KEY:inode 节点UBIFS_DENT_KEY:目录项节点UBIFS_DATA_KEY:数据块节点UBIFS_XATTR_KEY:扩展属性节点
Znode 标志(ubifs.h L173-L177):
enum {
DIRTY_ZNODE = 0, // znode 已脏(需要写回)
COW_ZNODE = 1, // 提交中,修改前需先 COW
OBSOLETE_ZNODE = 2, // 已删除,等待提交后释放
};LPT 管理每个 LEB 的属性(空闲空间、脏数据量等),帮助 UBIFS 选择合适的 LEB
用于写入或垃圾回收。LPT_HEAP_SZ = 256(ubifs.h L75)是 LPT 堆的最大条目数。
UBIFS 后台线程(ubifs_bgt%d_%d)负责垃圾回收,将已删除/修改数据占用的 LEB
清理出来,返还给 UBI。垃圾回收的 LEB 操作结果(ubifs.h L249-L252):
enum {
LEB_FREED, // LEB 已释放,可立即使用
LEB_FREED_IDX, // 索引 LEB 已释放,只能在下次提交后使用
LEB_RETAINED, // LEB 被保留用于 GC
};struct ubifs_inode(ubifs.h L399+)扩展了 VFS inode,包含:
creat_sqnum:创建时的全局序列号synced_i_size:已同步到 Flash 的 inode 大小ui_size:UBIFS 使用的"影子"大小变量(与inode->i_size解耦)compr_type:该 inode 使用的压缩算法(LZO/ZLIB/ZSTD)bulk_read:批量读取优化标志
SPI NOR Flash 是通过 SPI 总线访问的 NOR 型闪存,常见于嵌入式系统存储 bootloader、 内核和小型只读文件系统。
应用/文件系统
|
+----------+
| MTD 层 | struct mtd_info(内嵌于 spi_nor)
+----------+
| spi-nor | struct spi_nor (drivers/mtd/spi-nor/core.c)
| 核心层 | - SFDP 解析
| | - 厂商特定修复
+----------+
| spi-mem | struct spi_mem (drivers/spi/spi-mem.c)
| 抽象层 | 统一 SPI 存储操作(cmd+addr+dummy+data)
+----------+
| SPI 控制器驱动
+----------+
struct spi_nor(include/linux/mtd/spi-nor.h L382-L423):
struct spi_nor {
struct mtd_info mtd; // 内嵌 MTD 设备
struct mutex lock; // 序列化读/写/擦除操作
struct spi_nor_rww { // Read-While-Write 同步(多 bank 芯片)
wait_queue_head_t wait;
bool ongoing_io;
bool ongoing_rd;
bool ongoing_pe; // 程序/擦除进行中
unsigned int used_banks;
} rww;
struct device *dev; // 指向 SPI 设备或 SPI NOR 控制器设备
struct spi_mem *spimem; // spi-mem 设备指针(如果使用 spi-mem 框架)
u8 *bouncebuf; // 弹跳缓冲区(栈上/vmalloc 数据不能 DMA)
size_t bouncebuf_size;
u8 *id; // JEDEC ID(3~20 字节)
const struct flash_info *info; // 芯片型号信息(名称/容量/标志)
u8 addr_nbytes; // 地址字节数(3 或 4)
u8 erase_opcode; // 擦除命令字节(如 0xD8)
u8 read_opcode; // 读命令字节(如 0x03、0x0B、0xEB)
u8 read_dummy; // 读命令后的 dummy 时钟数
u8 program_opcode; // 编程命令字节(如 0x02)
enum spi_nor_protocol read_proto; // 读操作协议
enum spi_nor_protocol write_proto; // 写操作协议
enum spi_nor_protocol reg_proto; // 寄存器操作协议
u32 flags; // SNOR_F_* 标志
enum spi_nor_cmd_ext cmd_ext_type; // DTR 模式命令扩展类型
struct sfdp *sfdp; // 解析后的 SFDP 数据
const struct spi_nor_controller_ops *controller_ops;
struct spi_nor_flash_parameter *params; // Flash 参数(可由 SFDP 覆盖)
struct {
struct spi_mem_dirmap_desc *rdesc; // 直接映射读描述符
struct spi_mem_dirmap_desc *wdesc; // 直接映射写描述符
} dirmap;
void *priv;
};include/linux/mtd/spi-nor.h L159-L176 定义了丰富的 SPI 传输协议:
enum spi_nor_protocol {
SNOR_PROTO_1_1_1 = ..., // 标准 SPI:1线指令-1线地址-1线数据
SNOR_PROTO_1_1_2 = ..., // Dual Output
SNOR_PROTO_1_2_2 = ..., // Dual I/O
SNOR_PROTO_1_1_4 = ..., // Quad Output
SNOR_PROTO_1_4_4 = ..., // Quad I/O(常见高速读)
SNOR_PROTO_1_1_8 = ..., // Octal Output
SNOR_PROTO_1_8_8 = ..., // Octal I/O
SNOR_PROTO_8_8_8 = ..., // Octal SPI(所有阶段 8 线)
SNOR_PROTO_8_8_8_DTR = ..., // Octal DTR(双倍速率)
// ...
};协议编码方式(spi-nor.h L131-L147)将指令位宽、地址位宽、数据位宽各用 8 位编码
打包进一个 u32 值中,DTR 标志位于第 24 位(SNOR_PROTO_IS_DTR BIT(24))。
常见 SPI NOR 操作码(spi-nor.h L22-L51):
#define SPINOR_OP_READ 0x03 // 标准读
#define SPINOR_OP_READ_FAST 0x0b // 快速读(需要 dummy byte)
#define SPINOR_OP_READ_1_4_4 0xeb // Quad I/O 读
#define SPINOR_OP_PP 0x02 // 页编程(Page Program,最多 256 字节)
#define SPINOR_OP_BE_4K 0x20 // 4KB 块擦除
#define SPINOR_OP_SE 0xd8 // 扇区擦除(通常 64KB)
#define SPINOR_OP_CHIP_ERASE 0xc7 // 全片擦除
#define SPINOR_OP_WREN 0x06 // 写使能
#define SPINOR_OP_RDID 0x9f // 读 JEDEC ID
#define SPINOR_OP_RDSFDP 0x5a // 读 SFDP 表spi-mem 框架将 SPI 存储操作抽象为四个阶段的操作结构体 struct spi_mem_op:
CMD (1字节指令) | ADDR (n字节地址) | DUMMY (m个空时钟) | DATA (数据)
spi_nor_spimem_setup_op()(core.c L82-L121)根据选定协议设置每个阶段的
位宽(buswidth)和 DTR 模式,是协议选择和实际总线操作之间的桥梁。
spi_nor_spimem_read_data()(core.c L201-L236)展示了读操作的完整流程:
static ssize_t spi_nor_spimem_read_data(struct spi_nor *nor, loff_t from,
size_t len, u8 *buf)
{
struct spi_mem_op op =
SPI_MEM_OP(SPI_MEM_OP_CMD(nor->read_opcode, 0),
SPI_MEM_OP_ADDR(nor->addr_nbytes, from, 0),
SPI_MEM_OP_DUMMY(nor->read_dummy, 0),
SPI_MEM_OP_DATA_IN(len, buf, 0));
spi_nor_spimem_setup_op(nor, &op, nor->read_proto); // 设置位宽
// 优先使用直接内存映射(dirmap),其次执行 spi_mem_op
if (nor->dirmap.rdesc) {
nbytes = spi_mem_dirmap_read(nor->dirmap.rdesc, ...);
} else {
error = spi_nor_spimem_exec_op(nor, &op);
}
return nbytes;
}dirmap(Direct Mapping)是高性能 SPI NOR 控制器的特性,将 Flash 地址空间
直接映射到 CPU 地址空间,允许 CPU 以普通内存读操作访问 Flash,避免每次读取
都经过 SPI 控制器驱动层。
SFDP(JEDEC JESD216 标准)是 SPI NOR Flash 的自描述参数表,芯片在出厂时将自身 特性(容量、擦除大小/命令、各种 Quad/Octal 模式支持情况等)写入内部 SFDP 区域。
通过操作码 SPINOR_OP_RDSFDP = 0x5a(spi-nor.h L47)读取 SFDP 数据。
spi_nor_scan()(spi-nor.h L450)是 SPI NOR 驱动的核心初始化函数:
- 读取 JEDEC ID
- 在已知芯片数据库中查找匹配项
- 若未找到,尝试读取 SFDP 表自动识别
- 根据控制器
hwcaps选择最优读写协议 - 填充
mtd_info字段并注册到 MTD 核心
struct spi_nor_controller_ops(spi-nor.h L305-L316):
struct spi_nor_controller_ops {
int (*prepare)(struct spi_nor *nor);
void (*unprepare)(struct spi_nor *nor);
int (*read_reg)(struct spi_nor *nor, u8 opcode, u8 *buf, size_t len);
int (*write_reg)(struct spi_nor *nor, u8 opcode, const u8 *buf, size_t len);
ssize_t (*read)(struct spi_nor *nor, loff_t from, size_t len, u8 *buf);
ssize_t (*write)(struct spi_nor *nor, loff_t to, size_t len, const u8 *buf);
int (*erase)(struct spi_nor *nor, loff_t offs);
};当控制器不支持 spi-mem 框架时,使用此接口;支持 spi-mem 时,nor->spimem
非 NULL,优先通过 spi_mem_exec_op() 执行所有操作。
MTD 核心为每个注册的 MTD 设备创建两个字符设备节点:
| 设备节点 | 次设备号 | 权限 | 用途 |
|---|---|---|---|
/dev/mtdX |
index * 2 | rw | 读写访问,支持全部 ioctl |
/dev/mtdXro |
index * 2 + 1 | r | 只读访问 |
字符设备主设备号 MTD_CHAR_MAJOR(通常为 90),设备号计算宏(mtdcore.c L87):
#define MTD_DEVT(index) MKDEV(MTD_CHAR_MAJOR, (index)*2)常用 ioctl 命令(来自 mtd/mtd-abi.h):
| ioctl | 功能 |
|---|---|
MEMGETINFO |
获取 MTD 设备信息(size/erasesize 等) |
MEMERASE |
擦除指定区域 |
MEMWRITEOOB |
写带 OOB 的页 |
MEMREADOOB |
读带 OOB 的页 |
MEMGETBADBLOCK |
检查某块是否为坏块 |
MEMSETBADBLOCK |
标记坏块 |
MEMLOCK / MEMUNLOCK |
锁定/解锁块(NOR Flash) |
MEMGETOOBSEL |
获取 OOB 布局选项(旧接口) |
MEMWRITE |
通用写操作(可附带 OOB) |
/dev/mtdblockX 是对 MTD 设备的块设备模拟(drivers/mtd/mtdblock.c),
主设备号 MTD_BLOCK_MAJOR = 31。由于 MTD 的擦写特性,此块设备不做磨损均衡,
仅适用于:
- 只读挂载(如挂载 squashfs 镜像)
- 测试环境
- 不需要可靠性的临时存储
对于需要可靠读写的场景,应通过 UBI + UBIFS 而非直接使用 mtdblock。
每个 MTD 设备在 /sys/class/mtd/mtdX/ 下暴露以下属性(由 mtdcore.c 实现):
/sys/class/mtd/mtd0/
├── type # "nand" / "nor" / "mlc-nand" 等
├── flags # 标志位(十六进制)
├── size # 设备总大小(字节)
├── erasesize # 擦除块大小
├── writesize # 最小写入单元
├── subpagesize # 子页大小(writesize >> subpage_sft)
├── oobsize # OOB 字节数
├── oobavail # 可用 OOB 字节数
├── ecc_strength # ECC 强度(可纠正位数)
├── ecc_step_size # ECC 步长
├── bitflip_threshold # bitflip 触发 -EUCLEAN 的阈值(可写)
└── numeraseregions # 擦除区域数(0 表示均匀)
JFFS2(Journalling Flash File System version 2)是 Linux 上第一个广泛使用的 Flash 文件系统(2001 年),由 David Woodhouse 和 Red Hat 开发,直接工作在 MTD 层之上,不需要 UBI。
JFFS2 是一个日志结构文件系统(Log-Structured File System):
- 所有数据和元数据都以"节点"(node)形式顺序追加写入
- 旧版本数据被标记为"过时"(obsolete),不立即擦除
- 垃圾回收进程寻找大部分为过时数据的块,将有效数据搬到新块,然后擦除旧块
- 磨损均衡:通过随机选择擦除目标块(而非总选最"脏"的)实现均匀擦写
JFFS2 在现代系统中已逐渐被 UBIFS 取代,原因:
- 挂载时间:JFFS2 挂载时需要扫描整个 Flash 建立内存索引,大容量 Flash 可能需要数分钟
- 内存占用:Flash 大小与内存中的节点索引成正比,大容量 Flash 内存开销大
- 不支持 UBI:JFFS2 自己管理磨损均衡,与 UBI 重复且不如 UBI 灵活
- 不支持加密:UBIFS 原生支持 fscrypt(Linux 文件系统加密框架)
JFFS2 的源码位于 fs/jffs2/,核心数据结构 struct jffs2_sb_info 和
struct jffs2_inode_info;在小容量(< 8MB)、只读或偶尔写入场景下仍有价值。
| 特性 | JFFS2 | UBIFS |
|---|---|---|
| 底层依赖 | 直接用 MTD | 需要 UBI |
| 挂载时间 | 慢(正比于 Flash 大小) | 快(O(1),或 Fastmap 极快) |
| 内存占用 | 大 | 小(B-tree 按需加载) |
| 压缩 | 支持(zlib/lzo) | 支持(lzo/zlib/zstd) |
| 加密 | 不支持 | 支持 fscrypt |
| 原子操作 | 支持 | 支持(journal + commit) |
| 适用场景 | 小容量、简单嵌入式系统 | 大容量、需要可靠性的系统 |
add_mtd_device()(mtdcore.c L693-L799+)是 MTD 设备注册的核心函数:
add_mtd_device(mtd)
|
+-- 基本校验(writesize != 0,读写 ops 不重叠等)
|
+-- 获取 mtd_table_mutex 全局锁
|
+-- idr_alloc() 分配设备编号
|
+-- 计算 erasesize_shift/mask、writesize_shift/mask
|
+-- MTD_POWERUP_LOCK: mtd_unlock() 解锁
|
+-- 注册 sysfs device(dev_set_name "mtd%d")
|
+-- mtd_check_of_node() 关联 DT 节点
|
+-- mtd_register_nvmem() 注册 NVMEM 提供者
|
+-- 通知所有已注册的 MTD notifier(mtd_notifiers 链表)
|
+-- 创建 debugfs 目录
|
+-- 释放锁
关键校验(mtdcore.c L714-L732):
// 读写 ops 不能同时提供 _read/_write 和 _read_oob/_write_oob
if (WARN_ON((mtd->_write && mtd->_write_oob) ||
(mtd->_read && mtd->_read_oob)))
return -EINVAL;
// 必须有擦除大小和擦除函数(或设置 MTD_NO_ERASE)
if (WARN_ON((!mtd->erasesize || !master->_erase) &&
!(mtd->flags & MTD_NO_ERASE)))
return -EINVAL;mtd->bitflip_threshold 默认值设置(mtdcore.c L753-L754):
if (mtd->bitflip_threshold == 0)
mtd->bitflip_threshold = mtd->ecc_strength;MTD 核心维护一个通知链 mtd_notifiers(mtdcore.c L84),当设备添加/删除时
通知所有订阅者(如 UBI、mtdblock 等):
// 注册通知
void register_mtd_user(struct mtd_notifier *new);
// 注销通知
int unregister_mtd_user(struct mtd_notifier *old);UBI 通过此机制在 MTD 设备出现时自动 attach(如果配置了 ubi.mtd= 内核参数)。
MTD 分区通过 mtd_device_parse_register()(drivers/mtd/mtdpart.c)注册。
分区解析器(Parsers)从不同来源读取分区信息:
| 解析器 | Kconfig | 来源 |
|---|---|---|
cmdlinepart |
CONFIG_MTD_CMDLINE_PARTS |
内核命令行 mtdparts= |
ofpart |
CONFIG_MTD_OF_PARTS |
设备树(Device Tree) |
redboot |
CONFIG_MTD_REDBOOT_PARTS |
RedBoot 分区表 |
tplink |
CONFIG_MTD_TP_LINK_PARTS |
TP-Link 路由器固件格式 |
设备树分区示例:
flash@0 {
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
partition-bootloader@0 {
label = "bootloader";
reg = <0x0 0x100000>;
read-only;
};
partition-kernel@100000 {
label = "kernel";
reg = <0x100000 0x400000>;
};
partition-rootfs@500000 {
label = "rootfs";
reg = <0x500000 0x1b00000>;
};
};
};
用户空间
mount -t ubifs ubi0:rootfs /
|
+-------------------------------+
| UBIFS |
| Journal + B-tree + GC |
+-------------------------------+
| UBI |
| WL + EBA + Bad Block 管理 |
| /dev/ubi0, /dev/ubi0_0 |
+-------------------------------+
| Raw NAND MTD |
| nand_chip + ECC(BCH) |
| /dev/mtd0 |
+-------------------------------+
| NAND 控制器驱动 |
| (如 sunxi_nand, mxc_nand) |
+-------------------------------+
| 硬件 |
+-------------------------------+
内核命令行参数示例:
ubi.mtd=0 root=ubi0:rootfs rootfstype=ubifs
文件系统(squashfs/jffs2)
|
+---------------------------------+
| SPI NOR MTD 层 |
| spi_nor + SFDP 自动识别 |
| /dev/mtd0(bootloader) |
| /dev/mtd1(kernel) |
| /dev/mtd2(rootfs) |
+---------------------------------+
| spi-mem 框架 + SPI 控制器 |
+---------------------------------+
| SPI NOR 芯片(W25Q128 等) |
+---------------------------------+
| 配置项 | 功能 |
|---|---|
CONFIG_MTD |
MTD 核心支持 |
CONFIG_MTD_NAND |
NAND Flash 通用支持 |
CONFIG_MTD_RAW_NAND |
Raw NAND 支持 |
CONFIG_MTD_NAND_ECC_SW_BCH |
软件 BCH ECC |
CONFIG_MTD_SPI_NOR |
SPI NOR Flash 驱动 |
CONFIG_MTD_UBI |
UBI 支持 |
CONFIG_MTD_UBI_FASTMAP |
UBI Fastmap 快速挂载 |
CONFIG_UBIFS_FS |
UBIFS 文件系统 |
CONFIG_JFFS2_FS |
JFFS2 文件系统(传统) |
CONFIG_MTD_PARTITIONED_MASTER |
允许 Master 设备本身也注册 |
CONFIG_MTD_CMDLINE_PARTS |
命令行分区 |
CONFIG_MTD_OF_PARTS |
设备树分区 |
MTD 设备信息:
cat /proc/mtd # 列出所有 MTD 设备
mtdinfo -a # 详细设备信息(mtd-utils)ECC 统计查看:
cat /sys/class/mtd/mtd0/ecc_strength
cat /sys/class/mtd/mtd0/bitflip_thresholdUBI 状态:
ubiinfo -a # UBI 设备信息
ubinfo /dev/ubi0 # 特定 UBI 设备
cat /sys/class/ubi/ubi0/total_eraseblocks
cat /sys/class/ubi/ubi0/bad_peb_countNAND 坏块:
nandtest -k /dev/mtd0 # 坏块扫描(mtd-utils)
flash_erase /dev/mtd0 0 0 # 全片擦除(谨慎使用)内核调试:
通过 debugfs(CONFIG_MTD_UBI_FASTMAP 启用 chk_gen、chk_io 等选项)可模拟
位翻转和 I/O 错误,方便测试 UBI/UBIFS 的错误恢复路径。
Linux MTD 子系统是一个层次清晰、扩展性强的闪存管理框架:
-
MTD 核心(mtd_info) 通过函数指针
_read/_write/_erase/_read_oob等提供统一接口,add_mtd_device()负责注册管理,IDR 分配设备编号。 -
NAND 驱动(nand_chip) 通过三层继承(
nand_device->nand_chip-> 具体控制器驱动)实现抽象,nand_ecc_ctrl管理 ECC 算法和布局,BBT 缓存 坏块信息。 -
UBI 在 MTD 之上实现磨损均衡(WL RB 树)和逻辑块映射(EBA),保护队列 防止热块,后台线程持续均衡磨损,Fastmap 优化启动时间。
-
UBIFS 以日志结构 + B-tree 索引构建在 UBI 之上,多 Journal Head 分流写入, commit 机制保证一致性,LPT 追踪空间状态。
-
SPI NOR(spi_nor) 通过 SFDP 标准自动识别芯片参数,
spi-mem框架统一 了 SPI 存储操作,dirmap 支持直接内存映射实现高性能读取。
由 Claude Code 分析生成
ONFI 是 NAND Flash 互操作性规范,由 Intel、Micron 等厂商于 2006 年联合制定。 ONFI 定义了:
- 命令集:统一的读/写/擦除/复位等命令编码
- Timing 参数:详尽的信号时序要求
- 参数页(Parameter Page):通过
READ PARAMETER PAGE(命令 0xEC)读取 256 字节 芯片信息,包含厂商、型号、几何参数和 timing 模式
ONFI 参数页在内核中由 struct nand_onfi_params 表示
(include/linux/mtd/rawnand.h 第 132 行附近):
/* include/linux/mtd/rawnand.h */
struct nand_onfi_params {
/* revision information and features block */
__le16 revision; /* ONFI 版本(bit0=1.0, bit1=2.0, bit2=3.0...)*/
__le16 features; /* 支持特性位图 */
__le16 opt_cmd; /* 可选命令支持 */
u8 advanced_cmd[2];
/* ... */
/* manufacturer information block */
char manufacturer[12]; /* 厂商名 */
char model[20]; /* 型号 */
u8 jedec_id; /* JEDEC 厂商 ID */
/* memory organization block */
__le32 byte_per_page; /* 每页字节数(不含 OOB)*/
__le16 spare_bytes_per_page;/* 每页 OOB 字节数 */
__le32 pages_per_block; /* 每块页数 */
__le32 blocks_per_lun; /* 每 LUN 块数 */
u8 lun_count; /* LUN 数量 */
/* electrical parameters block */
u8 async_timing_mode; /* 异步 timing 模式位图 (bit N = 支持模式 N) */
/* ... */
};ONFI 定义了 0~5 共 6 种 Timing 模式,数字越大速度越快:
模式 tCLS(min) tALS(min) tRC(min) tWC(min) 最大速度
------ --------- --------- --------- --------- --------
Mode 0 50 ns 50 ns 100 ns 100 ns 10 MB/s
Mode 1 25 ns 25 ns 50 ns 45 ns 20 MB/s
Mode 2 15 ns 15 ns 35 ns 35 ns 28 MB/s
Mode 3 10 ns 10 ns 30 ns 30 ns 33 MB/s
Mode 4 10 ns 10 ns 25 ns 25 ns 40 MB/s
Mode 5 7 ns 7 ns 20 ns 20 ns 50 MB/s
内核中 timing 参数由 struct nand_interface_config 封装
(include/linux/mtd/rawnand.h):
struct nand_interface_config {
enum nand_interface_type type; /* NAND_SDR_IFACE 或 NAND_NVDDR_IFACE */
union {
struct nand_sdr_timings sdr; /* 异步 SDR 时序 */
struct nand_nvddr_timings nvddr; /* NV-DDR 时序 */
} timings;
int cs_hold_delay; /* CS 保持延时(ns)*/
int cs_setup_delay; /* CS 建立延时(ns)*/
};struct nand_sdr_timings 包含约 20 个字段(单位皆为 ps):
struct nand_sdr_timings {
u64 tBERS_max; /* 块擦除时间(最大值)*/
u32 tCCS_min; /* 更换芯片选择建立时间 */
u32 tPROG_max; /* 页编程时间(最大值)*/
u32 tR_max; /* 页读取时间(最大值)*/
u32 tALH_min; /* ALE 保持时间 */
u32 tALS_min; /* ALE 建立时间 */
u32 tARH_min; /* ALE 到 RE# 保持时间 */
u32 tADL_min; /* ALE 到数据加载时间 */
/* ... 约 40 个时序参数 */
}; NAND 控制器驱动初始化
|
nand_scan() -> nand_scan_ident()
|
nand_detect() 读取 ONFI 参数页
|
nand_onfi_detect() 解析 timing 模式
|
nand_choose_best_sdr_timings()
遍历支持的 mode 5..0,选最高兼容模式
|
controller->setup_interface(chip, cs, cfg)
控制器驱动配置硬件寄存器
|
nand_change_interface_config(chip)
发送 SET FEATURES(timing mode) 命令到芯片
ONFI SET FEATURES 命令(0xEF + feature addr 0x01)将 timing 模式写入芯片寄存器:
/* drivers/mtd/nand/raw/nand_base.c */
int nand_set_features(struct nand_chip *chip, int feature_addr,
const u8 *subfeature_param)
{
/* 发送 SET FEATURES 命令 */
chip->legacy.cmd_ctrl(chip, NAND_CMD_SET_FEATURES, ...);
/* 写入 feature address */
chip->legacy.cmd_ctrl(chip, feature_addr, ...);
/* 写入 4 字节参数 */
for (i = 0; i < ONFI_SUBFEATURE_PARAM_LEN; i++)
chip->legacy.write_byte(chip, subfeature_param[i]);
/* 等待 R/B# 信号 */
return nand_wait_ready(chip);
}部分高端 NAND(如 Samsung 的 Toggle Mode DDR)支持双倍数据速率传输。
JEDEC NAND 规范(JESD230)也定义了类似的 NV-DDR 接口,速度可达 200 MB/s 以上。
内核在 nand_detect_interface_config() 中区分 SDR 与 NV-DDR 接口。
MTD 分区子系统位于 drivers/mtd/partitions/,提供将一个物理 MTD 设备划分为
多个逻辑子设备的机制。每个子分区也是一个完整的 struct mtd_info,其 parent
字段指向父设备。
分区解析器列表(Kconfig 选项):
drivers/mtd/partitions/
├── cmdlinepart.c # CONFIG_MTD_CMDLINE_PARTS: bootargs 中指定分区
├── ofpart.c # CONFIG_MTD_OF_PARTS: Device Tree 分区(主流)
├── redboot.c # CONFIG_MTD_REDBOOT_PARTS: RedBoot FIS 表
├── afs.c # CONFIG_MTD_AFS_PARTS: ARM Firmware Suite
└── bcm63xxpart.c # Broadcom 私有分区格式
现代嵌入式系统普遍通过 Device Tree 描述 NAND 分区布局。
典型 DTS 片段:
/* 以 QSPI NAND 为例 */
&qspi {
flash@0 {
compatible = "spi-nand";
reg = <0>;
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
partition@0 {
label = "u-boot";
reg = <0x00000000 0x00100000>; /* 1 MB */
read-only; /* 只读分区 */
};
partition@100000 {
label = "u-boot-env";
reg = <0x00100000 0x00040000>; /* 256 KB */
};
partition@140000 {
label = "kernel";
reg = <0x00140000 0x00800000>; /* 8 MB */
};
partition@940000 {
label = "rootfs";
reg = <0x00940000 0x07000000>; /* 112 MB */
};
};
};
};
ofpart.c 的 parse_ofpart_partitions() 遍历 partitions 子节点,读取
reg 属性为 mtd_partition 数组,然后调用 add_mtd_partitions() 注册。
通过内核命令行(bootargs)指定分区,格式为:
mtdparts=<mtddef>[;<mtddef>]
mtddef := <mtd-id>:<partdef>[,<partdef>]
partdef := <size>[@offset][(<name>)][ro]
示例:
mtdparts=spi0.0:1m(u-boot)ro,256k(u-boot-env),8m(kernel),-(rootfs)
- 表示"剩余所有空间",ro 表示只读。内核启动时由 cmdlinepart.c 的
parse_cmdline_partitions() 解析并注册。
mtd_device_parse_register(mtd, types, &parser_data, parts, nr_parts)
|
+-- 按 types 数组顺序尝试各解析器
| parse_mtd_partitions(master, type, ...)
| -> parser->parse_fn(master, &pparts, data)
|
+-- 如失败,使用静态 parts 数组(驱动硬编码)
|
+-- add_mtd_partitions(master, parts, nr_parts)
for each part:
allocate slave mtd_info
slave->parent = master
slave->_read = mtd_part_read (限制偏移/长度后转发到 parent)
slave->_write = mtd_part_write
slave->_erase = mtd_part_erase
add_mtd_device(slave) -> 注册为独立 MTD 设备
分区的 read/write/erase 操作通过 drivers/mtd/mtd_part.c(或集成在 mtdcore.c)
的包装函数转发到父设备,同时做边界检查:
/* drivers/mtd/mtd_part.c */
static int mtd_part_read(struct mtd_info *mtd, loff_t from, size_t len,
size_t *retlen, u_char *buf)
{
struct mtd_part *part = mtd_to_part(mtd);
/* 检查越界 */
if (from >= mtd->size || len > mtd->size - from)
return -EINVAL;
/* 加上分区起始偏移,转发到父设备 */
return parent->_read(parent, from + part->offset, len, retlen, buf);
}MTD_WRITEABLE 标志(include/linux/mtd/mtd.h)控制分区是否可写。
DTS 中的 read-only 属性会清除此标志:
/* drivers/mtd/partitions/ofpart.c */
if (of_property_read_bool(child, "read-only"))
parts[i].mask_flags |= MTD_WRITEABLE;汉明码是 NAND Flash 最早使用的 ECC 方案,可纠正 1 位错误,检测 2 位错误(1-bit
ECC per 256/512 bytes)。内核实现位于 lib/reed_solomon/ 和 drivers/mtd/nand/。
汉明码计算原理(以 256 字节为例):
256 字节数据:b[0]..b[255],每字节 8 位
共 256 * 8 = 2048 位
校验位计算:
P1 = 所有奇数列位 XOR (列奇偶校验 1)
P2 = 所有偶数列位 XOR (列奇偶校验 2)
P4 = 所有奇数行(上半)XOR
P8 = 所有偶数行(下半)XOR
P16 = 所有奇数位(0,2,4...)XOR
P32 = 所有偶数位(1,3,5...)XOR
(共 6 位行校验 + 6 位列校验 = 12 位,存 22 字节 OOB)
纠错:
纠错码 = 接收到的校验位 XOR 重新计算的校验位
如果非零,指出了错误位的位置 -> 翻转该位
内核 Hamming ECC 实现:drivers/mtd/nand/raw/nand_ecc.c,函数
nand_calculate_ecc() 和 nand_correct_data()。
限制:每 256 字节只能纠 1 位,对于现代 MLC/TLC NAND 的多位错误无能为力。
BCH 是 NAND Flash 现代 ECC 的主流选择,支持纠正多位错误(4-bit、8-bit、 16-bit、24-bit 等)。
内核 BCH 库位于 lib/bch.c,关键 API:
/* lib/bch.c */
struct bch_control *bch_init(int m, int t, unsigned int prim_poly,
bool swap_bits);参数说明:
m:GF(2^m) 域参数,常用 m=13(GF(8192))t:纠错能力(可纠正 t 个错误位)prim_poly:本原多项式(定义 Galois 域)
BCH(1024, 956) 示例:
数据:1024 字节(8192 位)
纠错:4 位
ECC:7 字节(56 位)校验码
OOB 占用:7 字节
BCH(512, 473) 纠 8 位:
ECC:13 字节校验码
BCH 编码步骤:
1. 将数据视为 GF(2^m) 上的多项式 d(x)
2. ECC = d(x) * x^(m*t) mod g(x)
其中 g(x) 是生成多项式(t 个共轭根的最小多项式乘积)
3. 发送 c(x) = d(x) * x^(m*t) + ECC
解码:
1. 计算伴随式 S_i = c(α^i),i=1..2t
2. Berlekamp-Massey 算法求错误定位多项式 σ(x)
3. Chien search 找出 σ(x) 的根(即错误位置)
4. 翻转对应位
NAND 控制器硬件 BCH 示例(以 Allwinner NAND 控制器为例):
/* drivers/mtd/nand/raw/sunxi_nand.c */
/* 控制器支持硬件 BCH,ECC 强度可配置 16/24/28/32/40/48/56/60/64 位 */
static const int sunxi_nand_ecc_strengths[] = {16, 24, 28, 32, 40, 48, 56, 60, 64};LDPC 是 3D TLC/QLC NAND 时代的高级 ECC,主要由 NAND 控制器硬件实现,内核层 通常不直接处理 LDPC(由控制器固件或 FTL 层处理)。
LDPC vs BCH 对比:
特性 BCH LDPC
--------- -------------------- --------------------
类型 代数码 迭代码
纠错能力 中(≤ 60 bit/1KB) 高(≥ 120 bit/1KB)
硬件复杂度 中等 高(需要大量存储)
解码延迟 低(解析算法) 较高(迭代收敛)
实现方式 硬件/软件均可 几乎仅硬件实现
应用场景 SLC/MLC NAND MLC/TLC/QLC NAND
对于软件 ECC 场景,内核优先使用 BCH;LDPC 主要在 SSD 控制器内部使用, Linux MTD 层不暴露 LDPC API。
NAND 类型 典型 RBER(原始误码率) 需要 ECC 强度
--------- ----------------------- -------------
SLC 1e-6 ~ 1e-5 1 bit/512B (Hamming)
MLC 1e-4 ~ 1e-3 8~24 bit/1KB (BCH)
TLC 1e-3 ~ 1e-2 40~60 bit/1KB (BCH/LDPC)
QLC 1e-2 ~ 1e-1 80-120 bit/1KB (LDPC)
nand_ecc_is_strong_enough() 函数(drivers/mtd/nand/raw/nand_base.c)
在初始化时检验 ECC 强度是否满足 ONFI 要求:
/* ECC 强度必须满足:strength / step_size >= nand_chip->base.eccreq.strength / step */
if (chip->ecc.strength * chip->base.eccreq.step_size <
chip->base.eccreq.strength * chip->ecc.size) {
WARN(1, "ECC strength too low for this NAND");
return false;
}BBT(Bad Block Table)将整个 NAND 的坏块信息以位图形式缓存在内存中, 避免每次访问都扫描 OOB 区域。
核心结构(include/linux/mtd/rawnand.h):
struct nand_bbt_descr {
int options; /* BBT 选项标志 */
int pages[NAND_MAX_CHIPS]; /* BBT 存放的页地址(每颗 die) */
int offs; /* 在 OOB 中的偏移 */
int veroffs; /* 版本号在 OOB 中的偏移 */
uint8_t version[NAND_MAX_CHIPS]; /* 各 die 的 BBT 版本号 */
int len; /* BBT 标识字节数 */
int maxblocks; /* BBT 保留的最大块数(从尾部算) */
int reserved_block_code; /* 保留块标识码 */
uint8_t *pattern; /* BBT 标识模式(魔数)*/
};内存 BBT 的位图格式(每块 2 位):
00 = 块好(good)
01 = 块坏(BBT 创建时发现的坏块)
10 = 保留(BBT 本身占用的块)
11 = 坏(运行时发现的新坏块)
一个 PEB 对应 2 位,1KB 可以描述 4096 个块(128MB @ 32KB/block)
BBT 存储在 NAND 芯片末尾几个块(maxblocks 决定,通常 4 个)。
默认主 BBT 使用模式 "Bbt0",镜像 BBT 使用 "1tbB"(反序):
BBT 页布局:
+------------------------------------+
| 魔数 ("Bbt0", 4 字节) | <- OOB[0..3]
| 版本号 (1 字节) | <- OOB[4]
+------------------------------------+
| BBT 位图数据(主区 = page data) |
| 每 2 位描述一个块 |
| ... |
+------------------------------------+
nand_scan()
-> nand_scan_tail()
-> nand_create_bbt()
-> chip->bbt_td 和 chip->bbt_md 描述符(若为 NULL 用默认值)
-> nand_scan_bbt(chip)
+--> 搜索已存在的 BBT(在末尾 maxblocks 个块中查找魔数)
| nand_read_bbt() 将位图读入 chip->bbt[]
|
+--> 如无 BBT(全新芯片或 BBT 损坏):
nand_create_bbt_from_oob()
-> 扫描所有块的 OOB 第一字节:
非 0xFF 判为坏块(工厂标记)
nand_write_bbt() 将新建 BBT 写入 NAND
/* drivers/mtd/nand/raw/nand_base.c */
/* 写入失败时标记坏块 */
static int nand_write_page(struct nand_chip *chip, const uint8_t *buf, ...)
{
ret = chip->ecc.write_page(chip, buf, oob_required, page);
if (ret) {
if (ret == -EIO) {
/* 写失败,标记此块为坏块 */
nand_block_markbad(mtd, page_to_block_offset(chip, page));
}
return ret;
}
return 0;
}nand_block_markbad() 流程:
1. 更新内存 BBT:chip->bbt[block/4] |= 0x01 << ((block & 0x03) * 2)
2. 调用 nand_markbad_bbts():将更新后的 BBT 写回 NAND(更新版本号)
3. 在坏块的 OOB 第一字节写入 0x00(工厂坏块标记格式兼容)
/* include/linux/mtd/rawnand.h */
#define NAND_BBT_LASTBLOCK 0x00000010 /* BBT 存放在最后几个块 */
#define NAND_BBT_ABSPAGE 0x00000020 /* BBT 在绝对页地址 */
#define NAND_BBT_SEARCH 0x00000040 /* 扫描查找 BBT */
#define NAND_BBT_PERCHIP 0x00000080 /* 每 die 有独立 BBT */
#define NAND_BBT_VERSION 0x00000100 /* 使用版本号追踪最新 BBT */
#define NAND_BBT_CREATE 0x00000200 /* 不存在时创建 BBT */
#define NAND_BBT_WRITE 0x00002000 /* 将 BBT 写入 NAND */
#define NAND_BBT_2BIT 0x00004000 /* 每块用 2 位描述 */
#define NAND_BBT_NO_OOB 0x00020000 /* BBT 存在 DATA 区(无 OOB 控制器用)*/UBI 在挂载时需要扫描所有 PEB 的头部(VID header + EC header),每次读取约 需要一次 NAND 页读,对于大容量 NAND(如 4GB = 4096 个 PEB),扫描时间可能 长达数十秒,严重影响启动速度。
Fastmap(CONFIG_MTD_UBI_FASTMAP)将 UBI 的内存数据结构(EBA 表、WL 树等)
序列化后存储在专用 PEB 中,挂载时只需读取少量 Fastmap PEB 即可恢复状态。
传统扫描:
4096 PEBs × 1 次读 × ~100μs = ~409ms
Fastmap 扫描:
Fastmap 通常 1~4 个 PEB × 读取 = ~0.4ms
加上解析时间 ~10ms
总计 < 15ms(提速约 40x)
Fastmap 在磁盘上的布局(drivers/mtd/ubi/fastmap.c):
Fastmap PEB(位于固定 PEB 范围 FM_MAX_START ~ FM_MAX_START+FM_MAX_BLOCKS):
+------------------------+
| EC Header |
+------------------------+
| VID Header (FM类型) |
+------------------------+
| struct ubi_fm_sb | Fastmap 超级块(magic, version, data_crc...)
+------------------------+
| struct ubi_fm_hdr | 头部(free/used/scrub/erase 块计数)
+------------------------+
| struct ubi_fm_scan_pool| FM pool(待处理 PEB 列表)
+------------------------+
| EBA 映射数据 | 所有 volume 的 LEB->PEB 映射
+------------------------+
Fastmap 不在每次 PEB 操作后更新(否则会造成大量写入),而是:
- 卸载时:
ubi_detach_mtd_dev()->ubi_update_fastmap()写入最新状态 - 后台线程:
ubi_bgt_thread()定期检查是否需要更新 - 强制触发:当 FM pool 耗尽时触发更新
SPI NAND(通过 SPI 总线连接的 NAND Flash)在内核中由 drivers/mtd/nand/spi/
管理,与 Raw NAND 共享部分基础设施:
SPI NAND 驱动栈:
+---------------------------------+
| MTD 核心层(mtd_info) |
+---------------------------------+
| NAND 通用层(nand_device) | <-- 共享 BBT、ECC 框架
+---------------------------------+
| SPI NAND 核心(spinand_core.c)|
+---------------------------------+
| SPI NOR/NAND 抽象(spi-mem) | <-- spi_mem_exec_op()
+---------------------------------+
| SPI 控制器驱动 |
+---------------------------------+
核心文件:
drivers/mtd/nand/spi/core.c:SPI NAND 通用逻辑drivers/mtd/nand/spi/gigadevice.c:GigaDevice SPI NANDdrivers/mtd/nand/spi/macronix.c:Macronix SPI NANDdrivers/mtd/nand/spi/micron.c:Micron SPI NANDdrivers/mtd/nand/spi/winbond.c:Winbond SPI NAND
/* drivers/mtd/nand/spi/spinand.h */
struct spinand_device {
struct spi_mem *spimem; /* SPI 内存设备句柄 */
struct mutex lock; /* 并发保护 */
struct spinand_id id; /* 芯片 ID */
const struct spinand_info *info; /* 芯片参数表 */
/* 操作模板(SPI 命令) */
struct spi_mem_op op_templates;
struct {
const struct spi_mem_op *read_cache;
const struct spi_mem_op *write_cache;
const struct spi_mem_op *update_cache;
} op_templates;
/* 数据/OOB 缓冲区 */
struct {
unsigned int buf_size;
void *buf;
void *databuf;
void *oobbuf;
} scratchbuf;
bool cont_read_possible;/* 支持连续读 */
struct nand_device base; /* 继承自 nand_device */
};SPI NAND 的操作与 Raw NAND 的主要差异在于命令通过 SPI 协议发送:
读操作:
1. PAGE READ (0x13) + 24-bit 行地址
-> 芯片将页数据从 NAND array 搬到内部 cache
2. GET FEATURE (0x0F) 轮询 OIP 位(操作进行中)
-> 等待 OIP=0
3. READ FROM CACHE (0x03) + 16-bit 列地址
-> 从内部 cache 通过 SPI 读取数据
写操作:
1. WRITE ENABLE (0x06)
2. PROGRAM LOAD (0x02) + 列地址 + 数据(写入内部 cache)
3. PROGRAM EXECUTE (0x10) + 行地址(将 cache 写入 array)
4. GET FEATURE 轮询 OIP + PROG_FAIL 位
擦除操作:
1. WRITE ENABLE (0x06)
2. BLOCK ERASE (0xD8) + 行地址
3. GET FEATURE 轮询 OIP + ERASE_FAIL 位
高速 SPI NAND 支持四线(Quad SPI)和八线(Octal SPI)传输:
SPI 模式 MISO 线数 最大速度(@104MHz 时钟)
-------------- --------- ---------------------------
Standard SPI (x1) 1 ~13 MB/s
Dual SPI (x2) 2 ~26 MB/s
Quad SPI (x4) 4 ~52 MB/s
Octal SPI (x8) 8 ~100+ MB/s
内核通过 spi_mem_op 的 buswidth 字段指定线宽:
#define SPINAND_PAGE_READ_FROM_CACHE_X4_OP(addr, ndummy, buf, len) \
SPI_MEM_OP(SPI_MEM_OP_CMD(0x6B, 1), /* 命令单线 */ \
SPI_MEM_OP_ADDR(2, addr, 1), /* 地址单线 */ \
SPI_MEM_OP_DUMMY(ndummy, 1), /* 虚拟字节 */ \
SPI_MEM_OP_DATA_IN(len, buf, 4)) /* 数据四线 */JFFS2 在大容量 NAND 上存在以下根本性问题:
启动时间问题:
JFFS2 挂载流程:
1. 扫描整个 MTD 设备,读取所有节点头部
2. 建立内存中的 inode 树和节点映射
3. 识别过时(obsolete)节点
对于 256MB NAND(1024 个 128KB 块):
每块读取时间 ~2ms(加上 ECC)
扫描时间 ≈ 1024 × 2ms = 2 秒
对于 1GB NAND:8 秒!
内存消耗问题: JFFS2 需要在内存中为每个节点保留元数据,内存占用随存储容量线性增长。 对于 256MB 分区,内存占用可达数十 MB,在内存受限的嵌入式设备上难以接受。
GC 效率问题: JFFS2 的 GC 粒度为整个擦除块,需要将块内所有有效节点迁移到其他块, 在随机写场景下 GC 会造成大量写放大。
YAFFS2(Yet Another Flash File System 2)是 Android 系统早期(2.2 之前) 采用的 NAND 文件系统,特点:
- 专为 NAND 设计,节点以 NAND 页为单位
- 每页存储一个对象块(object chunk)+对象头
- 使用"分代垃圾回收"减少 GC 延迟
- 历史意义:Android 3.x 之后被 ext4(配合 eMMC 的 FTL)替代, 再后来 Android 10+ 系统分区改用 EROFS
文件系统 类型 MTD/块设备 压缩 日志 适用场景
--------- --------- ---------- ---- ---- --------
JFFS2 读写 LFS MTD 软件 是 小容量 NOR/NAND(传统)
YAFFS2 读写 MTD 否 是 NAND(Android 旧版)
UBIFS 读写 B-tree MTD+UBI 软件 是 大容量 NAND(主流嵌入式)
F2FS 读写 LFS 块设备 软件 是 eMMC/UFS/SSD(Android 现代)
EROFS 只读 块设备 硬件 否 系统分区(Android/容器)
SquashFS 只读 块设备 zlib 否 LiveCD/路由器(传统只读)
特性比较:
JFFS2 UBIFS
挂载时间 O(容量) O(1)(日志重放)
内存占用 O(容量) O(目录树深度)
并发性 单一挂载 支持并发读写
压缩 zlib/lzo lzo/lz4/zstd
日志 无独立日志 有 Journal Heads
坏块处理 直接在 MTD 上 通过 UBI 抽象
大容量支持 较差(>256MB 慢) 良好(GB 级)
工具名 功能
----------- --------------------------------------------------
flash_erase 擦除 MTD 分区(支持 -j 选项创建 JFFS2 擦除标记)
nandwrite 将镜像写入 NAND(跳过坏块)
nanddump 读取 NAND 内容(含 OOB)
flash_otp_info 读取 OTP 区域信息
mtdinfo 显示 MTD 设备详细信息
ubiformat 格式化 MTD 设备为 UBI
ubinize 创建 UBI 镜像文件
ubiattach 将 UBI 镜像附加到 MTD 设备
ubimkvol 创建 UBI 卷
ubirmvol 删除 UBI 卷
ubiupdatevol 更新 UBI 卷内容
cat /proc/mtd
# 输出示例:
# dev: size erasesize name
# mtd0: 00100000 00020000 "u-boot" (1MB, 128KB/block)
# mtd1: 00040000 00020000 "u-boot-env" (256KB)
# mtd2: 00800000 00020000 "kernel" (8MB)
# mtd3: 07000000 00020000 "rootfs" (112MB)CONFIG_MTD_TESTS # MTD 测试模块(mtd_torturetest、mtd_stresstest 等)
CONFIG_MTD_UBI_DEBUG # UBI 调试(fastmap 一致性检查)
CONFIG_UBIFS_FS_DEBUG # UBIFS 调试(挂载时额外一致性验证)
CONFIG_MTD_NAND_ECC_SW_BCH # 软件 BCH ECC(无硬件 ECC 时使用)
场景一:ECC 错误率统计
# 通过 debugfs 查看 UBI ECC 统计(需 CONFIG_MTD_UBI_DEBUG)
cat /sys/kernel/debug/ubi/ubi0/detailed_erase_counters
# MTD ECC 统计
cat /sys/class/mtd/mtd0/ecc_failures
cat /sys/class/mtd/mtd0/corrected_bits场景二:NAND 坏块分布
# 查看 BBT 内容(十六进制)
cat /sys/kernel/debug/mtd/mtd0/bbt
# 查看坏块数量
cat /sys/class/ubi/ubi0/bad_peb_count场景三:UBI 磨损均衡状态
# EC 分布直方图(需 debugfs)
cat /sys/kernel/debug/ubi/ubi0/wear_leveling_worker
# 查看最大、最小、平均 EC
ubiinfo -a /dev/ubi0场景四:ftrace 跟踪 MTD 操作
# 追踪 MTD 读写操作
echo 'mtd_read mtd_write mtd_erase' > /sys/kernel/tracing/set_ftrace_filter
echo function > /sys/kernel/tracing/current_tracer
echo 1 > /sys/kernel/tracing/tracing_on
# 执行操作后查看 trace
cat /sys/kernel/tracing/traceNAND Flash Floating Gate Cell(浮栅型):
Control Gate (CG)
|
========= <- 绝缘层(ONO: Oxide-Nitride-Oxide)
| F.Gate | <- 浮栅(存储电荷)
========= <- 隧道氧化层(~8nm)
[ 沟道 ] <- p 型半导体
S D <- 源极和漏极
SLC: 1 bit/cell(2 个阈值电压状态)
+-----+-----+
| 擦除| 编程 | 阈值电压
| 0 | 1 |
+-----+-----+ -3V 0V 3V 6V
MLC: 2 bits/cell(4 个状态)
+--+--+--+--+
|11|10|00|01| 4 个电压窗口需精确控制
+--+--+--+--+ -3V 0V 2V 4V 6V
TLC: 3 bits/cell(8 个状态)
|111|110|101|100|011|010|001|000|
8 个电压窗口,更小的间距 -> 更高误码率
NAND 类型 典型 P/E 寿命 实际使用场景
--------- ------------- ----------------
SLC 100,000 次 工业级、高可靠场景
eMLC 30,000 次 企业 SSD、工业存储
MLC 3,000~10,000 次 消费级 SSD
TLC 500~3,000 次 主流消费 SSD、手机存储
QLC 100~500 次 大容量冷存储
软件层(UBI/FTL)通过磨损均衡延长有效寿命:
有效寿命 ≈ 单块寿命 × 存储总容量 / 实际数据量
现代 3D NAND(如三星 V-NAND、Intel 3D XPoint 外的 QLC NAND)大多采用 Charge Trap Flash 而非传统浮栅技术:
- 电荷存储在氮化物层的陷阱(trap)中,而非金属浮栅
- 更好的隔离特性,减少单元间串扰(Cell-to-Cell Interference)
- 适合 3D 堆叠(TANOS 结构)
内核层面对 CTF 和浮栅 NAND 透明处理,差异由控制器固件吸收。
由 Claude Code 分析生成