设备树基础语法(一):.dts/.dtsi 文件结构、节点与属性

好,咱们正式开始啃设备树这块硬骨头。说实话,我刚接触设备树那会儿,看着满屏的尖括号和花括号,心里直犯嘀咕——这玩意儿跟 C 语言长得挺像,但又完全不是一回事。后来做项目多了,才慢慢摸清它的脾气。

设备树说白了就是一套描述硬件信息的「说明书」。你想想看,Linux 内核要跑在不同的板子上,总不能每换一块板子就重新编译一次内核吧?设备树就是用来解决这个问题的——把硬件信息从内核代码里抽出来,单独描述。

一、设备树文件长什么样?

设备树源文件有两种后缀:.dts 和 .dtsi。我个人习惯把 .dts 看作「板级文件」,.dtsi 看作「芯片级头文件」。就像 C 语言里的 .c 和 .h 一样。

来看一个最简单的例子:

/dts-v1/;

/ {
    model = "MyCustomBoard";
    compatible = "vendor,myboard-v1", "vendor,myboard";

    chosen {
        bootargs = "console=ttyS0,115200";
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x10000000>;
    };

    cpus {
        cpu@0 {
            compatible = "arm,cortex-a7";
            reg = <0>;
        };
    };
};

嗯,这里要注意几个关键点:

  • /dts-v1/; 是版本声明,必须写在最前面。我见过有人漏掉这行,结果编译报错半天找不出原因。
  • 根节点 / 是整个设备树的起点,所有设备都挂在这个根下面。
  • 节点名后面可以跟 @地址,比如 memory@80000000,这个地址用来区分同名的节点。

核心要点:设备树是一棵倒挂的树,根节点是 /,下面挂 CPU、内存、外设等节点。每个节点描述一个硬件设备或总线。

二、节点(node)的定义与规则

节点是设备树的基本组成单元。每个节点用花括号 { } 包裹,里面可以包含属性和子节点。

节点命名的规则其实挺宽松的,但我建议你遵循几条约定:

  • 节点名尽量用硬件手册里的名称,比如 uart、i2c、spi
  • 如果同一类型有多个实例,用 @地址 区分,比如 uart@10000000 和 uart@20000000
  • 不要用特殊字符,保持字母、数字、下划线、连字符

节点可以嵌套,形成层级关系。比如:

&i2c1 {
    status = "okay";
    clock-frequency = <100000>;

    eeprom@50 {
        compatible = "atmel,24c02";
        reg = <0x50>;
    };

    temperature@48 {
        compatible = "lm75";
        reg = <0x48>;
    };
};

这里 i2c1 是父节点,eeprom@50 和 temperature@48 是子节点。子节点通过 reg 属性指定 I2C 地址。

我的经验:写节点名时,尽量跟芯片手册保持一致。我曾经接手一个项目,前任工程师把 UART 节点命名为 serial0,结果驱动里找 uart0 死活匹配不上,排查了整整一个下午。

三、属性(property)的定义与类型

属性就是节点的「参数」。设备树规范定义了多种属性类型,咱们一个一个来看。

属性类型 语法示例 说明
字符串 compatible = "vendor,device"; 用双引号包裹,用于匹配驱动
32位整数 reg = <0x10000000 0x1000>; 尖括号包裹,多个值用空格隔开
64位整数 reg = <0x00000000 0x80000000 0x0 0x10000000>; 用两个32位表示一个64位值
布尔值 status = "okay"; 实际上用字符串表示,常见值:okay、disabled
字节数组 local-mac-address = [00 11 22 33 44 55]; 方括号包裹,十六进制字节
混合类型 foo = <0x1234>, "string"; 逗号分隔不同类型

最常用的属性是 compatible 和 reg。前者告诉内核「这个设备是什么型号」,后者告诉内核「设备的地址和大小」。

// 一个典型的 SPI 控制器节点
spi@10001000 {
    compatible = "vendor,spi-controller";
    reg = <0x10001000 0x1000>;
    interrupts = <0 31 4>;
    clocks = <&clkc 15>;
    #address-cells = <1>;
    #size-cells = <0>;

    flash@0 {
        compatible = "jedec,spi-nor";
        reg = <0>;
        spi-max-frequency = <50000000>;
    };
};

避坑指南:我曾经在 reg 属性里写错地址范围,导致驱动申请内存时失败。记住,reg 的第一个值是起始地址,第二个值是长度。别搞反了!

四、设备树文件的结构组织

实际项目中,设备树文件通常按模块拆分。我一般这样组织:

  • 芯片级 .dtsi:描述 SoC 内部资源,比如 CPU、中断控制器、时钟、DMA 等
  • 板级 .dts:描述板级外设,比如 DDR 大小、网卡、USB 接口等
  • 头文件 .h:定义宏和常量,方便复用

用 #include 来组合它们:

// myboard.dts
/dts-v1/;

#include "soc.dtsi"
#include "myboard-pinfunc.h"

/ {
    model = "MyBoard v2";
    compatible = "vendor,myboard-v2";

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

// 覆盖 soc.dtsi 中的节点
&uart1 {
    status = "okay";
    pinctrl-0 = <&uart1_pins>;
};

看到 &uart1 这种写法了吗?这叫「引用标签」,用来覆盖或扩展其他文件中定义的节点。这是设备树最强大的特性之一。

核心思想:芯片厂商提供 .dtsi,板级开发者提供 .dts。通过 #include 和标签引用,实现「芯片通用 + 板级定制」的分离。

五、知识体系结构图

下面这张图帮你理清设备树文件的结构关系:

根节点 / chosen memory@80000000 cpus cpu@0 属性示例: compatible = "arm,cortex-a7"; reg = <0>; device_type = "cpu"; 属性类型:字符串、32位整数、64位整数、布尔值、字节数组... 设备树结构:根节点 → 子节点 → 子子节点,每个节点携带属性

这张图展示了设备树的层级结构。从根节点出发,每个节点都可以挂载子节点,形成一棵完整的硬件描述树。属性则附着在节点上,提供具体的配置信息。

六、避坑与实战建议

最后分享几个我踩过的坑:

  • 分号别漏:每个属性后面都要加分号,漏一个编译就报错。我刚开始写的时候,经常漏掉最后一个属性后面的分号。
  • 逗号别乱用:字符串列表用逗号分隔,但整数列表不用。比如 compatible = "a", "b"; 是对的,但 reg = <0x1000, 0x2000>; 是错的。
  • 标签引用要小心:&node 只能用于覆盖已有节点,不能凭空创建新节点。如果你引用的标签不存在,编译会报错。
  • 地址和大小要匹配:#address-cells 和 #size-cells 决定了 reg 中每个地址和大小占几个32位。不匹配的话,驱动读到的地址就是错的。

我的习惯:每次写完设备树,先用 dtc -I dts -O dtb test.dts 编译成 dtb,再用 dtc -I dtb -O dts test.dtb 反编译回来,看看有没有信息丢失。这招帮我发现过不少低级错误。

好了,设备树的基础语法就讲到这里。记住三个核心:文件结构、节点定义、属性类型。把这三点吃透了,后面的内容就好办了。

可可精品资料库