目录
一、基础简介
1.1、Linux设备驱动分类
1.2、字符设备驱动概念
二、驱动基本构成
2.1、驱动模块的加载和卸载
2.2、添加LICENNSE以及其他信息
三、字符设备驱动开发步骤
3.1、分配主次设备号
3.1.1 主次设备号
3.1.2静态注册设备号
3.1.3动态注册设备号
3.1.4释放设备号
3.2文件操作函数fops设置
3.2.1file_operations结构体
3.2.2数据交互
3.2.3ioctl实现
3.3字符设备结构的分配和初始化
3.3.1分配cdev 结构体
3.3.2初始化cdev结构体
3.3.3注册字符设备
3.3.4注销字符设备
3.4创建设备节点
3.4.1创建和删除类
3.4.2创建和删除设备文件
四、代码演示
4.1驱动部分演示
4.1.1驱动代码
4.1.2驱动编译
4.1.3安装驱动
4.2应用空间程序测试
4.2.1测试代码
4.2.2编译测试
五、 总结
一、基础简介
1.1、Linux设备驱动分类
有一句话,相信大家一定不会感觉到陌生---“Linux下一切皆是文件”!所以我们可以这样理解,Linux内核会将设备抽象成文件,然后我们通过文件I/O就可以对设备进行操作。而Linux内核又按照访问特性将其分成三类:字符设备、块设备、网络设备。
- 字符设备:在数据读取操作时,以字节为单位进行的,比如串口、LED、蜂鸣器等等。
- 块设备:在数据读取操作时,以块或扇区为单位进行的,比如硬盘、U盘、eMMC等等。
- 网络设备:通过数据包传输的设备,比如以太网卡、无线网卡等。这类设备在/dev/下没有对应的设备节点,如果想要查看,需要使用 ifconfig 。
1.2、字符设备驱动概念
接下来,我们将从最简单的字符设备入手,开始学习驱动的概念。我们需要先了解一下Linux下的应用程序是如何调用驱动程序的,其关系如图所示:
我们的驱动程序成功加载后,会在 /dev 目录下生成一个对应的文件,应用程序通过这个名为 /dev/xxx 的文件进行相应的操作即可实现对硬件的操作。比如现在有个叫 /dev/led 这个文件,我们在应用程序中调用了open()函数,它会通过系统调用从用户空间切换到内核空间,在执行驱动程序中对应的open()函数,从而实现了对硬件的操作。
我们会发现,每一个系统调用都会有一个与之对应的驱动函数。说到这里,就必须要提到file_operations结构体,此结构体就是Linux内核操作函数的集合,会在 3.3.1 进行详细整理。
二、驱动基本构成
在正式写驱动代码前,我们需要知道驱动程序必不可少的几部分,这也是与应用程序不同的地方。
2.1、驱动模块的加载和卸载
Linux驱动有两种运行方式:1、将驱动编译进Linux内核中,这样当Linux内核启动的时候就会自动运行驱动程序;2、将驱动编译成模块(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 函数就会被调用。
2.2、添加LICENNSE以及其他信息
Linux 是以 GNU 通用公共版权( GPL )的版本 2 作为许可的,所以LICENSE是必须添加的!模块的作者等其他信息是可选择性添加的。
MODULE_LICENSE() //添加模块 LICENSE 信息
MODULE_AUTHOR() //添加模块作者信息
三、字符设备驱动开发步骤
当我们了解了字符设备驱动的基础知识,我们就要开始学习字符设备设备的开发步骤了。与应用层开发不同,驱动开发的框架是固定的,所以学习框架是十分重要的!
3.1、分配主次设备号
3.1.1 主次设备号
Linux中,每个设备都有一个设备号。设备号由两部分组成,分别是主设备号和次设备号。主设备号用于标识某一个具体的驱动,次设备号用于标识使用该驱动的某一个设备。在编写Linux内核驱动时,每个设备都要有一个独一无二的设备号(包括主、次设备号),它通常使用 dev_t 类型(在<linux/types.h>中)来定义。
typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
我们可以看到,dev_t是一个32位的数据,其中高12位为主设备号(0~4095),低20位为次设备号。在驱动编程中,我们不应该管哪些位是主设备号,哪些位是次设备号,而应该统一使用 <linux/kdev_t.h>中的一套宏设置/获取一个dev_t 的主、次编号:
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
其中,宏MAJOR用于从dev_t中获取主设备号;宏MINOR用于从dev_t中获取次设备号;宏MKDEV用于将给定的主设备号和次设备号的值组合成dev_t类型的设备号。
3.1.2静态注册设备号
我们可以通过 cat /proc/devices 来查看所有已被系统使用的设备号,我们可以选择一个未被使用的设备号来进行静态注册,其中静态注册设备号的API函数如下:
int register_chrdev_region(dev_t first, unsigned int count, char *name);
//first:要分配的起始设备号,其为 dev_t 类型,可以由 MKDEV() 宏来生成 。first的次编号部分通常是从0开始,但不是强制的
//count:请求分配的设备号的总数
//name:设备名称
//成功返回值是0。出错的情况下,返回一个负的错误码
3.1.3动态注册设备号
我们可以使用动态注册一个设备号,在根据宏来获取它的主次设备号,其中动态注册设备号的API函数如下:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
//dev:这是一个输出参数,用来保存申请到的 dev_t 类型设备号。这样我们可以使用 MAJOR() 宏从它里面提取出相应设备的主设备号
//baseminor:传入给内核的次设备号起始值,通常次设备号从0开始编号
//count:要申请的设备号数量
//name:设备名称
//成功返回值是0。出错的情况下,返回一个负的错误码
动态分配的缺点是我们无法提前创建设备节点,因为分配给我们的主设备号会发生变化,只能通过查看 /proc/devices
文件才能知道它的值,然后再创建设备节点。
3.1.4释放设备号
通常我们在驱动安装时会申请主、次设备号,那很显然我们应该在驱动卸载时应该释放主次设备号。设备号释放函数如下:
void unregister_chrdev_region(dev_t from, unsigned count)
//from:要释放的设备号
//count:表示从 from 开始,要释放的设备号数量
3.2文件操作函数fops设置
3.2.1file_operations结构体
我们在上文提到了Linux内核操作函数的集合---file_operations结构体,接下来我们详细整理一下。
#include <linux/fs.h>
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 f