本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:DeviceTree是一种用于描述嵌入式系统硬件结构的数据结构,广泛应用于Linux内核启动过程中,帮助操作系统动态识别和初始化硬件资源。它通过.dts源文件定义硬件信息,并编译为.dtb文件供内核解析。DeviceTree采用节点与属性的方式描述硬件组件及其连接关系,支持驱动自动匹配,极大提升了系统的可移植性与灵活性。本文介绍了DeviceTree的基本概念、结构组成、编译流程及其在Windows 7环境下通过虚拟化工具进行开发的方法,为深入Linux驱动开发打下基础。
DeviceTree

1. DeviceTree基本概念与作用

DeviceTree(设备树)是一种以数据结构形式描述硬件配置的机制,起源于IEEE 1275标准中的Open Firmware规范。它通过结构化的文本文件(.dts)或编译后的二进制文件(.dtb)来描述系统硬件信息,使得操作系统可以在启动早期获取硬件布局,而无需硬编码平台细节。

在现代嵌入式Linux系统中,DeviceTree起到了关键的桥梁作用,它实现了硬件描述与操作系统内核逻辑的解耦,使同一内核镜像可适配多种硬件平台。通过DeviceTree,操作系统在启动过程中可以动态读取硬件信息,完成设备初始化和资源配置,极大提升了系统的可移植性与灵活性。

本章将从DeviceTree的基本组成结构入手,逐步深入其在系统启动流程中的作用机制,并结合实例说明其如何实现硬件抽象与驱动分离的核心设计理念。

2. DeviceTree在嵌入式系统中的应用场景

嵌入式系统以其广泛的应用领域和多样的硬件平台著称。从智能穿戴设备到工业控制系统,再到车载电子系统,每种应用场景都可能涉及不同的处理器架构、外设配置和系统资源分配。这种硬件多样性为嵌入式软件开发带来了巨大挑战,尤其是在驱动适配和设备初始化方面。而DeviceTree的引入,为解决这些问题提供了一种高效、灵活、可扩展的机制。

2.1 嵌入式系统的硬件多样性挑战

嵌入式系统的一个显著特点是其硬件平台的多样性。不同的项目可能使用不同型号的CPU、内存控制器、I/O接口、通信模块等。这种多样性使得嵌入式操作系统(如Linux)在支持多种硬件时面临巨大的适配压力。

2.1.1 硬件平台差异带来的驱动适配难题

在传统嵌入式Linux系统中,驱动程序通常与特定硬件平台紧密耦合。例如,一个基于ARM Cortex-A7的开发板可能需要专门为其编写或配置GPIO、UART、SPI等驱动模块。一旦更换为另一款ARM处理器,或者换用RISC-V架构的平台,原有的驱动代码可能无法直接使用,必须进行大量修改甚至重写。

这样的开发模式存在以下问题:

  • 维护成本高 :每个硬件平台都需要维护一套独立的驱动代码。
  • 代码冗余严重 :许多驱动逻辑相似,但由于平台差异,无法共享。
  • 开发周期长 :新平台引入时需要重新适配大量驱动。

例如,以下是一段典型的GPIO驱动初始化代码,其中硬编码了寄存器地址和中断号:

static int my_gpio_probe(struct platform_device *pdev)
{
    struct resource *res;
    void __iomem *base;

    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(base))
        return PTR_ERR(base);

    writel(0x1, base + GPIO_DIR);  // 设置GPIO方向为输出
    writel(0x0, base + GPIO_DATA); // 设置GPIO输出低电平

    return 0;
}

代码逻辑分析:
- platform_get_resource :获取设备资源(如内存地址);
- devm_ioremap_resource :将物理地址映射为虚拟地址供驱动访问;
- writel :写入寄存器值,设置GPIO方向和初始输出。

这段代码虽然逻辑清晰,但其资源地址和寄存器偏移是硬编码的,无法适应不同硬件平台的差异。

2.1.2 使用DeviceTree统一硬件描述的优势

DeviceTree的出现,为解决这一问题提供了标准化的描述机制。通过将硬件信息从驱动代码中抽离出来,写入设备树文件(.dts),操作系统可以在运行时动态解析这些信息,从而实现驱动代码与硬件平台的解耦。

以GPIO驱动为例,使用DeviceTree后,驱动可以改写为:

static int my_gpio_probe(struct platform_device *pdev)
{
    struct device_node *np = pdev->dev.of_node;
    struct resource res;
    void __iomem *base;

    of_address_to_resource(np, 0, &res); // 从设备树中获取内存地址
    base = devm_ioremap_resource(&pdev->dev, &res);
    if (IS_ERR(base))
        return PTR_ERR(base);

    int irq = irq_of_parse_and_map(np, 0); // 获取中断号
    if (irq <= 0)
        return -EINVAL;

    // 其他初始化逻辑
    return 0;
}

代码逻辑分析:
- of_address_to_resource :从设备树节点中获取内存地址;
- irq_of_parse_and_map :从设备树中获取中断号;
- 驱动不再硬编码任何硬件参数,完全依赖设备树。

这样一来,驱动代码可以适用于多种硬件平台,只需在设备树中配置不同的资源信息即可。

