天天看點

Linux驅動開發|input子系統

input子系統

一、input子系統

1.1 input子系統簡介

input 子系統就是管理輸入的子系統,和 pinctrl、 gpio 子系統一樣,都是 Linux 核心針對某一類裝置而建立的架構。比如按鍵輸入、鍵盤、滑鼠、觸摸屏等等這些都屬于輸入裝置,不同的輸入裝置所代表的含義不同,按鍵和鍵盤就是代表按鍵資訊,滑鼠和觸摸屏代表坐标資訊,是以在應用層的處理就不同,對于驅動編寫者而言不需要去關心應用層的事情,隻需要按照要求上報這些輸入事件即可。為此 input 子系統分為 input 驅動層、 input 核心層、 input 事件處理層,最終給使用者空間提供可通路的裝置節點。input子系統架構如下圖示

Linux驅動開發|input子系統

可見在 Linux 核心空間,分為驅動層、核心層和事件層。編寫驅動程式的時候隻需要關注這三個層,這三個層的分工如下

  • 驅動層:輸入裝置的具體驅動程式,向核心層報告輸入内容
  • 核心層:為驅動層提供輸入裝置注冊和操作接口,通知事件層對輸入事件進行處理
  • 事件層:主要和使用者空間進行互動
Linux驅動開發|input子系統

1.2 input 驅動編寫流程

input 核心層會向 Linux 核心注冊一個字元裝置,drivers/input/input.c 這個檔案就是 input 輸入子系統的核心層,此檔案裡面有如下所示代碼:

struct class input_class = {
  .name = "input",
  .devnode = input_devnode,
};
......
......
static int __init input_init(void) {
  int err;
  err = class_register(&input_class);
  if (err) {
    pr_err("unable to register input_dev class\n");
    return err;
  }

  err = input_proc_init();
  if (err)
    goto fail1;

  err = register_chrdev_region(MKDEV(INPUT_MAJOR, 0), INPUT_MAX_CHAR_DEVICES,"input");
  if (err) {
    pr_err("unable to register char major %d", INPUT_MAJOR);
    goto fail2;
  }

  return 0;

fail2: input_proc_exit();
fail1: class_unregister(&input_class);
  return err;
}
      

從以上代碼可以看出,核心層向核心注冊了一個 input 類(在/sys/class下),并且注冊了一個主裝置号為 INPUT_MAJOR(13)的字元裝置。是以,input 子系統的所有裝置主裝置号都為 13,在使用 input 子系統處理輸入裝置時就不需要去注冊字元裝置了,隻需要向系統注冊一個 input_device 即可

  • 注冊 input_dev:使用 input 子系統時,需要注冊一個 input 裝置,由 input_dev 結構體表示
/***** input_dev 結構體定義 *****/
struct input_dev {
  const char *name;
  const char *phys;
  const char *uniq;
  struct input_id id;

  unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)];

  unsigned long evbit[BITS_TO_LONGS(EV_CNT)];   /* 事件類型的位圖 */
  unsigned long keybit[BITS_TO_LONGS(KEY_CNT)];   /* 按鍵值的位圖 */
  unsigned long relbit[BITS_TO_LONGS(REL_CNT)];   /* 相對坐标的位圖 */
  unsigned long absbit[BITS_TO_LONGS(ABS_CNT)];   /* 絕對坐标的位圖 */
  unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)];   /* 雜項事件的位圖 */
  unsigned long ledbit[BITS_TO_LONGS(LED_CNT)];   /*LED 相關的位圖 */
  unsigned long sndbit[BITS_TO_LONGS(SND_CNT)]; /* sound 有關的位圖 */
  unsigned long ffbit[BITS_TO_LONGS(FF_CNT)];   /* 壓力回報的位圖 */
  unsigned long swbit[BITS_TO_LONGS(SW_CNT)];   /*開關狀态的位圖 */
  ......
  bool devres_managed;
};
/***** input_dev 結構體中的evbit 表示輸入事件類型,可選的事件類型
定義在 include/uapi/linux/input.h 檔案中,事件類型如下。比如要使用
按鍵,那麼久需要注冊EV_KEY事件,若要連按功能,還需要注冊EV_REP事件 ***/
#define EV_SYN 0x00     /* 同步事件 */
#define EV_KEY 0x01     /* 按鍵事件 */
#define EV_REL 0x02     /* 相對坐标事件 */
#define EV_ABS 0x03     /* 絕對坐标事件 */
#define EV_MSC 0x04     /* 雜項(其他)事件 */
#define EV_SW 0x05      /* 開關事件 */
#define EV_LED 0x11     /* LED */
#define EV_SND 0x12     /* sound(聲音) */
#define EV_REP 0x14     /* 重複事件 */
#define EV_FF 0x15      /* 壓力事件 */
#define EV_PWR 0x16     /* 電源事件 */
#define EV_FF_STATUS 0x17   /* 壓力狀态事件 */
/***** input_dev 結構體中的keybit、 relbit 等都是存放不同僚件對應
的值。比如要使用按鍵事件,是以要用到keybit按鍵值位圖,其按鍵值定義
在 include/uapi/linux/input.h 檔案中 **************************/
#define KEY_RESERVED 0
#define KEY_ESC 1
#define KEY_1 2
#define KEY_2 3
#define KEY_3 4
#define KEY_4 5
#define KEY_5 6
#define KEY_6 7
#define KEY_7 8
#define KEY_8 9
#define KEY_9 10
#define KEY_0 11
......
#define BTN_TRIGGER_HAPPY39 0x2e6
#define BTN_TRIGGER_HAPPY40 0x2e7
      

