第2章:设备树语法基础——节点、属性、标签与包含文件

设备树(Device Tree)说白了就是一块“硬件描述画板”。你用它告诉内核:我这块板子上有哪些外设、它们挂在哪条总线上、地址是多少、中断怎么接。我刚开始接触设备树时,总觉得它像一堆乱糟糟的括号和分号。后来摸清了套路,才发现它其实挺有章法的。

这一章,咱们就把设备树的四个基本构件——节点、属性、标签、包含文件——彻底讲透。你掌握了这些,后面写任何设备树文件都不会发怵。

核心一句话:设备树 = 一棵树,节点是树枝,属性是树叶,标签是路牌,包含文件是工具箱。

设备树四大基本构件关系图 设备树四大基本构件关系图 根节点 / 子节点 cpu 子节点 memory 子节点 serial 属性:compatible = "arm,cortex-a9" 属性:device_type = "memory" 属性:reg = <0x44e09000 0x2000> 标签:cpu0: cpu { ... } 标签:serial0: serial { ... } 包含文件:#include "imx6ull.dtsi" 根节点 子节点 属性 标签

2.1 节点(Node)——设备树的骨架

节点就是设备树里的“文件夹”。每个节点代表一个硬件设备或总线。节点从根(/)开始,一层套一层,形成树状结构。

我习惯把节点想象成公司组织架构:根节点是CEO,子节点是各部门经理,再往下是小组长。每个节点只管自己的那一摊事。

/ {
    // 根节点,代表整块板子
    model = "TI AM335x BeagleBone Black";
    compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";

    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>;  // 512MB DDR3
    };
};

看到没?cpus、memory@80000000 都是子节点。节点名字后面可以跟 @地址,用来区分同类型设备。比如两个串口:serial@44e09000 和 serial@44e0a000。

我的习惯:节点命名尽量跟芯片手册保持一致。比如UART节点就叫 uart 或 serial,别自己发明名字。内核匹配驱动时靠的是 compatible 属性,不是节点名,但好名字能让你自己少掉头发。

2.2 属性(Property)——节点的“身份证”

属性就是键值对,描述节点的各种特征。常见的属性类型有这些:

属性类型 写法示例 说明
字符串 compatible = "ti,am335x-bone-black"; 驱动匹配的关键,厂商+设备名
32位整数 reg = <0x44e09000 0x2000>; 地址和长度,尖括号包起来
64位整数 reg = <0x0 0x80000000 0x0 0x20000000>; 高32位在前,低32位在后
布尔值 status = "disabled"; 用字符串"okay"或"disabled"表示
字节数组 local-mac-address = [00 11 22 33 44 55]; 方括号,十六进制字节
字符串列表 compatible = "ti,am335x-bone-black", "ti,am33xx"; 逗号分隔,内核按顺序匹配

这里有个坑,我踩过不止一次:reg属性的地址和长度是配对的。#address-cells 和 #size-cells 决定了每个地址和长度占几个32位整数。比如 #address-cells = <1>; #size-cells = <1>; 时,reg = <0x44e09000 0x2000> 表示基址0x44e09000,长度0x2000。如果 #address-cells = <2>,那地址就得写两个数。

我曾经在配置一个FPGA桥接设备时,把 #address-cells 写成了2,但reg只给了一个地址值。结果内核解析出来的地址完全错位,设备死活不工作。查了两天才发现是cells数量不匹配。记住:address-cells和size-cells必须跟reg里的参数个数严格对应。

2.3 标签(Label)——给节点起个“外号”

标签就是节点的别名。你想想看,如果节点名很长,比如 spi@ff110000,每次引用都要写全名,多累啊。标签就是用来解决这个问题的。

// 定义标签
spi0: spi@ff110000 {
    compatible = "snps,dw-apb-ssi";
    reg = <0xff110000 0x1000>;
    interrupts = <0 36 4>;
    #address-cells = <1>;
    #size-cells = <0>;
};

// 在其他地方引用标签
&spi0 {
    status = "okay";
    cs-gpios = <&gpio1 17 0>;
};

