夜雨聆风学习资料网

ARTICLE · 1293787

V4L2 源码深度解析(十七):从 Sensor 到 ISP 模块——v4l2_subdev 如何统一设备操作?

V4L2 源码深度解析(十七):从 Sensor 到 ISP 模块——v4l2_subdev 如何统一设备操作?

开篇:为什么摄像头系统需要 subdev?

一个摄像头系统通常不只有一个采集节点。图像可能依次经过 Sensor、接收端、颜色处理模块、缩放模块,最后才进入 Capture 队列。每个模块有自己的状态和操作,但又需要被桥接驱动以相对统一的方式配置和启动。

struct v4l2_subdev 是 V4L2 为这类子设备提供的通用接口对象。它不代表某一种具体硬件,也不等于 /dev/v4l-subdevX 这个设备节点。Sensor、Debayer、Scaler 可以使用同一种抽象,分别提供自己的操作表和私有状态。

本文以 v4l2-subdev.c 为框架主线,以 VIMC 的 vimc-common.c、vimc-sensor.c、vimc-debayer.c、vimc-scaler.c 为实例,讲清对象初始化、操作表分组、回调分发、驱动私有数据和子设备节点的边界。格式协商的完整规则留给后续专题,本篇只用它作为调用实例。

1. 先从真实问题理解 subdev

1.1 为什么不让桥接驱动直接调用每个模块的私有函数?

如果每个 Sensor、Debayer 和 Scaler 都使用完全不同的接口,桥接驱动就必须知道每个模块的内部函数名和私有结构。模块替换或增加时,连接代码会越来越难维护。

subdev 把常见操作组织成接口组,例如:

  • core:通用 ioctl、状态日志和事件相关操作;控制值的常规 ioctl 也会由子设备 ioctl 层调用控制框架处理,并非全部经由 core_ops;
  • video:视频流控制、帧间隔等操作;
  • pad:围绕 media pad 的格式枚举、格式设置、selection 等操作。

这不是说每个模块都必须实现全部回调。驱动只提供自己支持的操作;调用方需要考虑某个回调可能不存在。

1.2 subdev 不等于独立设备节点

内核中的 struct v4l2_subdev 是一个对象。只有驱动选择暴露节点、并完成相应节点注册后,才会出现可供应用打开的 /dev/v4l-subdevX。

VIMC 的 vimc_ent_sd_register() 设置了 V4L2_SUBDEV_FL_HAS_DEVNODE,但这只是后续注册节点的条件之一。真正创建节点发生在 __v4l2_device_register_subdev_nodes() 一类注册路径中,而不是 v4l2_subdev_init() 内部自动完成。

名称
代表什么
不代表什么
v4l2_subdev
内核中的通用子设备对象
不必然对应独立设备节点
media_entity
媒体拓扑中的实体
不等于一个 /dev 文件
/dev/v4l-subdevX
暴露子设备操作的用户空间入口
不等于整条图像链路

2. 先看 VIMC 的真实对象:通用层和私有层

VIMC 的 Sensor、Debayer 和 Scaler 都有自己的设备结构体。结构体里保存具体模块的状态,同时嵌入或关联 struct v4l2_subdev。通用层负责接口与框架关联,私有层负责模块自己的格式、帧缓存、控制值或处理状态。

在 vimc-common.c 中,vimc_ent_sd_register() 接收 struct v4l2_subdev *sd 和 const struct v4l2_subdev_ops *sd_ops。这意味着各个模块可以复用相同的注册辅助函数,但传入不同的操作表。

关键思想是:公共辅助函数负责共同的接入步骤,模块操作表决定具体能力,私有结构保存各模块的实际状态。 这三件事不要混为一谈。

3. v4l2_subdev_init() 实际初始化了什么?

框架函数 v4l2_subdev_init(sd, ops) 的工作相对克制。对本文所读实现,它会初始化子设备链表节点,保存 ops 指针,清空 v4l2_dev、flags、name、grp_id、dev_priv 和 host_priv 等字段;启用 Media Controller 时,还会设置 entity 的基础名称和对象类型。

它不会替驱动完成所有后续工作:不会自动填写模块名称,不会初始化 pad 数组,不会把子设备登记到 v4l2_device,也不会自动创建 /dev/v4l-subdevX。

VIMC 的 vimc_ent_sd_register() 可以概括为下面这段真实执行顺序(省略日志与错误处理):

ved->ent = &sd->entity;
v4l2_subdev_init(sd, sd_ops);
sd->entity.function = function;
sd->entity.ops = &vimc_ent_sd_mops;
sd->owner = THIS_MODULE;
strscpy(sd->name, name, sizeof(sd->name));
v4l2_set_subdevdata(sd, ved);
sd->flags |= V4L2_SUBDEV_FL_HAS_DEVNODE;

ret = media_entity_pads_init(&sd->entity, num_pads, pads);
if (ret)
return ret;

ret = v4l2_device_register_subdev(v4l2_dev, sd);
if (ret)
goto err_clean_m_ent;

