设备树基础语法(一):.dts/.dtsi 文件结构、节点与属性
好,咱们正式开始啃设备树这块硬骨头。说实话,我刚接触设备树那会儿,看着满屏的尖括号和花括号,心里直犯嘀咕——这玩意儿跟 C 语言长得挺像,但又完全不是一回事。后来做项目多了,才慢慢摸清它的脾气。
设备树说白了就是一套描述硬件信息的「说明书」。你想想看,Linux 内核要跑在不同的板子上,总不能每换一块板子就重新编译一次内核吧?设备树就是用来解决这个问题的——把硬件信息从内核代码里抽出来,单独描述。
一、设备树文件长什么样?
设备树源文件有两种后缀:.dts 和 .dtsi。我个人习惯把 .dts 看作「板级文件」,.dtsi 看作「芯片级头文件」。就像 C 语言里的 .c 和 .h 一样。
来看一个最简单的例子:
/dts-v1/;
/ {
model = "MyCustomBoard";
compatible = "vendor,myboard-v1", "vendor,myboard";
chosen {
bootargs = "console=ttyS0,115200";
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x10000000>;
};
cpus {
cpu@0 {
compatible = "arm,cortex-a7";
reg = <0>;
};
};
};
嗯,这里要注意几个关键点:
/dts-v1/;是版本声明,必须写在最前面。我见过有人漏掉这行,结果编译报错半天找不出原因。- 根节点
/是整个设备树的起点,所有设备都挂在这个根下面。 - 节点名后面可以跟
@地址,比如memory@80000000,这个地址用来区分同名的节点。
核心要点:设备树是一棵倒挂的树,根节点是 /,下面挂 CPU、内存、外设等节点。每个节点描述一个硬件设备或总线。
二、节点(node)的定义与规则
节点是设备树的基本组成单元。每个节点用花括号 { } 包裹,里面可以包含属性和子节点。
节点命名的规则其实挺宽松的,但我建议你遵循几条约定:
- 节点名尽量用硬件手册里的名称,比如
uart、i2c、spi - 如果同一类型有多个实例,用
@地址区分,比如uart@10000000和uart@20000000 - 不要用特殊字符,保持字母、数字、下划线、连字符
节点可以嵌套,形成层级关系。比如:
&i2c1 {
status = "okay";
clock-frequency = <100000>;
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
};
temperature@48 {
compatible = "lm75";
reg = <0x48>;
};
};
这里 i2c1 是父节点,eeprom@50 和 temperature@48 是子节点。子节点通过 reg 属性指定 I2C 地址。
我的经验:写节点名时,尽量跟芯片手册保持一致。我曾经接手一个项目,前任工程师把 UART 节点命名为 serial0,结果驱动里找 uart0 死活匹配不上,排查了整整一个下午。
三、属性(property)的定义与类型
属性就是节点的「参数」。设备树规范定义了多种属性类型,咱们一个一个来看。
| 属性类型 | 语法示例 | 说明 |
|---|---|---|
| 字符串 | compatible = "vendor,device"; |
用双引号包裹,用于匹配驱动 |
| 32位整数 | reg = <0x10000000 0x1000>; |
尖括号包裹,多个值用空格隔开 |
| 64位整数 | reg = <0x00000000 0x80000000 0x0 0x10000000>; |
用两个32位表示一个64位值 |
| 布尔值 | status = "okay"; |
实际上用字符串表示,常见值:okay、disabled |
| 字节数组 | local-mac-address = [00 11 22 33 44 55]; |
方括号包裹,十六进制字节 |
| 混合类型 | foo = <0x1234>, "string"; |
逗号分隔不同类型 |
最常用的属性是 compatible 和 reg。前者告诉内核「这个设备是什么型号」,后者告诉内核「设备的地址和大小」。
// 一个典型的 SPI 控制器节点
spi@10001000 {
compatible = "vendor,spi-controller";
reg = <0x10001000 0x1000>;
interrupts = <0 31 4>;
clocks = <&clkc 15>;
#address-cells = <1>;
#size-cells = <0>;
flash@0 {
compatible = "jedec,spi-nor";
reg = <0>;
spi-max-frequency = <50000000>;
};
};
避坑指南:我曾经在 reg 属性里写错地址范围,导致驱动申请内存时失败。记住,reg 的第一个值是起始地址,第二个值是长度。别搞反了!
四、设备树文件的结构组织
实际项目中,设备树文件通常按模块拆分。我一般这样组织:
- 芯片级 .dtsi:描述 SoC 内部资源,比如 CPU、中断控制器、时钟、DMA 等
- 板级 .dts:描述板级外设,比如 DDR 大小、网卡、USB 接口等
- 头文件 .h:定义宏和常量,方便复用
用 #include 来组合它们:
// myboard.dts
/dts-v1/;
#include "soc.dtsi"
#include "myboard-pinfunc.h"
/ {
model = "MyBoard v2";
compatible = "vendor,myboard-v2";
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>; // 512MB DDR
};
};
// 覆盖 soc.dtsi 中的节点
&uart1 {
status = "okay";
pinctrl-0 = <&uart1_pins>;
};
看到 &uart1 这种写法了吗?这叫「引用标签」,用来覆盖或扩展其他文件中定义的节点。这是设备树最强大的特性之一。
核心思想:芯片厂商提供 .dtsi,板级开发者提供 .dts。通过 #include 和标签引用,实现「芯片通用 + 板级定制」的分离。
五、知识体系结构图
下面这张图帮你理清设备树文件的结构关系:
这张图展示了设备树的层级结构。从根节点出发,每个节点都可以挂载子节点,形成一棵完整的硬件描述树。属性则附着在节点上,提供具体的配置信息。
六、避坑与实战建议
最后分享几个我踩过的坑:
- 分号别漏:每个属性后面都要加分号,漏一个编译就报错。我刚开始写的时候,经常漏掉最后一个属性后面的分号。
- 逗号别乱用:字符串列表用逗号分隔,但整数列表不用。比如
compatible = "a", "b";是对的,但reg = <0x1000, 0x2000>;是错的。 - 标签引用要小心:
&node只能用于覆盖已有节点,不能凭空创建新节点。如果你引用的标签不存在,编译会报错。 - 地址和大小要匹配:
#address-cells和#size-cells决定了reg中每个地址和大小占几个32位。不匹配的话,驱动读到的地址就是错的。
我的习惯:每次写完设备树,先用 dtc -I dts -O dtb test.dts 编译成 dtb,再用 dtc -I dtb -O dts test.dtb 反编译回来,看看有没有信息丢失。这招帮我发现过不少低级错误。
好了,设备树的基础语法就讲到这里。记住三个核心:文件结构、节点定义、属性类型。把这三点吃透了,后面的内容就好办了。