1. 嵌入式存储布局概述:Flash类型、分区概念、U-Boot与内核的存储关系
做嵌入式开发这么多年,我见过不少新手拿到一块板子,第一件事就是急着烧系统。结果呢?要么把U-Boot刷没了,要么内核和根文件系统打架。说白了,就是没搞懂存储布局这回事。
今天咱们就从最基础的东西聊起。你想想看,一块嵌入式板子通电后,CPU怎么知道该从哪里执行代码?Flash里那么多数据,谁先谁后?分区又是干嘛用的?这些搞清楚了,后面改分区表、做固件升级才不会翻车。
1.1 嵌入式系统中的Flash类型
嵌入式设备里,存储介质主要分两大类。我习惯把它们比作「短跑选手」和「马拉松选手」。
| 特性 | NOR Flash | NAND Flash |
|---|---|---|
| 读取速度 | 快(支持XIP,芯片内执行) | 较慢(需先拷贝到RAM) |
| 写入速度 | 慢 | 快 |
| 擦除方式 | 按扇区擦除(通常64KB~128KB) | 按块擦除(通常128KB~2MB) |
| 坏块管理 | 不需要 | 必须要有(出厂就有坏块) |
| 典型容量 | 1MB ~ 64MB | 128MB ~ 8GB+ |
| 典型用途 | U-Boot、SPL、环境变量 | 内核、根文件系统、用户数据 |
关键点:NOR Flash支持XIP(eXecute In Place),CPU可以直接在Flash上跑代码,不需要先搬到RAM里。这就是为什么U-Boot的SPL(第一阶段引导)通常放在NOR里。NAND不行,它必须先把代码读到RAM才能执行。
我在一个工业控制项目里遇到过这么个坑:客户选了颗大容量NAND,觉得便宜。结果U-Boot死活起不来,查了半天才发现——NAND的前4KB有坏块,而SPL正好放在那里。后来我强制在NAND控制器里做了坏块跳过,才解决问题。嗯,选型的时候一定要想清楚:你的引导代码放哪儿?
1.2 分区概念:为什么需要分区?
分区,说白了就是把Flash切成几块,各干各的活。你总不能让U-Boot、内核、文件系统都挤在一起吧?那不乱套了。
一个典型的嵌入式分区布局长这样:
+------------------+ <-- Flash起始地址 (0x00000000)
| SPL / BootROM | 第一阶段引导程序
+------------------+
| U-Boot | 主引导程序
+------------------+
| U-Boot Env | 环境变量(启动参数、IP地址等)
+------------------+
| Kernel | Linux内核镜像 (zImage/Image)
+------------------+
| Device Tree | 设备树文件 (.dtb)
+------------------+
| Rootfs | 根文件系统 (squashfs/ext4/ubifs)
+------------------+
| User Data | 用户数据区(可读写)
+------------------+ <-- Flash结束地址
我的习惯:每个分区之间留4KB~64KB的空白区域,叫「guard zone」。为什么?防止分区表写错时,一个分区的数据擦到隔壁去。我曾经因为分区边界没对齐,升级内核时把U-Boot环境变量给抹了,板子直接变砖。从那以后,guard zone成了我的标配。
分区的大小不是拍脑袋定的。你得算清楚:
- U-Boot:一般512KB~2MB,看功能多少。如果加了网卡驱动、文件系统支持,会更大。
- 环境变量:通常64KB~256KB。别给太小,不然保存不了几个参数。
- 内核:压缩后2MB~8MB,解压后可能10MB+。
- 设备树:几十KB到几百KB,别省这点空间。
- 根文件系统:这个是大头,从几MB到几百MB都有可能。
1.3 U-Boot与内核的存储关系
U-Boot和内核之间,说白了就是「接力赛」的关系。U-Boot跑第一棒,初始化硬件、加载内核,然后把控制权交给内核。这个交接过程,全靠分区表来保证。
具体流程是这样的:
- 上电复位:CPU从Flash的0地址开始执行,通常是BootROM或SPL。
- SPL加载U-Boot:SPL初始化DDR,然后把U-Boot主体从Flash拷贝到RAM。
- U-Boot启动:U-Boot读取环境变量,解析bootcmd命令。
- 加载内核:U-Boot根据分区地址,从Flash读取内核镜像到RAM。
- 启动内核:U-Boot跳转到内核入口地址,内核接管系统。
注意:U-Boot和内核之间通过「启动参数」传递信息。比如内核在哪个分区、根文件系统在哪个分区、控制台用哪个串口。这些参数存在U-Boot的环境变量里。如果你改了分区表,却没更新环境变量里的地址,内核就找不到根文件系统——板子会卡在「Kernel panic - not syncing: VFS: Unable to mount root fs」。
我记得有一次帮客户做OTA升级,他们改了内核分区的大小,从4MB扩到6MB。结果升级后所有设备都起不来了。为什么?因为U-Boot的环境变量里,内核加载地址还是旧的。内核确实写进去了,但U-Boot从错误的位置读数据,读出来的全是乱码。从那以后,我每次改分区表,都会同步检查三样东西:
- 分区表本身(mtdparts)
- U-Boot环境变量里的加载地址
- 内核命令行里的root参数
1.4 一张图看懂存储布局
下面这张图,把上面讲的东西串起来了。你盯着看30秒,应该就能理解整个存储布局的逻辑。
这张图左边是Flash里的分区布局,右边是启动流程。你注意看那些虚线——每个启动步骤都对应着从特定分区读取数据。如果分区地址变了,U-Boot的环境变量没跟着改,虚线就断了,板子就起不来了。
1.5 避坑指南:我踩过的几个坑
坑一:分区大小没对齐擦除块
NOR Flash的擦除单位是扇区(比如64KB),NAND是块(比如128KB)。如果你分区大小不是擦除块的整数倍,擦除时就会擦到隔壁分区。我曾经把内核分区设为3MB,但NAND的块大小是128KB,3MB ÷ 128KB = 24块,刚好对齐。但如果你设成3.1MB,那就多出来12KB,这12KB和下一个分区共享一个块——擦除时一起没了。
坑二:环境变量分区太小
U-Boot环境变量默认大小是16KB,但如果你加了大量启动参数、网络配置、升级脚本,很容易超。超了会怎样?U-Boot写环境变量时不会报错,但重启后读回来的数据是乱的。我建议至少给256KB,省心。
我的经验:做分区表设计时,先画一张像上面那样的布局图,标清楚每个分区的起始地址、大小、擦除块大小。然后拿着这张图去写mtdparts和U-Boot环境变量。这样能避免90%的「低级错误」。
好了,这一章的内容就这些。你理解了Flash类型、分区概念、U-Boot和内核的存储关系,后面讲分区表修改和固件升级联动时,就不会觉得云里雾里了。记住一句话:分区表是嵌入式系统的骨架,骨架歪了,肉长再好也没用。