2. 设备树基础语法:节点、属性、值的表示方法、设备树源文件(.dts)结构

好,咱们正式开始啃设备树这块硬骨头。说实话,我刚接触 Android 底层时,看到 .dts 文件第一反应是——这玩意儿长得像 C 语言的结构体,又有点像 JSON,但又不完全是。后来被坑了几次才摸清楚门道。

设备树说白了就是一套描述硬件信息的「说明书」。内核通过它知道你的板子上有哪些外设、它们挂在哪条总线上、地址是多少、中断怎么配。嗯,就这么简单。

2.1 节点(Node)——硬件的最小单元

每个硬件设备或总线,在设备树里都是一个节点。节点用花括号 { } 包裹,里面可以嵌套子节点。

/ {
    model = "My AOSP Board";
    compatible = "vendor,board-v1";

    cpu@0 {
        device_type = "cpu";
        reg = <0x00>;
    };

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

看到没?根节点 / 下面挂了 cpu 和 memory。每个节点名字后面可以跟 @地址,这个地址通常是该设备在总线上的基地址。我个人习惯把地址写全,哪怕只有一个设备,也写上 @0,这样后面调试时一眼就能看出映射关系。

小技巧:节点名尽量用标准名称,比如 cpu、memory、i2c、spi。内核里有很多匹配规则是硬编码的,名字写错了可能驱动加载不上。

2.2 属性(Property)——节点的特征描述

属性就是键值对,用来描述节点的各种特征。常见的属性有:

  • compatible:最重要的属性,驱动匹配全靠它。格式是 "厂商,设备型号"
  • reg:地址和长度信息,格式是 <地址 长度>
  • interrupts:中断号、触发方式等。
  • status:设备状态,常用 "okay""disabled"
uart@1c28000 {
    compatible = "snps,dw-apb-uart";
    reg = <0x1c28000 0x1000>;
    interrupts = <0 0 4>;
    clock-frequency = <24000000>;
    status = "okay";
};

这里我多说一句:compatible 的命名一定要规范。我曾经接手过一个项目,同事把 "vendor,device" 写成了 "device-vendor",结果驱动死活匹配不上,查了两天才发现是字符串顺序反了。你想想看,这种低级错误多耽误事。

2.3 值的表示方法——三种基本类型

设备树的值有三种基本形式,搞懂了它们,写 .dts 就顺了。

类型 表示方式 示例
32位整数 <值1 值2 ...> <0x1000 0x2000>
字符串 "字符串" "okay"
字节数组 [字节1 字节2 ...] [00 01 02]

还有一种混合写法,比如 <&gpio1 0 GPIO_ACTIVE_HIGH>,这种在 GPIO 和中断里很常见。说白了就是引用一个 phandle(指针),再跟上参数。

重点:整数默认是大端序。如果你在 x86 机器上交叉编译,解析设备树时要注意字节序问题。我踩过这个坑,调试时打印出来的 reg 值全是反的。

2.4 设备树源文件结构——.dts 的骨架

一个完整的 .dts 文件通常包含这几部分:

  1. 头文件包含#include 引入 .dtsi 文件(类似 C 语言的 .h)
  2. 根节点/ { }; 定义整个板级信息
  3. 子节点:各种外设节点
  4. & 引用:通过 &节点标签 修改或追加已有节点
// 引入 SoC 级设备树
#include "msm8998.dtsi"

/ {
    model = "Qualcomm MSM8998 MTP";
    compatible = "qcom,msm8998-mtp";

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

// 引用 SoC 中的 uart 节点,追加板级配置
&uartblsp1 {
    status = "okay";
    pinctrl-0 = <&uart_console_active>;
};

这种 &引用 的写法非常实用。SoC 厂商提供 .dtsi 文件定义了所有外设的默认配置,板级 .dts 只需要引用并修改需要的部分。比如把某个 UART 从 disabled 改成 okay,或者调整时钟频率。

注意:.dtsi 文件里定义的节点,如果在 .dts 里没有显式引用,默认就是 status = "disabled"。所以别以为写了节点就能用,一定要检查状态。

2.5 实战经验:一个典型的调试场景

我记得有一次调试 I2C 触摸屏,设备树里配了地址和中断,但内核就是报 no device found。后来用 cat /proc/device-tree 查看实际解析出来的设备树,发现 reg 属性里的地址是 0x38,但触摸屏芯片手册上写的是 0x38(7位地址)。

问题出在哪?I2C 地址在设备树里要写 7 位地址,但驱动里可能用了 8 位地址(左移一位)。说白了就是地址表示方式没对齐。从那以后,我每次配 I2C 设备都会先确认芯片手册上的地址格式。

嗯,这里再补充一个实用技巧:调试设备树时,可以用 dtc -I dtb -O dts 把编译好的 .dtb 反编译回 .dts,看看实际解析结果是不是你想要的。这个命令我几乎每天都要用。

2.6 小结

设备树语法其实不复杂,核心就是节点、属性、值这三板斧。但真正用好它,需要你对硬件有足够的了解。我见过太多人把设备树当成「玄学」,配错了就瞎试。其实你只要记住:

  • 节点对应硬件模块
  • 属性描述模块特征
  • 值就是具体的参数

下次遇到设备树问题,先反编译看看实际解析结果,再对照芯片手册逐项检查。相信我,90% 的问题都能这样解决。