Linux - 使用V4L2(总结)

一、 什么是V4L2

概述: Video for linux 2(简称V4L2),是linux中关于视频设备的内核驱动
它也是 linux操作系统下用于采集图片、视频和音频数据的 API接口,配合适当的视频采集设备和相应的驱动程序;

作用: 支持许多USB 网络摄像头,电视调谐器和相关设备,使它们的输出标准化,因此程序员可以轻松地向其应用程序添加视频支持。MythTV,tvtime和Tvheadend是使用V4L框架的典型应用程序;
可以实现图片、视频、音频等的采集。在远程会议、可视电话、视频监控系统和嵌入式多媒体终端中都有广泛的应用。

存放位置: Linux中,一切皆文件,视频设备为设备文件,可以像普通文件一样进行读写操作,而采用 V4L2驱动的摄像头设备文件是 /dev/v4l/video0,为了通用,可以建立到一个和普通摄像头一样的 /dev/video0的链接。

V4L2 架构图如下:

Linux - 使用V4L2(总结)_第1张图片

Linux - 使用V4L2(总结)_第2张图片

Linux - 使用V4L2(总结)_第3张图片

在这里插入图片描述

二、 什么是ioctl

概述: ioctl是设备驱动程序中对设备的 I/O通道进行管理的接口函数
所谓对 I/O通道进行管理,就是对设备的一些特性进行控制,例如串口的传输波特率、马达的转速等等。

作用: 一个字符设备驱动通常会实现设备打开、关闭、读、写等功能,在一些需要细分的情境下,如果需要扩展新的功能,通常以增设 ioctl() 命令的方式实现。例:当你用 read,write不能完成某一功能时,就用 ioctl来操作。
配合一些头文件(v4l2-controls.h / videodev2.h),根据命令,实现对摄像头的操作,如:白平衡、聚焦、曝光、饱和度、亮度…

函数:

#include <sys/ioctl.h>  //需要引用的头文件

//参数一:文件描述符
//参数二:设备驱动命令,执行对应操作
//参数三:根据参数二变化
int ioctl(int fd, int cmd, ...) ;

在这里插入图片描述

三、 操作流程

  1. 打开设备文件。(int fd=open("/dev/video0",O_RDWR);)

  1. 查询设备属性:取得设备的capability,看看设备具有什么功能,比如是否具有视频输入,或者音频输入输出等。(ioctl(fd_v4l, VIDIOC_QUERYCAP, &cap);)

  1. 选择视频输入,一个视频设备可以有多个视频输入。(VIDIOC_S_INPUT, struct v4l2_input)

  1. 设置视频采集的参数。 —设置视频的制式,制式包括PAL/NTSC,使用 ioctl(fd_v4l, VIDIOC_S_STD, &std_id)
    —设置视频图像的采集窗口的大小,使用 ioctl(fd_v4l, VIDIOC_S_CROP, &crop)
    —设置视频帧格式,包括帧的点阵格式,宽度和高度等,使用 ioctl(fd_v4l, VIDIOC_S_FMT, &fmt)
    —设置视频的帧率,使用 ioctl(fd_v4l, VIDIOC_S_PARM, &parm)
    —设置视频的旋转方式,使用 ioctl(fd_v4l, VIDIOC_S_CTRL, &ctrl)

  1. 向驱动申请帧缓冲,一般不超过5个。(ioctl(fd_v4l, VIDIOC_REQBUFS, &req);)

  1. 查询帧缓冲区在内核空间中的长度和偏移量 (ioctl(fd_v4l, VIDIOC_QUERYBUF, &buf);)

  1. 将申请到的帧缓冲映射到用户空间 mmap,这样就可以直接操作采集到的帧了,而不必去复制。(buffers[i].start = mmap (NULL, buffers[i].length, PROT_READ | PROT_WRITE, MAP_SHARED,fd_v4l, buffers[i].offset); )

  1. 将申请到的帧缓冲全部入队列,以便存放采集到的数据。(ioctl (fd_v4l, VIDIOC_QBUF, &buf) )

  1. 开始视频的采集。(ioctl (fd_v4l, VIDIOC_STREAMON, &type) )

  1. 出队列以取得已采集数据的帧缓冲,取得原始采集数据。(ioctl (fd_v4l, VIDIOC_DQBUF, &buf) )

  1. 处理完后, 将该帧缓冲重新入队列尾,这样可以循环采集(循环步骤8-10),直到停止采集。

  1. 停止视频的采集。(ioctl (fd_v4l, VIDIOC_STREAMOFF, &type) ;)

  1. 释放申请的视频帧缓冲区 unmap,关闭视频设备。(close(fd_v4l);)

