4. Bootloader原理与移植

Bootloader,说白了就是板子上电后跑的第一段代码。它的任务很明确:初始化硬件、加载内核、跳过去执行。很多刚入行的朋友觉得这步就是跑个流水灯,其实不然——Bootloader搞不好,后面内核连影子都见不着。

我个人习惯把Bootloader比作“硬件化妆师”。它得把CPU、DDR、Flash这些“素颜”硬件,快速打扮成内核喜欢的样子。今天我们就从启动流程开始,一步步拆解U-Boot。

核心要点: Bootloader是硬件与内核之间的桥梁,移植工作的80%都在这里。

4.1 Bootloader启动流程

一个典型的嵌入式Bootloader,启动过程大致分三个阶段。我画了张图,方便你理解整体脉络。

Bootloader 启动流程 阶段一:ROM Code 固化在芯片内部 初始化时钟、PLL、基础外设 阶段二:SPL / MLO 初始化DDR、时钟 加载完整U-Boot到DDR 阶段三:U-Boot 完整功能 加载内核、设备树 详细执行流程 上电 → 异常向量表 → 汇编初始化(Cache/MMU关) → 板级初始化 → DDR初始化 → 搬移U-Boot → 跳转 关Cache/MMU DDR初始化 搬移镜像 跳转 ⚠️ 注意:SPL阶段DDR初始化是关键,参数配错直接死机 我曾经在DDR时序参数上栽过跟头,板子死活不跑

你可能会问:为什么非要分三个阶段?直接一个Bootloader搞定不行吗?

原因很简单——芯片内部SRAM太小了。以我常用的i.MX6为例,内部RAM只有96KB,根本放不下完整的U-Boot。所以必须搞个“瘦身版”SPL先初始化DDR,再把完整镜像搬进去。

我的经验: 移植SPL时,优先确认DDR初始化代码。我习惯先用芯片厂商的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为例)。这段汇编代码干了三件事:

  1. 设置异常向量表——CPU复位后第一个跳转目标
  2. 关Cache、关MMU——刚上电时这些必须关掉,否则地址映射混乱
  3. 初始化C运行环境——设置栈指针、清零BSS段,然后跳转到C语言的 board_init_f()
避坑指南: 我曾经在移植时忘了关MMU,结果DDR地址映射出错,U-Boot死活跑不起来。查了两天才发现是start.S里少了一句 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对设备树的支持,主要体现在三个方面:

  1. 编译设备树——把.dts编译成.dtb
  2. 传递设备树——启动内核时把dtb地址传给内核
  3. 修改设备树——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号别搞反了
避坑指南: 我曾经在设备树里把UART2的时钟配成了24MHz,实际是26MHz。结果串口输出全是乱码。查了两天才发现是clock-frequency写错了。记住:设备树里的时钟频率必须和芯片手册完全一致。

U-Boot启动内核时,传递设备树的命令是这样的:

# 加载内核和设备树到DDR
tftp 0x80800000 zImage
tftp 0x83000000 myboard.dtb

# 启动内核,传递设备树地址
bootz 0x80800000 - 0x83000000

这里的 0x83000000 就是设备树在DDR中的地址。U-Boot会把这个地址写入寄存器 r2,内核从 r2 读取设备树地址。

我的经验: 调试阶段,我习惯在U-Boot命令行里用 fdt 命令查看设备树内容。比如 fdt list /soc 可以查看soc节点下的所有子节点。这比反复编译烧写快多了。

嗯,Bootloader这块内容不少,但核心就这些。启动流程搞清楚了,源码结构摸透了,配置编译走通了,设备树会写了——移植工作就完成了一大半。剩下的就是针对具体板子调参数、修bug。