0%

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