标签用冒号跟在节点名前。引用时用 &标签名。这招在设备树覆盖(overlay)里特别有用——你可以在一个单独的文件里用 &spi0 { ... } 来追加配置,不用动原始dts文件。

我个人习惯给所有重要节点都加上标签。哪怕当前不需要引用,也先加上。因为产品迭代时,你永远不知道哪个节点后面会被覆盖。我见过太多项目后期为了加一个标签,把整个dts翻个底朝天。

2.4 包含文件(Include)——代码复用的利器

设备树支持C语言的 #include 预处理指令。这玩意儿太实用了。SoC厂商通常会提供一个 .dtsi 文件(i表示include),里面定义了芯片内部所有外设的骨架。板级开发者只需要 #include 这个文件,然后填充或覆盖自己板子特有的部分。

// imx6ull.dtsi(SoC厂商提供)
#include "skeleton.dtsi"

/ {
    compatible = "fsl,imx6ull";
    cpus { ... };
    soc {
        aips1: aips-bus@02000000 { ... };
        aips2: aips-bus@02100000 { ... };
        uart1: serial@02020000 {
            compatible = "fsl,imx6ul-uart", "fsl,imx21-uart";
            reg = <0x02020000 0x4000>;
            interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
            status = "disabled";
        };
    };
};

// myboard.dts(板级开发者)
#include "imx6ull.dtsi"

/ {
    model = "My i.MX6ULL Board";
    chosen {
        stdout-path = &uart1;
    };
};

&uart1 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_uart1>;
};

你看,myboard.dts 只写了板子特有的东西——型号、控制台、使能UART。所有SoC内部细节都在 imx6ull.dtsi 里。这就是设备树的精髓:分层设计,各司其职。

避坑指南:包含文件可以嵌套,但别搞成循环包含。我曾经见过一个项目,A.dtsi包含B.dtsi,B.dtsi又包含A.dtsi,编译直接报“too many include files”。另外,.dtsi 文件里不要写 /dts-v1/; 版本声明,这个只在最终的 .dts 文件里写一次。

2.5 四个构件如何协同工作

咱们用一个实际的数据采集设备例子,把节点、属性、标签、包含文件串起来。假设你有一块基于i.MX6ULL的采集板,带一个SPI接口的ADC芯片:

// my_adc_board.dts
/dts-v1/;

#include "imx6ull.dtsi"

/ {
    model = "Data Acquisition Board v1.0";
    compatible = "mycomp,adc-board", "fsl,imx6ull";

    adc@0 {
        compatible = "ti,ads1256";
        reg = <0>;
        spi-max-frequency = <19200000>;
        vref-supply = <&reg_vref>;
        #address-cells = <1>;
        #size-cells = <0>;
    };

    reg_vref: regulator-vref {
        compatible = "regulator-fixed";
        regulator-name = "vref";
        regulator-min-microvolt = <2500000>;
        regulator-max-microvolt = <2500000>;
    };
};

&ecspi1 {
    status = "okay";
    cs-gpios = <&gpio4 26 GPIO_ACTIVE_LOW>;
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_ecspi1>;
};

这个例子里:

  • 节点:/(根)、adc@0(ADC设备)、reg_vref(电压基准)
  • 属性:compatible、reg、spi-max-frequency 等
  • 标签:reg_vref 和 &ecspi1 引用
  • 包含文件:#include "imx6ull.dtsi"

嗯,到这里你应该明白了。设备树不是什么玄学,它就是一套结构化的硬件描述语言。节点搭骨架,属性填细节,标签方便引用,包含文件实现复用。把这四个东西玩转了,后面学中断映射、GPIO控制、时钟配置,都会轻松很多。

最后提醒一句:设备树编译工具(dtc)对语法很敏感。少一个分号、多一个括号、标签重复定义,都会直接报错。我建议你每次修改dts文件后,先用 dtc -I dts -O dtb -o test.dtb test.dts 编译一下,确认没有语法错误再烧到板子上。别问我为什么强调这个——我曾经在客户现场改完dts直接烧录,结果板子起不来,场面一度非常尴尬。