1. 程式人生 > 其它 >Mini2440之linux驅動移植-按鍵中斷驅動

Mini2440之linux驅動移植-按鍵中斷驅動

一、按鍵硬體資源

1.1 硬體接線

檢視Mini2440原理圖、S3C2440資料手冊,瞭解如何讀取按鍵的狀態。這裡粗略介紹一下Mini2440 K1~K14的接線方式,以及暫存器的設定,這裡簡單說一下,就不具體介紹了:

  • K1~K6依次對應引腳GPG0、GPG3、GPG5、GPG6、GPG7、GPG11,以K1為例;
  • 按鍵按下引腳輸入低電平、按鍵鬆開引腳輸入高電平;
  • 配置控制暫存器GPGCON(0x56000060)的bit[1:0]=00,使GPB5引腳為輸入模式;
  • 讀取配置資料暫存器GPGDAT(0x56000064)的bit0的電位;

二、讀取按鍵狀態方式

試想一下,如果我們想判斷按鍵K1有沒有按下,我們有幾種方式:

  • 通過輪詢的方式,不停的去GPGDATA暫存器的狀態,從而得到K1按鍵的狀態;這種方式會導致CPU佔用率急劇升高;
  • 採用中斷的方式,按鍵按下時K1為低電平,鬆開時為高電平,採用雙邊沿觸發方式;可以極大的提高CPU執行效率;

三、liunux中的中斷

3.1 中斷介紹

中斷就是CPU正常執行期間,由於內、外部事件引起的CPU暫時停止正在執行的程式,去執行該內部事件或外部事件的引起的服務中去,服務執行完畢後再返回斷點處繼續執行的情形。

中斷不屬於任何一個程序,因此不能在中斷程式中休眠和呼叫schedule函式放棄CPU,實現中斷處理函式有一個原則,就是儘可能的處理並返回。

3.2 Linux中斷處理程式架構

linux作業系統是多個程序執行,巨集觀上達到並行執行的狀態,外設中斷則會打斷核心中任務排程和執行,如果中斷函式耗時過長則使得系統實時性和併發性降低。

為保證系統實時性,中斷服務程式必須足夠簡短,但實際應用中某些時候發生中斷時必須處理大量的事務,這時候如果都在中斷服務程式中完成,則會嚴重降低中斷的實時性,基於這個原因,linux系統提出了一個概念:把中斷服務程式分為兩部分-頂半部-底半部。

  • 頂半部:完成儘可能少的比較緊急的任務,它往往只是簡單的讀取暫存器中的中斷狀態並清除中斷標誌後就進行”登記中斷“(也就是將底半部處理程式掛到裝置的底半部執行佇列中)的工作;
  • 底半部:中斷處理的大部分工作都在底半部,它幾乎做了中斷處理程式的所有事情;

Linux中斷頂-底部分區別:

  • 頂半部由外設中斷觸發,底半部由頂半部觸發;
  • 頂半部不會被其他中斷打斷,底半部是可以被打斷的;
  • 頂半部分處理任務要快,主要任務、耗時任務放在底半部;

一般來說,在中斷頂半部執行完畢,底半部即在核心的排程下被執行,當然如果有其他更高優先順序需處理的任務,會先處理該任務再排程處理底半部,或者在系統空閒時間進行處理。

3.3 linux中斷處理程式設計原則

對於Linux系統裝置而言,一個完整的中斷程式由頂半部和底半部分共同構成,在編寫裝置驅動程式前,就需考慮好頂半部和底半部的分配。很多時候頂半部與底半部並沒有嚴格的區分界限,主要由程式設計師根據實際設計,如某些外設中斷可以沒有底半部。關於頂底半部的劃分原則,就是主要事務、耗時事務劃分在底半部處理。可以參考以下原則:

  • 與硬體相關的操作,如操作暫存器,必須放在頂半部;
  • 對時間敏感、要求實時性的任務放在頂半部;
  • 該任務不能被其他中斷或者程序打斷的放在頂半部;
  • 實時性要求不高的任務、耗時任務放在底半部;

四、按鍵中斷驅動程式

這裡我們先動手跟著學習按鍵中斷程式的編寫,熟悉一下中斷程式編寫的流程。至於具體中斷的實現原理,我們後面文章會進行介紹。

在/work/sambashare/drivers下新建3.button_dev資料夾,用來編寫我們的按鍵中斷程式。

4.1 註冊中斷

GPG0、GPG3、GPG5、GPG6、GPG7、GPG11對應的外部中斷依次為EINT8、EINT11、EINT13、EINT14、EINT15、EINT19。

這裡通過request_irq函式註冊GPG0、GPG3、GPG5、GPG6、GPG7、GPG11為外部中斷,觸發方式為雙邊沿。

/* 
   GPG0、GPG3、GPG5、GPG6、GPG7、GPG11配置為中斷功能 
   IRQ_EINTX 定義在 arch/arm/mach-s3c24xx/include/mach/irqs.h
*/
static int button_open(struct inode *inode, struct file *file)
{
    /* 註冊中斷 */
    request_irq(IRQ_EINT8,button_irq,IRQT_BOTHEDGE,"K1",&pins_desc[0]);
    request_irq(IRQ_EINT11,button_irq,IRQT_BOTHEDGE,"K2",&pins_desc[1]);
    request_irq(IRQ_EINT13,button_irq,IRQT_BOTHEDGE,"K3",&pins_desc[2]);
    request_irq(IRQ_EINT14,button_irq,IRQT_BOTHEDGE,"K4",&pins_desc[3]);
    request_irq(IRQ_EINT15,button_irq,IRQT_BOTHEDGE,"K5",&pins_desc[4]);
    request_irq(IRQ_EINT19,button_irq,IRQT_BOTHEDGE,"K6",&pins_desc[5]);

    return 0;
}

函式request_irq原型:

int request_irq(unsigned int irq, irq_handler_t handler, unsigned long irqflags, const char *devname, void *dev_id)

其中:

  • irq:要分配的硬體中斷號,定義在arch/arm/mach-s3c24xx/include/mach/irqs.h,被linux/irq.h呼叫;
  • handler:向系統登記的中斷處理程式,這是一個回撥函式,中斷髮生時,系統呼叫這個函式;
  • irqflags:標記中斷處理屬性的標誌位,這裡設定為IRQT_BOTHEDGE、雙邊沿觸發;
  • dev_name:驅動裝置的字串名稱,用來在/pro/interrupt/xx中顯示中斷的所有者;
  • dev_id:用於共享中斷號的非NULL標識,若中斷沒有被共享,dev_id可以設定為NULL。共享中斷號時,該標識必須是全域性唯一的,在釋放中斷時會用到它。驅動程式也可以用它來指向自己的私有資料區。一般將它設定為這個裝置device結構本身,中斷處理程式可以用dev_id找到相應的產生這個中斷的裝置;

這裡我們EINT8、EINT11、EINT13、EINT14、EINT15、EINT19中斷共用一箇中斷處理程式,因此我們將dev_id欄位設定為一個結構體,用來標識唯一裝置。

struct pin_desc{
    unsigned int pin;
    unsigned int key_val;
};

/*
 * S3C2410_GPG(x) 定義在arch/arm/mach-s3c24xx/include/mach/gpio-samsung.h
 */
static struct pin_desc pins_desc[4] = {
    {S3C2410_GPG(0), 0x01},
    {S3C2410_GPG(3), 0x02},
    {S3C2410_GPG(5), 0x03},
    {S3C2410_GPG(6), 0x04},
    {S3C2410_GPG(7), 0x05},
    {S3C2410_GPG(11), 0x06},
};

pin_desc用來儲存當前裝置的引腳名稱和初始值(並不是引腳電平值,這是一個虛擬的值,可以用來區分是哪個按鍵)。

4.2 釋放中斷

我們在.close函式中通過free_irq函式進行釋放中斷資源:

/*
 * 釋放中斷資源
 */
