加入星计划,您可以享受以下权益:

  • 创作内容快速变现
  • 行业影响力扩散
  • 作品版权保护
  • 300W+ 专业用户
  • 1.5W+ 优质创作者
  • 5000+ 长期合作伙伴
立即加入
  • 正文
    • 一、V4L2架构
    • 二、V4L2架构包括哪些设备
    • 三、Linux内核中V4L2驱动代码
    • 四、结构体详解
    • 五、 video_device、v4l2_device和v4l2_subdev的关系举例
    • 六、videobuf2
    • 七、v4l2拓扑结构
  • 相关推荐
  • 电子产业图谱
申请入驻 产业图谱

Camera | 5.Linux v4l2架构(基于rk3568)

2023/03/02
3823
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论
瑞芯微 | 摄像头ov13850移植笔记

Camera | 1.Camera基础知识​

Camera | 2.MIPI、CSI基础

Camera | 3.瑞芯微平台MIPI摄像头常用调试命令

Camera | 4.瑞芯微平台MIPI摄像头应用程序编写

上一篇我们讲解了如何编写基于V4L2的应用程序编写,本文主要讲解内核中V4L2架构,以及一些最重要的结构体、注册函数。

厂家在实现自己的摄像头控制器驱动时,总体上都遵循这个架构来实现,但是不同厂家、不同型号的SoC,具体的驱动实现仍然会有一些差别。

读者可以通过本文了解各个结构体与对应的摄像头模块、SoC上控制器模块、以及他们之间接口关系,并能够了解这些硬件模块与V4L2架构之间关系。

下一张我们基于瑞芯微rk3568来详细讲解具体V4L2的实现。

一、V4L2架构

V4L2子系统是Linux内核中关于Video(视频)设备的API接口,是V4L(Video for Linux)子系统的升级版本。

V4L(Video for Linux)是Linux内核中关于视频设备的API接口,出现于Linux内核2.1版本,经过修改bug和添加功能,Linux内核2.5版本推出了V4L2(Video for Linux Two)子系统,功能更多且更稳定。

V4L2子系统向上为虚拟文件系统提供了统一的接口,应用程序可通过虚拟文件系统访问Video设备。

V4L2子系统向下给Video设备提供接口,同时管理所有Video设备。

二、V4L2架构包括哪些设备

Video设备又分为主设备和从设备对于Camera来说,

主设备:
Camera Host控制器为主设备,负责图像数据的接收和传输,

从设备:
从设备为Camera Sensor,一般为I2C接口,可通过从设备控制Camera采集图像的行为,如图像的大小、图像的FPS等。

V4L2的主设备号是81,次设备号范围0~255
这些次设备号又分为多类设备:

    视频设备(次设备号范围0-63)Radio(收音机)设备(次设备号范围64-127)Teletext设备(次设备号范围192-223)VBI设备(次设备号范围224-255)。
    1. V4L2设备对应的设备节点有

/dev/videoX、/dev/vbiX、/dev/radioX。本文只讨论视频设备,视频设备对应的设备节点是/dev/videoX

    ,视频设备以高频摄像头或Camera为输入源,Linux内核驱动该类设备,接收相应的视频信息并处理。

V4L2框架的架构如下图所示:

user space:

应用程序主要通过libv4l库来操作摄像头

Applications: 也可以基于字符设备/dev/videoX自己编写应用程序

guvcview:用于调试usb摄像头(还有个软件cheese也可以)

v4l2 utilities: v4l2 的工具集(参考前面第3篇文章)

kernel space:

ensor、ISP、VIPP、CSI、CCI都为从设备

从dphy物理层获取视频数据册通过vb2子模块

CCI :主要是通过GPIO(供电、片选)、I2C(下发配置命令给sensor)实现配置sensor

EHCI/OHCI:USB类型摄像头

hardware

SIC Controller:从dphy获取mipi协议帧
I2C Controller:与sensor的i2c block通信

GPIO Controller:sensor通常需要供电或者片选

