第二讲:设备树语法精讲——节点、属性与标准属性

各位同学,今天我们来啃设备树语法这块硬骨头。说实话,我刚接触设备树那会儿,看着满屏的.dts文件,心里直犯嘀咕:这不就是个配置文件吗?怎么搞得这么复杂?后来踩过几次坑才明白——设备树就是嵌入式世界的"宪法",它规定了硬件长什么样、驱动该怎么找它。

1. 节点与属性:设备树的基本单元

设备树的结构,说白了就是一棵树。根节点是/,下面挂着一堆子节点。每个节点代表一个硬件设备或总线,属性就是描述这个设备的参数。

/ {
    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 = <0x0>;
        };
    };
    
    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x20000000>;  /* 512 MB */
    };
};

你看,每个节点用节点名@地址来标识。地址部分不是必须的,但如果有reg属性,我建议你加上——这样调试时一眼就能看出设备映射到哪个地址空间。

核心要点:

  • 节点名:尽量用通用名称,比如serial@... 而不是my_uart@...
  • 属性值:字符串用双引号,32位整数用尖括号,字节数组用方括号
  • 标签(label):可以在节点前加&标签名,方便在其他地方引用

2. 标准属性:compatible、reg、interrupts

这三个属性是设备树的"三驾马车"。我在项目中见过有人把compatible写错,结果驱动死活加载不上——嗯,这种低级错误我也犯过。

2.1 compatible:驱动匹配的钥匙

compatible属性决定了哪个驱动来接管这个设备。格式是"厂商,型号",可以有多个值,内核会按顺序匹配。

i2c@44e0b000 {
    compatible = "ti,omap4-i2c", "ti,omap3-i2c";
    reg = <0x44e0b000 0x1000>;
    interrupts = <70>;
};

我个人习惯把最具体的型号写在最前面,通用的放后面。这样如果新硬件有特殊处理,驱动能优先匹配到。

2.2 reg:地址空间的"身份证"

reg描述设备占用的地址范围。它由#address-cells和#size-cells决定格式。比如#address-cells = <1>表示地址用1个32位整数,#size-cells = <1>表示长度也用1个32位整数。

避坑指南:我曾经在移植一个网卡驱动时,发现reg里的地址长度写错了。父节点定义的是#size-cells = <1>,但我写成了<0x10000000 0x10000 0x2000>——多了一个值。内核解析时直接崩溃。记住:reg的格式必须和父节点严格一致。

2.3 interrupts:中断的"接线图"

中断属性描述设备的中断号、触发类型等信息。它依赖interrupt-parent指定中断控制器。

gpio_keys {
    compatible = "gpio-keys";
    #address-cells = <1>;
    #size-cells = <0>;
    
    button@0 {
        label = "user-button";
        linux,code = <KEY_ENTER>;
        gpios = <&gpio1 15 GPIO_ACTIVE_LOW>;
        interrupts = <15 IRQ_TYPE_EDGE_FALLING>;
    };
};

你想想看,如果中断号写错了,驱动注册中断时就会失败。我调试过一个触摸屏驱动,中断一直触发不了,最后发现是interrupts里的IRQ_TYPE写成了IRQ_TYPE_EDGE_RISING,而硬件实际是低电平触发。

3. 特殊节点:chosen 与 aliases

这两个节点是设备树的"基础设施",虽然不直接对应硬件,但内核启动时离不开它们。

3.1 chosen:内核启动参数

chosen节点不描述硬件,而是传递启动参数。比如bootargs、stdout-path等。

chosen {
    bootargs = "console=ttyO0,115200 root=/dev/mmcblk0p2 rw";
    stdout-path = &uart0;
};

我记得有一次调试,内核启动后串口没输出,查了半天发现是stdout-path指向了错误的UART节点。说白了,这就是个"默认控制台"的配置。

3.2 aliases:节点的"快捷方式"

aliases给节点起别名,方便在代码中引用。比如serial0 = &uart0;,这样驱动里可以直接用of_alias_get_id()获取序号。

aliases {
    serial0 = &uart0;
    serial1 = &uart1;
    mmc0 = &mmc1;
    mmc1 = &mmc2;
};

注意:别名不要乱起。我见过有人把mmc0和mmc1搞反,结果系统从eMMC启动时,内核却去SD卡找根文件系统——直接启动失败。别名最好和硬件设计文档保持一致。

4. 宏定义与include:让设备树"活"起来

设备树支持C语言的预处理,这意味着我们可以用#include、#define来组织代码。这招在大型项目中特别有用。

// am335x-boneblack.dts
#include "am33xx.dtsi"
#include "am335x-bone-common.dtsi"
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>

/ {
    model = "TI AM335x BeagleBone Black";
    
    &i2c0 {
        eeprom@50 {
            compatible = "atmel,24c256";
            reg = <0x50>;
            pagesize = <64>;
        };
    };
};

你看,am33xx.dtsi定义了SoC的公共部分,am335x-bone-common.dtsi定义了板级公共部分,最后am335x-boneblack.dts只添加差异部分。这样维护起来轻松多了。

我的经验:

  • 把SoC级定义放在.dtsi文件中,板级差异放在.dts中
  • 用&标签语法覆盖或追加节点属性,不要直接修改原始文件
  • 头文件中的宏定义(如GPIO_ACTIVE_HIGH)能提高可读性,建议多用

5. 知识体系总览

下面这张图总结了设备树语法的核心结构,我建议你把它打印出来贴在工位上。

根节点 / cpus / memory / i2c chosen / aliases 标签 & 引用 标准属性 compatible: 驱动匹配 reg: 地址空间 interrupts: 中断配置 特殊节点 bootargs stdout-path 别名映射 预处理 #include 头文件 #define 宏定义 &标签 覆盖/追加 设备树 = 节点树 + 属性描述 + 预处理机制 硬件描述 → 驱动匹配 → 资源分配

这张图把设备树的核心要素都串起来了。你从根节点出发,往下走是各种设备节点,每个节点挂载标准属性;特殊节点chosen和aliases提供启动参数和别名;预处理机制让代码可以模块化组织。

最后说一句:设备树语法不难,难的是理解硬件和驱动之间的映射关系。我建议你拿到一块新板子时,先读它的.dts文件,对照原理图把每个节点的reg和interrupts过一遍。这样调试驱动时,心里就有底了。

可可精品资料库