linux内核设备树及编译

1、设备树的概念

在内核源码中,存在大量对板级细节信息描述的代码。这些代码充斥在/arch/arm/plat-xxx和/arch/arm/mach-xxx目录,对内核而言这些platform设备、resource、i2c_board_info、spi_board_info以及各种硬件的platform_data绝大多数纯属垃圾冗余代码。为了解决这一问题,ARM内核版本3.x之后引入了原先在Power PC等其他体系架构已经使用的Flattened Device Tree。

开源文档中对设备树的描述是,一种描述硬件资源的数据结构,它通过bootloader将硬件资源传给内核,使得内核和硬件资源描述相对独立。

Device Tree可以描述的信息包括CPU的数量和类别、内存基地址和大小、总线和桥、外设连接、中断控制器和中断使用情况、GPIO控制器和GPIO使用情况、Clock控制器和Clock使用情况。

另外,设备树对于可热插拔的设备不进行具体描述,它只描述用于控制该热插拔设备的控制器。

设备树的主要优势:对于同一SOC的不同主板,只需更换设备树文件.dtb即可实现不同主板的无差异支持,而无需更换内核文件。

(注:要使得3.x之后的内核支持使用设备树,除了内核编译时需要打开相对应的选项外,bootloader也需要支持将设备树的数据结构传给内核。)

2、设备树的组成和使用

设备树包含DTC(device tree compiler),DTS(device tree source和DTB(device tree blob)。其对应关系如下图所示:

图片[1]-linux内核设备树及编译-趣考网

2.1 DTS和DTSI(源文件)

.dts文件是一种ASCII文本对Device Tree的描述,放置在内核的/arch/arm/boot/dts目录。一般而言,一个.dts文件对应一个ARM的machine。

由于一个SOC可能有多个不同的电路板( .dts文件为板级定义, .dtsi文件为SoC级定义),而每个电路板拥有一个 .dts。这些dts势必会存在许多共同部分,为了减少代码的冗余,设备树将这些共同部分提炼保存在.dtsi文件中,供不同的dts共同使用。.dtsi的使用方法,类似于C语言的头文件,在dts文件中需要进行include .dtsi文件。当然,dtsi本身也支持include 另一个dtsi文件。

2.2 DTC (编译工具)

DTC为编译工具,dtc编译器可以把dts文件编译成为dtb,也可把dtb编译成为dts文件。在3.x内核版本中,DTC的源码位于内核的scripts/dtc目录,内核选中CONFIG_OF,编译内核的时候,主机可执行程序DTC就会被编译出来。 即scripts/dtc/Makefile中

hostprogs-y := dtcalways := $(hostprogs-y) 

在内核的
arch/arm/boot/dts/Makefile中,若选中某种SOC,则与其对应相关的所有dtb文件都将编译出来。在linux下,make dtbs可单独编译dtb。以下截取了TEGRA平台的一部分。

ifeq ($(CONFIG_OF),y)dtb-$(CONFIG_ARCH_TEGRA) += tegra20-harmony.dtb \\tegra30-beaver.dtb \\tegra114-dalmore.dtb \\tegra124-ardbeg.dtb 

在2.6.x版本内核中,只在powerpc架构下使用了设备树,DTC的源码位于内核的arch/powerpc/boot/dtc-src目录,编译内核后,可将DTC编译出来,DTC编译工具位于arch/powerpc/boot目录下。

2.3 DTB (二进制文件)

DTC编译.dts生成的二进制文件(.dtb),bootloader在引导内核时,会预先读取.dtb到内存,进而由内核解析。

在2.6.x版本内核中,在powerpc架构下,dtb文件可以单独进行编译,编译命令格式如下:

dtc [-I input-format] [-O output-format][-o output-filename] [-V output_version] input_filename

参数说明

input-format:- “dtb”: “blob” format- “dts”: “source” format.- “fs” format.output-format:- “dtb”: “blob” format- “dts”: “source” format- “asm”: assembly language fileoutput_version:

定义”blob”的版本,在dtb文件的字段中有表示,支持1 2 3和16,默认是3,在16版本上有许多特性改变

(1) Dts编译生成dtb

./dtc -I dts -O dtb -o B_dtb.dtb A_dts.dts

把A_dts.dts编译生成B_dtb.dtb

(2) Dtb编译生成dts

./dtc -I dtb -O dts -o A_dts.dts A_dtb.dtb

把A_dtb.dtb反编译生成为A_dts.dts

在linux 3.x内核中,可以使用make的方式进行编译。

2.4 Bootloader(boottloader支持)

Bootloader需要将设备树在内存中的地址传给内核。在ARM中通过bootm或bootz命令来进行传递。

bootm [kernel_addr] [initrd_address] [dtb_address],其中kernel_addr为内核镜像的地址,initrd为initrd的地址,dtb_address为dtb所在的地址。若initrd_address为空,则用“-”来代替。

(小编自己整理了一些个人觉得比较好的学习书籍、视频资料有需要的可以私信回复【内核】自行免费领取哦!!

3、linux内核对硬件的描述方式

在以前的内核版本中:

1)内核包含了对硬件的全部描述;

2)bootloader会加载一个二进制的内核镜像,并执行它,比如uImage或者zImage;

