轻松理解Input子系统驱动详解
本文已参与「新人创作礼」活动,一起开启掘金创作之路。
之前已经分析过了编写一个驱动程序,主要有以下几个步骤:
- 自己设定或由系统自动分配驱动设备的主设备号;
- 编写设备操作函数(drv_open、drv_read、drv_write、drv_close等);
- 构造file_operations结构体,将上一步编写的操作函数赋值给结构体内的
.open、.read、.write、.poll、.fasync
等 - 注册驱动程序,调用
register_chrdev(major, name, fops);
- 编写入口函数和出口函数。
但输入子系统驱动框架将以上步骤分开了,它是由设备层、核心层、事件层共同组成的。其中核心层提供一些设备层与事件层公用的函数,比如说注册函数、反注册函数、事件到来的处理函数等等,完成了驱动程序编写的第1-4步。但在input框架的file_operations中,只含有一个open函数,正是通过它调用特定input_handler结构体,其里面包含有根据不同次设备号映射到不同的输入设备大类的**.fops**;事件处理层其实在Linux内核里面已经帮我们写好了很多有关的事件;而设备层就是我们要新添加到输入系统的具体设备相关的程序了。
在分析输入子系统框架时,我们已经知道内核对不同类型的输入设备已经抽象出了不同的handler进行处理,device层实现纯硬件的操作,我们可以根据所实现驱动的功能对device层进行设计,主要是内容是当检测有效输入发送,调用input_event函数向handler层上报结果即可。
1、编写符合输入子系统框架的驱动程序步骤
- 分配一个
input_dev
结构体,调用input_allocate_device()
或者devm_input_allocate_device(struct input_dev*)
实现; - 编写函数,设置input_dev结构体,选择input设备的事件类型(调用
set_bit()
函数实现); - 注册input_dev结构体,调用
input_register_device(struct input_dev*)
函数实现; - 编写硬件相关代码:注册中断处理函数,比如键盘设备需要编写按键的抬起、放下,触摸屏设备需要编写按下、抬起、绝对移动,鼠标设备需要编写单击、抬起、相对移动,并且需要在必要的时候提交硬件数据(键值/坐标/状态等等),即上报输入事件。
- 当提交输入设备产生的输入事件之后,需要调用
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结构体的定义:
-
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 000e 0001 0026 0000 0000
0000030 0bb2 0000 581f 000e 0000 0000 0000 0000
3.2 如果当前没有启动QT,则按如下方法:
- 执行
cat /dev/tty1
- 开发板上按下:S2、S3、S4
- 可以看到:ls
或者
- 执行
exec 0</dev/tty1
- 可以使用开发板上的键盘来输入ls命令
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, };
- drivers/input/input.c:
-
怎么读按键?
- 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
- intput_open_file
-
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_handler
-
如何 注册输入设备:
-
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函数建立"连接"
-
- 怎么建立连接?
- 分配一个input_handle结构体
- input_handle.dev = input_dev; // 指向左边的input_dev input_handle.handler = input_handler; // 指向右边的input_handler
- 注册: input_handler->h_list = &input_handle; inpu_dev->h_list = &input_handle;
evdev_connect
- evdev = kzalloc(sizeof(struct evdev), GFP_KERNEL); // 分配一个input_handle
- // 设置 evdev->handle.dev = dev; // 指向左边的input_dev evdev->handle.name = evdev->name; evdev->handle.handler = handler; // 指向右边的input_handler evdev->handle.private = evdev;
- // 注册 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);
-
推荐阅读
-
工厂模式详解:让你轻松理解最容易懂的设计模式之一
-
轻松理解继电器驱动工作原理详解
-
理解CPU的C状态与cpuidle驱动器:简单易明的详解
-
轻松理解Java泛型中的通配符:一篇详解文章
-
深入理解Linux驱动中的输入模块(input subsystem)
-
轻松理解Input子系统驱动详解
-
轻松理解进化树详解:图说与评价方法深入解析 - 第三部分
-
【2022新手指南】Java编程进阶之路 - 六、技术架构篇 ### MySQL索引底层解析与优化实战 - 你会讲解MySQL索引的数据结构吗?性能调优技巧知多少? - Redis深度揭秘:你知道多少?从基础到哨兵、主从复制全梳理 - Redis持久化及哨兵模式详解,还有集群搭建和Leader选举黑箱打开 - Zookeeper是个啥?特性和应用场景大公开 - ZooKeeper集群搭建攻略及 Leader选举、读写一致性、共享锁实现细节 - 探究ZooKeeper中的Leader选举机制及其在分布式环境中的作用 - Zab协议深入剖析:原理、功能与在Zookeeper中的核心地位 - RabbitMQ全方位解读:工作模式、消费限流、可靠投递与配置策略 - 设计者视角:RabbitMQ过期时间、死信队列与延时队列实践指南 - RocketMQ特性和应用场景揭示:理解其精髓与差异化优势 - Kafka详细介绍:特性及广泛应用于实时数据处理的场景解析 - ElasticSearch实力揭秘:特性概述与作为搜索引擎的广泛应用 - MongoDB认知升级:非关系型数据库的优势阐述,安装与使用实战教学 - BIO/NIO/AIO网络模型对比:掌握它们的区别与在网络编程中的实际应用 - Netty带你飞:理解其超快速度背后的秘密,包括线程模型分析 - 网络通信黑科技:Netty编解码原理与常用编解码器的应用,Protostuff实战演示 - 解密Netty粘包与拆包现象,怎样有效应对这一常见问题 - 自定义Netty心跳检测机制,轻松调整检测间隔时间的艺术 - Dubbo轻骑兵介绍:核心特性概览,服务降级实战与其实现益处 - Dubbo三大神器解读:本地存根与本地伪装的实战运用与优势呈现 ----------------------- 七、结语与回顾
-
深入理解NVMe Linux驱动教程系列四:SQ与CQ工作机制详解
-
米思齐伴侣(高配版):轻松指南 - 驱动安装与操作详解