从上电到系统就绪:ARM+U-Boot 嵌入式 Linux 启动流程

原作者:Linux教程
原文地址:https://mp.weixin.qq.com/s/frX6swZHa6jAfNvfwLN-Cg

想在嵌入式界混得风生水起?一定要把系统启动流程搞明白,尤其是ARM架构+U-Boot这套目前最主流的组合。

这不仅直接关系到底层驱动的高效开发与系统的顺利移植,更是在遭遇启动异常等棘手问题时,能够迅速排查并解决问题的关键所在。

今天就带大家从头到尾,剖析基于 ARM 架构的嵌入式 Linux 系统最为典型的启动过程:BootROM → U-Boot → Kernel → RootFS。

一、启动流程总览

嵌入式设备的启动过程,前一级加载器负责初始化基础硬件,并加载下一级更复杂的工具,直到最终启动完整的 Linux 操作系统。

👉 第一棒:BootROM(芯片自带,启动发令员)

👉 第二棒:U-Boot(启动管家,衔接硬件与内核)

👉 第三棒:Linux内核(系统框架搭建者)

👉 第四棒:根文件系统+用户空间(应用载体)

二、启动完整链路

2.1 从 SoC 上电开始:Boot ROM 的第一脚

ARM SoC 上电后,程序计数器直接指向一个固定的 reset vector (通常是 0xFFFFFFF0 或厂商定义的内部 ROM 地址)。这时硬件还是一片沉静,DRAM 没初始化,外部存储器也读不了。Boot ROM(芯片出厂固化的代码)接管一切,它会先做最基本的时钟和电源域配置,然后根据引脚状态(BOOT_MODE 引脚)决定从哪里加载下一阶段代码------常见的是 eMMC、SD 卡、NAND 或 SPI NOR。

在我的项目里,用的是 eMMC 启动。Boot ROM 会从 eMMC 的特定偏移(比如 sector 0 或厂商规定的位置)读取 SPL(Secondary Program Loader)。SPL 体积很小,通常只有几十 KB,能塞进 SoC 的内部 SRAM 或 OCRAM 里运行。这一步如果失败,串口什么都看不到,整板就像死机一样。生产线上遇到过一批板子突然不亮,测下来是 eMMC 前几个 sector 被擦坏了,Boot ROM 读不到 SPL,直接卡死。

排错思路:用示波器或逻辑分析仪抓 BOOT_MODE 引脚电平,确认启动模式;或者用厂商提供的 USB 下载工具(比如 NXP 的 MFGTool 或 imx_usb)强制下载 SPL 测试。如果怀疑存储介质损坏,换一张干净的 SD 卡做对比是最快的。

2.2 SPL:从 SRAM 到 DRAM 的桥梁

SPL 的核心任务就是初始化 DRAM。没有内存,后面的代码根本跑不起来。它会配置 DDR 控制器、时序参数(这些参数往往来自厂商的 DDR training 工具或 board-specific 文件),然后把完整的 U-Boot 镜像加载到 DRAM 指定地址,再跳转过去。

ARM64 系统里,流程经一般是 SPL → ARM Trusted Firmware (TF-A BL31) → U-Boot。TF-A 负责安全世界初始化、EL3 到 EL2 的切换、电源管理等。SPL 加载 BL31 后,BL31 再把 U-Boot 作为 BL33 启动。

我遇到过一次在新批次 DDR 颗粒更换后,SPL 初始化内存时频繁 crash,现象是串口输出到 "DRAM:" 就卡住或乱码。查下来是 DDR training 参数没更新,timing 不匹配。生产环境里这种事很常见------供应商换颗粒不通知,或者温度范围变化导致 margin 不足。

具体命令与调试

  • 编译时开启 CONFIG_DEBUG_UART 和 earlycon,让 SPL 阶段就能看到日志。

  • 用 print 或 md 命令查看内存内容验证加载是否正确。

  • 如果怀疑 SPL 问题,单独编译 SPL,烧录测试:make spl/u-boot-spl.bin 然后用 dd 写到对应偏移。示例(假设 eMMC):

    dd if=SPL of=/dev/mmcblk0 bs=1K seek=1 conv=fsync

自身案例:一台设备批量返修,症状是偶尔启动失败(概率约 5%)。用 JTAG 挂上去单步,发现 SPL 在 DDR calibration 阶段某个 loop 超时。最终解决办法是把 DDR training 放到 U-Boot 阶段动态做,牺牲一点启动时间换稳定(从 300ms 增加到 800ms,但故障率降到 0)。

2.3 U-Boot 主阶段:硬件初始化与环境准备

完整的 U-Boot 接手后,会进行更全面的硬件初始化:时钟树、GPIO、串口、存储控制器、网络(如果需要)、USB 等。它会读取保存在存储介质里的 环境变量(environment),这些变量通常存在 eMMC 的一个 raw 分区或 SPI flash 里,用 env 命令管理。

