UVC驱动分析一条龙

资源:

分析UVC驱动所使用的应用程序源码:drivers/media/usb/uvc/uvc_app.c · suiren/rk3328_kernel - Gitee.com

本博客的txt版:drivers/media/usb/uvc/uvc_probe_read.c · suiren/rk3328_kernel - Gitee.com

UVC驱动精简后的代码:rk3328_kernel: UVC 驱动精简.详见 drivers/media/usb/uvc.4.4.194 内核 rk3328 - Gitee.com

注意:

本博客中使用的函数名, 在分析过程中被我修改了,添加了前缀gm.

去掉前缀,即为原函数名.譬如:

gm_uvc_parse_control  对应原函数名为 uvc_parse_control .

总体uvc驱动执行流程


1. 驱动初始化, 生成video0节点. uvc_probe().
2. 流协商, 设置视频格式与分辨率.主要见gm_uvc_ioctl_s_fmt_vid_cap()函数.
3. 创建urb并提交, 等待urb完成. 主要见gm_uvc_init_video().
4. 将urb的视频数据复制到v4l2提供的buf内. 应用程序从此buf获得视频数据.
    主要见 gm_uvc_video_complete()和 uvc_video_copy_data_work()函数.


 uvc驱动初始化流程

@uvc_driver.c
uvc_probe

    /* 填充 stream, stream->format, stream->format->frame*/
    gm_uvc_parse_control

    /* 生成video0设备节点, 关联 ops供应用层调用. */
    gm_uvc_register_chains

@uvc_driver.c
gm_uvc_parse_control

  /* while循环处理配置描述符之后的信息.
     * 1. 首先是VC Interface Header Descriptor;
     * 2. 第二为VC Interface Input Terminal Descriptor
     * 3. VC Interface Processing Unit Descriptor
     * 4. VC Interface Output Terminal Descriptor
     *
     * 实际流程根据描述符顺序而定.*/
    while() gm1_uvc_parse_standard_control;

     /* 这里以处理VC Interface Header Descriptor 为例;
      * 因为头部描述符是必须的, 其他的输入,输出,处理终端描述符,
      * 都是不必要的,可以省略的....
      *
      * 而且处理这个比较特殊,会顺带将 以下描述符也一并处理.
      * 1. VS Interface Input Header Descriptor
      * 2. VS Video Format Descriptor
      * 3. VS Uncompressed Frame Type Descriptor*/
    gm1_uvc_parse_standard_control
        /*  */
        case UVC_VC_HEADER:
            /* 数据获得方式:VC Interface Header Descriptor->bcdUVC */
            dev->uvc_version = 0x100;
            /*48Mhz
             * 数据获得方式: VC Interface Header Descriptor->dwClockFrequency */
            dev->clock_frequency = 0x2dc6c00;
            /* 获得设备的接口1描述符,
             * 即是 视频流接口描述符. 根据设备而定.
             * 调试时我将接口号写死了...*/
            intf = usb_ifnum_to_if(udev, 1);
            /* Parse all USB Video Streaming interfaces. */
            gm1_uvc_parse_streaming(dev, intf);

@uvc_driver.c
gm1_uvc_parse_streaming

    /* 将接口与驱动关联, 说明该接口已被驱动中了.
     * 即一个driver处理两个接口, 因为这两个接口为关联接口.
     * IAD Descriptor 声明 视频控制描述符和视频流描述符为关联接口.*/
    usb_driver_claim_interface(&uvc_driver.driver, intf, dev);

    /* streaming 结构体, 关联视频流接口. */
    streaming->intf = usb_get_intf(intf);
    /* GM: 0号接口为视频控制接口;1号接口为视频流接口,视实际设备而定.. */
    streaming->intfnum = 1;

    /* 以下为对视频流输入头部信息的处理 */
    case UVC_VS_INPUT_HEADER:
    /* 只要是有VS_INPUT_HEADER, 那么type的值就是 V4L2_BUF_TYPE_VIDEO_CAPTURE... */
    streaming->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

    /* 视频流接口input_header 相关信息.
     * 根据VS Interface Input Header Descriptor  进行填充*/
    streaming->header.bNumFormats = 1;
    streaming->header.bEndpointAddress = 0x82; /* GM: 端点2 */

    nframes = 5;  /* VIDEO_FORMAT_DESCRIPTOR->bNumberFrameDescriptor */
    nintervals = 10; /* VIDEO_FRAME_DESCRIPTOR->dwFrameInterval[x]
                        dwFrameInterval 为不定数组.
                        nintervals 即为不定数组的大小.
                        此摄像头的160x120 frame的dwFrameInterval数组的大小为2.
                        在未解析VIDEO_FRAME_DESCRIPTOR 前,不知其数组大小.
                        因此预先分配个大的.*/

    streaming->format = format;
    streaming->nformats = nformats;

    /* Parse the format descriptors. */
    format->frame = frame;
    gm1_uvc_parse_format(dev, streaming, format, &interval, buffer, buflen);

    /* GM: 值为 视频流接口(非视频控制接口)的端点的wMaxPacketSize */
    psize = 5120; /*le16_to_cpu(ep->desc.wMaxPacketSize); */
    psize = (psize & 0x07ff) * (1 + ((psize >> 11) & 3));
    streaming->maxpsize = psize; /* 3072 */

    /* 将已填充好数据的streaming 将入 列表.
     * streaming 所包含的主要信息为
     *      ->header  1. 进行视频流数据传送的 端点
     *                2 . 所支持的视频格式 数量.
     *
     *      ->format  1. 所支持格式的具体信息. 即是VS Video Format Descriptor 的内容
     *          format-> frame  1. 所支持的 分辨率信息. 即是VS Frame Type Descriptor 的内容.
     *
     *      ->nformats  1. 所支持的视频格式 数量.
     *
     *      ->maxpsize  1. 端点最大传送大小. 如果格式为压缩格式(例如yuv2),
     *                   则该值会不使用,而由get_cur(vc_probe)所得的dwMaxPayloadTransferSize来替代.
     *
     *                   其实该值只要往大里取就可以了, 这样就能保证每次IN传输都能完整地
     *                   接受设备发来的视频数据.
     *                   很多时候,设备每次IN传输发来的数据长度可能只有300~1024. 远远小于
     *                   dwMaxPayloadTransferSize的3072, 和端点的wMaxPacketSize的5120.*/
    list_add_tail(&streaming->list, &dev->streams);

@uvc_driver.c
gm1_uvc_parse_format

    /* 根据VS Video Format Descriptor->bDescriptorSubtype 决定*/
    format->type = UVC_VS_FORMAT_UNCOMPRESSED;
    /* 第1个格式, 从1开始计数. 我的设备只有yuv2 一种格式..*/
    format->index = 0x1;

    /* 根据VS Video Format Descriptor->guidFormat 决定 */
    format->fcc = V4L2_PIX_FMT_YUV422P;

    /* 根据VS Video Format Descriptor->bBitsPerPixel 决定*/
    format->bpp = 16;

     /* 填充frame的信息. 从VS Frame Type Descriptor 获得 */
    frame->bFrameIndex = 0x5; /* 第5个frame的信息. 从1开始计数. */
    frame->bmCapabilities = 0x0;
    frame->wWidth = 160;
    frame->wHeight = 120;
    frame->dwDefaultFrameInterval = 333333;


@uvc_driver.c
gm_uvc_register_chains

    gm_uvc_register_terms
        /* 获得stream结构, 从dev的stream列表中.
         * 当前列表仅加入了一个stream, 由gm1_uvc_parse_streaming() 函数.*/
        stream = gm_uvc_stream_by_id(dev, 0);
        gm_uvc_register_video
            gm_uvc_video_init
            /* 两个ops由此被关联, 被应用层和v4l2调用. */
            gm_uvc_register_video_device(xxx,..., &uvc_fops, &uvc_ioctl_ops)


