第1章:设备树语法入门——节点、属性、标签、路径的基本规则

各位同学好,我是老李。做嵌入式Linux驱动开发十几年了,设备树这东西,我刚开始接触时也是一头雾水。说实话,它看起来像XML,又有点像JSON,但又不完全是。今天咱们就把设备树最基础的语法掰开揉碎了讲清楚。

设备树,说白了就是描述硬件信息的“说明书”。CPU有几个核?外设挂在哪条总线上?中断号是多少?这些信息都写在设备树里。内核启动时读这份说明书,就知道怎么跟硬件打交道了。

核心概念一句话:设备树是用树形结构描述硬件资源的数据格式,驱动通过它找到自己对应的硬件。

1.1 节点(Node)——设备树的骨架

节点是设备树最基本的组成单元。每个节点代表一个硬件设备或总线。我习惯把节点想象成“文件夹”,里面可以放子节点,也可以放属性。

看个最简单的例子:

/dts-v1/;

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

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

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

这里 / 是根节点,下面挂了 cpu@0memory@80000000 两个子节点。节点名后面的 @0@80000000 是地址,用来区分同类型设备。

我的经验:节点名尽量用通用名,比如 serialuart1 好。因为内核会通过节点名匹配驱动,名字太随意容易出问题。我曾经见过有人把串口节点命名为 my_uart_test,结果驱动死活加载不上……

1.2 属性(Property)——节点的“身份证”

属性就是键值对,描述节点的具体特征。常见的属性类型有:

属性类型 写法示例 说明
字符串 compatible = "vendor,device"; 驱动匹配的关键
32位整数 reg = <0x1000 0x100>; 地址和大小
64位整数 reg = <0x0 0x80000000 0x0 0x40000000>; 高地址+低地址
布尔值 status = "disabled"; 存在即true
字符串列表 interrupt-names = "rx", "tx"; 用空格分隔

这里重点说下 compatible 属性。它是驱动和设备树之间的“暗号”。内核启动时,会遍历所有节点,找到 compatible 值匹配的驱动。格式一般是 "厂商名,设备名"

注意:compatible 的顺序很重要!内核会按列表顺序依次尝试匹配。我遇到过把 fallback 兼容写在前面的情况,结果驱动匹配到了错误的版本,折腾了两天才发现。

1.3 标签(Label)——给节点起个“外号”

标签就是节点的别名。为什么要用标签?你想想看,如果节点路径特别深,比如 /soc/bus@40000000/i2c@1000/rtc@68,每次引用都要写这么长,多累啊。

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

&i2c1: i2c@1000 {
    compatible = "vendor,i2c";
    reg = <0x1000 0x100>;
    rtc@68 {
        compatible = "maxim,ds3231";
        reg = <0x68>;
    };
};

这样在其他地方就可以用 &i2c1 来引用这个节点了。比如在另一个节点里引用它:

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

我的习惯:所有重要的外设节点都加上标签。标签名用 <总线名><序号> 的格式,比如 &i2c1&spi2&uart0。这样在 .dtsi 文件里覆盖配置时特别方便。

1.4 路径(Path)——设备树的“地址”

路径就是从根节点到目标节点的完整路线。用斜杠分隔,比如:

/soc/bus@40000000/i2c@1000/rtc@68

路径在 devicetree 的 API 里经常用到。比如在驱动里通过 of_find_node_by_path() 函数查找节点:

struct device_node *np;
np = of_find_node_by_path("/soc/bus@40000000/i2c@1000/rtc@68");
if (np) {
    // 找到节点,可以读取属性了
}

路径和标签的区别?说白了,路径是绝对地址,标签是相对引用。路径写死了,标签可以灵活覆盖。

1.5 知识体系总览

下面这张图是我画的设备树语法结构图,帮你理清这几个概念的关系:

设备树语法结构图 根节点 / cpu@0 memory@80000000 i2c@1000 标签: &i2c1 属性示例 compatible = "arm,cortex-a7" reg = <0> 属性示例 device_type = "memory" reg = <0x80000000 0x40000000> 属性示例 compatible = "vendor,i2c" reg = <0x1000 0x100> 路径示例:/soc/bus@40000000/i2c@1000/rtc@68 从根节点到目标节点的完整路线 根节点 子节点 标签 属性 路径

1.6 避坑指南

做设备树开发这些年,我踩过不少坑。分享几个最常见的:

  • 分号别漏了——每个属性后面都要加分号。我曾经漏了一个分号,编译报错找了半小时。
  • 标签名别重复——整个设备树里标签必须唯一。重复了编译不会报错,但引用时会混乱。
  • 路径大小写敏感——Linux是大小写敏感的系统,/SOC/soc 是两个不同的路径。
  • 地址别写错进制——0x 开头是十六进制,不加是十进制。我见过有人把 0x1000 写成 1000,结果地址对不上。

特别提醒:设备树编译(dtc)不会检查语义错误。它只检查语法。也就是说,你写个不存在的寄存器地址,编译也能通过。但内核启动时就会挂掉。所以写完设备树,一定要在目标板上实际测试。

好了,设备树的基本语法就这些。节点是骨架,属性是血肉,标签是别名,路径是地址。把这四个概念搞明白,后面学中断、时钟、GPIO映射就轻松多了。

记住一句话:设备树就是硬件的“身份证”,驱动通过它找到对应的硬件。写设备树时,把自己想象成硬件工程师,把每个外设的细节都描述清楚,驱动开发就事半功倍了。


公众号:蓝海资料掘金营,微信deep3321