天天看點

input子系統驅動編寫(按鍵)

之前已經分析過了編寫一個驅動程式,主要有以下幾個步驟:

  1. 自己設定或由系統自動配置設定驅動裝置的主裝置号;
  2. 編寫裝置操作函數(drv_open、drv_read、drv_write、drv_close等);
  3. 構造file_operations結構體,将上一步編寫的操作函數指派給結構體内的​

    ​.open、.read、.write、.poll、.fasync​

    ​等
  4. 注冊驅動程式,調用​

    ​register_chrdev(major, name, fops);​

  5. 編寫入口函數和出口函數。

但輸入子系統驅動架構将以上步驟分開了,它是由裝置層、核心層、事件層共同組成的。其中核心層提供一些裝置層與事件層公用的函數,比如說注冊函數、反注冊函數、事件到來的處理函數等等,完成了驅動程式編寫的第1-4步。但在input架構的file_operations中,隻含有一個open函數,正是通過它調用特定input_handler結構體,其裡面包含有根據不同次裝置号映射到不同的輸入裝置大類的**.fops**;事件處理層其實在Linux核心裡面已經幫我們寫好了很多有關的事件;而裝置層就是我們要新添加到輸入系統的具體裝置相關的程式了。

在分析輸入子系統架構時,我們已經知道核心對不同類型的輸入裝置已經抽象出了不同的handler進行處理,device層實作純硬體的操作,我們可以根據所實作驅動的功能對device層進行設計,主要是内容是當檢測有效輸入發送,調用input_event函數向handler層上報結果即可。

1、編寫符合輸入子系統架構的驅動程式步驟

  1. 配置設定一個​

    ​input_dev​

    ​​結構體,調用​

    ​input_allocate_device()​

    ​​或者​

    ​devm_input_allocate_device(struct input_dev*)​

    ​實作;
  2. 編寫函數,設定input_dev結構體,選擇input裝置的事件類型(調用​

    ​set_bit()​

    ​函數實作);
  3. 注冊input_dev結構體,調用​

    ​input_register_device(struct input_dev*)​

    ​函數實作;
  4. 編寫硬體相關代碼:注冊中斷處理函數,比如鍵盤裝置需要編寫按鍵的擡起、放下,觸摸屏裝置需要編寫按下、擡起、絕對移動,滑鼠裝置需要編寫單擊、擡起、相對移動,并且需要在必要的時候送出硬體資料(鍵值/坐标/狀态等等),即上報輸入事件。
  5. 當送出輸入裝置産生的輸入事件之後,需要調用​

    ​void input_sync(struct input_dev *dev)​

    ​函數來通知輸入子系統,以處理裝置産生的完整事件。

Linux輸入子系統支援的事件類型:(在include/linux/input.h中)

​EV_SYN 0x00 同步事件 EV_KEY 0x01 按鍵事件 EV_REL 0x02 相對坐标(如:滑鼠移動,報告相對最後一次位置的偏移) EV_ABS 0x03 絕對坐标(如:觸摸屏或操作杆,報告絕對的坐标位置) EV_MSC 0x04 其它 EV_SW 0x05 開關 EV_LED 0x11 按鍵/裝置燈 EV_SND 0x12 聲音/警報 EV_REP 0x14 重複 EV_FF 0x15 力回報 EV_PWR 0x16 電源 EV_FF_STATUS 0x17 力回報狀态 EV_MAX 0x1f 事件類型最大個數和提供位掩碼支援 ​

Linux輸入子系統提供的裝置驅動層上報輸入事件的函數:(在include/linux/input.h中)

void input_event(struct input_dev* dev, unsigned int type, unsigned int code, int value); //上報指定的type、code的輸入事件

void input_report_key(struct input_dev *dev, unsigned int code, int 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); //上報絕對坐标事件

2、編寫符合input系統架構的按鍵驅動程式

