Camera | 5.Linux v4l2架构(基于rk3568)
上一篇我们讲解了如何编写基于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)。
- 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);
这个函数主要做四件事:
- 检查设备类型,赋予设备名称
- 寻找一个空闲的设备位置,寻找合适的主设备号和次设号
- 初始化字符设备,使用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;#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的关系举例
下面以我们手机的摄像头来举例:
- 假定一款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内含一个cdev
- v4l2_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拓扑结构
关于如何使用设备树节点描述拓扑结构,后续文章会详细讲解。
文中各种mipi技术文档,后台回复关键字:mipi
后面还会继续更新几篇Camera文章,
建议大家订阅本专题!
也可以后台留言,加一口君好友yikoupeng,
拉你进高质量技术交流群。
相关文章:
Camera | 5.Linux v4l2架构(基于rk3568)
上一篇我们讲解了如何编写基于V4L2的应用程序编写,本文主要讲解内核中V4L2架构,以及一些最重要的结构体、注册函数。 厂家在实现自己的摄像头控制器驱动时,总体上都遵循这个架构来实现,但是不同厂家、不同型号的SoC,具…...
机房PDU如何挑选?
PDU PDU(Power Distribution Unit,电源分配单元),也就是我们常说的机柜用电源分配插座,PDU是为机柜式安装的电气设备提供电力分配而设计的产品,拥有不同的功能、安装方式和不同插位组合的多种系列规格,能为不同的电源环境提供适合的机架式电源分配解决方案。PDU的应用,…...
lab备考第二步:HCIE-Cloud-Compute-第一题:FusionCompute
第一题 FusionCompute 一、题目介绍 1.1. 扩容CAN节点与对接共享存储(必选) 题目及【考生提醒关键点】 扩容一台CNA节点,配置管理地址设置为:192.168.100.212。密码设置为:Cloud12#$。【输入之前确认自己的大小写是否…...
js-cookie和vue-cookies(Cookie使用教程)
简述:js-cookie和vue-cookies都是vue项目中的插件,下载相关依赖后,可以用来存储、获取、删除Cookie等操作,思路相同,操作时稍有不同,当然也可以用原生js来获取Cookie; ⭐ js-coo…...
开创高质量发展新局面,优炫数据库助推数字中国建设
最新印发《数字中国建设整体布局规划》,建设数字中国是数字时代推进中国式现代化的重要引擎,是构筑国家竞争新优势的有力支撑。 数字中国建设按照“2522”的整体框架进行布局,即夯实数字基础设施和数据资源体系“两大基础”,推进…...
【项目实战】为什么我选择使用CloseableHttpClient,而不是HttpClient,他们俩有什么区别?
一、HttpClient介绍 HttpClient是Commons HttpClient的老版本,已被抛弃,不推荐使用; HttpClient是一个接口,定义了客户端HTTP协议的操作方法。 它可以用于发送HTTP请求和接收HTTP响应。 HttpClient接口提供了很多方法来定制请求…...
Spark 内存运用
RDD Cache 当同一个 RDD 被引用多次时,就可以考虑进行 Cache,从而提升作业的执行效率 // 用 cache 对 wordCounts 加缓存 wordCounts.cache // cache 后要用 action 才能触发 RDD 内存物化 wordCounts.count// 自定义 Cache 的存储介质、存储形式、副本…...
SpringBoot集成Swagger3.0(入门) 02
文章目录Swagger3常用配置注解接口测试API信息配置Swagger3 Docket开关,过滤,分组Swagger3常用配置注解 ApiImplicitParams,ApiImplicitParam:Swagger3对参数的描述。 参数名参数值name参数名value参数的具体意义,作用。required参…...
网络协议丨ICMP协议
ICMP协议,全称 Internet Control Message Protocol,就是互联网控制报文协议。我们其实对它并不陌生,我们平时经常使用的”ping“一下就是基于这个协议工作的。网络包在异常复杂的网络环境中传输时,常常会遇到各种各样的问题。当遇…...
12.1 基于Django的服务器信息查看应用(系统信息、用户信息)
文章目录新建Django项目创建子应用并设置本地化创建数据库表创建超级用户git管理项目(requirements.txt、README.md、.ignore)主机信息监控应用的框架搭建具体功能实现系统信息展示前端界面设计视图函数设计用户信息展示视图函数设计自定义过滤器的实现前…...
ExSwin-Unet 论文研读
ExSwin-Unet摘要1 引言2 方法2.1 基于窗口的注意力块2.2 外部注意力块2.3 不平衡的 Unet 架构2.4 自适应加权调整2.5 双重损失函数3 实验结果3.1 数据集3.2 实现细节3.3 与 SOTA 方法的比较3.4 消融研究4 讨论和限制5 结论数据集来源: https://feta.grand-challenge…...
置顶!!!主页禁言提示原因:在自己论坛发动态误带敏感词,在自己论坛禁止评论90天
置顶!!!主页禁言提示原因:在自己论坛发动态误带敏感词,在自己论坛禁止评论90天 置顶!!!主页禁言提示原因:在自己论坛发动态误带敏感词,在自己论坛禁止评论90天…...
优思学院|解密六西格玛:探索DMAIC和DMADV之间的区别
六西格玛方法中最为广泛使用的两种方法是DMAIC和DMADV。这两种方法都是为了让企业流程更加高效和有效而设计的。虽然这两种方法有一些重要的共同特点,但它们并不可以互相替代,并且被开发用于不同的企业流程。在更详细地比较这两种方法之前,我…...
Pytorch的DataLoader输入输出(以文本为例)
本文不做太多原理介绍,直讲使用流畅。想看更多底层实现-〉传送门。DataLoader简介torch.utils.data.DataLoader是PyTorch中数据读取的一个重要接口,该接口定义在dataloader.py脚本中,只要是用PyTorch来训练模型基本都会用到该接口。本文介绍t…...
代谢组学:Microbiome又一篇!绘制重症先天性心脏病新生儿肠道微生态全景图谱
文章标题:Mapping the early life gut microbiome in neonates with critical congenital heart disease: multiomics insights and implications for host metabolic and immunological health 发表期刊:Microbiome 影响因子:16.837…...
Java基本类型所占字节简述
类型分类所占字节取值范围boolean布尔型1bit0 false、 1 true (1个bit 、1个字节、4个字节)char 字符型(Unicode字符集中的一个元素) 2字节-32768~32767(-2的15次方~2的15次方-1)byte整型1字节-128&a…...
Linux vi常用操作
vi/vim 共分为三种模式,分别是命令模式(Command mode),输入模式(Insert mode)和底线命令模式(Last line mode)。 这三种模式的作用分别是: 命令模式: 用户刚…...
Unicode(宽字节)、ANSI(多字节)
1、什么时候用Unicode(宽字节),什么时候用ANSI(多字节)? 在linux/windows等操作系统中使用的,一般都是Unicode(宽字节)。 下位机PLC/单片机等硬件设备中使用,一般都是ANSI(多字节)。 所以,通讯中(比如VS项目&#x…...
STM32实战之LED循环点亮
接着上一章讲。本章我们来讲一讲LED流水灯,循环点亮LED。 在LED章节有的可能没有讲到,本章会对其进行说明,尽量每个函数说一下作用。也会在最后说一下STM32的寄存器,在编程中寄存器是避免不了的东西,寄存器也是非常好理…...
智慧厕所智能卫生间系统有哪些功能
南宁北站智能厕所主要功能有哪些?1、卫生间环境空气监测男厕、女厕环境空气监测系统包括对厕所内的温度、湿度、氨气、硫化氢、PM2.5、烟雾等气体数据的实时监测。2、卫生间厕位状态监测系统实时监测厕位内目前的使用状态(有人或无人),数据信…...
多模态2025:技术路线“神仙打架”,视频生成冲上云霄
文|魏琳华 编|王一粟 一场大会,聚集了中国多模态大模型的“半壁江山”。 智源大会2025为期两天的论坛中,汇集了学界、创业公司和大厂等三方的热门选手,关于多模态的集中讨论达到了前所未有的热度。其中,…...
Java多线程实现之Thread类深度解析
Java多线程实现之Thread类深度解析 一、多线程基础概念1.1 什么是线程1.2 多线程的优势1.3 Java多线程模型 二、Thread类的基本结构与构造函数2.1 Thread类的继承关系2.2 构造函数 三、创建和启动线程3.1 继承Thread类创建线程3.2 实现Runnable接口创建线程 四、Thread类的核心…...
Device Mapper 机制
Device Mapper 机制详解 Device Mapper(简称 DM)是 Linux 内核中的一套通用块设备映射框架,为 LVM、加密磁盘、RAID 等提供底层支持。本文将详细介绍 Device Mapper 的原理、实现、内核配置、常用工具、操作测试流程,并配以详细的…...
LINUX 69 FTP 客服管理系统 man 5 /etc/vsftpd/vsftpd.conf
FTP 客服管理系统 实现kefu123登录,不允许匿名访问,kefu只能访问/data/kefu目录,不能查看其他目录 创建账号密码 useradd kefu echo 123|passwd -stdin kefu [rootcode caozx26420]# echo 123|passwd --stdin kefu 更改用户 kefu 的密码…...
Go 语言并发编程基础:无缓冲与有缓冲通道
在上一章节中,我们了解了 Channel 的基本用法。本章将重点分析 Go 中通道的两种类型 —— 无缓冲通道与有缓冲通道,它们在并发编程中各具特点和应用场景。 一、通道的基本分类 类型定义形式特点无缓冲通道make(chan T)发送和接收都必须准备好࿰…...
AI+无人机如何守护濒危物种?YOLOv8实现95%精准识别
【导读】 野生动物监测在理解和保护生态系统中发挥着至关重要的作用。然而,传统的野生动物观察方法往往耗时耗力、成本高昂且范围有限。无人机的出现为野生动物监测提供了有前景的替代方案,能够实现大范围覆盖并远程采集数据。尽管具备这些优势…...
探索Selenium:自动化测试的神奇钥匙
目录 一、Selenium 是什么1.1 定义与概念1.2 发展历程1.3 功能概述 二、Selenium 工作原理剖析2.1 架构组成2.2 工作流程2.3 通信机制 三、Selenium 的优势3.1 跨浏览器与平台支持3.2 丰富的语言支持3.3 强大的社区支持 四、Selenium 的应用场景4.1 Web 应用自动化测试4.2 数据…...
从面试角度回答Android中ContentProvider启动原理
Android中ContentProvider原理的面试角度解析,分为已启动和未启动两种场景: 一、ContentProvider已启动的情况 1. 核心流程 触发条件:当其他组件(如Activity、Service)通过ContentR…...
学习一下用鸿蒙DevEco Studio HarmonyOS5实现百度地图
在鸿蒙(HarmonyOS5)中集成百度地图,可以通过以下步骤和技术方案实现。结合鸿蒙的分布式能力和百度地图的API,可以构建跨设备的定位、导航和地图展示功能。 1. 鸿蒙环境准备 开发工具:下载安装 De…...
Easy Excel
Easy Excel 一、依赖引入二、基本使用1. 定义实体类(导入/导出共用)2. 写 Excel3. 读 Excel 三、常用注解说明(完整列表)四、进阶:自定义转换器(Converter) 其它自定义转换器没生效 Easy Excel在…...
