设备树(Device Tree),可以理解为按照树形结构描述板级设备。描述设备树的文件为DTS(Device Tree Source),也就是开发板上的设备信息,比如CPU数量,内存基地址,IIC接口及连接的设备等。使用DTC工具(支持Makefile-gcc编译)将DTS文件进行编译,就得到了设备树镜像xxxx.dtb文件。
如下图所示,树的主干就是系统总线, IIC 控制器、 GPIO 控制器、 SPI 控制器等都是接到系统主线上的分支。IIC 控制器有分为 IIC1 和 IIC2 两种,其中 IIC1 上接了 FT5206 和 AT24C02这两个 IIC 设备, IIC2 上只接了 MPU6050 这个设备。 DTS 文件的主要功能就是按照下面的结构来描述板子上的设备信息。
早期的Linux内核是没有设备树的,那时候的Linux板级支持包(BSP)会将所有的板载设备信息编译进内核中,只需要一次烧写对应的开发板镜像,即可支持所有的板载设备。在2011年,Linux之父Linus认为这种内核组成方式违背了Linux设计之初的简洁、高效理念,所以要求开发人员将设备信息这部分从内核中摘除,独立编译成相关的设备树文件。这样大大精简了内核部分的相关代码,同时也提高了Linux Kernel部分的通用性,只需要分别移植内核和相关的板级设备树文件,即可完成适配,而所需要付出的代价仅仅是多烧写一个设备树镜像而已,如果使用相关的烧写工具或烧写脚本,则可以忽略不计。所以一些厂商后来设计的开发板,没有了历史包袱,只支持设备树类型的系统镜像。
以.dtsi
结尾的文件为DTS的头文件,dtsi文件与dts文件的语法和内容相同,只不过一般存放的是一些通用的设备描述,便于移植。在dts文件中可以使用#include xxxx.dtsi
来引用头文件。dts文件一般是特定的板级设备的适配,且dts文件也可以使用include进行包含(一般不推荐),除此之外,还可以包含C语言的.h
文件。
例:
#include
#include
#include
#include
#include
/ {
#address-cells = <1>;
#size-cells = <1>;
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 {
compatible = "arm,cortex-a7";
......
nvmem-cell-names = "part_number";
#cooling-cells = <2>;
};
};
cpu0_opp_table: cpu0-opp-table {
compatible = "operating-points-v2";
opp-shared;
};
...
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
interrupt-parent = <&intc>;
ranges;
sram: sram@10000000 {
compatible = "mmio-sram";
reg = <0x10000000 0x60000>;
#address-cells = <1>;
#size-cells = <1>;
ranges = <0 0x10000000 0x60000>;
};
};
};
描述了板载的CPU和外设信息。/
表示的是根节点,一个dts文件只会有一个根节点,不同的dts合并时会将根节点下的设备进行合并。在设备树中节点命名格式为node-name@unit-address
,“node-name”是节点名字,可以用来描述节点的功能。“unit-address”一般表示设备的地址或寄存器首地址,如果没有地址或者寄存器可以不要。还可以使用标签来更方便的访问节点,如label: node-name@unit-address
可以使用&label
来访问节点。
节点是具体的设备,由一堆属性组成,不同的设备需要的属性不同,用户可以自定义属性。除了用户自定义属性,有很多属性是标准属性, Linux 下的很多外设驱动都会使用这些标准属性。
①compatible属性
“兼容性”属性,值为一个字符串列表,用于绑定设备和驱动。例如:compatible = "cirrus,my_cs42l51","cirrus,cs42l51";
其中cirrus为厂商,第二个值表示驱动模块名,属性值可以是多个。Linux 内核首先使用第一个兼容值进行查找,如果没有找到就使用第二个,直到查找完 compatible 属性中的所有值。一般驱动文件会使用OF匹配表保存可使用的 compatible 属性值,如果设备节点的值与其中某一个相等,则可以使用这个驱动。
②model属性
model 属性值也是一个字符串,一般用于描述开发板名称或设备模块信息。
③status属性
用来描述设备的状态信息:
值 | 描述 |
---|---|
okey | 可操作 |
disable | 当前不可操作,但可以根据情况改变 |
fail | 不可操作,且不大可能变为可操作 |
fail-sss | 与fail相同,sss为检测到的错误内容 |
④#address-cells 和#size-cells 属性
值为无符号32位整形,可以用在任何拥有子节点的设备中,用于描述子节点的地址信息。#address-cells 属性值决定了子节点 reg 属性中地址信息所占用的字长(32 位), #size-cells 属性值决定了子节点 reg 属性中长度信息所占的字长(32 位),即描述起始地址和地址长度的属性。比如reg =
,address为起始地址,length为地址长度,#address-cells则表明address数据的字长,#size-cells 表明 length 这个数据所占用的字长。
⑤reg属性
一般是(address, length)对,用于描述设备地址空间资源信息或者设备地址信息。
⑥ranges属性
可以为空或者按照(child-bus-address,parent-bus-address,length)格式编写的数字矩阵。ranges是一个地址映射/转换表,每个属性由子地址、父地址和地址空间长度三部分组成。
例:
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
interrupt-parent = <&intc>;
ranges = <0 0x10000000 0x100000>;
sram: sram@10000000 {
compatible = "mmio-sram";
reg = <0x0 0x60000>;
#address-cells = <1>;
#size-cells = <1>;
ranges = <0 0x10000000 0x60000>;
};
};
⑦name属性
用于记录节点名称,新版本中已经弃用,不推荐使用。
⑧device_type属性
多数已弃用,有些CPU或Memory节点可能会使用一下
板载设备:
① 这个芯片是由两个 Cortex-A7 架构的 32 位 CPU 和 Cortex-M4 组成。
② STM32MP157 内部 sram,起始地址为 0x10000000,大小为 384KB(0x60000)。
③STM32MP157 内部 timers6,起始地址为 0x40004000,大小为 25.6KB(0x400)。
④STM32MP157 内部 spi2,起始地址为 0x4000b000,大小为 25.6KB(0x400)。
⑤ STM32MP157 内部 usart2,起始地址为 0x4000e000,大小为 25.6KB(0x400)。
⑥ STM32MP157 内部 i2c1,起始地址为 0x40012000,大小为 25.6KB(0x400)
设备树描述文件myfirst.dts:
#include xxxx.dtsi
/{
compatible = "st, stm32mp157d-atk", "st, stm32mp157";
/*CPU节点*/
cpus {
#address-cells = <1>;
#size-cells = <0>;
/* CPU0 节点 */
cpu0: cpu@0 {
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <0>;
};
/* CPU1 节点 */
cpu1: cpu@1 {
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <1>;
};
};
/* soc 节点 */
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges; /*ranges 属性为空,说明子空间和父空间地址范围相同*/
/* sram 节点 */
sram: sram@10000000 {
compatible = "mmio-sram";
reg = <0x10000000 0x60000>;
ranges = <0 0x10000000 0x60000>;
};
/* timers6 节点 */
timers6: timer@40004000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "st,stm32-timers";
reg = <0x40004000 0x400>;
};
/* spi2 节点 */
spi2: spi@4000b000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "st,stm32h7-spi";
reg = <0x4000b000 0x400>;
};
/* usart2 节点 */
usart2: serial@4000e000 {
compatible = "st,stm32h7-uart";
reg = <0x4000e000 0x400>;
};
/* i2c1 节点 */
i2c1: i2c@40012000 {
compatible = "st,stm32mp15-i2c";
reg = <0x40012000 0x400>;
};
};
};
linux内核在启动时,会在/proc/device-tree目录下,根据节点名称创建不同的文件夹。例如,/proc/devicetree/base目录下,文件module的内容为“ STMicroelectronics STM32MP157D eval daughter”
,与dts文件中的属性值相同。在xxx/base/soc下,则是soc的子节点信息。
在根节点“/”中有两个特殊节点:aliases
和chosen
,aliases的功能是定义别名,类似于前面的lable: xxxx
定义,然后用&lable
访问。chosen不是一个真实设备,此节点主要是为了向内核传递数据,一般dts文件中的chosen节点为空或者内容很少。
例:
aliases {
serial0 = &uart4;
};
chosen {
stdout-path = "serial0:115200n8";
};
在源文件中,仅设置了chosen的属性stdout-path
,表示默认输出使用serial0,而在aliases中又将serial0设置为uart4,所以默认的输出会通过串口4打印。在板子的/proc/device-tree/chosen目录下,会发现还会多出一个bootargs属性,这个就是在U-Boot中自定义的启动参数,即chosen将人为的设置传递给了内核。
在Linux源码目录/Documentation/devicetree/bindings中,有详细的文档指导如何为设备树添加不同种类的节点,包括cpu、clock、connector等。
例:
设备树中描述了详细的设备信息,当编写驱动时往往需要用到。例:设备树使用 reg 属性描述某个外设的寄存器地址为 0X02005482,长度为 0X400,在编写驱动的时候需要获取到 reg 属性的0X02005482 和 0X400 这两个值初始化外设。那么就可以使用Linux内核提供的"of_xxx"
函数来获取这些参数,又称为OF函数
,相关函数原型在"include/linux/of.h"中。
Linux内核使用device_node结构体来描述一个节点,结构体的定义也在of.h
中,内容如下:
struct device_node {
const char *name; /*节点名字 */
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; /*子节点 */
struct device_node *sibling;
#if defined(CONFIG_OF_KOBJ)
struct kobject kobj;
#endif
unsigned long _flags;
void *data;
#if defined(CONFIG_SPARC)
unsigned int unique_id;
struct of_irq_controller *irq_trans;
#endif
};
其中包括5个OF函数:
①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
原型:
struct device_node *of_find_node_by_type(struct device_node *from, const char *type)
功能:通过节点的device_type属性查找节点
参数:
from:开始查找的节点,如果为NULL,查找整个设备树
type:要查找的节点对应的type字符串,即device_type的值
返回值:找到的节点,NULL表示查找失败
③of_find_compatible_node
原型:
struct device_node *of_find_compatible_node(struct device_node *from, const char *type, const char *compatible)
功能:通过节点的 device_type 和 compatible 两个属性查找节点
参数:
from:开始查找的节点,如果为NULL,查找整个设备树
type:要查找的节点对应的type字符串,即device_type的值,可以为NULL,表示忽略type属性
compatible:要查找的节点所对应的 compatible 属性列表
返回值:找到的节点,NULL表示查找失败
④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)
功能:通过 of_device_id 匹配表查找指定的节点
参数:
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树
matches:of_device_id 匹配表,在此匹配表里面查找节点
match:找到的匹配的 of_device_id
返回值: 找到的节点,如果为 NULL 表示查找失败
⑤of_find_node_by_path
原型:
inline struct device_node *of_find_node_by_path(const char *path)
功能:通过路径来查找指定的节点
参数:
path:带有全路径的节点名,可以使用节点的别名,比如“/backlight”就是 backlight 这个节点的全路径
返回值:找到的节点,如果为 NULL 表示查找失败
①of_get_parent
原型:
struct device_node *of_get_parent(const struct device_node *node)
功能:获取指定节点的父节点
参数:
node:要查找的父节点的节点
返回值:找到的父节点
②of_get_next_child
原型:
struct device_node *of_get_next_child(const struct device_node *node, struct device_node *prev)
功能:迭代的查找子节点
参数:
node:父节点
prev:前一个子节点,也就是从哪一个子节点开始迭代的查找下一个子节点。可以置为NULL,表示从第一个子节点开始
返回值:找到的下一个子节点
节点的属性值保存在结构体property中,此结构体的定义也在of.h中,内容如下:
struct property {
char *name; /* 属性名字 */
int length; /* 属性长度 */
void *value; /* 属性值 */
struct property *next; /* 下一个属性 */
#if defined(CONFIG_OF_DYNAMIC) || defined(CONFIG_SPARC)
unsigned long _flags;
#endif
#if defined(CONFIG_OF_PROMTREE)
unsigned int unique_id;
#endif
#if defined(CONFIG_OF_KOBJ)
struct bin_attribute attr;
#endif
};
提取属性的OF函数有:
①of_find_property
原型:
property *of_find_property(const struct device_node *np, const char *name, int *lenp)
功能:查找指定的属性
参数:
np:设备节点
name:属性名字
lenp:属性值的字节数
返回值:找到的属性
②of_property_count_elems_of_size
原型:
int of_property_count_elems_of_size(const struct device_node *np, const char *propname, int elem_size)
功能:
获取属性中元素的数量,例:reg 属性值是一个数组,使用此函数可以获取到这个数组的大小
参数:
np:设备节点
proname:需要统计元素数量的属性名字
elem_size:元素长度。
返回值:得到的属性元素数量
③of_property_read_u32_index
原型:
int of_property_read_u32_index(const struct device_node *np, const char *propname, u32 index, u32 *out_value)
功能:
用于从属性中获取指定标号的无符号32位类型数据值(u32),如果某个属性有多个 u32 类型的值,可以使用此函数来获取指定标号的数据值
参数:
np:设备节点
proname:要读取的属性名字
index:要读取的值标号
out_value:读取到的值
返回值:
0:读取成功
负值:读取失败
-EINVAL:属性不存在
-ENODATA:没有要读取的数据
-EOVERFLOW:属性值列表太小
④of_property_read_u8(u16 u32 u64)_array
原型:
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)
int of_property_read_u64_array(const struct device_node *np, const char *propname, u64 *out_values, size_t sz)
功能:
读取属性中 u8、u16、u32 和 u64 类型的数组数据,比如大多数的 reg 属性都是数组数据,可以使用这 4 个函数一次读取出 reg 属性中的所有数据
参数:
np:设备节点
proname:要读取的属性名字
out_value:读取到的数组值,分别为 u8、u16、u32 和 u64。
返回值:
0:读取成功
负值:读取失败
-EINVAL:属性不存在
-ENODATA:没有要读取的数据
-EOVERFLOW:属性值列表太小
⑤of_property_read_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)
功能:
有些属性只有一个整型值,这四个函数就是用于读取这种只有一个整形值的属性,分别用于读取 u8、u16、u32 和 u64 类型属性值
参数:
np:设备节点
proname:要读取的属性名字
out_value:读取到的属性值
返回值:
0:读取成功
负值:读取失败
-EINVAL:属性不存在
-ENODATA:没有要读取的数据
-EOVERFLOW:属性值列表太小
⑥of_property_read_string
原型:
int of_property_read_string(struct device_node *np, const char *propname, const char **out_string)
功能:
用于读取属性中字符串值
参数:
np:设备节点
proname:要读取的属性名字
out_value:获取到的字符串值
返回值:
0:读取成功
负值:读取失败
⑦of_n_addr_cells
原型:
int of_n_addr_cells(struct device_node *np)
功能:
用于获取 #address-cells 属性值
参数:
np:设备节点
返回值:
获取到的 #address-cells 属性值
⑧of_n_size_cells
原型:
int of_n_size_cells(struct device_node *np)
功能:
用于获取 #size-cells 属性值
参数:
np:设备节点
返回值:
获取到的 #size-cells 属性值
①of_device_is_compatible
原型:
int of_device_is_compatible(const struct device_node *device, const char *compat)
功能:
用于查看节点的 compatible 属性是否有包含 compat 指定的字符串,即检查设备节点的兼容性
参数:
device:设备节点
compat:要查看的字符串
返回值:
0:节点的 compatible 属性中不包含 compat 指定的字符串
正数:节点的 compatible属性中包含 compat 指定的字符串。
②of_get_address
原型:
const __be32 *of_get_address(struct device_node *dev, int index, u64 *size, unsigned int *flags)
功能:
用于获取地址相关属性,主要是“reg”或者“assigned-addresses”属性值
参数:
dev:设备节点
index:要读取的地址标号
size:地址长度
flags:参数,比如 IORESOURCE_IO、 IORESOURCE_MEM 等
返回值:
读取到的地址数据首地址,为 NULL 的话表示读取失败
③of_translate_address
原型:
u64 of_translate_address(struct device_node *dev, const __be32 *addr)
功能:
将从设备树读取到的地址转换为物理地址
参数:
dev:设备节点
in_addr:要转换的地址
返回值:
得到的物理地址,如果为 OF_BAD_ADDR 的话表示转换失败
④of_address_to_resource
原型:
int of_address_to_resource(struct device_node *dev, int index, struct resource *r)
功能:
将 reg 属性值转换为 resource 结构体类型,resource结构体描述的都是设备资源信息,如IIC、SPI外设中的寄存器地址或资源
参数:
dev:设备节点。
index:地址资源标号
r:得到的 resource 类型的资源值
返回值:
0:成功
负值:失败
⑤of_iomap
原型:
void __iomem *of_iomap(struct device_node *np, int index)
功能:
用于直接内存映射,采用设备树后可以使用of_iomap()替换原先使用的ioremap()函数,直接获取内存地址到虚拟地址的映射。很常用!
参数:
dev:设备节点
index: reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为 0
返回值:
经过内存映射后的虚拟内存首地址,如果为 NULL 的话表示内存映射失败