Linux设备树解析

1. Device Tree简介
Linus Torvalds在2011年3月17日的ARM Linux邮件列表宣称“this whole ARM thing is a fucking pain in the ass”,引发ARM Linux社区的地震,随后ARM社区进行了一系列的重大修正。在过去的ARM Linux中,arch/arm/plat-xxx和arch/arm/mach-xxx中充斥着大量的垃圾代码,相当多数的代码只是在描述板级细节,而这些板级细节对于内核来讲,不过是垃圾,如板上的platform设备、resource、i2c_board_info、spi_board_info以及各种硬件的platform_data。 社区必须改变这种局面,于是PowerPC等其他体系架构下已经使用的Flattened Device Tree(FDT)进入ARM社区的视野。Device Tree是一种描述硬件的数据结构,它起源于OpenFirmware(OF)。在Linux2.6中,ARM架构的板极硬件细节过多地被硬编码在arch/arm/plat-xxx和arch/arm/mach-xxx,采用Device Tree后,许多硬件的细节可以直接透过它传递给Linux,而不再需要在kernel中进行大量的冗余编码。

Device Tree由一系列被命名的结点(node)和属性(property)组成,而结点本身可包含子结点。所谓属性,其实就是成对出现的name和value。在Device Tree中,可描述的信息包括(原先这些信息大多被hard code到kernel中):

CPU的数量和类别
内存基地址和大小
总线和桥
外设连接
中断控制器和中断使用情况
GPIO控制器和GPIO使用情况
它基本上就是画一棵电路板上CPU、总线、设备组成的树,Bootloader会将这棵树传递给内核,然后内核可以识别这棵树,并根据它展开出Linux内核中的platform_device、i2c_client、spi_device等设备。这些设备用到的内存、IRQ等资源,也被传递给了kernel,kernel会将这些资源绑定给展开的相应的设备。
 

2. Device Tree编译
Device Tree文件的格式为dts,包含的头文件格式为dtsi,dts文件是一种人可以看懂的编码格式。但是uboot和linux不能直接识别,他们只能识别二进制文件,所以需要把dts文件编译成dtb文件。dtb文件是一种可以被kernel和uboot识别的二进制文件。把dts编译成dtb文件的工具是dtc。Linux源码目录下scripts/dtc目录包含dtc工具的源码。在Linux的scripts/dtc目录下除了提供dtc工具外,也可以自己安装dtc工具,linux下执行:sudo apt-get install device-tree-compiler安装dtc工具。其中还提供了一个fdtdump的工具,可以反编译dtb文件。dts和dtb文件的转换如图1所示。
dtc工具的使用方法是:dtc –I dts –O dtb –o xxx.dtb xxx.dts,即可生成dts文件对应的dtb文件了。
图1 dts和dtb文件转换

3. Device Tree头信息
fdtdump工具使用,Linux终端执行ftddump –h,输出以下信息:
fdtdump -h
Usage: fdtdump [options]
Options: -[dshV]
-d, –debug Dump debug information while decoding the file
-s, –scan Scan for an embedded fdt in file
-h, –help Print this help and exit
-V, –version Print version and exit
本文采用s5pv21_smc.dtb文件为例说明fdtdump工具的使用。Linux终端执行fdtdump –sd s5pv21_smc.dtb > s5pv21_smc.txt,打开s5pv21_smc.txt文件,部分输出信息如下所示:
// magic: 0xd00dfeed
// totalsize: 0xce4 (3300)
// off_dt_struct: 0x38
// off_dt_strings: 0xc34
// off_mem_rsvmap: 0x28
// version: 17
// last_comp_version: 16
// boot_cpuid_phys: 0x0
// size_dt_strings: 0xb0
// size_dt_struct: 0xbfc
以上信息便是Device Tree文件头信息,存储在dtb文件的开头部分。在Linux内核中使用struct fdt_header结构体描述。struct fdt_header结构体定义在scripts\dtc\libfdt\fdt.h文件中。
 

