设备树

内核相关文档:

Documentation/devicetree/bindings/

相关约定如下:

dts:device tree source,设备树源文件

dtb:device tree blob,设备树二进制文件, 由dts编译得来

dtc:设备树编译工具

blob:binary large object

一、设备树的规范(dts和dtb)

1. DTS格式

参考文档:官方文档

DTS文件布局(layout)

1
2
3
4
5
6
/dts-v1/;//设备树版本
[memory reservations] // 保留该处的内存(一般这里就是存放设备树文件的地方),内核不会使用该处内存,格式为: /memreserve/ <address> <length>;//address和length均为64位,如:/memreserve/ 0x33f00000 0x100000
/ {
[property definitions]
[child nodes]
};

相关规定:

  • 一般来说一些公共部分会组合起来写成.dtsi文件

  • 普通的.dts文件可以包含这些.dtsi文件(类似C语言)

    1
    #include"XXXX.dtsi"
  • 普通的.dts文件可以通过重写所包含的.dtsi文件中的属性来进行覆盖操作

  • 编译.dts文件为.dtb

    1
    2
    3
    4
    5
    #Linux内核源码下操作
    #编译所有改动过的设备树
    make dtbs
    #编译指定设备树
    make xxxx.dtb
  • 反汇编.dtb文件为.dts

    1
    ./scripts/dtc/dtc -I dtb -O dts -o 输出文件.dts 输入文件.dtb

语法

Devicetree node(设备节点)格式:

1
2
3
4
[label:] node-name[@unit-address] {
[properties definitions]
[child nodes]
};
  • 同一级别下,node-name[@unit-address]不能一样,不同级别可以一样

  • 每个属性后必须加上分号,每个节点大括号后必须加上分号

Property 格式1:

1
[label:] property-name = value;

Property 格式2(没有值):

1
[label:] property-name;

Property取值只有3种:

  • arrays of cells(1个或多个32位数据, 64位数据使用2个32位数据表示)
  • string(字符串)
  • bytestring(1个或多个字节)
相关示例

a. Arrays of cells : cell就是一个32位的数据

1
interrupts = <17 0xc>;

b. 64bit数据使用2个cell来表示:

1
clock-frequency = <0x00000001 0x00000000>;

c. A null-terminated string (有结束符的字符串):

1
compatible = "simple-bus";

d. A bytestring(字节序列) :

1
2
local-mac-address = [00 00 12 34 56 78];  // 每个byte使用2个16进制数来表示
local-mac-address = [000012345678]; // 每个byte使用2个16进制数来表示

e. 可以是各种值的组合, 用逗号隔开:

1
2
compatible = "ns16550", "ns8250";
example = <0xf00f0000 19>, "a strange property format";

特殊以及默认属性

a. /(根节点)

这部分可以理解为通用属性

1
2
3
4
5
6
7
8
9
10
11
#address-cells   // 在它的子节点的reg属性中, 使用多少个u32整数来描述地址(address)
#size-cells // 在它的子节点的reg属性中, 使用多少个u32整数来描述大小(size)

compatible // 兼容性列表。通过定义一系列的字符串, 用来指定内核中哪个machine_desc可以支持本设备
// 即这个板子兼容哪些平台
// 内核会通过该属性找到对应平台的machine_desc结构体初始化该板卡
//(该属性可以有很多个字符串组成,内核会按照先后顺序依次查找,直到找到为止)

model // 描述设备模块信息。可以理解为相同板卡的不同软件版本,即使用的板卡是采用那个版本的配置文件初始化的
// 比如有2款板子配置基本一致, 它们的compatible是一样的
// 那么就通过model来分辨这2款板子

注意:只有在/节点下,compatible中的字符串才需要和machine_desc结构体进行匹配。

在其余节点中,compatible一般用于将设备和驱动绑定起来

b. /memory节点
1
2
device_type = "memory";// 约定俗成,必须加该属性
reg // 根据上层的#address-cells、#size-cells来指定内存的地址、大小
c. /chosen节点
1
bootargs        // 内核command line参数, 跟u-boot中设置的bootargs作用一样
  • 该参数由uboot根据启动参数bootargs自行设置,一般无需自己处理
d. /cpus节点

/cpus节点下有1个或多个cpu子节点, cpu子节点中用reg属性用来标明自己是哪一个cpu,所以 /cpus 中有以下2个属性:

1
2
3
#address-cells   // 在它的子节点(即/cpus/cpu*)的reg属性中, 使用多少个u32整数来描述地址(address)

#size-cells // 在它的子节点(即/cpus/cpu*)的reg属性中, 使用多少个u32整数来描述大小(size),必须设置为 0
e. /cpus/cpu*
1
2
device_type = "cpu";
reg // 表明自己是哪一个cpu

引用其他节点

a. phandle属性

节点中的phandle属性, 它的取值必须是唯一的(不要跟其他的phandle值一样)

1
2
3
4
5
6
7
8
pic@10000000 {
phandle = <1>; //phandle属性必须唯一
interrupt-controller; //该属性表明这是中断控制器
};

another-device-node {
interrupt-parent = <1>; // 引用phandle值为1的节点
};
b. label属性

