3. 启动流程详解:芯片上电启动、Bootloader、引导加载、跳转到主程序

芯片上电之后到底发生了什么?这个问题,我当年刚入行时也困惑了很久。看着开发板插上电源,LED 亮起来,程序就跑起来了——这中间的过程,就像一个黑盒子。今天咱们就把这个盒子拆开,一步步看清楚。

说白了,整个启动流程就是芯片从「裸金属」状态,到最终执行你的应用程序的过程。我习惯把它分成四个阶段:上电复位 → BootROM 执行 → Bootloader 加载 → 跳转到主程序。每个阶段都有它的使命,也有不少坑。

核心观点:启动流程的本质是「硬件自举 → 引导加载 → 系统初始化 → 应用执行」的链式接力。任何一个环节出问题,板子都起不来。

3.1 芯片上电复位:第一行代码从哪里来?

芯片一上电,内部电路并不稳定。电源电压从 0V 爬升到额定值需要时间,晶振起振也需要时间。所以芯片内部有一个 POR(Power-On Reset,上电复位) 电路,它负责在电压稳定之前,把整个芯片锁在复位状态。

等电压稳定后,POR 释放复位信号。CPU 开始从 复位向量(Reset Vector) 取指执行。这个地址是固定的,由芯片设计时决定。比如 ARM Cortex-M 系列,复位向量通常指向 0x00000000 或 0x08000000(取决于 BOOT 引脚配置)。

我记得第一次调试一块新板子时,焊好芯片,上电,用示波器量复位引脚——波形正常,但程序就是跑不起来。后来发现,是电源纹波太大,POR 电路反复触发复位。嗯,这算是一个经典坑:电源质量直接影响启动可靠性。

芯片架构 复位向量地址 启动介质
ARM Cortex-M3/M4 0x00000000 或 0x08000000 Flash / System Memory
ARM Cortex-A 系列 0x00000000 或 0xFFFF0000 ROM / NAND / eMMC
RISC-V (典型) 0x80000000 Flash / SPI Flash
X86 (UEFI) 0xFFFFFFF0 SPI Flash (BIOS)

小技巧:调试启动问题时,先确认复位引脚波形、电源纹波、晶振频率。这三个信号正常,再怀疑软件。我见过太多人一上来就查代码,结果发现是晶振虚焊。

3.2 BootROM:芯片自带的「最小引导程序」

CPU 从复位向量取到的第一条指令,通常存放在芯片内部的 BootROM 里。BootROM 是芯片出厂时固化的只读程序,用户改不了。它的任务很简单:检测启动介质,把下一级程序加载到 SRAM 或 DDR 中。

BootROM 会做什么?我总结了一下:

  • 初始化基础硬件:关闭看门狗、配置系统时钟(PLL)、初始化内部 SRAM。
  • 检测启动模式:根据 BOOT 引脚电平,决定从 NAND Flash、SPI Flash、SD 卡、UART 还是 USB 启动。
  • 加载下一级程序:从选定的介质中读取固定大小(比如 4KB 或 8KB)的数据到 SRAM,然后跳转过去执行。

你想想看,BootROM 本身很小,通常只有 4KB 到 32KB。它不可能完成复杂的初始化工作,所以它只负责「把接力棒交给下一棒」。这个下一棒,就是 SPL(Secondary Program Loader) 或者 Bootloader 的第一阶段。

注意:BootROM 的启动介质检测顺序是固定的。比如有些芯片先检测 SD 卡,再检测 SPI Flash。如果你同时插了 SD 卡和烧写了 SPI Flash,芯片可能从 SD 卡启动,而不是你预期的 SPI Flash。我曾经因为这个原因,调试了一整天——SD 卡里有个旧版本的测试程序,每次都被优先加载了。

3.3 Bootloader:从 SPL 到完整引导

Bootloader 是嵌入式系统的「操作系统引导管理器」。最常见的两个是 U-Boot(通用引导加载程序)和 Barebox。我个人习惯用 U-Boot,生态好,社区活跃,支持的芯片多。

Bootloader 通常分为两个阶段:

3.3.1 第一阶段(SPL / TPL)

SPL(Secondary Program Loader)是 BootROM 加载进来的小程序。它的任务:

  • 初始化 DDR 控制器(因为 BootROM 只初始化了内部 SRAM,容量太小)。
  • 从存储介质(NAND、eMMC、SD 卡)中加载完整的 U-Boot 到 DDR。
  • 跳转到 U-Boot 的入口地址。

SPL 的代码量必须严格控制,因为它被限制在 BootROM 加载的大小内(通常是 4KB 到 64KB)。我做过一个项目,SPL 编译出来 63KB,而芯片限制是 64KB——差一点就超了。最后硬是砍掉了一些调试打印才塞进去。

3.3.2 第二阶段(U-Boot 完整版)

U-Boot 完整版被加载到 DDR 后,开始执行更复杂的初始化:

  • 初始化串口、网络、USB、存储控制器。
  • 解析设备树(Device Tree),获取硬件配置信息。
  • 加载内核镜像(zImage / uImage)和设备树到 DDR 指定位置。
  • 设置内核启动参数(ATAG / 设备树 / cmdline)。
  • 跳转到内核入口。
// U-Boot 启动日志(典型输出)
U-Boot 2023.04 (Apr 10 2024 - 14:30:00 +0800)

