第1章:设备树基础语法——节点、属性与.dts格式

大家好,我是你们的讲师。今天咱们聊聊设备树的基础语法。说实话,我刚接触设备树那会儿,也被它那套“树形结构”搞得有点懵。但后来我发现,这东西说白了就是个描述硬件的“配置文件”。你想想看,UEFI要启动操作系统,总得知道板子上接了哪些外设吧?设备树就是干这个的。

1.1 节点(Node)——设备树的“骨架”

设备树的结构,就像一棵倒着长的树。根节点是“/”,下面挂着一堆子节点。每个节点代表一个硬件设备或总线。我习惯把节点想象成“硬件设备的身份证”。

来看个最简单的例子:

/ {
    cpu {
        compatible = "arm,cortex-a72";
        reg = <0x0 0x100>;
    };

    memory {
        device_type = "memory";
        reg = <0x80000000 0x40000000>;
    };
};

这里“/”是根节点,“cpu”和“memory”是它的子节点。每个节点用花括号包裹,最后用分号结束。嗯,这里要注意:节点名不能重复,否则解析器会报错。我在项目中遇到过有人把两个I2C控制器都叫“i2c”,结果系统只认了第一个。

1.2 属性(Property)——节点的“血肉”

节点光有名字不行,还得有属性来描述它。属性就是键值对,格式是:

属性名 = 值;

值的类型有几种:

  • 字符串:用双引号包着,比如 compatible = "ti,am335x-uart";
  • 32位整数:用尖括号包着,比如 reg = <0x48020000 0x1000>;
  • 64位整数:用两个尖括号,比如 reg = <0x0 0x80000000 0x0 0x40000000>;
  • 字节数组:用方括号,比如 local-mac-address = [00 11 22 33 44 55];
  • 布尔值:只有属性名,没有值,比如 status = "disabled"; 或者直接写 status;(表示true)

我个人习惯把属性分成三类:标准属性、自定义属性和可选属性。标准属性是内核或UEFI固件必须识别的,自定义属性是驱动开发者自己加的,可选属性嘛,有最好,没有也能凑合。

1.3 标准属性详解

这部分是重点,我建议你记牢。标准属性就像硬件的“通用语言”,不管哪个平台都得懂。

1.3.1 compatible——设备匹配的“钥匙”

compatible 属性是设备树里最重要的属性之一。它告诉操作系统:“我是谁,我该用哪个驱动”。格式是 "厂商,设备型号"。比如:

compatible = "ti,am335x-uart", "ns16550";

这里有两个字符串,第一个是精确匹配,第二个是通用匹配。操作系统会按顺序尝试,直到找到能驱动的。我在项目中遇到过一个问题:有人把顺序写反了,结果系统用了通用驱动,没用到厂商优化过的驱动,性能差了一大截。所以,顺序很重要!

1.3.2 reg——地址与大小

reg 属性描述设备的地址空间。格式是 <地址 大小>,可以有多组。比如:

reg = <0x48020000 0x1000>;

这表示设备从0x48020000开始,占用0x1000字节。如果设备有多个地址段,可以写多组:

reg = <0x48020000 0x1000>, <0x48021000 0x100>;

嗯,这里要注意:地址和大小都是32位整数。如果地址超过4GB,就得用ranges属性配合#address-cells和#size-cells来处理。我曾经在调试一个64位平台时,忘了改#address-cells,结果地址被截断了,折腾了两天才发现。

1.3.3 interrupts——中断配置

中断属性描述设备怎么触发中断。格式取决于中断控制器。常见写法:

interrupts = <0 31 4>;
interrupt-parent = <&gic>;

这里interrupt-parent指向中断控制器节点,interrupts里的三个数字分别是:中断类型(0=SPI,1=PPI)、中断号、触发方式(1=上升沿,2=下降沿,4=高电平,8=低电平)。

我建议你写中断属性时,一定要查清楚中断控制器的spec。不同厂商的中断控制器,格式可能不一样。比如有些用interrupt-cells = <2>,有些用<3>。搞错了,中断就收不到。

1.3.4 其他标准属性

属性名 作用 示例
status 设备状态,"okay"表示启用,"disabled"表示禁用 status = "okay";
device_type 设备类型,通常用于内存节点 device_type = "memory";
model 设备型号,用于区分同一厂商的不同版本 model = "TI AM335x EVM";
phandle 节点句柄,用于其他节点引用 phandle = <1>;
ranges 地址转换,用于桥接不同地址空间 ranges = <0x0 0x80000000 0x10000>;

1.4 设备树源文件(.dts)格式

.dts文件是设备树的“源代码”。它用C语言风格的预处理器,支持#include、#define等宏。我习惯把公共部分放在.dtsi文件里,然后通过#include包含进来。比如:

// am335x.dtsi
#include <dt-bindings/interrupt-controller/irq.h>

/ {
    model = "TI AM335x";
    compatible = "ti,am335x";

    ocp {
        #address-cells = <1>;
        #size-cells = <1>;
        ranges;

        uart0: serial@48020000 {
            compatible = "ti,am335x-uart", "ns16550";
            reg = <0x48020000 0x1000>;
            interrupts = <72>;
            status = "disabled";
        };
    };
};

然后在具体板子的.dts里:

// am335x-evm.dts
#include "am335x.dtsi"

/ {
    model = "TI AM335x EVM";
    compatible = "ti,am335x-evm", "ti,am335x";

    &uart0 {
        status = "okay";
        clock-frequency = <48000000>;
    };
};

看到&uart0了吗?这是设备树的“引用语法”。它允许你在不破坏原有结构的情况下,修改某个节点的属性。我个人觉得这个设计非常巧妙,就像面向对象里的“继承”一样。

1.5 设备树编译与反编译

.dts文件不能直接给UEFI或内核用,得编译成.dtb(设备树二进制)文件。工具是dtc(Device Tree Compiler)。常用命令:

# 编译
dtc -I dts -O dtb -o am335x-evm.dtb am335x-evm.dts

# 反编译
dtc -I dtb -O dts -o am335x-evm.dts am335x-evm.dtb

我曾经在调试一个UEFI启动问题,发现设备树没生效。反编译一看,原来编译时忘了加-@选项,导致overlay功能没打开。嗯,细节决定成败啊。

1.6 知识体系结构图

下面我用一张SVG图来总结本章的知识结构:

设备树基础语法 节点 (Node) 属性 (Property) 根节点 "/" 子节点 标准属性 自定义属性 compatible reg interrupts status / model / ... .dts 源文件格式

核心要点:设备树就是一棵描述硬件的树。节点是树枝,属性是树叶。标准属性是“通用语言”,让不同操作系统都能读懂硬件。

我的小技巧:写.dts文件时,先用dtc -I dts -O dtb编译一下,看看有没有语法错误。别等到烧到板子上才发现问题,那太浪费时间了。

注意:设备树里的地址、中断号等数字,都是十进制或十六进制。我建议统一用十六进制,避免混淆。另外,reg属性的地址和大小顺序不能搞反,否则设备会访问到错误的内存区域。

好了,这一章的内容就到这里。设备树的基础语法其实不难,关键是多写、多编译、多调试。下一章我们会深入设备树的“地址映射”和“中断映射”,那才是真正考验功力的时候。

可可精品资料库