int button_close(struct inode *inode, struct file *file)
{
    free_irq(IRQ_EINT8, &pins_desc[0]);
    free_irq(IRQ_EINT11, &pins_desc[1]);
    free_irq(IRQ_EINT13, &pins_desc[2]);
    free_irq(IRQ_EINT14, &pins_desc[3]);
    free_irq(IRQ_EINT15, &pins_desc[4]);
    free_irq(IRQ_EINT19, &pins_desc[5]);

    return 0;
}

4.3 中斷處理程式

中斷髮生時,系統呼叫這個函式,傳入的引數包括硬體的中斷號,dev_id。dev_id是request_ird函式傳遞給系統的引數的引數dev_id。

/*
 * 中斷處理服務
 */
static irqreturn_t button_irq(int irq, void *dev_id)
{
    struct pin_desc *pindesc = (struct pin_desc *)dev_id;
    unsigned int pinval;

   pinval = gpio_get_value(pindesc->pin);
 
    if (pinval){
        /* 鬆開 */
        key_val = 0x80 | pindesc->key_val;
    }
    else{
        /* 按下 */
        key_val = pindesc->key_val;
    }
 
    ev_press = 1;                           /* 表示中斷髮生了,退出等待佇列 */
    wake_up_interruptible(&button_waitq);   /* 喚醒休眠的程序 */
 
    return IRQ_RETVAL(IRQ_HANDLED);
}

這裡通過gpio_get_value獲取引腳的電平。

s3c2410_gpio_getpin(unsigned int pin);  

其中:

  • pin: 引腳名稱,例如:S3C2410_GPG(0),定義在<mach/gpio-samsung.h>;

如果引腳為高電平,我們就將key_val裝置值設定為0x8x,否則為初始值,key_val宣告如下:

/* 鍵值: 按下時, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06 */
/* 鍵值: 鬆開時, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86 */
static unsigned char key_val;

此外,中斷髮生了,我們設定全域性變數ev_press為1。然後從button_waitq佇列中喚醒休眠的程序。等待佇列的宣告如下:

/* 定義並初始化等待佇列    */
static DECLARE_WAIT_QUEUE_HEAD(button_waitq);

然後呼叫wake_up_interruptible喚醒以button_waitq等待佇列中的所有程序,並將程序的狀態設定為TASK_RUNNING。

4.4 button_read函式

static ssize_t button_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
    int count;
    if (size != 1)     // 需要讀取長度
        return -EINVAL;
 
    /* 如果沒有按鍵動作, 休眠 */
    wait_event_interruptible(button_waitq, ev_press);
 
    /* 如果有按鍵動作, 上傳key_val給使用者層 */
    count = copy_to_user(buf, &key_val, 1);

    /* 資料發完後,立馬設為休眠狀態,避免誤操作 */
    ev_press = 0;   
    
    return count;
}

wait_event_interruptible函式第二個引數為condition:

  • 條件condition為真時呼叫這個函式將直接返回0;
  • 條件condition為假時等待佇列中的所有程序進入休眠狀態;

當程序喚醒後,上傳上傳key_val給使用者層。資料發完後,立馬設為休眠狀態,避免誤操作。

4.5註冊button驅動程式

static struct file_operations button_fops = {
    .owner   =   THIS_MODULE,
    .open    =   button_open,
    .read    =   button_read,
    .release =   button_close,
};

static dev_t devid;                      // 起始裝置編號
static struct cdev button_cdev;          // 儲存操作結構體的字元裝置 
static struct class *button_cls;

static int button_init(void)
{
    
    /* 動態分配字元裝置: (major,0) */
    if(OK == alloc_chrdev_region(&devid, 0, 1,"button")){   // ls /proc/devices看到的名字
        printk("register_chrdev_region ok\n");
    }else {
        printk("register_chrdev_region error\n");
        return ERROR;
    }
    
     cdev_init(&button_cdev, &button_fops);
     cdev_add(&button_cdev, devid, 1);


    /* 建立類,它會在sys目錄下建立/sys/class/button這個類  */
     button_cls = class_create(THIS_MODULE, "button");
     if(IS_ERR(button_cls)){
         printk("can't create class\n");
         return ERROR;
     }
    /* 在/sys/class/button下建立buttons裝置,然後mdev通過這個自動建立/dev/buttons這個裝置節點 */
     device_create(button_cls, NULL, devid, NULL, "buttons"); 
return 0;
}

