1. 简介
-
概念:设备树(Device Tree),将这个词分开就是“设备”和“树”,描述设备树的文件叫做 DTS(Device Tree Source),这个 DTS 文件采用树形结构描述板级设备,也就是开发板上的设备信息,比如 CPU 数量、 内存基地址、IIC 接口上接了哪些设备、SPI 接口上接了哪些设备等等。如下图所示 -
树的主干就是系统总线,IIC 控制器、GPIO 控制器、SPI 控制器等都是接 到系统主线上的分支。IIC 控制器有分为 IIC1 和 IIC2 两种,其中 IIC1 上接了 FT5206 和 AT24C02 这两个 IIC 设备,IIC2 上只接了 MPU6050 这个设备。DTS 文件的主要功能就是按图所示的结构来描述板子上的设备信息。 -
DTS 是为 Linux 提供一种硬件信息的描述方法,以此代替源码中的 硬件编码 (hard code)。DTS 即 Device Tree Source 设备树源码, Device Tree 是一种描述硬件的数据结构,起源于 OpenFirmware (OF). 在 Linux 2.6 中, ARM 架构的板级硬件细节过多的被硬编码在 arch/arm/plat-xxx 和 arch/arm/mach-xxx (比如板上的 platform 设备,resource, i2c_board_info, spi_board_info 以及各种硬件的 platform_data), 这些板级细节代码对内核来讲只不过是垃圾代码。而采用 Device Tree 后, 许多硬件的细节可以直接透过它传递给 Linux,而不再需要在 kernel 中 进行大量的冗余编码。 -
DTS设备树描述文件中什么代表总线,什么代表设备
- 设备:一个含有compatible属性的节点就是一个设备
- 总线:包含一组设备节点的父节点即为总线
1.1 引入DTS的原因
- 对ARM平台的相关code做出如下相关规范调整,这个也正是引入DTS的原因:
- ARM的核心代码仍然保存在arch/arm目录下
- ARM SoC core architecture code保存在arch/arm目录下
- ARM SOC的周边外设模块的驱动保存在drivers目录下
- ARM SOC的特定代码在arch/arm/mach-xxx目录下
- ARM SOC board specific的代码被移除,由DeviceTree机制来负责传递硬件拓扑和硬件资源信息。
- 本质上,Device Tree改变了原来用hardcode方式将HW 配置信息嵌入到内核代码的方法,改用bootloader传递一个DB的形式。
- 如果我们认为kernel是一个black box,那么其输入参数应该包括:
- 识别platform的信息
- runtime的配置参数
- 设备的拓扑结构以及特性
- 对于嵌入式系统,在系统启动阶段,bootloader会加载内核并将控制权转交给内核,此外, 还需要把上述的三个参数信息传递给kernel,以便kernel可以有较大的灵活性。在linux kernel中,Device Tree的设计目标就是如此。
1.2 文件格式
缩写 | 描述 |
---|
DTS(.dts) | 设备树源文件(描述板级信息:开发板上有哪些 IIC 设备、SPI 设备等) | DTS(.dtb) | 设备树编译文件 | .dtsi | 设备树头文件(描述SOC级信息:CPU 架构、主频、外设寄存器地址范围等) | DTC | 将.dts 编译为.dtb:DTC的源代码位于内核的scripts/dtc目录,在Linux内核使能了Device Tree的情况下, 编译内核的时候主机工具dtc会被编译出来,对应scripts/dtc/Makefile中的“hostprogs-y := dtc”这一hostprogs编译target。 在Linux内核的arch/arm/boot/dts/Makefile中,描述了当某种SoC被选中后,哪些.dtb文件会被编译出来 |
- 设备树相关文件均在 arch/arm/boot/dts/ 文件夹
- DTC 工具源码在 Linux 内核的 scripts/dtc 目录下
- DTC 工具依赖于 dtc.c、flattree.c、fstree.c 等文件,最终编译并链接出 DTC 这个主机文件
- 在 arch/arm/boot/dts/Makefile 中新增需要编译的DTS文件。
- 如果要编译 DTS 文件的话只需要进入到 Linux 源码根目录下,然后执行如下命令:
- make all:编译 Linux 源码中的所有东西,包括 zImage,.ko 驱动模块以及设备 树
- make dtbs:仅编译设备树
2. DTS语法
符号 | 描述 |
---|
/ | 根节点 | @ | 如果设备有地址,则由此符号指定 | & | 引用节点 | : | 冒号前的label是为了方便引用给节点起的别名,此label一般使用为&label | , | 属性名称中可以包含逗号。如compatible属性的名字 组成方式为”[manufacturer], [model]”,加入厂商名是为了避免重名。自定义属性名中通常也要有厂商名,并以逗号分隔。 | # | #并不表示注释。如 #address-cells ,#size-cells 用来决定reg属性的格式 | | 空属性并不一定表示没有赋值。如 interrupt-controller 一个空属性用来声明这个node接收中断信号数据类型 | ”” | 引号中的为字符串,字符串数组:”strint1”,”string2”,”string3” | < > | 尖括号中的为32位整形数字,整形数组<12 3 4> | [ ] | 方括号中的为32位十六进制数,十六机制数据[0x11 0x12 0x13] 其中0x可省略 |
2.1 .dtsi头文件
- 在 .dts 设备树文件中,可以通过“#include ”来引用 .h 、 .dtsi 和 .dts 文件。
- .dtsi 文件用于描述 SOC 的内部外设信息,比如 CPU 架构、主频、外设寄存器地址范围,比如 UART 、 IIC 等等。
2.2 设备节点
- 设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点,每个节点都通过一些属性信息来描述节点信息,属性就是 键值对。每个节点都有不同属性,不同的属性又有不同的内容,属性都是键值对,值可以为空或任意的字节流。
- .dts举例说明
/ {
aliases {
can0 = &flexcan1;
};
cpus {
cpu0: cpu@0 {
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <0>;
};
};
intc: interrupt-controller@00a01000 {
compatible = "arm,cortex-a7-gic";
interrupt-controller;
reg = <0x00a01000 0x1000>,
<0x00a02000 0x100>;
};
}
2.2.1 设备树中节点命名格式
label: node-name@unit-address ???
// e.g.:
cpu0:cpu@0
2.2.2 设备树源码中常用的几种数据类型
1) 字符串
compatible ="arm,cortex-a7";
上述代码设置 compatible 属性的值为字符串“arm,cortex-a7”。
2) 32 位无符号整数
reg =<0>;
上述代码设置 reg 属性的值为 0,reg 的值也可以设置为一组值,比如: reg =<0 0x123456 100>;
3) 字符串列表
属性值也可以为字符串列表,字符串和字符串之间采用“,”隔开,如下所示:
compatible ="fsl,imx6ull-gpmi-nand", "fsl, imx6ul-gpmi-nand";
上述代码设置属性 compatible 的值为“fsl,imx6ull-gpmi-nand”和“fsl, imx6ul-gpmi-nand”。
2.2.3 标准属性
- 节点是由一堆属性组成,节点都是具体的设备,不同的设备需要的属性不同,用户可以自定义属性。除了用户自定义属性,有很多属性是标准属性,Linux 下的很多外设驱动都会使用这些标准属性
属性名 | 描述 |
---|
compatible | compatible 属性也叫做“兼容性”属性。 用于将设备和驱动绑定起来 值是一个字符串列表,用于选择设备所要使用的驱动程序 | model | 值是字符串,用于描述设备模块信息,例如名字 | status | 值是字符串,设备的状态信息 | reg | reg意为region,区域。值一般是(address,length)对。 用于描述设备地址空间资源信息,指定某个外设的寄存器地址范围信息 格式为reg = <address1 length1 [address2 length2] [address3 length3]> | #address-cells #size-cells | 值是无符号 32 位整数。可以用在任何拥有子节点的设备中,用于描述子节点的地址信息 #address-cells 属性值决定了子节点 reg 属性中地址信息所占用的字长(32 位 ),即决定了address1/2/3分别包含几个cell #size-cells 属性值决定了子节点 reg 属性中长度信息所占的 字长 (32 位 ),即决定了length1/2/3分别包含了几个cell | ranges | ranges 是一个地址映射/ 转换表, ranges 属性每个项目由子地址、父地址和地址空间长度 这三部分组成 如果 ranges 属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换 | name | 值是字符串,name 属性用于记录节点名字。 name 属性已经被弃用,不推荐使用 name 属性,一些老的设备树文件可能会使用此属性 | device_type | 值是字符串,IEEE 1275 会用到此属性,用于描述设备的 FCode ,但是设 备树没有 FCode ,所以此属性也被抛弃了。此属性只能用于 cpu 节点或者 memory 节点 | interrupt-controller | 一个空属性用来声明这个node接收中断信号 | #interrupt-cells | 这是中断控制器节点的属性,用来标识这个控制器需要几个单位做中断描述符 | interrupt-parent | 标识此设备节点属于哪一个中断控制器,如果没有设置这个属性,会自动依附父节点的 | interrupts | 一个中断标识符列表,表示每一个中断输出信号 |
2.3 DTS的加载过程
- 如果要使用Device Tree,首先用户要了解自己的硬件配置和系统运行参数,并把这些信息组织成Device Tree source file。 通过DTC(Device Tree Compiler),可以将这些适合人类阅读的Device Tree source file变成适合机器处理的 Device Tree binary file(有一个更好听的名字,DTB,device tree blob)。在系统启动的时候,boot program (例如:firmware、bootloader)可以将保存在flash中的DTB copy到内存(当然也可以通过其他方式, 例如可以通过bootloader的交互式命令加载DTB,或者firmware可以探测到device的信息,组织成DTB保存在内存中), 并把DTB的起始地址传递给client program(例如OS kernel,bootloader或者其他特殊功能的程序)。 对于计算机系统(computer system),一般是firmware->bootloader->OS,对于嵌入式系统,一般是bootloader->OS。
2.4 DTS的描述信息
-
设备树的组成:节点+属性
- Device Tree由一系列被命名的结点(node)和属性(property)组成,而结点本身可包含子结点。所谓属性, 其实就是成对出现的name和value。在Device Tree中,可描述的信息包括(原先这些信息大多被hard code到kernel中):
- CPU的数量和类别
- 内存基地址和大小
- 总线和桥
- 外设连接
- 中断控制器和中断使用情况
- GPIO控制器和GPIO使用情况
- Clock控制器和Clock使用情况
-
CPU、总线和设备组成的树
- 它基本上就是画一棵电路板上CPU、总线、设备组成的树,Bootloader会将这棵树传递给内核,然后内核可以识别这棵树, 并根据它展开出Linux内核中的platform_device、i2c_client、spi_device等设备,而这些设备用到的内存、IRQ等资源, 也被传递给了内核,内核会将这些资源绑定给展开的相应的设备。
-
只描述无法动态探测到的设备
- 是否Device Tree要描述系统中的所有硬件信息?答案是否定的。基本上,那些可以动态探测到的设备是不需要描述的, 例如USB device。不过对于SOC上的usb hostcontroller,它是无法动态识别的,需要在device tree中描述。同样的道理, 在computersystem中,PCI device可以被动态探测到,不需要在device tree中描述,但是PCI bridge如果不能被探测,那么就需要描述之。
-
.dtsi包含公共部分
- .dts文件是一种ASCII 文本格式的Device Tree描述,此文本格式非常人性化,适合人类的阅读习惯。
- 基本上,在ARM Linux中,一个.dts文件对应一个ARM的machine,一般放置在内核的arch/arm/boot/dts/目录。
- 由于一个SoC可能对应多个machine(一个SoC可以对应多个产品和电路板),势必这些.dts文件需包含许多共同的部分, Linux内核为了简化,把SoC公用的部分或者多个machine共同的部分一般提炼为.dtsi,类似于C语言的头文件。 其他的machine对应的.dts就include这个.dtsi。
- 例如,对于RK3288而言, rk3288.dtsi就被rk3288-chrome.dts所引用, rk3288-chrome.dts有如下一行:#include“rk3288.dtsi”, 对于rtd1195,在 rtd-119x-nas.dts中就包含了/include/ ”rtd-119x.dtsi” 当然,和C语言的头文件类似,.dtsi也可以include其他的.dtsi,譬如几乎所有的ARM SoC的.dtsi都引用了skeleton.dtsi,即#include”skeleton.dtsi“ 或者 /include/ “skeleton.dtsi”
-
DTS合并
- 正常情况下所有的dts文件以及dtsi文件都含有一个根节点”/”,这样include之后就会造成有很多个根节点? 按理说 device tree既然是一个树,那么其只能有一个根节点,所有其他的节点都是派生于根节点的child node. 其实Device Tree Compiler会对DTS的node进行合并,最终生成的DTB中只有一个 root node.
-
节点
- device tree的基本单元是node。这些node被组织成树状结构,除了root node,每个node都只有一个parent。 一个device tree文件中只能有一个root node。每个node中包含了若干的property/value来描述该node的一些特性。
- 每个node用节点名字(node name)标识,节点名字的格式是node-name@unit-address。如果该node没有reg属性(后面会描述这个property), 那么该节点名字中必须不能包括@和unit-address。unit-address的具体格式是和设备挂在那个bus上相关。例如对于cpu, 其unit-address就是从0开始编址,以此加一。而具体的设备,例如以太网控制器,其unit-address就是寄存器地址。root node的node name是确定的,必须是“/”。 在一个树状结构的device tree中,如何引用一个node呢?要想唯一指定一个node必须使用full path,例如/node-name-1/node-name-2/node-name-N。
2.3 DTS举例
- 1个双核ARM Cortex-A9 32位处理器;
- ARM的local bus:上的内存映射区域分布了2个串口(分别位于0x101F1000 和 0x101F2000)、 GPIO控制器(位于0x101F3000)、SPI控制器(位于0x10115000)、中断控制器(位于0x10140000)和一个external bus桥;
- External bus:桥上又连接了SMC SMC91111 Ethernet(位于0x10100000)、I2C控制器(位于0x10160000)、64MB NOR Flash(位于0x30000000);
- External bus桥上连接的I2C控制器所对应的I2C总线上又连接了Maxim DS1338实时钟(I2C地址为0x58)
- 其对应的.dts文件为:
/ {
compatible = "acme,coyotes-revenge";
interrupt-parent = <&intc>;
cpus {
cpu@0 {
compatible = "arm,cortex-a9";
reg = <0>;
};
cpu@1 {
compatible = "arm,cortex-a9";
reg = <1>;
};
};
serial@101f1000 {
compatible = "arm,pl011";
reg = <0x101f1000 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;
};
spi@10115000 {
compatible = "arm,pl022";
reg = <0x10115000 0x1000 >;
interrupts = < 4 0 >;
};
external-bus {
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>; // 片选、偏移量、地址长度 (前2个cell为地址,后一个cell为长度)
interrupts = < 5 2 >;
};
i2c@1,0 {
compatible = "acme,a1234-i2c-bus";
reg = <1 0 0x1000>; // 片选、偏移量、地址长度
rtc@58 {
compatible = "maxim,ds1338";
reg = <58>;
interrupts = < 7 3 >;
};
};
flash@2,0 {
compatible = "samsung,k8f1315ebm", "cfi-flash";
reg = <2 0 0x4000000>; // 片选、偏移量、地址长度
};
};
};
...
external-bus{
ranges = <0 0 0x10100000 0x10000 // Chipselect 1,Ethernet
1 0 0x10160000 0x10000 // Chipselect 2, i2c controller
2 0 0x30000000 0x1000000>; // Chipselect 3, NOR Flash
};
-
节点名
- node的名字自己可以随便定义,当然最好是见名知意,可以通过驱动程序打印当前使用的设备树节点 printk(“now dts node name is %s\n”,pdev->dev.of_node->name);
-
compatible
- compatible选项是用来和驱动程序中的of_match_table指针所指向的of_device_id结构里的compatible字段匹配的,只有dts里的compatible字段的名字和驱动程序中of_device_id里的compatible字段的名字一样,驱动程序才能进入probe函数
3. 在系统中查看设备树
- Linux 内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的 /proc/device-tree 目录下根据节点名字创建不同文件夹和文件,/proc/device-tree 目录就是设备树在根文件系统中的体现。
cd /proc/device-tree
ls
- 特殊节点
- aliases 子节点:主要功能就是定义别名,定义别名的目的就是为了方便访问节点
- chosen 子节点:主要是为了 uboot 向 Linux 内核传递数据,重点是 bootargs 参数。bootargs 环境变量的值是在uboot 中设置的,而 uboot 中的 fdt_chosen 函数在设备树的 chosen 节点中加入了 bootargs 属性,并且还设置了 bootargs 属性值。
4. Linux内核解析DTB文件流程
- Linux 内核在启动的时候会解析 DTB 文件,然后在/proc/device-tree 目录下生成相应的设备树节点文件
5. 绑定信息文档
- Linux 内核源码中有详细的 .txt 文档描述了如何添加节点,这些 .txt 文档叫做绑定文档。
- 路径在Linux 源码目录: /Documentation/devicetree/bindings
6. 设备树常用操作函数
- Linux 内核给我们提供了一系列函数来获取设备树中的节点或者属性信息,这一系列函数都有统一的前缀“of_ ”,也叫做 OF 函数。OF 函数原型都定义在 include/linux/of.h 文件。
6.1 查找节点的 OF 函数
- 设备都是以节点的形式“挂”到设备树上的,因此要想获取这个设备的其他属性信息,必须先获取到这个设备的节点。Linux 内核使用 device_node 结构体来描述一个节点,此结构体定义在文件 include/linux/of.h 中,且of函数的实现位于kernel/drivers/of目录。
方法 | 描述 |
---|
通过节点名字 查找指定的节点 | struct device_node * of_find_node_by_name (struct device_node *from, const char *name) from :开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。 name :要查找的节点名字。 返回值 :找到的节点,如果为 NULL 表示查找失败。 | 通过 device_type 属性 查找指定的节点 | struct device_node * of_find_node_by_type (struct device_node *from, const char *type) from :开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。 type :要查找的节点对应的 type 字符串,也就是 device_type 属性值。 返回值 :找到的节点,如果为 NULL 表示查找失败。 | 根据 device_type 和 compatible 两个属性查找指定的节点 | struct device_node * of_find_compatible_node (struct device_node *from, const char *type, const char *compatible) from :开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。 type :要查找的节点对应的 device_type 属性值,可以为 NULL ,表示忽略 device_type 属性。 compatible : 要查找的节点所对应的 compatible 属性列表。 返回值 :找到的节点,如果为 NULL 表示查找失败 | 通过 of_device_id 匹配表 查找指定的节点 | struct device_node * of_find_matching_node_and_match (struct device_node *from, const struct of_device_id *matches, const struct of_device_id **match) from :开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。 matches : of_device_id 匹配表,也就是在此匹配表里面查找节点。 match : 找到的匹配的 of_device_id。 返回值 :找到的节点,如果为 NULL 表示查找失败 | 通过路径来查找指定的节点 | inline struct device_node * of_find_node_by_path (const char *path) path :全路径的节点名,可以使用节点的别名,比如“ /backlight ”就是 backlight 这个节点的全路径。 返回值 :找到的节点,如果为 NULL 表示查找失败 |
6.2 查找父/子节点的 OF 函数
方法 | 描述 |
---|
用于获取指定节点的父节点 | struct device_node * of_get_parent (const struct device_node *node) node :要查找的父节点的节点。 返回值 :找到的父节点。 | 用迭代的查找子节点 | struct device_node * of_get_next_child (const struct device_node *node, struct device_node *prev) node :父节点。 prev :前一个子节点,从此开始迭代的查找下一个子节点。NULL,表示从第一个子节点开始。 返回值 :找到的下一个子节点 |
6.3 提取属性值的 OF 函数
方法 | 描述 |
---|
用于查找指定的属性 | property * of_find_property (const struct device_node *np, const char *name, int *lenp) np :设备节点; name : 属性名字; lenp :属性值的字节数; 返回值:找到的属性。 | 用于获取属性中元素的数量 (获取到属性数组的大小) | int of_property_count_elems_of_size (const struct device_node *np, const char *propname,int elem_size) np :设备节点; proname: 属性名; elem_size:元素长度; 返回值 :得到的属性元素数量。 | 用于从属性中获取指定 标号的 u32 类型数据值 | int of_property_read_u32_index (const struct device_node *np, const char *propname, u32 index, u32 *out_value) np :设备节点。 proname : 要读取的属性名字。 index :要读取的值标号。 out_value :读取到的值 返回值 :0 读取成功,负值,读取失败, -EINVAL 表示属性不存在, -ENODATA 表示没有要读取的数据,-EOVERFLOW 表示属性值列表太小。 | 读取属性中 u8 、 u16 、 u32 和 u64 类型的数组数据 | int of_property_read_u8_array (const struct device_node *np, const char *propname, u8 *out_values, size_t sz) of_property_read_u16_array of_property_read_u32_array of_property_read_u64_array np :设备节点。 proname : 要读取的属性名字。 out_value :读取到的数组值,分别为 u8 、 u16 、 u32 和 u64。 sz :要读取的数组元素数量。 | 用于读取属性中字符串值 | int of_property_read_string(struct device_node *np, const char *propname,const char **out_string) np :设备节点。 proname : 要读取的属性名字。 out_string :读取到的字符串值。 返回值 :0:读取成功,负值:读取失败。 | 用于获取 #address-cells 属性值 | int of_n_addr_cells(struct device_node *np) np :设备节点。 返回值 :获取到的 #address-cells 属性值。 | 用于获取 #size-cells 属性值 | int of_n_size_cells (struct device_node *np) np :设备节点。 返回值 :获取到的 #size-cells 属性值。 |
6.4 其它常用OF函数
方法 | 描述 |
---|
用于查看节点的 compatible 属性是否有包含 compat 指定的字符串, 也就是检查设备节点的兼容性 | int of_device_is_compatible (const struct device_node *device, const char *compat) device :设备节点。 compat :要查看的字符串。 返回值 :0 :节点的 compatible 属性中不包含 compat 指定的字符串; 正数:节点的 compatible 属性中包含 compat 指定的字符串。 | 用于获取地址相关属性, 主要是“ reg ”或者 “assigned-addresses”属性值 | const __be32 * of_get_address (struct device_node *dev, int index, u64 *size, unsigned int *flags) dev :设备节点。 index :要读取的地址标号。 size :地址长度。 flags :参数,比如 IORESOURCE_IO 、 IORESOURCE_MEM 等 返回值 :读取到的地址数据首地址,为 NULL 的话表示读取失败。 | 将从设备树读取到的 地址转换为物理地址 | u64 of_translate_address (struct device_node *dev, const __be32 *in_addr) dev :设备节点。 in_addr :要转换的地址。 返回值 :得到的物理地址,如果为 OF_BAD_ADDR 的话表示转换失败。 | 从设备树里面提取资源值, 将 reg 属性值转换为 resource 结构体类型 | int of_address_to_resource(struct device_node *dev, int index, struct resource *r) dev :设备节点。 index :地址资源标号。 r :得到的 resource 类型的资源值。 返回值 :0,成功;负值,失败。 | 将 reg 属性中地址 信息转换为虚拟地址 | void __iomem * of_iomap (struct device_node *np, int index) np :设备节点。 index : reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为 0。 返回值 :经过内存映射后的虚拟内存首地址,如果为 NULL 的话表示内存映射失败。 |
|