4. Bootloader原理与移植
Bootloader,说白了就是板子上电后跑的第一段代码。它的任务很明确:初始化硬件、加载内核、跳过去执行。很多刚入行的朋友觉得这步就是跑个流水灯,其实不然——Bootloader搞不好,后面内核连影子都见不着。
我个人习惯把Bootloader比作“硬件化妆师”。它得把CPU、DDR、Flash这些“素颜”硬件,快速打扮成内核喜欢的样子。今天我们就从启动流程开始,一步步拆解U-Boot。
4.1 Bootloader启动流程
一个典型的嵌入式Bootloader,启动过程大致分三个阶段。我画了张图,方便你理解整体脉络。
你可能会问:为什么非要分三个阶段?直接一个Bootloader搞定不行吗?
原因很简单——芯片内部SRAM太小了。以我常用的i.MX6为例,内部RAM只有96KB,根本放不下完整的U-Boot。所以必须搞个“瘦身版”SPL先初始化DDR,再把完整镜像搬进去。
4.2 U-Boot源码结构分析
U-Boot的源码目录,初看挺吓人。其实核心就几个文件夹。我列个表,你对照着看。
| 目录 | 作用 | 我重点关注什么 |
|---|---|---|
arch/ |
CPU架构相关代码(ARM、RISC-V、x86等) | arch/arm/cpu/ 下的start.S,这是入口 |
board/ |
板级初始化代码 | 每个板子一个文件夹,移植时直接复制修改 |
drivers/ |
外设驱动(串口、网卡、MMC等) | 串口驱动最先调通,否则没日志 |
include/configs/ |
板级配置头文件 | 裁剪和移植的核心,宏定义都在这里 |
dts/ |
设备树源文件 | 新板子必须自己写.dts |
common/ |
通用功能(命令行、环境变量等) | 一般不动,除非要加自定义命令 |
启动入口在 arch/arm/cpu/armv7/start.S(ARMv7为例)。这段汇编代码干了三件事:
- 设置异常向量表——CPU复位后第一个跳转目标
- 关Cache、关MMU——刚上电时这些必须关掉,否则地址映射混乱
- 初始化C运行环境——设置栈指针、清零BSS段,然后跳转到C语言的
board_init_f()
mrc p15, 0, r0, c1, c0, 0。记住:上电后第一件事就是关MMU和Cache。
4.3 U-Boot配置与编译
U-Boot的配置系统基于Kconfig,跟Linux内核很像。我个人觉得这是U-Boot做得最舒服的地方——不用再手动改一堆宏了。
基本流程分三步:
# 第一步:配置
make <板子名>_defconfig
# 第二步:菜单配置(可选)
make menuconfig
# 第三步:编译
make -j4
举个例子,我手头有块i.MX6ULL的开发板:
make mx6ull_14x14_evk_defconfig
make menuconfig # 裁剪不需要的功能
make -j4
编译完成后,会生成 u-boot.bin 和 u-boot-dtb.bin。后者把设备树打包进去了,推荐用这个。
V=1 参数(make V=1 -j4),把编译过程打出来。这样能清楚看到链接顺序和使用了哪些文件,排查问题很方便。
配置裁剪时,我建议你关注这几个选项:
| 配置项 | 说明 | 裁剪建议 |
|---|---|---|
CONFIG_CMD_NET |
网络命令(ping、tftp等) | 量产固件可以关掉 |
CONFIG_CMD_USB |
USB支持 | 不用USB启动就关掉 |
CONFIG_CMD_BOOTD |
bootd命令 | 调试阶段保留,量产可关 |
CONFIG_SUPPORT_RAW_INITRD |
原始initrd支持 | 不用initrd就关掉 |
裁剪后,U-Boot体积能缩小30%-50%。我做过一个项目,从500KB裁到280KB,完全够用。
4.4 U-Boot设备树支持
设备树(Device Tree)是U-Boot和内核之间的“硬件说明书”。U-Boot负责把设备树传给内核,内核根据设备树知道板子上有什么外设。
U-Boot对设备树的支持,主要体现在三个方面:
- 编译设备树——把.dts编译成.dtb
- 传递设备树——启动内核时把dtb地址传给内核
- 修改设备树——U-Boot可以在运行时修改dtb(比如改MAC地址)
设备树文件放在 arch/arm/dts/ 目录下。移植新板子时,我通常这么做:
# 找一个相近的.dts复制一份
cp arch/arm/dts/imx6ull-14x14-evk.dts arch/arm/dts/myboard.dts
# 修改Makefile,加入编译
# 在 arch/arm/dts/Makefile 中添加:
# dtb-$(CONFIG_MX6ULL) += myboard.dtb
设备树里最关键的几个节点:
| 节点 | 作用 | 常见坑 |
|---|---|---|
/chosen |
内核启动参数(bootargs) | console参数写错,内核没日志 |
/memory |
DDR内存布局 | reg属性必须和硬件一致 |
/soc |
片上外设(UART、I2C等) | 时钟频率配错,外设不工作 |
/leds |
GPIO LED(调试用) | GPIO号别搞反了 |
U-Boot启动内核时,传递设备树的命令是这样的:
# 加载内核和设备树到DDR
tftp 0x80800000 zImage
tftp 0x83000000 myboard.dtb
# 启动内核,传递设备树地址
bootz 0x80800000 - 0x83000000
这里的 0x83000000 就是设备树在DDR中的地址。U-Boot会把这个地址写入寄存器 r2,内核从 r2 读取设备树地址。
fdt 命令查看设备树内容。比如 fdt list /soc 可以查看soc节点下的所有子节点。这比反复编译烧写快多了。
嗯,Bootloader这块内容不少,但核心就这些。启动流程搞清楚了,源码结构摸透了,配置编译走通了,设备树会写了——移植工作就完成了一大半。剩下的就是针对具体板子调参数、修bug。