本章我们就来详细的谈一谈设备树。掌握设备树是 Linux 驱动开发人员必备的技能!因为在新版本的 Linux 中,ARM 相关的驱动全部采用了设备树(也有支持老式驱动 的,比较少),最新出的 CPU 其驱动开发也基本都是基于设备树的,比如 ST 新出的 STM32MP157、NXP 的 I.MX8 系列等。
我所使用的是正点原子 I.MX6UALPHA 开发板,Linux版本为 4.1.15,支持设备树,所以后面的 Linux 驱动也都是基于设备树的。本章我们就来了解一下设备树的起源、重点学习一下有关设备树的知识点。
设备树(Device Tree),将这个词分开就是“设备”和“树”,描述设备树的文件叫做 DTS(Device Tree Source),这个 DTS 文件采用树形结构描述板级设备,也就是开发板上的设备信息,比如CPU 数量、 内存基地址、IIC 接口上接了哪些设备、SPI 接口上接了哪些设备等等,如图所示:
在图中,树的主干就是系统总线,IIC 控制器、GPIO 控制器、SPI 控制器等都是接到系统主线上的分支。IIC 控制器有分为 IIC1 和 IIC2 两种,其中 IIC1 上接了 FT5206 和 AT24C02这两个 IIC 设备,IIC2 上只接了 MPU6050 这个设备。DTS 文件的主要功能就是按照图中所示的结构来描述板子上的设备信息。
一般我们会使用.c 或.h 文件去定义那些板级信息文件,例如一些设备io的引脚定义啊,定时器,I2C 控制器、GPIO 控制器、SPI 控制器等,但是每年新出的 ARM 架构 芯片少说都在数十、数百款,Linux 内核下板级信息文件将会成指数级增长!这些板级信息文件 都是.c 或.h 文件,都会被硬编码进 Linux 内核中,导致 Linux 内核“虚胖”。
当 Linux 之父 linus 看到 ARM 社区向 Linux 内核添加了大量“无用”、冗余 的板级信息文件,不禁的发出了一句“This whole ARM thing is a f*cking pain in the ass”。
从此以 后 ARM 社区就引入了 PowerPC 等架构已经采用的设备树(Flattened Device Tree),将这些描述 板级硬件信息的内容都从 Linux 内中分离开来,用一个专属的文件格式来描述,这个专属的文 件就叫做设备树,文件扩展名为.dts。
总结一下,也就是因为以前在linux内核中,包含了许多个厂家的.c 或.h 板级信息文件,不包含的话系统就无法去启动那些设备,包含的话就太多了,所以将这些描述板级硬件信息的内容都从 Linux 内中分离开来,也就是内核不会包含这些信息了,这些信息你们那些厂家自己去用一个.dts(设备树)文件去描述,以后要给板子烧入系统,就把内核以及设备树文件一起烧入进入就可以了,这样就不会使得内核文件显得庞大且臃肿。
设备树源文件扩展名为.dts,但是我们在前面移植 Linux 的时候却一直在使 用.dtb 文件,那么 DTS 和 DTB 这两个文件是什么关系呢?DTS 是设备树源码文件,DTB 是将DTS 编译以后得到的二进制文件。将.c 文件编译为.o 需要用到 gcc 编译器,那么将.dts 编译为.dt需要什么工具呢?需要用到 DTC 工具!DTC 工具源码在 Linux 内核的 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
......
可以看出,DTC 工具依赖于 dtc.c、flattree.c、fstree.c 等文件,最终编译并链接出 DTC 这 个主机文件。如果要编译 DTS 文件的话只需要进入到 Linux 源码根目录下,然后执行如下命 令:
make all
或者
make dtbs
“make all”命令是编译 Linux 源码中的所有东西,包括 zImage,.ko 驱动模块以及设备 树,如果只是编译设备树的话建议使用“make dtbs”命令。
基于 ARM 架构的 SOC 有很多种,一种 SOC 又可以制作出很多款板子,每个板子都有一 个对应的 DTS 文件,那么如何确定编译哪一个 DTS 文件呢?我们就以 I.MX6ULL 这款芯片对 应的板子为例来看一下,打开 arch/arm/boot/dts/Makefile,有如下内容:
381 dtb-$(CONFIG_SOC_IMX6UL) += \
382 imx6ul-14x14-ddr3-arm2.dtb \
383 imx6ul-14x14-ddr3-arm2-emmc.dtb \
384 imx6ul-14x14-ddr3-arm2-flexcan2.dtb \
385 imx6ul-14x14-ddr3-arm2-gpmi-weim.dtb \
386 imx6ul-14x14-ddr3-arm2-mqs.dtb \
387 imx6ul-14x14-ddr3-arm2-spdif.dtb \
388 imx6ul-14x14-ddr3-arm2-wm8958.dtb \
389 imx6ul-14x14-evk.dtb \
390 imx6ul-14x14-evk-btwifi.dtb \
391 imx6ul-14x14-evk-csi.dtb \
392 imx6ul-14x14-evk-emmc.dtb \
393 imx6ul-14x14-evk-gpmi-weim.dtb \
394 imx6ul-14x14-evk-usb-certi.dtb \
395 imx6ul-14x14-lpddr2-arm2.dtb \
396 imx6ul-9x9-evk.dtb \
397 imx6ul-9x9-evk-btwifi.dtb \
398 imx6ul-9x9-evk-csi.dtb \
399 imx6ul-9x9-evk-ldo.dtb
400 dtb-$(CONFIG_SOC_IMX6ULL) += \
401 imx6ull-14x14-ddr3-arm2.dtb \
402 imx6ull-14x14-ddr3-arm2-adc.dtb \
403 imx6ull-14x14-ddr3-arm2-cs42888.dtb \
404 imx6ull-14x14-ddr3-arm2-ecspi.dtb \
405 imx6ull-14x14-ddr3-arm2-emmc.dtb \
406 imx6ull-14x14-ddr3-arm2-epdc.dtb \
407 imx6ull-14x14-ddr3-arm2-flexcan2.dtb \
408 imx6ull-14x14-ddr3-arm2-gpmi-weim.dtb \
409 imx6ull-14x14-ddr3-arm2-lcdif.dtb \
410 imx6ull-14x14-ddr3-arm2-ldo.dtb \
411 imx6ull-14x14-ddr3-arm2-qspi.dtb \
412 imx6ull-14x14-ddr3-arm2-qspi-all.dtb \
413 imx6ull-14x14-ddr3-arm2-tsc.dtb \
414 imx6ull-14x14-ddr3-arm2-uart2.dtb \
415 imx6ull-14x14-ddr3-arm2-usb.dtb \
416 imx6ull-14x14-ddr3-arm2-wm8958.dtb \
417 imx6ull-14x14-evk.dtb \
418 imx6ull-14x14-evk-btwifi.dtb \
419 imx6ull-14x14-evk-emmc.dtb \
420 imx6ull-14x14-evk-gpmi-weim.dtb \
421 imx6ull-14x14-evk-usb-certi.dtb \
422 imx6ull-alientek-emmc.dtb \
423 imx6ull-alientek-nand.dtb \
424 imx6ull-9x9-evk.dtb \
425 imx6ull-9x9-evk-btwifi.dtb \
426 imx6ull-9x9-evk-ldo.dtb
427 dtb-$(CONFIG_SOC_IMX6SLL) += \
当选中 I.MX6ULL 这个 SOC 以后(CONFIG_SOC_IMX6ULL=y),所有使用I.MX6ULL 这个 SOC 的板子对应的.dts 文件都会被编译为.dtb。如果我们使用 I.MX6ULL 新做 了一个板子,只需要新建一个此板子对应的.dts 文件,然后将对应的.dtb 文件名添加到dtb-$(CONFIG_SOC_IMX6ULL)下,这样在编译设备树的时候就会将对应的.dts 编译为二进的.dt文件。
我们基本上不会从头到尾重写一个.dts 文件,大多时候是直接在 SOC 厂商提供的.dts文件上进行修改。但是 DTS 文件语法我们还是需要简单学习一下,因为我们肯定需要修改.dts文件。DTS 语法非常的人性化,是一种 ASCII文本文件,不管是阅读还是修改都很方便。
和 C 语言一样,设备树也支持头文件,设备树的头文件扩展名为.dtsi。引用头文件的方法也和C语言很像。都是使用的#include,我们可以引用头文件类型为:.h 头文件、.dtsi 头文件、.dts 文件。例如imx6ull-14x14-evk.dts中。
一般.dtsi 文件用于描述 SOC 的内部外设信息,比如 CPU 架构、主频、外设寄存器地址范 围,比如 UART、IIC 等等。比如 imx6ull.dtsi 就是描述 I.MX6ULL 这颗 SOC 内部外设情况信息 的,内容如下:
/ {
aliases {
can0 = &flexcan1;
can1 = &flexcan2;
....
usbphy1 = &usbphy2;
};
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 {
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <0>;
clock-latency = <61036>; /* two CLK32 periods */
operating-points = <
/* kHz uV */
996000 1275000
792000 1225000
/*696000 1225000*/
528000 1175000
396000 1025000
198000 950000
>;
...
};
};
这里就是 cpu0 这个设备节点信息,这个节点信息描述了I.MX6ULL 这颗 SOC 所使用的 CPU 信息,比如架构是 cortex-A7,频率支持 996MHz、792MHz、528MHz、396MHz 和 198MHz 等等。在 imx6ull.dtsi 文件中不仅仅描述了 cpu0 这一个节点信息,I.MX6ULL 这颗 SOC 所有的外设都描述的清清楚楚,比如 ecspi1~4、uart1~8、usbphy1~2、i2c1~4等等。
设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设 备节点,每个节点都通过一些属性信息来描述节点信息,属性就是键—值对。以下是从imx6ull.dtsi 文件中缩减出来的设备树文件内容:
/{
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 行:
“/”是根节点,每个设备树文件只有一个根节点。细心的同学应该会发现,imx6ull.dtsi和 imx6ull-alientek-emmc.dts 这两个文件都有一个“/”根节点,这样不会出错吗?不会的,因为 这两个“/”根节点的内容会合并成一个根节点。
第 2、6 和 17 行:
aliases、cpus 和 intc 是三个子节点,在设备树中节点命名格式如下:
node-name@unit-address
还有一种命名格式是这样的:
label: node-name@unit-address
引入 label,目的是为了方便访问节点,可以直接通过&label 来访问这个节点,比如通过&cpu0 就可以访问“cpu@0”这个节点,而不需要输入完整的节点名字。再比如节点 “intc: interrupt-controller@00a01000”,节点 label 是 intc,而节点名字就很长,“interruptcontroller@00a01000”。很明显通过&intc 来访问“interrupt-controller@00a01000”这个节点要方便很多!
在上面我们可以知道,节点是由一堆的属性组成,节点都是具体的设备,不同的设备需要的属性不同,用户可以 自定义属性。除了用户自定义属性,有很多属性是标准属性,Linux 下的很多外设驱动都会使用 这些标准属性,本节我们就来学习一下几个常用的标准属性。
compatible 属性也叫做“兼容性”属性,这是非常重要的一个属性!compatible 属性的值是 一个字符串列表,compatible 属性用于将设备和驱动绑定起来。字符串列表用于选择设备所要使用的驱动程序,compatible 属性的值格式如下所示:
"manufacturer,model"
其中 manufacturer 表示厂商,model 一般是模块对应的驱动名字。
举个例子:imx6ull-alientekemmc.dts 中,sound 节点是 I.MX6U-ALPHA 开发板的音频设备节点。
可以看到sound 节点的 compatible 属性值如下:
compatible = "fsl,imx6ul-evk-wm8960","fsl,imx-audio-wm8960";
属性值有两个,分别为“fsl,imx6ul-evk-wm8960”和“fsl,imx-audio-wm8960”,其中
sound这个设备首先使用第一个兼容值在 Linux 内核里面查找,看看能不能找到与之匹配的驱动文件, 如果没有找到的话就使用第二个兼容值查。
model 属性值也是一个字符串,一般 model 属性描述设备模块信息,比如名字什么的,比 如: model = "wm8960-audio"。
status 属性看名字就知道是和设备状态有关的,status 属性值也是字符串,字符串是设备的 状态信息,可选的状态如表所示:
这两个属性的值都是无符号 32 位整形,#address-cells 和#size-cells 这两个属性可以用在任 何拥有子节点的设备中,用于描述子节点的地址信息。
举例:
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>;
};
}
节点 spi4 的#address-cells =<1> ,#size-cells =<1> ,说明 spi4 的子节点 reg 属 性中起始地址所占用的字长为 1,地址长度所占用的字长为 1。若是#size-cells =<0>,则表示地址长度所占用的字长为0,相当于设置了起始地址,而没 有设置地址长度。
reg 属性前面已经提到过,reg 属性的值一般是(address,length)对。reg 属性一般用于描述设备地址空间资源信息。一般reg属性都是和地址有关的内容,和地址相关的信息有两种:起始地址和地址长度,
reg 属性的格式为:
reg =
其中address是起始地址,length是地址长度
以上面那个aips3结点为例:
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02280000 0x4000>;
相当于设置了起始地址长度所占用的字长为1,地址长度所占用的字长也为1,起始地址为0x02280000,地址长度为0x40000。
ranges 是一个地址映射/转换表,ranges 属性每个项目由子地址、父地址和地址空间长度 这三部分组成:
child-bus-address, parent-bus-address, length
ranges属性值为空:
如果 ranges 属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换。
对于我们所使用的 I.MX6ULL 来说,子地址空间和父地址空间完全相同,因此会在 imx6ull.dtsi中找到大量的值为空的 ranges 属性,如下所示:
ranges属性值不为空:
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x0 0xe0000000 0x00100000>;
serial {
device_type = "serial";
compatible = "ns16550";
reg = <0x4600 0x100>;
clock-frequency = <0>;
interrupts = <0xA 0x8>;
interrupt-parent = <&ipic>;
};
};
第 5 行,节点 soc 定义的 ranges 属性,值为<0x0 0xe0000000 0x00100000>,此属性值指定了一个 1024KB(0x00100000)的地址范围,子地址空间的物理起始地址为 0x0,父地址空间的物 理起始地址为 0xe0000000。
第 10 行,serial 是串口设备节点,reg 属性定义了 serial 设备寄存器的起始地址为 0x4600, 寄存器长度为 0x100。经过地址转换,serial 设备可以从 0xe0004600 开始进行读写操作,0xe0004600=0x4600+0xe0000000。
没有使用设备树以前,uboot 会向 Linux 内核传递一个叫做 machine id 的值,machine id也就是设备 ID,告诉 Linux 内核自己是个什么设备,看看 Linux 内核是否支持。Linux 内核是支持很多设备的,针对每一个设备(板子),Linux内核都用MACHINE_START和MACHINE_END来定义一个 machine_desc 结构体来描述这个设备。
MACHINE_START和MACHINE_END宏定义在arch/arm/include/asm/mach/arch.h 中:
在文件 arch/arm/mach-imx/machmx35_3ds.c 中有如下初始化:
当 Linux 内核引入设备树以后就不再使用 MACHINE_START了,而是换为DT_MACHINE_START。DT_MACHINE_START 也定义在文件 arch/arm/include/asm/mach/arch.h里面,定义如下
可以看出,DT_MACHINE_START 和 MACHINE_START 基本相同,只是.nr 的设置不同, 在 DT_MACHINE_START 里面直接将.nr 设置为~0。说明引入设备树以后不会再根据 machine id 来检查 Linux 内核是否支持某个设备了。
打开文件 arch/arm/mach-imx/mach-imx6ul.c,有如下所示内容:
machine_desc 结构体中有个.dt_compat 成员变量,此成员变量保存着本设备兼容属性 。
只要某个设备(板子)根节点“/”的 compatible 属性值与imx6ul_dt_compat 表中的任何一个值相等,那么就表示 Linux 内核支持此设备。
在我们板子的设备树imx6ull-alientek-emmc.dts中,根节点的 compatible 属性值如下:
接下来我们简单看一下 Linux 内核是如何根据设备树根节点的 compatible 属性来匹配出对 应的 machine_desc。这里就不详细介绍了,贴个流程图看看:
产品开发过程中可能面临着频繁的需求更改,比如第一版硬件上有一个 IIC 接口的六轴芯片MPU6050,第二版硬件又要把这个 MPU6050 更换为 MPU9250 等。一旦硬件修改了,我们就要同步的修改设备树文件,毕竟设备树是描述板子硬件信息的文件。假设有个六轴芯片fxls8471,fxls8471 要接到 I.MX6U-ALPHA 开发板的 I2C1 接口上,那么相当于需要在 i2c1 这 个节点上添加一个 fxls8471 子节点。
但是这样会有个问题!i2c1 节 点是定义在 imx6ull.dtsi 文件中的,而 imx6ull.dtsi 是设备树头文件,其他所有使用到 I.MX6ULL这颗 SOC 的板子都会引用 imx6ull.dtsi 这个文件。直接在 i2c1 节点中添加 fxls8471 就相当于在 其他的所有板子上都添加了 fxls8471 这个设备,但是其他的板子并没有这个设备啊!所以引入另外一个内容,那就是向节点追加数据。
例如,我们要向i2c1 节点追加一个名为 fxls8471 的子节点,而且不能影响到其他使用到 I.MX6ULL 的板子。通过&label 来访问节点,然后直接在里面编写要追加或者修改的内容。
I.MX6U-ALPHA 开发板使用的设备树文件为 imx6ull-alientek-emmc.dts,因此我们需要在imx6ull-alientek-emmc.dts 文件中完成数据追加的内容,方式如下:
Linux 内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的/proc/devicetree 目录下根据节点名字创建不同文件夹,如图 所示:
在根节点“/”中有两个特殊的子节点:aliases 和 chosen,我们接下来看一下这两个特殊的 子节点。
打开 imx6ull.dtsi 文件,aliases 节点内容如下所示:
单词 aliases 的意思是“别名”,因此 aliases 节点的主要功能就是定义别名,定义别名的目 的就是为了方便访问节点。不过我们一般会在节点命名的时候会加上 label,然后通过&label来访问节点,这样也很方便,而且设备树里面大量的使用&label 的形式来访问节点。
chosen 并不是一个真实的设备,chosen 节点主要是为了 uboot 向 Linux 内核传递数据,重 点是 bootargs 参数。一般.dts 文件中 chosen 节点通常为空或者内容很少,imx6ull-alientekemmc.dts 中 chosen 节点内容如下所示:
chosen 节点仅仅设置了属性“stdout-path”,表示标准输 出使用 uart1。但是当我们到/proc/device-tree/chosen 目录里面,会发现多了 bootargs 这个 属性。
输入 cat 命令查看 bootargs 这个文件的内容,结果如图
这是因为,uboot 在启动 Linux 内核的时候会将 bootargs 的值传递给 Linux内核,bootargs 会作为 Linux 内核的命令行参数。所以是 uboot 自己在 chosen 节点里面添加了 bootargs 属性!并且设置 bootargs 属性的值为 bootargs环境变量的值。
那么uboot是怎么向内核的 chosen 节点里面添加了 bootargs 属性的呢,流程如下:
框起来的部分就是函数 do_bootm_linux 函数的执行流程,也就是说do_bootm_linux 函数会通过一系列复杂的调用,最终通过 fdt_chosen 函数在 chosen 节点中加入 了 bootargs 属性。而我们通过 bootz 命令启动 Linux 内核的时候会运行 do_bootm_linux 函数, 至此,真相大白,一切事情的源头都源于如下命令:
当我们输入上述命令并执行以后,do_bootz 函数就会执行。
Linux 内核在启动的时候会解析 DTB 文件,然后在/proc/device-tree 目录下生成相应的设备 树节点文件。接下来我们简单分析一下 Linux 内核是如何解析 DTB 文件的,流程如图
从图中可以看出,在 start_kernel 函数中完成了设备树节点解析的工作,最终实际工作的函数为 unflatten_dt_node。
设备树是用来描述板子上的设备信息的,不同的设备其信息不同,反映到设备树中就是属 性不同。那么我们在设备树中添加一个硬件对应的节点的时候从哪里查阅相关的说明呢?
在Linux 内核源码中有详细的.txt 文档描述了如何添加节点,这些.txt 文档叫做绑定文档,路径为:Linux 源码目录/Documentation/devicetree/bindings,如图所示:
比如我们现在要想在 I.MX6ULL 这颗 SOC 的 I2C 下添加一个节点,那么就可以查看:Documentation/devicetree/bindings/i2c/i2c-imx.txt,此文档详细的描述了 I.MX 系列的 SOC 如何 在设备树中添加 I2C 设备节点,文档内容如下所示: