前面在介绍中断时以按键为例,我们要检测按键输入,需要做如下工作
(1) 从设备树获取到按键节点、初始化gpio节点、获取中断号、注册中断
(2) 注册设备号、初始化字符设备、自动创建驱动节点
(3) 实现文件操作函数逻辑(read、open、release)
Linux内核为了处理输入事件(按键、鼠标、键盘、触摸屏),专门设计了input子系统,使用 input 子系统后无需执行上面的步骤 (2)、(3),大大节省了编写驱动的时间。
(1) 从设备树获取到按键节点、初始化 gpio 节点、获取中断号、注册中断
(2) 为按键注册 input 子系统(事件注册、按键注册)
注册到 input 子系统中的设备称为 input 设备,Linux 内核专门提供了一种数据类型 input_dev 用来表示 input 设备,该结构体定义在
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 设备,那么我们就需要向这个成员中添加按键事件,若考虑到按键会被重复按下,我们还可以添加重复事件。各种事件的定义在
#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。
注册顺序:
(1) 申请 input 设备 —— input_allocate_device
(2) 注册 input 设备 —— input_register_device
注销顺序:
(1) 注销 input 设备 —— input_unregister_device
(2) 释放 input 设备 —— input_free_device
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);
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);
我们没有手动实现文件操作函数 read,所以站在驱动的角度,即便内核检测到按键事件触发也无法通知上层,对此 Linux 内核提供了 input_event 函数和 input_event 结构体。具体流程如下:
1、内核检测到 input 设备的按键事件触发
2、驱动层调用 input_event 函数上报(更新 input_event 结构体)
3、上层应用层读取 input_event 结构体的 value 成员(value 成员记录了状态值)
input_event 函数的声明在
/**
* @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; // 事件状态值
};
__set_bit 可以将某个变量的指定 bit 位置 1。函数声明如下:
/**
* @param nr 要置1的bit位
* @param addr 被操作的变量
*/
void __set_bit(int nr, volatile unsigned long *addr);
对于中断注册的介绍,这里便不再赘述,下面仅介绍 input 设备的注册。这里暂时没有用到 platform 驱动,所以大部分的实现都在驱动入口函数和驱动退出函数中。
static struct input_dev *inputdev; /* input设备 */
/* input 设备申请 */
inputdev = input_allocate_device();
因为是按键,我们要向 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
if(input_register_device(inputdev) != 0) // 注册 input 设备
{
printk("input device register failed!\n");
return -1;
}
按键被按下时会触发中断,我们要在中断中上报按键触发事件,这里因为加入了定时器消抖,所以实际的处理逻辑为
// inputdev 设备触发事件
// 触发的事件类型为 EV_KEY (按键事件)
// 触发的外设代号为 KEY_0
// 事件触发状态为 Released (这里的 released是一个宏定义)
input_event(inputdev, EV_KEY, KEY_0, Pressed);
// 上报同步
input_sync(inputdev);
/* 释放input设备 */
input_unregister_device(inputdev);
input_free_device(inputdev);
#include
#include
#include
#include
#include
#include
#include
#include // cdev_init
#include // device_create
#include // IS_ERR
#include // of_get_named_gpio
#include // gpio_set_value
#include // request_irq / free_irq
#include // 获取中断号
#include
#include // copy_to_user & copy_from_user
#include // 定时器
#include
#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!\n");
return -1;
}
irqdesc.gpioNum = of_get_named_gpio(keyNode, "key-gpio", 0); // 获取 gpio 编号
if (irqdesc.gpioNum < 0)
{
printk("gpio property fetch failed!\n");
return -1;
}
ret = gpio_direction_input(irqdesc.gpioNum); // 配置 gpio 为输入
if (ret < 0)
{
printk("gpio set failed!\n");
return -1;
}
irqdesc.irqNum = irq_of_parse_and_map(keyNode, 0); // 根据节点获取中断号
if (irqdesc.irqNum < 0)
{
printk("key irq number fetch failed!\n");
return -1;
}
ret = request_irq(irqdesc.irqNum, key0_handler, IRQ_TYPE_EDGE_FALLING, "key0-int", NULL); // 注册中断服务函数
if (ret < 0)
{
printk("key irq subscribe failed!\n");
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!\n");
return -1;
}
printk("input device init!\n");
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!\n");
}
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
#include
#include
#include
#include
#include
#include
#define delayms(x) usleep(x * 1000)
void printHelp()
{
printf("usage: ./xxxApp \n");
}
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\n");
break;
}
switch(inputevent.type)
{
case EV_KEY:
printf("key%d %s\n", inputevent.code, inputevent.value ? "pressed":"released");
break;
default: break;
}
}
close(fd);
return 0;
}