4| Linux 设备树

Linux 设备树

什么是“设备树”,本章我们就来详细的谈一谈设备树。掌握设备树是 Linux 驱动开发人员必备的技能!因为在新版本的 Linux 中,ARM 相关的驱动全部采用了设备树(也有支持老式驱动的,比较少),最新出的 CPU 其驱动开发也基本都是基于设备树的,比如 ST 新出的 STM32MP157、NXP的I.MX8系列等。

我们所使用的Linux版本为4.1.15,其支持设备树,所以正点原子I.MX6U-ALPHA 开发板的所有 Linux 驱动都是基于设备树的。本章我们就来了解一下设备树的起源、重点学习一下设备树语法。

一、什么是设备树?

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

image-20200820184958257

在图中,树的主干就是系统总线,IIC 控制器、GPIO 控制器、SPI 控制器等都是接到系统主线上的分支。IIC 控制器有分为 IIC1 和 IIC2 两种,其中 IIC1 上接了 FT5206 和 AT24C02这两个 IIC 设备,IIC2 上只接了 MPU6050 这个设备。

DTS 文件的主要功能就是按照图所示的结构来描述板子上的设备信息,DTS 文件描述设备信息是有相应的语法规则要求的,稍后我们会详细的讲解 DTS 语法规则。

在 3.x 版本(具体哪个版本我也无从考证)以前的 Linux 内核中 ARM 架构并没有采用设备树。在没有设备树的时候 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 开发板还有很多的其他外设硬件和平台硬件信息。使用 2440 这个芯片的板子有很多,每个板子都有描述相应板级信息的文件,这仅仅只是一个 2440。随着智能手机的发展,每年新出的 ARM 架构芯片少说都在数十、数百款,Linux 内核下板级信息文件将会成指数级增长!这些板级信息文件都是.c 或.h 文件,都会被硬编码进 Linux 内核中,导致 Linux 内核“虚胖”。

就好比你喜欢吃自助餐,然后花了 100 多到一家宣传看着很不错的自助餐厅,结果你想吃的牛排、海鲜、烤肉基本没多少,全都是一些凉菜、炒面、西瓜、饮料等小吃,相信你此时肯定会脱口而出一句“Fk!”、
“骗子!”。

同样的,当 Linux 之父 linus 看到 ARM 社区向 Linux 内核添加了大量“无用”、冗余的板级信息文件,不禁的发出了一句“This whole ARM thing is a f*cking pain in the ass”。从此以后 ARM 社区就引入了 PowerPC 等架构已经采用的设备树(Flattened Device Tree),将这些描述板级硬件信息的内容都从 Linux 内中分离开来,用一个专属的文件格式来描述,这个专属的文件就叫做设备树,文件扩展名为.dts。

一个 SOC 可以作出很多不同的板子,这些不同的板子肯定是有共同的信息,将这些共同的信息提取出来作为一个通用的文件,其他的.dts 文件直接引用这个通用文件即可,这个通用文件就是.dtsi 文件,类似于 C 语言中的头文件。一般.dts 描述板级信息(也就是开发板上有哪些 IIC 设备、SPI 设备等),.dtsi 描述 SOC 级信息(也就是 SOC 有几个 CPU、主频是多少、各个外设控制器信息等)。

这个就是设备树的由来,简而言之就是,Linux 内核中 ARM 架构下有太多的冗余的垃圾板级信息文件,导致 linus 震怒,然后 ARM 社区引入了设备树。

二、DTS 、DTB 和 和 DTC

上一小节说了,设备树源文件扩展名为.dts,但是我们在前面移植 Linux 的时候却一直在使用.dtb 文件,那么 DTS 和 DTB 这两个文件是什么关系呢?DTS 是设备树源码文件,DTB 是将DTS 编译以后得到的二进制文件。将.c 文件编译为.o 需要用到 gcc 编译器,那么将.dts 编译为.dtb需要什么工具呢?需要用到 DTC 工具!DTC 工具源码在 Linux 内核的 scripts/dtc 目录下,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-lxg-emmc.dtb \
423 imx6ull-lxg-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 系统的时候添加的设备树。

