http://linuxtv.org/downloads/v4l-dvb-apis/io.html
第三节:输入和输出
V4L2 API定义了一些不同的方法来从设备读取或写入,所有需要与应用程序交换数据的驱动最少必须支持其中之一。
在打开一个V4L2设备后会自动选择使用经典的I/O方法read()和write(),当驱动不支持写或读时会失败。
其他的方法必须通过协商。应用程序通过VIDIOC_REQBUFS ioctl来选择内存映射或用户缓存的I/O流,I/O同步方法未定义。
尽管应用程序不直接接受图片数据,视频overlay也可被考虑做其他I/O方法。初始化视频overlay是通过VIDIOC_S_FMT ioctl,其他信息见“视频Overlay接口”章节。
通常一个I/O方法,包括overlay,是与每一个文件描述符关联的。只是以下情况是例外,若应用程序不与驱动进行数据交换(表盘程序,见“打开关闭设备”章节),以及驱动准许使用同一个文件描述符进行视频捕捉和overlay,这是为了对V4L和早期版本的V4L2兼容。
VIDIOC_S_FMT和VIDIOC_REQBUFS会在某方面允许这种情况,但是?对于关闭和重打开设备来说简单驱动不需要支持I/O方法切换(第一次切换read/write后)。之后的小节描述了各种I/O方法的细节。
读/写
若VIDIOC_QUERYCAP ioctl返回的struct v4l2_capability中capabilities包含了V4L2_CAP_READWRITE时,输入和输出设备需要分别支持read()和write()函数。
驱动可能需要CPU进行数据拷贝操作,但是他们也可以通过支持DMA来处理用户内存,所以此I/O方法并不一定比其他只是交换缓冲区指针的方法效率低。因为像帧计数器或时间戳传输的无元数据的一些类型,所以它不优先考虑,此信息对于识别掉帧和与其他数据流同步来说很有必要。无论怎样,这是个简单的IO方法,需要很少甚至不需要任何的设置来交换数据。它允许在命令行中使用(vidvtrl工具的虚构的):
> vidctrl /dev/video --input=0 --format=YUYV --size=352x288
> dd if=/dev/video of=myimage.422 bs=202752 count=1
应用程序使用read()函数从设备中读取,使用write()函数对设备进行写入,若驱动与应用程序交换数据则必须声明一个I/O方法,但是不一定像是这样。若读取或写入支持了,那么驱动还必须支持select()和poll()函数。
在驱动等级select()和poll()是一样的,select()是非常重要的选项。
I/O流 (内存映射)
若VIDIOC_QUERYCAP ioctl后返回的struct v4l2_capability中capabilities包含了V4L2_CAP_STREAMING标志则输入、输出设备要支持这个I/O方法。有两种流方法,应用程序通过VIDIOC_REQBUFS ioctl决定内存映射特性是否支持。
应用程序和驱动只交换缓存指针,而数据本身并不被拷贝的I/O方法叫做“流”。内存映射即是将设备内存中的缓存映射到应用程序的地址空间去,设备内存可以是比如做视频捕捉等时显卡上的视频内存。无论如何,作为长久以来效率最高的I/O方法,非常多的驱动支持流,他们将在可DMA操作的主内存中申请缓存。
一个驱动可支持许多套缓存,每一套通过一个独一无二的缓存类型值定义。他们都是互相独立的,而且每一套缓存处理不同的数据类型。要同时访问不同的缓存必须通过使用不同的文件描述符。
应用程序通过VIDIOC_REQBUFS ioctl申请设备缓存,同时要带入需求的缓存数量和缓存类型,比如V4L2_BUF_TYPE_VIDEO_CAPTURE。这个ioctl也可以用来改变缓存数量或释放已申请的内存,对已映射的缓存无效。
应用程序在操作缓存前必须要通过mmap()函数将他们映射到应用程序地址空间中,而所映射的缓存在设备内存中具体哪个位置是由VIDIOC_QUERYBUF ioctl决定的。在单一平面API中,返回的struct v4l2_buffer中m.offset和length成员用作mmap()函数的第六个和第二个参数。多平面API中,struct v4l2_buffer结构体包含了struct v4l2_plane结构体集合,每一个都包含了各自的,m.offset和length。当使用多平面API时,每个缓存冲的每个平面都需要分别映射,所以调用mmap()的次数就等于缓存数乘以每个缓存内的平面数量。offset和length的值必须不能修改。切记,缓存被分配在物理内存中,不同于虚拟内存的是,可以与硬盘做交换。应用程序在执行了munmap()函数后要尽快释放缓存。
例3.1 单平面API中的缓存映
struct v4l2_requestbuffers reqbuf;
struct {
void *start;
size_t length;
} *buffers;
unsigned int i;
memset(&reqbuf, 0, sizeof(reqbuf));
reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuf.memory = V4L2_MEMORY_MMAP;
reqbuf.count = 20;
if (-1 == ioctl (fd, VIDIOC_REQBUFS, &reqbuf)) {
if (errno == EINVAL)
printf("Video capturing or mmap-streaming is not supported\n");
else
perror("VIDIOC_REQBUFS");
exit(EXIT_FAILURE);
}
/* We want at least five buffers. */
if (reqbuf.count < 5) {
/* You may need to free the buffers here. */
printf("Not enough buffer memory\n");
exit(EXIT_FAILURE);
}
buffers = calloc(reqbuf.count, sizeof(*buffers));
assert(buffers != NULL);
for (i = 0; i < reqbuf.count; i++) {
struct v4l2_buffer buffer;
memset(&buffer, 0, sizeof(buffer));
buffer.type = reqbuf.type;
buffer.memory = V4L2_MEMORY_MMAP;
buffer.index = i;
if (-1 == ioctl (fd, VIDIOC_QUERYBUF, &buffer)) {
perror("VIDIOC_QUERYBUF");
exit(EXIT_FAILURE);
}
buffers[i].length = buffer.length; /* remember for munmap() */
buffers[i].start = mmap(NULL, buffer.length,
PROT_READ | PROT_WRITE, /* recommended */
MAP_SHARED, /* recommended */
fd, buffer.m.offset);
if (MAP_FAILED == buffers[i].start) {
/* If you do not exit here you should unmap() and free()
the buffers mapped so far. */
perror("mmap");
exit(EXIT_FAILURE);
}
}
/* Cleanup. */
for (i = 0; i < reqbuf.count; i++)
munmap(buffers[i].start, buffers[i].length);
例3.2 多平面API中的缓存映射
struct v4l2_requestbuffers reqbuf;
/* Our current format uses 3 planes per buffer */
#define FMT_NUM_PLANES = 3
struct {
void *start[FMT_NUM_PLANES];
size_t length[FMT_NUM_PLANES];
} *buffers;
unsigned int i, j;
memset(&reqbuf, 0, sizeof(reqbuf));
reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
reqbuf.memory = V4L2_MEMORY_MMAP;
reqbuf.count = 20;
if (ioctl(fd, VIDIOC_REQBUFS, &reqbuf) < 0) {
if (errno == EINVAL)
printf("Video capturing or mmap-streaming is not supported\n");
else
perror("VIDIOC_REQBUFS");
exit(EXIT_FAILURE);
}
/* We want at least five buffers. */
if (reqbuf.count < 5) {
/* You may need to free the buffers here. */
printf("Not enough buffer memory\n");
exit(EXIT_FAILURE);
}
buffers = calloc(reqbuf.count, sizeof(*buffers));
assert(buffers != NULL);
for (i = 0; i < reqbuf.count; i++) {
struct v4l2_buffer buffer;
struct v4l2_plane planes[FMT_NUM_PLANES];
memset(&buffer, 0, sizeof(buffer));
buffer.type = reqbuf.type;
buffer.memory = V4L2_MEMORY_MMAP;
buffer.index = i;
/* length in struct v4l2_buffer in multi-planar API stores the size
* of planes array. */
buffer.length = FMT_NUM_PLANES;
buffer.m.planes = planes;
if (ioctl(fd, VIDIOC_QUERYBUF, &buffer) < 0) {
perror("VIDIOC_QUERYBUF");
exit(EXIT_FAILURE);
}
/* Every plane has to be mapped separately */
for (j = 0; j < FMT_NUM_PLANES; j++) {
buffers[i].length[j] = buffer.m.planes[j].length; /* remember for munmap() */
buffers[i].start[j] = mmap(NULL, buffer.m.planes[j].length,
PROT_READ | PROT_WRITE, /* recommended */
MAP_SHARED, /* recommended */
fd, buffer.m.planes[j].m.offset);
if (MAP_FAILED == buffers[i].start[j]) {
/* If you do not exit here you should unmap() and free()
the buffers and planes mapped so far. */
perror("mmap");
exit(EXIT_FAILURE);
}
}
}
/* Cleanup. */
for (i = 0; i < reqbuf.count; i++)
for (j = 0; j < FMT_NUM_PLANES; j++)
munmap(buffers[i].start[j], buffers[i].length[j]);
概念上讲,流驱动包含两个缓存序列,一个传入序列、一个传出序列。他们使同步捕捉或输出操作从应用程序分开来锁定到视频时钟上,因为应用程序可能受限于随机的硬盘或网络延迟,还有其他进程的抢占,因此减少了数据丢失的可能性。这些序列由FIFO管道组成,缓存可以在输入管道上进行输出,可以在输出管道上进行捕捉。
驱动可能会一直需要一个队列缓存的最小数,在没有缓存限制的情况下应用程序可以预先装配、解除和处理。他们还可以在缓存解除前以不同的顺序进行队列装配,驱动可以用任何顺序对空缓存进行填充。缓存的索引号并没有任何规定,只要是唯一的就好。
初始化所有映射缓存要在其没有进入队列的时候进行,驱动很难做到这一点。对于捕捉应用来说,通常是先装配所有已映射缓存,然后开始捕捉并进入读循环中。应用程序会一直等到一个被填充的缓存能被解除(移除队列),然后若不再需要其数据了就重新将其放入队列。输出程序填充缓存,然后将其放入队列中,当累计了足够的缓存后开始VIDIOC_STREAMON。在写循环中,若应用程序把空闲缓存用完了,那么必须等待有空的buffer可以移除队列和再利用。
应用程序通过VIDIOC_QBUF和VIDIOC_DQBUF来使一个缓存入列和出列。缓存的状态是已映射、已入列、满还是空可以在任何时候通过VIDIOC_QUERYBUF ioctl进行查询。有两个方法使应用程序的执行挂起来等待可出列的缓存。VIDIOC_DQBUF会在传出序列中没有缓存时自动阻塞,若打开设备的时候设置了O_NONBLOCK标志则VIDIOC_DQBUF会在没有可用缓存时返回EAGAIN。select()或poll()函数总是可用的。
应用程序通过调用VIDIOC_STREAMON和VIDIOC_STREAMOFF ioctl来开始、停止捕捉或输出。注意VIDIOC_STREAMOFF会移除掉所有的缓存。因为在多任务操作系统中并没有“当前”的概念,所以如果应用程序需要与其他项目进行同步,那么它应该检查struct v4l2_buffer中捕捉的或输出的缓存timestamp。
声明了内存映射I/O的驱动必须支持VIDIOC_REQBUFS, VIDIOC_QUERYBUF, VIDIOC_QBUF, VIDIOC_DQBUF, VIDIOC_STREAMON和VIDIOC_STREAMOFF ioctl,和mmap()、munmap()、select()及poll()函数。
I/O流 (用户指针)
若VIDIOC_QUERYCAP ioctl后返回的struct v4l2_capability中capabilities包含了V4L2_CAP_STREAMING标志则输入、输出设备要支持这个I/O方法。是否需要支持用户指针方法由VIDIOC_REQBUFS ioctl决定。
此I/O方法结合了高级读写以及内存映射方法。缓存(平面)通过应用程序本身进行申请,可以贮存在虚拟或共享内存中,需要交换的只是数据指针。这些指针和元数据在struct v4l2_buffer(对多平面API来说是struct v4l2_plane)中。若调用VIDIOC_REQBUFS且带有相应缓存类型,则驱动必须切换到用户指针I/O模式。不需要预先分配缓存(平面),因此他们没有索引,也不能像映射缓存那样通过VIDIOC_QUERYBUF ioctl进行查询。
例3.3 初始化用户指针I/O流
struct v4l2_requestbuffers reqbuf;
memset (&reqbuf, 0, sizeof (reqbuf));
reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuf.memory = V4L2_MEMORY_USERPTR;
if (ioctl (fd, VIDIOC_REQBUFS, &reqbuf) == -1) {
if (errno == EINVAL)
printf ("Video capturing or user pointer streaming is not supported\n");
else
perror ("VIDIOC_REQBUFS");
exit (EXIT_FAILURE);
}
缓存地址和大小通过VIDIOC_QBUF ioctl快速写入,尽管缓存通常是循环的,应用程序也可以在每个VIDIOC_QBUF调用中使用不同的地址和大小进行区分。如果硬件需要,驱动将在物理内存中进行内存页交换来创建一个连续的内存区域,这对处于内核虚拟内存子系统中的应用来说很显然就有必要。当缓存页被交换到了磁盘上,他们会被拿回来并锁定到物理内存中为了DMA使用。
填充的或显示的缓存通过VIDIOC_DQBUF ioctl出列。驱动可以在DMA完成和这个ioctl间的任何时候解锁内存页。当VIDIOC_STREAMOFF、VIDIOC_REQBUFS调用或设备关闭时内存也会被解锁。应用程序必须要注意,不要把未出列的缓存释放掉。那样的话,这些缓存一直是锁定的,浪费着物理内存。还有一点需要注意的是,当内存被应用程序释放然后利用为其他目的的时候,驱动并不会被通知,比如完成了请求的DMA以及更新了有效数据。
对于捕捉应用程序来说,将一定数量的空缓存入列是很正常的,他们是为了开启捕捉以及进入读循环。应用程序会等待一个填充后的缓存何时能被出列,然后当其数据不再需要的时候将此缓存重新入列。输出应用程序填充缓存,然后将缓存入列,当累积了足够的缓存输出动作就开始了。在写循环中,若应用程序用光了空闲缓存,那么他必须等到某个空缓存可以被出列,然后重新利用它。应用程序通过挂起的方式等待缓存有两种途径,默认的是当传出序列中没有缓存时VIDIOC_DQBUF会阻塞住。而如果在打开设备时候open()设定了O_NONBLOCK参数,那么VIDIOC_DQBUF在这种情况下会直接返回EAGAIN错误代码。这select()和poll()往往都是有效的。
可以通过VIDIOC_STREAMON和VIDIOC_STREAMOFF来控制捕捉或输出应用程序的开始和停止。需要注意的是,VIDIOC_STREAMOFF存在着一定的副作用,就是它会将所有序列冲的缓存移除,并将他们解锁。由于在多任务系统中并没有“当前”的概念,所以如果一个应用程序需要同其他事件进行同步,应该通过检查捕捉或输出的缓存struct v4l2_buffer中的timestamp成员来达成同步的目的。
实现用户指针I/O方法的驱动必须支持VIDIOC_REQBUFS,VIDIOC_QBUF,VIDIOC_DQBUF,VIDIOC_STREAMON及VIDIOC_STREAMOFF ioctl,还有select()和poll()函数。
V4L2文档翻译(十二)