8. Linux设备树

Linux3.x以后的版本才引入了设备树,设备树用于描述一个硬件平台的板级细节。 在早些的linux内核,这些“硬件平台的板级细节”保存在linux内核目录“/arch”, 以ARM为例“硬件平台的板级细节”保存在“/arch/arm/plat-xxx”和“/arch/arm/mach-xxx”目录下。 随着处理器数量的增多用于描述“硬件平台板级细节”的文件越来越多导致Linux内核非常臃肿, Linux之父发现这个问题之后决定使用设备树解决这个问题。设备树简单、易用、可重用性强, linux3.x之后大多采用设备树编写驱动。

关于设备树的详细请参考:https://www.devicetree.org/

8.1. 设备树简介

设备树(Device Tree)的作用就是描述一个硬件平台的硬件资源,一般描述那些不能动态探测到的设备,可以被动态探测到的设备是不需要描述。设备树可以被bootloader(uboot)传递到内核, 内核可以从设备树中获取硬件信息。

设备树

设备树描述硬件资源时有两个特点。

  • 第一,以“树状”结构描述硬件资源。例如本地总线为树的“主干”在设备树里面称为“根节点”, 挂载到本地总线的IIC总线、SPI总线、UART总线为树的“枝干”在设备树里称为“根节点的子节点”, IIC 总线下的IIC设备不止一个,这些“枝干”又可以再分,除了根节点没有父节点外,其他节点都只有一个父节点。

  • 第二,设备树源文件可以像头文件(.h文件)那样,一个设备树文件引用另外一个设备树文件, 这样可以实现“代码”的重用。例如多个硬件平台都使用rk系列处理器作为主控芯片, 那么我们可以将rk系列芯片的硬件资源写到一个单独的设备树文件里面一般使用“.dtsi”后缀, 其他板级设备树文件直接使用“# include xxx.dtsi”引用即可。

DTS、DTC和DTB它们是文档中常见的几个缩写。

  • DTS 是指.dts格式的文件,是一种ASII 文本格式的设备树描述,也是我们要编写的设备树源码,一般一个.dts文件对应一个硬件平台,对应arm架构,源文件位于Linux源码的“/arch/arm/boot/dts”目录下。

  • DTC 是指编译设备树源码的工具,一般情况下我们需要手动安装这个编译工具。

  • DTB 是设备树源码编译生成的文件,类似于我们C语言中“.C”文件编译生成“.bin”文件。

8.2. 设备树框架

设备树(Device Tree)由一系列被命名的结点(node)和属性(property)组成,以lubancat2为例, 不妨打开本章节的配套代码 linux_driver/device_tree/rk3568-lubancat2.dts或者 内核源码/arch/arm64/boot/dts/rockchip/rk3568-lubancat2.dts先睹为快。

下面的内容将围绕着设备树源码,来讲解设备树框架和基本语法。

设备树 (内核源码/arch/arm64/boot/dts/rockchip/rk3568-lubancat2.dts)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/dts-v1/;

#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pwm/pwm.h>
#include <dt-bindings/pinctrl/rockchip.h>
#include <dt-bindings/input/rk-input.h>
#include <dt-bindings/display/drm_mipi_dsi.h>
#include <dt-bindings/sensor-dev.h>
#include "rk3568.dtsi"

/ {
    model = "EmbedFire LubanCat2 HDMI";
    compatible = "embedfire,lubancat2", "rockchip,rk3568";

    chosen: chosen {
        bootargs = "earlycon=uart8250,mmio32,0xfe660000 console=ttyFIQ0 root=PARTUUID=614e0000-0000 rw rootwait";
    };

    fiq-debugger {
        compatible = "rockchip,fiq-debugger";
        rockchip,serial-id = <2>;
        rockchip,wake-irq = <0>;
        /* If enable uart uses irq instead of fiq */
        rockchip,irq-mode-enable = <1>;
        rockchip,baudrate = <1500000>;  /* Only 115200 and 1500000 */
        interrupts = <GIC_SPI 252 IRQ_TYPE_LEVEL_LOW>;
        pinctrl-names = "default";
        pinctrl-0 = <&uart2m0_xfer>;
        status = "okay";
    };
/*-------------内容省略--------------*/
    &saradc {
        vref-supply = <&vcca_1v8>;
        status = "okay";
    };

    &tsadc {
        status = "okay";
    };
/*-------------以下内容省略--------------*/
}
rk3568.dtsi头文件 (内核源码/arch/arm64/boot/dts/rockchip/rk3568.dtsi)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <dt-bindings/clock/rk3568-cru.h>
#include <dt-bindings/interrupt-controller/arm-gic.h>
#include <dt-bindings/interrupt-controller/irq.h>
#include <dt-bindings/pinctrl/rockchip.h>
#include <dt-bindings/soc/rockchip,boot-mode.h>
#include <dt-bindings/phy/phy.h>
#include <dt-bindings/power/rk3568-power.h>
#include <dt-bindings/soc/rockchip-system-status.h>
#include <dt-bindings/suspend/rockchip-rk3568.h>
#include <dt-bindings/thermal/thermal.h>
#include "rk3568-dram-default-timing.dtsi"

