设备驱动的主要目的是控制和利用底层硬件,同时向用户展示功能。 这些用户可以是在用户空间或其他内核驱动中运行的应用。 虽然前两章讨论的是 V4L2 设备驱动,但在本章中,我们将学习如何利用内核公开的 V4L2 设备功能。 我们将从描述和枚举用户空间 V4L2API 开始,然后学习如何利用这些 API 从传感器获取视频数据,包括破坏传感器属性。
本章将介绍以下主题:
- V4L2 用户空间 API
- 来自用户空间的视频设备属性管理
- 来自用户空间的缓冲区管理
- V4L2 用户空间工具
为了充分利用本章,您需要以下内容:
- 具备高级计算机体系结构知识和 C 编程技能
- Linux 内核 v4.19.X 源代码,可从https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/refs/tags获得
编写设备驱动的主要目的是简化应用对底层设备的控制和使用。 用户空间处理 V4L2 设备有两种方式:一种是使用一体化实用程序(如GStreamer
及其gst-*
工具),另一种是使用用户空间 V4L2API 编写专用应用。 在本章中,我们只介绍代码 de,因此我们将介绍如何编写使用 V4L2API 的应用。
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_QUERYBUF
ioctl 用于获取缓冲区的信息,mmap()
系统调用可以使用这些信息将缓冲区映射到用户空间。 驱动填充struct v4l2_buffer
结构。VIDIOC_QBUF
用于通过传递与缓冲区相关联的struct v4l2_buffer
结构来对该缓冲区进行排队。 在此 ioctl 的执行路径上,驱动会将此缓冲区添加到其缓冲区列表中,以便在前面没有挂起的排队缓冲区时将其填满。 一旦缓冲区被填满,它就会被传递到 V4L2 内核,该内核维护自己的列表(即就绪缓冲区列表),并将其从驱动的 DMA 缓冲区列表中移出。VIDIOC_DQBUF
用于通过传递与缓冲区相关的struct v4l2_buffer
结构,将已填满的缓冲区(从 V4L2 的输入设备就绪缓冲区列表中)或显示的(输出设备)缓冲区出列。 如果没有准备好的缓冲区,这将阻塞,除非O_NONBLOCK
与open()
一起使用,在这种情况下,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_QUERYCAP
ioctl 命令执行此操作。 为了实现这一点,应用传递一个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_QUERYCAP
ioctl 从代码中查询设备功能:
#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_QBUF
ioctl)。 驱动按照缓冲区排队的顺序填充缓冲区。 填充后,每个缓冲区将从输入队列移出,放入输出队列(即用户队列)。
每当用户应用调用VIDIOC_DQBUF
以使缓冲区出列时,都会在输出队列中查找该缓冲区。 如果它在其中,缓冲区将出列并推送到用户应用;否则,应用将等待,直到缓冲区被填满。 用户使用完缓冲区后,必须对该缓冲区调用VIDIOC_QBUF
,以便将其重新排入输入队列,以便可以再次填充。
驱动初始化后,应用调用VIDIOC_REQBUFS
ioctl 来设置需要使用的缓冲区数量。 一旦授权,应用就会使用VIDIOC_QBUF
将所有缓冲区排队,然后调用VIDIOC_STREAMON
ioctl。 然后,驱动自行前进,填满所有排队的缓冲区。 如果没有更多的排队缓冲区,则驱动将等待应用将缓冲区排队。 如果出现这种情况,则意味着某些帧在捕获本身中丢失。
在确保设备属于正确类型并支持其可以使用的模式后,应用必须协商其所需的视频格式。 应用必须确保视频设备配置为以应用可以处理的格式发送视频帧。 在开始抓取和收集数据(或视频帧)之前,它必须这样做。 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_FMT
ioctl 命令查询当前缓冲区格式。 它必须传递一个设置了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_PARM
ioctl 查询视频设备的流参数。 此 ioctl 接受新的struct v4l2_streamparm
结构及其type
成员集作为参数。 此类型应为enum v4l2_buf_type
值之一。 -
Checking
v4l2_streamparm.parm.capture.capability
and making sure theV4L2_CAP_TIMEPERFRAME
flag is set. This means that the driver allows changing the capture frame rate.如果是这样,我们可以(可选)使用
VIDIOC_ENUM_FRAMEINTERVALS
ioctl 来获得可能的帧间隔列表(API 使用帧间隔,它与帧速率相反)。 -
使用
VIDIOC_S_PARM
ioctl 并用适当的值填充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_REQBUFS
ioctl 就是为了实现这一点。 此 ioctl 采用新的struct v4l2_requestbuffers
结构作为参数。 在提供给 ioctl 之前,v4l2_requestbuffers
必须设置它的一些字段:
v4l2_requestbuffers.count
:应使用要分配的内存缓冲区数量设置此成员。 此成员应设置一个值,以确保不会因为输入队列中缺少排队缓冲区而丢弃帧。 大多数情况下,3
或4
是正确的值。 因此,驱动可能会对请求的缓冲区数量感到不舒服。 在这种情况下,驱动将使用 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_MMAP
、V4L2_MEMORY_USERPTR
和V4L2_MEMORY_DMABUF
。 这些都是流方法。 但是,根据此成员的值,应用可能需要执行其他任务。不幸的是,VIDIOC_REQBUFS
命令是应用发现给定驱动支持哪些类型的流 I/O 缓冲区的唯一方法。 然后,应用可以尝试VIDIOC_REQBUFS
个个值,并根据个值的失败或成功来调整其逻辑。
此步骤涉及支持流模式的驱动,特别是支持用户指针 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 毫不迟疑,让我们来介绍一下。
在驱动缓冲区模式下,此 ioctl 还返回在v4l2_requestbuffer
结构的count
成员中分配的实际缓冲区数。 该流方法还需要个新数据结构struct v4l2_buffer
。 在内核中的驱动分配缓冲区之后,此结构与VIDIOC_QUERYBUFS
ioctl 一起使用,以便查询每个已分配缓冲区的物理地址,该地址可与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_QUERYBUF
ioctl,以获得它们的对应的物理地址,如下面的示例所示:
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 是,主要用于mem2mem
设备,并引入了导出器和导入器的概念。 假设驱动A想要使用驱动B创建的缓冲区;然后我们调用B作为导出器,调用A作为缓冲区用户/导入器。
export
方法指示驱动通过文件描述符将其 DMA 缓冲区导出到用户空间。 应用使用VIDIOC_EXPBUF
ioctl 实现这一点,并需要一个新的数据结构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_EXPBUF
ioctl。 此 ioctl 扩展了内存映射 I/O 方法,因此它仅适用于V4L2_MEMORY_MMAP
个缓冲区。 但是,在使用VIDIOC_EXPBUF
导出捕获缓冲区然后映射它们时,它实际上是无用的。 您应该改用V4L2_MEMORY_MMAP
。
当涉及到 V4L2 输出设备时,VIDIOC_EXPBUF
变得非常有趣。 这样,应用使用VIDIOC_REQBUFS
ioctl 在捕获和输出设备上分配缓冲区,然后应用将输出设备的缓冲区导出为 DMABUF 文件描述符,并使用这些文件描述符在捕获设备上排队 ioctl 之前设置v4l2_buffer.m.fd
字段。 然后,排队的缓冲器将填充其对应的缓冲器(对应于v4l2_buffer.m.fd
的输出设备缓冲器)。
在下面的示例中,我们将输出设备缓冲区导出为 DMABUF 文件描述符。 这假设已经使用VIDIOC_REQBUFS
ioctl 分配了该输出设备的缓冲器,其中req.type
将设置为V4L2_BUF_TYPE_VIDEO_OUTPUT
,req.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_QBUF
ioctl。 将缓冲区排队会将该缓冲区的内存页锁定在物理内存中。 这样,这些页面就不能换出到磁盘。 请注意,这些缓冲区将保持锁定状态,直到它们出列,直到调用VIDIOC_STREAMOFF
或VIDIOC_REQBUFS
ioctls,或者直到设备关闭。
在 V4L2 上下文中,锁定缓冲区意味着将此缓冲区传递给驱动以进行硬件访问(通常为 DMA)。 如果应用访问(读/写)锁定的缓冲区,则结果未定义。
要将缓冲区入队,应用必须准备struct v4l2_buffer
,v4l2_buffer.type
、v4l2_buffer.memory
和v4l2_buffer.index
应根据缓冲区类型、流模式和缓冲区分配时的索引进行设置。 其他字段取决于流模式。
重要的个音符
读/写 I/O方法不需要排队。
对于捕获应用,习惯上是在开始捕获并进入读取循环之前将一定数量的空缓冲区(大多数情况下是分配的缓冲区数量)入队。 这有助于提高应用的 Smoot 完整性,并防止它因为缺少填充的 buffer 而被阻塞。 这应该在分配缓冲区之后立即执行。
要将用户指针缓冲区排队,应用必须将v4l2_buffer.memory
成员设置为V4L2_MEMORY_USERPTR
。 这里的特殊性是v4l2_buffer.m.userptr
字段,该字段必须设置为先前分配的缓冲区地址,并将v4l2_buffer.length
设置为其大小。 当使用多平面 API 时,必须使用传递的struct v4l2_plane
数组的m.userptr
和length
成员:
/* 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");
}
要将内存可映射缓冲区排队,应用必须通过设置type
、memory
(必须是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");
}
要将输出设备的 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_FD
ioctl,它允许通过PRIME
将缓冲区导出到 DMABUF 文件描述符中,然后通过drmModeAddFB2()
API 创建与该缓冲区对应的framebuffer
对象(该对象将被读取并显示在屏幕上,或者确切地说,CRT 控制器),以便最终可以使用drmModeSetPlane()
或drmModeSetPlane()
API 呈现它。 然后,应用可以使用DRM_IOCTL_PRIME_HANDLE_TO_FD
ioctl 返回的文件描述符来设置v4l2_requestbuffers.m.fd
字段。 然后,在读取循环中,在每个VIDIOC_DQBUF
ioctl 之后,应用可以使用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_DQBUF
ioctl 将缓冲区出列。 只有在以前启用了流式处理的情况下,才能执行此操作。 当应用调用VIDIOC_DQBUF
ioctl 时,它指示驱动检查输出队列中是否有已填满的缓冲区,如果有,则输出一个已填满的缓冲区,ioctl 立即返回。 但是,如果输出队列中没有缓冲区,则应用将阻塞(除非在open()
系统调用期间设置了O_NONBLOCK
标志),直到缓冲区排队并填满为止。
重要音符
尝试在不先将缓冲区排队的情况下将其出列是错误的,VIDIOC_DQBUF
ioctl 应该返回-EINVAL
。 当O_NONBLOCK
标志被赋予open()
函数时,当没有可用的缓冲区时,VIDIOC_DQBUF
立即返回并返回EAGAIN
错误代码。
在 buffer 出列并处理其数据后,应用必须立即再次将此缓冲区排回队列,以便可以为下一次读取重新填充它,依此类推。
以下是将已内存映射的缓冲区出列的示例:
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 循环重新实现缓冲区出列,其中计数器表示需要捕获的图像数量。
以下是使用用户指针将缓冲区出列的示例:
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");
上面的代码显示了如何将用户指针缓冲区出列,并且注释良好,足够,不需要任何进一步的解释。 但是,如果需要很多缓冲区,这可以在循环中实现。
这是最后一个示例,显示了如何使用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-ctl
,我们将重点介绍它;它随v4l-utils
包一起提供。
虽然本章不讨论,但也有yavta工具(代表另一个 V4L2 测试应用),可用于测试、调试和控制摄像机子系统。
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 调试和用户空间。
由于我们的视频系统设置可能不是没有 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-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 ioctls
和Output ioctls
(这些都不是故障)。 虽然输出足够详细,但最好还是使用--verbose
命令,这会使输出更加用户友好且更加详细。 不用说,如果您想要提交一个新的 V4L2 驱动,该驱动必须通过 V4L2 兼容性测试。
在本章中,我们介绍了 V4L2 的用户空间实现。 我们从 V4L2 缓冲区管理开始,从视频流开始。 我们还学习了如何处理视频设备属性管理,所有这些都是从用户空间开始的。 然而,V4L2 是一个沉重的框架,不仅在代码方面,在功耗方面也是如此。 因此,在下一章中,我们将讨论 Linux 内核电源管理,以便在不降低系统性能的情况下将系统保持在尽可能低的消耗水平。