Input子系统驱动框架 = 设备层 + 核心层 + 事件处理层
其中,设备层部分的代码跟具体的输入设备相关,由驱动工程师来具体实现,负责监测并上报具体的输入事件。核心层起承上启下的作用,接受设备层上传的输入事件,然后转发给事件处理层。事件处理层则处理核心层上报的输入事件,负责字符设备驱动那一套,对用户空间提供访问接口。系统框架图如下:
Input子系统的实现也借鉴了总线技术,核心层作为联系设备层和事件处理层的桥梁,可以将它类比为一条总线。总线下面有一条设备链表和事件处理链表。调用input_register_device()
向核心层注册输入设备时,把input_dev
挂到设备链表下;调用input_register_handler()
向核心层注册事件处理handler时,把input_handler
挂到事件处理链表下。
注册输入设备或事件处理handler时,设备链表和事件处理链表会一一进行匹配,如果设备和事件处理handler匹配上,则input_handler
里面的connet
函数会被调用。在connet
函数里面,匹配上的双方被放入一个input_handle
的结构体里面(.dev
和.handler
成员),并且该input_handle
结构体被分别放入input_dev
和input_handler
的h_list
链表里里面,这样通过input_dev
可以找到与之匹配的事件处理handler,通过input_handler
也可以找到与之匹配的设备。除此之外,connet
函数还向内核注册一个字符设备,为用户提供驱动访问的接口。
当我们调用input_event()
来上报事件时,该函数就通过input_dev来找到对应的input_handler,按照优先级顺序选择调用其中的filter
, events
, event
方法来处理数据。
使用Input子系统编写输入设备驱动的思路:
由于核心层和事件处理层内核都已经做好了,所以我们只需要关心设备层
1)构造设备树
2)借助platform总线,在probe函数里面构造struct input_dev
, 然后调用input_register_device()
,将设备注册到Input子系统
3] 调用input_register_handler()
,将事件处理hanler注册到Input子系统(这一步不是必须的,因为内核里面有一个通用的事件处理handler(位于evdev.c),这个handler在内核启动的时候已经注册,可以匹配所有的input_dev。也就是说,对每个input_dev上报的事件,至少都会有一个handler进行处理。)
如何调试:
使用 ls /dev/input/*
命令能查看有哪些输入设备节点。
使用 cat /proc/bus/input/devices
命令能查看输入设备节点的详细信息,各个字母表示含义如下:
接口
分配并初始化/释放一个input_dev结构体
struct input_dev *devm_input_allocate_device(struct device *dev);
struct input_dev *input_allocate_device(void);
void input_free_device(struct input_dev *dev);
注册/注销输入设备
int input_register_device(struct input_dev *dev);
void input_unregister_device(struct input_dev *dev);
设置输入设备支持的事件类型/编号
__set_bit(nr, vaddr);
void input_set_capability(struct input_dev *dev, unsigned int type, unsigned int code);
上报输入事件
void input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value);
static inline void input_report_key(struct input_dev *dev, unsigned int code, int value);
static inline void input_report_key(struct input_dev *dev, unsigned int code, int value);
static inline void input_report_abs(struct input_dev *dev, unsigned int code, int value);
static inline void input_sync(struct input_dev *dev);
注意,上报数据时并不是直接上报,而是先放到一个循环队列里面,当上报SYNC同步事件时,才表示输入数据上报完成。
使用Input子系统实现的内核按键驱动
设备树
gpio_keys {
compatible = "gpio-keys";
#address-cells = <1>;
#size-cells = <0>;
autorepeat;
button@21 {
label = "GPIO Key UP";
linux,code = <103>;
gpios = <&gpio1 0 1>;
};
button@22 {
label = "GPIO Key DOWN";
linux,code = <108>;
interrupts = <1 IRQ_TYPE_LEVEL_HIGH 7>;
};
}
几点说明:
1)外层使用一个gpio_keys的节点,表示一个按键设备,其compatible属性为"gpio-keys",表示使用内核提供的gpio按键驱动。
2)因为一个按键设备可能有多个按键(比如键盘),所以用子节点表示各个按键,这些按键使用GPIO引脚与主控相连。
3)父节点设置属性autorepeat,表示为按键上报长按事件。
4)使用gpios属性和interrupts属性都可以使用中断,但是驱动对两种形式的处理不同,因为gpios方式能根据引脚电平判断按键是按下还是弹起,推荐使用这种形式。
5)当使用gpios属性指定中断时,使用的中断处理函数为gpio_keys_gpio_isr()
,内核work函数为gpio_keys_gpio_work_func()
6) 子节点里可以通过设置debounce-interval属性,来为按键设置消抖延时时间,如果没有设置则驱动默认使用5ms
7) 消抖会优先使用gpiolib提供的引脚硬件消抖,如果gpiolib没有实现硬件消抖,则使用定时器消抖
GPIO按键驱动的内核配置路径
-> Device Drivers
->Input device support
->Generic input layer
->Keyboards
<*> GPIO Buttons
关于按键长按的几点说明
1)如果input_register_device()
注册的input_dev
设置了rep
成员的REP_DELAY或REP_PERIOD值,则input子系统核心认为设备驱动(我们)将自行上报长按事件,核心层则不会进行上报。
2)如果input_dev
没有设置rep
成员的REP_DELAY和REP_PERIOD值,则input子系统核心会为设备设置一个定时器用来自动上报长按事件,并将dev
的rep[REP_DELAY]
设置为250ms,将dev
的rep[REP_PERIOD]
设置为33ms,这两个参数是内核头文件里的宏定义,不可通过设备树配置。
3)长按支持的实现过程如下:
3.1)input_register_device()里面初始化了一个定时器:
dev->timer.data = (long) dev;
dev->timer.function = input_repeat_key;
dev->rep[REP_DELAY] = 250; //250ms后开始上报长按事件
dev->rep[REP_PERIOD] = 33; //每间隔33ms上报一次长按事件
3.2)最底层的按键驱动只需要负责消抖,并且上报按下和弹起事件。
3.3)当按键调用input_event()上报按下事件时,如果按键开启了长按AUTO_REPEATE标志,则会调用input_start_autorepeat(),该函数做了如下事情:
3.3.1)设置产生长按的CODE,dev->repeat_key = code;
3.3.1)启动定时器dev->timer,定时时间为dev->rep[REP_DELAY]毫秒
3.3.3) 定时时间到后,执行input_repeat_key();
3.3.4) input_repeat_key()函数会构造一个长按事件 + 同步事件,上报后再次启动定时器,定时rep[REP_PERIOD]毫秒,如此反复上报长按事件,直到按键松开时删除该定时器
3.4)当按键调用input_event()上报松开事件时,如果按键开启了长按AUTO_REPEATE标志,则会调用input_stop_autorepeat(),该函数删除自动上报定时器:
del_timer(&dev->timer);
关于read的几点说明
1)input子系统核心字符设备驱动相关的代码源文件为evdev.c
2)应用打开输入设备(比如/dev/input/event0)的时候,驱动的open函数会为其分配一个evdev_client
的结构体,用来保存设备句柄,以及缓存该设备上报的事件。所以,open()之前的按键事件将不会被缓存,也不会被应用获取到。
3)当应用调用read函数读取输入设备时,驱动的read函数会从fd句柄对应的evdev_client
结构体里面取出缓存的事件(struct input_event
),返回给应用层。
4)驱动的read函数会根据应用层提供的buffer的大小,使用一个while()循环尽量多地返回上报事件,直到buffer不足以再多放一个事件。
5)非阻塞模式下,有无上报事件都直接返回,不休眠。阻塞模式下,有事件则读取事件并返回,无事件则休眠直到有事件上报。