Linux多媒体子系统01:从用户空间使用V4L2子系统

1 V4L2应用编程基础

1.1 概述

  1. V4L2应用编程需要使用如下系统调用,

open(): 打开V4L2设备
close(): 关闭V4L2设备
ioctl(): 向V4L2设备驱动程序发送控制命令
mmap(): 将V4L2设备驱动程序分配的缓冲区内存映射到用户空间
read()或write(): 这2个系统调用是否支持取决于流传输方法

  1. V4L2应用编程所需的宏定义和数据结构可通过如下头文件包含,

#include <linux/videodev2.h>

说明1:videodev2.h头文件路径

应用程序和内核驱动都需要包含videodev2.h头文件,才能够确保用户态和内核态数据结构一致,其中,

① 用户态头文件由工具链提供

② 内核态头文件为include/uapi/linux/videodev2.h,但是相应的驱动程序中只需要包含<linux/videodev2.h>即可,该头文件会包含uapi目录下的头文件

说明2:V4L2 ioctl控制命令按如下方式定义,详细分析在后文的程序分步骤详解中进行,这种控制命令定义方式可参考Linux设备驱动基础02:Linux内核模块 chapter 3.6

扫描二维码关注公众号,回复: 14614392 查看本文章

1.2 UVC Camera图像采集程序

1.2.1 概述

  1. 实验硬件设备为飞凌imx8mp开发板 + uvc camera

  1. 本示例用于说明V4L2应用编程的基本框架,以及如何获取并保存single-planar格式的图像数据

1.2.2 程序分步骤详解

1.2.2.1 打开设备节点

说明1:打印信息

说明2:在当前实验环境中,UVC camera对应的设备节点为/dev/video4

说明3:对于V4L2设备,也可以在open函数中指定O_NONBLOCK标志,即以非阻塞模式打开。在非阻塞模式下,如果在进行ioctl + VIDIOC_DQBUF操作时没有已经就绪的缓冲区,则应用程序不会阻塞,而是立即返回-1,并且将errno置为EAGAIN

1.2.2.2 获取设备能力

说明1:打印信息

说明2:v4l2_capability结构体

说明3:VIDIOC_QUERYCAP控制命令

① VIDIOC_QUERYCAP命令用于向驱动程序查询设备能力

② 命令参数传递方向从内核态到用户态,用户态需要传递v4l2_capability结构体地址,驱动程序会向其中填充设备能力信息

说明4:capabilities和device_cap字段的关系

① 一个物理设备可以导出多个设备节点供用户态使用(e.g. 一个物理设备可以同时导出/dev/videoX、/dev/vbiY和/dev/radioZ),capabilities字段就是描述给物理设备作为一个整体的能力

② device_cap字段则是描述当前打开设备的能力(e.g. 上述物理设备导出的/dev/videoX设备节点的能力)

说明5:capabilities和device_cap字段的构成

capabilities和device_cap字段由一系列标志位构成,此处简要说明如下概念的含义,

① single-planar和multi-planar用于描述图像类型的存储方式,相关概念可参考视频技术基础01:图像基础和前处理 chapter 2

② capture和output用于描述设备数据流向,capture的数据是从设备到内存(e.g. 视频采集设备),output的数据是从内存到设备(e.g. 视频输出设备)

③ memory-to-memory也是用于描述设备的数据流向,即数据从内存到设备再到内存,典型的就是图像codec的数据流向(e.g. 待编码的图像来自内存,编码后的码流也输出到内存)

说明6:实验硬件设备能力分析

① capabilities字段值为0x84a00001,对应标志位如下,

V4L2_CAP_DEVICE_CAPS

设备支持device_caps字段

只有capabilities字段设置该标志位,才表示驱动会设置device_caps字段

V4L2_CAP_STREAMING

设备支持流式IO

V4L2_CAP_EXT_PIX_FORMAT

设备支持格式扩展字段,使用方式详见chapter 1.2.2.5

V4L2_CAP_META_CAPTURE

设备支持获取元数据

V4L2_CAP_VIDEO_CAPTURE

设备支持single-planar格式图像获取

② device_caps字段值为0x04200001,对应标志位如下,

V4L2_CAP_STREAMING

设备支持流式IO

V4L2_CAP_EXT_PIX_FORMAT

设备支持格式扩展字段

V4L2_CAP_VIDEO_CAPTURE

设备支持single-planar格式图像获取

说明7:获取到设备能力后,常见的操作是对其进行检查,以确保设备支持我们需要使用的模式

说明8:使用CLEAR宏清零ioctl参数

① 示例程序在使用cap变量之前,先使用CLEAR宏将其清零,从而避免结构体中原有内容的干扰

② 始终将提供给V4L2 API的参数清零是一种很好的做法

1.2.2.3 枚举设备格式

说明1:打印信息

可见实验硬件设备支持MJPEG和YUYV输出格式

说明2:v4l2_fmtdesc结构体

说明2:VIDIOC_ENUM_FMT控制命令

① VIDIOC_ENUM_FMT命令用于枚举驱动程序支持的图像格式

② 命令参数传递方向为双向传递,

  • 用户态在传递v4l2_fmtdesc结构体地址时,需要设置其中的type和index字段

  • 驱动程序会填充v4l2_fmtdesc结构体的其他字段

③ 枚举图像格式时,index字段总是从0开始。当枚举完成时,ioctl系统调用返回-1,并将errno设置为EINVAL

说明4:关于缓冲区类型

① 缓冲区类型由v4l2_buf_type枚举值指定,具体枚举值如下,

② 应用程序设置的缓冲区类型主要用于在驱动中索引vb2_queue结构体

从实现逻辑上来说,是驱动程序先指定vb2_queue结构体的缓冲区类型,之后应用程序需要指定相应的缓冲区类型才能索引到正确的vb2_queue结构体

说明5:关于格式标志flags

① 格式标志flags由一系列标志位构成,具体如下,

② 实验硬件设备支持格式的标志flags字段值如下,

  • MJPEG格式的flags字段值为0x1,即MJPEG格式是一种压缩格式

  • YUYV格式的flags字段值为0x0,即YUYV格式既不是压缩格式,也不是软件模拟格式

说明6:关于fourcc格式编码

① 在V4L2中,图像格式使用V4L2_PIX_FMT_开头的宏定义标识,其核心是将描述图像格式的4个字符编码为一个4B无符号整数,也就是fourcc编码

② fourcc编码的具体方式如下,

1.2.2.4 枚举指定格式分辨率

说明1:打印信息

示例程序中分别遍历了设备支持的MJPEG和YUYV格式分辨率

说明2:v4l2_frmsizeenum结构体

说明3:VIDIOC_ENUM_FRAMESIZES控制命令

① VIDIOC_ENUM_FRAMESIZES命令用于枚举驱动程序支持的指定格式的分辨率

② 命令参数传递方向为双向传递,

  • 用户态在传递v4l2_frmsizeenum结构体地址时,需要设置其中的index和pixel_format字段

  • 驱动程序会填充v4l2_frmsizeenum结构体的其他字段

③ 枚举指定格式分辨率时,index字段总是从0开始。当枚举完成时,ioctl系统调用返回-1,并将errno设置为EINVAL

说明4:关于分辨率类型

分辨率类型由v4l2_frmsizetypes枚举值指定,具体枚举值如下,

① 离散型分辨率:设备支持的分辨率由一系列离散值构成

② 阶梯型分辨率:设备支持的分辨率在指定的范围内以步长为单位递增

③ 连续型分辨率:连续型分辨率可以理解为阶梯型分辨率的特例,即步长为1的阶梯型分辨率

1.2.2.5 设置图像格式

在获取了设备支持的格式和分辨率之后,就可以在该范围内根据应用程序的需要设置图像格式

说明1:打印信息

可见应用程序中设置的不是设备支持的分辨率,但是驱动程序会对其进行修改,将其设置为与应用设置值最接近的设备支持的分辨率

说明2:v4l2_format结构体

根据不同的缓冲区类型,会使用不同的图像格式结构体,其中v4l2_pix_format结构体用于描述sigle-planar格式

说明3:VIDIOC_S_FMT控制命令

① VIDIOC_S_FMT命令用于设置设备的图像格式

② 命令参数传递方向为双向传递,

  • 用户态在传递v4l2_format结构体地址时,需要设置其中的type字段,并且可以设置与之对应的图像格式结构体中的所有字段(e.g. 如果type字段设置为V4L2_BUF_TYPE_VIDEO_CAPTURE,则可以设置v4l2_pix_format结构体中的字段)

  • 驱动程序会返回当前实际设置的图像格式

③ 驱动程序会根据设备特性对应用程序传递的图像格式进行修改(e.g. 应用程序传递了设备不支持的分辨率),因此应用程序需要检查VIDIOC_S_FMT命令返回的实际参数

④ 驱动程序只有在应用程序设置的type字段无效时才会返回错误,其他情况下都会根据设备特性进行处理

说明4:为了验证上述分析,我们将图像格式设置为设备不支持的YUV420格式,可见驱动程序会将其修正为设备支持的MJPEG格式

说明5:图像存储方式

① YUYV格式的存储方式和采样方式如下图所示,详情可参考2.7.12. V4L2_PIX_FMT_YUYV (‘YUYV’) ‒ The Linux Kernel documentation

② 示例程序在设置了图像格式为YUYV,分辨率为1280 * 720的情况下,驱动程序填充了bytesperline和sizeimage字段,其中,

  • bytesperline = 2560(= 1280 * 2,YUYV格式存储每个像素需要2B)

  • sizeimage = 1843200(= 1280 * 720 * 2,YUYV格式存储每个像素需要2B)

说明6:如果将图像格式设置为MJPEG,由于码流没有跨距的概念,所以驱动程序将bytesperline字段设置为0;而sizeimage字段也按照1280 * 720 * 2的方式计算,这是因为MJPEG压缩格式的图像大小肯定不会大于YUYV格式的图像

说明7:VIDIOC_G_FMT控制命令

① V4L2提供的VIDIOC_G_FMT命令用于获取设备当前图像格式

② VIDIOC_G_FMT命令传递的参数结构体与VIDIOC_S_FMT命令相同,但是应用程序只需要设置type字段,可见VIDIOC_G_FMT命令获取的图像格式与VIDIOC_S_FMT命令返回的图像格式相同

说明8:VIDIOC_TRY_FMT控制命令

① V4L2提供的VIDIOC_TRY_FMT命令用于尝试应用程序要设置的图像格式是否可行,驱动程序也会根据设备特性对应用程序传递的图像格式进行修改

② VIDIOC_TRY_FMT命令和VIDIOC_S_FMT命令的唯一区别就是他不会修改驱动程序的状态,也就是说VIDIOC_TRY_FMT命令只是尝试设置而不会真正设置图像格式

