3. 设备树基础:DTS语法、节点属性、compatible匹配机制

设备树,说白了就是一块「硬件描述板」。

我刚开始接触Linux驱动时,最头疼的就是搞不懂设备树到底在干嘛。后来有个老同事跟我说了一句话,我到现在还记得:「设备树就是给内核看的硬件说明书,告诉它你的板子上接了哪些外设,地址是多少,中断挂在哪根线上。」

嗯,这句话一下子就点醒了我。今天我们就来聊聊这个「说明书」怎么写。

3.1 为什么需要设备树?

在设备树出现之前,Linux内核是怎么知道硬件信息的?答案是——硬编码。每个板子都在arch/arm/mach-xxx目录下有一堆C文件,里面写死了GPIO、中断、时钟等信息。你想想看,换一个板子就得改内核代码,重新编译,这谁受得了?

设备树(Device Tree)就是为了解决这个问题。它把硬件描述从内核代码中剥离出来,变成一个独立的.dts文件。内核启动时解析这个文件,动态构建出设备模型。说白了,就是「硬件配置」和「驱动代码」解耦了。

核心思想:设备树描述硬件,驱动代码处理逻辑。两者通过compatible属性「握手」。

3.2 DTS语法基础

设备树源文件(.dts)的语法其实很简单。我个人的习惯是把它想象成一棵倒着长的树——根节点是根,下面挂着一堆子节点,每个节点代表一个设备或总线。

先看一个最简单的例子:

/dts-v1/;

/ {
    model = "MyBoard";
    compatible = "vendor,myboard", "vendor,common-board";

    cpu@0 {
        compatible = "arm,cortex-a7";
        reg = <0>;
    };

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

    uart0: serial@10000000 {
        compatible = "vendor,uart";
        reg = <0x10000000 0x1000>;
        interrupts = <0 33 4>;
        status = "okay";
    };
};

嗯,这里有几个关键点:

  • /dts-v1/; —— 版本声明,告诉编译器这是DTS v1格式。没有这行,编译器会报错。
  • / { }; —— 根节点。所有设备都挂在这个根下面。
  • model —— 板子名称,纯描述性,内核不依赖它做匹配。
  • compatible —— 最重要的属性,后面细讲。
  • reg —— 地址和大小信息。格式是 <地址 大小>。
  • uart0: —— 标签(label),方便在其他地方引用这个节点。

我的小技巧:写设备树时,我习惯先写根节点,再写memory节点,最后写外设节点。这样结构清晰,不容易漏掉关键信息。

3.3 节点属性详解

设备树的节点属性,说白了就是键值对。值可以是字符串、32位整数、64位整数、字节数组,或者它们的组合。

我整理了一个常用属性表,方便你查阅:

属性名 含义 示例
compatible 设备兼容性标识,驱动匹配的关键 "vendor,device"
reg 寄存器地址和长度 <0x10000000 0x1000>
interrupts 中断号、触发类型 <0 33 4>
status 设备状态,"okay"或"disabled" "okay"
clock-frequency 时钟频率(单位Hz) <24000000>
gpios GPIO引脚描述 <&gpio1 15 0>

这里要注意一点:reg属性的格式取决于父节点的#address-cells和#size-cells。如果#address-cells=1,地址就是32位;如果=2,地址就是64位。size-cells同理。我在项目中就踩过这个坑——写错了address-cells,结果内核解析出来的地址完全不对,调试了一整天。

3.4 compatible匹配机制

compatible属性是设备树和驱动之间的「暗号」。内核驱动通过of_match_table声明自己支持哪些设备,设备树通过compatible告诉内核「我是谁」。两者对上号了,驱动就probe。

匹配规则是这样的:

  1. 内核遍历设备树节点,读取compatible属性。
  2. 遍历已注册驱动的of_match_table。
  3. 字符串逐条比较,匹配成功则调用驱动的probe函数。
  4. 如果compatible有多个值(如"vendor,myboard", "vendor,common-board"),按顺序匹配,第一个成功就停。

举个例子,驱动端这样写:

static const struct of_device_id my_uart_of_match[] = {
    { .compatible = "vendor,uart", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_uart_of_match);

static struct platform_driver my_uart_driver = {
    .probe  = my_uart_probe,
    .remove = my_uart_remove,
    .driver = {
        .name = "my_uart",
        .of_match_table = my_uart_of_match,
    },
};

设备树节点里写:

uart0: serial@10000000 {
    compatible = "vendor,uart";
    reg = <0x10000000 0x1000>;
    interrupts = <0 33 4>;
};

这样,内核启动时就会自动匹配,调用my_uart_probe函数。

我曾经踩过的坑:compatible字符串的格式一定要是"厂商,设备名",比如"ti,am335x-uart"。不要随便写,否则驱动匹配不上。另外,MODULE_DEVICE_TABLE宏一定要加,否则模块热插拔时找不到匹配关系。

3.5 编写一个简单的设备树节点

好了,理论讲完了,我们来动手写一个。假设我有一个LED设备,挂在GPIO1的第15号引脚上,高电平点亮。

完整的设备树节点如下:

myled: led@0 {
    compatible = "vendor,myled";
    reg = <0>;  /* 仅用于索引,无实际地址 */
    gpios = <&gpio1 15 GPIO_ACTIVE_HIGH>;
    label = "myboard:green:status";
    linux,default-trigger = "heartbeat";
    status = "okay";
};

这里解释一下:

  • compatible —— 驱动匹配用,写"vendor,myled"。
  • gpios —— 引用gpio1控制器,第15号引脚,高电平有效。
  • label —— 用户空间的设备名,比如/sys/class/leds/myboard:green:status。
  • linux,default-trigger —— 默认触发模式,这里用心跳灯。

对应的驱动端,of_match_table里写:

{ .compatible = "vendor,myled", },

probe函数里通过of_get_named_gpio_flags获取GPIO号,然后注册LED设备。嗯,就是这么简单。

我的建议:刚开始写设备树时,多看看内核源码里arch/arm/boot/dts/目录下的.dts文件。那些都是官方写好的例子,照着抄不会错。我自己就是靠「抄」学会的——先抄一个能用的,再慢慢改出自己需要的。

3.6 设备树知识体系总览

为了让你对设备树有个整体认识,我画了一张图:

根节点 / cpu@0 memory@80000000 serial@10000000 属性 compatible = "arm,cortex-a7" reg = <0> device_type = "cpu" 属性 device_type = "memory" reg = <0x80000000 0x20000000> 属性 compatible = "vendor,uart" reg = <0x10000000 0x1000> interrupts = <0 33 4> status = "okay" 设备树核心逻辑 1. 设备树描述硬件拓扑结构(树形) 2. 每个节点通过compatible属性声明设备类型 3. 驱动通过of_match_table与compatible匹配 4. 匹配成功后调用probe函数,驱动开始工作

这张图展示了设备树的基本结构。根节点下面挂CPU、内存、外设,每个节点都有自己的属性。驱动通过compatible找到对应的节点,然后读取属性来配置硬件。

好了,设备树的基础知识就讲到这里。记住一句话:设备树是硬件的「身份证」,compatible是驱动的「钥匙」。把这两点搞明白,后面写platform驱动就顺了。


可可精品资料库