/ {
    compatible = "rockchip,rk3568";

    interrupt-parent = <&gic>;
    #address-cells = <2>;
    #size-cells = <2>;

    aliases {
        csi2dphy0 = &csi2_dphy0;
        csi2dphy1 = &csi2_dphy1;
        csi2dphy2 = &csi2_dphy2;
        dsi0 = &dsi0;
        dsi1 = &dsi1;
        ethernet0 = &gmac0;

/*-------------以下内容省略--------------*/

}

设备树源码分为三部分,介绍如下:

  • 第3-9行: 头文件。设备树是可以像C语言那样使用“#include”引用“.h”后缀的头文件,也可以引用设备树“.dtsi”后缀的头文件。 rk3568.dtsi由rockchip官方提供,是一个rk3568.dtsi平台“共用”的设备树文件。

  • 第11-30行: 设备树节点。设备树给我们最直观的感受是它由一些嵌套的大括号“{}”组成, 每一个“{}”都是一个“节点”。“/ {…};”表示“根节点”,每一个设备树只有一个根节点。 如果打开“rk3568.dtsi”文件可以发现它也有一个根节点,虽然“rk3568-lubancat2.dts”引用了“rk3568.dtsi”文件, 但这并不代表“rk3568-lubancat2.dts”设备树有两个根节点,因为不同文件的根节点最终会合并为一个。 在根节点内部的“aliases {…}”、“chosen {…}”、“memory {…}”等字符,都是根节点的子节点。

  • 第32-39行: 设备树节点追加内容。第三部分的子节点比根节点下的子节点多了一个“&”, 这表示该节点在向已经存在的子节点追加数据。这些“已经存在的节点”可能定义在“rk3568-lubancat2.dts”文件, 也可能定义在“rk3568.dtsi”文件所包含的设备树文件里。 rk3568-lubancat2.dts代码中的“&cpu0 {…}”、“&dmc {…}”、“&i2c0 {…}”等等追加的目标节点,就是定义在“stm32mp157c.dtsi”中。

到目前为止我们知道设备树由一个根节点和众多子节点组成,子节点也可以继续包含其他节点,也就是子节点的子节点。 设备树的组成很简单,下面我们一起来看看节点的基本格式和节点属性。

8.2.1. 节点基本格式

节点的结构参考:

设备树结构

node-name 节点名称

节点格式中的 node-name 用于指定节点的名称。 它的长度为1至31个字符,只能由如下字符组成

表 节点名称

字符

描述

0-9

数字

a-z

小写字母

A-Z

大写字母

,

英文逗号

.

英文句号

_

下划线

加号

减号

另外,节点名应当使用大写或小写字母开头,并且能够描述设备类别。

注意,根节点没有节点名,它直接使用“/”指代这是一个根节点。

@unit-address

@unit-address ,其中的符号“@”可以理解为是一个分割符,“unit-address”用于指定“单元地址”, 它的值要和节点“reg”属性的第一个地址一致。如果节点没有“reg”属性值,可以直接省略“@unit-address”, 不过要注意这时要求同级别的设备树下(相同级别的子节点)节点名唯一,从这个侧面也可以了解到, 同级别的子节点的节点名可以相同,但是要求“单元地址”不同,node-name@unit-address 的整体要求同级唯一。

8.2.2. 节点标签

在rk3568.dtsi头文件中,节点名“cpu0”前面多了个“cpu@0”,这个“cpu0”就是我们所说的节点标签。 通常节点标签是节点名的简写,所以它的作用是当其它位置需要引用时可以使用节点标签来向该节点中追加内容。

8.2.3. 节点路径

通过指定从根节点到所需节点的完整路径,可以唯一地标识设备树中的节点, 不同层次的设备树节点名字可以相同,同层次的设备树节点要唯一 。 这有点类似于我们Windows上的文件,一个路径唯一标识一个文件或文件夹,不同目录下的文件文件名可以相同。例如前面节点的结构参考图中, 节点node1-name的子节点child-node1,节点路径就是 “/node1-name/child-node1”。

8.2.4. 节点属性

在节点的“{}”中包含的内容是节点属性,通常情况下一个节点包含多个属性信息, 这些属性信息就是要传递到内核的“板级硬件描述信息”,驱动中会通过一些API函数获取这些信息。

例如根节点“/”就有属性compatible = “rockchip,rk3568”。 我们可以通过该属性了解到硬件设备相关的名字叫“rk3568”,设备所使用的的是“rk3568”这颗 SOC。

我们编写设备树最主要的内容是编写节点的节点属性,通常情况下一个节点代表一个设备, 设备有哪些属性、怎么编写这些属性、在驱动中怎么引用这些属性是我们后面讲解的重点, 这一小节只讲解设备节点有哪些可设置属性。有一些节点属性是所有节点共有的,一些作用于特定的节点, 我们这里介绍那些共有的节点属性,其他节点属性使用到时再详细介绍。

节点属性分为标准属性和自定义属性,也就是说我们在设备树中可以根据自己的实际需要定义、添加设备属性。 标准属性的属性名是固定的,自定义属性名可按照要求自行定义。

compatible属性

属性值类型:字符串

compatible属性
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
    model = "EmbedFire LubanCat2 HDMI";
compatible = "embedfire,lubancat2", "rockchip,rk3568";

    aliases {
            csi2dphy0 = &csi2_dphy0;
            csi2dphy1 = &csi2_dphy1;
            csi2dphy2 = &csi2_dphy2;
            dsi0 = &dsi0;
            dsi1 = &dsi1;
            ethernet0 = &gmac0;
            ethernet1 = &gmac1;
            gpio0 = &gpio0;
    .....
    };

compatible属性值由一个或多个字符串组成,有多个字符串时使用“,”分隔开。

设备树中的每一个代表了一个设备的节点都要有一个compatible属性。 compatible是系统用来决定绑定到设备的设备驱动的关键。 compatible属性是用来查找节点的方法之一,另外还可以通过节点名或节点路径查找指定节点。

例如系统初始化时会初始化platform总线上的设备时,根据设备节点”compatible”属性和驱动中of_match_table对应的值,匹配了就加载对应的驱动。

model属性

属性值类型:字符串

示例:

model属性
1
model = "EmbedFire LubanCat2 HDMI+MIPI";

model属性用于指定设备的制造商和型号,推荐使用“制造商, 型号”的格式,当然也可以自定义。

status属性

属性值类型:字符串

示例:

status属性
1
2
3
4
/* External sound card */
sound: sound {
    status = "disabled";
};

状态属性用于指示设备的“操作状态”,通过status可以去禁止设备或者启用设备,可用的操作状态如下表。默认情况下不设置status属性设备是使能的。

节点名称

状态值

描述

okay

使能设备

disabled

禁用设备

fail

表示设备不可运行,目前驱动不支持,待修复。

fail-sss

表示设备不可运行,目前驱动不支持,待修复。“sss”的值与具体的设备相关。

#address-cells 和 #size-cells

属性值类型:u32

示例:

#address-cells和 #size-cells
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
soc {
    #address-cells = <1>;
    #size-cells = <1>;
    compatible = "simple-bus";
    interrupt-parent = <&gpc>;
    ranges;
    ocrams: sram@900000 {
            compatible = "fsl,lpm-sram";
            reg = <0x900000 0x4000>;
    };
};

#address-cells和 #size-cells属性同时存在,在设备树ocrams结构中, 它们用在有子节点的设备节点(节点),用于设置子节点的“reg”属性的“书写格式”。

补充:reg属性值由一串数字组成,如上图中的reg = <0x900000 0x4000>, ret属性的书写格式为reg = < cells cells cells cells cells cells…>,长度根据实际情况而定, 这些数据分为地址数据(地址字段),长度数据(大小字段)。

#address-cells,用于指定子节点reg属性“地址字段”所占的长度(单元格cells的个数)。 #size-cells,用于指定子节点reg属性“大小字段”所占的长度(单元格cells的个数)。

例如#address-cells=2,#address-cells=1,则reg内的数据含义为reg = <address address size address address size>, 因为每个cells是一个32位宽的数字,例如需要表示一个64位宽的地址时,就要使用两个address单元来表示。 而假如#address-cells=1,#address-cells=1,则reg内的数据含义为reg = < address size address size address size>。

总之#size-cells和#address-cells决定了子节点的reg属性中哪些数据是“地址”,哪些数据是“长度”信息。

reg属性

属性值类型:地址、长度数据对

reg属性描述设备资源在其父总线定义的地址空间内的地址。通常情况下用于表示一块寄存器的起始地址(偏移地址)和长度, 在特定情况下也有不同的含义。例如上例中#address-cells = <1>,#address-cells = <1>,reg = <0x9000000 x4000>, 其中0x9000000表示的是地址,0x4000表示的是地址长度,这里的reg属性指定了起始地址为0x9000000,长度为0x4000的一块地址空间。

ranges

属性值类型:任意数量的 <子地址、父地址、地址长度>编码

示例:

ranges属性
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
soc {
            #address-cells = <1>;
            #size-cells = <1>;
            compatible = "simple-bus";
            interrupt-parent = <&gpc>;
            ranges;

            busfreq {
        /*-------------以下内容省略--------------*/
            };
}

该属性提供了子节点地址空间和父地址空间的映射(转换)方法,常见格式是ranges = <子地址, 父地址, 转换长度>。 如果父地址空间和子地址空间相同则无需转换,如示例中所示,只写了renges,内容为空,我们也可以直接省略renges属性。

比如对于#address-cells和#size-cells都为1的话,以ranges=<0x0 0x10 0x20>为例,表示将子地址的从0x0~(0x0 + 0x20)的地址空间映射到父地址的0x10~(0x10 + 0x20)。

name和device_type

属性值类型:字符串。

示例:

name属性
1
2
3
example{
    name = "name"
}
device_type属性
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
    cpus {
            #address-cells = <2>;
            #size-cells = <0>;

            cpu0: cpu@0 {
                    device_type = "cpu";
                    compatible = "arm,cortex-a55";
                    reg = <0x0 0x0>;
                    enable-method = "psci";
                    clocks = <&scmi_clk 0>;
                    operating-points-v2 = <&cpu0_opp_table>;
                    cpu-idle-states = <&CPU_SLEEP>;
                    #cooling-cells = <2>;
                    dynamic-power-coefficient = <187>;
            };
    ...
}

这两个属性很少用(已经被废弃),不推荐使用。name用于指定节点名,在旧的设备树中它用于确定节点名, 现在我们使用的设备树已经弃用。device_type属性也是一个很少用的属性,只用在CPU和内存的节点上。 如上例中所示,device_type用在了CPU节点。

8.2.5. 追加/修改节点内容

追加/修改节点内容
1
2
3
&cpu0 {
    cpu-supply = <&vdd_cpu>;
};

这些源码并不包含在根节点“/{…}”内,它们不是一个新的节点,而是向原有节点追加内容。 以上方源码为例,“&cpu0”表示向“节点标签”为“cpu0”的节点追加数据, 这个节点可能定义在本文件也可能定义在本文件所包含的设备树文件中, 本例子中源码的“cpu0”定义在“rk3568.dtsi”文件中。

8.2.6. 特殊节点

aliases子节点

aliases子节点的作用就是为其他节点起一个别名,如下所示。

别名子节点
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
    aliases {
            csi2dphy0 = &csi2_dphy0;
            csi2dphy1 = &csi2_dphy1;
            csi2dphy2 = &csi2_dphy2;
    /*----------- 省略------------*/
            mmc0 = &sdhci;
            mmc1 = &sdmmc0;
            mmc2 = &sdmmc1;
            mmc3 = &sdmmc2;
            serial0 = &uart0;
            serial1 = &uart1;
            serial2 = &uart2;
    /*----------- 以下省略------------*/
}

以“serial0 = &uart0;”为例。“serial0”是一个节点的名字, 设置别名后我们可以使用“serial0”来指代uart0节点,与节点标签类似。 在设备树中更多的是为节点添加标签,没有使用节点别名,别名的作用是“快速找到设备树节点”。 在驱动中如果要查找一个节点,通常情况下我们可以使用“节点路径”一步步找到节点。 也可以使用别名“一步到位”找到节点。

chosen子节点

chosen子节点位于根节点下,如下所示

chosen子节点
1
2
3
    chosen {
            bootargs = "earlycon=uart8250,mmio32,0xfe660000 console=ttyFIQ0 root=PARTUUID=614e0000-0000 rw rootwait";
    };

chosen子节点不代表实际硬件,它主要用于给内核传递参数。 此外这个节点还用作uboot向linux内核传递配置参数的“通道”, 我们在Uboot中设置的参数就是通过这个节点传递到内核的, 这部分内容是uboot和内核自动完成的,作为初学者我们不必深究。

在中断、时钟部分也有自己的节点标准属性,随着深入的学习我们会详细介绍这些节点标准属性。

8.3. 如何获取设备树节点信息

在设备树中“节点”对应实际硬件中的设备,我们在设备树中添加了一个“led”节点, 正常情况下我们可以从这个节点获取编写led驱动所用到的所有信息,例如led相关控制寄存器地址、 led时钟控制寄存器地址等等。

这一小节我们就开始学习如何从设备树的设备节点获取我们想要的数据。 内核提供了一组函数用于从设备节点获取资源(设备节点中定义的属性)的函数,这些函数以of_开头,称为OF操作函数。 常用的OF函数介绍如下:

8.3.1. 查找节点函数

8.3.1.1. 根据节点路径寻找节点函数

of_find_node_by_path函数 (内核源码/include/linux/of.h)
1
struct device_node *of_find_node_by_path(const char *path)

参数:

  • path: 指定节点在设备树中的路径。

返回值:

  • device_node: 结构体指针,如果查找失败则返回NULL,否则返回device_node类型的结构体指针,它保存着设备节点的信息。

device_node结构体如下所示。

device_node结构体
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct device_node {
    const char *name;
    const char *type;
    phandle phandle;
    const char *full_name;
    struct fwnode_handle fwnode;

    struct  property *properties;
    struct  property *deadprops;    /* removed properties */
    struct  device_node *parent;
    struct  device_node *child;
    struct  device_node *sibling;
#if defined(CONFIG_OF_KOBJ)
    struct  kobject kobj;
#endif
    unsigned long _flags;
    void    *data;
#if defined(CONFIG_SPARC)
    const char *path_component_name;
    unsigned int unique_id;
    struct of_irq_controller *irq_trans;
#endif
};
  • name: 节点中属性为name的值

  • type: 节点中属性为device_type的值

  • full_name: 节点的名字,在device_node结构体后面放一个字符串,full_name指向它

  • properties: 链表,连接该节点的所有属性

  • parent: 指向父节点

  • child: 指向子节点

  • sibling: 指向兄弟节点

得到device_node结构体之后我们就可以使用其他of 函数获取节点的详细信息。

8.3.1.2. 根据节点名字寻找节点函数

of_find_node_by_name函数 (内核源码/include/linux/of.h)
1
struct device_node *of_find_node_by_name(struct device_node *from,const char *name);

参数:

  • from: 指定从哪个节点开始查找,它本身并不在查找行列中,只查找它后面的节点,如果设置为NULL表示从根节点开始查找。

  • name: 要寻找的节点名。

返回值:

  • device_node: 结构体指针,如果查找失败则返回NULL,否则返回device_node类型的结构体指针,它保存着设备节点的信息。

8.3.1.3. 根据节点类型寻找节点函数

of_find_node_by_type函数 (内核源码/include/linux/of.h)
1
struct device_node *of_find_node_by_type(struct device_node *from,const char *type)

参数:

  • from: 指定从哪个节点开始查找,它本身并不在查找行列中,只查找它后面的节点,如果设置为NULL表示从根节点开始查找。

  • type: 要查找节点的类型,这个类型就是device_node-> type。

返回值:

  • device_node: device_node类型的结构体指针,保存获取得到的节点。同样,如果失败返回NULL。

8.3.1.4. 根据节点类型和compatible属性寻找节点函数

of_find_compatible_node函数 (内核源码/include/linux/of.h)
1
struct device_node *of_find_compatible_node(struct device_node *from,const char *type, const char *compatible)

相比of_find_node_by_name函数增加了一个compatible属性作为筛选条件。

参数:

  • from: 指定从哪个节点开始查找,它本身并不在查找行列中,只查找它后面的节点,如果设置为NULL表示从根节点开始查找。

  • type: 要查找节点的类型,这个类型就是device_node-> type。

  • compatible: 要查找节点的compatible属性。

返回值:

  • device_node: device_node类型的结构体指针,保存获取得到的节点。同样,如果失败返回NULL。

8.3.1.5. 根据匹配表寻找节点函数

of_find_matching_node_and_match函数 (内核源码/include/linux/of.h)
1
static inline 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)

