Bootloader 基础:概念、作用与启动流程

做嵌入式开发这么多年,我见过不少新手一上来就追着 Linux 内核啃,结果板子点不亮,连串口打印都没有。其实问题往往出在 Bootloader 上。说白了,Bootloader 就是硬件上电后跑的第一段程序,它负责把内核从 Flash 里请出来,再交棒给操作系统。

我个人习惯把 Bootloader 比作「系统保安」——它先检查硬件环境是否正常,然后引导内核进场。保安要是没站好岗,后面的事全白搭。

什么是 Bootloader?

Bootloader 是嵌入式系统上电后执行的第一个软件模块。它运行在裸机环境下,没有操作系统依赖。它的核心任务就两个:初始化硬件和加载操作系统。

关键认知:Bootloader 不是操作系统的一部分,它是独立于内核的固件程序。你换内核版本,不一定需要换 Bootloader;但换硬件平台,Bootloader 几乎必须重写或移植。

Bootloader 的核心作用

我把它拆成四个维度来讲,这样好记:

作用维度 具体说明 我踩过的坑
硬件初始化 设置时钟、DDR、串口、存储控制器等 DDR 时序配错,内核解压到一半直接挂掉
内核加载与启动 从 Flash/网络读取内核镜像,跳转到入口 分区偏移算错,加载了空数据
设备树传递 将设备树二进制(DTB)地址传给内核 DTB 地址没对齐,内核不认外设
系统恢复与升级 提供命令行接口,支持烧写、调试、网络下载 曾经把 Flash 擦坏了,全靠 Bootloader 的网络功能救回来

典型启动流程

一个标准的嵌入式 Bootloader 启动流程,大致分三个阶段。我画了张图,你一看就明白:

Bootloader 典型启动流程 阶段一:ROM Code 固化在芯片内部 阶段二:SPL / MLO 初始化 DDR 与时钟 阶段三:U-Boot 完整版 命令行 + 加载内核 ROM Code 做的事 • 读取启动介质选择引脚 • 从 SD/eMMC/NAND 头部 加载 SPL 到 SRAM • 跳转到 SPL 入口 SPL 做的事 • 初始化 DDR 控制器 • 配置 PLL 与时钟树 • 从存储介质加载 U-Boot 完整版到 DDR U-Boot 做的事 • 提供命令行交互 • 加载内核与 DTB • 设置启动参数 • 跳转到内核入口 跳转到 Linux 内核入口

你想想看,为什么要有 SPL 这个中间层?因为芯片内部的 SRAM 通常只有几十 KB,装不下完整的 U-Boot。SPL 就像个「瘦身版」的 Bootloader,只做最必要的事——把 DDR 初始化好,然后把真正的 U-Boot 从 Flash 搬到内存里。

实战技巧:我在调试 i.MX6 平台时,经常遇到 SPL 加载 U-Boot 后卡死。后来发现是 DDR 的写电平时序没调对。建议你在移植新板子时,先用芯片厂商提供的 DDR 训练工具跑一遍,把参数记下来,再写进 SPL。

常见 Bootloader 介绍

市面上 Bootloader 种类不少,但真正在工业级产品里广泛用的,其实就那么几个。我列个对比表:

名称 适用架构 特点 我的评价
U-Boot ARM、RISC-V、x86、MIPS 功能最全,社区活跃,支持设备树 嵌入式 Linux 事实标准,首选
Barebox ARM、x86 代码更现代,POSIX 风格 API 适合喜欢干净代码的团队
GRUB x86、x86_64 PC 标准,支持多系统引导 嵌入式用得少,桌面/服务器为主
RedBoot ARM、MIPS 轻量,支持 eCos 基本被 U-Boot 取代了
Coreboot x86、ARM 极速启动,安全启动支持好 适合对启动时间有苛刻要求的场景

我个人强烈建议你从 U-Boot 入手。为什么?因为它的资料最多,支持的板子最广,而且设备树支持最成熟。我在给客户做培训时,90% 的项目都在用 U-Boot。

U-Boot 的启动细节

U-Boot 的启动流程,我习惯把它拆成更细的步骤。嗯,这里要注意,不同版本细节有差异,但大框架不变:

  1. CPU 初始化——设置异常向量表、关中断、切换 CPU 模式
  2. 板级初始化——调用 board_init_f(),初始化串口、时钟、定时器
  3. DDR 初始化——配置内存控制器,完成 DDR 训练
  4. 重定位——将 U-Boot 自身从 Flash 复制到 DDR 中运行
  5. 板级初始化第二阶段——调用 board_init_r(),初始化更多外设
  6. 进入命令行——等待用户输入,或执行自动启动脚本
  7. 加载内核与 DTB——从存储介质读取镜像,设置启动参数
  8. 跳转到内核——清理环境,跳转到内核入口地址

避坑指南:我曾经在重定位阶段吃过亏。U-Boot 把自己从 Flash 搬到 DDR 后,如果链接地址和实际运行地址不一致,程序直接跑飞。解决办法是检查 CONFIG_SYS_TEXT_BASE 是否与硬件映射地址匹配。这个坑,我至少见过三个工程师踩过。

设备树在 Bootloader 中的角色

设备树(Device Tree)是 Bootloader 和内核之间的「通信协议」。Bootloader 负责把 DTB(设备树二进制)加载到内存,然后把地址通过寄存器或参数结构体传给内核。

具体来说,U-Boot 里做这几件事:

  • 从存储分区读取 .dtb 文件
  • 可以修改 DTB 中的某些节点(比如修改 chosen 节点的 bootargs)
  • 将 DTB 地址写入 r2 寄存器(ARM 架构约定)
  • 跳转到内核时,内核从 r2 获取 DTB 地址
// U-Boot 中加载设备树的典型命令
=> load mmc 0:1 0x82000000 /boot/kernel.itb
=> load mmc 0:1 0x83000000 /boot/board.dtb
=> bootm 0x82000000 - 0x83000000

这里 0x83000000 就是 DTB 在内存中的位置。内核启动时会解析这个地址,枚举所有外设。

调试建议:如果你发现内核启动后某些外设不工作,先用 fdt list 命令在 U-Boot 命令行里查看 DTB 内容。我曾经遇到过 DTB 里 I2C 节点的时钟频率写错了,导致触摸屏不响应。在 U-Boot 里用 fdt set 临时改一下就能验证。