UVC 设备框架在 Linux 4.15 内核的演变

1. 概述

发现之前的uvc框架和现在的还是有一些差别的(比如从videobuf 过渡到videobuf2),写个blog记录一下,方便以后查找,内核版本:Linux 4.15

  • UVC(USB Video Class)设备框架是建立在V4L2(Video4Linux version 2)子系统之上的。UVC框架主要负责管理通过USB接口连接的视频捕获设备,我们会调用V4L2 的一些通用的管理机制(比如缓冲区管理以及排队机制,后面有所体现),对于一些特定的在UVC框架下进行单独的编写

2. 流程分析

  • 打开设备文件 : 应用程序通过文件I/O打开设备时,内核创建一个uvc_fh 文件句柄实例,将其关联到特定的视频流
  • 缓冲区队列操作:uvc_fh通过它的视频流uvc_streaming 结构体间接操作vb2_queue ,以执行如缓冲区排队(qbuf),和缓冲区准备(reqbufs)等操作
  • 数据流传输: 视频数据通过 vb2_buffer 结构体在用户空间和UVC硬件之间传输

3. 主要的内核结构体

图1
可以看出当前版本下的UVC驱动和V4L2之间的交互非常密切。V4L2是Linux内核中用于处理视频捕获和输出设备的一个标准API框架,而UVC驱动则是V4L2框架下用于支持符合USB视频类规范的摄像头和其他视频设备的一个具体实现

3.1 UVC驱动和V4L2之间主要的交互方式:

  1. 设备注册和初始化
    UVC驱动在系统启动或USB视频设备插入时,会被初始化并注册为V4L2设备。这一过程包括设置设备的V4L2能力、支持的格式、控制操作等,确保UVC设备能够通过V4L2接口与用户空间应用程序交互。
  2. 控制查询和设置
    UVC驱动实现了一系列V4L2控制类(control class)接口,允许用户空间应用程序查询和设置视频设备的参数,如亮度、对比度、饱和度等。这些控制操作通过UVC驱动转换为USB传输,与硬件设备交互。
  3. 缓冲区管理
    UVC驱动使用V4L2提供的videobuf2 API来管理视频帧的缓冲区。这包括缓冲区的分配、队列管理、数据传输等。Videobuf2作为V4L2的一部分,提供了一个高效的机制来处理视频数据的缓冲和流转。
  4. 数据流控制
    用户空间应用程序可以通过V4L2接口来启动和停止视频流。UVC驱动响应这些请求,通过USB接口与硬件设备进行交互,控制视频数据的捕获和传输。
  5. 事件处理
    UVC驱动能够处理来自硬件的事件,比如状态变化、错误报告等,并通过V4L2框架将这些事件上报给用户空间应用程序,使得应用程序能够对特定的硬件事件做出响应。
  6. 格式协商
    在视频捕获或输出过程中,UVC驱动和用户空间应用程序会通过V4L2接口进行格式协商,确定视频数据的格式、分辨率、帧率等参数。UVC驱动根据这些协商结果配置USB视频设备,以满足应用程序的需求。

4. UVC_Driver 驱动入口出口函数

static int __init uvc_init(void)
{
...
	ret = usb_register(&uvc_driver.driver);
	return 0;
...
}

static void __exit uvc_cleanup(void)
{
	usb_deregister(&uvc_driver.driver);
}
module_init(uvc_init);
module_exit(uvc_cleanup);

接下里就是关于uvc_driver 结构体

struct uvc_driver uvc_driver = {
	.driver = {
		.name		= "uvcvideo",
		.probe		= uvc_probe,
		.disconnect	= uvc_disconnect,
		.suspend	= uvc_suspend,
		.resume		= uvc_resume,
		.reset_resume	= uvc_reset_resume,
		.id_table	= uvc_ids,
		.supports_autosuspend = 1,
	},
};

4.1 uvc_probe

这里主要是分配 设置 设置结构体,以及一些其他的操作

4.1.1 分配 uvc_device

if ((dev = kzalloc(sizeof *dev, GFP_KERNEL)) == NULL)

	INIT_LIST_HEAD(&dev->entities); 
	INIT_LIST_HEAD(&dev->chains);  
	INIT_LIST_HEAD(&dev->streams); 
  • entities: 指的也就是摄像头、输入终端(ITT)、输出终端(OTT)、处理单元(PU)和扩展单元(XU)等。每个实体代表设备的一个功能部分,可能是视频捕获的源头(例如摄像头),或是对视频数据进行处理的模块(例如编码器)。
  • chains: 视频链是由一系列实体(entities)连接而成的路径,从视频捕获的源头开始,经过一系列处理,最终到达输出。每个视频链代表了一种特定的视频流处理流程
  • streams :视频流可以理解为通过一个特定视频链(chains)实现的数据流,包含实际的视频帧数据
    在这里插入图片描述

解析设备接口描述符 并将信息填充到 uvc_device *dev; 以便后面使用

uvc_parse_control(dev)

4.1.2 分配 uvc_device注册 video 设备节点

  1. 注册uvc设备到V4L2框架下, 将V4L2设备结构体 v4l2_device 与内核结构体device 关联起来使其成为一个可以由用户空间访问和控制的设备
if (v4l2_device_register(&intf->dev, &dev->vdev) < 0)
		goto error;
  1. 初始化uvc设备的控制项
if (uvc_ctrl_init_device(dev) < 0)
		goto error;
/* 
uvc_ctrl_init_device 主要就是
1.. 遍历所有与设备管理的实体(比如处理单元PU,控制单元CU等),并且为每
个实体分配了一个 struct uvc_control *ctrl 的结构体
3. 对于每个支持的控制项,函数设置控制的索引,并调用
 uvc_ctrl_init_ctrl 来进一步初始化控制项。
*/

  1. 扫描设备实体(entities) 并注册成 视频链 (chains)