struct fdt_header {
    fdt32_t magic;               /* magic word FDT_MAGIC */
    fdt32_t totalsize;           /* total size of DT block */
    fdt32_t off_dt_struct;       /* offset to structure */
    fdt32_t off_dt_strings;      /* offset to strings */
    fdt32_t off_mem_rsvmap;      /* offset to memory reserve map */
    fdt32_t version;                 /* format version */
    fdt32_t last_comp_version;   /* last compatible version */

    /* version 2 fields below */
    fdt32_t boot_cpuid_phys;     /* Which physical CPU id we're booting on */
    /* version 3 fields below */
    fdt32_t size_dt_strings;     /* size of the strings block */

    /* version 17 fields below */
    fdt32_t size_dt_struct;      /* size of the structure block */
};
fdtdump工具的输出信息即是以上结构中每一个成员的值,struct fdt_header结构体包含了Device Tree的私有信息。例如: fdt_header.magic是fdt的魔数,固定值为0xd00dfeed,fdt_header.totalsize是fdt文件的大小。使用二进制工具打开s5pv21_smc.dtb验证。s5pv21_smc.dtb二进制文件头信息如图2所示。从图2中可以得到Device Tree的文件是以大端模式储存。并且,头部信息和fdtdump的输出信息一致。
图2 头信息

 Device Tree中的节点信息举例如图3所示。

图3 设备树全景试图

 上述.dts文件并没有什么真实的用途,但它基本表征了一个Device Tree源文件的结构。1个root结点”/”;root结点下面含一系列子结点,本例中为”node@0”和”node@1”;结点”node@0”下又含有一系列子结点,本例中为”child-node@0”;各结点都有一系列属性。这些属性可能为空,如” an-empty-property”;可能为字符串,如”a-string-property”;可能为字符串数组,如”a-string-list-property”;可能为Cells(由u32整数组成),如”second-child-property”,可能为二进制数,如”a-byte-data-property”。Device Tree源文件的结构分为header、fill_area、dt_struct及dt_string四个区域。header为头信息,fill_area为填充区域,填充数字0,dt_struct存储节点数值及名称相关信息,dt_string存储属性名。例如:a-string-property就存储在dt_string区,”A string”及node1就存储在dt_struct区域。
我们可以给一个设备节点添加lable,之后可以通过&lable的形式访问这个lable,这种引用是通过phandle(pointer handle)进行的。例如,图3中的node1就是一个lable,node@0的子节点child-node@0通过&node1引用node@1节点。像是这种phandle的节点,在经过DTC工具编译之后,&node1会变成一个特殊的整型数字n,假设n值为1,那么在node@1节点下自动生成两个属性,属性如下:
linux,phandle = <0x00000001>;
phandle = <0x00000001>;
node@0的子节点child-node@0中的a-reference-to-something = <&node1>会变成a-reference-to-something = < 0x00000001>。此处0x00000001就是一个phandle得值,每一个phandle都有一个独一无二的整型值,在后续kernel中通过这个特殊的数字间接找到引用的节点。通过查看fdtdump输出信息以及dtb二进制文件信息,得到struct fdt_header和文件结构之间的关系信息如所示。
图4 struct fdt_header和文件结构之间的关系

4. Device Tree文件结构
通过以上分析,可以得到Device Tree文件结构如图5所示。dtb的头部首先存放的是fdt_header的结构体信息,接着是填充区域,填充大小为off_dt_struct – sizeof(struct fdt_header),填充的值为0。接着就是struct fdt_property结构体的相关信息。最后是dt_string部分。
 

图5 Device Tree文件结构

Device Tree源文件的结构分为header、fill_area、dt_struct及dt_string四个区域。fill_area区域填充数值0。节点(node)信息使用struct fdt_node_header结构体描述。属性信息使用struct fdt_property结构体描述。各个结构体信息如下:
 

struct fdt_node_header {
    fdt32_t tag;
    char name[0];
};