通过&进行引用,底层原理和phandle属性的方法是一样的,只是phandle属性部分由编译器将替我们完成了

1
2
3
4
5
6
7
8
9
PIC: pic@10000000 {
interrupt-controller; //该属性表明这是中断控制器
};

another-device-node {
interrupt-parent = <&PIC>; // 使用label来引用上述节点,
// 使用lable时实际上也是使用phandle来引用,
// 在编译dts文件为dtb文件时, 编译器dtc会在dtb中插入phandle属性
};

label属性可以在**/节点外**直接引用并修改对应节点中的值

1
2
3
4
5
6
7
8
9
//以下为XXXX.dtsi文件内容:
/ {
......
PIC: pic@10000000 {
pin = <XXX>;
interrupt-controller; //该属性表明这是中断控制器
};
......
};
1
2
3
4
#include"XXXX.dtsi"
&PIC {
pin = <XXXXXXX>;//直接修改
};

2. DTB二进制文件的格式

官方文档

内核文档路径:Documentation/devicetree/booting-without-of.txt

dtb文件是由dts文件编译而来

DTB文件布局

dtb文件以大字节序存储(低地址放高位)。根据官方说明,dtb文件结构如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
        ------------------------------
base -> | struct boot_param_header | //头部信息,包含后几个部分的指针
------------------------------
| (alignment gap) (*) |
------------------------------
| memory reserve map |//保留给自己使用的内存,即dts中格式为: /memreserve/ <address> <length>;的地方
------------------------------
| (alignment gap) |
------------------------------
| |
| device-tree structure |//设备数的二进制文件
| |
------------------------------
| (alignment gap) |
------------------------------
| |
| device-tree strings |//设备数节点中的属性名字字符串
| |
-----> ------------------------------
|
|
--- (base + totalsize)

具体格式可见下图:

DTB格式

3. 通用函数

device_node结构体与property结构体内容可以看下文中 第二章第4节

查找节点

  1. 通过名字查找

    1
    struct device_node *of_find_node_by_name(struct device_node *from, const char *name);
    • from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
    • name:要查找的节点名字(不是table和name属性)。
    • 返回值: 找到的节点,如果为 NULL 表示查找失败。
  2. 通过device_type 属性查找

    1
    struct device_node *of_find_node_by_type(struct device_node *from, const char *type)
    • from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
    • type:要查找的节点对应的 type 字符串,即 device_type 属性值。
    • 返回值: 找到的节点,如果为 NULL 表示查找失败。
  3. 根据 device_type 和 compatible查找

    1
    struct device_node *of_find_compatible_node(struct device_node *from, const char *type, const char *compatible)
    • from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
    • type:要查找的节点对应的 type 字符串,即 device_type 属性值(若为 NULL则表示忽略 device_type 属性)
    • compatible: 要查找的节点所对应的 compatible 属性列表。
    • 返回值: 找到的节点,如果为 NULL 表示查找失败
  4. 通过 of_device_id 匹配表来查找

    1
    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 表示查找失败
  5. 通过路径查找

    1
    inline struct device_node *of_find_node_by_path(const char *path)
    • path:带有全路径的节点名,可以使用节点的别名。
    • 返回值: 找到的节点,如果为 NULL 表示查找失败
  6. 查找指定节点的父节点

    1
    struct device_node *of_get_parent(const struct device_node *node)
    • node:要查找的父节点的节点。
    • 返回值: 找到的父节点。
  7. 查找指定节点的子节点

    1
    struct device_node *of_get_next_child(const struct device_node *node, struct device_node *prev)
    • node:父节点。
    • prev:前一个子节点,也就是从哪一个子节点开始迭代的查找下一个子节点。可以设置为NULL,表示从第一个子节点开始。
    • 返回值: 找到的下一个子节点。

