The Linux usage model for device tree data
Linux and the Device Tree
The Linux usage model for device tree data
Author: Grant Likely grant.likely@secretlab.ca
这篇文章介绍了Linux中使用Device Tree的方法。可以在http://devicetree.org/Device_Tree_Usage获取到Device Tree数据格式。
Device Tree是一种描述硬件的语言,它可以让操作系统不硬编码硬件的信息。
结构上讲,Device Tree是树形结构,或者非循环的有名节点组成的图。每个节点包含一定数目的属性和键值对。一个节点可以通过声明链接的形式别其他节点引用,这一点类似于C语言的指针。
概念上讲,数据以何种形式来描述硬件特性,包括数据总线、中断线、GPIO连接以及外设等,都是由一组约定的使用方法来实现的,被称为”bindings”。
在使用过程中,我们尽可能的使用现有的bindings来描述硬件设备,这样可以最大限度地支持现存的代码。但是,因为属性和节点名从本质上讲仅仅是文本字符串,我们也可以很容易地通过创建新的属性和节点来扩展现有的bindings。
- 历史
- 数据模型
2.1 顶层视图
首先要明确一个概念,DT仅仅是一个描述硬件的数据结构,它本身没有任何魔力,也没有魔力使硬件配置的所有问题都消弭。它提供了一种描述语言使硬件配置与平台和设备驱动去耦。
理想状态下,这种数据驱动的平台设置方式能够减少复用的代码,并使单一内核镜像支持多个平台。
Linux使用DT数据实现三个目的:
1) 平台识别
2) 运行时配置
3) 设备枚举
2.2 平台识别
首要的是,内核使用DT中的数据识别特定平台。理想状态下,所有的硬件细节都在DT中以连续的和可靠的方式描述,这样内核就与特定平台无关。但是事实上硬件平台没有这么完美,因此,内核必须在早期启动过程中识别平台,这样就有机会运行一些硬件特定的修复代码。(通过软件的方式修补硬件的缺陷)
在大多数情况下,机器型号识别和DT无关,取而代之,内核通过检测机器的核心CPU或者SoC来选择启动代码。以ARM平台为例,arch/arm/kernel/setup.c中的setup_arch()函数会调用setup_machine_fdt() (在arch/arm/kernel/devtree.c中), 这个函数会遍历machine_desc表并选择最匹配设备树数据的machine_desc结构体。它通过比较DT根结点的compatible属性和machine_desc结构体中的dt_compat链表,从而甄选出最匹配的machine_desc结构体。(struct machine_desc定义在arch/arm/include/asm/mach/arch.h中)
Compatible属性包含一组字符串,其形式是准确的机器名,紧跟着一系列可选的平台名称, 平台名称的顺序决定匹配的优先级。例如:
compatible = "ti,omap3-beagleboard", "ti,omap3450", "ti,omap3";
遍历完所有machine_desc链表后,setup_machine_fdt()函数返回最匹配的machine_desc结构体指针,如果没有匹配的machine_desc结构体,那么它会返回NULL。
这种机制被引用的原因是,在大多数情况下,当多个平台使用同一款SoC时,一个machine_desc结构体可以支持多个平台。那么,当一个特定的平台需要执行特定的初始化代码时,我们就需要在通用的初始化函数中快速地甄别需要执行特殊启动代码的平台。
反过来,compatible链表通过在dt_compat链表中插入less compatible值,从而允许一个通用的(在一个系列产品中)machine_desc支持多个平台。
2.3 运行时配置
在大多数情况下,DT是boot loader与内核之间通信的唯一方式。所以,DT也被用来向内核传递一些运行时数据或者配置数据,例如内核启动参数、initrd镜像的加载地址等。
这类数据一般被包含在/chosen节点中,在引导Linux时,它们是以如下形式呈现给内核的:
chosen {
bootargs = "console=ttyS0,115200 loglevel=8";
initrd-start = <0xc8000000>;
initrd-end = <0xc8200000>;
};
Chosen节点也可以包含特定数量的描述平台特殊配置的属性。
在早期启动过程中,平台启动代码在内存页表创建之前多次调用of_scan_flat_dt() (使用不同的helper callbacks)来解析设备树。该函数遍历设备树,通过helper获取需要的信息。例如,early_init_dt_scan_chosen()回调函数用来解析chosen节点,包括内核启动参数,early_init_dt_scan_root()回调函数用来初始化DT寻址空间,early_init_dt_scan_memory()回调函数用来决定可用内存的大小和位置。
在ARM平台上,setup_machine_fdt()负责在选择正确的machine_desc之后,初步遍历设备树。
2.4 设备枚举
在这一阶段,unflatten_device_tree()函数被调用,从而将DT数据实时地有组织地呈现给内核其他模块。与此同时,特定机器的启动钩子函数会被调用,例如machine_desc中的init_early(),init_irq(),init_machine()等。Init_early()函数用来在启动早期阶段执行特定平台的启动代码,init_irq()用来设置中断处理流程。使用DT并不会改变这些函数的特性行为,但是DT可以让这些函数利用DT查询函数(of_* in include/linux/of*.h)获取到额外的平台信息。
DT上下文中最有兴趣的钩子函数是init_machine(),这个函数主要负责枚举Linux设备模型。以前的实现方式是,通过在板级支持文件中定义一组静态的时钟结构体、平台设备和其他数据,然后在init_machine()中向内核注册它们。采用DT之后,硬件的信息就不以这种硬编码的方式实现了,而是通过解析设备树动态地分配设备结构体。
最简单的情形是,init_machine()只负责注册一批platform_devices。概念上将,platform_device是不能被硬件检测到的、被Linux以memory或者I/O映射的方式利用的设备,也用来表示组合(composite)或者虚拟(virtual)设备。在DT中,并没有platform device这个术语,platform device只是粗略地对应着根结点下的设备结点和简单地内存映射的总线节点的子结点。
讲到这个地方,是时候给大家举个例子了:
256/{
257 compatible = "nvidia,harmony", "nvidia,tegra20";
258 #address-cells = <1>;
259 #size-cells = <1>;
260 interrupt-parent = <&intc>;
261
262 chosen { };
263 aliases { };
264
265 memory {
266 device_type = "memory";
267 reg = <0x00000000 0x40000000>;
268 };
269
270 soc {
271 compatible = "nvidia,tegra20-soc", "simple-bus";
272 #address-cells = <1>;
273 #size-cells = <1>;
274 ranges;
275
276 intc: interrupt-controller@50041000 {
277 compatible = "nvidia,tegra20-gic";
278 interrupt-controller;
279 #interrupt-cells = <1>;
280 reg = <0x50041000 0x1000>, < 0x50040100 0x0100 >;
281 };
282
283 serial@70006300 {
284 compatible = "nvidia,tegra20-uart";
285 reg = <0x70006300 0x100>;
286 interrupts = <122>;
287 };
288
289 i2s1: i2s@70002800 {
290 compatible = "nvidia,tegra20-i2s";
291 reg = <0x70002800 0x100>;
292 interrupts = <77>;
293 codec = <&wm8903>;
294 };
295
296 i2c@7000c000 {
297 compatible = "nvidia,tegra20-i2c";
298 #address-cells = <1>;
299 #size-cells = <0>;
300 reg = <0x7000c000 0x100>;
301 interrupts = <70>;
302
303 wm8903: codec@1a {
304 compatible = "wlf,wm8903";
305 reg = <0x1a>;
306 interrupts = <347>;
307 };
308 };
309 };
310
311 sound {
312 compatible = "nvidia,harmony-sound";
313 i2s-controller = <&i2s1>;
314 i2s-codec = <&wm8903>;
315 };
316};
仔细看上面的实例,Tegra板级支持代码需要解析上述设备树并决定哪些节点用来创建platform_device。然后,我们很难一眼看出来每个节点代表何种类型的设备,甚至很难看出一个节点是否代表一个设备。例如,/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、SDIO总线而言也是如此。同样地层级关系也表现在DT中,例如,所有的I2C设备节点只以I2C总线节点的子结点出现。唯一一类不需要特定父设备的设备是platform_device(还有amba_devices),platform_device可以快活地在/sys/devices树中存在。因此,如果一个节点在根结点下,那么它很有可能被注册成为platform_device。
Linux板级支持代码调用of_platform_populate(NULL,NULL,NULL,NULL)开始在DT中发现根结点下的设备。之所以所有的参数都是NULL,是因为从根结点开始查找时,没必要指定起始节点(第一个参数),父结构体设备(最后一个参数),也不是用匹配表。对于一个只需要注册设备的平台,init_machine()可以只包含of_platform_populate()函数调用。
对于上述Tegra的设备树实例,根结点下的/SoC和/sound节点可以被注册为platform_device,但是,它们的子结点不应该被注册成为platform_device吗?通用的做法是,在父设备驱动的probe()函数执行时注册这些子设备。那么,一个I2C总线驱动会为每一个子结点注册一个i2c_client设备结构体,一个SPI总线驱动会注册spi_device设备,其他总线类型也是如此。根据这种模型,可以写一个与/SoC节点绑定的驱动用来将它的子结点都注册成platform_device设备,例如/soc/interrupt-controller, /soc/serial, /soc/i2s, /soc/i2c等。
事实证明,将platform_device设备的子设备注册成为platform_device是一种常用的形式,设备树支持代码也反映出了这一点。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”是ePAPR 1.0规范中定义的一个属性,用来代表内存映射的总线,of_platform_populate()函数默认会遍历所有/soc下的节点。然而,我们通过向它传递一个参数,这样就可以覆盖其默认行为。
Appendix A: AMBA devices
ARM Primecell外设依附在ARM AMBA总线上,ARM AMBA总线支持硬件检测和电源管理。在Linux中,ARM Primecell外设用结构体amba_device和amba_bus_type来描述。然而,不是所有AMBA总线上的设备都是Primecell类型的外设,这样,Linux中就允许在AMBA总线设备下同时存在amba_device实例和platform_device实例。
使用DT时,这样就会给of_platform_populate()函数带来困扰,因为它必须决定将每个节点注册成amba_device还是platform_device。这显然给设备创建模型引入了麻烦,但是实际的解决方案却很巧妙。如果一个节点的compatible属性是”arm,amba-primecell”,那么of_platform_populate()函数就会把它注册成为amba_device而不是platform_device。