字符设备驱动基础知识

一、字符设备结构体(cdev)

在Linux内核中, 使用cdev结构体来描述一个字符设备。

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成员定义了设备号,为32位,其中12位为主设备号,20位为次设备号。
使用下面的宏来定义dev_t

MKDEV(int major, int minor)

使用下面的宏来获取主设备号和次设备号

MAJOR(dev_t dev)
MINOR(dev_t dev)

二、字符设备API

分配和释放设备号

/*
函数功能:分配一些设备号
参数说明:
    from:起始设备号,必须要包含主设备号
    count: 连续分配的数量
    name: 设备或驱动的名称
*/
int register_chrdev_region(dev_t from, unsigned count, const char *name)

/*
函数功能:分配一些字符设备号
参数说明:
    dev:第一个被分配的设备号
    baseminor:起始次设备号
    count:分配的次设备号数(分配的设备号数)
    name: 相关设备或驱动的名称
*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
			const char *name)

/*
函数功能: 释放一些设备号
参数说明:
    from:释放的起始设备号
    count: 释放的设备号数
*/      
void unregister_chrdev_region(dev_t from, unsigned count)
/*
函数功能:初始化一个cdev结构体
参数说明:
    cdev:要初始化的cdev结构体
    fops:该字符设备的file_operations (操作函数)
*/
void cdev_init(struct cdev *cdev, const struct file_operations *fops)

/*
函数功能:动态分配一个cdev结构体
说明:该函数不常用,因为字符设备都会封装一个新的结构体,
    然后使用kzalloc来分配内存(结构体)
*/
struct cdev *cdev_alloc(void)

/*
函数功能: 添加一个字符设备到系统
参数说明:
    p:设备的cdev结构体
    dev:第一个设备的设备号
    count:连续的次设备号数
*/
int cdev_add(struct cdev *p, dev_t dev, unsigned count)

/*
函数功能: 从系统中移除一个cdev
参数说明:
    p:要移除的cdev结构体
*/
void cdev_del(struct cdev *p)

register_chrdev_region()和alloc_chrdev_region()的区别
   register_chrdev_region()函数用于已知起始设备的设备号的情况,而alloc_chrdev_region()用于设备号未知,向系统动态申请未被占用的设备号的情况。alloc_chrdev_region()相比于register_chrdev_region()的优点就在于它会自动避开设备号重复的冲突。

三、字符设备的驱动架构

(1) 为设备定义一个设备相关的结构体(包含设备所涉及的cdev,私有数据及锁等信息)
(2) 初始化函数xxx_init的定义
  1. 向系统申请设备号(register_chrdev_region()或alloc_chrdev_region())
  2. 使用kzalloc申请设备内存(为(1)中定义的结构体申请存储空间)
  3. 调用cdev_init()初始化cdev
  4. 调用cdev_add()向系统注册设备

(3) 卸载函数xxx_exit的定义
  1. 释放设备号(unregister_chrdev_region())
  2. 调用cdev_del()注销设备

(4) 定义file_operations
  1. 实现write()函数 (copy_to_user())
  2. 实现read()函数(copy_from_user())
  3. 根据需要实现其他函数…

举例:

/*
设备结构体
*/
struct xxx_dev_t{
    struct cdev cdev;
    ...
};

struct xxx_dev_t *dev;
dev_t devno;

//读设备
ssize_t xxx_read(struct file *filp, char __user *buf, size_t count, loff_t* f_pos)
{
    ...
    copy_to_user(buf, ..., ...);
}
//写设备
ssize_t xxx_write(struct file *filp, const char __user *buf, size_t count, loff_t* f_pos)
{
    ...
    copy_from_user(..., buf, ...);
}

//操作函数file_operations
struct file_operations xxx_fops = {
    .owner = THIS_MODULE,
    .read = xxx_read,
    .write = xxx_write,
    ...
};

//设备驱动模块加载函数
static int __init xxx_init(void)
{
    ...
    devno = MKDEV(xxx_major, 0);
    //(1)申请设备号
    if(xxx_major)
    {
        register_chrdev_region(devno, 1, "xxx_dev");
    } 
    else
    {
        alloc_chrdev_region(&devno, 0, 1, "xxx_dev");
    }
    //(2)为设备结构体申请内存(推荐使用devm_kzalloc)
    dev = kzalloc(sizeof(struct xxx_dev_t), GFP_KERNEL); 
    //(3)初始化cdev
    cdev_init(&dev.cdev, &xxx_fops);
    dev.cdev.owner = THIS_MODULE;
    //(4)向系统注册设备
    cdev_add(dev.cdev, dev_no, 1);
}
module_init(xxx_init);