struct fdt_property {
    fdt32_t tag;
    fdt32_t len;
    fdt32_t nameoff;
    char data[0];
};
 

struct fdt_node_header描述节点信息,tag是标识node的起始结束等信息的标志位,name指向node名称的首地址。tag的取值如下:

#define FDT_BEGIN_NODE  0x1     /* Start node: full name */
#define FDT_END_NODE    0x2     /* End node */
#define FDT_PROP        0x3     /* Property: name off, size, content */
#define FDT_NOP         0x4     /* nop */
#define FDT_END         0x9
 

FDT_BEGIN_NODE和FDT_END_NODE标识node节点的起始和结束,FDT_PROP标识node节点下面的属性起始符,FDT_END标识Device Tree的结束标识符。因此,对于每个node节点的tag标识符一般为FDT_BEGIN_NODE,对于每个node节点下面的属性的tag标识符一般是FDT_PROP。
描述属性采用struct fdt_property描述,tag标识是属性,取值为FDT_PROP;len为属性值的长度(包括‘\0’,单位:字节);nameoff为属性名称存储位置相对于off_dt_strings的偏移地址。
例如:compatible = “samsung,goni”, “samsung,s5pv210”;compatible是属性名称,”samsung,goni”, “samsung,s5pv210”是属性值。compatible属性名称字符串存放的区域是dt_string。”samsung,goni”, “samsung,s5pv210”存放的位置是fdt_property.data后面。因此fdt_property.data指向该属性值。fdt_property.tag的值为属性标识,len为属性值的长度(包括‘\0’,单位:字节),此处len = 29。nameoff为compatible字符串的位置相对于off_dt_strings的偏移地址,即&compatible = nameoff + off_dt_strings。
dt_struct在Device Tree中的结构如图6所示。节点的嵌套也带来tag标识符的嵌套。
 

图6 dt_struct结构图

5. kernel解析Device Tree
Device Tree文件结构描述就以上struct fdt_header、struct fdt_node_header及struct fdt_property三个结构体描述。kernel会根据Device Tree的结构解析出kernel能够使用的struct property结构体。kernel根据Device Tree中所有的属性解析出数据填充struct property结构体。struct property结构体描述如下:
struct property {
    char *name;                          /* property full name */
    int length;                          /* property value length */
    void *value;                         /* property value */
    struct property *next;             /* next property under the same node */
    unsigned long _flags;
    unsigned int unique_id;
    struct bin_attribute attr;        /* 属性文件,与sysfs文件系统挂接 */
};

总的来说,kernel根据Device Tree的文件结构信息转换成struct property结构体,并将同一个node节点下面的所有属性通过property.next指针进行链接,形成一个单链表。
kernel中究竟是如何解析Device Tree的呢?下面分析函数解析过程。函数调用过程如图7所示。kernel的C语言阶段的入口函数是init/main.c/stsrt_kernel()函数,在early_init_dt_scan_nodes()中会做以下三件事:

扫描/chosen或者/chose@0节点下面的bootargs属性值到boot_command_line,此外,还处理initrd相关的property,并保存在initrd_start和initrd_end这两个全局变量中;
扫描根节点下面,获取{size,address}-cells信息,并保存在dt_root_size_cells和dt_root_addr_cells全局变量中;
扫描具有device_type =“memory”属性的/memory或者/memory@0节点下面的reg属性值,并把相关信息保存在meminfo中,全局变量meminfo保存了系统内存相关的信息。
 

图7 函数调用过程

Device Tree中的每一个node节点经过kernel处理都会生成一个struct device_node的结构体,struct device_node最终一般会被挂接到具体的struct device结构体。struct device_node结构体描述如下:

struct device_node {
    const char *name;              /* node的名称,取最后一次“/”和“@”之间子串 */
    const char *type;              /* device_type的属性名称,没有为<NULL> */
    phandle phandle;               /* phandle属性值 */
    const char *full_name;         /* 指向该结构体结束的位置,存放node的路径全名,例如:/chosen */
    struct fwnode_handle fwnode;

    struct  property *properties;  /* 指向该节点下的第一个属性,其他属性与该属性链表相接 */
    struct  property *deadprops;   /* removed properties */
    struct  device_node *parent;   /* 父节点 */
    struct  device_node *child;    /* 子节点 */
    struct  device_node *sibling;  /* 姊妹节点,与自己同等级的node */
    struct  kobject kobj;          /* sysfs文件系统目录体现 */
    unsigned long _flags;          /* 当前node状态标志位,见/include/linux/of.h line124-127 */
    void    *data;
};

/* flag descriptions (need to be visible even when !CONFIG_OF) */
#define OF_DYNAMIC        1 /* node and properties were allocated via kmalloc */
#define OF_DETACHED       2 /* node has been detached from the device tree*/
#define OF_POPULATED      3 /* device already created for the node */
#define OF_POPULATED_BUS  4 /* of_platform_populate recursed to children of this node */

4 内核对设备树中设备信息的处理

4.1 内核对DTB所在内存的处理

内核会保留DTB所占据的内存区域,因此DTB文件中的数据在kernel启动后也是可用的,相关源码的调用路径如下:

在这里插入图片描述

4.2 struct device_node和struct property

DTB文件的structure block区域记录了很多设备节点,每个节点又有很多属性,对此,kernel使用struct device_node来描述节点,使用struct property来描述属性。下面就介绍一下这两个结构的主要成员(不是全部成员):

struct device_node:

成员    含义
name    指向节点的name属性的属性值(字符串),位于DTB的structure block
phandle    节点的唯一的数字标识符
full_name    指向节点名字符串,该字符串紧跟着结构体本身
properties    指向节点的属性
deadprops    指向被移除的属性
parent    指向父节点
child    指向孩子节点
sibling    指向兄弟节点

struct property

成员含义
name指向属性名字符串,位于DTB的strings block
length属性的长度
valuevoid *类型,指向属性值,位于DTB的structure block
next一个节点的所有属性构成一个链表

4.3 从DTB到struct device_node示例
kernel调用unflatten_device_tree函数,将DTB文件中的设备节点转换为一个个的struct device_node,这些结构体有着树状的层次,在分析相关源码之前,我们不妨先看一个设备树完成转换之后的结果,建立起总体上的认知。

首先给出一个设备树文件的示例,这个设备树文件并不完整,只是用作示例:

/ {
    model = "SMDK2416";
    compatible = "samsung,s3c2416";
    #address-cells = <1>;
    #size-cells = <1>;

    memory@30000000 {
        device_type = "memory";
        reg =  <0x30000000 0x4000000>;
    };

    pinctrl@56000000 {
        name = "example_name";
        compatible = "samsung,s3c2416-pinctrl";
    };
};
该设备树文件被DTC编译为DTB之后,被u-boot传递给kernel,然后内核读取其节点信息,建立如下的由device_node构成的树状结构:

在这里插入图片描述

 值得一提的是,为了突出device_node的name成员和full_name成员的差别,在上图中,我将没有name属性的节点的name成员置为<NULL>,这源于:

static bool populate_node(const void *blob,
              int offset,
              void **mem,
              struct device_node *dad,
              struct device_node **pnp,
              bool dryrun)
{
    ......
    populate_properties(blob, offset, mem, np, pathp, dryrun);
    if (!dryrun) {
        np->name = of_get_property(np, "name", NULL);
        if (!np->name)
            /* 没有name属性,则name成员置为"<NULL>" */
            np->name = "<NULL>";
    }
    ......
}
但实际上,populate_properties函数会为没有name属性的节点创建name属性:

static void populate_properties(const void *blob,
                int offset,
                void **mem,
                struct device_node *np,
                const char *nodename,
                bool dryrun)
{
    struct property *pp, **pprev = NULL;
    int cur;
    bool has_name = false;

    pprev = &np->properties;
    for (cur = fdt_first_property_offset(blob, offset);
         cur >= 0;
         cur = fdt_next_property_offset(blob, cur)) {
        ......
        if (!strcmp(pname, "name"))
            has_name = true;
        ......
    }

    /* With version 0x10 we may not have the name property,
     * recreate it here from the unit name if absent
     */
    if (!has_name) {
        /* 为没有name属性的节点创建name属性 */
        ......
    }
    ......
}

