嵌入式系统启动流程:从复位到内核

大家好,我是你们的Bootloader讲师。今天咱们来聊聊嵌入式系统上电后,到底发生了什么。说白了,就是从芯片复位到操作系统跑起来,这中间的一整套流程。

很多初学者觉得启动就是“上电→跑系统”,其实没那么简单。我刚开始做嵌入式那会儿,也以为Bootloader就是个搬运工,把内核从Flash搬到内存就完事了。直到有一次,板子死活起不来,查了三天才发现是复位向量表配错了位置……嗯,从那以后,我对启动流程的每个细节都不敢马虎。

2.1 启动流程全景图

先看一张整体流程图,把五个阶段串起来。你想想看,CPU从复位到执行用户程序,其实是一连串的“接力跑”——每一棒都有明确的任务。

核心观点:嵌入式启动不是一步到位,而是分阶段、分权限、分职责的递进过程。每一级Bootloader都在为下一级铺路。

嵌入式系统启动流程(五阶段) CPU复位向量 0x00000000 ROM代码 BootROM 一级Bootloader SPL / MLO 二级Bootloader U-Boot 内核启动 Linux Kernel 各阶段核心职责: ① CPU复位向量:硬件固定入口,跳转到ROM代码 ② ROM代码:芯片出厂固化的微型加载器,初始化时钟/存储控制器 ③ 一级Bootloader(SPL):极小体积,初始化DDR,加载二级Bootloader ④ 二级Bootloader(U-Boot):功能完整,支持文件系统/网络/命令行 ⑤ 内核启动:解压内核,挂载根文件系统,启动init进程

2.2 CPU复位向量——一切从这里开始

芯片上电或复位后,CPU会去一个固定地址取第一条指令。这个地址就是复位向量。对于ARM Cortex-A系列,通常是0x00000000或0xFFFF0000;对于RISC-V,一般是0x80000000。

我记得第一次看ARM手册时,被“异常向量表”搞得晕头转向。其实说白了,复位向量就是一张表的第一个条目。CPU复位后,硬件强制PC指向这个地址,然后开始执行。

实战经验:很多SoC允许通过外部引脚(如BOOT_MODE)选择启动介质。比如从NAND Flash启动,复位向量指向片内ROM;从SD卡启动,则可能映射到不同的地址。设计板子时,一定要先确认启动引脚的硬件连接。

2.3 ROM代码——芯片出厂自带的“小管家”

ROM代码,也叫BootROM,是芯片出厂时固化在片内ROM里的一段小程序。它的任务很简单:初始化最基本的硬件(时钟、存储控制器),然后根据启动介质选择策略,把下一级代码加载到SRAM中。

这里有个关键点:BootROM通常只有几KB到几十KB,功能极其精简。它不会初始化DDR——因为DDR初始化需要大量配置参数,ROM里放不下。所以BootROM的目标是把一级Bootloader(SPL)加载到片内SRAM里。

特性 BootROM SPL U-Boot
存储位置 片内ROM(只读) Flash特定分区 Flash文件系统或分区
大小 4KB ~ 64KB 16KB ~ 128KB 256KB ~ 2MB
运行环境 SRAM SRAM DDR
主要功能 加载SPL 初始化DDR,加载U-Boot 加载内核/文件系统
可修改性 不可修改 可更新 可更新

2.4 一级Bootloader(SPL)——承上启下的“小个子”

SPL,全称Secondary Program Loader,但习惯上叫它“一级Bootloader”。名字有点绕,你只要记住:它是BootROM加载的第一个用户可修改的程序。

SPL的核心任务就两个:初始化DDR和加载U-Boot。为什么它要做DDR初始化?因为BootROM太小,做不了;U-Boot太大,放不进SRAM。所以SPL必须先把DDR跑起来,然后把U-Boot从Flash搬到DDR里。

我在项目中遇到过一个问题:SPL里DDR时序参数配错了,导致U-Boot加载到DDR后,一运行就崩溃。查了两天才发现是某个寄存器少配了一位。嗯,从那以后我养成了一个习惯——每次改DDR参数,都会用示波器量一下DQS和CLK的相位关系。

避坑指南:我曾经因为SPL体积超限,导致BootROM加载失败。SPL必须严格控制在芯片SRAM大小以内(通常是64KB或128KB)。如果SPL太大,可以考虑裁剪功能,或者使用压缩方式加载。

2.5 二级Bootloader(U-Boot)——功能完整的“大管家”

U-Boot是目前最主流的二级Bootloader。它功能丰富:支持文件系统(FAT、ext4)、网络协议(TFTP、NFS)、命令行交互、环境变量存储等。说白了,U-Boot就是一个微型操作系统。

U-Boot的启动流程大致如下:

  1. 第一阶段(汇编):设置异常向量表,初始化CPU模式,关闭中断,设置栈指针
  2. 第二阶段(C语言):初始化串口、定时器、存储控制器,检测启动设备
  3. 第三阶段(命令行):读取环境变量,执行bootcmd命令,加载内核

我个人习惯在U-Boot里保留一个“救急”功能:如果正常启动失败,自动进入命令行模式,方便手动调试。这个设计救过我很多次——有一次文件系统损坏,就是靠U-Boot命令行从TFTP重新烧录才恢复的。

2.6 内核启动——最后的冲刺

U-Boot加载内核后,通过bootm或bootz命令跳转到内核入口。内核开始执行后,Bootloader的任务就结束了。

内核启动过程包括:

  • 解压内核:如果内核是压缩的(zImage),先自解压
  • 架构初始化:设置页表、MMU、中断控制器
  • 设备驱动初始化:根据设备树(DTB)初始化外设
  • 挂载根文件系统:从指定设备(如eMMC、NFS)挂载根分区
  • 启动init进程:通常是/sbin/init,然后启动用户空间服务

关键参数传递:U-Boot通过bootargs环境变量向内核传递启动参数,包括根文件系统位置、控制台设备、内存大小等。我曾经因为bootargs里写错了根设备,导致内核启动后挂载失败——屏幕上一堆VFS: Unable to mount root fs,看着就头疼。

2.7 本章小结

嵌入式启动流程,说白了就是一场接力赛:

  • CPU复位向量:起跑线,硬件固定
  • ROM代码:第一棒,芯片出厂自带
  • 一级Bootloader(SPL):第二棒,初始化DDR
  • 二级Bootloader(U-Boot):第三棒,加载内核
  • 内核启动:冲刺,进入操作系统