提取属性

  1. 查找节点中的指定属性

    1
    property *of_find_property(const struct device_node *np, const char *name, int *lenp)
    • np:设备节点。
    • name: 属性名字。
    • lenp:属性值的字节数,一般为NULL
    • 返回值: 找到的属性。
  2. 获取属性中元素的数量

    1
    int of_property_count_elems_of_size(const struct device_node *np, const char *propname, int elem_size)
    • np:设备节点。
    • proname: 需要统计元素数量的属性名字。
    • elem_size:每个元素的长度。(如果元素为u32类型则此处填sizeof(u32))
    • 返回值: 得到的属性元素数量。
  3. 从属性中获取指定标号的 u32 类型数据值

    1
    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 表示属性值列表太小。
  4. 读取属性中 u8、 u16、 u32 和 u64 类型的数组数据

    1
    2
    3
    4
    int of_property_read_u8_array(const struct device_node *np, const char *propname, u8 *out_values, size_t sz)
    int of_property_read_u16_array(const struct device_node *np, const char *propname, u16 *out_values, size_t sz)
    int of_property_read_u32_array(const struct device_node *np, const char *propname, u32 *out_values, size_t sz)
    int of_property_read_u64_array(const struct device_node *np, const char *propname, u64 *out_values, size_t sz)
    • np:设备节点。
    • proname: 要读取的属性名字。
    • out_values:读取到的数组值,分别为 u8、 u16、 u32 和 u64。
    • sz: 要读取的数组元素数量。
    • 返回值: 0,读取成功,负值,读取失败, -EINVAL 表示属性不存在, -ENODATA 表示没有要读取的数据, -EOVERFLOW 表示属性值列表太小。
  5. 读取只有一个整形值的属性

    1
    2
    3
    4
    int of_property_read_u8(const struct device_node *np,const char *propname, u8 *out_value)
    int of_property_read_u16(const struct device_node *np, const char *propname, u16 *out_value)
    int of_property_read_u32(const struct device_node *np, const char *propname, u32 *out_value)
    int of_property_read_u64(const struct device_node *np, const char *propname, u64 *out_value)
    • np:设备节点。
    • proname: 要读取的属性名字。
    • out_value:读取到的数组值。
    • 返回值: 0,读取成功,负值,读取失败, -EINVAL 表示属性不存在, -ENODATA 表示没有要读取的数据, -EOVERFLOW 表示属性值列表太小。
  6. 读取属性中字符串值

    1
    int of_property_read_string(struct device_node *np, const char *propname, const char **out_string)
    • np:设备节点。
    • proname: 要读取的属性名字。
    • out_string:读取到的字符串值。
    • 返回值: 0,读取成功,负值,读取失败。
  7. 获取#address-cells 属性值

    1
    int of_n_addr_cells(struct device_node *np)
    • np:设备节点。
    • 返回值: 获取到的#address-cells 属性值。
  8. 获取#size-cells 属性值

    1
    int of_n_size_cells(struct device_node *np)
    • np:设备节点。
    • 返回值: 获取到的#size-cells 属性值。

其他常用函数

  1. 查看节点的 compatible 属性是否有包含指定的字符串

    1
    int of_device_is_compatible(const struct device_node *device, const char *compat)
    • device:设备节点。
    • compat:要查看的字符串。
    • 返回值: 0,节点的 compatible 属性中不包含 compat 指定的字符串; 正数,节点的compatible属性中包含 compat 指定的字符串。
  2. 获取地址相关属性

    主要是“reg”或者“assigned-addresses”属性值

    1
    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 的话表示读取失败。
  3. 将从设备树读取到的地址转换为物理地址

    1
    u64 of_translate_address(struct device_node *dev, const __be32 *in_addr)
    • dev:设备节点。
    • in_addr:要转换的地址。
    • 返回值: 得到的物理地址,如果为 OF_BAD_ADDR 的话表示转换失败。
  4. 从设备树里面提取资源值

    本质上是将 reg 属性值转换为 resource 结构体类型

    1
    int of_address_to_resource(struct device_node *dev, int index, struct resource *r)
    • dev:设备节点。
    • index:地址资源标号。
    • r:得到的 resource 类型的资源值。
    • 返回值: 0,成功;负值,失败。
  5. 直接内存映射(获取内存地址所对应的虚拟地址 )

    本质上是将 reg 属性中地址信息转换为虚拟地址(将原来的先提取属性在映射结合起来),如果 reg 属性有多段的话,可以通过 index 参数指定要完成内存映射的是哪一段

    1
    void __iomem *of_iomap(struct device_node *np, int index)
    • np:设备节点。
    • index: reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为0。(从0开始,一次映射一对,即一个地址一个长度)
    • 返回值: 经过内存映射后的虚拟内存首地址,如果为 NULL 的话表示内存映射失败。

二、内核对设备树的处理

根据内核文档中的描述,内核对设备数的处理大概分为以下三个部分:

  1. platform identification,平台识别信息
  2. runtime configuration,运行时配置信息
  3. device population,设备信息

1. 内核head.S对dtb的简单处理

uboot启动后,会设置r0 r1 r2 三个寄存器,将参数传给linux内核:

  • r0一般设置为0;
  • r1一般设置为machine id (机器ID,在使用设备树时该参数没有被使用);
  • r2一般设置ATAGS或DTB的开始地址

inux内核启动后会运行head.S文件,head.S和head-common.S文件一起将传入的这几个参数又传给了内核中的C语言变量:

  • 把bootloader传来的r1值, 赋给了C变量: __machine_arch_type
  • 把bootloader传来的r2值, 赋给了C变量: __atags_pointer // dtb首地址

之后会调用start_kernel启动内核。

2. 对设备树中平台信息的处理(选择machine_desc结构体)

大概流程如下:

  • a. 设备树根节点的compatible属性列出了一系列的字符串,表示它兼容的单板名,从"最兼容"到次之

  • b. 内核中有多个machine_desc,其中有dt_compat成员, 它指向一个字符串数组, 里面表示该machine_desc支持哪些单板

    没有用设备树的时候,uboot向linux内核传递ATAGS,此时是根据machine_desc.nr成员来匹配机器id是否相等进而选取的

  • c. 使用compatile属性的值,跟每一个machine_desc.dt_compat比较,如若多项均匹配上,compatile属性中越靠前的字符串优先级越高

