Bootloader 基础:概念、作用与启动流程
做嵌入式开发这么多年,我见过不少新手一上来就追着 Linux 内核啃,结果板子点不亮,连串口打印都没有。其实问题往往出在 Bootloader 上。说白了,Bootloader 就是硬件上电后跑的第一段程序,它负责把内核从 Flash 里请出来,再交棒给操作系统。
我个人习惯把 Bootloader 比作「系统保安」——它先检查硬件环境是否正常,然后引导内核进场。保安要是没站好岗,后面的事全白搭。
什么是 Bootloader?
Bootloader 是嵌入式系统上电后执行的第一个软件模块。它运行在裸机环境下,没有操作系统依赖。它的核心任务就两个:初始化硬件和加载操作系统。
关键认知:Bootloader 不是操作系统的一部分,它是独立于内核的固件程序。你换内核版本,不一定需要换 Bootloader;但换硬件平台,Bootloader 几乎必须重写或移植。
Bootloader 的核心作用
我把它拆成四个维度来讲,这样好记:
| 作用维度 | 具体说明 | 我踩过的坑 |
|---|---|---|
| 硬件初始化 | 设置时钟、DDR、串口、存储控制器等 | DDR 时序配错,内核解压到一半直接挂掉 |
| 内核加载与启动 | 从 Flash/网络读取内核镜像,跳转到入口 | 分区偏移算错,加载了空数据 |
| 设备树传递 | 将设备树二进制(DTB)地址传给内核 | DTB 地址没对齐,内核不认外设 |
| 系统恢复与升级 | 提供命令行接口,支持烧写、调试、网络下载 | 曾经把 Flash 擦坏了,全靠 Bootloader 的网络功能救回来 |
典型启动流程
一个标准的嵌入式 Bootloader 启动流程,大致分三个阶段。我画了张图,你一看就明白:
你想想看,为什么要有 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 的启动流程,我习惯把它拆成更细的步骤。嗯,这里要注意,不同版本细节有差异,但大框架不变:
- CPU 初始化——设置异常向量表、关中断、切换 CPU 模式
- 板级初始化——调用
board_init_f(),初始化串口、时钟、定时器 - DDR 初始化——配置内存控制器,完成 DDR 训练
- 重定位——将 U-Boot 自身从 Flash 复制到 DDR 中运行
- 板级初始化第二阶段——调用
board_init_r(),初始化更多外设 - 进入命令行——等待用户输入,或执行自动启动脚本
- 加载内核与 DTB——从存储介质读取镜像,设置启动参数
- 跳转到内核——清理环境,跳转到内核入口地址
避坑指南:我曾经在重定位阶段吃过亏。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 临时改一下就能验证。