掌握设备树是 Linux驱动开发人员必备的技能

1、了解设备树文件

在3.x版本以前的Linux内核源码中,存在大量的“arc/arm/mach-xxx”和“arc/arm/plat-xxx”文件夹,里面很多个“.c”和“.h”文件,它们用来描述设备信息。而现在的ARM架构是采用“设备树”来描述设备信息。“设备树”英文名字叫“Device Tree”。描述设备树的文件叫做“DTS”,是Device Tree Source的缩写。它主要用来描述板子上有哪些设备构成,如:I2C控制器、GPIO控制器、SPI控制器、UART控制器等。

设备树源文件扩展名为“.dts”,设备树头文件扩展名为“.dtsi”,它们经过DTC工具编译后生成的二进制文件,就是“.dtb”文件。

“.dts”设备树文件可以通过“#include”来引用“.h”、“.dtsi”和“.dts”文件。在编写设备树头文件时,我们最好选择“.dtsi”作为后缀。

一般“.dtsi”文件用于描述芯片的“内部外设信息”,比如CPU架构、主频、外设寄存器地址范围,如:UART、IIC、SPI、TIMER等等。

2、了解节点

设备树是描述板子上的“设备信息”的文件,均采用“树形结构”。每个设备树文件只有一个“根节点”,而“不同设备树文件的根节点”会合在一起形成一个根节点。每个设备都是一个节点,称之为“设备节点”,每个节点都采用“属性信息”来描述其“节点信息”。

认识“标准属性”、“节点”和“节点标签”,见下图:

标准属性

1)、compatiable属性,也叫兼容性属性。

如:compatible = “cirrus,cs42l51“;

这个compatible只有一个属性值,就是“cirrus,cs42l51”,其中“cirrus”表示厂商,“cs42l51”表示驱动模块。

再如:compatible = “cirrus,cs42l51“,”cirrus,My_cs42l51“;

这个compatible就有两个属性值,就是“cirrus,cs42l51”和”cirrus,My_cs42l51“,其中“cirrus”表示厂商,“cs42l51”表示驱动模块,“My_cs42l51”表示驱动模块。

驱动文件的“OF匹配表”

const structof_device_id cs42l51_of_match[] = {{ .compatible = “cirrus,cs42l51“, },{ }};

设备首先会使用第一个兼容值在Limux内核里面查找。看看能不能找到与之匹配的驱动文件,如果没有找到,就使用第二个兼容值查,以此类推,直到查找完 commpatible属性中的所有值。

compatible属性用于将设备和驱动绑定起来。驱动程序文件有一个“OF匹配表”,这个“OF匹配表”保存着一些compatible的值,如果设备节点的compatible属性值和“OF匹配表”中的任何一个值相等,那么这个设备就可以使用这个驱动。

2)、model属性

用于描述开发板的名字或设备模块的信息。

比如:model = “STMicroelectronics STM32MP157C-DK2 Discovery Board“;

3)、status属性

status属性值

描述

okay

表明设备是可操作的

disabled

表明设备当前是不可操作的,但是在未来可以变为可操作的,比如:热插拔设备插入以后。至于disabled的具体含义还要看设备的绑定文档。

fail

表明设备不可操作,设备检测到了一系列的错误,而且设备也不大可能变得可操作。

fail-sss

含义和“fai1”相同,后面的“sss部分”是检测到的错误内容。

4)、reg属性

reg属性的值一般是(address,length)对。

reg 属性一般用于描述“设备地址空间资源信息”或者“设备地址信息”,比如某个外设的“寄存器地址范围信息”,或者I2C器件的设备地址等。

5)、ranges属性

ranges = <child-bus-address,parent-bus-address,length>;

child-bus-address表示“子总线地址空间的物理地址”;

parent-bus-address表示“父总线地址空间的物理地址”;

length表示“子总线地址空间的长度”;

若ranges属性值为空,则说明子地址空间和父地址空间完全相同;

6)、“#address-cells”和“#size-cells”属性

#address-cells的属性值为无符号32位整型,用于描述子节点的“reg和ranges”中的address所占的位数,1表示32位,2表示64位;

#size-cells的属性值为无符号32位整型,用于描述子节点的“reg和ranges”中的length所占的位数,0表示没有位数,1表示32位,2表示64位;

举例说明“reg、 #address-cells和#size-cells”属性:

cpus {#address-cells = ;//表示子节点cpureg和rangesaddress占32个位

#size-cells = ; //表示子节点cpureg和rangeslength占0个位cpu0: cpu@0{compatible = “arm,cortex-a7”;device_type = “cpu”;reg = ;//表示addres=0, 没有length,和前面定义一致

clocks = ;clock-names = “cpu”;operating-points-v2 = ;nvmem-cells = ; nvmem-cell-names = “part_number”; #cooling-cells = ; }; };

scmi_sram: sram@2ffff000 {compatible = “mmio-sram”;reg = ;//表示address=0x2ffff000,length=0x1000

#address-cells = ;//表示scmi_shmreg和rangesaddress占32个位

#size-cells = ;//表示scmi_shmreg和rangeslength占32个位

ranges = ;//分别为32位,和前面定义一致

scmi0_shm: scmi_shm@0{reg = ;//reg的address和length分别为32位,和前面定义一致

};

};

soc{

compatible = “simple-bus“;#address-cells = ;//表示sramreg和rangesaddres长度为32个位#size-cells = ;//表示sramreg和rangeslength长度为32个位

interrupt-parent = ;ranges = ;

sram: sram@10000000{compatible = “mmio-sram”;reg = ;//reg的address和length均为32位,和前面定义一致

#address-cells = ;#size-cells = ;ranges = ;//均为32位,和前面定义一致

};};

以STM32MP157为例,创建设备树文件myfrst.dts ,要求在设备树里面的内容如下:

1)、芯片是由两个Cortex-A7 架构的32位CPU和Cortex-M4组成。

2)、STM32MP157内部sram,起始地址为 0x10000000,大小为384KB(0x60000)。

先搭建 “根节点框架”,如下:

/{compatible = “st,stm32mp157d-atk”, “st,stm32mp157”;};

接着添加cpus节点,如下:

/{compatible = “st,stm32mp157d-atk”, “st,stm32mp157”;cpus{

#address-cells = ;//表示子节点cpureg和rangesaddress占32个位

#size-cells = ;//表示子节点cpureg和rangeslength占0个位

};

};

接着添加cpu0节点,如下:

/{compatible = “st,stm32mp157d-atk”, “st,stm32mp157”;cpus{

#address-cells = ;//表示子节点cpureg和rangesaddress占32个位

#size-cells = ; //表示子节点cpureg和rangeslength占0个位

cpu0: cpu@0 {compatible = “arm,cortex-a7”;device_type = “cpu”;reg = ;//表示addres=0, 没有length,和前面定义一致

};

};

};

接着添加cpu1节点,如下:

/{compatible = “st,stm32mp157d-atk”, “st,stm32mp157”;cpus{

#address-cells = ;//表示子节点cpu的reg和ranges的address占32个位

#size-cells = ; //表示子节点cpureg和rangeslength占0个位

cpu0: cpu@0 {compatible = “arm,cortex-a7”;device_type = “cpu”;reg = ;//表示addres=0, 没有length,和前面定义一致

};

cpu1: cpu@1{compatible = “arm,cortex-m4“;device_type = “cpu”;reg = <1>;//表示addres=1, 没有length,和前面定义一致

};

};

};

接着添加soc节点,如下:

/{compatible = “st,stm32mp157d-atk”, “st,stm32mp157”;cpus{

#address-cells = ;//表示子节点cpureg和rangesaddress占32个位

#size-cells = ; //表示子节点cpureg和rangeslength占0个位

cpu0: cpu@0{compatible = “arm,cortex-a7”;device_type = “cpu”;reg = ;//表示addres=0, 没有length,和前面定义一致

};

cpu1: cpu@1{compatible = “arm,cortex-m4”;device_type = “cpu”;reg = <1>;//表示addres=1, 没有length,和前面定义一致

};

};

soc {

compatible = “simple-bus”;#address-cells = ;//表示sramreg和rangesaddres长度为32个位#size-cells = ;//表示sramreg和rangeslength长度为32个位

ranges;//说明子地址空间和父地址空间完全相同;

};

};

接着添加sram节点,如下:

/{compatible = “st,stm32mp157d-atk”, “st,stm32mp157”;cpus{

#address-cells = ;//表示子节点cpureg和rangesaddress占32个位

#size-cells = ; //表示子节点cpureg和rangeslength占0个位

cpu0: cpu@0{compatible = “arm,cortex-a7”;device_type = “cpu”;reg = ;//表示addres=0, 没有length,和前面定义一致

};

cpu1: cpu@1{compatible = “arm,cortex-m4”;device_type = “cpu”;reg = <1>;//表示addres=1, 没有length,和前面定义一致

};

};

soc{

compatible = “simple-bus“;#address-cells = ;//表示sramreg和rangesaddres长度为32个位#size-cells = ;//表示sramreg和rangeslength长度为32个位

ranges; //说明子地址空间和父地址空间完全相同;

sram: sram@10000000 {compatible = “mmio-sram”;reg = ;//address和length均为32位,和前面定义一致

ranges = ;//均为32位,和前面定义一致

};

};

};

3、了解特殊节点

1)、aliases子节点

aliases {

serial0 = &uart4;//给&uart4起个别名叫“serial0”

};

2)、chosen子节点

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

chosen {

stdout-path = “serial0:115200n8”;

//设置“stdout-path”属性,表示标准输出使用“serial0

};

4、学习“OF函数”

“OF函数原型”都定义在“include/linux/of.h”文件中。

1)、了解相关结构体

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

structdevice_node {

const char*name;/*节点名字*/

phandle phandle;

const char*full_name;/*节点全名*/

structfwnode_handle fwnode;

struct property *properties;/*属性*/

struct property *deadprops; /* removed properties */

struct device_node *parent;/*父节点*/

struct device_node *child;/*子节点*/

struct device_node *sibling;

#ifdefined(CONFIG_OF_KOBJ)

struct kobject kobj;

#endif

unsigned long_flags;

void *data;

#ifdefined(CONFIG_SPARC)

unsigned intunique_id;

structof_irq_controller *irq_trans;

#endif

};

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

structproperty {

char *name;/*属性名字*/

int length;/*属性长度*/

void *value;/*属性值*/

structproperty *next;/*下一个属性*/

#ifdefined(CONFIG_OF_DYNAMIC) || defined(CONFIG_SPARC)

unsigned long_flags;

#endif

#ifdefined(CONFIG_OF_PROMTREE)

unsigned intunique_id;

#endif

#ifdefined(CONFIG_OF_KOBJ)

structbin_attribute attr;

#endif

}

resource结构体定义在文件 include/linux/ioport.h中。

structresource {

resource_size_t start;/*开始地址,占32位*/

resource_size_t end;/*结束地址,占32位*/

const char*name;/*资源的名字*/

unsigned longflags;/*资源标志或资源类型*/

unsigned longdesc;

structresource *parent, *sibling, *child;

};

2)、通过节点的名字查找指定的节点:

structdevice_node *of_find_node_by_name(structdevice_node *from, const char*name)

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

3)、通过device_type属性查找指定的节点

structdevice_node *of_find_node_by_type(structdevice_node *from, const char*type)

from:开始查找的节点,如果为NULL,表示从根节点开始查找整个设备树;

type:要查找的节点对应的type字符串,也就是 device_type属性值。

返回值:返回找到的节点,如果为NULL,表示查找失败。

4)、根据device_type属性和compatible属性查找指定的节点

structdevice_node *of_find_compatible_node(structdevice_node *from, const char*type, const char*compatible)

from:开始查找的节点,如果为NULL,表示从根节点开始查找整个设备树;

type:要查找的节点对应的type字符串,也就是device_type属性值。若为NULL,则可忽略掉device_type属性值;

compatible:要查找的节点所对应的compatible 属性列表;

返回值:返回找到的节点,如果为NULL,表示查找失败。

5)、通过of_device_id匹配表来查找指定的节点

structdevice_node *of_find_matching_node_and_match(structdevice_node *from, const structof_device_id *matches, const structof_device_id **match)

from:开始查找的节点,如果为NULL,表示从根节点开始查找整个设备树。matches: of_device_id匹配表,也就是在此匹配表里面查找节点。

match:找到的匹配的of_device_id

返回值:返回找到的节点,如果为NULL,表示查找失败。

6)、通过路径来查找指定的节点

inline structdevice_node *of_find_node_by_path(const char*path)

path:带有全路径的节点名,可以使用节点的别名,比如“/backlight”就是 backlight 这个节点的全路径。

返回值:返回找到的节点,如果为NULL,表示查找失败。

7)、获取指定节点的父节点

structdevice_node *of_get_parent(const structdevice_node *node)

node:要查找的父节点的节点名称。返回值: 返回找到的父节点。

8)、采用迭代方式查找子节点,即查找“prev”的下一个节点。

structdevice_node *of_get_next_child(const structdevice_node *node, structdevice_node *prev)

node:父节点。

prev:前一个子节点,也就是从哪一个子节点开始迭代的查找下一个子节点。若设置为NULL,则表示从第一个子节点开始。

返回值: 返回找到的下一个子节点。

9)、查找指定的属性

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

np:设备节点。

name:属性名字。

lenp:属性值的字节数

返回值:返回找到的属性。

10)、获取属性中元素的数量

intof_property_count_elems_of_size(const structdevice_node *np, const char*propname, intelem_size)

np:设备节点。

proname:需要统计元素数量的属性名字。

elem_size:元素长度。

返回值: 返回得到的属性元素数量。

11)、从属性中获取指定标号的数据值

intof_property_read_u32_index(const structdevice_node *np, const char*propname, u32index, u32*out_value)

np:设备节点。

proname:要读取的属性名字。index:要读取的值标号;

out_value:读取到的值返回值:0读取成功,负值,读取失败,-EINVAL表示属性不存在,-ENODATA 表示没有要读取的数据,-EOVERFLOW 表示属性值列表太小。

