转自:https://blog.csdn.net/Luckiers/article/details/124772722
一、简介
设备树是在PowerPC平台最先使用,后来2011年3月份Linux创始人Linus Torvalds在邮件建议ARM社区也使用设备树的方式去描述板级结构。所以设备树其实就是描述开发板上的硬件信息,由于其结构就像现实世界的大树一样,所以就将这种结构叫设备树,如下图所示。
二、设备树基础内容
2.1 设备树文件存放路径
dts源码都在对应的架构目录下,如arm平台arch/arm/boot/dts,对应的文件主要有1个dts文件+n个dtsi文件,它们编译而成的dtb文件就是真正的设备树。
soc厂商会把soc公共的特性和多块开发板公用的特性提炼为dtsi,而dts则负责描述某个具体的产品(开发板)的特性。dts直接或间接的包含多个dtsi(类似于c语言的头文件),就体现了一个完整的产品(开发板)所有的特性。以solidrun公司的hummingboard为例,其组成为
imx6dl-hummingboard.dts
|_imx6dl.dtsi
| |_imx6qdl.dtsi
|_imx6qdl-microsom.dtsi
|_imx6qdl-microsom-ar8035.dtsi
2.2 DTS、DTB和DTC关系
DTS(devcie tree source):设备树源码文件
DTB(device tree binary):将 .dts 编译后得到二进制文件,下载到 DDR 中的是 .dtb 文件
DTC(device tree compiler):将 .dts 编译为 .dtb 的编译工具,它有个文件夹,经过编译后得到 DTC
2.3 传统驱动代码和使用设备树的对比
传统方式:
(1)代码冗余
在没有使用设备树的时候,有关板级的硬件信息都被硬编码在内核中,这就导致了内核中描述板级硬件的代码过于庞大,不利于阅读。以arm架构为例,通常这些硬件信息会放在/arch/arm/mach-xxx和/arch/arm/plat-xxx里面。
(2)修改麻烦
在内核中有一种叫总线的模型,这种模型的作用是将设备信息与驱动进行分离,在没有使用设备树的情况下,这里的设备信息就被硬编码到/arch/arm/mach-xxx和/arch/arm/plat-xxx中。这就会导致一种不好的结果,当你修改了某个设备的信息时,你就需要重新编译内核并把内核烧到系统中。
设备树的优点:
在使用设备树后,内核中就不再需要那些硬件信息了,描述硬件的信息都会被统一放到/arch/arm/boot/dts的设备树文件中,这样就可以减少代码的冗余。
当你需要修改设备的硬件信息时,只需要在修改设备树之后,对设备树进行编译并放到系统中,uboot就会将设备树的地址告诉Linux内核,让Linux内核到相应的内存地址去解析设备树信息,不再需要重新编译内核。
三、设备树内容属性介绍
3.1 节点名称
node-name@unit-address //node-name:节点名字 unit-address:表示寄存器基地址或设备地址,如下serial@101f0000
label:node-name@unit-address // 引入label目的就是为了方便便访问节点,可以直接通过&label来访问这个
serial@101f0000 {
compatible = "arm,pl011";
reg = <0x101f0000 0x1000 >;
interrupts = < 1 0 >;
3.2 compatible
compatible 属性值为字符串列表,⽤于将设备和驱动绑定起来,字符串列表⽤于选择设备所要使用的驱动程序。
"manufacturer,model" //manufacturer :厂商 model:模块对应的驱动名
一般驱动程序文件中都会有一个 OF 匹配表,此 OF 匹配表保存着一些 compatible 值,如果设备节点的 compatible 属性值和 OF 匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动。
3.3 model 属性
model 属性:描述设备模块信息,比如名字什么的,如:model = “wm8960-audio”。
3.4 status 属性
status 属性:描述设备状态,如:okay – 设备可操作,disabled – 设备不可操作
3.5 #address-cells 和 #size-cells 属性
#address-cells 和 #size-cells 描述⼦节点应如何编写 reg 属性值,一般 reg 属性是某个外设的寄存器地址范围信息。
3.6 ranges 属性
ranges它是一个地址映射/转换表,如果 ranges 属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换。
3.7 aliases 节点
用 aliases 节点给多个同类型的控制器分配唯一编号,便于Linux内核区分。在Linux启动时会解析aliases节点。
3.8 chosen 节点
chosen 并不是一个真实的设备,主要用于将 uboot 中的 bootargs 环境变量值传递给 Linux 内核作为命令行参数。
四、设备树文件内容示例解析
/ {
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>;
};
};
};
4.1 设备树关键内容解析
(1)address、length和#address-cells 、#size-cells的关系
reg的组织形式如reg=<地址1 长度1 [地址2 长度2] [地址3 长度3]…>,每个元组都表示该设备使用的一个地址范围。每个地址值是一个或者多个32整数列表,成为一个cell。由于地址和长度字段都是可变大小的变量,那么父节点的#adderss-cells和#size-cells属性就用来声明各个字段的cell数量。换句话说,正确完整的解释reg属性需要用到父节点的#address-cells和#size-cells的值。
在本例中,root结点的#address-cells = <1>;和#size-cells = <1>;决定了serial、gpio、spi等结点的address和length字段的长度分别为1,如: reg = <0x10115000 0x1000 >;
cpus 结点的#address-cells = <1>;和#size-cells = <0>;决定了2个cpu子结点的address为1,而length为空,于是形成了2个cpu的reg = <0>;和reg = <1>;
external-bus结点的#address-cells = <2>和#size-cells = <1>;决定了其下的ethernet、i2c、flash的reg字段形如:
reg = <0 0 0x1000>;
reg = <1 0 0x1000>;
reg = <2 0 0x4000000>;
其中,address字段长度为2,开始的第一列cell(0、1、2)是对应的片选,第2列cell(0,0,0)是相对该片选的基地址,第3列cell(0x1000、0x1000、0x4000000)为length。
特别要留意的是i2c结点中定义的 #address-cells = <1>;和#size-cells = <0>;又作用到了I2C总线上连接的RTC,它的address字段为0x58,是设备的I2C地址。
(2)中断号和电平触发方式
interrupts = < 6 2 >; //中断号为6,下降沿触发
#电平触发方式定义
1 = low-to-high edge triggered
2 = high-to-low edge triggered
4 = active high level-sensitive
8 = active low level-sensitive
(3)驱动获取设备树内容的常用函数
设备树描述了设备的详细信息,我们在编写驱动时需要获取到这些信息,Linux内核给我们提供了一系列的函数来获取设备树中的节点或者属性信息,内核启动时会解析.dtb文件,从而获取设备树中各个节点的信息,并且在根文件系统的/proc/device-tree目录下根据节点名字创建不同文件夹。
查找节点:of_find_node_by_path 函数,通过指定全路径来查找指定节点。
提取属性值:of_find_property 函数 ,获取到的值保存到了 property 结构体中。
读取属性中字符串值:of_property_read_string 函数。
读取属性中数组数据:of_property_read_u32_array 函数,常用于一次读取出 reg 属性中的所有数据。
直接内存映射:of_iomap 函数,获取内存地址所对应的虚拟地址
(4)绑定信息文档查询
我们往往不知道如何在设备树中添加一个硬件对应的节点,此时我们可以参考Linux内核源码中有详细的.txt文档描述了如何添加节点,这些.txt文档叫做绑定文档。
路径:/Documentation/devicetree/bindings