【正点原子Linux连载】 第八章 Linux设备树 摘自【正点原子】ATK-DLRK3568嵌入式Linux驱动开发指南

1)实验平台:正点原子ATK-DLRK3568开发板
2)平台购买地址:https://detail.tmall.com/item.htm?id=731866264428
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/docs/boards/xiaoxitongban

第八章 Linux设备树

在前面章节中我们多次提到“设备树”这个概念,本章我们就来详细的谈一谈设备树。掌握设备树是Linux驱动开发人员必备的技能!因为在新版本的Linux中,ARM相关的驱动全部采用了设备树(也有支持老式驱动的,比较少),最新出CPU在系统启动的时候就支持设备树,比如我们的RK3568系列、NXP的I.MX8系列等。我们所使用的Linux版本为4.19.232,其支持设备树,所以正点原子ATK-DLRK3568开发板的所有Linux驱动都是基于设备树的。本章我们就来了解一下设备树的起源、重点学习一下设备树语法。

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

图8.1.1 设备树结构示意图
在图8.1.1中,树的主干就是系统总线,IIC控制器、GPIO控制器、SPI控制器等都是接到系统主线上的分支。IIC控制器有分为IIC1和IIC2两种,其中IIC1上接了FT5206和AT24C02这两个IIC设备,IIC2上只接了MPU6050这个设备。DTS文件的主要功能就是按照图8.1.1所示的结构来描述板子上的设备信息,DTS文件描述设备信息是有相应的语法规则要求的,稍后我们会详细的讲解DTS语法规则。
在3.x版本(具体哪个版本笔者也无从考证)以前的Linux内核中ARM架构并没有采用设备树。在没有设备树的时候Linux是如何描述ARM架构中的板级信息呢?在Linux内核源码中大量的arch/arm/mach-xxx和arch/arm/plat-xxx文件夹,这些文件夹里面的文件就是对应平台下的板级信息。比如在arch/arm/mach-s3c24xx/mach-smdk2440.c中有如下内容(有缩减):
示例代码8.1.1 mach-smdk2440.c文件代码段

100 static struct s3c2410fb_display smdk2440_lcd_cfg __initdata = {
101 
102     .lcdcon5    = S3C2410_LCDCON5_FRM565 |
103               S3C2410_LCDCON5_INVVLINE |
104               S3C2410_LCDCON5_INVVFRAME |
105               S3C2410_LCDCON5_PWREN |
106               S3C2410_LCDCON5_HWSWP,
......
123 };
124 
125 static struct s3c2410fb_mach_info smdk2440_fb_info __initdata = {
126     .displays   = &smdk2440_lcd_cfg,
127     .num_displays   = 1,
128     .default_display = 0,
......
143 };
144 
145 static struct platform_device *smdk2440_devices[] __initdata = {
146     &s3c_device_ohci,
147     &s3c_device_lcd,
148     &s3c_device_wdt,
149     &s3c_device_i2c0,
150     &s3c_device_iis,
151 };

上述代码中第125行的结构体变量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 fcking 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社区引入了设备树。
8.2 DTS、DTB和DTC
上一小节说了,设备树源文件扩展名为.dts,但是我们在前面移植Linux的时候却一直在使用.dtb文件,那么DTS和DTB这两个文件是什么关系呢?DTS是设备树源码文件,DTB是将DTS编译以后得到的二进制文件。将.c文件编译为.o需要用到gcc编译器,那么将.dts编译为.dtb需要用到DTC工具!DTC工具源码在Linux内核的scripts/dtc目录下,scripts/dtc/Makefile文件内容如下:
示例代码8.2.1 scripts/dtc/Makefile文件代码段

4  hostprogs-$(CONFIG_DTC) := dtc
5  always       := $(hostprogs-y)
6  
7  dtc-objs := dtc.o flattree.o fstree.o data.o livetree.o treesource.o 
8           srcpos.o checks.o util.o
9  dtc-objs += dtc-lexer.lex.o dtc-parser.tab.o
......

可以看出,DTC工具依赖于dtc.c、flattree.c、fstree.c等文件,最终编译并链接出DTC这个主机文件。如果要编译DTS文件的话只需要进入到Linux源码根目录下,然后执行如下命令:(对于RK3568,需要指定ARCH=arm64)
make ARCH=arm64 all
或者:
make ARCH=arm64 dtbs
“make ARCH=arm64 all”命令是编译Linux源码中的所有东西,包括uImage/zImage,.ko驱动模块以及设备树,如果只是编译设备树的话建议使用“make ARCH=arm64 dtbs”命令,“make ARCH=arm64 dtbs”会编译选中的所有设备树文件。如果只要编译指定的某个设备树,比如我们ATK-DLRK3568开发板对应的 “rk3568-atk-evb1-ddr4-v10-linux.dts”,可以输入如下命令:
make ARCH=arm64 rockchip/rk3568-atk-evb1-ddr4-v10-linux.dtb
结果如图8.2.1所示:
在这里插入图片描述

图8.2.1 编译单独的设备树
基于ARM架构的SOC有很多种,一种SOC又可以制作出很多款板子,每个板子都有一个对应的DTS文件,那么如何确定编译哪一个DTS文件呢?我们就以RK3568这款芯片对应的板子为例来看一下,打开arch/arm64/boot/dts/rockchip/Makefile,有如下内容:
示例代码8.2.2 arch/arm64/boot/dts/rockchip/Makefile文件代码段

