Linux和设备树

设备树数据的Linux使用模型

作者:Grant Likely <[email protected]>

本文介绍Linux如何使用设备树。可以在下面的网址设备树使用页面上找到设备树数据格式的概述。

https://elinux.org/Device_Tree_Usage

"Open Firmware Device Tree"或简称设备树(DT)是用于描述硬件的数据结构和语言。更具体地,它是操作系统可读的硬件的描述,使得操作系统不需要硬编码机器的细节。

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

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

尽可能地使用现有绑定来描述硬件以最大限度地使用现有支持代码,但由于属性和节点名称只是文本字符串,因此通过定义新节点和属性可以轻松扩展现有绑定或创建新绑定。但是,要小心创建一个新的绑定,而不先做一些关于已经存在的功课。目前有两种不同的,不兼容的i2c总线绑定,因为新的绑定是在没有首先研究如何在现有系统中枚举i2c设备的情况下创建的。

1.历史

DT最初由Open Firmware创建,作为将数据从Open Firmware传递到客户端程序(如操作系统)的通信方法的一部分。操作系统使用设备树在运行时发现硬件的拓扑结构,从而支持大多数没有硬编码信息的可用硬件(假设驱动程序可用于所有设备)。

由于Open Firmware通常用于PowerPC和SPARC平台,因此Linux对这些体系结构的支持长期以来一直使用设备树。

2005年,当PowerPC Linux开始大规模清理并合并32位和64位支持时,决定在所有powerpc平台上要求DT支持,无论他们是否使用Open Firmware。为此,创建了一个称为偏平设备树(FDT)的DT表示,它可以作为二进制blob传递给内核,而无需实际的Open Firmware实现。修改了U-Boot,kexec和其他引导加载程序,以支持传递设备树二进制文件(dtb)并在引导时修改dtb。DT还被添加到PowerPC启动包装器(arch/powerpc/boot/*)中,以便dtb可以与内核映像一起包装,以支持启动现有的非DT感知固件。

一段时间后,FDT基础设施被推广为可供所有架构使用。在撰写本文时,6个主流架构(arm,microblaze,mips,powerpc,sparc和x86)和1个主线(nios)都有一定程度的DT支持。

2.数据模型

如果您尚未阅读设备树用法页面,请立即阅读。 没关系,我等一下......

2.1高级视角

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

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

Linux将DT数据用于三个主要目的:
1).平台识别;
2).运行时配置;
3).设备识别;

2.2.平台识别

首先,内核将使用DT中的数据来识别特定的machine。在完美的世界中,特定平台对内核无关紧要,因为设备树将以一致且可靠的方式完美地描述所有平台细节。硬件并不完美,因此内核必须在早期启动期间识别机器,以便它有机会运行特定于机器的修正。

在大多数情况下,机器标识是无关紧要的,内核将根据机器的核心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列表进行比较来确定最佳匹配。

"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列表条目出现在compatible属性中。如果有,那么machine_desc是驱动machine的候选者。在搜索整个machine_descs表之后,setup_machine_fdt()函数根据每个machine_desc匹配的兼容属性中的哪个条目返回"最兼容"的machine_desc。如果找不到匹配的machine_desc,则返回NULL。

这种方案背后的原因是,在大多数情况下,如果单个machine_desc都使用相同的SoC或相同的SoC系列,则它们可以支持大量的board。但是,总会有一些例外情况,特定的电路板需要特殊的设置代码,这在通用情况下是无用的。可以通过在通用设置代码中明确检查麻烦的板来处理特殊情况,但如果它不仅仅是几个案例,那么很快就会变得难看和/或不可维护。

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

PowerPC使用稍微不同的方案,它从每个machine_desc调用.probe()函数,并使用返回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映像之后的第一个地址,因此这与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()函数负责在选择支持board的正确machine_desc后对设备树进行早期扫描。

2.4.设备识别

在识别board之后,并且在解析了早期配置数据之后,内核初始化可以以正常方式进行。在此过程的某个时刻,调用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),父结构设备(最后一个NULL),并且我们没有使用匹配表。对于只需要注册设备的板,除了of_platform_populate()调用之外,.init_machine()函数可以完全为空。

在Tegra示例中,这说明了/soc和/sound节点,但是SoC节点的子节点呢?它们不应该也被注册为平台设备吗?对于Linux DT支持,通用行为是由父设备驱动程序在驱动程序.probe()时注册子设备。因此,i2c总线设备驱动程序将为每个子节点注册i2c_client,SPI总线驱动程序将注册其spi_device子节点,类似地为其他bus_types注册。根据该模型,可以编写一个绑定到SoC节点的驱动程序,并简单地为每个子节点注册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"兼容节点。但是,我们将其作为参数传递,以便板级支持代码始终可以覆盖默认行为。

猜你喜欢

转载自blog.csdn.net/caihaitao2000/article/details/83716475