从上电到系统就绪: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地址。

相关推荐
雨中来客2 小时前
在开启了FBDEV 模拟层的设备上屏蔽/dev/fb0的显示功能
linux
Yupureki2 小时前
《MySQL数据库基础》8.复合查询
linux·运维·服务器·网络·数据库·mysql
上海云盾安全满满2 小时前
海外服务器使用高防CDN的优势
运维·服务器
眷蓝天2 小时前
Jenkins部署与配置
运维·jenkins
Face2 小时前
WSL2网络不通修改
linux
reikocao2 小时前
ubuntu系统源
linux·运维·ubuntu
Promise微笑2 小时前
SF6综合测试仪:国产替代SF6综合测试仪的精密化进阶与自主实践
运维·人工智能·安全
Sisphusssss2 小时前
DiskGenius 备份 Ubuntu 系统
linux·ubuntu·diskgenius
Mortalbreeze2 小时前
进程间通信 ---- System V 共享内存
linux·运维·服务器