函数调用过程如下:


  • start_kernel // init/main.c

    • setup_arch(&command_line); (先尝试将__atags_pointer处的数据认为是dtb并进行分析,如若出错就将其当做ATAGS分析)// arch/arm/kernel/setup.c

      • mdesc = setup_machine_fdt(__atags_pointer); // arch/arm/kernel/devtree.c

        • early_init_dt_verify(phys_to_virt(dt_phys) // 检查头部信息,判断是否有效的dtb, drivers/of/ftd.c

          • initial_boot_params = params;//保存在全局变量initial_boot_params中
        • mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach); // 找到最匹配的machine_desc,arch_get_next_mach为一个函数指针,该函数每调用一次就会返回一个machine_desc.dt_compat, drivers/of/ftd.c

          •   while ((data = get_next_compat(&compat))) {
                  score = of_flat_dt_match(dt_root, compat);
                  if (score > 0 && score < best_score) {
                      best_data = data;
                      best_score = score;
                  }
              }
              
            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

            * machine_desc = mdesc;

            ---

            ### 3. 对设备树中运行时配置信息的处理

            大致流程如下:

            * a. /chosen节点中bootargs属性的值, 存入全局变量: boot_command_line
            * b. 确定根节点的这2个属性的值: #address-cells, #size-cells存入全局变量: dt_root_addr_cells, dt_root_size_cells
            * c. 解析/memory中的reg属性, 提取出"base, size", 最终调用memblock_add(base, size);

            函数调用过程如下:

            ---

            * start_kernel // init/main.c

            * setup_arch(&command_line); // arch/arm/kernel/setup.c

            * mdesc = setup_machine_fdt(__atags_pointer); // arch/arm/kernel/devtree.c

            * early_init_dt_scan_nodes(); // drivers/of/ftd.c

            * ```c
            /* Retrieve various information from the /chosen node */
            of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);//将bootargs属性的值, 存入全局变量boot_command_line

            /* Initialize {size,address}-cells info */
            of_scan_flat_dt(early_init_dt_scan_root, NULL);//处理并保存 根节点信息中的#address-cells, #size-cells属性

            /* Setup memory, calling early_init_dt_add_memory_arch */
            of_scan_flat_dt(early_init_dt_scan_memory, NULL);//取出/memory节点reg属性中的"base, size"并告诉内核

4. dtb转换为device_node(unflatten_device_tree函数)

大致过程:

  • a. 在DTB文件中, 每一个节点都以TAG(FDT_BEGIN_NODE, 0x00000001)开始, 节点内部可以嵌套其他节点,每一个属性都以TAG(FDT_PROP, 0x00000003)开始

  • b. 每一个节点最后都会转换为一个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; // 来自节点中的name属性, 如果没有该属性, 则设为"NULL"
    const char *type; // 来自节点中的device_type属性, 如果没有该属性, 则设为"NULL"
    phandle phandle;
    const char *full_name; // 节点的名字, node-name[@unit-address]
    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
    };
  • c. device_node结构体中有properties, 用来表示该节点的属性,每一个属性对应一个property结构体:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    struct property {
    char *name; // 属性名字, 指向dtb文件中的字符串
    int length; // 属性值的长度,按字节计算
    void *value; // 属性值, 指向dtb文件中value所在位置, 数据仍以big endian存储
    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
    };
  • d. 这些device_node构成一棵树, 根节点为: of_root

函数调用过程:


  • start_kernel // init/main.c

    • setup_arch(&command_line); // arch/arm/kernel/setup.c

      • arm_memblock_init(mdesc); // arch/arm/kernel/setup.c

        • early_init_fdt_reserve_self();//将DTB所占区域保留下来, 最后会调用: memblock_reserve
        • early_init_fdt_scan_reserved_mem(); // 根据dtb中的memreserve信息, 调用memblock_reserve保留其他区域
      • unflatten_device_tree(); // arch/arm/kernel/setup.c//开始分配设置device_node结构体

        • __unflatten_device_tree(initial_boot_params, NULL, &of_root,early_init_dt_alloc_memory_arch, false); // drivers/of/fdt.c

          •   /* First pass, scan for size */
              size = unflatten_dt_nodes(blob, NULL, dad, NULL);
              
              /* Allocate memory for the expanded device tree */
              mem = dt_alloc(size + 4, __alignof__(struct device_node));
              
              /* Second pass, do actual unflattening */
              unflatten_dt_nodes(blob, mem, dad, mynodes);
              
            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

            ---

            ### 5. device_node结构体转换为platform_device

            基本流程:dts -> dtb -> device_node -> platform_device

            * 转化为platform_device的只有一部分device_node

            > 条件:
            >
            > 1. 一般来说<u>根节点下</u>包含compatile属性的<u>子节点</u>都会转化为platform_device。而其他子节点不应该转化为platform_device。如:i2c, spi等总线节点下的子节点, 应该交给对应的总线驱动程序来处理, 它们不应该被转换为platform_device
            > 2. 特殊情况:如果一个节点的compatile属性含有这些特殊的值(`simple-bus`、`simple-mfd`、`isa`、`arm,amba-bus`)(由drivers\of\platform.c中的of_default_bus_match_table表定义)之一, 那么它的子节点(也需含compatile属性。详见下面of_platform_bus_create的实现)也可以转换为platform_device
            >
            > 附:
            >
            > 根节点下的iic、spi总线节点一般都会转化为platform_device结构体,这些总线节点下的子节点一般不再会转换为platform_device,而是交给bus的probe函数去注册为i2c_client、spi_device等设备结构体

            转化过程:

            * 利用device_node中的reg, interrupts属性设置platform_device中的`resource`数组
            * 将device_node结构体挂载在`platform_device.dev.of_node`上

            函数调用过程:

            ---

            核心函数:of_platform_default_populate_init

            > 注:
            >
            > 该函数不会直接调用,会通过do_initcall_level(level);函数间接调用,of_platform_default_populate_init函数通过一个宏将其放到了.initcall3s.init段处。do_initcall_level(3)会调用.initcall3s.init段的所有函数。

            调用of_platform_default_populate_init:

            * start_kernel // init/main.c

            * rest_init();

            * pid = kernel_thread(kernel_init, NULL, CLONE_FS);//启动线程kernel_init

            * kernel_init线程中:

            * kernel_init_freeable();

            * do_basic_setup();

            * do_initcalls();

            * ```c
            for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
            do_initcall_level(level); // 比如 do_initcall_level(3)

