一个鼠标驱动程序的分析转

一个鼠标驱动程序的分析(/driver/input/mouse/amimouse.c)

 

一:Input设备驱动,(我称为虚拟输入设备)//drivers/input/input.c文件

     input设备是一种字符设备在模块初始化时创建设备类"input",注册Input字符设备,input的操作函数只有Open函数。当打开特定设备时才将特定的设备操作函数

     static struct file_operations input_fops = {

    .owner = THIS_MODULE,

        .open = input_open_file,

    };

函数调用如下:

/input/input . c

input_init()//这是虚拟输入设备的入口,既模块加载时调用

{

    class_simple_create(THIS_MODULE,"input");//创建设备类

    input_proc_init()//创建proc下的文件节点

        retval = register_chrdev(INPUT_MAJOR, "input", &input_fops);//注册字符设备驱动程序,在系统字符设备数组中添加一个字符设备,主设备号为INPUTMAJOR,操作函数集为input_fops,在特殊文件打开时会根据文件的节点结构INODE中的主设备号在系统字符设备数组中搜索主设备号相同的字符设备驱动程序的操作函数集并将此操作函数集付给FILE结构的操作函数集指针f_ops并执行该函数集中的打开操作。。对于本input类设备即为input_fops中的input_open_file

    devfs_mk_dir("input")//在设备目录"/dev"下创建devfs文件系统的"input"目录,以后的具体输入设备也将在这个目录下建立特殊设备文件,当打开该特殊文件时即对设备进行操作

}

当打开具体文件时既执行input_ipen_file此函数会寻到具体设备的文件操作函数集并付给file->f_op(这是一个文件的操作函数集,当系统打开一个文件时即产生一个file结构对该文件的操作都通过file中的f_opes 如读取即调用FILE->f_op->read

二:输入驱动器,输入驱动器是指一类的输入设备(比如鼠标输入驱动器,键盘输入驱动器等等)这里说的是鼠标输入驱动器。。(我也称之为虚拟鼠标驱动器,因为他并不完成真正的硬件相关的鼠标驱动,真正的硬件IO驱动要在device中完成)他的描述结构是:

struct input_handler {

        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);//断开设备

       struct file_operations *fops;//文件操作函数集

       int minor;//次设备号

       char *name;//设备名

        .....

};

static struct mousedev *mousedev_table[MOUSEDEV_MINORS];

static struct input_handler mousedev_handler = {

       .event =    mousedev_event,//鼠标事件回调函数

        .connect =  mousedev_connect,//连接device

        .disconnect =   mousedev_disconnect,

        .fops =     &mousedev_fops,//文件操作函数集

        .minor =    MOUSEDEV_MINOR_BASE,//次设备号基数

        .name =     "mousedev",//设备名

        .id_table = mousedev_ids,//本驱动支持的设备ID

};

他的入口是:

/input/mousedev . c

static int __init mousedev_init(void)

{

    input_register_handler(&mousedev_handler);//在虚拟输入设备中注册鼠标输入驱动器

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

    INIT_LIST_HEAD(&mousedev_mix.list);

    init_waitqueue_head(&mousedev_mix.wait);

    mousedev_table[MOUSEDEV_MIX] = &mousedev_mix;//给设备数组一个默认的鼠标device

    mousedev_mix.exist = 1;

    mousedev_mix.minor = MOUSEDEV_MIX;

    devfs_mk_cdev(MKDEV(INPUT_MAJOR, MOUSEDEV_MINOR_BASE + MOUSEDEV_MIX),

    S_IFCHR|S_IRUGO|S_IWUSR, "input/mice");//建立input/mice文件

    class_simple_device_add(input_class, MKDEV(INPUT_MAJOR, MOUSEDEV_MINOR_BASE + MOUSEDEV_MIX),

        NULL, "mice");//input_class设备类中增加本设备驱动程序

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

    return 0;

}

虚拟鼠标设备驱动器就提供了各种操作的操作集函数,如read,write,ioctrl,

