> 文章列表 > Linux驱动开发—设备树开发详解

Linux驱动开发—设备树开发详解

Linux驱动开发—设备树开发详解

设备树开发详解

设备树概念

Device Tree是一种描述硬件的数据结构,以便于操作系统的内核可以管理和使用这些硬件,包括CPU或CPU,内存,总线和其他一些外设。

Linux内核从3.x版本之后开始支持使用设备树,可以实现驱动代码与设备的硬件信息相互的隔离,减少了代码中的耦合性

  • 引入设备树之前:一些与硬件设备相关的具体信息都要写在驱动代码中,如果外设发生相应的变化,那么驱动代码就需要改动

  • 引入设备树之后:通过设备树对硬件信息的抽象,驱动代码只要负责处理逻辑,而关于设备的具体信息存放到设备树文件中。如果只是硬件接口信息的变化而没有驱动逻辑的变化,开发者只需要修改设备树文件信息,不需要改写驱动代码

一、DTS、DTB和DTC

Linux驱动开发—设备树开发详解

  • DTS
    • 设备树源码文件,硬件的相应信息都会写在.dts为后缀的文件中,每一款硬件可以单独写一份xxxx.dts
  • DTSI
    • 对于一些相同的dts配置可以抽象到dtsi文件中,然后可以用include的方式到dts文件
    • 同一芯片可以做一个dtsi,不同的板子不同的dts,然后include同一dtsi
    • 对于同一个节点的设置情况,dts中的配置会覆盖dtsi中的配置
  • DTC
    • dtc是编译dts的工具
  • DTB
    • dts经过dtc编译之后会得到dtb文件,设备树的二进制执行文件
    • dtb通过Bootloader引导程序加载到内核。

二、设备树框架

1.根节点:\\2.设备节点:nodex①节点名称:node②节点地址:node@0, @后面即为地址3.属性:属性名称(Property   name)和属性值(Property value)4.标签
  • “/”是根节点,每个设备树文件只有一个根节点。在设备树文件中会发现有的文件下也有“/”根节点,这两个**“/”根节点的内容会合并成一个根节点。**
  • Linux 内核启动的时会解析设备树中各个节点的信息,并且在根文件系统的/proc/devicetree 目录下根据节点名字创建不同文件夹

三、DTS语法

3.1 dtsi头文件

#include <dt-bindings/input/input.h>
#include "imx6ull.dtsi"

设备树也支持头文件,设备树的头文件扩展名为.dtsi。在.dts 设备树文件中,还可以通过“#include”来引用.h、 .dtsi 和.dts 文件。

3.2 设备节点

  • 设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点

  • 每个节点都通过一些属性信息来描述节点信息,属性就是键—值对

    label: node-name@unit-address
    label:节点标签,方便访问节点:通过&label访问节点,追加节点信息
    node-name:节点名字,为字符串,描述节点功能
    unit-address:设备的地址或寄存器首地址,若某个节点没有地址或者寄存器,可以省略
    
  • 设备树源码中常用的几种数据形式

    1.字符串:  compatible = "arm,cortex-a7";设置 compatible 属性的值为字符串“arm,cortex-a7”
    2.32位无符号整数:reg = <0>; 设置reg属性的值为0
    3.字符串列表:字符串和字符串之间采用“,”隔开
    compatible = "fsl,imx6ull-gpmi-nand", "fsl, imx6ul-gpmi-nand";
    设置属性 compatible 的值为“fsl,imx6ull-gpmi-nand”和“fsl, imx6ul-gpmi-nand”。
    

