欢迎光临
我们一直在努力

i.MX6ULL 平台 V4L2 摄像头驱动框架解析

V4L2 简介

V4L2 的全称是 Video4Linux2,它是 Linux 内核中用于管理视频设备的一套统一驱动框架。简单来说,V4L2 为摄像头提供了统一的访问接口。应用程序不需要直接关心底层硬件的具体实现,只需要通过 /dev/videoX 设备节点,就可以完成图像采集、格式设置、缓冲区管理等操作。

常见的摄像头主要分为两类:一类是 USB 摄像头,通常遵循 UVC 协议,由 Linux 的 uvcvideo 通用驱动支持;另一类是 MIPI 摄像头,通过 MIPI CSI 接口连接到 SoC,需要 sensor、CSI、ISP/VI 等驱动协同工作,最终也会注册成 V4L2 设备供应用程序使用。

由于 IMX6ULL 不支持 MIPI CSI-2 控制器,因此本文将以 USB 摄像头 为例,从硬件层、USB Core、UVC 驱动、V4L2 Core 和应用层几个方面,介绍 V4L2 的基本工作流程。

硬件层

由于本文使用的是 USB 摄像头,因此首先需要查看原理图,确认摄像头接口对应哪一路 USB 控制器。从原理图可以看出,USB_OTG_DP/DM 经过 USB HUB 分发后,得到 USB_HUB3_DP/DM,该接口就是 USB 摄像头使用的接口。因此,设备树中需要重点关注 usbotg2 节点。

一般情况下,.dtsi 文件已经完成 USB 引脚和 PHY 的基础配置,我们主要关注 USB 的工作模式。由于 USB 摄像头是外接设备,i.MX6ULL 需要作为主机端进行枚举,因此应配置为 dr_mode = "host";   这样系统才能正常识别 USB 摄像头,并通过 UVC 驱动注册成 V4L2 设备。

&usbotg2 {
dr_mode = "host";
disable-over-current;
status = "okay";
};

USB Core层

在硬件层确认 USB 摄像头连接到 usbotg2 后,需要继续关注设备树和内核驱动的注册流程。系统启动后,Linux 会根据设备树中的 usbotg2 节点创建对应的 platform_device,通常可以在下面目录中看到类似设备:

/sys/bus/platform/devices/2184200.usb

随后,该设备会匹配到 i.MX USB Glue 层驱动,进入 ci_hdrc_imx_probe(),用于获取 USB PHY、时钟、中断等硬件资源,并注册 ChipIdea USB 控制器。

接着,ChipIdea Core 层会继续完成 USB 控制器初始化,包括配置 PHY、中断和 dr_mode。由于 USB 摄像头属于外接设备,因此 USB 控制器需要配置为 Host 模式:

dr_mode = "host";

进入 Host 模式后,内核会注册 Root Hub,并通过 USB Core 对下游设备进行枚举。当 USB 摄像头接入后,系统会读取其描述符信息,并最终匹配到 uvcvideo 驱动。

关于 i.MX6ULL USB 控制器从设备树注册、Host 初始化到 USB 设备枚举的详细流程,可参考另一篇文章: i.MX6ULL USB 驱动分析——以 RTL8732 USB 设备为例-CSDN博客

ls /sys/bus/usb/devices

可能看到:

usb1
1-0:1.0
1-1
1-1:1.0
1-1:1.1

含义:

usb1
虚拟 root hub,也就是 USB 主机控制器下面的根 hub

1-0:1.0
root hub 的 interface

1-1
真实 USB 摄像头设备本体

1-1:1.0
摄像头的 interface 0,通常是 VideoControl interface

1-1:1.1
摄像头的 interface 1,通常是 VideoStreaming interface

UVC 驱动层

UVC 是 USB 摄像头常用的一套标准协议。Linux 通过 uvcvideo 驱动来解析和控制 UVC 摄像头,并最终把摄像头注册成 V4L2 设备,供用户空间程序通过 /dev/video0 这类节点访问。在 UVC 驱动层,主要会完成下面几个关键步骤:

1、uvcvideo 驱动会先注册到 USB 驱动框架中。当 USB 摄像头接入后,USB Core 会先完成设备枚举,读取相关描述符;当发现设备接口符合 UVC 协议并与 uvcvideo 驱动匹配成功后,才会调用 uvc_probe()。

2、uvc_probe() 的主要功能是解析 UVC 描述符,并建立 uvc_device 中的关键数据结构。它会先解析 VideoControl Interface,获取摄像头的功能实体,例如 Camera Terminal、Processing Unit 和 Output Terminal的控制项并通过 list_add_tail() 将这些实体加入到 dev->entities 链表中。其中:

list_add_tail(&term->list, &dev->entities);

用于把 Camera Terminal 或 Output Terminal 加入到 dev->entities;

list_add_tail(&unit->list, &dev->entities);

用于把 Processing Unit 加入到 dev->entities。

那么 uvc_parse_standard_control() 解析完后,大概得到:

dev->entities

|

+-> entity0: Camera Terminal

| id = 1

| type = UVC_VC_INPUT_TERMINAL | UVC_TERM_INPUT

| name = "Camera 1"

| camera.bmControls = 支持曝光、对焦、变焦等控制项的 bitmap

|

+-> entity1: Processing Unit

| id = 2

| type = UVC_VC_PROCESSING_UNIT

| name = "Processing 2"

| baSourceID[0] = 1

| processing.bmControls = 支持亮度、对比度、白平衡等控制项的 bitmap

|

+-> entity2: Output Terminal

id = 3

type = UVC_VC_OUTPUT_TERMINAL | UVC_TERM_OUTPUT

name = "Output 3"

baSourceID[0] = 2

随后,uvc_probe() 会解析 VideoStreaming Interface,获取视频格式、数据类型、分辨率、帧率和端点信息,例如 MJPEG、YUYV、640×480、1280×720 等,并将这些视频流信息加入到 dev->streams 链表中。

那么 uvc_parse_streaming() 解析完后,大概得到:

dev->streams
|
+-> stream0
|
+-> stream0->dev = dev
| 注释:属于当前这个 USB 摄像头
|
+-> stream0->intf = VideoStreaming Interface 1
| 注释:保存 USB 的 VideoStreaming interface
|
+-> stream0->intfnum = 1
| 注释:interface 编号是 1
|
+-> stream0->type = V4L2_BUF_TYPE_VIDEO_CAPTURE
| 注释:这是摄像头采集流,应用层用 VIDEO_CAPTURE
|
+-> stream0->header.bEndpointAddress = 0x81
| 注释:视频数据从 USB IN endpoint 0x81 传回来
|
+-> stream0->header.bTerminalLink = 3
| 注释:这个 stream 连接到 Output Terminal ID 3
|
+-> stream0->nformats = 2
| 注释:支持 2 种图像格式
|
+-> stream0->format[0]
| |
| +-> name = "MJPEG"
| +-> fcc = V4L2_PIX_FMT_MJPEG
| +-> nframes = 2
| |
| +-> frame[0]
| | +-> width = 640
| | +-> height = 480
| | +-> dwFrameInterval = 30fps / 15fps 对应的时间间隔
| |
| +-> frame[1]
| +-> width = 1280
| +-> height = 720
| +-> dwFrameInterval = 30fps 对应的时间间隔
|
+-> stream0->format[1]
|
+-> name = "YUYV"
+-> fcc = V4L2_PIX_FMT_YUYV
+-> nframes = 1
|
+-> frame[0]
+-> width = 640
+-> height = 480
+-> dwFrameInterval = 30fps 对应的时间间隔

因此,dev->entities 保存的是 UVC 的功能实体,dev->streams 保存的是实际的视频流接口信息。

重点:

一个 UVC 摄像头通常包含两个重要接口:VideoControl Interface 和 VideoStreaming Interface。

VideoControl Interface 主要描述摄像头的控制结构,里面会解析出多个 UVC 功能实体,例如:

  •  Input Terminal:表示摄像头图像输入端;
  • Processing Unit:表示图像处理单元,例如亮度、对比度、曝光、白平衡等控制;
  • Output Terminal:表示视频输出端,通常连接到 VideoStreaming Interface。

这些实体会组成一条视频链路:

Camera Terminal -> Processing Unit -> Output Terminal -> VideoStreaming Interface

VideoStreaming Interface 主要描述实际的视频数据流,包括视频格式、分辨率、帧率和 USB 端点等信息,例如 YUYV、MJPEG、640×480、1280×720 等。

V4L2 Core层

USB Core 完成枚举并匹配到 uvcvideo 驱动后,会调用 uvc_probe()。

uvc_probe() 的主要作用是解析 USB 摄像头的 UVC 描述符。它会先解析 VideoControl Interface,得到摄像头的功能实体,例如 Camera Terminal、Processing Unit 和 Output Terminal。这些实体表示摄像头的“输入 → 处理 → 输出”链路,并且每个实体中都有 bmControls,用来表示它支持哪些控制项,例如曝光、对焦、亮度、对比度、白平衡等。解析出的实体会保存到 dev->entities 链表中。

然后,uvc_probe() 会解析 VideoStreaming Interface,得到视频格式、分辨率、帧率和端点信息,例如 YUYV、MJPEG、640×480、30fps 等,并保存到 dev->streams 链表中。当这些信息解析完成后,驱动还需要把 UVC 的控制项转换成 V4L2 能识别的控制项。这个工作由 uvc_ctrl_init_device(struct uvc_device *dev) 完成。

——————————————————————————————————————————-

当 UVC 设备的控制实体和视频流信息解析完成后,驱动还需要把这些信息转换成用户空间能够使用的接口,这一层接口就是 V4L2。用户空间程序最终通过 /dev/video0 以及 V4L2 ioctl 来访问摄像头、设置参数和获取图像数据。

其中,uvc_ctrl_init_device(struct uvc_device *dev) 的作用就是初始化 UVC 控制项。它会遍历 dev->entities 链表中的每个 entity,找到该 entity 的 bmControls 位图,然后逐 bit 检查哪些控制项被设备支持。对于每一个被支持的 bit,驱动都会创建一个 struct uvc_control,记录该控制项所属的 entity 以及它在 bmControls 中的 bit 编号。

随后,uvc_ctrl_init_ctrl(dev, ctrl) 会继续初始化这个控制项。它会根据 entity 的 GUID 和控制项 index 找到对应的 UVC selector,并保存到控制项的 info 信息中;然后再通过 __uvc_ctrl_add_mapping() 将这个 UVC 控制项映射成 V4L2 控制项,例如 V4L2_CID_BRIGHTNESS、V4L2_CID_EXPOSURE、V4L2_CID_WHITE_BALANCE_TEMPERATURE 等。

举一个具体例子,假设摄像头的 Processing Unit 是:

Processing Unit ID 2

bmControls:
bit0 = 1 支持 Brightness
bit1 = 1 支持 Contrast
bit2 = 0 不支持 Hue

那么 uvc_ctrl_init_device() 解析后得到:

entity: Processing Unit ID 2
|
+-> entity->ncontrols = 2
|
+-> entity->controls[0]
| |
| +-> ctrl->entity = Processing Unit ID 2
| +-> ctrl->index = 0
| +-> ctrl->info.selector = UVC_PU_BRIGHTNESS_CONTROL
| +-> ctrl->info.size = 2
| +-> ctrl->info.flags = 支持 SET_CUR / GET_RANGE
| |
| +-> ctrl->info.mappings
| |
| +-> mapping
| +-> mapping->id = V4L2_CID_BRIGHTNESS
| +-> mapping->name = "Brightness"
| +-> mapping->entity = UVC_GUID_UVC_PROCESSING
| +-> mapping->selector = UVC_PU_BRIGHTNESS_CONTROL
|
+-> entity->controls[1]
|
+-> ctrl->entity = Processing Unit ID 2
+-> ctrl->index = 1
+-> ctrl->info.selector = UVC_PU_CONTRAST_CONTROL
|
+-> ctrl->info.mappings
|
+-> mapping->id = V4L2_CID_CONTRAST

代码里的 Brightness 对应关系是:

static struct uvc_control_info uvc_ctrls[] = {
{
.entity = UVC_GUID_UVC_PROCESSING,
.selector = UVC_PU_BRIGHTNESS_CONTROL,
.index = 0,
.size = 2,
.flags = UVC_CTRL_FLAG_SET_CUR
| UVC_CTRL_FLAG_GET_RANGE
| UVC_CTRL_FLAG_RESTORE,
},
};

再映射到 V4L2:

static struct uvc_control_mapping uvc_ctrl_mappings[] = {
{
.id = V4L2_CID_BRIGHTNESS,
.name = "Brightness",
.entity = UVC_GUID_UVC_PROCESSING,
.selector = UVC_PU_BRIGHTNESS_CONTROL,
.size = 16,
.offset = 0,
.v4l2_type = V4L2_CTRL_TYPE_INTEGER,
.data_type = UVC_CTRL_DATA_TYPE_SIGNED,
},
};

uvc_scan_device() 的作用是根据 dev->entities 建立完整的视频链路。

它会从 dev->entities 中找到 Output Terminal,然后分配一个 struct uvc_video_chain。接着根据各个 entity 的 baSourceID,从 Output Terminal 开始不断向上游回溯,依次找到 Processing Unit、Camera Terminal 等实体,并把它们加入到当前 chain 中。

最后,建立好的视频链路会被加入到 dev->chains 链表中,供后续 V4L2 设备注册和控制项处理使用。

举一个最常见的 USB 摄像头例子:

dev->entities 原来是这样:

Camera Terminal ID 1
Processing Unit ID 2
Output Terminal ID 3

其中连接关系是:

Processing Unit ID 2
baSourceID[0] = 1

Output Terminal ID 3
baSourceID[0] = 2

也就是:

Camera Terminal ID 1
|
Processing Unit ID 2
|
Output Terminal ID 3
最后得到:

dev->chains
|
+-> chain0
|
+-> chain0->dev = dev
|
+-> chain0->entities
| |
| +-> Output Terminal ID 3
| +-> Processing Unit ID 2
| +-> Camera Terminal ID 1
|
+-> chain0->processing = Processing Unit ID 2
|
+-> chain0->selector = NULL

uvc_register_chains() 的作用是把前面解析得到的 视频链路 chain 和 视频流 stream 关联起来,并最终注册成 V4L2 设备节点。

前面已经得到:

dev->entities
Camera Terminal
Processing Unit
Output Terminal

dev->chains
chain0:
Output Terminal -> Processing Unit -> Camera Terminal

dev->streams
stream0:
header.bTerminalLink = Output Terminal ID
format = MJPEG / YUYV
endpoint = 0x81

uvc_register_chains() 会遍历 dev->chains,在每条 chain 中找到连接到 VideoStreaming Interface 的 terminal,然后根据 terminal 的 ID 去 dev->streams 中找到对应的 stream。

找到以后,会执行:

stream->chain = chain;

这样就把视频流 stream0 和控制链路 chain0 绑定起来了。

接着,驱动会调用 uvc_register_video(dev, stream) 注册 V4L2 视频设备。在这个过程中,会初始化视频 buffer 队列,设置 video_device 的操作函数,并把 stream0 绑定到 /dev/video0 上。

最终形成的关系大致是:

/dev/video0
|
+-> video_device
|
+-> drvdata = stream0
+-> fops = uvc_fops
+-> ioctl_ops = uvc_ioctl_ops

stream0
|
+-> chain = chain0
+-> queue = 视频 buffer 队列
+-> format = MJPEG / YUYV
+-> endpoint = 0x81

chain0
|
+-> Camera Terminal
+-> Processing Unit
+-> Output Terminal

简单来说,uvc_register_chains() 就是把 UVC 控制链路、视频流接口和 /dev/videoX 设备节点 绑定起来,让用户空间可以通过 V4L2 访问摄像头

