基于V4L2编程详解(一)

基于V4L2编程详解(一)   
 
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/types.h>
#include <linux/videodev.h>
#include <malloc.h>

#include <string.h>
#include <sys/time.h>


#define USB_VIDEO_DEV "/dev/ video0"
#define FILE_NAME "/tmp/1.jpg"
#define STILL_IMAGE -1
#define VIDEO_START 0
#define VIDEO_STOP 1
#define VIDEO_PALETTE_RAW_JPEG 20
#define VIDEO_PALETTE_JPEG 21
static int debug 1;

int get_jpegsize(unsigned char *buf, int size)
{
  int i;
  for(i 1024; size; i++)
   
  if (buf[i] == 0xFF)&&(buf[i+1] == 0xD9)) return i+2;//jpeg文件格式中是以0xFF 0xD9结尾的,

  //以此判断文件大小
  }
  return -1;
}

int main(int argc, char *argv[])
  
int usb_camera_fd -1,framesize=0,jpegsize=0;
char *usb_video_dev USB_VIDEO_DEV; //"/dev/ video0"
char *filename FILE_NAME;// "/tmp/1.jpg"

FILE *fp;
struct video_capability video_caps;
struct video_channel video_chan;
struct video_picture video_pic;   
struct video_mbuf video_mbuffer;
struct video_mmap vid_mmap;
unsigned char *mapaddr=NULL,*framebuffer=NULL,*destbuffer=NULL;
usb_camera_fd open(usb_video_dev,O_RDWR);//打开设备,可读写,也即打开"/dev/ video0"

// usb_camera_fd是设备号,open成功则返回设备号
if (usb_camera_fd == -1)
{
fprintf(stderr,"Can't open device %s",usb_video_dev);
return 1;
  
-----------------------------video_capability-----------------------------

if (ioctl(usb_camera_fd,VIDIOCGCAP,&video_caps) == -1)//get videodevice capability 获取设

//    备基本信息
{
perror("Couldn't get videodevice capability");
return -1;
}

if (debug)
{
printf("video name %s\n",video_caps.name);// struct video_capability video_caps;
printf("video_caps.channels :%d\n",video_caps.channels);
printf("video_caps.type 0x%x\n",video_caps.type);
printf("video maxwidth %d\n",video_caps.maxwidth);
printf("video maxheight %d\n",video_caps.maxheight);
printf("video minwidth %d\n",video_caps.minwidth);
printf("video minheight %d\n",video_caps.minheight);   
}

-----------------------------video_channel-----------------------------
// struct video_channel video_chan;
if (ioctl(usb_camera_fd,VIDIOCGCHAN,&video_chan) == -1)//获取信号源的属性
{
perror("ioctl (VIDIOCGCAP)");
return -1;
}
if (debug)
{
printf("video channel: %d\n",video_chan.channel);
printf("video channel name: %s\n",video_chan.name);
printf("video channel type: %d\n",video_chan.type);
}*/

-----------------------------video_picture-----------------------------

if (ioctl(usb_camera_fd,VIDIOCGPICT,&video_pic) == -1)//获取设备采集的图象的各种属性
{
perror("ioctl (VIDIOCGPICT)");
return -1;
}

屏蔽了?屏蔽了?

if (debug)
{
printf("video_pic.brightness %d\n",video_pic.brightness);
printf("video_pic.colour %d\n",video_pic.colour);
printf("video_pic.contrast %d\n",video_pic.contrast);
printf("video_pic.depth %d\n",video_pic.depth);
printf("video_pic.hue %d\n",video_pic.hue);
printf("video_pic.whiteness %d\n",video_pic.whiteness);
printf("video_pic.palette %d\n",video_pic.palette);
}
-----------------------------video_mbuf-----------------------------

memset(&video_mbuffer,0,sizeof(video_mbuffer));


//初始化video_mbuf,以得到所映射的buffer的信息
if (ioctl(usb_camera_fd,VIDIOCGMBUF,&video_mbuffer) == -1)//video_mbuf
{
perror("ioctl (VIDIOCGMBUF)");
return -1;
}
if (debug)
{
printf("video_mbuffer.frames %d\n",video_mbuffer.frames); // frames最多支持的帧数
printf("video_mbuffer.offsets[0] %d\nvideo_mbuffer.offsets[1] :%d\n",video_mbuffer.offsets [0],video_mbuffer.offsets[1]); //每帧相对基址的偏移
printf("video_mbuffer.size %d\n",video_mbuffer.size); //每帧大小
   
}
等一等,对照v4l.docx看到这儿了

//mmapvideo_mbuf绑定
mapaddr=(unsigned char *)mmap(0,video_mbuffer.size,PROT_READ,MAP_SHARED,usb_camera_fd, 0);

if (mapaddr 0)
{
perror("v4l mmap");
return -1;
}
-----------------------------video_mmap-----------------------------

vid_mmap.width 320;
vid_mmap.height 240;
vid_mmap.frame 0;//单帧采集
vid_mmap.format VIDEO_PALETTE_JPEG;

-----------------------------start capture-----------------------------Mmap方式下真正开始视频截取

//若调用成功,开始一帧的截取,是非阻塞的 是否截取完毕留给VIDIOCSYNC来判断
if (ioctl(usb_camera_fd,VIDIOCCAPTURE,VIDEO_START) == -1)
{
perror("ioctl (VIDIOCCAPTURE)");
return -1;
}

-----------------------------wait for ready---------------------///调用VIDIOCSYNC等待一帧截取结束

//若成功,表明一帧截取已完成。可以开始做下一次 VIDIOCMCAPTURE
//frame是当前截取的帧的序号

   
framesize=320*240>>2;//实际采集到的jpeg图像的大小最多也就十几KB

//获取刚刚采集到的图像数据帧的地址

framebuffer mapaddr video_mbuffer.offsets[vid_mmap.frame];

//获取图像的大小

//jpegsize get_jpegsize(framebuffer, framesize);

if (jpegsize 0)
{
printf("Can't get size of jpeg picture\n");
return 1;
}

//分配空间,准备将destbuffer 缓冲区中的图像数据写入文件
destbuffer (unsigned char *)malloc(video_mbuffer.size);
if destbuffer == NULL)
{
printf("malloc memory for destbuffer error\n");
return -1;
}
memcpy(destbuffer,framebuffer,video_mbuffer.size);

