嵌入式Linux驱动笔记(二十二)------设备树实现dtb转换成platform_device

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Guet_Kite/article/details/87943165

你好!这里是风筝的博客,

欢迎和我一起交流。


说来惭愧,虽说一直用设备树,但是都没有好好去看过他的实现细节,
所以今天抽空看了一下代码和网上的文章,试着通过读代码的方式来好好了解一下设备树dtb->platform_device的过程。

Linux内核从3.x开始引入设备树的概念,用于实现驱动代码与设备信息相分离。在设备树出现以前,所有关于设备的具体信息都要写在驱动里,一旦外围设备变化,驱动代码就要重写。引入了设备树之后,驱动代码只负责处理驱动的逻辑,而关于设备的具体信息存放到设备树文件中,这样,如果只是硬件接口信息的变化而没有驱动逻辑的变化,驱动开发者只需要修改设备树文件信息,不需要改写驱动代码。

Device Tree有自己的独立的语法,它的源文件为.dts,编译后得到.dtb,Bootloader在引导Linux内核的时候会将.dtb地址告知内核。之后内核会展开Device Tree并创建和注册相关的设备。我们就以Linux4.14为例,看一下kernel是怎么解析dtb的。

我们从Linux里启动的第一个C函数看起:

start_kernel // init/main.c
    setup_arch(&command_line);  // arch/arm/kernel/setup.c
    rest_init(); // init/main.c

先看setup_arch函数

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

	setup_processor();
	mdesc = setup_machine_fdt(__atags_pointer);//根据Device Tree的信息,找到最适合的machine描述符
	if (!mdesc)
		mdesc = setup_machine_tags(__atags_pointer, __machine_arch_type);//传统的方法setup_machine_tags来setup machine描述符
	machine_desc = mdesc;//得到machine_desc
	machine_name = mdesc->name;
	dump_stack_set_arch_desc("%s", mdesc->name);
	//省略部分.........
	arm_memblock_init(mdesc);//把DTB所占区域保留下来
	if (mdesc->restart)
		arm_pm_restart = mdesc->restart;

	unflatten_device_tree();//扫描并解析dtb文件,将节点组织成一个由device_node结构连接而成的单项链表
	
	//省略部分.........

这里我们主要了解两个函数:
【1】setup_machine_fdt(__atags_pointer)
【2】unflatten_device_tree()

其中__atags_pointer是bootloader传递参数的物理地址,我们看下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)))// 判断是否有效的dtb
		return NULL;

	mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);//找到最匹配的machine_desc

	if (!mdesc) {
		const char *prop;
		int size;
		unsigned long dt_root;

		early_print("\nError: unrecognized/unsupported "
			    "device tree compatible list:\n[ ");//打印错误。表示不支持

		dt_root = of_get_flat_dt_root();//找到设备树的根节点,dt_root指向根节点的属性地址处
		prop = of_get_flat_dt_prop(dt_root, "compatible", &size);//读出根节点的"compatible"属性的属性值
		while (size > 0) {//将根节点的"compatible"属性的属性值打印出来
			early_print("'%s' ", prop);
			size -= strlen(prop) + 1;
			prop += strlen(prop) + 1;
		}
		early_print("]\n\n");

		dump_machine_table(); /* does not return */
	}
	//省略部分.........
	early_init_dt_scan_nodes();//对设备树中运行时配置信息的处理
	//省略部分.........

	return mdesc;
}

里面的内容也是很简单:
根据传递进来的参数(__atags_pointer)判断是否是有效的dtb,然后通过函数【1.1】of_flat_dt_match_machine寻找设备树中根节点属性"compatible"的属性,找到最匹配的machine_desc。
如果找不到,则打印出根节点的"compatible"属性的属性值,以供开发人员调试。
最后调用【1.2】early_init_dt_scan_nodes函数做一些配置处理

所以我们先看看【1.1】of_flat_dt_match_machine函数:

