如何編寫一個簡單的Linux驅動(三)——完善設備驅動


前期知識

  1.如何編寫一個簡單的Linux驅動(一)——驅動的基本框架

  2.如何編寫一個簡單的Linux驅動(二)——設備操作集file_operations

前言

  在上一篇文章中,我們編寫設備驅動遇到了不少問題:

  (1) 注冊設備時,設備號需要程序員給定,每次編寫驅動時,程序員需要知道有哪些設備號是空閑的;

  (2) 加載驅動后,需要用戶使用mknod命令手動生成設備節點;

  (3) 雖然用戶程序調用了讀寫設備的函數,但是並沒有數據傳輸。

  在本篇文章中,我們會一次解決這三個問題。

  要下載上一篇文章所寫的全部代碼,請點擊這里

1.自定義一個設備結構體

  為了方便,我們自己定義一個結構體,用於描述我們的設備,存放和設備有關的屬性。打開上一篇文章所寫的源代碼文件,加入如下代碼。  

 1 struct shanwuyan_dev
 2 {
 3     struct cdev c_dev;        //字符設備
 4     dev_t dev_id;            //設備號
 5     struct class *class;      //
 6     struct device *device;    //設備
 7     int major;                //主設備號
 8     int minor;                //次設備號
 9 };
10 
11 struct shanwuyan_dev shanwuyan;    //定義一個設備結構體    

  我們對成員變量分別進行解析。

成員變量 描述
struct cdev c_dev 這是一個字符設備結構體,在后文我們再介紹
dev_t dev_id  這是一個32位的數據,其中高12位表示主設備號,低20位表示次設備號,高低設備號組合在一起表示一個完整的設備號
struct class *class 類,主要作用后文再介紹
struct device *device 設備,主要作用后文再介紹
int major 主設備號
int minor 次設備號

  接下來我們要介紹三個宏函數"MAJOR"、"MINOR"、"MKDEV",它們的原型如下。  

1 #define MINORBITS    20
2 #define MINORMASK    ((1U << MINORBITS) - 1)
3 
4 #define MAJOR(dev)    ((unsigned int) ((dev) >> MINORBITS))
5 #define MINOR(dev)    ((unsigned int) ((dev) & MINORMASK))
6 #define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi))

  看起來很復雜,但是它們的功能很簡單:"MAJOR"的作用是根據設備號獲取主設備號,即設備號的高12位;"MINOR"的作用是根據設備號獲取次設備號,即設備號的低20位;"MKDEV"的作用是把主設備號和次設備號合並成一個完整的設備號。

2.新的注冊與注銷字符設備的方法  

  在上一篇文章中,我們使用"register_chrdev"函數來注冊設備,使用"unregister_chrdev"函數來注銷設備。這一組函數的缺點是:首先,主設備號需要用戶給定;其次,使用該函數的話,設備會占據整個主設備號,對應的次設備號無法使用,造成設備號的浪費。為了克服以上缺點,我們引入兩組新的注冊設備號的函數"register_chrdev_region"和"alloc_chrdev_region",這兩個函數對應的注銷設備號的函數都是"unregister_chrdev_region"。它們的函數原型如下。

1 //這些函數的聲明都在linux/fs.h中
2 extern int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);    //第一個參數是設備號的地址,第二個參數是次設備號的起始號,第三個參數是要申請的個數,第四個參數是設備名稱
3 extern int register_chrdev_region(dev_t, unsigned, const char *);    //第一個參數是設備號,第二個參數是要申請的個數,第三個參數是設備名稱
4 extern void unregister_chrdev_region(dev_t, unsigned);    //第一個參數是設備號,第二個參數是申請的個數

  如果用戶給定了主設備號,可以使用"register_chrdev_region"函數來讓系統分配次設備號;如果用戶未給定主設備號,可以使用"alloc_chrdev_region"函數,由系統分配主設備號和次設備號。這兩個函數在驅動的入口函數里調用,作初始化用。相應的,要在驅動出口函數中調用"unregister_chrdev_region"函數來注銷設備號。如下方代碼。

 1 static int __init shanwuyan_init(void)    //驅動入口函數
 2 {
 3     int ret = 0;
 4     
 5     shanwuyan.major = 0;    //主設備號設置為0,表示用戶不給定主設備號,主次設備號都由系統分配
 6     /*1.分配設備號*/
 7     if(shanwuyan.major)        //如果給定了主設備號,則由系統分配次設備號
 8     {
 9         shanwuyan.dev_id = MKDEV(shanwuyan.major, 0);    //把用戶給的主設備號和0號次設備號合並成一個設備號
10         ret = register_chrdev_region(shanwuyan.dev_id, 1, SHANWUYAN_NAME);    //因為我們只考慮一個設備的情況,所以只分配一個設備號,即設備號0
11     }
12     else                    //如果沒有給定主設備號,則主次設備號全部由系統分配
13     {
14         ret = alloc_chrdev_region(&(shanwuyan.dev_id), 0, 1, SHANWUYAN_NAME);    //只考慮一個設備的情況
15         shanwuyan.major = MAJOR(shanwuyan.dev_id);    //獲取主設備號
16         shanwuyan.minor = MINOR(shanwuyan.dev_id);    //獲取次設備號
17     }
18     if(ret < 0)    //設備號分配失敗,則打印錯誤信息,然后返回
19     {
20         printk(KERN_EMERG "shanwuyan chrdev_region error!\r\n");
21         return -EINVAL;
22     }
23     else    //如果設備號分配成功,則打印設備的主次設備號
24     {
25         printk(KERN_EMERG "shanwuyan.major = %d, shanwuyan.minor = %d\r\n", shanwuyan.major, shanwuyan.minor);
26     }
27 
28 
29     return 0;
30 }
31 
32 static void __exit shanwuyan_exit(void)    //驅動出口函數
33 {
34     /*1.注銷設備號*/
35     unregister_chrdev_region(shanwuyan.dev_id, 1);
36 }

  以上代碼的功能是:入口函數實現由系統分配主次設備號,出口函數實現注銷系統分配的設備號。

  聽起來這兩組新的注冊設備號的函數好處多多,但是它們卻有一個致命的缺點,那就是只能實現分配設備號的功能,卻無法像"register_chrdev"函數那樣還可以把設備添加到內核中。為了把設備添加到內核,我們就要引進字符設備結構體"struct cdev",這也是我們文章開頭的自定義結構體的第一個成員變量。該結構體的原型如下。  

