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/,因为移植工作主要集中在这两个地方。

小技巧: 想快速了解一个板子的U-Boot怎么跑的?直接看 board/厂商/板子名/ 下的 board.c,那里是板级初始化的入口。

4.2 编译与配置:从零构建U-Boot

配置U-Boot,现在主流是用 make menuconfig,跟配置Linux内核一样。但早期可不是这样,早期是用 include/configs/ 下的头文件,里面一堆宏定义,改起来特别痛苦。

我记得有一次,客户要改个DDR参数,我在那个头文件里翻了半天,最后发现有个宏没定义对,导致整个板子起不来。后来新版U-Boot引入了Kconfig,才算是解放了。

4.2.1 配置流程

  1. 选择默认配置: make <板子名>_defconfig
  2. 定制配置: make menuconfig 图形化调整
  3. 保存配置: 自动生成 .config 文件

举个例子,我用的是QEMU的virt板子:

# 加载默认配置
make qemu_arm64_defconfig

# 打开图形界面微调
make menuconfig

# 开始编译
make -j$(nproc)

编译完成后,你会得到 u-boot.bin。但注意,这个文件不一定能直接用。很多平台需要 u-boot-dtb.bin,就是把设备树和U-Boot本体打包在一起。

注意: 如果你用的是旧版U-Boot(2016年之前的),配置方式完全不同。那时候是用 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不用解析那么多没用的节点。

核心原则: U-Boot的设备树只包含“U-Boot需要用到”的硬件信息。比如网卡驱动、存储控制器、串口——这些必须有。但音频编解码器、摄像头接口这些,U-Boot根本不管,删掉就行。

4.3.2 设备树在启动流程中的角色

U-Boot启动时,设备树在哪个阶段起作用?我画了张图,你看完就明白了。

U-Boot启动流程与设备树关系 U-Boot启动流程与设备树关系 SPL阶段 初始化DDR、时钟 U-Boot加载 从存储介质读取 设备树解析 读取内存布局 初始化外设驱动 启动内核 传递设备树地址给内核 关键说明: 1. SPL阶段通常不使用设备树,硬编码初始化最小硬件 2. U-Boot主程序启动后,立即解析设备树,获取硬件配置 3. 启动内核时,U-Boot将设备树内存地址传给内核,内核接管 4. 设备树在U-Boot和内核之间是“传递”关系,不是“共享”

你看这个流程就清楚了。设备树在U-Boot里主要干三件事:

  1. 告诉U-Boot内存有多大: 从 memory 节点读取DDR范围
  2. 告诉U-Boot外设怎么配: 串口地址、时钟频率、GPIO复用
  3. 传递给内核: 启动时把设备树地址放在寄存器里,内核自己解析

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用。结果U-Boot编译出来的固件大了200KB,启动慢了将近1秒。后来老老实实精简了一遍,只保留U-Boot需要的节点,启动时间从3.2秒降到了2.1秒。所以别偷懒,该精简就精简。

嗯,关于U-Boot的源码结构、编译配置和设备树,今天就聊到这儿。这些东西看起来琐碎,但每一条都是实战中摸出来的。你动手做一遍移植,比我讲十遍都管用。