2. 设备树基础概念

各位好,欢迎来到第二章。上一章我们聊了设备树是什么、为什么需要它。今天咱们直接上手——把设备树的语法啃下来。

说实话,我刚开始学设备树时,觉得这东西不就是个配置文件嘛,有啥难的?结果第一次自己写一个外设节点,编译报错报得我怀疑人生。后来才发现,语法细节没吃透,踩坑是必然的。

这一章我会把最核心的语法讲清楚:节点与属性怎么定义、标签怎么用、引用符号 & 是干嘛的、#include 又是怎么工作的。你把这些搞明白,后面写设备树就跟写配置文件一样顺手。

2.1 节点(Node)——设备树的基本单元

设备树本质上是一棵树。树的每个节点就是一个设备或总线。节点用花括号 { } 包裹,里面可以放属性,也可以放子节点。

看个最简单的例子:

/dts-v1/;

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

最外层的 / 是根节点。每个 .dts 文件必须有且只有一个根节点。根节点下面可以挂各种子节点,比如 CPU、内存、外设。

我个人的习惯是:根节点只放顶层描述(model、compatible),具体设备都放到子节点里。这样结构清晰,后期维护也方便。

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

属性就是键值对。写法是 属性名 = 值;。值的类型有几种:

类型 写法示例 说明
字符串 compatible = "vendor,device"; 用双引号包裹
32位整数 reg = <0x10000000 0x1000>; 尖括号内,空格分隔
64位整数 reg = <0x00000000 0x10000000>; 两个32位拼一个64位
字节数组 local-mac-address = [00 11 22 33 44 55]; 方括号内,十六进制
字符串列表 compatible = "vendor1,dev1", "vendor2,dev2"; 逗号分隔多个字符串
混合 foo = <0x1234>, "string"; 可以混搭,但少见

这里有个坑,我当年踩过:reg 属性里的地址和长度,顺序不能乱。比如 reg = <0x10000000 0x1000>,第一个是基地址,第二个是长度。如果你写反了,驱动读到的地址就是错的,系统直接挂掉。

警告:reg 属性的地址和长度顺序由 #address-cells 和 #size-cells 决定。默认都是 1,但如果你改了 cells 数,reg 的格式也要跟着变。别想当然。

2.3 标签(Label)与引用(&)

标签是给节点起个别名。语法很简单:在节点名前加个 标签名:。

uart0: serial@10000000 {
    compatible = "vendor,uart";
    reg = <0x10000000 0x1000>;
};

这里 uart0 就是标签。有了标签,你就可以在其他地方用 &uart0 来引用这个节点。

引用最常见的用途是追加属性或修改节点。比如:

&uart0 {
    status = "okay";
    clock-frequency = <115200>;
};

这段代码的意思是:找到标签为 uart0 的节点,给它加上 status 和 clock-frequency 属性。如果节点里已经有同名属性,就覆盖掉。

我刚开始用引用时犯过一个低级错误:在同一个 .dts 文件里重复引用同一个节点,结果属性被覆盖了两次,调试了半天才发现。所以建议你:引用时尽量集中写,别分散到文件各处,容易乱。

小技巧:标签名最好用有意义的名称,比如 uart0、i2c1、mmc0。别用 a、b、c 这种,三个月后你自己都看不懂。

