zoukankan      html  css  js  c++  java
  • Linux and the Device Tree

    Linux and the Device Tree
    ------------------------- 
     
    The Linux usage model for device tree data
    本文阅读翻译自linux内核的说明文档usage-model.txt
     
    本文讲述linux如何使用device tree,关于device tree数据类型的详细描述可以参考文档:
    或者,无法FQ的话,
     
    The "Open Firmware Device Tree", or simply Device Tree (DT)是一种描述硬件的数据结构或者说语言, 更具体地说,DT是一种操作系统可读的硬件描述语言,这样操作系统就不必把硬件详细信息硬编码到代码里。
     
    从结构上来讲,DT是一种树形结构,或者说一种带有命名节点的非循环(acyclic)图。每一个节点,可能有任意个命名属性和任意数据的键值对。也有一种机制,可以让一个节点和树形结构之外的节点建立连接。
     
    从概念上来讲,有一种名为绑定(bindings)的通用的约定来描述,DT的数据如何来描述物理硬件的信息,数据总线、中断线、GPIO连接和外围设备。
     
    在实际操作中,硬件信息应该尽可能的用已经存在的“绑定”来描述,以便最大化的利用现有的代码,但是实际上“属性”和“节点”都是简单的文本字符串,它可以很容易的用来扩展现有的“绑定”或者创建一种新的“绑定”。不管如何,创建一种新的“绑定”需要非常小心,在这之前,一定要先做做功课,了解现有的“绑定”有哪些。现在就有两种关于i2c总线的描述方法,这是因为一种新的i2c设备描述方法创建之前,没有去调查i2c设备已经如何在系统中描述。
     
    1 History

    DT最初被OPEN Firmware创建,作为一种从OPEN Firmware传输数据到客户端程序(比如一个操作系统)的传输方法的一部分。操作系统利用DT来实时的获取硬件拓扑结构,从而可以支持多数的硬件设备,而不用将硬件信息硬编码到代码中。(假设驱动程序可用于所有设备)
     
    由于Open Firmware被广泛应用有PowerPC和SPARC平台,linux早已长期使用设备树(Device Tree)来支持这些体系结构。
     
    在2005年,当PowerPC linux开始一次重大的代码清理以合并对32-bit和64bit的支持。当时决定所有的PowerPC平台都需要支持DT,而不管它是否使用了Open Firmware。为了达到这一目标,一个被称为FDT(Flattened Device Tree)的DT版本被设计出来,它可以以二进制的形式传给kernel,而不需要一个真正的Open Firmware。uboot、kexec或者其他bootloaders也都通过修改,支持了传递二进制形式的DT文件(DTB)和在启动阶段修改dtb文件。DT也添加到了PowerPC的引导启动包装器( arch/powerpc/boot/*)中,所以dtb文件也可以被打包到kernel image文件中来支持在启动阶段没有DT的固件
     
    在一段时间以后,FDT已经应用到了所有的架构。在本文编写的时候,6个主线架构( arm, microblaze, mips, powerpc, sparc, and x86)和一个非主线架构(nios)都在一定程度支持了DT。
     
    2 Data Model
     
    如果你还没有读过DT的使用说明(https://elinux.org/Device_Tree_Usage),那么赶紧去读吧!
    请读完之后,在继续阅读本文。
     
    2.1 High Level View
    理解DT,最重要的事情是,DT就是一种简单的,用来描述硬件信息用的数据数据结构。它没有什么魔力,更没有什么魔法来解决所有的硬件配置问题。它能做的,就是提供一种语言,让板级硬件配置和linux内核(或其他支持DT的操作系统)中支持的设备驱动去耦。使用DT,能让对板卡和设备的支持变成数据驱动;在启动过程中的一些决策将基于传入kernel的数据,而不是硬编码到内核本身。
     
    理想情况下,数据驱动的平台启动方式能够减少kernel中的代码拷贝,能够用简简单单一个kernel image来支持很多的不同硬件设备。
     
    linux使用DT数据有三个主要的目的:
    1)平台识别
    2)运行时配置
    3)设备信息管理
     
    2.2 平台识别
     
    首先,kernel使用DT数据来识别具体的机器型号。在完美的世界里,由于所有的平台细节都能由DT以一种一致且可靠的方式完美描述,特定的平台类型对于kernel来说就不那么重要了。但是硬件不那么完美,所以kernel必须在启动的早期就能识别机器的型号,从而能够有机会运行特定机器对应的特定代码。
     
    在大多数情况下,机器型号识别都是硬件无关的,kernel将根据机器的核心CPU或者Soc来选择setup代码。以ARM为例,函数setup_arch( arch/arm/kernel/setup.c)调用函数 setup_machine_fdt( arch/arm/kernel/devicetree.c )来搜索设备描述表( machine_desc),选择出与DT数据最匹配的设备型号。它通过将DT数据的根节点的  'compatible' 属性与 machine_desc结构体的 dt_compat字段来进行比较,来找到最匹配的设备型号。
     
    'compatible'属性包含一个有序的字符串列表,字符串以准确的机器名字为起始,然后跟着的是一个可选的板卡列表,按照匹配性的高低来排列。比如说,Ti BeagleBoard以及其后续版本  BeagleBoard xM board的根'compatible'属性可能可以这样来写:
    compatible = "ti,omap3-beagleboard", "ti,omap3450", "ti,omap3";
    compatible = "ti,omap3-beagleboard-xm", "ti,omap3450", "ti,omap3";
     
    这里的"ti,omap3-beagleboard-xm"指定了准确的设备型号,它也表明它与 OMAP 3450 SoC兼容,属于ti的omap3系列Soc。 您会注意到,这个compatible列表是从最特定的(确切的板)到最不特定的(SoC族)排序的。
     
    精明的读者可能会指出,BeagleBoard xM board也可以声明与原始版本的BeagleBoard相匹配兼容的。然而,在板卡级别这样做必须非常小心谨慎,因为通常情况下,即使在同一产品线中,从一个板到另一个板之间也会有很大程度的变化,当一个板声称与另一个板兼容时,很难确切地确定这意味着什么。从更高层次看,宁可谨慎行事,也不要声称一个板卡与另一个板卡兼容。值得注意的例外是,一个板卡是另一个的载板时,比如一个附加在载板的CPU模块。
     
    关于“ compatible”,还有一个值得注意事项是, compatible属性中使用任何字符串都必须根据它所指示的内容进行记录,并将“ compatible”字符串归档。(Documentation/devicetree/bindings)
     
    仍然以ARM为例,对于每一项 machine_desc,内核都会判断其 dt_compat是否出现在“ compatible”属性中。如果在,则该machine_desc将作为设备启动的一个候选。在搜索了整个machine_desc描述表之后,函数 setup_machine_fdt返回最匹配的machine_desc。如果没有匹配的machine_desc,则函数返回NULL。
     
    这种方案背后的原因是,大多数情况下,一个machine_desc可以支持很多种类的板卡,如果他们使用同样的Soc,或者同系列的Socs。然儿,总会有一些例外情况,特定的板卡需要特殊的启动代码,而这些启动代码在通常版本下是没有用的。当然,也可以在通用的代码中显示地检查板卡来处理特殊情况,但是这样会迅速的让代码变得ugly和不可维护,如果特殊情况不止一两处的话。
     
    相反,兼容性(“ compatible”)列表通过在dt_compat列表中指定“不兼容”值来支持广泛的通用板卡集。在上面的例子中,通用板卡的支持,可以把“ compatible”属性声明为 "ti,omap3" or "ti,omap3450"。如果在最初的 beagleboard板卡上发现有问题,需要在早期启动阶段,加载一些特殊的代码来解决问题。 那么可以添加一个新的machine_desc,来实现这些代码,并且其属性只与 "ti,omap3-beagleboard"匹配。
    PowerPC使用一种稍微不同的方案,它从每个machine_desc调用.probe()钩子,并使用返回TRUE的第一个。然而,这种方法无法实现兼容性(“ compatible”)列表的优先级,因此,新的体系结构可能应该避免使用这种方法。
     
    2.3 Runtime configuration
     
    在大部分情况下,DT是固件和内核之间数据通讯的唯一方法,因此也被用来传输运行时配置数据,比如内核参数字符串和initrd镜像的位置。
     
    这种数据大部分被包含在/chosen节点下,在linux启动中,运行时配置参数,经常如下面的代码所示:
        chosen { bootargs = "console=ttyS0,115200 loglevel=8";
            initrd-start = <0xc8000000>;
            initrd-end = <0xc8200000>; };
     
    bootargs属性包含内核参数,以及initrd-*属性定义了initrd镜像的地址和大小。值得注意的是,initrd-end是initrd镜像之后的第一个地址,所以这里也与通常的结构体资源语义不同。chosen节点还可以为平台特定的配置数据,包含任意数量的额外属性(可选)。
     
    在早期的引导阶段,在分页初始化完成之前,架构设置代码使用不同的回调函数调用of_scan_flat_dt函数来解析设备树数据(device tree data)。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()函数负责在选择了正确的machine_desc之后,对设备树进行早期的扫描。
     
    2.4 Device population
     
    在完成了板卡识别,以及早期配置数据解析之后, 内核初始化就可以以正常的方式进行了。在这个过程的某些时候,unflatten_device_tree()函数被用来将DT数据转换成运行时效率更高的形式。这时,machine-specific的配置回调函数也会被调用,比如ARM架构的machine_desc .init_early()函数,.init_irq() 和 .init_machine() 回调函数。 本节的其余部分将使用来自ARM实现的示例,但是在使用DT时,所有架构都将做几乎相同的事情。
    顾名思义,.init_early()函数的作用是,执行一些板卡特定的,需要在启动过程的早期执行的配置项,.init_irq()函数是用于配置中断处理。使用DT并不会实质性的改变这些函数的行为。如果提供了DT,则.init_early() 和 .init_irq()函数能够调用任意DT获取函数(of_* in include/linux/of*.h)来从平台获取额外的数据。
     
    在DT使用过程中,最有趣的回调函数是.init_machine(),它主要负责用平台有关的数据,管理linux设备模型。过去,这个函数在嵌入式平台,是通过在板级支持.c文件中,定义一系列的静态时钟结构体,platform_devices以及其他数据,并大量的注册来完成的。在使用了DT数据之后,就不再为每个平台硬编码描述静态设备,设备列表可以通过解析DT数据获取,然后动态申请设备结构体。
     
    最简单的例子是 .init_machine() 函数只负责注册platform_devices块,platform_device是Linux中的一个概念,用于表示内存或者I/O映射设备(硬件无法检测到这些设备)以及“复合”或“虚拟”设备(稍后将详细介绍)。但是DT中并没有platform_device的这样的术语,platform_device大致对应于设备树的根节点以及简单内存映射总线节点的子节点。
     
    现在是时候举出一个例子了,下面是NVIDIA Tegra板卡的DT的一部分:
    /{ 
        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。然后,从设备树(Device Tree),并不能直接看出节点所代表的设备类型,甚至是否代表设备都不能判断。“/chosen”,“/aliases”,“/memory”节点是信息性的节点,并不描述设备(尽管内存也可能被考虑为设备)。/soc节点的子节点是内存映射设备,但是“codec@1a”是一个i2c设备,而“sound”节点不代表设备, 而是其他设备如何连接在一起来创建音频子系统。我知道每个设备是什么,是因为我对板卡硬件设计很熟悉,但是linux kernel是如何知道为每个节点做什么呢?
     
    诀窍是kernel从设备树的根节点开始,寻找带有“compatible”属性的节点。首先,通常假设,节点带有的“compatible”属性表述它代表了哪种设备;第二,可以这样假定,任何设备树(Device Tree)的根下的节点都直接连接到处理器总线,或者是其他系统设备(不能以其他方式描述)。 对于每个这种节点,Linux分配并注册一个platform_device,而该设备又可能绑定到一个platform_driver。
    为什么对这些节点使用platform_device是一个安全的假设?因为,对于linux设备模型,几乎所有总线类型都假设它的设备是总线控制器的子设备。例如,每个i2c_client都是i2c_master的子设备。每一个spi_device是SPI总线的子设备。USB、PCI、MDIO等都是如此。同样的层级结构在DT中是如此,I2C设备节点只会以i2c总线结点的子节点的形式出现。SPI、MDIO、USB等总线也是如此。唯一不需要特定的父设备的是platform_device(和amba_devices,稍后会详细介绍),它位于linux的设备树的/sys/devices。因此,如果一个DT节点在设备树(Device Tree)的根路径下, 那么很可能需要将其注册为platform_device。
    linux板卡支持代码调用of_platform_populate(NULL, NULL, NULL, NULL)函数,来启动DT的根设备发现。所有的参数都是NULL,因为从DT的根路径开始查找,所以不需要提供起始的节点(第一个NULL),父设备的结构体(最后一个NULL),而且我们也不需要一个匹配表。 对于只需要注册设备的板卡,.init_machine()可以完全为空,除了调用of_platform_populate()以外。
    在Tegra的例子里,这是指"/soc"和“/sound”节点,但是“/soc”节点的子节点呢? 它们不应该也注册为平台设备吗? 以linux对设备树的支持,通常的方式是,子设备的注册在父设备驱动的.probe函数中完成。所以i2c总线驱动将为每一个子节点,注册一个i2c_client,一个spi总线驱动也把子节点注册为spi设备,其他的总线类型也是如此。根据这种模型,可以为绑定到"/soc"的节点编写驱动,并把它的子节点都注册为platform_devices。板卡支持代码将分配和注册一个Soc设备,一个(理论上的)Soc设备驱动可以绑定到Soc设备,在回调函数.probe中将 /soc/interrupt-controller, /soc/serial, /soc/i2s, /soc/i2c注册为 platform_device。很容易,对吧!
    实际上,将一些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”表示一种简单的内存映射总线,定义在 ePAPR 1.0规范中,所以  of_platform_populate() 函数,可以假设“simple-bus”兼容的节点始终会被遍历到。然而,我们将它作为参数传入, 这样board support代码就可以始终覆盖默认行为。
    【 需要添加关于添加i2c/spi/etc子设备的讨论】
     
    附录 A: AMBA devices

    ARM Primecells是一种附加在ARM AMBA总线上的设备,它包含一些对硬件检测和电源控制的支持。 在Linux中,结构体amba_device和amba_bus_type用于表示Primecell设备。 然而,棘手的一点是,AMBA总线上的所有设备不都是primecell,对于Linux系统而言, amba_device和platform_device实例通常是同一总线上的兄弟节点。在使用设备树时,这会给of_platform_populate()带来问题,因为它必须决定是将每个节点注册为platform_device还是amba_device。 不幸的是,这会使设备模型的创建变得有点复杂,但是解决方案并不复杂。如果一个节点与“arm,amba-primecell”兼容,那么of_platform_populate()函数将把它注册为amba_device,而非platform_device。
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
  • 相关阅读:
    ZAB 和 Paxos 算法的联系与区别?
    Spring支持的ORM?
    解释对象/关系映射集成模块?
    哪种依赖注入方式你建议使用,构造器注入,还是 Setter方法注入?
    我们能自己写一个容器类,然后使用 for-each 循环码?
    Struts2的Action中获取request对象的几种方式?
    Chroot 特性?
    String是最基本的数据类型吗?
    @Autowired 注解?
    比较HQL、Criteria、Native-SQL这三者做查询的区别,以及应该如何进行选择?
  • 原文地址:https://www.cnblogs.com/djw316/p/10297042.html
Copyright © 2011-2022 走看看