Linux V4L2子系统分析(一)

1.概述

Linux系统上的Video设备多种多样,如通过Camera Host控制器接口连接的摄像头,通过USB总线连接的摄像头等。为了兼容更多的硬件,Linux内核抽象了V4L2(Video for Linux Two)子系统。V4L2子系统是Linux内核中关于Video(视频)设备的API接口,是V4L(Video for Linux)子系统的升级版本。V4L2子系统向上为虚拟文件系统提供了统一的接口,应用程序可通过虚拟文件系统访问Video设备。V4L2子系统向下给Video设备提供接口,同时管理所有Video设备。Video设备又分为主设备和从设备,对于Camera来说,Camera Host控制器为主设备,负责图像数据的接收和传输,从设备为Camera Sensor,一般为I2C接口,可通过从设备控制Camera采集图像的行为,如图像的大小、图像的FPS等。主设备可通过v4l2_subdev_call的宏调用从设备提供的方法,反过来从设备可以调用主设备的notify方法通知主设备某些事件发生了。

V4L2框架

2.V4L2子系统

V4L(Video for Linux)是Linux内核中关于视频设备的API接口,涉及视频设备的音频和视频信息采集及处理、视频设备的控制。V4L出现于Linux内核2.1版本,经过修改bug和添加功能,Linux内核2.5版本推出了V4L2(Video for Linux Two)子系统,功能更多且更稳定。V4L2的主设备号是81,次设备号范围0~255,这些次设备号又分为多类设备,如视频设备(次设备号范围0-63)、Radio(收音机)设备(次设备号范围64-127)、Teletext设备(次设备号范围192-223)、VBI设备(次设备号范围224-255)。V4L2设备对应的设备节点有/dev/videoX、/dev/vbiX、/dev/radioX。这里只讨论视频设备,视频设备对应的设备节点是/dev/videoX。视频设备以高频头或Camera为输入源,Linux内核驱动该类设备,接收相应的视频信息并处理。

2.1.V4L2主设备数据结构

V4L2主设备实例使用struct v4l2_device结构体表示,v4l2_device是V4L2子系统的入口,管理着V4L2子系统的主设备和从设备。简单设备可以仅分配这个结构体,但在大多数情况下,都会将这个结构体嵌入到一个更大的结构体中。需要与媒体框架整合的驱动必须手动设置dev->driver_data,指向包含v4l2_device结构体实例的驱动特定设备结构体。这可以在注册V4L2设备实例前通过dev_set_drvdata()函数完成。同时必须设置v4l2_device结构体的mdev域,指向适当的初始化并注册过的media_device实例。
对于视频设备,Camera控制器可以视为主设备,接在Camera控制器上的摄像头可以视为从设备。V4L2子系统使用v4l2_device结构体管理设备,设备的具体操作方法根据设备类型决定,若是视频设备,则需要注册video_device结构体,并提供相应的操作方法。

    [include/media/v4l2-device.h]
    struct v4l2_device {
        struct device *dev;  // 父设备指针
    #if defined(CONFIG_MEDIA_CONTROLLER)  // 多媒体设备配置选项
        // 用于运行时数据流的管理,
        struct media_device *mdev;
    #endif
        // 注册的子设备的v4l2_subdev结构体都挂载此链表中
        struct list_head subdevs;
        // 同步用的自旋锁
        spinlock_t lock;
        // 独一无二的设备名称,默认使用driver name + bus ID
        char name[V4L2_DEVICE_NAME_SIZE];
        // 被一些子设备回调的通知函数,但这个设置与子设备相关。子设备支持的任何通知必须在
        // include/media/<subdevice>.h 中定义一个消息头。
        void (*notify)(struct v4l2_subdev *sd, unsigned int notification, void *arg);
        // 提供子设备(主要是video和ISP设备)在用户空间的特效操作接口,
        // 比如改变输出图像的亮度、对比度、饱和度等等
        struct v4l2_ctrl_handler *ctrl_handler;
        // 设备优先级状态
        struct v4l2_prio_state prio;
        /* BKL replacement mutex. Temporary solution only. */
        struct mutex ioctl_lock;
        // struct v4l2_device结构体的引用计数,等于0时才释放
        struct kref ref;
        // 引用计数ref为0时,调用release函数进行释放资源和清理工作
        void (*release)(struct v4l2_device *v4l2_dev);
    };

使用v4l2_device_register注册v4l2_device结构体.如果v4l2_dev->name为空,则它将被设置为从dev中衍生出的值(为了更加精确,形式为驱动名后跟bus_id)。如果在调用v4l2_device_register前已经设置好了,则不会被修改。如果dev为NULL,则必须在调用v4l2_device_register前设置v4l2_dev->name。可以基于驱动名和驱动的全局atomic_t类型的实例编号,通过v4l2_device_set_name()设置name。这样会生成类似ivtv0ivtv1等名字。若驱动名以数字结尾,则会在编号和驱动名间插入一个破折号,如:cx18-0cx18-1等。dev参数通常是一个指向pci_devusb_interfaceplatform_device的指针,很少使其为NULL,除非是一个ISA设备或者当一个设备创建了多个PCI设备,使得v4l2_dev无法与一个特定的父设备关联。
使用v4l2_device_unregister卸载v4l2_device结构体。如果dev->driver_data域指向 v4l2_dev,将会被重置为NULL。主设备注销的同时也会自动注销所有子设备。如果你有一个热插拔设备(如USB设备),则当断开发生时,父设备将无效。由于v4l2_device有一个指向父设备的指针必须被清除,同时标志父设备
已消失,所以必须调用v4l2_device_disconnect函数清理v4l2_device中指向父设备的dev指针。v4l2_device_disconnect并不注销主设备,因此依然要调用v4l2_device_unregister函数注销主设备。

    [include/media/v4l2-device.h]
    // 注册v4l2_device结构体,并初始化v4l2_device结构体
    // dev-父设备结构体指针,若为NULL,在注册之前设备名称name必须被设置,
    // v4l2_dev-v4l2_device结构体指针
    // 返回值-0成功,小于0-失败
    int v4l2_device_register(struct device *dev, struct v4l2_device *v4l2_dev)

    // 卸载注册的v4l2_device结构体
    // v4l2_dev-v4l2_device结构体指针
    void v4l2_device_unregister(struct v4l2_device *v4l2_dev)

    // 设置设备名称,填充v4l2_device结构体中的name成员
    // v4l2_dev-v4l2_device结构体指针
    // basename-设备名称基本字符串
    // instance-设备计数,调用v4l2_device_set_name后会自加1
    // 返回值-返回设备计数自加1的值
    int v4l2_device_set_name(struct v4l2_device *v4l2_dev, 
            const char *basename, atomic_t *instance)

    // 热插拔设备断开时调用此函数
    // v4l2_dev-v4l2_device结构体指针
    void v4l2_device_disconnect(struct v4l2_device *v4l2_dev);

同一个硬件的情况下。如ivtvfb驱动是一个使用ivtv硬件的帧缓冲驱动,同时alsa驱动也使用此硬件。可以使用如下例程遍历所有注册的设备:

    static int callback(struct device *dev, void *p)
    {
        struct v4l2_device *v4l2_dev = dev_get_drvdata(dev);

        /* 测试这个设备是否已经初始化 */
        if (v4l2_dev == NULL)
            return 0;
        ...
        return 0;
    }

    int iterate(void *p)
    {
        struct device_driver *drv;
        int err;

        /* 在PCI 总线上查找ivtv驱动。
        pci_bus_type是全局的. 对于USB总线使用usb_bus_type。 */
        drv = driver_find("ivtv", &pci_bus_type);
        /* 遍历所有的ivtv设备实例 */
        err = driver_for_each_device(drv, NULL, p, callback);
        put_driver(drv);
        return err;
    }

有时你需要一个设备实例的运行计数。这个通常用于映射一个设备实例到一个模块选择数组的索引。推荐方法如下:

    static atomic_t drv_instance = ATOMIC_INIT(0);
    static int drv_probe(struct pci_dev *pdev, const struct pci_device_id *pci_id)
    {
        ...
        state->instance = atomic_inc_return(&drv_instance) - 1;
    }

如果有多个设备节点,对于热插拔设备,知道何时注销v4l2_device结构体就比较困难。为此v4l2_device有引用计数支持。当调用video_register_device时增加引用计数,而设备节点释放时减小引用计数。当引用计数为零,则v4l2_devicerelease回调将被执行。可以在此时做最后的清理工作。如果创建了其他设备节点(比如ALSA),则你可以通过以下函数手动增减引用计数:

    [include/media/v4l2-device.h]
    // 增加引用计数
    void v4l2_device_get(struct v4l2_device *v4l2_dev);
    // 减少引用计数
    int v4l2_device_put(struct v4l2_device *v4l2_dev);

由于引用技术初始化为1,需要在disconnect回调(对于USB设备)中调用v4l2_device_put,或者remove回调(例如对于PCI设备),否则引用计数将永远不会为0。

2.2.V4L2从设备数据结构

V4L2从设备使用struct v4l2_subdev结构体表示。一个V4L2主设备可能对应多个V4L2从设备,所有主设备对应的从设备都挂到v4l2_device结构体的subdevs链表中。对于视频设备,从设备就是摄像头,通常情况下是I2C设备,主设备可通过I2C总线控制从设备,例如控制摄像头的焦距、闪光灯等。struct v4l2_subdev结构体重要成员示意图如下图所示。struct v4l2_subdev_video_ops将在《Linux V4L2子系统-Video设备框架分析(二)》中介绍。

v4l2_subdev结构体示意

    [include/media/v4l2-subdev.h]
    #define V4L2_SUBDEV_FL_IS_I2C		(1U << 0)  // 从设备是I2C设备
    #define V4L2_SUBDEV_FL_IS_SPI		(1U << 1)  // 从设备是SPI设备
    #define V4L2_SUBDEV_FL_HAS_DEVNODE	(1U << 2)  // 从设备需要设备节点
    #define V4L2_SUBDEV_FL_HAS_EVENTS	(1U << 3)  // 从设备会产生事件

    struct v4l2_subdev {
    #if defined(CONFIG_MEDIA_CONTROLLER)  // 多媒体配置选项
        struct media_entity entity;
    #endif
        struct list_head list;  // 子设备串联链表
        struct module *owner;  // 属于那个模块,一般指向i2c_lient驱动模块
        bool owner_v4l2_dev;
        // 标志位,确定该设备属于那种设备,由V4L2_SUBDEV_FL_IS_XX宏确定
        u32 flags;
        // 指向主设备的v4l2_device结构体
        struct v4l2_device *v4l2_dev;
        // v4l2子设备的操作函数集合
        const struct v4l2_subdev_ops *ops;
        // 提供给v4l2框架的操作函数,只有v4l2框架会调用,驱动不使用
        const struct v4l2_subdev_internal_ops *internal_ops;
        // 从设备的控制接口
        struct v4l2_ctrl_handler *ctrl_handler;
        // 从设备的名称,必须独一无二
        char name[V4L2_SUBDEV_NAME_SIZE];
        // 从设备组的ID,由驱动定义,相似的从设备可以编为一组,
        u32 grp_id;
        // 从设备私有数据指针,一般指向i2c_client的设备结构体dev
        void *dev_priv;
        // 主设备私有数据指针,一般指向v4l2_device嵌入的结构体
        void *host_priv;
        // 指向video设备结构体
        struct video_device *devnode;
        // 指向物理设备
        struct device *dev;
        // 将所有从设备连接到全局subdev_list链表或notifier->done链表
        struct list_head async_list;
        // 指向struct v4l2_async_subdev,用于异步事件
        struct v4l2_async_subdev *asd;
        // 指向管理的notifier,用于主设备和从设备的异步关联
        struct v4l2_async_notifier *notifier;
        /* common part of subdevice platform data */
        struct v4l2_subdev_platform_data *pdata;
    };
    // 提供给v4l2框架的操作函数,只有v4l2框架会调用,驱动不使用
    struct v4l2_subdev_internal_ops {
        // v4l2_subdev注册时回调此函数,使v4l2_dev指向主设备的v4l2_device结构体
        int (*registered)(struct v4l2_subdev *sd);
        // v4l2_subdev卸载时回调此函数
        void (*unregistered)(struct v4l2_subdev *sd);
        // 应用调用open打开从设备节点时调用此函数
        int (*open)(struct v4l2_subdev *sd, struct v4l2_subdev_fh *fh);
        // 应用调用close时调用此函数
        int (*close)(struct v4l2_subdev *sd, struct v4l2_subdev_fh *fh);
    };

使用v4l2_subdev_init初始化v4l2_subdev结构体。然后必须用一个唯一的名字初始化subdev->name,同时初始化模块的owner域。若从设备是I2C设备,则可使用v4l2_i2c_subdev_init函数进行初始化,该函数内部会调用v4l2_subdev_init,同时设置flagsownerdevname等成员。

    [include/media/v4l2-subdev.h]
    // 初始化v4l2_subdev结构体
    // ops-v4l2子设备的操作函数集合指针,保存到v4l2_subdev结构体的ops成员中
    void v4l2_subdev_init(struct v4l2_subdev *sd,
		    const struct v4l2_subdev_ops *ops);

    [include/media/v4l2-common.h]
    // 初始化V4L2从设备为I2C设备的v4l2_subdev结构体
    // sd-v4l2_subdev结构体指针
    // client-i2c_client结构体指针
    // ops-v4l2子设备的操作函数集合指针,保存到v4l2_subdev结构体的ops成员中
    void v4l2_i2c_subdev_init(struct v4l2_subdev *sd, 
        struct i2c_client *client,
		const struct v4l2_subdev_ops *ops);

