作者:linuxer 发布于:2014-5-30 16:47 分类:统一设备模型

一、前言

一些背景知识(例如:为何要引入 Device Tree,这个机制是用来解决什么问题的)请参考引入 Device Tree 的原因,本文主要是介绍 Device Tree 的基础概念。

简单的说,如果要使用 Device Tree,首先用户要了解自己的硬件配置和系统运行参数,并把这些信息组织成 Device Tree source file。通过 DTC(Device Tree Compiler),可以将这些适合人类阅读的 Device Tree source file 变成适合机器处理的 Device Tree binary file(有一个更好听的名字,DTB,device tree blob)。在系统启动的时候,boot program(例如:firmware、bootloader)可以将保存在 flash 中的 DTB copy 到内存(当然也可以通过其他方式,例如可以通过 bootloader 的交互式命令加载 DTB,或者 firmware 可以探测到 device 的信息,组织成 DTB 保存在内存中),并把 DTB 的起始地址传递给 client program(例如 OS kernel,bootloader 或者其他特殊功能的程序)。对于计算机系统(computer system),一般是 firmware->bootloader->OS,对于嵌入式系统,一般是 bootloader->OS。

本文主要描述下面两个主题:

1、Device Tree source file 语法介绍

2、Device Tree binaryfile 格式介绍

二、Device Tree 的结构

在描述 Device Tree 的结构之前,我们先问一个基础问题:是否 Device Tree 要描述系统中的所有硬件信息?答案是否定的。基本上,那些可以动态探测到的设备是不需要描述的,例如 USB device。不过对于 SOC 上的 usb host controller,它是无法动态识别的,需要在 device tree 中描述。同样的道理,在 computer system 中,PCI device 可以被动态探测到,不需要在 device tree 中描述,但是 PCI bridge 如果不能被探测,那么就需要描述之。

为了了解 Device Tree 的结构,我们首先给出一个 Device Tree 的示例:

/ o device-tree
|- name = “device-tree”
|- model = “MyBoardName”
|- compatible = “MyBoardFamilyName”
|- #address-cells = <2>
|- #size-cells = <2>
|- linux,phandle = <0>
|
o cpus
| | - name = “cpus”
| | - linux,phandle = <1>
| | - #address-cells = <1>
| | - #size-cells = <0>
| |
| o PowerPC,970@0
| |- name = “PowerPC,970”
| |- device_type = “cpu”
| |- reg = <0>
| |- clock-frequency = <0x5f5e1000>
| |- 64-bit
| |- linux,phandle = <2>
|
o memory@0
| |- name = “memory”
| |- device_type = “memory”
| |- reg = <0x00000000 0x00000000 0x00000000 0x20000000>
| |- linux,phandle = <3>
|
o chosen
|- name = “chosen”
|- bootargs = “root=/dev/sda2”
|- linux,phandle = <4>

从上图中可以看出,device tree 的基本单元是 node。这些 node 被组织成树状结构,除了 root node,每个 node 都只有一个 parent。一个 device tree 文件中只能有一个 root node。每个 node 中包含了若干的 property/value 来描述该 node 的一些特性。每个 node 用节点名字(node name)标识,节点名字的格式是node-name@unit-address。如果该 node 没有 reg 属性(后面会描述这个 property),那么该节点名字中必须不能包括 @和 unit-address。unit-address 的具体格式是和设备挂在那个 bus 上相关。例如对于 cpu,其 unit-address 就是从 0 开始编址,以此加一。而具体的设备,例如以太网控制器,其 unit-address 就是寄存器地址。root node 的 node name 是确定的,必须是 “/”。

在一个树状结构的 device tree 中,如何引用一个 node 呢?要想唯一指定一个 node 必须使用 full path,例如 / node-name-1/node-name-2/node-name-N。在上面的例子中,cpu node 我们可以通过 / cpus/PowerPC,970@0 访问。

属性(property)值标识了设备的特性,它的值(value)是多种多样的:

1、可能是空,也就是没有值的定义。例如上图中的 64-bit ,这个属性没有赋值。

2、可能是一个 u32、u64 的数值(值得一提的是 cell 这个术语,在 Device Tree 表示 32bit 的信息单位)。例如 #address-cells = <1> 。当然,可能是一个数组。例如 < 0x00000000 0x00000000 0x00000000 0x20000000>

4、可能是一个字符串。例如 device_type = “memory” ,当然也可能是一个 string list。例如 “PowerPC,970”

三、Device Tree source file 语法介绍

了解了基本的 device tree 的结构后,我们总要把这些结构体现在 device tree source code 上来。在 linux kernel 中,扩展名是 dts 的文件就是描述硬件信息的 device tree source file,在 dts 文件中,一个 node 被定义成:

[label:] node-name[@unit-address] {
[properties definitions]
[child nodes]
}

“[]” 表示 option,因此可以定义一个只有 node name 的空节点。label 方便在 dts 文件中引用,具体后面会描述。child node 的格式和 node 是完全一样的,因此,一个 dts 文件中就是若干嵌套组成的 node,property 以及 child note、child note property 描述。

考虑到空泛的谈比较枯燥,我们用实例来讲解 Device Tree Source file 的数据格式。假设蜗窝科技制作了一个 S3C2416 的开发板,我们把该 development board 命名为 snail,那么需要撰写一个 s3c2416-snail.dts 的文件。如果把所有的开发板的硬件信息(SOC 以及外设)都描述在一个文件中是不合理的,因此有可能其他公司也使用 S3C2416 搭建自己的开发板并命令 pig、cow 什么的,如果大家都用自己的 dts 文件描述硬件,那么其中大部分是重复的,因此我们把和 S3C2416 相关的硬件描述保存成一个单独的 dts 文件可以供使用 S3C2416 的 target board 来引用并将文件的扩展名变成 dtsi(i 表示 include)。同理,三星公司的 S3C24xx 系列是一个 SOC family,这些 SOCs(2410、2416、2450 等)也有相同的内容,因此同样的道理,我们可以将公共部分抽取出来,变成 s3c24xx.dtsi,方便大家 include。同样的道理,各家 ARM vendor 也会共用一些硬件定义信息,这个文件就是 skeleton.dtsi。我们自下而上(类似 C++中的从基类到顶层的派生类)逐个进行分析。

1、skeleton.dtsi。位于 linux-3.14\arch\arm\boot\dts 目录下,具体该文件的内容如下:

/ {

  1. #address-cells = <1>;<br />
  2. #size-cells = <1>;<br />
  3. chosen { };<br />
  4. aliases { };<br />
  5. memory { device_type = "memory"; reg = <0 0>; };<br />

};

device tree 顾名思义是一个树状的结构,既然是树,必然有根。“/”是根节点的 node name。“{”和 “}” 之间的内容是该节点的具体的定义,其内容包括各种属性的定义以及 child node 的定义。chosen、aliases 和 memory 都是 sub node,sub node 的结构和 root node 是完全一样的,因此,sub node 也有自己的属性和它自己的 sub node,最终形成了一个树状的 device tree。属性的定义采用 property = value 的形式。例如 #address-cells 和 #size-cells 就是 property,而 < 1 > 就是 value。value 有三种情况:

1)属性值是 text string 或者 string list,用双引号表示。例如 device_type = “memory”

2)属性值是 32bit unsigned integers,用尖括号表示。例如 #size-cells = <1>

3)属性值是 binary data,用方括号表示。例如binary-property = [0x01 0x23 0x45 0x67]

如果一个 device node 中包含了有寻址需求(要定义 reg property)的 sub node(后文也许会用 child node,和 sub node 是一样的意思),那么就必须要定义这两个属性。“#” 是 number 的意思,#address-cells 这个属性是用来描述 sub node 中的 reg 属性的地址域特性的,也就是说需要用多少个 u32 的 cell 来描述该地址域。同理可以推断 #size-cells 的含义,下面对 reg 的描述中会给出更详细的信息。

chosen node 主要用来描述由系统 firmware 指定的 runtime parameter。如果存在 chosen 这个 node,其 parent node 必须是名字是 “/” 的根节点。原来通过 tag list 传递的一些 linux kernel 的运行时参数可以通过 Device Tree 传递。例如 command line 可以通过 bootargs 这个 property 这个属性传递;initrd 的开始地址也可以通过 linux,initrd-start 这个 property 这个属性传递。在本例中,chosen 节点是空的,在实际中,建议增加一个 bootargs 的属性,例如:

“root=/dev/nfs nfsroot=1.1.1.1:/nfsboot ip=1.1.1.2:1.1.1.1:1.1.1.1:255.255.255.0::usbd0:off console=ttyS0,115200 mem=64M@0x30000000”

通过该 command line 可以控制内核从 usbnet 启动,当然,具体项目要相应修改 command line 以便适应不同的需求。我们知道,device tree 用于 HW platform 识别,runtime parameter 传递以及硬件设备描述。chosen 节点并没有描述任何硬件设备节点的信息,它只是传递了 runtime parameter。

aliases 节点定义了一些别名。为何要定义这个 node 呢?因为 Device tree 是树状结构,当要引用一个 node 的时候要指明相对于 root node 的 full path,例如 / node-name-1/node-name-2/node-name-N。如果多次引用,每次都要写这么复杂的字符串多少是有些麻烦,因此可以在 aliases 节点定义一些设备节点 full path 的缩写。skeleton.dtsi 中没有定义 aliases,下面的 section 中会进一步用具体的例子描述之。

