[What]linux -> kernel解析设备树

参考文档: /Documentation/devicetree/booting-without-of.txt , Documentation/arm/Booting

了解内核如何解析设备树对于写驱动至关重要。

bootloader 将设备树传递给kernel

bootloader 到 kernel

对于arm来讲,其参数传递的状态具体说明位于 /arch/arm/kernel/head.S :

/*
 * Kernel startup entry point.
 * ---------------------------
 *
 * This is normally called from the decompressor code.  The requirements
 * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,
 * r1 = machine nr, r2 = atags or dtb pointer.
 *
 * This code is mostly position independent, so if you link the kernel at
 * 0xc0008000, you call this at __pa(0xc0008000).
 *
 * See linux/arch/arm/tools/mach-types for the complete list of machine
 * numbers for r1.
 *
 * We're trying to keep crap to a minimum; DO NOT add any machine specific
 * crap here - that's what the boot loader (or in extreme, well justified
 * circumstances, zImage) is for.
 */

对于uboot对应的代码位于 /arch/arm/lib/bootm.c :

static void boot_jump_linux(bootm_headers_t *images, int flag)
{

#ifdef CONFIG_ARM64
...
#else
//目标id
unsigned long machid = gd->bd->bi_arch_number;
char *s;
//入口函数,分别传入 r0, r1, r2
void (*kernel_entry)(int zero, int arch, uint params);
unsigned long r2;
int fake = (flag & BOOTM_STATE_OS_FAKE_GO);

//得到kernel的入口点地址
kernel_entry = (void (*)(int, int, uint))images->ep;

s = getenv("machid");
if (s) {
if (strict_strtoul(s, 16, &machid) < 0) {
debug("strict_strtoul failed!\n");
return;
}
printf("Using machid 0x%lx from environment\n", machid);
}

debug("## Transferring control to Linux (at address %08lx)" \
"...\n", (ulong) kernel_entry);
bootstage_mark(BOOTSTAGE_ID_RUN_OS);
announce_and_cleanup(fake);

//得到设备树或taglist地址
if (IMAGE_ENABLE_OF_LIBFDT && images->ft_len)
r2 = (unsigned long)images->ft_addr;
else
r2 = gd->bd->bi_boot_params;

if (!fake) {
#ifdef CONFIG_ARMV7_NONSEC
...
#endif
//PC指向此地址并运行,此时便进入了内核
kernel_entry(0, machid, r2);
}
#endif
}

kernel获取到 r0,r1,r2

依然需要分析文件 /arch/arm/kernel/head.S .

此文件前面部分并没有看到明确的对设备树和taglist的区分代码,不过在最后有 #include "head-common.S" ,所以继续往下跟 /arch/arm/kernel/head-common.S

哈哈,一开始就出现了区别二者的宏定义:

#define ATAG_CORE 0x54410001
#define ATAG_CORE_SIZE ((2*4 + 3*4) >> 2)
#define ATAG_CORE_SIZE_EMPTY ((2*4) >> 2)

#ifdef CONFIG_CPU_BIG_ENDIAN
#define OF_DT_MAGIC 0xd00dfeed
#else
#define OF_DT_MAGIC 0xedfe0dd0 /* 0xd00dfeed in big-endian */
#endif

然后是格式标记的判断:

#ifdef CONFIG_OF_FLATTREE
ldr	r6, =OF_DT_MAGIC		@ is it a DTB?
cmp	r5, r6
beq	2f
#endif

到此,原来 head.S 已经调用了此代码 bl __vet_atags .

接下来是 __mmap_switched 功能模块:

/*
 * The following fragment of code is executed with the MMU on in MMU mode,
 * and uses absolute addresses; this is not position independent.
 *
 *  r0  = cp#15 control register
 *  r1  = machine ID
 *  r2  = atags/dtb pointer
 *  r9  = processor ID
 */
  __INIT
__mmap_switched:
  adr	r3, __mmap_switched_data

  ldmia	r3!, {r4, r5, r6, r7}
  cmp	r4, r5				@ Copy data segment if needed
