前言
在 Linux 启动过程中,U-Boot / UEFI 将一个“设备树 Blob”(DTB)文件加载到内存中,并将指向它的指针传递给内核。此 DTB 文件描述了 Linux 内核的系统硬件布局,允许将特定于平台的代码移出内核源代码并替换为可以解析 DTB 并根据需要配置系统的通用代码。
编译设备树
设备树源 (DTS
) 文件是简单的文本文件,可以使用设备树编译器 (DTC
) 工具将其编译为二进制设备树 Blob (DTB
) 格式。DTC 工具位于/scripts/dtc
下的 Linux 内核源代码中,也可通过一些分发包管理器(例如 Ubuntu 上的 APT)进行安装:
$ sudo apt-get update
$ sudo apt-get install device-tree-compiler
使用 DTC
反编译 DTB
是无损的,即可以将 DTB
反编译为 DTS
,然后将该 DTS
编译回 DTB
而不会丢失任何信息。反编译:
$ dtc -I dtb -O dts juno.dtb > juno.dts
并重新编译:
$ dtc -I dts -O dtb juno.dts > juno.dtb
设备树源 (DTS) 语法
注意:通用设备树语法的完整文档可以在DeviceTree.org找到,Linux 绑定文档可以在这里找到 Linux 内核源代码
设备树源文件是由具有相关属性的节点和子节点组成的树结构。一般语法如下:
/ {
node1 {
a-string-property = "a string";
a-string-list-property = "first string", "second string";
a-byte-data-property = [0x01 0x02 0x03];
child-node1 {
first-child-property;
second-child-property
a-string-property = "child string";
};
child-node2 {
};
};
node2 {
an-empty-property;
a-cell-property = <1 2 3>;
child-node1 {
};
};
};
/
表示树的根节点node1
并且node2
是根节点的子节点node1
有两个子节点:child-node1
和child-node2
每个节点都有一组与之关联的属性:
- 以空字符结尾的文本字符串用双引号括起来:
a-string-property = "一个字符串";
- 单元格(32 位无符号整数)以空格分隔并用尖括号括起来:
细胞属性 = <1 2 3>;
- 二进制数据以空格分隔并用方括号括起来:
二进制属性 = [0x01 0x02 0x03];
- 可以使用逗号连接混合表示数据:
a-mixed-property = "一个字符串", [0x01 0x02 0x03], <1 2 3>;
- 逗号也用于创建字符串列表:
a-string-list = "第一个字符串", "第二个字符串";
DTS 示例
这是 Juno 的 DTS 条目示例,特别是 GIC-400:
interrupt-controller@2c010000 {
compatible = "arm,gic-400";
reg = <0x0 0x2c010000 0x0 0x1000 0x0 0x2c02f000 0x0 0x2000 0x0 0x2c04f000 0x0 0x2000 0x0 0x2c06f000 0x0 0x2000>;
#address-cells = <0x2>;
#interrupt-cells = <0x3>;
#size-cells = <0x2>;
interrupt-controller;
interrupts = <0x1 0x9 0x3f04>;
ranges = <0x0 0x0 0x0 0x2c1c0000 0x0 0x40000>;
linux,phandle = <0x1>;
phandle = <0x1>;
v2m@0 {
compatible = "arm,gic-v2m-frame";
msi-controller;
reg = <0x0 0x0 0x0 0x1000>;
};
};
让我们分解这个条目,以了解如何使用设备树节点来描述真实硬件:
interrupt-controller@2c010000 {
中断控制器位于父节点“总线”上的地址 0x2C010000
(稍后会详细介绍)。在这种情况下,父节点是根节点,因此从 CPU 的角度来看,这意味着地址 0x2C010000
。
compatible = "arm,gic-400";
Linux 内核使用它来决定将哪个设备驱动程序绑定到外围设备。在这种情况下,中断控制器与 ARM GIC-400 驱动程序兼容。
reg = <0x0 0x2c010000 0x0 0x1000 0x0 0x2c02f000 0x0 0x2000 0x0 0x2c04f000 0x0 0x2000 0x0 0x2c06f000 0x0 0x2000>;
reg
属性定义了外围设备将响应的地址范围。该属性由许多元组组成,每个元组包含一个区域的基地址和该区域的大小。要解析属性,我们需要了解三件事:
-
每个单元格值是一个 32 位无符号整数
-
父节点的
#address-cells
值定义了多少个单元格值构成一个区域的基地址 -
父节点的
#size-cells
值定义了多少个单元格值构成一个区域的长度
中断控制器节点是根节点的子节点,在 Juno 上有#address-cells = #size-cells= 2
。按顺序移动该reg
属性并将前 2 个单元格组合为基地址,接下来的 2 个单元格为长度,接下来的 2 个单元格为基地址等给出中断控制器将响应的以下区域:
64-bit address 0x0000_0000_2C01_0000 with 64-bit length 0x0000_0000_0000_1000
64-bit address 0x0000_0000_2C02_F000 with 64-bit length 0x0000_0000_0000_2000
64-bit address 0x0000_0000_2C04_F000 with 64-bit length 0x0000_0000_0000_2000
64-bit address 0x0000_0000_2C06_F000 with 64-bit length 0x0000_0000_0000_2000
interrupt-controller;
所有中断控制器节点都必须包含一个空interrupt-controller
属性。
interrupts = <0x1 0x9 0x3f04>;
GIC 的中断设备树绑定格式可以 在 Linux 内核文档中找到 :
- 第一个单元格表示中断类型(0 表示 SPI,1 表示 PPI)
- 第二个单元格包含许多标志,编码如下:位 [3:0] 定义触发类型和电平标志,其中 4 对应于 Active High Level-Sensitive位 [15:8] 定义 CPU 中断掩码,其中每个位表示中断连接到特定 CPU(位 8 = CPU0,位 9 = CPU1,…,位 15 = CPU7)
所以这里 GIC 本身将 PPI #9 作为针对 CPU [0…5] 的活动高电平敏感中断,即 Juno 上的所有 CPU,总共有 6 个 CPU。此 PPI(中断 ID 25)对应于 Juno 上的 VGIC 维护中断,是支持虚拟化扩展的 CPU 所必需的。
ranges = <0x0 0x0 0x0 0x2c1c0000 0x0 0x40000>;
v2m@0 {
compatible = "arm,gic-v2m-frame";
msi-controller;
reg = <0x0 0x0 0x0 0x1000>;
};
节点中的所有地址都指定为该节点本地的总线地址。range
属性提供了一种将这些总线地址映射到父节点地址空间的机制。
Juno 的中断控制器节点是v2m
地址 0x0
的子节点,位于中断控制器“总线”的本地。该节点用于 GICv2m 扩展,需要从中断控制器“总线”映射到 CPU 的内存角度。
reg与属性类似,ranges
使用#address-cellsand#size-cells
来解析 (Local Address, Parent Address, Length) 的元组,其中:
-
本地地址的宽度由本地节点的
#address-cells
值决定 -
父地址的宽度由父节点的
#address-cells
值决定 -
Length
的宽度由本地节点的#size-cells
值决定
在 Juno 上,这些都是 2
,因此范围属性被解释为“将 64 位本地地址 0x0 映射到 64 位长度为 0x40000
的 64 位父地址 0x2C1C0000
”。因为v2m
节点在本地地址0x0
,所以会映射到父节点的地址0x2C1C0000
,而父节点是根节点,这就是CPU对内存的看法。
查找设备树
您可能需要检查某些 dtb
的值,以进行调试,检查 dtb
在您修改后是否确实得到了更新等。虽然您无法获得已加载的设备树的类似 .dts
文件的视图,但您可以使用 /proc/device-tree
中的条目检查值。
在 Linux 的命令提示符下,您应该会在 /proc/device-tree
目录中看到类似以下内容:
root@j7-evm:~# ls /proc/device-tree/
#address-cells firmware l3-cache0
#size-cells fixedregulator-evm12v0 main_r5fss_cpsw9g_virt_mac0
__symbols__ fixedregulator-sd memory@80000000
aliases fixedregulator-vsys3v3 model
chosen fixedregulator-vsys5v0 name
compatible gpio-keys pmu
connector interconnect@100000 reserved-memory
cpus interrupt-parent serial-number
dma_buf_phys ion sound@0
dummy-panel l2-cache0 timer-cl0-cpu0
假设您不知道要查找的设备树的绝对路径。/proc/device-tree
中有一个 __symbol__
目录。它对设备树中的每个符号标签都有。我们可以通过cat
来找到该符号的确切路径。以下是演示使用的示例:
root@j7-evm:~# cat /proc/device-tree/__symbols__/main_i2c0
/interconnect@100000/i2c@2000000
root@j7-evm:~# ls /proc/device-tree/interconnect@100000/i2c@2000000/
#address-cells clock-names gpio@20 name pinctrl-names
#size-cells clocks gpio@22 phandle power-domains
clock-frequency compatible interrupts pinctrl-0 reg