if (uvc_scan_device(dev) < 0)
		goto error;
/* 1.遍历所有实体 从输出端OT 反向扫描
   2.为没用分配到的实体分配视频链结构体
   3.初始化视频链:
	4.设置默认标志:
*/
  1. 为识别到的视频链注册到video_device 设备节点
uvc_register_chains(dev)	
	uvc_register_terms(dev, chain);
		uvc_register_video(dev, stream);//根据传输终端的 ID 查找视频流并将其注册为设备节点 dev/video x 

接下来分析一下uvc_register_video 这里面的函数

a. 首先对uvc_queue 进行初始化, 设置它的队列的类型,以及是否丢帧行为 (uvc_no_drop_param),同时将uvc_queue 和V4L2下的 vb2 操作集uvc_queue_qops , vb2_vmalloc_memops 联系起来

	ret = uvc_queue_init(&stream->queue, stream->type, !uvc_no_drop_param);
	if (ret)
		return ret;
int uvc_queue_init(struct uvc_video_queue *queue, enum v4l2_buf_type type,int drop_corrupted)
{
queue->queue.type = type;
	/*VB2_MMAP 内存映射 ,VB2_USERPTR 用户指针, VB2_DMABUF DMA缓冲区*/
	queue->queue.io_modes = VB2_MMAP | VB2_USERPTR | VB2_DMABUF;
	queue->queue.drv_priv = queue;
	queue->queue.buf_struct_size = sizeof(struct uvc_buffer);
	queue->queue.ops = &uvc_queue_qops;
	queue->queue.mem_ops = &vb2_vmalloc_memops;
	queue->queue.timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC
		| V4L2_BUF_FLAG_TSTAMP_SRC_SOE;
	queue->queue.lock = &queue->mutex;
	/*在调用vb2_queue_init*/
	ret = vb2_queue_init(&queue->queue);
}

这种设计允许 UVC 驱动以及其他基于 vb2 框架的驱动灵活地实现特定的缓冲区处理逻辑,同时又能享受 vb2 框架提供的通用缓冲区管理和排队机制的好处。这样,我们可以专注于实现与设备通信的部分,而不必从头开始编写缓冲区管理的代码

在这里插入图片描述

b .初始化视频流 :确保视频流在开始传输数据之前已经正确配置,包括分辨率、帧率等参数

ret = uvc_video_init(stream);
	if (ret < 0) {
		uvc_printk(KERN_ERR, "Failed to initialize the device "
			"(%d).\n", ret);
		return ret;
	}

c. 注册视频设备

	/*设置相关的参数*/
	vdev->v4l2_dev = &dev->vdev;
	vdev->fops = &uvc_fops;
	vdev->ioctl_ops = &uvc_ioctl_ops;
	vdev->release = uvc_release;
	vdev->prio = &stream->chain->prio;
	if (stream->type == V4L2_BUF_TYPE_VIDEO_OUTPUT)
		vdev->vfl_dir = VFL_DIR_TX;
	strlcpy(vdev->name, dev->name, sizeof vdev->name);

//设置视频设备的私有数据,确保在设备操作中可以访问到关联的 uvc_streaming 结构
	video_set_drvdata(vdev, stream);
	ret = video_register_device(vdev, VFL_TYPE_GRABBER, -1);
	if (ret < 0) {
		return ret;
	}

c. 更新视频流和链的能力

	if (stream->type == V4L2_BUF_TYPE_VIDEO_CAPTURE)
		stream->chain->caps |= V4L2_CAP_VIDEO_CAPTURE;
	else
		stream->chain->caps |= V4L2_CAP_VIDEO_OUTPUT;

	atomic_inc(&dev->nstreams); /*更新活跃视频流的计数*/
  1. 将数据指针保存在数据接口中
usb_set_intfdata(intf, dev);
  1. 初始化中断 URB。
if ((ret = uvc_status_init(dev)) < 0) {
		uvc_printk(KERN_INFO, "Unable to initialize the status "
			"endpoint (%d), status interrupt will not be "
			"supported.\n", ret);
	}

5. UVC驱动的调用过程详解

UVC驱动通过一系列文件操作和IO控制操作(ioctl)来管理视频捕获和控制流程

5.1 核心操作集合

uvc驱动的调用过程主要是涉及之前注册时候设置的

    vdev->fops = &uvc_fops; //文件操作
	vdev->ioctl_ops = &uvc_ioctl_ops; //ioctl操作

文件操作(uvc_fops)

const struct v4l2_file_operations uvc_fops = {
	.owner		= THIS_MODULE,
	.open		= uvc_v4l2_open,
	.release	= uvc_v4l2_release,
	.unlocked_ioctl	= video_ioctl2,
#ifdef CONFIG_COMPAT
	.compat_ioctl32	= uvc_v4l2_compat_ioctl32,
#endif
	.read		= uvc_v4l2_read,
	.mmap		= uvc_v4l2_mmap,
	.poll		= uvc_v4l2_poll,
#ifndef CONFIG_MMU
	.get_unmapped_area = uvc_v4l2_get_unmapped_area,
#endif
};

IOCTL操作(uvc_ioctl_ops)

static const struct v4l2_ioctl_ops uvc_ioctl_ops = {
    /* 基本设备操作 */
    .vidioc_querycap = uvc_ioctl_querycap,
    .vidioc_enum_fmt_vid_cap = uvc_ioctl_enum_fmt_vid_cap,
    .vidioc_g_fmt_vid_cap = uvc_ioctl_g_fmt_vid_cap,
    .vidioc_s_fmt_vid_cap = uvc_ioctl_s_fmt_vid_cap,
    .vidioc_try_fmt_vid_cap = uvc_ioctl_try_fmt_vid_cap,

    /* 缓冲区操作 */
    .vidioc_reqbufs = uvc_ioctl_reqbufs,
    .vidioc_querybuf = uvc_ioctl_querybuf,
    .vidioc_qbuf = uvc_ioctl_qbuf,
    .vidioc_dqbuf = uvc_ioctl_dqbuf,

    /* 流操作 */
    .vidioc_streamon = uvc_ioctl_streamon,
    .vidioc_streamoff = uvc_ioctl_streamoff,

    /* 输出格式操作(如果设备支持输出) */
    .vidioc_enum_fmt_vid_out = uvc_ioctl_enum_fmt_vid_out,
    .vidioc_g_fmt_vid_out = uvc_ioctl_g_fmt_vid_out,
    .vidioc_s_fmt_vid_out = uvc_ioctl_s_fmt_vid_out,
    .vidioc_try_fmt_vid_out = uvc_ioctl_try_fmt_vid_out,
    .vidioc_create_bufs = uvc_ioctl_create_bufs,

    /* 输入源操作 */
    .vidioc_enum_input = uvc_ioctl_enum_input,
    .vidioc_g_input = uvc_ioctl_g_input,
    .vidioc_s_input = uvc_ioctl_s_input,

    /* 控制操作 */
    .vidioc_queryctrl = uvc_ioctl_queryctrl,
    .vidioc_query_ext_ctrl = uvc_ioctl_query_ext_ctrl,
    .vidioc_g_ctrl = uvc_ioctl_g_ctrl,
    .vidioc_s_ctrl = uvc_ioctl_s_ctrl,
    .vidioc_g_ext_ctrls = uvc_ioctl_g_ext_ctrls,
    .vidioc_s_ext_ctrls = uvc_ioctl_s_ext_ctrls,
    .vidioc_try_ext_ctrls = uvc_ioctl_try_ext_ctrls,
    .vidioc_querymenu = uvc_ioctl_querymenu,
    .vidioc_g_selection = uvc_ioctl_g_selection,

    /* 帧率和分辨率操作 */
    .vidioc_g_parm = uvc_ioctl_g_parm,
    .vidioc_s_parm = uvc_ioctl_s_parm,
    .vidioc_enum_framesizes = uvc_ioctl_enum_framesizes,
    .vidioc_enum_frameintervals = uvc_ioctl_enum_frameintervals,

    /* 事件订阅 */
    .vidioc_subscribe_event = uvc_ioctl_subscribe_event,
    .vidioc_unsubscribe_event = v4l2_event_unsubscribe,

    /* 默认操作 */
    .vidioc_default = uvc_ioctl_default,
};


5.2 主要的调用过程

5.2.1 open

内核创建一个uvc_fh 文件句柄实例,将其关联到特定的视频流,以及初始化V4L2文件句柄

static int uvc_v4l2_open(struct file *file)

5.2.2 VIDIOC_QUERYCAP 查询设备的功能

static int uvc_ioctl_querycap(struct file *file, void *fh,
			      struct v4l2_capability *cap)
{
...
	usb_make_path(stream->dev->udev, cap->bus_info, sizeof(cap->bus_info));
	cap->capabilities = V4L2_CAP_DEVICE_CAPS | V4L2_CAP_STREAMING
			  | chain->caps;
	if (stream->type == V4L2_BUF_TYPE_VIDEO_CAPTURE)
		cap->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING;
	else
		cap->device_caps = V4L2_CAP_VIDEO_OUTPUT | V4L2_CAP_STREAMING;

	return 0;
}

5.2.3 VIDIOC_ENUM_FMT 枚举设备支持的格式

枚举设备支持的格式 ,比如MJPEG或H264等 填充到v4l2_fmtdesc *fmt

static int uvc_ioctl_enum_fmt(struct uvc_streaming *stream,
			      struct v4l2_fmtdesc *fmt)
{
	struct uvc_format *format;
	enum v4l2_buf_type type = fmt->type;
	__u32 index = fmt->index;

	if (fmt->type != stream->type || fmt->index >= stream->nformats)
		return -EINVAL;

	memset(fmt, 0, sizeof(*fmt)); //将fmt结构内存清0 
	fmt->index = index;
	fmt->type = type;

	format = &stream->format[fmt->index];
	fmt->flags = 0;
	if (format->flags & UVC_FMT_FLAG_COMPRESSED)
		fmt->flags |= V4L2_FMT_FLAG_COMPRESSED;
	strlcpy(fmt->description, format->name, sizeof(fmt->description));
	fmt->description[sizeof(fmt->description) - 1] = 0;
	fmt->pixelformat = format->fcc;
	return 0;
}

5.2.4 VIDIOC_G_FMT 得到当前的支持的格式 比如分辨率等

int uvc_ioctl_g_fmt_vid_cap(struct file *file, void *fh, struct v4l2_format *fmt)
--------->	uvc_v4l2_get_format(stream, fmt); /*跳转到v4l2框架下的函数来进行*/
static int uvc_v4l2_get_format(struct uvc_streaming *stream,
	struct v4l2_format *fmt)
{
...
	fmt->fmt.pix.pixelformat = format->fcc;
	fmt->fmt.pix.width = frame->wWidth;
	fmt->fmt.pix.height = frame->wHeight;
	fmt->fmt.pix.field = V4L2_FIELD_NONE; //逐行扫描
	fmt->fmt.pix.bytesperline = format->bpp * frame->wWidth / 8;
	fmt->fmt.pix.sizeimage = stream->ctrl.dwMaxVideoFrameSize;
	fmt->fmt.pix.colorspace = format->colorspace;//色彩空间
	fmt->fmt.pix.priv = 0;
	return ret;
...
}

5.2.5 VIDIOC_S_FMT 更新UVC视频流的当前格式和帧大小 以匹配应用程序的请求配置

