linux下通过V4L2驱动USB摄像头

时间:2022-11-24 17:26:03

目录

前言

在移植罗技C270摄像头到6818的过程中,内核已经检测到了USB摄像头,但是直接用OpenCV的API(比如CvCapture*cvCaptureFromCAM(int index)接口,无法打开USB摄像头,至少目前我是这么认为的。然后,网上搜索答案说是要使用V4l2进行操作。没有别的办法!只有一边学一边试试看行不行喽!

感谢超群天晴大神的(原创)基于ZedBoard的Webcam设计(一):USB摄像头(V4L2接口)的图片采集这篇博文中提供的源码,我直接移植后,在6818是成功获取bmp图片和yuv图片。虽然在Ubuntu中打开bmp图片和yuv图片是错误的!!!

在找到超群天晴的博文后,我又找到这两篇博客和菜鸟一起学linux之V4L2摄像头应用流程嵌入式LINUX环境下视频采集知识验证成功后,就是花时间理解,然后获取视频流了!希望获取的视频流能够流畅!

学习!分享!感谢!

v4l2解析

v4l2介绍

v4l2是linux操作系统下用于采集图片、视频和音频数据的API接口,配合适当的视频采集设备和相应的驱动程序,可以实现图片、视频、音频等的采集。在远程会议、可视电话、视频监控系统和嵌入式多媒体终端中都有广泛应用。
在linux下,所有外设都被看成一种特殊的文件,称为”设备文件”,可以像访问普通文件一样对设备文件进行访问。
V4L2支持两种方式来采集图像:内存映射(mmap)和直接读取方式(read)。V4L2在include/linux/video.h文件下定义了一些重要的数据结构,在采集图像的过程中,就是通过对这些数据的操作来获得最终的图像数据。Linux系统V4L2使能可在内核编译阶段配置,默认情况下是在make menuconfig是打开的。

linux下视频捕捉具体的linux调用参见下图:
linux下通过V4L2驱动USB摄像头

应用程序可以通过V4L2进行视频采集。V4L2支持内存映射(mmap)方式和直接读取方式(read)方式采集数据。前者一般用于连续的视频数据采集,后者常用静态图片数据采集。

v4l2 中不仅定义了通用API元素,图像的格式,输入/输出方法,还定义了Linux内核驱动处理视频信息的一系列接口,这些接口主要有:

视频采集接口——Video Capture interface
视频输出接口 ——Video Output Interface;
视频覆盖/预览接口——Video Overlay Interface;
视频输出覆盖接口—— Video Output Overlay Interface;
编解码接口——Codec Interface

应用程序通过V4L2接口采集视频数据步骤

  • 打开视频设备文件,通过视频采集的参数初始化,通过V4L2接口设置视频图像属性。
  • 申请若干视频采集的帧缓存区,并将这些帧缓冲区从内核空间映射到用户空间,便于应用程序读取/处理视频数据。
  • 将申请到的帧缓冲区在视频采集输入队列排队,并启动视频采集。
  • 驱动开始视频数据的采集,应用程序从视频采集输出队列中取出帧缓冲区,处理后,将帧缓冲区重新放入视频采集输入队列,循环往复采集连续的视频数据。
  • 停止视频采集。
    具体实现过程如下图:
    linux下通过V4L2驱动USB摄像头

  • 整理下其中ioctl控制符:

VIDIOC_QUERYCAP 查询设备的属性
VIDIOC_ENUM_FMT 帧格式
VIDIOC_S_FMT 设置视频帧格式,对应struct v4l2_format
VIDIOC_G_FMT 获取视频帧格式等
VIDIOC_REQBUFS 请求/申请若干个帧缓冲区,一般为不少于3
VIDIOC_QUERYBUF 查询帧缓冲区在内核空间的长度和偏移量
VIDIOC_QBUF 将申请到的帧缓冲区全部放入视频采集输出队列
VIDIOC_STREAMON 开始视频流数据的采集
VIDIOC_DQBUF 应用程序从视频采集输出队列中取出已含有采集数据的帧缓冲区
VIDIOC_STREAMOFF 应用程序将该帧缓冲区重新挂入输入队列

其实,图解过程已经很详细了,但是比较笨,而且图片也比较容易眼花,重新总结下。整个过程:
首先:先启动视频采集,驱动程序开始采集一帧数据,把采集的数据放入视频采集输入队列的第一个帧缓冲区,一帧数据采集完成,也就是第一个帧缓冲区存满一帧数据后,驱动程序将该帧缓冲区移至视频采集输出队列,等待应用程序从输出队列取出。驱动程序则继续采集下一帧数据放入第二个缓冲区,同样帧缓冲区存满下一帧数据后,被放入视频采集输出队列。
然后:应用程序从视频采集输出队列中取出含有视频数据的帧缓冲区,处理帧缓冲区中的视频数据,如存储或压缩。
最后:应用程序将处理完数据的帧缓冲区重新放入视频采集输入队列,这样可以循环采集。
看到这里,摘录一段Hi3519的开发文档中《系统控制》部分的介绍:

视频缓存池主要向媒体业务提供大块物理内存管理功能,负责内存的分配和回收,充分发挥内存缓存池的作用,让物理内存资源在各个媒体处理模块中合理使用。
一组大小相同、物理地址连续的缓存块组成一个视频缓存池。
视频输入通道需要使用公共视频缓存池。所有的视频输入通道都可以从公共视频缓存池中获取视频缓存块用于保存采集的图像。由于视频输入通道不提供创建爱你和销毁公共视频缓存池功能。因此,在系统初始化之前,必须为视频输入通道配置公共缓存池。根据业务的不同,公共缓存池的数量、缓存块的大小和数量不同。缓存块的生存期是指经过VPSS通道传给后续模块的情形。如果该缓存块完全没有经过VPSS通道传给其他模块,则将在VPSS模块处理后被放回公共缓存池。
linux下通过V4L2驱动USB摄像头
所以,我们从摄像头中获取的视频帧数据会放入视频缓存队列中,当其他模块需要处理对应的视频帧的时候,就会占用缓存块,也就是这一块内存被占用,当处理完之后,对应的数据通过VO/VENC/VDA显示之后,这一缓存块就没有用了,可以回收利用。现在来看,其实海思的底层处理和linux的底层处理是一样的。不过海思本身使用的就是linux内核。应该也就是对这一块进行封装了而已吧!
linux下通过V4L2驱动USB摄像头
从这张图可以看出,海思的公共视频缓存池按我的理解应该有两部分,一部分是视频采集输入队列,另一部分是视频采集输出队列,VI通道是是视频采集输出队列中获取的视频帧,而中间linux内核的驱动程序会在视频采集输入队列中填充视频帧,变成视频输出队列。
每一个帧缓冲区都有一个对应的状态标志变量,其中每一个比特代表一个状态:

V4L2_BUF_FLAG_UNMAPPED  0B0000
V4L2_BUF_FLAG_MAPPED 0B0001
V4L2_BUF_FLAG_ENQUEUED 0B0010
V4L2_BUF_FLAG_DONE 0B0100

缓冲区的状态转化如图:
linux下通过V4L2驱动USB摄像头
在申请了缓冲区(VIDIOC_REQBUFS)后标志为0000,但是这时候还没有映射到应用层,所以在Mmap之后,缓冲区的表示为0001。
应用程序(VIDIOC_DQBUF)从视频采集输出队列中取出已含有采集数据的帧缓冲区,这时候(V4L2_BUF_FLAG_DONE|V4L2_BUF_FLAG_MAPPED),所以缓冲区标志为0101.
VIDIOC_QBUF将申请到的帧缓冲区全部放入视频采集输出队列:V4L2_BUF_FLAG_ENQUEUED|V4L2_BUF_FLAG_MAPPED

相关结构体解析

  1. VIDIOC_QUERYCAP——–>struct v4l2_capability
struct v4l2_capability {
__u8 driver[16]; // 驱动模块的名字
__u8 card[32]; // 设备名字
__u8 bus_info[32]; // 总线信息
__u32 version; // 内核版本
__u32 capabilities; // 整个物理设备支持的功能
__u32 device_caps; // 通过这个特定设备访问的功能
__u32 reserved[3];
};

如下是我的罗技C270摄像头的通过VIDIOC_QUERYCAP获取的设备功能

driver:         uvcvideo
card: UVC Camera (046d:0825)
bus_info: usb-nxp-ehci-1.3
version: 197671
capabilities: 4000001
Device /dev/video9: supports capture.
Device /dev/video9: supports streaming.

其中capabilities: 4000001通过与各种宏位与,可以获得物理设备的功能属性。比如:

        //#define V4L2_CAP_VIDEO_CAPTURE 0x00000001 /* Is a video capture device */ // 是否支持视频捕获
if ((cap.capabilities & V4L2_CAP_VIDEO_CAPTURE) == V4L2_CAP_VIDEO_CAPTURE)
{
printf("Device %s: supports capture.\n", FILE_VIDEO);
}

//#define V4L2_CAP_STREAMING 0x04000000 /* streaming I/O ioctls */ // 是否支持输入输出流控制
if ((cap.capabilities & V4L2_CAP_STREAMING) == V4L2_CAP_STREAMING)
{
printf("Device %s: supports streaming.\n", FILE_VIDEO);
}
  1. VIDIOC_ENUM_FMT——–>struct v4l2_fmtdesc
/*
* F O R M A T E N U M E R A T I O N
*/

struct v4l2_fmtdesc {
__u32 index; /* Format number */
__u32 type; /* enum v4l2_buf_type */
__u32 flags;
__u8 description[32]; /* Description string */
__u32 pixelformat; /* Format fourcc */
__u32 reserved[4];
};

通过这个结构体,可以显示对应的摄像头所支持视频帧格式。

struct v4l2_fmtdesc fmtdesc;  
fmtdesc.index = 0;
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
printf("Supportformat:/n");
while(ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc) != -1)
{
printf("/t%d.%s/n",fmtdesc.index+1,fmtdesc.description);
fmtdesc.index++;
}

