【V4L2】Linux多媒体框架 - V4L2浅析

概述

V4L2(Video4Linux的缩写)是Linux下关于视频采集相关设备的驱动框架,为驱动和应用程序提供了一套统一的接口规范。

V4L2支持的设备十分广泛,但是其中只有很少一部分在本质上是真正的视频设备。

按照v4l2的标准,它会将一个数据流设备抽象成一个videoX节点,从属主设备都对应着各自的v4l2_subdev实现,并且按照media controller进行统一管理。

V4L2框架下面就是相机驱动部分,驱动部分控制着上下电逻辑以及寄存器读取时序并按照I2C协议与硬件进行通信,和根据MIPI CSI协议传递数据,从而达到控制各个硬件设备,并且获取图像数据的目的

V4L2的主要数据结构

v4l2_device:嵌入到video_device中,表示一个v4l2设备的实例,内部通过一个链表管理着整个从属的所有子设备。(/include/media)

struct v4l2_device {
    struct device *dev;
#if defined(CONFIG_MEDIA_CONTROLLER)
    struct media_device *mdev;                                                                                                                         
#endif
    struct list_head subdevs;
    spinlock_t lock;
    char name[V4L2_DEVICE_NAME_SIZE];
    void (*notify)(struct v4l2_subdev *sd,
        unsigned int notification, void *arg);
    struct v4l2_ctrl_handler *ctrl_handler;
    struct v4l2_prio_state prio;
    struct kref ref;
    void (*release)(struct v4l2_device *v4l2_dev);
};

v4l2_subdev:依附在v4l2_device之下,表示一个v4l2设备的子设备,一个v4l2_device下可以有多个sub_device

subdevice的设计目的是为了多路复用,用一个v4l2_device可以服务多个v4l2_subdev,而有些驱动没有v4l2_subdev,只有video_device。

struct v4l2_subdev {
#if defined(CONFIG_MEDIA_CONTROLLER)
	struct media_entity entity;
#endif
	struct list_head list;
	struct module *owner;
	bool owner_v4l2_dev;
	u32 flags;
	struct v4l2_device *v4l2_dev;
	const struct v4l2_subdev_ops *ops;  // subdev的操作方法
	const struct v4l2_subdev_internal_ops *internal_ops;
	struct v4l2_ctrl_handler *ctrl_handler;
	char name[V4L2_SUBDEV_NAME_SIZE];
	u32 grp_id;
	void *dev_priv;
	void *host_priv;
	struct video_device *devnode;
	struct device *dev;
	struct fwnode_handle *fwnode;
	struct list_head async_list;
	struct v4l2_async_subdev *asd;
	struct v4l2_async_notifier *notifier;
	struct v4l2_async_notifier *subdev_notifier;
	struct v4l2_subdev_platform_data *pdata;

	struct v4l2_subdev_state *state;
};

video_device:是一个字符设备,video_device包含一个cdev,v4l2_device是一个v4l2实例,嵌入到video_device中,v4l2维护着一张链表管理v4l2_subdevice,v4l2_subdev表示摄像头的I2C控制模块

struct video_device
{
#if defined(CONFIG_MEDIA_CONTROLLER)
	struct media_entity entity;
	struct media_intf_devnode *intf_devnode;
	struct media_pipeline pipe;
#endif
	const struct v4l2_file_operations *fops;   // v4l2设备操作接口
	u32 device_caps;
	/* sysfs */
	struct device dev;
	struct cdev *cdev;                  // 字符设备 
	struct v4l2_device *v4l2_dev;       // v4l2设备
	struct device *dev_parent;

	struct v4l2_ctrl_handler *ctrl_handler;
	struct vb2_queue *queue;
	struct v4l2_prio_state *prio;
	
	/* device info */
	char name[32];
	enum vfl_devnode_type vfl_type;
	enum vfl_devnode_direction vfl_dir;
	int minor;
	u16 num;
	unsigned long flags;
	int index;
	
	/* V4L2 file handles */
	spinlock_t		fh_lock;
	struct list_head	fh_list;
	int dev_debug;
	v4l2_std_id tvnorms;

	/* callbacks */
	void (*release)(struct video_device *vdev);
	const struct v4l2_ioctl_ops *ioctl_ops;  // v4l2d的ioctl操作集
	DECLARE_BITMAP(valid_ioctls, BASE_VIDIOC_PRIVATE);

	struct mutex *lock;
};


v4l2驱动框架:
image

v4l2主要注册接口

  • v4l2_device
int v4l2_device_register(struct device *dev, struct v4l2_device *v4l2_dev);
void v4l2_device_unregister(struct v4l2_device *v4l2_dev);
  • v4l2_subdev
int v4l2_device_register_subdev(struct v4l2_device *v4l2_dev,struct v4l2_subdev *sd);
void v4l2_device_unregister(struct v4l2_device *v4l2_dev);
  • video_device
int video_register(struct video_device *vdev,int type,int nr);
void video_unregister_device(struct video_device *vdev);

V4L2执行流程

image

1.打开video设备

在需要进行视频数据流操作之前,首先通过标准的字符设备操作接口open方法来打开一个video设备,并且将返回设备句柄存在本地,之后的一系列操作都是基于对此句柄的操作。在打开的过程中,会给每一个子设备进行各自的一系列初始化操作。

int vfd;

if((vfd = open("/dev/video0",O_RDWR)) < 0)
{
	perror("DEVICE open error");
	return -1;
}

2.查看并设置设备

在打开设备获取其文件句柄后,就需要查询设备的属性,主要通过ioctl传入VIDIOC_QUERYCAP参数来完成,设备属性通过v4l2_capability结构体来表达,另外还可以通过传入VIDIOC_ENUM_FMT来枚举支持的数据格式,通过传入VIDIOC_G_FMT/VIDIOC_S_FMT来分别获取和设置数据格式,通过传入VIDIOC_G_PARM/VIDIOC_S_PARM来分别获取和设置参数。

// init device
struct v4l2_format fmt = {0};    
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width  = 1280;   // 设置相机宽度
fmt.fmt.pix.height = 720;	 // 设置相机高度
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;   // 像素格式:YUYV
fmt.fmt.pix.field  = V4L2_FIELD_NONE;

if(-1 == xioctl(vfd , VIDIOC_S_FMT, &fmt)) {
	perror("VIDIOC_S_FMT");
	return errno;
}

3.向驱动申请帧缓冲区

完成设备的配置后,可以向设备申请多个用于盛装图像数据的帧缓冲区,通过调用ioctl传入VIDIOC_REQBUFS命令来完成最后将缓冲区通过mmap方式映射到用户空间

struct v4l2_requestbuffers *request = &config->request;
request->type   = V4L2_BUF_TYPE_VIDEO_CAPTURE;
request->memory = V4L2_MEMORY_MMAP;
request->count  = 4;   

if(-1 == ioctl(config->vfd, VIDIOC_REQBUFS, request)){
	perror("VIDIOC_REQBUFS");
	return errno;
}

if(request->count < 2) {
	fprintf(stderr, "Not enough buffer memory.\n");
	return EXIT_FAILURE;
}

4.将帧缓冲区入队

申请好缓冲区后,通过调用ioctl方法传入VIDIOC_QBUF命令来将帧缓冲区加入到V4L2框架中的缓冲区队列中,静等硬件模块将图像数据填充到缓冲区中。

struct v4l2_buffer buffer;

// config->buffer_count为4,VIDIOC_REQBUFS时已指定count
for(i = 0; i < config->buffer_count; i++) {
	memset(&buffer, 0, sizeof(buffer));
	buffer.type   = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	buffer.memory = V4L2_MEMORY_MMAP;
	buffer.index  = i;   

	if(-1 == xioctl(config->vfd, VIDIOC_QBUF, &buffer)) {
		perror("VIDIOC_QBUF");
		return errno;
	}
}

5.开启数据流

将所有的缓冲区都加入到队列之后,就可以调用ioctl并且传入VIDIOC_STREAMON命令,来通知整个框架开始进行数据传输,其中大致包括了通知各个子设备开始进行工作,最终将数据填充到V4L2框架中的缓冲区队列中。

enum v4l2_buf_type type;

type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if(-1 == xioctl(config->vfd, VIDIOC_STREAMON, &type)) {
	perror("VIDIOC_STREAMON");
	return errno;
}

6.将帧缓冲区出队

一旦数据流开始进行流转,就可以通过调用ioctk下发VIDIOC_DQBUF命令来获取帧缓冲区,并且将缓冲区的图像数据取出,进行预览、拍照或者录像的处理。(此处一般配合epoll/poll/select使用)

struct v4l2_buffer buffer;
{
	memset(&buffer, 0, sizeof(buffer));
	buffer.type   = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	buffer.memory = V4L2_MEMORY_MMAP;

	if(-1 == xioctl(config->vfd, VIDIOC_DQBUF, &buffer)) {
		switch(errno) {
		case EAGAIN:
			return 0;
		case EIO:
		default:
			perror("VIDIOC_DQBUF");
			return errno;
		}
	}
	// swap buffer pointer.
	v4l2_capture_t *capture = (v4l2_capture_t *)config->refs;
	capture->data = config->buffers[buffer.index].data;
	capture->len  = config->buffers[buffer.index].len;
}

7.将缓冲重新入列

通过调用ioctk下发VIDIOC_QBUF命令将缓冲区重新入列,这样可以实现循环采集。

// 将已经取完的帧缓冲区,清零
memset(&buffer, 0, sizeof(buffer));

// 重新压入v4l2框架的队列中
if(-1 == xioctl(config->vfd, VIDIOC_QBUF, &buffer)){
     perror("VIDIOC_QBUF");
     return errno;
}

8.停止视频的采集

enum v4l2_buf_type type;

if(-1 == xioctl(config->vfd, VIDIOC_STREAMOFF, &type)) {
	perror("VIDIOC_STREAMOFF");
	return errno;
}

9.关闭视频设备

close(vfd);

调用流程:

img

V4L2常用API及命令标识符

1.常用数据结构体在内核目录include/linux/videodev2.h中定义

struct v4l2_requestbuffers reqbufs;   // 向驱动申请帧缓冲的情求,里面包含申请的个数
struct v4l2_capability cap;	      // 设备的功能,比如是否是视频输入设备
struct v4l2_input input;	      // 视频输入
struct v4l2_standard std;	      // 视频的制式,比如PAL,NTSC制
struct v4l2_fromat fmt;		      // 帧的格式,比如宽度,高度等
struct v4l2_buffer buf;		      // 代表驱动中的帧缓存

2.常用ioctl接口命令include/linux/videodev2.h定义

VIDIOC_REQBUFS:请求缓存的数量,驱动会据此申请对应数量的视频缓存。
VIDIOC_QUERYBUF:把VIDIOC_REQBUFS中分配的数据缓存转换成物理地址
VIDIOC_QUERYCAP:通过 struct v4l2_buffer 结构体的 index,访问对应序号的 buffer,获取到对应 buffer 的缓存信息。
VIDIOC_ENUM_FMT:获取当前驱动支持的视频格式
VIDIOC_S_FMT:设置当前驱动的视频捕获格式,设置之前最好调用VIDIOC_TRY_FMT判断是否支持
VIDIOC_G_FMT:读取当前驱动的视频捕获格式,比如:width,height,sizeimage等参数
VIDIOC_TRY_FMT:验证当前驱动的格式是否被驱动支持,不会改变任何硬件设置硬件设置
VIDIOC_CROPCAP:查询驱动的修剪能力
VIDIOC_S_CROP:设置视频信号的边框
VIDIOC_G_CROP:读取视频信号的边框
VIDIOC_QBUF:将User空间已经处理过的buffer,重新入队,移交给 driver,等待填充数据。
VIDIOC_DQBUF:将driver已经填充好数据的 buffer 出列,供应用使用。
VIDIOC_STREAMON:开始视频显示函数
VIDIOC_STREAMOFF:结束视频显示函数
VIDIOC_QUERYSTD:检查当前视频设备支持的标准,例如PAL或NTSC。
VIDIOC_OVERLAY:设置使能osd参数,传入1表示使能,0表示关闭

总结

在linux端开发视频设备大部分使用场景是通过ioctl发送命令符到V4L2框架中,实现获取设备配置信息,设置设备数据格式,拉取设备视频流,获取视频帧进行处理等过程。

此篇仅为v4l2框架简介,还有event,mem2mem,videobuf2等部分,详情请看Linux源码,V4L2部分完全开源。








参考文章:

https://blog.csdn.net/weixin_42462202/article/details/99680969
https://www.cnblogs.com/unreal/articles/1820295.html
https://blog.csdn.net/thisway_diy/article/details/128459836

posted @ 2022-09-07 15:58  Emma1111  阅读(2929)  评论(0编辑  收藏  举报