基本构成
驱动是用户开发的基础,在linux系统上进行软件设计,几乎都是调用驱动的函数对低层进行操作,实现相应的功能。这就表明驱动开发是给上层开发者提供接口的一种开发,其重要性不言而喻。驱动也分类别,字符设备驱动(点灯、I2C、SPI、音频)、块设备驱动(这里的块主要指的是存储块,所以主要是:EMMC、NAND、SD、U盘)和网络设备驱动(USB、wifi),这三种类别也有交叉,比如usb使用某些功能的时候属于字符驱动,使用网络功能时又是网络驱动。驱动开发必须遵循一定的规则,比如一些初始、加载、使用的函数名是固定的,这是为了给linux上层的无平台差异性进行的限制(自己的理解,大概就是规范化的意思)。
编写驱动时,open、close、write 和 read 等这些函数是必须要实现的,而这里个函数的定义是在linux内核源码的include/linux/fs.h文件中进行定义的,所以编写驱动必须用到linux源码才能进行编译工作,主要用到的是linclude目录下的头文件(根目录下的、arch/arm下的、arch/arm/include/generated/下的)。
驱动的加载和卸载函数也是固定的,分别是module_init(xxx_init);module_exit(xxx_exit);
参数为加载函数。
驱动编译完成以后扩展名为.ko,驱动的加载命令有insmod(简单,依赖需要自己加载)和 modprobe(严谨,自动加载依赖),modprobe 命令默认会去/lib/modules/<kernel-version>目录中查找模块。(有的开发板默认没有此文件夹,需要自己创建,执行modprobe命令报错时,需要执行)
- 编写驱动中不能使用printf(运行于用户态),而是使用printk(运行于内核),printk具有不同的优先级
- 编写驱动的makefile,驱动的Makefile会调用linux源码中的makefile,详情见正点P1046
Linux 应用程序对驱动程序的调用
Linux 内核驱动操作函数声明集合所在的位置:include/linux/fs.h中的file_operations 结构体
驱动加载流程
- 使用
modprobe
进行驱动加载 - 此时提示无法打开“modules.dep”
- 针对上一步骤执行
depmod
生成文件 lsmod
查看当前系统已经加载的模块
驱动流程
总体流程如上图所示
一个驱动主要由三个函数构成:
- int __init xxx_init**(void)** 驱动入口函数,也就是上面的初始函数(也就是module_init所传入的函数)
- void __exit xxx_exit**(**void)驱动出口函数,也就是上面的驱动卸载(也就是module_exit所传入的函数)
- register_chrdev()
驱动模块的加载与卸载
驱动的运行方式有两种,一种是直接写入Linux内核,另一种是以模块的形式,通过insmod
命令加载驱动直接写入内核,如果修改的话需要重新编译内核,这很麻烦,所以我们写驱动一般采用第二种方式。
驱动加载所需要的函数如下:
module_init(xxx_init) | 注册模块 |
---|---|
module_exit(xxx_exit) | 卸载模块 |
上述函数需要传入的即为驱动的初始化函数
设备的注册与注销
在初始化函数中,我们需要进行设备的注册,实现注册需要调用函数register_chrdev()
(还有其他函数,这里用这个命令是因为作为新手简单),函数的第一个参数是设备号,“cat /proc/devices”可以查看当前已经被使用掉的设备号,设备号不能重复。
设备号申请命令:int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
设备的注销是通过函数unregister_chrdev
进行注销
设备操作函数的实现
当用户编写应用程序时调用的设备函数是固定的(例如open、read、write、release),这些函数在file_operations 结构体中被声明,
在编写设备驱动时,需要对结构体进行实例化(例如:static struct file_operations test_fops
,test_fops就是实例化)
注册函数: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 下每个设备都有一个设备号,使用命令
cat /proc/devices
查看已经使用的设备号 - name:设备名字
- fops:结构体 file_operations 类型指针,指向设备的操作函数集合变量
具体的例子如下:
static struct file_operations test_fops = {
.owner = THIS_MODULE, //设备名称
.open = chrtest_open,//具体实现open操作的函数
.read = chrtest_read,
.write = chrtest_write,
.release = chrtest_release,
};
static int chrtest_open(struct inode *inode, struct file *filp)
{
return 0;
}
添加LICENSE信息
上述操作我们基本把驱动程序的框架搭建完成,现在就差一步,把LICENSE信息和作者信息添加到驱动中,其中 LICENSE 是必须添加的,否
则的话编译的时候会报错。
具体函数如下:
MODULE_LICENSE("GPL") //添加模块 LICENSE 信息
MODULE_AUTHOR("liming") //添加模块作者信息
驱动安装与测试
驱动安装
函数
驱动通过编译将产生.ko文件,即驱动文件,然后我们把此文件放入文件夹/lib/modules/4.1.15 (4.1.15此处应修改为自己的内核版本号)
然后通过函数insmod或modprobe加载驱动,第一次加载驱动可能会报错,提示无法打开“modules.dep”,此时我们需要输入命令depmod
即可,然后再输入加载驱动函数。
接下来我们通过命令lsmod
查看当前系统中存在的模块,然后再输入cat /proc/devices
查看有没有我们的设备(我们的设备名为register_chrdev()函数中的第二个参数)
通过上述步骤确保我们驱动加载完成,接下来需要进行创建设备节点文件,驱动加载成功需要在/dev 目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作(此步骤可以在驱动程序中实现),
我们输入命令mknod /dev/chrdevbase c 200 0
进行创建节点文件。
- chrdevbase :驱动名称
- c:表示这是个字符设备
- 200:设备的主设备号
- 0:设备的次设备号
有两种命令可以加载驱动模块:insmod和 modprobe,
函数 | 说明 |
---|---|
insmod [驱动] | 加载函数,不能解决依赖 |
modprobe | 加载函数,自动加载依赖模块 |
rmmod | 驱动模块卸载,不删除依赖 |
modprobe | 驱动模块卸载,也删除依赖 |
驱动测试
驱动的测试一般通过编写linux应用程序,然后调用此驱动进行测试,在函数编写过程中,会使用open、write、close等函数,这些函数的详细用法可以通过man [1] [命令]进行查看(1代表在驱动函数中查找次命令,此处也有234,不过查询的域不同,具体操作请百度)
Linux设备号
前面说到驱动需要有一个设备号,Linux 提供了一个名为 dev_t 的数据类型表示设备号,dev_t 定义在文件 include/linux/types.h 里面。
dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型,其中高 12 位为主设备号,低 20 位为次设备号。
设备号的申请
- 静态分配,上面说的方式就是静态分布
- 动态分配设备号:调用函数
alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
进行分配
- dev:申请到的设备号
- baseminor:次设备号的起始地址,目前还没碰到次设备号所以一般为0
- count:申请的设备号的数量
- name:设备名字
- from:要释放的设备号
- count:从第一个参数开始,要释放的数量,一般为1
对应的释放设备号:void unregister_chrdev_region(dev_t from, unsigned count)
,参数解释如上。