v4l2文档之——basic frame IO

一、v4l2驱动编写篇第六A--基本的帧输入输出 
1、基本的帧输入输出
关于视频驱动的这一系列文章己经更新了好几期,但是我们还没有传输过一帧的视频数据。虽然在这一点上,我们己经了解了足够多的关于格式协定方面的细节,我们可以看一下视频帧是如何在应用和设备之间传输的了。
V4L2 API定义了三种不同的传输视频帧的方法,现在有两种是可以实现的:

read() 和write() 系统调被用于普通的方法。根据不同的硬件和驱动来实现,这种方法可能会非常慢 -但也不是一定会那样。

将帧直接以视频流的方法送到应用可以访问的缓冲区。 视频流这际上是传输视频数据的最有效的方法;这种接口还允许在图像帧中附带一些其他信息.。视频流的方法有两种变种,其分别在于缓冲的开辟是在用户空间还是内核空间。

Video4Linux2 API规范提供一种视频的输入输出机制用于帧的传输。然而这种模式还没有实现,因此不能使用。
这一篇将关注的是简单的read()和write()接口,视频流的方式将在下一期来讲解。

2、read() 和 write()
Video4Linux2 规范并没有规定要实现read()和write(),然而很多简单的应用希望这种系统调用可用,所以可能的话,驱动的作者应该使之工作。如果驱动实现这些系统调用,它应该在VIDIOC_QUERYCAP调用时保证V4L2_CAP_READWRITE置位。然而以笔者的经验,多数的应用在使用调用之前,根本就不会是费心查看调用是否可用。

驱动的read()和/或write()方法必须存在相关的video_device结构中的fops字段里。注意:V4L2规范要求实现这些方法的驱动也提供poll()操作。

在一个视频帧捕获设备上实现read()操作是非常直接的:驱动告诉硬件开始捕获帧,发送一帧到用户空间缓冲,然后关停硬件并返回。如果可能的话,驱动应该安排DMA操作直接将数据传送到目的缓冲区,但这种方式只有在控制器可以处理分散/聚集I/O的时候可能。否则,驱动应该在内核里启用帧缓冲区。同样,写操作也是尽可能直接传到设备,否则启用帧缓冲区。

不那么简单的操作也是可以的。例如笔者的”cafe”驱动会在read()操作后让摄像头控制器工作在一种投机的状态,在一秒钟的下一部分,摄像头中的后续帧将会存储在内核的缓冲区中,如果应用发出了另一个读的调用,它将会更快的反应,无续再次启动硬件。一定数目的帧都没有读的话,控制器就会被放回空闲的状态。同理,写操作时,也会廷时几十毫秒,意在帮助应用与硬件帧同步。


2、流参数
VIDIOC_G_PARM 和VIDIOC_S_PARM ioctl()系统调用会调整一些read(),write()专用的参数,其中一些更加普便。它看起来像是一个设置没有明显归属的杂项的调用。我们在这里就了解一下,虽然有些参数同时会影响流输入输出的参数。
支持这些调用的Video4Linux2驱动提供如下两个方法:
int (*vidioc_g_parm) (struct file *file, void *private_data, struct v4l2_streamparm *parms);

     int (*vidioc_s_parm) (struct file *file, void *private_data, struct v4l2_streamparm *parms);
v4l2_streamparm结构包含下面的联合,这一系列文章的读者到现在应该对它们己经很熟悉了。
struct v4l2_streamparm
    {
        enum v4l2_buf_type type;
        union
        {
                struct v4l2_captureparm        capture;
                struct v4l2_outputparm        output;
                __u8 raw_data[200];
        } parm;
    };
type字段描述的是在涉及的操作的类型。对于视频捕获设备,应该为V4L2_BUF_TYPE_VIDEO_CAPTURE。对于输出设备应该为 V4L2_BUF_TYPE_VIDEO_OUTPUT。它的值也可以是V4L2_BUF_TYPE_PRIVATE,在这种情况下,raw_data字 段用来传递一些私有的,不可移植的,很可能不通过内核配置数据 。
对于捕获设备而言,parm.capture字段是要关注的内容,这个结构体如下:
struct v4l2_captureparm
    {
        __u32                   capability;
        __u32                   capturemode;
        struct v4l2_fract  timeperframe;
        __u32                   extendedmode;
        __u32              readbuffers;
        __u32                   reserved[4];
    };
capability字段是一组功能标签。目前为止已经定义的只有一个V4L2_CAP_TIMEPERFRAME,它代表可以改变帧频率。 capturemode也是一个只定义了一个标签的字段:V4L2_MODE_HIGHQUALITY,这个标签意在使硬件在高清模式下工作,实现单帧的 捕获。这个模式可以做出任何的牺牲(包括支持的格式,曝光时间等),以达到设备可以处理的最佳图片质量。
timeperframe字段用于指定想要使用的帧频率,它又是一个结构体:
    struct v4l2_fract {
        __u32   numerator;
        __u32   denominator;
    };
numerator 和denominator所描述的系数给出的是成功的帧之间的时间间隔。另一个驱动相关的字段是:extendedmode,它在API中没有明确的意义。readbuffers字段是read()操作被调用时内核应为输入的帧准备的缓冲区数量。
对于输出设备,其结构体如下:
struct v4l2_outputparm
    {
        __u32                   capability;
        __u32                   outputmode;
        struct v4l2_fract  timeperframe;
        __u32                   extendedmode;
        __u32              writebuffers;
        __u32                   reserved[4];
    };
capability, timeperframe, 和 extendedmode字段与捕获设备中的意义相同。outputmode 和writebuffers与capturemode 和readbuffers对应相同。
当应用想要查询现在的参数时,它会发出一个VIDIOC_G_PARM调用,因而调用驱动的vidioc_g_parm()方法。驱动应该提供现在的设置,不用的话确保将extendedmode设为0,并且把reserved字段永远设为0.
设置参数将调用vidioc_s_parm()。在这种情况下,驱动将参数设为与应用所提供的参数尽可能 近的值,并调整v4l2_streamparm结构体以反应现行使用的值 。例如,应用可以会请求一个比硬件所能提供的更高的帧频率,在这种情况下,帧频率会设为最高,并把imeperframe设为这个最高的帧频率。
如果应用提供timeperframe为0,the driver should program the nominal frame rate associated with the current video norm.(这句不懂)。如果readbuffers 或writebuffers是0,驱动应返回现行设置而不是删除缓冲区。
到现在为止,我们已经可以写一个支持read()和write()方式帧传输的简单的驱动了。然而多数正式的应用要使用流输入输出方式:流的方式使高性能变得更简单;帧可以打包带上附加信息如帧序号,请继续关注本系列的下篇文章,我们将讨论如何在视频驱动中实现流API.
v4l2驱动编写篇第六B--流输入输出 
在本系列文章的上一期中,我们讨论了如何通过read()和write()的方式实现视频帧的传输,这样的实现可以完成基本的工作,却并不是普便上用来实现视频输入输出大家偏爱的方法。为了实现最高的性能和最好的信息传输,视频驱动应该支持V4L2 流输入输出。
使用read()和write()方法,每一帧都要通过I/O操作在用户和内核空间之间拷贝数据。然而, 当使用流输入输出的方式时,这种情况就不会发生。替代的方案是用户与内核空间之间交换缓冲区的指针,这些缓冲区将被映射到应用的地址空间,这也就使零帧复 制数成为可能。有两种流输入输出缓冲区:
    * 内存映谢缓冲区(memory-mapped buffers) (typeV4L2_MEMORY_MMAP) 是在内核空间开辟的;应用通过themmap()系统调用将其映谢到地址空间。这些缓冲区可以是大而相临DMA缓冲区,通过vmalloc()创建的虚拟 缓冲区,或者(如果硬件支持的话)直接在设备的输入输出存储器中开辟的。
    * 用户空间缓冲区 (V4L2_MEMORY_USERPTR) 是在用户空间的应用中开辟的.很明显,在这种情况下,是不需要mmap()调用的,但驱动在有效地支持用户空间缓冲区上的工作将会更难一些。
注意:驱动支持流输入输出的方式并非必需,即便做了实现,驱动也不必两种缓冲区类型都做处理。一个灵活的驱动可以支持更多的应用。在实际应用中,似乎多数应用都是使用内存映射缓冲区的。同时使用两种缓冲区是不可能的。

现在,我们将要探索一下支持流输入输出的众多而邋遢的细节。任何Video4Linux2驱动的作者都要 了解这部分API。然而值得指出的是,有一个更高层次的API,它能够帮助驱动作者写流驱动。当底层设备可以支持分散/聚集I/O的时候,这一层(称为 video-buf)可以使事情变得容易。关于 video-buf API我们将在将来的某期讨论。

支持流输入输出的驱动应该通知应用这一事实,方法是在vidioc_querycap()方法中设置V4L2_CAP_STREAMING标签。注意:并没有办法来描述支持的是哪一种缓冲区,那是后话。

v4l2_buffer结构体
当流输入输出是有效的,帧是以v4l2_buffer的形式在应用和驱动之间传输的。这个结构体是一个复杂的东西,要用很长的时间才能描述完。一个很好的出发点是要注意的,一个缓冲区可以有三种基本的状态:
    * 在驱动的传入队列中:如果驱动用它做什么有用事的话,应用就可以把缓冲区放在这个队列里。对于一个视频捕获设备,传入队列中的缓冲区是空的,等待驱动向其内填入视频数据。对于输入设备来讲,这些缓冲区内是要送入设备的帧数据。
    * 在驱动的传出队列中.这些缓冲区已经经过驱动的处理,正等待应用来使用。对于捕获设备而言,传出缓冲区内是新的帧数据;对出输出设备而言,这个缓冲区是空的。
    * 不在上述两个队列里.在这种状态时,缓冲区是由用户空间拥有的,驱动无法访问。这是应用可以对缓冲区进行操作的唯一的时间。我们称其为用户空间状态。
这些状态和造成他们之间传输的操作都放在一起,在下图中示出:
 
实际上的v4l2_buffer结构体如下:
    struct v4l2_buffer
    {
        __u32                        index;
        enum v4l2_buf_type      type;
        __u32                        bytesused;
        __u32                        flags;
        enum v4l2_field                field;
        struct timeval                timestamp;
        struct v4l2_timecode        timecode;
        __u32                        sequence;
        /* memory location */
        enum v4l2_memory        memory;
        union {
                __u32           offset;
                unsigned long   userptr;
        } m;
        __u32                        length;
        __u32                        input;
        __u32                        reserved;
    };
index 字段是鉴别缓冲区的序号;它只在内存映射缓冲区中使用。与其它可以在V4L2接口中枚举的对象一样,内存映射缓冲区的index从0开始,依次递增。
type字段描述的是缓冲区的类型 ,通常是V4L2_BUF_TYPE_VIDEO_CAPTURE 或V4L2_BUF_TYPE_VIDEO_OUTPUT.
缓冲区的大小是论长度给定的,单位为byte.缓冲区中的图像数据大小可以在bytesused字段中找到。很明显,bytesused<=length.对于捕获设备而言,驱动会设置bytesused; 对输出设备而言,应用必须设置这个字段。
field字段描述的是图像存在缓冲区的那一个区域。这些区域在这系统文章中的part5a 中可以找到。
timestamp(时间戳)字段,对于输入设备来说,代表帧捕获的时间.对输出设备来说,在没有到达时间戳所代表的时间前,驱动不可以把帧发送出去;时间戳值为0代表越快越好。驱动会把时间戳设为帧的第一个字节传送到设备的时间,或者说是驱动所能达到的最接近的时间。timecode 字段可以用来存放时间编码,对于视频编辑类的应用是非常有用的。
驱动对传过设备的帧维护了一个递增的计数; 每一帧传送时,它都会在sequence字段中存入现行序号。对于输入设备来讲,应用可以观察这一字段来检测帧。
memory 字段表示的是缓冲是内存映射的还是用户空间的。对于内存映谢的缓冲区,m.offset 描述的是缓冲区的位置. 规范将它描述为“从设备存储器开发的缓冲区偏移”,但其实质却是一个 magic cookie,应用可以将其传给mmap(),以确定那一个缓冲区被映射了。然而对于用户空间缓冲区而言,m.userptr是缓冲区的用户空间地址。
input 字段可以用来快速切换捕获设备的输入 – 当然,这要得设备支持帧与帧的快速切换才来。reserved字段应置0.
最后,还有几个标签定义:
    * V4L2_BUF_FLAG_MAPPED 暗示缓冲区己映射到用户空间。它只应用于内存映射缓冲区。
* V4L2_BUF_FLAG_QUEUED: the buffer is in the driver’s incoming queue.
    * V4L2_BUF_FLAG_DONE: 缓冲区在驱动的传出队列.
    * V4L2_BUF_FLAG_KEYFRAME: 缓冲区包含一个关键帧,它在压缩流中是非常有用的.
    * V4L2_BUF_FLAG_PFRAME 和V4L2_BUF_FLAG_BFRAME 也是应用于压缩流中;他们代表的是预测的或者说是差分的帧.
    * V4L2_BUF_FLAG_TIMECODE: timecode 字段有效.
    * V4L2_BUF_FLAG_INPUT: input字段有效.

缓冲区设定
一旦流应用已经完成了基本的设置,它将转去执行组织I/O缓冲区的任务。第一步就是使用VIDIOC_REQBUFS ioctl()来建立一组缓冲区,它将由V4L2转换成对驱动的vidioc_reqbufs()方法的调用。
    int (*vidioc_reqbufs) (struct file *file, void *private_data,  struct v4l2_requestbuffers *req);
我们要关注的所以内容都在v4l2_requestbuffers结构体中,如下所示:
    struct v4l2_requestbuffers
    {
        __u32                        count;
        enum v4l2_buf_type      type;
        enum v4l2_memory        memory;
        __u32                        reserved[2];
    };
type 字段描述的是完成的I/O操作的类型。通常它的值要么是视频获得设备的V4L2_BUF_TYPE_VIDEO_CAPTURE ,要么是输出设备的V4L2_BUF_TYPE_VIDEO_OUTPUT.也有其它的类型,但在这里我们不予讨论。
如果应用想要使用内存映谢的缓冲区,它将会把memory字段置为 V4L2_MEMORY_MMAP,count置为它想要使用的缓冲区的数目。如果驱动不支持内存映射,它就该返回-EINVAL.否则它将在内部开辟请求的缓冲区并返回0.返回之后,应用就会认为缓冲区是存在的,所以任何可以失败的任务都在在这个阶段进行处理 (比如说内存开辟)

注意:驱动并不一定要开辟与请求的一样数目的缓冲区。在很多情况下,有一个最小值缓冲区数的有意义。如果应用请求的比最小值小,可能实际返回的要多于请求的。以笔者的经验,mplayer要用两个缓冲区,如果用户空间速度慢下来的话,这将很容易超支(因而丢失帧)。通过强制一个大一点的最小缓冲区数(可调整的模块参数),cafe_ccic驱动可以使流输入输出通道更加强壮。count 字段应设为方法返回前实际开辟的缓冲区数。
应用可以通设置count字段为0的方式来释放掉所有已存在的缓冲区。在这种情况下,驱动必须在释放缓冲前停止所有的DMA操作,否则会发生非常严重的事情。如果缓冲区已映射到用户空间,则释放缓冲区是不可能的。

相反,如果用的是用户空间缓冲区,则有意义的字段只有缓冲区的type和memory 字段V4L2_MEMORY_USERPTR这个值。应用不需要指定它想用的缓冲区的数目。因为内存是在用户空间开辟的,驱动无须操心。如果驱动支持用户空间缓冲区,它只须注意应用会使用这一特性,返 回0就可以了,否则通常的-EINVAL 返回值会被调用到.
VIDIOC_REQBUFS 命令是应用得知驱动支持的流输入输出缓冲区类型的唯一方法。

将缓冲区映射到用户空间
如果使用了用户空间,在应用向传入队列放置缓冲区之前,驱动看不到任何缓冲区相关的调用。内存映射缓冲区需要更多的设置。应用通常会查看每一个开辟了的缓冲区,并将其映射到地址空间。第一站是VIDIOC_QUERYBUF命令,它将转换成驱动中的 vidioc_querybuf() 方法:
    int (*vidioc_querybuf)(struct file *file, void *private_data, truct v4l2_buffer *buf);
进入这个方法时,buf 字段中要设置的字段有type (在缓冲区开辟时,它将被检查是否与给定的类型相同)和index,它们可以确定一个特定的缓冲区。驱动要保证index有意义,并添充buf中的其余字段。通常来说,驱动内部存储着一个v4l2_buffer结构体的数组, 所以vidioc_querybuf()方法的核心只是一个结构体的赋值。
应用访问内存映射缓冲区的唯一方法就是将其映射到它们的地址空间,所以 vidioc_querybuf() 调用后面通常会跟着一个驱动的mmap()方法 -这个方法,大家要记住,是存储在相关设备的video_device结构体中的fops字段中的。 设备如何处理mmap() 将依赖于内核中缓冲区是如何设置的。如果缓冲区可以在remap_pfn_range() 或remap_vmalloc_range()之前映射,那就应该在这个时间来做.对于内核空间的缓冲区,页也可以在页错误时通过常规的使用 nopage()方法的方式单独被映射,对于需要的人来说,在Linux Device Drivers 可以找到一个关于handlingmmap()的一个很好的讨论。
mmap() 被调用时,传递的VMA结构应该含有vm_pgoff字段中的某个缓冲区的地址-当然是经过PAGE_SHIFT右移过的。特别是,它应该是你的驱动对于 VIDIOC_QUERYBUF调用返回值的楄移。请您遍历缓冲区列表,并确保传入地址匹配其中之一。视频驱动程序不应该是一个可以让恶意程序映射内存的 任意区域手段。
你所提供的偏移值可几乎所有的东西.有些驱动只是返回 (index<<PAGE_SHIFT),意思是说传入的vm_pgoff字段应该正好是缓冲区索引。有一件事你不可以做的就是把缓冲区的内 核实际地址存储到offset字段,把内核地址泄露给用户空间永远也不是一个好注意。
当用户空间映射缓冲区时,驱动应该在相关的v4l2_buffer结构体中调置 V4L2_BUF_FLAG_MAPPED标签.它也必须设定open() 和close() VMA 操作,这样它才可以跟踪映谢了缓冲区的进程数。只要缓冲区在哪里映射了,它就不可以在内核里释放掉。如果一个或多个缓冲区的映谢计算降为0,驱动就应该停 止正在进行的输入输出,因为没有进程要用它。
流输入输出
到现为止,我们己经看了很多设置,却没有传输过一帧的数据,我们离这步己经很近了,但在些之前还有一个步 骤要做。当应用通过 VIDIOC_REQBUFS获得了缓冲区后,那个缓冲区都处于用户空间状态。如果他们是用户空间缓冲区,他们甚至还并不真的存在。在应用可以开始流的输入输出之前,它必须至少将一个缓冲区放到驱动传入队列,对于输出设备,那些缓冲区当然还要先添完有效的帧数据。
要把一个缓冲区加入队列,应用首先要发出一个VIDIOC_QBUF ioctl()调用,这个调用V4L2会映射为对驱动的vidioc_qbuf()方法的调用。
    int (*vidioc_qbuf) (struct file *file, void *private_data, struct v4l2_buffer *buf);
对于内存映射缓冲而言,还是那样,只有buf的type和 index字段有效. 驱动只能进行一些明显的检查(type 和index 有意义,缓冲区还没有在驱动的队列里,缓冲区己映射等。),把缓冲区放在传入队列里 (设置V4L2_BUF_FLAG_QUEUED 标签),并返回.

