3. 设备树语法:节点、属性、标签、引用的完整解析

设备树(Device Tree)这东西,说白了就是一块描述硬件信息的“数据板子”。Linux内核启动时,Bootloader把这块板子传给内核,内核就知道自己跑在什么硬件上了。我刚开始接触设备树时,觉得它不就是个配置文件嘛,后来踩了几个坑才发现,它的语法细节远比想象中讲究。

今天咱们就把设备树的四个核心概念——节点、属性、标签、引用——彻底捋一遍。嗯,保证你学完就能自己写一个简单的设备树。

3.1 节点:硬件设备的“身份证”

设备树里的每个硬件设备,都对应一个节点。节点用花括号 {} 包裹,里面可以嵌套子节点。来看个最基础的例子:

/dts-v1/;

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

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

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

根节点 / 是整棵树的起点。每个节点都有个名字,比如 cpu@0@ 后面跟的是地址或单元号。我个人习惯把节点名写得尽量短,但一定要能一眼看出它代表什么硬件。

关键点:节点名中的 @ 地址部分不是必须的,但如果有多个同类型设备(比如两个UART),建议加上地址来区分。

3.2 属性:描述硬件的“标签”

属性就是键值对,写在节点内部。常见的属性类型有:

  • 字符串compatible = "vendor,device";
  • 整数reg = <0x1000 0x100>;
  • 布尔值status = "okay";status = "disabled";
  • 字节数组local-mac-address = [00 11 22 33 44 55];

我在项目中遇到过最坑的事,就是 compatible 属性写错了。驱动匹配全靠它,一旦拼写错误,内核根本认不出你的设备。你想想看,调试了半天,结果发现是少了个逗号,那感觉……

我的建议:compatible 时,遵循“厂商,设备型号”的格式,比如 "ti,am335x-uart"。这样既规范,又不容易冲突。

3.3 标签:给节点起个“外号”

标签(Label)就是给节点起个别名,方便在其他地方引用。语法很简单,在节点名前加个冒号:

uart0: serial@44e09000 {
    compatible = "ti,am3352-uart";
    reg = <0x44e09000 0x2000>;
    interrupts = <72>;
};

这里的 uart0 就是标签。有了它,你可以在别处用 &uart0 来引用这个节点。为什么要这么做?因为设备树里经常需要覆盖或扩展某个节点,直接写全路径太啰嗦了。

我曾经在一个项目里,设备树有上百个节点,没有标签的话,每次修改都得翻半天路径。后来我强制团队所有新节点都加标签,效率提升了一大截。

3.4 引用:把节点“串”起来

引用就是通过标签或路径,把多个节点关联起来。最常见的用法是 &标签

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

这段代码的意思是:找到标签为 uart0 的节点,给它添加或覆盖属性。注意,引用不会创建新节点,它只是修改已有节点。

还有一种引用是 <&节点路径>,但说实话,我很少用。标签引用更直观,也更不容易出错。

注意:引用时,如果标签不存在,编译会报错。我曾经因为复制粘贴忘了改标签名,排查了整整一下午。所以,写完后记得用 dtc -I dts -O dtb 编译检查一下。

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

咱们把上面学的东西串起来,写一个完整的例子:

/dts-v1/;

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

    chosen {
        stdout-path = &uart0;
    };

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

        uart0: serial@44e09000 {
            compatible = "ti,am3352-uart";
            reg = <0x44e09000 0x2000>;
            interrupts = <72>;
            status = "disabled";
        };

        gpio0: gpio@44e07000 {
            compatible = "ti,am3352-gpio";
            reg = <0x44e07000 0x1000>;
            interrupts = <96>;
            gpio-controller;
            #gpio-cells = <2>;
        };
    };
};

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

这个例子包含了根节点、子节点、标签、引用,以及常见的属性。你仔细看看 chosen 节点里的 stdout-path,它引用了 uart0,告诉内核把控制台输出到这个串口。

3.6 设备树语法结构图

下面这张图,帮你把设备树的语法结构理清楚:

根节点 / 子节点 cpu@0 子节点 memory@80000000 子节点 uart0: serial@44e09000 标签 uart0 属性 compatible 属性 reg 属性 interrupts 属性 status 引用 &uart0 根节点 子节点 标签 属性 引用

从这张图可以看得很清楚:根节点下面挂子节点,子节点可以带标签,标签用来做引用,属性则描述节点的具体信息。整个结构就像一棵倒挂的树,根在上,枝叶在下。

3.7 避坑指南

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

  • 标签不能重复:整个设备树里,标签名必须唯一。我曾经在两个不同文件里定义了同名标签,编译时没报错,但运行时就乱套了。
  • 引用不会覆盖整个节点&uart0 { ... }; 只会添加或覆盖花括号里的属性,不会删除节点原有的其他属性。这一点很多人搞混。
  • 路径引用要小心:如果用 &/soc/uart0 这种路径引用,一旦节点路径变了,引用就失效了。标签引用更安全。

我的习惯:写设备树时,先画个草图,把节点、标签、引用关系理清楚。然后按“根节点 → 子节点 → 标签 → 引用”的顺序写,这样不容易乱。

好了,设备树的语法就讲到这里。你只要把节点、属性、标签、引用这四个概念吃透,写设备树就跟搭积木一样简单。下次咱们聊聊设备树的编译和调试,那才是真正见真章的地方。


专注资料整理