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跑第一棒,初始化硬件、加载内核,然后把控制权交给内核。这个交接过程,全靠分区表来保证。

具体流程是这样的:

  1. 上电复位:CPU从Flash的0地址开始执行,通常是BootROM或SPL。
  2. SPL加载U-Boot:SPL初始化DDR,然后把U-Boot主体从Flash拷贝到RAM。
  3. U-Boot启动:U-Boot读取环境变量,解析bootcmd命令。
  4. 加载内核:U-Boot根据分区地址,从Flash读取内核镜像到RAM。
  5. 启动内核: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存储布局与启动流程 Flash 分区布局 SPL / BootROM U-Boot 主程序 环境变量 Linux Kernel (zImage / Image) 设备树 (.dtb) 根文件系统 (squashfs / ubifs) 用户数据 启动流程(接力赛) ① 上电 → CPU从0地址执行SPL 初始化时钟、DDR、串口 ② SPL从Flash加载U-Boot到DDR 读取分区表,找到U-Boot位置 ③ U-Boot读取环境变量 解析bootcmd、加载地址、root参数 ④ U-Boot加载内核+设备树到DDR 根据分区地址读取Flash数据 ⑤ 跳转到内核入口 → 内核接管 映射 关键:分区地址必须与U-Boot环境变量、内核命令行中的地址一致,否则启动失败

这张图左边是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和内核的存储关系,后面讲分区表修改和固件升级联动时,就不会觉得云里雾里了。记住一句话:分区表是嵌入式系统的骨架,骨架歪了,肉长再好也没用。