1.2.2.6 申请缓冲区

说明1:打印信息

可见此处实际分配的缓冲区个数与应用程序申请的个数相同

说明2:v4l2_requestbuffers结构体

说明3:VIDIOC_REQBUFS控制命令

① VIDIOC_REQBUFS命令用于向驱动程序申请用于存储图像数据的缓冲区

② 命令参数传递方向为双向传递,

  • 用户态在传递v4l2_requestbuffers结构体地址时,需要设置其中的count、type和memory字段

  • 驱动程序会根据设备特性修改用户设置的count字段,将其设置为实际分配的缓冲区数量;同时会根据驱动程序支持的缓冲区内存IO模式设置capabilities字段

③ 由于驱动程序实际分配的缓冲区数量可能多于或少于应用程序请求的数量,因此应用程序应在系统调用返回后检查count字段,以确保该值符合需求

④ 如果执行VIDIOC_REQBUFS命令时将count字段设置为0,表示释放已分配的所有缓冲区

说明4:缓冲区内存类型

① 首先需要说明的是,缓冲区内存类型是本文中使用的名字,在Linux内核中也被称作IO mode,例如内核中的vb2_queue结构体中就通过io_modes字段描述相关属性

缓冲区内存类型这个名字主要体现缓冲区内存的分配方式,IO mode这个名字主要体现对缓冲区内存的访问方式

② 需要特别说明的是,在V4L2框架中缓冲区管理结构体的分配和缓冲区内存的分配是可以分离的。在执行VIDIOC_REQBUFS命令时,缓冲区管理结构体总是被分配的(对应内核中的vb2_buffer结构体),但是缓冲区内存的分配时机则是可配置的,这也就引出了缓冲区内存类型的概念

③ 缓冲区内存类型被设置为v4l2_memory枚举值,具体含义如下,

其中,只有mmap类型才会在执行VIDIOC_REQBUFS命令时,既分配缓冲区管理结构体,也分配缓冲区内存

④ 从IO mode的角度,mmap / userptr / dmabuf类型都属于流式访问,与之相对的是read / write访问方式

说明5:关于capabilities字段

① capabilities字段由驱动根据自身支持的IO mode进行设置

② capabilities字段可由如下标志位位或得到,具体含义如下,

③ 示例程序执行VIDIOC_REQBUFS命令后返回的capabilities值为0x17,即相应驱动程序支持mmap / userptr / dmabuf / orphaned_bufs

说明6:VIDIOC_REQBUFS命令是应用程序判断驱动程序支持哪些缓冲区内存类型的唯一方法,如果驱动程序不支持应用程序设置的缓冲区内存类型,ioctl系统调用会返回-1,并将errno置为EINVAL

1.2.2.7 映射缓冲区

我们首先以最常用的mmap方式使用内存缓冲区,

说明1:打印信息

① bytesused字段为0,因为目前缓冲区内存中还没有图像数据

② offset字段每个缓冲区不同,对于示例程序设置的YUYV格式和分辨率,offset字段从0开始,以每帧图像的大小为步长。该字段供驱动程序在处理mmap映射时,索引到对应的缓冲区

说明2:缓冲区内存在用户态的管理结构体

① 一般在应用程序中需要设计相关数据结构来管理缓冲区内存信息

② 示例程序中定义的user_buf结构体兼顾了不同图像格式的plane数量以及不同的缓冲区内存类型

说明3:v4l2_buffer结构体

v4l2_buffer结构体用于描述缓冲区信息,操作缓冲区的控制命令均会传递v4l2_buffer结构体类型参数,包括VIDIOC_QUERYBUF / VIDIOC_QBUF / VIDIOC_DQBUF / VIDIOC_PREPARE_BUF等

说明4:VIDIOC_QUERYBUF控制命令

① VIDIOC_QUERYBUF命令用于获取缓冲区的状态信息

② 命令参数传递方向为双向传递,

  • 对于single-planar图像格式,用户态在传递v4l2_buffer结构体地址时,需要设置index和type字段

  • 驱动程序会根据缓冲区当前状态填充其余字段

③ 对于multi-planar图像格式,需要应用程序准备v4l2_plane数组,并且将数组地址设置到planes字段,具体使用方法详见后文示例

1.2.2.8 将缓冲区加入队列

对于图像采集应用程序,习惯上在开始采集并进入读取循环之前将一定数量的空缓冲区加入队列(在大多数情况下,就是分配的缓冲区数量)。这有助于提高应用程序的流畅性,并防止应用程序因为缺少填充的缓冲区而被阻塞,该操作一般在分配缓冲区之后立即进行

说明1:V4L2缓冲区管理概述

① 对于一个内核态的vb2_queue结构体,V4L2框架会维护2个缓冲区队列,

  • queued_list:list of buffers currently queued from userspace

由应用程序提交给驱动程序使用的缓冲区队列

e.g. 对于图像采集设备,应用程序将缓冲区通过VIDIOC_QBUF操作加入queued_list,以便填充数据

  • done_list:list of buffers ready to be dequeued to userspace

驱动程序处理完成,提交给应用程序使用的缓冲区队列

e.g. 对于图像采集设备,驱动程序会将填充好数据的缓冲区加入done_list,等待应用程序通过VIDIOC_DQBUF操作取出

