Linux 字符設備驅動簡單總結

        看完宋寶華的《Linux設備驅動開發詳解》及其有關博客,對字符設備驅動作一個小總結。node

1、字符設備、字符設備驅動與用戶空間訪問該設備的程序三者之間的關係。linux

        如圖,在Linux內核中使用cdev結構體來描述字符設備,經過其成員dev_t來定義設備號(分爲主、次設備號)以肯定字符設備的惟一性。經過其成員file_operations來定義字符設備驅動提供給VFS的接口函數,如常見的open()、read()、write()等。
數據結構

        在Linux字符設備驅動中,模塊加載函數經過register_chrdev_region( ) 或alloc_chrdev_region( )來靜態或者動態獲取設備號,經過cdev_init( )創建cdev與file_operations之間的鏈接,經過cdev_add( )向系統添加一個cdev以完成註冊。模塊卸載函數經過cdev_del( )來註銷cdev,經過unregister_chrdev_region( )來釋放設備號。函數

        用戶空間訪問該設備的程序經過Linux系統調用,如open( )、read( )、write( ),來「調用」file_operations來定義字符設備驅動提供給VFS的接口函數。spa

2、字符設備驅動模型.net

        (PS:神馬狀況!本地上傳的圖片,質量降低這麼多)
設計

     1. 驅動初始化
指針

     1.1. 分配cdev
code

        在2.6的內核中使用cdev結構體來描述字符設備,在驅動中分配cdev,主要是分配一個cdev結構體與申請設備號,以按鍵驅動爲例:
blog

/*……*/
/* 分配cdev*/
struct cdev btn_cdev;
/*……*/
/* 1.1 申請設備號*/
	if(major){
		//靜態
		dev_id = MKDEV(major, 0);
		register_chrdev_region(dev_id, 1, "button");
	} else {
		//動態
		alloc_chardev_region(&dev_id, 0, 1, "button");
		major = MAJOR(dev_id);
	}
/*……*/

        從上面的代碼能夠看出,申請設備號有動靜之分,其實設備號還有主次之分。

        在Linux中以主設備號用來標識與設備文件相連的驅動程序。次編號被驅動程序用來辨別操做的是哪一個設備。cdev 結構體的 dev_t 成員定義了設備號,爲 32 位,其中高 12 位爲主設備號,低20 位爲次設備號。

        設備號的得到與生成:

        得到:主設備號:MAJOR(dev_t dev);

                  次設備號:MINOR(dev_t dev);

        生成:MKDEV(int major,int minor);

        設備號申請的動靜之分:

        靜態:   

int register_chrdev_region(dev_t from, unsigned count, const char *name);
/*功能:申請使用從from開始的count 個設備號(主設備號不變,次設備號增長)*/

        靜態申請相對較簡單,可是一旦驅動被普遍使用,這個隨機選定的主設備號可能會致使設備號衝突,而使驅動程序沒法註冊。

        動態:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name);
/*功能:請求內核動態分配count個設備號,且次設備號從baseminor開始。*/

        動態申請簡單,易於驅動推廣,可是沒法在安裝驅動前建立設備文件(由於安裝前尚未分配到主設備號)。

    1.2. 初始化cdev

        void cdev_init(struct cdev *, struct file_operations *); 

        cdev_init()函數用於初始化 cdev 的成員,並創建 cdev 和 file_operations 之間的鏈接。

    1.3. 註冊cdev

        int cdev_add(struct cdev *, dev_t, unsigned);

        cdev_add()函數向系統添加一個 cdev,完成字符設備的註冊。

    1.4. 硬件初始化

        硬件初始化主要是硬件資源的申請與配置,以TQ210的按鍵驅動爲例:

/* 1.4 硬件初始化*/
	//申請GPIO資源
	gpio_request(S5PV210_GPH0(0), "GPH0_0");
	//配置輸入
	gpio_direction_input(S5PV210_GPH0(0));

    2.實現設備操做

        用戶空間的程序以訪問文件的形式訪問字符設備,一般進行open、read、write、close等系統調用。而這些系統調用的最終落實則是file_operations結構體中成員函數,它們是字符設備驅動與內核的接口。以TQ210的按鍵驅動爲例:

/*設備操做集合*/
static struct file_operations btn_fops = {
	.owner = THIS_MODULE,
	.open = button_open,
	.release = button_close,
	.read = button_read
};

        上面代碼中的button_open、button_close、button_read是要在驅動中本身實現的。file_operations結構體成員函數有不少個,下面就選幾個常見的來展現:

    2.1. open()函數

        原型:

int(*open)(struct inode *, struct file*); 
/*打開*/

        案例:

static int button_open(struct inode *inode, struct file *file){
	unsigned long flags;
	//獲取分配好的私有數據結構的首地址
	struct button_priv *pbtnp = container_of(inode->i_cdev, 
	                                         struct button_priv, 
	                                         btn_cdev);
	//保存首地址到file->private_data
	file->private_data = pbtnp;
	if(down_interruptible(&pbtnp->sema)){
		printk("Proccess is INT!\n");
		return -EINTR;
	}
	printk("open button successfully !\n");
	return 0;
}

    2.2. read( )函數

     原型:

ssize_t(*read)(struct file *, char __user*, size_t, loff_t*); 
/*用來從設備中讀取數據,成功時函數返回讀取的字節數,出錯時返回一個負值*/

    案例:

static ssize_t button_read(struct file *file, char __user *buf,
			    size_t count, loff_t *ppos){
	//獲取首地址
	struct button_priv *pbtnp = file->private_data;
	//判斷按鍵是否有操做,若是有,則讀取鍵值並上報給用戶;反之,則休眠
	wait_event_interruptible(pbtnp->btn_wq, is_press != 0);
	is_press = 0;
	//上報鍵值
	copy_to_user(buf, &key_value, sizeof(key_value));
	return count;
}
/*參數:file是文件結構體指針,buf是用戶空間內存的地址,該地址在內核空間不能直接讀寫,
  count 是要讀的字節數,ppos是讀的位置相對於文件開頭的偏移*/

    2.3. write( )函數

    原型:

ssize_t(*write)(struct file *, const char__user *, size_t, loff_t*);
/*向設備發送數據,成功時該函數返回寫入的字節數。若是此函數未被實現,
  當用戶進行write()系統調用時,將獲得-EINVAL返回值*/

    案例:

static ssize_t mem_write(struct file *filp, const char __user *buf, 
                         size_t size, loff_t *ppos){
    unsigned long p =  *ppos;
    unsigned int count = size;
    int ret = 0;
    int *register_addr = filp->private_data; /*獲取設備的寄存器地址*/ 
    /*分析和獲取有效的寫長度*/
    if (p >= 5*sizeof(int))
        return 0;
    if (count > 5*sizeof(int) - p)
        count = 5*sizeof(int) - p;   
    /*從用戶空間寫入數據*/
    if (copy_from_user(register_addr + p, buf, count))
        ret = -EFAULT;
    else {
        *ppos += count;
        ret = count;
    }
    return ret;
}
/*參數:filp是文件結構體指針,buf是用戶空間內存的地址,該地址在內核空間不能直接讀寫,
  count 是要讀的字節數,ppos是讀的位置相對於文件開頭的偏移*/

    2.4. close( )函數

    原型:

int(*release)(struct inode *, struct file*); 
/*關閉*/

    案例:

static int button_close(struct inode *inode, struct file *file){
	/* 1.獲取首地址*/
	struct button_priv *pbtnp = file->private_data;
	up(&pbtnp->sema);
	return 0;
}

    2.5. 補充說明

        1. 在Linux字符設備驅動程序設計中,有3種很是重要的數據結構:struct file、struct inode、struct file_operations。

        struct file 表明一個打開的文件。系統中每一個打開的文件在內核空間都有一個關聯的struct file。它由內核在打開文件時建立, 在文件關閉後釋放。其成員loff_t f_pos 表示文件讀寫位置。

        struct inode 用來記錄文件的物理上的信息。所以,它和表明打開文件的file結構是不一樣的。一個文件能夠對應多個file結構,但只有一個inode結構。其成員dev_t i_rdev表示設備號。

        struct file_operations 一個函數指針的集合,定義能在設備上進行的操做。結構中的成員指向驅動中的函數,這些函數實現一個特別的操做, 對於不支持的操做保留爲NULL。

        2. 在read( )和write( )中的buff 參數是用戶空間指針。所以,它不能被內核代碼直接引用,由於用戶空間指針在內核空間時可能根本是無效的——沒有那個地址的映射。所以,內核提供了專門的函數用於訪問用戶空間的指針:

unsigned long copy_from_user(void *to, const void __user *from, unsigned long count);
unsigned long copy_to_user(void __user *to, const void *from, unsigned long count);

    3. 驅動註銷

    3.1. 刪除cdev

        在字符設備驅動模塊卸載函數中經過cdev_del()函數向系統刪除一個cdev,完成字符設備的註銷。

/*原型:*/
void cdev_del(struct cdev *);
/*例:*/
cdev_del(&btn_cdev);

    3.2. 釋放設備號

        在調用cdev_del()函數從系統註銷字符設備以後,unregister_chrdev_region()應該被調用以釋放原先申請的設備號。

/*原型:*/
void unregister_chrdev_region(dev_t from, unsigned count);
/*例:*/
unregister_chrdev_region(MKDEV(major, 0), 1);

3、Linux字符設備驅動模板與案例

    1. 字符設備驅動模塊加載與卸載函數模板

        在實際開發中,一般習慣爲設備定義一個設備相關的結構體,其包含該設備所涉及到的cdev、私有數據及信號量等信息。

/*字符設備驅動模塊加載與卸載函數模板*/
/* 設備結構體
struct xxx_dev_t {
    struct cdev cdev;
    ...
} xxx_dev;
/* 設備驅動模塊加載函數
static int __init xxx_init(void) {
    ...
    cdev_init(&xxx_dev.cdev, &xxx_fops);/* 初始化cdev */
    xxx_dev.cdev.owner = THIS_MODULE;
    /* 獲取字符設備號*/
    if (xxx_major) {
        register_chrdev_region(xxx_dev_no, 1,DEV_NAME);
    } else {
        alloc_chrdev_region(&xxx_dev_no, 0, 1,DEV_NAME);
    }
    ret = cdev_add(&xxx_dev.cdev,xxx_dev_no, 1); /* 註冊設備*/
    ...
}

/*設備驅動模塊卸載函數*/
static void __exit xxx_exit(void) {
    unregister_chrdev_region(xxx_dev_no, 1); /* 釋放佔用的設備號*/
    cdev_del(&xxx_dev.cdev); /* 註銷設備*/
    ...
}

        2.字符設備驅動讀、寫、IO控制函數模板

/*字符設備驅動讀、寫、IO控制函數模板*/
/* 讀設備*/
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, ...);
    ...
}

/* ioctl函數 */
int xxx_ioctl(struct inode *inode, struct file*filp, 
                unsigned int cmd, unsigned long arg) {
    ...
    switch(cmd) {
    caseXXX_CMD1:
        ...
        break;
    caseXXX_CMD2:
        ...
        break;
    default:
    /* 不能支持的命令 */
         return  - ENOTTY;
    }
    return 0;
}

        在設備驅動的讀、寫函數中,filp是文件結構體指針,buf是用戶空間內存的地址,該地址在內核空間不能直接讀寫,count 是要讀的字節數,f_pos是讀的位置相對於文件開頭的偏移。

    3.TQ210的最簡單按鍵驅動示例

#include <linux/init.d>
#include <linux/module.h>
#include <linux/cdev>
#include <linux/fs.h>
#include <linux/types.h>
#include <linux/uaccess.h>
#include <linux/device.h>

#include <plat/gpio-cfg.h>
#include <asm/gpio.h>

static int major;
/* 分配cdev*/
struct cdev btn_cdev;

/* 記錄按鍵值*/
static unsigned char key_value;

/* 2. 實現設備操做*/
/* 2.1 read*/
static ssize_t button_read(struct file *file, char __user *buf,
							size_t count, loff_t *ppos) {
	int status = 0;

	//1. 獲取GPIO的狀態
	status = gpio_get_value(S5PV210_GPH0(0));
	if(status == 1)
		key_value = 0x50;
	else
		key_value = 0x51;

	//2. 上報GPIO的狀態
	copy_to_user(buf, &key_value, sizeof(key_value));

	return count;
}

/* 2.2 設備操做集合*/
static struct file_operations btn_fops = {
	.owner = THIS_MODULE,
	.read = button_read
};

//設備類
static struct class *btn_cls;

/* 1. 驅動初始化*/
static init button_init(void){
	dev_t dev_id;
/* 1.1 申請設備號*/
	if(major){
		//靜態
		dev_id = MKDEV(major, 0);
		register_chrdev_region(dev_id, 1, "button");
	} else {
		//動態
		alloc_chardev_region(&dev_id, 0, 1, "button");
		major = MAJOR(dev_id);
	}

/* 1.2 初始化cdev*/
	cdev_init(&btn_cdev, &btn_fops);

/* 1.3 註冊cdev*/
	cdev_add(&btn_cdev, dev_id, 1);

/* 1.4 自動建立設備節點*/
/* 1.4.1 建立設備類*/
	//sys/class/button
	btn_cls = class_create(THIS_MODULE, "button");
/* 1.4.2 建立設備節點*/	
	device_create(btn_cls, NULL, dev_id, NULL, "button");

/* 1.4 硬件初始化*/
	//申請GPIO資源
	gpio_request(S5PV210_GPH0(0), "GPH0_0");
	//配置輸入
	gpio_direction_input(S5PV210_GPH0(0));

	return 0;

}


/* 3. 驅動註銷*/
static void button_exit(void){
/* 3.1 釋放GPIO資源*/
	gpio_free(S5PV210_GPH0(0));

/* 3.2 刪除設備節點*/
	device_destroy(btn_cls, MKDEV(major, 0));
	class_destroy(btn_cls);

/* 3.3 刪除cdev*/
	cdev_del(&btn_cdev);

/* 3.4 釋放設備號*/
	unregister_chrdev_region(MKDEV(major, 0), 1);
}

module_init(button_init);
module_exit(button_exit);
MODULE_LICENSE("GPL v2");
相關文章
相關標籤/搜索