嵌入式 Linux 启动流程详解 (以 ARM + U-Boot 为例)
嵌入式 Linux 启动流程详解 (以 ARM + U-Boot 为例)
对于嵌入式开发者而言,深入理解系统的启动流程至关重要。这不仅有助于进行底层驱动开发和系统移植,还能在遇到启动失败等问题时,快速定位和解决。本文将详细分解基于 ARM 架构的嵌入式 Linux 系统最常见的启动流程:BootROM -> U-Boot -> Kernel -> RootFS
。过段时间仔细研究一下Uboot,看看能不能给手上这块3506移植个Uboot试试。
启动流程总览
嵌入式设备的启动过程是一个环环相扣的链条,前一级加载器负责初始化基础硬件,并加载下一级更复杂的程序,直到最终启动完整的 Linux 操作系统。
graph TDsubgraph SoC 内部芯片A[电源开启] --> B(BootROM);endsubgraph 引导加载程序 (Bootloader)B --> |从 Flash/SD卡 加载| C[U-Boot SPL];C --> |初始化DDR| D[U-Boot 主体];endsubgraph 内核与用户空间D --> |执行 bootcmd| E[加载 Linux 内核 (zImage)];D --> |执行 bootcmd| F[加载设备树 (DTB)];E & F --> G{启动内核};G --> H[内核初始化硬件];H --> I[挂载根文件系统 (RootFS)];I --> J[执行第一个用户进程 /sbin/init];J --> K[启动系统服务与应用];end
1. BootROM - 芯片内的第一行代码
BootROM 是整个启动链的起点。
- 是什么:一段固化在 SoC (System on Chip) 芯片内部的只读存储器 (ROM) 中的程序。它由芯片制造商编写,用户无法修改,是上电后 CPU 执行的第一段代码。
- 核心职责:
- 最小化硬件初始化:进行最基础的硬件设置,如配置内部时钟、初始化用于加载下一阶段代码的存储接口(如 SD 卡控制器、eMMC、SPI Flash 等)。
- 加载外部引导程序:根据预设的启动介质顺序(通常由一组
BOOT_MODE
引脚的电平状态决定),从外部存储设备(Flash, eMMC, SD Card)中查找并加载第二阶段的引导加载程序(通常是 U-Boot 的一个小型版本SPL
)到芯片内部的 SRAM 中。 - 移交控制权:加载完成后,将 CPU 的执行权限交给这段刚刚加载进来的代码。
BootROM 的工作非常单一,就是为更强大的 Bootloader 搭建一个最小化的运行环境。
2. U-Boot - 通用引导加载程序
U-Boot (Universal Boot Loader) 是嵌入式领域应用最广泛的开源引导加载程序。它功能强大,通常分两个阶段执行,以适应嵌入式系统 SRAM 较小的限制。
2.1 第一阶段: U-Boot SPL (Secondary Program Loader)
- SPL 是什么:一个精简版的 U-Boot,主要目的是初始化 DDR 内存。由于 BootROM 加载 SPL 时使用的是芯片内部的 SRAM,而 SRAM 通常很小(几十到几百 KB),无法容纳完整的 U-Boot。
- 核心职责:
- 初始化 DDR 控制器:这是 SPL 最重要的任务。DDR 内存是运行 Linux 内核和应用程序所必需的大容量内存空间。
- 加载 U-Boot 主体:DDR 初始化完成后,SPL 会从外部存储设备中将完整的 U-Boot 镜像 (
u-boot.img
) 加载到 DDR 内存中。 - 跳转执行:将控制权移交给位于 DDR 中的 U-Boot 主体。
2.2 第二阶段: U-Boot 主体
这是我们通常所说的 U-Boot,它功能完善,为启动 Linux 内核提供了所有必要的准备。
- 核心职责:
- 全面的硬件初始化:初始化系统所需的各种外设,如串口(用于打印启动日志和提供命令行交互)、网络接口(用于网络启动和调试)、存储设备(eMMC, NAND)等。
- 设置内核启动参数 (
bootargs
):这是 U-Boot 的一个关键功能。它会设置一个名为bootargs
的环境变量,然后将其传递给 Linux 内核。这个参数字符串告诉内核一些重要的初始配置,例如:console=ttyS0,115200
:指定内核使用哪个串口作为控制台,以及波特率。root=/dev/mmcblk0p2 rootwait
:指定根文件系统的位置(例如在 SD 卡的第 2 个分区),并让内核等待设备就绪。rootfstype=ext4
:指定根文件系统的类型。
- 加载内核和设备树:根据
bootcmd
环境变量中定义的命令(或手动输入),从存储介质(或通过网络 TFTP)中将 Linux 内核镜像 (zImage
) 和设备树文件 (.dtb
) 加载到 DDR 内存的指定地址。 - 启动内核:执行
bootz
或bootm
命令,将设备树文件的内存地址传递给内核,然后跳转到内核的入口点,正式开始 Linux 系统的启动。
3. Linux Kernel - 操作系统的核心
内核接管控制权后,标志着真正的操作系统开始运行。
- 内核自解压:
zImage
是一个压缩的镜像,所以第一步是在内存中将自己解压出来。 - 解析设备树 (DTB):内核会读取 U-Boot 传递过来的 DTB 文件。DTB 描述了板级的所有硬件信息,内核根据这些信息来初始化对应的驱动程序。这使得内核代码可以与硬件解耦,一份内核源码可以适配不同的开发板。
- 初始化硬件:内核开始全面初始化由 DTB 描述的所有硬件设备,并建立起驱动模型、内存管理、进程调度等核心子系统。
- 挂载根文件系统 (RootFS):内核根据
bootargs
中的root
参数,找到并挂载根文件系统。- 在开发阶段,常常使用 NFS (Network File System),方便快速修改和调试。
- 在量产产品中,根文件系统通常存放在 eMMC 或 Flash 的一个分区上,格式为
ext4
,squashfs
(只读压缩),ubifs
(用于 NAND Flash) 等。
- 启动
init
进程:根文件系统被挂载后,内核会创建并运行第一个用户空间的进程/sbin/init
(其 PID 永远为 1)。内核的初始化工作到此结束,后续的用户空间初始化全权交由init
进程处理。
4. Init 与用户空间
init
进程是所有用户空间进程的“始祖”。它的任务是根据配置文件,将系统带入一个可用的状态。
- 常见的
init
程序:- BusyBox init:在资源受限的嵌入式系统中非常流行。它解析
/etc/inittab
文件,按顺序执行其中的脚本来启动系统服务。 - SystemV init:传统的
init
系统,同样使用/etc/inittab
和运行级别 (Runlevel) 脚本 (/etc/rc.d/
)。 - systemd:现代 Linux 发行版的主流选择,功能强大,并行启动速度快,但在嵌入式领域有时被认为过于复杂和庞大。
- BusyBox init:在资源受限的嵌入式系统中非常流行。它解析
- 执行流程:
init
进程会运行一系列启动脚本,这些脚本会:- 挂载其他必要的文件系统(如
/proc
,/sys
,/tmp
)。 - 配置网络。
- 启动系统日志、SSH 服务等后台守护进程。
- 最终,启动核心的应用程序。
- 挂载其他必要的文件系统(如
当用户的应用程序成功运行起来后,整个嵌入式 Linux 系统的启动流程便宣告完成。