現在我們開始使用核心的輸入子系統架構,将自己編寫驅動程式融入其中,編寫更通用的按鍵驅動程式。這裡以JZ2440開發闆上的4個按鍵作為輸入子系統的按鍵,我們編寫的驅動程式實作将開發闆上的4個按鍵分别映射成鍵盤上不同鍵值,代表鍵盤上的字母L、S和Enter(回車)。這幾個值是在include\linux\input.h中被定義的。

2.1 編寫入口函數、出口函數,搭建好架構

/* 參考drivers\input\keyboard\gpio_keys.c */
#include <linux/module.h>
#include <linux/version.h>

#include <linux/init.h>
#include <linux/fs.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/sched.h>
#include <linux/pm.h>
#include <linux/sysctl.h>
#include <linux/proc_fs.h>
#include <linux/delay.h>
#include <linux/platform_device.h>
#include <linux/input.h>
#include <linux/irq.h>

#include <asm/gpio.h>
#include <asm/io.h>

static int buttons_init(void)
{
    /* 1.配置設定一個input_dev結構體。 */
    
    /* 2.設定input_dev結構體。 */
    
    /* 3.注冊input_dev結構體。 */
    
    /* 4.硬體相關的操作。 */
}

static void buttons_exit(vod)
{
    
}

module_init(buttons_init);
module_exit(buttons_exit);
MODULE_LICENSE("GPL");      

2.1.1 配置設定一個input_dev結構體

/* 以下結構體定義位于函數外,屬于全局變量*/
static struct input_dev *buttons_dev;

/*以下語句位于buttons_init函數中*/
buttons_dev = input_allocate_device();
if (!buttons_dev)
    return -ENOMEM;      

2.1.2 設定input_dev結構體

先看以下Input_dev結構體的定義:

input子系統驅動編寫(按鍵)
  • evbit:用來設定該裝置能産生哪類事件類型(在第1節有具體描述輸入子系統所支援的所有事件類型),在此我們選擇按鍵事件EV_KEY和EV_REP,代碼如下:
set_bit(EV_KEY, buttons_input->evbit);  //按鍵事件
set_bit(EV_REP, buttons_input->evbit);  //重複事件(長按按鍵會重複輸入按鍵值)      
  • keybit:用來設定該裝置能産生哪些按鍵值,在此我們設定本節開頭說的鍵盤上的L、S、左shift、enter(為了可以在開發闆上執行ls指令)。按鍵碼定義在include\linux\input.h中,截取其中一小部分:
#define KEY_ENTER        28//enter的按鍵碼
#define KEY_S            31//S的按鍵碼
#define KEY_L            38//L的按鍵碼
#define KEY_LEFTSHIFT    42//leftshift的按鍵碼      

設定輸入事件類型的哪一種按鍵的代碼:

set_bit(KEY_L, buttons_input->keybit);
set_bit(KEY_S, buttons_input->keybit);
set_bit(KEY_ENTER, buttons_input->keybit);
set_bit(KEY_LEFTSHIFT, buttons_input->keybit);      
  • relbit:表示能産生哪些相對位移事件, 例如滾輪
  • absbit:表示能産生哪些絕對位移事件。

2.1.3 注冊input_dev結構體

注冊的功能其實就是将目前裝置buttons_dev放到input_dev連結清單中去,然後和事件層的evdev_handler逐個比較(通過比較id和id.table[]),如果比對,則會調用evdev_handler中的.connect函數,産生一個handle結構體(隻含有.handler和.dev,一個指向裝置,一個指向handler),将目前裝置與handler建立起關聯,将目前dev及其比對的handler放到各自的.h_list中。代碼如下:

input_register_device(buttons_input);//注冊裝置驅動      

2.1.4 硬體相關的操作

  • 定義各按鍵描述結構體
/* 以下2個結構體定義位于函數外,屬于全局變量*/
struct pin_desc{
  int irq;
  char *name;
  unsigned int pin;
  unsigned int key_val;
};
 
struct pin_desc pins_desc[4] = {
  {IRQ_EINT0,  "S2", S3C2410_GPF(0),   KEY_L},
  {IRQ_EINT2,  "S3", S3C2410_GPF(2),   KEY_S},
  {IRQ_EINT11, "S4", S3C2410_GPG(3),   KEY_ENTER},
};      
  • 初始化定時器(防抖動)
