Exynos4412 内核移植(六)—— 设备树解析

一、描述

        ARM Device Tree起源于OpenFirmware (OF),在过去的Linux中,arch/arm/plat-xxx和arch/arm/mach-xxx中充斥着大量的垃圾代码,相当多数的代码只是在描述板级细节,而这些板级细节对于内核来讲,不过是垃圾,如板上的platform设备、resource、i2c_board_info、spi_board_info以及各种硬件的platform_data。为了改变这种局面,Linux社区的大牛们参考了PowerPC等体系架构中使用的Flattened Device Tree(FDT),也采用了Device Tree结构,许多硬件的细节可以直接透过它传递给Linux,而不再需要在kernel中进行大量的冗余编码。  

       Device Tree是一种描述硬件的数据结构,它起源于 OpenFirmware (OF)。在Linux 2.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使用情况
  • Clock控制器和Clock使用情况

     它基本上就是画一棵电路板上CPU、总线、设备组成的树,Bootloader会将这棵树传递给内核,然后内核可以识别这棵树,并根据它展开出Linux内核中的platform_device、i2c_client、spi_device等设备,而这些设备用到的内存、IRQ等资源,也被传递给了内核,内核会将这些资源绑定给展开的相应的设备。

     通常.dts文件以文本方式对系统设备树进行描述,经过Device Tree Compiler(dtc)将dts文件转换成二进制文件binary device tree blob(dtb),.dtb文件可由Linux内核解析,有了device tree就可以在不改动Linux内核的情况下,对不同的平台实现无差异的支持,只需更换相应的dts文件,即可满足。


二、相关结构体

1、U-Boot需要将设备树在内存中的存储地址传给内核。该树主要由三大部分组成:头(Header)、结构块(Structure block)、字符串块(Strings block)。

设备树在内存中的存储布局图:



1.1 头(header)
      头主要描述设备树的一些基本信息,例如设备树大小,结构块偏移地址,字符串块偏移地址等。偏移地址是相对于设备树头的起始地址计算的。

struct boot_param_header {
    __be32 magic;                //设备树魔数,固定为0xd00dfeed
    __be32 totalsize;            //整个设备树的大小
    __be32 off_dt_struct;        //保存结构块在整个设备树中的偏移
    __be32 off_dt_strings;        //保存的字符串块在设备树中的偏移
    __be32 off_mem_rsvmap;        //保留内存区,该区保留了不能被内核动态分配的内存空间
    __be32 version;            //设备树版本
    __be32 last_comp_version;    //向下兼容版本号
    __be32 boot_cpuid_phys;    //为在多核处理器中用于启动的主cpu的物理id
    __be32 dt_strings_size;    //字符串块大小
    __be32 dt_struct_size;     //结构块大小
};

1.2 结构块(struct block)

       设备树结构块是一个线性化的结构体,是设备树的主体,以节点node的形式保存了目标单板上的设备信息。

       在结构块中以宏OF_DT_BEGIN_NODE标志一个节点的开始,以宏OF_DT_END_NODE标识一个节点的结束,整个结构块以宏OF_DT_END结束。一个节点主要由以下几部分组成。

(1)节点开始标志:一般为OF_DT_BEGIN_NODE。
(2)节点路径或者节点的单元名(ersion<3以节点路径表示,version>=0x10以节点单元名表示)
(3)填充字段(对齐到四字节)
(4)节点属性。每个属性以宏OF_DT_PROP开始,后面依次为属性值的字节长度(4字节)、属性名称在字符串块中的偏移量(4字节)、属性值和填充(对齐到四字节)。
(5)如果存在子节点,则定义子节点。
(6)节点结束标志OF_DT_END_NODE。


1.3 字符串块

       通过节点的定义知道节点都有若干属性,而不同的节点的属性又有大量相同的属性名称,因此将这些属性名称提取出一张表,当节点需要应用某个属性名称时直接在属性名字段保存该属性名称在字符串块中的偏移量。

1.4 设备树源码 DTS 表示

      设备树源码文件(.dts)以可读可编辑的文本形式描述系统硬件配置设备树,支持 C/C++方式的注释,该结构有一个唯一的根节点“/”,每个节点都有自己的名字并可以包含多个子节点。设备树的数据格式遵循了 Open Firmware IEEE standard 1275。这个设备树中有很多节点,每个节点都指定了节点单元名称。每一个属性后面都给出相应的值。以双引号引出的内容为 ASCII 字符串,以尖括号给出的是 32 位的16进制值。这个树结构是启动 Linux 内核所需节点和属性简化后的集合,包括了根节点的基本模式信息、CPU 和物理内存布局,它还包括通过/chosen 节点传递给内核的命令行参数信息。

1.5 machine_desc结构

      内核提供了一个重要的结构体struct machine_desc ,这个结构体在内核移植中起到相当重要的作用,内核通过machine_desc结构体来控制系统体系架构相关部分的初始化。machine_desc结构体通过MACHINE_START宏来初始化,在代码中, 通过在start_kernel->setup_arch中调用setup_machine_fdt来获取。

struct machine_desc {
    unsigned int nr; /* architecture number */
    const char *name; /* architecture name */
    unsigned long atag_offset; /* tagged list (relative) */
    const char *const *dt_compat; /* array of device tree* 'compatible' strings */
    unsigned int nr_irqs; /* number of IRQs */
    
#ifdef CONFIG_ZONE_DMA
    phys_addr_t dma_zone_size; /* size of DMA-able area */
#endif
    
    unsigned int video_start; /* start of video RAM */
    unsigned int video_end; /* end of video RAM */
    
    unsigned char reserve_lp0 :1; /* never has lp0 */
    unsigned char reserve_lp1 :1; /* never has lp1 */
    unsigned char reserve_lp2 :1; /* never has lp2 */
    enum reboot_mode reboot_mode; /* default restart mode */
    struct smp_operations *smp; /* SMP operations */
    bool (*smp_init)(void);
    void (*fixup)(struct tag *, char **,struct meminfo *);
    void (*init_meminfo)(void);
    void (*reserve)(void);/* reserve mem blocks */
    void (*map_io)(void);/* IO mapping function */
    void (*init_early)(void);
    void (*init_irq)(void);
    void (*init_time)(void);
    void (*init_machine)(void);
    void (*init_late)(void);
#ifdef CONFIG_MULTI_IRQ_HANDLER
    void (*handle_irq)(struct pt_regs *);
#endif
    void (*restart)(enum reboot_mode, const char *);
};

