1、简介
字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比
应用程序运行在用户空间,而 Linux 驱动属于内核的一部分,因此驱动运行于内核空间。当我们在用户空间想要实现对内核的操作,比如使用 open 函数打开/dev/led 这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入”到内核空间,这样才能实现对底层驱动的操作。open、close、write 和 read 等这些函数是由 C 库提供的,在 Linux 系统中,系统调用作为 C 库的一部分。
2、字符设备注册与注销
字符设备的注册和注销函数原型
static inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)
register_chrdev 函数用于注册字符设备,此函数一共有三个参数,这三个参数的含义如下:
major:主设备号,Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两
部分,关于设备号后面会详细讲解。
name:设备名字,指向一串字符串。
fops:结构体 file_operations 类型指针,指向设备的操作函数集合变量。
unregister_chrdev 函数用户注销字符设备,此函数有两个参数,这两个参数含义如下:
major:要注销的设备对应的主设备号。
name:要注销的设备对应的设备名。
unregister_chrdev函数用于注销字符设备
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 (*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
};
owner 拥有该结构体的模块的指针,一般设置为 THIS_MODULE。
llseek 函数用于修改文件当前的读写位置。
read 函数用于读取设备文件。
write 函数用于向设备文件写入(发送)数据。
poll 是个轮询函数,用于查询设备是否可以进行非阻塞的读写。
unlocked_ioctl 函数提供对于设备的控制功能,与应用程序中的 ioctl 函数对应。
compat_ioctl 函数与 unlocked_ioctl 函数功能一样,区别在于在 64 位系统上,32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是unlocked_ioctl。
mmap 函数用于将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,比如 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。
open 函数用于打开设备文件。
release 函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应。
fasync 函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。
aio_fsync 函数与 fasync 函数的功能类似,只是 aio_fsync 是异步刷新待处理的数据。
3、新字符设备驱动
新字符设备驱动框架是为了解决驱动加载后还需要手动使用mknod命令挂载/dev/xxx文件的问题。
大概初始化流程:
一、创建设备号
二、初始化 cdev
三、添加一个 cdev
四、创建类
五、创建设备
1)创建设备号
动态设备号的申请函数如下:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
函数 alloc_chrdev_region 用于申请设备号,此函数有 4 个参数:
dev:保存申请到的设备号。
baseminor:次设备号起始地址,alloc_chrdev_region 可以申请一段连续的多个设备号,这些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor 为起始地址地址开始递增。一般 baseminor 为 0,也就是说次设备号从 0 开始。
count:要申请的设备号数量。
name:设备名字。
2)cdev
在 Linux 中使用 cdev 结构体表示一个字符设备,cdev 结构体在 include/linux/cdev.h 文件中的定义.
cdev初始化,cdev_init 函数原型如下:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
例子:
struct cdev testcdev;
/* 设备操作函数 */
static struct file_operations test_fops = {
.owner = THIS_MODULE,
/* 其他具体的初始项 */
};
testcdev.owner = THIS_MODULE;
cdev_init(&testcdev, &test_fops); /* 初始化 cdev 结构体变量 */
cdev_init 初始化后。cdev_add 函数向 Linux 系统添加这个字符设备。
cdev_add 函数原型如下:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
参数 p 指向要添加的字符设备(cdev 结构体变量),参数 dev 就是设备所使用的设备号,参数 count 是要添加的设备数量。
cdev_del卸载cdev。
void cdev_del(struct cdev *p)
3)创建类
struct class *class_create (struct module *owner, const char *name)
class_create 一共有两个参数,参数 owner 一般为 THIS_MODULE,参数 name 是类名字。
返回值是个指向结构体 class 的指针,也就是创建的类。
卸载驱动程序的时候需要删除掉类,类删除函数为 class_destroy,函数原型如下:
void class_destroy(struct class *cls);
4)创建设备
使用 device_create 函数在类下面创建设备,device_create 函数原型如下:
struct device *device_create(struct class *class, struct device *parent,dev_t devt, void *drvdata, const char *fmt, ...)
device_create 是个可变参数函数,参数 class 就是设备要创建哪个类下面;参数 parent 是父设备,一般为 NULL,也就是没有父设备;参数 devt 是设备号;参数 drvdata 是设备可能会使用的一些数据,一般为 NULL;参数 fmt 是设备名字,如果设置 fmt=xxx 的话,就会生成/dev/xxx这个设备文件。返回值就是创建好的设备。
卸载驱动的时候需要删除掉创建的设备。
void device_destroy(struct class *class, dev_t devt)
5)例程
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/device.h>
//设备名
#define DEV_NAME "new_fs"
//申请设备数
#define DEV_NUM 1
struct My_Config_typedef{
struct cdev cdev; //cdev句柄
struct class *class; //class句柄
dev_t dev; //设备号
struct device *device; //设备句柄
int MAJOY; //主设备号
int MINOR; //次设备号
};
static struct My_Config_typedef Config_t;
int dev_open(struct inode *inode, struct file *filep)
{
printk("dev open!\r\n");
return 0;
}
ssize_t dev_read(struct file *filp, char __user *buf,size_t cnt, loff_t *offt)
{
printk("dev_read!\r\n");
return cnt;
}
ssize_t dev_write(struct file *filp, char __user *buf,size_t cnt, loff_t *offt)
{
printk("dev_write!\r\n");
return cnt;
}
int dev_release(struct inode *inode, struct file *filep)
{
printk("dev_release!\r\n");
return 0;
}
/*
* 设备操作函数结构体
*/
static struct file_operations dev_fops = {
.owner = THIS_MODULE,
.open = dev_open,
.read = dev_read,
.write = dev_write,
.release = dev_release,
};
static int __init module_my_init(void){
printk("module_init !\r\n");
//动态申请设备号
alloc_chrdev_region(&Config_t.dev,0,DEV_NUM,DEV_NAME);
Config_t.MAJOY = MAJOR(Config_t.dev); /* 获取主设备号 */
Config_t.MINOR = MINOR(Config_t.dev); /* 获取次设备号 */
printk("MAJOY:%d\tMINOR:%d\r\n",Config_t.MAJOY,Config_t.MINOR);
//cdev初始化
Config_t.cdev.owner = THIS_MODULE;
cdev_init(&Config_t.cdev, &dev_fops);
//添加字符设备
cdev_add(&Config_t.cdev,Config_t.dev,DEV_NUM);
//创建类
Config_t.class = class_create(THIS_MODULE,DEV_NAME);
if (IS_ERR(Config_t.class)) {
return PTR_ERR(Config_t.class);
}
//创建设备
Config_t.device = device_create(Config_t.class,NULL,Config_t.dev,NULL,DEV_NAME);
if(IS_ERR(Config_t.device)){
return PTR_ERR(Config_t.device);
}
return 0;
}
static void __exit module_my_exit(void){
printk("module_exit !\r\n");
//注销设备号
unregister_chrdev(Config_t.MAJOY,DEV_NAME);
//删除cdev
cdev_del(&Config_t.cdev);
//撤销设备
device_destroy(Config_t.class,Config_t.dev);
//撤销类
class_destroy(Config_t.class);
}
module_init(module_my_init); //注册模块加载函数
module_exit(module_my_exit); //注册模块卸载函数
MODULE_LICENSE("GPL"); //添加模块 LICENSE 信息
MODULE_AUTHOR("PGLaoZu");//添加模块作者信息