Linux设备树


前言

参考:
https://blog.csdn.net/gzxb1995/article/details/108751514
https://blog.csdn.net/gzxb1995/article/details/108504202
关于设备树详细的语法规则请参考《Devicetree SpecificationV0.2.pdf》和《Power_ePAPR_APPROVED_v1.12.pdf》这两份文档。


1 前言

如何去描述设备这一定是设备驱动需要考虑的重要问题。早先,linux中ARM架构使用源文件中的宏、结构等去描述设备,不过现在已全面转向设备树,3x之后的版本。

2 概念

不使用源文件描述设备
设备信息还是源文件,那么我们修改设备的时候就又需要编译一遍源码。
设备树就使用DTS文件来描述设备,将设备信息和源代码分离:
具体的,使用某种特定格式的文本文件来描述设备信息,然后将该文本文件转化为一个按照某种特定格式组织的二进制文件(直接解析文本文件较复杂,不适合内核来做),之后将二进制文件传递给内核(就像u-boot传参那样),由内核根据二进制文件的信息来动态构建并注册相应的设备结构。至此,终于摆脱了源文件。上述文本文件即为DTS文件,上述二进制文件即为DTB文件:
在这里插入图片描述
DTS 是设备树源码文件,DTB 是将 DTS 编译以后得到的二进制文件。将.c 文件编译为.o 需要用到 gcc 编译器,那么将.dts 编译为.dtb 需要什么工具呢?需要用到 DTC 工具!DTC 工具源码在 Linux 内核的 scripts/dtc 目录下,如果要编译 DTS 文件的话只需要进入到 Linux 源码根目录下,然后执行命令:make all 或者 make dtbs。

基于 ARM 架构的 SOC 有很多种,一种 SOC 又可以制作出很多款板子,每个板子都有一个对应的 DTS 文件,那么如何确定编译哪一个 DTS 文件呢?打开 arch/arm/boot/dts/Makefile 可以查看。

在ARM Linux内,一个.dts(device tree source)文件对应一个ARM的machine,一般放置在内核的"arch/arm/boot/dts/"目录内。

基于同样的软件分层设计的思想,由于一个SoC可能对应多个machine,如果每个machine的设备树都写成一个完全独立的.dts文件,那么势必相当一些.dts文件有重复的部分,为了解决这个问题,Linux设备树目录把一个SoC公用的部分或者多个machine共同的部分提炼为相应的.dtsi文件。这样每个.dts就只有自己差异的部分,公有的部分只需要"include"相应的.dtsi文件。
一般.dts 描述板级信息(也就是开发板上有哪些 IIC 设备、SPI 设备等),.dtsi 描述 SOC 级信息(也就是 SOC 有几个 CPU、主频是多少、各个外设控制器信息等)。

3 设备树文件的格式

3.1 DTS文件格式

/* 设备树文件支持c语言的注释符 */
// 下面是设备树的总体布局
/dts-v1/;							//设备树文件的版本
[memory reservations]				//指定保留内存,内核不会使用保留内存				格式:<address> <length>;需要注意的是,address和length都是64位。
/ {									//根节点(使用花括号括出属于根节点的内容)
	[property definitions]			//根节点的属性,用来描述硬件						格式:有值的格式:[label:] property-name = value;
																					   //无值的格式:[label:] property-name;
	[child nodes]					//孩子节点(使用花括号括出属于孩子节点的内容)
};					

3.1.1属性的格式

属性名

[label:] property-name = value;		//标签,属性名,属性值
//属性名支持的字符:
//0-9	a-z		A-Z		,	.	_	+	?	#	-

属性名的长度为1~31个字符,可以自己取,只要能够提供可以解读该属性名的驱动即可。也有一些属性名有着特定的含义,比如compatible用于表示哪个或哪些驱动支持该设备。

属性值
属性值有以下四种:
array of cells
一个cell就是一个u32类型的数据,一个或多个cell用尖括号括起来,并以空格隔开就可以作为一种合法的属性值,如example = <0x1 0x2 0x3>;。
含有结束符的字符串
如example = “example value”;
字节序列
用方括号括起一个或多个字节,字节之间可用也可不用空格隔开,且字节以两位16进制数表示,如example = [12 34 56 78];。
以上三种值的混合(以逗号隔开)
如compatible = “fsl,mpc8641”, “ns16550”;。

一些特定的属性:

#address-cells
该属性的值表示在该节点的子节点的reg属性中,使用使用多少个cell,也即使用多少个u32整数来表示地址(对于32位系统,一个u32整数就够了;而对于64位系统,需要两个u32整数)。
#size-cells
该属性的值表示在该节点的子节点的reg属性中,使用多少个cell,也即使用多少个u32整数来表示大小(一段地址空间的长度)。
compatible
其值为一个或多个字符串,用来描述支持该设备的驱动程序。比如,该属性位于根节点时,用于指定内核中哪个machine_desc可以支持本设备,即当前设备与哪些平台兼容。其值的格式一般是"manufacturer,model",其中manufacturer表示厂家,model表示型号(厂家的哪型产品)。
当该属性的值有多个字符串时,从左往右,从最特殊到最一般。举例来说,compatible = “samsung,smdk2416”, “samsung,s3c2416”;作为根节点的属性时,第一个字符串指示了一个具体的开发板型号,而第二个字符串要更一般,只指示了SoC的型号。在linux初始化时,会优先找支持"samsung,smdk2416"的machine_desc用以初始化硬件,找不到时才退而求其次——“samsung,s3c2416”。
model
其值为一个字符串,用来描述当前设备的型号(单板的名字)。当多个设备的compatible相同时,可以通过model来进一步区分多个设备。
phandle
该属性可以为节点指定一个全局唯一的数字标识符。这个标识符可以被需要引用该节点的另一个节点使用。举例来说,现有一个中断控制器:

pic@10000000 {
	phandle = <1>;
	interrupt-controller;
};

还有一个可以产生中断的设备,且这个设备的中断信号线连接到了上述中断控制器,为了描述清楚这种关系,该设备的设备节点就需要引用中断控制器的节点:

another-device-node {
	interrupt-parent = <1>; /* 数字1就唯一标识了节点pic@10000000 */
};

interrupt-controller
这是一个没有值的属性,用在中断控制器的设备节点中,以表明这个节点描述的是一个中断控制器。
interrupt-parent
该属性用于可以产生中断,且中断信号连接到某中断控制器的设备的设备节点,用于表示该设备的中断信号连接到了哪个中断控制器。该属性的值通常是中断控制器设备节点的数字标识(phandle),具体示例在上文已经出现过了。
reg
reg属性描述了设备资源在其父总线定义的地址空间内的地址。通俗的说,该属性使用一对或多对(地址,长度)来描述设备所占的地址空间。至于地址和长度使用多少个cell来表示呢?这取决于上文介绍的#address-cells、#size-cells属性的值。
举个例子,当:

#address-cells = <1>;
#size-cells = <1>;

那么reg = <0x3000 0x20 0xFE00 0x100>,表示该属性所属的设备占据了两块内存空间,第一块是以0x3000为起始的32字节内存块;第二块是以0xFE00为起始的256字节内存块。

3.1.2节点的格式

[label:] node-name[@unit-address] {
	[properties definitions]
	[child nodes]
};
//node-name  0-9	a-z		A-Z		,	.	_	+	-
一些特殊的节点:

/aliases
/aliases节点应当作为根节点的孩子节点:

aliases {
	serial0 = "/simple-bus@fe000000/serial@llc500";
	ethernet0 = "/simple-bus@fe000000/ethernet@31c000";
};
//其支持的字符:a-z	0-9		-		

别名通常以数字结尾,比如别名为i2c1,设备树的初始化程序在解析别名属性时,会将数字1记录在struct alias_prop结构的id成员中,使用of_alias_get_id可以获得这个数字。因为本文主要介绍设备树文件的格式,因此这里不再深究这部分内容。
/chosen
/chosen节点应当用作根节点的孩子节点,有以下可选属性:
bootargs
stdout-path
stdin-path