/* 以下結構體定義位于函數外,屬于全局變量*/
static struct timer_list buttons_timer;  //定義一個定時器

/*以下語句位于buttons_init函數中*/
init_timer(&buttons_timer);  //初始化定時器
buttons_timer.function = buttons_timer_function;//定義逾時處理函數
add_timer(&buttons_timer);  //定義逾時時間      
  • 為每個按鍵注冊中斷
/*以下語句位于buttons_init函數中*/
for (i = 0; i < 3; i++)  注冊中斷
{
  request_irq(pins_desc[i].irq, buttons_irq, (IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING), pins_desc[i].name, &pins_desc[i]);
}      
  • 是以整體buttons_init函數的代碼如下:
static int buttons_init(void)
{
  int i;
  
  /* 1. 配置設定一個input_dev結構體 */
  buttons_dev = input_allocate_device();;

  /* 2. 設定 */
  /* 2.1 能産生哪類事件 */
  set_bit(EV_KEY, buttons_dev->evbit);
  set_bit(EV_REP, buttons_dev->evbit);
  
  /* 2.2 能産生這類操作裡的哪些事件: L,S,ENTER,LEFTSHIT */
  set_bit(KEY_L, buttons_dev->keybit);
  set_bit(KEY_S, buttons_dev->keybit);
  set_bit(KEY_ENTER, buttons_dev->keybit);
  
  /* 3. 注冊 */
  input_register_device(buttons_dev);
  
  /* 4. 硬體相關的操作 */
  init_timer(&buttons_timer);
  buttons_timer.function = buttons_timer_function;
  add_timer(&buttons_timer);
  
  for (i = 0; i < 3; i++)
  {
    request_irq(pins_desc[i].irq, buttons_irq, (IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING), pins_desc[i].name, &pins_desc[i]);
  }
  
  return 0;
}      
  • 編寫出口函數
static void buttons_exit(void)
{
  int i;
  for (i = 0; i < 3; i++)
  {
    free_irq(pins_desc[i].irq, &pins_desc[i]);
  }

  del_timer(&buttons_timer);
  input_unregister_device(buttons_dev);
  input_free_device(buttons_dev);  
}      

2.2 編寫按鍵的中斷處理函數

/* 以下結構體指針定義位于函數外,屬于全局變量*/
static struct pin_desc *irq_pd;

static irqreturn_t buttons_irq(int irq, void *dev_id)
{
  /* 10ms後啟動定時器 */
  irq_pd = (struct pin_desc *)dev_id;
  mod_timer(&buttons_timer, jiffies+HZ/100);
  return IRQ_RETVAL(IRQ_HANDLED);
}      

2.3 編寫定時器逾時處理函數

當有按鍵資料産生時,調用input_event函數來上報事件,該函數會從input_dev連結清單中的.h_list找到對應的.handler,并調用裡面的.event函數。

static void buttons_timer_function(unsigned long data)
{
  struct pin_desc * pindesc = irq_pd;
  unsigned int pinval;

  if (!pindesc)
    return;
  
  pinval = s3c2410_gpio_getpin(pindesc->pin);

  if (pinval)
  {
    /* 松開 : 最後一個參數: 0-松開, 1-按下 */
    input_event(buttons_dev, EV_KEY, pindesc->key_val, 0);
    input_sync(buttons_dev);  //上報同步事件
  }
  else
  {
    /* 按下 */
    input_event(buttons_dev, EV_KEY, pindesc->key_val, 1);
    input_sync(buttons_dev);
  }
}      

2.4 整體代碼buttons.c

/* 參考drivers\input\keyboard\gpio_keys.c */
/* 運作于JZ2440開發闆、Linux3.4.2核心、 gcc4.3.2*/
#include <linux/module.h>
#include <linux/version.h>

#include <linux/init.h>
#include <linux/fs.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/sched.h>
#include <linux/pm.h>
#include <linux/sysctl.h>
#include <linux/proc_fs.h>
#include <linux/delay.h>
#include <linux/platform_device.h>
#include <linux/input.h>
#include <linux/irq.h>

