1、简介


        v4l2是专门为linux设备设计的一套视频框架,其主体框架在linux内核,可以理解为是整个 linux 系统上面的视频源捕获驱动框架。其广泛应用在嵌入式设备以及移动端、个人电脑设备上面,市面上的编码产品类如:SDV、手机、IPC、行车记录仪都会用到这个框架来进行视频采集。

2、V4l2框架的结构


        v4l2的核心源码位于linux/drivers/media/v4l2-core。


2.1、相关对象


        v4l2驱动框架主要的对象有video_device、v4l2_device、v4l2_subdev、videobuf。
        video_device:一个字符设备,为用户空间提供设备节点(/dev/video*),提供系统调用的相关操作;
        v4l2_device:表示一个v4l2设备的实例;
        v4l2_subdev:属于v4l2设备的子设备,一个v4l2设备可以拥有多个子设备;
        videobuf:v4l2驱动的缓存管理。

 

2.2、v4l2设计思路理解


        上面说了,一个v4l2设备可以拥有多个子设备。那么,为什么要这么做呢?这么做的目的又是什么呢?
        以手机为例,我们知道,目前的手机,基本上都是有着两个摄像头,一个前置,一个后置。而手机厂商为了节约成本和控制手机大小,控制芯片却只有一个。
        因此,一个v4l2设备可以拥有多个子设备这样的设计思路就很好地解决了这个问题。我们把控制芯片比作一个v4l2设备,把前置与后置摄像头比作两个v4l2_subdev。
        所以,我们就可以用下面这张图来理解,从用户空间到内核控件再到硬件的大致框架。


2.3、v4l2框架


        在简单了解v4l2的设计思路之后,接下来我们需要了解v4l2的框架。
        通过上面,其实我们已经了解,v4l2就是提供一个字符设备节点供用户操作,从而实现设备控制。
        我们知道,简单的字符设备是这么编写的:
        - 分配一个字符设备(cdev);
        - 设置一个fops;
        - 注册字符设备。

       而复杂的字符设备,linux内核通常采用分层的方法,一般分为驱动核心层和硬件相关层。

       核心层会帮你完成字符设备的分配,fops的设置,注册字符设备,并向硬件相关层提供一个相应的对象和注册接口;

       硬件相关层则需要分配相应的对象,设置对象和对象的fops,并注册到核心层中,当应用层发生系统调用,会先来到核心层,核心层再通过回调函数调用到硬件相关层的驱动。
       对于V4L2的驱动框架也是如此,可分为V4L2驱动核心层和硬件相关层。

       下面先用一张图来总结大致V4L2的驱动框架:


       从图中可以看出V4L2分为核心层还有硬件相关层。
       - 核心层负责注册字符设备,然后提供video_device对象和相应的注册接口给硬件相关层使用;
       - 硬件相关层需要分配一个video_device并设置它,然后向核心层注册,核心层会为其注册字符设备并且创建设备节点(/dev/videox)。同时硬件相关层还需要分配和设置相应的v4l2_device和v4l2_subdev,其中v4l2_device的一个比较重要的意义就是管理v4l2_subdev,当然有一些驱动并不需要实现v4l2_subdev,此时v4l2_device的意义就不是很大了。
       当应用层通过/dev/video来操作设备的时候,首先会来到V4L2的核心层,核心层通过注册进的video_device的回调函数调用相应的操作函数,video_device可以直接操作硬件或者是通过v4l2_subdev来操作硬件。

3、v4l2主要数据结构体

       - video_device

	struct video_device
	{
		/* 字符设备 */
		struct cdev *cdev;
		
		/* v4l2_device父对象 */
		struct v4l2_device *v4l2_dev;

		/* v4l2文件操作函数结构体 */
		const struct v4l2_file_operations *fops;
		
		/* v4l2 ioctl回调函数结构体 */
		const struct v4l2_ioctl_ops *ioctl_ops;
	};

       可以看到video_device中含有一个cdev还有v4l2_device,此外还有fops和ioctl_ops,从应用层进行系统调用会经过v4l2的核心层回调到这里。
       其中v4l2_file_operations和v4l2_ioctl_ops如下。

    struct v4l2_file_operations {
		struct module *owner;
		ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
		ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
		unsigned int (*poll) (struct file *, struct poll_table_struct *);
		long (*ioctl) (struct file *, unsigned int, unsigned long);
		long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
		unsigned long (*get_unmapped_area) (struct file *, unsigned long,
					unsigned long, unsigned long, unsigned long);
		int (*mmap) (struct file *, struct vm_area_struct *);
		int (*open) (struct file *);
		int (*release) (struct file *);
	};
    struct v4l2_ioctl_ops {
		int (*vidioc_querycap)(struct file *file, void *fh, struct v4l2_capability *cap);
		/* Buffer handlers */
		int (*vidioc_reqbufs) (struct file *file, void *fh, struct v4l2_requestbuffers *b);
		int (*vidioc_querybuf)(struct file *file, void *fh, struct v4l2_buffer *b);
		int (*vidioc_qbuf)    (struct file *file, void *fh, struct v4l2_buffer *b);
		int (*vidioc_dqbuf)   (struct file *file, void *fh, struct v4l2_buffer *b);
		/* Stream on/off */
		int (*vidioc_streamon) (struct file *file, void *fh, enum v4l2_buf_type i);
		int (*vidioc_streamoff)(struct file *file, void *fh, enum v4l2_buf_type i);
		...
	};

       - v4l2_device

    struct v4l2_device {
		/* 跟踪子设备的链表 */
		struct list_head subdevs;
		...
	};

       - v4l2_subdev

	struct v4l2_subdev {
		struct list_head list;
		struct v4l2_device *v4l2_dev;
		const struct v4l2_subdev_ops *ops;
	};

       v4l2_subdev中有一个v4l2_subdev_ops,实现了一系列的操作,供v4l2_device调用。

	struct v4l2_subdev_ops {
		const struct v4l2_subdev_core_ops	*core;
		const struct v4l2_subdev_tuner_ops	*tuner;
		const struct v4l2_subdev_audio_ops	*audio;
		const struct v4l2_subdev_video_ops	*video;
		const struct v4l2_subdev_vbi_ops	*vbi;
		const struct v4l2_subdev_ir_ops		*ir;
		const struct v4l2_subdev_sensor_ops	*sensor;
	};

	struct v4l2_subdev_core_ops {
		...
		int (*s_config)(struct v4l2_subdev *sd, int irq, void *platform_data);
		int (*init)(struct v4l2_subdev *sd, u32 val);
		int (*s_gpio)(struct v4l2_subdev *sd, u32 val);
		int (*g_ctrl)(struct v4l2_subdev *sd, struct v4l2_control *ctrl);
		int (*s_ctrl)(struct v4l2_subdev *sd, struct v4l2_control *ctrl);
		int (*s_std)(struct v4l2_subdev *sd, v4l2_std_id norm);
		long (*ioctl)(struct v4l2_subdev *sd, unsigned int cmd, void *arg);
		...
	};

	struct v4l2_subdev_video_ops {
		...
		int (*enum_fmt)(struct v4l2_subdev *sd, struct v4l2_fmtdesc *fmtdesc);
		int (*g_fmt)(struct v4l2_subdev *sd, struct v4l2_format *fmt);
		int (*try_fmt)(struct v4l2_subdev *sd, struct v4l2_format *fmt);
		int (*s_fmt)(struct v4l2_subdev *sd, struct v4l2_format *fmt);
		int (*cropcap)(struct v4l2_subdev *sd, struct v4l2_cropcap *cc);
		int (*g_crop)(struct v4l2_subdev *sd, struct v4l2_crop *crop);
		int (*s_crop)(struct v4l2_subdev *sd, struct v4l2_crop *crop);
		int (*g_parm)(struct v4l2_subdev *sd, struct v4l2_streamparm *param);
		int (*s_parm)(struct v4l2_subdev *sd, struct v4l2_streamparm *param);
		...
	};

4、源码分析之v4l2驱动模板

/* 本文为伪驱动代码,为大家大概讲解一下v4l2框架*/

/* 实现各种系统调用 */
static const struct v4l2_file_operations video_dev_fops = {
	.owner		    = THIS_MODULE,
	.release        = vdev_close,
	.read           = vdev_read,
	.poll		    = vdev_poll,
	.ioctl          = video_ioctl2,
	.mmap           = vdev_mmap,
};

/* 实现各种ioctl调用 */
static const struct v4l2_ioctl_ops video_dev_ioctl_ops = {
	.vidioc_querycap      = vidioc_querycap,
	.vidioc_enum_fmt_vid_cap  = vidioc_enum_fmt_vid_cap,
	.vidioc_g_fmt_vid_cap     = vidioc_g_fmt_vid_cap,
	.vidioc_try_fmt_vid_cap   = vidioc_try_fmt_vid_cap,
	.vidioc_s_fmt_vid_cap     = vidioc_s_fmt_vid_cap,
	.vidioc_reqbufs       = vidioc_reqbufs,
	.vidioc_querybuf      = vidioc_querybuf,
	.vidioc_qbuf          = vidioc_qbuf,
	.vidioc_dqbuf         = vidioc_dqbuf,
	.vidioc_enum_input    = vidioc_enum_input,
	.vidioc_g_input       = vidioc_g_input,
	.vidioc_s_input       = vidioc_s_input,
	.vidioc_streamon      = vidioc_streamon,
	.vidioc_streamoff     = vidioc_streamoff,
};

static int __init video_init(void)
{
    static struct video_device* video_dev;
    static struct v4l2_device v4l2_dev;

    /* 分配并设置一个video_device */
    video_dev = video_device_alloc();
    video_dev->fops = &video_dev_fops;
    video_dev->ioctl_ops = &video_dev_ioctl_ops;
    video_dev->release = video_device_release;
    video_dev->tvnorms = V4L2_STD_525_60;
    video_dev->current_norm = V4L2_STD_NTSC_M;

    /* 注册一个v4l2_device */
    v4l2_device_register(video_dev->dev, &v4l2_dev);    
    video_dev->v4l2_dev = &v4l2_dev;

    /* 注册一个video_device字符设备 */
    video_register_device(video_dev, VFL_TYPE_GRABBER, -1);

    return 0;
}

static void __exit video_exit(void)
{
    video_unregister_device(video_dev);
    v4l2_device_unregister(&v4l2_dev);
    video_device_release(video_dev);
}


module_init(video_init);
module_exit(video_exit);

       看驱动代码,毫无疑问,从驱动入口(module_init)开始。
       从代码中,我们可以知道,驱动入口为video_init函数,那我们找到video_init函数。
       video_init函数,做的事情很少,大概三件:
       - 申请并设置video_device(video_device_alloc)
       - 注册v4l2_device(v4l2_device_register)
       - 注册video_device的字符设备(video_register_device)
       我们先来看v4l2_device_register函数:

	int v4l2_device_register(struct device *dev, struct v4l2_device *v4l2_dev)
	{
		/* 初始化链表 */
		INIT_LIST_HEAD(&v4l2_dev->subdevs);
		
		/* 初始化自旋锁 */
		spin_lock_init(&v4l2_dev->lock);

		/* Set name to driver name + device name if it is empty. */
		if (!v4l2_dev->name[0])
			snprintf(v4l2_dev->name, sizeof(v4l2_dev->name), "%s %s", dev->driver->name, dev_name(dev));
		
		/* 设置数据 */
		if (!dev_get_drvdata(dev))
			dev_set_drvdata(dev, v4l2_dev);
	}

       从源码中可以看出v4l2_device_register并没有做什么事,只是初始化链表,自旋锁,还有设置数据。
       下面来看video_register_device函数:

	int video_register_device(struct video_device *vdev, int type, int nr)
	{   
		/* 申请字符设备 */
		vdev->cdev = cdev_alloc();
		
		/* 设置fops */
		vdev->cdev->ops = &v4l2_fops;
		
		/* 向内核添加字符设备 */
		ret = cdev_add(vdev->cdev, MKDEV(VIDEO_MAJOR, vdev->minor), 1);
		
		/* 生成设备节点 */
		dev_set_name(&vdev->dev, "%s%d", name_base, vdev->num);
		device_register(&vdev->dev);
		
		/* 增加v4l2_device引用计数 */
		v4l2_device_get(vdev->v4l2_dev);
	}

       可以看到这个函数会为video_device分配一个cdev,然后设置fops,向内核注册字符设备,再者生成设备节点等。
       接下来看一下其中设置的fops(v4l2_fops):

	static const struct file_operations v4l2_fops = {
		.owner = THIS_MODULE,
		.read = v4l2_read,
		.write = v4l2_write,
		.open = v4l2_open,
		.get_unmapped_area = v4l2_get_unmapped_area,
		.mmap = v4l2_mmap,
		.unlocked_ioctl = v4l2_ioctl,
	#ifdef CONFIG_COMPAT
		.compat_ioctl = v4l2_compat_ioctl32,
	#endif
		.release = v4l2_release,
		.poll = v4l2_poll,
		.llseek = no_llseek,
	};

       这个是video_device中的字符设备对应的fops,应用层发生系统调用会率先调用到这里,我们来好好看一看这些调用。
       - v4l2_open

	static int v4l2_open(struct inode *inode, struct file *filp)
	{
		struct video_device *vdev;
		/* 获取video_device */
		video_get(vdev);
		/* 回调video_device的fops */
		if (vdev->fops->open) {
			if (video_is_registered(vdev))
				ret = vdev->fops->open(filp);
			else
				ret = -ENODEV;
		}
	}

       从这个函数可以看到,发生系统调用首先来到v4l2核心层的字符设备,然后再回调到对应的video_device,video_device在前面已经实现了v4l2_file_operations和v4l2_ioctl_ops一系列回调。
       - v4l2_ioctl:V4L2的应用编程会有非常多的ioctl,会先调用到此处。

	static long v4l2_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
	{
		/* 获取video_device */
		struct video_device *vdev = video_devdata(filp);
		/* 回调video_device的ioctl */
		ret = vdev->fops->unlocked_ioctl(filp, cmd, arg);
	}

       下面来看一下video_device怎么实现ioctl:

	static const struct v4l2_file_operations video_dev_fops = {
		.owner		    = THIS_MODULE,
		.release        = vdev_close,
		.read           = vdev_read,
		.poll		    = vdev_poll,
		.ioctl          = video_ioctl2,
		.mmap           = vdev_mmap,
	};

       从上面驱动程序的编写,我们可以知道video_device对应ioctl就是video_ioctl2,这个函数是内核提供的,我们看一看这个函数的内容:

	long video_ioctl2(struct file *file, unsigned int cmd, unsigned long arg)
	{
		__video_do_ioctl(file, cmd, parg);
	}
	static long __video_do_ioctl(struct file *file, unsigned int cmd, void *arg)
	{
		/* 获取video_device */
		struct video_device *vfd = video_devdata(file);
		
		/* 获取video_device的ioctl_ops */
		const struct v4l2_ioctl_ops *ops = vfd->ioctl_ops;

		switch (cmd) {
		case VIDIOC_QUERYCAP:
			ops->vidioc_querycap(file, fh, cap);
		case VIDIOC_ENUM_FMT:
			 ops->vidioc_enum_fmt_vid_cap(file, fh, f);
		...
		}
	}

       可以看出,最终会调用到video_device实现的v4l2_ioctl_ops。

	/* 实现各种ioctl调用 */
	static const struct v4l2_ioctl_ops video_dev_ioctl_ops = {
		.vidioc_querycap      = vidioc_querycap,
		.vidioc_enum_fmt_vid_cap  = vidioc_enum_fmt_vid_cap,
		.vidioc_g_fmt_vid_cap     = vidioc_g_fmt_vid_cap,
		.vidioc_try_fmt_vid_cap   = vidioc_try_fmt_vid_cap,
		.vidioc_s_fmt_vid_cap     = vidioc_s_fmt_vid_cap,
		.vidioc_reqbufs       = vidioc_reqbufs,
		.vidioc_querybuf      = vidioc_querybuf,
		.vidioc_qbuf          = vidioc_qbuf,
		.vidioc_dqbuf         = vidioc_dqbuf,
		.vidioc_enum_input    = vidioc_enum_input,
		.vidioc_g_input       = vidioc_g_input,
		.vidioc_s_input       = vidioc_s_input,
		.vidioc_streamon      = vidioc_streamon,
		.vidioc_streamoff     = vidioc_streamoff,
	};

       所以系统调用最先都会调用到字符设备的fops,然后经过v4l2核心层最终调用到video_device这里。
       关于v4l2的驱动框架就分析到这里,关于更加详细的实现v4l2驱动,就靠各位自己就实际的项目分析了。

GitHub 加速计划 / li / linux-dash
10.39 K
1.2 K
下载
A beautiful web dashboard for Linux
最近提交(Master分支:2 个月前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