找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2515

积分

0

好友

322

主题
发表于 18 小时前 | 查看: 0| 回复: 0

最近面试嵌入式工程师时,我常问一个问题:按下电源键到内核跑起来,中间到底发生了什么?大部分人的回答都停留在“先运行Bootloader,再加载内核”的层面。

那么内核被加载到哪里了?是直接加载到RAM里吗?为什么不直接从Flash里运行?MMU又是什么时候打开的?设备树是什么时候加载的?当追问这些细节时,很多人就开始含糊其辞了。

其实这个启动流程看似简单,深入下去却有不少门道。今天,我就结合RK3588这款SoC,把这些细节给大家梳理清楚。

1. 芯片上电那一刻

当你按下电源键,芯片内部究竟在做什么?

首先,芯片的电源管理单元会完成初始化,系统时钟开始运行。紧接着,芯片会执行一段内置的ROM代码。这段代码是芯片出厂时固化在硅片里的,无法修改,我们通常称之为一级BootloaderROM Bootloader

以瑞芯微的高端八核SoC RK3588为例,其上电时,内置ROM代码会执行以下操作:

  1. 检测启动介质 — RK3588支持从eMMC、SD卡、SPI Flash等多种存储介质启动。ROM代码会按照预设的优先级逐个尝试检测。
  2. 读取Bootloader — 从检测到的存储介质的特定位置(例如eMMC的第64个扇区)读取Bootloader镜像到片内SRAM中。
  3. 校验和跳转 — 验证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。

嵌入式SoC启动流程架构图

二级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会按顺序调用一系列初始化函数,其流程示意图如下:

U-Boot board_init_r 阶段初始化流程图

图示流程简要说明:

  • 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);        // 结束标记

U-Boot bootm 命令准备启动参数的代码片段

U-Boot 跳转到内核前的清理与跳转代码

就这样,设备树的地址通过预先约定好的寄存器传递给了内核。这也解释了为什么如果设备树文件损坏或地址传递错误,内核启动会立即失败——它根本无法找到描述硬件的“地图”。

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节点中:

RK3588设备树中 chosen 节点与 aliases 配置

RK3588设备树中 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)的代码,动手跟踪调试一遍。如果在学习过程中遇到问题,欢迎到 云栈社区 与更多开发者交流探讨。

电量从1%充到100%的趣味动图




上一篇:苹果新款智能家居中枢曝光:搭载可旋转屏,AI交互是亮点
下一篇:嘉立创EDA面板设计实战:文字添加、3D预览与图层管理
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-1 20:43 , Processed in 0.488096 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表