Linux 并口 NAND(PPI NAND)驱动开发指南
编制日期 2026-10-05
MTD / rawnand 路线全解 · 控制器驱动三条路线 · 设备树 · ECC 与坏块 · UBI 落地
这份文档的核心主张:并口 NAND 在 Linux 下有现成的主线框架可走,先确认你的 SoC 有没有硬件 NFC 控制器。有就走厂商 host 驱动,没有就用主线的 gpio-control-nand,两条路都不通才需要自研——自研的代价(DMA、超时、并发、掉电保护)远大于多数人预期。本文把「什么时候必须自研」「自研要付多少代价」写清楚,而不是鼓励自研。
这份文档解决什么
并口 NAND(也写作 PPI NAND / Parallel NAND / Raw NAND)是容量与成本最优的一类非易失存储,在工控、路由器、IoT 网关、国产化替代的主控板上大量使用。但和 eMMC、SD NAND 不同,它在 Linux 里没有一个「插上就能用」的通用块设备路径:内核只提供 MTD 框架和几套 host 驱动,剩下的要靠设备树和几何表说清楚。
本文回答的问题:
- PPI NAND 和 SPI NAND 到底差在哪,为什么驱动不能互相套用;
- 你的项目应该走哪条控制器路线,各自的代价是什么;
- 设备树怎么写、哪些属性是必填、常见错法长什么样;
- ECC 引擎怎么选、OOB 怎么排、XTX XT27G02A/B 的坏块标记为什么不在 OOB 里;
- UBI 的 min_io_size 对齐陷阱,以及 UBIFS 参数怎么算;
- 出现「探测不到」「ECC 报错」「UBI attach 失败」时怎么定位。
读者路径
本文是Linux 主机侧指南。器件层面的内容不在这里重复造轮子:
- 引脚电气、走线、R/B# 上拉电阻取值 → 《并行NAND_PPI_NAND 电路设计指南》
- 命令编码细则、OOB 通用布局、ECC 分级与迁块阈值、BBT 格式、掉电场景分析 → 《并行NAND_PPI_NAND 软件设计规范》
- Linux 内核侧怎么用、驱动怎么写、设备树怎么配、出错怎么查 → 本文
1 · 先把事实说清楚
在写第一行驱动代码之前,有四件事必须先确定: 它在物理上是什么、它在 Linux 里以什么形态存在、它和本系列其它器件差在哪、 以及它的可靠性责任归谁。这四件事里有任何一件搞错, 后面所有的工作都会建立在一个错误的前提上,而这种错误的代价往往要到量产才暴露。
1.1 并口 NAND 到底是什么:一颗没有封装外壳的 SPI Flash
先把这个比喻说清楚,因为它能省掉后面很多困惑。 并口 NAND 与 SPI NAND 的关系,好比「并口硬盘」与「USB 硬盘」: 里面都是 NAND 存储阵列 + 一颗片内控制器,差别只在外面怎么接线、 主机怎么跟它说话。
「并口」指的是数据总线直接挂在芯片引脚上,典型 x8(8 根数据线) 或 x16(16 根),加上 CLE、ALE、CE#、WE#、RE#、R/B#、WP# 共 7 根控制线, 一共 15~23 根信号。主机通过这套并行接口逐字节地把命令、地址、数据打进器件。 「SPI」则是把这套接口串行化成 4 根线(SCLK、MOSI、MISO、CS), 代价是带宽低,换来的是布线简单、能跑高时钟。
| 维度 | 并口 NAND(PPI) | SPI NAND |
|---|---|---|
| 数据线数量 | 8 或 16 根 | 1 根双向(MOSI/MISO 共用一根) |
| 控制线数量 | 7 根(CLE/ALE/CE#/WE#/RE#/R-B#/WP#) | 1 根(CS) |
| 主机接口 | SoC 的 NFC 控制器或 GPIO 模拟 | SPI 控制器 |
| 单线带宽 | 一次并行传 8/16 位 | 每次传 1 位 |
| 可达吞吐 | 高(受总线与控制器限制) | 受 SPI 时钟限制,通常 < 50 MHz |
| 布线复杂度 | 高,14~23 根等长走线 | 低,4 根 |
| 引脚占用 | 多,可能与其它外设抢引脚 | 少 |
| 典型应用 | 工控、网关、国产化主控板、大容量低成本存储 | 小型终端、消费电子、SD 卡替代 |
很多 SoC 的引脚预算是紧的。一个 32 引脚的 QFN 封装主控, 留给外部存储的引脚可能只有十几根——并行 NAND 单独就要占掉 20 个左右。 这时候即使并口 NAND 在成本和容量上再划算,也只能选 SPI NAND 或 SD NAND。 评估并口 NAND 方案时,第一步应该数引脚,而不是比价格。
1.2 Linux 主线的位置:MTD/rawnand,不是块设备
这是并口 NAND 在 Linux 里最容易被误解的一点,也是一系列后续困惑的根源。
在 Linux 里,并口 NAND 不是块设备。
内核不会给它创建 /dev/sda 这样的节点,
也不会把它挂成某种可以直接 mount 的普通文件系统。
它挂在 MTD(Memory Technology Device) 子系统下面, 对外表现为两种设备:
| 设备节点 | 类型 | 用途 | 谁在用 |
|---|---|---|---|
/dev/mtd0 | 字符设备 | 按页 / 按 OOB 精确读写,绕过缓存与文件系统 | nanddump、nandwrite、烧写工具 |
/dev/mtdblock0 | 块设备 | 按块读写,不含 OOB 与坏块信息 | dd、文件系统、UBI |
内核里的实现路径是:drivers/mtd/nand/(6.5 起为
drivers/mtd/nand/raw/)里的 rawnand 框架,
加上 SoC 厂商的 host 驱动(或主线的 gpio-control-nand)。
rawnand 之上可选地叠 UBI 与 UBIFS,得到一个真正的文件系统。
这五层的责任划分,是理解后面所有章节的基础。
- 不能直接 mount 一个裸 MTD 分区当 rootfs。
裸分区上没有文件系统,mount 会报
wrong fs type。 要么用 JFFS2(它支持直接挂裸 MTD),要么先建 UBI 再建 UBIFS。 - 不能指望内核做磨损均衡。
你往
/dev/mtdblock0里连续写 1 GB,同一个物理块会被反复擦写, 而同一条命令对 NAND 芯片的其它块毫无影响。这就是第 10 章要引入 UBI 的原因。 - 不能假设「写成功了」等于「能读回来」。 写路径的 ECC 字节如果没配对,下次读时校验会失败, 而这个失败是延迟的——写的时候不报错。
1.3 三条技术路线与本系列其它器件的根本差异
Linux 生态里有四类主流的非易失 Flash:SPI NOR、SPI NAND、PPI NAND、SD NAND(eMMC 那一类)。 它们在主机侧的接口形态完全不同,驱动代码不能互相套用。 这也是本系列每类器件单独出一份指南的原因。
| 器件类型 | 内核框架 | 用户态接口 | 有无 OOB | 有无坏块 | 有无内置 FTL |
|---|---|---|---|---|---|
| SPI NOR | MTD / spi-nor | /dev/mtd0 | 无 | 无 | 无(但有 OTP 区) |
| SPI NAND | MTD / rawnand + SPI 层 | /dev/mtd0 | 有(64~224 B) | 有 | 无 |
| PPI NAND(本文) | MTD / rawnand + NFC 或 GPIO | /dev/mtd0 | 有(16~256 B) | 有 | 无 |
| SD NAND / eMMC | MMC 框架 | /dev/mmcblk0 | 片内,不暴露 | 片内管理 | 有 |
把这张表横着看,可以得到三条最容易被忽略的差异:
差异一:OOB 是并口与 SPI NAND 独有的
SPI NOR 与 SD NAND 都不暴露 OOB 概念——它们的数据是连续字节流,主机读写 「地址 + 长度」就完事。而 PPI NAND 与 SPI NAND 每一页尾部都带一段 OOB (也叫 Spare Area),用来存 ECC 字节、坏块标记、厂商元数据。 OOB 的存在让「读一页」变成了「读主区 + 读 OOB」两个动作, 而两者的读取方式在硬件上并不相同——这正是第 4 章 exec_op 模型要解决的问题。
差异二:SPI NOR 与 SD NAND 没有坏块概念
SPI NOR 是 NOR 结构,擦写单位是整个扇区,没有「块」这个中间层级, 因此不需要坏块管理。SD NAND 与 eMMC 有片内 FTL,把坏块与磨损均衡全包了, 主机看到的是一个干净的块设备。只有 PPI NAND 与 SPI NAND 需要主机操心坏块, 而这两者的坏块标记位置还互不相同(一个是 OOB 首位,一个可能别的地方), 这也是第 8 章要专门讲一节的原因。
差异三:SD NAND 有一个「能用的块设备」,PPI NAND 没有
这是选型时最实际的差别。SD NAND 插上就是块设备,格式化即用;
PPI NAND 插上是一堆 /dev/mtdN,要自己决定用不用 UBI、用什么文件系统。
前者是「集成成本低但单价高」,后者是「单价低但集成工作量大」。
本文档要讲清楚的,就是这部分集成工作的具体内容与代价。
常见错误是把 SPI NAND 驱动开发指南里的东西直接搬到并口 NAND 上。至少有三处必错:
- 页编程命令不同。并口是
80h → 10h两段式, SPI 是02h → 10h三段式,还多一对0Fh / 1FhFeature 命令。 - 随机访问的编码不同。并口是
05h … E0h, SPI 是2Ch / 3Dh系列。 - badblock_pattern 的偏移基准不同。并口侧 OOB 偏移是相对 OOB 首字节, SPI 侧还有页内偏移的换算,写错一个字节就会读不到坏块标记。
具体的逐项对照见附录 E。
- 为什么我的并口 NAND 上不会出现
/dev/sda? /dev/mtd0与/dev/mtdblock0的区别是什么,各自谁在用?- 为什么并口 NAND 不能直接当 rootfs 挂载?
- 并口 NAND 与 SPI NAND 最容易搞混的三处是什么?
- 为什么说「评估方案先数引脚」?
这五个问题答不上来就直接看第 3 章,会漏掉关键前提。
2 · 器件与接口面
在讨论任何驱动代码之前,先把器件这一侧的「事实」摆清楚。驱动里所有的几何表、时参数、坏块偏移都来自本章,而这些值一旦错了,症状往往是「能识别但数据错」,比「识别不到」难查得多。
2.1 PPI NAND 的关键几何与能力参数
「几何」指器件的物理组织结构。内核靠一组几何参数把「行地址 / 列地址」翻译成「第几块第几页」,所有几何参数都直接来自器件 datasheet 或 Read ID 后的能力表,不能猜。
| 参数 | 含义 | 典型值(x8 SLC) | 配错的后果 |
|---|---|---|---|
| page_main | 每页主区数据字节数 | 512 / 2048 / 4096 | UBI 的 min_io_size 错、UBIFS 全部参数连带错 |
| page_oob | 每页 OOB(Spare)字节数 | 16 / 64 / 128 / 218 | ECC 写越界、坏块标记读不到、UBI 头写不下 |
| pages_per_block | 每块页数 | 32 / 64 / 128 | 块大小算错,分区表整体偏移 |
| blocks_per_lun | 每 LUN 块数 | 2048 / 4096 | 容量算错,rootfs 挂载位置偏 |
| address_cycles | 地址周期数与列/行分配 | 5(2 列 + 3 行) | 读到错误页,或器件无响应 |
| on_die_ecc | 是否有片内 ECC 引擎 | 无 / 8 bit per 528 B | 主机重复算 ECC 反而破坏数据;或该纠的不纠 |
| bb_marker | 坏块标记位置与判据 | OOB 首字节 / Main 首列 | 漏判坏块,数据静默损坏(最严重) |
| tR | 页读忙时间(typ / max) | 25 µs / 100 µs | 超时报错,或轮询不够导致读出错数据 |
| tPROG | 页编程忙时间(typ) | 300 µs | 同上 |
| tBERS | 块擦除忙时间(typ) | 3.5 ms | 同上 |
识别失败会立刻暴露,几何配错不会:驱动会「正常」地读出数据,只是数据属于错误的页或错误的块。更糟的是,如果 OOB 大小配小,坏块标记就落在你以为的 OOB 范围之外,所有出厂坏块都会被当成好块——文件系统把数据写进去,然后再也读不出来。
因此第 8 章的坏块扫描必须实际跑一遍并打印结果,不能假设。
2.2 引脚与信号面:x8 与 x16 的差别在哪
「x8」「x16」指数据总线宽度:x8 是 I/O0~I/O7 共 8 根,x16 是 I/O0~I/O15 共 16 根。在设备树里体现为 nand-bus-width 取 8 或 16。这不只是「多几根线」:
| 维度 | x8 | x16 |
|---|---|---|
| 数据引脚数 | 8 根(I/O0~7) | 16 根(I/O0~15) |
| 列地址周期 | 2048 B 主区需 2 个 CA | 同样主区可能只需 1 个 CA(视器件定义) |
| 单芯片容量上限 | 通常做到 4~8 Gb | 通常做到 2~4 Gb |
| 控制器支持情况 | 绝大多数 SoC NFC 都支持 | 不少 SoC 只支持 x8 |
| Linux 属性 | nand-bus-width = <8> | nand-bus-width = <16> |
把 x8 器件按 x16 读,最典型的现象是能探测到器件、能读出 ID,但读数据全是 0xFF 或 0x00。 反过来把 x16 按 x8 读,地址周期数不对,会读到相邻页或直接触发未定义行为。
如果 SoC 的 NFC 控制器不支持 x16 而器件是 x16 位宽, 不要试图用软件位拆来凑——地址周期与时序都会错,正确做法是换器件型号。
2.3 ONFI 异步命令集与时序前提
ONFI(Open NAND Flash Interface)定义了并行 NAND 的标准命令集,绝大多数市面器件在基础命令上兼容 ONFI,扩展命令各家自定义。内核里没有一份「ONFI 命令表」可以直接填,几何与命令语义是通过 struct nand_chip 的能力表和 host 驱动的 exec_op 回调共同表达的。
| 命令 | 编码 | 作用 | 内核侧的常见用法 |
|---|---|---|---|
| Read Page | 00h … 30h | 整页读入片内缓存并校验 | nand_read_page() 的默认路径 |
| Change Read Column | 05h … E0h | 页内随机定位(配合 05h 重定位) | 读 OOB、读指定列区间 |
| Read Cache | 31h | 多页连读(提高吞吐) | 部分 host 驱动启用(rand/seq read) |
| Read Cache End | 3Fh | 结束 Cache 连读 | 与 31h 成对 |
| Block Erase | 60h … D0h | 擦除一个块 | nand_erase() 的默认路径 |
| Read Status | 70h | 读状态寄存器(OIP / Fail / ECC 位) | 擦除编程后二次确认 |
| Page Program | 80h … 10h | 两段式:先把数据装缓存,再提交 | nand_write_page() 的默认路径 |
| Copyback Program | 85h … 10h | 页内/页间内部搬移,不经主机 | 少用,需按器件支持情况开 |
| Read ID | 90h | 读厂商 ID / 器件 ID | 探测与能力表匹配 |
| Reset | FFh | 软复位,清除状态机 | 上电初始化第一步、异常恢复 |
| Read Parameter Page | ECh | 读 ONFI 参数页(256 B) | ONFI 器件的几何来源 |
| Get / Set Features | EEh / EFh | 读改写器件配置特性 | 必须 RMW,慎用 |
| Read Status Enhanced | 78h | 多 LUN 场景的带 LUN 状态读 | 多 die 器件才需要 |
并口 NAND 的页编程是 80h(列地址)→ 数据 → 10h(行地址) 两段式;而 SPI NAND 是 02h(load)→ 10h(confirm),并且 SPI 侧还有 0F/1F 这对 Feature 命令。两者编码空间部分重叠但语义不同,把 SPI NAND 的 0F/GET_FEATURE 用到并口器件上会得到完全错误的结果。
并口器件上 Get Features(EEh) / Set Features(EFh) 是另一套东西,而且很多传统并行器件(包括 XTX XT27G02A/B)根本没有这两个命令。
| 时序参数 | ONFI 含义 | Mode 0 要求(典型) | 谁负责满足 |
|---|---|---|---|
| tCLS | CLE 建立时间 | ≥ 50 ns | SoC 的 NFC 或 GPIO 翻转速度 |
| tALS | ALE 建立时间 | ≥ 50 ns | 同上 |
| tWP | WE# 脉宽 | ≥ 50 ns | 总线写周期决定 |
| tWH | WE# 高电平时间 | ≥ 30 ns | 同上 |
| tRP | RE# 脉宽 | ≥ 50 ns | 总线读周期决定 |
| tRC | RE# 周期 | ≥ 100 ns | 同上 |
| tCCS | 命令到命令间隔 | ≥ 70~100 ns | 软件插入的 ndelay 决定 |
| tR | 页读忙时间 | typ 25 µs / max 100 µs | R/B# 等待,不能靠固定延时 |
| tPROG | 页编程忙时间 | typ 300 µs | 同上 |
| tBERS | 块擦除忙时间 | typ 3.5 ms | 同上 |
上表数值为 ONFI 异步 Mode 0 的典型要求,具体到你的器件必须以 datasheet 为准。注意 tR / tPROG / tBERS 这三项由器件内部决定,主机无法加速,只能等。
2.4 芯天下 XTX XT27G02A / XT27G02B 型号对照
本系列器件在 Linux 侧使用时的关键差异点集中如下。这些值覆盖本章的通用表述,落地时以本表替换第 2.1 / 2.3 节的示例值。
| 参数 | XT27G02A | XT27G02B |
|---|---|---|
| 容量 / 组织 | 2 Gb(256M × 8) | 2 Gb(256M × 8) |
| 接口 | 传统并行(非 ONFI Async 命令集之上的 ONFI 扩展) | 同左 |
| 电压 | 3.3 V | 3.3 V |
| 数据位宽 | x8 | x8 |
| 页(Main + Spare) | 2048 + 128 B | 2048 + 64 B |
| 页寄存器大小 | 2176 B | 2112 B |
| 每块页数 | 64 页 × 2048 B | 64 页 × 2048 B |
| 块大小 | 128 KiB | 128 KiB |
| 地址周期 | 2 个列地址(CA0–CA11)+ 3 个行地址(PA0–PA16)= 5 | 同左 |
| ECC | 主机侧实现,8 bit / 544 B(每页 4 个子页) | 片内 ECC 引擎,8 bit / 528 B |
| 主机需要的 ECC 强度 | 8 bit / 544 B | 0 bit(片内已纠) |
| 坏块标记位置 | Main 区第 0 列(Column 0),值 00h | 同左 |
| 随机访问 | 05h–E0h 随机列地址变更 / 85h 随机写 | 同左 |
| ONFI 参数页 ECh | 不适用,禁止发送 | 不适用,禁止发送 |
| Get / Set Features(EEh / EFh) | 不适用,禁止发送 | 不适用,禁止发送 |
| ECC 状态读取 | 无(由主机 ECC 引擎给出结果) | 专用 ECC Status Read 命令,不是 70h 的 ECC 位 |
| tR 典型 / 最大 | 25 µs / 100 µs | 同左 |
| tPROG 典型 | 300 µs | 同左 |
| tBERS 典型 | 3.5 ms | 同左 |
1. 坏块标记不在 OOB,而在 Main 区第 0 列,值 00h。手册原文:坏块标记是整页级的,请读取每块中任意一页的一列数据,若该列数据为 00h 则该块为坏块。这与「扫 OOB 首字节是否非 0xFF」的通用做法完全相反,用通用做法会漏判全部出厂坏块。详见第 8.3 节。
2. 禁止发送 ECh / EEh / EFh。这两款是传统并行 NAND,手册中无 ONFI Parameter Page 与 Get/Set Features,发送这些命令器件响应无定义,可能进入未定义状态。若驱动需同时兼容 ONFI 与非 ONFI 器件,能力表须显式声明 onfi_supported: false 并跳过相关分支。
3. XT27G02A 必须由主机做 ECC。XT27G02B 的片内 ECC 引擎已完成 8 bit / 528 B 纠正,主机侧配置的 ECC 强度应为 0,不要再叠加一层软件 ECC。
3 · 硬件前提核查
这一章全是「动手前要确认的事」。 每一条都对应一类在软件层无法补救的问题——硬件层的问题在软件里怎么绕都绕不过去,只能返板。
3.1 SoC 侧必须先确认的三件事
| 要确认的事 | 怎么查 | 确认不通过的后果 |
|---|---|---|
| SoC 有无硬件 NAND 控制器 | 查 SoC 硬件手册章节标题;搜 NAND / EBI / FSMC / LPC / SmartLPC | 只能走 GPIO 模拟或自研,性能上限被锁死 |
| R/B# 接到哪个可读引脚 | 看原理图;确认该引脚在所用 SoC 上是双向 GPIO 且能读回 | 只能主机轮询状态寄存器,速度降一个数量级且易误判 |
| 是否有 ECC 硬件引擎 | 查手册 ECC / BCH / LDPC 章节;确认位宽与 step_size | 只能用软件 BCH,CPU 开销 30%~50% |
| 数据线挂哪个总线窗口 | 看原理图;确认地址译码与访问宽度(8 或 16 bit) | GPIO 模拟路线不成立 |
| IO 电压域 | 查 SoC 该 Bank 的电压配置寄存器(IOV 等) | 3.3 V 器件接 1.8 V 域会烧或读不出 |
| pinmux 是否被其它外设占用 | 看设备树 pinctrl 与当前生效配置 | probe 时申请 GPIO 失败,报 -EBUSY |
3.2 引脚连接、电压域与上电时序
并口 NAND 的引脚分三类,各自的处理方式不同:
| 类别 | 引脚 | 方向(对 SoC) | 上电常态电平 | 要点 |
|---|---|---|---|---|
| 数据总线 | I/O0 到 I/O7(x8) | 双向 | 高阻 | 必须配置为 GPIO 或总线复用;空闲时不得被内部上下拉拉成确定电平 |
| 命令与地址锁存 | CLE、ALE | 输出 | 低 | 上电默认为低,RESET 期间必须保持低 |
| 片选 | CE# | 输出 | 高(不选中) | 上电期间 CE# 应保持高,避免器件进入未知状态 |
| 写与读使能 | WE#、RE# | 由总线产生或 GPIO | 高 | GPIO 模拟路线中必须由软件严格控制时序 |
| 就绪与忙 | R/B# | 输入 | 高(就绪) | 开漏输出,必须外接上拉电阻(典型 10 kΩ),见 3.3 节 |
| 写保护 | WP# | 输出 | 高(允许写) | 上电必须为高;低电平下擦除与编程命令会被忽略 |
1. WP# 必须先拉高再释放 CE#。 若 CE# 已选中而 WP# 仍为低,器件会忽略所有写与擦除命令, 表现为「能读、写了没反应」——极难定位。
2. VCC 稳定后至少等 tPOR(typ 5 us,max 300 us)再发 FFh 复位。 提前复位器件可能处于不确定状态机,后续所有命令静默失败。
3. CE# 与 R/B# 的上电态要安全。 CE# 上为高(不选中),R/B# 上为高(就绪)。 如果 R/B# 上拉电压与 VCC 不同(例如 VCC 掉电而 IO 域还有电), 器件可能通过 IO 灌入反向电流触发闩锁,这是并口 NAND 唯一的「怕反压」场景。
/* 设备树 pinctrl 示例:NAND 专用引脚组(SoC 相关,此处以通用写法示意) */
&pinctrl_nand {
nand_ctrl_default: nand_ctrl {
/* 数据线:复用为 NAND 数据总线,不是普通 GPIO */
pinmux_ /* 以实际 SoC 的 PINMUX 宏为准 */;
};
};
/* IO 电压域设置(示意,寄存器名因 SoC 而异) */
/* 将 NAND 所在 Bank 设为 3.3 V 或 3.3 V_HDR */
3.3 R/B# 上拉与就绪判定:最容易翻车的一根线
R/B# 是整条并口接口里信息量最大、也最容易出问题的一根线。 它把「器件内部忙不忙」这件事直接给出来,让主机可以被动等待而不是猜时间。
典型取 10 kΩ。取值权衡:太大则总线上升沿慢、与长走线的高频噪声耦合; 太小则低电平时下拉电流大、器件功耗上升。 在 20 cm 以内、走线阻抗可控 的板子上 4.7 kΩ 到 20 kΩ 都能工作, 建议按「先 10 kΩ 实测上升沿波形,再按需调整」的方式定值。
| 现象 | 可能根因 | 验证方法 |
|---|---|---|
| 卡在等待 R/B# 直到超时 | R/B# 悬空或上拉缺失;或引脚配成了输出 | 示波器量 R/B# 空闲电平应为高;读 sysfs 的 GPIO 方向 |
| 等待立刻通过(读到高但器件其实忙) | R/B# 接反到 WP#;或引脚号写错 | 量 WP# 与 R/B# 是否接反 |
| 读数据全 0xFF 但 ID 正常 | WE# 与 RE# 逻辑反了;或数据位序与宽度错 | 示波器看 RE# 是否有脉冲;试改 nand-bus-width |
| 偶发读出错数据,温度升高更频繁 | 固定延时不够,或等待超时值太小 | 把 max_wait_ready 放大到 datasheet 最大值的 2 倍观察 |
3.4 布线与信号完整性要点
- 数据线与控制线分开走:I/O0~7 与 CLE/ALE/RE#/WE# 分层或分开走线, 避免 RE# 串扰到数据线上(表现为随机位翻转,ECC 报可纠正错误);
- 等长要求:x8 情况下数据线之间建议不超过 5 mm 长度差, 这是 x8 与 x16 唯一真正需要匹配的信号组;
- R/B# 单独走线:这根线上的任何毛刺都会让主机误判已完成;
- 去耦电容:器件 VCC 引脚正下方放 1 只 100 nF 加 1 只 1 uF; 并口 NAND 阵列操作电流跳变大,供电不足表现为随机 ECC 错误;
- CE# 与 WP# 上拉:虽然常态由 SoC 驱动为确定电平, 建议各加 10 kΩ 上拉,保证 SoC 复位与高阻期间器件仍处于安全态;
- 避免测试点与长支线:在 RE# 与 WE# 上加测试点要评估支线电感, 50 mm 支线在 40 MHz 以上的时序下可能接近四分之一波长;
SPI NAND 用时钟线做时序参考,走线要求相对宽松。 并口 NAND 是异步接口,tRP 与 tWP 靠总线周期保证,走线延迟直接吃掉时序余量。 在 100 MHz 以上的 SoC 总线上,10 cm 走线(约 7 ps/mm,单向约 0.7 ns, round trip 约 1.4 ns)相对于 50 ns 的 tRP 看似很小, 但多条控制线累积的偏斜会让设备在量产测试中偶发失败。
建议:量产前做全温(-40 ℃ 到 +85 ℃)的读写抽测。 温度是 NAND 时序余量的主要杀手——高温时 tR 与 tPROG 显著变长, 而部分器件的 tRP 与 tWP 要求反而更严。
4 · MTD / rawnand 子系统解剖
这一章是「看懂内核」的部分。 如果你只是照抄设备树就能跑通,可以跳过;但如果你要自研 host 驱动,或者要 debug 到底层, 这章决定了你能走多远。
4.1 三层结构与完整调用链
Linux 的裸 NAND 支持可以理解成三个互不重叠的抽象:
- mtd_info:对上层(VFS、UBI、mtdblock)暴露的统一接口。 提供 read、write、erase、point、lock、unlock、block_isbad 七个语义。 上层只认这个结构,不关心底下是 NAND 还是 NOR。
- nand_chip:描述「这是一颗什么样的 NAND」。
包含几何(pagesize、oobsize、erasesize、numblocks)、
特性位(options)、函数指针(ecc、badblock、dev_ready)、
以及最关键的
ops->exec_op指令执行器。 - nand_controller:描述「怎么驱动这颗 NAND」。
由 host 驱动提供
exec_op()和attach_chip()两个回调。 内核 6.5 之后 rawnand 全面转向 exec_op 模型, 老式的cmd_ctrl()、read_buf()、write_buf()已被移除。
老驱动方式是「填函数指针」:read_page() 读主区、
read_oob() 读 OOB、write_page() 写。
每个函数内部自己去发命令、自己管时序。问题是同一件「发 5 个字节」的事要在十几个函数里重复写。
exec_op 模型改成「填指令序列」:核心构造一个 struct nand_operation,
里面按顺序装好 CMD_INSTR、ADDR_INSTR、DATA_INSTR、WAITRDY_INSTR,
交给 exec_op() 一次发完。
好处是命令顺序、地址周期数、等待时机全部由核心统一管理,
host 驱动只负责「把字节放到总线上」这一件事。
代价:host 驱动必须实现 exec_op() 里的指令分发 switch,
且不能自行改变顺序。新写驱动必须用这个模型。
/* 核心构造的指令序列(简化示意,主机侧看到的形态) */
struct nand_operation op = {
.type = NAND_OP_PAGE_READ, /* 高层语义,核心负责展开 */
.instrs = {
{ .type = NAND_OP_CMD_INSTR, .ctx.cmd.opcode = NAND_CMD_READ0 }, /* 00h */
{ .type = NAND_OP_ADDR_INSTR, .ctx.addr.naddrs = 5,
.ctx.addr.addrs = { col_lo, col_hi, row0, row1, row2 } },
{ .type = NAND_OP_CMD_INSTR, .ctx.cmd.opcode = NAND_CMD_READ30 }, /* 30h */
{ .type = NAND_OP_WAITRDY_INSTR, .ctx.waitrdy.timeout_ms = 100 },
{ .type = NAND_OP_DATA_INSTR, .len = 2048, .buf = buf },
{ .type = NAND_OP_CMD_INSTR, .ctx.cmd.opcode = NAND_CMD_READ30 }, /* 随机读换页 */
},
.ecc = &ecc_op, /* 让核心决定在哪一步插 ECC 引擎操作 */
};
this->ops->exec_op(this, &op, check_only);
| 指令类型 | 含义 | host 驱动要做什么 |
|---|---|---|
| NAND_OP_CMD_INSTR | 在总线上放一个命令字节 | 拉高 CLE,写字节,拉低 CLE |
| NAND_OP_ADDR_INSTR | 放 naddrs 个地址字节 | 拉高 ALE,逐字节写入,拉低 ALE |
| NAND_OP_DATA_INSTR | 在总线上放或收 len 个数据字节 | 交 DMA 或 memcpy;数据方向由核心在别处指明 |
| NAND_OP_WAITRDY_INSTR | 等待器件就绪 | 轮询 R/B#,或轮询 70h 状态 |
| NAND_OP_DELAY_INSTR | 插入固定延时 | ndelay();只在时序确实不够时用 |
4.2 nand_scan 探测时序逐拍
nand_scan() 是核心的探测入口,host 驱动在 probe 里调用它。
它做的事按顺序是:复位、读 ID、查 idt 表、装载能力表、
(可选)读 ONFI 参数页交叉校验、重新复位、填超时值、扫描坏块、建立 mtd_info、解析分区。
驱动在这个过程里唯一必须提供的是:
- 一个
struct nand_controller,含exec_op回调; - 把几何的初始猜测写进
chip->parameters(可选, 有 idt 表命中时会被覆盖); - 如果器件不在 idt 表里,
attach_chip()回调里手工填能力表。
这两款器件是传统并行 NAND(非 ONFI 参数页路线),大概率不在主线的
nand_ids.c 里。三个选项,按推荐度排序:
- 选项 1(推荐):在 host 驱动的
attach_chip()里手工填能力表。 几何、时序、OOB 大小按 datasheet 硬编码。不污染主线代码。 - 选项 2:往主线 nand_ids.c 提交补丁,加一条器件记录。 好处是全生态受益(mtd-utils、mtools 都能识别),坏处是要等合入或长期维护 out-of-tree。
- 选项 3:用设备树覆盖几何字段。
部分内核版本支持通过设备树
nand,<xxx>覆盖几何, 但字段覆盖能力在各版本间不一致,不推荐依赖。
/* 在自研或修改后的 host 驱动里,为 XTX XT27G02A 装载能力表(示意) */
static int my_nand_attach_chip(struct nand_chip *chip)
{
/* 先让核心走默认流程,若有匹配则直接返回 */
int ret = nand_attach_chip(chip);
if (ret == 0)
return 0;
/* 手工装载:XT27G02A —— 2048+128,64 页每块,128 KiB 每块,5 地址周期 */
chip->mtd._point.phys_erase_size = 128 * 1024; /* 块大小 128 KiB */
chip->mtd._point.phys_block_size = 128 * 1024;
chip->mtd._point.phys_page_size = 2176; /* 2048 Main + 128 Spare */
chip->mtd.oobsize = 128; /* Spare / OOB 大小 */
chip->mtd.writesize = 2048; /* 页数据区大小 */
chip->mtd.subpage_size = 2048; /* 2048 需整页写 */
chip->mtd.eraseblock_size = 64; /* 每块页数 */
chip->mtd.nr_banks = 1;
chip->chip_delay = 20; /* tR,宿主要求的额外等待 */
chip->ecc.algo = NAND_ECC_ALGO_BCH; /* 主机侧 ECC */
chip->ecc.step_size = 544; /* 8 bit 每 544 B */
chip->ecc.strength = 8;
chip->ecc.engine_type = NAND_ECC_ENGINE_TYPE_SOFT;
chip->onfi_chip = 0; /* 非 ONFI:不读 ECh */
return 0;
}
phys_page_size 是 Main 加 Spare 之和(XT27G02A 等于 2176),
而 writesize 与 oobsize 之和必须等于它(2048 加 128 等于 2176)。
这两个数填得不一致,核心的地址计算会整体错位,症状是「读到的是相邻页」。
onfi_chip = 0 必须显式设上。若误设为 1,
核心会向 XT27G02A 发送禁用 的 ECh 命令,
器件响应无定义,可能进入未定义状态并导致后续所有操作异常。
4.3 关键结构体与移植接口
| 结构体 / 字段 | 作用 | 移植时是否要动 |
|---|---|---|
| struct nand_chip | 器件描述:几何、特性位、函数指针 | 要填几何与能力 |
| chip->ops->exec_op | 指令执行器,必填 | 必填 |
| chip->ops->attach_chip | 能力表装载,可选 | 器件不在 idt 表时必填 |
| chip->ecc.algo / step_size / strength | ECC 算法、步长、强度 | 要填 |
| chip->ecc.engine_type | 软件或硬件 ECC 引擎 | 要填 |
| chip->options | 特性位(ECC_REQUIRED、NO_ECC_BCH 等) | 按器件能力设置 |
| chip->bbt_td / bbt_md | 坏块表描述符(自定义位置时用) | 第 8 章详述 |
| chip->badblock_pattern | 坏块标记判据 | XTX 需自定义 |
| chip->ecc_pattern | ECC 在 OOB 中的分布 | 按 datasheet 的 OOB 布局 |
| chip->legacy.chip_delay | 额外等待(us),叠在 ONFI 表之上 | 可留 0 |
| chip->onfi_chip | 是否 ONFI 器件(决定是否读 ECh) | XTX 设为 0 |
| struct nand_controller | host 驱动与核心的接口 | 必填 |
/* 一份最小可用的 nand_controller(示意) */
static const struct nand_controller_ops my_ops = {
/* 把一条 nand_operation 落到总线上。这是唯一必须实现的回调。 */
.exec_op = my_exec_op,
/* 可选:核心探测后调用,让你修正几何与能力表 */
.attach_chip = my_attach_chip,
};
static struct nand_controller controller = {
.ops = &my_ops,
};
/* 之后:nand_to_mtd(&controller) 得到 mtd,再 nand_scan(mtd, 1) */
4.4 源码位置速查
| 文件(内核 6.x 路径) | 内容 | 移植时的关注点 |
|---|---|---|
| drivers/mtd/nand/raw/nand_base.c | 几何管理、读写擦流程编排、nand_scan | 看 nand_scan 的执行顺序;看 nand_read_page 怎么展开成 op |
| drivers/mtd/nand/raw/nand_ecc.c | Hamming / BCH / RS 软引擎 | 看 BCH calculate 与 correct 的调用时机 |
| drivers/mtd/nand/raw/nand_timings.c | ONFI 时序表与检查 | 看你的控制器在哪些项上不达标会告警 |
| drivers/mtd/nand/raw/nand_ids.c | 器件 ID 表 | 看是否已有你的料号;看同类器件的能力表写法 |
| drivers/mtd/nand/raw/nand_bbt.c | 坏块表扫描与更新 | 看 badblock_pattern 的语义(offs、len、pattern) |
| drivers/mtd/nand/raw/gpio.c | GPIO 模拟 host 驱动(路线 B) | 路线 B 的全部实现,约 400 行,是最好的参考 |
| drivers/mtd/nand/raw/sunxi_nand.c | 全志 NAND host 驱动 | 硬件控制器 host 驱动的完整范例,含 DMA 与硬件 ECC |
| drivers/mtd/nand/raw/mxc_nand.c | Freescale i.MX NAND host 驱动 | 另一套硬件实现,含硬件 ECC 与 BBM 处理 |
| include/linux/mtd/rawnand.h | 核心数据结构定义 | struct nand_chip / nand_operation / nand_controller_ops 全在这里 |
| Documentation/devicetree/bindings/mtd/ | 设备树绑定 | gpio-control-nand.yaml、raw-nand-chip.yaml、nand-chip.yaml |
不要从 mtdcore.c 开始读(它与 NAND 无关)。推荐顺序:
- 先读
include/linux/mtd/rawnand.h里的struct nand_chip与struct nand_operation,建立数据结构的心智模型; - 读
nand_base.c的nand_scan(),看它按什么顺序调谁; - 读
nand_base.c的nand_read_page(),看它怎么把高层语义展开成 op; - 读
raw/gpio.c的gpio_nand_exec_op(),看最小实现长什么样; - 需要时再读
sunxi_nand.c学硬件控制器的 DMA 与 ECC 集成。