kernel启动流程_DTS解析(源码层面)
此篇博客有很多参考其他文章的内容,由于参考内容繁杂,不一一标注角标了,在末尾会贴上所有参考博客的link,如有侵权,请联系本人处理,谢谢。
深入,并且广泛
-沉默犀牛
我认为作为初学者去学习kernel代码的一个重要方法就是:先知道这些代码是干嘛的,然后再找代码来验证想法。这样的探索顺序会变得事半功倍,让我们直接去看繁杂的代码来分析出代码用途,是非人道主义的。所以此篇博客会先用文字描述一下大致流程,再带着读者到代码中去验证。
执行流程
从dts文件的内容来看,系统平台上挂载了很多总线,i2c,spi,uart等等,每一个总线都被描述为一个节点,Linux启动到kernel 入口后,会执行以下操作来加载系统平台上的总线和设备:start_kernel() ==> setup_arch() ==> unflatten_device_tree()
,执行完unflatten_device_tree()
后,dts的节点信息被解析出来,保存到allnodes链表中。随后启动到board文件时,调用.init_machine,再调用of_platform_populate(....)
接口,加载平台总线和平台设备。至此,系统平台上的所有已配置的总线和设备将被注册到系统中。(对这句话更加正确的解释是:此时所说的设备指的是platform device,此时的总线指的是i2c,spi等,因为i2c总线和spi总线可以理解为注册在platform总线上的device)
注意:不是dtsi文件中所有的节点都会被注册,在注册总线和设备时,会对dts节点的状态作一个判断,如果节点里面的status属性没有被定义,或者status属性被定义了并且值被设为“ok”或者“okay”,其他情况则不被注册到系统中。
那么其他设备,例如i2c、spi设备是怎样注册到系统中的呢?下面我们就以i2c设备为例,看看Linux怎样注册i2c设备到系统中。以高通平台为例,在注册i2c总线时,会调用到qup_i2c_probe()接口,该接口用于申请总线资源和添加i2c适配器。在成功添加i2c适配器后,会调用of_i2c_register_devices()接口。此接口会解析i2c总线节点的子节点(挂载在该总线上的i2c设备节点),获取i2c设备的地址、中断号等硬件信息。然后调用request_module()加载设备对应的驱动文件,调用i2c_new_device(),生成i2c设备。此时设备和驱动都已加载,于是drvier里面的probe方法将被调用。后面流程就和之前一样了。
代码验证
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);
machine_desc = mdesc;
machine_name = mdesc->name;
dump_stack_set_arch_desc("%s", mdesc->name);
if (mdesc->reboot_mode != REBOOT_HARD)
reboot_mode = mdesc->reboot_mode;
init_mm.start_code = (unsigned long) _text;
init_mm.end_code = (unsigned long) _etext;
init_mm.end_data = (unsigned long) _edata;
init_mm.brk = (unsigned long) _end;
/* populate cmd_line too for later use, preserving boot_command_line */
strlcpy(cmd_line, boot_command_line, COMMAND_LINE_SIZE);
*cmdline_p = cmd_line;
early_fixmap_init();
early_ioremap_init();
parse_early_param();
#ifdef CONFIG_MMU
early_paging_init(mdesc);
#endif
setup_dma_zone(mdesc);
xen_early_init();
efi_init();
/*
* Make sure the calculation for lowmem/highmem is set appropriately
* before reserving/allocating any mmeory
*/
adjust_lowmem_bounds();
arm_memblock_init(mdesc);
/* Memory may have been removed so recalculate the bounds. */
adjust_lowmem_bounds();
early_ioremap_reset();
paging_init(mdesc);
request_standard_resources(mdesc);
if (mdesc->restart)
arm_pm_restart = mdesc->restart;
unflatten_device_tree(); //将DTB转换成节点是device_node的树状结构
我注释的两行代码就是有关DTS解析的重要代码,如注释所述,它们分别做了:
1.根据Device Tree的信息,找到最适合的machine描述符
2.将DTB转换成节点是device_node的树状结构
现在分别仔细看一下实现过程:
1.根据Device Tree的信息,找到最适合的machine_desc,先了解一下结构体machine_desc:
struct machine_desc {
unsigned int nr; /* architecture number */
const char *name; /* architecture name */
unsigned long atag_offset; /* tagged list (relative) */
const char *const *dt_compat; /*!!!!本文中最重要的成员!!!!!
用于匹配DTS文件*/
unsigned int nr_irqs; /* number of IRQs */
#ifdef CONFIG_ZONE_DMA
phys_addr_t dma_zone_size; /* size of DMA-able area */
#endif
unsigned int video_start; /* start of video RAM */
unsigned int video_end; /* end of video RAM */
unsigned char reserve_lp0 :1; /* never has lp0 */
unsigned char reserve_lp1 :1; /* never has lp1 */
unsigned char reserve_lp2 :1; /* never has lp2 */
enum reboot_mode reboot_mode; /* default restart mode */
unsigned l2c_aux_val; /* L2 cache aux value */
unsigned l2c_aux_mask; /* L2 cache aux mask */
void (*l2c_write_sec)(unsigned long, unsigned);
const struct smp_operations *smp; /* SMP operations */
bool (*smp_init)(void);
void (*fixup)(struct tag *, char **);
void (*dt_fixup)(void);
long long (*pv_fixup)(void);
void (*reserve)(void);/* reserve mem blocks */
void (*map_io)(void);/* IO mapping function */
void (*init_early)(void);
void (*init_irq)(void);
void (*init_time)(void);
void (*init_machine)(void);
void (*init_late)(void);
#ifdef CONFIG_MULTI_IRQ_HANDLER
void (*handle_irq)(struct pt_regs *);
#endif
void (*restart)(enum reboot_mode, const char *);
};
接下来看一看setup_machine_fdt()函数的具体实现:
const struct machine_desc * __init setup_machine_fdt(unsigned int dt_phys)
{
const struct machine_desc *mdesc, *mdesc_best = NULL;
#if defined(CONFIG_ARCH_MULTIPLATFORM) || defined(CONFIG_ARM_SINGLE_ARMV7M)
DT_MACHINE_START(GENERIC_DT, "Generic DT based system")
.l2c_aux_val = 0x0,
.l2c_aux_mask = ~0x0,
MACHINE_END
mdesc_best = &__mach_desc_GENERIC_DT;
#endif
if (!dt_phys || !early_init_dt_verify(phys_to_virt(dt_phys))) /*early_init_dt_verify()检查fdt头部的合法性,然后设置fdt全局变量以
及计算crc,赋值了一个initial_boot_params变量后边在访问设备树的时候还会用到*/
return NULL;
mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach); //重要函数,下面详细介绍
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();
prop = of_get_flat_dt_prop(dt_root, "compatible", &size);
while (size > 0) {
early_print("'%s' ", prop);
size -= strlen(prop) + 1;
prop += strlen(prop) + 1;
}
early_print("]\n\n");
dump_machine_table(); /* does not return */
}
/* We really don't want to do this, but sometimes firmware provides buggy data */
if (mdesc->dt_fixup)
mdesc->dt_fixup();
early_init_dt_scan_nodes();
/* Change machine number to match the mdesc we're using */
__machine_arch_type = mdesc->nr;
return mdesc;
}
为了讲清楚mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);
这一句话的作用,我们先分析这个函数的第二个参数arch_get_next_match
:
static const void * __init arch_get_next_mach(const char *const **match)
{
static const struct machine_desc *mdesc = __arch_info_begin;
const struct machine_desc *m = mdesc;
if (m >= __arch_info_end)
return NULL;
mdesc++;
*match = m->dt_compat;
return m;
}
这个函数很简单,注意的是mdesc是静态局部变量,第一次调用指向__arch_info_begin,后边每次调用都mdesc++,如果超过了__arch_info_end就返回NULL。以上代码说明在__arch_info_begin和__arch_info_end两个地址之间存储着多个machine_desc变量(也可能是一个),该函数遍历这些变量,通过match参数返回所有machine_desc结构体的dt_compat变量指针。
那么__arch_info_begin和__arch_info_end地址是怎么来的呢?在arch/arm/kernel/vmlinux.lds.S连接脚本中定义了.arch.info.init段,__arch_info_begin和__arch_info_end地址分别是该段的首尾地址。
.init.arch.info : {
__arch_info_begin = .;
*(.arch.info.init)
__arch_info_end = .;
}
那么.init.arch.info段的内容怎么来的呢?这就要参考DT_MACHINE_START和MACHINE_END宏了,arm架构的定义在arch/arm/include/asm/mach/arch.h文件,如下所示:
define DT_MACHINE_START(_name, _namestr) \
static const struct machine_desc __mach_desc_##_name \
__used \
__attribute__((__section__(".arch.info.init"))) = { \
.nr = ~0, \
.name = _namestr,
#endif
从该宏代码看出它定义了一个machine_desc类型的静态局部变量,该变量位于.arch.info.init段中。
我们已经知道了arch_get_next_match
指针的具体实现了,现在继续看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**))
{
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 = of_get_flat_dt_root();
/*遍历.arch.info.init段中所有的dt_compat变量, 然后通过of_flat_dt_match计算一个分数,
并且寻找那个score最小的*/
while ((data = get_next_compat(&compat))) {
score = of_flat_dt_match(dt_root, compat);
if (score > 0 && score < best_score) {
best_data = data;
best_score = score;
}
}
if (!best_data) {
const char *prop;
int size;
pr_err("\n unrecognized device tree list:\n[ ");
prop = of_get_flat_dt_prop(dt_root, "compatible", &size);
if (prop) {
while (size > 0) {
printk("'%s' ", prop);
size -= strlen(prop) + 1;
prop += strlen(prop) + 1;
}
}
printk("]\n\n");
return NULL;
}
pr_info("Machine model: %s\n", of_flat_dt_get_machine_name());
return best_data;
}
of_flat_dt_match_machine的其余部分代码都是出错处理及打印,现在我们看of_flat_dt_match的实现,该函数仅仅是直接调用of_fdt_match而已,不同的是增加了initial_boot_params参数(还记得我们说过前边说过的这个变量的初始化吧,其实这就是内核中的一个简单封装而已)。
int __init of_flat_dt_match(unsigned long node, const char *const *compat)
{
return of_fdt_match(initial_boot_params, node, compat);
}
int of_fdt_match(const void *blob, unsigned long node,
const char *const *compat)
{
unsigned int tmp, score = 0;
if (!compat)
return 0;
while (*compat) {
tmp = of_fdt_is_compatible(blob, node, *compat);
if (tmp && (score == 0 || (tmp < score)))
score = tmp;
compat++;
}
return score;
}
这个函数就是遍历compat数组的每一个字符串,然后通过of_fdt_is_compatible函数计算匹配度(以最小的数值作为最终的结果)。代码到这个地方已经很好理解了,compat中的数据来自内核的.arch.info.init段,这个段表示内核支持的平台,blob是设备树起始地址,通过node节点指定根节点的compatible属性,然后计算匹配度。还记得我们前边说过的compatible属性包含多个字符串,从前向后范围越来越大,优先匹配前边的,这个地方代码计算分数(score变量)就是这个目的。
继续看一下of_fdt_is_compatible()函数:
int of_fdt_is_compatible(const void *blob,
unsigned long node, const char *compat)
{
const char *cp;
int cplen;
unsigned long l, score = 0;
cp = fdt_getprop(blob, node, "compatible", &cplen);
if (cp == NULL)
return 0;
while (cplen > 0) {
score++;
if (of_compat_cmp(cp, compat, strlen(compat)) == 0)
return score;
l = strlen(cp) + 1;
cp += l;
cplen -= l;
}
return 0;
}
这个函数就是比较compatible属性值了,等到while中的if条件满足,返回score,即代表找到了最匹配的DTS,再总结一下就是:内核通过"compatible"属性找到对应的平台描述信息,按照范围从小到大尽量匹配范围最小的,如果匹配不到,那么说明内核不支持该平台,系统将在初始化的时候就出错。
那么到这里,我们就整个完成了我们这一小节的主题:根据Device Tree的信息,找到最适合的machine_desc。
2.将DTB转换成节点是device_node的树状结构:
在此之前,我们先了解一下device_node结构体:
struct device_node {
const char *name;----------------------device node name
const char *type;-----------------------对应device_type的属性
phandle phandle;-----------------------对应该节点的phandle属性
const char *full_name; ----------------从“/”开始的,表示该node的full path
struct property *properties;-------------该节点的属性列表
struct property *deadprops; ----------如果需要删除某些属性,kernel并非真的删除,而是挂入到deadprops的列表
struct device_node *parent;------parent、child以及sibling将所有的device node连接起来
struct device_node *child;
struct device_node *sibling;
struct device_node *next; --------通过该指针可以获取相同类型的下一个node
struct device_node *allnext;-------通过该指针可以获取node global list下一个node
struct proc_dir_entry *pde;--------开放到userspace的proc接口信息
struct kref kref;-------------该node的reference count
unsigned long _flags;
void *data;
};
void __init unflatten_device_tree(void)
{
//解析设备树,将所有的设备节点链入全局链表of_allnodes中
__unflatten_device_tree(initial_boot_params,&of_allnodes,early_init_dt_alloc_memory_arch);
//设置内核输出终端,以及遍历“/aliases”节点下的所有的属性,挂入相应链表
of_alias_scan(early_init_dt_alloc_memory_arch);
}
分析以上代码,在unflatten_device_tree()
中,调用函数__unflatten_device_tree()
,参数initial_boot_params
指向Device Tree在内存中的首地址,of_root
在经过该函数处理之后,会指向根节点,early_init_dt_alloc_memory_arch
是一个函数指针,为struct device_node和struct property结构体分配内存的回调函数(callback)。
在__unflatten_device_tree()
函数中,两次调用unflatten_dt_node()
函数,第一次是为了得到Device Tree转换成struct device_node和struct property结构体需要分配的内存大小,这时候可能递归调用自身(如有子node的话)。第二次调用才是具体填充每一个struct device_node和struct property结构体。
static void __unflatten_device_tree(struct boot_param_header*blob,
struct device_node **mynodes,
void *(*dt_alloc)(u64 size, u64 align))
{
unsigned long size;
void *start,*mem;
struct device_node **allnextp= mynodes;
pr_debug(" -> unflatten_device_tree()\n");
if (!blob){
pr_debug("No device tree pointer\n");
return;
}
pr_debug("Unflattening device tree:\n");
pr_debug("magic: %08x\n", be32_to_cpu(blob->magic));
pr_debug("size: %08x\n", be32_to_cpu(blob->totalsize));
pr_debug("version: %08x\n", be32_to_cpu(blob->version));
//检查设备树magic
if (be32_to_cpu(blob->magic)!= OF_DT_HEADER){
pr_err("Invalid device tree blob header\n");
return;
}
//找到设备树的设备节点起始地址
start = ((void*)blob)+ be32_to_cpu(blob->off_dt_struct);
//第一次调用mem传0,allnextpp传NULL,实际上是为了计算整个设备树所要的空间
size = (unsigned long)unflatten_dt_node(blob, 0,&start, NULL, NULL, 0);
size = ALIGN(size, 4);//4字节对齐
pr_debug(" size is %lx, allocating...\n", size);
//调用early_init_dt_alloc_memory_arch函数,为设备树分配内存空间
mem = dt_alloc(size+ 4, __alignof__(struct device_node));
memset(mem, 0, size);
//设备树结束处赋值0xdeadbeef,为了后边检查是否有数据溢出
*(__be32*)(mem+ size) = cpu_to_be32(0xdeadbeef);
pr_debug(" unflattening %p...\n", mem);
//再次获取设备树的设备节点起始地址
start = ((void*)blob)+ be32_to_cpu(blob->off_dt_struct);
//mem为设备树分配的内存空间,allnextp指向全局变量of_allnodes,生成整个设备树
unflatten_dt_node(blob, mem,&start, NULL, &allnextp, 0);
if (be32_to_cpup(start)!= OF_DT_END)
pr_warning("Weird tag at end of tree: %08x\n", be32_to_cpup(start));
if (be32_to_cpup(mem+ size) != 0xdeadbeef)
pr_warning("End of tree marker overwritten: %08x\n",be32_to_cpup(mem+ size));
*allnextp = NULL;
pr_debug(" <- unflatten_device_tree()\n");
}
可以看到主要起作用的函数是 unflatten_dt_node()
,接下来进入这个函数看一下:
static void * unflatten_dt_node(struct boot_param_header*blob,
void *mem,void**p,
struct device_node *dad,
struct device_node ***allnextpp,
unsigned long fpsize)
{
struct device_node *np;
struct property *pp, **prev_pp= NULL;
char *pathp;
u32 tag;
unsigned int l, allocl;
int has_name = 0;
int new_format = 0;
//*p指向设备树的设备块起始地址
tag = be32_to_cpup(*p);
//每个有孩子的设备节点,其tag一定是OF_DT_BEGIN_NODE
if (tag!= OF_DT_BEGIN_NODE){
pr_err("Weird tag at start of node: %x\n", tag);
return mem;
}
*p += 4;//地址+4,跳过tag,这样指向节点的名称或者节点路径名
pathp = *p;//获得节点名或者节点路径名
l = allocl = strlen(pathp)+ 1;//该节点名称的长度
*p = PTR_ALIGN(*p+ l, 4);//地址对齐后,*p指向该节点属性的地址
//如果是节点名则进入,若是节点路径名则(*pathp)== '/'
if ((*pathp)!= '/'){
new_format = 1;
if (fpsize== 0){//fpsize=0
fpsize = 1;
allocl = 2;
l = 1;
*pathp = '\0';
} else{
fpsize += l;//代分配的长度=本节点名称长度+父亲节点绝对路径的长度
allocl = fpsize;
}
}
//分配一个设备节点device_node结构,*mem记录分配了多大空间,最终会累加计算出该设备树总共分配的空间大小
np = unflatten_dt_alloc(&mem, sizeof(struct device_node)+ allocl,__alignof__(struct device_node));
//第一次调用unflatten_dt_node时,allnextpp=NULL
//第二次调用unflatten_dt_node时,allnextpp指向全局变量of_allnodes的地址
if (allnextpp){
char *fn;
//full_name保存完整的节点名,即包括各级父节点的名称
np->full_name= fn = ((char *)np)+ sizeof(*np);
//若new_format=1,表示pathp保存的是节点名,而不是节点路径名,所以需要加上父节点的name
if (new_format){
if (dad && dad->parent){
strcpy(fn, dad->full_name);//把父亲节点绝对路径先拷贝
fn += strlen(fn);
}
*(fn++)= '/';
}
memcpy(fn, pathp, l);//拷贝本节点的名称
//prev_pp指向节点的属性链表
prev_pp = &np->properties;
//当前节点插入全局链表of_allnodes
**allnextpp= np;
*allnextpp = &np->allnext;
//若父亲节点不为空,则设置该节点的parent
if (dad!= NULL) {
np->parent= dad;//指向父亲节点
if (dad->next== NULL)//第一个孩子
dad->child= np;//child指向第一个孩子
else
dad->next->sibling= np;//把np插入next,这样孩子节点形成链表
dad->next= np;
}
kref_init(&np->kref);
}
//分析该节点的属性
while (1){
u32 sz, noff;
char *pname;
//前边已经将*p移到指向节点属性的地址处,取出属性标识
tag = be32_to_cpup(*p);
//空属性,则跳过
if (tag== OF_DT_NOP){
*p += 4;
continue;
}
//tag不是属性则退出,对于有孩子节点退出时为OF_DT_BEGIN_NODE,对于叶子节点退出时为OF_DT_END_NODE
if (tag!= OF_DT_PROP)
break;
//地址加4,跳过tag
*p += 4;
//获得属性值的大小,是以为占多少整形指针计算的
sz = be32_to_cpup(*p);
//获取属性名称在节点的字符串块中的偏移
noff = be32_to_cpup(*p+ 4);
//地址加8,跳过属性值的大小和属性名称在节点的字符串块中的偏移
*p += 8;
//地址对齐后,*P指向属性值所在的地址
if (be32_to_cpu(blob->version)< 0x10)
*p = PTR_ALIGN(*p, sz>= 8 ? 8 : 4);
//从节点的字符串块中noff偏移处,得到该属性的name
pname = of_fdt_get_string(blob, noff);
if (pname== NULL) {
pr_info("Can't find property name in list !\n");
break;
}
//如果有名称为name的属性,表示变量has_name为1
if (strcmp(pname,"name") == 0)
has_name = 1;
//计算该属性name的大小
l = strlen(pname)+ 1;
//为该属性分配一个属性结构,即struct property,
//*mem记录分配了多大空间,最终会累加计算出该设备树总共分配的空间大小
pp = unflatten_dt_alloc(&mem, sizeof(structproperty),__alignof__(structproperty));
//第一次调用unflatten_dt_node时,allnextpp=NULL
//第二次调用unflatten_dt_node时,allnextpp指向全局变量of_allnodes的地址
if (allnextpp){
if ((strcmp(pname,"phandle") == 0)|| (strcmp(pname,"linux,phandle")== 0)){
if (np->phandle== 0)
np->phandle= be32_to_cpup((__be32*)*p);
}
if (strcmp(pname,"ibm,phandle")== 0)
np->phandle= be32_to_cpup((__be32*)*p);
pp->name= pname;//属性名
pp->length= sz;//属性值长度
pp->value= *p;//属性值
//属性插入该节点的属性链表np->properties
*prev_pp = pp;
prev_pp = &pp->next;
}
*p = PTR_ALIGN((*p)+ sz, 4);//指向下一个属性
}
//至此遍历完该节点的所有属性
//如果该节点没有"name"的属性,则为该节点生成一个name属性,插入该节点的属性链表
if (!has_name){
char *p1 = pathp, *ps= pathp, *pa = NULL;
int sz;
while (*p1){
if ((*p1)== '@')
pa = p1;
if ((*p1)== '/')
ps = p1 + 1;
p1++;
}
if (pa< ps)
pa = p1;
sz = (pa- ps) + 1;
pp = unflatten_dt_alloc(&mem, sizeof(structproperty) + sz,__alignof__(structproperty));
if (allnextpp){
pp->name= "name";
pp->length= sz;
pp->value= pp + 1;
*prev_pp = pp;
prev_pp = &pp->next;
memcpy(pp->value, ps, sz- 1);
((char*)pp->value)[sz- 1] = 0;
pr_debug("fixed up name for %s -> %s\n", pathp,(char*)pp->value);
}
}
//若设置了allnextpp指针
if (allnextpp){
*prev_pp = NULL;
//设置节点的名称
np->name= of_get_property(np,"name", NULL);
//设置该节点对应的设备类型
np->type= of_get_property(np,"device_type",NULL);
if (!np->name)
np->name= "<NULL>";
if (!np->type)
np->type= "<NULL>";
}
//前边在遍历属性时,tag不是属性则退出
//对于有孩子节点退出时tag为OF_DT_BEGIN_NODE,对于叶子节点退出时tag为OF_DT_END_NODE
while (tag== OF_DT_BEGIN_NODE|| tag == OF_DT_NOP){
//空属性则指向下个属性
if (tag== OF_DT_NOP)
*p += 4;
else
//OF_DT_BEGIN_NODE则表明其还有子节点,所以递归分析其子节点
mem = unflatten_dt_node(blob, mem, p, np, allnextpp,fpsize);
tag = be32_to_cpup(*p);
}
//对于叶子节点或者分析完成
if (tag!= OF_DT_END_NODE){
pr_err("Weird tag at end of node: %x\n", tag);
return mem;
}
*p += 4;
//mem返回整个设备树所分配的内存大小,即设备树占的内存空间
return mem;
}
至此已经将DTB转换成节点是device_node的树状结构了。
下一篇文章会补充device_node是如何成为注册到各个bus上的device
参考文章:
我眼中的linux设备数系列 :https://www.cnblogs.com/targethero/p/5086085.html
Device Tree(三)代码分析:http://www.wowotech.net/linux_kenrel/dt-code-analysis.html
宋牧春:Linux设备树文件结构与解析深度分析:http://www.360doc.com/content/18/0926/15/60139132_789843621.shtml
linux device tree源代码解析 :https://www.cnblogs.com/sky-heaven/p/6742033.html
Linux DTS(Device Tree Source)设备树详解之二(dts匹配及发挥作用的流程篇):https://blog.csdn.net/radianceblau/article/details/74722395
(DT系列五)Linux kernel 是怎么将 devicetree中的内容生成plateform_device:https://blog.csdn.net/lichengtongxiazai/article/details/38942033