视频:第19.1讲 Linux INPUT子系统驱动实验-INPUT驱动框架简介_哔哩哔哩_bilibili
资料:《【正点原子】I.MX6U开发指南V1.81》五十八章


一、简介

        和pincttl、gpio等子系统一样,INPUT子系统专门用于处理一类事件——输入事件。对于输入设备(包括按键、触摸屏等),用户只需要上报事件、传入信息,由INPUT子系统来处理事件。

《开发指南》原话:

        input子系统分为input驱动层、input核心层、input事件处理层,最终给用户空间提供可访问的设备节点,input子系统框架如图58.1.1.1所示:

        我们编写驱动程序时只需要关注中间的驱动层、核心层和事件层。这三个层的分工如下:

        驱动层:输入设备的具体驱动程序,比如按键驱动程序,向内核层报告输入内容

        核心层:承上启下,为驱动层提供输入设备注册和操作接口通知事件层对输入事件进行处理。

        事件层:主要和用户空间进行交互

二、驱动函数

2.1 input类

        打开文件/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/drivers/input/input.c,能看到很多熟悉的东西:

struct class input_class = { // 定义一个input类
	.name		= "input",
	.devnode	= input_devnode,
};
…………
// 驱动入口
static int __init input_init(void)
{
	int err;
	err = class_register(&input_class);  // 注册类
    …………
    // 注册字符设备驱动
	err = register_chrdev_region(MKDEV(INPUT_MAJOR, 0),   // 设备号INPUT_MAJOR=13
				     INPUT_MAX_CHAR_DEVICES, "input");    // 设备数量INPUT_MAX_CHAR_DEVICES=1024
    …………
	return 0;
    …………
}
// 驱动出口
static void __exit input_exit(void)
{
	input_proc_exit();
	unregister_chrdev_region(MKDEV(INPUT_MAJOR, 0),   // 注销字符设备驱动
				 INPUT_MAX_CHAR_DEVICES);
	class_unregister(&input_class);    // 注销类
}
subsys_initcall(input_init);
module_exit(input_exit);

        input.c中的代码和之前几次实验写的代码类似,都有注册设备、class等。因此在使用input子系统处理输入设备时,就不需要再进行以上步骤,只需要向系统注册一个input设备即可。

2.2 input_dev结构体

        使用input_dev结构体来表示一个input设备,定义如下:

// 定义在include/linux/input.h
// 这里我直接搬了《开发指南》的示例代码58.1.2.2中的注释
// 注释后面的“对应XXX部分”指的是对应文件include/uapi/linux/input.h中的对应注释,直接ctrl+f搜索即可
struct input_dev {
	const char *name;
	const char *phys;
	const char *uniq;
	struct input_id id;
	unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)];
	unsigned long evbit[BITS_TO_LONGS(EV_CNT)];    // 事件类型的位图  对应Event types部分
	unsigned long keybit[BITS_TO_LONGS(KEY_CNT)];  // 按键值的位图    对应Keys and buttons部分
	unsigned long relbit[BITS_TO_LONGS(REL_CNT)];  // 相对坐标的位图  对应Relative axes部分
	unsigned long absbit[BITS_TO_LONGS(ABS_CNT)];  // 绝对坐标的位图  对应Absolute axes部分
	unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)];  // 杂项事件的位图  对应Misc events部分
	unsigned long ledbit[BITS_TO_LONGS(LED_CNT)];  // LED相关的位图   对应LEDs部分
	unsigned long sndbit[BITS_TO_LONGS(SND_CNT)];  // sound有关的位图 对应Sounds部分
	unsigned long ffbit[BITS_TO_LONGS(FF_CNT)];    // 压力反馈的位图  对应Values describing the status of a force-feedback effect部分
	unsigned long swbit[BITS_TO_LONGS(SW_CNT)];    // 开关状态的位图  对应Switch部分
    …………
};

        以事件类型evbit为例,其取值可以为以下内容:

// 从include/uapi/linux/input.h的174行起
/* 这里的注释来自《开发指南》的示例代码58.1.2.3
 * Event types
 */
#define EV_SYN           0x00    /* 同步事件   */
#define EV_KEY           0x01    /* 按键事件   */
#define EV_REL           0x02    /* 相对坐标事件  如鼠标的坐标都是计算相对上一时刻的位移 */
#define EV_ABS           0x03    /* 绝对坐标事件  如触摸屏,触摸的是屏幕坐标系中的某个位置 */
#define EV_MSC           0x04    /* 杂项(其他)事件  */
#define EV_SW            0x05    /* 开关事件   */
#define EV_LED           0x11    /* LED    */
#define EV_SND           0x12    /* sound(声音)  */
#define EV_REP           0x14    /* 重复事件   如长按键盘上的键,会打出很长一串*/
#define EV_FF            0x15    /* 压力事件   */
#define EV_PWR           0x16    /* 电源事件   */
#define EV_FF_STATUS     0x17    /* 压力状态事件  */