1 //該結構體原型在linux/cdev.h中,記得在驅動代碼中包含進去
2 struct cdev {
3     struct kobject kobj;
4     struct module *owner;
5     const struct file_operations *ops;
6     struct list_head list;
7     dev_t dev;
8     unsigned int count;
9 };

  在本文中,我們只用到該結構體中的三個成員變量"struct module *owner"、"const struct file_operations *ops"、"dev_t dev",他們的描述如下。

成員變量 描述
struct module *owner
一般取值為THIS_MODULE
const struct file_operations *ops
設備操作集file_operations的地址
dev_t dev
就是設備號

  接下來要介紹兩個與該結構體相關的函數,"cdev_init"和"cdev_add",它們的原型如下。

1 void cdev_init(struct cdev *, const struct file_operations *);    //第一個參數是struct cdev結構體變量的地址,第二個參數是字符設備操作集的地址
2 int cdev_add(struct cdev *, dev_t, unsigned);    //第一個參數是struct cdev結構體變量的地址,第二個參數是設備號,第三個參數是要添加的數量

  這兩個函數的作用分別是初始化字符設備結構體向內核添加字符設備

  向入口函數中添加代碼,將字符設備注冊到內核中,添加的代碼如下。  

 1 static int __init shanwuyan_init(void)    //驅動入口函數
 2 {
 3     int ret = 0;
 4     
 5     /*1.分配設備號*/
 6     ...
 7 
 8     /*2.向內核添加字符設備*/
 9     shanwuyan.c_dev.owner = THIS_MODULE;
10     cdev_init(&(shanwuyan.c_dev), &(shanwuyan_fops));    //初始化字符設備結構體
11     cdev_add(&(shanwuyan.c_dev), shanwuyan.dev_id, 1);    //添加設備到內核
12 
13     return 0;
14 }

  這樣,設備就注冊成功了。

3.自動創建設備節點

  要實現自動創建設備節點,我們需要引進兩個結構體,"struct class"和"struct device"。即,文章開頭的自定義設備結構體中的成員變量"struct class *class"和"struct device *device"是用於實現自動生成設備節點的。這兩個結構體的具體實現我們先不作深入了解,只需要了解如何在這里使用他們。我們先引進四個關於這兩個結構體的函數,"class_create"、"class_destroy"、"device_create"、"device_destroy",這些函數的作用分別是創建類、摧毀類、創建設備、摧毀設備。它們的原型如下。  

 1 //位於"linux/device.h"中,記得在驅動代碼中包含進去
 2 #define class_create(owner, name)        \    //第一個參數是所有者(一般為THIS_MODULE),第二個參數是設備名稱
 3 ({                                  \
 4     static struct lock_class_key __key;    \
 5     __class_create(owner, name, &__key);    \
 6 })                              
 7 
 8 extern void class_destroy(struct class *cls);    //參數是創建的類的地址
 9 
