12. Linux设备树

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

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

12.1. 设备树简介

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

设备树

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

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

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

DTS、DTC和DTB它们是文档中常见的几个缩写。常见的设备树术语有:

设备树相关术语

术语

描述

设备树

一种用于描述系统硬件设备的数据结构,它采用树形结构来组织硬件信息。

DTS

设备树源文件(Device Tree Source),指.dts格式的文件,是一种ASII文本格式的设备树描述,

也是我们要编写的设备树源码,一般一个.dts文件对应一个硬件平台,比如对应arm64架构,

源文件则位于Linux源码的“/arch/arm64/boot/dts”目录下。

DTSI

设备树包含文件(Device Tree Include)

DTC

指编译设备树源码的工具,即设备树编译器(Device Tree Compiler)

DTB

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

12.2. 设备树框架

设备树(Device Tree)由一系列被命名的节点(node)和属性(property)组成,以Lubancat-A1为例, 不妨打开 内核源码/arch/arm64/boot/dts/sunxi/sun50iw9-lubancat-a1.dts先睹为快。

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

设备树 (内核源码/arch/arm64/boot/dts/sunxi/sun50iw9-lubancat-a1.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
42
43
44
45
46
47
48
49
50
51
52
53
/dts-v1/;
#include <dt-bindings/gpio/sun4i-gpio.h>

#include "sun50iw9.dtsi"
#include "sun50iw9-h618-pinctrl.dtsi"

/{
    model = "sun50iw9";
    compatible = "allwinner,h616", "arm,sun50iw9p1";

    aliases {
        disp = &uboot_disp;
        lcd0 = &lcd0;
        pmu0 = &pmu0;
        standby_param = &standby_param;
        hdmi = &uboot_hdmi;
        spi0 = &spi0;
        spi1 = &spi1;
        twi0 = &twi0;
        twi1 = &twi1;
        twi2 = &twi2;
        twi3 = &twi3;
        twi4 = &twi4;
        twi5 = &twi5;
    };

    usb0_drvvbus: usb0-drvvvbus {
        compatible = "regulator-fixed";
        regulator-name = "usb0-drvvbus";
        regulator-min-microvolt = <5000000>;
        regulator-max-microvolt = <5000000>;
        regulator-enable-ramp-delay = <1000>;
        gpio = <&pio PG 14 GPIO_ACTIVE_HIGH>;
        enable-active-high;
    };
    /*-------------内容省略--------------*/
};
/*-------------内容省略--------------*/
&usbc0 {
    device_type             = "usbc0";
    usb_port_type           = <0x2>;
    usb_detect_type         = <0x1>;
    usb_detect_mode         = <0x0>;
    usb_id_gpio = <&pio PC 2 GPIO_ACTIVE_HIGH>;
    usb_det_vbus_gpio = <&pio PC 2 GPIO_ACTIVE_HIGH>;
    usb_regulator_io        = "nocare";
    usb_wakeup_suspend      = <0x0>;
    usb_luns                = <0x3>;
    usb_serial_unique       = <0>;
    usb_serial_number       = "20080411";
    status                  = "okay";
};
/*-------------以下内容省略--------------*/

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

  • 第2-5行: 头文件。设备树是可以像C语言那样使用“#include”引用“.h”后缀的头文件,也可以引用设备树“.dtsi”后缀的头文件。


  • 第7-37行: 设备树节点。设备树给我们最直观的感受是它由一些嵌套的大括号“{}”组成, 每一个“{}”都是一个“节点”。“/ {…};”表示“根节点”,每一个设备树只有一个根节点。

    • 如果打开“sun50iw9.dtsi”文件可以发现它也有一个根节点,虽然“sun50iw9-lubancat-a1.dts”引用了“sun50iw9.dtsi”文件,但这并不代表“sun50iw9-lubancat-a1.dts”设备树有两个根节点,因为 不同文件的根节点最终会合并为一个

    • 在根节点内部的“aliases {…}”、“usb0_drvvbus: usb0-drvvvbus {…}”等字符,都是根节点的子节点。


  • 第39-52行: 设备树节点追加内容。第三部分的子节点比根节点下的子节点多了一个“&”,这表示该节点在向已经存在的子节点追加数据。

    • 这些“已经存在的节点”可能定义在“sun50iw9-lubancat-a1.dts”文件,也可能定义在“sun50iw9.dtsi”等所包含的设备树文件里。

    • sun50iw9-lubancat-a1.dts代码中的“&usbc0 {…}”、“&ehci0 {…}”、“&cpu0 {…}”等等追加的目标节点,就是定义在“sun50iw9.dtsi”中。


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

12.2.1. 节点基本格式

节点的结构参考:

设备树结构

12.2.1.1. node-name 节点名称

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

字符

描述

0-9

数字

a-z

小写字母

A-Z

大写字母

,

英文逗号

.

英文句号

_

下划线

+

加号

-

减号

重要

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

注意

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

12.2.1.2. @unit-address

@unit-address

  • @ 可以理解为是一个分割符, unit-address 用于指定“单元地址”,它的值要和节点“reg”属性的第一个地址一致。

  • 如果节点没有“reg”属性值,可以直接省略“@unit-address”,不过要注意这时要求同级别的设备树下(相同级别的子节点)节点名唯一,从这个侧面也可以了解到,同级别的子节点的节点名可以相同,但是要求“单元地址”不同,node-name@unit-address 的整体要求同级唯一。

12.2.2. 节点标签

在sun50iw9.dtsi头文件中,节点名“cpu@0”前面多了个“cpu0”,这个“cpu0”就是我们所说的节点标签。

通常节点标签是节点名的简写,所以它的作用是当其它位置需要引用时可以使用节点标签来向该节点中追加内容。

内核源码/arch/arm64/boot/dts/sunxi/sun50iw9.dtsi
cpus {
    ......
    cpu0: cpu@0 {
        ......
    };

    cpu@1 {
        ......
    };
    ......
};

12.2.3. 节点路径

通过指定从根节点到所需节点的完整路径,可以唯一地标识设备树中的节点, 不同层次的设备树节点名字可以相同,同层次的设备树节点要唯一

这有点类似于我们Windows上的文件,一个路径唯一标识一个文件或文件夹,不同目录下的文件文件名可以相同。

例如前面节点的结构参考图中,节点1 node1-name的子节点child-node1 和节点2 node2-name的子节点child-node1,虽然子节点的名称相同,但其节点路径是不同的。

12.2.4. 节点属性

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

例如根节点“/”就有属性 compatible = "allwinner,h616", "arm,sun50iw9p1" 。 我们可以通过该属性了解到硬件设备相关的名字叫全志(allwinner)的“h616”,其实使用的的是全志“H618”这颗 SOC,只是这里没修改。

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

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

12.2.4.1. compatible属性

属性值类型:字符串

compatible属性
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
leds: leds {
    compatible = "gpio-leds";
    status = "okay";

    led-sys {
        label = "led_sys";
        gpios =  <&pio PF 6 GPIO_ACTIVE_HIGH>;
        linux,default-trigger = "heartbeat";
    };
};

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

  • 设备树中的每一个代表了一个设备的节点都要有一个compatible属性。

  • compatible是系统用来决定绑定到设备的设备驱动的关键。

  • compatible属性是用来查找节点的方法之一,另外还可以通过节点名或节点路径查找指定节点。

注意

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

12.2.4.2. model属性

属性值类型:字符串

示例:

model属性
1
model = "sun50iw9";

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

12.2.4.3. status属性

属性值类型:字符串

示例:

status属性
1
2
3
4
5
6
7
// DEBUG UART0
&uart0 {
    pinctrl-names = "default", "sleep";
    pinctrl-0 = <&uart0_ph_pins>;
    pinctrl-1 = <&uart0_ph_sleep>;
    status = "okay";
};

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

节点名称

状态值

描述

okay

使能设备

disabled

禁用设备

fail

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

fail-sss

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

12.2.4.4. #address-cells 和 #size-cells

属性值类型:u32

示例:

#address-cells和 #size-cells
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
cpus {
    #address-cells = <1>;
    #size-cells = <0>;
    ......
    cpu0: cpu@0 {
        device_type = "cpu";
        compatible = "arm,cortex-a53";
        reg = <0x0>;
        ......
    }
}
soc: soc@3000000 {
        compatible = "simple-bus";
        #address-cells = <2>;
        #size-cells = <2>;
        ......
        pwm0: pwm0@300a010 {
            compatible = "allwinner,sunxi-pwm0";
            reg = <0x0 0x0300a010 0x0 0x4>;
            reg_base = <0x0300a000>;
        };
        ......
}

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

注解

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

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

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

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

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

12.2.4.5. reg属性

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

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

12.2.4.6. ranges

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

示例:

ranges属性
1
2
3
4
5
6
7
8
soc: soc@3000000 {
    compatible = "simple-bus";
    #address-cells = <2>;
    #size-cells = <2>;
    ranges;

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

该属性提供了子节点地址空间和父地址空间的映射(转换)方法,常见格式是ranges = <子地址, 父地址, 转换长度>。

如果父地址空间和子地址空间相同则无需转换,如示例中所示,只写了renges,内容为空,我们也可以直接省略renges属性。

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

12.2.4.7. name和device_type

属性值类型:字符串。

示例:

name属性
1
2
3
example{
    name = "name"
}
device_type属性
1
2
3
4
5
6
7
8
9
cpus {
    #address-cells = <1>;
    #size-cells = <0>;

    cpu0: cpu@0 {
        device_type = "cpu";
        ......
    }
}

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

12.2.5. 追加/修改节点内容

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

这些源码并不包含在根节点“/{…}”内,它们不是一个新的节点,而是向原有节点追加内容。

以上方源码为例,“&cpu0”表示向“节点标签”为“cpu0”的节点追加数据,这个节点可能定义在本文件也可能定义在本文件所包含的设备树文件中,本例子中源码的“cpu0”定义在“sun50iw9.dtsi”文件中。

12.2.6. 特殊节点

12.2.6.1. aliases子节点

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

别名子节点
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
aliases {
    serial0 = &uart0;
    serial1 = &uart1;
    serial2 = &uart2;
    serial3 = &uart3;
    serial4 = &uart4;
    serial5 = &uart5;
    pwm = &pwm;
    pwm0 = &pwm0;
    pwm1 = &pwm1;
    pwm2 = &pwm2;
    pwm3 = &pwm3;
    pwm4 = &pwm4;
    pwm5 = &pwm5;
    ......
};

以“serial0 = &uart0;”为例。“serial0”是一个节点的名字,设置别名后我们可以使用“serial0”来指代uart0节点,与节点标签类似。

在设备树中更多的是为节点添加标签,没有使用节点别名,别名的作用是“快速找到设备树节点”。 在驱动中如果要查找一个节点,通常情况下我们可以使用“节点路径”一步步找到节点。 也可以使用别名“一步到位”找到节点。

12.2.6.2. chosen子节点

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

chosen子节点(内核源码/arch/arm64/boot/dts/sunxi/sun50iw10p1.dtsi)
1
2
3
4
5
chosen {
    bootargs = "earlyprintk=sunxi-uart,0x05000000 loglevel=8 initcall_debug=0 console=ttyS0 init=/init";
    linux,initrd-start = <0x0 0x0>;
    linux,initrd-end = <0x0 0x0>;
};

chosen子节点不代表实际硬件,它主要用于给内核传递参数。

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

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

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

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

这一小节我们就开始学习如何从设备树的设备节点获取我们想要的数据。

内核提供了一组函数用于从设备节点获取资源(设备节点中定义的属性)的函数,这些函数以of_开头,称为OF操作函数。

12.3.1. 查找节点函数

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

of_find_node_by_path函数 (内核源码/include/linux/of.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/*******************************************************************************
*  @brief     of_find_node_by_path
*  @note      根据节点路径寻找节点
*  @param     path        指定节点在设备树中的路径
*  @return    查找失败返回NULL,成功返回device_node类型的结构体指针
*******************************************************************************/
static inline struct device_node *of_find_node_by_path(const char *path)
{
    return NULL;
}

device_node结构体如下所示。

device_node结构体(内核源码/include/linux/of.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
struct device_node {
    const char *name;
    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)
    unsigned int unique_id;
    struct of_irq_controller *irq_trans;
#endif
};
device_node结构体成员

成员

描述

name

节点中属性为name的值

full_name

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

properties

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

parent

指向父节点

child

指向子节点

sibling

指向兄弟节点

注解

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

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

of_find_node_by_name函数 (内核源码/include/linux/of.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/*******************************************************************************
*  @brief     of_find_node_by_name
*  @note      根据节点名字寻找节点
*  @param     from       指定从哪个节点开始查找,如果设置为NULL表示从根节点开始查找
*  @param     name       要寻找的节点名
*  @return    查找失败返回NULL,成功返回device_node类型的结构体指针
*******************************************************************************/
static inline struct device_node *of_find_node_by_name(struct device_node *from,
    const char *name)
{
    return NULL;
}

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

of_find_node_by_type函数 (内核源码/include/linux/of.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/*******************************************************************************
*  @brief     of_find_node_by_type
*  @note      根据节点类型寻找节点
*  @param     from       指定从哪个节点开始查找,如果设置为NULL表示从根节点开始查找
*  @param     type       要查找节点的类型,这个类型就是device_node-> type
*  @return    查找失败返回NULL,成功返回device_node类型的结构体指针
*******************************************************************************/
static inline struct device_node *of_find_node_by_type(struct device_node *from,
    const char *type)
{
    return NULL;
}

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

of_find_compatible_node函数 (内核源码/include/linux/of.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/*******************************************************************************
*  @brief     of_find_compatible_node
*  @note      根据节点类型和compatible属性寻找节点
*  @param     from       指定从哪个节点开始查找,如果设置为NULL表示从根节点开始查找
*  @param     type       要查找节点的类型,这个类型就是device_node-> type
*  @param     compat     要查找节点的compatible属性
*  @return    查找失败返回NULL,成功返回device_node类型的结构体指针
*******************************************************************************/
static inline struct device_node *of_find_compatible_node(
                        struct device_node *from,
                        const char *type,
                        const char *compat)
{
    return NULL;
}

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

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

of_find_matching_node_and_match函数 (内核源码/include/linux/of.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/*******************************************************************************
*  @brief     of_find_matching_node_and_match
*  @note      根据匹配表寻找节点
*  @param     from       指定从哪个节点开始查找,如果设置为NULL表示从根节点开始查找
*  @param     matches    of_device_id 匹配表,也就是在此匹配表里面查找节点
*  @param     match      找到的匹配的 of_device_id
*  @return    查找失败返回NULL,成功返回device_node类型的结构体指针
*******************************************************************************/
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)
{
    return NULL;
}
of_device_id结构体(内核源码/include/linux/mod_devicetable.h)
1
2
3
4
5
6
7
8
9
/*
* Struct used for matching a device
*/
struct of_device_id {
    char    name[32];
    char    type[32];
    char    compatible[128];
    const void *data;
};
of_device_id结构体成员

成员

描述

name

节点中属性为name的值

type

节点中属性为device_type的值

compatible

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

data

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

12.3.1.6. 寻找父节点函数

of_get_parent函数 (内核源码/include/linux/of.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/*******************************************************************************
*  @brief     of_get_parent
*  @note      寻找父节点
*  @param     node       指定谁(节点)要查找父节点
*  @return    查找失败返回NULL,成功返回device_node类型的结构体指针
*******************************************************************************/
static inline struct device_node *of_get_parent(const struct device_node *node)
{
    return NULL;
}

12.3.1.7. 寻找子节点函数

of_get_next_child函数 (内核源码/include/linux/of.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/*******************************************************************************
*  @brief     of_get_next_child
*  @note      寻找子节点
*  @param     node       指定谁(节点)要查找它的子节点
*  @param     prev       前一个子节点,寻找的是prev节点之后的节点
*  @return    查找失败返回NULL,成功返回device_node类型的结构体指针
*******************************************************************************/
static inline struct device_node *of_get_next_child(
    const struct device_node *node, struct device_node *prev)
{
    return NULL;
}

注意

参数prev表示前一个子节点。这是一个迭代寻找过程,例如寻找第二个子节点(参数node),参数prev就要填第一个子节点。参数prev为NULL,表示寻找第一个子节点(参数node)。

这里介绍了7个寻找节点函数,这7个函数有一个共同特点——返回值类型相同。

只要找到了节点就会返回节点对应的device_node结构体,在驱动程序中我们就是通过这个device_node获取设备节点的属性信息、顺藤摸瓜查找它的父、子节点等等。

第一函数of_find_node_by_path与后面六个不同,它是通过节点路径寻找节点的,“节点路径”是从设备树源文件(.dts)中得到的。而中间四个函数是根据节点属性在某一个节点之后查找符合要求的设备节点,这个“某一个节点”是设备节点结构体(device_node),也就是说这个节点是已经找到的。最后两个函数与中间四个类似,只不过最后两个没有使用节点属性,而是根据父、子关系查找。

12.3.2. 提取属性值的of函数

上一小节我们讲解了7个查找节点的函数,它们有一个共同特点,找到一个设备节点就会返回这个设备节点对应的结构体指针(device_node*)。

这个过程可以理解为把设备树中的设备节点“获取”到驱动中。“获取”成功后我们再通过一组of函数从设备节点结构体(device_node)中获取我们想要的设备节点属性信息。

12.3.2.1. 查找节点属性函数

of_find_property函数 (内核源码/include/linux/of.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/*******************************************************************************
*  @brief     of_find_property
*  @note      查找节点属性
*  @param     np         指定要获取哪个设备节点的属性信息
*  @param     name       属性名
*  @param     lenp       属性值的长度,单位为字节
*  @return    失败返回NULL,成功返回property类型的结构体指针
*******************************************************************************/
static inline struct property *of_find_property(const struct device_node *np,
                        const char *name,
                        int *lenp)
{
    return NULL;
}

注意

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

property属性结构体(内核源码/include/linux/of.h)
 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
};
property结构体成员

成员

描述

name

属性名

length

属性长度

value

属性值

next

下一个属性

12.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
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*******************************************************************************
*  @brief     of_property_read_u(x)_array
*  @note      读取整型属性(8/16/32/64位)
*  @param     np           指定要读取哪个设备节点结构体
*  @param     propname     指定要获取设备节点的哪个属性(属性名)
*  @param     out_values   读出的属性值(一个输出参数,保存读取得到的数据)
*  @param     sz           一个输入参数,它用于设置读取的长度
*  @return    成功返回0,失败返回-EOVERFLOW/-EINVAL/-ENODATA
*******************************************************************************/
//8位整数读取函数
static inline int of_property_read_u8_array(const struct device_node *np,
            const char *propname, u8 *out_values, size_t sz)
{
    return -ENOSYS;
}
//16位整数读取函数
static inline int of_property_read_u16_array(const struct device_node *np,
            const char *propname, u16 *out_values, size_t sz)
{
    return -ENOSYS;
}
//32位整数读取函数
static inline int of_property_read_u32_array(const struct device_node *np,
                        const char *propname,
                        u32 *out_values, size_t sz)
{
    return -ENOSYS;
}
//64位整数读取函数
static inline int of_property_read_u64_array(const struct device_node *np,
                        const char *propname,
                        u64 *out_values, size_t sz)
{
    return -ENOSYS;
}

返回值:

  • 0 :读取成功;

  • -EINVAL :指定的属性不存在;

  • -ENODATA :没有数据可读;

  • -EOVERFLOW :属性值列表太小;

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

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

of_property_read_uX函数组 (内核源码/include/linux/of.h)
 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
/*******************************************************************************
*  @brief     of_property_read_u(x)
*  @note      读取长度为1的读取整型属性(8/16/32/64位)
*  @param     np           指定要读取哪个设备节点结构体
*  @param     propname     指定要获取设备节点的哪个属性(属性名)
*  @param     out_values   读出的属性值(一个输出参数,保存读取得到的数据)
*  @return    成功返回0,失败返回-EOVERFLOW/-EINVAL/-ENODATA
*******************************************************************************/
//8位整数读取函数
static inline int of_property_read_u8(const struct device_node *np,
                    const char *propname,
                    u8 *out_value)
{
    return of_property_read_u8_array(np, propname, out_value, 1);
}
//16位整数读取函数
static inline int of_property_read_u16(const struct device_node *np,
                    const char *propname,
                    u16 *out_value)
{
    return of_property_read_u16_array(np, propname, out_value, 1);
}
//32位整数读取函数
static inline int of_property_read_u32(const struct device_node *np,
                    const char *propname,
                    u32 *out_value)
{
    return of_property_read_u32_array(np, propname, out_value, 1);
}
//64位整数读取函数
static inline int of_property_read_u64(const struct device_node *np,
                    const char *propname, u64 *out_value)
{
    return -ENOSYS;
}

返回值:

  • 0 :读取成功;

  • -EINVAL :指定的属性不存在;

  • -ENODATA :没有数据可读;

  • -EOVERFLOW :属性值列表太小;

12.3.2.4. 读取字符串属性函数

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

of_property_read_string函数 (内核源码/include/linux/of.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/*******************************************************************************
*  @brief     of_property_read_string
*  @note      读取字符串属性
*  @param     np           指定要读取哪个设备节点结构体
*  @param     propname     指定要获取设备节点的哪个属性(属性名)
*  @param     out_string   读出的字符串
*  @return    成功返回0,失败返回负数
*******************************************************************************/
static inline int of_property_read_string(const struct device_node *np,
                    const char *propname,
                    const char **out_string)
{
    return -ENOSYS;
}

注意

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

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

of_property_read_string_index函数 (内核源码/include/linux/of.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/*******************************************************************************
*  @brief     of_property_read_string_index
*  @note      读取字符串属性(可指定读取哪个字符串)
*  @param     np           指定要读取哪个设备节点结构体
*  @param     propname     指定要获取设备节点的哪个属性(属性名)
*  @param     index        指定读取属性值中第几个字符串
*  @param     output       读出的字符串
*  @return    成功返回0,失败返回负数
*******************************************************************************/
static inline int of_property_read_string_index(const struct device_node *np,
                        const char *propname,
                        int index, const char **output)
{
    int rc = of_property_read_string_helper(np, propname, output, 1, index);
    return rc < 0 ? rc : 0;
}

注意

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

12.3.2.5. 读取布尔型属性函数

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

of_property_read_bool函数 (内核源码/include/linux/of.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/*******************************************************************************
*  @brief     of_property_read_bool
*  @note      读取布尔型属性
*  @param     np           指定要读取哪个设备节点结构体
*  @param     propname     指定要获取设备节点的哪个属性(属性名)
*  @return    true/false
*******************************************************************************/
static inline bool of_property_read_bool(const struct device_node *np,
                    const char *propname)
{
    struct property *prop = of_find_property(np, propname, NULL);

    return prop ? true : false;
}

重要

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

12.3.3. 内存映射相关of函数

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

of_iomap函数 (内核源码/drivers/of/address.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/*******************************************************************************
*  @brief     of_iomap
*  @note      用于将设备节点中定义的内存区域映射到内核的虚拟地址空间
*  @param     np           指定要读取哪个设备节点结构体
*  @param     index        用于指定映射那一段,标号从0开始
*  @return    成功返回一个指向映射后的虚拟地址的指针,失败返回NULL
*******************************************************************************/
void __iomem *of_iomap(struct device_node *np, int index)
{
    struct resource res;

    if (of_address_to_resource(np, index, &res))
        return NULL;

    return ioremap(res.start, resource_size(&res));
}

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

of_address_to_resource函数 (内核源码/drivers/of/address.c)
 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
/*******************************************************************************
*  @brief     of_address_to_resource
*  @note      将设备树中的地址信息转换为resource结构体
*  @param     np           指定要读取哪个设备节点结构体
*  @param     index        指定要转换的地址资源的索引(指定映射那一段,标号从0开始)
*  @param     r            指向resource结构体的指针
*  @return    成功时返回0,失败时返回负数
*******************************************************************************/
int of_address_to_resource(struct device_node *dev, int index,
            struct resource *r)
{
    const __be32    *addrp;
    u64             size;
    unsigned int    flags;
    const char      *name = NULL;

    addrp = of_get_address(dev, index, &size, &flags);
    if (addrp == NULL)
        return -EINVAL;

    /* Get optional "reg-names" property to add a name to a resource */
    of_property_read_string_index(dev, "reg-names", index, &name);

    return __of_address_to_resource(dev, addrp, size, flags, name, r);
}

resource结构体在前面《平台设备》章节已经介绍过了,该结构体主要描述了硬件资源的位置和大小。这个结构体比较简单,很容从中得到获取得到的具体信息。

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