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))
#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);
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);
};
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;
};
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 *);
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:减少模块的引用计数,很少有驱动程序会调用该函数。
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;
}
{
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);
/* cdev_put(&scx200_gpio_cdev); */
unregister_chrdev_region(MKDEV(major, 0), MAX_PINS);
}
{
cdev_del(&scx200_gpio_cdev);
/* cdev_put(&scx200_gpio_cdev); */
unregister_chrdev_region(MKDEV(major, 0), MAX_PINS);
}
在出够函数里面一般做和init函数里面相反的工作。
总结上面的字符设备驱动程序,一般字符驱动设备程序的大概步骤包括如下:
1、申请file_operations结构体;
2、申请设备编号
3、初始化并且注册cdev机构体
4、动态创建设备节点
5、添加入口(如果里面包括2、3、4)
6、添加出口(和入口里面的函数执行相反的操作)