1.6 设备节点结构体
struct device_node {
    const char *name;    //设备name
    const char *type; //设备类型
    phandle phandle;
    const char *full_name; //设备全称,包括父设备名

    struct property *properties; //设备属性链表
    struct property *deadprops; //removed properties
    struct device_node *parent; //指向父节点
    struct device_node *child; //指向子节点
    struct device_node *sibling; //指向兄弟节点
    struct device_node *next; //相同设备类型的下一个节点
    struct device_node *allnext; //next in list of all nodes
    struct proc_dir_entry *pde; //该节点对应的proc
    struct kref kref;
    unsigned long _flags;
    void *data;
#if defined(CONFIG_SPARC)
    const char *path_component_name;
    unsigned int unique_id;
    struct of_irq_controller *irq_trans;
#endif
};

1.7 属性结构体

struct property {
    char *name;        //属性名
    int length;        //属性值长度
    void *value;        //属性值
    struct property *next; //指向下一个属性
    unsigned long _flags; //标志
    unsigned int unique_id;
};


三、设备树初始化及解析

        分析Linux内核的源码,可以看到其对扁平设备树的解析流程如下:

(1)首先在内核入口处将从u-boot传递过来的镜像基地址。

(2)通过调用early_init_dt_scan()函数来获取内核前期初始化所需的bootargs,cmd_line等系统引导参数。

(3)根据bootargs,cmd_line等系统引导参数进入start_kernel()函数,进行内核的第二阶段初始化。

(4)调用unflatten_device_tree()函数来解析dtb文件,构建一个由device_node结构连接而成的单项链表,并使用全局变量of_allnodes指针来保存这个链表的头指针。

(5)内核调用OF提供的API函数获取of_allnodes链表信息来初始化内核其他子系统、设备等。

//kernel 初始化的代码(init/main.c)
asmlinkage void __init start_kernel(void)
{
    ...
    //这个setup_arch就是各个架构自己的设置函数,哪个参与了编译就调用哪个,arm架构应当是arch/arm/kernel/setup.c中的 setup_arch。
    setup_arch(&command_line);
    ...
}

void __init setup_arch(char **cmdline_p)
{
    const struct machine_desc *mdesc;
     
    setup_processor();
    //setup_machine_fdt函数获取内核前期初始化所需的bootargs,cmd_line等系统引导参数
    mdesc = setup_machine_fdt(__atags_pointer);//__atags_pointer是bootloader传递参数的物理地址
    if (!mdesc)
        mdesc = setup_machine_tags(__atags_pointer, __machine_arch_type);
    machine_desc = mdesc;
    machine_name = mdesc->name;
        
    if (mdesc->reboot_mode != REBOOT_HARD)
        reboot_mode = mdesc->reboot_mode;
        
    init_mm.start_code = (unsigned long) _text;
    init_mm.end_code = (unsigned long) _etext;
    init_mm.end_data = (unsigned long) _edata;
    init_mm.brk = (unsigned long) _end;

    strlcpy(cmd_line, boot_command_line, COMMAND_LINE_SIZE);
    *cmdline_p = cmd_line;
 
    parse_early_param();
 
    sort(&meminfo.bank, meminfo.nr_banks, sizeof(meminfo.bank[0]), meminfo_cmp, NULL);
 
    early_paging_init(mdesc, lookup_processor_type(read_cpuid_id()));
    setup_dma_zone(mdesc);
    sanity_check_meminfo();
    arm_memblock_init(&meminfo, mdesc);
 
    paging_init(mdesc);
    request_standard_resources(mdesc);
 
    if (mdesc->restart)
        arm_pm_restart = mdesc->restart;
 
     //解析设备树
    unflatten_device_tree();
 
    ......
}</span>

(一) 函数获取内核前期初始化所需的bootargs,cmd_line等系统引导参数

1. setup_machine_fdt()函数获取内核前期初始化所需的bootargs,cmd_line等系统引导参数。

const struct machine_desc * __init setup_machine_fdt(unsigned int dt_phys)
{
    const struct machine_desc *mdesc, *mdesc_best = NULL;
 
#ifdef CONFIG_ARCH_MULTIPLATFORM
    DT_MACHINE_START(GENERIC_DT, "Generic DT based system")
    MACHINE_END
     
    mdesc_best = &__mach_desc_GENERIC_DT;
#endif

    //bootloader传递参数的物理地址不为空,并将物理地址转化为虚拟地址,
    //通过函数early_init_dt_scan从设备树中读出bootargs,cmd_line等系统引导参数。
    if (!dt_phys || !early_init_dt_scan(phys_to_virt(dt_phys)))
        return NULL;
    
    //根据设备树中根节点属性"compatible"的属性描述,找到系统中定义的最匹配的machine_desc结构,该结构控制系统体系架构相关部分的初始化
    mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);
    if (!mdesc) {
        const char *prop;
        long size;
        unsigned long dt_root;
 
        early_print("\nError: unrecognized/unsupported ""device tree compatible list:\n[ ");
 
        //找到设备树的根节点,dt_root指向根节点的属性地址处
        dt_root = of_get_flat_dt_root();
        //读出根节点的"compatible"属性的属性值
        prop = of_get_flat_dt_prop(dt_root, "compatible", &size);
        //将根节点的"compatible"属性的属性值打印出来
        while (size > 0) {
            early_print("'%s' ", prop);
            size -= strlen(prop) + 1;
            prop += strlen(prop) + 1;
        }
        early_print("]\n\n");
        
        dump_machine_table(); /* does not return */
    }
         
    //Change machine number to match the mdesc we're using
    __machine_arch_type = mdesc->nr;

    return mdesc;
}

struct boot_param_header *initial_boot_params;
bool __init early_init_dt_scan(void *params)
{
    if (!params)
        return false;
        
    //参数params是bootloader传递参数的物理地址转化为的虚拟地址,保存设备树起始地址
    initial_boot_params = params;
        
    //验证设备树的magic 
    if (be32_to_cpu(initial_boot_params->magic) != OF_DT_HEADER) {
        initial_boot_params = NULL;
        return false;
    }
        
    //从设备树中读取chosen节点的信息,包括命令行boot_command_line,initrd location及size
    of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);
        
    //得到根节点的{size,address}-cells信息
    of_scan_flat_dt(early_init_dt_scan_root, NULL);
        
    //读出设备树的系统内存设置
    of_scan_flat_dt(early_init_dt_scan_memory, NULL);
        
    return true;
}

int __init of_scan_flat_dt(int (*it)(unsigned long node,const char *uname, int depth,void *data),void *data)
{
    //找到设备树中结构块的地址
    unsigned long p = ((unsigned long)initial_boot_params) + be32_to_cpu(initial_boot_params->off_dt_struct);
    int rc = 0;
    int depth = -1;
 
    do {
        //获得节点起始标志,即OF_DT_BEGIN_NODE,OF_DT_PROP等
        u32 tag = be32_to_cpup((__be32 *)p);
        const char *pathp;
        
        p += 4;//跳过节点起始标志
        
        //如果是OF_DT_END_NODE标志,表示该节点结束,继续下一结点
        if (tag == OF_DT_END_NODE) {
            depth--;
            continue;
        }
        
        //OF_DT_NOP标志代表空节点
        if (tag == OF_DT_NOP)
            continue;
            
        //OF_DT_END标志整个结构块结束
        if (tag == OF_DT_END)
            break;
            
        //OF_DT_PROP标示属性,Property:属性值的字节长度、属性名称在字符串块中的偏移量、属性值和填充。
        if (tag == OF_DT_PROP) {
            //属性值的字节大小
            u32 sz = be32_to_cpup((__be32 *)p);
            p += 8;//跳过属性值的字节长度、属性名称在字符串块中的偏移量
            if (be32_to_cpu(initial_boot_params->version) < 0x10)
                p = ALIGN(p, sz >= 8 ? 8 : 4);
            //跳过该属性值的大小
            p += sz;
            //地址对齐
            p = ALIGN(p, 4);
            //表示一个属性节点遍历完成,因为这里并不是寻找属性节点,而是找OF_DT_BEGIN_NODE开始的节点
            continue;
        }
        
        //若都不是以上节点类型,也不是节点开始标示(OF_DT_BEGIN_NODE),则出错返回
        if (tag != OF_DT_BEGIN_NODE) {
            pr_err("Invalid tag %x in flat device tree!\n", tag);
            return -EINVAL;
        }
        
        //执行到这里,标示tag=OF_DT_BEGIN_NODE,表示一个节点的开始,探索深度加1
        depth++;
        //节点路径或者节点名
        pathp = (char *)p;
        //节点地址四字节对齐,然后p指向节点属性地址
        p = ALIGN(p + strlen(pathp) + 1, 4);
        //如果是节点路径,则返回路径名的最后一段,假如为/root/my_root,则返回my_root,即获得节点名
        if (*pathp == '/')
            pathp = kbasename(pathp);
        //调用相应的节点处理函数,p指向节点属性地址
        rc = it(p, pathp, depth, data);
        if (rc != 0)
            break;
    } while (1);
 
    return rc;
}

1.1 chosen节点
       chosen 节点并不代表一个真实的设备,只是作为一个为固件和操作系统之间传递数据的地方,比如引导参数。chosen 节点里的数据也不代表硬件。通常,chosen 节点在.dts 源文件中为空,并在启动时填充。在我们的示例系统中,固件可以往 chosen 节点添加以下信息:
chosen {
    bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200"; //节点属性
    linux,initrd-start = <0x85500000>; //节点属性
    linux,initrd-end = <0x855a3212>; //节点属性
};
int __init early_init_dt_scan_chosen(unsigned long node, const char *uname,int depth, void *data)
{
    unsigned long l;
    char *p;
    
    pr_debug("search \"chosen\", depth: %d, uname: %s\n", depth, uname);

    //depth深度要为1,表示在根节点下(一般根节点/的depth为0)
    //data表示系统启动命令行boot_command_line要分配空间
    //检查节点名是否为chosen节点    
    if (depth != 1 || !data || (strcmp(uname, "chosen") != 0 && strcmp(uname, "chosen@0") != 0))
        return 0;
    
    //从设备树的chosen节点中读出initrd的起始、结束地址
    early_init_dt_check_for_initrd(node);
 
    //设备树的chosen节点中读取bootargs属性的属性值,并拷贝给boot_command_line
    p = of_get_flat_dt_prop(node, "bootargs", &l);
    if (p != NULL && l > 0)
        strlcpy(data, p, min((int)l, COMMAND_LINE_SIZE));

    pr_debug("Command line is: %s\n", (char*)data);
     
    return 1;
}

static void __init early_init_dt_check_for_initrd(unsigned long node)
{
    u64 start, end;
    unsigned long len;
    __be32 *prop;
        
    pr_debug("Looking for initrd properties... ");
         
    //返回该chosen节点中属性名为"linux,initrd-start"的地址
    prop = of_get_flat_dt_prop(node, "linux,initrd-start", &len);
    if (!prop)
        return;
    //从该地址读出initrd-start的地址
    start = of_read_number(prop, len/4);
        
    //返回该chosen节点中属性名为"linux,initrd-end"的地址
    prop = of_get_flat_dt_prop(node, "linux,initrd-end", &len);
    if (!prop)
        return;
    //从该地址读出initrd-end的地址
    end = of_read_number(prop, len/4);
        
    //将读出的地址赋值给全局变量initrd_start和initrd_end,用于跟文件系统的挂载
    initrd_start = (unsigned long)__va(start);
    initrd_end = (unsigned long)__va(end);
    initrd_below_start_ok = 1;
        
    pr_debug("initrd_start=0x%llx initrd_end=0x%llx\n",(unsigned long long)start, (unsigned long long)end);
}

void *__init of_get_flat_dt_prop(unsigned long node, const char *name,unsigned long *size)
{
    return of_fdt_get_property(initial_boot_params, node, name, size);
}

void *of_fdt_get_property(struct boot_param_header *blob,unsigned long node, const char *name,unsigned long *size)
{
    //p指向该chosen节点的节点属性地址
    unsigned long p = node;
     
    //因为一个节点中可能包含多个属性,所以这里遍历chosen节点中的所有属性,找到属性名为name的属性
    do {
        //取得该节点属性的起始标志OF_DT_PROP
        u32 tag = be32_to_cpup((__be32 *)p);
        u32 sz, noff;
        const char *nstr;
         
        p += 4;//跳过节点属性的起始标志

        //空节点则继续
        if (tag == OF_DT_NOP)
            continue;
        //非属性标志则返回NULL
        if (tag != OF_DT_PROP)
            return NULL;
         
        //运行到这里表示为属性OF_DT_PROP
        //取得该节点属性的的属性值size
        sz = be32_to_cpup((__be32 *)p);
        //取得该属性名的在字符串块中的偏移值
        noff = be32_to_cpup((__be32 *)(p + 4));
        p += 8;
        //跳过对齐填充字段
        if (be32_to_cpu(blob->version) < 0x10)
            p = ALIGN(p, sz >= 8 ? 8 : 4);
         
        //在字符串块取出该属性的名称
        nstr = of_fdt_get_string(blob, noff);
        if (nstr == NULL) {
            pr_warning("Can't find property index name !\n");
            return NULL;
        }

        //若名称一致,表示找到我们要找的属性
        if (strcmp(name, nstr) == 0) {
            if (size)
                *size = sz;//返回该属性的属性值size
            //返回该属性值所在的地址
            return (void *)p;
        }
        //否则继续下一个属性
        p += sz;
        p = ALIGN(p, 4);
    } while (1);
}

char *of_fdt_get_string(struct boot_param_header *blob, u32 offset)
{
    //从设备树的字符串块的offset处读出name
    return ((char *)blob) + be32_to_cpu(blob->off_dt_strings) + offset;
}

static inline u64 of_read_number(const __be32 *cell, int size)
{
    u64 r = 0;
    //读出属性值,属性值大小为size
    while (size--)
        r = (r << 32) | be32_to_cpu(*(cell++));
    return r;
}

1.2 根节点"/"
       设备树有且仅有一个根节点,即“/”,根节点下包含很多子节点,例入下图,根节点为"/",根节点的子节点为"chosen",根节点的属性包含"compatible","#address-cells","#size-cells","interrupt-parent"等。属性model指明了目标板平台或模块的名称,属性compatible值指明和目标板为同一系列的兼容的开发板名称。对于大多数32位平台,属性#address-cells和#size-cells的值一般为1。#address-cells = <1>; 1表示地址32位,2表示地址64位。#size-cells = <1>;1表示rangs的每部分占一个cell,依此类推 
 {
    compatible = "sprd,spx15";
    #address-cells = <1>;
    #size-cells = <1>;
    interrupt-parent = <&gic>;


    chosen {
        bootargs = "loglevel=8 console=ttyS1,115200n8 init=/init root=/dev/ram0 rw";
        linux,initrd-start = <0x85500000>;
        linux,initrd-end = <0x855a3212>;
    };
}
所以本函数就是读取根节点的"#address-cells","#size-cells"属性
int __init early_init_dt_scan_root(unsigned long node, const char *uname,int depth, void *data)
{
    __be32 *prop;
    
    //根节点的探索深度depth一定为0,否则不是根节点"/",node为根节点的属性地址    
    if (depth != 0)
        return 0;
        
    dt_root_size_cells = OF_ROOT_NODE_SIZE_CELLS_DEFAULT;
    dt_root_addr_cells = OF_ROOT_NODE_ADDR_CELLS_DEFAULT;
        
    //返回根节点节点属性名为"#size-cells"的地址
    prop = of_get_flat_dt_prop(node, "#size-cells", NULL);
    if (prop)
        dt_root_size_cells = be32_to_cpup(prop);//从该属性获得root size
        
    pr_debug("dt_root_size_cells = %x\n", dt_root_size_cells);
        
    //返回根节点节点属性名为"#address-cells"的地址
    prop = of_get_flat_dt_prop(node, "#address-cells", NULL);
    if (prop)
        dt_root_addr_cells = be32_to_cpup(prop);//从该属性获得root address
    pr_debug("dt_root_addr_cells = %x\n", dt_root_addr_cells);
        
    return 1;
}


1.3 memory节点
      memory节点用于描述目标板上物理内存范围,一般称作/memory节点,可以有一个或多个。当有多个节点时,需要后跟单元地址予以区分;只有一个单元地址时,可以不写单元地址,默认为0。此节点包含板上物理内存的属性,一般要指定device_type(固定为"memory")和reg属性。其中reg的属性值以<起始地址 空间大小>的形式给出,如下示例中目标板内存起始地址为0x80000000,大小为0x20000000字节。 
memory {
    device_type = "memory";
    reg = <0x80000000 0x20000000>;
};
int __init early_init_dt_scan_memory(unsigned long node, const char *uname,int depth, void *data)
{
    //获取该节点中属性"device_type"的属性值
    char *type = of_get_flat_dt_prop(node, "device_type", NULL);
    __be32 *reg, *endp;
    unsigned long l;
    
    //检查"device_type"的属性值,确定该节点是否为memory节点
    if (type == NULL) {
        if (depth != 1 || strcmp(uname, "memory@0") != 0)
            return 0;
    } else if (strcmp(type, "memory") != 0)
        return 0;
    
    //从该memory节点中获取属性"linux,usable-memory"的属性值,及属性值大小l
    reg = of_get_flat_dt_prop(node, "linux,usable-memory", &l);
    if (reg == NULL)
        reg = of_get_flat_dt_prop(node, "reg", &l);
    if (reg == NULL)
        return 0;
    
    //reg为属性值的起始地址,endp为结束地址
    endp = reg + (l / sizeof(__be32));
    
    pr_debug("memory scan node %s, reg size %ld, data: %x %x %x %x,\n",
                        uname, l, reg[0], reg[1], reg[2], reg[3]);
    
    while ((endp - reg) >= (dt_root_addr_cells + dt_root_size_cells)) {
        u64 base, size;
    
        //读出物理内存的起始地址以及size
        base = dt_mem_next_cell(dt_root_addr_cells, &reg);
        size = dt_mem_next_cell(dt_root_size_cells, &reg);
    
        if (size == 0)
            continue;
        pr_debug(" - %llx , %llx\n", (unsigned long long)base,(unsigned long long)size);
    
        //添加物理内存到memblock中进行管理,这里不再展开
        early_init_dt_add_memory_arch(base, size);
    }
    
    return 0;
}


