Linux设备树讲解(Device Tree)含设备树基本语法
前言
在内核源码中,存在大量对板级细节信息描述的代码。这些代码充斥在/arch/arm/plat-xxx和/arch/arm/mach-xxx目录,对内核而言这些platform设备、resource、i2c_board_info、spi_board_info以及各种硬件的platform_data绝大多数纯属垃圾冗余代码。为了解决这一问题,ARM内核版本3.x之后引入了 Device Tree。
文章对设备树的组成、设备树的基本语法、标准属性、节点添加方式及Linux匹配设备树的方法,并且创建了一个小型设备树框架模板。
1. 设备树
设备树(Device Tree),将这个词分开就是“设备”和“树”,描述设备树的文件叫做 DTS(Device Tree Source),这个 DTS 文件采用树形结构描述板级设备,也就是开发板上的设备信息,比如CPU 数量、 内存基地址、IIC 接口上接了哪些设备、SPI 接口上接了哪些设备等等,如下图:
在上图中,树的主干就是系统总线,IIC 控制器、GPIO 控制器、SPI 控制器等都是接到系统主线上的分支。
例如,IIC 控制器分为 IIC1 和 IIC2 两种,其中 IIC1 上接了 FT5206 和 AT24C02这两个 IIC 设备,IIC2 上只接了 MPU6050 这个设备。
另外,设备树对于可热插拔的热备不进行具体描述,它只描述用于控制该热插拔设备的控制器。
设备树的主要优势:对于同一SOC的不同主板,只需更换设备树文件.dtb即可实现不同主板的无差异支持,而无需更换内核文件。
2. 设备树的组成
设备树包含DTC(device tree compiler)、DTS(device tree source)和DTB(device tree blob)。
2.1. dts和dtsi
xxx.dts文件是一种ASCII文本对Device Tree的描述,放置在内核的/arch/arm/boot/dts目录。一般而言,一个xxx.dts文件对应一个ARM的machine。
xxx.dtsi文件作用:由于一个SOC可能有多个不同的电路板,而每个电路板拥有一个 xxx.dts。这些dts势必会存在许多共同部分,为了减少代码的冗余,设备树将这些共同部分提炼保存在*.dtsi文件中,供不同的dts共同使用。*.dtsi的使用方法,类似于C语言的头文件,在dts文件中需要进行include *.dtsi文件。当然,dtsi本身也支持include 另一个dtsi文件。
2.2. DTB
DTC编译 xxx.dts 生成的二进制文件(xxx.dtb),bootloader在引导内核时,会预先读取xxx.dtb到内存,进而由内核解析。
2.3. DTC
dts和dtsi转换为二进制DTB文件需要使用DTC工具,DTC可以将.dts文件编译成.dtb文件。DTC的源码位于内核的scripts/dtc目录,scripts/dtc/Makefile 文件内容如下:
hostprogs-y := dtc
always := $(hostprogs-y)
dtc-objs:= dtc.o flattree.o fstree.o data.o livetree.o treesource.o \
srcpos.o checks.o util.o
dtc-objs += dtc-lexer.lex.o dtc-parser.tab.o
......
变异DTS文件的活就只需要进入Linux根目录下,执行如下命令:
make all // 全编译
make dtbs // 编译设备树文件
-
如何确定编译的DTS文件?
基于 ARM 架构的 SOC 有很多种,一种 SOC 又可以制作出很多开发板,每个板子都有一个对应的 DTS 文件。
下面就以imx6ull为例,arch/arm/boot/dts/Makefile,有如下内容:
dtb-$(CONFIG_SOC_IMX6UL) += \ imx6ul-14x14-ddr3-arm2.dtb \ imx6ul-14x14-ddr3-arm2-emmc.dtb \ ...... dtb-$(CONFIG_SOC_IMX6ULL) += \ imx6ull-14x14-ddr3-arm2.dtb \ imx6ull-14x14-ddr3-arm2-adc.dtb \ imx6ull-14x14-ddr3-arm2-cs42888.dtb \ imx6ull-14x14-ddr3-arm2-ecspi.dtb \ imx6ull-14x14-ddr3-arm2-emmc.dtb \ imx6ull-14x14-ddr3-arm2-epdc.dtb \ imx6ull-14x14-ddr3-arm2-flexcan2.dtb \ ...... dtb-$(CONFIG_SOC_IMX6SLL) += \ imx6sll-lpddr2-arm2.dtb \ imx6sll-lpddr3-arm2.dtb \ ......
当选中IMX6ULL这个SOC后,该板子对应的.dts都会被编译为.dtb文件。
-
如何在SOC下添加新的设备树病生成二进制文件?
例如,在IMX6ULL此SOC新添加设备树 asu.dts 时 若想生成对应的 asu.dtb 需要在 dtb-$(CONFIG_SOC_IMX6ULL)下添加 asu.dtb ,编译时就会生产新的二进制文件。
3. 设备树基本语法
在 .dts 和 .dtsi 文件中包含了一些列节点,以及描述节点的属性。
3.1. dtsi头文件
因为 .dtsi 是描述 SOC 内部信息(即,同等SOC的通用信息),而 .dts 描述的是板级信息,所以 .dts
都会包含 .dtsi 文件。例如,imx6ull的头文件就包含imx6ull.dtsi.
#include <dt-bindings/input/input.h>
#include "imx6ull.dtsi"
3.2. 设备节点
备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点,每个节点都通过一些属性信息来描述节点信息,属性就是键 —值对。下面是从imx6ull.dtsi中缩减的部分内容:
/ { // “/” 根节点,每个设备树只有一个根节点(.dts和.dtsi的根节点最后会合并为一个)
aliases { // aliases node,子节点
gpio0 = &gpio1;
};
/*
* cpus node,子节点
* 格式:node-name@unit-address
* "node-name"是节点名字,"unit-address"设备地址活寄存器首地址,没有可以不要
*/
cpus {
#address-cells = <1>;
#size-cells = <0>;
/*
* cpu0, cpus的子节点
* cpu0:cpu@0,使用":"隔开,前面的是节点标签(label),":"后面是节点名字
* 格式:label:node-name@unit-address
*/
cpu0: cpu@0 {
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <0>;
};
};
/*
* intc node,子节点
* intc:label,interrupt-controller@00a01000:节点名
* 节点名太长,使用label更方便
*/
intc: interrupt-controller@00a01000 {
compatible = "arm,cortex-a7-gic";
#interrupt-cells = <3>;
interrupt-controller;
reg = <0x00a01000 0x1000>,
<0x00a02000 0x100>;
};c
};
3.3. 特殊节点
3.3.1. aliases node
aliases node用于定义别名,使节点引用变得方便。
{
aliases {
gpio0 = &gpio1;
mmc0 = &usdhc1;
mmc1 = &usdhc2;
.......
};
3.3.2. chosen node
chosen不是真实的节点,chosen节点是为了 uboot 向 Linux 内核传递 bootargs 参数。
chosen {
stdout-path = &uart1;
};
如上设置了 “stdout-path = &uart1;”,表示标准输出使用 uart1。在开发板 /proc/device-tree/chosen目录下查看,有 stdout-path和bootargs,如下图所示:
-
bootargs属性
chosen中多了bootargs属性,查看该属性,发现掐红内容为uboot中设置的环境变量,如下:
uboot在启动Linux时会把bootargs的值传递给Linux内核,bootargs会作为Linux内核的命令行参数,Linux内核启动的时候会打印出命令行参数(即uboot传递的bootargs值),如下所示:
-
uboot如何在chosen节点添加bootargs属性?
全局搜索在common/fdt_support.c 文件中发现了“chosen”,fdt_support.c 文件中有个 fdt_chosen 函
数,此函数内容如下所示:int fdt_chosen(void *fdt){ int nodeoffset; int err; char *str; /* used to set string properties */ err = fdt_check_header(fdt); if (err < 0) { printf("fdt_chosen: %s\n", fdt_strerror(err)); return err; } /* find or create "/chosen" node. */ /* 调用函数 fdt_find_or_add_subnode 从设备树(.dtb)中找到 chosen 节点,如果没有 找到的话就会自己创建一个 chosen 节点 */ nodeoffset = fdt_find_or_add_subnode(fdt, 0, "chosen"); if (nodeoffset < 0) return nodeoffset; /* 读取 uboot 中 bootargs 环境变量的内容 */ str = getenv("bootargs"); if (str) { /* 调用函数 fdt_setprop 向 chosen 节点添加 bootargs 属性,并且 bootargs 属性的值 就是环境变量 bootargs 的内容 */ err = fdt_setprop(fdt, nodeoffset, "bootargs", str, strlen(str) + 1); if (err < 0) { printf("WARNING: could not set bootargs %s.\n", fdt_strerror(err)); return err; } } return fdt_fixup_stdout(fdt, nodeoffset); }
由fdt_support.c可知,uboot 中的 fdt_chosen 函数在设备树的 chosen 节点中加入了 bootargs 属性,并且还设置了 bootargs 属性值。
3.3.3. memory node
memory node是以0x80000000为起始地址,0x20000000 size的512MB的空间。
一般而言,在.dts中不对memory进行描述,而是通过bootargs中类似521M@0x80000000的方式传递给内核。
memory {
reg = <0x80000000 0x20000000>;
};
3.4. 标准属性
节点是由属性组成,节点是具体的设备,不同的设备需要的属性不同。Linux 下很多外设驱动都会使用标准属性,下面讲解一些常用的标准属性。
3.4.1. compatible 属性
compatible 属性用于将设备和驱动绑定起来。字符串列表用于选择设备所要使用的驱动程序,compatible 属性的值格式如下所示:
"manufacturer,model" // "厂商,驱动名"
compatible = "fsl,imx6ul-evk-wm8960","fsl,imx-audio-wm8960";
例,“fsl”:表示厂商飞思卡尔;“imx6ul-evk-wm8960”和“imx-audio-wm8960”表示驱动模块名字。
-
设备绑定驱动
驱动程序文件会有一个 OF 匹配表,此 OF 匹配表保存着一些 compatible 值,若设备节点的 compatible 属性值和 OF 匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动。比如在文件 imx-wm8960.c 中有如下内容:
/* imx_wm8960_dt_ids imx-wm8960.c 驱动的匹配表, * 此匹配表中的compatible属性与设备树中匹配就会绑定此驱动文件 */ static const struct of_device_id imx_wm8960_dt_ids[] = { { .compatible = "fsl,imx-audio-wm8960", }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, imx_wm8960_dt_ids); static struct platform_driver imx_wm8960_driver = { .driver = { .name = "imx-wm8960", .pm = &snd_soc_pm_ops, .of_match_table = imx_wm8960_dt_ids, }, .probe = imx_wm8960_probe, .remove = imx_wm8960_remove, };
3.4.2. model 属性
model 属性值是一个字符串,一般 model 属性描述设备模块信息,比如名字:
model = "wm8960-audio";
3.4.3. staus 属性
设备状态,属性值是字符串,字符串是设备的状态信息,可选的状态如下表所示:
值 | 描述 |
---|---|
“okay” | 设备是可操作的 |
“disabled” | 设备当前不可操作,但在未来可以变为可操作的,比如热插拔设备插入以后。 |
“fail” | 表明设备不可操作,设备检测到了一系列的错误 |
“fail-sss” | 含义和“fail”相同,后面的 sss 部分是检测到的错误内容 |
3.4.4. #address-cells 、#size-cells 和 reg 属性
#address-cells 属性值决定了子节点 reg 属性中地址信息所占用的字长(32 位)。
#size-cells 属性值决定了子节点 reg 属性中长度信息所占的字长(32 位)。
reg =< address,length >,一般用于描述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息。
#address-cells 和#size-cells 表明了子节点应该如何编写 reg 属性值:起始地址和地址长度,reg 属性的格式为:
reg = <address1 length1 address2 length2 address3 length3……>
aips2: aips-bus@02100000 {
compatible = "fsl,aips-bus", "simple-bus";
#address-cells = <1>; /* 说明reg属性起始地址所占字长为1 */
#size-cells = <1>; /* reg属性地址长度所占字长为1 */
reg = <0x02100000 0x100000>; /* 起始地址为0x02100000,地址长度为0x100000 */
ranges;
usdhc1: usdhc@02190000 {
compatible = "fsl,imx6ull-usdhc", "fsl,imx6sx-usdhc";
reg = <0x02190000 0x4000>; /* 起始地址为0x02100000,地址长度为0x4000 */
......
......
status = "disabled";
};
3.4.5. rangs 属性
ranges 属性值可以为空或者按照(child-bus-address,parent-bus-address,length)格式编写的数字
矩阵,ranges 是一个地址映射/转换表,ranges 属性每个项目由子地址、父地址和地址空间长度
三部分组成:
- child-bus-address:子总线地址空间的物理地址,由父节点的#address-cells 确定此物理地址所占字长
- parent-bus-address:父总线地址空间的物理地址,由父节点的#address-cells 确定此物理地址所占字长
- length:子地址空间的长度,由父节点的#size-cells 确定此地址长度所占用的字长
注:如果rangs属性值为空。说明子空间和父空间完全相同,不需要进行地址转换。
如下为rangs为空的代码示例:
soc {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
interrupt-parent = <&gpc>;
ranges;
.....
};
如下为rangs不为空的代码示例:
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x0 0xe0000000 0x00100000>;
/* 指定了一个 1024KB(0x00100000)的地址范围,子地址空间的物理起始地址为 0x0,父地址空间的物理起始地址为 0xe0000000*/
serial {
device_type = "serial";
compatible = "ns16550";
reg = <0x4600 0x100>;
/* serial 是串口设备节点
* reg 属性定义了 serial 设备寄存器的起始地址为 0x4600,寄存器长度为 0x100。
* 经过地址转换,serial 设备可以从 0xe0004600 开始进行读写操作,
0xe0004600=0x4600+0xe0000000 */
clock-frequency = <0>;
interrupts = <0xA 0x8>;
interrupt-parent = <&ipic>;
};
};
3.4.6. device_type 属性
device_type 属性值为字符串,IEEE 1275 会用到此属性,用于描述设备的 FCode,但是设备树没有 FCode,所以此属性被弃用了。此属性只能用于 cpu 节点或者 memory 节点。imx6ull.dtsi 的 cpu0 节点用到了此属性,内容如下所示:
cpu0: cpu@0 {
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <0>;
};
4. Linux内核匹配设备树
4.1.根节点 compatible 属性
每个节点都有自己的compatible属性,根节点也有。
例如,imx6ull-alientek-emmc.dts 文件中根节点的 compatible 属性内容如下所示:
/ {
model = "Freescale i.MX6 ULL 14x14 EVK Board";
compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";
......
}
根节点的 compatible 属性可以知道我们所使用的设备,第一个值描述了所使用的硬件设备名字,比如这里使用的是“imx6ull-14x14-evk”这个设备,第二个值描述设备所使用的 SOC,比如这里使用的是“imx6ull”这颗 SOC。
Linux 内核会通过根节点的 compoatible 属性查看是否支持此设备,支持的话设备就会启动 Linux 内核。
4.2. 使用设备树的匹配方法
Linux 内核引入设备树之后使用DT_MACHINE_START描述设备。
DT_MACHINE_START 宏定义在文件 arch/arm/include/asm/mach/arch.h里面,如下:
#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 函数在 arch/arm/mach-imx/mach-imx6ul.c,有如下所示内容:
static const char *imx6ul_dt_compat[] __initconst = {
"fsl,imx6ul",
"fsl,imx6ull",
NULL,
};
DT_MACHINE_START(IMX6UL, "Freescale i.MX6 Ultralite (Device Tree)")
.map_io = imx6ul_map_io,
.init_irq = imx6ul_init_irq,
.init_machine = imx6ul_init_machine,
.init_late = imx6ul_init_late,
.dt_compat = imx6ul_dt_compat,
MACHINE_END
结构体中有个.dt_compat 成员变量,此成员变量保存着本设备兼容属性
如上所示 .dt_compat = imx6ul_dt_compat,imx6ul_dt_compat 表里面有"fsl,imx6ul"和 “fsl,imx6ull” 这 两 个 兼 容 值。 只 要 某 个 设备 ( 板子 ) 根 节 点 “ / ” 的 compatible 属性 值 与imx6ul_dt_compat 表中的任何一个值相等,那么就表示 Linux 内核支持此设备。
5. 创建小型模版设备树
通过上面对设备树的了解,这里以 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。
首先搭建一个仅含有根节点的基础框架,然后逐步补充各个节点,创建 asu.dts 文件,如下:
/ {
compatible = "fsl,imx6ull-alientek-evk", "fsl,imx6ull";
}
5.1. 添加 cpus 节点
I.MX6ULL 采用 Cortex-A7 架构,而且只有一个 CPU,因此只有一个cpu0 节点,完成以后如下所示:
/ {
compatible = "fsl,imx6ull-alientek-evk", "fsl,imx6ull";
cpus {
#address-cells = <1>;
#size-cells = <0>;
//CPU0 节点
cpu0: cpu@0 {
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <0>;
};
};
}
5.2. 添加 soc 节点
像 uart,iic 控制器等等这些都属于 SOC 内部外设,因此一般会创建一个叫做 soc 的父节点来管理这些 SOC 内部外设的子节点,添加 soc 节点以后的 myfirst.dts 文件内容如下所示:
/ {
compatible = "fsl,imx6ull-alientek-evk", "fsl,imx6ull";
cpus {
......
};
//soc 节点
soc {
/* reg 属性中起始地占用一个字长,地址空间长度也占用一个字长 */
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
ranges; /* 子空间和父空间地址范围相同 */
}
}
5.3. 添加 ocram 节点
ocram 是 I.MX6ULL 内部 RAM,因此 ocram 节点应该是 soc 节点的子节点。ocram 起始地址为 0x00900000,大小为 128KB(0x20000),如下所示:
/ {
compatible = "fsl,imx6ull-alientek-evk", "fsl,imx6ull";
cpus {
......
};
//soc 节点
soc {
/* reg 属性中起始地占用一个字长,地址空间长度也占用一个字长 */
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
ranges; /* 子空间和父空间地址范围相同 */
//ocram 节点
ocram: sram@00900000 {
compatible = "fsl,lpm-sram";
reg = <0x00900000 0x20000>;
};
}
}
5.4. 添加 aips1、aips2 和 aips3 子节点
I.MX6ULL 内部分三个域:aips13,三个域分管不同外设控制器,aips13 这三个域对应的内存范围如表 所示:
域 | 起始地址 | 大小(16进制) |
---|---|---|
AIPS1 | 0X02000000 | 0X100000 |
AIPS2 | 0X02100000 | 0X100000 |
AIPS3 | 0X02200000 | 0X100000 |
/ {
compatible = "fsl,imx6ull-alientek-evk", "fsl,imx6ull";
cpus {
......
};
//soc 节点
soc {
/* reg 属性中起始地占用一个字长,地址空间长度也占用一个字长 */
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
ranges; /* 子空间和父空间地址范围相同 */
//ocram 节点
ocram: sram@00900000 {
compatible = "fsl,lpm-sram";
reg = <0x00900000 0x20000>;
};
//aips1 节点
aips1: aips-bus@02000000 {
compatible = "fsl,aips-bus", "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02000000 0x100000>;
ranges;
}
//aips2 节点
aips2: aips-bus@02100000 {
compatible = "fsl,aips-bus", "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02100000 0x100000>;
ranges;
};
//aips3 节点
aips3: aips-bus@02200000 {
compatible = "fsl,aips-bus", "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02200000 0x100000>;
ranges;
};
}
}
5.5. 添加 ecspi1、usbotg1 和 rngb 外设控制器节点
最后加入 ecspi1,usbotg1 和 rngb 这三个外设控制器对应的节点,其中 ecspi1 属于 aips1 的子节点,usbotg1 属于 aips2 的子节点,rngb 属于 aips3 的子节点。最终的 asu.dts 文件内容如下:
/ {
compatible = "fsl,imx6ull-alientek-evk", "fsl,imx6ull";
cpus {
#address-cells = <1>;
#size-cells = <0>;
//CPU0 节点
cpu0: cpu@0 {
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <0>;
};
};
//soc 节点
soc {
/* reg 属性中起始地占用一个字长,地址空间长度也占用一个字长 */
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
ranges; /* 子空间和父空间地址范围相同 */
//ocram 节点
ocram: sram@00900000 {
compatible = "fsl,lpm-sram";
reg = <0x00900000 0x20000>;
};
//aips1 节点
aips1: aips-bus@02000000 {
compatible = "fsl,aips-bus", "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02000000 0x100000>;
ranges;
//ecspi1 节点
ecspi1: ecspi@02008000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "fsl,imx6ul-ecspi", "fsl,imx51-ecspi";
reg = <0x02008000 0x4000>;
status = "disabled";
};
}
//aips2 节点
aips2: aips-bus@02100000 {
compatible = "fsl,aips-bus", "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02100000 0x100000>;
ranges;
//usbotg1 节点
usbotg1: usb@02184000 {
compatible = "fsl,imx6ul-usb", "fsl,imx27-usb";
reg = <0x02184000 0x4000>;
status = "disabled";
};
}
//aips3 节点
aips3: aips-bus@02200000 {
compatible = "fsl,aips-bus", "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02200000 0x100000>;
ranges;
//rngb 节点
rngb: rngb@02284000 {
compatible = "fsl,imx6sl-rng", "fsl,imx-rng", "imx-rng";
reg = <0x02284000 0x4000>;
};
}
}
}
到这里 asu.dts 这个小型设备树就编写好了,和 imx6ull.dtsi 类似。