设备树定义

设备树(Device tree),描述设备树的文件叫做DTS(Tree Source),这个DTS采用树形结构描述板级设备(开发板上的设备信息)。树的主干就是系统总线, IIC 控制器、 GPIO 控制器、 SPI 控制器等都是接到系统主线上的分支。

设备树源文件扩展名为.dts, DTS 是设备树源码文件, DTB 是将DTS 编译以后得到的二进制文件。将.c 文件编译为.o 需要用到 gcc 编译器,那么将.dts 编译为.dtb需要什么工具呢?需要用到 DTC 工具!

DTS语法

设备树也支持头文件,设备树的头文件扩展名为.dtsi。例如:在imx6ull-14x14-evk.dts中有

1
#include "imx6ulll-14x14-evk.dsti"

设备点

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
						示例代码 43.3.2.1 设备树模板
1 / {
2 aliases {
3 can0 = &flexcan1;
4 };
5
6 cpus {
7 #address-cells = <1>;
8 #size-cells = <0>;
9
10 cpu0: cpu@0 {
11 compatible = "arm,cortex-a7";
12 device_type = "cpu";
13 reg = <0>;
14 };
15 };
16
17 intc: interrupt-controller@00a01000 {
18 compatible = "arm,cortex-a7-gic";
19 #interrupt-cells = <3>;
20 interrupt-controller;
21 reg = <0x00a01000 0x1000>,
22 <0x00a02000 0x100>;
23 };
24 }

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

1
node-name@unit-address

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

但是我们在示例代码 43.3.2.1 中我们看到的节点命名却如下所示:

1
cpu0:cpu@0

上述命令并不是“node-name@unit-address”这样的格式,而是用“:”隔开成了两部分,“:”
前面的是节点标签(label),“:”后面的才是节点名字,格式如下所示:

1
label: node-name@unit-address

设备节点

设备树源码中常用的几种数据形式如下所示:

①、字符串

1
compatible = "arm,cortex-a7";

上述代码设置 compatible 属性的值为字符串“arm,cortex-a7”。
②、32 位无符号整数

1
reg = <0>;

上述代码设置 reg 属性的值为 0,reg 的值也可以设置为一组值,比如:

1
reg = <0 0x123456 100>;

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

1
compatible = "fsl,imx6ull-gpmi-nand", "fsl, imx6ul-gpmi-nand";

标准属性

节点是由一堆的属性组成,节点都是具体的设备,不同的设备需要的属性不同,用户可以自定义属性。除了用户自定义属性,有很多属性是标准属性, Linux 下的很多外设驱动都会使用这些标准属性。

1、compatible属性

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

1
"manufacturer,model"

其中 manufacturer 表示厂商, model 一般是模块对应的驱动名字。

I.MX6U-ALPHA 开发板上的音频芯片采用的欧胜(WOLFSON)出品的 WM8960, sound 节点的 compatible 属性值如下:

1
compatible = "fsl,imx6ul-evk-wm8960","fsl,imx-audio-wm8960";

属性值有两个,分别为“fsl,imx6ul-evk-wm8960”和“fsl,imx-audio-wm8960” ,sound这个设备首先使用第一个兼容值在 Linux 内核里面查找,看看能不能找到与之匹配的驱动文件,如果没有找到的话就使用第二个兼容值查。

一般驱动程序文件都会有一个 OF 匹配表,此 OF 匹配表保存着一些 compatible 值,如果设备节点的compatible 属性值和 OF 匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动。

2、model属性

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

1
model = "wm8960-audio";

3、status属性

status 属性看名字就知道是和设备状态有关的, status 属性值也是字符串,字符串是设备的状态信息,可选的状态如表 :

描述
”okey“ 表明设备是可操作的。
”disabled“ 表明设备当前是不可操作的,但是在未来可以变为可操作的,比如热插拔设备插入以后。具体含义看设备绑定的文档。
”fail“ 表明设备不可操作,设备检测到了一系列的错误,而且设备也不大可能变得操作。
”fail-sss“ 含义和“fail”相同,后面的 sss 部分是检测到的错误内容。

4、#address-cells和#size-cells属性

这两个属性的值都是无符号 32 位整形, #address-cells 和#size-cells 这两个属性可以用在任何拥有子节点的设备中,用于描述子节点的地址信息。 #address-cells 属性值决定了子节点 reg 属性中地址信息所占用的字长(32 位),

