一、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
可以看到Name字段是之前驱动里面设置的,Sysfs字段是在加载驱动的时候显示的信息,input9是input子系统分配的,在linux里面搜索可以看到
卸载驱动,重新搜索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,可以看到如下结果
"-------------------------"是事件上报这个过程,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著