设备树片段示例:
gpio-controller@100 {
    compatible = "mycompany,gpio-ctrl";
    reg = <0x100 0x20>;     // 内存地址和长度
    interrupts = <0x5A 0x4>; // 中断号和触发类型
};

参数说明:
- compatible :用于驱动匹配,标识该GPIO控制器型号;
- reg :指定寄存器起始地址和长度;
- interrupts :指定中断号和触发类型(高电平/低电平/边沿触发)。

2.2 DeviceTree在ARM架构中的广泛应用

ARM架构广泛应用于嵌入式设备中,从智能手机到工业控制系统,ARM平台的多样性对操作系统支持提出了更高要求。DeviceTree在ARM架构中的应用尤为广泛,几乎成为其标准配置。

2.2.1 ARM平台为何依赖设备树

ARM架构不像x86平台那样有统一的硬件枚举机制(如ACPI)。早期ARM Linux内核采用静态配置的方式,每个平台都有独立的配置头文件,导致代码臃肿、难以维护。为了应对这一问题,ARM社区逐步转向DeviceTree机制,以实现硬件信息的动态描述。

DeviceTree为ARM平台带来的主要优势包括:

  • 硬件信息动态加载 :无需修改内核代码即可支持新平台;
  • 多平台统一构建 :一个内核镜像可支持多个设备;
  • 驱动与硬件分离 :提高驱动代码复用率,降低维护成本。

2.2.2 常见ARM开发板中的设备树配置实例

以常见的ARM开发板BeagleBone Black为例,其设备树片段如下:

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

    cpus {
        cpu@0 {
            compatible = "arm,cortex-a8";
        };
    };

    memory {
        device_type = "memory";
        reg = <0x80000000 0x20000000>; // 512MB内存
    };

    uart0: serial@44e09000 {
        compatible = "ti,omap3-uart";
        reg = <0x44e09000 0x2000>;
        interrupts = <0x48 0x4>;
        status = "okay";
    };
};

参数说明:
- model :设备型号;
- compatible :用于平台匹配;
- memory :定义系统内存地址和大小;
- uart0 :串口设备节点,包含寄存器地址、中断号和状态。

通过这样的设备树配置,Linux内核可以在启动时自动识别BeagleBone Black的硬件资源,并加载相应的驱动。

2.3 多平台支持与设备树复用

在嵌入式开发中,常常需要支持多个硬件平台。DeviceTree提供了良好的复用机制,使得开发者可以在不同平台上复用部分设备树结构,从而减少重复工作。

2.3.1 设备树的重用机制与通用性设计

DeviceTree支持通过 include 语句引入通用的设备树片段。例如,多个ARM平台可能共享相同的CPU子系统描述,可以将其抽象为通用的 .dtsi 文件。

#include "skeleton.dtsi"

/ {
    model = "My Custom Board";
    compatible = "mycompany,custom-board", "mycompany,common-board";

    chosen {
        bootargs = "console=ttyO0,115200n8";
    };
};

参数说明:
- skeleton.dtsi :通用设备树模板,包含基本CPU、内存等配置;
- compatible :第一个值为当前平台标识,第二个值为通用平台标识;
- chosen :指定启动参数。

通过这种方式,开发者可以构建一个设备树“家族”,通过继承和覆盖实现不同平台的差异化配置。

2.3.2 如何通过设备树实现一次编译多平台运行

Linux内核支持多平台编译,即在一个内核镜像中包含多个设备树文件(.dtb),启动时根据Bootloader传递的设备树文件名加载对应的设备树。

多平台编译步骤如下:
  1. 配置内核支持多个设备树:
make menuconfig
# 选择 Processor type and features  --->
#     [*] Multi-platform support
#     [*]   Select an ARM system type (ARM generic DT based system)  --->
  1. 编译多个设备树文件:
make dtbs

这将生成多个 .dtb 文件,分别对应不同的平台。

  1. 将多个 .dtb 文件打包进内核镜像:
make Image dtbs
  1. 在U-Boot中指定加载的设备树:
setenv fdtfile myboard.dtb
bootz 0x80008000 - ${fdtaddr}

这样,同一个内核镜像可以在不同硬件平台上运行,只需加载对应的设备树文件即可。

本章小结(不输出)

(注:根据要求,过滤总结类语句)

下一章将深入探讨Linux内核如何解析设备树,以及设备树如何与内核设备模型进行绑定。

3. Linux内核与DeviceTree的交互机制

Linux 内核与 DeviceTree 的交互机制是现代嵌入式系统中硬件抽象与驱动管理的关键环节。随着 ARM 架构设备的普及,DeviceTree 成为了内核识别硬件配置的主要手段。通过设备树,Linux 内核能够在启动阶段动态加载硬件信息,实现驱动与硬件平台的解耦。本章将从设备树的解析机制、内核设备模型与设备树的绑定关系、以及设备树在驱动匹配中的作用三个核心方面,深入剖析 Linux 内核如何利用 DeviceTree 实现硬件描述与驱动加载的统一。

3.1 Linux内核如何解析设备树

设备树的解析是 Linux 内核启动过程中的重要环节。内核通过解析设备树节点来获取硬件平台的配置信息,包括 CPU、内存布局、外设地址、中断控制器等关键参数。这一过程依赖于 Open Firmware(OF)接口,并最终将设备树结构转换为内核可操作的设备模型。

3.1.1 内核启动时的设备树加载过程