/ {
	......
	chosen {
		bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200";
	};
	......
};

3.1.3必要的节点和必要的属性

一个完整的设备树文件(DTS文件),有一些节点是必须要有的:

节点名节点的必要属性
/#address-cells、#size-cells、model、compatible
/memorydevice_type、reg
/cpus#address-cells、#size-cells
/cpus/cpu*device_type、reg、clock-frequency、timebase-frequency
/cpus/cpu*/l?-cachecompatible、cache-level

3.1.4标签label

一般情况下:

pic@10000000 {
	phandle = <1>;
	interrupt-controller;
};
......
another-device-node {
	interrupt-parent = <1>; /* 数字1就唯一标识了节点pic@10000000 */
};

如果我们使用phandle来标识设备,当设备多了,数字标识符是比较难记忆的,可读性也差,此时可以使用标签:

PIC: pic@10000000 {
	interrupt-controller;
};
......
another-device-node {
	interrupt-parent = <&PIC>; /* 用标签来引用设备节点 */
};

还有一种常见的标签的用法,当我们需要修改某设备节点的属性,但又不想直接在原地修改(保持原来的内容不被破坏),此时可以在设备树文件的末尾重写该设备的相应属性,从而覆盖之前的内容:

/ {
	......
	device-node {
		p = "xxx";
		......
	};
	......
};

/* 重写device-node的属性p */
/ {	                         /* 根节点也要写上,完整的体现device-node的路径 */
	device-node {            /* 因此这样写比较麻烦(特别是在路径比较深的时候) */
		p = "yyy";
	};
};

使用标签的写法:

/ {
	......
	DN: device-node {
		p = "xxx";
		......
	};
	......
}

/* 重写device-node的属性p */
&DN {  /* 通过标签引用节点 */
	p = "yyy";
};

3.2 编写DTS文件

在DTS文件中包含其他文件
编写设备树文件时,我们通常会把多型设备的共性抽出来,写在DTSI文件(后缀为.dtsi)中,其语法与DTS文件一样。比如,多款使用了am335x的板子,因为使用了同一款SoC,描述设备时肯定会有一些相同的部分,可以把这部分抽出来,写到am335x.dtsi中,然后在具体的某型板子的设备树中包含相应的DTSI文件,包含的方式有:

/include/ “xxx.dtsi”
#include “xxx.dtsi”
设备树编译器还支持c语言的头文件,因此,如果有需要可以定义一些宏并在设备树文件中使用。

如何在设备树文件中描述设备
设备树写出来是给驱动程序看的,也就是说驱动程序怎么写的,相应的设备树就该怎么写;或者反过来,先约定好设备树怎么写,在相应的设计驱动。驱动和设备树有着对应的关系,这种对应关系也被称为bindings。具体的:

对于上游芯片厂商,应当按照devicetree-specification推荐的设备树写法,遵守各种约定,确定好如何规范的描述设备,并提供相应的驱动程序。devicetree-specification-v0.3的第四章给出了一些推荐的做法。
对于下游产品厂商,当使用芯片厂商的芯片做产品时,芯片厂商通常会提供驱动程序和设备树文件编写的参考文档,这些文档位于linux内核源码树的Documentation/devicetree/bindings目录下。如果芯片厂商没提供相应文档的话,就要读驱动的源码,知道驱动怎么写的,自然也就知道如何写设备树了。

3.3 DTB文件格式

DTB文件使用大端模式存储数值。整体上,该文件由四个部分组成:
struct ftd_header

在这里插入图片描述

2.2 memory reservation block
该部分由1.2节介绍的memory reservations编译得来,由一个或多个表项组成,每一项都描述了一块要保留的内存区域,每项由两个64位数值(起始地址、长度)组成:

struct fdt_reserve_entry{
	uint64_t address;
	uint64_t size;
};

structure block
在这里插入图片描述

strings block
该部分类似于ELF文件中的字符串表,存储了所有属性名(字符串),考虑到很多节点拥有一些同名的属性,集中存放属性名可以有效的节约DTB文件的空间,存放有属性的structure block部分只需要保存一个32位的偏移值——属性名的起始位置在strings block中的偏移。

4 uboot对设备树的支持

在不支持设备树时,u-boot给内核传递的参数保存在struct tag结构体中,一系列的参数就对应着内存中连续分布的一系列tag,以ATAG_CORE起始,以ATAG_NONE结束。
设备树方式传参和tag方式传参如下:

寄存器tag方式传参设备树方式传参
r0通常设置为0通常0
r1板子的machine id
r2ATAGS起始地址DTB起始地址

在不使用设备树时,参数的构建由u-boot完成,u-boot是知道参数的地址的,因此bootm命令中只需要kernel镜像的地址,下面是一个例子:

nand read.jffs2 0x30007FC0 kernel
bootm 0x30007FC0

使用设备树启动内核时,除了要加载kernel镜像,还要将DTB文件加载到内核,同时bootm命令中需要加上DTB文件在内存中的地址,格式为bootm <uImage_addr> <initrd_addr> <dtb_addr>。下面是一个例子:

 nand read.jffs2 0x30007FC0 kernel
 nand read.jffs2 32000000 device_tree // 读dtb到内存地址0x32000000处
 bootm 0x30007FC0 - 0x32000000        // 没有initrd时对应参数写为"-"

fdt加载的位置
uboot加载fdt到内存时,需注意不能将uboot和kernel覆盖,并且不能在kerenl用于临时页表的内存。

相关命令
fdt

修改fdt的内容:
修改属性值的实现过程(设旧值长度为len,新值长度为newlen > len):
把DTB中旧值之后的所有内容向后移动newlen - len字节
把新值写入val所占的newlen字节空间
修改DTB头部信息中structure block的长度(size_dt_struct)
修改DTB头部信息中string block的偏移值(off_dt_strings)
修改DTB头部信息中的总长度(totalsize)

添加一个新属性的实现过程:
如果DTB的string block中没有这个属性的名字,就在string block尾部添加新属性的的属性名字符串,并且修改DTB头部信息中string block的长度(size_dt_strings),修改DTB头部信息中的总长度(totalsize)
找到属性所在节点,在节点尾部扩展一块空间(移动DTB),在扩展出的空间内填充新属性的信息:
属性标记:4字节,0x00000003
len:4字节,表示属性值的长度
nameoff:4字节,表示属性名在string block中的偏移
val:len字节的属性值
修改DTB头部信息中structure block的长度(size_dt_struct)
修改DTB头部信息中string block的偏移值(off_dt_strings)
修改DTB头部信息中的总长度(totalsize)

5 内核对DTB文件的解析

5.1从u-boot传参到__atags_pointer

当u-boot向内核传递DTB文件时,内核真正要关注的只有r2寄存器中存放的DTB文件的内存起始地址。内核会在启动的汇编阶段把这个地址保存到全局变量__atags_pointer,大体的过程如下:

/*
	1、设置r13,跳转执行__enable_mmu
*/
ENTRY(stext)
	......
	ldr r13, =__mmap_switched
	......
	/* 跳转执行__enable_mmu */
	b __enable_mmu
ENDPROC(stext)

/*
	2、使能MMU,跳转执行__mmap_switched
*/
__enable_mmu:
	/* 在r0设置好打算写往MMU控制寄存器的值 */
	......
	/* 跳转执行 */
	b	__turn_mmu_on

ENTRY(__turn_mmu_on)
	mov	r0, r0
	instr_sync
	mcr	p15, 0, r0, c1, c0, 0		@ write control reg
	mrc	p15, 0, r3, c0, c0, 0		@ read id reg
	instr_sync
	mov	r3, r3
	/* r13 = __mmap_switched */
	mov	r3, r13
	ret	r3
__turn_mmu_on_end:
ENDPROC(__turn_mmu_on)

