0%

Linux dtb 文件及相关启动流程

介绍 Linux 内核设备树 Device Tree,及其他相关知识,包括镜像制作及启动内核

Device Tree

开源文档中对设备树的描述是,一种描述硬件资源的数据结构,它通过 BootLoader 将硬件资源传给内核,使得内核和硬件资源描述相对独立。ARM 内核版本 3.x 之后引入
Device Tree 可以描述的信息包括

  • CPU 的数量和类别
  • 内存基地址和大小
  • 总线
  • 外设连接
  • 中断控制器
  • 中断使用情况
  • GPIO 控制器和 GPIO 使用情况
  • Clock 控制器和 Clock 使用情况。
  • 热插拔设备的控制器

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

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

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

设备树的组成

设备树 包括 DTC(device tree compiler) , DTS(device tree source)DTB(device tree binary)
1 个 dts 文件 + n 个 dtsi 文件,它们经 DTC 编译而成的 dtb 文件 就是真正的设备树

DTS

soc 厂商 会把 soc 公共的特性和多块开发板公用的特性提炼为 dtsi,而 dts 则负责描述某个具体的产品(开发板)的特性。 dts 直接或间接的包含多个 dtsi (类似于 c 语言的头文件),就体现了一个完整的产品(开发板)所有的特性。以 solidrun 公司的 hummingboard 为例,其组成为

目录 arch/arm/boot/dts/imx6dl-hummingboard.dts

#include "imx6dl.dtsi"
#include "imx6qdl-hummingboard.dtsi"

/ {
    model = "SolidRun HummingBoard Solo/DualLite";
    compatible = "solidrun,hummingboard/dl", "fsl,imx6dl";
};

dts/dtsi 兼容 c 语言的一些语法,能使用宏定义,也能包含 .h 文件

dts 语法可以看设备树详解

DTC

DTC 为编译工具,它可以将 .dts 文件编译成 .dtb 文件。 DTC 的源码位于内核的 scripts/dtc 目录下,内核选中 CONFIG_OF,编译内核的时候,主机可执行程序 DTC 就会被编译出来。

DTB

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

使用 DTC 工具可以将 .dtb 生成 .dts

dtc -I dtb -O dts -o xxxxxxx.dts /arch/arm64/boot/dts/qcom/xxxx.dtb

内核启动与 DTB

内核必须知道 DTB 文件地址进行解析,这一步是由 bootloader 来传入,以 uboot 为例来介绍

需要在编译 u-boot 时打开 #define CONFIG_OF_LIBFDT,传入参数一般分为三种情况

  1. 利用 U-boot 的命令,在引导 kernel 时将 dts 传入。这种方式需要将 dtb 的地址写到 uboot 中(一般是环境变量),比如:首先将 kernel 载入内存,然后用 fdt addr ${fdtaddr} 命令将 dtb 载入内存,最后使用 bootz ${loadaddr} ${initrdaddr} ${fdtaddr} 来引导内核,(其中 initrd 是临时文件系统,嵌入式中用得极少)实际使用时用 - 代替: bootz ${loadaddr} - ${fdtaddr}。总之, U-boot 中的命令和环境变量是很灵活的,可以随意组合
  2. dtskernel 打包为 pImage。这种方式无需将 dtb 的地址写到 uboot 中(uboot 中要实现读 pImage 头部的功能), uboot 可以去 pImage 的头部信息处读取到 dtb 的地址,然后传给传递给 kernel
  3. 启用 kernelARM_APPENDED_DTB 选项,该选项的意思是将 dtbkernel 打包在一起,如此一来 kernel 启动时会去紧挨着它的地方寻找 dtb,这样就不需要 uboot 来传递 dtb 地址

项目中选用第二种,因此需要修改相应的 boot镜像制作工具

