2、设备树语法入门:节点、属性、值的表示方法,dts/dtsi/dtb文件格式

好,咱们正式开始啃设备树的语法。说实话,我第一次看设备树文件时,觉得这东西长得像C语言和JSON的混血儿。但用久了你会发现,它其实挺朴素的——就是一套描述硬件信息的树形结构。

2.1 设备树长什么样?

设备树的源文件是 .dts 文件。我习惯叫它“硬件描述书”。你打开一个典型的 .dts 文件,会看到类似这样的结构:

/dts-v1/;

/ {
    model = "TI AM335x BeagleBone Black";
    compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am335x";

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;
        cpu@0 {
            compatible = "arm,cortex-a8";
            device_type = "cpu";
            reg = <0x0>;
        };
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x20000000>;
    };
};

嗯,看着有点眼熟吧?每个花括号 {} 就是一个节点,节点里可以嵌套子节点。说白了,这就是一棵树——根节点是 /,下面挂着 cpusmemory 这些大树枝,再往下是更细的树枝。

2.2 节点:设备树的骨架

节点是设备树最基本的组成单元。每个节点代表一个硬件设备或总线。节点名字的格式是 node-name@unit-address

举个例子:

uart0: serial@44e09000 {
    compatible = "ti,am3352-uart", "ti,omap3-uart";
    reg = <0x44e09000 0x2000>;
    interrupts = <72>;
};

这里 serial@44e09000 是节点名,@ 后面的 44e09000 是它的地址。前面的 uart0: 是标签(label),方便其他地方引用。我在项目中经常用标签来引用节点,比如 &uart0 就能直接指向这个串口节点。

我的小技巧: 节点名尽量用通用名,比如 seriali2cspi。别用 my_uart 这种名字,否则别人看不懂,三个月后的你自己也看不懂。

2.3 属性:描述节点的特征

属性就是键值对,用来描述节点的各种特征。常见的属性有:

  • compatible:兼容性字符串,告诉内核这个设备用什么驱动。格式是 "厂商,型号"。我踩过坑——字符串写反了,结果驱动死活匹配不上。
  • reg:地址和长度信息。格式是 <地址 长度>
  • interrupts:中断号。
  • status:设备状态,常用 "okay""disabled"

看个实际例子:

gpio0: gpio@44e07000 {
    compatible = "ti,am3352-gpio", "ti,omap4-gpio";
    reg = <0x44e07000 0x1000>;
    interrupts = <96>;
    gpio-controller;
    #gpio-cells = <2>;
    status = "okay";
};

注意那个 gpio-controller;——这是个空属性,没有值。它就像个开关,告诉内核“我是GPIO控制器”。

2.4 值的表示方法

设备树里的值有几种表示方式,我整理了个表格:

类型 写法 示例
32位整数 <值> <0x1000>
64位整数 用两个32位表示 <0x0 0x80000000>
字符串 "字符串" "ti,am3352-uart"
字节数组 [字节流] [00 01 02 03]
字符串列表 逗号分隔 "ti,am3352-uart", "ti,omap3-uart"

你想想看,为什么地址要用 <> 括起来?因为设备树编译器(dtc)看到 <> 就知道里面是整数,看到 [] 就知道是字节。我曾经在调试时把 <0x1000> 写成了 [0x1000],结果地址解析全乱了——嗯,这种低级错误犯一次就够了。

2.5 dts、dtsi、dtb 文件格式

这三个后缀名,我刚开始也搞混过。简单说:

  • .dts:设备树源文件。你写代码的地方。
  • .dtsi:设备树包含文件。相当于C语言的 .h 头文件。
  • .dtb:编译后的二进制文件。U-Boot和内核真正读取的。

编译命令很简单:

dtc -I dts -O dtb -o myboard.dtb myboard.dts

反过来也能从 .dtb 反编译成 .dts

dtc -I dtb -O dts -o myboard.dts myboard.dtb
重点来了: .dtsi 文件用 #include 引入。比如 #include "soc.dtsi"。这样你可以把SoC公共部分放在 .dtsi 里,板级差异放在 .dts 里。我在项目中就是这么做的——一个 .dtsi 给同一系列的所有板子用,省事多了。

2.6 避坑指南

我曾经踩过的坑:
  1. 分号漏了——设备树里每个属性后面都要加分号 ;。漏一个,编译就报错。我调试过半小时才发现少了个分号。
  2. 地址写错——reg 里的地址和长度,顺序不能乱。先地址后长度,这是铁律。
  3. compatible 字符串不匹配——驱动里写的 "ti,am3352-uart",设备树里写 "ti,am335x-uart",差一个字符都匹配不上。

我个人习惯是,写完 .dts 文件后,先用 dtc -I dts -O dtb 编译一下,看看有没有语法错误。别等到烧到板子上才发现问题——那会儿调试起来就麻烦了。

2.7 小结

设备树语法其实不复杂。记住三点:

  • 节点是树形结构,用 {} 表示层级
  • 属性是键值对,值有四种类型
  • .dts 是源码,.dtsi 是头文件,.dtb 是二进制

下一章咱们会深入讲 #address-cells#size-cells 这两个让人头疼的属性。说实话,我第一次看到这两个属性时也是一头雾水——但搞懂了它们,设备树的地址映射你就通了八成。