三、DTS 语法

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

本节我们就以 imx6ull-lxg-emmc.dts 这个文件为例来讲解一下 DTS 语法。

关于设备树详细的语法规则请参考《 Devicetree SpecificationV0.2.pdf 》 和《Power_ePAPR_APPROVED_v1.12.pdf》这两份文档

1|.dtsi 头文件

和 C 语言一样,设备树也支持头文件,设备树的头文件扩展名为.dtsi。在 imx6ull-lxg-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-weim.dts 这个文件,此文件中有如下内容:

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

可以看出,示例代码中直接引用了.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 };

2|设备节点

设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点,每个节点都通过一些属性信息来描述节点信息,属性就是键—值对。以下是从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”。

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

cpu0:cpu@0

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

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”。

3|标准属性

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

1 、compatible 属性

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

"manufacturer,model"

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

比如 imx6ull-lxg-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 所示:

image-20200820192141947


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 属性的格式一为:

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

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

image-20200820192517802

第 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 等使用的标准属性等到具体的例程再讲解

4|根节点 compatible 属性

每个节点都有 compatible 属性,根节点“/”也不例外,imx6ull-lxg-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属性是为了做什么工作的? 通过根节点的 compatible 属性可以知道我们所使用的设备,一般第一个值描述了所使用的硬件设备名字,比如这里使用的是“imx6ull-14x14-evk”这个设备,第二个值描述了设备所使用的 SOC,比如这里使用的是“imx6ull”这颗 SOC。Linux 内核会通过根节点的 compoatible 属性查看是否支持此设备,如果支持的话设备就会启动 Linux 内核。接下来我们就来学习一下 Linux 内核在使用设备树前后是如何判断是否支持某款设备的。

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

在没有使用设备树以前,uboot 会向 Linux 内核传递一个叫做 machine id 的值,machine id也就是设备 ID,告诉 Linux 内核自己是个什么设备,看看 Linux 内核是否支持。Linux 内核是支持很多设备的,针对每一个设备(板子),Linux内核都用MACHINE_STARTMACHINE_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

其中 MACHINE_START 和MACHINE_END 定义在文件 arch/arm/include/asm/mach/arch.h 中

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

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

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

前面说了,uboot 会给 Linux 内核传递 machine id 这个参数,Linux 内核会检查这个machine id,其实就是将 machine id 与 include/generated/mach-types.h 中的这些 MACH_TYPE_XXX 宏进行对比,看看有没有相等的,如果相等的话就表示 Linux 内核支持这个设备,如果不支持的话那么这个设备就没法启动 Linux 内核。

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

当 Linux 内核引入设备树以后就不再使用 MACHINE_START 了,而是换为了DT_MACHINE_STARTDT_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_STARTMACHINE_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** 成员变量,此成员变量保存着本设备兼容属性,示例代码中设置.dt_compat = imx6ul_dt_compatimx6ul_dt_compat 表里面有”fsl,imx6ul”和”fsl,imx6ull”这两个兼容值。只要某个设备(板子)根节点“/”的 compatible 属性值与imx6ul_dt_compat 表中的任何一个值相等,那么就表示 Linux 内核支持此设备。imx6ull-lxg-emmc.dts 中根节点的 compatible 属性值如下:

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

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

接下来我们简单看一下 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。

具体详情参考正点原子IMX6U驱动开发指南43.3.4节

5|向节点追加或修改内容

产品开发过程中可能面临着频繁的需求更改,比如第一版硬件上有一个 IIC 接口的六轴芯片 MPU6050,第二版硬件又要把这个 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 };

示例代码 就是 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 这个设备,但是其他的板子并没有这个设备啊!因此,按照示例代码 这样写肯定是不行的。

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

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

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

示例代码 就是向 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这颗六轴芯片。

因为示例代码 中的内容是 imx6ull-lxg-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-lxg-evk", "fsl,imx6ull";
3 }

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

1|添加 cpus 节点

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

/{
    compatible = "fsl,imx6ull-lxg-evk", "fsl,imx6ull";
    cpus {
        #address-cells = <1>;
        #size-cells = <0>;

        //cpu0节点
        cpu0:cpu@0 {
            compatible = "arm,cortex-a7";
            device_type = "cpu";
            reg = <0>;
        };

    };


};

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