12)、读取属性中u8类型的数组数据

intof_property_read_u8_array(const structdevice_node *np, const char*propname, u8*out_values, size_t sz)

np:设备节点。proname:要读取的属性名字。out_value:读取到的数组值,数据类型为u8。

sz:要读取的数组元素数量。返回值:0,读取成功,负值,读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没有要读取的数据,-EOVERFLOW 表示属性值列表太小。

13)、读取属性中u16类型的数组数据

intof_property_read_u16_array(const structdevice_node *np, const char*propname, u16*out_values, size_t sz)

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

14)、读取属性中u32类型的数组数据

intof_property_read_u32_array(const structdevice_node *np, const char*propname, u32*out_values, size_t sz)

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

15)、读取属性中u64类型的数组数据

intof_property_read_u64_array(const structdevice_node *np, const char*propname, u64*out_values, size_t sz)

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

16)、读取属性中只有一个u8类型的数据

intof_property_read_u8(const structdevice_node *np, const char*propname, u8*out_value)

np:设备节点。proname:要读取的属性名字。out_value:读取到的u8类型的数据值。返回值:0,读取成功,负值,读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没有要读取的数据,-EOVERFLOW 表示属性值列表太小。

17)、读取属性中只有一个u16类型的数据

intof_property_read_u16(const structdevice_node *np, const char*propname, u16*out_value)

np:设备节点。proname:要读取的属性名字。out_value:读取到的u16类型的数据值。返回值:0,读取成功,负值,读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没有要读取的数据,-EOVERFLOW 表示属性值列表太小。

18)、读取属性中只有一个u32类型的数据

intof_property_read_u16(const structdevice_node *np, const char*propname, u32*out_value)

np:设备节点。proname:要读取的属性名字。out_value:读取到的u32类型的数据值。返回值:0,读取成功,负值,读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没有要读取的数据,-EOVERFLOW 表示属性值列表太小。

19)、读取属性中只有一个u64类型的数据

intof_property_read_u16(const structdevice_node *np, const char*propname, u64*out_value)

np:设备节点。proname:要读取的属性名字。out_value:读取到的u64类型的数据值。返回值:0,读取成功,负值,读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没有要读取的数据,-EOVERFLOW 表示属性值列表太小。

20)、读取属性中的字符串值

intof_property_read_string(structdevice_node *np, const char*propname, const char**out_string)

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

21)、获取“#address-cells”属性值

intof_n_addr_cells(structdevice_node *np)

np:设备节点。返回值:返回“#address-cells”的属性值。

22)、获取“#size-cells”属性值

intof_n_size_cells(structdevice_node *np)

np:设备节点。返回值:返回“#size-cells”的属性值。

23)、査看“节点的compatible属性”是否有包含“compat指定的字符串”,也就是检查设备节点的兼容性

intof_device_is_compatible(const structdevice_node *device, const char*compat)

device:设备节点;

compat:要查看的字符串。返回值:0,表示“节点的compatible属性”中不包含“compat指定的字符串”;正数,表示“节点的compatible属性”中包含“compat指定的字符串”。

24)、读取“reg”或者“assigned-addresses”属性值

const__be32 *of_get_address(structdevice_node *dev, intindex, u64 *size, unsigned int*flags)

dev:设备节点。

index:要读取的地址标号。

size:地址长度。flags:参数,比如“IORESOURCE_IO”、“IORESOURCE_MEM”等返回值:返回读取到的地址数据首地址,若返回值为NUIL,则表示读取失败。

25)、将从设备树读取到的地址addr转换为物理地址

u64 of_translate_address(structdevice_node *dev, const__be32 *addr)

dev:设备节点。

addr:要转换的地址。

返回值:返回得到的物理地址,如果为 OF_BAD_ADDR,则表示转换失败。

26)、将reg属性值转换为resource结构体类型

intof_address_to_resource(structdevice_node *dev, intindex, structresource *r)

dev:设备节点。

index:地址资源标号

r:得到的resource 类型的资源值。

返回值:0,成功:负值,失败。

27)、将“reg属性中地址信息”转换为“虚拟地址”,如果reg属性有多段的话,可以通过index参数指定要完成内存映射的是哪一段

void__iomem *of_iomap(structdevice_node *np, intindex)

np:设备节点。

index:reg属性中要完成内存映射的段,如果reg属性只有一段的话,则index=0。

返回值:经过内存映射后的虚拟内存首地址,如果为NULL的话,则表示内存映射失败。

注意:也可以使用ioremap()函数来完成物理地址到虚拟地址的内存映射。在采用设备树以后,大部分的驱动都使用of_iomap()函数来获取内存地址所对应的虚拟地址。