const void * __init of_flat_dt_match_machine(const void *default_match,
		const void * (*get_next_compat)(const char * const**))
{
	dt_root = of_get_flat_dt_root();//读出设备树的根节点,dt_root指向根节点的属性地址处
	while ((data = get_next_compat(&compat))) {//遍历该系统中的所有machine_desc结构,返回给data,获取该结构的compatible
		score = of_flat_dt_match(dt_root, compat);//将系统中的所有machine_desc结构的compatible字符串与设备树根节点的compatible属性进行match
		if (score > 0 && score < best_score) {
			best_data = data;
			best_score = score;//返回与根节点属性compatible属性值最匹配的machine_desc结构
		}
	}
	//省略部分.........
	return best_data;
}

这里就是通过of_flat_dt_match函数去匹配设备树中machine的compatible属性和Linux的machine了,找到最合适的那个machine描述符,匹配成功就可以获取描述符machine_desc。那么Linux中的machine描述在哪呢?
很简单,搜索DT_MACHINE_START,machine描述符的列表就是通过DT_MACHINE_START和MACHINE_END来定义的。编译的时候,compiler会把这些machine descriptor放到一个特殊的段中(.arch.info.init),形成machine描述符的列表。
我的板子是allwiner h3:

static const char * const sun6i_board_dt_compat[] = {
	"allwinner,sun6i-a31",
	"allwinner,sun6i-a31s",
	NULL,
};
DT_MACHINE_START(SUN6I_DT, "Allwinner sun6i (A31) Family")
	.init_time	= sun6i_timer_init,
	.dt_compat	= sun6i_board_dt_compat,
MACHINE_END

接着我们看【1.2】early_init_dt_scan_nodes函数:

void __init early_init_dt_scan_nodes(void)
{
	/* Retrieve various information from the /chosen node */
	of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);
	/* Initialize {size,address}-cells info */
	of_scan_flat_dt(early_init_dt_scan_root, NULL);
	/* Setup memory, calling early_init_dt_add_memory_arch */
	of_scan_flat_dt(early_init_dt_scan_memory, NULL);
}

里面就三个函数,分别做的是:
1.解析chosen节点中bootargs属性的值,得到启动参数, 存入全局变量: boot_command_line
2.获取 #address-cells, #size-cells信息,并保存在dt_root_addr_cells和dt_root_size_cells全局变量中
3.解析 /memory 节点,获取内存配置信息,并把相关信息"base, size"保存在meminfo中,全局变量meminfo保存了系统内存相关的信息

所以总体来说,【1】setup_machine_fdt函数还是很简单的。

使用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用于运行时参数的传递。

在系统初始化的过程中,我们需要将DTB转换成节点是device_node的树状结构,以便后续方便操作。所以接下来看【2】unflatten_device_tree()函数:

void __init unflatten_device_tree(void)
{
	__unflatten_device_tree(initial_boot_params, NULL, &of_root,
				early_init_dt_alloc_memory_arch, false);//解析设备树

	/* Get pointer to "/chosen" and "/aliases" nodes for use everywhere */
	of_alias_scan(early_init_dt_alloc_memory_arch);//设置内核输出终端,以及遍历“/aliases”节点下的所有的属性,挂入相应链表
	unittest_unflatten_overlay_base();
}

这一轮我们也是主要看两个函数:
【2.1】__unflatten_device_tree
【2.2】of_alias_scan

注释如上。

看下【2.1】__unflatten_device_tree函数是如何组织device_node 的:

void *__unflatten_device_tree(const void *blob,
			      struct device_node *dad,
			      struct device_node **mynodes,
			      void *(*dt_alloc)(u64 size, u64 align),
			      bool detached)
{
	if (!blob) {//blob由initial_boot_params传入,initial_boot_params在setup_arch的arm_memblock_init
		pr_debug("No device tree pointer\n");
		return NULL;
	}
	//省略部分.........

	/* First pass, scan for size */
	size = unflatten_dt_nodes(blob, NULL, dad, NULL);//第一轮主要是确定device-tree structure的长度,保存在size变量中
	if (size < 0)
		return NULL;

	size = ALIGN(size, 4);
	pr_debug("  size is %d, allocating...\n", size);

	/* Allocate memory for the expanded device tree */
	mem = dt_alloc(size + 4, __alignof__(struct device_node));//一次性的分配了一大片内存,并不是扫描到一个node或者property就分配相应的内存
	if (!mem)
		return NULL;

	memset(mem, 0, size);

	*(__be32 *)(mem + size) = cpu_to_be32(0xdeadbeef);//设备树结束处赋值0xdeadbeef

	pr_debug("  unflattening %p...\n", mem);

	/* Second pass, do actual unflattening */
	unflatten_dt_nodes(blob, mem, dad, mynodes);//dtb解析主要函数,解析完构成了 device node 树
	if (be32_to_cpup(mem + size) != 0xdeadbeef)//检查是否有数据溢出
		pr_warning("End of tree marker overwritten: %08x\n", be32_to_cpup(mem + size));
	//省略部分.........
	return mem;
}

函数里面会调用两次【2.1.1】unflatten_dt_nodes,第一次获取device-tree structure的大小,用以分配内存(一次性的分配了一大片内存)。第二次则是构建device node tree,当然,是调用函数里面的【2.1.1.1】populate_node实现的。
这里不得不说一下device_node结构体:

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

	struct	property *properties;//该节点的属性列表
	struct	property *deadprops;/* removed properties *///如果需要删除某些属性,kernel并非真的删除,而是挂入到deadprops的列表
	struct	device_node *parent;//parent、child以及sibling将所有的device node连接起来
	struct	device_node *child;
	struct	device_node *sibling;//// 节点的兄弟(同级节点)
	struct	kobject kobj;
	unsigned long _flags;
	void	*data;
#if defined(CONFIG_SPARC)
	const char *path_component_name;
	unsigned int unique_id;
	struct of_irq_controller *irq_trans;
#endif
};

每一个节点都转换为一个device_node结构体,这些device_node构成一棵树, 根节点为: of_root,device_node结构体中有properties, 用来表示该节点的属性
每一个属性对应一个property结构体,通过property.next指针进行链接,形成一个单链表:

struct property {
	char	*name;// 属性名字, 指向dtb文件中的字符串
	int	length;// 属性值的长度
	void	*value;// 属性值, 指向dtb文件中value所在位置, 数据仍以big endian存储
	struct property *next;
	unsigned long _flags;
	unsigned int unique_id;
	struct bin_attribute attr;
};

【2.1.1.1】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)
{
	//省略部分.........
	np = unflatten_dt_alloc(mem, sizeof(struct device_node) + allocl,
				__alignof__(struct device_node));//分配device_node结构
	if (!dryrun) {
		char *fn;
		of_node_init(np);//里面设置node->fwnode.ops各种操作函数
		np->full_name = fn = ((char *)np) + sizeof(*np);// 完整节点的名字, node-name[@unit-address],即包括各级父节点的名称
		if (new_format) {//传入的路径为'/'则new_format为0,否则为1,建立新的full path
			/* rebuild full path for new format */
			if (dad && dad->parent) {
				strcpy(fn, dad->full_name);
#ifdef DEBUG
				if ((strlen(fn) + l + 1) != allocl) {
					pr_debug("%s: p: %d, l: %d, a: %d\n",
						pathp, (int)strlen(fn),
						l, allocl);
				}
#endif
				fn += strlen(fn);
			}
			*(fn++) = '/';
		}
		memcpy(fn, pathp, l);

		if (dad != NULL) {//若父亲节点不为空,则设置该节点的parent
			np->parent = dad;//指向父亲节点
			np->sibling = dad->child;
			dad->child = np;
		}
	}
	populate_properties(blob, offset, mem, np, pathp, dryrun);//里面设置节点的属性,也就是device_node 的*properties
	if (!dryrun) {
		np->name = of_get_property(np, "name", NULL);
		np->type = of_get_property(np, "device_type", NULL);

		if (!np->name) // 来自节点中的name属性, 如果没有该属性, 则设为"NULL"
			np->name = "<NULL>";
		if (!np->type)
			np->type = "<NULL>";
	}
}