生成platform_device:

  • of_platform_default_populate_init//遍历所有节点并生成platform_device结构体

    • of_platform_default_populate(NULL, NULL, NULL);

      • of_platform_populate(NULL, of_default_bus_match_table, NULL, NULL)

        •   for_each_child_of_node(root, child) {
                rc = of_platform_bus_create(child, matches, lookup, parent, true);  // 调用过程看下面
                dev = of_device_alloc(np, bus_id, parent);   // 根据device_node节点的属性设置platform_device的resource
                if (rc) {
                    of_node_put(child);
                    break;
                }
            }
            
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18

          of_platform_bus_create创建总线节点(处理bus节点生成platform_devie, 并决定是否处理它的子节点)

          * of_platform_bus_create

          * ```c
          dev = of_platform_device_create_pdata(bus, bus_id, platform_data, parent); // 生成bus节点的platform_device结构体
          if (!dev || !of_match_node(matches, bus)) // 如果bus节点的compatile属性不吻合matches成表, 就不处理它的子节点
          return 0;

          for_each_child_of_node(bus, child) { // 取出每一个子节点
          pr_debug(" create child: %pOF\n", child);
          rc = of_platform_bus_create(child, matches, lookup, &dev->dev, strict); // 处理它的子节点, of_platform_bus_create是一个递归调用
          if (rc) {
          of_node_put(child);
          break;
          }
          }

6. platform_device跟platform_driver的匹配

匹配时通过platform_bus_type的match成员函数实现的,即platform_match

匹配的优先级如下:

  • 比较 platform_dev.driver_override 和 platform_driver.drv->name
  • 比较 platform_dev.dev.of_node的compatible属性 和 platform_driver.drv->of_match_table
  • 比较 platform_dev.name 和 platform_driver.id_table
  • 比较 platform_dev.name 和 platform_driver.drv->name

7. 内核中设备树的操作函数

  • a. 处理DTB
    of_fdt.h // dtb文件的相关操作函数, 我们一般用不到, 因为dtb文件在内核中已经被转换为device_node树(它更易于使用)

  • b. 处理device_node
    of.h // 提供设备树的一般处理函数, 比如 of_property_read_u32(读取某个属性的u32值), of_get_child_count(获取某个device_node的子节点数)
    of_address.h // 地址相关的函数, 比如 of_get_address(获得reg属性中的addr, size值)
    of_match_device(从matches数组中取出与当前设备最匹配的一项)
    of_dma.h // 设备树中DMA相关属性的函数
    of_gpio.h // GPIO相关的函数
    of_graph.h // GPU相关驱动中用到的函数, 从设备树中获得GPU信息
    of_iommu.h // 很少用到
    of_irq.h // 中断相关的函数
    of_mdio.h // MDIO (Ethernet PHY) API
    of_net.h // OF helpers for network devices.
    of_pci.h // PCI相关函数
    of_pdt.h // 很少用到
    of_reserved_mem.h // reserved_mem的相关函数

  • c. 处理设备相关的信息 platform_device
    of_platform.h // 把device_node转换为platform_device时用到的函数,
    // 比如of_device_alloc(根据device_node分配设置platform_device),
    // of_find_device_by_node (根据device_node查找到platform_device),
    // of_platform_bus_probe (处理device_node及它的子节点)
    of_device.h // 设备相关的函数, 比如 of_match_device

8. 在根文件系统中查看设备树(有助于调试)

  • a. /sys/firmware/fdt // 原始dtb文件

    1
    2
    #打印设备数文件(dtb文件)
    hexdump -C /sys/firmware/fdt
  • b. /sys/firmware/devicetree // 以目录结构程现的dtb文件, 根节点对应base目录, 每一个节点对应一个目录, 每一个属性对应一个文件

  • c. /sys/devices/platform // 系统中所有的platform_device, 有来自设备树的, 也有来有.c文件中注册的
    对于来自设备树的platform_device,可以进入 /sys/devices/platform/<设备名>/of_node 查看它的设备树属性

  • d. /proc/device-tree 是链接文件, 指向 /sys/firmware/devicetree/base

三、u-boot对设备树的支持

1. 传递dtb给内核(r2)

  • u-boot中内核启动命令

    1
    2
    bootm <uImage_addr>                            // 无设备树,bootm 0x30007FC0
    bootm <uImage_addr> <initrd_addr> <dtb_addr> // 有设备树

    eg:

    1
    2
    3
    nand read.jffs2 0x30007FC0 kernel;     // 读内核uImage到内存0x30007FC0
    nand read.jffs2 32000000 device_tree; // 读dtb到内存32000000
    bootm 0x30007FC0 - 0x32000000 // 启动, 没有initrd时对应参数写为"-"
  • 设备数的存放地址 dtb_addr 的选取原则

    • 不要破坏u-boot本身
    • 不要挡内核的路: 内核本身的空间不能占用, 内核要用到的内存区域也不能占用内核启动时一般会在它所处位置的下边放置页表, 这块空间(一般是0x4000即16K字节)不能被占用

JZ2440内存使用情况:

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
                   ------------------------------
0x33f80000 ->| u-boot |
------------------------------
| u-boot所使用的内存(栈等)|
------------------------------
| |
| |
| 空闲区域 |
| |
| |
| |
| |
------------------------------
0x30008000 ->| zImage |
------------------------------ uImage = 64字节的头部+zImage
0x30007FC0 ->| uImage头部 |
------------------------------
0x30004000 ->| 内核创建的页表 | head.S
------------------------------
| |
| |
-----> ------------------------------
|
|
--- (内存基址 0x30000000)
  • 正常启动linux:

    1
    2
    3
    nand read.jffs2 30000000 device_tree
    nand read.jffs2 0x30007FC0 kernel
    bootm 0x30007FC0 - 30000000
  • 启动错误:

    1
    2
    3
    4
    #设备数放到内核创建的页表处,会导致linux启动后设备树文件缺失
    nand read.jffs2 30004000 device_tree
    nand read.jffs2 0x30007FC0 kernel
    bootm 0x30007FC0 - 30004000

2. uboot中对dtb文件修改的原理

  • 修改属性的值

    • a. 把原属性val所占空间从len字节扩展为newlen字节,把老值之后的所有内容向后移动(newlen - len)字节
    • b. 把新值写入val所占的newlen字节空间
    • c. 修改dtb头部信息中structure block的长度: size_dt_struct
    • d. 修改dtb头部信息中string block的偏移值: off_dt_strings
    • e. 修改dtb头部信息中的总长度: totalsize
  • 添加一个全新的属性

    • a. 如果在string block中没有这个属性的名字,就在string block尾部添加一个新字符串: 属性的名。并且修改dtb头部信息中string block的长度: size_dt_strings,修改dtb头部信息中的总长度: totalsize
    • b. 找到属性所在节点, 在节点尾部扩展一块空间, 内容及长度为:
      TAG // 4字节, 对应0x00000003
      len // 4字节, 表示属性的val的长度
      nameoff // 4字节, 表示属性名的offset
      val // len字节, 用来存放val
    • c. 修改dtb头部信息中structure block的长度: size_dt_struct
    • d. 修改dtb头部信息中string block的偏移值: off_dt_strings
    • e. 修改dtb头部信息中的总长度: totalsize

3.uboot中dtb修改命令fdt的移植

使用:

  1. 配置交叉编译工具链 gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabi

  2. 解压

    1
    tar xjf u-boot-1.1.6.tar.bz2
  3. 打补丁

    1
    2
    3
    patch -p1 < ../u-boot-1.1.6_device_tree_for_jz2440_add_fdt_20181022.patch   // 打补丁
    make 100ask24x0_config // 配置
    make // 编译, 可以得到u-boot.bin

移植:

高版本中已经存在,无需移植

创建补丁文件

1
diff -urN 原始未修改文件夹 修改后文件夹 > 补丁文件.patch

打补丁

1
patch -p1 < ../补丁文件.patch

四、中断系统中的设备树

1. Linux对中断处理的框架及代码流程简述

如图:

Linux中断处理的框架

2. 中断号的演变与irq_domain

之前的irq

  • 以前中断号(virq)跟硬件密切相关,现在的趋势是中断号跟硬件无关, 仅仅是一个标号而已
  • 以前, 对于每一个硬件中断(hwirq)都预先确定它的中断号(virq),这些中断号一般都写在一个头文件里, 比如arch\arm\mach-s3c24xx\include\mach\irqs.h使用时:
    • 执行 request_irq(virq, my_handler) :内核根据virq可以知道对应的硬件中断, 然后去设置、使能中断等
    • 发生硬件中断时,内核读取硬件信息, 确定hwirq, 反算出virq,然后调用 irq_desc[virq].handle_irq, 最终会用到用户注册的my_handler函数

现在的irq

  • hwirq跟virq之间不再通过代码绑定,而是进行动态绑定:

    • 每一个hwirq会去irq_desc全局表中从hwirq项开始向后查找,直到找到空闲项就进行绑定:

      • 每一个中断控制器都有一个对应的irq_domain(域)
    • 绑定信息保存在irq_domain(域)中

    • irq_domain中有个linear_revmap数组成员用于保存hwirq到virq的关系:

      1
      irq_domain.linear_revmap[hwirq] = virq;
    • 不难看出linear_revmap数组成员中用到的项比较少,大部分为空闲项

    • irq_domain对之前的irq的兼容:

      • irq_domain中有linear_revmap成员用于保存之前irq架构中的hwirq到预先设置好的virq之间的对应关系
      • linear_revmap成员中基本上都使用了,很少的空闲项

例子:子中断控制器中的中断发生:

  1. 进入中断入口,再跳转到C语言入口,执行相应的保存现场等动作
  2. 总中断控制器通过hwirq和irq_domain.linear_revmap查到具体的virq
  3. 通过virq查到全局表中具体的中断函数irq_desc[virq].handle_irq,发现是分发函数(s3c_irq_demux)
  4. 接着查询子中断控制器的hwirq,并找到对应的virq:irq_domain.linear_revmap[hwirq]
  5. 找到新的全局表中的中断函数irq_desc[virq].handle_irq并执行
  6. 该函数是用户通过request_irq(virq, my_handler)向系统注册的用户函数my_handler,真正交给用户
  7. 此时request_irq中的virq不再通过固定文件获得,而是通过设备树中相互约定得来的。

设备树中irq的设置

设备树中应该对中断节点设置对应的中断控制器(决定使用哪个irq_domain)和hwirq(硬件中断号)

设备树中的设置会被irq_domain中的xlate成员函数解析,得到相应的virq和irq_type

irq_domain中的map成员函数会建立hwirq和virq之间的联系(详细分析见后文)

3. 设备树中对中断的描述

ARM中顶层的中断控制器一般为intc: interrupt-controller@xxxxxxxx,其余节点均为其子节点

中断节点

  • interrupts属性:指定要使用的硬件中断号, 中断的触发类型等等,具体格式由对应的中断控制器决定

  • interrupt-parent属性:指定该节点所在的中断控制器节点(若该节点中没有,在该节点的父节点中肯定已经包含)

    • phandle属性指定
    • label属性指定
  • interrupt-extended属性(可选):通过该属性可以解决一个节点产生多个中断的问题,其具体格式为:

    <phandle> <prop-encoded-array>

    其中:

    • <phandle>:具体的发给那个中断控制器

    • <prop-encoded-array>:相关的中断描述,取决于中断控制器中的#interrupt-cells属性

中断控制器节点

官方文档:Documentation/devicetree/bindings/arm/gic.txt

  • interrupt-controller属性:表明自己是中断控制器
  • #interrupt-cells属性:表明对应的子设备里interrupts属性应该用几个u32的数据来描述

对于 ARM 处理的 GIC(通用中断控制器)来说,一般#interrupt-cells为 3,其子节点interrupts属性中cells一般为:

  • 第一个 cells:中断类型, 0 表示 SPI 中断, 1 表示 PPI 中断
  • 第二个 cells:中断号,对于 SPI 中断来说中断号的范围为 0~987,对于 PPI 中断来说中断号的范围为 0~15
  • 第三个 cells:标志, bit[3:0]表示中断触发类型,为 1 的时候表示上升沿触发,为 2 的时候表示下降沿触发,为 4 的时候表示高电平触发,为 8 的时候表示低电平触发。 bit[15:8]为 PPI 中断的 CPU 掩码。
  • interrupt-parent属性(可选):指定该节点所在的中断控制器节点(除去最顶层的intc节点之外,其他所有中断节点中若没有,则在该节点的父节点中肯定已经包含)

4.使用设备树描述按键中断

  • 一般来说,一个节点会生成一个platform_device结构体,通过设备树中的compatible属性与platform_driver驱动结构体中的of_math_table结构体成员进行匹配,匹配上后会调用其下的probe函数,在该函数中会通过platform_get_resource函数来获取到platform_device对应的设备树中定义的相关资源(自定义属性的话需要利用of_XXXX系列函数自己转换),此时需要在这里保存下节点中的中断相关信息,以便于之后使用request_irq函数注册中断

5.内核对设备树中断信息的处理过程

  • 在Linux内核初始化时,会首先创建并初始化总中断控制器中的irq_domain结构体
  • 各个子中断控制器会在各个设备节点所对应的platform_driver驱动结构体中的probe函数中对irq_domain结构体初始化
  • 中断节点中,各个和中断相关的资源均会在内核初始化阶段(具体为of_device_alloc函数中,详见第二章第五节),将其初始化为platform_driver的resource结构体
  • 各种中断数据的解析和处理均利用到了irq_domain->ops->xlate或者irq_domain->ops->map函数,此处不做详解,了解即可

五、实践操作

1. 使用设备树给DM9000网卡_触摸屏指定中断

2. 在设备树中时钟的简单使用

参考文档:

  1. 内核 Documentation/devicetree/bindings/clock/clock-bindings.txt
  2. 内核 Documentation/devicetree/bindings/clock/samsung,s3c2410-clock.txt

Clock providers节点

  • 这种节点属于内核中时钟树的提供者,用于提供时钟给别的模块
  • #clock-cells属性:指定引用该节点的子节点中时钟的参数结构是由几个u32位数据描述的

Clock consumers节点

  • clocks属性:通过该属性指定具体的Clock providers节点和需要用到的时钟信号,其具体格式为:

    <Clock providers节点id> <相关时钟参数>

    其中:

    • <Clock providers节点id>:具体的Clock providers节点,通过lable或phandle属性引用均可

    • <相关时钟参数>:相关的中断描述,取决于Clock providers节点中的#clock-cells属性

以前时钟驱动写法

  1. clk=clk_get(NULL,“时钟名”):通过时钟名字获得相应时钟
  2. clk_prepare_enable(clk):使能相关时钟

用上设备树后

  1. of_count_phandle_with_args函数获取设备树中时钟的个数
  2. of_clk_get函数获取具体的时钟
  3. clk_prepare_enable使能时钟
  4. clk_disable_unprepare禁止时钟

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 确定时钟个数
int nr_pclks = of_count_phandle_with_args(dev->of_node, "clocks",
"#clock-cells");
// 获得时钟
for (i = 0; i < nr_pclks; i++) {
struct clk *clk = of_clk_get(dev->of_node, i);
}

// 使能时钟
clk_prepare_enable(clk);

// 禁止时钟
clk_disable_unprepare(clk);

3. 在设备树中pinctrl的简单使用

参考文档:内核 Documentation/devicetree/bindings/pinctrl/samsung-pinctrl.txt

Bank:以引脚名为依据, 这些引脚分为若干组, 每组称为一个Bank。比如s3c2440里有GPA、GPB、GPC等Bank,每个Bank中有若干个引脚, 比如GPA0,GPA1, …, GPC0, GPC1,…等引脚

Group:以功能为依据, 具有相同功能的引脚称为一个Group。比如s3c2440中串口0的TxD、RxD引脚使用 GPH2,GPH3, 那这2个引脚可以列为一组。比如s3c2440中串口0的流量控制引脚使用 GPH0,GPH1, 那这2个引脚也可以列为一组

State:设备的某种状态, 比如内核自己定义的"default",“init”,“idel”,"sleep"状态。也可以是其他自己定义的状态, 比如串口的"flow_ctrl"状态(使用流量控制)

pinctrl节点

  • 节点中会定义各种pin bank节点,比如s3c2440有GPA,GPB,GPC,…,GPB各种BANK, 每个BANK中有若干引脚

    • bank节点
      • gpio-controller属性:表明是gpio的控制器

      • #gpio-cells属性:以后想使用bank中的引脚时, 需要用该属性来表明用多少个u32数据指定引脚

      • eg:

        1
        2
        3
        4
        5
        6
        7
        8
        pinctrl_0: pinctrl@56000000 {
        reg = <0x56000000 0x1000>;

        gpa: gpa {
        gpio-controller;
        #gpio-cells = <2>; /* 以后想使用gpa bank中的引脚时, 需要2个u32来指定引脚 */
        };
        };
  • pinctrl节点中也会有各种group(组合)子节点

    • group节点(这里以samsung为例,具体属性由每家平台的驱动决定)

      • 属性依平台不一致而有所变化

      • samsung,pins属性:该组内要使用到的gpio

      • samsung,pin-function属性:在对应的gpio控制寄存器中,每个组中引脚对应的复用功能。

      • samsung,pin-val属性:初始电平

      • eg:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        uart0_data: uart0-data {
        samsung,pins = "gph-0", "gph-0";
        samsung,pin-function = <2>; /* 在GPHCON寄存器中gph0,gph1可以设置以下值:
        0 --- 输入功能
        1 --- 输出功能
        2 --- 串口功能
        我们要使用串口功能,
        samsung,pin-function 设置为2
        */
        };
  • 一般来说一个设备节点引用pin group时才有可能涉及到State

    • pinctrl-names属性:定义各种State,可以多个一起使用
  • pinctrl-x属性:每个State都需要一个对应的pinctrl-x属性指定使用哪一个或多个group节点

  • 设备树中若指定了pinctrl节点,在对应的probe函数被调用之前,内核会先"bind pins", 即先绑定、设置引脚

    • 会优先设置"init"状态的引脚

    • 如果没有"init"状态, 则设置"default"状态的引脚

      eg:

      1
      2
      3
      4
      5
      6
      serial@50000000 {
      ......
      pinctrl-names = "default", "sleep"; /* 既是名字, 也称为state(状态) */
      pinctrl-0 = <&uart0_data>;
      pinctrl-1 = <&uart0_sleep>;
      };
      • pinctrl-names中定义了2种state: default 和 sleep
        • default 对应的引脚是: pinctrl-0, 它指定了使用哪些pin group: uart0_data
        • sleep 对应的引脚是: pinctrl-1, 它指定了使用哪些pin group: uart0_sleep