怎样使用V4L2最常用的控制命令

嵌入式技术

1372人已加入

描述

Linux视频设备驱动常用控制命令使用说明

设置视频设备属性通过ioctl来进行设置,ioctl有三个参数,分别是fd, cmd,和parameter,表示设备描述符,控制命令和控制命令参数。

1. 控制命令VIDIOC_QUERYCAP

功能: 查询设备驱动的功能 ;

参数说明:参数类型为V4L2的能力描述类型struct v4l2_capability;

struct v4l2_capability {

__u8 driver[16]; /* i.e. “bttv” */ //驱动名称,

__u8 card[32]; /* i.e. “Hauppauge WinTV” */ //

__u8 bus_info[32]; /* “PCI:” + pci_name(pci_dev) */ //PCI总线信息

__u32 version; /* should use KERNEL_VERSION() */

__u32 capabilities; /* Device 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中的设备能力信息来判断设备是否支持某项功能。

2. 控制命令 VIDIOC_ENUM_FMT

功能: 获取当前视频设备支持的视频格式 。

参数说明:参数类型为V4L2的视频格式描述符类型 struct v4l2_fmtdesc

struct v4l2_fmtdesc {

__u32 index; /* Format number */

enum v4l2_buf_type type; /* buffer type */

__u32 flags;

__u8 description[32]; /* Description string */

__u32 pixelformat; /* Format fourcc */

__u32 reserved[4];

};

返回值说明: 执行成功时,函数返回值为 0;

struct v4l2_fmtdesc 结构体中的 .pixelformat和 .description 成员返回当前视频设备所支持的视频格式;

使用举例:

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

struct v4l2_fmtdesc fmt;

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

fmt.index = 0;

fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

while ((ret = ioctl(dev, VIDIOC_ENUM_FMT, &fmt)) == 0)

{

fmt.index++;

printf(“{ pixelformat = ‘’%c%c%c%c‘’, description = ‘’%s‘’ }\n”,

fmt.pixelformat & 0xFF, (fmt.pixelformat 》》 8) & 0xFF, (fmt.pixelformat 》》 16) & 0xFF,

(fmt.pixelformat 》》 24) & 0xFF, fmt.description);

}

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

3. 控制命令VIDIOC_S_FMT

功能: 设置视频设备的视频数据格式,例如设置视频图像数据的长、宽,图像格式(JPEG、YUYV格式);

参数说明:参数类型为V4L2的视频数据格式类型 struct v4l2_format;

struct v4l2_format {

enum v4l2_buf_type type; //数据流类型,必须永远是V4L2_BUF_TYPE_VIDEO_CAPTURE

union {

struct v4l2_pix_format pix; /* V4L2_BUF_TYPE_VIDEO_CAPTURE */

struct v4l2_window win; /* V4L2_BUF_TYPE_VIDEO_OVERLAY */

struct v4l2_vbi_format vbi; /* V4L2_BUF_TYPE_VBI_CAPTURE */

struct v4l2_sliced_vbi_format sliced; /* V4L2_BUF_TYPE_SLICED_VBI_CAPTURE */

__u8 raw_data[200]; /* user-defined */

} fmt;

};

struct v4l2_pix_format {

__u32 width; // 宽,必须是16的倍数

__u32 height; // 高,必须是16的倍数

__u32 pixelformat; // 视频数据存储类型,例如是YUV4:2:2还是RGB

enum v4l2_field field;

__u32 bytesperline;

__u32 sizeimage;

enum v4l2_colorspace colorspace;

__u32 priv;

};

返回值说明: 执行成功时,函数返回值为 0;

使用举例:

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

struct v4l2_format tv4l2_format;

tv4l2_format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

tv4l2_format.fmt.pix.width = img_width;

tv4l2_format.fmt.pix.height = img_height;

tv4l2_format.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;

tv4l2_format.fmt.pix.field = V4L2_FIELD_INTERLACED;

iret = ioctl(fd_usbcam, VIDIOC_S_FMT, &tv4l2_format);

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

注意:如果该视频设备驱动不支持你所设定的图像格式,视频驱动会重新修改struct v4l2_format结构体变量的值为该视频设备所支持的图像格式,所以在程序设计中,设定完所

有的视频格式后,要获取实际的视频格式,要重新读取 struct v4l2_format结构体变量。

