Linux 并口 NAND(PPI NAND)驱动开发指南
编制日期 2026-10-28
MTD / rawnand 路线全解 · 控制器驱动三条路线

若走 gpio-control-nand 路线
PPI NAND 在 Linux 里的三条归宿:主线能覆盖前两条,第三条要你自己写SoC 侧引脚资源I/O0~7 / CLE / ALE / CE# / WE# / RE# / R-B# / WP#并口 NAND 器件(裸片)NAND 阵列 + 片内控制器(无 FTL/ECC)路线 A:SoC 原生 NFC 控制器首选 · 性能与可靠性最好SoC 内部NFC 硬件厂商或主线host 驱动nand_chipexec_op/dev/mtd0 + /dev/mtdblock0DMA 搬运 + 硬件 ECC 引擎读 20~40 MB/s,写 8~20 MB/s典型:sunxi-nand / mxc_nand / fsl_nand工作量:写设备树 + 调试 pinmux/时序路线 B:gpio-control-nand主线自带 · 无需写代码SoC 内存映射总线窗口GPIO 翻转CLE/ALE/CE#软件 ECC或 hwecc/dev/mtd0内核配置:CONFIG_MTD_NAND_GPIO文件:drivers/mtd/nand/raw/gpio.c读 1~3 MB/s,写 0.5~1.5 MB/s工作量:0 行 C,仅设备树路线 C:自研 nand_controller仅当前两者都走不通时自研 host驱动自研 ECC或外挂引擎自研 BBM策略/dev/mtd0 或自定义节点DMA、超时、并发、掉电全自己写需长期跟随内核 rawnand API 演进适用:总线时序需非标调整 / 特殊 LUN 拓扑工作量:600~2000 行 C 代码关键事实:PPI NAND 对内核呈现为 /dev/mtdN 字符设备或 mtdblock 块设备,不存在 /dev/mtdN 之外的标准块设备节点
图 1 · PPI NAND 在 Linux 里的三条归宿
同系列四类 Flash 对内核暴露的东西完全不同,驱动不能互相套用SPI NOR块设备/dev/mtdblock0mtd_blktrans· 主机算 CRC 或用控制器 ECC· 无坏块· 无 OOB 概念SPI NANDMTD 原始设备/dev/mtd0/dev/mtdblock0· 主机 ECC(软件或硬件)· 有坏块片内标记· 有 OOB64~224 BPPI NANDMTD 原始设备/dev/mtd0/dev/mtdblock0· 主机 ECC(软件或硬件)· 有坏块位置不统一· 有 OOB16~256 BSD NANDMMC 块设备/dev/mmcblk0· 片内 FTL 已做· 片内管理· 对主机不可见三者共同点:都是 /dev/mtdN;内核 rawnand 核心代码完全共用差别集中在 host 驱动(怎么搬数据、怎么算 ECC)与器件几何表(idt / bbt / eccpos)最容易犯的错:把 PPI NAND 当 SPI NAND 处理两者命令编码不同(并口是 80h/10h 两段式,SPI 是 02h→10h 三段式),几何也不同,套用必然读出乱码
图 2 · 同系列四类 Flash 的内核接口面对照
这份文档的核心主张:并口 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 失败」时怎么定位。
读者路径
路线 A · 首次移植
从硬件到挂载一条路走完
按第 3 章查硬件 → 第 5 章定路线 → 第 6 章写设备树 → 第 7/8 章配 ECC 与坏块 → 第 9 章走完落地流程 → 第 10 章做分区与格式化。
预计阅读 2 小时
路线 B · 出问题了在排查
直接跳到第 14 章
按现象查速查表,每条给出根因与验证命令,不做推导。
预计阅读 15 分钟
路线 C · 要做自研
重点读第 5.3 与第 4 章
先看 nand_controller 接口要实现哪几个回调、核心在调用它们时保证了什么,再评估工作量是否可接受。
预计阅读 1 小时
路线 D · 与本系列其它文档配套
硬件与软件规范已经在别处
器件几何、OOB 布局、坏块标记、ECC 分级策略以《并行NAND_PPI_NAND 软件设计规范》为准,本文只讲 Linux 侧如何落地上层软件。
预计阅读 40 分钟
配套文档与本文的分工
本文是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), 代价是带宽低,换来的是布线简单、能跑高时钟。
它不是一颗「带外壳的芯片」,而是一片必须自己伺候的存储裸片裸 die晶圆切割后未封装只有芯片厂内部流转,你拿不到TSOP / BGA带引脚或焊球的封装你能买到,但引脚数 48~200 不等SON / CSP小尺寸贴片同样可买,尺寸更小对比:SPI NAND 长什么样并口 NAND:8 根数据线 + 6 根控制线= 至少 20 根信号,占引脚与布线资源SPI NAND:4 线(SCLK/MOSI/MISO/CS)= 省引脚、省布线,但协议更复杂在电路板上,它长这样主控 SoC含 NFC 或无 NFCI/O0~I/O7(8 根)CLE / ALE / CE# / WE# / RE# / R-B#PPI NAND 器件2~8 Gb,x8 或 x16物理形态带来的三个直接后果它没有 FTL,坏块不会自动屏蔽------主机必须自己扫、自己记、自己避开(第 8 章)它没有 ECC 纠错------数据完整性完全由主机侧的 OOB 校验负责(第 7 章)它没有磨损均衡------同一块被反复写就会提前失效,这就是 UBI 存在的理由(第 10 章)一句话:并口 NAND 的一切麻烦,都源于它把「可靠性」的责任从片内搬到了主机。
图 3 · PPI NAND 的物理形态与它在板上的位置
| 维度 | 并口 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,得到一个真正的文件系统。 这五层的责任划分,是理解后面所有章节的基础。
每一层「以为自己不用管可靠性」的地方,都是别人在替你管应用 / 文件系统open / read / write谁在管:文件系统的日志与一致性UBI 磨损均衡层把逻辑块映射到物理 PEB谁在管:动态坏块替换、擦写均衡MTD 抽象层mtdblock / mtd 字符设备谁在管:ECC 编排、静态坏块表控制器 host 驱动exec_op / DMA / 超时谁在管:时序、状态机、错误恢复NAND 器件(裸片)阵列 + 片内控制器谁在管:物理磨损、随机缺陷没有任何一层兜住的情况掉电:写操作横跨「擦除 → 编程 → 更新索引」三步,断电可能落在任意一步之间。对策:日志文件系统(UBIFS)+ 掉电通知(PM / PFO)本系列文档负责的范围第 3~6 章:第 2 层以上的接线(硬件前提、host 驱动、设备树)第 7~8 章:第 3 层的正确配置(ECC、坏块)第 10 章:第 2 层的启用(UBI / UBIFS)
图 4 · 从应用代码到 NAND 晶粒的完整栈与责任划分
「它不是块设备」带来的三个具体后果
- 不能直接 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。
读完第 1 章你应该能回答
- 为什么我的并口 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> |
x16 不是 x8 的简单升级
把 x8 器件按 x16 读,最典型的现象是能探测到器件、能读出 ID,但读数据全是 0xFF 或 0x00。 反过来把 x16 按 x8 读,地址周期数不对,会读到相邻页或直接触发未定义行为。
如果 SoC 的 NFC 控制器不支持 x16 而器件是 x16 位宽, 不要试图用软件位拆来凑------地址周期与时序都会错,正确做法是换器件型号。
x8 并口 NAND 的引脚分组:只有 CLE/ALE/CE# 三根控制线决定当前总线上的字节是命令、地址还是数据并口 NAND 器件(x8)内部:NAND 阵列 + 内部控制器 + 地址计数器命令锁存器(CLE 有效时)地址锁存器(ALE 有效时)数据寄存器(两者都无效时)页缓存寄存器(Page Buffer)读命令后先整页搬进这里,主机再从 RE# 取数据片内 ECC 引擎(部分型号有)坏块标记区(位置依型号而异)CLE命令锁存使能,高有效ALE地址锁存使能,高有效CE#片选,低有效I/O0 ~ I/O78 根双向数据线上电必须配成 GPIO或总线复用,不能浮空WE#写使能,低有效RE#读使能低有效每根脉冲吐 1 字节WP#写保护低有效,擦除期间必须拉高R/B#(Ready / Busy)开漏输出,必须外接上拉忙时拉低、完成后释放为高内核靠它判断完成,不接只能软件轮询主机侧实现方式(决定你走哪条路线)路线 A:SoC 的 NFC/EBI/FSMC 硬件自动产生 CLE/ALE/WE#/RE# 时序,主机只写命令字节到寄存器路线 B:数据线挂到 SoC 内存映射窗口(读写即产生 WE#/RE#),CLE/ALE/CE#/R-B#/WP# 用 GPIO 手动翻转
图 5 · x8 并口 NAND 的引脚分组与信号方向
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 器件才需要 |
80h/10h 两段式不是 SPI NAND 的 02h/10h 三段式
并口 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 异步 Read Page(00h ... 30h)逐拍时序:注意 30h 之前必须完成 5 个地址周期CE#CLEALEI/O0-7R/B#低1高仅在 00h 这一拍11Col0 Col1 | Pa0 Pa1 Pa2(x8 / 2Gb / 2048+64)00h地址 5 字节30h数据输出:Main 区 2048 B(按 RE# 每字节一拍)内部搬页期间拉低,完成后释放为高(tR,typ 25 µs)① 命令② 地址③ 确认④ 等待地址周期数必须由器件几何决定:x8 + 2048 B Main 只需 2 个列地址,2048 B 需要 3 个 ------ 用错周期数会读到错误页甚至报错对应到内核代码00h + 地址 + 30h 这一整串,在 rawnand 核心里是一个struct nand_operation,由 chip->ops->exec_op() 发出。驱动写的是「指令序列」,不是「发几个字节」。顺序由核心决定,驱动不能改 ------ 这是与老式cmd_ctrl() 式驱动的根本区别。常见错误· 把 30h 写成 31h(那是 Read Cache)· 地址周期按 x16 算成 2 个列地址· 等待 R/B# 用固定 usleep 而不是等引脚· 读 OOB 时按 Main 同址算,实际是列地址 +OOB 偏移
图 6 · ONFI 异步 Read Page 命令周期逐拍时序
上表数值为 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 | 同左 |
XT27G02A/B 的三条强制特例
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 · 硬件前提核查
这一章全是「动手前要确认的事」。 每一条都对应一类在软件层无法补救的问题------硬件层的问题在软件里怎么绕都绕不过去,只能返板。
上电前先把三件事问清楚:答案决定你走哪条路线,猜错就是几周的返工问题 1:SoC 有没有硬件 NAND 控制器(NFC / EBI / FSMC / LPC)?查 SoC 手册章节标题:搜 NAND、EBI、FSMC、LCDC(带 NAND 接口的)、SmartLPC 等关键词;有寄存器手册且含 CLE/ALE 写入口的即为硬件控制器。有没有路线 A:找该 SoC 的 NAND host 驱动主线已有:sunxi-nand、mxc_nand、fsl_nand、at91nand、stm32nand、davinci-nand、omap-nand、marvell-nand、intel-lpss、bcm-nand厂商 BSP 里若有 NAND 驱动,优先用厂商版(时序与该芯片最匹配)路线 B:gpio-control-nand(主线自带)前提:数据线能挂到 SoC 的内存映射窗口(读写即产生 WE#/RE#),且 GPIO 能当通用输出用。数据总线宽度受窗口大小限制。仅适合小容量(通常 512 Mb 以内)与对速度不敏感的场合。问题 2:R/B#(Ready/Busy)引脚接到了 SoC 的哪个可读引脚?必须是一根能读回电平的 GPIO 或 GPIO 控制器自身的状态寄存器。接了 PC 机的 LPT 并口或悬空,就只能退化为主机轮询 70h 读状态,速度慢且易误判。问题 3:SoC 有没有可用的 ECC 引擎?位宽与步长能否覆盖器件要求?没有则用内核软件 BCH 引擎(CONFIG_MTD_ECC_BCH),CPU 开销约 30%~50%,但正确性有保障;有则走硬件 ECC,务必核对 step_size 与 strength 与 datasheet 一致。三条问题组合出的落地结论硬件 NFCR/B#ECC 引擎落地结论硬件 NFC 有R/B# 已接有 ECC 引擎路线 A + 硬件 ECC,量产首选硬件 NFC 有R/B# 已接无 ECC 引擎路线 A + 软件 BCH,可靠性优先时首选硬件 NFC 无R/B# 已接任意路线 B gpio-control-nand,仅小容量与启动用途硬件 NFC 无R/B# 未接任意先改硬件;确实无法改才评估路线 C
图 7 · 上电前的三问:硬件前提核查流程
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# 是整条并口接口里信息量最大、也最容易出问题的一根线。 它把「器件内部忙不忙」这件事直接给出来,让主机可以被动等待而不是猜时间。
R/B# 的三种接法:只有接了可读引脚的那种才是推荐做法接法 A:接到 SoC 可读 GPIO(推荐)· 主机直接读引脚电平判断忙闲· 等待精确到微秒级,不浪费 CPU 轮询· 内核 dev_ready 回调直接用这个 GPIOwhile (gpiod_get_value(rdy) == 0) { /* 忙 */ }接法 B:接不到 GPIO,主机轮询 70h· 每次发 70h 命令、读状态寄存器判断 OIP 位· 每次查询要占用一次总线往返· 轮询间隔不当会漏掉刚好完成的窗口,或超时误判每 50 us 发一次 70h,读到 OIP=0 才算完成接法 C:完全靠固定延时· 按 datasheet 最大值 usleep(tR_max)· 温度升高时器件变慢,固定延时会读到旧数据· 全片并发时所有等待都按最大值算,性能被最坏情况拖死危险:可能读到上一条命令的残留数据,ECC 也救不了R/B# 是开漏输出,板上必须外接上拉(典型 10 kΩ 到该 Bank 的 IO 电压);上拉电压必须与 VCC 同源,不能高于 VCC 太多。
图 8 · 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 以上的时序下可能接近四分之一波长;
并口 NAND 与 SPI NAND 的 SI 要求差一个量级
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()已被移除。
exec_op 模型的意义
老驱动方式是「填函数指针」: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();只在时序确实不够时用 |
从 open() 到 NAND 阵列:一次页读的完整调用链,以及每层的责任用户态VFS + MTD 核心mtd_info 抽象层nand_chip 核心层host 驱动层硬件分层的意义:上三层(MTD / UBI / 文件系统)与器件无关,所有 SoC 共用;你只需要保证最下两层正确。
图 9 · MTD / rawnand 分层结构与完整调用链
4.2 nand_scan 探测时序逐拍
nand_scan() 是核心的探测入口,host 驱动在 probe 里调用它。 它做的事按顺序是:复位、读 ID、查 idt 表、装载能力表、 (可选)读 ONFI 参数页交叉校验、重新复位、填超时值、扫描坏块、建立 mtd_info、解析分区。
驱动在这个过程里唯一必须提供的是:
- 一个
struct nand_controller,含exec_op回调; - 把几何的初始猜测 写进
chip->parameters(可选, 有 idt 表命中时会被覆盖); - 如果器件不在 idt 表里,
attach_chip()回调里手工填能力表。
XT27G02A 与 XT27G02B 不在主线 idt 表里怎么办
这两款器件是传统并行 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;}
attach_chip 里的四个字段必须一次填对
phys_page_size 是 Main 加 Spare 之和(XT27G02A 等于 2176), 而 writesize 与 oobsize 之和必须等于它(2048 加 128 等于 2176)。 这两个数填得不一致,核心的地址计算会整体错位,症状是「读到的是相邻页」。
onfi_chip = 0 必须显式设上。若误设为 1, 核心会向 XT27G02A 发送禁用 的 ECh 命令, 器件响应无定义,可能进入未定义状态并导致后续所有操作异常。
从上电到 /dev/mtd0 出现:核心做了这些步骤,驱动只需填能力表01复位器件发送 FFh(Reset),等待 tRST(typ 100~250 us)后读 70h 确认 OIP 清零02读 ID90h 加 00h,拿到厂商 ID 与器件 ID,在 idt 表里匹配03装载能力表从匹配到的芯片模板复制几何、时序、OOB 布局、特性位04ONFI 参数页(可选)ECh 读 256 B 参数页,交叉校验几何;非 ONFI 器件跳过05重新复位参数页会改变器件状态,必须再 FFh 一次06设置就绪超时根据参数页的 tR / tPROG / tBERS 填 max_wait_ready07扫描坏块按 badblock_pattern 逐块读标记,建立 RAM 坏块表08建立 MTD 设备填充 mtd_info 的几何,注册 /dev/mtdN09解析分区设备树 fixed-partitions 或内核命令行 mtdparts=10注册字符/块设备mtd_device_register 注册字符设备,mtdblock 视配置注册块设备注意:驱动代码里写的是「能力表」和「指令序列」,不是「发几个字节」------这是 rawnand 框架与老式驱动的根本区别
图 10 · nand_scan 的探测与初始化流程
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 集成。
5 · 控制器驱动:三条路线
这一章是全文的技术核心。 「Linux 并口 NAND 驱动」这个题目下,真正要写代码的只有路线 C; 路线 A 和 B 都是「找到对的驱动 + 写对的设备树」。
5.1 路线 A:SoC 原生 NFC 控制器
「NFC」这个名字在不同厂商手册里出现很多变体:NAND Flash Controller、 Embedded Memory Controller(EMC)、Flexible Memory Controller(FMC)、 Local Bus Controller(LBC)、EBI。在 Linux 里对应的驱动文件是:
| SoC 系列 | 内核驱动文件 | CONFIG 开关 | 备注 |
|---|---|---|---|
| 全志 Allwinner | drivers/mtd/nand/raw/sunxi_nand.c | CONFIG_MTD_SUNXI_NAND | A20/A40/H3 等,社区活跃 |
| Freescale i.MX | drivers/mtd/nand/raw/mxc_nand.c | CONFIG_MTD_RAW_NAND_MXC | i.MX6/i.MX8,硬件 ECC 完善 |
| Freescale 高端 | drivers/mtd/nand/raw/fsl_nand.c | CONFIG_MTD_NAND_FSL | P2020/P3041 等 |
| TI AM335x / OMAP | drivers/mtd/nand/raw/omap_nand.c | CONFIG_MTD_OMAP_NAND | 含 GPMC 版本 |
| TI DaVinci | drivers/mtd/nand/raw/davinci-nand.c | CONFIG_MTD_DAVINCI_NAND | EMIF 接口 |
| ST STM32 | drivers/mtd/nand/raw/stm32nand.c | CONFIG_MTD_RAW_NAND_STM32 | F7/H7,带 FMC |
| Atmel AT91 | drivers/mtd/nand/raw/at91nand.c | CONFIG_MTD_NAND_AT91 | SMC 控制器 |
| Marvell Kirkwood | drivers/mtd/nand/raw/mv_nand.c | CONFIG_MTD_NAND_MV | 或已并入 spi-nand 路径 |
| Broadcom | drivers/mtd/nand/raw/bcm_nand.c | CONFIG_MTD_BCM_NAND | BCM2835 等 |
| NXP LPC | drivers/mtd/nand/raw/lpc32xx_nand.c | CONFIG_MTD_NAND_LPC32XX | 与 gpio-nand 同一文件演进 |
优先用厂商 BSP 里的驱动,而不是主线的
主线驱动面向「一堆 SoC 的公共最小集」,时序往往只跑到规范允许的中等档位; 厂商 BSP 里的驱动会针对自家芯片打开更多特性(更大的页、 更激进的多 die 交织、厂商私有 ECC 增强)。
但在用厂商驱动前先确认两件事:它是否已迁移到 exec_op 模型 (新内核上老驱动会直接编译不过),以及它与主线 UBI 的兼容性。
路线 A 的寄存器级调试通常按这个顺序走:
- 确认 pinmux 与时钟:NAND 控制器时钟源是否打开、pinmux 是否切到 NAND 功能(不是 GPIO)。这一步失败的表现是「寄存器读写无响应」。
- 确认总线时序寄存器:把 tCLS、tALS、tWP、tWH、tRP、tRC 全部设为最宽松值(通常是规范最小值的 2~4 倍),先保证能通。
- 确认 ECC 引擎配置:ECC step size 与 strength 必须与 datasheet 一致。不一致时现象很隐蔽------能读写,但偶发数据错,且错误率随数据量上升。
- 小范围验证:先只读第 0 页与第 1 页(nftl 或 nanddump 读单页),确认地址周期数正确;再读最后一页,确认容量算对。
- 全盘 nandtest:擦除+写入+读回校验全盘。这一步能暴露 99% 的几何与时序类问题。
5.2 路线 B:gpio-control-nand(主线 GPIO 模拟)
这是主线自带的 GPIO 模拟 NAND 控制器,配置项是 CONFIG_MTD_NAND_GPIO,实现文件是 drivers/mtd/nand/raw/gpio.c,约 400 行。 它的存在意义是:让没有 NAND 控制器的低成本 SoC 也能挂上裸 NAND, 典型场景是启动盘、配置存储区、以及几百 Mb 以内的小容量场景。
host 驱动的 exec_op 就是一张指令类型分发表------这是路线 B 的全部实现核心调用:chip->ops->exec_op(chip, &op, check_only)op 里按顺序装好了一串指令;驱动逐条执行,不需要理解高层语义NAND_OP_CMD_INSTR在总线上写 1 个命令字节1. dosync(防总线重排序)2. gpiod_set_value(cle, 1)3. writeb(opcode, io)4. gpiod_set_value(cle, 0)NAND_OP_ADDR_INSTR写 naddrs 个地址字节1. gpiod_set_value(ale, 1)2. 循环 writeb(addrsi, io)3. gpiod_set_value(ale, 0)NAND_OP_DATA_INSTR在总线上收或发 len 字节1. 若 DATA_IN:memcpy_fromio()2. 若 DATA_OUT:memcpy_toio()3. 或交 DMA(硬件控制器路线)NAND_OP_WAITRDY_INSTR等待器件就绪1. 有 rdy GPIO:gpio_nand_waitrdy()2. 无 rdy GPIO:nand_soft_waitrdy()3. 内部循环 + 超时判定路线 B 相对路线 A 唯一的能力缺失:没有 DMA数据搬移只能 memcpy,CPU 逐字节参与。这是它速度只有 1~3 MB/s 的根本原因,不是内核抽象的开销。硬件控制器路线里这一步是 register_to_mem / mem_to_register 的 DMA 描述符。
图 11 · exec_op 的指令分发:host 驱动的全部职责
它的实现原理很直白:
- 数据线接到 SoC 的一个内存映射窗口。对这个窗口做
writeb(), 总线控制器自动产生 WE# 脉冲;对它做readb(),自动产生 RE# 脉冲。 这就是「不需要自己翻转 WE#/RE#」的原因; - CLE、ALE、CE#、WP#、R/B# 用 GPIO:发命令前拉高 CLE,写完拉低; 发地址前拉高 ALE,写完拉低;
- 每条指令前后调
gpio_nand_dosync(),用一个额外寄存器读操作做屏障, 防止总线重排序导致 GPIO 状态与总线写顺序颠倒(某些 ARM SoC 上这是真实存在的坑); check_only参数为真时只遍历指令不实际发出------ 这是核心用来测试控制器是否支持某个 op 的机制。
路线 B 的四条硬限制
**1. 单片。**该驱动没有多 die 支持,因为没有片选控制(ce-gpios 可选但只用于切换单片)。
**2. 容量受窗口与速度限制。**它本质是 bit-banging,每字节一次总线访问, 实测顺序读 1~3 MB/s。2 Gb(256 MiB)全盘 nandtest 需要数小时,量产测试要评估工时。
**3. 无 DMA。**数据搬移是 memcpy,CPU 全程参与。
**4. 窗口大小限制。**reg 的 size 必须不小于页大小。 某些 SoC 的局部总线窗口配置粒度较粗,需确认可用窗口大小。
路线 B 的设备树写法(可直接使用):
nand-controller@20200000 {
compatible = "gpio-control-nand";
reg = <0x20200000 0x1000>; /* 数据窗口,通常 4 KiB 足够 */
rdy-gpios = <&gpio 12 GPIO_ACTIVE_LOW>; /* R/B#,开漏,需上拉 */
ce-gpios = <&gpio 13 GPIO_ACTIVE_LOW>; /* CE#,可不写 */
ale-gpios = <&gpio 14 GPIO_ACTIVE_HIGH>; /* ALE,必填 */
cle-gpios = <&gpio 15 GPIO_ACTIVE_HIGH>; /* CLE,必填 */
wp-gpios = <&gpio 16 GPIO_ACTIVE_LOW>; /* WP#,可不写 */
nand-bus-width = <8>;
chip-delay = <20>; /* tR 额外等待,单位 us */
/* 软件 BCH ECC:8 bit 每 544 B(XT27G02A 的要求) */
nand-use-soft-ecc-engine;
nand-ecc-algo = "bch";
nand-ecc-step-size = <544>;
nand-ecc-strength = <8>;
partitions { ... };
};
chip-delay 到底加在哪里
chip-delay 是在器件报告忙之后额外增加的等待 , 单位是微秒,作用是覆盖走线延迟与总线周转带来的余量。 它的正确取法:datasheet 的 tR 最大值减去内核已知部分,取一个小的正数。 通常 10~20 us 足够。
不要用它来「等更久以防万一」------设成 100 us 会让每次页读多花 100 us, 2 Gb 器件全盘 nandtest 会多花几十分钟。真正等器件忙的是 WAITRDY 指令,走的是 R/B# 或状态轮询。
5.3 路线 C:自研 nand_controller 驱动
自研是最后选项。启动之前,先确认下面这张清单里的每一条都不成立:
| 自研的理由 | 先问自己 | 更好的替代 |
|---|---|---|
| SoC 没有硬件 NAND 控制器 | 能否换一颗带 NFC 的 SoC?换 SoC 的成本通常低于自研驱动 + 长期维护 | 换 SoC |
| GPIO 模拟速度不够 | 真的需要那么快吗?启动盘与配置区通常 1 MB/s 够用 | 降低需求;或换 SPI NAND |
| 总线时序需要非标调整 | 硬件 NFC 的时序寄存器是否已覆盖你的需求?很多 SoC 的时序粒度很细 | 调寄存器;或找该 SoC 的社区补丁 |
| 需要多 die 交织 | 是否真的需要多 die?容量需求能否用单片大容量解决 | 换更大容量的单片器件 |
| 厂商驱动有 bug | 能否在厂商驱动里打补丁?out-of-tree 补丁比新写一个驱动风险低得多 | 补丁 + 记录 |
| 需要特殊 FTL | FTL 该在 UBI 层做,不该在 host 驱动里做 | 配置 UBI 动态卷 |
如果以上都排除,自研 driver 需要实现的清单如下(按工作量排序):
-
exec_op 指令分发(必做):CMD_INSTR / ADDR_INSTR / DATA_INSTR / WAITRDY_INSTR 四类,其中 DATA_INSTR 可用 DMA 优化;
-
probe / remove 回调(必做):申请 GPIO、ioremap、注册 MTD 设备;
-
attach_chip(器件不在 idt 表时必做):填几何与 ECC 能力表;
-
DMA 通道管理(性能必需):请求 DMA 通道、描述符的缓存一致性处理、超时与中断处理;
-
并发与锁:exec_op 可能被多线程调用(UBI 的 GC 与读并发),控制器寄存器操作必须串行化;
-
超时与恢复:WAITRDY 超时后能否继续用这颗器件、要不要发 FFh 复位、如何标记该块为坏;
-
DMA 失败与信号中断:DMA 错误如何上报、如何避免设备卡在忙状态;
-
runtime PM(功耗要求时):suspend/resume 时保持器件状态或复位;
-
debugfs 接口(强烈建议):把裸页读写能力暴露出来,绕过 MTD 层做几何验证,比在 MTD 层猜问题快得多;
/* 自研 host 驱动的骨架(约 60 行,剩下的是各平台的 DMA 与时序适配) */
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio/consumer.h>
#include <linux/of.h>
#include <linux/mtd/mtd.h>
#include <linux/mtd/rawnand.h>struct mynand {
struct nand_controller controller; /* 必须是第一个成员 */
struct nand_chip chip;
void __iomem io; / 数据窗口 */
struct gpio_desc *rdy, *ce, *ale, *cle, *wp;
struct device *dev;
};/* exec_op:把一条 nand_operation 落到硬件上(最小实现,示意) */
static int my_nand_exec_op(struct nand_chip *chip,
const struct nand_operation *op,
bool check_only)
{
struct mynand *priv = container_of(chip, struct mynand, chip);
unsigned int i;if (check_only) return 0; /* 只做能力查询,不实际发 */ gpiod_set_value(priv->ce, 1); /* 选中器件(低有效) */ for (i = 0; i < op->ninstrs; i++) { const struct nand_op_instr *in = &op->instrs[i]; switch (in->type) { case NAND_OP_CMD_INSTR: gpiod_set_value(priv->cle, 1); writeb(in->ctx.cmd.opcode, priv->io); gpiod_set_value(priv->cle, 0); break; case NAND_OP_ADDR_INSTR: { unsigned int j; gpiod_set_value(priv->ale, 1); for (j = 0; j < in->ctx.addr.naddrs; j++) writeb(in->ctx.addr.addrs[j], priv->io); gpiod_set_value(priv->ale, 0); break; } case NAND_OP_DATA_INSTR: /* 实际工程这里应换成 DMA 的 mem_to_reg */ memcpy_fromio(buf, priv->io, in->len); break; case NAND_OP_WAITRDY_INSTR: wait_for_completion_timeout(&priv->ready, msecs_to_jiffies(in->ctx.waitrdy.timeout_ms)); break; default: dev_err(priv->dev, "unhandled instr type %d\n", in->type); return -EINVAL; } if (in->delay_ns) ndelay(in->delay_ns); } gpiod_set_value(priv->ce, 0); /* 取消选中 */ return 0;}
static const struct nand_controller_ops my_nand_ops = {
.exec_op = my_nand_exec_op,
};static struct nand_controller my_controller = { .ops = &my_nand_ops };
static int my_nand_probe(struct platform_device *pdev)
{
struct mynand *priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
struct device_node *of = pdev->dev.of_node;
struct mtd_info *mtd;
struct resource *res;
int ret;if (!priv) return -ENOMEM; priv->dev = &pdev->dev; res = platform_get_resource(pdev, IORESOURCE_MEM, 0); priv->io = devm_ioremap_resource(&pdev->dev, res); if (IS_ERR(priv->io)) return PTR_ERR(priv->io); /* 四个控制 GPIO:极性由设备树的 GPIO_ACTIVE_LOW / HIGH 决定 */ priv->rdy = devm_gpiod_get(&pdev->dev, "rdy", GPIOD_INPUT); priv->ale = devm_gpiod_get(&pdev->dev, "ale", GPIOD_OUT_HIGH); priv->cle = devm_gpiod_get(&pdev->dev, "cle", GPIOD_OUT_HIGH); if (IS_ERR(priv->rdy) || IS_ERR(priv->ale) || IS_ERR(priv->cle)) return PTR_ERR(priv->rdy); priv->chip.controller = &my_controller; /* 指向 ops */ mtd = nand_to_mtd(&priv->chip); mtd->owner = THIS_MODULE; ret = nand_scan(mtd, 1); if (ret) return ret; ret = nand_register(&priv->chip); /* 注册 mtd 设备并解析分区 */ return ret;}
static const struct of_device_id my_nand_of_match[] = {
{ .compatible = "myvendor,my-nand" },
{}
};
MODULE_DEVICE_TABLE(of, my_nand_of_match);static struct platform_driver my_nand_driver = {
.probe = my_nand_probe,
.driver = { .name = "my-nand", .of_match_table = my_nand_of_match },
};
module_platform_driver(my_nand_driver);
自研驱动的验证顺序
不要一上来就接 UBI。正确顺序是:
- 驱动 probe 成功,dmesg 打印出正确的几何(size / erasesize / oobsize / numblocks);
- 用
nanddump --noecc /dev/mtd0 0 0x100 | xxd | head读第 0 页前 256 字节, 确认是 0xFF(空片状态); - 用
mtdblock或nandtest只测前 4 MB,看基本读写是否通; - 确认
nand_test的 erase/write/read/compare 四个子测试都能过; - 再测全盘;
- 最后才接 UBI、格式化 UBIFS。
每一步都通过后再进下一步。跨级排查是自研驱动调试效率最低的原因。
三条路线在速度、成本、灵活性上的差异(数值为典型量级,实测以自家板子为准)维度路线 A 硬件 NFC路线 B GPIO 模拟路线 C 自研顺序读吞吐20~40 MB/s1~3 MB/s取决于自研实现写吞吐8~20 MB/s0.5~1.5 MB/s取决于自研实现随机小包性能好(DMA 支持)差(每次都要切命令)可优化ECC硬件引擎,开销近零软件 BCH,CPU 占 30~50%自研决定开发工作量设备树加调试 pinmux0 行 C 代码600~2000 行 C调试难度中(寄存器级排查)低(改设备树即可)高(并发与时序与掉电)主线上游状态各 SoC 单独维护主线维护,长期可用自维护,易随内核演进失效适用容量8 Gb 到 2 Tb通常 512 Mb 以内不限技术风险低,厂商已量产验证低,性能上限低高,掉电与并发易出隐蔽 bug选择建议:能走 A 就走 A;B 的定位是启动盘、配置区与小容量非性能敏感场合;C 必须在 A、B 都被证明不可行后才启动。
图 12 · 三条控制器路线的能力与代价对比
5.4 三条路线对比与选型决策
按这张树走,每一步的答案都指向唯一合理的下一步项目要用 PPI NAND 存什么?存 rootfs 或需要频繁随机写?是否是必须 UBI + UBIFS裸 mtd + ext4 必坏。UBI 提供磨损均衡、坏块替换与原子 LEB 写;UBIFS 基于 LEB日志结构,掉电可恢复。结论:优先路线 A。可以 mtdblock 或 JFFS2只存配置与日志、写入量很小、不要求掉电一致性时可用。结论:A 或 B 均可。吞吐优先日志、缓存、数据采集需要持续大吞吐写入。GPIO 模拟会成为整板瓶颈。结论:必须路线 A。SoC 有硬件 NAND 控制器吗?有没有路线 A:用 SoC 的硬件 NFC找主线或厂商 host 驱动(sunxi-nand / mxc_nand /stm32nand 等),按其 binding 写设备树。开发重点:时序寄存器、ECC 引擎配置、pinmux。路线 B:gpio-control-nand确认数据线已挂到可访问的内存窗口;写设备树指定该窗口与四个 GPIO。0 行 C 代码。注意:只能单片、容量通常 512 Mb 以内。路线 C:自研前先自问三个问题B 真的不够用吗?换器件型号 / 换 SoC 是否更省事?
图 13 · 三条路线的选型决策树
把三条路线的决策压缩成一句话:能走 A 就走 A; 如果项目只是存点配置和启动镜像,B 足够; 只有在 A、B 都被证明确实不可行时,才启动 C。
后续章节请下载「芯参谋」查看
......(篇幅原因, 还有12章· 详见芯参谋→方案设计)
打开路径:🔧解决方案&应用市场分析 -> 原理方案设计 -> 嵌入式驱动&系统开发 -> Linux_PPI_NAND_驱动开发指南
相关报告 · 1
- 瑞萨(Renesas)平台_SPI_NOR_eMMC_样机排查调试&解决方案
- 运动相机(Action Camera)_市场分析市场应用分析
- 座舱域控制器(Cockpit DC)_市场分析市场应用分析
- SPI_NOR_首次启动失败_样机排查调试&解决方案
- 嵌入式零基础入门_(硬件与软件)学习指南原理方案设计
相关报告 · 2
- 能读ID但读写失败_样机排查调试&解决方案
- 展讯(Spreadtrum)平台_SPI_NAND_eMMC_量产排查调试&解决方案
- 为旌(Visinextek)_开发指南原理方案设计
- 中控屏(Center Control Display)_市场分析市场应用分析
- 汽车仪表盘(Instrument Cluster)_市场分析市场应用分析