LV06-10-设备树-04-设备树的应用

怎么使用设备树?若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
PC端开发环境 Windows Windows11
Ubuntu Ubuntu20.04.2的64位版本
VMware® Workstation 17 Pro 17.6.0 build-24238078
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Win32DiskImager Win32DiskImager v1.0
Linux开发板环境 Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04
linux内核 linux-4.19.71(NXP官方提供)
点击查看本文参考资料
分类 网址 说明
官方网站 https://www.arm.com/ ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.nxp.com.cn/ NXP官方网站
https://www.nxpic.org.cn/NXP 官方社区
https://u-boot.readthedocs.io/en/latest/u-boot官网
https://www.kernel.org/linux内核官网
点击查看相关文件下载
分类 网址 说明
NXP https://github.com/nxp-imx NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库
nxp-imx/linux-imx/releases/tag/v4.19.71 NXP linux内核仓库tags中的v4.19.71
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0
I.MX6ULL i.MX 6ULL Applications Processors for Industrial Products I.MX6ULL 芯片手册(datasheet,可以在线查看)
i.MX 6ULL Applications ProcessorReference Manual I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网)
Source Code https://elixir.bootlin.com/linux/latest/source linux kernel源码
kernel/git/stable/linux.git - Linux kernel stable tree linux kernel源码(官网,tag 4.19.71)
https://elixir.bootlin.com/u-boot/latest/source uboot源码

一、内核操作设备树的函数

内核源码中 linux - include/linux 目录下有很多 of 开头的头文件, of 表示“open firmware”即开放固件。

image-20250225164508489

1. 内核中设备树相关的头文件介绍

前面我们已经了解过了,设备树的处理过程是: dtb -> device_node -> platform_device。

1.1 处理 DTB

  • of_fdt.h - include/linux/of_fdt.h:dtb 文件的相关操作函数, 我们一般用不到, 因为 dtb 文件在内核中已经被转换为 device_node 树(它更易于使用)

1.2 处理 device_node

1.3 处理 platform_device

比如 of_device_alloc(根据 device_node 分配设置 platform_device),of_find_device_by_node (根据 device_node 查找到 platform_device),of_platform_bus_probe (处理 device_node 及它的子节点)

2. OF函数

设备树描述了设备的详细信息,这些信息包括数字类型的、字符串类型的、数组类型的,我们在编写驱动的时候需要获取到这些信息。比如设备树使用 reg 属性描述了某个外设的寄存器地址为 0X02005482,长度为 0X400,我们在编写驱动的时候需要获取到 reg 属性的0X02005482 和 0X400 这两个值,然后初始化外设。

Linux 内核给我们提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_”,所以在很多资料里面也被叫做 OF 函数。这些 OF 函数原型一般都定义在 of.h - include/linux/of.h 文件中。在使用的时候要包含#include <linux/of.h>文件。

在内核中以 device_node 结构体来对设备树进行描述, 所以 of 操作函数实际上就是获取device_node 结构体 。

2.1 获取设备树节点

2.1.1 of_find_node_by_name()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct device_node *of_find_node_by_name(struct device_node *from,
const char *name)
{
struct device_node *np;
unsigned long flags;

raw_spin_lock_irqsave(&devtree_lock, flags);
for_each_of_allnodes_from(from, np)
if (np->name && (of_node_cmp(np->name, name) == 0)
&& of_node_get(np))
break;
of_node_put(from);
raw_spin_unlock_irqrestore(&devtree_lock, flags);
return np;
}
EXPORT_SYMBOL(of_find_node_by_name);

该函数通过指定的节点名称在设备树中进行查找, 返回匹配的节点的 struct device_node 指针。但是在设备树的官方规范中不建议使用“ name”属性,所以这函数也不建议使用。

参数说明:

  • from: 指定起始节点, 表示从哪个节点开始查找。 如果 from 参数为 NULL, 则从设备树的根节点开始查找。
  • name: 要查找的节点名称。

返回值:如果找到匹配的节点, 则返回对应的 struct device_node 指针。如果未找到匹配的节点, 则返回 NULL。

2.1.2 of_find_node_by_path()

1
2
3
4
static inline struct device_node *of_find_node_by_path(const char *path)
{
return of_find_node_opts_by_path(path, NULL);
}

该函数通过节点路径在设备树中进行查找。 路径是设备树节点从根节点到目标节点的完整路径。 可以通过指定正确的路径来准确地访问设备树中的特定节点。使用该函数时, 可以直接传递节点的完整路径作为 path 参数, 函数会在设备树中查找匹配的节点。 这对于已知节点路径的情况非常有用。

参数说明:

  • path: 节点的路径, 以斜杠分隔的字符串表示。 路径格式为设备树节点的绝对路径, 例如 / topeet/myLed。

返回值:如果找到匹配的节点, 则返回对应的 struct device_node 指针。如果未找到匹配的节点, 则返回 NULL。

2.1.3 of_find_node_by_type()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct device_node *of_find_node_by_type(struct device_node *from,
const char *type)
{
struct device_node *np;
unsigned long flags;

raw_spin_lock_irqsave(&devtree_lock, flags);
for_each_of_allnodes_from(from, np)
if (np->type && (of_node_cmp(np->type, type) == 0)
&& of_node_get(np))
break;
of_node_put(from);
raw_spin_unlock_irqrestore(&devtree_lock, flags);
return np;
}
EXPORT_SYMBOL(of_find_node_by_type);

该函数根据类型找到节点,节点如果定义了 device_type 属性,那我们可以根据类型找到它。但是在设备树的官方规范中不建议使用“ device_type”属性,所以这函数也不建议使用。

参数说明:

  • from: 指定起始节点, 表示从哪个节点开始查找。 如果 from 参数为 NULL, 则从设备树的根节点开始查找。
  • type: 要查找的节点的 device_type 属性。

返回值:如果找到匹配的节点, 则返回对应的 struct device_node 指针。如果未找到匹配的节点, 则返回 NULL。

2.1.4 of_find_compatible_node()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct device_node *of_find_compatible_node(struct device_node *from,
const char *type, const char *compatible)
{
struct device_node *np;
unsigned long flags;

raw_spin_lock_irqsave(&devtree_lock, flags);
for_each_of_allnodes_from(from, np)
if (__of_device_is_compatible(np, compatible, type, NULL) &&
of_node_get(np))
break;
of_node_put(from);
raw_spin_unlock_irqrestore(&devtree_lock, flags);
return np;
}
EXPORT_SYMBOL(of_find_compatible_node);

该函数根据 compatible 找到节点,节点如果定义了 compatible 属性,那我们可以根据 compatible 属性找到它。

参数说明:

  • from: 指定起始节点, 表示从哪个节点开始查找。 如果 from 参数为 NULL, 则从设备树的根节点开始查找。
  • type: 要匹配的设备类型字符串,用来指定 device_type 属性的值,可以传入 NULL,表示忽略掉 device_type 属性。
  • compatible: 要匹配的兼容性字符串, 通常是设备树节点的 compatible 属性中的值。

返回值:如果找到匹配的节点, 则返回对应的 struct device_node 指针。如果未找到匹配的节点, 则返回 NULL。

使用 of_find_compatible_node() 函数时, 可以指定起始节点和需要匹配的设备类型字符串以及兼容性字符串。 函数会从起始节点开始遍历设备树, 查找与指定兼容性字符串匹配的节点,并返回匹配节点的指针。

2.1.5 of_find_node_by_phandle()

1
2
3
4
5
struct device_node *of_find_node_by_phandle(phandle handle)
{
//......
}
EXPORT_SYMBOL(of_find_node_by_phandle);

该函数根据 phandle 找到节点。 dts 文件被编译为 dtb 文件时,每一个节点都有一个数字 ID,这些数字 ID 彼此不同。可以使用数字 ID 来找到 device_node。这些数字 ID 就是 phandle。

参数说明:

  • phandle: 要查找节点的数字id。

返回值:如果找到匹配的节点, 则返回对应的 struct device_node 指针。如果未找到匹配的节点, 则返回 NULL。

2.1.6 of_find_matching_node_and_match()

1
2
3
4
5
6
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

返回值:如果找到匹配的节点, 则返回对应的 struct device_node 指针。如果未找到匹配的节点, 则返回 NULL。

of_find_matching_node_and_match()函数在设备树中遍历节点, 对每个节点使用 __of_match_node() 函数进行匹配。 如果找到匹配的节点, 将返回该节点的指针, 并将 match 指针更新为匹配到的 of_device_id 条目, 函数会自动增加匹配节点的引用计数。下面是一个简单示例:

1
2
3
4
5
6
7
8
9
#include <linux/of.h>
static const struct of_device_id my_match_table[] = {
{ .compatible = "vendor,device" },
{ /* sentinel */ }
};
const struct of_device_id *match;
struct device_node *np;
// 从根节点开始查找匹配的节点
np = of_find_matching_node_and_match(NULL, my_match_table, &match);

我们定义了一个 of_device_id 匹配表 my_match_table, 其中包含了一个兼容性字符串为”vendor,device”的匹配项。 然后, 我们使用 of_find_matching_node_and_match 函数从根节点开始查找匹配的节点。

2.2 查找父子节点

2.2.1 of_get_parent()

1
2
3
4
5
6
7
8
9
10
11
12
13
struct device_node *of_get_parent(const struct device_node *node)
{
struct device_node *np;
unsigned long flags;

if (!node)
return NULL;

raw_spin_lock_irqsave(&devtree_lock, flags);
np = of_node_get(node->parent);
raw_spin_unlock_irqrestore(&devtree_lock, flags);
return np;
}

该函数接收一个指向设备树节点的指针 node, 并返回该节点的父节点(如果有父节点的话)的指针。

参数说明:

  • node: 要获取父节点的设备树节点指针。

返回值:如果找到匹配的节点, 则返回对应的 struct device_node 指针。如果未找到匹配的节点, 则返回 NULL。

使用 of_get_parent() 函数时, 可以将特定的设备树节点作为参数传递给函数, 然后它将返回该节点的父节点。 这对于在设备树中导航和访问节点之间的层次关系非常有用。

父节点在设备树中表示了节点之间的层次结构关系。 通过获取父节点, 我们可以访问上一级节点的属性和配置信息, 从而更好地理解设备树中的节点之间的关系。

2.2.2 of_get_next_parent()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct device_node *of_get_next_parent(struct device_node *node)
{
struct device_node *parent;
unsigned long flags;

if (!node)
return NULL;

raw_spin_lock_irqsave(&devtree_lock, flags);
parent = of_node_get(node->parent);
of_node_put(node);
raw_spin_unlock_irqrestore(&devtree_lock, flags);
return parent;
}
EXPORT_SYMBOL(of_get_next_parent);

这个函数名比较奇怪,怎么可能有“ next parent”?它实际上也是找到 device_node 的父节点,跟 of_get_parent() 的返回结果是一样的。差别在于它多调用 of_node_put() 函数,把 node 节点的引用计数减少了 1。这意味着调用 of_get_next_parent() 之后,我们不再需要调用 of_node_put() 释放 node 节点

参数说明:

  • node: 要获取父节点的设备树节点指针。

返回值:如果找到匹配的节点, 则返回对应的 struct device_node 指针。如果未找到匹配的节点, 则返回 NULL。

2.2.3 of_get_next_child()

1
2
3
4
5
6
7
8
9
10
11
12
struct device_node *of_get_next_child(const struct device_node *node,
struct device_node *prev)
{
struct device_node *next;
unsigned long flags;

raw_spin_lock_irqsave(&devtree_lock, flags);
next = __of_get_next_child(node, prev);
raw_spin_unlock_irqrestore(&devtree_lock, flags);
return next;
}
EXPORT_SYMBOL(of_get_next_child);

该函数函数用于获取设备树节点的下一个子节点。

参数说明:

  • node: 当前节点, 用于指定要获取子节点的起始节点。
  • prev: 上一个子节点, 用于指定从哪个子节点开始获取下一个子节点。 如果为 NULL, 则从起始节点的第一个子节点开始。

返回值:如果找到匹配的节点, 则返回对应的 struct device_node 指针。如果未找到匹配的节点, 则返回 NULL。

使用 of_get_next_child() 函数时, 可以传递当前节点以及上一个子节点作为参数。 函数将从上一个子节点的下一个节点开始, 查找并返回下一个子节点。设备树中的子节点表示了节点之间的层次关系。 通过获取子节点, 可以遍历和访问当前节点的所有子节点, 以便进一步处理它们的属性和配置信息。

2.2.4 of_get_next_available_child()

1
2
3
4
5
struct device_node *of_get_next_available_child(const struct device_node *node,
struct device_node *prev)
{
//......
}

该函数取出下一个“可用”的子节点,有些节点的 status 是“disabled”,那就会跳过这些节点。

参数说明:

  • node: 当前节点, 用于指定要获取子节点的起始节点。
  • prev: 上一个子节点, 用于指定从哪个子节点开始获取下一个子节点。 如果为 NULL, 则从起始节点的第一个子节点开始。

返回值:如果找到匹配的节点, 则返回对应的 struct device_node 指针。如果未找到匹配的节点, 则返回 NULL。

2.2.5 of_get_child_by_name()

1
2
3
4
5
6
7
8
9
10
struct device_node *of_get_child_by_name(const struct device_node *node,
const char *name)
{
struct device_node *child;

for_each_child_of_node(node, child)
if (child->name && (of_node_cmp(child->name, name) == 0))
break;
return child;
}

该函数根据名字取出子节点。

参数说明:

  • node: 当前节点, 用于指定要获取子节点的起始节点。
  • name: 表示子节点的名字。

返回值:如果找到匹配的节点, 则返回对应的 struct device_node 指针。如果未找到匹配的节点, 则返回 NULL。

2.3 获取属性

节点的属性信息里面保存了驱动所需要的内容,因此对于属性值的提取非常重要, Linux 内核中使用结构体 struct property 表示属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
};

2.3.1 of_find_property()

1
2
3
4
5
6
7
8
9
10
11
12
13
struct property *of_find_property(const struct device_node *np,
const char *name,
int *lenp)
{
struct property *pp;
unsigned long flags;

raw_spin_lock_irqsave(&devtree_lock, flags);
pp = __of_find_property(np, name, lenp);
raw_spin_unlock_irqrestore(&devtree_lock, flags);

return pp;
}

该函数用于在节点 np 下查找指定名称 name 的属性。

参数说明:

  • np: 要查找的节点。
  • name: 要查找的属性的属性名。
  • lenp: 一个指向整数的指针, 用于接收属性值的字节数。

返回值:如果成功找到了指定名称的属性, 则返回对应的属性结构体指针 struct property * ; 如果未找到, 则返回 NULL。

示例:

在设备树中,节点大概是这样:

1
2
3
xxx_node {
xxx_pp_name = “hello”;
};

上述节点中,“ xxx_pp_name”就是属性的名字,值的长度是 6。

2.3.2 of_property_count_elems_of_size()

1
2
3
4
5
int of_property_count_elems_of_size(const struct device_node *np,
const char *propname, int elem_size)
{
//......
}

该函数在设备树中的设备节点下查找指定名称的属性, 并获取该属性中元素的数量。 调用该函数可以用于获取设备树属性中某个属性的元素数量, 比如一个字符串列表的元素数量或一个整数数组的元素数量等,在比如我们常见的 reg 属性值就是一个数组,那么使用此函数可以获取到这个数组的大小 。

参数说明:

  • np: 设备节点。
  • propname: 需要获取元素数量的属性名。
  • elem_size: 单个元素的尺寸。

返回值:如果成功获取了指定属性中元素的数量, 则返回该数量; 如果未找到属性或属性中没有元素, 则返回 0。

示例:

在设备树中,节点大概是这样:

1
2
3
xxx_node {
xxx_pp_name = <0x50000000 1024> <0x60000000 2048>;
};
  • 调用 of_property_count_elems_of_size(np, “xxx_pp_name”, 8)时,返回值是 2;
  • 调用 of_property_count_elems_of_size(np, “xxx_pp_name”, 4)时,返回值是 4。

2.3.3 读数组 u8/u16/u32/u64

下面四个函数定义在:property.c - drivers/of/property.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//从指定属性中读取变长的 u8 数组
int of_property_read_variable_u8_array(const struct device_node *np,
const char *propname, u8 *out_values,
size_t sz_min, size_t sz_max);
//从指定属性中读取变长的 u16 数组
int of_property_read_variable_u16_array(const struct device_node *np,
const char *propname, u16 *out_values,
size_t sz_min, size_t sz_max);
//从指定属性中读取变长的 u32 数组
int of_property_read_variable_u32_array(const struct device_node *np,
const char *propname, u32 *out_values,
size_t sz_min, size_t sz_max);
//从指定属性中读取变长的 u64 数组
int of_property_read_variable_u64_array(const struct device_node *np,
const char *propname, u64 *out_values,
size_t sz_min, size_t sz_max)

这几个函数用于从设备树中读取指定属性名的变长数组。 通过提供设备节点、 属性名和输出数组的指针, 可以将设备树中的数组数据读取到指定的内存区域中。 同时, 还需要指定数组的最小大小和最大大小, 以确保读取到的数组符合预期的大小范围。

参数说明:

  • np: 设备节点。propname: 要读取的属性名。
  • out_values: 用于存储读取到的 u8/u16/u32/u64 数组的指针。
  • sz_min: 数组的最小大小。
  • sz_max: 数组的最大大小。

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

示例:

在设备树中,节点大概是这样:

1
2
3
xxx_node {
name2 = <0x50000012 0x60000034>;
};

调用 of_property_read_variable_u8_array (np, “name2”, out_values, 1, 10)时,out_values 中将会保存这 8 个字节: 0x12,0x00,0x00,0x50,0x34,0x00,0x00,0x60。

调用 of_property_read_variable_u16_array (np, “name2”, out_values, 1, 10)时,out_values 中将会保存这 4 个 16 位数值: 0x0012, 0x5000,0x0034,0x6000。

总之,这些函数要么能取到全部的数值,要么一个数值都取不到;如果值的长度在 sz_min 和 sz_max 之间,就返回全部的数值;否则一个数值都不返回。

点击查看详情

从这几个函数衍生出下面一组函数:

  • 这里的四个函数会调用of_property_read_variable_uX_array ()

of_property_read_u8_array()of_property_read_u16_array()of_property_read_u32_array()of_property_read_u64_array()

2.3.4 读整数 u8/u16/u32/u64

下面的三个函数定义在这里:of.h - include/linux/of.h1162 - 1181

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static inline int of_property_read_u8(const struct device_node *np,
const char *propname,
u8 *out_value)
{
return of_property_read_u8_array(np, propname, out_value, 1);
}

static inline int of_property_read_u16(const struct device_node *np,
const char *propname,
u16 *out_value)
{
return of_property_read_u16_array(np, propname, out_value, 1);
}

static inline int of_property_read_u32(const struct device_node *np,
const char *propname,
u32 *out_value)
{
return of_property_read_u32_array(np, propname, out_value, 1);
}

读u64整数的函数与上面不太一样,定义在property.c - drivers/of/property.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int of_property_read_u64(const struct device_node *np, const char *propname,
u64 *out_value)
{
const __be32 *val = of_find_property_value_of_size(np, propname,
sizeof(*out_value),
0,
NULL);

if (IS_ERR(val))
return PTR_ERR(val);

*out_value = of_read_number(val, 2);
return 0;
}

示例:

在设备树中,节点大概是这样:

1
2
3
4
xxx_node {
name1 = <0x50000000>;
name2 = <0x50000000 0x60000000>;
};

调用 of_property_read_u32 (np, “name1”, &val)时, val 将得到值 0x50000000;

调用 of_property_read_u64 (np, “name2”, &val)时, val 将得到值 0x6000000050000000。

2.3.5 读某个整数 u32

of_property_read_u32_index()函数用于读取某个u32的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int of_property_read_u32_index(const struct device_node *np,
const char *propname,
u32 index, u32 *out_value)
{
const u32 *val = of_find_property_value_of_size(np, propname,
((index + 1) * sizeof(*out_value)),
0,
NULL);

if (IS_ERR(val))
return PTR_ERR(val);

*out_value = be32_to_cpup(((__be32 *)val) + index);
return 0;
}

该函数在设备树中的设备节点下查找指定名称的属性, 并获取该属性在给定索引位置处的u32 类型的数据值。这个函数通常用于从设备树属性中读取特定索引位置的整数值。 通过指定属性名和索引,可以获取属性中指定位置的具体数值。

参数说明:

  • np: 设备节点。
  • propname: 要读取的属性名。
  • index: 要读取的属性值在属性中的索引, 索引从 0 开始。
  • out_value: 用于存储读取到的值的指针。

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

示例:

在设备树中,节点大概是这样:

1
2
3
xxx_node {
name2 = <0x50000000 0x60000000>;
}

调用 of_property_read_u32 (np, “name2”, 1, &val)时, val 将得到值 0x60000000。

2.3.6 读某个整数 u64

of_property_read_u64_index()用于读取某个u64的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int of_property_read_u64_index(const struct device_node *np,
const char *propname,
u32 index, u64 *out_value)
{
const u64 *val = of_find_property_value_of_size(np, propname,
((index + 1) * sizeof(*out_value)),
0, NULL);

if (IS_ERR(val))
return PTR_ERR(val);

*out_value = be64_to_cpup(((__be64 *)val) + index);
return 0;
}

