《网蜂A8实战演练》——5.Linux输入子系统

第7章  Linux 输入子系统
学习完驱动教程的 1~6 章,如果你觉得你能够消化前面学习的知识,并把实例都自己动手做成功的话,恭喜你,你的驱动已经入门了。但,这只是驱动的刚刚开始,后面的路还很难走。从本章开始,知识的难度越来越大,不过你放心,只要你认真把教程看完,我相信,你一定能少走很多弯路。到时候,别忘了回头感谢网蜂哟。好了,废话不多说,直奔主题。在此文章之前,我们讲解的都是简单的字符驱动,涉及的内容有字符驱动的框架、自动创建设备节点、linux 中断、poll 机制、异步通知、定时器去抖动等等相关内容。可以说,这些真是驱动部分最基础的知识,但又起着重要的作用,没有这些基础的内容,一下子进入困难级别,这谁受得了,对吧?


7.1  Linux Input 子系统
什么是 Linux Input 子系统呢?顾名思义,就是输入子系统。这不是废话吗?说了等于没说。换句话说,Linux 系统下把键盘、鼠标、触摸屏、游戏句柄等等设备都当做是输入设备。作为一个输入设备,好处可大了。因为 Linux 输入子系统已经帮输入设备完成很大一部分通用的处理功能了。就比如说,以前我们写字符设备驱动时的读写函数,这些都不用驱动工程师去编写了,输入子系统已经帮我们写好了。这种思想,在 Linux 内核多了去呢!


7.1.1  Input 子系统层次框架
输入(Input)子系统是分层架构的,总共分为 3 层,从上到下分别是:事件处理层(Event Handler)、输入子系统核心层(Input Core)、硬件驱动层(Input Driver)。如图 7.1:


(1) 硬件驱动层负责操作具体的硬件设备,这层的代码是针对具体的驱动程序的,比如你的设备是触摸输入设备,还是鼠标输入设备,还是键盘输入设备,这些不同的设备,自然有不同的硬件操作,驱动工程师往往只需要完成这层的代码编写。
(2) 输入子系统核心层是链接其他两层之间的纽带与桥梁,向下提供硬件驱动层的接口,向上提供事件处理层的接口。
(3) 事件处理厂负责与用户程序打交道,将硬件驱动层传来的事件报告给用户程序。

各层之间通信的基本单位就是事件,任何一个输入设备的动作都可以抽象成一种事件,如键盘的按下,触摸屏的按下,鼠标的移动等。事件有三种属性:类型(type),编码(code),值(value),Input 子系统支持的所有事件都定义在 input.h中,包括所有支持的类型,所属类型支持的编码等。事件传送的方向是 硬件驱动层-->子系统核心-->事件处理层-->用户空间

7.1.2  三个重要的结构体

7.1.2.1 输入设备(input_dev)

Linux 系统里,使用 input_dev 结构体抽象一个输入设备,编写硬件驱动就是主要围绕这个结构体而进行的。由于它的成员比较多,这里只挑最重点的讲解。其他成员请参考 include/linux/input.h

struct input_dev {
const char *name;
/* 标识设备驱动特征,如总线类型、生产厂商、产品类型、版本 */
struct input_id id;
/* 表示能产生哪类事件 */
unsigned long evbit[NBITS(EV_MAX)];
/* 表示能产生哪些按键 */
unsigned long keybit[NBITS(KEY_MAX)];
/* 表示能产生哪些相对位移事件, x,y,滚轮 */
unsigned long relbit[NBITS(REL_MAX)];
/* 表示能产生哪些绝对位移事件, x,y */
unsigned long absbit[NBITS(ABS_MAX)];
struct device dev;

/* 用来链接他所支持的 input_handle 结构,然后用
* input_handle 找到里面的 input_handler
*/
struct list_head h_list;
/* 链接到 input_handler_list,这个链表
* 链接了所有注册到内核的事件处理器
*/
struct list_head node;
...
}


input_dev 结构体里有几个数组也是非常重要的,都是我们写硬件驱动需要进行设置的,所以我们务必认识一下它们。

(1) evbit[BITS_TO_LONGS(EV_CNT)]数组,这个数组以位掩码的形式,代表了这个设备支持哪类事件,比如:

#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
#define EV_SND  0x12
#define EV_REP  0x14 //重复类
#define EV_FF  0x15
#define EV_PWR  0x16
#define EV_FF_STATUS  0x17
#define EV_MAX  0x1f
#define EV_CNT  (EV_MAX+1)

(2) keybit[BITS_TO_LONGS(KEY_CNT)]数组,这个数组也是以位掩码的形式,代表这个设备支持哪些按键,比如:

#define KEY_ESC  1
#define KEY_1  2
#define KEY_2  3
#define KEY_3  4
#define KEY_TAB  15
#define KEY_ENTER  28
#define KEY_A  30
#define KEY_B  48
#define KEY_C  46
......



(3) relbit[BITS_TO_LONGS(REL_CNT)]数组,这个数组也是以位掩码的形式,代表这个设备支持哪些相对位移事件,比如:

#define REL_X  0x00
#define REL_Y  0x01
#define REL_Z  0x02
#define REL_WHEEL  0x08 //滚轮
......



(4) absbit[BITS_TO_LONGS(ABS_CNT)]数组,这个数组也是以位掩码的形式,代表这个设备支持哪些绝对位移事件,比如:

#define ABS_X  0x00
#define ABS_Y  0x01

7.1.2.2 事件处理器(input_handler)
input_handler 属于输入子系统三层中的事件处理层的一个重要结构体。主要成员有:其他成员请参考 include/linux/input.h

struct input_handler {
/* 当事件处理器接收到了来自 input 设备传来的
* 事件时调用的处理函数,负责处理事件。
*/
void (*event)(struct input_handle *handle,unsigned int type, unsigned int code, int value);
/* 当一个 input 设备注册到内核的时候被调用,将事件处理器与输入设备
* 联系起来的函数,也就是将 input_dev 和 input_handler 配对的函数。
*/
int (*connect)(struct input_handler *handler, struct input_dev *dev,const struct input_device_id *id);
/* 与 connect 相反 */
void (*disconnect)(struct input_handle *handle);
/* 文件操作集,因为事件处理器要完成读写功能 */
const struct file_operations  *fops;
/* 事件处理器所支持的 input 设备 */
const struct input_device_id *id_table;
/* 链接他所支持的 input_handle 结构,然后用
* input_handle 找到里面的 input_dev
*/
struct list_head  h_list;

/* 链接到 input_handler_list,这个链表
* 链接了所有注册到内核的事件处理器
*/
struct list_head node;
};