应用层

应用层操作 UVC 摄像头,大体可以分成两条线:

一条是控制项线,用于设置摄像头参数,例如亮度、对比度、曝光、白平衡等。

另一条是视频数据队列线,用于获取图像数据,例如申请 buffer、入队、开始采集、出队取帧等。

需要注意的是,应用层并不是直接操作 UVC 的 VideoControl Interface,而是通过 V4L2 ioctl 进入内核。进入内核后,先由 V4L2 框架分发到 UVC 驱动,再由 UVC 驱动转换成对应的 USB UVC 请求发送给摄像头。

整体关系可以理解为:

应用层 V4L2 ioctl
|
v
V4L2 框架
|
v
UVC 驱动
|
v
USB UVC 请求
|
v
USB 摄像头

一、控制项线

控制项线主要用于设置摄像头参数,例如亮度、曝光、白平衡等。

以设置亮度为例,应用层会使用 struct v4l2_control:

struct v4l2_control c;

c.id = V4L2_CID_BRIGHTNESS;
c.value = 99;

ioctl(fd, VIDIOC_S_CTRL, &c);

这表示应用层希望把摄像头亮度设置为 99。

在内核中,大致会形成下面的查找关系:

/dev/video0
|
+-> stream0
|
+-> stream0->chain = chain0
|
+-> Processing Unit ID 2
|
+-> struct uvc_control
|
+-> struct uvc_control_mapping
|
+-> V4L2_CID_BRIGHTNESS
+-> UVC_PU_BRIGHTNESS_CONTROL

控制项设置流程可以简化为:

VIDIOC_S_CTRL
|
v
V4L2_CID_BRIGHTNESS
|
v
找到对应的 uvc_control_mapping
|
v
找到 Processing Unit 中的 Brightness Control
|
v
发送 UVC SET_CUR 请求
|
v
通过 USB control message 设置摄像头亮度

所以,控制项线的本质是:

应用层传入的是“参数”
例如 brightness = 99
最后由 UVC 驱动转换成 USB control request
发送给摄像头

这条线中主要涉及的结构体有:

struct v4l2_control
struct uvc_video_chain
struct uvc_entity
struct uvc_control
struct uvc_control_mapping

二、视频数据队列线

视频数据队列线主要用于获取摄像头采集到的图像数据。

应用层常用的结构体有:

struct v4l2_requestbuffers req;
struct v4l2_buffer buf;

典型流程如下:

VIDIOC_REQBUFS
|
v
申请视频 buffer

VIDIOC_QBUF
|
v
把 buffer 交给内核

VIDIOC_STREAMON
|
v
开始视频采集

VIDIOC_DQBUF
|
v
取出一帧图像数据

在内核中,视频 buffer 主要由 stream0->queue 和 vb2_queue 管理:

stream0
|
+-> stream0->queue
|
+-> struct vb2_queue q
|
+-> q->bufs[]
| |
| +-> struct vb2_buffer
| |
| +-> struct vb2_plane
| 注释:真正描述视频数据内存
|
+-> q->queued_list
| 注释:应用 QBUF 进来的 buffer
|
+-> q->done_list
注释:已经采集完成,等待应用 DQBUF 的 buffer

UVC 驱动内部还会涉及:

struct uvc_video_queue
struct uvc_buffer
USB URB

视频数据流可以简化理解为:

USB 摄像头发送视频数据
|
v
USB URB 接收数据
|
v
UVC 驱动解析 payload
|
v
写入 vb2_plane 背后的内存
|
v
vb2_buffer_done()
|
v
buffer 进入 q->done_list
|
v
应用层通过 VIDIOC_DQBUF 取走这一帧

因此,这两条线可以总结为:

控制项线:
应用层传入的是“参数”
例如 brightness = 99
最后走 USB control request

视频队列线:
应用层传入的是“buffer”
USB 视频数据最终填充到 buffer 对应的 plane 内存中

赞(0)
未经允许不得转载:171主机测评 » i.MX6ULL 平台 V4L2 摄像头驱动框架解析
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址