基于设备树的嵌入式系统硬件平台识别与参数传递流程解析

发布于:2024-10-10 ⋅ 阅读:(8) ⋅ 点赞:(0)

往期内容

总线:

  1. 驱动中的device和device_driver结构体-CSDN博客
  2. bus总线的相关结构体和注册逻辑-CSDN博客
  3. bus中设备驱动的probe触发逻辑和device、driver的添加逻辑-CSDN博客
  4. platform bus平台总线详解-CSDN博客

设备树:

  1. 设备树语法规则讲解-CSDN博客

前言

主要讲解的是嵌入式系统中如何通过设备树(Device Tree)进行扫描,完成硬件平台的识别和运行时参数的传递过程。内容涵盖了ARM架构下bootloader如何通过寄存器传递设备树地址,内核启动时如何解析设备树中的compatible字符串找到对应的硬件平台描述符(machine descriptor),以及如何通过设备树的/chosen节点获取启动参数(如命令行参数、内存布局)并初始化内核的内存管理。这种流程帮助内核自动适应不同硬件平台,提升可移植性。

1. 汇编部分的启动参数传递

在ARM架构上,内核的启动依赖于bootloader传递的几个关键寄存器值。汇编代码中的启动要求:

  • r0 = 0:保留,必须为0。
  • r1 = machine type ID:标识具体的硬件平台。
  • r2 = atags(旧方法)或 device tree binary(DTB) 的内存地址。

这里 r2 可以是指向传统的 atags 数据结构(旧的方式),也可以是指向设备树文件(DTB)的地址。设备树是以二进制文件的形式存储在内存中的,bootloader 将其加载并通过 r2 传递给内核。

汇编代码的作用

启动时,bootloader将这些寄存器设置好后,把控制权交给内核的启动代码,这些代码位于 linux/arch/arm/kernel/head.Shead-common.S 文件中。关键的任务是将 r1r2 的值保存到内核的变量中,方便后续的C代码处理。这两个值会被保存在以下两个变量中:

  • __machine_arch_type:保存 r1,即 machine type ID
  • __atags_pointer:保存 r2,即指向DTB或atags的指针。

在汇编阶段,这些信息只是简单地保存下来,实际的解析和使用会在C代码部分进行。

2. 内核 C 代码中 setup_arch 的处理

setup_arch 是启动过程中内核进行硬件平台初始化的入口函数。它的主要任务包括:

  • 确定硬件平台的类型(即确定用哪个 machine descriptor 描述符)。
  • 解析从bootloader传递的参数(例如命令行参数、内存布局等)。

下面看具体的C代码:Linux-4.9.88\arch\arm\kernel\setup.csetup.c

void __init setup_arch(char **cmdline_p)
{
    const struct machine_desc *mdesc;

    // 尝试通过设备树(DTB)查找合适的 machine 描述符
    mdesc = setup_machine_fdt(__atags_pointer);
    if (!mdesc)
        // 如果没有找到合适的 machine,使用传统的 tags 方法查找
        mdesc = setup_machine_tags(__atags_pointer, __machine_arch_type);

    // 保存找到的 machine 描述符
    machine_desc = mdesc;
    machine_name = mdesc->name;
}

步骤解析:

  • 尝试通过设备树匹配平台:内核首先调用 setup_machine_fdt 函数,根据 __atags_pointer 指向的DTB文件解析设备树,并找到一个合适的 machine descriptor(平台描述符)。
  • 如果DTB无法匹配:如果通过DTB文件找不到合适的平台描述符,则退回到传统的方式 setup_machine_tags,使用 machine type IDatags 查找平台信息。
  • 保存匹配的结果:无论通过DTB还是传统方式,最终都会得到一个 machine descriptor,保存到全局变量 machine_desc 中,用来标识当前系统运行在哪个硬件平台上。

3. 平台识别(Machine Descriptor匹配)

setup_machine_fdt 函数的任务是通过DTB文件的 compatible 字符串,找到合适的硬件平台描述符。代码如下:Linux-4.9.88\arch\arm\kernel\devtree.cdevtree.c

setup_arch----->setup_machine_fdt:
const struct machine_desc * __init setup_machine_fdt(unsigned int dt_phys)
{
    const struct machine_desc *mdesc, *mdesc_best = NULL;

    // 如果DTB指针无效,或者DTB扫描失败,返回NULL
    if (!dt_phys || !early_init_dt_scan(phys_to_virt(dt_phys)))
        return NULL;

    // 根据 compatible 字符串匹配最合适的 machine descriptor
    mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);

    if (!mdesc) { 
        // 错误处理
    }

    // 修改 machine type ID 以匹配找到的 machine 描述符
    __machine_arch_type = mdesc->nr;

    return mdesc;
}

解析:

  • early_init_dt_scan:这是解析DTB文件的核心函数之一,负责扫描DTB文件中的节点信息,包括 compatible 字符串。
  • of_flat_dt_match_machine:该函数负责将从DTB解析出来的 compatible 字符串与内核中静态定义的 machine descriptor 列表中的 compatible 字符串进行匹配。machine descriptor 是内核用来描述不同硬件平台的结构体,包含了每个平台的 compatible 字符串和其他硬件信息。

例子:

假设你的硬件平台在设备树中定义的 compatible 字符串为 "samsung,s3c2410", 内核会去扫描所有定义了 samsung,s3c2410 作为 compatible 字符串的平台描述符,找到最匹配的一个。