2. 通过比较根节点属性compatible值指明和目标板为同一系列的兼容的开发板名称
     compatible制定系统的名称。它包含"<manufacture>,<model>"格式的字符串。准确地确定器件型号是非常重要的,并且我们需要包含厂商的名字来避免名字空间冲突。因为操作系统会使用compatible这个值来决定怎样在这个机器上运行,所以在这个属性中放入正确的值是非常重要的。
const void * __init of_flat_dt_match_machine(const void *default_match,
                                            const void * (*get_next_compat)(const char * const**))
{
    const void *data = NULL;
    const void *best_data = default_match;
    const char *const *compat;
    unsigned long dt_root;
    unsigned int best_score = ~1, score = 0;
    
    //读出设备树的根节点,dt_root指向根节点的属性地址处
    dt_root = of_get_flat_dt_root();

    //调用arch_get_next_mach,遍历该系统中的所有machine_desc结构,返回给data,并且返回该结构的compatible
    while ((data = get_next_compat(&compat))) {
        //将系统中的所有machine_desc结构的compatible字符串与设备树根节点的compatible属性进行match
        score = of_flat_dt_match(dt_root, compat);
        //返回与根节点属性compatible属性值最匹配的machine_desc结构
        if (score > 0 && score < best_score) {
            best_data = data;
            best_score = score;
        }
    }
    
    if (!best_data) {
        const char *prop;
        long size;
    
        pr_err("\n unrecognized device tree list:\n[ ");
     
        prop = of_get_flat_dt_prop(dt_root, "compatible", &size);
        if (prop) {
            while (size > 0) {
                printk("'%s' ", prop);
                size -= strlen(prop) + 1;
                prop += strlen(prop) + 1;
            }
        }
        printk("]\n\n");
        return NULL;
    }
     
    pr_info("Machine model: %s\n", of_flat_dt_get_machine_name());
    
    return best_data;
}


//查找设备树的根节点
unsigned long __init of_get_flat_dt_root(void)
{
    //找到设备树的设备块起始地址
    unsigned long p = ((unsigned long)initial_boot_params) +
                                    be32_to_cpu(initial_boot_params->off_dt_struct);
    
    //跳过空节点 
    while (be32_to_cpup((__be32 *)p) == OF_DT_NOP)
        p += 4;
    BUG_ON(be32_to_cpup((__be32 *)p) != OF_DT_BEGIN_NODE);
    p += 4;
    //第一个节点就是根节点,p指向根节点的属性地址处
    return ALIGN(p + strlen((char *)p) + 1, 4);
}

//arch/arm/kernel/devtree.c
__arch_info_begin 和 __arch_info_end是在 arch/arm/kernel/vmlinux.lds.S中:
00034: __arch_info_begin = .;
00035: *(.arch.info.init)
00036: __arch_info_end = .;
这里是声明了两个变量:__arch_info_begin 和 __arch_info_end,其中等号后面的"."是location counter。在__arch_info_begin 的位置上,放置所有文件中的 ".arch.info.init" 段的内容,然后紧接着是 __arch_info_end 的位置.".arch.info.init" 段中定义了设备的machine_desc结构。
//这里就是取出一个machine_desc结构
static const void * __init arch_get_next_mach(const char *const **match)
{
    static const struct machine_desc *mdesc = __arch_info_begin;
    const struct machine_desc *m = mdesc;
        
    if (m >= __arch_info_end)
        return NULL;
        
    mdesc++;//指针后移,确保下次取出下一个machine_desc结构
    *match = m->dt_compat;
    return m;//返回当前的machine_desc结构
}

//与设备树根节点进行match
int __init of_flat_dt_match(unsigned long node, const char *const *compat)
{
    //initial_boot_params指向设备树起始地址
    //node指向根节点的属性地址
    //compat为系统中machine_desc结构的compatible字符串
    return of_fdt_match(initial_boot_params, node, compat);
}

int of_fdt_match(struct boot_param_header *blob, unsigned long node,const char *const *compat)
{
    unsigned int tmp, score = 0;
        
    if (!compat)
        return 0;
    
    //遍历compatible字符串数组    
    while (*compat) {
        //返回compatible的匹配值
        tmp = of_fdt_is_compatible(blob, node, *compat);
        if (tmp && (score == 0 || (tmp < score)))
            score = tmp;//返回最大的匹配值
        compat++;//下一个字符串
    }
        
    return score;
}

int of_fdt_is_compatible(struct boot_param_header *blob,unsigned long node, const char *compat)
{
    const char *cp;
    unsigned long cplen, l, score = 0;
    
    //从根节点中读出属性"compatible"的属性值
    cp = of_fdt_get_property(blob, node, "compatible", &cplen);
    if (cp == NULL)
        return 0;
    
    //比较compatible的指定的属性字符串的一致性
    while (cplen > 0) {
        score++;
        if (of_compat_cmp(cp, compat, strlen(compat)) == 0)
            return score;
        l = strlen(cp) + 1;
        cp += l;
        cplen -= l;
    }
        
    return 0;
}


(二)、解析设备树

       unflatten_device_tree()函数来解析dtb文件,构建一个由device_node结构连接而成的单项链表,并使用全局变量of_allnodes指针来保存这个链表的头指针。内核调用OF提供的API函数获取of_allnodes链表信息来初始化内核其他子系统、设备。
void __init unflatten_device_tree(void)
{
    //解析设备树,将所有的设备节点链入全局链表    of_allnodes中
    __unflatten_device_tree(initial_boot_params, &of_allnodes,early_init_dt_alloc_memory_arch);
     
    //设置内核输出终端,以及遍历“/aliases”节点下的所有的属性,挂入相应链表
    of_alias_scan(early_init_dt_alloc_memory_arch);
}

static void __unflatten_device_tree(struct boot_param_header *blob,
                                    struct device_node **mynodes,
                                    void * (*dt_alloc)(u64 size, u64 align))
{
    unsigned long size;
    void *start, *mem;
    struct device_node **allnextp = mynodes;
    
    pr_debug(" -> unflatten_device_tree()\n");
     
    if (!blob) {
        pr_debug("No device tree pointer\n");
        return;
    }
     
    pr_debug("Unflattening device tree:\n");
    pr_debug("magic: %08x\n", be32_to_cpu(blob->magic));
    pr_debug("size: %08x\n", be32_to_cpu(blob->totalsize));
    pr_debug("version: %08x\n", be32_to_cpu(blob->version));
    
    //检查设备树magic
    if (be32_to_cpu(blob->magic) != OF_DT_HEADER) {
        pr_err("Invalid device tree blob header\n");
        return;
    }
    
    //找到设备树的设备节点起始地址
    start = ((void *)blob) + be32_to_cpu(blob->off_dt_struct);
    //第一次调用mem传0,allnextpp传NULL,实际上是为了计算整个设备树所要的空间
    size = (unsigned long)unflatten_dt_node(blob, 0, &start, NULL, NULL, 0);
    size = ALIGN(size, 4);//4字节对齐
     
    pr_debug(" size is %lx, allocating...\n", size);
     
    //调用early_init_dt_alloc_memory_arch函数,为设备树分配内存空间
    mem = dt_alloc(size + 4, __alignof__(struct device_node));
    memset(mem, 0, size);
    
    //设备树结束处赋值0xdeadbeef,为了后边检查是否有数据溢出
    *(__be32 *)(mem + size) = cpu_to_be32(0xdeadbeef); 
    pr_debug(" unflattening %p...\n", mem);
     
    //再次获取设备树的设备节点起始地址
    start = ((void *)blob) + be32_to_cpu(blob->off_dt_struct);
    //mem为设备树分配的内存空间,allnextp指向全局变量of_allnodes,生成整个设备树
    unflatten_dt_node(blob, mem, &start, NULL, &allnextp, 0);
    if (be32_to_cpup(start) != OF_DT_END)
        pr_warning("Weird tag at end of tree: %08x\n", be32_to_cpup(start));
    if (be32_to_cpup(mem + size) != 0xdeadbeef)
        pr_warning("End of tree marker overwritten: %08x\n",be32_to_cpup(mem + size));
    *allnextp = NULL;
     
    pr_debug(" <- unflatten_device_tree()\n");
}

static void * unflatten_dt_node(struct boot_param_header *blob,
                                void *mem,void **p,
                                struct device_node *dad,
                                struct device_node ***allnextpp,
                                unsigned long fpsize)
{
    struct device_node *np;
    struct property *pp, **prev_pp = NULL;
    char *pathp;
    u32 tag;
    unsigned int l, allocl;
    int has_name = 0;
    int new_format = 0;
    
    //*p指向设备树的设备块起始地址
    tag = be32_to_cpup(*p);
    //每个有孩子的设备节点,其tag一定是OF_DT_BEGIN_NODE
    if (tag != OF_DT_BEGIN_NODE) {
        pr_err("Weird tag at start of node: %x\n", tag);
        return mem;
    }

    *p += 4;//地址+4,跳过tag,这样指向节点的名称或者节点路径名
    pathp = *p;//获得节点名或者节点路径名
    l = allocl = strlen(pathp) + 1;//该节点名称的长度
    *p = PTR_ALIGN(*p + l, 4);//地址对齐后,*p指向该节点属性的地址
    
    //如果是节点名则进入,若是节点路径名则(*pathp) == '/'
    if ((*pathp) != '/') {
        new_format = 1;
        if (fpsize == 0) {//fpsize=0
            fpsize = 1;
            allocl = 2;
            l = 1;
            *pathp = '\0';
        } else {
            fpsize += l;//代分配的长度=本节点名称长度+父亲节点绝对路径的长度
            allocl = fpsize;
        }
    }
    
    //分配一个设备节点device_node结构,*mem记录分配了多大空间,最终会累加计算出该设备树总共分配的空间大小
    np = unflatten_dt_alloc(&mem, sizeof(struct device_node) + allocl,__alignof__(struct device_node));

    //第一次调用unflatten_dt_node时,allnextpp=NULL
    //第一次调用unflatten_dt_node时,allnextpp指向全局变量of_allnodes的地址
    if (allnextpp) {
        char *fn;
        //full_name保存完整的节点名,即包括各级父节点的名称
        np->full_name = fn = ((char *)np) + sizeof(*np);
        //若new_format=1,表示pathp保存的是节点名,而不是节点路径名,所以需要加上父节点的name
        if (new_format) {
            if (dad && dad->parent) {
                strcpy(fn, dad->full_name);//把父亲节点绝对路径先拷贝
                fn += strlen(fn);
            }
            *(fn++) = '/';
        }
        memcpy(fn, pathp, l);//拷贝本节点的名称
    
        //prev_pp指向节点的属性链表
        prev_pp = &np->properties;

        //当前节点插入全局链表of_allnodes
        **allnextpp = np;
        *allnextpp = &np->allnext;

        //若父亲节点不为空,则设置该节点的parent
        if (dad != NULL) {
            np->parent = dad;//指向父亲节点
            if (dad->next == NULL)//第一个孩子
                dad->child = np;//child指向第一个孩子
            else
                dad->next->sibling = np;//把np插入next,这样孩子节点形成链表
            dad->next = np;
        }
        kref_init(&np->kref);
    }

    //分析该节点的属性
    while (1) {
        u32 sz, noff;
        char *pname;
     
        //前边已经将*p移到指向节点属性的地址处,取出属性标识
        tag = be32_to_cpup(*p);
        //空属性,则跳过
        if (tag == OF_DT_NOP) {
            *p += 4;
            continue;
        }
        //tag不是属性则退出,对于有孩子节点退出时为OF_DT_BEGIN_NODE,对于叶子节点退出时为OF_DT_END_NODE
        if (tag != OF_DT_PROP)
            break;
        //地址加4,跳过tag
        *p += 4;
        //获得属性值的大小,是以为占多少整形指针计算的
        sz = be32_to_cpup(*p);
        //获取属性名称在节点的字符串块中的偏移
        noff = be32_to_cpup(*p + 4);
        //地址加8,跳过属性值的大小和属性名称在节点的字符串块中的偏移
        *p += 8;
        //地址对齐后,*P指向属性值所在的地址
        if (be32_to_cpu(blob->version) < 0x10)
            *p = PTR_ALIGN(*p, sz >= 8 ? 8 : 4);
        
        //从节点的字符串块中noff偏移处,得到该属性的name
        pname = of_fdt_get_string(blob, noff);
        if (pname == NULL) {
            pr_info("Can't find property name in list !\n");
            break;
        }

        //如果有名称为name的属性,表示变量has_name为1
        if (strcmp(pname, "name") == 0)
            has_name = 1;
        //计算该属性name的大小
        l = strlen(pname) + 1;

        //为该属性分配一个属性结构,即struct property,
        //*mem记录分配了多大空间,最终会累加计算出该设备树总共分配的空间大小
        pp = unflatten_dt_alloc(&mem, sizeof(struct property),__alignof__(struct property));

        //第一次调用unflatten_dt_node时,allnextpp=NULL
        //第一次调用unflatten_dt_node时,allnextpp指向全局变量of_allnodes的地址
        if (allnextpp) {
            if ((strcmp(pname, "phandle") == 0) || (strcmp(pname, "linux,phandle") == 0)) {
                if (np->phandle == 0)
                    np->phandle = be32_to_cpup((__be32*)*p);
            }
            if (strcmp(pname, "ibm,phandle") == 0)
                np->phandle = be32_to_cpup((__be32 *)*p);
            pp->name = pname;//属性名
            pp->length = sz;//属性值长度
            pp->value = *p;//属性值

            //属性插入该节点的属性链表np->properties
            *prev_pp = pp;
            prev_pp = &pp->next;
        }
        *p = PTR_ALIGN((*p) + sz, 4);//指向下一个属性
    }
    //至此遍历完该节点的所有属性

    //如果该节点没有"name"的属性,则为该节点生成一个name属性,插入该节点的属性链表
    if (!has_name) {
        char *p1 = pathp, *ps = pathp, *pa = NULL;
        int sz;
     
        while (*p1) {
            if ((*p1) == '@')
                pa = p1;
            if ((*p1) == '/')
                ps = p1 + 1;
                p1++;
        }
        if (pa < ps)
            pa = p1;
        sz = (pa - ps) + 1;
        pp = unflatten_dt_alloc(&mem, sizeof(struct property) + sz,__alignof__(struct property));
        if (allnextpp) {
            pp->name = "name";
            pp->length = sz;
            pp->value = pp + 1;
            *prev_pp = pp;
            prev_pp = &pp->next;
            memcpy(pp->value, ps, sz - 1);
            ((char *)pp->value)[sz - 1] = 0;
            pr_debug("fixed up name for %s -> %s\n", pathp,(char *)pp->value);
        }
    }

    //若设置了allnextpp指针
    if (allnextpp) {
        *prev_pp = NULL;
        //设置节点的名称
        np->name = of_get_property(np, "name", NULL);
        //设置该节点对应的设备类型
        np->type = of_get_property(np, "device_type", NULL);
    
        if (!np->name)
            np->name = "<NULL>";
        if (!np->type)
            np->type = "<NULL>";
    }

    //前边在遍历属性时,tag不是属性则退出
    //对于有孩子节点退出时tag为OF_DT_BEGIN_NODE,对于叶子节点退出时tag为OF_DT_END_NODE
    while (tag == OF_DT_BEGIN_NODE || tag == OF_DT_NOP) {
        //空属性则指向下个属性
        if (tag == OF_DT_NOP)
            *p += 4;
        else
            //OF_DT_BEGIN_NODE则表明其还有子节点,所以递归分析其子节点
            mem = unflatten_dt_node(blob, mem, p, np, allnextpp,fpsize);
        tag = be32_to_cpup(*p);
    }

    //对于叶子节点或者分析完成
    if (tag != OF_DT_END_NODE) {
        pr_err("Weird tag at end of node: %x\n", tag);
        return mem;
    }
    *p += 4;
    //mem返回整个设备树所分配的内存大小,即设备树占的内存空间
    return mem;
}


//从mem分配内存空间,*mem记录分配了多大空间
static void *unflatten_dt_alloc(void **mem, unsigned long size,unsigned long align)
{
    void *res;
     
    *mem = PTR_ALIGN(*mem, align);
    res = *mem;
    *mem += size;
        
    return res;
}

//一个特定的节点通常是以完整的路径来引用,比如/external-bus/ethernet@0,0,不过当一个用户真的想知道“哪个设备是eth0”时,这将会很繁琐。aliases节点可以用来为一个完整的设备路径分配一个短的别名。比如:
//aliases {
//    serial0 = &uart0;
//    serial1 = &uart1;
//    serial2 = &uart2;
//    serial3 = &uart3;
//    ethernet0 = ð0;
//    serial0 = &serial0;
//};
//当需要为设备指定一个标示符时,操作系统欢迎大家使用别名。
//设置内核输出终端,以及遍历“/aliases”节点下的所有的属性,挂入相应链表
void of_alias_scan(void * (*dt_alloc)(u64 size, u64 align))
{
    struct property *pp;
    
    //根据全局的device_node结构的链表of_allnodes,查找节点名为“/chosen”或者“/chosen@0”的节点,赋值给全局变量of_chosen
    of_chosen = of_find_node_by_path("/chosen");
    if (of_chosen == NULL)
        of_chosen = of_find_node_by_path("/chosen@0");
    
    //找到的话,则在该节点查找"linux,stdout-path"    属性
    //"linux,stdout-path"的属性值,常常为标准终端设备的节点路径名,内核会以此作为默认终端
    if (of_chosen) {
        const char *name;
        //返回属性"linux,stdout-path"的属性值
        name = of_get_property(of_chosen, "linux,stdout-path", NULL);
        //根据属性值查找设备节点device_node,即内核默认终端的设备节点,赋值给全局变量of_stdout
        if (name)
            of_stdout = of_find_node_by_path(name);
    }
    
    //据全局链表of_allnodes,查找节点名为“/aliases”的节点,赋值给全局变量of_aliases
    of_aliases = of_find_node_by_path("/aliases");
    if (!of_aliases)
        return;
     
    //遍历“/aliases”节点下的所有的属性
    for_each_property_of_node(of_aliases, pp) {
        const char *start = pp->name;//属性名
        const char *end = start + strlen(start);//属性名结尾
        struct device_node *np;
        struct alias_prop *ap;
        int id, len;
        
        //跳过"name"、"phandle"和"linux,phandle"的属性
        if (!strcmp(pp->name, "name") ||
            !strcmp(pp->name, "phandle") ||
            !strcmp(pp->name, "linux,phandle"))
            continue;
            
            //根据属性值找到对应的设备节点
            np = of_find_node_by_path(pp->value);
            if (!np)
                continue;
        
            //去除属性名中结尾的数字,即设备id
            while (isdigit(*(end-1)) && end > start)
                end--;
            //len为属性名去掉结尾数字序号的长度
            len = end - start;
        
            //此时end指向属性名中结尾的数字,即开始时start指向“&uart0”,end指向字符串结尾。
            //经过上步操作,start仍指向“&uart0”字符串开始处,而end指向字符‘0’。
            //将end字符串转化为10进制数,赋值给id,作为设备的id号
            if (kstrtoint(end, 10, &id) < 0)
                continue;
        
            //分配alias_prop结构
            ap = dt_alloc(sizeof(*ap) + len + 1, 4);
            if (!ap)
                continue;
            memset(ap, 0, sizeof(*ap) + len + 1);
            ap->alias = start;
            //将该设备的aliases指向对应的device_node,并且链入aliases_lookup链表中
            of_alias_add(ap, np, id, start, len);
        }
}


四、OF提供的常用API函数
    OF提供的函数主要集中在drivers/of/目录下,有address.c,base.c,device.c,fdt.c,irq.c,platform.c等等
1. 用来查找在dtb中的根节点
unsigned long __init of_get_flat_dt_root(void)


2. 根据deice_node结构的full_name参数,在全局链表of_allnodes中,查找合适的device_node
struct device_node *of_find_node_by_path(const char *path)
例如:
struct device_node *cpus;
cpus=of_find_node_by_path("/cpus");


3. 若from=NULL,则在全局链表of_allnodes中根据name查找合适的device_node
struct device_node *of_find_node_by_name(struct device_node *from,const char *name)
例如:
struct device_node *np;
np = of_find_node_by_name(NULL,"firewire");


4. 根据设备类型查找相应的device_node
struct device_node *of_find_node_by_type(struct device_node *from,const char *type)
例如:
struct device_node *tsi_pci;
tsi_pci= of_find_node_by_type(NULL,"pci");


5. 根据compatible字符串查找device_node
struct device_node *of_find_compatible_node(struct device_node *from,const char *type, const char *compatible)


6. 根据节点属性的name查找device_node
struct device_node *of_find_node_with_property(struct device_node *from,const char *prop_name)


7. 根据phandle查找device_node
struct device_node *of_find_node_by_phandle(phandle handle)


8. 根据alias的name获得设备id号
int of_alias_get_id(struct device_node *np, const char *stem)


9. device node计数增加/减少
struct device_node *of_node_get(struct device_node *node)
void of_node_put(struct device_node *node)


10. 根据property结构的name参数,在指定的device node中查找合适的property
struct property *of_find_property(const struct device_node *np,const char *name,int *lenp)


11. 根据property结构的name参数,返回该属性的属性值
const void *of_get_property(const struct device_node *np, const char *name,int *lenp)


12. 根据compat参数与device node的compatible匹配,返回匹配度
int of_device_is_compatible(const struct device_node *device,const char *compat)


13. 获得父节点的device node
struct device_node *of_get_parent(const struct device_node *node)


14. 将matches数组中of_device_id结构的name和type与device node的compatible和type匹配,返回匹配度最高的of_device_id结构
const struct of_device_id *of_match_node(const struct of_device_id *matches,const struct device_node *node)


15. 根据属性名propname,读出属性值中的第index个u32数值给out_value
int of_property_read_u32_index(const struct device_node *np,const char *propname,u32 index, u32 *out_value)


16. 根据属性名propname,读出该属性的数组中sz个属性值给out_values
int of_property_read_u8_array(const struct device_node *np,const char *propname, u8 *out_values, size_t sz)
int of_property_read_u16_array(const struct device_node *np,const char *propname, u16 *out_values, size_t sz)
int of_property_read_u32_array(const struct device_node *np,const char *propname, u32 *out_values,size_t sz)


17. 根据属性名propname,读出该属性的u64属性值
int of_property_read_u64(const struct device_node *np, const char *propname,u64 *out_value)


18. 根据属性名propname,读出该属性的字符串属性值
int of_property_read_string(struct device_node *np, const char *propname,const char **out_string)


19. 根据属性名propname,读出该字符串属性值数组中的第index个字符串
int of_property_read_string_index(struct device_node *np, const char *propname,int index, const char **output)


20. 读取属性名propname中,字符串属性值的个数
int of_property_count_strings(struct device_node *np, const char *propname)


21. 读取该设备的第index个irq号
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)


22. 读取该设备的第index个irq号,并填充一个irq资源结构体
int of_irq_to_resource(struct device_node *dev, int index, struct resource *r)


23. 获取该设备的irq个数
int of_irq_count(struct device_node *dev)


24. 获取设备寄存器地址,并填充寄存器资源结构体
int of_address_to_resource(struct device_node *dev, int index,struct resource *r)
const __be32 *of_get_address(struct device_node *dev, int index, u64 *size,unsigned int *flags)


25. 获取经过映射的寄存器虚拟地址
void __iomem *of_iomap(struct device_node *np, int index)


24. 根据device_node查找返回该设备对应的platform_device结构
struct platform_device *of_find_device_by_node(struct device_node *np)


25. 根据device node,bus id以及父节点创建该设备的platform_device结构
struct platform_device *of_device_alloc(struct device_node *np,const char *bus_id,struct device *parent)
static struct platform_device *of_platform_device_create_pdata(struct device_node *np,const char *bus_id,
                                                            void *platform_data,struct device *parent)


26. 遍历of_allnodes中的节点挂接到of_platform_bus_type总线上,由于此时of_platform_bus_type总线上还没有驱动,所以此时不进行匹配
int of_platform_bus_probe(struct device_node *root,const struct of_device_id *matches,struct device *parent)


27. 遍历of_allnodes中的所有节点,生成并初始化platform_device结构
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 = root ? of_node_get(root) : of_find_node_by_path("/

你可能感兴趣的:(linux,内核移植,exynos4412)