本节内容主要为,对设备树中运行时配置信息的处理。
设备树主要是做一些信息的传递,简单的说就是将信息从设备树中读出出来,再赋给内核中的某个变量。
下面是之前写的设备树文件。
// SPDX-License-Identifier: GPL-2.0
/*
* SAMSUNG SMDK2440 board device tree source
*
* Copyright (c) 2018 weidongshan@qq.com
* dtc -I dtb -O dts -o jz2440.dts jz2440.dtb
*/
#define S3C2410_GPA(_nr) ((0<<16) + (_nr))
#define S3C2410_GPB(_nr) ((1<<16) + (_nr))
#define S3C2410_GPC(_nr) ((2<<16) + (_nr))
#define S3C2410_GPD(_nr) ((3<<16) + (_nr))
#define S3C2410_GPE(_nr) ((4<<16) + (_nr))
#define S3C2410_GPF(_nr) ((5<<16) + (_nr))
#define S3C2410_GPG(_nr) ((6<<16) + (_nr))
#define S3C2410_GPH(_nr) ((7<<16) + (_nr))
#define S3C2410_GPJ(_nr) ((8<<16) + (_nr))
#define S3C2410_GPK(_nr) ((9<<16) + (_nr))
#define S3C2410_GPL(_nr) ((10<<16) + (_nr))
#define S3C2410_GPM(_nr) ((11<<16) + (_nr))
/dts-v1/;
/memreserve/ 0x33f00000 0x100000;
/ {
model = "SMDK24440";
compatible = "samsung,smdk2440";
#address-cells = <1>;
#size-cells = <1>;
memory { /* /memory */
device_type = "memory";
reg = <0x30000000 0x4000000 0 4096>;
};
/*
cpus {
cpu {
compatible = "arm,arm926ej-s";
};
};
*/
chosen {
bootargs = "noinitrd root=/dev/mtdblock4 rw init=/linuxrc console=ttySAC0,115200";
};
led {
compatible = "jz2440_led";
pin = <S3C2410_GPF(5)>;
};
};
根节点下面设置了一个chosen子节点,chosen子节点里有一个属性叫bootargs,这个就是内核启动时的命令行参数。
根节点下面还有memory子节点,memory子节点则是将内存的起始地址和大小告诉内核。起始地址和大小都保存在memory节点的reg属性中。
问:起始地址和大小,分别用几个32bit的数值来说明呢?
答:这就要看根节点下的#address-cells和#size-cells的大小,设为1表示1个32bit的数值,2表示2个32bit的数值,以此类推。所以,在解析memory节点之前,要先解析#address-cells和#size-cells这两个属性。
显然,上面的dts文件中,内存的起始地址和大小分别为1个32bit的数值,共两块内存,第一块内存的起始地址和大小分别为0x30000000和0x4000000,第二块内存的起始地址和大小分别为0和4096。
下面来分析代码。
从之前分析得出,head.S首先会调用init路径下的main.c文件中的start_kernel函数,然后在start_kernel函数中调用setup_arch函数,在setup_arch函数中再调用setup_machine_fdt函数,使用dtb文件来设置板子。
函数调用过程:start_kernel->setup_arch->setup_machine_fdt
通过of_flat_dt_match_machine函数,选择最合适的machine_desc结构体之后,会调用early_init_dt_scan_nodes函数,函数名意为早期的设备树扫描节点。
early_init_dt_scan_nodes的内容如下,根据注释可以知道,分别是处理chosen节点中的bootargs属性,#address-cells和#size-cells,memory。
将bootargs属性的值赋给boot_command_line变量,他是一个char类型的数组。
这三个处理过程都是调用了同一个函数,of_scan_flat_dt,只不过是传入的值不同。
查看of_scan_flat_dt函数,it是一个函数指针,data主要是用来配合it使用的。
根据注释,这个函数的主要作用是扫描dtb文件,提取内存信息的。
处理过程如下:
所以,对于of_scan_flat_dt函数,我们主要关心it传入的callback函数即可。
early_init_dt_scan_chosen函数的分析如下,最终就是将chosen节点的bootargs属性值赋给boot_command_line变量。
再看early_init_dt_scan_root函数,它主要是处理根节点下的#address-cells和#size-cells,分析过程如下图所示,处理的过程比较简单,就是取值后赋值。需要注意的是,dtb文件中是按大端字节序存储的数值信息,要先通过be32_to_cpup转换成小端字节序再赋值。
另外,dt_root_size_cells和dt_root_addr_cells的默认值都是1,表示由1个32bit表示。
最后是对memory的处理,分析如下:
从dtb中读出内存信息后,调用early_init_dt_add_memory_arch函数,在early_init_dt_add_memory_arch函数中再调用memblock_add,这个函数就是用来添加内存块的。
最后,总结下函数调用过程如下:
start_kernel // init/main.c
setup_arch(&command_line); // arch/arm/kernel/setup.c
mdesc = setup_machine_fdt(__atags_pointer); // arch/arm/kernel/devtree.c
early_init_dt_scan_nodes(); // drivers/of/ftd.c
/* Retrieve various information from the /chosen node */
of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);
/* 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);
三个函数的作用如下:
a. 解析得到根节点下的chosen节点中bootargs属性的值, 存入全局变量: boot_command_line
b. 确定根节点的这两个属性的值: #address-cells, #size-cells
存入全局变量: dt_root_addr_cells, dt_root_size_cells
c. 解析根节点下memory节点中的reg属性, 提取出"base, size", 最终调用memblock_add(base, size)添加内存块;