7.1.2.3 事件沟通者(input_handle)
之所以称 input_handle(注意了,不是事件处理器 input_handler)为事件沟通者,是因为它代表一个成功配对的 input_dev 和 input_handler。主要成员有:其他成员请参考 include/linux/input.h

struct input_handle {
/* 每个配对的事件处理器都会分配一个对应的设备结构,
* 如 evdev 事件处理器的 evdev 结构,注意这个结构与
* 设备驱动层的 input_dev 不同,初始化 handle 时,保存到这里。
*/
void *private;
/* 指向 input_dev 结构体实例 */
struct input_dev *dev;
/* 指向 input_handler 结构体实例 */
struct input_handler *handler;
/* input_handle 通过 d_node 连接到了 input_dev 上的 h_list 链表上 */
struct list_head d_node;
/* input_handle 通过 h_node 连接到了 input_handler 的 h_list 链表 */
struct list_head h_node;
};

7.1.2.4 三个结构体之间的关系
input_dev 是硬件驱动层,代表一个 input 设备。通过全局的 input_dev_list 链接在一起。设备注册的时候实现这个操作。
input_handler 是事件处理层,代表一个事件处理器。通过全局的 input_handler_list 链接在一起。事件处理器注册的时候实现这个操作(事件处理器一般内核自带,一般不需要我们来写)
input_handle 个人认为属于核心层,代表一个配对的 input 设备与 input 事件处理器。它没有一个全局的链表,它注册的时候将自己分别挂在了 input_dev 和input_handler 的 h_list 上了。通过 input_dev 和 input_handler 就可以找到input_handle 在设备注册和事件处理器, 注册的时候都要进行配对工作,配对后就会实现链接。通过 input_handle 也可以找到 input_dev 和 input_handler。


7.2  Input 子系统核心层分析
前面我们说过,子系统核心的功能是:向下提供硬件驱动层的接口,向上提供事件处理层的接口。子系统的核心文件在 drivers/input/input.c,这文件代码量有 2000+行,我们也不可能每一个函数都去分析一遍,只把重点抓出来。前面讲的三个重要的数据结构都会对应一个注册函数,他们都定义在子系统核心的 input.c 文件中。

主要有三个注册函数:

/* 向内核注册一个 input 设备 */
int input_register_device(struct input_dev *dev)
/* 向内核注册一个事件处理器 */
int input_register_handler(struct input_handler *handler)
/* 向内核注册一个 handle 结构 */
int input_register_handle(struct input_handle *handle)


7.2.1  input_register_device

/* 详细请参考 drivers/input/input.c */
int input_register_device(struct input_dev *dev)
{
struct input_handler *handler;
...
/* 设置输入设备支持同步类事件 */
__set_bit(EV_SYN, dev->evbit);
/* 将 device 加入到 linux 设备模型中去 */
device_add(&dev->dev);
...
/* 把 input_dev 放入 input_dev_list 链表 */
list_add_tail(&dev->node, &input_dev_list);
...
/* 对于 input_handler_list 链表的每一项,都调用 input_attach_handler
* 它根据 input_handler 的 id_table 判断能否支持这个 input_dev
*/
list_for_each_entry(handler, &input_handler_list, node)
input_attach_handler(dev, handler);
...
}


input_register_device 完成的主要功能就是:初始化一些默认的值,将自己的 device 结构添加到 linux 设备模型当中,将 input_dev 添加到 input_dev_list
链表中,对于 input_handler_list 链表的每一项,都调用 input_attach_handler它根据 input_handler 的 id_table 判断能否支持这个输入设备。


7.2.2  input_attach_handler

/* 详细请参考 drivers/input/input.c */
static int input_attach_handler(struct input_dev *dev,struct input_handler *handler)
{
const struct input_device_id *id;
int error;
/* 根据 input_handler 的 id_table 判断能否支持这个输入设备 */
id = input_match_device(handler, dev);
if (!id)
return -ENODEV;
/* 若支持,则调用 input_handler 的 connect 函数,建立连接
* 对于 evdev.c 里,则调用 evdev_connect 函数
*/
error = handler->connect(handler, dev, id);
if (error && error != -ENODEV)
pr_err("failed to attach handler %s to device %s, error: %d\n",
handler->name, kobject_name(&dev->dev.kobj), error);
return error;
}


7.2.3  evdev_connect

evdev_connect 函数在 drivers/input/evdev.c 文件里实现。对于 connect 函数,每种事件处理器的实现都有差异,但原理都相同,按键、触摸屏用的事件处理器都为 evdev,下面分析 evdev 的 connect 函数 evdev_connect。

static int evdev_connect(struct input_handler *handler,struct input_dev *dev, const struct input_device_id *id)
{
struct evdev *evdev;
...
/* 分配一个 evdev 结构体,它里面包括 input_handle 结构体 */
evdev = kzalloc(sizeof(struct evdev), GFP_KERNEL);
...
/* 设备节点,如/dev/event0 */
dev_set_name(&evdev->dev, "event%d", dev_no);
...
/* handle 里的 input_dev 指向的实例化的 input_dev */
evdev->handle.dev = input_get_device(dev);
evdev->handle.name = evdev->name;
/* handle 里的 input_handler 指向的实例化的 input_handler */
evdev->handle.handler = handler;
evdev->handle.private = evdev;
......
/* 注册 */
input_register_handle(&evdev->handle);
/* 初始化 cdev */
cdev_init(&evdev->cdev, &evdev_fops);
evdev->cdev.kobj.parent = &evdev->dev.kobj;
/* 注册字符设备 */
error = cdev_add(&evdev->cdev, evdev->dev.devt, 1);
...
}


7.2.4  input_register_handle


/* 详细请参考 drivers/input/input.c */
int input_register_handle(struct input_handle *handle)
{
struct input_handler *handler = handle->handler;
struct input_dev *dev = handle->dev;
...
/* 把 handle->d_node 添加到 dev->h_list
* 这样,就可以从 dev->h_list 找到 handle,进而找到 handler
*/
list_add_tail_rcu(&handle->d_node, &dev->h_list);
...
/* 把 handle->h_node 添加到 handler->h_list
* 这样,就可以从 handler->h_list 找到 handle,进而找到 dev
*/
list_add_tail(&handle->h_node, &handler->h_list);
...
return 0;
}

