🚀 设备号

✈ 设备号简述

设备号由主设备号和 次设备号构成。主设备号用来表示一个特定的驱动程序。 次设备号用来表示使用该驱动程序的各个设备。 Linux 提供了一个名为dev_t 的数据类型表示设备号, dev_t 定义在文件 include/linux/types.h 里面, 定义如下:

1
2
typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t

由 dev_t 类型的定义可知,设备号是一个32位的变量, 其中 前12 位用来表示主设备号(0-4095 ), 后20 位用来表示次设备号。

Linux 提供了几个宏定义来操作设备号:

1
2
3
4
5
#define MINORBITS20 //次设备号的位数, 一共是 20 位
#define MINORMASK ((1U << MINORBITS) - 1) //次设备号的掩码
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) //在 dev_t 里面获取我们的主设备号
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) //在 dev_t 里面获取我们的次设备号
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) //将我们的主设备号和次设备号组成一个 dev_t 类型。

✈ 设备号的分配

分配设备号有两种方案,静态分配和动态分配。由于静态分配设备号可能存在冲突问题, 因此建议使用动态分配设备号。

函数定义:int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);

各参数的含义:

参数 含义
dev 保存申请到的设备号。
baseminor 次设备号的起始地址,一般取0,即次设备号从 0 开始。
count 要申请的设备号数量。
name 设备名字。

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

动态分配会优先分配 255 到 234 的主设备号。

✈ 设备号的注销

设备号释放函数 :void unregister_chrdev_region(dev_t from, unsigned count);

参数 含义
from 要释放的设备号。
count 表示从 from 开始, 要释放的设备号数量。

🚀 字符类设备的注册和注销

✈ 初始化cdev 结构体

在 Linux 内核中, 使用 cdev 结构体描述一个字符设备, cdev 结构体的定义如下:

1
2
3
4
5
6
7
8
9
//描述字符设备的一个结构体
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops; // 文件操作集
struct list_head list;
dev_t dev; // 设备号
unsigned int count; // 设备数量
};

函数 cdev_init 用于初始化 cdev 结构体,其定义如下:

1
2
3
// cdev: 要初始化的 cdev 结构体变量。
// ops: 实际就是把此文件操作集写给 cdev
void cdev_init(struct cdev *cdev, const struct file_operations *ops);

cdev_init 函数只给 cdev 结构体的 ops 成员变量赋了值,一般还需给 owner 赋值:

1
cdev.owner = THIS_MODULE;

✈ 字符类设备的注册和注销

1
2
3
4
5
6
7
8
9
10
// 功能: 注册字符类设备
// cdev: 要注册的 cdev
// dev: 设备号
// count: 次设备号的数量
int cdev_add(struct cdev *cdev, dev_t dev, unsigned count);


// 功能: 注销字符类设备
// cdev: 要注销的 cdev
void cdev_del(struct cdev *cdev);

🚀 创建和卸载设备

✈ 类的创建和删除

内核中 struct class 结构体类型变量对应一个类, 使用宏定义 class_create 创建一个类 ,然后再调用函数 device_create 来在 /dev 目录下创建相应的设备节点。

class_create 的定义:

1
2
3
4
5
6
7
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})

struct class *__class_create(struct module *owner, const char *name, struct lock_class_key *key);

class_create 一共有两个参数, 参数 owner 一般为 THIS_MODULE, 参数 name 是类名字。 返回值是个指向结构体 class 的指针, 也就是创建的类。

卸载驱动程序的时候需要删除掉类, 类删除函数为 class_destroy, 函数原型如下:

1
void class_destroy(struct class *cls);		// cls 为要删除的类

✈ 创建和卸载设备

创建完成一个类后, 使用 device_create 函数在这个类下创建一个设备。 device_create
函数原型如下:

1
2
3
4
5
6
7
8
9
10
11
/*
参数:
class: 就是设备要创建哪个类下面。
parent: 父设备,一般为 NULL,即没有父设备。
devt: 设备号。
drvdata:设备可能会使用的一些数据,一般为 NULL。
fmt: 设备名字,如果设置 fmt=xxx 的话, 就会生成/dev/xxx 这个设备文件。
返回值:
创建好的设备。
*/
struct device *device_create(struct class *class,struct device *parent,dev_t devt,void *drvdata,const char *fmt, ...);

设备删除函数为 device_destroy, 函数原型如下:

1
void device_destroy(struct class *class, dev_t devt);	// 参数 devt 是要删除的设备号。

🚀 字符设备驱动程序框架

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
#include <linux/init.h>         // 包含初始化宏定义
#include <linux/module.h> // 包含初始化加载模块
#include <linux/fs.h> // 包含文件操作集定义
#include <linux/kdev_t.h>
#include <linux/cdev.h> // 包含对字符设备结构cdv以及操作函数的定义
#include <linux/device.h> // 包含device、class等结构的定义

#define DEVICE_NUMBER 1 // 次设备的数量
#define DEVICE_NAME "chrdev" // 设备名称
#define DEVICE_CLASS_NAME "chrdev_class" // 类的名称
#define DEVICE_NODE_NAME "chrdev_node" // 设备节点名称

dev_t dev_num; // 设备号
struct cdev cdev;
struct device *device; // 设备
struct class *class; // 类

static int cdev_open(struct inode *inode, struct file *file)
{
printk("cdev open!!!\n");
return 0;
}

static int cdev_close(struct inode *inode, struct file *file)
{
printk("cdev close!!!\n");
return 0;
}

/* 设备操作函数结构体 */
struct file_operations cdev_fops = {
.owner = THIS_MODULE,
.open = cdev_open,
.release = cdev_close,
};

/* 模块的入口 */
static int chrdev_init(void)
{
int ret;
ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME); // 动态分配设备号
if (ret < 0)
{
printk("alloc_chrdev_region error!\n");
return -1;
}
printk("alloc_chrdev_region succeed!\n");
printk("dev_num = %d\n", dev_num); // 打印设备号
printk("major_num = %d, minor_num = %d\n", MAJOR(dev_num), MINOR(dev_num)); // 打印主设备号和次设备号

cdev.owner = THIS_MODULE;
cdev_init(&cdev, &cdev_fops);
cdev_add(&cdev, dev_num, DEVICE_NUMBER); // 注册设备

class = class_create(THIS_MODULE, DEVICE_CLASS_NAME); // 创建class类
device = device_create(class, NULL, dev_num, NULL, DEVICE_NODE_NAME); // 在class类下创建设备

return 0;
}

/* 模块的出口 */
static void chrdev_exit(void)
{
unregister_chrdev_region(dev_num, DEVICE_NUMBER); // 注销设备号
cdev_del(&cdev); // 删除设备
device_destroy(class, dev_num); // 注销设备
class_destroy(class); // 删除类
printk("goodbye!\n");
}

/* 声明模块的入口和出口 */
module_init(chrdev_init);
module_exit(chrdev_exit);

MODULE_LICENSE("GPL"); /* 声明模块的开源许可证 */