LV06-03-chrdev-08-1个驱动兼容多个设备

可不可以通过一个驱动控制多个设备?怎么实现?若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
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源码

一、概述

1. 背景

在Linux内核中,主设备号用于标识设备对应的驱动程序,告诉Linux内核使用哪一个驱动程序为该设备服务。但是, 次设备号表示了同类设备的各个设备。每个设备的功能都是不一样的。如何能够用一个驱动程序去控制各种设备呢?

很明显,首先,我们可以根据次设备号,来区分各种设备;其次,就是提到过的file结构体的私有数据成员private_data。 我们可以通过该成员来指向不同的设备,不难想到为什么只有open函数和close函数的形参才有file结构体, 因为驱动程序第一个执行的是操作就是open,通过open函数就可以控制我们想要驱动的底层硬件。

2. 可以有哪些方式

有两种方式:

  • (1)一个cdev对象管理多个设备;
  • (2)多个cdev对象,每个cdev对象管理一个设备。

3. inode结构体

前面学习这个inode结构体的时候有这么两个成员:fs.h - include/linux/fs.h - struct inode

1
2
3
4
5
6
7
8
9
10
11
12
13
struct inode {
// ......
dev_t i_rdev;
// ......
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev;
char *i_link;
unsigned i_dir_seq;
};
// ......
} __randomize_layout;
  • dev_t i_rdev: 表示设备文件的结点,这个域实际上包含了设备号。
  • struct cdev *i_cdev: struct cdev是内核的一个内部结构,它是用来表示字符设备的,当inode结点指向一个字符设备文件时,此域为一个指向inode结构的指针。

一个含有设备号,一个指向字符设备,我们可以利用这两个成员来区分不同设备的。后面会说明这两个成员怎么用。

二、1个cdev设备

1. 怎么实现?

1个cdev的话,我们只需要一个cdev对象,一个类,但是设备号需要创建多个,并且根据不同的次设备号创建多个设备节点,用来控制不同的缓冲区。

image-20241226105838093

不同的设备节点对应不同的次设备号,我们可以在open的时候根据设备节点,找到设备号,根据次设备号来对应不同的缓冲区。那另一个问题来了,怎么在open的时候获取次设备号?

inode结构体中,对于设备文件的设备号会被保存到其成员i_rdev中,所以:

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 sdev_open(struct inode *inode, struct file *file)
{

switch (MINOR(inode->i_rdev))
{
case 0:
{
file->private_data = &g_sdev_child[0]; //设置私有数据
break;
}
case 1:
{
file->private_data = &g_sdev_child[1]; //设置私有数据
break;
}
default:
{
file->private_data = &g_sdev_child[0]; //设置私有数据
break;
}
}
printk("This is sdev_open!inode->i_rdev=%d, minor=%d\n", inode->i_rdev, MINOR(inode->i_rdev));
return 0;
}

为什么?这个我没有去深究inode->i_rdev中怎么吧设备号保存下来的,我查到在使用mknod命令创建节点的时候会调用这个函数inode.c - fs/inode.c - init_special_inode

1
2
3
4
5
6
7
8
9
10
void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
{
inode->i_mode = mode;
if (S_ISCHR(mode)) {
inode->i_fop = &def_chr_fops;
inode->i_rdev = rdev;
}
//......
}
EXPORT_SYMBOL(init_special_inode);

会发现这里是有赋值的,大概应该是这里,不过自动创建节点的函数中怎么赋值的我追了一下代码,没找到,这里先不管了,知道它保存的是设备号就是了,后面有需要了再详细去了解。

2. demo实现

直接看我的gitee仓库吧:04_chrdev_basic/09_chrdev_more_dev_1c · 苏木/imx6ull-driver-demo - 码云 - 开源中国

image-20241226112813160

直接执行make即可进行编译,编译完成后会得到对应的驱动程序和应用测试程序。

点击查看详情
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
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
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
#include <linux/init.h> /* module_init module_exit */
#include <linux/kernel.h>
#include <linux/module.h> /* MODULE_LICENSE */

#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>

#include "./timestamp_autogenerated.h"
#include "./version_autogenerated.h"

#define CHRDEV_NAME "sdev" /* 设备名,cat /proc/devices 查看与设备号的对应关系 */
#define CLASS_NAME "sclass" /* 类名,在 /sys/class 中显示的名称 */
#define DEVICE_NAME "sdevice" /* 设备节点名,在 /sys/class/class_name/ 中显示的名称以及 /dev/ 下显示的节点名 */
#define BUFSIZE 32 /* 设置最大偏移量为32 */