这段代码说明,通用初始化只是第一步;VIMC 先把驱动自己的信息补齐,再初始化 media entity 的 pad,最后才登记子设备。实际源码还会根据是否存在 control handler 设置事件能力标志,并在登记失败时调用 media_entity_cleanup()。

VIMC 的 vimc_ent_sd_register() 在调用 v4l2_subdev_init() 后,继续完成这些工作:

  1. 将 entity.function 设置为模块对应的功能类型,并设置实体操作;
  2. 设置 owner、名称,并用 v4l2_set_subdevdata() 关联驱动对象;
  3. 根据该模块的能力设置 V4L2_SUBDEV_FL_HAS_DEVNODE 等标志;
  4. 调用 media_entity_pads_init() 初始化实体的 pad;
  5. 调用 v4l2_device_register_subdev() 把子设备登记到所属 V4L2 设备。

如果 pad 初始化失败,注册尚未发生;如果子设备注册失败,VIMC 会清理已经初始化的 media entity。资源回滚要按“已经成功完成的步骤”处理,不能无条件调用所有清理函数。

4. struct v4l2_subdev_ops:回调按职责分组

struct v4l2_subdev_ops 本身是分组表,常见成员指向不同类别的操作表。具体字段以当前内核配套头文件为准;本篇从源码中实际用到的 core、video、pad 三组讲起。

以 VIMC Sensor 为例,操作表采用分组初始化,而不是把所有函数塞进同一个扁平表:

staticconststructv4l2_subdev_opsvimc_sensor_ops = {
    .core  = &vimc_sensor_core_ops,
    .pad   = &vimc_sensor_pad_ops,
    .video = &vimc_sensor_video_ops,
};

这段表只把三个操作组连接起来。具体函数指针在 vimc_sensor_core_ops、vimc_sensor_pad_ops 和 vimc_sensor_video_ops 中定义;如果调用的是 pad.set_fmt,不会因为 video.s_stream 存在就自动调用流控制函数。

4.1 core_ops

这组用于通用 ioctl、状态日志和事件相关操作。VIMC 的 Sensor、Debayer 会挂接控制状态日志、事件订阅等辅助函数。需要区分:VIDIOC_G_CTRL、VIDIOC_S_CTRL 等常见控制命令在这份 subdev_do_ioctl() 中由 V4L2 控制框架处理,并不意味着所有控制值都通过 core_ops 中的同名回调完成。某个驱动没有实现某项操作,也不代表框架会替它模拟出硬件功能。

4.2 video_ops

这组包括视频相关操作,例如 s_stream 和帧间隔操作。VIMC 的 Sensor、Debayer、Scaler 都提供自己的 s_stream,但实现不同:Sensor 准备测试图样帧缓存,Debayer/Scaler 则按各自格式和处理需求分配中间帧存储。

4.3 pad_ops

这组面向 media pad,包含格式枚举、get_fmt、set_fmt,以及 Scaler 使用的 selection 操作。Pad 操作让框架和桥接驱动可以针对实体的输入或输出端口配置格式,而不是把整条链路简化成一个全局像素格式。

回调表只声明“支持哪些操作”,不等于回调已经执行。 真正的执行发生在框架调用对应函数指针时。

5. v4l2_subdev_call() 怎样找到实际实现?

桥接驱动不必直接调用 vimc_sensor_set_fmt() 这样的私有函数。它可以通过 v4l2_subdev_call(sd, pad, set_fmt, state, format) 请求框架调用子设备的 pad 操作。

可以把分发过程理解成四步:

  1. 找到目标 v4l2_subdev;
  2. 选择操作类别,例如 pad;
  3. 检查对应操作表和回调是否存在;
  4. 存在时调用驱动函数,不存在时按框架约定返回“不支持该操作”的错误。

在这份实现配套的调用宏中,缺少回调会以 -ENOIOCTLCMD 表示。上层是否把它转换为其他错误、忽略可选操作或继续处理,要看具体调用点,不能一概说成“所有缺失回调都导致整个设备失败”。

6. 驱动私有数据:v4l2_get_subdevdata() 与 container_of()

框架回调接收到的参数通常是 struct v4l2_subdev *sd,但模块真正需要访问的可能是 struct vimc_sensor_device 或 struct vimc_scaler_device。驱动必须从通用对象找到自己的私有状态。

VIMC 公共注册辅助函数用 v4l2_set_subdevdata(sd, ved) 保存驱动关联指针;相应回调可以通过 v4l2_get_subdevdata(sd) 取回。若 v4l2_subdev 是外层结构的嵌入成员,也可以使用 container_of() 从成员地址还原外层对象。

两种方式表达的关系不同:

  • v4l2_get_subdevdata() 取的是显式保存到 subdev 数据槽中的指针;
  • container_of() 根据已知成员在外层结构中的偏移计算外层对象地址。

不要不看结构体定义就把两者互换。具体实例中采用哪种方式,以对应驱动函数为准。

7. 用 VIMC 看三个模块怎样实现同一接口

7.1 Sensor:格式、测试图样和流控制