input_register_handle 比较简单,主要把 handle 结构体通过 d_node 链表项链接到 input_dev 的 h_list;把 handle 结构体通过 h_node 链表项链接到
input_handler 的 h_list。这样一来,就可以通过 input_dev 的 h_list 找到 handle,进而找到 handle里的 handler;或者通过 input_handler 的 h_list 找到 handle,进而找到 handle里的 input_dev。


7.2.5  input_register_handler

/* 详细请参考 drivers/input/input.c */
int input_register_handler(struct input_handler *handler)
{
struct input_dev *dev;
......
INIT_LIST_HEAD(&handler->h_list);
/* 将 handler 放入 input_handler_list 链表 */
list_add_tail(&handler->node, &input_handler_list);
/* 对于每个 input_dev,调用 input_attach_handler(前面讲过)
* 它根据 input_handler 的 id_table 判断能否支持这个输入设备
* 如果支持,则调用 handler->connect 函数,建立“连接”
*/
list_for_each_entry(dev, &input_dev_list, node)
input_attach_handler(dev, handler);
......
return 0;
}



7.2.6  input 子系统接口总结
再一次强调 Input 子系统核心的功能是:向下提供硬件驱动层的接口,向上提供事件处理层的接口。

向下对硬件驱动层的接口主要有:input_allocate_device 这个函数主要是分配一个 input_dev 输入设备接口,并初始化一些基本的成员,这就是我们不能简单用 kmalloc 分配 input_dev 结构的原因,因为缺少了一些初始化。input_register_device 注册一个 input 设备,input_unregister_device 注销一个 input 设备。input_event()函数是硬件驱动层向 input 子系统核心报告事件的函数,这 4 个函数一般用在驱动工程师需要完成的具体硬件驱动程序中。
向上对事件处理层接口主要有:input_register_handler 注册一个事件处理器,input_register_handle注册一个 input_handle 结构,它将输入设备和事件处理器联系起来。


7.3  事件处理层分析
进入 drivers/input/目录可以发现,Linux 系统中有很多事件处理器, joydev、如mousedev、evdev 等等。因为 evdev 事件处理器可以处理所有的事件,按键、
触摸屏设备驱动用的时间处理器就是 evdev。比较通用,我们这里以 evdev 为例,分析事件处理器做了什么。

还记得前面说的吗?事件处理层与用户程序和输入子系统核心打交道,是他们两层的桥梁。如果忘了,就回去看看图 7.1 呗~~先看它的入口函数(参考 drivers/input/evdev.c):

static int __init evdev_init(void)
{
/* 向内核注册 evdev_handler */
return input_register_handler(&evdev_handler);
}

evdev_handler 是一个 input_handler 型结构体,它代表一个事件处理器,既然是一个事件处理器,当然有重要的成员,我们来看看。

static struct input_handler evdev_handler = {
.event= evdev_event,
/* 用来处理事件 */
.events= evdev_events,
/* 用来连接 input_dev 和 input_handler */
.connect= evdev_connect,
.disconnect= evdev_disconnect,
.legacy_minors = true,
.minor= EVDEV_MINOR_BASE,
.name= "evdev",
/* 表示 evdev_handler 能够支持哪一些输入设备 */
.id_table= evdev_ids,
};

前 面我们说过, 一个事件 处理器能不能支持一个输入设备,主要通过input_attach_handler 函数来判定,它根据 input_handler 的 id_table 判断能否支持这个 input_dev。那我们来看看 evdev_handler 的 id_table:


static const struct input_device_id evdev_ids[] = {
{

.driver_info = 1 }, /* Matches all devices */
{ },/* Terminating zero entry */
}
evdev_ids 没有定义 flags,也没有定义匹配属性值。这个 evdev_ids 的意思就是:evdev_handler 可以匹配所有 input_dev 设备,也就是所有的 input_dev 发
出的事件,都可以由 evdev_handler 来处理。


7.3.1  evdev_connect

evdev_connect 函 数 在 前 面 已 经 分 析 过 了 , 这 里 简 单 讲 述 一 下 。 当input_register_handler 向内核注册事件处理器时,就会对于每个 input_dev,调
用 input_attach_handler(前面讲过) ,它根据 input_handler 的 id_table 判断能否支持这个输入设备如果支持,则调用 handler->connect 函数,建立“连接” 。
而对于 evdev.c 里,则调用 evdev_connect 函数。evdev_connect 主要工作是:分配设置一个包含 input_handle 的 evdev 结构,关联 input_dev 和input_handler,向内核注册一个 handle,最后注册一个主设备号为 13,file_operations 为 evdev_fops 的字符设备。这样不就又回到字符设备的那套框架了?

static const struct file_operations evdev_fops = {
.owner= THIS_MODULE,
.read= evdev_read,
.write= evdev_write,
.poll= evdev_poll,
.open= evdev_open,
.release= evdev_release,
.unlocked_ioctl = evdev_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl= evdev_ioctl_compat,
#endif
.fasync= evdev_fasync,
.flush= evdev_flush,
.llseek= no_llseek,
};

后面写硬件驱动时,你会发现并没有编写相关的读写函数,是因为事件处理层已经帮我们做好了,只需要硬件设备把产生的事件传递给事件处理层就可以了,
事件处理层会帮我们完成。这就大大减少了驱动工程师的工作量啦。其实,除了输入子系统,Linux 内核还有很多利用这种思想的。比如:总线设备驱动模型,
I2C 模型等等。


7.3.2  evdev_open
当事件处理器找到支持的输入设备时,就会建立连接,然后注册一个主设备号为 13,file_operations 为 evdev_fops 的字符设备。用户程序可以通过设备节点/dev/input/event0,/dev/input/event1 来访问输入设备。比如用户程序使用open(“/dev/input/event0”,RDWR),最终就会调用到 evdev_open 函数。在分析 evdev_open 函数前,我们来看两个重要的结构。