fp fopen(filename,"wb");//打开文件
if (!fp)
{
printf("Can't open file %s",filename);
//return -1;
}

fwrite(destbuffer,video_mbuffer.size,1,fp);//写入文件
fclose(fp);//关闭文件

free(destbuffer);//释放空间
munmap(mapaddr,video_mbuffer.size);//取消绑定
close(usb_camera_fd);
return 1;
}

 

一、介绍

原文网址:http://lwn.net/Articles/203924/
笔者最近有机会写了一个摄像头的驱动,是“One laptop per child”项目的中摄像头专用的。这个驱动使用了为此目的而设计的内核APIthe Video4Linux2 API。在写这个驱动的过程中,笔者发现了一个惊人的问题:这个API的文档工作做得并不是很好,而用户层的文档则写的,实际上,相当不错。为了补救现在的状况,LWN将在未来的内个月里写一系列文章,告诉大家如何写V4L2接口的驱动。
V4L2有一段历史了。大约在1998的秋天,它的光芒第一次出现在Bill Dirks的眼中。经过长足的发展,它于200211月,发布2.5.46时,融入了内核主干之中。然而直到今天,仍有一部分内核驱不支持新的API,这种新旧API的转换工作仍在进行。同时,V4L2 API也在发展,并在2.6.18版本中进行了一些重大的改变。支持V4L2的应用依旧相对较少。
V4L2在设计时,是要支持很多广泛的设备的,它们之中只有一部分在本质上是真正的视频设备:

video capture interface (影像捕获接口)从调谐器或是摄像头上获取视频数据。对很多人来讲,影像捕获(video capture) V4L2的基本应用。由于笔者在这方面的经验是强项,这一系列文章也趋于强调捕获API,但V4L2不止这些。 

video output interface (视频输出接口)允许应用使用PC的外设,让其提供视频图像。有可能是通过电视信号的形式。 

捕获接口还有一个变体,存在于video overlay interface(视频覆盖接口)之中。它的工作是方便视频显示设备直接从捕获设备上获取数据。视频数据直接从捕获设备传到显示设备,无需经过CPU。 

VBI interfaces Vertical blanking interval interface,垂直消隐接口)提供垂直消隐期的数据接入。这个接口包括rawsliced两种接口,其分别在于硬件中处理的VBI数据量。(为什么要在消隐期间接入数据呢?看这里 

radio interface (广播接口) 用于从AMFM调谐器中获得音频数据。

也可能出现其它种类的设备。V4L2 API中还有一些关于编译码和效果设备的stub,他们都用来转换视频数据流。然而这块的东西尚未完成确定,更不说应用了。还有“teletext””radio data system”的接口,他们目前在V4L1 API中实现。他们没有移动到V4L2API中来,而且目前也没有这方面的计划。
视频驱动与其他驱动不同之处,在于它的配置方式多种多样。因此大部分V4L2驱动都有一些特定的代码,好让应用可以知道给定的设备有什么功能,并配置设备,使其按期望的方式工作。V4L2API定义了几十个回调函数,用来配置如调谐频率、窗口和裁剪、帧速率、视频压缩、图像参数(亮度、对比度…)、视频标准、视频格式等参数。这一系列文章的很大部分都要用来考察这些配置的过程。
然后,还有一个小任务,就是有效地在视频频率下进行I/O操作。V4L2定义了三种方法来在用户空间和外设之间移动视频数据,其中有些会比较复杂。视频I/O和视频缓冲层,将会分成两篇文章来写,它们是用来处量一些共性的任务的。
随后的文章每几周发一篇,共会加入到下面的列表中。

二、注册和打开

原文网址:http://lwn.net/Articles/204545/
这篇文章是LWNV4L2接口的设备驱动系列文章的第二篇。没看过介绍篇的,也许可以从那篇开始看。这一期文章将关注Video for Linux驱动的总体结构和设备注册过程。
开始之前,有必要提一点,那就是对于搞视频驱动的人来说,有两份资料是非常有价值的。

TheV4L2 API Specification.V4L2 API说明)这份文档涵盖了用户空间视角下的API,但在很大程度上,V4L2驱动直接实现的就是那些API。所以大部分结构体是相同的,而且V4L2调用的语义也表述很很明了。打印一份出来(可以考虑去掉自由文本协议的文本内容,以保护树木[前面是作者原文,节省纸张就是保护树木嘛 ]),放在容易够到的地方。 

内核代码中的vivi驱动,即drivers/media/video/vivi.c.这是一个虚拟驱动。它可以用来测试,却不使用任何实际硬件。这样,它就成一个教人如何写V4L2驱动的非常好的实例。

首先,每个V4L2驱动都要包含一个必须的头文件:

1

  #include 


大部分所需的信息都在这里。作为一个驱动作者,当挖掘头文件的时候,你可能也得看看include/media/v4l2-dev.h,它定义了许多你将来要打交道的结构体。
一个视频驱动很可能要有处理PCI总线,或USB总线的部分。这里我们不会花什么时间还接触这些东西。通常会有一个内部一I2C接口,我们在这一系列的后续文章中会接触到它。然后还有一个V4L2的子系统接口。这个子系统是围绕video_device这个结构体建立的,它代表的是一个V4L2设备。讲解进入这个结构体的一切,将会是这个系列中几篇文章的主题。这里我们先有一个概览。
video_device结构体的name字段是这一类设备的名字,它会出现在内核日志和sysfs中出现。这个名字通常与驱动的名字相同。
所表示的设备有两个字段来描述。第一个字段(type)似乎是从V4L1API中遗留下来的,它可以下列四个值之一:

VFL_TYPE_GRABBER 表明是一个图像采集设备包括摄像头、调谐器,诸如此类。 

VFL_TYPE_VBI 代表的设备是从视频消隐的时间段取得信息的设备。 

VFL_TYPE_RADIO 代表无线电设备。 

VFL_TYPE_VTX 代表视传设备。

如果你的设备支持上面提到的不只一种功能,那就要为每个功能注册一个V4L2设备。然而在V4L2中,注册的每个设备都可以用作它实际支持的各种模式(就是说,你要为一个物理设备创建不多个设备节点,但你却可以调用任意一个设备节点,来实现这个物理设备支持的任意功能)。实质上的问题是,在V4L2中,你实际上只需一个设备,注册多个V4l2设备只是为了与V4l1兼容。
第二个字段是type2,它以掩码的形式对设备的功能提供了更详尽的描述。它可以包含以下值:

VID_TYPE_CAPTURE 它可以捕获视频数据 

VID_TYPE_TUNER 它可以接收不同的频率 

VID_TYPE_TELETEXT 它可以抓取字幕 

VID_TYPE_OVERLAY 它可以将视频数据直接覆盖到显示设备的帧缓冲区 

VID_TYPE_CHROMAKEY 一种特殊的覆盖能力,覆盖的仅是帧缓冲区中像素值为某特定值的区域 

VID_TYPE_CLIPPING 它可以剪辑覆盖数据 

VID_TYPE_FRAMERAM 它使用帧缓冲区中的存储器 

VID_TYPE_SCALES 它可以缩放视频数据 

VID_TYPE_MONOCHROME 这个是一个纯灰度设备 

VID_TYPE_SUBCAPTURE 它可以捕获图像的子区域 

VID_TYPE_MPEG_DECODER 它支持mpeg码流解码 

VID_TYPE_MPEG_ENCODER 它支持编码mpeg码流 

VID_TYPE_MJPEG_DECODER 它支持mjpeg解码 

VID_TYPE_MJPEG_ENCODER 它支持mjpeg编码

V4L2驱动还要初始化的一个字段是minor,它是你想要的子设备号。通常这个值都设为-1,这样会让video4linux子系统在注册时自动分配一个子设备号。
在video_device结构体中,还有三组不同的函数指针集。第一组只包含一个函数,那就是release(),如果驱动没有release()函数,内核就会抱怨(笔者发现一个件有趣的事,就是这个抱怨涉及到冒犯一篇LWN文章的作者)。release()函数很重要:由于多种原因,对video_device的引用可以在最后一个应用关闭文件描述符后很长一段时间依然保持。它们甚至可以在设备己经注销后依然保持。因此,在release()函数调用前,释放这个结构体是不安全的。所以这个函数通常要包含一个简单的kfree()调用。
video_devicefile_operations结构体包含都是常规的函数指针。视频设备通常都包括open()release()函数。注意:这里所说的release函数并非上面所讲到的同名的release()函数,这个release() 函数只要设备关闭就要调用。通常都还要有read()write()函数,这取决于设备的功能是输入还是输出。然而我们要注意的是,对于视频流设备而言,传输数据还有别的方法。多数处理视频流数据的设备还需要实现poll()mmap();而且每个V4L2设备都要有ioctl()函数,但是也可以使用V4L2子系统的video_ioctl2();
第三组函数存在于video_device结构体本身里面,它们是V4L2 API的核心。这组函数有几十个,处理不同的设备配置操作、流输入输出和其他操作。
最后,从一开始就要知道的一个字段就是debug.可以把它设成是V4L2_DEBUG_IOCTLV4L2_DEBUG_IOCTL_ARG(或是两个都设,这是个掩码),可以生成很多的调试信息,它们可以帮助一个迷糊的程序员找到毛病,知道为什么驱动和应用谁也不知道对方在说什么。
视频设备注册
一旦video_device己经配置好,就可以下面的函数注册了:

1

int video_register_device(struct video_device *vfd, int type, int nr);


这里vfd是设备的结构体(video_device),type的值与它的type字段值相同,nr也是一样,想要的子设备号(为-1则注册时自动分配)。返回值当为0,若返加的是负的
出错码,则表明出错了,和通常一样,我们要知道,设备一旦注册,它的函数可能就会立即调用,所以不到一切准备就绪,不要调用video_register_device();
设备的注销方法为:

1

void video_unregister_device(struct video_device *vfd);


请继续关注本系列的下篇文章,我们将会看看这些函数的具体实现。
open() 和 release()每个V4L2设备都需要open()函数,其原型也与常规的相同。
int (*open)(struct inode *inode, struct file *filp);

open()函数要做的第一件事是通过给定的inode找到内部设备,这是通过找到inode中存存储的子设备号来完成的。这里还可以实现一定数量的初始化,如果有关闭电源选项的话,这个时间恰好可以用来开启硬件电源。
V4L2规范还定义了一些相关的惯例。其一是:根据其设计,文件描述符可以在给定的任何时间重复打开。这样设定的目的是当一个应用在显示(或是产生)视频信号时,另一个应用可以改变控制值。所以,虽然某些操作是独占性质的(特别是数据读、写等),但是设备总体本身是要支持描述符复用的。
另一个值得一提的惯例是:open()函数,总体上讲,不可以改变硬件中现行的操作参数。有些时候可能会有这样的情况:通过命令行程序,根据一组参数(分辨率,视频格式等)来改变摄像头配置,然后运行一个完全不同的程序来,比如说,从摄像头获取上帧图像。如果摄像头在设置在中途改变了,这种模式就不好用。所以除非应用明确表示要改变设置(这种情况当然不包括在open函数中)V4L2驱动要尽量保持设定不变。
release()函数做一些必要清理工作。因为文件描述符可以重复打开,所以release函数中减小引用计数,并在彻底退出之前做检查。如果关闭的文件描述符是用来传输数据的,release函数很可能要关掉DMA,并做一些其他的清理工作。
本系列的下一篇文章我们将进入查询设备功能和设定系统模式的冗长过程之中,请续断关注。

1. 控制命令VIDIOC_QUERYCAP
功能:查询设备驱动的功能 ;
参数说明:参数类型为V4L2的能力描述类型struct v4l2_capability;
struct v4l2_capability {
       __u8   driver[16];             //驱动名称,
       __u8   card[32];            //
       __u8   bus_info[32];     //PCI总线信息
       __u32  version;       
       __u32  capabilities;        //设备能力
       __u32   reserved[4];
};
返回值说明: 执行成功时,函数返回值为 0;
函数执行成功后,struct v4l2_capability 结构体变量中的返回当前视频设备所支持的功能;
例如支持视频捕获功能V4L2_CAP_VIDEO_CAPTURE、 V4L2_CAP_STREAMING等。
使用举例:
-------------------------------------------------------------------------------------------------------
struct v4l2_capability cap;
iret = ioctl(fd_usbcam, VIDIOC_QUERYCAP,&cap);
if(iret < 0){
       printf("get vidieo capability error,error code: %d \n",errno);
       return ;
}
------------------------------------------------------------------------------------------------------
执行完VIDIOC_QUERYCAP命令后,cap变量中包含了该视频设备的能力信息,程序中通过检查cap中的设备能力信息来判断设备是否支持某项功能。

三、基本I/O处理

如果有人在video for linux API规范上花了我时间的话,他肯定已经注意到了一个问题,那就是V4L2大量使用了ioctl接口。视频硬件有大量的可操作旋钮,可能比其它任何处设都要多。视频流要与许多参数相联系,而且有很大一部分处理要通过硬件进行。不使用硬件有良好支持模式可能导致表现不好,甚至根本没有表现。所以我们不得不揭露硬件的许多特性,而对最终应用表现得怪异一点。
传统上来讲,视频驱动中包含的ioctl()函数一般会长得像一部小说,而函数所得到的结论也往往比小说更令人满意来,他们往往在中间拖了很多(这句话完全不明白什么意思)。所以V4L2API2.6.18版本的内核开始做出了改变。冗长的ioctl函数被替换成了一个大的回调函数的集合,每个回调函数实现自己的ioctl函数。实际上,在2.6.19-rc3中,有79个这样的回调函数。而幸运的是,多数驱动并不需实现所有的回调函数,甚至都不是大部分回调函数。
ioctl()函数中发生的事情都放到了drivers/media/video/videodev.c里面。这部分代码处理数据在内核和用户空间之间的传输并把ioctl调用发送给驱动。要使用它的话,只要把video_device中的video_ioctl2()做为ioctl()来调用就行了。实际上,多数驱动也要把它当成unlocked_ioctl()来用。Video4Linux2层的锁可以对其进行处理,而且驱动也应该在合适的地方加锁。(这一段没看明白,乱写的)

 

四、输入输出

输入和输出 

