上篇文章介绍了字符设备的开发模板,但那是一种旧版本的驱动开发模式,设备驱动需要手动分配设备号再使用 register_chrdev进行注册,加载成功以后还需要手动使用mknod命令创建设备节点,比较麻烦

目前Linux内核推荐的新字符设备驱动API函数,使得驱动的使用更加自动化,本篇就来一起研究下。

先看目录:

[TOC]

1 旧字符设备驱动的弊端

使用register_chrdev函数注册字符设备,需要指定一个设备号,这就造成:

  • 需要事先确定好哪些主设备号没有使用
  • 会将一个主设备号下的所有次设备号都使用掉,比如主设备号为200,那么 0~1048575(2^20-1)这个区间的次设备号就全部都被占用了

回顾上一篇的操作,先是加载驱动:

加载完,还有手动使用mknod指令来手动创建该设备节点,并且指定驱动程序中写死的设备号:

本篇,就要使用一种新的字符驱动编写方式,实现设备号的自动分配,省去mknod指令操作

2 新字符设备驱动原理

2.1 分配和释放设备号

使用设备号的时候向Linux内核申请,需要几个就申请几个,由Linux内核分配设备可以使用的设备号。

使用如下函数来申请设备号(该函数在上篇提到过):

1
2
3
4
5
6
7
/*
* dev:保存申请到的设备号
* baseminor:次设备号起始地址,一般baseminor为0 (次设备号以baseminor为起始地址地址开始递)
* count:要申请的设备号数量
* name:设备名字
*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

如果给定了设备的主设备号和次设备号就使用如下所示函数来注册设备号即可:

1
2
3
4
5
6
/*
* from:要申请的起始设备号
* count:要申请的设备号数量
* name:设备名字
*/
int register_chrdev_region(dev_t from, unsigned count, const char *name)

注销字符设备之后要释放设备号,不管是通过alloc_chrdev_region函数的动态分配还是register_chrdev_region函数手动指定的设备号,统一使用(和上篇使用的一样)的释放函数:

1
2
3
4
5
/*
* from:要释放的设备号
* count:表示从from开始,要释放的设备号数量
*/
void unregister_chrdev_region(dev_t from, unsigned count)

新字符设备驱动下,设备号分配示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int major;      /*主设备号*/ 
int minor; /*次设备号*/
dev_t devid; /*设备号*/

/*定义了主设备号*/
if (major)
{
devid = MKDEV(major, 0); /*大部分驱动次设备号都选择0*/
register_chrdev_region(devid, 1, "test");
}
/*没有定义设备号*/
else
{
alloc_chrdev_region(&devid, 0, 1, "test"); /*申请设备号*/
major = MAJOR(devid); /*获取分配号的主设备号*/
minor = MINOR(devid); /*获取分配号的次设备号*/
}

2.2 字符设备注册

2.2.1 cdev字符设备结构

在Linux中使用cdev结构体表示一个字符设备,其定义在include/linux/cdev.h文件中:

1
2
3
4
5
6
7
8
struct cdev { 
struct kobject kobj;
struct module *owner;
const struct file_operations *ops; /*文件操作函数集合*/
struct list_head list;
dev_t dev; /*设备号*/
unsigned int count;
};

2.2.2 cdev_init 函数

定义好cdev变量以后就要使用cdev_init函数对其进行初始化:

1
2
3
4
5
/*
* cdev:要初始化的cdev结构体变量
* fops:字符设备文件操作函数集合
*/
void cdev_init(struct cdev *cdev, const struct file_operations *fops)

该函数的使用示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/*要初始化的cdev结构体*/
struct cdev testcdev;

/* 设备操作函数 */
static struct file_operations test_fops = {
.owner = THIS_MODULE,
/* 其他具体的初始项 */
};

testcdev.owner = THIS_MODULE;

/* 初始化cdev*/
cdev_init(&testcdev, &test_fops);

2.2.3 cdev_add函数

该函数用于向Linux系统添加字符设备,即cdev结构体变量:

1
2
3
4
5
6
/*
* cdev:要初始化的cdev结构体变量
* dev:字符设备所使用的设备号
* count:要添加的设备数量
*/
int cdev_add(struct cdev *p, dev_t dev, unsigned count)

2.2.4 cdev_del函数

卸载驱动的时候要使用cdev_del函数从Linux内核中删除字符设备:

1
2
3
4
/*
* p:要删除的字符设备
*/
void cdev_del(struct cdev *p)

2.3 自动创建设备节点

上篇的Linux驱动实验中,在使用modprobe加载驱动程序以后还需要使用“mknod”命令手动创建设备节点,比较麻烦,这里就来研究一下如何实现自动创建设备节点。

2.3.1 mdev机制

在Linux下通过udev来实现设备文件的自动创建与删除。使用busybox构建根文件系统的时候,busybox会创建一个udev的简化版本mdev

所以,在嵌入式开发中使用mdev来实现设备节点文件的自动创建与删除。Linux系统中的热插拔事件也由mdev 管理,在/etc/init.d/rcS 文件中如下语句:

1
echo /sbin/mdev > /proc/sys/kernel/hotplug 

2.3.2 创建和删除类

自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在cdev_add函数后面添
加自动创建设备节点相关代码。

首先要创建一个class类,其实是个结构体,定义在include/linux/device.h里面。class_create是类创建函数(宏定义):

1
2
3
4
5
6
7
8
9
#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_destroy删除掉类

1
2
3
4
/*
* cls:要删除的类
*/
void class_destroy(struct class *cls);

2.3.3 创建设备

创建好类以后还不能实现自动创建设备节点,还需要在这个类下创建一个设备。使用device_create函数创建设备:

1
2
3
4
5
6
7
8
9
10
11
12
/*
* class:设备要创建哪个类下面
* parent:父设备, 一般为 NULL
* devt:设备号
* drvdata:设备可能会使用的一些数据,一般为 NULL
* fmt:设备名字
*/
struct device *device_create(struct clas *class,
struct device *parent,
dev_t devt,
void *drvdata,
const char *fmt, ...)

参数最后的...表示这在是一个可变参数的函数。

2.4 设置文件私有数据

每个硬件设备都有一些属性, 比如主设备号(dev_t),类(class)、设备(device)、开关状态(state)等等,在编写驱动的时候你可以将这些属性全部写成变量的形式:

1
2
3
4
5
6
dev_t         devid;     /*设备号*/ 
struct cdev cdev; /*cdev*/
struct class *class; /*类*/
struct device *device; /*设备*/
int major; /*主设备号*/
int minor; /*次设备号*/

可以将所有属性信封装到结构体中, 并在编写驱动open函数的时候将其作为私有数据添加到设备文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*设备结构体*/ 
struct test_dev{
dev_t devid; /*设备号*/
struct cdev cdev; /*cdev*/
struct class *class; /*类*/
struct device *device; /*设备*/
int major; /*主设备号*/
int minor; /*次设备号*/
};

struct test_dev testdev;

/*open函数*/
static int test_open(struct inode *inode, struct file *filp)
{
filp->private_data = &testdev; /*设置私有数据*/
return 0;
}

3 驱动程序编写

在上篇的基础上进行修改,因为只是更换的驱动程序的编写方式,与应用程序无关,因此只修改驱动程序即可。

3.1 添加一些定义

因为上篇文章的代码中使用的是chrdevbase这个名称,为了减少修改量,这里仅把结构体类型定义为带有new标志的newchr_dev,变量名仍使用chrdevbase这个名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define CHRDEVBASE_CNT			1		/* 设备号个数 */
#define CHRDEVBASE_NAME "chrdevbase" /* 名字 */

/*newchr设备结构体 */
struct newchr_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
};

struct newchr_dev chrdevbase; /* 自定义字符设备 */

3.2 修改open函数

在上篇程序的基础上增加了一条“设置私有数据”

1
2
3
4
5
6
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
printk("chrdevbase open!\r\n");
filp->private_data = &chrdevbase; /* 设置私有数据 */
return 0;
}

3.3 修改init函数

