1. 框架分层

实际上的v4l2框架:

Android虚拟摄像头 开源 安卓虚拟摄像头模块_ide

v4l2本质是还是一个字符设备驱动,有自己的fops。

每注册一个video_device都会以次设备号为下标放到v4l2层的一个数组里。

应用调用open函数时,v4l2会根据次设备号找到对应的video_device,进而调用video_device对应的fops。

2. 注册v4l2_dev和video_device

(1) 注册platform_device和platform_driver,也并不是一定要这样做,只是大家都这样做,那也就跟着做了

(2) 注册v4l2_dev

v4l2_device_register(&pdev->dev,&myvivi->v4l2_dev);

(3) 分配一个video_device

定义到大结构体

struct vivi {
	struct video_device 	vid_cap_dev;
    ...
};

或者调用以下函数分配

video_device_alloc();

(4) 初始化video_device

struct video_device *vfd;
	vfd = &myvivi->vid_cap_dev;
	snprintf(vfd->name, sizeof(vfd->name),
		 "myvivi-00-vid-cap");
	vfd->fops = &myvivi_fops;
	vfd->ioctl_ops = &myvivi_ioctl_ops;
	vfd->device_caps = myvivi->vid_cap_caps;
	vfd->release = video_device_release_empty;
	vfd->v4l2_dev = &myvivi->v4l2_dev;
	vfd->queue = &myvivi->vb_vid_cap_q;
	vfd->lock = &myvivi->mutex;
	video_set_drvdata(vfd, myvivi);
  • 第5行,这里的fops最终会被v4l2层调用;
  • 第6行,ioctl_ops就是我们要实现的重点,上层与底层的交互主要是ioctl,当然也比较多,这里实现必不可少的11个,如下:
static const struct v4l2_ioctl_ops myvivi_ioctl_ops = {
//表示它是一个摄像头设备
 .vidioc_querycap = myvivi_vidoc_querycap,//用于列举、获取、测试、设置摄像头的数据格式
 .vidioc_enum_fmt_vid_cap = myvivi_vidioc_enum_fmt_vid_cap,
 .vidioc_g_fmt_vid_cap = myvivi_vidioc_g_fmt_vid_cap,
 .vidioc_try_fmt_vid_cap = myvivi_vidioc_try_fmt_vid_cap,
 .vidioc_s_fmt_vid_cap = myvivi_vidioc_s_fmt_vid_cap,//缓冲区操作: 申请/查询/放入/取出队列
 .vidioc_reqbufs = vb2_ioctl_reqbufs,
 .vidioc_querybuf = vb2_ioctl_querybuf,
 .vidioc_qbuf =vb2_ioctl_qbuf,
 .vidioc_dqbuf = vb2_ioctl_dqbuf,//启动/停止
 .vidioc_streamon = vb2_ioctl_streamon,
 .vidioc_streamoff = vb2_ioctl_streamoff,};

ps:使用xawtv打开,需要加另外两个函数“vidioc_g_fbuf”和“vidioc_s_fbuf”,可以给个空函数,否则不能出图。

常用ioctl接口命令一览:

ioctl接口命令

解析

VIDIOC_REQBUFS

请求系统分配缓冲区

VIDIOC_QUERYBUF

查询映射缓冲区

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

  • 第7行,表示这个video_device有哪些能力,V4L2_CAP_VIDEO_CAPTURE表示支持图像获取,V4L2_CAP_STREAMING表示可以通过streaming方式获取图像,还有一种通过读写方式获取图像,那就是V4L2_CAP_READWRITE。
  • 第8行,release函数必须实现,否则注册会报错,可以是个空函数;
  • 第12行,设置video_device的私有数据为myvivi。

(5) 注册video_device

ret = video_register_device(myvivi->vdev,VFL_TYPE_GRABBER,-1);
  • 第1个参数,就是video_device指针;
  • 第2个参数,注册的类型,VFL_TYPE_GRABBER就是注册video,会生成/dev/videoX; 注册v4l2_subdev也会调用这个函数,但是它的类型是VFL_TYPE_SUBDEV,生成/dev/v4l2-subdevX;
  • 第3个参数,就是“/dev/videoX”中的“X”,-1表示自动生成,从0开始。

3. 实现ioctl_ops

3.1 VIDIOC_QUERYCAP 查询设备能力

static int myvivi_vidoc_querycap(struct file *file,void *priv,
					struct v4l2_capability *cap) {
	struct vivi *vind = video_drvdata(file);

	strcpy(cap->driver, "myvivi");
	strcpy(cap->card, "myvivi");
	snprintf(cap->bus_info, sizeof(cap->bus_info),
			"platform:%s", vind->v4l2_dev.name);

	cap->capabilities = vind->vid_cap_caps | V4L2_CAP_DEVICE_CAPS;
	return 0;
}

一般我们只关心 capabilities 成员,比如V4L2_CAP_VIDEO_CAPTURE 具有视频捕获能力。

第10行,vind->vid_cap_caps为:

myvivi->vid_cap_caps = 	V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_VIDEO_OVERLAY | V4L2_CAP_STREAMING;

3.2 VIDIOC_ENUM_FMT 枚举(查询)设备支持的视频格式

static int myvivi_vidioc_enum_fmt_vid_cap(struct file *file, void *priv,
					struct v4l2_fmtdesc *f){
	const struct myvivi_fmt *fmt;

	if (f->index >= 1)
		return -EINVAL;

	fmt = &myvivi_formats;

	f->pixelformat = fmt->fourcc;
	return 0;
}

一般一个设备支持多种视频格式,但是这里为了简单,只支持一种。

  • 第5~6行,由于应用层并不知道设备支持多少种格式,因此通过index从0开始尝试;这里当index大于等于1时就错误返回,表示只支持一种格式;
  • 第8~9行,myvivi_formats就是我们定义的格式,如下
struct myvivi_fmt myvivi_formats = {
	//.fourcc   = V4L2_PIX_FMT_YUYV,
	//.bit_depth = 16,
	//.buffers = 1,
	//.colorspace = V4L2_COLORSPACE_SRGB,
	
	.fourcc   = V4L2_PIX_FMT_RGB565, /* gggbbbbb rrrrrggg */
	.bit_depth = 16,
	.buffers = 1,
	.colorspace = V4L2_COLORSPACE_SRGB,
};

常用的格式是V4L2_PIX_FMT_YUYV,但是我们要模拟数据输入,YUV比较难处理,所以使用V4L2_PIX_FMT_RGB565;

另外补充YUV数据格式相关知识:

YUV 4:4:4, YUV 4:2:2, YUV 4:2:0, YUV 4:1:1, 换种叫法

YCrCb 4:4:4, YCrCb 4:2:2, YCrCb 4:2:0, YCrCb 4:1:1

Y表示亮度,U和V表示色度

(1) YUV 4:4:4

YUV 4:4:4意思就是4个像素里的数据有4个Y, 4个U, 4个V。YUV三个信道的抽样率相同,因此在生成的图像里,每个象素的三个分量信息完整(每个分量通常8比特),经过8比特量化之后,未经压缩的每个像素占用3个字节。

下面的四个像素为: [Y0 U0 V0] [Y1 U1 V1] [Y2 U2 V2] [Y3 U3 V3]

存放的码流为: Y0 U0 V0 Y1 U1 V1 Y2 U2 V2 Y3 U3 V3

一个像素大小为:3 * 8* 4 / 4 = 24 bit

(2) YUV 4:2:2

意思就是相邻的4个像素里有4个Y, 2个U, 2个V。每个色差信道的抽样率是亮度信道的一半,所以水平方向的色度抽样率只是4:4:4的一半。对非压缩的8比特量化的图像来说,每个由两个水平方向相邻的像素组成的宏像素需要占用4字节内存。

下面的四个像素为:[Y0 U0 V0] [Y1 U1 V1] [Y2 U2 V2] [Y3 U3 V3]

存放的码流为:Y0 U0 Y1 V1 Y2 U2 Y3 V3

映射出像素点为:[Y0 U0 V1] [Y1 U0 V1] [Y2 U2 V3] [Y3 U2 V3]

一个像素大小为:(4 * 8 + 2 * 8 + 2 * 8) / 4 = 16 bit

(3) YUV 4:1:1

4:1:1的色度抽样,是在水平方向上对色度进行4:1抽样。对于低端用户和消费类产品这仍然是可以接受的。对非压缩的8比特量化的视频来说,每个由4个水平方向相邻的像素组成的宏像素需要占用6字节内存。

下面的四个像素为: [Y0 U0 V0] [Y1 U1 V1] [Y2 U2 V2] [Y3 U3 V3]

存放的码流为: Y0 U0 Y1 Y2 V2 Y3

映射出像素点为:[Y0 U0 V2] [Y1 U0 V2] [Y2 U0 V2] [Y3 U0 V2]

一个像素大小为:(4 * 8 + 1 * 8 + 1 * 8) / 4 = 12 bit

(4)YUV4:2:0

4:2:0并不意味着只有Y,Cb而没有Cr分量。它指得是对每行扫描线来说,只有一种色度分量以2:1的抽样率存储。相邻的扫描行存储不同的色度分量,也就是说,如果一行是4:2:0的话,下一行就是4:0:2,再下一行是4:2:0…以此类推。对每个色度分量来说,水平方向和竖直方向的抽样率都是2:1,所以可以说色度的抽样率是4:1。对非压缩的8比特量化的视频来说,每个由2x2个2行2列相邻的像素组成的宏像素需要占用6字节内存。

下面八个像素为:[Y0 U0 V0] [Y1 U1 V1] [Y2 U2 V2] [Y3 U3 V3]

[Y5 U5 V5] [Y6 U6 V6] [Y7U7 V7] [Y8 U8 V8]

存放的码流为:Y0 U0 Y1 Y2 U2 Y3 Y5 V5 Y6 Y7 V7 Y8

映射出的像素点为:[Y0 U0 V5] [Y1 U0 V5] [Y2 U2 V7] [Y3 U2 V7]

[Y5 U0 V5] [Y6 U0 V5] [Y7U2 V7] [Y8 U2 V7]

一个像素大小为:(4 * 8 + 2 * 8) / 4 = 12bit

3.3 VIDIOC_G_FMT 获得设置好的视频格式

static int myvivi_vidioc_g_fmt_vid_cap(struct file *file, void *priv,
					struct v4l2_format *f) {
	struct v4l2_pix_format *pix = &f->fmt.pix;
	struct vivi *vind = video_drvdata(file);
	
	pix->width = vind->fmt_cap_rect.width;
	pix->height = vind->fmt_cap_rect.height;
	pix->pixelformat = vind->fmt_cap->fourcc;
	pix->field = V4L2_FIELD_NONE;
	pix->colorspace = vind->fmt_cap->colorspace;
	pix->bytesperline = vind->bytesperline;
	pix->sizeimage = pix->bytesperline * pix->width;
	return 0;
}

将格式返回。

3.4 VIDIOC_S_FMT 设置视频格式

static int myvivi_vidioc_try_fmt_vid_cap(struct file *file,void *priv, 
			struct v4l2_format *f) {
	struct v4l2_pix_format *pix = &f->fmt.pix;
	struct vivi *vind = video_drvdata(file);
	u32 w;
	u32 bit_depth;
	v4l_bound_align_image(&pix->width, MIN_WIDTH, MAX_WIDTH, 2,
                  &pix->height, MIN_HEIGHT, MAX_HEIGHT, 0, 0);
	
	pix->pixelformat = vind->fmt_cap->fourcc;
	pix->field = V4L2_FIELD_NONE;
	pix->colorspace = vind->fmt_cap->colorspace;
	w = pix->width;
	bit_depth = vind->fmt_cap->bit_depth;
	pix->bytesperline = (w * bit_depth) >> 3;   
	pix->sizeimage = pix->bytesperline * pix->width;
	return 0;
}

static int myvivi_vidioc_s_fmt_vid_cap(struct file *file, void *priv,
					struct v4l2_format *f) {
	struct v4l2_pix_format *pix = &f->fmt.pix;
	struct vivi *vind = video_drvdata(file);
	
	int ret = myvivi_vidioc_try_fmt_vid_cap(file, priv, f);
    if (ret < 0) {
		printk(KERN_ERR"try format error!!!\n");
        return ret;
	}
	
	vind->fmt_cap_rect.width = pix->width;
	vind->fmt_cap_rect.height = pix->height;
	vind->bytesperline = pix->bytesperline;
	
	return 0;
}

设置宽高,视频格式等。

3.5 VIDIOC_REQBUFS 请求在内核空间分配视频缓冲区

.vidioc_reqbufs 			= vb2_ioctl_reqbufs,

vb2_ioctl_reqbufs() 会调用到vb2_core_reqbufs(), 接下来调用关系:

int vb2_core_reqbufs(struct vb2_queue *q, enum vb2_memory memory,unsigned int *count)
    call_qop(q, queue_setup, q, &num_buffers, &num_planes, plane_sizes, q->alloc_devs);
	allocated_buffers = __vb2_queue_alloc(q, memory, num_buffers, num_planes, plane_sizes);
  • 第2行,会回调到myvivi的vid_cap_queue_setup(), 计算buffer大小和个数;
  • 第3行,分配内存,num_buffers由应用决定,plane_sizes是个数组,myvivi设置只有1个plane,所以分配内存大小为:num_buffers * 1 * plane_sizes[0];

3.6 VIDIOC_QUERYBUF 查询分配好的 buffer 信息

.vidioc_querybuf 			= vb2_ioctl_querybuf,

vb2_ioctl_querybuf() --> vb2_querybuf() 查询所分配的缓冲区,根据应用设置的index获得缓冲区的使用状态、在内核空间的偏移地址、长度等等信息,然后应用程序根据这些信息使用mmap把内核空间地址映射到用户空间。

myvivi这里的mmap使用核心层的vb2_fop_mmap()。

3.7 VIDIOC_QBUF 将缓存放入队列中

.vidioc_qbuf 				= vb2_ioctl_qbuf,

关键调用:

vb2_ioctl_qbuf()
	vb2_qbuf(vdev->queue, p);
		vb2_core_qbuf(q, b->index, b);
			ret = __buf_prepare(vb, pb);
				ret = __qbuf_mmap(vb, pb);
					call_vb_qop(vb, buf_prepare, vb);//回调到myvivi的buf_prepare
					
			list_add_tail(&vb->queued_entry, &q->queued_list);//加入queued_list尾
		
			__enqueue_in_driver(vb);
				call_void_vb_qop(vb, buf_queue, vb);//回调到myvivi的buf_queue

myvivi的buf_queue()会将vb加入到本地的链表(vid_cap_active)尾部,填充数据时,从这个链表头取vb。

3.8 VIDIOC_STREAMON 开始捕获

.vidioc_streamon 			= vb2_ioctl_streamon,

关键调用:

vb2_ioctl_streamon
	vb2_streamon(vdev->queue, i);
		vb2_core_streamon(q, type);
			ret = vb2_start_streaming(q);
				ret = call_qop(q, start_streaming, q,
					atomic_read(&q->owned_by_drv_count));//回调到myvivi的start_streaming

myvivi的start_streaming()会启动定时器,开始产生虚拟数据,如下。

static int vid_cap_start_streaming(struct vb2_queue *vq, unsigned count) {
	struct vivi *vind = vb2_get_drv_priv(vq);
	timer_setup(&vind->timer, myvivi_timer_function, 0);
	vind->timer.expires = jiffies + HZ/2;
	add_timer(&vind->timer);
	return 0;
}

3.9 VIDIOC_DQBUF

.vidioc_dqbuf 				= vb2_ioctl_dqbuf,

关键调用:

vb2_ioctl_dqbuf
	vb2_dqbuf(vdev->queue, p, file->f_flags & O_NONBLOCK);
		ret = vb2_core_dqbuf(q, NULL, b, nonblocking);
			ret = __vb2_get_done_vb(q, &vb, pb, nonblocking);
				//从done_list的链表头取出vb
				*vb = list_first_entry(&q->done_list, struct vb2_buffer, done_entry);
				list_del(&(*vb)->done_entry);//从done_list中删除
			//将取出的vb信息给到应用,应用从对应的buffer中取数据
			call_void_bufop(q, fill_user_buffer, vb, pb);
			list_del(&vb->queued_entry);

3.10 VIDIOC_STREAMOFF 关闭捕获

.vidioc_streamoff 			= vb2_ioctl_streamoff,

关键调用:

vb2_ioctl_streamoff
	vb2_streamoff(vdev->queue, i);
		vb2_core_streamoff(q, type);
			__vb2_queue_cancel(q);
				call_void_qop(q, stop_streaming, q);//回调到myvivi的stop_streaming

myvivi的stop_streaming()会停止定时器,停止产生数据。

4. videobuf2

videobuf2是嵌入到v4l2子系统,以供驱动与用户空间提供数据申请与交互的接口集合,它实现了包括buffer分配,根据状态可以入队出队的控制流。

使用过程:

  • (1) 调用vb2_queue_init 初始化队列 q ,如下:
q = &myvivi->vb_vid_cap_q;
	q->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	q->io_modes = VB2_MMAP | VB2_USERPTR | VB2_DMABUF | VB2_READ;
	q->drv_priv = myvivi;
	q->buf_struct_size = sizeof(struct myvivi_buffer);
	q->ops = &myvivi_vid_cap_qops;
	q->mem_ops = &vb2_vmalloc_memops;
	q->lock = &myvivi->mutex;
	q->timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC;
	q->dev = myvivi->v4l2_dev.dev;
	ret = vb2_queue_init(q);

第6行的ops就是上面ioctl操作buffer的回调函数,定义如下:

const struct vb2_ops myvivi_vid_cap_qops = {
	.queue_setup		= vid_cap_queue_setup,
	.buf_prepare		= vid_cap_buf_prepare,
	.buf_finish			= vid_cap_buf_finish,
	.buf_queue			= vid_cap_buf_queue,
	.start_streaming	= vid_cap_start_streaming,
	.stop_streaming		= vid_cap_stop_streaming,
};
  • (2) 调用reqbuf 时候会根据请求分配内存, 将vb加入到q->bufs数组,并且回调q->ops.queue_setup, queue_setup就是vid_cap_queue_setup,定义如下:
// vb2 核心层 vb2_reqbufs 中调用它,确定申请缓冲区的大小
static int vid_cap_queue_setup(struct vb2_queue *vq,
		       unsigned *nbuffers, unsigned *nplanes,
		       unsigned sizes[], struct device *alloc_devs[]){
	struct vivi *vind = vb2_get_drv_priv(vq);
	unsigned buffers = vind->fmt_cap->buffers;
	
	printk("width = %d \n",vind->fmt_cap_rect.width);
    printk("height = %d \n",vind->fmt_cap_rect.height);
    printk("pixelsize = %d \n",vind->bytesperline);
	printk("buffers = %d \n",buffers);
	
	sizes[0] = vind->bytesperline * vind->fmt_cap_rect.height;
	
	if (vq->num_buffers + *nbuffers < 2)
		*nbuffers = 2 - vq->num_buffers;
	*nplanes = buffers;
	printk("%s: count=%d\n", __func__, *nbuffers);
	
	return 0;
}
  • (3) 调用querybuf时候,根据信息(v4l2_buffer)返回q->bufs中对应的vb2_buffer的信息(v4l2_buffer);
  • (4) mmap上面信息对应的 vb空间到用户空间
  • (5) 调用qbuf 时,将对应的vb2_buffer ( vivi_bufer->list )添加到 q->queued_list 队列中,并且回调q->ops.buf_prepare, buf_prepare就是vid_cap_buf_prepare,如下:
//APP调用ioctl VIDIOC_QBUF时导致此函数被调用
static int vid_cap_buf_prepare(struct vb2_buffer *vb){
	struct vivi *vind = vb2_get_drv_priv(vb->vb2_queue);
	unsigned long size;
	size = vind->bytesperline * vind->fmt_cap_rect.height;;
	if (vb2_plane_size(vb, 0) < size) {
		printk(KERN_ERR"%s data will not fit into plane (%lu < %lu)\n",
				__func__ ,vb2_plane_size(vb, 0), size);
		return -EINVAL;
	}
	vb2_set_plane_payload(vb, 0, size);
	return 0;
}

还会回调q->ops.buf_queue, buf_queue就是vid_cap_buf_queue,如下:

static void vid_cap_buf_queue(struct vb2_buffer *vb) {
	struct vb2_v4l2_buffer *vbuf = to_vb2_v4l2_buffer(vb);
	struct vivi *vind = vb2_get_drv_priv(vb->vb2_queue);
	struct myvivi_buffer *buf = container_of(vbuf, struct myvivi_buffer, vb);

	spin_lock(&vind->slock);
	//把buf放入本地一个队列尾部,定时器处理函数就可以从本地队列取出vb
	list_add_tail(&buf->list, &vind->vid_cap_active);
	spin_unlock(&vind->slock);
}
  • (6) 使用select 调用poll 休眠等待 q->done_list 有数据
  • (7) 数据存放完成后 调用vb2_buffer_done函数,即将上面有数据的vb2_buffer放入q->done_list中,然后唤醒上面poll休眠的进程。定时器处理函数如下:
static void myvivi_timer_function(struct timer_list *t){
	struct vivi *vind = container_of(t, struct vivi, timer);
    struct myvivi_buffer *vid_cap_buf = NULL;
	char *vbuf;
	
	if (!list_empty(&vind->vid_cap_active)) {
		vid_cap_buf = list_entry(vind->vid_cap_active.next, struct myvivi_buffer, list);
		if(vid_cap_buf->vb.vb2_buf.state != VB2_BUF_STATE_ACTIVE) {
			printk(KERN_ERR"buffer no active,error!!!\n");
			return;
		}
		list_del(&vid_cap_buf->list);
	}else {
		printk("No active queue to serve\n");
        goto out;
	}
    
	//取buf
	vbuf = vb2_plane_vaddr(&vid_cap_buf->vb.vb2_buf, 0);
	printk("bytesperline=%d\n",vind->bytesperline);
	
	//填充数据
	memset(vbuf,0xff,vind->bytesperline * vind->fmt_cap_rect.height);
	fillbuff(vbuf,vind->bytesperline,vind->fmt_cap_rect.height);
	
    // 它干两个工作,把buffer 挂入done_list 另一个唤醒应用层序,让它dqbuf
    vb2_buffer_done(&vid_cap_buf->vb.vb2_buf, VB2_BUF_STATE_DONE);
    
out:
    //修改timer的超时时间 : 30fps, 1秒里有30帧数据,每1/30 秒产生一帧数据
    mod_timer(&vind->timer, jiffies + HZ/30);
}

到此可以看出,入队/出队的流程如下:

Android虚拟摄像头 开源 安卓虚拟摄像头模块_linux_02

  • (8) poll唤醒后会调用dqbuf将q->done_list 中的vb2_buffer提出来后,将此vb2的信息(v4l2_buffer)返回
  • (9) 应用程序得到buffer信息后,就去对应的mmap后的用户空间中读数据。

5.总结

纸上得来终觉浅,绝知此事要躬行,理解一个系统框架最有效的方法莫过于亲手去实现一遍,虽然有点费劲。

测试平台:Ubuntu 16.04

Makefile可能要根据不同ubuntu版本修改,参考7.camera驱动06-自己实现v4l2驱动-准备 测试命令:

$ sudo modprobe vivid

$ sudo rmmod vivid

$ sudo insmod myvivi.ko

$ xawtv

附上代码链接:https://github.com/stksss/myV4L2, 分支是MYVIVI, 到第8个提交就完成了以下效果:

Android虚拟摄像头 开源 安卓虚拟摄像头模块_linux_03