Uboot如何将设备树传给kernel

在riscv架构下,uboot会将dtb的物理地址放到a1寄存器中。

这点可在arch/riscv/head.S中看到些许端倪。

#ifdef CONFIG_BUILTIN_DTB
la a0, __dtb_start
XIP_FIXUP_OFFSET a0
#else
mv a0, a1
#endif /* CONFIG_BUILTIN_DTB */
/* Set trap vector to spin forever to help debug */
la a3, .Lsecondary_park
csrw CSR_TVEC, a3
call setup_vm

将a1,也就是fdt地址放到a0里。

a0是调用c函数存放第一个变量的寄存器,所以就作为了setup_vm的参数传递到setup_vm中

Linux解析设备树

在Linux中,经过汇编代码会跳到start_kernel函数,start_kernel中调用了setup_arch函数。

该函数在不同架构下有不同的实现,riscv在arch/riscv/setup.c中有实现。

setup_arch

函数实现如下:

void __init setup_arch(char **cmdline_p)
{
parse_dtb();
setup_initial_init_mm(_stext, _etext, _edata, _end);

*cmdline_p = boot_command_line;

early_ioremap_setup();
sbi_init();
jump_label_init();
parse_early_param();

efi_init();
paging_init();

/* Parse the ACPI tables for possible boot-time configuration */
acpi_boot_table_init();

parse_dtb实现如下:

static void __init parse_dtb(void) {
/* Early scan of device tree from init memory */
if (early_init_dt_scan(dtb_early_va, dtb_early_pa)) {
const char *name = of_flat_dt_get_machine_name();

if (name) {
pr_info("Machine model: %s\n", name);
dump_stack_set_arch_desc("%s (DT)", name);
}
} else {
pr_err("No DTB passed to the kernel\n");
}
}

其中用到了dtb_early_va和dtb_early_pa这两个变量。

定义如下:

#define dtb_early_va	_dtb_early_va
#define dtb_early_pa _dtb_early_pa

void *_dtb_early_va __initdata;
uintptr_t _dtb_early_pa __initdata;

这两个变量在setup_vm中被初始化,setup_vm是在head.S中被调用,上文提到过了。

setup_vm

setup_vm中只需要关注与dtb相关的就可以了:

asmlinkage void __init setup_vm(uintptr_t dtb_pa)
{
...

/* Setup early mapping for FDT early scan */
create_fdt_early_page_table(__fix_to_virt(FIX_FDT), dtb_pa);

...
}

TODO 参数1

参数2就是通过a0传过来的设备树地址。

create_fdt_early_page_table

create_fdt_early_page_table函数定义如下:

/*
* Setup a 4MB mapping that encompasses the device tree: for 64-bit kernel,
* this means 2 PMD entries whereas for 32-bit kernel, this is only 1 PGDIR
* entry.
*/
static void __init create_fdt_early_page_table(uintptr_t fix_fdt_va,
uintptr_t dtb_pa)
{
#ifndef CONFIG_BUILTIN_DTB
uintptr_t pa = dtb_pa & ~(PMD_SIZE - 1);

/* Make sure the fdt fixmap address is always aligned on PMD size */
BUILD_BUG_ON(FIX_FDT % (PMD_SIZE / PAGE_SIZE));

/* In 32-bit only, the fdt lies in its own PGD */
if (!IS_ENABLED(CONFIG_64BIT)) {
create_pgd_mapping(early_pg_dir, fix_fdt_va,
pa, MAX_FDT_SIZE, PAGE_KERNEL);
} else {
create_pmd_mapping(fixmap_pmd, fix_fdt_va,
pa, PMD_SIZE, PAGE_KERNEL);
create_pmd_mapping(fixmap_pmd, fix_fdt_va + PMD_SIZE,
pa + PMD_SIZE, PMD_SIZE, PAGE_KERNEL);
}

dtb_early_va = (void *)fix_fdt_va + (dtb_pa & (PMD_SIZE - 1));
#else
/*
* For 64-bit kernel, __va can't be used since it would return a linear
* mapping address whereas dtb_early_va will be used before
* setup_vm_final installs the linear mapping. For 32-bit kernel, as the
* kernel is mapped in the linear mapping, that makes no difference.
*/
dtb_early_va = kernel_mapping_pa_to_va(dtb_pa);
#endif

dtb_early_pa = dtb_pa;
}

create_fdt_early_page_table 这个函数的目的是在启动过程中为设备树(Device Tree Blob, DTB)创建早期的内存页表映射。它在内核初始化阶段进行,确保设备树在内存中的地址被正确映射,以便内核能够访问设备树数据。具体来说,这个函数的作用是在虚拟地址空间中为设备树(DTB)创建一个页表映射,允许内核访问设备树。

如果设备树是编译到内核当中的话,dtb就会作为内核内存一部分被一起映射,所以直接从pa转va就可以了。

并且dtb_early_va和dtb_early_pa都会在这个函数中被设置。

parse_dtb

接着回到parse_dtb函数中:

static void __init parse_dtb(void)
{
/* Early scan of device tree from init memory */
if (early_init_dt_scan(dtb_early_va, dtb_early_pa)) {
const char *name = of_flat_dt_get_machine_name();

if (name) {
pr_info("Machine model: %s\n", name);
dump_stack_set_arch_desc("%s (DT)", name);
}
} else {
pr_err("No DTB passed to the kernel\n");
}
}

首先调用early_init_dt_scan函数,并检查返回值:

early_init_dt_scan

bool __init early_init_dt_scan(void *dt_virt, phys_addr_t dt_phys)
{
bool status;

status = early_init_dt_verify(dt_virt, dt_phys);
if (!status)
return false;

early_init_dt_scan_nodes();
return true;
}

early_init_dt_scan函数首先调用early_init_dt_verify验证设备树是否是linux内核预期的格式。

如果不是的话返回false。

early_init_dt_scan_nodes_root

early_init_dt_verify中还调用了early_init_dt_scan_root函数,该函数获取了根节点的两个信息:

/*
* early_init_dt_scan_root - fetch the top level address and size cells
*/
int __init early_init_dt_scan_root(void)
{
const __be32 *prop;
const void *fdt = initial_boot_params;
int node = fdt_path_offset(fdt, "/");

if (node < 0)
return -ENODEV;

dt_root_size_cells = OF_ROOT_NODE_SIZE_CELLS_DEFAULT;
dt_root_addr_cells = OF_ROOT_NODE_ADDR_CELLS_DEFAULT;

prop = of_get_flat_dt_prop(node, "#size-cells", NULL);
if (!WARN(!prop, "No '#size-cells' in root node\n"))
dt_root_size_cells = be32_to_cpup(prop);
pr_debug("dt_root_size_cells = %x\n", dt_root_size_cells);

prop = of_get_flat_dt_prop(node, "#address-cells", NULL);
if (!WARN(!prop, "No '#address-cells' in root node\n"))
dt_root_addr_cells = be32_to_cpup(prop);
pr_debug("dt_root_addr_cells = %x\n", dt_root_addr_cells);

return 0;
}

early_init_dt_scan_root 这个函数的作用是从设备树的根节点中获取系统的地址单元和大小单元的配置,并将这些信息存储到全局变量 dt_root_size_cellsdt_root_addr_cells 中。设备树(Device Tree,DT)通常描述了硬件的结构和配置信息,#address-cells#size-cells 是设备树根节点中定义的属性,指定了设备树中地址和大小的单元数。

如果获取不到的话,默认为OF_ROOT_NODE_SIZE_CELLS_DEFAULTOF_ROOT_NODE_ADDR_CELLS_DEFAULT

这两个默认值定义如下:

#if defined(CONFIG_SPARC)
#define OF_ROOT_NODE_ADDR_CELLS_DEFAULT 2
#else
#define OF_ROOT_NODE_ADDR_CELLS_DEFAULT 1
#endif

#define OF_ROOT_NODE_SIZE_CELLS_DEFAULT 1

early_init_dt_scan_nodes

如果验证成功的话就调用early_init_dt_scan_nodes, early_init_dt_scan_nodes定义如下:

void __init early_init_dt_scan_nodes(void)
{
int rc;

/* Retrieve various information from the /chosen node */
rc = early_init_dt_scan_chosen(boot_command_line);
if (rc)
pr_warn("No chosen node found, continuing without\n");

/* Setup memory, calling early_init_dt_add_memory_arch */
early_init_dt_scan_memory();

/* Handle linux,usable-memory-range property */
early_init_dt_check_for_usable_mem_range();
}

/chosen 节点获取内核启动参数: 通过 early_init_dt_scan_chosen 获取设备树中的 /chosen 节点信息,特别是启动命令行参数。

初始化内存信息: 通过 early_init_dt_scan_memory 处理内存相关的设备树节点,并将内存信息传递给内核的内存管理系统。

检查可用内存范围: 通过 early_init_dt_check_for_usable_mem_range 检查设备树中是否有 linux,usable-memory-range 属性,并根据这些信息设置可用的内存范围。

TODO chosen解析

TODO 内存解析

TODO 保留内存解析

unflatten_device_tree

接下来回到setup_arch函数,与设备树相关的就是这个函数了。

这个函数才是真正的设备树解析函数。

函数定义如下:

/**
* 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.
*/
void __init unflatten_device_tree(void)
{
void *fdt = initial_boot_params;

/* Save the statically-placed regions in the reserved_mem array */
fdt_scan_reserved_mem_reg_nodes();

/* Populate an empty root node when bootloader doesn't provide one */
if (!fdt) {
fdt = (void *) __dtb_empty_root_begin;
/* fdt_totalsize() will be used for copy size */
if (fdt_totalsize(fdt) >
__dtb_empty_root_end - __dtb_empty_root_begin) {
pr_err("invalid size in dtb_empty_root\n");
return;
}
of_fdt_crc32 = crc32_be(~0, fdt, fdt_totalsize(fdt));
fdt = copy_device_tree(fdt);
}

__unflatten_device_tree(fdt, 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);

unittest_unflatten_overlay_base();
}

其中,fdt_scan_reserved_mem_reg_nodes函数是将设备树保存到保留内存当中,防止被覆盖。

如果fdt是nullptr的话,就创建一个空的fake设备树。

最后调用__unflatten_device_tree函数进行真正的展开。

__unflatten_device_tree

该函数定义如下:

/**
* __unflatten_device_tree - create tree of device_nodes from flat blob
* @blob: The blob to expand
* @dad: Parent device node
* @mynodes: The device_node tree created by the call
* @dt_alloc: An allocator that provides a virtual address to memory
* for the resulting tree
* @detached: if true set OF_DETACHED on @mynodes
*
* 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.
*
* Return: NULL on failure or the memory chunk containing the unflattened
* device tree on success.
*/
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;
int ret;

...
/* First pass, scan for size */
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);

/* Allocate memory for the expanded device tree */
mem = dt_alloc(size + 4, __alignof__(struct device_node));
if (!mem)
return NULL;

...

/* Second pass, do actual unflattening */
ret = unflatten_dt_nodes(blob, mem, dad, mynodes);

...
return mem;
}

__unflatten_device_tee中会调用两次unflatten_dt_nodes,第一次是为了获取大小,第二次是真正的展开。

unflatten_dt_nodes函数定义如下:

unflatten_dt_nodes

/**
* unflatten_dt_nodes - 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
* @dad: Parent struct device_node
* @nodepp: The device_node tree created by the call
*
* Return: The size of unflattened device tree or error code
*/
static int unflatten_dt_nodes(const void *blob,
void *mem,
struct device_node *dad,
struct device_node **nodepp)
{
struct device_node *root;
int offset = 0, depth = 0, initial_depth = 0;
#define FDT_MAX_DEPTH 64
struct device_node *nps[FDT_MAX_DEPTH];
void *base = mem;
bool dryrun = !base;
int ret;

...

for (offset = 0;
offset >= 0 && depth >= initial_depth;
offset = fdt_next_node(blob, offset, &depth)) {
if (WARN_ON_ONCE(depth >= FDT_MAX_DEPTH - 1))
continue;

if (!IS_ENABLED(CONFIG_OF_KOBJ) &&
!of_fdt_device_is_available(blob, offset))
continue;

ret = populate_node(blob, offset, &mem, nps[depth],
&nps[depth+1], dryrun);
if (ret < 0)
return ret;

if (!dryrun && nodepp && !*nodepp)
*nodepp = nps[depth+1];
if (!dryrun && !root)
root = nps[depth+1];
}

...

return mem - base;
}