注释如上,通俗易懂!
里面就是开始为节点分配device_node了,接着就是调用【2.1.1.1.1】populate_properties函数设置节点的属性properties和填充device_node结构体了

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;// 设置每个device_node的属性
	for (cur = fdt_first_property_offset(blob, offset);
	     cur >= 0;
	     cur = fdt_next_property_offset(blob, cur)) {//处理该node节点下面所有的property
	//省略部分.........
		pp = unflatten_dt_alloc(mem, sizeof(struct property),
						__alignof__(struct property));//分配内存
						
		if (!strcmp(pname, "phandle") ||
		    !strcmp(pname, "linux,phandle")) {/* 处理phandle,得到phandle值 */
			if (!np->phandle)
				np->phandle = be32_to_cpup(val);
		}
		pp->name   = (char *)pname; // 属性名字, 指向dtb文件中的字符串
		pp->length = sz;// 属性值的长度
		pp->value  = (__be32 *)val; // 属性值, 指向dtb文件中value所在位置, 数据仍以big endian存储
		*pprev     = pp;
		pprev      = &pp->next;
	}
	if (!has_name) {
		const char *p = nodename, *ps = p, *pa = NULL;
		int len;

		while (*p) {
			if ((*p) == '@')
				pa = p;
			else if ((*p) == '/')
				ps = p + 1;
			p++;
		}

		if (pa < ps)
			pa = p;
		len = (pa - ps) + 1;// 得到长度
		pp = unflatten_dt_alloc(mem, sizeof(struct property) + len,
					__alignof__(struct property));
		if (!dryrun) {
			pp->name   = "name";//每一个node都会自动添加一个名称为“name”的property
			pp->length = len;
			pp->value  = pp + 1;
			*pprev     = pp;
			pprev      = &pp->next;
			memcpy(pp->value, ps, len - 1);//属性name的value值为node节点的名称,取“/”和“@”之间的子串
			((char *)pp->value)[len - 1] = 0;
			pr_debug("fixed up name for %s -> %s\n",
				 nodename, (char *)pp->value);
		}
	}
}

到这里就是【2.1】__unflatten_device_tree函数以及它的子函数的调用过程了,到这里就差不多是dtb转换为device_node的过程了。
不过还有一点点,看完【2.1】__unflatten_device_tree函数别忘了还有【2.2】of_alias_scan函数。

在设备树中有一个叫做aliases的节点,例如:

 aliases {
        spi0 = "/spi@13920000";
        spi1 = "/spi@13930000";
        spi2 = "/spi@13940000";
         i2c0 = "/i2c@13860000";
         i2c1 = "/i2c@13870000";
         ... ...
    };