//设备驱动模块卸载函数
static void __exit xxx_exit(void)
{
    //释放设备号
    unregister_chrdev_region(dev_no, 1);
    //注销设备
    cdev_del(&dev.cdev);
    ...
}
module_exit(xxx_exit);
MODULE_LICENSE("GPL v2");

四、简化字符设备驱动架构

每次编写初始化函数都要一步一步写,为了简化这个流程,Linux提供了一些封装的接口函数来让我们调用。

/*
函数功能:注册字符设备
参数说明:major:主设备号,0表示动态分配
        name: 设备名
        fops: 操作函数
返回值: 申请的主设备号 
*/
static inline int register_chrdev(unsigned int major, const char *name,
				  const struct file_operations *fops)
/*
函数功能:注销字符设备
参数说明:major:主设备号
        name: 设备名
*/
static inline void unregister_chrdev(unsigned int major, const char *name)

分析一下上面两个函数的源码

//register_chrdev调用了__register_chrdev
int __register_chrdev(unsigned int major, unsigned int baseminor,
		      unsigned int count, const char *name,
		      const struct file_operations *fops)
{
	struct char_device_struct *cd;
	struct cdev *cdev;
	int err = -ENOMEM;
    //1. 申请设备号
	cd = __register_chrdev_region(major, baseminor, count, name);
	if (IS_ERR(cd))
		return PTR_ERR(cd);
    //2. 为设备结构体申请内存
	cdev = cdev_alloc();
	if (!cdev)
		goto out2;
    // 3. 初始化cdev
	cdev->owner = fops->owner;
	cdev->ops = fops;
	kobject_set_name(&cdev->kobj, "%s", name);
   //4. 向系统注册设备
	err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);
	if (err)
		goto out;

	cd->cdev = cdev;

	return major ? 0 : cd->major;
out:
	kobject_put(&cdev->kobj);
out2:
	kfree(__unregister_chrdev_region(cd->major, baseminor, count));
	return err;
}

//unregister_chrdev调用__unregister_chrdev
void __unregister_chrdev(unsigned int major, unsigned int baseminor,
			 unsigned int count, const char *name)
{
	struct char_device_struct *cd;
    //释放设备号
	cd = __unregister_chrdev_region(major, baseminor, count);
	if (cd && cd->cdev)
		cdev_del(cd->cdev); //注销设备
	kfree(cd);
}

通过上面的分析发现和上面的驱动框架是一样的,只是做了个封装罢了。目前大部分字符设备驱动都不会直接使用最低层的那些函数了(cdev_alloc或cdev_add等)。

简化上面的驱动模板

/*
设备结构体
*/
struct xxx_dev_t{
    struct cdev cdev;
    ...
};

struct xxx_dev_t *dev;
dev_t devno;

//读设备
ssize_t xxx_read(struct file *filp, char __user *buf, size_t count, loff_t* f_pos)
{
    ...
    copy_to_user(buf, ..., ...);
}
//写设备
ssize_t xxx_write(struct file *filp, const char __user *buf, size_t count, loff_t* f_pos)
{
    ...
    copy_from_user(..., buf, ...);
}

//操作函数file_operations
struct file_operations xxx_fops = {
    .owner = THIS_MODULE,
    .read = xxx_read,
    .write = xxx_write,
    ...
};

//设备驱动模块加载函数
static int __init xxx_init(void)
{
    ...
    //注册字符设备
    xxx_major = register_chrdev(0, "xxx_dev", &xxx_fops);
}
module_init(xxx_init);

//设备驱动模块卸载函数
static void __exit xxx_exit(void)
{
    //注销字符设备
    unregister_chrdev(major, "xxx_dev"); 
    ...
}
module_exit(xxx_exit);
MODULE_LICENSE("GPL v2");

五、其他API

下面是一些平时编写字符设备驱动时可能会用到的API:

//创建一个类,在sys/class/目录下创建一个类
#define class_create(owner, name)		\
({						\
	static struct lock_class_key __key;	\
	__class_create(owner, name, &__key);	\
})

//创建一个设备(在/dev目录下创建设备文件),并注册到sysfs
struct device *device_create(struct class *class, struct device *parent,
			     dev_t devt, void *drvdata, const char *fmt, ...)
//物理地址映射为虚拟地址
void __iomem *ioremap(resource_size_t res_cookie, size_t size)
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页