我们知道 Linux 内核使用设备树的形式去描述芯片硬件设备节点的各种属性,设备树的树形结构可以层次化的组织这些节点属性。设备树源码属于脚本格式的文件,Linux 内核无法直接使用脚本格式,所以最终使用时需要将设备树源码编译为二进制的 “.dtb” 格式,最终 Uboot 将 ".dtb " 格式设备树传递给 Linux 内核使用。
Linux 内核从 Uboot 获取到 “.dtb” 之后依然无法直接使用(准确地说是不便于使用)为了便于我们访问设备树内容 Linux 内核还会解析 “.dtb” 设备树内容,并转换为其他的格式同时保存到 rootFS 中。
那么 Linux 内核要将 “.dtb” 内容转为什么样的格式呢?由于设备树使用了树形层次结构去记录设备节点信息,所以 Linux 内核同样也将 “.dtb” 内容转换为更易于访问的树形结构。
在 Linux 内核中用 struct device_node 表示设备树的数据结构,它是一种树形结构,该类型如下。
struct device_node {
const char *name;
phandle phandle;
const char *full_name;
struct fwnode_handle fwnode;
struct property *properties;
struct property *deadprops; /* removed properties */
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
};
Uboot 将 “.dtb” 文件传递给 Linux 内核后,Linux 内核解析 “.dtb” 并将内容转化成设备节点 device_node 结构,该结构能够将内容层次化的组织起来。
Linux 内核将 “.dtb” 设备树内容转换为 device_node 数据结构中后,我们又该如何去访问这些设备节点内容。 不用担心 Linux 内核为我们提供了配套的设备树操作函数,在设备树上设备都是以节点表示的,因此要获取这个设备的属性信息,必须先获取到这个设备的节点,所以本质上这些函数的核心用途都用于遍历树形结构的节点啊,匹配节点,比较内容。我们目前只需要学习这些函数的使用规则,熟练使用这些函数之后就可以轻松的访问设备树的节点内容了。
这些函数被称为 OF 操作函数,之所以称为 OF 函数是因为设备树以及这些函数是集成在 OpenFrame 中的开源项目,并不是 Linux 的原创。
Linux 内核为我们提供了 5 种查找节点的方法,分别为使用节点名字查找指定节点,使用 device_type 属性查找指定节点,使用 device_type 和 compatible 这两个属性查找指定节点,使用 of_device_id 匹配表来查找指定节点,使用节点路径查找指定节点。这些方法对应 5 个与查找节点有关的 OF 操作函数。
简单的函数声明和定义位于 of.h 复杂的实现位于 base.c,更多细节可查看文件 linux\include\linux\of.h 或 linux\drivers\of\base.c
(1) 通过节点名字查找指定节点的函数,其中 from
开始查找的节点,如果为 NULL
表示从根节点开始查找整个设备树。name
表示要查找的节点名字。返回值为查找到的节点,如果为 NULL
则表示查找失败。
struct device_node *of_find_node_by_name(struct device_node *from,
const char *name));
(2) 通过 device_type 属性查找指定节点的函数,其中 from
开始查找的节点,如果为 NULL
表示从根节点开始查找整个设备树。type
表示要查找的节点对应的 type 字符串(实际就是 device_type 属性值)。返回值为查找到的节点,如果为 NULL
则表示查找失败。
struct device_node *of_find_node_by_type(struct device_node *from,
const char *type);
(3) 通过 device_type 和 compatible 这两个属性查找指定节点的函数,其中 from
开始查找的节点,如果为 NULL
表示从根节点开始查找整个设备树。type
表示要查找的节点对应的 type 字符串(实际就是 device_type 属性值),注意可以为 NULL,表示忽略掉 device_type 属性。compatible 表示要查找的节点所对应的 compatible 属性列表。返回值为查找到的节点,如果为 NULL
则表示查找失败。
struct device_node *of_find_compatible_node(struct device_node *from,
const char *type, const char *compatible);
(4) 通过 of_device_id 匹配表来查找指定节点的函数,其中 from
开始查找的节点,如果为 NULL
表示从根节点开始查找整个设备树。matches
表示of_device_id 匹配表,也就是在此匹配表里面查找节点。match
找到的匹配的 of_device_id。返回值为查找到的节点,如果为 NULL
则表示查找失败。
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);
(5) 通过节点路径查找指定节点的函数,其中 path
表示带有全路径的节点名,可以使用节点的别名,比如 “/leds”,“/pinctrl” 就是 leds,pinctrl 节点的全路径。返回值为查找到的节点,如果为 NULL
则表示查找失败。
static inline struct device_node *of_find_node_by_path(const char *path));
Linux 内核提供了几种查找指定节点对应的父节点或对应子节点的 OF 函数,有了这些函数我们就可以在已知一个节点的情况下访问其父节点或子节点。
简单的函数声明和定义位于 of.h 复杂的实现位于 base.c,更多细节可查看文件 linux\include\linux\of.h 或 linux\drivers\of\base.c
(1) 获取指定节点的父节点的函数,如果当前节点有父节点的话就可以用这个操作函数获取到父节点,node
表示当前的节点,返回值为找到的父节点,如果没有返回 NULL。
struct device_node *of_get_parent(const struct device_node *node);
(2) 获取指定节点的子节点的函数,注意这个函数可以用来迭代查找子节点, node
表示父节点,prev
表示前一个子节点,也就是从哪一个子节点开始迭代的查找下一个子节点。可以设置为 NULL,表示从第一个子节点开始。返回值为找到的下一个子节点。
struct device_node *of_get_next_child(const struct device_node *node,
struct device_node *prev);
迭代查找子节点意思是可递归的向下查找,比如查到节点 A 的子节点 B,那通过子节点 B 可以查找 B 的子节点 C,而节点 C 就是 A 的子节点的子节点。
设备树通过节点的属性(变量)保存内容,内容包含驱动的硬件配置,驱动的状态等等,要访问这些信息的前提是要能够访问到这些属性,前面查找节点就是为了定位节点的属性所在位置,为访问属性做准备的。好的既然我们能够定位属性了,那接下来就是将定位到的属性内容提取出来。
Linux 内核中使用结构体 property 表示属性,“.dtb” 设备树被内核解析后节点属性信息就被保存在该结构体中。
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.h 复杂的实现位于 base.c,更多细节可查看文件 linux\include\linux\of.h 或 \linux\drivers\of\property.c
(1) 查找指定属性的函数,其中 np
表示设备节点,name
表示属性的字符串名,lenp
指定属性字节数。返回值为找到的属性。
struct property *of_find_property(const struct device_node *np,
const char *name,
int *lenp);
(2) 获取属性中元素数量的函数,其中 np
表示设备节点,propnam
表示需要统计元素数量的属性字符串名称,elem_size
表示元素长度。返回值为得到的属性元素数量。
int of_property_count_elems_of_size(const struct device_node *np,
const char *propname, int elem_size);
(3) 获取属性中指定标号的 u32
类型数据值的函数,其中 np
表示设备节点,propnam
要读取的属性字符串名称,index
表示要读取的值标号,out_val
表示读取到的值。返回值为 0 表示读取成功,负值表示读取失败,-EINVAL 表示属性不存在,而 -ENODATA 表示没有要读取的数据,-EOVERFLOW 表示属性值列表太小。
int of_property_read_u32_index(const struct device_node *np,
const char *propname,
u32 index, u32 *out_value);
比如某个属性有多个 u32 类型的值,那么就可以利用该操作函数获取指定标号的数据值。
(4) 读取属性中 u8,u16,u32 和 u64 类型数组值的函数,其中 np
表示设备节点,propnam
要读取的属性字符串名称,out_value
表示读取到的数组值,类型分别为 u8,u16,u32 和 u64,sz
表示要读取的数组元素数量。返回值 0 表示读取成功,负值表示读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没有要读取的数据,-EOVERFLOW 表示属性值列表太小。
static inline int of_property_read_u8_array(const struct device_node *np,
const char *propname,
u8 *out_values, size_t sz);
static inline int of_property_read_u16_array(const struct device_node *np,
const char *propname,
u16 *out_values, size_t sz);
static inline int of_property_read_u32_array(const struct device_node *np,
const char *propname,
u32 *out_values, size_t sz);
static inline int of_property_read_u64_array(const struct device_node *np,
const char *propname,
u64 *out_values, size_t sz);
比如设备树中大多数的 reg 属性都是属于数组类型的,根据数组类型可以分别使用这 4 个函数中的一个一次读取出 reg 属性中的所有数据。
(5) 读取属性中 u8,u16,u32 和 u64 类型值的函数,其中 np
表示设备节点,propnam
要读取的属性字符串名称,out_value
表示读取到的数值。返回值 0 表示读取成功,负值表示读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没有要读取的数据,-EOVERFLOW 表示属性值列表太小。
static inline int of_property_read_u8(const struct device_node *np,
const char *propname,
u8 *out_value);
static inline int of_property_read_u16(const struct device_node *np,
const char *propname,
u16 *out_value);
static inline 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);
设备树节点有些属性只有一个整形值,读取属性的整形值就可以使用上述内核提供的4 个操作函数,根据属性整形值的范围选择相应的函数。
(6) 读取属性中字符串值的函数,其中 np
表示设备节点,propnam
要读取的属性字符串名称,out_string
表示读取到的字符串值。返回值 0 表示读取成功,负值表示读取失败。
int of_property_read_string(const struct device_node *np, const char *propname,
const char **out_string);
(7) 获取设备树中 #address-cells 属性值的函数,其中 np
表示设备节点。返回值为获取到的 #address-cells 属性值,注意这是一个整形值。
int of_n_addr_cells(struct device_node *np);
(8) 获取设备树中 #size-cells 属性值的函数,其中 np
表示设备节点。返回值为获取到的 #size-cells 属性值,注意这是一个整形值。
int of_n_size_cells(struct device_node *np);
(1) 查看节点的 compatible 属性是否有包含形参 compat 指定的字
符串的函数,也就是检查设备节点的兼容性。其中 device
表示设备节点,compat
要查看(比较)的字符串。返回值 0 表示节点的 compatible 属性中不包含 compat 指定的字符串,正数表示节点的 compatible 属性中包含 compat 指定的字符串。
int of_device_is_compatible(const struct device_node *device,
const char *compat);
简单的函数声明和定义位于 of.h 复杂的实现位于 base.c,更多细节可查看文件 linux\linux-6.4.3\drivers\of\base.c
(2) 获取地址相关属性的函数,主要是 “reg” 或者 “assigned-addresses” 属性
值。其中 dev
表示设备节点,index
要读取的地址标号,size
地址长度,flags
参数,比如 IORESOURCE_IO,IORESOURCE_MEM 等。返回值:读取到的地址数据首地址,为 NULL 的话表示读取失败。
const __be32 *of_get_address(struct device_node *dev, int index, u64 *size,
unsigned int *flags);
简单的函数声明和定义位于 of_address.h 复杂的实现位于 of_address.c,更多细节可查看文件 linux\include\linux\of_address.h
(3) 将从设备树读取到的地址转换为物理地址的函数,其中 dev
表示设备节点,in_addr
要转换的地址。返回值为得到的物理地址,如果为 OF_BAD_ADDR 的话表示转换失败。
u64 of_translate_address(struct device_node *dev, const __be32 *in_addr);
简单的函数声明和定义位于 of_address.h 复杂的实现位于 of_address.c,更多细节可查看文件 linux\include\linux\of_address.c
(4) 将 reg 属性值,转换为 resource 结构体类型的函数,其中 dev
表示设备节点,index
地址资源标号,r
得到的 resource 类型的资源值。返回值为 0 成功,负值表示失败。
int of_address_to_resource(struct device_node *dev, int index,
struct resource *r);
简单的函数声明和定义位于 of_address.h 复杂的实现位于 of_address.c,更多细节可查看文件 linux\include\linux\of_address.c
I2C,SPI,GPIO 等这些外设都有对应的寄存器,这些寄存器其实就是一组内存空间,Linux 内核使用 resource 结构体来描述一段内存空间,“resource” 翻译出来就是 资源,因此用 resource结构体描述的都是设备资源信息,resource 结构体定义在 linux/include/linux/ioport.h 文件中,如下。
/*
* Resources are tree-like, allowing
* nesting etc..
*/
struct resource {
resource_size_t start;
resource_size_t end;
const char *name;
unsigned long flags;
unsigned long desc;
struct resource *parent,
*sibling, *child;
};
对于 32 位的 SOC 来说,resource_size_t 是 u32 类型的。其中 start
表示开始地址,end
表示结束地址,name
表示资源的名称,flags
是资源标志位,一般表示资源类型,可选的资源标志位有以下定义,详细可查看看 ioport.h 文件。
#define IORESOURCE_BITS 0x000000ff /* Bus-specific bits */
#define IORESOURCE_TYPE_BITS 0x00001f00 /* Resource type */
#define IORESOURCE_IO 0x00000100 /* PCI/ISA I/O ports */
#define IORESOURCE_MEM 0x00000200
#define IORESOURCE_REG 0x00000300 /* Register offsets */
#define IORESOURCE_IRQ 0x00000400
#define IORESOURCE_DMA 0x00000800
#define IORESOURCE_BUS 0x00001000
#define IORESOURCE_PREFETCH 0x00002000 /* No side effects */
#define IORESOURCE_READONLY 0x00004000
#define IORESOURCE_CACHEABLE 0x00008000
#define IORESOURCE_RANGELENGTH 0x00010000
#define IORESOURCE_SHADOWABLE 0x00020000
#define IORESOURCE_SIZEALIGN 0x00040000 /* size indicates alignment */
#define IORESOURCE_STARTALIGN 0x00080000 /* start field is alignment */
#define IORESOURCE_MEM_64 0x00100000
#define IORESOURCE_WINDOW 0x00200000 /* forwarded by bridge */
#define IORESOURCE_MUXED 0x00400000 /* Resource is software muxed */
#define IORESOURCE_EXT_TYPE_BITS 0x01000000 /* Resource extended types */
#define IORESOURCE_SYSRAM 0x01000000 /* System RAM (modifier) */
/* IORESOURCE_SYSRAM specific bits. */
#define IORESOURCE_SYSRAM_DRIVER_MANAGED 0x02000000 /* Always detected via a driver. */
#define IORESOURCE_SYSRAM_MERGEABLE 0x04000000 /* Resource can be merged. */
#define IORESOURCE_EXCLUSIVE 0x08000000 /* Userland may not map this resource */
#define IORESOURCE_DISABLED 0x10000000
#define IORESOURCE_UNSET 0x20000000 /* No address assigned yet */
#define IORESOURCE_AUTO 0x40000000
#define IORESOURCE_BUSY 0x80000000 /* Driver has marked this resource busy */
(5) 用于直接内存映射的函数,其中 np
表示设备节点,index
reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为 0。返回值为经过内存映射后的虚拟内存首地址,如果为 NULL 的话表示内存映射失败。
void __iomem *of_iomap(struct device_node *np, int index);
简单的函数声明和定义位于 of_address.h 复杂的实现位于 of_address.c,更多细节可查看文件 linux\include\linux\of_address.c
以前我们会通过 ioremap 函数来完成物理地址到虚拟地址的映射,采用设备树以后就可以直接通过 of_iomap 函数来获取内存地址所对应的虚拟地址,而不再需要使用 ioremap 函数。不过依旧可以使用 ioremap 函数来完成物理地址到虚拟地址的内存映射,但使用设备树后建议用 of_iomap 函数。
以上就是 Linux 设备树常用的 OF 操作函数相关的知识点,本文如果对你有帮助不介意的话帮忙点个支持一下呗。