gm_uvc_video_init

    struct uvc_streaming_control *probe = &stream->ctrl;

    /* GM: 设置视频流接口为0号设置.  即当前是关闭视频流的
     * stream->intfnum 的值,是在gm1_uvc_parse_streaming() 函数里, 被设置.*/
    usb_set_interface(stream->dev->udev, stream->intfnum, 0);

    /* probe 结构将作为urb的buffer发送给摄像头,
     * 在Video Request Set Cur (Video Stream Interface Control)
     * 的设置请求里.*/
    /* 这里的format->index原本是通过get_cur获得摄像头的默认format来决定的,但我直接就写死了.. */
    probe->bFormatIndex = format->index;
    probe->bFrameIndex = frame->bFrameIndex; /* 值为5,即第5个frame. 从1开始计数. */

    stream->def_format = format;
    /* 在gm_uvc_v4l2_set_format()函数内, 也会设置以下的2个值,根据用户的输入参数决定.
     * 而在gm_uvc_video_init()里, 这2个值是根据usb请求get_cur获得摄像头的默认参数而决定的
     * 显然, 即使不执行gm_uvc_v4l2_set_format(), 直接取读取视频数据,
     * 那么将获得摄像头默认视频格式以及分辨率的视频数据 */
    stream->cur_format = format;
    stream->cur_frame = frame;

    /* 当urb完成时, 将调用stream->decode() 进行视频数据处理. */
    stream->decode = gm_uvc_video_decode_isoc;

@uvc_driver.c
gm_uvc_register_video_device

    /* 设置queue的许多成员, 在不了解v4l2的情况下,也没有什么好分析的. */
    gm_uvc_queue_init
        /* uvc_queue_qops 将被使用到. */
        queue->queue.ops = &uvc_queue_qops;
        /* rmmod 驱动时, 将执行 uvc_release */
        vdev->release = uvc_release;
        vb2_queue_init(&queue->queue);
    /* 生成设备节点, video0 */
    video_register_device

UVC执行流程分析

接下来, 结合应用程序,来分析uvc驱动的执行流程.

我所使用的应用程序代码, 执行流程为以下:

    fd = open("/dev/video0",O_RDWR);
    ioctl(fd,VIDIOC_S_FMT,&format);
    ioctl(fd,VIDIOC_REQBUFS,&reqbufs);
    ioctl(fd,VIDIOC_QUERYBUF,&bufs);
    mmap
    ioctl(fd,VIDIOC_QBUF,&bufs);
    ioctl(fd,VIDIOC_STREAMON,&type);
    ioctl(fd,VIDIOC_DQBUF,&readbuffer);
    ioctl(fd,VIDIOC_QBUF,&readbuffer);
    ioctl(fd,VIDIOC_STREAMOFF,&type);
    close(fd);

@uvc_v4l2.c
#open("/dev/video0",O_RDWR); => uvc_fops->open
gm_uvc_v4l2_open

    /* 形成 file->private_data->stream 的关联
     * 每一个的应用层调用,包括ioctl, 都有fie作为输入参数,
     * 因此,stream则一直伴随着我们...*/
    handle->stream = stream;
    file->private_data = handle;

@uvc_v4l2.c
#ioctl(fd,VIDIOC_S_FMT,&format);  =>  uvc_ioctl_ops->vidioc_s_fmt_vid_cap
gm_uvc_ioctl_s_fmt_vid_cap

    gm_uvc_v4l2_set_format
        gm_uvc_v4l2_try_format(stream, fmt, &probe, &format, &frame);

        uvc_queue_allocated(&stream->queue);

        /* probe 在gm_uvc_v4l2_try_format() 里被设置. */
        stream->ctrl = probe;
        /* 在gm_uvc_video_init()函数内, 也会设置以下的2个值.
         * 而在gm_uvc_video_init()里, 这2个值是根据usb请求get_cur获得摄像头的默认参数而决定的
         **/
        stream->cur_format = format;
        stream->cur_frame = frame;

