Linux MTD 层与设备树配置
MTD 子系统、SPI MEM 框架、设备树描述与分区设计 —— 一份以「裸器件到挂载点」为主线的软件设计参考 | 编制日期 2026-10-14 | 版本号 Rev 1.0
把一颗裸 Flash(raw NAND / SPI NAND / SPI NOR)接进 Linux,让它在开机后变成一个能读写、能掉电安全、能被 OTA 更新的文件系统,中间要经过一条很长的软件链:设备树描述器件、控制器驱动接管总线、MTD 核心层统一抽象、分区表切分地址空间、UBI 承担磨损与坏块、UBIFS 或 SquashFS 提供文件接口,最后才是 mount 与启动脚本。这条链上任何一环写错,症状都出现在最后一环——「根文件系统挂不上」。
这份文档只讲这条链的软件侧配置与设计:MTD 核心抽象是什么、控制器驱动怎么分层、spi-mem 框架怎么描述一条总线操作、设备树里每个属性对应驱动的哪个参数、分区表按什么原则切、UBI/UBIFS 分别在替谁干活、启动链路三处配置怎么保持一致。文中只写机制、属性名、结构名与配置顺序,所有具体数值一律以正式 datasheet / 内核绑定文档 / 器件手册为准。
结论: Linux 用两条路径对待存储器件——FTL 在器件内的走块设备(eMMC / UFS / SD),FTL 在软件侧的走 MTD 子系统(raw / SPI NAND); MTD 配置的正确形态是三件事同时成立:设备树把器件与分区描述清楚、控制器驱动把 ECC / 坏块 / 时序管起来、UBI 与 UBIFS 把磨损均衡和文件级掉电安全接住。 这条链的设计动作可以压缩成一句话:按绑定文档写设备树、按擦除块切分区、按最坏情况调 ECC 与坏块策略、按掉电场景选文件系统。 所有属性名与参数值以内核绑定文档与器件 datasheet 的正式版本为准。
读者路径
- 刚接手裸 Flash 适配,不知道从哪下手:第 1 章(两条路径怎么分)+ 第 5 章(设备树怎么写)+ 第 9 章(启动链路怎么配)。
- 设备树写了但驱动不认、分区不出来:第 4 章(spi-mem 与存储驱动)+ 第 5 章(属性与驱动参数的对应)+ 第 10 章(调试接口)。
- 要定分区表、支持 OTA:第 6 章(分区表设计)+ 附录 B(分区布局模板)。
- 关心掉电安全与可写数据:第 7 章(UBI 与 UBIFS)+ 第 8 章(挂载参数)+ 第 10 章(掉电测试)。
- 从厂商 BSP 往新内核移植:第 11 章(版本差异与移植检查单)。
- 只想要结论:第 12 章(要点汇总与配置-验证闭环)。
前言:关于这份参考文档
本文的读者是做 BSP、驱动与系统集成的工程师:需要把一颗存储器件接进 Linux、把分区表定下来、让系统稳定启动并可在现场升级。阅读前不需要你是 MTD 专家,但需要你大概知道设备树与内核驱动的基本关系。
三条约定先说清楚。其一,只写机制不写死数值:全文不出现寄存器地址、位域位置、属性字节默认值与具体 opcode 数值;出现的属性名(如 compatible、reg、partition 子节点)与结构名(如 mtd_info、spi_mem)都是可以照写的配置接口,但它们的取值一律以正式 datasheet / 绑定文档为准,量级性描述仅用于建立判断。其二,不含商业数据:不涉及任何单价、份额、产能与厂商排名,选型讨论只围绕技术机制。其三,引图用章号:正文引用插图一律写「第 N 章」,不依赖图号跳转,图号仅作检索。
本文是存储技术系列中的「软件配置」一篇:器件侧的电气与架构在系列其他篇中讨论,本篇聚焦从设备树一个属性到 mount 成功一次之间的全部软件决策,并在第 12.3 节说明与系列其他篇的衔接关系。
1. 问题本质:Linux 怎么看待一块存储器件
这一章不涉及任何具体配置,只把问题还原成机制:Linux 面对一颗存储器件时到底有几种用法、MTD 子系统在软件栈里站在什么位置、裸器件与块设备的边界划在哪里。后面所有章节的判断——设备树怎么写、ECC 怎么配、分区怎么切——都是这一章三条边界的具体投影。
1.1 从块设备到裸器件的两条路径
同一颗存储介质,接进 Linux 的方式由「谁来做 FTL」决定。FTL(Flash Translation Layer)是 Flash 与应用之间那一层映射与管理者:把「写前必须擦除的块」伪装成「可以覆写的扇区」、处理坏块替换、做磨损均衡。这层工作由谁承担,就决定了内核怎么看待这颗器件:
- 路径一:当块设备用。器件内部(eMMC / UFS 的控制器,SD 卡内部)已经内置了完整的 FTL,主机看到的就是一个规整的块设备——512B 或 4KB 扇区、可随机覆写、坏块被内部替换。Linux 侧只需要块设备驱动(
mmc/scsi/mmcblk),文件系统直接架在上面。 - 路径二:走 MTD 子系统当裸器件用。器件是裸片(raw NAND / SPI NAND)或只做了最少管理的介质(SPI NOR),没有内部 FTL。Linux 侧必须自己完成映射、ECC、坏块与磨损管理,入口就是 MTD(Memory Technology Device)子系统。
两条路径没有优劣,只有分工不同:块设备路径把复杂度卖给器件供应商,MTD 路径把控制权留给软件。需要深度参与掉电策略、需要现场可预测的擦写行为、或者器件本身就是裸片时,MTD 是唯一选择;反之,如果器件已经内置控制器且容量成本合适,没有必要把 FTL 的活接到软件侧。
| 维度 | 块设备路径 | MTD 路径 |
|---|---|---|
| FTL 位置 | 器件内部(eMMC / UFS / SD 控制器) | 软件侧(控制器驱动 + UBI / 文件系统) |
| 访问单位 | 逻辑扇区(512B / 4KB,可覆写) | 擦除块与页,写前必须擦除 |
| 坏块管理 | 器件内部替换,对主机透明 | 驱动维护坏块表(BBT),软件侧跳过 |
| ECC | 器件内部或对主机隐藏 | 驱动显式配置(片上 / 控制器 / 软件) |
| 磨损均衡 | 器件内部 | UBI 或文件系统层(JFFS2 一类) |
| 典型器件 | eMMC / UFS / SD 卡 | raw NAND(并行 / SPI)/ SPI NAND / SPI NOR |
| 内核子系统 | block 层 + mmc / scsi 驱动 | mtd 子系统(+ ubi 上层) |
| 典型文件系统 | ext4 / f2fs | SquashFS(只读)/ UBIFS / JFFS2(可写) |
| 掉电语义 | 依赖器件内部兜底,主机不可见 | 软件侧必须显式设计(日志、原子提交) |
有些器件「看起来是块设备,其实是 MTD」:例如 SPI NAND 在内核里可以挂 mtdblock 生成一个块设备视图,或者用 ubiblock 把 UBI 卷伪装成只读块设备。它们解决的是「让 ext4 这类只认块设备的文件系统有地方挂」,并没有改变 FTL 在软件侧这个事实——坏块、磨损、掉电语义仍然由 MTD / UBI 兜着。看到 /dev/mtdblockX 时,不要按 eMMC 的经验推断它的行为。
图 1 是全文的总图:两条路径共享顶层的应用与 VFS,分叉发生在「谁把块介质管理掉」这一层。右半侧就是本文的主线——MTD 子系统把裸器件抽象成「擦除块 + 页 + OOB」的统一视图,控制器驱动与 spi-mem 框架负责把一条条操作送到器件,文件系统(UBIFS / JFFS2 / SquashFS)在 MTD 之上提供文件语义。这条链上的每一层,后面都有一章专门拆解。
1.2 MTD 子系统的定位
MTD 子系统的定位可以用一句话概括:它是裸 Flash 在 Linux 里的「设备类」,就像网络子系统是网卡的设备类、输入子系统是键鼠的设备类。它不提供文件语义,也不做磨损均衡;它只负责把「一块会坏块、要擦除、带 OOB、要 ECC」的介质,抽象成一个接口稳定的字符设备与一套操作回调,让上层的文件系统和下层的控制器驱动都能各自演进。
这个定位带来三个直接后果。其一,文件系统必须懂 Flash:架在 MTD 上的是 UBIFS / JFFS2 / SquashFS 这类原生支持裸器件的文件系统,而不是 ext4——ext4 假设底层是可覆写块设备,直接架在裸 NAND 上会反复触发读-改-写,把磨损与坏块问题放大。其二,驱动必须显式管理缺陷:ECC 强度、坏块策略、时序参数都要在驱动与设备树里显式声明,没有「默认值兜底」这回事。其三,工具链独立:mtd-utils(mtdinfo / flash_erase / nandwrite / ubiformat)是 MTD 世界的标准工具,调试与量产烧录都围绕它们组织。
| MTD 提供的能力 | 说明 | 深入章节 |
|---|---|---|
| 统一器件抽象 | mtd_info 描述容量、擦除块大小、页大小与 OOB 布局 | 第 2 章 |
| 操作回调 | read / write / erase / OOB 访问的驱动实现入口 | 第 2 章 |
| 分区机制 | 把一个 mtd 器件切成多个 mtdX,边界按擦除块对齐 | 第 2 章 / 第 6 章 |
| 字符设备与 sysfs | /dev/mtdX、ioctl 与 sysfs 属性,供用户态查询与操作 | 第 2 章 |
| 器件驱动框架 | raw NAND 核心层、spi-mem 抽象与 spi-nor / spi-nand 驱动 | 第 3 章 / 第 4 章 |
| UBI 上层 | 卷、逻辑-物理块映射、磨损均衡、坏块透明化 | 第 7 章 |
图 2 是 MTD 世界的分层地图。记住两个边界:MTD 核心层与文件系统之间的契约是「块设备语义的裸器件」——文件系统拿到的是「可读可写可擦的块」,不需要知道坏块在哪;MTD 核心层与驱动之间的契约是「操作回调」——核心层说「读这一页」,驱动负责把这条命令连同时序、ECC、坏块检查一起送到器件。中间三层的差异(NAND 还是 SPI、什么线宽、什么 ECC 引擎)全部被吸收在分层内部,这就是 MTD 抽象的价值,也是第 2 章要逐层展开的内容。
1.3 raw 器件与块设备的边界
「裸器件」和「块设备」的差别不是速度、不是容量,而是访问契约。块设备的契约是:给你一个逻辑扇区号,你可以随时读、随时覆写,原子性由器件保证。裸器件的契约是:读以页为单位,写只能把位从 1 改成 0,把 0 改回 1 必须整块擦除,坏块可能出现且必须避开,位错误可能出现且必须纠正。下表把两条契约的差异逐条列出——这些差异就是后面每一章要处理的问题清单。
| 对比项 | raw 器件(MTD 视图) | 块设备(FTL 之后) |
|---|---|---|
| 写入前操作 | 必须先擦除整个擦除块 | 直接覆写目标扇区 |
| 最小写单位 | 页;页内只能顺序写、不能回头改 | 扇区(512B / 4KB) |
| 坏块 | 出厂与使用中都会出现,软件标记并跳过 | 内部替换,主机不可见 |
| 读干扰 / 电荷保持 | 需要软件策略关注(读阈值、定期巡检) | 器件内部处理 |
| 掉电中断 | 可能使整块进入不确定状态,需软件恢复 | 由器件内部兜底 |
| ECC | 驱动显式配置强度与步长 | 对主机隐藏 |
| 容量口径 | 按擦除块报告,坏块不计入可用空间 | 按扇区报告,容量固定 |
图 3 把两条契约并排画开:左边每一条「EB」里装着若干页,擦除的动作作用于整条;右边每一条「LBA」里装着若干扇区,覆写直接落在扇区上。中间那一箭就是 FTL——它存在的全部意义就是把左边的契约翻译成右边的契约。MTD 路径把这层翻译工作接回了软件侧,于是第 2 章的核心抽象、第 3 章的 ECC 配置、第 6 章的分区对齐、第 7 章的 UBI 映射,本质上都是在回答「没有 FTL 之后,谁来干这些活」。
看到 /dev/mtdblockX 就按 eMMC 的经验往上架 ext4,是 MTD 世界最高频的误用。mtdblock 是简陋的可写块设备仿真:写入靠读-改-擦-写缓存完成,不管坏块新增、不做磨损均衡、掉电易损坏,只适合只读挂载(SquashFS 一类),任何可写场景一律走第 7、8 章的 UBI / UBIFS;ubiblock 只用于给只读镜像(SquashFS / 只读 ext4)提供块设备视图。
1.4 路径选择的三个输入
「用块器件还是 MTD」这个问题,在项目里由三个输入共同决定,缺一个都会选错。器件类型是硬约束:裸片没有内置 FTL,除非外加控制器,否则只能走 MTD;eMMC / UFS 内置控制器,走块设备是天经地义。系统需求是软约束:需要现场可预测的擦写行为、需要掉电安全由自己掌握、需要固件级的数据恢复能力时,MTD + UBI 给的控制权更大。生态与成本是现实约束:MTD 侧的驱动、分区、UBI、文件系统、烧录工具都要自己搭,团队没有相关积累时,选内置 FTL 的器件更稳。
| 输入 | 要回答的问题 | 导向 |
|---|---|---|
| 器件类型 | 器件是否内置控制器与 FTL | 内置 → 块设备路径;裸片 → MTD 路径 |
| 系统需求 | 掉电安全、磨损可预测性、数据恢复是否要自主可控 | 要求越高 → 越倾向 MTD + UBI 的显式设计 |
| 生态与成本 | 驱动、工具、烧录、人力的投入是否可承受 | 投入有限 → 优先内置 FTL 的块器件方案 |
| 容量与成本结构 | 大容量、高吞吐、随机写密集 | 块器件内置 FTL 通常更划算 |
| 寿命与更换策略 | 是否要现场长期运行、不可开盖 | MTD + UBI 的磨损策略可显式调优 |
1.5 本文主线与读法
本文的主线只有一条:从一颗裸器件出发,沿着软件链一路走到挂载点。读法建议按链走,不要按章节跳:
- 第 1~2 章建立模型:两条路径的边界、MTD 核心抽象(mtd_info / 分区 / 回调 / 坏块与 ECC 的位置)。
- 第 3~4 章进入驱动:NAND 控制器驱动的分层与接口、spi-mem 抽象与 SPI NOR / SPI NAND 驱动结构。
- 第 5~6 章落到配置:设备树里存储节点与分区子节点的写法、分区表设计的原则与 OTA 配合。
- 第 7~8 章架文件系统:UBI / UBIFS 分别替谁干活、只读与可写方案怎么选、挂载参数与掉电安全。
- 第 9~10 章管启动与验证:启动链路三处配置如何保持一致、调试接口、掉电测试与回归。
- 第 11~12 章收口:内核版本差异与移植、配置-验证闭环与要点汇总。
每一章的写法相同:先讲机制与判据,再给可直接对照的表格,最后用提示框把「最容易踩的坑」单独标出。所有表格都可以当核对底稿用——把具体型号、具体内核版本填进去,评审时就是证据。
2. MTD 核心抽象
这一章讲 MTD 子系统的「内功」:一块裸器件怎么注册进内核、怎么被切成分区、驱动通过什么回调被调用、ECC 与坏块的责任落在哪一层、用户态又能看到哪些接口。这一章的概念是后面所有配置的地基——设备树里的每个属性,最终都落到本章的某个结构体字段或某个回调上。
2.1 器件注册与核心结构
一颗裸器件进入 MTD 世界要过三关:总线探测(控制器驱动在总线上发现器件)、结构填充(驱动填好描述器件几何与能力的结构体)、注册(调注册接口把器件挂进 MTD 核心层,此后分区、字符设备、sysfs 才陆续出现)。这三步的顺序不能颠倒——分区出现在注册之后,用户态工具能查到 mtdX 在分区创建之后。
核心结构只需要认识五个,全部是「名字级」的了解:mtd_info 是每块 MTD 器件的身份证,记录容量、擦除块大小、页大小、OOB 大小与操作回调表;mtd_partition 描述一个分区,记录偏移、大小与名字;nand_chip 是 raw NAND 器件的私有状态,挂着控制器钩子、坏块表与 ECC 配置;spi_device / spi_mem 是 SPI 总线侧的器件与「一条总线操作」的描述结构;ubi_volume 是 UBI 层的卷。第 3、4 章会展开 nand_chip 与 spi_mem,本章只建立地图。
| 结构 / 概念 | 回答的问题 | 关键字段(名字级) |
|---|---|---|
| mtd_info | 这块裸器件是什么、能做什么 | size / erasesize / writesize / oobsize / 操作回调表 |
| mtd_partition | 一个分区切在哪里、叫什么 | offset / size / name / mask_flags |
| nand_chip | NAND 器件的私有状态 | 控制器挂接点、坏块表、ECC 配置入口 |
| spi_device / spi_mem | SPI 总线上的器件与操作 | 片选、速率、线宽、op 描述结构 |
| ubi_volume | UBI 层的一个卷 | vol_id / vol_type / usable_leb_size |
| nand_controller | 一颗控制器管几颗片 | 挂接多颗 nand_chip、锁与调度 |
2.2 分区机制
分区的本质是在一个 mtd 器件的地址空间上划出若干段,每段生成一个新的 mtdX。对上层来说,每个分区就是一块独立的 MTD 器件:有自己的容量、自己的文件系统、自己的挂载点;对驱动来说,分区只是偏移与长度的记录,坏块表与 ECC 配置仍然归整个物理器件所有。
分区从哪来?主线内核的默认来源是设备树里的 partitions 子节点(解析器名为 ofpart),此外还有命令行 mtdparts、镜像里残留的 RedBoot 表等。选择来源的原则只有一条:分区表要能被 BootLoader 与内核同时、一致地解读——否则内核看到一个分区布局,U-Boot 看到另一个,烧写与启动就会错位(第 6.5 节与第 9 章展开)。
| 分区来源 | 配置位置 | 特点 | 适用场景 |
|---|---|---|---|
| ofpart | 设备树 partitions 子节点 | 静态、随 DTB 一起发布,主线默认 | 正式产品 |
| cmdlinepart | 内核 cmdline 的 mtdparts | 不改 DTB 即可调整,便于试产调参 | 调试 / 试产 |
| RedBoot 表等 | Flash 上的残留分区表格式 | 兼容旧镜像,仅迁移期使用 | 旧平台迁移 |
| 用户态创建 | ioctl / 工具动态创建 | 运行期生效,重启即失效 | 特殊场景 |
2.3 操作回调与并发
MTD 核心层与驱动之间靠一张回调表对话:读(read)、写(write)、擦除(erase)三大件,加上 OOB 读写、坏块查询、加锁等辅助入口。每条回调都有 MTD 核心层定好的语义:读要返回实际读出量与位错误信息;写以页为单位、页内顺序;擦除以块为单位、耗时最长且期间器件不可访问。驱动实现这些回调时,负责把语义翻译成具体的总线命令序列。
并发的规则比 API 表面看起来严格。同一颗 MTD 器件上的并发访问,由驱动层的锁串行化——两个进程同时 nandwrite 同一分区,落到器件上也排队执行;擦除进行中器件不可访问,此时对该分区的读写会等待。真正要小心的不是「并发」,而是「掉电打断」:擦除擦到一半掉电、写写了一半掉电,这块的状态由器件的物理性质决定,恢复策略属于文件系统与 UBI 的职责(第 7、8 章)。
| 回调 | 语义 | 单位 | 实现要点 |
|---|---|---|---|
| read | 读一页到缓冲区,返回实际读出量 | 页 | 位错误按阈值上报,可纠正的静默处理 |
| write | 写一页,只能把位从 1 改 0 | 页 | 页内顺序写,不支持回头改 |
| erase | 擦除一个块,耗时最长 | 擦除块 | 期间器件不可访问,状态需等待就绪 |
| OOB 读写 | 读写页外区域 | 页 | 坏块标记与 ECC 校验位的家 |
| 坏块查询 | 报告某块是否坏块 | 擦除块 | 读坏块表,不直接访问器件 |
| sync / lock | 刷新与加锁 | 器件 | 掉电前的统一刷新点 |
用户态拿到 /dev/mtdX 之后,ioctl 与读写接口仍然受同一套锁与语义保护;但绕过 MTD、直接用 SPI 用户态接口怼器件的做法,会把坏块检查、ECC、锁全部废掉——写进去的数据当时是对的,下一次读就可能因为坏块或位错误而损坏。量产烧录也要走 mtd-utils 或内核接口,不要自建总线直写。
2.4 ECC 与坏块在 MTD 中的位置
ECC 与坏块是 MTD 路径的两个「必修课」,它们的责任划分必须心里有数:ECC 的正确性与强度由驱动配置(用片上 ECC、控制器 ECC 引擎还是软件 ECC),坏块的标记与跳过由驱动维护的坏块表(BBT)负责,而「坏块之后怎么继续提供服务」是 UBI 与文件系统的事。三者串起来才是完整的答案:驱动保证「这一页读对了、这块跳过了」,UBI 保证「跳过坏块之后容量与磨损仍然均衡」,文件系统保证「掉电时正在写的元数据能恢复」。
配置层面的判断顺序:先看器件是否带片上 ECC(SPI NAND 多数带),能用片上就用片上;不能用再看 SoC 控制器的 ECC 引擎支持什么强度与步长;最后才考虑软件 ECC(只适用于小容量、低速场景)。强度与步长不是越大越好——步长决定每页要几次校验,强度决定纠错能力,两者都要与页大小、OOB 大小匹配,取值以器件与 SoC 手册的推荐组合为准。
| 职责 | 谁负责 | 配置入口 | 配错的典型表现 |
|---|---|---|---|
| ECC 计算与校验 | 驱动 / 硬件引擎 | ECC 模式、强度、步长的选择 | 位错误累积成读失败,日志刷 UBE |
| 坏块标记与查询 | 驱动(坏块表 BBT) | OOB 标记或片上坏块表 | 坏块被当成数据区,读写出错 |
| 坏块跳过策略 | MTD 核心层 / 驱动 | 坏块是否允许被分区使用 | 分区在坏块上建立,挂载即失败 |
| 坏块重映射与磨损 | UBI(若启用) | 卷表与 PEB 分配策略 | 某几块被写穿,寿命远低于预期 |
| 掉电一致性 | 文件系统 / UBI | 日志模式、原子提交设计 | 掉电后文件系统需要全盘重建 |
2.5 字符设备与 sysfs 接口
注册成功之后,用户态能看到三样东西:/proc/mtd(分区总览,启动早期就能读,是排查「分区怎么切的」第一现场)、sysfs 里的 mtd 类属性(器件的 size / erasesize / writesize、位错误纠正与坏块的统计阈值等)、/dev/mtdX 与 /dev/mtdXro(字符设备,配合 ioctl 完成查询、擦除、读写 OOB;只读节点 ro 是刻意保留的安全阀,防止手滑)。此外还有一个 /dev/mtdblockX 块设备视图——第 1.3 节说过,它是简陋的可写块设备仿真,只适合只读挂载,不要当普通块设备用。
调试与量产绕不开 mtd-utils 这一套工具:mtdinfo 看器件与分区几何;flash_erase 擦除;nandwrite 烧写并处理 OOB 与坏块跳过;nanddump 连 OOB 一起导出;UBI 侧还有 ubiattach / ubiformat / ubinfo。把「dmesg 里的 mtd 注册日志 + mtdinfo -a 的输出」当成裸器件调试的起点,90% 的「分区不对、容量不对」在这两处就能定位(第 10 章展开)。
拿到一块新板,裸器件侧先跑三行:dmesg | grep -i mtd 看器件是否注册、几何是否合理;cat /proc/mtd 看分区是否按设备树切出来;mtdinfo -a 看每个分区的擦除块、页与 OOB 细节。三行输出对不上设备树,问题就在设备树或绑定匹配上,不用往下走。
本章小结:MTD 核心层用 mtd_info 描述器件、用分区切分地址、用回调表与驱动对话;ECC 与坏块的责任在驱动,磨损与重映射的责任在 UBI,掉电一致性的责任在文件系统。下一章进入驱动的第一层——NAND 控制器驱动怎么把「读一页」这条语义变成总线上的动作。
3. NAND 控制器驱动
这一章进入驱动的第一层:一颗 NAND 接进 SoC,控制器驱动到底要干哪些活、raw NAND 核心层与控制器层之间的接口长什么样、ECC 引擎与时序怎么配、器件怎么被认出来。主线是:核心层定策略,控制器驱动定执行——移植一颗新 NAND,大部分工作发生在下层两格,上三层一般不用动。
图 4 把「从 probe 到 /dev/mtdX」的注册链画成了一条流水线,下半部分是 MTD 核心层最终看到的景象:一个完整的 Flash 地址空间,按擦除块对齐切成若干分区,每段生成一个 mtdX。这条链有三个工程含义:其一,注册失败(probe 阶段就报错)时用户态什么都看不到,排查只能看 dmesg;其二,分区切得对不对,在 /proc/mtd 里一眼能验证;其三,坏块表与 ECC 配置属于整个物理器件,不随分区划分而变化。
| 注册阶段 | 触发条件 | 产物 | 失败时的现象 |
|---|---|---|---|
| 控制器驱动 probe | 设备树 compatible 匹配到控制器 | nand_controller 就绪 | dmesg 无控制器日志,器件完全不出现 |
| 器件识别 | 片选上有器件响应 | 容量、页大小、厂商信息 | 识别失败,probe 直接退出 |
| mtd_info 注册 | 几何与回调填充完成 | mtdX 出现在 /proc/mtd | 内核 panic 或注册日志缺失 |
| 分区解析 | 设备树 partitions 节点存在 | 每个分区成为独立 mtdX | 只有一个整片 mtd0,无子分区 |
| 设备节点 | 注册成功 | /dev/mtdX 与 /dev/mtdblockX | 用户态工具报找不到设备 |
3.1 控制器驱动的职责划分
一颗 SoC 上的 NAND 控制器,驱动要同时伺候三样东西:总线(怎么把命令、地址、数据送到器件)、引擎(ECC 与 DMA 这两个硬件块怎么用)、时序(跑多快器件才受得了)。这三样在代码里分成两个层次:raw NAND 核心层(所有 NAND 通用)负责命令序列的编排、坏块表与器件模型;控制器驱动(SoC 厂商提供)负责把每条指令翻译成对控制器寄存器的操作、配置 ECC 引擎、发起 DMA。
划分的好处是可移植性:换一颗 NAND 器件,改的只是核心层的器件参数;换一个 SoC,改的只是控制器驱动。坏味道是反过来写的驱动——把命令编排也塞进控制器驱动,换器件就要动驱动。判断一份驱动是否「分层干净」,看它是否实现了核心层定义的执行入口(exec_op 或旧式的 cmdfunc),而不是自己另起炉灶。
| 职责 | 归属层 | 具体内容 | 配置入口 |
|---|---|---|---|
| 命令序列编排 | raw NAND 核心层 | 读页 / 写页 / 擦块的指令顺序 | 器件参数(页大小、时序档) |
| 指令执行 | 控制器驱动 | 命令 / 地址 / 数据的锁存与搬运 | exec_op 或 cmdfunc 实现 |
| 片选与就绪 | 控制器驱动 | 选择器件、等待就绪引脚 | 设备树片选号与就绪属性 |
| ECC 计算校验 | 控制器驱动 + 硬件引擎 | 纠错码计算、校验、错误上报 | ECC 模式、强度、步长 |
| 坏块表 | raw NAND 核心层 | 扫描、缓存、查询、标记 | 坏块表存放位置的选择 |
| DMA | 控制器驱动 | 大数据量搬运,卸载 CPU | 缓冲区对齐与长度约束 |
| 时序协商 | 控制器驱动 | 速率档位与参数下发 | ONFI 时序模式或等效配置 |
图 5 给出分层的全貌与三条横向配置线。ECC 引擎的选择顺序是:器件带片上 ECC 优先用片上;否则用 SoC 控制器的硬件引擎;软件 ECC 只留给小容量低速场景。时序参数经核心层统一下发,速率档位是协商结果而不是单方面设定。坏块管理里最值得调的一个开关是「坏块表是否存放在器件上」(nand-on-flash-bbt)——打开后上电不必全片扫描坏块,启动更快,代价是表本身也要维护。
拿到一份厂商 NAND 驱动,先问三个问题:它实现了 exec_op 还是老式 cmdfunc?ECC 是走硬件引擎还是软件循环?坏块表放内存还是放器件?三个答案决定了移植工作量:exec_op + 硬件 ECC + 片上坏块表的驱动最接近主线结构,往新内核移植最省事;三者都是老式实现的,基本按重写估算。
3.2 raw NAND 层与控制器层的接口
两层之间的接口经历过一次大改:老式接口是一堆「一条命令一个函数」的 cmdfunc——发命令、发地址、读数据各一个入口,灵活但难以描述一次操作里的时序细节;新式接口是 exec_op:把一次完整操作(命令 + 地址 + 等待 + 数据 + 方向)打包成一个指令序列,一次性交给控制器执行。新接口让控制器有机会做优化(合并等待、流水 DMA),也让「哪些线数 / 速率组合支持」变得可声明。
对配置的影响很直接:用 exec_op 的驱动,设备树里声明线宽、速率时,控制器能回答「支持 / 不支持」;用 cmdfunc 的老驱动,这些声明常常被静默忽略,跑出问题才发现根本没按四线在跑。移植时如果设备树声明了四线但实测是单线速度,第一个要怀疑的就是驱动还在用老接口。
| 对比项 | 老式 cmdfunc 接口 | exec_op 接口 |
|---|---|---|
| 操作描述 | 拆成多个函数调用,逐步下发 | 一条指令序列一次提交 |
| 等待处理 | 驱动自己轮询或中断 | 指令内声明等待周期与就绪检测 |
| 线宽 / 速率 | 难以声明,常被忽略 | 可通过 supports_op 声明 |
| DMA 协同 | 逐个函数手工触发 | 序列级优化,整段搬运 |
| 可移植性 | 控制器驱动各自为政 | 核心层统一,驱动只实现执行 |
| 主线状态 | 逐步废弃,仅存量驱动 | 当前推荐结构 |
3.3 ECC 引擎的配置
ECC 配置要同时定三件事:用哪个引擎(片上 / 控制器 / 软件)、多强的纠错(强度)、多大的校验单元(步长)。步长与页大小、OOB 大小强相关:页越大、步长越大,校验次数越少,但单次校验覆盖的数据越多、出错后损失越大;强度决定能纠几个位,选小了位错误累积成不可纠,选大了 OOB 放不下、延迟还高。取值以器件与 SoC 手册的推荐组合为准,不建议自创组合。
一个常被忽略的细节是 OOB 布局的分配权:坏块标记、ECC 校验位、文件系统元数据都住在 OOB 里,布局由驱动或引擎配置决定,用户态工具(nandwrite 的 OOB 选项)只是按同一布局读写。换了 ECC 配置再读旧镜像,往往不是数据坏了,而是校验位位置对不上——量产烧录镜像与驱动 ECC 配置必须成对管理。
| ECC 模式 | 适用场景 | 优点 | 代价与限制 |
|---|---|---|---|
| 片上 ECC | SPI NAND 与部分并行 NAND | 无需控制器引擎,配置简单 | 强度固定,按器件档位选 |
| 控制器硬件引擎 | 多数 SoC 的并行 NAND | 强度步长可配,CPU 占用低 | 受引擎能力上限约束 |
| 软件 ECC | 小容量、低速、无引擎场景 | 不依赖硬件 | CPU 占用高,强度有限 |
| 无 ECC | 仅 SLC 小容量调试场景 | 最简 | 位错误不可纠,不可量产 |
量产最常见的返工之一是「镜像烧进去能启动,升级后再读数据全错」——根因是两次烧录用了不同的 ECC 配置,校验位在 OOB 里的位置变了。把 ECC 配置(引擎、强度、步长、OOB 布局)与镜像版本一起归档,烧录脚本从同一份配置生成,不允许现场手改。
3.4 时序参数的下发
并行 NAND 的速率协商走 ONFI 或 Toggle 一类的标准机制:器件在上电时报告自己支持哪些时序档位,控制器按档位把一组时序参数(建立时间、保持时间、访问周期一类)写进自己的寄存器。档位越高越快,但对信号完整性的要求也越严——PCB 布线差的板子,高档位表现为偶发读错误,且往往查不出原理问题。拿不准时先降一档验证,把「时序问题」与「器件问题」分开。
SPI 侧的「时序」概念不同:没有 ONFI 档位,速率是控制器与器件按 spi-max-frequency 一类属性协商出的一个上限,真正限制常常来自信号质量而不是协议。设备树里写了高速率、器件手册支持、控制器也支持,实测仍掉码,优先查布线(第 4 章)。
| 机制 | 谁报告能力 | 谁下发参数 | 配置位置 | 常见问题 |
|---|---|---|---|---|
| ONFI 时序档 | 器件参数页 | 控制器 setup_data_interface | 速率档位选择 | 档位过高偶发读错 |
| Toggle DDR | 器件 READ ID 后的特性 | 控制器等效配置 | 控制器驱动选项 | 布线不满足时误码 |
| SPI 速率 | 器件手册上限 | spi-mem 协商 | 设备树速率属性 | 声明速率超器件能力 |
| 就绪检测方式 | 器件 / 控制器能力 | 控制器驱动 | 就绪引脚或轮询 | 轮询方式拖慢擦除等待 |
3.5 芯片探测与识别
器件识别的流程各总线大同小异:上电后控制器向片选发读 ID 命令,拿回厂商 ID 与器件 ID,核心层拿这组 ID 去查内置的器件表,命中就填好容量、页大小、OOB 布局等参数。并行 NAND 还可以进一步读 ONFI 参数页,把容量、时序档、ECC 要求全部从器件侧拿到;SPI NAND 与 SPI NOR 各有自己的参数表(SFDP 或厂商参数页)作为补充。表里没有的型号不是不能用——多数驱动支持按「容量 + 页大小 + OOB」手工声明一个兼容项,但新器件优先推动厂商把 ID 补进器件表,手工声明只作试产应急。
| 探测步骤 | 动作 | 拿到的信息 | 失败的处理 |
|---|---|---|---|
| 片选使能 | 选中目标器件 | 器件在线与否 | 重试与超时,最终 probe 失败 |
| 读 ID | 发读 ID 命令 | 厂商 ID、器件 ID | 换片选 / 查硬件连线 |
| 查器件表 | ID 匹配内置表 | 容量、页大小、OOB | 手工声明兼容项 |
| 读参数页 | ONFI / SFDP / 厂商页 | 时序档、ECC 要求、特性位 | 回退到器件表默认值 |
| 坏块扫描 | 建坏块表(可选放器件) | 坏块清单 | 扫描失败按保守策略处理 |
识别失败的排查顺序固定为:供电与时钟 → 总线连接(片选、极性)→ 读 ID 命令本身(线宽、速率)→ 器件表匹配。前两项是硬件问题,占实际案例的大多数;后两项才是软件。拿万用表量过供电、示波器看过片选波形再怀疑驱动,能省下大量时间。
本章小结:NAND 控制器驱动的分层是「核心层定策略、控制器定执行」;exec_op 是新式接口的标志;ECC 三要素(引擎、强度、步长)与 OOB 布局必须和镜像成对管理;时序是协商不是单方面声明;识别失败先查硬件再查软件。下一章把这套思路搬到 SPI 总线,看 spi-mem 抽象怎么统一 SPI NOR 与 SPI NAND 的驱动结构。
4. SPI MEM 框架与 SPI 存储驱动
SPI 总线上的存储(SPI NOR / SPI NAND)走的是与并行 NAND 不同的框架:spi-mem 抽象层把「一条总线操作」结构体化,存储驱动(spi-nor / spi-nand)在它之上实现 MTD 接口。这一章的核心是理解 spi-mem 的抽象边界:它管总线操作,不管存储语义;存储语义归 spi-nor / spi-nand,最终仍汇入 MTD。
图 6 左右对照:左边是一次操作的四个成分——opcode(命令码)、addr(地址与线宽)、dummy(等待周期)、data(方向 / 线数 / 长度),四线、八线协议就是这四个成分的线数组合;右边是 spi-mem 之上的驱动栈,最下面一格是 SPI 控制器驱动,它通过 supports_op 声明能力、通过 exec_op 执行操作。理解这张图,就理解了「为什么 SPI 存储驱动可以不管控制器差异」——差异全被 supports_op 挡住了。
| spi-mem 概念 | 回答的问题 | 说明 |
|---|---|---|
| spi_mem_op | 一次操作怎么描述 | 命令、地址、等待、数据四成分的集合 |
| spi_mem_adapter | 控制器能做什么 | supports_op 声明支持的操作组合 |
| exec_op | 怎么执行 | 同步提交一条操作,等待完成 |
| dirmap | 怎么加速大读 | 内存映射读写通道,XIP 的基础 |
| adjust_op | 怎么适配硬件 | 控制器对操作的微调钩子 |
4.1 spi-mem 抽象层
spi-mem 存在的理由是SPI 存储操作越来越复杂,老接口描述不了:一条读操作可能是「单线发命令、单线发地址、四线读数据、中间插若干等待周期」,这在老式「消息 + 传输」模型里要拼多个传输段,控制器还无法判断整条操作是否支持。spi-mem 把整条操作变成一个结构体,控制器拿到完整信息才能回答「支持 / 不支持」,也才能做真正的优化。
对驱动开发者的实际影响:写 SPI 存储驱动时不再直接碰 SPI 消息,而是构造 spi_mem_op 调 exec_op;写控制器驱动时不再只实现「传一个字节」,而是实现「执行一条操作」并声明能力。这个抽象也带来了一个调试福利——启动日志里能直接看到协商后的操作参数(线数、速率),排查「为什么没跑上四线」时比老框架容易得多。
4.2 SPI NOR 驱动结构
spi-nor 驱动的结构可以概括为「一个核心 + 一张参数表 + 若干后端的组合」:核心(spi_nor)负责把 MTD 的读 / 写 / erase 翻译成 spi_mem_op;参数表按器件 ID 与 SFDP 描述容量、页大小、擦除块大小、四线使能方式;后端处理厂商特有的使能序列(四线使能、八线使能、软件保护)。SPI NOR 没有坏块、没有片上 ECC,它在 MTD 世界里是最「干净」的器件——配置项少,主要工作量在设备树与分区,而不是 ECC。
两个值得记住的能力。其一是 XIP(就地执行):NOR 支持内存映射读,代码可以直接在 Flash 上运行,省掉把根文件系统搬进内存的开销——对启动时间敏感的小系统很有价值。其二是 SFDP(串行 Flash 可发现参数):新器件把自己的能力标准化报告,驱动不必为每个新型号加表,设备树里的 compatible 可以尽量写通用值,让 SFDP 说话。
| spi-nor 组成 | 职责 | 与设备树的关联 |
|---|---|---|
| 核心(spi_nor) | MTD 操作到 spi_mem_op 的翻译 | 决定 /dev/mtdX 的行为 |
| 器件参数表 | 容量、擦除块、页大小、使能序列 | compatible 匹配到具体型号 |
| SFDP 解析 | 从器件读标准化能力 | 减少对具体型号的依赖 |
| 厂商后端 | 四线 / 八线使能、软件保护 | 影响线宽协商结果 |
| dirmap / XIP | 内存映射读通道 | 影响启动性能与代码就地执行 |
4.3 SPI NAND 驱动结构
spi-nand 的结构与 spi-nor 平行,但多两样东西:ECC 与坏块。spi-nand 核心把 MTD 的读 / 写 / erase 翻译成 SPI 命令序列,ECC 默认用片上 ECC(多数 SPI NAND 内置),强度按器件档位选择;坏块表沿用 raw NAND 核心层那一套。厂商差异比 NOR 大——不同厂家的使能、状态寄存器、缓存操作各有花样,核心层用一张「厂商操作表」把这些差异挂接进来,驱动移植时主要工作就是填这张表。
与 SPI NOR 的选择判断很简单:要容量、要可写、能接受坏块管理 → SPI NAND;要 XIP、要简单、容量需求不大 → SPI NOR。两者的设备树结构几乎一样(都是 SPI 总线上的子节点),差别在 compatible 与 ECC / 坏块相关属性——这也是为什么第 5 章讲设备树时用一套结构覆盖两者。
| 对比项 | SPI NOR | SPI NAND |
|---|---|---|
| MTD 语义 | 读 / 写 / 擦除,无坏块 | 读 / 写 / 擦除,有坏块 |
| ECC | 不需要 | 默认片上 ECC,强度按档位 |
| 访问单位 | 页 + 大擦除块 | 页 + 擦除块,与并行 NAND 同构 |
| XIP | 支持(内存映射) | 通常不支持 |
| 设备树差异 | compatible 与线宽属性 | 增加 ECC 与坏块相关属性 |
| 典型用途 | 启动代码、小根文件系统 | 大容量数据、可写文件系统 |
4.4 四线与八线模式协商
线宽不是「写了属性就生效」的单方面声明,而是器件能力、设备树请求、控制器能力三者的交集。设备树用 spi-rx-bus-width / spi-tx-bus-width 声明收 / 发方向各用几根线;控制器通过 supports_op 声明能执行哪些线数组合;器件通过 SFDP 或参数页声明自己支持哪些模式。三者取交集,驱动按结果拼装操作——请求超出能力时不报错,而是静默降级,只在启动日志里留下记录。这个「静默降级」是排查线宽问题必须知道的第一个知识点。
八线(Octal)是更激进的模式:命令、地址、数据都可能走八线,还需要器件特定的使能序列与配置段(常涉及非易失配置寄存器的修改,改错会让器件「消失」)。八线的使能顺序必须严格按器件手册执行,且一旦在非易失配置里写错,恢复手段有限——试产阶段先在样板上验证完整流程,再往量产配置里固化。
图 7 把协商画成了一次「求交」:三张卡片分别是器件能力、设备树请求与控制器能力,汇入模式协商后输出一档具体模式。常用档位从单线兜底到八线,中间是四线的两个变体(地址单线 / 地址四线)。选档的原则是「够用就好」:线数越多,对信号完整性的要求越高,PCB 上并行走线越长越容易出问题;容量不大、速率要求不高的场景,四线通常是性价比最优解。
现象是「设备树写了四线,速度却是单线」。按顺序问三个问题:一、启动日志里协商后的线宽是几?(日志会记录实际使用的操作参数)二、控制器驱动实现了 supports_op 吗、支持四线组合吗?(老驱动常常不支持)三、器件的四线使能序列执行了吗?(有些器件要发一条使能命令才进四线,漏了就一直单线)。三个问题定位不到再怀疑硬件。
4.5 与旧式框架的差异
很多在产项目跑的是几年前的厂商内核,SPI 存储那一层还是旧式结构:一个 m25p80 驱动包打天下、直接拼 SPI 消息、四线靠全局开关。往新内核迁移时,设备树侧最大的变化是 compatible 的取值与线宽属性的改名,驱动侧则是把「拼消息」改成「构造 spi_mem_op」。迁移前先确认目标内核的绑定文档版本——同一个属性名在不同内核里含义可能不同(第 11 章展开)。
| 维度 | 旧式框架 | spi-mem 框架 |
|---|---|---|
| 驱动组织 | m25p80 一类大驱动 | spi-nor / spi-nand 分离 |
| 操作描述 | 拼 SPI 消息与传输 | spi_mem_op 结构体 |
| 线宽声明 | 全局开关或驱动内写死 | 设备树属性 + 能力协商 |
| 控制器适配 | 每驱动各自实现 | adapter 统一接口 |
| 设备树兼容值 | 厂商型号字符串 | 通用 compatible + SFDP |
| 大读优化 | 无统一通道 | dirmap 内存映射 |
从旧内核迁到新内核,先做一次「绑定文档对表」:把旧设备树里每个属性在新绑定文档里查一遍。属性还在、含义没变 → 只改驱动;属性被改名或废弃 → 先改设备树再验驱动;兼容值需要换 → 改设备树 compatible。顺序反了会同时面对两个变量,问题无法定位。
本章小结:spi-mem 把一条总线操作结构体化,控制器用 supports_op 声明能力、exec_op 执行操作;spi-nor 干净(无 ECC 无坏块、支持 XIP),spi-nand 与并行 NAND 同构(有 ECC 有坏块);线宽是三方协商的结果,不生效时按「日志 → 控制器能力 → 使能序列」顺序排查。下一章回到设备树,把这些驱动参数一个个对应到树上的属性。