2|添加 soc 节点

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

/{
    compatible = "fsl,imx6ull-lxg-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 {
        #address-cells = <1>;
        #size-cells = <1>;
        compatible = "simple-bus";
        ranges;
    };

};

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

3|添加 ocram 节点

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

/{
    compatible = "fsl,imx6ull-lxg-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 {
        #address-cells = <1>;
        #size-cells = <1>;
        compatible = "simple-bus";
        ranges;
        //ocram 节点
        ocram: sram@00900000 {
            compatible = "fsl,lpm-sram";
            reg = <0x00900000 0x20000>;
        };
    };

};

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

4|添加 aips1 、aips2 和 和 aips3 这三个子节点

I.MX6ULL 内部分为三个域:aips13,这三个域分管不同的外设控制器,aips13 这三个域对应的内存范围如表所示

起始地址 大小( 十六进制)
AIPS1 0X02000000 0X100000
AIPS2 0X02100000 0X100000
AIPS3 0X02200000 0X100000

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

/{
    compatible = "fsl,imx6ull-lxg-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 {
        #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|添加 ecspi1 、usbotg1 和 和 rngb 这三个外设控制器节点

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

/{
    compatible = "fsl,imx6ull-lxg-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 {
        #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 0x200>;
            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>;
        };
    };
};

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

五、设备树在系统中的体现

Linux 内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的/proc/device-tree 目录下根据节点名字创建不同文件夹,如图 所示:

image-20200820202310685

图 就是目录/proc/device-tree 目录下的内容,/proc/device-tree 目录下是根节点“/”的所有属性和子节点,我们依次来看一下这些属性和子节点。

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

在图中,根节点属性属性表现为一个个的文件(图中细字体文件),比如图中的“#address-cells”、“#size-cells”、“compatible”、“model”和“name”这 5 个文件,它们在设备树中就是根节点的5个属性。

既然是文件那么肯定可以查看其内容,输入cat命令来查看model和 compatible 这两个文件的内容,结果如图 所示

image-20200820202617800

从图 可以看出,文件 model 的内容是“Freescale i.MX6 ULL 14x14 EVK Board”,文件 compatible 的内容为“fsl,imx6ull-14x14-evkfsl,imx6ull”。

打开文件 imx6ull-lxg-emmc.dts查看一下,这不正是根节点“/”的 model 和 compatible 属性值吗!

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

image-20200820202310685

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

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

image-20200820202737979

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

六、特殊节点

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

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-lxg-emmc.dts 中 chosen 节点内容如下所示:

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

从示例代码中可以看出,chosen 节点仅仅设置了属性“stdout-path”,表示标准输出使用 uart1。但是当我们进入到/proc/device-tree/chosen 目录里面,会发现多了 bootargs 这个属性,如图 所示:

image-20200820203103001

输入 cat 命令查看 bootargs 这个文件的内容,结果如图所示

image-20200820203119368

从图可以看出,bootargs 这个文件的内容为“console=ttymxc0,115200……”,这个不就是我们在 uboot 中设置的 bootargs 环境变量的值吗?现在有两个疑点:
①、我们并没有在设备树中设置 chosen 节点的 bootargs 属性,那么图中 bootargs这个属性是怎么产生的?
②、为何 bootargs 文件的内容和 uboot 中 bootargs 环境变量的值一样?它们之间有什么关系?

前面讲解 uboot 的时候说过,uboot 在启动 Linux 内核的时候会将 bootargs 的值传递给 Linux内核,bootargs 会作为 Linux 内核的命令行参数,Linux 内核启动的时候会打印出命令行参数(也就是 uboot 传递进来的 bootargs 的值),如图 所示:

image-20200820203214452

