本笔记学习时的参考资料为Device Tree Reference,且笔记尽可能保持该资料原有的层次结构。
Linux中设备树的主要目的是提供一种描述不可发现硬件的方法。此信息以前在源代码中使用硬编码实现。
设备树并非在Linux中首创,在PowerPC中已有应用。
如下是一个简单的设备树示例:
/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 {
};
};
};
上述的设备树中:
/dts-v1/;
表示设备树版本号,必须添加,否则报错。/
表示根节点,其后的花括号内定义了两个子节点 node1
和 node2
。
node1
节点中提供了若干 key-value
式的键值对:
a-string-property
:一个字符串类型的键值对,值为 A string
。a-string-list-property
:一个由字符串组成的数组。a-byte-data-property
:一个由二进制构成的数组,值为 { 0x01, 0x23, 0x34, 0x56}
。child-node1
,子元素为:
node2
节点而在设备树中支持的数据类型有:
文本字符串 | 双引号包围 | a-string-property = "A string"; |
A string |
文本字符串构成的数组 | 逗号分割 | a-string-list-property = "first string", "second string"; |
略 |
uint32_t 构成的数组 |
使用 <> 包围,空格分割 |
second-child-property = <1 0xbeef>; |
{ 1, 48879 } |
二进制构成的数组 | 使用 [] 包围,空格分割 |
a-byte-data-property = [01 23 34 56]; |
{ 0x01, 0x23, 0x34, 0x56} |
混合数据 | 逗号分割 | mixed-property = "a string", [0x01 0x23 0x45 0x67], <0x12345678>; |
版本号在设备树源文件开头使用如下方式定义即可:
/dts-v1/;
根节点的统一格式为:
/ {
};
子节点的格式为:
[${label}: ]node-name[${@unit-address}] {
[${properties}]
[${child nodes}]
};
注:
${label}
即别名,方便被引用。当包含设备地址时,其节点被引用时也需要带地址引用。但是使用 ${label}
后则引用 ${label}
即可。[${@unit-address}]
没有实际的语法意义,只是为了方便阅读,方便命名和避免重名。具体地址定义应当在 [${properties}]
区域。不过当节点中含有reg属性后,地址区域也应当加上地址后缀,否则会有警告。/dts-v1/;
/ {
node1 {
child {
};
};
node2 {
child {
};
};
serial@101F0000 {
};
serial@101F2000 {
};
};
由于笔记书写顺序问题,以及部分属性需要结合后续示例进行学习,因此本章节部分属性以跳转形式标记的可以到跳转的目标章节进行学习。
一般使用model描述设备的名称,其值为字符串。
该属性仅为描述性属性,并无程序性作用。
status属性表示该属性所述的设备状态,其可选项有:
okay
:设备是可用状态disabled
:设备不可用fail
:设备错误且不可用fail-${content}
:设备错误且不可用,错误内容为 ${content}
在一些设备树文件中,会使用 device_type
来描述内存和CPU节点(只能用于描述这俩)。
例如:
/dts-v1/;
/ {
compatible = "acme,coyotes-revenge";
cpus {
cpu@0 {
compatible = "arm,cortex-a9";
device_type = "cpu";
};
};
memory@30000000 {
device_type = "memory";
reg = ...;
};
不过该属性已被新版本Linux废弃。
基本概念:
假设给定的机器有如下属性:
0x101F1000
,占用内存大小为 0x1000
0x101F2000
,占用内存大小为 0x1000
0x101F3000
,占用内存大小为 0x1010
。其中:
0x101F3000 ~ 0x101F3FFF
0x101F4000 ~ 0x101F401F
0x10170000
,占用内存大小为 0x1000
,并且外挂有如下设备:
GPIO#1
0x10100000
并占用大小 0x10000
的内存片。0x10160000
并占用大小 0x10000
的内存片,并外挂如下设备:
0x58
0x30000000
并占用大小 0x1000000
的内存片。0
在根节点上添加子节点 compatible
,要求格式为 <manufacturer>,<model>
:
/dts-v1/;
/ {
compatible = "acme,coyotes-revenge";
};
该属性名务必正确填写,compatible属性将被用于匹配驱动,具体可详见章节compatible属性。
/dts-v1/;
/ {
compatible = "acme,coyotes-revenge";
cpus {
cpu@0 {
compatible = "arm,cortex-a9";
};
cpu@1 {
compatible = "arm,cortex-a9";
};
};
};
CPU的 compatible
填写格式也必须为 <manufacturer>,<model>
,该值会指向正确的CPU架构。
上述示例中出现了 cpu@0
,其命名规则为 <name>[@<unit-address>]
,具体规则如下:
<name>
为一个简单字符串,长度最大为31字符。通常节点是根据它所代表的设备类型来命名的。unit-address
。通常单元地址是用于访问设备的主地址,并列在节点 reg
的属性中。reg
属性可见后文。<name>[@<unit-address>]
格式的)名称,地址不同的节点名称不同。针对上述假设,可以初步编写如下的设备树框架:
/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";
};
};
};
注:
compatible
属性,具体可详见章节compatible属性compatible
属性有两个字符串compatible属性是操作系统选择设备驱动时使用的key值,因此其基本要求为:
<manufacturer>,<model>
格式。compatible = "fsl,mpc8349-uart", "ns16550"
:fsl,mpc8349-uart
指向确切的器件ns16550
说明该器件与 ns16550
完全兼容。ns16550
也应当遵守 <manufacturer>,<model>
格式,但是由于历史原因没有保留制造商前缀。"fsl,mpc8349-uart"
不可写为 "fsl,mpc83xx-uart"
。因为即使现在mpc83xx的所有型号都互相兼容,但是不能保证该制造商未来的所有mpc83xx型号依旧会兼容该驱动程序(通常来说,编写设备树的不一定是设备制造商自己。甚至设备制造商自己也无法做到向前兼容,例如如果遇到重大设计问题导致不得不改版等)。在操作系统原理中,将各种可寻址外设统一映射到一片固定的物理内存上是一个常见的设计。例如上述章节中将串口1映射到了 0x101F1000
上。
不过同样的,也存在无法映射到内存上的设备,比如上述的I2C总线上外挂的rtc设备等。
但是这些设备的寻找通常都需要指定一个地址,例如上述rtc也需要在I2C总线上指定一个从设备地址。(当然也有例外,例如CPU不需要寻址)
而设计统一的寻址方式需要解决如下的问题:
#address-cells = <n>
指定该节点的子节点的地址所占用的大小,单位32Bit。例如 #address-cells = <2>
表示每个地址使用2个32位字(因为尖括号内是32位变量,因此单位就是32Bit)。 #size-cells = <n>
指定该节点的子节点所占用内存大小的宽度,单位32Bit。例如 #size-cells = <1>
表示需要使用1个32位字定义内存大小变量,即 uint32_t size
。而具体占用的大小需要在下一条中定义。 reg = < ${region1}[ ${region2} ...] >
reg
可以同时写一个或多个内存区域。${regionx}
的格式如下:address [size]
#address-cells
和 #size-cells
规定的大小不为1时,则形式为:[address_high ]address_low[ size_high size_low]
具体示例如各子章节。
通常来说,CPU不需要寻址,因此该值通常用于指定 CPU_ID
。
则明显地:
CPU_ID
位数)为1位,即 #address-cells = <1>;
#size-cells = <0>;
address high
、 size high
、 size low
均可省略,有:reg = <0>;
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
compatible = "arm,cortex-a9";
reg = <0>;
};
cpu@1 {
compatible = "arm,cortex-a9";
reg = <1>;
};
};
在内存映射设备中,由于设备都被映射到内存中了,而上述内存宽度的两个配置都是针对子节点才开始生效。因此在根节点中定义内存宽度即可。
例如上述的32位Cortex A9中,则有平台内存映射设备的:
#address-cells = <1>
#size-cells = <1>
/dts-v1/;
/ {
#address-cells = <1>;
#size-cells = <1>;
compatible = "acme,coyotes-revenge";
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 >;
};
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 >;
};
external-bus {
// 由于外部总线下的节点是非根节点
// (也就是不直接与CPU相连的,位于特定总线或设备下的节点)
// 因此其不能直接使用上述方法直接使用CPU地址域配置。
// 详细的方法见 "地址转换章节" 。
};
};
其中:
reg
中。有些设备不能被映射到内存,其可能是没有地址范围,也可能是其内存不能被CPU访问而需要父设备代为访问。这种不能映射到内存的设备被称为非内存映射设备。
例如上述假设中,被挂在I2C总线下的DS1338 RTC时钟就属于非内存映射设备。其可以如下编写设备树:
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地址上。例如:
reserved-memory
)通常原封不动的映射到CPU地址上。因此ranges属性被设计用于完成子地址和父地址之间的转换,其格式为:
ranges = <[${addr_pairs1} ${addr_pairs2} ...]>
${addr_pairsx}
的格式为:${slave_addr} ${master_addr}[${size}]
。ranges;
),此时含义为将总线上的地址原封不动的映射到CPU地址上。<${片选ID} ${偏移量}>
的两个32位字ranges
属性:ranges = <0 0 0x10100000 0x10000 // Chipselect 1, Ethernet
1 0 0x10160000 0x10000 // Chipselect 2, i2c controller
2 0 0x30000000 0x1000000>; // Chipselect 3, NOR Flash
随后从总线上挂的设备的reg使用从总线地址即可,即:
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>;
};
};
首先引入以下四个属性:
interrupt-controller
,中断控制器,是中断信号的接收设备。其是空属性,即 interrupt-controller;
,用于声明该节点为中断信号的接收设备。#interrupt-cells
,定义中断说明符有多少个单元。常见的单元有中断号、中断类型触发等(例如GPIO的上升沿、下降沿触发等...)。interrupt-parent
,用于定义节点所属中断控制器。该属性可以继承给子节点。interrupts
,给出中断事件的属性,其符合 #interrupt-cells
中的单元要求。随后依次完成:
interrupt-controller;
属性添加到中断的接收设备下。#interrupt-cells
定义中断说明符。interrupt-parent
为该节点和子节点定义接收中断的设备。interrupts
给出各中断源设备中断的说明符。下方代码即是将所有中断均绑定到同一个中断接收器下。
/dts-v1/;
/ {
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>;
};
};
};
不过这种方式限制了一个设备只能有一个中断接收器。
Linux的设备树支持为节点添加任意的属性,但是为了避免冲突,该属性必须使用 ${manufacture},
前缀,例如rockchip的 rk3288.dtsi
中就使用了 rockchip,grf
、 rockchip,playback-channels
、rockchip,pins
等属性。
不过如果需要并入mainline kernel,则需要:
compatible
值都应该有自己的绑定(或声明与另一个兼容值的兼容性)。新设备的绑定记录在此 wiki 中。有关文档格式和审核过程的描述,请参阅Main Page。例如对于设备树:
/dts-v1/;
/ {
compatible = "acme,coyotes-revenge";
#address-cells = <1>;
#size-cells = <1>;
interrupt-parent = <&intc>;
cpus {
// ...
};
serial@101f0000 {
// ...
};
ethernet0:ethernet@101f1000 {
// ...
};
external-bus {
#address-cells = <2>;
#size-cells = <1>;
ranges = ...
ethernet@0,0 {
// ...
};
};
};
可以使用:
aliases {
// 直接使用节点名赋予别名
serial0 = &serial@101f0000;
// 使用label赋予别名
eth0 = ethernet0
// 使用路径赋予别名
eth1 = "/external-bus/ethernet@0,0"
};
上述使用了三种不同的方式为节点名赋予一个新的短别名。Linux推荐使用此方式为长设备名分配短别名。
chosen节点用于在固件和操作系统之间传递数据(uboot传递给kernel),该节点通常用于传递启动参数。
该节点不代表硬件。
该节点必须是根节点的子节点。
例如:
chosen {
bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200";
};
到目前位置,已经完成了基础硬件外设的设备树编写,接下来讨论一些高级特性。
本章节需要了解PCI的基础知识。
编译设备树:
# 编译单个设备树
dtc -I dts -O dtb -o xxx.dtb xxx.dts
# 编译 arch/${arch}/boot/dts 下所有设备树,不过一些设备树的编译需要设置环境变量
# 因此通常采用上方的单一编译进行测试
make dtbs
反编译设备树:
dtc -I dtb -O dts -o xxx.dts xxx.dtb
一些在其他章节中未详细陈述的部分可能会在本章节中进行补充。