上一篇我们讲解了如何编写基于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架构包括哪些设备

  1. Video设备又分为主设备和从设备对于Camera来说,
    主设备:
    Camera Host控制器为主设备,负责图像数据的接收和传输,
    从设备:
    从设备为Camera Sensor,一般为I2C接口,可通过从设备控制Camera采集图像的行为,如图像的大小、图像的FPS等。

  2. 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库来操作摄像头
    也可以基于字符设备/dev/videoX自己编写应用程序
    guvcview:用于调试usb摄像头(还有个软件cheese也可以)
    v4l2 utilities: v4l2 的工具集(参考前面第3篇文章)

  • kernel space:
    sensor、ISP、VIPP、CSI、CCI都为从设备
    从dphy物理层获取视频数据册通过vb2子模块
    CCI :主要是通过GPIO(供电、片选)、I2C(下发配置命令给sensor)实现配置sensor
    EHCI/OHCI:USB类型摄像头

  • hardware
    CSIC 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 IDchar 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_220718\kernel\drivers\media\v4l2-core\v4l2-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;//次设备个数默认为256const char *name_base;/* A minor value of -1 marks this video device as neverhaving 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 number\n");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, sowe 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 minor\n");return -ENFILE;}
#endifvdev->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,名称为videoXdev_set_name(&vdev->dev, "%s%d", name_base, vdev->num);//注册device文件,生成设备文件/dev/videoXret = device_register(&vdev->dev);/* Register the release callback that will be called when the lastreference to the device goes away. *///设置销毁video设备的回调函数vdev->dev.release = v4l2_device_release;/* Increase v4l2_device refcount */v4l2_device_get(vdev->v4l2_dev);

这个函数主要做四件事:

  1. 检查设备类型,赋予设备名称
  2. 寻找一个空闲的设备位置,寻找合适的主设备号和次设号
  3. 初始化字符设备,使用v4l2_device的v4l2_fops初始化video_device的fops,release函数等
  4. 注册字符设备,并生成/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;#endifstruct 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的设备结构体devvoid *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_subdevstatic 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模块

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

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

如下图所示:

我们可以选择让控制器去操作哪一个摄像头(可以使用某个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

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

  1. video_device是一个字符设备,video_device内含一个cdev
  2. v4l2_device是一个v4l2实例,嵌入到video_device中
  3. v4l2_device维护者一个链表管理v4l2_subdev,v4l2_subdev表示摄像头的I2C控制模块
  4. 主设备可通过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拓扑结构

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

文中各种mipi技术文档,后台回复关键字:mipi

后面还会继续更新几篇Camera文章,

建议大家订阅本专题!

也可以后台留言,加一口君好友yikoupeng,

拉你进高质量技术交流群。

Camera | 5.Linux v4l2架构(基于rk3568)相关推荐

  1. Linux v4l2架构学习

    写在开始之前. 网上有很多文章讲v4l2架构,讲的都很好,但是很多都是讲讲主要的结构体已经注册接口这些,个人觉得入门还是要深入的去看代码,至少把整个流程粗略的看一遍,才能真正的了解架构是什么样的,调用 ...

  2. linux v4l2架构分析之v4l2_ctrl_handler初始化及添加v4l2_ctrl的过程分析

    本文根据原代码分析v4l2的handler初始化以及添加ctrl的过程,会涉及v4l2_ctrl_handler.v4l2_ctrl.v4l2_ctrl_ref结构体的分析,以及介绍v4l2_ctrl ...

  3. v4l2架构专题模块handler分析 -- handler ctrl的注册2

    Linux v4l2架构学习总链接 上一篇文章中忽略了ctrl class,这里补上 static int handler_new_ref(struct v4l2_ctrl_handler *hdl, ...

  4. v4l2架构专题模块handler分析 --- handler的初始化及handler ctrl注册

    Linux v4l2架构学习总链接 handler初始化代码调用如下: v4l2_ctrl_handler_init(handler, 9); 对应源码: #define v4l2_ctrl_hand ...

  5. linux v4l2 示例程序,linux驱动由浅入深系列:camera驱动之二(基于高通平台的V4L2结构及代码分析)...

    在上一篇文章中介绍了camera的基础知识和相关概念,我们一起来了解一下驱动相关的代码结构.本文以高通+android平台为示例,首先看一下整体框图: 这张图是从整体上来看的 1,图中最下面的是ker ...

  6. linux驱动由浅入深系列:camera驱动之二(基于高通平台的V4L2结构及代码分析)

    在上一篇文章中介绍了camera的基础知识和相关概念,我们一起来了解一下驱动相关的代码结构.本文以高通+android平台为示例,首先看一下整体框图: 这张图是从整体上来看的 1,图中最下面的是ker ...

  7. linux驱动由浅入深系列:camera驱动之二(基于高通平台的V4L2结构及代码分析

    点击打开链接 在上一篇文章中介绍了camera的基础知识和相关概念,我们一起来了解一下驱动相关的代码结构.本文以高通+android平台为示例,首先看一下整体框图: 这张图是从整体上来看的 1,图中最 ...

  8. 图像处理自学(五):CAMERA驱动软件硬件架构V4L2

    一.CAMERA驱动框架V4L2 芯片模块对应Soc的各个子模块,video_device结构体主要用来控制Soc的video模块,v4l2_device会包含多个v4l2_subdev ,每个v4l ...

  9. 基于Ti Omap3x 分析v4l2架构

    1 概述 本文将基于Ti Omap3x这个典型的实例来分析v4l2在具体media场景中的应用.通过分析app层的行为以及driver层的实现来对整个多媒体框架有一个大概的认识.内容主要包括主要包括v ...

最新文章

  1. sscanf用法简介
  2. Applese 的取石子游戏
  3. 7.Android的学习(活动的生命周期以及小例子)
  4. 45 | 答疑(五):网络收发过程中,缓冲区位置在哪里?
  5. 工作329:uni-数据为空不显示
  6. bootstrap 兼容哪些浏览器
  7. Ubuntu Server 16.04 安装 Redis 3.2.0
  8. mobi格式电子书_这几种电子书格式的关系与区别,资深Kindler有必要了解了解 !...
  9. OPENSSL introduce itself from baidu
  10. 高程计算 高程计算 高程计算
  11. 淘宝 NPM 镜像解决软件下载速度慢的问题
  12. 新浪vip邮箱 服务器,新浪手机邮箱
  13. html刮刮乐百分比,jQuery+html5实现彩票刮刮乐效果
  14. 染发染膏的认识与使用
  15. pandas使用to_feather函数将dataframe保存为feather文件(需要依赖pyarrow包)提升大文件读取效率、pandas使用read_feather函数读取feather文件
  16. 景观智慧路灯:基于景观智慧灯杆的浙江嘉兴智慧旅游景区项目分享
  17. 【无人机 学习笔记 1】无人机导航技术及其特点分析
  18. SQLServer表关联查询
  19. Android原生OS风格ROM包,ZUK Z1 魔趣OS 安卓9 MagiskV21版 完美ROOT 纯净完美 原生极简 纯净推荐...
  20. MySql系列之mysql查询执行过程(附Mysql架构图及实操解析)

热门文章

  1. Python基础——继承、多态
  2. CST(CST Studio Suite)建立工程和基本绘图方法
  3. Excel 基础的操作
  4. Linux命令-查看用户的UID和GID
  5. i9 12900hk参数 i9 12900HK功耗
  6. python开发web服务器——搭建简易网站
  7. 通达信量化接口主力进出指标分析
  8. spring文件上传拦截器及异常处理
  9. 使用PicGo配置七牛云图床(图文步骤详细)
  10. C语言程序设计李,C语言程序设计4--李2012.ppt