4. 启动流程与Bootloader:CPU上电启动流程、一级Bootloader(ROM Code)、二级Bootloader(U-Boot)、设备树(DTB)的传递

做嵌入式系统移植,第一个要啃的硬骨头就是启动流程。很多刚入行的朋友觉得这玩意儿太底层,想绕过去。我劝你别这么干——启动流程搞不清楚,后面遇到板子起不来,你连从哪儿查都不知道。

今天咱们就把CPU上电后那点事儿彻底捋清楚。从第一条指令到U-Boot加载内核,每一步我都会结合项目里踩过的坑来讲。

4.1 CPU上电启动流程:从Reset向量到第一条指令

CPU上电那一刻,它在干什么?说白了就三件事:

  1. 硬件复位:所有寄存器回到默认值
  2. 取第一条指令:从固定的复位地址读取代码
  3. 开始执行:跳转到启动代码

不同的CPU架构,复位地址不一样。我列个表,你一看就明白:

架构 复位地址 说明
ARM Cortex-A 0x00000000 或 0xFFFF0000 由硬件配置引脚决定
ARM Cortex-M 0x00000000(向量表) 栈指针在0x0000,Reset_Handler在0x0004
RISC-V 0x80000000 或 0x00000000 由具体芯片设计决定
x86 0xFFFFFFF0 传统BIOS启动地址

我记得第一次调一块Cortex-A7的板子,上电后死活没反应。查了半天,发现是复位引脚电平没拉对,CPU根本没启动。嗯,这种低级错误,犯过一次就记住了。

小提示: 拿到新板子,第一件事就是用示波器看复位引脚波形。上电后复位信号必须保持足够时间(通常几十毫秒),否则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是芯片厂商写的,你不需要改它,但必须知道它怎么工作。因为你的U-Boot必须放在ROM Code能识别的位置,格式也得对。

我曾经遇到一个坑:某款芯片的ROM Code只支持从FAT32分区加载U-Boot,但我把SD卡格式化成ext4了。结果板子一直卡在"waiting for boot device"……折腾了两天才发现是分区格式不对。

4.3 二级Bootloader(U-Boot):嵌入式世界的"BIOS"

U-Boot是目前最主流的二级Bootloader。它比ROM Code灵活得多,你可以修改、配置、甚至完全重写。

U-Boot的启动流程,我习惯分成四个阶段:

  1. SPL(Secondary Program Loader):如果芯片内部SRAM太小,U-Boot会先加载一个精简版——SPL。SPL初始化DDR,然后把完整的U-Boot从存储介质拷到DDR里。
  2. U-Boot完整版:在DDR里运行,初始化更多外设(网卡、USB、MMC等)。
  3. 环境变量加载:读取bootargs、bootcmd等配置。
  4. 启动内核:根据bootcmd指定的命令,加载内核镜像和设备树,跳转到内核入口。

下面这张图,把整个流程串起来了:

嵌入式系统启动流程总览 CPU上电复位 一级Bootloader (ROM Code) 初始化时钟/SRAM/看门狗 检测启动介质 (SD/eMMC/NAND/UART) 根据Boot Mode引脚选择 SPL (若需要) 初始化DDR,加载完整U-Boot U-Boot完整版 初始化外设,加载环境变量 加载内核 + DTB → 跳转 bootm / booti 命令执行 注意 • 不同芯片ROM Code实现不同 • SPL不是必须的,取决于SRAM大小 • DTB必须与内核版本匹配 常见启动介质 • SD/MMC卡 • eMMC • NAND Flash • SPI NOR Flash
避坑指南: 我曾经在移植U-Boot时,SPL编译出来太大了,超过了芯片内部SRAM的大小(只有64KB)。结果SPL加载到一半就崩了。解决办法是裁剪SPL功能,只保留DDR初始化和最基本的存储驱动。

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地址必须与内核约定的内存区域不重叠。我习惯把内核放在DDR起始地址+偏移处,DTB放在内核后面一段安全距离。比如DDR从0x80000000开始,内核放0x80000000,DTB放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以外的地址,直接挂了。

调试技巧: U-Boot里可以用bdinfo命令查看板子信息,包括内存布局、启动参数等。配合printenv看环境变量,基本能定位90%的启动问题。

嗯,启动流程这块内容不少,但核心就三条:知道CPU上电后去哪取代码、理解ROM Code和U-Boot的分工、搞懂DTB怎么传给内核。这三条捋顺了,大部分板子的启动问题你都能搞定。