因为Linux字符设备驱动主要依赖于struct cdev结构,原型为:
所以我们需要对所使用到的结构成员进行配置,驱动开发所使用到的结构成员分别为:【unsigned int count;】、【dev_t dev;】、【const struct file_operations *ops;】。
开发流程如下图:
一、字符设备驱动初始化
1、分配设备cdev
Cdev变量的定义可以采用静态和动态两种方法进行分配。静态方法直接分配内存,而动态方法随机分配内存。
静态分配cdev:
定义:struct cdev mdev;
mdev即代表相应的字符设备空间地址。
动态分配cdev:
定义:struct cdev *pdev = cdev_alloc();
pdev即代表对于的字符设备的空间地址。
2、初始化设备cdev
Linux内核中,字符设备struct cdev的初始化使用cdev_init。其原型如下:
参数:
cdev:待初始化的struct cdev结构
fops:设备对应的操作函数集
由原型所要求的参数可知,需要初始化一个字符设备,必须根据申请一个struct cdev结构的空间,然后对其成员进行配置。即实现设备的操作函数集、为设备申请设备号(包括主设备号和次设备号)、指定此类型的字符设备有多少个相同设备。
3、注册设备cdev
Linux内核中字符设备的注册使用cdev_add函数来进行完成注册。其原型如下:
其相关参数定义为:
p:待添加到内核中的字符设备结构,即为struct cdev。
dev:设备号
count:该类设备的设备数量,各个设备的区别体现为从设备号。
在开发驱动时,当确定了字符设备的结构,主设备号和从设备号、设备的数量,就可以使用cdev_add函数将相应的字符设备添加到Linux内核驱动中进行注册。
4、硬件初始化
关于硬件的初始化就简单了。直接根据所需要操作的字符设备,阅读器Datasheet,然后根据Datasheet进行硬件的配置即可。
二、设备操作和驱动操作映射
从上图基本可知,在用户空间的每一种硬件设备操作函数,在内核空间通用有一个映射操作函数实现。
•int (*open) (struct inode *, struct file *):打开设备,响应open系统
•int (*release) (struct inode *, struct file *):关闭设备,响应close系统调用
•loff_t (*llseek) (struct file *, loff_t, int):重定位读写指针,响应lseek系统调用
•ssize_t (*read) (struct file *, char __user *, size_t, loff_t *):从设备读取数据,响应read系统调用
•ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *):向设备写入数据,响应write系统调用
从以上函数指针操作方法中可以提取出两个重要的参数成员,即为struct file和struct inode。
【struct file】:在Linux系统中,每一个打开的文件,在内核中都会对应的关联一个struct file结构体,它由内核在打开文件时创建,在文件关闭后释放。
其非常重要的成员有:
loff_t f_pos /*文件读写指针*/
struct file_operations *f_op /*该文件所对应的操作*/
【struct inode】:每一个存在于文件系统里面的文件都会关联一个inode 结构,该结构主要用来记录文件物理上的信息。因此, 它和代表打开文件的file结构是不同的。一个文件没有被打开时不会关联file结构,但是却会关联一个inode 结构。
在struct inode结构中,dev_t i_rdev尤其重要,表示设备号。
三、struct file_operations结构常用设备操作解析
1.open设备操作
根据原型可知,open设备函数方法是在进行开始启用操作设备时的初始化工作,与用户空间的open函数对应,通常情况下,open函数实现:
(1)表明次设备号
(2)启用设备
当然,如果在启用设备时,不需要任何的准备操作或者初始化操作,那么open函数可以为空函数,不实现。
2.release设备操作
release函数方法的作用正好和open函数方法的作用相反,它在设备关闭时用到,与用户空间的close对应。通常用来进行关闭设备的实现。如果所操作的设备在关闭时不需要其他操作,那么使其为空函数即可。
3.read设备操作
read方法主要完成两件事:
(1)访问硬件操作,从设备中读取数据。
(2)将从设备端读取到的数据返回给用户空间的应用程序read函数。
其read方法的具体原型可分析如下:
ssize_t (*read) (struct file *filp, char __user *buff, size_t count, loff_t *offp)
参数分析:
filp:与字符设备文件关联的file结构指针, 由内核创建。
buff : 从设备读取到的数据,需要保存到的位置。由read系统调用提供该参数。
count: 请求传输的数据量,由read系统调用提供该参数。
offp: 文件的读写位置,由内核从file结构中取出后,传递进来。
那么实际上在这里就存在了一个问题,从内核空间如何将数据返回给用户空间???
在Linux内核中,为从内核空间将数据返回到用户空间提供了函数方法,即为:copy_to_user。其原型如下:
之所以采用copy_to_user函数的原因是,buff参数来源于用户空间的指针,这类指针在内核空间中不能直接被内核代码直接引用,所以必须采用拷贝的方式进行数据的传递。具体的操作过程如下图表示:
4.write设备操作
write函数方法也主要完成两件事:
(1)从应用程序提供的地址中取出数据到内核空间。
(2)访问硬件设备,将数据写入到设备中。
write函数方法直接对应于用户空间的write函数。
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *)
write函数方法的参数基本上和read函数方法类似,在此不再做解释。
那么实际上在此也存在一个问题,如何将数据从用户空间取出到内核空间使用???
实际上Linux内核中也提供了相应的方法函数,将用户空间的数据拷贝到内核空间进行使用,其方法为:copy_from_user。原型如下:
四、设备驱动注销
当内核中不再需要使用一个字符设备时,我们可以在内核中将相应的设备驱动程序进行卸载。那么就需要对其字符设备进行注销。Linux内核中为我们提供了cdev_del函数来完成字符设备的注销。其原型如下: