设备树基础知识
.dts 文件表示是设备树的源文件,里面可以添加各种设备的相关节点信息。
.dtsi 文件表示是设备输的头文件,其作用类似c语言中的头文件,需要用到时,也是#inxlude <xxx.dtsi>,其类容一般是属性相似的设备的节点信息。
.dtb 文件表示被编译成二进制的设备树文件。
dtc 就是设备树的编译工具。
基本用法如下:
dtc -S .dts -O .dtb -o xxx.dtb xxx.dts
上条指令中 dtc表示采用dtc工具进行编译, 参数 -S 表示被编译的文件类型,-O 表示编译后输出的文件类型,-o表示指定生成的编译文件名字,xxx.dtb就是我们指定的生成文件名字,xxx.dts是被编译的文件名字。
/dts-v1/ /一个完整的设备树文件必须加这一行,不然编译会报错/
/ { /根节点,一个完整的设备树必须且只有一个根节点。虽然我们常看见每个设备树文件都有一个根节点,但是在编译时会整合成一个/
node_label : nodename{ /node_label表示设备节点的便签类似于别名,方便其他地方引用。nodename表示设备节点名字。/
string-porperty = “a string”; /字符串属性的格式/
string-list = “a string”, “a string”; /多个字符串属性的格式/
one-int-porperty = <126>; /一个单个属性的格式/
int-list-property = <0xbeef 123 0xadfs>; /多个单元格属性的格式,每个单元格是32位,这里定义了三个单元格的属性/
byte-array-property = [0x12 0x22]; /多个8位属性的格式/
boolean-porperty; /无值的属性,也可以理解成Bool类型,存在表示true,反之成立/
};
};
以下是设备树使用的一些数据类型的定义及语法:
-
文本字符串用双引号,多个值可以用逗号隔开。
-
单元格是由尖括号包含的32位整数,多个值可以用空格隔开。
-
布尔类型的值为空。
-
属性的名字除了有很多常用的,也可以自定义。
-
每条属性是以“;”符号结尾的,类似c语言语句。
-
设备树文件中注释类似c语言中多行注释一样。
-
设备树文件中有些属性开头有“#”,这并不代表注释,而是属性名字的一部分而已,并且通常以“#”开头的属性都表示其属性描述的是某一个属性的长度或者size。
-
简而言之,设备树就是由一个一个的设备节点构成,而每个节点又是由一些属性构成,这些属性就是具体设备的信息。
设备树中特殊的节点
- chosen节点
chosen节点是一个特殊的节点,它并不用来描述特定的设备信息,而是用于传递一些数据给OS,比如bootloader的启动参数。
chosen{
bootarge = “root =/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200”;
};
- aliase节点
该节点也不是用来描述具体设备信息的,而是集合了一些节点别名的定义。
注:该图片来源于https://blog.csdn.net/weixin_45309916/article/details/109880928 ,如有侵权,请联系653382021@qq.com删除。
设备节点常见属性
- compatible属性
一般的,用于匹配设备节点和设备驱动,规则是驱动设备ID表中的compatible域的值(字符串),和设备树中设备节点中的compatible属性值完全一致,则节点的内容是给驱动的。
注意:相关变量的值或名称可能不匹配,这里主要是想表达一个驱动和设备树对应的关系。
- address属性
#address-cells:描述子节点reg属性值的地址表中首地址cell数量
#size-cells:描述子节点reg属性值的地址表中地址长度cell数量
reg:描述地址表
- CPU的地址描述
每个CPU都分配了唯一的一个ID,描述没有大小的CPU ids.
- 内存映射设备
描述一个设备的内存地址的时候,一般使用1个cell(32bits)描述地址,紧接着1一个cell (32bits)描述地址长度。
- 非内存映射设备
譬如i2c设备,有一个寻址地址,没有内存地址那样的地址长度和范围,一般使用1个cell (32bits)描述该地址,而没有描述地址长度的cell。
- 地址转换范围
有些设备是有片选的,就需要描述片选及片选的偏移量,在说明地址时,还需要说明地 址映射范围。
- interrupt属性
interrupt-controller 一个空属性用来声明这个node接收中断信号;
#interrupt-cells 这是中断控制器节点的属性,用来标识这个控制器需要几个单 位做中断描述符;
interrupt-parent 标识此设备节点属于哪一个中断控制器,如果没有设置这个 属性,会自动依附父节点的;
interrupts 一个中断标识符列表,表示每一个中断输出信号
一般的,如果父节点的#interrupt-cells的值是3,则子节点的interrupts一个cell三个32bits整型值:<中断域 中断 触发方式>
实际解析情况,得根据实际使用内核的设备树参加资料来决定。
一般的,如果父节点的#interrupt-cells的值是2,则子节点的interrupts一个cell两个32bits整型值:中断和触发方式。
实际解析情况,得根据实际使用内核的设备树参加资料来决定。
- gpio属性
常用的属性如下:
gpio-controller:说明该节点描述的是一个gpio控制器
#gpio-cells:描述gpio使用节点的属性一个cell的内容
属性名=<&引用GPIO节点别名 GPIO标号 工作模式>;
- regulator属性
常用属性如下:
regulator-name:电源管理的名字,对应struct regulation_constraints中的name
regulator-min-microvolt:设备最小电压对应struct regulation_constraints中的min_uV
regulator-max-microvolt:设备最大电压
regulator-always-on:对应struct regulation_constraints中的always_on
regulator-boot-on:对应struct regulation_constraints中的boot_on
关于regulator的详细介绍可参考:https://cloud.tencent.com/developer/article/1603958
- pinctrl属性
常用属性:
pinctrl-names:常见的值为default\sleep,对应了列表项的名称。
pinctrl-:提供设备某种状态需要的Pinctrl配置列表。
驱动中提取设备信息的常用函数
- 每一个节点都转换为一个device_node结构体
- device_node结构体中有properties, 用来表示该节点的属性, 每一个属性对应一个property结构体:
查找节点的OF函数
-
of_find_node_by_name函数
of_find_node_by_name函数通过节点名字查找指定的节点,函数原型如下:
struct device_node*of_find_node_by)name(struct device_node *from,const char *name);
from : 开始查找的节点,如果为NULL表示从根节点开始查找整个设备树
name : 要查找的节点名字
返回值:找到的节点,如果为NULL表示查找失败 -
of_find_node_by_type 函数
of_find_node_by_type函数通过 device_type 属性查找指定的节点,函数原型如下:
struct device_node *of_find_node_by_type(struct device_node *from, const char *type)
from: 开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
type: 要查找的节点对应的 type 字符串,也就是 device_type 属性值。
返回值: 找到的节点,如果为 NULL 表示查找失败。 -
of_find_compatible_node 函数
of_find_compatible_node 函数根据 device_type 和 compatible 这两个属性查找指定的节点,函数原型如下:
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 表示查找失败 -
of_find_node_by_path 函数
of_find_node_by_path 函数通过路径来查找指定的节点,函数原型如下:
inline struct device_node *of_find_node_by_path(const char *path)
函数参数和返回值含义如下:
path:带有全路径的节点名,可以使用节点的别名,比如“/backlight”就是 backlight 这个节点的全路径。
返回值: 找到的节点,如果为 NULL 表示查找失败
举个栗子
/*获取设备树中的属性数据*/
//1.获取设备节点:alphaled
dtsled.nd = of_find_node_by_path("/alphaled");
if(dtsled.nd == NULL)
{
printk("alphaled node can not found!\r\n");
return -EINVAL;
}
else
{
printk("alphaled node has been found!\r\n");
}
/dtsled设备结构体/
struct dtsled_dev{
dev_t devid; /设备号/
struct cdev cdev; /cdev/
struct class *class; /类/
struct device device; /设备/
int major; / 主设备号 /
int minor; / 次设备号 */
struct device_node nd; / 设备节点 */
};
struct dtsled_dev dtsled; /* led 设备 */
这里的dtsled.nd的信息如下,指的是led设备的设备节点,后面所用的也是这个
/dtsled设备结构体/
struct dtsled_dev{
dev_t devid; /设备号/
struct cdev cdev; /cdev/
struct class *class; /类/
struct device device; /设备/
int major; / 主设备号 /
int minor; / 次设备号 */
struct device_node nd; / 设备节点 */
};
struct dtsled_dev dtsled; /* led 设备 */
查找父\子节点的OF函数
Linux内核提供了几个查找节点对应的父节点或子节点的OF函数
-
of_get_parent 函数
of_get_parent 函数用于获取指定节点的父节点(如果有父节点的话),函数原型如下:
struct device_node *of_get_parent(const struct device_node *node)函数参数和返回值含义如下
node:要查找的父节点的节点。
返回值: 找到的父节点。 -
of_get_next_child 函数
of_get_next_child函数用迭代的查找子节点,函数原型如下:
struct device_node *of_get_next_child(const struct device_node *node, struct device_node *prev)
函数参数和返回值含义如下:
node:父节点。
prev:前一个子节点,也就是从哪一个子节点开始迭代的查找下一个子节点。可以设置为NULL,表示从第一个子节点开始。
返回值: 找到的下一个子节点。
提取属性值的OF函数
节点的属性信息里面保存了驱动所需要的内容,因此对于属性值的提取非常重要,Linux内核中使用结构体property表示属性,此结构体同样定义在文件include/linux/of.h中,内容如下:
struct property {
char *name; //属性名字
int length; //属性长度
void *value; //属性值
struct property *next; //下一个属性值
unsigned long _flags;
unsigned int unique_id;
struct bin_attribute attr;
};
- of_find_property 函数
of_find_property 函数用于查找指定的属性,函数原型如下:
property *of_find_property(const struct device_node *np, const char *name, int *lenp)
函数参数和返回值含义如下:
np: 设备节点。
name: 属性名字。
lenp:属性值的字节数
返回值: 找到的属性
举个栗子
struct property proper;
//2.获取compatible属性内容
proper = of_find_property(dtsled.nd,“compatible”,NULL);
if(proper == NULL)
{
printk(“compatible property find failed\r\n”);
}
else
{
printk(“compatible = %s\r\n”, (char)proper->value);
}
-
of_property_count_elems_of_size 函数
of_property_count_elems_of_size 函数用于获取属性中元素的数量,比如 reg 属性值是一个数组,那么使用此函数可以获取到这个数组的大小,此函数原型如下:
int of_property_count_elems_of_size(const struct device_node *np, const char *propname, int elem_size)
函数参数和返回值含义如下:
np:设备节点。
proname: 需要统计元素数量的属性名字。
elem_size:元素长度。
返回值: 得到的属性元素数量。 -
of_property_read_u32_index 函数
of_property_read_u32_index函数用于从属性中获取指定标号的 u32 类型数据值(无符号 32位),比如某个属性有多个 u32 类型的值,那么就可以使用此函数来获取指定标号的数据值,此函数原型如下:
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 表示属性值列表太小 -
of_property_read_u8_array 函数
of_property_read_u16_array 函数
of_property_read_u32_array 函数
of_property_read_u64_array 函数
这 4 个函数分别是读取属性中 u8、 u16、 u32 和 u64 类型的数组数据,比如大多数的 reg 属性都是数组数据,可以使用这 4 个函数一次读取出 reg 属性中的所有数据。这四个函数的原型如下:
int of_property_read_u8_array(const struct device_node *np,const char *propname,u8 *out_values,size_t sz)
int of_property_read_u8_array(const struct device_node *np,const char *propname,u16 *out_values,size_t sz)
int of_property_read_u8_array(const struct device_node *np,const char *propname,u32 *out_values,size_t sz)
int of_property_read_u8_array(const struct device_node *np,const char *propname,u64 *out_values,size_t sz)
函数参数和返回值含义如下:
np:设备节点。
proname: 要读取的属性名字。
out_value:读取到的数组值,分别为 u8、 u16、 u32 和 u64。
sz: 要读取的数组元素数量。
返回值: 0,读取成功,负值,读取失败, -EINVAL 表示属性不存在, -ENODATA 表示没有要读取的数据,
-EOVERFLOW 表示属性值列表太小
举个栗子/* 4、获取 reg 属性内容 */
int ret;
ret = of_property_read_u32_array(dtsled.nd, “reg”, regdata, 10);
if(ret < 0)
{
printk(“reg property read failed!\r\n”);
}
else
{
u8 i = 0;
printk(“reg data:\r\n”);
for(i = 0; i < 10; i++)
printk("%#X “, regdata[i]);
printk(”\r\n");
}
5、 of_property_read_u8 函数
of_property_read_u16 函数
of_property_read_u32 函数
of_property_read_u64 函数
有些属性只有一个整形值,这四个函数就是用于读取这种只有一个整形值的属性,分别用
于读取 u8、 u16、 u32 和 u64 类型属性值,函数原型如下:
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、 of_property_read_string 函数
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,读取成功,负值,读取失败。
这也是个栗子
/* 3、获取 status 属性内容 */
int ret;
ret = of_property_read_string(dtsled.nd, "status", &str);
if(ret < 0)
{
printk("status read failed!\r\n");
}
else
{
printk("status = %s\r\n",str);
}
7、 of_n_addr_cells 函数
of_n_addr_cells 函数用于获取#address-cells 属性值,函数原型如下:
int of_n_addr_cells(struct device_node *np)
函数参数和返回值含义如下:
np: 设备节点。
返回值: 获取到的#address-cells 属性值。
8、 of_n_size_cells 函数
of_size_cells 函数用于获取#size-cells 属性值,函数原型如下:
int of_n_size_cells(struct device_node *np)
函数参数和返回值含义如下:
np: 设备节点。
返回值:获取到的#size-cells 属性值。
其他常用的OF函数
of_iomap 函数
of_iomap 函数用于直接内存映射,以前我们会通过 ioremap 函数来完成物理地址到虚拟地址的映射,采用设备树以后就可以直接通过 of_iomap 函数来获取内存地址所对应的虚拟地址,不需要使用 ioremap 函数了。当然了,你也可以使用 ioremap 函数来完成物理地址到虚拟地址的内存映射,只是在采用设备树以后,大部分的驱动都使用 of_iomap 函数了。 of_iomap 函数本质上也是将 reg 属性中地址信息转换为虚拟地址,如果 reg 属性有多段的话,可以通过 index 参数指定要完成内存映射的是哪一段, of_iomap 函数原型如下:
np:设备节点。
index: reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为 0。
返回值: 经过内存映射后的虚拟内存首地址,如果为 NULL 的话表示内存映射失败。
完整的一个例子:
最后,推荐一些关于设备树的文章:
https://blog.csdn.net/zj82448191/article/details/109195364
推荐理由:介绍了设备树的解析过程,还有驱动与设备树的匹配方法。
https://www.cnblogs.com/hellokitty2/p/9975711.html
https://cloud.tencent.com/developer/article/1603958
推荐理由:有完整的驱动示列,同时侧重讲的regulator管理。