介绍 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);