一、驱动如何访问设备树?
Linux 内核提供了一系列OpenFirmware (OF) API函数,让驱动可以访问并解析设备树中的内容。
只要知道设备树节点的 compatible、节点名等特征,就可以手动查找节点,然后提取它的属性(如 u32、string、数组等)。
二、常用 OF API 介绍
of_find_node_by_name(NULL, "节点名")
- 通过名字查找设备树节点 (全系统遍历,可能重复)
of_find_node_by_path("/mychardev@0")
- 通过完整路径查找唯一节点
of_find_compatible_node(NULL, NULL, "compatible-name")
- 通过 compatible 字符串查找节点(最推荐)
of_property_read_u32(np, "属性名", &val)
- 读取 u32 属性
of_property_read_string(np, "属性名", &ptr)
- 读取字符串属性
三、字符设备驱动解析设备树
创建一个 11_driver_devicetree_get/ 目录,进入目录创建一个 driver_devicetree_get.c 驱动文件,我们直接复用 《实现一个字符设备》 章节的代码,将 05_char_device/mychardev.c 中的内容拷贝进来,进行修改。
1、头文件准备
在驱动代码开头添加:
c
#include <linux/of.h> // 设备树接口核心头文件
#include <linux/of_address.h> // 读内存映射资源时用到的头文件1
2
2
2、查找节点并读取属性
在 mychardev_init 函数中,编写代码:
c
static int __init mychardev_init(void)
{
// ...原有代码...
struct device_node *np; // 设备树节点指针
u32 buffer_len = 0, dev_id = 0; // 用于存储设备树属性值
/* 从设备树中获取设备节点(用 compatible 查找节点) */
// 一定要和设备树中设定的compatible名字一致!
np = of_find_compatible_node(NULL, NULL, "lckfb,mychardev");
if (!np) {
printk(KERN_ERR "mychardev: device tree node not found!\n");
return -ENODEV;
}
// 读 buffer-len
if (of_property_read_u32(np, "buffer-len", &buffer_len)) {
// 打印错误信息
printk(KERN_ERR "mychardev: failed to get buffer-len!\n");
}
// 读 dev-id
if (of_property_read_u32(np, "dev-id", &dev_id)) {
// 打印错误信息
printk(KERN_ERR "mychardev: failed to get dev-id!\n");
}
// 将读出的信息打印
printk(KERN_INFO "mychardev: from dtb buffer-len=%u, dev-id=0x%X\n", buffer_len, dev_id);
// ...原有代码...
return 0;
}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
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
3、代码解释
- 获取设备树节点
c
np = of_find_compatible_node(NULL, NULL, "lckfb,mychardev");
if (!np) {
printk(KERN_ERR "mychardev: device tree node not found!\n");
return -ENODEV;
}1
2
3
4
5
2
3
4
5
- 用
of_find_compatible_node函数从设备树中按照"lckfb,mychardev"这个compatible属性查找并获取设备节点指针(struct device_node *np)。 - 如果找不到节点,内核日志报错并返回
-ENODEV,初始化失败。
- 读取设备树属性
c
if (of_property_read_u32(np, "buffer-len", &buffer_len)) {
printk(KERN_ERR "mychardev: failed to get buffer-len!\n");
}
if (of_property_read_u32(np, "dev-id", &dev_id)) {
printk(KERN_ERR "mychardev: failed to get dev-id!\n");
}1
2
3
4
5
6
2
3
4
5
6
- 用
of_property_read_u32分别读取设备树节点下"buffer-len"和"dev-id"这两个属性(32 位无符号整型)。 - 如果读失败,分别输出错误日志。
- 打印读取结果
c
printk(KERN_INFO "mychardev: from dtb buffer-len=%u, dev-id=0x%X\n", buffer_len, dev_id);1
无论是否读取成功,都会在内核日志打印获取到的(或默认初值为 0 的)属性值。
4、完整代码
c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h> // 设备树接口核心头文件
#include <linux/of_address.h> // 读内存映射资源时用到的头文件
// 设备名和类名的宏定义
#define DEV_NAME "mychardev" // 设备节点名称
#define CLASS_NAME "class_mychardev" // 设备类名称
static dev_t dev_num; // 保存设备号
static struct cdev cdev_test; // 字符设备结构体
static struct class *class_test; // 设备类指针
// 打开设备时调用的函数
// inode: 指向文件的 inode 结构体指针
// file: 文件结构体指针
static int chrdev_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "mychardev: chrdev_open called\n"); // 打印打开信息到内核日志
return 0; // 返回0表示成功
}
// 读设备时调用的函数
// file: 文件结构体指针
// buf: 用户空间的缓冲区指针
// size: 期望读取的字节数
// off: 偏移量指针
static ssize_t chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
printk(KERN_INFO "mychardev: chrdev_read called\n"); // 打印读操作信息
return 0; // 返回0表示没有数据可读
}
// 写设备时调用的函数
// file: 文件结构体指针
// buf: 用户空间的缓冲区指针
// size: 要写入的字节数
// off: 偏移量指针
static ssize_t chrdev_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
printk(KERN_INFO "mychardev: chrdev_write called\n"); // 打印写操作信息
return size; // 返回写入的字节数,表示写入成功
}
// 关闭设备时调用的函数
// inode: 指向文件的 inode 结构体指针
// file: 文件结构体指针
static int chrdev_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "mychardev: chrdev_release called\n"); // 打印关闭信息
return 0; // 返回0表示成功
}
// file_operations 结构体,指明本设备支持的操作
static struct file_operations cdev_fops_test = {
.owner = THIS_MODULE, // 拥有者,一般为 THIS_MODULE
.open = chrdev_open, // open 操作
.read = chrdev_read, // read 操作
.write = chrdev_write, // write 操作
.release = chrdev_release, // release 操作
};
// 模块加载时自动调用的初始化函数
static int __init mychardev_init(void)
{
int ret;
int major, minor;
struct device_node *np; // 设备树节点指针
u32 buffer_len = 0, dev_id = 0; // 用于存储设备树属性值
/* 从设备树中获取设备节点(用 compatible 查找节点) */
np = of_find_compatible_node(NULL, NULL, "lckfb,mychardev");
if (!np) {
printk(KERN_ERR "mychardev: device tree node not found!\n");
return -ENODEV;
}
// 读 buffer-len
if (of_property_read_u32(np, "buffer-len", &buffer_len)) {
// 打印错误信息
printk(KERN_ERR "mychardev: failed to get buffer-len!\n");
}
// 读 dev-id
if (of_property_read_u32(np, "dev-id", &dev_id)) {
// 打印错误信息
printk(KERN_ERR "mychardev: failed to get dev-id!\n");
}
// 将读出的信息打印
printk(KERN_INFO "mychardev: from dtb buffer-len=%u, dev-id=0x%X\n", buffer_len, dev_id);
// 1. 自动申请设备号,主设备号和次设备号由内核分配
ret = alloc_chrdev_region(&dev_num, 0, 1, DEV_NAME);
if (ret < 0) {
printk(KERN_ERR "mychardev: alloc_chrdev_region failed\n"); // 申请失败
return ret;
}
major = MAJOR(dev_num); // 获取主设备号
minor = MINOR(dev_num); // 获取次设备号
printk(KERN_INFO "mychardev: alloc_chrdev_region ok: major=%d, minor=%d\n", major, minor);
// 2. 初始化 cdev 结构体,并添加到内核
cdev_init(&cdev_test, &cdev_fops_test); // 初始化 cdev
ret = cdev_add(&cdev_test, dev_num, 1); // 注册 cdev 到内核
if (ret < 0) {
printk(KERN_ERR "mychardev: cdev_add failed\n");
unregister_chrdev_region(dev_num,1); // 失败时释放设备号
return ret;
}
// 3. 创建设备类,便于自动创建设备节点
class_test = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(class_test)) {
printk(KERN_ERR "mychardev: class_create failed\n");
cdev_del(&cdev_test);
unregister_chrdev_region(dev_num,1);
return PTR_ERR(class_test);
}
// 4. 创建设备节点 /dev/device_test
if (device_create(class_test, NULL, dev_num, NULL, DEV_NAME) == NULL) {
printk(KERN_ERR "mychardev: device_create failed\n");
class_destroy(class_test);
cdev_del(&cdev_test);
unregister_chrdev_region(dev_num,1);
return -ENOMEM;
}
printk(KERN_INFO "mychardev: chrdev driver loaded successfully\n"); // 驱动加载成功
return 0;
}
// 模块卸载时自动调用的清理函数
static void __exit mychardev_exit(void)
{
device_destroy(class_test, dev_num); // 删除设备节点
class_destroy(class_test); // 删除设备类
cdev_del(&cdev_test); // 注销 cdev
unregister_chrdev_region(dev_num, 1); // 释放设备号
printk(KERN_INFO "mychardev: chrdev driver unloaded\n"); // 卸载信息
}
// 指定模块的初始化和退出函数
module_init(mychardev_init); // 加载模块时调用
module_exit(mychardev_exit); // 卸载模块时调用
MODULE_LICENSE("GPL"); // 模块许可证声明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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
5、Makefile与编译
Makefile几乎就和 05_char_device/Makefile 中的一摸一样了,内容如下:
makefile
export ARCH=arm64
# 交叉编译器绝对路径前缀
export CROSS_COMPILE=/home/lckfb/TaishanPi-3-Linux/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-
# 和源文件名一致
obj-m += driver_devicetree_get.o
# 内核源码目录
KDIR := /home/lckfb/TaishanPi-3-Linux/kernel-6.1
PWD ?= $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CROSS_COMPILE:依旧是SDK中的编译器路径前缀。KDIR:依旧是内核源码目录。- 需要将
obj-m +=后面的改为driver_devicetree_get.o。
然后执行 make 编译生成 .ko 文件:
6、运行测试
我们必须要使用之前修改过设备树的内核烧录到开发板中,然后将刚刚编译出来的 driver_devicetree_get.ko 文件,复制到开发板中进行挂载:
bash
sudo insmod driver_devicetree_get.ko1
然后我们就能看到,设备树中我们设定的值被驱动读取出来并打印:
四、多设备支持
如果需要查找多个同类节点(比如有多个 mychardev),可用of_find_compatible_node配合循环/链表。
c
struct device_node *np = NULL;
while ((np = of_find_compatible_node(np, NULL, "lckfb,mychardev"))) {
// 每次np指向下一匹配的节点
// 可继续读取每个节点的属性
}1
2
3
4
5
2
3
4
5
但如果只加了一个节点,这种用法足够。
五、说明
有了设备树解析之后,我们就可以把很多原本应该写死在驱动里面的参数,抽象到设备树中,修改就会无比方便。
配合动态设备树,效果更是爆炸。