Linux设备树

在3.x 版本(具体哪个版本笔者也无从考证)以前的Linux 内核中ARM 架构并没有采用设备树。前面章节中我们多次提到“设备树”这个概念,在新版本的Linux 中,ARM相关的驱动全部采用了设备树(也有支持老式驱动的,比较少),最新出的CPU 其驱动开发也基本都是基于设备树的。本章我们就来了解一下设备树的起源、重点学习一下设备树语法。

什么是设备树

在这里插入图片描述

设备树(Device Tree),将这个词分开就是“设备”和“树”,描述设备树的文件叫做DTS(Device Tree Source),这个DTS 文件采用树形结构描述板级设备,也就是开发板上的设备信息,比如CPU 数量、内存基地址、IIC 接口上接了哪些设备、SPI 接口上接了哪些设备等等,如图43.1.1所示:
在这里插入图片描述

在图43.1.1 中,树的主干就是系统总线,IIC 控制器、GPIO 控制器、SPI 控制器等都是接到系统主线上的分支。IIC 控制器有分为IIC1 和IIC2 两种,其中IIC1 上接了FT5206 和AT24C02这两个IIC 设备,IIC2 上只接了MPU6050 这个设备。DTS 文件描述设备信息是有相应的语法规则要求的,稍后会详细。

在没有设备树的时候Linux 是如何描述ARM 架构中的板级信息呢?在Linux 内核源码中大量的arch/arm/mach-xxx 和arch/arm/plat-xxx 文件夹,这些文件夹里面的文件就是对应平台下的板级信息。比如在三星arch/arm/mach-smdk2440.c 中有如下内容(结构体方式):

90 static struct s3c2410fb_display smdk2440_lcd_cfg __initdata = {
91
92 .lcdcon5 = S3C2410_LCDCON5_FRM565 |
93 S3C2410_LCDCON5_INVVLINE |
94 S3C2410_LCDCON5_INVVFRAME |
95 S3C2410_LCDCON5_PWREN |
96 S3C2410_LCDCON5_HWSWP,
......
113 };
114
115 static struct s3c2410fb_mach_info smdk2440_fb_info __initdata = {
116 .displays = &smdk2440_lcd_cfg,
117 .num_displays = 1,
118 .default_display = 0,
......
133 };
134
135 static struct platform_device *smdk2440_devices[] __initdata = {
136 &s3c_device_ohci,
137 &s3c_device_lcd,
138 &s3c_device_wdt,
139 &s3c_device_i2c0,
140 &s3c_device_iis,
141 };

上述代码中的结构体变量smdk2440_fb_info 就是描述SMDK2440 这个开发板上的LCD 信息的,结构体指针数组smdk2440_devices 描述的SMDK2440 这个开发板上的所有平台相关信息。这个仅仅是使用2440 这个芯片的SMDK2440 开发板下的LCD 信息,SMDK2440 开发板还有很多的其他外设硬件和平台硬件信息。随着智能手机的发展,每年新出的ARM 架构芯片至少都在数十、数百款,这些板级信息文件都是.c 或.h 文件都会被硬编码进Linux 内核中,导致Linux 内核“虚胖”,Linux应主要聚焦内核调度算法。

从此以后ARM 社区就引入了PowerPC 等架构已经采用的设备树(Flattened Device Tree),将这些描述板级硬件信息的内容都从Linux 内中分离开来,放到一个单独的文件夹内,这就叫设备树,文件扩展名为.dts。

在这里插入图片描述

一个SOC 可以作出很多不同的板子,这些不同的板子肯定是有共同的信息,将这些共同的信息提取出来作为一个通用的.dtsi 文件,其他的 .dts 文件直接引用这个通用文件,类似于C 语言中的头文件。

  • 一般.dts 描述板级信息(也就是开发板上有哪些IIC 设备、SPI 设备等)
  • .dtsi 描述SOC级信息(也就是SOC 有几个CPU、主频是多少、各个外设控制器信息等)。

DTS、DTB 和DTC

DTS 和DTB 这两个文件是什么关系呢?

  • DTS 是设备树源码文件
  • DTB 是将DTS 编译以后得到的可执行二进制文件
  • DTC 是将.dts 编译为.dtb的编译工具

DTC 工具源码在Linux 内核scripts/dtc/Makefile 文件内容如下:

1 hostprogs-y := dtc
2 always := $(hostprogs-y)
3
4 dtc-objs:= dtc.o flattree.o fstree.o data.o livetree.o treesource.o \
5 srcpos.o checks.o util.o
6 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 \
......
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) += \
428 imx6sll-lpddr2-arm2.dtb \
429 imx6sll-lpddr3-arm2.dtb \
......

可以看出,当选中I.MX6ULL 这个SOC 以后(CONFIG_SOC_IMX6ULL=y),所有使用到I.MX6ULL 这个SOC 的板子对应的.dts 文件都会被编译为.dtb。如果我们使用I.MX6ULL 新做了一个板子,只需要新建一个此板子对应的.dts 文件,然后将对应的.dtb 文件名添加到dtb-$(CONFIG_SOC_IMX6ULL)下,这样在编译设备树的时候就会将对应的.dts 编译为二进制的.dtb文件。

第422 和423 行就是我们在给正点原子的I.MX6U-ALPHA 开发板移植Linux 系统的时候添加的设备树。

.dtb 文件怎么使用?前面讲解Uboot 移植、Linux 内核移植的时候已经提到了,uboot 中使用bootz 或bootm命令向Linux 内核传递二进制设备树文件(.dtb)。

DTS语法

虽然我们基本上不会从头到尾重写一个.dts 文件,大多时候是直接在SOC 厂商提供的.dts文件上进行修改。但是DTS 文件语法我们还是需要详细的学习一遍,因为我们肯定需要修改.dts文件。DTS 语法非常的人性化,是一种ASCII文本文件,不管是阅读还是修改都很方便。

本节我们就以imx6ull-alientek-emmc.dts 这个文件为例来讲解一下DTS 语法。关于设备树详细的语法规则请参考《Devicetree SpecificationV0.2.pdf 》和《Power_ePAPR_APPROVED_v1.12.pdf》这两份文档,此两份文档已经放到了开发板光盘中,路径为:4 、参考资料->Devicetree SpecificationV0.2.pdf 、4 、参考资料-> Power_ePAPR_APPROVED_v1.12.pdf

.dtsi 头文件

和C 语言一样,设备树也支持头文件,设备树的头文件扩展名为.dtsi。在imx6ull-alientek-emmc.dts 中有如下所示内容:

12 #include <dt-bindings/input/input.h>
13 #include "imx6ull.dtsi"

第12 行,使用“#include”来引用“input.h”这个.h 头文件。
第13 行,使用“#include”来引用“imx6ull.dtsi”这个.dtsi 头文件。

看到这里,大家可能会疑惑,不是说设备树的扩展名是.dtsi 吗?为什么也可以直接引用C语言中的.h 头文件呢?这里并没有错,.dts文件引用C 语言中的.h 文件,甚至也可以引用.dts 文件,打开imx6ull-14x14-evk-gpmi-eim.dts
这个文件,此文件中有如下内容:

9 #include "imx6ull-14x14-evk.dts"

可以看出,示例代码43.3.1.2 中直接引用了.dts 文件,因此在.dts> 设备树文件中,可以通过“#include”来引用.h、.dtsi 和.dts 文件。只是,我们在编写设备树头文件的时候最好选择.dtsi> 后缀。

一般.dtsi 文件用于描述SOC 的内部外设信息,比如CPU 架构、主频等;外设寄存器地址范围(比如UART、IIC 等)。imx6ull.dtsi 就是描述I.MX6ULL 这颗SOC 内部外设情况信息的,内容如下:

10 #include <dt-bindings/clock/imx6ul-clock.h>
11 #include <dt-bindings/gpio/gpio.h>
12 #include <dt-bindings/interrupt-controller/arm-gic.h>
13 #include "imx6ull-pinfunc.h"
14 #include "imx6ull-pinfunc-snvs.h"
15 #include "skeleton.dtsi"
16
17 / {
18 aliases {
19 can0 = &flexcan1;
......
48 };
49
50 cpus {
51 #address-cells = <1>;
52 #size-cells = <0>;
53
54 cpu0: cpu@0 {
55 compatible = "arm,cortex-a7";
56 device_type = "cpu";
......
89 };
90 };
91
92 intc: interrupt-controller@00a01000 {
93 compatible = "arm,cortex-a7-gic";
94 #interrupt-cells = <3>;
95 interrupt-controller;
96 reg = <0x00a01000 0x1000>,
97 <0x00a02000 0x100>;
98 };
99
100 clocks {
101 #address-cells = <1>;
102 #size-cells = <0>;
103
104 ckil: clock@0 {
105 compatible = "fixed-clock";
106 reg = <0>;
107 #clock-cells = <0>;
108 clock-frequency = <32768>;
109 clock-output-names = "ckil";
110 };
......
135 };
136
137 soc {
138 #address-cells = <1>;
139 #size-cells = <1>;
140 compatible = "simple-bus";
141 interrupt-parent = <&gpc>;
142 ranges;
143
144 busfreq {
145 compatible = "fsl,imx_busfreq";
......
162 };
197
198 gpmi: gpmi-nand@01806000{
199 compatible = "fsl,imx6ull-gpmi-nand", "fsl, imx6ul-gpmi-nand";
200 #address-cells = <1>;
201 #size-cells = <1>;
202 reg = <0x01806000 0x2000>, <0x01808000 0x4000>;
......
216 };
......
1177 };
1178 };

示例代码43.3.1.3 中第54~89 行就是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 文件中缩减出来的设备树文件内容:

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 }

第1 行,“/”是根节点,每个设备树文件只有一个根节点。细心的同学应该会发现,imx6ull.dtsi和imx6ull-alientek-emmc.dts 这两个文件都有一个“/”根节点,这样不会出错吗?不会的,因为这两个“/”根节点的内容会合并成一个根节点。

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

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

引入标签:
我们在示例代码43.3.2.1 中我们看到的节点命名却如下所示:

c cpu0:cpu@0

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

c label: node-name@unit-address

引入label 的目的就是为了方便访问节点,可以直接通过&label 来访问这个节点,比如通过&cpu0就可以访问“cpu@0”这个节点,而不需要输入完整的节点名字。再比如节点“intc:
interrupt-controller@00a01000”,节点label是intc,而节点名字就很长了,为“interrupt-controller@00a01000”。很明显通过&intc来访问“interrupt-controller@00a01000”这个节点要方便很多!

第10 行,cpu0 也是一个节点,只是cpu0 是cpus 的子节点
每个节点都有不同属性,不同的属性又有不同的内容,属性都是键值对,值可以为空或任意的字节流。设备树源码中常用的几种数据形式如下所示:
①、字符串

compatible = "arm,cortex-a7";

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

reg = <0>;

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

reg = <0 0x123456 100>;

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

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

上述代码设置属性compatible 的值为“fsl,imx6ull-gpmi-nand”和“fsl, imx6ul-gpmi-nand”。

设备标准属性(compatible绑定、status状态、reg设备寄存器地址…)

节点是由一堆的属性组成,节点都是具体的设备,不同的设备需要的属性不同,用户可以自定义属性,也可以使用标准属性,Linux 下的很多外设驱动都会使用这些标准属性,本节我们就来学习一下几个常用的标准属性。

1、compatible 属性

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

"manufacturer,model"
  • 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";

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

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

632 static const struct of_device_id imx_wm8960_dt_ids[] = {
633 { .compatible = "fsl,imx-audio-wm8960", },
634 { /* sentinel */ }
635 };
636 MODULE_DEVICE_TABLE(of, imx_wm8960_dt_ids);
637
638 static struct platform_driver imx_wm8960_driver = {
639 .driver = {
640 .name = "imx-wm8960",
641 .pm = &snd_soc_pm_ops,
642 .of_match_table = imx_wm8960_dt_ids,
643 },
644 .probe = imx_wm8960_probe,
645 .remove = imx_wm8960_remove,
646 };

第632~635 行的数组imx_wm8960_dt_ids 就是imx-wm8960.c 这个驱动文件的匹配表,此匹配表只有一个匹配值“fsl,imx-audio-wm8960”。如果在设备树中有哪个节点的compatible 属 性值与此相等,那么这个节点就会使用此驱动文件。

第642 行,wm8960 采用了platform_driver 驱动模式,关于platform_driver 驱动后面会讲解。此行设置.of_match_table 为imx_wm8960_dt_ids,也就是设置这个platform_driver 所使用的OF 匹配表。

2、model 属性

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

model = "wm8960-audio";

3、status 属性

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

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

4、#address-cells 和#size-cells 属性(决定如何编写reg属性值)

这两个属性的值都是无符号32 位整形,用于描述子节点的地址信息

  • #address-cells 属性值决定了子节点reg属性中地址信息所占用的字长(32 位)
  • #size-cells 属性值决定了子节点reg 属性中长度信息所占的字长(32 位)。

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

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

每个“address length”组合表示一个地址范围

  • address 是起始地址,length 是地址长度
  • #address-cells 表明address 这个数据所占用的字长,#size-cells 表明length 这个数据所占用的字长,比如下面的两种情况:
1 spi4 {
2 		compatible = "spi-gpio";
3 		#address-cells = <1>;
4 		#size-cells = <0>;
5
6 		gpio_spi: gpio_spi@0 {
7 		compatible = "fairchild,74hc595";
8 		reg = <0>;
9 		};
10 };
11
12 aips3: aips-bus@02200000 {
13 		compatible = "fsl,aips-bus", "simple-bus";
14 		#address-cells = <1>;
15 		#size-cells = <1>;
16
17 		dcp: dcp@02280000 {
18 		compatible = "fsl,imx6sl-dcp";
19 		reg = <0x02280000 0x4000>;
20 		};
21 };

第3,4 行,节点spi4 的#address-cells = <1>,#size-cells = <0>,说明spi4 的子节点reg 属性中起始地址所占用的字长为1,地址长度所占用的字长为0。

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


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

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

5、reg 属性

reg 属性前面已经提到过了,reg 属性的值一般是(address,length)对。reg 属性一般用于描述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息,比如在imx6ull.dtsi 中有如下内容:

323 uart1: serial@02020000 {
324 		compatible = "fsl,imx6ul-uart",
325 		"fsl,imx6q-uart", "fsl,imx21-uart";
326 		reg = <0x02020000 0x4000>;
327 		interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
328 		clocks = <&clks IMX6UL_CLK_UART1_IPG>,
329 		<&clks IMX6UL_CLK_UART1_SERIAL>;
330 		clock-names = "ipg", "per";
331 		status = "disabled";
332 };

上述代码是节点uart1,uart1 节点描述了I.MX6ULL 的UART1 相关信息,重点是第326 行的reg 属性。其中uart1 的父节点aips1: aips-bus@02000000 设置了#address-cells = <1>、#size-cells = <1>,因此reg 属性中address=0x02020000,length=0x4000。

查阅《I.MX6ULL 参考手册》可知,I.MX6ULL 的UART1 寄存器首地址为0x02020000,但是UART1 的地址长度(范围)并没有0x4000 这么多,这里我们重点是获取UART1 寄存器首地址。

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 确定此地址长度所占用的字长。

如果ranges 属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换,对于我们所使用的I.MX6ULL 来说,子地址空间和父地址空间完全相同,因此会在imx6ull.dtsi中找到大量的值为空的ranges 属性,如下所示:

137 soc {
138 #address-cells = <1>;
139 #size-cells = <1>;
140 compatible = "simple-bus";
141 interrupt-parent = <&gpc>;
142 ranges;
......
1177 }

第142 行定义了ranges 属性,但是ranges 属性值为空。
ranges 属性不为空的示例代码如下所示:

1 soc {
2 compatible = "simple-bus";
3 #address-cells = <1>;
4 #size-cells = <1>;
5 ranges = <0x0 0xe0000000 0x00100000>;
6
7 serial {
8 device_type = "serial";
9 compatible = "ns16550";
10 reg = <0x4600 0x100>;
11 clock-frequency = <0>;
12 interrupts = <0xA 0x8>;
13 interrupt-parent = <&ipic>;
14 };
15 };

