字符驅動程序框架與應用測試程序編寫

該文章主要目的爲了學習並掌握以下幾個方面:

  1. 熟悉並編寫字符設備驅動框架
  2. 學習應用層的測試程序編寫
  3. 內核空間與用戶空間的數據傳輸
  4. 驅動安裝與卸載、設備文件查看、應用程序運行等相關命令使用。

嵌入式系統框架簡單介紹

嵌入式系統由硬件、驅動、操作系統、應用,這幾部分層次構成。其中,驅動程序是硬件層與系統層之間的交互層,主要作用是操作底層硬件,實現硬件控制,而應用層位於操作系統層之上,應用層以操作系統爲中介,對驅動層的相關主要函數進行調用(如open(),read(),write()等函數),進而實現對硬件控制。
通常,驅動程序是編譯進操作系統內核中的,與內核融爲一體,區別在於,驅動程序可以以模塊化的形式動態編譯進內核中,相當於拼圖一樣,靈活可拆卸。操作系統可以理解爲龐大的函數庫,提供驅動和應用程序的調用。
應用程序的存放在文件系統中,文件系統可以理解爲一個目錄系統,由很多目錄組成,而這些應用程序就可以存放在某目錄下某文件中(相當於Windows電腦下各個盤符和目錄一樣,存在各個應用程序和文件),因此應用程序可以方便被查找和運行。
(以上均爲個人理解o( ̄︶ ̄)o,有不足之處請指教哦。下面正式進入實操)


實驗內容與源碼

編寫驅動程序和測試程序,實現:開發板任意2個按鍵按下,3個led燈反轉,並在shell終端顯示開發板運行的相關信息。

前期條件:開發板燒錄好u-boot,linux內核,文件系統
芯片:S3C440
開發板:韋老大的JZ2440
引腳說明 KEY1:GPP0,KEY2:GPG3 LED1:GPP4,LED2:GPP5,LED3:GPP6

本實驗源碼下載鏈接點擊此處


字符驅動程序框架與編寫

驅動程序很多中,本文章以字符驅動程序爲入門。學完驅動程序後,就感覺還算蠻簡單,主要是熟悉它的整體結構形式,以及底層寄存器的控制。下面爲編寫流程:

1. 新建以驅動源碼文件,名爲:drv_keyled.c ,然後找好一個已經寫好的字符驅動源碼,作爲模板參考。
2. 將需要的頭文件寫進來,可以直接從模板中copy過來(這些頭文件就是系統內核的源碼庫,驅動程序會從中調用),頭文件如下:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>

3.定義需要用的寄存器名稱等

typedef  unsigned long Uint32;
volatile unsigned long *gpiog_con=NULL;     //寄存器
volatile unsigned long *gpiog_data=NULL;   //寄存器

volatile unsigned long *gpiof_con=NULL;  //寄存器
volatile unsigned long *gpiof_data=NULL;//寄存器

int major;//主設備號

//類和設備類,可以幫助用於自動創建設備文件
static struct class* drv_keyled_class;  //定義類
static struct class_device* drv_keyled_class_device;  //定義一類設備

4.綁定驅動有關的重要函數到內核中。關聯綁定後,使得應用層(用戶層)調用open(),read(),write()等函數時,系統能找到驅動對應的xxx_open(),xxx_read(),xxx_write()函數,代碼如下:

//關鍵函數綁定(結構體)
static struct file_operations drv_key_fops=
{
    .owner = THIS_MODULE,
    .write = drv_keyled_write,
    .read = drv_keyled_read,
    .open = drv_keyled_open,

};

//初始化、卸載函數綁定(宏)
module_init(drv_keyled_init);
module_exit(drv_keyled_exit);

5.關鍵驅動函數編寫——初始化和卸載函數(即drv_keyled_init,drv_keyled_exit)。