/* 参考 drivers/input/evdev.c */
struct evdev {
/* 打开标志 */
int open;
/* 通过 handle 可以找到输入设备和事件处理器 */
struct input_handle handle;
/* 等待队列,当进程读取设备,而没有事件
* 产生的时候,进程将在队列里休眠
*/
wait_queue_head_t wait;
/* 强制绑定的 evdev_client 结构 */
struct evdev_client __rcu *grab;
/* evdev_client 链表,这说明一个 evdev 设备可以处理
* 多个 evdev_client,可以有多个进程访问 evdev 设备
*/
struct list_head client_list;
spinlock_t client_lock; /* protects client_list */
struct mutex mutex;
struct device dev;
/* 字符设备结构 */
struct cdev cdev;
bool exist;
};


/* 参考 drivers/input/evdev.c */
struct evdev_client {
/* input_event 代表一个事件,基本成员:
* 类型(type)
,编码(code)
,值(value)
*/
struct input_event buffer[];
/* 针对 buffer 数组的索引 */
unsigned int head;
/* 针对 buffer 数组的索引,当 head 与 tail 相等的时候,说明没有事件 */
unsigned int tail;
unsigned int packet_head;
spinlock_t buffer_lock;
/* 异步通知函数 */
struct fasync_struct *fasync;
/* evdev 设备 */
struct evdev *evdev;
/* evdev_client 链表项 */
struct list_head node;
int clkid;
unsigned int bufsize;
};


evdev_client 这个结构在进程打开 event0 设备的时候调用 evdev 的 open方法,在 open 中创建这个结构,并初始化。

static int evdev_open(struct inode *inode, struct file *file)
{
struct evdev *evdev = container_of(inode->i_cdev,struct evdev, cdev);
unsigned int bufsize =evdev_compute_buffer_size(evdev->handle.dev);
struct evdev_client *client;
int error;
/* 分配并初始化 evdev_client 结构 */
client = kzalloc(sizeof(struct evdev_client) +bufsize * sizeof(struct input_event),GFP_KERNEL);
...
client->bufsize = bufsize;
spin_lock_init(&client->buffer_lock);
/* 使 client 与 evdev 设备相关联 */
client->evdev = evdev;
/* 把 client 挂到 evdev->client_list */
evdev_attach_client(evdev, client);
/* 当设备是第一次被打开时,则调用 input_open_device 函数 */
error = evdev_open_device(evdev);
...
/* 把 evdev_client 放到文件的私有数据,
* 以备其他函数取出,如 evdev_read
*/
file->private_data = client;
...
return 0;
}

7.3.3  evdev_open_device

/* 参考 drivers/input/evdev.c */
static int evdev_open_device(struct evdev *evdev)
{
int retval;
/* 获得互斥锁 */
retval = mutex_lock_interruptible(&evdev->mutex);
if (retval)
return retval;
/* 检查设备是否存在 */
if (!evdev->exist)
retval = -ENODEV;
/* 判断设备是否已经被打开?如果没有打开,
* 则调用 input_open_device(&evdev->handle)
* 打开 evdev 对应的 handle;否则不做任何操作返回
*/
else if (!evdev->open++) {
retval = input_open_device(&evdev->handle);
if (retval)
evdev->open--;
}
mutex_unlock(&evdev->mutex);
return retval;
}

7.3.4  input_open_device

/* 参考 drivers/input/input.c
*
* This function should be called by input handlers when they
* want to start receive events from given input device.
*/
int input_open_device(struct input_handle *handle)
{
/* 从 handle 中获得 input_dev 结构体 */
struct input_dev *dev = handle->dev;
/* 递增 handle 的打开计数 */
handle->open++;
/* 如果是第一次打开,并且 dev 存在 open 函数,
* 则调用 input_dev 的 open 函数
*/
if (!dev->users++ && dev->open)
retval = dev->open(dev);
...
}

7.3.5  evdev_read
当用户程序 read 时,最终会调用到 evdev_read。

static ssize_t evdev_read(struct file *file, char __user *buffer,size_t count, loff_t *ppos)
{
/* 这个客户端结构在打开的时候分配并保存在 file->private_data 中 */
struct evdev_client *client = file->private_data;
struct evdev *evdev = client->evdev;
struct input_event event;
size_t read = 0;
int error;
/* 用户进程每次读取设备的字节数,不要少于 input_event 结构的大小 */
if (count != 0 && count < input_event_size())
return -EINVAL;
for (;;) {
if (!evdev->exist)
return -ENODEV;
/* 如果缓存中没有数据可读而设备又存在,
* 并且被设置为 O_NONBLOCK 方式,则退出
*/
if (client->packet_head == client->tail &&(file->f_flags & O_NONBLOCK))
return -EAGAIN;
/*
* count == 0 is special - no IO is done but we check
* for error conditions (see above).
*/
if (count == 0)
break;
while (read + input_event_size() <= count &&evdev_fetch_next_event(client, &event)) {
/* 最终使用 copy_to_user 函数读取数据 */
if (input_event_to_user(buffer + read, &event))
return -EFAULT;
read += input_event_size();
}
if (read)
break;
/* 如果设备没有被设为 O_NONBLOCK,并且缓存中没有数据可读
* 而设备又存在,就调用 wait_event_interruptible 休眠
*/
if (!(file->f_flags & O_NONBLOCK)) {
error = wait_event_interruptible(evdev->wait,client->packet_head != client->tail ||!evdev->exist);
if (error)
return error;
}
}
return read;
}
evdev_fops 的其他函数成员也是这么分析,这里就不继续分析下去了。有兴趣的读者,请自行阅读更多的源码。

7.4  硬件驱动层分析
Linux 系统自带了很多类型的输入设备驱动,打开 drivers/input/目录,确实可以看到有很多类型的输入设备,比如:keyboard、mouse、joystick、touchscreen等等。进入这些目录后,发现还有很多相应类型下的具体设备,比如 keyboard目录下有 atkbds.c、 mcs_touchkey.c、gpio_keys.c 文件,这些文件都是某一种输入设备类型下的具体的输入设备。这里,我们以 gpio_keys.c 文件来分析,它是 Linux 系统自带的通用的,基于 GPIO 的按键驱动程序。先看它的入口函数(参考 drivers/input/keyboard/gpio_keys.c):

static int __init gpio_keys_init(void)
{
/* 注册一个平台驱动 */
return platform_driver_register(&gpio_keys_device_driver);
}