#define CHRDEV_CNT (2) /* 设备数量 */

#define CMD_TEST0 _IO('S', 0)
#define CMD_TEST1 _IOW('S', 1, int)
#define CMD_TEST2 _IOR('S', 2, int)
#define CMD_TEST3 _IOW('S', 3, int)

struct __CMD_TEST{
int a;
int b;
int c;
};

typedef struct __CHAR_DEVICE
{
dev_t dev_num; // 定义dev_t类型(32位大小)的变量dev_num,用来存放设备号
struct cdev s_cdev; // 定义cdev结构体类型的变量scdev
struct class *class; // 定于struct class *类型结构体变量 class,表示要创建的类
} _CHAR_DEVICE;

typedef struct __CHAR_DEVICE_CHILD
{
struct device *device; // 设备
char buf[BUFSIZE]; // 设置数据存储数组mem
}_CHAR_DEVICE_CHILD;

_CHAR_DEVICE g_sdev; //定义一个 g_sdev 结构体变量,只包含一个cdev对象
_CHAR_DEVICE_CHILD g_sdev_child[CHRDEV_CNT] = {0}; // 一个cdev对象要管理的 CHRDEV_CNT 个设备


static int sdev_open(struct inode *inode, struct file *file)
{

switch (MINOR(inode->i_rdev))
{
case 0:
{
file->private_data = &g_sdev_child[0]; //设置私有数据
break;
}
case 1:
{
file->private_data = &g_sdev_child[1]; //设置私有数据
break;
}
default:
{
file->private_data = &g_sdev_child[0]; //设置私有数据
break;
}
}
printk("This is sdev_open!inode->i_rdev=%d, minor=%d\n", inode->i_rdev, MINOR(inode->i_rdev));
return 0;
}

static ssize_t sdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
int i = 0;
loff_t offset = *off; // 将读取数据的偏移量赋值给loff_t类型变量p
size_t count = size;
_CHAR_DEVICE_CHILD *pDev=(_CHAR_DEVICE_CHILD *)file->private_data; //在write函数中读取private_data

if (offset > BUFSIZE)
{
return 0;
}

if (count > BUFSIZE - offset)
{
count = BUFSIZE - offset;
}

if (copy_to_user(buf, pDev->buf + offset, count))
{
// 将mem中的值写入buf,并传递到用户空间
printk("copy_to_user error!\n");
return -1;
}

for (i = 0; i < BUFSIZE; i++)
{
printk("buf[%d] %c\n", i, pDev->buf[i]); // 将mem中的值打印出来
}
printk("read offset is %llu, count is %d\n", offset, count);
*off = *off + count; // 更新偏移值

return count;
}

static ssize_t sdev_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
int i = 0;
loff_t offset = *off; // 将读取数据的偏移量赋值给loff_t类型变量p
size_t count = size;
_CHAR_DEVICE_CHILD *pDev=(_CHAR_DEVICE_CHILD *)file->private_data; //在write函数中读取private_data

if (offset > BUFSIZE)
{
return 0;
}

if (count > BUFSIZE - offset)
{
count = BUFSIZE - offset;
}

if (copy_from_user(pDev->buf + offset, buf, count))
{
// 将buf中的值,从用户空间传递到内核空间
printk("copy_to_user error \n");
return -1;
}

for (i = 0; i < BUFSIZE; i++)
{
printk("buf[%d] %c\n", i, pDev->buf[i]); // 将mem中的值打印出来
}
printk("write offset is %llu, count is %d\n", offset, count); // 打印写入的值
*off = *off + count; // 更新偏移值

return count;
}

static int sdev_release(struct inode *inode, struct file *file)
{
printk("This is sdev_release!\n");
return 0;
}

static loff_t sdev_llseek(struct file *file, loff_t offset, int whence)
{
loff_t new_offset = 0; // 定义loff_t类型的新的偏移值
switch (whence) // 对lseek函数传递的whence参数进行判断
{
case SEEK_SET:
if (offset < 0 || offset > BUFSIZE)
{
return -EINVAL; // EINVAL=22 表示无效参数
}
new_offset = offset; // 如果whence参数为SEEK_SET,则新偏移值为offset
break;
case SEEK_CUR:
if ((file->f_pos + offset < 0) || (file->f_pos + offset > BUFSIZE))
{
return -EINVAL;
}
new_offset = file->f_pos + offset; // 如果whence参数为SEEK_CUR,则新偏移值为file->f_pos + offset,file->f_pos为当前的偏移值
break;
case SEEK_END:
if (file->f_pos + offset < 0)
{
return -EINVAL;
}
new_offset = BUFSIZE + offset; // 如果whence参数为SEEK_END,则新偏移值为BUFSIZE + offset,BUFSIZE为最大偏移量
break;
default:
break;
}
file->f_pos = new_offset; // 更新file->f_pos偏移值

return new_offset;
}

static long sdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
int val = 0;//定义 int 类型向应用空间传递的变量 val
switch(cmd)
{
case CMD_TEST0:
printk("this is CMD_TEST0\n");
break;
case CMD_TEST1:
printk("this is CMD_TEST1\n");
printk("arg is %ld\n",arg);//打印应用空间传递来的 arg 参数
break;
case CMD_TEST2:
val = 1;//将要传递的变量 val 赋值为 1
printk("this is CMD_TEST2\n");
if(copy_to_user((int *)arg, &val, sizeof(val)) != 0)
{
//通过 copy_to_user 向用户空间传递数据
printk("copy_to_user error \n");
}
break;
case CMD_TEST3:
{
struct __CMD_TEST cmd_test3 = {0};
if (copy_from_user(&cmd_test3, (int *)arg, sizeof(cmd_test3)) != 0)
{
printk("copy_from_user error\n");
}
printk("cmd_test3.a = %d\n", cmd_test3.a);
printk("cmd_test3.b = %d\n", cmd_test3.b);
printk("cmd_test3.c = %d\n", cmd_test3.c);
break;
}
default:
break;
}

return 0;
}

static struct file_operations cdev_ops = {
.owner = THIS_MODULE,//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
.open = sdev_open,
.read = sdev_read,
.write = sdev_write,
.release = sdev_release,
.llseek = sdev_llseek,
.unlocked_ioctl = sdev_ioctl,
}; // 定义file_operations结构体类型的变量g_cdev_dev_ops

// 模块入口函数
static int __init chrdev_more_dev_1c_demo_init(void)
{
int i = 0;
int ret = 0; // 定义int类型的变量ret,用来判断函数返回值
int major = 0, minor = 0; // 定义int类型的主设备号major和次设备号minor
printk("*** [%s:%d]Build Time: %s %s, git version:%s ***\n", __FUNCTION__,
__LINE__, KERNEL_KO_DATE, KERNEL_KO_TIME, KERNEL_KO_VERSION);
printk("chrdev_more_dev_1c_demo module init!\n");

ret = alloc_chrdev_region(&g_sdev.dev_num, 0, CHRDEV_CNT, CHRDEV_NAME); // 自动获取设备号,设备名为chrdev_name
if (ret < 0)
{
printk("alloc_chrdev_region is error!\n");
}
printk("alloc_register_region is ok!\n");
major = MAJOR(g_sdev.dev_num); // 使用MAJOR()函数获取主设备号
minor = MINOR(g_sdev.dev_num); // 使用MINOR()函数获取次设备号
printk("major is %d, minor is %d !\n", major, minor);

cdev_init(&g_sdev.s_cdev, &cdev_ops); // 使用cdev_init()函数初始化g_sdev.s_cdev结构体,并链接到 cdev_ops 结构体
g_sdev.s_cdev.owner = THIS_MODULE; // 将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
ret = cdev_add(&g_sdev.s_cdev, g_sdev.dev_num, CHRDEV_CNT); // 使用cdev_add()函数进行字符设备的添加
if (ret < 0)
{
printk("cdev_add is error !\n");
}
printk("cdev_add is ok!CHRDEV_CNT=%d\n", CHRDEV_CNT);

g_sdev.class = class_create(THIS_MODULE, CLASS_NAME); // 使用class_create进行类的创建,类名称为 class_dev

// 这里还是需要创建多个设备的
for(i = 0; i < CHRDEV_CNT; i++)
{
g_sdev_child[i].device = device_create(g_sdev.class, NULL, g_sdev.dev_num + i, NULL, "%s%d",DEVICE_NAME, i); // 使用device_create进行设备的创建,设备名称为 device_dev
}
return 0;
}

