3. V4L2核心数据结构:初始化与注册
V4L2(Video for Linux 2)框架通过四个核心数据结构将摄像头设备、子设备、视频节点和缓冲区管理串联起来。理解它们的初始化顺序与注册机制,是编写稳定摄像头驱动的基石。本章逐一剖析 struct video_device、struct v4l2_device、struct v4l2_subdev 和 struct vb2_queue 的生命周期与操作要点。
核心原则: 注册顺序必须遵循“自底向上”原则——先注册 v4l2_device,再注册 subdev,最后注册 video_device;vb2_queue 在 video_device 注册前完成初始化并与 video_device 关联。
3.1 struct v4l2_device:顶层容器
struct v4l2_device 是整个 V4L2 设备的顶层抽象,代表一个物理或逻辑摄像头设备。它负责管理所有子设备(subdev)和视频节点(video_device),并提供统一的 sysfs 接口。
3.1.1 核心字段
| 字段 | 类型 | 说明 |
|---|---|---|
name | char[32] | 设备名称,用于 sysfs 和日志 |
dev | struct device * | 指向父设备(通常是 platform_device 或 i2c_client) |
subdevs | struct list_head | 已注册子设备链表头 |
ctrl_handler | struct v4l2_ctrl_handler * | 全局控制处理器(可选) |
3.1.2 初始化与注册
#include <media/v4l2-device.h>
struct v4l2_device v4l2_dev;
/* 1. 初始化 v4l2_device */
v4l2_device_init(&v4l2_dev, parent_dev);
/* 2. 注册(通常在 probe 函数末尾) */
ret = v4l2_device_register(parent_dev, &v4l2_dev);
if (ret < 0) {
dev_err(parent_dev, "Failed to register v4l2_device\n");
return ret;
}
/* 3. 注销(在 remove 函数中) */
v4l2_device_unregister(&v4l2_dev);
注意:
v4l2_device_register() 不会自动创建 video 节点,它只建立设备层次结构。video 节点由 video_register_device() 单独创建。
3.2 struct v4l2_subdev:子设备抽象
摄像头传感器、ISP、AF 控制器等硬件模块都抽象为 struct v4l2_subdev。它通过 v4l2_subdev_ops 暴露操作接口,并挂载到 v4l2_device 的 subdev 链表上。
3.2.1 初始化流程
#include <media/v4l2-subdev.h>
struct v4l2_subdev sd;
struct v4l2_subdev_ops sensor_ops = {
.core = &sensor_core_ops,
.video = &sensor_video_ops,
.pad = &sensor_pad_ops,
};
/* 1. 初始化 subdev */
v4l2_subdev_init(&sd, &sensor_ops);
sd.entity.function = MEDIA_ENT_F_CAM_SENSOR;
snprintf(sd.name, sizeof(sd.name), "sensor_%s", dev_name(client));
/* 2. 设置私有数据(可选) */
sd.dev = &client->dev;
v4l2_set_subdevdata(&sd, sensor_priv);
/* 3. 注册到 v4l2_device */
ret = v4l2_device_register_subdev(&v4l2_dev, &sd);
if (ret < 0) {
dev_err(&client->dev, "Failed to register subdev\n");
return ret;
}
3.2.2 子设备操作集
| 操作集 | 用途 | 典型回调 |
|---|---|---|
core | 核心控制 | s_power, ioctl, log_status |
video | 视频流控制 | s_stream, g_frame_interval |
pad | Pad 级操作 | get_fmt, set_fmt, enum_mbus_code |
sensor | 传感器专用 | g_skip_frames, s_exposure |
3.3 struct video_device:用户空间接口
struct video_device 是用户空间通过 /dev/videoX 访问的实体。它封装了 file_operations、ioctl 分发和 v4l2_ioctl_ops。
3.3.1 关键字段与初始化
#include <media/v4l2-dev.h>
#include <media/v4l2-ioctl.h>
struct video_device vdev;
/* 1. 设置 ioctl 操作集 */
vdev.ioctl_ops = &camera_ioctl_ops;
vdev.release = video_device_release_empty; /* 或自定义 release */
/* 2. 设置设备类型 */
vdev.device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING;
vdev.vfl_dir = VFL_DIR_RX; /* 捕获方向 */
/* 3. 关联 v4l2_device */
vdev.v4l2_dev = &v4l2_dev;
/* 4. 设置队列(见 3.4 节) */
vdev.queue = &vb2_q;
/* 5. 注册 video 节点 */
ret = video_register_device(&vdev, VFL_TYPE_VIDEO, -1);
if (ret < 0) {
dev_err(v4l2_dev.dev, "Failed to register video device\n");
return ret;
}
3.3.2 注册参数
| 参数 | 说明 |
|---|---|
VFL_TYPE_VIDEO | 标准视频设备,生成 /dev/videoX |
VFL_TYPE_VBI | VBI 设备,生成 /dev/vbiX |
VFL_TYPE_RADIO | 收音机设备,生成 /dev/radioX |
-1(次设备号) | 由内核自动分配,推荐使用 |
3.4 struct vb2_queue:缓冲区管理引擎
struct vb2_queue 是 V4L2 内存管理的核心,负责 buffer 的分配、入队、出队和 DMA 操作。它通过 vb2_ops 回调与驱动交互。
3.4.1 初始化与回调
#include <media/videobuf2-v4l2.h>
#include <media/videobuf2-dma-contig.h>
struct vb2_queue vb2_q;
/* 1. 设置队列参数 */
vb2_q.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
vb2_q.io_modes = VB2_MMAP | VB2_USERPTR | VB2_DMABUF;
vb2_q.drv_priv = &camera_priv;
vb2_q.buf_struct_size = sizeof(struct camera_buffer);
vb2_q.ops = &camera_vb2_ops;
vb2_q.mem_ops = &vb2_dma_contig_memops;
vb2_q.timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC;
/* 2. 初始化 vb2_queue */
ret = vb2_queue_init(&vb2_q);
if (ret < 0) {
dev_err(dev, "vb2_queue_init failed\n");
return ret;
}
/* 3. 在 video_device 注册前关联 */
vdev.queue = &vb2_q;
3.4.2 vb2_ops 回调表
| 回调 | 触发时机 | 驱动职责 |
|---|---|---|
queue_setup | 用户调用 REQBUFS | 计算 buffer 数量和大小,分配内存 |
buf_prepare | buffer 入队前 | 验证格式,准备 DMA 映射 |
buf_queue | buffer 入队 | 将 buffer 加入硬件队列,启动传输 |
start_streaming | 流启动 | 开启硬件时钟、MIPI 信号 |
stop_streaming | 流停止 | 停止硬件,取消未完成 buffer |
3.5 整体初始化顺序
以下 SVG 图展示了四个核心数据结构的初始化与注册时序,以及它们之间的依赖关系。
3.6 常见错误与避坑
⚠️ 常见错误:
- 注册顺序颠倒: 先注册 video_device 再注册 v4l2_device,导致 video_device 无法找到父设备,sysfs 链接断裂。
- vb2_queue 未关联: 忘记将
vdev.queue指向已初始化的 vb2_queue,用户调用REQBUFS时内核崩溃。 - release 回调未设置:
vdev.release为 NULL,设备注销时内核 panic。 - subdev 重复注册: 同一个 subdev 被多次调用
v4l2_device_register_subdev(),导致链表损坏。
3.7 完整初始化示例(伪代码)
static int camera_probe(struct platform_device *pdev)
{
struct camera_priv *priv;
int ret;
/* 1. 分配私有结构体 */
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
/* 2. 初始化 v4l2_device */
ret = v4l2_device_register(&pdev->dev, &priv->v4l2_dev);
if (ret)
return ret;
/* 3. 初始化并注册 subdev(传感器) */
v4l2_subdev_init(&priv->sensor, &sensor_ops);
priv->sensor.entity.function = MEDIA_ENT_F_CAM_SENSOR;
ret = v4l2_device_register_subdev(&priv->v4l2_dev, &priv->sensor);
if (ret)
goto err_unregister_v4l2;
/* 4. 初始化 vb2_queue */
priv->vb2_q.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
priv->vb2_q.io_modes = VB2_MMAP;
priv->vb2_q.ops = &camera_vb2_ops;
priv->vb2_q.mem_ops = &vb2_dma_contig_memops;
priv->vb2_q.drv_priv = priv;
ret = vb2_queue_init(&priv->vb2_q);
if (ret)
goto err_unregister_subdev;
/* 5. 初始化 video_device */
priv->vdev.ioctl_ops = &camera_ioctl_ops;
priv->vdev.release = video_device_release_empty;
priv->vdev.v4l2_dev = &priv->v4l2_dev;
priv->vdev.queue = &priv->vb2_q;
priv->vdev.device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING;
ret = video_register_device(&priv->vdev, VFL_TYPE_VIDEO, -1);
if (ret)
goto err_vb2_release;
platform_set_drvdata(pdev, priv);
return 0;
err_vb2_release:
/* vb2_queue 不需要显式释放,随 priv 释放 */
err_unregister_subdev:
v4l2_device_unregister_subdev(&priv->sensor);
err_unregister_v4l2:
v4l2_device_unregister(&priv->v4l2_dev);
return ret;
}
调试技巧: 注册完成后,检查
/sys/class/video4linux/videoX/device/ 是否指向正确的父设备。如果链接缺失,说明 v4l2_device 与 video_device 的关联未正确建立。