在 gpio_keys_init 入口函数里,注册了一个平台驱动,关于什么是平台驱动,将在第八章学习,这里可以暂且跳过,暂且不需要理会它。这个平台驱动注册函数需要一个 platform_driver 型的参数,它是一个结构体。 在这里,注册 的platform_driver 是这样的 driver:

static struct platform_driver gpio_keys_device_driver = {
.probe= gpio_keys_probe,
.remove= gpio_keys_remove,
.driver={
.name = "gpio-keys",
.owner = THIS_MODULE,
.pm= &gpio_keys_pm_ops,
.of_match_table = of_match_ptr(gpio_keys_of_match),
}
};


如 果内核中 存在有 与 gpio-keys 同 名的平台设备的话,最终 就会调用gpio_keys_probe 函数。这里你暂且记住这句话,以后你将会看到大量类似的调用,因为 Linux 内核大量的使用了总线设备驱动模型,这里暂且先说一下,后面章节再详细分析。

7.4.1  gpio_keys_probe

/* 参考 drivers/input/keyboard/gpio_keys.c */
static int gpio_keys_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
/* 设备相关的信息会保存在 pdev->dev.platform_data 中 */
const struct gpio_keys_platform_data *pdata = dev_get_platdata(dev);
struct gpio_keys_drvdata *ddata;
struct input_dev *input;
int i, error;
int wakeup = 0;
...
/* 为驱动数据 ddata 开辟一段内存空间,并清零 */
ddata = kzalloc(sizeof(struct gpio_keys_drvdata) +pdata->nbuttons * sizeof(struct gpio_button_data),GFP_KERNEL);
/* 分配设置 input_dev 结构体 */
input = input_allocate_device();
...
/*把 ddata 保存到 pdev->dev->p->driver_data,供其他函数中使用
*(比如:remove),通过 platform_get_drvdata 函数来获取。
*/
platform_set_drvdata(pdev, ddata);
/* 把驱动数据 ddata 以私有数据形式存放在
* 输入设备 input 中,以备日后使用
*/
input_set_drvdata(input, ddata);
/* 初始化 input_dev 结构体的部分成员,加载驱动后可以
* 通过 cat /proc/bus/input/devices 命令查看
*/
input->name = pdata->name ? : pdev->name;
input->phys = "gpio-keys/input0";
input->dev.parent = &pdev->dev;
input->open = gpio_keys_open;
input->close = gpio_keys_close;
input->id.bustype = BUS_HOST;
input->id.vendor = 0x0001;
input->id.product = 0x0001;
input->id.version = 0x0100;
...
/* 为每个按键进行初始化,及设置属性 */
for (i = 0; i < pdata->nbuttons; i++) {
const struct gpio_keys_button *button = &pdata->buttons[i];
struct gpio_button_data *bdata = &ddata->data[i];
/* 初始化按键,后面分析 */
error = gpio_keys_setup_key(pdev, input, bdata, button);
}
...
/* 注册输入设备 input_dev,把输入设备加到输入设备链表中,
* 并寻找合适的 input_handle 中的 handler 与 input_handler 配对
*/
error = input_register_device(input);
return 0;
...
}


gpio_keys_probe 函数的代码量将近 100 多行,为了便于分析输入子系统中硬件驱动层主要工作是什么,我们只抓重点分析。详细的请看源码,源码里也有详细的注释。gpio_keys_probe 函数的主要工作是:
(1) 分配设置一个 input_dev 结构体,这个结构体前面说过了,它代表一个输入设备。如果你已经忘了,请回头看 7.1.2.1 小节的内容。
(2) 获取平台数据
(3) 初始化部分非必要的 input_dev 结构体成员,剩下重要的 input_dev 结构体成员在后面设置。
(4) 初始化按键
(5) 向内核注册一个 input_dev

7.4.2  gpio_keys_setup_key

/* 参考 drivers/input/keyboard/gpio_keys.c */
static int gpio_keys_setup_key(struct platform_device *pdev,struct input_dev *input,struct gpio_button_data *bdata,const struct gpio_keys_button *button)
{
const char *desc = button->desc ? button->desc : "gpio_keys";
struct device *dev = &pdev->dev;
irq_handler_t isr;
unsigned long irqflags;
int irq, error;
....
/* 判断按键所对应的 gpio 是否有效 */
if (gpio_is_valid(button->gpio)) {
/* 向系统申请 gpio */
error = gpio_request_one(button->gpio, GPIOF_IN, desc);
...
/* 提取 gpio 所对应的外部中断 */
irq = gpio_to_irq(button->gpio);
...
bdata->irq = irq;
/* 初始化工作队列,在 bdata->work 的工作队列中
* 增加一个任务 gpio_keys_gpio_work_func
*/
INIT_WORK(&bdata->work, gpio_keys_gpio_work_func);
/* 设置定时器,当定时器的时间到了,就执行 gpio_keys_gpio_timer
* 回调函数,回调函数调度工作队列 schedule_work(&bdata->work),
* 最终调用 gpio_keys_gpio_work_func
*/
setup_timer(&bdata->timer,gpio_keys_gpio_timer, (unsigned long)bdata);
/* 设置按键中断处理函数为 gpio_keys_gpio_isr */
isr = gpio_keys_gpio_isr;
/* 设置外部中断上升沿或下降沿触发 */
irqflags = IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING;
}
...
/* 设置这个输入设备支持按键类(EV_KEY)事件 */
input_set_capability(input, button->type ?: EV_KEY, button->code);
...
/* 分配一条中断线,即当按键中断(bdata->irq)发生时,
* 进入中断函数(isr,即 gpio_keys_irq_isr 函数内)
*/
error = request_any_context_irq(bdata->irq, isr, irqflags, desc, bdata);
...
return 0;
...
}

gpio_keys_setup_key 这个函数的工作内容就是完成一系列的硬件初始化工作,主要有:
(1) 判断 GPIO 引脚是否有效,有效的话获取 GPIO 对应的外部中断号。
(2) 初 始 化 工 作 队 列 , 在 bdata->work 的 工 作 队 列 中 增 加 一 个 任 务gpio_keys_gpio_work_func。
(3) 定义设置一个定时器,定时器中断处理函数为 gpio_keys_gpio_timer
(4) 定义按键中断处理函数:gpio_keys_gpio_isr
(5) 设置 input_dev,让它支持按键类事件。
(6) 分配一条中断线,即当按键中断(bdata->irq)发生时,进入中断函数(isr,即 gpio_keys_irq_isr 函数内)

