目录
一、Linux内核终端设备模型中的设备树
设备树(Device Tree)是一种描述硬件设备及其连接关系的数据结构,广泛应用于嵌入式系统中,特别是在Linux内核中。设备树的核心思想是将硬件描述与操作系统内核分离,使得同一内核可以支持多种硬件平台,从而减少内核的冗余代码,提高系统的可移植性和可维护性。
二、设备树的基本结构
设备树(Device Tree)是嵌入式系统中用于描述硬件配置的一种数据结构,通常以.dts(Device Tree Source)文件的形式存在。这种文件采用文本格式,详细描述了硬件设备的属性、地址、中断信息以及设备之间的连接关系。.dts文件通过设备树编译器(DTC,Device Tree Compiler)编译为二进制格式的.dtb(Device Tree Blob)文件,供内核在启动时加载和解析。设备树的主要目的是将硬件描述与操作系统内核解耦,使得同一内核可以支持多种硬件平台,而无需修改内核代码。设备树的基本结构包括以下几个部分:
1. 节点(Node)
在设备树(Device Tree)中,节点(Node)是描述硬件设备或功能模块的基本单元。每个节点代表一个具体的硬件设备或逻辑功能模块,并通过树状结构组织起来,形成完整的硬件描述。节点之间通过父子关系连接,父节点可以包含多个子节点,从而构建出层次化的硬件描述结构。每个节点都有一个唯一的路径标识符,用于在设备树中精确定位该节点。例如,路径标识符 /soc/i2c@4000
表示位于系统级芯片(SoC)上的一个 I2C 控制器,其基地址为 0x4000
。路径标识符的格式通常遵循以下规则:
-
/
表示根节点。 -
soc
是根节点下的一个子节点,通常表示系统级芯片(SoC)部分。 -
i2c@4000
是soc
节点下的一个子节点,表示一个 I2C 控制器,@4000
表示该控制器的基地址为0x4000
。
节点可以表示多种类型的硬件设备或功能模块,包括但不限于:
-
物理设备:
-
CPU:描述处理器的类型、核心数量、频率等信息。
-
内存控制器:描述内存的布局、大小、类型等。
-
外设:如 UART、SPI、I2C、GPIO 等接口控制器。
-
-
逻辑功能模块:
-
时钟:描述系统中各个时钟源的频率、分频器等。
-
中断控制器:描述中断的分配、优先级、触发方式等。
-
每个节点通常包含一组属性(Properties),用于描述该节点的具体配置和功能。例如,一个 I2C 控制器节点可能包含以下属性:
-
compatible
:指定设备的兼容性,用于匹配驱动程序。 -
reg
:指定设备的寄存器地址范围。 -
clock-frequency
:指定 I2C 总线的时钟频率。
以下是一个设备树节点的示例:
i2c@4000 {
compatible = "vendor,i2c-controller";
reg = <0x4000 0x100>;
clock-frequency = <100000>;
interrupts = <10>;
#address-cells = <1>;
#size-cells = <0>;
};
-
compatible
属性表示该节点与vendor,i2c-controller
驱动程序兼容。 -
reg
属性指定了 I2C 控制器的寄存器地址范围为0x4000
到0x40FF
。 -
clock-frequency
属性指定了 I2C 总线的时钟频率为 100 kHz。 -
interrupts
属性指定了该设备使用的中断号为 10。 -
#address-cells
和#size-cells
属性用于描述子节点的地址和大小格式。
通过这种结构化的描述方式,设备树能够清晰地表达硬件系统的组成和配置,为操作系统和驱动程序提供准确的硬件信息。
2. 属性(Property)
在设备树(Device Tree)中,属性(Property)是描述设备特性的关键元素。每个节点可以包含多个属性,这些属性以键值对的形式存在,用于定义设备的硬件配置和功能。以下是一些常见属性的详细说明及其应用场景:
-
reg:
reg
属性用于描述设备的寄存器地址范围。它通常由两个值组成:基地址和大小。例如,reg = <0x4000 0x100>
表示设备的寄存器基地址为0x4000
,寄存器区域的大小为0x100
。这个属性在硬件驱动中非常重要,因为它告诉操作系统如何访问设备的寄存器。
示例:
在描述一个 UART 设备时,reg = <0x1000 0x100>
表示 UART 的寄存器从地址0x1000
开始,占用0x100
字节的空间。 -
interrupts:
interrupts
属性用于描述设备的中断号。它通常包括中断号和中断触发类型(如边沿触发或电平触发)。例如,interrupts = <0 1>
表示设备使用中断号0
,触发类型为1
(通常表示边沿触发)。
示例:
在描述一个 GPIO 设备时,interrupts = <5 2>
表示 GPIO 使用中断号5
,触发类型为2
(通常表示电平触发)。 -
clocks:
clocks
属性用于描述设备的时钟源。它通常引用设备树中的时钟节点,并指定时钟的频率或时钟标识符。例如,clocks = <&clk 1>
表示设备使用时钟节点clk
的第1
个时钟源。
示例:
在描述一个 SPI 设备时,clocks = <&spi_clk 0>
表示 SPI 设备使用spi_clk
节点的第0
个时钟源。 -
status:
status
属性用于描述设备的状态,常见的值包括okay
和disabled
。okay
表示设备处于启用状态,disabled
表示设备被禁用。这个属性在设备树中用于控制设备的初始化行为。
示例:
在描述一个 I2C 设备时,status = "disabled"
表示该 I2C 设备在系统启动时不会被初始化。
除了上述常见属性,设备树中还有许多其他属性,如 compatible
(用于匹配设备驱动)、pinctrl
(用于描述引脚控制配置)、dma
(用于描述 DMA 通道)等。这些属性共同构成了设备树的完整描述,帮助操作系统正确识别和配置硬件设备。
应用场景
在嵌入式系统中,设备树被广泛用于描述硬件平台的结构。例如,在 Linux 内核中,设备树文件(.dts
或 .dtsi
)被编译成二进制格式(.dtb
),并在系统启动时由引导加载程序传递给内核。内核通过解析设备树中的属性,自动配置硬件设备并加载相应的驱动程序,从而简化了硬件平台的移植和开发工作。
3. 兼容性(Compatible)
compatible
属性是设备树(Device Tree)中最为关键和核心的属性之一,它在设备与驱动程序的匹配过程中扮演着至关重要的角色。设备树是一种用于描述硬件设备及其配置的数据结构,广泛应用于嵌入式系统中,特别是在Linux内核中。compatible
属性的主要作用是指定设备的驱动程序,内核通过解析该属性来找到与设备匹配的驱动程序。
compatible
属性通常由一个或多个字符串组成,这些字符串按照特定的格式排列,格式通常为"vendor,device"
,其中vendor
表示设备的制造商或供应商,device
表示设备的型号或具体类型。例如,compatible = "ti,omap3-i2c"
表示该设备由德州仪器(TI)制造,型号为omap3-i2c
。内核会根据compatible
属性中的字符串顺序,依次尝试匹配相应的驱动程序。如果第一个字符串没有找到匹配的驱动程序,内核会继续尝试匹配后续的字符串,直到找到合适的驱动程序或所有字符串都尝试完毕。
举例来说,假设设备树中定义了如下compatible
属性:
compatible = "ti,omap3-i2c", "ti,omap-i2c";
这表示该设备首先尝试匹配ti,omap3-i2c
驱动程序。如果内核中没有找到与该字符串匹配的驱动程序,则会继续尝试匹配ti,omap-i2c
驱动程序。这种机制允许设备树在保持向后兼容性的同时,支持多种不同的硬件版本或变体。
在实际应用中,compatible
属性的设计通常遵循以下原则:
-
唯一性:每个设备的
compatible
属性应尽可能唯一,以确保内核能够准确匹配到正确的驱动程序。 -
兼容性:如果设备与多个驱动程序兼容,可以通过添加多个字符串来支持不同的驱动程序版本或变体。
-
标准化:
vendor
和device
的命名应遵循行业标准或厂商的命名规范,以确保一致性和可维护性。
例如,在嵌入式系统中,一个I2C控制器可能支持多种不同的硬件版本,设备树中可能会这样定义:
compatible = "ti,omap4-i2c", "ti,omap3-i2c", "ti,omap-i2c";
这种定义方式确保了即使内核中没有最新的ti,omap4-i2c
驱动程序,设备仍然可以使用较旧的ti,omap3-i2c
或ti,omap-i2c
驱动程序,从而提高了系统的兼容性和稳定性。
总之,compatible
属性是设备树中不可或缺的一部分,它通过提供设备与驱动程序之间的匹配机制,确保了硬件设备能够在内核中正确初始化并运行。
三、设备树的应用场景
设备树(Device Tree)在嵌入式系统中具有广泛的应用,特别是在以下场景中:
1. 多平台支持
通过设备树,Linux内核可以支持多种硬件平台,而无需为每个平台编写特定的内核代码。设备树文件(通常以 .dts
或 .dtb
为扩展名)描述了硬件的具体配置,包括处理器、内存、外设等。例如,ARM架构的Linux内核可以通过加载不同的设备树文件来支持不同的开发板。以常见的ARM开发板为例,树莓派(Raspberry Pi)和BeagleBone Black虽然都基于ARM架构,但它们的硬件配置不同。通过为每个开发板提供特定的设备树文件,Linux内核可以在不修改内核代码的情况下,正确识别和配置硬件资源。这种机制极大地简化了内核的维护和移植工作。
2. 硬件抽象
设备树将硬件描述与内核代码分离,使得硬件的变化不会影响内核的稳定性。设备树文件作为硬件配置的描述文件,独立于内核代码,因此当硬件设备更换时,只需修改设备树文件,而无需重新编译内核。例如,在一个嵌入式系统中,如果某个I2C设备(如传感器)被替换为另一个兼容的型号,只需在设备树文件中更新该设备的描述,而无需修改内核驱动代码。这种分离机制不仅提高了系统的可维护性,还降低了因硬件变更导致的内核不稳定性风险。
3. 动态配置
设备树可以在系统启动时动态加载,使得同一内核可以适应不同的硬件配置。这种特性在嵌入式系统中尤为重要,因为许多嵌入式设备需要根据不同的硬件模块或应用场景进行灵活配置。例如,某些嵌入式系统可以根据不同的硬件模块加载不同的设备树文件。以工业控制设备为例,同一款设备可能根据客户需求配置不同的通信模块(如以太网、CAN总线或RS485)。通过为每种配置提供独立的设备树文件,系统可以在启动时根据实际硬件加载相应的设备树,从而实现动态配置。此外,设备树还可以通过U-Boot等引导加载程序在启动时动态修改,进一步增强了系统的灵活性。
4. 其他应用场景
除了上述场景,设备树还在以下方面发挥着重要作用:
- 电源管理:设备树可以描述设备的电源管理信息,如休眠模式、唤醒源等,帮助内核实现高效的电源管理。
- 中断管理:设备树可以描述设备的中断配置,包括中断号、触发方式等,确保内核能够正确处理硬件中断。
- 设备驱动匹配:设备树中的设备节点可以与内核驱动进行匹配,确保正确的驱动被加载并初始化。
总之,设备树作为一种硬件描述机制,在嵌入式系统中提供了强大的灵活性和可维护性,使得内核能够更好地适应多样化的硬件环境。
四、设备树在Linux内核中的工作流程
在Linux内核启动时,设备树(Device Tree)的工作流程是一个关键的系统初始化过程,它确保了内核能够正确识别和配置硬件设备。以下是设备树工作流程的详细步骤:
1. 加载设备树
- Bootloader的作用:在系统启动的早期阶段,Bootloader(如U-Boot)负责将设备树二进制文件(.dtb)从存储介质(如NAND Flash、eMMC或SD卡)加载到内存中。这个文件包含了硬件平台的详细描述信息。
- 传递设备树地址:Bootloader在加载设备树文件后,会将设备树在内存中的地址通过特定的寄存器(如ARM架构中的
r2
寄存器)传递给内核。这个地址是内核启动时解析设备树的起点。
2. 解析设备树
- 内核启动阶段:在内核启动的早期阶段,内核会从Bootloader传递的地址开始解析设备树文件。设备树文件采用了一种树状结构,描述了系统中所有硬件设备的属性和连接关系。
- 设备树节点:设备树中的每个节点代表一个硬件设备或子系统,节点中包含了设备的属性(如寄存器地址、中断号、时钟频率等)。内核通过遍历设备树节点,获取这些硬件设备的描述信息。
3. 初始化设备
- 硬件设备初始化:内核根据设备树中提供的信息,初始化硬件设备。这包括配置设备的寄存器、分配中断、设置时钟等操作。设备树中的信息为内核提供了硬件配置的蓝图,使得内核能够正确地与硬件进行交互。
- 驱动程序加载:在初始化硬件设备的过程中,内核会加载相应的驱动程序。驱动程序是内核与硬件设备之间的桥梁,负责管理设备的操作和数据传输。
4. 匹配驱动程序
- compatible属性:设备树中的每个设备节点都包含一个
compatible
属性,该属性用于描述设备的兼容性。compatible
属性通常包含一个或多个字符串,表示设备与哪些驱动程序兼容。 - 驱动程序匹配:内核通过
compatible
属性与已注册的驱动程序进行匹配。当内核找到一个与设备节点compatible
属性匹配的驱动程序时,会调用驱动程序的初始化函数,完成设备的注册和初始化。 - 设备注册:在驱动程序初始化完成后,设备会被注册到内核的设备模型中,成为系统中可用的设备。此时,设备已经准备好被应用程序或内核的其他部分使用。
通过以上步骤,Linux内核能够利用设备树文件准确地识别和配置硬件设备,确保系统能够正常启动并运行。设备树的使用使得内核与硬件平台的耦合度降低,提高了系统的可移植性和灵活性。
五、设备树中的终端设备
在Linux内核中,终端设备通常通过串口(UART)实现。设备树中描述串口设备的节点通常包含以下属性:
compatible
:用于匹配驱动程序,通常包含设备的生产商和型号。reg
:设备的寄存器地址范围。interrupts
:设备使用的中断号。
serial@101f0000 {
compatible = "arm,pl011";
reg = <0x101f0000 0x1000>;
interrupts = <0 1 4>;
};
六、设备树的解析与使用
Linux内核在启动过程中,设备树(Device Tree)扮演着至关重要的角色。设备树是一种描述硬件设备及其连接关系的数据结构,通常以.dts
或.dtb
文件的形式存在。内核在启动时会加载并解析设备树,以获取系统中所有硬件设备的详细信息,包括设备类型、寄存器地址、中断号、时钟频率等。
设备树的解析过程始于内核的早期启动阶段。内核首先会读取设备树文件,并将其转换为内部数据结构,即设备树节点(Device Tree Node)。每个节点代表一个硬件设备或子系统,并包含该设备的属性和配置信息。内核通过遍历设备树节点,逐步初始化系统中的硬件设备。
在内核的设备树解析过程中,of_platform_populate
函数是一个关键步骤。该函数负责将设备树中的节点转换为平台设备(Platform Device),并将其注册到内核的设备模型中。具体来说,of_platform_populate
函数会遍历设备树中的节点,查找与平台设备相关的节点(通常以compatible
属性标识),然后为每个节点创建一个对应的platform_device
结构体。这个结构体包含了设备的名称、资源(如内存地址、中断号等)以及其他配置信息。
一旦平台设备被创建,内核会通过platform_device_register
函数将其注册到内核的设备管理子系统中。注册后,内核会触发设备与驱动程序的匹配过程。如果存在与设备匹配的驱动程序,内核会调用驱动程序的probe
函数,完成设备的初始化和配置。
设备树的解析和平台设备的注册过程在内核启动时是自动进行的,无需用户干预。这一机制使得Linux内核能够灵活地支持多种硬件平台,尤其是在嵌入式系统中,设备树的使用大大简化了硬件配置和内核移植的工作。
例如,在一个基于ARM架构的嵌入式系统中,设备树文件可能包含以下内容:
&i2c1 {
status = "okay";
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
};
};
在这个例子中,i2c1
节点描述了一个I2C总线控制器,eeprom@50
节点描述了一个连接到该总线的EEPROM设备。内核在启动时会解析这些节点,并通过of_platform_populate
函数将EEPROM设备注册为平台设备。随后,内核会查找与atmel,24c02
兼容的驱动程序,并调用其probe
函数来初始化EEPROM设备。
通过设备树和of_platform_populate
函数的配合,Linux内核能够高效地管理和初始化系统中的硬件设备,确保系统能够正确启动并运行。
#include <linux/of_platform.h>
static int __init myboard_init(void)
{
of_platform_populate(NULL, of_default_bus_match_table, NULL, NULL);
return 0;
}
arch_initcall(myboard_init);
七、实例:串口设备的驱动
以下是一个简单的串口设备驱动示例,展示了如何通过设备树匹配并初始化串口设备。
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/serial_core.h>
#include <linux/tty.h>
#include <linux/tty_flip.h>
static int my_serial_probe(struct platform_device *pdev)
{
struct uart_port *port;
int ret;
port = devm_kzalloc(&pdev->dev, sizeof(*port), GFP_KERNEL);
if (!port)
return -ENOMEM;
port->line = 0;
port->type = PORT_UNKNOWN;
port->iotype = UPIO_MEM;
port->mapbase = 0x101f0000;
port->irq = 1;
port->uartclk = 1843200;
port->flags = UPF_BOOT_AUTOCONF;
ret = uart_add_one_port(&my_serial_driver, port);
if (ret)
return ret;
platform_set_drvdata(pdev, port);
return 0;
}
static int my_serial_remove(struct platform_device *pdev)
{
struct uart_port *port = platform_get_drvdata(pdev);
uart_remove_one_port(&my_serial_driver, port);
return 0;
}
static const struct of_device_id my_serial_of_match[] = {
{ .compatible = "arm,pl011" },
{},
};
MODULE_DEVICE_TABLE(of, my_serial_of_match);
static struct platform_driver my_serial_driver = {
.probe = my_serial_probe,
.remove = my_serial_remove,
.driver = {
.name = "my_serial",
.of_match_table = my_serial_of_match,
},
};
module_platform_driver(my_serial_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Simple Serial Driver");
八、总结
设备树在Linux内核中扮演着重要角色,特别是在嵌入式系统中。通过设备树,内核可以动态地识别和初始化硬件设备,而无需修改内核代码。本文介绍了设备树的基本结构、终端设备的描述方法,以及如何通过设备树匹配并初始化串口设备。通过实例代码,展示了设备树在实际驱动开发中的应用。