external device
sensror:

像头的接口主要有:USB,DVP.MIPI(CSI)

三、Linux内核中V4L2驱动代码

Linux系统中视频输入设备主要包括以下四个部分:

1.字符设备驱动:V4L2本身就是一个字符设备,具有字符设备所有的特性,暴露接口给用户空间;

2.V4L2驱动核心:主要是构建一个内核中标准视频设备驱动的框架,为视频操作提供统一的接口函数;

3.平台V4L2设备驱动:在V4L2框架下,根据平台自身的特性实现与平台相关的V4L2驱动部分,包括注册video_device和v4l2_device;

4.具体的sensor驱动:主要上电、提供工作时钟、视频图像裁剪、流IO开启等,实现各种设备控制方法供上层调用并注册v4l2_subdev。

V4L2核心源码位于drivers/media/v4l2-core,根据功能可以划分为四类:

由上图可知:

1.字符设备模块:由v4l2-dev.c实现,主要作用申请字符主设备号、注册class和提供video device注册注销等相关函数。

2.V4L2基础框架:由v4l2-device.c、v4l2-subdev.c、v4l2-fh.c、v4l2-ctrls.c等文件构建V4L2基础框架。

3.videobuf管理
由videobuf2-core.c、videobuf2-dma-contig.c、videobuf2-dma-sg.c、videobuf2-memops.c、videobuf2-vmalloc.c、v4l2-mem2mem.c等文件实现,完成videobuffer的分配、管理和注销。

4.Ioctl框架:由v4l2-ioctl.c文件实现,构建V4L2 ioctl的框架。

瑞芯微平台还包括ISP的驱动框架,下面是rk3568对应的ISP相关代码:
Linux Kernel-4.19
 |-- arch/arm/boot/dts DTS配置文件
 |-- drivers/phy/rockchip
 |-- phy-rockchip-mipi-rx.c mipi dphy驱动
 |-- phy-rockchip-csi2-dphy-common.h
 |-- phy-rockchip-csi2-dphy-hw.c
 |-- phy-rockchip-csi2-dphy.c
 |-- drivers/media
  |-- v4l2-core
  |-- platform/rockchip/cif RKCIF驱动
  |-- platform/rockchip/isp RKISP驱动
   |-- dev.c 包含 probe、异步注册、clock、pipeline、 iommu及media/v4l2 framework
   |-- capture_v21.c 包含 mp/sp/rawwr的配置及 vb2,帧中断处理
   |-- dmarx.c 包含 rawrd的配置及 vb2,帧中断处理
   |-- isp_params.c 3A相关参数设置
   |-- isp_stats.c 3A相关统计
   |-- isp_mipi_luma.c mipi数据亮度统计
   |-- regs.c 寄存器相关的读写操作
   |-- rkisp.c isp subdev和entity注册,包含从 mipi 接收数据,并有 crop 功能
   |-- csi.c csi subdev和mipi配置
   |-- bridge.c bridge subdev,isp和ispp交互桥梁
  |-- platform/rockchip/ispp rkispp驱动
   |-- dev.c 包含 probe、异步注册、clock、pipeline、 iommu及media/v4l2 framework
   |-- stream.c 包含 4路video输出的配置及 vb2,帧中断处理
   |-- rkispp.c ispp subdev和entity注册
   |-- params.c TNR/NR/SHP/FEC/ORB参数设置
   |-- stats.c ORB统计信息
  |-- i2c
   |-- ov13850.c CIS(cmos image sensor)驱动

四、结构体详解

V4L2中有几个最重要的几个结构体,v4l2_device、video_device、v4l2_subdev等。他们大致关系如下:

1.v4l2_device主设备

V4L2主设备实例使用struct v4l2_device结构体表示,v4l2_device是V4L2子系统的入口,管理着V4L2子系统的主设备和从设备;

v4l2_device用来描述一个v4l2设备实例,可以包含多个子设备,对应的是例如 I2C、CSI、MIPI 等设备,它们是从属于一个 V4L2 device 之下的;

