LINUX 输入子系统分析

1 输入子系统架构Overview

        输入子系统(Input Subsystem)的架构如下图所示

 

 

        输入子系统由 输入子系统核心层( Input Core),驱动层和事件处理层(Event Handler)三部份组成。一个输入事件,如鼠标移动,键盘按键按下,joystick的移动等等通过Driver -> InputCore -> Eventhandler -> userspace的顺序到达用户空间传给应用程序。

        其中Input Core 即 Input Layer 由 driver/input/input.c及相关头文件实现。对下提供了设备驱动的接口,对上提供了Event Handler层的编程接口。

1.1 主要数据结构

表 1     Input Subsystem main data structure

 

 

数据结构
 用途
 定义位置
 具体数据结构的分配和初始化
 
Struct input_dev
 驱动层物理Input设备的基本数据结构
 Input.h
 通常在具体的设备驱动中分配和填充具体的设备结构
 
Struct Evdev

Struct Mousedev

Struct Keybdev…
 Event Handler层逻辑Input设备的数据结构
 Evdev.c

Mousedev.c

Keybdev.c
 Evdev.c/Mouedev.c …中分配

 
 
Struct Input_handler
 Event Handler的结构
 Input.h
 Event Handler层,定义一个具体的Event Handler。
 
Struct Input_handle
 用来创建驱动层Dev和Handler链表的链表项结构
 Input.h
 Event Handler层中分配,包含在Evdev/Mousedev…中。
 

 

1.2 输入子系统架构示例图

 

图2  输入子系统架构示例图

2 输入链路的创建过程

        由于input子系统通过分层将一个输入设备的输入过程分隔为独立的两部份:驱动到Input Core,Input Core到Event Handler。所以整个链路的这两部分的接口的创建是独立的。

2.1 硬件设备的注册

        驱动层负责和底层的硬件设备打交道,将底层硬件对用户输入的响应转换为标准的输入事件以后再向上发送给Input Core。

        驱动层通过调用Input_register_device函数和Input_unregister_device函数来向输入子系统中注册和注销输入设备。

这两个函数调用的参数是一个Input_dev结构,这个结构在driver/input/input.h中定义。驱动层在调用Input_register_device之前需要填充该结构中的部分字段

#include <linux/input.h>

#include <linux/module.h>

#include <linux/init.h>

MODULE_LICENSE("GPL");

struct input_dev ex1_dev;

static int __init ex1_init(void)

{

    /* extra safe initialization */

    memset(&ex1_dev, 0, sizeof(struct input_dev));

    init_input_dev(&ex1_dev);

    /* set up descriptive labels */

    ex1_dev.name = "Example 1 device";

    /* phys is unique on a running system */

    ex1_dev.phys = "A/Fake/Path";

    ex1_dev.id.bustype = BUS_HOST;

    ex1_dev.id.vendor = 0x0001;

    ex1_dev.id.product = 0x0001;

    ex1_dev.id.version = 0x0100;

   

    /* this device has two keys (A and B) */

    set_bit(EV_KEY, ex1_dev.evbit);

    set_bit(KEY_B, ex1_dev.keybit);

    set_bit(KEY_A, ex1_dev.keybit);

   

    /* and finally register with the input core */

    input_register_device(&ex1_dev);

   

    return 0;

}

        其中比较重要的是evbit字段用来定义该输入设备可以支持的(产生和响应)的事件的类型。

包括:

Ø EV_RST   0x00  Reset

Ø EV_KEY   0x01  按键

Ø EV_REL   0x02  相对坐标

Ø EV_ABS   0x03  绝对坐标

Ø EV_MSC   0x04  其它

Ø EV_LED   0x11   LED

Ø EV_SND   0x12  声音

Ø EV_REP   0x14  Repeat

Ø EV_FF   0x15  力反馈

一个设备可以支持一个或多个事件类型。每个事件类型下面还需要设置具体的触发事件,比如EV_KEY事件,支持哪些按键等。

2.2 Event Handler层

2.2.1 注册Input Handler

       驱动层只是把输入设备注册到输入子系统中,在驱动层的代码中本身并不创建设备结点。应用程序用来与设备打交道的设备结点的创建由EventHandler层调用Input core中的函数来实现。而在创建具体的设备节点之前,EventHandler层需要先注册一类设备的输入事件处理函数及相关接口

        以MouseDev Handler为例:

static struct input_handler mousedev_handler = {

 event:  mousedev_event,

 connect:  mousedev_connect,

 disconnect: mousedev_disconnect,

 fops:  &mousedev_fops,

 minor:  MOUSEDEV_MINOR_BASE,

};

static int __init mousedev_init(void)

{

 input_register_handler(&mousedev_handler);

 memset(&mousedev_mix, 0, sizeof(struct mousedev));z

 init_waitqueue_head(&mousedev_mix.wait);

 mousedev_table[MOUSEDEV_MIX] = &mousedev_mix;

 mousedev_mix.exist = 1;

 mousedev_mix.minor = MOUSEDEV_MIX;

 mousedev_mix.devfs = input_register_minor("mice", MOUSEDEV_MIX, MOUSEDEV_MINOR_BASE);

 printk(KERN_INFO "mice: PS/2 mouse device common for all mice/n");

 return 0;

}

        在Mousedev_init中调用input.c中定义的input_register_handler来注册一个鼠标类型的Handler. 这里的Handler不是具体的用户可以操作的设备,而是鼠标类设备的统一的处理函数接口。

2.2.2 设备节点的创建

       接下来,mousedev_init函数调用input_register_minor注册一个通用mice设备,这才是与用户相关联的具体的设备接口。然而这里在init函数中创建一个通用的Mice设备只是鼠标类EventHandler层的特例。在其它类型的EventHandler层中,并不一定会创建一个通用的设备。

       标准的流程见是硬件驱动向Input子系统注册一个硬件设备后,在input_register_device中调用已经注册的所有类型的InputHandler的connect函数,每一个具体的Connect函数会根据注册设备所支持的事件类型判断是否与自己相关,如果相关就调用input_register_minor创建一个具体的设备节点。

void input_register_device(struct input_dev *dev)

