Linux内核中的中断子系统+内核定时器+输入子系统驱动

Linux内核中的中断子系统

中断和信号有什么区别?
中断是基于硬件实现的,不论有没有linux内核中断的流程都一样。而信号是基于软件实现的。信号是中断的一种软件模拟。如果没有linux内核就没有信号。

异常处理流程

中断是基于硬件实现的,不管有没有Linux内核中断的执行都是一样的,中断是异常的一种,遵从4大步3小步的执行流程。

面试题:四大步三小步的执行流程?
1.将cpsr保存spsr中
2.修改cpsr的值
设置为ARM模式
设置为对应的异常模式
如果有必要的话就禁止中断
3.将LR的值保存起来(返回的地址)
4.设置PC(程序状态寄存器)跳转到对应的位置执行

arm裸机中断代码流程:
在这里插入图片描述

Linux内核中断处理流程

在这里插入图片描述
–>外部中断
–>head.S汇编中找异常向量表
–>找到中断后 保存现场 bl handle_irq 恢复现场
–>handle_irq中断处理函数(在内核中写中断处理函数的话 必须叫handle_irq,这样不方便。于是在handle_irq中通过一个函数指针回调中断处理函数)我们写一个中断处理函数之后,把这个函数赋给函数指针,此时handle_irq执行的时候就可以通过函数指针调用到我们写的中断处理函数。
(handle_irq函数没有参数没有返回值)
—>为什么将硬件中断号映射成软中断号?
对于linux内核可以移植到不同的平台上,不同的平台中断对应的功能是不同的(比如我们90号中断对应的触摸屏的中断,换一个板子,90号中断有可能对应其他的中断)。面对这些差异,就设计了内核中使用的中断全部都是软中断号(这个软中断和我们之前将的swi不是一个软中断)。映射的过程是内核自动完成的,加减一个偏移值即可。
–>内核中那么多的中断处理函数,我们调用哪一个呢?
内核在启动的时候会为每一个软中断创建一个irq_desc的结构体。
这个结构体又构成了一个结构体数组。
当产生硬件中断之后,内核就会映射成软中断号,软中断号作为数组下标,就能取到irq_desc的结构体。
–>在irq_desc中嵌套了一个irqaction结构体。这个结构体中的handler就是指向中断处理函数的函数指针。flags触发方式。
(在使用内核中的中断子系统的时候,完全不用关心底层的寄存器,中断子系统已经设置好了)

软中断号怎么获取?在设备树上存着的。

Linux内核中断子系统API

#include <linux/interrupt.h>
#include <linux/of_irq.h>
unsigned int irq_of_parse_and_map(struct device_node *np, int index)
功能:解析得到软中断号
参数:
    @np:节点指针
    @index:设备树节点中interrupts键对应值的下标
返回值:成功返回软中断号,失败返回0
    
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
     const char *name, void *dev)
功能:注册中断
参数:
    @irq:软中断号(内核中用到的中断号全部都是软中断号)(设备树获取)
    @handler:中断处理函数指针
        //typedef irqreturn_t (*irq_handler_t)(int, void *);
        irqreturn_t key_irq_handle(int irqno,void *dev)
     {
      //中断的处理---中断处理函数中不能加延时耗时的操作
         return IRQ_NONE;    //失败
         return IRQ_HANDLED; //成功
     }
 	@flags:中断触发方式
         IRQF_TRIGGER_RISING  //上升沿
         IRQF_TRIGGER_FALLING //下降沿 
         IRQF_TRIGGER_HIGH  //高电平
         IRQF_TRIGGER_LOW  //低电平
         IRQF_SHARED          //共享中断
	 @name:中断的名字
       cat /proc/interrupts 命令查看(设备号的查看方式 cat /proc/device)
 	 @dev:给中断处理函数传递的参数
返回值:成功返回0,失败返回错误码

const void *free_irq(unsigned int irq, void *dev_id)
功能:释放中断
参数:
    @irq:软中断号
    @dev_id:给中断处理函数传递的参数
返回值:返回设备的名字

中断底半部专门为延时耗时操作设计的。