简单设备可以仅分配这个结构体,但在大多数情况下,都会将这个结构体嵌入到一个更大的结构体中以提供v4l2框架的功能,比如struct isp_device

需要与媒体框架整合的驱动必须手动设置dev->driver_data,指向包含v4l2_device结构体实例的驱动特定设备结构体。这可以在注册V4L2设备实例前通过dev_set_drvdata()函数完成。

同时必须设置v4l2_device结构体的mdev域,指向适当的初始化并注册过的media_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_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。

这样会生成类似ivtv0、ivtv1等名字。若驱动名以数字结尾,则会在编号和驱动名间插入一个破折号,如:cx18-0、cx18-1等。

dev参数通常是一个指向pci_dev、usb_interface或platform_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;
    }

2. video_device

V4L2子系统使用v4l2_device结构体管理设备,设备的具体操作方法根据设备类型决定,

前面说过管理的设备分为很多种,

若是视频设备,则需要注册video_device结构体,并提供相应的操作方法。

对于视频设备Camera而言,Camera控制器可以视为主设备,接在Camera控制器上的摄像头可以视为从设备

struct video_device
{
 const struct v4l2_file_operations *fops;
 struct cdev *cdev;     //vdev->cdev->ops = &v4l2_fops;  字符设备描述符
 struct v4l2_device *v4l2_dev;
 struct v4l2_ctrl_handler *ctrl_handler;

 struct vb2_queue *queue;  //q->ops = &dmarx_vb2_ops; buf操作真正驱动回调函数
  …………
 const struct v4l2_ioctl_ops *ioctl_ops;//vdev->ioctl_ops = &rkisp_dmarx_ioctl; 
  …………
};

注册函数:

[rk_android11.0_sdk_220718kerneldriversmediav4l2-corev4l2-dev.c]


static inline int __must_check video_register_device(struct video_device *vdev,
        int type, int nr)
{
    return __video_register_device(vdev, type, nr, 1, vdev->fops->owner);
}
int __video_register_device(struct video_device *vdev, int type, int nr,
        int warn_if_nr_in_use, struct module *owner)
{
    ····
    int minor_cnt = VIDEO_NUM_DEVICES;//次设备个数默认为256
    const char *name_base;

    /* A minor value of -1 marks this video device as never
       having been registered */
    vdev->minor = -1;

    /* the release callback MUST be present 如果之前没有声明销毁函数,则报错*/
    if (WARN_ON(!vdev->release))
        return -EINVAL;
    /* the v4l2_dev pointer MUST be present 如果之前未注册v4l2_device则报错*/
    if (WARN_ON(!vdev->v4l2_dev))
        return -EINVAL;

    /* Part 1: check device type */
    switch (type) {
    //根据设备类型类注册设备,摄像头设备为VFL_TYPE_GRABBER类型
    case VFL_TYPE_GRABBER:
        name_base = "video";
    ·····
    ·····
    vdev->vfl_type = type;
    vdev->cdev = NULL;
    if (vdev->dev_parent == NULL)
        vdev->dev_parent = vdev->v4l2_dev->dev;
    if (vdev->ctrl_handler == NULL)
        //设置video_device的ctrl_handler,存在v4l2_device结构体中
        vdev->ctrl_handler = vdev->v4l2_dev->ctrl_handler;
    /* Part 2: find a free minor, device node number and device index. */
    /*2.寻找空闲次设备号,设备个数和设备下标*/

    /* Pick a device node number 寻找一个空项位置*/
    mutex_lock(&videodev_lock);
    nr = devnode_find(vdev, nr == -1 ? 0 : nr, minor_cnt);
    //
    if (nr == minor_cnt)
        nr = devnode_find(vdev, 0, minor_cnt);
    if (nr == minor_cnt) {
        printk(KERN_ERR "could not get a free device node numbern");
        mutex_unlock(&videodev_lock);
        return -ENFILE;
    }
#ifdef CONFIG_VIDEO_FIXED_MINOR_RANGES
    /* 1-on-1 mapping of device node number to minor number */
    i = nr;
#else
    /* The device node number and minor numbers are independent, so
       we just find the first free minor number. */
    for (i = 0; i < VIDEO_NUM_DEVICES; i++)
        if (video_device[i] == NULL)
            break;
    if (i == VIDEO_NUM_DEVICES) {
        mutex_unlock(&videodev_lock);
        printk(KERN_ERR "could not get a free minorn");
        return -ENFILE;
    }
#endif
    vdev->minor = i + minor_offset;
    vdev->num = nr;
    devnode_set(vdev);

    /* Should not happen since we thought this minor was free */

    vdev->index = get_index(vdev);
    video_device[vdev->minor] = vdev;

    if (vdev->ioctl_ops)
        determine_valid_ioctls(vdev);

    /* Part 3: Initialize the character device */
    vdev->cdev = cdev_alloc();
    if (vdev->cdev == NULL) {
        ret = -ENOMEM;
        goto cleanup;
    }
    vdev->cdev->ops = &v4l2_fops;//设置字符设备的系统调用函数
    vdev->cdev->owner = owner;

    //注册字符设备
    ret = cdev_add(vdev->cdev, MKDEV(VIDEO_MAJOR, vdev->minor), 1);

    /* Part 4: register the device with sysfs */
    vdev->dev.class = &video_class;
    vdev->dev.devt = MKDEV(VIDEO_MAJOR, vdev->minor);
    vdev->dev.parent = vdev->dev_parent;
    //设置video结点名称,如果设备类型为VFL_TYPE_GRABBER,名称为videoX
    dev_set_name(&vdev->dev, "%s%d", name_base, vdev->num);

    //注册device文件,生成设备文件/dev/videoX
    ret = device_register(&vdev->dev);
    /* Register the release callback that will be called when the last
       reference to the device goes away. */
    //设置销毁video设备的回调函数
    vdev->dev.release = v4l2_device_release;

    /* Increase v4l2_device refcount */
    v4l2_device_get(vdev->v4l2_dev);

这个函数主要做四件事:

    检查设备类型,赋予设备名称寻找一个空闲的设备位置,寻找合适的主设备号和次设号初始化字符设备,使用v4l2_device的v4l2_fops初始化video_device的fops,release函数等注册字符设备,并生成/dev/videoX结点,注册subdev时也会调用这个接口

3. v4l2_subdev从设备

V4L2从设备使用struct v4l2_subdev结构体表示,该结构体用于对子设备进行抽象。

几乎所有的设备都有多个 IC 模块

    它们可能是实体的(例如 USB 摄像头里面包含 ISP、sensor 等)也可能是抽象的(如 USB 设备里面的抽象拓扑结构)它们在 /dev 目录下面生成了多个设备节点,并且这些 IC 模块还创建了一些非 v4l2 设备:DVB、ALSA、FB、I2C 和输入设备。

通常情况下,这些IC模块通过一个或者多个 I2C 总线连接到主桥驱动上面,同时其它的总线仍然可用,这些 IC 就称为 ‘sub-devices’

一个V4L2主设备可能对应多个V4L2从设备,所有主设备对应的从设备都挂到v4l2_device结构体的subdevs链表中。

对于视频设备,从设备就是摄像头,通常情况下是I2C设备,主设备可通过I2C总线控制从设备

例如控制摄像头的焦距、闪光灯等,同时使用 MIPI 或者 LVDS 等接口进行图像数据传输

struct v4l2_subdev中包含的struct v4l2_subdev_ops是一个完备的操作函数集,用于对接各种不同的子设备,比如video、audio、sensor等;

同时还有一个核心的函数集struct v4l2_subdev_core_ops,提供更通用的功能。子设备驱动根据设备特点实现该函数集中的某些函数即可。

 [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,同时设置flags、owner、dev、name等成员。

 [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);

从设备必须向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找到实际的低层总线特定设备数据。

对于常用的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_hostdata和 v4l2_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_ops、v4l2_subdev_audio_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子系统来说就完全透明了。

4.    v4l2_fh

文件访问控制

5.   v4l2_ctrl_handler

控制模块,提供子设备(主要是 video 和 ISP 设备)在用户空间的特效操作接口

6.   media_device

用于运行时数据流的管理,嵌入在 V4L2 device 内部

五、 video_device、v4l2_device和v4l2_subdev的关系举例

下面以我们手机的摄像头来举例:

    1. 假定一款CMOS摄像头,有两个接口:

一个是摄像头接口(数据),一个是I2C接口(控制命令)

摄像头接口负责传输图像数据,I2C接口负责传输控制信息,所以又可以将CMOS摄像头看作是一个I2C模块

在一款SoC芯片上面,摄像头相关的有摄像头控制器、摄像头接口、I2C总线SOC上可以有多个摄像头控制器,多个摄像头接口,多个I2C总线
摄像头控制器负责接收和处理摄像头数据,摄像头接口负责传输图像数据,I2C总线负责传输控制信息

对于手机而言,一般都有两个摄像头:一个前置摄像头,一个后置摄像头

如下图所示:

我们可以选择让控制器去操作哪一个摄像头(可以使用某个gpio供电,通过电平来选择摄像头),这就做到了使用一个摄像头控制器来控制多个摄像头,这就是多路复用

我们回到V4L2来,再来谈v4l2_device和v4l2_subdev:

    • v4l2_device表示一个v4l2实例,在V4L2驱动中,使用v4l2_device来表示摄像头控制器使用v4l2_subdev来表示具体的某一个摄像头的I2C控制模块,进而通过其控制摄像头v4l2_device里有一个v4l2_subdev链表,可以选择v4l2_device去控制哪一个v4l2_subdev

 

    subdev的设计目的是为了多路复用,就是用一个v4l2_device可以服务多个v4l2_subdev

然而某些驱动是没有v4l2_subdev,只有video_device

我们用一张图来总结设备之间关系:

    video_device是一个字符设备,video_device内含一个cdevv4l2_device是一个v4l2实例,嵌入到video_device中v4l2_device维护者一个链表管理v4l2_subdev,v4l2_subdev表示摄像头的I2C控制模块主设备可通过v4l2_subdev_call的宏调用从设备提供的方法,反过来从设备可以调用主设备的notify方法通知主设备某些事件发生了。

核心层(core)负责注册字符设备,然后提供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来操作硬件。

一口君再把各个结构体与各回调函数之间关系汇总到下面这个图里(rk3568):

主要架构部分Linux内核已经实现了,Camera控制器驱动,厂家一般都会实现,对于一般驱动工程师来说,我们只需要实现子设备驱动即可。

六、videobuf2

从数据流角度来分析,V4L2框架可以分成两个部分看:控制流+数据流

    控制流主要由v4l2_subdev的回调函数实现(一般由摄像头厂商提供),主要用于控制摄像数据流的部分就是video buffer,驱动部分通常由SoC厂商提供(比如瑞芯微rk3568平台,对应到rkisp_rawrd0_m、rkisp_rawrd2_s子模块)。

V4L2的buffer管理是通过videobuf2来完成的,它充当用户空间和驱动之间的中间层,并提供low-level,模块化的内存管理功能;

回顾一下获取摄像头视频流的主要步骤:

要获取图像信息需要执行VIDIOC_DQBUF、VIDIOC_QBUF命令。

瑞芯微rk3568平台videobuf2相关结构体和ops回调函数关系如下:

其中struct rkisp_device是瑞芯微3568平台用于管理Camera控制器的最重要的结构体

struct rkisp_capture_device 对应拓扑结构中的模块rkisp_rawrd0_m 、rkisp_rawrd2_s 。

该模块是一个video设备,用于获取原始图像信息,所以在struct rkisp_vdev_node vnode中包含了struct vb2_queue buf_queue、struct video_device vdev

struct vb2_queue中的回调函数struct vb2_mem_ops *mem_ops、struct vb2_buf_ops *buf_ops、struct vb2_ops *ops就是videobuf2驱动。

videobuf2驱动部分相关结构体如下:

上图大体包含了videobuf2的框架;

    vb2_queue:核心的数据结构,用于描述buffer的队列,其中struct vb2_buffer *bufs[]是存放buffer节点的数组,该数组中的成员代表了vb2 buffer,并将在queued_list和done_list两个队列中进行流转;struct vb2_buf_ops:buffer的操作函数集,由驱动来实现,并由框架通过call_bufop宏来对特定的函数进行调用;struct vb2_mem_ops:内存buffer分配函数接口,buffer类型分为三种:1)虚拟地址和物理地址都分散,可以通过dma-sg来完成;2)物理地址分散,虚拟地址连续,可以通过vmalloc分配;3)物理地址连续,可以通过dma-contig来完成;三种类型也vb2框架中都有实现,框架可以通过call_memop来进行调用;struct vb2_ops:vb2队列操作函数集,由驱动来实现对应的接口,并在框架中通过call_vb_qop宏被调用;

