设备树数据的Linux使用模型
作者:Grant Likely grant.likely@secretlab.ca
本文介绍Linux如何使用设备树。概述
可以在设备树使用情况页面上找到设备树数据格式
在https://github.com/devicetree-org/devicetree-specification
“开放固件设备树(Open Firmware Device Tree)”或简称为设备树(DT)是数据
描述硬件的结构和语言。更具体地说,是对操作系统可读的硬件的描述。这样,操作系统就无需对应用程序的详细信息进行硬编码。
从结构上讲,DT是具有命名节点的树或无环图,并且节点可以封装任意数量的命名属性任意数据。还存在一种机制来创建任意从一个节点到自然树结构外部的另一个节点的链接。
从概念上讲,一组通用的使用约定(称为“绑定”)定义了数据应如何在树中出现以描述典型
硬件特性,包括数据总线,中断线,GPIO,连接和外围设备。
尽可能使用现有的绑定来描述硬件最大限度地利用现有支持代码,但是由于属性和节点名称只是文本字符串,很容易扩展现有的绑定或通过定义新的节点和属性来创建新的节点。警惕,
但是,无需先做功课就可以创建新的绑定关于已经存在的东西。目前有两种不同不兼容,因为新的i2c总线的绑定是在没有先调查i2c设备如何进行的情况下创建的,在现有系统中已经被枚举。
1.历史
DT最初是由Open Firmware创建的,它是用于将数据从开放固件传递到客户端的通信方法程序(类似于操作系统)。一个操作系统使用了设备树,用于在运行时发现硬件的拓扑,以及
从而支持大多数可用硬件,而无需硬编码信息(假设驱动程序适用于所有设备)。
由于开放式固件通常在PowerPC和SPARC平台上使用,Linux对这些架构的支持已经使用设备树很长时间了。
2005年,当PowerPC Linux开始进行重大清理并合并32位和64位支持,因此决定要求所有
powerpc平台,无论是否使用开放固件(Open Firmware)。为此,DT表示称为Flattened Device
树(FDT)已创建,可以作为二进制文件传递到内核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数据用于三个主要目的:
1)平台识别;
2)运行时配置,以及
3)设备数量。
2.2平台识别
首先,内核将使用DT中的数据来识别特定机器。在理想世界中,特定平台不应该对内核至关重要,因为将描述所有平台细节通过设备树以一致和可靠的方式完美实现。尽管硬件不是完美的,但是内核必须识别机器在早期启动期间运行,以便有机会运行机器特定的修复程序。
在大多数情况下,机器标识是无关紧要的,并且内核将根据计算机的内核选择设置代码
CPU或SoC。例如,在ARM上,位于arch/arm/kernel/setup.c的setup_arch()将在以下位置调用setup_machine_fdt()在machine_desc中搜索的arch / arm / kernel / devtree.c
表并选择与设备树最匹配的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模块载板。
关于兼容值的另一注。兼容中使用的任何字符串属性必须记录其所指示的内容。加文档/设备树/绑定中的兼容字符串的文档。
再次在ARM上,对于每个machine_desc,内核都会查看是否任何dt_compat列表条目都会出现在compatible属性中。如果是,则该machine_desc是驱动机。搜索完整个machine_descs表之后,
setup_machine_fdt()返回基于“最兼容”的machine_desc每个machine_desc在兼容属性中的哪个条目上匹配反对。如果找不到匹配的machine_desc,则返回NULL。
该方案背后的原因是观察到在某些情况下,单个machine_desc可以支持大量板
如果它们都使用相同的SoC或相同的SoC系列。然而,总是会有一些例外,具体的板将
需要特殊的设置代码,这些代码在一般情况下没有用。特殊情况可以通过显式检查是否存在
通用设置代码中麻烦的板子,但是很快如果不仅仅是几个,就会变得丑陋和/或难以维护。
相反,兼容列表允许通用的machine_desc提供通过指定“较少”来支持广泛的通用板dt_compat列表中的“ compatible”值。在上面的示例中通用板支持可以要求与“ ti,omap3”兼容,或者“ ti,omap3450”。如果在原始Beagleboard上发现错误在早期启动期间需要特殊的变通方法代码,然后重新启动可以添加machine_desc来实现解决方法,并且仅在“ ti,omap3-beagleboard”上匹配。
PowerPC使用略有不同的方案,在其中调用.probe()钩子从每个machine_desc,并使用第一个返回TRUE的。但是,这种方法没有考虑到兼容列表,对于新架构,应该避免使用支持。
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映像之后的第一个地址,因此这不是
匹配结构资源的通常语义。所选节点也可能
(可选)包含任意数量的其他属性,用于
平台特定的配置数据。
在早期启动期间,体系结构设置代码调用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后扫描设备树
支持董事会。
2.4设备数量
确定板卡后以及早期配置数据之后已被解析,则内核初始化可以正常进行办法。在此过程中的某个时刻,将调用unflatten_device_tree()将数据转换为更有效的运行时表示形式。
这也是机器特定的安装挂钩将被调用的时候,例如machine_desc .init_early()、. init_irq()和.init_machine()挂钩在ARM上。本节的其余部分使用ARM的示例实施,但是所有架构都将执行几乎相同的操作使用DT时的事情。
可以通过名称猜出,.init_early()用于任何机器-需要在启动过程中尽早执行的特定设置,和.init_irq()用于设置中断处理。使用DT不会从本质上改变这两个函数的行为。
如果提供了DT,则.init_early()和.init_irq()都可以调用任何DT查询函数(include / linux / of * .h中的of_ *)获取有关平台的其他数据。
DT上下文中最有趣的钩子是.init_machine(),它主要负责使用以下命令填充Linux设备模型
有关平台的数据。从历史上讲,嵌入式平台通过定义一组静态时钟结构,platform_devices以及板上的其他数据支持.c文件,以及在.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设备,声音节点
代表的不是设备,而是其他设备的连接方式
一起创建音频子系统。我知道每个设备是什么
因为我熟悉电路板设计,但是内核如何
知道如何处理每个节点?
诀窍是内核从树的根部开始并看起来
具有“兼容”属性的节点。首先,通常
假定具有“兼容”属性的任何节点都代表一个设备
其次,可以假定根的任何节点
的树要么直接连接到处理器总线,要么是
不能用其他任何方式描述的其他系统设备。
对于这些节点中的每一个,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()可以完全为空,除了
of_platform_populate()调用。
在Tegra示例中,这说明了/ soc和/ sound节点,但是
SoC节点的子代呢?他们不应该注册吗
作为平台设备吗?对于Linux DT支持,一般行为
用于由父级的设备驱动程序在以下位置注册子级设备
驱动程序.probe()时间。因此,i2c总线设备驱动程序将注册一个
每个子节点都有i2c_client,SPI总线驱动程序将注册
它的spi_device子级,以及类似的其他bus_type。
根据该模型,可以编写绑定到
SoC节点,只需为其每个节点注册platform_devices
孩子们。电路板支持代码将分配和注册SoC
设备,(理论上的)SoC设备驱动程序可以绑定到SoC设备,
并为/ soc / interrupt-controller,/ soc / serial注册platform_devices,
/ soc / i2s和/ soc / i2c在其.probe()挂钩中。容易吧?
实际上,事实证明,注册一些
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);
}
在设备树规范中将“简单总线”定义为一个属性
意味着一个简单的内存映射总线,因此of_platform_populate()代码
可以写成只是假设与简单总线兼容的节点
总是被遍历。但是,我们将其作为参数传递给
主板支持代码始终可以覆盖默认行为。
[需要增加有关添加i2c / spi / etc子设备的讨论]
附录A:AMBA设备
ARM Primecell是连接到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,amba-primecell”兼容,则
of_platform_populate()会将其注册为amba_device而不是
platform_device。