一旦找到匹配的平台描述符,内核会保存该描述符,并更新 machine type ID 为找到的描述符的编号。

4. 运行时参数传递

除了识别硬件平台之外,设备树还承担了传递运行时参数的任务。例如,bootloader会将启动时的命令行参数(bootargs)和内存布局等信息通过DTB传递给内核。

设备树的 /chosen 节点 是专门用来存储这些启动信息的。内核在启动时会扫描DTB中的 /chosen 节点,提取运行时参数,并保存到相应的全局变量中。以下是相关代码:\Linux-4.9.88\drivers\of\fdt.c fdt.c

bool __init early_init_dt_scan(void *params)
{
    bool status;

    // 验证设备树的有效性
    status = early_init_dt_verify(params);
    if (!status)
        return false;

    // 扫描设备树的所有节点
    early_init_dt_scan_nodes();
    return true;
}


bool __init early_init_dt_verify(void *params) 
{
    if (!params)
        return false;

    // 检查设备树头部的有效性
    if (fdt_check_header(params))
        return false;

    // 保存设备树的指针
    initial_boot_params = params;

    // 计算设备树的 CRC32 校验码,用于数据完整性检查
    of_fdt_crc32 = crc32_be(~0, initial_boot_params,
                            fdt_totalsize(initial_boot_params));
    return true;
}

void __init early_init_dt_scan_nodes(void)
{
    // 从 /chosen 节点中提取启动参数(如 bootargs)
    of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);

    // 初始化设备树的 {size,address}-cells 属性
    of_scan_flat_dt(early_init_dt_scan_root, NULL);

    // 扫描 memory 节点,设定内存区域信息
    of_scan_flat_dt(early_init_dt_scan_memory, NULL);
}

解析:

early_init_dt_verify: 这个函数负责验证设备树是否有效,并将其指针保存为全局变量。 保存DTB的指针,以便后续访问DTB文件中的节点。

  • fdt_check_header(params):这是一个标准函数,用来检查设备树头部的魔数和格式是否正确,确保传递的 params 符合设备树的格式。如果设备树无效,函数返回 false,停止后续处理。
  • *initial_boot_params = params:验证成功后,将 params 保存到全局变量 initial_boot_params,以便在其他地方可以访问设备树内容。
  • crc32_be():计算设备树的校验和 of_fdt_crc32,这在后续处理中可能会用来验证设备树数据的完整性。

early_init_dt_scan_nodes: 该函数的作用是扫描设备树中的各个节点,提取与运行时参数、内存布局等相关的重要信息。

  • of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line)
    • 该调用扫描设备树的 /chosen 节点,提取引导参数(如 bootargs)。
    • bootargs 是运行时传递给内核的命令行参数,如根文件系统位置、内核启动参数等。
    • 提取的信息会保存到全局变量 boot_command_line 中,供后续内核启动使用。
  • of_scan_flat_dt(early_init_dt_scan_root, NULL)
    • 这一步扫描设备树根节点,提取与 size-cellsaddress-cells 相关的信息。
    • size-cellsaddress-cells 是设备树中的两个重要属性,决定了地址和尺寸的表示方式(例如,使用几个字节表示内存的地址和大小)。
  • of_scan_flat_dt(early_init_dt_scan_memory, NULL)
    • 该调用扫描 memory 节点,提取系统的物理内存布局,并调用 early_init_dt_add_memory_arch 来设置内存区域。
    • 这一步是设定物理内存信息的关键步骤。

整个流程的核心工作包括设备树验证和节点扫描,其中 /chosen 节点负责传递引导参数(如 bootargs),/memory 节点负责定义系统的内存布局。

运行时参数传递流程总结

  1. 引导参数:通过扫描 /chosen 节点,从中提取 bootargs 等关键的启动参数(例如内核启动时的命令行参数),并保存到全局变量 boot_command_line 中。
  2. 内存布局:通过扫描 /memory 节点,提取物理内存信息,并设置内存区域。这一步确保内核知道物理内存的分布,为后续内存管理做准备。
  3. 数据验证:通过 early_init_dt_verify 对设备树的头部进行检查,并通过 crc32_be 验证设备树数据的完整性。

5. 内存布局的传递

在ARM架构上,内核需要知道系统的物理内存布局(即内存的起始地址和大小)。通过Device Tree,bootloader可以在设备树中的 /memory 节点传递这些信息。内核通过解析这个节点,将内存信息保存到全局变量 meminfo 中。

/* 扫描设备树中的 /memory 节点,提取内存信息 */
of_scan_flat_dt(early_init_dt_scan_memory, NULL);

这一步是获取系统内存布局信息的核心,确保内核知道系统中的物理内存位置和大小。

6. 大概

  1. 汇编部分:bootloader通过寄存器传递硬件平台的 machine type ID 和设备树的地址。
  2. 内核C代码的setup_arch:通过DTB的 compatible 字符串匹配平台描述符,识别当前运行的硬件平台。
  3. 运行时参数传递:通过扫描设备树的 /chosen 节点,内核提取启动命令行参数 bootargs,并保存内存布局信息。

通过这种方式,Linux内核能够使用Device Tree文件在启动时自动识别硬件平台,并传递运行时参数,从而减少对硬件平台的依赖,提升了内核的可移植性。