{

 ……

 while (handler) {

  if ((handle = handler->connect(handler, dev)))

   input_link_handle(handle);

  handler = handler->next;

 }

}

        此外如果已经注册了一些硬件设备,此后再注册一类新的Input Handler,则同样会对所有已注册的Device调用新的Input Handler的Connect函数已确定是否需要创建新的设备节点:

void input_register_handler(struct input_handler *handler)

{

……

 while (dev) {

  if ((handle = handler->connect(handler, dev)))

   input_link_handle(handle);

  dev = dev->next;

 }

}

        从上面的分析中可以看到一类Input Handler可以和多个硬件设备相关联,创建多个设备节点。而一个设备也可能与多个Input Handler相关联,创建多个设备节点。

        直观起见,物理设备,Input Handler,逻辑设备之间的多对多关系可见下图:

 

图3  物理设备,Input Handler,逻辑设备关系图

3 设备的打开和读写

    用户程序通过Input Handler层创建的设备节点的Open,read,write等函数打开和读写输入设备。

3.1 Open

    设备节点的Open函数,首先会调用一类具体的InputHandler的Open函数,处理一些和该类型设备相关的通用事务,比如初始化事件缓冲区等。然后通过Input.c中的input_open_device函数调用驱动层中具体硬件设备的Open函数。

3.2 Read

    大多数Input Handler的Read函数等待在EventLayer层逻辑设备的wait队列上。当设备驱动程序通过调用Input_event函数将输入以事件的形式通知给输入子系统的时候,相关的InputHandler的event函数被调用,该event函数填充事件缓冲区后将等待队列唤醒。

    在驱动层中,读取设备输入的一种可能的实现机制是扫描输入的函数睡眠在驱动设备的等待队列上,在设备驱动的中断函数中唤醒等待队列,而后扫描输入函数将设备输入包装成事件的形式通知给输入子系统。

3.3 Write

    2.4内核中没有固定的模式,根据具体的Input Handler,可能不实现,也可能通过调用Input_event将写入的数据以事件的形式再次通知给输入子系统,或者调用设备驱动的Write函数等等。

    2.6内核的代码中,通过调用Input_event将写入的数据以事件的形式再次通知给输入子系统,而后在Input.c中根据事件的类型,将需要反馈给物理设备的事件通过调用物理设备的Event函数传给设备驱动处理,如EV_LED事件:

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

{

 ......

 case EV_LED:

         if (code > LED_MAX || !test_bit(code, dev->ledbit) || !!test_bit(code, dev->led) == value)

            return;

         change_bit(code, dev->led);

         if (dev->event) dev->event(dev, type, code, value);

         break;

 ......

 }

4 其它

    本文中对Input子系统架构的分析主要是基于2.4.20内核,在2.6内核中对Input子系统做了很大的扩充增加了对许多设备的支持(如触摸屏,键盘等)。不过整体的框架还是一致的。

    另,参考了linux journal上的两篇文章:

The Linux USB Input Subsystem, Part I | Linux Journal

Using the Input Subsystem, Part II | Linux Journal

 

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/wind20/archive/2008/04/25/2327452.aspx

 

现在我就如何通过input_dev、input_handle、input_handler这三者传递信息进行详细的分析:

触摸屏驱动中,s3c2410ts_probe函数的最后一步,调用input_register_device函数开始进入三者建立联系的过程:void input_register_device(struct input_dev *dev){ struct input_handle *handle; struct input_handler *handler; struct input_device_id *id;........................................................................................ INIT_LIST_HEAD(&dev->h_list); list_add_tail(&dev->node, &input_dev_list); list_for_each_entry(handler, &input_handler_list, node)  if (!handler->blacklist || !input_match_device(handler->blacklist, dev))   if ((id = input_match_device(handler->id_table, dev)))    if ((handle = handler->connect(handler, dev, id)))     input_link_handle(handle);..........................................................................................}注: 我只保留重要的部分,省略号部分不是我关心的,以下同。list_for_each_entry(handler, &input_handler_list, node)的作用在于:从input_handler_list的链表中提取input_handler的指针。##################################################################################那这个input_handler的指针又是何时存放在input_handler_list链表里面的呢?答案是像tsdev.c这些接口驱动里面调用input_register_handler进而调用list_add_tail(&handler->node, &input_handler_list);把其input_handler指针加进input_handler_list里面,详细请查看源码,在此不做详细分析。###################################################################################获取了input_handler指针后通过input_match_device进行匹配选择:static struct input_device_id *input_match_device(struct input_device_id *id, struct input_dev *dev){ int i; for (; id->flags || id->driver_info; id++) {................................................................  MATCH_BIT(evbit,  EV_MAX);  MATCH_BIT(keybit, KEY_MAX);  MATCH_BIT(relbit, REL_MAX);  MATCH_BIT(absbit, ABS_MAX);  MATCH_BIT(mscbit, MSC_MAX);  MATCH_BIT(ledbit, LED_MAX);  MATCH_BIT(sndbit, SND_MAX);  MATCH_BIT(ffbit,  FF_MAX);  MATCH_BIT(swbit,  SW_MAX);  return id; } return NULL;}该函数拿刚才获得的input_handler指针所拥有的特性表handler->id_table与我们所注册的input_dev的特性表dev.id进行对照。仍以触摸屏驱动s3c2410-ts.c与触摸屏接口tsdev.c为例:s3c2410-ts.c: ts.dev.evbit[0] = BIT(EV_SYN) | BIT(EV_KEY) | BIT(EV_ABS); ts.dev.keybit[LONG(BTN_TOUCH)] = BIT(BTN_TOUCH); input_set_abs_params(&ts.dev, ABS_X, 0, 0x3FF, 0, 0); input_set_abs_params(&ts.dev, ABS_Y, 0, 0x3FF, 0, 0); input_set_abs_params(&ts.dev, ABS_PRESSURE, 0, 1, 0, 0); ts.dev.id.bustype = BUS_RS232; ts.dev.id.vendor = 0xDEAD; ts.dev.id.product = 0xBEEF; ts.dev.id.version = S3C2410TSVERSION;tsdev.c:static struct input_device_id tsdev_ids[] = { {       .flags = INPUT_DEVICE_ID_MATCH_EVBIT | INPUT_DEVICE_ID_MATCH_KEYBIT    | INPUT_DEVICE_ID_MATCH_RELBIT,       .evbit = { BIT(EV_KEY) | BIT(EV_REL) },       .keybit = { [LONG(BTN_LEFT)] = BIT(BTN_LEFT) },       .relbit = { BIT(REL_X) | BIT(REL_Y) },  },/* A mouse like device, at least one button, two relative axes */ {       .flags = INPUT_DEVICE_ID_MATCH_EVBIT | INPUT_DEVICE_ID_MATCH_KEYBIT    | INPUT_DEVICE_ID_MATCH_ABSBIT,       .evbit = { BIT(EV_KEY) | BIT(EV_ABS) },       .keybit = { [LONG(BTN_TOUCH)] = BIT(BTN_TOUCH) },       .absbit = { BIT(ABS_X) | BIT(ABS_Y) },  },/* A tablet like device, at least touch detection, two absolute axes */ {       .flags = INPUT_DEVICE_ID_MATCH_EVBIT | INPUT_DEVICE_ID_MATCH_ABSBIT,       .evbit = { BIT(EV_ABS) },       .absbit = { BIT(ABS_X) | BIT(ABS_Y) | BIT(ABS_PRESSURE) },  },/* A tablet like device with several gradations of pressure */ {},/* Terminating entry */};可以看到,tsdev.c接口定义了三项特性,对应id为0、1、2,input_match_device函数依次取出其中的选项与s3c2410-ts.c里面定义的input_dev的选项进行对比。这里对比的标准是tsdev.c里面定义的选项s3c2410-ts.c里面必须满足,否则continue,继续判断下一个id号的选项。详细请看MATCH_BIT这个宏的定义:#define MATCH_BIT(bit, max) /  for (i = 0; i < NBITS(max); i++) /   if ((id->bit[i] & dev->bit[i]) != id->bit[i]) /    break; /  if (i != NBITS(max)) /   continue;例如: 在这里,tsdev.c定义的id为0的选项里面定义的BIT(EV_REL)这一项在s3c2410-ts.c里面定义的input_dev设备上是不具备的,所以,执行到MATCH_BIT(evbit,  EV_MAX);后直接continue,继续判断tsdev.c里面id为1的选项,直到找到合适的,然后返回真,否则返回NULL。###########################################################################################在list_for_each_entry(handler, &input_handler_list, node)这个大循环里与我们所注册的input_dev所匹配的不限于一个接口,例如,以下是我的调试记录:s3c2410 TouchScreen successfully loadedkbdinput_match_devicemousedevinput_match_devicemousedev_connectjoydevinput_match_deviceevdevinput_match_deviceevdev_connecttsdevinput_match_devicetsdev_connectevbuginput_match_deviceevbug_connect可以看到,对于s3c2410-ts.c里面定义的input_dev设备,同时与其匹配的就有mousedev、evdev、tsdev、evbug等众多接口(不知道我的理解是否正确,如果理解错了,还望指正^_^)###########################################################################################找到匹配的选项以后,就可以开始着手把input_dev、input_handle、input_handler这三者联系齐来了,具体调用handle = handler->connect(handler, dev, id)函数,主要的目的是填充input_handle结构,然后接着调用input_link_handle(handle)函数:static void input_link_handle(struct input_handle *handle){ list_add_tail(&handle->d_node, &handle->dev->h_list); list_add_tail(&handle->h_node, &handle->handler->h_list);}看到吧,就是上面那位大侠提到的,把input_handle分别链入input_dev和input_handler中h_list为Hash头的链中。好了,到此,input_dev、input_handle、input_handler这三者总算是联系起来了^_^


################
从<tsdev.c>开始
################
static ssize_t tsdev_read(struct file *file, char __user *buffer, size_t count,
                          loff_t * ppos)
{
        struct tsdev_list *list = file->private_data;
        int retval = 0;

     /* 设备存在(exist=1:在tsdev_connect函数里设置),但缓冲中无数据,而又配置为非阻塞读取方式,直接返回 */
        if (list->head == list->tail && list->tsdev->exist && (file->f_flags & O_NONBLOCK))
                return -EAGAIN;
     /* 否则睡眠等待数据都来临 */
        retval = wait_event_interruptible(list->tsdev->wait,
                        list->head != list->tail || !list->tsdev->exist);
     /* 被信号中断唤醒,直接返回 */
        if (retval)
                return retval;
     /* 检查设备是否还存在,不存在(exist=0:在tsdev_disconnect函数中设置)的话直接返回 */
        if (!list->tsdev->exist)
                return -ENODEV;
     /* 有数据,循环读取用户所需要都数据 */
        while (list->head != list->tail &&
               retval + sizeof (struct ts_event) <= count) {
                if (copy_to_user (buffer + retval, list->event + list->tail,
                                  sizeof (struct ts_event)))
                        return -EFAULT;
                list->tail = (list->tail + 1) & (TSDEV_BUFFER_SIZE - 1);//更新读指针
                retval += sizeof (struct ts_event);//更新读字节数
        }

        return retval;
}

#################
<s3c2410-ts.c>
#################
        input_report_abs(&ts.dev, ABS_X, ts.xp);
        input_report_abs(&ts.dev, ABS_Y, ts.yp);
 
        input_report_key(&ts.dev, BTN_TOUCH, 1);
        input_report_abs(&ts.dev, ABS_PRESSURE, 1);
        input_sync(&ts.dev);

###########################
<input.h>
###########################
static inline void input_report_abs(struct input_dev *dev, unsigned int code, int value)
{
        input_event(dev, EV_ABS, code, value);
}


##########################
<input.c>
##########################
void input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value)
{
struct input_handle *handle;
........................................................................................................
        switch (type) {
                case EV_ABS:

                        if (code > ABS_MAX || !test_bit(code, dev->absbit))//一些条件测试
                                return;

                        if (dev->absfuzz[code]) {
                                if ((value > dev->abs[code] - (dev->absfuzz[code] >> 1)) &&
                                    (value < dev->abs[code] + (dev->absfuzz[code] >> 1)))
                                        return;

                                if ((value > dev->abs[code] - dev->absfuzz[code]) &&
                                    (value < dev->abs[code] + dev->absfuzz[code]))
                                        value = (dev->abs[code] * 3 + value) >> 2;

                                if ((value > dev->abs[code] - (dev->absfuzz[code] << 1)) &&
                                    (value < dev->abs[code] + (dev->absfuzz[code] << 1)))
                                        value = (dev->abs[code] + value) >> 1;
                        }

                        if (dev->abs[code] == value)//比较当前值与上一次都值是否相同,相同则不作处理
                                return;

                        dev->abs[code] = value;//备份当前值,以便下一次作比较
                        break;
...............................................................................................................
        if (type != EV_SYN)
                dev->sync = 0;

        if (dev->grab)
                dev->grab->handler->event(dev->grab, type, code, value);
        else
                list_for_each_entry(handle, &dev->h_list, d_node)//通过input_dev找出与其联系的input_handle
                        if (handle->open)//相应的接口设备(比如tsdev)被打开(通过调用tsdev.c的tsdev_open函数进而调用input_open_device
                         函数增加handle->open的计数值)
                                handle->handler->event(handle, type, code, value);//调用该接口设备的event函数对数据进行处理
}

