1. 启动流程概述:从CPU复位到操作系统内核启动的完整链路

大家好,我是你们的嵌入式系统讲师。今天咱们聊聊启动流程——说白了,就是芯片上电后,从“啥也没有”到“操作系统跑起来”的完整过程。

我记得刚入行那会儿,总觉得启动流程就是个黑盒子。板子一上电,灯一亮,系统就起来了。直到有一次调试一个死活起不来的板子,我才意识到:启动流程里的每一个细节,都可能成为你排查问题的关键

好,咱们把启动流程拆成三个阶段来看:BootROM阶段、Bootloader阶段、内核启动阶段。这三个阶段,就像接力赛一样,一棒接一棒。

1.1 第一阶段:BootROM——芯片出厂自带的“硬核”代码

CPU复位后,PC指针会指向一个固定的地址。这个地址,就是BootROM的入口。BootROM是固化在芯片内部ROM里的代码,你改不了它,也删不掉它。

核心要点:BootROM是芯片厂商写死的,它的任务只有一个——把下一阶段的代码(通常是Bootloader)加载到RAM里,然后跳过去执行。

具体来说,BootROM会做这几件事:

  • 初始化最基本的硬件:比如CPU的时钟、内存控制器(如果芯片内部有SRAM的话)。注意,这时候外部DDR还没初始化呢。
  • 检测启动介质:从哪启动?是NAND Flash、eMMC、SD卡,还是UART?BootROM会按照芯片预设的启动顺序去检测。
  • 加载Bootloader:从检测到的启动介质里,把Bootloader的前几KB(比如SPL或MLO)读到芯片内部的SRAM里。
  • 跳转到Bootloader:最后,修改PC指针,让CPU开始执行Bootloader的代码。

个人经验:我曾经在一个项目里,板子死活起不来。查了半天,发现是BootROM检测启动介质的顺序跟我们实际焊接的存储芯片不匹配。嗯,这种坑,你遇到了就知道有多头疼。

1.2 第二阶段:Bootloader——承上启下的“桥梁”

Bootloader,说白了就是一段“稍微复杂一点”的引导程序。它不像BootROM那么“硬”,但比操作系统内核要“轻”得多。

常见的Bootloader有U-Boot、GRUB、LILO等。在嵌入式领域,U-Boot是绝对的主流。

Bootloader的核心任务,我总结为三点:

  1. 初始化外部硬件:比如DDR内存、串口、网卡、Flash控制器等。这时候,系统才真正有了“大内存”可用。
  2. 加载操作系统内核:从存储介质(eMMC、NAND、网络等)里,把内核镜像读到内存中。
  3. 传递启动参数:把一些硬件信息、启动参数(比如根文件系统在哪、控制台用哪个串口)告诉内核。

你想想看,BootROM只初始化了芯片内部那点SRAM,通常只有几十KB到几百KB。而Bootloader要初始化DDR,可能几百MB甚至几GB。这就是为什么Bootloader比BootROM复杂得多的原因。

注意:Bootloader本身也分阶段。比如U-Boot就有SPL(Secondary Program Loader)和U-Boot proper两个阶段。SPL负责初始化DDR,然后加载完整的U-Boot。为什么这么设计?因为DDR初始化需要更大的代码量,而BootROM能加载的代码大小有限。

1.3 第三阶段:内核启动——操作系统正式“接管”系统

Bootloader跳转到内核入口后,内核就开始执行了。这个阶段,我习惯把它再细分成两步:

1.3.1 内核自解压与早期初始化

大多数嵌入式内核镜像是压缩的(比如zImage)。内核入口的第一件事,就是自解压。解压完成后,CPU开始执行真正的内核代码。

早期初始化阶段,内核会做:

  • 设置页表,开启MMU(内存管理单元)
  • 初始化中断控制器
  • 初始化定时器
  • 初始化控制台(这时候你就能看到内核打印信息了)

我记得有一次调试,内核打印到一半就卡住了。我盯着串口输出看了半天,发现是中断控制器初始化时,某个外设的中断号配置错了。这种问题,没有对启动流程的深入理解,根本无从下手。

1.3.2 设备驱动加载与初始化

这是咱们这门课的核心内容。内核在早期初始化之后,会开始枚举总线上的设备,加载对应的驱动程序。

这个过程,我简单说一下:

阶段 做的事 关键函数/机制
设备树解析 解析DTB(设备树二进制),获取硬件拓扑信息 unflatten_device_tree()
总线枚举 遍历设备树,为每个节点创建device对象 of_platform_populate()
驱动匹配 将device与driver进行匹配 platform_match()
驱动初始化 调用驱动的probe函数,完成设备初始化 driver->probe()

说白了,内核就是通过设备树知道“板子上有哪些硬件”,然后找到对应的驱动,让它们“跑起来”。

避坑指南:我曾经在一个项目里,设备树里一个GPIO的pinmux配置错了。结果驱动加载时,probe函数返回了-ENODEV。查了两天才发现,是设备树里一个逗号写成了分号。嗯,设备树的语法检查,一定要做仔细了。

1.4 三个阶段的关系——一张图说清楚

虽然咱们不用图片,但我用文字给你描述一下:

CPU复位
   ↓
BootROM(固化在芯片ROM里)
   ├── 初始化内部SRAM
   ├── 检测启动介质(eMMC/NAND/UART等)
   ├── 加载Bootloader到SRAM
   └── 跳转到Bootloader
        ↓
Bootloader(如U-Boot)
   ├── 初始化DDR、串口、网卡等
   ├── 加载内核镜像到DDR
   ├── 传递启动参数(ATAGS或设备树)
   └── 跳转到内核入口
        ↓
内核启动
   ├── 自解压
   ├── 早期初始化(MMU、中断、定时器)
   ├── 设备驱动加载与初始化
   └── 启动init进程(用户空间开始)

你看,整个链路就像一条流水线。每一阶段都只做自己该做的事,然后把接力棒交给下一阶段。

我的建议:如果你刚开始接触嵌入式系统,我建议你先从Bootloader入手。因为Bootloader是你能直接接触到的、可修改的、最底层的代码。通过修改Bootloader,你能直观地感受到硬件初始化的每一个步骤。我自己就是这么过来的。

1.5 小结

这一章,咱们把启动流程的完整链路捋了一遍。从CPU复位到内核启动,三个阶段各司其职:

  • BootROM:芯片出厂自带的“硬核”代码,负责加载Bootloader
  • Bootloader:承上启下的桥梁,负责初始化硬件、加载内核
  • 内核启动:操作系统正式接管,完成设备驱动加载与初始化

下一章,咱们会深入BootROM的细节,聊聊它是怎么检测启动介质、怎么加载Bootloader的。到时候,我会分享一个我在调试SPL时遇到的“诡异”问题——嗯,保证让你大开眼界。

好,今天就到这儿。我是你们的讲师,咱们下章见。