既然 chosen 节点的 bootargs 属性不是我们在设备树里面设置的,那么只有一种可能,那就是 uboot 自己在 chosen 节点里面添加了 bootargs 属性!并且设置 bootargs 属性的值为 bootargs环境变量的值。因为在启动 Linux 内核之前,只有 uboot 知道 bootargs 环境变量的值,并且 uboot也知道.dtb 设备树文件在 DRAM 中的位置,因此 uboot 的“作案”嫌疑最大。在 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,一直找到最终的源头。这里我就不卖关子了,直接告诉大家整个流程是怎么样的,见图 :

image-20200820203540965

图 中框起来的部分就是函数 do_bootm_linux 函数的执行流程,也就是说do_bootm_linux 函数会通过一系列复杂的调用,最终通过 fdt_chosen 函数在 chosen 节点中加入了 bootargs 属性。而我们通过 bootz 命令启动 Linux 内核的时候会运行 do_bootm_linux 函数,至此,真相大白,一切事情的源头都源于如下命令:

bootz 8080000083000000

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

七、Linux内核解析DTB文件

Linux 内核在启动的时候会解析 DTB 文件,然后在/proc/device-tree 目录下生成相应的设备树节点文件。接下来我们简单分析一下 Linux 内核是如何解析 DTB 文件的,流程如图 所示:

image-20200820211529586

从图中可以看出,在 start_kernel 函数中完成了设备树节点解析的工作,最终实际工作的函数为 unflatten_dt_node。

八、绑定信息文档

设备树是用来描述板子上的设备信息的,不同的设备其信息不同,反映到设备树中就是属性不同。

那么我们在设备树中添加一个硬件对应的节点的时候从哪里查阅相关的说明呢?在Linux 内核源码中有详细的.txt 文档描述了如何添加节点,这些.txt 文档叫做绑定文档,路径为:Linux 源码目录/Documentation/devicetree/bindings,如图所示:

image-20200820211703146

比如我们现在要想在 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 属性描述了某个外设的寄存器地址为 0X02005482,长度为 0X400,我们在编写驱动的时候需要获取到 reg 属性的0X02005482 和 0X400 这两个值,然后初始化外设。

Linux 内核给我们提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_”,所以在很多资料里面也被叫做 OF 函数。这些 OF 函数原型都定义在 include/linux/of.h 文件中。

1|查找节点的OF函数

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

struct device_node {
	const char *name;		/* 节点名字 */
	const char *type;		/* 设备类型 */
	phandle phandle;
	const char *full_name;	/* 节点全名 */
	struct fwnode_handle fwnode;

	struct	property *properties;	/* 属性 */
	struct	property *deadprops;	/* removed 属性 */
	struct	device_node *parent;	/* 父节点 */
	struct	device_node *child;		/* 子节点 */
	struct	device_node *sibling;
	struct	kobject kobj;
	unsigned long _flags;
	void	*data;
#if defined(CONFIG_SPARC)
	const char *path_component_name;
	unsigned int unique_id;
	struct of_irq_controller *irq_trans;
#endif
};

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

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 表示查找失败。


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 表示查找失败。


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 表示查找失败


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 表示查找失败


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 表示查找失败

2| 查找父/ 子节点的 OF 函数

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

of_get_parent 函数

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

struct device_node *of_get_parent(const struct device_node *node)

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

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


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,表示从第一个子节点开始。
返回值:找到的下一个子节点。

3| 提取属性值的 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 函数,我们依次来看一下。

of_find_property 函数

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

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

函数参数和返回值含义如下:
np:设备节点。
name: 属性名字。
lenp:属性值的字节数
返回值:找到的属性。

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:元素长度。
返回值:得到的属性元素数量。


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 表示属性值列表太小。


of_property_read_u?_array 函数

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 表示属性值列表太小。


of_property_read_u? 函数

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 表示属性值列表太小。


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,读取成功,负值,读取失败。


of_n_addr_cells 函数

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

int of_n_addr_cells(struct device_node *np)

函数参数和返回值含义如下:
np:设备节点。
返回值:获取到的#address-cells 属性值。


of_n_size_cells 函数

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

int of_n_size_cells(struct device_node *np)

函数参数和返回值含义如下:
np:设备节点。
返回值:获取到的#size-cells 属性值。

4|其他常用的 OF 函数

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 指定的字符串。


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 的话表示读取失败。


of_translate_address 函数

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

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

