第2章:设备树基础语法:DTS文件结构、节点与属性、标签与引用、include机制
好,咱们接着往下聊。上一章我带你看了设备树到底长什么样,这一章咱们就深入进去,把它的语法规则掰开揉碎了讲清楚。说实话,我刚接触设备树那会儿,看着一堆花括号和分号,心里直犯嘀咕——这不就是个配置文件吗?后来踩的坑多了,才明白它背后那套严谨的语法设计。
2.1 DTS文件的基本骨架
一个标准的DTS文件,说白了就是一棵树。根节点是唯一的,下面挂着一堆子节点。每个节点描述一个硬件设备或者总线。我习惯把DTS文件想象成公司组织架构图——CEO是根节点,各部门经理是子节点,每个员工就是具体的属性。
来看个最简单的例子:
/dts-v1/;
/ {
compatible = "acme,coyote";
#address-cells = <1>;
#size-cells = <1>;
cpu@0 {
compatible = "arm,cortex-a9";
reg = <0x0 0x100>;
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x40000000>;
};
};
注意第一行 /dts-v1/;,这个版本声明不能少。我见过有人抄代码漏掉这行,结果编译直接报错。嗯,这里要记住:每个DTS文件必须以这个开头。
根节点用 / 表示,里面可以嵌套任意深度的子节点。节点名后面可以跟 @地址,这个地址通常是设备在总线上的基地址。比如 cpu@0 表示CPU的起始地址是0。
2.2 节点(node)的命名规则
节点命名其实有讲究。格式是 node-name@unit-address,其中 @unit-address 是可选的。但如果你有多个同类型设备,建议加上地址区分。
举个例子:
uart0: serial@10000000 {
compatible = "ns16550";
reg = <0x10000000 0x1000>;
interrupts = <33>;
};
uart1: serial@10001000 {
compatible = "ns16550";
reg = <0x10001000 0x1000>;
interrupts = <34>;
};
这里 uart0 是标签(label),后面会讲。节点名是 serial,地址分别是 0x10000000 和 0x10001000。这样一看就知道是两个不同的串口。
小技巧:节点名尽量用标准名称,比如 serial、i2c、spi。Linux内核里有很多匹配规则是基于节点名的。我习惯参考内核源码里的 Documentation/devicetree/bindings/ 目录,那里有官方命名规范。
2.3 属性(property)的写法
属性就是节点的配置信息。常见的属性类型有几种:
| 属性类型 | 写法示例 | 说明 |
|---|---|---|
| 字符串 | compatible = "arm,cortex-a9"; |
用双引号括起来 |
| 32位整数 | reg = <0x10000000 0x1000>; |
尖括号括起来,空格分隔 |
| 64位整数 | reg = <0x00000000 0x80000000>; |
用两个32位表示 |
| 字符串列表 | compatible = "ti,am335x-uart", "ns16550"; |
逗号分隔多个字符串 |
| 二进制数据 | mac-address = [00 11 22 33 44 55]; |
方括号括起来,十六进制 |
| 混合类型 | mixed = <0x1234 0x5678>, "hello"; |
逗号分隔不同类型 |
这里我要特别强调 compatible 属性。它是设备树里最重要的属性之一,Linux内核就是靠它来匹配驱动。格式是 "厂商,设备型号"。比如 "ti,am335x-uart" 表示TI公司的AM335x系列UART。
注意:我曾经在项目里把 compatible 写成了 compatiable,少了个i。结果内核死活不认这个设备,查了两天才发现是拼写错误。这种低级错误最坑人,一定要仔细检查。
2.4 标签(label)与引用
标签是设备树里非常实用的功能。你可以给节点起个外号,然后在其他地方通过 &标签 来引用它。这有点像C语言里的指针。
看个例子:
/ {
i2c0: i2c@44e0b000 {
compatible = "ti,omap4-i2c";
#address-cells = <1>;
#size-cells = <0>;
reg = <0x44e0b000 0x1000>;
interrupts = <70>;
eeprom@50 {
compatible = "atmel,24c32";
reg = <0x50>;
};
};
};
&i2c0 {
status = "okay";
clock-frequency = <400000>;
};
这里 i2c0: 就是标签。在文件后面,我用 &i2c0 引用了这个节点,给它添加了 status 和 clock-frequency 属性。这种写法在设备树里非常常见,特别是当你需要覆盖或扩展某个节点时。
我个人习惯把标签写得简短有意义。比如 uart0、i2c1、mmc0,一看就知道是什么设备。别用 a、b、c 这种,时间长了你自己都记不住。
2.5 include机制
设备树支持 #include 预处理指令,这跟C语言一模一样。你可以把公共的部分抽出来放到头文件里,然后在多个DTS文件中引用。
举个例子:
// am335x.dtsi
/ {
ocp {
uart0: serial@44e09000 {
compatible = "ti,am335x-uart";
reg = <0x44e09000 0x2000>;
interrupts = <72>;
status = "disabled";
};
};
};
// myboard.dts
#include "am335x.dtsi"
&uart0 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&uart0_pins>;
};
你看,我把AM335x芯片的公共定义放在 .dtsi 文件里,板级文件只需要 #include 进来,然后覆盖需要的部分。这样既减少了重复代码,又方便维护。
核心思想:设备树的include机制实现了SoC级定义和板级定义的分离。SoC厂商提供 .dtsi 文件,板卡厂商只需要写自己的 .dts 文件,引用并修改少量配置即可。这大大降低了工作量。
我记得第一次做设备树移植时,面对几百行的DTS文件完全不知道从哪下手。后来发现大部分内容都在 .dtsi 里定义好了,我只需要关注板级特有的部分——GPIO复用、电源管理、外设使能这些。嗯,想通了这一点,事情就简单多了。
2.6 实战中的常见陷阱
最后分享几个我踩过的坑:
- 分号不能少:每个属性声明和节点结束都要加分号。漏一个分号,编译就报错,而且错误位置往往不准确。
- 地址和大小要匹配:
#address-cells和#size-cells决定了reg属性里每个地址和大小占几个32位。比如#address-cells = <2>表示地址用两个32位表示,也就是64位地址。 - 标签不能重复:同一个文件里标签必须唯一。我曾经在两个不同的
.dtsi文件里定义了同名的gpio0标签,结果编译时冲突了。 - 路径引用要小心:用
&标签引用时,确保标签在之前已经定义过。虽然编译器会检查,但错误信息有时候不太直观。
好了,这一章的内容就到这里。设备树语法其实不复杂,但细节很多。我建议你动手写几个简单的DTS文件,用 dtc 工具编译一下,看看报错信息是什么样的。实践出真知嘛。