setup_arch() // arch/arm64/kernel/setup.c
--> setup_machine_fdt(__fdt_pointer); 检查DTB是否正确,以及早期配置系统
--> unflatten_device_tree();对DTB进行解析,填充设备树结构下内核针对设备树定义的struct device_node类型对象
初始化。
static void __init setup_machine_fdt(phys_addr_t dt_phys)
{
void *dt_virt = fixmap_remap_fdt(dt_phys);// A
const char *name;
if (!dt_virt || !early_init_dt_scan(dt_virt)) {// B
pr_crit("\n"
"Error: invalid device tree blob at physical address %pa (virtual address 0x%p)\n"
"The dtb must be 8-byte aligned and must not exceed 2 MB in size\n"
"\nPlease check your bootloader.",
&dt_phys, dt_virt);
while (true)
cpu_relax();
}
name = of_flat_dt_get_machine_name();// C
if (!name)
return;
pr_info("Machine model: %s\n", name);// D
dump_stack_set_arch_desc("%s (DT)", name);
}
此函数分为4个点来解析即ABCD,首先需要注意,此函数是ARM64实现的。即位于文件
arch/arm64/kernel/setup.c 中。
函数参数dt_phys 是DTB位于内存中的物理地址。对于此时运行内核的CPU来说,其已经开启MMU,
使用虚拟地址阶段。
此时内核把dt_phys物理地址转换为虚拟地址。并且针对这块物理内存映射一个虚拟地址范围。
void *__init fixmap_remap_fdt(phys_addr_t dt_phys)
{
void *dt_virt;
int size;
dt_virt = __fixmap_remap_fdt(dt_phys, &size, PAGE_KERNEL_RO);
if (!dt_virt)
return NULL;
memblock_reserve(dt_phys, size);
return dt_virt;
}
函数把以dt_phys为起始地址,大小为DTB,页面属性设置为PAGE_KERNEL_RO进行映射。
然后把这块内存给reserve起来。这样在后续的内存分配中就不会使用DTB占用的内存。当然最后
会通过memblock_free()把内存释放。
再来看下这个内核针对DTB占用的内存是如何映射的。
arch/arm64/mm/mmu.c
void *__init __fixmap_remap_fdt(phys_addr_t dt_phys, int *size, pgprot_t prot)
{
const u64 dt_virt_base = __fix_to_virt(FIX_FDT);// 给DTB找个虚拟地址,这是虚拟地址base
int offset;
void *dt_virt;
/*
* Check whether the physical FDT address is set and meets the minimum
* alignment requirement. Since we are relying on MIN_FDT_ALIGN to be
* at least 8 bytes so that we can always access the magic and size
* fields of the FDT header after mapping the first chunk, double check
* here if that is indeed the case.
*/
BUILD_BUG_ON(MIN_FDT_ALIGN < 8);
if (!dt_phys || dt_phys % MIN_FDT_ALIGN) //参数检查,必须MIN_FDT_ALIGN大小对齐
return NULL;
/*
* Make sure that the FDT region can be mapped without the need to
* allocate additional translation table pages, so that it is safe
* to call create_mapping_noalloc() this early.
*
* On 64k pages, the FDT will be mapped using PTEs, so we need to
* be in the same PMD as the rest of the fixmap.
* On 4k pages, we'll use section mappings for the FDT so we only
* have to be in the same PUD.
*/
BUILD_BUG_ON(dt_virt_base % SZ_2M);//必须2MB对齐
BUILD_BUG_ON(__fix_to_virt(FIX_FDT_END) >> SWAPPER_TABLE_SHIFT !=
__fix_to_virt(FIX_BTMAP_BEGIN) >> SWAPPER_TABLE_SHIFT);
offset = dt_phys % SWAPPER_BLOCK_SIZE;//对齐后的偏移量
dt_virt = (void *)dt_virt_base + offset;//偏移后,真正的地址
/* map the first chunk so we can read the size from the header */
create_mapping_noalloc(round_down(dt_phys, SWAPPER_BLOCK_SIZE),
dt_virt_base, SWAPPER_BLOCK_SIZE, prot);
需要注意上面这个映射,根据提供的物理地址和虚拟地址进行设置,并且设置页表属性
if (fdt_magic(dt_virt) != FDT_MAGIC) //映射后,我们再来检查一下指定偏移后是否是指定数据
return NULL;// 如果上面是DTB的魔幻数字,说明映射没有问题,否则返回NULL
*size = fdt_totalsize(dt_virt);//这个是获取DTB的大小。这个直接取自DTB头中成员值大小
if (*size > MAX_FDT_SIZE) //需要注意,当前支持的DTB大小不能超过2MB
return NULL;
if (offset + *size > SWAPPER_BLOCK_SIZE)// 如果大于上面我们映射的SWAPPER_BLOCK_SIZE大小,则再映射一下
create_mapping_noalloc(round_down(dt_phys, SWAPPER_BLOCK_SIZE), dt_virt_base,
round_up(offset + *size, SWAPPER_BLOCK_SIZE), prot);
return dt_virt;
}
至此,函数__fixmap_remap_fdt完成映射,注意这是个固定映射,即给出虚拟地址范围,然后映射物理地址范围。
在函数fixmap_remap_fdt()运行完后,DTB对应的物理地址被映射了一段虚拟地址,并且这块内存被reserve
起来。
再回到函数 setup_machine_fdt中,在映射完物理内存后,成员dt_virt表示物理内存地址对应的虚拟地址。li
接下来进行B阶段处理,此时针对DTB进行早期的scan。
drivers/of/fdt.c
bool __init early_init_dt_scan(void *params)
{
bool status;
status = early_init_dt_verify(params);//主要是对DTB头进行检查。
if (!status)
return false;
early_init_dt_scan_nodes();//针对DTB进行scan。
return true;
}
函数early_init_dt_verify()
bool __init early_init_dt_verify(void *params)
{
if (!params)
return false;
/* check device tree validity */
if (fdt_check_header(params)) // 针对提供的DTB,检查其header是否满足要求
return false;
/* Setup flat device-tree pointer */
initial_boot_params = params;//把DTB基地址放入到initial_boot_params
of_fdt_crc32 = crc32_be(~0, initial_boot_params,
fdt_totalsize(initial_boot_params)); // 计算DTB的CRC校验值,校验值在of_fdt_raw_init使用
return true;
}
函数 early_init_dt_scan_nodes()主要是进行早期的初始化,根据DTB配置,进行早期的初始化。
void __init early_init_dt_scan_nodes(void)
{
int rc = 0;
/* Retrieve various information from the /chosen node */
rc = of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);
if (!rc)
pr_warn("No chosen node found, continuing without\n");
/* Initialize {size,address}-cells info */
of_scan_flat_dt(early_init_dt_scan_root, NULL);
/* Setup memory, calling early_init_dt_add_memory_arch */
of_scan_flat_dt(early_init_dt_scan_memory, NULL);
}
函数of_scan_flat_dt针对每个DTB里面的节点进行扫描,然后调用提供的函数。需要注意回调函数返回0继续扫描
返回非0结束扫描。当然,扫描到最后一个也是必须返回了。此时返回值也是0.
第一个扫描是处理chosen 节点。并且把bootargs属性值拷贝到boot_command_line中。如果config文件定义了
CONFIG_CMDLINE这个宏,即也通过config文件配置命令行,则把配置的命令行参数内容拷贝到
boot_command_line。
函数early_init_dt_scan_chosen最后通过pr_debug打印命令行参数。
值得注意的是,函数early_init_dt_scan_chosen还针对ramdisk进行处理。此时需要内核配置
CONFIG_BLK_DEV_INITRD.
这里主要是查找DTS里面 linux,initrd-start和"linux,initrd-end",需要注意这里配置的是ramdisk的物理地址。
最后把ramdisk的起始地址和大小存放到 phys_initrd_start和phys_initrd_size中。我们将在linux 根文件系统中详细介绍
根文件系统相关内容。
继续看第二个函数early_init_dt_scan_root。
这里获取#size-cells 和 #address-cells 然后设置到 dt_root_size_cells和dt_root_addr_cells中去。
#address-cells表示用几个cell表示地址,#size-cells表示用几个cell表示地址长度,就是表示地址和长度
时需要几个32位数。所以上面给出的注释是初始化 {size,address}-cells信息。
最后扫描函数 提供的回调函数 early_init_dt_scan_memory是针对内存配置进行初始化。
这里主要是调用 memblock_add()函数把整个内存块加入内核中。
函数device_type属性的类型为memory,然后获取linux,usable-memory或者reg属性的值。如下形式:
/*
* Reserve below regions from memory node:
*
* 0x05e0,0000 - 0x05ef,ffff: MCU firmware runtime using
* 0x05f0,1000 - 0x05f0,1fff: Reboot reason
* 0x06df,f000 - 0x06df,ffff: Mailbox message data
* 0x0740,f000 - 0x0740,ffff: MCU firmware section
* 0x21f0,0000 - 0x21ff,ffff: pstore/ramoops buffer
* 0x3e00,0000 - 0x3fff,ffff: OP-TEE
*/
memory@0 {
device_type = "memory";
reg = <0x00000000 0x00000000 0x00000000 0x05e00000>,
<0x00000000 0x05f00000 0x00000000 0x00001000>,
<0x00000000 0x05f02000 0x00000000 0x00efd000>,
<0x00000000 0x06e00000 0x00000000 0x0060f000>,
<0x00000000 0x07410000 0x00000000 0x1aaf0000>,
<0x00000000 0x22000000 0x00000000 0x1c000000>;
};
当然如果配置了hotpluggable属性,也会设置memory的hotplug性质。
再次回到函数early_init_dt_scan()中,在进行完上面处理后,早期的基本设置初始化完毕。
函数of_flat_dt_get_machine_name()获取root节点的model或者compatible字段。
const char * __init of_flat_dt_get_machine_name(void)
{
const char *name;
unsigned long dt_root = of_get_flat_dt_root();
name = of_get_flat_dt_prop(dt_root, "model", NULL);
if (!name)
name = of_get_flat_dt_prop(dt_root, "compatible", NULL);
return name;
}
在model没有设置情况下,则了解compatible属性内容。一般如下形式:
/ {
model = "HiKey Development Board";
compatible = "hisilicon,hi6220-hikey", "hisilicon,hi6220";
最后函数打印平台相关信息。
pr_info("Machine model: %s\n", name);
通过上面处理可以看到,在对DTB进行处理时,早期需要针对DTB扫描几次,然后每次针对不同情况处理。
那么如果每个情况都这样进行,每次都扫描,这样性能会非常低,所以,函数unflatten_device_tree()会统一展开
设备树,然后针对每个节点初始化,即初始化一个struct device_node对象关联节点。后面我们需要关注
struct device_node类型对象和struct device以及struct device_driver关系。