当前位置: 首页 > news >正文

i.MX 6ULL 驱动开发 十五:按键中断(input子系统)

一、input 子系统基本概念

Linux 驱动开发 四十八:Linux INPUT 子系统实验_lqonlylove的博客-CSDN博客

二、input 子系统相关数据类型和API

1、input_dev

/**
 * struct input_dev - represents an input device
 * @name: name of the device
 * @phys: physical path to the device in the system hierarchy
 * @uniq: unique identification code for the device (if device has it)
 * @id: id of the device (struct input_id)
 * @propbit: bitmap of device properties and quirks
 * @evbit: bitmap of types of events supported by the device (EV_KEY,
 *	EV_REL, etc.)
 * @keybit: bitmap of keys/buttons this device has
 * @relbit: bitmap of relative axes for the device
 * @absbit: bitmap of absolute axes for the device
 * @mscbit: bitmap of miscellaneous events supported by the device
 * @ledbit: bitmap of leds present on the device
 * @sndbit: bitmap of sound effects supported by the device
 * @ffbit: bitmap of force feedback effects supported by the device
 * @swbit: bitmap of switches present on the device
 * @hint_events_per_packet: average number of events generated by the
 *	device in a packet (between EV_SYN/SYN_REPORT events). Used by
 *	event handlers to estimate size of the buffer needed to hold
 *	events.
 * @keycodemax: size of keycode table
 * @keycodesize: size of elements in keycode table
 * @keycode: map of scancodes to keycodes for this device
 * @getkeycode: optional legacy method to retrieve current keymap.
 * @setkeycode: optional method to alter current keymap, used to implement
 *	sparse keymaps. If not supplied default mechanism will be used.
 *	The method is being called while holding event_lock and thus must
 *	not sleep
 * @ff: force feedback structure associated with the device if device
 *	supports force feedback effects
 * @repeat_key: stores key code of the last key pressed; used to implement
 *	software autorepeat
 * @timer: timer for software autorepeat
 * @rep: current values for autorepeat parameters (delay, rate)
 * @mt: pointer to multitouch state
 * @absinfo: array of &struct input_absinfo elements holding information
 *	about absolute axes (current value, min, max, flat, fuzz,
 *	resolution)
 * @key: reflects current state of device's keys/buttons
 * @led: reflects current state of device's LEDs
 * @snd: reflects current state of sound effects
 * @sw: reflects current state of device's switches
 * @open: this method is called when the very first user calls
 *	input_open_device(). The driver must prepare the device
 *	to start generating events (start polling thread,
 *	request an IRQ, submit URB, etc.)
 * @close: this method is called when the very last user calls
 *	input_close_device().
 * @flush: purges the device. Most commonly used to get rid of force
 *	feedback effects loaded into the device when disconnecting
 *	from it
 * @event: event handler for events sent _to_ the device, like EV_LED
 *	or EV_SND. The device is expected to carry out the requested
 *	action (turn on a LED, play sound, etc.) The call is protected
 *	by @event_lock and must not sleep
 * @grab: input handle that currently has the device grabbed (via
 *	EVIOCGRAB ioctl). When a handle grabs a device it becomes sole
 *	recipient for all input events coming from the device
 * @event_lock: this spinlock is is taken when input core receives
 *	and processes a new event for the device (in input_event()).
 *	Code that accesses and/or modifies parameters of a device
 *	(such as keymap or absmin, absmax, absfuzz, etc.) after device
 *	has been registered with input core must take this lock.
 * @mutex: serializes calls to open(), close() and flush() methods
 * @users: stores number of users (input handlers) that opened this
 *	device. It is used by input_open_device() and input_close_device()
 *	to make sure that dev->open() is only called when the first
 *	user opens device and dev->close() is called when the very
 *	last user closes the device
 * @going_away: marks devices that are in a middle of unregistering and
 *	causes input_open_device*() fail with -ENODEV.
 * @dev: driver model's view of this device
 * @h_list: list of input handles associated with the device. When
 *	accessing the list dev->mutex must be held
 * @node: used to place the device onto input_dev_list
 * @num_vals: number of values queued in the current frame
 * @max_vals: maximum number of values queued in a frame
 * @vals: array of values queued in the current frame
 * @devres_managed: indicates that devices is managed with devres framework
 *	and needs not be explicitly unregistered or freed.
 */
// input 输入设备描述结构体
struct input_dev {
	const char *name;	// 设备名
	const char *phys;	// 设备在系统层次结构中的物理路径
	const char *uniq;	// 设备的唯一标识码(如果设备有)
	struct input_id id;	// 设备的 id

	unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)];	// 设备属性位图

	unsigned long evbit[BITS_TO_LONGS(EV_CNT)];		// 设备支持的事件类型的位图
	unsigned long keybit[BITS_TO_LONGS(KEY_CNT)];	// 这个设备的按键位图
	unsigned long relbit[BITS_TO_LONGS(REL_CNT)];	// 设备相对轴位图
	unsigned long absbit[BITS_TO_LONGS(ABS_CNT)];	// 设备的绝对轴位图
	unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)];	// 设备支持的杂项事件位图
	unsigned long ledbit[BITS_TO_LONGS(LED_CNT)];	// 设备上led的位图
	unsigned long sndbit[BITS_TO_LONGS(SND_CNT)];	// 设备支持的声音效果位图
	unsigned long ffbit[BITS_TO_LONGS(FF_CNT)];		// 设备支持的力反馈效应位图
	unsigned long swbit[BITS_TO_LONGS(SW_CNT)];		// 设备上存在的交换机位图

    // 设备在数据包中生成的平均事件数(EV_SYN/SYN_REPORT事件之间)。由事件处理程序用于估计容纳事件所需缓冲区的大小
	unsigned int hint_events_per_packet;

	unsigned int keycodemax;	// 键码表的大小
	unsigned int keycodesize;	// 键码表中元素的大小
	void *keycode;				// 扫描码映射到此设备的键码

    // 检索当前键映射的可选遗留方法。
	int (*setkeycode)(struct input_dev *dev,
			  const struct input_keymap_entry *ke,
			  unsigned int *old_keycode);
    // 用于更改当前键盘映射的可选方法,用于实现稀疏键盘映射。如果未提供,将使用默认机制。
    // 该方法在持有事件锁时被调用,因此不能休眠。
	int (*getkeycode)(struct input_dev *dev,
			  struct input_keymap_entry *ke);

    // 如果设备支持力反馈效应,则与设备相关联的力反馈结构。
	struct ff_device *ff;

	unsigned int repeat_key;	// 存储最后按下的键的键码。用于实现软件自动重复
	struct timer_list timer;	// 软件自动重复定时器

	int rep[REP_CNT];	// 自动重复参数的当前值(延迟、速率)

	struct input_mt *mt;	// 指向多点触控状态的指针

    // input_absinfo 结构体数组元素保存绝对轴的信息(当前值,最小值,最大值,平坦值,模糊值,分辨率)
	struct input_absinfo *absinfo;

	unsigned long key[BITS_TO_LONGS(KEY_CNT)];	// 反映设备按键的当前状态
	unsigned long led[BITS_TO_LONGS(LED_CNT)];	// 反映设备led的当前状态
	unsigned long snd[BITS_TO_LONGS(SND_CNT)];	// 反映声音效果的当前状态
	unsigned long sw[BITS_TO_LONGS(SW_CNT)];	// 反映设备开关的当前状态

    // 当第一次调用 input_open_device 方法时。驱动进程必须准备设备以开始生成事件(启动轮询线程、请求 IRQ、提交 URB 等)。
	int (*open)(struct input_dev *dev);
    // 当最后一个用户调用 input_close_device 时调用此方法。
	void (*close)(struct input_dev *dev);
    // 清除设备。最常用于摆脱与设备断开连接时加载到设备中的力反馈效应
	int (*flush)(struct input_dev *dev, struct file *file);
    // 发送到设备的事件的事件处理进程,如EV_LED或EV_SND。设备应执行请求的操作(打开 LED、播放声音等)调用受 event_lock保护,不得休眠
	int (*event)(struct input_dev *dev, unsigned int type, unsigned int code, int value);

    // 当前已抓取设备的输入句柄。当句柄抓取设备时,它将成为来自该设备的所有输入事件的唯一接收者。
	struct input_handle __rcu *grab;

    // 当输入内核接收并处理设备的新事件时,将采用此自旋锁。
	spinlock_t event_lock;
    // 串行化对open()、close()和flush()方法的调用
	struct mutex mutex;

	unsigned int users;
	bool going_away;

    // 此设备的驱动进程模型视图
	struct device dev;

	struct list_head	h_list;
	struct list_head	node;

	unsigned int num_vals;
	unsigned int max_vals;
	struct input_value *vals;

	bool devres_managed;
};

2、事件类型

事件类型定义在 include/uapi/linux/input.h 文件中。

3、input_event

struct timeval {
	__kernel_time_t		tv_sec;		/* seconds */
	__kernel_suseconds_t	tv_usec;	/* microseconds */
};

struct input_event {
	struct timeval time;	// 发生事件
	__u16 type;				// 事件类型
	__u16 code;				// 事件码
	__s32 value;			// 值
};

4、API

1、申请一个 input_dev 设备结构体

struct input_dev *input_allocate_device(void)

2、初始化 input_dev 设备结构体

/*********第一种设置事件和事件值的方法***********/
__set_bit(EV_KEY, inputdev->evbit); /* 设置产生按键事件 */
__set_bit(EV_REP, inputdev->evbit); /* 重复事件 */
__set_bit(KEY_0, inputdev->keybit); /*设置产生哪些按键值 */

/*********第二种设置事件和事件值的方法***********/
keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
keyinputdev.inputdev->keybit[BIT_WORD(KEY_0)] | BIT_MASK(KEY_0);

/*********第三种设置事件和事件值的方法***********/
keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
input_set_capability(keyinputdev.inputdev, EV_KEY, KEY_0);

3、注册 input_dev 设备

int input_register_device(struct input_dev *dev)

4、上报事件

void input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value)

5、注销 input_dev 设备

void input_unregister_device(struct input_dev *dev)

6、释放 input_dev 设备结构体

void input_free_device(struct input_dev *dev)

三、按键原理

i.MX 6ULL 驱动开发 九:中断(非阻塞处理)_lqonlylove的博客-CSDN博客

四、设计思路

1、初始化

1、申请 input_dev 设备结构体。

2、初始化 input_dev 设备。主要设置事件类型和事件值。

3、注册 input_dev 设备。

2、事件上报

1、上报事件。

3、卸载

1、卸载 input_dev 设备。

2、释放 input_dev 设备结构体。

五、添加设备树

1、确定引脚

通过原理图可以确定 key 使用 UART1_CTS 引脚。

2、查找引脚定义是否冲突

3、添加 pinctrl 子系统相关配置

pinctrl_key: keygrp {
	fsl,pins = <
		MX6UL_PAD_UART1_CTS_B__GPIO1_IO18		0xF080	/* KEY0 */
	>;
};

4、添加 gpio 子系统相关配置

key {
	#address-cells = <1>;
	#size-cells = <1>;
	compatible = "lq-key";
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_key>;
	key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>; /* KEY0 */
	status = "okay";
};

5、添加 interrupts 相关配置

key {
	#address-cells = <1>;
	#size-cells = <1>;
	compatible = "lq-key";
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_key>;
	key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>; /* KEY0 */
	interrupt-parent = <&gpio1>;
	interrupts = <18 IRQ_TYPE_LEVEL_LOW>;  /* 低电平触发 */
	status = "okay";
};

主要添加 interrupt-parentinterrupts 属性。

6、测试

1、编译设备树

onlylove@ubuntu:~/my/linux/linux-imx-4.1.15$ make dtbs
  CHK     include/config/kernel.release
  CHK     include/generated/uapi/linux/version.h
  CHK     include/generated/utsrelease.h
make[1]: 'include/generated/mach-types.h' is up to date.
  CHK     include/generated/bounds.h
  CHK     include/generated/asm-offsets.h
  CALL    scripts/checksyscalls.sh
  DTC     arch/arm/boot/dts/imx6ull-alientek-emmc.dtb
  DTC     arch/arm/boot/dts/imx6ull-alientek-nand.dtb
onlylove@ubuntu:~/my/linux/linux-imx-4.1.15$

2、拷贝编译成功的设备树文件

onlylove@ubuntu:~/my/linux/linux-imx-4.1.15$ ls /home/onlylove/my/tftp/ -l
total 11528
-rwxrwxr-x 1 onlylove onlylove 5901744 Sep 17 04:04 zImage
-rwxrwxr-x 1 onlylove onlylove 5901752 Aug 20 01:24 zImage.bak
onlylove@ubuntu:~/my/linux/linux-imx-4.1.15$ cp arch/arm/boot/dts/imx6ull-alientek-emmc.dtb /home/onlylove/my/tftp
onlylove@ubuntu:~/my/linux/linux-imx-4.1.15$ ls /home/onlylove/my/tftp/ -l
total 11568
-rw-rw-r-- 1 onlylove onlylove   39084 Sep 23 21:18 imx6ull-alientek-emmc.dtb
-rwxrwxr-x 1 onlylove onlylove 5901744 Sep 17 04:04 zImage
-rwxrwxr-x 1 onlylove onlylove 5901752 Aug 20 01:24 zImage.bak
onlylove@ubuntu:~/my/linux/linux-imx-4.1.15$

3、启动 linux 查看设备树解析是否成功

/ # cd /proc/
/proc # ls
1              41             65             driver         mounts
10             46             66             execdomains    mtd
11             47             7              fb             net
12             48             8              filesystems    pagetypeinfo
13             49             83             fs             partitions
14             5              84             interrupts     self
15             50             9              iomem          softirqs
16             51             95             ioports        stat
17             52             asound         irq            swaps
18             53             buddyinfo      kallsyms       sys
19             54             bus            key-users      sysrq-trigger
2              55             cgroups        keys           sysvipc
20             56             cmdline        kmsg           thread-self
21             57             consoles       kpagecount     timer_list
22             58             cpu            kpageflags     tty
23             59             cpuinfo        loadavg        uptime
24             6              crypto         locks          version
3              60             device-tree    meminfo        vmallocinfo
4              61             devices        misc           vmstat
40             62             diskstats      modules        zoneinfo
/proc # cd device-tree/
/sys/firmware/devicetree/base # ls
#address-cells                 key
#size-cells                    memory
aliases                        model
alphaled                       name
backlight                      pxp_v4l2
beep                           regulators
chosen                         reserved-memory
clocks                         sii902x-reset
compatible                     soc
cpus                           sound
gpioled                        spi4
interrupt-controller@00a01000
/sys/firmware/devicetree/base # cd key/
/sys/firmware/devicetree/base/key # ls
#address-cells    interrupt-parent  name              status
#size-cells       interrupts        pinctrl-0
compatible        key-gpio          pinctrl-names
/sys/firmware/devicetree/base/key # cat compatible
lq-key
/sys/firmware/devicetree/base/key #
/sys/firmware/devicetree/base/key #

六、驱动编写

#include "linux/init.h"
#include "linux/module.h"
#include "linux/kdev_t.h"
#include "linux/fs.h"
#include "linux/cdev.h"
#include "linux/device.h"
#include <linux/uaccess.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/workqueue.h>
#include <linux/input.h>

#define NEWCHRDEV_NAME  "key"       /* 名子 */
#define TIMEOUT 1000                /* 定时器超时时间 */

typedef struct{
    struct device_node	*nd;        /* 设备节点 */
    int gpio;                       /* key gpio */
    int irqnum;                     /* 中断号 */
    struct tasklet_struct tasklet;  /* tasklet 变量(中断下半部) */
    unsigned long tasklet_data;     /* 传递给 tasklet 处理函数的参数 */
    struct work_struct work;        /* 工作队列 变量(中断下半部) */
    struct timer_list timer;        /* key 消抖定时器 */
    struct input_dev *keyinput;     /* 按键 input_dev 结构体变量 */
}lqkey_t;

typedef struct{
    lqkey_t key_data;         /* key 相关数据 */
}newchrdev_t;

newchrdev_t newchrdev;

/*
 * @description	: 按键IO初始化
 * @param 		: 无
 * @return 		: 无
 */
static int keyio_init(void)
{
    int ret = 0;
    /***** 处理设备树 *****/
    /* 1、获取设备树节点 */
    newchrdev.key_data.nd = of_find_node_by_path("/key");
	if (newchrdev.key_data.nd== NULL){
		printk("key node not find!\r\n");
		goto keyio_init_error1;
	}else {
		printk("beep node find!\r\n");
	}
    /* 1、获取设备树中的gpio属性 */
    newchrdev.key_data.gpio = of_get_named_gpio(newchrdev.key_data.nd, "key-gpio", 0);
	if(newchrdev.key_data.gpio < 0) {
		printk("can't get key-gpio");
		goto keyio_init_error1;
	}
	printk("key num = %d\r\n", newchrdev.key_data.gpio);

    /***** 使用gpio子系统设置引脚 *****/
    /* 1、向 gpio 子系统申请 GPIO 管脚 */
    ret = gpio_request(newchrdev.key_data.gpio,"key");
    if(ret){
        printk("can't request key-gpio!\r\n");
        goto keyio_init_error1;
    }
    /* 2、设置key-gpio为输入 */
    gpio_direction_input(newchrdev.key_data.gpio);
    if(ret < 0) {
		printk("can't set key-gpio!\r\n");
        goto keyio_init_error2;
	}
    return 0;

keyio_init_error2:
    gpio_free(newchrdev.key_data.gpio);
keyio_init_error1:
    return 1;
}

static int keyio_exit(void)
{
    /* 1、向gpio子系统请求释放gpio */
    gpio_free(newchrdev.key_data.gpio);
    return 0;
}

static void key_tasklet(unsigned long data)
{
    printk("key_tasklet\r\n");
}

static void key_work(struct work_struct *work)
{
//    printk("key_work\r\n");
    mod_timer(&newchrdev.key_data.timer, jiffies + msecs_to_jiffies(TIMEOUT));
}

/* @description		: 中断服务函数
 *				  	  定时器用于按键消抖
 * @param - irq 	: 中断号 
 * @param - dev_id	: 设备结构
 * @return 			: 中断执行结果
 */
static irqreturn_t key0_handler(int irq, void *dev_id)
{
//    printk("key0_handler\r\n");
//    tasklet_schedule(&newchrdev.key_data.tasklet);
    schedule_work(&newchrdev.key_data.work);
	return IRQ_RETVAL(IRQ_HANDLED);
}

/*
 * @description	: key中断初始化
 * @param 		: 无
 * @return 		: 无
 */
static int keyirq_init(void)
{
    int ret = 0;
    /***** 使用中断子系统设置 *****/
    /* 1、从设备树获取key-gpio对于中断号 */
    newchrdev.key_data.irqnum = irq_of_parse_and_map(newchrdev.key_data.nd, 0);
    if (!newchrdev.key_data.irqnum){
        goto keyirq_init_error1;
    }
    /* 2、向中断子系统请求中断号 */
    ret = request_irq(newchrdev.key_data.irqnum, key0_handler, IRQF_TRIGGER_FALLING,"KEY0",&newchrdev);
    if(ret < 0){
        printk("can't request irq!\r\n");
        goto keyirq_init_error1;
    }
    /***** 使用中断上下部(tasklet和工作队列选择一个) *****/
    /* 1、初始化中断下半部 */
//    tasklet_init(&newchrdev.key_data.tasklet,key_tasklet,newchrdev.key_data.tasklet_data);
    INIT_WORK(&newchrdev.key_data.work, key_work);
    return 0;
keyirq_init_error1:
    return 1;
}

static int keyirq_exit(void)
{
    /* 1、向中断子系统请求释放中断号 */
    free_irq(newchrdev.key_data.irqnum,&newchrdev);
    /***** 使用中断上下部(tasklet和工作队列选择一个) *****/
    /* 1、销毁中断下半部 */
//    tasklet_kill(&newchrdev.key_data.tasklet);
    flush_work(&newchrdev.key_data.work);
    return 0;
}

void key_timer_function(unsigned long arg)
{
    unsigned char value;
//    printk("key_timer_function\r\n");
    /* 上报事件(特别注意上报1后,再上报0,才算一次完整按键上报) */
    input_report_key(newchrdev.key_data.keyinput, KEY_0, 1);
    input_sync(newchrdev.key_data.keyinput);
    input_report_key(newchrdev.key_data.keyinput,KEY_0, 0);
    input_sync(newchrdev.key_data.keyinput);
}

/*
 * @description	: key消抖定时器初始化
 * @param 		: 无
 * @return 		: 无
 */
static int keytimer_init(void)
{
    /* 1、创建定时器 */
    init_timer(&newchrdev.key_data.timer);
    newchrdev.key_data.timer.function = key_timer_function;
    newchrdev.key_data.timer.expires = jiffies + msecs_to_jiffies(TIMEOUT);
    newchrdev.key_data.timer.data = (unsigned long)&newchrdev;
    return 0;
}

static int keytimer_exit(void)
{
    /* 删除、创建定时器 */
    del_timer(&newchrdev.key_data.timer);
    return 0;
}

/*
 * @description		: 按键相关 input_dev 设备初始化
 * @param - dev 	: 按键私有数据
 * @return 			: 0 成功;其他 失败
 */
static int key_input_dev_init(void)
{
    int ret = 0;
    /* 1、申请 input_dev 设备结构体 */
    newchrdev.key_data.keyinput = input_allocate_device();
    if (newchrdev.key_data.keyinput == NULL) {
        printk("input_allocate_device failed!\r\n");
		ret = -ENOMEM;
        goto key_input_dev_init_fail1;
	}
    /* 2、初始化 input_dev 设备结构体 */
    newchrdev.key_data.keyinput->name = NEWCHRDEV_NAME;
    __set_bit(EV_KEY, newchrdev.key_data.keyinput->evbit);
    __set_bit(EV_REP, newchrdev.key_data.keyinput->evbit);
    __set_bit(KEY_0, newchrdev.key_data.keyinput->keybit);	
    /* 3、注册 input_dev 设备 */
    ret = input_register_device(newchrdev.key_data.keyinput);
	if (ret) {
		printk("input_register_device failed!\r\n");
		goto key_input_dev_init_fail2;
	}
    return 0;
key_input_dev_init_fail2:
    input_free_device(newchrdev.key_data.keyinput);
key_input_dev_init_fail1:
    return ret;
}

/*
 * @description		: 按键相关 input_dev 设备注销
 * @param - dev 	: 按键私有数据
 * @return 			: 0 成功;其他 失败
 */
static int key_input_dev_exit(void)
{
    /* 1、注销 input_dev 设备 */
    input_unregister_device(newchrdev.key_data.keyinput);
    /* 2、释放 input_dev 设备结构体 */
    input_free_device(newchrdev.key_data.keyinput);

    return 0;
}

/* 驱动入口函数 */
static int __init newchrdev_init(void)
{
    /* 驱动入口函数具体内容 */
    int ret;
    /* IO引脚初始化 */
    ret = keyio_init();
    if(ret != 0){
        goto keyio_init_failed;
    }
    /* 中断初始化 */
    ret = keyirq_init();
    if(ret != 0){
        goto keyirq_init_failed;
    }

    /* 初始化定时器 */
    keytimer_init();
    /* input 子系统初始化 */
    ret = key_input_dev_init();
    if(ret != 0){
        goto key_input_dev_init_failed;
    }

    return 0;
key_input_dev_init_failed:
    keyirq_exit();
keyirq_init_failed:
    keyio_exit();
keyio_init_failed:
    return ret;
}

/* 驱动卸载函数 */
static void __exit newchrdev_exit(void)
{
    key_input_dev_exit();
    keytimer_exit();
    keyirq_exit();
    keyio_exit();
}

module_init(newchrdev_init);
module_exit(newchrdev_exit);

MODULE_LICENSE("GPL");

测试:

# ls -l /dev/input/
total 0
crw-r-----    1 root     root       13,  64 Jan  1 00:00 event0
crw-r-----    1 root     root       13,  65 Jan  1 00:00 event1
crw-rw----    1 root     root       13,   0 Jan  1 00:00 js0
crw-r-----    1 root     root       13,  63 Jan  1 00:00 mice
# cat /proc/bus/input/devices
I: Bus=0019 Vendor=0000 Product=0000 Version=0000
N: Name="20cc000.snvs:snvs-powerkey"
P: Phys=snvs-pwrkey/input0
S: Sysfs=/devices/platform/soc/2000000.aips-bus/20cc000.snvs/20cc000.snvs:snvs-powerkey/input/input0
U: Uniq=
H: Handlers=kbd event0
B: PROP=0
B: EV=3
B: KEY=100000 0 0 0

I: Bus=0018 Vendor=0000 Product=0000 Version=0000
N: Name="EP0820M09"
P: Phys=
S: Sysfs=/devices/platform/soc/2100000.aips-bus/21a4000.i2c/i2c-1/1-0038/input/input1
U: Uniq=
H: Handlers=js0 event1
B: PROP=0
B: EV=9
B: ABS=2608000 3

#
# ls
key.ko       keyinput.ko  led.ko       led_app
# insmod key.ko
beep node find!
key num = 18
input: key as /devices/virtual/input/input2
# ls -l /dev/input/
total 0
crw-r-----    1 root     root       13,  64 Jan  1 00:00 event0
crw-r-----    1 root     root       13,  65 Jan  1 00:00 event1
crw-r-----    1 root     root       13,  66 Jan  1 01:26 event2
crw-rw----    1 root     root       13,   0 Jan  1 00:00 js0
crw-r-----    1 root     root       13,  63 Jan  1 00:00 mice
# cat /proc/bus/input/devices
I: Bus=0019 Vendor=0000 Product=0000 Version=0000
N: Name="20cc000.snvs:snvs-powerkey"
P: Phys=snvs-pwrkey/input0
S: Sysfs=/devices/platform/soc/2000000.aips-bus/20cc000.snvs/20cc000.snvs:snvs-powerkey/input/input0
U: Uniq=
H: Handlers=kbd event0
B: PROP=0
B: EV=3
B: KEY=100000 0 0 0

I: Bus=0018 Vendor=0000 Product=0000 Version=0000
N: Name="EP0820M09"
P: Phys=
S: Sysfs=/devices/platform/soc/2100000.aips-bus/21a4000.i2c/i2c-1/1-0038/input/input1
U: Uniq=
H: Handlers=js0 event1
B: PROP=0
B: EV=9
B: ABS=2608000 3

I: Bus=0000 Vendor=0000 Product=0000 Version=0000
N: Name="key"
P: Phys=
S: Sysfs=/devices/virtual/input/input2
U: Uniq=
H: Handlers=kbd event2
B: PROP=0
B: EV=100003
B: KEY=800

#
# hexdump /dev/input/event2
0000000 146a 0000 d356 000e 0001 000b 0001 0000
0000010 146a 0000 d356 000e 0000 0000 0000 0000
0000020 146a 0000 d38f 000e 0001 000b 0000 0000
0000030 146a 0000 d38f 000e 0000 0000 0000 0000
random: nonblocking pool is initialized
0000040 146d 0000 ed1b 000b 0001 000b 0001 0000
0000050 146d 0000 ed1b 000b 0000 0000 0000 0000
0000060 146d 0000 ed54 000b 0001 000b 0000 0000
0000070 146d 0000 ed54 000b 0000 0000 0000 0000
0000080 1470 0000 2d49 0000 0001 000b 0001 0000
0000090 1470 0000 2d49 0000 0000 0000 0000 0000
00000a0 1470 0000 2d80 0000 0001 000b 0000 0000
00000b0 1470 0000 2d80 0000 0000 0000 0000 0000
00000c0 1472 0000 24eb 0004 0001 000b 0001 0000
00000d0 1472 0000 24eb 0004 0000 0000 0000 0000
00000e0 1472 0000 2526 0004 0001 000b 0000 0000
00000f0 1472 0000 2526 0004 0000 0000 0000 0000
^C
# rmmod key.ko
#

七、input_event 类型的原始事件数据值含义

在这里插入图片描述

八、应用程序编写

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "stdio.h"
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <linux/input.h>

int main(int argc, char *argv[])
{
    int fd = 0;
    int err = 0;
    struct input_event inputevent;
    
    if(argc != 2){
		printf("Error Usage!\r\n");
		return -1;
	}

    fd = open(argv[1],O_RDWR);
    if(fd < 0){
        printf("Can't open file %s\r\n", argv[1]);
        return -1;
    }
    while(1){
        err = read(fd, &inputevent, sizeof(inputevent));
        if(err < 0){
            close(fd);
            return -1;
        }else if(err == 0){
            printf("No data\r\n");
        }else{
            switch(inputevent.type){
                case EV_KEY:
                    printf("EV_KEY key code = %d value = %d\r\n",inputevent.code,inputevent.value);
                    break;
                case KEY_RESERVED:
                    printf("KEY_RESERVED key code = %d value = %d\r\n",inputevent.code,inputevent.value);
                    break;
                default:
                    printf("default key code = %d value = %d\r\n",inputevent.code,inputevent.value);
                    break;
            }
        }
    }

    return 0;
}

九、测试

# ls
key.ko   key_app
# ls -l /dev/input/
total 0
crw-r-----    1 root     root       13,  64 Jan  1 00:00 event0
crw-r-----    1 root     root       13,  65 Jan  1 00:00 event1
crw-rw----    1 root     root       13,   0 Jan  1 00:00 js0
crw-r-----    1 root     root       13,  63 Jan  1 00:00 mice
#
# insmod key.ko
beep node find!
key num = 18
input: key as /devices/virtual/input/input3
# ls -l /dev/input/
total 0
crw-r-----    1 root     root       13,  64 Jan  1 00:00 event0
crw-r-----    1 root     root       13,  65 Jan  1 00:00 event1
crw-r-----    1 root     root       13,  66 Jan  1 00:24 event2
crw-rw----    1 root     root       13,   0 Jan  1 00:00 js0
crw-r-----    1 root     root       13,  63 Jan  1 00:00 mice
#
# rmmod key.ko
#
# ls -l /dev/input/
total 0
crw-r-----    1 root     root       13,  64 Jan  1 00:00 event0
crw-r-----    1 root     root       13,  65 Jan  1 00:00 event1
crw-rw----    1 root     root       13,   0 Jan  1 00:00 js0
crw-r-----    1 root     root       13,  63 Jan  1 00:00 mice
#
# insmod key.ko
beep node find!
key num = 18
input: key as /devices/virtual/input/input4
#
# ls -l /dev/input/
total 0
crw-r-----    1 root     root       13,  64 Jan  1 00:00 event0
crw-r-----    1 root     root       13,  65 Jan  1 00:00 event1
crw-r-----    1 root     root       13,  66 Jan  1 00:25 event2
crw-rw----    1 root     root       13,   0 Jan  1 00:00 js0
crw-r-----    1 root     root       13,  63 Jan  1 00:00 mice
#
# ./key_app /dev/input/event2
^C
#
# ./key_app /dev/input/event2 &
#
# EV_KEY key code = 11 value = 1
KEY_RESERVED key code = 0 value = 0
EV_KEY key code = 11 value = 0
KEY_RESERVED key code = 0 value = 0
EV_KEY key code = 11 value = 1
KEY_RESERVED key code = 0 value = 0
EV_KEY key code = 11 value = 0
KEY_RESERVED key code = 0 value = 0
EV_KEY key code = 11 value = 1
KEY_RESERVED key code = 0 value = 0
EV_KEY key code = 11 value = 0
KEY_RESERVED key code = 0 value = 0

#
#

十、系统资源使用情况

在这里插入图片描述

十一、特别说明

1、使用 input 子系统,不需要再注册设备字符,input 子系统会创建一个字符设备,主设备号为 13

2、查看与 /dev/input 目录下的 event 对应的设备

cat /proc/bus/input/devices
# cat /proc/bus/input/devices
I: Bus=0019 Vendor=0000 Product=0000 Version=0000
N: Name="20cc000.snvs:snvs-powerkey"
P: Phys=snvs-pwrkey/input0
S: Sysfs=/devices/platform/soc/2000000.aips-bus/20cc000.snvs/20cc000.snvs:snvs-powerkey/input/input0
U: Uniq=
H: Handlers=kbd event0
B: PROP=0
B: EV=3
B: KEY=100000 0 0 0

I: Bus=0018 Vendor=0000 Product=0000 Version=0000
N: Name="EP0820M09"
P: Phys=
S: Sysfs=/devices/platform/soc/2100000.aips-bus/21a4000.i2c/i2c-1/1-0038/input/input1
U: Uniq=
H: Handlers=js0 event1
B: PROP=0
B: EV=9
B: ABS=2608000 3

I: Bus=0000 Vendor=0000 Product=0000 Version=0000
N: Name="key"
P: Phys=
S: Sysfs=/devices/virtual/input/input2
U: Uniq=
H: Handlers=kbd event2
B: PROP=0
B: EV=100003
B: KEY=800

#

3、上报事件时,需要特别注意(重要)

  • 按键按下上报 1
  • 按键释放上报 0
  • 如果按键只上报 1,一直未上报 0Linux 内核认定按键一直按下,一直上报重复事件。
  • 如果按键上报 1 后,马上上报 0,即使一直按下,也不会算重复事件。

十二、input子系统+platform总线

1、驱动代码

#include "linux/init.h"
#include "linux/module.h"
#include "linux/kdev_t.h"
#include "linux/fs.h"
#include "linux/cdev.h"
#include "linux/device.h"
#include <linux/uaccess.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/workqueue.h>
#include <linux/input.h>
#include <linux/platform_device.h>

#define NEWCHRDEV_NAME  "key"       /* 名子 */
#define TIMEOUT 1000                /* 定时器超时时间 */

typedef struct{
    struct device_node	*nd;        /* 设备节点 */
    int gpio;                       /* key gpio */
    int irqnum;                     /* 中断号 */
    struct tasklet_struct tasklet;  /* tasklet 变量(中断下半部) */
    unsigned long tasklet_data;     /* 传递给 tasklet 处理函数的参数 */
    struct work_struct work;        /* 工作队列 变量(中断下半部) */
    struct timer_list timer;        /* key 消抖定时器 */
    struct input_dev *keyinput;     /* 按键 input_dev 结构体变量 */
}lqkey_t;

typedef struct{
    lqkey_t key_data;         /* key 相关数据 */
}newchrdev_t;

newchrdev_t newchrdev;

/*
 * @description	: 按键IO初始化
 * @param 		: 无
 * @return 		: 无
 */
static int keyio_init(void)
{
    int ret = 0;
    /***** 处理设备树 *****/
    /* 1、获取设备树节点 */
    newchrdev.key_data.nd = of_find_node_by_path("/key");
	if (newchrdev.key_data.nd== NULL){
		printk("key node not find!\r\n");
		goto keyio_init_error1;
	}else {
		printk("beep node find!\r\n");
	}
    /* 1、获取设备树中的gpio属性 */
    newchrdev.key_data.gpio = of_get_named_gpio(newchrdev.key_data.nd, "key-gpio", 0);
	if(newchrdev.key_data.gpio < 0) {
		printk("can't get key-gpio");
		goto keyio_init_error1;
	}
	printk("key num = %d\r\n", newchrdev.key_data.gpio);

    /***** 使用gpio子系统设置引脚 *****/
    /* 1、向 gpio 子系统申请 GPIO 管脚 */
    ret = gpio_request(newchrdev.key_data.gpio,"key");
    if(ret){
        printk("can't request key-gpio!\r\n");
        goto keyio_init_error1;
    }
    /* 2、设置key-gpio为输入 */
    gpio_direction_input(newchrdev.key_data.gpio);
    if(ret < 0) {
		printk("can't set key-gpio!\r\n");
        goto keyio_init_error2;
	}
    return 0;

keyio_init_error2:
    gpio_free(newchrdev.key_data.gpio);
keyio_init_error1:
    return 1;
}

static int keyio_exit(void)
{
    /* 1、向gpio子系统请求释放gpio */
    gpio_free(newchrdev.key_data.gpio);
    return 0;
}

static void key_tasklet(unsigned long data)
{
    printk("key_tasklet\r\n");
}

static void key_work(struct work_struct *work)
{
//    printk("key_work\r\n");
    mod_timer(&newchrdev.key_data.timer, jiffies + msecs_to_jiffies(TIMEOUT));
}

/* @description		: 中断服务函数
 *				  	  定时器用于按键消抖
 * @param - irq 	: 中断号 
 * @param - dev_id	: 设备结构
 * @return 			: 中断执行结果
 */
static irqreturn_t key0_handler(int irq, void *dev_id)
{
//    printk("key0_handler\r\n");
//    tasklet_schedule(&newchrdev.key_data.tasklet);
    schedule_work(&newchrdev.key_data.work);
	return IRQ_RETVAL(IRQ_HANDLED);
}

/*
 * @description	: key中断初始化
 * @param 		: 无
 * @return 		: 无
 */
static int keyirq_init(void)
{
    int ret = 0;
    /***** 使用中断子系统设置 *****/
    /* 1、从设备树获取key-gpio对于中断号 */
    newchrdev.key_data.irqnum = irq_of_parse_and_map(newchrdev.key_data.nd, 0);
    if (!newchrdev.key_data.irqnum){
        goto keyirq_init_error1;
    }
    /* 2、向中断子系统请求中断号 */
    ret = request_irq(newchrdev.key_data.irqnum, key0_handler, IRQF_TRIGGER_FALLING,"KEY0",&newchrdev);
    if(ret < 0){
        printk("can't request irq!\r\n");
        goto keyirq_init_error1;
    }
    /***** 使用中断上下部(tasklet和工作队列选择一个) *****/
    /* 1、初始化中断下半部 */
//    tasklet_init(&newchrdev.key_data.tasklet,key_tasklet,newchrdev.key_data.tasklet_data);
    INIT_WORK(&newchrdev.key_data.work, key_work);
    return 0;
keyirq_init_error1:
    return 1;
}

static int keyirq_exit(void)
{
    /* 1、向中断子系统请求释放中断号 */
    free_irq(newchrdev.key_data.irqnum,&newchrdev);
    /***** 使用中断上下部(tasklet和工作队列选择一个) *****/
    /* 1、销毁中断下半部 */
//    tasklet_kill(&newchrdev.key_data.tasklet);
    flush_work(&newchrdev.key_data.work);
    return 0;
}

void key_timer_function(unsigned long arg)
{
//    printk("key_timer_function\r\n");
    /* 上报事件(特别注意上报1后,再上报0,才算一次完整按键上报) */
    input_report_key(newchrdev.key_data.keyinput, KEY_0, 1);
    input_sync(newchrdev.key_data.keyinput);
    input_report_key(newchrdev.key_data.keyinput,KEY_0, 0);
    input_sync(newchrdev.key_data.keyinput);
}

/*
 * @description	: key消抖定时器初始化
 * @param 		: 无
 * @return 		: 无
 */
static int keytimer_init(void)
{
    /* 1、创建定时器 */
    init_timer(&newchrdev.key_data.timer);
    newchrdev.key_data.timer.function = key_timer_function;
    newchrdev.key_data.timer.expires = jiffies + msecs_to_jiffies(TIMEOUT);
    newchrdev.key_data.timer.data = (unsigned long)&newchrdev;
    return 0;
}

static int keytimer_exit(void)
{
    /* 删除、创建定时器 */
    del_timer(&newchrdev.key_data.timer);
    return 0;
}

/*
 * @description		: 按键相关 input_dev 设备初始化
 * @param - dev 	: 按键私有数据
 * @return 			: 0 成功;其他 失败
 */
static int key_input_dev_init(void)
{
    int ret = 0;
    /* 1、申请 input_dev 设备结构体 */
    newchrdev.key_data.keyinput = input_allocate_device();
    if (newchrdev.key_data.keyinput == NULL) {
        printk("input_allocate_device failed!\r\n");
		ret = -ENOMEM;
        goto key_input_dev_init_fail1;
	}
    /* 2、初始化 input_dev 设备结构体 */
    newchrdev.key_data.keyinput->name = NEWCHRDEV_NAME;
    __set_bit(EV_KEY, newchrdev.key_data.keyinput->evbit);
    __set_bit(EV_REP, newchrdev.key_data.keyinput->evbit);
    __set_bit(KEY_0, newchrdev.key_data.keyinput->keybit);	
    /* 3、注册 input_dev 设备 */
    ret = input_register_device(newchrdev.key_data.keyinput);
	if (ret) {
		printk("input_register_device failed!\r\n");
		goto key_input_dev_init_fail2;
	}
    return 0;
key_input_dev_init_fail2:
    input_free_device(newchrdev.key_data.keyinput);
key_input_dev_init_fail1:
    return ret;
}

/*
 * @description		: 按键相关 input_dev 设备注销
 * @param - dev 	: 按键私有数据
 * @return 			: 0 成功;其他 失败
 */
static int key_input_dev_exit(void)
{
    /* 1、注销 input_dev 设备 */
    input_unregister_device(newchrdev.key_data.keyinput);
    /* 2、释放 input_dev 设备结构体 */
    input_free_device(newchrdev.key_data.keyinput);

    return 0;
}

/* 驱动入口函数 */
static int key_probe(struct platform_device *dev)
{
    /* 驱动入口函数具体内容 */
    int ret;
    /* IO引脚初始化 */
    ret = keyio_init();
    if(ret != 0){
        goto keyio_init_failed;
    }
    /* 中断初始化 */
    ret = keyirq_init();
    if(ret != 0){
        goto keyirq_init_failed;
    }

    /* 初始化定时器 */
    keytimer_init();
    /* input 子系统初始化 */
    ret = key_input_dev_init();
    if(ret != 0){
        goto key_input_dev_init_failed;
    }

    return 0;
key_input_dev_init_failed:
    keyirq_exit();
keyirq_init_failed:
    keyio_exit();
keyio_init_failed:
    return ret;
}

/* 驱动卸载函数 */
static int key_remove(struct platform_device *dev)
{
    key_input_dev_exit();
    keytimer_exit();
    keyirq_exit();
    keyio_exit();

    return 0;
}

/* 匹配列表 */
static const struct of_device_id key_of_match[] = {
	{ .compatible = "lq-key" },
	{ /* Sentinel */ }
};

/* platform驱动结构体 */
static struct platform_driver key_driver = {
	.probe		= key_probe,
	.remove         = key_remove,
	.driver         = {
		.name   = "lq-key",
		.of_match_table = key_of_match,
	},
};

/* 驱动入口函数 */
static int __init newchrdev_init(void)
{
    return platform_driver_register(&key_driver);
}

static void __exit newchrdev_exit(void)
{
    platform_driver_unregister(&key_driver);
}

module_init(newchrdev_init);
module_exit(newchrdev_exit);

MODULE_LICENSE("GPL");

2、应用程序代码

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "stdio.h"
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <linux/input.h>

int main(int argc, char *argv[])
{
    int fd = 0;
    int err = 0;
    struct input_event inputevent;
    
    if(argc != 2){
		printf("Error Usage!\r\n");
		return -1;
	}

    fd = open(argv[1],O_RDWR);
    if(fd < 0){
        printf("Can't open file %s\r\n", argv[1]);
        return -1;
    }
    while(1){
        err = read(fd, &inputevent, sizeof(inputevent));
        if(err < 0){
            close(fd);
            return -1;
        }else if(err == 0){
            printf("No data\r\n");
        }else{
            switch(inputevent.type){
                case EV_KEY:
                    printf("EV_KEY key code = %d value = %d\r\n",inputevent.code,inputevent.value);
                    break;
                case KEY_RESERVED:
                    printf("KEY_RESERVED key code = %d value = %d\r\n",inputevent.code,inputevent.value);
                    break;
                default:
                    printf("default key code = %d value = %d\r\n",inputevent.code,inputevent.value);
                    break;
            }
        }
    }

    return 0;
}

3、测试

# ls
key.ko   key_app
#
# find / -name lq-key
#
# ls -l /dev/input/
total 0
crw-r-----    1 root     root       13,  64 Jan  1 00:00 event0
crw-r-----    1 root     root       13,  65 Jan  1 00:00 event1
crw-rw----    1 root     root       13,   0 Jan  1 00:00 js0
crw-r-----    1 root     root       13,  63 Jan  1 00:00 mice
#
# insmod key.ko
beep node find!
key num = 18
input: key as /devices/virtual/input/input2
#
# find / -name lq-key
/sys/bus/platform/drivers/lq-key
#
# ls -l /dev/input/
total 0
crw-r-----    1 root     root       13,  64 Jan  1 00:00 event0
crw-r-----    1 root     root       13,  65 Jan  1 00:00 event1
crw-r-----    1 root     root       13,  66 Jan  1 00:56 event2
crw-rw----    1 root     root       13,   0 Jan  1 00:00 js0
crw-r-----    1 root     root       13,  63 Jan  1 00:00 mice
#
# ./key_app /dev/input/event
Can't open file /dev/input/event
# ./key_app /dev/input/event2
EV_KEY key code = 11 value = 1
KEY_RESERVED key code = 0 value = 0
EV_KEY key code = 11 value = 0
KEY_RESERVED key code = 0 value = 0
EV_KEY key code = 11 value = 1
KEY_RESERVED key code = 0 value = 0
EV_KEY key code = 11 value = 0
KEY_RESERVED key code = 0 value = 0
^C
#
# rmmod key.ko
#
# find / -name lq-key
#
# ls -l /dev/input/
total 0
crw-r-----    1 root     root       13,  64 Jan  1 00:00 event0
crw-r-----    1 root     root       13,  65 Jan  1 00:00 event1
crw-rw----    1 root     root       13,   0 Jan  1 00:00 js0
crw-r-----    1 root     root       13,  63 Jan  1 00:00 mice
#

相关文章:

  • Django用户认证系统
  • 论坛介绍 | COSCon'22 开源硬件(H)
  • 【Vulnhub靶场】——HARRYPOTTER第三部: FAWKES
  • [附源码]Java计算机毕业设计SSMjava视频点播系统
  • Day768.大佬推荐的经典的Redis学习资料 -Redis 核心技术与实战
  • fastdfs简介及在springboot中使用
  • OpenCASCADE使用(Stp to Gltf)
  • 进程互斥的硬件实现方式【操作系统学习笔记】
  • JavaScript教程-原生的原型,Object.prototype,其他的内建原型,从原型当中借用,原型方法,_proto_
  • (Java岗)秋招打卡!一本学历拿下美团、阿里、快手、米哈游offer
  • 基于微信小程序的数码商城程序设计与实现(后台PHP+Mysql)
  • FA_06.不用刷机情况下升级或者降级系统中的fridaserver
  • 【笔试题】【day3】
  • 微信小程序|基于小程序实现人脸识别对比
  • [附源码]Java计算机毕业设计SSMJava商场会员管系统
  • [case10]使用RSQL实现端到端的动态查询
  • 【笔记】你不知道的JS读书笔记——Promise
  • 【从零开始安装kubernetes-1.7.3】2.flannel、docker以及Harbor的配置以及作用
  • 0x05 Python数据分析,Anaconda八斩刀
  • Debian下无root权限使用Python访问Oracle
  • ES10 特性的完整指南
  • Essential Studio for ASP.NET Web Forms 2017 v2,新增自定义树形网格工具栏
  • Java面向对象及其三大特征
  • linux学习笔记
  • Meteor的表单提交:Form
  • PHP面试之三:MySQL数据库
  • Swift 中的尾递归和蹦床
  • Vue ES6 Jade Scss Webpack Gulp
  • 大数据与云计算学习:数据分析(二)
  • 大整数乘法-表格法
  • 分布式任务队列Celery
  • 高性能JavaScript阅读简记(三)
  • 关于extract.autodesk.io的一些说明
  • 聊聊flink的BlobWriter
  • 排序算法之--选择排序
  • 浅谈Kotlin实战篇之自定义View图片圆角简单应用(一)
  • 悄悄地说一个bug
  • 实战|智能家居行业移动应用性能分析
  • 通过git安装npm私有模块
  • 用简单代码看卷积组块发展
  • - 语言经验 - 《c++的高性能内存管理库tcmalloc和jemalloc》
  • 找一份好的前端工作,起点很重要
  • ​一文看懂数据清洗:缺失值、异常值和重复值的处理
  • #if #elif #endif
  • #Z0458. 树的中心2
  • $L^p$ 调和函数恒为零
  • (02)vite环境变量配置
  • (多级缓存)缓存同步
  • (附源码)springboot宠物医疗服务网站 毕业设计688413
  • (一)硬件制作--从零开始自制linux掌上电脑(F1C200S) <嵌入式项目>
  • (原創) 物件導向與老子思想 (OO)
  • (最全解法)输入一个整数,输出该数二进制表示中1的个数。
  • *p++,*(p++),*++p,(*p)++区别?
  • ./和../以及/和~之间的区别
  • .NET 4 并行(多核)“.NET研究”编程系列之二 从Task开始