关键变量包括:

  • bootdelay:倒计时秒数,默认 2-3 秒,生产环境常设为 0 以加速启动。
  • bootcmd:自动执行的启动命令串,比如 run load_kernel; bootm {kernel_addr} - {fdt_addr}。
  • bootargs:传递给内核的命令行参数,典型值如 console=ttyS0,115200 root=/dev/mmcblk0p2 rootwait rw earlycon=uart8250,mmio32,0x30860000。

U-Boot 会根据 bootcmd 从存储介质加载 内核镜像 (zImage / Image / uImage)和 Device Tree Blob (DTB) 到内存指定地址。

加载过程常见命令示例(手动测试时):

复制代码
mmc dev 0
fatload mmc 0:1 0x48000000 Image          # 加载内核到 0x48000000
fatload mmc 0:1 0x43000000 imx8mp-evk.dtb # 加载 DTB
setenv bootargs console=ttyS0,115200 root=/dev/mmcblk0p2 rw rootwait
booti 0x48000000 - 0x43000000             # ARM64 用 booti

或者用 FIT 镜像(推荐生产环境,包含内核 + DTB + 可能还有 initramfs):

复制代码
bootm ${fit_addr}

我记得几年前的一次项目中,客户反馈设备偶尔"黑屏不启动",日志停在 "Starting kernel ..."。排查发现 bootargs 里的 root= 参数写死了旧分区号,新固件改了分区布局后没更新 env。另一个常见问题是 DTB 不匹配:内核版本升级后,DTB 还是老的,导致外设驱动 probe 失败,系统 hang 在用户空间前。

排错思路

  1. 按任意键中断 autoboot,进入 U-Boot 命令行。
  2. printenv 查看所有环境变量,重点看 bootcmd、bootargs、fdt_addr_r、kernel_addr_r。
  3. fatls mmc 0:1 或 ext4ls 检查文件是否存在、大小是否正确。
  4. md.b <addr> 0x100 查看加载到内存的内容,确认镜像头部魔数(zImage 是 0x016f2818 等)。
  5. 如果内核启动后 hang,用 earlyprintk + kgdb 或在内核配置里开 CONFIG_DEBUG_INFO + JTAG 单步。
  6. 生产批量问题:用脚本批量 dump 设备 env(env export),对比良品与不良品差异。

我还遇到过因为 U-Boot 版本升级引入的 regression:新版本默认开启了某个 cache 配置,导致在特定温度下数据一致性问题。解决时回滚到稳定 tag(比如 2023.04),并在 board 文件里加 patch 锁定配置。

2.4 内核启动:从 head.S 到 start_kernel()

U-Boot 把控制权交给内核后(通过 bootm/booti 等),内核先做解压(如果是 zImage)、MMU 初始化、页表建立,然后进入 start_kernel()。这时会解压 initramfs(如果有)、挂载 rootfs、启动 init 进程(通常是 systemd 或 busybox init)。

常见卡点:

  • DTB 传递错误,导致外设无法识别。
  • bootargs 里的 console 参数不对,日志看不到。
  • initramfs 缺失或损坏,无法挂载真实 rootfs。

说一个生产环境中一个真实案例,智能网关设备在高温环境(>60℃)下,内核启动到 "Freeing unused kernel memory" 后 panic,报 "Unable to mount root fs"。原因是 eMMC 在高温下读速变慢,超时了。解决办法是把 rootdelay=10 加到 bootargs,并优化文件系统为 f2fs + 更好的 wear leveling。

调试技巧

  • 内核命令行加 debug earlyprintk=serial,ttyS0,115200 loglevel=7。
  • 用 bootm 前在 U-Boot 执行 fdt addr ${fdtcontroladdr}; fdt print /chosen 检查传递的参数。
  • 如果怀疑内核镜像坏,用 iminfo <addr> 检查 uImage/zImage 头部。

2.5 进入用户空间后

内核起来后,init 进程启动,运行 rcS 脚本或 systemd units,挂载文件系统、启动服务。这时启动流程才算真正完成。但生产痛点往往在这里产生:某个服务依赖的硬件没 ready,导致超时重试,拉长整体时间。

在我的 i.MX8M 项目里,初期启动时间 12 秒多,后来通过以下优化压到 7.2 秒:

  • U-Boot bootdelay=0,移除不必要的驱动 probe。
  • 内核裁剪掉 debug 配置和无用模块。
  • 用 squashfs + overlayfs 做 rootfs,减少 fsck 时间。
  • 并行启动非关键服务。

三、关键技术:设备树(DTB)

调试启动问题时,最头疼的就是设备树配置。设备树(Device Tree)核心作用是"解耦内核与硬件",今天我们只重点讲两个关键知识点**(详细的设备树可以看之前发的文章:深入浅出Linux设备树:从原理到实战):**

3.1 设备树的本质

在没有设备树之前,Linux内核的硬件信息都是硬编码在代码中的,比如某个外设的地址、中断号,都直接写在驱动里。这样带来的问题是:一旦更换开发板(哪怕只是外设位置变了),就需要重新修改内核代码、重新编译,非常繁琐。

设备树的出现,就是为了解决这个问题。它是一种树形结构的数据结构,通过DTS(设备树源码)编译为DTB(二进制文件),专门用来描述硬件配置。内核通过解析DTB,就能知道当前系统的硬件信息,无需修改内核代码,只需更换DTB文件,就能让一份内核镜像适配多块开发板,大幅降低系统移植难度。

简单来说,设备树就是给内核看的"硬件说明书",上面写清了"CPU是什么型号""串口挂在哪个地址""中断号是多少",内核照着说明书就能驱动硬件。

3.2 U-Boot DTB vs Linux内核 DTB

很多同学会误以为U-Boot使用的DTB和Linux内核使用的DTB是一样的,可以直接复用,其实不然。两者虽为同源文件(都是由同一个DTS编译而来),但功能定位不同,不能混用:

👉 U-Boot DTB:侧重基础硬件初始化,包含内存测试、串口配置、存储介质识别等专用节点,它的生命周期只到内核启动,内核接管后,U-Boot的DTB就不再起作用。

👉 Linux内核 DTB:需要完整描述所有外设的细节(比如中断控制、复位机制、外设寄存器地址等),支撑驱动加载与设备管理,贯穿整个系统运行全程。

开发中一定要注意:U-Boot和内核使用的DTB虽然可以来自同一个DTS,但需要根据各自的需求配置对应的节点,否则会出现启动失败(比如U-Boot无法识别存储介质,或内核无法驱动外设)。

四、U-Boot调试与启动配置

讲完理论,再给大家上点实战干货。

4.1 启动排坑

嵌入式Linux启动失败,大概率是某一个阶段出了问题,我们可以通过串口日志,按以下3步快速定位:

  1. 验证SPL阶段:看串口是否打印"SPL"相关信息,是否提示"DDR initialized"(DDR初始化成功)。如果没有,说明SPL未正常运行,可能是BootROM未加载到SPL,或SPL代码有问题。

  2. 验证U-Boot主程序阶段:看是否能进入U-Boot命令行(通常按回车键进入),能否识别存储介质(比如用ls mmc 0:查看SD卡内容)。如果无法识别存储介质,检查U-Boot的存储驱动配置。

  3. 验证内核与根文件系统阶段:如果U-Boot能加载内核,但内核启动卡住,检查内核镜像地址、DTB地址是否正确,bootargs参数是否正确(尤其是根文件系统路径);如果提示"can't find rootfs",说明根文件系统挂载失败,检查根文件系统格式、挂载路径是否正确。

常见故障:

  • 串口乱码:U-Boot的串口波特率与终端不一致(通常是115200);

  • 内核启动黑屏:DTB中显示驱动配置错误;

  • 根文件系统挂载失败:bootargs中根文件系统路径错误,或根文件系统镜像损坏。

4.2 U-Boot高频命令

  1. 设置bootargs参数(挂载ext4格式的根文件系统):

    setenv bootargs 'root=/dev/mmcblk0p2 rootfs=ext4 rw console=ttyS0,115200'

root=/dev/mmcblk0p2表示根文件系统在SD卡的第二个分区,console=ttyS0,115200表示串口0,波特率115200。

  1. 保存环境变量(避免重启后丢失):

    saveenv

  2. 从SD卡加载内核镜像到DDR指定地址:

    fatload mmc 0:1 80008000 zImage

mmc 0:1表示SD卡(0)的第一个分区(1),80008000是内核加载地址,zImage是内核镜像文件名。

  1. 从SD卡加载DTB文件到DDR指定地址:

    fatload mmc 0:1 83000000 xxx.dtb

说明:83000000是DTB加载地址,xxx.dtb是设备树文件名。

  1. 启动内核:

    bootz 80008000 - 83000000

bootz后面依次是内核地址、initrd地址(这里用"-"表示没有initrd)、DTB地址。

相关推荐
_wyt00114 小时前
洛谷 B3930 [GESP202312 五级] 烹饪问题 题解
c++·gesp
大树8816 小时前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠16 小时前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质16 小时前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush416 小时前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行52016 小时前
Linux 11 动态监控指令top
linux
Inhand陈工17 小时前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
玖玥拾17 小时前
C/C++ 数据结构(七)栈、容器适配器
c语言·数据结构·c++··容器适配器
酣大智18 小时前
ARP代理--工作原理
运维·网络·arp·arp代理
不会C语言的男孩18 小时前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言