2. 嵌入式系统启动流程全景:从复位向量到用户空间
嵌入式系统的启动过程是一条从硬件复位到操作系统用户空间的完整执行链。理解这一全景图是分析启动时序、定位启动瓶颈、优化依赖关系的基础。本章将系统性地划分启动阶段,并定义每个阶段的关键里程碑。
2.1 启动阶段总览
嵌入式系统启动通常可划分为以下 6 个核心阶段,每个阶段都有明确的输入、输出和边界条件:
| 阶段编号 | 阶段名称 | 起始事件 | 结束里程碑 | 典型耗时量级 |
|---|---|---|---|---|
| 1 | 复位与ROM代码 | 硬件复位信号释放 | 跳转到Boot ROM入口 | 微秒级 |
| 2 | 一级引导加载 | Boot ROM执行 | SPL/TPL加载到SRAM | 毫秒级 |
| 3 | 二级引导加载 | SPL跳转到u-boot | u-boot命令行/自动启动 | 百毫秒级 |
| 4 | 内核解压与初始化 | u-boot加载内核镜像 | 内核挂载根文件系统 | 秒级 |
| 5 | 根文件系统挂载 | 内核调用init进程 | init进程启动 | 百毫秒级 |
| 6 | 用户空间初始化 | init进程执行 | 系统就绪/应用启动 | 秒级 |
关键认知: 启动时序优化的核心在于识别每个阶段的"等待依赖"——即前一个阶段必须完成哪些硬件或数据准备,后一个阶段才能开始。这些依赖关系决定了启动延迟的瓶颈所在。
2.2 复位向量与ROM代码阶段
当系统上电或复位信号释放后,CPU 从预定义的复位向量地址开始执行。对于 ARM Cortex-A 系列,复位向量通常位于 0x00000000 或 0xFFFF0000;对于 RISC-V,则位于 0x80000000 或芯片定义的 Boot ROM 区域。
此阶段的关键活动包括:
- 硬件自检(POR):电源稳定、时钟 PLL 锁定、内部 LDO 输出正常。
- Boot ROM 执行:芯片内部固化的只读代码,负责初始化最基本的硬件(如时钟、堆栈指针),并检测启动介质选择引脚。
- 启动介质识别:根据 eFuse 或 GPIO 电平,决定从 NAND Flash、eMMC、SD 卡、SPI NOR Flash 或 UART 启动。
- 加载一级引导程序:从选定介质读取 SPL(Secondary Program Loader)到内部 SRAM。
里程碑 M1:Boot ROM 完成并跳转到 SPL。 此时 CPU 已具备最小运行环境(时钟、堆栈、基础内存),但外部 DRAM 尚未初始化。
2.3 一级引导加载(SPL/TPL)阶段
SPL(Secondary Program Loader)是 u-boot 的轻量级前导程序,通常被限制在 32KB~128KB 以内,以适应有限的 SRAM 空间。对于更复杂的 SoC,可能引入 TPL(Tertiary Program Loader)作为中间层。
SPL 的核心职责:
- 初始化最小硬件:包括时钟树、串口(用于调试输出)、定时器。
- 初始化 DRAM 控制器:这是 SPL 最重要的任务——配置 DDR 时序、训练 PHY,使外部 DRAM 可用。
- 加载 u-boot 主程序:从启动介质将完整的 u-boot 镜像拷贝到 DRAM 中。
- 跳转到 u-boot:设置栈指针并跳转到 DRAM 中的 u-boot 入口。
// SPL 典型执行流程伪代码
void spl_main(void) {
// 1. 基础硬件初始化
clock_init(); // 设置系统时钟
uart_init(); // 初始化调试串口
timer_init(); // 启动系统定时器
// 2. DRAM 初始化(关键依赖)
ddr_init(); // 配置 DDR 控制器
ddr_training(); // DDR 读写训练
// 3. 加载 u-boot 到 DRAM
boot_device = detect_boot_device();
load_image(boot_device, CONFIG_SYS_UBOOT_BASE);
// 4. 跳转到 u-boot
jump_to_uboot(CONFIG_SYS_UBOOT_BASE);
}
常见陷阱: DDR 初始化失败是 SPL 阶段最常见的启动失败原因。由于 DDR 时序参数与硬件布线、温度、电压强相关,错误的配置会导致系统在 SPL 阶段"死锁",且无任何调试输出(因为串口可能依赖 DDR 初始化后的 DMA 通道)。
2.4 二级引导加载(u-boot)阶段
u-boot 是嵌入式 Linux 中最广泛使用的二级引导加载程序。与 SPL 相比,u-boot 拥有完整的命令行界面、文件系统支持和网络协议栈。
u-boot 阶段的执行流程:
- board_init_f():早期板级初始化,包括 GPIO、I2C、PMIC 配置。
- relocate_code():将 u-boot 自身从加载地址重定位到 DRAM 高地址区域,为内核腾出空间。
- board_init_r():后期板级初始化,包括环境变量加载、设备树解析。
- main_loop():进入命令行循环,等待用户输入或自动启动计数。
- bootm/booti/bootz:根据内核镜像格式,执行加载、解压、设备树传递和最终跳转。
里程碑 M2:u-boot 完成环境变量加载并进入自动启动倒计时。 此时系统已具备完整的硬件初始化,等待用户干预或自动加载内核。
2.5 内核解压与初始化阶段
u-boot 通过 bootm 命令将内核镜像加载到 DRAM 指定地址,并传递设备树(DTB)和启动参数。内核入口点开始执行后,经历以下子阶段:
| 子阶段 | 描述 | 关键函数 |
|---|---|---|
| 解压 | 自解压内核(zImage/Image.gz)到 DRAM | decompress_kernel() |
| 架构初始化 | 设置页表、MMU、中断向量、CPU 模式 | start_kernel() → setup_arch() |
| 内核子系统初始化 | 调度器、内存管理、中断子系统、定时器 | sched_init(), mm_init(), init_IRQ() |
| 驱动初始化 | 设备驱动模型、总线枚举、设备探测 | do_initcalls() |
| 挂载根文件系统 | 根据 root= 参数挂载根分区 | prepare_namespace() |
时序分析要点: 内核初始化阶段的耗时主要取决于
do_initcalls() 中驱动的初始化顺序。每个驱动模块的 initcall 级别(pure_initcall → fs_initcall → device_initcall → late_initcall)决定了其执行优先级,这也是启动时序优化的重点区域。
2.6 根文件系统挂载与用户空间初始化
内核挂载根文件系统后,执行第一个用户空间进程 /sbin/init(或由 init= 参数指定的程序)。用户空间初始化阶段包括:
- init 进程启动:读取
/etc/inittab或使用 systemd 的单元文件。 - 系统服务启动:网络服务、日志服务、硬件监控等。
- 应用层初始化:业务主程序、GUI 界面、通信协议栈。
里程碑 M3:用户空间第一个业务进程启动。 这是嵌入式系统启动时序分析的最终目标——系统从复位到应用就绪的总时间。
2.7 启动流程全景 SVG 图
下图展示了从复位向量到用户空间的完整启动流程,包含 6 个阶段及其关键依赖关系:
2.8 阶段间依赖关系矩阵
为了更精确地分析启动时序,下表列出了各阶段之间的关键依赖关系及其影响:
| 依赖方向 | 依赖内容 | 依赖类型 | 失败后果 |
|---|---|---|---|
| 阶段1 → 阶段2 | 启动介质识别、SPL 镜像完整性 | 数据依赖 | 系统无法加载任何代码,进入死循环或 Boot ROM 恢复模式 |
| 阶段2 → 阶段3 | DDR 初始化成功、u-boot 镜像校验通过 | 硬件依赖 + 数据依赖 | DDR 失败则系统挂死;镜像损坏则 u-boot 无法执行 |
| 阶段3 → 阶段4 | 设备树传递、内核镜像加载、启动参数设置 | 数据依赖 | 设备树错误导致内核 panic;内核镜像地址错误导致无法解压 |
| 阶段4 → 阶段5 | 根文件系统驱动(MMC/SD/NFS)、分区表识别 | 硬件依赖 + 数据依赖 | 内核无法挂载根文件系统,进入 kernel panic - not syncing |
| 阶段5 → 阶段6 | init 进程可执行文件存在、动态链接库完整 | 数据依赖 | init 进程崩溃,系统进入重启循环或内核 oops |
实战提醒: 在嵌入式产品开发中,超过 70% 的启动失败问题发生在阶段2(DDR 初始化)和阶段5(根文件系统挂载)。建议在 BSP 开发初期就为这两个阶段添加详细的调试日志和超时检测机制。
2.9 启动时序关键指标
在实际项目中,启动时序分析需要关注以下量化指标:
- 冷启动时间:从系统上电到用户空间应用就绪的总时间,通常要求 < 5 秒(消费类)或 < 1 秒(汽车/工业)。
- 热启动时间:从软件复位到系统就绪的时间,通常比冷启动快 30%~50%。
- 阶段耗时占比:典型分布为 SPL 5%、u-boot 15%、内核 40%、用户空间 40%。
- 串口输出时间戳:在关键函数入口/出口添加
printk或printf时间戳,是启动时序分析最基础的手段。