Linux设备树专有名词及语法规则详解(上)

2019-11-21 11:34发布

设备树源文件扩展名为.dts,但是我们在移植 Linux 的时候却一直在使用.dtb 文件,那么 DTS 和DTB 这两个文件是什么关系呢?DTS 是设备树源码文件,DTB 是将DTS 编译以后得到的二进制文件,Linux 内核和 uboot 只能 DTB 文件。将.c 文件编译为.o 需要用到 gcc 编译器,那么将.dts 编译为.dtb 需要什么工具呢?需要用到DTC 工具。

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

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

12 #include 

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 

11 #include 

12 #include 

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

示例代码中第 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”。

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

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

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

1、compatible 属性

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 内核里面查找,看看能不能找到与之匹配的驱动文件,如果没有找到的话就使用第二个兼容值查找,直到找到或者查找完整个 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 属性值也是字符串,字符串是设备的状态信息,可选的状态如表所示:

dbc677c78f724b0bb753b8f042297de4.jpg

status 属性值表

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 =

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

1 spi4 {

 #address-cells 和#size-cells 属性

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

第 2,3 行,节点 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 = ;

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。

第 6 行,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 等使用的标准属性等后面文章再讲解。