U-Boot flat device tree(fdt)

在Linux系统中,设备树Device Tree的概念已经广泛使用了,启动相关代码和驱动代码中随处可见设备树的身影。U-Boot中也引入了设备树的内容,通过设备树信息在运行时进行动态配置,使得U-Boot对板级信息的适配和描述更加灵活,一个单一的U-Boot二进制能够支持多个板子。U-Boot中使用flat device tree(fdt)扁平设备树,前面的文章中已经分析过,在代码中U-Boot通过gd->fdt_blob来指向内存中的设备树信息,阅读本文前,请思考

  • U-Boot加载设备树文件的形式是什么?是只有一种方式还是有多种方式?它们各自是什么原理?

  • 在编译阶段,如何告诉U-Boot使用哪个设备树文件?U-Boot对设备树文件做了什么?

指定设备树文件

在U-Boot中,有3种方式向U-Boot指定设备树文件,其中前三种是在编译时指定,后一种是在运行时指定

  • CONFIG_DEFAULT_DEVICE_TREE

  • make DEVICE_TREE

  • make EXT_DTB

  • env fdtcontroladdr

通过CONFIG_DEFAULT_DEVICE_TREE宏定义直接指定设备树源文件dts路径,该选项位于板级配置文件中,在configs文件夹下可以看到很多板子的设置

1
grep "CONFIG_DEFAULT_DEVICE_TREE" * -Rn

注意这里输入的文件不带后缀.dts,并且文件必须放在arch/\/dts文件夹下

2、3两种方式是通过make命令跟参数的形式,make DEVICE_TREE和CONFIG_DEFAULT_DEVICE_TREE宏定义是一个道理,都是指定设备树源文件,且路径都是确定在arch//dts中,因此make DEVICE_TREE=xxx.dts直接传入文件名,而不需要文件路径。而make EXT_DTB是直接指定一个用户已经单独编译好的dtb文件,这个文件是需要加路径的

这3种方式的处理都可以在U-Boot的dts/Makefile中看到

1
2
3
4
5
6
7
8
9
10
DEVICE_TREE ?= $(CONFIG_DEFAULT_DEVICE_TREE:"%"=%)
ifeq ($(DEVICE_TREE),)
DEVICE_TREE := unset
endif

ifneq ($(EXT_DTB),)
DTB := $(EXT_DTB)
else
DTB := arch/$(ARCH)/dts/$(DEVICE_TREE).dtb
endif

可以看到,DEVICE_TREE用CONFIG_DEFAULT_DEVICE_TREE赋值是使用了”?=”,如果make命令没有指定DEVICE_TREE的话,就是用宏定义的值,如果命令指定了,就用命令中的,也就是说make DEVICE_TREE的优先级高于CONFIG_DEFAULT_DEVICE_TREE宏定义

如果make命令指定了EXT_DTB,就直接使用EXT_DTB指定的dtb,否则将DEVICE_TREE指定的dts后缀替换为dtb,就是Makefile的DTB目标,因此EXT_DTB的优先级又高于DEVICE_TREE

最后一种指定方式是通过设置环境变量”fdtcontroladdr”来指定一个内存中的地址作为设备树的地址,这里的地址格式必须是16进制的,这个选项一般不使用,调试过程为了灵活性会使用这种方式。因为是运行时指定,因此这种方式优先级是最高的

加载设备树的形式

U-Boot加载设备树有很灵活的形式,要使用fdt功能,首先需要在配置中设置CONFIG_OF_CONTROL选项,该选项是一个使能选项,使能后U-Boot才支持fdt功能。在使能CONFIG_OF_CONTROL的前提下,U-Boot可以通过编译选项的方式指定设备树的不同加载形式

  • CONFIG_OF_EMBED:嵌入方式,设备树文件将编译到U-Boot镜像内部

  • CONFIG_OF_SEPARATE:设备树被编译为u-boot.dtb文件,并以cat的形式与U-Boot镜像合并

  • CONFIG_OF_BOARD:使用板级特定的代码来加载设备树文件,把加载工作交给用户处理,用户需要实现board_fdt_blob_setup函数

嵌入U-Boot镜像

在U-Boot顶层Makefile中,顶层Makefile目标是all,all依赖$(ALL-y),而$(ALL-y)又依赖u-boot.srec u-boot.bin u-boot.sym System.map u-boot.cfg binary_size_check这些目标,其中最主要的是U-Boot镜像”u-boot.bin”

1
2
3
all:  $(ALL-y)

ALL-y += u-boot.srec u-boot.bin u-boot.sym System.map u-boot.cfg binary_size_check

u-boot.bin的生成规则与CONFIG_OF_SEPARATE有关,如果定义了CONFIG_OF_SEPARATE,则u-boot.bin依赖u-boot-nodtb.bin和dts/dt.dtb,否则只依赖u-boot-nodtb.bin

1
2
3
4
5
6
7
8
9
10
ifeq ($(CONFIG_OF_SEPARATE),y)
u-boot-dtb.bin: u-boot-nodtb.bin dts/dt.dtb FORCE
$(call if_changed,cat)

u-boot.bin: u-boot-dtb.bin FORCE
$(call if_changed,copy)
else
u-boot.bin: u-boot-nodtb.bin FORCE
$(call if_changed,copy)
endif

而u-boot-nodtb.bin依赖于u-boot,u-boot又依赖于$(u-boot-init)$(u-boot-main)和u-boot.lds,u-boot-main依赖于$(libs-y)

