4. 启动流程与Bootloader:CPU上电启动流程、一级Bootloader(ROM Code)、二级Bootloader(U-Boot)、设备树(DTB)的传递
做嵌入式系统移植,第一个要啃的硬骨头就是启动流程。很多刚入行的朋友觉得这玩意儿太底层,想绕过去。我劝你别这么干——启动流程搞不清楚,后面遇到板子起不来,你连从哪儿查都不知道。
今天咱们就把CPU上电后那点事儿彻底捋清楚。从第一条指令到U-Boot加载内核,每一步我都会结合项目里踩过的坑来讲。
4.1 CPU上电启动流程:从Reset向量到第一条指令
CPU上电那一刻,它在干什么?说白了就三件事:
- 硬件复位:所有寄存器回到默认值
- 取第一条指令:从固定的复位地址读取代码
- 开始执行:跳转到启动代码
不同的CPU架构,复位地址不一样。我列个表,你一看就明白:
| 架构 | 复位地址 | 说明 |
|---|---|---|
| ARM Cortex-A | 0x00000000 或 0xFFFF0000 | 由硬件配置引脚决定 |
| ARM Cortex-M | 0x00000000(向量表) | 栈指针在0x0000,Reset_Handler在0x0004 |
| RISC-V | 0x80000000 或 0x00000000 | 由具体芯片设计决定 |
| x86 | 0xFFFFFFF0 | 传统BIOS启动地址 |
我记得第一次调一块Cortex-A7的板子,上电后死活没反应。查了半天,发现是复位引脚电平没拉对,CPU根本没启动。嗯,这种低级错误,犯过一次就记住了。
4.2 一级Bootloader(ROM Code):芯片出厂自带的"硬核"
一级Bootloader,也叫ROM Code,是芯片出厂时固化在内部ROM里的程序。用户改不了,也删不掉。
它的任务很明确:
- 初始化最基本的硬件:时钟、SRAM、看门狗
- 检测启动介质:从哪启动?NAND?eMMC?SD卡?UART?
- 加载二级Bootloader:把U-Boot从存储介质读到SRAM或DDR里
ROM Code的启动顺序,一般由芯片的启动引脚(Boot Mode Pins)决定。比如某款i.MX芯片,通过拉高/拉低几个GPIO,就能选择从SD卡还是NAND启动。
我曾经遇到一个坑:某款芯片的ROM Code只支持从FAT32分区加载U-Boot,但我把SD卡格式化成ext4了。结果板子一直卡在"waiting for boot device"……折腾了两天才发现是分区格式不对。
4.3 二级Bootloader(U-Boot):嵌入式世界的"BIOS"
U-Boot是目前最主流的二级Bootloader。它比ROM Code灵活得多,你可以修改、配置、甚至完全重写。
U-Boot的启动流程,我习惯分成四个阶段:
- SPL(Secondary Program Loader):如果芯片内部SRAM太小,U-Boot会先加载一个精简版——SPL。SPL初始化DDR,然后把完整的U-Boot从存储介质拷到DDR里。
- U-Boot完整版:在DDR里运行,初始化更多外设(网卡、USB、MMC等)。
- 环境变量加载:读取bootargs、bootcmd等配置。
- 启动内核:根据bootcmd指定的命令,加载内核镜像和设备树,跳转到内核入口。
下面这张图,把整个流程串起来了:
4.4 设备树(DTB)的传递:硬件描述的"身份证"
设备树(Device Tree)是Linux内核用来描述硬件的一种机制。它把板级信息从内核代码里剥离出来,变成一个独立的数据结构——DTB(Device Tree Blob)。
U-Boot加载内核时,必须把DTB的地址告诉内核。怎么传?
- 通过寄存器传参:ARM架构下,r2寄存器存放DTB的物理地址
- 通过bootm命令传参:U-Boot的bootm命令可以指定内核、DTB、ramdisk三个地址
举个例子,U-Boot里常见的启动命令:
# 从SD卡加载内核和DTB到DDR
load mmc 0:1 0x80000000 /boot/zImage
load mmc 0:1 0x83000000 /boot/board.dtb
# 启动内核,传递DTB地址
bootm 0x80000000 - 0x83000000
这里有个细节:bootm 0x80000000 - 0x83000000中间的-表示没有ramdisk。如果你有ramdisk,就写成bootm 0x80000000 0x82000000 0x83000000。
你想想看,如果没有设备树,每换一块板子就得重新编译内核。有了DTB,内核镜像可以通用,只需要换一个.dtb文件就行。这就是设备树最大的价值。
4.5 实战中的常见问题与排查思路
启动流程这块,我见过的问题五花八门。挑几个典型的说说:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 上电后完全无输出 | 复位电路问题、电源不稳、时钟未起振 | 示波器测复位引脚、电源纹波、晶振波形 |
| ROM Code阶段卡住 | 启动介质未识别、启动引脚配置错误 | 检查Boot Mode引脚电平、换已知好的SD卡 |
| U-Boot加载后无输出 | DDR初始化失败、U-Boot镜像损坏 | 用串口工具看打印信息、重新烧录U-Boot |
| 内核启动时崩溃 | DTB与内核不匹配、DTB地址错误 | 核对内核版本与DTB版本、检查bootm参数 |
我记得有一次调一块新板子,U-Boot能跑起来,但一加载内核就死。查了两天,最后发现是DTB里内存大小写错了——DDR实际512MB,DTB里配成了256MB。内核访问到256MB以外的地址,直接挂了。
bdinfo命令查看板子信息,包括内存布局、启动参数等。配合printenv看环境变量,基本能定位90%的启动问题。
嗯,启动流程这块内容不少,但核心就三条:知道CPU上电后去哪取代码、理解ROM Code和U-Boot的分工、搞懂DTB怎么传给内核。这三条捋顺了,大部分板子的启动问题你都能搞定。