@uvc_v4l2.c
gm_uvc_v4l2_try_format

    /* 在gm_uvc_video_init()设置过一次probe, 使用的是摄像头的默认参数.
     * 但其实我将probe的值写死了, 在gm_uvc_video_init()函数里.
     * 这里直接将probe清零, 根据用户给参数, 重新设置...*/
    memset(probe, 0, sizeof *probe);
    probe->bFormatIndex = 1;  /* 格式1, yuv2. 从1开始计数 */
    probe->bFrameIndex = 5;

    gm_uvc_probe_video(stream, probe);
        /* GM: 为什么, 设置后又读?
         * 因为设置的参数可能有一些是错误的,或者我们不知道该设置什么值.
         * 而这些不知道的值,也会是我们需要的.
         * 譬如dwMaxVideoFrameSize, dwMaxPayloadTransferSize.
         *
         * 设置时, 这两个值都是0. 需要靠get_cur获得他们的值. 摄像头会返回正确的值给我们.
         *
         * 这里的步骤属于 uvc规范里的 流协商 流程中的 协商环节.
         * 使用的是VS_PROBE_CONTROL请求.
         *
         * 接下来还有一个步骤为确认, 使用VS_COMMIT_CONTROL请求.
         * 在gm_uvc_video_enable() => uvc_commit_video() 函数里进行.*/
        /* gm_uvc_get_video_ctrl()将摄像头返回的参数保存到 probe */
        gm_uvc_set_video_ctrl(stream, probe, 1);
        gm_uvc_get_video_ctrl(stream, probe, 1, UVC_GET_CUR);


@uvc_v4l2.c
#ioctl(fd,VIDIOC_REQBUFS,&reqbufs);  => uvc_ioctl_ops->vidioc_reqbufs
gm_uvc_ioctl_reqbufs
    /* 这就没什么好分析了, 直接使用v4l2的函数.. */
    uvc_request_buffers(&stream->queue, rb);
        vb2_reqbuf

@uvc_queue.c
/* 执行gm_uvc_ioctl_reqbufs 时会由v4l2来继续执行gm_uvc_queue_setup
 * 在gm_uvc_register_video_device()函数内, 关联了uvc_queue_qops.*/
uvc_queue_qops->queue_setup
gm_uvc_queue_setup

@uvc_v4l2.c
#ioctl(fd,VIDIOC_QUERYBUF,&bufs); => uvc_ioctl_ops->vidioc_querybuf
gm_uvc_ioctl_querybuf
    uvc_query_buffer(&stream->queue, buf)
        vb2_querybuf


@uvc_v4l2.c
#mmap => uvc_fops->mmap
uvc_v4l2_mmap
    uvc_queue_mmap(&stream->queue, vma)
        vb2_mmap

@uvc_v4l2.c
#ioctl(fd,VIDIOC_QBUF,&bufs); => uvc_ioctl_ops->vidioc_qbuf
gm_uvc_ioctl_qbuf
    uvc_queue_buffer
        vb2_qbuf

@uvc_queue.c
/* v4l2 会接着执行以下函数.*/
gm_uvc_buffer_prepare

@uvc_v4l2.c
#ioctl(fd,VIDIOC_STREAMON,&type); => uvc_ioctl_ops->vidioc_streamon
uvc_queue_streamon
    vb2_streamon

/* v4l2 会接着执行以下函数.*/
@uvc_queue.c
gm_uvc_buffer_queue

@uvc_queue.c
gm_uvc_start_streaming

    /* 表示当前queue的buffer是空的. */
    queue->buf_used = 0;
    gm_uvc_video_enable
        /* Commit the streaming parameters. */
        /* GM: 又来一次设置摄像头参数. 不可省略的步骤! */
        /* stream->ctrl 在gm_uvc_ioctl_s_fmt_vid_cap()里被设置.
         *
         * gm_uvc_ioctl_s_fmt_vid_cap() => gm_uvc_v4l2_try_format()的执行流程为:
         *  1. gm_uvc_v4l2_try_format
         *          => gm_uvc_set_video_ctrl(xxxx, probe) VS_PROBE_CONTROL
         *          => gm_uvc_get_video_ctrl(xxx, probe)  VS_PROBE_CONTROL
         *  2. stream->ctrl = probe;
         *
         *  gm_uvc_get_video_ctrl()将当前设置的正确配置保存至probe
         *  在这里使用probe里的正确配置, 再次对摄像头进行设置.*/

        /* GM: 根据uvc规范, 对视频流设置, 先进行VS_PROBE_CONTROL 协商,
         * 然后再确认VS_COMMIT_CONTROL. 仅协商不确认,摄像头无法工作..*/
        uvc_commit_video(stream, &stream->ctrl); /* VS_COMMIT_CONTROL */
        gm_uvc_init_video(stream, GFP_KERNEL);