② 缓冲区在queued_list和done_list队列之间的流转状态如下,

  • 刚分配的缓冲区既不在queued_list,也不在done_list

  • 应用程序通过VIDIOC_QBUF操作,将缓冲区加入queued_list,以便驱动程序使用

  • 驱动程序从queued_list获取缓冲区使用,在操作完成后(e.g. 填充完图像后),将缓冲区再加入done_list

  • 应用程序通过VIDIOC_DQBUF操作,将缓冲区同时从done_list和queued_list队列中取出使用

说明2:VIDIOC_QBUF控制命令

① VIDIOC_QBUF命令供应用程序将准备好的缓冲区提交给驱动程序,V4L2框架会将其加入queued_list队列,

  • 对于输入缓冲区(capture数据流向设备),提交的是空缓冲区

  • 对于输出缓冲区(output数据流向设备),提交的是填充好的缓冲区

② 命令参数传递方向为双向传递,

  • 用户态在传递v4l2_buffer结构体地址时,通用地需要设置其中的index、type和memory字段。根据不同的数据流向和缓冲区内存类型,还需要设置相应字段

  • 对于输出缓冲区(output数据流向设备),需要设置bytesused字段,以标识有效数据长度

  • 如果缓冲区内存类型为userptr,需要设置userptr和length字段

  • 如果缓冲区内存类型为dmabuf,需要设置fd字段,而length字段无需设置

  • 如果缓冲区内存类型为mmap,并不需要设置offset字段(因为内核态V4L2框架可根据index字段索引到相应的offset),也不需要设置length字段(内核态V4L2框架会维护该字段)

  • 对于multi-planar图像格式,需要应用程序准备v4l2_plane数组,并改为设置v4l2_plane结构体中的相应字段

说明3:将缓冲区加入队列会将缓冲区的内存页锁定在物理内存中,这样这些页面就不会被交换到磁盘中,以便于硬件设备访问(通常是DMA操作)

1.2.2.9 开启流传输

说明:VIDIOC_STREAMON控制命令

① VIDIOC_STREAMON命令用于打开流传输

② 命令参数传递方向从用户态到内核态,用户态需要传递存储缓冲区类型的整型变量地址,驱动程序将使用该缓冲区类型索引vb2_queue结构体

1.2.2.10 采集图像

说明1:打印信息

可见驱动程序设置的前2个sequence字段分别为0和2,后续则按1累加(这可能是程序处理耗时较长导致的丢帧)

说明2:VIDIOC_DQBUF控制命令

① VIDIOC_DQBUF命令供应用程序将驱动程序准备就绪的缓冲区取出队列,

  • 对于输入缓冲区(capture数据流向设备),取出的是驱动程序填充好的缓冲区

  • 对于输出缓冲区(output数据流向设备),取出的是驱动程序使用完毕的缓冲区

② 命令参数传递方向为双向传递,

  • 用户态在传递v4l2_buffer结构体地址时,无论缓冲区内存类型和数据流向,只需要设置其中的type和memory字段

  • 驱动程序会设置v4l2_buffer结构体的其余字段

  • 对于multi-planar图像格式,需要应用程序准备v4l2_plane数组,且将数组地址设置到planes字段

③ 在进行VIDIOC_DQBUF操作时,如果没有准备就绪的缓冲区,默认会将调用进程阻塞。如果在打开设备节点时设置O_NONBLOCK标志,则ioctl会立即返回-1,并将errno置为EAGAIN

说明4:保存图像

① 在将缓冲区中的图像写入文件时,应该使用bytesused字段,该字段标识缓冲区中有效数据的长度

② 对于YUV类型图像格式,由于是非压缩类型,缓冲区中有效数据长度和缓冲区长度是一致的。如果将示例程序中的格式改为MJPEG这类压缩类型,则每帧图像有效数据的长度是不同的(但是都不会超过缓冲区长度)

说明5:缓冲区重新加入队列

① 在将缓冲区中的有效数据写入文件后,可以将该缓冲区重新加入队列,用于继续获取图像

② 将缓冲区再次加入队列时,v4l2_buffer结构体中包含了之前驱动程序设置的byteused等字段。但是这不会导致问题,因为V4L2框架不会处理用户态传递的这些字段

说明6:使用poll函数等待缓冲区就绪

① poll函数用于实现多路复用的IO模型,可以通过该函数监听多个文件描述符的状态,poll函数原型如下,

/*
* fds: 要监听的文件描述符及事件数组(pollfd结构体数组)
* nfds: 监听的文件描述符个数,即fds数组的成员个数
* timeout: 监听超时时间,以ms为单位
* 返回值: 
* 有事件发生,返回revents字段不为0的文件描述符个数
* 等待超时,返回0
* 发生错误,返回-1,并设置errno
*/
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

其中pollfd结构体如下,

② 将示例程序修改为使用poll函数等待缓冲区就绪,效果如下,

1.2.2.11 关闭流传输

此处在关闭流传输之后,也关闭了设备节点

说明:VIDIOC_STREAMOFF控制命令

① VIDIOC_STREAMOFF命令用于关闭流传输,该命令会释放所有缓冲区

② 命令参数传递方向从用户态到内核态,用户态需要传递存储缓冲区类型的整型变量地址,驱动程序将使用该缓冲区类型索引vb2_queue结构体

1.2.3 其他缓冲区内存类型操作

1.2.3.1 ION内存分配示例

为了验证userptr和dmabuf缓冲区内存类型,我们通过ION机制分配的物理连续内存进行验证。下面给出ION内存分配的示例程序,并按步骤分析

1.2.3.1.1 打开设备节点

说明:打印信息

1.2.3.1.2 查询ION可用的heap个数

说明1:打印信息

可见ION可用的heap个数为3

说明2:每个ION可用的heap对应/sys/kernel/debug/ion目录下的一个子目录

在每个子目录中,可获取如下信息,

① num_of_buffers:当前通过ION分配的buffer个数

② num_of_alloc_bytes:当前通过ION分配buffer的总字节数

③ alloc_bytes_wm:通过ION分配内存的总字节数水位线(watermark),即记录历史上最高的总字节数

1.2.3.1.3 获取heap信息

此处根据上一步获取的heap个数分配ion_heap_data结构体数组,用于获取heap信息。可见此处使用的也是ION_IOC_HEAP_QUERY命令,只是之前将ion_heap_query.heaps字段设置为NULL,此时驱动程序不会拷贝heap信息

说明1:打印信息

可见通过ION_IOC_HEAP_QUERY操作获取的heap信息与/sys/kernel/debug/ion目录下的信息是匹配的

说明2:示例程序中通过heap_id变量计算ION_HEAP_TYPE_DMA类型heap的掩码,供后续分配内存时使用

1.2.3.1.4 分配物理连续内存

说明1:打印信息

可见ION分配的内存是以dma_buf的方式提供给应用程序

说明2:通过查询/sys/kernel/debug/ion/linux,cma目录下的节点,可见相关信息与示例程序运行状态匹配

1.2.3.1.5 获取分配内存的物理地址

通过DMA_BUF_IOCTL_PHYS操作,可以根据ION分配内存的dma_buf fd获取分配内存的物理地址

说明1:打印信息

说明2:实验环境ION linux,cma heap所在区间

① 实验环境物理内存信息如下,

  • 起始地址为0x4000 0000

  • 大小为0x1 0000 0000(4GB)

  • 物理内存范围为0x4000 0000 ~ 0x1 4000 0000

② 设备树中对linux,cma heap内存的设置如下,

  • 起始地址由操作系统在0x4000 0000 ~ 0xC000 0000之间选择

  • 大小为0x3C00 0000(960MB)

  • 根据内核启动阶段的打印信息,linux,cma heap物理内存范围为0xC400 0000 ~ 0x1 0000 0000,可见示例程序分配内存的物理地址在该范围中

1.2.3.1.6 将分配内存映射到用户空间

如果想在应用程序中操作ION分配的内存,需要将其映射到进程的用户空间

说明1:打印信息

说明2:查看ion_test进程的虚拟地址空间布局,可见分配了128MB的虚拟内存区(vm_area_struct)用于映射dam_buf,示例程序中返回的正是该虚拟内存区的起始地址

说明3:经过验证,关闭ION文件节点不会释放已分配的内存,相关内存将在进程结束时被释放

说明4:实现alloc_ion_buffer函数

① 在示例程序的基础上,封装实现alloc_ion_buffer函数用于后续验证,该函数原型如下,

/*
* fd[IN]: ion设备节点文件描述符
* len[IN]: 要分配的内存大小
* dmabuf_fd[OUT]: 分配内存的dma_buf fd
* paddr[OUT]: 分配内存的物理地址
* vaddr[OUT]: 分配内存的虚拟地址
* 返回值: 成功返回0,错误返回负值错误码
*/
int alloc_ion_buffer(int fd, int len, int *dmabuf_fd,
    unsigned long *paddr, unsigned long *vaddr);

② alloc_ion_buffer函数的实现和测试用例如下,

1.2.3.2 userptr缓冲区内存操作

说明:本节仅记录与mmap类型不同的操作步骤

1.2.3.2.1 申请缓冲区

说明:打印信息

可见此处申请的缓冲区内存类型为V4L2_MEMORY_USERPTR

1.2.3.2.2 获取缓冲区状态

通过VIDIOC_QUERYBUF操作获取各缓冲区状态,此处需要记录缓冲区内存大小,供后续分配缓冲区内存时使用

说明:打印信息

可见此时userptr字段尚未设置,字段值为0

1.2.3.2.3 分配缓冲区内存

说明:打印信息

1.2.3.2.4 将缓冲区加入队列

对于userptr类型缓冲区内存,需要length和userptr字段

说明:关于length字段的正确设置

① 如果将length字段设置为0,VIDIOC_QBUF操作会返回EINVAL错误

② 如果设置length字段的值超过实际分配的内存长度,VIDIOC_QBUF操作会返回EFAULT错误

③ 如果设置length字段的值小于实际分配的内存长度,VIDIOC_QBUF操作会返回EINVAL错误

1.2.3.2.5 采集图像

说明1:打印信息

最终采集图像的效果与使用mmap类型缓冲区内存相同

说明2:在将缓冲区重新加入队列时,由于v4l2_buffer结构体中已经包含了正确的index / type / memory / userptr / length字段,所以无需重新设置

1.2.3.3 dmabuf缓冲区内存操作

说明:本节仅记录与mmap类型不同的操作步骤

1.2.3.3.1 申请缓冲区

说明:打印信息

1.2.3.2.2 获取缓冲区状态

通过VIDIOC_QUERYBUF操作获取各缓冲区状态,此处也需要记录缓冲区内存大小,供后续分配缓冲区内存时使用

说明:打印信息

可见此时fd字段尚未设置,字段值为0

1.2.3.3.3 分配缓冲区内存

此处与userptr类型缓冲区内存的分配是相同的,只是在用户态记录的相关信息不同

说明:打印信息

