DeviceTree详解与嵌入式开发实战
dts文件由一个根节点开始,其余节点嵌套在其中,形成树状结构。每个节点可以包含多个属性,也可以嵌套子节点。节点和属性的书写遵循特定的语法规范。为了提高设备树的可维护性和复用性,.dts文件中可以包含头文件(.dtsi),并通过宏定义简化重复代码。项目建议版本一致性确保SDK、交叉编译链、DTC版本一致测试验证使用QEMU或实际硬件测试新.dtb备份旧版本升级前备份原有.dts文件以便回退依赖关系检
简介:DeviceTree是一种用于描述嵌入式系统硬件结构的数据结构,广泛应用于Linux内核启动过程中,帮助操作系统动态识别和初始化硬件资源。它通过.dts源文件定义硬件信息,并编译为.dtb文件供内核解析。DeviceTree采用节点与属性的方式描述硬件组件及其连接关系,支持驱动自动匹配,极大提升了系统的可移植性与灵活性。本文介绍了DeviceTree的基本概念、结构组成、编译流程及其在Windows 7环境下通过虚拟化工具进行开发的方法,为深入Linux驱动开发打下基础。 
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传递的设备树文件名加载对应的设备树。
多平台编译步骤如下:
- 配置内核支持多个设备树:
make menuconfig
# 选择 Processor type and features --->
# [*] Multi-platform support
# [*] Select an ARM system type (ARM generic DT based system) --->
- 编译多个设备树文件:
make dtbs
这将生成多个 .dtb 文件,分别对应不同的平台。
- 将多个
.dtb文件打包进内核镜像:
make Image dtbs
- 在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[设备树解析完成]
解析过程关键步骤:
- 设备树地址获取 :Bootloader 会将设备树的物理地址传递给内核,通常通过寄存器或特定内存地址。
- 调用
unflatten_device_tree():该函数负责将扁平设备树(Flattened Device Tree)转换为结构化的设备树节点树。 - 初始化 OF 接口 :注册
of_platform_populate()等函数,为后续设备驱动注册做准备。 - 设备树节点缓存 :将解析后的设备树节点缓存为
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", ®_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,
};
注册流程分析:
- I2C 控制器驱动匹配成功 :
i2c@7000c000节点匹配成功,驱动初始化 I2C 主控制器。 - 子设备注册 :控制器驱动调用
i2c_add_numbered_adapter()后,内核会自动遍历子节点,并注册i2c_client。 - 驱动匹配 :
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 升级设备树版本时的注意事项
升级设备树版本时,开发者应遵循以下步骤:
- 确认DTC版本 :使用
dtc --version查看当前版本。 - 更新.dts文件格式 :确保语法符合新版DTC要求。
- 验证编译结果 :使用
dtc -I dts -O dtb -o output.dtb input.dts进行编译测试。 - 使用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_unflattenNo suitable driver foundFailed 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集线器。
这种方式使得设备树结构清晰,便于内核在初始化过程中递归解析整个设备树,并正确加载相应的驱动程序。
(本章内容完)
简介:DeviceTree是一种用于描述嵌入式系统硬件结构的数据结构,广泛应用于Linux内核启动过程中,帮助操作系统动态识别和初始化硬件资源。它通过.dts源文件定义硬件信息,并编译为.dtb文件供内核解析。DeviceTree采用节点与属性的方式描述硬件组件及其连接关系,支持驱动自动匹配,极大提升了系统的可移植性与灵活性。本文介绍了DeviceTree的基本概念、结构组成、编译流程及其在Windows 7环境下通过虚拟化工具进行开发的方法,为深入Linux驱动开发打下基础。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐


所有评论(0)