Linux MTD 子系统 Nor Flash 驱动全链路剖析:从 CFI 命令集到 JFFS2 挂载的寄存器级追踪

📅 2026/7/16 16:49:40
Linux MTD 子系统 Nor Flash 驱动全链路剖析:从 CFI 命令集到 JFFS2 挂载的寄存器级追踪
Linux MTD 子系统 Nor Flash 驱动全链路剖析从 CFI 命令集到 JFFS2 挂载的寄存器级追踪一、Nor Flash 驱动的真实战场当内核日志刷屏unsupported flash时在嵌入式 Linux BSP 移植中Nor Flash 驱动的调试往往是板级 bring-up 的第一道硬仗。内核启动到 MTD 探测阶段串口输出physmap-flash: unsupported flash type后直接 panic原因是看似标准的 CFI 查询命令未能正确返回 Manufacturer ID 和 Device ID。这个问题的根因在于 Nor Flash 驱动的复杂性被严重低估。CFICommon Flash Interface查询需要分三步发送0x98进入 CFI 查询模式、读取结构体解析命令集、发送0xF0退出查询回到读阵列模式。任何一个步骤的时序、电压或总线配置错误都会导致cfi_probe失败。更隐蔽的问题是不同厂商的 CFI 实现存在微妙差异SST 的 Auto-Select 命令是0x90而 Intel 是0x90后跟不同地址偏移。MTDMemory Technology Device子系统在 Linux 内核中承担着抽象角色的重任对上向文件系统如 JFFS2、UBIFS提供统一的读写擦除接口对下封装 Nor Flash、NAND Flash、DataFlash 等设备的差异性。理解 MTD 与 Nor Flash 驱动的完整数据流路径是构建可靠嵌入式存储方案的前提。二、MTD 子系统的分层架构与 CFI 探测流程Linux MTD 子系统的设计遵循分层解耦原则从文件系统到硬件设备数据流经过多层抽象CFI 探测的最底层入口位于drivers/mtd/chips/cfi_probe.c通过向 Flash 芯片写入特定命令序列来读取芯片信息。Nor Flash 的命令接口通过总线写特定地址实现无需独立的命令引脚/* CFI 探测的核心逻辑 — 发送查询命令并解析 CFI 结构体 */ static int cfi_probe_chip(struct map_info *map, __u32 base, unsigned long *chip_offset, struct flchip *chip) { int ret; struct cfi_private *cfi; /* 1. 发送进入 CFI 查询模式的命令 */ /* CFI 查询命令为 0x98写入芯片基地址偏移 0x55 */ cfi_send_gen_cmd(0x98, 0x55, base, map, cfi, cfi-device_type, NULL); /* 2. 读取 CFI 查询结构体 — 从偏移 0x10 开始 */ /* 结构体以 Q, R, Y 三个 ASCII 字符开头 */ __u8 qry[3]; qry[0] cfi_read_query(map, base 0x10); qry[1] cfi_read_query(map, base 0x11); qry[2] cfi_read_query(map, base 0x12); /* 3. 验证 CFI 签名 */ if (qry[0] ! Q || qry[1] ! R || qry[2] ! Y) { printk(KERN_ERR cfi_probe: Bad CFI signature at 0x%lx\n, base); goto exit_probe; } /* 4. 解析 CFI 结构体字段 */ /* 偏移 0x13: 主版本号, 0x14: 次版本号 */ cfi-cfiq-P_ID cfi_read_query(map, base 0x13) | (cfi_read_query(map, base 0x14) 8); /* 偏移 0x1B: 芯片擦除超时典型值 (2^N ms) */ cfi-cfiq-DevInterfaceDesc cfi_read_query(map, base 0x1B); /* 偏移 0x27: 芯片尺寸 (2^N 字节) */ cfi-cfiq-DevSize 1 cfi_read_query(map, base 0x27); /* 偏移 0x2A-0x2B: 擦除区域描述 */ cfi-cfiq-NumEraseRegions cfi_read_query(map, base 0x2C); /* 5. 发送退出 CFI 查询模式的命令 — 回到读阵列模式 */ cfi_send_gen_cmd(0xF0, 0x00, base, map, cfi, cfi-device_type, NULL); /* 6. 匹配命令集并初始化 chip 结构体 */ ret cfi_cmdset_setup(map, cfi, chip_offset, chip); if (ret ! 0) { printk(KERN_ERR cfi_probe: Failed to setup command set\n); goto exit_probe; } return 0; exit_probe: cfi_send_gen_cmd(0xF0, 0x00, base, map, cfi, cfi-device_type, NULL); return -ENODEV; }CFI 探测成功后MTD 核心会为芯片创建设备节点。关键的数据结构是struct mtd_info它持有 Flash 芯片的全部几何参数与操作函数指针。分区信息由mtdpart.c管理支持静态分区表通过内核 cmdline 或设备树和动态分区RedBoot 或命令行解析。JFFS2 文件系统最终通过mtd-_read、mtd-_write、mtd-_erase三个函数指针对 Flash 进行操作。三、physmap 映射驱动与 JFFS2 挂载流程通用物理映射驱动physmap.c负责将 Nor Flash 的物理地址映射到内核虚拟地址空间是 Nor Flash 驱动栈中最简单但最常用的映射层实现/* drivers/mtd/maps/physmap.c 核心映射逻辑示意 */ static int physmap_flash_probe(struct platform_device *dev) { struct physmap_flash_data *physmap_data; struct physmap_flash_info *info; struct resource *res; int err 0; void __iomem *virt_addr; /* 1. 从设备树或平台数据获取物理地址资源 */ res platform_get_resource(dev, IORESOURCE_MEM, 0); if (!res) { dev_err(dev-dev, No memory resource for Nor Flash\n); return -ENXIO; } /* 2. 将物理地址 ioremap 到内核虚拟地址空间 */ /* 关键: Nor Flash 是内存映射设备读操作直接通过指针解引用 */ virt_addr devm_ioremap_resource(dev-dev, res); if (IS_ERR(virt_addr)) { dev_err(dev-dev, ioremap failed for Nor Flash\n); return PTR_ERR(virt_addr); } /* 3. 填充 map_info 结构体MTD 核心通过它访问硬件 */ info-map.phys res-start; info-map.size resource_size(res); info-map.virt virt_addr; info-map.name dev_name(dev-dev); info-map.bankwidth physmap_data-width; /* 1/2/4 字节总线宽度 */ /* 4. 调用 MTD 核心的 do_map_probe 进行 CFI 探测 */ /* cfi_probe 是可选的探测驱动名 */ info-mtd do_map_probe(cfi_probe, info-map); if (!info-mtd) { /* 降级尝试 JEDEC 探测 */ info-mtd do_map_probe(jedec_probe, info-map); } if (!info-mtd) { dev_err(dev-dev, No supported flash detected\n); return -ENODEV; } /* 5. 注册到 MTD 核心框架创建 /dev/mtdX 设备节点 */ err mtd_device_parse_register(info-mtd, part_probe_types, NULL, physmap_data-parts, physmap_data-nr_parts); if (err) { mtd_device_unregister(info-mtd); return err; } dev_info(dev-dev, Nor Flash mapped: %s (%d KB)\n, info-mtd-name, info-mtd-size / 1024); return 0; }当 JFFS2 通过mount -t jffs2 /dev/mtdblock3 /mnt/flash挂载时内核执行路径是VFS →jffs2_mount→jffs2_do_fill_super→ 扫描 Flash 上的节点和数据。JFFS2 在挂载时会逐块扫描 Flash通过读取每个节点的 inode 号和版本号重建内存中的文件系统树。/* JFFS2 挂载时扫描的核心函数 — 重建文件系统元数据树 */ static int jffs2_build_filesystem(struct jffs2_sb_info *c) { int ret; struct jffs2_inode_cache *ic; uint32_t block; /* 1. 遍历所有擦除块 */ for (block 0; block c-nr_blocks; block) { struct jffs2_eraseblock *jeb c-blocks[block]; /* 跳过已标记为坏块或正在擦除的块 */ if (jeb-bad || (jeb-used_size 0 jeb-dirty_size 0)) continue; /* 2. 扫描块内的每个节点 */ ret jffs2_scan_eraseblock(c, jeb, jeb-offset, buf, buf_size); if (ret 0) { JFFS2_ERROR(Scan failed at block %u\n, block); return ret; } /* 3. 统计擦除块使用情况 */ jeb-used_size ret; jeb-free_size c-sector_size - jeb-used_size; } /* 4. 重建目录树 — 将扫描到的 inode 挂接到 VFS 树中 */ for_each_inode(i, c, ic) { if (ic-state INO_STATE_READING) continue; ret jffs2_do_read_inode(c, ic-ino, ic-scan_dents); if (ret) { JFFS2_ERROR(Failed to read inode #%u\n, ic-ino); return ret; } } return 0; }四、JFFS2 的擦写放大与内存占用的边界约束JFFS2 的设计目标是为 Nor Flash 提供日志结构文件系统所有更新以追加写方式进行通过垃圾回收合并有效数据、释放脏空间。但这一设计在 Nor Flash 密度增长后面临两个致命边界。第一挂载时间与 Flash 容量呈线性关系。JFFS2 挂载时必须扫描全部擦除块以重建 inode 树在 128MB Nor Flash 上可能需要数秒在 512MB 上可能长达数十秒——这对工业设备的快速启动是灾难性的。UBIFS 通过超级块中的索引信息跳过了全量扫描实质上是 JFFS2 在大容量场景下的替代方案。第二垃圾回收触发不确定的擦写放大。JFFS2 的 GC 在干净块不足时被触发GC 过程中需要将有效数据从旧块复制到新块然后擦除旧块。最坏情况下一次 4KB 写入可能触发多次块擦除和数十 KB 的数据搬迁擦写放大因子可达 10 倍以上。在对于电源不稳定场景GC 过程中的断电可能使文件系统处于部分已搬迁的状态JFFS2 的扫描恢复机制虽能修复但会增加下次挂载时间。五、总结Linux MTD 子系统与 Nor Flash 驱动的完整链路涉及 CFI 协议、命令集匹配、物理地址映射、分区管理和文件系统适配五个层次。工程实践中需要重点把握CFI 探测的时序正确性是 Nor Flash 驱动 bring-up 的第一关命令序列发送前后必须正确切换芯片模式physmap 驱动的 ioremap 映射为后续所有操作提供虚拟地址入口bankwidth 参数必须与硬件总线宽度匹配JFFS2 适用于 16~128MB 的 Nor Flash场景超出此范围应评估 UBIFS 或 YAFFS2挂载时间与 GC 擦写放大的边界受 Flash 容量直接影响需在设计阶段建立擦写预算模型。