1.2.3.3.4 将缓冲区加入队列

说明1:打印信息

经过验证,对于当前实验使用的UVC Camera驱动在对dmabuf类型的缓冲区进行VIDIOC_QBUF操作时会报错

说明2:错误分析

① 通过如下2条命令启用V4L2框架调试,然后重新执行示例程序,即可得到相关报错信息

echo 0x3 > /sys/module/videobuf2_v4l2/parameters/debug
echo 0x3 > /sys/module/videobuf2_common/parameters/debug

② 对照内核代码,错误位置如下。可见在进行attach_dmabuf操作时没有报错,但是在进行map_dmabuf操作时映射失败

③ 上述代码中调用的是vb2_queue.mem_ops操作函数集中的回调函数,根据uvc驱动代码,此处设置的操作函数集为vb2_vmalloc_memops(同时可见此处设置了mmap / userptr / dmabuf三种io mode)

④ 在相关函数添加打印信息,可见因为dma_buf.ops->vmap回调函数设置为NULL,因此dma_buf_vmap函数返回NULL,因此映射失败

⑤ 分析ion内核代码,可见确实没有设置vmap回调函数,因此也就不支持在vb2_vmalloc_memops中实现dmabuf到vaddr的映射

1.2.3.4 补充:VIDIOC_EXPBUF控制命令

介绍VIDIOC_EXPBUF控制命令主要是为了与上文的dmabuf类型的缓冲区内存进行辨析

  1. v4l2_exportbuffer结构体

  1. VIDIOC_EXPBUF控制命令

① VIDIOC_EXPBUF命令是mmap类型缓冲区内存的扩展命令,用于将已经申请的mmap类型缓冲区内存导出为dmabuf文件描述符供其他驱动程序使用,因此VIDIOC_EXPBUF命令只能用于mmap类型缓冲区内存

② 命令参数传递方向为双向传递,

  • 用户态在传递v4l2_exportbuffer结构体地址时,需要设置其中的type、index和plane字段(如果对flags字段没有特别要求,可以就设置为0)

  • 驱动程序会根据导出的dmabuf文件描述符设置fd字段

  1. 示例程序

① 导出single-planar格式缓冲区

② 导出multi-planar格式缓冲区

1.3 MIPI Camera图像采集程序

1.3.1 概述

  1. 实验硬件为飞凌imx8mp开发板 + ov5645 mipi camera

  1. 本示例在上一章示例的基础上,说明如何获取并保存multi-planar格式的图像数据

1.3.2 程序分步骤详解

1.3.2.1 打开设备节点

说明1:打印信息

说明2:在当前实验环境中,MIPI Camera对应的设备节点为/dev/video3

1.3.2.2 获取设备能力

说明1:打印信息

说明2:实验硬件设备能力分析

① capabilities字段值为0x84201000,对应标志位如下,

V4L2_CAP_DEVICE_CAPS

设备支持device_caps字段

只有capabilities字段设置该标志位,才表示驱动会设置device_caps字段

V4L2_CAP_STREAMING

设备支持流式IO

V4L2_CAP_EXT_PIX_FORMAT

设备支持格式扩展字段,使用方式详见chapter 1.2.2.5

V4L2_CAP_VIDEO_CAPTURE_MPLANE

设备支持multi-planar格式图像获取

② device_caps字段值为0x4201000,对应标志位如下,

V4L2_CAP_STREAMING

设备支持流式IO

V4L2_CAP_EXT_PIX_FORMAT

设备支持格式扩展字段,使用方式详见chapter 1.2.2.5

V4L2_CAP_VIDEO_CAPTURE_MPLANE

设备支持multi-planar格式图像获取

1.3.2.3 枚举设备格式

说明:打印信息

为了验证multi-planar格式的图像采集,后续将图像格式设置为2 plane的NV12格式

1.3.2.4 枚举指定格式分辨率

说明:打印信息

1.3.2.5 设置图像格式

说明1:打印信息

说明2:v4l2_pix_format_mplane结构体

说明3:图像存储方式

① NV12格式的存储方式和采样方式如下图所示,详情可参考2.7.24. V4L2_PIX_FMT_NV12 (‘NV12’), V4L2_PIX_FMT_NV21 (‘NV21’) ‒ The Linux Kernel documentation

② 示例程序在设置了图像格式为NV12,分辨率为1920 * 1080的情况下,驱动程序填充了2个plane的sizeimage和bytesperline字段,其中,

  • Y分量

bytersperline = 1920 (= 1920 * 1,存储每个Y分量需要1B)

sizeimage = 2073600(= 1920 * 1080 * 1,存储每个Y分量需要1B)

  • UV分量

bytersperline = 1920 (= 1920 / 2 * 2,UV分量水平采样率是Y分量的一半,存储一组UV分量需要2B)

sizeimage = 1036800(= 1920 * 1080 / 2 / 2 * 2,UV分量水平采样率和垂直采样率都是是Y分量的一半,存储一组UV分量需要2B)

1.3.2.6 申请缓冲区

说明1:打印信息

说明2:示例程序执行VIDIOC_REQBUFS命令后返回的capabilities值为0x17,即相应驱动程序支持mmap / userptr / dmabuf / orphaned_bufs

1.3.2.7 映射缓冲区

说明:打印信息

1.3.2.8 将缓冲区加入队列

1.3.2.9 开启流传输

1.3.2.10 采集图像

说明1:打印信息

说明2:将缓冲区重新加入队列时,v4l2_buffer结构体和v4l2_plane数组中均包含需要设置参数的正确值,因此可以不做处理直接加入

说明3:关于sequence字段

① 示例程序中驱动程序填充的sequence字段不连续,3之后是6,猜测可能是将图像数据写入文件耗时较长导致丢帧

② 将示例程序中写入文件的操作注释掉,可见sequence字段连续

1.3.2.11 关闭流传输

1.3.3 其他缓冲区内存类型操作

1.3.3.1 userptr缓冲区内存操作

1.3.3.1.1 申请缓冲区

说明:打印信息

1.3.3.1.2 获取缓冲区状态

通过VIDIOC_QUERYBUF操作获取各缓冲区状态,此处需要记录缓冲区内存每个plane的大小,供后续分配缓冲区内存时使用

说明:打印信息

可见此时userptr字段尚未设置,字段值为0

1.3.3.1.3 分配缓冲区内存

说明:打印信息

1.3.3.1.4 将缓冲区加入队列
1.3.3.1.5 采集图像

说明:打印信息

1.3.3.2 dmabuf缓冲区内存操作

1.3.3.2.1 申请缓冲区

说明:打印信息

1.3.3.2.2 获取缓冲区状态

通过VIDIOC_QUERYBUF操作获取各缓冲区状态,此处也需要记录缓冲区内存每个plane的大小,供后续分配缓冲区内存时使用

说明:打印信息

可见此时fd字段尚未设置,字段值为0

1.3.3.2.3 分配缓冲区内存

说明:打印信息

1.3.3.2.4 将缓冲区加入队列

经过验证,对于dmabuf类型缓冲区内存,在进行VIDIOC_QBUF操作时确实可以不设置length字段

1.3.3.2.5 采集图像

说明:打印信息

2 V4L2用户空间工具

2.1 v4l-utils工具包概述

  1. v4l-utils是由Linux维护的V4L2开发工具包,他提供了一系列用于操作V4L2设备属性和媒体框架的工具,同时还提供了相关函数库(e.g. libv4l2库)

  1. v4l-utils工具包中最常用的工具是v4l2-ctl和media-ctl,其中,

① v4l2-ctl用于查看和配置V4L2设备

② media-ctl用来查看和配置media framework中的各entity的信息

  1. 安装v4l-utils工具包

① 在Ubuntu中可以通过如下命令安装

sudo apt-get install v4l-utils

② 如果想从源码构建v4l-utils工具包,可参考V4l-utils - LinuxTVWiki

2.2 v4l2-ctl工具使用示例

2.2.1 列出视频设备及其功能

  1. 使用--list-devices选项可以列出系统中所有的V4L2设备

v4l2-ctl --list-devices

  1. 如果想要获取指定设备的信息,可以使用-d选项指定设备文件名,并使用-D选项获取其信息

# 1. 如不使用-d选项指定设备,则默认操作/dev/video0
# 2. -d /dev/video<x>可以简化为-d<x>,例如-d4等价于-d /dev/video4
v4l2-ctl -d /dev/video<x> -D

  1. 使用--all选项替换上面的-D选项,则可以显示与该设备相关的所有信息

v4l2-ctl -d /dev/video<x> --all

说明:获取v4l2-ctl工具命令行选项

# 获取帮助信息
v4l2-ctl --help
# 获取所有命令行选项
v4l2-ctl --help-all

通过如下命令则可以获取某个方面的命令行选项,

2.2.2 更改设备属性

  1. 更改设备属性之前,需要了解设备支持哪些属性,这可以通过-L选项实现

  1. 通过--get-ctrl选项可以获取指定属性的当前值

v4l2-ctl -d /dev/video<x> --get-ctrl <属性名称>

  1. 通过--set-ctrl选项可以设置指定属性的值

v4l2-ctl -d /dev/video<x> --set-ctrl <属性名称>=<属性值>

2.2.3 设置图像格式、分辨率和帧率

  1. 设置图像格式、分辨率和帧率之前,需要了解设备支持的参数,这可以通过--list-formats-ext选项实现

v4l2-ctl -d /dev/video<x> --list-formats-ext

可见--list-formats-ext选项会列出输入设备(capture数据流向)支持的图像格式,指定图像格式支持的分辨率,以及指定格式在指定分辨率之下支持的帧率

说明1:对于输出设备(output数据流向),则是使用--list-formats-out-ext选项。需要注意的是,--list-formats-out-ext选项只有较新版本的v4l-utils工具包才支持

说明2:--list-formats-ext相当于是--list-formats、--list-framesizes和--list-frameintervals选项的集合

① 首先使用--list-formats选项列出输入设备支持的图像格式(输出设备则使用--list-formats-out选项)

② 之后使用--list-framesizes选项列出指定图像格式支持的分辨率

# 图像格式名是图像格式的fourcc编码字符
v4l2-ctl -d /dev/video<x> --list-framesizes <图像格式名>

③ 最后使用--list-frameintervals选项列出指定图像格式在指定分辨率之下支持的帧率

v4l2-ctl -d /dev/video<x> --list-frameintervals width=<w>,height=<h>,pixelformat=<f>

  1. 在设置分辨率和帧率之前,需要先设置图像格式,这可以通过--set-fmt-video选项实现

v4l2-ctl -d /dev/video<x> --set-fmt-video width=<w>,height=<h>,pixelformat=<f>

说明1:使用--set-fmt-video选项设置的是输入设备格式(capture数据流向设备,也就是该设备的输出格式),使用--set-fmt-video-out选项则可以设置输出设备格式(output数据流向设备,也就是该设备的输入格式)

说明2:获取当前的图像格式和分辨率

① 使用--get-fmt-video选项可以获取输入设备图像格式和分辨率

② 使用--get-fmt-video-out选项可以获取输出设备图像格式和分辨率

  1. 在确定了图像格式和分辨率之后,就可以设置帧率,这可以通过--set-parm选项实现

说明:使用--get-parm和--set-parm选项是操作输入设备,如果要操作输出设备,则是使用--get-output-parm和--set-output-parm选项

2.2.4 采集帧和流传输

在设置完图像格式、分辨率和帧率之后,就可以进行图像采集了。而其中的核心就是缓冲区的分配与管理,这可以通过--stream-mmap和--stream-count选项实现

# 如果--stream-mmap选项后不加缓冲区个数,默认会分配3个缓冲区
v4l2-ctl -d /dev/video<x> --stream-mmap <缓冲区个数> --stream-count <要采集的图像个数> --stream-to <输出文件名>

说明1:一般可以在设置图像格式和分辨率的同时启动流传输

v4l2-ctl -d /dev/video<x> --set-fmt-video width=<w>,height=<h>,pixelformat=<f> --stream-mmap <缓冲区个数> --stream-count <要采集的图像个数>

说明2:--stream-mmap设置的是输入设备缓冲区内存类型,如果要设置输出设备缓冲区内存类型,需要使用--stream-out-mmap选项

说明3:关于缓冲区内存类型

① --stream-mmap <缓冲区个数>:输入设备缓冲区内存类型为mmap(对应输出设备缓冲区为--stream-out-mmap)

② --stream-user <缓冲区个数>:输入设备缓冲区内存类型为userptr(对应输出设备缓冲区为--stream-out-user)

③ --stream-dmabuf:输入设备缓冲区内存类型为dmabuf,此时需要配套设置--stream-out-mmap选项。也就是说--stream-dmabuf选择仅用于支持memory-to-memory设备,此时v4l2-ctl将mmap类型的输出设备缓冲区内存导出为dmabuf供输入设备使用

同样地,如果使用--stream-out-dmabuf设置输入设备缓冲区内存类型为dmabuf,也需要配套设置--stream-mmap选项。也就是将mmap类型的输入设备缓冲区导出为dmabuf供输出设备使用

说明4:输出到文件

如果要将V4L2设备的输出内容保存到文件(言外之意是可以不保存到文件,此时相当于丢弃采集到的数据,这是v4l2-ctl的默认行为),可以使用如下选项,

① --stream-to:将输出数据写入指定文件

② --stream-to-hdr:与--stream-to相同,但是每帧数据前会有一个header,主要用于压缩格式(e.g. 输出MJPEG类型的数据)

说明5:从文件输入

对于某些V4L2设备,可以从文件中获取输入数据(e.g. 解码器从文件中获取码流数据),此时可以使用如下选项,

① --stream-from:从指定文件获取输入数据

② --stream-from-hdr:与--stream-from相同,但是每帧数据前会有一个header,也是用于压缩格式(e.g. 输入H264类型的码流数据)

2.3 V4L2驱动合规性测试

使用v4l-utils工具包中的v4l2-compliance工具可以测试V4L2设备的所有方面,例如设备对V4L2 ioctl命令的支持特性

3 在用户空间调试V4L2

  1. 使用如下2条命令可以启用V4L2内核调试框架

echo 0x3 > /sys/module/videobuf2_v4l2/parameters/debug
echo 0x3 > /sys/module/videobuf2_common/parameters/debug

启用V4L2内核调试框架后再次执行UVC Camera图像采集程序,此时内核态会打印关键步骤的调试信息

说明:该内核调试框架通过内核模块参数实现,关于内核模块参数的相关内容,可参考Linux设备驱动基础02:Linux内核模块 chapter 3

  1. /sys/module/videobuf2_common/parameters/debug实现

① 在drivers/media/common/videobuf2/videobuf2-core.c文件中定义内核模块参数debug

② videobuf2-core.c文件被编译在videobuf2-common内核模块

  1. /sys/module/videobuf2_v4l2/parameters/debug实现

① 在drivers/media/common/videobuf2/videobuf2-v4l2.c文件中定义内核模块参数debug

② videobuf2-v4l2.c文件被编译在videobuf2-v4l2内核模块

说明1:关于内核模块名中的"-"和"_"

① 在Makefile中指定的模块名是videobuf2-common和videobuf2-v4l2,但是在sysfs中的模块名却是videobuf2_common和videobuf2_v4l2

② 为了验证该现象,我们将Linux设备驱动基础02:Linux内核模块 chapter 2中的hello_module模块改为hello-module模块(源文件需要修改为hello-module.c)

③ 加载hello-module.ko,可见在sysfs中的模块名被修改为hello_module,因此这种修改应该是Linux操作系统的行为

说明2:videobuf2_common模块和videobuf2_v4l2模块对应的源文件中并没有模块加载和模块卸载函数,也就是说内核模块可以没有模块加载和模块卸载函数

为了验证该观点,将Linux设备驱动基础02:Linux内核模块 chapter 2中的hello_module模块修改如下,取消模块加载和模块卸载函数,同时导出模块参数

经过验证,修改后的内核模块可以加载,并且可以正确导出内核模块参数

猜你喜欢

转载自blog.csdn.net/chenchengwudi/article/details/129176862