Linux 和设备树¶
Linux 设备树数据的使用模型
- 作者:
Grant Likely <grant.likely@secretlab.ca>
本文描述了 Linux 如何使用设备树。有关设备树数据格式的概述,请访问 devicetree.org 上的设备树用法页面[1]。
“开放固件设备树”,或简称设备树 (DT),是一种用于描述硬件的数据结构和语言。更具体地说,它是硬件的描述,操作系统可以读取该描述,这样操作系统就不需要硬编码机器的细节。
在结构上,DT 是一个树,或者说是一个非循环图,其中包含已命名的节点,节点可以有任意数量的已命名属性,封装任意数据。还存在一种机制,可以从一个节点到另一个节点创建自然的树结构之外的任意链接。
从概念上讲,为数据如何在树中出现定义了一组通用的使用约定,称为“绑定”,用于描述典型的硬件特征,包括数据总线、中断线、GPIO 连接和外围设备。
尽可能使用现有绑定来描述硬件,以最大限度地利用现有支持代码,但由于属性和节点名称只是文本字符串,因此可以通过定义新的节点和属性来轻松扩展现有绑定或创建新的绑定。但是,在创建新绑定之前,请务必先做一些功课,了解已存在的内容。目前,i2c 总线有两种不同的、不兼容的绑定,出现的原因是没有事先调查现有系统中如何枚举 i2c 设备就创建了新绑定。
1. 历史¶
DT 最初由开放固件创建,作为将数据从开放固件传递到客户端程序(如操作系统)的通信方法的一部分。操作系统使用设备树在运行时发现硬件的拓扑结构,从而在无需硬编码信息的情况下支持大多数可用硬件(假设所有设备都有驱动程序)。
由于开放固件通常用于 PowerPC 和 SPARC 平台,因此 Linux 对这些架构的支持长期以来一直使用设备树。
2005 年,当 PowerPC Linux 开始进行重大清理并合并 32 位和 64 位支持时,决定要求所有 powerpc 平台都支持 DT,无论它们是否使用开放固件。为此,创建了一种名为扁平化设备树 (FDT) 的 DT 表示形式,它可以作为二进制 Blob 传递给内核,而无需真正的开放固件实现。U-Boot、kexec 和其他引导加载程序经过修改,以支持传递设备树二进制文件 (dtb) 和在引导时修改 dtb。DT 也被添加到 PowerPC 引导包装器 (arch/powerpc/boot/*
) 中,以便可以将 dtb 与内核镜像一起包装起来,以支持引导现有的非 DT 感知固件。
稍后,FDT 基础设施被推广到可供所有架构使用。在撰写本文时,6 个主线架构(arm、microblaze、mips、powerpc、sparc 和 x86)和 1 个非主线架构 (nios) 具有某种程度的 DT 支持。
2. 数据模型¶
如果您还没有阅读设备树用法[1]页面,请立即阅读。没关系,我会等的……
2.1 高级视图¶
最重要的是要理解,DT 只是一个描述硬件的数据结构。它没有什么神奇之处,也不能神奇地解决所有硬件配置问题。它所做的是提供一种语言,将硬件配置与 Linux 内核(或任何其他操作系统)中的主板和设备驱动程序支持分离。使用它允许主板和设备支持成为数据驱动的;基于传递到内核的数据而不是基于每个机器硬编码的选择来做出设置决策。
理想情况下,数据驱动的平台设置应减少代码重复,并使使用单个内核映像支持各种硬件变得更容易。
Linux 使用 DT 数据主要用于三个目的
平台识别,
运行时配置,以及
设备填充。
2.2 平台识别¶
首先,内核将使用 DT 中的数据来识别特定的机器。在理想的世界中,特定的平台对内核来说无关紧要,因为所有平台细节都将通过设备树以一致且可靠的方式完美地描述。但是,硬件并不完美,因此内核必须在早期引导期间识别机器,以便有机会运行特定于机器的修复程序。
在大多数情况下,机器标识是无关紧要的,内核将改为基于机器的核心 CPU 或 SoC 选择设置代码。例如,在 ARM 上,arch/arm/kernel/setup.c 中的 setup_arch() 将调用 arch/arm/kernel/devtree.c 中的 setup_machine_fdt(),它会搜索 machine_desc 表并选择与设备树数据最匹配的 machine_desc。它通过查看根设备树节点中的 “compatible” 属性,并将其与 struct machine_desc 中的 dt_compat 列表(如果您好奇,它在 arch/arm/include/asm/mach/arch.h 中定义)进行比较来确定最佳匹配。
“compatible” 属性包含一个已排序的字符串列表,以机器的确切名称开头,后跟一个可选的与之兼容的板列表,从最兼容到最不兼容排序。例如,TI BeagleBoard 及其后续产品 BeagleBoard xM 板的根 compatible 属性可能如下所示,分别为
compatible = "ti,omap3-beagleboard", "ti,omap3450", "ti,omap3";
compatible = "ti,omap3-beagleboard-xm", "ti,omap3450", "ti,omap3";
其中 “ti,omap3-beagleboard-xm” 指定了确切的型号,它还声明它与 OMAP 3450 SoC 以及一般的 omap3 系列 SoC 兼容。您会注意到,该列表从最具体(确切的板)到最不具体(SoC 系列)排序。
精明的读者可能会指出,Beagle xM 也可以声明与原始 Beagle 板兼容。但是,应该注意在板级别这样做,因为从一个板到另一个板通常会有很高程度的更改,即使在同一产品线中也是如此,并且很难准确地确定当一个板声明与另一个板兼容时意味着什么。对于顶层,最好谨慎行事,不要声明一个板与另一个板兼容。值得注意的例外是当一个板是另一个板的载体时,例如连接到载板的 CPU 模块。
关于 compatible 值的另外一点说明。compatible 属性中使用的任何字符串都必须记录它指示的内容。在 Documentation/devicetree/bindings 中添加 compatible 字符串的文档。
同样在 ARM 上,对于每个 machine_desc,内核会查看 dt_compat 列表中的任何条目是否出现在 compatible 属性中。如果出现,则该 machine_desc 是驱动机器的候选者。在搜索整个 machine_descs 表之后,setup_machine_fdt() 会根据每个 machine_desc 与 compatible 属性中的哪个条目匹配,返回“最兼容”的 machine_desc。如果未找到匹配的 machine_desc,则返回 NULL。
此方案背后的原因是观察到,在大多数情况下,如果所有主板都使用相同的 SoC 或相同的 SoC 系列,则单个 machine_desc 可以支持大量的主板。但是,总会有一些例外情况,即特定的主板将需要通用情况下无用的特殊设置代码。特殊情况可以通过在通用设置代码中显式检查有问题的板来处理,但如果不仅仅是几个案例,这样做会很快变得难看和/或难以维护。
相反,compatible 列表允许通用 machine_desc 通过在 dt_compat 列表中指定“较不兼容”的值来为广泛的通用主板提供支持。在上面的示例中,通用主板支持可以声明与 “ti,omap3” 或 “ti,omap3450” 兼容。如果在原始 beagleboard 上发现了一个需要在早期引导期间进行特殊解决方法代码的错误,那么可以添加一个新的 machine_desc,该 machine_desc 实现解决方法并且仅匹配 “ti,omap3-beagleboard”。
PowerPC 使用稍微不同的方案,它从每个 machine_desc 调用 .probe() 钩子,并使用第一个返回 TRUE 的钩子。但是,此方法没有考虑 compatible 列表的优先级,并且对于新的架构支持可能应该避免使用。
2.3 运行时配置¶
在大多数情况下,DT 将是固件向内核传递数据的唯一方法,因此也用于传递运行时和配置数据,如内核参数字符串和 initrd 镜像的位置。
这些数据大部分包含在 /chosen 节点中,并且在引导 Linux 时,它看起来像这样
chosen {
bootargs = "console=ttyS0,115200 loglevel=8";
initrd-start = <0xc8000000>;
initrd-end = <0xc8200000>;
};
bootargs 属性包含内核参数,initrd-* 属性定义 initrd Blob 的地址和大小。请注意,initrd-end 是 initrd 镜像之后的第一个地址,因此这与 struct resource 的常用语义不匹配。chosen 节点还可以选择性地包含任意数量的附加属性,用于特定于平台的配置数据。
在早期引导期间,架构设置代码会多次调用 of_scan_flat_dt(),并使用不同的辅助回调函数来解析设备树数据,然后再设置分页。of_scan_flat_dt() 代码会扫描设备树,并使用辅助函数提取早期引导期间所需的信息。通常,early_init_dt_scan_chosen() 辅助函数用于解析 chosen 节点,包括内核参数,early_init_dt_scan_root() 用于初始化 DT 地址空间模型,而 early_init_dt_scan_memory() 用于确定可用 RAM 的大小和位置。
在 ARM 上,setup_machine_fdt() 函数负责在选择支持该主板的正确 machine_desc 后,对设备树进行早期扫描。
2.4 设备填充¶
在识别主板并解析早期配置数据后,内核初始化可以按正常方式继续。在此过程中的某个时刻,将调用 unflatten_device_tree() 将数据转换为更高效的运行时表示形式。这也是调用特定于机器的设置钩子的时候,例如 ARM 上的 machine_desc .init_early()、.init_irq() 和 .init_machine() 钩子。本节的其余部分使用 ARM 实现中的示例,但所有架构在使用 DT 时都会执行几乎相同的操作。
顾名思义,.init_early() 用于需要在启动过程早期执行的任何特定于机器的设置,而 .init_irq() 用于设置中断处理。使用 DT 不会实质性地改变这两个函数的行为。如果提供了 DT,则 .init_early() 和 .init_irq() 都能够调用任何 DT 查询函数 (include/linux/of*.h 中的 of_*) 来获取有关平台的其他数据。
DT 上下文中最重要的钩子是 .init_machine(),它主要负责使用有关平台的数据填充 Linux 设备模型。在历史上,这在嵌入式平台上是通过在板级支持 .c 文件中定义一组静态时钟结构、platform_devices 和其他数据,并在 .init_machine() 中批量注册来实现的。当使用 DT 时,设备列表可以通过解析 DT 并动态分配设备结构来获得,而不是为每个平台硬编码静态设备。
最简单的情况是 .init_machine() 仅负责注册一个 platform_devices 块。platform_device 是 Linux 用于内存或 I/O 映射设备的概念,这些设备无法通过硬件检测到,以及用于“复合”或“虚拟”设备(稍后会详细介绍)。虽然 DT 没有“平台设备”的术语,但平台设备大致对应于树根处的设备节点和简单内存映射总线节点的子节点。
现在是举例说明的好时机。以下是 NVIDIA Tegra 板的设备树的一部分
/{
compatible = "nvidia,harmony", "nvidia,tegra20";
#address-cells = <1>;
#size-cells = <1>;
interrupt-parent = <&intc>;
chosen { };
aliases { };
memory {
device_type = "memory";
reg = <0x00000000 0x40000000>;
};
soc {
compatible = "nvidia,tegra20-soc", "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges;
intc: interrupt-controller@50041000 {
compatible = "nvidia,tegra20-gic";
interrupt-controller;
#interrupt-cells = <1>;
reg = <0x50041000 0x1000>, < 0x50040100 0x0100 >;
};
serial@70006300 {
compatible = "nvidia,tegra20-uart";
reg = <0x70006300 0x100>;
interrupts = <122>;
};
i2s1: i2s@70002800 {
compatible = "nvidia,tegra20-i2s";
reg = <0x70002800 0x100>;
interrupts = <77>;
codec = <&wm8903>;
};
i2c@7000c000 {
compatible = "nvidia,tegra20-i2c";
#address-cells = <1>;
#size-cells = <0>;
reg = <0x7000c000 0x100>;
interrupts = <70>;
wm8903: codec@1a {
compatible = "wlf,wm8903";
reg = <0x1a>;
interrupts = <347>;
};
};
};
sound {
compatible = "nvidia,harmony-sound";
i2s-controller = <&i2s1>;
i2s-codec = <&wm8903>;
};
};
在 .init_machine() 时,Tegra 板支持代码需要查看此 DT 并决定为哪些节点创建 platform_devices。但是,查看树,并不立即清楚每个节点代表哪种类型的设备,甚至节点是否代表设备。/chosen、/aliases 和 /memory 节点是不描述设备的的信息节点(尽管可以说内存可以被视为设备)。/soc 节点的子节点是内存映射设备,但 codec@1a 是 i2c 设备,而 sound 节点不代表设备,而是代表其他设备如何连接在一起以创建音频子系统。我知道每个设备是什么,因为我熟悉板级设计,但是内核如何知道如何处理每个节点呢?
诀窍是内核从树的根开始,并查找具有“compatible”属性的节点。首先,通常假设任何具有“compatible”属性的节点都代表某种设备,其次,可以假设树根处的任何节点要么直接连接到处理器总线,要么是无法以任何其他方式描述的杂项系统设备。对于这些节点中的每一个,Linux 分配并注册一个 platform_device,它反过来可能会绑定到 platform_driver。
为什么为这些节点使用 platform_device 是安全的假设?好吧,对于 Linux 建模设备的方式,几乎所有的 bus_types 都假设其设备是总线控制器的子设备。例如,每个 i2c_client 都是 i2c_master 的子设备。每个 spi_device 都是 SPI 总线的子设备。USB、PCI、MDIO 等也是如此。在 DT 中也发现了相同的层次结构,其中 I2C 设备节点只作为 I2C 总线节点的子节点出现。SPI、MDIO、USB 等也是如此。唯一不需要特定类型父设备的设备是 platform_devices (和 amba_devices,稍后会详细介绍),它们将愉快地存在于 Linux /sys/devices 树的底部。因此,如果一个 DT 节点在树的根部,那么它最好注册为 platform_device。
Linux 板级支持代码调用 of_platform_populate(NULL, NULL, NULL, NULL) 来启动在树根处发现设备。这些参数都为 NULL,因为当从树的根开始时,不需要提供起始节点(第一个 NULL)、父 struct device
(最后一个 NULL),而且我们没有使用匹配表(尚未)。对于只需要注册设备的板,除了 of_platform_populate()
调用之外,.init_machine() 可以完全为空。
在 Tegra 示例中,这解释了 /soc 和 /sound 节点,但是 SoC 节点的子节点呢?它们不应该也注册为平台设备吗?对于 Linux DT 支持,通用行为是在驱动程序 .probe() 时由父设备的驱动程序注册子设备。因此,i2c 总线设备驱动程序将为每个子节点注册一个 i2c_client,SPI 总线驱动程序将注册其 spi_device 子节点,其他 bus_types 也是如此。根据该模型,可以编写一个驱动程序,该驱动程序绑定到 SoC 节点,并简单地为每个子节点注册 platform_devices。板级支持代码将分配并注册一个 SoC 设备,一个(理论上的)SoC 设备驱动程序可以绑定到 SoC 设备,并在其 .probe() 钩子中为 /soc/interrupt-controller、/soc/serial、/soc/i2s 和 /soc/i2c 注册 platform_devices。很简单,对吧?
实际上,事实证明,将某些 platform_devices 的子节点注册为更多的 platform_devices 是一种常见的模式,设备树支持代码反映了这一点,并使上述示例更加简单。of_platform_populate()
的第二个参数是 of_device_id 表,并且任何与该表中条目匹配的节点也将注册其子节点。在 Tegra 示例中,代码可以如下所示
static void __init harmony_init_machine(void)
{
/* ... */
of_platform_populate(NULL, of_default_bus_match_table, NULL, NULL);
}
“simple-bus” 在 Devicetree 规范中定义为表示简单的内存映射总线的属性,因此 of_platform_populate()
代码可以编写为仅假设始终会遍历 simple-bus 兼容节点。但是,我们将其作为参数传递,以便板级支持代码始终可以覆盖默认行为。
[需要添加关于添加 i2c/spi 等子设备的讨论]
附录 A:AMBA 设备¶
ARM Primecells 是一种连接到 ARM AMBA 总线的特定类型的设备,它包含对硬件检测和电源管理的一些支持。在 Linux 中,struct amba_device 和 amba_bus_type 用于表示 Primecell 设备。但是,棘手的是,并非 AMBA 总线上的所有设备都是 Primecell,对于 Linux 来说,amba_device 和 platform_device 实例通常是同一总线段的同级设备。
当使用 DT 时,这会给 of_platform_populate()
带来问题,因为它必须决定将每个节点注册为 platform_device 还是 amba_device。不幸的是,这使得设备创建模型变得有点复杂,但解决方案最终并没有太大的侵入性。如果一个节点与“arm,primecell”兼容,则 of_platform_populate()
将其注册为 amba_device 而不是 platform_device。