/*
	3、在__mmap_switched中,将r2(保存DTB的地址)的值存到__atags_pointer,然后跳转start_kernel
*/
__mmap_switched:
	mov	r7, r1
	mov	r8, r2    /* 把DTB的地址转存到r8 */
	mov	r10, r0

	adr	r4, __mmap_switched_data
	mov	fp, #0
	
    ARM(	ldmia	r4!, {r0, r1, sp} )
	sub	r2, r1, r0
	mov	r1, #0
	bl	memset				@ clear .bss

	ldmia	r4, {r0, r1, r2, r3}
	str	r9, [r0]			@ Save processor ID
	str	r7, [r1]			@ Save machine type
	
	/* 把DTB的地址写到全局变量__atags_pointer */
	str	r8, [r2]			@ Save atags pointer
	
	cmp	r3, #0
	strne	r10, [r3]			@ Save control register values
	mov	lr, #0
	b	start_kernel
ENDPROC(__mmap_switched)

__mmap_switched_data:
	.long	__bss_start			@ r0
	.long	__bss_stop			@ r1
	.long	init_thread_union + THREAD_START_SP @ sp

	.long	processor_id			@ r0
	.long	__machine_arch_type		@ r1
	.long	__atags_pointer			@ r2
	......

kernel启动的汇编阶段结束后,会跳转执行start_kernel,此时__atags_pointer指向内存中的DTB文件。kernel对DTB的解析在start_kernel==>setup_arch函数中进行。kernel将DTB中的信息分为三类:

平台识别信息,通常指的是根节点的compatible、model属性记录的信息;
运行时配置信息,通常指的是/chosen节点和/memory节点记录的信息;
设备信息,指各个设备节点。
下文将分别介绍内核对这三种信息的处理。

5.2内核对设备树中平台信息的处理

machine_desc
一个kernel镜像通常会支持很多板子,针对每种板子,kernel都会为其定义一个struct machine_desc的结构,其中就记录各个板子的硬件信息,比如板子的ID号、名字、支持的中断数量、初始化函数等。这样,在kernel启动时,可以根据u-boot传递的参数/DTB文件选则合适的machine_desc,从而正确的初始化当前硬件。

kernel会将一系列machine_desc集中存放在.init.arch.info节中,形成如同数组一样的内存分布:

 .init.arch.info : {
  __arch_info_begin = .;
  *(.arch.info.init)
  __arch_info_end = .;
 }

并以符号__arch_info_begin和__arch_info_end记录该节的起始和结尾,如此一来,就可以向访问数组元素那样访问每个machine_desc。

在选则machine_desc时,kernel首先会获取DTB的根节点的compatible属性,将其中的一个或多个字符串与machine_desc的dt_compat成员记录的一个或多个字符串进行比较,当匹配时,返回相应的machine_desc。值得一提的是,compatible属性值中,位置靠前的字符串会优先比较,换句话说,位置越靠前说明该字符串指示的machine_desc越适合当前单板。

源码分析
setup_arch

void __init setup_arch(char **cmdline_p)
{
	const struct machine_desc *mdesc;
	/* 初始化一些处理器相关的全局变量 */
	setup_processor();

	/* 优先按照设备树获取machine_desc */
	mdesc = setup_machine_fdt(__atags_pointer);
	/* 如果u-boot传递的不是DTB,则按照ATAGS获取machine_desc */
	if (!mdesc)
		mdesc = setup_machine_tags(__atags_pointer, __machine_arch_type);
	......
	/* 记录获取到的machine_desc及其名字 */
	machine_desc = mdesc;
	machine_name = mdesc->name;
	dump_stack_set_arch_desc("%s", mdesc->name);
	......
	/* 将boot_command_line的内容拷贝到cmd_line */
	strlcpy(cmd_line, boot_command_line, COMMAND_LINE_SIZE);
	/* 输出指向启动参数的指针 */
	*cmdline_p = cmd_line;
	......
	/* 根据DTB创建device_node树 */
	unflatten_device_tree();
	......
#ifdef CONFIG_GENERIC_IRQ_MULTI_HANDLER
	/* 设置handle_arch_irq */
	handle_arch_irq = mdesc->handle_irq;
#endif
	......
	/* 调用machine_desc中注册的初始化函数 */
	if (mdesc->init_early)
		mdesc->init_early();
}

由上文可知,setup_arch函数调用setup_machine_fdt解析设备树中的相关信息,并返回合适的machine_desc:

const struct machine_desc * __init setup_machine_fdt(unsigned int dt_phys)
{
	const struct machine_desc *mdesc, *mdesc_best = NULL;
	/* 验证DTB文件是否存在:地址不为NULL && 文件头部magic正确 */
	/* initial_boot_params = dt_phys */
	if (!dt_phys || !early_init_dt_verify(phys_to_virt(dt_phys)))
		return NULL;
	/* 获取compatible属性并匹配合适的machine_desc */
	mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);

	if (!mdesc) {
		/* 打印一些信息 */
		......
		/* 把当前kernel支持的单板的名字和单板ID打印出来 */
		/* 该函数不会返回(内部有死循环) */
		dump_machine_table();
	}

	/* 当DTB文件提供的数据有问题,这里会做一些修补工作 */
	if (mdesc->dt_fixup)
		mdesc->dt_fixup();
	
	/* 获取运行时配置信息,再第3节中细说 */
	early_init_dt_scan_nodes();

	/* 记录machine ID */
	__machine_arch_type = mdesc->nr;

	return mdesc;
}

of_flat_dt_match_machine函数是匹配合适的machine_desc的关键:

/*
	传入的第一个参数为NULL
	传入的第二个参数为arch_get_next_mach
	
	arch_get_next_mach的原理非常简单:初始化一个静态局部变量为__arch_info_begin,
	每次被调用时该变量(指针)+1并返回,如果超出了__arch_info_end,则返回NULL
*/
const void * __init of_flat_dt_match_machine(const void *default_match,
		const void * (*get_next_compat)(const char * const**))
{
	const void *data = NULL;
	const void *best_data = default_match;
	const char *const *compat;
	unsigned long dt_root;
	unsigned int best_score = ~1, score = 0;
	/* dt_root = 0 */
	dt_root = of_get_flat_dt_root();
	/* 
		遍历所有machine_desc,将machine_desc的dt_compat保存到compat
		compat指向一系列字符串(一个machine_desc也可能支持多个单板)
	*/
	while ((data = get_next_compat(&compat))) {
		/*
			DTB根节点的compatible属性值是一系列字符串,假设为"aaa", "bbb", "ccc"
			machine_desc的dt_compat(指针的指针)也指向一系列字符串,假设为"xxx", "ccc"
			第一轮比较(score = 0):
			1、score++, compatible的"aaa"<==>dt_compat的"xxx"
			2、score++, compatible的"bbb"<==>dt_compat的"xxx"
			3、score++, compatible的"ccc"<==>dt_compat的"xxx"
			第二轮比较(score = 0):
			1、score++, compatible的"aaa"<==>dt_compat的"ccc"
			2、score++, compatible的"bbb"<==>dt_compat的"ccc"
			3、score++, compatible的"ccc"<==>dt_compat的"ccc",此时匹配上,返回score(值为3)
		*/
		score = of_flat_dt_match(dt_root, compat);
		/* 记录得分最低(最匹配)的machine_desc */
		if (score > 0 && score < best_score) {
			best_data = data;
			best_score = score;
		}
	}

	/* 没有匹配到合适的machine_desc就返回NULL */
	if (!best_data) {
		/* 打印根节点的compatible属性值 */
		......
		return NULL;
	}
	
	/* 打印根节点的model属性值,若不存在则打印compatible属性值 */
	pr_info("Machine model: %s\n", of_flat_dt_get_machine_name());

	return best_data;
}

至此,如何根据DTB的根节点的compatible属性匹配machine_desc就介绍完了。

5.3内核对设备树中运行时配置信息的处理

kernel使用setup_arch ==> setup_machine_fdt ==> early_init_dt_scan_nodes来处理DTB中的运行时配置信息:

void __init early_init_dt_scan_nodes(void)
{
	int rc = 0;

	/* 获取/chosen节点的信息 */
	rc = of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);
	if (!rc)
		pr_warn("No chosen node found, continuing without\n");

	/* 获取根节点的{size,address}-cells属性值,之后才方便解析根节点的子节点的reg属性 */
	of_scan_flat_dt(early_init_dt_scan_root, NULL);

	/* 解析/memory节点,设置内存信息 */
	of_scan_flat_dt(early_init_dt_scan_memory, NULL);
}

要进一步了解其中的细节,我们需要先弄清楚of_scan_flat_dt函数做了什么:

/**
 * 遍历DTB的节点,直到参数传入的回调函数it返回非0值
 */
int __init of_scan_flat_dt(int (*it)(unsigned long node,
				     const char *uname, int depth,
				     void *data),
			   void *data)
{
	/* blob指向DTB在内存中的起始地址 */
	const void *blob = initial_boot_params;
	const char *pathp;
	int offset, rc = 0, depth = -1;
	/* 若设备树不存在则返回 */
	if (!blob)
		return 0;
         /* 从根节点开始遍历 */
	for (offset = fdt_next_node(blob, -1, &depth);
	     /* 如果找到了有效的节点并且回调函数it返回0,则执行循环体 */
	     offset >= 0 && depth >= 0 && !rc;
	     /* 继续遍历下一个节点 */
	     offset = fdt_next_node(blob, offset, &depth)) {
		
		/* 获取节点名 */
		pathp = fdt_get_name(blob, offset, NULL);
		/* 对于老版本的设备树,得到的是节点的路径名,因此要去掉多余的前缀 */
		/* 不过fdt_get_name已经考虑过这个问题了,这里有点多余 */
		if (*pathp == '/')
			pathp = kbasename(pathp);
		/*
			调用回调函数it
			offset: 节点起始位置在DTB的structure block中的偏移
			pathp : 指向节点名
			depth : 节点的深度(层次)
			data  : 参数data,取决于调用者
		*/
		rc = it(offset, pathp, depth, data);
	}
	return rc;
}

不难看出,该函数只是一个遍历设备树节点的工具函数:遍历设备树节点,调用回调函数,如果回调函数判断该节点就是想要解析的节点,则进行相应的解析操作,并返回非0值,以指示该函数停止遍历动作。

解析/chosen节点
根据函数名字,以及3.1节的分析,猜也应该猜到传入的回调函数early_init_dt_scan_chosen用于解析/chosen节点:

/*
	offset: 节点起始位置在DTB的structure block中的偏移
	pathp : 指向节点名
	depth : 节点的深度(层次)
	data  : boot_command_line,一个字符数组
*/
int __init early_init_dt_scan_chosen(unsigned long node, const char *uname,
				     int depth, void *data)
{
	int l;
	const char *p;
	const void *rng_seed;

	pr_debug("search \"chosen\", depth: %d, uname: %s\n", depth, uname);
	/* 如果遍历到的不是作为根节点的子节点的chosen节点,则指示of_scan_flat_dt继续遍历下一个节点 */
	if (depth != 1 || !data ||
	    (strcmp(uname, "chosen") != 0 && strcmp(uname, "chosen@0") != 0))
		return 0;
	
	/* 当前节点是/chosen节点 */
	/* 解析/chosen节点的initrd属性,设置全局变量phys_initrd_start和phys_initrd_size */
	early_init_dt_check_for_initrd(node);

	/* 获取/chosen节点的bootargs属性的属性值 */
	p = of_get_flat_dt_prop(node, "bootargs", &l);
	/* 如果属性存在,则p指向bootargs属性值——一个字符串,l记录了字符串的长度(含'\0') */
	if (p != NULL && l > 0)
		/* 将启动参数拷贝到boot_command_line */
		strlcpy(data, p, min(l, COMMAND_LINE_SIZE));

	/*
	 * CONFIG_CMDLINE配置项意味着如果u-boot传递的参数不含启动参数,那么
	 * CONFIG_CMDLINE就是默认的启动参数。如果含有启动参数,那么,是追加
	 * 还是覆盖已有的启动参数,取决于另外两个配置项CONFIG_CMDLINE_EXTEND
	 * 和CONFIG_CMDLINE_FORCE。
	 */
#ifdef CONFIG_CMDLINE
#if defined(CONFIG_CMDLINE_EXTEND)
	strlcat(data, " ", COMMAND_LINE_SIZE);
	strlcat(data, CONFIG_CMDLINE, COMMAND_LINE_SIZE);
#elif defined(CONFIG_CMDLINE_FORCE)
	strlcpy(data, CONFIG_CMDLINE, COMMAND_LINE_SIZE);
#else
	/* 如果DTB不带有启动参数,就使用kernel的启动参数——CONFIG_CMDLINE */
	if (!((char *)data)[0])
		strlcpy(data, CONFIG_CMDLINE, COMMAND_LINE_SIZE);
#endif
#endif /* CONFIG_CMDLINE */

	pr_debug("Command line is: %s\n", (char*)data);
	
	/* 对rng-seed节点的解析,暂时不清楚这个东西 */
	rng_seed = of_get_flat_dt_prop(node, "rng-seed", &l);
	if (rng_seed && l > 0) {
		......
	}

	/* 返回非0值,指示of_scan_flat_dt停止遍历 */
	return 1;
}

解析根节点的{size,address}-cells属性
在解析/memory节点之前,应该先得到根节点的{size,address}-cells属性值,因为/memory节点使用reg属性来存放内存的起始地址和长度,而解析reg属性少不了{size,address}-cells。

int __init early_init_dt_scan_root(unsigned long node, const char *uname,
				   int depth, void *data)
{
	const __be32 *prop;
	/* 验证当前节点是否是根节点 */
	if (depth != 0)
		return 0;
	/* 设置默认值 */
	dt_root_size_cells = OF_ROOT_NODE_SIZE_CELLS_DEFAULT;
	dt_root_addr_cells = OF_ROOT_NODE_ADDR_CELLS_DEFAULT;
	/* 如果有#size-cells属性则获取其值,并重新设置dt_root_size_cells */
	prop = of_get_flat_dt_prop(node, "#size-cells", NULL);
	if (prop)
		/* 注意大小端的转换 */
		dt_root_size_cells = be32_to_cpup(prop);
	pr_debug("dt_root_size_cells = %x\n", dt_root_size_cells);
	
	/* 如果存在#address-cells属性,则重新设置dt_root_addr_cells */
	prop = of_get_flat_dt_prop(node, "#address-cells", NULL);
	if (prop)
		dt_root_addr_cells = be32_to_cpup(prop);
	pr_debug("dt_root_addr_cells = %x\n", dt_root_addr_cells);

	/* 停止遍历 */
	return 1;
}

解析/memory节点
是时候解析/memory节点了:

int __init early_init_dt_scan_memory(unsigned long node, const char *uname,
				     int depth, void *data)
{
	/* 获取/memory节点的device_type属性 */
	const char *type = of_get_flat_dt_prop(node, "device_type", NULL);
	const __be32 *reg, *endp;
	int l;
	bool hotpluggable;

	/* /memory节点的device_type属性值必须是memory */
	if (type == NULL || strcmp(type, "memory") != 0)
		return 0;
	
	/* 获取/memory节点的linux,usable-memory或reg属性值(存放了内存的起始地址和长度信息) */
	reg = of_get_flat_dt_prop(node, "linux,usable-memory", &l);
	if (reg == NULL)
		reg = of_get_flat_dt_prop(node, "reg", &l);
	if (reg == NULL)
		return 0;

	endp = reg + (l / sizeof(__be32));
	/* 获取hotpluggable属性值(指示是否可以热插拔) */
	hotpluggable = of_get_flat_dt_prop(node, "hotpluggable", NULL);

	pr_debug("memory scan node %s, reg size %d,\n", uname, l);

	/* 遍历reg属性记录的一块或多块内存 */
	while ((endp - reg) >= (dt_root_addr_cells + dt_root_size_cells)) {
		u64 base, size;
		/* 获取当前内存块的起始地址 */
		base = dt_mem_next_cell(dt_root_addr_cells, &reg);
		size = dt_mem_next_cell(dt_root_size_cells, &reg);

		if (size == 0)
			continue;
		pr_debug(" - %llx ,  %llx\n", (unsigned long long)base,
		    (unsigned long long)size);
		/* 对base和size进行一系列校验后,调用memblock_add添加内存块(struct memblock) */
		early_init_dt_add_memory_arch(base, size);

		if (!hotpluggable)
			continue;
		/* 若当前内存块可以热插拔,那么标记之 */
		if (early_init_dt_mark_hotplug_memory_arch(base, size))
			pr_warn("failed to mark hotplug range 0x%llx - 0x%llx\n",
				base, base + size);
	}

	return 0;
}

至此,内核对运行时配置信息的处理就介绍完了。
在这里插入图片描述

5.4内核对设备树中设备信息的处理

内核对DTB所在内存的处理
内核会保留DTB所占据的内存区域,因此DTB文件中的数据在kernel启动后也是可用的,相关源码的调用路径如下:
在这里插入图片描述
struct device_node和struct property
DTB文件的structure block区域记录了很多设备节点,每个节点又有很多属性,对此,kernel使用struct device_node来描述节点,使用struct property来描述属性。下面就介绍一下这两个结构的主要成员(不是全部成员):

struct device_node:

成员含义
name指向节点的name属性的属性值(字符串),位于DTB的structure block
phandle节点的唯一的数字标识符
full_name指向节点名字符串,该字符串紧跟着结构体本身
properties指向节点的属性
deadprops指向被移除的属性
parent指向父节点
child指向孩子节点
sibling指向兄弟节点

struct property:

成员含义
name指向属性名字符串,位于DTB的strings block
length属性的长度
valuevoid *类型,指向属性值,位于DTB的structure block
next一个节点的所有属性构成一个链表

从DTB到struct device_node示例
kernel调用unflatten_device_tree函数,将DTB文件中的设备节点转换为一个个的struct device_node,这些结构体有着树状的层次,在分析相关源码之前,我们不妨先看一个设备树完成转换之后的结果,建立起总体上的认知。

首先给出一个设备树文件的示例,这个设备树文件并不完整,只是用作示例:

/ {
	model = "SMDK2416";
	compatible = "samsung,s3c2416";
	#address-cells = <1>;
	#size-cells = <1>;

	memory@30000000 {
		device_type = "memory";
		reg =  <0x30000000 0x4000000>;
	};

	pinctrl@56000000 {
		name = "example_name";
		compatible = "samsung,s3c2416-pinctrl";
	};
};

该设备树文件被DTC编译为DTB之后,被u-boot传递给kernel,然后内核读取其节点信息,建立如下的由device_node构成的树状结构:
在这里插入图片描述
值得一提的是,为了突出device_node的name成员和full_name成员的差别,在上图中,我将没有name属性的节点的name成员置为,这源于:

static bool populate_node(const void *blob,
			  int offset,
			  void **mem,
			  struct device_node *dad,
			  struct device_node **pnp,
			  bool dryrun)
{
	......
	populate_properties(blob, offset, mem, np, pathp, dryrun);
	if (!dryrun) {
		np->name = of_get_property(np, "name", NULL);
		if (!np->name)
			/* 没有name属性,则name成员置为"<NULL>" */
			np->name = "<NULL>";
	}
	......
}

但实际上,populate_properties函数会为没有name属性的节点创建name属性:

static void populate_properties(const void *blob,
				int offset,
				void **mem,
				struct device_node *np,
				const char *nodename,
				bool dryrun)
{
	struct property *pp, **pprev = NULL;
	int cur;
	bool has_name = false;

	pprev = &np->properties;
	for (cur = fdt_first_property_offset(blob, offset);
	     cur >= 0;
	     cur = fdt_next_property_offset(blob, cur)) {
		......
		if (!strcmp(pname, "name"))
			has_name = true;
		......
	}

	/* With version 0x10 we may not have the name property,
	 * recreate it here from the unit name if absent
	 */
	if (!has_name) {
		/* 为没有name属性的节点创建name属性 */
		......
	}
	......
}

unflatten_device_tree分析
有了上文的铺垫,我们就可以开始从源码的层面分析kernel如何根据DTB构建device_node树:

void __init unflatten_device_tree(void)
{
	/* 根据DTB构建device_node树 */
	__unflatten_device_tree(initial_boot_params, NULL, &of_root,
				early_init_dt_alloc_memory_arch, false);

	/*
		设置of_aliases指向/aliases节点对应的device_node
		设置of_chosen指向/chosen节点对应的device_node
		
		对于of_chosen:
			从其属性中找到属性名为stdout-path或linux,stdout-path的属性的属性值,
			并根据该属性值获得标准输出设备对应的device_node,将其赋给of_stdout
		
		对于of_aliases:
			遍历其属性,跳过name、phandle、linux,phandle,对于其他的属性,如果
			该属性的属性值指示了一个device_node,那么为这个device_node创建一个
			struct alias_prop,并添加到aliases_lookup链表。

			举一个例子来说,假设一个别名属性为i2c1 = "xxx",并且"xxx"指示了一个
			device_node,那么为其创建的alias_prop的np成员指向相应的device_node;
			id成员为1,零长数组stem指向字符串"i2c"(数字部分作为id去掉了)。
	*/
	of_alias_scan(early_init_dt_alloc_memory_arch);
	
	/* 看名字是用作测试的,具体不清楚 */
	unittest_unflatten_overlay_base();
}

再看__unflatten_device_tree:

void *__unflatten_device_tree(const void *blob,
			      struct device_node *dad,
			      struct device_node **mynodes,
			      void *(*dt_alloc)(u64 size, u64 align),
			      bool detached)
{
	int size;
	void *mem;

	/* 打印一些信息并校验DTB文件 */
	......

	/* 第一次调用unflatten_dt_nodes,计算整个device_node树包括属性所需的全部内存 */
	size = unflatten_dt_nodes(blob, NULL, dad, NULL);
	if (size < 0)
		return NULL;
	
	size = ALIGN(size, 4);
	pr_debug("  size is %d, allocating...\n", size);

	/* 为整个device_node树申请内存 */
	mem = dt_alloc(size + 4, __alignof__(struct device_node));
	if (!mem)
		return NULL;
	/* 将这段内存清0 */
	memset(mem, 0, size);
	/* 在这段内存的末尾填充0xdeadbeef(大端模式) */
	*(__be32 *)(mem + size) = cpu_to_be32(0xdeadbeef);

	pr_debug("  unflattening %p...\n", mem);

	/* 真正创建device_node树 */
	unflatten_dt_nodes(blob, mem, dad, mynodes);
	
	/* 根据之前在mem内存末尾填充的内容检查有没有踩内存 */
	if (be32_to_cpup(mem + size) != 0xdeadbeef)
		pr_warning("End of tree marker overwritten: %08x\n",
			   be32_to_cpup(mem + size));
	
	if (detached && mynodes) {
		/* 为当前创建的device_node树打上OF_DETACHED标记 */
		of_node_set_flag(*mynodes, OF_DETACHED);
		pr_debug("unflattened tree is detached\n");
	}

	pr_debug(" <- unflatten_device_tree()\n");
	return mem;
}

__unflatten_device_tree函数调用进一步调用unflatten_dt_nodes创建device_node树,具体过程是遍历DTB的各个节点,对每个节点调用populate_node申请并填充之(节点的属性也会在该函数中被构建)。过程很好理解,就不再继续跟踪了。

将device_node转换成platform_device
哪些节点需要转换成platform_device
首先,我们需要知道,在linux中什么样的设备是platform_device,内核文档里是这么说的:

平台设备包括基于旧端口的设备和接到到外围总线的主机桥,以及大多数集成到片上系统平台的控制器(如i2c控制器)。它们通常的共同点是从CPU总线直接寻址(对arm来说,这些设备的寄存器位于CPU的寻址空间,CPU可以像访存一样访问设备的寄存器)。

举例来说,i2c控制器是platform_device,但是连接在SoC的i2c总线上的i2c设备,比如一个i2c接口的EEPROM就不是platform_device。在设备树中,通常i2c控制器对应的设备节点作为连接该i2c控制器的片外外设的父节点,如:

i2c0: i2c@7f004000 {
			compatible = "samsung,s3c2440-i2c";
			reg = <0x7f004000 0x1000>;
			interrupt-parent = <&vic1>;
			interrupts = <18>;
			clock-names = "i2c";
			clocks = <&clocks PCLK_IIC0>;
			#address-cells = <1>;
			#size-cells = <0>;
			pinctrl-names = "default";
			pinctrl-0 = <&i2c0_bus>;
			status = "okay";
			/* 连接到i2c@7f004000的外设——AT24C08 */
			eeprom@50 {
				compatible = "atmel,24c08";
				reg = <0x50>;
				pagesize = <16>;
			};
		};

对platform_device有了一定的了解之后,我们再对需要转换成platform_device的设备节点做一个总结:

含有compatible属性的根节点的子节点;
或者,compatibe属性值为"simple-bus"、“simple-mfd”、“isa”、"arm,amba-bus"的节点的含有compatible属性的子节点。

在哪里做的转换工作
位于drivers/of/platform.c的of_platform_default_populate_init函数负责为合适的设备节点构建platform_device。该函数的调用比较隐晦,这里简单介绍一下:

arch_initcall_sync(of_platform_default_populate_init);

#define arch_initcall_sync(fn)		__define_initcall(fn, 3s)

#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)