memory device node 是所有设备树文件的必备节点,它定义了系统物理内存的 layout。device_type 属性定义了该 node 的设备类型,例如 cpu、serial 等。对于 memory node,其 device_type 必须等于 memory。reg 属性定义了访问该 device node 的地址信息,该属性的值被解析成任意长度的(address,size)数组,具体用多长的数据来表示 address 和 size 是在其 parent node 中定义(#address-cells 和 #size-cells)。对于 device node,reg 描述了 memory-mapped IO register 的 offset 和 length。对于 memory node,定义了该 memory 的起始地址和长度。

本例中的物理内存的布局并没有通过 memory node 传递,其实我们可以使用 command line 传递,我们 command line 中的参数“mem=64M@0x30000000” 已经给出了具体的信息。我们用另外一个例子来加深对本节描述的各个属性以及 memory node 的理解。假设我们的系统是 64bit 的,physical memory 分成两段,定义如下:

RAM: starting address 0x0, length 0x80000000 (2GB)
RAM: starting address 0x100000000, length 0x100000000 (4GB)

对于这样的系统,我们可以将 root node 中的 #address-cells 和 #size-cells 这两个属性值设定为 2,可以用下面两种方法来描述物理内存:

方法 1:

memory@0 {
device_type = “memory”;
reg = <0x000000000 0x00000000 0x00000000 0x80000000
0x000000001 0x00000000 0x00000001 0x00000000>;
};

方法 2:

memory@0 {
device_type = “memory”;
reg = <0x000000000 0x00000000 0x00000000 0x80000000>;
};

memory@100000000 {
device_type = “memory”;
reg = <0x000000001 0x00000000 0x00000001 0x00000000>;
};

2、s3c24xx.dtsi。位于 linux-3.14\arch\arm\boot\dts 目录下,具体该文件的内容如下(有些内容省略了,领会精神即可,不需要描述每一个硬件定义的细节):

include “skeleton.dtsi”

/ {
compatible = “samsung,s3c24xx”; -------------------(A)
interrupt-parent = <&intc>; ----------------------(B)

aliases {
pinctrl0 = &pinctrl_0; ------------------------(C)
};

intc:interrupt-controller@4a000000 { ------------------(D)
compatible = “samsung,s3c2410-irq”;
reg = <0x4a000000 0x100>;
interrupt-controller;

  1. #interrupt-cells = <4>;<br />
  2. };

serial@50000000 { ----------------------(E)
compatible = “samsung,s3c2410-uart”;
reg = <0x50000000 0x4000>;
interrupts = <1 0 4 28>, <1 1 4 28>;
status = “disabled”;
};

pinctrl_0: pinctrl@56000000 {------------------(F)
reg = <0x56000000 0x1000>;

wakeup-interrupt-controller {
compatible = “samsung,s3c2410-wakeup-eint”;
interrupts = <0 0 0 3>,
<0 0 1 3>,
<0 0 2 3>,
<0 0 3 3>,
<0 0 4 4>,
<0 0 5 4>;
};
};

……
};

这个文件描述了三星公司的 S3C24xx 系列 SOC family 共同的硬件 block 信息。首先提出的问题就是:为何定义了两个根节点?按理说 Device Tree 只能有一个根节点,所有其他的节点都是派生于根节点的。我的猜测是这样的:Device Tree Compiler 会对 DTS 的 node 进行合并,最终生成的 DTB 只有一个 root node。OK,我们下面开始逐一分析:

(A)在描述 compatible 属性之前要先描述 model 属性。model 属性指明了该设备属于哪个设备生产商的哪一个 model。一般而言,我们会给 model 赋值 “manufacturer,model”。例如 model = “samsung,s3c24xx”。samsung 是生产商,s3c24xx 是 model 类型,指明了具体的是哪一个系列的 SOC。OK,现在我们回到 compatible 属性,该属性的值是 string list,定义了一系列的 modle(每个 string 是一个 model)。这些字符串列表被操作系统用来选择用哪一个 driver 来驱动该设备。假设定义该属性:compatible = “aaaaaa”, “bbbbb”。那么操作操作系统可能首先使用 aaaaaa 来匹配适合的 driver,如果没有匹配到,那么使用字符串 bbbbb 来继续寻找适合的 driver,对于本例,compatible =”samsung,s3c24xx”,这里只定义了一个 modle 而不是一个 list。对于 root node,compatible 属性是用来匹配 machine type 的(在 device tree 代码分析文章中会给出更细致的描述)。对于普通的 HW block 的节点,例如 interrupt-controller,compatible 属性是用来匹配适合的 driver 的。

(B)具体各个 HW block 的 interrupt source 是如何物理的连接到 interruptcontroller 的呢?在 dts 文件中是用 interrupt-parent 这个属性来标识的。且慢,这里定义 interrupt-parent 属性的是 root node,难道 root node 会产生中断到 interrupt controller 吗?当然不会,只不过如果一个能够产生中断的 device node 没有定义 interrupt-parent 的话,其 interrupt-parent 属性就是跟随 parent node。因此,与其在所有的下游设备中定义 interrupt-parent,不如统一在 root node 中定义了。

intc 是一个 lable,标识了一个 device node(在本例中是标识了 interrupt-controller@4a000000 这个 device node)。实际上,interrupt-parent 属性值应该是是一个 u32 的整数值(这个整数值在 Device Tree 的范围内唯一识别了一个 device node,也就是 phandle),不过,在 dts 文件中中,可以使用类似 c 语言的 Labels and References 机制。定义一个 lable,唯一标识一个 node 或者 property,后续可以使用 & 来引用这个 lable。DTC 会将 lable 转换成 u32 的整数值放入到 DTB 中,用户层面就不再关心具体转换的整数值了。

关于 interrupt,我们值得进一步描述。在 Device Tree 中,有一个概念叫做 interrupt tree,也就是说 interrupt 也是一个树状结构。我们以下图为例(该图来自 Power_ePAPR_APPROVED_v1.1):

Device Tree(二):基本概念 - 图1

系统中有一个 interrrupt tree 的根节点,device1、device2 以及 PCI host bridge 的 interrupt line 都是连接到 root interrupt controller 的。PCI host bridge 设备中有一些下游的设备,也会产生中断,但是他们的中断都是连接到 PCI host bridge 上的 interrupt controller(术语叫做 interrupt nexus),然后报告到 root interrupt controller 的。每个能产生中断的设备都可以产生一个或者多个 interrupt,每个 interrupt source(另外一个术语叫做 interrupt specifier,描述了 interrupt source 的信息)都是限定在其所属的 interrupt domain 中。

在了解了上述的概念后,我们可以回头再看看 interrupt-parent 这个属性。其实这个属性是建立 interrupt tree 的关键属性。它指明了设备树中的各个 device node 如何路由 interrupt event。另外,需要提醒的是 interrupt controller 也是可以级联的,上图中没有表示出来。那么在这种情况下如何定义 interrupt tree 的 root 呢?那个没有定义 interrupt-parent 的 interrupt controller 就是 root。

(C)pinctrl0 是一个缩写,他是 / pinctrl@56000000 的别名。这里同样也是使用了 Labels and References 机制。

(D)intc(node name 是 interrupt-controller@4a000000 ,我这里直接使用 lable)是描述 interrupt controller 的 device node。根据 S3C24xx 的 datasheet,我们知道 interrupt controller 的寄存器地址从 0x4a000000 开始,长度为 0x100(实际 2451 的 interrupt 的寄存器地址空间没有那么长,0x4a000074 是最后一个寄存器),也就是 reg 属性定义的内容。interrupt-controller 属性为空,只是用来标识该 node 是一个 interrupt controller 而不是 interrupt nexus(interrupt nexus 需要在不同的 interrupt domains 之间进行翻译,需要定义 interrupt-map 的属性,本文不涉及这部分的内容)。#interrupt-cells 和 #address-cells 概念是类似的,也就是说,用多少个 u32 来标识一个 interrupt source。我们可以看到,在具体 HW block 的 interrupt 定义中都是用了 4 个 u32 来表示,例如串口的中断是这样定义的:

interrupts = <1 0 4 28>, <1 1 4 28>;

(E) 从 reg 属性可以 serial controller 寄存器地址从 0x50000000 开始,长度为 0x4000。对于一个能产生中断的设备,必须定义 interrupts 这个属性。也可以定义 interrupt-parent 这个属性,如果不定义,则继承其 parent node 的 interrupt-parent 属性。 对于 interrupt 属性值,各个 interrupt controller 定义是不一样的,有的用 3 个 u32 表示,有的用 4 个。具体上面的各个数字的解释权归相关的 interrupt controller 所有。对于中断属性的具体值的描述我们会在 device tree 的第三份文档-代码分析中描述。

(F)这个 node 是描述 GPIO 控制的。这个节点定义了一个 wakeup-interrupt-controller 的子节点,用来描述有唤醒功能的中断源。

3、s3c2416.dtsi。位于 linux-3.14\arch\arm\boot\dts 目录下,具体该文件的内容如下(有些内容省略了,领会精神即可,不需要描述每一个硬件定义的细节):

include “s3c24xx.dtsi”

include “s3c2416-pinctrl.dtsi”

/ {
model = “Samsung S3C2416 SoC”;
compatible = “samsung,s3c2416”; ---------------A

cpus { ----------------------------B

  1. #address-cells = <1>;<br />
  2. #size-cells = <0>;

cpu {
compatible = “arm,arm926ejs”;
};
};

interrupt-controller@4a000000 { -----------------C
compatible = “samsung,s3c2416-irq”;
};

……

};

(A)在 s3c24xx.dtsi 文件中已经定义了 compatible 这个属性,在 s3c2416.dtsi 中重复定义了这个属性,一个 node 不可能有相同名字的属性,具体如何处理就交给 DTC 了。经过反编译,可以看出,DTC 是丢弃掉了前一个定义。因此,到目前为止,compatible = samsung,s3c2416。在 s3c24xx.dtsi 文件中定义了 compatible 的属性值被覆盖了。

(B)对于根节点,必须有一个 cpus 的 child node 来描述系统中的 CPU 信息。对于 CPU 的编址我们用一个 u32 整数就可以描述了,因此,对于 cpus node,#address-cells 是 1,而 #size-cells 是 0。其实 CPU 的 node 可以定义很多属性,例如 TLB,cache、频率信息什么的,不过对于 ARM,这里只是定义了 compatible 属性就 OK 了,arm926ejs 包括了所有的 processor 相关的信息。

(C)s3c24xx.dtsi 文件和 s3c2416.dtsi 中都有interrupt-controller@4a000000这个 node,DTC 会对这两个 node 进行合并,最终编译的结果如下:

interrupt-controller@4a000000 {
compatible = “samsung,s3c2416-irq”;
reg = <0x4a000000 0x100>;
interrupt-controller;

  1. #interrupt-cells = <0x4>;<br />
  2. linux,phandle = <0x1>;<br />
  3. phandle = <0x1>;<br />
  4. };

4、s3c2416-pinctrl.dtsi

这个文件定义了 pinctrl@56000000 这个节点的若干 child node,主要用来描述 GPIO 的 bank 信息。

5、s3c2416-snail.dts

这个文件应该定义一些 SOC 之外的 peripherals 的定义。

四、Device Tree binary 格式

1、DTB 整体结构

经过 Device Tree Compiler 编译,Device Tree source file 变成了 Device Tree Blob(又称作 flattened device tree)的格式。Device Tree Blob 的数据组织如下图所示:

Device Tree(二):基本概念 - 图2

2、DTB header。

对于 DTB header,其各个成员解释如下:

| header field name | description |
| magic | 用来识别 DTB 的。通过这个 magic,kernel 可以确定 bootloader 传递的参数 block 是一个 DTB 还是 tag list。 |
| totalsize | DTB 的 total size |
| off_dt_struct | device tree structure block 的 offset |
| off_dt_strings | device tree strings block 的 offset |
| off_mem_rsvmap | offset to memory reserve map。有些系统,我们也许会保留一些 memory 有特殊用途(例如 DTB 或者 initrd image),或者在有些 DSP+ARM 的 SOC platform 上,有写 memory 被保留用于 ARM 和 DSP 进行信息交互。这些保留内存不会进入内存管理系统。 |
| version | 该 DTB 的版本。 |
| last_comp_version | 兼容版本信息 |
| boot_cpuid_phys | 我们在哪一个 CPU(用 ID 标识)上 booting |
| dt_strings_size | device tree strings block 的 size。和 off_dt_strings 一起确定了 strings block 在内存中的位置 |
| dt_struct_size | device tree structure block 的 size。和和 off_dt_struct 一起确定了 device tree structure block 在内存中的位置 |

3、 memory reserve map 的格式描述

这个区域包括了若干的 reserve memory 描述符。每个 reserve memory 描述符是由 address 和 size 组成。其中 address 和 size 都是用 U64 来描述。

4、device tree structure block 的格式描述

device tree structure block 区域是由若干的分片组成,每个分片开始位置都是保存了 token,以此来描述该分片的属性和内容。共计有 5 种 token:

(1)FDT_BEGIN_NODE (0x00000001)。该 token 描述了一个 node 的开始位置,紧挨着该 token 的就是 node name(包括 unit address)

(2)FDT_END_NODE (0x00000002)。该 token 描述了一个 node 的结束位置。

(3)FDT_PROP (0x00000003)。该 token 描述了一个 property 的开始位置,该 token 之后是两个 u32 的数据,分别是 length 和 name offset。length 表示该 property value data 的 size。name offset 表示该属性字符串在 device tree strings block 的偏移值。length 和 name offset 之后就是长度为 length 具体的属性值数据。

(4)FDT_NOP (0x00000004)。

(5)FDT_END (0x00000009)。该 token 标识了一个 DTB 的结束位置。

一个可能的 DTB 的结构如下:

(1)若干个 FDT_NOP(可选)

(2)FDT_BEGIN_NODE

node name

paddings

(3)若干属性定义。

(4)若干子节点定义。(被 FDT_BEGIN_NODE 和 FDT_END_NODE 包围)

(5)若干个 FDT_NOP(可选)

(6)FDT_END_NODE

(7)FDT_END

5、device tree strings bloc 的格式描述

device tree strings bloc 定义了各个 node 中使用的属性的字符串表。由于很多属性会出现在多个 node 中,因此,所有的属性字符串组成了一个 string block。这样可以压缩 DTB 的 size。

原创文章,转发请注明出处。蜗窝科技www.wowotech.net。

标签: Device tree

Device Tree(二):基本概念 - 图3

http://www.wowotech.net/linux_kenrel/dt_basic_concept.html