本文关注的主要是内核如何处理DTB文件中记录的设备信息,会分析内核解析DTB文件的主体流程,不会关注所有细节,同时,也不会关注对特定设备信息的处理,比如对有关中断的设备信息的处理,相关内容在做相应模块的笔记时再细究(学习linux的中断管理时会分析内核对DTB中有关中断的部分的处理)。
前文已经说过,DTB文件由u-boot传递给内核,u-boot在跳转到内核时,会把一些关键的信息通过参数(实际使用通用寄存器r0、r1、r2)传递给内核:
machine id
;传递的是设备树时,该参数无用;可见,当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中的信息分为三类:
compatible
、model
属性记录的信息;/chosen
节点和/memory
节点记录的信息;下文将分别介绍内核对这三种信息的处理。
一个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.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();
}
由上文可知,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;
}
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
就介绍完了。
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);
}
要进一步了解其中的细节,我们需要先弄清楚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.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;
}
在解析/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;
}
是时候解析/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, ®);
size = dt_mem_next_cell(dt_root_size_cells, ®);
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;
}
至此,内核对运行时配置信息的处理就介绍完了。
内核会保留DTB所占据的内存区域,因此DTB文件中的数据在kernel启动后也是可用的,相关源码的调用路径如下:
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 | 一个节点的所有属性构成一个链表 |
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成员置为
,这源于:
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属性 */
......
}
......
}
有了上文的铺垫,我们就可以开始从源码的层面分析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
申请并填充之(节点的属性也会在该函数中被构建)。过程很好理解,就不再继续跟踪了。
首先,我们需要知道,在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
属性的子节点。位于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);
}
本节将分析一下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,具体的就不再跟踪了。
上文已经说过,对于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
节,这里就不再多说了。
kernel源码树下的include/linux
目录下存在着一些以of
开头的头文件,这些头文件内是一些与设备树相关的函数的声明。下面将对这些头文件做一个分类。
头文件 | 内容 |
---|---|
of_fdt.h | 声明了dtb文件的相关操作函数,一般用不到,因为dtb文件在内核中被转换为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相关的函数 |
头文件 | 内容 |
---|---|
of_platform.h | 声明了把device_node转换为platform_device时用到的函数 |
of_device.h | 主要声明了struct device相关的函数,如 of_match_device |
所有设备树的信息存放于/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对应的目录。
/proc/device-tree
作为链接文件指向/sys/firmware/devicetree/base
。
[1] linux-kernel-5.4.26源码及文档
[2] 韦东山老师的设备树视频教程