4.6 解除安裝button驅動程式

static void __exit button_exit(void)
{
    printk("button driver exit\n");
    /* 登出類、以及類裝置 /sys/class/button會被移除*/
    device_destroy(button_cls, devid);
    class_destroy(button_cls);

    cdev_del(&button_cdev);
    unregister_chrdev_region(devid, 1);
    return;
}

4.7 完整程式碼

button_dev.c:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/io.h>
#include <linux/uaccess.h>
#include <linux/gpio.h>
#include <linux/irq.h>        // 包含了mach/irqs.h
#include <linux/interrupt.h>
#include <linux/gpio/machine.h>
#include <mach/gpio-samsung.h>
/*
 全域性靜態變數:希望全域性變數僅限於在本原始檔中使用,在其他原始檔中不能引用,也就是說限制其作用域只在
 定義該變數的原始檔內有效,而在同一源程式的其他原始檔中不能使用
 */
#define OK   (0)
#define ERROR  (-1)
#define __IRQT_FALEDGE IRQ_TYPE_EDGE_FALLING
#define __IRQT_RISEDGE IRQ_TYPE_EDGE_RISING
#define __IRQT_LOWLVL IRQ_TYPE_LEVEL_LOW
#define __IRQT_HIGHLVL IRQ_TYPE_LEVEL_HIGH
#define IRQT_NOEDGE (0)
#define IRQT_RISING (__IRQT_RISEDGE)
#define IRQT_FALLING (__IRQT_FALEDGE)
#define IRQT_BOTHEDGE (__IRQT_RISEDGE|__IRQT_FALEDGE)
#define IRQT_LOW (__IRQT_LOWLVL)
#define IRQT_HIGH (__IRQT_HIGHLVL)
#define IRQT_PROBE IRQ_TYPE_PROBE

/* 定義並初始化等待佇列    */
static DECLARE_WAIT_QUEUE_HEAD(button_waitq);

 /*
  * 定義中斷事件標誌
  * 0:進入等待佇列        1:退出等待佇列
  */
static int ev_press =0; 

/* 鍵值: 按下時, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06 */
/* 鍵值: 鬆開時, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86 */
static unsigned char key_val;

struct pin_desc{
    unsigned int pin;
    unsigned int key_val;
};

/*
 * S3C2410_GPG 定義在arch/arm/mach-s3c24xx/include/mach/gpio-samsung.h
 */
static struct pin_desc pins_desc[4] = {
    {S3C2410_GPG(0), 0x01},
    {S3C2410_GPG(3), 0x02},
    {S3C2410_GPG(5), 0x03},
    {S3C2410_GPG(6), 0x04},
    {S3C2410_GPG(7), 0x05},
    {S3C2410_GPG(11), 0x06},
};

/*
 * 中斷處理服務
 */
static irqreturn_t button_irq(int irq, void *dev_id)
{
    struct pin_desc *pindesc = (struct pin_desc *)dev_id;
    unsigned int pinval;

   pinval = gpio_get_value(pindesc->pin);
 
    if (pinval){
        /* 鬆開 */
        key_val = 0x80 | pindesc->key_val;
    }
    else{
        /* 按下 */
        key_val = pindesc->key_val;
    }
 
    ev_press = 1;                           /* 表示中斷髮生了,退出等待佇列 */
    wake_up_interruptible(&button_waitq);   /* 喚醒休眠的程序 */
 
    return IRQ_RETVAL(IRQ_HANDLED);
}

/* 
   GPG0、GPG3、GPG5、GPG6、GPG7、GPG11配置為中斷功能 
   IRQ_EINTX 定義在 arch/arm/mach-s3c24xx/include/mach/irqs.h
*/
static int button_open(struct inode *inode, struct file *file)
{
    /* 註冊中斷 */
    request_irq(IRQ_EINT8,button_irq,IRQT_BOTHEDGE,"K1",&pins_desc[0]);
    request_irq(IRQ_EINT11,button_irq,IRQT_BOTHEDGE,"K2",&pins_desc[1]);
    request_irq(IRQ_EINT13,button_irq,IRQT_BOTHEDGE,"K3",&pins_desc[2]);
    request_irq(IRQ_EINT14,button_irq,IRQT_BOTHEDGE,"K4",&pins_desc[3]);
    request_irq(IRQ_EINT15,button_irq,IRQT_BOTHEDGE,"K5",&pins_desc[4]);
    request_irq(IRQ_EINT19,button_irq,IRQT_BOTHEDGE,"K6",&pins_desc[5]);

    return 0;
}

static ssize_t button_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
    int count;
    if (size != 1)
        return -EINVAL;
 
    /* 如果沒有按鍵動作, 休眠, */
    wait_event_interruptible(button_waitq, ev_press);
 
    /* 如果有按鍵動作, 上傳key_val給使用者層 */
    count = copy_to_user(buf, &key_val, 1);

    /* 資料發完後,立馬設為休眠狀態,避免誤操作 */
    ev_press = 0;   
    
    return count;
}

/*
 * 釋放中斷資源
 */
int button_close(struct inode *inode, struct file *file)
{
    free_irq(IRQ_EINT8, &pins_desc[0]);
    free_irq(IRQ_EINT11, &pins_desc[1]);
    free_irq(IRQ_EINT13, &pins_desc[2]);
    free_irq(IRQ_EINT14, &pins_desc[3]);
    free_irq(IRQ_EINT15, &pins_desc[4]);
    free_irq(IRQ_EINT19, &pins_desc[5]);

    return 0;
}


static struct file_operations button_fops = {
    .owner   =   THIS_MODULE,
    .open    =   button_open,
    .read    =   button_read,
    .release =   button_close,
};

static dev_t devid;                      // 起始裝置編號
static struct cdev button_cdev;          // 儲存操作結構體的字元裝置 
static struct class *button_cls;

static int button_init(void)
{
    
    /* 動態分配字元裝置: (major,0) */
    if(OK == alloc_chrdev_region(&devid, 0, 1,"button")){   // ls /proc/devices看到的名字
        printk("register_chrdev_region ok\n");
    }else {
        printk("register_chrdev_region error\n");
        return ERROR;
    }
    
     cdev_init(&button_cdev, &button_fops);
     cdev_add(&button_cdev, devid, 1);


    /* 建立類,它會在sys目錄下建立/sys/class/button這個類  */
     button_cls = class_create(THIS_MODULE, "button");
     if(IS_ERR(button_cls)){
         printk("can't create class\n");
         return ERROR;
     }
    /* 在/sys/class/button下建立buttons裝置,然後mdev通過這個自動建立/dev/buttons這個裝置節點 */
     device_create(button_cls, NULL, devid, NULL, "buttons"); 

     return 0;
}

static void __exit button_exit(void)
{
    printk("button driver exit\n");
    /* 登出類、以及類裝置 /sys/class/button會被移除*/
    device_destroy(button_cls, devid);
    class_destroy(button_cls);

    cdev_del(&button_cdev);
    unregister_chrdev_region(devid, 1);
    return;
}

module_init(button_init);
module_exit(button_exit);
MODULE_LICENSE("GPL");
View Code

4.8 Makefile

KERN_DIR :=/work/sambashare/linux-5.2.8
all:
    make -C $(KERN_DIR) M=`pwd` modules 
clean:
    make -C $(KERN_DIR) M=`pwd` modules clean
    rm -rf modules.order

obj-m += button_dev.o

五、按鍵驅動測試應用程式

在3.button_dev下建立test資料夾,儲存測試應用程式。

5.1 main.c

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

