Linux驱动input 子系统

前面在介绍中断时以按键为例,我们要检测按键输入,需要做如下工作

        (1) 从设备树获取到按键节点、初始化gpio节点、获取中断号、注册中断

        (2) 注册设备号、初始化字符设备、自动创建驱动节点

        (3) 实现文件操作函数逻辑(read、open、release)

Linux内核为了处理输入事件(按键、鼠标、键盘、触摸屏),专门设计了input子系统,使用 input 子系统后无需执行上面的步骤 (2)、(3),大大节省了编写驱动的时间。

        (1) 从设备树获取到按键节点、初始化 gpio 节点、获取中断号、注册中断

        (2) 为按键注册 input 子系统(事件注册、按键注册)

一、input 设备类型

注册到 input 子系统中的设备称为 input 设备,Linux 内核专门提供了一种数据类型 input_dev 用来表示 input 设备,该结构体定义在 <linux/input.h> 文件中。

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 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)];       /*开关状态的位图 */
    ...
}

① name 成员

input_dev 的name成员用于标识输入设备的名称,当 input 设备注册成功后,输入 lsinput 命令可以看到当前 Linux 内核管理的 input 设备,

(a) /dev/input/event0:表示当前 input 设备对应的驱动文件是 /dev/input/event0 

(b) name:表示当前input 设备的名字

(c) bits ev:表示当前input设备监听的事件类型

② evbit 成员

input 设备支持多种输入事件,evbit 成员的作用便是管理当前 input 设备需要监听的事件,假设我们要把一个按键注册为 input 设备,那么我们就需要向这个成员中添加按键事件,若考虑到按键会被重复按下,我们还可以添加重复事件。各种事件的定义在 <linux/input.h> 文件中。

#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 /* 压力状态事件 */

③ 其他以 bit 结尾的成员(keybit、relbit)

不同事件发生时,可能会发生状态变化,为了记录这些事件的状态变化,input_dev 为每个事件提供了一个位图,用来保存状态值。按键事件对应的位图便是 keybit,一些辅助性的事件并不会提供位图来记录状态。

注意:无论是向 evbit 添加事件,还是向 keybit 添加状态值,建议使用 API,可以使用 __set_bit。

二、input 设备相关API

注册顺序:

        (1) 申请 input 设备 —— input_allocate_device

        (2) 注册 input 设备 —— input_register_device

注销顺序:

        (1) 注销 input 设备 —— input_unregister_device

        (2) 释放 input 设备 —— input_free_device

1、input 设备申请 / 释放

input 设备申请可以理解为初始化一个 input 设备,毕竟使用 input 设备无需编写文件操作函数、创建驱动节点,进行 input 设备申请相当于将这些事交由内核代劳。

input 设备申请:

/**
 * @return 返回申请好的 input_dev 结构体指针
 */
struct input_dev *input_allocate_device(void);

/**
 * @param pdev 要释放的 input_dev 结构体指针
 */
void input_free_device(struct input_dev *pdev);

2、input 设备注册 / 注销

input 设备注册可以理解为将当前 input 设备添加到内核,input 设备注销可以理解为将input设备从内核移除。

/**
 * @param pdev 要注册的 input 设备
 * @return 成功返回0,失败返回负值
 */
int input_register_device(struct input_dev *pdev);

/**
 * @param pdev 要注销的 input 设备
 */
void input_unregister_device(struct input_dev *pdev);

3、上报输入事件

我们没有手动实现文件操作函数 read,所以站在驱动的角度,即便内核检测到按键事件触发也无法通知上层,对此 Linux 内核提供了 input_event 函数和 input_event 结构体。具体流程如下:

        1、内核检测到 input 设备的按键事件触发

        2、驱动层调用 input_event 函数上报(更新 input_event 结构体)

        3、上层应用层读取 input_event 结构体的 value 成员(value 成员记录了状态值)

input_event 函数的声明在 <linux/input.h> 文件中,函数声明如下:

/**
 * @param dev   哪个input设备有事件触发 
 * @param type  事件类型
 * @param code  外设代码
 * @param value 事件状态值
 */
void input_event(struct input_dev *dev, 
                 unsigned int type,
                 unsigned int code,
                 int value);

input_event 结构体:

struct input_event {
	struct timeval time;
	__u16 type;        // 事件类型
	__u16 code;        // 外设代号
	__s32 value;       // 事件状态值
};

4、__set_bit

__set_bit 可以将某个变量的指定 bit 位置 1。函数声明如下:

/**
 * @param nr     要置1的bit位
 * @param addr   被操作的变量
 */
void __set_bit(int nr, volatile unsigned long *addr);

三、驱动实现

对于中断注册的介绍,这里便不再赘述,下面仅介绍 input 设备的注册。这里暂时没有用到 platform 驱动,所以大部分的实现都在驱动入口函数和驱动退出函数中。

1、input 设备申请

static struct input_dev		*inputdev;			/* input设备 */

/* input 设备申请 */
inputdev = input_allocate_device();

2、input 设备初始化

因为是按键,我们要向 input 设备添加按键事件的监听,若有必要还可以添加按键重复按下事件的监听。此外,input 设备是通过代号来区分不同外设的,Linux内核提供了很多代号可使用,这里就使用 KEY_0 来代表当前按键。

/* input 设备初始化 */
inputdev->name = "test_inputdev";
__set_bit(EV_KEY, inputdev->evbit);	 // 向 evbit 添加按键事件(evbit 中的 EV_KEY 位置1)
__set_bit(EV_REP, inputdev->evbit);	 // 向 evbit 添加重复事件 (evbit 中的 EV_REP 位置1)
__set_bit(KEY_0, inputdev->keybit);  // 初始化 KEY_0 按键值(也可以理解为 KEY_0 位被占用)

注意:这里的 evbit 虽然是一个long 类型的数组,但建议看做是一个多bit组成的连续空间,因为实际 input_dev 内在声明这个成员的时候,是将 bit 转换成了 long

3、注册 input 设备