若需同媒体框架整合,必须调用media_entity_init初始化v4l2_subdev结构体中的media_entity结构体(entity域)。pads数组必须预先初始化。无须手动设置media_entitytypename域,但如有必要,revision域必须初始化。当(任何)从设备节点被打开/关闭,对entity的引用将被自动获取/释放。在从设备被注销之后,使用media_entity_cleanup清理media_entity结构体。如果从设备驱动趋向于处理视频并整合进了媒体框架,必须使用v4l2_subdev_pad_ops替代v4l2_subdev_video_ops实现格式相关的功能。这种情况下,子设备驱动应该设置link_validate域,以提供它自身的链接验证函数。链接验证函数应对管道(两端链接的都是V4L2从设备)中的每个链接调用。驱动还要负责验证子设备和视频节点间格式配置的正确性。如果link_validate操作没有设置,默认的v4l2_subdev_link_validate_default函数将会被调用。这个函数保证宽、高和媒体总线像素格式在链接的收发两端都一致。子设备驱动除了它们自己的检测外,也可以自由使用这个函数以执行上面提到的检查。

    [include/media/media-entity.h]
    // 初始化v4l2_subdev结构体中的media_entity结构体
    // entity-要初始化的media_entity结构体指针
    // num_pads-源pad的数量,与驱动子设备结构相关
    // pads-media_pad结构体数组,通常pad被嵌入到驱动自定义的结构体里面,
    //      数组地址被传递给该参数,pad需提前初始化
    // extra_links-函数会根据num_pads分配media_link数目,该参数则指明除了预分
                   配的数量之外还需要多少额外的media_link数目
    // 返回值 0-成功,小于0-失败
    int media_entity_init(struct media_entity *entity, u16 num_pads,
            struct media_pad *pads, u16 extra_links)

    // 清理media_entity结构体
    // entity-media_entity结构体指针
    void media_entity_cleanup(struct media_entity *entity)

    // v4l2-subdev pad层级的操作函数
    [include/media/v4l2-subdev.h]
	struct v4l2_subdev_pad_ops {
		// ioctl VIDIOC_SUBDEV_ENUM_MBUS_CODE命令处理函数
		int (*enum_mbus_code)(struct v4l2_subdev *sd,
				      struct v4l2_subdev_pad_config *cfg,
				      struct v4l2_subdev_mbus_code_enum *code);
		// ioctl VIDIOC_SUBDEV_ENUM_FRAME_SIZE命令处理函数
		int (*enum_frame_size)(struct v4l2_subdev *sd,
				       struct v4l2_subdev_pad_config *cfg,
				       struct v4l2_subdev_frame_size_enum *fse);
		// ioctl VIDIOC_SUBDEV_ENUM_FRAME_INTERVAL命令处理函数
		int (*enum_frame_interval)(struct v4l2_subdev *sd,
					   struct v4l2_subdev_pad_config *cfg,
					   struct v4l2_subdev_frame_interval_enum *fie);
		// ioctl VIDIOC_SUBDEV_G_FMT命令处理函数
		int (*get_fmt)(struct v4l2_subdev *sd,
			       struct v4l2_subdev_pad_config *cfg,
			       struct v4l2_subdev_format *format);
		// ioctl VIDIOC_SUBDEV_S_FMT命令处理函数
		int (*set_fmt)(struct v4l2_subdev *sd,
			       struct v4l2_subdev_pad_config *cfg,
			       struct v4l2_subdev_format *format);
		// ioctl VIDIOC_SUBDEV_G_SELECTION命令处理函数
		int (*get_selection)(struct v4l2_subdev *sd,
				     struct v4l2_subdev_pad_config *cfg,
				     struct v4l2_subdev_selection *sel);
		// ioctl VIDIOC_SUBDEV_S_SELECTION命令处理函数
		int (*set_selection)(struct v4l2_subdev *sd,
				     struct v4l2_subdev_pad_config *cfg,
				     struct v4l2_subdev_selection *sel);
		// ioctl VIDIOC_SUBDEV_G_EDID命令处理函数
		int (*get_edid)(struct v4l2_subdev *sd, struct v4l2_edid *edid);
		// ioctl VIDIOC_SUBDEV_S_EDID命令处理函数
		int (*set_edid)(struct v4l2_subdev *sd, struct v4l2_edid *edid);
		// ioctl VIDIOC_SUBDEV_DV_TIMINGS_CAP命令处理函数
		int (*dv_timings_cap)(struct v4l2_subdev *sd, struct v4l2_dv_timings_cap *cap);
		// ioctl VIDIOC_SUBDEV_ENUM_DV_TIMINGS命令处理函数
		int (*enum_dv_timings)(struct v4l2_subdev *sd, struct v4l2_enum_dv_timings *timings);
	#ifdef CONFIG_MEDIA_CONTROLLER
		// 用于多媒体控制器检查属于管道的链接是否可以用于流
		int (*link_validate)(struct v4l2_subdev *sd, struct media_link *link,
				     struct v4l2_subdev_format *source_fmt,
				     struct v4l2_subdev_format *sink_fmt);
	#endif /* CONFIG_MEDIA_CONTROLLER */
		// 获取当前低级媒体总线帧参数
		int (*get_frame_desc)(struct v4l2_subdev *sd, unsigned int pad,
				      struct v4l2_mbus_frame_desc *fd);	      
		// 设置低级媒体总线帧参数
		int (*set_frame_desc)(struct v4l2_subdev *sd, unsigned int pad,
				      struct v4l2_mbus_frame_desc *fd);
	};

从设备必须向V4L2子系统注册v4l2_subdev结构体,使用v4l2_device_register_subdev注册,使用v4l2_device_unregister_subdev注销。

    [include/media/v4l2-device.h]
    // 向V4L2子系统注册v4l2_subdev结构体
    // v4l2_dev-主设备v4l2_device结构体指针
    // sd-从设备v4l2_subdev结构体指针
    // 返回值 0-成功,小于0-失败
    int v4l2_device_register_subdev(struct v4l2_device *v4l2_dev,
                    struct v4l2_subdev *sd)

    // 从V4L2子系统注销v4l2_subdev结构体
    // sd-从设备v4l2_subdev结构体指针    
    void v4l2_device_unregister_subdev(struct v4l2_subdev *sd);

V4L2从设备驱动都必须有一个v4l2_subdev结构体。这个结构体可以单独代表一个简单的从设备,也可以嵌入到一个更大的结构体中,与更多设备状态信息保存在一起。通常有一个下级设备结构体(比如:i2c_client)包含了内核创建的设备数据。建议使用v4l2_set_subdevdata()将这个结构体的指针保存在v4l2_subdev的私有数据域(dev_priv)中。这使得通过v4l2_subdev找到实际的低层总线特定设备数据变得容易。同时也需要一个从低层结构体获取v4l2_subdev指针的方法。对于常用的i2c_client结构体,i2c_set_clientdata函数可用于保存一个v4l2_subdev指针,i2c_get_clientdata可以获取一个v4l2_subdev指针;对于其他总线可能需要使用其他相关函数。

    [include/media/v4l2-subdev.h]
    // 将i2c_client的指针保存到v4l2_subdev结构体的dev_priv成员中
    static inline void v4l2_set_subdevdata(struct v4l2_subdev *sd, void *p)
    {
        sd->dev_priv = p;
    }

    [include/linux/i2c.h]
    // 可以将v4l2_subdev结构体指针保存到i2c_client中dev成员的driver_data中
    static inline void i2c_set_clientdata(struct i2c_client *dev, void *data)
    {
        dev_set_drvdata(&dev->dev, data);
    }
    // 获取i2c_client结构体中dev成员的driver_data,一般指向v4l2_subdev
    static inline void *i2c_get_clientdata(const struct i2c_client *dev)
    {
        return dev_get_drvdata(&dev->dev);
    }

主设备驱动中也应保存每个子设备的私有数据,比如一个指向特定主设备的各设备私有数据的指针。为此v4l2_subdev结构体提供主设备私有数据域(host_priv),并可通过v4l2_get_subdev_hostdatav4l2_set_subdev_hostdata访问。

    [include/media/v4l2-subdev.h]
    static inline void *v4l2_get_subdev_hostdata(const struct v4l2_subdev *sd)
    {
        return sd->host_priv;
    }
    static inline void v4l2_set_subdev_hostdata(struct v4l2_subdev *sd, void *p)
    {
        sd->host_priv = p;
    }

每个v4l2_subdev都包含子设备驱动需要实现的函数指针(如果对此设备不适用,可为NULL),具体在v4l2_subdev_ops结构体当中。由于子设备可完成许多不同的工作,而在一个庞大的函数指针结构体中通常仅有少数有用的函数实现其功能肯定不合适。所以,函数指针根据其实现的功能被分类,每一类都有自己的函数指针结构体,如v4l2_subdev_core_opsv4l2_subdev_audio_opsv4l2_subdev_video_ops等等。v4l2_subdev_video_ops在视频设备中(第二章中)详细介绍。顶层函数指针结构体包含了指向各类函数指针结构体的指针,如果子设备驱动不支持该类函数中的任何一个功能,则指向该类结构体的指针为NULL。

    [include/media/v4l2-subdev.h]
    /* v4l2从设备的操作函数集合,从设备根据自身设备类型选择实现,
       其中core函数集通常可用于所有子设备,其他类别的实现依赖于
       子设备。如视频设备可能不支持音频操作函数,反之亦然。这样的
       设置在限制了函数指针数量的同时,还使增加新的操作函数和分类
       变得较为容易。 */
    struct v4l2_subdev_ops {
        // 从设备的通用操作函数集合,进行初始化、reset、控制等操作
        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;    // VBI设备
        const struct v4l2_subdev_ir_ops		*ir;
        const struct v4l2_subdev_sensor_ops	*sensor;
        const struct v4l2_subdev_pad_ops	*pad;
    };
    // 适用于所有v4l2从设备的操作函数集合
    struct v4l2_subdev_core_ops {
        // IO引脚复用配置
        int (*s_io_pin_config)(struct v4l2_subdev *sd, size_t n,
                        struct v4l2_subdev_io_pin_config *pincfg);
        // 初始化从设备的某些寄存器,使其恢复默认
        int (*init)(struct v4l2_subdev *sd, u32 val);
        // 加载固件
        int (*load_fw)(struct v4l2_subdev *sd);
        // 复位
        int (*reset)(struct v4l2_subdev *sd, u32 val);
        // 设置GPIO引脚输出值
        int (*s_gpio)(struct v4l2_subdev *sd, u32 val);
        // 设置从设备的电源状态,0-省电模式,1-正常操作模式
        int (*s_power)(struct v4l2_subdev *sd, int on);
        // 中断函数,被主设备的中断函数调用
        int (*interrupt_service_routine)(struct v4l2_subdev *sd,
                            u32 status, bool *handled);
        ......
    };

使用v4l2_device_register_subdev注册从设备后,就可以调用v4l2_subdev_ops中的方法了。可以通过v4l2_subdev直接调用,也可以使用内核提供的宏定义v4l2_subdev_call间接调用某一个方法。若要调用多个从设备的同一个方法,则可使用v4l2_device_call_all宏定义。

    // 直接调用
    err = sd->ops->video->g_std(sd, &norm);

    // 使用宏定义调用,这个宏将会做NULL指针检查,如果su为NULL,则返回-ENODEV;
    // 如果sd->ops->video或sd->ops->video->g_std为NULL,则返回-ENOIOCTLCMD;
    // 否则将返回sd->ops->video->g_std的调用的实际结果
	err = v4l2_subdev_call(sd, video, g_std, &norm);

    [include/media/v4l2-subdev.h]
    #define v4l2_subdev_call(sd, o, f, args...)				\
	(!(sd) ? -ENODEV : (((sd)->ops->o && (sd)->ops->o->f) ?	\
		(sd)->ops->o->f((sd) , ##args) : -ENOIOCTLCMD))


    v4l2_device_call_all(v4l2_dev, 0, video, g_std, &norm);
    [include/media/v4l2-device.h]
    #define v4l2_device_call_all(v4l2_dev, grpid, o, f, args...)		\
	do {								\
		struct v4l2_subdev *__sd;				\
		__v4l2_device_call_subdevs_p(v4l2_dev, __sd,		\
			!(grpid) || __sd->grp_id == (grpid), o, f ,	\
			##args);					\
	} while (0)

如果子设备需要通知它的v4l2_device主设备一个事件,可以调用v4l2_subdev_notify(sd,notification, arg)。这个宏检查是否有一个notify回调被注册,如果没有,返回-ENODEV。否则返回 notify调用结果。notify回调函数由主设备提供。

    [include/media/v4l2-device.h]
    // 从设备通知主设备,最终回调到v4l2_device的notify函数
    static inline void v4l2_subdev_notify(struct v4l2_subdev *sd,
                        unsigned int notification, void *arg)
    {
        if (sd && sd->v4l2_dev && sd->v4l2_dev->notify)
            sd->v4l2_dev->notify(sd, notification, arg);
    }

使用v4l2_subdev的好处在于它是一个通用结构体,且不包含任何底层硬件信息。所有驱动可以包含多个I2C总线的从设备,但也有从设备是通过GPIO控制。这个区别仅在配置设备时有关系,一旦子设备注册完成,对于v4l2子系统来说就完全透明了。

2.3.V4L2主设备和从设备匹配过程分析

V4L2主设备和从设备采用异步的匹配方法。首先介绍一下异步匹配用到的方法。主设备使用v4l2_async_notifier_register函数进行异步匹配,匹配到从设备,则调用v4l2_device_register_subdev函数注册从设备,使用v4l2_async_notifier_unregister函数异步取消匹配。从设备使用v4l2_async_register_subdev函数异步匹配主设备,若匹配到主设备,则调用v4l2_device_register_subdev函数注册从设备,使用v4l2_async_unregister_subdev函数异步取消匹配。
匹配的方法由v4l2_async_subdev结构体决定,主设备可以有多个v4l2_async_subdev结构体,也说明主设备有多种匹配从设备的方法。match_type表示匹配方式,由枚举v4l2_async_match_type定义,具体有使用设备名称匹配-V4L2_ASYNC_MATCH_DEVNAME、使用I2C adapter ID and address进行匹配-V4L2_ASYNC_MATCH_I2C等。联合体match中包含了具体的匹配信息,根据匹配方式进行设置。v4l2_async_notifier管理整个匹配过程,未匹配的v4l2_async_subdev结构体被挂到waiting链表,匹配完成的挂到done链表同时调用bound函数进行绑定。

    [include/media/v4l2-async.h]
    // 主设备和从设备的匹配方式 
    enum v4l2_async_match_type {
        // 传统的匹配方式,使用v4l2_async_subdev的match方法进行匹配
        V4L2_ASYNC_MATCH_CUSTOM,  
        // 使用设备名称进行匹配
        V4L2_ASYNC_MATCH_DEVNAME,
        // 使用I2C adapter ID and address进行匹配
        V4L2_ASYNC_MATCH_I2C,
        // 使用firmware node 进行匹配
        V4L2_ASYNC_MATCH_OF,
    };
    struct v4l2_async_subdev {
        // 匹配方式
        enum v4l2_async_match_type match_type;
        union {
            struct {
                // 设备树匹配方式
                const struct device_node *node;
            } of;
            struct {
                // 设备名称匹配方式
                const char *name;
            } device_name;
            struct {
                // 使用I2C adapter ID and address进行匹配
                int adapter_id;
                unsigned short address;
            } i2c;
            struct {
                // 传统的匹配方式
                bool (*match)(struct device *,
                        struct v4l2_async_subdev *);
                void *priv;
            } custom;
        } match;
        // v4l2-async核心层使用,将此结构体挂入到notifier的waiting链表,驱动不可使用
        struct list_head list;
    };

    // 主设备注册一个notifier,用于异步的和从设备进行匹配
    int v4l2_async_notifier_register(struct v4l2_device *v4l2_dev,
                    struct v4l2_async_notifier *notifier);
    // 主设备注销notifier
    void v4l2_async_notifier_unregister(struct v4l2_async_notifier *notifier);
    
    // 从设备异步注册v4l2_subdev,用于异步的和主设备进行匹配
    int v4l2_async_register_subdev(struct v4l2_subdev *sd);
    // 从设备注销
    void v4l2_async_unregister_subdev(struct v4l2_subdev *sd);

    struct v4l2_async_notifier {
        unsigned int num_subdevs;  // subdevices的数量
        // 指针数组,指向subdevice descriptors
        struct v4l2_async_subdev **subdevs;  
        // 指向struct v4l2_device
        struct v4l2_device *v4l2_dev;
        // v4l2_async_subdev的链表,等待匹配drivers
        struct list_head waiting;
        // 已经probed的v4l2_subdev链表
        struct list_head done;
        // 挂在全局的notifiers链表上
        struct list_head list;
        // 驱动匹配到从设备后调用此函数
        int (*bound)(struct v4l2_async_notifier *notifier,
                struct v4l2_subdev *subdev,
                struct v4l2_async_subdev *asd);
        // 所有从设备被probed成功,调用此函数
        int (*complete)(struct v4l2_async_notifier *notifier);
        // 从设备注销时调用此函数
        void (*unbind)(struct v4l2_async_notifier *notifier,
                struct v4l2_subdev *subdev,
                struct v4l2_async_subdev *asd);
    };

以imx6ull为例分析主设备和从设备的匹配过程。v4L2主设备和从设备的匹配过程可通过分析v4l2_async_notifier_register函数得到。可总结如下:
(1)首先初始化需要匹配的v4l2_async_notifier结构体,主要设备匹配方式、bound函数和指向v4l2_async_subdev结构体的指针。
(2)设置v4l2_async_notifierv4l2_dev指针指向主设备的v4l2_device结构体。
(3)遍历subdevs指向的v4l2_async_subdev结构体,全部挂入waiting链表。
(4)将v4l2_async_notifier结构体挂入到全局的notifier_list链表。
(5)遍历subdev_list链表(所有的从设备都挂到subdev_list链表中),和waiting链表中的每一个v4l2_async_subdev结构体进行匹配。
(6)若匹配成功,则将v4l2_async_subdevwaiting链表中删除,设置从设备V4l2_subdev结构体的notifierasd成员,使其指向mx6s_csi_dev结构体中的v4l2_async_notifierv4l2_async_subdev结构体。
(7)调用bound函数,也就是使主设备mx6s_csi_dev结构体中的sd成员指向了从设备的v4l2_subdev结构体,从而完成了主设备和从设备进行绑定。
(8)将匹配成功的从设备V4l2_subdev结构体从全局的subdev_list链表中移除,同时添加到v4l2_async_notifierdone链表中。
(9)调用v4l2_device_register_subdev注册从设备。

V4L2设备匹配过程

    v4l2_async_notifier_register
        // num_subdevs为0或num_subdevs大于128,返回错误
        if (!notifier->num_subdevs || notifier->num_subdevs > V4L2_MAX_SUBDEVS)
		    return -EINVAL;
        // notifier->v4l2_dev指向v4l2_device结构体指针v4l2_dev
        notifier->v4l2_dev = v4l2_dev;
        
        for (i = 0; i < notifier->num_subdevs; i++) {
            asd = notifier->subdevs[i];
            switch (asd->match_type) {
            case V4L2_ASYNC_MATCH_CUSTOM:
            case V4L2_ASYNC_MATCH_DEVNAME:
            case V4L2_ASYNC_MATCH_I2C:
            case V4L2_ASYNC_MATCH_OF:
                break;
                ......
            }
            // 将指针数组subdevs指向的v4l2_async_subdev结构体挂入
            // notifier的waiting链表
            list_add_tail(&asd->list, &notifier->waiting);
        }
        mutex_lock(&list_lock);  // 加锁,保护全局链表
        // 将v4l2_async_notifier挂到全局链表notifier_list
        list_add(&notifier->list, &notifier_list);   

        // 遍历subdev_list链表,所有从设备的v4l2_subdev结构体都挂到subdev_list链表
        list_for_each_entry_safe(sd, tmp, &subdev_list, async_list) {
            int ret;
            // 判断子设备的v4l2_subdev是否和主设备的notifier匹配,
            // 匹配则返回v4l2_async_subdev结构体
            asd = v4l2_async_belongs(notifier, sd);
            if (!asd)
                continue;
            // 匹配成功,调用bound函数,注册从设备的v4l2_subdev结构体
            ret = v4l2_async_test_notify(notifier, sd, asd);
            if (ret < 0) {
                mutex_unlock(&list_lock);
                return ret;
            }
        }
        mutex_unlock(&list_lock);
        return 0;

    v4l2_async_belongs
        // 定义匹配的函数指针
        bool (*match)(struct device *, struct v4l2_async_subdev *);
        // 遍历waiting链表,取出v4l2_async_subdev结构体
        list_for_each_entry(asd, &notifier->waiting, list) {
            // 确认匹配方式
            switch (asd->match_type) {
            case V4L2_ASYNC_MATCH_CUSTOM:
                match = asd->match.custom.match;
                if (!match)
                    return asd;
                break;
            case V4L2_ASYNC_MATCH_DEVNAME:
                match = match_devname;  // 设备名称匹配方法
                break;
            case V4L2_ASYNC_MATCH_I2C:  // I2C匹配方法
                match = match_i2c;
                break;
            case V4L2_ASYNC_MATCH_OF:  // 设备树的匹配方法
                match = match_of;
                break;
                ......
            }

            // 根据匹配方式,调用相应的匹配方法
            if (match(sd->dev, asd))
                return asd;
        }
        return NULL;

    // 根据设备名称匹配,
    static bool match_devname(struct device *dev, struct v4l2_async_subdev *asd)
    {
        return !strcmp(asd->match.device_name.name, dev_name(dev));
    }
    // 对比i2c_client的adapter_id和address
    static bool match_i2c(struct device *dev, struct v4l2_async_subdev *asd)
    {
    #if IS_ENABLED(CONFIG_I2C)
        struct i2c_client *client = i2c_verify_client(dev);
        return client &&
            asd->match.i2c.adapter_id == client->adapter->nr &&
            asd->match.i2c.address == client->addr;
    #else
        return false;
    #endif
    }
    // 对比设备树节点
    static bool match_of(struct device *dev, struct v4l2_async_subdev *asd)
    {
        return dev->of_node == asd->match.of.node;
    }

    v4l2_async_test_notify
        // 从waiting链表中移除
        list_del(&asd->list);
        sd->asd = asd;
        sd->notifier = notifier;
        // bound非空,则调用bound函数,bound函数的主要作用是设置主设备的v4l2_subdev指针,
        // 使其指向匹配的从设备的v4l2_subdev结构体,从而完成主设备到从设备的绑定
        if (notifier->bound) {
            ret = notifier->bound(notifier, sd, asd);
            ......
        }
        // 将subdevice从async_list链表中移除后挂到done链表中
        list_move(&sd->async_list, &notifier->done);
        // 注册从设备v4l2_subdev结构体
        v4l2_device_register_subdev(notifier->v4l2_dev, sd)
        // 如果waiting链表无等待的v4l2_async_subdev,且complete非空
        if (list_empty(&notifier->waiting) && notifier->complete)
            return notifier->complete(notifier);  // 调用完成函数

参考资料

  1. https://blog.csdn.net/kickxxx/article/details/8484498
  2. https://blog.csdn.net/weixin_44139476/article/details/107021395
  3. 内核文档-v4l2-framework_zh_CN.txt
  4. Linux内核4.1版本源码
  5. Android驱动开发权威指南
  6. Android驱动开发与移植实战详解
  7. https://linuxtv.org/downloads/v4l-dvb-apis/driver-api/v4l2-subdev.html
  • 14
    点赞
  • 76
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
Linux是一种自由开源的操作系统,而C是一种被广泛应用于系统编程的高级程序设计语言,而V4L2则是Linux内核中提供的视频设备接口,用于控制、操作和驱动视频设备。 首先,Linux提供了广泛的功能和不同类型的应用程序,但也需要与硬件设备进行交互和通信。这就需要使用C语言进行系统编程,以实现对操作系统的底层控制和访问。 V4L2是Linux内核中的一个子系统,用于管理视频设备。这个子系统通过V4L2接口提供了访问和控制视频设备的功能,如摄像头、视频录制设备等。通过V4L2接口,我们可以通过C语言编写程序来访问视频设备,实现视频的捕获、处理、显示等功能。 在Linux C下使用V4L2可以实现丰富的视频处理和应用。例如,我们可以通过调用V4L2接口在C语言程序中实现视频捕获功能,从摄像头获取视频数据,再通过C语言对视频数据进行处理或分析,最后将结果在显示上展示出来。此外,我们还可以通过V4L2的接口设置摄像头的曝光、对焦、白平衡等参数,以及调整图像质量、编码格式等。 总之,Linux C和V4L2是一对强大的组合,可以让开发者以C语言的方式来访问和控制视频设备,实现丰富的视频处理和应用。无论是开发视频监控系统、图像处理应用,还是进行计算机视觉研究,Linux C和V4L2都提供了灵活、高效的编程环境和接口。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值