设备树语法详解:节点、属性与包含文件
设备树(Device Tree)说白了就是一块描述硬件的数据结构。我刚开始接触时也觉得它挺绕,后来发现它其实就是个树形结构——根节点下面挂子节点,子节点下面再挂子节点,每个节点上贴一堆标签(属性)。今天咱们就把这棵树的语法掰开揉碎了讲清楚。
核心思想:设备树不是代码,是数据。它告诉内核“我有哪些硬件,它们怎么连接,怎么访问”。
2.1 节点(Node)与属性(Property)
设备树的基本单位是节点。每个节点代表一个硬件设备或总线。节点可以嵌套,形成层级关系。
/dts-v1/;
/ {
#address-cells = <1>;
#size-cells = <1>;
cpu@0 {
compatible = "arm,cortex-a9";
reg = <0x0 0x100>;
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x40000000>; /* 1GB RAM */
};
serial@101f1000 {
compatible = "ns16550";
reg = <0x101f1000 0x1000>;
interrupts = <0x0 0x1 0x4>;
};
};
看到这个结构了吗?/ 是根节点,下面挂了三个子节点。每个节点用 @ 后面的地址来区分同一层级的不同实例。比如 cpu@0 和 cpu@1 就是两个不同的 CPU 核心。
节点命名规则:
node-name@unit-address格式node-name是通用名称,比如serial、i2c、spiunit-address是设备在总线上的地址,通常与reg属性的第一个地址一致- 如果节点没有地址(比如根节点),可以省略
@部分
我的习惯:命名时尽量用标准名称,别自己发明。比如串口就叫 serial,别叫 uart_port_0。内核匹配驱动时靠的是 compatible 属性,不是节点名。
2.2 标准属性详解
属性就是键值对。值可以是整数、字符串、字节数组,或者它们的组合。下面这几个属性是设备树的“标配”,几乎每个节点都会用到。
2.2.1 compatible —— 驱动匹配的关键
compatible 是设备树里最重要的属性。它告诉内核:“这个设备是什么型号,可以用哪个驱动”。
compatible = "vendor,device", "generic,device";
内核匹配驱动时,会从左到右依次尝试。第一个匹配上了就用第一个,否则试第二个。我遇到过一个问题:某款芯片的 I2C 控制器兼容性字符串写反了,结果内核加载了通用驱动,导致高速模式打不开。排查了一整天才发现是字符串顺序问题。
避坑指南:compatible 字符串的格式是 "厂商,型号",厂商名用小写,型号用标准名称。不要随便写,内核源码的 Documentation/devicetree/bindings/ 目录下有规范,写之前先查一下。
2.2.2 reg —— 地址与大小
reg 描述设备的地址空间。格式是 <地址 长度> 的序列。
reg = <0x101f1000 0x1000>; /* 基地址 0x101f1000,长度 4KB */
地址和长度的位数由父节点的 #address-cells 和 #size-cells 决定。比如:
#address-cells = <2>; /* 地址用两个 32 位整数表示,即 64 位 */
#size-cells = <1>; /* 长度用一个 32 位整数表示 */
那么 reg = <0x0 0x80000000 0x40000000> 就表示地址 0x80000000(高 32 位是 0),长度 0x40000000(1GB)。
| 属性 | 含义 | 示例 |
|---|---|---|
#address-cells |
地址字段占几个 32 位整数 | <1> 表示 32 位地址 |
#size-cells |
长度字段占几个 32 位整数 | <1> 表示 32 位长度 |
reg |
地址空间列表 | <0x1000 0x100> |
2.2.3 interrupts —— 中断连接
描述设备使用哪个中断。格式取决于中断控制器。
interrupts = <0x0 0x1 0x4>;
interrupt-parent = <&gic>;
这里 interrupt-parent 指定中断控制器(通常是 GIC),interrupts 里的三个值分别是:中断类型(SPI=0,PPI=1)、中断号、触发方式(高电平=4,上升沿=1 等)。
我踩过的坑:中断号是硬件相关的,不同芯片的中断号映射可能不同。我曾经把 SPI 中断号写成了 PPI 的,结果中断一直触发不了。后来用 cat /proc/interrupts 一看,中断根本没注册上。
2.2.4 其他常用属性
- status:设备状态。
"okay"表示启用,"disabled"表示禁用。调试时很有用。 - device_type:设备类型。早期设备树用得比较多,现在基本被
compatible取代了。 - label:给节点起个别名,方便在板级文件中引用。
- clock-frequency:时钟频率,常用于串口、I2C 等外设。
2.3 设备树包含文件(.dtsi)
一个芯片可能有几十种外设,如果每个板子都写一遍,那得累死。所以设备树引入了 .dtsi 文件,相当于 C 语言的头文件。
典型结构:
// imx6ull.dtsi —— SoC 级定义
/ {
soc {
aips1: aips-bus@2000000 {
uart1: serial@2020000 {
compatible = "fsl,imx6ul-uart", "fsl,imx-uart";
reg = <0x2020000 0x4000>;
interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
status = "disabled";
};
};
};
};
// myboard.dts —— 板级文件
#include "imx6ull.dtsi"
/ {
model = "MyBoard";
compatible = "mycompany,myboard", "fsl,imx6ull";
};
&uart1 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart1>;
};
看到 &uart1 了吗?这是引用语法。在 .dtsi 里定义节点时加上标签(比如 uart1:),然后在板级文件里用 &uart1 引用它,就可以修改或追加属性。
我的建议:把 SoC 通用的外设定义放在 .dtsi 里,默认状态设为 disabled。板级文件里只启用实际用到的外设,并配置引脚复用。这样复用性最好。
2.4 知识体系结构图
下面这张图展示了设备树语法的核心结构,从根节点到属性,再到包含文件的组织方式:
2.5 实战:从零写一个设备树片段
假设我们要描述一个板子,上面有一颗 i.MX6ULL 芯片,带一个串口和一个 GPIO 按键。我会这样写:
// soc.dtsi —— SoC 通用定义
/ {
#address-cells = <1>;
#size-cells = <1>;
soc {
#address-cells = <1>;
#size-cells = <1>;
ranges;
uart1: serial@2020000 {
compatible = "fsl,imx6ul-uart", "fsl,imx-uart";
reg = <0x2020000 0x4000>;
interrupts = <0 26 4>;
status = "disabled";
};
gpio1: gpio@2090000 {
compatible = "fsl,imx6ul-gpio", "fsl,imx-gpio";
reg = <0x2090000 0x4000>;
interrupts = <0 66 4>;
gpio-controller;
#gpio-cells = <2>;
status = "disabled";
};
};
};
// myboard.dts —— 板级文件
/dts-v1/;
#include "soc.dtsi"
/ {
model = "My i.MX6ULL Board";
compatible = "mycompany,imx6ull-board", "fsl,imx6ull";
chosen {
stdout-path = &uart1;
};
gpio-keys {
compatible = "gpio-keys";
user-button {
label = "User Button";
gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
linux,code = <KEY_ENTER>;
};
};
};
&uart1 {
status = "okay";
};
&gpio1 {
status = "okay";
};
关键点:
.dtsi里外设默认disabled,板级文件按需启用chosen节点设置内核启动参数,比如控制台串口gpio-keys是内核标准驱动,通过gpios属性绑定硬件引脚&uart1语法用于引用和修改已定义的节点