Linux 设备树

设备树的由来
要想了解为什么会有设备树, 设备树是怎么来的, 我们就要先来回顾一下在没有设备树之前我们是怎么来写一个驱动程序的。 以字符设备驱动代码框架为例, 我们一起来回顾下。
任何的设备驱动的编写, Linux 已经为我们打好了框架, 我们只要像做完形填空一样填写进去就可以了。字符设备驱动框架如下图所示:

Linux 设备树_第1张图片

杂项设备驱动框架:

Linux 设备树_第2张图片

通过这些框架, 我们可以很容易编写驱动代码, 但是, 当我们用这个框架非常熟练的时候, 我们就会发现虽然这个方法很简单, 但是非常不容易扩展, 当我们有很多很多相似设备的时候, 如果我们都是按照这个框架来完成, 那就要写很多遍这个流程, 但是多个相似设备之间真正有差异的地方只有框架的初始化硬件的部分, 其他步骤的代码基本都是一样的。 这样就会造成大量的重复代码。 但是, 我们在编写驱动代码的时候, 我们要尽量做到代码的复用, 也就是一套驱动尽量可以兼任很多设备, 如果我们还按照这个来编写就不太符合规则了。
为了实现这个目标, 我们就要把通用的代码和有差异的代码分离出来, 来增强我们驱动代码的可移植性。 所以, 设备驱动分离的思想也就应运而生了, 在 Linux 中, 我们是在写代码的时候进行分离, 分离是把一些不相似的东西放到了 device.c, 把相似的东西放在了 driver.c, 如果我们有很多相似的设备或者平台, 我们只要修改 device.c 就可以了, 这样我们重复性的工作就大大的减少了。 这个就是平台总线的由来。
平台总线这个方法有什么弊端呢?
当我们用这个方法用习惯以后就会发现, 假如 soc 不变, 我们每换一个平台, 都要修改 C 文件, 并且还要重新编译。 而且会在 arch/arm/plat-xxx 和 arch/arm/mach-xxx 下面留下大量的关于板级细节的代码。 并不是说这个方法不好, 只是从 Linux 的发展来看, 这些代码相对于 Linux 内核来说就是“垃圾代码” , 而且这些“垃圾代码” 非常多, 于是就有了 Linux Torvalds 那句简单粗暴的话:


为了改变这个现状, 设备树也就被引进到 Linux 上了, 用来剔除相对内核来说的“垃圾代码” , 即用设备树文件来描述这些设备信息, 也就是代替 device.c 文件, platform 匹配上基本不变, 并且相比于之前的方法, 使用设备树不仅可以去掉大量“垃圾代码” , 并且采用文本格式, 方便阅读和修改, 如果需要修改部分资源, 我们也不用在重新编译内核了, 只需要把设备树源文件编译成二进制文件, 在通过 bootloader 传递给内核就可以了。 内核在对其进行解析和展开得到一个关于硬件的拓扑图。 我们通过内核提供的接口获取设备树的节点和属性就可以了。 即内核对于同一 soc 的不同主板, 只需更换设备树文件 dtb 即可实现不同主板的无差异支持, 而无需更换内核文件。
Linux 设备树_第3张图片
 

什么是设备树?
 Device Tree 是一种描述硬件的数据结构, 由一系列被命名的节点(node) 和属性(property) 组成, 而节点本身可包含子节点。 所谓属性, 其实就是成对出现的 name 和 value。
在 Device Tree 中, 可描述的信息包括: CPU 的数量和类别, 内存基地址和大小, 总线和桥, 外设连接,中断控制器和中断使用情况, GPIO 控制器和 GPIO 使用情况, Clock 控制器和 Clock 使用情况。 设备树基本上就是画一棵电路板上由 CPU、 总线、 设备组成的树, Bootloader 会将这棵树传递给内核, 然后内核可以识别这棵树, 并根据它展开出 Linux 内核中的 platform_device、 i2c_client、 spi_device 等设备, 而这些设备用到的内存、 IRQ 等资源, 也被传递给了内核, 内核会将这些资源绑定给展开的相应的设备。

 DTS 、 DTC 和 DTB

文件.dts 是一种 ASCII 文件格式设备树描述, 在 Linux 中, 一个.dts 文件对应一个 ARM 设备, 一般放置在 arch/arm/boot/dts 目录下。
dtb 文件是 dts 文件被编译后生成的二进制文件, 由 Linux 内核解析, 有了设备树文件就可以在不改动Linux 内核的情况下, 对不同的平台实现无差异的支持, 只需更换相应的 dts 文件, 即可满足。dtc 是将 dts 编译为 dtb 的工具。 在 Linux 内核下可以单独编译设备树文件, 那么如何确定去编译我们自己的板子对应的 dts 文件? 以 i.MX6ULL 终结者为例, 我们来看一下arch/arm/boot/dts/Makefile 这个文件的内容:

381 dtb-$(CONFIG_SOC_IMX6UL) += \
382 imx6ul-14x14-ddr3-arm2.dtb \
383 imx6ul-14x14-ddr3-arm2-emmc.dtb \
384 imx6ul-14x14-ddr3-arm2-flexcan2.dtb \
.......
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-emmc-10.1-1280x800-c.dtb \
422 imx6ull-14x14-emmc-7-1024x600-c.dtb \
423 imx6ull-14x14-emmc-7-800x480-c.dtb \
424 imx6ull-14x14-emmc-4.3-800x480-c.dtb \
425 imx6ull-14x14-emmc-4.3-480x272-c.dtb \
426 imx6ull-14x14-emmc-vga.dtb \
427 imx6ull-14x14-emmc-hdmi.dtb \
428 imx6ull-14x14-nand-10.1-1280x800-c.dtb \
429 imx6ull-14x14-nand-7-1024x600-c.dtb \
430 imx6ull-14x14-nand-7-800x480-c.dtb \
431 imx6ull-14x14-nand-4.3-800x480-c.dtb \
432 imx6ull-14x14-nand-4.3-480x272-c.dtb \
433 imx6ull-14x14-nand-vga.dtb \
434 imx6ull-14x14-nand-hdmi.dtb \
435 imx6ull-14x14-evk-usb-certi.dtb \
436 topeet_emmc_4_3.dtb \
437 imx6ull-9x9-evk.dtb \
438 imx6ull-9x9-evk-btwifi.dtb \
439 imx6ull-9x9-evk-ldo.dtb
440 dtb-$(CONFIG_SOC_IMX6SLL) += \
441 imx6sll-lpddr2-arm2.dtb \
442 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 文件。 第 436 行就是在 i.MX6ULL 终结者开发板移植时添加的设备树文件。
其中, DTS, DTSI, DTB, DTC, 他们之间的关系如下:

Linux 设备树_第4张图片

DTS 设备树语法结构
 一般情况下, 我们不会从头编写一个完整的 dts 文件, SOC 厂商一般会直接提供一个有着基本框架的dts 文件, 当需要添加自己的板子设备树文件时, 基于厂商提供的 dts 文件修改即可。 所以我们要了解 dts设备树文件的语法, 这样我们才清楚如何添加我们自己的设备。

dtsi 头文件
由于一个 SOC 可能对应多个 ARM 设备, 这些 dts 文件势必包含许多共同的部分, Linux 内核为了简化, 把 SOC 公用的部分或者多个设备共同的部分提炼为.dtsi 文件, 类似于 C 语言的头文件。device tree source include(dtsi)是更通用的设备树代码, 也就是相同芯片但不同平台都可以使用的代码。.dtsi 文件也可以包含其他的.dtsi。 在 topeet_emmc_4_3.dts 文件中有如下内容:

#include 
#include "imx6ull.dtsi"

用“#include”关键字来引用了 input.h 和 imx6ull.dtsi 文件,
在 imx6ull-14x14-evk-gpmi-weim.dts 文件中有如下内容:

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

用“ #include” 关键字来引用了 imx6ull-14x14-evk.dts 文件, 由此可以看出在.dts 文件中可以
通过“ #include” 来引用.h、 .dtsi 和.dts 文件。
一般.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 等等。 下面我们就来介绍一下设备节点的具体信息。

设备节点信息
设备树从根节点开始, 每个设备都是一个节点。 根节点就相当于树根。 节点和节点之间可以互相嵌套,形成父子关系。 可以理解为树枝可以分成好几个小的树枝。 设备的属性用 key-value 对(键值对)来描述, 每个属性用分号结束。 下面先来看一个设备树结构模板:

1 / {
2 node1 {
3        a-string-property = "A string";
4        a-string-list-property = "first string", "second string";
5        a-byte-data-property = [0x01 0x23 0x34 0x56];
6        child-node1 {
7                first-child-property;
8                second-child-property = <1>;
9                a-string-property = "Hello, world";
10              };
11       child-node2 {
12                   };
13 };
14 node2 {
15         an-empty-property;
16         a-cell-property = <1 2 3 4>;
17         child-node1 {
18 };
19 };
20 }

上面的 dts 文件内容并没有实际的用途, 只是基本表示了一个设备树源文件的结构。 但是这里面体现了一些属性:
 一个单独的根节点: “/”
 两个子节点: “node1” 和“node2”
 两个 node1 的子节点: “child-node1” 和“child-node2”
 一些分散在树里的属性, 属性是最简单的键-值对, 它的值可以为空或者包含一个任意的字节流。

虽然数据类型并没有编码进数据结构, 但是设备树源文件中仍有几个基本的数据表示形式:
1) 文本字符串(无结束符) , 可以用双引号表示:
a-string-property = "A string";
2) “cells” 是 32 位无符号整数, 用尖括号限定:
cell-property = <0xbeef 123 0xabcd1234>;
3) 二进制数据用方括号限定:
binary-property = [0x01 0x23 0x45 0x67];
4) 不同表示形式的数据可以用逗号连在一起:
mixed-property = "a string", [0x01 0x23 0x45 0x67], <0x12345678>;
5) 逗号也可以用于创建字符串列表:
string-list = "red fish", "blue fish";
下面我们看一下简化之后的 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 行, “/” 是根节点。
第 2、 6 和 17 行, aliases、 cpus 和 intc 是三个子节点。
第 10 行, cpu0 是 cpus 的子节点。
简单来说, 节点就好比一颗大树, 从树的主干开始, 然后有一节一节的树枝, 这个就叫节点。 在代码中的节点是什么样子的呢。 我们把上面模板中的根节点摘出来, 如下图所示, 这个就是根节点, 相当于大树的树干。

/{

};

而树枝就相当于设备树的子节点, 同样我们把子节点摘出来就是根节点里面的 node1 和 node2, 如下图所示:

/{ //根节点
     node1//子节点 node1
     { 
     };
     node2//子节点 node2
    { 
    };
};

一个树枝是不是也可以继续分成好几个树枝呢, 也就是说子节点里面可以包含子子节点。 所以
child-node1 和 child-node2 是 node1 的子节点, 如下图所示:

/{ //根节点
node1//子节点 node1
{
   child-node1 //子子节点
   { 
   };
};

node2//子节点 node2
{
   child-node2 //子子节点
   { 
   };
};
};

设备节点及 lable 命名
在前面的代码中, 我们注意到节点和子节点之间的命名有所不同, 它们都遵循了下面的命名格式:

格式: <名称>[@<设备地址>]

<名称>节点的名称也不是任意起的, 一般要体现设备的类型而不是特点的型号, 比如网口, 应该命名为ethernet, 而不是随意起一个, 比如 111。
<设备地址>就是用来访问该设备的基地址。 但并不是说在操作过程中来描述一个地址, 他主要用来区分用。
注意事项:
 同一级的节点只要地址不一样, 名字是可以不唯一的。
 设备地址是一个可选选项, 可以不写。 但为了容易区分和理解, 一般是都写的。