第5 行,节点soc 定义的ranges 属性,值为<0x0 0xe0000000 0x00100000>,此属性值指定了一个1024KB(0x00100000)的地址范围,子地址空间的物理起始地址为0x0,父地址空间的物理起始地址为0xe0000000。

第10 行,serial 是串口设备节点,reg 属性定义了serial 设备寄存器的起始地址为0x4600,寄存器长度为0x100。经过地址转换,serial 设备可以从0xe0004600 开始进行读写操作,0xe0004600=0x4600+0xe0000000。

7、name 属性(弃用)

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

8、device_type 属性(弃用)

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

54 cpu0: cpu@0 {
55 compatible = "arm,cortex-a7";
56 device_type = "cpu";
57 reg = <0>;
......
89 };

关于标准属性就讲解这么多,其他的比如中断、IIC、SPI 等使用的标准属性等到具体的例程再讲解。

根节点compatible属性(设备怎么匹配到对应的内核驱动?了解)

每个节点都有compatible 属性,根节点“/”也不例外,imx6ull-alientek-emmc.dts 文件中根节点的compatible 属性内容如下所示:

14 / {
15 model = "Freescale i.MX6 ULL 14x14 EVK Board";
16 compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";
......
148 }

可以看出,compatible 有两个值:“fsl,imx6ull-14x14-evk”和“fsl,imx6ull”。

  • 设备节点的compatible 属性值是为了匹配Linux 内核中的驱动程序
  • 根节点的compatible 属性可以知道我们所使用的设备,一般第一个值描述了所使用的硬件设备名字,比如这里使用的是“imx6ull-14x14-evk”这个设备,第二个值描述了设备所使用的SOC,比如这里使用的是“imx6ull”这颗SOC。

Linux 内核会通过根节点的compoatible 属性查看是否支持此设备,如果支持的话设备就会启动Linux 内核。接下来我们就来学习一下Linux 内核在使用设备树前后是如何判断是否支持某款设备的。

1、使用设备树之前设备匹配方法(machine id)

在没有使用设备树以前,uboot 会向Linux 内核传递一个叫做machine id(设备ID)的值,告诉Linux 内核自己是个什么设备,看看Linux 内核是否支持。Linux 内核是支持很多设备的,针对每一个设备(板子),Linux内核都用MACHINE_START 和MACHINE_END来定义一个machine_desc 结构体来描述这个设备,比如在文件arch/arm/mach-imx/mach-mx35_3ds.c 中有如下定义:

613 MACHINE_START(MX35_3DS, "Freescale MX35PDK")
614 /* Maintainer: Freescale Semiconductor, Inc */
615 .atag_offset = 0x100,
616 .map_io = mx35_map_io,
617 .init_early = imx35_init_early,
618 .init_irq = mx35_init_irq,
619 .init_time = mx35pdk_timer_init,
620 .init_machine = mx35_3ds_init,
621 .reserve = mx35_3ds_reserve,
622 .restart = mxc_restart,
623 MACHINE_END

上述代码就是定义了“Freescale MX35PDK”这个设备,其中MACHINE_START 和MACHINE_END 定义在文件arch/arm/include/asm/mach/arch.h 中,内容如下:

#define MACHINE_START(_type,_name) \
static const struct machine_desc __mach_desc_##_type \
__used \
__attribute__((__section__(".arch.info.init"))) = { \
.nr = MACH_TYPE_##_type, \
.name = _name,
#define MACHINE_END \
};

根据MACHINE_START 和MACHINE_END 的宏定义,将示例代码43.3.4.2 展开后如下所示:

1 static const struct machine_desc __mach_desc_MX35_3DS \
2 __used \
3 __attribute__((__section__(".arch.info.init"))) = {
4 .nr = MACH_TYPE_MX35_3DS,
5 .name = "Freescale MX35PDK",
6 /* Maintainer: Freescale Semiconductor, Inc */
7 .atag_offset = 0x100,
8 .map_io = mx35_map_io,
9 .init_early = imx35_init_early,
10 .init_irq = mx35_init_irq,
11 .init_time = mx35pdk_timer_init,
12 .init_machine = mx35_3ds_init,
13 .reserve = mx35_3ds_reserve,
14 .restart = mxc_restart,
15 };

从示例代码43.3.4.3 中可以看出,这里定义了一个machine_desc 类型的结构体变量__mach_desc_MX35_3DS ,这个变量存储在“.arch.info.init ”段中。第4 行的MACH_TYPE_MX35_3DS 就是“Freescale MX35PDK ”这个板子的machine id 。

MACH_TYPE_MX35_3DS 定义在文件include/generated/mach-types.h 中,此文件定义了大量的machine id,内容如下所示:

15 #define MACH_TYPE_EBSA110 0
16 #define MACH_TYPE_RISCPC 1
17 #define MACH_TYPE_EBSA285 4
18 #define MACH_TYPE_NETWINDER 5
19 #define MACH_TYPE_CATS 6
20 #define MACH_TYPE_SHARK 15
21 #define MACH_TYPE_BRUTUS 16
22 #define MACH_TYPE_PERSONAL_SERVER 17
......
287 #define MACH_TYPE_MX35_3DS 1645
......
1000 #define MACH_TYPE_PFLA03 4575

第287 行就是MACH_TYPE_MX35_3DS 的值,为1645。
前面说了,uboot 会给Linux 内核传递machine id 这个参数,Linux 内核将machine id 与这些MACH_TYPE_XXX 宏进行对比,如果相等的话就表示Linux 内核支持这个设备,如果不支持的话那么这个设备就没法启动Linux 内核。

2、使用设备树以后的设备匹配方法(设备根节点“/”的compatible 属性值匹配)

当Linux 内核引入设备树以后就不再使用MACHINE_START 了,而是换为了
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 和MACHINE_START 基本相同,只是.nr 的设置不同,在DT_MACHINE_START 里面将.nr 设置为~0,说明引入设备树以后不再根据machine id 来检查Linux内核是否支持某个设备
打开文件arch/arm/mach-imx/mach-imx6ul.c,有如下所示内容:

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 成员变量(保存着本设备兼容属性),示例代码43.3.4.5 中设置.dt_compat = imx6ul_dt_compat,imx6ul_dt_compat 表里面有"fsl,imx6ul"和"fsl,imx6ull"这两个兼容值。只要某个设备(板子)根节点“/”的compatible 属性值与imx6ul_dt_compat 表中的任何一个值相等,那么就表示Linux 内核支持此设备。imx6ull-alientek-emmc.dts 中根节点的compatible 属性值如下:

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

其中“fsl,imx6ull”与imx6ul_dt_compat 中的“fsl,imx6ull”匹配,因此I.MX6U-ALPHA 开发板可以正常启动Linux 内核。如果将imx6ull-alientek-emmc.dts 根节点的compatible 属性改为其他的值,比如:

compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ullll"

重新编译DTS,并用新的DTS 启动Linux 内核,结果如图43.3.4.1 所示的错误提示:
在这里插入图片描述
当我们修改了根节点compatible 属性内容以后,因为Linux 内核找不到对应的设备,因此Linux 内核无法启动。在uboot 输出Starting kernel…以后就再也没有其他信息输出了。

接下来我们简单看一下Linux 内核是如何根据设备树根节点的compatible 属性来匹配出对应的machine_desc,Linux 内核调用start_kernel 函数来启动内核,start_kernel 函数会调用setup_arch 函数来匹配machine_desc,setup_arch 函数定义在文件arch/arm/kernel/setup.c 中,函数内容如下(有缩减):

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 中,内容如下(有缩减):

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 中,内容如下(有缩减):

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 属性找到对应的设备的函数调用过程,如图43.3.4.2 所示:

在这里插入图片描述

向设备节点添加或修改内容(不要直接添加在.dtsi头文件,通过&在各自的.dts里添加)

