前一阵帮朋友调一块带 NAND Flash 的板子,uboot 引导 kernel 都没问题,rootfs 用 initramfs 也能起来,就是要做真正的产品化时卡住了——得把根文件系统放到 NAND 里,而板子里的 Flash 分区偏偏是给 yaffs2 预留的。折腾了几天,踩了不少坑,把整个移植过程重新捋了一遍。这篇文章就把我的完整操作记录、遇到的各种问题,还有最后定下来的那套配置,原原本本分享出来。如果你也正打算在自己板子上把 rootfs 固化到 NAND,跑 yaffs2 文件系统,这篇应该能帮你少走几个来回。
1. 为什么偏偏是 yaffs2
做嵌入式 Linux 的都知道,NAND Flash 不是硬盘,它的读写单位是 page,擦除单位是 block,而且有坏块、位翻转这些“与生俱来”的问题。
传统文件系统(ext2/3、FAT)跑在 NAND 上,用不了多久就会出幺蛾子——因为它们在设计时根本没考虑“写之前要先擦除”“擦除次数有限”“坏块要跳过”这些 Flash 的物理特性。
yaffs2 是专门为 NAND Flash 设计的日志型文件系统,几个核心特性正好打在痛点上面:
- 日志型(log-structured)设计:数据总是顺序写入,配合 NAND 只能先擦后写、写次数有限的特点尽量均衡磨损,掉电后 recovery 也快。
- 坏块管理:读写时自动检测坏块并跳过,不需要你手动维护坏块表。
- 每页 OOB(Out-of-Band)区利用:文件系统元数据就放在 NAND 每页的 spare 区里,ECC 校验和坏块标记都从这里走,不像 ext2 那样需要额外维护 block bitmap。
该选 yaffs1 还是 yaffs2,看的是 NAND 的大小和页大小。512 字节小页 NAND 用 yaffs1,2KB 及以上大页 NAND 用 yaffs2。现在板子基本都 2KB 页起步了,说“移植 yaffs2 根文件系统”,本质上就是三件事:
- 内核得支持 yaffs2(内核配置 + 文件系统源码)
- 得有合适的 MTD 分区和 NAND 驱动配合
- 根文件系统内容要能放进一个干净、可引导的分区里
下面逐步说。
2. 第一步:准备内核侧支持
2.1 yaffs2 源码怎么进内核
yaffs2 没有进入 Linux 官方内核主线,这是很多人第一次搞时最容易懵的地方。我当时的做法是:确认好内核版本(比如 4.19 或 5.10),然后从 yaffs2 的官方维护仓库拉对应版本的补丁,或者直接将相关源码目录整体添加到内核源码树内。
具体来说,就是把yaffs2源码下的yaffs_vfs.c、yaffs_guts.c、yaffs_mtdif.c、yaffs_mtdif2.c等一系列文件放进fs/yaffs2/目录,然后补一份Kconfig和Makefile:
# fs/yaffs2/Makefile obj-$(CONFIG_YAFFS_FS) += yaffs2.o yaffs2-y := yaffs_vfs.o yaffs_guts.o yaffs_mtdif.o yaffs_mtdif2.o# fs/yaffs2/Kconfig config YAFFS_FS tristate "YAFFS2 file system support" select MTD_NAND_ECC help YAFFS2 is a log-based file system designed for NAND Flash.两级路径都要注意,fs/Kconfig里需要加source "fs/yaffs2/Kconfig",fs/Makefile里加obj-$(CONFIG_YAFFS_FS) += yaffs2/。
如果你用的交叉编译工具链和内核版本差得比较远,源码树放好之后编译报错也很正常,重点检查fs/yaffs2/下的文件是否有针对不同内核版本的#ifdef适配。官方仓库里一般有宏分支,选匹配你当前内核分支的即可。
2.2 内核配置项逐个过
进入内核配置界面,我习惯直接在menuconfig里搜索yaffs关键字:
Device Drivers ---> Memory Technology Device (MTD) support ---> <*> NAND Device Support ---> <*> 你的 NAND 控制器驱动(比如某某平台 NAND 驱动) [*] Support for NAND Flash [*] MTD 分区支持 / 命令行分区解析 File systems ---> Miscellaneous filesystems ---> <*> YAFFS2 file system support [*] Enable yaffs2 mode [*] Enable yaffs1 mode(如果你的 NAND 还有小页)几个容易漏的配置:
- 如果 rootfs 通过
mtdparts=分区参数传递,必须开启CONFIG_MTD_CMDLINE_PARTS,否则内核里看不到你从 bootargs 指定的分区。 - ECC 校验建议打开,配合 OOB 使用,因为 NAND 的数据完整性全靠它。
- 块大小模式选择
yaffs2模式而不是yaffs1,不然底层 block 管理方式都不一样。
确认完后交叉编译内核,得到 zImage/Image,这一步先不急着烧写,先把文件系统部分准备好。
3. 第二步:构建根文件系统并制作 yaffs2 镜像
3.1 从哪里获取 rootfs 内容
我通常的做法是先用 BusyBox 搭一个最小可用的 rootfs。步骤基本固定:
- 下载 BusyBox 源码,配置为静态编译(
CONFIG_STATIC=y),避免在目标板上依赖动态库找不到。 make menuconfig勾选需要的 applet,比如mount、ls、cat、mkdir、mdev、sh这些。make && make install安装到_install目录。- 在 rootfs 根目录下补充
dev/、proc/、sys/、tmp/、etc/等目录和必要的配置文件,比如etc/inittab、etc/fstab、etc/init.d/rcS。
这就是最基础的一套 rootfs,后续如果想要跑业务程序,再往里面复制动态库、可执行文件、配置脚本。
3.2 mkyaffs2image 工具的来历
制作 yaffs2 镜像需要一个主机端工具,Linux 内核源码树里不带这个,要单独拿mkyaffs2image。它一般可以在 yaffs2 源码包的utils目录里找到,或者从一些老牌嵌入式厂商的 BSP 包里拆出来。
编译这个工具也简单:
cd utils make运行时它默认按 2KB 页、128字节 OOB、块大小 128KB 来打包,如果你的 NAND 参数不同,得在工具源码里改PAGE_SIZE、SPARE_SIZE这类宏定义,或者看它是否支持命令行参数。
3.3 生成 yaffs2 镜像
假设我的 rootfs 目录是rootfs_arm/,执行:
mkyaffs2image rootfs_arm rootfs.yaffs2生成的rootfs.yaffs2就是一个可以直接写到 NAND 对应分区的镜像文件。它内部会尽量保留文件属性、符号链接、硬链接,BusyBox 那种“一个二进制挂多个命令名”的符号链接方式没问题。
如果你要控制分区大小,还得先确认你要写的 MTD 分区是多大,然后让 rootfs 内容不要超过它。我在第一次做的时候就因为没注意分区大小,镜像写进去了,结果内核挂载 rootfs 时一直报错,后面细说。
3.4 设备节点别漏
yaffs2 镜像里要包含无法用mknod在运行时创建的设备节点。头一个必不可少的是控制台设备dev/console,一般是 character major 5 minor 1;另一个是 null 设备,major 1 minor 3。
我在制作 rootfs 的时候,直接用 fakeroot 或者 root 权限预先创建:
mkdir -p rootfs_arm/dev sudo mknod rootfs_arm/dev/console c 5 1 sudo mknod rootfs_arm/dev/null c 1 3如果不这样做,内核起来之后你会发现 console 没有设备节点,打印日志没问题,但 shell 输入输出不正常,特别难排查。
4. 第三步:NAND 分区与 bootargs 设置
4.1 分区是跟谁对齐的
NAND 的 MTD 分区通常由 uboot 传给内核,或者由内核命令行参数mtdparts=指定。
我板子上的 NAND 是 256MB,1MB 一个块,规划出来的分区大概是这样的:
mtdparts=nand0:1M(uboot),4M(kernel),-(rootfs)意思是第一个 1MB 放 uboot,接下来 4MB 放内核,剩余全部给 rootfs。注意这里-表示“剩余所有空间都归这个分区”,如果你的 rootfs 镜像比较小,剩余的空间会自动被识别为一个分区,实际使用时会发现文件系统只有镜像大小的数据,但是可以正常扩容使用(yaffs2 会在挂载时扫描整个分区)。
4.2 uboot 引导参数里怎么写
在 uboot 环境变量bootargs里,我最终用的配置是:
setenv bootargs 'console=ttyS0,115200 root=/dev/mtdblockN rootfstype=yaffs2 rw mtdparts=nand0:1M(uboot),4M(kernel),-(rootfs)'这一段里有几个关键点要解释一下:
root=/dev/mtdblockN这里的 N 要和你的分区顺序对应上。如果第 0 个分区是 uboot,第 1 个是 kernel,第 2 个是 rootfs,那 N 就是 2。用mtdblock设备作为 root 设备,是因为内核在启动初期需要通过块设备接口把 rootfs 读起来,然后 yaffs2 再挂载在 MTD 设备上。rootfstype=yaffs2必不可少,如果漏掉,内核默认会尝试 ext2/ext4 等,直接给你报VFS: Unable to mount root fs。rw我加上了,这样可以方便在开发阶段直接改文件系统内容。
4.3 内核必须内置 yaffs2,而不是模块
这一点非常重要:为了保证 rootfs 能挂载,yaffs2 一定要*(编进内核),不能是M(模块)。原因很简单——rootfs 是根文件系统,内核在挂载根文件系统的时候,还没有能力去加载一个外置的模块文件(模块本身就在 rootfs 里,这就成了先有鸡还是先有蛋的问题)。如果你非要用模块方式,就得借助 initramfs 先去加载模块再切根,工程上绕了一大圈,完全没必要。
5. 第四步:烧写与首次启动实录
5.1 烧写 yaffs2 镜像
我用的烧写方式是 uboot 的nand write,先把镜像放到内存,再写入 NAND:
# 假设已经通过 tftp 加载 rootfs.yaffs2 到 0x82000000 nand erase 0x500000 0xFB00000 # 擦除 rootfs 分区,大小要对齐块 nand write 0x82000000 0x500000 0x2000000 # 写镜像注意nand write的地址必须是块对齐的。rootfs 分区的起始偏移若为 5MB(0x500000),正好是块大小(0x100000)的整数倍,没问题。写入大小也尽量是块大小的整数倍,如果镜像文件本身不是整块大小,uboot 的 nand write 一般也能写,但保险起见可以补零到整块。
5.2 第一次启动后见到的现象
正常启动完应该出现:
[ 1.234567] VFS: Mounted root (yaffs2 filesystem) on device 31:2. [ 1.234678] Freeing unused kernel memory: 1024K Starting init: /sbin/init exists!看到这一行,rootfs 就算起来了。
如果在这个阶段出了错,后面那一节专门说问题排查。
6. 第五步:问题排查与避坑技巧
6.1 挂载不上?先检查这几个点
我把这几次踩坑经验总结成一个速查表:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
内核报Unknown filesystem type | 内核没编入 yaffs2 | grep YAFFS /boot/config-*或 menuconfig 确认 |
挂载成功但提示No such device | mtdblockN 分区号对不上 | 检查mtdparts顺序,对比/proc/mtd里的编号 |
| 挂载后 ls 看到乱码/文件丢失 | OOB 校验不匹配,或 mkyaffs2image 页大小和 NAND 不一致 | 核对mkyaffs2image默认参数,检查 NAND 扫描信息 |
启动卡在Failed to execute /init | rootfs 里没有 /init,或 Console 设备没建 | 检查/dev/console是否存在、rcS 是否可执行 |
写入文件报错No space left | 分区被镜像大小耗尽 | 确认分区实际大小,尽量在 mkyaffs2image 中限制大小 |
6.2 一个容易忽略的坑:spare/OOB 的 ECC 布局
嵌入式 NAND 控制器会自己算 ECC,硬件 ECC 和软件 ECC 的 OOB 布局可能不一样。yaffs2 使用的是文件系统自己定义的 OOB 布局,如果 NAND 驱动在 OOB 里预先存了 ECC 数据,二者会发生冲突。
我在实际调试中遇到的怪现象是:文件系统能挂载,创建文件也正常,但一重启就丢文件或者出现文件内容损坏。最后查下来就是 NAND ECC 模式的问题。解决思路是:
- 优先使用硬件 ECC,让 NAND 控制器自动算校验值,同时在 yaffs2 内部关闭自带的 ECC(或按驱动要求配合)。
- 或者强制 yaffs2 使用软件 ECC,并在内核里把硬件 ECC 关掉(如果控制器支持)。
具体操作取决于平台 NAND 控制器,通常要在平台代码里确认nand_chip->ecc.mode是NAND_ECC_HW还是NAND_ECC_SOFT。我用硬件 ECC 时,如果 OOB 里没有足够的空闲字节给 yaffs2 写 tags,就得适当调整 yaffs2 的配置,这一点在量产时特别容易出问题,一定要提前花时间验证。
6.3 关于坏块
NAND 出厂就会有些坏块。yaffs2 会在首次挂载时扫描并跳过坏块,但前提是坏块标记不能被动过。烧写镜像之前,如果 NAND 里已经残留乱数据,或者之前刷过别的文件系统,坏块标记可能被覆盖,导致 yaffs2 把坏块当普通块使用,后面写入会越来越不稳定。
建议量产流程里保留“整片擦除 + 坏块扫描”这一步:
nand erase.chip nand bad然后确认坏块表,再往指定分区写入镜像。这是我加进生产烧写脚本里的一个保险动作。
6.4 关于 rootfs 挂载为只读还是读写
开发阶段我开了 rw,但到了产品阶段,如果对稳定性要求高,推荐把 rootfs 挂成 ro,配合 tmpfs/ramfs 来挂/tmp、/var。这样可以避免因为意外断电导致 yaffs2 日志区频繁恢复,延长 Flash 寿命。
不过如果你需要 OTA 升级覆盖 rootfs,那么还是要留一个可写挂载点或独立升级分区,这个根据产品需求来权衡。
7. 优化与扩展
7.1 让 yaffs2 更适合产品化
如果只是验证功能,前面内容已经够了。但要做量产,我还会注意下面几点:
- 保留足够的保留块:yaffs2 允许你设定 reserved blocks,专门给 GC(垃圾回收)和坏块替代用。默认值可能在 5% 左右,如果你的 NAND 容量紧张,也不要压得太狠,否则 Flash 磨损过快。
- 检查挂载选项:比如
noatime可以避免每次读文件都更新访问时间,减少写入次数,对 Flash 友好。 - 定期做掉电测试:yaffs2 虽然日志型设计对掉电比较稳,但实际产品里掉电发生在“正在写数据”的瞬间,才是最考验的时刻。我一般用反复断电台架测试来验证 rootfs 不会在几十次掉电后损坏。
7.2 有没有更省事的替代方案
如果你手里的 NAND 不是特别老旧,且对兼容性要求更高,其实也可以考虑 UBI/UBIFS。UBIFS 是基于 UBI 子系统的,它把 NAND 的管理做得更完善,支持 write-back、压缩、卷管理,很多现代嵌入式平台默认就是 UBI。
但既然你已经跑 yaffs2 了,多半是 BSP 里带好了驱动,或者芯片厂商的 bootloader 里已经默认兼容它。yaffs2 的移植成本低、代码量小、社区资料多,这也是它至今仍有市场的原因。
我在另一个项目里做 NAND 产品的存储方案时,对比过 yaffs2 和 UBI,结论很简单:
- 你的平台、BSP、量产工具链里已经烧好了 yaffs2 相关工具和配置,直接沿用,最快最稳。
- 新平台从零开始做,存储方案可以优先考虑 UBI。
7.3 后续扩展:从 rootfs 可读到可写、可升级
yaffs2 rootfs 做好之后,如果想实现系统升级,我最常看到两种做法:
- A/B 分区方案:两个 rootfs 分区,互为备份,配合升级标记位来回切换。因为 NAND 本身容量有限,A/B 方案通常配合压缩镜像使用,先写临时分区,校验成功后切换。
- 内核 + rootfs 打包烧写:不单独升级 rootfs,而是把 rebase 后的完整镜像直接烧到整片 NAND。这种方案简单粗暴,适合小批量产品。
无论哪种,yaffs2 镜像的生成方式都和我前面用mkyaffs2image一致,只是最后写入分区的逻辑有区别。
8. 最后的注意事项
回顾整个移植过程,有几个细节再强调一下:
- 内核源码树里 yaffs2 目录的 Kconfig/Makefile 一定要和内核版本匹配,否则编译报错或者运行起来行为诡异。如果你是从厂商 BSP 里找现成的,直接用他们的版本最稳。
- mkyaffs2image 时的页大小/块大小必须和 NAND 驱动扫描到的一致,可以用一条命令确认:
cat /proc/mtd它会打印出每个分区的大小,同时内核日志里也会显示页大小和块大小信息。
别忽略 OOB 区大小。2KB 页的 NAND 一般 64 字节 OOB,但有的厂商 OOB 是 128 字节。如果 mkyaffs2image 默认按 64 字节生成,而你实际 NAND OOB 是 128,写进去之后数据可能“错位”,挂载后文件内容错乱。这一点尤其隐蔽,我第一次量产验证时就栽在这上面。
开发阶段保留串口输出的完整日志,尤其是 VFS 挂载那一行。真出了问题,这个日志是定位 rootfs 问题最重要的一条线索。
我个人在实际操作中的体会是:yaffs2 的移植,三分在代码,七分在参数匹配。文件系统本身逻辑并不复杂,难的是它和具体 NAND 控制器、ECC 布局、uboot 分区参数三者之间的配合。只要把这几层对齐了,rootfs 固化到 NAND 就是一条很稳定的路。如果你也正卡在某个挂载错误日志上无从下手,建议按我前面整理的排查表一项项过,尤其是把内核里 yaffs2 和 MTD 的相关配置确认清楚,问题大概率出在最容易忽略的那一环。