目录

  • 前言
  • 1 从u-boot传参到__atags_pointer
  • 2 内核对设备树中平台信息的处理
    • 2.1 machine_desc
    • 2.2 源码分析
      • 2.2.1 setup_arch
      • 2.2.2 setup_machine_fdt
      • 2.2.3 of_flat_dt_match_machine
  • 3 内核对设备树中运行时配置信息的处理
    • 3.1 of_scan_flat_dt
    • 3.2 解析/chosen节点
    • 3.3 解析根节点的{size,address}-cells属性
    • 3.4 解析/memory节点
    • 3.5 小结
  • 4 内核对设备树中设备信息的处理
    • 4.1 内核对DTB所在内存的处理
    • 4.2 struct device_node和struct property
    • 4.3 从DTB到struct device_node示例
    • 4.4 unflatten_device_tree分析
    • 4.5 将device_node转换成platform_device
      • 4.5.1 哪些节点需要转换成platform_device
      • 4.5.2 在哪里做的转换工作
      • 4.5.3 浅析转换的过程
      • 4.5.4 如何处理平台设备对应的设备节点的子节点
    • 4.6 小结
  • 5 内核中设备树相关头文件的总结
    • 5.1 声明处理DTB文件的函数的头文件
    • 5.2 处理device_node的函数的头文件
    • 5.3 声明处理platform_device的函数的头文件
  • 6 设备树在文件系统中的表示
    • 6.1 /sys与设备树
    • 6.2 /proc与设备树
  • 参考文献

前言

本文关注的主要是内核如何处理DTB文件中记录的设备信息,会分析内核解析DTB文件的主体流程,不会关注所有细节,同时,也不会关注对特定设备信息的处理,比如对有关中断的设备信息的处理,相关内容在做相应模块的笔记时再细究(学习linux的中断管理时会分析内核对DTB中有关中断的部分的处理)。

1 从u-boot传参到__atags_pointer

前文已经说过,DTB文件由u-boot传递给内核,u-boot在跳转到内核时,会把一些关键的信息通过参数(实际使用通用寄存器r0、r1、r2)传递给内核:

  • r0:通常设置为0;
  • r1:传递的是ATAGS时,通常设置为板子的machine id;传递的是设备树时,该参数无用;
  • r2:通常设置为ATAGS或DTB在内存中的起始地址。

可见,当u-boot向内核传递DTB文件时,内核真正要关注的只有r2寄存器中存放的DTB文件的内存起始地址。内核会在启动的汇编阶段把这个地址保存到全局变量__atags_pointer,大体的过程如下:

/*1、设置r13,跳转执行__enable_mmu
*/
ENTRY(stext)......ldr r13, =__mmap_switched....../* 跳转执行__enable_mmu */b __enable_mmu
ENDPROC(stext)/*2、使能MMU,跳转执行__mmap_switched
*/
__enable_mmu:/* 在r0设置好打算写往MMU控制寄存器的值 */....../* 跳转执行 */b    __turn_mmu_onENTRY(__turn_mmu_on)mov    r0, r0instr_syncmcr p15, 0, r0, c1, c0, 0       @ write control regmrc p15, 0, r3, c0, c0, 0       @ read id reginstr_syncmov r3, r3/* r13 = __mmap_switched */mov   r3, r13ret  r3
__turn_mmu_on_end:
ENDPROC(__turn_mmu_on)/*3、在__mmap_switched中,将r2(保存DTB的地址)的值存到__atags_pointer,然后跳转start_kernel
*/
__mmap_switched:mov r7, r1mov   r8, r2    /* 把DTB的地址转存到r8 */mov r10, r0adr  r4, __mmap_switched_datamov fp, #0ARM(  ldmia   r4!, {r0, r1, sp} )sub  r2, r1, r0mov   r1, #0bl    memset              @ clear .bssldmia  r4, {r0, r1, r2, r3}str r9, [r0]            @ Save processor IDstr r7, [r1]            @ Save machine type/* 把DTB的地址写到全局变量__atags_pointer */str   r8, [r2]            @ Save atags pointercmp    r3, #0strne r10, [r3]           @ Save control register valuesmov  lr, #0b start_kernel
ENDPROC(__mmap_switched)__mmap_switched_data:.long  __bss_start         @ r0.long  __bss_stop          @ r1.long  init_thread_union + THREAD_START_SP @ sp.long processor_id            @ r0.long  __machine_arch_type     @ r1.long  __atags_pointer         @ r2......

