4. 分区基础
U-Boot这玩意儿,说简单也简单,说复杂是真复杂。我最早接触它的时候,觉得不就是个引导程序嘛,能有多难?结果第一次移植就把我折腾得够呛。后来慢慢摸透了它的脾气,才发现它的设计其实很有章法。
今天咱们就把它拆开来看。源码怎么组织的、怎么配置编译、设备树在里面到底扮演什么角色——把这些搞明白,你再看U-Boot就不会觉得它是个黑盒子了。
4.1 U-Boot源码结构:别被目录吓到
我第一次解压U-Boot源码,看到那一堆目录,说实话有点懵。但其实它的结构很清晰,核心就几个部分。
| 目录/文件 | 作用 | 我的理解 |
|---|---|---|
arch/ |
架构相关代码,比如arm、riscv、x86 | 换芯片主要动这里 |
board/ |
板级初始化代码,厂商定制 | 每个开发板都有自己的文件夹 |
common/ |
通用功能,比如命令行、环境变量 | 大部分时候不用改 |
drivers/ |
各种外设驱动 | 串口、网卡、存储都在这里 |
include/ |
头文件和配置文件 | configs/ 下是板级配置 |
dts/ |
设备树源文件 | 新版U-Boot都放这了 |
tools/ |
编译工具和脚本 | 比如mkimage |
你看,其实就这几大类。我个人习惯是先看 arch/ 和 board/,因为移植工作主要集中在这两个地方。
board/厂商/板子名/ 下的 board.c,那里是板级初始化的入口。
4.2 编译与配置:从零构建U-Boot
配置U-Boot,现在主流是用 make menuconfig,跟配置Linux内核一样。但早期可不是这样,早期是用 include/configs/ 下的头文件,里面一堆宏定义,改起来特别痛苦。
我记得有一次,客户要改个DDR参数,我在那个头文件里翻了半天,最后发现有个宏没定义对,导致整个板子起不来。后来新版U-Boot引入了Kconfig,才算是解放了。
4.2.1 配置流程
- 选择默认配置:
make <板子名>_defconfig - 定制配置:
make menuconfig图形化调整 - 保存配置: 自动生成
.config文件
举个例子,我用的是QEMU的virt板子:
# 加载默认配置
make qemu_arm64_defconfig
# 打开图形界面微调
make menuconfig
# 开始编译
make -j$(nproc)
编译完成后,你会得到 u-boot.bin。但注意,这个文件不一定能直接用。很多平台需要 u-boot-dtb.bin,就是把设备树和U-Boot本体打包在一起。
make <板子名>_config,而且没有menuconfig。碰到老项目别用错方法。
4.2.2 编译产物说明
| 文件 | 说明 | 使用场景 |
|---|---|---|
u-boot.bin |
纯U-Boot二进制 | 需要单独加载设备树 |
u-boot-dtb.bin |
U-Boot + 设备树打包 | 大多数嵌入式场景 |
u-boot.img |
带U-Boot头格式 | SPL或Falcon模式 |
u-boot.srec |
SREC格式 | 串口下载 |
我个人建议,除非你有特殊需求,否则直接用 u-boot-dtb.bin 最省事。设备树和U-Boot一起烧进去,启动时自动解析,不用额外操心。
4.3 设备树在U-Boot中的作用
设备树(Device Tree)这东西,刚出来的时候很多人不适应。以前都是写死的代码,现在改成描述性的文本了。但用久了你会发现,它确实灵活。
U-Boot里的设备树,说白了就是告诉U-Boot:你的硬件长什么样。比如DDR地址范围、串口用哪个、GPIO怎么配的——这些信息都写在 .dts 文件里。
4.3.1 U-Boot设备树 vs 内核设备树
这里有个容易搞混的点:U-Boot用的设备树和内核用的设备树,是不是同一个?
答案是:不一定。
- 相同的情况: 很多平台直接复用内核的
.dts,U-Boot编译时拷贝一份。 - 不同的情况: U-Boot只需要初始化硬件,不需要知道所有外设细节。所以U-Boot的设备树可以精简,只保留必要的节点。
我曾经在一个项目里,内核设备树有3000多行,U-Boot的设备树我精简到400行。启动速度明显快了,因为U-Boot不用解析那么多没用的节点。
4.3.2 设备树在启动流程中的角色
U-Boot启动时,设备树在哪个阶段起作用?我画了张图,你看完就明白了。
你看这个流程就清楚了。设备树在U-Boot里主要干三件事:
- 告诉U-Boot内存有多大: 从
memory节点读取DDR范围 - 告诉U-Boot外设怎么配: 串口地址、时钟频率、GPIO复用
- 传递给内核: 启动时把设备树地址放在寄存器里,内核自己解析
4.3.3 实战:修改设备树适配新板子
假设你拿到一块新板子,DDR是512MB,串口用的是UART2。怎么改?
找到对应的 .dts 文件,比如 arch/arm/dts/myboard.dts:
/dts-v1/;
#include "soc.dtsi"
/ {
model = "My Custom Board";
compatible = "mycompany,myboard";
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>; // 512MB DDR
};
chosen {
stdout-path = &uart2;
};
};
&uart2 {
status = "okay";
clock-frequency = <24000000>;
};
改完之后,重新编译U-Boot,设备树会自动打包进去。你不需要手动处理 .dtb 文件,除非你想单独更新设备树。
.dtb,用 fdt addr 命令在U-Boot命令行里手动加载测试。确认没问题了,再烧进固件里。这样不用反复烧写整个U-Boot。
4.4 避坑指南
做U-Boot移植这些年,我踩过不少坑。挑几个典型的说说:
- 设备树语法错误: 少个分号、括号不匹配,U-Boot启动时直接挂。用
dtc -I dts -O dtb test.dts先检查语法。 - 内存节点写错: 有一次我把
reg的地址写成了0x80000000 0x40000000,结果1GB内存只认了512MB。查了半天才发现是size写错了。 - 时钟频率不匹配: 串口波特率算不对,打印全是乱码。检查
clock-frequency和分频系数。 - SPL和U-Boot设备树不一致: SPL用硬编码初始化DDR,U-Boot用设备树。如果两者参数不同,启动到一半就崩。
嗯,关于U-Boot的源码结构、编译配置和设备树,今天就聊到这儿。这些东西看起来琐碎,但每一条都是实战中摸出来的。你动手做一遍移植,比我讲十遍都管用。