说来惭愧,虽说一直用设备树,但是都没有好好去看过他的实现细节,
所以今天抽空看了一下代码和网上的文章,试着通过读代码的方式来好好了解一下设备树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 = "";
if (!np->type)
np->type = "";
}
}
注释如上,通俗易懂!
里面就是开始为节点分配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节点解析
韦东山设备树视频