字符设备驱动简介

字符设备是Linux驱动基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的;数据是实时传输的,没有缓存,字符设备是没有文件系统的。

绝大部分设备驱动是字符设备:LED、按键、声卡、IIC、SPI、LCD、摄像头等都是字符设备。这些设备的驱动都是字符设备驱动。

在 Linux 中一切皆为文件,驱动加载成功以后会在“/dev”目录下生成一个相应的文件,应用程序通过对这个名为“/dev/xxx” (xxx 是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。

应用程序运行在用户空间,而 Linux 驱动属于内核的一部分,因此驱动运行于内核空间。当我们在用户空间想要实现对内核的操作,比如使用 open 函数打开/dev/led 这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入” 到内核空间,这样才能实现对底层驱动的操作。

驱动模块加载和卸载

Linux 驱动有两种运行方式,第一种就是将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用“insmod”命令加载驱动模块,或者使用modprobe命令来加载驱动模块,modprobe命令的好处是将这个驱动的依赖项同时加载上,但是要在特定的目录下才能执行(后续再说)。

模块有加载和卸载两种操作,我们在编写驱动的时候需要注册这两种操作函数,模块的加载和卸载注册函数如下:

1
2
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数

module_init 函数用来向 Linux 内核注册一个模块加载函数,xxx_init 就是需要注册的具体函数,当使用“insmod”命令加载驱动的时候, xxx_init 这个函数就会被调用。 module_exit()函数用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数,当使用“rmmod”命令卸载具体驱动的时候 xxx_exit 函数就会被调用。

编写一个chrdevbase的驱动

首先根据加载模板写出chrdevabase.c文件

1
2
3
4
5
6
7
8
9
10
#include <linux/module.h>
static int __init chrdevbase_init(void)//驱动字符入口函数
{
return 0;
}
static void __exit chrdevbase_exit(void)//驱动出口函数
{
}
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);

编写Makefile

1
2
3
4
5
6
7
8
KERNELDIR := /home/moss/linux/imx-kernel/linux-fslc-4.9-2.0.x-imx
CURRENT_PATH := $(shell pwd)
obj-m := chrdevbase.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

第 1 行, KERNELDIR 表示开发板所使用的 Linux 内核源码目录,使用绝对路径,大家根据自己的实际情况填写即可。可以在内核目录中使用pwd来进行查询。
第 2 行, CURRENT_PATH 表示当前路径,直接通过运行“pwd”命令来获取当前所处路径。
第 3 行, obj-m 表示将 chrdevbase.c 这个文件作为模块进行编译(用来获取.ko驱动模块文件),如果是编译到内核是obj -y。
第 86行,具体的编译命令,后面的 modules 表示编译模块, -C 表示将当前的工作目录切换到指定目录中,也就是 KERNERLDIR 目录。 M 表示模块源码目录,“make modules”命令中加入 M=dir 以后程序会自动到指定的 dir 目录中读取模块的源码并将其编译为.ko 文件。

之后在根文件系统中使用modprobe加载模块命令来看缺失的文件,之后都创立好,将编译好的.ko文件复制到下面,之后可使用modprobe命令加载设备,或者使用rmmod卸载设备

1
2
modprobe chrdevbase.ko
rmmod chrdevbase.ko

在板子的命令终端使用(推荐使用软件MobaXterm来查看串口)。

字符设备注册与注销

对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。字符设备的注册和注销函数原型如下所示:

1
2
static inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)

major: 主设备号, Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两部分,关于设备号后面会详细讲解。
name:设备名字,指向一串字符串。查看设备中的设备号可以使用“cat /proc/devices ”来查看。
fops: 结构体 file_operations 类型指针,指向设备的操作函数集合变量。 (每个驱动都是运用一部分)

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
                                        /*file_operations结构体*/
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*mremap)(struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsignedlong);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
};

字符设备驱动框架

设备的具体操作函数就是通过file_operations结构体来进行设置,首先初始化之前要进行分析需求,也就是对chrtest这个设备进行哪些操作,才知道要实现哪些操作函数。此设置是假设对字符设备进行打开和关闭操作(基本设备都要打开关闭),对字符设备进行读写(假设 这个设备控制着一段缓冲区(内存),应用程序需要通过 read 和 write 这两个函数对 chrtest 的缓冲区进行读写操作 )。

所以进行修改chrdevbase.c文件内容:

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
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>

static int chrtest_open(struct inode *inode, struct file *filp)
{
return 0;
}
static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
static ssize_t chrtest_write(struct file *filp, const char __user *buf , size_t cnt, loff_t *offt)
{
return 0;
}
static int chrtest_release(struct inode *inode, struct file *filp)
{
return 0;
}
static struct file_operations test_fops = {
.owner = THIS_MODULE,
.open = chrtest_open,
.read = chrtest_read,
.write = chrtest_write,
.release = chrtest_release,
};
static int __init chrdevbase_init(void)//驱动字符入口函数
{
int value = 0;

value = register_chrdev(200, "chrtest", &test_fops);
if(value < 0)
{
printk("注册失败\n");//内核空间的printf
}

return 0;
}
static void __exit chrdevbase_exit(void)//驱动出口函数
{
unregister_chrdev(200, "chrtest");
}
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
MODULE_LICENSE("GPL");//防止加载模块警告

关于设备号码

静态分配设备号

注册字符设备的时候需要给设备指定一个设备号,这个设备号可以是驱动开发者静态的指定一个设备号,比如选择 200 这个主设备号。有一些常用的设备号已经被 Linux 内核开发者给分配掉了,具体分配的内容可以查看文档 Documentation/devices.txt。并不是说内核开发者已经分配掉的主设备号我们就不能用了,具体能不能用还得看我们的硬件平台运行过程中有没有使用这个主设备号,。

