Linux設備驅動開發基礎---字符設備驅動程序開發之基於中斷的按鍵驅動

二、硬件原理分析

Mini2440 具有6 個用戶測試按鍵,它們都是連接到CPU 的中斷引腳。如圖:

由原理圖可知,這些引腳在按鍵沒有按下的情況下被上拉爲高電平,按鍵被按下的時候變爲低電平。

三、實現方式

1、在/linux-2.6.32.2/drivers/buttons目錄下創建一個新的驅動程序文件mini2440_buttons.c,內容及詳細註釋如下:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/poll.h>
#include <linux/irq.h>
#include <asm/irq.h>
#include <linux/interrupt.h>
#include <asm/uaccess.h>
#include <mach/regs-gpio.h>
#include <mach/hardware.h>
#include <linux/platform_device.h>
#include <linux/cdev.h>
#include <linux/sched.h>
#include <linux/gpio.h>
#include <linux/device.h>


/*創建類和設備*/
static struct class *button_class;
static struct device *button_class_dev;


#define DEVICE_NAME "buttons"  //加載模式後,執行”cat /proc/devices”命令看到的設備名稱 


/*
*定義中斷所用的結構體
*/
struct button_irq_desc {
 int irq;          //按鍵對應的中斷號
 int pin;          //按鍵所對應的GPIO 端口
 int number;       //定義鍵值,以傳遞給應用層/用戶態
 char *name;       //每個按鍵的名稱
};


/*
*結構體實體定義
*/
static struct button_irq_desc button_irqs[]={
 {IRQ_EINT8 , S3C2410_GPG(0)  , 0, "K1"},
 {IRQ_EINT11, S3C2410_GPG(3)  , 1, "K2"},
 {IRQ_EINT13, S3C2410_GPG(5)  , 2, "K3"},
 {IRQ_EINT14, S3C2410_GPG(6)  , 3, "K4"},
 {IRQ_EINT15, S3C2410_GPG(7)  , 4, "K5"},
 {IRQ_EINT19, S3C2410_GPG(11) , 5, "K6"},
};


/*
*開發板上按鍵的狀態變量,注意這裏是'0',對應的ASCII 碼爲30
*/
static volatile char key_values[] = {'0', '0', '0', '0', '0', '0'};


/*
*因爲本驅動是基於中斷方式的,在此創建一個等待隊列,以配合中斷函數使用;當有按鍵按下並讀取到鍵
*值時,將會喚醒此隊列,並設置中斷標誌,以便能通過read函數判斷和讀取鍵值傳遞到用戶態;當沒有按
*鍵按下時,如果有進程調用mini2440_buttons_read函數,它將休眠
*/
static DECLARE_WAIT_QUEUE_HEAD(button_waitq);


/*
*中斷標識變量,配合上面的隊列使用,中斷服務程序會把它設置爲1,read 函數會把它清零
*/
static volatile int ev_press = 0;


/*
*按鍵驅動的中斷服務程序
*/
static irqreturn_t button_interrupt(int irq, void *dev_id)
{
 struct button_irq_desc *button_irqs = (struct button_irq_desc *)dev_id;
 int down;
 
 /*獲取被按下的按鍵狀態*/ 
 down = !s3c2410_gpio_getpin(button_irqs->pin);
 
 /*狀態改變,按鍵被按下,從這句可以看出,當按鍵沒有被按下的時候,寄存器的值爲1(上拉),當按
  鍵被按下的時候,寄存器對應的值爲0*/
 if (down != (key_values[button_irqs->number] & 1)) { // Changed


  /*如果key1 被按下,則key_value[0]就變爲'1',對應的ASCII 碼爲31*/


  key_values[button_irqs->number] = '0' + down;
  ev_press = 1; /*設置中斷標誌爲1*/
  
  wake_up_interruptible(&button_waitq); /*喚醒等待隊列*/
 }
 return IRQ_RETVAL(IRQ_HANDLED);/*具體解釋見LDD3p272,增加了中斷返回,來區別那些僞中斷,
                                    驅動程序正在處理那個中斷時卻返回了 IRQ_NONE,說明存在bug*/
}