#define ___define_initcall(fn, id, __sec) \
	static initcall_t __initcall_##fn##id __used \
		__attribute__((__section__(#__sec ".init"))) = fn;

内核使用上述的宏,定义了一个函数指针,指向of_platform_default_populate_init,通过链接脚本将该函数指针保存到一个名为.initcall3s.init的节。实际上,该节保存了一系列的初始化函数。链接脚本中的相关部分如下:

__initcall3_start = .;
KEEP(*(.initcall3.init))
KEEP(*(.initcall3s.init))
__initcall4_start = .;

kernel中,名为do_initcalls的函数会遍历这些存有初始化函数指针的节,逐个取出函数指针,并调用相应的初始化函数:

static void __init do_initcalls(void)
{
	int level;
	/* 遍历各个初始化函数指针所在的节,并调用初始化函数 */
	for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
		do_initcall_level(level);
}

最后在提一下do_initcalls的调用点:
在这里插入图片描述

浅析转换的过程
本节将分析一下of_platform_default_populate_init的源码,探究一下从device_node到platform_device的细节:

static int __init of_platform_default_populate_init(void)
{
	struct device_node *node;
	/* 根据of_root是否为NULL来判断device_node是不是都创建好了 */
	if (!of_have_populated_dt())
		return -ENODEV;

	/* 为一些含有特殊的compatible属性值的节点构建platform_device(不是所有平台都有) */
	for_each_matching_node(node, reserved_mem_matches)
		of_platform_device_create(node, NULL, NULL);
	
	/* 为/firmware节点构建platform_device */
	node = of_find_node_by_path("/firmware");
	if (node) {
		of_platform_populate(node, NULL, NULL, NULL);
		of_node_put(node);
	}

	/* 上面是对特殊节点的处理,这里才是为大部分节点构建platform_device的函数 */
	of_platform_default_populate(NULL, NULL, NULL);

	return 0;
}

of_platform_default_populate只是of_platform_populate的简单包装,因此我们直接看后者:

/*
	root    = NULL
	matches = of_default_bus_match_table
	lookup  = NULL
	parent  = NULL
*/
int of_platform_populate(struct device_node *root,
			const struct of_device_id *matches,
			const struct of_dev_auxdata *lookup,
			struct device *parent)
{
	struct device_node *child;
	int rc = 0;
	/* 传入的root为NULL,因此这里执行of_find_node_by_path("/")获得根节点 */
	root = root ? of_node_get(root) : of_find_node_by_path("/");
	if (!root)
		return -EINVAL;

	pr_debug("%s()\n", __func__);
	pr_debug(" starting at: %pOF\n", root);
	/* 遍历根节点的每个孩子节点 */
	for_each_child_of_node(root, child) {
		/* 为根节点的孩子节点创建platform_device(不是所有孩子节点,需要符合一定的条件) */
		rc = of_platform_bus_create(child, matches, lookup, parent, true);
		if (rc) {
			of_node_put(child);
			break;
		}
	}
	/* 为根节点设置OF_POPULATED_BUS,标志着已经为其孩子节点创建完platform_device */
	of_node_set_flag(root, OF_POPULATED_BUS);

	of_node_put(root);
	return rc;
}

再看of_platform_bus_create:

/*
	bus     : 该节点可能需要创建platform_device
	matches : 如果bus节点的compatible属性能和matches匹配上,说明其孩子节点也要创建platform_device(比如compatible的值为"simple-bus")
	lookup  : 如果bus节点的compatible属性匹配lookup数组,那么相应的paltform_device的device.kobj.name设置为lookup数组中匹配元素的name(不是同一块内存)
	parent  : 创建的paltform_device的device.parent = parent(device.kobj.parent = &device.parent.kobj)
	strict  : bus节点是否一定要具备compatibile属性
*/
static int of_platform_bus_create(struct device_node *bus,
				  const struct of_device_id *matches,
				  const struct of_dev_auxdata *lookup,
				  struct device *parent, bool strict)
{
	const struct of_dev_auxdata *auxdata;
	struct device_node *child;
	struct platform_device *dev;
	const char *bus_id = NULL;
	void *platform_data = NULL;
	int rc = 0;

	/* 如果要求必须有compatible属性,那么对于没有该属性的节点直接返回 */
	if (strict && (!of_get_property(bus, "compatible", NULL))) {
		pr_debug("%s() - skipping %pOF, no compatible prop\n",
			 __func__, bus);
		return 0;
	}

	/* 跳过由of_skipped_node_table指定的节点,这些节点不用创建platform_device */
	if (unlikely(of_match_node(of_skipped_node_table, bus))) {
		pr_debug("%s() - skipping %pOF node\n", __func__, bus);
		return 0;
	}
	/* 已经为该节点及其孩子节点创建过platform_device,则返回 */
	if (of_node_check_flag(bus, OF_POPULATED_BUS)) {
		pr_debug("%s() - skipping %pOF, already populated\n",
			__func__, bus);
		return 0;
	}
	/* 查找lookup数组中是否有匹配的数组项,如果有则取其name和platform_data用于后面的创建platform_device */
	auxdata = of_dev_lookup(lookup, bus);
	if (auxdata) {
		bus_id = auxdata->name;
		platform_data = auxdata->platform_data;
	}
	/* 处理特殊的节点(兼容就版本的设备树) */
	if (of_device_is_compatible(bus, "arm,primecell")) {
		/*
		 * Don't return an error here to keep compatibility with older
		 * device tree files.
		 */
		of_amba_device_create(bus, bus_id, platform_data, parent);
		return 0;
	}
	
	/* 为bus节点创建platform_device */
	dev = of_platform_device_create_pdata(bus, bus_id, platform_data, parent);

	/* 如果bus节点的compatible属性比较特殊,比如是"simple-bus",则需要尝试为其子节点创建platform_device */
	if (!dev || !of_match_node(matches, bus))
		return 0;
	
	for_each_child_of_node(bus, child) {
		pr_debug("   create child: %pOF\n", child);
		/* 整个过程是递归进行的 */
		rc = of_platform_bus_create(child, matches, lookup, &dev->dev, strict);
		if (rc) {
			of_node_put(child);
			break;
		}
	}
	of_node_set_flag(bus, OF_POPULATED_BUS);
	return rc;
}

函数of_platform_device_create_pdata会调用of_device_alloc以及of_device_add创建并注册platform_device,具体的就不再跟踪了。

4.5.4 如何处理平台设备对应的设备节点的子节点
上文已经说过,对于compatible属性为simple-bus等特殊值的节点,kernel也会为其含有compatible属性的子节点创建platform_device。那么对于其他设备节点呢,怎么处理它们的子节点?所谓知子莫若父,它们的子节点应该交由父节点(也就是创建了platform_device的节点)来处理。仍以4.5.1节中i2c的例子来说明。

kernel为该节点创建platform_device后,将其注册到platform_bus_type,根据kernel的总线-设备-驱动模型,如果该节点的compatible属性samsung,s3c2410-i2c,匹配总线上的某个platform_driver,那么该驱动的probe函数会被调用,在该函数中,会为SoC的i2c控制器创建i2c_adapter,也会为连接在该控制器上的i2c接口的外设eeprom@50创建i2c_client。具体的函数调用过程放在4.6节,这里就不再多说了。

小结:
在这里插入图片描述

5.5 内核中设备树相关头文件的总结

kernel源码树下的include/linux目录下存在着一些以of开头的头文件,这些头文件内是一些与设备树相关的函数的声明。下面将对这些头文件做一个分类。

声明处理DTB文件的函数的头文件

头文件内容
of_fdt.h声明了dtb文件的相关操作函数,一般用不到,因为dtb文件在内核中被转换为device_node树,后者更易于使用

处理device_node的函数的头文件

头文件内容
of.h提供设备树的一般处理函数,如 of_property_read_u32(读取某个属性的u32值)
of_address.h地址相关的函数,如 of_get_address(获得reg属性中的addr、size值)
of_dma.h处理设备树中DMA相关属性的函数
of_gpio.hGPIO相关的函数
of_graph.hGPU相关驱动中用到的函数,从设备树中获得GPU信息
of_iommu.h暂不清楚
of_irq.h中断相关的函数
of_mdio.hMDIO (Ethernet PHY) API
of_net.hOF helpers for network devices
of_pci.hPCI相关函数
of_pdt.h暂不清楚
of_reserved_mem.h设备树中reserved_mem相关的函数

声明处理platform_device的函数的头文件

头文件内容
of_platform.h声明了把device_node转换为platform_device时用到的函数
of_device.h主要声明了struct device相关的函数,如 of_match_device

5.6设备树在文件系统中的表示

/sys与设备树
所有设备树的信息存放于/sys/firmware目录下:

目录/文件含义
/sys/firmware/fdt该文件表示原始DTB文件,可用hexdump -C /sys/firmware/fdt查看
/sys/firmware/devicetree以目录结构呈现设备树,每个device_node对应一个目录,每个属性对应节点目录下的一个文件吗,比如根节点对应base目录,该目录下有compatible等文件

所有的platform_device会在/sys/devices/platform下对应一个目录,这些platform_device有来自设备树的,也有来自.c文件中手工注册的。由kernel根据设备树创建的platform_device对应的目录下存在一个名为of_node的软链接,链接向该platform_device对应的device_node对应的目录。

/proc与设备树
/proc/device-tree作为链接文件指向/sys/firmware/devicetree/base。

设备树常用 OF 操作函数

设备树描述了设备的详细信息,这些信息包括数字类型的、字符串类型的、数组类型的,我们在编写驱动的时候需要获取到这些信息。

Linux 内核给我们提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_”,所以在很多资料里面也被叫做 OF 函数。这些 OF 函数原型都定义在 include/linux/of.h 文件中。

查找节点的 OF 函数

of_find_node_by_name 函数
of_find_node_by_type 函数
of_find_compatible_node 函数
of_find_matching_node_and_match 函数
of_find_node_by_path 函数

查找父/子节点的 OF 函数

of_get_parent 函数2、of_get_next_child 函数

提取属性值的 OF 函数

1、of_find_property 函数
2、of_property_count_elems_of_size 函数
3、of_property_read_u32_index 函数
4、 
  of_property_read_u8_array 函数
  of_property_read_u16_array 函数
  of_property_read_u32_array 函数
  of_property_read_u64_array 函数
5、
  of_property_read_u8 函数
  of_property_read_u16 函数
  of_property_read_u32 函数
  of_property_read_u64 函数
6、of_property_read_string 函数
7、of_n_addr_cells 函数
8、of_n_size_cells 函数

其他常用的 OF 函数

1、of_device_is_compatible 函数
2、of_get_address 函数
3、of_translate_address 函数
4、of_address_to_resource 函数
5、of_iomap 函数

1、.dtsi 头文件
和 C 语言一样,设备树也支持头文件,设备树的头文件扩展名为.dtsi。与此同时,.dts 文件也可以引用 C 语言中的.h 文件,甚至也可以引用.dts 文件。

一般.dtsi 文件用于描述 SOC 的内部外设信息,比如 CPU 架构、主频、外设寄存器地址范围,比如 UART、IIC 等等。

2、设备节点
设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点,每个节点都通过一些属性信息来描述节点信息,属性就是键—值对。

/ {
aliases {
can0 = &flexcan1;
};

cpus {
#address-cells = <1>;
#size-cells = <0>;

cpu0: cpu@0 {
compatible = “arm,cortex-a7”;
device_type = “cpu”;
reg = <0>;
};
};

intc: interrupt-controller@00a01000 {
compatible = “arm,cortex-a7-gic”;
#interrupt-cells = <3>;
interrupt-controller;
reg = <0x00a01000 0x1000>,
<0x00a02000 0x100>;
};
}
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
“/”是根节点,每个设备树文件只有一个根节点。

aliases、cpus 和 intc 是三个子节点,在设备树中节点命名格式如下:

node-name@unit-address
1.
“node-name”是节点名字,为 ASCII 字符串,节点名字应该能够清晰的描述出节点的功能,比如“uart1”就表示这个节点是 UART1 外设。“unit-address”一般表示设备的地址或寄存器首地址,如果某个节点没有地址或者寄存器的话“unit-address”可以不要,比如“cpu@0”、“interrupt-controller@00a01000”。

另一种格式:

label: node-name@unit-address
1.
引入 label 的目的就是为了方便访问节点,可以直接通过&label 来访问这个节点,比如通过&cpu0 就可以访问“cpu@0”这个节点,而不需要输入完整的节点名字。

每个节点都有不同属性,不同的属性又有不同的内容,属性都是键值对,值可以为空或任意的字节流。设备树源码中常用的几种数据形式如下所示:

①、字符串

compatible = “arm,cortex-a7”;
1.
上述代码设置 compatible 属性的值为字符串“arm,cortex-a7”。

②、32 位无符号整数

reg = <0>;
1.
上述代码设置 reg 属性的值为 0,reg 的值也可以设置为一组值,比如:

reg = <0 0x123456 100>;
1.
③、字符串列表

属性值也可以为字符串列表,字符串和字符串之间采用“,”隔开,如下所示:

compatible = “fsl,imx6ull-gpmi-nand”, “fsl, imx6ul-gpmi-nand”;
1.
3、标准属性(8个)
节点是由一堆的属性组成,节点都是具体的设备,不同的设备需要的属性不同,用户可以自定义属性。除了用户自定义属性,有很多属性是标准属性,Linux 下的很多外设驱动都会使用这些标准属性,几个常用的标准属性:

compatible 属性
compatible 属性也叫做“兼容性”属性,这是非常重要的一个属性!compatible 属性的值是一个字符串列表,compatible 属性用于将设备和驱动绑定起来。字符串列表用于选择设备所要使用的驱动程序,compatible 属性的值格式如下所示:

“manufacturer,model”
1.
manufacturer 表示厂商,model 一般是模块对应的驱动名字。

比如 imx6ull-alientek-emmc.dts 中 sound 节点是 I.MX6U-ALPHA 开发板的音频设备节点,I.MX6U-ALPHA 开发板上的音频芯片采用的欧胜(WOLFSON)出品的 WM8960,sound 节点的 compatible 属性值如下:

compatible = “fsl,imx6ul-evk-wm8960”,“fsl,imx-audio-wm8960”;
1.
“fsl”表示厂商是飞思卡尔,“imx6ul-evk-wm8960”和“imx-audio-wm8960”表示驱动模块名字。sound这个设备首先使用第一个兼容值在 Linux 内核里面查找,看看能不能找到与之匹配的驱动文件,如果没有找到的话就使用第二个兼容值查。

model 属性
model 属性值也是一个字符串,一般 model 属性描述设备模块信息,比如名字什么的,比如:

model = “wm8960-audio”;
1.
status 属性
status 属性是和设备状态有关的,status 属性值也是字符串,字符串是设备的状态信息。

#address-cells 和#size-cells 属性
这两个属性的值都是无符号 32 位整形,#address-cells 和#size-cells 这两个属性可以用在任何拥有子节点的设备中,用于描述子节点的地址信息。#address-cells 属性值决定了子节点 reg 属性中地址信息所占用的字长(32 位),#size-cells 属性值决定了子节点 reg 属性中长度信息所占的字长(32 位)。#address-cells 和#size-cells 表明了子节点应该如何编写 reg 属性值,一般 reg 属性都是和地址有关的内容,和地址相关的信息有两种:起始地址和地址长度,reg 属性的格式一为:

reg = <address1 length1 address2 length2 address3 length3……>
1.
每个“address length”组合表示一个地址范围,其中 address 是起始地址,length 是地址长度,#address-cells 表明 address 这个数据所占用的字长,#size-cells 表明 length 这个数据所占用的字长。比如:

spi4 {
compatible = “spi-gpio”;
#address-cells = <1>;
#size-cells = <0>;

gpio_spi: gpio_spi@0 {
compatible = “fairchild,74hc595”;
reg = <0>;
};
};

aips3: aips-bus@02200000 {
compatible = “fsl,aips-bus”, “simple-bus”;
#address-cells = <1>;
#size-cells = <1>;

dcp: dcp@02280000 {
compatible = "fsl,imx6sl-dcp";
reg = <0x02280000 0x4000>;
};

};
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
节点 spi4 的#address-cells = <1>,#size-cells = <0>,说明 spi4 的子节点 reg 属性中起始地址所占用的字长为 1,地址长度所占用的字长为 0。

子节点 gpio_spi: gpio_spi@0 的 reg 属性值为 <0>,因为父节点设置了#address-cells = <1>,#size-cells = <0>,因此 addres=0,没有 length 的值,相当于设置了起始地址,而没有设置地址长度。

设置 aips3: aips-bus@02200000 节点#address-cells = <1>,#size-cells = <1>,说明 aips3: aips-bus@02200000 节点起始地址长度所占用的字长为 1,地址长度所占用的字长也为 1。

子节点 dcp: dcp@02280000 的 reg 属性值为<0x02280000 0x4000>,因为父节点设置了#address-cells = <1>,#size-cells = <1>,address= 0x02280000,length= 0x4000,相当于设置了起始地址为 0x02280000,地址长度为 0x40000。

reg 属性
reg 属性前面已经提到过了,reg 属性的值一般是(address,length)对。reg 属性一般用于描述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息。

ranges 属性
ranges属性值可以为空或者按照(child-bus-address,parent-bus-address,length)格式编写的数字矩阵,ranges 是一个地址映射/转换表,ranges 属性每个项目由子地址、父地址和地址空间长度这三部分组成:

child-bus-address:子总线地址空间的物理地址,由父节点的#address-cells 确定此物理地址所占用的字长。

parent-bus-address:父总线地址空间的物理地址,同样由父节点的#address-cells 确定此物理地址所占用的字长。

length:子地址空间的长度,由父节点的#size-cells 确定此地址长度所占用的字长。

如果 ranges 属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换,

name 属性
name 属性值为字符串,name 属性用于记录节点名字,name 属性已经被弃用,不推荐使用name 属性,一些老的设备树文件可能会使用此属性。

device_type 属性
device_type 属性值为字符串,IEEE 1275 会用到此属性,用于描述设备的 FCode,但是设备树没有 FCode,所以此属性也被抛弃了。此属性只能用于 cpu 节点或者 memory 节点。

当然还有一些其他内部属性,比如 interrupt 等属性。

4、特殊节点
在根节点“/”中有两个特殊的子节点:aliases 和 chosen。

单词 aliases 的意思是“别名”,因此 aliases 节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。不过我们一般会在节点命名的时候会加上 label,然后通过&label来访问节点,这样也很方便,而且设备树里面大量的使用&label 的形式来访问节点。

chosen 并不是一个真实的设备,chosen 节点主要是为了 uboot 向 Linux 内核传递数据,重点是 bootargs 参数。一般.dts 文件中 chosen 节点通常为空或者内容很少。

5、绑定信息文档
设备树是用来描述板子上的设备信息的,不同的设备其信息不同,反映到设备树中就是属性不同。那么我们在设备树中添加一个硬件对应的节点的时候从哪里查阅相关的说明呢?在Linux 内核源码中有详细的.txt 文档描述了如何添加节点,这些.txt 文档叫做绑定文档,路径为:Linux 源码目录/Documentation/devicetree/bindings。

有时候使用的一些芯片在 Documentation/devicetree/bindings 目录下找不到对应的文档,这个时候就要咨询芯片的提供商,让他们给你提供参考的设备树文件。

6、设备树常用 OF 操作函数
设备树描述了设备的详细信息,这些信息包括数字类型的、字符串类型的、数组类型的,我们在编写驱动的时候需要获取到这些信息。

Linux 内核给我们提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_”,所以在很多资料里面也被叫做 OF 函数。这些 OF 函数原型都定义在 include/linux/of.h 文件中。

查找节点的 OF 函数
1、of_find_node_by_name 函数
2、of_find_node_by_type 函数
3、of_find_compatible_node 函数
4、of_find_matching_node_and_match 函数
5、of_find_node_by_path 函数
1.
2.
3.
4.
5.
查找父/子节点的 OF 函数
1、of_get_parent 函数2、of_get_next_child 函数
1.
提取属性值的 OF 函数
1、of_find_property 函数
2、of_property_count_elems_of_size 函数
3、of_property_read_u32_index 函数
4、
of_property_read_u8_array 函数
of_property_read_u16_array 函数
of_property_read_u32_array 函数
of_property_read_u64_array 函数
5、
of_property_read_u8 函数
of_property_read_u16 函数
of_property_read_u32 函数
of_property_read_u64 函数
6、of_property_read_string 函数
7、of_n_addr_cells 函数
8、of_n_size_cells 函数
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
其他常用的 OF 函数
1、of_device_is_compatible 函数
2、of_get_address 函数
3、of_translate_address 函数
4、of_address_to_resource 函数
5、of_iomap 函数
1.
2.
3.
4.
5.
关于OF函数的用法太多了,这里就不介绍了,大家百度即可。

关于设备树就讲解到这里,关于设备树我们重点要了解一下几点内容:

①、DTS、DTB 和 DTC 之间的区别,如何将.dts 文件编译为.dtb 文件。

②、设备树语法,这个是重点,因为在实际工作中我们是需要修改设备树的。

③、设备树的几个特殊子节点。

④、关于设备树的 OF 操作函数,也是重点,因为设备树最终是被驱动文件所使用的,而驱动文件必须要读取设备树中的属性信息,比如内存信息、GPIO 信息、中断信息等等。要想在驱动中读取设备树的属性值,那么就必须使用 Linux 内核提供的众多的 OF 函数。

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值