八、INPUT子系统和内核自带的GPIO按键驱动

Input子系统驱动框架 = 设备层 + 核心层 + 事件处理层

其中,设备层部分的代码跟具体的输入设备相关,由驱动工程师来具体实现,负责监测并上报具体的输入事件。核心层起承上启下的作用,接受设备层上传的输入事件,然后转发给事件处理层。事件处理层则处理核心层上报的输入事件,负责字符设备驱动那一套,对用户空间提供访问接口。系统框架图如下:
八、INPUT子系统和内核自带的GPIO按键驱动_第1张图片
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_devinput_handlerh_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 命令能查看输入设备节点的详细信息,各个字母表示含义如下:

  • I 对应input_dev结构体里的id成员
  • N 输入设备名称
  • P 输入设备物理信息,比如input/ts表示触摸屏
  • H 表示使用的核心事件层类型
  • B 表示位图信息
  • S 表示现在sysfs中的位置

接口

分配并初始化/释放一个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子系统核心会为设备设置一个定时器用来自动上报长按事件,并将devrep[REP_DELAY]设置为250ms,将devrep[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)非阻塞模式下,有无上报事件都直接返回,不休眠。阻塞模式下,有事件则读取事件并返回,无事件则休眠直到有事件上报。

你可能感兴趣的:(驱动开发,linux)