Linux 内核在启动过程中,通常由 Bootloader(如 U-Boot)将设备树二进制文件(.dtb)加载到内存中。随后,内核在启动早期阶段(通常是 setup_arch 函数中)读取设备树地址并开始解析。

以下是设备树加载过程的流程图:

graph TD
    A[Bootloader启动] --> B[加载Linux内核镜像]
    B --> C[加载.dtb设备树文件]
    C --> D[跳转至内核入口]
    D --> E[setup_arch函数调用]
    E --> F[调用unflatten_device_tree]
    F --> G[初始化设备树结构]
    G --> H[注册OF接口]
    H --> I[设备树解析完成]

解析过程关键步骤:

  1. 设备树地址获取 :Bootloader 会将设备树的物理地址传递给内核,通常通过寄存器或特定内存地址。
  2. 调用 unflatten_device_tree() :该函数负责将扁平设备树(Flattened Device Tree)转换为结构化的设备树节点树。
  3. 初始化 OF 接口 :注册 of_platform_populate() 等函数,为后续设备驱动注册做准备。
  4. 设备树节点缓存 :将解析后的设备树节点缓存为 struct device_node 结构,供后续驱动访问。

3.1.2 OF(Open Firmware)接口与设备树节点解析

Open Firmware 接口是 Linux 内核提供的用于访问设备树节点的统一接口,定义在 include/linux/of.h 头文件中。以下是一些常用的 OF 接口函数及其作用:

函数名 功能描述
of_find_node_by_path 根据路径查找设备树节点
of_get_property 获取设备树节点的属性值
of_property_read_u32 读取指定属性的 u32 类型值
of_match_node 根据 compatible 属性匹配驱动
of_platform_populate 遍历设备树节点并注册 platform_device

以下是一个使用 OF 接口读取设备树属性值的代码示例:

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

static int example_probe(struct platform_device *pdev)
{
    struct device_node *np = pdev->dev.of_node;
    u32 reg_value;

    if (of_property_read_u32(np, "reg", &reg_value)) {
        dev_err(&pdev->dev, "Failed to read 'reg' property\n");
        return -EINVAL;
    }

    dev_info(&pdev->dev, "Register value: 0x%x\n", reg_value);
    return 0;
}

代码分析:

  • pdev->dev.of_node :获取与该 platform_device 关联的设备树节点。
  • of_property_read_u32 :尝试读取名为 reg 的属性值。若读取失败则返回错误码。
  • dev_info :输出调试信息,显示寄存器值。

通过 OF 接口,驱动程序可以动态地从设备树中获取配置信息,而无需硬编码硬件地址,从而提高代码的可移植性和复用性。

3.2 内核设备模型与设备树的绑定关系

Linux 内核设备模型是构建在设备树之上的抽象结构,它将设备树中的节点转换为 platform_device 或其他总线设备(如 i2c_client spi_device 等)。设备模型与设备树的绑定机制,是实现硬件抽象与驱动匹配的基础。

3.2.1 platform_device与设备树节点的对应关系

platform_device 是 Linux 中用于描述 SoC 内部集成设备的一种设备类型,通常用于 ARM 架构中的设备驱动。每个 platform_device 都与设备树中的一个节点相对应。

以下是一个设备树节点示例(.dts 文件):

my_device: mydevice@1a4 {
    compatible = "mycompany,mydevice";
    reg = <0x1a4 0x20>;
    interrupts = <0x5a 0x4>;
    status = "okay";
};

该设备树节点在内核中将被转换为一个 platform_device ,并通过 of_match_table 与驱动程序进行匹配。

驱动代码中使用 of_match_table 的示例如下:

static const struct of_device_id mydevice_of_match[] = {
    { .compatible = "mycompany,mydevice" },
    {}
};
MODULE_DEVICE_TABLE(of, mydevice_of_match);

static struct platform_driver mydevice_driver = {
    .probe = mydevice_probe,
    .remove = mydevice_remove,
    .driver = {
        .name = "mydevice",
        .of_match_table = mydevice_of_match,
    },
};

绑定机制说明:

  • of_match_table 提供了驱动与设备树节点之间的匹配规则。
  • of_platform_populate() 被调用时,内核会遍历设备树节点,并为每个匹配的 compatible 字符串创建对应的 platform_device
  • 匹配成功后, probe() 函数将被调用,完成设备初始化。

3.2.2 驱动程序如何通过设备树获取资源信息

驱动程序可以通过设备树获取资源信息,例如寄存器地址、中断号、时钟频率等。Linux 提供了多个辅助函数用于提取这些信息。

例如,读取寄存器地址并映射为虚拟地址的代码如下:

struct resource res;
void __iomem *regs;

if (of_address_to_resource(np, 0, &res)) {
    dev_err(dev, "Failed to get memory resource\n");
    return -ENODEV;
}

regs = devm_ioremap_resource(dev, &res);
if (IS_ERR(regs)) {
    dev_err(dev, "Failed to ioremap registers\n");
    return PTR_ERR(regs);
}

参数说明:

  • of_address_to_resource() :从设备树中提取地址资源,填充 struct resource
  • devm_ioremap_resource() :将物理地址映射为内核可访问的虚拟地址。
  • regs :用于访问寄存器的指针。

这种机制使得驱动程序能够灵活适应不同平台的硬件配置,而无需修改代码即可支持多种设备。

3.3 设备树在设备驱动匹配中的作用

设备树在驱动匹配中的核心作用体现在 compatible 属性的使用以及总线驱动对设备树节点的解析。设备驱动通过 of_match_table 匹配 compatible 字符串,从而确定该驱动是否适用于当前设备节点。

3.3.1 compatible属性在驱动匹配中的关键地位

compatible 属性是设备树中最为关键的字段之一,它用于标识设备的类型和厂商信息。驱动程序通过 of_match_table 指定其支持的 compatible 值,从而实现设备与驱动的自动匹配。

以下是一个设备树节点的 compatible 属性定义:

i2c_controller: i2c@7000c000 {
    compatible = "nvidia,tegra20-i2c";
    reg = <0x7000c000 0x100>;
    interrupts = <0x2e 0x4>;
};

对应的驱动匹配代码如下:

static const struct of_device_id tegra_i2c_of_match[] = {
    { .compatible = "nvidia,tegra20-i2c" },
    { .compatible = "nvidia,tegra30-i2c" },
    {}
};

static struct platform_driver tegra_i2c_driver = {
    .driver = {
        .name = "tegra-i2c",
        .of_match_table = tegra_i2c_of_match,
    },
};

匹配机制说明:

  • 当内核遍历设备树节点时,会检查该节点的 compatible 字符串是否存在于驱动的 of_match_table 中。
  • 若存在匹配项,则调用驱动的 probe() 函数进行初始化。
  • 若多个驱动支持相同设备,优先级由匹配顺序和模块加载顺序决定。

3.3.2 总线类型(如I2C、SPI)如何利用设备树进行设备注册

I2C 和 SPI 等总线类型的设备注册同样依赖于设备树。以 I2C 为例,主控制器在设备树中定义后,其子设备(如传感器)也会作为其子节点列出,由 I2C 总线驱动自动注册。

以下是一个 I2C 子设备的设备树定义:

i2c_controller: i2c@7000c000 {
    compatible = "nvidia,tegra20-i2c";
    reg = <0x7000c000 0x100>;
    status = "okay";

    my_sensor@40 {
        compatible = "mycompany,my-sensor";
        reg = <0x40>;
    };
};

I2C 总线驱动在匹配 i2c_controller 节点后,会自动遍历其子节点,并为每个子设备创建对应的 i2c_client 结构体。

驱动匹配代码如下:

static const struct of_device_id my_sensor_of_match[] = {
    { .compatible = "mycompany,my-sensor" },
    {}
};

static struct i2c_driver my_sensor_driver = {
    .driver = {
        .name = "my-sensor",
        .of_match_table = my_sensor_of_match,
    },
    .probe = my_sensor_probe,
};

注册流程分析:

  1. I2C 控制器驱动匹配成功 i2c@7000c000 节点匹配成功,驱动初始化 I2C 主控制器。
  2. 子设备注册 :控制器驱动调用 i2c_add_numbered_adapter() 后,内核会自动遍历子节点,并注册 i2c_client
  3. 驱动匹配 my_sensor@40 节点的 compatible 匹配到 my_sensor_driver ,触发 probe() 函数。

这种机制使得 I2C、SPI 等总线设备的驱动注册变得自动化,减少了手动注册设备的复杂性,提高了代码的可维护性。

本章深入剖析了 Linux 内核与 DeviceTree 的交互机制,包括设备树的加载与解析流程、设备模型与设备树的绑定方式,以及设备树在驱动匹配中的核心作用。通过对 OF 接口的使用、platform_device 的绑定、以及 I2C/SPI 等总线设备的自动注册机制的分析,展示了 DeviceTree 在现代嵌入式系统中的重要性和灵活性。

4. .dts与.dtb文件格式解析

设备树源文件(.dts)和设备树二进制文件(.dtb)是嵌入式Linux系统中描述硬件配置的核心组件。理解它们的格式和编译过程,对于开发者调试设备驱动、适配新硬件平台具有重要意义。本章将深入剖析.dts文件的语法结构、.dts到.dtb的编译机制,以及不同版本设备树文件的兼容性问题。

4.1 .dts源文件的语法结构

DeviceTree源文件(.dts)是一种结构化文本文件,采用类C语言的语法风格,通过节点和属性来描述硬件信息。理解其语法结构是编写和修改设备树的前提。

4.1.1 节点定义与属性书写规范

.dts文件由一个根节点开始,其余节点嵌套在其中,形成树状结构。每个节点可以包含多个属性,也可以嵌套子节点。节点和属性的书写遵循特定的语法规范。

示例代码:
/ {
    model = "Example Board";
    compatible = "vendor,example-board";
    chosen {
        bootargs = "console=ttyS0,115200";
    };
};
逐行解析:
  • / { ... } :根节点定义,所有其他节点都嵌套在根节点中。
  • model = "Example Board"; :设置模型名称,用于标识开发板型号。
  • compatible = "vendor,example-board"; :指定平台兼容性标识符,内核根据此字段匹配驱动。
  • chosen { ... } chosen 是一个特殊节点,通常用于存储启动参数等信息。
  • bootargs = "console=ttyS0,115200"; :启动参数配置,指定串口终端设备及波特率。
