brintos

brintos / linux-shallow public Read only

0
0
Text · 17.1 KiB · 19ba4ae Raw
331 lines · plain
1.. SPDX-License-Identifier: GPL-2.02.. include:: ../disclaimer-zh_CN.rst3 4:Original: Documentation/devicetree/usage-model.rst5 6:翻译:7 8 司延腾 Yanteng Si <siyanteng@loongson.cn>9 10:校译:11 12 13===================14Linux 和 Devicetree15===================16 17Linux对设备树数据的使用模型18 19:作者: Grant Likely <grant.likely@secretlab.ca>20 21这篇文章描述了Linux如何使用设备树。关于设备树数据格式的概述可以在22devicetree.org\ [1]_ 的设备树使用页面上找到。23 24.. [1] https://www.devicetree.org/specifications/25 26"Open Firmware Device Tree",或简称为Devicetree(DT),是一种用于描述硬27件的数据结构和语言。更确切地说,它是一种操作系统可读的硬件描述,这样操作系统就不28需要对机器的细节进行硬编码。29 30从结构上看,DT是一棵树,或者说是带有命名节点的无环图,节点可以有任意数量的命名31属性来封装任意的数据。还存在一种机制,可以在自然的树状结构之外创建从一个节点到32另一个节点的任意链接。33 34从概念上讲,一套通用的使用惯例,称为 "bindings"(后文译为绑定),被定义为数据35应该如何出现在树中,以描述典型的硬件特性,包括数据总线、中断线、GPIO连接和外围36设备。37 38尽可能使用现有的绑定来描述硬件,以最大限度地利用现有的支持代码,但由于属性和节39点名称是简单的文本字符串,通过定义新的节点和属性来扩展现有的绑定或创建新的绑定40很容易。然而,要警惕的是,在创建一个新的绑定之前,最好先对已经存在的东西做一些41功课。目前有两种不同的、不兼容的i2c总线的绑定,这是因为在创建新的绑定时没有事先42调查i2c设备在现有系统中是如何被枚举的。43 441. 历史45-------46DT最初是由Open Firmware创建的,作为将数据从Open Firmware传递给客户程序47(如传递给操作系统)的通信方法的一部分。操作系统使用设备树在运行时探测硬件的拓48扑结构,从而在没有硬编码信息的情况下支持大多数可用的硬件(假设所有设备的驱动程49序都可用)。50 51由于Open Firmware通常在PowerPC和SPARC平台上使用,长期以来,对这些架构的52Linux支持一直使用设备树。53 542005年,当PowerPC Linux开始大规模清理并合并32位和64位支持时,决定在所有55Powerpc平台上要求DT支持,无论它们是否使用Open Firmware。为了做到这一点,56我们创建了一个叫做扁平化设备树(FDT)的DT表示法,它可以作为一个二进制的blob57传递给内核,而不需要真正的Open Firmware实现。U-Boot、kexec和其他引导程序58被修改,以支持传递设备树二进制(dtb)和在引导时修改dtb。DT也被添加到PowerPC59引导包装器(arch/powerpc/boot/\*)中,这样dtb就可以被包裹在内核镜像中,以60支持引导现有的非DT察觉的固件。61 62一段时间后,FDT基础架构被普及到了所有的架构中。在写这篇文章的时候,6个主线架63构(arm、microblaze、mips、powerpc、sparc和x86)和1个非主线架构(ios)64有某种程度的DT支持。65 661. 数据模型67-----------68如果你还没有读过设备树用法\ [1]_页,那么现在就去读吧。没关系,我等着....69 702.1 高层次视角71--------------72最重要的是要明白,DT只是一个描述硬件的数据结构。它没有什么神奇之处,也不会神73奇地让所有的硬件配置问题消失。它所做的是提供一种语言,将硬件配置与Linux内核74(或任何其他操作系统)中的板卡和设备驱动支持解耦。使用它可以使板卡和设备支持75变成数据驱动;根据传递到内核的数据做出设置决定,而不是根据每台机器的硬编码选76择。77 78理想情况下,数据驱动的平台设置应该导致更少的代码重复,并使其更容易用一个内核79镜像支持各种硬件。80 81Linux使用DT数据有三个主要目的:82 831) 平台识别。842) 运行时配置,以及853) 设备数量。86 872.2 平台识别88------------89首先,内核将使用DT中的数据来识别特定的机器。在一个理想的世界里,具体的平台对90内核来说并不重要,因为所有的平台细节都会被设备树以一致和可靠的方式完美描述。91但是,硬件并不完美,所以内核必须在早期启动时识别机器,以便有机会运行特定于机92器的修复程序。93 94在大多数情况下,机器的身份是不相关的,而内核将根据机器的核心CPU或SoC来选择95设置代码。例如,在ARM上,arch/arm/kernel/setup.c中的setup_arch()将调96用arch/arm/kernel/devtree.c中的setup_machine_fdt(),它通过97machine_desc表搜索并选择与设备树数据最匹配的machine_desc。它通过查看根98设备树节点中的'compatible'属性,并将其与struct machine_desc中的99dt_compat列表(如果你好奇,该列表定义在arch/arm/include/asm/mach/arch.h100中)进行比较,从而确定最佳匹配。101 102“compatible” 属性包含一个排序的字符串列表,以机器的确切名称开始,后面是103一个可选的与之兼容的板子列表,从最兼容到最不兼容排序。例如,TI BeagleBoard104和它的后继者BeagleBoard xM板的根兼容属性可能看起来分别为::105 106	compatible = "ti,omap3-beagleboard", "ti,omap3450", "ti,omap3";107	compatible = "ti,omap3-beagleboard-xm", "ti,omap3450", "ti,omap3";108 109其中 "ti,map3-beagleboard-xm "指定了确切的型号,它还声称它与OMAP 3450 SoC110以及一般的OMP3系列SoC兼容。你会注意到,该列表从最具体的(确切的板子)到最111不具体的(SoC系列)进行排序。112 113聪明的读者可能会指出,Beagle xM也可以声称与原Beagle板兼容。然而,我们应114该当心在板级上这样做,因为通常情况下,即使在同一产品系列中,每块板都有很高115的变化,而且当一块板声称与另一块板兼容时,很难确定到底是什么意思。对于高层116来说,最好是谨慎行事,不要声称一块板子与另一块板子兼容。值得注意的例外是,117当一块板子是另一块板子的载体时,例如CPU模块连接到一个载体板上。118 119关于兼容值还有一个注意事项。在兼容属性中使用的任何字符串都必须有文件说明它120表示什么。在Documentation/devicetree/bindings中添加兼容字符串的文档。121 122同样在ARM上,对于每个machine_desc,内核会查看是否有任何dt_compat列表条123目出现在兼容属性中。如果有,那么该machine_desc就是驱动该机器的候选者。在搜索124了整个machine_descs表之后,setup_machine_fdt()根据每个machine_desc125在兼容属性中匹配的条目,返回 “最兼容” 的machine_desc。如果没有找到匹配126的machine_desc,那么它将返回NULL。127 128这个方案背后的原因是观察到,在大多数情况下,如果它们都使用相同的SoC或相同129系列的SoC,一个machine_desc可以支持大量的电路板。然而,不可避免地会有一些例130外情况,即特定的板子需要特殊的设置代码,这在一般情况下是没有用的。特殊情况131可以通过在通用设置代码中明确检查有问题的板子来处理,但如果超过几个情况下,132这样做很快就会变得很难看和/或无法维护。133 134相反,兼容列表允许通用machine_desc通过在dt_compat列表中指定“不太兼容”的值135来提供对广泛的通用板的支持。在上面的例子中,通用板支持可以声称与“ti,ompa3”136或“ti,ompa3450”兼容。如果在最初的beagleboard上发现了一个bug,需要在137早期启动时使用特殊的变通代码,那么可以添加一个新的machine_desc,实现变通,138并且只在“ti,omap3-beagleboard”上匹配。139 140PowerPC使用了一个稍微不同的方案,它从每个machine_desc中调用.probe()钩子,141并使用第一个返回TRUE的钩子。然而,这种方法没有考虑到兼容列表的优先级,对于142新的架构支持可能应该避免。143 1442.3 运行时配置145--------------146在大多数情况下,DT是将数据从固件传递给内核的唯一方法,所以也被用来传递运行147时和配置数据,如内核参数字符串和initrd镜像的位置。148 149这些数据大部分都包含在/chosen节点中,当启动Linux时,它看起来就像这样::150 151	chosen {152		bootargs = "console=ttyS0,115200 loglevel=8";153		initrd-start = <0xc8000000>;154		initrd-end = <0xc8200000>;155	};156 157bootargs属性包含内核参数,initrd-\*属性定义initrd blob的地址和大小。注158意initrd-end是initrd映像后的第一个地址,所以这与结构体资源的通常语义不一159致。选择的节点也可以选择包含任意数量的额外属性,用于平台特定的配置数据。160 161在早期启动过程中,架构设置代码通过不同的辅助回调函数多次调用162of_scan_flat_dt()来解析设备树数据,然后进行分页设置。of_scan_flat_dt()163代码扫描设备树,并使用辅助函数来提取早期启动期间所需的信息。通常情况下,164early_init_dt_scan_chosen()辅助函数用于解析所选节点,包括内核参数,165early_init_dt_scan_root()用于初始化DT地址空间模型,early_init_dt_scan_memory()166用于确定可用RAM的大小和位置。167 168在ARM上,函数setup_machine_fdt()负责在选择支持板子的正确machine_desc169后,对设备树进行早期扫描。170 1712.4 设备数量172------------173在电路板被识别后,在早期配置数据被解析后,内核初始化可以以正常方式进行。在174这个过程中的某个时刻,unflatten_device_tree()被调用以将数据转换成更有175效的运行时表示。这也是调用机器特定设置钩子的时候,比如ARM上的machine_desc176.init_early()、.init_irq()和.init_machine()钩子。本节的其余部分使用177了ARM实现的例子,但所有架构在使用DT时都会做几乎相同的事情。178 179从名称上可以猜到,.init_early()用于在启动过程早期需要执行的任何机器特定设180置,而.init_irq()则用于设置中断处理。使用DT并不会实质性地改变这两个函数的181行为。如果提供了DT,那么.init_early()和.init_irq()都能调用任何一个DT查182询函数(of_* in include/linux/of*.h),以获得关于平台的额外数据。183 184DT上下文中最有趣的钩子是.init_machine(),它主要负责将平台的数据填充到185Linux设备模型中。历史上,这在嵌入式平台上是通过在板卡support .c文件中定186义一组静态时钟结构、platform_devices和其他数据,并在.init_machine()中187大量注册来实现的。当使用DT时,就不用为每个平台的静态设备进行硬编码,可以通过188解析DT获得设备列表,并动态分配设备结构体。189 190最简单的情况是,.init_machine()只负责注册一个platform_devices。191platform_device是Linux使用的一个概念,用于不能被硬件检测到的内存或I/O映192射的设备,以及“复合”或 “虚拟”设备(后面会详细介绍)。虽然DT没有“平台设备”的193术语,但平台设备大致对应于树根的设备节点和简单内存映射总线节点的子节点。194 195现在是举例说明的好时机。下面是NVIDIA Tegra板的设备树的一部分::196 197  /{198	compatible = "nvidia,harmony", "nvidia,tegra20";199	#address-cells = <1>;200	#size-cells = <1>;201	interrupt-parent = <&intc>;202 203	chosen { };204	aliases { };205 206	memory {207		device_type = "memory";208		reg = <0x00000000 0x40000000>;209	};210 211	soc {212		compatible = "nvidia,tegra20-soc", "simple-bus";213		#address-cells = <1>;214		#size-cells = <1>;215		ranges;216 217		intc: interrupt-controller@50041000 {218			compatible = "nvidia,tegra20-gic";219			interrupt-controller;220			#interrupt-cells = <1>;221			reg = <0x50041000 0x1000>, < 0x50040100 0x0100 >;222		};223 224		serial@70006300 {225			compatible = "nvidia,tegra20-uart";226			reg = <0x70006300 0x100>;227			interrupts = <122>;228		};229 230		i2s1: i2s@70002800 {231			compatible = "nvidia,tegra20-i2s";232			reg = <0x70002800 0x100>;233			interrupts = <77>;234			codec = <&wm8903>;235		};236 237		i2c@7000c000 {238			compatible = "nvidia,tegra20-i2c";239			#address-cells = <1>;240			#size-cells = <0>;241			reg = <0x7000c000 0x100>;242			interrupts = <70>;243 244			wm8903: codec@1a {245				compatible = "wlf,wm8903";246				reg = <0x1a>;247				interrupts = <347>;248			};249		};250	};251 252	sound {253		compatible = "nvidia,harmony-sound";254		i2s-controller = <&i2s1>;255		i2s-codec = <&wm8903>;256	};257  };258 259在.init_machine()时,Tegra板支持代码将需要查看这个DT,并决定为哪些节点260创建platform_devices。然而,看一下这个树,并不能立即看出每个节点代表什么261类型的设备,甚至不能看出一个节点是否代表一个设备。/chosen、/aliases和262/memory节点是信息节点,并不描述设备(尽管可以说内存可以被认为是一个设备)。263/soc节点的子节点是内存映射的设备,但是codec@1a是一个i2c设备,而sound节264点代表的不是一个设备,而是其他设备是如何连接在一起以创建音频子系统的。我知265道每个设备是什么,因为我熟悉电路板的设计,但是内核怎么知道每个节点该怎么做?266 267诀窍在于,内核从树的根部开始,寻找具有“兼容”属性的节点。首先,一般认为任何268具有“兼容”属性的节点都代表某种设备;其次,可以认为树根的任何节点要么直接连269接到处理器总线上,要么是无法用其他方式描述的杂项系统设备。对于这些节点中的270每一个,Linux都会分配和注册一个platform_device,它又可能被绑定到一个271platform_driver。272 273为什么为这些节点使用platform_device是一个安全的假设?嗯,就Linux对设备274的建模方式而言,几乎所有的总线类型都假定其设备是总线控制器的孩子。例如,每275个i2c_client是i2c_master的一个子节点。每个spi_device都是SPI总线的一276个子节点。类似的还有USB、PCI、MDIO等。同样的层次结构也出现在DT中,I2C设277备节点只作为I2C总线节点的子节点出现。同理,SPI、MDIO、USB等等。唯一不需278要特定类型的父设备的设备是platform_devices(和amba_devices,但后面会279详细介绍),它们将愉快地运行在Linux/sys/devices树的底部。因此,如果一个280DT节点位于树的根部,那么它真的可能最好注册为platform_device。281 282Linux板支持代码调用of_platform_populate(NULL, NULL, NULL, NULL)来283启动树根的设备发现。参数都是NULL,因为当从树的根部开始时,不需要提供一个起284始节点(第一个NULL),一个父结构设备(最后一个NULL),而且我们没有使用匹配285表(尚未)。对于只需要注册设备的板子,除了of_platform_populate()的调用,286.init_machine()可以完全为空。287 288在Tegra的例子中,这说明了/soc和/sound节点,但是SoC节点的子节点呢?它们289不应该也被注册为平台设备吗?对于Linux DT支持,一般的行为是子设备在驱动290.probe()时被父设备驱动注册。因此,一个i2c总线设备驱动程序将为每个子节点291注册一个i2c_client,一个SPI总线驱动程序将注册其spi_device子节点,其他292总线类型也是如此。根据该模型,可以编写一个与SoC节点绑定的驱动程序,并简单293地为其每个子节点注册platform_device。板卡支持代码将分配和注册一个SoC设294备,一个(理论上的)SoC设备驱动程序可以绑定到SoC设备,并在其.probe()钩295中为/soc/interruptcontroller、/soc/serial、/soc/i2s和/soc/i2c注296册platform_devices。很简单,对吗?297 298实际上,事实证明,将一些platform_device的子设备注册为更多的platform_device299是一种常见的模式,设备树支持代码反映了这一点,并使上述例子更简单。300of_platform_populate()的第二个参数是一个of_device_id表,任何与该表301中的条目相匹配的节点也将获得其子节点的注册。在Tegra的例子中,代码可以是302这样的::303 304  static void __init harmony_init_machine(void)305  {306	/* ... */307	of_platform_populate(NULL, of_default_bus_match_table, NULL, NULL);308  }309 310“simple-bus”在Devicetree规范中被定义为一个属性,意味着一个简单的内存映射311的总线,所以of_platform_populate()代码可以被写成只是假设简单总线兼容的节312点将总是被遍历。然而,我们把它作为一个参数传入,以便电路板支持代码可以随时覆313盖默认行为。314 315[需要添加关于添加i2c/spi/etc子设备的讨论] 。316 317附录A:AMBA设备318---------------319 320ARM Primecell是连接到ARM AMBA总线的某种设备,它包括对硬件检测和电源管理321的一些支持。在Linux中,amba_device和amba_bus_type结构体被用来表示322Primecell设备。然而,棘手的一点是,AMBA总线上的所有设备并非都是Primecell,323而且对于Linux来说,典型的情况是amba_device和platform_device实例都是同324一总线段的同义词。325 326当使用DT时,这给of_platform_populate()带来了问题,因为它必须决定是否将327每个节点注册为platform_device或amba_device。不幸的是,这使设备创建模型328变得有点复杂,但解决方案原来并不是太具有侵略性。如果一个节点与“arm,primecell”329兼容,那么of_platform_populate()将把它注册为amba_device而不是330platform_device。331