在这点上,用户空间缓冲区可能会更加复杂,因为驱动可能从来没看过缓冲区的样子。使用这个方法时,允许应用在每次向队列传入缓冲区时,传递不同的地址,所以驱动不能提前做任何的设置。如果你的驱动通过内核空间缓冲区将帧送回,它只须记录一下应用提供的用户空间地址就可以了。然而如果你正尝试将通过 DMA直接将数据传送到用户空间,那将会非常的具有挑战性。

要想把数据直接传送到用户空间,驱必须先fault in缓冲区的所以的页,并将其锁定。get_user_pages() 可以做这件事。注意这个函数会开辟很大的内存空间和硬盘I/O-它可能会卡住很长时间。你得注意要保证重要的驱动功能不能在 get_user_pages()时停止,因为它可能停止很长时间等待许多视频帧通过。
下面就是要告诉设备把图像数据传到用户空间缓冲区(或是相反的方向)了。缓冲区在物理上不是相临的,相反,它会被分散成很多很多单独的4096字节的页(在大部分架构上如此)。很明显,设备得可以实现分散/聚集DMA操作才行。如果设备立即传输一个完整的视频帧,它就需要接受一个包含很多页的分散列表(scatterlist)。一个 16位格式的VGA解决方案的图像需要150个页,随着图像大小的增加,分散列表的大小也会增加。V4L2规范说:
    如果硬件需要,驱动要与物理内存交换内存页,以产生相临的内存区。这对内核子系统的虚拟内存中的应用来说是很明显的。
然而,笔者却不推荐驱动作者尝试这种深层的虚拟内存技巧。有一个更有前途的方法就是要求用户空间缓冲区分配成大的hugetlb页,但是现在的驱动不那么做。

如果你的设备传输的是小图像(如USB摄像头),直接从DMA到用户空间的设定就简单些。在任何情况下,面对支持直接I/O到用户空间缓冲的改变,驱动作者都应该做到以下两点:(1)确定的确值得这么大的麻烦,因为应用更趋向于使用内存映射缓冲区。(2)使用video buf层,它可以帮你解决一些痛苦的难 题 。

一旦流输入输出开始,驱动就要从它的传入队列里抓取缓冲区,让设备更快地实现传送请求,然后把缓冲区移动到传出队列。转输开始时,缓冲区标签也要相应调整。像序号和时间戳这样的字段必需在这个时候添充。最后,应用会在传出队列中认领缓冲区,让它变回为用户空 间状态。这是VIDIOC_DQBUF的工作,它最终变为如下调用:

int (*vidioc_dqbuf) (struct file *file, void *private_data, struct v4l2_buffer *buf);

这里,驱动会从传出队列中移除第一个缓冲区,把相关的信息存入buf,通常,如果传出队列是空的,这个调用会处于阻塞状态直到有缓冲区可用。然而V4L2是用来处理非阻塞I/O的,所以如果视频设备是以O_NONBLOCK方式打开的,在队列为空的情况下驱动就该返回-EAGAIN .当然,这个要求也暗示驱动必须为流I/O支持poll();
剩下最后的一个步骤实际上就是告诉设备开始流输入输出操作。这个任务的 Video4Linux2驱动方法是:

int (*vidioc_streamon) (struct file *file, void *private_data, enum v4l2_buf_type type);

    int (*vidioc_streamoff)(struct file *file, void *private_data, enum v4l2_buf_type type);

对vidioc_streamon()的调用应该在检查类型有意义之后让设备开始工作。如查需要的话,驱动可以请求传入队列中有一定数目的缓冲区后再开始流的转输.
当应用关闭时,它应发出一个对vidioc_streamoff()的调用,这个调用要停止设备。驱动还应从传入传出队列中移除所有的缓冲区,使它们都处于用户空间状态。当然,驱动必须准备好,应用可能在没有停流转输的情况下关闭设备。


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值