3)bootloader会提供一些额外的信息,成为ATAGS,它的地址会通过r2寄存器传给内核;

ATAGS包含了内存大小和地址,kernel command line等等;

4)bootloader会告诉内核加载哪一款board,通过r1寄存器存放的machine type integer;

5)U-Boot的内核启动命令:bootm

6)Barebox变量:bootm.image (?)

图片[2]-linux内核设备树及编译-趣考网

现今的内核版本使用了Device Tree:

1)内核不再包含对硬件的描述,它以二进制的形式单独存储在另外的位置:the device tree blob

2)bootloader需要加载两个二进制文件:内核镜像和DTB

内核镜像仍然是uImage或者zImage;

DTB文件在arch/arm/boot/dts中,每一个board对应一个dts文件;

3)bootloader通过r2寄存器来传递DTB地址,通过修改DTB可以修改内存信息,kernel command line,以及潜在的其它信息;

4)不再有machine type;

5)U-Boot的内核启动命令:bootm -

6)Barebox变量:bootm.image,bootm.oftree

图片[3]-linux内核设备树及编译-趣考网

有些bootloader不支持Device Tree,或者有些专门给特定设备写的版本太老了,也不包含。为了解决这个问题,CONFIG_ARM_APPENDED_DTB被引进。

它告诉内核,在紧跟着内核的地址里查找DTB文件;

由于没有built-in Makefile rule来产生这样的内核,因此需要手动操作:

        cat arch/arm/boot/zImage arch/arm/boot/dts/myboard.dtb > my-zImage        mkimage ... -d my-zImage my-uImage

(cat这个命令,还能够直接合并两个mp3文件哦!so easy!)

另外,
CONFIG_ARM_ATAG_DTB_COMPAT选项告诉内核去bootloader里面读取ATAGS,并使用它们升级DT。

4、DTB加载及解析过程

图片[4]-linux内核设备树及编译-趣考网

先从uboot里的do_bootm出发,根据之前描述,DTB在内存中的地址通过bootm命令进行传递。在bootm中,它会根据所传进来的DTB地址,对DTB所在内存做一系列操作,为内核解析DTB提供保证。上图为对应的函数调用关系图。

在do_bootm中,主要调用函数为do_bootm_states,第四个参数为bootm所要处理的阶段和状态。

在do_bootm_states中,bootm_start会对lmb进行初始化操作,lmb所管理的物理内存块有三种方式获取。起始地址,优先级从上往下:

  1. 环境变量“bootm_low”
  2. 宏CONFIG_SYS_SDRAM_BASE(在tegra124中为0x80000000)
  3. gd->bd->bi_dram[0].start

大小:

  1. 环境变量“bootm_size”
  2. gd->bd->bi_dram[0].size

