设备树语法基础:DTS文件结构、节点和属性、数据类型、标签和引用、包含文件

设备树(Device Tree)说白了就是一块「硬件描述板」。你写进去什么外设、什么地址、什么中断,内核启动时就读什么。我刚开始接触Yocto时,总觉得设备树是内核的事,跟构建系统没关系。后来被坑了几次才明白——设备树写不对,整个系统都起不来。

这一章我们就把设备树的语法基础掰开揉碎。你掌握了这些,后面做Yocto集成才不会手忙脚乱。

4.1 DTS文件结构:从根节点往下看

一个标准的DTS文件,结构其实很简单。它就是一个树形结构,从根节点 / 开始,往下挂各种子节点。每个节点描述一个硬件设备或总线。

/dts-v1/;

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

    chosen {
        stdout-path = &uart0;
    };

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

    leds {
        pinctrl-names = "default";
        pinctrl-0 = <&led_pins>;

        led@0 {
            label = "beaglebone:green:usr0";
            gpios = <&gpio1 21 GPIO_ACTIVE_HIGH>;
            linux,default-trigger = "heartbeat";
        };
    };
};

你看,根节点下面有 chosen、memory、leds 这些子节点。每个节点用花括号包裹,末尾加分号。节点名后面可以跟 @地址,比如 memory@80000000,表示这个内存的基地址是0x80000000。

我的习惯:写DTS文件时,我一般先把根节点下的顶层节点列出来——memory、chosen、cpus、soc。这些是标配。然后再往里填具体外设。这样结构清晰,不容易漏。

4.2 节点和属性:设备树的「名词」和「形容词」

节点就是硬件设备,属性就是描述这个设备的参数。你可以把节点想象成「名词」,属性就是「形容词」。

每个节点必须有一个 compatible 属性,告诉内核这个设备用什么驱动。格式是 "厂商,型号"。比如 "ti,am335x-bone-black",内核匹配驱动时,会从最具体的开始试,不行再试通用的。

常用的属性我列个表,你写设备树时基本都会用到:

属性名 作用 示例
compatible 驱动匹配字符串 "ti,am335x-uart"
reg 寄存器地址和大小 <0x44e09000 0x2000>
interrupts 中断号与触发方式 <0 56 4>
clocks 时钟引用 <&clk_uart0>
status 设备启用/禁用 "okay" 或 "disabled"
pinctrl-0 引脚复用配置 <&uart0_pins>
我曾经踩过的坑:有一次我写SPI控制器,忘了加 status = "okay"。内核默认是 disabled,结果SPI设备死活不工作。查了两天才发现是这个问题。所以,新加的节点一定要显式设置 status。

4.3 数据类型:u32、u64、字符串和数组

设备树里的数据类型不多,但每个都有讲究。我直接给你总结:

  • 整数(u32):用尖括号包裹,比如 <0x1000>。多个整数用空格隔开:<0x1000 0x2000>。
  • 64位整数(u64):用两个u32表示,比如 <0x00000000 0x80000000> 表示0x80000000。
  • 字符串:用双引号包裹,比如 "okay"。
  • 字符串列表:用逗号分隔,比如 "ti,am335x-bone", "ti,am33xx"。
  • 字节数组:用方括号包裹,比如 [00 01 02 03]。
  • 混合类型:用 <> 和 [] 组合,比如 <0x1000> [00 01]。

你可能会问:为什么64位地址要用两个u32?因为设备树编译器(DTC)默认按32位处理。如果你用 #address-cells = <2>,那地址就是64位的。这个后面讲地址映射时会细说。

4.4 标签和引用:别写死,用「指针」

设备树里最让我觉得舒服的设计就是标签(label)和引用(phandle)。你想想看,如果每个节点都写全路径,比如 /soc/spi@48030000,那改个名字或层级,所有引用都得改。多麻烦。

标签的写法很简单,在节点名前加个名字和冒号:

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

这里 uart0 就是标签。其他地方想引用这个节点,用 &uart0 就行:

chosen {
    stdout-path = &uart0;
};

编译器会把 &uart0 自动替换成对应的phandle(一个整数指针)。这样既清晰又灵活。

我个人的习惯:所有重要的外设节点,我都会加标签。比如 uart0、i2c0、spi0、mmc0。命名规则统一用「外设名+编号」。这样在Yocto的bbappend文件里覆盖节点时,直接 &uart0 { ... }; 就搞定了,不用写一长串路径。

4.5 包含文件:dtsi和dts的分工

一个完整的设备树,不可能全写在一个文件里。那样太乱了。实际项目中,我们会把通用的部分放在 .dtsi 文件里,把板级特有的部分放在 .dts 文件里。

.dtsi 就像C语言的头文件。它定义SoC通用的外设、时钟、中断控制器等。.dts 则包含具体的板级配置——内存大小、引脚复用、外设使能等。

包含的语法和C语言一样:

// am335x-boneblack.dts
#include "am33xx.dtsi"
#include "am335x-bone-common.dtsi"

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

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

&uart0 {
    status = "okay";
};

&mmc1 {
    status = "okay";
    vmmc-supply = <&vmmcsd_fixed>;
};

你看,am33xx.dtsi 里定义了UART、MMC、I2C等外设节点,但状态都是 disabled。板级文件里用 &uart0 { status = "okay"; }; 来启用。这就是设备树的「增量覆盖」机制。

核心思路:SoC厂商提供 .dtsi,板级厂商提供 .dts。你在Yocto里做定制,通常只需要修改或追加 .dts 文件,不要动 .dtsi。这样升级内核时,SoC的通用配置自动更新,你的板级改动不受影响。

4.6 知识体系总览

说了这么多,我画张图帮你把整个知识体系串起来。你一看就明白设备树语法各部分之间的关系了。

设备树语法知识体系 DTS 文件结构 节点与属性 数据类型 标签与引用 包含文件(dtsi/dts) compatible reg / interrupts u32 / u64 字符串 / 数组 label: 节点 &label 引用 #include "soc.dtsi" 增量覆盖 &node 核心原则:dtsi 定义通用硬件,dts 做板级定制,Yocto 通过 bbappend 做增量修改

这张图把设备树语法的五个核心模块串起来了。你写设备树时,脑子里要有这个结构:先搭文件框架(dtsi/dts),再定义节点和属性,用标签做引用,用正确的数据类型填值。每一步都有章可循。

一个小建议:刚开始学设备树,别急着写复杂的。先拿一块开发板的现有DTS文件,对照着看。打开 arch/arm/boot/dts/ 目录,找一个你熟悉的板子,一行一行读。读懂了再改,改通了再自己写。我当年就是这么过来的。