linux V4L2子系统——v4l2架构(7)之V4L2应用编程

  • Post author:
  • Post category:linux




linux V4L2子系统——v4l2架构(7)之V4L2应用编程

备注:

1. Kernel版本:5.4

2. 使用工具:Source Insight 4.0

3. 参考博客:

(1)

Linux V4L2子系统-应用层访问video设备(四)


(2)

深入学习Linux摄像头(一)v4l2应用编程



概述

V4L2子系统向上提供了很多访问Video设备的接口,应用程序可以通过系统调用访问Video设备。但由于Video设备千差万别,很少有设备驱动程序能支持所有的接口功能,因此在使用之前,需要了解设备驱动程序支持的功能。



v4l2 API介绍

详见:

linux V4L2子系统——v4l2的结构体(4)之ioctl



v4l2设备操作流程

V4L2支持多种接口:capture(捕获)、output(输出)、overlay(预览)等等这里讲解如何使用capture功能,下面讲解操作流程



打开设备文件

在Linux中,视频设备节点为/dev/videox,使用open函数将其打开。视频设备与其他设备一样可以视为一个文件,所以使用open打开文件。可以是阻塞打开,也可以是非阻塞打开,非阻塞打开,若没有数据,则会返回错误。

// 阻塞打开
fd = open(vmsg->dev, O_RDWR | O_NONBLOCK, 0);
if (fd == -1) {
    sys_log_err("Cann't open '%s': %d, %s\n",
            vmsg->dev, errno, strerror(errno));
    return -ENODEV;
}

// 非阻塞打开
fd = open(vmsg->dev, O_RDWR);
if (fd == -1) {
    sys_log_err("Cann't open '%s': %d, %s\n",
            vmsg->dev, errno, strerror(errno));
    return -ENODEV;
}



获取设备支持的功能

在使用Video设备之前,需要了解Video设备支持哪些功能,如是图像捕获设备还是图像输出设备、可对视频信号进行何种调制。支持VBI、是否具有音频功能等。可通过VIDIOC_QUERYCAP命令获取Video设备支持的功能。最终通过检查struct v4l2_capability中的capabilities变量获取设备支持的功能。

struct v4l2_capability cap;
......
ret = ioctl(fd, VIDIOC_QUERYCAP, &cap);
if (ret < 0) {
    sys_log_err("%s is no V4L2 device %d-%d, %s\n", 
        vmsg->dev, ret, errno, strerror(errno));
    goto error;
}

// 判断是否支持某些功能
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
    sys_log_err("%s isn't video capture device\n", vmsg->dev);
    ret = -ENODEV;
    goto error;
}

if (!(cap.capabilities & V4L2_CAP_STREAMING)) {
    sys_log_err("%s does not support streaming i/o\n", vmsg->dev);
    ret = -ENODEV;
    goto error;
}
......

设备的功能保存在 struct v4l2_capability 结构体中,capabilities 变量具体表示了设备具有的功能,功能由宏定义 V4L2_CAP_XXXX 表示。

// 源码:include/uapi/linux/videodev2.h
struct v4l2_capability {
	__u8	driver[16];		// 驱动模块的名称,如"sun6i-video"
	__u8	card[32];		// 品牌名称,如"sunxi WinTV"
	__u8	bus_info[32];	// 总线名称,如"PCI:" + pci_name(pci_dev)
	__u32   version;		// 版本信息,KERNEL_VERSION
	__u32	capabilities;	// 设备整体的功能
	__u32	device_caps;
	__u32	reserved[3];
};

其中域 capabilities 代表设备支持的操作模式,常见的值有 V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING 表示是一个视频捕捉设备并且具有数据流控制模式;另外 driver 域需要和 struct video_device 中的 name 匹配。

如下:

// 源码:include/uapi/linux/videodev2.h

/* Values for 'capabilities' field */
#define V4L2_CAP_VIDEO_CAPTURE		0x00000001  /* Is a video capture device 图像捕获设备 */
#define V4L2_CAP_VIDEO_OUTPUT		0x00000002  /* Is a video output device  图像输出设备 */
#define V4L2_CAP_VIDEO_OVERLAY		0x00000004  /* Can do video overlay      支持预览功能 */
#define V4L2_CAP_VBI_CAPTURE		0x00000010  /* Is a raw VBI capture device */
#define V4L2_CAP_VBI_OUTPUT		0x00000020  /* Is a raw VBI output device */
#define V4L2_CAP_SLICED_VBI_CAPTURE	0x00000040  /* Is a sliced VBI capture device */
#define V4L2_CAP_SLICED_VBI_OUTPUT	0x00000080  /* Is a sliced VBI output device */
#define V4L2_CAP_RDS_CAPTURE		0x00000100  /* RDS data capture */
#define V4L2_CAP_VIDEO_OUTPUT_OVERLAY	0x00000200  /* Can do video output overlay */
#define V4L2_CAP_HW_FREQ_SEEK		0x00000400  /* Can do hardware frequency seek  */
#define V4L2_CAP_RDS_OUTPUT		0x00000800  /* Is an RDS encoder */

/* Is a video capture device that supports multiplanar formats */
#define V4L2_CAP_VIDEO_CAPTURE_MPLANE	0x00001000
/* Is a video output device that supports multiplanar formats */
#define V4L2_CAP_VIDEO_OUTPUT_MPLANE	0x00002000
/* Is a video mem-to-mem device that supports multiplanar formats */
#define V4L2_CAP_VIDEO_M2M_MPLANE	0x00004000
/* Is a video mem-to-mem device */
#define V4L2_CAP_VIDEO_M2M		0x00008000

#define V4L2_CAP_TUNER			0x00010000  /* has a tuner */
#define V4L2_CAP_AUDIO			0x00020000  /* has audio support */
#define V4L2_CAP_RADIO			0x00040000  /* is a radio device */
#define V4L2_CAP_MODULATOR		0x00080000  /* has a modulator */

#define V4L2_CAP_SDR_CAPTURE		0x00100000  /* Is a SDR capture device */
#define V4L2_CAP_EXT_PIX_FORMAT		0x00200000  /* Supports the extended pixel format */
#define V4L2_CAP_SDR_OUTPUT		0x00400000  /* Is a SDR output device */
#define V4L2_CAP_META_CAPTURE		0x00800000  /* Is a metadata capture device */

#define V4L2_CAP_READWRITE              0x01000000  /* read/write systemcalls */
#define V4L2_CAP_ASYNCIO                0x02000000  /* async I/O */
#define V4L2_CAP_STREAMING              0x04000000  /* streaming I/O ioctls */
#define V4L2_CAP_META_OUTPUT		0x08000000  /* Is a metadata output device */

#define V4L2_CAP_TOUCH                  0x10000000  /* Is a touch device */

#define V4L2_CAP_DEVICE_CAPS            0x80000000  /* sets device capabilities field */



获取和设置像素格式

有些摄像头支持多个像素格式,有的摄像头只支持一种像素格式。因此在设置像素格式之前需要了解摄像头支持的像素格式,然后再进行设置。VIDIOC_ENUM_FMT 命令枚举设备支持的像素格式,VIDIOC_S_FMT 命令设置设备当前采用的像素格式。

  • 枚举支持的像素格式
struct v4l2_fmtdesc fmtdesc = {0};
......
// 获取支持的像素格式
while (!ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc)) {
    printf("fmt: %s\n", fmtdesc.description);
    fmtdesc.index++;
}
......
  • 设置像素格式
struct v4l2_format fmt = {0};

memset(&fmt, 0, sizeof(fmt));
fmt.type           = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width  = 640;
fmt.fmt.pix.height = 480;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
fmt.fmt.pix.field = V4L2_FIELD_ANY;

ret = ioctl(fd, VIDIOC_S_FMT, &fmt);
if (ret < 0) {
    sys_log_err("%s VIDIOC_S_FMT error %d-%d, %s\n", 
        vmsg->dev, ret, errno, strerror(errno));

    memset(&fmt, 0, sizeof(fmt));
    if (0 == ioctl(fd, VIDIOC_G_FMT, &fmt)) {
        sys_log_err("%s support pixformat:\n", vmsg->dev);

        video_show_format(&fmt, _LOG_ERR);
    }

    goto error;
}
  • 获取像素格式
struct v4l2_format fmt = {0};
......
ret = ioctl(fd, VIDIOC_G_FMT, &fmt);
if (ret < 0) {
    sys_log_err("%s VIDIOC_G_FMT error %d-%d, %s\n", 
        vmsg->dev, ret, errno, strerror(errno));

    goto error;
}



设置缓存

v4l2设备读取数据的方式有两种,一种是 read 方式,一种是 streaming 方式,具体需要看其功能是支持 V4L2_CAP_READWRITE 还是V4L2_CAP_STREAMING

read 方式很容易理解,就是通过 read 函数读取,那么 streaming是什么意思呢?

streaming就是在内核空间中维护一个 缓存队列,然后将内存映射到用户空间,应用读取图像数据就是一个不断地 出队列 和 入队列 的过程,如下图所示

在这里插入图片描述


申请缓冲区


Video设备捕获的视频数据应该存放在预先分配的缓冲区中。使用 VIDIOC_REQBUFS 命令向内核申请缓冲区。在申请之前,需要设置申请的缓冲区数量 nr_bufs、缓冲区数据流类型type和缓冲区内存使用方式 memory。

struct v4l2_requestbuffers *req = NULL;
req = &vmsg->reqbuf;

memset(req, 0, sizeof(*req));
req->count   = buf_cnt;     //缓存数量
req->type    = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req->memory  = V4L2_MEMORY_MMAP; // 内存映射的方式,可提高效率,减少内存占用

ret = ioctl(fd, VIDIOC_REQBUFS, req);
if (ret) {
    sys_log_err("%s request buffer failed %d-%d, %s\n", 
        vmsg->dev, ret, errno, strerror(errno));
    return ret;
}

下面是struct v4l2_requestbuffers结构体的具体定义。

// 源码:include/uapi/linux/videodev2.h
/*
 *	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;		/* enum v4l2_buf_type 数据流类型,通常为V4L2_BUF_TYPE_VIDEO_CAPTURE */
	__u32			memory;		/* enum v4l2_memory   缓冲区内存使用方式,通常为V4L2_MEMORY_MMAP */
	__u32			capabilities;
	__u32			reserved[1];
};

enum v4l2_memory {
	V4L2_MEMORY_MMAP             = 1,	// 内存映射
	V4L2_MEMORY_USERPTR          = 2,	// 用户空间指针
	V4L2_MEMORY_OVERLAY          = 3,	// 内存重叠
	V4L2_MEMORY_DMABUF           = 4,	// DMABUF
};


映射缓存


为什么要映射缓存?

因为如果使用read方式读取的话,图像数据是从内核空间拷贝会应用空间,而一副图像的数据一般来讲是比较大的,所以效率会比较低。而如果使用映射的方式,讲内核空间的内存应用到用户空间,那么用户空间读取数据就想在操作内存一样,不需要经过内核空间到用户空间的拷贝,大大提高效率

映射缓存需要先查询缓存信息,然后再使用缓存信息进行映射,下面是一个例子

struct video_buff {
    void* start; 
    size_t length;
};
......

struct video_buff *vbuff = NULL;
struct v4l2_buffer buf;
......

vbuff = (struct video_buff *)calloc(req->count, sizeof(struct video_buff));
if (NULL == vbuff) {
    sys_log_err("%s request vbuff failed %d, %s\n", 
        vmsg->dev, errno, strerror(errno));
    return -ENOMEM;
}

for (i = 0; i < req->count; i++) {
    memset(&buf, 0, sizeof(buf));

    buf.type    = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory  = V4L2_MEMORY_MMAP;
    buf.index   = i;

    ret = ioctl(fd, VIDIOC_QUERYBUF, &buf);
    if (ret) {
        sys_log_err("%s VIDIOC_QUERYBUF faild %d-%d, %s\r\n", 
            vmsg->dev, ret, errno, strerror(errno));
        goto mmap_faild;
    }

    vbuff[i].length = buf.length;
    vbuff[i].start = mmap(NULL,
                          buf.length,
                          PROT_READ | PROT_WRITE,
                          MAP_SHARED | MAP_LOCKED,
                          fd,
                          buf.m.offset);

    if (MAP_FAILED == vbuff[i].start) {
        ret = (int)MAP_FAILED;
        sys_log_err("%s map buff faild\r\n", vmsg->dev);
        goto mmap_faild;
    }
}
......


将所有的缓存放入队列

struct v4l2_buffer buf;
enum v4l2_buf_type type;
struct v4l2_requestbuffers *req = NULL;
......
    
req = &vmsg->reqbuf;
for (i = 0; i < req->count; i++) {
    memset(&buf, 0, sizeof(buf));
    buf.type     = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory   = V4L2_MEMORY_MMAP;
    buf.index    = i;

    ret = ioctl(vmsg->video_fd, VIDIOC_QBUF, &buf);
    if (ret < 0) {
        sys_log_err("%s VIDIOC_QBUF failed:%d-%d, %s\n", 
            vmsg->dev, ret, errno, strerror(errno));
        goto error;
    }
}
......



开始采集视频

经过前面的准备工作,现在可以使能设备,开始采集视频数据了。VIDIOC_STREAMON命令使能设备,开始采集视频。

enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
......
ret = ioctl(vmsg->video_fd, VIDIOC_STREAMON, &type);
if (ret < 0) {
    sys_log_err("%s VIDIOC_STREAMON START faild::%d-%d, %s\n", 
        vmsg->dev, ret, errno, strerror(errno));
    goto error;
}
......



处理数据

struct v4l2_buffer v4l2_buf = {0};
for (;;) {
    ret = ioctl(fd, VIDIOC_QBUF, &v4l2_buf);  // 从环形队列中获取一个缓冲区
    ......
    data = buf[v4l2_buf.index].start;    // 缓冲区地址
    length = buf[v4l2_buf.index].length  // 缓冲区数据长度
    ......
    ret = ioctl(fd, VIDIOC_QBUF, &v4l2_buf);  // 将缓冲区放入环形队列中
    ......
}



停止采集视频

应用可使用VIDIOC_STREAMOFF命令停止视频采集,同时禁止设备。

enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
......
ret = ioctl(vmsg->video_fd, VIDIOC_STREAMOFF, &type);
if (ret < 0) {
    sys_log_err("%s VIDIOC_STREAMONVIDIOC_STREAMOFF STOP faild::%d-%d, %s\n", 
        vmsg->dev, ret, errno, strerror(errno));
    goto error;
}



关闭设备

不再使用设备时,可使用close关闭设置,close调用后之前申请的缓冲区会被释放。



版权声明:本文为u013836909原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。