采用V4L2读取的USB摄像头

来源:互联网 发布:梦幻小号注册软件 编辑:程序博客网 时间:2024/06/05 07:49
   Video4linux2(简称V4L2),是linux中关于视频设备的内核驱动。在Linux中,视频设备被视为设备文件,可以像访问普通文件一样对其进行读写,摄像头在/dev/video0下。
 
使用read读取摄像头一帧
   最初参考了华恒提供的摄像头例程,采用V4L2方式来读取视频,设置了framebuffer并用read来读取视频,非常简洁,顺序为open, ioctl, read。其中 ioctl中的顺序大致是:查询摄像头设备能力(VIDIOC_QUERYCAP),查询和设置图像格式(VIDIOC_S_FMT),查询和设置framebuffer(VIDIOC_G_FBUF)。但是执行到ioctl(cam_fp, VIDIOC_G_FBUF, &fb) 这一句的时候出现了错误,另外使用read的时候也返回-1错误。
   当时第一反应是看看网络上别的读取视频的方法,看到一篇博文,提到“V4L2可以对多种设备编程,所以并不是所有API可以对所有设备编程,哪怕是同类型的设备,使用ioctl--VIDIOC_QUERYCAP去询问支持什么功能。”
http://blog.163.com/laorenyuhai126@126/blog/static/1935077920106154190356/
http://blog.sina.com.cn/s/blog_602f87700100znq7.html
7569
   头文件linux/videodev2.h和kernel头文件linux/videodev2.h中都有描述(我只列出部分):
#define V4L2_CAP_VIDEO_CAPTURE    0x00000001  
#define V4L2_CAP_VIDEO_OUTPUT     0x00000002  
#define V4L2_CAP_VIDEO_OVERLAY     0x00000004  
#define V4L2_CAP_VIDEO_OUTPUT_OVERLAY    0x00000200  
#define V4L2_CAP_AUDIO      0x00020000
 //这个设备是否支持 read() 和 write()这两个 I/O 操作函数
#define V4L2_CAP_READWRITE  0x01000000  
//这个设备是否支持 streaming I/O 操作函数
#define V4L2_CAP_STREAMING              0x04000000  
   我的程序中ioctl(cam_fp, VIDIOC_QUERYCAP, &cap) 返回的执行结果是0x4000001,是V4L2_CAP_VIDEO_CAPTURE和V4L2_CAP_STREAMING,没有V4L2_CAP_READWRITE功能,所以就不能使用read()来读取摄像头,只能用streaming方式了。而我之前试过的查询和设置framebuffer属性(VIDIOC_G_FBUF)也不能使用,究竟VIDIOC_G_FBUF需要什么样的能力才能使用它呢?
   看到linuxtv.org中对于VIDIOC_G_FBUF和VIDIOC_S_FBUF的说明:
Applications can use the VIDIOC_G_FBUF and VIDIOC_S_FBUF ioctl to get and set the framebuffer parameters for aVideo Overlay or Video Output Overlay (OSD). The type of overlay is implied by the device type (capture or output device) and can be determined with theVIDIOC_QUERYCAP ioctl. One /dev/videoN device must not support both kinds of overlay.
   更多内容见http://www.linuxtv.org/downloads/v4l-dvb-apis/vidioc-g-fbuf.html
   就是说这两个功能是输入或输出设备(比如摄像头或LCD)给Video Overlay或Video Output Overlay用的,可以用VIDIOC_QUERYCAP来查询是否具备Overlay能力。/dev/videoN设备只要有Video Overlay或Video Output Overlay其中之一,就可以使用VIDIOC_G_FBUF。显然我的摄像头不具备。
   vieo overlay不同于video capture,是指不需要对video信号的帧进行copy,在这个功能下会将采集到的 image放在视频设备的 meomory 中保存,并直接在屏幕上显示或将视频信号转化成显卡的VGA信号,具体过程就是将视频帧直接写入framebuffer中。Video overlay需要硬件的支持,必须是支持video overlay的camera才能使用这套overlay interface。
   因为video overlay直接使用linux 的framebuffer来显示捕获到的image,所以和capture相比它更具有效率,而不是将捕获到的image拷贝以后通过其他的方式(android surfaceflinger)来显示。Viedo overlay只用来preview,又被称为framebuffer overlay或previwing。  
   更多内容见http://blog.chinaunix.net/uid-26851094-id-3293288.html
   看到这里我才意识到,其实framebuffer相关的VIDIOC_G_FBUF和Video Overlay等和我读取摄像头基本没有关系,它们是起到跳过系统操作,直接连通输入(摄像头)输出(LCD)设备的作用。之前用过的华恒提供的摄像头模块可以使用framebuffer和read,我估计它还有V4L2_CAP_READWRITE能力。
   所以接下来的方向也明确了,看看streaming能力支持什么方式来读取视频。
 
