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 命令。这个命令会:
- 解析内核镜像头,获取入口地址和加载地址。
- 如果镜像是压缩的(如 zImage),先解压到加载地址。
- 设置 CPU 寄存器:r0=0,r1=机器类型 ID(老内核),r2=设备树地址(新内核)。
- 关闭 MMU 和 Cache(部分架构需要)。
- 跳转到内核入口地址。
跳转之后,U-Boot 的使命就结束了。CPU 开始执行内核的第一条指令——通常是 stext 或 _start。内核会重新初始化页表、中断向量、控制台,然后一步步启动整个系统。
这里有一个容易忽略的点:跳转前必须关闭中断。如果 U-Boot 阶段开了中断,但没有清理中断控制器,跳转到内核后,一个悬而未决的中断可能让内核在初始化早期就崩溃。我遇到过这种情况,最后在 U-Boot 的 board_init_r() 里加了一句 disable_interrupts() 才解决。
避坑指南:我曾经在一个项目里,U-Boot 跳转到内核后,内核打印完「Starting kernel ...」就死机了。用 JTAG 调试发现,是 U-Boot 没有关闭看门狗。内核启动初期来不及喂狗,看门狗超时复位了芯片。所以,跳转前务必关闭外部看门狗。
3.6 启动流程全景图
下面这张图,把整个启动流程串起来了。我建议你把它保存下来,调试时对照着看。
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 在执行代码。这个小技巧帮我省了不少时间。