1、设备树基础概念:什么是设备树、为什么需要设备树、设备树与ACPI的区别、设备树在Linux内核中的角色

1.1 什么是设备树?

设备树,英文叫 Device Tree,简称 DT。说白了,它就是一套描述硬件信息的「数据结构」。你想想看,Linux 内核要跑起来,总得知道板子上有哪些硬件吧?CPU 是什么型号?内存多大?I2C 控制器挂在哪?GPIO 怎么映射?

在早期,这些信息都硬编码在 Linux 内核源码里。每个板子一个 board 文件,里面全是平台设备、资源注册的代码。我刚开始做嵌入式那会儿,最怕的就是换板子。换个 Flash 型号,得改内核代码,重新编译。效率低不说,还容易引入 bug。

设备树解决了这个问题。它把硬件描述从内核代码里剥离出来,变成一份独立的 .dts 文件。内核启动时,通过解析这个文件,动态构建出硬件设备模型。嗯,这里要注意:设备树本身不是代码,它只是一份「硬件清单」。

核心要点:设备树是一种描述硬件资源的树形数据结构,它让 Linux 内核与具体硬件解耦。

1.2 为什么需要设备树?

我遇到过不少刚入行的朋友问:没有设备树,内核不也能跑吗?确实能跑。但你要知道,嵌入式世界和 PC 不一样。PC 的硬件是标准化的,BIOS/UEFI 能帮你枚举出所有设备。嵌入式呢?千奇百怪。同一个 SoC,可能被用在路由器、机顶盒、工业控制器上,外设完全不同。

没有设备树之前,Linux 社区是怎么做的?每个 ARM 板子都往内核里塞一个 board-xxx.c 文件。结果呢?内核源码里充斥着大量重复的、平台相关的代码。维护起来简直是噩梦。

设备树带来的好处,我总结了几点:

  • 硬件与内核解耦:换硬件只需改 .dts 文件,不用改内核代码
  • 统一硬件描述语言:所有 ARM 平台用同一套语法描述硬件
  • 减少内核分支:厂商不用再维护一堆内核补丁
  • 启动参数灵活:内存大小、时钟频率等可以在设备树里直接配

我个人习惯是,拿到一块新板子,第一件事就是看它的 .dts 文件。这比看原理图还快,能帮你快速了解整个板子的硬件拓扑。

1.3 设备树与 ACPI 的区别

说到这个,很多做 x86 开发的朋友会问:ACPI 不也是描述硬件的吗?没错,但两者有本质区别。

对比项 设备树 (DT) ACPI
起源 嵌入式、ARM 生态 x86、PC 生态
格式 文本文件 (.dts),编译成二进制 (.dtb) AML 字节码,存储在 BIOS 中
运行时 内核启动时静态解析 支持运行时动态修改(热插拔、电源管理)
复杂度 简单、轻量 庞大、功能丰富
适用场景 嵌入式、资源受限设备 PC、服务器、复杂电源管理

我曾经在做一个 ARM + FPGA 的混合项目时,客户非要上 ACPI。结果呢?ACPI 的 AML 解释器在 ARM 上跑得慢,而且很多嵌入式外设根本不需要那么复杂的电源管理。最后还是换回了设备树。说白了,设备树就是为嵌入式量身定做的——简单、直接、够用。

我的建议:如果你的板子只有几十个外设,没有热插拔需求,用设备树就够了。别为了「高大上」硬上 ACPI,那是给自己找麻烦。

1.4 设备树在 Linux 内核中的角色

设备树在 Linux 内核里扮演什么角色?我打个比方:它就像一份「硬件说明书」。内核启动时,Bootloader 把 .dtb 文件加载到内存,然后把地址传给内核。内核解析这份说明书,知道哪里有内存、哪里有串口、哪个 GPIO 接了 LED,然后一一初始化。

具体流程是这样的:

  1. Bootloader(如 U-Boot)加载内核镜像和 .dtb 文件
  2. 内核启动早期,调用 unflatten_device_tree() 解析 .dtb
  3. 解析结果存为 struct device_node 树形结构
  4. 驱动通过 of_* 系列 API 查询设备树节点
  5. 匹配到 compatible 属性后,驱动 probe 函数被调用

你看,整个过程是「静态」的。内核启动时读一次,后面就不改了。这也是设备树和 ACPI 最大的不同——ACPI 支持运行时动态修改,设备树不行。

避坑指南:我曾经遇到一个坑——设备树里内存节点写错了,导致内核只识别了 512MB 内存,而板子上实际有 1GB。查了两天才发现是 .dts 里 reg = <0x80000000 0x20000000> 写成了 0x10000000。所以,拿到新板子,第一件事就是核对设备树里的内存大小、串口地址这些关键参数。

在 Linux 内核里,设备树相关的代码主要分布在 drivers/of/ 目录下。常用的 API 有:

// 查找设备树节点
struct device_node *np = of_find_node_by_path("/soc/uart@fe000000");

// 读取属性值
const char *str;
of_property_read_string(np, "compatible", &str);

// 读取中断号
int irq = irq_of_parse_and_map(np, 0);

// 读取寄存器地址
struct resource res;
of_address_to_resource(np, 0, &res);

嗯,这些 API 我几乎每天都在用。写驱动时,80% 的工作就是和设备树打交道——读属性、映射地址、注册中断。说白了,设备树就是驱动和硬件之间的「翻译官」。

最后说一句:设备树虽然叫「树」,但它本质上是一个扁平的属性集合。每个节点就是一组键值对,只是通过父子关系组织成了树形结构。理解这一点,你就能明白为什么设备树解析起来那么快了。