CPU:   i.MX6ULL at 792 MHz
Board: MY-IMX6ULL-EK
DRAM:  512 MiB
MMC:   FSL_SDHC: 0, FSL_SDHC: 1
Net:   FEC [PRIME]
Normal Boot
Hit any key to stop autoboot:  0
## Booting kernel from Legacy Image at 12000000 ...
   Image Name:   Linux-5.15.32
   Image Type:   ARM Linux Kernel Image (uncompressed)
   Data Size:    4194304 Bytes = 4 MiB
   Load Address: 80008000
   Entry Point:  80008000
## Flattened Device Tree blob at 13000000
   Booting using the fdt blob at 0x13000000
   Loading Kernel Image
   Loading Device Tree to 9fff0000, end 9fffffff ... OK

Starting kernel ...

经验之谈:U-Boot 的 autoboot 延时(默认 2-3 秒)在生产测试时很烦人。我一般会把它改成 0,或者用 bootdelay=0 环境变量。但调试阶段建议保留,方便中断启动过程。

3.4 引导加载:内核和设备树是怎么被请进内存的?

U-Boot 加载内核和设备树的过程,说白了就是「从存储介质读到内存」。但这里有几个关键点:

  • 加载地址:内核镜像必须加载到指定的物理地址(比如 0x80008000)。这个地址由内核链接脚本决定,不能乱改。
  • 设备树地址:设备树(DTB)也要加载到内存,U-Boot 会把它的地址通过寄存器 r2 传递给内核。
  • 校验机制:U-Boot 支持 CRC32 校验,确保镜像没被损坏。我建议生产环境开启这个功能。

我记得有一次,内核加载后启动到一半就挂了。查了半天,发现是设备树地址和内核加载地址重叠了——U-Boot 把 DTB 加载到了内核镜像的范围内,内核解压时把 DTB 覆盖了。从那以后,我每次都会检查加载地址是否有冲突。

推荐做法:在 U-Boot 中设置 loadaddr=0x12000000 和 fdtaddr=0x13000000,两者间隔至少 16MB,避免重叠。

3.5 跳转到主程序:最后一步的「临门一脚」

U-Boot 完成所有准备工作后,执行 bootm 命令。这个命令会:

  1. 解析内核镜像头,获取入口地址和加载地址。
  2. 如果镜像是压缩的(如 zImage),先解压到加载地址。
  3. 设置 CPU 寄存器:r0=0,r1=机器类型 ID(老内核),r2=设备树地址(新内核)。
  4. 关闭 MMU 和 Cache(部分架构需要)。
  5. 跳转到内核入口地址。

跳转之后,U-Boot 的使命就结束了。CPU 开始执行内核的第一条指令——通常是 stext 或 _start。内核会重新初始化页表、中断向量、控制台,然后一步步启动整个系统。

这里有一个容易忽略的点:跳转前必须关闭中断。如果 U-Boot 阶段开了中断,但没有清理中断控制器,跳转到内核后,一个悬而未决的中断可能让内核在初始化早期就崩溃。我遇到过这种情况,最后在 U-Boot 的 board_init_r() 里加了一句 disable_interrupts() 才解决。

避坑指南:我曾经在一个项目里,U-Boot 跳转到内核后,内核打印完「Starting kernel ...」就死机了。用 JTAG 调试发现,是 U-Boot 没有关闭看门狗。内核启动初期来不及喂狗,看门狗超时复位了芯片。所以,跳转前务必关闭外部看门狗。

3.6 启动流程全景图

下面这张图,把整个启动流程串起来了。我建议你把它保存下来,调试时对照着看。

嵌入式系统启动流程全景图 1. 上电复位 (POR) 2. BootROM 执行 检测启动介质 加载 SPL 到 SRAM 3. SPL 初始化 DDR 初始化 DDR 控制器 加载 U-Boot 到 DDR 4. U-Boot 完整引导 初始化外设 加载内核 + DTB 5. 跳转到内核入口 设置 r0/r1/r2 关闭中断/看门狗 6. Linux 内核启动 初始化 MMU/Cache 挂载根文件系统 关键:每个阶段都依赖前一阶段完成初始化,任何一个环节失败都会导致启动中断 调试时建议从后往前排查——先确认内核是否被加载,再检查 Bootloader 是否执行

3.7 常见启动故障排查清单

最后,我整理了一份排查清单。你在项目里遇到启动问题,可以按这个顺序查:

现象 可能原因 排查方法
上电后无任何输出 电源不稳、晶振不起振、复位异常 示波器量电源/复位/晶振波形
BootROM 无输出 启动介质损坏、BOOT 引脚配置错误 检查 BOOT 引脚电平,换一片 Flash
SPL 加载失败 DDR 初始化参数错误、SPL 超尺寸 检查 DDR 时序配置,确认 SPL 大小
U-Boot 启动后死机 环境变量损坏、设备树错误 进入 U-Boot 命令行,用 printenv 检查
内核启动后 panic 内核与设备树不匹配、cmdline 错误 检查内核版本,确认 DTB 来源

我的习惯:新板子第一次上电,我会在 U-Boot 里加一个 led_blink() 函数,用 GPIO 控制 LED 闪烁。这样即使串口没输出,也能确认 CPU 在执行代码。这个小技巧帮我省了不少时间。