static struct file_operations mousedev_fops = {

    .owner =    THIS_MODULE,

        .read =     mousedev_read,

        .write =    mousedev_write,

        .poll =     mousedev_poll,

        .open =     mousedev_open,

        .release =  mousedev_release,

        .fasync =   mousedev_fasync,

};

/input/input . c

void input_register_handler(struct input_handler *handler)

{

    struct input_dev *dev;//具体设备描述结构

    struct input_handle *handle;//设备句柄

    struct input_device_id *id;//设备ID

    if (!handler) return;

    INIT_LIST_HEAD(&handler->h_list);//初始化设备驱动器的链表

    if (handler->fops != NULL)

    input_table[handler->minor >> 5] = handler;//把驱动器按次设备号放到驱动器数组里,虚拟设备支持最多8个次设备

    list_add_tail(&handler->node, &input_handler_list);//加入驱动器链表

    list_for_each_entry(dev, &input_dev_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);//连接成功将设备句柄加到设备的句柄链表中

}

我们先假设注册驱动器时还没有一个输入设备在链表中,即注册时不需要进行驱动器适配。在后面设备注册时再讲适配连接

这样就完成了一个驱动器的注册

三,具体设备。。

这里完成的是真实与硬件交互的设备程序。。设备的描述结构是input_dev

struct input_dev {

    void *private;//私有数据

    char *name;//

    ......

    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);//都是些回调函数了

    struct input_handle *grab;//设备句柄

    struct device *dev;//通用设备结构

    struct list_head    h_list;//句柄链表input_link_handle函数将设备句柄加入到链表中。dev响应中断时如果没有自己专用的event函数就遍历handle列表找到handle指向的input_handlerevnt函数调用

    struct list_head    node;

};

struct input_handle {

    void *private;//私有数据

    int open;

    char *name;

    struct input_dev *dev;//设备

    struct input_handler *handler;//句柄拥有者驱动器

    struct list_head    d_node;

    struct list_head    h_node;

};

它的入口是:

/input/mouse/amimouse . c

static int __init amimouse_init(void)

{

    if (!MACH_IS_AMIGA || !AMIGAHW_PRESENT(AMI_MOUSE))

        return -ENODEV;

    amimouse_dev.evbit[0] = BIT(EV_KEY) | BIT(EV_REL);

    amimouse_dev.relbit[0] = BIT(REL_X) | BIT(REL_Y);

    amimouse_dev.keybit[LONG(BTN_LEFT)] = BIT(BTN_LEFT) | BIT(BTN_MIDDLE) | BIT(BTN_RIGHT);

    amimouse_dev.open = amimouse_open;

    amimouse_dev.close = amimouse_close;

   

    amimouse_dev.name = amimouse_name;

    amimouse_dev.phys = amimouse_phys;

    amimouse_dev.id.bustype = BUS_AMIGA;

    amimouse_dev.id.vendor = 0x0001;

    amimouse_dev.id.product = 0x0002;

    amimouse_dev.id.version = 0x0100;

   

    input_register_device(&amimouse_dev);//注册设备

   

    printk(KERN_INFO "input: %s at joy0dat/n", amimouse_name);

    return 0;

}

/input/input . c

void input_register_device(struct input_dev *dev)

{

    struct input_handle *handle;

    struct input_handler *handler;

    struct input_device_id *id;

    /*

    * If delay and period are pre-set by the driver, then autorepeating

    * is handled by the driver itself and we don't do it in input.c.

    */

    init_timer(&dev->timer);//初始化设备的定时器,对鼠标没什么用mousedev里没对他做什么,对键盘有用

    if (!dev->rep[REP_DELAY] && !dev->rep[REP_PERIOD]) {

        dev->timer.data = (long) dev;

        dev->timer.function = input_repeat_key;

        dev->rep[REP_DELAY] = 250;

        dev->rep[REP_PERIOD] = 33;

    }

    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)))//找到与设备相配的驱动器,用id_table适配

    if ((handle = handler->connect(handler, dev, id)))//适配成功与驱动器连接// 这里是mouse驱动器mousedevconnect函数是mousedev_connect

