五 根节点
一个最简单的设备树必须包含根节点,cpus节点,memory节点。根节点的名字及全路径都是“/”,至少需要包含model和compatible两个属性。model属性我们在属性那节已经说过是用来描述产品型号的,类型为字符串,推荐的格式为“manufacturer,model-number”(非强制的)。根节点的model属性描述的是板子的型号或者芯片平台的型号,如:
model = "Atmel AT91SAM9G20 family SoC"
model = "Samsung SMDK5420 board based on EXYNOS5420"
从软件的层面讲model属性仅仅表示一个名字而已,没有更多的作用。compatible属性则不同,该属性决定软件如何匹配硬件对硬件进行初始化。属性那一节我们说过compatible属性的类型是字符串数组,按照范围从小到大的顺序排列,每个字符串表示一种匹配类型。根节点的compatible属性表示平台如何匹配,比如‘compatible = "samsung,smdk5420", "samsung,exynos5420", "samsung,exynos5"’,表示软件应该首先匹配'samsung,smdk5420',这个是一款开发板。如果无法匹配,再试着匹配"samsung,exynos5420",这个是一款芯片平台。如果还是无法匹配,还可以试着匹配 "samsung,exynos5",这是一个系列的芯片平台。这里说的匹配是指软件根据该信息找到对应的代码,如对应的初始化函数。
根节点表示的是整个板子或者芯片平台,所以在系统初始化比较早的时候就需要确认是什么平台,怎样初始化。对于Linux,是通过在start_kernel函数调用setup_arch函数实现的。不同的架构,setup_arch函数的实现不同,对于arm架构,setup_arch函数源代码位于arch/arm/kernel/setup.c中。以下是该函数的部分源代码(代码来自内核版本4.4-rc7的官方版本,本节后边所有代码都来自该版本)。
935 void __init setup_arch(char **cmdline_p) 936 { 937 const struct machine_desc *mdesc; 938 939 setup_processor(); 940 mdesc = setup_machine_fdt(__atags_pointer); 941 if (!mdesc) 942 mdesc = setup_machine_tags(__atags_pointer, __machine_arch_type); 943 machine_desc = mdesc; 944 machine_name = mdesc->name;
第940行setup_machine_fdt函数的输入是设备树(DTB)首地址,返回的mdesc是描述平台信息的结构体。还记得我们在概述那节说过启动程序如uboot把设备树读到内存中,然后在启动内核的同时将设备树首地址传给内核,此处__atags_pointer就是启动程序传给内核的设备树地址(此时内存中的设备树已经是DTB形式)。setup_machine_fdt中的fdt是flat device tree的缩写,fdt的意思是说设备树在内存中是在一块连续地址存储的,fdt和dtb说的都是同一个东西。setup_machine_tags是在设备树初始化失败的时候才调用的,所以不用管他。machine_desc和machine_name都是静态全局变量,用来保存指针方便后边引用的。为了更好的理解setup_machine_fdt具体实现了什么功能,我们首先看下machine_desc结构体。不同的架构,该结构体定义差别很大,arm架构源代码位于arch/arm/include/asm/mach/arch.h,复制如下:
27 struct machine_desc { 28 unsigned int nr; /* architecture number */ 29 const char *name; /* architecture name */ 30 unsigned long atag_offset; /* tagged list (relative) */ 31 const char *const *dt_compat; /* array of device tree 32 * 'compatible' strings */ 33 34 unsigned int nr_irqs; /* number of IRQs */ 35 36 #ifdef CONFIG_ZONE_DMA 37 phys_addr_t dma_zone_size; /* size of DMA-able area */ 38 #endif 39 40 unsigned int video_start; /* start of video RAM */ 41 unsigned int video_end; /* end of video RAM */ 42 43 unsigned char reserve_lp0 :1; /* never has lp0 */ 44 unsigned char reserve_lp1 :1; /* never has lp1 */ 45 unsigned char reserve_lp2 :1; /* never has lp2 */ 46 enum reboot_mode reboot_mode; /* default restart mode */ 47 unsigned l2c_aux_val; /* L2 cache aux value */ 48 unsigned l2c_aux_mask; /* L2 cache aux mask */ 49 void (*l2c_write_sec)(unsigned long, unsigned); 50 const struct smp_operations *smp; /* SMP operations */ 51 bool (*smp_init)(void); 52 void (*fixup)(struct tag *, char **); 53 void (*dt_fixup)(void); 54 long long (*pv_fixup)(void); 55 void (*reserve)(void);/* reserve mem blocks */ 56 void (*map_io)(void);/* IO mapping function */ 57 void (*init_early)(void); 58 void (*init_irq)(void); 59 void (*init_time)(void); 60 void (*init_machine)(void); 61 void (*init_late)(void); 62 #ifdef CONFIG_MULTI_IRQ_HANDLER 63 void (*handle_irq)(struct pt_regs *); 64 #endif 65 void (*restart)(enum reboot_mode, const char *); 66 };
从以上结构体的注释可以看出,该结构体包含了非常多的信息。注意第31行的dt_compat变量,该变量就是用来匹配设备树的compatible属性的。
setup_machine_fdt函数的实现也是架构相关的,arm架构源代码位于arch/arm/kernel/devtree.c,复制代码如下:
203 /** 204 * setup_machine_fdt - Machine setup when an dtb was passed to the kernel 205 * @dt_phys: physical address of dt blob 206 * 207 * If a dtb was passed to the kernel in r2, then use it to choose the 208 * correct machine_desc and to setup the system. 209 */ 210 const struct machine_desc * __init setup_machine_fdt(unsigned int dt_phys) 211 { 212 const struct machine_desc *mdesc, *mdesc_best = NULL; 213 214 #ifdef CONFIG_ARCH_MULTIPLATFORM 215 DT_MACHINE_START(GENERIC_DT, "Generic DT based system") 216 MACHINE_END 217 218 mdesc_best = &__mach_desc_GENERIC_DT; 219 #endif 220 221 if (!dt_phys || !early_init_dt_verify(phys_to_virt(dt_phys))) 222 return NULL; 223 224 mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach); 225 226 if (!mdesc) { 227 const char *prop; 228 int size; 229 unsigned long dt_root; 230 231 early_print(" Error: unrecognized/unsupported " 232 "device tree compatible list: [ "); 233 234 dt_root = of_get_flat_dt_root(); 235 prop = of_get_flat_dt_prop(dt_root, "compatible", &size); 236 while (size > 0) { 237 early_print("'%s' ", prop); 238 size -= strlen(prop) + 1; 239 prop += strlen(prop) + 1; 240 } 241 early_print("] "); 242 243 dump_machine_table(); /* does not return */ 244 } 245 246 /* We really don't want to do this, but sometimes firmware provides buggy data */ 247 if (mdesc->dt_fixup) 248 mdesc->dt_fixup(); 249 250 early_init_dt_scan_nodes(); 251 252 /* Change machine number to match the mdesc we're using */ 253 __machine_arch_type = mdesc->nr; 254 255 return mdesc; 256 }
第221行检查fdt指针是否为空并且调用early_init_dt_verify函数,该函数代码位于drivers/of/fdt.c,这个函数算是of模块(还记得么?是open firmware的缩写)的第一个函数,复制代码如下:
1060 1061 bool __init early_init_dt_verify(void *params) 1062 { 1063 if (!params) 1064 return false; 1065 1066 /* check device tree validity */ 1067 if (fdt_check_header(params)) 1068 return false; 1069 1070 /* Setup flat device-tree pointer */ 1071 initial_boot_params = params; 1072 of_fdt_crc32 = crc32_be(~0, initial_boot_params, 1073 fdt_totalsize(initial_boot_params)); 1074 return true; 1075 }
early_init_dt_verify首先检查fdt头部的合法性,然后设置fdt全局变量以及计算crc。这个initial_boot_params变量后边在访问设备树的时候还会用到。继续看前边第224行,of_flat_dt_match_machine函数算是of模块的第二个函数吧,在分析这个函数前,我们首先分析这个函数的第二个参数arch_get_next_mach,这是一个函数指针,arm架构的实现位于arch/arm/kernel/devtree.c,复制代码如下:
190 static const void * __init arch_get_next_mach(const char *const **match) 191 { 192 static const struct machine_desc *mdesc = __arch_info_begin; 193 const struct machine_desc *m = mdesc; 194 195 if (m >= __arch_info_end) 196 return NULL; 197 198 mdesc++; 199 *match = m->dt_compat; 200 return m; 201 }
这个函数很简单,注意的是mdesc是静态局部变量,第一次调用指向__arch_info_begin,后边每次调用都mdesc++,如果超过了__arch_info_end就返回NULL。以上代码说明在__arch_info_begin和__arch_info_end两个地址之间存储着多个machine_desc变量(也可能是一个),该函数遍历这些变量,通过match参数返回所有machine_desc结构体的dt_compat变量指针。问题是__arch_info_begin和__arch_info_end地址是怎么来的呢?在arch/arm/kernel/vmlinux.lds.S连接脚本中定义了.arch.info.init段,__arch_info_begin和__arch_info_end地址分别是该段的首尾地址。
188 .init.arch.info : { 189 __arch_info_begin = .; 190 *(.arch.info.init) 191 __arch_info_end = .; 192 }
那么.init.arch.info段的内容怎么来的呢?这就要参考DT_MACHINE_START和MACHINE_END宏了,arm架构的定义在arch/arm/include/asm/mach/arch.h文件,如下所示:
94 #define DT_MACHINE_START(_name, _namestr)
95 static const struct machine_desc __mach_desc_##_name
96 __used
97 __attribute__((__section__(".arch.info.init"))) = {
98 .nr = ~0,
99 .name = _namestr,
100
101 #endif
从该宏代码看出他定义了一个machine_desc类型的静态局部变量,该变量位于.arch.info.init段中。参考arch/arm/mach-exynos/exynos.c中如下代码,以下代码在.arch.info.init段定义了一个名字为__mach_desc_EXYNOS_DT,类型为machine_desc的静态局部变量,并且该变量的dt_compat字符串矩阵中有"samsung,exynos5420"的字符串。
277 static char const *const exynos_dt_compat[] __initconst = {
278 "samsung,exynos3",
279 "samsung,exynos3250",
280 "samsung,exynos4",
281 "samsung,exynos4210",
282 "samsung,exynos4212",
283 "samsung,exynos4412",
284 "samsung,exynos4415",
285 "samsung,exynos5",
286 "samsung,exynos5250",
287 "samsung,exynos5260",
288 "samsung,exynos5420",
289 "samsung,exynos5440",
290 NULL
291 };
319 DT_MACHINE_START(EXYNOS_DT, "SAMSUNG EXYNOS (Flattened Device Tree)")
320 /* Maintainer: Thomas Abraham <thomas.abraham@linaro.org> */
321 /* Maintainer: Kukjin Kim <kgene.kim@samsung.com> */
322 .l2c_aux_val = 0x3c400001,
323 .l2c_aux_mask = 0xc20fffff,
324 .smp = smp_ops(exynos_smp_ops),
325 .map_io = exynos_init_io,
326 .init_early = exynos_firmware_init,
327 .init_irq = exynos_init_irq,
328 .init_machine = exynos_dt_machine_init,
329 .init_late = exynos_init_late,
330 .dt_compat = exynos_dt_compat,
331 .reserve = exynos_reserve,
332 .dt_fixup = exynos_dt_fixup,
333 MACHINE_END
我们已经知道了get_next_compat指针的具体实现了,现在继续看of_flat_dt_match_machine。从第732行开始的循环就是遍历.arch.info.init段中所有的dt_compat变量,然后通过of_flat_dt_match计算一个分数,并且寻找那个分数最小的。
713 /**
714 * of_flat_dt_match_machine - Iterate match tables to find matching machine.
715 *
716 * @default_match: A machine specific ptr to return in case of no match.
717 * @get_next_compat: callback function to return next compatible match table.
718 *
719 * Iterate through machine match tables to find the best match for the machine
720 * compatible string in the FDT.
721 */
722 const void * __init of_flat_dt_match_machine(const void *default_match,
723 const void * (*get_next_compat)(const char * const**))
724 {
725 const void *data = NULL;
726 const void *best_data = default_match;
727 const char *const *compat;
728 unsigned long dt_root;
729 unsigned int best_score = ~1, score = 0;
730
731 dt_root = of_get_flat_dt_root();
732 while ((data = get_next_compat(&compat))) {
733 score = of_flat_dt_match(dt_root, compat);
734 if (score > 0 && score < best_score) {
735 best_data = data;
736 best_score = score;
737 }
738 }
....
759 return best_data;
760 }
761
of_flat_dt_match_machine的其余部分代码都是出错处理及打印,现在我们看of_flat_dt_match的实现,该函数仅仅是直接调用of_fdt_match而已,不同的是增加了initial_boot_params参数(还记得我们说过前边说过的这个变量的初始化吧,其实这就是内核中的一个简单封装而已)。
685 /**
686 * of_flat_dt_match - Return true if node matches a list of compatible values
687 */
688 int __init of_flat_dt_match(unsigned long node, const char *const *compat)
689 {
690 return of_fdt_match(initial_boot_params, node, compat);
691 }
of_fdt_match函数从142行开始遍历compat数组的每一个字符串,然后通过of_fdt_is_compatible函数计算匹配度(以最小的数值作为最终的结果)。代码到这个地方已经很好理解了,compat中的数据来自内核的.arch.info.init段,这个段表示内核支持的平台,blob是设备树其实地址,通过node节点指定根节点的compatible属性,然后计算匹配度。还记得我们前边说过的compatible属性包含多个字符串,从前向后范围越来越大,优先匹配前边的,这个地方代码计算分数(score变量)就是这个目的。
131 /**
132 * of_fdt_match - Return true if node matches a list of compatible values
133 */
134 int of_fdt_match(const void *blob, unsigned long node,
135 const char *const *compat)
136 {
137 unsigned int tmp, score = 0;
138
139 if (!compat)
140 return 0;
141
142 while (*compat) {
143 tmp = of_fdt_is_compatible(blob, node, *compat);
144 if (tmp && (score == 0 || (tmp < score)))
145 score = tmp;
146 compat++;
147 }
148
149 return score;
150 }
继续看of_fdt_is_compatible函数的实现,第97行已经看到找该节点下的"compatible"属性了。
80 /**
81 * of_fdt_is_compatible - Return true if given node from the given blob has
82 * compat in its compatible list
83 * @blob: A device tree blob
84 * @node: node to test
85 * @compat: compatible string to compare with compatible list.
86 *
87 * On match, returns a non-zero value with smaller values returned for more
88 * specific compatible values.
89 */
90 int of_fdt_is_compatible(const void *blob,
91 unsigned long node, const char *compat)
92 {
93 const char *cp;
94 int cplen;
95 unsigned long l, score = 0;
96
97 cp = fdt_getprop(blob, node, "compatible", &cplen);
98 if (cp == NULL)
99 return 0;
100 while (cplen > 0) {
101 score++;
102 if (of_compat_cmp(cp, compat, strlen(compat)) == 0)
103 return score;
104 l = strlen(cp) + 1;
105 cp += l;
106 cplen -= l;
107 }
108
109 return 0;
110 }
关于根节点的"compatible"属性我们就说到这,一句话总结下就是内核通过"compatible"属性找到对应的平台描述信息,按照范围从小到大尽量匹配范围最小的,如果匹配不到,那么说明内核不支持该平台,系统将在初始化的时候就出错。
根节点还可能包含的属性为#address-cells和#size-cells,规范中说明这两个属性是必须的,实际应用时是可选的,还记得属性那一节说这两个属性如果没有都是有默认值的,#address-cells默认值为2,#size-cells默认值为1。根节点下必须包含的子节点为cpus和memory,后边会说明cpus下边还有每个cpu的子节点,memory节点下边定义的就是memory的起始地址及大小,所以根节点的#address-cells和#size-cells属性实际上说明的就是从cpu角度看系统总线的地址长度和大小。
规范中还写根节点下边必须有一个epapr-version属性用来描述设备树的版本,实际上在linux中根本不用这个属性。