1. 嵌入式系统启动流程全景:从按下电源到用户界面的完整旅程

说实话,做了十几年嵌入式开发,我见过太多工程师只盯着自己那一亩三分地——搞bootloader的不知道根文件系统怎么挂载,写驱动的没看过内核怎么解压。结果呢?出了问题就抓瞎。

这一章,咱们把镜头拉远,看看整个启动流程的全貌。你想想看,从你按下电源键到屏幕上出现用户界面,这短短几秒里,芯片到底经历了什么?

核心认知: 嵌入式启动不是简单的"上电→跑程序",而是一场精心编排的接力赛——ROM代码把接力棒交给bootloader,bootloader交给内核,内核再交给根文件系统里的init进程。

1.1 启动流程的五个阶段

我把整个启动过程拆成五个阶段,每个阶段都有明确的分工。我个人习惯用这张图来跟团队新人讲解,一目了然。

嵌入式系统启动流程全景图 ① ROM代码 BootROM / 一级引导 ② SPL/MLO 二级引导器 ③ U-Boot 主引导加载程序 ④ Linux内核 解压 + 初始化 ⑤ 根文件系统挂载 → init进程 → 用户界面 挂载根文件系统 → 启动init → 启动服务 → 显示界面 各阶段关键动作: ① ROM代码:初始化时钟、PLL、DDR控制器,从NAND/eMMC/SD卡加载SPL ② SPL/MLO:初始化DDR(如果ROM没做完),加载U-Boot到DDR ③ U-Boot:初始化外设,加载内核和设备树到DDR,传递启动参数 ④ Linux内核:解压内核,初始化驱动,挂载根文件系统 ⑤ 根文件系统:启动init进程(PID=1),运行服务,启动GUI ⏱ 总耗时:通常2~15秒,取决于硬件性能和优化程度

1.2 第一阶段:ROM代码(BootROM)

芯片上电后,CPU会从一个固定地址开始执行。这个地址指向芯片内部固化的ROM代码,厂家出厂就写死了,你改不了。

ROM代码干了什么?

  • 初始化最基本的硬件:关闭看门狗、设置系统时钟、初始化CPU缓存
  • 初始化DDR控制器(有些芯片这一步留给SPL做)
  • 根据启动引脚的电平状态,决定从哪个设备加载下一级代码
  • 从NAND Flash、eMMC、SD卡或SPI NOR Flash读取SPL到内部SRAM
经验之谈: 我遇到过一块板子,上电后死活起不来。查了半天,发现是启动引脚虚焊,ROM代码读到的电平不对,一直尝试从空的SPI Flash启动。这种问题示波器都抓不到,只能看启动电流波形。

ROM代码的大小通常只有4KB~32KB,受限于芯片内部SRAM的大小。所以它只能做最基础的事情,然后赶紧把接力棒交出去。

1.3 第二阶段:SPL/MLO(二级引导器)

SPL(Secondary Program Loader)也叫MLO(MMC Loader),是ROM代码加载到SRAM里的小程序。它存在的意义很简单——ROM代码太小了,干不了重活。

SPL的核心任务:

  1. 完成DDR的完整初始化(如果ROM没做的话)
  2. 从存储设备加载U-Boot到DDR
  3. 跳转到U-Boot的入口地址

嗯,这里要注意:SPL本身也受限于SRAM大小,通常只有64KB~256KB。所以它必须精简,不能带太多功能。

避坑指南: 我曾经在某个项目里,SPL加载U-Boot时总是校验失败。查了两天才发现,是eMMC的块大小配置不对——SPL按512字节读,但U-Boot实际存储在2048字节块的区域。这种低级错误,一旦踩过就再也不会犯了。

1.4 第三阶段:U-Boot(主引导加载程序)

U-Boot到了DDR里,就像鱼入了水——空间大了,能干的事多了。它才是真正意义上的bootloader。

U-Boot的典型工作流程:

步骤 动作 说明
1 初始化串口、网卡、存储控制器 让你能看到打印信息,能通过网络下载文件
2 读取环境变量 bootargs、bootcmd等关键参数
3 加载内核镜像到DDR 从Flash、网络或SD卡读取zImage/Image
4 加载设备树(DTB) 告诉内核硬件布局
5 设置启动参数并跳转到内核 通过寄存器传递bootargs等

你想想看,U-Boot其实就是一个微型操作系统——它有命令行、文件系统支持、网络协议栈。我经常在U-Boot里用tftp下载内核调试,比反复烧写Flash快多了。

1.5 第四阶段:Linux内核启动

U-Boot跳转到内核入口后,内核开始接管系统。这个过程分两步:

第一步:解压内核

大多数嵌入式内核是压缩存储的(zImage或Image.gz)。内核开头有一段自解压代码,先把自身解压到DDR的另一个位置,然后跳过去执行。

// 内核启动时的关键打印信息(从串口看到的)
Uncompressing Linux... done, booting the kernel.
[    0.000000] Booting Linux on physical CPU 0x0
[    0.000000] Linux version 5.10.120 (builder@buildserver) (gcc 9.3.0)
[    0.000000] CPU: ARMv7 Processor [410fc075] revision 5 (ARMv7)
[    0.000000] Memory: 512MB

第二步:内核初始化

  • 设置页表、开启MMU
  • 初始化中断控制器、定时器
  • 遍历设备树,匹配并初始化所有驱动
  • 创建内核线程(kthreadd、kswapd等)
  • 挂载根文件系统(这是关键一步,后面章节会细讲)
  • 执行根文件系统里的init程序
关键点: 内核挂载根文件系统时,如果找不到正确的根设备或文件系统类型不匹配,内核会panic。这就是所谓的"Kernel panic - not syncing: VFS: Unable to mount root fs"——我当年第一次看到这个错误,整整折腾了两天。

1.6 第五阶段:根文件系统与用户界面

内核执行init程序(通常是/sbin/init),init进程的PID永远是1。它是所有用户进程的祖先。

init进程启动后:

  1. 读取配置文件(/etc/inittab或使用systemd的单元文件)
  2. 启动系统服务:网络、日志、SSH、看门狗等
  3. 启动显示服务器(如Qt的QWS、Wayland/Weston)
  4. 启动应用程序——用户界面终于出现了

说白了,从按下电源到看到界面,就是一场接力赛。每一棒都不能掉,每一棒的时间都要控制好。

优化思路: 我做过一个工业平板项目,客户要求冷启动到界面不超过5秒。我们做了三件事:① 去掉U-Boot里不必要的驱动和延时 ② 内核裁剪掉用不到的驱动 ③ 根文件系统用initramfs,省去挂载块设备的时间。最后做到了4.2秒。

1.7 各阶段耗时分布

给你一个典型数据,心里有个数:

阶段 典型耗时 优化空间
ROM代码 50~200ms 几乎无(芯片厂家固化的)
SPL 100~500ms 减少打印、优化存储读取
U-Boot 300ms~2s 裁剪驱动、去掉延时、使用快速存储
内核解压+初始化 500ms~3s 内核裁剪、使用LZ4压缩、设备树精简
根文件系统+应用 1~8s 使用initramfs、并行启动服务、延迟加载

看到没?根文件系统这块反而是最耗时的。这也是为什么我把这门课叫做《Bootloader分区与根文件系统协同实战》——这两者配合好了,启动速度能快一大截。