input_link_handle(handle);//Mousedev返回input_handle(注意不是input_handler)加到devhandle连表中//这样dev响应中断时如果没有自己专用的event函数就遍历handle列表找到handle指向的input_handlerevnt函数调用            

#ifdef CONFIG_HOTPLUG

                input_call_hotplug("add", dev);

#endif

                #ifdef CONFIG_PROC_FS

                input_devices_state++;

                wake_up(&input_devices_poll_wait);

#endif

}

看看驱动器是怎么连接设备的,他申请一个mousedev结构用来保存鼠标设备信息。将设备和设备句柄及input_handler联系起来放在mousedev_table指针数组中并给每个mousedev分配一个ID minor进行唯一标志

struct mousedev {

    int exist;//设备是否存在

    int open;//

    int minor;

    char name[16];

    wait_queue_head_t wait;//等待队列

    struct list_head list;//打开mousdev的文件的列表

    struct input_handle handle;//设备句柄- 

    struct mousedev_hw_data packet;

    unsigned int pkt_count;

    int old_x[4], old_y[4];

    int frac_dx, frac_dy;

    unsigned long touch;

};

struct input_handle {

    void *private;

    int open;

    char *name;

    struct input_dev *dev;

    struct input_handler *handler;

    struct list_head    d_node;

    struct list_head    h_node;

};

/input/mousedev . c

static struct input_handle *mousedev_connect(struct input_handler *handler, struct input_dev *dev, struct input_device_id *id)

{

    struct mousedev *mousedev;

    int minor = 0;

    for (minor = 0; minor < MOUSEDEV_MINORS && mousedev_table[minor]; minor++);

    if (minor == MOUSEDEV_MINORS) {

        printk(KERN_ERR "mousedev: no more free mousedev devices/n");

        return NULL;

    }

    if (!(mousedev = kmalloc(sizeof(struct mousedev), GFP_KERNEL)))

        return NULL;

    memset(mousedev, 0, sizeof(struct mousedev));

   

    INIT_LIST_HEAD(&mousedev->list);

    init_waitqueue_head(&mousedev->wait);

   

    mousedev->minor = minor;

    mousedev->exist = 1;

    mousedev->handle.dev = dev;

    mousedev->handle.name = mousedev->name;

    mousedev->handle.handler = handler;

    mousedev->handle.private = mousedev;

    sprintf(mousedev->name, "mouse%d", minor);

    if (mousedev_mix.open)

    input_open_device(&mousedev->handle);//看默认的mousedev是否需要打开,如果需要就打开设备。

    mousedev_table[minor] = mousedev;

    devfs_mk_cdev(MKDEV(INPUT_MAJOR, MOUSEDEV_MINOR_BASE + minor),

    S_IFCHR|S_IRUGO|S_IWUSR, "input/mouse%d", minor);建立设备文件

    class_simple_device_add(input_class,

    MKDEV(INPUT_MAJOR, MOUSEDEV_MINOR_BASE + minor),

    dev->dev, "mouse%d", minor);

    return &mousedev->handle;

}

所有的注册已经完成

四:设备控制。。。。

打开设备。

当系统打开一个设备时 如"/dev/input/mouse0",虚拟文件系统根据文件节点INODE中的主设备

号在系统字符设备数组中搜索主设备号相同的字符设备驱动程序的操作函数集并将此操作函数集付给FILE结构的操作函数集指针f_ops并执行该函数集中的打开操作。。对于input类设备即为input_fops中的input_open_file

/input/input . c

static int input_open_file(struct inode *inode, struct file *file)