void of_alias_scan(void * (*dt_alloc)(u64 size, u64 align))
{
	struct property *pp;

	of_aliases = of_find_node_by_path("/aliases");// 找到/aliases节点对应的device_node
	of_chosen = of_find_node_by_path("/chosen");// 找到/chosen节点对应的device_node
	if (of_chosen == NULL)// 如果没有/chosen的话,就找/chosen@0节点
		of_chosen = of_find_node_by_path("/chosen@0");

	if (of_chosen) {
		/* linux,stdout-path and /aliases/stdout are for legacy compatibility */
		const char *name = NULL;

		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);
	}

	if (!of_aliases)
		return;

	for_each_property_of_node(of_aliases, pp) {//遍历/aliases节点的属性,以属性i2c2 = "/i2c@13880000";为例
		const char *start = pp->name;// 属性的名字,如"i2c2"
		const char *end = start + strlen(start);// 名字的结尾,*end是'\0'
		struct device_node *np;
		struct alias_prop *ap;
		int id, len;

		/* Skip those we do not want to proceed */
		if (!strcmp(pp->name, "name") ||//不处理名字是name、phandle、linux,phandle的属性
		    !strcmp(pp->name, "phandle") ||
		    !strcmp(pp->name, "linux,phandle"))
			continue;

		np = of_find_node_by_path(pp->value);
		 /*
                根据属性的值(如"/i2c@13880000")获得这个值对应的节点
                i2c@13880000 {
                    #address-cells = <0x1>;
                    #size-cells = <0x0>;
                    compatible = "samsung,s3c2440-i2c";
                    reg = <0x13880000 0x100>;
                    interrupts = <0x0 0x3c 0x0>;
                    clocks = <0x7 0x13f>;
                    clock-names = "i2c";
                    pinctrl-names = "default";
                    pinctrl-0 = <0x22>;
                    status = "disabled";
                };        
         */
		if (!np)
			continue;

		/* walk the alias backwards to extract the id and work out
		 * the 'stem' string */
		while (isdigit(*(end-1)) && end > start)//对于"i2c2",end最终会指向字符'2'的地址
			end--;
		len = end - start; // 获得"i2c"的长度(不包含结尾的数字2),就是3

		if (kstrtoint(end, 10, &id) < 0)// 将end指向的字符'2'转化为数字2,赋值给id
			continue;

		/* Allocate an alias_prop with enough space for the stem */
		ap = dt_alloc(sizeof(*ap) + len + 1, __alignof__(*ap));// 分配内存,多分配的"len+1"用于存放stem的名字
		if (!ap)
			continue;
		memset(ap, 0, sizeof(*ap) + len + 1);
		ap->alias = start;// ap->alias指向字符串"i2c2"
		of_alias_add(ap, np, id, start, len);//里面会添加到aliases_lookup
	}
}

of_alias_add函数:

static void of_alias_add(struct alias_prop *ap, struct device_node *np,
			 int id, const char *stem, int stem_len)
{
	ap->np = np;// np是"/i2c@13880000"对应的节点device_node
	ap->id = id;// id的值是2
	strncpy(ap->stem, stem, stem_len);// 由于stem_len是3,所以ap->stem被赋值为"i2c"
	ap->stem[stem_len] = 0;
	list_add_tail(&ap->link, &aliases_lookup);// 将这个ap加入到全局aliases_lookup链表中
	pr_debug("adding DT alias:%s: stem=%s id=%i node=%pOF\n",
		 ap->alias, ap->stem, ap->id, np);
}

在i2c里面,就会调用i2c_add_adapter函数

int i2c_add_adapter(struct i2c_adapter *adapter)
 {
    struct device *dev = &adapter->dev;
    int id;  
   if (dev->of_node) {
       id = of_alias_get_id(dev->of_node, "i2c");//获得与这个device_node(即/i2c@13880000节点)对应的alias_prop的id
       //如果以/i2c@13880000节点为例,这里得到的id就是2
        if (id >= 0) {
            adapter->nr = id;
             return __i2c_add_numbered_adapter(adapter);
         }
     }
 }
int of_alias_get_id(struct device_node *np, const char *stem)
 {
     mutex_lock(&of_mutex);
     list_for_each_entry(app, &aliases_lookup, link) { // 遍历全局链表aliases_lookup
        if (strcmp(app->stem, stem) != 0) // 找到 stem 是 "i2c" 的alias_prop
            continue;
  
         if (np == app->np) { // 判断这个alias_prop指向的device_node是不是跟传入的匹配
             id = app->id;  // 获得 id,2
            break;
        }
     }
     mutex_unlock(&of_mutex);
     return id;
}