用一个for循环遍历每一个node,fdt_next_node的实现就跟我们前面分析的dtb结构一样,寻找每个structure的开始的标签,也就是0x01,找到的话检查这个structure是否合法,如果合法的话就返回这个structure的offset。

随后的populate_node才是将设备树节点转换成内核中的对象。

populate_node
static int populate_node(const void *blob,
int offset,
void **mem,
struct device_node *dad,
struct device_node **pnp,
bool dryrun)
{
struct device_node *np;
const char *pathp;
int len;

pathp = fdt_get_name(blob, offset, &len);
if (!pathp) {
*pnp = NULL;
return len;
}

len++;

np = unflatten_dt_alloc(mem, sizeof(struct device_node) + len,
__alignof__(struct device_node));
if (!dryrun) {
char *fn;
of_node_init(np);
np->full_name = fn = ((char *)np) + sizeof(*np);

memcpy(fn, pathp, len);

if (dad != NULL) {
np->parent = dad;
np->sibling = dad->child;
dad->child = np;
}
}

populate_properties(blob, offset, mem, np, pathp, dryrun);
if (!dryrun) {
np->name = of_get_property(np, "name", NULL);
if (!np->name)
np->name = "<NULL>";
}

*pnp = np;
return 0;
}

populate_node通过fdt_get_name获取该node的名字, 原理就是找到struct的偏移地址,加上offset,然后+4字节(跳过tag,也就是开始信号),然后剩下的长度就是node的full-name。

申请内存的时候多申请full-name的长度+1,这样可以把full-name拷贝到结构体的最后,也就是不属于结构体的范围了,但还是能根据这个结构体+sizeof(结构体)找到该字符串指针。

最后通过populate_properties函数遍历该structure的属性,并填充到np里面,也就是device_node。

device_node

struct device_node {
const char *name;
phandle 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;
#if defined(CONFIG_OF_KOBJ)
struct kobject kobj;
#endif
unsigned long _flags;
void *data;
#if defined(CONFIG_SPARC)
unsigned int unique_id;
struct of_irq_controller *irq_trans;
#endif
};

这个device_node就是我们平常使用的driver.dev->of_node。

Ref

link1