利用内存映射mmap读取摄像头
   具有V4L2_CAP_STREAMING 和V4L2_CAP_VIDEO_CAPTURE能力的摄像头一般操作流程(视频设备):
1.打开设备文件。 int fd=open(”/dev/video0″,O_RDWR);
2.取得设备的capability,看看设备具有什么功能,比如是否具有视频输入,或者音频输入输出等。VIDIOC_QUERYCAP,struct v4l2_capability
3.设置视频帧格式,帧的格式个包括宽度和高度等。VIDIOC_S_STD,VIDIOC_S_FMT ,struct v4l2_format
4.向驱动申请帧缓冲,一般不超过5个。struct v4l2_requestbuffers
5.将申请到的帧缓冲映射到用户空间,这样就可以直接操作采集到的帧了,而不必去复制。Mmap
6.将申请到的帧缓冲全部入队列,以便存放采集到的数据.VIDIOC_QBUF,struct v4l2_buffer
7.开始视频的采集。VIDIOC_STREAMON
8.出队列以取得已采集数据的帧缓冲,取得原始采集数据。VIDIOC_DQBUF
9.将缓冲重新入队列尾,这样可以循环采集。VIDIOC_QBUF
10.停止视频的采集。VIDIOC_STREAMOFF
11.关闭视频设备并且munmap。close(fd);
   程序参考了以下博客,video4linux获取camera原始帧
http://leave001.blog.163.com/blog/static/162691293201252161440628/
   常用的结构体在内核目录include/linux/videodev2.h中定义。程序调通后我整理思路的时候发现了这2篇博文,写得很清晰。
http://www.cnblogs.com/hzhida/archive/2012/05/29/2524397.html
http://blog.sina.com.cn/s/blog_602f87700101bmvu.html
 
1. 打开摄像头设备
cam_fb=open(“/dev/videoX”, O_RDWR);
要注意的是先确认下驱动有没有安装,否则/dev下没有video节点。
2. 设置摄像头
2.1 查询设备能力
int ret;
struct v4l2_capability cap;
ret=ioctl(cam_fb, VIDIOC_QUERYCAP, &cap);
   使用ioctl VIDIOC_QUERYCAP 来查询当前driver是否合乎规范。因为V4L2要求所有driver 和Device都支持这个ioctl,所以可以通过这个ioctl是否成功来判断当前设备和dirver 是否支持V4L2规范,这样同时还能够得到设备足够的能力信息。
   执行成功返回0,并且会将v4l2_capability数据结构填充。要确保capabilities 具有V4L2_CAP_VIDEO_CAPTURE和V4L2_CAP_STREAMING能力,否则无法顺利设置和读取摄像头。
struct v4l2_capability
{
 __u8 driver[16];   //驱动名。通常为uvcvideo
 __u8 card[32];     // Device名。通常是摄像头厂家产品名字
 __u8 bus_info[32];  //在Bus系统中存放位置
 __u32 version;      //driver 版本
 __u32 capabilities;  //能力集
 __u32 reserved[4];
}; 
2.1 枚举设备支持的图像格式
struct v4l2_fmtdesc fmtdesc;
fmtdesc.index=0;
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
do{
ret=ioctl(cam_fb, VIDIOC_ENUM_FMT, &fmtdesc);
                        printf("index %d, format 0x%x ", fmtdesc.index,  fmtdesc.pixelformat);   
                        fmtdesc.index++;
}while(0==ret);
   type 字段描述的是完成的I/O操作的类型。通常它的值要么是视频获得设备的V4L2_BUF_TYPE_VIDEO_CAPTURE ,要么是输出设备的V4L2_BUF_TYPE_VIDEO_OUTPUT。将帧类型设置为V4L2_BUF_TYPE_VIDEO_CAPTURE,并将index从0往上依次累加,使用VIDIOC_ENUM_FMT来枚举设备支持的图像格式,成功返回0,并自动将结构中的pixelformat等数据填充。循环执行一直到ioctl返回非零则设备支持的格式枚举完毕。我的设备只支持一种格式,pixelformat为0x56595559,根据v4l2_fourcc 计算,是YUYV格式。        
struct v4l2_fmtdesc
{
u32 index; // 要查询的格式序号,应用程序设置
enum v4l2_buf_type type; // 帧类型,应用程序设置,V4L2_BUF_TYPE_VIDEO_CAPTURE
u32 flags; // 是否为压缩格式
u8 description[32]; // 格式名称
u32 pixelformat; // 格式
u32 reserved[4]; // 保留
}; 
   具体pixelformat的定义如下,
#define v4l2_fourcc(a, b, c, d) (__u32)(a) ((__u32)(b) << 8) ((__u32)(c) << 16) ((__u32)(d) << 24))  
   列出部分格式如下,更多内容见链接http://blog.csdn.net/myarrow/article/details/8516266 
#define V4L2_PIX_FMT_YVU410  v4l2_fourcc('Y', 'V', 'U', '9')    
#define V4L2_PIX_FMT_YVU420  v4l2_fourcc('Y', 'V', '1', '2')    
#define V4L2_PIX_FMT_YUYV    v4l2_fourcc('Y', 'U', 'Y', 'V')    
#define V4L2_PIX_FMT_YYUV    v4l2_fourcc('Y', 'Y', 'U', 'V')    
#define V4L2_PIX_FMT_YVYU    v4l2_fourcc('Y', 'V', 'Y', 'U')    
2.2 设置帧格式
struct v4l2_format format;
memset(&format, 0, sizeof(struct v4l2_format));
format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
format.fmt.pix.width =  width;//自己指定
format.fmt.pix.height = height;// 自己指定
format.fmt.pix.pixelformat=pixelFormat;// 自己指定,但一定是设备支持的格式; 
ioctl(cam_fb, VIDIOC_TRY_FMT, &format);
ioctl(cam_fb, VIDIOC_S_FMT, &format); 
   VIDIOC_TRY_FMT、VIDIOC_S_FMT和VIDIOC_G_FMT的说明:
   VIDIOC_G_FMT与VIDIOC_S_FMT很好理解,一个显然是获取当前格式,另一个设置格式。但是这里突然冒出来的VIDIOC_TRY_FMT,它的作用是什么,是否必须。我最初使用它是因为看到一篇博文中的程序用到VIDIOC_TRY_FMT,说ioctl(VIDIOC_TRY_FMT)一定不可少,否则后面的VIDIOC_REQBUFS返回的buffer.m.offset和buffer.length都为0,导致mmap失败。于是我查了这几个操作的原始说明,原文不复制了,简述如下:
   使用ioctl(cam_fb, VIDIOC_G_FMT, &format),驱动会自己填充v4l2_format这个结构中的fmt单元。VIDIOC_S_FMT则是根据硬件本身具有的能力来检查并改变当前参数。最好先查询设备的能力,选择硬件和你的程序都接受的参数;使用VIDIOC_G_FMT查询当前格式然后再修改其中的一部分参数。有的设备坚持使用自己的默认设置,所以你使用VIDIOC_S_FMT来设置自己期望的格式后其实格式并未改变。
   当v4l2_format中的type不被支持,这两个操作才会返回错误码EINVAL。当设备已经在使用中,使用VIDIOC_S_FMT会返回EBUSY错误码。
   VIDIOC_TRY_FMT不会改变驱动的状态,除此之外它和VIDIOC_S_FMT是等效的,并且它可以随时被调用,它还可以用来交涉参数,了解硬件的限制。不过主流驱动没有规定一定要支持这个VIDIOC_TRY_FMT能力。
   原文:http://www.linuxtv.org/downloads/v4l-dvb-apis/vidioc-g-fmt.html
   帧的格式:
struct v4l2_pix_format  
    __u32                   width;  
    __u32                   height;  
    __u32                   pixelformat;  
    enum v4l2_field         field;  
    __u32                   bytesperline;     
    __u32                   sizeimage;  
    enum v4l2_colorspace    colorspace;  
    __u32                   priv;             
};  
2.3 申请帧缓冲
struct v4l2_requestbuffers
{
 __u32 count; //缓冲区数目, 即在缓存队列里存几帧,V4L2_MEMORY_MMAP时,此处才有效
 enum v4l2_buf_type type;// 应用程序设置,V4L2_BUF_TYPE_VIDEO_CAPTURE
 enum v4l2_memory memory;// 应用程序设置, V4L2_MEMORY_MMAP
 __u32 reserved[2];
};
enum v4l2_memory {
 V4L2_MEMORY_MMAP = 1,
 V4L2_MEMORY_USERPTR = 2,
 V4L2_MEMORY_OVERLAY = 3,
};                  
   有两种方式的I/O。 Memory Mapping 和User Pointer,这里只讲前者。Memory Mapping的Buffer由Driver申请为物理连续的内存空间(Kernel空间),在此ioctl调用时被分配,需要早于mmap()动作(将他们映射到用户空间)。
   注意:count是个输入输出函数。因为你所申请到的Buffer个数不一定就是你所输入的Number。所以在ioctl执行后,driver会将真实申请到的buffer个数填充到此field. 这个数目有可能大于你想要申请的,也可能小于,甚至可能是0个,所以执行完毕后要再确认一下数目是否不小于你想要申请的。
   应用程序可以再次调用ioctl--VIDIOC_REQBUFS 来修改buffer个数。但前提是必须先释放已经 mapped 的 buffer ,可以先 munmap ,然后设置参数 count 为 来释放所有的 buffer。
   支持Memory  Mapping  I/O方式的前提是:v4l2_capability  中支持V4L2_CAP_STREAMING。
   在这个模式下,数据本身不会被Copy,只是在Kernel和用户态之间交换。在应用程序想要访问到这些数据之前,它必须调用mmap()影射到用户态。
同时也要注意,通过ioctl申请的内存,是物理内存,无法被交换入Disk,所以一定要释放:munmap()。
struct v4l2_requestbuffers req;
unsigned char *video_buffer_ptr[BUFFER_COUNT];
req.count = BUFFER_COUNT;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;   
req.memory = V4L2_MEMORY_MMAP;   
ret = ioctl(cam_fb, VIDIOC_REQBUFS, &req);   
if (ret != 0 || req.count < BUFFER_COUNT)
{
        printf("VIDIOC_REQBUFS error\n");
        goto err;
}
   补充:最初“video4linux获取camera原始帧”中提到,map的buffer数必须是3,若是2,则会导致kernel crash。这大概是驱动的bug。我觉得这现象应该只是个例,不过还是试验了一下,从1到7,都成功了没有出现内核崩掉的情况。
2.4 内存映射
   使用VIDIOC_REQBUFS,我们获取了req.count个缓存,下一步通过调用VIDIOC_QUERYBUF命令来获取这些缓存的地址,然后使用mmap函数转换成应用程序中的绝对地址,最后把这段缓存放入缓存队列。
struct v4l2_buffer
{
 __u32 index;// 应用程序设置0到count-1.
 enum v4l2_buf_type type;// 应用程序设置V4L2_BUF_TYPE_VIDEO_CAPTURE
 __u32 bytesused;
 __u32 flags;
 enum v4l2_field field;
 struct timeval timestamp;
 struct v4l2_timecode timecode;
 __u32 sequence;
 enum v4l2_memory memory;
 union {
                __u32 offset;
                unsigned long userptr;
               } m;
 __u32 length;
 __u32 input;
 __u32 reserved;
};
   在调用ioctl-VIDIOC_QUERYBUF后,Driver会填充v4l2_buffer 结构体内所有信息供用户使用。如果一切正常:
flags中V4L2_BUF_FLAG_MAPPED、 V4L2_BUF_FLAG_QUEUED 和 V4L2_BUF_FLAG_DONE被设置;
memory中V4L2_MEMORY_MMAP被设置;
m.offset中,从将要mapping 的device memory头到数据头的offset;
length 中,填充当前Buffer长度;
其它的Field有可能设置,也有可能不被设置。
   这样,mmap()想要有的信息就全了。而mmap()之后,Device Driver 申请的或者Device Memory就能映射到用户空间。数据就可以被应用程序使用了。这才是ioctl-VIDIOC_QUERYBUF的关键作用。
mmap操作提供了一种机制,让用户程序直接访问设备内存,这种机制,相比较在用户空间和内核空间互相拷贝数据,效率更高。在要求高性能的应用中比较常用。mmap映射内存必须是页面大小的整数倍,面向流的设备不能进行mmap,mmap的实现和硬件有关。
struct v4l2_buffer buffer;
memset(&buffer, 0, sizeof(buffer));
buffer.type = req.type;   
buffer.memory = V4L2_MEMORY_MMAP;   
for (i=0; i
      
        buffer.index = i;       
        //获取缓存的地址
tmp = ioctl (cam_fb, VIDIOC_QUERYBUF, &buffer);       
    //转换成应用程序中的绝对地址
    video_buffer_ptr[i] = (unsigned char*) mmap(NULL, buffer.length, PROT_READ, MAP_SHARED, cam_fb, buffer.m.offset);       
        if (video_buffer_ptr[i] == MAP_FAILED)       
        {            
               printf("mmap() failed\n");           
               goto err;     
           
 
        buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;       
        buffer.memory = V4L2_MEMORY_MMAP;       
        buffer.index = i;
     //把这段缓存放入缓存队列
        ret = ioctl(cam_fb, VIDIOC_QBUF, &buffer);       
        if (ret != 0)       
                 
               printf("ioctl(VIDIOC_QBUF) failed\n");           
               goto err;     
         
2.5 开始捕获
int buffer_type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = ioctl(cam_fb, VIDIOC_STREAMON, &buffer_type);  
 
3 读取摄像头一帧
   操作系统一般把系统使用的内存划分成用户空间和内核空间,分别由应用程序管理和操作系统管理。应用程序可以直接访问内存的地址,而内核空间存放的是供内核访问的代码和数据,用户不能直接访问。v4l2捕获的数据,最初是存放在内核空间的,这意味着用户不能直接访问该段内存,必须通过某些手段来转换地址。
   一共有三种视频采集方式:使用read/write方式;内存映射方式和用户指针模式。
   read、write方式,在用户空间和内核空间不断拷贝数据,占用了大量用户内存空间,效率不高,也需要摄像头能力的支持。(之前华恒的板子就是用这个方式)
   内存映射方式:把设备里的内存映射到应用程序中的内存控件,直接处理设备内存,这是一种有效的方式。上面的mmap函数就是使用这种方式。
   用户指针模式:内存片段由应用程序自己分配。这点需要在v4l2_requestbuffers里将memory字段设置成V4L2_MEMORY_USERPTR。
 
   V4L2有一个数据缓存,存放req.count数量的缓存数据。数据缓存采用FIFO的方式,当应用程序调用缓存数据时,缓存队列将最先采集到的视频数据缓存送出,并重新采集一张视频数据。对Camera这样的捕获设备来说,Device将数据放到Buffer中,用户得到数据。Device再次将数据放到Buffer中。
   那么Device Driver 怎样知道哪个Buffer是可以存放数据的呢?这就用到当前这两个ioctl-VIDIOC_QBUF, ioctl-VIDIOC_DQBUF.
ioctl-VIDIOC_QBUF: 将指定的Buffer放到输入队列中,即向Device表明这个Buffer可以存放东西。
ioctl-VIDIOC_DQBUF: 将输出队列中的数据 buffer取出。
   在 driver 内部管理着两个 buffer queues ,一个输入队列,一个输出队列。对于 capture device 来说,当输入队列中的 buffer 被塞满数据以后会自动变为输出队列,等待调用 VIDIOC_DQBUF 将数据进行处理以后重新调用 VIDIOC_QBUF 将 buffer 重新放进输入队列。 
struct v4l2_buffer buf;
memset(&buf,0,sizeof(buf));
buf.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory=V4L2_MEMORY_MMAP;
buf.index=0;
if (ioctl(cameraFd, VIDIOC_DQBUF, &buf) == -1) return -1;//读取缓存
if (ioctl(cam_fb, VIDIOC_QBUF, &buf) == -1) return -1; //重新放入缓存队列
   从输出队列中取出一个有数据的Buffer。这个Buffer中的数据被处理后,此Buffer可以通过ioctl-VIDIOC_QBUF再次放入输入队列中去。
  
4 内存映射的补充说明
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被设为以下的某个值。
0 0