int main(int argc,char **argv)
{
    int fd,ret;
    unsigned int key_val = 0;
    
    fd = open("/dev/buttons", O_RDWR);
    if (fd < 0)
    {
        printf("can't open!\n");
        return -1;
    }
 
    while (1)
    {
        ret = read(fd, &key_val, 1);    // 讀取一個位元組值,(當在等待佇列時,本程序就會進入休眠狀態)
        if(ret < 0){
            printf("read error\n");
            continue;
        }
        printf("key_val = 0x%x\n", key_val);
    }
    
    return 0;
}

5.2 Makefile

all:
    arm-linux-gcc -march=armv4t -o main main.c
clean:
    rm -rf *.o main

六、燒錄開發板測試

按鍵驅動目錄結構如下:

6.1 編譯驅動

執行make命令編譯驅動,並將驅動程式拷貝到nfs檔案系統:

cd /work/sambashare/drivers/3.button_dev
make
cp /work/sambashare/drivers/3.button_dev/led_dev.ko /work/nfs_root/rootfs

安裝驅動:

[root@zy:/]# insmod button_dev.ko
led_dev: loading out-of-tree module taints kernel.
register_chrdev_region ok

檢視裝置節點檔案:

[root@zy:/]# ls /dev/buttons -l
crw-rw----    1 0        0         249,   0 Jan  1 00:00 /dev/buttons

6.2 中斷檢視

執行命令cat /proc/interrupts可以檢視當前系統有哪些中斷服務:

[root@zy:/]# cat /proc/interrupts
           CPU0       
 29:     144431       s3c  13 Edge      samsung_time_irq
 32:          0       s3c  16 Edge      s3c2410-lcd
 42:          0       s3c  26 Edge      ohci_hcd:usb1
 43:          0       s3c  27 Edge      s3c2440-i2c.0
 55:       2897   s3c-ext   7 Edge      eth0
 56:         21   s3c-ext   8 Edge    
 59:          3   s3c-ext  11 Edge    
 61:         20   s3c-ext  13 Edge    
 62:         67   s3c-ext  14 Edge    
 63:          9   s3c-ext  15 Edge    
 67:          6   s3c-ext  19 Edge    
 74:        118  s3c-level   0 Edge      s3c2440-uart
 75:        780  s3c-level   1 Edge      s3c2440-uart
 87:          0  s3c-level  13 Edge      s3c2410-wdt

可以看到我們註冊的外部中斷8、11、13、14、15、19.

6.3 編譯測試應用程式

執行make命令編譯測試應用程式,並將測試應用程式拷貝到nfs檔案系統:

cd test
make
cp ./main /work/nfs_root/rootfs

執行應用程式:

./main

按下按鍵K1:

[root@zy:/]# ./main
key_val = 0x1
key_val = 0x81

按下按鍵K3:

key_val = 0x3
key_val = 0x83

6.4 解除安裝驅動

通過用lsmod可以檢視當前安裝了哪些驅動:

[root@zy:/]# lsmod
button_dev 2521 0 - Live 0xbf000000 (O)

解除安裝時直接執行:

rmmod button_dev

6.5 程式整體執行流程

  • 當呼叫insmod裝載驅動後,會呼叫button_init註冊/dev/buttons字元裝置;
  • ./main執行應用程式後,應用程式執行open進行系統呼叫到button_open,驅動程式呼叫request_irq進行6個按鍵中斷的註冊;
  • 然後應用程式繼續執行read進行系統呼叫到button_read,進而呼叫wait_event_interruptible程式休眠;
  • 只有當按鍵按下或鬆開觸發中斷程式button_irq執行,函式裡呼叫wake_up_interruptible喚醒休眠的程序;
  • button_read然後將驅動裡讀到的按鍵值呼叫copy_to_user拷貝到使用者空間,應用程式再將按鍵值打印出來;

參考文章:

[1]Linux中斷上半部和下半部概念

[2]四、Linux驅動之使用中斷

[3]Linux驅動中的 wait_event_interruptible 與 wake_up_interruptible 深度理解