Linux 设备树小记

记录一些设备树的关键知识

Posted by Jerry Chen on September 24, 2020

设备树是设备的纯粹描述,不会去改变设备的状态,也不能设置设备初值。应该由驱动程序读取这些描述信息进而控制设备。

先导知识

简述启动过程

设备树 dtb 由 kernel 工程编译生成;

在启动 uboot 时传入 dtb 地址,由 uboot 解析后获取存储器信息以进行系统初始化,接着 uboot 会传递 dtb 地址给 kernel;

设备树源文件

源文件有 dts、dtsi 等,dts 可以引用 dtsi 文件,最后由 dtc 编译生成 dtb 文件;dts/dtsi 兼容c语言的一些语法,能使用宏定义,也能包含 .h 文件;

源文件路径: linux-3.10/arch/arm/boot/dts/xxx.dts

文档路径: linux-3.10/Documentation/devicetree/bindings

BSP 中设备树的属性和子节点含义必须存档在 binding 文档中,当我们发现设备树中的一些属性不能理解时,在该目录下查看相应的文档都能找到答案。

设备树原理

linux3.x 之前我们需要为每一个设备编写设备模块和驱动模块,并将它们注册到系统,如果两者 compatible 字段匹配,就可以正常关联;

现在只需要提供一份包含设备信息的设备树文件,系统会遍历树中的节点,将设备信息注册到系统(还未生成设备),此时已经可用于匹配;在驱动中我们通过 of 系列 API 从设备树中读取配置并使用,一般也在驱动中生成设备。of API 见附录;

最终的结果是,驱动匹配后会在 /sys 目录下创建关于设备和驱动的虚拟文件。通常 i2c、spi、usb 等总线节点会生成到 platform 总线下,总线下的设备会生成到对应总线下。可以在 drivers 目录下搜索设备树中的 compatible 字段,查看驱动的具体实现。

#address-cells 、#size-cells、reg 三者的关系

格式如下:

父节点的 #address-cells 和 #size-cells 决定子节点的 reg 的 address 和 lenth 字段的长度,cell 的单位为 32bit;

reg 的长度为 (address-cells+size-cells) 的倍数;

1
2
3
4
5
6
#address-cells // 子节点 reg 的 address 为 n1 个 32bit 的整型数据
#size-cells   // 长度为 n2 个 32bit 整型数据,如果为 0,则没有 lenth 字段

node{
      reg = <address1 lenth1 [address2 lenth2] [address3 lenth3]...>;
}

示例1:

有两块内存:

第一块起始地址 0x30000000,大小 0x04000000;

第二块内存起始地址 0x00000000 大小 4096;

1
2
3
4
5
6
#address-cells = <1>;
#size-cells = <1>;

memory {
    reg = <0x30000000 0x04000000 0x00000000 4096>;
};

示例2:

有一块内存:

地址范围:0x4000000~0x8000000,多个 32bit 位表示时使用大端模式;

1
2
3
4
5
6
7
#address-cells = <2>;
#size-cells = <2>;

memory@40000000 {
    device_type = "memory";
    reg = <0x0 0x40000000 0x0 0x40000000>;
};

预定义的属性

  • “compatible” 属性用来匹配驱动,一般用 ”厂商芯片,控制器型号” 表示;

  • “model” 属性用来表示设备的型号,填写主板名,整个 dts 中只需要一个;

  • “#address-cells”、”#size-cells”、”reg”、”ranges”、”dma-ranges” 属性都是和地址有关的;

  • “reg” 属性用来表示节点地址资源的,格式是二元组(地址,大小),常见的就是寄存器的起始地址及大小;

  • ‘ranges’ 属性是 cpu 使用的地址,格式是三元组(子总线地址,父总线地址,大小);

  • ‘dma-ranges’ 属性是 dma 使用的地址,格式是三元组(子总线地址,父总线地址,大小);

  • ‘phandle’ 属性是专门为方便引用节点设计的;

    1
    2
    3
    4
    5
    6
    7
    
    intc1:intrxxx{
    	phandle = <1>
    }
      
    //两种引用方法
    interrupt-parent = <1>      //引用 phandle 值
    interrupt-parent = <&intc1> //引用节点标签
    
  • ‘status’ 属性用来表示节点的状态的,常用 okay 和 disabled;

设备树框架

第一部分:根和内存