7.4.3  gpio_keys_gpio_isr

当有按键事件发生时,就会进入 gpio_keys_gpio_isr 按键中断处理函数

/* 参考 drivers/input/keyboard/gpio_keys.c */
static irqreturn_t gpio_keys_gpio_isr(int irq, void *dev_id)
{
struct gpio_button_data *bdata = dev_id;
...
/* 如果有设置按键去抖动,则修改定时器时间 */
if (bdata->timer_debounce)
mod_timer(&bdata->timer,jiffies + msecs_to_jiffies(bdata->timer_debounce));
/* 否则执行调度队列 */
else
schedule_work(&bdata->work);
return IRQ_HANDLED;
}

在 后 面 的 移 植 中 , 并 没 有 设 置 去 抖 动 标 志 , 也 就 是 没 有 设 置 struct gpio_button_data *bdata 结 构 体 中 的 timer_debounce , 所 以 不 会 调 用mod_timer 修改定时器时间。暂且认为,这里就调用 schedule_work 函数,它的作用是将 gpio_keys_gpio_work_func 挂在工作队列上,利用它来上报事件。

7.4.4  gpio_keys_gpio_timer

假设定时时间到达,就会调用 gpio_keys_gpio_timer 函数。

/* 参考 drivers/input/keyboard/gpio_keys.c */
static void gpio_keys_gpio_timer(unsigned long _data)
{
struct gpio_button_data *bdata = (struct gpio_button_data *)_data;
schedule_work(&bdata->work);
}

它也是调用 schedule_work 函数,它的作用是将 gpio_keys_gpio_work_func挂在工作队列上,利用它来上报事件。也就说,当定时时间到了,它就会调用gpio_keys_gpio_work_func 函数进行上报事件。跟 gpio_keys_gpio_isr 按键中断处理函数的作用是一样的。显然,这里是多余的。因为我们在移植按键平台设备时,没有设置去抖动标志,也就相当于定时器是无效的。如果你想实现定时器去抖动的话,就需要设置 bdata->timer_debounce。

7.4.5  gpio_keys_gpio_work_func

当有按键事件发生时,就会触发按键中断,从而进入 gpio_keys_gpio_isr 函数,由于没有设置 bdata->timer_debounce 去抖动标志,所以就直接调用了
schedule_work(&bdata->work)。又因为前面 gpio_keys_setup_key 函数里初始化了一个 bdata->work 工作队列:

INIT_WORK(&bdata->work, gpio_keys_gpio_work_func);
并 将 gpio_keys_gpio_work_func 放 入 了 工 作 队 列 , 所 以 当 有 人 调 用schedule_work(&bdata->work)时,就相当于调用 gpio_keys_gpio_work_func函数。

/* 参考 drivers/input/keyboard/gpio_keys.c */
static void gpio_keys_gpio_work_func(struct work_struct *work)
{
/* container_of 实现了根据一个结构体变量中的一个域成员
* 变量的指针来获取指向整个结构体变量的指针的功能。
*/
struct gpio_button_data *bdata =container_of(work, struct gpio_button_data, work);
/* 向系统上报事件 */
gpio_keys_gpio_report_event(bdata);
if (bdata->button->wakeup)
pm_relax(bdata->input->dev.parent);
}

7.4.6  gpio_keys_gpio_report_event

static void gpio_keys_gpio_report_event(struct gpio_button_data *bdata)
{
const struct gpio_keys_button *button = bdata->button;
struct input_dev *input = bdata->input;
unsigned int type = button->type ?: EV_KEY;
if()
...
/* 把按键类输入事件上报给系统 */
else {
input_event(input, type, button->code, !!state);
}
/* 用于事件的同步,即告知系统设备已传递完一组完整的数据 */
input_sync(input);
}

gpio_keys_gpio_report_event 函数使用输入子系统核心提供的接口函数input_event 来上报事件,input_sync 其实也是调用 input_event 来实现的。

7.4.7  事件传递过程分析

以 gpio_keys 为例子,它是 Linux 系统自带的通用的,基于 GPIO 的按键驱动程序。evdev 处理器支持这个输入设备,结合 evdev 来大概梳理一下事件的传递过程。

当用户程序打开/dev/event0 后,接着 read 这个输入设备,最终就会调用到事件处理层的 evdev_read 函数,在 7.3.5 小节已经将过。如果没有数据可读进程就会休眠,什么时候被唤醒呢?有事件发生的时候才会唤醒进程。说到事件,那自然是硬件设备产生的。
当按键被按下或松开时,就会进入到硬件设备驱动层,以 gpio_key 为例,它 就 会 进 入 到 gpio_keys_gpio_isr 按 键 中 断 处 理 函 数 , 它 最 终 调 用gpio_keys_gpio_work_func 函数,进而使用 gpio_keys_gpio_report_event 来上报 事 件 , 而 gpio_keys_gpio_report_event 利 用 了 输 入 子 系 统 核 心 提 供 的input_event 函数来上报事件。好啦,接着我们用函数流程来看看 input_event是怎么上报事件的。

input_event
   -->input_handle_event
        -->input_pass_values
            -->input_to_handler
                 -->handler->events(handle, vals, count);或者
                 -->handler->event(handle, v->type, v->code, v->value);

看到最后一个,是事件处理器 handler 里的 events 函数。对于 gpio_keys这个输入设备对应的事件处理器是 evdev。换句话说,input_event 函数最终调用到 evdev.c 里的 evdev_events 函数来上报事件。

7.4.8evdev_events

/* 参考 drivers/input/keyboard/gpio_keys.c
* Pass incoming events to all connected clients.
*/
static void evdev_events(struct input_handle *handle,const struct input_value *vals, unsigned int count)
{
struct evdev *evdev = handle->private;
struct evdev_client *client;
...
/* 在 evdev_pass_values 函数里把在 evdev_read 时休眠的进程唤醒 */
if (client)
evdev_pass_values(client, vals, count, time_mono, time_real);
else
list_for_each_entry_rcu(client, &evdev->client_list, node)
evdev_pass_values(client, vals, count,
time_mono, time_real);
...
}

7.4.9  evdev_pass_values

