Camera | 5.Linux v4l2架构(基于rk3568)
Posted 一口Linux
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 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_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;//次设备个数默认为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 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, 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 minor\\n");
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结构体提供主设备
以上是关于Camera | 5.Linux v4l2架构(基于rk3568)的主要内容,如果未能解决你的问题,请参考以下文章