这次改动的直接原因是设备没有 A/B 分区。如果继续把 Squashfs 格式的 rootfs 挂载到物理 Flash 分区,由于 Squashfs 只读文件系统按需读取、不会一次性全部载入内存,OTA 升级时一边擦除 Flash 一边使用就会产生竞争,最终可能导致系统成砖。
所以我们把根文件系统制作成 Squashfs 只读镜像,由 U‑Boot 在启动时从 Flash 加载到内存,再通过内核的 initrd 机制将其作为根设备。这样根文件系统完全在内存中运行,既加快了启动速度,也避免了对 Flash 的写操作,所有临时数据通过挂载到 tmpfs 解决。
方案架构梳理与原理分析
整个启动过程如下:
U‑Boot 启动
1.从Flash中读取rootfs镜像数据到DRAM的指定地址
2.root=/dev/ram0
3.跳转内核
内核启动
1.解析initrd参数,将内存区域模拟为 /dev/ram0 设备
2.挂载 /dev/ram0(文件系统类型为 squashfs)作为根
3.执行 /linuxrc或/sbin/init完成系统初始化
initrd 与 initramfs 的区别
initrd(Initial RAM Disk):内核将内存中的一段数据作为块设备(/dev/ramX)处理,然后挂载其中的文件系统。它支持任意文件系统类型,例如 ext2、squashfs。
initramfs:内核将 cpio 归档解压到 tmpfs 中,形成可读写根,然后执行 /init。它只接受 cpio 格式。
我们选择 initrd+squashfs 组合,主要原因如下:
- Squashfs 压缩率高,能节省内存空间;
- 无需制作 cpio 包,减少构建步骤;
- 内核原生支持挂载 squashfs,性能优良。
修改过程:U‑Boot / 内核 / project.mak
内核配置(必须项)
确保以下选项被选中(.config 中为 y):
CONFIG_BLK_DEV=y
CONFIG_BLK_DEV_LOOP=y
CONFIG_BLK_DEV_INITRD=y
CONFIG_BLK_DEV_RAM=y
CONFIG_BLK_DEV_RAM_COUNT=1
CONFIG_BLK_DEV_RAM_SIZE=8192
CONFIG_SQUASHFS=y
CONFIG_DEVTMPFS=y
CONFIG_DEVTMPFS_MOUNT=y
注意:CONFIG_BLK_DEV_RAM_SIZE 必须大于等于 rootfs 镜像解压后的大小。Squashfs 挂载后占用空间即为镜像大小,因为它是按块读取,并非展开。如果设置过小,挂载会失败。
如果内核未开启 DEVTMPFS,则 /dev/ram0 节点不会自动创建,需要手动执行 mknod,因此强烈建议开启。
内存地址规划与参数传递
我们项目中的内存布局规划关键计算如下:
# DRAM 基址
SYSD_RAM_BASE = 0x40000000
# 内核占用大小(包括设备树等)
OS_MEM_SIZE = 73 # MB
# 内核结束地址即文件系统加载起始
CMP_START_ADDR = $(call AddAddressMB, $(SYSD_RAM_BASE), $(OS_MEM_SIZE))
# rootfs 镜像大小(来自 Flash 分区)
ROOTFS_PARTITION_SIZE_MB = 32
# rootfs 内存起始地址(紧挨内核)
ROOTFS_START_ADDR = $(shell printf "0x%x" $$(($(CMP_START_ADDR) - $(ROOTFS_PARTITION_SIZE_MB) * 1024 * 1024)))
# Flash 中 rootfs 分区的偏移和大小
ROOTFS_DATA_FLASH_BASE = $(call calculate_flash_base,$(FLASH_PARTITIONS),rootfs)
ROOTFS_SIZE_BYTES = $(shell expr $(ROOTFS_PARTITION_SIZE_MB) \* 1024 \* 1024)
# 构建 U‑Boot 加载命令
ROOTFS_LOAD_CMD_PARAM = "sf probe; sf read $(ROOTFS_START_ADDR) $(ROOTFS_DATA_FLASH_BASE) $(ROOTFS_SIZE_BYTES)"
需要特别注意的是,rootfs 最好放在内核结束地址的下方,也就是低地址方向,确保不与内核重叠。如果内核占用 73MB,从 0x40000000 开始,结束地址就是 0x40000000 + 73*1024*1024 = 0x44900000。按 32MB 的 rootfs 分区大小计算,rootfs 起始地址应为 0x44900000 - 32*1024*1024 = 0x42900000。这样内核与 rootfs 之间没有空隙,也不会互相重叠。
此处一旦疏忽,很容易发生数据踩踏,导致 U‑Boot 启动内核时直接报找不到内核。
U‑Boot 修改:自动读取 rootfs 到内存
修改 uboot/Makefile.common:将读取命令作为宏传递给 C 代码。
EXT_FLAG := ... KCPPFLAGS="-I... -DROOTFS_LOAD_CMD='$(ROOTFS_LOAD_CMD_PARAM)'"
修改 cmd/axera/boot/axera_boot.c:在启动内核前执行该命令。
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)
// 在 do_axera_boot() 函数中,加载内核之后、启动之前添加:
#ifdef ROOTFS_LOAD_CMD
printf("Loading rootfs: %s\n", TOSTRING(ROOTFS_LOAD_CMD));
ret = run_command(TOSTRING(ROOTFS_LOAD_CMD), 0);
if (ret) {
printf("rootfs load failed\n");
goto failed;
}
#endif
这样 U‑Boot 在引导内核前,就会将 Flash 中的 Squashfs 镜像完整读入内存指定位置。
内核启动参数(bootargs)
bootargs 的宏定义会被编译到 U‑Boot 中,再由 U‑Boot 传递给内核。我们在 project.mak 中重新构造 bootargs:
KERNEL_BOOTARGS = $(OS_MEM) console=ttyS0,115200 root=/dev/ram0 ro rootfstype=squashfs \
initrd=$(ROOTFS_START_ADDR),$(ROOTFS_PARTITION_SIZE_BYTES) \
mtdparts=spi3.0:$(FLASH_PARTITIONS) ...
root=/dev/ram0:指定根设备为 ramdisk。
rootfstype=squashfs:明确文件系统类型,防止内核自动探测失败。
initrd=addr,size:告知内核 initrd 镜像的位置和大小,单位为字节。
我踩过的两大坑与避坑方法
坑一:initrd 地址与内核重叠导致数据损坏
现象:U‑Boot 下启动内核,提示找不到内核。
根源:计算地址时没有完全避开内核所在内存区域,发生了数据踩踏。
对策:
- 明确各段内存布局:内核、DTB、initrd 三者必须互不重叠;
- 在
project.mak 中仔细校验计算公式,使用十六进制打印输出,确保对齐。
坑二:bootargs 修改不生效——环境变量缓存作祟
现象:在 U‑Boot 中执行 printenv bootargs 显示已经更新,但内核启动时打印的 Kernel command line 仍是旧值,导致 initrd 参数没有传递给内核。
根源:U‑Boot 的环境变量通常保存在 Flash 中,并且带有缓存。如果只修改了内存中的环境变量,比如使用 setenv 却没有执行 saveenv,下次重启或重载时会从存储区恢复旧值。此外,如果代码中通过 CONFIG_BOOTARGS 硬编码了默认值,而环境变量没有覆盖它,也可能导致用户设置被忽略。
对策:
- 在 U‑Boot 命令行修改后,务必执行
saveenv;
- 如果通过构建系统生成
bootargs,要在 project.mak 中正确赋值,并在启动前通过 printenv bootargs 确认;
- 也可以在 U‑Boot 脚本中直接执行
setenv bootargs ... 再 saveenv,或者直接先擦除 env 分区。
注意:如果内核配置了 CONFIG_INITRAMFS_SOURCE,也就是内嵌 initramfs,内核会优先使用内嵌的 cpio,而忽略外部 initrd。因此务必确保该选项为空,让外部 initrd 生效。
Squashfs 与 CPIO 格式的选择分析
很多传统方案会把 rootfs 制作成 cpio.gz 或 cpio.lzma,作为 initramfs 使用。我们则直接使用 Squashfs 镜像。两者适用场景对比如下:
| 特性 |
Squashfs + initrd |
CPIO + initramfs |
| 镜像格式 |
原生压缩文件系统,按块压缩 |
归档文件,整体压缩(gz/lzma) |
| 挂载方式 |
内核挂载为块设备,只读 |
内核解压到 tmpfs,可读写 |
| 内存占用 |
镜像本身大小,访问时缓存部分数据 |
解压后完整大小,占用全部内存 |
| 启动速度 |
无需完全解压,按需读取,较快 |
需完整解压到内存,稍慢 |
| 可写性 |
只读,需额外挂载 tmpfs 用于临时数据 |
初始即可读写,便于临时修改 |
| 构建复杂度 |
直接生成 Squashfs 镜像,简单 |
需要制作 cpio 并压缩,多一步 |
| 适用场景 |
内存紧张、根文件系统稳定的产品 |
需要启动阶段灵活写入、调试方便的场合 |
在我们的场景中,DRAM 总大小只有 128MB,内存资源比较紧张,因此 Squashfs + initrd 是更优的选择。它不仅节省内存,还减少了内核解压时间,整体启动耗时更短。
文末小结
通过本次改造,成功将根文件系统从 Flash 分区迁移到内存中挂载。整个过程只需要调整内核配置、内存地址计算和 U‑Boot 加载逻辑,不用修改根文件系统本身。
核心经验:
- 内核配置三件套:
BLK_DEV_INITRD、BLK_DEV_RAM、SQUASHFS,缺一不可;
- 内存地址计算务必严谨,建议画出内存布局图,避免重叠;
- U‑Boot 环境变量修改后立即保存,避免缓存干扰;
- 根据实际需求选择文件系统格式,Squashfs 适合只读内存运行,CPIO 更适合灵活的 initramfs。
💡 提示:总结也是让自己将原理、坑点牢记于心,同时也希望本文能为同类项目的开发者提供参考,让后来者少走弯路。
如果你也在做嵌入式 Linux、U‑Boot 相关的根文件系统改造,欢迎到 云栈社区 一起交流。