一、字符設備、字符設備驅動與用戶空間訪問該設備的程序三者之間的關係。
如圖,在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的接口函數。
二、字符設備驅動模型
(PS:神馬情況!本地上傳的圖片,質量下降這麼多)
1. 驅動初始化
1.1. 分配cdev
在2.6的內核中使用cdev結構體來描述字符設備,在驅動中分配cdev,主要是分配一個cdev結構體與申請設備號,以按鍵驅動爲例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
/*……*/ /* 分配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);
設備號申請的動靜之分:
靜態:
1
2
|
int register_chrdev_region(dev_t from, unsigned count, const char *name); /*功能:申請使用從from開始的count 個設備號(主設備號不變,次設備號增加)*/ |
靜態申請相對較簡單,但是一旦驅動被廣泛使用,這個隨機選定的主設備號可能會導致設備號衝突,而使驅動程序無法註冊。
動態:
1
2
|
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
2
3
4
5
|
/* 1.4 硬件初始化*/ //申請GPIO資源 gpio_request(S5PV210_GPH0(0), "GPH0_0" ); //配置輸入 gpio_direction_input(S5PV210_GPH0(0)); |
2.實現設備操作
用戶空間的程序以訪問文件的形式訪問字符設備,通常進行open、read、write、close等系統調用。而這些系統調用的最終落實則是file_operations結構體中成員函數,它們是字符設備驅動與內核的接口。以TQ210的按鍵驅動爲例:
1
2
3
4
5
6
7
|
/*設備操作集合*/ 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()函數
原型:
1
2
|
int (*open)( struct inode *, struct file*); /*打開*/ |
案例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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( )函數
原型:
1
2
|
ssize_t(*read)( struct file *, char __user*, size_t , loff_t*); /*用來從設備中讀取數據,成功時函數返回讀取的字節數,出錯時返回一個負值*/ |
案例:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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( )函數
原型:
1
2
3
|
ssize_t(*write)( struct file *, const char__user *, size_t , loff_t*); /*向設備發送數據,成功時該函數返回寫入的字節數。如果此函數未被實現, 當用戶進行write()系統調用時,將得到-EINVAL返回值*/ |
案例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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( )函數
原型:
1
2
|
int (*release)( struct inode *, struct file*); /*關閉*/ |
案例:
1
2
3
4
5
6
|
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 參數是用戶空間指針。因此,它不能被內核代碼直接引用,因爲用戶空間指針在內核空間時可能根本是無效的——沒有那個地址的映射。因此,內核提供了專門的函數用於訪問用戶空間的指針:
1
2
|
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,完成字符設備的註銷。
1
2
3
4
|
/*原型:*/ void cdev_del( struct cdev *); /*例:*/ cdev_del(&btn_cdev); |
3.2. 釋放設備號
在調用cdev_del()函數從系統註銷字符設備之後,unregister_chrdev_region()應該被調用以釋放原先申請的設備號。
1
2
3
4
|
/*原型:*/ void unregister_chrdev_region(dev_t from, unsigned count); /*例:*/ unregister_chrdev_region(MKDEV(major, 0), 1); |
三、Linux字符設備驅動模板與案例
1. 字符設備驅動模塊加載與卸載函數模板
在實際開發中,通常習慣爲設備定義一個設備相關的結構體,其包含該設備所涉及到的cdev、私有數據及信號量等信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
/*字符設備驅動模塊加載與卸載函數模板*/ /* 設備結構體 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控制函數模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
/*字符設備驅動讀、寫、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的最簡單按鍵驅動示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
|
#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" );
|