[ARM笔记]字符设备驱动

ARM 95浏览

来自:《嵌入式Linux初级实验s3c2410》

参考文献:李俊编著的《嵌入式Linux设备驱动开发详解》(人民邮电出版社),《Linux设备驱动程序开发详解》(人民邮电出版社)等。

 

1. 字符设备驱动程序的结构

字符设备驱动是嵌入式Linux最基本,也是最常用的驱动程序。下表1.1 应用程序、驱动程序和硬件设备之间的层次结构显示了应用程序,驱动程序和硬件设备之间的层次结构。

应用程序

标准的系统调用:read(),write(),ioctl(),open(),close()等

file_operations 文件操作结构体

驱动程序中对应硬件设备的接口函数:

xxx_read(),xxx_write(),xxx_ioctl(),xxx_open(),xxx_close()等

硬件设备

表1.1 应用程序、驱动程序和硬件设备之间的层次结构

 

1.1 常用的头文件

驱动程序中需要调用内核提供的很多接口函数,这些接口函数都会在相应的内核头文件中声明,因此,在驱动程序中,必须添加这些头文件的支持。下面来看看编写驱动时需要经常用到的内核头文件。

#include <linux/kernel.h>   //内核相关的头文件

#include <linux/module.h>  //所有模块都需要的头文件

#include <linux/device.h>

#include <linux/types.h>    //设备类型定义

#include <linux/version.h>  //版本相关

#include <linux/ioctl.h>    //IO操作

#include <linux/errno.h>   //错误处理

#include <linux/delay.h>   //延时处理相关的头文件

#include <linux/init.h>    //设备驱动初始化相关的头文件

#include <linux/cdev.h>   //字符设备相关的头文件

#include <linux/fs.h>     //文件系统相关的头文件

#include <linux/mm.h>   //内存管理相关的头文件

 

//调用get_user,put_user,copy_to_user,copy_from_user等函数时引用的头文件

#include <asm/uaccess.h>

 

// 处理器GPIO相关的头文件

#include <asm/io.h>

#include <asm/arch/regs-gpio.h>

 

// 中断相关的头文件

#include <linux/sched.h>

#include <linux/irq.h>

#include <linux/interrupt.h>

 

1.2 主次设备号

字符设备通过文件系统中的名字来存取。那些名字称为文件系统的设备文件, 或者文件系统的结点,它们一般位于 /dev 目录。

进入宿主机的/dev目录,然后输入ls -l命令可以查看宿主机的所有设备文件,下图是笔者截取的输入ls -l命令后的部分显示结果:

crw-rw----         1 root   root       7,     135    2009-12-11 21:39 vcsa7

crw-rw----         1 root   root       7,     136    2009-12-11 21:38 vcsa8

crw-------    1 root   root     253,       0    2009-12-11 21:39 vmci

crw-rw-rw-   1 root   root     10,      63    2009-12-11 21:39 vsock

prw-r-----  1 syslog  adm              0    2009-12-12 08:01 xconsole

crw-rw-rw-  1 root   root      1,       5    2009-12-11 21:38 zero

从这些显示结果可以看出,设备文件的类型由第一个字母标识,我们知道,"c"代表字符设备,"b"代表块设备。另外,在最后修改日期之前你会看到两个数,这两个数字就是设备的主次设备号。如上图所示的几个设备。它们的主设备号分别是 7、7、253、10和1, 而次设备号分别是135、136、0、63和5。其中,主设备号7对应两个次设备号:135和136,说明vcsa7和vcsa8是一类设备,使用同一个驱动程序。

主设备号标识设备相连的驱动。示例,/dev/null和 /dev/zero都由驱动1来管理, 而虚拟控制台和串口终端都由驱动4管理;相同类型的的设备可以共用同一个设备驱动,次设备号就是被内核用来决定引用这些设备中的哪个设备。

 

2. cdev结构体

在Linux 2.6 内核中使用cdev结构体来描述字符设备,cdev结构体的定义如下:

struct cdev {

       struct kobject kobj;              //内嵌的kojbect对象

       struct module *owner;          //所属模块

       const struct file_operations *ops;      //文件操作结构

       struct list_head list;

       dev_t dev;   //设备号

       unsigned int count;

};

cdev结构体的dev_t类型成员定义了设备号,主次设备号都包括。对于2.6内核,dev_t 是 32位的量,12位用作主设备号,20位用作次设备号。利用在<linux/kdev_t.h>中的一套宏定义,可以获得一个 dev_t 的主次设备号:

MAJOR(dev_t dev)

MINOR(dev_t dev)

相反, 如果你已经有了主次设备号, 可以使用下面的宏将其转换为一个dev_t:

MKDEV(int major, int minor)

cdev结构体的成员定义了字符设备驱动提供给虚拟文件系统的接口函数。关于file_operations笔者在后面将详细介绍。

下面一组函数用来对cdev结构体进行操作:

void cdev_init(struct cdev *, const struct file_operations *); //初始化cdev

struct cdev *cdev_alloc(void); //动态申请一个cdev内存

void cdev_put(struct cdev *p);

int cdev_add(struct cdev *, dev_t, unsigned);//注册cdev设备

void cdev_del(struct cdev *);//注销cdev设备

下面逐一介绍一下上述操作。

(1)初始化cdev成员

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

{

memset(cdev,0,sizeof *cdev); //对字符设备进行清零操作

INIT_LIST_HEAD(&cdev->list); //该宏定义使得list_head的next和prev都指向自身

dev->kobj.ktype=&ktype_cdev_default;            

kobject_init(&cdev->kobj); //进行kobject的初始化

cdev->ops=fops;  //将传入的文件操作结构体指针赋值给cdev的ops

}

(2)动态申请一个cdev内存

cdev_alloc()函数用于动态申请一个cdev内存,其源代码如下:

struct cdev *cdev_alloc(void)

{

// kmalloc 的第1个参数是要分配的块的大小,第2个参数分配标志。

struct cdev *p=kmalloc(sizeof(struct cdev),GFP_KERNEL);

if(p){

    memset(p,0,sizeof(struct cdev));

    p->kobj.ktype=&ktype_cdev_dynamic;

    INIT_LIST_HEAD(&p->list);

    kobject_init(&p->kobj);

}

return p;

}

(3)注册cdev设备

cdev_add()函数向系统添加一个cdev,完成字符设备的注册,对cdev_add()函数的调用通常发生在字符设备驱动模块加载函数中。下面是cdev_add()函数的定义:

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

{

p->dev = dev;

p->count = count;

//添加一个对应的cdev对象.

return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);

}

(4)注销cdev设备

cdev_del()函数向系统删除一个cdev,完成字符设备的注销,对cdev_del()函数的调用通常发生在字符设备驱动模块卸载函数中。下面是cdev_del()函数的定义:

void cdev_del(struct cdev *p)

{

cdev_unmap(p->dev, p->count);  //删除一个cdev

kobject_put(&p->kobj);  //减少计数量

}

 

3. 分配和释放设备号

在注册设备时应该先调用:int register_chrdev_region(dev_t from,unsigned count,const char *name)函数或者int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count,const char *name)函数为其分配设备号,在第一个函数中,from是你要分配的起始设备号,count是你请求的连续设备号的总数,name是应当连接到这个设备号范围的设备名称;在第二个函数中,
dev是函数成功执行后返回的一个参数, 它是分配范围的第一个数,baseminor 应当是请求的第一个要用的次设备号,通常是0,count和name参数不用说你也明白了。这两个函数的区别是:register_chrdev_region()用于已知设备号的情况;而int alloc_chrdev_region()用于动态申请系统中未被占用的设备号的情况,其优点在于不会造成设备号重复的冲突。

在注销设备之后,应该调用void unregister_chrdev_region(dev_t from,unsigned count)函数释放原先申请的设备号。

他们之间的顺序关系如下:

register_chrdev_region()->cdev_add()     //此过程在加载模块中

cdev_del()->unregister_chrdev_region()      //此过程在卸载模块中

 

4. File_operation结构体

由于用户进程是通过设备文件同硬件打交道,对设备文件的操作方式不外乎就是一些系统调用,如open,read,write,close等,但是如何把这些系统调用和设备驱动程序关联起来呢?这需要了解一个非常关键的数据结构file_operations,它定义在include/linux/fs.h中。

file_operations的数据结构介绍如下:

struct module *owner

第一个 file_operations 成员是一个指向拥有这个结构的模块的指针,一般被简单初始化为THIS_MODULE,它是定义在<linux/module.h>中的一个宏。内核使用这个字段以避免在模块的操作正在被使用时卸载该模块。

loff_t (*llseek) (struct file *, loff_t, int);

llseek 用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值。参数loff_t是一个“长偏移量”,即使在32位平台上也至少占用64位的数据宽度。出错时返回一个负的返回值。

ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);

用来从设备中读取数据。该函数指针被赋为NULL值时,导致read系统调用出错,并返回-EINVAL("Invalid argument",非法参数)。函数返回非负值表示成功读取的字节数(返回值为"signed size"数据类型,通常就是目标平台上的固有整数类型)。

ssize_t (*aio_read)(struct kiocb *, char __user *, size_t, loff_t *);

aio_read初始化一个异步读操作——即在函数返回之前可能不会完成的读取操作。如果该方法为NULL,所有的操作将通过read(同步)处理。

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

向设备发送数据.如果设备为NULL返回-EINVAL,否则返回成功发送的字节数。

ssize_t (*aio_write)(struct kiocb *, const char __user *, size_t, loff_t *);

aio_write初始化设备上的一个异步写操作。