2.3 申请/释放 input_dev

// 申请:
struct input_dev *input_allocate_device(void)
// return:申请到的input_dev
// 这只是一个空的内核对象,需要后面注册时填入参数
// 释放:
void input_free_device(struct input_dev *dev)
// dev:需要释放的input_dev

2.4 注册/注销 input_dev

        申请到input_dev以后,需要初始化事件类型evbit和事件值(如本次使用按键,就需要初始化keybit)。经过注册,/sys/class/input/就能找到对应的设备了,同时/dev/input/下也会出现“eventX”(X=0….n),这个/dev/input/eventX就是对应的input设备文件。

// 注册:
int input_register_device(struct input_dev *dev)
// dev:   要注册的input_dev
// return:0则成功,负值则失败
// 注销:
void input_unregister_device(struct input_dev *dev)
// dev:要注销的input_dev

2.5 事件上报——input_event函数

       事件发生后 需要通知给内核,同时还需要将事件值一并上报,如发生按键事件时按键的值等等。

// 定义在drivers/input/input.c
void input_event(struct input_dev *dev, // 需要上报的input_dev
                 unsigned int type,     // 上报的事件类型evbit,具体值详见2.2
                 unsigned int code,     // 事件码
                 int value);            // 事件值

        linux针对一些具体事件提供包装好的上报函数,但本质还是在调用input_event:

// 定义在include/linux/input.h
static inline void input_report_key(struct input_dev *dev, unsigned int code, int value)
static inline void input_report_rel(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_report_ff_status(struct input_dev *dev, unsigned int code, int value)
static inline void input_report_switch(struct input_dev *dev, unsigned int code, int value)

        上报事件后还需要用input_sync函数来告诉Linux内核上报结束。input_sync函数本质是上报一个同步事件EV_SYN:(详见3.8)

static inline void input_sync(struct input_dev *dev){ // dev:要上报同步事件的input_dev
	input_event(dev, EV_SYN, SYN_REPORT, 0);
}

综上,事件上报的示例代码:(来自《开发指南》示例代码58.1.2.7)

/* 用于按键消抖的定时器服务函数 */
void timer_function(unsigned long arg){
  unsigned char value;
  value = gpio_get_value(keydesc->gpio);  /* 读取IO值 */
  if(value == 0){                         /* 按下按键 */
    /* 上报按键值=1 */
    input_report_key(inputdev, KEY_0, 1); /* 最后一个参数1,按下 */
    input_sync(inputdev);                 /* 同步事件 */
  } else {                                /* 按键松开 */
    /* 上报按键值=0 */
    input_report_key(inputdev, KEY_0, 0); /* 最后一个参数0,松开 */
    input_sync(inputdev);                 /* 同步事件 */
  }
}

2.6 input_event结构体

       各种事件(如按键,触摸屏,鼠标等等)都是使用input_event()来上报,这就需要一个统一的结构体,能表示所有输入事件。linux内核使用input_event结构体表示所有输入事件:

// 定义在include/uapi/linux/input.h
struct input_event {
	struct timeval time;
	__u16 type;   // 事件类型,如按键事件EV_KEY   unsigned short  16位
	__u16 code;   // 事件码,如KEY_0             unsigned short  16位
	__s32 value;  // 具体的值,如按键的状态0/1    int   32位
};

        结构体中的type、code、value即为input_event上报的type、code、value。

input_event结构体中,成员timeval的定义为:

// 定义在time.h
struct timeval
{
  __kernel_time_t tv_sec;		    // Seconds. 秒         long类型 32位
  __kernel_suseconds_t tv_usec;	    // Microseconds. 微秒  long类型 32位
};

对input_event数据的具体分析可见3.5。

三、实验

        本次实验在中断代码基础上修改而来。

3.1 文件结构

21_INPUT (工作区)
├── .vscode
│   ├── c_cpp_properties.json
│   └── settings.json
├── 21_input.code-workspace
├── Makefile
├── keyinput.c
└── keyinputAPP.c

3.2 Makefile

CFLAGS_MODULE += -w
KERNELDIR := /.../linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek  # 内核路径
# KERNELDIR改成自己的 linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek文件路径(这个文件从正点原子“01、例程源码”中直接搜,cp到虚拟机里面)
CURRENT_PATH := $(shell pwd)	# 当前路径
obj-m := keyinput.o			    # 编译文件
build: kernel_modules			# 编译模块
kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

3.3 设备树

        依照中断这一节的写法,在arch/arm/boot/dts/imx6ull-alientek-emmc.dts中的根节点/下添加按键节点:

	key{
		compatible = "alientek,key";
		pinctrl-names = "default";
		pinctrl-0 = <&pinctrl_key>;
		key-gpios = <&gpio1 18 GPIO_ACTIVE_HIGH>;
		status = "okay";
		interrupt-parent = <&gpio1>;
		interrupts = <18 IRQ_TYPE_EDGE_BOTH>;
	};

然后编译并放置到tftp路径下,并重启开发板加载设备树:

make dtbs  # 编译
cp arch/arm/boot/dts/imx6ull-alientek-emmc.dtb /.../tftpboot/  # 编译出的dtb文件放置到tftp路径下
重启开发板

3.4 驱动代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#define KEYINPUT_CNT  1          /* 设备号数量 */
#define KEYINPUT_NAME "keyinput"    /* 设备名 */
#define KEY_NUM 1           /* 按键数量 */
#define INVAKEY    0xFF     /* 按键无效值 */
/* 按键结构体 */
struct irq_keydesc{
    int gpio;   /* io号 */
    int irqnum; /* 中断号 */
    unsigned char value; /* 键值 (默认/空闲电平,比如 1) */
    char name[10];  /* 名字 */
    irqreturn_t (*handler) (int, void *);   /* 中断处理函数*/
    struct tasklet_struct tasklet;
};
/* keyinput设备结构体 */
struct keyinput_dev_struct{
    struct device_node *nd; // 节点
    struct irq_keydesc irqkey[KEY_NUM]; // KEY_NUM 个按键
    struct timer_list timer;// 定时器
    struct input_dev *inputdev; // 输入设备结构体input_dev
};
static struct keyinput_dev_struct keyinput_dev;
/* 中断处理函数 */
static irqreturn_t key0_handler(int irq, void *dev_id){
    struct keyinput_dev_struct *dev = (struct keyinput_dev_struct*)dev_id;
    int current_level;  // 保存中断时的电平
    current_level = gpio_get_value(dev->irqkey[0].gpio);
    tasklet_schedule(&dev->irqkey[0].tasklet); // 调度tasklet处理后续逻辑,避免中断耗时
    return IRQ_HANDLED;
}
/* 定时器处理函数 */
static void timer_func(unsigned long arg){
    int value = 0;
    struct keyinput_dev_struct* dev = (struct keyinput_dev_struct*)arg;
    value = gpio_get_value(dev->irqkey[0].gpio);
    if(value == 0){ // 按下
        input_event(dev->inputdev, EV_KEY, KEY_0, 1); // 上报 按键事件EV_KEY,按键为KEY_0,按键值为1
        input_sync(dev->inputdev);
    }else if(value == 1){  // 释放
        input_event(dev->inputdev, EV_KEY, KEY_0, 0); // 上报 按键事件EV_KEY,按键为KEY_0,按键值为0
        input_sync(dev->inputdev);
    }
}
/* tasklet处理函数 */
static void key_tasklet(unsigned long data){
    struct keyinput_dev_struct* dev = (struct keyinput_dev_struct*)data;
    printk("key_tasklet\r\n");
    dev->timer.data = data;
    mod_timer(&dev->timer, jiffies + msecs_to_jiffies(20)); /* 20ms定时 */
}
/* 初始化按键 */
static int keyio_init(struct keyinput_dev_struct *dev){
    int ret = 0;
    int i = 0;
    /* 1. 按键初始化 */
    dev->nd = of_find_node_by_path("/key");  // 获取设备节点
    if(dev->nd == NULL){
        printk("获取设备节点of_find_node_by_path failed!!\r\n");
        ret = -EINVAL;
        goto fail_nd;
    }
    for(i=0; iirqkey[i].gpio = of_get_named_gpio(dev->nd, "key-gpios", i); // 获取设备树中 gpio
        if (dev->irqkey[i].gpio < 0) {
            pr_err("get gpio %d failed\n", i);
            ret = -EINVAL;
            goto fail_nd;
        }
    }
    for(i=0; iirqkey[i].name, 0, sizeof(dev->irqkey[i].name));
        sprintf(dev->irqkey[i].name, "KEY%d", i);   // 命名
        ret = gpio_request(dev->irqkey[i].gpio, dev->irqkey[i].name); // 申请gpio
        if (ret) {
            pr_err("gpio_request %d failed\n", dev->irqkey[i].gpio);
            goto fail_gpio_req;
        }
        gpio_direction_input(dev->irqkey[i].gpio);  // 设置io方向
        dev->irqkey[i].irqnum = gpio_to_irq(dev->irqkey[i].gpio);// 获取中断号
        if (dev->irqkey[i].irqnum < 0) {
            pr_err("gpio_to_irq failed for gpio %d\n", dev->irqkey[i].gpio);
            ret = dev->irqkey[i].irqnum;
            goto fail_gpio_req;
        }
    }
    dev->irqkey[0].handler = key0_handler; // 中断处理函数
    /* 2. 中断初始化 */
    for(i=0; iirqkey[i].irqnum,
                          dev->irqkey[i].handler,
                          IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
                          dev->irqkey[i].name,
                          &keyinput_dev); /* dev_id 传入结构体指针 */
        if(ret < 0){
            printk("irq %d request failed! ret=%d\r\n", dev->irqkey[i].irqnum, ret);
            while (--i >= 0) {
                free_irq(dev->irqkey[i].irqnum, &keyinput_dev);
            }
            goto fail_irq;
        }
        tasklet_init(&dev->irqkey[0].tasklet, key_tasklet, (unsigned long)dev);
    }
    return 0;