属性书写规范:
属性类型 示例 说明
字符串属性 "hello" 使用双引号包裹
整数属性 <0x1234> 使用尖括号包裹,支持16进制和十进制
数组属性 <0x12 0x34 0x56> 多个整数用空格分隔
字符串数组 "a", "b", "c" 多个字符串用逗号分隔

4.1.2 包含头文件与宏定义的使用方法

为了提高设备树的可维护性和复用性,.dts文件中可以包含头文件(.dtsi),并通过宏定义简化重复代码。

示例代码:
#include "skeleton.dtsi"

/ {
    model = "Custom Board";
    compatible = "vendor,custom-board";

    cpus {
        cpu@0 {
            compatible = "arm,cortex-a9";
        };
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x20000000>;
    };
};
代码解析:
  • #include "skeleton.dtsi" :引入通用设备树模板,通常包含CPU、中断控制器等基础配置。
  • 宏定义在设备树中不直接支持,但可以通过 /include/ 机制实现代码复用,例如:
    dts /include/ "common-definitions.dtsi"
设备树包含机制流程图:
graph TD
    A[主.dts文件] --> B[包含.dtsi模板]
    B --> C[引入通用配置]
    C --> D[添加平台特有配置]
    D --> E[生成完整设备树结构]

4.2 .dts到.dtb的编译过程

.dts文件需要通过设备树编译器(DTC)转换为二进制格式(.dtb),才能被Linux内核加载。理解其编译过程有助于开发者调试和优化设备树。

4.2.1 Flattened Device Tree的结构组成

Flattened Device Tree(扁平设备树)是.dtb文件的内部结构,由多个段组成,包括:

段名 描述
struct fdt_header 文件头,包含版本信息、总长度等
memory reservation block 内存保留区域信息
structure block 节点和子节点的结构描述
strings block 所有属性名称的字符串池
data block 属性值的数据部分
设备树编译流程图:
graph LR
    A[.dts源文件] --> B{设备树编译器}
    B --> C[语法解析]
    C --> D[结构转换]
    D --> E[生成.dtb二进制文件]

4.2.2 编译后的.dtb文件如何被内核加载

Linux内核在启动阶段通过Bootloader(如U-Boot)加载.dtb文件,并在初始化过程中解析其内容,构建设备模型。

内核加载流程:
graph TD
    A[Bootloader] --> B[加载.dtb到内存]
    B --> C[跳转到内核入口]
    C --> D[内核解析.dtb]
    D --> E[创建platform_device等设备节点]
    E --> F[驱动程序匹配compatible属性]
示例:内核启动时加载.dtb的代码片段
void __init setup_machine_fdt(phys_addr_t dt_phys)
{
    void *dt_virt = fixmap_remap_fdt(dt_phys);
    struct boot_param_header *fdt = dt_virt;

    if (fdt_check_header(fdt)) {
        pr_err("Invalid device tree blob\n");
        return;
    }

    initial_boot_params = fdt;
    machine_name = of_get_flat_dt_prop("/chosen", "bootargs", NULL);
}
代码解析:
  • fixmap_remap_fdt() :将.dtb物理地址映射为虚拟地址。
  • fdt_check_header() :检查设备树头部是否合法。
  • of_get_flat_dt_prop() :从设备树中读取指定节点的属性值,如 bootargs

4.3 设备树文件的版本与兼容性问题

设备树编译器(DTC)随着Linux内核的发展不断演进,不同版本之间可能存在语法差异和功能变化,因此开发者在维护设备树时需关注版本兼容性问题。

4.3.1 不同版本设备树编译器的兼容性分析

DTC工具支持多个设备树格式版本,常见的版本如下:

DTC版本 支持的设备树版本 特性变化
v1.4.x v17 支持宏定义、include机制
v1.5.x v17 增强错误提示
v1.6.x v17 支持更多编译选项
v1.7.x v17 性能优化、语法增强
常见兼容性问题:
  • 属性格式变化 :某些版本中属性格式要求更严格,如数组必须用空格分隔。
  • 宏定义支持 :早期版本不支持宏定义,需使用 /include/ 替代。
  • 编译器警告 :新版DTC会提示更多语法警告,提升代码质量。

4.3.2 升级设备树版本时的注意事项

升级设备树版本时,开发者应遵循以下步骤:

  1. 确认DTC版本 :使用 dtc --version 查看当前版本。
  2. 更新.dts文件格式 :确保语法符合新版DTC要求。
  3. 验证编译结果 :使用 dtc -I dts -O dtb -o output.dtb input.dts 进行编译测试。
  4. 使用dtc进行反编译检查 :反编译生成的.dtb文件验证是否结构正确。
示例:升级设备树版本后验证流程
# 编译设备树
dtc -I dts -O dtb -o custom_board.dtb custom_board.dts

# 反编译验证
dtc -I dtb -O dts -o custom_board_verified.dts custom_board.dtb

# 对比源文件与反编译文件
diff custom_board.dts custom_board_verified.dts
注意事项总结:
项目 建议
版本一致性 确保SDK、交叉编译链、DTC版本一致
测试验证 使用QEMU或实际硬件测试新.dtb
备份旧版本 升级前备份原有.dts文件以便回退
依赖关系 检查.dtsi文件是否被其他平台引用

