dtb 数据解析
内核启动阶段获得 dtb 位置指针
以 arm64 为例,内核启动如下:
1 2 3 4 5 6 7 8 9 10 11 __HEAD _head: #ifdef CONFIG_EFI add x13, x18, #0x16 b stext #else b stext // branch to kernel start, magic .long 0 // reserved #endif ...
在开启 UEFI 支持时,add x13, x18, #0x16 这个 code 实际上是为了满足 EFI 格式的”MZ”头。如果使用 UEFI 来启动 kernel, 会识别出来并走 UEFI 启动的流程,如果是普通的启动过程如使用 uboot 的 booti 进行引导,那么第一条指令就是一条 dummy 指令,第二条就跳转到 stext 运行了。
x0 寄存器保存的是 dtb 里 blob 块的物理地址,x0 寄存器内容由 uboot 设置,uboot 将 dtb 地址传递给内核,跳转到 stext,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ENTRY(stext) bl preserve_boot_args bl el2_setup // Drop to EL1, w0=cpu_boot_mode adrp x23, __PHYS_OFFSET and x23, x23, MIN_KIMG_ALIGN - 1 // KASLR offset, defaults to 0 bl set_cpu_boot_mode_flag bl __create_page_tables bl __cpu_setup // initialise processor b __primary_switch ENDPROC(stext) ... preserve_boot_args: mov x21, x0 // x21=FDT adr_l x0, boot_args // record the contents of stp x21, x1, [x0] // x0 .. x3 at kernel entry stp x2, x3, [x0, #16] dmb sy // needed before dc ivac with // MMU off add x1, x0, #0x20 // 4 x 8 bytes b __inval_cache_range // tail call ENDPROC(preserve_boot_args)
arm64 linux 寄存器规定如下:
x0 = physical address of device tree blob (dtb) in system RAM.
x1 = 0 (reserved for future use)
x2 = 0 (reserved for future use)
x3 = 0 (reserved for future use)
将 dtb 的物理地址保存在 x21 寄存器中。 __inval_cache_range 这个函数用来 invalidate 指定区域的 cache。如果指定内存区域有跨越 cacheline, 那么对两边跨越了 cacheline 的地址使用的 clean + invalidate, 对于中间区域可以直接 invalidate 不用写回内存,从而加快 invalidate 速度。
调用__primary_switch 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 __primary_switch: bl __enable_mmu #ifdef CONFIG_RELOCATABLE bl __relocate_kernel #ifdef CONFIG_RANDOMIZE_BASE ldr x8, =__primary_switched adrp x0, __PHYS_OFFSET blr x8 ... bl __relocate_kernel #endif ldr x8, =__primary_switched adrp x0, __PHYS_OFFSET br x8 ENDPROC(__primary_switch)
调用__primary_switched 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 __primary_switched: adrp x4, init_thread_union add sp, x4, #THREAD_SIZE msr sp_el0, x4 // Save thread_info adr_l x8, vectors // load VBAR_EL1 with virtual msr vbar_el1, x8 // vector table address isb stp xzr, x30, [sp, #-16]! mov x29, sp str_l x21, __fdt_pointer, x5 // Save FDT pointer ... b start_kernel ENDPROC(__primary_switched)
通过 str_l x21, __fdt_pointer, x5 这句将 dtb 的地址保存在__fdt_pointer 指针里,以便后续内核可以找到 dtb 并解析设备树文件。
关于 linux 启动过程可以参考下图:原图
dtb 解析过程
start_kernel 阶段会处理 dtb 文件生成 device_node 结构体以及他们之间的组织关系。整体的调用逻辑如下:原图
下面以一个简单的设备树为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 / { leds { act { gpios = <&gpio 42 GPIO_ACTIVE_HIGH>; }; pwr { label = "PWR" ; gpios = <&expgpio 2 GPIO_ACTIVE_LOW>; default -state = "keep" ; linux,default -trigger = "default-on" ; }; }; wifi_pwrseq: wifi-pwrseq { compatible = "mmc-pwrseq-simple" ; reset-gpios = <&expgpio 1 GPIO_ACTIVE_LOW>; }; };
该设备树经过 populate_node() 解析处理后,结构体关系如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 - /节点: - parent:NULL - sibling:NULL - child:wifi_pwrseq - led 节点: - parent:/ - sibling:NULL - child:pwr - act 节点: - parent:led - sibling:NULL - child:NULL - pwr 节点: - parent:led - sibling:act - child:NULL - wifi_pwrseq 节点: - parent:/ - sibling:led - child:NULL
device_node 与 device 绑定
kernel 会为设备树 root 节点下所有带'compatible' 属性的节点都分配并注册一个 platform_device;另外,如果某节点的'compatible' 符合某些 matches 条件,则会为该节点下所有带'compatible' 属性的子节点(child)也分配并注册一个 platform_device。 整体调用流程如下图所示:原图
至此,为所有设备树中所有符合条件的 node 都创建了 platform_device 结构体,node 下描述的资源也解析到了 platform_device 中,并通过 dev 成员将该 node 描述的设备加入了统一设备模型。
补充:标签是标记节点的方法,可以用唯一的名称来标识节点,在 dt 编译器编译过程中,dt 编译器将该名称转换为唯一的 32 位值,之后可以使用标签引用节点,因为标签对于节点是唯一的。phandle 是与节点相关联的 32 位值,用于唯一标识该节点,以便可以从另一个节点属性引用该节点。为了在查找节点时不遍历整个树,引入别名的概念,在 dt 中,别名可以看作是节点的快速查找表,即一个节点的索引
参考文献
https://zhuanlan.zhihu.com/p/141623370
https://zhuanlan.zhihu.com/p/143092868
http://gngshn.github.io/
http://www.wowotech.net/sort/device_model
《Linux 设备驱动开发》