排版 | 嵌入式应用研究院
素材来源 |CSDN
vl42是video for Linux 2的缩写,是一套Linux内核视频设备的驱动框架,该驱动框架为应用层提供一套统一的操作接口(一系列的ioctl)
V4L2在设计时,是要支持很多广泛的设备的,它们之中只有一部分在本质上是真正的视频设备,可以支持多种设备,它可以有以下几种接口
video capture interface:视频采集接口,这种接口应用于摄像头,v4l2在最初设计的时候就是应用于这种功能
video output interface:视频输出接口,将静止图像或图像序列编码为模拟视频信号,通过此接口,应用程序可以控制编码过程并将图像从用户空间移动到驱动程序
video overlay interface:视频直接传输接口,可以将采集到的视频数据直接传输到显示设备,不需要cpu参与,这种方式的显示图像的效率比其他方式高得多
其他接口这里就不介绍了,下面来看一下v4l2的API
对V4L2设备进行编程包括以下步骤
其中大多数操作都是通过应用层调用ioctl实现的,可以将这些ioctl分为下面几类
由于V4L2涵盖了各种各样的设备,因此并非API的所有方面都适用于所有类型的设备,在使用v4l2设备时,必须调用此API,获得设备支持的功能(capture、output、overlay…)
注:可以点击名称查看API讲解
当多个应用程序共享设备时,可能需要为它们分配不同的优先级。视频录制应用程序可以例如阻止其他应用程序改变视频控制或切换当前的电视频道。另一个目标是允许在后台工作的低优先级应用程序,这些应用程序可以被用户控制的应用程序抢占,并在以后自动重新获得对设备的控制
图像由多种格式YUV和RGB还有压缩格式等等,其中每种格式又分有多种格式,比如RGB:RGB565、RGB888…所以在使用设备时,需要对格式进行设置
内核中使用缓存队列对图像数据进行管理,用户空间获取图像数据有两种方式,一种是通过read、write方式读取内核空间的缓存,一种是将内核空间的缓存映射到用户空间。在操作v4l2设备时,通过VIDIOC_QUERYCAP获取设备支持哪种方式
ioctl API就先介绍到这里,还有非常多的接口这里就不一一介绍了,具体可以查看V4L2 Function Reference;下面来讲一讲如何使用这些接口
V4L2支持多种接口:capture(捕获)、output(输出)、overlay(预览)等等 这里讲解如何使用capture功能,下面讲解操作流程
int fd = open(name, flag);
if(fd < 0)
{
printf("ERR(%s):failed to open %s\n", __func__, name);
return -1;
}
return fd;
if (ioctl(fd, VIDIOC_QUERYCAP, cap) < 0)
{
printf("ERR(%s):VIDIOC_QUERYCAP failed\n", __func__);
return -1;
}
看一看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) */
__u32 version; /* should use KERNEL_VERSION() */
__u32 capabilities; /* Device capabilities */
__u32 reserved[4];
};
其中最重要的是capabilities字段,这个字段标记着v4l2设备的功能,capabilities有以下部分标记位:
我们可以通过这样子去判断设备的功能:
当然,并不是所有的设备都需要设置输入,比如:uvc摄像头,一般只有一个输入,默认就会选择,不需要设置
下面介绍如何设置输入设备
struct v4l2_input input;
input.index = 0;
while (!ioctl(fd, VIDIOC_ENUMINPUT, &input))
{
printf("input:%s\n", input.name);
++input.index;
}
struct v4l2_input input;
input.index = index; //指定输入设备
if (ioctl(fd, VIDIOC_S_INPUT, &input) < 0)
{
printf("ERR(%s):VIDIOC_S_INPUT failed\n", __func__);
return -1;
}
struct v4l2_fmtdesc fmtdesc;
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmtdesc.index = 0;
while (!ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc))
{
printf("fmt:%s\n", fmtdesc.description);
fmtdesc.index++;
}
struct v4l2_format v4l2_fmt;
memset(&v4l2_fmt, 0, sizeof(struct v4l2_format));
v4l2_fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
v4l2_fmt.fmt.pix.width = width; //宽度
v4l2_fmt.fmt.pix.height = height; //高度
v4l2_fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; //像素格式
v4l2_fmt.fmt.pix.field = V4L2_FIELD_ANY;
if (ioctl(fd, VIDIOC_S_FMT, &v4l2_fmt) < 0)
{
printf("ERR(%s):VIDIOC_S_FMT failed\n", __func__);
return -1;
}
read方式很容易理解,就是通过read函数读取,那么streaming是什么意思呢?
streaming就是在内核空间中维护一个缓存队列,然后将内存映射到用户空间,应用读取图像数据就是一个不断地出队列和入队列的过程,如下图所示:
下面讲解如何去申请和映射缓存:
struct v4l2_requestbuffers req;
req.count = nr_bufs; //缓存数量
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0)
{
printf("ERR(%s):VIDIOC_REQBUFS failed\n", __func__);
return -1;
}
因为如果使用read方式读取的话,图像数据是从内核空间拷贝会应用空间,而一副图像的数据一般来讲是比较大的,所以效率会比较低。而如果使用映射的方式,讲内核空间的内存应用到用户空间,那么用户空间读取数据就想在操作内存一样,不需要经过内核空间到用户空间的拷贝,大大提高效率
映射缓存需要先查询缓存信息,然后再使用缓存信息进行映射,下面是一个例子:
struct v4l2_buffer v4l2_buffer;
void* addr;
memset(&v4l2_buffer, 0, sizeof(struct v4l2_buffer));
v4l2_buffer.index = i; //想要查询的缓存
v4l2_buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
v4l2_buffer.memory = V4L2_MEMORY_MMAP;
/* 查询缓存信息 */
ret = ioctl(fd, VIDIOC_QUERYBUF, &v4l2_buffer);
if(ret < 0)
{
printf("Unable to query buffer.\n");
return -1;
}
/* 映射 */
addr = mmap(NULL /* start anywhere */ ,
v4l2_buffer.length, PROT_READ | PROT_WRITE, MAP_SHARED,
fd, v4l2_buffer.m.offset);
注:需要将所有申请的缓存使用上述方法进行映射
struct v4l2_buffer v4l2_buffer;
for(i = 0; i < nr_bufs; i++)
{
memset(&v4l2_buffer, 0, sizeof(struct v4l2_buffer));
v4l2_buffer.index = i; //想要放入队列的缓存
v4l2_buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
v4l2_buffer.memory = V4L2_MEMORY_MMAP;
ret = ioctl(fd, VIDIOC_QBUF, &v4l2_buffer);
if(ret < 0)
{
printf("Unable to queue buffer.\n");
return -1;
}
}
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(fd, VIDIOC_STREAMON, &type) < 0)
{
printf("ERR(%s):VIDIOC_STREAMON failed\n", __func__);
return -1;
}
struct pollfd poll_fds[1];
poll_fds[0].fd = fd;
poll_fds[0].events = POLLIN; //等待可读
poll(poll_fds, 1, 10000);
struct v4l2_buffer buffer;
buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buffer.memory = V4L2_MEMORY_MMAP;
if (ioctl(fd, VIDIOC_DQBUF, &buffer) < 0)
{
printf("ERR(%s):VIDIOC_DQBUF failed, dropped frame\n", __func__);
return -1;
}
出队列后得到了缓存的下标buffer.index,然后找到对饮的缓存,通过映射过后的地址进行数据的读取
struct v4l2_buffer v4l2_buf;
v4l2_buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
v4l2_buf.memory = V4L2_MEMORY_MMAP;
v4l2_buf.index = i; //指定buf
if (ioctl(fd, VIDIOC_QBUF, &v4l2_buf) < 0)
{
printf("ERR(%s):VIDIOC_QBUF failed\n", __func__);
return -1;
}
读取数据就是在上面这三步一直不断地循环
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(fd, VIDIOC_STREAMOFF, &type) < 0)
{
printf("ERR(%s):VIDIOC_STREAMOFF failed\n", __func__);
return -1;
}
for(i = 0; i < nr_bufs; ++i)
munmap(buf[i].addr, buf[i]->length);
close(fd);
其中附带一个实例example_cature,通过capture /dev/video0运行程序采集一张YUYV格式的图片,采集后得到了pic.yuv,可以通过ffplay查看ffplay -pixel_format yuyv422 -f rawvideo -video_size 640x480 pic.yuv,效果图如下
如何将采集图像在frame buff上显示?
具体的实现过程这里就不详细说了,下面给出一个例子,获取链接:
https://github.com/ImSjt/libv4l2
执行make编译后可以得到video2lcd,执行video2lcd /dev/video0
运行效果如下:
如何使用qt显示,道理跟在frame buff上显示是一样的,都是采集,转化格式,显示,只是在显示部分不同而已,这里给出一个例子,获取链接:
https://github.com/ImSjt/libv4l2
运行效果如下