
做嵌入式Linux开发这些年我每次把一块新板子的内核跑起来最先关注的往往不是CPU频率而是启动日志里MTD那一段。干过这行的都懂UBI、JFFS2、YAFFS2这些文件系统全部架在NAND Flash驱动上驱动没写好后面全是假象。曾经遇到一个非常隐蔽的问题内核对NAND识别很正常分区也在UBIFS也能挂载但跑几天后总有文件损坏最后追到的问题是ECC强度配置低于芯片规格数据在静置状态发生翻转后驱动没有能力纠正。这些经验让我觉得有必要把NAND Flash基础与Linux驱动这条线系统整理一遍。这篇文章是《Linux NandFlash基础及驱动讲解》系列的第一篇主要面向刚接触嵌入式Linux、打算把NAND驱动或分区结构搞清楚的开发者当然搞过但没系统捋过整套框架的人也能从中对照出不少盲区。1. NAND Flash 物理层基础驱动代码以外的必修课很多人一上来就翻驱动源码结果被各种回调函数绕晕。其实NAND驱动的复杂度很大一部分来自芯片本身的物理特性。先把物理模型搞明白驱动里的很多设计就都说得通了。1.1 一个存储单元的变化决定了整个驱动模型NAND Flash 的基本存储单元是浮栅晶体管或者电荷俘获型晶体管靠内部存储的电荷量来区分0和1。一片NAND由成千上万个这样的单元组成但这些单元并不能像内存那样按字节随机访问而是有严格的读写擦除边界。我从实际开发中总结的最关键三条规则读和写的最小单位是页不是字节。擦除的最小单位是块一个块通常包含64页、128页甚至更多页。写之前必须先擦除而且擦除只能把整块内容全部变成0xFF。第三条尤其重要。NAND的编程操作本质上只能把某些位从1写成0无法把0改回1。所以如果某一页已经写过数据想要修改其中几个字节就必须先把整个块擦掉再把包含旧数据和新数据的整块内容重新写一遍。这也是为什么NAND驱动里到处是“read-modify-erase-write”的影子而不是普通块设备那种“改哪个扇区就写哪个扇区”的模式。页和块的尺寸在驱动里对应两个关键参数writesize和erasesize。以常见NAND为例小页老芯片每页512字节或者2048字节块大小16KB/64KB。现在主流每页4096字节或8192字节块大小128KB/256KB甚至更大。两颗甚至多颗Die堆叠封装后单颗容量轻松到8Gbit、16Gbit。驱动在做地址换算时必须把页内偏移、页号、块号、Die选择位拆开一位一位地通过地址线发给芯片。地址周期多一位或少一位都会导致读出来的ID不对或者访问越界。这些问题在Linux里往往表现为“识别到NAND但容量少一半”或者“读出来全是0xFF”。1.2 SLC、MLC、TLC 背后不只是价格更是ECC强度同样是“NAND Flash”SLC、MLC、TLC的电气特性差别非常大。SLC每个单元只存1bit只有两个电压阈值MLC每个单元存2bit需要区分4个电压等级TLC每个单元存3bit要区分8个电压等级。电压等级越多单元之间的电压窗口越窄电荷泄漏、读干扰、写干扰带来的误码率就越高。从驱动视角看选型带来的直接后果是ECC能力需求完全不同。SLC芯片在做老式小页NAND时常见的硬件ECC只需要1bit/512字节或者4bit/512字节。MLC通常要求8bit/512字节甚至更高。TLC普遍要求16bit/1KB以上很多还要配合LDPC纠错算法。我在项目选型时有一条很朴素的准则工业设备、长期存储、宽温环境优先SLC消费品、大容量、成本敏感才考虑MLC/TLC。因为NAND寿命不只是“擦除次数”一个指标还有数据保持时间。SLC在同样的电荷丢失率下电压判读窗口宽得多静置几年后读出来还是准的TLC可能放几个月就需要靠强纠错把数据捞回来。驱动里的nand-ecc-strength、nand-ecc-step-size这些设备树参数本质就是告诉Linux“这颗Flash到底需要多强的纠错能力”。1.3 页读取时多出来的 OOB 区为什么要关注每一页可以分成两个区域主数据区和备用区。主数据区就是我们说的页大小通常是2048字节或4096字节备用区也经常叫OOBOut Of Band或spare区常见大小是64字节、128字节或224字节。这个OOB区不是给用户随便用的闲散空间它承担两个关键职能存放ECC校验数据。存放坏块标记。读NAND时主数据和OOB是一起从芯片里读出来的。驱动能不能正确地把OOB数据安排到合适的位置决定了文件系统能不能在掉电后恢复、能不能识别坏块。比如UBI层需要知道OOB里哪些字节是文件系统数据哪些字节是ECC校验数据哪些字节是坏块标志。如果驱动里OOB布局跟UBI的预期不一致就会出现“attach失败”或者“CRC error”。我经常打一个比方NAND的一页像一张答题卡主数据区是考生填的答案OOB区是机器阅卷用的标记位。没有OOB答案本身可能是残缺的OOB布局错了机器根本不知道去哪一行读标记。所以后面讲驱动时OOB布局永远是一个绕不开的重点。2. Linux 对 NAND 的抽象MTD 子系统从内核角度看NAND Flash不是标准块设备。如果直接丢给块设备层处理很多Flash特性会被抹掉。Linux专门提供了一个中间层叫MTDMemory Technology Device。NAND驱动做的所有事情最终都是为了让MTD层能够正确操作这颗芯片。2.1 不能直接把 NAND 当普通块设备来用普通块设备比如SD卡、SSD它们内部已经有主控帮你做坏块管理、擦写均衡、ECC纠错。主机看到的是一个“逻辑块设备”可以随机读写任意扇区。NAND Flash裸片没有这些能力它只有最原始的页读写、块擦除指令坏块要软件自己标记ECC要软件自己做或者依赖SoC的NAND控制器帮忙。因此Linux不能把NAND直接挂到通用块层上否则会出现几个严重问题写数据前不知道要擦除整块覆盖导致旧数据残留。坏块没人管一写就挂。位翻转没人纠正读回来的数据可能是错的。MTD层就是为了解决这些问题而生的。它向上提供统一接口比如read、write、erase、read_oob、write_oob向下由具体NAND驱动实现这些接口针对不同芯片做适配。文件系统不用关心你用的是哪家Flash也不用关心页大小和块大小只需要通过MTD拿到正确的参数。这一点也是整个嵌入式存储栈的核心应用不可能直接操作裸Flash驱动、MTD、文件系统各管一段谁也不能越权。2.2 /dev/mtd0 与 /dev/mtdblock0 的角色差别在Linux设备节点里NAND MTD设备通常会注册成两类设备字符设备和块设备。字符设备是/dev/mtd0、/dev/mtd1这种块设备是/dev/mtdblock0、/dev/mtdblock1这种。启动后查看/proc/mtd会看到类似下面这样的输出dev: size erasesize name mtd0: 00200000 00020000 boot mtd1: 00400000 00020000 kernel mtd2: 07a00000 00020000 rootfs这里的size和erasesize都是十六进制单位是字节。比如0x00200000就是2MB0x00020000就是128KB。很多初学者分不清mtdN和mtdblockN的区别简单说/dev/mtdN是MTD字符设备用来做ioctl、read、write、erase比如flash_erase、nanddump这类工具访问的就是它。/dev/mtdblockN是MTD模拟出来的块设备主要是为了兼容某些老应用。但我不建议在正经项目里用mtdblock挂文件系统。因为NAND必须先擦后写而块设备层假设的是随机覆写mtdblock层面的模拟缓存很容易产生不一致。真要挂文件系统要么用JFFS2这类专门针对MTD设计的文件系统要么通过UBI层再挂UBIFS而不是简单地把mtdblock当U盘用。2.3 设备树、平台设备和 MTD 分区注册的调用链现在主流平台基本都靠设备树来声明NAND控制器的硬件信息。NAND控制器本身是一个平台设备SoC上的片选、中断、寄存器基址都从设备树里拿。典型的结构像这样nand_controller { status okay; pinctrl-names default; pinctrl-0 nand_pins; nand0 { reg 0; nand-ecc-mode hw; nand-ecc-strength 8; nand-ecc-step-size 512; nand-on-flash-bbt; partitions { #address-cells 1; #size-cells 1; partition0 { label boot; reg 0x0 0x200000; }; partition1 { label rootfs; reg 0x200000 0x7a00000; }; }; }; };设备树解析完成之后内核会找到匹配的NAND控制器驱动调用驱动的probe函数。真正的注册流程大致是probe函数里获取寄存器基址、中断、时钟等资源。分配并初始化struct nand_chip设置底层回调函数。调用nand_scan整颗芯片识别厂商ID、容量、页大小、OOB大小。调用mtd_device_register把MTD设备注册进系统。解析设备树里的partitions节点创建/dev/mtd0、/dev/mtd1等分区设备。这里会有个容易混淆的地方设备树里写的partitions是内核MTD分区解析器来处理的并不是NAND驱动自己一个一个注册的。驱动只需要把整颗芯片作为一个MTD设备注册上去分区逻辑由mtd_device_register配合设备树来完成。所以写驱动时不要自己手动去拆分区否则会和内核标准机制打架。3. NAND 驱动核心数据结构与关键 API 解读真正写NAND驱动时最常用的两个结构体是struct mtd_info和struct nand_chip。理解它们的分工比背一百行代码都有用。3.1 分清两层结构体MTD 设备层与 NAND 芯片控制层从分层角度看struct mtd_info描述的是一个“MTD设备”比如NAND Flash、NOR Flash、DataFlash。它向上面对VFS、文件系统、mtdchar提供read、write、erase、read_oob等标准方法。struct nand_chip描述的是NAND芯片本身的控制信息包括芯片的ID、页大小、块大小、OOB大小、ECC配置以及访问NAND所需的各种底层回调函数比如cmd_ctrl、dev_ready、read_buf、write_buf。在经典框架里struct nand_chip内部会包含一个内嵌的struct mtd_info或者通过指针把两者关联起来。MTD层的统一接口最终会调用到NAND芯片层的具体操作函数。也就是说上层只认识mtd_info底层只认nand_chip驱动开发者的工作就是把这两层焊在一起。我见过不少人把这两个结构体搞混以为往mtd_info里填了erase函数就行。实际上对于裸NAND你大部分时候不需要自己实现mtd_info里的erase因为NAND核心框架已经帮你写好了通用实现它内部会用nand_chip提供的底层接口去发擦除命令。你需要做的是把“怎么拉高片选”“怎么发命令”“怎么读状态”这些最底层的动作实现出来。3.2 必须实现的核心底层操作不同SoC的NAND控制器差别很大。有些控制器内部有完整的状态机软件只需要写命令寄存器和数据寄存器有些控制器则非常简单需要软件模拟CLE、ALE、CE这些引脚时序。不管控制器多复杂nand_chip里这几个回调是驱动最常需要实现的select_chip选择或释放片选对应NAND芯片的CE引脚。cmd_ctrl命令/地址控制往命令锁存器或地址锁存器写数据对应CLE和ALE时序。dev_ready查询芯片的R/B#引脚判断芯片是否忙。read_byte/read_word读数据总线。read_buf/write_buf批量读写数据缓冲区。waitfunc等待一次操作完成。下面是一段非常示意化的代码体现的是内存映射型NAND控制器最朴素的写法static void demo_nand_cmd_ctrl(struct nand_chip *chip, int cmd, unsigned int ctrl) { void __iomem *base chip-priv; if (ctrl NAND_CLE) { writeb(cmd, base NAND_CMD_REG); } else if (ctrl NAND_ALE) { writeb(cmd, base NAND_ADDR_REG); } } static int demo_nand_dev_ready(struct nand_chip *chip) { void __iomem *base chip-priv; return readb(base NAND_STATUS_REG) 0x01; } static void demo_nand_read_buf(struct nand_chip *chip, u8 *buf, int len) { void __iomem *base chip-priv; int i; for (i 0; i len; i) buf[i] readb(base NAND_DATA_REG); } static void demo_nand_write_buf(struct nand_chip *chip, const u8 *buf, int len) { void __iomem *base chip-priv; int i; for (i 0; i len; i) writeb(buf[i], base NAND_DATA_REG); }实现时特别注意一点cmd_ctrl里不同控制器的寄存器偏移完全不同有的是CLE和ALE各一个地址有的是共用地址靠bit区分。千万不要追求“一段代码通吃”一定要对着芯片手册和SoC手册的时序图去逐项核对。3.3 probe 函数里的三步曲一个标准NAND驱动probe函数的核心骨架通常可以浓缩成三步static int demo_nand_probe(struct platform_device *pdev) { struct mtd_info *mtd; struct nand_chip *chip; struct resource *res; void __iomem *base; res platform_get_resource(pdev, IORESOURCE_MEM, 0); base devm_ioremap_resource(pdev-dev, res); if (IS_ERR(base)) return PTR_ERR(base); mtd kzalloc(sizeof(*mtd), GFP_KERNEL); if (!mtd) return -ENOMEM; chip kzalloc(sizeof(*chip), GFP_KERNEL); if (!chip) { kfree(mtd); return -ENOMEM; } mtd-priv chip; mtd-name demo-nand; chip-priv base; chip-cmd_ctrl demo_nand_cmd_ctrl; chip-dev_ready demo_nand_dev_ready; chip-read_buf demo_nand_read_buf; chip-write_buf demo_nand_write_buf; chip-chip_delay 50; if (nand_scan(mtd, 1)) { kfree(chip); kfree(mtd); return -EIO; } mtd_device_register(mtd, NULL, 0); platform_set_drvdata(pdev, mtd); return 0; }这里需要说明一下不同内核版本的nand_scan入参并不完全相同。老版本常用nand_scan(mtd, maxchips)新版本逐渐改成nand_scan(chip, maxchips)。上面代码只是展示经典思路你在移植时一定要以当前内核源码目录下的头文件为准。nand_scan内部做的事情非常多它会发送复位命令读取NAND的ID。根据ID查找内核的NAND Flash ID表确认容量、页大小、OOB大小。扫描坏块信息建立坏块表。根据ECC配置计算OOB布局。初始化MTD的读写擦除接口。随后mtd_device_register会把整个MTD设备注册进内核并且根据分区信息生成mtd0、mtd1这些子设备。到这一步你在应用层就能看到/proc/mtd里多出设备了。3.4 时序和等待为什么 cmd_ctrl 后面通常要接 dev_readyNAND芯片执行命令不是瞬间完成的。比如页编程发完编程命令后芯片需要几十微秒到几百微秒才能把电荷写入浮栅块擦除更慢通常需要几毫秒。驱动如果不等芯片空闲就继续发下一个命令结果必然是数据错乱。等待机制一般有两种dev_ready回调直接读R/B#引脚电平低电平表示忙高电平表示闲。状态寄存器查询控制器把芯片状态寄存器映射到某个地址软件读出来判断。写驱动时很多人把chip_delay设得很大比如50甚至100用死等代替状态查询。短时间测试能过但真正压测时性能很差。正确做法是尽量使用dev_ready或中断方式等待只在极简单场景下才用固定延时兜底。我在实际调试时会先用示波器抓CE、CLE、ALE、R/B#这几根线的波形确认每条命令之后芯片确实进入了忙状态再决定要不要优化等待逻辑。4. ECC、OOB 布局与坏块管理驱动里最容易翻车的地方很多NAND驱动跑不出问题不是因为写得好而是运气好。一旦遇到坏块、位翻转、文件系统崩溃吃亏的往往是那些没有认真处理ECC和OOB的人。这一块必须单独讲。4.1 ECC 强度必须对齐芯片规格NAND芯片出厂时就允许存在一定比例的错误位。芯片容量越大制程越先进误码率越高。Linux驱动里ECC相关的关键参数有两个nand-ecc-strength能纠正几个bit错误。nand-ecc-step-size每多少字节数据算一个ECC单元。常见组合是4bit/512B、8bit/512B、16bit/1KB。设备树里配的nand-ecc-strength如果低于芯片手册要求系统可能能正常启动但运行一段时间后文件损坏。如果高于芯片要求也不是完全不行但会浪费OOB空间并增加实时校验开销。EEPROM、NOR这类设备对ECC的依赖低很多但NAND不行。我遇到过一起很难查的问题内核日志没有任何错误读写测试也正常但数据放了两周后部分文件读出来校验失败。最后定位到原因就是ECC只配了4bit/512B而芯片在数据保持边缘需要8bit/512B。所以拿到新的NAND芯片第一件事就是去手册里查最小ECC强度然后写进设备树。4.2 OOB 布局必须与UBI和文件系统约定一致OOB区要同时存放坏块标记、ECC校验数据和文件系统元数据。空间就那么大怎么分配必须有明确规则。假设一个NAND页面主区是2048字节OOB是64字节ECC要求8bit/512B。一种常见布局可能是OOB偏移0坏块标记通常1字节。某些偏移区间文件系统的自由OOB区比如UBI卷管理数据。剩余区间每512字节数据对应的ECC校验码。不同的文件系统和不同的ECC算法对OOB布局的接受范围不一样。UBI attach时它会读取每个EB擦除块头部的ec_hdr这些数据就放在OOB或者主数据区头部。如果驱动生成的OOB布局不符合UBI的预期常见现象是ubi0: scanning is finished ubi0 error: ubi_read_volume_table: ...或者干脆UBI error: vtbl_check: too large reserved_pebs这时候不要直接怀疑UBI而要先检查NAND驱动的OOB布局。最笨也最有效的办法是用nanddump把整块OOB数据导出来对照UBI对OOB偏移的定义一字节一字节地核对。4.3 坏块管理不是文件系统的事也不完全是驱动的事NAND出厂时就有坏块使用过程中还会产生新坏块。Linux里坏块管理通常分两层底层驱动在扫描芯片时标记坏块生成坏块表BBT。上层UBI或文件系统在运行过程中发现某个块写失败、擦除失败把它纳入坏块列表并替换。在驱动层面如果设备树里开了nand-on-flash-bbtLinux会尝试在Flash上保存坏块表这样下次启动时不用全盘扫描。如果没有开每次启动都会重新扫描所有块速度会慢一些尤其是大容量NAND。实际项目里我不建议驱动把坏块管理完全包揽。驱动更适合做“识别坏块、暴露坏块信息”这一层至于磨损均衡、坏块替换、掉电保护是UBI这类上层机制的主场。如果你在驱动里强行做太多坏块管理反而容易和UBI冲突。5. 常见问题排查从启动日志到读写出错的完整思路NAND驱动调试说难也难说简单也简单。难在信号时序不可见简单在绝大多数问题都有固定套路。我总结了一套排查顺序能解决九成以上的NAND异常。5.1 第一步永远先读 ID不管问题多花哨都要先确认NAND芯片能不能被识别。启动时看内核日志正常情况会有一行类似nand: device found, Manufacturer ID: 0x2c, Chip ID: 0xda nand: 1024 MiB, SLC, erase size: 128 KiB, page size: 2048, OOB size: 64如果日志里出现nand: No NAND device found那问题基本出在硬件访问链路上。常见原因包括片选没拉对CE引脚根本没选中芯片。命令/地址锁存器的地址映射错误cmd_ctrl写到了错误的寄存器。数据线引脚配置错误读出来的字节全是0xFF或0x00。时钟没开控制器根本没有工作时钟。这时候不要浪费时间调ECC先拿示波器看CE、CLE、ALE、数据线有没有信号翻转。如果连ID都读不对后面都是空谈。5.2 常见症状速查表下面这张表是我处理NAND问题时的自用速查表也分享给读者现象可能原因检查方向启动时找不到NAND片选、时钟、引脚、电源问题检查设备树reg、控制器时钟、cmd_ctrl寄存器地址识别到NAND但容量不对地址周期配置不对ID表匹配错误核对芯片手册的地址周期数检查nand_scan传参读回数据全是0xFF命令时序不对数据线没连通用示波器抓CLE/ALE/数据线检查read_buf实现UBI attach失败OOB布局或擦除块大小不匹配用nanddump导出OOB对照UBI头结构文件系统挂载后跑几天损坏ECC强度不足静置位翻转检查nand-ecc-strength对照芯片最小ECC要求写失败伴随大量坏块电源或时序不稳导致误判坏块降低时钟频率检查VCC纹波重新擦除测试5.3 一条快速验证命令链拿到一个能识别MTD设备的内核我通常会按顺序执行下面这几条命令确认驱动是否真的可靠。先看分区cat /proc/mtd再用mtdinfo看单个MTD设备的参数mtdinfo /dev/mtd0如果分区是空的数据区可以先擦掉flash_erase /dev/mtd0 0 0然后把读出来的内容导成文件检查是否全为0xFFnanddump -f /tmp/mtd0.bin /dev/mtd0有条件的话用nandtest做读写压力测试nandtest /dev/mtd0nandtest会反复写入、读取、比较数据能比较快地暴露ECC和时序问题。但千万注意这会破坏目标分区上的数据只能在测试分区上执行别在真实rootfs分区上乱跑。5.4 一个真实踩坑场景的复盘有块板子换了颗容量更大的NAND芯片内核识别正常mtdinfo显示的页大小、块大小都正确但UBIFS在挂载时一直报结构错误。我一开始怀疑UBI版本不对后来用nanddump -o导出OOB数据发现驱动把ECC校验数据放在OOB前8字节而新主控硬件ECC方案要求从OOB第16字节开始。这导致UBI写在OOB里的卷头数据被遮住了系统自然读不到有效超级块。这个问题特别典型同一个SoC换了一颗不同厂家的NAND虽然页大小和块大小看起来一样但OOB布局默认可能完全不同。所以在移植NAND驱动时除了看容量还要专门确认新芯片的OOB尺寸、坏块标记位置、ECC校验区域偏移。这三样不匹配驱动再怎么调时序都没用。6. 驱动和上层文件系统之间还差一个 UBI 层这一节稍微讲点后续内容因为很多人在问“NAND驱动写完了为什么不能直接挂UBIFS”。理解了这一层整个Linux NAND栈就串起来了。6.1 为什么不是直接从 MTD 挂 UBIFSUBIFS是设计在UBI之上的文件系统而UBI是设计在MTD之上的卷管理/磨损均衡层。一个完整的链路是NAND芯片 - NAND控制器 - Linux NAND驱动 - MTD层 - UBI层 - UBIFS文件系统NAND驱动负责把芯片变成MTD设备MTD层向上提供统一的read、write、erase、read_oob接口UBI层负责把连续的擦除块分成多个逻辑卷同时做磨损均衡和坏块管理UBIFS在UBI卷上实现文件系统语义。JFFS2和YAFFS2可以不依赖UBI直接在MTD上跑因为它们在文件系统内部自带磨损均衡和坏块管理。但在现代内核里UBIFS是NAND上的主流方案它要求必须先有UBI层。所以如果应用层想用UBIFS你在驱动里需要保证mtd_info提供的erasesize、writesize、oobsize都真实可信。任何一个参数和芯片实际不符UBI在扫描时都会产生莫名其妙的问题。6.2 驱动参数如何传导到 UBIUBI在创建时主要关心这样几个参数物理擦除块大小也就是erasesize。逻辑擦除块大小通常是物理擦除块减去UBI头部的保留空间。MTD设备的总大小。OOB布局中可用于UBI的空间。这些参数几乎全部来自NAND驱动扫描芯片后填充的struct mtd_info。如果你在驱动里对erasesize做过特殊处理而没有同步更新UBI侧那么UBI attach时就会报unknown erase counter或者bad image sequence number。我在实际调试中会养成一个习惯在内核启动后先停下来读一遍mtdinfo强制自己看一眼里面的Minimum I/O unit size、OOB size和Erasable block size再继续后面的文件系统操作。因为这几项数值如果错了后面所有错误都像是文件系统惹的祸追起来非常痛苦。另外还有一个很实际的小技巧换NAND芯片后不要只改设备树里的容量。要重新确认页大小、块大小、OOB大小、ECC强度和坏块标记位置。这些参数是整套存储栈的地基地基偏一寸上面所有楼层都会跟着偏。这也是为什么我一直强调先吃透NAND物理层特性和Linux MTD框架再动手改驱动。基础不牢后面调试花的时间往往是前期省下的好几倍。