一.设备树是什么

1.设备树的概念

​ 设备树是由DTSpec规范定义的一个描述系统硬件的数据结构。加载程序加载一个设备树到客户端程序的内存中并且传递指向该设备树的指针给客户端程序。客户端程序通过设备树指针获取系统硬件参数。

2.使用设备树的意义

​ 由于各个芯片厂家的硬件平台差异,导致在linux内核平台架构目录下充斥着大量的厂家平台代码,导致内核冗余代码多,移植复杂等诸多问题,设备树的引入使得内核和硬件资源的描述相对独立。精简了平台代码,简化了驱动开发和移植。

3.设备树文件

​ 设备树有设备树源码(dts和dtsi)文件和二进制的设备树(.dtb)文件。设备树以二进制的形式加载到内存中供客户端程序使用。设备树的二进制文件由设备树源码文件通过设备树编译器(dtc)编译而成。符合DTSpec的设备树描述那些系统中不能被客户端程序动态检测到的设备信息。比如,PCI架构允许客户端检测附加设备,因此可能不需要描述PCI设备的设备树节点,然而需要一个描述系统中的PCI主控制器的设备节点如果该控制器不能被检测到。以下的例子描述了一个简单的具有platform type,cpu,memory,和一个uart的设备树,它基本可以启动一个简单的操作系统。该设备树的每个节点的属性和值都在节点内部。

一个简单的设备树文件

二.DTS(Devicetree Source)的格式

1.DTS文件布局(Version 1)

1
2
3
4
5
6
/dts-v1/;
[memory reservations]
/{
[property definitions]
[child nodes]
};

/dts-v1/用来描述设备树的版本为V1(设备树没有指定版本的这一行的话,设备树将默认按V0版本编译,除了其他小的但不兼容的更改外,该版本对整数使用不同的格式。)。

Memory reservations定义了设备树中保留内存表的入口,格式如下:

1
/memreserve/ <address> <length>

/{};定义了设备树的根节点。