1:	cmpne	r5, r6
  ldrne	fp, [r4], #4
  strne	fp, [r5], #4
  bne	1b

  mov	fp, #0				@ Clear BSS (and zero fp)
1:	cmp	r6, r7
  strcc	fp, [r6],#4
  bcc	1b

 ARM(	ldmia	r3, {r4, r5, r6, r7, sp})
 THUMB(	ldmia	r3, {r4, r5, r6, r7}	)
 THUMB(	ldr	sp, [r3, #16]		)
  str	r9, [r4]			@ Save processor ID
  str	r1, [r5]			@ Save machine type
  str	r2, [r6]			@ Save atags pointer
  cmp	r7, #0
  strne	r0, [r7]			@ Save control register values
  b	start_kernel
ENDPROC(__mmap_switched)

  .align	2
  .type	__mmap_switched_data, %object
__mmap_switched_data:
  .long	__data_loc			     @ r4
  .long	_sdata				       @ r5
  .long	__bss_start			     @ r6
  .long	_end				         @ r7
  .long	processor_id			   @ r4
  .long	__machine_arch_type	 @ r5
  .long	__atags_pointer			 @ r6
#ifdef CONFIG_CPU_CP15
  .long	cr_alignment			   @ r7
#else
  .long	0				             @ r7
#endif
  .long	init_thread_union + THREAD_START_SP @ sp
  .size	__mmap_switched_data, . - __mmap_switched_data

由上可以看出 :

  • machine type 被赋值给了变量 __machine_arch_type
  • atags pointer 被赋值给了变量 __atags_pointer

那么接下来就是着重追踪 __atags_pointer 变量.

解析设备树

通过 grep 变量 __atags_pointer 找到了c的入口点函数 setup_arch 位于 /arch/arm/kernel/setup.c ,下面就来看看它到底是如何解析设备树的。

获取设备树结构

查看函数 setup_arch 中的代码:

const struct machine_desc *mdesc;

setup_processor();
mdesc = setup_machine_fdt(__atags_pointer);
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);

可以知道最终的设备树被结构 struct machine_desc 所描述,而 __atags_pointer 的任务已经完成了。

下面就得看看 struct machine_desc 结构体是如何被填充的:

bool __init early_init_dt_verify(void *params)
{
if (!params)
return false;

/* check device tree validity */
if (fdt_check_header(params))
return false;

/* Setup flat device-tree pointer */
//此处保存了device tree 在虚拟空间中的地址
initial_boot_params = params;
of_fdt_crc32 = crc32_be(~0, initial_boot_params,
fdt_totalsize(initial_boot_params));
return true;
}
/**
* setup_machine_fdt - Machine setup when an dtb was passed to the kernel
* @dt_phys: physical address of dt blob
*
* If a dtb was passed to the kernel in r2, then use it to choose the
* correct machine_desc and to setup the system.
*/
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)))
return NULL;

mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);

if (!mdesc) {
//设备树内容识别错误处理
...
}

/* 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;
}

/**
* of_flat_dt_match_machine - Iterate match tables to find matching machine.
*
* @default_match: A machine specific ptr to return in case of no match.
* @get_next_compat: callback function to return next compatible match table.
*
* Iterate through machine match tables to find the best match for the machine
* compatible string in the FDT.
*/
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();
//获取一个machine描述字符串
while ((data = get_next_compat(&compat))) {
//进行匹配比较,匹配得越直接,score越低
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;
}

//在终端输出设备树中的 "model" 属性值
pr_info("Machine model: %s\n", of_flat_dt_get_machine_name());

return best_data;
}

在machine中查找匹配

获取一个machine的compatible字符串是使用下面的函数:

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

其中 __arch_info_begin__arch_info_end 在链接脚本中定义:

.init.arch.info : {
  __arch_info_begin = .;
  *(.arch.info.init)
  __arch_info_end = .;
}