89  dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3566-evb5-lp4x-v10.dtb
90  dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3566-rk817-eink.dtb
91  dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3566-rk817-eink-w6.dtb
92  dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3566-rk817-eink-w103.dtb
93  dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3566-rk817-tablet.dtb
94  dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3566-rk817-tablet-k108.dtb
95  dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3566-rk817-tablet-rkg11.dtb
96  dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3566-rk817-tablet-v10.dtb
97  dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-evb1-ddr4-v10.dtb
98  dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-evb1-ddr4-v10-android9.dtb
99  dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-evb1-ddr4-v10-linux.dtb
100 dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-evb1-ddr4-v10-linux-spi-nor.dtb
101 dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-evb2-lp4x-v10.dtb
102 dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-evb2-lp4x-v10-bt1120-to-hdmi.dtb
103 dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-evb4-lp3-v10.dtb
104 dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-evb5-ddr4-v10.dtb
105 dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-evb6-ddr3-v10.dtb
106 dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-evb6-ddr3-v10-linux.dtb
107 dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-evb6-ddr3-v10-rk628-bt1120-to-hdmi.dtb
108 dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-evb6-ddr3-v10-rk628-rgb2hdmi.dtb
109 dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-evb6-ddr3-v10-rk630-bt656-to-cvbs.dtb
110 dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-evb7-ddr4-v10.dtb
111 dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-iotest-ddr3-v10.dtb
112 dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-iotest-ddr3-v10-linux.dtb
113 dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-nvr-demo-v10.dtb
114 dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-nvr-demo-v10-linux.dtb
115 dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-nvr-demo-v10-linux-spi-nand.dtb
116 dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-nvr-demo-v12-linux.dtb
117 dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-nvr-demo-v12-linux-spi-nand.dtb
118 dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-atk-evb1-ddr4-v10-linux.dtb
119 dtb-$(CONFIG_ARCH_ROCKCHIP) += rk630-rk3568-ddr3-v10.dtb

可以看出,比如这个Makefile下有许多RK3326、RK3566、RK3568等不同的.dtb文件。如果我们使用RK3568新做了一个板子,只需要新建一个此板子对应的.dts文件,然后将对应的.dtb文件名添加到这个Makefile下,这样在编译设备树的时候就会将对应的.dts编译为二进制的.dtb文件。
示例代码8.2.2中118行就是我们在给正点原子的开发板移植Linux系统的时候添加的设备树。
8.3 DTS语法
虽然我们基本上不会从头到尾重写一个.dts文件,大多时候是直接在SOC厂商提供的.dts文件上进行修改。但是DTS文件语法我们还是需要详细的学习一遍,因为后续工作中肯定需要修改.dts文件。大家不要看到要学习新的语法就觉得会很复杂,DTS语法非常的人性化,是一种ASCII文本文件,不管是阅读还是修改都很方便。
本节我们就以rk3568-atk-evb1-ddr4-v10-linux.dts这个文件为例来讲解一下DTS语法。关于设备树详细的语法规则请参考《Devicetree SpecificationV0.2.pdf》和《Power_ePAPR_APPROVED_v1.12.pdf》这两份文档,此两份文档已经放到了开发板光盘中,路径为:开发板光盘 06、参考资料Devicetree SpecificationV0.2.pdf以及Power_ePAPR_APPROVED_v1.12.pdf
8.3.1 .dtsi头文件
和C语言一样,设备树也支持头文件,设备树的头文件扩展名为.dtsi。在rk3568-atk-evb1-ddr4-v10.dtsi中有如下所示内容:
示例代码8.3.1.1 rk3568-atk-evb1-ddr4-v10-linux.dts文件代码段
7 #include “rk3568-atk-evb1-ddr4-v10.dtsi”
8 #include “rk3568-linux.dtsi”
示例代码8.3.1.1中使用“#include”来引用“rk3568-atk-evb1-ddr4-v10.dtsi”和“rk3568-linux.dtsi”这两个.dtsi头文件。
设备树里面除了可以通过“#include”来引用.dtsi文件,也可以引用.h文件头文件,大家打开rk3568.dtsi这个文件,找到如下代码:
示例代码8.3.1.2 rk3568.dtsi文件代码段

1   #include <dt-bindings/clock/rk3568-cru.h>
2   #include <dt-bindings/interrupt-controller/arm-gic.h>
3   #include <dt-bindings/interrupt-controller/irq.h>
4   #include <dt-bindings/pinctrl/rockchip.h>
5   #include <dt-bindings/soc/rockchip,boot-mode.h>
6   #include <dt-bindings/phy/phy.h>
7   #include <dt-bindings/power/rk3568-power.h>
8   #include <dt-bindings/soc/rockchip-system-status.h>
9   #include <dt-bindings/suspend/rockchip-rk3568.h>
10  #include <dt-bindings/thermal/thermal.h>
11  #include "rk3568-dram-default-timing.dtsi"

设备树文件不仅可以应用C语言里面的.h头文件,甚至也可以引用.dts文件。因此在.dts设备树文件中,可以通过“#include”来引用.h、.dtsi和.dts文件。只是,我们在编写设备树头文件的时候最好选择.dtsi后缀。
一般.dtsi文件用于描述SOC的内部外设信息,比如CPU架构、主频、外设寄存器地址范围,比如UART、IIC等等。比如rk3568.dtsi就是描述RK3568芯片本身的外设信息,内容如下:
示例代码8.3.1.4 rk3568.dtsi文件代码段

6       #include <dt-bindings/clock/rk3568-cru.h>
7       #include <dt-bindings/interrupt-controller/arm-gic.h>
8       #include <dt-bindings/interrupt-controller/irq.h>
......
18      / {
19          compatible = "rockchip,rk3568";
20  
21          interrupt-parent = <&gic>;
22          #address-cells = <2>;
23          #size-cells = <2>;
24  
25          aliases {
26              csi2dphy0 = &csi2_dphy0;
27              csi2dphy1 = &csi2_dphy1;
28              csi2dphy2 = &csi2_dphy2;
29              dsi0 = &dsi0;
......
61              spi3 = &spi3;
62              spi4 = &sfc; // for U-Boot
63          };
64  
65          cpus {
66              #address-cells = <2>;
67              #size-cells = <0>;
68  
69              cpu0: cpu@0 {
70                  device_type = "cpu";
71                  compatible = "arm,cortex-a55";
72                  reg = <0x0 0x0>;
73                  enable-method = "psci";
74                  clocks = <&scmi_clk 0>;
75                  operating-points-v2 = <&cpu0_opp_table>;
76                  cpu-idle-states = <&CPU_SLEEP>;
77                  #cooling-cells = <2>;
78                  dynamic-power-coefficient = <187>;
79              };
80  
81              cpu1: cpu@100 {
82                  device_type = "cpu";
83                  compatible = "arm,cortex-a55";
84                  reg = <0x0 0x100>;
85                  enable-method = "psci";
86                  clocks = <&scmi_clk 0>;
87                  operating-points-v2 = <&cpu0_opp_table>;
88                  cpu-idle-states = <&CPU_SLEEP>;
89              };
90  
91              cpu2: cpu@200 {
92                  device_type = "cpu";
93                  compatible = "arm,cortex-a55";
94                  reg = <0x0 0x200>;
95                  enable-method = "psci";
96                  clocks = <&scmi_clk 0>;
97                  operating-points-v2 = <&cpu0_opp_table>;
98                  cpu-idle-states = <&CPU_SLEEP>;
99              };
100 
101             cpu3: cpu@300 {
102                 device_type = "cpu";
103                 compatible = "arm,cortex-a55";
104                 reg = <0x0 0x300>;
105                 enable-method = "psci";
106                 clocks = <&scmi_clk 0>;
107                 operating-points-v2 = <&cpu0_opp_table>;
108                 cpu-idle-states = <&CPU_SLEEP>;
109             };
110 
111             idle-states {
112                 entry-method = "psci";
113                 CPU_SLEEP: cpu-sleep {
114                     compatible = "arm,idle-state";
115                     local-timer-stop;
116                     arm,psci-suspend-param = <0x0010000>;
117                     entry-latency-us = <100>;
118                     exit-latency-us = <120>;
119                     min-residency-us = <1000>;
120                 };
121             };
122         };

示例代码8.3.1.4中第65~122行就是RK3568的CPU信息,这个节点信息描述了RK3568这颗SOC所有的CPU信息,一共有4个CPU,也就是4核,架构是cortex-A55。 rk3568.dtsi文件中不仅仅描述了CPU信息,RK3568这颗SOC所有的外设都描述的清清楚楚,比如i2c0i2c5、uart0uart9等等,关于这些设备节点信息的具体内容我们后面具体章节里面再详细的讲解。
8.3.2 设备节点
设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点,每个节点都通过一些属性信息来描述节点信息,属性就是键—值对。以下文件是结合RK官方的设备树缩减出来设备树文件内容:
示例代码8.3.2.1 设备树模板

1   / {
2       compatible = "rockchip,rk3568";
3 
4       interrupt-parent = <&gic>;
5       #address-cells = <2>;
6       #size-cells = <2>;
7 
8       aliases {
9               serial0 = &uart0;
10      };
11
12      cpus {
13          #address-cells = <2>;
14          #size-cells = <0>;
15
16          cpu0: cpu@0 {
17              device_type = "cpu";
18              compatible = "arm,cortex-a55";
19              reg = <0x0 0x0>;
20              enable-method = "psci";
21              clocks = <&scmi_clk 0>;
22              operating-points-v2 = <&cpu0_opp_table>;
23              cpu-idle-states = <&CPU_SLEEP>;
24              #cooling-cells = <2>;
25              dynamic-power-coefficient = <187>;
26          };
27
28          cpu1: cpu@100 {
29              device_type = "cpu";
30              compatible = "arm,cortex-a55";
31              reg = <0x0 0x100>;
32              enable-method = "psci";
33              clocks = <&scmi_clk 0>;
34              operating-points-v2 = <&cpu0_opp_table>;
35              cpu-idle-states = <&CPU_SLEEP>;
36          };
37
38          cpu2: cpu@200 {
39              device_type = "cpu";
40              compatible = "arm,cortex-a55";
41              reg = <0x0 0x200>;
42              enable-method = "psci";
43              clocks = <&scmi_clk 0>;
44              operating-points-v2 = <&cpu0_opp_table>;
45              cpu-idle-states = <&CPU_SLEEP>;
46          };
47
48          cpu3: cpu@300 {
49              device_type = "cpu";
50              compatible = "arm,cortex-a55";
51              reg = <0x0 0x300>;
52              enable-method = "psci";
53              clocks = <&scmi_clk 0>;
54              operating-points-v2 = <&cpu0_opp_table>;
55              cpu-idle-states = <&CPU_SLEEP>;
56          };
57
58          idle-states {
59              entry-method = "psci";
60              CPU_SLEEP: cpu-sleep {
61                  compatible = "arm,idle-state";
62                  local-timer-stop;
63                  arm,psci-suspend-param = <0x0010000>;
64                  entry-latency-us = <100>;
65                  exit-latency-us = <120>;
66                  min-residency-us = <1000>;
67              };
68          };
69      };
70      
71      i2c0: i2c@fdd40000 {
72          compatible = "rockchip,rk3399-i2c";
73          reg = <0x0 0xfdd40000 0x0 0x1000>;
74          clocks = <&pmucru CLK_I2C0>, <&pmucru PCLK_I2C0>;
75          clock-names = "i2c", "pclk";
76          interrupts = <GIC_SPI 46 IRQ_TYPE_LEVEL_HIGH>;
77          pinctrl-names = "default";
78          pinctrl-0 = <&i2c0_xfer>;
79          #address-cells = <1>;
80          #size-cells = <0>;
81          status = "disabled";
82  };
83 };

第1行,“/”是根节点,每个设备树文件只有一个根节点。细心的同学应该会发现,在rk3568.dtsi和rk3568-linux.dtsi这两个文件都有一个“/”根节点,这样不会出错吗?不会的,因为这两个“/”根节点的内容会合并成一个根节点。
第8、12和71行,aliases、cpus和i2c0是根节点“/”的三个子节点,在设备树中节点命名格式如下:
node-name@unit-address
其中“node-name”是节点名字,为ASCII字符串,节点名字应该能够清晰的描述出节点的功能,比如“uart1”就表示这个节点是UART1外设。“unit-address”一般表示设备的地址或寄存器首地址,如果某个节点没有地址或者寄存器的话“unit-address”可以不要,比如“cpus”、“cpu@f00”、“i2c@ff3f0000”。
但是我们在示例代码8.3.2.1中我们看到的节点命名却如下所示:
cpu0:cpu@0
上述命令并不是“node-name@unit-address”这样的格式,而是用“:”隔开成了两部分,“:”前面是节点标签(label),“:”后面的才是节点名字,格式如下所示:
label: node-name@unit-address
引入label的目的就是为了方便访问节点,可以直接通过&label来访问这个节点,比如通过&cpu0就可以访问“cpu@f00”这个节点,而不需要输入完整的节点名字。再比如节点 “i2c0: i2c@ff3f0000”,节点label是i2c0,而节点名字就很长了,为“i2c@ff3f0000”。很明显通过&i2c0来访问“i2c@ff3f0000”这个节点要方便很多!
第16行,cpu0也是一个节点,只是cpu0是cpus的子节点。
每个节点都有不同属性,不同的属性又有不同的内容,属性都是键值对,值可以为空或任意的字节流。设备树源码中常用的几种数据形式如下所示:
1、字符串
compatible = “rockchip,rk3568”;
上述代码设置compatible属性的值为字符串“rockchip,rk3568”。
2、32位无符号整数
reg = <0>;
上述代码设置reg属性的值为0,reg的值也可以设置为一组值,比如:
reg = <0 0x123456 100>;
3、字符串列表
属性值也可以为字符串列表,字符串和字符串之间采用“,”隔开,如下所示:
compatible = "rockchip,rk3568-evb ", “rockchip,rk3568”;
上述代码设置属性compatible的值为“rockchip,RK3568-evb”和“rockchip,rk3568”。
8.3.3 标准属性
节点是由一堆的属性组成,节点都是具体的设备,不同的设备需要的属性不同,用户可以自定义属性。除了用户自定义属性,有很多属性是标准属性,Linux下的很多外设驱动都会使用这些标准属性,本节我们就来学习一下几个常用的标准属性。
1、compatible属性
compatible属性也叫做“兼容性”属性,这是非常重要的一个属性!compatible属性的值是一个字符串列表,compatible属性用于将设备和驱动绑定起来。字符串列表用于选择设备所要使用的驱动程序,compatible属性值的格式如下所示:
“manufacturer,model”
其中manufacturer表示厂商,model一般是模块对应的驱动名字。比如rk3568-atk-evb1-ddr4-v10.dtsi中有一个MIPI摄像头节点,这个节点的摄像头芯片采用的SONY公司出品的IMX415,compatible属性值如下:
compatible = “sony,imx415”;
属性值为“sony,imx415”,其中‘sony’表示厂商是sony,也就是索尼,“imx415”表示驱动模块名字。
compatible也可以多个属性值。比如:
compatible = “ilitek,ili9881d”, “simple-panel-dsi”;
这样我们的设备就有两个属性值,这个设备首先使用第一个兼容值在Linux内核里面查找,看看能不能找到与之匹配的驱动文件,如果没有找到的话就使用第二个兼容值查,以此类推,直到查找完compatible属性中的所有值。
一般驱动程序文件都会有一个OF匹配表,此OF匹配表保存着一些compatible值,如果设备节点的compatible属性值和OF匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动。比如在文件imx415.c中有如下内容:
示例代码8.3.3.1 imx415.c 文件代码段

2598 static const struct of_device_id imx415_of_match[] = {
2599    { .compatible = "sony,imx415" },
2600    {},
2601 };
数组imx415_of_match就是imx415.c这个驱动文件的匹配表,此匹配表只有一个匹配值“sony,imx415”。如果在设备树中有哪个节点的compatible属性值与此相等,那么这个节点就会使用此驱动文件。

2、model属性
model属性值也是一个字符串,一般model属性描述开发板的名字或者设备模块信息,比如名字什么的,比如:
model = “Rockchip rk3568 EVB DDR4 V10 Board”;
3、status属性
status属性看名字就知道是和设备状态有关的,status属性值也是字符串,字符串是设备的状态信息,可选的状态如表8.3.3.1所示:
值 描述
“okay” 表明设备是可操作的。
“disabled” 表明设备当前是不可操作的,但是在未来可以变为可操作的,比如热插拔设备插入以后。至于disabled的具体含义还要看设备的绑定文档。
“fail” 表明设备不可操作,设备检测到了一系列的错误,而且设备也不大可能变得可操作。
“fail-sss” 含义和“fail”相同,后面的sss部分是检测到的错误内容。
表8.3.3.1 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 = <address1 length1 address2 length2 address3 length3……>
每个“address length”组合表示一个地址范围,其中address是起始地址,length是地址长度,#address-cells表明address这个数据所占用的字长,#size-cells表明length这个数据所占用的字长,比如:
示例代码8.3.3.2 #address-cells和#size-cells属性

1  cpus {
2   #address-cells = <1>;
3   #size-cells = <0>;
4  
5   cpu0: cpu@f00 {
6       device_type = "cpu";
7       compatible = "arm,cortex-a7";
8       reg = <0xf00>;
9       enable-method = "psci";
10      clocks = <&cru ARMCLK>;
11      operating-points-v2 = <&cpu0_opp_table>;
12      dynamic-power-coefficient = <60>;
13      #cooling-cells = <2>;
14      cpu-idle-states = <&CPU_SLEEP>;
15  };
16 };
17 
18 amba {
19  compatible = "simple-bus";
20  #address-cells = <1>;
21  #size-cells = <1>;
22  ranges;
23 
24  dmac: dma-controller@ff4e0000 {
25      compatible = "arm,pl330", "arm,primecell";
26      reg = <0xff4e0000 0x4000>;
27      interrupts = <GIC_SPI 1 IRQ_TYPE_LEVEL_HIGH>,
28               <GIC_SPI 2 IRQ_TYPE_LEVEL_HIGH>;
29      #dma-cells = <1>;
30      clocks = <&cru ACLK_DMAC>;
31      clock-names = "apb_pclk";
32      arm,pl330-periph-burst;
33  };
34 };

第2,3行,节点cpus的#address-cells = <1>,#size-cells = <0>,说明cpus的子节点reg属性中起始地址所占用的字长为1,地址长度所占用的字长为0。
第5行,子节点cpu0: cpu@0的reg 属性值为 <0xf00>,因为父节点设置了#address-cells = <1>,#size-cells = <0>,因此addres=0xf00,没有length的值,相当于设置了起始地址,而没有设置地址长度。
第20,21行,设置amba节点#address-cells = <1>,#size-cells = <1>,说明amba的所有子节点起始地址长度所占用的字长为1,地址长度所占用的字长也为1。
第26行,amba子节点dmac: dma-controller@ff4e0000的reg属性值为< 0xff4e0000 0x4000>,因为父节点设置了#address-cells = <1>,#size-cells = <1>,address= 0xff4e0000,length= 0x4000,相当于设置了起始地址为0xff4e0000,地址长度为0x4000。
5、reg属性
reg属性前面已经提到过了,reg属性的值一般是(address,length)对。reg属性一般用于描述设备地址空间资源信息或者设备地址信息,比如某个外设的寄存器地址范围信息,或者IIC器件的设备地址等,比如在rk3568.dtsi中有如下内容:
示例代码8.3.3.3 uart5节点信息

3102    uart5: serial@fe690000 {
3103        compatible = "rockchip,rk3568-uart", "snps,dw-apb-uart";
3104        reg = <0x0 0xfe690000 0x0 0x100>;
3105        interrupts = <GIC_SPI 121 IRQ_TYPE_LEVEL_HIGH>;
3106        clocks = <&cru SCLK_UART5>, <&cru PCLK_UART5>;
3107        clock-names = "baudclk", "apb_pclk";
3108        reg-shift = <2>;
3109        reg-io-width = <4>;
3110        dmas = <&dmac0 10>, <&dmac0 11>;
3111        pinctrl-names = "default";
3112        pinctrl-0 = <&uart5m0_xfer>;
3113        status = "disabled";
3114    };

uart5节点描述了rk3568系列芯片的UART5相关信息,重点是第3104行的reg属性。由于uart5的父节点“/”设置了#address-cells = <1>、#size-cells = <1>,因此reg属性中address=0xff5a0000,length=0x100。查阅《Rockchip RK3568 TRM Part1》可知,RK3568芯片的UART5寄存器首地址为0xfe690000,长度为64KB,但是UART5的寄存器远远用不了64KB,0X100完全够了,这里我们重点是获取UART5寄存器首地址。
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属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换,对于我们所使用的RK3568来说,子地址空间和父地址空间完全相同,因此会在rk3568.dtsi中找到大量的值为空的ranges属性,如下所示:
示例代码8.3.3.4 rk3568.dtsi文件代码段

3532    pinctrl: pinctrl {
3533        compatible = "rockchip,rk3568-pinctrl";
3534        rockchip,grf = <&grf>;
3535        rockchip,pmu = <&pmugrf>;
3536        #address-cells = <2>;
3537        #size-cells = <2>;
3538        ranges;
......
3605 };

第2576行定义了ranges属性,但是ranges属性值为空。
7、name属性
name属性值为字符串,name属性用于记录节点名字,name属性已经被弃用,不推荐使用name属性,一些老的设备树文件可能会使用此属性。
8、device_type属性
device_type属性值为字符串,IEEE 1275会用到此属性,用于描述设备的FCode,但是设备树没有FCode,所以此属性也被抛弃了。此属性只能用于cpu节点或者memory节点。rk3568.dtsi的cpu0节点用到了此属性,内容如下所示:
示例代码8.3.3.6 rk3568.dtsi文件代码段

69  cpu0: cpu@0 {
70      device_type = "cpu";
71      compatible = "arm,cortex-a55";
72      reg = <0x0 0x0>;
73      enable-method = "psci";
74      clocks = <&scmi_clk 0>;
75      operating-points-v2 = <&cpu0_opp_table>;
76      cpu-idle-states = <&CPU_SLEEP>;
77      #cooling-cells = <2>;
78      dynamic-power-coefficient = <187>;
79  };

关于标准属性就讲解这么多,其他的比如中断、IIC、SPI等使用的标准属性等到具体的例程再讲解。
8.3.4 根节点compatible属性
每个节点都有compatible属性,根节点“/”也不例外,在rk3568-atk-evb1-ddr4-v10.dtsi文件中根节点的compatible属性内容如下所示:
示例代码8.3.4.1 rk3568-atk-evb1-ddr4-v10.dtsi根节点compatible属性

15  / {
16          model = "Rockchip RK3568 ATK EVB1 DDR4 V10 Board";
17          compatible = "rockchip,rk3568-evb1-ddr4-v10", "rockchip,rk3568";
18
19          rk_headset: rk-headset {

可以看出,compatible有两个值:“rockchip,rk3568-evb1-ddr4-v10”和“rockchip,rk3568”。前面我们说了,设备节点的compatible属性值是为了匹配Linux内核中的驱动程序,那么根节点中的compatible属性是为了做什么工作的? 通过根节点的compatible属性可以知道我们所使用的设备,一般第一个值描述了所使用的硬件设备名字,比如这里使用的是“rk3568-evb1-ddr4-v10”这个设备,第二个值描述了设备所使用的SOC,比如这里使用的是“RK3568”这颗SOC。Linux内核会通过根节点的compoatible属性查看是否支持此设备,如果支持的话设备就会启动Linux内核。
打开include/linux/rockchip/cpu.h文件,有如下内容:
示例代码8.3.4.2 判断是否为RK3568

164 static inline bool cpu_is_rk3568(void)
165 {
166         if (rockchip_soc_id)
167                 return (rockchip_soc_id & ROCKCHIP_CPU_MASK) == ROCKCHIP_CPU_RK3568;
168         return of_machine_is_compatible("rockchip,rk3568");
169 }
86 }
函数cpu_is_rk3568用于判断当前是否为RK3568,第168行使用of_machine_is_compatible函数判断根节点 compatible值里面是否有“rockchip,rk3568”。根据示例代码8.3.4.1可知,根节点的compatible中有“rockchip,rk3568”,所以匹配。

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

2952 i2c5: i2c@fe5e0000 {
2953    compatible = "rockchip,rk3399-i2c";
2954    reg = <0x0 0xfe5e0000 0x0 0x1000>;
2955    clocks = <&cru CLK_I2C5>, <&cru PCLK_I2C5>;
2956    clock-names = "i2c", "pclk";
2957    interrupts = <GIC_SPI 51 IRQ_TYPE_LEVEL_HIGH>;
2958    pinctrl-names = "default";
2959    pinctrl-0 = <&i2c5m0_xfer>;
2960    #address-cells = <1>;
2961    #size-cells = <0>;
2962    status = "disabled";
2963 };

示例代码8.3.5.1就是RK3568的i2c5节点,现在要在i2c5节点下创建一个子节点,这个子节点就是fxls8471,最简单的方法就是在i2c5下直接添加一个名为fxls8471的子节点,如下所示:
示例代码8.3.5.2 添加 fxls8471子节点

2952 i2c5: i2c@fe5e0000 {
2953    compatible = "rockchip,rk3399-i2c";
2954    reg = <0x0 0xfe5e0000 0x0 0x1000>;
2955    clocks = <&cru CLK_I2C5>, <&cru PCLK_I2C5>;
2956    clock-names = "i2c", "pclk";
2957    interrupts = <GIC_SPI 51 IRQ_TYPE_LEVEL_HIGH>;
2958    pinctrl-names = "default";
2959    pinctrl-0 = <&i2c5m0_xfer>;
2960    #address-cells = <1>;
2961    #size-cells = <0>;
2962    status = "disabled"; 
2963    //fxls8471子节点
2964    fxls8471@1e {
2965        compatible = "fsl,fxls8471";
2966        reg = <0x1e>;
2967    };
2968 };

第2963~2967行就是添加的fxls8471这个芯片对应的子节点。但是这样会有个问题!i2c5节点是定义rk3568.dtsi文件中的,而rk3568.dtsi是共有的设备树头文件,其他所有使用到rk3568这颗SOC的板子都会引用rk3568.dtsi这个文件。直接在i2c5节点中添加fxls8471就相当于在其他的所有板子上都添加了fxls8471这个设备,但是其他的板子并没有这个设备啊!因此,按照示例代码8.3.5.2这样写肯定是不行的。
这里就要引入另外一个内容,那就是如何向节点追加数据,我们现在要解决的就是如何向i2c5节点追加一个名为fxls8471的子节点,而且不能影响到其他使用到RK3568的板子。ATK-DLRK3568开发板使用的设备树文件为rk3568-atk-evb1-ddr4-v10.dtsi和rk3568-linux.dtsi,因此我们需要在rk3568-atk-evb1-ddr4-v10.dtsi文件中完成数据追加的内容,方式如下:
示例代码8.3.5.3 节点追加数据方法

1 &i2c5 {
2 	/* 要追加或修改的内容 */
3 };
第1行,&i2c5表示要访问i2c5这个label所对应的节点,也就是rk3568.dtsi中的“i2c5: i2c@fe5e0000”。
第2行,花括号内就是要向i2c5这个节点添加的内容,包括修改某些属性的值。
正确的做法就是在rk3568-atk-evb1-ddr4-v10.dtsi中,向i2c5节点追加fxls8471相关的信息,如下所示:

示例代码8.3.5.4 向i2c1节点追加数据

1 &i2c5 {
2   status = "okay";
3   clock-frequency = <400000>;
4   
5   fxls8471@1e {
6       compatible = "fsl,fxls8471";
7       reg = <0x1e>;
8   }; 
9 }

示例代码8.3.5.4就是向i2c5节点添加/修改数据。
第5~8行,i2c5子节点fxls8471,表示I2C5上连接的fxls8471,“fxls8471”子节点里面描述了fxls8471这颗芯片的相关信息。
因为示例代码8.3.5.4中的内容是rk3568-atk-evb1-ddr4-v10.dtsi这个文件内的,所以不会对使用RK3568这颗SOC的其他板子造成任何影响。这个就是向节点追加或修改内容,重点就是通过&label来访问节点,然后直接在里面编写要追加或者修改的内容。
8.4 创建小型模板设备树
上一节已经对DTS的语法做了比较详细的讲解,本节我们就根据前面讲解的语法,从头到尾编写一个小型的设备树文件。当然了,这个小型设备树没有实际的意义,做这个的目的是为了掌握设备树的语法。在实际产品开发中,我们是不需要完完全全的重写一个.dts设备树文件,一般都是使用SOC厂商提供好的.dts文件,我们只需要在上面根据自己的实际情况做相应的修改即可。在编写设备树之前要先定义一个设备,我们就以RK3568这个SOC为例,我们需要在设备树里面描述的内容如下:
①、这个芯片是由四个Cortex-A55架构的64位CPU组成。
②、RK3568内部uart2,起始地址为0xfe660000,大小为256B(0x100)。
③、RK3568内部spi0,起始地址为0xfe610000,大小为4KB(0x1000)。
④、RK3568内部i2c5,起始地址为0xfe5e0000,大小为4KB(0x1000)。
为了简单起见,我们在设备树里面就实现这些内容即可,首先,搭建一个仅含有根节点“/”的基础的框架,新建一个名为myfirst.dts文件,在里面输入如下所示内容:
示例代码8.4.1 设备树基础框架

1   / {
2       compatible = "rockchip,rk3568-evb1-ddr4-v10", "rockchip,rk3568";
3   }

设备树框架很简单,就一个根节点“/”,根节点里面只有一个compatible属性。我们就在这个基础框架上面将上面列出的内容一点点添加进来。
1、添加cpus节点
首先添加CPU节点,RK3568采用Cortex-A55架构,先添加一个cpus节点,在cpus节点下添加cpu0~cpu3子节点,完成以后如下所示:
示例代码8.4.2 添加cpus节点

1  / {
2       compatible = "rockchip,rk3568-evb1-ddr4-v10", "rockchip,rk3568";
3   
4       cpus {
5           #address-cells = <2>;
6           #size-cells = <0>;
7  
8           /* CPU0节点 */
9           cpu0: cpu@0 {
10              device_type = "cpu";
11              compatible = "arm,cortex-a55";
12              reg = <0x0 0x0>;
13          };
14      
15          /* CPU1节点 */
16          cpu1: cpu@1 {
17              device_type = "cpu";
18              compatible = "arm,cortex-a55";
19              reg = <0x0 0x100>;
20          };
21 
22          /* CPU2节点 */
23          cpu2: cpu@2 {
24              device_type = "cpu";
25              compatible = "arm,cortex-a55";
26              reg = <0x0 0x200>;
27          };
28 
29          /* CPU3节点 */
30          cpu3: cpu@3 {
31              device_type = "cpu";
32              compatible = "arm,cortex-a55";
33              reg = <0x0 0x300>;
34          };
35      };
36 }

第4~35行,cpus节点,此节点用于描述SOC内部的所有CPU,因为RK3568有四个CPU核,所以在cpus下添加四个子节点分别为cpu0、cpu1、cpu2和cpu3。注意,示例代码8.4.2里面cpu0等子节点内容是参考示例代码,并没有写全。
2、添加uart2、spi0和i2c5节点
最后我们在myfirst.dts文件中加入uart2、spi0和i2c1这三个外设控制器的节点。最终的myfirst.dts文件内容如下:

示例代码8.4.3 最终的myfirst.dts文件

1  / {
2       compatible = "rockchip,rk3568-evb1-ddr4-v10", "rockchip,rk3568";
3   
4       cpus {
5           #address-cells = <2>;
6           #size-cells = <0>;
7  
8           /* CPU0节点 */
9           cpu0: cpu@0 {
10              device_type = "cpu";
11              compatible = "arm,cortex-a55";
12              reg = <0x0 0x0>;
13          };
14      
15          /* CPU1节点 */
16          cpu1: cpu@100 {
17              device_type = "cpu";
18              compatible = "arm,cortex-a55";
19              reg = <0x0 0x100>;
20          };
21 
22          /* CPU2节点 */
23          cpu2: cpu@200 {
24              device_type = "cpu";
25              compatible = "arm,cortex-a55";
26              reg = <0x0 0x200>;
27          };
28 
29          /* CPU3节点 */
30          cpu3: cpu@300 {
31              device_type = "cpu";
32              compatible = "arm,cortex-a55";
33              reg = <0x0 0x300>;
34          };
35      };
36  
37      /* uart2节点 */
38      uart2: serial@fe660000 {
39          compatible = "rockchip,rk3568-uart", "snps,dw-apb-uart";
40          reg = <0x0 0xfe660000 0x0 0x100>;
41      };
42  
43      /* spi0节点 */
44      spi0: spi@fe610000 {
45          compatible = "rockchip,rk3568-spi", "rockchip,rk3066-spi";
46          reg = <0x0 0xfe610000 0x0 0x1000>;
47      };
48  
49      /* i2c5节点 */
50      i2c5: i2c@fe5e0000 {
51          compatible = "rockchip,rk3568-i2c", "rockchip,rk3399-i2c";
52          reg = <0x0 0xfe5e0000 0x0 0x1000>;
53      };
54 }
第38~41行,uart2外设控制器节点。
第44~47行,spi0外设控制器节点。
第50~53行,i2c5外设控制器节点。
到这里,我们的myfirst.dts小型的设备树模板就编辑好了,基本和rk3568.dtsi很像,可以看做是rk3568.dtsi的缩小版。在myfirst.dts里面我们仅仅是编写了RK3568的外设控制器节点,节点下的属性并没有写出来。

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

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

在图8.5.1中,根节点属性表现为一个个的文件,比如图8.5.1中的“#address-cells”、“#size-cells”、“compatible”、“model”和“name”这5个文件,它们在设备树中就是根节点的5个属性。既然是文件那么肯定可以查看其内容,输入cat命令来查看model和compatible这两个文件的内容,结果如图8.5.2所示:
在这里插入图片描述

图8.5.2 model和compatible文件内容
从图8.5.2可以看出,文件model的内容是“Rockchip RK3568 ATK EVB1 DDR4 V10 Board”,文件compatible的内容为“rockchip,rk3568-evb1-ddr4-v10rockchip,rk3568”。打开文件rk3568-atk-evb1-ddr4-v10.dtsi查看一下,这不正是根节点“/”的model和compatible属性值吗!
2、根节点“/”下各子节点
图8.5.1中各个文件夹就是根节点“/”的各个子节点,比如“aliases”、“chosen”和“cpus”等等。大家可以查看一下rk3568-atk-evb1-ddr4-v10.dtsi和其所引用的所有.dtsi文件,看看根节点的子节点都有哪些,是否和图8.5.1中的一致。
/proc/device-tree目录就是设备树在根文件系统中的体现,同样是按照树形结构组织的,进入/proc/device-tree/cpus目录中就可以看到cpus节点的所有子节点,如图8.5.3所示:
图8.5.3 cpus子节点
和根节点“/”一样,图8.5.3中的所有文件分别为soc节点的属性文件和子节点文件夹。大家可以自行查看一下这些属性文件的内容是否和rk3568.dtsi中cpus节点的属性值相同。
8.6 特殊节点
在根节点“/”中有两个特殊的子节点:aliases和chosen,我们接下来看一下这两个特殊的子节点。
8.6.1 aliases子节点
打开rk3568.dtsi文件,aliases节点内容如下所示:
示例代码8.6.1.1 aliases子节点

26 aliases {
27  i2c0 = &i2c0;
28  i2c1 = &i2c1;
29  i2c2 = &i2c2;
30  i2c3 = &i2c3;
......
44  dphy0 = &csi_dphy0;
45  dphy1 = &csi_dphy1;
46 };

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

  • 7
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值