天天看点

Linux驱动程序开发 - 字符设备驱动

Linux驱动程序开发-字符设备驱动程序

绪论:

Linux下的大部分驱动程序都是字符设备驱动程序,字符设备驱动程序:顺序存取设备数据的内核代码。字符设备驱动程序能从打印机、鼠标、看门狗、磁带、内存、实时时钟等几类设备获取原始数据,但它不能管理硬盘、软盘和光盘等随机访问的设备。

主设备号和次设备号:

应用程序通过设备节点访问驱动程序,其根本是通过设备号(主设备号+次设备号)来关联设备驱动程序。在内核当中,dev_t类型(在/include/linux/types.h中定义)用来保存设备编号——包括主设备号和次设备号。dev_t用unsigned int类型来定义,高12表示主设备号,低20位表示次设备号。相关操作函数定义如下:

#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 ))  

分配和释放设备编号:

在建立一个字符设备之前,驱动程序首先要做的工作就是获得一个或者多个设备编号。关于编号的分配和释放函数如下:

int alloc_chrdev_region (dev_t *dev , unsigned int firstminor , unsigned int count , const char *name );

int register_chrdev_region (dev_t first , unsigned int count , const char *name );

void unregister_chrdev_region (dev_t first , unsigned int count );

alloc_chrdev_region函数成功,出参dev_t会带出设备编号。firstminor该参数为其实次设备号,count:表示次设备号个数。动态分配设备号。

register_chrdev_region函数是通过已知first向系统申请设备号。成功返回值为0,错误的情况下,返回一个负值错误码。静态分配设备号。

unregister_chrdev_region函数用来释放设备号。

重要的数据结构:

在/include/linux/fs.h文件中定义了file_operations结构体,该结构体里面的函数对应于应用程序里面的函数,该结构体定义如下:

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 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 );

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

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 *, loff_t , loff_t , 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 );

unsigned long (*get_unmapped_area )( struct file *, unsigned long , unsigned long , unsigned long , 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 **);

long (*fallocate )( struct file *file , int mode , loff_t offset ,

loff_t len );

};

在文件/include/linux/cdev.h文件中定义了cdev结构体,用该结构体来表示字符设备。在内核调用设备之前,必须先分配并且注册这个结构体。结构体定义如下:

struct cdev {

struct kobject kobj ;

struct module *owner ;

const struct file_operations *ops ;

struct list_head list ;

dev_t dev ;

unsigned int count ;

};

机构体里面的const struct file_operations *ops;和上面的file_operations结构体对应,设置cdev结构体的时候,就用我们构建的file_operations填充cdev结构体。字符设备的注册操作函数如下:

void cdev_init ( struct cdev *, const struct file_operations *);

struct cdev *cdev_alloc ( void );

void cdev_put ( struct cdev *p );

int cdev_add ( struct cdev *, dev_t , unsigned );

void cdev_del ( struct cdev *);

cdev_init:用于初始化一个静态分配的cdev对象。该函数自动初始化ops数据。cdev_init的功能和cdev_alloc函数的功能基本相同,唯一区别的是cdev_init初始化一个已经创建的cdev对象。

cdev_add:向内核系统中添加一个新的字符设备cdev。

cdev_del:从内核系统中移除cdev字符设备。如果字符设备是cdev_alloc动态分配的,则会释放分配的内存。

cdev_put:减少模块的引用计数,很少有驱动程序会调用该函数。

具体的字符设备驱动关键代码段:

比如内核下面Scx200_gpio.c (drivers\char)中的驱动程序的init函数如下:

static int __init scx200_gpio_init ( void )

{

int rc ;

dev_t devid ;

if (major ) {

devid = MKDEV (major , 0) ;

rc = register_chrdev_region (devid , MAX_PINS , "scx200_gpio" );

} else {

rc = alloc_chrdev_region (&devid , 0, MAX_PINS , "scx200_gpio" );

major = MAJOR (devid );

}

if (rc < 0) {

dev_err (&pdev ->dev , "SCx200 chrdev_region err: %d\n" , rc );

return rc ;

}

cdev_init (&scx200_gpio_cdev , &scx200_gpio_fileops );

cdev_add (&scx200_gpio_cdev , devid , MAX_PINS );

return rc ;

}

先申请设备号,然后初始化cdev,然在注册cdev。如果我们需要自动增加设备节点,则需要增加class_creat(创建类)函数,class_device_creat(在上面创建的类下面创建设备)函数。在exit函数里面做如下所示:

static void __exit scx200_gpio_cleanup ( void )

{

cdev_del (&scx200_gpio_cdev );

unregister_chrdev_region (MKDEV (major , 0) , MAX_PINS );

}

在出够函数里面一般做和init函数里面相反的工作。

总结上面的字符设备驱动程序,一般字符驱动设备程序的大概步骤包括如下: 1、申请file_operations结构体; 2、申请设备编号 3、初始化并且注册cdev机构体 4、动态创建设备节点 5、添加入口(如果里面包括2、3、4) 6、添加出口(和入口里面的函数执行相反的操作)

继续阅读