可以看出,只要将代码限定在 .arch.info.init 段中,就会被遍历到,实现的宏为:

#define DT_MACHINE_START(_name, _namestr)               \
static const struct machine_desc __mach_desc_##_name \
__used \
__attribute__((__section__(".arch.info.init"))) = { \
.nr = ~0, \
.name = _namestr,


#define MACHINE_END \
};

比如zynq中的定义为:

static const char * const zynq_dt_match[] = {
"xlnx,zynq-7000",
NULL
};

DT_MACHINE_START(XILINX_EP107, "Xilinx Zynq Platform")
/* 64KB way size, 8-way associativity, parity disabled */
.l2c_aux_val = 0x00400000,
.l2c_aux_mask = 0xffbfffff,
.smp = smp_ops(zynq_smp_ops),
.map_io = zynq_map_io,
.init_irq = zynq_irq_init,
.init_machine = zynq_init_machine,
.init_late = zynq_init_late,
.init_time = zynq_timer_init,
.dt_compat = zynq_dt_match,
.reserve = zynq_memory_init,
MACHINE_END

于此同时,设备树中的machine compatible 为 :

compatible = "xlnx,zynq-7000";

通过匹配就可以找出对应的 struct machine_desc ,也就可以找到对于当前machine的初始化回调函数了。

获取启动参数

设备树结构中,通过 chosen 节点来传递启动参数已及一些配置命令,关于这些配置参数的详细描述位于文档 /Documentation/kernel-parameters.txt

在函数 setup_arch() 中调用了解析设备树函数 unflatten_device_tree() :

/**
* unflatten_device_tree - create tree of device_nodes from flat blob
*
* unflattens the device-tree passed by the firmware, creating the
* tree of struct device_node. It also fills the "name" and "type"
* pointers of the nodes so the normal device-tree walking functions
* can be used.
*/

/**
* @brief 此函数将设备树以 struct device_node 的形式树形化的表示
*/

void __init unflatten_device_tree(void)
{

__unflatten_device_tree(initial_boot_params, &of_root,
early_init_dt_alloc_memory_arch);

/* Get pointer to "/chosen" and "/aliases" nodes for use everywhere */
of_alias_scan(early_init_dt_alloc_memory_arch);
}

目前首先关注启动参数的获取:

/**
* of_alias_scan - Scan all properties of the 'aliases' node
*
* The function scans all the properties of the 'aliases' node and populates
* the global lookup table with the properties. It returns the
* number of alias properties found, or an error code in case of failure.
*
* @dt_alloc: An allocator that provides a virtual address to memory
* for storing the resulting tree
*/
void of_alias_scan(void * (*dt_alloc)(u64 size, u64 align))
{
struct property *pp;

of_aliases = of_find_node_by_path("/aliases");
of_chosen = of_find_node_by_path("/chosen");
if (of_chosen == NULL)
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 = of_get_property(of_chosen, "stdout-path", NULL);
if (!name)
name = of_get_property(of_chosen, "linux,stdout-path", NULL);
if (IS_ENABLED(CONFIG_PPC) && !name)
name = of_get_property(of_aliases, "stdout", NULL);
if (name)
of_stdout = of_find_node_opts_by_path(name, &of_stdout_options);
}

if (!of_aliases)
return;

...
}

在这里只有标准输入,那比较重要的 bootargs 在哪里呢? 原来还是在之前的 setup_machine_fdt() 函数中调用了 early_init_dt_scan_nodes() 函数:

void __init early_init_dt_scan_nodes(void)
{

/* Retrieve various information from the /chosen node */
//获取chosen中的bootargs设置,保存于boot_command_line中
//另外跟进函数也会发现其内部还会获取 linux,initrd-start,linux,initrd-end 设置
of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);

/* Initialize {size,address}-cells info */
//得到 #size-cells,#address-cells 并分别保存于dt_root_size_cells,dt_root_addr_cells 变量
//用于后面扫描内存节点信息
of_scan_flat_dt(early_init_dt_scan_root, NULL);

/* Setup memory, calling early_init_dt_add_memory_arch */
//获取memory节点信息,底层调用 memblock_add_range 将内存加入代码控制区
of_scan_flat_dt(early_init_dt_scan_memory, NULL);
}

映射 device_node

在函数 unflatten_device_tree() 中调用了函数 __unflatten_device_tree() :

void __init unflatten_device_tree(void)
{

//此处的 initial_boot_params 就是在最开始被赋值的设备树的虚拟地址
//而 of_root 就是设备树的根节点了
__unflatten_device_tree(initial_boot_params, &of_root,
early_init_dt_alloc_memory_arch);

...
}
/**
* __unflatten_device_tree - create tree of device_nodes from flat blob
*
* unflattens a device-tree, creating the
* tree of struct device_node. It also fills the "name" and "type"
* pointers of the nodes so the normal device-tree walking functions
* can be used.
* @blob: The blob to expand
* @mynodes: The device_node tree created by the call
* @dt_alloc: An allocator that provides a virtual address to memory
* for the resulting tree
*/

static void __unflatten_device_tree(const void *blob,
struct device_node **mynodes,
void * (*dt_alloc)(u64 size, u64 align))
{
unsigned long size;
int start;
void *mem;

//打印调试信息以及检查基本格式
...

/* First pass, scan for size */
start = 0;
size = (unsigned long)unflatten_dt_node(blob, NULL, &start, NULL, NULL, 0, true);
size = ALIGN(size, 4);

pr_debug(" size is %lx, allocating...\n", size);

/* Allocate memory for the expanded device tree */
mem = dt_alloc(size + 4, __alignof__(struct device_node));
memset(mem, 0, size);

*(__be32 *)(mem + size) = cpu_to_be32(0xdeadbeef);

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

/* Second pass, do actual unflattening */
start = 0;
//此处开始正式的解析设备树,并赋值给 mynodes
unflatten_dt_node(blob, mem, &start, NULL, mynodes, 0, false);
if (be32_to_cpup(mem + size) != 0xdeadbeef)
pr_warning("End of tree marker overwritten: %08x\n",
be32_to_cpup(mem + size));

pr_debug(" <- unflatten_device_tree()\n");
}

由此可见,是由 unflatten_dt_node() 来完成了 device_node 的填充,关于此数据结构中各项的意义,需要结合函数一起来看才比较明白:

/**
* unflatten_dt_node - Alloc and populate a device_node from the flat tree
* @blob: The parent device tree blob
* @mem: Memory chunk to use for allocating device nodes and properties
* @poffset: pointer to node in flat tree
* @dad: Parent struct device_node
* @nodepp: The device_node tree created by the call
* @fpsize: Size of the node path up at the current depth.
* @dryrun: If true, do not allocate device nodes but still calculate needed
* memory size
*/

static void * unflatten_dt_node(const void *blob,
void *mem,
int *poffset,
struct device_node *dad,
struct device_node **nodepp,
unsigned long fpsize,
bool dryrun)

