设备树语法入门:节点、属性、标签、别名、包含文件的基本写法
大家好,我是你们的老朋友。今天咱们来聊聊设备树语法的基础。说实话,我刚接触设备树那会儿,看着那一堆花括号和分号,心里直犯嘀咕——这玩意儿跟C语言长得也太像了吧?但用久了你会发现,它其实比C语言简单多了。
设备树说白了就是一套描述硬件信息的“说明书”。它告诉内核:你有哪些外设、它们挂在哪条总线上、中断怎么接、GPIO怎么用。没有它,内核就像瞎子摸象,啥也干不了。
1. 节点——设备树的基本单元
节点就是设备树里的“文件夹”。每个节点代表一个硬件设备或总线。根节点用斜杠 / 表示,所有设备都挂在它下面。
/ {
model = "TI AM335x BeagleBone Black";
compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";
cpu@0 {
compatible = "arm,cortex-a8";
reg = <0>;
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>; /* 512 MB */
};
};
看到没?每个节点都有名字,比如 cpu@0。@后面的数字是地址,用来区分同类型设备。我刚开始写的时候老忘记加@,结果内核死活认不出第二个CPU——嗯,这个坑我踩过。
节点命名规则:
- 节点名格式:
名称@地址 - 名称不能有特殊字符,一般用字母、数字、短横线
- 地址部分通常用十六进制,比如
uart@44e09000 - 根节点没有名字,直接用
/
2. 属性——描述节点的“身份证”
属性就是节点的特征。它告诉内核这个设备是什么、怎么用。常见的属性类型有字符串、整数数组、二进制数据等。
| 属性类型 | 写法示例 | 说明 |
|---|---|---|
| 字符串 | compatible = "ti,am335x-uart"; |
用于匹配驱动 |
| 32位整数 | reg = <0x44e09000 0x2000>; |
地址和大小 |
| 64位整数 | reg = <0x00000000 0x80000000 0x00000000 0x20000000>; |
高32位在前 |
| 布尔值 | status = "disabled"; |
存在即true |
| 数组 | interrupts = <0 56 4>; |
中断描述 |
我个人习惯把 compatible 属性放在每个节点的第一行。为什么?因为内核匹配驱动时第一个就看它。你想想看,如果驱动和设备树里的compatible对不上,那设备根本没法工作。
小技巧:compatible 的命名格式一般是 "厂商,设备型号"。比如 "ti,am335x-uart" 表示TI公司的AM335x系列UART。这样能避免不同厂商的同名设备冲突。
3. 标签——给节点起个“外号”
标签就是节点的别名。用冒号加在节点名前。有了标签,其他地方引用这个节点就方便多了。
&uart0: serial@44e09000 {
compatible = "ti,am335x-uart";
reg = <0x44e09000 0x2000>;
interrupts = <72>;
};
这里的 &uart0 就是标签。以后想引用这个串口节点,直接用 &uart0 就行。我在项目中经常用标签来覆盖节点属性,比如在板级dts里修改某个外设的status:
&uart0 {
status = "okay";
};
这样就不用把整个节点重新写一遍了。省事吧?
4. 别名——全局的“快捷方式”
别名和标签有点像,但它是放在根节点的 aliases 子节点里的。别名的作用是给一些常用设备起个简短的名字,方便bootloader或内核代码引用。
/ {
aliases {
serial0 = &uart0;
serial1 = &uart1;
mmc0 = &mmc1;
mmc1 = &mmc2;
};
};
我记得有一次调试SD卡启动,bootloader就是通过 mmc0 这个别名找到SD卡控制器的。如果别名写错了,系统根本起不来。所以别名这东西,看着简单,但千万别写错。
注意:别名和标签的区别:
- 标签(label)是节点的一部分,写在节点定义里
- 别名(alias)是独立的节点,放在
/aliases下 - 标签用于设备树内部引用,别名更多用于bootloader和内核代码
5. 包含文件——代码复用的利器
设备树支持 #include 指令,就像C语言一样。这样可以把通用的硬件定义放在一个文件里,不同板子只需要包含它,再修改差异部分就行。
// am335x-boneblack.dts
#include "am33xx.dtsi"
#include "am335x-bone-common.dtsi"
&uart0 {
status = "okay";
pinctrl-0 = &uart0_pins;
};
&i2c0 {
status = "okay";
clock-frequency = <400000>;
};
你看,板级dts文件通常只有几十行。大部分硬件定义都在 .dtsi 文件里。这样维护起来特别方便——SoC升级了,只需要改 .dtsi 文件,所有板子自动受益。
我曾经接手过一个项目,前任工程师把所有硬件定义都写在一个dts文件里,足足3000多行。每次改个GPIO都要翻半天。后来我把它拆成多个 .dtsi 文件,清爽多了。所以我的建议是:能用 #include 就别硬编码。
包含文件的最佳实践:
.dtsi文件放SoC通用定义,比如CPU、中断控制器、时钟.dts文件放板级特有定义,比如外设使能、引脚复用- 头文件(
.h)放宏定义,比如GPIO号、中断号 - 用
#include而不是/include/(后者是旧语法)
6. 实战:一个完整的设备树片段
说了这么多,咱们来看个完整的例子。这是一个I2C控制器节点的定义:
/* i2c0: 位于 0x44e0b000,中断号 70 */
&i2c0: i2c@44e0b000 {
compatible = "ti,omap4-i2c";
reg = <0x44e0b000 0x1000>;
interrupts = <70>;
status = "disabled";
#address-cells = <1>;
#size-cells = <0>;
/* 子节点:挂载在I2C总线上的设备 */
eeprom@50 {
compatible = "atmel,24c32";
reg = <0x50>;
pagesize = <32>;
};
rtc@68 {
compatible = "dallas,ds1307";
reg = <0x68>;
};
};
这里有几个关键点:
#address-cells和#size-cells告诉内核子节点的地址和大小用几个32位整数表示- 子节点的
reg属性就是I2C从设备地址 - 每个子节点也有自己的
compatible,用于匹配对应的驱动
说实话,我刚学设备树时最困惑的就是 #address-cells 和 #size-cells。后来才明白,这俩属性决定了 reg 的解析方式。比如 #address-cells = <1> 表示地址用1个32位数,#size-cells = <0> 表示没有大小字段。你想想看,I2C设备只需要一个地址,不需要大小,所以size-cells为0。
避坑指南:我曾经在写SPI设备节点时,把 #address-cells 设成了2,结果reg怎么都对不上。后来查了半天才发现,SPI从设备只需要片选号,一个32位就够了。所以记住:地址和大小字段的数量一定要和总线协议匹配。
好了,设备树的基本语法就这些。节点、属性、标签、别名、包含文件——这五个概念搞懂了,你就能看懂大部分设备树文件了。下一节咱们会深入中断和GPIO的配置,那才是真正考验功力的时候。
可可精品资料库