uboot 传参及引导,最后把入口地址 ep 转化为一个函数指针 theKernel = (void (*)(int, int, uint))ep,然后通过函数指针去执行镜像。

    void (*theKernel)(int zero, int arch, uint params);// 定义了一个函数指针
    // 中间代码略过
    theKernel = (void (*)(int, int, uint))ep;// 把入口地址赋给函数指针
    // 中间代码略过
    theKernel (0, machid, bd->bi_boot_params);// 跳到内核入口执行内核,再也不返回

生成镜像

内核编译完成之后生成 uImage.dtb,使用 genflash 来制作镜像,核心代码如下

从配置文件中解析 .dtb 文件名

else if (keycmp(str, "dtb_file") == 0) {
    key = strtok(str, " \t");
    value = strtok(NULL, " \t");
    trim(value);

    strcpy(dtb_file, value);
} else {

KERNLEDTB 合并

if ((keycmp(info->name, "KERNEL") == 0) && (*dtb_file)) {
    size_t dtb_size = 0;
    void *dtb_buf = NULL;
    snprintf(filename, 255, "%s/%s", basedir, dtb_file);
    dtb_buf = x_fmmap(filename, &dtb_size);
    if (!dtb_buf)
        return NULL;
    total_size = dtb_size + org_size;
    total_buf = malloc(total_size);
    if (!total_buf)
        return NULL;
    memcpy(total_buf, dtb_buf, dtb_size);
    memcpy(total_buf + dtb_size, org_buf, org_size);
    x_munmap(dtb_buf, dtb_size);
} else {
    total_size = org_size;
    total_buf = malloc(total_size);
    memcpy(total_buf, org_buf, total_size);
}

镜像制作之后 .dtb 被放在头部

bootloader 传递 DTB

采用第二种方式的 bootloader 流程如下

doboot_kernel 函数

...
// 解析 DTB 头部,计算长度,得到内核偏移地址
if (modify_dtb(&kernel_offset) < 0) {
    printf("errror: %s %s %d\n", __FILE__, __func__, __LINE__);
    return -1;
}

// 启动内核
while (kernel_names[id]) {
    if (loader_file(&part, kernel_names[id], kernel_offset) == 0) {
        run_kernel(part.dest);
        return 0;
    }
    id++;
}
...

modify_dtb 实现 FDT 解析 .dtb

static int modify_dtb(unsigned int *kernel_offset)
{
    int ret = -1;
    char *dtb_addr = (char *)KERNEL_DTB_START_ADDR;
    unsigned int dtb_totalsize = KERNEL_DTB_SIZE;
    char *cmdline = (char *)((struct tag *)bootstr_cmdline)->u.cmdline.cmdline;

    if (get_dtb(dtb_addr, dtb_totalsize) < 0)
        return ret;
    if (fdt_check_header((void *)dtb_addr) < 0)
        return ret;
    *kernel_offset = fdt_totalsize((void *)dtb_addr);
    if (fdt_open_into((void *)dtb_addr, (void *)dtb_addr, dtb_totalsize))
        return ret;
    if (fdt_chosen((void *)dtb_addr, cmdline))
        return ret;

    ret = 0;
    return ret;
}

fdt_chosen 将设备树中的 bootargs 覆盖掉,实现将 bootloader 中的 cmdline 加载到树中

run_kernel 实现如下

    param_to_kernel = (char *)KERNEL_DTB_START_ADDR;
    void (*theKernel) ( unsigned int, unsigned int, char*) = (void (*) (unsigned int, unsigned int, char*))addr;
    (*theKernel)(0, 0, param_to_kernel);

Ref

  1. 使用 dtb 文件引导内核
  2. 设备树详解
  3. U-boot 引导内核流程分析
  4. [[uboot] (番外篇)uboot 之 fdt 介绍』(https://blog.csdn.net/ooonebook/article/details/53206623)
  5. 基于 tiny4412 的 Linux 内核移植(支持 device tree)
  6. making_kernel_with_dtb
  7. uboot 流程——uboot 启动流程