10 struct device *device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...);    //第一個參數是類的地址,第二個參數為父設備地址(一般為NULL),第三個參數為設備號,第四個參數為可能用到的數據(一般為NULL),第五個參數為設備名稱
11 extern void device_destroy(struct class *cls, dev_t devt);    //第一個參數為類的地址,第二個參數為設備號

   為了實現自動創建設備節點,我們要在入口函數中創建一個類,然后在類里創建一個設備。在出口函數中,也要相應地摧毀設備和類。代碼如下。  

 1 static int __init shanwuyan_init(void)    //驅動入口函數
 2 {
 3     int ret = 0;
 4     
 5     /*1.分配設備號*/
 6     ...
 7 
 8     /*2.向內核添加字符設備*/
 9     ...
10 
11     /*3.自動創建設備節點*/
12     shanwuyan.class = class_create(THIS_MODULE, SHANWUYAN_NAME);    //創建類
13     shanwuyan.device = device_create(shanwuyan.class, NULL, shanwuyan.dev_id, NULL, SHANWUYAN_NAME);    //創建設備,設備節點就自動生成了。正常情況下,要考慮類和設備創建失敗的情況,為了簡化代碼,這里就不寫了
14   
15     return 0;
16 }
17 
18 static void __exit shanwuyan_exit(void)    //驅動出口函數
19 {
20     /*1.注銷設備號*/
21     ...
22     /*2.摧毀設備*/
23     device_destroy(shanwuyan.class, shanwuyan.dev_id);
24     /*3.摧毀類*/
25     class_destroy(shanwuyan.class);
26 }

  在入口函數中,我們先創建了類,后創建了設備,即有類才能有設備,所以在出口函數中,我們要先把設備摧毀了,然后再摧毀類。

4.實現與用戶程序的數據傳輸

  上一篇文章中,file_operations的讀寫操作並沒有發揮真正的作用。在本文中,我們改寫一下驅動讀寫函數和用戶程序代碼,讓設備和用戶程序實現數據傳輸。

  首先修改一下驅動程序的"shanwuyan_write"函數和"shanwuyan_read"函數,其中讀函數的作用是向用戶程序傳輸一個字符串,寫函數的作用是接收用戶程序發來的數據,並打印出來,代碼如下。   

 1 /*讀設備*/
 2 static ssize_t shanwuyan_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos)
 3 {
 4     char device_data[] = "device data";
 5     copy_to_user(buf, device_data, sizeof(device_data));    //向用戶程序傳輸設備數據
 6     return 0;
 7 }
 8 
 9 /*寫設備*/
10 static ssize_t shanwuyan_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos)
11 {
12     char user_data[50];
13     copy_from_user(user_data, buf, count);        //獲取用戶程序寫到設備的數據
14     printk("device get data:%s\r\n", user_data);
15     return 0;
16 }

   這里用到了兩個函數,"copy_to_user"和"copy_from_user",作用分別是向用戶程序傳輸數據和從用戶程序接收數據。它們的原型如下。  

1 //聲明在文件linux/uaccess.h中,記得在驅動代碼中包含進去
2 static __always_inline unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n)    //第一個參數是目的地址,第二個參數是源地址,第三個參數是數據的size
3 static __always_inline unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n)    //第一個參數是目的地址,第二個參數是源地址,第三個參數是數據的size

  接下來改造用戶程序,全部代碼如下。

 1 //源代碼文件名為"shanwuyanAPP.c"
 2 #include <sys/types.h>
 3 #include <sys/stat.h>
 4 #include <fcntl.h>
 5 #include <stdio.h>
 6 #include <unistd.h>
 7 #include <stdlib.h>
 8 #include <string.h>
 9 
10 /*
11 *argc:應用程序參數個數,包括應用程序本身
12 *argv[]:具體的參數內容,字符串形式
13 *./shanwuyanAPP <filename> <r:w> r表示讀,w表示寫
14 */
15 int main(int argc, char *argv[])
16 {
17     int ret = 0;
18     int fd = 0;
19     char *filename;
20     char readbuf[50];
21     char user_data[] = "user data";
22 
23     if(argc != 3)
24     {
25         printf("Error usage!\r\n");
26         return -1;
27     }
28         
29     filename = argv[1];    //獲取文件名稱
30 
31     fd = open(filename, O_RDWR);
32     if(fd < 0)
33     {
34         printf("cannot open file %s\r\n", filename);
35         return -1;
36     }
37     /*讀操作*/
38     if(!strcmp(argv[2], "r"))
39     {
40         read(fd, readbuf, 50);
41         printf("user get data:%s\r\n", readbuf);
42     }
43     /*寫操作*/
44     else if(!strcmp(argv[2], "w"))
45     {
46         write(fd, user_data, 50);
47     }
48     else
49     {
50         printf("ERROR usage!\r\n");
51     }
52 
53     /*關閉操作*/
54     ret = close(fd);
55     if(ret < 0)
56     {
57         printf("close file %s failed\r\n", filename);
58     }
59 
60     return 0;
61 }

5.應用

  編譯驅動程序,交叉編譯用戶程序,拷貝到開發板中。

  在終端輸入命令"insmod shanwuyan.ko"加載驅動,可以看到系統分配的主次設備號分別為246和0.

  在終端輸入命令"ls /dev/shanwuyan",可以看到已經自動創建了設備節點"/dev/shanwuyan"。

  在終端輸入"./shanwuyanAPP /dev/shanwuyan r",讓用戶程序讀設備,可以看到終端打印出了設備傳遞給用戶程序的信息。

  在終端輸入"./shanwuyanAPP /dev/shanwuyan w",讓用戶程序寫設備,可以看到終端打印出了用戶程序傳遞給設備的信息。

  本文的全部代碼在這里


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM