第二章 嵌入式系统启动流程:从复位向量到操作系统启动的完整链路分析

做嵌入式开发这么多年,我见过不少工程师把精力全花在应用层代码上,一碰到系统起不来就抓瞎。说白了,启动流程就是嵌入式系统的「生命线」——从芯片上电那一刻起,到操作系统跑起来,中间每一步都有讲究。这一章,我就带你把这根线从头捋一遍。

2.1 复位向量:一切从这里开始

芯片上电后,CPU 会去一个固定的地址取第一条指令。这个地址就叫复位向量。不同架构的复位向量地址不一样:

架构 复位向量地址 说明
ARM Cortex-A 0x00000000 或 0xFFFF0000 由异常向量基址寄存器决定
ARM7/9 0x00000000 固定地址,不可重映射
x86 0xFFFFFFF0 靠近 4GB 地址空间顶端
RISC-V 0x80000000 或 0x00000000 由硬件配置决定

这里有个坑,我踩过。有一次做 Cortex-A7 的板子,复位向量默认指向 0x00000000,但我的 BootROM 在 0x00000000 放的是跳转指令,而异常向量表却在 0xFFFF0000。结果一触发中断就飞了。嗯,后来我养成了习惯——启动初期第一件事就是检查异常向量基址寄存器的值。

2.2 启动 ROM 阶段:芯片出厂自带的「小管家」

大部分现代 SoC 内部都有一小块 Mask ROM,出厂就固化好了代码。这段代码叫 BootROM,它干的事很固定:

  1. 初始化最基本的硬件——时钟、SRAM、看门狗
  2. 检测启动介质——从哪启动?NAND?eMMC?SD 卡?SPI Flash?
  3. 加载下一级代码——把 Bootloader 的第一阶段从存储介质搬到 SRAM 里
  4. 跳转执行——把控制权交给 Bootloader

关键点:BootROM 是只读的,你改不了它。所以它的代码必须足够小、足够稳定。我见过有些芯片的 BootROM 只支持 4 种启动介质,选错了板子就起不来。选型时一定要看芯片的 BootROM 支持列表。

2.3 Bootloader 第一阶段:汇编里的「黄金三秒」

Bootloader 通常分两个阶段。第一阶段用汇编写,因为这时候 C 环境还没准备好。它要干的事,说白了就三件:

  • 关中断——防止在初始化过程中被异常打断
  • 初始化 CPU 模式——设置栈指针、切换到 SVC 模式
  • 初始化关键硬件——时钟、DDR 控制器、串口

我习惯在第一阶段末尾加一段「自检灯」代码——点亮一个 LED,表示第一阶段跑通了。调试的时候特别管用,能快速定位问题出在前半段还是后半段。

@ U-Boot 第一阶段典型代码片段 (arch/arm/cpu/armv7/start.S)
reset:
    /* 设置 CPU 为 SVC 模式 */
    mrs     r0, cpsr
    bic     r0, r0, #0x1f
    orr     r0, r0, #0xd3
    msr     cpsr, r0

    /* 关中断 */
    cpsid   if

    /* 初始化栈指针 */
    ldr     sp, =CONFIG_SYS_INIT_SP_ADDR

    /* 跳转到 C 语言入口 */
    bl      board_init_f

我的经验:第一阶段代码越短越好。我见过有人把 DDR 初始化写得巨复杂,结果第一阶段代码体积超标,BootROM 的 SRAM 装不下。记住,第一阶段只做「不得不做」的事,其他都扔给第二阶段。

2.4 Bootloader 第二阶段:C 语言的主战场

第二阶段用 C 写,这时候 DDR 已经能用了,代码可以跑在更大的内存空间里。它要干的事就多了:

  • 初始化更多外设——网卡、USB、存储控制器
  • 建立内存映射——MMU 初始化、TLB 表设置
  • 加载内核镜像——从 Flash、网络或 SD 卡读取内核
  • 准备启动参数——设置 ATAG 或设备树
  • 跳转到内核——清理环境,跳转到内核入口

你想想看,第二阶段其实就是一个「小操作系统」——它有设备驱动、文件系统支持、网络协议栈。U-Boot 的第二阶段代码量通常在几十万行,比很多 RTOS 还大。

2.5 内核启动:从压缩镜像到进程调度

Bootloader 跳转到内核后,内核自己还要走一段路才能跑起来。这段路大致分几步:

  1. 解压缩——如果内核是压缩的(zImage),先自解压
  2. 架构初始化——设置页表、中断向量、CPU 特性检测
  3. 内核子系统初始化——调度器、内存管理、文件系统、网络栈
  4. 驱动初始化——按设备树或模块顺序加载驱动
  5. 挂载根文件系统——从指定设备挂载 /
  6. 启动 init 进程——用户空间的第一个进程

注意:内核启动过程中,如果根文件系统挂载失败,系统会直接 panic。我曾经遇到过一个问题——Bootloader 传的设备树里根设备路径写错了,内核死活找不到根分区。查了两天才发现是设备树里一个字符串多了一个斜杠。这种问题,printk 都救不了你。

2.6 完整启动链路图

下面这张图把整个流程串起来了。我建议你把它打印出来贴在工位上——调试启动问题的时候,对着图一步步排查,比瞎猜快得多。

嵌入式系统完整启动链路 芯片上电 / 复位 复位向量取指 BootROM 执行 Bootloader 第一阶段 (汇编) Bootloader 第二阶段 (C语言) 内核解压 / 架构初始化 内核子系统 / 驱动初始化 init 进程启动 说明: 红色 = 固件阶段 绿色/紫色 = Bootloader 橙色/青色 = 内核阶段 时间轴 T0: 上电 T1: BootROM 完成 T2: 第一阶段完成 T3: 第二阶段完成 T4: 内核初始化完成 T5: 用户空间就绪

2.7 常见启动失败场景与排查思路

启动出问题,别慌。我总结了一套排查思路,按阶段来:

现象 可能原因 排查方法
上电后无任何输出 复位电路问题、电源不稳、晶振不起振 示波器测复位引脚、电源纹波、晶振波形
BootROM 阶段卡死 启动介质选择错误、启动介质损坏 检查启动引脚电平、换一片 Flash 试试
Bootloader 第一阶段崩溃 DDR 初始化参数错误、栈溢出 用 JTAG 单步调试、检查 DDR 时序配置
Bootloader 第二阶段无法加载内核 存储驱动问题、分区表错误 在 Bootloader 命令行手动读 Flash 验证
内核启动后 panic 设备树错误、根文件系统缺失 开启 early printk、检查内核启动日志

我的习惯:每次做新板子,我都会在 Bootloader 里加一个「启动计时」功能——记录每个阶段花了多少毫秒。这样不仅能定位问题,还能优化启动速度。有一次我发现 BootROM 阶段花了 3 秒,查了半天发现是 Flash 的读取模式没配好,改成 Quad SPI 后直接降到 0.3 秒。

2.8 小结

嵌入式系统的启动链路,说白了就是一条「接力棒」——从硬件到固件,从固件到 Bootloader,从 Bootloader 到内核,最后到用户空间。每一棒交接的时候,都要保证状态干净、参数正确。我见过太多人只盯着内核那一端,忽略了前面的环节。其实大部分启动问题,都出在 BootROM 和 Bootloader 的交接处。