设备树支持C风格(/ /)和C++风格(//)的注释方式。

设备树可以包含dtsi文件或头文件,dtsi文件本身也可以包含其他文件。

2.Labels

​ 设备树源文件的任何节点或属性值都支持label,使用label代替绝对路径或phandle引用节点或属性值可自动在dtb中生成phandle。label名字长度为1-31个字符,label名以下划线,大写字母或小写字母开头,由数字,下划线,大小写字母组成。label的使用方法如下。

2.1.为节点定义label:在节点名前面加上label名字和’:’即可。

2.2.通过label引用节点或属性值:在label名前加上&即可。

3.节点和属性的定义

节点由节点名和单元地址定义,并跟一组花括号表示节点的开始和结束,节点名之前可以定义label。

1
2
3
4
[label:] node-name[@unit-address] {
[properties definitions]
[child nodes]
};

如果一个节点中同时有节点的属性和子节点的话,应该先定义属性,然后是子节点。

可以删除前面定义的节点:

1
2
/delete-node/ node-name;
/delete-node/ &label;

属性定义为属性名和值的键值对,格式如下:

1
[label:] property-name = value;

属性值长度为0的属性定义格式如下:

1
[label:] property-name;

之前定义的属性可以被删除:

1
/delete-property/ property-name;

属性的值可以是32位整数或null结尾的字符串或者字节串的数组,也可以是这些类型的组合。

1
interrupts = <17 0xc>;

值可以用括号内的算术、位或逻辑表达式表示。支持的运算符如下:

算术运算符:

1
+,-,*,/

位运算符:

1
&,|,^, ~,<<, >>

逻辑运算符:

1
&&, ||, !

关系运算符:

1
<, >, <=, >=, ==, !=
1
2
Ternary operators
?: (condition ? value_if_true : value_if_false)

一个64bit的值用两个32bit的cells表示:

1
clock-frequency = <0x00000001 0x00000000>;

用双引号表示字符串:

1
compatible = "simple-bus";

字节串用方括号[]括起来,每个字节由两个十六进制数字表示。每个字节之间的空格是可选的:

1
local-mac-address = [00 00 12 34 56 78];

或者:

1
local-mac-address = [000012345678];

属性有多个值的时候用逗号分开:

1
2
compatible = "ns16550", "ns8250";
example = <0xf00f0000 19>, "a strange property format";

在一个节点属性值的cell数组中引用另一个节点将会被扩展为该节点的phandle。可以通过&加上节点的label引用:

1
interrupt-parent = < &mpic >;

也可以通过&加上一个用花括号括起来的绝对路径引用:

1
interrupt-parent = < &{/soc/interrupt-controller@40000} >;

不在cell数组中的引用,将会扩展为该节点的绝对路径:

1
ethernet0 = &EMAC0;

Labels也可能在属性值的某个成员前或后,或者cell数组里面的cell之间,或者字节串的字节之间:

1
2
3
reg = reglabel: <0 sizelabel: 0x1000000>;
prop = [ab cd ef byte4: 00 ff fe];
str = start: "string value" end: ;

三.DTB(Devicetree Blob)的格式

1.设备树文件的结构和布局

设备树文件布局

2.struct fdt_header

扁平设备树的头部结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
struct fdt_header {
uint32_t magic;
uint32_t totalsize;
uint32_t off_dt_struct;
uint32_t off_dt_strings;
uint32_t off_mem_rsvmap;
uint32_t version;
uint32_t last_comp_version;
uint32_t boot_cpuid_phys;
uint32_t size_dt_strings;
uint32_t size_dt_struct;
}

magic:0xd00dfeed (大端格式)

totalsize:设备树数据结构总大小的字节数。包括设备树结构的所有段: the header, the memory reservation block, structure block and strings block, as well as any free space gaps between the blocks or after the final block。

off_dt_struct:structure block相对于fdt_header起始地址的偏移。

off_dt_strings: strings block相对于ftd_header起始地址的偏移。

off_mem_rsvmap: memory reservation block相对于fdt_header起始地址的偏移。

version: 设备树数据结构的版本。

last_comp_version:当前使用的设备树版本能向后兼容的最低版本。

boot_cpuid_phys:系统启动cpu的物理ID,与设备树中cpu节点reg属性给出的物理ID一致。

size_dt_strings:设备树中strings block 的大小字节数。

size_dt_struct:设备树中structure block段的大小字节数。

2.Memory Reservation block

​ 描述了一组物理内存的保留区域,正常情况下这块内存区域将不被客户端程序使用。除非加载程序的其他信息明确说明需要访问,客户端程序才可能以指定的方式访问。内存预留在设备树中的格式如下:

Memory reservation block由两个64大端位整数组成的一对数据组成的链表组成。每一对都通过下面的C结构表示:

1
2
3
4
struct fdt_reserve_entry {
uint64_t address;
uint64_t size
};

每一对都给出了保留内存区域的物理地址和大小,给出的区域不能和其他区域重复。内存保留块的链表以一个地址和大小都为0的数据对节点结束。每一个数据对的address和size变量总是64位的,在32位cpu中,高32位被忽略。内存保留块相对dtb起始地址的偏移地址应该是8字节对齐的。

3.Structure Block

​ 结构块表示了设备树本身的结构和内容,它由一系列带数据的令牌组成,呈线性树结构。每个令牌都在结构块中,因此结构块相对dtb起始地址的偏移地址应该以4字节对齐。

结构块的结构:

结构块由一系列以一个32位大端整数表示的令牌开始的段构成,有些令牌后面跟着一段用一个令牌标识结束的附加数据。每一个令牌都是以32位宽度的,因此一个令牌区域除了令牌的值,剩余部分用0x0填充。

5种令牌类型如下:

FDT_BEGIN_NODE (0x00000001),节点开始的令牌,后面应该跟着节点名作为附加数据。名字是一个以null结束的字符串,如果有地址的话名字应该包含地址。如果有必要对齐的话,名字后面应该填充0值,然后是下一个令牌(除了FDT_END令牌)。

FDT_END_NODE(0x00000002),节点结束令牌,该令牌没有附加数据,所以它后面紧跟着下一个令牌(除了FDT_PROP令牌).

FDT_PROP(0x00000003),属性开始的令牌,它后面应该跟着描述属性的附加数据。附加数据首先由属性的长度和名字组成,用以下的C结构表示:(32位大端整数)

1
2
3
4
struct {
uint32_t len;
uint32_t nameoff;
}

len变量表示属性的值的长度,该长度可能为0,表示是一个空属性。

nameoff表示属性的名字(null结束的字符串)在string block中的偏移值。

紧跟着这个结构的是表示该属性的value的字符串,长度为结构中的len。然后是按照32位对齐方式补0,再接着就是下一个令牌(除了FDT_END)。

FDT_NOP (0x00000004),这个令牌在解析设备树时会被忽略。该令牌没有附加数据;所以它后面紧跟着其他任何有效令牌。可以通过使用该token重写某个属性或者节点,达到不移动其他段就可以删除该属性或节点的目的。

FDT_END (0x00000009),struct block结束的令牌。设备树中只有一个FDT_END,并且FDT_END令牌是struct block的最后一个令牌。该令牌没有附加数据,因此其后所跟的字节相对struct block起始地址的偏移值等于struct fdt_header中size_dt_struct变量的值。

树结构:设备树结构是一个线性树,每一个节点都以FDT_BEGIN_NODE令牌开始,并且以FDT_END_NODE令牌结束。节点的属性或者子节点在FDT_END_NODE之前表示,因此,这些子节点的FDT开始令牌和FDT结束令牌嵌套在父节点的开始和结束令牌中。struct block作为一个整体,由一个根节点(其他节点都嵌套在根节点中)和FDT_END结束。而每一个节点由下面的部分组成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
• (optionally) any number of FDT_NOP tokens
• FDT_BEGIN_NODE token
– The node’s name as a null-terminated string
– [zeroed padding bytes to align to a 4-byte boundary]
• For each property of the node:
– (optionally) any number of FDT_NOP tokens
– FDT_PROP token
*
property information
*
[zeroed padding bytes to align to a 4-byte boundary]
• Representations of all child nodes in this format
• (optionally) any number of FDT_NOP tokens
• FDT_END_NODE token

4.Strings Block

​ Strings Block包含设备树中所有属性的名字的字符串。这些以null结尾的字符串简单的连接在一起,通过structure block中的偏移值引用。Strings Block没有字节对齐,并可能出现在相对DTB开始偏移地址的任何偏移处。

四.uboot如何将设备树传递给内核

bootloader通过设置r0,r1,r2三个寄存器来给内核传递参数

r0一般设置为0;

r1一般设置为machine id,当内核使用设备树时该参数未使用。

r2一般设置为ATAGS或者fdt的首地址

在内核启动代码head.S和head-common.S中将r1寄存器的值赋给变量__machine_arch_type,将r2寄存器的值赋给了atags_pointer变量。

在setup_arch函数中调用setup_machine_fdt时将atags_pointer(设备树首地址)传入。

五.Linux内核如何处理设备树

在arch/arm/kernel/setup.c文件中的setup_arch函数中,通过调用两个函数分两个步骤来解析设备树。

1.setup_machine_fdt

​ 通过setup_machine_fdt主要完成了设备树的检查,machine_desc的匹配,命令行参数的解析,根节点dt_root_address_cells和dt_root_size_cells的设置以及内存添加。以下是简单代码流程分析。

在setup_arch函数中调用setup_machine_fdt时传入了设备树的指针:

1
mdesc = setup_machine_fdt(__atags_pointer);

setup_machine_fdt函数主要完成以下工作:

1.检查设备树。

1
2
if (!dt_phys || !early_init_dt_verify(phys_to_virt(dt_phys)))
return NULL;

2.通过设备树的根节点的compatible属性匹配machine_desc。

1
mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);

3.调用early_init_dt_scan_nodes函数完成设置boot_command_line,设置dt_root_size_cells和dt_root_address_cells,添加内存的操作。命令行参数通过扫描设备树的chosen节点获得,dt_root_size_cells和dt_root_address_cells是两个全局变量,在设备树解析的过程中都会用到。从设备树解析出内存信息后通过调用memblock_add函数添加内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void __init early_init_dt_scan_nodes(void)
{
int rc = 0;

/* Retrieve various information from the /chosen node */
rc = of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);
if (!rc)
pr_warn("No chosen node found, continuing without\n");

/* Initialize {size,address}-cells info */
of_scan_flat_dt(early_init_dt_scan_root, NULL);

/* Setup memory, calling early_init_dt_add_memory_arch */
of_scan_flat_dt(early_init_dt_scan_memory, NULL);
}

2.设备树展开为device_node

​ 通过调用unflatten_device_tree函数将设备树展开为一个device_node类型的树,就是将所有节点从设备树中解析出来并添加到全局链表of_root上。设置系统chosen节点。解析aliases节点并将aliases的属性(其他节点,aliases节点中主要是一些节点的别名和引用)添加到aliases_lookup链表中,以供后续使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
*unflatten_device_tree - create tree of device_nodes from flat blob
*
*unflattens the device-tree passed by the firmware, creating the
*tree of struct device_node. It also fills the "name" and "type"
*pointers of the nodes so the normal device-tree walking functions
*can be used.
*/
void __init unflatten_device_tree(void)
{
__unflatten_device_tree(initial_boot_params, NULL, &of_root,
early_init_dt_alloc_memory_arch, false);

/* Get pointer to "/chosen" and "/aliases" nodes for use everywhere */
of_alias_scan(early_init_dt_alloc_memory_arch);

unittest_unflatten_overlay_base();
}

3.Device_node如何转换成平台设备

​ 设备树解析后要被平台驱动使用,就需要将设备树device_node转换为平台设备platform_device,在系统初始化调用里设置了如下入口,该函数通过调用其他接口为满足条件的device_node创建了平台设备,并设置平台设备的of_node指针为device_node,并将平台设备添加到平台链表中,这样在添加平台驱动的时候就可以匹配到平台设备,并调用相应的probe回调,进而在probe回调中反过来使用平台设备的of_node。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static int __init of_platform_default_populate_init(void)
{
struct device_node *node;

if (!of_have_populated_dt())
return -ENODEV;

/*
Handle certain compatibles explicitly, since we don't want to create
platform_devices for every node in /reserved-memory with a
"compatible",
*/
for_each_matching_node(node, reserved_mem_matches)
of_platform_device_create(node, NULL, NULL);

node = of_find_node_by_path("/firmware");
if (node) {
of_platform_populate(node, NULL, NULL, NULL);
of_node_put(node);
}

/* Populate everything else. */
of_platform_default_populate(NULL, NULL, NULL);

return 0;
}
arch_initcall_sync(of_platform_default_populate_init);

六.内核中设备树操作接口

在内核目录的/drivers/of/目录下是设备树相关的接口文件,里面有各种设备树读取或修改设备,属性,等的接口函数,在内核模块中可以包含相应头文件并利用这些接口操作设备树。