最近面试嵌入式工程师时,我常问一个问题:按下电源键到内核跑起来,中间到底发生了什么?大部分人的回答都停留在“先运行Bootloader,再加载内核”的层面。
那么内核被加载到哪里了?是直接加载到RAM里吗?为什么不直接从Flash里运行?MMU又是什么时候打开的?设备树是什么时候加载的?当追问这些细节时,很多人就开始含糊其辞了。
其实这个启动流程看似简单,深入下去却有不少门道。今天,我就结合RK3588这款SoC,把这些细节给大家梳理清楚。
1. 芯片上电那一刻
当你按下电源键,芯片内部究竟在做什么?
首先,芯片的电源管理单元会完成初始化,系统时钟开始运行。紧接着,芯片会执行一段内置的ROM代码。这段代码是芯片出厂时固化在硅片里的,无法修改,我们通常称之为一级Bootloader或ROM Bootloader。
以瑞芯微的高端八核SoC RK3588为例,其上电时,内置ROM代码会执行以下操作:
- 检测启动介质 — RK3588支持从eMMC、SD卡、SPI Flash等多种存储介质启动。ROM代码会按照预设的优先级逐个尝试检测。
- 读取Bootloader — 从检测到的存储介质的特定位置(例如eMMC的第64个扇区)读取Bootloader镜像到片内SRAM中。
- 校验和跳转 — 验证Bootloader镜像的校验和(如CRC32),验证通过后,跳转到SRAM中开始执行。
具体的流程可以概括如下:
RK3588启动流程(第一级):
上电 → ROM代码执行
↓
检测启动介质(eMMC/SD卡/SPI Flash)
↓
从对应位置读取Bootloader(通常是U-Boot或Rockchip的miniloader)
位置通常是:
- eMMC: 第64个扇区开始(0x20000字节)
- SD卡: 第64个扇区开始
- SPI Flash: 0x8000偏移处
↓
校验CRC32
↓
跳转到RAM中执行第二级Bootloader
你可能会问,为什么不能直接把完整的U-Boot加载进来?原因在于RK3588的片内SRAM容量有限,通常只有几百KB,而一个功能完整的U-Boot镜像可能超过1MB。因此,瑞芯微设计了一个小型的miniloader先行启动,完成最基础的初始化(尤其是DRAM初始化),然后再由它去加载完整的U-Boot。

二级Bootloader(U-Boot)
U-Boot是嵌入式Linux领域最主流的Bootloader。一旦ROM Bootloader将其加载到RAM并跳转执行,U-Boot就正式接管了系统。
此时系统的状态是怎样的呢?CPU处于特权模式(例如ARM架构的SVC模式),MMU尚未开启,CPU使用物理地址直接访问内存。
以RK3588平台为例,U-Boot的启动流程大致分为几个阶段:
U-Boot启动时的主要工作流程:
1. 汇编阶段(board_init_f)
├─ 关闭中断和缓存
├─ 初始化CPU寄存器(栈指针、帧指针等)
├─ 检测CPU类型
├─ 初始化时钟(RK3588需要配置PLL频率)
├─ 初始化DRAM(对RK3588很关键,支持LPDDR4/LPDDR5)
└─ 清空BSS段
2. 重定位阶段
├─ 计算U-Boot在RAM中的新位置
├─ 把U-Boot代码从eMMC/SD卡复制到RAM的高地址
├─ (RK3588上通常是0x4FFE0000附近)
└─ 更新地址符号表
3. C语言阶段(board_init_r)
├─ 初始化串口(此时才能看到U-Boot启动信息)
├─ 初始化eMMC/SD卡驱动
├─ 初始化USB(用于USB烧写模式)
├─ 初始化网络(支持网络启动)
└─ 加载内核和设备树
为什么要大费周章地初始化DRAM并重定位? 以RK3588为例,其DRAM容量可达8GB。在DRAM初始化完成前,U-Boot的代码仍在eMMC中运行,读取速度可能只有几十MB/s。一旦DRAM初始化完成(这个过程通常需要几百毫秒),U-Boot就会执行重定位,将自己从低速存储介质搬移到高速的RAM高地址区域,此后代码执行速度就能达到GHz级别。这不仅为后续快速加载内核和设备树提供了条件,也提供了充足的缓存空间。
在RK3588的启动日志中,你通常会看到这样的信息,标志着DRAM初始化成功:
DDR Version 1.08 20221121
LPDDR4X, 2112MHz
BW=32 Col=10 Bk=8 CS0 Row=16 CS=1 Die BW=16 Size=4096M
...
U-Boot 2022.11-rk (Jan 20 2026 - 12:00:00)
Model: Rockchip RK3588 Evaluation Board
DRAM: 4 GiB
在 board_init_r 阶段,U-Boot会按顺序调用一系列初始化函数,其流程示意图如下:

图示流程简要说明:
board_init_r:板级初始化主函数,按顺序执行所有初始化例程。
initr_dm:初始化驱动模型,扫描设备树节点并绑定对应的驱动程序。
initr_net:网络初始化,初始化板载的所有以太网接口。
run_main_loop:执行U-Boot的主循环,等待用户输入命令或执行自动启动脚本。
加载设备树
一个关键问题:设备树文件(.dtb)是什么时候加载的?
答案是:在U-Boot阶段加载的。
设备树并非内核镜像的一部分,而是由Bootloader准备并传递给内核的一份“硬件配置清单”。内核启动时,会从Bootloader传递的参数中获取设备树在内存中的地址,然后进行解析。
在RK3588的典型系统中,U-Boot会从eMMC的固定布局中加载内核和设备树:
eMMC布局(RK3588典型配置):
0x00000 ─────────────────────
Bootloader (miniloader)
0x20000 ─────────────────────
U-Boot镜像
0x80000 ─────────────────────
环境变量(可选)
0x100000 ────────────────────
内核镜像(Image或zImage)
0x400000 ────────────────────
设备树 (rk3588-evb.dtb)
0x500000 ────────────────────
根文件系统或其他
在U-Boot命令行中,手动启动的命令可能如下:
# U-Boot交互式命令(你可以手动输入这些)
load mmc 0 0x50000000 Image
load mmc 0 0x5f000000 rk3588-evb.dtb
bootm 0x50000000 - 0x5f000000
更常见的做法是在U-Boot环境变量 bootcmd 中配置自动启动命令:
bootcmd=load mmc 0 0x50000000 Image; load mmc 0 0x5f000000 rk3588-evb.dtb; bootm 0x50000000 - 0x5f000000
这背后涉及启动参数传递协议。在ARM平台上,通常遵循以下约定:
- X0寄存器:通常设置为0。
- X1寄存器:存储机器类型ID(Machine Type ID,较旧的启动方式使用)。
- X2寄存器:存储设备树在RAM中的起始地址(现代设备树启动方式)。
因此,当U-Boot即将跳转到内核入口点时,它必须确保这些寄存器的值被正确设置。内核启动后,其第一条指令就会读取X2寄存器,获得设备树地址(例如0x5f000000),并开始解析。
在U-Boot源码中,准备并传递这些参数的逻辑可能类似以下代码片段:
setup_start_tag(bd);
setup_memory_tags(bd); // 设置内存信息
setup_commandline_tag(bd, commandline); // 设置kernel参数
setup_initrd_tag(bd, initrd_start, initrd_end); // 如果有initrd
setup_end_tag(bd); // 结束标记


