2、设备树语法:节点、属性、标签、包含文件、宏定义

好,咱们今天聊聊设备树的基础语法。说实话,这部分内容看起来琐碎,但却是你写设备树的「基本功」。我见过不少工程师,上来就复制粘贴别人的dts,结果板子跑不起来,查半天发现是语法写错了。嗯,咱们先把地基打牢。

2.1 节点:设备树的骨架

设备树本质上是一棵树。每个硬件设备就是一个节点。节点用花括号 {} 包裹,里面可以嵌套子节点。

最基本的节点长这样:

/dts-v1/;

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

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

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

看到没?根节点是 /,下面挂了一个CPU节点和一个内存节点。每个节点都有一个名字,比如 cpu@0。@后面的数字是地址,用来区分同类型设备。我在项目中遇到过有人把两个CPU都写成 cpu@0,结果内核只认了一个——这种低级错误其实挺常见的。

节点命名规则:

  • 节点名格式:名称@地址
  • 名称尽量用通用名,比如 cpu、memory、i2c、spi
  • 地址部分用十六进制,小写

2.2 属性:描述设备的语言

节点里放的是属性。属性就是键值对,用来描述设备的特征。常用的属性类型有几种:

属性类型 写法 说明
字符串 compatible = "vendor,device"; 用来匹配驱动,最重要
32位整数 reg = <0x10000000 0x1000>; 地址和大小,用尖括号
64位整数 reg = <0x00 0x80000000 0x00 0x10000000>; 两个32位拼一个64位
布尔值 status = "disabled"; 或者 "okay",没有true/false
字节数组 local-mac-address = [00 11 22 33 44 55]; 用方括号,十六进制
字符串列表 interrupt-names = "rx", "tx"; 用逗号分隔

这里我要特别强调一下 compatible 属性。说白了,它就是设备和驱动之间的「暗号」。内核通过这个字符串找到对应的驱动。我建议你养成好习惯:格式用 厂商名,设备名,比如 ti,am335x-uart。我曾经接手过一个项目,compatible 写成了 uart,am335x,结果驱动死活匹配不上——折腾了两天才发现是顺序反了。

我的小技巧:写 reg 属性时,地址和长度成对出现。比如 reg = <0x10000000 0x1000> 表示从 0x10000000 开始,大小 4KB。如果有多段地址,就写多对:reg = <0x10000000 0x1000 0x20000000 0x2000>;

2.3 标签:给节点起个外号

标签就是节点的别名。用 标签名: 节点名 的格式。为什么要用标签?你想想看,如果设备树里有个串口节点,你在好几个地方都要引用它,每次都写完整路径多麻烦。

uart0: serial@10000000 {
    compatible = "ns16550";
    reg = <0x10000000 0x1000>;
    interrupts = <32>;
};

// 在其他地方引用
&uart0 {
    clock-frequency = <24000000>;
};

看到 &uart0 了吗?这就是通过标签引用节点。我在项目中习惯给所有重要外设都加上标签,比如 i2c0spi1mmc0。这样后面要修改或者引用的时候,直接 &i2c0 就搞定了,不用去翻整棵树找路径。

注意:标签名不能重复。整个设备树里,标签是全局唯一的。如果你不小心写了两个 uart0,编译器会报错。嗯,我犯过这个错,编译时一脸懵。

2.4 包含文件:别重复造轮子

设备树支持 #include 预处理指令。这跟C语言的头文件是一个道理。常用的包含文件有:

  • .dtsi 文件:设备树包含文件,放通用定义
  • .h 文件:头文件,放宏定义

举个例子:

#include "soc.dtsi"
#include "pinctrl.h"

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

    &uart0 {
        pinctrl-0 = <&uart0_pins>;
        status = "okay";
    };
};

这里 soc.dtsi 里定义了 SoC 的公共部分,比如所有外设的基地址、中断号。你的板级文件只需要 #include 进来,然后修改或启用需要的部分就行。我建议你把 SoC 的通用配置都放到 .dtsi 里,板级差异放到 .dts 里。这样换板子时只需要改 .dts,省事不少。

2.5 宏定义:让设备树更清爽

设备树里可以用宏。宏定义通常放在 .h 文件里,然后在 .dts#include 进来。比如:

// pinctrl.h
#define PIN_CTRL_DEFAULT  0x00
#define PIN_CTRL_PULLUP   0x01
#define PIN_CTRL_PULLDOWN 0x02

// board.dts
#include "pinctrl.h"

&uart0_pins {
    pinmux = <PIN_CTRL_PULLUP>;
};

你看,用宏代替魔法数字,代码可读性一下子就上来了。我在项目中经常把 GPIO 编号、时钟频率、中断号都定义成宏。比如:

#define UART0_BASE  0x10000000
#define UART0_IRQ   32

&uart0 {
    reg = <UART0_BASE 0x1000>;
    interrupts = <UART0_IRQ>;
};

为什么要这么做?你想想看,如果哪天 UART0 的基地址变了,你只需要改一个宏,而不是满世界找 0x10000000。我曾经维护过一个项目,里面散落着几十个硬编码的地址,改起来简直要命。从那以后,我坚持用宏定义所有关键数值。

总结一下我的习惯:

  • SoC 通用配置放 .dtsi
  • 板级差异放 .dts
  • 所有数值常量用宏定义
  • 重要节点加标签,方便引用
  • compatible 字符串严格按 厂商,设备 格式

好了,设备树的基础语法就这些。说白了就是节点套节点,属性描述设备,标签方便引用,包含文件避免重复,宏定义让代码更干净。下一节咱们聊聊地址映射和中断,那才是真正让人头疼的地方。