Linux and the Device Tree

Stella981
• 阅读 709

Linux and the Device Tree

------------------------- 

The Linux usage model for device tree data

本文阅读翻译自linux内核的说明文档usage-model.txt

本文讲述linux如何使用device tree,关于device tree数据类型的详细描述可以参考文档:

http://devicetree.org/Device_Tree_Usage

或者,无法翻墙的话,

https://elinux.org/Device_Tree_Usage

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。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
Python之time模块的时间戳、时间字符串格式化与转换
Python处理时间和时间戳的内置模块就有time,和datetime两个,本文先说time模块。关于时间戳的几个概念时间戳,根据1970年1月1日00:00:00开始按秒计算的偏移量。时间元组(struct_time),包含9个元素。 time.struct_time(tm_y
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这