Skip to content

Latest commit

 

History

History
1040 lines (834 loc) · 55.3 KB

File metadata and controls

1040 lines (834 loc) · 55.3 KB

九、从用户空间利用 V4L2API

设备驱动的主要目的是控制和利用底层硬件,同时向用户展示功能。 这些用户可以是在用户空间或其他内核驱动中运行的应用。 虽然前两章讨论的是 V4L2 设备驱动,但在本章中,我们将学习如何利用内核公开的 V4L2 设备功能。 我们将从描述和枚举用户空间 V4L2API 开始,然后学习如何利用这些 API 从传感器获取视频数据,包括破坏传感器属性。

本章将介绍以下主题:

  • V4L2 用户空间 API
  • 来自用户空间的视频设备属性管理
  • 来自用户空间的缓冲区管理
  • V4L2 用户空间工具

技术要求

为了充分利用本章,您需要以下内容:

从用户空间介绍 V4L2

编写设备驱动的主要目的是简化应用对底层设备的控制和使用。 用户空间处理 V4L2 设备有两种方式:一种是使用一体化实用程序(如GStreamer及其gst-*工具),另一种是使用用户空间 V4L2API 编写专用应用。 在本章中,我们只介绍代码 de,因此我们将介绍如何编写使用 V4L2API 的应用。

V4L2 用户空间 API

V4L2 用户空间 API 减少了函数数量和大量数据结构,所有这些都是在include/uapi/linux/videodev2.h中定义的。 在这一节中,我们将尝试描述其中最重要的--或者更确切地说,是最常用的。 您的代码应包含以下标题:

#include <linux/videodev2.h>

该接口依赖于以下函数:

  • open():打开视频设备
  • 发布帖子:关闭视频设备
  • ioctl():向显示驱动发送 ioctl 命令
  • mmap():将驱动分配的缓冲区内存映射到用户空间
  • read()write(),具体取决于流方法

这组精简的 API 通过大量的 ioctl 命令进行扩展,其中最重要的命令如下:

  • VIDIOC_QUERYCAP:用于查询驱动能力。 人们过去常说它是用来查询设备的功能的,但事实并非如此,因为设备可能能够执行驱动中没有实现的功能。 用户空间传递一个struct v4l2_capability结构,视频驱动将用相关信息填充该结构。
  • VIDIOC_ENUM_FMT:用于枚举驱动支持的图像格式。 驱动用户空间传递一个struct v4l2_fmtdesc结构,驱动将使用相关信息填充该结构。
  • VIDIOC_G_FMT:对于采集设备,用于获取当前的图像格式。 但是,对于显示设备,您可以使用它来获取当前的显示窗口。 在这两种情况下,用户空间都会传递一个struct v4l2_format结构,驱动将用相关信息填充该结构。
  • 当您不确定要提交给设备的格式时,应使用VIDIOC_TRY_FMT。 这用于验证捕获设备的新图像格式或新显示窗口,具体取决于输出(显示)设备。 用户空间传递带有它想要应用的属性的struct v4l2_format结构,如果不支持,驱动可以更改给定值。 然后,应用应该检查授予了什么。
  • VIDIOC_S_FMT用于设置捕获设备的新图像格式或显示器(输出设备)的新显示窗口。 如果用户空间传递的值不受支持,驱动可能会更改这些值。 如果没有首先使用VIDIOC_TRY_FMT,应用应该检查授予了什么。
  • VIDIOC_CROPCAP用于根据当前图像大小和当前显示面板大小获取默认裁剪矩形。 驱动填充struct v4l2_cropcap结构。
  • VIDIOC_G_CROP用于获取当前的裁剪矩形。 驱动填充struct v4l2_crop结构。
  • VIDIOC_S_CROP用于设置新的裁剪矩形。 驱动填充struct v4l2_crop结构。 应用应该检查授予了什么。
  • VIDIOC_REQBUFS:此 ioctl 用于请求多个缓冲区,这些缓冲区稍后可以进行内存映射。 驱动填充struct v4l2_requestbuffers结构。 由于驱动分配的缓冲区数可能比实际请求的缓冲区数多或少,因此应用应该检查实际授予了多少缓冲区。 在此之后还没有缓冲区排队。
  • VIDIOC_QUERYBUFioctl 用于获取缓冲区的信息,mmap()系统调用可以使用这些信息将缓冲区映射到用户空间。 驱动填充struct v4l2_buffer结构。
  • VIDIOC_QBUF用于通过传递与缓冲区相关联的struct v4l2_buffer结构来对该缓冲区进行排队。 在此 ioctl 的执行路径上,驱动会将此缓冲区添加到其缓冲区列表中,以便在前面没有挂起的排队缓冲区时将其填满。 一旦缓冲区被填满,它就会被传递到 V4L2 内核,该内核维护自己的列表(即就绪缓冲区列表),并将其从驱动的 DMA 缓冲区列表中移出。
  • VIDIOC_DQBUF用于通过传递与缓冲区相关的struct v4l2_buffer结构,将已填满的缓冲区(从 V4L2 的输入设备就绪缓冲区列表中)或显示的(输出设备)缓冲区出列。 如果没有准备好的缓冲区,这将阻塞,除非O_NONBLOCKopen()一起使用,在这种情况下,VIDIOC_DQBUF将立即返回并返回EAGAIN错误代码。 只有在调用了STREAMON之后才能调用VIDIOC_DQBUF。 同时,在STREAMOFF之后调用此 ioctl 将返回-EINVAL
  • VIDIOC_STREAMON用于打开流。 在此之后,将呈现图像中的任何VIDIOC_QBUF结果。
  • VIDIOC_STREAMOFF用于关闭流。 此 ioctl 删除所有缓冲区。 它实际上会刷新缓冲区队列。

除了我们刚才列举的那些命令之外,还有更多的 ioctl 命令。 实际上,内核的v4l2_ioctl_ops数据结构中的 ioctls 至少和 op 一样多。 但是,前面的 ioctls 足以更深入地了解 V4L2 用户空间 API。 在本节中,我们不会详细介绍每个数据结构。 然后,您应该保持打开include/uapi/linux/videodev2.h文件(也可以从https://elixir.bootlin.com/linux/v4.19/source/include/uapi/linux/videodev2.h获得),因为它包含所有 V4L2API 和数据结构。 也就是说,下面的伪代码显示了使用 V4L2API 从用户空间抓取视频的典型 ioctl 序列:

open()
int ioctl(int fd, VIDIOC_QUERYCAP,           struct v4l2_capability *argp)
int ioctl(int fd, VIDIOC_S_FMT, struct v4l2_format *argp)
int ioctl(int fd, VIDIOC_S_FMT, struct v4l2_format *argp)
/* requesting N buffers */
int ioctl(int fd, VIDIOC_REQBUFS,           struct v4l2_requestbuffers *argp)
/* queueing N buffers */
int ioctl(int fd, VIDIOC_QBUF, struct v4l2_buffer *argp)
/* start streaming */
int ioctl(int fd, VIDIOC_STREAMON, const int *argp) 
read_loop: (for i=0; I < N; i++)
    /* Dequeue buffer i */
    int ioctl(int fd, VIDIOC_DQBUF, struct v4l2_buffer *argp)
    process_buffer(i)
    /* Requeue buffer i */
    int ioctl(int fd, VIDIOC_QBUF, struct v4l2_buffer *argp)
end_loop
    releases_memories()
    close()

前面的序列将作为在用户空间处理 V4L2API 的指导。

请注意,ioctl系统调用在errno = EINTR时返回-1值是可能的。 在这种情况下,这并不意味着出现错误,而只是系统调用被中断,在这种情况下,应该再次尝试。 为了解决这个(罕见但可能的)问题,我们可以考虑为ioctl编写我们自己的包装器,如下所示:

static int xioctl(int fh, int request, void *arg)
{
        int r;
        do {
                r = ioctl(fh, request, arg);
        } while (-1 == r && EINTR == errno);
        return r;
}

现在我们已经完成了视频抓取序列概述,我们可以通过格式协商确定从设备打开到关闭进行视频流传输需要执行哪些步骤。 现在我们可以跳到代码,从设备打开开始,一切都从这里开始。

视频设备开机及物业管理

驱动公开与其负责的视频接口对应的/dev/目录中的节点条目。 这些文件节点对应于捕获设备的/dev/videoX个特殊文件(在我们的示例中)。 在与视频设备进行任何交互之前,应用必须打开适当的文件节点。 为此,它使用open()系统调用,该调用将返回一个文件描述符,该文件描述符将成为发送到设备的任何命令的入口点,如下例所示:

static const char *dev_name = "/dev/video0";
fd = open (dev_name, O_RDWR);
if (fd == -1) {
    perror("Failed to open capture device\n");
    return -1;
}

前面的代码片断是阻塞模式下的开口。 如果在尝试出列时没有就绪缓冲区,则将O_NONBLOCK传递给open()可以防止应用被阻塞。 使用完视频设备后,应使用close()系统调用将其关闭:

close (fd);

在我们能够打开视频设备之后,我们就可以开始与它进行交互了。 一般来说,视频设备打开后的第一个动作就是询问它的能力,通过它我们可以使它以最优的方式运行。

查询设备功能

通常会查询设备的功能,以确保它支持我们需要使用的模式。 您可以使用VIDIOC_QUERYCAPioctl 命令执行此操作。 为了实现这一点,应用传递一个struct v4l2_capability结构(在include/uapi/linux/videodev2.h中定义),该结构将由驱动填充。 此结构有一个必须检查的.capabilities字段。 该字段包含整个设备的功能。 以下摘录自内核源代码,显示了可能的值:

/* Values for 'capabilities' field */
#define V4L2_CAP_VIDEO_CAPTURE 0x00000001 /*video capture device*/ #define V4L2_CAP_VIDEO_OUTPUT 0x00000002  /*video output device*/ #define V4L2_CAP_VIDEO_OVERLAY 0x00000004 /*Can do video overlay*/ [...] /* VBI device skipped */
/* video capture device that supports multiplanar formats */#define V4L2_CAP_VIDEO_CAPTURE_MPLANE	0x00001000
/* video output device that supports multiplanar formats */ #define V4L2_CAP_VIDEO_OUTPUT_MPLANE	0x00002000
/* 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
[...] /* radio, tunner and sdr devices skipped */
#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_TOUCH	0x10000000	/* Is a touch device */

以下代码块显示了一个常见用例,该用例显示了如何使用VIDIOC_QUERYCAPioctl 从代码中查询设备功能:

#include <linux/videodev2.h>
[...]
struct v4l2_capability cap;
memset(&cap, 0, sizeof(cap));
if (-1 == xioctl(fd, VIDIOC_QUERYCAP, &cap)) {
    if (EINVAL == errno) {
        fprintf(stderr, "%s is no V4L2 device\n", dev_name);
        exit(EXIT_FAILURE);
    } else {
        errno_exit("VIDIOC_QUERYCAP" 
    }
}

在前面的代码中,由于memset()struct v4l2_capability在被赋予ioctl命令之前首先被置零。 在此步骤中,如果没有发生错误,则我们的cap变量现在包含设备功能。 您可以使用以下内容检查设备类型和 I/O 方法:

if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
    fprintf(stderr, "%s is not a video capture device\n",             dev_name);
    exit(EXIT_FAILURE);
}
if (!(cap.capabilities & V4L2_CAP_READWRITE))
    fprintf(stderr, "%s does not support read i/o\n",             dev_name);
/* Check whether USERPTR and/or MMAP method are supported */
if (!(cap.capabilities & V4L2_CAP_STREAMING))
    fprintf(stderr, "%s does not support streaming i/o\n",             dev_name);
/* Check whether driver support read/write i/o */
if (!(cap.capabilities & V4L2_CAP_READWRITE))
    fprintf (stderr, "%s does not support read i/o\n",              dev_name);

您可能已经注意到,在使用cap变量之前,我们首先将其置零。 最好始终清除将提供给 V4L2API 的参数,以避免内容陈旧。 然后,让我们定义一个宏(比方说CLEAR),它将把作为参数给定的任何变量置零,并在本章的其余部分中使用它:

#define CLEAR(x) memset(&(x), 0, sizeof(x))

现在,我们已经完成了对视频设备功能的查询。 这使我们可以根据需要配置设备和调整图像格式。 通过协商合适的图像格式,我们可以利用视频设备,正如我们将在 next 部分中看到的那样。

缓冲区管理

您应该考虑在 V4L2 中维护两个缓冲队列:一个用于驱动(称为输入队列)和一个用于用户(称为输出队列)。 缓冲区由用户空间应用排队到驱动队列中,以便填充数据(应用为此使用VIDIOC_QBUFioctl)。 驱动按照缓冲区排队的顺序填充缓冲区。 填充后,每个缓冲区将从输入队列移出,放入输出队列(即用户队列)。

每当用户应用调用VIDIOC_DQBUF以使缓冲区出列时,都会在输出队列中查找该缓冲区。 如果它在其中,缓冲区将出列并推送到用户应用;否则,应用将等待,直到缓冲区被填满。 用户使用完缓冲区后,必须对该缓冲区调用VIDIOC_QBUF,以便将其重新排入输入队列,以便可以再次填充。

驱动初始化后,应用调用VIDIOC_REQBUFSioctl 来设置需要使用的缓冲区数量。 一旦授权,应用就会使用VIDIOC_QBUF将所有缓冲区排队,然后调用VIDIOC_STREAMONioctl。 然后,驱动自行前进,填满所有排队的缓冲区。 如果没有更多的排队缓冲区,则驱动将等待应用将缓冲区排队。 如果出现这种情况,则意味着某些帧在捕获本身中丢失。

图像(缓冲区)格式

在确保设备属于正确类型并支持其可以使用的模式后,应用必须协商其所需的视频格式。 应用必须确保视频设备配置为以应用可以处理的格式发送视频帧。 在开始抓取和收集数据(或视频帧)之前,它必须这样做。 V4L2API 使用struct v4l2_format来表示缓冲区格式,无论设备的类型是什么。 该结构定义如下:

struct v4l2_format {
 u32 type;
 union {
  struct v4l2_pix_format pix; /* V4L2_BUF_TYPE_VIDEO_CAPTURE */    
  struct v4l2_pix_format_mplane pix_mp; /* _CAPTURE_MPLANE */
  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;/*_SLICED_VBI_CAPTURE */ 
  struct v4l2_sdr_format sdr;   /* V4L2_BUF_TYPE_SDR_CAPTURE */
  struct v4l2_meta_format meta;/* V4L2_BUF_TYPE_META_CAPTURE */
        [...]
    } fmt;
};

在前面的结构中,type字段表示数据流的类型,应该由应用设置。 根据其值的不同,fmt字段将具有适当的类型。 在我们的例子中,type必须是V4L2_BUF_TYPE_VIDEO_CAPTURE,因为我们处理的是视频捕获设备。 然后,fmt将为struct v4l2_pix_format类型。

重要音符

几乎所有(如果不是全部)直接或间接使用缓冲区的 ioctls(例如裁剪、缓冲区请求/队列/出列/查询)都需要指定缓冲区类型,这是有意义的。 我们将使用V4L2_BUF_TYPE_VIDEO_CAPTURE,因为它是我们设备类型的唯一选择。 缓冲区类型的整个列表都是include/uapi/linux/videodev2.h中定义的enum v4l2_buf_type类型。 你应该去看看。

通常,应用查询视频设备的当前格式,然后只更改感兴趣的属性,然后将新的、损坏的缓冲区格式发回视频设备。 然而,这不是强制性的。 我们在这里这样做只是为了演示如何获取或设置当前格式。 应用使用VIDIOC_G_FMTioctl 命令查询当前缓冲区格式。 它必须传递一个设置了type字段的新(我指的是清零)struct v4l2_format结构。 驱动将在 ioctl 的返回路径中填充其余部分。 以下是一个示例:

struct v4l2_format fmt;
CLEAR(fmt);
/* Get the current format */
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(fd, VIDIOC_G_FMT, &fmt)) {
    printf("Getting format failed\n");
    exit(2);
}

获得当前格式后,我们可以更改相关属性并将新格式发回设备。 这些属性可以是像素格式、每个颜色分量的存储器组织、以及每个场的隔行扫描捕获存储器组织。 我们还可以描述缓冲区的大小和间距。 设备支持的常见(但不是唯一)像素格式如下:

  • V4L2_PIX_FMT_YUYV:YUV422(交错)
  • V4L2_PIX_FMT_NV12:YUV420(半平面)
  • V4L2_PIX_FMT_NV16:YUV422(半平面)
  • V4L2_PIX_FMT_RGB24:RGB888(包装)

现在,让我们编写更改所需属性的代码片段。 但是,将新格式发送到视频设备需要使用新的 ioctl 命令-即VIDIOC_S_FMT

#define WIDTH	1920
#define HEIGHT	1080
#define PIXFMT	V4L2_PIX_FMT_YUV420
/* Changing required properties and set the format */ fmt.fmt.pix.width = WIDTH;
fmt.fmt.pix.height = HEIGHT;
fmt.fmt.pix.bytesperline = fmt.fmt.pix.width * 2u;
fmt.fmt.pix.sizeimage = fmt.fmt.pix.bytesperline * fmt.fmt.pix.height; 
fmt.fmt.pix.colorspace = V4L2_COLORSPACE_REC709;
fmt.fmt.pix.field = V4L2_FIELD_ANY;
fmt.fmt.pix.pixelformat = PIXFMT;
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (xioctl(fd, VIDIOC_S_FMT, &fmt)) {
    printf("Setting format failed\n");
    exit(2);
}

重要音符

我们本可以在不需要当前格式的情况下使用前面的代码。

IOCTL 可能会成功。 但是,这并不意味着您的参数已按原样应用。 默认情况下,设备可能不支持图像宽度和高度的所有组合,甚至不支持所需的像素格式。 在这种情况下,驱动将根据您请求的值应用其支持的最接近的值。 然后,您必须检查您的参数是否已被接受,或者授予的参数是否足够好,以便您继续操作:

if (fmt.fmt.pix.pixelformat != PIXFMT)
   printf("Driver didn't accept our format. Can't proceed.\n");
/* because VIDIOC_S_FMT may change width and height */
if ((fmt.fmt.pix.width != WIDTH) ||     (fmt.fmt.pix.height != HEIGHT))     
 fprintf(stderr, "Warning: driver is sending image at %dx%d\n",
            fmt.fmt.pix.width, fmt.fmt.pix.height);

我们甚至可以通过更改流参数(例如每秒的帧数)来更进一步。 我们可以通过以下方式来实现这一点:

  • 使用VIDIOC_G_PARMioctl 查询视频设备的流参数。 此 ioctl 接受新的struct v4l2_streamparm结构及其type成员集作为参数。 此类型应为enum v4l2_buf_type值之一。

  • Checking v4l2_streamparm.parm.capture.capability and making sure the V4L2_CAP_TIMEPERFRAME flag is set. This means that the driver allows changing the capture frame rate.

    如果是这样,我们可以(可选)使用VIDIOC_ENUM_FRAMEINTERVALSioctl 来获得可能的帧间隔列表(API 使用帧间隔,它与帧速率相反)。

  • 使用VIDIOC_S_PARMioctl 并用适当的值填充v4l2_streamparm.parm.capture.timeperframe个成员。 该应允许设置捕获侧帧速率。 你的任务是确保你的阅读速度足够快,不会出现帧丢失。

以下是一个示例:

#define FRAMERATE 30
struct v4l2_streamparm parm;
int error;
CLEAR(parm);
parm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
/* first query streaming parameters */
error = xioctl(fd, VIDIOC_G_PARM, &parm);
if (!error) {
    /* Now determine if the FPS selection is supported */
    if (parm.parm.capture.capability & V4L2_CAP_TIMEPERFRAME) {
        /* yes we can */
        CLEAR(parm);
        parm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        parm.parm.capture.capturemode = 0;
        parm.parm.capture.timeperframe.numerator = 1;
        parm.parm.capture.timeperframe.denominator = FRAMERATE;
        error = xioctl(fd, VIDIOC_S_PARM, &parm);
        if (error)
            printf("Unable to set the FPS\n");
        else
           /* once again, driver may have changed our requested 
            * framerate */
            if (FRAMERATE != 
                  parm.parm.capture.timeperframe.denominator)
                printf ("fps coerced ......: from %d to %d\n",
                        FRAMERATE,
                   parm.parm.capture.timeperframe.denominator);

现在,我们可以协商图像格式并设置流参数。 下一个逻辑继续将是请求缓冲区并继续进行进一步处理。

请求缓冲区

完成格式准备后,就可以指示驱动分配用于存储视频帧的内存了。 VIDIOC_REQBUFSioctl 就是为了实现这一点。 此 ioctl 采用新的struct v4l2_requestbuffers结构作为参数。 在提供给 ioctl 之前,v4l2_requestbuffers必须设置它的一些字段:

  • v4l2_requestbuffers.count:应使用要分配的内存缓冲区数量设置此成员。 此成员应设置一个值,以确保不会因为输入队列中缺少排队缓冲区而丢弃帧。 大多数情况下,34是正确的值。 因此,驱动可能会对请求的缓冲区数量感到不舒服。 在这种情况下,驱动将使用 ioctl 返回路径上授予的缓冲区数量设置v4l2_requestbuffers.count。 然后,应用应检查此值,以确保授予的值符合其需要。
  • v4l2_requestbuffers.type:此必须设置为enum 4l2_buf_type类型的视频缓冲区类型。 这里,我们再次使用V4L2_BUF_TYPE_VIDEO_CAPTURE。 例如,对于输出设备,这将是V4L2_BUF_TYPE_VIDEO_OUTPUT
  • v4l2_requestbuffers.memory:这必须是可能的enum v4l2_memory值之一。 可能感兴趣的值有V4L2_MEMORY_MMAPV4L2_MEMORY_USERPTRV4L2_MEMORY_DMABUF。 这些都是流方法。 但是,根据此成员的值,应用可能需要执行其他任务。不幸的是,VIDIOC_REQBUFS命令是应用发现给定驱动支持哪些类型的流 I/O 缓冲区的唯一方法。 然后,应用可以尝试VIDIOC_REQBUFS个个值,并根据个值的失败或成功来调整其逻辑。

通过ffERS-VIDIOC_REQBUFS 和 MALLOC 请求用户指针

此步骤涉及支持流模式的驱动,特别是支持用户指针 I/O 模式。 在这里,应用通知驱动它即将分配给定数量的缓冲区:

#define BUF_COUNT 4
struct v4l2_requestbuffers req; CLEAR (req);
req.count	= BUF_COUNT;
req.type	= V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory	= V4L2_MEMORY_USERPTR;
if (-1 == xioctl (fd, VIDIOC_REQBUFS, &req)) {
    if (EINVAL == errno)
        fprintf(stderr,                 "%s does not support user pointer i/o\n", 
                dev_name);
    else
        fprintf("VIDIOC_REQBUFS failed \n");
}

然后,应用从用户空间分配缓冲存储器:

struct buffer_addr {
    void  *start;
    size_t length;
};
struct buffer_addr *buf_addr;
int i;
buf_addr = calloc(BUF_COUNT, sizeof (*buffer_addr));
if (!buf_addr) {
    fprintf(stderr, "Out of memory\n");
    exit (EXIT_FAILURE);
}
for (i = 0; i < BUF_COUNT; ++i) {
    buf_addr[i].length = buffer_size;
    buf_addr[i].start = malloc(buffer_size);
    if (!buf_addr[i].start) {
        fprintf(stderr, "Out of memory\n");
        exit(EXIT_FAILURE);
    }
}

这是第一种类型的流,其中缓冲区在用户空间中被错误地分配给内核,以便填充视频数据:所谓的 USER 指针 I/O 模式。 还有另一种奇特的流模式,在这种模式下几乎所有事情都是从内核完成的。 W 毫不迟疑,让我们来介绍一下。

请求内存可映射 BUffer-VIDIOC_REQBUFS、VIDIOC_QUERYBUF 和 MAP

在驱动缓冲区模式下,此 ioctl 还返回在v4l2_requestbuffer结构的count成员中分配的实际缓冲区数。 该流方法还需要个新数据结构struct v4l2_buffer。 在内核中的驱动分配缓冲区之后,此结构与VIDIOC_QUERYBUFSioctl 一起使用,以便查询每个已分配缓冲区的物理地址,该地址可与mmap()系统调用一起使用。 从驱动器返回的物理地址将存储在buffer.m.offset中。

以下代码摘录指示驱动分配内存缓冲区,并检查授予的缓冲区数量:

#define BUF_COUNT_MIN 3
struct v4l2_requestbuffers req; CLEAR (req);
req.count	= BUF_COUNT;
req.type	= V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory	= V4L2_MEMORY_MMAP;
if (-1 == xioctl (fd, VIDIOC_REQBUFS, &req)) {
    if (EINVAL == errno)
        fprintf(stderr, "%s does not support memory mapping\n", 
                dev_name);
    else
        fprintf("VIDIOC_REQBUFS failed \n");
}
/* driver may have granted less than the number of buffers we
 * requested let's then make sure it is not less than the
 * minimum we can deal with
 */
if (req.count < BUF_COUNT_MIN) {
    fprintf(stderr, "Insufficient buffer memory on %s\n",             dev_name);
    exit (EXIT_FAILURE);
}

在此之后,应用应该调用每个分配的缓冲区上的VIDIOC_QUERYBUFioctl,以获得它们的对应的物理地址,如下面的示例所示:

struct buffer_addr {
    void *start;
    size_t length;
};
struct buffer_addr *buf_addr;
buf_addr = calloc(BUF_COUNT, sizeof (*buffer_addr));
if (!buf_addr) {
    fprintf (stderr, "Out of memory\n");
    exit (EXIT_FAILURE);
}
for (i = 0; i < req.count; ++i) {
    struct v4l2_buffer buf;
    CLEAR (buf);
    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP; buf.index	= i;
    if (-1 == xioctl (fd, VIDIOC_QUERYBUF, &buf))
        errno_exit("VIDIOC_QUERYBUF");
    buf_addr[i].length = buf.length;
    buf_addr[i].start =
        mmap (NULL /* start anywhere */, buf.length,
              PROT_READ | PROT_WRITE /* required */,
              MAP_SHARED /* recommended */, fd, buf.m.offset);
    if (MAP_FAILED == buf_addr[i].start)
        errno_exit("mmap");
}

为了让应用在内部跟踪每个缓冲区的内存映射(使用mmap()获得),我们定义了一个为每个授予的缓冲区分配的自定义数据结构struct buffer_addr,它将保存与该缓冲区对应的映射。

请求 DMABUF 缓冲区-VIDIOC_REQBUFS、VIDIOC_EXPBUF 和 mmap

DMABUF 是,主要用于mem2mem设备,并引入了导出器导入器的概念。 假设驱动A想要使用驱动B创建的缓冲区;然后我们调用B作为导出器,调用A作为缓冲区用户/导入器。

export方法指示驱动通过文件描述符将其 DMA 缓冲区导出到用户空间。 应用使用VIDIOC_EXPBUFioctl 实现这一点,并需要一个新的数据结构struct v4l2_exportbuffer。 在此 ioctl 的返回路径上,驱动将使用与给定缓冲区相对应的文件描述符设置v4l2_requestbuffers.md成员。 这是一个 DMABUF 文件描述符:

/* V4L2 DMABuf export */
struct v4l2_requestbuffers req;
CLEAR (req);
req.count = BUF_COUNT;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_DMABUF;
if (-1 == xioctl(fd, VIDIOC_REQBUFS, &req))
    errno_exit ("VIDIOC_QUERYBUFS");

应用可以将这些缓冲区作为 DMABUF 文件描述符导出,以便对它们进行内存映射以访问捕获的视频内容。 为此,应用应使用VIDIOC_EXPBUFioctl。 此 ioctl 扩展了内存映射 I/O 方法,因此它仅适用于V4L2_MEMORY_MMAP个缓冲区。 但是,在使用VIDIOC_EXPBUF导出捕获缓冲区然后映射它们时,它实际上是无用的。 您应该改用V4L2_MEMORY_MMAP

当涉及到 V4L2 输出设备时,VIDIOC_EXPBUF变得非常有趣。 这样,应用使用VIDIOC_REQBUFSioctl 在捕获和输出设备上分配缓冲区,然后应用将输出设备的缓冲区导出为 DMABUF 文件描述符,并使用这些文件描述符在捕获设备上排队 ioctl 之前设置v4l2_buffer.m.fd字段。 然后,排队的缓冲器将填充其对应的缓冲器(对应于v4l2_buffer.m.fd的输出设备缓冲器)。

在下面的示例中,我们将输出设备缓冲区导出为 DMABUF 文件描述符。 这假设已经使用VIDIOC_REQBUFSioctl 分配了该输出设备的缓冲器,其中req.type将设置为V4L2_BUF_TYPE_VIDEO_OUTPUTreq.memory设置为V4L2_MEMORY_DMABUF

int outdev_dmabuf_fd[BUF_COUNT] = {-1};
int i;
for (i = 0; i < req.count; i++) {
    struct v4l2_exportbuffer expbuf;
    CLEAR (expbuf);
    expbuf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
    expbuf.index = i;
    if (-1 == xioctl(fd, VIDIOC_EXPBUF, &expbuf)
        errno_exit ("VIDIOC_EXPBUF");
    outdev_dmabuf_fd[i] = expbuf.fd;
}

现在,我们已经了解了基于 DMABUF 的流媒体,并介绍了它的相关概念。 下一个也是最后一个流方法 od 要简单得多,需要的代码更少。 让我们直奔主题吧。

请求读/写 I/O 内存

从编码的角度来看,这是更简单的流模式。 在读/写 I/O的情况下,除了分配应用将存储读取数据的内存位置之外,没有什么可做的,如下例所示:

struct buffer_addr {
    void *start;
    size_t length;
};
struct buffer_addr *buf_addr;
buf_addr = calloc(1, sizeof(*buf_addr));
if (!buf_addr) {
    fprintf(stderr, "Out of memory\n");
    exit(EXIT_FAILURE);
}
buf_addr[0].length = buffer_size;
buf_addr[0].start = malloc(buffer_size);
if (!buf_addr[0].start) {
    fprintf(stderr, "Out of memory\n");
    exit(EXIT_FAILURE);
}

在前面的代码片段中,我们定义了相同的自定义数据结构struct buffer_addr。 但是,这里没有真正的缓冲区请求(没有使用VIDIOC_REQBUFS),因为还没有任何东西进入内核。 只是分配了缓冲内存,仅此而已。

现在,我们完成了个缓冲区请求。 下一步是将请求的缓冲区排队,以便内核可以用视频数据填充它们。 现在让我们看看如何做到这一点。

将缓冲区排队并启用流

在访问缓冲区并读取其数据之前,必须将该缓冲区排队。 这包括在使用流式 I/O 方法(读/写 I/O 除外)时使用缓冲区上的VIDIOC_QBUFioctl。 将缓冲区排队会将该缓冲区的内存页锁定在物理内存中。 这样,这些页面就不能换出到磁盘。 请注意,这些缓冲区将保持锁定状态,直到它们出列,直到调用VIDIOC_STREAMOFFVIDIOC_REQBUFSioctls,或者直到设备关闭。

在 V4L2 上下文中,锁定缓冲区意味着将此缓冲区传递给驱动以进行硬件访问(通常为 DMA)。 如果应用访问(读/写)锁定的缓冲区,则结果未定义。

要将缓冲区入队,应用必须准备struct v4l2_bufferv4l2_buffer.typev4l2_buffer.memoryv4l2_buffer.index应根据缓冲区类型、流模式和缓冲区分配时的索引进行设置。 其他字段取决于流模式。

重要的个音符

读/写 I/O方法不需要排队。

素数 buffers 的概念

对于捕获应用,习惯上是在开始捕获并进入读取循环之前将一定数量的空缓冲区(大多数情况下是分配的缓冲区数量)入队。 这有助于提高应用的 Smoot 完整性,并防止它因为缺少填充的 buffer 而被阻塞。 这应该在分配缓冲区之后立即执行。

将用户指针蜂鸣器排队

要将用户指针缓冲区排队,应用必须将v4l2_buffer.memory成员设置为V4L2_MEMORY_USERPTR。 这里的特殊性是v4l2_buffer.m.userptr字段,该字段必须设置为先前分配的缓冲区地址,并将v4l2_buffer.length设置为其大小。 当使用多平面 API 时,必须使用传递的struct v4l2_plane数组的m.userptrlength成员:

/* Prime buffers */
for (i = 0; i < BUF_COUNT; ++i) {
    struct v4l2_buffer buf;
    CLEAR(buf);
    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_USERPTR; buf.index = i;
    buf.m.userptr = (unsigned long)buf_addr[i].start;
    buf.length = buf_addr[i].length;
    if (-1 == xioctl(fd, VIDIOC_QBUF, &buf))
        errno_exit("VIDIOC_QBUF");
}

将内存可映射 BuffER 排队

要将内存可映射缓冲区排队,应用必须通过设置typememory(必须是V4L2_MEMORY_MMAP)和index成员来填充struct v4l2_buffer,如以下摘录所示:

/* Prime buffers */
for (i = 0; i < BUF_COUNT; ++i) {
    struct v4l2_buffer buf; CLEAR (buf);
    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;
    buf.index = i;
    if (-1 == xioctl (fd, VIDIOC_QBUF, &buf))
        errno_exit ("VIDIOC_QBUF");
}

排队 DMABOF 蜂鸣器

要将输出设备的 DMABUF 缓冲区排队到捕获设备的 DMABUF 缓冲区中,应用应填充struct v4l2_buffer,将memory字段设置为V4L2_MEMORY_DMABUF,将type字段设置为V4L2_BUF_TYPE_VIDEO_CAPTURE,并将m.fd字段设置为与输出设备的 DMABUF 缓冲区相关联的文件描述符,如以下摘录所示:

/* Prime buffers */
for (i = 0; i < BUF_COUNT; ++i) {
    struct v4l2_buffer buf; CLEAR (buf);
    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_DMABUF; buf.index	= i;
    buf.m.fd = outdev_dmabuf_fd[i];
    /* enqueue the dmabuf to capture device */
    if (-1 == xioctl (fd, VIDIOC_QBUF, &buf))
        errno_exit ("VIDIOC_QBUF");
}

前面的代码摘录显示了 V4L2DMABUF 导入的工作原理。 Ioctl 中的fd参数是在open()syscall 中获得的与捕获设备相关联的文件描述符。 outdev_dmabuf_fd是包含输出设备的 DMABUF 文件描述符的数组。 例如,您可能想知道这如何在不是 V4L2 但与 DRM 兼容的输出设备上工作。 以下是一个简短的解释。

首先,DRM 子系统以依赖于驱动的方式提供 API,您可以使用这些 API 在 GPU 上分配一个(哑巴)缓冲区,该缓冲区将返回一个 gem 句柄。 DRM 还提供了DRM_IOCTL_PRIME_HANDLE_TO_FDioctl,它允许通过PRIME将缓冲区导出到 DMABUF 文件描述符中,然后通过drmModeAddFB2()API 创建与该缓冲区对应的framebuffer对象(该对象将被读取并显示在屏幕上,或者确切地说,CRT 控制器),以便最终可以使用drmModeSetPlane()drmModeSetPlane()API 呈现它。 然后,应用可以使用DRM_IOCTL_PRIME_HANDLE_TO_FDioctl 返回的文件描述符来设置v4l2_requestbuffers.m.fd字段。 然后,在读取循环中,在每个VIDIOC_DQBUFioctl 之后,应用可以使用drmModeSetPlane()API 更改平面的帧缓冲区和位置。

重要音符

Prime是与GEM集成的drm dma-buf接口层的名称,GEM是 DRM 子系统支持的内存管理器之一

启用流式处理

启用流类似于通知 V4L2 现在将访问输出队列。 应用应该使用VIDIOC_STREAMON来实现这一点。 以下是一个示例:

/* Start streaming */
int ret;
int a = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = xioctl(capt.fd, VIDIOC_STREAMON, &a);
if (ret < 0) {
    perror("VIDIOC_STREAMON\n");
    return -1;
}

前面的摘录很短,但它是 manatory 来启用流,如果没有流,缓冲区以后就不能出列。

出列缓冲区

这实际上是应用读取循环的一部分。 应用使用VIDIOC_DQBUFioctl 将缓冲区出列。 只有在以前启用了流式处理的情况下,才能执行此操作。 当应用调用VIDIOC_DQBUFioctl 时,它指示驱动检查输出队列中是否有已填满的缓冲区,如果有,则输出一个已填满的缓冲区,ioctl 立即返回。 但是,如果输出队列中没有缓冲区,则应用将阻塞(除非在open()系统调用期间设置了O_NONBLOCK标志),直到缓冲区排队并填满为止。

重要音符

尝试在不先将缓冲区排队的情况下将其出列是错误的,VIDIOC_DQBUFioctl 应该返回-EINVAL。 当O_NONBLOCK标志被赋予open()函数时,当没有可用的缓冲区时,VIDIOC_DQBUF立即返回并返回EAGAIN错误代码。

在 buffer 出列并处理其数据后,应用必须立即再次将此缓冲区排回队列,以便可以为下一次读取重新填充它,依此类推。

将内存映射的 Bubueer 出列

以下是将已内存映射的缓冲区出列的示例:

struct v4l2_buffer buf;
CLEAR (buf);
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
if (-1 == xioctl (fd, VIDIOC_DQBUF, &buf)) {
    switch (errno) {
    case EAGAIN:
        return 0;
    case EIO:
    default:
        errno_exit ("VIDIOC_DQBUF");
    }
}
/* make sure the returned index is coherent with the number
 * of buffers allocated  */
assert (buf.index < BUF_COUNT);
/* We use buf.index to point to the correct entry in our  * buf_addr  */ 
process_image(buf_addr[buf.index].start);
/* Queue back this buffer again, after processing is done */
if (-1 == xioctl (fd, VIDIOC_QBUF, &buf))
    errno_exit ("VIDIOC_QBUF");

这可以在循环中完成。 例如,假设您需要 200 张图像。 读取循环可能如下所示:

#define MAXLOOPCOUNT 200
/* Start the loop of capture */
for (i = 0; i < MAXLOOPCOUNT; i++) {
    struct v4l2_buffer buf;
    CLEAR (buf);
    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;
    if (-1 == xioctl (fd, VIDIOC_DQBUF, &buf)) {
        [...]
    }
   /* Queue back this buffer again, after processing is done */
    [...]
}

前面的 snippet 只是使用 g 循环重新实现缓冲区出列,其中计数器表示需要捕获的图像数量。

通过ffERS 将用户指针出列

以下是使用用户指针将缓冲区出列的示例:

struct v4l2_buffer buf; int i;
CLEAR (buf);
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_USERPTR;
/* Dequeue a captured buffer */
if (-1 == xioctl (fd, VIDIOC_DQBUF, &buf)) {
    switch (errno) {
    case EAGAIN:
        return 0;
    case EIO:
        [...]
    default:
        errno_exit ("VIDIOC_DQBUF");
    }
}
/*
 * We may need the index to which corresponds this buffer
 * in our buf_addr array. This is done by matching address
 * returned by the dequeue ioctl with the one stored in our
 * array  */
for (i = 0; i < BUF_COUNT; ++i)
    if (buf.m.userptr == (unsigned long)buf_addr[i].start &&
                        buf.length == buf_addr[i].length)
        break;
/* the corresponding index is used for sanity checks only */ 
assert (i < BUF_COUNT);
process_image ((void *)buf.m.userptr);
/* requeue the buffer */
if (-1 == xioctl (fd, VIDIOC_QBUF, &buf))
    errno_exit ("VIDIOC_QBUF");

上面的代码显示了如何将用户指针缓冲区出列,并且注释良好,足够,不需要任何进一步的解释。 但是,如果需要很多缓冲区,这可以在循环中实现。

读/写 I/O

这是最后一个示例,显示了如何使用read()系统调用将缓冲区出列:

if (-1 == read (fd, buffers[0].start, buffers[0].length)) {
    switch (errno) {
    case EAGAIN:
        return 0;
    case EIO:
        [...]
    default:
        errno_exit ("read");
    }
}
process_image (buffers[0].start);

前面的示例都没有详细讨论过,因为每个示例都使用了在V4L2 用户空间 API一节中已经介绍过的概念。 既然我们已经熟悉了编写 V4L2 用户空间代码,那么让我们看看如何使用专用工具来不编写任何代码,这些工具可用于快速构建相机系统的原型。

V4L2 用户空间工具

到目前为止,我们已经学习了如何编写用户空间代码来与内核中的驱动交互。 对于快速原型和测试,我们可以利用一些社区提供的 V4L2 用户空间工具。 通过使用这些工具,我们可以集中精力进行系统设计,并对摄像机系统进行验证。 最知名的工具是v4l2-ctl,我们将重点介绍它;它随v4l-utils包一起提供。

虽然本章不讨论,但也有yavta工具(代表另一个 V4L2 测试应用),可用于测试、调试和控制摄像机子系统。

使用 V4L2-ctl

v4l2-utils是一个用户空间应用,可用于查询或配置 V4L2 设备(包含子设备)。 该工具可以帮助设置和设计基于 V4L2 的细粒度系统,因为它有助于调整和利用设备的功能。

重要音符

qv4l2是相当于v4l2-ctl的 Qt GUI。 v4l2-ctl是嵌入式系统的理想选择,而qv4l2是交互式测试的理想选择。

列出视频设备及其功能

首先,我们需要使用--list-devices选项列出所有可用的视频设备:

# v4l2-ctl --list-devices
Integrated Camera: Integrated C (usb-0000:00:14.0-8):
	/dev/video0
	/dev/video1

如果有多个设备可用,我们可以在任何v4l2-ctl命令后使用-d选项,以确定特定设备的目标。 请注意,如果未指定-d选项,则默认情况下以/dev/video0为目标。

要获得特定设备的信息,您必须使用-D选项,如下所示:

# v4l2-ctl -d /dev/video0 -D
Driver Info (not using libv4l2):
	Driver name   : uvcvideo
	Card type     : Integrated Camera: Integrated C
	Bus info      : usb-0000:00:14.0-8
	Driver version: 5.4.60
	Capabilities  : 0x84A00001
		Video Capture
		Metadata Capture
		Streaming
		Extended Pix Format
		Device Capabilities
	Device Caps   : 0x04200001
		Video Capture
		Streaming
		Extended Pix Format

前面的命令显示设备信息(如驱动及其版本)及其功能。 也就是说,--all命令提供了更好的冗长。 你应该试一试。

更改设备属性(控制设备)

在我们查看更改设备属性之前,我们首先需要知道设备支持哪些控件,它们的值类型(整数、布尔、字符串等)是什么,它们的默认值是什么,以及可以接受哪些值。

为了获得设备支持的控件列表,我们可以使用带有-L选项的v4l2-ctl,如下所示:

# v4l2-ctl -L
                brightness 0x00980900 (int)  : min=0 max=255 step=1 default=128 value=128
                contrast 0x00980901 (int)    : min=0 max=255 step=1 default=32 value=32
                saturation 0x00980902 (int)  : min=0 max=100 step=1 default=64 value=64
                     hue 0x00980903 (int)    : min=-180 max=180 step=1 default=0 value=0
 white_balance_temperature_auto 0x0098090c (bool)   : default=1 value=1
                     gamma 0x00980910 (int)  : min=90 max=150 step=1 default=120 value=120
         power_line_frequency 0x00980918 (menu)   : min=0 max=2 default=1 value=1
				0: Disabled
				1: 50 Hz
				2: 60 Hz
      white_balance_temperature 0x0098091a (int)  : min=2800 max=6500 step=1 default=4600 value=4600 flags=inactive
                    sharpness 0x0098091b (int)    : min=0 max=7 step=1 default=3 value=3
       backlight_compensation 0x0098091c (int)    : min=0 max=2 step=1 default=1 value=1
                exposure_auto 0x009a0901 (menu)   : min=0 max=3 default=3 value=3
				1: Manual Mode
				3: Aperture Priority Mode
         exposure_absolute 0x009a0902 (int)    : min=5 max=1250 step=1 default=157 value=157 flags=inactive
         exposure_auto_priority 0x009a0903 (bool)   : default=0 value=1
jma@labcsmart:~$

在前面的输出中,"value="字段返回控件的当前值,其他字段不言而喻。

现在我们已经知道了设备支持的控件列表,可以使用--set-ctrl选项更改控件值,如下例所示:

# v4l2-ctl --set-ctrl brightness=192

之后,我们可以使用以下内容检查当前值:

# v4l2-ctl -L
                 brightness 0x00980900 (int)    : min=0 max=255 step=1 default=128 value=192
                     [...]

或者,我们可以使用--get-ctrl命令,如下所示:

# v4l2-ctl --get-ctrl brightness 
brightness: 192

现在可能是调整设备的时候了。 在此之前,我们先来检查一下该设备的视频特性。

设置像素格式、分辨率和帧速率

在选择特定格式或分辨率之前,我们需要枚举可用于该设备的内容。 为了获得支持的像素格式以及分辨率和帧率,需要将--list-formats-ext选项赋予v4l2-ctl,如下所示:

# v4l2-ctl --list-formats-ext
ioctl: VIDIOC_ENUM_FMT
	Index       : 0
	Type        : Video Capture
	Pixel Format: 'MJPG' (compressed)
	Name        : Motion-JPEG
		Size: Discrete 1280x720
			Interval: Discrete 0.033s (30.000 fps)
		Size: Discrete 960x540
			Interval: Discrete 0.033s (30.000 fps)
		Size: Discrete 848x480
			Interval: Discrete 0.033s (30.000 fps)
		Size: Discrete 640x480
			Interval: Discrete 0.033s (30.000 fps)
		Size: Discrete 640x360
			Interval: Discrete 0.033s (30.000 fps)
		Size: Discrete 424x240
			Interval: Discrete 0.033s (30.000 fps)
		Size: Discrete 352x288
			Interval: Discrete 0.033s (30.000 fps)
		Size: Discrete 320x240
			Interval: Discrete 0.033s (30.000 fps)
		Size: Discrete 320x180
			Interval: Discrete 0.033s (30.000 fps)
	Index       : 1
	Type        : Video Capture
	Pixel Format: 'YUYV'
	Name        : YUYV 4:2:2
		Size: Discrete 1280x720
			Interval: Discrete 0.100s (10.000 fps)
		Size: Discrete 960x540
			Interval: Discrete 0.067s (15.000 fps)
		Size: Discrete 848x480
			Interval: Discrete 0.050s (20.000 fps)
		Size: Discrete 640x480
			Interval: Discrete 0.033s (30.000 fps)
		Size: Discrete 640x360
			Interval: Discrete 0.033s (30.000 fps)
		Size: Discrete 424x240
			Interval: Discrete 0.033s (30.000 fps)
		Size: Discrete 352x288
			Interval: Discrete 0.033s (30.000 fps)
		Size: Discrete 320x240
			Interval: Discrete 0.033s (30.000 fps)
		Size: Discrete 320x180
			Interval: Discrete 0.033s (30.000 fps)

从前面的输出中,我们可以看到目标设备支持的是MJPG(mjpeg)压缩格式和 YUYV RAW 格式。

现在,为了更改摄像机配置,首先使用--set-parm选项选择帧速率,如下所示:

# v4l2-ctl --set-parm=30
Frame rate set to 30.000 fps
#

然后,您可以使用--set-fmt-video选项选择所需的分辨率和/或像素格式,如下所示:

# v4l2-ctl --set-fmt-video=width=640,height=480,  pixelformat=MJPG

当涉及到帧速率时,您可能希望将v4l2-ctl--set-parm选项一起使用,只给出帧速率分子-分母固定为1(只允许整数帧速率值)-如下所示:

# v4l2-ctl --set-parm=<framerate numerator>

捕获帧和流

v4l2-ctl支持的选项比您想象的要多。 为了查看可能的选项,您可以打印相应部分的帮助消息。 与流媒体和视频采集相关的常见帮助命令如下:

  • --help-streaming:打印处理流的所有选项的帮助消息
  • --help-subdev:打印处理v4l-subdevX设备的所有选项的帮助消息
  • --help-vidcap:打印获取/设置/列出视频捕获格式的所有选项的帮助消息

通过这些帮助命令,我构建了以下命令,以便在磁盘上捕获 QVGA MJPG 压缩帧:

# v4l2-ctl --set-fmt-video=width=320,height=240,  pixelformat=MJPG \
   --stream-mmap --stream-count=1 --stream-to=grab-320x240.mjpg

我还成功地使用以下命令捕获了具有相同分辨率的原始 YUV 图像:

# v4l2-ctl --set-fmt-video=width=320,height=240,  pixelformat=YUYV \
  --stream-mmap --stream-count=1 --stream-to=grab-320x240-yuyv.raw

除非您使用合适的 RAW 图像查看器,否则无法显示原始 YUV 图像。 为此,必须使用ffmpeg工具转换原始图像,例如,如下所示:

# ffmpeg -f rawvideo -s 320x240 -pix_fmt yuyv422 \
         -i grab-320x240-yuyv.raw grab-320x240.png

您可以注意到原始图像和压缩图像在大小方面有很大的不同,如以下代码片断所示:

# ls -hl grab-320x240.mjpg
-rw-r--r-- 1 root root 8,0K oct.  21 20:26 grab-320x240.mjpg
# ls -hl grab-320x240-yuyv.raw 
-rw-r--r-- 1 root root 150K oct.  21 20:26 grab-320x240-yuyv.raw

请注意,最好在原始捕获的文件名中包含图像格式(如grab-320x240-yuyv.raw中的yuyv),这样您就可以轻松地从正确的格式进行转换。 压缩图像格式不需要此规则,因为这些格式是图像容器格式,其标题描述了后面的像素数据,可以使用gst-typefind-1.0工具轻松读取。 JPEG 就是这样一种格式,下面是其标题的读取方式:

# gst-typefind-1.0 grab-320x240.mjpg 
grab-320x240.mjpg - img/jpeg, width=(int)320, height=(int)240, sof-marker=(int)0
# gst-typefind-1.0 grab-320x240-yuyv.raw 
grab-320x240-yuyv.raw - FAILED: Could not determine type of stream.

现在我们已经完成了工具使用,让我们看看如何更深入地了解 V4L2 调试和用户空间。

在用户空间调试 V4L2

由于我们的视频系统设置可能不是没有 bug,所以 V4L2 提供了一个简单但很大的后门,用于从用户空间进行调试,以便跟踪和消除来自 VL4L2 框架核心或用户空间 API 的问题。

框架调试可以按如下方式启用:

# echo 0x3 > /sys/module/videobuf2_v4l2/parameters/debug
# echo 0x3 > /sys/module/videobuf2_common/parameters/debug

上述命令将指示 V4L2 向内核日志消息添加核心跟踪。 这样,假设故障来自核心,它将很容易跟踪故障的来源。 运行以下命令:

# dmesg
[831707.512821] videobuf2_common: __setup_offsets: buffer 0, plane 0 offset 0x00000000
[831707.512915] videobuf2_common: __setup_offsets: buffer 1, plane 0 offset 0x00097000
[831707.513003] videobuf2_common: __setup_offsets: buffer 2, plane 0 offset 0x0012e000
[831707.513118] videobuf2_common: __setup_offsets: buffer 3, plane 0 offset 0x001c5000
[831707.513119] videobuf2_common: __vb2_queue_alloc: allocated 4 buffers, 1 plane(s) each
[831707.513169] videobuf2_common: vb2_mmap: buffer 0, plane 0 successfully mapped
[831707.513176] videobuf2_common: vb2_core_qbuf: qbuf of buffer 0 succeeded
[831707.513205] videobuf2_common: vb2_mmap: buffer 1, plane 0 successfully mapped
[831707.513208] videobuf2_common: vb2_core_qbuf: qbuf of buffer 1 succeeded
[...]

在前面的内核日志消息中,我们可以看到与内核相关的 V4L2 核心函数调用,以及其他一些细节。 如果由于任何原因,V4L2 内核跟踪对您来说是不必要的或不够的,您也可以使用以下命令启用 V4L2 用户端 API 跟踪:

$ echo 0x3 > /sys/class/video4linux/video0/dev_debug

在运行该命令以允许您捕获原始映像之后,我们可以在内核日志消息中看到以下内容:

$ dmesg
[833211.742260] video0: VIDIOC_QUERYCAP: driver=uvcvideo, card=Integrated Camera: Integrated C, bus=usb-0000:00:14.0-8, version=0x0005043c, capabilities=0x84a00001, device_caps=0x04200001
[833211.742275] video0: VIDIOC_QUERY_EXT_CTRL: id=0x980900, type=1, name=Brightness, min/max=0/255, step=1, default=128, flags=0x00000000, elem_size=4, elems=1, nr_of_dims=0, dims=0,0,0,0
[...]
[833211.742318] video0: VIDIOC_QUERY_EXT_CTRL: id=0x98090c, type=2, name=White Balance Temperature, Auto, min/max=0/1, step=1, default=1, flags=0x00000000, elem_size=4, elems=1, nr_of_dims=0, dims=0,0,0,0
[833211.742365] video0: VIDIOC_QUERY_EXT_CTRL: id=0x98091c, type=1, name=Backlight Compensation, min/max=0/2, step=1, default=1, flags=0x00000000, elem_size=4, elems=1, nr_of_dims=0, dims=0,0,0,0
[833211.742376] video0: VIDIOC_QUERY_EXT_CTRL: id=0x9a0901, type=3, name=Exposure, Auto, min/max=0/3, step=1, default=3, flags=0x00000000, elem_size=4, elems=1, nr_of_dims=0, dims=0,0,0,0
[...]
[833211.756641] videobuf2_common: vb2_mmap: buffer 1, plane 0 successfully mapped
[833211.756646] videobuf2_common: vb2_core_qbuf: qbuf of buffer 1 succeeded
[833211.756649] video0: VIDIOC_QUERYBUF: 00:00:00.00000000 index=2, type=vid-cap, request_fd=0, flags=0x00012000, field=any, sequence=0, memory=mmap, bytesused=0, offset/userptr=0x12e000, length=614989
[833211.756657] timecode=00:00:00 type=0, flags=0x00000000, frames=0, userbits=0x00000000
[833211.756698] videobuf2_common: vb2_mmap: buffer 2, plane 0 successfully mapped
[833211.756704] videobuf2_common: vb2_core_qbuf: qbuf of buffer 2 succeeded
[833211.756706] video0: VIDIOC_QUERYBUF: 00:00:00.00000000 index=3, type=vid-cap, request_fd=0, flags=0x00012000, field=any, sequence=0, memory=mmap, bytesused=0, offset/userptr=0x1c5000, length=614989
[833211.756714] timecode=00:00:00 type=0, flags=0x00000000, frames=0, userbits=0x00000000
[833211.756751] videobuf2_common: vb2_mmap: buffer 3, plane 0 successfully mapped
[833211.756755] videobuf2_common: vb2_core_qbuf: qbuf of buffer 3 succeeded
[833212.967229] videobuf2_common: vb2_core_streamon: successful
[833212.967234] video0: VIDIOC_STREAMON: type=vid-cap

在前面的输出中,我们可以跟踪不同的 V4L2 用户端 API 调用,这些调用对应于不同的ioctl命令及其参数。

V4L2 合规性驱动测试

为了使驱动符合 V4L2,它必须满足一些标准,其中包括通过v4l2-compliance工具测试,该测试用于测试所有类型的 V4L 设备。 v4l2-compliance尝试测试 V4L2 设备的几乎所有方面,它几乎涵盖了所有 V4L2ioctls。

与其他 V4L2 工具一样,可以使用-d--device=命令瞄准视频设备。 如果未指定设备,则目标为/dev/video0。 以下是一段输出摘录:

# v4l2-compliance
v4l2-compliance SHA   : not available
Driver Info:
	Driver name   : uvcvideo
	Card type     : Integrated Camera: Integrated C
	Bus info      : usb-0000:00:14.0-8
	Driver version: 5.4.60
	Capabilities  : 0x84A00001
		Video Capture
		Metadata Capture
		Streaming
		Extended Pix Format
		Device Capabilities
	Device Caps   : 0x04200001
		Video Capture
		Streaming
		Extended Pix Format
Compliance test for device /dev/video0 (not using libv4l2):
Required ioctls:
	test VIDIOC_QUERYCAP: OK
Allow for multiple opens:
	test second video open: OK
	test VIDIOC_QUERYCAP: OK
	test VIDIOC_G/S_PRIORITY: OK
	test for unlimited opens: OK
Debug ioctls:
	test VIDIOC_DBG_G/S_REGISTER: OK (Not Supported)
	test VIDIOC_LOG_STATUS: OK (Not Supported)
[]
Output ioctls:
	test VIDIOC_G/S_MODULATOR: OK (Not Supported)
	test VIDIOC_G/S_FREQUENCY: OK (Not Supported)
[...]
Test input 0:
	Control ioctls:
		fail: v4l2-test-controls.cpp(214): missing control class for class 00980000
		fail: v4l2-test-controls.cpp(251): missing control class for class 009a0000
		test VIDIOC_QUERY_EXT_CTRL/QUERYMENU: FAIL
		test VIDIOC_QUERYCTRL: OK
		fail: v4l2-test-controls.cpp(437): s_ctrl returned an error (84)
		test VIDIOC_G/S_CTRL: FAIL
		fail: v4l2-test-controls.cpp(675): s_ext_ctrls returned an error (

在前面的日志中,我们可以看到/dev/video0已成为目标。 此外,我们注意到我们的驱动不支持Debug ioctlsOutput ioctls(这些都不是故障)。 虽然输出足够详细,但最好还是使用--verbose命令,这会使输出更加用户友好且更加详细。 不用说,如果您想要提交一个新的 V4L2 驱动,该驱动必须通过 V4L2 兼容性测试。

摘要

在本章中,我们介绍了 V4L2 的用户空间实现。 我们从 V4L2 缓冲区管理开始,从视频流开始。 我们还学习了如何处理视频设备属性管理,所有这些都是从用户空间开始的。 然而,V4L2 是一个沉重的框架,不仅在代码方面,在功耗方面也是如此。 因此,在下一章中,我们将讨论 Linux 内核电源管理,以便在不降低系统性能的情况下将系统保持在尽可能低的消耗水平。