初始化函數主要實現功能如下幾點:(卸載函數與初始化函數相反,略述)

  • 向內核註冊驅動,即將結構體綁定的關鍵函數告訴內核,同時從內核中獲取主設備號:major,主設備號用於應用程序對具體哪個驅動的識別。
  • 創建類和類設備,運行後,系統能自動創建設備文件:/dev/keyled,相當於應用層上手動完成“mknod“命令操作。
  • 物理地址映射到虛擬地址VA(ioremap函數),由於系統開啓了MMU,因此程序都是在虛擬地址運行,如果要完成指定寄存器的控制,就需要將該寄存器的物理地址進行映射,通過虛擬地址完成寄存器控制。

初始化和卸載函數調用方法:

  • 當應用層運行命令“insmod”進行驅動裝載時,系統自動調用初始化函數。
  • 當應用層運行命令“rmmod”進行驅動卸載時,系統自動調用卸載函數。

源碼如下:


/*
函數名:drv_keyled_init
功能:初始化模塊功能(insmod裝載驅動時調用)
*/
static int drv_keyled_init(void)
{

    //註冊主設備號,由fops結構體告訴內核綁定的函數
    major = register_chrdev(0, "drv_keyled", &drv_key_fops);

    //創建類
    drv_keyled_class = class_create(THIS_MODULE, "drv_keyled");
    //創建類設備
    drv_keyled_class_device = class_device_create(drv_keyled_class, NULL, MKDEV(major, 0), NULL, "keyled");//   "/dev/keyled"

    //虛擬地址VA映射
    gpiof_con = (unsigned long*)ioremap(0x56000050, 12); //映射物理地址的起始地址與長度,返回虛擬地址
    gpiof_data =  gpiof_con+1;//地址在類型長度上加1(即+4地址)

    gpiog_con = (unsigned long*)ioremap(0x56000060, 12); //映射物理地址的起始地址與長度,返回虛擬地址
    gpiog_data =  gpiog_con+1;

    return 0;
}

/*
函數名:drv_keyled_exit
功能:卸載模塊功能(rmmod卸載驅動時調用)
*/
static void drv_keyled_exit(void)
{
    unregister_chrdev(major, "drv_keyled");
    class_device_unregister(drv_keyled_class_device);
    class_destroy(drv_keyled_class);

    iounmap(gpiof_con);
    iounmap(gpiog_con);

}

6.關鍵驅動函數編寫—–xxx_open(),xxx_read()等函數(即drv_keyled_open,drv_keyled_open等)。

該部分的編寫是編寫驅動源碼的核心內容,實現了對寄存器配置與控制操作。當應用層調用open(),read(),系統就得調用這些對應的函數,因此,這些函數裏的內容以及要實現什麼樣的功能,由用戶自行發揮。該實驗將這些函數定義成如下功能:

  • drv_keyled_open(): 實現按鍵和LED的引腳配置與初始化操作。
  • drv_keyled_read(): 實現對按鍵電平狀態的讀取。
  • drv_keyled_write(): 實現對LED燈的亮滅控制。 (注:實驗只用到了open,read,write三個常用函數,系統其實還提供了很多其他函數,可以見file_operations結構體裏的內容)

源碼如下:

/************驅動關鍵函數實現************/
/*
函數名:drv_keyled_open
功能:配置引腳功能
*/
int drv_keyled_open(struct inode *inode, struct file *file)
{
    char key_dat=1;
    printk("drv_keyled_open2\n");

    //GPF4.5.6
    *gpiof_con |= (1<<2*4) | (1<<2*5) | (1<<2*6) ;    //led 輸出
    *gpiof_data &= ((~(1<<4)) & (~(1<<5)) & (~(0<<6)));      //初始化2個點亮

    //GPG3,GPP0,GPP2
    *gpiog_con |= (0x3<<2*11) ;    //按鍵

    return 0;
}


