Device Tree(三):代码分析

作者:linuxer 发布于:2014-6-6 16:03 分类:统一设备模型

一、前言

Device Tree总共有三篇,分别是:

1、为何要引入Device Tree,这个机制是用来解决什么问题的?(请参考引入Device Tree的原因

2、Device Tree的基础概念(请参考DT基础概念

3、ARM linux中和Device Tree相关的代码分析(这是本文的主题)

本文主要内容是:以Device Tree相关的数据流分析为索引,对ARM linux kernel的代码进行解析。主要的数据流包括:

1、初始化流程。也就是扫描dtb并将其转换成Device Tree Structure。

2、传递运行时参数传递以及platform的识别流程分析

3、如何将Device Tree Structure并入linux kernel的设备驱动模型。

注:本文中的linux kernel使用的是3.14版本。

 

二、如何通过Device Tree完成运行时参数传递以及platform的识别功能?

1、汇编部分的代码分析

linux/arch/arm/kernel/head.S文件定义了bootloader和kernel的参数传递要求:

MMU = off, D-cache = off, I-cache = dont care, r0 = 0, r1 = machine nr, r2 = atags or dtb pointer.

目前的kernel支持旧的tag list的方式,同时也支持device tree的方式。r2可能是device tree binary file的指针(bootloader要传递给内核之前要copy到memory中),也可以能是tag list的指针。在ARM的汇编部分的启动代码中(主要是head.S和head-common.S),machine type ID和指向DTB或者atags的指针被保存在变量__machine_arch_type和__atags_pointer中,这么做是为了后续c代码进行处理。

2、和device tree相关的setup_arch代码分析

具体的c代码都是在setup_arch中处理,这个函数是一个总的入口点。具体代码如下(删除了部分无关代码):

void __init setup_arch(char **cmdline_p)
{
    const struct machine_desc *mdesc;

……

    mdesc = setup_machine_fdt(__atags_pointer);
    if (!mdesc)
        mdesc = setup_machine_tags(__atags_pointer, __machine_arch_type);
    machine_desc = mdesc;
    machine_name = mdesc->name;

……
}

对于如何确定HW platform这个问题,旧的方法是静态定义若干的machine描述符(struct machine_desc ),在启动过程中,通过machine type ID作为索引,在这些静态定义的machine描述符中扫描,找到那个ID匹配的描述符。在新的内核中,首先使用setup_machine_fdt来setup machine描述符,如果返回NULL,才使用传统的方法setup_machine_tags来setup machine描述符。传统的方法需要给出__machine_arch_type(bootloader通过r1寄存器传递给kernel的)和tag list的地址(用来进行tag parse)。__machine_arch_type用来寻找machine描述符;tag list用于运行时参数的传递。随着内核的不断发展,相信有一天linux kernel会完全抛弃tag list的机制。

3、匹配platform(machine描述符)

setup_machine_fdt函数的功能就是根据Device Tree的信息,找到最适合的machine描述符。具体代码如下:

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_scan(phys_to_virt(dt_phys)))
        return NULL;

    mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);

    if (!mdesc) { 
        出错处理
    }

    /* Change machine number to match the mdesc we're using */
    __machine_arch_type = mdesc->nr;

    return mdesc;
}

early_init_dt_scan函数有两个功能,一个是为后续的DTB scan进行准备工作,另外一个是运行时参数传递。具体请参考下面一个section的描述。

of_flat_dt_match_machine是在machine描述符的列表中scan,找到最合适的那个machine描述符。我们首先看如何组成machine描述符的列表。和传统的方法类似,也是静态定义的。DT_MACHINE_START和MACHINE_END用来定义一个machine描述符。编译的时候,compiler会把这些machine descriptor放到一个特殊的段中(.arch.info.init),形成machine描述符的列表。machine描述符用下面的数据结构来标识(删除了不相关的member):

struct machine_desc {
    unsigned int        nr;        /* architecture number    */
    const char *const     *dt_compat;    /* array of device tree 'compatible' strings    */

……

   };

nr成员就是过去使用的machine type ID。内核machine描述符的table有若干个entry,每个都有自己的ID。bootloader传递了machine type ID,指明使用哪一个machine描述符。目前匹配machine描述符使用compatible strings,也就是dt_compat成员,这是一个string list,定义了这个machine所支持的列表。在扫描machine描述符列表的时候需要不断的获取下一个machine描述符的compatible字符串的信息,具体的代码如下:

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++;
    *match = m->dt_compat;
    return m;
}

__arch_info_begin指向machine描述符列表第一个entry。通过mdesc++不断的移动machine描述符指针(Note:mdesc是static的)。match返回了该machine描述符的compatible string list。具体匹配的算法倒是很简单,就是比较字符串而已,一个是root node的compatible字符串列表,一个是machine描述符的compatible字符串列表,得分最低的(最匹配的)就是我们最终选定的machine type。

4、运行时参数传递

运行时参数是在扫描DTB的chosen node时候完成的,具体的动作就是获取chosen node的bootargs、initrd等属性的value,并将其保存在全局变量(boot_command_line,initrd_start、initrd_end)中。使用tag list方法是类似的,通过分析tag list,获取相关信息,保存在同样的全局变量中。具体代码位于early_init_dt_scan函数中:

bool __init early_init_dt_scan(void *params)
{
    if (!params)
        return false;

    /* 全局变量initial_boot_params指向了DTB的header*/
    initial_boot_params = params;

    /* 检查DTB的magic,确认是一个有效的DTB */
    if (be32_to_cpu(initial_boot_params->magic) != OF_DT_HEADER) {
        initial_boot_params = NULL;
        return false;
    }

    /* 扫描 /chosen node,保存运行时参数(bootargs)到boot_command_line,此外,还处理initrd相关的property,并保存在initrd_start和initrd_end这两个全局变量中 */
    of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);

    /* 扫描根节点,获取 {size,address}-cells信息,并保存在dt_root_size_cells和dt_root_addr_cells全局变量中 */
    of_scan_flat_dt(early_init_dt_scan_root, NULL);

    /* 扫描DTB中的memory node,并把相关信息保存在meminfo中,全局变量meminfo保存了系统内存相关的信息。*/
    of_scan_flat_dt(early_init_dt_scan_memory, NULL);

    return true;
}

设定meminfo(该全局变量确定了物理内存的布局)有若干种途径:

1、通过tag list(tag是ATAG_MEM)传递memory bank的信息。

2、通过command line(可以用tag list,也可以通过DTB)传递memory bank的信息。

3、通过DTB的memory node传递memory bank的信息。

目前当然是推荐使用Device Tree的方式来传递物理内存布局信息。

 

三、初始化流程

在系统初始化的过程中,我们需要将DTB转换成节点是device_node的树状结构,以便后续方便操作。具体的代码位于setup_arch->unflatten_device_tree中。

void __init unflatten_device_tree(void)
{
    __unflatten_device_tree(initial_boot_params, &of_allnodes,
                early_init_dt_alloc_memory_arch);

    /* Get pointer to "/chosen" and "/aliases" nodes for use everywhere */
    of_alias_scan(early_init_dt_alloc_memory_arch);
}

我们用struct device_node 来抽象设备树中的一个节点,具体解释如下:

struct device_node {
    const char *name;----------------------device node name
    const char *type;-----------------------对应device_type的属性
    phandle phandle;-----------------------对应该节点的phandle属性
    const char *full_name; ----------------从“/”开始的,表示该node的full path

    struct    property *properties;-------------该节点的属性列表
    struct    property *deadprops; ----------如果需要删除某些属性,kernel并非真的删除,而是挂入到deadprops的列表
    struct    device_node *parent;------parent、child以及sibling将所有的device node连接起来
    struct    device_node *child;
    struct    device_node *sibling;
    struct    device_node *next;  --------通过该指针可以获取相同类型的下一个node
    struct    device_node *allnext;-------通过该指针可以获取node global list下一个node
    struct    proc_dir_entry *pde;--------开放到userspace的proc接口信息
    struct    kref kref;-------------该node的reference count
    unsigned long _flags;
    void    *data;
};

unflatten_device_tree函数的主要功能就是扫描DTB,将device node被组织成:

1、global list。全局变量struct device_node *of_allnodes就是指向设备树的global list

2、tree。

这些功能主要是在__unflatten_device_tree函数中实现,具体代码如下(去掉一些无关紧要的代码):

static void __unflatten_device_tree(struct boot_param_header *blob,---需要扫描的DTB
                 struct device_node **mynodes,---------global list指针
                 void * (*dt_alloc)(u64 size, u64 align))------内存分配函数
{
    unsigned long size;
    void *start, *mem;
    struct device_node **allnextp = mynodes;

    此处删除了health check代码,例如检查DTB header的magic,确认blob的确指向一个DTB。

    /* scan过程分成两轮,第一轮主要是确定device-tree structure的长度,保存在size变量中 */
    start = ((void *)blob) + be32_to_cpu(blob->off_dt_struct);
    size = (unsigned long)unflatten_dt_node(blob, 0, &start, NULL, NULL, 0);
    size = ALIGN(size, 4);

    /* 初始化的时候,并不是扫描到一个node或者property就分配相应的内存,实际上内核是一次性的分配了一大片内存,这些内存包括了所有的struct device_node、node name、struct property所需要的内存。*/
    mem = dt_alloc(size + 4, __alignof__(struct device_node));
    memset(mem, 0, size);

    *(__be32 *)(mem + size) = cpu_to_be32(0xdeadbeef);   //用来检验后面unflattening是否溢出

    /* 这是第二轮的scan,第一次scan是为了得到保存所有node和property所需要的内存size,第二次就是实打实的要构建device node tree了 */
    start = ((void *)blob) + be32_to_cpu(blob->off_dt_struct);
    unflatten_dt_node(blob, mem, &start, NULL, &allnextp, 0); 
   

    此处略去校验溢出和校验OF_DT_END。
}

具体的scan是在unflatten_dt_node函数中,如果已经清楚地了解DTB的结构,其实代码很简单,这里就不再细述了。

四、如何并入linux kernel的设备驱动模型

在linux kernel引入统一设备模型之后,bus、driver和device形成了设备模型中的铁三角。在驱动初始化的时候会将代表该driver的一个数据结构(一般是xxx_driver)挂入bus上的driver链表。device挂入链表分成两种情况,一种是即插即用类型的bus,在插入一个设备后,总线可以检测到这个行为并动态分配一个device数据结构(一般是xxx_device,例如usb_device),之后,将该数据结构挂入bus上的device链表。bus上挂满了driver和device,那么如何让device遇到“对”的那个driver呢?那么就要靠缘分了,也就是bus的match函数。

上面是一段导论,我们还是回到Device Tree。导致Device Tree的引入ARM体系结构的代码其中一个最重要的原因的太多的静态定义的表格。例如:一般代码中会定义一个static struct platform_device *xxx_devices的静态数组,在初始化的时候调用platform_add_devices。这些静态定义的platform_device往往又需要静态定义各种resource,这导致静态表格进一步增大。如果ARM linux中不再定义这些表格,那么一定需要一个转换的过程,也就是说,系统应该会根据Device tree来动态的增加系统中的platform_device。当然,这个过程并非只是发生在platform bus上(具体可以参考“Platform Device”的设备),也可能发生在其他的非即插即用的bus上,例如AMBA总线、PCI总线。一言以蔽之,如果要并入linux kernel的设备驱动模型,那么就需要根据device_node的树状结构(root是of_allnodes)将一个个的device node挂入到相应的总线device链表中。只要做到这一点,总线机制就会安排device和driver的约会。

当然,也不是所有的device node都会挂入bus上的设备链表,比如cpus node,memory node,choose node等。

1、cpus node的处理

这部分的处理可以参考setup_arch->arm_dt_init_cpu_maps中的代码,具体的代码如下:

void __init arm_dt_init_cpu_maps(void)
{
    scan device node global list,寻找full path是“/cpus”的那个device node。cpus这个device node只是一个容器,其中包括了各个cpu node的定义以及所有cpu node共享的property。
    cpus = of_find_node_by_path("/cpus");

 

    for_each_child_of_node(cpus, cpu) {           遍历cpus的所有的child node
        u32 hwid;

        if (of_node_cmp(cpu->type, "cpu"))        我们只关心那些device_type是cpu的node
            continue;


        if (of_property_read_u32(cpu, "reg", &hwid)) {    读取reg属性的值并赋值给hwid
            return;
        }

        reg的属性值的8 MSBs必须设置为0,这是ARM CPU binding定义的。
        if (hwid & ~MPIDR_HWID_BITMASK)  
            return;

        不允许重复的CPU id,那是一个灾难性的设定
        for (j = 0; j < cpuidx; j++)
            if (WARN(tmp_map[j] == hwid, "Duplicate /cpu reg "
                             "properties in the DT\n"))
                return;

数组tmp_map保存了系统中所有CPU的MPIDR值(CPU ID值),具体的index的编码规则是: tmp_map[0]保存了booting CPU的id值,其余的CPU的ID值保存在1~NR_CPUS的位置。
        if (hwid == mpidr) {
            i = 0;
            bootcpu_valid = true;
        } else {
            i = cpuidx++;
        }

        tmp_map[i] = hwid;
    }

根据DTB中的信息设定cpu logical map数组。

    for (i = 0; i < cpuidx; i++) {
        set_cpu_possible(i, true);
        cpu_logical_map(i) = tmp_map[i];
    }
}

要理解这部分的内容,需要理解ARM CUPs binding的概念,可以参考linux/Documentation/devicetree/bindings/arm目录下的CPU.txt文件的描述。

2、memory的处理

这部分的处理可以参考setup_arch->setup_machine_fdt->early_init_dt_scan->early_init_dt_scan_memory中的代码。具体如下:

int __init early_init_dt_scan_memory(unsigned long node, const char *uname,
                     int depth, void *data)
{
    char *type = of_get_flat_dt_prop(node, "device_type", NULL); 获取device_type属性值
    __be32 *reg, *endp;
    unsigned long l;

    在初始化的时候,我们会对每一个device node都要调用该call back函数,因此,我们要过滤掉那些和memory block定义无关的node。和memory block定义有的节点有两种,一种是node name是memory@形态的,另外一种是node中定义了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的起始地址和length的信息。有两种属性和该信息有关,一个是linux,usable-memory,不过最新的方式还是使用reg属性。

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;

    endp = reg + (l / sizeof(__be32));

reg属性的值是address,size数组,那么如何来取出一个个的address/size呢?由于memory node一定是root node的child,因此dt_root_addr_cells(root node的#address-cells属性值)和dt_root_size_cells(root node的#size-cells属性值)之和就是address,size数组的entry size。

    while ((endp - reg) >= (dt_root_addr_cells + dt_root_size_cells)) {
        u64 base, size;

        base = dt_mem_next_cell(dt_root_addr_cells, ®);
        size = dt_mem_next_cell(dt_root_size_cells, ®);

        early_init_dt_add_memory_arch(base, size);  将具体的memory block信息加入到内核中。
    }

    return 0;
}

 

3、interrupt controller的处理

初始化是通过start_kernel->init_IRQ->machine_desc->init_irq()实现的。我们用S3C2416为例来描述interrupt controller的处理过程。下面是machine描述符的定义。

DT_MACHINE_START(S3C2416_DT, "Samsung S3C2416 (Flattened Device Tree)")
……
    .init_irq    = irqchip_init,
……
MACHINE_END

在driver/irqchip/irq-s3c24xx.c文件中定义了两个interrupt controller,如下:

IRQCHIP_DECLARE(s3c2416_irq, "samsung,s3c2416-irq", s3c2416_init_intc_of);

IRQCHIP_DECLARE(s3c2410_irq, "samsung,s3c2410-irq", s3c2410_init_intc_of);

当然,系统中可以定义更多的irqchip,不过具体用哪一个是根据DTB中的interrupt controller node中的compatible属性确定的。在driver/irqchip/irqchip.c文件中定义了irqchip_init函数,如下:

void __init irqchip_init(void)
{
    of_irq_init(__irqchip_begin);
}

__irqchip_begin就是所有的irqchip的一个列表,of_irq_init函数是遍历Device Tree,找到匹配的irqchip。具体的代码如下:

void __init of_irq_init(const struct of_device_id *matches)
{
    struct device_node *np, *parent = NULL;
    struct intc_desc *desc, *temp_desc;
    struct list_head intc_desc_list, intc_parent_list;

    INIT_LIST_HEAD(&intc_desc_list);
    INIT_LIST_HEAD(&intc_parent_list);

    遍历所有的node,寻找定义了interrupt-controller属性的node,如果定义了interrupt-controller属性则说明该node就是一个中断控制器。

    for_each_matching_node(np, matches) {
        if (!of_find_property(np, "interrupt-controller", NULL) ||
                !of_device_is_available(np))
            continue;
       

分配内存并挂入链表,当然还有根据interrupt-parent建立controller之间的父子关系。对于interrupt controller,它也可能是一个树状的结构。
        desc = kzalloc(sizeof(*desc), GFP_KERNEL);
        if (WARN_ON(!desc))
            goto err;

        desc->dev = np;
        desc->interrupt_parent = of_irq_find_parent(np);
        if (desc->interrupt_parent == np)
            desc->interrupt_parent = NULL;
        list_add_tail(&desc->list, &intc_desc_list);
    }

    正因为interrupt controller被组织成树状的结构,因此初始化的顺序就需要控制,应该从根节点开始,依次递进到下一个level的interrupt controller。
    while (!list_empty(&intc_desc_list)) {  intc_desc_list链表中的节点会被一个个的处理,每处理完一个节点就会将该节点删除,当所有的节点被删除,整个处理过程也就是结束了。
        
        list_for_each_entry_safe(desc, temp_desc, &intc_desc_list, list) {
            const struct of_device_id *match;
            int ret;
            of_irq_init_cb_t irq_init_cb;

            最开始的时候parent变量是NULL,确保第一个被处理的是root interrupt controller。在处理完root node之后,parent变量被设定为root interrupt controller,因此,第二个循环中处理的是所有parent是root interrupt controller的child interrupt controller。也就是level 1(如果root是level 0的话)的节点。

            if (desc->interrupt_parent != parent)
                continue;

            list_del(&desc->list);      -----从链表中删除
            match = of_match_node(matches, desc->dev);-----匹配并初始化
            if (WARN(!match->data,----------match->data是初始化函数
                "of_irq_init: no init function for %s\n",
                match->compatible)) {
                kfree(desc);
                continue;
            }

            irq_init_cb = (of_irq_init_cb_t)match->data;
            ret = irq_init_cb(desc->dev, desc->interrupt_parent);-----执行初始化函数
            if (ret) {
                kfree(desc);
                continue;
            }

           处理完的节点放入intc_parent_list链表,后面会用到
            list_add_tail(&desc->list, &intc_parent_list);
        }

        对于level 0,只有一个root interrupt controller,对于level 1,可能有若干个interrupt controller,因此要遍历这些parent interrupt controller,以便处理下一个level的child node。
        desc = list_first_entry_or_null(&intc_parent_list,
                        typeof(*desc), list);
        if (!desc) {
            pr_err("of_irq_init: children remain, but no parents\n");
            break;
        }
        list_del(&desc->list);
        parent = desc->dev;
        kfree(desc);
    }

    list_for_each_entry_safe(desc, temp_desc, &intc_parent_list, list) {
        list_del(&desc->list);
        kfree(desc);
    }
err:
    list_for_each_entry_safe(desc, temp_desc, &intc_desc_list, list) {
        list_del(&desc->list);
        kfree(desc);
    }
}

只有该node中有interrupt-controller这个属性定义,那么linux kernel就会分配一个interrupt controller的描述符(struct intc_desc)并挂入队列。通过interrupt-parent属性,可以确定各个interrupt controller的层次关系。在scan了所有的Device Tree中的interrupt controller的定义之后,系统开始匹配过程。一旦匹配到了interrupt chip列表中的项次后,就会调用相应的初始化函数。如果CPU是S3C2416的话,匹配到的是irqchip的初始化函数是s3c2416_init_intc_of。

OK,我们已经通过compatible属性找到了适合的interrupt controller,那么如何解析reg属性呢?我们知道,对于s3c2416的interrupt controller而言,其#interrupt-cells的属性值是4,定义为。每个域的解释如下:

(1)ctrl_num表示使用哪一种类型的interrupt controller,其值的解释如下:

      - 0 ... main controller
      - 1 ... sub controller
      - 2 ... second main controller

(2)parent_irq。对于sub controller,parent_irq标识了其在main controller的bit position。

(3)ctrl_irq标识了在controller中的bit位置。

(4)type标识了该中断的trigger type,例如:上升沿触发还是电平触发。

为了更顺畅的描述后续的代码,我需要简单的介绍2416的中断控制器,其block diagram如下:

2416intc

53个Samsung2416的中断源被分成两种类型,一种是需要sub寄存器进行控制的,例如DMA,系统中的8个DMA中断是通过两级识别的,先在SRCPND寄存器中得到是DMA中断的信息,具体是哪一个channel的DMA中断需要继续查询SUBSRC寄存器。那些不需要sub寄存器进行控制的,例如timer,5个timer的中断可以直接从SRCPND中得到。
中断MASK寄存器可以控制产生的中断是否要报告给CPU,当一个中断被mask的时候,虽然SRCPND寄存器中,硬件会set该bit,但是不会影响到INTPND寄存器,从而不会向CPU报告该中断。对于SUBMASK寄存器,如果该bit被set,也就是该sub中断被mask了,那么即便产生了对应的sub中断,也不会修改SRCPND寄存器的内容,只是修改SUBSRCPND中寄存器的内容。

不过随着硬件的演化,更多的HW block加入到SOC中,这使得中断源不够用了,因此中断寄存器又被分成两个group,一个是group 1(开始地址是0X4A000000,也就是main controller了),另外一个是group2(开始地址是0X4A000040,叫做second main controller)。group 1中的sub寄存器的起始地址是0X4A000018(也就是sub controller)。

了解了上面的内容后,下面的定义就比较好理解了:

static struct s3c24xx_irq_of_ctrl s3c2416_ctrl[] = {
    {
        .name = "intc", -----------main controller
        .offset = 0,
    }, {
        .name = "subintc", ---------sub controller
        .offset = 0x18,
        .parent = &s3c_intc[0],
    }, {
        .name = "intc2", ----------second main controller
        .offset = 0x40,
    }
};

对于s3c2416而言,irqchip的初始化函数是s3c2416_init_intc_of,s3c2416_ctrl作为参数传递给了s3c_init_intc_of,大部分的处理都是在s3c_init_intc_of函数中完成的,由于这个函数和中断子系统非常相关,这里就不详述了,后续会有一份专门的文档描述之。

4、GPIO controller的处理

暂不描述,后续会有一份专门的文档描述GPIO sub system。

5、machine初始化

machine初始化的代码可以沿着start_kernel->rest_init->kernel_init->kernel_init_freeable->do_basic_setup->do_initcalls路径寻找。在do_initcalls函数中,kernel会依次执行各个initcall函数,在这个过程中,会调用customize_machine,具体如下:

static int __init customize_machine(void)
{

    if (machine_desc->init_machine)
        machine_desc->init_machine();
    else
        of_platform_populate(NULL, of_default_bus_match_table, NULL, NULL);

    return 0;
}
arch_initcall(customize_machine);

在这个函数中,一般会调用machine描述符中的init_machine callback函数来把各种Device Tree中定义的platform device设备节点加入到系统(即platform bus的所有的子节点,对于device tree中其他的设备节点,需要在各自bus controller初始化的时候自行处理)。如果machine描述符中没有定义init_machine函数,那么直接调用of_platform_populate把所有的platform device加入到kernel中。对于s3c2416,其machine描述符中的init_machine callback函数就是s3c2416_dt_machine_init,代码如下:

static void __init s3c2416_dt_machine_init(void)
{
    of_platform_populate(NULL, --------传入NULL参数表示从root node开始scan

of_default_bus_match_table, s3c2416_auxdata_lookup, NULL);

    s3c_pm_init(); --------power management相关的初始化
}

由此可见,最终生成platform device的代码来自of_platform_populate函数。该函数的逻辑比较简单,遍历device node global list中所有的node,并调用of_platform_bus_create处理,of_platform_bus_create函数代码如下:

static int of_platform_bus_create(struct device_node *bus,-------------要创建的那个device node
                  const struct of_device_id *matches,-------要匹配的list
                  const struct of_dev_auxdata *lookup,------附属数据
                  struct device *parent, bool strict)---------------parent指向父节点。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;

删除确保device node有compatible属性的代码。

    auxdata = of_dev_lookup(lookup, bus);  在传入的lookup table寻找和该device node匹配的附加数据
    if (auxdata) {
        bus_id = auxdata->name;-----------------如果找到,那么就用附加数据中的静态定义的内容
        platform_data = auxdata->platform_data;
    }

ARM公司提供了CPU core,除此之外,它设计了AMBA的总线来连接SOC内的各个block。符合这个总线标准的SOC上的外设叫做ARM Primecell Peripherals。如果一个device node的compatible属性值是arm,primecell的话,可以调用of_amba_device_create来向amba总线上增加一个amba device。

    if (of_device_is_compatible(bus, "arm,primecell")) {
        of_amba_device_create(bus, bus_id, platform_data, parent);
        return 0;
    }

    如果不是ARM Primecell Peripherals,那么我们就需要向platform bus上增加一个platform device了

    dev = of_platform_device_create_pdata(bus, bus_id, platform_data, parent);
    if (!dev || !of_match_node(matches, bus))
        return 0;

    一个device node可能是一个桥设备,因此要重复调用of_platform_bus_create来把所有的device node处理掉。

    for_each_child_of_node(bus, child) {
        pr_debug("   create child: %s\n", child->full_name);
        rc = of_platform_bus_create(child, matches, lookup, &dev->dev, strict);
        if (rc) {
            of_node_put(child);
            break;
        }
    }
    return rc;
}