從上面的分析就可以知道alias節點的作用了:
比如SoC上有如果多個i2c控制器,alias的相當於給每個i2c控制器分配一個唯一的編號,如上面的i2c@13880000對應的alias是i2c2,那麼這個編號就是2,將來就可以在/dev下看到名爲i2c-2的設備節點。
在內核中可以看到很多地方都會調用of_alias_get_id,他的作用就是根據傳入的device
node,在alias中找到對應的唯一編號,如: of_alias_get_id(pdev->dev.of_node, “spi”)
of_alias_get_id(node, “fimc”) of_alias_get_id(pdev->dev.of_node,
“serial”)

上面关于alias节点的分析,来自:基于tiny4412的Linux内核移植 — aliases节点解析

接下来就是device_node 怎么转换为platform_device。
文章开头就有,start_kernel 里有两个函数,一个是setup_arch(&command_line),另一个就是rest_init()了

rest_init();
        pid = kernel_thread(kernel_init, NULL, CLONE_FS);
                    kernel_init
                        kernel_init_freeable();
                            do_basic_setup();
                                do_initcalls();
                                    for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
                                        do_initcall_level(level);

do_initcall_level就会调用一些__define_initcall修饰过的函数

	#define arch_initcall(fn)          __define_initcall("3",fn,3)
    #define arch_initcall_sync(fn)        __define_initcall("3s",fn,3s)
如:
arch_initcall(customize_machine);
arch_initcall_sync(of_platform_default_populate_init);

以前版本里customize_machine函数里会调用of_platform_populate函数的,现在没有了:

static int __init customize_machine(void)
{
	if (machine_desc->init_machine)
		machine_desc->init_machine();

	return 0;
}
arch_initcall(customize_machine);

这里会调用machine描述符的init_machine函数,machine_desc在文章开头说有,我们是allwiner h3,没有填充init_machine字段。

of_platform_default_populate_init
	of_platform_default_populate(NULL, NULL, NULL)
		of_platform_populate(root, of_default_bus_match_table, lookup,parent)

注意了,这里of_platform_populate会传入参数of_default_bus_match_table

int of_platform_populate(struct device_node *root,
			const struct of_device_id *matches,//match table重点是后续节点递归处理时,需要和该table mach后才可以继续递归处理
			const struct of_dev_auxdata *lookup,
			struct device *parent)
{
	struct device_node *child;
	int rc = 0;
	//此时传入的root=NULL,从dts的\节点开始逐一的递归处理,
	//否则根据所选择的device node作为root,做递归处理
	root = root ? of_node_get(root) : of_find_node_by_path("/");// 找到root device node
	if (!root)
		return -EINVAL;

	for_each_child_of_node(root, child) {// 遍历root device node的 device node
		rc = of_platform_bus_create(child, matches, lookup, parent, true);//创建
		if (rc) {
			of_node_put(child);
			break;
		}
	}
	of_node_set_flag(root, OF_POPULATED_BUS);

	of_node_put(root);
	return rc;
}

里面就是遍历device_node,然后调用of_platform_bus_create为每个 “适合” 的node创建注册platform_device。

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)
{
	if (strict && (!of_get_property(bus, "compatible", NULL))) {// 这样可以把chosen、aliases、memory等没有compatible属性的节点排除在外
		pr_debug("%s() - skipping %pOF, no compatible prop\n",
			 __func__, bus);
		return 0;
	}
	//省略部分.........
	
	dev = of_platform_device_create_pdata(bus, bus_id, platform_data, parent);// 根据device node创建 platform_device并注册
	if (!dev || !of_match_node(matches, bus))// 判断是不是需要继续遍历这个device node下的child device node
		return 0;

	for_each_child_of_node(bus, child) {// 遍历这个device node下的child device node,将child device node也注册为platform_device
		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_bus_create函数一开始就是去寻找有"compatible"属性的节点,如果节点属性不存在,则表明其不需要生成为platform_device,这样可以把chosen、aliases、memory等没有compatible属性的节点排除在外。
接着调用of_platform_device_create_pdata函数,它里面把各种Device Tree中定义的platform device设备节点加入到系统(即platform bus的所有的子节点,对于device tree中其他的设备节点,需要在各自bus controller初始化的时候自行处理)。

之后接着调用of_match_node(matches, bus)函数,
我们知道,设备树里存在很多节点以及他的子节点, 不是所有的节点都是需要我们注册成platform_device的。也就是说dts中定义的各种device node,往往只是用来辅助核心的device node而存在的,这些node存在并不需要加载为platform device。
但是系统怎么知道该把那些子节点注册成platform_device呢?
答案就在of_match_node的参数matches里,matches是由of_default_bus_match_table得来:

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

只有节点里compatible 属性与of_default_bus_match_table里的任一.compatible字段匹配(“simple-bus”,“simple-mfd”,“isa”,“arm,amba-bus”),of_match_node函数才会成立,程序才会往下执行,才会继续调用for_each_child_of_node函数(递归调用)遍历子节点并为子节点(需含compatile属性)注册platform_device。

当然,也不是所有的device node都会挂入bus上的设备链表,比如cpus node,memory node,choose node等
也就是说,arm dts中的设备,除了cpu、memory、chosen、interrupt节点,全部都默认挂入platform总线
DTS只描述platform
device(AMBA除外)。这是基于这样的考虑:其它bus上的device,其bus应该具备动态枚举设备的能力。只有platform_device,才一开机就存在,需要靠device tree来描述

举个例子,参考自韦东山设备树视频:

 / {
          mytest {
              compatile = "mytest", "simple-bus";
              mytest@0 {
                    compatile = "mytest_0";
              };
          };
          
          i2c {
              compatile = "samsung,i2c";
              at24c02 {
                    compatile = "at24c02";                      
              };
          };
      };

/mytest会被转换为platform_device, 因为它兼容"simple-bus", 它的子节点/mytest/mytest@0 也会被转换为platform_device
/i2c节点一般表示i2c控制器, 它会被转换为platform_device, 在内核中有对应的platform_driver;
/i2c/at24c02节点不会被转换为platform_device!!!

就像我们之前说的,“其它bus上的device,其bus应该具备动态枚举设备的能力”。i2c, spi等总线节点下的子节点, 应该交给其对应的总线驱动程序来处理, 它们不应该被转换为platform_device。这里i2c节点(i2c adapter)是挂载在platform_bus上,被转换为platform_device, 在内核中有对应的platform_driver,它的子节点(at24c02 节点)被如何处理完全由父节点的platform_driver决定。
i2c adapter驱动的probe函数会调用如下:

i2c_add_numbered_adapter   // drivers/i2c/i2c-core-base.c
        __i2c_add_numbered_adapter
            i2c_register_adapter
                of_i2c_register_devices(adap);   // drivers/i2c/i2c-core-of.c
                    for_each_available_child_of_node(bus, node) {//遍历i2c节点下面的每一个子节点
                        client = of_i2c_register_device(adap, node);//为子节点(status = “disable”的除外)创建i2c_client结构体,并与子节点的device_node挂接
                                    client = i2c_new_device(adap, &info);// 设备树中的i2c子节点被转换为i2c_client
                    }

spi_master驱动的probe函数会调用如下:

spi_register_controller        // drivers/spi/spi.c
        of_register_spi_devices   // drivers/spi/spi.c
            for_each_available_child_of_node(ctlr->dev.of_node, nc) {//遍历spi节点下面的每一个子节点
                spi = of_register_spi_device(ctlr, nc);  // 设备树中的spi子节点被转换为spi_device
                                spi = spi_alloc_device(ctlr);
                                rc = of_spi_parse_dt(ctlr, spi, nc);
                                rc = spi_add_device(spi);
            }

i2c adapter设备
在of_i2c_register_devices()函数内部遍历i2c节点下面的每一个子节点,并为子节点(status = “disable”的除外)创建i2c_client结构体,并与子节点的device_node挂接。其中i2c_client的填充是在i2c_new_device()中进行的,最后device_register()。在构建i2c_client的时候,会对node下面的compatible属性名称的厂商名字去除作为i2c_client的name。例如:compatible = “maxim,ds1338”,则i2c_client->name = “ds1338”

明白了这一层关系,我们继续回到之前的of_platform_bus_create函数里,他在遍历子节点前是要先调用of_platform_device_create_pdata(bus, bus_id, platform_data, parent)函数为当前节点创建platform_device的。

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;//这里就是platform_device结构体
	
	dev = of_device_alloc(np, bus_id, parent);
	
	dev->dev.bus = &platform_bus_type;//挂到platform_bus上
	dev->dev.platform_data = platform_data;
	of_msi_configure(&dev->dev, dev->dev.of_node);

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

这个函数最关键,可以说是本文章的最后一步了,就是转换成plateform_device,里面
of_device_alloc函数分配struct platform_device的内存,还分配了该platform device需要的resource的内存(参考struct platform_device 中的resource成员)。当然,这就需要解析该device node的interrupt资源以及memory address资源

of_device_add(dev)就是把plateform_device添加到设备模型系统中了。

struct platform_device *of_device_alloc(struct device_node *np,
				  const char *bus_id,
				  struct device *parent)
{
	struct platform_device *dev;//这里就是platform_device结构体
	int rc, i, num_reg = 0, num_irq;//reg、中断 个数
	struct resource *res, temp_res;

	dev = platform_device_alloc("", PLATFORM_DEVID_NONE);//创建一个平台设备对象,该对象可以附加其他对象,并且在释放时将释放附加的对象
	if (!dev)
		return NULL;
	while (of_address_to_resource(np, num_reg, &temp_res) == 0)
		num_reg++;//获取reg个数
	num_irq = of_irq_count(np);//获取中断个数
	/* Populate the resource table */
	if (num_irq || num_reg) {//填充资源
		res = kzalloc(sizeof(*res) * (num_irq + num_reg), GFP_KERNEL);
		if (!res) {
			platform_device_put(dev);
			return NULL;
		}

		dev->num_resources = num_reg + num_irq;//资源数
		dev->resource = res;
		for (i = 0; i < num_reg; i++, res++) {
			rc = of_address_to_resource(np, i, res);//将io地址转换成struct resource资源信息
			WARN_ON(rc);
		}
		if (of_irq_to_resource_table(np, res, num_irq) != num_irq)//将中断号转换成中断资源信息并赋值给平台设备
			pr_debug("not all legacy IRQ resources mapped for %s\n",
				 np->name);
	}
	dev->dev.of_node = of_node_get(np);//设置dev->dev.of_node
	dev->dev.fwnode = &np->fwnode;
	dev->dev.parent = parent ? : &platform_bus;

	if (bus_id)
		dev_set_name(&dev->dev, "%s", bus_id);
	else
		of_device_make_bus_id(&dev->dev);

	return dev;
}

of_device_alloc函数里计算出reg和irq个数,就可以从设备树获取property来填充resource了。
of_address_to_resource函数是获取reg
of_irq_to_resource_table函数是获取irq

自此,dtb就完全转换为platform_device并注册进platform_bus了。

这里有蜗窝科技里的一段话:

device tree的本质。 没错,device tree的目的是抽象、描述硬件,但是,抽象、描述硬件并不是最终目标。最终目标是:“通过修改device tree,方便的支持不同的平台、版型,从而不需要重新编译kernel”。 要实现这个最终目标,device tree所描述的内容要具备哪些基本条件?共性!
回想一下device tree的基本内容:属性=值。因此所谓的共性,就是要有相同的属性,通过修改属性值,达到支持不同平台、版型的目的,例如reg、irq、gpio、reset、power gate等等

参考:
蜗窝科技设备树系列文章
Device Tree(三):代码分析
彭东林博客:基于tiny4412的Linux内核移植 — aliases节点解析
韦东山设备树视频

展开阅读全文

没有更多推荐了,返回首页