// 模块出口函数
static void __exit chrdev_more_dev_1c_demo_exit(void)
{
int i = 0;
// 需要注意的是, 字符设备的注册要放在申请字符设备号之后,
// 字符设备的删除要放在释放字符驱动设备号之前。
cdev_del(&g_sdev.s_cdev); // 使用cdev_del()函数进行字符设备的删除
unregister_chrdev_region(g_sdev.dev_num, CHRDEV_CNT); // 释放字符驱动设备号
for(i = 0; i < CHRDEV_CNT; i++)
{
device_destroy(g_sdev.class, g_sdev.dev_num + i); // 删除创建的设备
}
class_destroy(g_sdev.class); // 删除创建的类
printk("chrdev_more_dev_1c_demo exit!\n");
}

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

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

3. 开发板测试

  • (1)加载驱动
1
insmod chrdev_more_dev_1c_demo.ko
  • (2)查看设备节点
1
ls /dev/sdevice* -alh

会看到有如下打印信息:

1
2
crw-rw----    1 0        root      246,   0 Jan  1 00:00 /dev/sdevice0
crw-rw---- 1 0 root 246, 1 Jan 1 00:00 /dev/sdevice1

在创建设备节点的时候命名就是/dev/sdeviceN,所以这里是0和1两个设备节点,他们会对应不同的缓冲区。

  • (3)向设备节点写入数据
1
2
3
4
5
6
7
echo 123456789 > /dev/sdevice0
echo 987654321 > /dev/sdevice1

# 或者使用应用测试程序
./xxx_demo_app.out /dev/sdevice 1 x # 从x位置读取
./xxx_demo_app.out /dev/sdevice 2 x # 从x位置写入
./xxx_demo_app.out /dev/sdevice 3 x # x为任意值
  • (4)从设备节点读取数据
1
2
3
4
5
6
7
cat /dev/sdevice0
cat /dev/sdevice1

# 或者使用应用测试程序
./xxx_demo_app.out /dev/sdevice 1 x # 从x位置读取
./xxx_demo_app.out /dev/sdevice 2 x # 从x位置写入
./xxx_demo_app.out /dev/sdevice 3 x # x为任意值

三、多个cdev设备

1. 怎么实现?

我们现在有多个设备,他们的主设备号一样,次设备号不同,他们也可以使用不同的cdev设备对象,都需要进行注册。但是他们都属于同一个类,所以我们只需要创建一个类就可以了。

  • 1、设备号申请的时候要直接连续申请多个;
  • 2、向内核注册新字符设备的时候,每个设备都要注册;
  • 3、创建类的时候只需要创建一个;
  • 4、创建设备的时候需要创建多个设备;
  • 5、删除设备的时候需要删除多个;
  • 6、删除字符设备对象的时候需要删除多个。
image-20241226151337208

1.1 设备号申请与注销

设备号身申请的时候不需要申请多次,只需要申请一次,并且指定要申请的设备数量即可,以 alloc_chrdev_region 为例:char_dev.c - fs/char_dev.c - alloc_chrdev_region

1
alloc_chrdev_region(&parentDevno, 0, dev_cnt, NEWCHAR_DEV_NAME); /* 申请设备号 */

这样我们就可以得到一个“父”设备号devno,这个设备号包含主设备号和次设备号:

1
2
major = MAJOR(parentDevno); /* 获取分配号的主设备号 */
minor = MINOR(parentDevno); /* 获取分配号的次设备号 */

我们可以从这个设备号中获取到dev_cnt个“子”设备号,”子“设备号怎么获取呢?这样来获取:

1
2
3
4
5
6
7
8
9
10
11
for(int i = 0; i < dev_cnt; i++)
{
devno[i] = MKDEV(major, minor + i);
}
...
// 或者
for(int i = 0; i < dev_cnt; i++)
{
devno[i] = parentDevno + i;
}
...

注销设备号的时候,直接注销多个设备即可:char_dev.c - fs/char_dev.c - unregister_chrdev_region

1
2
/* 注销设备号(将会删除 /proc/devices 中的设备号和对应的名称记录) */
unregister_chrdev_region(devno, dev_cnt);

1.2 设备注册与注销

设备注册主要是cdev对象的初始化和注册,这个是每个”子”设备都需要进行的:

1
2
3
4
5
6
7
8
9
10
struct cdev dev[dev_cnt]; /* 定义一个字符设备对象设备 */
for(int i = 0; i < dev_cnt; i++)
{
dev[i].owner = THIS_MODULE;
/* 初始化 cdev 对象, 给该对象添加操作函数集 */
cdev_init(&dev[i], &func_ops);
/* 向 Linux 系统添加字符设备 (cdev结构体变量,将会被添加到一个hash链表中) */
/* 会在 /proc/devices 中创建设备号和对应的名称记录(最终只会有一条主设备号的记录,但还是要分多次添加) */
cdev_add(&dev2, devno + i, 1); /* 此处是固定的一个一个添加 */
}

注销时候就是一个一个删除就行了:

1
2
3
4
5
/* 3.删除字符设备对象 cdev */
for (i = 0; i < dev_cnt; i++)
{
cdev_del(&dev[i]);
}

1.3 类的创建与销毁

这些多个设备属于同一个类,我们只需要创建一个类就可以了:

1
2
/* 创建类(要注意无论是一个设备还是多个设备,类只有一个) */
p_class = class_create(THIS_MODULE, NEWCHAR_DEV_NAME);

删除时候也只需要删除一个类:

1
2
/* 删除类(只有一个类,删除一次即可) */
class_destroy(p_class);

1.4 设备创建与销毁

因为我们是多个设备,所以每个设备都需要进行创建

1
2
3
4
5
6
/* 创建设备(有几个设备,就创建几个) */
for (i = 0; i < dev_cnt; i++)
{
/* 创建的设备为 /dev/dev_name */
device[i] = device_create(p_class, NULL, devno[i], NULL, "%s%d", NEWCHAR_DEV_NAME, i);}
}

销毁的时候也要逐个销毁:

1
2
3
4
5
6
 /* 删除设备(多个设备,逐个进行删除) */
for (i = 0; i < dev_cnt; i++)
{
/* 删除的是 /dev/dev_name */
device_destroy(p_class, devno[i]);
}

1.5 怎么区分不同的设备?

还是一样的问题,之前我们是通过inode→i_rdev,也就是设备号来获取子设备号,然后找到对应的设备的缓冲区,理论上来说,多个cdev设备的时候,也可以通过这种方式,但是我没试。这次我们来使用inode的i_cdev成员。

inode中的i_cdev成员保存了对应字符设备结构体的地址,但是我们的虚拟设备是把cdev封装起来的一个结构体,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 新字符设备的属性 */
struct _my_dev
{
/* 1.设备号与设备数量 */
int major; /* 主设备号:占高12位,用来表示驱动程序相同的一类设备,Linux系统中主设备号范围为 0~4095 */
int minor; /* 次设备号:占低20位,用来表示被操作的哪个具体设备 */
dev_t devno; /* 新字符设备的设备号:由主设备号和次设备号组成 */
/* 2.字符设备对象 */
struct cdev mydev; /* 定义一个字符设备对象设备 */
/* 3.设备读写相关成员变量 */
char mydev_buf[BUF_LEN]; /* 读写缓冲区 */
int curlen; /* 读写缓冲区中当前数据字节数 */

/* 自动创建设备节点相关成员变量 */
struct device *device; /* 设备 */
};
struct _my_dev g_my_dev[NEWCHAR_DEV_CNT]; /* 定义新字符设备数组 */

那我们要如何能够得到虚拟设备的数据缓冲区呢?为此,Linux提供了一个宏定义 container_of(),该宏可以根据结构体的某个成员的地址, 来得到该结构体的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* container_of - cast a member of a structure out to the containing structure
* @ptr: the pointer to the member.
* @type: the type of the container struct this is embedded in.
* @member: the name of the member within the struct.
*
*/
#define container_of(ptr, type, member) ({ \
void *__mptr = (void *)(ptr); \
BUILD_BUG_ON_MSG(!__same_type(*(ptr), ((type *)0)->member) && \
!__same_type(*(ptr), void), \
"pointer type mismatch in container_of()"); \
((type *)(__mptr - offsetof(type, member))); })

该宏需要三个参数,分别是代表结构体成员的真实地址,结构体的类型以及结构体成员的名字。这个在这里就详细再去分析原理了,可以看后面参考资料的【1】和【2】。在sdev_open()函数中,我们需要通过inode的i_cdev成员,来得到对应的虚拟设备结构体,并保存到文件指针file的私有数据成员中。 假如,我们打开虚拟设备1,那么inode->i_cdev便指向了sdevice0的成员mydev,利用container_of宏, 我们就可以得到g_my_dev结构体的地址,也就可以操作对应的数据缓冲区了。

2. demo实现