可以看到,该结构体包含了更多的匹配参数,也就是说相比前三个寻找节点函数,这个函数匹配的参数更多,对节点的筛选更细。参数match,查找得到的结果。

参数:

  • from: 指定从哪个节点开始查找,它本身并不在查找行列中,只查找它后面的节点,如果设置为NULL表示从根节点开始查找。

  • matches: 源匹配表,查找与该匹配表想匹配的设备节点。

  • of_device_id: 结构体如下。

返回值:

  • device_node: device_node类型的结构体指针,保存获取得到的节点。同样,如果失败返回NULL。

of_device_id结构体
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/*
  * Struct used for matching a device
  */

struct of_device_id {
    char    name[32];
    char    type[32];
    char    compatible[128];
    const void *data;
};
  • name: 节点中属性为name的值

  • type: 节点中属性为device_type的值

  • compatible: 节点的名字,在device_node结构体后面放一个字符串,full_name指向它

  • data: 链表,连接该节点的所有属性

8.3.1.6. 寻找父节点函数

of_get_parent函数 (内核源码/include/linux/of.h)
1
struct device_node *of_get_parent(const struct device_node *node)

参数:

  • node: 指定谁(节点)要查找父节点。

返回值:

  • device_node: device_node类型的结构体指针,保存获取得到的节点。同样,如果失败返回NULL。

8.3.1.7. 寻找子节点函数

of_get_next_child函数 (内核源码/include/linux/of.h)
1
struct device_node *of_get_next_child(const struct device_node *node, struct device_node *prev)

参数:

  • node: 指定谁(节点)要查找它的子节点。

  • prev: 前一个子节点,寻找的是prev节点之后的节点。这是一个迭代寻找过程,例如寻找第二个子节点,这里就要填第一个子节点。参数为NULL 表示寻找第一个子节点。

返回值:

  • device_node: device_node类型的结构体指针,保存获取得到的节点。同样,如果失败返回NULL。

这里介绍了7个寻找节点函数,这7个函数有一个共同特点——返回值类型相同。只要找到了节点就会返回节点对应的device_node结构体,在驱动程序中我们就是通过这个device_node获取设备节点的属性信息、顺藤摸瓜查找它的父、子节点等等。第一函数of_find_node_by_path与后面六个不 同,它是通过节点路径寻找节点的,“节点路径”是从设备树源文件(.dts)中的到的。而中间四个函数是根据节点属性在某一个节点之后查找符合要求的设备节点,这个“某一个节点”是设备节点结构体(device_node),也就是说这个节点是已经找到的。最后两个函数与中间四个类似,只不过最后两个没有使用节点属性 而是根据父、子关系查找。

