Linux 和设备树

发布于:2025-02-11 ⋅ 阅读:(23) ⋅ 点赞:(0)

“开放固件设备树”,简称 Devicetree (DT),是一种用于描述硬件的数据结构和语言。更具体地说,它是操作系统可读取的硬件描述,因此操作系统无需对机器的详细信息进行硬编码。

从结构上看,DT 是一棵树,或具有命名节点的非循环图,节点可以具有任意数量的命名属性,封装任意数据。在自然树结构之外,还存在一种机制来创建从一个节点到另一个节点的任意链接。

从概念上讲,定义了一组通用的使用约定(称为“绑定”),用于描述数据应如何出现在树中,以描述典型的硬件特性,包括数据总线、中断线、GPIO 连接和外围设备。

尽可能使用现有绑定来描述硬件,以最大限度地利用现有支持代码,但由于属性和节点名称只是文本字符串,因此很容易扩展现有绑定或通过定义新节点和属性来创建新绑定。但是,在创建新绑定之前,请务必先了解现有绑定。目前有两种不同的、不兼容的 i2c 总线绑定,这是因为在创建新绑定时没有先研究现有系统中 i2c 设备是如何枚举的。

数据模型

高级视图

最重要的是要理解 DT 只是一种描述硬件的数据结构。它没有什么神奇之处,也不能神奇地解决所有硬件配置问题。它的作用是提供一种语言,将硬件配置与 Linux 内核(或任何其他操作系统)中的主板和设备驱动程序支持分离开来。使用它可以让主板和设备支持由数据驱动;根据传入内核的数据而不是每台机器的硬编码选择来做出设置决策。

理想情况下,数据驱动平台设置应该减少代码重复,并更容易使用单个内核映像支持各种硬件。

Linux 使用 DT 数据有三个主要目的:

  • 平台识别,
  • 运行时配置,以及
  • 设备数量。
平台识别

首先,内核将使用 DT 中的数据来识别特定机器。在理想情况下,特定平台对内核来说并不重要,因为所有平台细节都会以一致且可靠的方式由设备树完美描述。然而,硬件并不完美,因此内核必须在早期启动期间识别机器,以便有机会运行特定于机器的修复程序。

在大多数情况下,机器身份无关紧要,内核将根据机器的核心 CPU 或 SoC 选择设置代码。例如,在 ARM 上,arch/arm/kernel/setup.c 中的 setup_arch() 将调用 arch/arm/kernel/devtree.c 中的 setup_machine_fdt(),后者搜索 machine_desc 表并选择与设备树数据最匹配的 machine_desc。它通过查看根设备树节点中的“兼容”属性并将其与 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 模块。

关于兼容值还有一点需要注意。兼容属性中使用的任何字符串都必须记录其含义。在 Documentation/devicetree/bindings 中添加兼容字符串的文档。

同样在 ARM 上,对于每个 machine_desc,内核会查看 dt_compat 列表条目中是否有任何条目出现在兼容属性中。如果出现,则该 machine_desc 是驱动机器的候选。在搜索整个 machine_descs 表后,setup_machine_fdt() 根据每个 machine_desc 与兼容属性中的哪个条目匹配,返回“最兼容”的 machine_desc。如果未找到匹配的 machine_desc,则返回 NULL。

这种方案背后的原因是,在大多数情况下,如果所有主板都使用相同的 SoC 或相同的 SoC 系列,则单个 machine_desc 可以支持大量主板。但是,总会有一些例外,即特定主板需要特殊的设置代码,而这些代码在一般情况下是没有用的。特殊情况可以通过在一般设置代码中明确检查有问题的主板来处理,但如果情况不止几种,这样做很快就会变得丑陋和/或难以维护。

相反,兼容列表允许通用 machine_desc 通过在 dt_compat 列表中指定“兼容性较差”的值来为广泛的通用主板集提供支持。在上面的示例中,通用主板支持可以声称与“ti,omap3”或“ti,omap3450”兼容。如果在原始 beagleboard 上发现一个错误,需要在早期启动期间使用特殊的解决方法代码,则可以添加一个新的 machine_desc,它实现解决方法并且仅与“ti,omap3-beagleboard”匹配。

PowerPC 使用略有不同的方案,它从每个 machine_desc 调用 .probe() 钩子,并使用第一个返回 TRUE 的钩子。但是,这种方法没有考虑兼容列表的优先级,对于新架构支持可能应该避免使用。

运行时配置

在大多数情况下,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 的通常语义不符。所选节点还可以选择包含任意数量的其他属性,用于特定于平台的配置数据。

在早期启动期间,架构设置代码会多次使用不同的辅助回调来调用 of_scan_flat_dt(),以便在设置分页之前解析设备树数据。of_scan_flat_dt() 代码会扫描设备树并使用辅助程序提取早期启动期间所需的信息。通常,early_init_dt_scan_chosen() 辅助程序用于解析所选节点(包括内核参数)、early_init_dt_scan_root() 用于初始化 DT 地址空间模型,early_init_dt_scan_memory() 用于确定可用 RAM 的大小和位置。

在 ARM 上,函数 setup_machine_fdt() 负责在选择支持主板的正确 machine_desc 后对设备树进行早期扫描。

设备填充

识别主板并解析早期配置数据后,内核初始化即可正常进行。在此过程中的某个时刻,会调用 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 节点的子节点是内存映射设备,但编解码器@ 1a是i2c 设备,声音节点不代表设备,而是其他设备如何连接在一起以创建音频子系统。我知道每个设备是什么,因为我熟悉主板设计,但内核如何知道如何处理每个节点?

诀窍在于,内核从树的根开始,寻找具有“兼容”属性的节点。首先,通常假设具有“兼容”属性的任何节点都代表某种设备;其次,可以假设树根处的任何节点要么直接连接到处理器总线,要么是无法以其他方式描述的杂项系统设备。对于每个节点,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)、父节点(最后一个 NULL),并且我们尚未使用匹配表(目前)。对于只需要注册设备的主板,.init_machine() 除了调用之外可以完全为空 。struct deviceof_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” 在 Devicetree 规范中定义为表示简单内存映射总线的属性,因此of_platform_populate()可以编写代码以假设始终会遍历 simple-bus 兼容节点。但是,我们将其作为参数传入,以便板级支持代码始终可以覆盖默认行为。

附录 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。


网站公告

今日签到

点亮在社区的每一天
去签到