3. 设备树基础语法:DTS文件结构、节点与属性、标准属性、设备树包含文件
好,咱们正式开始啃设备树的语法。说实话,我刚接触设备树那会儿,看着满屏的dts文件,心里直犯嘀咕——这玩意儿跟C语言长得有点像,但又完全不是一回事。后来做项目多了,才慢慢摸清它的脾气。今天我就把这些年积累的经验,掰开了揉碎了讲给你听。
3.1 DTS文件结构:一棵倒着长的树
设备树,名字里带个「树」字,它真就是一棵树。根节点叫/,下面挂CPU、内存、外设,一层套一层。我习惯把设备树想象成公司组织架构图——CEO在最顶上,下面分技术部、市场部,每个部门再往下分小组。设备树也是这个逻辑。
来看一个最简单的DTS文件骨架:
/dts-v1/;
/ {
model = "MyBoard";
compatible = "vendor,myboard", "vendor,baseboard";
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
compatible = "arm,cortex-a9";
reg = <0>;
};
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x40000000>;
};
serial@101f1000 {
compatible = "ns16550";
reg = <0x101f1000 0x1000>;
interrupts = <0 12 4>;
};
};
看到没?根节点/下面挂了三个子节点:cpus、memory、serial。每个节点又可以继续挂子节点,比如cpus下面又挂了cpu@0。这就是设备树的层级结构——说白了,就是一个嵌套的键值对集合。
核心要点:设备树的每个节点代表一个硬件设备或总线,节点名通常写成名称@地址的形式。地址部分用来区分同类型设备,比如两个串口可以叫serial@101f1000和serial@101f2000。
3.2 节点与属性:设备树的细胞和DNA
节点是设备树的「细胞」,属性是节点的「DNA」。每个节点用一对花括号{ }包裹,里面可以放属性,也可以放子节点。属性就是键值对,写法很灵活。
我总结了几种常见的属性写法:
- 字符串:
compatible = "vendor,device"; - 32位整数:
reg = <0x1000 0x100>; - 整数数组:
interrupts = <0 12 4>; - 布尔值:
status = "okay";或status = "disabled"; - 字节数组:
local-mac-address = [00 11 22 33 44 55];
嗯,这里要注意:属性值要么用双引号括起来(字符串),要么用尖括号括起来(整数),要么用方括号括起来(字节)。混着用?不行,编译器会报错。我曾经在项目里把reg的值写成了字符串格式,结果内核死活认不出内存范围,排查了半天才发现是这种低级错误。
3.3 标准属性:compatible、reg、interrupts
这三个属性是设备树的「三驾马车」,几乎每个设备节点都会用到。咱们一个一个说。
3.4.1 compatible:设备的身分证
compatible属性告诉内核:这个设备是什么型号,用哪个驱动来管它。格式是"厂商,设备型号"。比如:
compatible = "ti,am335x-uart", "ns16550";
内核匹配驱动时,会从左到右依次尝试。第一个匹配不上,就试第二个。我建议你写的时候,把最具体的型号放前面,通用的放后面。这样既保证精确匹配,又有兜底方案。
避坑指南:我曾经见过有人把compatible写反了——通用型号在前,具体型号在后。结果内核匹配到了通用驱动,导致某些特殊功能无法启用。记住:越具体越靠前。
3.4.2 reg:设备的地址空间
reg属性描述设备在总线上的地址范围和大小。格式是<地址 长度>,地址和长度都是32位整数。如果地址或长度超过32位,就需要配合#address-cells和#size-cells来调整。
// 32位地址,32位长度
reg = <0x101f1000 0x1000>;
// 64位地址,32位长度(需要父节点设置#address-cells=<2>)
reg = <0x0 0x80000000 0x1000>;
说白了,#address-cells决定地址占几个32位整数,#size-cells决定长度占几个。默认都是1。我刚开始学的时候,老搞混这两个值,后来记住一句话:父节点定义规则,子节点遵守规则。
3.4.3 interrupts:设备的中断信号
中断描述稍微复杂一点,因为它涉及中断控制器。一个典型的中断描述包含三部分:中断类型(SPI/P PI)、中断号、触发方式。
interrupts = <0 12 4>;
这里的三个数字分别代表:
- 0:SPI中断(共享外设中断)
- 12:中断号(硬件线号)
- 4:触发方式(高电平触发)
触发方式的常用值:1=上升沿触发,2=下降沿触发,4=高电平触发,8=低电平触发。嗯,这个数字是位掩码,可以组合使用。
注意:中断号的编号方式因芯片而异。有些芯片从0开始编号,有些从32开始。我在做TI AM335x项目时,就踩过这个坑——手册上写的中断号是32,但设备树里要写0。为什么?因为中断控制器内部做了偏移。所以,一定要看芯片手册里的「中断映射表」,别想当然。
3.4 设备树包含文件:.dtsi 的妙用
一个复杂的板子,外设可能有几十个。如果所有内容都写在一个.dts文件里,那文件会变得又臭又长。这时候.dtsi文件就派上用场了——它相当于C语言里的头文件,用来存放通用的、可复用的设备树片段。
比如,TI AM335x芯片的通用定义放在am335x.dtsi里:
// am335x.dtsi
#include <dt-bindings/interrupt-controller/irq.h>
/ {
compatible = "ti,am335x";
ocp {
#address-cells = <1>;
#size-cells = <1>;
uart0: serial@44e09000 {
compatible = "ti,am335x-uart", "ns16550";
reg = <0x44e09000 0x2000>;
interrupts = <72>;
status = "disabled";
};
i2c0: i2c@44e0b000 {
compatible = "ti,am335x-i2c";
reg = <0x44e0b000 0x1000>;
interrupts = <70>;
#address-cells = <1>;
#size-cells = <0>;
status = "disabled";
};
};
};
然后具体的板子文件myboard.dts只需要包含它,再覆盖或添加板级特有的内容:
// myboard.dts
/dts-v1/;
#include "am335x.dtsi"
/ {
model = "MyBoard";
compatible = "mycompany,myboard", "ti,am335x";
&uart0 {
status = "okay";
clock-frequency = <48000000>;
};
&i2c0 {
status = "okay";
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
};
};
};
看到&uart0这种写法了吗?这叫「引用标签」。在.dtsi里用uart0:给节点打上标签,在.dts里用&uart0引用它,然后添加或覆盖属性。这样既复用了芯片的通用定义,又保留了板级的灵活性。
我的习惯:我会把芯片相关的所有外设都定义在.dtsi里,并且默认把status设为disabled。然后在板级.dts里,只把用到的外设打开。这样既清晰又安全——万一哪个外设没用到,也不会意外占用资源。
3.5 知识体系总览
为了让你对本章内容有个整体把握,我画了一张图:
这张图把本章的知识点串起来了。你想想看,设备树其实就三件事:节点怎么组织、属性怎么写、公共部分怎么复用。掌握了这三块,大部分DTS文件你都能看懂,也能自己动手写了。
最后说一句:设备树语法不难,难的是把硬件手册里的信息准确翻译成设备树。我刚开始做的时候,经常对着几百页的芯片手册发呆。后来慢慢摸索出一个方法——先找到芯片厂商提供的.dtsi文件,照着它的风格写,遇到不确定的就去查手册。这样效率高很多。
好,这一章就到这里。下一章咱们聊聊设备树的编译和调试,到时候我会分享一些我踩过的坑和调试技巧。