看完宋寶華的《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");