kernel启动的汇编阶段结束后,会跳转执行start_kernel,此时__atags_pointer指向内存中的DTB文件。kernel对DTB的解析在start_kernel==>setup_arch函数中进行。kernel将DTB中的信息分为三类:

  1. 平台识别信息,通常指的是根节点的compatiblemodel属性记录的信息;
  2. 运行时配置信息,通常指的是/chosen节点和/memory节点记录的信息;
  3. 设备信息,指各个设备节点。

下文将分别介绍内核对这三种信息的处理。

2 内核对设备树中平台信息的处理

2.1 machine_desc

一个kernel镜像通常会支持很多板子,针对每种板子,kernel都会为其定义一个struct machine_desc的结构,其中就记录各个板子的硬件信息,比如板子的ID号、名字、支持的中断数量、初始化函数等。这样,在kernel启动时,可以根据u-boot传递的参数/DTB文件选则合适的machine_desc,从而正确的初始化当前硬件。

kernel会将一系列machine_desc集中存放在.init.arch.info节中,形成如同数组一样的内存分布:

 .init.arch.info : {__arch_info_begin = .;*(.arch.info.init)__arch_info_end = .;}

并以符号__arch_info_begin__arch_info_end记录该节的起始和结尾,如此一来,就可以向访问数组元素那样访问每个machine_desc。

在选则machine_desc时,kernel首先会获取DTB的根节点的compatible属性,将其中的一个或多个字符串与machine_desc的dt_compat成员记录的一个或多个字符串进行比较,当匹配时,返回相应的machine_desc。值得一提的是,compatible属性值中,位置靠前的字符串会优先比较,换句话说,位置越靠前说明该字符串指示的machine_desc越适合当前单板。

2.2 源码分析

有了2.1节的铺垫,接下来就可以进行源码分析了,从setup_arch开始:

2.2.1 setup_arch

void __init setup_arch(char **cmdline_p)
{const struct machine_desc *mdesc;/* 初始化一些处理器相关的全局变量 */setup_processor();/* 优先按照设备树获取machine_desc */mdesc = setup_machine_fdt(__atags_pointer);/* 如果u-boot传递的不是DTB,则按照ATAGS获取machine_desc */if (!mdesc)mdesc = setup_machine_tags(__atags_pointer, __machine_arch_type);....../* 记录获取到的machine_desc及其名字 */machine_desc = mdesc;machine_name = mdesc->name;dump_stack_set_arch_desc("%s", mdesc->name);....../* 将boot_command_line的内容拷贝到cmd_line */strlcpy(cmd_line, boot_command_line, COMMAND_LINE_SIZE);/* 输出指向启动参数的指针 */*cmdline_p = cmd_line;....../* 根据DTB创建device_node树 */unflatten_device_tree();......
#ifdef CONFIG_GENERIC_IRQ_MULTI_HANDLER/* 设置handle_arch_irq */handle_arch_irq = mdesc->handle_irq;
#endif....../* 调用machine_desc中注册的初始化函数 */if (mdesc->init_early)mdesc->init_early();
}

2.2.2 setup_machine_fdt

由上文可知,setup_arch函数调用setup_machine_fdt解析设备树中的相关信息,并返回合适的machine_desc

const struct machine_desc * __init setup_machine_fdt(unsigned int dt_phys)
{const struct machine_desc *mdesc, *mdesc_best = NULL;/* 验证DTB文件是否存在:地址不为NULL && 文件头部magic正确 *//* initial_boot_params = dt_phys */if (!dt_phys || !early_init_dt_verify(phys_to_virt(dt_phys)))return NULL;/* 获取compatible属性并匹配合适的machine_desc */mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);if (!mdesc) {/* 打印一些信息 */....../* 把当前kernel支持的单板的名字和单板ID打印出来 *//* 该函数不会返回(内部有死循环) */dump_machine_table();}/* 当DTB文件提供的数据有问题,这里会做一些修补工作 */if (mdesc->dt_fixup)mdesc->dt_fixup();/* 获取运行时配置信息,再第3节中细说 */early_init_dt_scan_nodes();/* 记录machine ID */__machine_arch_type = mdesc->nr;return mdesc;
}

2.2.3 of_flat_dt_match_machine

of_flat_dt_match_machine函数是匹配合适的machine_desc的关键:

/*传入的第一个参数为NULL传入的第二个参数为arch_get_next_macharch_get_next_mach的原理非常简单:初始化一个静态局部变量为__arch_info_begin,每次被调用时该变量(指针)+1并返回,如果超出了__arch_info_end,则返回NULL
*/
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 = 0 */dt_root = of_get_flat_dt_root();/* 遍历所有machine_desc,将machine_desc的dt_compat保存到compatcompat指向一系列字符串(一个machine_desc也可能支持多个单板)*/while ((data = get_next_compat(&compat))) {/*DTB根节点的compatible属性值是一系列字符串,假设为"aaa", "bbb", "ccc"machine_desc的dt_compat(指针的指针)也指向一系列字符串,假设为"xxx", "ccc"第一轮比较(score = 0):1、score++, compatible的"aaa"<==>dt_compat的"xxx"2、score++, compatible的"bbb"<==>dt_compat的"xxx"3、score++, compatible的"ccc"<==>dt_compat的"xxx"第二轮比较(score = 0):1、score++, compatible的"aaa"<==>dt_compat的"ccc"2、score++, compatible的"bbb"<==>dt_compat的"ccc"3、score++, compatible的"ccc"<==>dt_compat的"ccc",此时匹配上,返回score(值为3)*/score = of_flat_dt_match(dt_root, compat);/* 记录得分最低(最匹配)的machine_desc */if (score > 0 && score < best_score) {best_data = data;best_score = score;}}/* 没有匹配到合适的machine_desc就返回NULL */if (!best_data) {/* 打印根节点的compatible属性值 */......return NULL;}/* 打印根节点的model属性值,若不存在则打印compatible属性值 */pr_info("Machine model: %s\n", of_flat_dt_get_machine_name());return best_data;
}

至此,如何根据DTB的根节点的compatible属性匹配machine_desc就介绍完了。

3 内核对设备树中运行时配置信息的处理

kernel使用setup_arch ==> setup_machine_fdt ==> early_init_dt_scan_nodes来处理DTB中的运行时配置信息:

void __init early_init_dt_scan_nodes(void)
{int rc = 0;/* 获取/chosen节点的信息 */rc = of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);if (!rc)pr_warn("No chosen node found, continuing without\n");/* 获取根节点的{size,address}-cells属性值,之后才方便解析根节点的子节点的reg属性 */of_scan_flat_dt(early_init_dt_scan_root, NULL);/* 解析/memory节点,设置内存信息 */of_scan_flat_dt(early_init_dt_scan_memory, NULL);
}

3.1 of_scan_flat_dt

要进一步了解其中的细节,我们需要先弄清楚of_scan_flat_dt函数做了什么:

/*** 遍历DTB的节点,直到参数传入的回调函数it返回非0值*/
int __init of_scan_flat_dt(int (*it)(unsigned long node,const char *uname, int depth,void *data),void *data)
{/* blob指向DTB在内存中的起始地址 */const void *blob = initial_boot_params;const char *pathp;int offset, rc = 0, depth = -1;/* 若设备树不存在则返回 */if (!blob)return 0;/* 从根节点开始遍历 */for (offset = fdt_next_node(blob, -1, &depth);/* 如果找到了有效的节点并且回调函数it返回0,则执行循环体 */offset >= 0 && depth >= 0 && !rc;/* 继续遍历下一个节点 */offset = fdt_next_node(blob, offset, &depth)) {/* 获取节点名 */pathp = fdt_get_name(blob, offset, NULL);/* 对于老版本的设备树,得到的是节点的路径名,因此要去掉多余的前缀 *//* 不过fdt_get_name已经考虑过这个问题了,这里有点多余 */if (*pathp == '/')pathp = kbasename(pathp);/*调用回调函数itoffset: 节点起始位置在DTB的structure block中的偏移pathp : 指向节点名depth : 节点的深度(层次)data  : 参数data,取决于调用者*/rc = it(offset, pathp, depth, data);}return rc;
}

不难看出,该函数只是一个遍历设备树节点的工具函数:遍历设备树节点,调用回调函数,如果回调函数判断该节点就是想要解析的节点,则进行相应的解析操作,并返回非0值,以指示该函数停止遍历动作。

3.2 解析/chosen节点

根据函数名字,以及3.1节的分析,猜也应该猜到传入的回调函数early_init_dt_scan_chosen用于解析/chosen节点:

/*offset: 节点起始位置在DTB的structure block中的偏移pathp : 指向节点名depth : 节点的深度(层次)data  : boot_command_line,一个字符数组
*/
int __init early_init_dt_scan_chosen(unsigned long node, const char *uname,int depth, void *data)
{int l;const char *p;const void *rng_seed;pr_debug("search \"chosen\", depth: %d, uname: %s\n", depth, uname);/* 如果遍历到的不是作为根节点的子节点的chosen节点,则指示of_scan_flat_dt继续遍历下一个节点 */if (depth != 1 || !data ||(strcmp(uname, "chosen") != 0 && strcmp(uname, "chosen@0") != 0))return 0;/* 当前节点是/chosen节点 *//* 解析/chosen节点的initrd属性,设置全局变量phys_initrd_start和phys_initrd_size */early_init_dt_check_for_initrd(node);/* 获取/chosen节点的bootargs属性的属性值 */p = of_get_flat_dt_prop(node, "bootargs", &l);/* 如果属性存在,则p指向bootargs属性值——一个字符串,l记录了字符串的长度(含'\0') */if (p != NULL && l > 0)/* 将启动参数拷贝到boot_command_line */strlcpy(data, p, min(l, COMMAND_LINE_SIZE));/** CONFIG_CMDLINE配置项意味着如果u-boot传递的参数不含启动参数,那么* CONFIG_CMDLINE就是默认的启动参数。如果含有启动参数,那么,是追加* 还是覆盖已有的启动参数,取决于另外两个配置项CONFIG_CMDLINE_EXTEND* 和CONFIG_CMDLINE_FORCE。*/
#ifdef CONFIG_CMDLINE
#if defined(CONFIG_CMDLINE_EXTEND)strlcat(data, " ", COMMAND_LINE_SIZE);strlcat(data, CONFIG_CMDLINE, COMMAND_LINE_SIZE);
#elif defined(CONFIG_CMDLINE_FORCE)strlcpy(data, CONFIG_CMDLINE, COMMAND_LINE_SIZE);
#else/* 如果DTB不带有启动参数,就使用kernel的启动参数——CONFIG_CMDLINE */if (!((char *)data)[0])strlcpy(data, CONFIG_CMDLINE, COMMAND_LINE_SIZE);
#endif
#endif /* CONFIG_CMDLINE */pr_debug("Command line is: %s\n", (char*)data);/* 对rng-seed节点的解析,暂时不清楚这个东西 */rng_seed = of_get_flat_dt_prop(node, "rng-seed", &l);if (rng_seed && l > 0) {......}/* 返回非0值,指示of_scan_flat_dt停止遍历 */return 1;
}

3.3 解析根节点的{size,address}-cells属性

在解析/memory节点之前,应该先得到根节点的{size,address}-cells属性值,因为/memory节点使用reg属性来存放内存的起始地址和长度,而解析reg属性少不了{size,address}-cells。

int __init early_init_dt_scan_root(unsigned long node, const char *uname,int depth, void *data)
{const __be32 *prop;/* 验证当前节点是否是根节点 */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属性则获取其值,并重新设置dt_root_size_cells */prop = of_get_flat_dt_prop(node, "#size-cells", NULL);if (prop)/* 注意大小端的转换 */dt_root_size_cells = be32_to_cpup(prop);pr_debug("dt_root_size_cells = %x\n", dt_root_size_cells);/* 如果存在#address-cells属性,则重新设置dt_root_addr_cells */prop = of_get_flat_dt_prop(node, "#address-cells", NULL);if (prop)dt_root_addr_cells = be32_to_cpup(prop);pr_debug("dt_root_addr_cells = %x\n", dt_root_addr_cells);/* 停止遍历 */return 1;
}

3.4 解析/memory节点

是时候解析/memory节点了:

int __init early_init_dt_scan_memory(unsigned long node, const char *uname,int depth, void *data)
{/* 获取/memory节点的device_type属性 */const char *type = of_get_flat_dt_prop(node, "device_type", NULL);const __be32 *reg, *endp;int l;bool hotpluggable;/* /memory节点的device_type属性值必须是memory */if (type == NULL || strcmp(type, "memory") != 0)return 0;/* 获取/memory节点的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));/* 获取hotpluggable属性值(指示是否可以热插拔) */hotpluggable = of_get_flat_dt_prop(node, "hotpluggable", NULL);pr_debug("memory scan node %s, reg size %d,\n", uname, l);/* 遍历reg属性记录的一块或多块内存 */while ((endp - reg) >= (dt_root_addr_cells + dt_root_size_cells)) {u64 base, 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);/* 对base和size进行一系列校验后,调用memblock_add添加内存块(struct memblock) */early_init_dt_add_memory_arch(base, size);if (!hotpluggable)continue;/* 若当前内存块可以热插拔,那么标记之 */if (early_init_dt_mark_hotplug_memory_arch(base, size))pr_warn("failed to mark hotplug range 0x%llx - 0x%llx\n",base, base + size);}return 0;
}

至此,内核对运行时配置信息的处理就介绍完了。

3.5 小结

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 属性的长度
value void *类型,指向属性值,位于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    = NULLmatches = of_default_bus_match_tablelookup  = NULLparent  = 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_devicematches : 如果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

参考文献

[1] linux-kernel-5.4.26源码及文档
[2] 韦东山老师的设备树视频教程

