往期内容
总线:
设备树:
前言
主要讲解的是嵌入式系统中如何通过设备树(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.S
和 head-common.S
文件中。关键的任务是将 r1
和 r2
的值保存到内核的变量中,方便后续的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 ID
和atags
查找平台信息。 - 保存匹配的结果:无论通过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-cells
和address-cells
相关的信息。 size-cells
和address-cells
是设备树中的两个重要属性,决定了地址和尺寸的表示方式(例如,使用几个字节表示内存的地址和大小)。
- 这一步扫描设备树根节点,提取与
of_scan_flat_dt(early_init_dt_scan_memory, NULL)
:- 该调用扫描
memory
节点,提取系统的物理内存布局,并调用early_init_dt_add_memory_arch
来设置内存区域。 - 这一步是设定物理内存信息的关键步骤。
- 该调用扫描
整个流程的核心工作包括设备树验证和节点扫描,其中 /chosen
节点负责传递引导参数(如 bootargs
),/memory
节点负责定义系统的内存布局。
运行时参数传递流程总结:
- 引导参数:通过扫描
/chosen
节点,从中提取bootargs
等关键的启动参数(例如内核启动时的命令行参数),并保存到全局变量boot_command_line
中。 - 内存布局:通过扫描
/memory
节点,提取物理内存信息,并设置内存区域。这一步确保内核知道物理内存的分布,为后续内存管理做准备。 - 数据验证:通过
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. 大概
- 汇编部分:bootloader通过寄存器传递硬件平台的
machine type ID
和设备树的地址。 - 内核C代码的
setup_arch
:通过DTB的compatible
字符串匹配平台描述符,识别当前运行的硬件平台。 - 运行时参数传递:通过扫描设备树的
/chosen
节点,内核提取启动命令行参数bootargs
,并保存内存布局信息。
通过这种方式,Linux内核能够使用Device Tree文件在启动时自动识别硬件平台,并传递运行时参数,从而减少对硬件平台的依赖,提升了内核的可移植性。