动态分配设备号

静态分配设备号需要我们检查当前系统中所有被使用了的设备号,然后挑选一个没有使用的。而且静态分配设备号很容易带来冲突问题, Linux 社区推荐使用动态分配设备号,在注册字符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,这样就避免了冲突。卸载驱动的时候释放掉这个设备号即可,

1
2
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)//申请函数
void unregister_chrdev_region(dev_t from, unsigned count)//释放函数

申请设备函数有 4 个参数:
dev:保存申请到的设备号。
baseminor: 次设备号起始地址, alloc_chrdev_region 可以申请一段连续的多个设备号,这些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor 为起始地址地址开始递增。一般 baseminor 为 0,也就是说次设备号从 0 开始。
count: 要申请的设备号数量。
name:设备名字。
释放函数有两个参数:
from:要释放的设备号。
count: 表示从 from 开始,要释放的设备号数量。

实现例程

应用程序调用 open 函数打开 chrdevbase 这个设备,打开以后可以使用 write 函数向chrdevbase 的写缓冲区 writebuf 中写入数据(不超过 100 个字节),也可以使用 read 函数读取读缓冲区 readbuf 中的数据操作,操作完成以后应用程序使用 close 函数关闭 chrdevbase 设备。

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
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/ide.h>
static char readbuf[100];
static char writebuf[100];
static char kerneldata[] = {"kernel data"};

static int chrdev_open(struct inode *inode, struct file *filp)
{
printk("open devices\n");
return 0;
}
static int chrdev_release(struct inode *inode, struct file *filp)
{
printk("release devices\n");
return 0;
}
static ssize_t chrdev_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int value = 0;
//向用户空间发送信息
memcpy(readbuf, kerneldata, sizeof(kerneldata));//将kerneldata的sizeof个数据复制readbuf
value = copy_to_user(buf, readbuf, cnt);
if(value == 0)
printk("kernel senddata ok\n");
else
printk("kernel senddata failed");

return 0;
}
static ssize_t chrdev_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue = 0;
retvalue = copy_from_user(writebuf, buf, cnt);
if(retvalue == 0)
printk("kernel recedata :%s ok\n", writebuf);
else
printk("kernel recedata failed\n");

return 0;
}
static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE,
.open = chrdev_open,
.read = chrdev_read,
.write = chrdev_write,
.release = chrdev_release,
};
static int __init chrdevbase_init(void)
{
int retvalue = 0;

/* 注册字符设备驱动 */
retvalue = register_chrdev(200, "chrdevbase", &chrdevbase_fops);
if(retvalue < 0){
printk("chrdevbase driver register failed\r\n");
}

return 0;
}
static void __exit chrdevbase_exit(void)
{
//卸载驱动
unregister_chrdev(200, "chrdevbase")
}
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
MODULE_LICENSE("GPL");

copy_to_user/copy_from_user函数解析

copy_to_user和copy_from_user是在进行驱动相关程序设计的时候,要经常遇到的函数。由于内核空间与用户空间的内存不能直接互访,因此借助函数copy_to_user()完成内核空间到用户空间的复制,函数copy_from_user()完成用户空间到内核空间的复制。

1
2
3
4
5
6
7
8
unsigned long copy_to_user (void __user * to, const void * from, unsigned long n);
to 目标地址,这个地址是用户空间的地址
from源地址 ,这个地址是内核空间的地址
n 要拷贝数据的字节数
unsigned long copy_from_user (void * to, const void __user * from, unsigned long n);
to 目标地址,这个地址是用户空间的地址
from源地址 ,这个地址是内核空间的地址
n 要拷贝数据的字节数

编写测试APP

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
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

static char usrdata[] = {"usr data!"};

/*
* @description : main 主程序
* @param - argc : argv 数组元素个数
* @param - argv : 具体参数
* @return : 0 成功;其他 失败
*/
int main(int argc, char *argv[])
{
int fd, retvalue;
char *filename;
char readbuf[100], writebuf[100];
if(argc != 3){
printf("Error Usage!\r\n");
return -1;
}

filename = argv[1];
/* 打开驱动文件 */
fd = open(filename, O_RDWR);
if(fd < 0){
printf("Can't open file %s\r\n", filename);
return -1;
}

if(atoi(argv[2]) == 1){ /* 从驱动文件读取数据 */
retvalue = read(fd, readbuf, 50);
if(retvalue < 0){
printf("read file %s failed!\r\n", filename);
}else{
/* 读取成功,打印出读取成功的数据 */
printf("read data:%s\r\n",readbuf);
}
}
if(atoi(argv[2]) == 2){
/* 向设备驱动写数据 */
memcpy(writebuf, usrdata, sizeof(usrdata));
retvalue = write(fd, writebuf, 50);
if(retvalue < 0){
printf("write file %s failed!\r\n", filename);
}
}
/* 关闭设备 */
retvalue = close(fd);
if(retvalue < 0){
printf("Can't close file %s\r\n", filename);
return -1;
}

return 0;
}

使用交叉编译器进行编译

1
arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp

把chrdevbaseApp也复制到那个文件夹中。

创建设备设备节点

驱动加载成功需要在/dev 目录下创建一个与之对应的设备节点文件,应用程序就是通过操这个设备节点文件来完成对具体设备的操作。输入如下命令创建/dev/chrdevbase 这个设备节点文件 :

1
2
3
mknod /dev/chrdevbase c 200 0
ls /dev/chrdevbase -l
./chrdevbaseApp /dev/chrdevbase 1 //进行测试