2.4 包含文件(#include)机制

设备树支持 C 语言的预处理。你可以用 #include 把其他文件的内容包含进来。这在实际项目中几乎是必须的,因为 SoC 厂商会提供公用的 .dtsi 文件。

看个实际例子:

#include "soc.dtsi"
#include "board-common.dtsi"

/ {
    model = "MyCustomBoard";
};

编译时,预处理器会把 soc.dtsi 和 board-common.dtsi 的内容直接插入到这个位置。这样你就可以把 SoC 通用的部分(CPU、中断控制器、时钟等)放到 .dtsi 里,板级差异部分放到 .dts 里。

我记得有一次,同事把 .dtsi 里定义的一个节点在 .dts 里又定义了一遍,结果编译报重复定义的错误。后来我告诉他:.dtsi 里用标签定义节点,.dts 里用 & 引用追加,这样就不会冲突。

2.5 节点命名规则

节点名格式是 名称@地址。名称一般用设备类型,地址是寄存器基地址。比如:

serial@10000000 {
    // 名称是 serial,地址是 0x10000000
};

如果节点没有地址(比如根节点下的虚拟设备),可以省略 @ 部分:

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

这里有个细节:同一级节点,名称可以重复,但地址不能重复。比如两个 serial@10000000 是不允许的,但 serial@10000000 和 serial@20000000 可以共存。

2.6 核心属性:compatible、reg、status

这三个属性你几乎每个节点都会用到,我单独拎出来说。

compatible:驱动匹配的关键。格式是 "厂商,设备"。Linux 内核通过这个字符串找到对应的驱动。比如:

compatible = "ti,omap3-uart", "ns16550";

内核会先尝试匹配 "ti,omap3-uart",如果没找到,再试 "ns16550"。所以越具体的字符串放前面。

reg:描述设备的地址空间。格式是 <地址1 长度1 地址2 长度2 ...>。地址和长度的位数由父节点的 #address-cells 和 #size-cells 决定。

status:设备状态。常用值:

  • "okay":设备启用
  • "disabled":设备禁用
  • "reserved":保留,驱动不碰

我一般习惯在 .dtsi 里把 status 设为 disabled,然后在 .dts 里按需改为 okay。这样板级文件只启用需要的设备,干净利落。

2.7 知识体系总览

下面这张图把本章的核心知识点串起来了。你可以把它当作设备树语法的速查地图。

设备树语法核心结构 根节点 / 子节点 serial@... 子节点 gpio@... 子节点 i2c@... 属性 (Property) compatible = "vendor,dev"; reg = <0x1000 0x100>; status = "okay"; 标签 (Label) uart0: serial@10000000 { ... }; 引用 (&) &uart0 { status = "okay"; }; #include "soc.dtsi" / #include "board.dtsi" 预处理器展开,复用 SoC 公共定义 💡 核心原则:.dtsi 定义节点 + 标签,.dts 用 & 引用追加 避免重复定义,保持板级文件简洁

2.8 实战:写一个完整的设备树片段

光说不练假把式。咱们写一个实际点的例子:一个 SoC 的 .dtsi 文件,加上板级的 .dts 文件。

soc.dtsi:

/dts-v1/;

/ {
    #address-cells = <1>;
    #size-cells = <1>;

    uart0: serial@10000000 {
        compatible = "vendor,uart";
        reg = <0x10000000 0x1000>;
        interrupts = <33>;
        status = "disabled";
    };

    gpio0: gpio@20000000 {
        compatible = "vendor,gpio";
        reg = <0x20000000 0x1000>;
        interrupts = <34>;
        gpio-controller;
        #gpio-cells = <2>;
        status = "disabled";
    };
};

myboard.dts:

/dts-v1/;

#include "soc.dtsi"

/ {
    model = "MyBoard v1.0";
    compatible = "vendor,myboard";

    chosen {
        bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2";
    };
};

&uart0 {
    status = "okay";
    clock-frequency = <115200>;
};

&gpio0 {
    status = "okay";
};

你看,.dtsi 里定义节点并打上标签,.dts 里用 #include 包含进来,再用 & 引用启用设备。这样 SoC 厂商只需要维护 .dtsi,板级工程师只需要写 .dts,各司其职。

重点回顾:
  • 节点是设备树的基本单元,用 { } 包裹
  • 属性描述节点特征,常见类型有字符串、整数、字节数组
  • 标签给节点起别名,方便引用
  • & 引用标签,用于追加或修改节点
  • #include 包含公共 .dtsi 文件,避免重复劳动
  • compatible、reg、status 是三个最常用的核心属性

嗯,这一章的内容就到这里。语法这东西,光看是记不住的。我建议你打开一个真实的 .dts 文件(内核源码里多的是),对照着看一遍,再自己动手改一改。踩几个坑,就彻底记住了。