调用流程:

                     通用接口    ----------isp ioctrl接口----------           驱动                        
字符设备->v4l2_ioctl->v4l_qbuf->vb2_ioctl_qbuf->vb2_qbuf->vb2_core_qbuf->rkisp_buf_queue
    下面是VIDIOC_DQBUF命令执行的 log【在函数vb2_core_dqbuf入口调用stack_dump()】:
/* */
[  105.813743] vb2_core_dqbuf+0x54/0x5b8
[  105.813753] vb2_dqbuf+0x94/0xc8
[  105.813763] vb2_ioctl_dqbuf+0x50/0x60

[  105.813774] v4l_dqbuf+0x44/0x58
[  105.813785] __video_do_ioctl+0x1a0/0x348
[  105.813795] video_usercopy+0x228/0x740
[  105.813805] video_ioctl2+0x14/0x20
[  105.813815] v4l2_ioctl+0x44/0x68
[  105.813825] v4l2_compat_ioctl32+0x1d0/0x3a48

[  105.813836] __arm64_compat_sys_ioctl+0xbc/0x15b0
[  105.813847] el0_svc_common.constprop.0+0x64/0x178
[  105.813859] el0_svc_compat_handler+0x18/0x20
[  105.813869] el0_svc_compat+0x8/0x34
    VIDIOC_QBUF命令执行的log:
[  105.944858] vb2_core_qbuf+0x28/0x338
[  105.944883] vb2_qbuf+0x6c/0x90
[  105.944904] vb2_ioctl_qbuf+0x48/0x58
[  105.944928] v4l_qbuf+0x44/0x58
[  105.944951] __video_do_ioctl+0x1a0/0x348
[  105.944972] video_usercopy+0x228/0x740
[  105.944993] video_ioctl2+0x14/0x20
[  105.945013] v4l2_ioctl+0x44/0x68
[  105.945036] v4l2_compat_ioctl32+0x1d0/0x3a48
[  105.945058] __arm64_compat_sys_ioctl+0xbc/0x15b0
[  105.945082] el0_svc_common.constprop.0+0x64/0x178
[  105.945105] el0_svc_compat_handler+0x18/0x20
[  105.945125] el0_svc_compat+0x8/0x34

七、v4l2拓扑结构

关于如何使用设备树节点描述拓扑结构,后续文章会详细讲解。

相关推荐

电子产业图谱

公众号『一口Linux』号主彭老师,拥有15年嵌入式开发经验和培训经验。曾任职ZTE,某研究所,华清远见教学总监。拥有多篇网络协议相关专利和软件著作。精通计算机网络、Linux系统编程、ARM、Linux驱动、龙芯、物联网。原创内容基本从实际项目出发,保持原理+实践风格,适合Linux驱动新手入门和技术进阶。