这是不定期发布的关于写视频驱动程序的LWN系统文章的第四篇.没有看过介绍篇的,也许想从这里开始.本周的文章介绍的是应用程序如何确定在特定适配器上哪些输入和输出可用,并且它们之间做出选择。

在很多情况下,视频适配器并不能提供很多的输入输出选项.比如说摄像头控制器,可能只是提供摄像头,而没什么别的功能.然而,在一些其他的情况下,事情将变得很复杂.一个电视卡可能对应板上不用的接头有不同的输入.他甚至可能有可以独立发挥其功能的多路调谐器.有时,那些输入会有不同的特性;有些调谐器可以支持比其他的更广泛的视频标准.对于输出来说,也有同样的问题.

很明显,若想一个应用可以有效地利用视频适配器,它必须有能力找到可用的输入和输出,而且他必须能找到他想操作的那一个.为此,Video4Linux2 API提供三种不同的ioctl()调用来处理输入,相应地有三个来处理输出.

这三个(对于硬件支持的每一个功能)驱动都要支持.虽然如此,对于简单的硬件而言,代码还是很简单的.驱动也要提供一此启动时的默认值.然而,驱动不应该做的是,在应用退出时重置输入输出信息.对于其他视频参数,在多次打开之间,参数应维持不变.

视频标准 

在我们进入输入输出的细节之前,我们应该先了解一下视频标准 .这些标准描述的是视频为进行传输而做出的格式转换分辨率,帧频率等.这些标准通常是由每一个国家的监管部门制定的。现在世界上使标准主要的有三个:NTSC(主要是北美使用),PAL(主要是欧洲,非洲和中国),SECAM(,俄和非洲部分地区).然而这在标准在国家之间都有变化,而且有些设备比其他设备能更加灵活,能与更多的标准变种协同工作.

V4L2使用v4l2_std_id来代表视频标准,它是一个64位的掩码。每个标准变种在掩码中就是一位。所以标准NTSC就是V4L2_STD_NTSC_M, 值为0x1000,而日本的变种就是V4L2_STD_NTSC_M_JP
(0x2000)。如果一个设备可以处理所以有NTSC变种,它就可以设为V4L2_STD_NTSC,它可以所有相关位置位。对PALSECAM标准,也存在一组类似的位集。Seethis pagefor complete list.

对于用户空间而言,V4L2提供一个ioctl()命令(VIDIOC_ENUMSTD),它允许应用查询设备实现了哪些标准。驱动却无需直接回答查询,而是将video_device结构体的tvnorm字段设置为它所支持的所有标准。然后V4L2层会向应用输出所支持的标准。VIDIOC_G_STD命令,可以用来查询现在哪种标准是激活的,它也是在V4L2层通过返回video_device结构的current_norm字段来处理的,驱动程序应在启动时,初始化current_norm来反应现实情况。有些应用即使他并没有设置过标准,发现标准没有设置也会感到困惑。

当某个应用想要申请某个标准的时候,会发出一个VIDIOC_S_STD调用,该调用通过下面的函数传到驱动:

------------------------------------------------------------------------------------------------------

  int (*vidioc_s_std) (struct file *file, void *private_data,     v4l2_std_id std);

------------------------------------------------------------------------------------------------------------

驱动要对硬件编程,以使用给定的标准,并返回0(或是负的出错编码).V4L2层需要把current_norm设为新的值。

应用可能想要知道硬件所看到的是何种信号,答案可以通过VIDIOC_QUERYSTD找到,它到了驱动里面就是:

---------------------------------------------------------------------------------------------------------------int (*vidioc_querystd) (struct file *file, void *private_data,    v4l2_std_id *std);

------------------------------------------------------------------------------------------------------------驱动要尽可能地在这个字段填写详细信息。如果硬件没有提供足够的信息,std字段就会暗示任何可能出现的标准。

这里还有一点值得一提:所以的视频设备必须支持(或是声明支持)至少一种视频标准。视频标准对于摄像头来说没什么意义,它不与任何监管制度绑定。但是也不存一个标准说我是个摄像头,我什么都能做,所以V4L2层有很多摄像头声明可以返回PALNTSC数据(实际只是如些声明而己)。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值