/*
*在應用程序執行open("/dev/buttons",…)時會調用到此函數,在這裏,它的作用主要是註冊6 個按鍵的中斷。
*所用的中斷類型是IRQ_TYPE_EDGE_BOTH,也就是雙沿觸發,在上升沿和下降沿均會產生中斷,這樣做
*是爲了更加有效地判斷按鍵狀態
*/
static int button_open(struct inode *inode, struct file *file)
{
 int i;
 int err = 0;


 for (i = 0; i < sizeof(button_irqs)/sizeof(button_irqs[0]); i++) {


  /*註冊中斷函數,如果註冊成功,則6個按鍵GPIO引腳的功能被設置爲外部中斷,觸發方式爲設定的觸發方式
    詳解見韋東山完全開發手冊P406*/  
  err = request_irq(button_irqs[i].irq, button_interrupt, IRQ_TYPE_EDGE_BOTH,
        button_irqs[i].name, (void *)&button_irqs[i]);
  
  if (err)
   break;
 }
 
 if (err) { /*如果出錯,釋放已經註冊的中斷,並返回*/
  i--;
  for (; i >= 0; i--) {
   disable_irq(button_irqs[i].irq);
   free_irq(button_irqs[i].irq, (void *)&button_irqs[i]);
  }
  
  return -EBUSY;
 }
 
 /*註冊成功,則中斷隊列標記爲1,表示可以通過read 讀取*/ 
 ev_press = 1;
 /*正常返回*/
 return 0;
}


/*
*此函數對應應用程序的系統調用close(fd)函數,在此,它的主要作用是當關閉設備時釋放6 個按鍵的中斷*
*處理函數
*/
static int button_close(struct inode *inode, struct file *file)
{
 int i;
 for (i = 0; i < sizeof(button_irqs)/sizeof(button_irqs[0]); i++) {
  if (button_irqs[i].irq < 0) {
   continue;
  }
  /*釋放中斷號,並註銷中斷處理函數*/
  free_irq(button_irqs[i].irq, (void *)&button_irqs[i]);
 }
 return 0;
}


/*
*對應應用程序的read(fd,…)函數,主要用來向用戶空間傳遞鍵值
*/
static int button_read(struct file *filp, char __user *buff, size_t count, loff_t *offp)
{
 unsigned long err;
 if (!ev_press) {
  if (filp->f_flags & O_NONBLOCK)
   /*當中斷標識爲0 時,並且該設備是以非阻塞方式打開時,返回*/
   return -EAGAIN;
  else
   /*當中斷標識爲0 時,並且該設備是以阻塞方式打開時,進入休眠狀態,等待被喚醒*/
   wait_event_interruptible(button_waitq, ev_press);
 }
 /*把中斷標識清零*/
 ev_press = 0;
 /*一組鍵值被傳遞到用戶空間*/
 err = copy_to_user(buff, (const void *)key_values, min(sizeof(key_values), count));
 return err ? -EFAULT : min(sizeof(key_values), count);
}


/* 這個結構是字符設備驅動程序的核心
 * 當應用程序操作設備文件時所調用的open、read、write等函數,
 * 最終會調用這個結構中的對應函數
 */
static struct file_operations buttons_fops = {
    .owner   =   THIS_MODULE,    /* 這是一個宏,指向編譯模塊時自動創建的__this_module變量 */
    .open    =   button_open,
    .release =   button_close, 
    .read    =   button_read,
};


/*
 * 執行“insmod mini2440_buttons.ko”命令時就會調用這個函數
 */
 int button_major;
static int __init button_init(void)
{
    /* 註冊字符設備驅動程序
     * 參數爲主設備號、設備名字、file_operations結構;
     * 這樣,主設備號就和具體的file_operations結構聯繫起來了,
     * 操作主設備爲BUTTON_MAJOR的設備文件時,就會調用mini2440_buttons_fops中的相關成員函數
     * BUTTON_MAJOR可以設爲0,表示由內核自動分配主設備號
     */
    //ret = register_chrdev(BUTTON_MAJOR, DEVICE_NAME, &mini2440_buttons_fops);
    button_major = register_chrdev(0, DEVICE_NAME, &buttons_fops);
   
    if (button_major < 0) {
      printk(DEVICE_NAME " can't register major number\n");
      return button_major;
    }


    button_class = class_create(THIS_MODULE, DEVICE_NAME); //創建類
    button_class_dev = device_create(button_class, NULL, MKDEV(button_major, 0), NULL, DEVICE_NAME); /* 創建設備/dev/buttons */


    printk(DEVICE_NAME " initialized\n");
    return 0;
}


/*
 * 執行”rmmod mini2440_buttons.ko”命令時就會調用這個函數 
 */
static void __exit button_exit(void)
{
    /* 卸載驅動程序 */
    device_unregister(button_class_dev); //刪除設備
    class_destroy(button_class);           //刪除類,順序不能顛倒
    unregister_chrdev(button_major, DEVICE_NAME);
}


/* 這兩行指定驅動程序的初始化函數和卸載函數 */
module_init(button_init);
module_exit(button_exit);


/* 描述驅動程序的一些信息,不是必須的 */
MODULE_AUTHOR("DreamCatcher");             // 驅動程序的作者
MODULE_DESCRIPTION("MINI2440 BUTTON Driver");   // 一些描述信息
MODULE_LICENSE("GPL"); //版權信息


2、編寫本目錄下的Makefile文件

KERN_DIR = /work/armlinux/linux-2.6.32.2

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

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

obj-m := mini2440_buttons.o

3、測試

爲了測試該驅動程序,我們還需要編寫一個簡單的測試程序,在友善官方提供的光盤中已經提供了該測試程序的源代碼

 #include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/time.h>
#include <errno.h>
int main(void)
{
 int buttons_fd;
 char buttons[6] = {'0', '0', '0', '0', '0', '0'}; //定義按鍵值變量,對於驅動函數中的key_values 數組
 buttons_fd = open("/dev/buttons", 0); /*打開按鍵設備/dev/buttons*/
 if (buttons_fd < 0) {
  perror("open device buttons"); /*打開失敗則退出*/
  exit(1);
 }
 for (;;) { /*永讀按鍵並打印鍵值和狀態*/
  char current_buttons[6];
  int count_of_changed_key;
  int i;
/*使用read 函數讀取一組按鍵值(6 個)*/
  if (read(buttons_fd, current_buttons, sizeof current_buttons) != sizeof current_buttons) {
   perror("read buttons:");
   exit(1);
  }
/*逐個分析讀取到的按鍵值*/
  for (i = 0, count_of_changed_key = 0; i < sizeof buttons / sizeof buttons[0]; i++) {
   if (buttons[i] != current_buttons[i]) {
    buttons[i] = current_buttons[i];
/*打印按鍵值,並標明按鍵按下/擡起的狀態*/
    printf("%skey %d is %s", count_of_changed_key? ", ": "", i+1, buttons[i] == '0' ? "up" : "down");
    count_of_changed_key++;
   }
  }
  if (count_of_changed_key) {
   printf("\n");
  }
 }
 close(buttons_fd); /*關閉按鍵設備文件*/
 return 0;
}

編譯後進行測試:

root@MINI2440:/lib/modules/2.6.32.2# insmod mini2440_buttons.ko
buttons initialized
root@MINI2440:/lib/modules/2.6.32.2# cd /opt
root@MINI2440:/opt# ls
Helloword       backlight_test  buttons_test    led_test
root@MINI2440:/opt# ./buttons_test
key 1 is down
key 1 is up
key 2 is down
key 2 is up
key 3 is down
key 3 is up

此驅動程序還存在不少問題,首先對於request_irq函數的用法,目前只查到韋東山的書中描述的用法,在網上沒有查到關於參數flags,如設置爲中斷的觸發方式時,會調用irq_desc[irq]結構中的chip->set_type成員函數進行設置:設置引腳功能爲外部中斷,設置中斷觸發方式。當然這樣設置沒有錯誤,否則驅動也不能正常運行了,自己對於內核的講解也是一知半解,後續將按照網上大部分的設置方式對驅動進行更改。

其次,按鍵沒有去抖動,後續將參照網上的方法進行更改。

 

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