注冊 input 裝置之前,先要申請 input_dev 結構體變量

登出 input 裝置之後,需要釋放 input_dev 結構體變量

/* 申請 input_dev 結構體變量 */
struct input_dev *input_allocate_device(void)
//傳回值:申請到的input_dev
/* 釋放 input_dev 結構體變量 */
void input_free_device(struct input_dev *dev)
//dev:需要釋放的input_dev
      
注冊 input 裝置和登出 input 裝置,使用如下函數
/* 注冊 input_dev */
int input_register_device(struct input_dev *dev)
//dev:要注冊的input_dev
//傳回值:0,注冊成功;負值,注冊失敗
/* 登出 input_dev */
void input_unregister_device(struct input_dev *dev)
//dev:要登出的 input_dev
      

綜上所述, input_dev 注冊過程如下

– 函數申請一個 input_dev 結構體變量

– 初始化 input_dev 的事件類型及事件值

– 向 Linux 核心注冊上面初始化好的 input_dev

– 解除安裝時,先登出 input_dev,再釋放 input_dev 結構體變量

/* input_dev 注冊流程示例代碼 */
struct input_dev *inputdev;     /* input 結構體變量 */
/* 驅動入口函數 */
static int __init xxx_init(void) {
  ......
  inputdev = input_allocate_device();   /* 申請 input_dev */
  inputdev->name = "test_inputdev";     /* 設定 input_dev 名字 */
  /*********第一種設定事件和事件值的方法***********/
  __set_bit(EV_KEY, inputdev->evbit);   /* 設定産生按鍵事件 */
  __set_bit(EV_REP, inputdev->evbit);   /* 重複事件 */
  __set_bit(KEY_0, inputdev->keybit);   /*設定産生哪些按鍵值 */
  /************************************************/
  /*********第二種設定事件和事件值的方法***********/
  keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
  keyinputdev.inputdev->keybit[BIT_WORD(KEY_0)] |= BIT_MASK(KEY_0);
  /************************************************/
  /*********第三種設定事件和事件值的方法***********/
  keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
  input_set_capability(keyinputdev.inputdev, EV_KEY, KEY_0);
  /************************************************/
  /* 注冊 input_dev */
  input_register_device(inputdev);
  ......
  return 0;
}
/* 驅動出口函數 */
static void __exit xxx_exit(void) {
  input_unregister_device(inputdev);  /* 登出 input_dev */
  input_free_device(inputdev);    /* 删除 input_dev */
}
      
  • 上報輸入事件:擷取輸入事件及輸入值,并上報給Linux核心。上報函數 input_event 原型如下
void input_event(struct input_dev *dev,
         unsigned int type,
         unsigned int code,
         int value)
//dev: 需要上報的 input_dev
//type: 上報的事件類型,比如 EV_KEY
//code: 事件碼,也就是我們注冊的按鍵值,比如 KEY_0、 KEY_1 等等
//value: 事件值,比如1表示按鍵按下,0表示按鍵松開
      
input_event 函數可以上報所有的事件類型和事件值,核心也提供了其他針對具體事件的上報函數,不過本質上還是input_event 函數
/* 上報按鍵所使用的 input_report_key 函數 */
static inline void input_report_key(struct input_dev *dev,
                  unsigned int code, 
                  int value){
  input_event(dev, EV_KEY, code, !!value);
}
/* 其他事件上報函數 */
void input_report_rel(struct input_dev *dev, unsigned int code, int value)
void input_report_abs(struct input_dev *dev, unsigned int code, int value)
void input_report_ff_status(struct input_dev *dev, unsigned int code, int value)
void input_report_switch(struct input_dev *dev, unsigned int code, int value)
void input_mt_sync(struct input_dev *dev)
      
上報事件以後還需要使用 input_sync 函數來告訴 Linux 核心 input 子系統上報結束
void input_sync(struct input_dev *dev)
//dev:需要上報同步事件的 input_dev
      

綜上所述,事件上報得過程如下(以按鍵上報為例)

/* 用于按鍵消抖的定時器服務函數 */
void timer_function(unsigned long arg) {
  unsigned char value;
  value = gpio_get_value(keydesc->gpio);    //讀取 IO 值
  if(value == 0){               //按下按鍵
    input_report_key(inputdev, KEY_0, 1);   //最後一個參數 1,按下
    input_sync(inputdev);           //同步事件
  } else {                  //按鍵松開
    input_report_key(inputdev, KEY_0, 0);   //最後一個參數 0,松開
    input_sync(inputdev);           //同步事件
  }
}
      
  • input_event結構體:Linux 核心使用該結構體來表示所有的輸入事件,使用者應用程式可以通過 input_event 來擷取到具體的輸入事件或相關的值
/* input_envent 結構體定義在include/uapi/linux/input.h 檔案中 */
struct input_event {
  struct timeval time;
  __u16 type;
  __u16 code;
  __s32 value;
};
//time:時間,也就是此事件發生的時間,為 timeval 結構體類型
//type:事件類型,比如EV_KEY,表示此次事件為按鍵事件
//code:事件碼,比如在EV_KEY事件中就表示具體的按鍵碼,KEY_0/KEY_1等按鍵
//value:值,比如EV_KEY事件中就表示按鍵值,表示按鍵有沒有被按下
      

二、input子系統驅動實驗

本實驗以IMX6ULL開發闆上的 KEY0 按鍵為例,介紹如何編寫 input 驅動

2.1 修改裝置樹

  • 添加pinctrl節點:在iomuxc節點的imx6ul-evk子節點下建立“pinctrl_key”節點,複用UART1_CTS_B
pinctrl_key: keygrp {
  fsl,pins = <
    MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 0xF080
  >;
};
//MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 用于設定pin的複用功能
//0xF080 用于設定pin的電氣特性
      
  • 添加KEY裝置節點:在根節點下建立KEY裝置節點,設定PIN對應的pinctrl節點,指定所使用的的GPIO
key {
  #address-cells = <1>;
  #size-cells = <1>;
  compatible = "alpha-key";
  pinctrl-names = "default";
  pinctrl-0 = <&pinctrl_key>;
  key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>;
  status = "okay";
};
      
  • 檢查PIN是否沖突:檢查pinctrl中設定以及裝置節點中指定的引腳有沒有被别的外設使用

儲存修改後,在kernel主目錄下使用“make dtbs”指令編譯裝置樹,使用新的裝置樹檔案啟動Llinux系統

2.2 驅動程式編寫

建立 keyinput.c 驅動檔案,并輸入如下内容

#define KEYINPUT_CNT    1     /* 裝置号個數  */
#define KEYINPUT_NAME   "keyinput"  /* 名字     */
#define KEY0VALUE     0X01    /* KEY0按鍵值  */
#define INVAKEY       0XFF    /* 無效的按鍵值 */
#define KEY_NUM       1     /* 按鍵數量   */
/* 中斷IO描述結構體 */
struct irq_keydesc {
  int gpio;               /* gpio */
  int irqnum;               /* 中斷号     */
  unsigned char value;          /* 按鍵對應的鍵值 */
  char name[10];              /* 名字 */
  irqreturn_t (*handler)(int, void *);  /* 中斷服務函數 */
};
/* keyinput裝置結構體 */
struct keyinput_dev{
  dev_t devid;              /* 裝置号   */
  struct cdev cdev;           /* cdev   */
  struct class *class;          /* 類    */
  struct device *device;          /* 裝置    */
  struct device_node  *nd;        /* 裝置節點 */
  struct timer_list timer;        /* 定義一個定時器*/
  struct irq_keydesc irqkeydesc[KEY_NUM]; /* 按鍵描述數組 */
  unsigned char curkeynum;        /* 目前的按鍵号 */
  struct input_dev *inputdev;       /* input結構體 */
};

struct keyinput_dev keyinputdev;  /* key input裝置 */
/*中斷服務函數 */
static irqreturn_t key0_handler(int irq, void *dev_id){
  struct keyinput_dev *dev = (struct keyinput_dev *)dev_id;

  dev->curkeynum = 0;
  dev->timer.data = (volatile long)dev_id;
  mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10)); /* 10ms定時 */
  return IRQ_RETVAL(IRQ_HANDLED);
}

/* 定時器服務函數,用于按鍵消抖 */
void timer_function(unsigned long arg){
  unsigned char value;
  unsigned char num;
  struct irq_keydesc *keydesc;
  struct keyinput_dev *dev = (struct keyinput_dev *)arg;

  num = dev->curkeynum;
  keydesc = &dev->irqkeydesc[num];
  value = gpio_get_value(keydesc->gpio);  /* 讀取IO值 */
  if(value == 0){             /* 按下按鍵 */
    /* 上報按鍵值 */
    //input_event(dev->inputdev, EV_KEY, keydesc->value, 1);
    input_report_key(dev->inputdev, keydesc->value, 1);/* 最後一個參數表示按下還是松開,1為按下,0為松開 */
    input_sync(dev->inputdev);
  } else {                  /* 按鍵松開 */
    //input_event(dev->inputdev, EV_KEY, keydesc->value, 0);
    input_report_key(dev->inputdev, keydesc->value, 0);
    input_sync(dev->inputdev);
  } 
}

/*
 * @description : 按鍵IO初始化
 * @param     : 無
 * @return    : 無
 */
static int keyio_init(void){
  unsigned char i = 0;
  char name[10];
  int ret = 0;

  keyinputdev.nd = of_find_node_by_path("/key");
  if (keyinputdev.nd== NULL){
    printk("key node not find!\r\n");
    return -EINVAL;
  } 

  /* 提取GPIO */
  for (i = 0; i < KEY_NUM; i++) {
    keyinputdev.irqkeydesc[i].gpio = of_get_named_gpio(keyinputdev.nd ,"key-gpio", i);
    if (keyinputdev.irqkeydesc[i].gpio < 0) {
      printk("can't get key%d\r\n", i);
    }
  }

  /* 初始化key所使用的IO,并且設定成中斷模式 */
  for (i = 0; i < KEY_NUM; i++) {
    memset(keyinputdev.irqkeydesc[i].name, 0, sizeof(name));  /* 緩沖區清零 */
    sprintf(keyinputdev.irqkeydesc[i].name, "KEY%d", i);    /* 組合名字 */
    gpio_request(keyinputdev.irqkeydesc[i].gpio, name);
    gpio_direction_input(keyinputdev.irqkeydesc[i].gpio); 
    keyinputdev.irqkeydesc[i].irqnum = irq_of_parse_and_map(keyinputdev.nd, i);
  }
  /* 申請中斷 */
  keyinputdev.irqkeydesc[0].handler = key0_handler;
  keyinputdev.irqkeydesc[0].value = KEY_0;

  for (i = 0; i < KEY_NUM; i++) {
    ret = request_irq(keyinputdev.irqkeydesc[i].irqnum, keyinputdev.irqkeydesc[i].handler, 
                     IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, keyinputdev.irqkeydesc[i].name, &keyinputdev);
    if(ret < 0){
      printk("irq %d request failed!\r\n", keyinputdev.irqkeydesc[i].irqnum);
      return -EFAULT;
    }
  }

  /* 建立定時器 */
  init_timer(&keyinputdev.timer);
  keyinputdev.timer.function = timer_function;

  /* 申請input_dev */
  keyinputdev.inputdev = input_allocate_device();
  keyinputdev.inputdev->name = KEYINPUT_NAME;
#if 0
  /* 初始化input_dev,設定産生哪些事件 */
  __set_bit(EV_KEY, keyinputdev.inputdev->evbit); /* 設定産生按鍵事件          */
  __set_bit(EV_REP, keyinputdev.inputdev->evbit); /* 重複事件,比如按下去不放開,就會一直輸出資訊      */

  /* 初始化input_dev,設定産生哪些按鍵 */
  __set_bit(KEY_0, keyinputdev.inputdev->keybit); 
#endif

#if 0
  keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
  keyinputdev.inputdev->keybit[BIT_WORD(KEY_0)] |= BIT_MASK(KEY_0);
#endif

  keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
  input_set_capability(keyinputdev.inputdev, EV_KEY, KEY_0);

  /* 注冊輸入裝置 */
  ret = input_register_device(keyinputdev.inputdev);
  if (ret) {
    printk("register input device failed!\r\n");
    return ret;
  }
  return 0;
}

