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

目录

  • 前言
  • 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_on

ENTRY(__turn_mmu_on)
	mov	r0, r0
	instr_sync
	mcr	p15, 0, r0, c1, c0, 0		@ write control reg
	mrc	p15, 0, r3, c0, c0, 0		@ read id reg
	instr_sync
	mov	r3, r3
	/* r13 = __mmap_switched */
	mov	r3, r13
	ret	r3
__turn_mmu_on_end:
ENDPROC(__turn_mmu_on)

/*
	3、在__mmap_switched中,将r2(保存DTB的地址)的值存到__atags_pointer,然后跳转start_kernel
*/
__mmap_switched:
	mov	r7, r1
	mov	r8, r2    /* 把DTB的地址转存到r8 */
	mov	r10, r0

	adr	r4, __mmap_switched_data
	mov	fp, #0
	
    ARM(	ldmia	r4!, {r0, r1, sp} )
	sub	r2, r1, r0
	mov	r1, #0
	bl	memset				@ clear .bss

	ldmia	r4, {r0, r1, r2, r3}
	str	r9, [r0]			@ Save processor ID
	str	r7, [r1]			@ Save machine type
	
	/* 把DTB的地址写到全局变量__atags_pointer */
	str	r8, [r2]			@ Save atags pointer
	
	cmp	r3, #0
	strne	r10, [r3]			@ Save control register values
	mov	lr, #0
	b	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_mach
	
	arch_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保存到compat
		compat指向一系列字符串(一个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);
		/*
			调用回调函数it
			offset: 节点起始位置在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 小结

认识设备树(四)——内核对DTB文件的解析_第1张图片

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

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

内核会保留DTB所占据的内存区域,因此DTB文件中的数据在kernel启动后也是可用的,相关源码的调用路径如下:
认识设备树(四)——内核对DTB文件的解析_第2张图片

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构成的树状结构:
认识设备树(四)——内核对DTB文件的解析_第3张图片
值得一提的是,为了突出device_node的name成员和full_name成员的差别,在上图中,我将没有name属性的节点的name成员置为,这源于:

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成员置为"" */
			np->name = "";
	}
	......
}

但实际上,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的调用点:
认识设备树(四)——内核对DTB文件的解析_第4张图片

4.5.3 浅析转换的过程

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

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

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

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

	return 0;
}

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

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

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

	of_node_put(root);
	return rc;
}

再看of_platform_bus_create

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

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

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

	/* 如果bus节点的compatible属性比较特殊,比如是"simple-bus",则需要尝试为其子节点创建platform_device */
	if (!dev || !of_match_node(matches, bus))
		return 0;
	
	for_each_child_of_node(bus, child) {
		pr_debug("   create child: %pOF\n", child);
		/* 整个过程是递归进行的 */
		rc = of_platform_bus_create(child, matches, lookup, &dev->dev, strict);
		if (rc) {
			of_node_put(child);
			break;
		}
	}
	of_node_set_flag(bus, OF_POPULATED_BUS);
	return rc;
}

函数of_platform_device_create_pdata会调用of_device_alloc以及of_device_add创建并注册platform_device,具体的就不再跟踪了。

4.5.4 如何处理平台设备对应的设备节点的子节点

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

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

4.6 小结

认识设备树(四)——内核对DTB文件的解析_第5张图片

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] 韦东山老师的设备树视频教程

你可能感兴趣的:(#,linux,设备树)