/* 参考 drivers/input/keyboard/gpio_keys.c */
static void evdev_pass_values(struct evdev_client *client,const struct input_value *vals, unsigned int count,ktime_t mono, ktime_t real)
{
struct evdev *evdev = client->evdev;
const struct input_value *v;
struct input_event event;
...
/* 把在 evdev_read 时休眠的进程唤醒 */
wake_up_interruptible(&evdev->wait);
}

唤醒进程后,evdev_read 函数把事件(数据)通过 input_event_to_user 函数传递给用户程序。而 input_event_to_user 正是调用了 copy_to_user 将事件(数据)从内核空间拷贝到用户空间。这样,用户程序就读取到了按键所产生的动作。

7.5  移植 gpio_key

前面说了那么多知识,把 Linux 内核的 Input 子系统说的那么牛叉,那是不是不用怎么移植就能够用上了呢?对,没错。linux 系统提供了很好的支持,只要把按键对应的 IO 端口配置好,按键就可以工作了。

7.5.1添加按键平台设备

如果你学习过本教程的内核移植篇,那么你对下面这个文件一定不会陌生!

arch/arm/mach-s5pv210/mach-smdkv210.c

在 smdkv210_devices 结构体指针数组里添加最后一项(红色那项)

static struct platform_device *smdkv210_devices[] __initdata = {
&s3c_device_nand,
&s3c_device_adc,
&s3c_device_cfcon,
&s3c_device_fb,
&s3c_device_hsmmc0,
&s3c_device_hsmmc1,
&s3c_device_hsmmc2,
&s3c_device_hsmmc3,
&s3c_device_i2c0,
&s3c_device_i2c1,
&s3c_device_i2c2,
&s3c_device_rtc,
&s3c_device_ts,
&s3c_device_usb_hsotg,
&s3c_device_wdt,
&s5p_device_fimc0,
&s5p_device_fimc1,
&s5p_device_fimc2,
&s5p_device_fimc_md,
&s5p_device_jpeg,
&s5p_device_mfc,
&s5p_device_mfc_l,
&s5p_device_mfc_r,
&s5pv210_device_ac97,
&s5pv210_device_iis0,
&s5pv210_device_spdif,
&samsung_asoc_idma,
&samsung_device_keypad,
&smdkv210_dm9000,
&smdkv210_lcd_lte480wv,
&webee210_button_device,
/* Add by Webee
*/
};

然后在定义 smdkv210_devices 结构体指针数组的前面,定义下面这些代码。
/***********************Add by Webee********************************/
#include <linux/gpio_keys.h> /* Add by Webee */
static struct gpio_keys_button webee210_buttons[] = {
{
.gpio= S5PV210_GPH2(0),
/* S1 */
.code= KEY_A,
.desc= "Button 1",
.active_low = 1,
},
{
.gpio= S5PV210_GPH2(1),
/* S2 */
.code= KEY_B,
.desc= "Button 2",
.active_low = 1,
},
{
.gpio = S5PV210_GPH2(2), /* S3 */
.code = KEY_C,
.desc = "Button 3",
.active_low = 1,

},

{

.gpio = S5PV210_GPH2(3), /* S4 */
.code = KEY_L,
.desc = "Button 4",
.active_low = 1,

},

{

.gpio = S5PV210_GPH3(0), /* S5 */
.code = KEY_S,
.desc = "Button 5",
.active_low = 1,

},

{

.gpio = S5PV210_GPH3(1), /* S6 */
.code = KEY_ENTER,
.desc = "Button 6",
.active_low = 1,

},

{

.gpio = S5PV210_GPH3(2), /* S7 */
.code = KEY_LEFTSHIFT,
.desc = "Button 7",
.active_low = 1,

},

{

.gpio = S5PV210_GPH3(3), /* S8 */
.code = KEY_DELETE,
.desc = "Button 8",
.active_low = 1,
},
};

static struct gpio_keys_platform_data webee210_button_data = {
.buttons= webee210_buttons,
.nbuttons = ARRAY_SIZE(webee210_buttons),
};

/* 设备相关的信息会保存在 pdev->dev.platform_data 中 */
static struct platform_device webee210_button_device = {
.name= "gpio-keys",
.id= -1,
.dev={
.platform_data = &webee210_button_data,
}
};
/***********************Add by Webee********************************/

把上面的内容添加好后,通过 make meuconfig,将 gpio_keys.c 配置上,就可以使用这个 GPIO 按键了。

7.5.2  配置支持 gpio_key

要想系统支持这个 gpio_key 硬件驱动,那自然要去编译这个 gpio_key.c 文件,这里通过 make menuconfig 命令,将 gpio_key.c 编译进内核,这样就不用我们手工加载驱动了。
进入 make menuconfig 界面后,搜索 KEYBOARD_GPIO,就可以发现配置gpio_key 的驱动路径了,如图 7.2。至于如何搜索,这是一个非常基础的问题,这里就不说了,不会的话,请自行百度吧。

也就是按下下面的配置要求,将配置项配置上。
-> Device Drivers
    -> Input device support
         -> Generic input layer (needed for keyboard, mouse, ...)   

[*]   Keyboards --->
         <*>GPIO Buttons
配置好后,保存退出 make menuconfig 配置界面,然后使用下面的命令编译内核。
#make uImage
编译好后,uImage 的存放路径是 arch/arm/boot/uImage,进行测试时,就需要将新内核烧到 NAND FLASH 里去。(使用网蜂自制 u-boot 菜单栏,选择 2烧写)。

7.5.3  测试程序

在分析测试程序前,我们先来看一个内核的结构体,Linux 输入子系统里,将事件抽象成 input_event 结构体,它代表一个事件,比如按键按下、或者按键松开,这些都是一个事件。

struct input_event {
struct timeval time;/* 时间 */
__u16 type;/* 事件类型 */
__u16 code;/* 事件类下的值 */
__s32 value;/* 0-松开, 1-按下,2-重复 */
};

struct timeval {
__kernel_time_t
tv_sec;/* 秒 */
__kernel_suseconds_t tv_usec;/* 微秒 */
};

gpio_key.c 按 键 驱 动 测 试 程 序 源 码 在 : webee210_drivers\6th_buttons_input\ 1th_Transplant_ok\ gpio_key_test.c

