4. 设备树(Device Tree):从硬件描述到Bootloader解析

设备树,英文叫 Device Tree。说白了,它就是一份硬件配置清单。

你想想看,一个嵌入式系统里有多少外设?UART、I2C、SPI、GPIO、中断控制器、时钟...每个外设的地址、中断号、时钟频率都不一样。以前的做法是把这些信息硬编码在代码里,换一个板子就要改代码,非常痛苦。

设备树就是来解决这个问题的。它把硬件信息从代码里抽离出来,变成一份独立的数据结构。Bootloader 启动时,读这份数据,然后把信息传给内核。这样,同一份内核镜像就能适配不同的硬件平台。

核心思想: 代码与硬件描述分离。改硬件,改设备树,不改内核代码。

4.1 设备树的作用与结构

设备树的作用,我总结为三点:

  • 描述硬件: CPU、内存、总线、外设的地址、中断、时钟、DMA 等信息。
  • 传递参数: 内核启动参数、内存大小、MAC 地址等运行时信息。
  • 解耦硬件: 同一份内核镜像,通过不同的设备树文件,适配不同的板卡。

它的结构是一棵树。根节点是 /,下面挂 CPU、内存、总线,总线下面再挂外设。每个节点都有属性,属性就是键值对,用来描述硬件特征。

我举个例子,一个简单的设备树片段:

/ {
    model = "TI AM335x BeagleBone Black";
    compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;
        cpu@0 {
            compatible = "arm,cortex-a8";
            device_type = "cpu";
            reg = <0x0>;
        };
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x10000000>;  /* 512 MB */
    };

    uart0: serial@44e09000 {
        compatible = "ti,am3352-uart", "ti,omap3-uart";
        reg = <0x44e09000 0x2000>;
        interrupts = <72>;
        clock-frequency = <48000000>;
    };
};

看到没?uart0 节点描述了 UART 的基地址、中断号、时钟频率。内核驱动读到这些信息,就知道怎么初始化这个串口了。

我的习惯: 写设备树时,先看芯片手册,把外设的地址、中断号、时钟频率列个表。然后对照着写节点,不容易出错。

4.2 DTS / DTC / DTB 文件格式

这三个缩写,你肯定见过。它们的关系很简单:

缩写 全称 说明
DTS Device Tree Source 文本格式的源文件,人类可读,后缀 .dts
DTC Device Tree Compiler 编译器,把 DTS 编译成 DTB
DTB Device Tree Blob 二进制格式,机器可读,Bootloader 直接加载

工作流程是这样的:

  1. 工程师写 .dts 文件。
  2. 用 dtc 工具编译成 .dtb 文件。
  3. Bootloader 把 .dtb 加载到内存,解析后传给内核。

编译命令很简单:

dtc -I dts -O dtb -o myboard.dtb myboard.dts

反过来,反编译 DTB 到 DTS 也常用:

dtc -I dtb -O dts -o myboard.dts myboard.dtb
我曾经踩过的坑: 有一次,我改了 DTS 文件,但忘了重新编译 DTB。结果烧进去,板子死活起不来。查了半天,发现 DTB 还是旧的。记住:改完 DTS,一定要重新编译 DTB!

另外,还有 .dtsi 文件,它是公共头文件。比如 am33xx.dtsi 定义了 AM335x 系列芯片的公共外设,不同板卡只需要 #include 它,然后覆盖或添加自己的节点。这样能减少重复代码。

4.3 如何在Bootloader中解析设备树

Bootloader 解析设备树,主要做三件事:

  • 加载 DTB: 从存储介质(Flash、SD卡、网络)读到内存。
  • 解析 DTB: 遍历节点,读取属性,获取硬件信息。
  • 传递 DTB: 把 DTB 在内存中的地址告诉内核。

以 U-Boot 为例,它内置了 fdt 命令,可以直接操作设备树。

常用的 U-Boot 命令:

# 加载 DTB 到内存地址 0x88000000
tftp 0x88000000 myboard.dtb

# 查看设备树内容
fdt list

# 修改某个节点的属性
fdt set /uart0 clock-frequency <50000000>

# 启动内核时,传递 DTB 地址
bootm 0x80000000 - 0x88000000

在代码层面,U-Boot 使用 libfdt 库来解析 DTB。核心 API 有:

函数 作用
fdt_check_header() 检查 DTB 头部是否合法
fdt_path_offset() 根据路径找到节点偏移量
fdt_getprop() 读取节点的某个属性值
fdt_first_subnode() 遍历子节点

我写一个简单的解析示例:

#include <libfdt.h>

void parse_dtb(void *fdt_blob)
{
    int offset;
    const void *prop;
    int len;

    // 1. 检查头部
    if (fdt_check_header(fdt_blob) != 0) {
        printf("Invalid DTB header!\n");
        return;
    }

    // 2. 找到 uart0 节点
    offset = fdt_path_offset(fdt_blob, "/uart0");
    if (offset < 0) {
        printf("Cannot find /uart0\n");
        return;
    }

    // 3. 读取 clock-frequency 属性
    prop = fdt_getprop(fdt_blob, offset, "clock-frequency", &len);
    if (prop) {
        uint32_t freq = fdt32_to_cpu(*(uint32_t *)prop);
        printf("UART clock frequency: %d Hz\n", freq);
    }

    // 4. 读取 interrupts 属性
    prop = fdt_getprop(fdt_blob, offset, "interrupts", &len);
    if (prop) {
        uint32_t irq = fdt32_to_cpu(*(uint32_t *)prop);
        printf("UART IRQ number: %d\n", irq);
    }
}

这段代码,我在好几个项目里都用过。核心逻辑就是:找节点 → 读属性 → 用属性值初始化硬件。

避坑指南: 注意字节序!DTB 里默认是大端(Big Endian),如果你的 CPU 是小端(Little Endian),记得用 fdt32_to_cpu() 转换。我曾经因为这个,读出来的时钟频率是 0,查了半天才发现是字节序问题。

下面这张图,展示了设备树从编写到解析的完整流程:

设备树从编写到解析流程 设备树从编写到解析流程 编写 .dts 文件 dtc 编译为 .dtb 存储 DTB 到 Flash Bootloader 加载 DTB 到内存(如 0x88000000) 解析 DTB 遍历节点,读取属性 根据属性初始化硬件 设置时钟、中断、地址等 传递 DTB 给内核 bootm 命令传递地址 💡 关键点:DTS 是源文件,DTB 是二进制,Bootloader 解析 DTB 获取硬件信息,最后传递给内核。 内核不再需要硬编码硬件信息,完全依赖设备树。

最后,说一个我个人的经验。调试设备树时,最常用的手段就是:

  1. 在 Bootloader 里用 fdt list 查看 DTB 内容,确认节点和属性是否正确。
  2. 如果内核启动时报设备树相关错误,用 dtc -I dtb -O dts 反编译 DTB,对比原始 DTS,看是不是编译或传输过程中出了问题。
  3. 实在不行,在 Bootloader 代码里加打印,把关键属性值打出来,一步步排查。
注意: DTB 的大小有限制。U-Boot 默认的 fdt 命令最大支持 256KB 的 DTB。如果你的设备树很复杂,超过了这个限制,需要修改 U-Boot 的配置。我遇到过一块 FPGA 加 ARM 的板子,设备树写了 300 多 KB,结果加载失败,后来改了 CONFIG_FDT_MAX_SIZE 才解决。