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 = "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_desc 表之后,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,它实现解决方法并且仅匹配“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_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),并且我们还没有使用匹配表(尚未)。对于只需要注册设备的板卡,.init_machine() 可以完全为空,除了 of_platform_populate()
调用之外。
在 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”在设备树规范中被定义为一个属性,意味着一个简单的内存映射总线,因此可以编写 of_platform_populate()
代码,使其仅假设 simple-bus 兼容节点将始终被遍历。但是,我们将其作为参数传递,以便板卡支持代码始终可以覆盖默认行为。
[需要添加有关添加 i2c/spi/etc 子设备的讨论]
附录 A:AMBA 设备¶
ARM Primecells 是一种连接到 ARM AMBA 总线的设备,它包括对硬件检测和电源管理的一些支持。在 Linux 中,struct amba_device 和 amba_bus_type 用于表示 Primecell 设备。然而,棘手的是,并非 AMBA 总线上的所有设备都是 Primecells,并且对于 Linux 来说,amba_device 和 platform_device 实例通常是同一总线段的兄弟设备。
当使用 DT 时,这会给 of_platform_populate()
带来问题,因为它必须决定是将每个节点注册为 platform_device 还是 amba_device。不幸的是,这使设备创建模型稍微复杂了一点,但是解决方案并不太具有侵入性。如果一个节点与“arm,primecell”兼容,那么 of_platform_populate()
将其注册为 amba_device 而不是 platform_device。