网页设计与网站架设,网页设计尺寸大小规范,超市会员管理系统,国际上比较认可的邮箱“开放固件设备树”#xff0c;简称 Devicetree (DT)#xff0c;是一种用于描述硬件的数据结构和语言。更具体地说#xff0c;它是操作系统可读取的硬件描述#xff0c;因此操作系统无需对机器的详细信息进行硬编码。
从结构上看#xff0c;DT 是一棵树#xff0c;或具有…“开放固件设备树”简称 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 consolettyS0,115200 loglevel8;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-controller50041000 {compatible nvidia,tegra20-gic;interrupt-controller;#interrupt-cells 1;reg 0x50041000 0x1000, 0x50040100 0x0100 ;};serial70006300 {compatible nvidia,tegra20-uart;reg 0x70006300 0x100;interrupts 122;};i2s1: i2s70002800 {compatible nvidia,tegra20-i2s;reg 0x70002800 0x100;interrupts 77;codec wm8903;};i2c7000c000 {compatible nvidia,tegra20-i2c;#address-cells 1;#size-cells 0;reg 0x7000c000 0x100;interrupts 70;wm8903: codec1a {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_clientSPI 总线驱动程序将注册其 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。