该函数在设备树中的设备节点下查找指定名称的属性, 并获取该属性在给定索引位置处的u64 类型的数据值。这个函数通常用于从设备树属性中读取特定索引位置的 64 位整数值。 通过指定属性名和索引, 可以获取属性中指定位置的具体数值。

参数说明:

  • np: 设备节点。
  • propname: 要读取的属性名。
  • index: 要读取的属性值在属性中的索引, 索引从 0 开始。
  • out_value: 用于存储读取到的值的指针。

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

2.3.7 读字符串

of_property_read_string()函数用于读取属性中字符串值:

1
2
3
4
5
6
7
8
9
10
11
12
13
int of_property_read_string(const struct device_node *np, const char *propname,
const char **out_string)
{
const struct property *prop = of_find_property(np, propname, NULL);
if (!prop)
return -EINVAL;
if (!prop->value)
return -ENODATA;
if (strnlen(prop->value, prop->length) >= prop->length)
return -EILSEQ;
*out_string = prop->value;
return 0;
}

该函数在设备树中的设备节点下查找指定名称的属性, 并获取该属性的字符串值, 最后返回读取到的字符串的指针, 通常用于从设备树属性中读取字符串值。 通过指定属性名, 可以获取属性中的字符串数据。

参数说明:

  • np: 设备节点。
  • propname: 要读取的属性名。
  • out_string: 用于存储读取到的字符串的指针。

返回值:如果成功读取到了指定属性的字符串, 则返回 0; 如果未找到属性或读取失败, 则返回相应的错误码。

2.3.8 读获取#address-cells

of_n_addr_cells()函数用于读取#address-cells 属性值 :

1
2
3
4
5
6
7
8
9
10
11
12
13
int of_n_addr_cells(struct device_node *np)
{
u32 cells;

do {
if (np->parent)
np = np->parent;
if (!of_property_read_u32(np, "#address-cells", &cells))
return cells;
} while (np->parent);
/* No #address-cells property for the root node */
return OF_ROOT_NODE_ADDR_CELLS_DEFAULT;
}

参数说明:

  • np: 要查找的节点。

返回值:获取到的#address-cells 属性值。

2.3.9 读取#size-cells

of_n_size_cells()用于读取#size-cells 属性值:

1
2
3
4
5
6
7
8
9
10
11
12
13
int of_n_size_cells(struct device_node *np)
{
u32 cells;

do {
if (np->parent)
np = np->parent;
if (!of_property_read_u32(np, "#size-cells", &cells))
return cells;
} while (np->parent);
/* No #size-cells property for the root node */
return OF_ROOT_NODE_SIZE_CELLS_DEFAULT;
}

参数说明:

  • np: 要查找的节点。

返回值:获取到的#size-cells 属性值。

2.4 其他函数

2.4.1 of_device_is_compatible()

of_device_is_compatible() 用于函数用于查看节点的 compatible 属性是否有包含 compat 指定的字符串,也就是检查设备节点的兼容性 :

1
2
3
4
5
6
7
8
9
10
11
int of_device_is_compatible(const struct device_node *device,
const char *compat)
{
unsigned long flags;
int res;

raw_spin_lock_irqsave(&devtree_lock, flags);
res = __of_device_is_compatible(device, compat, NULL, NULL);
raw_spin_unlock_irqrestore(&devtree_lock, flags);
return res;
}

参数说明:

  • device:设备节点。
  • compat:要查看的字符串。

返回值:0,节点的 compatible 属性中不包含 compat 指定的字符串; 正数,节点的 compatible属性中包含 compat 指定的字符串。

2.4.2 of_get_address()

of_get_address()用于函数用于获取地址相关属性,主要是“reg”或者“assigned-addresses”属性值 :

1
2
3
4
5
const __be32 *of_get_address(struct device_node *dev, int index, u64 *size,
unsigned int *flags)
{
//......
}

参数说明:

  • dev:设备节点。
  • index:要读取的地址标号。
  • size:地址长度。
  • flags:参数,比如 IORESOURCE_IO、 IORESOURCE_MEM 等

返回值:读取到的地址数据首地址,为 NULL 的话表示读取失败。

2.4.3 of_translate_address()

of_translate_address()用于函数负责将从设备树读取到的地址转换为物理地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
u64 of_translate_address(struct device_node *dev, const __be32 *in_addr)
{
struct device_node *host;
u64 ret;

ret = __of_translate_address(dev, in_addr, "ranges", &host);
if (host) {
of_node_put(host);
return OF_BAD_ADDR;
}

return ret;
}

参数说明:

  • dev:设备节点。
  • in_addr:要转换的地址。

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

2.4.4 of_address_to_resource()

of_address_to_resource()函数看名字像是从设备树里面提取资源值,但是本质上就是将 reg 属性值,然后将其转换为 resource 结构体类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int of_address_to_resource(struct device_node *dev, int index,
struct resource *r)
{
const __be32 *addrp;
u64 size;
unsigned int flags;
const char *name = NULL;

addrp = of_get_address(dev, index, &size, &flags);
if (addrp == NULL)
return -EINVAL;

/* Get optional "reg-names" property to add a name to a resource */
of_property_read_string_index(dev, "reg-names", index, &name);

return __of_address_to_resource(dev, addrp, size, flags, name, r);
}

参数说明:

  • dev:设备节点。
  • index:地址资源标号。
  • r:得到的 resource 类型的资源值。

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

关于resource

IIC、 SPI、 GPIO 等这些外设都有对应的寄存器,这些寄存器其实就是一组内存空间, Linux内核使用 resource 结构体来描述一段内存空间,“resource”翻译出来就是“资源”,因此用 resource结构体描述的都是设备资源信息,resource 结构体定义在文件 ioport.h - include/linux/ioport.h中,定义如下:

1
2
3
4
5
6
7
8
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 - include/linux/ioport.h 中,如下所示

1
2
3
4
5
6
7
8
9
10
#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
//......

一般最常见的资源标志就是 IORESOURCE_MEM 、 IORESOURCE_REG 和IORESOURCE_IRQ 等。

2.4.5 of_iomap()

of_iomap()函数用于直接内存映射,以前我们会通过 ioremap() 函数来完成物理地址到虚拟地址的映射,采用设备树以后就可以直接通过 of_iomap() 函数来获取内存地址所对应的虚拟地址,不需要使用 ioremap() 函数了。当然了,也可以使用 ioremap() 函数来完成物理地址到虚拟地址的内存映射,只是在采用设备树以后,大部分的驱动都使用 of_iomap() 函数了。 of_iomap() 函数本质上也是将 reg 属性中地址信息转换为虚拟地址,如果 reg 属性有多段的话,可以通过 index 参数指定要完成内存映射的是哪一段, of_iomap() 函数原型如下

1
2
3
4
5
6
7
8
9
void __iomem *of_iomap(struct device_node *np, int index)
{
struct resource res;

if (of_address_to_resource(np, index, &res))
return NULL;

return ioremap(res.start, resource_size(&res));
}

参数说明:

  • np:设备节点。
  • index: reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话, index 就设置为 0。

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

3. platform_device 相关的函数

我们知道,设备树的节点有一些会转化成platform_device,针对这种类型的节点,也有几个相关的函数。

3.1 of_find_device_by_node()

1
2
3
4
5
6
7
8
struct platform_device *of_find_device_by_node(struct device_node *np)
{
struct device *dev;

dev = bus_find_device(&platform_bus_type, NULL, np, of_dev_node_match);
return dev ? to_platform_device(dev) : NULL;
}
EXPORT_SYMBOL(of_find_device_by_node);

设备树中的每一个节点,在内核里都有一个 device_node;我们可以使用device_node 去找到对应的 platform_device。

3.2 platform_get_resource()

这个函数跟设备树没什么关系,但是设备树中的节点被转换为platform_device 后,设备树中的 reg 属性、 interrupts 属性也会被转换为“ resource”,这个时候我们可以通过这个函数获取这些资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct resource *platform_get_resource(struct platform_device *dev,
unsigned int type, unsigned int num)
{
int i;

for (i = 0; i < dev->num_resources; i++) {
struct resource *r = &dev->resource[i];

if (type == resource_type(r) && num-- == 0)
return r;
}
return NULL;
}
EXPORT_SYMBOL_GPL(platform_get_resource);

参数说明:

  • dev:struct platform_device类型指针,包含了设备树节点的相关信息。
  • type:资源的类型,IORESOURCE_MEM、 IORESOURCE_REG、IORESOURCE_IRQ 等
  • num:这类资源中的哪一个。

对于设备树节点中的 reg 属性,它对应 IORESOURCE_MEM 类型的资源;对于设备树节点中的 interrupts 属性,它对应IORESOURCE_IRQ 类型的资源。

二、of函数使用实例

1. 设备树节点准备

这里可以在设备树中添加这样一个节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
alpha {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";

sdev_led {
#address-cells = <1>;
#size-cells = <1>;
compatible = "led";
status = "okay";
reg = < 0X020C406C 0X04 /* CCM_CCGR1_BASE */
0X020E0068 0X04 /* SW_MUX_GPIO1_IO03_BASE */
0X020E02F4 0X04 /* SW_PAD_GPIO1_IO03_BASE */
0X0209C000 0X04 /* GPIO1_DR_BASE */
0X0209C004 0X04 >; /* GPIO1_GDIR_BASE */
};
};

为了避免#address-cells = <1>; 和 #size-cells = <1>;这两个属性改变根节点其他的节点的属性, 所以在这里创建了一个 alpha 节点。 在这个示例中, #address-cells 设置为 1 表示地址使用一个 32 位的单元, #size-cells 也设置为 1 表示大小使用一个 32 位的单元。

第 4 行: 将 compatible 属性设置为”simple-bus”用于表示 alpha 节点的兼容性, 指明它是一个简单总线设备, 在转换 platform_device 的过程中, 会继续查找该节点的子节点。

第 9 行: sdev_led 节点下的 compatible 属性为”led”, 表明该节点将会被转换为 platform_device。

第 11 行: 这个属性用于描述 sdev_led 节点的寄存器信息。可以看到有5个寄存器 。

这个节点在编译的时候报警告,但是并没有什么太大影响,这里可以先不管:

image-20250309161117557

我们之前用的是从tftp下载设备树,所以我们直接替换服务器中的设备树文件即可,然后重启设备,看一下节点情况:

image-20250309161625812

2. of获取设备节点实例

2.1 demo源码

源码可以看这里:11_device_tree/02_of_get_node/drivers_demo/sdriver_demo.c · 苏木/imx6ull-driver-demo - 码云 - 开源中国中的函数of_get_node_test(void)

2.2 开发板测试

我们加载驱动,会看到如下打印信息:

image-20250309162949432

3. of获取设备节点属性实例

