Linux字符设备驱动基础知识
在Linux设备驱动中,字符设备驱动较为基础,字符设备即一个一个的字节,按照字节流进行读写操作的设备,读写数据具有一定的先后顺序,例如I2C、SPI、LCD等都属于字符设备。
在Linux中一切皆文件,设备驱动加载成功后会在/dev
目录下生成相应的文件,应用程序通过对这个名为、dev/xxx
(xxx为具体设备驱动的名称)的文件进行相应操作即可实现对硬件的操作。
cdev 结构体
在字符设备驱动程序的管理核心是字符设备,内核为字符设备驱动抽象出了一个具体的数据结构 struct cdev 定义如下:
\include\linux\cdev.h
/*
* 字符设备的内核抽象
*/
struct cdev {
struct kobject kobj; /* 内嵌的内核(kobject)对象 */
struct module *owner; /* 所属模块在内核的对象指针 */
const struct file_operations *ops; /* 文件操作结构体,用于实现与硬件的一系列操作 */
struct list_head list; /* 用来将已经向内核注册的所有字符设备形成链接 */
dev_t dev; /* 设备号 */
unsigned int count; /* 隶属于同一设备号的次设备号的个数 */
};
cdev结构体的dev_t成员定义设备号,为了方便管理,Linux中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,共为32位,其中高12位为主设备号,低20位为次设备号,因此Linux系统中主设备号范围为0-4095。
主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。
可使用以下宏对设备号进行操作:
\include\linux\kdev_t.h
#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)) /* 使用主设备号和次设备号生成dev_t */
操作cdev的相关函数
\include\linux\cdev.h
/*
* 用于初始化cdev的成员,并建立cdev与file_operations之间的连接
*/
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops; /* 将传入的文件操作结构体指针赋值给cdev的ops成员 */
}
/*
* 动态申请一个cdev结构体内存
*/
struct cdev *cdev_alloc(void)
{
struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
if (p) {
INIT_LIST_HEAD(&p->list);
kobject_init(&p->kobj, &ktype_cdev_dynamic);
}
return p;
}
/*
* 释放一个cdev
*/
void cdev_put(struct cdev *p)
{
if (p) {
struct module *owner = p->owner;
kobject_put(&p->kobj);
module_put(owner);
}
}
/*
* 向系统注册一个字符设备
*/
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
int error;
p->dev = dev;
p->count = count;
error = kobj_map(cdev_map, dev, count, NULL,
exact_match, exact_lock, p);
if (error)
return error;
kobject_get(p->kobj.parent);
return 0;
}
/*
* 删除一个字符设备
*/
void cdev_del(struct cdev *p)
{
cdev_unmap(p->dev, p->count);
kobject_put(&p->kobj);
}
分配和释放设备号
在调用cdev_add()函数向内核注册字符设备之前,需要先调用register_chrdev_region()或alloc_chrdev_region()函数向系统申请设备号。
\fs\char_dev.c
/*
* 对已知起始设备的设备号申请设备号
*/
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);
/*
* 释放之前申请的设备号
*/
void unregister_chrdev_region(dev_t from, unsigned count);
file_operations 结构体
file_operation是将Linux系统调用和驱动程序关联起来的关键数据结构。这个结构体包含对文件进行打开、关闭、读写、控制的一系列成员函数。读取file_operation中相应的函数指针,接着把控制权转交给函数,从而完成了Linux设备驱动程序的工作。
\include\linux\fs.h
/*
* 文件操作结构体,用于实现与硬件的一系列操作
*/
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 (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
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 (*mremap)(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 **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
};
结构体常用成员的主要含义
- struct module *owner
功能:owner并不是一个操作,它是指向拥有这个结构的模块的指针,主要作用是在设备还在被使用时阻止其被卸载,一般为 THIS_MODULE。定义在<linux/module.h>中的宏。
- loff_t (*llseek) (struct file *, loff_t, int)
功能: 用于改变文件的当前读/写位置,并将新位置返回,出错时,返回一个负值。
参数1: 指针指向进行读取目标文件的结构体;
参数2: 文件定位的目标偏移量。
参数3: 对文件定位的起始地址,这个值可以为文件开头(SEEK_SET)、当前位置(SEEK_CUR)、文件末尾(SEEK_END)。
- ssize_t (*read) (struct file *, char __user *, size_t, loff_t *)
功能: 用于从设备文件中获取数据,成功时返回读取的字节数,出错时返回一个负值。(此操作为阻塞操作)
参数1: 为进行读取信息的目标文件;
参数2: 为对应放置信息的缓冲区(用户空间内存地址);
参数3: 要读取的信息长度;
参数4: 读取位置相对于文件开头的偏移,读取信息后,指针会移动读取信息的长度值的距离;
- ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *)
功能: 用于向设备文件写入数据,成功时返回写入的字节数,若用户未实现此函数的回调函数,用户运行write()时将得到-EINVAL返回值。(此操作为阻塞操作)
参数1: 为目标文件结构体指针;
参数2: 为要写入文件的信息缓冲区;
参数3: 为要写如信息的长度;
参数4: 为当前的偏移位置,这个值通常是用来判断写文件是否越界;
- unsigned int (*poll) (struct file *, struct poll_table_struct *)
功能: 轮询函数,用于查询设备是否能够进行非阻塞读写,函数返回设备资源的可获取状态,即POLLIN,POLLOUT,POLLPRI,POLLERR,POLLNVAL等宏的位“或”结果。
参数1: 为文件对象结构指针;
参数2: 轮询表指针。
- long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long)
功能: 用于提供对设备的控制功能,与应用程序的ioctl函数对应。
- long (*compat_ioctl) (struct file *, unsigned int, unsigned long)
功能: 与unlocked_ioctl功能类似,区别在与64位系统会调用此函数,32位系统调用unlocked_ioctl。
- int (*mmap) (struct file *, struct vm_area_struct *)
功能: 用于将设备内存映射到进程空间(用户空间)。
参数1: 为文件对象结构指针;
参数2: 为进程地址空间。
- int (*open) (struct inode *, struct file *)
功能: 打开设备文件,与release文件对应。
参数1: 为文件索引节点,文件索引节点只有一个,无论用户打开多少个文件,都只对应一个inode结构;
参数2: 为文件对象结构体,只要打开一个文件,就对应一个file结构体,file结构体通常用于追踪文件在运行时的状态信息。
- int (*release) (struct inode *, struct file *)
功能: 用于释放(关闭)设备文件,常与应用程序的close函数对应。
参数1: 同open;
参数2: 同open。
- int (*fasync) (int, struct file *, int)
功能: 用于刷新待处理的数据,类似于将内存缓冲区的数据刷新至磁盘内,允许进程把所有的脏缓冲区刷新到磁盘。
- int (*aio_fsync) (struct kiocb *, int datasync):
功能: 与fasync类似,aio_fsync用于异步刷新。
字符设备驱动开发
设备驱动的加载与卸载
linux设备驱动一般有两种方式运行,一种是将驱动跟内核一起编译,此时linux将会自动加载运行;另外一种是将驱动程序编译为模块(.ko文件)后,使用驱动加载命令 insmod 将相应的模块加载即可。
在这里一般会使用两个宏
\include\linux\init.h
module_init(x)
module_exit(x)
module_init 宏用于向linux内核注册一个模块的加载函数,参数x为需要注册的具体函数。在命令行调用 insmod 命令时,x将会被调用。
module_exit 宏用于向linux内核卸载一个模块,参数为具体需要卸载的函数。在命令行调用 rmmod 命令时,x将会被调用。
相关加载卸载命令
- 加载驱动模块命令:
insmod xxx.ko
insmod命令不能解决模块的依赖关系,即:a.ko依赖于b.ko模块,必须使用 insmod 命令加载b.ko模块,然后再加载a.ko模块。
- 驱动卸载命令:
rmmod xxx.ko
modprobe -r xxx.ko
字符设备注册注销
对字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备。同样,卸载驱动时也需要注销字符设备。相应函数如下:
\include\linux\fs.h
/*
* 字符设备驱动注册
*/
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
{
return __register_chrdev(major, 0, 256, name, fops);
}
/*
* 字符设备驱动卸载
*/
static inline void unregister_chrdev(unsigned int major, const char *name)
{
__unregister_chrdev(major, 0, 256, name);
}
当前设备被用掉的设备号查看命令 cat /proc/devices 。
创建设备节点文件命令
驱动加载成功之后需要在 /dev 目录下创建一个与之对应的设备节点文件,应用程序通过操作这个节点文件来完成对具体设备的操作。
/* 创建chrdev设备节点文件,c表示字符设备,a为主设备号,与/proc/devices下的加载的设备号一致,b为次设备号 */
mknod /dev/chrdev c a b
分配和释放设备号
动态分配设备号
\fs\char_dev.c
/*
* 分配设备号
*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
{
struct char_device_struct *cd;
cd = __register_chrdev_region(0, baseminor, count, name);
if (IS_ERR(cd))
return PTR_ERR(cd);
*dev = MKDEV(cd->major, cd->baseminor);
return 0;
}
创建设备
\driver\base\core.c
/*
* 创建设备
*/
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...)
{
va_list vargs;
struct device *dev;
va_start(vargs, fmt);
dev = device_create_vargs(class, parent, devt, drvdata, fmt, vargs);
va_end(vargs);
return dev;
}
创建类
\linux\devices.h
/*
* 创建类
*/
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})
参考
《Linux设备驱动开发详解 基于最新的Linux4.0内核.pdf》
《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.5.pdf》
🎃
🎐
🎨