1、前言
设备树是一种描述硬件平台和设备的数据结构,它以一种结构化的方式描述了系统中的各种设备和资源,包括处理器、内存、外设和总线等。设备树通常用于嵌入式系统和嵌入式 Linux 系统中,它可以帮助操作系统内核在启动时自动识别硬件,并正确配置驱动程序和资源。
使用设备树的主要原因是为了解决硬件平台的多样性和复杂性。在嵌入式系统中,不同的硬件平台可能有不同的设备和资源配置,使用设备树可以使操作系统内核在不同的硬件平台上运行,而无需修改内核代码。比如在之前讲解的platform驱动模型中,device实现的设备节点的数据内容可以通过设备树节点转换得到,这样可以大大简化软件开发和维护工作,同时也提高了代码的可移植性和可重用性。
设备树官网:https://www.devicetree.org
2、DTS文件
2.1 文件布局
官方文档中提供的文件布局示例如下,其中“/dts-v1/;”表示使用版本;“[memory reservations]”为可选设置,表示设置预留内存;“/”表示根节点;“[property definitions]”表示属性定义;“[child nodes]”表示子节点:
/dts-v1/;
[memory reservations]
/ {
[property definitions]
[child nodes]
};
一个简单的示例如下:
/dts-v1/; // 设备树版本声明
/ {
#address-cells = <1>; // 设备地址的单元数量
#size-cells = <1>; // 设备大小的单元数量
compatible = "virtual,machine"; // 设备兼容性描述
memory {
device_type = "memory"; // 设备类型描述
reg = <0x00000000 0x40000000>; // 1GB内存,起始地址为0x00000000
};
cpus {
#address-cells = <1>; // 设备地址的单元数量
#size-cells = <0>; // 设备大小的单元数量
cpu@0 {
compatible = "virtual,cpu"; // 设备兼容性描述
reg = <0>; // 寄存器地址
status = "okay"; // 设备状态描述
};
};
};
2.2 属性格式
属性有两种格式:
[label:] property-name = value;
[label:] property-name;
其中value的取值有三种,以定义为一个由32位整数单元格组成的数组、以空结尾的字符串、字节字符串或它们的组合,以下为示例:
// 32位整数(常用)
assigned-clocks = <0x6>;
reg = <0x0 0x6520000 0x0 0x100 0x0 0x6524000 0x0 0x3fc>;
// 字符串(常用)
compatible = "allwinner,pll-clock";
lock-mode = "new";
// 字节序列(少见) 每个字节必须使用两个16进制数字表示 比如1要写成01 中间空格可省略
flash0_flvdd = [0011];
// 组合方式(少见)
pinctrl = "active", "sleep", <0x1>;
2.3 节点格式
对象节点用节点名称和单位地址定义,用大括号标记节点定义的开始和结束,在它们之前可能会有一个标签(非必选),其一般格式如下:
[label:] node-name[@unit-address] {
[properties definitions]
[child nodes]
};
节点可以包含属性定义和子节点定义,如果两者都存在,属性应该放在子节点之前。
3、实例分析
3.1 常见属性
(1)compatible
根节点下兼容的属性值由一个或多个为设备定义特定编程模型的字符串组成。此字符串列表应该被客户端程序用于选择设备驱动程序,香橙派的设备树文件中指定如下,一个操作系统系统将首先尝试找到一个支持allwinner,h616的设备驱动程序,如果没有的话会即继续往后查找"arm,sun50iw9p1"。
compatible = "allwinner,h616", "arm,sun50iw9p1";
(2)model
根节点下模型属性值是一个<字符串>,它指定了设备的制造商型号。推荐的格式是:“制造商,型号”,其中制造商是描述制造商名称的字符串,型号指定型号,作用和compatible类似:
model = "Orange Pi Zero 2";
(3)phandle
phandle属性为设备树中唯一的节点指定一个数字标识符。phandle属性值用于需要引用与该属性关联的节点的其他节点。其使用示例如下:
pic@10000000 {
phandle = <1>;
interrupt-controller;
reg = <0x10000000 0x100>;
};
// 引用上述节点
another-device-node {
interrupt-parent = <1>;
};
(4)status
状态属性表示设备的运行状态,一般取值如下:
"okay"
| 指示设备可运行 |
"disabled"
| 表示设备目前未运行,但可能在将来运行(例如,没有插入或关闭) |
"reserved"
| 表示该设备已运行,但不应使用,通常用于由另一个软件组件控制的设备,如平台固件 |
"fail"
| 表示该设备无法运行。在设备中检测到严重的错误,不能运行 |
"fail-sss"
| 表示该设备无法运行。在设备中检测到严重的错误,不能运行,该值的sss部分是特定于该设备的,并指示所检测到的错误条件 |
(5)#address-cells 和#size-cells
address-cells和size-cells表示的是元素所处的当前节点的子节点中表示内存地址和大小锁占用多少个32位的数据。示例如下,soc节点的#地址单元格和#大小单元格属性都被设置为1。此设置指定需要一个32位数据来表示一个地址,并且需要一个32位数据来表示区间的大小,在serial设备中寄存器起始地址为0x4600,大小为0x100:
soc {
#address-cells = <1>;
#size-cells = <1>;
serial@4600 {
compatible = "ns16550";
reg = <0x4600 0x100>;
clock-frequency = <0>;
interrupts = <0xA 0x8>;
interrupt-parent = <&ipic>;
};
};
使用两个32位数据表示的示例如下,表示的起始地址和大小分别是0x3001000,0x1000;0x7010000,0x400;0x7000000,0x4:
/ {
interrupt-parent = <0x1>;
#address-cells = <0x2>;
#size-cells = <0x2>;
model = "Orange Pi Zero 2";
compatible = "allwinner,h616", "arm,sun50iw9p1";
clocks {
compatible = "allwinner,clk-init";
device_type = "clocks";
#address-cells = <0x2>;
#size-cells = <0x2>;
ranges;
reg = <0x0 0x3001000 0x0 0x1000 0x0 0x7010000 0x0 0x400 0x0 0x7000000 0x0 0x4>;
};
}
(6)reg
reg属性描述了设备在其父总线定义的地址空间内的资源的地址。最常见的意思是内存映射的IO寄存器块的偏移量和长度,但在某些总线类型上可能有不同的含义。由根节点定义的地址空间中的地址为CPU真实地址。示例可以参考上面的节点。
(7)ranges
用于描述设备地址映射,通常用于指定设备地址在系统地址空间中的映射关系,示例如下:
uart0: serial@11000 {
compatible = "xxxxxx";
reg = <0x13000 0x100>; // 物理地址
ranges = <0x0 0x13000 0x0 0x100>; // 映射地址
};
(8)name
用于指定设备节点的名称,通常用于标识设备节点的类型或用途,示例如下:
uart0: serial@11000 {
compatible = "xxxxxx";
reg = <0x13000 0x100>;
name = "serialport";
};
(9)device_type
用于指定设备节点的类型,通常用于区分设备节点的功能或类别,示例如下:
uart0: serial@11000 {
compatible = "xxxxxx";
reg = <0x13000 0x100>;
device_type = "serial";
};
3.2 常见节点
(1)aliases node
aliases 节点用于定义设备的别名,可以将设备的逻辑名称映射到实际的设备节点。这样可以在设备树中使用逻辑名称来引用设备,而不必关心实际的物理地址。
aliases {
serial0 = "/simple-bus@fe000000/serial@llc500";
ethernet0 = "/simple-bus@fe000000/ethernet@31c000";
};
(2)memory node
节点用于描述系统的内存布局,包括内存的起始地址和大小。
memory@0 {
device_type = "memory";
reg = <0x000000000 0x00000000 0x00000000 0x80000000>;
};
memory@100000000 {
device_type = "memory";
reg = <0x000000001 0x00000000 0x00000001 0x00000000>;
};
(3)reserved-memory node
reserved-memory节点用于描述系统中保留的内存区域,这些区域可能被用于设备 DMA 缓冲区等。
reserved-memory {
#address-cells = <2>;
#size-cells = <2>;
ranges;
dma_reserved: buffer@90000000 {
reg = <0x90000000 0x00400000>; // 描述一块保留给设备 DMA 缓冲区的内存,起始地址是 0x90000000,大小为 4MB
};
};
(4)chosen node
chosen节点用于存放一些全局的配置信息,比如引导参数等。
chosen {
bootargs = "earlyprintk=sunxi-uart,0x05000000 loglevel=8 initcall_debug=1 console=ttyS0 init=/init";
linux,initrd-start = <0x0 0x0>;
linux,initrd-end = <0x0 0x0>;
};
(5)cpus Node
cpus 节点用于描述系统中的 CPU,而 /cpus /cpus *
节点属性用于描述每个 CPU 的相关信息,比如 CPU 的寄存器配置、中断控制器等。
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
compatible = "arm,cortex-a53";
reg = <0>;
// 其他 CPU 相关属性
};
cpu@1 {
compatible = "arm,cortex-a53";
reg = <1>;
// 其他 CPU 相关属性
};
};
4、dts文件使用
dts文件会被编译成dtb文件,编译过程如图所示,通过编译工具dtc实现:
dtb文件经过一系列处理转换为节点供内核使用,图示为大致的处理流程:
- 在引导加载阶段,Bootloader 会将设备树的地址传递给内核
- 内核在启动过程中会对加载的设备树进行解析,将设备树节点转换为内核中的数据结构,如device_node。
- 在设备初始化阶段,内核会根据解析后的设备树信息创建对应的设备结构,如platform_device。
- 在内核启动后,可以通过加载设备树的修正补丁来动态修改设备树的内容。
5、总结
文本讲解了dts文件的一般格式,简单阐述了dtb文件的解析使用流程。
文章部分内容引用官方文档《Devicetree Specification》Release v0.4,文档可在官网获取