摘要:如何安装使用字符设备驱动,总结了字符设备驱动编程模型,初始化,注册等,最后分析了file_operations。
一、如何安装使用字符设备驱动
参见内核模块,比如有一个驱动叫led.ko
# insmodled.ko
创建设备文件节点,我们应用程序看驱动也是当做文件一样,这个设备文件节点就是这个作用。一般有两种方法,一种是相当于动态地,一种是静态的,我们在make menuconfig的时候选中的模块编译进去,其实就是把驱动编译进内核了,这种方法后面分析完led驱动再总结。这里的方法如下:
#mknod /dev/驱动名 c 主设备号 次设备号
然后我们写应用程序假如叫:led_app.c
编译的时候如果你的开发板的lib库里面没有库文件,那么就需要静态编译,在编译的时候加上-static选项,例如:
#arm-linux-gcc –static led_app.c –o led_app
然后我们就可以运行应用程序去操作设备了。
二、字符设备驱动编程模型
2.1字符设备描述结构
我们一般操作一个对象,都会有一个结构体,这个结构体里面存放这个对象的信息,或者说要素?每一个要素就是一个成员,这是C++的思想,字符设备当然也有,该描述结构如下:
structcdev
{
structkobject kobj;
structmodule *owner;
conststruct file_operations *ops;
structlist_head list;
dev_tdev;
unsignedint count;
};
其中kobject实现了基本的面向对象管理机制,是构成Linux2.6设备模型的核心结构。它与sysfs文件系统紧密相连,在内核中注册的每个kobject对象对应sysfs文件系统中的一个目录。类似于C++中的基类,Kobject常被嵌入于其他类型(即:容器)中。如drivers。这些容器通过kobject连接起来,形成了一个树状结构。
struct module *owner;这里是一个指定初始化,模块所有者。
const struct file_operations *ops;我们知道kobj是将drivers和内核其他部分连接起来,那么我们的drivers是怎么和具体设备连接起来的呢?那就是这个file_operations,我们需要对设备进行的打开,读写,关闭等等操作,都是通过这个来完成,在这里面定义,这个结构体很重要。
struct list_head list;内核链表,这里是因为我们一个字符驱动程序可以驱动多个设备,比如串口设备就可以驱动串口1和串口2,那么将这些此设备通过内核链表连接起来便于我们去管理和操作。
dev_t dev;设备号,这里用来存放我们的设备号,高12位为主设备号,低20位为次设备号。
unsigned int count;设备计数。
2.2主次设备号
我们在虚拟机下:
#ls –l /dev/
可以看到输出很多设备有关的信息:
上一个方框部分以c开头的都是字符设备,下一个方框内以b开头的都是块设备,可以看到一个设备有两个号,一个是主设备号,一个次设备号,我们看到下面块设备的主设备号都是1,因为都是ram,但是此设备号就有很多了,说明我们一个驱动是可以驱动同一类型的多个设备。
字符设备文件操作通过主设备号与一个类型的字符驱动建立关系,而次设备号用来区分同一驱动类型下的多个设备。比如要驱动串口,我们的文件操作和串口驱动建立连接,然后通过次设备号决定是操作串口1,还是串口2.
关于主次设备号操作,有如下几种方法:
将主设备号和次设备号转换成dev_t类型可以通过
MKDEV(int major, int minor);
如果要从dev_t类型中取得主设备号可用如下的方法:
MAJOR(dev_t dev);
如果要从dev_t类型中取得次设备号可用如下的方法:
MINOR(dev_t dev);
从inode中获得主设备号与次设备号
unsignedint imajor(struct inode *inode);
unsignedint iminor(struct inode *inode);
2.3设备号分配
静态分配方式:register_chrdev_region,自己制定一个主设备号,如果内核中已经使用了这个主设备号,那么就会失败。
动态分配方式:alloc_chrdev_region,由内核自己去分配一各可用的主设备号,这样就不会导致分配到已经使用了的设备号。
释放设备号:unregister_chrdev_region在驱动退出时候,应该释放设备号,毕竟资源有限。
2.4file_operations
这其实是一个函数指针的集合,定义的是我们能在设备上进行的操作,原型如下:
struct file_operations {
structmodule *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(*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t(*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
int(*readdir) (struct file *, void *, filldir_t);
unsignedint (*poll) (struct file *, struct poll_table_struct *);
int(*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
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(*open) (struct inode *, struct file *);
int(*flush) (struct file *, fl_owner_t id);
int(*release) (struct inode *, struct file *);
int(*fsync) (struct file *, struct dentry *, 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);
unsignedlong (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsignedlong, 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 **);
};
每个函数指针都是一种对设备的具体操作,我们对设备需要进行何种操作,就把对应的操作函数指针加进去,不需要的就给NULL。
三、字符设备驱动初始化
3.1分配cdev
既然前面已经介绍了字符设备描述符cdev,那么如何进行初始化,首先要做的就是对cdev进行一个分配。两种分配方法,静态和动态。
静态分配:
structcdev mdev;
动态分配:
structcdev *pdev = cdev_alloc();
3.2初始化cdev
使用cdev_init来初始化:
cdev_init(struct cdev *cdev , const struct file_operations*fops);
第一个参数是待初始化的cdev,第二个是针对这个cdev实现的具体操作,cedev_init将两者“绑定”在一起,初始化之后就可以使用这个ops来进行cdev的操作了。
3.3设备注册和注销
初始化之后,就需要将cdev注册到内核中去,使用cdev_add来注册:
cdev_add(struct cdev *p , dev_t dev , unsigned count);
第一个参数是待添加到内核的cdev,第二个参数是设备号,第三个是该类设备有多少个。
注销使用cdev_del。
3.4硬件初始化
这个主要是根据datasheet做一些设置,比如你要操作nand,首先是挂在哪一个bank上,控制端口的地址,然后时序等等做一个初始化,把硬件的状态带好了,才能去驱动。
四、分析file_operations
4.1 open
int(*open)(struct inode * , struct file *);
打开设备,表明次设备号。
4.2 release
int(*release)(struct inode * , struct file *);
也叫colse,关闭设备。
4.3 lseek
loff_t(*llseek)(struct file * , loff_t , int);
重定位读写指针,主要是对应在一个文件里我们需要从什么位置开始读写或者别的操作。
4.4 read
ssize_t (*read) (struct file *filp,char __user buff,size_tcount,loff_t *offp)
从设备读取数据,属于硬件访问类操作。
filp:与字符设备文件关联的file结构指针, 由内核创建。
buff : 从设备读取到的数据,需要保存到的位置。由read系统调用提供该参数。
count: 请求传输的数据量,由read系统调用提供该参数。
offp: 文件的读写位置,由内核从file结构中取出后,传递进来。
4.5write
ssize_t (*wtite) (struct file *filp,const char __userbuff,size_t count,loff_t *offp)
往设备写入数据。
filp:与字符设备文件关联的file结构指针, 由内核创建。
buff :往设备里面写的数据,一开始保存在buff里。
count: 请求传输的数据量,由write系统调用提供该参数。
offp: 文件的读写位置,由内核从file结构中取出后,传递进来。
4.6 struct file
因为我们的设备也是被当做文件来操作,在打开设备的时候相当于打开一个文件,在linux中,每打开一个file,内核就会关联一个struct file,关闭的时候释放。
loff_t*fpos是文件读写指针。
structfile_operations *f_ops是对应的该文件的操作。
4.7struct inode
每个文件都会关联一个inode,这个inode主要记录文件的物理信息,例如字节大小,读写修改的权限,时间戳,软硬件的链接等等。文件从创建开始就有这个inode,打开的时候才会关联struct file。inode里面重要的成员是设备号:dev_t i_rdev。
4.8buff操作
我们在read,write的时候,都会用到这个buff,但是它是在用户空间的指针,内核不能直接使用,所以要使用一系列函数。
int copy_from_user(void *to, const void__user *from, int n)
从用户空间拷贝,对应的是写write
int copy_to_user(void __user *to, const void*from, int n)
拷贝到用户空间,对应的是读read
这篇帖子就总结到这里吧,如有不正确的地方还请指出,大家共同进步。