1
2
3
4
5
6
7
8
9
10
u-boot-nodtb.bin: u-boot FORCE
$(call if_changed,objcopy)
$(call DO_STATIC_RELA,$<,$@,$(CONFIG_SYS_TEXT_BASE))
$(BOARD_SIZE_CHECK)

u-boot: $(u-boot-init) $(u-boot-main) u-boot.lds FORCE
$(call if_changed,u-boot__)

u-boot-init := $(head-y)
u-boot-main := $(libs-y)

如果定义了CONFIG_OF_EMBED,会将dts路径添加到$(libs-y)

1
libs-$(CONFIG_OF_EMBED) += dts/

在dts/Makefiles中,定义了第二目标为dt.dtb.S汇编文件,然后如果定义了嵌入设备树选项,会把dt.dtb.o添加到$(obj-y)

1
2
3
.SECONDARY: $(obj)/dt.dtb.S

obj-$(CONFIG_OF_EMBED) := dt.dtb.o

dt.dtb.S的生成是在scripts/Makefile.lib中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Generate an assembly file to wrap the output of the device tree compiler
quiet_cmd_dt_S_dtb= DTB $@
# Modified for U-Boot
cmd_dt_S_dtb= \
( \
echo '.section .dtb.init.rodata,"a"'; \
echo '.balign 16'; \
echo '.global __dtb_$(subst -,_,$(*F))_begin'; \
echo '__dtb_$(subst -,_,$(*F))_begin:'; \
echo '.incbin "$<" '; \
echo '__dtb_$(subst -,_,$(*F))_end:'; \
echo '.global __dtb_$(subst -,_,$(*F))_end'; \
echo '.balign 16'; \
) > $@

$(obj)/%.dtb.S: $(obj)/%.dtb
$(call cmd,dt_S_dtb)

这里定义了一个cmd,即依赖dtb生成dtb.S,选择一个支持CONFIG_OF_EMBED选项的board进行配置并编译,会在dts目录下生成dt.dtb.S汇编文件,该文件内容如下

1
2
3
4
5
6
7
8
.section .dtb.init.rodata,"a"
.balign 16
.global __dtb_dt_begin
__dtb_dt_begin:
.incbin "dts/dt.dtb"
__dtb_dt_end:
.global __dtb_dt_end
.balign 16

声明了一个只读的数据段”.dtb.init.rodata”,前后进行了对齐,其内容就是dt.dtb文件,前后用__dtb_dt_begin__dtb_dt_end标签标记,方便程序中进行索引

该汇编文件会在编译时被编译生成dt.dtb.o然后链接到u-boot-nodtb.bin中。因此,嵌入方式本质上是通过一个汇编文件声明数据段的方式将外部文件链接到U-Boot镜像中

与U-Boot拼接

由以上分析可知,如果定义了CONFIG_OF_SEPARATE,u-boot.bin依赖u-boot-nodtb.bin和dts/dt.dtb,而dts/dt.dtb依赖u-boot,且会进入dtbs目录执行Makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
ifeq ($(CONFIG_OF_SEPARATE),y)
u-boot-dtb.bin: u-boot-nodtb.bin dts/dt.dtb FORCE
$(call if_changed,cat)

u-boot.bin: u-boot-dtb.bin FORCE
$(call if_changed,copy)
else
u-boot.bin: u-boot-nodtb.bin FORCE
$(call if_changed,copy)
endif

dts/dt.dtb: checkdtc u-boot
$(Q)$(MAKE) $(build)=dts dtbs

dtbs目录下的Makefile负责生成dtb文件,然后u-boot-dtb.bin是由u-boot-nodtb.bin和dts/dt.dtb通过cat来生成的,命令cat的定义如下

1
2
quiet_cmd_cat = CAT     $@
cmd_cat = cat $(filter-out $(PHONY), $^) > $@

本质上就是以下命令

1
cat u-boot-dtb.bin dts/dt.dtb > u-boot-dtb.bin

可以看到实质上是进行了文件内容拼接操作

运行时加载设备树

运行时加载设备树文件的操作在board_f.c文件中的init_sequence_f数组中的fdtdec_setup,该函数定义在lib/fdtdec.c中。通过代码可以看出,首先是在init_sequence_f数组定义的位置,如果未定义CONFIG_OF_EMBED,是不会编译fdtdec_setup函数的。在fdtdec_setup函数内部,如果判断是嵌入形式的设备树,则指针直接指向__dtb_dt_begin,如果是拼接方式,则指向__bss_end,是U-Boot镜像尾部,也就是设备树头部

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int fdtdec_setup(void)
{
#if CONFIG_IS_ENABLED(OF_CONTROL)
# ifdef CONFIG_OF_EMBED
/* Get a pointer to the FDT */
gd->fdt_blob = __dtb_dt_begin;
# elif defined CONFIG_OF_SEPARATE
# ifdef CONFIG_SPL_BUILD
/* FDT is at end of BSS unless it is in a different memory region */
if (IS_ENABLED(CONFIG_SPL_SEPARATE_BSS))
gd->fdt_blob = (ulong *)&_image_binary_end;
else
gd->fdt_blob = (ulong *)&__bss_end;
# else
/* FDT is at end of image */
gd->fdt_blob = (ulong *)&_end;
# endif
# elif defined(CONFIG_OF_HOSTFILE)
if (sandbox_read_fdt_from_file()) {
puts("Failed to read control FDT\n");
return -1;
}
# endif
# ifndef CONFIG_SPL_BUILD
/* Allow the early environment to override the fdt address */
gd->fdt_blob = (void *)getenv_ulong("fdtcontroladdr", 16,
(uintptr_t)gd->fdt_blob);
# endif
#endif
return fdtdec_prepare_fdt();
}