首页 > 代码库 > V4L2文档翻译(十二)

V4L2文档翻译(十二)

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()函数。