@uvc_video.c
gm_uvc_init_video

    /* fid,即帧的ID号.  
     * Video Stream Payload Header->bmHeaderInfo的第一个位即为fid.
     * 同一帧内, 每次urb的数据的payload_header的fid是相同的.
     * 且fid在每一帧中, 是0,1,0,1地切换.*/                                                                
    /* 这里是fid的初始化. */
    stream->last_fid = -1;
        
    /* 该工作队列将在 */
    stream->async_wq = alloc_workqueue("uvcvideo", WQ_UNBOUND | WQ_HIGHPRI, 0);

    /* GM: 在该视频流接口的1号设置里,才包含端点.
     * 0号设置, 只是包含一些视频格式, frame 的描述符信息.
     * */
    alts = &intf->altsetting[1];
    /* 找到该接口下的端点. */
    ep = uvc_find_endpoint(alts, stream->header.bEndpointAddress);

    altsetting = alts->desc.bAlternateSetting;

    /* GM: 设置视频流接口为1号设置. 终于要开始传输视频数据了! */
    usb_set_interface(stream->dev->udev, intfnum, altsetting);

    /* 分配并填充同步传输用的urb.(stream->uvc_urb) */
    gm_uvc_init_video_isoc(stream, ep, gfp_flags);

    /* 这些urb就是用来传输视频数据的了. */
    for (i = 0; i < UVC_URBS; ++i) /* UVC_URBS == 5 */
    {
        uvc_urb = &stream->uvc_urb[i];
        usb_submit_urb(uvc_urb->urb, gfp_flags);
    }

@uvc_video.c
gm_uvc_init_video_isoc

    psize = uvc_endpoint_max_bpi(stream->dev->udev, ep); /* 3072 */
    /* stream->ctrl 的值 最终是在 gm_uvc_video_enable() 内被确定. */
    size = stream->ctrl.dwMaxVideoFrameSize; /* 38400 */

    /* size 为一帧图像的大小, psize为每次USB的IN传输的大小.
     * npackets 为所需IN传输的次数, 根据size 和 psize计算. */
    npackets = gm_uvc_alloc_urb_buffers(stream, size, psize, gfp_flags);

    /* 再次计算所需buffer的大小, 得出的size>= 图像大小. */
    size = npackets * psize;

    for (i = 0; i < UVC_URBS; ++i) { /* UVC_URBS == 5 */                                   
        /* 将分配5个urb */
        struct uvc_urb *uvc_urb = &stream->uvc_urb[i];
        /* 每个urb将重复利用13(npackets)次. */
        urb = usb_alloc_urb(npackets, gfp_flags);
        urb->number_of_packets = npackets;
        /* urb传输完成后,即被重复传输13次后, 执行complete函数. */
        urb->complete = gm_uvc_video_complete;
        for (j = 0; j < npackets; ++j) { /* npackets == 13 */
            /* urb重复传输13, 每次重复传输的buffer的地址偏移,和大小. */
            urb->iso_frame_desc[j].offset = j * psize;
            urb->iso_frame_desc[j].length = psize; /* 3072 */
        }
    }

@uvc_video.c
gm_uvc_video_complete

    /* 从queue中取出一个buffer, 这个buffer最终是v4l2给到应用程序的.*/
    buf = uvc_queue_get_current_buffer(queue);

    /* 将urb的buffer内的图像数据指针,
     * 进行Video Stream Payload Header大小的地址偏移,
     * 并保存偏移后的指针.
     *
     * urb的buffer数据分布:
     *      0~x  Video Stream Payload Header
     *      x~y  图像数据
     *
     * 因此,在之后的uvc_video_copy_data_work()函数里,
     * 就可以方便地使用偏移后的buffer地址, 直接复制里面的图像数据了.
     * */
    stream->decode(uvc_urb, buf, buf_meta); /* decode=>gm_uvc_video_decode_isoc */

    /* If no async work is needed, resubmit the URB immediately. */
    /* 当此次urb没有数据接收, 则无需async_work(uvc_video_copy_data_work).
     * uvc_video_copy_data_work()的作用为:
     * 将urb的视频数据,复制到v4l2提供的buf内.
     * 就立即将此urb再此提交.*/
    if (!uvc_urb->async_operations) {
        ret = usb_submit_urb(uvc_urb->urb, GFP_ATOMIC);
        return;
    }

    INIT_WORK(&uvc_urb->work, uvc_video_copy_data_work);
    queue_work(stream->async_wq, &uvc_urb->work);

@uvc_video.c
gm_uvc_video_decode_isoc

    /* urb->number_of_packets 在gm_uvc_init_video_isoc()内,
     * 被设置.*/
    for (i = 0; i < urb->number_of_packets; ++i) {
        /* 每次IN传输共用一个buffer,只是偏移不同而已.*/
        mem = urb->transfer_buffer + urb->iso_frame_desc[i].offset; /* offset = offset*i, offset=3072 */
        /* GM: 获得stream_payload_header的长度而已. */
        ret = gm_uvc_video_decode_start(stream, buf, mem, urb->iso_frame_desc[i].actual_length);

        gm_uvc_video_decode_data(uvc_urb, buf, mem + ret, urb->iso_frame_desc[i].actual_length - ret);

        gm_uvc_video_decode_end(stream, buf, mem, urb->iso_frame_desc[i].actual_length);

         /* 当一帧已保存完成, 或buf的空间用完了, 就申请下一个buf.
          * buf->state 在 gm_uvc_video_decode_end()内被设置.*/
        if (buf->state == UVC_BUF_STATE_READY) {
            /* 从queue获取下一个空buf. */
            gm_uvc_video_next_buffers(stream, &buf, &meta_buf);
        }
    }


@uvc_video.c
gm_uvc_video_decode_data

    /* decode结构 将在 uvc_video_copy_data_work()内使用.
     * 将decode->scr的数据复制到 decode->dst, 长度为decode->len*/
    decode->src = data; /* 去掉头部(stream_payload_header)之后的视频数据. */
    decode->dst = buf->mem + buf->bytesused; /* 目的地:将urb的视频数据 复制 到这里. */
    decode->len = min_t(unsigned int, len, maxlen); /* 当前urb的IN包的数据流size不能大于剩余空间 */

    /* 增加queue的buffer的已用空间 */
    buf->bytesused += decode->len;

@uvc_video.c
gm_uvc_video_decode_end

    /* 什么情况下设置 buf->state:
     * 1. 当视频数据到末尾了,说明一帧已传送完毕, 则当前buf就是准备好的了.
     * 2. 当该buf已无剩余空间了,则该buf也是准备好了, 要申请下一个buf,继续接收当前一帧的数据.*/
    if (data[1] & UVC_STREAM_EOF && buf->bytesused != 0) {
        uvc_trace(UVC_TRACE_FRAME, "Frame complete (EOF found).\n");
        buf->state = UVC_BUF_STATE_READY;
    }

@uvc_video.c
uvc_video_copy_data_work

    /* 就是将urb的数据复制到queue的buf内.
     * 这里的uvc_urb->copy_operations 的成员的值,
     * 在gm_uvc_video_decode_data() 内被设置.*/
    for (i = 0; i < uvc_urb->async_operations; i++) {
        struct uvc_copy_op *op = &uvc_urb->copy_operations[i];
        memcpy(op->dst, op->src, op->len);
    }

    /* 重新提交这个已完成的urb */
    usb_submit_urb(uvc_urb->urb, GFP_ATOMIC);

@uvc_v4l2.c
#ioctl(fd,VIDIOC_DQBUF,&readbuffer); => uvc_ioctl_ops->vidioc_dqbuf
gm_uvc_ioctl_dqbuf
    uvc_dequeue_buffer
        vb2_dqbuf

@uvc_v4l2.c
#ioctl(fd,VIDIOC_STREAMOFF,&type); => uvc_ioctl_ops->vidioc_streamoff
gm_uvc_ioctl_streamoff
    uvc_queue_streamoff
        vb2_streamoff

/* v4l2 会接着执行以下函数.*/
@ uvc_queue.c
gm_uvc_stop_streaming

    gm1_uvc_video_enable
        /* 将stream->uvc_urb释放 */
        gm_uvc_uninit_video(stream, 1);
        /* 设置摄像头的视频流接口为 0号设置.
         * 即关闭视频流传输.*/
        usb_set_interface(stream->dev->udev, stream->intfnum, 0);

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值