fail_irq:
fail_gpio_req:
    for (i = 0; i < KEY_NUM; i++) {
        if (gpio_is_valid(dev->irqkey[i].gpio))
            gpio_free(dev->irqkey[i].gpio);
    }
fail_nd:
    return ret;
}
/* 驱动入口 */
static int __init key_init(void){
    int ret = 0;
    /* 初始化IO*/
    ret = keyio_init(&keyinput_dev);
    if(ret < 0){
        goto fail_keyinit;
    }
    /* 初始化定时器 */
    init_timer(&keyinput_dev.timer);
    keyinput_dev.timer.function = timer_func;
    keyinput_dev.timer.data = (unsigned long)&keyinput_dev; /* 初始化 timer.data 指向设备结构体 */
    keyinput_dev.inputdev = input_allocate_device(); // 申请input_dev
    if(keyinput_dev.inputdev == NULL){
        printk("申请input_dev失败!\r\n");
        ret = -EINVAL;
        goto fail_keyinit;
    }
    keyinput_dev.inputdev->name = KEYINPUT_NAME;
    __set_bit(EV_KEY, keyinput_dev.inputdev->evbit); // 按键事件
    __set_bit(EV_REP, keyinput_dev.inputdev->evbit); // 重复事件
    __set_bit(KEY_0, keyinput_dev.inputdev->keybit); // 另按键对应KEY_0
    ret = input_register_device(keyinput_dev.inputdev);    // 注册input_dev
    if(ret){
        goto fail_input_register;
    }
    return 0;
fail_input_register:
    input_free_device(keyinput_dev.inputdev);  // 释放input_dev
fail_keyinit:
    return ret;
}
/* 驱动出口 */
static void __exit key_exit(void){
    int i=0;
    /* 释放中断 */
    for(i=0; i

3.5 input_event上报数据格式

# VSCODE终端
make
sudo cp keyinput.ko /.../nfs/rootfs/lib/modules/4.1.15/
# 串口
ls /dev/input/  # 此时应当输出:event0 mice
depmod
modprobe keyinput.ko   # 此时应有输出:input: keyinput as /devices/virtual/input/input1
ls /dev/input/  # 此时应当输出:event0 event1 mice
hexdump /dev/input/event1  # 查看event1的原始数据。按下和松开都会输出两行数据,长按会输出连续的数据

        这些输出的数据就是上面2.6提到的input_event的内容。input_event的成员从前到后依次为:
秒tv_sec(32位),微秒tv_usec(32位),事件类型type(16位),事件码code(16位),值value(32位)。因此上面这张图的内容可以翻译为:

             编号          秒           微秒      事件类型   事件码        值
key_tasklet(此时按下按键)
           0000000     b638 0002     751e 0009     0001    000b    0001 0000
           0000010     b638 0002     751e 0009     0000    0000    0000 0000
key_tasklet(此时释放按键)
           0000020     b638 0002     49ee 000b     0001    000b    0000 0000
           0000030     b638 0002     49ee 000b     0000    0000    0000 0000
key_tasklet(按下)
           0000040     b639 0002     e139 0004     0001    000b    0001 0000
           0000050     b639 0002     e139 0004     0000    0000    0000 0000
key_tasklet(释放)
           0000060     b639 0002     b1c0 0008     0001    000b    0002 0000
           0000070     b639 0002     b1c0 0008     0000    0000    0001 0000
key_tasklet(开始长按)
           0000080     b639 0002     4e01 0009     0001    000b    0002 0000
           0000090     b639 0002     4e01 0009     0000    0000    0001 0000
           00000a0     b639 0002     ea40 0009     0001    000b    0002 0000
           00000b0     b639 0002     ea40 0009     0000    0000    0001 0000
           00000c0     b639 0002     8684 000a     0001    000b    0002 0000
           …………

        事件类型:0001为按键事件EV_KEY;0000为同步事件EV_SYN
        事件码:   000b即为十进制11,为KEY_0;0000为KEY_RESERVED,表示占位/无效(定义在input.h)
        值:即为上报的值,本次实验中定义0表示释放、1表示按下。

        看弹幕在争大小端的事。查了一下,x86/ARM都是小端存储,只是打印方式的问题导致低位在前高位在后。
        每次事件上报后面都会跟一个同步事件EV_SYN,具体分析在3.8中。

3.6 应用程序代码

#include
#include
#include
#include
#include
#include
#include
#include
#include 
#define LEDOFF 0
#define LEDON  1
/*
 * @description    : main主程序
 * @param - argc   : argv数组元素个数
 * @param - argv   : 具体参数
 * @return         : 0 成功; else失败
 * 调用  ./keyintAPP /dev/input/event1
 */
static struct input_event inputevent;
int main(int argc, char *argv[]){
    if(argc != 2){
        printf("Error Usage!\r\n");
        return -1;
    }
    int fd, err;
    char *filename;
    filename = argv[1];
    fd = open(filename, O_RDWR);
    if(fd <0){
        printf("file %s open failed!\r\n",filename);
        return -1;
    }
    while(1){
        err = read(fd, &inputevent, sizeof(inputevent));
        if(err > 0){  // 读取成功
            switch(inputevent.type){
                case EV_KEY:
                    if(inputevent.code < BTN_MISC){ // 在input.h中可以看到,从0到BTN_MISC以前的编号都是KEY
                        printf("=KEY%d %s=\r\n",inputevent.code, inputevent.value?"press":"release");
                    } else {
                        // 可以根据自己的code值进行编写
                    }
                    break;
                case EV_SYN:
                    printf("          SYN EVENT\r\n");
                    break;
                default: // 剩下的懒得写了
                    break;
            }
        } else {
            printf("read failed!\r\n");
        }
    }
    return 0;
}

3.7 测试

# VSCODE终端
make
arm-linux-gnueabihf-gcc keyinputAPP.c -o keyinputAPP
sudo cp keyinputAPP keyinput.ko /.../nfs/rootfs/lib/modules/4.1.15/
# 串口
depmod
modprobe keyinput.ko   # 此时应有输出:input: keyinput as /devices/virtual/input/input1
./keyinputAPP /dev/input/event1  # 按下按键应有对应的输出

3.8 各种问题

input_sync的问题:
        如2.5中所述,input_event上报事件后还要跟一个input_sync同步事件。input_sync定义如下:

static inline void input_sync(struct input_dev *dev)
{
	input_event(dev, EV_SYN, SYN_REPORT, 0);
}
// 这里的EV_SYN、SYN_REPORT都是0(定义在input.h),也就是3.5中看到的事件码和值都是0的SYN事件

        input_event()只是把事件写入input缓冲,而input_sync()会产生一个同步事件EV_SYN/ SYN_REPORT,此时就会将缓冲里的事件送到用户态。
        可以试一下,把按下或者释放后面的input_sync去掉一个、保留一个,会发现只有在执行input_sync时用户态才read到事件。


        CPU占用问题:

        让应用程序后台运行,用ps查看会发现cpu占用很低。用户态进程是阻塞睡眠的。


        长按的问题:

        当按下按键时,边沿触发了中断。之后长按,因为没有边沿,因此不会再触发中断。但因为打开了重复事件EV_REP,input子系统在长按时会按照一定频率不断产生EV_KEY事件。

四、linux自带的按键驱动程序

第19.5讲 Linux INPUT子系统驱动实验-内核自带按键驱动程序_哔哩哔哩_bilibili