linux驱动系列学习之input子系统(二)

一、input子系统简介

        linux系统支持的输入设备众多,例如键盘、鼠标、按键、触摸屏等,linux系统通过抽象出一个input子系统去支持众多的输入设备。input子系统分为三层:上层:输入事件处理层 、中层:输入核心层 、下层:输入设备驱动层。对于驱动部分,关注最多的是下层,输入设备驱动层。

二、输入设备驱动层,以按键驱动为例

        任何驱动都是由入口函数开始、退出函数结束。按键驱动入口函数和退出函数分别为:
 static int __init input_init(void)和static void __exit input_exit(void)。对于驱动使用的各种linux系统资源,将其抽象成一个结构体方便管理,如下:

struct irq_keydesc
{
	int             gpio;                    //gpio
	int             irq;                     //中断号  
	int             value;                   //按键对应的键值
	char            name[10];                //名字 
	irqreturn_t     (*handler)(int,void*);   //中断服务函数 
};  //描述key资源
struct input_device
{ 
	struct device_node  *device_node;          //设备节点
	void                *privative_data;       //私有数据
	struct timer_list   timer;                 //定时器
	struct input_dev    *input_dev;            //input设备结构体 
	int                 key_id;                //按键id
	struct irq_keydesc  irq_keydesc[KEY_CNT];  //中断按键描述
}; //key作为input需要的资源

struct input_device key_input_device;

         注册一个input按键需要需要的资源包括gpio、定时器、中断。在驱动的入口函数里面完成这些工作即可。包括初始化GPIO、注册中断、添加定时器、注册input子系统等。

1. 初始化GPIO
    1.1 从设备树中获取设备节点
        key_input_device.device_node = of_find_node_by_path("/gpio_keys@0/key1@1");
    1.2 根据节点获取GPIO
        key_input_device.irq_keydesc[i].gpio = of_get_named_gpio(key_input_device.device_node, "gpios", 0);
        这里只有一个按键,所以最后一个配置成第0个,返回的是gpio引脚号,通过值可以直接操作gpio,包括gpio_set_value、gpio_direction_output等等。
    1.3 配置GPIO,包括设置为输入模式、配置成中断,为引脚分配中断号
        gpio_request(key_input_device.irq_keydesc[i].gpio, name);    //对gpio做标识,可以不用
        gpio_direction_input(key_input_device.irq_keydesc[i].gpio);  //设置gpio方向
        key_input_device.irq_keydesc[i].irq = gpio_to_irq(key_input_device.irq_keydesc[i].gpio);  //对gpio分配中断号
        
2. 注册中断
    2.1 设置中断回调函数,回调函数类型为:irqreturn_t (*handler)(int,void*);
    key_input_device.irq_keydesc[0].handler = key_0_input_handler;  //设置回调函数
    中断回调函数:static irqreturn_t key_0_input_handler(int irq,void* dev_id);
    2.2 根据分配的中断号,注册中断 
    request_irq(key_input_device.irq_keydesc[i].irq, key_input_device.irq_keydesc[0].handler, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING|IRQF_SHARED, key_input_device.irq_keydesc[i].name, &key_input_device);
    根据刚才分配到的中断号,进行注册,同时传入回调函数、设置中断出发方式等。
    最后一个参数是需要传递给中断回调函数(dev_id参数)
3. 添加定时器
    key_input_device.timer.function = timer_func;   //设置定时器回调函数
    init_timer(&key_input_device.timer);  //初始化定时器
    定时器回调函数:static void timer_func(unsigned long arg);
4. 注册input子系统
    4.1 分配input子系统结构体
    key_input_device.input_dev = input_allocate_device();
    4.2 设置上报事件    
    key_input_device.input_dev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
    input_set_capability(key_input_device.input_dev,EV_KEY,KEY_L); //这里设置的上报类型是按键,按键值为KEY_L,对应的数字是38
    4.3 注册input
    ret = input_register_device(key_input_device.input_dev);
到这里完成了gpio的设置和注册input子系统所需的资源分配。其中用到了两个回调函数,如下:

中断回调函数
static irqreturn_t key_0_input_handler(int irq,void* dev_id)
{    
    //dev_id是在request_irq函数中传入的参数
    struct input_device *input_device = (struct input_device *)dev_id;
    input_device->key_id = 0;   
    input_device->timer.data = (volatile long)dev_id; //将传递给定时器回调函数
    printk("dev_id: %lx\n",dev_id);
    mod_timer(&input_device->timer, jiffies + msecs_to_jiffies(10));  //10ms延时
    return IRQ_RETVAL(IRQ_HANDLED);    
}
定时器回调函数
static void timer_func(unsigned long arg)
{
    int ret = 0;
    int num = 0;
    int value = 0;
    struct irq_keydesc *irq_keydesc;
    struct input_device *input_device  = (struct input_device *)arg;  //input_device->timer.data传过来,获取指针进行操作

    num = input_device->key_id;  //根据key_id确定是哪个按键
    printk("arg: 0x%lx   num: %d\n",arg,num);
    irq_keydesc = &(input_device->irq_keydesc[num]);
    value = gpio_get_value(irq_keydesc->gpio);
    if(value == 0){
        input_report_key(input_device->input_dev, irq_keydesc->value,1);   //1表示按下,0表示松开
        input_sync(input_device->input_dev);  //上报事件
    } else { 
        input_report_key(input_device->input_dev, irq_keydesc->value,0);
        input_sync(input_device->input_dev);
    }
}

本文并不是专门介绍linux下的中断和定时器的文章,中断和定时器部分请参考其他博主的文章,或者本系列后面的文章。

整个驱动的完整代码如下:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 



#define KEY_HTQ_CNT           1             //按键数量
#define KEY_HTQ_NAME          "key_htq"     //名字

struct irq_keydesc
{
	int             gpio;                    //gpio
	int             irq;                     //中断号  
	int             value;                   //按键对应的键值
	char            name[10];                //名字 
	irqreturn_t     (*handler)(int,void*);   //中断服务函数 
};


struct input_device
{ 
	struct device_node  *device_node;          //设备节点
	void                *privative_data;       //私有数据
	struct timer_list   timer;                 //定时器
	struct input_dev    *input_dev;            //input设备结构体 
	int                 key_id;                //按键id
	struct irq_keydesc  irq_keydesc[KEY_CNT];  //中断按键描述
};


struct input_device key_input_device;

static irqreturn_t key_0_input_handler(int irq,void* dev_id)
{	
	struct input_device *input_device = (struct input_device *)dev_id; //将传递给定时器回调函数
	input_device->key_id = 0;
	input_device->timer.data = (volatile long)dev_id;
	printk("dev_id: %lx\n",dev_id);
	mod_timer(&input_device->timer, jiffies + msecs_to_jiffies(10));  //10ms延时
	return IRQ_RETVAL(IRQ_HANDLED);	
}
static void timer_func(unsigned long arg)
{
	int ret = 0;
	int num = 0;
	int value = 0;
	struct irq_keydesc *irq_keydesc;
	struct input_device *input_device  = (struct input_device *)arg;;

	num = input_device->key_id;
	printk("arg: 0x%lx   num: %d\n",arg,num);
	irq_keydesc = &(input_device->irq_keydesc[num]);
	value = gpio_get_value(irq_keydesc->gpio);
	if(value == 0){
		input_report_key(input_device->input_dev, irq_keydesc->value,1);   //1表示按下,0表示松开
		input_sync(input_device->input_dev);  //上报事件
	} else {
		input_report_key(input_device->input_dev, irq_keydesc->value,0);
		input_sync(input_device->input_dev);
	}
}
static int key_input_init(void)
{
	int ret = 0;
	char name[10] = {0};
	int i = 0;
    //获取设备节点
	key_input_device.device_node = of_find_node_by_path("/gpio_keys@0/key1@1");
	if(key_input_device.device_node == NULL){
		printk("key node not find\n");
		return -EINVAL;
	}

	//获取GPIO
	for(i = 0;i < KEY_HTQ_CNT;i++){
		key_input_device.irq_keydesc[i].gpio = of_get_named_gpio(key_input_device.device_node, "gpios", i);
		if(key_input_device.irq_keydesc[i].gpio < 0){
			printk("not get gpio %d\n",i);
			return -EINVAL;
		}
	}

	//初始化io,设置成中断模式
	for(i = 0;i < KEY_HTQ_CNT;i++){
		memset(key_input_device.irq_keydesc[i].name,0,sizeof(key_input_device.irq_keydesc[i].name));
		sprintf(key_input_device.irq_keydesc[i].name,"key_htq%2d",i);
	    gpio_request(key_input_device.irq_keydesc[i].gpio, name);
		gpio_direction_input(key_input_device.irq_keydesc[i].gpio);
		//key_input_device.irq_keydesc[i].irq = irq_of_parse_and_map(key_input_device.device_node,i);  //获取设备树中的中断号
		key_input_device.irq_keydesc[i].irq = gpio_to_irq(key_input_device.irq_keydesc[i].gpio);
	}

	key_input_device.irq_keydesc[0].value = KEY_L;
	key_input_device.irq_keydesc[0].handler = key_0_input_handler;
	printk("dev irq: %d\n",key_input_device.irq_keydesc[i].irq);
	
	//使用中断号,对其注册
	for(i = 0;i < KEY_HTQ_CNT;i++){
		ret = request_irq(key_input_device.irq_keydesc[i].irq, key_input_device.irq_keydesc[0].handler, 
			IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING|IRQF_SHARED, key_input_device.irq_keydesc[i].name, &key_input_device);
		if(ret< 0){
			printk("request_irq  gpio irq %d\n",key_input_device.irq_keydesc[i].irq);
			return -EINVAL;
		}
	}

	//创建定时器
	init_timer(&key_input_device.timer);
	key_input_device.timer.function = timer_func;

	//申请input子系统结构体
	key_input_device.input_dev = input_allocate_device();
	key_input_device.input_dev->name = KEY_HTQ_NAME;

	//设置上报事件
	key_input_device.input_dev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
	input_set_capability(key_input_device.input_dev,EV_KEY,KEY_L);


	//注册input子系统
	ret = input_register_device(key_input_device.input_dev);
	if(ret){
		printk("input_register_device  fail %d\n",ret);
		return -EINVAL;
	}
	printk("key_input_init ok\n");
	return 0;
}