{

    struct input_handler *handler = input_table[iminor(inode) >> 5];//根据次设备号找到驱动器这里我们就假设是打开

    鼠标既找到了mousedev_handler

    struct file_operations *old_fops, *new_fops = NULL;

    int err;

   

    /* No load-on-demand here? */

    if (!handler || !(new_fops = fops_get(handler->fops)))//得到输入驱动器的操作函数集//输入驱动器。可以是一个鼠标输入驱动器

        //或者一个键盘输入驱动器,这里分析的是鼠标

        return -ENODEV;

    /*

        * That's _really_ odd. Usually NULL ->open means "nothing special",

        * not "no device". Oh, well...

    */

    if (!new_fops->open) {//如果输入驱动器没有打开操作函数,既设备文件无法完成打开操作

        fops_put(new_fops);

        return -ENODEV;

    }

    old_fops = file->f_op;

    file->f_op = new_fops;//将句柄的操作函数集给file->f_op

    err = new_fops->open(inode, file);//执行输入驱动器的打开操作

    if (err) {

        fops_put(file->f_op);

        file->f_op = fops_get(old_fops);

    }

    fops_put(old_fops);

    return err;

}

联系err = new_fops->open(inode, file);

static struct input_handler mousedev_handler = {

    .event =    mousedev_event,//鼠标事件回调函数

        .connect =  mousedev_connect,//连接device

        .disconnect =   mousedev_disconnect,

        .fops =     &mousedev_fops,//文件操作函数集

        .minor =    MOUSEDEV_MINOR_BASE,//次设备号基数

        .name =     "mousedev",//设备名

        .id_table = mousedev_ids,//本驱动支持的设备ID

};

这个就相当于调用mousedev_handler.fops中的函数也即mousedev_fops的函数,

mousedev_fopsopen函数是mousedev_open

分配一个mousedev_list结构保存给file结构的私有数据

mousedev_list里保存了一个mousedev结构。。而mousedev也有个文件链表。这样当

中断发生时可以根据链表反向找到file结构,而后驱动器打开他拥有并存在的未打开的所有设备句柄

调用了input_open_device函数

/input/mousedev . c

static int mousedev_open(struct inode * inode, struct file * file)

{

    struct mousedev_list *list;

    struct input_handle *handle;

    struct mousedev *mousedev;

    int i;

   

#ifdef CONFIG_INPUT_MOUSEDEV_PSAUX

    if (imajor(inode) == MISC_MAJOR)

        i = MOUSEDEV_MIX;

    else

#endif

        i = iminor(inode) - MOUSEDEV_MINOR_BASE;

   

    if (i >= MOUSEDEV_MINORS || !mousedev_table[i])

        return -ENODEV;

   

    if (!(list = kmalloc(sizeof(struct mousedev_list), GFP_KERNEL)))

        return -ENOMEM;

    memset(list, 0, sizeof(struct mousedev_list));

   

    spin_lock_init(&list->packet_lock);

    list->pos_x = xres / 2;

    list->pos_y = yres / 2;

    list->mousedev = mousedev_table[i];

    list_add_tail(&list->node, &mousedev_table[i]->list);

    file->private_data = list;

   

    if (!list->mousedev->open++) {

        if (list->mousedev->minor == MOUSEDEV_MIX) {

            list_for_each_entry(handle, &mousedev_handler.h_list, h_node) {

                mousedev = handle->private;

                if (!mousedev->open && mousedev->exist)

                    input_open_device(handle);

            }

        } else

            if (!mousedev_mix.open && list->mousedev->exist)

                input_open_device(&list->mousedev->handle);

    }

   

    return 0;

}

input_open_device 调用设柄句柄中保存的设备指针的open函数直接打开设备

/input/input . c

int input_open_device(struct input_handle *handle)//mousedev里的mousedevopen调用

{

    handle->open++;

    if (handle->dev->open)

        return handle->dev->open(handle->dev);//调用具体设备的OPEN比如amimouse里的Open

    return 0;

}

这就相当于打开了第三节中input_dev::open函数既amimouse_open函数

amimouse_open函数为设备申请了中断资源并设定了中断服务程序amimouse_interrupt