这个修改比较大,因为要在init函数中使用设备号的自动分配

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
static int __init chrdevbase_init(void)
{
/* 注册字符设备驱动 */
/* 1、创建设备号 */
if (chrdevbase.major) /* 定义了设备号 */
{
chrdevbase.devid = MKDEV(chrdevbase.major, 0);
register_chrdev_region(chrdevbase.devid, CHRDEVBASE_CNT, CHRDEVBASE_NAME);
}
else /* 没有定义设备号 */
{
alloc_chrdev_region(&chrdevbase.devid, 0, CHRDEVBASE_CNT, CHRDEVBASE_NAME); /* 申请设备号 */
chrdevbase.major = MAJOR(chrdevbase.devid); /* 获取分配号的主设备号 */
chrdevbase.minor = MINOR(chrdevbase.devid); /* 获取分配号的次设备号 */
}
printk("chrdevbase major=%d,minor=%d\r\n",chrdevbase.major, chrdevbase.minor);

/* 2、初始化cdev */
chrdevbase.cdev.owner = THIS_MODULE;
cdev_init(&chrdevbase.cdev, &chrdevbase_fops);

/* 3、添加一个cdev */
cdev_add(&chrdevbase.cdev, chrdevbase.devid, CHRDEVBASE_CNT);

/* 4、创建类 */
chrdevbase.class = class_create(THIS_MODULE, CHRDEVBASE_NAME);
if (IS_ERR(chrdevbase.class))
{
return PTR_ERR(chrdevbase.class);
}

/* 5、创建设备 */
chrdevbase.device = device_create(chrdevbase.class, NULL, chrdevbase.devid, NULL, CHRDEVBASE_NAME);
if (IS_ERR(chrdevbase.device))
{
return PTR_ERR(chrdevbase.device);
}

printk("chrdevbase init done!\r\n");
return 0;
}

3.4 修改exit函数

因为init修改较大,对应的exit也要进行大的修改:

1
2
3
4
5
6
7
8
9
10
11
static void __exit chrdevbase_exit(void)
{
/* 注销字符设备驱动 */
cdev_del(&chrdevbase.cdev);/* 删除cdev */
unregister_chrdev_region(chrdevbase.devid, CHRDEVBASE_CNT); /* 注销设备号 */

device_destroy(chrdevbase.class, chrdevbase.devid);
class_destroy(chrdevbase.class);

printk("chrdevbase exit done!\r\n");
}

至此,修改完毕,其它的与之前的一样。

3.5 新旧驱动方式对比

通过一张图来对比新旧两种驱动编写方式的区别

  • 旧方式编写驱动的流程

  • 新方式编写驱动的流程

可以看出主要区别在驱动的加载和卸载。

4 编译驱动

和上次编译驱动的方式一样,使用makefile,因为驱动的c文件名由chrdevbase.c改为了newchrdevbase.c,因此makefile文件中也要把名字改掉。

编译完之后,将编译出的ko文件先复制到ubuntu虚拟机的tftpboot目录中,为后面的测序做准备。

复制后,看一下tftpboot目录:

5 程序测试

5.1 文件发送到板子

和上篇一样,使用tftp传输,将ubuntu虚拟机编译出的ko文件发送到linux板子中

再来看下tftp传输的硬件环境示意图:

然后是传输指令以及传输结果,可以看到newchrdevbase.ko已经从ubuntu虚拟机的tftpboot目录传输到了linux板子的/lib/modules/4.1.15目录中了。

5.2 测试

输入如下两条指令加载 newchrdevbase.ko 驱动模块:

1
2
depmod   //第一次加载驱动的时候需要运行此命令 
modprobe newchrdevbase.ko //加载驱动

驱动加载成功后,可以看到自动申请到的主设备号和次设备号,如下图,主设备号为249。

再输入ls /dev/chrdevbase -l指令验证/dev/chrdevbase 这个设备节点文件是否存在,如下图,可以看到设备存在,注意和上篇旧驱动方式操作上的不同之处,旧的驱动方式需要额外使用mknod指令来手动创建该设备节点

驱动已经加载成功,再来测试APP程序,理论上和上篇的效果一样,实测也是:

OK,测试完毕,测试完使用rmmod指令卸载驱动。

6 总结

此篇文章针对上篇文章使用旧字符驱动编写方式存在的不足,介绍了一种新的字符驱动编写方式,对比两种方式编写的主要区别,在上篇驱动代码的基础上进行修改,并测试通过,和上篇实现一样的效果,但驱动的加载更加方便,不再需要人为指定设备号。

进入公众号,回复加群,可加入学习交流群~