/* 某些头文件 */
int main(int argc,char *argv[])
{
int fd;
struct input_event ev_key;
if (argc != 2)
{
printf("Usage:\n");
printf("%s /dev/event0 or /dev/event1\n",argv[0]);
return 0;
}
fd= open(argv[1], O_RDWR);
if(fd < 0)
{
perror("open device buttons");
exit(1);
}
while(1)
{
read(fd,&ev_key,sizeof(struct input_event));
printf("type:%d,code:%d,value:%d\n",ev_key.type,
ev_key.code,ev_key.value);
}
close(fd);
return 0;
}

编译+拷贝到开发板。
[root@localhost 1th_Transplant_ok]# arm-linux-gcc -o  gpio_key_test gpio_key_test.c
[root@localhost 1th_Transplant_ok]# cp gpio_key_test  /home/webee210v2/rootfs/webee210_driver_and_test/

测试程序首先打开/dev/event0 或者/dev/event1 设备文件,这个设备文件正是 evdev_connect 函数创建的。/dev/event0 设备节点的创建,说明了 evdev 事件处理器支持 gpio_key 输入设备。为什么是/dev/evnet0 或/dev/event1 呢?那event1 又是指什么设备呢?
如果使用移植 gpio_key 后的内核启动的话,那么 event0 指的的是 gpio_key设备,而 event1 指的是触摸屏设备,关于触摸屏的内容,后面再详细介绍,这里暂且告诉大家,触摸屏也使用了输入子系统的框架。但是,如果使用后面 7.6节,自己从零编写按键设备驱动,手动加载驱动后,event0 代表触摸屏设备,event1 代表按键设备。很简单,这是时间先后的问题。

打开设备后,测试程序就一直在读/dev/event0 设备,最终就会调用到事件处理层的 evdev_read 函数,如果没有数据可读进程就会休眠,什么时候被唤醒呢?有事件发生的时候才会唤醒进程。说到事件,那自然是硬件设备产生的。

当按键被按下或松开时,就会进入到硬件设备驱动层,以 gpio_key 为例,它 就 会 进 入 到 gpio_keys_gpio_isr 按 键 中 断 处 理 函 数 , 它 最 终 调 用gpio_keys_gpio_work_func 函数,进而使用 gpio_keys_gpio_report_event 来上报 事 件 , 而 gpio_keys_gpio_report_event 利 用 了 输 入 子 系 统 核 心 提 供 的input_event 函数来上报事件。 input_event 函数最终调用到 evdev.c 里 的evdev_events 函 数 来 上 报 事 件 。 它 又 调 用 evdev_pass_values 函 数 ,而evdev_pass_values 函数的工作就是唤醒在 evdev_read 时休眠的进程,唤醒进程后,evdev_read 函数把事件(数据)通过 input_event_to_user 函数传递给用户程序。而 input_event_to_user 正是调用了 copy_to_user 将事件(数据)从内核空间拷贝到用户空间。这样,用户程序就读取到了按键所产生的动作。

7.5.4  测试结果

在使用新的内核前,先看看还未移植 gpio_key 时,这些操作的结果是怎样的,对比一下,移植 gpio_key 后,使用新内核启动后,结果有什么区别。

第一个红色方框打印出的内容,其实就类似于在 gpio_keys_probe 函数里,设置 input_dev 结构里的成员。只不过,这里在未移植 gpio_key 前,这里对应的设备是 virtual 设备。
第二个红色方框打印出的内容,在未移植 gpio_key 前,它代表触摸屏设备,主设备号为 13,次设备号为 64。但如果,使用移植 gpio_key 后,使用新内核启动后,/dev/event0 则代表按键设备了,/dev/event1 代表触摸屏设备。这里最后强调一下,后面不再提示。并且,值得注意的是,做本章实验,不要在启动有 QT 界面的环境下做,因为 QT 环境里设置了触摸屏,设置了/dev/event0,这里会有冲突。


7.6  从零编写输入子系统下的设备驱动

怎么写符合输入子系统框架的驱动程序?

1. 分配一个 input_dev 结构体
2. 设置
     2.1 设置按键能产生哪类事件
     2.2 设置能产生这类操作的哪些事件
3. 注册一个输入设备
4. 硬件相关的代码,比如在中断服务程序里上报事件
    4.1 定时器相关的操作
    4.2 申请中断
从零编写输 入子系 统下的设 备驱动 ,源码路 径在: webee210_drivers\6th_buttons_input\ 2th_buttons_input\ buttons_input.c
考虑到,本章的内容实在是太多了,担心部分基础比较薄弱的童鞋消化不了,这里就不分析 buttons_input.c 文件了,大家打开源码,自己根据上面的 4 点要求来分析,webee 已经在源码里标有详细的注释。

7.6.1 测试程序
测试程序与 7.5.3 小节的测试程序是一模一样的,这里也不重复分析了。这里再增加一种测试方法,加载 buttons_input.ko 后,输入下面命令可以在LCD 上,看见你输入的按键对应的字母,比如你按下 S1 对应 a,按下 S2 对应b 等等,一旦你按下 S6,对应的字母也显示在 SecureCRT 终端上。
[Webee210]\# cat /dev/tty1

7.6.2 测试结果


7.7  本章小结

学习到这里,是不是有种感叹,觉得第 7 章内容与前面 6 章内容都复杂。如果你有这种感觉,那就对了。如果你看了本章内容还是一头雾水的话, Webee建议大家多看几遍本章内容。对于初学者的话,Webee 觉得没有三遍,我都觉得你应该还没学透彻。并且 Webee 建议大家结合教程,一遍看教程的总体总结,一般打开 Source Insight 工程看源码。好了,我们来总结一下本章学习了什么?
首先 Webee 给大家灌输的三层架构,输入(Input)子系统是分层架构的,总共分为 3 层,从上到下分别是:事件处理层(Event Handler)、输入子系统核心层(Input Core)、硬件驱动层(Input Driver)。上厕所你都应该记得清楚,输入子系统有三层,并且这三层指的是哪三层。
最底那层是硬件驱动层,它主要是利用子系统核心层提供的接口,分配、设置一个输入设备结构(input_dev),然后注册它。子系统核心层主要是提供三个注册函数,对上提供事件处理层接口,对下提供硬件驱动层接口。最上面那层是事件处理层,它主要是将硬件驱动底层产生的事件通过它上报给用户程序。最后,还移植分析了 gpio_key 驱动,并提供了从零编写输入子系统的设备驱动。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值