原文地址:https://elinux.org/Device_Tree_Usage
第一次翻译技术类文章,如有错误之处,还请各位看客批评指正~~~
一、基本数据结构
设备树是一个由节点和属性组成的简单树状结构。属性是键值对,节点可包含属性和子节点。如下是一个简单的设备树的例子:
/dts-v1/;
/ {
node1 {
a-string-property = "A string";
a-string-list-property = "first string", "second string";
// hex is implied in byte arrays. no '0x' prefix is required
a-byte-data-property = [01 23 34 56];
child-node1 {
first-child-property;
second-child-property = <1>;
a-string-property = "Hello, world";
};
child-node2 {
};
};
node2 {
an-empty-property;
a-cell-property = <1 2 3 4>; /* each number (cell) is a uint32 */
child-node1 {
};
};
};
很明显,上面的设备树并没有实际意义,因为它没有描述任何设备,但是它展示了节点和属性的结构,如下:
属性是简单的键值对,它可以为空,也可以是任意的字节流。虽然在上面的列子中,数据类型没有被编码到数据结构中,但还是可以从中看到一些基本的数据表示方法:
二、基本概念
为了理解如何使用设备树,我们从一个简单的machine开始,描述如何一步一步创建设备树的过程。
1.样机
考虑如下虚拟的machine(基于ARM架构),制造商是”Acme”,名字是”Coyote Revenge”:
2.初始化设备树
第一步,指定machine的骨架结构,这是一个有效的设备树的基本结构,在这一步你需要唯一地识别此machine
/dts-v1/;
/ {
compatible = "acme,coyotes-revenge";
};
“compatible”指定了系统的名字, 它包含了”,”格式的字符串,指定具体的设备非常重要,指定厂商是为了避免名字冲突。由于操作系统需要通过”compatible”指定的值决定如何在此machine上运行,因此正确地配置此属性非常重要。
理论上,”compatible”属性足以唯一地描述此machine了。如果设备描述是硬编码的,那么操作系统可以在顶层”compatible”属性获取到”acme,coyotes-revenge”。
下一步是描述每一个具体的CPU。一个名为”cpus”的node包含了多个子节点用来描述每个具体的CPU,在当前的例子中,系统是双核ARM Cortex A9 处理器。
/dts-v1/;
/ {
compatible = "acme,coyotes-revenge";
cpus {
cpu@0 {
compatible = "arm,cortex-a9";
};
cpu@1 {
compatible = "arm,cortex-a9";
};
};
};
每个CPU节点中的compatible 属性和顶层的compatible 属性一样,包含了一个以”
“格式来指定特定CPU型号的字符串。之后会有更多的属性添加到CPU节点中,但现在我们需要先了解一些更多的基本概念。
节点名字
了解节点的命名惯例很重要,所有的节点名字都必须要按照格式”
“命名。
“name”是一个简单的ASCII码字符串,最大长度31个字符。通常来说,节点的名字指明了其属于何种设备,例如一个3com的以太网适配器的节点名字应该是”ethernet”而不是3com。
“unit-address”用于描述带有地址的设备,通常 “unit-address”是用于访问设备的首地址,并且在节点的reg属性中列出。文章后续会介绍reg属性。
兄弟节点必须具有不同的名字,通常多个节点具有同一通用名称却有不同的地址,(如, serial@101f1000 & serial@101f2000)
设备
系统中的每一个设备都由一个设备树节点代表,接下来需要填充设备树中描述每个设备的节点,从现在开始,新的节点会保留为空直至我们学习了如何处理地址空间和中断为止。
/dts-v1/;
/ {
compatible = "acme,coyotes-revenge";
cpus {
cpu@0 {
compatible = "arm,cortex-a9";
};
cpu@1 {
compatible = "arm,cortex-a9";
};
};
serial@101F0000 {
compatible = "arm,pl011";
};
serial@101F2000 {
compatible = "arm,pl011";
};
gpio@101F3000 {
compatible = "arm,pl061";
};
interrupt-controller@10140000 {
compatible = "arm,pl190";
};
spi@10115000 {
compatible = "arm,pl022";
};
external-bus {
ethernet@0,0 {
compatible = "smc,smc91c111";
};
i2c@1,0 {
compatible = "acme,a1234-i2c-bus";
rtc@58 {
compatible = "maxim,ds1338";
};
};
flash@2,0 {
compatible = "samsung,k8f1315ebm", "cfi-flash";
};
};
};
现在,已经为系统中的每个设备添加了节点,节点的继承关系反应了设备在系统中是如何连接的。例如,外部总线上的设备是外部总线节点的子节点,i2c 设备节点是i2c总线节点的子节点。通常,继承关系反应了从CPU侧看到的系统的透视图。
到现在为止,此设备树还是无效的,因为它还缺少设备间的连接信息,这些信息在稍后会被添加。
一些需要注意的地方:
理解compatible属性
设备树中的每一个节点都代表了一个设备并且都具有compatible属性。compatible是操作系统决定将哪一个设备驱动和设备绑定的关键。
compatible是字符串列表,列表中的第一个字符串以”
“的格式描述了设备的具体信息。随后的字符串代表了其他与其兼容的设备。
例如,飞思卡尔的MPC8349片上系统(SOC)拥有一个串口设备,其实现了国家半导体ns16550寄存器接口。因此MPC8349串口设备的compatible属性应该是:
compatible = “fsl,mpc8349-uart”, “ns16550”。在这个例子中,fsl,mpc8349-uart描述了设备的具体信息,ns16550代表了其与国家半导体16550串口在寄存器层面上是兼容的。
注意:由于历史原因,ns16550不带厂商前缀。所有新的compatible的值都必须带制造商前缀。
这种做法允许现存的设备驱动绑定到新的设备,然而仍然唯一地识别特定的设备。
警告:不要在compatible属性中使用通配符,如”fsl,mpc83xx-uart”或者类似。芯片厂商会不断地更新,从而打破之前的假定,然而此时修改可能已经太晚了。因此,选择特定的芯片实现并且确保所有后续的芯片都会与其兼容。
如何寻址
可寻址的设备使用如下的属性封装地址信息到设备树中:
reg
#address-cells
#size-cells
每一个可寻址的设备都有一个”reg”属性,其是以reg =
格式组成的一系列元组,每一个元组代表了设备使用的地址空间。每一个元组是一个或者多个32位的整型数据(cell),类似地,长度值可以是一系列cell或者为空。
由于地址和长度的大小都是可变的变量,父节点的address-cells和size-cells属性用来描述每个区域包含多少个cell。换言之,要正确的翻译reg属性,需要借助父节点的address-cells和size-cells属性的值。为了了解其如何工作,我们在设备树中增加地址属性,从CPU开始。
CPU寻址
CPU节点是最简单的寻址的例子,每一个CPU分配了一个独立的ID,且CPU ID没有与其关联的size参数。
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
compatible = "arm,cortex-a9";
reg = <0>;
};
cpu@1 {
compatible = "arm,cortex-a9";
reg = <1>;
};
};
在cpu节点,address-cells被设置为0,size-cells被设置为1。意味着reg属性由一个32位的地址组成,且没有size域。在这个例子中,两个cpu被分配了0和1两个地址。cpu 节点的#size-cells=0因为每个CPU只分配了一个的地址。
需要注意的是,reg的值和name节点中的值一致。约定,如果一个节点有reg属性,那么节点名必须包含单元地址,也就是reg中的第一个地址。
存储映射设备
和cpu节点的reg属性只有单一的地址不同,一个存储映射设备分配了其能够访问的一系列地址。#size-cells用来指定每一个子节点的reg元组的寻址范围。在如下的例子中,每一个地址由一个32位的cell组成,长度也是一个cell,这在32位的系统中是非常典型的。在64位的系统中,可能使#address-cells=2 and #size-cells=2来获取64位的地址。
/dts-v1/;
/ {
#address-cells = <1>;
#size-cells = <1>;
...
serial@101f0000 {
compatible = "arm,pl011";
reg = <0x101f0000 0x1000 >;
};
serial@101f2000 {
compatible = "arm,pl011";
reg = <0x101f2000 0x1000 >;
};
gpio@101f3000 {
compatible = "arm,pl061";
reg = <0x101f3000 0x1000
0x101f4000 0x0010>;
};
interrupt-controller@10140000 {
compatible = "arm,pl190";
reg = <0x10140000 0x1000 >;
};
spi@10115000 {
compatible = "arm,pl022";
reg = <0x10115000 0x1000 >;
};
...
};
为每个设备分配一个基地址,并指定可访问空间的长度。在这个例子中,为GPIO分配了两块寻址范围:0x101f3000…0x101f3fff and 0x101f4000..0x101f400f。
有些挂载在总线上的设备拥有不同的寻址方式。例如,有些挂载在外部总线上的设备有一个片选的引脚。由于父节点决定了子节点的地址域,因此地址映射也可反应整个系统的组成结构。如下的代码就体现了挂在外部总线上带有片选引脚的设备的地址组成:片选+偏移地址。
external-bus {
#address-cells = <2>;
#size-cells = <1>;
ethernet@0,0 {
compatible = "smc,smc91c111";
reg = <0 0 0x1000>;
};
i2c@1,0 {
compatible = "acme,a1234-i2c-bus";
reg = <1 0 0x1000>;
rtc@58 {
compatible = "maxim,ds1338";
};
};
flash@2,0 {
compatible = "samsung,k8f1315ebm", "cfi-flash";
reg = <2 0 0x4000000>;
};
};
“external-bus”(外部总线)节点使用了两个cell来表示地址,一个是片选,另一个是偏移量。长度域仍然使用一个cell表示,因为只有offset部分才有范围的概念。因此reg属性包含3个cell,分别为片选、偏移量和地址范围。
由于地址域包含在节点及其子节点上,因此父节点可以自由地定义任何针对总线的寻址方案。在父节点和子节点之外的节点通常不需要关心本地寻址域,而地址必须从一个域映射到另一个域。
没有存储映射的设备
其它设备的存储空间并没有映射到处理器总线上,它们可以有地址区域,但是不能被CPU直接访问,作为替换,父设备的驱动可以使CPU能够间接访问这类设备。以i2c为例,每个i2c设备分配了一个设备地址,但是没有与之关联的长度或者范围,这和CPU节点的地址分配很类似:
i2c@1,0 {
compatible = "acme,a1234-i2c-bus";
#address-cells = <1>;
#size-cells = <0>;
reg = <1 0 0x1000>;
rtc@58 {
compatible = "maxim,ds1338";
reg = <58>;
};
};
ranges(地址转换)
我们之前讨论了如何给设备分配地址,但是这个地址仅仅是相对于当前的设备节点而言的。我们还没有介绍如何将这个地址转换成CPU能够使用的地址。
根节点描述了从CPU侧看的地址分配情况,根节点的子节点已经使用了CPU的地址域,因此无需做任何显性的映射。例如我们为串口设备”serial@101f0000”直接分配CPU地址0x101f0000。
注意那些不是根节点的子节点没有使用CPU的地址域。为了得到映射地址,设备树必须指明如何实现一个地址域到另一个地址域的转换。ranges属性就是为这个目的设置的。
/dts-v1/;
/ {
compatible = "acme,coyotes-revenge";
#address-cells = <1>;
#size-cells = <1>;
...
external-bus {
#address-cells = <2>
#size-cells = <1>;
ranges = <0 0 0x10100000 0x10000 // Chipselect 1, Ethernet
1 0 0x10160000 0x10000 // Chipselect 2, i2c controller
2 0 0x30000000 0x1000000>; // Chipselect 3, NOR Flash
ethernet@0,0 {
compatible = "smc,smc91c111";
reg = <0 0 0x1000>;
};
i2c@1,0 {
compatible = "acme,a1234-i2c-bus";
#address-cells = <1>;
#size-cells = <0>;
reg = <1 0 0x1000>;
rtc@58 {
compatible = "maxim,ds1338";
reg = <58>;
};
};
flash@2,0 {
compatible = "samsung,k8f1315ebm", "cfi-flash";
reg = <2 0 0x4000000>;
};
};
};
“ranges”是一个地址转换表,ranges 表中的每一个条目是一个包含了子设备地址,父设备地址和子节点地址域大小的元组,每一个地址域的大小由子节点和父节点的”#address-cells”属性以及子节点的”#size-cells”属性决定。在我们外部总线的例子中,父节点的地址包含1个cell,子节点的地址包含2个cell,大小为1个cell。ranges转换后的关系如下:
Offset 0 from chip select 0 is mapped to address range 0x10100000..0x1010ffff
Offset 0 from chip select 1 is mapped to address range 0x10160000..0x1016ffff
Offset 0 from chip select 2 is mapped to address range 0x30000000..0x30ffffff
相应地,如果父设备的地址和子设备的地址相同,则可以用一个空的ranges属性代替。ranges属性为空意味着父节点的地址空间和子节点的地址空间是1:1的映射关系。
你可能会问,为什么1:1映射还需要地址转换呢?一些总线(如PCI)拥有完全不同的地址空间,因此一些与此相关的详细信息需要暴露给CPU。一些DMA设备需要知道总线上的真实地址(物理地址)。一些设备需要被划分到同一个组,因为它们拥有相同的可编程物理地址映射。是否需要1:1映射很大程度上取决于操作系统所需要的信息以及硬件设计。
需要注意的是在i2c节点中并没有ranges属性。和外部总线不同,i2c总线没有被映射到CPU地址域,因此CPU访问rtc@58设备需要通过i2c@1,0设备进行间接访问。缺少ranges属性意味着子设备不能被除了父设备之外的任何设备直接访问。