第2章:设备树语法入门

好,咱们正式开始啃设备树的语法。说实话,很多嵌入式工程师第一次看到.dts文件时,第一反应是「这什么鬼?长得像C语言又不是C语言」。别急,我当年也是这样。花半小时搞懂它的骨架,后面就顺了。

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";
            reg = <0>;
        };
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x10000000>;  /* 256 MB */
    };
};

看到没?最外层是根节点/,里面包了cpusmemory这些子节点。每个节点用花括号{}包起来,最后用分号;结束。嗯,这里要注意:分号不能丢,丢了编译器直接报错,我踩过这个坑。

2.2 节点(node)和属性(property)

节点就是硬件单元。比如一个I2C控制器、一个GPIO引脚、一个SPI Flash,都对应一个节点。节点里面放的是属性——描述这个硬件的信息。

属性分两种:

  • 标准属性:比如compatibleregstatus,内核规定好的
  • 自定义属性:厂商自己加的,比如max-frequencygpios

举个例子,一个I2C设备节点:

&i2c0 {
    status = "okay";
    clock-frequency = <400000>;

    eeprom@50 {
        compatible = "atmel,24c02";
        reg = <0x50>;
        pagesize = <8>;
    };
};

这里eeprom@50是子节点,@50表示它的I2C地址是0x50。compatible告诉内核用哪个驱动,reg就是地址。我在项目中遇到过有人把reg写成了addr,结果驱动死活匹配不上——因为内核只认reg

核心原则:节点描述「有什么硬件」,属性描述「硬件长什么样」。

2.3 标签(label)和引用

标签这东西,说白了就是给节点起个外号。方便你在别的地方引用它。比如:

i2c0: i2c@44e0b000 {
    #address-cells = <1>;
    #size-cells = <0>;
    compatible = "ti,omap4-i2c";
    reg = <0x44e0b000 0x1000>;
    interrupts = <70>;
};

这里的i2c0:就是标签。后面你想修改这个节点,直接用&i2c0引用它:

&i2c0 {
    status = "okay";
    clock-frequency = <400000>;
};

为什么要这么干?因为设备树文件经常被多个板子共用。基板定义好i2c0的寄存器地址和中断,具体板子再通过&i2c0打开它、配频率。这样代码复用性极高。

我的习惯:标签名尽量跟节点名保持一致,比如uart0对应uart@44e09000。不然过两个月你自己都忘了哪个标签是哪个。

2.4 include机制

设备树支持#include,跟C语言一样。但注意,它用的是C预处理器,所以你可以用#define定义宏。

实际项目中,一个完整的设备树文件通常长这样:

#include "am33xx.dtsi"
#include "am335x-bone-common.dtsi"
#include "am335x-boneblack-common.dtsi"

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

.dtsi文件是「设备树包含文件」,相当于头文件。SoC厂商会提供soc.dtsi,里面定义好所有内置外设的节点。板级厂商再#include它,然后修改或启用需要的节点。

我曾经接手一个项目,板子改了三次PCB,每次只改几个GPIO和I2C地址。我直接把公共部分放.dtsi,每个板子一个.dts,只写差异部分。嗯,维护起来轻松多了。

注意#include/include/是两回事。前者是C预处理器指令,支持宏定义;后者是dtc编译器自己的语法,功能弱很多。我建议统一用#include

2.5 避坑指南

最后分享几个我踩过的坑:

  • 分号漏了:每个属性结束都要分号,每个节点结束也要分号。漏一个,编译报错,而且报错信息经常指向下一行,很坑。
  • 标签重复:不同.dtsi里定义了同名标签,编译不会报错,但后面的会覆盖前面的。我曾经查了半天,才发现是标签冲突。
  • reg格式搞错reg = <0x80000000 0x10000000>,第一个是起始地址,第二个是长度。有人写成reg = <0x80000000>,少了长度,内核直接懵。
  • compatible拼写:驱动匹配靠字符串比较,拼错一个字母就匹配不上。我建议直接从驱动源码里复制,别手敲。

好了,设备树语法就这些。说白了就是「节点套节点,属性填里面,标签方便引用,include复用代码」。下一章咱们聊聊怎么编译和调试设备树,那才是真正动手的时候。