本文介绍如何为新的机器或板卡编写设备树(Device Tree), 它旨在概要性的介绍设备树概念,以及如何使用它们来描述机器或者板卡。
有关设备树数据格式的完整技术描述,请参阅ePAPR v1.1规范。 ePAPR技术规范比本文所介绍的基础主题更加详细,所以请参阅它了解本页未涉及的更高级用法。 ePAPR目前正在用Devicetree规范文档的新名称进行更新。
1 基础数据结构
设备树(Device Tree)是一种包含节点和属性的简单树形结构。属性是键值对,节点则可能包含属性和子节点。 例如,下面是一个.dts格式的简单设备树:
/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 { }; }; };
上面这个设备树,显然没有实际用处,因为它没有描述任何信息,但是它确实显示了节点的结构和属性:
-
一个简单的root节点:“/”
-
一组子节点:“node1”和“node2”
-
一组node1的子节点:“child-node1”和“child-node2”
-
一堆分散在设备树中的属性
属性是简单的键-值对,其中的值可以是空的,也可以包含任意的字节流。虽然数据类型没有编码到数据结构中,但是有一些基本的数据表示可以在设备树源文件中表示。
-
文本字符串(以null结尾),用双引号表示:
-
string-property = "a string";
-
‘cell’是被<>括号括起来的32bit无符号int数
-
cell-property = <0xbeef 123 0xabcd1234>;
-
二进制数据是被[]括号括起来
-
binary-property = [0x01 0x23 0x45 0x67];
-
不同类型的数据,可以以逗号“,”串起来
-
mixed-property = "a string", [0x01 0x23 0x45 0x67], <0x12345678>;
-
逗号“,”也可以用来表示字符串列表:
-
string-list = "red fish", "blue fish";
2 基础概念Basic Concepts
为了理解设备树(device tree)如何使用,我们将从一个简单的设备(machine)开始,建立一个设备树(device tree),然后一步一步描述它。
2.1示例设备(sample machine)
假设有这样一台虚拟的设备(基于ARM的通用版本),由“Acme”公司生产,名为“Cpyote's Revenge”:
-
一个32位宽的ARM CPU
-
处理器本地总线连接到内存映射串口、spi总线控制器、i2c控制器、中断控制器和外部总线桥
-
从0地址开始的256MB 字节的SDRAM
-
2个串口,寄存器基地址分别是0x101F1000和0x101F2000
-
GPIO的控制寄存器的基地址是0x101F3000
-
SPI的控制寄存器的基地址是0x10170000,并挂载下列设备
-
MMC slot,SS管脚连接到GPIO1
-
外部总线桥接着下列设备
-
SMC SMC91111网络设备连接到外部总线,基地址是0x10100000
-
i2c 控制寄存器基地址是0x10160000,并挂载下列设备
-
Maxim DS1338实时时钟,其地址是1101000(0x58)
-
64M的Nor flash基地址是0x30000000
2.2 初始化结构体(Initial structure)
第一步是为设备铺设骨架, 这是有效设备树所需的最小结构。在这一阶段,你需要能唯一的标识设备。
/dts-v1/; / { compatible = "acme,coyotes-revenge"; };
“compatible”表明系统的名字。它包含一个以“制造商”,“品牌”形式组成的字符串。准确的表明设备非常重要,而且需要包含制造商的名称以避免命名冲突。 由于操作系统将使用compatible值来决定如何在机器上运行,因此将正确的数据写入此属性中非常重要。理论上,一个操作系统唯一识别一台设备只要有“compatible”属性就够了。如果所有设备细节都是硬编码的,那么操作系统可以专门在Device Tree的最顶层的“compatible”属性中查找 "acme,coyotes-revenge"即可。
2.3 CPUs
下一步是描述每一个CPU。添加一个名为“cpus”的容器节点,为每一个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”的属性都是一个字符串,以“制造商”,“型号”的形式表明CPU的准确型号,就像设备树(DT)最顶层的“compatible”属性一样。
稍后将向cpu节点添加更多属性,但是我们首先需要讨论更多的基本概念。
2.4 节点名字(Node Names)
首先,我们需要了解命名的规则。每一个节点必须有一个名字,名字的形式必须是“ <name>[@<unit-address>”。<name>是一个简单的ascii字符串,最大长度为31字节。通常,节点是根据它所代表的设备来命名。比如,一个3com公司的网络适配器的名字可能会是“ethernet”,而不是“3com509”。如果这个节点描述设备需要一个地址,则包含“<unit-address>”字段。通常,这个地址是访问该设备寄存器所需要的首地址。而且会列在node的“reg”属性里。关于“reg”属性将在本文后续的内容中介绍。兄弟节点的名字不能相同,但是通常都是<name>字段相同,而<unit-address>字段不同( (比如, serial@101f1000 & serial@101f2000).)。可以查阅ePAPR的2.2.1节来了解节点命名的全部细节。
2.5 设备(Devices)
系统中的每个设备都由一个设备树节点表示。下一步是用每个设备的对应的节点填充树。现在,新的节点将保持为空,直到我们可以讨论如何处理地址范围和irq。
/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”属性
-
flash节点的“compatible”属性包含了两个字符串,下一节将说明为什么
-
就像之前提到的,节点名字反映的是设备的种类,而不是代表具体的品牌型号。 请参阅ePAPR规范的2.2.2节,其中列出了已定义的通用节点名。应该尽可能使用这些节点名,而不要发明新的名字。
2.6 理解“compatible”属性
设备树中每一个表示设备的节点都要有“compatible”属性。 “compatible”属性是操作系统用来决定将哪个设备驱动程序绑定到这个设备的关键。
“compatible”是一个字符串列表,列表中的第一个字符串以“制造商”,“型号”的形式指定确切的设备。接下来的字符串表示该设备兼容的其他设备。例如,Freescale MPC8349片上系统(Soc)有一个串行设备,执行National Semiconductor的 ns16550 寄存器接口。那么Freescale MPC8349的串行设备的“compatible”属性就是“ fsl,mpc8349-uart”,“ns16550”.在这个例子里,“ fsl,mpc8349-uart”表示确切的设备,“ns16550”表示它在寄存器级别与National Semiconductor的ns16550串行设备兼容。这里的“ns16550”没有制造商的名字,是由于历史原因。所有新的兼容性设备名称,都需要加制造商的名字。这种做法允许将现有设备驱动程序绑定到新设备,同时仍然惟一地标识确切的硬件。
警告:不要使用通配符来实现兼容性,比如 "fsl,mpc83xx-uart"或者类似的值。但芯片供应商总是会修改命名规则,一旦等到其改变打破了你的通配符假设,再来修改就已经晚了。所以,请选择一个具体的芯片型号,然后再兼容性列表里面列出所兼容的芯片型号。
3 如何寻址(How addressing work)
可寻址设备使用以下属性将地址信息编码到设备树中:
* reg * #address-cells * #size-cells
每一个可寻址设备都有一个叫“reg”的属性,reg由一系列元组构成,形式是reg = <address1 length1 [address2 length2] [address3 length3] ... >。也就是address、length交替出现。每一个元组代表一个设备使用的地址范围。每一个地址的值是一个或者多个32位int型数构成的列表,称为cells。长度的取值也是一个cells列表,或者为空。
由于address和length字段都是可变大小的变量,因此父节点中的#address-cells和#size-cells属性用于说明每个字段中有多少个单元格。换句话说,正确地解释reg属性需要父节点的#address-cells和#size-cells值。要了解这一切是如何工作的,让我们将寻址属性添加到示例设备树中,从cpu开始。#address-cells表示address的长度,#size-cells表示length的长度,如果为0,则表示没有。
3.1 CPU寻址
当讲述如何寻址的时候,CPU节点是一个最简单的例子。每个CPU被分配一个唯一的ID,而且这个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设置为1,将#size-cells设置为0。这意味着子reg值是一个uint32数,它是一个没有size字段的地址。在这种情况下,为这两个cpu分配了地址0和1。对于cpu节点,#size-cells为0,因为每个cpu只分配一个地址,没有其他的地址。
您还会注意到reg值与节点名中的@符号后面的值相同。按照习惯,如果节点具有reg属性,那么节点名必须包含单元地址,而且是reg属性中的第一个地址的值。
3.2 内存映射设备
与CPU节点只有一个地址值不同,内存映射设备会分配一个它需要响应的地址区间。#size-cells表示reg中length的位宽,如果是32位宽就是1,64位宽就是2.在下面的示例里,每个address值是1个单元格(32位),每个length也是一个单元格(32位),这在32位操作系统中是非常常见的。在64位系统可能将#address-cells、#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”使用2个单元格来表示地址域,一个是片选号,一个是该片选的设备的基地址偏移。“length”字段仍然是一个单元格,因为只有地址的偏移需要一个范围。所以在这个例子里,每一个reg包含3个单元格:片选,地址偏移,偏移的范围。
由于地址域包含在节点及其子节点中, 所以父节点可以自由地定义任何对总线有意义的寻址方案。设备节点不需要考虑本节点之外的地址域的情况。地址映射必须按照地址域一个一个的进行。
3.3 非内存映射设备
有一些设备不是地址映射设备。他们可以有地址范围,但是不能被CPU直接访问。相反,父设备的驱动将代替CPU间接的访问设备。以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>; }; };
3.4 ranges(地址转换)
我们已经讨论了如何为设备分配地址,但目前这些地址只在设备节点地址域有意义。它还没有描述如何将这些地址映射到CPU可以使用的地址。 根节点总是以CPU的视角来描述地址空间。根节点的子节点已经使用了CPU的地址域,因此不需要任何显式映射。例如,serial@101f0000设备被直接分配地址0x101f0000。
一些非根节点的子节点的设备没有直接使用CPU地址域。 为了获得内存映射地址,设备树必须指定如何将地址从一个域转换到另一个域。ranges属性正是为这一目的而设计的。
下面就是一个简单的例子,展示了一个包含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 的值,父节点的 #address-cells 的值,以及子节点的 #size-cells 值决定。 对于我们示例中的外部总线,子地址是2个单元格,父地址是1个单元格,子地址大小也是1个单元格。因此,三项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)具有完全不同的地址空间,其详细信息需要暴露给操作系统。其他的DMA引擎需要知道总线上的真实地址。有时需要将设备分组,因为它们共享相同的软件可编程物理地址映射。是否应该使用1:1映射在很大程度上取决于操作系统和具体的硬件设计信息。
您还应该注意到,i2c@1,0节点中没有ranges属性。原因在于,与外部总线不同,i2c总线上的设备不是映射到CPU地址域中的内存。相反,CPU通过i2c@1,0设备间接访问rtc@58设备。缺少范围属性意味着设备不能被除其父设备之外的任何设备直接访问。