当我们找一个节点的时候, 我们必须书写完整的节点路径, 如果我们的节点名很长, 那么我们在引用的时候就十分不方便, 所以, 设备树允许我们用下面的形式为节点标注引用(起别名)。 比如一个动漫人物的名字是蒙其· D· 路飞, 他的小名是路飞, 那是不是小名要比我们的全名更容易记忆了。 这个就是别名。举例:

uart8: serial@02288000

其中, uart8 就是这个节点名称的别名, serial@02288000 就是节点名称。
一般我往一个节点里面添加内容的时候, 不会直接把添加的内容写到节点里面, 而是通过节点的引用来添加。举例

&uart8 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_uart8>;
    status = "okay";
};

&uart8 表示引用节点别名为 uart8 的节点, 并往这个节点里面添加以下内容:

pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart8>;
status = "okay";

注意事项:编译设备树的时候, 相同的节点的不同属性信息都会被合并, 相同节点的相同的属性会被重写, 使用引用可以避免四处找节点。 如 dts 和 dtsi 里面都有根节点, 但最终会合并成一个根节点。

标准属性
address-cells 和 size-cells 属性
不同的平台, 不同的总线, 地址位长度可能不同, 有 32 位地址, 有 64 位地址, 为了适应这个, 规范规定一个 32 位的长度为一个 cell。
"#address-cells"属性用来表示总线地址需要几个 cell 表示, 该属性本身是 u32 类型的。
"#size-cells"属性用来表示子总线地址空间的长度需要几个 cell 表示, 属性本身的类型也是 u32。
可以这么理解父节点表示总线, 总线上每个设备的地址长度以及地址范围是总线的一个特性, 用
"#address-cells","#size-cells"属性表示, 比如总线是 32 位, 那么"#address-cells"设置成 1 就可以了。 这两个属性不可以继承, 就是说在未定义这两个属性的时候, 不会继承更高一级父节点的设置, 如果没有设置的话, 内核默认认为"#address-cells"为 2, "#size-cells"为 1。 举例来说, 如下所示:

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 行 reg 属性值为 <0>, 相当于设置了起始地址, 而没有设置地址长度。
第 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>, 相当于设置了起始地址为 0x02280000,地址长度为 0x40000。

reg 属性
"reg"属性用来表示节点地址资源的, 比如常见的就是寄存器的起始地址及大小。 要想表示一块连续地址, 必须包含起始地址和空间大小两个参数, 如果有多块地址, 那么就需要多组这样的值表示。 对于'reg'属性, 每个元素是一个二元组, 包含起始地址和大小。 还有另外一个问题, 地址和大小用几个 u32 表示呢?这个就由父节点的"#address-cells","#size-cells"属性确定。
比如在 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。
compatible 属性
设备树中的每个表示一个设备的节点都需要一个 compatible 属性, compatible 属性是操作系统用来决定设备和驱动绑定的关键因素。 compatible 属性也叫做兼容性属性, 属性的值是一个字符串列表, 用于表示是何种设备, 可以在代码中进行匹配。举例:

compatible = "manufacturer,model";

第一个字符串表示厂商, 后面的字符串表示确切的设备名称。 比如在 topeet_emmc_4_3.dts 文件中sound 节点表示开发板的音频设备节点, i.MX6ULL 终结者开发板上的音频芯片是欧胜(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 内核也没有找到对应的驱动。

status 属性
status 属性用来表示节点的状态, 其实就是硬件的状态, 用字符串表示。
 “okay” 表示硬件正常工作
 “disable” 表示当前硬件不可用
 “fail” 表示因为出错不可用
 “fail-sss” 表示某种原因出错不可用, sss 表示具体出错的原因。
实际中, 基本只用“okay” 和“disabl” 。
 

你可能感兴趣的:(嵌入式,设备树,linux)