Linux字符设备驱动开发

一.字符设备驱动简介

        字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节
流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、IIC、SPI,
LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。Linux 应用程序对驱动程序的调用如下图所示:

Linux字符设备驱动开发_第1张图片

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

        应用程序运行在用户空间,而 Linux 驱动属于内核的一部分,因此驱动运行于内核空间。当我们在用户空间想要实现对内核的操作,比如使用 open 函数打开/dev/led 这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入”到内核空间,这样才能实现对底层驱动的操作。open、close、write 和 read 等这些函数是由 C 库提供的,在 Linux 系统中,系统调用作为 C 库的一部分。当我们调用 open 函数的时候流程如下图所示:

        应用程序中调用了 open 函数,那么在驱动程序中也得有一个名为 open 的函数。每一个系统调用,在驱动中都有与之对应的一个驱动函数,Linux 内核文件中的file_operations 结构体就是 Linux 内核驱动操作函数集合,内容如下所示:

struct file_operations {
    //owner 拥有该结构体的模块的指针,一般设置为 THIS_MODULE。
    struct module *owner;
    //llseek 函数用于修改文件当前的读写位置。
    loff_t (*llseek) (struct file *, loff_t, int);
    //read 函数用于读取设备文件。
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    //write 函数用于向设备文件写入(发送)数据。
    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 *);
    //poll 是个轮询函数,用于查询设备是否可以进行非阻塞的读写。
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    //unlocked_ioctl 函数提供对于设备的控制功能,与应用程序中的 ioctl 函数对应。
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    //mmap 函数用于将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,
    //比如 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*mremap)(struct file *, struct vm_area_struct *);
    //open 函数用于打开设备文件。
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    //release 函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应。
    int (*release) (struct inode *, struct file *);
    //fasync 函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。
    int (*fsync) (struct file *, loff_t, loff_t, int datasync);
    //aio_fsync 函数与 fasync 函数的功能类似,只是 aio_fsync 是异步刷新待处理的数据。
    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, unsigned long);
    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
};

二.字符设备驱动开发步骤

       1. 驱动模块的加载和卸载

        Linux 驱动有两种运行方式,第一种就是将驱动编译进 Linux 内核中,这样当 Linux 内核启
动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块(Linux 下模块扩展名为.ko),在
Linux 内核启动以后使用“insmod”命令加载驱动模块。

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

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

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

/* 驱动入口函数,定义了个名为 xxx_init 的驱动入口函数,并且使用了“__init”来修饰。*/
static int __init xxx_init(void)
{
    /* 入口函数具体内容 */
    return 0;
}

/* 驱动出口函数,定义了个名为 xxx_exit 的驱动出口函数,并且使用了“__exit”来修饰。*/
static void __exit xxx_exit(void)
{
    /* 出口函数具体内容 */
}

/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);

        驱动编译完成以后扩展名为.ko,有两种命令可以加载驱动模块: insmod和modprobe,modprobe 命令默认会去/lib/modules/目录中查找模块insmod 是最简单的模块加载命令,此命令用于加载指定的.ko 模块,比如加载 drv.ko 这个驱动模块,命令如下:

insmod drv.ko
modprobe drv.ko

        驱动模块的卸载使用命令“rmmod”即可,比如要卸载 drv.ko,使用如下命令即可:

rmmod drv.ko

       2. 字符设备的注册与注销

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

        register_chrdev 函数用于注册字符设备,unregister_chrdev 函数用户注销字符设备。

static inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)
//major:主设备号,Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两部分
//name:设备名字,指向一串字符串。
//fops: 结构体 file_operations 类型指针,指向设备的操作函数集合变量。

static inline void unregister_chrdev(unsigned int major, const char *name)
//major: 要注销的设备对应的主设备号。
//name: 要注销的设备对应的设备名。

        一般字符设备的注册在驱动模块的入口函数 xxx_init 中进行,字符设备的注销在驱动模块的出口函数 xxx_exit 中进行。

        3.实现设备的具体操作函数

        定义的file_operations 结构体就是设备的具体操作函数,几乎所有的设备都得提供打开和关闭的功能。因此我们需要实现 file_operations 中的 open 和 release 这两个函数。假设 chrtest 这个设备控制着一段缓冲区(内存),应用程序需要通过 read 和 write 这两个函数对 chrtest 的缓冲区进行读写操作。所以需要实现 file_operations 中的 read 和 write 这两个函数。完成以后的内容如下所示:

