设备树简介
我们前面介绍过平台设备驱动,知道硬件资源信息可以放在设备中,然后在驱动的probe函数中从设备中获取资源信息。但是,Linux3.x以后的版本引入了设备树,设备树用于描述一个硬件平台的硬件资源,一般描述那些不能动态探测到的设备,可以被动态探测到的设备是不需要描述。设备树可以被bootloader(uboot)传递到内核,内核可以从设备树中获取硬件信息。
设备树描述硬件资源时有两个特点:
- 以树状结构描述硬件资源。
- 设备树源文件可以像头文件(.h文件)那样,一个设备树文件引用用一个设备树文件,这样可以实现代码的重用。例如多个硬件平台都使用rk系列处理器作为主控芯片, 那么我们可以将rk系列芯片的硬件资源写到一个单独的设备树文件里面一般使用
.dtsi
后缀, 其他板级设备树文件直接使用# include "xxx.dtsi"
引用即可。
设备树框架
设备树由一系列被命名的节点(node)和属性(property)组成。
/dts-v1/; //需要的DTS 文件版本说明
#include "example.dtsi"; //包含头文件 可以是.dtsi .dts .h文件等
/ { //根节点 node1-name@unit-address { //节点1, 名称是"node1-name", 单元地址和reg属性的第一个地址一致 compatible = "xxx, xxxx"; a-string-property = "A string"; //节点属性和属性值, 是字符串 a-string-list-property = "first string", "second string"; a-byte-data-property = <0x00 0x13 0x24 0x36>; label:child-node1 { //节点1的子节点名 "label" 是标签 first-child-property; second-child-property = <1>; a-string-property = "Hello, world"; }; child-node2 { //子节点2 }; }; node2-name { //节点2 an-empty-property; a-cell-property = <1 2 3 4>; /* each number (cell) is a uint32 */ node1 { //节点1的子节点 my-cousin = <&cousin>; }; };
};
设备节点命名
设备节点的命名方式有三种。
node-name{};
: node-name是结点名称。node-name@unit-address{};
:其中的符号@
可以理解为是一个分割符,unit-address
用于指定“单元地址”, 它的值要和节点reg
属性的第一个地址一致。如果节点没有reg
属性值,可以直接省略@unit-address
。同级别的子节点的node-name
可以相同,但是要求unit-address
不同。label: node-name@unit-address{};
:label是节点标签,可以使用&label快捷地访问节点。
注意,根节点没有节点名,它直接使用“/”指代这是一个根节点。
特殊的设备节点
aliases
节点:用于保存其他节点的别名。chosen
节点:该节点并不是一个真的设备,它的主要功能是帮助uboot向内核传递数据,最主要的参数是bootargs参数。
追加/修改节点内容
&cpu0 {cpu-supply = <&vdd_cpu>;
};
这些源码并不包含在根节点“/{…}”内,它们不是一个新的节点,而是向原有节点追加内容。 以上方源码为例,&cpu0
表示向节点标签为cpu0
的节点追加数据, 这个节点可能定义在本文件也可能定义在本文件所包含的设备树文件中。
节点路径
通过指定从根节点到所需节点的完整路径,可以唯一地标识设备树中的节点, 不同层次的设备树节点名字可以相同,同层次的设备树节点要唯一。 这有点类似于我们Windows上的文件,一个路径唯一标识一个文件或文件夹,不同目录下的文件文件名可以相同。例如前面节点的结构参考图中, 节点node1-name
的子节点child-node1
,节点路径就是 /node1-name/child-node1
。
设备树节点的标准属性
节点属性分为标准属性和自定义属性,也就是说我们在设备树中可以根据自己的实际需要定义、添加设备属性。 标准属性的属性名是固定的,自定义属性名可按照要求自行定义。
-
根节点的
compatible
属性(字符串):用于标识设备树能否与Linux内核匹配,该属性值的一半格式为"厂商,板子名称"
。 -
普通节点的
compatible
属性(字符串):指兼容性。该属性值的一般格式为"厂商,设备驱动名"
。如果Linux内核中的匹配表中有与compatible属性中的值相同的值,则该Linux内核可以使用该设备驱动。当驱动的兼容性信息与设备树的compatible属性匹配后,会运行驱动代码里的probe函数。 -
status
属性(字符串):标识设备可用(“okay”)还是不可用(“disabled”)。状态值 描述 okay 使能设备 disabled 禁用设备 fail 表示设备不可运行,目前驱动不支持,待修复。 fail-sss 表示设备不可运行,目前驱动不支持,待修复。“sss”的值与具体的设备相关。 -
#address-cells
和#size-cells
属性(u32):#address-cells和 #size-cells属性同时存在,用于标明该如何编写reg属性值。#address-cells用于标明reg属性中address所占字长数,size-cells用于标明length所占的字长数。 -
reg
属性:该属性的格式一般为reg = <address,length, address,length,…>
,address
表示其实地址,length
表示地址长度,一般用于内存中(也可以用于其他设备)。
#address-cells = <2>; //起始地址占两个字长
#size-cells = <1>; //地址长度占一个字长
reg = <0x400080,0x600040,0x4000>; //表示0x400080和0x600040是起始地址,地址长度为0x4000
获取设备树节点信息
内核提供了一组函数用于从设备节点获取资源(设备节点中定义的属性)的函数,这些函数以of_开头,称为OF操作函数。
查找节点函数
根据节点路径寻找节点
of_find_node_by_path函数 (内核源码/include/linux/of.h)
参数:path: 指定节点在设备树中的路径。
返回值:device_node: 结构体指针,如果查找失败则返回NULL,否则返回device_node类型的结构体指针,它保存着设备节点的信息。struct device_node *of_find_node_by_path(const char *path)
根据节点名字寻找节点
of_find_node_by_name函数 (内核源码/include/linux/of.h)
参数:from: 指定从哪个节点开始查找,它本身并不在查找行列中,只查找它后面的节点,如果设置为NULL表示从根节点开始查找。name: 要寻找的节点名。
返回值:device_node: 结构体指针,如果查找失败则返回NULL,否则返回device_node类型的结构体指针,它保存着设备节点的信息。struct device_node *of_find_node_by_name(struct device_node *from,const char *name);
提取属性值的of函数
上一小节我们讲解了查找节点的函数,它们有一个共同特点,找到一个设备节点就会返回这个设备节点对应的结构体指针(device_node*)。这个过程可以理解为把设备树中的设备节点“获取”到驱动中。“获取”成功后我们再通过一组of函数从设备节点结构体(device_node)中获取我们想要的设备节点属性信息。
查找节点属性函数
of_find_property函数 (内核源码/include/linux/of.h)
参数:np: 指定要获取那个设备节点的属性信息。name: 属性名。lenp: 获取得到的属性值的大小,这个指针作为输出参数,这个参数“带回”的值是实际获取得到的属性大小。
返回值:property: 获取得到的属性。property结构体,我们把它称为节点属性结构体,如下所示。失败返回NULL。从这个结构体中我们就可以得到想要的属性值了。struct property *of_find_property(const struct device_node *np,const char *name,int *lenp)
读取整型属性函数
of_property_read_uX_array函数组 (内核源码/include/linux/of.h)
参数:np: 指定要读取那个设备节点结构体,也就是说读取那个设备节点的数据。 propname: 指定要获取设备节点的哪个属性。out_values: 这是一个输出参数,是函数的“返回值”,保存读取得到的数据。sz: 这是一个输入参数,它用于设置读取的长度。
返回值:返回值,成功返回0,错误返回错误状态码(非零值),-EINVAL(属性不存在),-ENODATA(没有要读取的数据),-EOVERFLOW(属性值列表太小)。//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)
简化后的读取整型属性函数
这里的函数是对读取整型属性函数的简单封装,将读取长度设置为1。用法与读取属性函数完全一致,这里不再赘述。
//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)
读取字符串属性函数
参数:np: 指定要获取那个设备节点的属性信息。propname: 属性名。 out_string: 获取得到字符串指针,这是一个“输出”参数,带回一个字符串指针。也就是字符串属性值的首地址。这个地址是“属性值”在内存中的真实位置,也就是说我们可以通过对地址操作获取整个字符串属性(一个字符串属性可能包含多个字符串,这些字符串在内存中连续存储,使用’0’分隔)。
返回值:返回值:成功返回0,失败返回错误状态码。int of_property_read_string(const struct device_node *np,const char *propname,const char **out_string)int of_property_read_string_index(const struct device_node *np,const char *propname, int index,const char **out_string)
相比前面的函数增加了参数index,它用于指定读取属性值中第几个字符串,index从零开始计数。 第一个函数只能得到属性值所在地址,也就是第一个字符串的地址,其他字符串需要我们手动修改移动地址,非常麻烦,推荐使用第二个函数。
读取布尔型属性函数
static inline bool of_property_read_bool(const struct device_node *np, const char *propname);
参数:
- np: 指定要获取那个设备节点的属性信息。
- propname: 属性名。
返回值:
这个函数不按套路出牌,它不是读取某个布尔型属性的值,仅仅是读取这个属性存在或者不存在。如果想要或取值,可以使用之前讲解的“全能”函数查找节点属性函数of_find_property。
内存映射相关of函数
在设备树的设备节点中大多会包含一些内存相关的属性,比如常用的reg属性。通常情况下,得到寄存器地址之后我们还要通过ioremap函数将物理地址转化为虚拟地址。现在内核提供了of函数,自动完成物理地址到虚拟地址的转换。
of_iomap函数 (内核源码/drivers/of/address.c)参数:np: 指定要获取那个设备节点的属性信息。index: 通常情况下reg属性包含多段,index 用于指定映射那一段,标号从0开始。
返回值:成功,得到转换得到的地址。失败返回NULL。void __iomem *of_iomap(struct device_node *np, int index)
内核也提供了常规获取地址的of函数,这些函数得到的值就是我们在设备树中设置的地址值。介绍如下
参数:np: 指定要获取那个设备节点的属性信息。index: 通常情况下reg属性包含多段,index 用于指定映射那一段,标号从0开始。r: 这是一个resource结构体,是“输出参数”用于返回得到的地址信息。
返回值:成功返回0,失败返回错误状态码。int of_address_to_resource(struct device_node *np, int index, struct resource *r)
示例
我们使用的开发板,有配套的SDK,在编译脚本配置中,我们可以知道使用的DTS为哪个。
其中.dtb
文件为.dts
文件编译成功后生成的二进制文件,类似于.c
文件编译生成.bin
文件。如果我们要定义自己的节点和属性,有两种方式
- 在已有的dts文件中直接添加我们自己的节点。
- 新建我们自己的dts文件,自己定义节点,然后在平台dts中include我们自己创建的dts文件。
显然,第二种方式更有利于我们的维护。我们新建一个文件haptics.dts
,文件内容如下:
/{node1-name@6B { /*节点1, 名称是"node1-name", 单元地址和reg属性的第一个地址一致 */compatible = "xxx, xxxx"; string-property = "a string";string-list-property = "first string", "second string"; byte-data-property = <0x00 0x13 0x24 0x36>;label:child-node1 { second-child-property = <1>; a-string-property = "Hello, world"; };};child-node2 { second-child-property = <1>; a-string-property = "Hello, world"; };
};
// SPDX-License-Identifier: (GPL-2.0+ OR MIT)
/** Copyright (c) 2021 Rockchip Electronics Co., Ltd.**//dts-v1/;#include "rk3588-9tripod-i3588.dtsi"
//#include "rk3588-9tripod-i3588-ov13850-dcphy0.dtsi"
#include "rk3588-9tripod-i3588-ov13855-dcphy0.dtsi"
//#include "rk3588-9tripod-i3588-ov13855-dcphy1.dtsi"
#include "rk3588-9tripod-i3588-imx415-dphy0.dtsi"
//#include "rk3588-9tripod-i3588-imx415-dphy1.dtsi"
#include "rk3588-9tripod-i3588-linux.dtsi"#include "haptics.dts"/ {model = "9Tripod I3588 Board";compatible = "9tripod,rk3588-9tripod-i3588", "rockchip,rk3588";
};
然后,我们还需要在我们SDK中使用的dts文件中include我们自己新建的这个文件。最后编译kernel,DTS文件是和kernel一起编译的,然后烧录,如果编译过程中出现DTS的问题,可自行解决。
设备树中的设备节点在文件系统中有与之对应的文件,位于/proc/device-tree
目录。
可以找到我们自己创建的节点,这里展现的应该都是根节点下的子节点或熟悉
,我们可以使用cat命令
获取一些属性的信息,或者进行节点目录中获取更多的信息。
那我们怎么在驱动中获取DTS信息呢,就需要我们上述介绍的接口。我们一般是在probe函数中进行DTS解析。
static void haptic_parse_dts(struct platform_device *pdev)
{int ret = 0;struct device_node* node1=NULL;struct device_node* child_node1=NULL;const char *out_string=NULL;uint32_t out_bytes[4]={0};node1 = of_find_node_by_path("/node1-name@6B");if(node1 == NULL){printk("/node1-name@6B is NULL\n");return;}printk("node1->name = %s\n",node1->name);ret = of_property_read_string(node1,"compatible",&out_string);if(0 != ret){printk("property compatible is NULL\n");return;}printk("compatible = %s\n",out_string);ret = of_property_read_string_index(node1,"string-list-property",0,&out_string);if(0 != ret){printk("string-list-property[0] is NULL\n");return;}printk("string-list-property[0] = %s\n",out_string);ret = of_property_read_string_index(node1,"string-list-property",1,&out_string);if(0 != ret){printk("string-list-property[1] is NULL\n");return;}printk("string-list-property[1] = %s\n",out_string);ret = of_property_read_u32_array(node1,"byte-data-property",out_bytes,ARRAY_SIZE(out_bytes));if(0 != ret){printk("byte-data-property is NULL\n");return;}printk("byte-data-property = 0x%02x 0x%02x 0x%02x 0x%02x\n",out_bytes[0],out_bytes[1],out_bytes[2],out_bytes[3]);child_node1 = of_find_node_by_name(node1,"child-node1");if(child_node1 == NULL){printk("/node1-name@6B/child-node1 is NULL\n");return;}printk("child_node1->name = %s\n",child_node1->name);ret = of_property_read_u32(child_node1,"second-child-property",out_bytes);if(0 != ret){printk("second-child-property is NULL\n");return;}printk("second-child-property= %d\n",out_bytes[0]);}static int haptic_drv_probe(struct platform_device *pdev)
{int ret = 0;int* status=NULL;struct resource* res0=NULL;struct resource* res1=NULL;haptic_miscdev_t *hap_miscdev=NULL;struct file_operations *haptics_fops=NULL;res0 = platform_get_resource(pdev,IORESOURCE_MEM,0);res1 = platform_get_resource(pdev,IORESOURCE_MEM,1);printk("res0 start=%d size=%d\n",(int)res0->start,(int)(res0->end-res0->start+1));printk("res1 start=%d size=%d\n",(int)res1->start,(int)(res1->end-res1->start+1));status = dev_get_platdata(&pdev->dev);printk("status=%d\n",*status);haptics_fops = devm_kzalloc(&pdev->dev,sizeof(struct file_operations), GFP_KERNEL);haptics_fops->open = haptics_open;haptics_fops->release = haptics_release;haptics_fops->unlocked_ioctl = haptics_ioctl;hap_miscdev = devm_kzalloc(&pdev->dev,sizeof(haptic_miscdev_t), GFP_KERNEL);hap_miscdev->res = res0;hap_miscdev->status = *status;hap_miscdev->miscdev.name = pdev->name;hap_miscdev->miscdev.fops = haptics_fops;hap_miscdev->miscdev.minor = MISC_DYNAMIC_MINOR,ret = misc_register(&hap_miscdev->miscdev);/* save as drvdata *///platform_set_drvdata函数,将设备数据信息存入在平台驱动结构体中pdev->dev->driver_data中platform_set_drvdata(pdev, hap_miscdev);ret = sysfs_create_group(&pdev->dev.kobj,&haptic_param_attr_group);haptic_parse_dts(pdev);return ret;
}
上述示例中,我们是将驱动代码编译进内核中,所以在内核启动过程中进行了probe。
总结
-
#address-cells
属性指定了子节点的reg
属性中地址信息所占用的单元格(cell)数量,每个单元格通常是32位(4字节)。#size-cells
属性指定了子节点的reg
属性中长度信息所占用的单元格数量。这两个属性通常一起使用,因为它们共同定义了子节点的reg
属性的格式。注意,每个单元格是32位。但是好像不是reg属性的也是这种格式,例如上例中的byte-data-property属性,每个单元格是32位的。 -
/{...};
表示根节点,每个设备都有一个根节点,即使include的文件中也有根节点,最终会合并为一个。个人感觉最好还是将上述示例改为.dtsi
文件,然后不需要版本声明。 -
上层的配置会覆盖底层的配置。被include的文件为底层文件,如果里面有节点与上层相同,追加修改后,还是以上层的为准。