2. 设备树语法入门

好,咱们正式开始动手写设备树了。说实话,我第一次接触设备树语法时,觉得它长得有点像 JSON,又有点像 C 语言的结构体。但真正上手之后才发现——它其实就是一个描述硬件的树形结构。你只要搞懂四个核心概念:节点、属性、标签、包含,剩下的就是多写多练。

1. 节点(node)—— 树上的每一个分叉

设备树的核心就是「树」。树的每个分叉点,就是一个节点。节点可以嵌套,就像文件夹套文件夹一样。

举个例子,一个简单的设备树长这样:

/dts-v1/;

/ {
    model = "STM32MP157 Development Board";
    compatible = "st,stm32mp157";

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

    memory@c0000000 {
        device_type = "memory";
        reg = <0xc0000000 0x20000000>;
    };
};

看到没?根节点是 /,下面挂了 cpu@0 和 memory@c0000000 两个子节点。每个节点用花括号 { } 包裹,最后用分号结束。嗯,这里要注意——分号千万别漏,我刚开始写的时候经常丢分号,编译报错找半天。

节点命名规则:

  • 节点名一般写成 名称@地址 的形式,比如 uart@40011000
  • 名称表示设备类型,地址表示它的基地址或编号
  • 同一个父节点下,节点名必须唯一

2. 属性(property)—— 节点的身份证

节点光有个名字还不够,你得告诉内核这个设备是什么、怎么用。属性就是干这个的。

属性说白了就是键值对。常见的属性类型有几种:

属性类型 写法示例 说明
字符串 compatible = "st,stm32mp157"; 用于匹配驱动,格式一般是"厂商,设备名"
32位整数 reg = <0x40011000 0x400>; 尖括号包起来,多个值用空格隔开
布尔值 status = "okay"; 或 "disabled" 用字符串表示,没有 true/false 关键字
字节数组 local-mac-address = [00 11 22 33 44 55]; 方括号包起来,每个字节用十六进制

我个人习惯把 compatible 属性放在每个节点的第一行,这样一眼就能看出这个节点对应什么驱动。我在项目中遇到过好几次,因为 compatible 写错了,驱动死活加载不上——后来发现是少了个逗号,或者厂商名拼错了。

3. 标签(label)—— 给节点起个外号

你有没有想过,如果节点名很长,比如 i2c@40012000,每次引用它都要写全名,多麻烦?标签就是解决这个问题的。

看上面那个例子里的 cpu0: cpu@0,cpu0 就是标签。标签写在节点名前面,用冒号隔开。之后你想引用这个节点,直接用 &cpu0 就行了。

&cpu0 {
    clock-frequency = <650000000>;
};

这招在修改或扩展节点时特别有用。比如你有一个基础设备树,想给某个节点加点东西,不用把整个节点重写一遍,直接用标签引用,然后追加属性就行。

我的小技巧:标签命名尽量简短且有规律。比如 UART 节点统一叫 uart1、uart2,I2C 节点叫 i2c1、i2c2。这样在 &uart1 的时候,一看就懂。

4. 包含(include)—— 像 C 语言一样复用代码

设备树支持 #include 预处理指令。没错,就是 C 语言那个 #include。你可以把通用的定义放在一个 .dtsi 文件里,然后在具体板子的 .dts 文件里包含它。

举个例子:

// stm32mp157.dtsi —— 芯片级通用定义
/ {
    cpu0: cpu@0 {
        compatible = "arm,cortex-a7";
        reg = <0>;
    };

    uart4: serial@40010000 {
        compatible = "st,stm32-uart";
        reg = <0x40010000 0x400>;
        status = "disabled";
    };
};
// my-board.dts —— 具体板子
/dts-v1/;
#include "stm32mp157.dtsi"

/ {
    model = "My Custom Board";
};

&uart4 {
    status = "okay";
    clock-frequency = <64000000>;
};

你看,芯片级的定义放在 .dtsi 里,板子级的配置放在 .dts 里。这样同一个芯片的不同板子,只需要改 .dts 文件就行了。我曾经在一个项目里,用这种方式同时维护了 5 块不同的板子,每块板子只需要写几十行差异配置,省了不少事。

避坑指南:我曾经犯过一个错误——在 .dtsi 文件里把某个节点的 status 设成了 "okay",然后在 .dts 里又用 &节点 把它设成 "disabled"。结果编译时发现,两个地方都生效了,但后加载的覆盖了先加载的。所以建议:.dtsi 里统一设 "disabled",具体板子需要哪个就开哪个,这样逻辑更清晰。

5. 知识体系总览

说了这么多,咱们用一张图把整个语法体系串起来:

根节点 / 节点 (node) 属性 (property) 标签 (label) cpu@0 { ... } memory@c0000000 { ... } compatible = "..." reg = <0x...> status = "okay" cpu0: cpu@0 &cpu0 { ... } 包含 (include) #include "stm32mp157.dtsi"

这张图把四个核心概念的关系理清楚了。节点是骨架,属性是血肉,标签是快捷方式,包含是复用利器。你写设备树的时候,脑子里时刻想着这四样东西,基本就不会跑偏。

6. 实战小练习

光看不练假把式。我给你留个小练习:

  1. 创建一个根节点,里面包含一个 uart7 节点,基地址 0x40018000,长度 0x400
  2. 给 uart7 加上标签 uart7
  3. 在另一个文件里用 #include 包含它,然后把 status 设为 "okay"

试试看,写完了对照我前面给的例子检查一下。嗯,语法这东西,写三遍就熟了。