18.2基于Linux的INPUT子系统实验(详细编写程序)_csdn
我尽量讲的更详细,为了关注我的粉丝!!!
这里我们将 STM32MP1 开发板上的三个按键: KEY0、 KEY1 和 WK_UP 都设置了,用的还是platform和pinctrl来实现,所以要在stm32mp15-pinctrl.dtsi 文件更改按键的节点。同时也要在stm32mp157d-atk.dts文件,修改key的设备节点。
1、添加 pinctrl 节点:
首先在 stm32mp15-pinctrl.dtsi 文件中创建按键对应key的 pinctrl 子节点:
编译一下这个镜像:
make uImage LOADADDR=0XC2000040 -j8 //编译内核
然后:
2、修改设备树文件:
首先打开 stm32mp157d-atk.dts,创建按键设备节点:
这里我们只测试按键KEY0,也就是PG3。低电平有效。并且有中断号,有pinctrl,那必然要用platform_driver。还有申请中断。
3、回顾知识点:
按键驱动;我们之前需要有什么?
申请字符设备,cdev,class,device,定时器,中断,原子变量(不能是自旋锁,这个中断需要快速,而且是要用异步通知,降低 CPU 使用率)、设备节点(这个在之前进行pdev传递就行)、gpio编号,队列可能也要。
现在呢:
1、因为input子系统的所有设备主设备号都为 13,我们在使用 input 子系统处理输入设备的时候就不需要去注册字符设备了,我们只需要向系统注册一个 input_device 即可。
2、不用自己分配cdev、device、class:
1. class
方面:无需手动编写 class
。input 子系统自带全局的 input_class
,所有注册到该系统的设备会自动归入此类别,在 /sys/class/input
目录下能看到对应设备节点。
2. device
方面:不用专门编写通用的 struct device
结构体实例。input 子系统提供了更高级别的抽象 struct input_device
,注册 input_device
时,系统会在内部处理与 device
相关的操作,如设备注册、注销以及与系统设备模型的集成。
3. cdev
方面:不必手动注册字符设备(cdev
)。input 子系统对字符设备的注册和注销操作进行了封装,使用固定主设备号(主设备号为 13),注册 input_device
时,内核会自动完成字符设备相关操作,包括分配次设备号、初始化 cdev
结构体等。
3、我们要:
①、使用 input_allocate_device 函数申请一个 input_dev。
②、初始化 input_dev 的事件类型以及事件值。
③、使用 input_register_device 函数向 Linux 系统注册前面初始化好的 input_dev。
④、卸载input驱动的时候需要先使用input_unregister_device函数注销掉注册的input_dev,然后使用 input_free_device 函数释放掉前面申请的 input_dev。
当在 Linux 内核中使用 input 子系统创建并注册一个 input_device
时,除了前面提到的自动处理 class
、device
和 cdev
相关内容外,还会自动创建以下一些东西:
1. 设备节点
在 /dev/input
目录下会自动创建对应的设备节点文件。这些设备节点是用户空间程序与输入设备进行交互的接口。例如,常见的有 /dev/input/eventX
(X
是一个数字),应用程序可以通过读取这些设备节点来获取输入设备产生的事件。
2. sysfs 条目
在 /sys/class/input
目录下会为该输入设备创建一个对应的子目录,目录名通常与设备相关,比如 eventX
。在这个子目录中,会包含一系列用于描述和控制设备的属性文件,这些文件允许用户空间程序通过读取或写入它们来获取设备信息或进行一些配置。例如:
name
:包含设备的名称。
phys
:表示设备的物理路径。
id
:包含设备的一些标识信息,如厂商 ID、产品 ID 等。
3. udev 规则匹配与处理
当输入设备注册时,udev (Linux 下的设备管理器)会检测到设备的变化,并根据预先定义的规则进行处理。这可能包括为设备节点设置合适的权限、创建符号链接等操作,以便用户空间程序更方便地访问和使用设备。例如,udev 可以为特定类型的输入设备创建一个更具描述性的符号链接,使得应用程序可以通过这个符号链接来访问设备,而不必关心具体的设备节点名。
4、编写代码程序
之前的博客也是跟大家按照肌肉记忆来编写程序!一步一步按照思路来编写!
总代码会放在最后。
为了让大家更能明白,可以先对着总代码,进行对我的写代码流程更加详细得当!
看正点原子的代码,并没有使用阻塞和非阻塞、异步通知。所以这里我们可以加或者不加,作用只是降低 CPU 使用率。
4.1、头文件
可以看到相比之前的实验多了什么,少了什么:
多了input.h。
因为input子系统自动创建了字符设备,cdev,class,device。所以没了
#include <linux/device.h>
、#include <linux/cdev.h>
、
下面这个是正点原子提供的头文件目录:
头文件也没有init.h。 ==呜呜==
背刺了:
问了AI:现代 Linux 内核开发里,一般不需要显式包含 init.h
。模块的初始化和退出函数通常使用 module_init
和 module_exit
宏来定义,而这些宏在 linux/module.h
头文件中已经定义好了。但是之前的实验一直有!!!
头文件也没有gpio.h。
of_gpio.h
依赖于 gpio.h
,使用了 of_get_named_gpio
函数从设备树中获取 GPIO 信息,这样的话就可以选择不用gpio.h。当然这也不是死的,按照实际情况来,是否包含 gpio.h
取决于你使用的 API。如果使用底层的 GPIO 操作函数,就需要包含 gpio.h
;如果使用更高级别的抽象 API,像 of_gpio.h
中定义的函数,就可以不直接包含 gpio.h
,不过这些高级 API 最终还是会依赖 gpio.h
里的函数。
没有uaccess.h就很离谱:
作用是copy_from_user,copy_to_user通过操作集来实现用户空间和内核完成数据交互。
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/input.h>
#include <linux/timer.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
这些就不过多介绍了。
用我的:
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/input.h>
#include <linux/timer.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <asm/uaccess.h>
接下来我来写后面的代码。
4.2、驱动注册和注销
代码也没有init和exit。
代码中有这个东西module_platform_driver
宏的作用:
在 Linux 内核开发里,module_platform_driver
是一个宏,其定义在 <linux/platform_device.h>
中。这个宏的主要功能是简化 platform
驱动的注册和注销过程。它把 platform_driver
的注册和注销操作封装起来,这样就无需手动编写 module_init
和 module_exit
函数了。
这个地方好像在Linux的自带LED实验讲到过。这里就当做回顾!
这样就不要module_init和module_exit;
有了platform注册和注销驱动后,就要编写platform_driver驱动结构体:跟之前的字符驱动注册差不多!后续要进行probe和remove,主要的工作都在这里!
这里你可能会问,input子系统本身就是自动创建了字符设备,cdev,class,device。为什么还要module_platform_driver。因为module_platform_driver本身并没有创建字符设备,cdev,class,device的功能!
作用:利用platform注册和注销驱动。驱动的分离和分层!
4.2.1、编写platform_driver驱动结构体
这个name主要在驱动中显示:/driver中。若注册成功就会在driver显示:stm32mp1-key。
而设备树匹配表:
主要匹配设备树下的compatible = “alientek,key”;值
1.编写设备树匹配表:
数组最后一个元素必须为空;
MODULE_DEVICE_TABLE(of,key_of_match);//声明这个匹配表
一旦匹配表中的:compatible匹配成功,就会执行:
.probe = atk_key_probe,
.remove = atk_key_remove,
2.编写probe函数和remove函数:
4.3、补充probe和remove函数
添加匹配成功后的信息:
一旦匹配成功,打印出:key driver and device was matched!
4.3.1、配置key中设备结构体的input子系统
之前在probe和remove函数内存放着注册和注销字符设备,及GPIO的初始化配置!
由于我们在上一章讲到过
input子系统的所有设备主设备号都为 13,不用自己申请设备,也不用自己分配cdev、device、class。
1、首先配置设备结构体:
2、使用input子系统来申请 input_dev:
流程:申请->初始化事件->注册->注销->删除:
其中宏定义名字和次设备号:
总结:
定义了input_dev类型的子系统指针,idev实际上来储存地址,用input_allocate_device()来取地址申请子系统初始地址。然后idev来指向input_dev下的name类,定义事件子系统名字,其他设备号,cdev,class,device都不用我们管了,有点像misc驱动实验了,都打包了,不过misc驱动主设备号是10,这里的主设备号是13。
3、初始化 input_dev 的事件类型以及事件值。
流程:申请->初始化事件->注册->注销->删除:
需要初始化的内容主要为事件类型(evbit)和事件值(keybit)这两种
这里有三种方法来初始化evbit和keybit:
作用:就只是上报按键发生了什么事情而已,没有太大的玄妙!
上面的三选一即可!
讲解代码:
1.__set_bit
是内核里的一个宏,其作用是把指定位置的位设置为 1。这里把 key.idev->evbit
中对应 EV_KEY
的位设置为 1,意味着该输入设备能够产生按键事件。这只是表明该输入设备具备产生这些类型事件的能力,并非是对按键按下(状态值为 1)这个状态进行初始化。这仅仅是告诉内核这个输入设备能够产生按键事件和重复事件和产生事件的条件,和按键当前处于按下还是释放状态没有关系。
2.BIT_MASK
宏用于生成一个仅在指定位置为 1 的掩码。BIT_MASK(EV_KEY)
会生成一个在 EV_KEY
对应位置为 1 的掩码。通过按位或操作(|
),将 EV_KEY
和 EV_REP
(重复事件)对应的位都设置为 1,这表明该输入设备既能产生按键事件,又能产生重复事件。
3.BIT_WORD
是一个宏,用于计算指定位置所在的字(word)的索引。
4.key.idev->evbit
是一个位图,用于表示该输入设备能够产生的事件类型。EV_REP
代表重复事件,当按键被按下并且不放开时,系统会持续产生重复事件。
5.key.idev->keybit
也是一个位图,用于表示该输入设备能够产生的按键。KEY_0
表示数字 0 键。
4、注册设备:
流程:申请->初始化事件->注册->注销->删除:
使用 input_register_device 函数向 Linux 系统注册前面初始化好的 input_dev:
5、注销设备:
流程:申请->初始化事件->注册->注销->删除:
使用input_unregister_device函数注销。在remove中:
6、删除设备:
流程:申请->初始化事件->注册->注销->删除:
卸载input驱动的时候需要先使用input_unregister_device函数注销掉注册的,所以先注销设备然后删除设备:
4.3.2配置定时器
1、配置相关设备结构体
2、初始化定时器
放在这个地方。一旦匹配成功就初始化定时器,效果更好!
其中key_timer_function:定时器回调函数。0是标志位,直接给0即可。
4.3.3配置GPIO设置
1、配置相关设备结构体
2、集成GPIO设备树节点信息模块
这些代码之前的文章讲到过;注意前面的代码。
当然,也可以直接在probe函数内获得设备节点信息、申请IO、设置输出模式。
我们这里直接集成了!
把这些通过struct platform_device *pdev
和pdev->dev.of_node、struct device_node *nd
来获取设备节点信息。
我们发现设备结构体没有定义struct device_node *node
我们这里的设备节点信息不需要存储在设备结构体里。所以不需要在miscbeep_dev里定义。
设备节点信息主要在 key_gpio_init
函数调用时作为参数传入,在函数内部使用完就可以了。
3、从设备树中获取 GPIO信息:
这些都是之前讲过很多遍的了!
4、申请使用 GPIO:
5、将 GPIO 设置为输入模式:
6、释放GPIO:
同样因为这里申请了GPIO ,所以要释放GPIO,可以按照逻辑放位置,可以放remove。同样也可以放其他位置,比如操作集的release函数等等。
我们这里放在remove里:
7、配置中断信息
7.1:配置中断的设备结构体:
7.2:获取中断号:
因为设备树上已经有了中断号了,每个都是唯一的,直接拿过来就行!
放在key_gpio_init那里!
7.3:获取设备树中指定的中断触发类型:
定义标志位:
7.4:申请中断:
其中key.irq_key:中断号;key_interrupt:中断处理函数;irq_flags:用于设置中断的触发方式;“Key0_IRQ”:这个名称会在 /proc/interrupts
文件中显示,方便调试和查看中断信息,中断名称为 "Key0_IRQ"
,表示这是按键 0 的中断;NULL:用于传递给中断处理程序的设备指针,通常在共享中断的情况下使用,用于区分不同的设备。
其中我们要在中断处理函数中提现中断的作用:
就是延时消抖的作用。延时15ms。
这里产生延时,一旦mod_timer结束之后就执行到了key_timer_function了,这样我们就可以在key_timer_function里面进行实现我们的目的了,比如上报事件。之前我们运用input只是初始化而已!
可以看到获取按键键值!input_report_key用于上报事件,当按键按下的时候val=0,所以设置上报值为1,设置事件值!val,同时使用 input_sync 函数来告诉 Linux 内核 input 子系统上报结束。调用 enable_irq
函数使能按键对应的中断,这样按键再次触发时就能产生中断信号。enable_irq 函数使能中断,因为在按键中断发生的时候我们会关闭中断,等事件处理完成之后再打开。因为之前开启了禁止按键中断。
代码是一个基于 Linux 内核的按键驱动程序,它借助 platform_driver
框架和输入子系统来实现按键功能,而没有采用传统字符设备驱动所用的 file_operations
操作集。
4.3.4、采用输入子系统
该代码运用了 Linux 内核的输入子系统,输入子系统本身已经封装好了设备的输入操作,像按键、鼠标、触摸屏等输入设备都可以借助输入子系统来处理。在输入子系统里,设备驱动只需上报输入事件,而用户空间程序则通过 /dev/input
下的设备节点来读取这些事件,无需驱动提供像 read
、write
这样的操作接口。这样就不需要操作集了,比如file_operations
就不需要了!
4.3.5、整体解释代码
自定义的按键设备结构体 struct key_dev,用于描述一个按键设备,其中的成员变量包括一个 input_dev 指针变量,定时器 timer、 GPIO 以及中断号。
按键中断处理函数 key_interrupt,当按键按下或松开的时候都会触发,也就是 上 升 沿 和 下 降 沿 都 会 触 发 此 中 断 。 key_interrupt 函 数 中 的 操 作 也 很 简 单 , 调 用disable_irq_nosync 函数先禁止中断,然后使用 mod_timer 打开定时器,定时时长为 15ms。
按键的 GPIO 初始化。
定时器服务函数 key_timer_function,使用到定时器的目的主要是为了使用软件的方式进行按键消抖处理,在 key_timer_function 函数中,我们使用 gpio_get_value 获取按键 GPIO 的电平状态,使用 input_report_key 函数上报按键事件。
在 val 变量前加了一个取反,因为按键按下的情况下,获取到的 val 为 0,按键松开的情况下,获取到的 val 为 1。但是 input子系统框架规定按键按下上报 1,按键松开上报 0,所以这里需要进行取反操作。事件上报完成之后使用 input_sync 函数同步事件,表示此事件已上报完成, input 子系统核心层就会进行相关的处理。 enable_irq 函数使能中断,因为在按键中断发生的时候我们会关闭中断,等事件处理完成之后再打开。
platform 驱动的 probe 函数 atk_key_probe,使用input_allocate_device 函数申请 input_dev,然后设置相应的事件以及事件码(也就是 KEY 模拟成那个按键,这里我们设置为 KEY_0)。最后使用 input_register_device 函数向 Linux 内核注册 input_dev。
platform 驱动的 remove 函数 mykey_remove,在该函数中先释放 GPIO 在使用 input_unregister_device 卸载按键设备并且调用del_timer_sync 删除定时器。
4.3.6、编写测试App
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <linux/input.h>int main(int argc, char *argv[]){int fd, ret;struct input_event ev;if(2 != argc) {printf("Usage:\n""\t./keyinputApp /dev/input/eventX @ Open Key\n");return -1;}/* 打开设备 */fd = open(argv[1], O_RDWR);if(0 > fd) {printf("Error: file %s open failed!\r\n", argv[1]);return -1;}/* 读取按键数据 */for ( ; ; ) {ret = read(fd, &ev, sizeof(struct input_event));if (ret) {switch (ev.type) {case EV_KEY: /* 按键事件 */if (KEY_0 == ev.code) { /* 判断是不是 KEY_0 按键 */if (ev.value) /* 按键按下 */printf("Key0 Press\n");else /* 按键松开 */printf("Key0 Release\n");}break;/* 其他类型的事件,自行处理 */case EV_REL:break;case EV_ABS:break;case EV_MSC:break;case EV_SW:break;};}else {printf("Error: file %s read failed!\r\n", argv[1]);goto out;}}out:/* 关闭设备 */close(fd);return 0;}
测试App.c:可以看到这里的代码最重要的就是。
include <linux/input.h>
struct input_event ev;定义了input_event函数,然后判断结构体子类,type、code和value。
5、效果测试
当驱动模块加载成功以后我们可以在/dev/input这个目录下看到一个名为“event0”文件:
这其实就是我们注册的驱动所对应的设备文件。 keyinputApp 就是通过读取/dev/input/event0 这个文件来获取输入事件信息的,输入如下测试命令:
./keyinputApp /dev/input/event0
然后按下开发板上的 KEY 按键:
另外,我们也可以不用 keyinputApp 来测试驱动,可以直接使用 hexdump 命令来查看/dev/input/event0 文件内容,输入如下命令:
hexdump /dev/input/event0
比如编号第一行就是按下,第二行就是同步事件。第三行为松开,第四行为同步事件。
5、总代码
keyinput.c
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/input.h>
#include <linux/timer.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <asm/uaccess.h>#define KEYINPUT_NAME "keyinput" /* 名字 *//*KEY设备结构体*/
struct key_dev{struct input_dev *idev; /* 按键对应的 input_dev 指针*/struct timer_list timer; /* 消抖定时器 */int gpio_key; /* 按键对应的 GPIO 编号 */int irq_key; /* 按键对应的中断号 */
};
struct key_dev key; /* 按键设备 */static irqreturn_t key_interrupt(int irq, void *dev_id)
{/*当按键产生中断时,内核会根据中断号找到对应的中断处理函数,并将该
中断号key.irq_key作为irq参数传递给中断处理函数*/
/*当系统中有多个中断源时,每个中断源都有自己唯一的中断号通过irq参数中断处理函数,
可以判断是哪个中断源触发了中断,进而执行相应的处理逻辑*/if(key.irq_key != irq)return IRQ_NONE;/* 按键防抖处理,开启定时器延时 15ms */disable_irq_nosync(irq); /* 禁止按键中断 */mod_timer(&key.timer, jiffies + msecs_to_jiffies(15));return IRQ_HANDLED;
}/*GPIO配置和中断相关处理*/
static int key_gpio_init(struct device_node *nd)
{int ret;unsigned long irq_flags;/* 1.从设备树中获取 GPIO */key.gpio_key = of_get_named_gpio(nd, "key-gpio", 0);if(!gpio_is_valid(key.gpio_key)) {printk("key: Failed to get key-gpio\n");return -EINVAL;}/* 2.申请使用 GPIO */ret = gpio_request(key.gpio_key, "KEY0");if (ret) {printk(KERN_ERR "key: Failed to request key-gpio\n");return ret;}/* 3.将 GPIO 设置为输入模式 */gpio_direction_input(key.gpio_key);/*4.获取 GPIO 对应的中断号 */key.irq_key = irq_of_parse_and_map(nd, 0);if(!key.irq_key){return -EINVAL;}/*5.获取设备树中指定的中断触发类型 */irq_flags = irq_get_trigger_type(key.irq_key);if (IRQF_TRIGGER_NONE == irq_flags){irq_flags = IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING;}/*7.申请中断*/ret = request_irq(key.irq_key, key_interrupt, irq_flags,"Key0_IRQ", NULL);if (ret) {gpio_free(key.gpio_key);return ret;}return 0;
}
static void key_timer_function(struct timer_list *arg)
{
//这个函数作用就是定时周期的执行程序:比如周期翻转led的状态
/* from_timer 是个宏,可以根据结构体的成员地址,获取到这个结构体的首地址。第一个参数表示结构体,第二个参数表示第一个参数里的一个成员,第三个参数表
示第二个参数的类型,得到第一个参数的首地址。
*/
/*
通过 mod_timer 函数设置了定时器的到期时间后,一旦系统时钟的节拍数(jiffies)
达到了设定的到期时间,内核就会触发定时器,进而调用注册的回调函数 timer_function
*/int val;/* 读取按键值并上报按键事件 */val = gpio_get_value(key.gpio_key);input_report_key(key.idev, KEY_0, !val);input_sync(key.idev);enable_irq(key.irq_key);
}/*platform_driver的probe函数*/
static int atk_key_probe(struct platform_device *pdev)
{int ret;printk("key driver and device was matched!\r\n");/*5.初始化GPIO*/ret = key_gpio_init(pdev->dev.of_node);if(ret < 0){return ret;}/*4.初始化定时器*/timer_setup(&key.timer, key_timer_function, 0);/* 1.申请 input_dev */key.idev = input_allocate_device();key.idev->name = KEYINPUT_NAME;/*2.初始化 input_dev*/#if 0/* 初始化 input_dev,设置产生哪些事件 */__set_bit(EV_KEY, key.idev->evbit); /* 设置产生按键事件 */__set_bit(EV_REP, key.idev->evbit); /* 重复事件,比如按下去不放开,就会一直输出信息 *//* 初始化 input_dev,设置产生哪些按键 */__set_bit(KEY_0, key.idev->keybit);#endif#if 0key.idev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);key.idev->keybit[BIT_WORD(KEY_0)] |= BIT_MASK(KEY_0);#endifkey.idev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);input_set_capability(key.idev, EV_KEY, KEY_0);/*3.注册设备*/ret = input_register_device(key.idev);if (ret) {printk("register input device failed!\r\n");goto free_gpio;}return 0;
free_gpio:free_irq(key.irq_key,NULL);gpio_free(key.gpio_key);del_timer_sync(&key.timer);return -EIO;
}
/*platform_driver的remove函数*/
static int atk_key_remove(struct platform_device *pdev)
{gpio_free(key.gpio_key); /* 释放 GPIO */input_unregister_device(key.idev); /* 注销 input_dev */input_free_device(key.idev); /* 删除 input_dev */return 0;
}/*匹配列表*/
static const struct of_device_id key_of_match[] = {{.compatible = "alientek,key"},{/* Sentinel */}
};
MODULE_DEVICE_TABLE(of,key_of_match);/*编写platform_driver结构体*/
static struct platform_driver atk_key_driver = {.driver = {.name = "stm32mp1-key",.of_match_table = key_of_match,},.probe = atk_key_probe,.remove = atk_key_remove,
};/*驱动注册和注销一体化*/
//atk_key_driver是等下要定义的struct platform_driver驱动变量
module_platform_driver(atk_key_driver);MODULE_LICENSE("GPL");
MODULE_AUTHOR("chensir");
MODULE_INFO(intree,"Y");
keyinputApp.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <linux/input.h>int main(int argc, char *argv[]){int fd, ret;struct input_event ev;if(2 != argc) {printf("Usage:\n""\t./keyinputApp /dev/input/eventX @ Open Key\n");return -1;}/* 打开设备 */fd = open(argv[1], O_RDWR);if(0 > fd) {printf("Error: file %s open failed!\r\n", argv[1]);return -1;}/* 读取按键数据 */for ( ; ; ) {ret = read(fd, &ev, sizeof(struct input_event));if (ret) {switch (ev.type) {case EV_KEY: /* 按键事件 */if (KEY_0 == ev.code) { /* 判断是不是 KEY_0 按键 */if (ev.value) /* 按键按下 */printf("Key0 Press\n");else /* 按键松开 */printf("Key0 Release\n");}break;/* 其他类型的事件,自行处理 */case EV_REL:break;case EV_ABS:break;case EV_MSC:break;case EV_SW:break;};}else {printf("Error: file %s read failed!\r\n", argv[1]);goto out;}}out:/* 关闭设备 */close(fd);return 0;}
makefile
KERNELDIR := /home/chensir/linux/atk-mp1/linux/my_linux/linux-5.4.31
CURRENT_PATH := $(shell pwd)
obj-m := keyinput.obuild: kernel_moduleskernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean