概述
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驱动框架:
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执行流程
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);
调用流程:
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