#include <asm/gpio.h>
#include <asm/io.h>
//#include <asm/arch/regs-gpio.h>

struct pin_desc{
  int irq;
  char *name;
  unsigned int pin;
  unsigned int key_val;
};

struct pin_desc pins_desc[4] = {
  {IRQ_EINT0,  "S2", S3C2410_GPF(0),   KEY_L},
  {IRQ_EINT2,  "S3", S3C2410_GPF(2),   KEY_S},
  {IRQ_EINT11, "S4", S3C2410_GPG(3),   KEY_ENTER},
//  {IRQ_EINT19, "S5",  S3C2410_GPG(11), KEY_LEFTSHIFT},
};

static struct input_dev *buttons_dev;
static struct pin_desc *irq_pd;
static struct timer_list buttons_timer;

static irqreturn_t buttons_irq(int irq, void *dev_id)
{
  /* 10ms後啟動定時器 */
  irq_pd = (struct pin_desc *)dev_id;
  mod_timer(&buttons_timer, jiffies+HZ/100);
  return IRQ_RETVAL(IRQ_HANDLED);
}

static void buttons_timer_function(unsigned long data)
{
  struct pin_desc * pindesc = irq_pd;
  unsigned int pinval;

  if (!pindesc)
    return;
  
  pinval = s3c2410_gpio_getpin(pindesc->pin);

  if (pinval)
  {
    /* 松開 : 最後一個參數: 0-松開, 1-按下 */
    input_event(buttons_dev, EV_KEY, pindesc->key_val, 0);
    input_sync(buttons_dev);
  }
  else
  {
    /* 按下 */
    input_event(buttons_dev, EV_KEY, pindesc->key_val, 1);
    input_sync(buttons_dev);
  }
}

static int buttons_init(void)
{
  int i;
  
  /* 1. 配置設定一個input_dev結構體 */
  buttons_dev = input_allocate_device();;

  /* 2. 設定 */
  /* 2.1 能産生哪類事件 */
  set_bit(EV_KEY, buttons_dev->evbit);
  set_bit(EV_REP, buttons_dev->evbit);
  
  /* 2.2 能産生這類操作裡的哪些事件: L,S,ENTER,LEFTSHIT */
  set_bit(KEY_L, buttons_dev->keybit);
  set_bit(KEY_S, buttons_dev->keybit);
  set_bit(KEY_ENTER, buttons_dev->keybit);
  //set_bit(KEY_LEFTSHIFT, buttons_dev->keybit);

  /* 3. 注冊 */
  input_register_device(buttons_dev);
  
  /* 4. 硬體相關的操作 */
  init_timer(&buttons_timer);
  buttons_timer.function = buttons_timer_function;
  add_timer(&buttons_timer);
  
  for (i = 0; i < 3; i++)
  {
    request_irq(pins_desc[i].irq, buttons_irq, (IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING), pins_desc[i].name, &pins_desc[i]);
  }
  
  return 0;
}

static void buttons_exit(void)
{
  int i;
  for (i = 0; i < 3; i++)
  {
    free_irq(pins_desc[i].irq, &pins_desc[i]);
  }

  del_timer(&buttons_timer);
  input_unregister_device(buttons_dev);
  input_free_device(buttons_dev);  
}

module_init(buttons_init);

module_exit(buttons_exit);

MODULE_LICENSE("GPL");      

3、測試

3.1 利用​

​hexdump​

​測試

hexdump /dev/event1  //等價于執行`open /dev/event1; read();`
     秒        微秒    類  code    value
0000000 0bb2 0000 0e48 000c 0001 0026 0001 0000
0000010 0bb2 0000 0e54 000c 0000 0000 0000 0000
0000020 0bb2 0000 5815      

3.2 如果目前沒有啟動QT,則按如下方法:

  • 執行​

    ​cat /dev/tty1​

  • 開發闆上按下:S2、S3、S4
  • 可以看到:ls