3.1 demo源码

源码可以看这里:11_device_tree/02_of_get_node/drivers_demo/sdriver_demo.c · 苏木/imx6ull-driver-demo - 码云 - 开源中国中的函数of_get_node_property_test(void)

3.2 开发板测试

我们加载驱动,会看到如下打印信息:

image-20250309164449464

4. led实例

上面已经可以从设备树获取对应的节点和属性了,那么我们来写一个led灯的demo,可以看这里:11_device_tree/03_dts_led · 苏木/imx6ull-driver-demo - 码云 - 开源中国

三、设备树下的平台设备驱动

前面我们学习了从 device_node 到 platform_device 的转换流程, 转换完成之后操作系统才能够识别和管理设备, 从而与 platform_driver 进行匹配 。这里我们就来学习一下吧。

1. platform_device 如何与 platform_driver 配对

从设备树转换得来的 platform_device 会被注册进内核里,以后当我们每注册一个 platform_driver 时,它们就会两两确定能否配对,如果能配对成功就调用 platform_driver 的 probe 函数。 (这里具体可以看《LV06-05-linux平台总线模型-03-平台总线的匹配.md》)

这里我们简单回顾一下,我们看一下platform_match()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static int platform_match(struct device *dev, struct device_driver *drv)
{
struct platform_device *pdev = to_platform_device(dev);
struct platform_driver *pdrv = to_platform_driver(drv);

/* (1)When driver_override is set, only bind to the matching driver */
if (pdev->driver_override)
return !strcmp(pdev->driver_override, drv->name);

/* (2)Attempt an OF style match first */
if (of_driver_match_device(dev, drv))
return 1;

/* Then try ACPI style match */
if (acpi_driver_match_device(dev, drv))
return 1;

/* (3)Then try to match against the id table */
if (pdrv->id_table)
return platform_match_id(pdrv->id_table, pdev) != NULL;

/* (4)fall-back to driver name match */
return (strcmp(pdev->name, drv->name) == 0);
}

1.1 强制选择驱动

  • (1)最先比较:是否强制选择某个 driver 。(比较 platform_device.driver_override 和 platform_driver.driver.name)

可以设置 platform_device 的 driver_override,强制选择某个 platform_driver。

1.2 设备树匹配

  • (2)设备树信息。(比较platform_device.dev.of_node 和 platform_driver.driver.of_match_table)

由设备树节点转换得来的 platform_device 中,含有一个结构体: of_node。它的类型为struct device_node

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct device_node {
const char *name; // 节点的name属性
const char *type; // 节点的device_type属性
phandle phandle;
const char *full_name;
struct fwnode_handle fwnode;

struct property *properties;//含有compatible属性
struct property *deadprops; /* removed properties */
struct device_node *parent;
struct device_node *child;
struct device_node *sibling;
// ......
};

如果一个 platform_driver 支持设备树,它的platform_driver.driver.of_match_table 是一个数组,类型为struct of_device_id

1
2
3
4
5
6
struct of_device_id {
char name[32];
char type[32];
char compatible[128];
const void *data;
};

使用设备树信息来判断 dev 和 drv 是否配对时:

①首先,如果 of_match_table 中含有 compatible 值,就跟 dev 的 compatile 属性比较,若一致则成功,否则返回失败;

②其次,如果 of_match_table 中含有 type 值,就跟 dev 的 device_type 属性比较,若一致则成功,否则返回失败;

③最后,如果 of_match_table 中含有 name 值,就跟 dev 的 name 属性比较,若一致则成功,否则返回失败。

而设备树中建议不再使用 devcie_type 和 name 属性,所以基本上只使用设备节点的 compatible 属性来寻找匹配的 platform_driver。

1.3 id匹配

  • (3)比较 platform_device_id ( 比较 platform_device. name 和 platform_driver.id_table[i].name)

id_table 中可能有多项。 platform_driver.id_table 是“ platform_device_id”指针,表示该 drv支持若干个 device,它里面列出了各个 device 的{.name, .driver_data},其中的“ name”表示该 drv 支持的设备的名字, driver_data 是些提供给该device 的私有数据。

1.4 name匹配

  • (4)名称匹配,比较platform_device.name 和 platform_driver.driver.name

platform_driver.id_table 可能为空,这时可以根据 platform_driver.driver.name 来寻找同名的platform_device。

1.5 总结

image-20250119115206386

2. 设备树匹配实例

2.1 需要哪些条件?

在我们的工作中,驱动要求设备树节点提供什么,我们就得按这要求去编写设备树。匹配过程所要求的东西是固定的:

  • (1)设备树要有 compatible 属性,它的值是一个字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/{
alpha {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";

sdev_led {
#address-cells = <1>;
#size-cells = <1>;
compatible = "led";
status = "okay";
reg = < 0X020C406C 0X04 /* CCM_CCGR1_BASE */
0X020E0068 0X04 /* SW_MUX_GPIO1_IO03_BASE */
0X020E02F4 0X04 /* SW_PAD_GPIO1_IO03_BASE */
0X0209C000 0X04 /* GPIO1_DR_BASE */
0X0209C004 0X04 >; /* GPIO1_GDIR_BASE */
};
};
};
  • (2)platform_driver 中要有 of_match_table,其中一项的.compatible 成员设置为一个字符串

我们需要定义一个设备树驱动匹配表:

1
2
3
4
5
/* 设备树匹配列表 */
static const struct of_device_id led_of_match[] = {
{ .compatible = "led" }, // 这里用于匹配设备树中的 compatible 属性节点
{ /* Sentinel */ }
};
  • (3)上述 2 个字符串要一致。