8.3.2. 提取属性值的of函数

上一小节我们讲解了7个查找节点的函数,它们有一个共同特点,找到一个设备节点就会返回这个设备节点对应的结构体指针(device_node*)。这个过程可以理解为把设备树中的设备节点“获取”到驱动中。“获取”成功后我们再通过一组of函数从设备节点结构体(device_node)中获取我们想要的设备节点属 性信息。

8.3.2.1. 查找节点属性函数

of_find_property函数 (内核源码/include/linux/of.h)
1
struct property *of_find_property(const struct device_node *np,const char *name,int *lenp)

参数:

  • np: 指定要获取那个设备节点的属性信息。

  • name: 属性名。

  • lenp: 获取得到的属性值的大小,这个指针作为输出参数,这个参数“带回”的值是实际获取得到的属性大小。

返回值:

  • property: 获取得到的属性。property结构体,我们把它称为节点属性结构体,如下所示。失败返回NULL。从这个结构体中我们就可以得到想要的属性值了。

property属性结构体
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct property {
    char    *name;
    int     length;
    void    *value;
    struct property *next;
#if defined(CONFIG_OF_DYNAMIC) || defined(CONFIG_SPARC)
    unsigned long _flags;
#endif
#if defined(CONFIG_OF_PROMTREE)
    unsigned int unique_id;
#endif
#if defined(CONFIG_OF_KOBJ)
    struct bin_attribute attr;
#endif
};
  • name: 属性名

  • length: 属性长度

  • value: 属性值

  • next: 下一个属性

8.3.2.2. 读取整型属性函数

读取属性函数是一组函数,分别为读取8、16、32、64位数据。

of_property_read_uX_array函数组 (内核源码/include/linux/of.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//8位整数读取函数
int of_property_read_u8_array(const struct device_node *np, const char *propname, u8 *out_values, size_t sz)

//16位整数读取函数
int of_property_read_u16_array(const struct device_node *np, const char *propname, u16 *out_values, size_t sz)

//32位整数读取函数
int of_property_read_u32_array(const struct device_node *np, const char *propname, u32 *out_values, size_t sz)

//64位整数读取函数
int of_property_read_u64_array(const struct device_node *np, const char *propname, u64 *out_values, size_t sz)

参数:

  • np: 指定要读取那个设备节点结构体,也就是说读取那个设备节点的数据。

  • propname: 指定要获取设备节点的哪个属性。

  • out_values: 这是一个输出参数,是函数的“返回值”,保存读取得到的数据。

  • sz: 这是一个输入参数,它用于设置读取的长度。

返回值:

  • 返回值,成功返回0,错误返回错误状态码(非零值),-EINVAL(属性不存在),-ENODATA(没有要读取的数据),-EOVERFLOW(属性值列表太小)。

8.3.2.3. 简化后的读取整型属性函数

这里的函数是对读取整型属性函数的简单封装,将读取长度设置为1。用法与读取属性函数完全一致,这里不再赘述。

of_property_read_uX函数组 (内核源码/include/linux/of.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//8位整数读取函数
int of_property_read_u8 (const struct device_node *np, const char *propname,u8 *out_values)

//16位整数读取函数
int of_property_read_u16 (const struct device_node *np, const char *propname,u16 *out_values)

//32位整数读取函数
int of_property_read_u32 (const struct device_node *np, const char *propname,u32 *out_values)

//64位整数读取函数
int of_property_read_u64 (const struct device_node *np, const char *propname,u64 *out_values)

读取字符串属性函数

在设备节点中存在很多字符串属性,例如compatible、status、type等等,这些属性可以使用查找节点属性函数of_find_property来获取,但是这样比较繁琐。内核提供了一组用于读取字符串属性的函数,介绍如下:

of_property_read_string函数 (内核源码/include/linux/of.h)
1
int of_property_read_string(const struct device_node *np,const char *propname,const char **out_string)

参数:

  • np: 指定要获取那个设备节点的属性信息。

  • propname: 属性名。

  • out_string: 获取得到字符串指针,这是一个“输出”参数,带回一个字符串指针。也就是字符串属性值的首地址。这个地址是“属性值”在内存中的真实位置,也就是说我们可以通过对地址操作获取整个字符串属性(一个字符串属性可能包含多个字符串,这些字符串在内存中连续存储,使用’0’分隔)。

返回值:

  • 返回值:成功返回0,失败返回错误状态码。

这个函数使用相对繁琐,推荐使用下面这个函数。

of_property_read_string_index函数 (内核源码/include/linux/of.h)
1
int of_property_read_string_index(const struct device_node *np,const char *propname, int index,const char **out_string)

相比前面的函数增加了参数index,它用于指定读取属性值中第几个字符串,index从零开始计数。 第一个函数只能得到属性值所在地址,也就是第一个字符串的地址,其他字符串需要我们手动修改移动地址,非常麻烦,推荐使用第二个函数。

读取布尔型属性函数

在设备节点中一些属性是BOOL型,当然内核会提供读取BOOL型属性的函数,介绍如下:

of_property_read_string_index函数 (内核源码/include/linux/of.h)
1
static inline bool of_property_read_bool(const struct device_node *np, const char *propname);

参数:

  • np: 指定要获取那个设备节点的属性信息。

  • propname: 属性名。

返回值:

这个函数不按套路出牌,它不是读取某个布尔型属性的值,仅仅是读取这个属性存在或者不存在。如果想要或取值,可以使用之前讲解的“全能”函数查找节点属性函数of_find_property。

8.3.3. 内存映射相关of函数

在设备树的设备节点中大多会包含一些内存相关的属性,比如常用的reg属性。通常情况下,得到寄存器地址之后我们还要通过ioremap函数将物理地址转化为虚拟地址。现在内核提供了of函数,自动完成物理地址到虚拟地址的转换。介绍如下:

of_iomap函数 (内核源码/drivers/of/address.c)
1
void __iomem *of_iomap(struct device_node *np, int index)

参数:

  • np: 指定要获取那个设备节点的属性信息。

  • index: 通常情况下reg属性包含多段,index 用于指定映射那一段,标号从0开始。

返回值:

  • 成功,得到转换得到的地址。失败返回NULL。

内核也提供了常规获取地址的of函数,这些函数得到的值就是我们在设备树中设置的地址值。介绍如下:

of_address_to_resource函数 (内核源码/drivers/of/address.c)
1
int of_address_to_resource(struct device_node *dev, int index, struct resource *r)

参数:

  • np: 指定要获取那个设备节点的属性信息。

  • index: 通常情况下reg属性包含多段,index 用于指定映射那一段,标号从0开始。

  • r: 这是一个resource结构体,是“输出参数”用于返回得到的地址信息。

返回值:

  • 成功返回0,失败返回错误状态码。

resource结构体如下所示:

resource属性结构体
1
2
3
4
5
6
7
8
struct resource {
    resource_size_t start;
    resource_size_t end;
    const char *name;
    unsigned long flags;
    unsigned long desc;
    struct resource *parent, *sibling, *child;
};
  • start: 起始地址

  • end: 结束地址

  • name: 属性名字

从这个结构体比较简单,很容从中得到获取得到的具体信息。这里不再赘述。

这里介绍了三类常用的of函数,这些基本满足我们的需求,其他of函数后续如果使用到我们在详细介绍。

8.4. 向设备树中添加设备节点实验

8.4.1. 实验说明

通常情况下我们几乎不会从零开始写一个设备树,因为一个功能完善的设备树通常比较庞大, 例如本教程引用的rockchip官方编写的设备树“rk3568.dtsi”就几千行, 另外官方已经写好了主干的部分,我们只需要引用官方写好的设备树,然后根据自己的实际情况修改即可。

本节实验使用野火Lubancat_RK系列板卡,使用lubancat2为例,板卡系统是ubuntu20.04,设备树文件是rk3568-lubancat2.dts。

实验中如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能, 几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。

8.4.2. 代码讲解

本章的示例代码目录为:linux_driver/device_tree

在实际应用中我们最常见的操作是向设备节点中增加一个节点、向现有设备节点追加数据、和编写设备树插件。

根据之前讲解,lubancat2默认使用的是”kernel/arch/arm64/boot/dts/rockchip/rk3568-lubancat2.dts”设备树, 我们就在这个设备树里尝试增加一个设备节点,如下所示。

添加子节点led_test(在rk3568-lubancat2.dts)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*
* Copyright (C) 2022 - All Rights Reserved by
* EmbedFire LubanCat
*/

/dts-v1/;

#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pwm/pwm.h>
#include <dt-bindings/pinctrl/rockchip.h>
#include <dt-bindings/input/rk-input.h>
#include <dt-bindings/display/drm_mipi_dsi.h>
#include <dt-bindings/sensor-dev.h>
#include "rk3568.dtsi"
/ {
    model = "EmbedFire LubanCat2 HDMI";
    compatible = "embedfire,lubancat2", "rockchip,rk3568";

    /*.......................*/

    /*添加led_test节点*/
    get_dts_info_test: get_dts_info_test{
        compatible = "get_dts_info_test";
        #address-cells = <1>;
        #size-cells = <1>;

        led@0xfdd60000{     //GPIO0基地址0xfdd60000
                compatible = "fire,led_test";
                reg = <0xfdd60000 0x00000100>;
                status = "okay";
        };
    };

    /*.......................*/
};

在我们在rk3568-lubancat2.dts设备树文件中新增了一个节点名为“led_test”的节点, 里面只添加了几个基本属性,我们这里只是学习添加一个设备节点。

在以上代码中,led_test节点的#address-cells = <1>,#size-cells = <1>, 意味着它的子节点的reg属性里的数据是“地址”、“长度”交替的。

第二部分是led 节点的子节点,它定义了三个属性分别为compatible、reg、status, 这三个属性在 “节点属性”章节已经介绍。需要注意的是rgb属性, 在父节点设置了#address-cells = <1>,#size-cells = <1>, 所以这里0xfdd60000表示的是地址(这里填写的是GPIO0控制寄存器的首地址), 0x00000100表示的是地址长度。“led@0xfdd60000”中的单元的地址0xfdd60000要和reg属性的第一个地址一致。

提示

其余系列板卡参考 驱动章节实验环境搭建 章节确定板卡使用的设备树文件,然后进行修改添加。

内核编译设备树:

编译内核时会自动编译设备树,但是编译内核很耗时,所以我们推荐使用如下命令只编译设备树:

make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- lubancat2_defconfig   //这里以rk356x系列配置文件为例
make ARCH=arm64 -j4 CROSS_COMPILE=aarch64-linux-gnu- dtbs

编译成功后生成的设备树文件(.dtb)位于源码目录下的arch/arm64/boot/dts/rockchip/,文件名为“rk3568-lubancat2.dtb”

8.4.3. 程序结果

8.4.3.1. 加载设备树

同SCP或NFS将编译的设备树拷贝到开发板上,替换/boot/dtb/rk3568-lubancat2.dtb。

uboot在启动的时候负责该目录的设备文件加载到内存,供内核解析使用,重启开发板。

8.4.3.2. 实验结果

设备树中的设备树节点在文件系统中有与之对应的文件,位于“/proc/device-tree”目录。进入“/proc/device-tree”目录如下所示。

查看get_dts_info_test节点

接着进入led 文件夹,可以发现led节点中定义的属性以及它的子节点,如下所示。

get_dts_info_test子节点

在节点属性中多了一个name,我们在led节点中并没有定义name属性,这是自从生成的,保存节点名。

这里的属性是一个文件,而子节点是一个文件夹,我们再次进入“led@0xfdd60000”文件夹。 里面有compatible、name、reg、status四个属性文件。 我们可以使用“cat”命令查看这些属性文件,如下所示。

查看子节点属性文件

至此,我们已经成功的在设备树中添加了一个名为“get_dts_info_test”的节点。

8.5. 在驱动中获取节点属性实验

本实验目的是演示如何使用上一小节讲解的of函数,进行本实验之前要先完成“在设备树中添加设备节点实验”, 因为本实验就是从我们添加的节点中获取设备节点属性,驱动是基于平台驱动。

8.5.1. 代码讲解

本章的示例代码目录为:linux_driver/device_tree/get_dts_info.c

程序源码如下所示,这里只列出了get_dts_info_probe函数中的内容,完整内容请参考本章配套源码。

获取节点属性实验
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/*get_dts_info_probe 函数*/
static int get_dts_info_probe(struct platform_device *pdev)
{
    int error_status = -1;
    pr_info("%s\n",__func__);

    led_test_device_node = of_find_node_by_path("/get_dts_info_test");
    if(led_test_device_node == NULL)
    {
        printk(KERN_ALERT "\n get led_device_node failed ! \n");
        return -1;
    }
    /*根据 led_test_device_node 设备节点结构体输出节点的基本信息*/
    printk(KERN_ALERT "name: %s",led_test_device_node->name); //输出节点名
    printk(KERN_ALERT "child name: %s",led_test_device_node->child->name);  //输出子节点的节点名


    /*获取 led_device_node 的子节点*/
    led_device_node = of_get_next_child(led_test_device_node,NULL);
    if(led_device_node == NULL)
    {
        printk(KERN_ALERT "\n get led_device_node failed ! \n");
        return -1;
    }
    printk(KERN_ALERT "name: %s",led_device_node->name); //输出节点名
    printk(KERN_ALERT "parent name: %s",led_device_node->parent->name);  //输出父节点的节点名


    /*获取 led_device_node 节点  的"compatible" 属性 */
    led_property = of_find_property(led_device_node,"compatible",&size);
    if(led_property == NULL)
    {
        printk(KERN_ALERT "\n get led_property failed ! \n");
        return -1;
    }
    printk(KERN_ALERT "size = : %d",size);                      //实际读取得到的长度
    printk(KERN_ALERT "name: %s",led_property->name);   //输出属性名
    printk(KERN_ALERT "length: %d",led_property->length);        //输出属性长度
    printk(KERN_ALERT "value : %s",(char*)led_property->value);  //属性值

    /*获取 reg 地址属性*/
    error_status = of_property_read_u32_array(led_device_node,"reg",out_values, 2);
    if(error_status != 0)
    {
        printk(KERN_ALERT "\n get out_values failed ! \n");
        return -1;
    }
    printk(KERN_ALERT"0x%08X ", out_values[0]);
    printk(KERN_ALERT"0x%08X ", out_values[1]);

    return 0;
}
  • 第7-12行:使用“of_find_node_by_path”函数寻找“get_dts_info_test”设备节点。参数是“get_dts_info_test”的设备节点路径。

  • 第14-15行:获取成功后得到的是一个device_node类型的结构体指针,然后我们就可以从这个结构体中获得我们想要的数据。获取完整的属性信息可能还需要使用其他of函数。

  • 第19-24行:获取 led_device_node 的子节点,在第二部分我们得到了“led”节点的“设备节点结构体”这里就可以使用“of_get_next_child”函数获取它的子节点。当然我们也可以从“led”节点的“设备节点结构体”中直接读取得到它的第一个子节点。

  • 第30-35行:使用“of_find_property”函数获取“led”节点的“compatible”属性。

  • 第42-49行:使用“of_property_read_u32_array”函数获取reg属性。

进入到驱动模块文件夹中,编译驱动模块:

make

该文件夹会产生get_dts_info.ko驱动模块

8.5.2. 程序结果

编译成功后将驱动.ko拷贝到开发板,使用insmod安装驱动模块然后可以看到:

sudo insmod get_dts_info.ko
获取设备树节点信息

从上图中可以看到,驱动程序中得到了设备树中设置的属性值。