3. Subdev注册流程

Subdev 的注册是 V4L2 框架中连接主设备(video device)与子设备(subdev)的核心步骤。本章深入分析 v4l2_device_register、v4l2_subdev_init 和 v4l2_device_register_subdev 三个关键函数的调用链,揭示从设备节点创建到 subdev 挂载的完整过程。

核心要点: 注册流程遵循“先主后子”原则——先初始化 v4l2_device 作为容器,再初始化 subdev 实例,最后将 subdev 挂载到主设备上。任何顺序错误都会导致内核崩溃或设备不可用。

3.1 整体调用链概览

下图展示了 subdev 注册的完整调用链,包含三个主要阶段:

Subdev 注册完整调用链 v4l2_device_register 分配 v4l2_device 结构体 初始化锁与链表头 关联 struct device 父设备 v4l2_subdev_init 初始化 v4l2_subdev 字段 设置 subdev 操作函数集 初始化内部链表节点 v4l2_device_register_subdev 检查 subdev 有效性 挂载到 subdev 链表 触发 subdev 通知 注意:v4l2_subdev_init 仅初始化软件结构,不涉及硬件交互;硬件初始化通常在 subdev 的 probe 回调中完成

3.2 v4l2_device_register:主设备容器初始化

该函数是注册流程的起点,负责创建 V4L2 设备实例并初始化内部数据结构。其原型如下:

int v4l2_device_register(struct device *dev, struct v4l2_device *v4l2_dev);

参数说明:

参数 类型 描述
dev struct device * 父设备指针,通常来自 platform_device 或 i2c_client
v4l2_dev struct v4l2_device * 待初始化的 v4l2_device 实例

内部执行步骤:

  1. 清零与赋值: 将 v4l2_dev 结构体清零,设置 dev 指针指向父设备。
  2. 初始化锁: 初始化 spin_lock 用于保护 subdev 链表操作。
  3. 初始化链表头: 初始化 subdevs 链表头,后续所有 subdev 将挂载到此链表上。
  4. 设置私有数据: 如果驱动需要,可通过 v4l2_dev->priv 存储私有数据指针。
工程经验: 在嵌入式系统中,v4l2_device_register 通常在 probe 函数中调用。如果父设备是 I2C 设备,dev 参数应传入 &client->dev;如果是 platform 设备,则传入 &pdev->dev。

3.3 v4l2_subdev_init:子设备软件初始化

该函数完成 subdev 结构体的软件初始化,不涉及硬件寄存器操作。其原型为:

void v4l2_subdev_init(struct v4l2_subdev *sd, const struct v4l2_subdev_ops *ops);

初始化内容清单:

字段 初始值 说明
sd->ops 传入的 ops 指针 设置 subdev 操作函数集(core/video/pad 等)
sd->v4l2_dev NULL 尚未关联主设备,后续由 register 函数设置
sd->list INIT_LIST_HEAD 初始化链表节点,准备挂载
sd->flags 0 清除所有标志位
sd->name 空字符串 驱动需在后续设置设备名称
常见错误: 驱动开发者常忘记在 v4l2_subdev_init 之后设置 sd->name,导致调试时无法区分多个 subdev。建议在 init 后立即调用 snprintf(sd->name, sizeof(sd->name), "sensor-%s", dev_name(dev))。

3.4 v4l2_device_register_subdev:挂载与通知

这是注册流程的最后一步,将已初始化的 subdev 挂载到主设备上。其原型为:

int v4l2_device_register_subdev(struct v4l2_device *v4l2_dev, struct v4l2_subdev *sd);

执行流程:

  1. 有效性检查: 检查 sd 是否为 NULL,sd->ops 是否已设置。
  2. 关联主设备: 设置 sd->v4l2_dev = v4l2_dev,建立双向引用。
  3. 链表操作: 调用 list_add_tail(&sd->list, &v4l2_dev->subdevs) 将 subdev 加入链表尾部。
  4. 通知回调: 如果 v4l2_dev->notify 回调已注册,则调用 notify(sd, V4L2_DEVICE_NOTIFY_SUBDEV_REGISTER) 通知上层驱动。
  5. 返回结果: 成功返回 0,失败返回负错误码。
// 典型注册代码片段
struct v4l2_device v4l2_dev;
struct v4l2_subdev sd;

// 步骤1:注册主设备
v4l2_device_register(&pdev->dev, &v4l2_dev);

// 步骤2:初始化 subdev
v4l2_subdev_init(&sd, &sensor_ops);
snprintf(sd.name, sizeof(sd.name), "sensor-%s", dev_name(&pdev->dev));

// 步骤3:挂载 subdev
ret = v4l2_device_register_subdev(&v4l2_dev, &sd);
if (ret < 0) {
    dev_err(&pdev->dev, "failed to register subdev: %d\n", ret);
    goto err_unregister;
}

3.5 注册流程状态机

下图以状态机形式展示 subdev 注册过程中各阶段的状态转换:

Subdev 注册状态机 未初始化 sd 结构体未分配 v4l2_subdev_init 已初始化 ops 已设置,未挂载 register_subdev 已挂载 在 subdevs 链表中 注册失败 错误状态 unregister_subdev 已注销 状态转换说明:未初始化 → 已初始化(v4l2_subdev_init)→ 已挂载(v4l2_device_register_subdev) 错误状态可通过重新 init 恢复;已注销状态不可逆,需重新分配结构体

3.6 注册流程对比表

下表对比三个核心函数的作用域、调用时机和关键行为:

函数 作用域 调用时机 关键行为 返回值
v4l2_device_register 主设备层 probe 函数最前 初始化 v4l2_device 容器 0 成功 / 负值失败
v4l2_subdev_init 子设备层 probe 函数中段 初始化 subdev 软件结构 void(无返回值)
v4l2_device_register_subdev 主设备→子设备 probe 函数后段 挂载 subdev 到链表 0 成功 / 负值失败
工程实践建议:
  • 始终按照“主设备 → subdev init → register subdev”的顺序调用,不可颠倒。
  • 在 v4l2_device_register_subdev 之后检查返回值,失败时需要调用 v4l2_device_unregister 清理主设备。
  • 对于多个 subdev 的场景,建议使用数组或链表管理 subdev 实例,并在 remove 函数中逆序注销。

3.7 常见问题与调试方法

问题1:注册后 subdev 不可见

  • 原因: v4l2_device_register_subdev 返回成功,但 sd->v4l2_dev 为 NULL。
  • 排查: 检查是否在 v4l2_subdev_init 之后错误地清零了 sd 结构体。

问题2:多次注册导致链表损坏

  • 原因: 同一个 sd 实例被多次调用 v4l2_device_register_subdev。
  • 排查: 在注册前检查 sd->v4l2_dev 是否为 NULL,非 NULL 表示已注册。

问题3:注销时内存泄漏

  • 原因: 只调用了 v4l2_device_unregister,未先调用 v4l2_device_unregister_subdev 从链表中移除 subdev。
  • 排查: 使用 list_for_each_entry_safe 遍历 subdevs 链表,逐个注销。
调试技巧: 在 /sys/kernel/debug/v4l2 目录下可以查看已注册的 subdev 列表。使用 cat /sys/kernel/debug/v4l2/devices 检查 subdev 是否成功挂载。