3. Subdev 注册流程:v4l2_device_register、v4l2_subdev_init、v4l2_device_register_subdev 函数调用链
Subdev 的注册是 V4L2 框架中连接主设备(video device)与子设备(subdev)的核心步骤。本章深入分析 v4l2_device_register、v4l2_subdev_init 和 v4l2_device_register_subdev 三个关键函数的调用链,揭示从设备节点创建到 subdev 挂载的完整过程。
核心要点: 注册流程遵循“先主后子”原则——先初始化 v4l2_device 作为容器,再初始化 subdev 实例,最后将 subdev 挂载到主设备上。任何顺序错误都会导致内核崩溃或设备不可用。
3.1 整体调用链概览
下图展示了 subdev 注册的完整调用链,包含三个主要阶段:
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 实例 |
内部执行步骤:
- 清零与赋值: 将
v4l2_dev结构体清零,设置dev指针指向父设备。 - 初始化锁: 初始化
spin_lock用于保护 subdev 链表操作。 - 初始化链表头: 初始化
subdevs链表头,后续所有 subdev 将挂载到此链表上。 - 设置私有数据: 如果驱动需要,可通过
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);
执行流程:
- 有效性检查: 检查
sd是否为 NULL,sd->ops是否已设置。 - 关联主设备: 设置
sd->v4l2_dev = v4l2_dev,建立双向引用。 - 链表操作: 调用
list_add_tail(&sd->list, &v4l2_dev->subdevs)将 subdev 加入链表尾部。 - 通知回调: 如果
v4l2_dev->notify回调已注册,则调用notify(sd, V4L2_DEVICE_NOTIFY_SUBDEV_REGISTER)通知上层驱动。 - 返回结果: 成功返回 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 注册过程中各阶段的状态转换:
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 是否成功挂载。