一、为什么需要管理多个设备?
在实际项目中,我们经常会遇到这样的场景:
- 一个芯片上有多个相同的外设(比如 3 个 UART、4 个 SPI)
- 同一个驱动需要支持不同型号的设备(比如 LED 驱动支持红、绿、蓝三个 LED)
- 多个设备共享相同的操作逻辑,只是参数不同
Platform 驱动支持多个设备,每个设备有独立的设备节点,参数也可以在设备树进行单独修改。
二、管理三个设备
- 设备树中定义 3 个设备节点
- 一个驱动自动识别并管理这 3 个设备
- 创建 3 个独立的设备节点:
/dev/mychardev0、/dev/mychardev1、/dev/mychardev2 - 每个设备有自己的
dev-id,驱动可以区分
三、设备树修改
1、编写设备树节点
以上个章节《设备树+platform驱动》为源文件,进行修改
/ {
mychardev0: mychardev@0 {
compatible = "lckfb,mychardev";
reg = <0x0 0xfec00000 0x0 0x1000>;
buffer-len = <64>;
dev-id = <0x01>; // 设备1的ID
status = "okay";
};
mychardev1: mychardev@1 {
compatible = "lckfb,mychardev";
reg = <0x0 0xfec01000 0x0 0x1000>;
buffer-len = <128>;
dev-id = <0x02>; // 设备2的ID
status = "okay";
};
mychardev2: mychardev@2 {
compatible = "lckfb,mychardev";
reg = <0x0 0xfec02000 0x0 0x1000>;
buffer-len = <256>;
dev-id = <0x03>; // 设备3的ID
status = "okay";
};
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
重点说明:
- 3 个节点的
compatible完全相同,都是"lckfb,mychardev" - 每个设备有不同的
dev-id(用于在驱动中区分) - 每个设备有不同的
buffer-len(模拟不同的硬件参数) - 每个设备有不同的
reg地址(避免冲突)
2、编译烧录
我们根据 Debian12内核编译 的教程,重新编译内核生成 boot.img,并单独烧录内核镜像的方式实现替换设备树。
烧录完成之后,板子开机,进行校验设备树节点:
查看解包后的设备树节点
ls /sys/firmware/devicetree/base/mychardev@*看到 0~2 三个设备,并有相关的字段说明生效!
四、编写驱动
1、创建项目目录
mkdir 13_platform_multi_device并且进入目录:
cd 13_platform_multi_device2、驱动文件
在 13_platform_multi_device 目录下创建 platform_multi_chardev.c 驱动文件,并编写如下代码:
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/slab.h>
#define DEV_NAME_PREFIX "mychardev"
#define CLASS_NAME "class_mychardev"
#define MAX_DEVICES 10
// 全局变量
static struct class *class_test;
static int device_count = 0; // 当前设备数量
// 每个设备的私有数据
struct mychardev_private {
struct cdev cdev; // 字符设备结构体
dev_t dev_num; // 设备号
u32 dev_id; // 设备ID
u32 buffer_len; // 缓冲区大小
resource_size_t reg_base; // 寄存器基地址
int minor; // 次设备号
struct device *device; // 设备指针
};
// ==================== 字符设备操作函数 ====================
static int chrdev_open(struct inode *inode, struct file *file)
{
struct mychardev_private *priv;
// 通过 inode 获取 cdev,再通过 container_of 获取私有数据
priv = container_of(inode->i_cdev, struct mychardev_private, cdev);
file->private_data = priv; // 保存到文件私有数据
printk(KERN_INFO "mychardev%d: open (dev-id=0x%02X)\n",
priv->minor, priv->dev_id);
return 0;
}
static ssize_t chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
struct mychardev_private *priv = file->private_data;
printk(KERN_INFO "mychardev%d: read (dev-id=0x%02X, buffer-len=%u)\n",
priv->minor, priv->dev_id, priv->buffer_len);
return 0;
}
static ssize_t chrdev_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
struct mychardev_private *priv = file->private_data;
printk(KERN_INFO "mychardev%d: write (dev-id=0x%02X, size=%zu)\n",
priv->minor, priv->dev_id, size);
return size;
}
static int chrdev_release(struct inode *inode, struct file *file)
{
struct mychardev_private *priv = file->private_data;
printk(KERN_INFO "mychardev%d: release\n", priv->minor);
return 0;
}
static struct file_operations cdev_fops = {
.owner = THIS_MODULE,
. open = chrdev_open,
. read = chrdev_read,
.write = chrdev_write,
.release = chrdev_release,
};
// ==================== Platform 驱动函数 ====================
static const struct of_device_id mychardev_of_match[] = {
{ .compatible = "lckfb,mychardev" },
{ }
};
MODULE_DEVICE_TABLE(of, mychardev_of_match);
static int mychardev_probe(struct platform_device *pdev)
{
int ret;
struct mychardev_private *priv;
struct resource *res;
char dev_name[32];
printk(KERN_INFO "========================================\n");
printk(KERN_INFO "mychardev: probe 被调用(设备 %d)\n", device_count);
// 分配私有数据
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->minor = device_count++; // 分配次设备号
// ========== 获取设备树资源 ==========
// 获取寄存器资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (res) {
priv->reg_base = res->start;
printk(KERN_INFO "mychardev%d: 寄存器基地址=0x%llx\n",
priv->minor, (unsigned long long)priv->reg_base);
}
// 读取 buffer-len 属性
if (of_property_read_u32(pdev->dev.of_node, "buffer-len", &priv->buffer_len)) {
priv->buffer_len = 64; // 默认值
}
printk(KERN_INFO "mychardev%d: buffer-len=%u\n", priv->minor, priv->buffer_len);
// 读取 dev-id 属性
if (of_property_read_u32(pdev->dev.of_node, "dev-id", &priv->dev_id)) {
priv->dev_id = 0;
}
printk(KERN_INFO "mychardev%d: dev-id=0x%02X\n", priv->minor, priv->dev_id);
// ========== 注册字符设备 ==========
// 申请设备号(次设备号从 minor 开始)
ret = alloc_chrdev_region(&priv->dev_num, priv->minor, 1, DEV_NAME_PREFIX);
if (ret < 0) {
printk(KERN_ERR "mychardev%d: alloc_chrdev_region failed\n", priv->minor);
return ret;
}
printk(KERN_INFO "mychardev%d: 设备号 major=%d, minor=%d\n",
priv->minor, MAJOR(priv->dev_num), MINOR(priv->dev_num));
// 初始化并注册 cdev
cdev_init(&priv->cdev, &cdev_fops);
priv->cdev.owner = THIS_MODULE;
ret = cdev_add(&priv->cdev, priv->dev_num, 1);
if (ret < 0) {
unregister_chrdev_region(priv->dev_num, 1);
return ret;
}
// 创建设备类(只在第一个设备时创建)
if (! class_test) {
class_test = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(class_test)) {
cdev_del(&priv->cdev);
unregister_chrdev_region(priv->dev_num, 1);
return PTR_ERR(class_test);
}
}
// 创建设备节点(名称:mychardev0, mychardev1, mychardev2)
snprintf(dev_name, sizeof(dev_name), "%s%d", DEV_NAME_PREFIX, priv->minor);
priv->device = device_create(class_test, &pdev->dev, priv->dev_num, NULL, dev_name);
if (IS_ERR(priv->device)) {
cdev_del(&priv->cdev);
unregister_chrdev_region(priv->dev_num, 1);
return PTR_ERR(priv->device);
}
platform_set_drvdata(pdev, priv);
printk(KERN_INFO "mychardev%d: 驱动加载成功!/dev/%s 已创建\n",
priv->minor, dev_name);
printk(KERN_INFO "========================================\n");
return 0;
}
static void mychardev_remove(struct platform_device *pdev)
{
struct mychardev_private *priv = platform_get_drvdata(pdev);
printk(KERN_INFO "mychardev%d: remove (dev-id=0x%02X)\n",
priv->minor, priv->dev_id);
device_destroy(class_test, priv->dev_num);
cdev_del(&priv->cdev);
unregister_chrdev_region(priv->dev_num, 1);
device_count--;
// 如果所有设备都移除了,销毁设备类
if (device_count == 0) {
class_destroy(class_test);
class_test = NULL;
}
printk(KERN_INFO "mychardev%d: 驱动卸载成功!\n", priv->minor);
}
static struct platform_driver mychardev_driver = {
.probe = mychardev_probe,
.remove_new = mychardev_remove,
.driver = {
.name = "mychardev",
.of_match_table = mychardev_of_match,
},
};
module_platform_driver(mychardev_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("LCKFB");
MODULE_DESCRIPTION("Platform Driver for Multiple Devices");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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
3、驱动讲解
此驱动与单设备驱动的关键区别:
核心私有数据结构:
struct mychardev_private {
struct cdev cdev; // 字符设备结构体(每个设备独立)
dev_t dev_num; // 设备号(每个设备不同)
u32 dev_id; // 从设备树读取的设备ID
u32 buffer_len; // 从设备树读取的缓冲区大小
resource_size_t reg_base; // 寄存器基地址
int minor; // 次设备号(0, 1, 2, ... )
struct device *device; // 指向 device_create 创建的设备
};2
3
4
5
6
7
8
9
这个数据结构使用
devm_kzalloc函数申请一个空间,用来存储每一个设备的实例化mychardev_private数据,我们不需要在remove函数中手动销毁,在相关的设备移除时他是会自动释放的。
设备号:
static int device_count = 0; // 当前设备数量
priv->minor = device_count++; // 分配次设备号
// 申请设备号(次设备号从 minor 开始)
ret = alloc_chrdev_region(&priv->dev_num, priv->minor, 1, DEV_NAME_PREFIX);
if (ret < 0) {
printk(KERN_ERR "mychardev%d: alloc_chrdev_region failed\n", priv->minor);
return ret;
}2
3
4
5
6
7
8
9
10
申请设备号时,主设备号我们不用管,自动分配,而我们次设备号需要设定一个起始值,这个起始值会有一个全局变量,记录下一个该申请的次设备号起始值是多少,防止次设备号重复和混乱。
注册字符设备:
// 初始化并注册 cdev
cdev_init(&priv->cdev, &cdev_fops);
priv->cdev.owner = THIS_MODULE;
ret = cdev_add(&priv->cdev, priv->dev_num, 1);
if (ret < 0) {
unregister_chrdev_region(priv->dev_num, 1);
return ret;
}2
3
4
5
6
7
8
函数会根据我们设备号(主设备号+次设备号)的不同,而注册不同的设备。
创建设备类(只创建一次):
// 创建设备类(只在第一个设备时创建)
if (! class_test) {
class_test = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(class_test)) {
cdev_del(&priv->cdev);
unregister_chrdev_region(priv->dev_num, 1);
return PTR_ERR(class_test);
}
}2
3
4
5
6
7
8
9
所有的设备用的是同一个驱动,他们呢都属于同一个类,所以只需要创建一次即可。
创建设备节点(每个设备独立):
// 创建设备节点(名称:mychardev0, mychardev1, mychardev2)
snprintf(dev_name, sizeof(dev_name), "%s%d", DEV_NAME_PREFIX, priv->minor);
priv->device = device_create(class_test, &pdev->dev, priv->dev_num, NULL, dev_name);2
3
我们需要根据设备名称和设备号,进行拼接名字,创建
/dev目录下不同的设备节点,这样就能直接访问不同的设备:/dev/mychardev0/dev/mychardev1/dev/mychardev2
保存私有数据:
platform_set_drvdata(pdev, priv);可以在
remove的时候取出私有数据,并针对性的销毁指定的设备。
设备移除:
static void mychardev_remove(struct platform_device *pdev)
{
struct mychardev_private *priv = platform_get_drvdata(pdev);
printk(KERN_INFO "mychardev%d: remove (dev-id=0x%02X)\n",
priv->minor, priv->dev_id);
device_destroy(class_test, priv->dev_num);
cdev_del(&priv->cdev);
unregister_chrdev_region(priv->dev_num, 1);
device_count--;
// 如果所有设备都移除了,销毁设备类
if (device_count == 0) {
class_destroy(class_test);
class_test = NULL;
}
printk(KERN_INFO "mychardev%d: 驱动卸载成功!\n", priv->minor);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
remove会被调用多次(每个设备一次)- 只有最后一个设备移除时才销毁设备类
完成流程图:
内存布局:
总结:
| 作用 | 代码 | |
|---|---|---|
| 私有数据结构 | 每个设备独立存储数据 | mychardev_private |
| container_of | 通过成员地址获取结构体地址 | container_of(inode->i_cdev, ...) |
| platform_set_drvdata | 保存私有数据到 pdev | platform_set_drvdata(pdev, priv) |
| platform_get_drvdata | 从 pdev 获取私有数据 | platform_get_drvdata(pdev) |
| devm_kzalloc | 自动管理内存 | 设备移除时自动释放 |
| device_count | 分配次设备号 | 0, 1, 2, ... |
| file->private_data | 在 open/read/write 间传递数据 | 存储 priv |
4、Makefile文件
在 13_platform_multi_device 目录下创建 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 += platform_multi_chardev.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) clean2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CROSS_COMPILE:依旧是SDK中的编译器路径前缀。KDIR:依旧是内核源码目录。- 需要将
obj-m +=后面的改为platform_multi_chardev.o
5、编译
我们在 13_platform_multi_device 目录下运行下面的命令:
make会编译并且生成 .ko 文件。
五、测试
将 platform_multi_chardev.ko 复制到开发板中(U盘、TF卡或者SSH都行),并使用以下的命令进行加载:
sudo insmod platform_multi_chardev.ko三个设备都已经创建了。
查看 /dev 目录下的所有 mychardev 设备:
ls /dev/mychardev*测试每一个设备的写入:
# 给予所有的mychardev设备权限
sudo chmod 666 /dev/mychardev*
# 依次运行下面的命令,查看输出信息
echo "test0" > /dev/mychardev0
echo "test1" > /dev/mychardev1
echo "test2" > /dev/mychardev22
3
4
5
6
7
卸载驱动:
sudo rmmod platform_multi_chardev可以使用
sudo lsmod命令查看当前挂载的模块信息。