input子系統驅動編寫(按鍵)

或者

  • 執行​

    ​exec 0</dev/tty1​

  • 可以使用開發闆上的鍵盤來輸入ls指令
input子系統驅動編寫(按鍵)

3.3 如果已經啟動了QT

  • 先打開記事本
  • 然後按鍵S2、S3、S4
  • 可以看到記事本上顯示ls

4、小結

  • 打開輸入裝置後發生了什麼?
  • drivers/input/input.c:
  • input_init > err = register_chrdev(INPUT_MAJOR, “input”, &input_fops);
  • static const struct file_operations input_fops = {

    .owner = THIS_MODULE,

    .open = input_open_file,

    };

  • 怎麼讀按鍵?
  • intput_open_file
  • struct input_handler *handler = input_table[iminor(inode) >> 5];
  • new_fops = fops_get(handler->fops) // =>&evdev_fops
  • file->f_op = new_fops;
  • err = new_fops->open(inode, file);
  • APP: read > … >file->f_op->read
  • input_table數組由誰構造?
  • input_register_handler
  • 如何注冊input_handler?
  • input_register_handler
  • input_table[handler->minor >> 5] = handler; // 放入數組
  • list_add_tail(&handler->node, &input_handler_list); // 放傳入連結表
  • // 對于每個input_dev,調用input_attach_handler
  • list_for_each_entry(dev, &input_dev_list, node)
  • input_attach_handler(dev, handler); // 根據input_handler的id_table判斷能否支援這個input_dev
  • 如何 注冊輸入裝置:
  • input_register_device
  • list_add_tail(&dev->node, &input_dev_list); // 放傳入連結表
  • // 對于每一個input_handler,都調用input_attach_handler

    list_for_each_entry(handler, &input_handler_list, node)

  • input_attach_handler(dev, handler); // 根據input_handler的id_table判斷能否支援這個input_dev

input_attach_handler

  • id = input_match_device(handler->id_table, dev);
  • error = handler->connect(handler, dev, id);

注冊input_dev或input_handler時,會兩兩比較左邊的input_dev和右邊的input_handler,

根據input_handler的id_table判斷這個input_handler能否支援這個input_dev,

如果能支援,則調用input_handler的connect函數建立"連接配接"

  • 怎麼建立連接配接?
  1. 配置設定一個input_handle結構體
  2. input_handle.dev = input_dev; // 指向左邊的input_dev

    input_handle.handler = input_handler; // 指向右邊的input_handler

  3. 注冊:

    input_handler->h_list = &input_handle;

    inpu_dev->h_list = &input_handle;

  1. evdev = kzalloc(sizeof(struct evdev), GFP_KERNEL); // 配置設定一個input_handle
  2. // 設定

    evdev->handle.dev = dev; // 指向左邊的input_dev

    evdev->handle.name = evdev->name;

    evdev->handle.handler = handler; // 指向右邊的input_handler

    evdev->handle.private = evdev;

  3. // 注冊

    error = input_register_handle(&evdev->handle);

  • 怎麼讀按鍵?
  • APP應用程式執行: read
  • 。。。。
  • evdev_read
  • // 無資料并且是非阻塞方式打開,則立刻傳回

    if (client->head == client->tail && evdev->exist && (file->f_flags & O_NONBLOCK))

    return -EAGAIN;

    // 否則休眠

    retval = wait_event_interruptible(evdev->wait,

    client->head != client->tail || !evdev->exist);

  • 誰來喚醒?

    evdev_event

    wake_up_interruptible(&evdev->wait);

  • evdev_event被誰調用?

    猜:應該是硬體相關的代碼,input_dev那層調用的

    在裝置的中斷服務程式裡,确定事件是什麼,然後調用相應的input_handler的event處理函數

    gpio_keys_isr

  • // 上報事件

    input_event(input, type, button->code, !!state);

    input_sync(input);

  • input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value)
  • struct input_handle *handle;
  • list_for_each_entry(handle, &dev->h_list, d_node)

    if (handle->open)

    handle->handler->event(handle, type, code, value);

繼續閱讀