Linux 系统(SPI NAND)驱动开发指南
编制日期 2026-10-05
面向 XTX XT26G01D / XT26G11D 等串行 SLC NAND 器件,覆盖 开发步骤 → 内核配置 → 驱动架构 → 设备树与分区 → 新器件适配 → 测试 → 调试 → 性能优化 → 可靠性 → 量产运维 的完整链路;每一章都给出可直接复制的 config 片段 / DTS 片段 / 命令行 / 判据, 并明确区分「测试(验合格)」与「调试(找 bug)」两条线。
| 文档编号 | LINUX-SPINAND-DRV-001 |
|---|---|
| 版本 / 状态 | V1.0 · 正式发布 |
| 适用器件 | XTX XT26G01D / XT26G11D / XT26Q01D / XT26G02D / XT26G04D 等同系列 1Gb~4Gb SPI NAND(On-Die ECC 8bit/512B);文中方法同样适用于 Winbond / GD / MXIC / Micron / Toshiba 等品牌器件 |
| 内核版本 | 主线 Linux 5.10 / 5.15 / 6.1 / 6.6 为主(drivers/mtd/nand/spi/ 原生 spinand 框架);4.19~5.4 的差异见 0.3 节 |
| 硬件平台 | ARM/ARM64 SoC(SPI 控制器走 spi-mem 框架)+ 板载 SPI NAND;示例以 3.3V 1Gb 器件、104MHz Quad 模式为基准 |
| 配套文档 | 《SPI_NAND_Flash_电路设计指南.html》(硬件 / 原理图 / 布线)、《SPI_NAND_Flash_软件设计规范.html》(命令 / 寄存器 / 坏块 / ECC 协议层) |
0.1 编写目的与读者对象
SPI NAND 在 Linux 下几乎不需要从零写驱动——内核主线已有完整的
spinand 框架与厂商器件表。真正的工作量集中在:选对内核配置、写对设备树、适配新器件、
把 UBI/UBIFS 跑通、以及量产阶段的可靠性与运维。本文把这些工作按可执行的顺序固化下来。
- 驱动工程师:按第 3~7 章完成配置、DTS、器件适配与驱动改造;按第 10 章定位问题。
- 硬件工程师:重点读第 2 章(原理图 / 电源 / 测试点 / BOM 约束)与第 5 章(reset-gpio、pinctrl、供电绑定)。
- 测试工程师:按第 9 章执行功能 / 压力 / 性能 / 回归四项测试,按第 17 章核查清单验收。
- 量产与运维:按第 6 章做产线烧录,按第 15 章做日志采集、坏块监控与 A/B 分区 OTA。
① SPI NAND 不是 SPI NOR:不能 XIP、不能直接挂 ext4、必须走 UBI/UBIFS(或只读 SquashFS + 可写 UBIFS 叠加)。
② 分区表必须在 U-Boot、设备树、内核命令行三处保持一致——这是量产阶段最高频的故障源(见 5.3)。
③ On-Die ECC 由器件内部完成,主机只负责读状态位判级:正确可纠要上报 bitflip 计数,不可纠必须返回 -EBADMSG,不能静默返回数据(见 12.1)。
一句话结论:SPI NAND 在 Linux 里的「驱动开发」,本质是
把器件能力(ID / 页大小 / OOB 布局 / ECC 强度 / 命令变体)正确喂给 spinand 框架,
再把 MTD → UBI → UBIFS 这条链路打通;剩下的 80% 工作量在设备树、分区表、bootargs、
量产烧录与可靠性验证上,而不是在 drivers/mtd/nand/spi/ 里写新代码。
0.2 术语与缩略语
下表是本文(以及内核 MTD 邮件列表、datasheet)里高频出现的词。驱动调试时大量日志来自这些子系统, 先建立词与层级的对应关系,能省掉一半读日志的时间。
| 缩写 / 术语 | 中文 | 所在层级 | 含义与要点 |
|---|---|---|---|
| MTD | 存储技术设备 | 内核 subsystem | Linux 对裸 Flash 的统一抽象层,向上提供字符设备 /dev/mtdX 与只读块设备 /dev/mtdblockX,向下接 raw nand / spinand / nor。 |
| spi-mem | SPI 存储器抽象 | drivers/spi | 把 Flash 操作抽象成「命令-地址-dummy-数据」四段式 spi_mem_op,屏蔽单/双/四线差异。spinand 框架依赖它。 |
| spinand | SPI NAND 框架 | drivers/mtd/nand/spi | 内核原生 SPI NAND 驱动框架,模块名 spinand,配置开关 CONFIG_MTD_SPI_NAND。 |
| UBI | 无序块镜像层 | drivers/mtd/ubi | 在 MTD 之上做卷管理、坏块管理、磨损均衡。把 PEB(物理擦除块)映射成 LEB(逻辑擦除块)。 |
| UBIFS | UBI 文件系统 | fs/ubifs | 跑在 UBI 卷之上的日志型 Flash 文件系统,支持压缩、掉电安全。 |
| PEB / LEB | 物理 / 逻辑擦除块 | UBI | PEB 即一个 NAND block(如 128KB);LEB = PEB − EC/VID 头开销(如 126KB)。 |
| OOB / spare | 备用区 | NAND 物理层 | 每页附带的额外字节(如 2048B 页配 64B/128B),放 ECC 校验值与坏块标记。 |
| ECC | 纠错码 | 物理层 / 控制器 | 纠正位翻转。SPI NAND 通常是 On-Die ECC(片内完成),主机读 status 判级。 |
| bitflip | 位翻转 | 物理层 | 读回数据与写入值不一致的 bit 数。可纠范围内由 ECC 修正,驱动需向上报 max_bitflips。 |
| On-Die ECC | 片内 ECC | 器件内部 | ECC 计算与纠错在 Flash 芯片内部完成,主机不生成校验码,只读 ECC 状态位。 |
| BBT / BBM | 坏块表 / 坏块标记 | NAND core / UBI | 出厂坏块由厂商标记在 OOB;运行时新增坏块由 UBI 或驱动标记。 |
| FTL | 闪存转换层 | 中间层 | 把块设备语义翻译成 Flash 语义。UBI 不是完整 FTL(不做地址映射表持久化),这是它和 eMMC 的根本差别。 |
| DTS / DTB | 设备树源 / 二进制 | boot 阶段 | 描述硬件连接。编译产物 .dtb 由 bootloader 传给内核。 |
| probe | 驱动探测 | 驱动模型 | 设备与驱动 matching 成功后调用的初始化入口,SPI NAND 是 spinand_probe()。 |
| compatible | 兼容属性 | 设备树 | 驱动与设备匹配的键。SPI NAND 通用值是 spi-nand,某些厂商再加具体型号。 |
| pinctrl | 引脚复用控制 | 设备树 / 驱动 | 配置 SPI 引脚功能与电气特性,错误的 pinctrl 会表现为「SPI 读全 FF」。 |
| Fastmap | 快速映射 | UBI | 把 UBI 的 EC/VID 头扫描结果持久化到固定 PEB,大幅缩短 attach 时间(CONFIG_MTD_UBI_FASTMAP)。 |
| GC | 垃圾回收 | UBIFS | 回收含大量无效页的 LEB。GC 压力大时写性能骤降,是性能调优重点。 |
| Wear Leveling | 磨损均衡 | UBI | 把擦写分散到不同 PEB,避免局部过早失效。 |
| XIP | 片内执行 | — | SPI NAND 不支持(无线性地址映射),这是与 SPI NOR 的关键差异。 |
| mtdparts | 命令行分区 | 内核 cmdline | 用内核命令行指定分区表的老方式,与设备树 fixed-partitions 二选一。 |
| ubinize | UBI 镜像打包 | 主机工具 | 把若干 UBIFS 卷镜像打包成一个可直接烧进裸 Flash 的 UBI 镜像(含 EC/VID 头)。 |
① MTD 分区 ≠ UBI 卷:分区是 Flash 上的一段地址范围(/dev/mtdX),
卷是 UBI 在一个分区内部再划分出来的逻辑卷(ubi0:rootfs)。
② /dev/mtdX ≠ /dev/mtdblockX:前者是字符设备(正确用法),
后者是无坏块管理、无磨损均衡的「假块设备」,禁止在其上挂 ext4/vfat(见 8.1)。
③ On-Die ECC 状态位 ≠ 驱动自己算 ECC:片内 ECC 器件只需实现
ecc_get_status(),不要再用 nand_ecc_ctrl 那套软件 ECC 流程。
0.3 内核版本与 MTD 接口差异
MTD 子系统在 4.x → 5.x → 6.x 之间做过多次重构,照抄旧教程会直接编译不过或行为异常。 下表按版本给出关键差异,适配前先确认自己的内核基线。
| 内核版本 | 关键状态 | 对 SPI NAND 驱动的影响 |
|---|---|---|
| 4.19 ~ 5.4 | spinand 框架已合入(CONFIG_MTD_SPI_NAND),但器件表较薄 | 只有 gigadevice / macronix / micron / toshiba / winbond 几家,缺 XTX、ESMT、ATO、Alliance 等;OOB layout 与 ECC 判级接口与新版不同。老项目多为这一档,新器件需要自己 backport。 |
| 5.10 ~ 5.15 | 器件表大幅扩充,OOB layout 接口稳定 | struct mtd_ooblayout_ops 的 .ecc/.free 语义定型;SPINAND_OP_VARIANTS 宏成为标准写法。量产项目推荐基线。 |
| 6.1 ~ 6.6 | nand_ecc_engine 抽象引入,xtx.c 等新厂商合入 | ECC 由 struct nand_ecc_engine 统一管理,spinand_eccinfo 的部分字段被替换;新增 on-die ECC 的 NAND_ECC_ENGINE_TYPE_ON_DIE。从 5.x backport patch 时需改 ECC 相关字段。 |
| 6.7+ | spinand 支持 Octal DTR、Continuous Read,厂商表继续扩充 | 高性能器件的 8 线 / DTR 模式需要 spi-mem 控制器侧同步支持;老 SoC 控制器不支持时会自动降级到低性能变体。 |
① spinand_eccinfo 字段名与新版不同,直接套用 6.x 的 patch 会编译失败;
② 老内核的 spinand_read_page() 不检查 ECCS 返回值,读坏块会静默返回坏数据;
③ 老内核(4.19/5.4)默认未开 FASTMAP——该选项自 3.7 起即存在,但早期默认关闭且偏实验性,ubiattach 会全片扫描、attach 耗时长达数十秒,容易被误判为「卡死」。务必打开 CONFIG_MTD_UBI_FASTMAP 并设 ubi.fm_autoconvert=1。
结论:能升级就升级到 5.10+ / 6.1+;无法升级时,务必把 ECC 判级与 OOB layout 两处按目标内核源码核对后再改。
0.4 硬件平台与参考器件
本文以「ARM SoC + 板载 SPI NAND」为基准平台。驱动视角下,硬件只需要落实三件事: SPI 控制器接在哪个片选、器件供电域是多少、RESET#/WP# 是否可控。
| 项目 | 示例值 | 驱动侧对应配置 |
|---|---|---|
| SoC SPI 控制器 | spi0(支持 spi-mem、DMA) | 设备树 &spi0 { ... } 下挂 flash@0 |
| 片选 / reg | CS#0 | reg = <0>; —— 写错片选号是最常见的 probe 失败原因 |
| 器件 | XTX XT26G01D(1Gb, 3.3V) | compatible = "spi-nand",ID 由 xtx.c 匹配(MFR ID 0x0B) |
| 页 / 块 / OOB | 2048B / 128KB / 64B 或 128B | 由驱动从 ID 表读出,设备树不应硬编码几何参数 |
| ECC | 片内 8bit / 512B | xt26xxxd_ecc_get_status() 判级,CPU 不参与 ECC 计算 |
| RESET# / WP# | 接 GPIO(可选) | reset-gpios;WP# 若接 GPIO 可配合块保护寄存器使用 |
| SPI 模式 | Mode 0 / Mode 3(以 datasheet 为准) | spi-cpol / spi-cpha,一般默认 Mode 0 即可 |
| 最高频率 | 104MHz(Quad) | spi-max-frequency = <104000000>;,超频会表现为偶发 ECC 错误 |
1 · 前置知识依赖
动手改驱动之前,先确认四类前置知识齐备:内核驱动模型、Flash 物理原理、交叉编译环境、调试环境。 这四项任何一项缺失,后面每调试一个问题都会多花一倍时间。
1.1 Linux 内核驱动基础
SPI NAND 驱动落在「SPI 总线 + MTD 子系统」的交叉点上,需要理解的概念可以收敛到下面几条:
| 概念 | 在 SPI NAND 场景下的具体表现 |
|---|---|
| 设备树(DTS) | 描述「SPI 控制器上挂了一个 spi-nand,片选 0,最高 104MHz」。内核启动时把节点转成 spi_device。 |
| SPI 总线模型 | spi_master / spi_controller(控制器侧驱动)与 spi_device(器件侧)通过 compatible 匹配到 spinand 驱动。 |
| spi-mem 层 | 把「读页 = 13h 命令 + 24bit 地址 + 读 cache = 03h/0Bh/6Bh/EBh」这类序列抽象成 spi_mem_op,控制器侧只要会执行 op 即可。 |
| platform 驱动模型 | probe() / remove() 生命周期、-EPROBE_DEFER(pinctrl/供电未就绪时延迟探测)、devm 资源管理。 |
| 字符设备 / MTD | MTD 向用户态暴露 /dev/mtdX(字符设备)与 /dev/mtdblockX(只读兼容块设备),并注册 sysfs 属性。 |
| 并发与锁 | spinand 框架内部有互斥;用户态对同一 MTD 分区并发擦写仍需要上层串行化,不要并发 flash_erase。 |
1.2 Flash 基础原理
SPI NAND 保留了 NAND 的全部物理约束,只是把并行总线换成了 SPI。下面这些概念在后面每一章都会反复出现:
| 概念 | 典型值(1Gb SLC) | 驱动 / 文件系统侧的含义 |
|---|---|---|
| Page(页) | 2048 B(+64/128 B OOB) | 最小读写单位。写必须整页(或按 ECC 扇区)对齐编程,不支持字节改写。 |
| Block(块) | 128 KB(64 页) | 最小擦除单位。改一个字节也要整块擦除,这是所有 Flash 文件系统的设计起点。 |
| Plane / LUN / Die | 1~2 plane | 影响多平面并发操作与 cache 读命令选择。 |
| 坏块(Bad Block) | 出厂 ≤ 2%,运行时新增 | 出厂坏块标记在 OOB 第 0 字节(非 0xFF);UBI 负责替换与屏蔽。 |
| 位翻转(bitflip) | 随 P/E 次数与读干扰增加 | ECC 可纠则返回修正后的数据并上报 bitflip 数;不可纠必须报 -EBADMSG。 |
| ECC | 片内 8 bit / 512 B | 每 512 B 一个 ECC 扇区,一页 4 个扇区。ECC 校验值写在 OOB 里,由器件内部使用。 |
| P/E Cycle | SLC 约 5 万~10 万次 | 决定寿命。UBI 的磨损均衡目标就是让各 PEB 的 EC 值尽量接近。 |
| Wear Leveling | UBI 静态 + 动态 | 把冷数据搬走,避免静态数据所在的块从不参与均衡。 |
| Read Disturb | 同一块读次数过多 | 反复读同一块会影响邻近页,需要在驱动或上层做读计数与刷新。 |
| Program / Erase 时间 | tPROG ~ 数百 µs ~ 数 ms;tBERS ~ 数 ms | 不能用固定延时,必须轮询 OIP 状态位,超时门限取 datasheet max。 |
① 不能 XIP:没有线性地址映射,代码必须在 RAM 里执行,所以 bootloader 要先把 SPL/U-Boot 读进 RAM。
② 有坏块、有 OOB、有 P/E 寿命:必须引入 UBI 做坏块管理与磨损均衡,不能像 NOR 一样直接挂 squashfs 了事(除非只读分区)。
③ 写前必须擦:NOR 可以字节改写(写 1→0),NAND 不行。造成 UBIFS 是日志型文件系统,写放大与 GC 成为性能主矛盾。
1.3 交叉编译与内核构建环境
下图给出一条标准的「源码 → 镜像 → 部署」流水线。关键是四条产物各自独立: 内核镜像、设备树 blob、内核模块、rootfs 镜像,任何一条没更新都会表现为「改了没生效」。
最小可用构建命令
# 1) 内核(以 arm64 为例;ARCH/CROSS_COMPILE 按平台替换)
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
make <soc>_defconfig
make menuconfig # 按第 3 章勾选 MTD / SPI_NAND / UBI / UBIFS
make -j$(nproc) Image dtbs modules
make modules_install INSTALL_MOD_PATH=./_install
# 2) 设备树单独编译(改 DTS 后只需重编 dtb,秒级)
make dtbs
# 或单文件反编译排查:dtc -I dtb -O dts board.dtb -o board.dts
# 3) 检查设备树语法
dtc -I dts -O dtb -o /dev/null arch/arm64/boot/dts/<vendor>/board.dts
① 只 make Image 没 make dtbs;
② dtb 没拷到 TFTP 目录 / 没烧进 Flash;
③ U-Boot 里 fdt_addr_r 指向的还是旧 dtb,或者用了 bootm 但没传 dtb 参数;
④ 改的是 dtsi 但板级 dts 又覆盖了一次同名节点(/delete-node/ 或重复定义)。
判据:启动后 cat /proc/device-tree/... 或 ls /sys/firmware/devicetree/base/ 复核实际生效的树。
1.4 开发环境:SDK 构建与 TFTP / NFS 调试环境
SPI NAND 项目通常是整机构建(Bootloader + Kernel + Rootfs),推荐先用 SDK(Buildroot / Yocto) 统一产出,再用 TFTP/NFS 做快速迭代。
| 环境组件 | 作用 | 配置要点 |
|---|---|---|
| Buildroot | 轻量 rootfs + 工具链 | BR2_PACKAGE_MTD_UTILS(含 mtd-utils)、BR2_PACKAGE_UBIFS_UTILS;target 上要有 ubiattach/ubimkvol/ubiformat/flash_erase/nanddump/nandwrite。 |
| Yocto / OpenEmbedded | 量产级发行版 | IMAGE_INSTALL:append = " mtd-utils";内核配置走 kernel-meta 的 .cfg 片段,避免每次 menuconfig。 |
| TFTP | U-Boot 拉内核 / dtb | 主机装 tftpd-hpa,U-Boot 侧设 serverip 与 tftpboot ${kernel_addr_r} Image。 |
| NFS rootfs | 免烧录调 rootfs | 内核需开 CONFIG_ROOT_NFS 与网卡驱动;bootargs 用 root=/dev/nfs nfsroot=...。注意 NAND 场景下 NFS 调试不能验证 Flash 写入行为,仅用于应用层调试。 |
| 串口日志 | 唯一的救砖手段 | 内核 cmdline 带 console=ttyS0,115200 earlycon;早期死机只能靠串口或 earlycon 输出判断。 |
| JTAG / SWD | 无串口时兜底 | 用于确认 SoC 是否跑飞、BootROM 是否加载到 SPL。 |
串口(看 dmesg)+ TFTP(换内核)+ NFS(换 rootfs)+ mtd-utils(验证 Flash) 四件套搭好之后, 一次「改 DTS → 重启 → 看 probe 日志」的循环可以压到 30 秒以内。项目初期先花半天搭好这套环境,后面能省下数天。
2 · 硬件设计规范(驱动视角)
驱动工程师不需要画原理图,但必须能读懂原理图并指出哪些地方会让驱动跑不起来。 本章只讲与驱动行为直接相关的硬件约定;器件级电气参数与封装以《SPI NAND Flash 电路设计指南》与 datasheet 为准。
2.1 原理图设计要点
| 信号 / 项目 | 设计要求 | 驱动侧症状(设计错时的表现) |
|---|---|---|
| SCLK | 尽量短、少过孔;与其他高速信号保持间距;串联阻尼电阻(典型 22~33Ω)靠近 SoC 端 | 高频下读回数据错位、ECC 报错;降低 spi-max-frequency 后恢复正常 → 基本可判定 SI 问题。 |
| MOSI / MISO | 与 SCLK 大致等长;避免形成长分支(Stub) | 特定命令(如 Quad 读 6Bh/EBh)返回全 FF 或全 00。 |
| CS# | 每器件独占;不建议硬件上下拉复用 | 多器件时 ID 读错,或 probe 到「不存在的器件」。 |
| 上拉 / 下拉 | CS# 上拉(避免上电期间误选中);MISO 视控制器要求;WP# / HOLD# 不可浮空 | 上电偶发读错;WP# 浮空导致写入被随机禁止或允许。 |
| RESET# | 建议接 GPIO(或硬件 RC 复位);无 GPIO 时至少保证上电复位可靠 | 掉电/热复位后器件状态机卡死,驱动只能等超时。 |
| WP# | 接 GPIO 或固定上拉(不使用写保护时) | 写保护位生效时写操作被静默丢弃,flash_erase 报 EIO。 |
| HOLD# / DQ3 | Quad 模式下该脚复用为 DQ3,必须按 datasheet 处理(通常上拉) | Quad 模式失效、退回单线,吞吐掉到 1/4。 |
| 阻抗与等长 | SPI 四线组内等长(误差按平台规范,通常 ≤ 数 mm~cm 量级) | 高频读写偶发失败,低温/高温下更明显。 |
2.2 电源设计
| 项目 | 要求 | 说明 |
|---|---|---|
| 供电域 | VCC 与 VCC_IO 按器件规格(1.8V 或 3.3V) | 1.8V 与 3.3V 器件混用是最低级的硬件事故:能读到 ID 但读写大量报错,甚至永久损坏器件。 |
| 去耦电容 | VCC 引脚就近 100 nF + 1~10 µF(按 datasheet) | 编程/擦除瞬间电流尖峰大,去耦不足会造成瞬时跌落 → 表现为偶发 P_FAIL / E_FAIL。 |
| 电源轨 | 与 SoC SPI 控制器 IO 电压匹配,必要时加电平转换 | 电平不匹配会导致读回数据恒为 0xFF 或 0x00。 |
| 浪涌 / ESD | 接口侧 ESD 保护;热插拔场景加缓启动 | ESD 损伤往往表现为「能读 ID 但某几块永远写不进」。 |
| 上电时序 | VCC 稳定后再释放 RESET#;满足 datasheet 的 tPU / tRST | 复位未释放时访问器件 → 全 FF 或总线挂死。 |
| 掉电检测 | 关键应用建议加掉电检测电路 + 足够储能 | 配合 12.4 节的上层 sync / 双备份策略才能真正保证数据完整。 |
2.3 硬件信号测试标准
| 测试项 | 判据(示例,以 datasheet 为准) | 工具与做法 |
|---|---|---|
| 电源纹波 | VCC 峰峰值在器件容限内(典型 ≤ 5% VCC) | 示波器 20MHz 带宽限制,探头地线尽量短,测试点见 2.4。 |
| SPI 信号完整性 | SCLK 单调、无过冲超限;数据建立/保持时间满足器件 tSU/tHD | 示波器看 SCLK/MOSI 眼图;高频(> 50MHz)必须实测。 |
| 复位时序 | RESET# 低电平宽度与释放后的 tRST 满足规格 | 逻辑分析仪抓 RESET# 与第一次读 ID 命令的间隔。 |
| CS# 时序 | 满足 tCSH / tCSS(片选建立保持) | 逻辑分析仪;CS# 提前撤销是高频下最常见的控制器侧 bug。 |
| Quad 模式 | QE 位使能后,6Bh/EBh 命令确实走四线 | 逻辑分析仪解码;若仍为单线,检查 QE 位与 op variant 选择。 |
| 读写耗时实测 | tPROG / tBERS 与 datasheet 一致 | 示波器抓 CS# 拉低持续时间,与驱动轮询耗时对照,可判断轮询是否正常。 |
2.4 必留测试点
板子回来看不到信号就没法联调。下面这张图是硬件必须提供的最小测试点集合, 新板评审时按图核对。
2.5 BOM 选型约束
不同品牌、不同系列的 SPI NAND 在页大小、OOB 大小、ECC 强度、命令集、QE 位位置上并不一致。 驱动侧的器件表就是为这些差异准备的——选型阶段就要把能力表定下来。
| 品牌 | MFR ID(常见值) | 典型差异点 | 适配注意 |
|---|---|---|---|
| XTX(芯天下) | 0x0B | XT26G01D/G11D(1Gb)、G02D/G12D(2Gb)、G04D(4Gb);On-Die ECC 8bit/512B | 主线 xtx.c 已支持(6.1+ 系列);老内核需 backport,注意 xt26xxxd_ooblayout 与 xt26g0xa 是两套不同布局。 |
| Winbond | 0xEF | W25N01GV / W25N02KV 等;部分型号支持 Continuous Read | 主线 winbond.c;注意 BUF 位(B0h)与 Continuous Read 的组合。 |
| GigaDevice(兆易) | 0xC8 | GD5FxGQ4xA / GD5FxGQ5x;ECC 状态位编码与 XTX 不同 | 主线 gigadevice.c;ECCS 编码各家不通用,必须按 datasheet 实现 ecc_get_status()。 |
| Macronix(旺宏) | 0xC2 | MX35LF 系列;OOB 布局与 ECC 状态位自成体系 | 主线 macronix.c;注意八线 / DTR 变体需要控制器支持。 |
| Micron | 0x2C | MT29F 系列;最早的参考实现(spinand 框架即源自 Micron 提交) | 主线 micron.c;老器件 ECC 状态位在 C0h 的 bit[5:4]。 |
| Toshiba / Kioxia | 0x98 | TC58CVG 系列;部分容量页大小为 4KB | 主线 toshiba.c;4KB 页器件要确认 UBI 的 subpage 与 VID 偏移。 |
| ESMT / ATO / Alliance / Paragon / Foresee | 各异 | 多为兼容型号,参数接近主流品牌 | 主线已分别提供 esmt.c / ato.c / alliancememory.c / paragon.c / foresee.c(版本不同略有差异)。 |
① 页大小(2KB / 4KB) ② OOB 大小(64B / 128B / 256B) ③ 块大小与总块数
④ ECC 强度与扇区粒度(如 8bit/512B) ⑤ 是否 On-Die ECC、能否关闭 ⑥ 命令集(是否支持 Quad / QPI / DTR / Continuous Read)
这 6 项直接决定第 7 章适配时 SPINAND_INFO() 怎么填。缺任何一项都要先找原厂要全量 datasheet,不要靠「兼容替代」的口头承诺。
3 · 内核配置与编译构建
SPI NAND 在内核里是一个「开开关」的问题:把 MTD、spinand、UBI、UBIFS 四项开对, 器件就能被识别。本章给出完整可勾选的 config 清单、内置/模块两种编译方式,以及 U-Boot 侧配套配置。
3.1 内核 config 配置项清单
必选项(缺一不可)
# —— SPI 侧 ——
CONFIG_SPI=y # SPI 总线核心
CONFIG_SPI_MASTER=y # (部分内核版本中与 SPI 合并)
CONFIG_SPI_MEM=y # spi-mem 抽象层(通常被自动 select)
CONFIG_SPI_<YOUR_SOC>=y # 具体 SoC 的 SPI 控制器驱动(如 CONFIG_SPI_PL022 / CONFIG_SPI_SUNXI 等)
# —— MTD / SPI NAND ——
CONFIG_MTD=y
CONFIG_MTD_SPI_NAND=y # 或 =m,产出 spinand.ko
CONFIG_MTD_NAND_CORE=y # 被 MTD_SPI_NAND 自动 select
CONFIG_MTD_OOPS=y # 可选:panic 时把日志写进 Flash,对量产排查很有用
# —— UBI / UBIFS ——
CONFIG_MTD_UBI=y
CONFIG_MTD_UBI_FASTMAP=y # 强烈建议:attach 时间从数十秒降到秒级
CONFIG_MTD_UBI_GLUEBI=m # 可选:把 UBI 卷再暴露成 MTD(很少用,按需)
CONFIG_UBIFS_FS=y
CONFIG_UBIFS_FS_ZSTD=y # 可选:压缩算法(另有 LZO / ZLIB)
# —— 分区表 ——
CONFIG_MTD_OF_PARTS=y # 设备树 fixed-partitions(推荐)
# CONFIG_MTD_CMDLINE_PARTS=y # 二选一:命令行 mtdparts(老方案)
调试用选项(问题定位时打开,量产后关闭)
CONFIG_MTD_TESTS=m # mtd_nandecctest / mtd_pagetest 等内核自测模块
CONFIG_MTD_UBI_DEBUG=y # UBI 内部详细日志(部分版本提供)
CONFIG_MTD_UBI_DEBUG_MSG=y
CONFIG_MTD_UBI_DEBUG_PARANOID=y
CONFIG_UBIFS_FS_DEBUG=y # UBIFS 断言与调试统计
CONFIG_UBIFS_FS_DEBUG_CHKS=y
CONFIG_DYNAMIC_DEBUG=y # 【关键】dynamic_debug 的前提,见 10.1
CONFIG_DEBUG_FS=y # debugfs,ubi/ubifs 的统计都在下面
CONFIG_MAGIC_SYSRQ=y # 掉电测试时手动触发 sync / 重启
① CONFIG_MTD_UBI_FASTMAP:不开的话大容量器件 attach 会全片扫描,
表现为「开机卡在 ubi attach 十几秒」,常被误判为驱动死锁。
② CONFIG_DYNAMIC_DEBUG:不开的话 dynamic_debug/control 不存在,
调试时想开 spinand / ubi 的逐条日志无从下手,只能重新编译内核。
3.2 内置进内核 vs 编译成 ko 模块
| 方式 | 配置与部署 | 适用场景 | 注意事项 |
|---|---|---|---|
内置(=y) | 编进 Image;无需 insmod | 放 rootfs 的那个器件必须内置——内核启动时就要用它挂载根文件系统,模块此时还没加载。 | 改一次要重编内核 + 重新烧录,迭代慢。 |
模块(=m) | make modules → spinand.ko → 拷进 rootfs 后 insmod | 调试阶段改驱动最快:只换 ko 文件,不用动内核镜像。 | 依赖 spi_mem / mtd 等基础模块先加载;模块版本与内核 vermagic 必须一致。 |
| 混合 | 核心内置 + 厂商 ops 做成 ko | 多品牌器件共用一套镜像时,按需加载不同厂商模块。 | 要注意模块加载顺序与 modprobe 依赖表(depmod)。 |
# 模块方式常用命令
make M=drivers/mtd/nand/spi modules # 只编 spinand 模块(秒级)
adb push spinand.ko /tmp/ # 或用 scp / NFS
insmod /lib/modules/$(uname -r)/kernel/drivers/mtd/nand/spi/spinand.ko
rmmod spinand
modinfo spinand # 看依赖与参数
depmod -a # 重建依赖表后可用 modprobe spinand
① 换 ko 前先 rmmod,否则加载的是内存里的旧代码,改了看不到效果;
② ko 与内核必须同一次编译产出,混用会报 版本号 Rev 1.4 ... should be 而拒绝加载;
③ 根文件系统所在的 MTD 器件,其驱动(含 SPI 控制器驱动)必须内置,否则会出现
VFS: Unable to mount root fs。
3.3 DTS 编译与设备树 overlay
# 全量编译设备树
make dtbs
# 单文件编译(快)
cpp -nostdinc -I include -I arch -undef -x assembler-with-cpp board.dts board.dts.pre
dtc -I dts -O dtb board.dts.pre -o board.dtb
# 反编译已生效的 dtb(排查「到底生效了哪棵树」)
dtc -I dtb -O dts /sys/firmware/fdt -o running.dts
# 运行时确认(比反编译更直接)
ls /sys/firmware/devicetree/base/
find /sys/firmware/devicetree/base -name compatible | xargs grep -l spi-nand
| 方案 | 做法 | 适用 |
|---|---|---|
| 板级 DTS / DTSI | 每个板型一个 board.dts,公共部分抽到 board.dtsi | 主流做法,量产推荐。 |
| 设备树 overlay | 运行时用 dtbo 叠加节点(需 CONFIG_OF_OVERLAY) | 同一镜像适配多种板型 / 扩展板;U-Boot 侧用 fdt apply。 |
| U-Boot 内修改 | U-Boot 命令行 fdt set / fdt rm 后再 bootm | 调试阶段临时改参数(如降 spi-max-frequency)最快,不用重编内核。 |
怀疑频率太高导致偶发 ECC 错误时,不要重编内核,直接在 U-Boot 里改:
fdt set /soc/spi@xxx/flash@0 spi-max-frequency <0x3200000>(改为 52MHz)后启动,
看是否稳定。确认是频率问题后再回头改 DTS。
3.4 Rootfs 构建:mtd-utils 工具包
没有 mtd-utils 就没有调试手段。无论 Buildroot 还是 Yocto,这个包必须进 rootfs。
# Buildroot
make menuconfig
Target packages ---> Hardware handling --->
[*] mtd-utils # 含 flash_erase / nanddump / nandwrite / mtd_debug
[*] ubi health # ubiattach / ubimkvol / ubiformat / ubinize ...
# 或直接写 defconfig
echo 'BR2_PACKAGE_MTD_UTILS=y' >> configs/board_defconfig
# Yocto
IMAGE_INSTALL:append = " mtd-utils mtd-utils-ubifs"
# 主机侧(做镜像用,通常 apt 安装)
sudo apt install mtd-utils
| 工具 | 来源包 | 用途 |
|---|---|---|
flash_erase / flash_eraseall | mtd-utils | 擦除 MTD 分区 / 整片 |
nanddump / nandwrite | mtd-utils | 带 OOB 的裸页读写(调试 ECC 与坏块必备) |
mtd_debug | mtd-utils | 直接 ioctl 读写 MTD(read/write/erase/info) |
flashcp / dd | mtd-utils / coreutils | 往 MTD 分区写镜像(NOR 常用,NAND 慎用) |
ubiformat | mtd-utils | 把 MTD 分区格式化为 UBI(含坏块处理) |
ubiattach / ubidetach | mtd-utils | 把 MTD 分区挂到 / 摘出 UBI |
ubimkvol / ubirmvol | mtd-utils | 创建 / 删除 UBI 卷 |
ubiupdatevol | mtd-utils | 往指定卷写镜像(OTA 常用) |
ubinize / mkfs.ubifs | mtd-utils(主机侧) | 制作 UBIFS 卷镜像 / 打包 UBI 镜像 |
ubinfo / ubihealthd | mtd-utils | 查看 UBI 状态 / 健康监控(版本差异较大) |
3.5 U-Boot 配套配置
U-Boot 需要能读 SPI NAND,才能把内核 / dtb / UBI 镜像加载起来。U-Boot 与内核用的是同一套 spinand 代码(U-Boot 会周期性从 Linux 同步),但配置开关名字不同。
# U-Boot defconfig 必开项
CONFIG_MTD=y
CONFIG_MTD_SPI_NAND=y # U-Boot 侧 spinand 框架
CONFIG_SPI=y
CONFIG_DM_SPI=y
CONFIG_SPI_MEM=y
CONFIG_CMD_MTD=y # mtd list / mtd read / mtd write / mtd erase
CONFIG_CMD_UBI=y # ubi part / ubi attach
CONFIG_CMD_UBIFS=y # ubifsmount / ubifsload / ubifsls
CONFIG_CMD_SF=y # (SPI NOR 用,NAND 场景可选)
CONFIG_ENV_IS_IN_UBI=y # 环境变量存 UBI 卷(可选)
CONFIG_SPL_SPI_NAND_SUPPORT=y # 若由 SPL 从 SPI NAND 加载 U-Boot
| U-Boot 命令 | 作用 | 示例 |
|---|---|---|
mtd list | 列出 MTD 设备与分区 | mtd list |
mtd read/write/erase | 裸读写擦(按分区名或偏移) | mtd read rootfs ${loadaddr} |
ubi part | 把某分区 attach 到 UBI | ubi part rootfs |
ubifsmount | 挂载 UBIFS 卷 | ubifsmount ubi0:rootfs |
ubifsload | 从 UBIFS 卷读文件 | ubifsload ${kernel_addr_r} /boot/Image |
ubi writevol | 写 UBI 卷(产线/OTA) | ubi writevol ${addr} rootfs ${size} |
U-Boot 的 drivers/mtd/nand/spi/ 是从 Linux 周期性同步的,版本落后于内核是常态。
后果是:同一颗器件,内核能识别、U-Boot 不认识(或反之),表现为「U-Boot 里 mtd 看不到设备,但 Linux 起来后一切正常」。
对策:新器件导入时,U-Boot 与内核要分别验证 ID 匹配,把两边都合上;不能只验证内核。
4 · 驱动架构与代码框架
本章回答「内核里到底有哪些代码在跑」。读完应当能定位任意一个问题落在哪一层, 以及新器件适配时要改哪个文件、填哪张表。
4.1 软件分层总览
SPI NAND 的完整链路是 UBIFS → UBI → MTD → NAND core → spinand → spi-mem → SPI 控制器 → 器件。 其中「自己写的代码」通常只占最下面两层中的厂商 ops 部分。
拿到任何一条异常日志,先判断它来自哪一层,问题范围立刻收敛一半:
spi-nand: ...→ spinand 框架或厂商 ops(ID、ECC 判级、命令序列)spi_master spi0: .../spi-xxx: ...→ SPI 控制器(DMA、时钟、CS)ubi0: .../UBI error→ UBI 层(卷、坏块、attach、fastmap)UBIFS error→ 文件系统(日志、GC、压缩、掉电恢复)mtd: ...→ MTD 核心(分区、OOB、ioctl)
4.2 源码目录与文件职责
| 路径 | 职责 | 什么时候要动它 |
|---|---|---|
drivers/mtd/nand/spi/core.c | spinand 框架主体:probe、读/写/擦、ECC 状态轮询、坏块判断 | 原则上不改;只有框架级 bug 才动,且优先 upstream。 |
drivers/mtd/nand/spi/xtx.c(winbond.c / gigadevice.c / macronix.c / micron.c / toshiba.c ...) | 厂商器件:ID 表、OOB layout、ECC 判级、QE 使能、op 变体 | 新增器件时主要改这里(见第 7 章)。 |
drivers/mtd/nand/spi/Makefiledrivers/mtd/nand/spi/Kconfig | 编译进 spinand 模块 | 新增厂商 .c 文件时改这两个。 |
include/linux/mtd/spinand.h | 框架数据结构与宏(SPINAND_ID / SPINAND_INFO / 各 OP 宏) | 新增厂商时加一行 extern const struct spinand_manufacturer xxx;。 |
drivers/mtd/nand/core.c | NAND 通用层:memorg、坏块表、ECC engine 抽象 | 一般不动。 |
drivers/mtd/mtdcore.c / mtdchar.c | MTD 子系统:分区解析、/dev/mtdX、ioctl | 一般不动。 |
drivers/mtd/ubi/*.c | UBI:attach、卷管理、磨损均衡、fastmap、坏块处理 | 调参与排错,不改逻辑。 |
drivers/spi/spi-mem.cdrivers/spi/spi-<soc>.c | spi-mem 抽象与具体控制器驱动 | 控制器不支持某 op / DMA 异常时才动。 |
fs/ubifs/*.c | UBIFS 文件系统 | 调挂载参数与排错,不改逻辑。 |
4.3 关键数据结构
下面三个(组)结构是所有适配工作的落点。字段名随内核版本有变化,
动手前请以手上源码的 include/linux/mtd/spinand.h 为准。
| 结构 | 作用 | 适配时关心的字段 |
|---|---|---|
struct spinand_device | 一个 SPI NAND 器件的软件实例 | slave(spi_mem 句柄)、base(nand_device)、scratchbuf(页缓存)、eccinfo(ECC 判级)、op_templates(读/写/更新 cache 的命令变体)、manufacturer |
struct nand_device( struct nand_memory_organization) | 器件几何与 NAND 通用行为 | pagesize / oobsize / pages_per_eraseblock / eraseblocks_per_lun / planes_per_lun;由 SPINAND_INFO 的 memorg 填充,不要手写 |
struct mtd_info | MTD 层对上暴露的抽象 | size / erasesize / writesize / oobsize / type / flags / _read / _write / _erase;驱动注册后由用户态通过 /dev/mtdX 访问 |
struct spinand_manufacturer | 厂商描述:ID + 器件表 + ops | id / name / chips / nchips / ops;新厂商要在 core.c 的 spinand_manufacturers[] 数组里登记 |
struct spinand_info(SPINAND_INFO 宏) | 单个器件的全部能力 | .model / .devid / .memorg / .op_variants / .flags / .eccinfo / .ooblayout / .select_target |
struct mtd_ooblayout_ops | OOB 里哪些字节归 ECC、哪些可给用户 | .ecc / .free;布局错了会导致 ECC 校验值被用户数据覆盖 |
struct nand_ecc_engine(6.x) | ECC 引擎抽象 | On-Die ECC 对应 NAND_ECC_ENGINE_TYPE_ON_DIE;5.x 用 spinand_eccinfo 的 get_status |
/* 典型厂商器件表条目(示意,字段名以实际内核为准) */
static const struct spinand_info xtx_spinand_table[] = {
SPINAND_INFO("XT26G01D",
SPINAND_ID(SPINAND_READID_METHOD_OPCODE_DUMMY, 0xE1),
NAND_MEMORG(1, 2048, 128, 64, 1024, 1, 1, 1), /* luns, pagesize, oobsize, pages_per_eraseblock(64), blocks_per_lun, planes, ... */
NAND_ECCREQ(8, 512), /* On-Die ECC: 8bit / 512B */
SPINAND_INFO_OP_VARIANTS(&read_cache_variants,
&write_cache_variants,
&update_cache_variants),
0,
SPINAND_ECCINFO(&xt26xxxd_ooblayout,
xt26xxxd_ecc_get_status)),
};
const struct spinand_manufacturer xtx_spinand_manufacturer = {
.id = SPINAND_MFR_XTX, /* 0x0B */
.name = "XTX",
.chips = xtx_spinand_table,
.nchips = ARRAY_SIZE(xtx_spinand_table),
.ops = &xtx_spinand_manuf_ops,
};
① OOB layout 的坏块标记区:section 0 的第 0 字节(部分规范要求 2 字节)必须保留给 BBM,
.free 从 offset 1(或 2)开始,否则 UBI 会把坏块标记当成可用空间写掉。
② ECC 状态判级函数返回值:可纠应返回纠正的 bitflip 数(0 表示无翻转),
不可纠必须返回 -EBADMSG;返回 0 会让上层以为数据完好,造成静默数据损坏。
4.4 probe 执行流程
下图是 spinand_probe() 的主干。理解它之后,「设备树写了但没出 mtd 设备」这类问题可以逐步对位。
各阶段失败时的典型日志与判据
| 阶段 | 失败判据 / 日志 | 下一步 |
|---|---|---|
| ① 设备树匹配 | /sys/bus/spi/devices/ 下没有对应设备;或 of_match 失败 | 检查 compatible 拼写、节点是否在 &spi0 下、pinctrl 是否生效、SPI 控制器驱动是否加载 |
| ② 读 ID | unknown raw ID / ID 为 000000 或 ffffff | 全 FF → 供电/片选/接线;全 00 → MISO 未连通或模式错;有值但不认识 → 器件表缺项 |
| ③ 匹配器件表 | spinand: unknown raw ID ... | 按第 7 章加 ID 表条目 |
| ④ 填 memorg | 启动后 /proc/mtd 的 erasesize / writesize 与器件不符 | 核对 NAND_MEMORG() 参数与 datasheet |
| ⑤ 选 op 变体 | Quad 读返回错误数据,单线正常 | 检查 QE 位使能流程与 op variants 顺序 |
| ⑥ ECC / OOB | 读写报 -EBADMSG 或 bitflip 异常升高 | 核对 ECC 判级函数与 OOB layout |
| ⑦ 注册 MTD | /proc/mtd 为空 | 看 mtd_device_register() 返回值;分区解析失败也会在此暴露 |
4.5 页 / OOB / ECC 布局与 UBI 开销
理解这张图,就能解释「为什么 128MB 的器件格式化成 UBI 后只剩 100 多 MB 可用」。
| 开销项 | 典型值(2KB 页 / 128KB 块) | 说明 |
|---|---|---|
| EC 头(Erase Counter Header) | 1 页 | 记录该 PEB 的擦除次数,磨损均衡依据 |
| VID 头(Volume Identifier Header) | 1 页 | 记录该 PEB 属于哪个卷、LEB 号 |
| LEB 大小 | PEB − 2 页 = 126 KB | 用户实际可用的逻辑块大小 |
| UBI 管理开销 | 约 2 个 PEB(layout volume)+ 坏块预留 | ubinize 与 ubiformat 会自动预留 |
| 坏块预留 | 默认按器件标称坏块比例的 2 倍左右预留 | 可用 ubiformat / ubinize 参数调整 |
| UBIFS 自身开销 | 日志 + 索引节点 + LPT | 挂载后 df 看到的可用空间还会再少一些 |
4.6 中断与 DMA
SPI NAND 走的是 SPI 控制器,没有 NAND 控制器那种专用中断。性能优化与稳定性问题基本都落在 SPI 控制器的 DMA 与 FIFO 行为上。
| 项目 | 说明 | 排查要点 |
|---|---|---|
| DMA 传输 | 大页读写(2KB / 4KB)应由 DMA 承担;PIO 模式 CPU 占用极高 | cat /proc/interrupts 看 SPI 中断频率;top 看 CPU 是否被读写打满 |
| FIFO 深度 | 控制器 FIFO 太小会频繁中断;大页传输可能被拆成多段 | 控制器驱动需支持 spi_mem 的分段传输;FIFO 溢出表现为数据错位 |
| CS 保持 | 整条 op 期间 CS# 必须保持有效,不能每字节拉高一次 | 逻辑分析仪看 CS#:若被切成碎段,驱动或控制器配置有问题 |
| 时钟上限 | spi-max-frequency 取「SoC 能力 ∩ 器件能力」 | 超规格偶发 ECC 错误;降温/加压可复现 |
| 中断 vs 轮询 | spinand 用轮询状态寄存器(OIP),不用中断 | 轮询间隔与超时在框架内;超时短会误判失败 |
| 缓存一致性 | DMA buffer 必须是 dma_alloc_coherent 或正确 sync | 非一致性 DMA 表现为「读出的数据总是旧的一页」 |
① 传给 SPI 的 buffer 不能是栈上的临时变量(除非框架内部已做 bounce buffer)——
栈内存可能不满足 DMA 对齐与一致性要求,在部分架构上会直接踩坏内存。
② DMA 完成前不要复用 buffer;spi_sync() 返回后才代表传输结束。
4.7 编码规范与 remove 函数
- checkpatch 必过:
./scripts/checkpatch.pl --strict --no-tree 0001-*.patch;upstream 提交对--strict也要求零 error。 - 错误处理走 goto 链:内核惯例,出错时按资源申请的反序释放,不要用「一路 return + 每层重复释放」。
- 优先 devm_* 系列:
devm_kzalloc/devm_gpiod_get等,remove 时自动释放,减少漏释放。 - remove 与 probe 严格对称:probe 里申请的每一份资源都要有对应释放;
rmmod后再insmod必须能正常工作(这是最有效的自测)。 - 日志等级克制:正常路径用
dev_dbg(由 dynamic_debug 控制),异常才用dev_err;量产日志里刷屏的dev_info要清掉。 - 提交粒度:一个 patch 做一件事;新增器件、修 bug、改格式分成三个 patch。
- Signed-off-by 必带:
git commit -s,否则 upstream 不接受。
/* remove 与 probe 对称的典型写法(示意) */
static int spinand_remove(struct spi_mem *mem)
{
struct spinand_device *spinand = spi_mem_get_drvdata(mem);
int ret;
ret = mtd_device_unregister(&spinand->base.mtd);
if (ret)
return ret;
nanddev_cleanup(&spinand->base);
kfree(spinand->scratchbuf);
/* devm_* 申请的资源无需在此释放 */
return 0;
}
/* 自测:反复加载卸载 20 次不应有内存泄漏或 oops */
# for i in $(seq 1 20); do modprobe spinand; rmmod spinand; done; dmesg | tail