设备树语法入门:节点、属性、标签与包含机制
各位好,欢迎来到第二章。上一章我们聊了设备树是什么、为什么需要它。今天咱们直接上手——把设备树的语法啃下来。
说实话,我刚开始学设备树时,觉得这东西不就是个配置文件嘛,有啥难的?结果第一次自己写一个外设节点,编译报错报得我怀疑人生。后来才发现,语法细节没吃透,踩坑是必然的。
这一章我会把最核心的语法讲清楚:节点与属性怎么定义、标签怎么用、引用符号 & 是干嘛的、#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>,第一个是基地址,第二个是长度。如果你写反了,驱动读到的地址就是错的,系统直接挂掉。
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 文件里重复引用同一个节点,结果属性被覆盖了两次,调试了半天才发现。所以建议你:引用时尽量集中写,别分散到文件各处,容易乱。
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 知识体系总览
下面这张图把本章的核心知识点串起来了。你可以把它当作设备树语法的速查地图。
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 文件(内核源码里多的是),对照着看一遍,再自己动手改一改。踩几个坑,就彻底记住了。