产品开发过程中可能面临着频繁的需求更改,比如第一版硬件上有一个IIC 接口的六轴芯片MPU6050,第二版硬件又要把这个IIC六轴芯片MPU6050 更换为MPU9250 等。一旦硬件修改了,我们就要同步的修改设备树文件,毕竟设备树是描述板子硬件信息的文件。假设现在有个六轴芯片fxls8471,fxls8471 要接到I.MX6U-ALPHA 开发板的I2C1 接口上,那么相当于需要在i2c1 这个节点上添加一个fxls8471 子节点。先看一下I2C1 接口对应的节点,打开文件imx6ull.dtsi 文件,找到如下所示内容:

937 i2c1: i2c@021a0000 {
938 #address-cells = <1>;
939 #size-cells = <0>;
940 compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";
941 reg = <0x021a0000 0x4000>;
942 interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;
943 clocks = <&clks IMX6UL_CLK_I2C1>;
944 status = "disabled";
945 };

示例代码43.3.5.1 就是I.MX6ULL 的I2C1 节点,现在要在i2c1 节点下创建一个子节点,这个子节点就是fxls8471,最简单的方法就是在i2c1 下直接添加一个名为fxls8471 的子节点,如下所示:

937 i2c1: i2c@021a0000 {
938 #address-cells = <1>;
939 #size-cells = <0>;
940 compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";
941 reg = <0x021a0000 0x4000>;
942 interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;
943 clocks = <&clks IMX6UL_CLK_I2C1>;
944 status = "disabled";
945
946 //fxls8471子节点
947 fxls8471@1e {
948 compatible = "fsl,fxls8471";
949 reg = <0x1e>;
950 };
951 };

第947~950 行就是添加的fxls8471 这个芯片对应的子节点。但是这样会有个问题!i2c1 节点是定义在imx6ull.dtsi 文件中的,而imx6ull.dtsi 是设备树头文件,其他所有使用到I.MX6ULL这颗SOC 的板子都会引用imx6ull.dtsi 这个文件。直接在i2c1 节点中添加fxls8471 就相当于在其他的所有板子上都添加了fxls8471 这个设备,但是其他的板子并没有这个设备啊!因此,按照示例代码43.3.5.2 这样写肯定是不行的。

这里就要引入另外一个内容,那就是如何向节点追加数据,我们现在要解决的就是如何向i2c1 节点追加一个名为fxls8471 的子节点,而且不能影响到其他使用到I.MX6ULL 的板子。
I.MX6U-ALPHA 开发板使用的设备树文件为imx6ull-alientek-emmc.dts,因此我们需要在imx6ull-alientek-emmc.dts 文件中完成数据追加的内容,方式如下:

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

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

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 };

示例代码43.3.5.4 就是向i2c1 节点添加/修改数据,比如第225 行的属性“clock-frequency”就表示i2c1 时钟为100KHz。“clock-frequency”就是新添加的属性。
第228 行,将status 属性的值由原来的disabled 改为okay。
第230~234 行,i2c1 子节点mag3110,因为NXP 官方开发板在I2C1 上接了一个磁力计芯片mag3110,正点原子的I.MX6U-ALPHA 开发板并没有使用mag3110。
第236~242 行,i2c1 子节点fxls8471,同样是因为NXP 官方开发板在I2C1 上接了fxls8471这颗六轴芯片。
因为示例代码43.3.5.4 中的内容是imx6ull-alientek-emmc.dts 这个文件内的,所以不会对使用I.MX6ULL 这颗SOC 的其他板子造成任何影响。这个就是向节点追加或修改内容,重点就是通过&label 来访问节点,然后直接在里面编写要追加或者修改的内容。

创建小型模板设备树(了解)

上一节已经对DTS 的语法做了比较详细的讲解,本节我们就根据前面讲解的语法,从头到尾编写一个小型的设备树文件。当然了,这个小型设备树没有实际的意义,做这个的目的是为了掌握设备树的语法。在实际产品开发中,我们是不需要完完全全的重写一个.dts 设备树文件,一般都是使用SOC 厂商提供好的.dts 文件,我们只需要在上面根据自己的实际情况做相应的修
改即可。在编写设备树之前要先定义一个设备,我们就以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。

为了简单起见,我们就在设备树里面就实现这些内容即可,首先,搭建一个仅含有根节点“/”的基础的框架,新建一个名为myfirst.dts 文件,在里面输入如下所示内容:

1 / {
2 compatible = "fsl,imx6ull-alientek-evk", "fsl,imx6ull";
3 }

设备树框架很简单,就一个根节点“/”,根节点里面只有一个compatible 属性。我们就在这个基础框架上面将上面列出的内容一点点添加进来。

1、添加cpus 节点
首先添加CPU 节点,I.MX6ULL 采用Cortex-A7 架构,而且只有一个CPU,因此只有一个cpu0 节点,完成以后如下所示:

1 / {
2 compatible = "fsl,imx6ull-alientek-evk", "fsl,imx6ull";
3
4 cpus {
5 #address-cells = <1>;
6 #size-cells = <0>;
7
8 //CPU0节点
9 cpu0: cpu@0 {
10 compatible = "arm,cortex-a7";
11 device_type = "cpu";
12 reg = <0>;
13 };
14 };
15 }

第4~14 行,cpus 节点,此节点用于描述SOC 内部的所有CPU,因为I.MX6ULL 只有一个CPU,因此只有一个cpu0 子节点。

2、添加soc 节点
像uart,iic 控制器等等这些都属于SOC 内部外设,因此一般会创建一个叫做soc 的父节点来管理这些SOC 内部外设的子节点,添加soc 节点以后的myfirst.dts 文件内容如下所示:

1 / {
2 compatible = "fsl,imx6ull-alientek-evk", "fsl,imx6ull";
3
4 cpus {
5 #address-cells = <1>;
6 #size-cells = <0>;
7
8 //CPU0节点
9 cpu0: cpu@0 {
10 compatible = "arm,cortex-a7";
11 device_type = "cpu";
12 reg = <0>;
13 };
14 };
15
16 //soc节点
17 soc {
18 #address-cells = <1>;
19 #size-cells = <1>;
20 compatible = "simple-bus";
21 ranges;
22 }
23 }

第17~22 行,soc 节点,soc 节点设置#address-cells = <1>,#size-cells = <1>,这样soc 子节点的reg 属性中起始地占用一个字长,地址空间长度也占用一个字长。
第21 行,ranges 属性,ranges 属性为空,说明子空间和父空间地址范围相同。

3、添加ocram 节点

根据第②点的要求,添加ocram 节点,ocram 是I.MX6ULL 内部RAM,因此ocram 节点应该是soc 节点的子节点。ocram 起始地址为0x00900000,大小为128KB(0x20000),添加ocram节点以后myfirst.dts 文件内容如下所示:

1 / {
2 compatible = "fsl,imx6ull-alientek-evk", "fsl,imx6ull";
3
4 cpus {
5 #address-cells = <1>;
6 #size-cells = <0>;
7
8 //CPU0节点
9 cpu0: cpu@0 {
10 compatible = "arm,cortex-a7";
11 device_type = "cpu";
12 reg = <0>;
13 };
14 };
15
16 //soc节点
17 soc {
18 #address-cells = <1>;
19 #size-cells = <1>;
20 compatible = "simple-bus";
21 ranges;
22
23 //ocram节点
24 ocram: sram@00900000 {
25 compatible = "fsl,lpm-sram";
26 reg = <0x00900000 0x20000>;
27 };
28 }
29 }

第24~27 行,ocram 节点,第24 行节点名字@后面的0x00900000 就是ocram 的起始地址。
第26 行的reg 属性也指明了ocram 内存的起始地址为0x00900000,大小为0x20000。

4、添加aips1、aips2 和aips3 这三个子节点
I.MX6ULL 内部分为三个域:aips1~ 3,这三个域分管不同的外设控制器,aips1~3 这三个域对应的内存范围如表43.4.1 所示:

起始地址大小(十六进制)
AIPS10X020000000X100000
AIPS20X021000000X100000
AIPS30X022000000X100000

我们先在设备树中添加这三个域对应的子节点。aips1~3 这三个域都属于soc 节点的子节点,完成以后的myfirst.dts 文件内容如下所示:

1 / {
2 compatible = "fsl,imx6ull-alientek-evk", "fsl,imx6ull";
3
4 cpus {
5 #address-cells = <1>;
6 #size-cells = <0>;
7
8 //CPU0节点
9 cpu0: cpu@0 {
10 compatible = "arm,cortex-a7";
11 device_type = "cpu";
12 reg = <0>;
13 };
14 };
15
16 //soc节点
17 soc {
18 #address-cells = <1>;
19 #size-cells = <1>;
20 compatible = "simple-bus";
21 ranges;
22
23 //ocram节点
24 ocram: sram@00900000 {
25 compatible = "fsl,lpm-sram";
26 reg = <0x00900000 0x20000>;
27 };
28
29 //aips1节点
30 aips1: aips-bus@02000000 {
31 compatible = "fsl,aips-bus", "simple-bus";
32 #address-cells = <1>;
33 #size-cells = <1>;
34 reg = <0x02000000 0x100000>;
35 ranges;
36 }
37
38 //aips2节点
39 aips2: aips-bus@02100000 {
40 compatible = "fsl,aips-bus", "simple-bus";
41 #address-cells = <1>;
42 #size-cells = <1>;
43 reg = <0x02100000 0x100000>;
44 ranges;
45 }
46
47 //aips3节点
48 aips3: aips-bus@02200000 {
49 compatible = "fsl,aips-bus", "simple-bus";
50 #address-cells = <1>;
51 #size-cells = <1>;
52 reg = <0x02200000 0x100000>;
53 ranges;
54 }
55 }
56 }

第30~36 行,aips1 节点。
第39~45 行,aips2 节点。
第48~54 行,aips3 节点。

5、添加ecspi1、usbotg1 和rngb 这三个外设控制器节点

最后我们在myfirst.dts 文件中加入ecspi1,usbotg1 和rngb 这三个外设控制器对应的节点,其中ecspi1 属于aips1 的子节点,usbotg1 属于aips2 的子节点,rngb 属于aips3 的子节点。最终的myfirst.dts 文件内容如下:

1 / {
2 compatible = "fsl,imx6ull-alientek-evk", "fsl,imx6ull";
3
4 cpus {
5 #address-cells = <1>;
6 #size-cells = <0>;
7
8 //CPU0节点
9 cpu0: cpu@0 {
10 compatible = "arm,cortex-a7";
11 device_type = "cpu";
12 reg = <0>;
13 };
14 };
15
16 //soc节点
17 soc {
18 #address-cells = <1>;
19 #size-cells = <1>;
20 compatible = "simple-bus";
21 ranges;
22
23 //ocram节点
24 ocram: sram@00900000 {
25 compatible = "fsl,lpm-sram";
26 reg = <0x00900000 0x20000>;
27 };
28
29 //aips1节点
30 aips1: aips-bus@02000000 {
31 compatible = "fsl,aips-bus", "simple-bus";
32 #address-cells = <1>;
33 #size-cells = <1>;
34 reg = <0x02000000 0x100000>;
35 ranges;
36
37 //ecspi1节点
38 ecspi1: ecspi@02008000 {
39 #address-cells = <1>;
40 #size-cells = <0>;
41 compatible = "fsl,imx6ul-ecspi", "fsl,imx51-ecspi";
42 reg = <0x02008000 0x4000>;
43 status = "disabled";
44 };
45 }
46
47 //aips2节点
48 aips2: aips-bus@02100000 {
49 compatible = "fsl,aips-bus", "simple-bus";
50 #address-cells = <1>;
51 #size-cells = <1>;
52 reg = <0x02100000 0x100000>;
53 ranges;
54
55 //usbotg1节点
56 usbotg1: usb@02184000 {
57 compatible = "fsl,imx6ul-usb", "fsl,imx27-usb";
58 reg = <0x02184000 0x4000>;
59 status = "disabled";
60 };
61 }
62
63 //aips3节点
64 aips3: aips-bus@02200000 {
65 compatible = "fsl,aips-bus", "simple-bus";
66 #address-cells = <1>;
67 #size-cells = <1>;
68 reg = <0x02200000 0x100000>;
69 ranges;
70
71 //rngb节点
72 rngb: rngb@02284000 {
73 compatible = "fsl,imx6sl-rng", "fsl,imx-rng", "imx-rng";
74 reg = <0x02284000 0x4000>;
75 };
76 }
77 }
78 }

第38~44 行,ecspi1 外设控制器节点。
第56~60 行,usbotg1 外设控制器节点。
第72~75 行,rngb 外设控制器节点。

至此,myfirst.dts 这个小型的模板设备树就编写好了,基本和imx6ull.dtsi 很像,可以看做是imx6ull.dtsi 的缩小版。在myfirst.dts 里面我们仅仅是编写了I.MX6ULL 的外设控制器节点,像IIC 接口,SPI 接口下所连接的具体设备我们并没有写,因为具体的设备其设备树属性内容不同,这个等到具体的实验在详细讲解。

设备树在系统中的体现(/proc/device-tree目录下列出所有根节点“/”及其属性,了解)

在这里插入图片描述

Linux 内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的/proc/device-tree 目录下根据节点名字创建不同文件夹,如图43.5.1 所示:
在这里插入图片描述
图43.5.1 就是目录/proc/device-tree 目录下的内容,/proc/device-tree 目录下列出根节点“/”的所有属性和子节点,我们依次来看一下这些属性和子节点。

1、根节点“/”各个属性

在图43.5.1 中,根节点属性属性表现为一个个的文件(图中细字体文件),比如图43.5.1 中的“#address-cells”、“#size-cells”、“compatible”、“model”和“name”这5 个文件,它们在设备树中就是根节点的5 个属性。既然是文件那么肯定可以查看其内容,输入cat 命令来查看model和compatible 这两个文件的内容,结果如图43.5.2 所示:
在这里插入图片描述
从图43.5.2 可以看出,文件model 的内容是“Freescale i.MX6 ULL 14x14 EVK Board”,文件compatible 的内容为“fsl,imx6ull-14x14-evkfsl,imx6ull”。打开文件imx6ull-alientek-emmc.dts查看一下,这不正是根节点“/”的model 和compatible 属性值吗!

2、根节点“/”各子节点

图43.5.1 中各个文件夹(图中粗字体文件夹)就是根节点“/”的各个子节点,比如“aliases”、“backlight”、“chosen”和“clocks”等等。大家可以查看一下imx6ull-alientek-emmc.dts 和imx6ull.dtsi 这两个文件,看看根节点的子节点都有哪些,看看是否和图43.5.1 中的一致。
请添加图片描述

/proc/device-tree 目录就是设备树在根文件系统中的体现,同样是按照树形结构组织的,进入/proc/device-tree/soc 目录中就可以看到soc 节点的所有子节点,如图43.5.3 所示:
在这里插入图片描述

和根节点“/”一样,图43.5.3 中的所有文件分别为soc 节点的属性文件和子节点文件夹。

大家可以自行查看一下这些属性文件的内容是否和imx6ull.dtsi 中soc 节点的属性值相同,也可以进入“busfreq”这样的文件夹里面查看soc 节点的子节点信息。

特殊节点(aliases和chosen,了解)

在根节点“/”中有两个特殊的子节点:aliases 和chosen,我们接下来看一下这两个特殊的子节点。

aliases 子节点

打开imx6ull.dtsi 文件,aliases 节点内容如下所示:

18 aliases {
19 		can0 = &flexcan1;
20 		can1 = &flexcan2;
21 		ethernet0 = &fec1;
22 		ethernet1 = &fec2;
23 		gpio0 = &gpio1;
24 		gpio1 = &gpio2;
......
42 		spi0 = &ecspi1;
43 		spi1 = &ecspi2;
44 		spi2 = &ecspi3;
45 		spi3 = &ecspi4;
46 		usbphy0 = &usbphy1;
47 		usbphy1 = &usbphy2;
48 };

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

在这里插入图片描述

chosen 子节点

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

18 chosen {
19 		stdout-path = &uart1;
20 };

从示例代码43.6.2.1 中可以看出,chosen 节点仅仅设置了属性“stdout-path”,表示标准输出使用uart1。但是当我们进入到/proc/device-tree/chosen 目录里面,会发现多了bootargs 这个属性,如图43.6.2.1 所示:
在这里插入图片描述
输入cat 命令查看bootargs 这个文件的内容,结果如图43.6.2.2 所示:
在这里插入图片描述
从图43.6.2.2 可以看出,bootargs 这个文件的内容为“console=ttymxc0,115200……”,这个不就是我们在uboot 中设置的bootargs 环境变量的值吗?现在有两个疑点:
①、我们并没有在设备树中设置chosen 节点的bootargs 属性,那么图43.6.2.1 中bootargs这个属性是怎么产生的?
②、为何bootargs 文件的内容和uboot 中bootargs 环境变量的值一样?它们之间有什么关系?

前面讲解uboot 的时候说过,uboot 在启动Linux 内核的时候会将bootargs 的值传递给Linux内核,bootargs 会作为Linux 内核的命令行参数,Linux 内核启动的时候会打印出命令行参数(也就是uboot 传递进来的bootargs 的值),如图43.6.2.3 所示:
在这里插入图片描述
既然chosen 节点的bootargs 属性不是我们在设备树里面设置的,那么只有一种可能,那就是uboot 自己在chosen 节点里面添加了bootargs 属性,并且设置bootargs 属性的值为bootargs环境变量的值。因为在启动Linux 内核之前,只有uboot 知道bootargs 环境变量的值,并且uboot也知道.dtb 设备树文件在DRAM 中的位置

在uboot 源码中全局搜索“chosen”这个字符串,发现在common/fdt_support.c 文件中发现了“chosen”,fdt_support.c 文件中有个fdt_chosen 函数,此函数内容如下所示:

275 int fdt_chosen(void *fdt)
276 {
277 int nodeoffset;
278 int err;
279 char *str; /* used to set string properties */
280
281 err = fdt_check_header(fdt);
282 if (err < 0) {
283 printf("fdt_chosen: %s\n", fdt_strerror(err));
284 return err;
285 }
286
287 /* find or create "/chosen" node. */
288 nodeoffset = fdt_find_or_add_subnode(fdt, 0, "chosen");
289 if (nodeoffset < 0)
290 return nodeoffset;
291
292 str = getenv("bootargs");
293 if (str) {
294 err = fdt_setprop(fdt, nodeoffset, "bootargs", str,
295 strlen(str) + 1);
296 if (err < 0) {
297 printf("WARNING: could not set bootargs %s.\n",
298 fdt_strerror(err));
299 return err;
300 }
301 }
302
303 return fdt_fixup_stdout(fdt, nodeoffset);
304 }

第288 行,调用函数fdt_find_or_add_subnode 从设备树(.dtb)中找到chosen 节点,如果没有找到的话就会自己创建一个chosen 节点。
第292 行,读取uboot 中bootargs 环境变量的内容。
第294 行,调用函数fdt_setprop 向chosen 节点添加bootargs 属性,并且bootargs 属性的值就是环境变量bootargs 的内容。

证据“实锤”了,就是uboot 中的fdt_chosen 函数在设备树的chosen 节点中加入了bootargs属性,并且还设置了bootargs 属性值。接下来我们顺着fdt_chosen 函数一点点的抽丝剥茧,看看都有哪些函数调用了fdt_chosen,一直找到最终的源头。最终发现整个流程见图43.6.2.4:
在这里插入图片描述
图43.6.2.4 中框起来的部分就是函数do_bootm_linux 函数的执行流程,也就是说do_bootm_linux 函数会通过一系列复杂的调用,最终通过fdt_chosen 函数在chosen 节点中加入了bootargs 属性。而我们通过bootz 命令启动Linux 内核的时候会运行do_bootm_linux 函数,至此,真相大白,一切事情的源头都源于如下命令:

bootz 8080000083000000

当我们输入上述命令并执行以后,do_bootz 函数就会执行,然后一切就按照图43.6.2.4 中所示的流程开始运行。

Linux 内核解析DTB文件(了解)

Linux 内核在启动的时候会解析DTB 文件,然后在/proc/device-tree 目录下生成相应的设备树节点文件。接下来我们简单分析一下Linux 内核是如何解析DTB 文件的,流程如图43.7.1 所示:
在这里插入图片描述
从图43.7.1 中可以看出,在start_kernel 函数中完成了设备树节点解析的工作,最终实际工
作的函数为unflatten_dt_node。

绑定信息文档(Linux内核源码中.txt 文档描述如何添加设备节点规则,了解)

设备树是用来描述板子上的设备信息的,不同的设备其信息不同,反映到设备树中就是属性不同。那么我们在设备树中添加一个硬件对应的节点的时候从哪里查阅相关的说明呢?在Linux 内核源码中有详细的.txt 文档描述了如何添加节点,这些.txt 文档叫做绑定文档,路径为:Linux 源码目录/Documentation/devicetree/bindings,如图43.8.1 所示:
在这里插入图片描述
比如我们现在要想在I.MX6ULL 这颗SOC 的I2C 下添加一个节点,那么就可以查看Documentation/devicetree/bindings/i2c/i2c-imx.txt,此文档详细的描述了I.MX 系列的SOC 如何在设备树中添加I2C 设备节点,文档内容如下所示:

* Freescale Inter IC (I2C) and High Speed Inter IC (HS-I2C) for i.MX
Required properties:
- compatible :
- "fsl,imx1-i2c" for I2C compatible with the one integrated on i.MX1 SoC
- "fsl,imx21-i2c" for I2C compatible with the one integrated on i.MX21 SoC
- "fsl,vf610-i2c" for I2C compatible with the one integrated on Vybrid vf610 SoC
- reg : Should contain I2C/HS-I2C registers location and length
- interrupts : Should contain I2C/HS-I2C interrupt
- clocks : Should contain the I2C/HS-I2C clock specifier
Optional properties:
- clock-frequency : Constains desired I2C/HS-I2C bus clock frequency in Hz.
The absence of the propoerty indicates the default frequency 100 kHz.
- dmas: A list of two dma specifiers, one for each entry in dma-names.
- dma-names: should contain "tx" and "rx".
Examples:
i2c@83fc4000 { /* I2C2 on i.MX51 */
compatible = "fsl,imx51-i2c", "fsl,imx21-i2c";
reg = <0x83fc4000 0x4000>;
interrupts = <63>;
};
i2c@70038000 { /* HS-I2C on i.MX51 */
compatible = "fsl,imx51-i2c", "fsl,imx21-i2c";
reg = <0x70038000 0x4000>;
interrupts = <64>;
clock-frequency = <400000>;
};
i2c0: i2c@40066000 { /* i2c0 on vf610 */
compatible = "fsl,vf610-i2c";
reg = <0x40066000 0x1000>;
interrupts =<0 71 0x04>;
dmas = <&edma0 0 50>,
<&edma0 0 51>;
dma-names = "rx","tx";
};

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

设备树常用OF操作函数(获取reg’寄存器地址’属性,操作外设)

设备树描述了设备的详细信息,这些信息包括数字类型的、字符串类型的、数组类型的,我们在编写驱动的时候需要获取到这些信息。比如设备树使用reg 属性描述了某个外设的寄存器地址为0X02005482,长度为0X400,我们在编写驱动的时候需要获取到reg属性的0X02005482 和0X400 这两个值,然后初始化外设。Linux 内核给我们提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_”,所以在很多资料里面也被叫做OF 函数。这些OF 函数原型都定义在include/linux/of.h 文件中。

查找节点的OF函数

设备都是以节点的形式 “挂”到设备树上的,因此要想获取这个设备的其他属性信息,必须先获取到这个设备的节点。Linux 内核使用device_node 结构体来描述一个节点,此结构体定义在文件include/linux/of.h 中,定义如下:

49 struct device_node {
50 		const char *name; 				/* 节点名字*/
51 		const char *type; 				/* 设备类型*/
52 		phandle phandle;
53 		const char *full_name; 			/* 节点全名*/
54 		struct fwnode_handle fwnode;
55
56 		struct property *properties;	/* 属性*/
57 		struct property *deadprops; 	/* removed属性*/
58 		struct device_node *parent; 	/* 父节点*/
59 		struct device_node *child; 		/* 子节点*/
60 		struct device_node *sibling;
61 		struct kobject kobj;
62 		unsigned long _flags;
63 		void *data;
64 		#if defined(CONFIG_SPARC)
65 		const char *path_component_name;
66 		unsigned int unique_id;
67 		struct of_irq_controller *irq_trans;
68 		#endif
69 };

与查找节点有关的OF 函数有5 个,我们依次来看一下。

1、of_find_node_by_name 函数

of_find_node_by_name 函数通过节点名字查找指定的节点,函数原型如下:

struct device_node *of_find_node_by_name(struct device_node *from,
const char *name);

函数参数和返回值含义如下:

  • from:开始查找的节点,如果为NULL 表示从根节点开始查找整个设备树(像链表)。
  • name:要查找的节点名字。
  • 返回值:找到的节点,如果为NULL 表示查找失败。

2、of_find_node_by_type 函数

of_find_node_by_type 函数通过device_type 属性查找指定的节点,函数原型如下:

struct device_node *of_find_node_by_type(struct device_node *from, const char *type)

函数参数和返回值含义如下:

  • from:开始查找的节点,如果为NULL 表示从根节点开始查找整个设备树。
  • type:要查找的节点对应的type 字符串,也就是device_type 属性值。
  • 返回值:找到的节点,如果为NULL 表示查找失败。

3、of_find_compatible_node 函数

of_find_compatible_node 函数根据device_type 和compatible 这两个属性查找指定的节点,
函数原型如下:

struct device_node *of_find_compatible_node(struct device_node *from,
const char *type,
const char *compatible)

函数参数和返回值含义如下:

  • from:开始查找的节点,如果为NULL 表示从根节点开始查找整个设备树。
  • type:要查找的节点对应的type 字符串,也就是device_type 属性值,可以为NULL,表示忽略掉device_type 属性。
  • compatible:要查找的节点所对应的compatible 属性列表。
  • 返回值:找到的节点,如果为NULL 表示查找失败

4、of_find_matching_node_and_match 函数

of_find_matching_node_and_match 函数通过of_device_id 匹配表来查找指定的节点,函数原型如下:

struct device_node *of_find_matching_node_and_match(struct device_node *from,
const struct of_device_id *matches,
const struct of_device_id **match)

函数参数和返回值含义如下:

  • from:开始查找的节点,如果为NULL 表示从根节点开始查找整个设备树。
  • matches:of_device_id 匹配表,也就是在此匹配表里面查找节点。
  • match:找到的匹配的of_device_id。
  • 返回值:找到的节点,如果为NULL 表示查找失败

5、of_find_node_by_path 函数

of_find_node_by_path 函数通过路径来查找指定的节点,函数原型如下:

inline struct device_node *of_find_node_by_path(const char *path)

函数参数和返回值含义如下:

  • path:带有全路径的节点名,可以使用节点的别名,比如“/backlight”就是backlight 这个节点的全路径。
  • 返回值:找到的节点,如果为NULL 表示查找失败

查找父/子节点的OF函数

Linux 内核提供了几个查找节点对应的父节点或子节点的OF 函数,我们依次来看一下。

1、of_get_parent 函数

of_get_parent 函数用于获取指定节点的父节点(如果有父节点的话),函数原型如下:

struct device_node *of_get_parent(const struct device_node *node)

函数参数和返回值含义如下:

  • node:要查找的父节点的节点。
  • 返回值:找到的父节点。

2、of_get_next_child 函数

of_get_next_child 函数用迭代的方式查找子节点,函数原型如下:

struct device_node *of_get_next_child(const struct device_node *node,
struct device_node *prev)

函数参数和返回值含义如下:

  • node:父节点。
  • prev:前一个子节点,也就是从哪一个子节点开始迭代的查找下一个子节点。可以设置为NULL,表示从第一个子节点开始。
  • 返回值:找到的下一个子节点。

提取属性值的OF函数

节点的属性信息里面保存了驱动所需要的内容,因此对于属性值的提取非常重要,Linux 内核中使用结构体property 表示属性,此结构体同样定义在文件include/linux/of.h 中,内容如下:

35 struct property {
36 		char *name; /* 属性名字*/
37 		int length; /* 属性长度*/
38	    void *value; /* 属性值*/
39 		struct property *next; /* 下一个属性*/
40 		unsigned long _flags;
41 		unsigned int unique_id;
42 		struct bin_attribute attr;
43 };

Linux 内核也提供了提取属性值的OF 函数,我们依次来看一下。

1、of_find_property 函数

of_find_property 函数用于查找指定的属性,函数原型如下:

property *of_find_property(const struct device_node *np,const char *name,int *lenp)

函数参数和返回值含义如下:

  • np:设备节点。
  • name:属性名字。
  • lenp:属性值的字节数
  • 返回值:找到的属性。

2、of_property_count_elems_of_size 函数

of_property_count_elems_of_size 函数用于获取属性中元素的数量,比如reg 属性值是一个数组,那么使用此函数可以获取到这个数组的大小,此函数原型如下:

int of_property_count_elems_of_size(const struct device_node *np,const char *propname,int elem_size)

函数参数和返回值含义如下:

  • np:设备节点。
  • proname:需要统计元素数量的属性名字。
  • elem_size:元素长度。
  • 返回值:得到的属性元素数量。

3、of_property_read_u32_index 函数

of_property_read_u32_index 函数用于从属性中获取指定标号的u32 类型数据值(无符号32位),比如某个属性有多个u32 类型的值,那么就可以使用此函数来获取指定标号的数据值,此函数原型如下:

int of_property_read_u32_index(const struct device_node *np,
const char *propname,
u32 index,
u32 *out_value)

函数参数和返回值含义如下:

  • np:设备节点。
  • proname:要读取的属性名字。
  • index:要读取的值标号。
  • out_value:读取到的值
  • 返回值:0 读取成功,负值,读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没有要读取的数据,-EOVERFLOW 表示属性值列表太小。

4、of_property_read_u8_array 函数

of_property_read_u16_array 函数
of_property_read_u32_array 函数
of_property_read_u64_array 函数

这4 个函数分别是读取属性中u8、u16、u32 和u64 类型的数组数据,比如大多数的reg 属性都是数组数据,可以使用这4 个函数一次读取出reg 属性中的所有数据。这四个函数的原型如下:

int of_property_read_u8_array(const struct device_node *np,
const char *propname,
u8 *out_values,
size_t sz)
int of_property_read_u16_array(const struct device_node *np,
const char *propname,
u16 *out_values,
size_t sz)
int of_property_read_u32_array(const struct device_node *np,
const char *propname,
u32 *out_values,
size_t sz)
int of_property_read_u64_array(const struct device_node *np,
const char *propname,
u64 *out_values,
size_t sz)

函数参数和返回值含义如下:

  • np:设备节点。
  • proname:要读取的属性名字。
  • out_value:读取到的数组值,分别为u8、u16、u32 和u64。
  • sz:要读取的数组元素数量。
  • 返回值:0,读取成功,负值,读取失败,-EINVAL 表示属性不存在,ENODATA 表示没有要读取的数据,-EOVERFLOW 表示属性值列表太小。

5、of_property_read_u8 函数

of_property_read_u16 函数
of_property_read_u32 函数
of_property_read_u64 函数

有些属性只有一个整形值,这四个函数就是用于读取这种只有一个整形值的属性,分别用于读取u8、u16、u32 和u64 类型属性值,函数原型如下:

int of_property_read_u8(const struct device_node *np,
const char *propname,
u8 *out_value)
int of_property_read_u16(const struct device_node *np,
const char *propname,
u16 *out_value)
int of_property_read_u32(const struct device_node *np,
const char *propname,
u32 *out_value)
int of_property_read_u64(const struct device_node *np,
const char *propname,
u64 *out_value)

函数参数和返回值含义如下:

  • np:设备节点。
  • proname:要读取的属性名字。
  • out_value:读取到的数组值。
  • 返回值:0,读取成功,负值,读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没有要读取的数据,-EOVERFLOW 表示属性值列表太小。

6、of_property_read_string 函数

of_property_read_string 函数用于读取属性中字符串值,函数原型如下:

int of_property_read_string(struct device_node *np,
const char *propname,
const char **out_string)

函数参数和返回值含义如下:

  • np:设备节点。
  • proname:要读取的属性名字。
  • out_string:读取到的字符串值。
  • 返回值:0,读取成功,负值,读取失败。

7、of_n_addr_cells 函数

of_n_addr_cells 函数用于获取#address-cells 属性值,函数原型如下:

int of_n_addr_cells(struct device_node *np)

函数参数和返回值含义如下:

  • np:设备节点。
  • 返回值:获取到的#address-cells 属性值。

8、of_n_size_cells 函数

of_size_cells 函数用于获取#size-cells 属性值,函数原型如下:

int of_n_size_cells(struct device_node *np)

函数参数和返回值含义如下:

  • np:设备节点。
  • 返回值:获取到的#size-cells 属性值。

其他常用的OF函数

1、of_device_is_compatible 函数

of_device_is_compatible 函数用于查看节点的compatible 属性是否有包含compat 指定的字符串,也就是检查设备节点的兼容性,函数原型如下:

int of_device_is_compatible(const struct device_node *device,
const char *compat)

函数参数和返回值含义如下:

  • device:设备节点。
  • compat:要查看的字符串。
  • 返回值:0,节点的compatible 属性中不包含compat 指定的字符串;正数,节点的compatible属性中包含compat 指定的字符串。

2、of_get_address 函数

of_get_address 函数用于获取地址相关属性,主要是“reg”或者“assigned-addresses”属性值,函数原型如下:

const __be32 *of_get_address(struct device_node *dev,
int index,
u64 *size,
unsigned int *flags)

函数参数和返回值含义如下:

  • dev:设备节点。
  • index:要读取的地址标号。
  • size:地址长度。
  • flags:参数,比如IORESOURCE_IO、IORESOURCE_MEM 等
  • 返回值:读取到的地址数据首地址,为NULL 的话表示读取失败。

3、of_translate_address 函数

of_translate_address 函数负责将从设备树读取到的地址转换为物理地址,函数原型如下:

u64 of_translate_address(struct device_node *dev,
const __be32 *in_addr)

函数参数和返回值含义如下:

  • dev:设备节点。
  • in_addr:要转换的地址。
  • 返回值:得到的物理地址,如果为OF_BAD_ADDR 的话表示转换失败。

4、of_address_to_resource 函数

IIC、SPI、GPIO 等这些外设都有对应的寄存器,这些寄存器其实就是一组内存空间,Linux内核使用resource 结构体来描述一段内存空间,“resource”翻译出来就是“资源”,因此用resource结构体描述的都是设备资源信息,resource 结构体定义在文件include/linux/ioport.h 中,定义如下:

18 struct resource {
19 resource_size_t start;
20 resource_size_t end;
21 const char *name;
22 unsigned long flags;
23 struct resource *parent, *sibling, *child;
24 };

对于32 位的SOC 来说,resource_size_t 是u32 类型的。其中start 表示开始地址,end 表示结束地址,name 是这个资源的名字,flags 是资源标志位,一般表示资源类型,可选的资源标志定义在文件include/linux/ioport.h 中,如下所示:

1 #define IORESOURCE_BITS 0x000000ff
2 #define IORESOURCE_TYPE_BITS 0x00001f00
3 #define IORESOURCE_IO 0x00000100
4 #define IORESOURCE_MEM 0x00000200
5 #define IORESOURCE_REG 0x00000300
6 #define IORESOURCE_IRQ 0x00000400
7 #define IORESOURCE_DMA 0x00000800
8 #define IORESOURCE_BUS 0x00001000
9 #define IORESOURCE_PREFETCH 0x00002000
10 #define IORESOURCE_READONLY 0x00004000
11 #define IORESOURCE_CACHEABLE 0x00008000
12 #define IORESOURCE_RANGELENGTH 0x00010000
13 #define IORESOURCE_SHADOWABLE 0x00020000
14 #define IORESOURCE_SIZEALIGN 0x00040000
15 #define IORESOURCE_STARTALIGN 0x00080000
16 #define IORESOURCE_MEM_64 0x00100000
17 #define IORESOURCE_WINDOW 0x00200000
18 #define IORESOURCE_MUXED 0x00400000
19 #define IORESOURCE_EXCLUSIVE 0x08000000
20 #define IORESOURCE_DISABLED 0x10000000
21 #define IORESOURCE_UNSET 0x20000000
22 #define IORESOURCE_AUTO 0x40000000
23 #define IORESOURCE_BUSY 0x80000000

大家一般最常见的资源标志就是IORESOURCE_MEM 、IORESOURCE_REG 和IORESOURCE_IRQ 等。接下来我们回到of_address_to_resource 函数,此函数看名字像是从设备树里面提取资源值,但是本质上就是将reg 属性值,然后将其转换为resource 结构体类型,
函数原型如下所示

int of_address_to_resource(struct device_node *dev,
int index,
struct resource *r)

函数参数和返回值含义如下:

  • dev:设备节点。
  • index:地址资源标号。
  • r:得到的resource 类型的资源值。
  • 返回值:0,成功;负值,失败。

5、of_iomap 函数

of_iomap 函数用于直接内存映射,以前我们会通过ioremap 函数来完成物理地址到虚拟地址的映射,采用设备树以后就可以直接通过of_iomap 函数来获取内存地址所对应的虚拟地址,不需要使用ioremap 函数了。当然了,你也可以使用ioremap 函数来完成物理地址到虚拟地址的内存映射,只是在采用设备树以后,大部分的驱动都使用of_iomap 函数了。of_iomap 函数本
质上也是将reg 属性中地址信息转换为虚拟地址,如果reg 属性有多段的话,可以通过index 参数指定要完成内存映射的是哪一段,of_iomap 函数原型如下:

void __iomem *of_iomap(struct device_node *np,
int index)

函数参数和返回值含义如下:

  • np:设备节点。
  • index:reg 属性中要完成内存映射的段,如果reg 属性只有一段的话index 就设置为0。
  • 返回值:经过内存映射后的虚拟内存首地址,如果为NULL 的话表示内存映射失败。

OF函数使用案例

在这里插入图片描述
在驱动入口函数里面填写编写代码获取设备属性(多去内核里看看别人是怎么写的):
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

关于设备树常用的OF 函数就先讲解到这里,Linux 内核中关于设备树的OF 函数不仅仅只有前面讲的这几个,还有很多OF 函数我们并没有讲解,这些没有讲解的OF 函数要结合具体的驱动,比如获取中断号的OF 函数、获取GPIO 的OF 函数等等,这些OF 函数我们在后面的驱动实验中再详细的讲解。

总结

关于设备树就讲解到这里,关于设备树我们重点要了解一下几点内容:
①、DTS、DTB 和DTC 之间的区别,如何将.dts 文件编译为.dtb 文件。
②、设备树语法,这个是重点,因为在实际工作中我们是需要修改设备树的。
③、设备树的几个特殊子节点。
④、关于设备树的OF 操作函数,也是重点,因为设备树最终是被驱动文件所使用的,而驱动文件必须要读取设备树中的属性信息,比如内存信息、GPIO 信息、中断信息等等。要想在驱动中读取设备树的属性值,那么就必须使用Linux 内核提供的众多的OF 函数。

从下一章开始所以的Linux 驱动实验都将采用设备树,从最基本的点灯,到复杂的音频、网络或块设备等驱动。将会带领大家由简入深,深度剖析设备树,最终掌握基于设备树的驱动开发技能。

设备树(Device Tree)是一种描述嵌入式系统硬件设备的文件格式,用于在Linux内核中动态配置和管理硬件。设备树采用树形结构,描述了开发板上的各种设备信息,比如CPU数量、内存基地址、接口上连接的设备等。设备树文件的扩展名为.dts,编译后得到.dtb文件。在引导Linux内核时,Bootloader会将.dtb文件的地址传递给内核,内核会根据设备树文件中描述的信息创建和注册相关设备。通过设备树开发者可以在不修改内核源代码的情况下配置和管理硬件设备。在编译Linux源码时,可以使用"make dtbs"命令来单独编译设备树文件,而不是编译整个源码。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [Linux 设备树](https://blog.csdn.net/m0_69211839/article/details/130601344)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] - *3* [linux设备树](https://blog.csdn.net/qq_27094347/article/details/121012569)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

行稳方能走远

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值