static int uvc_ioctl_s_fmt_vid_cap(struct file *file, void *fh, struct v4l2_format *fmt)
-------> uvc_v4l2_set_format(stream, fmt);
/*
struct uvc_streaming *stream: 视频流对象的指针。
struct v4l2_format *fmt: 应用程序请求的视频格式信息。
*/
static int uvc_v4l2_set_format(struct uvc_streaming *stream,struct v4l2_format *fmt)
{
	/*尝试匹配和调整应用请求的格式*/
	ret = uvc_v4l2_try_format(stream, fmt, &probe, &format, &frame);

	
	mutex_lock(&stream->mutex);
	
	if (uvc_queue_allocated(&stream->queue)) {
		/*如果视频流帧缓冲区已经分配,此时不能修改格式*/
		ret = -EBUSY;
	}
}

5.2.6 VIDIOC_TRY_FMT 检查格式是否匹配

5.2.3.1 视频缓冲区的 内存类型有
enum v4l2_memory {
	V4L2_MEMORY_MMAP             = 1, /*内存映射到用户空间*/
	V4L2_MEMORY_USERPTR          = 2,/*用户指针*/
	V4L2_MEMORY_OVERLAY          = 3,/*覆盖方式 不需要额外分配内存 视频数据直接覆盖到另外一个图像上*/
	V4L2_MEMORY_DMABUF           = 4,/*DMA缓存方式 无需经过cpu*/
};
5.2.3.2 枚举视频缓冲区的状态有:
enum vb2_buffer_state {
	VB2_BUF_STATE_DEQUEUED,  // 未入队
	VB2_BUF_STATE_PREPARING,// 正在准备buffer
	VB2_BUF_STATE_PREPARED, //准备好了
	VB2_BUF_STATE_QUEUED, //入队
	VB2_BUF_STATE_ACTIVE, //激活状态 正在传输数据或者其他操作
	VB2_BUF_STATE_DONE,  //完成任务 等待进一步处理
	VB2_BUF_STATE_ERROR, //错误
};
5.2.3.4 视频缓冲区的类型有:
enum v4l2_buf_type {
	V4L2_BUF_TYPE_VIDEO_CAPTURE        = 1, 
	/*表示视频捕获缓冲区。用于从视频输入设备(如摄像头)捕获视频数据。*/
	V4L2_BUF_TYPE_VIDEO_OUTPUT         = 2,//表示视频输出缓冲区
	V4L2_BUF_TYPE_VIDEO_OVERLAY        = 3,//表示视频叠加缓冲区
	V4L2_BUF_TYPE_VBI_CAPTURE          = 4,//表示VBI(垂直消隐间隔)捕获缓冲区
	V4L2_BUF_TYPE_VBI_OUTPUT           = 5,//表示VBI输出缓冲区
	V4L2_BUF_TYPE_SLICED_VBI_CAPTURE   = 6,//表示切片VBI捕获缓冲区
	V4L2_BUF_TYPE_SLICED_VBI_OUTPUT    = 7,//表示切片VBI输出缓冲区
#if 1
	/* Experimental */
	V4L2_BUF_TYPE_VIDEO_OUTPUT_OVERLAY = 8,//表示视频输出叠加缓冲区
#endif
	V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE = 9,//表示多平面视频捕获缓冲区
	V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE  = 10,//表示多平面视频输出缓冲区
	V4L2_BUF_TYPE_SDR_CAPTURE          = 11,//表示SDR(软件定义无线电)捕获缓冲区
	/* Deprecated, do not use */
	V4L2_BUF_TYPE_PRIVATE              = 0x80,//表示私有类型的视频缓冲区
};

调用关系

static int uvc_ioctl_reqbufs(struct file *file, void *fh, struct v4l2_requestbuffers *rb)
-------> uvc_request_buffers(&stream->queue, rb);
--------->vb2_reqbufs(&queue->queue, rb); //跳转到video_buf2
-----------> __reqbufs(q, req)
--------------> __vb2_queue_alloc(q, req->memory, num_buffers, num_planes)

------------------>__vb2_buf_mem_alloc (对于MMAP类型的内存)--->vb2_vmalloc_alloc()

5.2.3.5 __vb2_queue_alloc -----分配videobuf buffer 结构体

这里主要是根据我们所需要的缓冲区的数量以及平面数量分配并初始化每个缓冲区,对于给定类型 V4L2_MEMORY_MMAP 类型的内存,还需要再进程一次额外的内存分配(V4L2_MEMORY_MMAP 指的是通过内存映射的方式访问缓冲区,允许用户空间应用程序 通过映射到用户空间的地址 来直接对视频数据进行访问,从而避免了复制数据到用户空间的内存开销)

  • 首先 为vb2_buffer 结构体本身分配内存,这是通用的缓冲区描述信息,适用于所有的内存
vb = kzalloc(q->buf_struct_size, GFP_KERNEL);

kzalloc 分配的内存位于内核空间,并且由于它是连续的物理内存,适用于需要直接物理地址的场合,如DMA操作。释放使用 kzalloc 分配的内存时,应使用 kfree 函数。

  • 接下来 如果内存类型是MMAP (V4L2_MEMORY_MMAP) 则调用__vb2_buf_mem_alloc 这个函数 为这个缓冲区分配可被映射的连续物理内存
if (memory == V4L2_MEMORY_MMAP)
		 {
			ret = __vb2_buf_mem_alloc(vb);
			/*缓冲区初始化回调*/
			ret = call_vb_qop(vb, buf_init, vb);
			}

接下来在来看看 __vb2_buf_mem_alloc 这个函数 ,它主要是为给定的 MMap 类型的vb2_buffer 分配足够的内存,以存储视频帧数据

/* 
struct vb2_buffer *vb 是指向一个视频缓冲区的指针。代表了视频数据的一个容器,可以是单平面的(整个帧在一个连续的内存块中)或多平面的(帧的不同部分,比如YUV分量,分布在不同的内存块中)。
*/
static int __vb2_buf_mem_alloc(struct vb2_buffer *vb)
{
	struct vb2_queue *q = vb->vb2_queue;
	/*确定DMA方向
	 * DMA_TO_DEVICE  从内存到设备  输出队列
	 * DMA_FROM_DEVICE  输入队列
	 */
	enum dma_data_direction dma_dir =
		V4L2_TYPE_IS_OUTPUT(q->type) ? DMA_TO_DEVICE : DMA_FROM_DEVICE;
	void *mem_priv;
	int plane;
	
	/*遍历所有平面*/
	for (plane = 0; plane < vb->num_planes; ++plane) {
		unsigned long size = PAGE_ALIGN(q->plane_sizes[plane]);/*对平面大小进行对其*/
		/*使用缓冲区提供的内存分配器call_ptr_memop*/
		mem_priv = call_ptr_memop(vb, alloc, q->alloc_ctx[plane],
				      size, dma_dir, q->gfp_flags);
		if (IS_ERR_OR_NULL(mem_priv))
			goto free;

		/*设置平面属性*/
		vb->planes[plane].mem_priv = mem_priv;
		vb->v4l2_planes[plane].length = q->plane_sizes[plane];
	}

	return 0;
free:
	/* Free already allocated memory if one of the allocations failed */
	for (; plane > 0; --plane) {
		call_void_memop(vb, put, vb->planes[plane - 1].mem_priv);
		vb->planes[plane - 1].mem_priv = NULL;
	}

	return -ENOMEM;
}

这里的 **mem_priv = call_ptr_memop(vb, alloc, q->alloc_ctx[plane],size, dma_dir, q->gfp_flags);**调用了vb2_queue->mem_ops操作集下的 alloc 操作,并将 q->alloc_ctx[plane],size, dma_dir, q->gfp_flags 作为参数传递进去

在代码中搜索 vb2_mem_ops 找到对应的代码

从代码中可以看出,它有两个分配步骤:

  • kzalloc 分配 vb2_vmalloc_buf 结构本身的内存 这个结构体包含了缓冲区的管理信息
buf = kzalloc(sizeof(*buf), GFP_KERNEL | gfp_flags);
  • vmalloc_user 分配的实际缓冲区内容,能够存储视频帧的数据的内存 这部分的内存虚拟连续,物理可能不连续,适合大块的内存分配
buf->vaddr = vmalloc_user(buf->size);
/*vmalloc_user 分配的内存通过 buf->vaddr 字段进行访问*/

完整的代码:

static void *vb2_vmalloc_alloc(void *alloc_ctx, unsigned long size,
			       enum dma_data_direction dma_dir, gfp_t gfp_flags)
{
	struct vb2_vmalloc_buf *buf;

	buf = kzalloc(sizeof(*buf), GFP_KERNEL | gfp_flags);
	if (!buf)
		return NULL;

	buf->size = size;
	buf->vaddr = vmalloc_user(buf->size);
	buf->dma_dir = dma_dir;
	buf->handler.refcount = &buf->refcount;
	buf->handler.put = vb2_vmalloc_put;
	buf->handler.arg = buf;

	if (!buf->vaddr) {
		pr_debug("vmalloc of size %ld failed\n", buf->size);
		kfree(buf);
		return NULL;
	}

	atomic_inc(&buf->refcount);
	return buf;
}

5.2.4 VIDIOC_QUERYBUF 获取buffer信息 以及根据用户设置buffer

static int uvc_ioctl_querybuf(struct file *file, void *fh, struct v4l2_buffer *buf)
-------> uvc_query_buffer(&stream->queue, buf)
--------->vb2_querybuf(&queue->queue, buf);
int vb2_querybuf(struct vb2_queue *q, struct v4l2_buffer *b)
{
	struct vb2_buffer *vb;
	int ret;
	vb = q->bufs[b->index];/*获得buffer结构体指针*/
	
	ret = __verify_planes_array(vb, b);/*验证*/
	if (!ret)
		__fill_v4l2_buffer(vb, b);/*根据用户需求填充buffer信息*/
	return ret;
}
EXPORT_SYMBOL(vb2_querybuf);

5.2.5 VIDIOC_DBUF 将buffer加入到队列之中

这里会涉及到vb2_buffer 框架下的两条链表 以及uvc的链表 大概的框架如下:
在这里插入图片描述

  • queued_entry:这个链表用于管理已经被应用程序通过 VIDIOC_QBUF ioctl 排队,但还未被设备驱动程序处理的缓冲区。当缓冲区通过 vb2_qbuf() 函数被排队时,它被添加到 vb2_queue 结构体的 queued_list 链表中,此时使用 queued_entry 作为链表节点。
    在流启动 (streamon) 后,如果驱动程序已经准备好接收数据,这些缓冲区会从 queued_list 链表中移除,并通过设备驱动程序特定的回调函数(如 UVC 驱动中的 uvc_buffer_queue 函数)进一步处理。
  • done_entry:当缓冲区被设备驱动程序处理完成后,不管是成功完成还是出现错误,它会被添加到 done_list 链表中,此时使用 done_entry 作为链表节点。这样做是为了让框架能够跟踪哪些缓冲区已经准备好被应用程序通过 VIDIOC_DQBUF ioctl 调用回收。done_list 链表允许 VB2 框架有效地管理完成的缓冲区,确保它们可以按顺序被应用程序查询和回收,从而保持数据流的连续性和完整性。
5.2.5.0 调用关系
static int uvc_ioctl_qbuf(struct file *file, void *fh, struct v4l2_buffer *buf)
------->uvc_queue_buffer(&stream->queue, buf);
--------->vb2_qbuf(&queue->queue, buf);
-------------> vb2_internal_qbuf(q, b);

看一下这个关键的函数是怎么将buffer 加入到队列(vb2_queue)中去的

5.2.5.1 首先进行一些必要的判断
  • 检查buffer和队列的信息是否匹配
	int ret = vb2_queue_or_prepare_buf(q, b, "qbuf");
  • 对buffer的状态进行判断,如果处于不在队列里面的状态,则调用__buf_prepare(vb, b)

   switch (vb->state) {
   case VB2_BUF_STATE_DEQUEUED: /* 如果该队列处于出队列的状态*/
   	ret = __buf_prepare(vb, b);
   	if (ret)
   		return ret;
   	break;
   case VB2_BUF_STATE_PREPARED:
   	break;
   case VB2_BUF_STATE_PREPARING:
   	dprintk(1, "buffer still being prepared\n");
   	return -EINVAL;
   default:
   	dprintk(1, "invalid buffer state %d\n", vb->state);
   	return -EINVAL;
   }
   
5.2.5.2 判断过后就将视频缓冲区(vb->queued_entry )加入到队列q->queued_list中,修改buffer 相应参数

注意到 vb2_buffer 下面定义了两条链表 struct list_head queued_entry 和 struct list_head done_entry ,这两个队列是 VB2 框架内部用于管理所有已排队但尚未传递给驱动程序处理的缓冲区

  • 一条是用来将vb2_buffer 加入到等待数据填充的队列。
  • 另外一个是将vb2_buffer 加入到已经完成处理的队列
	list_add_tail(&vb->queued_entry, &q->queued_list);
	q->queued_count++;
	q->waiting_for_buffers = false;
	/*1.修改状态 */
	vb->state = VB2_BUF_STATE_QUEUED;/*排队等待的意思*/
5.2.5.3 如果buffer类型是输出缓冲区类型 还得进一步处理
	if (V4L2_TYPE_IS_OUTPUT(q->type)) {

		if ((q->timestamp_flags & V4L2_BUF_FLAG_TIMESTAMP_MASK) ==
		    V4L2_BUF_FLAG_TIMESTAMP_COPY)
			vb->v4l2_buf.timestamp = b->timestamp;
		vb->v4l2_buf.flags |= b->flags & V4L2_BUF_FLAG_TIMECODE;
		if (b->flags & V4L2_BUF_FLAG_TIMECODE)
			vb->v4l2_buf.timecode = b->timecode;
	}
5.2.5.4 启动或者继续流处理
  • 如果流已经启动,将缓冲区交给驱动进行处理。
	if (q->start_streaming_called)
		__enqueue_in_driver(vb);
		/*调用的是vb2_ops uvc_queue_qops 下的 buffer_queue

	/* Fill buffer information for the userspace */
	__fill_v4l2_buffer(vb, b);

__enqueue_in_driver(vb)调用的是vb2_ops uvc_queue_qops 下的 buffer_queue 将缓冲区buffer加入到队列

static void uvc_buffer_queue(struct vb2_buffer *vb)
{
	struct uvc_video_queue *queue = vb2_get_drv_priv(vb->vb2_queue);
	struct uvc_buffer *buf = container_of(vb, struct uvc_buffer, buf);//将vb2_buffer 转化为 uvc_buffer 
	unsigned long flags;

	spin_lock_irqsave(&queue->irqlock, flags);
	if (likely(!(queue->flags & UVC_QUEUE_DISCONNECTED))) { //如果队列没用标记为断开
	
		list_add_tail(&buf->queue, &queue->irqqueue); //将buffer 添加到irqqueue 链表中
	} else {
		/* If the device is disconnected return the buffer to userspace
		 * directly. The next QBUF call will fail with -ENODEV.
		 */
		buf->state = UVC_BUF_STATE_ERROR;
		vb2_buffer_done(&buf->buf, VB2_BUF_STATE_ERROR);
	}
	spin_unlock_irqrestore(&queue->irqlock, flags);
}
}
  • 如果还没开始流处理但已经达到了开始所需的缓冲区数量,尝试启动流处理。
	if (q->streaming && !q->start_streaming_called &&
	    q->queued_count >= q->min_buffers_needed) {
		ret = vb2_start_streaming(q);
		if (ret)
			return ret;
	}
	dprintk(1, "qbuf of buffer %d succeeded\n", vb->v4l2_buf.index);
	return 0;
}
5.2.5.5 全部的代码
static int vb2_internal_qbuf(struct vb2_queue *q, struct v4l2_buffer *b)
{
	int ret = vb2_queue_or_prepare_buf(q, b, "qbuf");
	/*先检查一下,缓冲区和队列是否匹配*/
	struct vb2_buffer *vb;

	if (ret)
		return ret;

	vb = q->bufs[b->index];

	switch (vb->state) {
	case VB2_BUF_STATE_DEQUEUED: /*出队列*/
		ret = __buf_prepare(vb, b);
		if (ret)
			return ret;
		break;
	case VB2_BUF_STATE_PREPARED:
		break;
	case VB2_BUF_STATE_PREPARING:
		dprintk(1, "buffer still being prepared\n");
		return -EINVAL;
	default:
		dprintk(1, "invalid buffer state %d\n", vb->state);
		return -EINVAL;
	}

	/*
	 * Add to the queued buffers list, a buffer will stay on it until
	 * dequeued in dqbuf.
	 */
	list_add_tail(&vb->queued_entry, &q->queued_list);
	q->queued_count++;
	q->waiting_for_buffers = false;
	/*1.修改状态 */
	vb->state = VB2_BUF_STATE_QUEUED;/*排队等待的意思*/
	if (V4L2_TYPE_IS_OUTPUT(q->type)) {
		/*
		 * For output buffers copy the timestamp if needed,
		 * and the timecode field and flag if needed.
		 */
		if ((q->timestamp_flags & V4L2_BUF_FLAG_TIMESTAMP_MASK) ==
		    V4L2_BUF_FLAG_TIMESTAMP_COPY)
			vb->v4l2_buf.timestamp = b->timestamp;
		vb->v4l2_buf.flags |= b->flags & V4L2_BUF_FLAG_TIMECODE;
		if (b->flags & V4L2_BUF_FLAG_TIMECODE)
			vb->v4l2_buf.timecode = b->timecode;
	}

	/*
	 * If already streaming, give the buffer to driver for processing.
	 * If not, the buffer will be given to driver on next streamon.
	 */
	if (q->start_streaming_called)
		__enqueue_in_driver(vb);

	/* Fill buffer information for the userspace */
	__fill_v4l2_buffer(vb, b);

	/*
	 * If streamon has been called, and we haven't yet called
	 * start_streaming() since not enough buffers were queued, and
	 * we now have reached the minimum number of queued buffers,
	 * then we can finally call start_streaming().
	 */
	if (q->streaming && !q->start_streaming_called &&
	    q->queued_count >= q->min_buffers_needed) {
		ret = vb2_start_streaming(q);
		if (ret)
			return ret;
	}

	dprintk(1, "qbuf of buffer %d succeeded\n", vb->v4l2_buf.index);
	return 0;
}

5.2.6 VIDIOC_STREAMON 启动摄像头

static int uvc_ioctl_streamon(struct file *file, void *fh, enum v4l2_buf_type type)
------->uvc_queue_streamon(&stream->queue, type);
---------> vb2_streamon(&queue->queue, type);
-------------> vb2_internal_streamon(q, type);
-----------------> vb2_start_streaming(struct vb2_queue *q)

对vb2_start_streaming 进行分析

5.2.6.1 遍历在排队中的缓冲区 并将缓存区传递给驱动进行处理
list_for_each_entry(vb, &q->queued_list, queued_entry)
		__enqueue_in_driver(vb);

将遍历出来的buffer交给uvc层的驱动处理

static void __enqueue_in_driver(struct vb2_buffer *vb)
{
	struct vb2_queue *q = vb->vb2_queue;
	unsigned int plane;
	// 1. 更新buffer状态 
	vb->state = VB2_BUF_STATE_ACTIVE;
	//2.增加驱动拥有buffe计数
	atomic_inc(&q->owned_by_drv_count);

	//3.同步缓冲区  确保缓冲区的内存与设备的DMA操作兼容
	for (plane = 0; plane < vb->num_planes; ++plane)
		call_void_memop(vb, prepare, vb->planes[plane].mem_priv);
	//4.将缓冲区正式提交给驱动进行处理
	call_void_vb_qop(vb, buf_queue, vb);
}
5.2.6.2 通知uvc层驱动开始流处理
	ret = call_qop(q, start_streaming, q,
	------>static int uvc_start_streaming(struct vb2_queue *vq, unsigned int count)
	--------> uvc_video_enable(stream, 1) 
  1. 其中启动视频流传输: uvc_video_enable(stream, 1),涉及到
  • 初始化时钟
ret = uvc_video_clock_init(stream);
  • 提交视频流的参数
ret = uvc_commit_video(stream, &stream->ctrl);
  • 初始化视频传输,设置必要的数据结构和 URB
ret = uvc_init_video(stream, GFP_KERNEL)
  1. 在初始化视频传输这个函数中 uvc_init_video(stream, GFP_KERNEL)
  • 根据设备是使用等时(isochronous)传输
uvc_init_video_isoc(stream, best_ep, gfp_flags); /*best_ep 最合适带宽的USB端点*/
  • 还是批量(bulk)传输来选择初始化流程
ret = uvc_init_video_bulk(stream, ep, gfp_flags);
  • 提交URB 给USB开始传输
	for (i = 0; i < UVC_URBS; ++i) 
	{
		ret = usb_submit_urb(stream->urb[i], gfp_flags);
		if (ret < 0) {
			uvc_printk(KERN_ERR, "Failed to submit URB %u "
					"(%d).\n", i, ret);
			uvc_uninit_video(stream, 1);
			return ret;
		}
	}

看一下uvc_init_video_isoc(stream, best_ep, gfp_flags); 这个函数
主要就是为视频流配置 和分配USB请求快(URB)这些URB用于管理和调度实际的数据传输

static int uvc_init_video_isoc(struct uvc_streaming *stream,
	struct usb_host_endpoint *ep, gfp_t gfp_flags)
{
	struct urb *urb;
	unsigned int npackets, i, j;
	u16 psize;
	u32 size;

	psize = uvc_endpoint_max_bpi(stream->dev->udev, ep);/*计算每个ISO包的最大字节*/
	size = stream->ctrl.dwMaxVideoFrameSize;/*最大视频帧的大小*/

	npackets = uvc_alloc_urb_buffers(stream, size, psize, gfp_flags);/*分配URB缓冲区*/
	if (npackets == 0)
		return -ENOMEM;

	size = npackets * psize;

	for (i = 0; i < UVC_URBS; ++i) {
		urb = usb_alloc_urb(npackets, gfp_flags);
		if (urb == NULL) {
			uvc_uninit_video(stream, 1);
			return -ENOMEM;
		}

		urb->dev = stream->dev->udev;
		urb->context = stream;
		urb->pipe = usb_rcvisocpipe(stream->dev->udev,
				ep->desc.bEndpointAddress);
		/*通过通过usb_rcvisocpipe函数和端点地址计算出用于数据传输的pipe值*/
#ifndef CONFIG_DMA_NONCOHERENT
		urb->transfer_flags = URB_ISO_ASAP | URB_NO_TRANSFER_DMA_MAP;
		urb->transfer_dma = stream->urb_dma[i];
#else
		urb->transfer_flags = URB_ISO_ASAP;
#endif
		urb->interval = ep->desc.bInterval;/*传输间隔*/
		urb->transfer_buffer = stream->urb_buffer[i];
		urb->complete = uvc_video_complete;//回调函数
		urb->number_of_packets = npackets;//包的数量
		urb->transfer_buffer_length = size;//缓冲区的长度

		for (j = 0; j < npackets; ++j) {
			urb->iso_frame_desc[j].offset = j * psize;
			urb->iso_frame_desc[j].length = psize;
		}

		stream->urb[i] = urb;
	}

	return 0;
}

当一个URB完成传输后,无论是成功、失败还是被取消,uvc_video_complete 这个函数都会被调用。它的主要职责是处理URB的完成状态,对接收到的数据进行解码(如果需要),并重新提交URB以继续数据传输

static void uvc_video_complete(struct urb *urb)
{
	struct uvc_streaming *stream = urb->context;
	struct uvc_video_queue *queue = &stream->queue;
	struct uvc_buffer *buf = NULL;
	unsigned long flags;
	int ret;

	switch (urb->status) {
	case 0:
		break;

	default:
		uvc_printk(KERN_WARNING, "Non-zero status (%d) in video "
			"completion handler.\n", urb->status);

	case -ENOENT:		/* usb_kill_urb() 被调用 主动停止URB. */
		if (stream->frozen)
			return;

	case -ECONNRESET:	/* usb_unlink_urb() 取消URB */
	case -ESHUTDOWN:	/* The endpoint is being disabled. */
		uvc_queue_cancel(queue, urb->status == -ESHUTDOWN);
		return;
	}

	spin_lock_irqsave(&queue->irqlock, flags);
	if (!list_empty(&queue->irqqueue))
		buf = list_first_entry(&queue->irqqueue, struct uvc_buffer,
				       queue);
	spin_unlock_irqrestore(&queue->irqlock, flags);

	stream->decode(urb, stream, buf); /*uvc_video_decode_isoc*/
	
	/*负责对URB传输的数据进行解码 如果有指定的话。这个处理可能包括将数据从
	USB缓冲区复制到视频缓冲区中,进行必要的格式转换等。*/

	if ((ret = usb_submit_urb(urb, GFP_ATOMIC)) < 0) { /*重新提交URB 继续传输*/
		uvc_printk(KERN_ERR, "Failed to resubmit video URB (%d).\n",
			ret);
	}
}

5.2.6 VIDIOC_DQBUF 出队列

static int uvc_ioctl_dqbuf(struct file *file, void *fh, struct v4l2_buffer *buf)
------->uvc_dequeue_buffer(&stream->queue, buf,file->f_flags & O_NONBLOCK);
---------> vvb2_dqbuf(&queue->queue, buf, nonblocking);
-------------> vb2_internal_dqbuf(q, b, nonblocking);

5.2.6.1 获取已完成的缓冲区

ret = __vb2_get_done_vb(q, &vb, b, nonblocking);

在__vb2_get_done_vb 这个函数下会等待 done_list队列中已经处理好的缓冲区done_entry,并将其从链表中删除

5.2.6.1 检查缓冲区以及处理缓冲区

	call_void_vb_qop(vb, buf_finish, vb);
	__fill_v4l2_buffer(vb, b);
	list_del(&vb->queued_entry);
	q->queued_count--;

5.2.6.1更新缓冲区信息以便再次入队使用

__vb2_dqbuf(vb);

5.2.7 VIDIOC_STREAMOFF 关闭摄像头

static int uvc_ioctl_streamoff(struct file *file, void *fh, enum v4l2_buf_type type)
------->uvc_queue_streamoff(&stream->queue, type);
--------->vb2_streamoff(&queue->queue, type);
------------->  vb2_internal_streamoff(q, type);
--------------->__vb2_queue_cancel(q);
---------------->call_void_qop(q, stop_streaming, q) --->uvc_video_enable(stream, 0);

调用 __vb2_queue_cancel(q)取消队列操作

  • 暂停流传输,确保不再有数据从缓冲区传输到硬件或从硬件传输到缓冲区。

  • 从驱动和videobuf2框架中移除所有缓冲区,将它们的状态重置为初始状态,从而返回缓冲区的控制权给用户空间。这意味着这些缓冲区可以被再次查询、入队或释放,而不会遇到由于仍处于活动状态而导致的错误。

  • 13
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Linux 系统中,UVC 设备和 U 盘都是通过 USB 接口与计算机进行连接的。它们都使用了 USB 协议,并且在 USB 子系统中都被视为 USB 设备UVC 设备是指符合 USB 视频类设备(USB Video Class,简称 UVC)标准的设备,这类设备通常是摄像头或者视频采集卡等。UVC 设备在 USB 子系统中被识别为 USB 视频类设备,并且包含了对应的设备描述符和接口描述符等信息。 U 盘则是指通用 USB 存储设备,它通常用于存储数据。U 盘在 USB 子系统中被识别为 USB 存储设备,并且包含了对应的设备描述符和接口描述符等信息。 Linux 系统是通过设备驱动程序来区分不同类型的 USB 设备的。对于 UVC 设备,通常需要使用 UVC 驱动程序进行识别和管理;对于 U 盘,则需要使用 USB 存储设备驱动程序进行识别和管理。在内核中,不同类型的 USB 设备驱动程序通常是通过不同的 USB 设备类别(如 USB 视频类、USB 存储类等)进行区分的。 当系统检测到一个 USB 设备被插入,它会自动根据设备描述符和接口描述符等信息进行匹配,并调用相应驱动程序进行识别和管理。对于 UVC 设备和 U 盘,系统会根据它们的设备描述符和接口描述符等信息进行区分,并调用相应驱动程序进行管理。 总的来说,UVC 设备和 U 盘在 Linux 系统中都是通过 USB 子系统进行识别和管理的,系统是通过设备描述符、接口描述符等信息来区分不同类型的 USB 设备的,并根据不同的设备驱动程序进行管理。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值