5. 输入子系统¶
计算机的输入设备繁多,有按键、鼠标、键盘、触摸屏、游戏手柄等等,Linux内核为了能够将所有的输入设备进行统一的管理, 设计了输入子系统。为上层应用提供了统一的抽象层,各个输入设备的驱动程序只需上报产生的输入事件即可。
下面以按键输入事件(使用GPIO模拟也行)为例讲解输入子系统的使用。 本章配套源码和设备树插件位于“ /linux_driver/input_sub_system ”目录下。
5.1. 输入子系统简介¶
linux为了统一各个输入设备,将输入子系统分为了Drivers(驱动层)、Input Core(输入子系统核心层)、 handlers(事件处理层)三部分。
Drivers主要实现对硬件设备的读写访问,设置中断,并将硬件产生的事件转为Input Core定义的规范提交给Handlers;
Input Core起到承上启下的作用,为Drivers提供了规范及接口,并通知Handlers对事件进行处理;
Handlers并不涉及硬件方面的具体操作,是一个纯软件层,包含了不同的解决方案,如按键、键盘、鼠标、游戏手柄等。
最终所有输入设备的输入信息将被抽象成以下结构体:
1 2 3 4 5 6 7 | //输入事件
struct input_event{
struct timeval time; //事件产生的时间
__u16 type; //输入设备的类型,鼠标、键盘、触摸屏
__u16 code;
__s16 value;
}
|
time :事件产生的时间。
type :输入设备的类型。
code :根据设备类型的不同而含义不同,如果设备类型是按键,code表示为按键值(如第几个按键等)。
value:根据设备类型的不同而含义不同。如果设备类型是按键,value表示的是松开或者按下。
本章目的是编写一个基于输入子系统和中断的按键驱动程序,重点在于了解Input Core为我们提供了哪些接口,并了解如何将 按键信息以事件上报。
input子系统Input Core实现代码是“ 内核源码/drivers/input/input.c ”以及“ 内核源码/include/linux/input.h ”两个文件 为我们提供了注册输入子系统的API,通过操作这些API就可以实现输入事件的注册、初始化、上报、注销等等工作。 下面我们介绍输入子系统常用的API接口及数据结构。
5.1.1. input_dev结构体¶
无论是什么类型的输入设备,无论发送什么类型的事件,输入设备在内核中都表示为struct input_dev实例,代表一个具体的输入设备, 后面将会根据具体的设备来初始化这个结构体,结构体成员介绍如下: (input_dev参数很多,有些不需要我们手动配置,所以这里只列出和介绍常用的参数,完整内容位于input.h文件)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | struct input_dev {
const char *name; //提供给用户的输入设备的名称
const char *phys; //提供给编程者的设备节点的名称
const char *uniq; //指定唯一的ID号
struct input_id 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)];
unsigned long sndbit[BITS_TO_LONGS(SND_CNT)];
unsigned long ffbit[BITS_TO_LONGS(FF_CNT)];
unsigned long swbit[BITS_TO_LONGS(SW_CNT)];
/*----------以下结构体成员省略----------------*/
};
|
结构体成员中最重要的是 evbit、keybit、relbit 等数组,这些数组设置了设备输入事件的类型和键值。
evbit:用于指定支持的事件类型,这要根据实际输入设备能够产生的事件来选择,可选选项如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #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
#define EV_SND 0x12
#define EV_REP 0x14
#define EV_FF 0x15
#define EV_PWR 0x16
#define EV_FF_STATUS 0x17
#define EV_MAX 0x1f
#define EV_CNT (EV_MAX+1)
|
上面代码中前几个宏定义较为常用的输入事件类型,介绍如代码后面所示。 完整的事件列表介绍可以参考内核源码目录下的“ ~/Documentation/input/event-codes.rst ”内核文档。 很明显,我们这章节要使用的按键的事件类型应该使用 EV_KEY 。
keybit:记录支持的键值,“键值”在程序中用于区分不同的按键,可选“键值”如下所示。
1 2 3 4 5 6 7 | #define KEY_RESERVED 0
#define KEY_ESC 1
#define KEY_1 2
#define KEY_2 3
#define KEY_3 4
#define KEY_4 5
/*-----------以下内容省略-------------*/
|
可以看出“键值”就是一些数字。只要实际设备与按键对应即可。
relbit、absbit:这两个参数和上面的keybit都和参数evbit有关,如果evbit中只选择了EV_KEY, 那么我们就不需要设置relbit(相对坐标)和absbit(绝对坐标)以及后面省略成员的内容。这些内容使用到时再具体介绍。 使用不同的输入事件类型需要设备不同的
总之,input_dev结构体成员很多,但是对应到一个具体的输入设备,只需要设置自己用到的其中一两个属性。
5.1.2. input_dev结构体的申请和释放¶
一个input_dev结构体代表了一个输入设备,它实际会占输入子系统的一个次设备号。 input子系统为我们提供了申请和释放input_dev结构体的函数。 由于input_dev结构体的成员很多,初始化过程也相对麻烦,一般都使用input子系统为我们提供的接口函数来 申请和释放input_dev结构体,如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | struct input_dev *input_allocate_device(void)
{
static atomic_t input_no = ATOMIC_INIT(-1);
struct input_dev *dev;
dev = kzalloc(sizeof(*dev), GFP_KERNEL); //动态内存申请
if (dev) {
dev->dev.type = &input_dev_type;
dev->dev.class = &input_class; //dev->dev为struct device类型结构体
device_initialize(&dev->dev); //初始化dev->dev结构体内部成员
mutex_init(&dev->mutex); //初始化互斥锁
spin_lock_init(&dev->event_lock); //初始化自旋锁
timer_setup(&dev->timer, NULL, 0); //初始化定时器
INIT_LIST_HEAD(&dev->h_list); //初始化handle链表节点
INIT_LIST_HEAD(&dev->node); //初始化输入设备链表节点
dev_set_name(&dev->dev, "input%lu",
(unsigned long)atomic_inc_return(&input_no)); //设置设备名称
__module_get(THIS_MODULE);
}
return dev;
}
EXPORT_SYMBOL(input_allocate_device);
|
参数: 无
返回值:
成功: struct input_dev类型指针
失败: NULL
我们只需要知道如何调用这个函数来申请input_dev即可,想要更深入学习的同学们可以尝试去分析整个输入子系统的实现源码, 对于输入子系统的源码分析就可以写一篇很长的文章了,这里并不展开详细的源码分析。
1 | void input_free_device(struct input_dev *dev)
|
参数: dev:struct input_dev类型指针
返回值: 无
申请和释放函数接口比较简单。申请函数input_allocate_device执行成功后会返回申请得到的input_dev结构体的地址, 如果失败,返回NULL。释放函数input_free_device只有一个参数dev,用于指定要释放的input_dev结构体。
5.1.3. 注册和注销input_dev结构体¶
input_dev申请成功后,我们需要根据自己的实际输入设备配置input_dev结构体,具体配置在实验代码编写部分会详细说明, 配置完成后还要使用注册和注销函数将input_dev注册到输入子系统。注册和注销函数如下:
1 | int input_register_device(struct input_dev *dev)
|
参数: dev:struct input_dev类型指针 返回值:
成功: 0
失败: 返回非0值
input_register_device函数将输入设备(input_dev)注册到输入子系统的核心层。 该函数使用需要注意以下几点
使用该函数注册的input_dev必须是使用input_allocate_device函数申请得到的。
注册之前需要根据实际输入设备配置好input_dev结构体。
如果注册失败必须调用input_free_device函数释放input_dev结构体。
如果注册成功,在函数退出时只需要使用input_unregister_device函数注销input_dev结构体不需要再调用 input_free_device函数释放input_dev结构体。
1 | void input_unregister_device(struct input_dev *dev)
|
参数: dev:struct input_dev类型指针
返回值: 无
input_unregister_device是注销函数,输入子系统的资源是有限的,不使用是应当注销。 调用input_unregister_device注销函数之后就不必调用input_free_device函数释放input_dev。
5.1.4. 上报事件函数和上报结束函数¶
以按键为例,按键按下后需要使用上报函数向输入子系统核心层上报按键事件,并且上报后还要发送上报结束信息。函数定义如下所示。
1 | void input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value);
|
input_event函数用于上报事件,共有4个参数介绍如下。
参数:
dev,指定输设备(input_dev结构体)。
type,事件类型。我们在根据实际输入设备配置input_dev结构体时会设置input_dev-> evbit参数, 用于设置输入设备能够产生的事件类型(可能是多个)。上报事件时要从“能够产生”的这些事件类型中选择。
code,编码。以按键为例,按键的编码就是我们设置的按键键值。
value,指定事件的值。
返回值: 无
1 2 3 4 5 6 7 8 9 | static inline void input_sync(struct input_dev *dev)
{
input_event(dev, EV_SYN, SYN_REPORT, 0);
}
static inline void input_report_key(struct input_dev *dev, unsigned int code, int value)
{
input_event(dev, EV_KEY, code, !!value);
}
|
input子系统为不同的输入事件函数提供了不同的函数接口,这些函数接口只是对input_event函数进行简单的封装, 具体的参数参照input_event函数。input_report_key用于上报按键事件,input_sync用于发送同步信号,表示上报结束。
5.2. 输入子系统实验¶
本小节以按键(或者普通GPIO)为例介绍输入子系统的具体使用方法,编程思路:第一种是轮询输入设备,它基于GPIO,没有映射IRQ,轮询输 入内核要轮询GPIO,获取GPIO的电平,根据GPIO状态对应按键的按下和释放。第二种是驱动程序使用GPIO中断,发送输入事件到内核。 当中断时去检测GPIO,判断键按下或键释放。
本章使用中断的方法,结合源码介绍如下(配套源码和设备树插件位于“ /linux_driver/input_sub_system ”目录下):
5.2.1. 设备树插件实现¶
设备树插件与上一章的“中断实验”使用的设备树插件几乎相同,我们只需要将中断类型修改为“上升和下降沿触发”。 修改部分如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | /dts-v1/;
/plugin/;
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/rockchip.h>
#include <dt-bindings/interrupt-controller/irq.h>
&{/} {
input_button: input_button {
status = "okay";
compatible = "input_button";
button-gpios = <&gpio0 RK_PB0 GPIO_ACTIVE_HIGH>; //引脚可以根据具体板卡修改,这里以GPIO0_B0为例
pinctrl-names = "default";
pinctrl-0 = <&input_button_pin>;
interrupt-parent = <&gpio0>;
interrupts = <RK_PB0 IRQ_TYPE_EDGE_BOTH>;
};
};
&{/pinctrl} {
pinctrl_button {
input_button_pin: input_button_pin {
rockchip,pins = <0 RK_PB0 RK_FUNC_GPIO &pcfg_pull_none>;
};
};
}
|
button-gpios :指定引脚,后面驱动会使用gpiod_get来获取gpio。
修改内容很简单只是将原来中断的触发方式修改为双边沿触发,其他的设备树内容和前面中断章节实验一致。
5.2.2. 驱动程序实现¶
5.2.2.1. 驱动入口函数¶
驱动入口函数如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | static int button_probe(struct platform_device *pdev)
{
struct button_data *priv;
struct gpio_desc *gpiod;
struct input_dev *i_dev;
int ret;
pr_info("button_probe\n");
priv = devm_kzalloc(&pdev->dev,sizeof(*priv), GFP_KERNEL); 8
if (!priv)
return -ENOMEM;
i_dev = input_allocate_device(); 12
if (!i_dev)
return -ENOMEM;
i_dev->open = btn_open;
i_dev->close = btn_close;
i_dev->name = "key input";
i_dev->dev.parent = &pdev->dev;
priv->button_input_dev = i_dev;
priv->pdev = pdev;
set_bit(EV_KEY, i_dev->evbit); /*设置要使用的输入事件类型*/
set_bit(BTN_0, i_dev->keybit); /* 设置事件值,按钮 0*/
gpiod = gpiod_get(&pdev->dev, "button", GPIOD_IN); /*获取gpio,并设置为输入*/
if (IS_ERR(gpiod))
return -ENODEV;
/*获取irq*/
priv->irq = gpiod_to_irq(gpiod);
priv->button_input_gpiod = gpiod;
/*注册input设备*/
ret = input_register_device(priv->button_input_dev);
if (ret) {
pr_err("Failed to register inputdevice\n");
goto err_input;
}
/*设置平台驱动数据*/
platform_set_drvdata(pdev, priv);
/*申请gpio中断*/
ret = request_any_context_irq(priv->irq, button_input_irq_hander,IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING, "input-button", priv);
if (ret < 0) {
dev_err(&pdev->dev,"请求gpio中断失败\n");
goto err_btn;
}
return 0;
err_btn:
gpiod_put(priv->button_input_gpiod);
err_input:
input_free_device(priv->button_input_dev);
return ret;
}
|
驱动入口函数完成基本的初始化工作,结合代码各部分介绍如下:
第8行:使用devm_kzalloc分配内存,该函数分配的内存,会在设备卸载时自动释放。
第12行:使用input_allocate_device()申请输入子系统结构体,申请得到的input_dev结构体代表了一个输入设备。
第16-24行:填充我们定义的button_data结构体,设置input设备的名字,函数等,然后使用set_bit函数设置输入事件类型和值。 input_dev参数很多,其中最主要的是事件类型和事件对应的code, evbit每一位代表了一种事件类型,为1则表示支持,0表示不支持。例如我们这里要支持“按键”事件, 那么就要将EV_KEY(等于0x01)位置1。内核提供了帮助宏BIT_MASK帮助我们开启某一“事件”。
第26行:使用gpiod_get函数获取gpio,详细描述如下:
1 2 3 4 5 | struct gpio_desc *__must_check gpiod_get(struct device *dev, const char *con_id,
enum gpiod_flags flags)
{
return gpiod_get_index(dev, con_id, 0, flags);
}
|
gpiod_get函数获取dev设备,con_id的第0个引脚信息,并做flags初始化。参数介绍如下。
参数:
dev 设备指针,从该设备获取引脚信息。
con_id,引脚组名称(不包含前缀)。如引脚组名为button-gpios。则 con_id=”button”。
flags,初始化的标志,可以设置gpio是输入还是输出,输出是高还是低,标识参考下面:
1 2 3 4 5 6 7 8
enum gpiod_flags { GPIOD_ASIS = 0, /*不初始化GPIO,方向必须使用其他函数设置*/ GPIOD_IN = GPIOD_FLAGS_BIT_DIR_SET, /*初始化GPIO引脚输入*/ GPIOD_OUT_LOW = GPIOD_FLAGS_BIT_DIR_SET | GPIOD_FLAGS_BIT_DIR_OUT, /*初始化GPIO引脚输出,输出0*/ GPIOD_OUT_HIGH = GPIOD_FLAGS_BIT_DIR_SET | GPIOD_FLAGS_BIT_DIR_OUT | GPIOD_FLAGS_BIT_DIR_VAL, GPIOD_OUT_LOW_OPEN_DRAIN = GPIOD_OUT_LOW | GPIOD_FLAGS_BIT_OPEN_DRAIN, /*初始化GPIO引脚开漏输出,输出0*/ GPIOD_OUT_HIGH_OPEN_DRAIN = GPIOD_OUT_HIGH | GPIOD_FLAGS_BIT_OPEN_DRAIN, };
返回值: gpio_desc 结构体指针,失败-ENOENT。
5.2.2.2. 驱动出口函数¶
出口函数主要完成驱动退出前的清理工作,很简单,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 | static int button_remove(struct platform_device *pdev)
{
struct button_data *priv;
priv = platform_get_drvdata(pdev);
input_unregister_device(priv->button_input_dev); 6
input_free_device(priv->button_input_dev);
free_irq(priv->irq, priv);
gpiod_put(priv->button_input_gpiod);
return 0;
}
|
第6行:注销input设备
第7行:释放input设备
第8-9行:释放irq和申请的gpio
5.2.2.3. 中断服务函数¶
中断服务函数中我们读取按键输入引脚的状态判断按键是按下还是松开。代码如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | static irqreturn_t button_input_irq_hander(int irq, void *dev_id)
{
struct button_data *priv = dev_id;
int button_satus;
/*读取按键引脚的电平,根据读取得到的结果输入按键状态*/
button_satus = (gpiod_get_value(priv->button_input_gpiod) & 1);
if(button_satus)
{
input_report_key(priv->button_input_dev, BTN_0, 1);
input_sync(priv->button_input_dev);
}
else
{
input_report_key(priv->button_input_dev, BTN_0, 0);
input_sync(priv->button_input_dev);
}
return IRQ_HANDLED;
}
|
第7行:读取按键对应引脚的电平。
第8-17行:根据按键引脚状态向系统上报按键事件。
5.2.3. 测试应用程序实现¶
测试应用程序中读取按键键值,打印按键状态。具体代码如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | struct input_event button_input_event;
int main(int argc, char *argv[])
{
int error = -20;
/*打开的文件/dev/input/event4需要根据实际设备文件修改*/
int fd = open("/dev/input/event4", O_RDONLY);
if (fd < 0)
{
printf("open file : /dev/input/event1 error!\n");
return -1;
}
printf("wait button down... \n");
do
{
/*读取按键状态*/
error = read(fd, &button_input_event, sizeof(button_input_event));
if (error < 0)
{
printf("read file error! \n");
}
/*判断并打印按键状态*/
if((button_input_event.type == 1) && (button_input_event.code == 0x100))
{
if(button_input_event.value == 0)
{
printf("button up\n");
}
else if(button_input_event.value == 1)
{
printf("button down\n");
}
}
} while (1);
printf("button Down !\n");
/*关闭文件*/
error = close(fd);
if (error < 0)
{
printf("close file error! \n");
}
return 0;
}
|
第1行:申请一个input_event类型的结构体变量,如我们在本章开头前所说的所有的输入设备传递的信息都会以事件的形式上报。
第8行:这里的打开的文件**/dev/input/event1** 是输入子系统为我们生成的输入设备设备,即我们使用的按键,要根据自己实际的设备文件修改。
第21行:读取按键信息,read函数没有读取到上报输入事件则将一直等待。
第27-37行:根据获取读取到的信息判断按键的状态。
测试应用程序的内容很简单,基本是按照打开文件、读取状态、判断状态并打印状态。
5.2.4. 实验准备¶
在板卡上的部分GPIO可能会被系统占用,引脚被占用后,设备树可能无法再加载或驱动中无法再申请对应的资源, 比如运行代码时出现“Device or resource busy”或者运行代码卡死等等现象,要确保所用的GPIO是没有被其他驱动占用的。
如出现 Permission denied
或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。
我们修改内核目录/arch/arm64/boot/dts/rockchip/overlays下的Makefile文件, 添加我们编辑好的设备树插件(本章的设备树插件源码是lubancat-button-input-overlay.dts),并把设备树插件文件放在和Makefile文件同级目录下, 以进行设备树插件的编译。
在内核的根目录下执行如下命令即可:
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- lubancat2_defconfig //以lubancat2为例
make ARCH=arm64 -j4 CROSS_COMPILE=aarch64-linux-gnu- dtbs
生成的.dtbo位于内核根目录下的“arch/arm64/boot/dts/rockchip/overlays”目录下。 本章设备树插件为“lubancat-button-input-overlay.dts”, 编译之后就会在内核源码/arch/arm64/boot/dts/rockchip/overlays目录下生成同名的lubancat-button-input-overlay.dtbo文件,得到.dtbo后,下一步就是将其加载到系统中。
5.2.4.1. 添加设备树插件文件¶
上一小节我们编译生成了 lubancat-button-input-overlay.dtbo
,该文件可以被动态的加载到系统,以lubancat2板卡uboot加载设备树插件为例,详细看下 环境搭建章节。
首先我们把编译好的设备树插件文件,上传到我们开发板中。 我们可以使用uboot加载编写好的设备树插件,只需完成简单的两个步骤:
1、将需要加载的.dtbo文件放入板卡
/boot/dtb/overlays/
目录下。2、将对应的设备树插件加载配置,写入uEnv.txt配置文件,系统启动过程中会自动从uEnv.txt读取要加载的设备树插件。打开位于“/boot/uEnv/”目录下的uEnv.txt文件,要将设备树插件写入uEnv.txt也,使用vim或者nano编辑器打开文件,书写格式为“dtoverlay=<设备树插件路径>”。
添加好后,我们重启开发板,使用命令ls /proc/device-tree/ 查看 是否有input_button目录,有就说明加载成功。
5.2.4.2. 编译驱动程序及测试程序¶
本节实验使用的Makefile如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | KERNEL_DIR=../../kernel/
ARCH=arm64
CROSS_COMPILE=aarch64-linux-gnu-
export ARCH CROSS_COMPIL
obj-m := input_sub_system.o
out = test_app
all:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
$(CROSS_COMPILE)gcc -o $(out) test_app.c
.PHONY:clean
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
rm test_app
|
将配套的驱动代码根目录放到内核同级目录下,然后进入input_sub_system子目录下,并在驱动目录中输入如下命令来编译驱动模块及测试程序:
make
5.2.5. 加载驱动验证¶
将编译好的驱动、应用程序、设备树插件并拷贝到开发板,这里就不再赘述这一部分内容了,前面小节中都有详细介绍。
重启后使用insmod input_sub_system.ko命令加载驱动,当设备匹配上时会在“/dev/input”目录下生成设备文件,我们这次实验的是“/dev/input/event4”如下所示:
使用cat /proc/interrupts命令可以查看我们驱动申请的中断:
使用udevadm info /dev/input/event4命令查看设备信息:
驱动加载成功后直接运行测试应用程序命令“./test_app” 测试程序运行后,接按键,按下按键(或者是拉低/高引脚电平)。 终端会输出按键状态,如下所示。
也可以使用下面evtest命令来测试,使用命令后按下按键或者拉高/低引脚电平:
sudo evtest /dev/input/event4 /* /dev/input/event4是根据自己加载驱动,增加的设备文件 */