00-Linux设备树系列-简介


设备树数据的Linux使用模型

作者:Grant Likely [email protected]

本文介绍Linux如何使用设备树。概述
设备树相关的数据格式的资料可以在设备树使用页面 elinux.org [1] 上找到。

“开放固件设备树”或简称设备树(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。为此,DT表示Flattened Device Tree(FDT)被创建了,它可以作为二进制blob文件传递给内核,而无需真正的Open Firmware代码实现。U-Boot、kexec和其他bootloader被修改为在启动时支持传递一个设备树二进制(dtb)给kernel以及修改dtb的功能。DT还被添加到PowerPC的启动包装器(arch / powerpc / boot /*)中,可以使内核映像包括dtb以支持现有的不支持引导DTB的BootLoader。

现如今,FDT基础设施被推广为可以使用到所有架构。在撰写本文时,主要是6个架构(arm,microblaze,mips,powerpc,sparc和x86)和主线(nios)中的1个具有一定程度的DT支持。

2.数据模型


如果您尚未阅读页面设备树使用页面 elinux.org [1] ,然后去看看吧。没关系,我等一下……

2.1高级视图


最重要的是要理解DT只是一个描述硬件的数据结构。它没有什么神奇的,并没有神奇地使所有硬件配置问题走开。
它所做的是提供一种解耦的语言将电路板的硬件配置与Linux内核(或任何启动操作系统)设备驱动程序进行解耦。运用
它允许电路板和设备相关的支持功能成为数据驱动型的;操作系统根据传入内核的数据而不是提前硬编码的结构来做出决策。

理想情况下,使用单个内核映像,数据驱动的平台模型应该会减少代码的重复并使其更容易支持各种硬件。

Linux将DT数据用于三个主要目的:

  • 1)平台识别,
  • 2)运行时配置,和
  • 3)设备管理。

2.2平台识别


首先,内核将使用DT中的数据来识别具体机器。在完美的世界中,特定的平台不应该对内核很重要,因为所有的平台细节描述都通过设备以一种一致和可靠的方式完美的表现出来。硬件并不完美,因此内核必须在启动早期识别机器,以便它有机会运行对于机器特定的修正。

在大多数情况下,机器身份是无关紧要的,内核将改为根据CPU或SoC选择设置机器的核心代码。例如,在ARM上,arch/arm/kernel/setup.c中的setup_arch()将调用arch/arm/kernel/devicetree.c中的setup_machine_fdt(),它搜索整个machine_desc
表并选择与设备树最匹配的machine_desc数据。它通过查看根设备树节点中的’兼容’属性来确定最佳匹配,并将其与
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电路板的兼容性。但是,如果在电路板级别这样做会被警告
,因为通常从一块板到另一块板的存在一个高层次的变化,即使在同一电路板的产品线内,当一个电路板声明与另一个兼容
时,也很难准确的说出两者具体的含义。对于顶级配置,最好在谨慎方面犯错,而不是要求一个电路板与另一个兼容。值得
注意的例外是一个board是另一个的载体,例如附加到一个电路上的CPU模块。

关于兼容的另一个需要注意的地方。任何应用到兼容属性中的字符串必须记录其具体代表的含义。
在Documentation/devicetree/bindings中添加中兼容字符串相关的文档。

再一次,ARM平台,对于每个machine_desc,内核会查看所有出现在compatible属性中的dt_compat列表条目。
如果有,那么machine_desc便会作为驱动该机器的候选者。搜索完整个machine_descs表后,setup_machine_fdt()返回
“最兼容”的machine_desc,其依据是machine_desc与兼容属性字段是否完全匹配。如果找不到匹配的machine_desc,则返回
NULL。

这个计划背后的原因是大多数人的经验,例如,如果它们都使用相同的SoC或同一系列的SoC,单个machine_desc可以支持大
量的电路板。然而,特定电路板会有一些例外情况,其需要一些在通常情况下无用的特殊配置代码。该特殊特殊情况可以通
过在通用的启动配置带中明确检查来处理,但很快就这样做了,并且其不仅仅是几个,那么其会变成丑陋或不可维护案例。

相反,兼容列表允许通用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的通常语义不匹配。 所选节点还可以可选地包含用于平台特定配置数据的任意数量的附加属性。

在早期启动期间,架构设置代码使用不同的帮助程序回调多次调用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设备数量(Device population)


在识别出板之后,并且在解析了早期配置数据之后,内核初始化可以以正常方式进行。 在此过程的某个时刻,调用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设备,声音节点不是设备,而是其他设备如何连接在一起以创建音频子系统。 我知道每个设备是什么,因为我熟悉电路板设计,但内核如何知道如何处理每个节点?

诀窍是内核从树的根开始,并寻找具有“兼容”属性的节点。首先,通常假设具有“兼容”属性的任何节点表示某种设备,其次,可以假设树根处的任何节点直接连接到处理器总线,或者是杂项系统设备,不能以任何其他方式描述。对于每个节点,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。 板支持代码将分配和注册SoC器件,(理论上)SoC器件驱动器可以绑定到SoC器件,并为/soc/interrupt-controller,/soc/serial,/soc/i2s和/soc注册platform_devices/2c在.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,NULLNULL);
}

“simple-bus”在ePAPR 1.0规范中定义为一个属性,意思是一个简单的内存映射总线,因此可以编写of_platform_populate()代码,假设总是遍历简单总线兼容节点。 但是,我们将其作为参数传递,以便板支持代码始终可以覆盖默认行为。

[需要添加关于添加i2c/spi/etc子设备的讨论]

附录A:AMBA设备


ARM Primecells是连接到ARM AMBA总线的某种设备,包括对硬件检测和电源管理的一些支持。 在Linux中,struct amba_device和amba_bus_type用于表示Primecell设备。 然而,令人费解的是,并非AMBA总线上的所有设备都是Primecells,而对于Linux,amba_device和platform_device实例通常都是同一总线段的兄弟。

使用DT时,这会给of_platform_populate()带来问题,因为它必须决定是将每个节点注册为platform_device还是amba_device。 不幸的是,这有点使设备创建模型复杂化,但解决方案结果并非过于侵入。 如果节点与“arm,amba-primecell”兼容,那么of_platform_populate()会将其注册为amba_device而不是platform_device。

[1] https://elinux.org/Device_Tree_Usage

猜你喜欢

转载自blog.csdn.net/linux_embedded/article/details/82387486