/input/mouse/amimouse . c

static int amimouse_open(struct input_dev *dev)

{

    unsigned short joy0dat;

   

    if (amimouse_used++)

        return 0;

   

    joy0dat = custom.joy0dat;

   

    amimouse_lastx = joy0dat & 0xff;

    amimouse_lasty = joy0dat >> 8;

   

    if (request_irq(IRQ_AMIGA_VERTB, amimouse_interrupt, 0, "amimouse", amimouse_interrupt)) {

        amimouse_used--;

        printk(KERN_ERR "amimouse.c: Can't allocate irq %d/n", IRQ_AMIGA_VERTB);

        return -EBUSY;

    }

   

    return 0;

}

中断服务程序amimouse_interrupt从内存得到数据//amimouse中断发生时会把数据存在一块固定地址

通过input_report_rel等函数通知设备已获得数据

/input/mouse/amimouse . c

static irqreturn_t amimouse_interrupt(int irq, void *dummy, struct pt_regs *fp)

{

/**/

        unsigned short joy0dat, potgor;

    int nx, ny, dx, dy;

   

    joy0dat = custom.joy0dat;

   

    nx = joy0dat & 0xff;

    ny = joy0dat >> 8;

   

    dx = nx - amimouse_lastx;

    dy = ny - amimouse_lasty;

   

    if (dx < -127) dx = (256 + nx) - amimouse_lastx;

    if (dx >  127) dx = (nx - 256) - amimouse_lastx;

    if (dy < -127) dy = (256 + ny) - amimouse_lasty;

    if (dy >  127) dy = (ny - 256) - amimouse_lasty;

   

    amimouse_lastx = nx;

    amimouse_lasty = ny;

   

    potgor = custom.potgor;

    input_regs(&amimouse_dev, fp);

   

    input_report_rel(&amimouse_dev, REL_X, dx);

    input_report_rel(&amimouse_dev, REL_Y, dy);

   

    input_report_key(&amimouse_dev, BTN_LEFT,   ciaa.pra & 0x40);

    input_report_key(&amimouse_dev, BTN_MIDDLE, potgor & 0x0100);

    input_report_key(&amimouse_dev, BTN_RIGHT,  potgor & 0x0400);

   

    input_sync(&amimouse_dev);

    .......

    return IRQ_HANDLED;

}

 

input_report_relinput_report_key函数只是简单调用input_event函数

/linux/input.h

static inline void input_report_rel(struct input_dev *dev, unsigned int code, int value)

{

    input_event(dev, EV_REL, code, value);

}

如果设备自已有专用event函数既input_dev结构中event非空 input_event调用event函数

如果与设备相关的设备句柄是唯一指定的保存在grab中就调用该句柄所指向的驱动器handlerevent函数

否则遍历设备设备句柄链表调用句柄指向的驱动器event函数。。

这里因为amimouse没有专属的event函数而grabinput_register_device时在mousedev_connect函数中也没给值,

但其返回的设备句柄在设备的句柄链表里。所以会调用mousedev驱动器的event函数既mousedev_event函数

/input/input . c

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

{

    struct input_handle *handle;

 

    if (type > EV_MAX || !test_bit(type, dev->evbit))

        return;

 

    add_input_randomness(type, code, value);

    switch (type) {

 

        case EV_SYN:

            switch (code) {

                case SYN_CONFIG:

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

                    break;

                    ...

                    ...

                    ...

            }

            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);//line 439

}

 

/input/mousedev . c

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

{

    struct mousedev *mousedev = handle->private;//mousedev_connect中给handle的私有变量的mousedev指针

   

    switch (type) {

        ....

    case EV_SYN:

        if (code == SYN_REPORT) {

            if (mousedev->touch) {

                mousedev->pkt_count++;

                /* Input system eats duplicate events, but we need all of them

                * to do correct averaging so apply present one forward

                */

                fx(0) = fx(1);

                fy(0) = fy(1);

            }

           

            mousedev_notify_readers(mousedev, &mousedev->packet);//通知设备有数据到达读取

           

            mousedev->packet.dx = mousedev->packet.dy = mousedev->packet.dz = 0;

            mousedev->packet.abs_event = 0;

        }

        break;

    case EV_REL:

        mousedev_rel_event(mousedev, code, value);

        /****/

        break;

        ....

    }

}

