Linux设备树
基本语法
节点语法
[label:] node-name[@unit-address] {
[properties definitions];
[child nodes];
};
label: 可选项,节点别名,后续节点中可以使用 &label 来表示引用指定节点
node-name: 节点名
unit-address: 设备地址,一般填写该设备寄存器组或内存块的首地址
properties definitions:属性定义
child nodes:子节点
根节点
根节点表示整块开发板的信息
#address-cells // 在子节点的reg属性中, 使用多少个u32整数来描述地址(address)
#size-cells // 在子节点的reg属性中, 使用多少个u32整数来描述大小(size)
compatible // 定义一系列的字符串, 使用“制造商,型号,兼容型号...”的格式
phandle属性
在后续节点中属性值性质表示某节点时,可以引用对应节点
pic@10000000 {
phandle = <1>;
interrupt-controller;
};
another-device-node {
interrupt-parent = <1>; // 使用phandle值为1来引用上述节点
};
基本数据格式
设备树是具有简单树形结构的节点和属性。属性是成对的键值,节点可能包含属性和子节点。举个例子来说,下面是一个简单的.dts格式的树:
/ {
node1 {
a-string-property = "A string";
a-string-list-property = "first string", "second string";
a-byte-data-property = [0x01 0x23 0x34 0x56];
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 {
};
};
};
这棵树很明显是没用的,因为它没用描述任何东西,但是它确实展示出了节点属性。即:
- 单个根节点:"/"
- 一对子节点:"node1", "node2"
- 一对node1的子节点:"child-node1", "child-node2"
- 树上散布的一堆属性
- 文本字符串(以null作为终止)以双引号的形式表示:
- string-property = "a string'
- 'Cells'是32位无符号整数,并且使用尖括号分隔:
- cell-property = <0xbeef 123 0xabcd1234>
- 二进制数据使用方括号分隔:
- binary-perperty = [0x01 0x23 0x45 0x67]
- 不同表示类型的数据可以通过逗号级联在一起:
- mixed-perperty = "a string", [0x01 0x23 0x45 0x67], <0x12345678>;
- 逗号也可以用来创建字符串列表:
- string-list = "red fish", "blue fish";
基本概念
为了理解怎样使用设备树,我们从一个样品机开始并且搭建设备树来一步步描述它
考虑下面的假想机器(粗略地基于ARM Versatile),"Acme"制造,命名 “Coyote's Revenge":
- 32位 ARM CPU单核
- PLB粘附在内存映射串口上,spi总线控制器,i2c控制器,中断控制器以及外部总线桥
- 256MB SDRAM基址从0开始
- 2个串口,基址从0x101F1000,0X101F2000开始
- GPIO控制器,基址从0x101F3000开始
- SPI控制器,基址从0x10170000并且总线上挂载着下列设备:
- MMC卡槽并且SS引脚连接在GPIO #1上
- 外部总线桥并且总线上挂载着下列设备:
- SMC SMC91111以太网设备连接在基址从0x10100000开始的外部总线上
- i2c控制器,基址从0x10160000开始并且总线上挂载着如下设备:
- Maxim DS1338实时时钟。该器件响应0x58的从机地址。
- 64MB NOR flash,基址从0x30000000开始
初始结构
第一步是为机器制定框架结构。下面是一个合法的设备树所需的最小结构。在这个节点,你想想要能够唯一地识别该机器
/ {
compatible = "acme,coyotes-revenge";
};
compatible制定系统的名称。它包含"<manufacture>,<model>"格式的字符串。准确地确定器件型号是非常重要的,并且我们需要包含厂商的名字来避免名字空间冲突。因为操作系统会使用compatible这个值来决定怎样在这个机器上运行,所以在这个属性中放入正确的值是非常重要的。
理论上来说,compatible是一个OS需要唯一地识别机器所需要的唯一数据。如果所有机器的细节都是写死的,那么OS可以在顶层compatible属性中专门查找"acme,coyotes-revenge"。
CPUs
下一步是描述每个CPU。一个命名为"cpus"的容器节点跟有对应每个CPU的子节点。在这个例子中,系统是一个双核ARM Cortex A9的系统。
/ {
compatible = "acme,coyotes-revenge";
cpus {
cpu@0 {
compatible = "arm,cortex-a9";
};
cpu@1 {
compatible = "arm,cortex-a9";
};
};
};
每个CPU节点中的compatible属性是以<manufacturer>,<model>的格式确定CPU型号的字符串,就像顶层的compatible属性那样。
更多的属性会在稍后添加到cpu节点中,但是我们首先需要讨论更多基本的概念。
节点名字
我们值得花一段时间讨论命名习惯。每一个几点必须要有一个以<name>[@<unit-address>]形式的名字。
<name>是简单的ascii字符串并且长度最大可以到31个字符。通常来说,节点是根据它所代表的设备类型来命名的。举个例子,3com公司的以太网适配器节点可能会使用ethernet作为它的名字,而不是3com509。
unit-address只有在节点描述含有地址的设备时会被包含进来。通常来说,unit address是用来访问设备的基址,并且在节点的reg属性中被罗列出来。我们稍后会在本文档中介绍reg属性。
兄弟节点必须被唯一地命名,不过对于不只一个节点的情况,我们通常会使用通用的名字,只要它们的地址不一样就可以。(比如:seriali@101f1000 和serial@101f2000)。
如果需要更多节点命名方面的详细情况,请参考ePAPR规范的第2.21部分
设备
系统中的每一个设备都由一个设备树节点来代表。下一步就是用每一个设备对应的节点来填充树。现在来说,新节点将会被置空知道我们可以讨论地址范围以及终端是怎样安排的。
/ {
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部分,规范中提到了一系列已经定义好了随处可能用到的通用节点名字
理解compatible属性
设备树中代表设备的每一个节点必须要有compatible属性。compatible是操作系统用来决定哪个设备驱动绑定哪个设备的关键字。
compatible是字符串列表。列表中的第一个字符串以"<manufacturer>,<model>"的形式确定了节点代表的设备。接下来的字符串表示该设备可以兼容的其他设备。
举个例子来说,Freescale MPC8349 片上系统(SoC)有一个串行设备实现了国家半导体 ns16550寄存器接口。MPC8349穿行设备的compatible属性因此应该是:compatible = "fsl,mpc8349-uart","ns16550"。在这个例子中,fsl,mpc8349-uart确定了设备并且ns16550表示它在寄存器级别兼容国家半导体16550 UART。
注意:ns16550没有厂商前缀纯粹是因为历史原因(IBM-PC/AT吧..)。所有新的compatible值应该使用厂商前缀。
该操作允许现存的设备驱动绑定到更新的设备上,不过它仍然唯一地识别确切的硬件。
警告:不要在compatible值中使用通配符,比如"fsl,mpc83xx-uart"或者类似地。硅片厂商总是会做一些打破你通配符假设的变化,到那时再改变就为时已晚了。相反,选择一个特定的硅片实现并且使所有随后的硅片与之兼容。
寻址是如何工作的
可寻址的设备使用下面的属性来将地址信息编码进入设备树:
- reg
- #address-cells
- #size-cells
因为地址和长度域都是长度可变的,所以父节点中的#address-cells和#size-cells属性是用来说明每一个域中有多少个cells。换句话说,正确地翻译一个reg属性需要父节点的#address-cells和#size-cells值。为了看到这一切是怎样工作的,让我们将寻址属性添加到样例设备树中,首先从CPU开始
CPU寻址
讨论到寻址时,CPU节点代表最简单的情况。每一个CPU被赋予了一个唯一的ID并且CPU的ID没有关联的尺寸。
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值是代表地址且没有size域的单个32位无符号整数。在这个例子中,2个cpu被赋予了地址0和1。cpu节点的#size-cells是0,因为cpu仅仅被赋予了单个地址。
你能注意到reg值匹配节点名字中的值。按照惯例,如果一个节点具有reg属性,这个节点必须包含单元地址,也就是reg属性中的第一个地址值。
内存映射设备
不同于cpu节点中找到的单地址值,内存映射设备被分配了它会响应的一个地址范围。#size-cells用来说明每一个子节点中的reg元组有多长。接下来的例子中,每一个地址值都是一个单元的(32位),每一个长度值也是一个单元,即典型的32位系统。64位系统可以把#address-cells和#size-cells赋值为2从而在设备设备树中得到64位寻址。
/ {
#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 和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>;
};
};
外部总线使用了2个单元的地址值。一个是片选数字,另一个是片选基地址的偏移。长度域仍旧是单个单元,因为只有地址的偏移部分需要一个范围。因此在本例中,每一个reg入口都包含了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>;
};
};
范围(地址转换)
我们已经讨论过了怎样为设备分配地址,但是这里的这些地址只是本地的设备节点地址。它缺并没有描述怎样从那些地址映射到CPU可以使用的地址。
根节点总是描述从CPU的视角看到的地址空间。根节点的子节点已经使用了CPU的地址域,是、因此不需要任何显示地映射。举个例子来说,serial@101f0000设备是直接被分配了0x101f0000地址。
那些不是根节点的直接子节点的节点不能使用CPU的地址域。为了得到一个内存映射的地址,设备树必须制定如何从一个域地址转换到另一个域。ranges属性就是用于这个目的的。
这里是添加了ranges属性的设备树例子。
/ {
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个单元,大小也是一个单元的。三个ranges是这样被转换的:
- 从片选0处偏移0开始的地方被映射到地址范围0x10100000...0x1010ffff
- 从片选1处偏移0开始的地方被映射到地址范围0x10160000...0x1016ffff
- 从片选2处偏移0开始的地方被映射到地址范围0x30000000...0x10000000
你可能会问为什么地址转换总是用在所有1:1映射的情况下。一些总线(比如PCI)具有完全不同的地址空间,而这些细节必须暴露给操作系统。其他总线具有DMA引擎,这些引擎需要知道总线上的实际滴孩子。有时候设备需要被组合在一起,因为它们都共享有同样的软件可编程物理地址映射方法。该不该用1:1映射更大程度上取决于操作系统所需的信息以及硬件设计。
你应该也注意到,i2c@1,0节点中没有ranges属性,原因是不像外部总线,i2c总线上的设备没有内存映射到CPU的地址域。相反地,CPU通过i2c@1,0设备间接地访问rtc@58设备。缺少ranges属性意味着,设备不能直接被除了其父节点之外的任何设备访问
中断是怎样工作的
中断不同于遵循树自然结构的地址范围转换,中断信号可能来自以及终止在机器的任何设备。不像设备树中自然表示的设备寻址,中断信号是以独立于设备树的节点之间的链接表示的。四个属性用来描述中断的联系:
- interrupt-controller —— 一个空的属性声明接收中断信号的设备为节点
- #interrupt-cells —— 这是中断控制器节点的属性。它表明这个中断控制器的中断描述符符中有多少单元。(类似于#adderss-cells以及#size-cells)
- interrupt-parent —— 包含phandle的设备节点的一个属性,这个phandle指向它所连接到的中断控制器
- interrupts —— 包含中断描述符列表的设备节点的一个属性,每一个设备上的中断输出信号都有一个
下面的代码添加了连接到我们的Coyote's Revenge样例机器的中断。
/ {
compatible = "acme,coyotes-revenge";
#address-cells = <1>;
#size-cells = <1>;
interrupt-parent = <&intc>;
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
compatible = "arm,cortex-a9";
reg = <0>;
};
cpu@1 {
compatible = "arm,cortex-a9";
reg = <1>;
};
};
serial@101f0000 {
compatible = "arm,pl011";
reg = <0x101f0000 0x1000 >;
interrupts = < 1 0 >;
};
serial@101f2000 {
compatible = "arm,pl011";
reg = <0x101f2000 0x1000 >;
interrupts = < 2 0 >;
};
gpio@101f3000 {
compatible = "arm,pl061";
reg = <0x101f3000 0x1000
0x101f4000 0x0010>;
interrupts = < 3 0 >;
};
intc: interrupt-controller@10140000 {
compatible = "arm,pl190";
reg = <0x10140000 0x1000 >;
interrupt-controller;
#interrupt-cells = <2>;
};
spi@10115000 {
compatible = "arm,pl022";
reg = <0x10115000 0x1000 >;
interrupts = < 4 0 >;
};
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>;
interrupts = < 5 2 >;
};
i2c@1,0 {
compatible = "acme,a1234-i2c-bus";
#address-cells = <1>;
#size-cells = <0>;
reg = <1 0 0x1000>;
interrupts = < 6 2 >;
rtc@58 {
compatible = "maxim,ds1338";
reg = <58>;
interrupts = < 7 3 >;
};
};
flash@2,0 {
compatible = "samsung,k8f1315ebm", "cfi-flash";
reg = <2 0 0x4000000>;
};
};
};
有一些事情需要注意:
- 这个机器只有一个中断控制器,interrupt-controller@10140000
- 标签'intc:'已经被添加到中断控制器节点,并且标签被用来分配指向根节点的interrupt-parent属性的phandle。这个interrupt-parent值变成了系统的默认值,因为所有的子节点都继承了它,除非它被显示地覆盖。
- 每一个设备都使用一个interrupt属性来制定一个不同的中断输入信号。
- #interrupt-cells是2,因此每一个中断描述符有2个单元。这个例子采用了使用了常见的模式,用第一个单元来编码中断线号,用第二个单元来编码标志,比如高有效还是低有效又或者是边缘触发还是电平触发。对于任何给定的中断控制器,请参考控制器绑定文档来了解描述符是怎样编码的。
设备特定数据
除了公共的属性之外,我们可以添加任何属性以及子节点到节点。我们可以添加操作系统需要的任何数据,只要遵守一些规则即可。
首先,新的设备特定的属性名字应该使用生产厂商的前缀从而它们不会与现存的标准属性名称冲突。
其次,属性以及子节点的意义必须以绑定的形式记录下来从而设备驱动作者了解怎样翻译数据。绑定记录一个特定的compatible值意味着什么,它应该要有什么属性,它可能会有什么样的子节点以及设备表示什么。每一个唯一的compatible值应该有它自己的绑定(或者声明与其他compatible值得兼容性)。新设备的绑定在这个wiki中被记录。请看主页描述文档格和审查过程。
第三,请将新的绑定提交到devicetree-discuss@lists.ozlabs.org邮件列表进行审查。审查新的绑定会抓住许多将来可能导致问题常见错误。
特殊节点
aliases节点
一个特定的节点通常是以完整的路径来引用,比如/external-bus/ethernet@0,0,不过当一个用户真的想知道“哪个设备是eth0”时,这将会很繁琐。aliases节点可以用来为一个完整的设备路径分配一个短的别名。比如:
aliases {
ethernet0 = ð0;
serial0 = &serial0;
};
当需要为设备指定一个标示符时,操作系统欢迎大家使用别名。
你将会注意到这里使用了一个新的语法。propert = &label; 语法分配由标签引用并作为字符串属性的的完整节点路径。这与早前使用的phandle = <&label>;不同,它在单元中嵌入了phandle值。
chosen节点
chosen节点并不代表真实的设备,不过充当为在固件和操作系统之间传递数据的地方,比如启动参数。在chosen节点中的数据不代表硬件。典型情况下,chosen节点在.dts源文件中留空并在启动时填充。
在我们的系统中,固件可能会添加下面的内容到chosen节点:
chosen {
bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200";
};
高级主题
高级的样例机器
现在我们已经理解了基本定义,让我们添加一些硬件到样例机器中来讨论一些更复杂的使用案例。
高级的样例机器添加了一个PCI主桥配有内存映射到0x10180000的控制寄存器和编程至从0x80000000地址以上开始的BARs。
假设我们已经知道关于设备树以上内容,我们可以从添加如下节点描述PCI主桥开始。
pci@10180000 {
compatible = "arm,versatile-pci-hostbridge", "pci";
reg = <0x10180000 0x1000>;
interrupts = <8 0>;
};
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律