在这里插入图片描述

1. 打开设备文件

  • 阻塞模式 / 非阻塞模式

—应用程序能够使用阻塞模式或非阻塞模式,打开视频设备。

阻塞操作: 是指在执行设备操作时,若不能获得资源,则挂起进程直到满足操作条件后再进行操作。被挂起的进程进入休眠(不占用cpu资源),被从调度器移走,直到条件满足。在设备驱动中,阻塞的实现通常是通过等待队列。

非阻塞操作: 如果使用非阻塞模式调用视频设备,在不能进行设备操作时,并不挂起或者放弃,或者不停地查询,直到可以进行操作(一直占用CPU资源)。即使尚未捕获到信息,驱动依旧会把缓存(DQBUFF)里的东西返回给应用程序。
非阻塞应用程序通常使用 select系统调用查询是否可以对设备进行无阻塞的访问,最终会引发设备驱动中 poll函数执行。

(建议 V4L2编程中使用阻塞方式打开一个设备文件,除非你能保重开始采集数据时队列里的n块缓存已有数据存在。)


  • 用非阻塞模式打开摄像头设备
int cameraFd = open(/dev/video0″, O_RDWR| O_NONBLOCK, 0);
  • 用阻塞模式打开摄像头设备
int cameraFd = open(/dev/video0″, O_RDWR, 0);

若出现错误:error: VIDIOC_DQBUF: Resource temporarily unavailable

在这里插入图片描述

2. 查询设备属性(VIDIOC_QUERYCAP)

函数使用:int ioctl(int fd, int request, struct v4l2_capability *argp);
  • v4l2相关结构体定义:
struct v4l2_capability
{
	u8 driver[16]; // 驱动名字
	u8 card[32]; // 设备名字
	u8 bus_info[32]; // 设备在系统中的位置
	u32 version;// 驱动版本号
	u32 capabilities;// 设备支持的操作
	u32 reserved[4]; // 保留字段
};

capabilities 常用值:

  • V4L2_CAP_VIDEO_CAPTURE (是否支持图像获取)
struct v4l2_capability cap;
ioctl(fd,VIDIOC_QUERYCAP,&cap);
printf(“Driver Name:%s\nCard Name:%s\nBus info:%s\nDriver Version:%u.%u.%u\n”,cap.driver,cap.card,cap.bus_info,cap.capabilities);
  • V4L2_BUF_TYPE_VIDEO_CAPTURE (获取设备支持的分辨率)

例一:

    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//初始化
    struct v4l2_fmtdesc fmt_1;
    struct v4l2_frmsizeenum frmsize;
    struct v4l2_frmivalenum frmival;
    fmt_1.index = 0; //索引
    fmt_1.type = type;
    
    while (ioctl(fd, VIDIOC_ENUM_FMT, &fmt_1) >= 0) {
      frmsize.pixel_format = fmt_1.pixelformat;
      frmsize.index = 0;
      while (ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frmsize) >= 0){
		if(frmsize.type == 		V4L2_FRMSIZE_TYPE_DISCRETE){
 			printf("line:%d %dx%d\n",__LINE__, frmsize.discrete.width, frmsize.discrete.height);
				}else if(frmsize.type == V4L2_FRMSIZE_TYPE_STEPWISE){
 					printf("line:%d %dx%d\n",__LINE__, frmsize.discrete.width, frmsize.discrete.height);
			}
		frmsize.index++;
      }
      fmt_1.index++;
    }
  }

例二:

struct v4l2_capability cap;
 
memset(&cap, 0, sizeof(cap));
/* 获取设备支持的操作 */
if(ioctl(dev->fd, VIDIOC_QUERYCAP, &cap) < 0){
    if(EINVAL == errno){   /*EINVAL为返回的错误值*/
        printf(stderr,"%s is no V4L2 device\n", dev->dev);
        return TFAIL;
    }
    else
    {
        printf(stderr,"%s is not V4L2 device,unknow error\n", dev->dev);
        return TFAIL;
    }
}
//获取成功,检查是否有视频捕获功能
if(!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)){
    printf(stderr, "%s is no video capture device\n",dev->dev);
    return TFAIL;
}
/* streaming I/O ioctls */
if(!(cap.capabilities & V4L2_CAP_STREAMING)){
    printf(stderr, "%s does not support streaming i/o\n",dev->dev);
    return TFAIL;
}

在这里插入图片描述

3. 选择视频输入

作用: 一个 video设备节点可能对应多个视频源,比如 saf7113可以最多支持四路cvbs输入,如果上层想在四个 cvbs视频输入间切换,那么就要调用 S_INPUT ioctl来切换。

因此saf7113驱动需要实现一个选择和查询 input的接口,当上层应用调用 v4l2的 G_INPUT S_INPUT时,会调用 saf7113的这个接口。(没用过这玩意儿)


  • input / output
ID 描述
VIDIOC_ENUMINPUT 枚举所有 input端口
VIDIOC_G_INPUT 获取当前正在使用的 input端口
VIDIOC_S_INPUT 设置将要使用的 input端口
VIDIOC_ENUMOUTPUT 枚举所有 output端口
VIDIOC_G_OUTPUT 获取当前正在使用的 output端口
VIDIOC_S_OUTPUT 设置将要使用的 output端口
VIDIOC_ENUMAUDIO 枚举所有 audio input端口
VIDIOC_G_AUDIO 获取当前正在使用的 audio input端口
VIDIOC_S_AUDIO 设置将要使用的 audio input端口
VIDIOC_ENUMAUDOUT 枚举所有 audio output端口
VIDIOC_G_AUDOUT 获取当前正在使用的 audio output端口
VIDIOC_S_AUDOUT 设置将要使用的 audio output端口

在这里插入图片描述

4. 设置视频属性及采集的参数

成功打开摄像头设备后,接下来就要设置摄像头设备的一些属性。使用 ioctl函数设置,
如:裁剪、缩放、调焦等等。不过这一步是可选的。

常用命令:

命令 作用
VIDIOC_REQBUFS 分配内存
VIDIOC_QUERYBUF 把VIDIOC_REQBUFS中分配的数据缓存转换成物理地址
VIDIOC_QUERYCAP 查询驱动功能
VIDIOC_ENUM_FMT 获取当前驱动支持的视频格式
VIDIOC_S_FMT 设置当前驱动的频捕获格式
VIDIOC_G_FMT 读取当前驱动的频捕获格式
VIDIOC_TRY_FMT 验证当前驱动的显示格式
VIDIOC_CROPCAP 查询驱动的修剪能力
VIDIOC_S_CROP 设置视频信号的边框
VIDIOC_G_CROP 读取视频信号的边框
VIDIOC_QBUF 把数据从缓存中读取出来
VIDIOC_DQBUF 把数据放回缓存队列
VIDIOC_STREAMON 开始视频显示函数
VIDIOC_STREAMOFF 结束视频显示函数
VIDIOC_QUERYSTD 检查当前视频设备支持的标准,例如PAL或NTSC
  • v4l2_format结构如下:
struct v4l2_format
{
	enum v4l2_buf_type type; // 数据流类型,必须永远是V4L2_BUF_TYPE_VIDEO_CAPTURE
	union
	{
		struct v4l2_pix_format pix;
		struct v4l2_window win;
		struct v4l2_vbi_format vbi;
		__u8 raw_data[200];
	} fmt;
};

struct v4l2_pix_format
{
	__u32 width; // 宽,必须是16的倍数
	__u32 height; // 高,必须是16的倍数
	__u32 pixelformat; // 视频数据存储类型,例如是YUV4:2:2还是RGB
	enum v4l2_field field;
	__u32 bytesperline;
	__u32 sizeimage;
	enum v4l2_colorspace colorspace;
	__u32 priv;
};
  • 检查当前视频设备支持的标准(VIDIOC_QUERYSTD)
v4l2_std_id std;

do {
	ret = ioctl(fd, VIDIOC_QUERYSTD, &std);
} while (ret == -1 && errno == EAGAIN);
  • 设置视频捕获格式
struct v4l2_format fmt;

javamemset (&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 720;
fmt.fmt.pix.height = 576;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;

if (ioctl(fd, VIDIOC_S_FMT, &fmt) == -1) 
{
	return -1;
}
  • 设置帧数
struct v4l2_streamparm Stream_Parm;
memset(&Stream_Parm, 0, sizeof(struct v4l2_streamparm));
Stream_Parm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 
 
Stream_Parm.parm.capture.timeperframe.denominator =Denominator;;
Stream_Parm.parm.capture.timeperframe.numerator = Numerator;
 
io_rel = ioctl(Handle, VIDIOC_S_PARM, &Stream_Parm);
  • 设置缩放(VIDIOC_G_CROP,VIDIOC_S_CROP)
int ioctl(int fd, int request, struct v4l2_crop *argp);
int ioctl(int fd, int request, const struct v4l2_crop *argp);

struct v4l2_crop
{
	enum v4l2_buf_type type;// 应用程序设置
	struct v4l2_rect c;
}

在这里插入图片描述

5. 向驱动申请帧缓冲(CAP_BUF_NUM)

功能: 请求V4L2驱动分配视频缓冲区(申请V4L2视频驱动分配内存)
V4L2是视频设备的驱动层,位于内核空间,所以通过 VIDIOC_REQBUFS控制命令字申请的内存位于内核空间,应用程序不能直接访问,需要通过调用mmap内存映射函数,把内核空间内存映射到用户空间后,应用程序通过访问用户空间地址来访问内核空间。

参数说明: 参数类型为 V4L2的申请缓冲区数据结构体类型 struct v4l2_requestbuffers;v4l2_requestbuffers 结构中定义了缓存的数量,驱动会据此申请对应数量的视频缓存。多个缓存可以用于建立FIFO,来提高视频采集的效率。
一般不超过5个,CAP_BUF_NUM = 4

返回值说明: 执行成功时,函数返回值为 0;V4L2驱动层分配好了视频缓冲区;


struct v4l2_requestbuffers req;
/* 申请设备的缓存区 */
    memset(&req, 0, sizeof(req));
    req.count = CAP_BUF_NUM;  //申请一个拥有四个缓冲帧的缓冲区
    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    req.memory = V4L2_MEMORY_MMAP;
 
    if (ioctl(dev->fd, VIDIOC_REQBUFS, &req) < 0)
    {
        if (EINVAL == errno)
        {
            printf(stderr, "%s does not support "
                     "memory mapping\n", dev->dev);
            return TFAIL;
        }
        else
        {
            printf(stderr, "%s does not support "
                     "memory mapping, unknow error\n", dev->dev);
            return TFAIL;
        }
    }
    if (req.count < 2)
    {
        printf(stderr, "Insufficient buffer memory on %s\n",
                 dev->dev);
        return TFAIL;
    }

在这里插入图片描述

6. 查询帧缓冲区在内核空间中的长度和偏移量 (VIDIOC_QUERYBUF)

Linux - 使用V4L2(总结)_第4张图片

  • 获取缓冲帧的地址,长度:
int ioctl(int fd, int request, struct v4l2_buffer *argp);
  • struct v4l2_buffer结构体:
struct v4l2_buffer
{
	__u32 index; //buffer 序号
	enum v4l2_buf_type type; //buffer 类型
	__u32 byteused; //buffer 中已使用的字节数
	__u32 flags; // 区分是MMAP 还是USERPTR
	enum v4l2_field field;
	struct timeval timestamp;// 获取第一个字节时的系统时间
	struct v4l2_timecode timecode;
	__u32 sequence; // 队列中的序号
	enum v4l2_memory memory;//IO 方式,被应用程序设置
	union m
	{
		__u32 offset;// 缓冲帧地址,只对MMAP 有效
		unsigned long userptr;
	};
	__u32 length;// 缓冲帧长度
	__u32 input;
	__u32 reserved;
};

在这里插入图片描述

7. 将申请到的帧缓冲映射到用户空间 mmap

函数:

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

参数说明:
addr:映射起始地址,一般为 NULL ,让内核自动选择
length:被映射内存块的长度
prot:标志映射后能否被读写,其值为 PROT_EXEC,PROT_READ,PROT_WRITE, PROT_NONE
flags:确定此内存映射能否被其他进程共享,MAP_SHARED,MAP_PRIVATE
fd,offset:确定被映射的内存地址

返回成功映射后的地址,不成功返回MAP_FAILED ((void*)-1);


  • 断开映射
// addr为映射后的地址,length为映射后的内存长度
int munmap(void *addr, size_t length);
  • 例:将四个已申请到的缓冲帧映射到应用程序,用buffers 指针记录
buffers = (buffer*)calloc (req.count, sizeof (*buffers));
if (!buffers) {
	fprintf (stderr, "Out of memory/n");
	exit (EXIT_FAILURE);
}

// 映射
for (unsigned int n_buffers = 0; n_buffers < req.count; ++n_buffers) {
	struct v4l2_buffer buf;
	memset(&buf,0,sizeof(buf));
	buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	buf.memory = V4L2_MEMORY_MMAP;
	buf.index = n_buffers;

	// 查询序号为 n_buffers的缓冲区,得到其起始物理地址和大小
	if (-1 == ioctl (fd, VIDIOC_QUERYBUF, &buf))
	exit(-1);
	buffers[n_buffers].length = buf.length;

	// 映射内存
	buffers[n_buffers].start =mmap (NULL,buf.length,PROT_READ | PROT_WRITE ,MAP_SHARED,fd, buf.m.offset);
	if (MAP_FAILED == buffers[n_buffers].start)
	exit(-1);
}

在这里插入图片描述

8、9、10、11. 将申请到的帧缓冲全部入队列,开始采集视频并处理

概述: 操作系统一般把系统使用的内存划分成用户空间和内核空间,分别由应用程序管理和操作系统管理。
应用程序可以直接访问内存的地址,而内核空间存放的是 供内核访问的代码和数据,用户不能直接访问。
v4l2捕获的数据,最初是存放在内核空间的,这意味着用户不能直接访问该段内存,必须通过某些手段来转换地址。

一共有三种视频采集方式: 使用read、write方式、内存映射方式和用户指针模式。

read、write方式:在用户空间和内核空间不断拷贝数据,占用了大量用户内存空间,效率不高。


内存映射方式:把设备里的内存映射到应用程序中的内存控件,直接处理设备内存,这是一种有效的方式。上面的mmap函数就是使用这种方式。


用户指针模式:内存片段由应用程序自己分配。这点需要在v4l2_requestbuffers里将memory字段设置成V4L2_MEMORY_USERPTR。

Linux - 使用V4L2(总结)_第5张图片


//把四个缓冲帧放入队列
for (i = 0; i < CAPBUFNUM; i++)
{
	memset(&buf, 0, sizeof(buf));
	buf.type = V4L2BUFTYPE_VIDEO_CAPTURE;
	buf.memory = V4L2_MEMORY_MMAP;
	buf.index = i;
	buf.m.offset = dev->buffer[i].offset;
	
	// 将空闲的内存加入可捕获视频的队列 
	if(ioctl(dev->fd, VIDIOC_QBUF, &buf) < 0)
	{
		printf(“ERROR: VIDIOC_QBUF[%s], FUNC[%s], LINE[%d]\n”, dev->dev, __FUNCTION, __LINE);
		return TFAIL;
	}
}
 
type = V4L2BUFTYPEVIDEOCAPTURE;
//打开设备视频流
if(ioctl(dev->fd, VIDIOC_STREAMON, &type) < 0)
{
	printf(“ERROR: VIDIOC_STREAMON[%s], FUNC[%s], LINE[%d]\n”, dev->dev, __FUNCTION, __LINE);
	return TFAIL;
}
  • 获取一帧数据并处理
struct v4l2_buffer buf;

CLEAR(buf);
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;

// 从缓冲区取出一个缓冲帧
ioctl (fd, VIDIOC_DQBUF, &buf);

// 图像处理
process_image (buffers[buf.index].start);

// 将取出的缓冲帧放回缓冲区
ioctl (fd, VIDIOC_QBUF, &buf);

在这里插入图片描述

12、13. 停止视频采集,解除映射,关闭设备

// 停止视频采集,解除映射
int ret = ioctl(fd, VIDIOC_STREAMOFF, &buf_type);
munmap(buffer[j].start, buffer[j].length);

// 关闭视频设备
close(fd);

在这里插入图片描述

第一次接触有关这方面的知识,本文为个人总结笔记(仅为参考)
结合各位大神的博客加自己理解,有误请指出 ✍。
不知道是不是属于原创,转载也不知道转哪篇,下面为参考文章(无序罗列):


*V412编程(供参考)
*嵌入式LINUX环境下视频采集知识
*嵌入式-v4l2摄像头的工作流程及ioctl功能详解
*ioctl函数的作用
*linux 内核 - ioctl函数详解
*关于构造IOCTL命令的学习心得
*Linux下使用ioctl设置v4l2摄像头参数--------案例二
*Linux设备驱动中的阻塞与非阻塞I/O
*V4L2打开video设备注意(阻塞方式与非阻塞方式)
*V4L2 获取设备支持的分辨率
*Linux v4l2编程(摄像头信息采集)
*第一章 V4L2简介
*VIDIOC_S_INPUT 作用
*V4L2视频采集接口使用说明
*V4L2 API详解 Camera详细设置
*V4L2驱动开发详解
*嵌入式LINUX环境下视频采集知识(V4L2)
*V4L2视频采集接口使用说明

你可能感兴趣的:(ubuntu,V4L2,linux,嵌入式,内核)