Linux驱动之V4L2设备驱动详解
在学完v4l2应用层编程之后我总是对底层camera驱动有着强烈的好奇心底层是怎么驱动摄像头的v4l2驱动和普通的字符设备驱动有什么区别同样是通过ioctl来操作设备为什么V4L2框架提供的ioctl CMD是固定的所以本文将从应用层open/read/write/ioctl开始逐层解析v4l2框架的具体内容同时深入理解V4L2驱动源码最终总结出我们该如何写一个v4l2设备驱动以及为什么要使用V4L2框架。在看本文之前需要先具备Liunx字符设备驱动开发基础V4L2应用编程基础我们在使用V4L2应用层的时候最繁杂的工作是通过一系列的ioctl来操作具体的硬件的所以我们先从ioctl入手解析一下在调用ioctl的时候是怎么操作到具体摄像头的在理解了ioctl的流程之后类似open read write的系统调用就相当简单了。ioctl 指令功能说明VIDIOC_QUERYCAP查询设备能力。传入v4l2_capability结构体获取驱动名称、总线信息以及设备支持的功能例如是否为视频采集设备V4L2_CAP_VIDEO_CAPTURE是否支持流式 I/OV4L2_CAP_STREAMING。VIDIOC_ENUM_FMT枚举支持格式。枚举设备支持的所有像素格式。应用程序通常使用循环递增 index来遍历。VIDIOC_G_FMT获取当前格式。获取设备当前的格式设置。VIDIOC_TRY_FMT测试格式支持。测试某一种格式或分辨率是否被硬件支持硬件可能会自动修正为传入参数的近似值但不会真正改变当前设置。VIDIOC_S_FMT设置视频格式。正式设置视频流的格式、分辨率和颜色空间。需传入v4l2_format结构体。VIDIOC_REQBUFS申请缓冲区。向驱动申请分配视频缓冲区。传入v4l2_requestbuffers告知驱动需要的缓冲区数量通常 3-4 个及内存管理方式如V4L2_MEMORY_MMAP。VIDIOC_QUERYBUF查询缓冲区状态。查询已分配缓冲区的物理状态长度、偏移量。获取偏移量后应用层可调用mmap()将内核内存映射到用户空间。VIDIOC_QBUF缓冲区入队 (Queue)。将一个空闲的缓冲区“排队”交还给内核供底层驱动向其中填充视频数据。VIDIOC_DQBUF缓冲区出队 (Dequeue)。从内核“出队”一个已经装满视频数据的缓冲区供应用层提取数据进行渲染或编码。VIDIOC_STREAMON开启视频流。驱动开始捕获数据并填充到由QBUF交给它的缓冲区中。VIDIOC_STREAMOFF停止视频流。硬件停止采集数据所有缓冲区的状态会被重置。VIDIOC_QUERYCTRLVIDIOC_QUERYMENU查询控制选项。查询设备支持哪些调节选项例如亮度调节的具体范围、是否包含自动对焦菜单等。VIDIOC_G_CTRLVIDIOC_S_CTRL获取/设置基础控制。用于获取或设置旧版的基础控制参数。VIDIOC_G_EXT_CTRLSVIDIOC_S_EXT_CTRLS获取/设置扩展控制。用于获取或设置扩展控制参数支持批量设置以及更复杂的硬件控制逻辑。在写字符设备驱动的时候我们知道系统层的ioctl调用最终会由驱动中的unlocked_ioctl进行处理。而基于V4L2的驱动本质上也是一个字符设备驱动那么在v4l2中也是一样最终也会由一个unlocked_ioctl函数来进行处理。但是这里我们一定要理清楚两个点普通字符设备驱动没有使用框架当然除字符设备驱动框架外应用层ioctl经过内核转发之后直接调用到我们编写的设备驱动中的ublocked_iotcl函数驱动暴露给应用层相应的ioctl 指令应用层用它来进行具体的ioctl操作我们可以自己随意编写ioctl 指令暴露给应用层。V4L2框架是一个比较复杂的框架其中的核心层帮我们管理了很多事情应用层ioctl经过内核转发之后并不能直接调用到我们编写的设备驱动中的ublocked_iotcl函数还要经过V4L2核心的处理V4L2框架中的ioctl 指令是固定的如上表这也很好理解V4L2是一个通用的video驱动框架倘若ioctl 指令仍然和普通字符设备驱动程序一样可以随意编写那么会给应用编程带来沉重的负担写应用程序的时候我们还要根据不同的摄像头来查询对应的ioctl 指令严重加重了应用层的工作。V4L2框架其实就是总结出摄像头驱动的共性在分析调用流程的时候我们一定要清楚哪一部分是V4L2核心层的工作哪一部分是我们编写的驱动完成的工作。v4l2核心源码位于kernel/drivers/media/v4l2-core目录下我使用的内核源码是6.1版本的不同版本的内核源码在V4L2的实现上一些细节上会有区别比如ioctl会区分是否需要特殊处理。我们暂时只从宏观上讨论调用流程不去管这些差异。在开始进行学习的时候我们先逐步分析整个系统的调用流程是什么样的逐步解析每个结构体的作用等到我们完整的建立起整个v4l2框架的认识之后我们再来分析一个具体的驱动程序来看看每个模块是怎么串联起来的。在开始学习的时候因为结构体太多一定是会被绕晕的这时候不要灰心多往前面翻一翻对照着结构体的定义来看笔者也是在不断学习的过程中希望能一起努力一句掌握v4l2子系统。调用流程分析当应用应用层执行ioctl(fd, VIDIOC_QUERYCAP, cap) 0或者类似的ioctl指令的时候经内核处理后首先进入v4l2核心层在v4l2核心层有一些操作函数集其定义位于kernel/drivers/media/v4l2-core目录下v4l2-dev.c中在这个操作函数集中有v4l2_ioctl 、read、 write、 mmap等函数这些函数是v4l2核心层已经写好的不需要我们更改。核心层会调用操作函数集中的v4l2_ioctl函数指针所指向的函数可以看到这个v4l2_fops本质上还是struct file_operations类型的结构体下面我们进一步看看看v4l2_ioctl的具体处理内容这个处理函数也是v4l2框架写好的首先是第一行struct video_device *vdev video_devdata(filp);这行代码的作用很重要在v4l2核心层操作集的函数中几乎都要用到它它的作用是根据内核传递的struct file指针filp找到具体的struct video_device这个video_device我们这里暂且理解为对具体摄像头的描述那么这一步就是在v4l2核心层中找到具体我们要操作的哪一个摄像头。这一步也很好理解我们在编写字符设备驱动程序的时候在open函数中通常会将字符设备指针保存在file-private_data中。在其他函数中就可以通过struct file* file来找到对应的字符设备。这里也是一样的。我们可以通过file来找到video_device。随后会对设备进行一些检查比如设备是否注册了unlocked_ioctl函数设备是否注册到了内核中然后执行ret vdev-fops-unlocked_ioctl(filp, cmd, arg);这一步是v4l2核心层 v4l2_ioctl 最关键的一步vdev是前面找到的摄像头设备vdev-fops说明这个设备里面也有操作函数集vdev-fops-unlocked_ioctl是调用摄像头设备函数集fops中的unlocked_ioctl可以得出核心层在这里的工作非常简单就是在核心层的操作函数集中转发对应的ioctl给驱动同理对应的open read write mmap等函数也是一样在核心层的操作函数集中转发对应的操作函数给驱动所以核心层目前的工作就是将应用层的ioctl转发给具体的设备那么核心层到这一步工作好像完成了因为下面会跳转到设备的操作函数集去执行相应的操作了但是我们前面提到的V4L2封装的各种ioctl指令是怎么使用的呢我们需要在struct video_device的操作函数集中对每种ioctl进行判断吗我们不得不去研究一下struct video_device这个结构体。这个结构体也是我们在编写V4L2驱动的时候最重要的一个结构体struct video_device结构体的内容非常多我们只关注与上述分析流程相关的内容其他内容会在后面逐步解析struct video_device的定义位于内核源码中 kernel/include/media 目录下的v4l2-dev.h文件中前面在v4l2核心层中执行的等函数就位于 struct video_device 中的 fops 中而这个fops是struct v4l2_file_operations类型的我们再进一步看 struct v4l2_file_operations的详细内容拆解到这里我们已经到达流程的终点了v4l2核心层的open read write 等系统调用最终都会调用到这里面相对应的函数。这里面的函数就是我们编写驱动的时候需要去实现的函数。但是实际情况并没有这么简单iotcl驱动程序示例分析这里不妨在内核源码中找一个对应的驱动程序示例来看看在内核源码目录kernel/drivers/media/usb/airspy 下 airspy.c 中可以看到对设备的定义airspy.c定义了一个设备airspy_template并且填充了.name .release .fops .ioctl_ops等信息前面我们说到struct video_device结构体里面有很多属性为什么这里就填写了这么几个呢这就是v4l2架构带来的好处对于很多共性的东西v4l2框架帮我们进行初始化了v4l2还提供了很多辅助函数帮助我们快速编写驱动代码避免“重复造轮子”我们需要关注的是 airspy_template 中的 fops 与 ioctl_ops操作函数集为什么会有两个操作函数集一个是我们熟知的、刚才分析的fops还有一个ioctl_ops是什么虽然从名字上能猜出来是和ioctl相关的但是在fops中不是已经有unlocked_ioctl了吗为什么这里还要一个ioctl_ops?这就要提到我们在一开始提到的V4L2的ioctl指令是固定的这种固定方法的实现就需要用到这两个操作函数集我们先来看看这个驱动例程是怎么实现fops的绿框内是例程填充的信息这些信息全部都是v4l2提供的辅助函数也就是v4l2写好的函数。我们可以简单地区分v4l2_开头这类函数属于 V4L2 核心层V4L2 Core。它们负责处理那些不涉及具体图像数据的“控制流”。比如管理设备节点、解析 ioctl 指令、处理事件订阅等。vb2_开头这类函数属于Videobuf2 (VB2)子系统。这是 V4L2 框架中最复杂、最精华的部分。凡是和“视频数据流Streaming”、“内存分配”、“缓冲区映射”相关的底层通用逻辑都在这里。重点需要关注的是video_ioctl2这个函数这个函数也是由内核提供的辅助函数我们具体看它是如何实现的这个函数定义在内核源码 kernel/drivers/media/v4l2-core目录中的 v4l2-ioctl.c从名字能看出这个文件主要用于处理v4l2框架中的ioctl事件。这里只是做了一些空户空间数据的拷贝处理完后会跳转到参数中的__video_do_ioctl继续执行下面我们继续分析__video_do_ioctl函数。先来看前几行我将其中需要重点关注的地方标注出来了其中非常关键这个指针指向了一个v4l2_ioctl_info结构体那v4l2_ioctl_info是什么上图是v4l2_ioctl_info结构体的定义可以看到里面就是简单存储了一些属性以及函数指针。现在我们再来看看这个info指针到底指向哪里在源码中可以看到首先是cmd当应用层调用ioctl(fd, VIDIOC_QUERYCAP, cap)时这里的cmd就是VIDIOC_QUERYCAP这个宏的值。 在 Linux 系统中ioctl的命令码并不是随便写的一个数字而是一个经过严格设计的 32 位无符号整数。其位数代表着一些特殊的含义。每一个cmd都有一个唯一的编号其实就是cmd的后八位。_IOC_NR(cmd)作用是根据cmd得到指令的编号。v4l2_ioctls[] 是存放着各种 v4l2_ioctl_info 的数组我们从里面取出用户传入的 cmd 所对应的v4l2_ioctl_info将info指向它下面我们再来看info会做什么会继续调用info里面的函数func到此ioctl的调用流程就又走到了终点其调用流程如下现在我们要分析的问题就变成了分析v4l2_ioctls[]数组因为我们最终调用的函数是从v4l2_ioctls[]数组中取出的元素的func函数指针。下面是这个数组的定义也是在v4l2-ioctl.c文件中我们再回顾一下struct v4l2_ioctl_info结构体可以看到v4l2_ioctls[]的元素都是由一个个IOCTL_INFO宏组成这个IOCTL_INFO宏主要负责将宏中的参数组合成struct v4l2_ioctl_info结构体具体定义如下可以看到struct v4l2_ioctl_info结构体中的信息也是由v4l2-ioctl.c预先填充的好的特别是启动的func函数指针最终会指向如v4l_querycapv4l_enum_fmt等。相当于struct v4l2_ioctl_info { .ioctl VIDIOC_QUERYCAP .flags 0 .name VIDIOC_QUERYCAP, .func v4l_querycap, .debug v4l_print_querycap, };而v4l_querycapv4l_enum_fmt这些函数也是v4l2框架预先写好的所以V4L2框架其实已经将所有的ioctl的cmd执行函数也就是v4l2_ioctl_info里面的func全部都封装好了用数组统一管理起来这里我们也能猜到在v4l2框架封装好的这些函数里面会调用我们自己写的ioctl函数我们以v4l_querycap为例看看他具体干了什么在v4l_querycap会调用传递给这个函数的const struct v4l2_ioctl_ops *ops参数中的函数准确说是函数指针指向的函数这个参数好像似曾相识没错他就是在struct video_device结构体中的.ioctl_ops函数我们进入到这个函数集中看看有没有这个被调用的函数显然第一个函数就是这里我们同样能看到vb2开头的和v4l2开头的函数这些前面说了是辅助函数。而airspy开头的就是这个例程编写的函数。那么这个airspy_querycap显然就是这个例程编写的函数。到此为止我们的ioctl调用流程终于结束了现在还剩下一个问题那就是这个ops是由谁传递给v4l2_querycap的还记得前面的__video_do_ioctl吗正是在这个函数中将设备的ioctl_ops传递给了核心层的v4l2_querycap好了现在我们对整个摄像头驱动的ioctl调用流程有了一个整体的印象。我们用一副图来总结buffer相关ioctl我们在写V4L2应用层程序的时候最重要的步骤其实是与buffer相关的也就是应用层ioctl(fd, VIDIOC_REQBUFS, req) ioctl(fd, VIDIOC_QUERYBUF, buf) ioctl(fd, VIDIOC_QBUF, buf) ioctl(fd, VIDIOC_DQBUF, buf)应用层用这些cmd指令来申请buffer并且将buffer入队或者出队但是这些buffer在底层的管理应用层是不需要关心的。这些buffer的管理显然是在驱动底层实现的。先介绍几个与buffer相关的结构体在struct video_device中有一个很重要的成员叫struct vb2_queue *queue;这个queue就是用来管理buffer的它的详细层级结构如下在vb2_queue中struct vb2_buffer *bufs[VB2_MAX_FRAME]就是用来存放我们申请的buffer的数组当我们调用ioctl VIDIOC_REQBUFS向驱动申请N个buffer时驱动程序分配n(nN)个vb2_buffer结构体用数组进行管理。而对于struct vb2_buffer结构体他的内部又进一步包含struct vb2_plane planes[VB2_MAX_PLANES]结构体这是因为一个相机可能有多个平面的数据对于多平面相机他只是相机的一种我们这里不过多讨论每个平面的数据又是通过结构体struct vb2_plane进行管理的这个数组就是用来存储多个平面的信息的如果相机只有一个平面那么这个数组中就只包含一个平面的信息。平面的数量同样记录在这个结构体中的unsigned int num_planes中。struct vb2_plane中void *mem_priv就是保存数据的地方一般会指向vb2_vmalloc_buf由它来管理最终的buffer。这里结构体的嵌套有点多但其实并不复杂从应用层的角度来说struct vb2_buffer就对应着我们申请的buffer只不过相机可能有多个平面struct vb2_plane的数据每个平面的数据都需要单独进行管理然后我们最终申请到的内存空间也需要有个结构体来管理vb2_vmalloc_buf现在我们申请到了bufferstruct vb2_buffer我们把他放在struct video_device 中的 struct vb2_queue 成员中下面就是对这些buffer的具体操作了比如入队出队以及硬件是怎么往里面填入数据的这里就要关注struct vb2_queue 中的几个操作函数了简单介绍一下这几个结构体的作用const struct vb2_ops是与硬件相关的回调函数我们编写驱动的工作就在这里另外两个结构体中的操作函数基本上不需要我们关心直接使用内核提供的辅助函数即可或者在初始化的时候内核也会帮我们填充这两个结构体。vb2_mem_ops中的是与内存分配相关的函数我们在申请buffer的时候buffer的大小属性等都是由他管理。vb2_buf_ops主要负责用户空间的struct v4l2_buffer和内核vb2_buffer间的通信怎样编写驱动程序ioctl调用流程可以说是比较复杂的一个流程了上面我们将整个流程梳理了一遍可以看到v4l2框架帮我们做了很多事情v4l2核心层会将应用层的 read write等指令下发给struct video_device中的fops操作函数集合最终调用到这个集合中的函数指针指向的函数而这个fops中函数指针指向的函数可以完全由v4l2框架提供不需要我们编写。其中最复杂的ioctl操作函数应用层的ioctl操作函数首先会被v4l2核心层下发给struct video_device中的fops操作函数.unlocked_ioctlunlocked_ioctl会进一步调用__video_do_ioctl函数__video_do_ioctl又会根据ioctl指令找到他在v4l2的静态v4l2_ioctl_info数组中被管理的位置随后调用v4l2_ioctl_info中绑定给cmd指令的函数再这个函数中再调用struct video_device中的const struct v4l2_ioctl_ops *ioctl_ops 函数集中的函数那么我们编写驱动的时候目标也就非常明确了重点是编写struct video_device中的const struct v4l2_ioctl_ops *ioctl_ops 中指向的函数