/* 打开设备 */
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 xxx_init(void)
{
    /* 入口函数具体内容 */
    int retvalue = 0;

    /* 注册字符设备驱动 */
    retvalue = register_chrdev(200, "chrtest", &test_fops);
    if(retvalue < 0){
    /* 字符设备注册失败,自行处理 */
    }
    return 0;
}

/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
    /* 注销字符设备驱动 */
    unregister_chrdev(200, "chrtest");
}

/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);

        4.添加 LICENSE 和作者信息

        最后我们需要在驱动中加入 LICENSE 信息和作者信息,其中 LICENSE 是必须添加的,否则的话编译的时候会报错,作者信息可以添加也可以不添加。 LICENSE 和作者信息的添加使用如下两个函数: 

MODULE_LICENSE() //添加模块 LICENSE 信息
MODULE_AUTHOR()  //添加模块作者信息

三.Linux 设备号

1.设备号的组成

        为了方便管理, Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。 Linux 提供了一个名为 dev_t 的数据类型表示设备号,dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型。这 32 位的数据构成了主设备号和次设备号两部分,其中高 12 位为主设备号, 低 20 位为次设备号。因此 Linux 系统中主设备号范围为 0~4095。

2.设备号的分配

        静态分配设备号:设备号分配主要是主设备号的分配。注册字符设备的时候需要给设备指定一个设备号,这个设备号可以是驱动开发者静态的指定一个设备号,比如选择 200 这个主设备号。可以使用“cat /proc/devices”命令即可查看当前系统中所有已经使用了的设备号。

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

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

/*
dev:保存申请到的设备号。

baseminor: 次设备号起始地址, alloc_chrdev_region 可以申请一段连续的多个设备号,这
			些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor 为起始地址地址开始递
			增。一般 baseminor 为 0,也就是说次设备号从 0 开始。

count: 要申请的设备号数量。

name:设备名字。
*/

        注销字符设备之后要释放掉设备号,设备号释放函数如下:

void unregister_chrdev_region(dev_t from, unsigned count)

/*
from:要释放的设备号。

count: 表示从 from 开始,要释放的设备号数量。
*/

四.新字符设备驱动

1.分配和释放设备号

        使用 register_chrdev 函数注册字符设备的时候只需要给定一个主设备号即可,但是这样会带来两个问题:

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

        如果没有指定设备号的话就使用如下函数来申请设备号:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

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

int register_chrdev_region(dev_t from, unsigned count, const char *name)

/*
from:要申请的起始设备号
count:要申请的数量,一般都是一个
name:设备名字
*/

        注 销 字 符 设 备 之 后 要 释 放 掉 设 备 号 , 不 管 是 通 过 alloc_chrdev_region 函 数 还 是 register_chrdev_region 函数申请的设备号,统一使用如下释放函数:

void unregister_chrdev_region(dev_t from, unsigned count)

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

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.新的字符设备注册方法

        a.字符设备结构

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

struct cdev {
	struct kobject kobj;
	struct module *owner;
	const struct file_operations *ops;
	struct list_head list;
	dev_t dev;
	unsigned int count;
};

        在 cdev 中有两个重要的成员变量: ops 和 dev,这两个就是字符设备文件操作函数集合 file_operations 以及设备号 dev_t。编写字符设备驱动之前需要定义一个 cdev 结构体变量,这个变量就表示一个字符设备,如下所示:

struct cdev test_cdev;

        b.cdev_init 函数

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

void cdev_init(struct cdev *cdev, const struct file_operations *fops)

        参数 cdev 就是要初始化的 cdev 结构体变量,参数 fops 就是字符设备文件操作函数集合。使用 cdev_init 函数初始化 cdev 变量的示例代码如下:

struct cdev testcdev;

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

testcdev.owner = THIS_MODULE;
cdev_init(&testcdev, &test_fops); /* 初始化 cdev 结构体变量 */

        c.cdev_add 函数

        cdev_add 函数用于向 Linux 系统添加字符设备(cdev 结构体变量),首先使用 cdev_init 函数完成对 cdev 结构体变量的初始化,然后使用 cdev_add 函数向 Linux 系统添加这个字符设备。cdev_add 函数原型如下:

int cdev_add(struct cdev *p, dev_t dev, unsigned count)

        参数 p 指向要添加的字符设备(cdev 结构体变量),参数 dev 就是设备所使用的设备号,参数 count 是要添加的设备数量。加入 cdev_add 函数,内容如下所示:

struct cdev testcdev;

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

testcdev.owner = THIS_MODULE;
cdev_init(&testcdev, &test_fops); /* 初始化 cdev 结构体变量 */
cdev_add(&testcdev, devid, 1); /* 添加字符设备 */

        d.cdev_del 函数

        卸载驱动的时候一定要使用 cdev_del 函数从 Linux 内核中删除相应的字符设备, cdev_del 函数原型如下:

void cdev_del(struct cdev *p)

        参数 p 就是要删除的字符设备。如果要删除字符设备,参考如下代码:

cdev_del(&testcdev); /* 删除 cdev */

五.自动创建设备节点

        当我们使用 modprobe 加载驱动程序以后还需要使用命令 “mknode”手动创建设备节点。本节就来讲解一下如何实现自动创建设备节点,在驱动中实现自动创建设备节点的功能以后,使用 modprobe 加载驱动模块成功的话就会自动在/dev 目录下创建对应的设备文件。

1.mdev机制

        udev 是一个用户程序,在 Linux 下通过 udev 来实现设备文件的创建与删除, udev 可以检测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。比如使用modprobe 命令成功加载驱动模块以后就自动在/dev 目录下创建对应的设备节点文件,使用 rmmod 命令卸载驱动模块以后就删除掉/dev 目录下的设备节点文件。 使用 busybox 构建根文件系统的时候, busybox 会创建一个 udev 的简化版本—mdev,所以在嵌入式 Linux 中我们使用 mdev 来实现设备节点文件的自动创建与删除,

2.创建和删除类

        自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在 cdev_add 函数后面添加自动创建设备节点相关代码。首先要创建一个 class 类, class 是个结构体,定义在文件 include/linux/device.h 里面。 class_create 是类创建函数, class_create 是个宏定义,内容如下:

struct class *class_create (struct module *owner, const char *name)

/*
owner: 一般为 THIS_MODULE

name:类名字

返回值:指向结构体 class 的指针,也就是创建的类
*/

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

void class_destroy(struct class *cls);

/*
cls:要删除的类
*/

3.创建设备

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

struct device *device_create(struct class *class,
								struct device *parent,
								dev_t devt,
								void *drvdata,
								const char *fmt, ...)

        device_create 是个可变参数函数,参数 class 就是设备要创建哪个类下面;参数 parent 是父设备,一般为 NULL,也就是没有父设备;参数 devt 是设备号;参数 drvdata 是设备可能会使用的一些数据,一般为 NULL;参数 fmt 是设备名字,如果设置 fmt=xxx 的话,就会生成/dev/xxx 这个设备文件。返回值就是创建好的设备。

        同样的,卸载驱动的时候需要删除掉创建的设备,设备删除函数为 device_destroy,函数原型如下:

void device_destroy(struct class *class, dev_t devt)

        参数 class 是要删除的设备所处的类,参数 devt 是要删除的设备号。

4.参考示例

        在驱动入口函数里面创建类和设备,在驱动出口函数里面删除类和设备,参考示例如下:

struct class *class; /* 类 */
struct device *device; /* 设备 */
dev_t devid; /* 设备号 */

/* 驱动入口函数 */
static int __init led_init(void)
{
	/* 创建类 */
	class = class_create(THIS_MODULE, "xxx");
	/* 创建设备 */
	device = device_create(class, NULL, devid, NULL, "xxx");
	return 0;
}

/* 驱动出口函数 */
static void __exit led_exit(void)
{
	/* 删除设备 */
	device_destroy(newchrled.class, newchrled.devid);
	/* 删除类 */
	class_destroy(newchrled.class);
}

module_init(led_init);
module_exit(led_exit);

六.设置文件私有数据

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

dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */

        对于一个设备的所有属性信息我们最好将其做成一个结构体。编写驱动 open 函数的时候将设备结构体作为私有数据添加到设备文件中,如下所示:

/* 设备结构体 */
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;
}

你可能感兴趣的:(Linux字符设备驱动开发)