static void mousedev_rel_event(struct mousedev *mousedev, unsigned int code, int value)

{

    switch (code) {

    case REL_X: mousedev->packet.dx += value; break;

    case REL_Y: mousedev->packet.dy -= value; break;

    case REL_WHEEL: mousedev->packet.dz -= value; break;

    }

}

读取鼠标位置时

同打开文件一样,读取file时会调用filef_op函数集的函数,但此是f_op已经在打开时付值为驱动器的函数集指针了。

mousedev_fops函数集,其中的read函数为mousedev_read,他在本设备等待有输入发生。

/input/mousedev . c

static ssize_t mousedev_read(struct file * file, char __user * buffer, size_t count, loff_t *ppos)

{

    struct mousedev_list *list = file->private_data;

    int retval = 0;

   

    if (!list->ready && !list->buffer && (file->f_flags & O_NONBLOCK))

        return -EAGAIN;

   

    retval = wait_event_interruptible(list->mousedev->wait,

        !list->mousedev->exist || list->ready || list->buffer);

   

    if (retval)

        return retval;

   

    if (!list->mousedev->exist)

        return -ENODEV;

   

    if (!list->buffer && list->ready) {

        mousedev_packet(list, list->ps2);

        list->buffer = list->bufsiz;

    }

   

    if (count > list->buffer)

        count = list->buffer;

   

    list->buffer -= count;

   

    if (copy_to_user(buffer, list->ps2 + list->bufsiz - list->buffer - count, count))

        return -EFAULT;

   

    return count;

}

通过mousedev->list遍历打开本设备的所有mousedev_list结构并对他们付值,mousedev_list结构是在打开设备时链接进list的,而在打开此设备的文件描述符File中的privatedata指向些list指针。。。而后置list->ready为真表有数据到达,再唤醒一个在本设备等待读数据的进程

/input/mousedev . c

static void mousedev_notify_readers(struct mousedev *mousedev, struct mousedev_hw_data *packet)

{

    struct mousedev_list *list;

    struct mousedev_motion *p;

    unsigned long flags;

   

    list_for_each_entry(list, &mousedev->list, node) {

        spin_lock_irqsave(&list->packet_lock, flags);

       

        p = &list->packets[list->head];

        if (list->ready && p->buttons != mousedev->packet.buttons) {

            unsigned int new_head = (list->head + 1) % PACKET_QUEUE_LEN;

            if (new_head != list->tail) {

                p = &list->packets[list->head = new_head];

                memset(p, 0, sizeof(struct mousedev_motion));

            }

        }

       

        if (packet->abs_event) {

            p->dx += packet->x - list->pos_x;

            p->dy += packet->y - list->pos_y;

            list->pos_x = packet->x;

            list->pos_y = packet->y;

        }

       

        list->pos_x += packet->dx;

        list->pos_x = list->pos_x < 0 ? 0 : (list->pos_x >= xres ? xres : list->pos_x);

        list->pos_y += packet->dy;

        list->pos_y = list->pos_y < 0 ? 0 : (list->pos_y >= yres ? yres : list->pos_y);

       

        p->dx += packet->dx;

        p->dy += packet->dy;

        p->dz += packet->dz;

        p->buttons = mousedev->packet.buttons;

       

        if (p->dx || p->dy || p->dz || p->buttons != list->last_buttons)

            list->ready = 1;

       

        spin_unlock_irqrestore(&list->packet_lock, flags);

       

        if (list->ready)

            kill_fasync(&list->fasync, SIGIO, POLL_IN);

    }

   

    wake_up_interruptible(&mousedev->wait);

}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值