引言

在嵌入式Linux开发中,设备树(Device Tree)是一个非常重要的概念。它提供了一种描述硬件设备的方式,使得内核可以在不同的硬件平台上运行,而无需为每个平台编写特定的代码。本文将详细介绍设备树的基础知识、语法、编译过程以及如何在驱动中使用设备树。

1. 设备树概述

1.1 什么是设备树?

设备树(Device Tree)是一种描述硬件设备的数据结构,通常用于嵌入式系统中。它通过一种树形结构来描述系统中的硬件设备及其连接关系。设备树的主要目的是将硬件的描述与操作系统的代码分离,从而提高代码的可移植性和可维护性。

1.2 设备树的组成部分

设备树主要由以下几个部分组成:

  • DTS(Device Tree Source):设备树源文件,通常以.dts为后缀。它是一个文本文件,描述了硬件设备的结构和属性。
  • DTB(Device Tree Blob):设备树二进制文件,通常以.dtb为后缀。它是DTS文件编译后生成的二进制文件,供内核使用。
  • DTC(Device Tree Compiler):设备树编译器,用于将DTS文件编译为DTB文件。

2. DTS、DTB 和 DTC 的区别与编译过程

2.1 DTS、DTB 和 DTC 的区别

  • DTS:设备树源文件,是文本文件,便于人类阅读和编辑。
  • DTB:设备树二进制文件,是DTS文件编译后生成的二进制文件,供内核使用。
  • DTC:设备树编译器,用于将DTS文件编译为DTB文件。

2.2 如何将.dts 文件编译为.dtb 文件

要将DTS文件编译为DTB文件,可以使用DTC编译器。以下是一个简单的编译命令:

dtc -I dts -O dtb -o output.dtb input.dts
  • -I dts:指定输入文件格式为DTS。
  • -O dtb:指定输出文件格式为DTB。
  • -o output.dtb:指定输出文件名。
  • input.dts:输入的DTS文件。

2.3 示例

假设我们有一个名为example.dts的设备树源文件,内容如下:

/dts-v1/;

/ {
    model = "Example Board";
    compatible = "example,board";

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

    memory {
        device_type = "memory";
        reg = <0x80000000 0x10000000>;
    };
};

我们可以使用以下命令将其编译为DTB文件:

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

编译完成后,将生成一个名为example.dtb的二进制文件。

3. 设备树语法

设备树语法是设备树描述的基础,掌握设备树语法对于理解和修改设备树至关重要。以下是设备树语法的详细介绍。

3.1 设备树的基本结构

设备树的基本结构是一个树形结构,由节点(Node)和属性(Property)组成。每个节点可以包含子节点和属性。

3.1.1 节点

节点是设备树的基本组成单位,通常表示一个硬件设备或一个功能模块。节点的定义格式如下:

node-name {
    property1 = value1;
    property2 = value2;
    ...
    child-node {
        ...
    };
};
  • node-name:节点的名称。
  • property1, property2:节点的属性。
  • child-node:子节点。

3.1.2 属性

属性是节点的键值对,用于描述节点的特性。属性的定义格式如下:

property-name = value;
  • property-name:属性的名称。
  • value:属性的值,可以是字符串、整数、数组等。

3.2 设备树的常用属性

以下是一些常用的设备树属性:

  • compatible:用于匹配驱动程序的字符串列表。
  • reg:表示设备的寄存器地址和大小。
  • interrupts:表示设备的中断号。
  • gpio:表示设备的GPIO引脚。
  • clock-frequency:表示设备的时钟频率。

3.3 设备树的特殊节点

设备树中有一些特殊的节点,它们在设备树中具有特殊的意义。

3.3.1 / 根节点

根节点是设备树的顶级节点,所有其他节点都是根节点的子节点。根节点通常包含一些全局属性,如modelcompatible

/ {
    model = "Example Board";
    compatible = "example,board";
    ...
};

3.3.2 aliases 节点

aliases节点用于定义节点的别名,方便在设备树中引用。

aliases {
    serial0 = &uart0;
    ethernet0 = &eth0;
};

3.3.3 chosen 节点

chosen节点用于传递内核启动参数或指定根文件系统。

chosen {
    bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2 rootfstype=ext4 rootwait";
};

3.4 设备树的示例

以下是一个完整的设备树示例:

/dts-v1/;

/ {
    model = "Example Board";
    compatible = "example,board";

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

    memory {
        device_type = "memory";
        reg = <0x80000000 0x10000000>;
    };

    uart0: serial@101f1000 {
        compatible = "ns16550a";
        reg = <0x101f1000 0x1000>;
        interrupts = <0 0 4>;
        clock-frequency = <1843200>;
    };

    eth0: ethernet@101f2000 {
        compatible = "smc911x";
        reg = <0x101f2000 0x1000>;
        interrupts = <0 0 5>;
    };
};

4. 设备树的OF操作函数

设备树最终是被驱动文件所使用的,驱动文件需要读取设备树中的属性信息,如内存信息、GPIO信息、中断信息等。为了在驱动中读取设备树的属性值,Linux内核提供了一系列的OF(Open Firmware)操作函数。

4.1 OF操作函数概述

OF操作函数是Linux内核提供的一组用于操作设备树的API。这些函数定义在include/linux/of.h头文件中,常用的函数包括:

  • of_find_node_by_name:根据节点名称查找节点。
  • of_find_node_by_path:根据路径查找节点。
  • of_property_read_u32:读取32位整数属性。
  • of_property_read_string:读取字符串属性。
  • of_get_named_gpio:获取GPIO引脚号。

4.2 OF操作函数的使用示例

以下是一个简单的驱动示例,展示了如何使用OF操作函数读取设备树中的属性。

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_gpio.h>

static int example_probe(struct platform_device *pdev)
{
    struct device_node *np = pdev->dev.of_node;
    int gpio;
    u32 value;
    const char *str;

    if (!np) {
        dev_err(&pdev->dev, "No device tree node found\n");
        return -EINVAL;
    }

    /* 读取整数属性 */
    if (of_property_read_u32(np, "example-value", &value)) {
        dev_err(&pdev->dev, "Failed to read 'example-value'\n");
        return -EINVAL;
    }
    dev_info(&pdev->dev, "example-value = %d\n", value);

    /* 读取字符串属性 */
    if (of_property_read_string(np, "example-string", &str)) {
        dev_err(&pdev->dev, "Failed to read 'example-string'\n");
        return -EINVAL;
    }
    dev_info(&pdev->dev, "example-string = %s\n", str);

    /* 获取GPIO引脚号 */
    gpio = of_get_named_gpio(np, "example-gpio", 0);
    if (gpio < 0) {
        dev_err(&pdev->dev, "Failed to get 'example-gpio'\n");
        return gpio;
    }
    dev_info(&pdev->dev, "example-gpio = %d\n", gpio);

    return 0;
}

static int example_remove(struct platform_device *pdev)
{
    dev_info(&pdev->dev, "Example device removed\n");
    return 0;
}

static const struct of_device_id example_of_match[] = {
    { .compatible = "example,device", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, example_of_match);

static struct platform_driver example_driver = {
    .probe = example_probe,
    .remove = example_remove,
    .driver = {
        .name = "example",
        .of_match_table = example_of_match,
    },
};

module_platform_driver(example_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Example Device Tree Driver");

4.3 设备树中的属性

假设我们有一个设备树节点如下:

example_device: example@101f3000 {
    compatible = "example,device";
    example-value = <1234>;
    example-string = "Hello, World!";
    example-gpio = <&gpio0 5 0>;
};

在驱动中,我们可以使用OF操作函数读取这些属性:

  • example-value:32位整数属性。
  • example-string:字符串属性。
  • example-gpio:GPIO引脚号。

4.4 编译和加载驱动

编译并加载上述驱动后,内核日志中将输出以下信息:

example-value = 1234
example-string = Hello, World!
example-gpio = 5

5. 总结

本文详细介绍了Linux设备树的基础知识、语法、编译过程以及如何在驱动中使用设备树。通过掌握设备树的基本概念和OF操作函数的使用,开发者可以更好地理解和修改设备树,从而编写出更加灵活和可移植的驱动程序。

设备树在嵌入式Linux开发中扮演着至关重要的角色,理解设备树的工作原理和使用方法,对于嵌入式Linux开发者来说是必不可少的技能。希望本文能够帮助读者更好地理解和应用设备树。

参考文献