{

const __be32 *p;
struct device_node *np;
struct property *pp, **prev_pp = NULL;
const char *pathp;
unsigned int l, allocl;
static int depth;
int old_depth;
int offset;
int has_name = 0;
int new_format = 0;

//获取节点名称
pathp = fdt_get_name(blob, *poffset, &l);
if (!pathp)
return mem;

allocl = ++l;

/* version 0x10 has a more compact unit name here instead of the full
* path. we accumulate the full path size using "fpsize", we'll rebuild
* it later. We detect this because the first character of the name is
* not '/'.
*/

//使用 fpsize 保存路径名称包含的字符数(包括结尾的空字符)
if ((*pathp) != '/') {
new_format = 1;
if (fpsize == 0) {
/* root node: special case. fpsize accounts for path
* plus terminating zero. root node only has '/', so
* fpsize should be 2, but we want to avoid the first
* level nodes to have two '/' so we use fpsize 1 here
*/

fpsize = 1;
allocl = 2;
l = 1;
pathp = "";
} else {
/* account for '/' and path size minus terminal 0
* already in 'l'
*/

fpsize += l;
allocl = fpsize;
}
}

np = unflatten_dt_alloc(&mem, sizeof(struct device_node) + allocl,
__alignof__(struct device_node));
//路径拼接
if (!dryrun) {
char *fn;
of_node_init(np);
np->full_name = fn = ((char *)np) + sizeof(*np);
if (new_format) {
/* 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);

prev_pp = &np->properties;
if (dad != NULL) {
np->parent = dad;
np->sibling = dad->child;
dad->child = np;
}
}
/* process properties */
/* 依次遍历此节点下的属性 */
for (offset = fdt_first_property_offset(blob, *poffset);
(offset >= 0);
(offset = fdt_next_property_offset(blob, offset))) {
const char *pname;
u32 sz;

if (!(p = fdt_getprop_by_offset(blob, offset, &pname, &sz))) {
offset = -FDT_ERR_INTERNAL;
break;
}

if (pname == NULL) {
pr_info("Can't find property name in list !\n");
break;
}
if (strcmp(pname, "name") == 0)
has_name = 1;
pp = unflatten_dt_alloc(&mem, sizeof(struct property),
__alignof__(struct property));
if (!dryrun) {
/* We accept flattened tree phandles either in
* ePAPR-style "phandle" properties, or the
* legacy "linux,phandle" properties. If both
* appear and have different values, things
* will get weird. Don't do that. */

if ((strcmp(pname, "phandle") == 0) ||
(strcmp(pname, "linux,phandle") == 0)) {
if (np->phandle == 0)
np->phandle = be32_to_cpup(p);
}
/* And we process the "ibm,phandle" property
* used in pSeries dynamic device tree
* stuff */

if (strcmp(pname, "ibm,phandle") == 0)
np->phandle = be32_to_cpup(p);
pp->name = (char *)pname;
pp->length = sz;
pp->value = (__be32 *)p;
*prev_pp = pp;
prev_pp = &pp->next;
}
}
/* with version 0x10 we may not have the name property, recreate
* it here from the unit name if absent
*/

if (!has_name) {
const 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(struct property) + sz,
__alignof__(struct property));
if (!dryrun) {
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);
}
}
if (!dryrun) {
*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>";
}

old_depth = depth;
*poffset = fdt_next_node(blob, *poffset, &depth);
if (depth < 0)
depth = 0;
while (*poffset > 0 && depth > old_depth)
mem = unflatten_dt_node(blob, mem, poffset, np, NULL,
fpsize, dryrun);

if (*poffset < 0 && *poffset != -FDT_ERR_NOTFOUND)
pr_err("unflatten: error %d processing FDT\n", *poffset);

/*
* Reverse the child list. Some drivers assumes node order matches .dts
* node order
*/

if (!dryrun && np->child) {
struct device_node *child = np->child;
np->child = NULL;
while (child) {
struct device_node *next = child->sibling;
child->sibling = np->child;
np->child = child;
child = next;
}
}

if (nodepp)
*nodepp = np;

return mem;
}
struct device_node {
const char *name; //节点对应的 "name" 属性值
const char *type; //节点对应的 "device type"属性值
phandle phandle; //节点对应的 "phandle"或"linux,phandle"或"ibm,phandle"
const char *full_name; //从根路径开始的全路径
struct fwnode_handle fwnode; //暂时没搞明白

struct property *properties; //属性列表
struct property *deadprops; /* removed properties */
struct device_node *parent; //父节点
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
};

各个设备子系统分析设备树

到目前为止 of_root 就成为了设备树根节点的起始节点了,至于不同的子系统如何来分析设备树那就需要在对应的子系统下在仔细分析了。

Last Updated 2018-10-14 日 19:32.
Render by hexo-renderer-org with Emacs 26.1 (Org mode 9.1.14)