static int __init input_init(void)
{
	key_input_init();
	return 0;
}

static void __exit input_exit(void)
{
	int i = 0;
	del_timer(&key_input_device.timer);  //删除定时器
	
	for(i = 0;i < KEY_HTQ_CNT;i++){
		free_irq(key_input_device.irq_keydesc[i].irq,&key_input_device);   //释放中断号
	}
	input_unregister_device(key_input_device.input_dev);  //注销input子系统
	input_free_device(key_input_device.input_dev);  //释放input子系统结构体资源
	
}
module_init(input_init);
module_exit(input_exit);
MODULE_LICENSE("GPL");

Makefile如下:

KERNELDIR := /home/htq/imx6ull/linux_source
CURRENT_PATH := $(shell pwd)

obj-m := input_key.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules

clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

三、编译驱动和测试

        1. 将驱动文件和Makefile放到linux系统中,make进行编译,得到ko文件,放到开发板里面。使用insmod input_key.ko加载驱动即可看到

 查看驱动相关信息,使用cat /proc/bus/input/devices

linux驱动系列学习之input子系统(二)_第1张图片

可以看到Name字段是之前驱动里面设置的,Sysfs字段是在加载驱动的时候显示的信息,input9是input子系统分配的,在linux里面搜索可以看到

linux驱动系列学习之input子系统(二)_第2张图片

卸载驱动,重新搜索input9,可以看到系统中已经没有这个文件 

 

 input子系统注册的时候会自动分配一个事件,这个可以在/dev/input/路径下看到

目前由0、1、2三个事件, 重新加载驱动,则应该由0、1、2、3四个已分配事件

 这里可以看到已经变成input10了,同时出现了event3这个事件,可以直接编写测试程序去读取这个文件: /dev/input/event3 ,测试程序如下:

#include 
#include 
#include 
#include 
#include 
int main(int argc, char **argv)
{
	int fd;    
    struct input_event ev;

    // 判断参数
    if (argc < 2) {
        printf("Usage: %s \n", argv[0]);
        return 0;
    }

    // 打开设备
    fd = open(argv[1], O_RDWR);
    if (fd < 0) {
        printf("open %s", argv[1]);
        fflush(stdout);
        perror(" ");
        return 0;
    }

    // 循环读取
    while(1) {       
        read(fd, &ev, sizeof(struct input_event));// 读取数据
        printf("ev ==  %x \n",ev.type );  // 打印当前触发类型 
		switch(ev.type) {
        case EV_SYN:  //事件上报
            printf("-------------------------\n");    
            break;
        case EV_KEY:// 按键
            printf("key down / up: %d \n",ev.code );    
            break; 
		defau:break;
    }
    close(fd);
    return 0;	
}
编译测试文件,输入./nfs/keyApp /dev/input/event3,可以看到如下结果

linux驱动系列学习之input子系统(二)_第3张图片

 "-------------------------"是事件上报这个过程,dev_id、arg、num是驱动程序显示的信息,dev_id和arg显示一样,说明传递给arg的值就是dev_id(本质是一个地址),key down / up 可以看到38这个数字,跟最开始设置KEY_L(数字38)是一致的,说明input子系统注册成功。

至此,input子系统入门算是完成,后面鼠标、键盘、触摸屏等以后写到时再谈。

总结:input子系统对现实中使用的各种输入设备进行抽象,同时对linux系统的驱动进行整合成一个框架,驱动开发不再与linux驱动模型打交道(就是fops那些操作,包括read、write、open、release、ioctl等等),对驱动屏蔽linux驱动模型,注册字符设备、分配设备号等过程对input子系统的输入驱动层透明。只对其暴露必要的API函数供驱动层使用,大大简化输入设备驱动的开发。

环境:服务器ubuntu16,正点原子imx6ull开发板emmc版本。

参考书:Linux设备驱动开发详解(基于最新的Linux4.0内核) 宋宝华著

              Linux设备驱动程序   J & G著  

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