字符設備驅動基礎知識

一、字符設備結構體(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)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章