前面在介绍中断时以按键为例,我们要检测按键输入,需要做如下工作
(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; }