使用VIDIOC_G_FMT设置视频设备的视频数据格式,VIDIOC_TRY_FMT验证视频设备的视频数据格式。

4. 控制命令VIDIOC_REQBUFS

功能: 请求V4L2驱动分配视频缓冲区(申请V4L2视频驱动分配内存),V4L2是视频设备的驱动层,位于内核空间,所以通过VIDIOC_REQBUFS控制命令字申请的内存位于内核空间,应

用程序不能直接访问,需要通过调用mmap内存映射函数把内核空间内存映射到用户空间后,应用程序通过访问用户空间地址来访问内核空间。

参数说明:参数类型为V4L2的申请缓冲区数据结构体类型struct v4l2_requestbuffers;

struct v4l2_requestbuffers {

u32 count; //缓存数量,也就是说在缓存队列里保持多少张照片

enum v4l2_buf_type type; //数据流类型,必须永远是V4L2_BUF_TYPE_VIDEO_CAPTURE

enum v4l2_memory memory; //V4L2_MEMORY_MMAP或V4L2_MEMORY_USERPTR

u32 reserved[2];

};

返回值说明: 执行成功时,函数返回值为 0,V4L2驱动层分配好了视频缓冲区;

使用举例:

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

struct v4l2_requestbuffers tV4L2_reqbuf;

memset(&tV4L2_reqbuf, 0, sizeof(struct v4l2_requestbuffers ));

tV4L2_reqbuf.count = 1; //申请缓冲区的个数

tV4L2_reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

tV4L2_reqbuf.memory = V4L2_MEMORY_MMAP; //mmap方式

iret = ioctl(fd_usbcam, VIDIOC_REQBUFS, &tV4L2_reqbuf);

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

注意:VIDIOC_REQBUFS会修改tV4L2_reqbuf的count值,tV4L2_reqbuf的count值返回实际申请成功的视频缓冲区数目;

5. 控制命令VIDIOC_QUERYBUF

功能: 查询已经分配的V4L2的视频缓冲区的相关信息,包括视频缓冲区的使用状态、在内核空间的偏移地址、缓冲区长度等。在应用程序设计中通过调 VIDIOC_QUERYBUF来获取内

核空间的视频缓冲区信息,然后调用函数mmap把内核空间地址映射到用户空间,这样应用程序才能够访问位于内核空间的视频缓冲区。

参数说明:参数类型为V4L2缓冲区数据结构类型 struct v4l2_buffer;

struct v4l2_buffer {

__u32 index;

enum v4l2_buf_type type;

__u32 bytesused;

__u32 flags;

enum v4l2_field field;

struct timeval timestamp;

struct v4l2_timecode timecode;

__u32 sequence;

/* memory location */

enum v4l2_memory memory;

union {

__u32 offset;

unsigned long userptr;

} m;

__u32 length;

__u32 input;

__u32 reserved;

};

返回值说明: 执行成功时,函数返回值为 0;

struct v4l2_buffer结构体变量中保存了指令的缓冲区的相关信息;一般情况下,应用程序中调用VIDIOC_QUERYBUF取得了内核缓冲区信息后,紧接着调用mmap函数把内核空间地址

映射到用户空间,方便用户空间应用程序的访问。

使用举例:

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

struct v4l2_buffer tV4L2buf;

memset(&tV4L2buf, 0, sizeof(struct v4l2_buffer));

tV4L2buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

tV4L2buf.memory = V4L2_MEMORY_MMAP;

tV4L2buf.index = i; // 要获取内核视频缓冲区的信息编号

iret = ioctl(fd_usbcam, VIDIOC_QUERYBUF, &tV4L2buf);

// 把内核空间缓冲区映射到用户空间缓冲区

AppBufLength = tV4L2buf.length;

AppBufStartAddr = mmap( NULL, /* start anywhere */

tV4L2buf.length,

PROT_READ | PROT_WRITE, /* access privilege */

MAP_SHARED, /* recommended */

fd_usbcam,

tV4L2buf.m.offset);

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

上述代码在通过调用VIDIOC_QUERYBUF取得内核空间的缓冲区信息后,接着调用mmap函数把内核空间缓冲区映射到用户空间;

6. 控制命令VIDIOC_QBUF

功能: 投放一个空的视频缓冲区到视频缓冲区输入队列中 ;

