什么是设备树?

August 7, 2026 · 访客

只要你在嵌入式板子上折腾过 Linux,迟早会遇到一个以 .dts 结尾的文件。通常还是在最糟糕的时候:板子起不来,屏幕上什么都没有,然后有人告诉你「去看看你的设备树」。这篇文章讲的就是那个文件是什么、为什么存在,以及你在里面究竟该找什么。

它要解决的问题

在台式机上,内核基本能自己找到硬件。PCI 和 USB 是有正式规范、可枚举的总线(1)。设备会用厂商号和产品号自报家门;驱动在一张表里列出它支持的这些标识,内核对每个匹配表项的设备调用该驱动的 probe() 函数(2)。USB 那边也是同样的机制,靠自己的标识表(3)。谁都不需要事先告诉谁任何事。

嵌入式板子没有这份奢侈。内核文档把这条界线划得很直白:集成在片上系统里的外设挂在基础设施极简的总线上,「与 PCI 或 USB 这类规模大且有正式规范的总线相对」,而这些设备的共同点是直接从 CPU 总线寻址(1)。一个 I²C 传感器,或者挂在 SPI 上的显示控制器,不会自报家门;它就那样待在那个地址上,接在那根中断线上。内核唯一能知道它的途径,就是有人告诉内核。

很长一段时间里,这个「有人」就是代码本身。每块板子在内核树里都有一个文件,用 C 描述哪个设备接在哪里——内核至今仍在文档里描述这条路径:板级初始化代码把设备一个一个注册进去(1)。在 ARM 上这些文件多到什么程度呢?2011 年 Linus Torvalds 在一封邮件里把这个局面拆得很难看,说 ARM 代码正走向不可维持的方向。最终被采纳的出路,就是设备树。

这个想法

设备树是一个描述硬件如何连接的数据文件。它不是代码——不会运行,也不会被编译进内核。它是一份描述:这个地址上有个控制器,它用这根中断线,由这个时钟供给,下面挂着这些设备。

格式本身有正式规范作依据(4)。之所以叫「树」,是因为它确实是棵树。从根节点开始,总线从根上分叉,挂在总线上的设备再从总线分叉出去——如实映射硬件的物理连接。

与其编一个例子,不如看真实的。下面这个节点来自 TI J722S/AM67A 系列 U-Boot 树中的 k3-am62p-main.dtsi,也就是一块 AM67A 板子启动时真正读到的描述:

main_i2c0: i2c@20000000 {
    compatible = "ti,am64-i2c", "ti,omap4-i2c";
    reg = <0x00 0x20000000 0x00 0x100>;
    interrupts = <GIC_SPI 161 IRQ_TYPE_LEVEL_HIGH>;
    #address-cells = <1>;
    #size-cells = <0>;
    power-domains = <&k3_pds 102 TI_SCI_PD_EXCLUSIVE>;
    clocks = <&k3_clks 102 2>;
    clock-names = "fck";
    status = "disabled";
};

这里每一行都有含义。reg 给出控制器在内存映射中的地址——0x20000000,长度 0x100。地址之所以写成两个数一组,是因为地址空间是 64 位的,第一个数承载高 32 位。interrupts 说明用的是哪根中断线;GIC_SPIIRQ_TYPE_LEVEL_HIGH 这类名字来自被包含的头文件——也就是说,设备树源文件虽然只是纯数据,却依然要过一遍 C 预处理器。

值得注意的是 compatible 带了两个值:"ti,am64-i2c""ti,omap4-i2c"。这个列表按从具体到通用的顺序读。内核先找专门针对 am64 的驱动,找不到就回落到更老、更通用的 omap4 驱动(5)。这样一来,新芯片不必等人专门为它写驱动就能跑起来。

clocksclock-names 说明设备的时钟从哪来:&k3_clks 上 102 号设备的第 2 路输出,驱动会用 "fck" 这个名字找到它。power-domains 用同样的逻辑接上电源域。这些都不是频率设置——它们描述的是接线,哪个时钟通向哪里。

最后一行很关键:status = "disabled"。芯片厂商描述了这个外设,却让它保持关闭,因为它无从知道那条 I²C 总线在你的板子上是否用得到。打开它是板子自己 .dts 的职责。在 J722S EVM 的板级文件里是这样:

&main_i2c0 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&main_i2c0_pins_default>;
    clock-frequency = <400000>;

    exp1: gpio@23 {
        compatible = "ti,tca6424";
        reg = <0x23>;
        gpio-controller;
        #gpio-cells = <2>;
    };
};

开头的 &main_i2c0 是对上面那个节点的引用——它不新建节点,而是覆盖已有的那个。status 被打开,引脚被分配,速率定在 400 kHz,下面还加进了一个真实设备:挂在 I²C 总线 0x23 地址上的 TCA6424 GPIO 扩展器。因为控制器声明了 #address-cells = <1>,这个地址只用一个数字来写,跟控制器自己的 reg 不同。

这就是设备树在实践中的运作方式:厂商把 SoC 描述一次,你说明自己板子上接了什么。

真正要紧的属性是 compatible。内核就是靠它把驱动和硬件配对的。驱动说「我兼容 ti,tca6424」,设备树里的某个节点带着同样的字符串,内核就把两者凑到一起。所以打错字会静默失败:字符串对不上并不会报错——设备只是从来没出现过。

有一点值得记住:设备树描述的是硬件,不是驱动的配置。内核关于编写 binding 的指南特别强调了这条界线(6)。实践中这条线一直在模糊,也是内核邮件列表上争论最多的话题——「这到底是硬件的属性,还是你的偏好?」

这些文件

你会见到三种扩展名,而且很容易混(11)。

.dts 是你写的源文件,人能读的形式。.dtsi 是可以被其他文件包含的片段——「i」取自 include。芯片厂商通常把整个 SoC 描述在一个 .dtsi 里;你为自己的板子写一个简短的 .dts,把它包含进来,只补上本板特有的部分。

.dtb 是编译后的形式——设备树 blob。内核真正读的就是它。编译器是 dtc(7):

dtc -I dts -O dtb -o my-board.dtb my-board.dts

它也能反着跑,这在查问题时极其有用。手上有一个能用的 .dtb,就可以把它拆开看看里面:

dtc -I dtb -O dts -o decoded.dts my-board.dtb

在运行中的系统上,你还能直接从文件系统读设备树。内核把它实际使用的那棵树摊在这里:

ls /proc/device-tree/

对于「我改的那个文件,是不是被加载的那个文件?」这个问题,这是最快的答案。相当高的比例下,答案是「不是」。

启动时发生了什么

顺序是这样的:引导加载程序——通常是 U-Boot(8)——把 .dtb 装进内存,装载内核,然后在启动内核时用一个寄存器把 blob 的内存地址交给它。在 ARM 上,这个寄存器以及引导程序必须满足的条件,写在内核的启动文档里(9)。在拉起任何驱动之前,内核先读这棵树,从中取出设备,再按 compatible 字符串去匹配驱动。

实际的后果是:内核和设备树可以分开更新。加一个传感器很少需要重新编译内核,更新 .dtb 通常就够了。但这是把双刃剑——正因为二者独立,它们可能对不上。用新内核配旧 .dtb 启动,板子会悄无声息地起得不完整,而你要花上一阵才能弄明白为什么。

还有 overlay 这个概念:在运行时叠加到既有树之上的片段(10)。你往树莓派上插一块 HAT 时发生的就是这件事——主树保持原样,一个小片段覆盖上去。当底板始终不变、只有插上去的模块在换时,这很有用。

当某个东西不工作时

设备树的故障有一种独特的讨厌之处:它们通常根本不报错。设备就是不在。按这个顺序排查能省时间。

先确认设备到底有没有被创建。/proc/device-tree 下有那个节点吗?没有的话,问题就不在你改的那个文件里——多半是加载了错误的 .dtb,或者节点在上一层被 status = "disabled" 关掉了。后面这种情况天天发生:厂商的 .dtsi 文件里外设一般是禁用状态,需要你在自家板子的 .dts 里用 status = "okay" 一个个打开。

节点在,但没有驱动挂上去,那就盯住 compatible 字符串。到驱动源码里找到 of_device_id 表,逐字符对比。差一个逗号、一个连字符就够了。

还有一点:dtc 的警告在多数项目里是关着的,而它们说的东西是真有用。地址与 reg 对不上、缺少 #address-cells ——恰恰是那种会在启动时变成静默故障的错误。构建时把警告开着是值得的。

收尾

设备树之所以难把握,是因为没人一开始就说清楚它到底是什么。它看着像配置文件,但不是;看着像驱动,也不是。最有用的框架是把它当作硬件与软件之间的契约:板子是怎么接的,用内核听得懂的语言写下来。

一旦放进这个框架,其余的就顺理成章了。为什么 compatible 如此关键,为什么 .dtb 独立于内核存在,为什么少了什么东西时第一个该看的是 /proc/device-tree ——全都出自同一个想法。

相关阅读

参考资料

  1. Platform Devices and Drivers — 为什么 SoC 外设不像 PCI 和 USB 那样可枚举,以及板级代码如何注册它们。
  2. How To Write Linux PCI Driverspci_device_id 表以及对匹配设备调用 probe()
  3. USB Device Drivers — USB 侧基于标识的驱动匹配。
  4. Devicetree Specification — 该格式的正式定义。
  5. Linux and the Devicetree — 内核如何读取这棵树并匹配驱动。
  6. Writing Devicetree Bindings — 描述硬件与配置驱动之间的界线。
  7. dtc — Device Tree Compiler — 编译器自身的仓库与用法。
  8. U-Boot: Devicetree Control — U-Boot 如何处理设备树。
  9. Booting ARM Linux — 引导程序用哪个寄存器把 blob 交给内核。
  10. Devicetree Overlay Notes — overlay 在运行时如何被应用。
  11. Device Tree Reference (eLinux) — 带实例的社区参考资料。

评论