直接看gitee仓库吧:04_chrdev_basic/10_chrdev_more_dev_mc · 苏木/imx6ull-driver-demo - 码云 - 开源中国

点击查看详情
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
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
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
#include <linux/init.h> /* module_init module_exit */
#include <linux/kernel.h>
#include <linux/module.h> /* MODULE_LICENSE */

#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>

#include "./timestamp_autogenerated.h"
#include "./version_autogenerated.h"

#define CHRDEV_NAME "sdev" /* 设备名,cat /proc/devices 查看与设备号的对应关系 */
#define CLASS_NAME "sclass" /* 类名,在 /sys/class 中显示的名称 */
#define DEVICE_NAME "sdevice" /* 设备节点名,在 /sys/class/class_name/ 中显示的名称以及 /dev/ 下显示的节点名 */
#define BUFSIZE 32 /* 设置最大偏移量为32 */

#define CHRDEV_CNT (2) /* 设备数量 */

#define CMD_TEST0 _IO('S', 0)
#define CMD_TEST1 _IOW('S', 1, int)
#define CMD_TEST2 _IOR('S', 2, int)
#define CMD_TEST3 _IOW('S', 3, int)

struct __CMD_TEST{
int a;
int b;
int c;
};

/** 父设备号相关结构体,为了创建子设备号(子设备 0 的设备号与父设备号相同)
* 父设备可以看做是代表了整个驱动程序,父设备下挂3个具体的设备
*/
typedef struct __MY_DEV_PARENT
{
/* 1.设备号与设备数量 */
int major; /* 主设备号:占高12位,用来表示驱动程序相同的一类设备,Linux系统中主设备号范围为 0~4095 */
int minor; /* 次设备号:占低20位,用来表示被操作的哪个具体设备 */
dev_t devno; /* 新字符设备的设备号:由主设备号和次设备号组成 */
int dev_cnt; /* 次设备数量 */
/* 2.自动创建设备节点相关成员变量 */
struct class *class; /* 类(无论几个次设备,这里的类只有一个,当然是同一类的情况下) */
}_MY_DEV_PARENT;

_MY_DEV_PARENT parent_dev = {
.major = 0,
.minor = 0,
.devno = 0,
.dev_cnt = CHRDEV_CNT,
.class = NULL,
};

typedef struct __CHAR_DEVICE
{
dev_t dev_num; // 定义dev_t类型(32位大小)的变量dev_num,用来存放设备号
int major; // 主设备号:占高12位,用来表示驱动程序相同的一类设备,Linux系统中主设备号范围为 0~4095
int minor; // 次设备号:占低20位,用来表示被操作的哪个具体设备
struct cdev s_cdev; // 定义cdev结构体类型的变量scdev
struct class *class; // 定于struct class *类型结构体变量 class,表示要创建的类
struct device *device; // 设备
char buf[BUFSIZE]; // 设置数据存储数组mem
} _CHAR_DEVICE;

_CHAR_DEVICE g_sdev[CHRDEV_CNT]; //定义一个 g_sdev 结构体变量

static int sdev_open(struct inode *inode, struct file *file)
{
printk("This is sdev_open!\n");
file->private_data = (void *)(container_of(inode->i_cdev, _CHAR_DEVICE, s_cdev));
return 0;
}

static ssize_t sdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
int i = 0;
loff_t offset = *off; // 将读取数据的偏移量赋值给loff_t类型变量p
size_t count = size;
_CHAR_DEVICE *pDev=(_CHAR_DEVICE *)file->private_data; //在write函数中读取private_data

if (offset > BUFSIZE)
{
return 0;
}

if (count > BUFSIZE - offset)
{
count = BUFSIZE - offset;
}

if (copy_to_user(buf, pDev->buf + offset, count))
{
// 将mem中的值写入buf,并传递到用户空间
printk("copy_to_user error!\n");
return -1;
}

for (i = 0; i < BUFSIZE; i++)
{
printk("buf[%d] %c\n", i, pDev->buf[i]); // 将mem中的值打印出来
}
printk("read offset is %llu, count is %d\n", offset, count);
*off = *off + count; // 更新偏移值

return count;
}

static ssize_t sdev_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
int i = 0;
loff_t offset = *off; // 将读取数据的偏移量赋值给loff_t类型变量p
size_t count = size;
_CHAR_DEVICE *pDev=(_CHAR_DEVICE *)file->private_data; //在write函数中读取private_data

if (offset > BUFSIZE)
{
return 0;
}

if (count > BUFSIZE - offset)
{
count = BUFSIZE - offset;
}

if (copy_from_user(pDev->buf + offset, buf, count))
{
// 将buf中的值,从用户空间传递到内核空间
printk("copy_to_user error \n");
return -1;
}

for (i = 0; i < BUFSIZE; i++)
{
printk("buf[%d] %c\n", i, pDev->buf[i]); // 将mem中的值打印出来
}
printk("write offset is %llu, count is %d\n", offset, count); // 打印写入的值
*off = *off + count; // 更新偏移值

return count;
}

static int sdev_release(struct inode *inode, struct file *file)
{
printk("This is sdev_release!\n");
return 0;
}

static loff_t sdev_llseek(struct file *file, loff_t offset, int whence)
{
loff_t new_offset = 0; // 定义loff_t类型的新的偏移值
switch (whence) // 对lseek函数传递的whence参数进行判断
{
case SEEK_SET:
if (offset < 0 || offset > BUFSIZE)
{
return -EINVAL; // EINVAL=22 表示无效参数
}
new_offset = offset; // 如果whence参数为SEEK_SET,则新偏移值为offset
break;
case SEEK_CUR:
if ((file->f_pos + offset < 0) || (file->f_pos + offset > BUFSIZE))
{
return -EINVAL;
}
new_offset = file->f_pos + offset; // 如果whence参数为SEEK_CUR,则新偏移值为file->f_pos + offset,file->f_pos为当前的偏移值
break;
case SEEK_END:
if (file->f_pos + offset < 0)
{
return -EINVAL;
}
new_offset = BUFSIZE + offset; // 如果whence参数为SEEK_END,则新偏移值为BUFSIZE + offset,BUFSIZE为最大偏移量
break;
default:
break;
}
file->f_pos = new_offset; // 更新file->f_pos偏移值

return new_offset;
}

static long sdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
int val = 0;//定义 int 类型向应用空间传递的变量 val
switch(cmd)
{
case CMD_TEST0:
printk("this is CMD_TEST0\n");
break;
case CMD_TEST1:
printk("this is CMD_TEST1\n");
printk("arg is %ld\n",arg);//打印应用空间传递来的 arg 参数
break;
case CMD_TEST2:
val = 1;//将要传递的变量 val 赋值为 1
printk("this is CMD_TEST2\n");
if(copy_to_user((int *)arg, &val, sizeof(val)) != 0)
{
//通过 copy_to_user 向用户空间传递数据
printk("copy_to_user error \n");
}
break;
case CMD_TEST3:
{
struct __CMD_TEST cmd_test3 = {0};
if (copy_from_user(&cmd_test3, (int *)arg, sizeof(cmd_test3)) != 0)
{
printk("copy_from_user error\n");
}
printk("cmd_test3.a = %d\n", cmd_test3.a);
printk("cmd_test3.b = %d\n", cmd_test3.b);
printk("cmd_test3.c = %d\n", cmd_test3.c);
break;
}
default:
break;
}

return 0;
}

static struct file_operations cdev_ops = {
.owner = THIS_MODULE,//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
.open = sdev_open,
.read = sdev_read,
.write = sdev_write,
.release = sdev_release,
.llseek = sdev_llseek,
.unlocked_ioctl = sdev_ioctl,
}; // 定义file_operations结构体类型的变量 cdev_ops

// 模块入口函数
static int __init chrdev_more_dev_mc_demo_init(void)
{
int i = 0;
int ret = 0; // 定义int类型的变量ret,用来判断函数返回值
printk("*** [%s:%d]Build Time: %s %s, git version:%s ***\n", __FUNCTION__,
__LINE__, KERNEL_KO_DATE, KERNEL_KO_TIME, KERNEL_KO_VERSION);
printk("chrdev_more_dev_mc_demo module init!\n");

ret = alloc_chrdev_region(&parent_dev.devno, 0, parent_dev.dev_cnt, CHRDEV_NAME); // 自动获取设备号,设备名为chrdev_name
if (ret < 0)
{
printk("alloc_chrdev_region is error!\n");
}
printk("alloc_register_region is ok!\n");
parent_dev.major = MAJOR(parent_dev.devno); // 使用MAJOR()函数获取主设备号
parent_dev.minor = MINOR(parent_dev.devno); // 使用MINOR()函数获取次设备号
printk("parent_dev major is %d, minor is %d !\n", parent_dev.major, parent_dev.minor);
for(i = 0; i < parent_dev.dev_cnt; i++)
{
g_sdev[i].dev_num = parent_dev.devno + i; /* devno + i 即可得到连续申请的设备号 */
g_sdev[i].major = MAJOR(g_sdev[i].dev_num); /* 获取分配号的主设备号 */
g_sdev[i].minor = MINOR(g_sdev[i].dev_num); /* 获取分配号的次设备号 */

cdev_init(&g_sdev[i].s_cdev, &cdev_ops); // 使用cdev_init()函数初始化g_sdev.s_cdev结构体,并链接到 cdev_ops 结构体
g_sdev[i].s_cdev.owner = THIS_MODULE; // 将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
ret = cdev_add(&g_sdev[i].s_cdev, g_sdev[i].dev_num, 1); // 使用cdev_add()函数进行字符设备的添加
if (ret < 0)
{
printk("cdev_add is error !\n");
}
printk("cdev_add dev %d is ok !\n", i);
}
//这几个设备都属于同一类,创建一个class
parent_dev.class = class_create(THIS_MODULE, CLASS_NAME); // 使用class_create进行类的创建,类名称为 class_dev
//创建设备
for (i = 0; i < parent_dev.dev_cnt; i++)
{
g_sdev[i].class = parent_dev.class;//这parent_dev.dev_cnt个设备属于同一个类
g_sdev[i].device = device_create(g_sdev[i].class, NULL, g_sdev[i].dev_num, NULL, "%s%d", DEVICE_NAME, i); // 使用device_create进行设备的创建,设备名称为 device_dev
}
return 0;
}

// 模块出口函数
static void __exit chrdev_more_dev_mc_demo_exit(void)
{
int i = 0;
// 需要注意的是, 字符设备的注册要放在申请字符设备号之后,
// 字符设备的删除要放在释放字符驱动设备号之前。
for (i = 0; i < parent_dev.dev_cnt; i++)
{
/* 删除的是 /dev/dev_name */
device_destroy(parent_dev.class, g_sdev[i].dev_num);
printk("g_sdev[%d]: [/dev/%s%d] has been deleted!\n", i, DEVICE_NAME, i);
}
// 删除的是 /sys/class/class_name
class_destroy(parent_dev.class); // 删除创建的类

/* 删除字符设备对象 cdev */
for (i = 0; i < parent_dev.dev_cnt; i++)
{
cdev_del(&g_sdev[i].s_cdev);// 使用cdev_del()函数进行字符设备的删除
}

/* 注销设备号(将会删除 /proc/devices 中的设备号和对应的名称记录) */
unregister_chrdev_region(parent_dev.devno, parent_dev.dev_cnt);
printk("chrdev_more_dev_mc_demo exit!\n");
}

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

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

3. 开发板测试

  • (1)加载驱动
1
insmod chrdev_more_dev_mc_demo.ko
  • (2)查看设备节点
1
ls /dev/sdevice* -alh

会看到有如下打印信息:

1
2
crw-rw----    1 0        root      246,   0 Jan  1 00:00 /dev/sdevice0
crw-rw---- 1 0 root 246, 1 Jan 1 00:00 /dev/sdevice1

在创建设备节点的时候命名就是/dev/sdeviceN,所以这里是0和1两个设备节点,他们会对应不同的缓冲区。

  • (3)向设备节点写入数据
1
2
3
4
5
6
7
echo 123456789 > /dev/sdevice0
echo 987654321 > /dev/sdevice1

# 或者使用应用测试程序
./xxx_demo_app.out /dev/sdevice 1 x # 从x位置读取
./xxx_demo_app.out /dev/sdevice 2 x # 从x位置写入
./xxx_demo_app.out /dev/sdevice 3 x # x为任意值
  • (4)从设备节点读取数据
1
2
3
4
5
6
7
cat /dev/sdevice0
cat /dev/sdevice1

# 或者使用应用测试程序
./xxx_demo_app.out /dev/sdevice 1 x # 从x位置读取
./xxx_demo_app.out /dev/sdevice 2 x # 从x位置写入
./xxx_demo_app.out /dev/sdevice 3 x # x为任意值

参考资料

【1】Linux内核宏Container_Of的详细解释_Linux_脚本之家

【2】【Linux API 揭秘】container_of函数详解 - 董哥聊技术 - 博客园

【3】Typeof (Using the GNU Compiler Collection (GCC))

【4】linux 内核宏container_of剖析 - 知乎