参数说明:参数类型为V4L2缓冲区数据结构类型 struct v4l2_buffer;

返回值说明: 执行成功时,函数返回值为 0;

函数执行成功后,指令(指定)的视频缓冲区进入视频输入队列,在启动视频设备拍摄图像时,相应的视频数据被保存到视频输入队列相应的视频缓冲区中。

使用举例:

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

struct v4l2_buffer tV4L2buf;

memset(&tV4L2buf, 0, sizeof(struct v4l2_buffer));

tV4L2buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

tV4L2buf.memory = V4L2_MEMORY_MMAP;

tV4L2buf.index = i; //指令(指定)要投放到视频输入队列中的内核空间视频缓冲区的编号;

iret = ioctl(fd_usbcam, VIDIOC_QBUF, &tV4L2buf);

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

7. 控制命令VIDIOC_DQBUF

功能: 从视频缓冲区的输出队列中取得一个已经保存有一帧视频数据的视频缓冲区;

参数说明:参数类型为V4L2缓冲区数据结构类型 struct v4l2_buffer;

返回值说明: 执行成功时,函数返回值为 0;函数执行成功后,相应的内核视频缓冲区中保存有当前拍摄到的视频数据,应用程序可以通过访问用户空间来读取该视频数据。(前面

已经通过调用函数 mmap做了用户空间和内核空间的内存映射)。

使用举例:

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

struct v4l2_buffer tV4L2buf;

memset(&tV4L2buf, 0, sizeof(struct v4l2_buffer));

tV4L2buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

tV4L2buf.memory = V4L2_MEMORY_MMAP;

iret = ioctl(fd_usbcam, VIDIOC_DQBUF, &tV4L2buf);

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

说明: VIDIOC_DQBUF命令结果, 使从队列删除的缓冲帧信息传给了此tVL2buf。

V4L2_buffer结构体的作用就相当于申请的缓冲帧的代理,找缓冲帧的都要先问问它,通过它来联系缓冲帧,起了中间桥梁的作用。

8. 控制命令VIDIOC_STREAMON

功能: 启动视频采集命令,应用程序调用VIDIOC_STREAMON启动视频采集命令后,视频设备驱动程序开始采集视频数据,并把采集到的视频数据保存到视频驱动的视频缓冲区中。

参数说明:参数类型为V4L2的视频缓冲区类型 enum v4l2_buf_type ;

enum v4l2_buf_type {

V4L2_BUF_TYPE_VIDEO_CAPTURE = 1,

V4L2_BUF_TYPE_VIDEO_OUTPUT = 2,

V4L2_BUF_TYPE_VIDEO_OVERLAY = 3,

V4L2_BUF_TYPE_VBI_CAPTURE = 4,

V4L2_BUF_TYPE_VBI_OUTPUT = 5,

V4L2_BUF_TYPE_SLICED_VBI_CAPTURE = 6,

V4L2_BUF_TYPE_SLICED_VBI_OUTPUT = 7,

#if 1

/* Experimental */

V4L2_BUF_TYPE_VIDEO_OUTPUT_OVERLAY = 8,

#endif

V4L2_BUF_TYPE_PRIVATE = 0x80,

};

返回值说明: 执行成功时,函数返回值为 0;函数执行成功后,视频设备驱动程序开始采集视频数据,此时应用程序一般通过调用select函数来判断一帧视频数据是否采集完成,

当视频设备驱动完成一帧视频数据采集并保存到视频缓冲区中时,select函数返回,应用程序接着可以读取视频数据;否则select函数阻塞直到视频数据采集完成。 Select函数的

使用请读者参考相关资料。

使用举例:

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

enum v4l2_buf_type v4l2type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

fd_set fds ;

struct timeval tv;

iret = ioctl(fd_usbcam, VIDIOC_STREAMON, &v4l2type);

FD_ZERO(&fds);

FD_SET(fd_usbcam, &fds);

tv.tv_sec = 2; /* Timeout. */

tv.tv_usec = 0;

iret = select(fd_usbcam+ 1, &fds, NULL, NULL, &tv);

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

9. 控制命令VIDIOC_STREAMOFF

功能: 停止视频采集命令,应用程序调用VIDIOC_ STREAMOFF停止视频采集命令后,视频设备驱动程序不在采集视频数据。

参数说明:参数类型为V4L2的视频缓冲区类型 enum v4l2_buf_type;