2.2 基本框架

定义platform_driver的时候要用上面的这个表:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 定义平台驱动结构体
*/
static struct platform_driver g_sdrv_platform = {
.probe = sdrv_probe, // 平台设备的探测函数指针
.remove = sdrv_remove, // 平台设备的移除函数指针
.driver = {
.name = "imx6ul-led-driver", // 和设备名称相同时,可以匹配成功
// 会在 /sys/bus/platform/drivers中创建对应目录,即/sys/bus/platform/drivers/driver-name
.owner = THIS_MODULE,
.of_match_table = led_of_match, /* 设备树匹配表 */
},
};

基本框架如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
static int sdrv_probe(struct platform_device *pdev)
{
PRT("probing platform device & driver!pdev->name=%s\n", pdev->name);
return 0;
}

static int sdrv_remove(struct platform_device *pdev)
{
PRT("removing platform driver!pdev->name=%s\n", pdev->name);
return 0;
}

/* 设备树匹配列表 */
static const struct of_device_id led_of_match[] = {
{ .compatible = "led" }, // 这里用于匹配设备树中的 compatible 属性节点
{ /* Sentinel */ }
};

/**
* 定义平台驱动结构体
*/
static struct platform_driver g_sdrv_platform = {
.probe = sdrv_probe, // 平台设备的探测函数指针
.remove = sdrv_remove, // 平台设备的移除函数指针
.driver = {
.name = "imx6ul-led-driver", // 和设备名称相同时,可以匹配成功
// 会在 /sys/bus/platform/drivers中创建对应目录,即/sys/bus/platform/drivers/driver-name
.owner = THIS_MODULE,
.of_match_table = led_of_match, /* 设备树匹配表 */
},
};

static __init int sdrv_demo_init(void)
{
int ret = 0;
printk("*** [%s:%d]Build Time: %s %s, git version:%s LINUX_VERSION=%d.%d.%d ***\n", __FUNCTION__,
__LINE__, KERNEL_KO_DATE, KERNEL_KO_TIME, KERNEL_KO_VERSION,
(LINUX_VERSION_CODE >> 16) & 0xff, (LINUX_VERSION_CODE >> 8) & 0xff, LINUX_VERSION_CODE & 0xff);

// 注册平台驱动
ret = platform_driver_register(&g_sdrv_platform);
if (ret)
{
PRT("Failed to register platform driver!ret=%d\n", ret);
goto err_platform_driver_register;
}

//PRT("sdrv_demo module init success!\n");
return 0;

err_platform_driver_register:
return ret;
}

static __exit void sdrv_demo_exit(void)
{
// 注销平台驱动
platform_driver_unregister(&g_sdrv_platform);
//PRT("sdrv_demo module exit!\n");
}

module_init(sdrv_demo_init); // 将__init定义的函数指定为驱动的入口函数
module_exit(sdrv_demo_exit); // 将__exit定义的函数指定为驱动的出口函数

/* 模块信息(通过 modinfo xxx.ko 查看) */
MODULE_LICENSE("GPL v2"); /* 源码的许可证协议 */
MODULE_AUTHOR("sumu"); /* 字符串常量内容为模块作者说明 */
MODULE_DESCRIPTION("Description"); /* 字符串常量内容为模块功能说明 */
MODULE_ALIAS("module's other name"); /* 字符串常量内容为模块别名 */

2.3 demo源码

demo源码可以看这里:11_device_tree/04_platform_dtb_match · 苏木/imx6ull-driver-demo - 码云 - 开源中国

需要注意,这个demo中完成了平台设备驱动的设备树匹配过程,但是获取节点的信息的时候还是通过of相关函数获取,下一个demo我们再研究通过platform_device来获取节点信息。

2.4 调试技巧

  • 设备树节点

我们换了设备树后就需要看一下是不是生成了正确的节点,可以到以下目录去看

1
2
3
ls /sys/firmware/devicetree/base
# 或者
ls /proc/device-tree/
  • platform_device查看

我们创建的节点是会被转化为platform_device的,可以看这个目录:

1
ls /sys/bus/platform/devices
image-20250309193144637
  • platform_driver 的信息

以下目录含有注册进内核的所有 platform_driver:

1
ls /sys/bus/platform/drivers
image-20250309193337157

一个 driver 对应一个目录,进入某个目录后,如果它有配对的设备,可以直接看到。例如这里我们的imx6ul-led-driver就匹配了alpha:sdev_led设备。

注意: 一个平台设备只能配对一个平台驱动,一个平台驱动可以配对多个平台设备。

3. 从platform_device获取节点信息

3.1 怎么拿到节点?

可以通过设备树节点指定资源, platform_driver 获得资源,那么 platform_drive怎么获得资源? 我们看一下probe函数:

1
2
3
4
static int sdrv_probe(struct platform_device *pdev)
{

}

在匹配成功后,会传入platform_device的指针,我们前面知道,platform_device.dev.of_node中存储了节点的相关信息,我们完全可以根据这个来找到对应的节点:

1
struct device_node* np = pdev->dev.of_node;

3.2 demo源码

源码可以看这里:11_device_tree/05_platform_get_node · 苏木/imx6ull-driver-demo - 码云 - 开源中国,源码中是通过这行获取节点:

image-20250309195107101

3.3 开发板测试

我们加载驱动可以看到:

image-20250309195026404