共享中断:在一个中断上有可能接两个可以产生中断的设备。
(有些处理器管脚有限制,在注册管脚的时候有可能需要共享同一个中断号,但是问题是如何区分是哪一个产生的中断?这时候用到了总线,比如说iic总线,一个总线外面可以接很多设备,在每一个设备里面有中断状态寄存器,读一下中断状态寄存器,如果是1 就说明是它产生的中断)

软中断号不只对应着一个irqaction,irqaction中有next指向的下一个irqaction。

Linux内核中断子系统设备树填写

画出硬件连接原理图

在这里插入图片描述
现在系统运行的时候M4的核并没有参与工作。

找出控制器的设备树

1.看控制器有没有使能
2.看对子节点成员的修饰

1.打开文件stm32mp151.dtsi
2.搜索gic

intc: interrupt-controller@a0021000 {
		compatible = "arm,cortex-a7-gic";
		#interrupt-cells = <3>;//修饰子节点使用当前节点成员个数(中断)
		interrupt-controller; 
		reg = <0xa0021000 0x1000>,
		      <0xa0022000 0x2000>;
	};

	soc {
		compatible = "simple-bus";
		#address-cells = <1>;
		#size-cells = <1>;
		interrupt-parent = <&intc>; //父节点
		exti: interrupt-controller@5000d000 {
					compatible = "st,stm32mp1-exti", "syscon";
					interrupt-controller;
					#interrupt-cells = <2>;
					reg = <0x5000d000 0x400>;
					hwlocks = <&hsem 1 1>;
		}
	}
pinctrl: pin-controller@50002000 {
	#address-cells = <1>;
	#size-cells = <1>;
	compatible = "st,stm32mp157-pinctrl";
	ranges = <0 0x50002000 0xa400>;
--- interrupt-parent = <&exti>;  父节点  
	st,syscfg = <&exti 0x60 0xff>;
	hwlocks = <&hsem 0 1>;
	pins-are-numbered;
	gpiof: gpio@50007000 {
		gpio-controller;
		#gpio-cells = <2>;
		interrupt-controller;
		#interrupt-cells = <2>; //修饰子节点使用当前节点成员个数-中断
		reg = <0x50007000 0x400>;
		clocks = <&rcc GPIOF>;
		st,bank-name = "GPIOF";
		status = "disabled";// 控制器已经使能了
};

gpiof 的父节点是exti 写法是 interrupt-parent = <&exti>; 父节点
exti的父节点是gic 写法是 interrupt-parent = <&intc>; //父节点
所以我们要写的中断的父节点应该是gpiof

参考内核帮助文档编写自己的设备树

linux@ubuntu:~/linux-5.10.61/Documentation/devicetree/bindings$ ls 
interrupt-controller  
linux@ubuntu:~/linux-5.10.61/Documentation/devicetree/bindings$ cd interrupt-controller/

//找通用的参考文档
vi interrupts.txt
在这里插入图片描述

在这里插入图片描述

myirqs{
 interrupt-parent = <&gpiof>; //父节点
 interrupts = <9 0>,<7 0>,<8 0>; //对应中断管脚及触发方式0代表默认状态
};

重新编译设备树:linux@ubuntu:~/linux-5.10.61$ make dtbs
运行 copy.sh

linux@ubuntu:~/linux-5.10.61$ cat copy.sh 
cp arch/arm/boot/uImage ~/tftpboot/ 
cp arch/arm/boot/dts/stm32mp157a-fsmp1a.dtb  ~/tftpboot/ 

编译 make arch=arm modname=myirq
make install
重启开发板
安装模块 查看现象
在这里插入图片描述
此时现象还有一定的问题:按下一个按键的时候,有出现两次的情况
后面会讲到按键消抖。

中断子系统实例(一个按键)

#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
// myirqs{
// 	interrupt-parent = <&gpiof>; //父节点
// 	interrupts = <9 0>,<7 0>,<8 0>; //对应中断管脚及触发方式0代表默认状态
// };
struct device_node *node;
unsigned int irqno;
irqreturn_t key_irq_handle(int irq, void *dev)
{
    printk("key1 down...\n");
    return IRQ_HANDLED;
}
static int __init myirqs_init(void)
{
    int ret;
    // 1.获取节点
    node = of_find_node_by_path("/myirqs");
    if (node == NULL)
    {
        printk("of_find_node_by_path error\n");
        return -EINVAL;
    }
    // 2.获取软中断号
    irqno = irq_of_parse_and_map(node, 0);
    if(irqno == 0)
    {
        printk("irq_of_parse_and_map error\n");
        return -EINVAL;
    }
    // 3.注册中断
    ret = request_irq(irqno, key_irq_handle, IRQF_TRIGGER_FALLING, "myirqs", NULL);
    if(ret){
        printk("request_irq error\n");
        return ret;
    }
    return 0;
}
static void __exit myirqs_exit(void)
{
    free_irq(irqno, NULL);
}
module_init(myirqs_init);
module_exit(myirqs_exit);
MODULE_LICENSE("GPL");

中断子系统实例(多个按键)

#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_irq.h>
// myirqs{
//     compatible = "hqyj,myirqs";
//     interrupt-parent = <&gpiof>;
//     interrupts = <9 0>,<7 0>,<8 0>;
// };
struct device_node* node;
unsigned int irqno[3];
//中断处理函数
irqreturn_t key_irq_handle(int irqno, void* dev)
{
    printk("key_irq_handle: irqno = %d,number = %d\n", irqno, (int)dev);
    return IRQ_HANDLED;
}
static int __init myirqs_init(void)
{
    int ret,i;
    // 1.获取中断的设备树节点--路径 名字 compatible都可以
    node = of_find_node_by_path("/myirqs");
    if (node == NULL) {
        printk("of_find_node_by_path error\n");
        return -EINVAL;
    }
    for(i=0;i<ARRAY_SIZE(irqno);i++){
        // 2.解析得到软中断号
        irqno[i] = irq_of_parse_and_map(node, i);
        if (irqno[i] == 0) {
            printk("of_irq_parse_and_map error\n");
            return -EINVAL;
        }
        // 3.注册中断
        ret = request_irq(irqno[i], key_irq_handle, IRQF_TRIGGER_FALLING,
            "myirqs", (void*)i);
        if (ret) {
            printk("request_irq error\n");
            return -EINVAL;
        }
    }
    return 0;
}
static void __exit myirqs_exit(void)
{
    int i;
    // 4.释放中断
    for(i=0;i<ARRAY_SIZE(irqno);i++){
        free_irq(irqno[i], (void*)i);
    }
}
module_init(myirqs_init);
module_exit(myirqs_exit);
MODULE_LICENSE("GPL");

内核定时器

上边讲到内核中不能使用延时耗时的操作。我们可以使用定时。
用于按键消抖的使用。消除上边的抖动情况。

内核定时器定时原理

1.内核当前时间如何获取?
jiffies:内核时钟节拍数,从系统启动这一刻起这个jiffies(64bit)值就一直在增加。jiffies可以直接使用,本身就是一个变量。
不需要包含头文件。

jiffies 在内核中的链接脚本的文件中有记录。

linux@ubuntu:~/linux-5.10.61$ find -name vmlinux.lds
./arch/arm/kernel/vmlinux.lds
./arch/arm/boot/compressed/vmlinux.lds
./arch/h8300/boot/compressed/vmlinux.lds
linux@ubuntu:~/linux-5.10.61$ vi ./arch/arm/kernel/vmlinux.lds

在这里插入图片描述
申请了64个bit位用来保存jiffies的值。

2.定时器每增加1走的时间?
定时器的频率可以通过make menuconfig选配,选配后的选项在.config文件中保存着。
开发板:CONFIG_HZ=100就是频率,定时器每增加1代表走10ms。
Ubuntu:CONFIG_HZ=250就是频率,定时器每增加1代表走4ms。

开发板:

linux@ubuntu:~/linux-5.10.61$  vi .config


Ubuntu:CONFIG_HZ=250就是频率,定时器每增加1代表走4ms。

ubuntu的.config文件在 /lib/modules 路径下
uname -r 获取内核版本号并进入相当应的内核版本
cd build进入这个目录下
vi .config 打开config文件

linux@ubuntu:~/linux-5.10.61$ cd /lib/modules/
linux@ubuntu:/lib/modules$ ls 
4.15.0-213-generic  5.4.0-146-generic  5.4.0-148-generic  5.4.0-42-generic
5.4.0-144-generic   5.4.0-147-generic  5.4.0-150-generic  5.4.0-52-generic
linux@ubuntu:/lib/modules$ uname -r
5.4.0-150-generic
linux@ubuntu:/lib/modules$ cd 5.4.0-150-generic/
linux@ubuntu:/lib/modules/5.4.0-150-generic$ ls
build          modules.alias.bin        modules.dep      modules.softdep
initrd         modules.builtin          modules.dep.bin  modules.symbols
kernel         modules.builtin.bin      modules.devname  modules.symbols.bin
modules.alias  modules.builtin.modinfo  modules.order    vdso
linux@ubuntu:/lib/modules/5.4.0-150-generic$ cd build
linux@ubuntu:/lib/modules/5.4.0-150-generic/build$ ls
arch   crypto         fs       ipc      kernel    mm              samples   sound   usr
block  Documentation  include  Kbuild   lib       Module.symvers  scripts   tools   virt
certs  drivers        init     Kconfig  Makefile  net             security  ubuntu
linux@ubuntu:/lib/modules/5.4.0-150-generic/build$ vi .config

在这里插入图片描述
写驱动:分配对象、初始化对象、注册、注销。

内核定时器的API

1.分配对象
    struct timer_list {
        struct hlist_node entry;   //构成内核链表成员
        unsigned long  expires; //定时器时间
        void   (*function)(struct timer_list *); //定时器处理函数
        u32			flags;      //标志位,一般写0
    }struct timer_list mytimer;
2.对象的初始化
    mytimer.expires = jiffies + HZ; //定时1s  #define HZ CONFIG_HZ
    timer_setup(&mytimer, 定时器处理函数, 0)(在开发板上定时是1s,在ubuntu上定时不一定是1s了,如果想要保证任何内核版本的定时时间都是1s的话,我们可以使用宏HZ  #define HZ CONFIG_HZ 这里写HZ不论是任何的内核,定时时间都是1s)
    
3.启动定时器
 void add_timer(struct timer_list *timer);
 //启动定时器,定时器启动之后只会执行一次
 
 mod_timer(struct timer_list *timer, unsigned long expires);
 //再次启动定时器
4.删除定时器
    int del_timer(struct timer_list * timer);
 //移除定时器

内核定时器的实例

#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/timer.h>
// myleds
// {
//     led1 = <&gpioe 10 0>;
//     led2 = <&gpiof 10 0>;
//     led3 = <&gpioe 8 0>;
// };
struct timer_list mytimer;
struct device_node* node;
int gpiono[3];
char* name[] = { "led1", "led2", "led3" };
// 定时器处理函数
void timer_led_handle(struct timer_list* timer)
{
    int i;
    for (i = 0; i < ARRAY_SIZE(name); i++) {
        gpio_set_value(gpiono[i],!gpio_get_value(gpiono[i]));
    }
    mod_timer(&mytimer, jiffies + HZ);
}
static int __init mytimer_init(void)
{
    int ret, i;
    // 1.获取节点
    node = of_find_node_by_name(NULL, "myleds");
    if (node == NULL) {
        printk("of_find_node_by_name error\n");
        return -EINVAL;
    }
    for (i = 0; i < ARRAY_SIZE(name); i++) {
        // 2.解析gpio号
        gpiono[i] = of_get_named_gpio(node, name[i], 0);
        if (gpiono[i] < 0) {
            printk("of_get_named_gpio error\n");
            return -EINVAL;
        }
        // 3.初始化LED
        ret = gpio_request(gpiono[i], name[i]);
        if (ret) {
            printk("gpio_request error\n");
            return ret;
        }
        ret = gpio_direction_output(gpiono[i], 0);
        if (ret) {
            printk("gpio_direction_output error\n");
            return ret;
        }
    }
    // 4.初始化定时器
    mytimer.expires = jiffies + HZ;
    timer_setup(&mytimer, timer_led_handle, 0);
    // 5.启动定时器
    add_timer(&mytimer);
    return 0;
}
static void __exit mytimer_exit(void)
{
    int i;
    del_timer(&mytimer);
    for (i = 0; i < ARRAY_SIZE(name); i++) {
        gpio_free(gpiono[i]);
    }
}
module_init(mytimer_init);
module_exit(mytimer_exit);
MODULE_LICENSE("GPL");

内核定时器的练习

1.使用内核定时器让LED闪烁
2.使用内核定时器完成按键中断消抖工作
在这里插入图片描述

#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <linux/of_gpio.h>
// myirqs{
// 	interrupt-parent = <&gpiof>; //父节点
// 	interrupts = <9 0>,<7 0>,<8 0>; //对应中断管脚及触发方式0代表默认状态
//  keys = <&gpiof 9 0>;
// };
struct device_node* node;
struct timer_list mytimer;
unsigned int irqno;
int gpiono;
void key_irq_timer_handle(struct timer_list* timer)
{
    if(!gpio_get_value(gpiono)){ //说明按键安下了
        printk("key1 down...\n");
    }
}
irqreturn_t key_irq_handle(int irq, void* dev)
{
    mod_timer(&mytimer, jiffies + 1); //再次启动定时器
    return IRQ_HANDLED;
}
static int __init myirqs_init(void)
{
    int ret;
    // 1.获取节点
    node = of_find_node_by_path("/myirqs");
    if (node == NULL) {
        printk("of_find_node_by_path error\n");
        return -EINVAL;
    }
    // 2.获取软中断号和获取gpio号
    irqno = irq_of_parse_and_map(node, 0);
    if (irqno == 0) {
        printk("irq_of_parse_and_map error\n");
        return -EINVAL;
    }
    gpiono = of_get_named_gpio(node, "keys", 0); //gpio在任何模式下都支持读
    if (gpiono < 0) {
        printk("of_get_named_gpio error\n");
        return -EINVAL;
    }
    // 3.初始化定时器
    mytimer.expires = jiffies + 1; // 定时时间10ms
    timer_setup(&mytimer, key_irq_timer_handle, 0);
    add_timer(&mytimer);

    // 4.注册中断
    ret = request_irq(irqno, key_irq_handle, IRQF_TRIGGER_FALLING, "myirqs", NULL);
    if (ret) {
        printk("request_irq error\n");
        return ret;
    }
    return 0;
}
static void __exit myirqs_exit(void)
{
    free_irq(irqno, NULL);
    del_timer(&mytimer);
}
module_init(myirqs_init);
module_exit(myirqs_exit);
MODULE_LICENSE("GPL");

输入子系统驱动

输入子系统驱动简介

—目标:模拟键盘驱动
输入类设备都是通过输入子系统进行数据上报的。比如鼠标,键盘,触摸屏等驱动都是需要通过输入子系统驱动上报数据的,上报的数据是input_event结构体,这个input_event结构体被上报/dev/input/event*节点中了。

输入子系统驱动框架结构

我们只需要写设备驱动层就可以了,事件处理层和核心层都是内核中处理好的。
所有的驱动都是在drivers下的。
/drivers/input下有evdev.c(键盘事件处理层代码) 、mousedev.c(鼠标事件处理层代码)、joydev.c(游戏手柄事件处理层代码)事件处理层代码。
事件处理层xxx.c的功能:接受 设备驱动层 向 事件处理层 上报的数据,并把数据提供给应用层。说明evdev.c中嵌套了file_operations结构体。

在这里插入图片描述
手机上的触摸屏:是iic子系统加输入子系统驱动。
所以我们最终要看一下 input_dev 怎么分配,怎么填充,怎么注册,怎么上报事件即可。

输入子系统驱动API

~/linux-5.10.61/Documentation$ vi input/input-programming.rst
1.分配对象
    struct input_dev *key_dev;  
 key_dev = input_allocate_device();
2.对象初始化
    set_bit(EV_KEY, key_ev->evbit); // 指定上报的是键盘类事件
    set_bit(KEY_L, key_ev->keybit); // 指定上报l键
    set_bit(KEY_S, key_ev->keybit); // 指定上报s键
    set_bit(KEY_ENTER, key_ev->keybit); // 指定上报s键
3.注册
    int input_register_device(struct input_dev *dev)
4.注销
    void input_unregister_device(struct input_dev *dev)
5.上报数据
    void input_event(struct input_dev *dev,
   unsigned int type, unsigned int code, int value) //上报数据
 void input_sync(struct input_dev *dev) //同步

键盘驱动的实例

keybroad.c

#include <linux/init.h>
#include <linux/input.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_irq.h>
// myirqs{
// 	interrupt-parent = <&gpiof>; //父节点
// 	interrupts = <9 0>,<7 0>,<8 0>; //对应中断管脚及触发方式0代表默认状态
// };
struct device_node* node;
unsigned int irqno[3];
struct input_dev* key_ev;
irqreturn_t key_irq_handle(int irq, void* dev)
{
    switch ((int)dev) {
    case 0:
        input_event(key_ev, EV_KEY, KEY_L, 1);
        input_sync(key_ev);
        input_event(key_ev, EV_KEY, KEY_L, 0);
        input_sync(key_ev);
        break;
    case 1:
        input_event(key_ev, EV_KEY, KEY_S, 1);
        input_sync(key_ev);
        input_event(key_ev, EV_KEY, KEY_S, 0);
        input_sync(key_ev);
        break;
    case 2:
        input_event(key_ev, EV_KEY, KEY_ENTER, 1);
        input_sync(key_ev);
        input_event(key_ev, EV_KEY, KEY_ENTER, 0);
        input_sync(key_ev);
        break;
    }
    return IRQ_HANDLED;
}
static int __init myirqs_init(void)
{
    int ret, i;
    // 1.获取节点
    node = of_find_node_by_path("/myirqs");
    if (node == NULL) {
        printk("of_find_node_by_path error\n");
        return -EINVAL;
    }
    for (i = 0; i < ARRAY_SIZE(irqno); i++) {
        // 2.获取软中断号
        irqno[i] = irq_of_parse_and_map(node, i);
        if (irqno[i] == 0) {
            printk("irq_of_parse_and_map error\n");
            return -EINVAL;
        }
        // 3.注册中断
        ret = request_irq(irqno[i], key_irq_handle,
            IRQF_TRIGGER_FALLING, "myirqs", (void*)i);
        if (ret) {
            printk("request_irq error\n");
            return ret;
        }
    }
    // 4.注册输入子系统
    key_ev = input_allocate_device();
    if (key_ev == NULL) {
        printk("input_allocate_device error\n");
        return -ENOMEM;
    }
    set_bit(EV_KEY, key_ev->evbit); // 指定上报的是键盘类事件
    set_bit(KEY_L, key_ev->keybit); // 指定上报l键
    set_bit(KEY_S, key_ev->keybit); // 指定上报s键
    set_bit(KEY_ENTER, key_ev->keybit); // 指定上报s键

    ret = input_register_device(key_ev);
    if (ret) {
        printk("input_register_device error\n");
        return ret;
    }
    return 0;
}
static void __exit myirqs_exit(void)
{
    int i;
    input_unregister_device(key_ev);
    for (i = 0; i < ARRAY_SIZE(irqno); i++) {
        free_irq(irqno[i], (void*)i);
    }
}
module_init(myirqs_init);
module_exit(myirqs_exit);
MODULE_LICENSE("GPL");

test.c

可以使用如下程序测试键盘上报的数据,也可以通过exec 0</dev/tty1

#include <head.h>
#include <linux/input.h>
#include <sys/select.h>
int main(int argc, const char* argv[])
{
    int fd1, fd2, ret;
    fd_set rfds;
    struct input_event key_ev;
    char buf[128] = { 0 };
    if ((fd2 = open("/dev/input/event0", O_RDWR)) == -1)
        PRINT_ERR("open keybroad error");
    while (1) {
        FD_ZERO(&rfds);
        FD_SET(fd2, &rfds);

        if ((ret = select(fd2 + 1, &rfds, NULL, NULL, NULL)) == -1)
            PRINT_ERR("select error");

        if (FD_ISSET(fd2, &rfds)) {
            memset(&key_ev, 0, sizeof(key_ev));
            read(fd2, &key_ev, sizeof(key_ev));
            printf("type = %d,code = %d,value=%d\n",
                key_ev.type,key_ev.code,key_ev.value);
        }
    }

    close(fd2);
    return 0;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值