把摄像头想象成一位新员工,Linux 内核是公司,而 UVC 驱动程序就是这位新员工的直属部门经理。这位“部门经理”(UVC 驱动)的主要职责有三件大事:
- 员工入职 (设备枚举):当新员工报到时,搞清楚他是谁、有什么能力、内部工作流程是怎样的。
- 日常管理 (设备控制):在工作中,向下属下达指令,比如“调整一下状态”、“改变一下工作模式”。
- 成果验收 (数据传输):接收下属完成的工作成果(视频画面),并汇报给需要的人(应用程序)。
1. 蓝图与档案 - 核心数据结构
-
struct uvc_device - 员工总档案
这是最顶层的结构体,代表一个完整的 USB 摄像头设备。它是这位新员工的“总档案袋”,在设备插入时由
uvc_probe函数创建,并作为所有信息的根节点。struct uvc_device { struct usb_device *udev; // 链接到公司底层USB系统 struct usb_interface *intf; // 链接到该员工的“行政接口”(Video Control) // --- 核心档案分类 --- struct list_head entities; // “零件清单”:记录了该员工内部的所有功能部件 struct list_head streams; // “能力清单”:记录了他能产出哪些规格的产品 struct list_head chains; // “工作流程图”:描绘了其内部部件如何协同工作 // ... };entities(实体/零件):摄像头不是一整块铁,它内部有传感器、图像处理器等多个功能部件。这个链表就存放着所有这些“零件”的信息,每个节点都是一个uvc_entity结构体。streams(数据流):记录了摄像头能输出的所有视频格式、分辨率和帧率组合,就像一份详细的“产品规格书”。每个节点是一个uvc_streaming结构体。chains(处理链):将entities里的“零件”按照实际工作顺序连接起来,形成一条或多条完整的“生产流水线”。每个节点是一个uvc_video_chain结构体。
-
struct uvc_entity- 生产线上的零件这个结构体代表摄像头内部流水线上的一个独立功能部件(Unit)或端口(Terminal)。
struct uvc_entity { struct list_head list; // 用于将自己登记到“零件清单”(uvc_device->entities)中 struct list_head chain; // 用于将自己编入某条“工作流程图”(uvc_video_chain)中 u8 id; // 零件的唯一编号 u8 *baSourceID; // “上游供应商编号”:记录了为我提供原料的零件ID // ... };baSourceID:这是组装流水线的关键图纸! 每个零件都通过它明确指出了自己的“上游”是谁。驱动就是靠着这个信息,才能将一堆独立的零件组装成有逻辑的生产线.
-
档案关系与建立流程
那么,
uvc_device(总档案)和uvc_entity(零件档案)是如何关联起来的呢?关系:
uvc_device包含uvc_entity。 更具体地说,uvc_device结构体中的entities成员是一个链表头 (struct list_head),它指向一个由uvc_entity结构体组成的双向链表。每一个uvc_entity则通过其自身的list成员 (struct list_head) 将自己“钩”在这个链表上。这个关系可以这样可视化:
uvc_device (dev)
|
+--> dev->entities (链表头)
^ |
| v
[uvc_entity 1] <---> [uvc_entity 2] <---> [uvc_entity 3] ...
(list成员) (list成员) (list成员)构建流程: 这个关联关系是在**设备枚举(
uvc_probe)**过程中动态建立的。- 创建总档案:当摄像头插入时,
uvc_probe函数首先会创建一个uvc_device结构体实例,此时它的entities链表是空的。 - 解析简历,创建零件档案:接着,驱动开始解析设备的描述符(简历)。每当它解析到一个功能部件(如输入终端、处理单元),它就会:
- 在内存中创建一个新的
uvc_entity结构体实例(一份新的零件档案)。 - 将从描述符中读到的信息(如ID、类型)填入这份档案。
- 执行
list_add_tail(&entity->list, &dev->entities),将这份新的零件档案添加到uvc_device的entities链表的末尾。
- 在内存中创建一个新的
- 循环构建:驱动会重复第2步,直到所有功能部件的描述符都被解析完毕。这时,
dev->entities链表就完整地记录了摄像头所有的内部“零件”。
简单来说,
uvc_device是容器,uvc_entity是内容物。在设备枚举的流程中,驱动程序扮演了“装配工”的角色,不断地制造出uvc_entity零件,并将它们一个个地装入uvc_device这个总容器的entities清单中。理解了这一点,我们再去看第二章的枚举过程,就会非常清晰了。 - 创建总档案:当摄像头插入时,
2. 新员工入职 - 设备枚举
-
部门经理就位 (驱动注册)
在摄像头插入前,UVC 驱动(部门经理)需要先在公司(内核)里“注册”,让公司知道有这么一个部门存在。
构建流程:
module_init->uvc_init->usb_registermodule_init(uvc_init):这是内核模块的入口。当模块被加载时,uvc_init函数会被调用。uvc_init():这个函数的核心工作是调用usb_register(&uvc_driver.driver)。usb_register():这个函数将uvc_driver这个全局结构体注册到内核的 USB 子系统中。uvc_driver结构体里包含了驱动的名字、probe函数指针、disconnect函数指针,以及最重要的id_table(即uvc_ids数组)。这相当于告诉人事部(USB 核心):“我是UVC部门经理,以后凡是简历上写着‘符合UVC标准’的求职者,都请通知我来面试。”
-
新员工面试 (
uvc_probe)当摄像头插入时,人事部(USB 核心)发现简历(设备描述符)与
uvc_ids匹配,立刻通知 UVC 驱动来面试。这个面试过程就是uvc_probe函数。 -
uvc_probe的详细工作流程构建流程: 用户插入设备 -> USB 核心匹配
uvc_ids-> 调用uvc_probeuvc_probe | +-> kzalloc // 1. 创建 uvc_device 总档案袋 | +-> uvc_parse_control(dev) // 2. 开始阅读简历 | | | +-> uvc_parse_standard_control // 逐个解析VC接口描述符 | | | +-> 遇到 INPUT_TERMINAL(ID=1), PROCESSING_UNIT(ID=2)... | | | | | +-> uvc_alloc_entity() // 创建零件档案(uvc_entity) | | +-> list_add_tail() // 存入 dev->entities 零件清单 | | | +-> 遇到 VC_HEADER | | | +-> uvc_parse_streaming(dev, intf) // 跳转去阅读“专业技能”部分 | | | +-> uvc_parse_format() // 解析FORMAT_UNCOMPRESSED, FORMAT_MJPEG等 | | | | | +-> uvc_format_by_guid() // 将GUID翻译成V4L2格式 | | | +-> list_add_tail() // 将解析出的 uvc_streaming 存入 dev->streams 能力清单 | +-> v4l2_device_register() // 3. 初始化V4L2设备 | +-> uvc_ctrl_init_device(dev) // 4. 根据简历中的bmControls,建立控制映射表 | +-> uvc_scan_device(dev) // 5. 绘制内部工作流程图 | | | +-> uvc_scan_chain() // 从OUTPUT_TERMINAL(ID=3)开始 | | | +-> uvc_scan_chain_backward() // 根据bSourceID=2找到PROCESSING_UNIT(ID=2) | | | +-> uvc_scan_chain_backward() // 根据bSourceID=1找到INPUT_TERMINAL(ID=1) | // 一条完整的chain建立,存入 dev->chains | +-> uvc_register_chains(dev) // 6. 办理工牌,分配工位 | +-> uvc_register_video() | +-> video_register_device() // 创建 /dev/videoX 文件至此,一个结构完整、信息齐全的
uvc_device对象已经完全建立,并且与我们在lsusb中看到的信息完全对应。
3. 日常管理 - 设备控制
员工入职后,经理需要对他进行日常管理。这部分的核心是翻译:将应用程序发出的标准 V4L2 指令,翻译成摄像头硬件能听懂的 UVC 特定指令。
-
核心档案:控制相关的“翻译手册”
为了实现精确翻译,驱动在枚举阶段就建立了一套“指令翻译手册”。这套手册由以下几个关键数据结构组成:
static const struct uvc_control_info uvc_ctrls[]: 这是静态的、全局的“UVC 指令字典”。它是一个在uvc_ctrl.c中预先定义好的数组。它描述了 UVC 规范中定义的各种控制项的 UVC 侧属性,比如某个控制项属于哪个类型的零件(由entityGUID 标识)、它的 UVC 选择子selector是多少、数据长度size是多少等。static const struct uvc_control_mapping uvc_ctrl_mappings[]: 这是静态的、全局的“V4L2-UVC 翻译规则表”。它也是一个预定义好的数组。它的作用是建立 V4L2 世界和 UVC 世界的桥梁。每一条规则都明确指出:一个 V4L2 的控制 ID(id = V4L2_CID_BRIGHTNESS),对应 UVC 世界里的哪个零件(entityGUID)、哪个选择子(selector),以及它在应用程序中显示的名字(name = "Brightness")和数据类型(v4l2_type)等struct uvc_control: 这是为每个设备动态创建的“运行时控制实例”。它不是静态的,而是当驱动发现一个设备实际支持某个控制项时,在内存中动态创建的。它代表一个具体的“控制旋钮”(如亮度)在某个设备上的当前状态,包含了:struct uvc_entity *entity: 指向拥有这个控制旋钮的具体零件。const struct uvc_control_info *info: 指向uvc_ctrls字典中关于这个控制的 UVC 侧定义。u8 index: 这个控制项在bmControls位图中的索引。...data: 存放当前值、默认值等运行时数据。
-
“翻译手册”的构建流程 (
uvc_ctrl_init_device)- 遍历所有零件:
uvc_ctrl_init_device函数在uvc_probe期间被调用,它会遍历dev->entities(所有零件清单)。 - 检查零件能力:当找到一个支持控制的零件(比如 Processing Unit),它会检查该零件描述符中的
bmControls位图。 - 为每个能力创建实例:对于位图中被置位的每一个控制项(比如亮度,假设在第0位),驱动会:
- 分配内存,创建一个动态的
uvc_control实例。 - 将这个实例的
entity指针指向当前的uvc_entity零件。 - 将实例的
index设为 0。
- 分配内存,创建一个动态的
- 链接静态定义 (
uvc_ctrl_init_ctrl):- 接着,
uvc_ctrl_init_ctrl函数被调用。它的任务是为这个动态的uvc_control实例找到它在静态字典中的定义。 - 它会遍历全局的
uvc_ctrls字典,查找哪个条目的entityGUID 与当前零件的 GUID 匹配,并且index也匹配(都为0)。 - 一旦找到,它就把这个
uvc_control实例的info指针指向uvc_ctrls字典中的这个条目。
- 接着,
- 注册到 V4L2:
- 现在,驱动需要让 V4L2 框架也知道这个控制项的存在。它会遍历全局的
uvc_ctrl_mappings翻译规则表。 - 查找哪个规则的
entityGUID 和selector与刚刚链接好的info中的信息匹配。 - 一旦找到匹配的规则,驱动就获得了所有需要的信息:V4L2 ID (
V4L2_CID_BRIGHTNESS)、名字 ("Brightness") 等。 - 最后,驱动使用这些信息调用
v4l2_ctrl_new_custom,在 V4L2 框架中创建一个标准的控制项,并将其与我们的uvc_control实例关联起来。
- 现在,驱动需要让 V4L2 框架也知道这个控制项的存在。它会遍历全局的
- 存入零件档案:所有这些初始化完成的
uvc_control实例,最终被存入它所属的uvc_entity的controls数组中。
- 遍历所有零件:
-
下达指令的完整流程
构建流程: 应用程序
ioctl-> V4L2 核心 ->uvc_ioctl_ops->uvc_query_ctrl->usb_control_msg- 应用程序提出需求:通过标准的
ioctl系统调用,向/dev/videoX这个工位发出指令,例如ioctl(fd, VIDIOC_S_CTRL, &control_struct),其中control_struct.id为V4L2_CID_BRIGHTNESS。 - V4L2 核心转接:V4L2 核心接到指令后,查找到
videoX对应的video_device,并调用其v4l2_ioctl_ops中的vidioc_s_ctrl函数指针,该指针指向了 V4L2 框架提供的通用函数v4l2_s_ctrl。 - UVC 驱动翻译并执行:
v4l2_s_ctrl会使用与video_device关联的v4l2_ctrl_handler(流水线的“总控制台”)。- 这个 handler 会根据
V4L2_CID_BRIGHTNESS快速找到对应的v4l2_ctrl,并进而找到我们与之关联的uvc_control实例。 - 通过
uvc_control实例,驱动可以访问到它的info指针(指向uvc_ctrls),从而获得 UVC 选择子selector。 - 通过
uvc_control实例,驱动也可以访问到它的entity指针,从而获得 单元IDentity->id。 - 最后,调用
uvc_query_ctrl(),将这些精确翻译出来的信息作为参数传入。
- USB 核心打包发货:
uvc_query_ctrl函数是真正与底层 USB 通信的接口。它使用收到的参数,组装一个标准的 USB 控制请求包,并通过usb_control_msg()发送给摄像头硬件。
- 应用程序提出需求:通过标准的
4. 成果验收 - 视频数据传输
-
“物流系统”的核心档案 (
uvc_video_queue&uvc_buffer)为了高效管理数据流,UVC 驱动在 V4L2
videobuf2(vb2) 框架的基础上,扩展了两个自己的核心结构体,这构成了它的“物流管理系统”。struct uvc_video_queue: 这是“物流中心”的总管理档案。struct vb2_queue queue: 内嵌了标准的vb2_queue。vb2_queue负责与应用程序交互,管理所有缓冲区的状态(比如哪些是空闲的,哪些正在被硬件使用,哪些已经填满数据等待用户取走)。struct list_head irqqueue: 这是 UVC 驱动自己增加的“待装货区”队列。 这是一个关键的设计。所有应用程序交给驱动的空缓冲区,都会先被放到这个队列里,等待硬件数据到达后被填充。spinlock_t irqlock: 用于保护irqqueue的自旋锁,因为这个队列会在中断上下文(收货)和进程上下文(发货)中被同时访问。
struct uvc_buffer: 这是每一份“货物”(视频帧)的“物流运单”。struct vb2_v4l2_buffer buf: 内嵌了标准的vb2_v4l2_buffer,其中又包含了vb2_buffer。这个标准部分记录了缓冲区的内存地址、大小、状态等通用信息。struct list_head queue: 用于将这个uvc_buffer挂载到uvc_video_queue的irqqueue(待装货区)链表上。enum uvc_buffer_state state: 记录了这份货物当前的装填状态(比如是空的、正在装填、还是已装满一帧)。
关系总结:一个
uvc_video_queue(物流中心)管理着多个uvc_buffer(运单)。uvc_video_queue内部有两个核心队列:一个是标准的vb2_queue,负责对外(与APP)的流程;另一个是私有的irqqueue,负责对内(与硬件中断)的流程。 -
视频数据的完整流转过程
第 ① 步:APP 将空仓库入队 (
VIDIOC_QBUF)- 应用程序:调用
ioctl(fd, VIDIOC_QBUF, &v4l2_buf),告诉驱动:“这个仓库(缓冲区)我已经用完了,现在交给你去装货。” - V4L2 核心:接收到请求,调用
vb2_qbuf函数。 vb2框架:将对应的vb2_buffer放入内部的queued_list(已入队列表),然后调用 UVC 驱动注册的buf_queue操作。- UVC 驱动 (
uvc_buffer_queue):- 这是 UVC 的“入库管理员”。它找到
vb2_buffer对应的uvc_buffer。 - 关键操作:它将这个
uvc_buffer通过其queue成员,挂载到uvc_video_queue的irqqueue链表上。 - 现在,这个空仓库正式进入了“待装货区”,等待硬件数据。
- 这是 UVC 的“入库管理员”。它找到
第 ② 步:启动生产与运输 (
VIDIOC_STREAMON)- 应用程序:调用
ioctl(fd, VIDIOC_STREAMON)下达开工令。 - V4L2 核心:调用
vb2_streamon,最终会调用 UVC 驱动注册的start_streaming操作,即uvc_start_streaming。 - UVC 驱动 (
uvc_start_streaming):- 这是“生产调度员”。它调用
uvc_video_init_transfers。 - 准备货车 (分配 URB):创建一批 URB (USB Request Block)。
- 派发货车 (提交 URB):通过
usb_submit_urb,把所有准备好的空货车一辆辆派往摄像头,并为每辆车都指定同一个完成回调函数uvc_video_complete。
- 这是“生产调度员”。它调用
第 ③ 步:硬件中断与收货 (
uvc_video_complete)摄像头开始源源不断地发送数据。每当一个 URB(货车)被装满数据并送达主机时,硬件会产生中断,最终导致
uvc_video_complete函数被调用。uvc_video_complete:这是“仓库收货员”,在中断上下文中执行。- 从“待装货区”取出一个空仓库:收货员首先会查看
irqqueue队列。如果队列中有空闲的uvc_buffer,它会取出一个。 - 卸货:它将 URB 中的视频数据,通过
uvc_video_decode_isoc等函数,小心地“卸”到刚刚取出的uvc_buffer所指向的内存中。 - 检查是否装满:收货员会检查这一帧视频是否已经完整接收。
- 如果未完整,它会继续等待下一个 URB 的到来,继续向同一个
uvc_buffer中填充数据。 - 如果已完整:
- a. 从
irqqueue移除:将这个已经装满的uvc_buffer从irqqueue(待装货区)链表中移除。 - b. 放入
done_list:调用vb2_buffer_done。这个函数会把vb2_buffer的状态标记为VB2_BUF_STATE_DONE,并将其放入vb2_queue的done_list(已完成待提货区)中,同时唤醒正在等待数据的应用程序。
- a. 从
- 如果未完整,它会继续等待下一个 URB 的到来,继续向同一个
- c. 重新派发货车:将这个空了的 URB 再次通过
usb_submit_urb派出去,实现运输的无缝循环。
第 ④ 步:APP 取出成品 (
VIDIOC_DQBUF)- 应用程序:调用
ioctl(fd, VIDIOC_DQBUF, &v4l2_buf),询问:“有没有已经装满货的仓库?” - V4L2 核心:接收请求,调用
vb2_dqbuf。 vb2框架:检查vb2_queue的done_list(已完成待提货区)。如果队列不为空,就从头部取出一个vb2_buffer,将其信息填入v4l2_buf,并返回给应用程序。
至此,一个视频帧的数据就完成了从硬件到应用程序的完整旅程。
- 应用程序:调用

1115

被折叠的 条评论
为什么被折叠?