vimc_sensor_ops 将 vimc_sensor_pad_ops、vimc_sensor_video_ops 和 vimc_sensor_core_ops 组合起来。get_fmt/set_fmt 维护 Sensor 的 media-bus 格式;s_stream 在启流时分配测试图样帧缓存并配置 TPG,停流时释放该缓存。

VIMC Sensor 是软件模拟源,不是真实 I²C Sensor。它适合说明接口组织方式,但不能用来证明寄存器配置、电源时序或 MIPI CSI-2 传输。

7.2 Debayer:同样的接口,不同的处理职责

Debayer 同样提供 pad、video 和 core 操作。其 s_stream 为处理路径准备中间帧存储,pad 回调负责输入输出格式的约束。它的实现与 Sensor 不同,但桥接方仍通过统一的 subdev 操作入口调用它。

7.3 Scaler:除了格式,还有选区

Scaler 除了 pad 格式操作,还实现 get_selection/set_selection。设置裁剪区域时,会检查 pad 和 target,并根据当前选择的是 TRY 还是 ACTIVE 状态访问对应配置。正在运行、已有中间帧存储时,部分 ACTIVE 修改会被拒绝。

这正体现了 subdev 的设计:公共框架提供接口约定,各模块按实际能力实现对应回调,并维护自己的约束。

8. TRY 与 ACTIVE:本篇需要知道的最小背景

子设备的格式操作常带有 which,典型值为 TRY 和 ACTIVE。

  • TRY 用于试探一组候选设置。它通常与当前打开文件关联的 subdev state 有关,不应当被误认为已经改变了硬件当前配置。
  • ACTIVE 表示当前生效配置,通常由驱动或子设备状态管理代码维护;修改可能受到运行状态、格式约束和链路条件限制。

本篇源码的 subdev_open() 会在启用 subdev API 时分配 v4l2_subdev_fh,为其创建状态对象,并将 v4l2_fh 放入 file->private_data。subdev_do_ioctl_lock() 会按命令选择状态,必要时获取状态锁,再进入具体 ioctl 处理。

需要注意:不同内核版本对 subdev state 的 API 和保存方式有演进。本文只解释这份源码中实际使用的接口,不把其他版本的 active_state 结构或新式 helper 强行套入。

9. 子设备节点的 open/ioctl 不是普通视频采集路径的复制

当子设备节点注册成功后,它使用 v4l2_subdev_fops。在启用相应配置时,打开路径会分配 v4l2_subdev_fh、初始化标准 v4l2_fh,并设置 file->private_data。关闭路径则调用内部 close(若存在)、清理句柄与状态对象。

ioctl 入口使用 video_usercopy() 处理参数,再进入 subdev_do_ioctl_lock() 和 subdev_do_ioctl()。其中标准的子设备命令会转换成对应的 v4l2_subdev_call(),例如 VIDIOC_SUBDEV_G_FMT 会调用 pad 的 get_fmt。

这与前面普通 /dev/videoX 的 video_ioctl2() 路径有联系,但不能说两者完全相同。子设备节点有自己的 ioctl 命令集合、状态选择和锁路径。

10. 生命周期:初始化、注册、使用、注销要分开

结合 VIMC,生命周期可以概括为:

  1. 驱动分配或准备自己的私有对象;
  2. v4l2_subdev_init() 初始化通用子设备字段;
  3. 驱动填写名称、owner、私有数据和操作表关联;
  4. 初始化 media entity 与 pads(若使用 Media Controller);
  5. v4l2_device_register_subdev() 完成组织登记;
  6. 如需独立节点,再由子设备节点注册流程创建节点;
  7. 运行时由桥接驱动或用户接口调用子设备操作;
  8. 退出时先注销子设备、撤销媒体实体关系,再清理实体和驱动私有资源。

具体错误路径要按已成功执行的步骤回滚。例如 pad 初始化失败时,不能假设子设备已经登记;子设备登记失败后,需要清理已经建立的 entity 状态。节点注册与子设备注册也是两个不同阶段。

11. 阅读源码时容易出现的四种误解

  1. 把 v4l2_subdev_init() 当成完整注册。 它只是初始化通用字段;pad、entity、设备组织登记和 devnode 是后续步骤。
  2. 认为每个 subdev 都必须有 /dev/v4l-subdevX。 节点暴露是可选能力,需要对应标志和节点注册流程。
  3. 认为所有模块都实现全部 core/video/pad 回调。 操作表允许按能力提供部分回调;缺失回调的处理取决于调用点。
  4. 把 VIMC 当成真实 Sensor。 VIMC 用软件测试图样和内存处理模拟模块关系,不能用于证明真实硬件的供电、寄存器和总线时序。

12. 本篇小结与下一篇衔接

这一篇最重要的结论是:v4l2_subdev 统一的是子设备操作的表达方式,不是把 Sensor、ISP 和 Scaler 变成同一种硬件。每个模块仍有自己的私有状态、支持的回调和运行约束;框架负责组织对象、提供通用分发,并与 Media Controller 和子设备节点机制协作。

下一篇可以进一步分析 Media Entity、Pad 与 Link:当子设备已经注册并拥有自己的操作接口后,框架怎样把这些实体连接成一张媒体拓扑图。

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料