3.3 属性

  1. compatible属性(兼容属性)

    "manufacturer,model"
    manufacturer:厂商名称
    model:模块对应的驱动名字
    

    例:
    imx6ull-alientekemmc.dts 中 sound 节点是 音频设备节点,采用的欧胜(WOLFSON)出品的 WM8960, sound 节点的 compatible 属性值如下:

    compatible = "fsl,imx6ul-evk-wm8960","fsl,imx-audio-wm8960";
    
    • 属性值有两个,分别为“fsl,imx6ul-evk-wm8960”和“fsl,imx-audio-wm8960”,其中“fsl”表示厂商是飞思卡尔,“imx6ul-evk-wm8960”和“imx-audio-wm8960”表示驱动模块名字。

    • sound这个设备首先使用第一个兼容值在 Linux 内核里面查找,看看能不能找到与之匹配的驱动文件,如果没有找到的话就使用第二个兼容值查。

    • 一般驱动程序文件会有一个 OF 匹配表,此 OF 匹配表保存着一些 compatible 值,如果设备节点的 compatible 属性值和 OF 匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动

    • 在根节点来说Linux 内核会通过根节点的 compoatible 属性查看是否支持此设备,如果支持的话设备就会启动 Linux 内核。如果不支持的话那么这个设备就没法启动 Linux 内核。

  2. model属性
    model 属性值是一个字符串,一般 model 属性描述设备模块信息

  3. status属性
    status 属性和设备状态有关的, status 属性值是字符串,描述设备的状态信息。
    Linux驱动开发—设备树开发详解

  4. #address-cells 和#size-cells 属性

    用于描述子节点的地址信息,reg属性的address 和 length的字长。

    • #address-cells 属性值决定了子节点 reg 属性中地址信息所占用的字长(32 位),
    • #size-cells 属性值决定了子节点 reg 属性中长度信息所占的字长(32 位)。
    • 子节点的地址信息描述来自于父节点的#address-cells 和#size-cells的值,而不是该节点本身的值(当前节点的信息是描述子节点的,自己的信息在父节点里)
    //每个“address length”组合表示一个地址范围,
    //其中 address 是起始地址, length 是地址长度,
    //#address-cells 表明 address 这个数据所占用的字长,
    // #size-cells 表明 length 这个数据所占用的字长.
    reg = <address1 length1 address2 length2 address3 length3……>
    
  5. reg属性
    reg 属性一般用于描述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息, reg 属性的值一般是(address, length)对.

    uart1: serial@02020000 {compatible = "fsl,imx6ul-uart","fsl,imx6q-uart", "fsl,imx21-uart";reg = <0x02020000 0x4000>;interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;clocks = <&clks IMX6UL_CLK_UART1_IPG>,<&clks IMX6UL_CLK_UART1_SERIAL>;clock-names = "ipg", "per";status = "disabled";
    };
    

    uart1 的父节点 aips1: aips-bus@02000000 设置了#address-cells = <1>、 #sizecells = <1>,因此 reg 属性中 address=0x02020000, length=0x4000。都是字长为1.

  6. ranges属性

    • ranges属性值可以为或者按照( child-bus-address , parent-bus-address , length )格式编写的数字

    • ranges 是一个地址映射/转换表, ranges 属性每个项目由子地址、父地址和地址空间长度这三部分组成。

    • 如果 ranges 属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换

    child-bus-address:子总线地址空间的物理地址,由父节点的#address-cells 确定此物理地址所占用的字长
    parent-bus-address: 父总线地址空间的物理地址,同样由父节点的#address-cells 确定此物理地址所占用的字长
    length: 子地址空间的长度,由父节点的#size-cells 确定此地址长度所占用的字长
    
  7. 特殊节点

    根节点“/”中有两个特殊的子节点: aliases 和 chosen

    • aliases

      aliases {can0 = &flexcan1;can1 = &flexcan2;...usbphy0 = &usbphy1;usbphy1 = &usbphy2;
      };
      

      aliases 节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。

      但是,一般会在节点命名的时候会加上 label,然后通过&label来访问节点。

    • chosen
      chosen 不是一个真实的设备, chosen 节点主要是为了 uboot 向 Linux 内核传递数据(bootargs 参数)。

四、OF操作函数

Linux 内核提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_” (称为OF 函数)

4.1 查找节点

Linux 内核使用 device_node 结构体来描述一个节点

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 属性 */struct device_node *parent; /* 父节点 */struct device_node *child; /* 子节点...
}
  1. 通过节点名字查找指定的节点:of_find_node_by_name

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

    from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
    name:要查找的节点名字。
    返回值: 找到的节点,如果为 NULL 表示查找失败。

  2. 通过 device_type 属性查找指定的节点:of_find_node_by_type

    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两个属性查找指定的节点:of_find_compatible_node

    struct device_node *of_find_compatible_node(struct device_node *from,const char *type,const char *compatible)
    

    from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
    type:要查找的节点对应的 type 字符串,device_type 属性值,可以为 NULL
    compatible: 要查找的节点所对应的 compatible 属性列表。
    返回值: 找到的节点,如果为 NULL 表示查找失败

  4. 通过 of_device_id 匹配表来查找指定的节点:of_find_matching_node_and_match

    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. 通过路径来查找指定的节点:of_find_node_by_path

    inline struct device_node *of_find_node_by_path(const char *path)
    

    path:设备树节点中绝对路径的节点名,可以使用节点的别名
    返回值: 找到的节点,如果为 NULL 表示查找失败

4.2 获取属性值

Linux 内核中使用结构体 property 表示属性

struct property {char *name; /* 属性名字 */int length; /* 属性长度 */void *value; /* 属性值 */struct property *next; /* 下一个属性 */unsigned long _flags;unsigned int unique_id;struct bin_attribute attr;
}
  1. 查找指定的属性:of_find_property

    
    property *of_find_property(const struct device_node *np,const char *name,int *lenp)
    

    np:设备节点。
    name: 属性名字。
    lenp:属性值的字节数,一般为NULL
    返回值: 找到的属性。

  2. 获取属性中元素的数量(数组):of_property_count_elems_of_size

    int of_property_count_elems_of_size(const struct device_node *np,const char *propnameint elem_size)
    

    np:设备节点。
    proname: 需要统计元素数量的属性名字。
    elem_size:元素长度。
    返回值: 得到的属性元素数量

  3. 从属性中获取指定标号的 u32 类型数据值:of_property_read_u32_index

    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 类型的数组数据

    of_property_read_u8_array
    of_property_read_u16_array 
    of_property_read_u32_array 
    of_property_read_u64_array 
    int of_property_read_u8_array(const struct device_node *np,const char *propname,u8 *out_values,size_t sz)
    

    np:设备节点。
    proname: 要读取的属性名字。
    out_value:读取到的数组值,分别为 u8、 u16、 u32 和 u64。
    sz: 要读取的数组元素数量。
    返回值: 0:读取成功;
    负值: 读取失败
    -EINVAL 表示属性不存在
    -ENODATA 表示没有要读取的数据
    -EOVERFLOW 表示属性值列表太小

  5. 读取属性中字符串值:of_property_read_string

    int of_property_read_string(struct device_node *np,const char *propname,const char **out_string)
    

    np:设备节点。
    proname: 要读取的属性名字。
    out_string:读取到的字符串值。
    返回值: 0,读取成功,负值,读取失败

  6. 获取 #address-cells 属性值:of_n_addr_cells ,获取 #size-cells 属性值:of_size_cells 。

    int of_n_addr_cells(struct device_node *np)
    int of_n_size_cells(struct device_node *np)
    

    np:设备节点。
    返回值: 获取到的#address-cells 属性值。
    返回值: 获取到的#size-cells 属性值。

  7. 内存映射
    of_iomap 函数用于直接内存映射,前面通过 ioremap 函数来完成物理地址到虚拟地址的映射,采用设备树以后就可以直接通过 of_iomap 函数来获取内存地址所对应的虚拟地址。这样就不用再去先获取reg属性值,再用属性值映射内存

    of_iomap 函数本质上也是将 reg 属性中地址信息转换为虚拟地址,如果 reg 属性有多段的话,可以通过 index 参数指定要完成内存映射的是哪一段, of_iomap 函数原型如下:

    void __iomem *of_iomap(struct device_node *np,  int index)
    

    np:设备节点。
    index: reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为 0。
    返回值: 经过内存映射后的虚拟内存首地址,如果为 NULL 的话表示内存映射失败。

    #if 1/* 1、寄存器地址映射 */IMX6U_CCM_CCGR1 = ioremap(regdata[0], regdata[1]);SW_MUX_GPIO1_IO03 = ioremap(regdata[2], regdata[3]);SW_PAD_GPIO1_IO03 = ioremap(regdata[4], regdata[5]);GPIO1_DR = ioremap(regdata[6], regdata[7]);GPIO1_GDIR = ioremap(regdata[8], regdata[9]);
    #else   //第一对:起始地址+大小 -->映射 这样就不用获取reg的值IMX6U_CCM_CCGR1 = of_iomap(dtsled.nd, 0); SW_MUX_GPIO1_IO03 = of_iomap(dtsled.nd, 1);SW_PAD_GPIO1_IO03 = of_iomap(dtsled.nd, 2);GPIO1_DR = of_iomap(dtsled.nd, 3);GPIO1_GDIR = of_iomap(dtsled.nd, 4);
    #endif