介绍 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,传入参数一般分为三种情况
- 利用
U-boot的命令,在引导kernel时将dts传入。这种方式需要将dtb的地址写到uboot中(一般是环境变量),比如:首先将kernel载入内存,然后用fdt addr ${fdtaddr}命令将dtb载入内存,最后使用bootz ${loadaddr} ${initrdaddr} ${fdtaddr}来引导内核,(其中initrd是临时文件系统,嵌入式中用得极少)实际使用时用-代替:bootz ${loadaddr} - ${fdtaddr}。总之,U-boot中的命令和环境变量是很灵活的,可以随意组合 - 将
dts和kernel打包为pImage。这种方式无需将dtb的地址写到uboot中(但uboot中要实现读pImage头部的功能),uboot可以去pImage的头部信息处读取到dtb的地址,然后传给传递给kernel - 启用
kernel中ARM_APPENDED_DTB选项,该选项的意思是将dtb和kernel打包在一起,如此一来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 {
将 KERNLE 及 DTB 合并
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);