认识设备树(四)——内核对DTB文件的解析相关推荐

  1. Ubuntu环境搭建支持设备树,内核和根文件系统启动

    Ubuntu环境搭建支持设备树,内核和根文件系统启动 使用tftp获取设备树和内核文件 1.搭建TFTP服务器 sudo apt-get install tftp-hpa tftpd-hpa sudo ...

  2. 设备树之DTS与DTB格式

    目录 一.设备树 二.DTS格式 2.1 属性 2.2 节点 2.3 引用其他节点 2.4 小总结 三.DTB格式 3.1 结构 3.2 分析 一.设备树 对于点灯字符设备驱动程序可以有三种写法,首先 ...

  3. 设备中的c语言代码文件,设备树编译器无法识别包含文件的C语法

    这不是设备树语法问题,您只需使用c预处理器cpp预处理.dts文件,就可以获得设备树编译器可以按原样消化的文件. 在特定情况下,假设当前目录是内核根目录,则必须使用以下两个命令: cpp -nostd ...

  4. 我眼中的Linux设备树(四 中断)

    四 中断 中断一般包括中断产生设备和中断处理设备.中断控制器负责处理中断,每一个中断都有对应的中断号及触发条件.中断产生设备可能有多个中断源,有时多个中断源对应中断控制器中的一个中断,这种情况中断产生 ...

  5. 认识设备树(二)——设备树文件的格式

    目录 前言 1 DTS文件的格式 1.1 DTS文件的总体布局 1.2 memory reservations的格式 1.3 属性的格式 1.3.1 有关属性名 1.3.2 有关属性值 1.4 节点的 ...

  6. I.MX6ULL ARM驱动开发---设备树下的LED驱动实验

    一.什么是设备树?   设备树(Device Tree),将这个词分开就是"设备"和"树",描述设备树的文件叫做 DTS(Device Tree Source) ...

  7. 【正点原子Linux连载】第四十三章 Linux设备树 -摘自【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.0

    1)实验平台:正点原子阿尔法Linux开发板 2)平台购买地址:https://item.taobao.com/item.htm?id=603672744434 2)全套实验源码+手册+视频下载地址: ...

  8. Linux设备树DTB存储格式

    文章目录 DTB存储格式 编译和查看工具 Device Tree中的节点信息举例 Device Tree文件结构 DTB数据结构 struct ftd_header区域数据结构 memory rese ...

  9. 【正点原子MP157连载】第二十三章 Linux设备树-摘自【正点原子】STM32MP1嵌入式Linux驱动开发指南V1.7

    1)实验平台:正点原子STM32MP157开发板 2)购买链接:https://item.taobao.com/item.htm?&id=629270721801 3)全套实验源码+手册+视频 ...

最新文章

  1. 多线程环境中安全使用集合API(含代码)
  2. 窗口分析函数_11_生成百分比排名
  3. BT5下使用Armitage的一些问题
  4. mysql实战20 | 幻读是什么,幻读有什么问题?
  5. 阿群笔记:CentOS7 在线安装 docker 的推荐方法
  6. 插个队 leetcode 142. 环形链表 II
  7. 易语言服务器取cookie,QQ取本机cookie操作空间易语言源码
  8. 屏幕共享技术及相关软件使用测评
  9. ssm项目实战------------OA管理项目
  10. 403错误服务器未响应是什么意思,什么是HTTP ERROR 403?导致403错误的主要原因及解决方法...
  11. C# + opengl + Tao 环境配置
  12. echart中饼图如何显示数据 实现鼠标移动切换显示(vue中)
  13. 【PCL】.asc文件转换为.pcd文件
  14. 广度优先算法之狄克斯特拉算法
  15. sublime_字体更换
  16. 互联网快讯:粉笔科技双核驱动实现突围;猿辅导以科技助力教育提质增效;抖音升级谣言专项治理
  17. [ABC283D] 题解
  18. puts和fputs函数及其区别,C语言puts和fputs函数详解
  19. Win7Codecs+设置程序中英文对照
  20. 国产手机操作系统艰难探索

热门文章

  1. 【爬虫二】爬取豆瓣音乐榜单
  2. 给自己的网页制作一个网页图标
  3. 异或(^)的含义与基本用法
  4. 如何在visio中添加希腊字母
  5. carla与ros2的自动驾驶算法-planning与control算法开发与仿真
  6. 生产环境的 ServiceMesh 流量劫持怎么搞?百度有新招
  7. Handler机制原理解析(一)
  8. 复合材料在计算机的应用,计算机在复合材料中的应用
  9. 从字符串中提取出汉字?
  10. 委托代理机构申请专利怎么做?