本章深入解析了.dts源文件的语法结构、.dts到.dtb的编译机制,以及设备树版本兼容性问题。通过理解设备树的组成与编译过程,开发者可以更高效地编写、调试和维护设备树文件,为后续的设备驱动开发和平台适配打下坚实基础。

5. 使用dtc工具编译DeviceTree文件

在嵌入式Linux开发中,设备树(Device Tree)作为硬件描述的重要载体,其源文件(.dts)需要通过工具链编译为内核可识别的二进制格式(.dtb)。 dtc (Device Tree Compiler)是Linux社区提供的一款核心工具,它不仅支持设备树的编译与反编译,还提供了丰富的调试与验证功能。本章将详细介绍dtc工具的使用方法,包括其安装流程、基本命令参数、设备树的编译与反编译操作,以及如何利用dtc进行语法检查与验证。

5.1 dtc工具的基本功能与安装方法

5.1.1 在Linux系统中安装dtc工具链

dtc(Device Tree Compiler)通常作为Linux内核源码树的一部分提供,也可以通过系统包管理器进行安装。在大多数Linux发行版中,可以通过以下命令安装dtc工具:

# 在Ubuntu/Debian系统中
sudo apt-get install device-tree-compiler

# 在Fedora系统中
sudo dnf install dtc

# 在Arch Linux系统中
sudo pacman -S dtc

安装完成后,可以使用以下命令验证是否安装成功:

dtc --version

输出结果类似于:

Version: DTC 1.4.7

如果你希望使用最新的dtc版本,或者需要定制编译,可以从源码编译安装:

git clone https://git.kernel.org/pub/scm/utils/dtc/dtc.git
cd dtc
make
sudo make install

注意 :从源码编译时,确保系统已安装 flex bison 等编译工具。

5.1.2 dtc命令行参数详解与使用示例

dtc支持多种编译选项,以下是一些常用的命令参数及其作用:

参数 说明
-I 指定输入格式(dts、dtb、fs)
-O 指定输出格式(dts、dtb)
-o 指定输出文件路径
-@ 启用标签(label)支持
-f 强制覆盖输出文件
-V 设置输出版本(例如:17)
-d 生成依赖文件
-q 静默模式,不输出日志信息
示例1:将.dts文件编译为.dtb
dtc -I dts -O dtb -o my_board.dtb my_board.dts

该命令将 my_board.dts 编译为 my_board.dtb ,适用于内核加载。

示例2:反编译.dtb为.dts
dtc -I dtb -O dts -o my_board_recovered.dts my_board.dtb

该命令用于将内核使用的二进制设备树还原为可读的源码格式,便于调试与修改。

示例3:启用标签支持并输出为特定版本
dtc -I dts -O dtb -@ -V 17 -o my_board_v17.dtb my_board.dts

该命令启用了标签支持,并指定输出为版本17的设备树格式。

小贴士 :在编译过程中,dtc会自动检查语法错误,输出错误信息,便于开发者快速定位问题。

5.2 设备树的编译与反编译操作

5.2.1 将.dts文件编译为.dtb文件

设备树源文件(.dts)是一种结构化的文本文件,需要通过dtc工具将其编译为扁平化设备树(Flattened Device Tree,简称FDT),即 .dtb 格式。这种格式是内核在启动阶段加载并解析的二进制数据。

编译流程示意图(mermaid流程图):
graph TD
    A[.dts源文件] --> B{dtc编译器}
    B --> C[语法检查]
    C --> D[节点与属性解析]
    D --> E[生成.dtb文件]
    E --> F[内核加载使用]
示例:编译设备树
dtc -I dts -O dtb -o imx6q-sabresd.dtb imx6q-sabresd.dts

执行完成后,将生成 imx6q-sabresd.dtb 文件,供U-Boot或Linux内核加载。

代码逻辑分析:
  • dtc :调用设备树编译器。
  • -I dts :指定输入文件为 .dts 格式。
  • -O dtb :指定输出文件为 .dtb 格式。
  • -o :指定输出文件路径。
  • imx6q-sabresd.dts :设备树源文件。

执行过程中,dtc将解析设备树节点结构、属性值,并将其转换为紧凑的二进制格式,供内核解析。

5.2.2 反编译.dtb文件为可读的.dts格式

反编译是设备树调试中的重要手段,可以帮助开发者查看运行时使用的设备树内容,或对比不同版本的设备树差异。

反编译流程图(mermaid):
graph TD
    A[.dtb文件] --> B{dtc反编译}
    B --> C[解析二进制结构]
    C --> D[生成.dts文本文件]
    D --> E[人工或工具分析]
示例:反编译设备树
dtc -I dtb -O dts -o imx6q-sabresd_recovered.dts imx6q-sabresd.dtb

该命令将 imx6q-sabresd.dtb 转换为 imx6q-sabresd_recovered.dts ,可用于查看设备树结构或进行修改。

代码逻辑分析:
  • -I dtb :指定输入为 .dtb 格式。
  • -O dts :输出为 .dts 格式。
  • -o :输出文件路径。
  • imx6q-sabresd.dtb :要反编译的二进制设备树文件。

执行后,生成的 .dts 文件将包含设备树节点、属性以及地址映射信息,便于开发者理解当前设备树配置。

5.3 设备树调试与验证技巧