if(input_register_device(inputdev) != 0)	// 注册 input 设备
{
	printk("input device register failed!
");
	return -1;
}

4、上报事件

按键被按下时会触发中断,我们要在中断中上报按键触发事件,这里因为加入了定时器消抖,所以实际的处理逻辑为

  • 在中断函数起始位置:上报按键被按下
  • 在定时器回调末尾位置:上报按键被释放
// inputdev 设备触发事件
// 触发的事件类型为 EV_KEY (按键事件)
// 触发的外设代号为 KEY_0 
// 事件触发状态为 Released (这里的 released是一个宏定义)
input_event(inputdev, EV_KEY, KEY_0, Pressed);

// 上报同步
input_sync(inputdev);

4、驱动出口函数注销 input 设备

/* 释放input设备 */
input_unregister_device(inputdev);
input_free_device(inputdev);

5、完整代码

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/uaccess.h>
#include <linux/cdev.h>			// cdev_init
#include <linux/device.h>		// device_create
#include <linux/err.h>			// IS_ERR
#include <linux/of_gpio.h>		// of_get_named_gpio
#include <asm/gpio.h>			// gpio_set_value
#include <linux/interrupt.h>	// request_irq / free_irq
#include <linux/of_irq.h>		// 获取中断号
#include <linux/irq.h>
#include <linux/uaccess.h>    	// copy_to_user & copy_from_user 
#include <linux/timer.h>		// 定时器
#include <linux/input.h>

#define timer_delay(x)			jiffies + msecs_to_jiffies(x)
typedef enum {
	Released = 0U,
	Pressed = 1U	
}Key_Status;

/* 中断IO描述 */
struct irq_gpiodesc
{
	uint32_t gpioNum;						/* gpio编号 */
	uint32_t irqNum;						/* 中断号 */
	irqreturn_t (*handler)(int, void*);		/* 中断服务函数 */
}; 
static struct irq_gpiodesc 	 irqdesc;			/* gpio中断描述 */
static struct timer_list 	 timer;				/* 定时器 */
static struct input_dev		*inputdev;			/* input设备 */

static irqreturn_t key0_handler(int irq, void * dev)
{
	input_event(inputdev, EV_KEY, KEY_0, Pressed);
	input_sync(inputdev);

	mod_timer(&timer, timer_delay(50));

	return IRQ_RETVAL(IRQ_HANDLED);
}

/* 定时器回调函数 */
void timer_callback(unsigned long arg)
{
	// 按键处理逻辑

	input_event(inputdev, EV_KEY, KEY_0, Released);
	input_sync(inputdev);
}

static int __init keyinput_init(void)
{
	uint32_t ret = 0;
	struct device_node* keyNode = NULL;

	/* 1、初始化定时器 */
	init_timer(&timer);
	timer.function = timer_callback;
	timer.data = (unsigned long)&irqdesc;

	/* 2、申请按键中断 */
	keyNode = of_find_node_by_path("/gpio-key0");			// 获取设备树节点
	if (keyNode == NULL)
	{
		printk("cannot find key node in dts!
");
		return -1;
	}
	irqdesc.gpioNum = of_get_named_gpio(keyNode, "key-gpio", 0);	// 获取 gpio 编号
	if (irqdesc.gpioNum < 0)
	{
		printk("gpio property fetch failed!
");
		return -1;
	}
	ret = gpio_direction_input(irqdesc.gpioNum);					// 配置 gpio 为输入
	if (ret < 0)
	{
		printk("gpio set failed!
");
		return -1;
	}
	irqdesc.irqNum = irq_of_parse_and_map(keyNode, 0);			// 根据节点获取中断号
	if (irqdesc.irqNum < 0)
	{
		printk("key irq number fetch failed!
");
		return -1;
	}
	ret = request_irq(irqdesc.irqNum, key0_handler, IRQ_TYPE_EDGE_FALLING, "key0-int", NULL);		// 注册中断服务函数
	if (ret < 0)
	{
		printk("key irq subscribe failed!
");
		return -1;
	}
	/* 3、注册input子系统 */
	inputdev = input_allocate_device();		// input 设备初始化
	inputdev->name = "inputdev_test";
	__set_bit(EV_KEY, inputdev->evbit);		// 按键事件
	__set_bit(EV_REP, inputdev->evbit);		// 重复事件
	__set_bit(KEY_0, inputdev->keybit);

	if(input_register_device(inputdev) != 0)	// 注册 input 设备
	{
		printk("input device register failed!
");
		return -1;
	}

	printk("input device init!
");
	return 0;
}

static void __exit keyinput_exit(void)
{
	/* 删除定时器 */
	del_timer_sync(&timer);

	/* 注销中断 */
	free_irq(irqdesc.irqNum, NULL);

	/* 释放input设备 */
	input_unregister_device(inputdev);
	input_free_device(inputdev);

	printk("input device exit!
");
}

module_init(keyinput_init);
module_exit(keyinput_exit);

/* 
 * LICENSE和作者信息
 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("author_name");

四、应用测试

通过 insmod 命令将当前模块注册到内核,然后输入 lsinput 命令查看当前内核中已有的 input 设备,可以看到当前 input 设备对应的驱动文件是 /dev/input/event1

因此,我们要获取到按键状态,要读取的驱动文件便是 /dev/input/event1,读取到的内容是 input_event 结构体的首地址。

注意:没有事件触发时,read 函数会阻塞

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <linux/input.h>

#define delayms(x)        usleep(x * 1000)

void printHelp()
{
    printf("usage: ./xxxApp <driver_path>
");
}

static struct input_event inputevent;

int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        printHelp();
        return -1;
    }
    
    char* driver_path = argv[1];       // 位置0 保存的是 ./chrdevbaseApp
    int ret = 0;
    int fd = 0;

    fd = open(driver_path, O_RDONLY);
    if (fd < 0)
    {
        perror("open file failed");
        return -2;
    }

    while (1)
    {
        ret = read(fd, &inputevent, sizeof(inputevent));
        if (ret < 0)
        {
            printf("read data error
");
            break;
        }

        switch(inputevent.type)
        {
            case EV_KEY:
                printf("key%d %s
", inputevent.code, inputevent.value ? "pressed":"released");
                break;
            default: break;
        }
    }
    
    close(fd);
    return 0;
}