4.4 unflatten_device_tree分析

有了上文的铺垫,我们就可以开始从源码的层面分析kernel如何根据DTB构建device_node树:

void __init unflatten_device_tree(void)
{
    /* 根据DTB构建device_node树 */
    __unflatten_device_tree(initial_boot_params, NULL, &of_root,
                early_init_dt_alloc_memory_arch, false);

    /*
        设置of_aliases指向/aliases节点对应的device_node
        设置of_chosen指向/chosen节点对应的device_node
        
        对于of_chosen:
            从其属性中找到属性名为stdout-path或linux,stdout-path的属性的属性值,
            并根据该属性值获得标准输出设备对应的device_node,将其赋给of_stdout
        
        对于of_aliases:
            遍历其属性,跳过name、phandle、linux,phandle,对于其他的属性,如果
            该属性的属性值指示了一个device_node,那么为这个device_node创建一个
            struct alias_prop,并添加到aliases_lookup链表。

            举一个例子来说,假设一个别名属性为i2c1 = "xxx",并且"xxx"指示了一个
            device_node,那么为其创建的alias_prop的np成员指向相应的device_node;
            id成员为1,零长数组stem指向字符串"i2c"(数字部分作为id去掉了)。
    */
    of_alias_scan(early_init_dt_alloc_memory_arch);
    
    /* 看名字是用作测试的,具体不清楚 */
    unittest_unflatten_overlay_base();
}
再看__unflatten_device_tree

void *__unflatten_device_tree(const void *blob,
                  struct device_node *dad,
                  struct device_node **mynodes,
                  void *(*dt_alloc)(u64 size, u64 align),
                  bool detached)
{
    int size;
    void *mem;

    /* 打印一些信息并校验DTB文件 */
    ......

    /* 第一次调用unflatten_dt_nodes,计算整个device_node树包括属性所需的全部内存 */
    size = unflatten_dt_nodes(blob, NULL, dad, NULL);
    if (size < 0)
        return NULL;
    
    size = ALIGN(size, 4);
    pr_debug("  size is %d, allocating...\n", size);

    /* 为整个device_node树申请内存 */
    mem = dt_alloc(size + 4, __alignof__(struct device_node));
    if (!mem)
        return NULL;
    /* 将这段内存清0 */
    memset(mem, 0, size);
    /* 在这段内存的末尾填充0xdeadbeef(大端模式) */
    *(__be32 *)(mem + size) = cpu_to_be32(0xdeadbeef);

    pr_debug("  unflattening %p...\n", mem);

    /* 真正创建device_node树 */
    unflatten_dt_nodes(blob, mem, dad, mynodes);
    
    /* 根据之前在mem内存末尾填充的内容检查有没有踩内存 */
    if (be32_to_cpup(mem + size) != 0xdeadbeef)
        pr_warning("End of tree marker overwritten: %08x\n",
               be32_to_cpup(mem + size));
    
    if (detached && mynodes) {
        /* 为当前创建的device_node树打上OF_DETACHED标记 */
        of_node_set_flag(*mynodes, OF_DETACHED);
        pr_debug("unflattened tree is detached\n");
    }

    pr_debug(" <- unflatten_device_tree()\n");
    return mem;
}
__unflatten_device_tree函数调用进一步调用unflatten_dt_nodes创建device_node树,具体过程是遍历DTB的各个节点,对每个节点调用populate_node申请并填充之(节点的属性也会在该函数中被构建)。过程很好理解,就不再继续跟踪了。

4.5 将device_node转换成platform_device

4.5.1 哪些节点需要转换成platform_device

首先,我们需要知道,在linux中什么样的设备是platform_device,内核文档里是这么说的:

平台设备包括基于旧端口的设备和接到到外围总线的主机桥,以及大多数集成到片上系统平台的控制器(如i2c控制器)。它们通常的共同点是从CPU总线直接寻址(对arm来说,这些设备的寄存器位于CPU的寻址空间,CPU可以像访存一样访问设备的寄存器)。

举例来说,i2c控制器是platform_device,但是连接在SoC的i2c总线上的i2c设备,比如一个i2c接口的EEPROM就不是platform_device。在设备树中,通常i2c控制器对应的设备节点作为连接该i2c控制器的片外外设的父节点,如:

i2c0: i2c@7f004000 {
            compatible = "samsung,s3c2440-i2c";
            reg = <0x7f004000 0x1000>;
            interrupt-parent = <&vic1>;
            interrupts = <18>;
            clock-names = "i2c";
            clocks = <&clocks PCLK_IIC0>;
            #address-cells = <1>;
            #size-cells = <0>;
            pinctrl-names = "default";
            pinctrl-0 = <&i2c0_bus>;
            status = "okay";
            /* 连接到i2c@7f004000的外设——AT24C08 */
            eeprom@50 {
                compatible = "atmel,24c08";
                reg = <0x50>;
                pagesize = <16>;
            };
        };

对platform_device有了一定的了解之后,我们再对需要转换成platform_device的设备节点做一个总结:

含有compatible属性的根节点的子节点;
或者,compatibe属性值为"simple-bus"、“simple-mfd”、“isa”、"arm,amba-bus"的节点的含有compatible属性的子节点。
 

4.5.2 在哪里做的转换工作

位于drivers/of/platform.cof_platform_default_populate_init函数负责为合适的设备节点构建platform_device。该函数的调用比较隐晦,这里简单介绍一下:

arch_initcall_sync(of_platform_default_populate_init);

#define arch_initcall_sync(fn)        __define_initcall(fn, 3s)

#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)

#define ___define_initcall(fn, id, __sec) \
    static initcall_t __initcall_##fn##id __used \
        __attribute__((__section__(#__sec ".init"))) = fn;
内核使用上述的宏,定义了一个函数指针,指向of_platform_default_populate_init,通过链接脚本将该函数指针保存到一个名为.initcall3s.init的节。实际上,该节保存了一系列的初始化函数。链接脚本中的相关部分如下:

__initcall3_start = .;
KEEP(*(.initcall3.init))
KEEP(*(.initcall3s.init))
__initcall4_start = .;
kernel中,名为do_initcalls的函数会遍历这些存有初始化函数指针的节,逐个取出函数指针,并调用相应的初始化函数:

static void __init do_initcalls(void)
{
    int level;
    /* 遍历各个初始化函数指针所在的节,并调用初始化函数 */
    for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
        do_initcall_level(level);
}
最后在提一下do_initcalls的调用点:

在这里插入图片描述

4.5.3 浅析转换的过程

本节将分析一下of_platform_default_populate_init的源码,探究一下从device_node到platform_device的细节:

static int __init of_platform_default_populate_init(void)
{
    struct device_node *node;
    /* 根据of_root是否为NULL来判断device_node是不是都创建好了 */
    if (!of_have_populated_dt())
        return -ENODEV;

    /* 为一些含有特殊的compatible属性值的节点构建platform_device(不是所有平台都有) */
    for_each_matching_node(node, reserved_mem_matches)
        of_platform_device_create(node, NULL, NULL);
    
    /* 为/firmware节点构建platform_device */
    node = of_find_node_by_path("/firmware");
    if (node) {
        of_platform_populate(node, NULL, NULL, NULL);
        of_node_put(node);
    }

    /* 上面是对特殊节点的处理,这里才是为大部分节点构建platform_device的函数 */
    of_platform_default_populate(NULL, NULL, NULL);

    return 0;
}
of_platform_default_populate只是of_platform_populate的简单包装,因此我们直接看后者:

/*
    root    = NULL
    matches = of_default_bus_match_table
    lookup  = NULL
    parent  = NULL
*/
int of_platform_populate(struct device_node *root,
            const struct of_device_id *matches,
            const struct of_dev_auxdata *lookup,
            struct device *parent)
{
    struct device_node *child;
    int rc = 0;
    /* 传入的root为NULL,因此这里执行of_find_node_by_path("/")获得根节点 */
    root = root ? of_node_get(root) : of_find_node_by_path("/");
    if (!root)
        return -EINVAL;

    pr_debug("%s()\n", __func__);
    pr_debug(" starting at: %pOF\n", root);
    /* 遍历根节点的每个孩子节点 */
    for_each_child_of_node(root, child) {
        /* 为根节点的孩子节点创建platform_device(不是所有孩子节点,需要符合一定的条件) */
        rc = of_platform_bus_create(child, matches, lookup, parent, true);
        if (rc) {
            of_node_put(child);
            break;
        }
    }
    /* 为根节点设置OF_POPULATED_BUS,标志着已经为其孩子节点创建完platform_device */
    of_node_set_flag(root, OF_POPULATED_BUS);

    of_node_put(root);
    return rc;
}
再看of_platform_bus_create

/*
    bus     : 该节点可能需要创建platform_device
    matches : 如果bus节点的compatible属性能和matches匹配上,说明其孩子节点也要创建platform_device(比如compatible的值为"simple-bus")
    lookup  : 如果bus节点的compatible属性匹配lookup数组,那么相应的paltform_device的device.kobj.name设置为lookup数组中匹配元素的name(不是同一块内存)
    parent  : 创建的paltform_device的device.parent = parent(device.kobj.parent = &device.parent.kobj)
    strict  : bus节点是否一定要具备compatibile属性
*/
static int of_platform_bus_create(struct device_node *bus,
                  const struct of_device_id *matches,
                  const struct of_dev_auxdata *lookup,
                  struct device *parent, bool strict)
{
    const struct of_dev_auxdata *auxdata;
    struct device_node *child;
    struct platform_device *dev;
    const char *bus_id = NULL;
    void *platform_data = NULL;
    int rc = 0;

    /* 如果要求必须有compatible属性,那么对于没有该属性的节点直接返回 */
    if (strict && (!of_get_property(bus, "compatible", NULL))) {
        pr_debug("%s() - skipping %pOF, no compatible prop\n",
             __func__, bus);
        return 0;
    }

    /* 跳过由of_skipped_node_table指定的节点,这些节点不用创建platform_device */
    if (unlikely(of_match_node(of_skipped_node_table, bus))) {
        pr_debug("%s() - skipping %pOF node\n", __func__, bus);
        return 0;
    }
    /* 已经为该节点及其孩子节点创建过platform_device,则返回 */
    if (of_node_check_flag(bus, OF_POPULATED_BUS)) {
        pr_debug("%s() - skipping %pOF, already populated\n",
            __func__, bus);
        return 0;
    }
    /* 查找lookup数组中是否有匹配的数组项,如果有则取其name和platform_data用于后面的创建platform_device */
    auxdata = of_dev_lookup(lookup, bus);
    if (auxdata) {
        bus_id = auxdata->name;
        platform_data = auxdata->platform_data;
    }
    /* 处理特殊的节点(兼容就版本的设备树) */
    if (of_device_is_compatible(bus, "arm,primecell")) {
        /*
         * Don't return an error here to keep compatibility with older
         * device tree files.
         */
        of_amba_device_create(bus, bus_id, platform_data, parent);
        return 0;
    }
    
    /* 为bus节点创建platform_device */
    dev = of_platform_device_create_pdata(bus, bus_id, platform_data, parent);

    /* 如果bus节点的compatible属性比较特殊,比如是"simple-bus",则需要尝试为其子节点创建platform_device */
    if (!dev || !of_match_node(matches, bus))
        return 0;
    
    for_each_child_of_node(bus, child) {
        pr_debug("   create child: %pOF\n", child);
        /* 整个过程是递归进行的 */
        rc = of_platform_bus_create(child, matches, lookup, &dev->dev, strict);
        if (rc) {
            of_node_put(child);
            break;
        }
    }
    of_node_set_flag(bus, OF_POPULATED_BUS);
    return rc;
}
函数of_platform_device_create_pdata会调用of_device_alloc以及of_device_add创建并注册platform_device,具体的就不再跟踪了。

4.5.4 如何处理平台设备对应的设备节点的子节点
上文已经说过,对于compatible属性为simple-bus等特殊值的节点,kernel也会为其含有compatible属性的子节点创建platform_device。那么对于其他设备节点呢,怎么处理它们的子节点?所谓知子莫若父,它们的子节点应该交由父节点(也就是创建了platform_device的节点)来处理。仍以4.5.1节中i2c的例子来说明。

kernel为该节点创建platform_device后,将其注册到platform_bus_type,根据kernel的总线-设备-驱动模型,如果该节点的compatible属性samsung,s3c2410-i2c,匹配总线上的某个platform_driver,那么该驱动的probe函数会被调用,在该函数中,会为SoC的i2c控制器创建i2c_adapter,也会为连接在该控制器上的i2c接口的外设eeprom@50创建i2c_client。具体的函数调用过程放在4.6节,这里就不再多说了。
4.6 小结

在这里插入图片描述

5 内核中设备树相关头文件的总结

kernel源码树下的include/linux目录下存在着一些以of开头的头文件,这些头文件内是一些与设备树相关的函数的声明。下面将对这些头文件做一个分类。

5.1 声明处理DTB文件的函数的头文件

头文件内容
of_fdt.h声明了dtb文件的相关操作函数,一般用不到,因为dtb文件在内核中被转换为device_node树,后者更易于使用

5.2 处理device_node的函数的头文件
头文件    内容
of.h    提供设备树的一般处理函数,如 of_property_read_u32(读取某个属性的u32值)
of_address.h    地址相关的函数,如 of_get_address(获得reg属性中的addr、size值)
of_dma.h    处理设备树中DMA相关属性的函数
of_gpio.h    GPIO相关的函数
of_graph.h    GPU相关驱动中用到的函数,从设备树中获得GPU信息
of_iommu.h    暂不清楚
of_irq.h    中断相关的函数
of_mdio.h    MDIO (Ethernet PHY) API
of_net.h    OF helpers for network devices
of_pci.h    PCI相关函数
of_pdt.h    暂不清楚
of_reserved_mem.h    设备树中reserved_mem相关的函数

5.3 声明处理platform_device的函数的头文件
头文件    内容
of_platform.h    声明了把device_node转换为platform_device时用到的函数
of_device.h    主要声明了struct device相关的函数,如 of_match_device

6 设备树在文件系统中的表示

6.1 /sys与设备树

所有设备树的信息存放于/sys/firmware目录下:

目录/文件    含义
/sys/firmware/fdt    该文件表示原始DTB文件,可用hexdump -C /sys/firmware/fdt查看
/sys/firmware/devicetree    以目录结构呈现设备树,每个device_node对应一个目录,每个属性对应节点目录下的一个文件吗,比如根节点对应base目录,该目录下有compatible等文件
所有的platform_device会在/sys/devices/platform下对应一个目录,这些platform_device有来自设备树的,也有来自.c文件中手工注册的。由kernel根据设备树创建的platform_device对应的目录下存在一个名为of_node的软链接,链接向该platform_device对应的device_node对应的目录。
 

6.2 /proc与设备树

/proc/device-tree作为链接文件指向/sys/firmware/devicetree/base

Linux设备树解析_smcdef的博客-CSDN博客_linux 设备树解析

认识设备树(四)——内核对DTB文件的解析_gzxb1995的博客-CSDN博客_dtb解析

中断-解析串口节点_chengbeng1745的博客-CSDN博客

  • 9
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值