5.3.1 使用dtc进行语法检查与错误定位

在编写设备树源文件时,语法错误是常见问题。dtc可以在编译前进行语法检查,帮助开发者快速定位错误。

示例:仅进行语法检查
dtc -I dts -O none my_board.dts
  • -O none :表示不输出任何文件,仅进行检查。
  • 若文件中存在语法错误,dtc会输出类似以下信息:
Error: my_board.dts:10.5-6 syntax error
FATAL ERROR: Unable to parse input tree
常见错误类型:
错误类型 示例 说明
节点名重复 cpu@0 { ... }; cpu@0 { ... }; 同一父节点下不能有两个同名节点
属性值缺失 reg; 属性值不能为空,必须赋值
地址格式错误 reg = <0x100>; 应使用 reg = <0x100 0x1000>; 表示地址和长度
缺少分号 clocks = <&clks 123> 每行必须以分号结束
调试技巧:
  • 使用 -q 参数静默执行,避免冗余输出。
  • 使用 -W 参数控制警告级别,例如 -Wno-unit_address_vs_reg 可忽略地址与reg字段不一致的警告。

5.3.2 通过QEMU验证设备树的有效性

QEMU是一款强大的虚拟化工具,可以用于模拟嵌入式设备环境,并加载设备树文件进行验证。

使用QEMU加载设备树:
qemu-system-arm -M vexpress-a9 \
                -nographic \
                -kernel zImage \
                -dtb my_board.dtb \
                -append "console=ttyAMA0"
  • -M vexpress-a9 :指定目标平台为ARM Versatile Express A9。
  • -dtb my_board.dtb :加载设备树文件。
  • -kernel zImage :指定内核镜像。
  • -append :传递内核启动参数。
QEMU验证流程图(mermaid):
graph TD
    A[QEMU启动] --> B[加载内核zImage]
    B --> C[加载.dtb设备树]
    C --> D[内核解析设备树]
    D --> E[初始化硬件设备]
    E --> F[启动Linux系统]
验证内容:
  • 内核是否能正确解析设备树。
  • 设备是否正常初始化(如串口、内存、中断控制器)。
  • 是否出现设备树节点缺失或配置错误导致的初始化失败。
日志分析技巧:
  • 使用 -nographic 参数避免图形界面,直接输出控制台日志。
  • 查看内核启动日志中是否有以下关键字:
  • OF: fdt_unflatten
  • No suitable driver found
  • Failed to map registers
调试建议:
  • 如果设备树加载失败,可使用dtc检查 .dtb 文件是否完整。
  • 使用 dtc -I dtb -O dts 反编译设备树,检查节点是否完整。
  • 修改设备树后重新编译加载,验证修复效果。

5.3.3 高级调试技巧与工具链集成

在实际开发中,设备树往往需要与U-Boot、Linux内核、交叉编译链等协同工作。以下是一些高级调试与集成技巧:

1. 使用 make dtbs 编译多个设备树

在Linux内核源码中,可以使用以下命令编译所有设备树:

make dtbs

这将编译 arch/arm64/boot/dts/ 目录下的所有设备树文件,生成对应的 .dtb 文件。

2. 与U-Boot集成

U-Boot也支持加载设备树,可以通过以下命令加载设备树:

load mmc 0:1 ${fdt_addr_r} my_board.dtb
bootz ${loadaddr} - ${fdt_addr_r}

3. 使用 dtc 生成设备树依赖

dtc -d my_board.dts

该命令将生成 my_board.dts.d 文件,记录设备树依赖关系,便于Makefile集成。

通过dtc工具的使用,开发者可以高效地编译、调试和验证设备树文件,确保设备树结构正确、配置完整,从而提升嵌入式系统的稳定性和可维护性。在后续章节中,我们将深入探讨设备树节点与属性的定义方式,进一步掌握设备树的编写技巧。

6. DeviceTree节点与属性结构详解

在Linux设备树中,节点(Node)和属性(Property)构成了整个设备树结构的核心。设备树通过树状结构描述系统的硬件信息,使得硬件信息与驱动代码解耦,提升了系统可移植性和可维护性。本章将深入剖析设备树的节点组织结构、常用属性的使用方式,以及如何在设备树中表达总线与设备之间的层级关系。

6.1 设备树节点的组织结构

设备树的结构本质上是一棵由节点构成的树,其中根节点是 / ,所有其他节点都嵌套在根节点之下,形成多层嵌套的结构。

6.1.1 根节点与子节点的层级关系

设备树的根节点是整个结构的起点,通常在 .dts 文件中以 / { ... } 的形式定义。根节点下可以包含多个一级子节点,每个子节点又可以包含自己的子节点。例如:

/ {
    cpus {
        cpu@0 {
            device_type = "cpu";
            compatible = "arm,cortex-a53";
        };
    };

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

在上述示例中:

  • / 是根节点;
  • cpus 是根节点的一级子节点;
  • cpu@0 cpus 节点的子节点;
  • memory@80000000 是另一个一级子节点。

这种层级结构清晰地表达了系统中各个硬件组件的组织关系。

6.1.2 设备树中设备节点的命名规范

设备树节点的命名遵循一定的规范,通常采用以下格式:

<device-type>[@<unit-address>]

其中:

  • device-type 表示设备类型,例如 cpu memory i2c spi 等;
  • unit-address 是该设备在系统中的物理地址或实例编号,用于唯一标识设备。

例如:

i2c@1a40000 {
    compatible = "fsl,imx6q-i2c";
    reg = <0x1a40000 0x2000>;
    ...
};

这里的 i2c@1a40000 表示一个位于地址 0x1a40000 的 I2C 控制器节点。

6.2 常用属性及其作用

设备树节点通过属性(Property)来描述具体的硬件信息。每个属性是一个键值对,包含属性名和属性值。以下是设备树中最常用且最重要的几个属性:

6.2.1 reg、interrupts、clocks等关键属性详解

属性名称 描述说明
reg 表示设备的地址范围,通常用于内存映射寄存器。格式为 <起始地址 长度>
interrupts 表示设备使用的中断号及触发类型。格式为 <中断号 触发类型>
clocks 表示设备使用的时钟源,通常指向其他节点
compatible 表示设备的兼容性字符串,用于匹配驱动程序
status 表示设备是否启用,常见值为 "okay" "disabled"
reg属性详解

reg 属性用于描述设备的地址空间。例如:

uart@1a00000 {
    compatible = "fsl,imx6q-uart";
    reg = <0x1a00000 0x4000>;
};
  • 0x1a00000 是 UART 控制器的起始地址;
  • 0x4000 表示该控制器占据的地址空间大小(即16KB)。
interrupts属性详解

中断是设备与CPU通信的重要方式。例如:

gpio@209c000 {
    interrupts = <0x5a 0x4>;
};
  • 0x5a 表示中断号;
  • 0x4 表示中断触发类型(0x4表示上升沿触发)。
clocks属性详解

设备通常依赖于多个时钟信号。例如:

spi@1a40000 {
    clocks = <&clks IMX6Q_CLK_ECSPI2>;
};
  • &clks 是一个 phandle,指向时钟控制器节点;
  • IMX6Q_CLK_ECSPI2 是时钟的编号,用于指定具体的时钟源。

6.2.2 地址映射与中断配置在设备树中的表示方式

地址映射通过 reg 属性完成,而中断配置则通过 interrupts 属性实现。例如,在一个 SPI 控制器节点中,可能同时包含地址映射和中断配置:

spi@1a40000 {
    compatible = "fsl,imx6q-ecspi";
    reg = <0x1a40000 0x2000>;
    interrupts = <0x4b 0x4>;
    clocks = <&clks IMX6Q_CLK_ECSPI2>;
};

在这个例子中:

  • reg 表示SPI控制器的寄存器地址范围;
  • interrupts 表示该控制器使用的中断号和触发方式;
  • clocks 表示该控制器依赖的时钟源。

这些信息被内核解析后,驱动程序可以根据这些信息正确初始化设备。

6.3 设备树中总线与设备的层次结构表示

设备树中通过节点的嵌套结构表示总线与设备之间的关系。总线节点下可以包含多个设备节点,每个设备节点代表一个具体的硬件设备。

6.3.1 I2C、SPI、PCI等总线在设备树中的描述方法

I2C总线描述示例:
i2c@1a40000 {
    compatible = "fsl,imx6q-i2c";
    reg = <0x1a40000 0x2000>;
    interrupts = <0x4b 0x4>;

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

在这个例子中:

  • i2c@1a40000 是一个I2C总线控制器;
  • eeprom@50 是连接在该I2C总线上的设备,其地址为 0x50
SPI总线描述示例:
spi@1a00000 {
    compatible = "fsl,imx6q-spi";
    reg = <0x1a00000 0x4000>;
    interrupts = <0x4d 0x4>;

    flash@0 {
        compatible = "jedec,spi-nor";
        reg = <0>;
        spi-max-frequency = <20000000>;
    };
};

在这个SPI控制器节点下:

  • flash@0 是一个SPI NOR Flash设备;
  • reg = <0> 表示其片选号为0;
  • spi-max-frequency 表示该设备支持的最大SPI时钟频率。

6.3.2 子设备如何嵌套在父设备节点中表示

在设备树中,子设备作为父节点的子节点嵌套表示。例如:

usb@1a00000 {
    compatible = "fsl,imx6q-usb";
    reg = <0x1a00000 0x200>;
    interrupts = <0x1a 0x4>;

    hub@1 {
        compatible = "ti,tusb8041-hub";
        reg = <0x1>;
    };
};

在这个例子中:

  • usb@1a00000 是USB控制器节点;
  • hub@1 是该控制器下的一个子设备节点,表示一个USB集线器。

这种方式使得设备树结构清晰,便于内核在初始化过程中递归解析整个设备树,并正确加载相应的驱动程序。

(本章内容完)

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:DeviceTree是一种用于描述嵌入式系统硬件结构的数据结构,广泛应用于Linux内核启动过程中,帮助操作系统动态识别和初始化硬件资源。它通过.dts源文件定义硬件信息,并编译为.dtb文件供内核解析。DeviceTree采用节点与属性的方式描述硬件组件及其连接关系,支持驱动自动匹配,极大提升了系统的可移植性与灵活性。本文介绍了DeviceTree的基本概念、结构组成、编译流程及其在Windows 7环境下通过虚拟化工具进行开发的方法,为深入Linux驱动开发打下基础。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