我的摄像头输出如下:

Support format:
1.YUV 4:2:2 (YUYV)
2.MJPEG

所以,我要读取视频帧的时候就要使用YUV422这种格式。
3. VIDIOC_S_FMT&VIDIOC_G_FMT——–>struct v4l2_format
查看或设置视频帧格式

struct v4l2_format {
__u32 type; // 帧类型
union {
/* V4L2_BUF_TYPE_VIDEO_CAPTURE */
struct v4l2_pix_format pix; //像素格式
/* V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE */
struct v4l2_pix_format_mplane pix_mp;
/* V4L2_BUF_TYPE_VIDEO_OVERLAY */
struct v4l2_window win;
/* V4L2_BUF_TYPE_VBI_CAPTURE */
struct v4l2_vbi_format vbi;
/* V4L2_BUF_TYPE_SLICED_VBI_CAPTURE */
struct v4l2_sliced_vbi_format sliced;
/* V4L2_BUF_TYPE_SDR_CAPTURE */
struct v4l2_sdr_format sdr;
/* user-defined */
__u8 raw_data[200];
} fmt;
};
/*
* V I D E O I M A G E F O R M A T
*/

struct v4l2_pix_format {
__u32 width; // 像素高度
__u32 height; // 像素宽度
__u32 pixelformat; // 像素格式
__u32 field; /* enum v4l2_field */
__u32 bytesperline; /* for padding, zero if unused */
__u32 sizeimage;
__u32 colorspace; /* enum v4l2_colorspace */
__u32 priv; /* private data, depends on pixelformat */
__u32 flags; /* format flags (V4L2_PIX_FMT_FLAG_*) */
__u32 ycbcr_enc; /* enum v4l2_ycbcr_encoding */
__u32 quantization; /* enum v4l2_quantization */
__u32 xfer_func; /* enum v4l2_xfer_func */
};

仔细看,发现这些其实和海思中的一些结构体非常类似,linux真的很强大!
4. VIDIOC_CROPCAP——–>struct v4l2_cropcap
5. VIDIOC_G_PARM&VIDIOC_S_PARM——–>struct v4l2_streamparm
设置Stream信息,主要设置帧率

struct v4l2_streamparm {
__u32 type; /* enum v4l2_buf_type */
union {
struct v4l2_captureparm capture;
struct v4l2_outputparm output;
__u8 raw_data[200]; /* user-defined */
} parm;
};
struct v4l2_captureparm {
/* Supported modes */
__u32 capability;
/* Current mode */
__u32 capturemode;
/* Time per frame in seconds */
struct v4l2_fract timeperframe;
/* Driver-specific extensions */
__u32 extendedmode;
/* # of buffers for read */
__u32 readbuffers;
__u32 reserved[4];
};
// timeperframe
// numerator和denominator所描述的系数给出的是成功的帧之间的时间间隔
struct v4l2_fract {
__u32 numerator; // 分子
__u32 denominator; // 分母
};
  1. VIDIOC_REQBUFS——–>struct v4l2_requestbuffers
    申请和管理缓冲区,应用程序和设备有三种交换数据方法,直接read/write(裸机)、内存映射(系统),用户指针(…)。一般在操作系统管理下,都是使用内存映射的方式。
/*
* M E M O R Y - M A P P I N G B U F F E R S
*/

struct v4l2_requestbuffers {
__u32 count; // 缓冲区内缓冲帧的数目
__u32 type; // 缓冲帧数据格式
__u32 memory; //
__u32 reserved[2];
};

enum v4l2_memory {
V4L2_MEMORY_MMAP = 1, // 内存映射
V4L2_MEMORY_USERPTR = 2, // 用户指针
V4L2_MEMORY_OVERLAY = 3,
V4L2_MEMORY_DMABUF = 4,
};
  1. VIDIOC_QUERYBUF——–>struct v4l2_buffer
struct v4l2_buffer {
__u32 index; // buffer的id
__u32 type; // enum v4l2_buf_type
__u32 bytesused; // buf中已经使用的字节数
__u32 flags; // MMAP 或 USERPTR
__u32 field;
struct timeval timestamp; // 帧时间戳
struct v4l2_timecode timecode;
__u32 sequence; // 队列中的序号

/* memory location */
__u32 memory;
union {
__u32 offset; // 设备内存起始offset
unsigned long userptr; // 指向用户空间的指针
struct v4l2_plane *planes;
__s32 fd;
} m;
__u32 length; // 缓存帧长度
__u32 reserved2;
__u32 reserved;
};
  1. VIDIOC_DQBUF
    应用程序从视频采集输出队列中取出已含有采集数据的帧缓冲区

  2. VIDIOC_STREAMON&VIDIOC_STREAMOFF
    开始视频采集和关闭视频采集

  3. VIDIOC_QBUF
    应用程序将该帧缓冲区重新挂入输入队列

总结

使用C语言高级应用—操作linux下V4L2摄像头应用程序源码成功的在我的开发板上显示出了800*600的视频图像。我的开发板的显示屏是1024*600的,当我设置为1024*600时,实际显示为1024*576,感觉很奇怪!而且显示的视频是反的,都是小问题。总之,显示视频的那一刻真的好开心!

参考链接

和菜鸟一起学linux之V4L2摄像头应用流程
(原创)基于ZedBoard的Webcam设计(一):USB摄像头(V4L2接口)的图片采集
V4L2 API详解 <二> Camera详细设置
V4L2 API
V4L2采集yuv视频花屏:Linux视频采集与编码(一)
C语言高级应用—操作linux下V4L2摄像头应用程序
v4l2 编程接口(一) — ioctl
capture.c