/* 驅動入口函數 */
static int __init keyinput_init(void){
  keyio_init();
  return 0;
}
/* 驅動出口函數 */
static void __exit keyinput_exit(void){
  unsigned int i = 0;
  /* 删除定時器 */
  del_timer_sync(&keyinputdev.timer); /* 删除定時器 */   
  /* 釋放中斷 */
  for (i = 0; i < KEY_NUM; i++) {
    free_irq(keyinputdev.irqkeydesc[i].irqnum, &keyinputdev);
  }
  /* 釋放input_dev */
  input_unregister_device(keyinputdev.inputdev);
  input_free_device(keyinputdev.inputdev);
}

module_init(keyinput_init);
module_exit(keyinput_exit);
MODULE_LICENSE("GPL");
      

2.3 測試程式編寫

建立測試檔案 keyinputApp.c,并編寫程式

/* 定義一個input_event變量,存放輸入事件資訊 */
static struct input_event inputevent;

int main(int argc, char *argv[]) {
  int fd;
  int err = 0;
  char *filename;

  filename = argv[1];
  if(argc != 2) {
    printf("Error Usage!\r\n");
    return -1;
  }

  fd = open(filename, O_RDWR);
  if (fd < 0) {
    printf("Can't open file %s\r\n", filename);
    return -1;
  }

  while (1) {
    err = read(fd, &inputevent, sizeof(inputevent));
    if (err > 0) { /* 讀取資料成功 */
      switch (inputevent.type) {
        case EV_KEY:
          if (inputevent.code < BTN_MISC) { /* 鍵盤鍵值 */
            printf("key %d %s\r\n", inputevent.code, inputevent.value ? "press" : "release");
          } else {
            printf("button %d %s\r\n", inputevent.code, inputevent.value ? "press" : "release");
          }
          break;
        /* 其他類型的事件,自行處理 */
        case EV_REL:
          break;
        case EV_ABS:
          break;
        case EV_MSC:
          break;
        case EV_SW:
          break;
      }
    } else {
      printf("讀取資料失敗\r\n");
    }
  }
  return 0;
}
      

2.4 運作測試

  • 修改Makefile編譯目标變量
obj-m := keyinput.o
      
  • 使用“make -j32”編譯出驅動子產品檔案
make -j32
      
  • 使用“arm-linux-gnueabihf-gcc”指令編譯測試APP
arm-linux-gnueabihf-gcc keyinputApp.c -o keyinputApp
      
  • 将驅動檔案和APP可執行檔案拷貝至“rootfs/lib/modules/4.1.15”中
  • 在加載 keyinput.ko驅動子產品前,先看一下 /dev/input 目錄下有哪些檔案
Linux驅動開發|input子系統
  • 使用“modprobe”指令加載驅動,加載成功後總線就會進行比對
depmod  #第一次加載驅動時,需使用“depmod”指令
modprobe keyinput.ko
      
  • 加載成功後 /dev/input 目錄下,多了一個 event1 檔案,該檔案就是注冊的驅動對應的裝置檔案
Linux驅動開發|input子系統
  • keyinputApp 通過讀取 /dev/input/event1 這個檔案來擷取輸入事件資訊
./keyinputApp /dev/input/event1
      
Linux驅動開發|input子系統

繼續閱讀