cpus 和 memory 节点是必要的;这里 cpus 节点未指出;memory 节点描述 RAM 信息;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/ {
    model = "主板名";
    compatible = "SOC名"; //必选,用于匹配BSP中的支持的SOC列表,以判断内核是否支持该主板

    // platform总线
    #address-cells = <A>; //必选, 地址 cell 数量(单位 cell 为 32bit)
    #size-cells = <S>;    //必选, 空间大小 cell 位数(单位 cell 为 32bit)

    // 内存
    memory {
        device_type = "memory";    //device_type值一定为"memory"
        reg = <address size......> //必选, 描述地址范围
    };
};

第二部分:chosen

1
2
3
4
5
6
7
8
/{
    ...
    chosen {
        bootargs = "内核启动参数";       //必选
        linux,stdout-path = "console"  //可选
    };
    ...
};

第三部分:设备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
标号:设备类型@地址 {                                          // 设备名字, 基本名通常为设备类型, 扩展名通常为设备在总线上的地址
    name = "设备(总线)类型";            // 可选,name属性值必须和基本名一致
    device_type = "芯片名";             // 可选,设备采用芯片名
    compatible = "兼容芯片","兼容产品";  // 必选, 用于匹配驱动

    // io内存(寄存器)地址
    #address-cells = <A>;               // 可选, 同"根和内存"相同名称属性
    #size-cells = <S>;                  // 可选, 同"根和内存"相同名称属性
    reg = <地址(端口号) 大小......>;     // 可选, 同"根和内存"相同名称属性

    // 中断(详细参考 Documentation/devicetree/bindings/interrupt-controller/interrupts.txt 文档)
    // 只有一个中断父设备时(必选)
    interrupt-parent = <&父设备标号>;
    interrupts = <irqno trigger>, <......>;

    // 有多个中断父设备时(必选)
    interrupts-extended = <&父设备标号 irqno trigger>, <.........>;
};
设备和驱动匹配的条件
  1. 匹配设备树上的设备

    驱动 设备树 匹配条件
    compatible compatible 必须
    type device_type 驱动存在 type 属性时
    name name 驱动存在 name 属性时
  2. 匹配代码添加的设备

    驱动 设备 匹配条件
    name name 必须
设备中断(非中断控制器)

interrupt-parent、interrupts 的组合或 interrupts-extended 在整个设备树中只允许出现一次;

  1. 父设备(中断控制器)中的中断编号 irqno

  2. 触发条件 trigger,目前只用到 32bit 中的 [0:3]

    (bit0) 1 = low-to-high edge triggered

    (bit1) 2 = high-to-low edge triggered

    (bit2) 4 = active high level-sensitive

    (bit3) 8 = active low level-sensitive

第四部分:中断控制器

1
2
3
4
5
6
7
8
9
10
11
12
标号:设备类型@地址 {
    ...
    interrupt-controller;                   // 必选
    interrupt-parent = <&父设备标号>;        // 必选

    #interrupt-cells = < 1>;                // 必选
    interrupts = <irqno>, <...>;            // 必选
    或
    #interrupt-cells = < 2>;                // 必选
    interrupts = <irqno trigger>, <......>; // 必选
    ...
};

第五部分:[可选]别名

1
2
3
aliases {
	serial0 = &uart1;
};

附录

设备树的操作接口源文件

linux-3.10/include/linux/ 目录下有一些 of 开头的文件,这些是设备树操作的接口头文件:

文件名 功能描述
of.h 提供设备树的一般处理函数,大部分的功能函数都在该文件中
of_address.h 地址相关的函数, 比如 of_get_address(获得reg属性中的addr, size值)
of_device.h 设备相关的函数, 比如 of_match_device、of_device_register
of_dma.h 设备树中DMA相关属性的函数
of_fdt.h dtb文件的相关操作函数,我们一般不使用
of_gpio.h GPIO相关的函数
of_graph.h GPU相关驱动中用到的函数, 从设备树中获得GPU信息
of_iommu.h IOMMU相关函数
of_irq.h 中断相关的函数
of_mdio.h mdio相关的函数
of_mtd.h mtd相关的函数
of_net.h net相关的函数
of_pci.h pci相关的函数
of_platform.h 把device_node转换为platform_device时用到的函数
of_reserved_mem.h reserved_mem的相关函数

一般设备树操作接口

  1. 用来查找在 dtb 中的根节点

    1
    
    unsigned long __init of_get_flat_dt_root(void);
    
  2. 根据 deice_node 结构的 full_name 参数,在全局链表 of_allnodes 中,查找合适的 device_node

    1
    
    struct device_node *of_find_node_by_path(const char *path);
    

    示例:

    1
    2
    
    struct device_node *cpus;
    cpus=of_find_node_by_path("/cpus");
    
  3. 若 from=NULL,则在全局链表 of_allnodes 中根据 name 查找合适的 device_node

    1
    
    struct device_node *of_find_node_by_name(struct device_node *from,const char *name);
    

    示例:

    1
    2
    
    struct device_node *np;
    np = of_find_node_by_name(NULL,"firewire");
    
  4. 根据设备类型查找相应的 device_node

    1
    
    struct device_node *of_find_node_by_type(struct device_node *from,const char *type);
    

    示例:

    1
    2
    
    struct device_node *tsi_pci;
    tsi_pci= of_find_node_by_type(NULL,"pci");
    
  5. 根据 compatible 字符串查找 device_node

    1
    
    struct device_node *of_find_compatible_node(struct device_node *from,const char *type, const char *compatible);
    
  6. 根据节点属性的 name 查找 device_node

    1
    
    struct device_node *of_find_node_with_property(struct device_node *from,const char *prop_name);
    
  7. 根据 phandle 查找 device_node

    1
    
    struct device_node *of_find_node_by_phandle(phandle handle);
    
  8. 根据 alias 的 name 获得设备 id 号

    1
    
    int of_alias_get_id(struct device_node *np, const char *stem);
    
  9. device node 计数增加/减少

    1
    2
    
    struct device_node *of_node_get(struct device_node *node);
    void of_node_put(struct device_node *node);
    
  10. 根据 property 结构的 name 参数,在指定的 device node 中查找合适的 property

    1
    
    struct property *of_find_property(const struct device_node *np,const char *name,int *lenp);
    
  11. 根据 property 结构的 name 参数,返回该属性的属性值

    1
    
    const void *of_get_property(const struct device_node *np, const char *name,int *lenp);
    
  12. 根据 compat 参数与 device node 的 compatible 匹配,返回匹配度

    1
    
    int of_device_is_compatible(const struct device_node *device,const char *compat);
    
  13. 获得父节点的 device node

    1
    
    struct device_node *of_get_parent(const struct device_node *node);
    
  14. 将 matches 数组中 of_device_id 结构的 name 和 type 与 device node 的 compatible 和 type 匹配,返回匹配度最高的 of_device_id 结构

    1
    
    const struct of_device_id *of_match_node(const struct of_device_id *matches,const struct device_node *node);
    
  15. 根据属性名 propname,读出属性值中的第 index 个 u32 数值给 out_value

    1
    
    int of_property_read_u32_index(const struct device_node *np,const char *propname,u32 index, u32 *out_value);
    
  16. 根据属性名 propname,读出该属性的数组中 sz 个属性值给 out_values

    1
    2
    3
    
    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);
    
  17. 根据属性名 propname,读出该属性的 u64 属性值

    1
    
    int of_property_read_u64(const struct device_node *np, const char *propname,u64 *out_value);
    
  18. 根据属性名 propname,读出该属性的字符串属性值

    1
    
    int of_property_read_string(struct device_node *np, const char *propname,const char **out_string);
    
  19. 根据属性名 propname,读出该字符串属性值数组中的第 index 个字符串

    1
    
    int of_property_read_string_index(struct device_node *np, const char *propname,int index, const char **output);
    
  20. 读取属性名 propname 中,字符串属性值的个数

    1
    
    int of_property_count_strings(struct device_node *np, const char *propname);
    
  21. 读取该设备的第 index 个 irq 号

    1
    
    unsigned int irq_of_parse_and_map(struct device_node *dev, int index);
    
  22. 读取该设备的第 index 个 irq 号,并填充一个 irq 资源结构体

    1
    
    int of_irq_to_resource(struct device_node *dev, int index, struct resource *r);
    
  23. 获取该设备的 irq 个数

    1
    
    int of_irq_count(struct device_node *dev);
    
  24. 根据 device_node 查找返回该设备对应的 platform_device 结构

    1
    
    struct platform_device *of_find_device_by_node(struct device_node *np);
    
  25. 根据 device node,bus id 以及父节点创建该设备的 platform_device 结构

    1
    2
    
    struct platform_device *of_device_alloc(struct device_node *np, const char *bus_id, struct device *parent);
    static struct platform_device *of_platform_device_create_pdata(struct device_node *np, const char *bus_id, void *platform_data, struct device *parent);
    
  26. 遍历 of_allnodes 中的节点挂接到 of_platform_bus_type 总线上,由于此时 of_platform_bus_type 总线上还没有驱动,所以此时不进行匹配

    1
    
    int of_platform_bus_probe(struct device_node *root,const struct of_device_id *matches,struct device *parent);
    
  27. 遍历 of_allnodes 中的所有节点,生成并初始化 platform_device 结构

    1
    
    int of_platform_populate(struct device_node *root, const struct of_device_id *matches, const struct of_dev_auxdata *lookup, struct device *parent);