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、添加出口(和入口里面的函数执行相反的操作)