#size-cells 属性值决定了子节点 reg 属性中长度信息所占的字长(32 位)。 #address-cells 和#size-cells 表明了子节点应该如何编写 reg 属性值,一般 reg 属性都是和地址有关的内容,和地址相关的信息有两种:起始地址和地址长度。reg格式一般为:

1
reg = <address1 length1 address2 length2 address3 length3……>

每个“address length”组合表示一个地址范围,其中 address 是起始地址, length 是地址长度, #address-cells 表明 address 这个数据所占用的字长, #size-cells 表明 length 这个数据所占用的字长

5、reg属性

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

6、 ranges 属性
ranges属性值可以为空或者按照(child-bus-address,parent-bus-address,length)格式编写的数字矩阵, ranges 是一个地址映射/转换表, ranges 属性每个项目由子地址、父地址和地址空间长度。
这三部分组成:
child-bus-address:子总线地址空间的物理地址,由父节点的#address-cells 确定此物理地址所占用的字长。
parent-bus-address: 父总线地址空间的物理地址,同样由父节点的#address-cells 确定此物理地址所占用的字长。
length: 子地址空间的长度,由父节点的#size-cells 确定此地址长度所占用的字长

7、 name 属性
name 属性值为字符串, name 属性用于记录节点名字, name 属性已经被弃用,不推荐使用name 属性,一些老的设备树文件可能会使用此属性。
8、 device_type 属性
device_type 属性值为字符串, IEEE 1275 会用到此属性,用于描述设备的 FCode,但是设备树没有 FCode,所以此属性也被抛弃了。此属性只能用于 cpu 节点或者 memory 节点。imx6ull.dtsi 的 cpu0 节点用到了此属性 。

根节点 compatible 属性

设备节点的 compatible 属性值是为了匹配 Linux 内核中的驱动程序,通过根节点的 compatible 属性可以知道我们所使用的设备,一般第
一个值描述了所使用的硬件设备名字,第二个值描述了设备所使用的 SOC。 Linux 内核会通过根节点的 compoatible 属性查看是否支持此设备,如果支持的话设备就会启动 Linux 内核。

1、使用设备树之前的设备匹配方法

在没有使用设备树以前, uboot 会向 Linux 内核传递一个叫做 machine id 的值, machine id也就是设备 ID,告诉 Linux 内核自己是个什么设备,看看 Linux 内核是否支持。

uboot 会给 Linux 内核传递 machine id 这个参数, Linux 内核会检查这个 machine id,其实就是将 machine id 与MACH_TYPE_XXX 宏进行对比,看看有没有相等的,如果相等的话就表示 Linux 内核支持这个设备,如果不支持的话那么这个设备就没法启动 Linux 内核。

2、使用设备树以后的设备匹配方法

当 Linux 内 核 引 入 设 备 树 以 后 就 不 再 使 用 MACHINE_START 了 , 而 是 换 为 了DT_MACHINE_START。 DT_MACHINE_START 也定义在文件 arch/arm/include/asm/mach/arch.h里面,定义如下:

1
2
3
4
5
6
#define DT_MACHINE_START(_name, _namestr) \
static const struct machine_desc __mach_desc_##_name \
__used \
__attribute__((__section__(".arch.info.init"))) = { \
.nr = ~0, \
.name = _namestr,

可以看出, DT_MACHINE_START 和 MACHINE_START 基本相同,只是.nr 的设置不同,在 DT_MACHINE_START 里面直接将.nr 设置为~0。说明引入设备树以后不会再根据 machineid 来检查 Linux 内核是否支持某个设备了。

打开文件 arch/arm/mach-imx/mach-imx6ul.c,有如下所示内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
208 static const char *imx6ul_dt_compat[] __initconst = {
209 "fsl,imx6ul",
210 "fsl,imx6ull",
211 NULL,
212 };
213
214 DT_MACHINE_START(IMX6UL, "Freescale i.MX6 Ultralite (Device Tree)")
215 .map_io = imx6ul_map_io,
216 .init_irq = imx6ul_init_irq,
217 .init_machine = imx6ul_init_machine,
218 .init_late = imx6ul_init_late,
219 .dt_compat = imx6ul_dt_compat,
220 MACHINE_END

machine_desc 结构体中有个.dt_compat 成员变量,此成员变量保存着本设备兼容属性,设置.dt_compat = imx6ul_dt_compat, imx6ul_dt_compat 表里面有”fsl,imx6ul”和”fsl,imx6ull”这两个兼容值。只要某个设备(板子)根节点“ /”的 compatible 属性值与imx6ul_dt_compat 表中的任何一个值相等,那么就表示 Linux 内核支持此设备。

imx6ull-alientekemmc.dts 中根节点的 compatible 属性值如下:

1
compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";

其中“fsl,imx6ull”与 imx6ul_dt_compat 中的“fsl,imx6ull”匹配,因此 I.MX6U-ALPHA 开发板可以正常启动 Linux 内核。

Linux 内核调用 start_kernel 函数来启动内核, start_kernel 函数会调用setup_arch 函数来匹配 machine_desc, setup_arch 函数定义在文件 arch/arm/kernel/setup.c 中 。

1
2
3
4
5
6
7
8
9
10
11
12
913 void __init setup_arch(char **cmdline_p)
914 {
915 const struct machine_desc *mdesc;
916
917 setup_processor();
918 mdesc = setup_machine_fdt(__atags_pointer);
919 if (!mdesc)
920 mdesc = setup_machine_tags(__atags_pointer, __machine_arch_type);
921 machine_desc = mdesc;
922 machine_name = mdesc->name;
......
986 }

第 918 行,调用 setup_machine_fdt 函数来获取匹配的 machine_desc,参数就是 atags 的首地址,也就是 uboot 传递给 Linux 内核的 dtb 文件首地址, setup_machine_fdt 函数的返回值就是找到的最匹配的 machine_desc。

函数 setup_machine_fdt 定义在文件 arch/arm/kernel/devtree.c 中,内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
204 const struct machine_desc * __init setup_machine_fdt(unsigned int dt_phys)
205 {
206 const struct machine_desc *mdesc, *mdesc_best = NULL;
......
214
215 if (!dt_phys || !early_init_dt_verify(phys_to_virt(dt_phys)))
216 return NULL;
217
218 mdesc = of_flat_dt_match_machine(mdesc_best,arch_get_next_mach);
219
......
247 __machine_arch_type = mdesc->nr;
248
249 return mdesc;
250 }

第 218 行,调用函数 of_flat_dt_match_machine 来获取匹配的 machine_desc,参数 mdesc_best是 默 认 的 machine_desc , 参 数 arch_get_next_mach 是 个 函 数 , 此 函 数 定 义 在 定 义 在arch/arm/kernel/devtree.c 文件中。找到匹配的 machine_desc 的过程就是用设备树根节点的compatible 属性值和 Linux 内核中 machine_desc 下.dt_compat 的值比较,看看那个相等,如果相等的话就表示找到匹配的 machine_desc, arch_get_next_mach 函数的工作就是获取 Linux 内核中下一个 machine_desc 结构体。

最后再来看一下 of_flat_dt_match_machine 函数,此函数定义在文件 drivers/of/fdt.c 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
705 const void * __init of_flat_dt_match_machine(const void *default_match,
706 const void * (*get_next_compat)(const char * const**))
707 {
708 const void *data = NULL;
709 const void *best_data = default_match;
710 const char *const *compat;
711 unsigned long dt_root;
712 unsigned int best_score = ~1, score = 0;
713
714 dt_root = of_get_flat_dt_root();
715 while ((data = get_next_compat(&compat))) {
716 score = of_flat_dt_match(dt_root, compat);
717 if (score > 0 && score < best_score) {
718 best_data = data;
719 best_score = score;
720 }
721 }
......
739
740 pr_info("Machine model: %s\n", of_flat_dt_get_machine_name());
741
742 return best_data;
743 }

第 714 行,通过函数 of_get_flat_dt_root 获取设备树根节点。
第 715~720 行,此循环就是查找匹配的 machine_desc 过程,第 716 行的 of_flat_dt_match 函数会将根节点 compatible 属性的值和每个 machine_desc 结构体中. dt_compat 的值进行比较,直至找到匹配的那个 machine_desc。

Linux 内核通过根节点 compatible 属性找到对应的设备的函数调用过程 如下:

向节点追加或修改内容

打开imx6ull.dtsi文件进行追加内容,因为imx6ull.dtsi是设备头文件,所有使用IMX6ULL这个soc的板子都会引用这个文件,向文件中添加一个设备节点,相当于所有的板子都会添加这个设备,所以要在自己板子的.dts文件进行数据追加内容。

比如要向I2C1节点追加一个fxls8471的子节点,需要在imx6ull-alientek-emmc.dts 文件中完成数据追加的内容,方式如下:

1
2
3
1 &i2c1 {
2 /* 要追加或修改的内容 */
3 };

第 1 行, &i2c1 表示要访问 i2c1 这个 label 所对应的节点,也就是 imx6ull.dtsi 中的“i2c1:i2c@021a0000”。
第 2 行,花括号内就是要向 i2c1 这个节点添加的内容,包括修改某些属性的值。

打开 imx6ull-alientek-emmc.dts,找到如下所示内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
224 &i2c1 {
225 clock-frequency = <100000>;
226 pinctrl-names = "default";
227 pinctrl-0 = <&pinctrl_i2c1>;
228 status = "okay";
229
230 mag3110@0e {
231 compatible = "fsl,mag3110";
232 reg = <0x0e>;
233 position = <2>;
234 };
235
236 fxls8471@1e {
237 compatible = "fsl,fxls8471";
238 reg = <0x1e>;
239 position = <0>;
240 interrupt-parent = <&gpio5>;
241 interrupts = <0 8>;
242 };
243}

本代码就是向 i2c1 节点添加/修改数据,比如第 225 行的属性“clock-frequency”就表示 i2c1 时钟为 100KHz。“clock-frequency”就是新添加的属性。
第 228 行,将 status 属性的值由原来的 disabled 改为 okay。
第 230234 行, i2c1 子节点 mag3110,因为 NXP 官方开发板在 I2C1 上接了一个磁力计芯片 mag3110,正点原子的 I.MX6U-ALPHA 开发板并没有使用 mag3110。
第 236
242 行, i2c1 子节点 fxls8471,同样是因为 NXP 官方开发板在 I2C1 上接了 fxls8471这颗六轴芯片 。

创建小型模板设备树

以 I.MX6ULL 这个 SOC 为例,我们需要在设备树里面描述的内容如下:
①、 I.MX6ULL 这个 Cortex-A7 架构的 32 位 CPU。
②、 I.MX6ULL 内部 ocram,起始地址 0x00900000,大小为 128KB(0x20000)。
③、 I.MX6ULL 内部 aips1 域下的 ecspi1 外设控制器,寄存器起始地址为 0x02008000,大小为 0x4000。
④、 I.MX6ULL 内部 aips2 域下的 usbotg1 外设控制器,寄存器起始地址为 0x02184000,大小为 0x4000。
⑤、 I.MX6ULL 内部 aips3 域下的 rngb 外设控制器,寄存器起始地址为 0x02284000,大小为 0x4000。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/{
compatible = "fsl, imx6ull-alientek-evk", "fsl, imx6ull";
cpus {
#address-cells = <1>;//基地址、片选号等绝对起始地址所占字长
#size-cells = <0>;//长度所占字长
cpu0 : cpu@0{
compatible = "arm ,cortex-a7";
device_type = "cpu";
reg = <0>;
};
};
soc {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
ranges;
ocram: sram@00900000 {
compatible = "fsl, lpm-sram";
reg = <0x00900000 0x20000>;
};

apis1: aips-bus@02000000 {
compatible = "fsl, aip-bus", "simble-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x2000000 0x100000>;
ranges;

ecspi1: ecspi@02008000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "fsl, imx6ul-ecspi", "fsl, imx5-ecspi";
reg = <0x02008000 0x4000>;
status = "disabled";
};
apis2: aips-bus@02100000 {
compatible = "fsl, apis-bus", "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02100000 0x100000>;
ranges;

usbotg1: usb@02184000 {
compatible = "fsl, imx6ul-usb" , "fsl, imx27-usb";
reg = <0x02184000 0x4000>;
status = "disabled";
};
aips3: aips-bus@02200000 {
compatible = "fsl,aips-bus", "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02200000 0x100000>;
ranges;

rngb: rngb@02284000 {
compatible = "fsl,imx6sl-rng", "fsl,imx-rng", "imxrng";
reg = <0x02284000 0x4000>;
};
}
}
}

设备树在系统中的体现

Linux 内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的/proc/devicetree 目录下根据节点名字创建不同文件夹, 可以使用cat命令来查看model和compatible文件信息。

1
2
3
cd proc/devicetree
cat model
cat compatible

/proc/device-tree 目录就是设备树在根文件系统中的体现,同样是按照树形结构组织的,进入/proc/device-tree/soc 目录中就可以看到 soc 节点的所有子节点 。

1
2
cd soc
ls

Linux内核解析DTB文件

如何添加节点

可以查看Linux的源码下的/Documentation/devicetree/bindings,添加哪个就看哪个文件。