################
在<tsdev.c>结束
################
static void tsdev_event(struct input_handle *handle, unsigned int type,
                        unsigned int code, int value)
{
        struct tsdev *tsdev = handle->private;
        struct tsdev_list *list;
        struct timeval time;

        switch (type) {
        case EV_ABS:
                switch (code) {
                case ABS_X:
                        tsdev->x = value;//记录x坐标值
                        break;
                case ABS_Y:
                        tsdev->y = value;//记录y坐标值
                        break;
                case ABS_PRESSURE:
                        if (value > handle->dev->absmax[ABS_PRESSURE])
                                value = handle->dev->absmax[ABS_PRESSURE];
                        value -= handle->dev->absmin[ABS_PRESSURE];
                        if (value < 0)
                                value = 0;
                        tsdev->pressure = value;//记录触摸屏的按压状态
                        break;
                }
                break;
...................................................................................................
        if (type != EV_SYN || code != SYN_REPORT)//键值的传递以EV_SYN为结束标志(通过input_sync函数),等到数据都填充好tsdev结构后再统一发送出去,否则直接返回,继续填充另一个数据
                return;

        list_for_each_entry(list, &tsdev->list, node) {    //通过tsdev获取struct tsdev_list结构(在tsdev_open函数中定义):
                                //list_add_tail(&list->node, &tsdev_table[i]->list);
                int x, y, tmp;

                do_gettimeofday(&time);            //填充事件的时间
                list->event[list->head].millisecs = time.tv_usec / 100;
                list->event[list->head].pressure = tsdev->pressure;//填充触摸屏的状态

                x = tsdev->x;
                y = tsdev->y;

                /* Calibration */
                if (!list->raw) {
                        x = ((x * tsdev->cal.xscale) >> 8) + tsdev->cal.xtrans;
                        y = ((y * tsdev->cal.yscale) >> 8) + tsdev->cal.ytrans;
                        if (tsdev->cal.xyswap) {
                                tmp = x; x = y; y = tmp;
                        }
                }

                list->event[list->head].x = x;        //填充x坐标值
                list->event[list->head].y = y;        //填充y坐标值
                list->head = (list->head + 1) & (TSDEV_BUFFER_SIZE - 1);//更新写指针
                kill_fasync(&list->fasync, SIGIO, POLL_IN);
        }
        wake_up_interruptible(&tsdev->wait);//唤醒睡眠在tsdev->wait下等待数据都进程读取数据。至此,数据传递过程结束,开始新一轮的数据传递。
}

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/wind20/archive/2008/04/25/2327502.aspx

 

 

最近在做linux2.6的键盘驱动程序的工作,接触到了input subsystem这一概念,现把我对其中相关结构体的理解写出来。如果我的理解有错误,希望大家指正,谢谢!
 1.input_dev
说明:输入子系统(input subsystem)的驱动层的核心结构。  

头文件:include/linux/input.h

成员说明:

void *private;

       //不清楚。

char *name;

       //设备名字,如键盘名字。

char *phys;

       //设备文件节点名,如input/kbd0。

char *uniq;

       //全球唯一的ID号。

struct input_id id;

       //后文作详细介绍。

unsigned long evbit[NBITS(EV_MAX);]

       //该设备驱动所能支持的事件。

       //EV_SYN      同步事件

       //EV_KEY       键盘事件

       //EV_REL       相对坐标事件,用于鼠标

       //EV_ABS       绝对坐标事件,用于摇杆

       //EV_MSC      其他事件

       //EV_LED       LED灯事件

       //EV_SND      声音事件

       //EV_REP       重复按键事件

       //EV_FF         受力事件

       //EV_PWR      电源事件

       //EV_FF_STATUS  受力状态事件

unsigned long keybit[NBITS(KEY_MAX)];

       //键值存放表

unsigned long relbit[NBITS(REL_MAX)];

       //用于存放相对坐标值等

unsigned long absbit[NBITS(ABS_MAX)];

       //用于存放绝对坐标值等

unsigned long mscbit[NBITS(MSC_MAX)];

       //存放其他事件类型

unsigned long ledbit[NBITS(LED_MAX)];

       //存放表示各种状态的LED值

unsigned long sndbit[NBITS(SND_MAX)];

       //存放各种事件的声音

unsigned long ffbit[NBITS(FF_MAX)];

       //存放受力设备的属性

int ff_effects_max;

       //显然与受力效果有关,具体作用还不大清楚。

unsigned int keycodemax;

unsigned int keycodesize;

void * keycode;

       //这三个不是很清楚,有点模糊理解。

unsigned int repeat_key;

       //存放重复按键时的键值

struct timer_list timer;

       //定时器

struct pm_dev *pm_dev;

       //考虑到有些设备可能有电源管理

struct pt_regs *regs;

       //不清楚

int state;

       //显然是表示一个状态,但不清楚具体是谁的状态

int sync;

       //具体用于什么也不大清楚

int abs[ABS_MAX + 1];

       //显然是与绝对坐标有关的,但具体的作用不清楚。

int rep[REP_MAX + 1];

       //存放重复按键时的延时,系统依靠这个延时时间来判断重复按键

       //rep[0]表示开始要重复按键时的延时时间,即第1个键与第2个键(开始重复按键)之间的延时

       //rep[1]此后重复按键之前的延时时间,直到按键抬起

       //通俗解释就是,假如我按了一个“a”,并且一直按着,那么在显示出来的第一个a与第二个a之间的时间延时为rep[0],而此后的相邻两个a之间的延时为rep[1]

 

unsigned long key[NBITS(KEY_MAX)];

unsigned long led[NBITS(LED_MAX)];

unsigned long snd[NBITS(SND_MAX)];

       //不知道有什么用

int absmax[ABS_MAX + 1];

int absmin[ABS_MAX + 1];

int absfuzz[ABS_MAX + 1];

int absflat[ABS_MAX + 1];

       //显然与绝对坐标值有关,但不知道具体作用

 

int (*open)(struct input_dev *dev);

void (*close)(struct input_dev *dev);

int (*accept)(struct input_dev *dev, struct file *file);

int (*flush)(struct input_dev *dev, struct file *file);

int (*event)(struct input_dev *dev, unsigned int type, unsigned int code, int value);

int (*upload_effect)(struct input_dev *dev, struct ff_effect *effect);

int (*erase_effect)(struct input_dev *dev, int effect_id);

       //底层与硬件相关的一组操作,若有具体定义,则会在input core层被调用,具体看input.c。

 

struct input_handle *grab;

       //该结构会在后文做具体介绍,这个指针用于占用输入设备用,如键盘

struct list_head h_list;

struct list_head node;

       //h_list链表用于与input_handler相联系

       //node链表:设备向输入子系统(input subsystem)注册后,会将该链表添加到系统维护的一个链表中去,从而系统可以管理这个设备

 


2. input_handler

说明:事件处理层(event handler)的核心结构。

头文件:include/linux/input.h

成员说明:

void *private;

       //不清楚有什么用

void (*event)(struct input_handle *handle, unsigned int type, unsigned int code, int value)

struct input_handle * (*connect)(struct input_handler *handler,

struct input_dev *dev,

struct input_device_id *id);

void (*disconnect)(struct input_handle *handle);

       //event handler层与input core层之间一组接口

struct file_operation *fops;

       //给应用程序所调用的一组接口

int minor;

       //不清楚有什么用

char *name;

       //input_handler的名字

struct input_device_id *id_table;

struct input_device_id *blacklist;

       //该结构体是对input_id结构体的扩展,从表面上看blacklist为被系统列为黑名单的输入设备列表

struct list_head h_list;

struct list_head node;

       //h_list链表用于与input_dev相联系

       //node链表:事件处理程序向输入子系统(input subsystem)注册后,会将该链表添加到系统维护的一个链表中去,从而系统可以管理这类事件

 3. input_handle

说明:是一个用于关联驱动层input_dev和事件处理层input_handler的中间结构。

头文件:include/linux/input.h

成员说明:

void *private;

       //不知道什么作用

int open;

       //不知道具体作用

char *name;

       //input_handle的名字

struct input_dev *dev;

struct input_handler *handler;

       //这两个就不解释了,前面都有具体介绍

struct list_head d_node;

struct list_head h_node;

       //d_node链表用于input_dev链,h_node链表用于input_handler链,有了input_handle,就把相关dev和handler联系起来,相互能容易找到。

 

 4. input_id

说明:输入设备的一些属性。

头文件:include/linux/input.h

成员说明:

__u16 bustype;

       //总线类型,如BUS_PCI、BUS_USB等

__u16 vendor;

       //设备生产商

__u16 product;

       //产品名字

__u16 version;

       //版本号

 5. input_event

说明:应用程序可通过此结构体获取输入设备事件信息,也就是说,比如在写键盘测试程序时,我们可用这个结构体,再结合ioctl系统调用来获取来自键盘的信息。

头文件:include/linux/input.h

成员说明:

struct timeval time;

       //time是一个时间戳(timestamp),储存着事件发生时的时间记录

__u16 type;

       //事件的类型,如EV_KEY,则表示输入事件为键盘事件

__u16 code;

       //事件的代码,如果为KEY_1,则表示键盘输入为“1”

__s32 value;

       //用于键盘时,value为0表示按键松开,value为1表示按键按下,value为2表示重复按键

 

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/wind20/archive/2008/04/25/2327510.aspx

 

前面对s3c2410的触摸屏驱动进行了分析,现深入一层,对其所在的输入子系统进行刺探。

首先引用一个不错的帖子,对2.6内核的输入子系统进行一个大致的描述:

引:

在做触摸屏?对于输入子系统,相信你也早看了网上一些介绍文章文章了,读一下就可了解对其基本架构,剩下的只是一些源码细节阅读。输入子系统的3层间的联系是很简单的,驱动层的核心结构为struct input_dev:struct input_dev {    ...    struct list_head        h_list;    ...};在input_register_device时就会将input_dev与input_handle联系起来;所谓联系就是将有关的input_handle链入以input_dev中h_list为Hash头的链中;而事件处理层的核心结构是struct input_handler:struct input_handler {    ...    struct list_head        h_list;    ...};在input_register_handler时同样会将input_handler与input_handle联系起来,所谓联系就是将有关的input_handle链入以input_handler中h_list为Hash头的链中;由上可见input_handle即是一个用于关联驱动层input_dev和事件处理层input_handler的中间结构:struct input_handle {    ...    struct input_dev *dev;    struct input_handler *handler;    struct list_head        d_node;    struct list_head        h_node;};其中d_node用于input_dev链,h_node用于input_handler链,有了input_handle,就把相关dev与handler联系起来,相互能容易的找到。注:
    原文请看一下网址:
http://bbs.ustc.edu.cn/cgi/bbstcon?board=Kernel&file=M.1179398612.A

看了以上的内容,相信你对2.6内核的输入子系统应该有个大概的了解了,
现在我就input_dev、input_handle、input_handler这三者建立联系的过程进行详细的分析:

触摸屏驱动中,s3c2410ts_probe函数的最后一步,调用input_register_device函数开始进入三者建立联系的过程:void input_register_device(struct input_dev *dev){ struct input_handle *handle; struct input_handler *handler; struct input_device_id *id;........................................................................................ INIT_LIST_HEAD(&dev->h_list); list_add_tail(&dev->node, &input_dev_list); list_for_each_entry(handler, &input_handler_list, node)  if (!handler->blacklist || !input_match_device(handler->blacklist, dev))   if ((id = input_match_device(handler->id_table, dev)))    if ((handle = handler->connect(handler, dev, id)))     input_link_handle(handle);..........................................................................................}注: 我只保留重要的部分,省略号部分不是我关心的,以下同。list_for_each_entry(handler, &input_handler_list, node)的作用在于:从input_handler_list的链表中提取input_handler的指针。##################################################################################那这个input_handler的指针又是何时存放在input_handler_list链表里面的呢?答案是像tsdev.c这些接口驱动里面调用input_register_handler进而调用list_add_tail(&handler->node, &input_handler_list);把其input_handler指针加进input_handler_list里面,详细请查看源码,在此不做详细分析。###################################################################################获取了input_handler指针后通过input_match_device进行匹配选择:static struct input_device_id *input_match_device(struct input_device_id *id, struct input_dev *dev){ int i; for (; id->flags || id->driver_info; id++) {................................................................  MATCH_BIT(evbit,  EV_MAX);  MATCH_BIT(keybit, KEY_MAX);  MATCH_BIT(relbit, REL_MAX);  MATCH_BIT(absbit, ABS_MAX);  MATCH_BIT(mscbit, MSC_MAX);  MATCH_BIT(ledbit, LED_MAX);  MATCH_BIT(sndbit, SND_MAX);  MATCH_BIT(ffbit,  FF_MAX);  MATCH_BIT(swbit,  SW_MAX);  return id; } return NULL;}该函数拿刚才获得的input_handler指针所拥有的特性表handler->id_table与我们所注册的input_dev的特性表dev.id进行对照。仍以触摸屏驱动s3c2410-ts.c与触摸屏接口tsdev.c为例:s3c2410-ts.c: ts.dev.evbit[0] = BIT(EV_SYN) | BIT(EV_KEY) | BIT(EV_ABS); ts.dev.keybit[LONG(BTN_TOUCH)] = BIT(BTN_TOUCH); input_set_abs_params(&ts.dev, ABS_X, 0, 0x3FF, 0, 0); input_set_abs_params(&ts.dev, ABS_Y, 0, 0x3FF, 0, 0); input_set_abs_params(&ts.dev, ABS_PRESSURE, 0, 1, 0, 0); ts.dev.id.bustype = BUS_RS232; ts.dev.id.vendor = 0xDEAD; ts.dev.id.product = 0xBEEF; ts.dev.id.version = S3C2410TSVERSION;tsdev.c:static struct input_device_id tsdev_ids[] = { {       .flags = INPUT_DEVICE_ID_MATCH_EVBIT | INPUT_DEVICE_ID_MATCH_KEYBIT    | INPUT_DEVICE_ID_MATCH_RELBIT,       .evbit = { BIT(EV_KEY) | BIT(EV_REL) },       .keybit = { [LONG(BTN_LEFT)] = BIT(BTN_LEFT) },       .relbit = { BIT(REL_X) | BIT(REL_Y) },  },/* A mouse like device, at least one button, two relative axes */ {       .flags = INPUT_DEVICE_ID_MATCH_EVBIT | INPUT_DEVICE_ID_MATCH_KEYBIT    | INPUT_DEVICE_ID_MATCH_ABSBIT,       .evbit = { BIT(EV_KEY) | BIT(EV_ABS) },       .keybit = { [LONG(BTN_TOUCH)] = BIT(BTN_TOUCH) },       .absbit = { BIT(ABS_X) | BIT(ABS_Y) },  },/* A tablet like device, at least touch detection, two absolute axes */ {       .flags = INPUT_DEVICE_ID_MATCH_EVBIT | INPUT_DEVICE_ID_MATCH_ABSBIT,       .evbit = { BIT(EV_ABS) },       .absbit = { BIT(ABS_X) | BIT(ABS_Y) | BIT(ABS_PRESSURE) },  },/* A tablet like device with several gradations of pressure */ {},/* Terminating entry */};可以看到,tsdev.c接口定义了三项特性,对应id为0、1、2,input_match_device函数依次取出其中的选项与s3c2410-ts.c里面定义的input_dev的选项进行对比。这里对比的标准是tsdev.c里面定义的选项s3c2410-ts.c里面必须满足,否则continue,继续判断下一个id号的选项。详细请看MATCH_BIT这个宏的定义:#define MATCH_BIT(bit, max) /  for (i = 0; i < NBITS(max); i++) /   if ((id->bit[i] & dev->bit[i]) != id->bit[i]) /    break; /  if (i != NBITS(max)) /   continue;例如: 在这里,tsdev.c定义的id为0的选项里面定义的BIT(EV_REL)这一项在s3c2410-ts.c里面定义的input_dev设备上是不具备的,所以,执行到MATCH_BIT(evbit,  EV_MAX);后直接continue,继续判断tsdev.c里面id为1的选项,直到找到合适的,然后返回真,否则返回NULL。###########################################################################################在list_for_each_entry(handler, &input_handler_list, node)这个大循环里与我们所注册的input_dev所匹配的不限于一个接口,例如,以下是我的调试记录:s3c2410 TouchScreen successfully loadedkbdinput_match_devicemousedevinput_match_devicemousedev_connectjoydevinput_match_deviceevdevinput_match_deviceevdev_connecttsdevinput_match_devicetsdev_connectevbuginput_match_deviceevbug_connect可以看到,对于s3c2410-ts.c里面定义的input_dev设备,同时与其匹配的就有mousedev、evdev、tsdev、evbug等众多接口(不知道我的理解是否正确,如果理解错了,还望指正^_^)###########################################################################################找到匹配的选项以后,就可以开始着手把input_dev、input_handle、input_handler这三者联系齐来了,具体调用handle = handler->connect(handler, dev, id)函数,主要的目的是填充input_handle结构,然后接着调用input_link_handle(handle)函数:static void input_link_handle(struct input_handle *handle){ list_add_tail(&handle->d_node, &handle->dev->h_list); list_add_tail(&handle->h_node, &handle->handler->h_list);}看到吧,就是上面那位大侠提到的,把input_handle分别链入input_dev和input_handler中h_list为Hash头的链中。好了,到此,input_dev、input_handle、input_handler这三者总算是联系起来了^_^转:http://www.cnitblog.com/luofuchong/archive/2007/08/24/32382.html

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/wind20/archive/2008/04/25/2327513.aspx

 

现在看用户获取触摸屏输入的一个流程(以tsdev为例/drivers/input/tsdev.c):static struct file_operations tsdev_fops = {        .owner =        THIS_MODULE,        .open =         tsdev_open,        .release =      tsdev_release,        .read =         tsdev_read,        .poll =         tsdev_poll,        .fasync =       tsdev_fasync,        .ioctl =        tsdev_ioctl,};假设所有初始化早已完成,用户open该设备后,使用read系统调用进入内核,系统转移控制到tsdev_read,使用wait_event_interruptible等待事件。此时驱动层得到用户输入,于是调用input_report_abs,input_report_abs只是input_event的简单包装:static inline void input_report_abs(struct input_dev *dev,                                      unsigned int code, int value){        input_event(dev, EV_ABS, code, value);}void input_event(struct input_dev *dev, unsigned int type,                                   unsigned int code, int value){    ...    switch (type) {        ...        case EV_ABS:            ...            break;        ...    }    ...    if (dev->grab)        dev->grab->handler->event(dev->grab, type, code, value);    else        list_for_each_entry(handle, &dev->h_list, d_node)            if (handle->open)                handle->handler->event(handle, type, code, value);}前面的处理关系具体设备,见最后对handler函数的调用,就是从input_dev的h_list链上的input_handle获得每一个相关input_handler,并调用其中的event函数,对tsdev来说:static struct input_handler tsdev_handler = {        .event =        tsdev_event,        .connect =      tsdev_connect,        .disconnect =   tsdev_disconnect,        .fops =         &tsdev_fops,        .minor =        TSDEV_MINOR_BASE,        .name =         "tsdev",        .id_table =     tsdev_ids,};即调用tsdev_event函数,接着看:static void tsdev_event(struct input_handle *handle, unsigned int type,                        unsigned int code, int value){    ...    switch (type) {        case EV_ABS:            break;    ...    list_for_each_entry(list, &tsdev->list, node) {        int x, y, tmp;        do_gettimeofday(&time);        list->event[list->head].millisecs = time.tv_usec / 100;        list->event[list->head].pressure = tsdev->pressure;        x = tsdev->x;        y = tsdev->y;        /* Calibration */        if (!list->raw) {            x = ((x * tsdev->cal.xscale) >> 8) + tsdev->cal.xtrans;            y = ((y * tsdev->cal.yscale) >> 8) + tsdev->cal.ytrans;            if (tsdev->cal.xyswap) {                tmp = x; x = y; y = tmp;            }        }        list->event[list->head].x = x;        list->event[list->head].y = y;        list->head = (list->head + 1) & (TSDEV_BUFFER_SIZE - 1);        kill_fasync(&list->fasync, SIGIO, POLL_IN);    }    wake_up_interruptible(&tsdev->wait);}它填充数据,并唤醒等待着的请求。于是前面等待着的read请求就可继续了,回到tsdev_read中,copy_to_user拷贝数据,最后返回用户层。一个简单流程就结束了。

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/wind20/archive/2008/04/25/2327531.aspx

 

上文介绍了input_dev、input_handle、input_handler三者是如何联系起来了,现在继续介绍如何通过它们来传递信息。
在开始之前还是先引用一位大侠的帖子:

引:

现在看用户获取触摸屏输入的一个流程(以tsdev为例/drivers/input/tsdev.c):static struct file_operations tsdev_fops = {        .owner =        THIS_MODULE,        .open =         tsdev_open,        .release =      tsdev_release,        .read =         tsdev_read,        .poll =         tsdev_poll,        .fasync =       tsdev_fasync,        .ioctl =        tsdev_ioctl,};假设所有初始化早已完成,用户open该设备后,使用read系统调用进入内核,系统转移控制到tsdev_read,使用wait_event_interruptible等待事件。此时驱动层得到用户输入,于是调用input_report_abs,input_report_abs只是input_event的简单包装:static inline void input_report_abs(struct input_dev *dev,                                      unsigned int code, int value){        input_event(dev, EV_ABS, code, value);}void input_event(struct input_dev *dev, unsigned int type,                                   unsigned int code, int value){    ...    switch (type) {        ...        case EV_ABS:            ...            break;        ...    }    ...    if (dev->grab)        dev->grab->handler->event(dev->grab, type, code, value);    else        list_for_each_entry(handle, &dev->h_list, d_node)            if (handle->open)                handle->handler->event(handle, type, code, value);}前面的处理关系具体设备,见最后对handler函数的调用,就是从input_dev的h_list链上的input_handle获得每一个相关input_handler,并调用其中的event函数,对tsdev来说:static struct input_handler tsdev_handler = {        .event =        tsdev_event,        .connect =      tsdev_connect,        .disconnect =   tsdev_disconnect,        .fops =         &tsdev_fops,        .minor =        TSDEV_MINOR_BASE,        .name =         "tsdev",        .id_table =     tsdev_ids,};即调用tsdev_event函数,接着看:static void tsdev_event(struct input_handle *handle, unsigned int type,                        unsigned int code, int value){    ...    switch (type) {        case EV_ABS:            break;    ...    list_for_each_entry(list, &tsdev->list, node) {        int x, y, tmp;        do_gettimeofday(&time);        list->event[list->head].millisecs = time.tv_usec / 100;        list->event[list->head].pressure = tsdev->pressure;        x = tsdev->x;        y = tsdev->y;        /* Calibration */        if (!list->raw) {            x = ((x * tsdev->cal.xscale) >> 8) + tsdev->cal.xtrans;            y = ((y * tsdev->cal.yscale) >> 8) + tsdev->cal.ytrans;            if (tsdev->cal.xyswap) {                tmp = x; x = y; y = tmp;            }        }        list->event[list->head].x = x;        list->event[list->head].y = y;        list->head = (list->head + 1) & (TSDEV_BUFFER_SIZE - 1);        kill_fasync(&list->fasync, SIGIO, POLL_IN);    }    wake_up_interruptible(&tsdev->wait);}它填充数据,并唤醒等待着的请求。于是前面等待着的read请求就可继续了,回到tsdev_read中,copy_to_user拷贝数据,最后返回用户层。一个简单流程就结束了。注:
    原文请看以下网址:
http://bbs.ustc.edu.cn/cgi/bbstcon?board=Kernel&file=M.1179398612.A

看了以上的内容,相信你对2.6内核的输入子系统的消息传递过程应该有个大概的了解了,现在我就如何通过input_dev、input_handle、input_handler这三者传递信息进行详细的分析:
################
从<tsdev.c>开始
################
static ssize_t tsdev_read(struct file *file, char __user *buffer, size_t count,
                          loff_t * ppos)
{
        struct tsdev_list *list = file->private_data;
        int retval = 0;

     /* 设备存在(exist=1:在tsdev_connect函数里设置),但缓冲中无数据,而又配置为非阻塞读取方式,直接返回 */
        if (list->head == list->tail && list->tsdev->exist && (file->f_flags & O_NONBLOCK))
                return -EAGAIN;
     /* 否则睡眠等待数据都来临 */
        retval = wait_event_interruptible(list->tsdev->wait,
                        list->head != list->tail || !list->tsdev->exist);
     /* 被信号中断唤醒,直接返回 */
        if (retval)
                return retval;
     /* 检查设备是否还存在,不存在(exist=0:在tsdev_disconnect函数中设置)的话直接返回 */
        if (!list->tsdev->exist)
                return -ENODEV;
     /* 有数据,循环读取用户所需要都数据 */
        while (list->head != list->tail &&
               retval + sizeof (struct ts_event) <= count) {
                if (copy_to_user (buffer + retval, list->event + list->tail,
                                  sizeof (struct ts_event)))
                        return -EFAULT;
                list->tail = (list->tail + 1) & (TSDEV_BUFFER_SIZE - 1);//更新读指针
                retval += sizeof (struct ts_event);//更新读字节数
        }

        return retval;
}

#################
<s3c2410-ts.c>
#################
        input_report_abs(&ts.dev, ABS_X, ts.xp);
        input_report_abs(&ts.dev, ABS_Y, ts.yp);
 
        input_report_key(&ts.dev, BTN_TOUCH, 1);
        input_report_abs(&ts.dev, ABS_PRESSURE, 1);
        input_sync(&ts.dev);

###########################
<input.h>
###########################
static inline void input_report_abs(struct input_dev *dev, unsigned int code, int value)
{
        input_event(dev, EV_ABS, code, value);
}


##########################
<input.c>
##########################
void input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value)
{
struct input_handle *handle;
........................................................................................................
        switch (type) {
                case EV_ABS:

                        if (code > ABS_MAX || !test_bit(code, dev->absbit))//一些条件测试
                                return;

                        if (dev->absfuzz[code]) {
                                if ((value > dev->abs[code] - (dev->absfuzz[code] >> 1)) &&
                                    (value < dev->abs[code] + (dev->absfuzz[code] >> 1)))
                                        return;

                                if ((value > dev->abs[code] - dev->absfuzz[code]) &&
                                    (value < dev->abs[code] + dev->absfuzz[code]))
                                        value = (dev->abs[code] * 3 + value) >> 2;

                                if ((value > dev->abs[code] - (dev->absfuzz[code] << 1)) &&
                                    (value < dev->abs[code] + (dev->absfuzz[code] << 1)))
                                        value = (dev->abs[code] + value) >> 1;
                        }

                        if (dev->abs[code] == value)//比较当前值与上一次都值是否相同,相同则不作处理
                                return;

                        dev->abs[code] = value;//备份当前值,以便下一次作比较
                        break;
...............................................................................................................
        if (type != EV_SYN)
                dev->sync = 0;

        if (dev->grab)
                dev->grab->handler->event(dev->grab, type, code, value);
        else
                list_for_each_entry(handle, &dev->h_list, d_node)//通过input_dev找出与其联系的input_handle
                        if (handle->open)//相应的接口设备(比如tsdev)被打开(通过调用tsdev.c的tsdev_open函数进而调用input_open_device
                         函数增加handle->open的计数值)
                                handle->handler->event(handle, type, code, value);//调用该接口设备的event函数对数据进行处理
}

################
在<tsdev.c>结束
################
static void tsdev_event(struct input_handle *handle, unsigned int type,
                        unsigned int code, int value)
{
        struct tsdev *tsdev = handle->private;
        struct tsdev_list *list;
        struct timeval time;

        switch (type) {
        case EV_ABS:
                switch (code) {
                case ABS_X:
                        tsdev->x = value;//记录x坐标值
                        break;
                case ABS_Y:
                        tsdev->y = value;//记录y坐标值
                        break;
                case ABS_PRESSURE:
                        if (value > handle->dev->absmax[ABS_PRESSURE])
                                value = handle->dev->absmax[ABS_PRESSURE];
                        value -= handle->dev->absmin[ABS_PRESSURE];
                        if (value < 0)
                                value = 0;
                        tsdev->pressure = value;//记录触摸屏的按压状态
                        break;
                }
                break;
...................................................................................................
        if (type != EV_SYN || code != SYN_REPORT)//键值的传递以EV_SYN为结束标志(通过input_sync函数),等到数据都填充好tsdev结构后再统一发送出去,否则直接返回,继续填充另一个数据
                return;

        list_for_each_entry(list, &tsdev->list, node) {    //通过tsdev获取struct tsdev_list结构(在tsdev_open函数中定义):
                                //list_add_tail(&list->node, &tsdev_table[i]->list);
                int x, y, tmp;

                do_gettimeofday(&time);            //填充事件的时间
                list->event[list->head].millisecs = time.tv_usec / 100;
                list->event[list->head].pressure = tsdev->pressure;//填充触摸屏的状态

                x = tsdev->x;
                y = tsdev->y;

                /* Calibration */
                if (!list->raw) {
                        x = ((x * tsdev->cal.xscale) >> 8) + tsdev->cal.xtrans;
                        y = ((y * tsdev->cal.yscale) >> 8) + tsdev->cal.ytrans;
                        if (tsdev->cal.xyswap) {
                                tmp = x; x = y; y = tmp;
                        }
                }

                list->event[list->head].x = x;        //填充x坐标值
                list->event[list->head].y = y;        //填充y坐标值
                list->head = (list->head + 1) & (TSDEV_BUFFER_SIZE - 1);//更新写指针
                kill_fasync(&list->fasync, SIGIO, POLL_IN);
        }
        wake_up_interruptible(&tsdev->wait);//唤醒睡眠在tsdev->wait下等待数据都进程读取数据。至此,数据传递过程结束,开始新一轮的数据传递。
}

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/wind20/archive/2008/04/25/2327589.aspx

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值