具体增加platform device的代码在of_platform_device_create_pdata中,代码如下:

static struct platform_device *of_platform_device_create_pdata(
                    struct device_node *np,
                    const char *bus_id,
                    void *platform_data,
                    struct device *parent)
{
    struct platform_device *dev;

    if (!of_device_is_available(np))---------check status属性,确保是enable或者OK的。
        return NULL;

    of_device_alloc除了分配struct platform_device的内存,还分配了该platform device需要的resource的内存(参考struct platform_device 中的resource成员)。当然,这就需要解析该device node的interrupt资源以及memory address资源。

    dev = of_device_alloc(np, bus_id, parent);
    if (!dev)
        return NULL;

设定platform_device 中的其他成员
    dev->dev.coherent_dma_mask = DMA_BIT_MASK(32);
    if (!dev->dev.dma_mask)
        dev->dev.dma_mask = &dev->dev.coherent_dma_mask;
    dev->dev.bus = &platform_bus_type;
    dev->dev.platform_data = platform_data;

    if (of_device_add(dev) != 0) {------------------把这个platform device加入统一设备模型系统中
        platform_device_put(dev);
        return NULL;
    }

    return dev;
}

原创文章,转发请注明出处。蜗窝科技www.wowotech.net。

标签: 设备树

评论:

lucky
2016-10-17 11:02
wowo你好:
请教一个问题:
/* 扫描DTB中的memory node,并把相关信息保存在meminfo中,全局变量meminfo保存了系统内存相关的信息。*/
of_scan_flat_dt(early_init_dt_scan_memory, NULL);
early_init_dt_scan_memory
    early_init_dt_add_memory_arch
    arm_add_memory
            初始化meminfo.bank[meminfo.nr_banks];
                        
这个是扫描DTS里面定义的系统所有的物理内存信息吗?
wowo
2016-10-17 13:52
@lucky:总的来说,是的。不过更细节一些内容,Linuxer同学正在写内存初始化有关的内容,随后应该有一篇基于x project的实战性的文档,应该可以回答你的问题。
linuxer
2016-10-18 09:10
@lucky:可以参考http://www.wowotech.net/memory_management/mm-init-1.html这份文档,这里讲了内核如何形成物理内存的布局信息。
BTW,现在应该已经不使用meminfo来保存内存的信息,在初始化阶段,都使用memblock来保存物理内存信息。
lucky
2016-10-18 09:18
@linuxer:多谢linuxer,接下来细细品味,^_^
麦兜要努力
2016-10-14 09:52
楼主您好:
   刚开始接触Linux driver,目前在学习dw dma的部分。无意中发现了楼主的文章,看了关于device tree和platform相关的部分,解决了一些platform_device和platorm_driver的疑惑,不过还是有些问题卡住了,想请教一下:
   在引入device tree之前要静态定义platform_device struct,注册device,然后定义platform_driver struct,注册driver,进行match,执行probe。引入device tree之后,最终通过of_platform_populate(NULL, of_default_bus_match_table, NULL, NULL)系统生成platform_device并注册到系统中。就我目前看的dw dma部分,一开始subsys_initcall(dw_init)->dw_init->platform_driver_register(&dw_driver),看到:
static struct platform_driver dw_driver = {
    .probe        = dw_probe,
    .remove        = dw_remove,
    .shutdown       = dw_shutdown,
    .driver = {
        .name    = DRV_NAME,
        .pm    = &dw_dev_pm_ops,
        .of_match_table = of_match_ptr(dw_dma_of_id_table),
        .acpi_match_table = ACPI_PTR(dw_dma_acpi_id_table),
    },
};

#define DRV_NAME    "dw_dmac"
#ifdef CONFIG_OF
static const struct of_device_id dw_dma_of_id_table[] = {
    { .compatible = "snps,dma-spear1340" },
    {}
};
MODULE_DEVICE_TABLE(of, dw_dma_of_id_table);
#endif
根据这个snps,dma-spear1340,找到spear13xx.dtsi文件。
可以在dts里面看到:
ahb {
        #address-cells = <1>;
        #size-cells = <1>;
        compatible = "simple-bus";
        ranges = <0x50000000 0x50000000 0x10000000
              0x80000000 0x80000000 0x20000000
              0xb0000000 0xb0000000 0x22000000
              0xd8000000 0xd8000000 0x01000000
              0xe0000000 0xe0000000 0x10000000>;

        sdhci@b3000000 {
            compatible = "st,sdhci-spear";
            reg = <0xb3000000 0x100>;
            interrupts = <0 28 0x4>;
            status = "disabled";
        };

        cf@b2800000 {
            compatible = "arasan,cf-spear1340";
            reg = <0xb2800000 0x1000>;
            interrupts = <0 29 0x4>;
            status = "disabled";
            dmas = <&dwdma0 0 0 0 0>;
            dma-names = "data";
        };

        dwdma0: dma@ea800000 {
            compatible = "snps,dma-spear1340";
            reg = <0xea800000 0x1000>;
            interrupts = <0 19 0x4>;
            status = "disabled";

            dma-channels = <8>;
            #dma-cells = <3>;
            dma-requests = <32>;
            chan_allocation_order = <1>;
            chan_priority = <1>;
            block_size = <0xfff>;
            dma-masters = <2>;
            data_width = <3 3 0 0>;
        };

        dma@eb000000 {
            compatible = "snps,dma-spear1340";
            reg = <0xeb000000 0x1000>;
            interrupts = <0 59 0x4>;
            status = "disabled";

            dma-requests = <32>;
            dma-channels = <8>;
            dma-masters = <2>;
            #dma-cells = <3>;
            chan_allocation_order = <1>;
            chan_priority = <1>;
            block_size = <0xfff>;
            data_width = <3 3 0 0>;
        };
.....

疑问:
在spear1340c.里面看到:
static void __init spear1340_dt_init(void)
{
    of_platform_populate(NULL, of_default_bus_match_table, NULL, NULL);
    platform_device_register_simple("spear-cpufreq", -1, NULL, 0);
}

static const char * const spear1340_dt_board_compat[] = {
    "st,spear1340",
    "st,spear1340-evb",
    NULL,
};

DT_MACHINE_START(SPEAR1340_DT, "ST SPEAr1340 SoC with Flattened Device Tree")
    .smp        =    smp_ops(spear13xx_smp_ops),
    .map_io        =    spear13xx_map_io,
    .init_time    =    spear13xx_timer_init,
    .init_machine    =    spear1340_dt_init,
    .restart    =    spear_restart,
    .dt_compat    =    spear1340_dt_board_compat,
MACHINE_END
   那这边有调用of_platform_populate,那dma node是在platform bus下面的,应该是有被创建了platform_device了吧?但是我根据 .name = "dw_dmac",我又找到了关于dw_dmac0_device的静态定义:

static struct dw_dma_platform_data dw_dmac0_data = {
    .nr_channels    = 3,
    .block_size    = 4095U,
    .nr_masters    = 2,
    .data_width    = { 2, 2, 0, 0 },
};

static struct resource dw_dmac0_resource[] = {
    PBMEM(0xff200000),
    IRQ(2),
};
DEFINE_DEV_DATA(dw_dmac, 0);
DEV_CLK(hclk, dw_dmac0, hsb, 10);


#define DEFINE_DEV_DATA(_name, _id)                \
static u64 _name##_id##_dma_mask = DMA_BIT_MASK(32);        \
static struct platform_device _name##_id##_device = {        \
    .name        = #_name,                \
    .id        = _id,                    \
    .dev        = {                    \
        .dma_mask = &_name##_id##_dma_mask,        \
        .platform_data    = &_name##_id##_data,        \
        .coherent_dma_mask = DMA_BIT_MASK(32),        \
    },                            \
    .resource    = _name##_id##_resource,        \
    .num_resources    = ARRAY_SIZE(_name##_id##_resource),    \
}
以及dw_dmac0_device注册:
static int __init system_device_init(void)
{
    platform_device_register(&at32_pm0_device);
    platform_device_register(&at32_intc0_device);
    platform_device_register(&at32ap700x_rtc0_device);
    platform_device_register(&at32_wdt0_device);
    platform_device_register(&at32_eic0_device);
    platform_device_register(&smc0_device);
    platform_device_register(&pdc_device);
    platform_device_register(&dw_dmac0_device);

    platform_device_register(&at32_tcb0_device);
    platform_device_register(&at32_tcb1_device);

    platform_device_register(&pio0_device);
    platform_device_register(&pio1_device);
    platform_device_register(&pio2_device);
    platform_device_register(&pio3_device);
    platform_device_register(&pio4_device);

    return 0;
}
那究竟这个platform_device,究竟是在of_platform_populate里面去创建和注册的,通过snps,dma-spear1340去匹配?还是说在platform_device_register(&dw_dmac0_device)这边静态定义并被注册,通过name dw_dmac去匹配?为什么会既存在于dts里面,又静态去定义呢,会不会冲突呢?而且有两个同样compatible string的dma,只是address不同,那应该是根据设备号id去区分,在静态定义的时候可以设置为0和1,但是在of_platform_populate里面的of_device_add,初始值是-1,那有两个同样的设备,这个id它会做什么样的变化呢?

希望能得到楼主的解答,感谢!
wowo
2016-10-14 11:43
@麦兜要努力:我觉得,代码是为了兼容OF enable(DTS场景)和disable(传统的mach场景)两种使用方式,不会共存。
你可以按照这个思路,查找一下代码,找到支持或者驳斥这个观点的证据(重点关注CONFIG_OF等和device tree有关的配置项)。
jey
2016-07-28 16:28
Hi,wowo:
现在项目中遇到一个问题,下面是相关的log:
_kernel_ring_buffer
[    0.000000] Initializing cgroup subsys cpu
[    0.000000] Initializing cgroup subsys cpuacct
[    0.000000] Linux version 3.10.73-ge5fb304-dirty (caojianye@compiler191) (gcc
[    0.000000] CPU: AArch64 Processor [410fd034] revision 4
[    0.000000]
[    0.000000] Error: invalid device tree blob at physical address 0x00000000122
[    0.000000] The dtb must be 8-byte aligned and passed in the first 512MB of m
[    0.000000]
[    0.000000] Please check your bootloader.
现象是在内核中增加两行打印后内核就不能启动,从log分析是dtb被踩,lk中能正常解析dtb,说明是在内核出事话的时候踩到了dtb,请教一下:
1.内核初始化的哪个阶段可能踩到dtb呢?
2.我的想法是在lk中把dtb的加载地址往后面挪一些,但是如果内核在进一步增大的话还是会有问题,并不能彻底解决,能不能通过修改内核代码彻底解决这种问题呢?
wowo
2016-07-28 18:28
@jey:bootloader会通过命令行参数告诉kernel,当前可用的memory空间是多少。只要这个空间和dtb所占的空间不重叠,就不会有问题。
大头元
2016-07-04 23:22
楼主你好:
很多驱动程序的probe函数中都会先去调用dev_get_platdata(&pdev->dev)获取platform_data,我想知道在设备树的条件下,platform_data是怎么来的啊?它跟of_device_id中的.data有关吗,但我发现.data并没赋值(如果有关的话)。
wowo
2016-07-05 08:58
@大头元:没有关系。
platform_data是driver调用dev_set_platdata自行设置的。
你可以去kenerl里面看看例子,看看别人怎么用的。
大头元
2016-07-05 10:22
@wowo:多谢楼主,我用的是3.14内核,没找到dev_set_platdata定义。而且我看的栗子直接就在probe函数中使用了dev_get_platdata,probe是匹配后第一个调用的函数,估计是在某个地方赋值好了吧。platform_data是提前在内核中写好的还是用设备树转化而来的啊?
wowo
2016-07-05 13:05
@大头元:抱歉,我看成了dev_set_drvdata可。
这个指针,要么直接赋值:
pdev->dev.platform_data = pdata;
要么调用platform_device_add_data,和device tree没有关系。
大头元
2016-07-06 08:53
@wowo:ok,多谢答疑解惑~
borman
2016-06-24 16:45
楼主,有一事不明,我在of_platform_bus_create中并没有找到与创建platform device时设备的操作方法的初始化,还是这里只是对设备节点的初始化,并没有设备的具体操作?
wowo
2016-06-24 17:02
@borman:不太明白你的意思,这个接口负责根据DTS,创建对应的device,并将其添加到kernel中:
of_platform_bus_create
    of_platform_device_create_pdata
        of_device_add
            device_add
device被添加进kernel之后,就按照设备模型,进行正常的probe操作。
borman
2016-06-24 17:52
@wowo:搞混了,那么向系统中添加了这些dts中设备,它们的驱动是何时注册呢,我是想说这个的
linuxer
2016-06-24 19:06
@borman:在各自驱动模块的初始化函数中注册驱动啊
simonzhang
2016-06-22 19:58
感谢郭同学获益良多
linuxer
2016-06-23 09:08
@simonzhang:太客气了,Simon同学,有空也多多贡献,多多分享啊,要不你脑袋里面的知识要发霉了,哈哈
js_wawayu
2016-06-21 22:39
请教一下boot cpu是不是可以有多个?
wowo
2016-06-22 09:11
@js_wawayu:应该不能吧?
snake
2016-05-15 11:58
请教一个问题, 文中说:
"我们知道,对于s3c2416的interrupt controller而言,其#interrupt-cells的属性值是4,定义为。每个域的解释如下"
请问每个域的解释如何得知的? 是从文档还是代码里面? 能说的稍微详细些吗,谢谢!
wowo
2016-05-16 09:19
@snake:DTS的东西,一般都放在Documentation/devicetree/bindings/目录中了,例如:
Documentation/devicetree/bindings/interrupt-controller/samsung,s3c24xx-irq.txt
snake
2016-05-16 23:16
@wowo:@wowo: 谢谢, s3c24xx的明白了, 那比如mtk,qualcom平台的#interrupt-cells的各个属性含义怎么看呢?  比如mtk的gic, #interrupt-cells = <3>; 这3个属性含义如何看呢?
Documentation/devicetree/bindings 下面好像没有说明吧, 麻烦请指教下, 谢谢!
wowo
2016-05-17 09:04
@snake:如果没有说明,就去看一下source code吧。
在linux kernel的世界里,source code是万能魔法棒,什么问题都能回答:-)
snake
2016-05-18 22:54
@wowo:好的,谢谢 ^_^
imjacob
2016-03-23 21:10
你好,请教下, 我的板子如果是sd卡或者emmc这种,那怎么用device tree来分区呢?我网上查了很久,只有查到对nand,nor这些raw的存储设备分区的方法(就是用partition)。但sd之类找不到,谢谢!
wowo
2016-03-24 09:28
@imjacob:在kernel driver中,只有MTD driver才会关注分区的概念,在新时代(eMMC)没有关注的必要。NAND和eMMC的简单区别如下:
        奇葩文件系统(yaffs等)----------->MTD---->NAND/NOR controller---->NAND/NOR
        普通文件系统(FAT、EXT4等)---->MMC---->eMMC controller---------->NAND
分区(partition)是一个软件概念,理应由文件系统负责抽象和处理,那么问题来了,为什么MTD driver(注意,是driver哦)需要关注分区的概念?这是现存Linux系统中MTD、NAND等有关实现的一个不友好的地方。
然后,eMMC出现了,NAND有关坏块/均衡有关的实现,统一做到eMMC片子里面了,Linux kernel以及之上的文件系统,都happy了,就没有必要在kernel中关注分区这个事情了。
imjacob
2016-03-29 21:37
@wowo:恩。很感谢大家的回复,收益良多。我又理解了一下,mtd的分区和   sd(及emmc,磁盘)的分区是不一样的,前者 特定于linux的实现,用了什么mtd_partition等结构来维护,而后者是有标准的,什么MBR上面的4个主分区这些概念。是这样吗?
郭健
2016-03-24 09:57
@imjacob:如果你的板子是SD或者eMMC的话,目前条件下,内核是不支持通过device tree来传递分区信息的,为什么呢?因为协议已经支持分区了,如果driver在面对一个SD或者eMMC设备的时候,能够通过命令从硬件获取分区信息,基本我认为也就不需要参数传递分区信息了。
郭健
2016-03-24 10:37
@郭健:你也可以考虑把eMMC当成一个普通的磁盘,这样,在磁盘上建立分区表(例如MBR或者GPT什么的),也就不需要device tree来传递参数了。
imjacob
2016-03-25 14:06
@郭健:@郭健 @wowo,
恩。很感谢大家的回复,收益良多。我又理解了一下,mtd的分区和   sd(及emmc,磁盘)的分区是不一样的,前者 特定于linux的实现,用了什么mtd_partition等结构来维护,而后者是有标准的,什么MBR上面的4个主分区这些概念。是这样吗?
wowo
2016-03-30 13:47
@imjacob:差不多是这个意思,不过“什么MBR上面的4个主分区这些概念”不太准确,这4个主分区,是物理上的分区概念(出厂就确定了),而我们平时所示的分区,大多是软件层面(FS的角度)的逻辑概念。
imjacob
2016-03-30 20:11
@wowo:恩。谢谢! 我查了一下,为什么现在的硬盘,sd卡,u盘只能分为4个主分区。这个可能和历史有关。历史上,当然,硬盘的容量不大。所以保留了第一个扇区为引导扇区,就是MBR。作为引导,其实还不止引导,还放入了一些其他信息,例如分区的信息。当初留给的是64个字节。而每个主分区呢,要16个字节,所以只能4个主分区了。  这个虽然现在硬盘什么大的吓死人了,但是还是保留了下来。
再次感谢!
wowo
2016-03-30 22:14
@imjacob:确实如此,也多谢你的分享~~~
摩斯电码
2016-03-10 10:19
你好,我发现在函数of_platform_bus_create中并不一定对所有的device node都进行遍历,比如像下面一个设备树:

    i2c-gpio-0 {
        compatible = "i2c-gpio";
        gpios = <0x3e 0x2 0x0 0x3e 0x3 0x0>;
        i2c-gpio,delay-us = <0x2>;
        #address-cells = <0x1>;
        #size-cells = <0x0>;
        status = "okay";

        mma7660@4c {
            compatible = "freescale,mma7660";
            reg = <0x4c>;
            interrupt-parent = <0x3f>;
            interrupts = <0x1 0x2>;
            poll_interval = <0x64>;
            input_fuzz = <0x4>;
            input_flat = <0x4>;
            status = "okay";
        };
    };

其中i2c-gpio-0这个device node确实是在of_platform_bus_create的时候被注册为了platform_device,但是mma7660@4c这个设备节点并没有被of_platform_bus_create遍历到,它是在注册i2c_adapter的时候,会遍历这个adapter对应的device node的child device node(目的是注册i2c_client),此时才会处理mma7660这个device node。

不知道你有没有知道注意到在一般在调用of_platform_bus_create的时候会传递一个参数:

of_platform_populate(NULL, of_default_bus_match_table, NULL, NULL)

其中of_default_bus_match_table的定义如下:

const struct of_device_id of_default_bus_match_table[] = {
    { .compatible = "simple-bus", },
    { .compatible = "simple-mfd", },
#ifdef config_arm_amba
    { .compatible = "arm,amba-bus", },
#endif /* config_arm_amba */
    {} /* empty terminated list */
};

意思是如果某个device node的compatible属性跟上面数组中定义的match,那么这个device node的child device node才会被注册为platform_device。

在函数of_platform_bus_create中有下面几句:

    dev = of_platform_device_create_pdata(bus, bus_id, platform_data, parent);
    if (!dev || !of_match_node(matches, bus))
        return 0;

    for_each_child_of_node(bus, child) {
        pr_debug("   create child: %s\n", child->full_name);
        rc = of_platform_bus_create(child, matches, lookup, &dev->dev, strict);
        if (rc) {
            of_node_put(child);
            break;
        }
    }


其中如果of_platform_device_create_pdata成功执行,然后会调动of_match_node,如果match的话,在会再次递归这个device node的下的child device node,如果不match,就直接返回了,不再遍历其下的child device node了。

2016-03-10 15:58
@摩斯电码:私以为再去遍历i2c下的child device node没什么意义。因为mma7660着个设备属于i2c bus上的设备,没必要再放到platform_bus上了。
郭健
2016-03-10 18:02
@摩斯电码:多谢您的提醒,的确是如此。对于支持热拔插的总线(例如USB),其实不需要体现在dts中,其对应的device数据结构(例如struct usb_device)会在设备插入的时候自行分配。而那些不能支持热拔插的总线,挂入其上的device需要在dts中体现并在总线设备初始化的时候进行实例化(就是分配一个xxx_device的数据结构并注册到设备模型)。在所有不支持hotplug的总线中,platform是一个特殊的总线,没有实际的bus设备对应,因此在machine init中调用of_platform_populate处理。

发表评论:

Copyright @ 2013-2015 蜗窝科技 All rights reserved. Powered by emlog