返回值说明: 执行成功时,函数返回值为 0;函数执行成功后,视频设备停止采集视频数据。

使用举例:

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

enum v4l2_buf_type v4l2type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

iret = ioctl(fd_usbcam, VIDIOC_STREAMOFF, &v4l2type);

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

10. 控制命令VIDIOC_QUERYSTD

功能:在亚洲,一般使用PAL(720X576)制式的摄像头,而欧洲一般使用NTSC(720X480),使用VIDIOC_QUERYSTD来检测:

参数说明:

返回值说明:

typedef __u64 v4l2_std_id;

使用举例:

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

v4l2_std_id std;

do{

ret= ioctl(fd, VIDIOC_QUERYSTD, &std);

} while(ret== -1 && errno== EAGAIN);

switch(std) {

caseV4L2_STD_NTSC:

//……

caseV4L2_STD_PAL:

//……

}

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

以上就是Linux 视频设备驱动V4L2最常用的控制命令使用说明,通过使用以上控制命令,可以完成一幅视频数据的采集过程。

V4L2更多的控制命令使用说明请参考:http://v4l2spec.bytesex.org/spec/book1.htm

下面是damo程序(经过实际验证,修改了网上的例程的错误)

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

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#define CAMERA_DEVICE “/dev/video0”

#define CAPTURE_FILE “frame.jpg”

#define VIDEO_WIDTH 640

#define VIDEO_HEIGHT 480

#define VIDEO_FORMAT V4L2_PIX_FMT_YUYV

#define BUFFER_COUNT 4

typedef struct VideoBuffer {

void *start;

size_t length;

} VideoBuffer;

VideoBuffer framebuf[BUFFER_COUNT]; //修改了错误,2012-5.21

int main()

{

int i, ret;

// 打开设备

int fd;

fd = open(CAMERA_DEVICE, O_RDWR, 0);

if (fd 《 0) {

printf(“Open %s failed\n”, CAMERA_DEVICE);

return -1;

}

// 获取驱动信息

struct v4l2_capability cap;

ret = ioctl(fd, VIDIOC_QUERYCAP, &cap);

if (ret 《 0) {

printf(“VIDIOC_QUERYCAP failed (%d)\n”, ret);

return ret;

}

// Print capability infomations

printf(“Capability Informations:\n”);

printf(“ driver: %s\n”, cap.driver);

printf(“ card: %s\n”, cap.card);

printf(“ bus_info: %s\n”, cap.bus_info);

printf(“ version: %08X\n”, cap.version);

printf(“ capabilities: %08X\n”, cap.capabilities);

// 设置视频格式

struct v4l2_format fmt;

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

fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

fmt.fmt.pix.width = VIDEO_WIDTH;

fmt.fmt.pix.height = VIDEO_HEIGHT;

fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;

fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;

ret = ioctl(fd, VIDIOC_S_FMT, &fmt);

if (ret 《 0) {

printf(“VIDIOC_S_FMT failed (%d)\n”, ret);

return ret;

}

// 获取视频格式

ret = ioctl(fd, VIDIOC_G_FMT, &fmt);

if (ret 《 0) {

printf(“VIDIOC_G_FMT failed (%d)\n”, ret);

return ret;

}

// Print Stream Format

printf(“Stream Format Informations:\n”);

printf(“ type: %d\n”, fmt.type);

printf(“ width: %d\n”, fmt.fmt.pix.width);

printf(“ height: %d\n”, fmt.fmt.pix.height);

char fmtstr[8];

memset(fmtstr, 0, 8);

memcpy(fmtstr, &fmt.fmt.pix.pixelformat, 4);

printf(“ pixelformat: %s\n”, fmtstr);

printf(“ field: %d\n”, fmt.fmt.pix.field);

printf(“ bytesperline: %d\n”, fmt.fmt.pix.bytesperline);

printf(“ sizeimage: %d\n”, fmt.fmt.pix.sizeimage);

printf(“ colorspace: %d\n”, fmt.fmt.pix.colorspace);

printf(“ priv: %d\n”, fmt.fmt.pix.priv);

printf(“ raw_date: %s\n”, fmt.fmt.raw_data);

// 请求分配内存

struct v4l2_requestbuffers reqbuf;

reqbuf.count = BUFFER_COUNT;

reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

reqbuf.memory = V4L2_MEMORY_MMAP;

ret = ioctl(fd , VIDIOC_REQBUFS, &reqbuf);

if(ret 《 0) {

printf(“VIDIOC_REQBUFS failed (%d)\n”, ret);

return ret;

}

// 获取空间

VideoBuffer* buffers = calloc( reqbuf.count, sizeof(*buffers) );

struct v4l2_buffer buf;

for (i = 0; i 《 reqbuf.count; i++)

{

buf.index = i;

buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

buf.memory = V4L2_MEMORY_MMAP;

ret = ioctl(fd , VIDIOC_QUERYBUF, &buf);

if(ret 《 0) {

printf(“VIDIOC_QUERYBUF (%d) failed (%d)\n”, i, ret);

return ret;

}

// mmap buffer

framebuf[i].length = buf.length;

framebuf[i].start = (char *) mmap(0, buf.length, PROT_READ|PROT_WRITE, MAP_SHARED, fd, buf.m.offset);

if (framebuf[i].start == MAP_FAILED) {

printf(“mmap (%d) failed: %s\n”, i, strerror(errno));

return -1;

}

// Queen buffer

ret = ioctl(fd , VIDIOC_QBUF, &buf);

if (ret 《 0) {

printf(“VIDIOC_QBUF (%d) failed (%d)\n”, i, ret);

return -1;

}

printf(“Frame buffer %d: address=0x%x, length=%d\n”, i, (unsigned int)framebuf[i].start, framebuf[i].length);

}

// 开始录制

enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

ret = ioctl(fd, VIDIOC_STREAMON, &type);

if (ret 《 0) {

printf(“VIDIOC_STREAMON failed (%d)\n”, ret);

return ret;

}

// Get frame

ret = ioctl(fd, VIDIOC_DQBUF, &buf);

if (ret 《 0) {

printf(“VIDIOC_DQBUF failed (%d)\n”, ret);

return ret;

}

// Process the frame

FILE *fp = fopen(CAPTURE_FILE, “wb”);

if (fp 《 0) {

printf(“open frame data file failed\n”);

return -1;

}

fwrite(framebuf[buf.index].start, 1, buf.length, fp);

fclose(fp);

printf(“Capture one frame saved in %s\n”, CAPTURE_FILE);

// Re-queen buffer

ret = ioctl(fd, VIDIOC_QBUF, &buf);

if (ret 《 0) {

printf(“VIDIOC_QBUF failed (%d)\n”, ret);

return ret;

}

// Release the resource

for (i=0; i《 4; i++)

{

munmap(framebuf[i].start, framebuf[i].length);

}

close(fd);

printf(“Camera test Done.\n”);

return 0;

}

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

附件:

void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

int munmap(void *start, size_t length);

参数说明:

——start:映射区的开始地址。

——length:映射区的长度。

——prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起

—PROT_EXEC //页内容可以被执行

—PROT_READ //页内容可以被读取

—PROT_WRITE //页可以被写入

—PROT_NONE //页不可访问

——flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体

—MAP_FIXED //使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失

败。并且起始地址必须落在页的边界上。

—MAP_SHARED //与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新。

—MAP_PRIVATE //建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。

—MAP_DENYWRITE //这个标志被忽略。

—MAP_EXECUTABLE //同上

—MAP_NORESERVE //不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起

段违例信号。

—MAP_LOCKED //锁定映射区的页面,从而防止页面被交换出内存。

—MAP_GROWSDOWN //用于堆栈,告诉内核VM系统,映射区可以向下扩展。

—MAP_ANONYMOUS //匿名映射,映射区不与任何文件关联。

—MAP_ANON //MAP_ANONYMOUS的别称,不再被使用。

—MAP_FILE //兼容标志,被忽略。

—MAP_32BIT //将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略。当前这个标志只在x86-64平台上得到支持。

—MAP_POPULATE //为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞。

—MAP_NONBLOCK //仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口。

——fd:有效的文件描述词。如果MAP_ANONYMOUS被设定,为了兼容问题,其值应为-1。

——offset:被映射对象内容的起点。

返回值:

成功执行时,mmap()返回被映射区的指针,munmap()返回0。

失败时,mmap()返回MAP_FAILED[其值为(void *)-1],munmap返回-1。errno被设为以下的某个值。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分