经过初始化之后,这块内存就归lmb所管辖。接着,调用bootm_find_os进行kernel镜像的相关操作,这里不具体阐述。

还记得之前讲过bootm的三个参数么,第一个参数内核地址已经被bootm_find_os处理,而接下来的两个参数会在bootm_find_other中执行操作。

首先,bootm_find_other根据第二个参数找到ramdisk的地址,得到ramdisk的镜像;然后根据第三个参数得到DTB镜像,同检查kernel和ramdisk镜像一样,检查DTB镜像也会进行一系列的校验工作,如果校验错误,将无法正常启动内核。另外,uboot在确认DTB镜像无误之后,会将该地址保存在环境变量“fdtaddr”中。

接着,uboot会把DTB镜像reload一次,使得DTB镜像所在的物理内存归lmb所管理:


boot_fdt_add_mem_rsv_regions会将原先的内存DTB镜像所在的内存置为reserve,保证该段内存不会被其他非法使用,保证接下来的reload数据是正确的;

②boot_relocate_fdt会在bootmap区域中申请一块未被使用的内存,接着将DTB镜像内容复制到这块区域(即归lmb所管理的区域)

注:若环境变量中,指定“fdt_high”参数,则会根据该值,调用lmb_alloc_base函数来分配DTB镜像reload的地址空间。若分配失败,则会停止bootm操作。因而,不建议设置fdt_high参数。

接下来,do_bootm会根据内核的类型调用对应的启动函数。与linux对应的是do_bootm_linux。

  • ① boot_prep_linux

为启动后的kernel准备参数

  • ② boot_jump_linux

图片[5]-linux内核设备树及编译-趣考网

以上是boot_jump_linux的片段代码,可以看出:若使用DTB,则原先用来存储ATAG的寄存器R2,将会用来存储.dtb镜像地址。

boot_jump_linux最后将调用kernel_entry,将.dtb镜像地址传给内核。

下面我们来看下内核的处理部分:

在arch/arm/kernel/head.S中,有这样一段:

图片[6]-linux内核设备树及编译-趣考网

_vet_atags定义在
/arch/arm/kernel/head-common.S中,它主要对DTB镜像做了一个简单的校验。

图片[7]-linux内核设备树及编译-趣考网

真正解析处理dbt的开始部分,是setup_arch->setup_machine_fdt。这部分的处理在第五部分的machine_mdesc中有提及。

图片[8]-linux内核设备树及编译-趣考网

如图,是setup_machine_fdt中的解析过程。

  • 解析chosen节点将对boot_command_line进行初始化。
  • 解析根节点的{size,address}将对dt_root_size_cells,dt_root_addr_cells进行初始化。为之后解析memory等其他节点提供依据。
  • 解析memory节点,将会把节点中描述的内存,加入memory的bank。为之后的内存初始化提供条件。
  • 解析设备树在函数unflatten_device_tree中完成,它将.dtb解析成device_node结构(第五部分有其定义),并构成单项链表,以供OF的API接口使用。

下面主要结合代码分析:/drivers/of/fdt.c

图片[9]-linux内核设备树及编译-趣考网

图片[10]-linux内核设备树及编译-趣考网

图片[11]-linux内核设备树及编译-趣考网

图片[12]-linux内核设备树及编译-趣考网

图片[13]-linux内核设备树及编译-趣考网

图片[14]-linux内核设备树及编译-趣考网

图片[15]-linux内核设备树及编译-趣考网

总的归纳为:

① kernel入口处获取到uboot传过来的.dtb镜像的基地址

② 通过early_init_dt_scan()函数来获取kernel初始化时需要的bootargs和cmd_line等系统引导参数。

③ 调用unflatten_device_tree函数来解析dtb文件,构建一个由device_node结构连接而成的单向链表,并使用全局变量of_allnodes保存这个链表的头指针。

④ 内核调用OF的API接口,获取of_allnodes链表信息来初始化内核其他子系统、设备等。

© 版权声明
THE END
喜欢就支持一下吧
点赞11 分享