就这样,设备树的地址通过预先约定好的寄存器传递给了内核。这也解释了为什么如果设备树文件损坏或地址传递错误,内核启动会立即失败——它根本无法找到描述硬件的“地图”。
MMU的开启时机
接下来是一个容易混淆的概念:MMU什么时候打开的?
答案是:在内核自身启动的早期阶段,具体是在进入C语言main函数之前。
更详细的过程如下:
内核入口(汇编代码)
↓
检查ATAGS或设备树,获取内存等信息
↓
初始化页表(在RAM中建立虚拟地址到物理地址的映射关系)
↓
开启MMU和缓存
↓
虚拟地址空间生效,所有内存访问都通过MMU转换
↓
跳转到C代码入口(内核start_kernel函数)
↓
初始化各种内核子系统
这里有一个至关重要的细节:在开启MMU的前后,程序计数器(PC)必须指向能连续执行的同一段代码。
假设内核被加载到物理地址0x10000000,但内核期望运行在虚拟地址0xC0000000。如果直接开启MMU,PC指向的物理地址0x10000000瞬间会经过MMU转换成另一个未知的虚拟地址,若该映射不存在,CPU就会立即出错。
因此,内核在开启MMU时,必须预先设置好一段恒等映射,即确保物理地址0x10000000映射到虚拟地址0x10000000。这样,在MMU开启的瞬间,即使CPU开始使用虚拟地址,它访问的0x10000000经过MMU转换后仍然指向原来的物理内存,代码得以继续执行。待MMU完全生效后,内核再切换到最终的、完整的虚拟地址空间映射。
这就是内核启动早期汇编代码看起来比较复杂的原因之一——它需要小心翼翼地处理物理地址与虚拟地址的过渡,确保执行流不会断裂。理解这个过程,对于操作系统底层开发和调试至关重要。
Initrd和Initramfs的作用
启动日志中有时会出现“Initrd”或“Initramfs”字样,它们又扮演着什么角色?
这是一个解决“先有鸡还是先有蛋”问题的巧妙设计。Linux内核启动后,需要加载驱动程序并挂载根文件系统。但问题来了:访问磁盘需要磁盘驱动,而驱动本身存放在磁盘的文件系统里。这就形成了一个循环依赖。
解决方案是:由Bootloader将一个压缩的临时根文件系统镜像(Initramfs)加载到内存中。内核启动初期,先解压并使用这个内存中的文件系统。在这个迷你环境里,内核可以加载必要的驱动(如磁盘控制器、文件系统驱动),然后才有能力去识别和挂载位于物理存储设备上的真正根文件系统。
流程示意:
Bootloader加载的内容:
1. 内核镜像 → 0x10000000
2. 设备树 → 0x18000000
3. Initramfs → 0x18100000
内核启动:
1. 解压内核镜像
2. 开启MMU
3. 在Initramfs基础上启动,加载必要驱动
4. 挂载真正的根文件系统
5. 执行init进程
许多嵌入式系统启动时,在显示内核版本信息后有一段明显的停顿,通常就是内核在加载和初始化Initramfs中的各种驱动模块。
常见的启动问题与排查
在实际开发中,启动过程常常遇到各种问题。以下是几种典型情况及排查思路:
问题1:U-Boot启动卡住
这通常是DRAM初始化失败导致的。在RK3588上,若DRAM初始化失败,日志可能只打印到DDR初始化信息就停止了:
DDR Version 1.08 20221121
LPDDR4X, 2112MHz
BW=32 Col=10 Bk=8 CS0 Row=16 CS=1 Die BW=16 Size=4096M
... (此处卡住,无后续U-Boot提示)
可能原因:LPDDR4X/LPDDR5颗粒焊接或接触不良(对高频信号很敏感)、时钟配置错误(需检查编译生成的设备树或初始化代码)、电源供电不稳定。
排查方法:观察打印停止的位置。如果是在“DRAM init”之前就卡住,可能是更早的时钟或电源初始化问题。可以尝试在U-Boot早期代码中加入调试输出,或者使用JTAG进行单步调试。
问题2:内核启动后无控制台输出
这种情况多半是控制台串口未正确初始化。RK3588有多个UART,内核需要通过设备树或命令行参数明确使用哪一个作为控制台。
在RK3588的设备树中,关键配置通常包含在 chosen 节点和具体的UART节点中:


如果 stdout-path 指向的UART节点寄存器地址与硬件原理图不一致,或者该UART的引脚复用、时钟配置错误,内核就无法输出日志。
排查方法:仔细核对设备树中 chosen 节点的 stdout-path 属性,以及对应UART节点的 reg(寄存器地址)、clocks(时钟)、pinctrl(引脚控制)等配置是否与硬件设计相符。
问题3:Kernel panic
内核启动过程中发生崩溃,通常伴随类似以下错误:
[ 0.123456] Unable to handle kernel NULL pointer dereference
[ 0.123457] PC is at ...
[ 0.123458] Call trace:
可能原因很多:MMU页表映射错误、Initramfs格式损坏、关键驱动初始化失败等。
对于RK3588,一些常见的Panic原因包括:
- 内存映射错误,特别是GIC(通用中断控制器)等关键外设的地址映射不对。
- GPU(Mali-G710)或NPU初始化失败。
- 板级特定驱动初始化失败。
排查方法:仔细分析Panic打印的调用栈(Call trace),根据函数名定位出问题的驱动模块。可以在内核命令行中添加 quiet 参数减少无关输出,让错误信息更突出。
完整的启动时间线
最后,让我们以RK3588为例,串起从按下电源到进入Shell的完整时间线:
T=0ms 按下电源键
↓
T=1-5ms RK3588内置ROM代码从eMMC读取miniloader到SRAM
↓
T=5-20ms Miniloader初始化DDR(LPDDR4X或LPDDR5)
↓
T=20-50ms Miniloader加载完整的U-Boot到DDR
↓
T=50-100ms U-Boot启动
输出:
“DDR Version 1.08 20221121
LPDDR4X, 2112MHz
...
U-Boot 2022.11-rk (Jan 20 2026 - 12:00:00)
Model: Rockchip RK3588 Evaluation Board
DRAM: 4 GiB”
↓
T=100-200ms U-Boot初始化eMMC,加载内核和设备树
“load mmc 0 0x50000000 Image”
“load mmc 0 0x5f000000 rk3588-orangepi-5-plus.dtb”
↓
T=200-210ms U-Boot跳转到内核入口地址0x50000000
↓
T=210-220ms 内核汇编阶段,初始化页表,设置MMU和GIC
↓
T=220-300ms 内核C代码启动,初始化时钟、regulator、pinctrl
输出:
“Linux version 5.10.0 #1 SMP ... (gcc version 10.0.0)”
“Booting Linux on physical CPU 0x0000000000 [0x410fd034]”
“Kernel command line: console=ttyFIQ0,1500000 root=/dev/mmcblk0p2”
↓
T=300-600ms 初始化各个子系统(GIC中断控制器、时钟、调节器)
初始化GPU(Mali-G710)、NPU、PCIE等
输出大量的驱动初始化信息
↓
T=600-1000ms 加载initramfs,挂载根文件系统
执行init进程
↓
T>1000ms 系统进入用户空间,启动各个用户进程
“Started The Apache HTTP Server”
“Started Hostname Service”
...
整个过程通常耗时1到2秒,具体时间取决于eMMC速度、内核优化程度以及需要初始化的驱动数量。
总结
嵌入式系统的启动流程是一个环环相扣的精密过程。从固化的ROM代码到灵活的U-Boot,再到最终接管系统的Linux内核,每一步都为下一步搭建好舞台。理解Bootloader如何初始化硬件、加载并传递参数,内核又如何接过接力棒并建立完整的虚拟内存世界,是深入嵌入式系统开发的基石。
希望这篇详细的解析能帮你理清思路。纸上得来终觉浅,要真正吃透,最好的方法还是结合具体平台(如RK3588)的代码,动手跟踪调试一遍。如果在学习过程中遇到问题,欢迎到 云栈社区 与更多开发者交流探讨。