/*
函數名:drv_keyled_write
功能:控制led亮滅,實現反轉
*/
static ssize_t drv_keyled_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
    unsigned char key_data[2]={1,1};
    Uint32 data=*gpiof_data;

    //測試:讀取用戶傳入的按鍵值
    copy_from_user(key_data, buf, count);
    printk("kernel:the key is:%d , %d\n",key_data[0] ,key_data[1] ); //檢驗用戶空間傳輸的按鍵值

    //實現反轉led電平(反轉指定位電平同時其他的位不影響)
    *gpiof_data |= (1<<4) | (1<<5) |(1<<6);
    *gpiof_data &= ~(data & ((1<<4) | (1<<5) |(1<<6))); //

    return 0;

}

/*
函數名:drv_keyled_read
功能:讀key電平
*/
static ssize_t drv_keyled_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
    unsigned char key_data[2]={1,1};
    unsigned char keybuf=0;

    if(size != sizeof(key_data))//用戶空間(應用程序)要讀取的字節與內核空間存的字節數一致
    {
        printk("read size has err\n");
        return -EINVAL; //返回錯誤
    }

    //把讀取的按鍵值發給用戶空間(應用端)
    key_data[0] = (*gpiof_data & (1<<0)) ? 1 : 0;
    key_data[1] = (*gpiog_data & (1<<3)) ?  1 : 0;

    if( key_data[0] == 0)
    {
         printk("key1 press \n");
        while(!keybuf)//按下彈出
        {
          keybuf= (*gpiof_data & (1<<0)) ? 1 : 0;
        }

    }
    if( key_data[1] == 0)
    {
        printk("key2 press \n");
        while(!keybuf)//按下彈出
        {
          keybuf= (*gpiog_data & (1<<3)) ? 1 : 0;
        }

    }

    copy_to_user(buf, key_data, sizeof(key_data));//copy_to_user(用戶空間,內核空間,字節數)

    return sizeof(key_data);
}

用戶空間與內核空間進行消息傳遞的關鍵函數:copy_from_user()和copy_to_user()。

  • copy_from_user():通常在xxx_write()函數內調用,實現用戶空間的數據到內核空間的傳遞,傳遞的數據存在buf的形參中。copy_from_user()可以將buf中的數據讀出來。

  • copy_to_user(): 通常在xxx_read()函數內調用,實現用戶空間讀取內核空間的數據,copy_to_user()是將內核裏的數據存於buf中,提供用戶層read()的讀取。

7.聲明驅動(模塊的許可證聲明),聲明後,某塊才能被正常安裝到系統內核。固定格式如下:

MODULE_LICENSE(“GPL”);

8.編寫驅動的makefile文件
同樣地,找一寫好的驅動makefile模板,修改主機上存儲開發板的linux源碼目錄(第一行),並更改obj-m選項(最後一行),代碼如下:

KERN_DIR = /work/mysystem/linux-2.6.22.6

all:
    make -C $(KERN_DIR) M=`pwd` modules 

clean:
    make -C $(KERN_DIR) M=`pwd` modules clean
    rm -rf modules.order

obj-m   += drv_keyled.o

obj-m的主要功能:將驅動代碼編譯成模塊(.ko文件),與obj-m對立的是:obj-y,是將代碼直接編譯到內核中。


應用測試程序編寫

1.新建以應用程序文件,名爲:keyled_test.c ,同樣找一應用程序模板,把必要頭文件包含進來,如下:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

2.編寫內容,根據驅動提供的幾個關鍵函數功能,用戶層需要調取這些功能函數,完成最終實現。實驗目的是按下按鍵,反轉LED燈,因此需要調用read函數,讀出內核傳遞過來的數據(按鍵值),根據按鍵值是否按下,調用write函數,實現對led控制。
程序開始需要通過設備文件,打開對應的驅動,然後返回一句柄fd,程序可以通過fd完成設備的讀寫操作。

形參:int argc, char **argv的作用:

當系統啓動應用程序時,可以在啓動程序文件的後面加上要傳入的參數,這些參數就會傳進int argc, char **argv的形參中,其中:
(注:文件名的本身也是個參數,因此參數至少1個,啓動程序文件方式見後面操作)

  • argc:顯示的是傳入參數的個數
  • 指針argv[i]:顯示第i個參數的字符串內容

源碼如下:

int main(int argc, char **argv)
{
    int fd;
    unsigned char key_data[2]={1,1};

    fd = open("/dev/keyled", O_RDWR);  //應用端通過識別設備文件來識別驅動
    if (fd < 0)
    {
        printf("can't open!\n");
    }


    if(argc == 2)//argc[0]爲文件名本身
    {
        printf("argv=%s\n",argv[1]);
    }
    else if(argc == 3)
    {
        printf("argv=%s , %s\n",argv[1],argv[2]);
    }

    printf("hello word fd=%d\n",fd);

    while(1)
    {

        read(fd,key_data,sizeof(key_data));
        if(key_data[0] == 0 || key_data[1] == 0)
        {
            printf("user:k1=%d k2=%d\n",key_data[0],key_data[1]);//檢驗內核到用戶空間的數據傳輸
            write(fd,key_data,sizeof(key_data));
        }

    }

    return 0;

}

3.測試程序的makefile
採用arm-linux-gcc編譯器進行編譯,生成可執行程序文件keyled_test ,內容如下:

CROSS.=arm-linux-

keyled_test : keyled_test.c
    $(CROSS)gcc -o  $@  $<
clean:
    rm keyled_test

裝載驅動程序與運行測試程序

make編譯驅動模塊以及測試程序後,分別生成.ko文件和可執行文件,這兩個文件需要存放在根文件系統上,分爲完成驅動裝載和運行測試程序。
開發板掛載根文件系統,通常採用NFS網絡方式直接掛載到PC主機上,簡單方便,並大大減少程序調試時間,但是筆者由於路由器問題,無法建立主機和開發板的網絡連接,只能採用直接下載根文件系統到開發板上運行勒,很麻煩,每一次調試都得重新下一次,簡直崩潰o(╥﹏╥)o,下面是驅動裝載與測試程序運行具體步驟:

1.在製作好的根文件系統上新建以任意名稱的文件夾:/moduel,將.ko文件和應用程序執行文件存在該目錄中。
這裏寫圖片描述

2.如果採用NFS掛載方式,可以省略此步。該步主要將文件系統下載在開發板中,因此需要在主機上把製作好的根文件系統轉成映像文件(採用mkyaffs2image工具),然後由dnw工具,通過usb方式 下載到開發板中。(篇幅有限,具體過程略述啦^_^,能用NFS最好咯)。

3.啓動開發板(uboot,kernel,文件系統都下載並裝載好),操作系統啓動後,在終端上進行驅動裝載(關鍵命令:insmod),如下:
這裏寫圖片描述

insmod爲驅動裝載,lsmod爲查看裝載情況

4.運行測試程序,如下圖所示,圖中可以觀察到如下幾個結論:

  • 運行的程序名稱後可加傳入的參數,如圖所示的參數爲:“iu”和“tr”。
  • 按下按鍵時,可以終端打印出相關信息,這些信息都是編寫程序時實現的。
  • 圖中顯示的kernel:…. 和user:…. 分別是內核空間與用戶空間之間傳遞的數據信息,可以看出它們傳遞的數據是按鍵值,並且結果正確的。

這裏寫圖片描述

4.開發板測試結果,激動人心的當然是開發板的實際運行效果啦,隨着兩個按鍵的任意按下,3個LED等就會翻轉,看看效果(^▽^):
這裏寫圖片描述
這裏寫圖片描述


其它工作:驅動卸載與設備、進程查看等

1.查看設備與設備文件。設備顯示在/proc/devices虛擬文件系統內,設備文件可以在/dev目錄下查看。
可以看出主設備號爲252,次設備號爲0
這裏寫圖片描述

2.查看當前測試程序的進程和cpu佔有率,此時啓動的應用程序需要在後臺運行(末位加上&字符),才能到前端查看進程和cpu佔有率。
可以看出:進程號爲791,cpu佔有率爲98%。

這裏寫圖片描述
1.驅動卸載(從內核中卸載掉該驅動)與關閉進程(退出測試程序運行,通過進程號關閉)。
這裏寫圖片描述

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