zoukankan      html  css  js  c++  java
  • Linux 内核:设备树(2)dtb转换成device_node

    Linux 内核:设备树(2)dtb转换成device_node

    背景

    前面我们了解到dtb的内存分布以后(dtb格式),接下来就来看看内核是如何把设备树解析成所需的device_node

    原文(有删改):https://www.cnblogs.com/downey-blog/p/10485596.html

    基于arm平台,Linux 4.14

    设备树的执行入口setup_arch

    linux最底层的初始化部分在HEAD.s中,这是汇编代码,我们暂且不作过多讨论。

    在head.s完成部分初始化之后,就开始调用C语言函数,而被调用的第一个C语言函数就是start_kernel

    asmlinkage __visible void __init start_kernel(void)
    {
        //...
        setup_arch(&command_line);
        //...
    }
    

    而对于设备树的处理,基本上就在setup_arch()这个函数中。

    可以看到,在start_kernel()中调用了setup_arch(&command_line);

    void __init setup_arch(char **cmdline_p)
    {
        const struct machine_desc *mdesc;
        
        // 根据传入的设备树dtb的首地址完成一些初始化操作
        mdesc = setup_machine_fdt(__atags_pointer);
        
        // ...
        
        // 保证设备树dtb本身存在于内存中而不被覆盖
        arm_memblock_init(mdesc);
        
        // ...
        // 对设备树具体的解析
        unflatten_device_tree();
        // ...
    }
    

    这三个被调用的函数就是主要的设备树处理函数:

    • setup_machine_fdt():根据传入的设备树dtb的首地址完成一些初始化操作。
    • arm_memblock_init():主要是内存相关函数,为设备树保留相应的内存空间,保证设备树dtb本身存在于内存中而不被覆盖。用户可以在设备树中设置保留内存,这一部分同时作了保留指定内存的工作。
    • unflatten_device_tree():对设备树具体的解析,事实上在这个函数中所做的工作就是将设备树各节点转换成相应的struct device_node结构体。

    下面我们再来通过代码跟踪仔细分析。

    setup_machine_fdt

        const struct machine_desc *mdesc;
        
        // 根据传入的设备树dtb的首地址完成一些初始化操作
        mdesc = setup_machine_fdt(__atags_pointer);
    

    __atags_pointer这个全局变量存储的就是r2的寄存器值,是设备树在内存中的起始地址,将设备树起始地址传递给setup_machine_fdt,对设备树进行解析。

    const struct machine_desc * __init setup_machine_fdt(unsigned int dt_phys)
    {
        const struct machine_desc *mdesc, *mdesc_best = NULL;
        // 内存地址检查
        if (!dt_phys || !early_init_dt_verify(phys_to_virt(dt_phys)))
            return NULL;
    
        // 读取 compatible 属性
        mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);
    
        // 扫描各个子节点
        early_init_dt_scan_nodes();
        // ...
    }
    

    setup_machine_fdt主要是获取了一些设备树提供的总览信息。

    内存地址检查

    先将设备树在内存中的物理地址转换为虚拟地址

    然后再检查该地址上是否有设备树的魔数(magic),魔数就是一串用于识别的字节码:

    • 如果没有或者魔数不匹配,表明该地址没有设备树文件,函数返回失败
    • 否则验证成功,将设备树地址赋值给全局变量initial_boot_params

    读取compatible属性

    逐一读取设备树根目录下的compatible属性。

    /**
     * of_flat_dt_match_machine - Iterate match tables to find matching machine.
     *
     * @default_match: A machine specific ptr to return in case of no match.
     * @get_next_compat: callback function to return next compatible match table.
     *
     * Iterate through machine match tables to find the best match for the machine
     * compatible string in the FDT.
     */
    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 = of_get_flat_dt_root();
        // 遍历
        while ((data = get_next_compat(&compat))) {
            // 将compatible中的属性一一与内核中支持的硬件单板相对比,
            // 匹配成功后返回相应的machine_desc结构体指针。
            score = of_flat_dt_match(dt_root, compat);
            if (score > 0 && score < best_score) {
                best_data = data;
                best_score = score;
            }
        }
    
        // ...
    
        pr_info("Machine model: %s
    ", of_flat_dt_get_machine_name());
    
        return best_data;
    }
    

    machine_desc结构体中描述了单板相关的一些硬件信息,这里不过多描述。

    主要的的行为就是根据这个compatible属性选取相应的硬件单板描述信息;一般compatible属性名就是"厂商,芯片型号"。

    扫描各子节点

    第三部分就是扫描设备树中的各节点,主要分析这部分代码。

    void __init early_init_dt_scan_nodes(void)
    {
        of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);
        of_scan_flat_dt(early_init_dt_scan_root, NULL);
        of_scan_flat_dt(early_init_dt_scan_memory, NULL);
    }
    

    出人意料的是,这个函数中只有一个函数的三个调用,每次调用时,参数不一样。

    of_scan_flat_dt

    首先of_scan_flat_dt()这个函数接收两个参数,一个是函数指针it,一个为相当于函数it执行时的参数。

    /**
     * of_scan_flat_dt - scan flattened tree blob and call callback on each.
     * @it: callback function
     * @data: context data pointer
     *
     * This function is used to scan the flattened device-tree, it is
     * used to extract the memory information at boot before we can
     * unflatten the tree
     */
    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 {
            u32 tag = be32_to_cpup((__be32 *)p);
            const char *pathp;
    
            p += 4;
            if (tag == OF_DT_END_NODE) {
                depth--;
                continue;
            }
            if (tag == OF_DT_NOP)
                continue;
            if (tag == OF_DT_END)
                break;
            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);
                continue;
            }
            if (tag != OF_DT_BEGIN_NODE) {
                pr_err("Invalid tag %x in flat device tree!
    ", tag);
                return -EINVAL;
            }
            depth++;
            pathp = (char *)p;
            p = ALIGN(p + strlen(pathp) + 1, 4);
            if (*pathp == '/')
                pathp = kbasename(pathp);
            rc = it(p, pathp, depth, data);
            if (rc != 0)
                break;
        } while (1);
    
        return rc;
    }
    

    结论:of_scan_flat_dt()函数的作用就是扫描设备树中的节点,然后对各节点分别调用传入的回调函数。

    那么重点关注函数指针,在上述代码中,传入的参数分别为

    • early_init_dt_scan_chosen
    • ``early_init_dt_scan_root`
    • early_init_dt_scan_memory

    从名称可以猜测,这三个函数分别是处理chosen节点、root节点中除子节点外的属性信息、memory节点。

    early_init_dt_scan_chosen

    of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);
    

    boot_command_lineboot_command_line是一个静态数组,存放着启动参数,

    int __init early_init_dt_scan_chosen(unsigned long node, const char *uname,int depth, void *data){
        // ...
        p = of_get_flat_dt_prop(node, "bootargs", &l);
        if (p != NULL && l > 0)
    	    strlcpy(data, p, min((int)l, COMMAND_LINE_SIZE));
        // ...
    }
    
    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
    ", depth, uname);
    
        if (depth != 1 || !data ||
            (strcmp(uname, "chosen") != 0 && strcmp(uname, "chosen@0") != 0))
            return 0;
    
        early_init_dt_check_for_initrd(node);
    
        /* Retrieve command line */
        // 找到设备树中的的chosen节点中的bootargs,并作为cmd_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
    ", (char*)data);
    
        /* break now */
        return 1;
    }
    

    经过代码分析,early_init_dt_scan_chosen的作用是获取从chosen节点中获取bootargs,然后将bootargs放入boot_command_line中,作为启动参数。

    而非字面意思的处理整个chosen

    以我之前调过的zynq平台为例:

    / {
        model = "ZynqMP ZCU104 RevA";
        compatible = "xlnx,zynqmp-zcu104-revA", "xlnx,zynqmp-zcu104", "xlnx,zynqmp";
    
        aliases {
            ethernet0 = &gem3;
            gpio0 = &gpio;
            i2c0 = &i2c1;
            mmc0 = &sdhci1;
            rtc0 = &rtc;
            serial0 = &uart0;
            serial1 = &uart1;
            serial2 = &dcc;
            spi0 = &qspi;
            usb0 = &usb0;
        };
    
        chosen {
            bootargs = "earlycon";
            stdout-path = "serial0:115200n8";
        };
    
        memory@0 {
            device_type = "memory";
            reg = <0x0 0x0 0x0 0x80000000>;
        };
    };
    

    在支持设备树的嵌入式系统中,实际上:

    • uboot基本上可以不通过显式的bootargs=xxx来传递给内核,而是在env拿出,并存放进设备树中的chosen节点中
    • Linux也开始在设备树中的chosen节点中获取出来,

    这样子就可以做到针对uboot与Linux在bootargs传递上的统一。

    early_init_dt_scan_root

    int __init early_init_dt_scan_root(unsigned long node, const char *uname,int depth, void *data)
    {
        dt_root_size_cells = OF_ROOT_NODE_SIZE_CELLS_DEFAULT;
        dt_root_addr_cells = OF_ROOT_NODE_ADDR_CELLS_DEFAULT;
    
        prop = of_get_flat_dt_prop(node, "#size-cells", NULL);
        if (prop)
            dt_root_size_cells = be32_to_cpup(prop);
        prop = of_get_flat_dt_prop(node, "#address-cells", NULL);
        if (prop)
            dt_root_addr_cells = be32_to_cpup(prop);
        // ...
    }
    

    通过进一步代码分析,early_init_dt_scan_root为了将root节点中的#size-cells#address-cells属性提取出来,并非获取root节点中所有的属性,放到全局变量dt_root_size_cellsdt_root_addr_cells中。

    size-cells和address-cells表示对一个属性(通常是reg属性)的地址需要多少个四字节描述,而地址的长度需要多少个四字节描述,数据长度基本单位为4。

    // 表示数据大小为一个4字节描述,32位
    #size-cells = 1
    
    // 表示地址由一个四字节描述
    #address-cells = 1
    
    // 而reg属性由四个四字节组成,所以存在两组地址描述,
    // 第一组是起始地址为0x12345678,长度为0x100,
    // 第二组起始地址为0x22,长度为0x4, 
    // 因为在<>中,所有数据都是默认为32位。
    reg = <0x12345678 0x100 0x22 0x4>  
    

    early_init_dt_scan_memory

    int __init early_init_dt_scan_memory(unsigned long node, const char *uname,int depth, void *data){
        // ...
        if (!IS_ENABLED(CONFIG_PPC32) || depth != 1 || strcmp(uname, "memory@0") != 0)
    		return 0;
        reg = of_get_flat_dt_prop(node, "reg", &l);
        endp = reg + (l / sizeof(__be32));
    
        while ((endp - reg) >= (dt_root_addr_cells + dt_root_size_cells)) {
            base = dt_mem_next_cell(dt_root_addr_cells, &reg);
    	    size = dt_mem_next_cell(dt_root_size_cells, &reg);
            early_init_dt_add_memory_arch(base, size);
        }
    }
    

    函数先判断节点的unit name是memory@0,如果不是,则返回。然后将所有memory相关的reg属性取出来,根据address-cell和size-cell的值进行解析,然后调用early_init_dt_add_memory_arch()来申请相应的内存空间。

        memory@0 {
            device_type = "memory";
            reg = <0x0 0x0 0x0 0x80000000>, <0x8 0x00000000 0x0 0x80000000>;
        };
    

    到这里,setup_machine_fdt()函数对于设备树的第一次扫描解析就完成了,主要是获取了一些设备树提供的总览信息。

    arm_memblock_init

    // arch/arm/mm/init.c
    void __init arm_memblock_init(const struct machine_desc *mdesc)
    {
        // ...
        early_init_fdt_reserve_self();
        early_init_fdt_scan_reserved_mem();
        // ...
    }
    

    对于设备树的初始化而言,主要做了两件事:

    • 调用early_init_fdt_reserve_self,根据设备树的大小为设备树分配空间,设备树的totalsize在dtb头部中有指明,因此当系统启动之后,设备树就一直存在在系统中。
    • 扫描设备树节点中的"reserved-memory"节点,为其分配保留空间。

    memblock_init对于设备树的部分解析就完成了,主要是为设备树指定保留内存空间。

    unflatten_device_tree

    这一部分就进入了设备树的解析部分:

    注意of_root这个对象,我们后续文章中会提到它。实际上,解析以后的数据都是放在了这个对象里面。

    void __init unflatten_device_tree(void)
    {
        // 展开设备树
        __unflatten_device_tree(initial_boot_params, NULL, &of_root,early_init_dt_alloc_memory_arch, false);
    
        // 扫描设备树
        of_alias_scan(early_init_dt_alloc_memory_arch);
        // ...
    }
    

    展开设备树

    property原型

    struct property {
        char	*name;
        int	length;
        void	*value;
        struct property *next;
        // ...
    };
    

    在设备树中,对于属性的描述是key = value,这个结构体中的name和value分别对应key和value,而length表示value的长度;

    next指针指向下一个struct property结构体(用于构成单链表)。

    __unflatten_device_tree

    __unflatten_device_tree(initial_boot_params, NULL, &of_root,early_init_dt_alloc_memory_arch, false);
    

    我们再来看最主要的设备树解析函数:

    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;
        // ...
        size = unflatten_dt_nodes(blob, NULL, dad, NULL);
        // ...
        mem = dt_alloc(size + 4, __alignof__(struct device_node));
        // ...
        unflatten_dt_nodes(blob, mem, dad, mynodes);
    }
    

    主要的解析函数为unflatten_dt_nodes(),在__unflatten_device_tree()函数中,unflatten_dt_nodes()被调用两次:

    • 第一次是扫描得出设备树转换成device node需要的空间,然后系统申请内存空间;
    • 第二次就进行真正的解析工作,我们继续看unflatten_dt_nodes()函数:

    值得注意的是,在第二次调用unflatten_dt_nodes()时传入的参数为unflatten_dt_nodes(blob, mem, dad, mynodes);

    unflatten_dt_nodes

    第一个参数是设备树存放首地址,第二个参数是申请的内存空间,第三个参数为父节点,初始值为NULL,第四个参数为mynodes,初始值为of_node.

    static int unflatten_dt_nodes(const void *blob,void *mem,struct device_node *dad,struct device_node **nodepp)
    {
        // ...
        for (offset = 0;offset >= 0 && depth >= initial_depth;offset = fdt_next_node(blob, offset, &depth)) {
            populate_node(blob, offset, &mem,nps[depth],fpsizes[depth],&nps[depth+1], dryrun);
            // ...
        }
    }
    

    这个函数中主要的作用就是从根节点开始,对子节点依次调用populate_node(),从函数命名上来看,这个函数就是填充节点,为节点分配内存。

    device_node原型
    // include/linux/of.h
    struct device_node {
        const char *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	kobject kobj;
        unsigned long _flags;
        void	*data;
        // ...
    };
    
    • name:设备节点中的name属性转换而来。
    • type:由设备节点中的device_type转换而来。
    • phandle:有设备节点中的"phandle"和"linux,phandle"属性转换而来,特殊的还可能由"ibm,phandle"属性转换而来。
    • full_name:这个指针指向整个结构体的结尾位置,在结尾位置存储着这个结构体对应设备树节点的unit_name,意味着一个struct device_node结构体占内存空间为sizeof(struct device_node)+strlen(unit_name)+字节对齐
    • properties:这是一个设备树节点的属性链表,属性可能有很多种,比如:"interrupts","timer","hwmods"等等。
    • parent,child,sibling:与当前属性链表节点相关节点,所以相关链表节点构成整个device_node的属性节点。
    • kobj:用于在/sys目录下生成相应用户文件。
    populate_node
    static unsigned int populate_node(const void *blob,int offset,void **mem,
    			  struct device_node *dad,unsigned int fpsize,struct device_node **pnp,bool dryrun){
        struct device_node *np;
        // 申请内存
        // 注,allocl是节点的unit_name长度(类似于chosen、memory这类子节点描述开头时的名字,并非.name成员)
        np = unflatten_dt_alloc(mem, sizeof(struct device_node) + allocl,__alignof__(struct device_node));
        
        // 初始化node(设置kobj,接着设置node的fwnode.ops。)
        of_node_init(np);
        
        // 将device_node的full_name指向结构体结尾处,
        // 即,将一个节点的unit name放置在一个struct device_node的结尾处。
        np->full_name = fn = ((char *)np) + sizeof(*np);
        
        // 设置其 父节点 和 兄弟节点(如果有父节点)
        if (dad != NULL) {
    		np->parent = dad;
    		np->sibling = dad->child;
    		dad->child = np;
    	}
        
        // 为节点的各个属性分配空间
        populate_properties(blob, offset, mem, np, pathp, dryrun);
        
        // 获取,并设置device_node节点的name和type属性
        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>";
        // ...
    }  
    

    一个设备树中节点转换成一个struct device_node结构的过程渐渐就清晰起来,现在我们接着看看populate_properties()这个函数,看看属性是怎么解析的,

    populate_properties
    static void populate_properties(const void *blob,int offset,void **mem,struct device_node *np,const char *nodename,bool dryrun){
        // ...
        for (cur = fdt_first_property_offset(blob, offset);
             cur >= 0;
             cur = fdt_next_property_offset(blob, cur)) 
        {
            fdt_getprop_by_offset(blob, cur, &pname, &sz);
            unflatten_dt_alloc(mem, sizeof(struct property),__alignof__(struct property));
            if (!strcmp(pname, "phandle") ||  !strcmp(pname, "linux,phandle")) {
                if (!np->phandle)
                    np->phandle = be32_to_cpup(val);
    
                pp->name   = (char *)pname;
                pp->length = sz;
                pp->value  = (__be32 *)val;
                *pprev     = pp;
                pprev      = &pp->next;
                // ...
            }
        }
    }
    

    从属性转换部分的程序可以看出,对于大部分的属性,都是直接填充一个struct property属性;

    而对于"phandle"属性和"linux,phandle"属性,直接填充struct device_node phandle字段,不放在属性链表中。

    扫描节点:of_alias_scan

    从名字来看,这个函数的作用是解析根目录下的alias

    struct device_node *of_chosen;
    struct device_node *of_aliases;
    
    void of_alias_scan(void * (*dt_alloc)(u64 size, u64 align)){
        of_aliases = of_find_node_by_path("/aliases");
        of_chosen = of_find_node_by_path("/chosen");
        if (of_chosen) {
            if (of_property_read_string(of_chosen, "stdout-path", &name))
                of_property_read_string(of_chosen, "linux,stdout-path",
                                        &name);
            if (IS_ENABLED(CONFIG_PPC) && !name)
                of_property_read_string(of_aliases, "stdout", &name);
            if (name)
                of_stdout = of_find_node_opts_by_path(name, &of_stdout_options);
        }
        for_each_property_of_node(of_aliases, pp) {
            // ...
            ap = dt_alloc(sizeof(*ap) + len + 1, __alignof__(*ap));
            if (!ap)
                continue;
            memset(ap, 0, sizeof(*ap) + len + 1);
            ap->alias = start;
            of_alias_add(ap, np, id, start, len);
            // ...
        }
    }
    

    of_alias_scan()函数先是处理设备树chosen节点中的"stdout-path"或者"stdout"属性(两者最多存在其一),然后将stdout指定的path赋值给全局变量of_stdout_options,并将返回的全局struct device_node类型数据赋值给of_stdout,指定系统启动时的log输出。

    接下来为aliases节点申请内存空间,如果一个节点中同时没有name/phandle/linux,phandle,则被定义为特殊节点,对于这些特殊节点将不会申请内存空间。

    然后,使用of_alias_add()函数将所有的aliases内容放置在aliases_lookup链表中。

    转换过程总结

    此后,内核就可以根据device_node来创建设备。

    如果说我的文章对你有用,只不过是我站在巨人的肩膀上再继续努力罢了。
    若在页首无特别声明,本篇文章由 Schips 经过整理后发布。
    博客地址:https://www.cnblogs.com/schips/
  • 相关阅读:
    低于时钟频率的任意频率生成(相位累加器)
    verilog实现奇数倍分频
    No.135 Candy
    No.42 Trapping Rain Water
    No.149 Max Point on a Line
    No.147 Insertion Sorted List
    No.21 Merge Two Sorted List
    No.88 Merge Sorted Array
    No.148 Sort List
    No.206 Reverse Linked List
  • 原文地址:https://www.cnblogs.com/schips/p/linux_driver_dtb_to_device_node.html
Copyright © 2011-2022 走看看