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的接口函數。

二、字符設備驅動模型

        (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");

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章