int (*readdir) (struct file *, void *, filldir_t);

readdir用来读取目录, 仅对文件系统有用,对于设备文件,这个成员应当为 NULL。

unsigned int (*poll) (struct file *, struct poll_table_struct *);

poll用作判断设备读或写是否会阻塞。

int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);

系统调用ioctl提供了一种执行设备特定命令的方法(比如格式化软盘的某个磁道,这既不是读操作也不是写操作)。另外,内核还能识别一部分ioctl命令,而不必调用fops表中的ioctl。如果设备不提供ioctl入口点,则对于任何内核未预先定义的请求,ioctl系统调用都将返回错误(-ENOTTY,“NO such ioctl for device”)。

int (*mmap) (struct file *, struct vm_area_struct *);

mmap 用来请求将设备内存映射到进程的地址空间。

int (*open) (struct inode *, struct file *);

open用来打开一个设备。尽管这始终是对设备文件执行的第一个操作,然而却并不要求驱动程序一定要声明一个相应的方法。如果这个入口为NULL,设备的打开操作永远成功,但系统不会通知该驱动。

int (*flush) (struct file *);

对flush操作的调用发生在进程关闭设备文件描述符副本的时候,它应该执行(并等待)设备上尚未完结的操作。目前,flush仅仅用于少数几个驱动程序,比如,SCSI磁带驱动程序用它来确保设备被关闭之前所有的数据被写入到磁带中。如果flush被置为NULL,内核将简单地忽略用户应用程序的请求。

int (*release) (struct inode *, struct file *);

release用来释放一个设备。与open相仿,也可以将它设置为NULL。

int (*fsync) (struct file *, struct dentry *, int);

fsync用来刷新被挂起的数据。

int (*aio_fsync)(struct kiocb *, int);

aio_fsync是一个异步 fsync。

int (*fasync) (int, struct file *, int);

fasync用来通知设备它的 FASYNC 标志的改变。

int (*lock) (struct file *, int, struct file_lock *);

lock 方法用来实现文件加锁。

ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);

ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);

ssize_t (*sendfile)(struct file *, loff_t *, size_t, read_actor_t, void *);

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)

允许模块检查传递给 fnctl(F_SETFL...) 调用的标志。

int (*dir_notify)(struct file *, unsigned long);

只对文件系统有用; 驱动不需要实现 dir_notify。

file_operations结构的每一个成员的名字都对应着一个系统调用。在设备驱动程序中,一般file_operations结构体初始化如下:

static struct file_operations mydevice_fops = {

    .owner   = THIS_MODULE,

    .open    = mydevice_open,

.release  = mydevice_release,

    .ioctl    = mydevice_ioctl,

.read    = mydevice_read,

.write   = mydevice_write,

//more……

};

应用程序通过系统调用在对设备文件进行诸如read/write操作时,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取设备驱动程序中初始化的file_operations结构体(如mydevice_fops),获取相应操作(read/write等)对应的函数指针(mydevice_read/mydevice_write),接着把控制权交给该函数。这是linux的设备驱动程序工作的基本原理。因此,编写设备驱动程序的主要工作就是编写这些文件操作的接口函数,并填充file_operations的各个域。

下面来看一个字符设备驱动程序的一般结构:

 

5. 字符设备驱动程序一般结构

//*****************头文件***********************

#include <linux/kernel.h>

#include <linux/module.h>

#include <linux/interrupt.h>

//……more

 

 

//*********** 文件操作接口函数定义 *********

static int mydevice_open(struct inode *inode,struct file *filp)

{    

//打开设备操作

}

 

static int mydevice_release(struct inode *inode,struct file *filp)

{

     //关闭设备操作

}

 

static int mydevice_ioctl(struct inode *inode,struct file *filp,unsigned int cmd,unsigned long arg)

{

//设备I/O控制

}

 

static ssize_t mydevice_read(struct file *filp,char *buffer,size_t count,loff_t *ppos)

{

    //设备读操作

}

 

static ssize_t mydevice_write(struct file *filp,char *buffer,size_t count,loff_t *ppos)

{

       //设备写操作

}

//……more

 

//*********** 文件操作结构体 *********

static struct file_operations mydevice_fops = {

    .owner   = THIS_MODULE,

      .open          = mydevice_open,

       .release  = mydevice_release,

    .ioctl    = mydevice_ioctl,

       .read    = mydevice_read,

       .write   = mydevice_write,

//……more

};

 

//**********设备驱动模块加载函数***********

static int  mydevice_init(void)

{

    //动态或静态分配设备号

    //注册设备

}

 

//**********设备驱动模块卸载函数***********

static void  mydevice_exit(void)

{

       //注销设备

//释放设备号

}

module_init(mydevice_init);

module_exit(mydevice_exit);

 

//*********** 设备驱动许可证 ***********

MODULE_LICENSE("GPL");