函数参数和返回值含义如下:
dev:设备节点。
in_addr:要转换的地址。
返回值:得到的物理地址,如果为 OF_BAD_ADDR 的话表示转换失败。


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,成功;负值,失败。


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

设备树下的LED驱动

本章我们使用设备树来向 Linux 内核传递相关的寄存器物理地址,Linux 驱动文件使用上一章讲解的 OF函数从设备树中获取所需的属性值,然后使用获取到的属性值来初始化相关的 IO。本章实验还是比较简单的,本章实验重点内容如下:

①、在 imx6ull-lxg-emmc.dts 文件中创建相应的设备节点。
②、编写驱动程序(在前面实验基础上完成),获取设备树中的相关属性值。
③、使用获取到的有关属性值来初始化 LED 所使用的 GPIO。

修改设备树文件

在根节点“/”下创建一个名为“alphaled”的子节点,打开 imx6ull-lxg-emmc.dts 文件,在根节点“/”最后面输入如下所示内容:

1 alphaled {
2 		#address-cells = <1>;
3 		#size-cells = <1>;
4 		compatible = "atkalpha-led";
5 		status = "okay";
6 		reg = < 0X020C406C 0X04 /* CCM_CCGR1_BASE */
7 				0X020E0068 0X04 /* SW_MUX_GPIO1_IO03_BASE */
8 				0X020E02F4 0X04 /* SW_PAD_GPIO1_IO03_BASE */
9 				0X0209C000 0X04 /* GPIO1_DR_BASE */
10 				0X0209C004 0X04 >; /* GPIO1_GDIR_BASE */
11 };

第 2、3 行,属性#address-cells 和#size-cells 都为 1,表示 reg 属性中起始地址占用一个字长(cell),地址长度也占用一个字长(cell)。
第 4 行,属性 compatbile 设置 alphaled 节点兼容性为“atkalpha-led”。
第 5 行,属性 status 设置状态为“okay”。
第 6~10 行,reg 属性,非常重要!reg 属性设置了驱动里面所要使用的寄存器物理地址,比如第 6 行的“0X020C406C 0X04”表示 I.MX6ULL 的 CCM_CCGR1 寄存器,其中寄存器首地址为 0X020C406C,长度为 4 个字节。设备树修改完成以后输入如下命令重新编译一下 imx6ull-lxg-emmc.dts:make dtbs

编译完成以后得到 imx6ull-lxg-emmc.dtb,使用新的 imx6ull-lxg-emmc.dtb 启动Linux 内核。Linux 启动成功以后进入到/proc/device-tree/目录中查看是有“alphaled”这个节点,结果如图 所示:

image-20200821114222276

可以进入到图中的 alphaled 目录中,查看一下都有哪些属性文件,结果如图所示:

image-20200821114301406

大家可以查看一下 compatible、status 等属性值是否和我们设置的一致。

LED灯驱动编写

设备树准备好以后就可以编写驱动程序了,本章实验在前面实验驱动文件newchrled.c 的基础上修改而来。新建名为“4_dtsled”文件夹,然后在 4_dtsled 文件夹里面创建vscode 工程,工作区命名为“dtsled”。工程创建好以后新建 dtsled.c 文件,在 dtsled.c 里面输入如下内容:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/slab.h>

#define DTSLED_DEV_NUM 1	/* 设备号数量 */

typedef enum led_switch_enum{
	LED_OFF = 0,
	LED_ON = !LED_OFF
}led_switch_t;

/* 寄存器虚拟地址 */
static void __iomem *VA_CCM_CCGR1;
static void __iomem *VA_SW_MUX_GPIO1_IO03;
static void __iomem *VA_SW_PAD_GPIO1_IO03;
static void __iomem *VA_GPIO1_DR;
static void __iomem *VA_GPIO1_GDIR;


static void led_switch(led_switch_t sw)
{
	int val = 0;
	if(sw == LED_ON)
	{
		/* 设置GPIO电平为低电平 默认点亮LED */
		val = readl(VA_GPIO1_DR);
		val &= ~(1 << 3);
		writel(val, VA_GPIO1_DR);
	}
	else
	{
		/* 设置GPIO电平为高电平 关闭LED */
		val = readl(VA_GPIO1_DR);
		val |= (1 << 3);
		writel(val, VA_GPIO1_DR);
	}
}

/* dtsled设备结构体 */
struct dtsled_dev {
	struct cdev cdev;		//字符设备
	dev_t devid;			//设备号
	struct class *class;	//类
	struct device *device;	//设备
	int major;				//主设备号
	int minor;				//次设备号
	char *chrDevName;		//设备名
	struct device_node *np;	//设备树节点
};
static struct dtsled_dev dtsled;

static int dtsled_open(struct inode *inode, struct file *filp)
{
	
	//filp->private_data = &dtsled;	/* 设置私有数据 */
	return 0;
}

static ssize_t dtsled_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	//struct dtsled_dev *dev = (struct dtsled_dev*)filp->private_data;	/* 以访问私有数据 */
	char userDataBuf[1];
	int ret = 0;
	ret = copy_from_user(userDataBuf, buf, cnt);
	if(ret < 0)
	{
		printk("write failed!\r\n");
		return -1;
	}
	led_switch(userDataBuf[0]);
	return 0;
	return 0;
}

static int dtsled_release(struct inode *inode, struct file *filp)
{
	
	return 0;
}


/* 设备操作集合 */
static const struct file_operations dtsled_fops = {
	.owner = THIS_MODULE,
	.write = dtsled_write,
	.open = dtsled_open,
	.release = dtsled_release
};

static int __init dtsled_init(void)
{
	int ret = 0;
	int result = 0;
	const char *str;
	u32 regdata[10], i = 0;
	unsigned int val = 0;


	/* 注册设备号 */
	dtsled.chrDevName = "dtsled";
	dtsled.major = 0;
	if(dtsled.major){	//如果给定了主设备号
		dtsled.devid = MKDEV(dtsled.major, 0);
		ret = register_chrdev_region(dtsled.devid, DTSLED_DEV_NUM, dtsled.chrDevName);

	}else{	//如果没给定主设备号那么申请设备号
		ret = alloc_chrdev_region(&dtsled.devid, 0, DTSLED_DEV_NUM, dtsled.chrDevName);
	}
	dtsled.major = MAJOR(dtsled.devid);
	dtsled.minor = MINOR(dtsled.devid);
	if(ret < 0){
		printk("register devid failed!\r\n");
		result = -EINVAL;
		goto fail_register_devid;
	}
	printk("dtsled  MAJOR:%d  MINOR:%d\r\n", dtsled.major, dtsled.minor);

	/* 添加字符设备 */
	dtsled.cdev.owner = dtsled_fops.owner;
	cdev_init(&dtsled.cdev, &dtsled_fops);
	ret = cdev_add(&dtsled.cdev, dtsled.devid, DTSLED_DEV_NUM);
	if(ret < 0){
		printk("register chrdev failed!\r\n");
		result = -EINVAL;
		goto fail_register_cdev;
	}
	
	/* 创建设备节点 */
	/* 	1.创建类 */
	dtsled.class = class_create(THIS_MODULE, dtsled.chrDevName);
	if(IS_ERR(dtsled.class)){
		result = PTR_ERR(dtsled.class);
		goto fail_class;
	}
	/*	2.创建设备*/
	dtsled.device = device_create(dtsled.class, NULL,
						dtsled.devid, NULL, dtsled.chrDevName);
	if(IS_ERR(dtsled.device)){
		result = PTR_ERR(dtsled.device);
		goto fail_device;
	}

	/* 获取设备节点 */
	dtsled.np = of_find_node_by_path("/alphaled");
	if(dtsled.np == NULL){
		printk("no device_node found!\r\n");
		ret = -EINVAL;
		goto fail_findnd;
	}

	/* 读取status属性 */
	ret = of_property_read_string(dtsled.np, "status", &str);
	if(ret < 0){
		printk("no status found!\r\n");
		goto fail_rs;
	}else{
		printk("status=%s\r\n", str);
	}	


	/* 读取reg属性 */
	ret = of_property_read_u32_array(dtsled.np, "reg", regdata, 10);
	if(ret < 0){
		printk("no reg found!\r\n");
		goto fail_rs;
	}else{
		printk("reg data:\r\n");
		for(i = 0; i < 10; i++){
			printk("%#X ", regdata[i]);
		}
		printk("\r\n");
	}	
#if 0
	/* LED初始化 */
	/* 	->	1.地址映射 */
	VA_CCM_CCGR1 = ioremap(regdata[0], regdata[1]);
	VA_SW_MUX_GPIO1_IO03 = ioremap(regdata[2], regdata[3]);
	VA_SW_PAD_GPIO1_IO03 = ioremap(regdata[4], regdata[5]);
	VA_GPIO1_DR = ioremap(regdata[6], regdata[7]);
	VA_GPIO1_GDIR = ioremap(regdata[8], regdata[9]);
#endif
	/* LED初始化 */
	/* 使用of_iomap() 可以直接将设备节点的<reg>属性获取并映射,
	 * 以省去使用of_property_read_u32_array(),和ioremap()
	 * 一般驱动使用设备树节点,都会这么使用,更加方便
	 * void __iomem *of_iomap(struct device_node *np, int index)
	 * index:reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为 0。
	 */
	VA_CCM_CCGR1 = of_iomap(dtsled.np, 0);
	VA_SW_MUX_GPIO1_IO03 = of_iomap(dtsled.np, 1);
	VA_SW_PAD_GPIO1_IO03 = of_iomap(dtsled.np, 2);
	VA_GPIO1_DR = of_iomap(dtsled.np, 3);
	VA_GPIO1_GDIR = of_iomap(dtsled.np, 4);

	/* 	->	2.初始化 */
	/* 	->	->	打开时钟 */
	val = readl(VA_CCM_CCGR1);
	val &=  ~(3 << 26);
	val |= (3 << 26);
	writel(val, VA_CCM_CCGR1);

	writel(0x5, VA_SW_MUX_GPIO1_IO03);		/* 设置复用 */
	writel(0x10B0, VA_SW_PAD_GPIO1_IO03);	/* 设置电气属性 */

	/* 	->	->	设置GPIO方向为输出*/
	val = readl(VA_GPIO1_GDIR);
	val |= (1 << 3);
	writel(val, VA_GPIO1_GDIR);

	/* 	->	->	设置GPIO电平为高电平 默认关闭LED */
	val = readl(VA_GPIO1_DR);
	val |= (1 << 3);
	writel(val, VA_GPIO1_DR);


	return 0;
fail_rs:
fail_findnd:
	/* 摧毁设备 */
	device_destroy(dtsled.class, dtsled.devid);
fail_device:
	/* 摧毁类 */
	class_destroy(dtsled.class);
fail_class:
	/* 注销字符设备 */
	cdev_del(&dtsled.cdev);
fail_register_cdev:
	/* 注销设备号 */
	unregister_chrdev_region(dtsled.devid, DTSLED_DEV_NUM);
fail_register_devid:
	return result;
}


static void __exit dtsled_exit(void)
{
	/* 地址映射释放 */
	iounmap(VA_CCM_CCGR1);
	iounmap(VA_SW_MUX_GPIO1_IO03);
	iounmap(VA_SW_PAD_GPIO1_IO03);
	iounmap(VA_GPIO1_DR);
	iounmap(VA_GPIO1_GDIR);

	/* 摧毁设备 */
	device_destroy(dtsled.class, dtsled.devid);

	/* 摧毁类 */
	class_destroy(dtsled.class);

	/* 注销字符设备 */
	cdev_del(&dtsled.cdev);

	/* 注销设备号 */
	unregister_chrdev_region(dtsled.devid, DTSLED_DEV_NUM);

}

/* 驱动入口和出口 */
module_init(dtsled_init);
module_exit(dtsled_exit);

/* 许可 */
MODULE_LICENSE("GPL");
/* 作者信息 */
MODULE_AUTHOR("LXG@firestaradmin");

编写测试 APP

本章直接使用前面的测试 APP,将上一章的 ledApp.c 文件复制到本章实验工程下即可。

编译测试运行即可