1. Device Tree简介
Linus Torvalds在2011年3月17日的ARM Linux邮件列表宣称“this whole ARM thing is a fucking pain in the ass”,引发ARM Linux社区的地震,随后ARM社区进行了一系列的重大修正。在过去的ARM Linux中,arch/arm/plat-xxx和arch/arm/mach-xxx中充斥着大量的垃圾代码,相当多数的代码只是在描述板级细节,而这些板级细节对于内核来讲,不过是垃圾,如板上的platform设备、resource、i2c_board_info、spi_board_info以及各种硬件的platform_data。 社区必须改变这种局面,于是PowerPC等其他体系架构下已经使用的Flattened Device Tree(FDT)进入ARM社区的视野。Device Tree是一种描述硬件的数据结构,它起源于OpenFirmware(OF)。在Linux2.6中,ARM架构的板极硬件细节过多地被硬编码在arch/arm/plat-xxx和arch/arm/mach-xxx,采用Device Tree后,许多硬件的细节可以直接透过它传递给Linux,而不再需要在kernel中进行大量的冗余编码。
Device Tree由一系列被命名的结点(node)和属性(property)组成,而结点本身可包含子结点。所谓属性,其实就是成对出现的name和value。在Device Tree中,可描述的信息包括(原先这些信息大多被hard code到kernel中):
CPU的数量和类别
内存基地址和大小
总线和桥
外设连接
中断控制器和中断使用情况
GPIO控制器和GPIO使用情况
它基本上就是画一棵电路板上CPU、总线、设备组成的树,Bootloader会将这棵树传递给内核,然后内核可以识别这棵树,并根据它展开出Linux内核中的platform_device、i2c_client、spi_device等设备。这些设备用到的内存、IRQ等资源,也被传递给了kernel,kernel会将这些资源绑定给展开的相应的设备。
2. Device Tree编译
Device Tree文件的格式为dts,包含的头文件格式为dtsi,dts文件是一种人可以看懂的编码格式。但是uboot和linux不能直接识别,他们只能识别二进制文件,所以需要把dts文件编译成dtb文件。dtb文件是一种可以被kernel和uboot识别的二进制文件。把dts编译成dtb文件的工具是dtc。Linux源码目录下scripts/dtc目录包含dtc工具的源码。在Linux的scripts/dtc目录下除了提供dtc工具外,也可以自己安装dtc工具,linux下执行:sudo apt-get install device-tree-compiler安装dtc工具。其中还提供了一个fdtdump的工具,可以反编译dtb文件。dts和dtb文件的转换如图1所示。
dtc工具的使用方法是:dtc –I dts –O dtb –o xxx.dtb xxx.dts,即可生成dts文件对应的dtb文件了。
3. Device Tree头信息
fdtdump工具使用,Linux终端执行ftddump –h,输出以下信息:
fdtdump -h
Usage: fdtdump [options]
Options: -[dshV]
-d, –debug Dump debug information while decoding the file
-s, –scan Scan for an embedded fdt in file
-h, –help Print this help and exit
-V, –version Print version and exit
本文采用s5pv21_smc.dtb文件为例说明fdtdump工具的使用。Linux终端执行fdtdump –sd s5pv21_smc.dtb > s5pv21_smc.txt,打开s5pv21_smc.txt文件,部分输出信息如下所示:
// magic: 0xd00dfeed
// totalsize: 0xce4 (3300)
// off_dt_struct: 0x38
// off_dt_strings: 0xc34
// off_mem_rsvmap: 0x28
// version: 17
// last_comp_version: 16
// boot_cpuid_phys: 0x0
// size_dt_strings: 0xb0
// size_dt_struct: 0xbfc
以上信息便是Device Tree文件头信息,存储在dtb文件的开头部分。在Linux内核中使用struct fdt_header结构体描述。struct fdt_header结构体定义在scripts\dtc\libfdt\fdt.h文件中。
struct fdt_header {
fdt32_t magic; /* magic word FDT_MAGIC */
fdt32_t totalsize; /* total size of DT block */
fdt32_t off_dt_struct; /* offset to structure */
fdt32_t off_dt_strings; /* offset to strings */
fdt32_t off_mem_rsvmap; /* offset to memory reserve map */
fdt32_t version; /* format version */
fdt32_t last_comp_version; /* last compatible version */
/* version 2 fields below */
fdt32_t boot_cpuid_phys; /* Which physical CPU id we're booting on */
/* version 3 fields below */
fdt32_t size_dt_strings; /* size of the strings block */
/* version 17 fields below */
fdt32_t size_dt_struct; /* size of the structure block */
};
fdtdump工具的输出信息即是以上结构中每一个成员的值,struct fdt_header结构体包含了Device Tree的私有信息。例如: fdt_header.magic是fdt的魔数,固定值为0xd00dfeed,fdt_header.totalsize是fdt文件的大小。使用二进制工具打开s5pv21_smc.dtb验证。s5pv21_smc.dtb二进制文件头信息如图2所示。从图2中可以得到Device Tree的文件是以大端模式储存。并且,头部信息和fdtdump的输出信息一致。
Device Tree中的节点信息举例如图3所示。
上述.dts文件并没有什么真实的用途,但它基本表征了一个Device Tree源文件的结构。1个root结点”/”;root结点下面含一系列子结点,本例中为”node@0”和”node@1”;结点”node@0”下又含有一系列子结点,本例中为”child-node@0”;各结点都有一系列属性。这些属性可能为空,如” an-empty-property”;可能为字符串,如”a-string-property”;可能为字符串数组,如”a-string-list-property”;可能为Cells(由u32整数组成),如”second-child-property”,可能为二进制数,如”a-byte-data-property”。Device Tree源文件的结构分为header、fill_area、dt_struct及dt_string四个区域。header为头信息,fill_area为填充区域,填充数字0,dt_struct存储节点数值及名称相关信息,dt_string存储属性名。例如:a-string-property就存储在dt_string区,”A string”及node1就存储在dt_struct区域。
我们可以给一个设备节点添加lable,之后可以通过&lable的形式访问这个lable,这种引用是通过phandle(pointer handle)进行的。例如,图3中的node1就是一个lable,node@0的子节点child-node@0通过&node1引用node@1节点。像是这种phandle的节点,在经过DTC工具编译之后,&node1会变成一个特殊的整型数字n,假设n值为1,那么在node@1节点下自动生成两个属性,属性如下:
linux,phandle = <0x00000001>;
phandle = <0x00000001>;
node@0的子节点child-node@0中的a-reference-to-something = <&node1>会变成a-reference-to-something = < 0x00000001>。此处0x00000001就是一个phandle得值,每一个phandle都有一个独一无二的整型值,在后续kernel中通过这个特殊的数字间接找到引用的节点。通过查看fdtdump输出信息以及dtb二进制文件信息,得到struct fdt_header和文件结构之间的关系信息如所示。
4. Device Tree文件结构
通过以上分析,可以得到Device Tree文件结构如图5所示。dtb的头部首先存放的是fdt_header的结构体信息,接着是填充区域,填充大小为off_dt_struct – sizeof(struct fdt_header),填充的值为0。接着就是struct fdt_property结构体的相关信息。最后是dt_string部分。
Device Tree源文件的结构分为header、fill_area、dt_struct及dt_string四个区域。fill_area区域填充数值0。节点(node)信息使用struct fdt_node_header结构体描述。属性信息使用struct fdt_property结构体描述。各个结构体信息如下:
struct fdt_node_header {
fdt32_t tag;
char name[0];
};
struct fdt_property {
fdt32_t tag;
fdt32_t len;
fdt32_t nameoff;
char data[0];
};
struct fdt_node_header描述节点信息,tag是标识node的起始结束等信息的标志位,name指向node名称的首地址。tag的取值如下:
#define FDT_BEGIN_NODE 0x1 /* Start node: full name */
#define FDT_END_NODE 0x2 /* End node */
#define FDT_PROP 0x3 /* Property: name off, size, content */
#define FDT_NOP 0x4 /* nop */
#define FDT_END 0x9
FDT_BEGIN_NODE和FDT_END_NODE标识node节点的起始和结束,FDT_PROP标识node节点下面的属性起始符,FDT_END标识Device Tree的结束标识符。因此,对于每个node节点的tag标识符一般为FDT_BEGIN_NODE,对于每个node节点下面的属性的tag标识符一般是FDT_PROP。
描述属性采用struct fdt_property描述,tag标识是属性,取值为FDT_PROP;len为属性值的长度(包括‘\0’,单位:字节);nameoff为属性名称存储位置相对于off_dt_strings的偏移地址。
例如:compatible = “samsung,goni”, “samsung,s5pv210”;compatible是属性名称,”samsung,goni”, “samsung,s5pv210”是属性值。compatible属性名称字符串存放的区域是dt_string。”samsung,goni”, “samsung,s5pv210”存放的位置是fdt_property.data后面。因此fdt_property.data指向该属性值。fdt_property.tag的值为属性标识,len为属性值的长度(包括‘\0’,单位:字节),此处len = 29。nameoff为compatible字符串的位置相对于off_dt_strings的偏移地址,即&compatible = nameoff + off_dt_strings。
dt_struct在Device Tree中的结构如图6所示。节点的嵌套也带来tag标识符的嵌套。
5. kernel解析Device Tree
Device Tree文件结构描述就以上struct fdt_header、struct fdt_node_header及struct fdt_property三个结构体描述。kernel会根据Device Tree的结构解析出kernel能够使用的struct property结构体。kernel根据Device Tree中所有的属性解析出数据填充struct property结构体。struct property结构体描述如下:
struct property {
char *name; /* property full name */
int length; /* property value length */
void *value; /* property value */
struct property *next; /* next property under the same node */
unsigned long _flags;
unsigned int unique_id;
struct bin_attribute attr; /* 属性文件,与sysfs文件系统挂接 */
};
总的来说,kernel根据Device Tree的文件结构信息转换成struct property结构体,并将同一个node节点下面的所有属性通过property.next指针进行链接,形成一个单链表。
kernel中究竟是如何解析Device Tree的呢?下面分析函数解析过程。函数调用过程如图7所示。kernel的C语言阶段的入口函数是init/main.c/stsrt_kernel()函数,在early_init_dt_scan_nodes()中会做以下三件事:
扫描/chosen或者/chose@0节点下面的bootargs属性值到boot_command_line,此外,还处理initrd相关的property,并保存在initrd_start和initrd_end这两个全局变量中;
扫描根节点下面,获取{size,address}-cells信息,并保存在dt_root_size_cells和dt_root_addr_cells全局变量中;
扫描具有device_type =“memory”属性的/memory或者/memory@0节点下面的reg属性值,并把相关信息保存在meminfo中,全局变量meminfo保存了系统内存相关的信息。
Device Tree中的每一个node节点经过kernel处理都会生成一个struct device_node的结构体,struct device_node最终一般会被挂接到具体的struct device结构体。struct device_node结构体描述如下:
struct device_node {
const char *name; /* node的名称,取最后一次“/”和“@”之间子串 */
const char *type; /* device_type的属性名称,没有为<NULL> */
phandle phandle; /* phandle属性值 */
const char *full_name; /* 指向该结构体结束的位置,存放node的路径全名,例如:/chosen */
struct fwnode_handle fwnode;
struct property *properties; /* 指向该节点下的第一个属性,其他属性与该属性链表相接 */
struct property *deadprops; /* removed properties */
struct device_node *parent; /* 父节点 */
struct device_node *child; /* 子节点 */
struct device_node *sibling; /* 姊妹节点,与自己同等级的node */
struct kobject kobj; /* sysfs文件系统目录体现 */
unsigned long _flags; /* 当前node状态标志位,见/include/linux/of.h line124-127 */
void *data;
};
/* flag descriptions (need to be visible even when !CONFIG_OF) */
#define OF_DYNAMIC 1 /* node and properties were allocated via kmalloc */
#define OF_DETACHED 2 /* node has been detached from the device tree*/
#define OF_POPULATED 3 /* device already created for the node */
#define OF_POPULATED_BUS 4 /* of_platform_populate recursed to children of this node */
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.c
的of_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 = 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 小结
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
。
Linux设备树解析_smcdef的博客-CSDN博客_linux 设备树解析