Linux 驱动之设备树

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 设备驱动开发》