RK3588:MIPI底层驱动学习——入门第四篇(驱动精华:OV13855驱动加载时究竟发生了什么?)

引言

上文我们基本理清了ov13855.c代码的基本框架,以及V4L2和IIC两个驱动框架是如何共存的。

但是,终归是框架性的内容,理解起来比较方便,重点内容比如probe函数的启动流程还是需要详细讲解一下的。

文件路径: drivers/media/i2c/ov13855.c


OV13855 probe函数详解(按三大任务组织)

文件路径 : drivers/media/i2c/ov13855.c

函数入口与基础准备

c 复制代码
static int ov13855_probe(struct i2c_client *client,
			 const struct i2c_device_id *id)
{
	struct device *dev = &client->dev;
	struct device_node *node = dev->of_node;
	struct ov13855 *ov13855;
	struct v4l2_subdev *sd;
	char facing[2];
	int ret;

	dev_info(dev, "driver version: %02x.%02x.%02x",
		DRIVER_VERSION >> 16,
		(DRIVER_VERSION & 0xff00) >> 8,
		DRIVER_VERSION & 0x00ff);

	ov13855 = devm_kzalloc(dev, sizeof(*ov13855), GFP_KERNEL);
	if (!ov13855)
		return -ENOMEM;

基础准备阶段 :分配驱动私有数据结构,这个结构体将贯穿整个probe流程,存储所有硬件资源句柄和状态信息。
probe函数执行后,内存中的数据结构

bash 复制代码
ov13855 (私有数据)
├── client (I2C客户端)
├── subdev (V4L2子设备)
│   ├── ops → ov13855_subdev_ops
│   ├── ctrl_handler → 控制器集合
│   └── entity (media实体)
│       └── pad[0] (输出pad)
├── xvclk (时钟句柄)
├── power_gpio (电源GPIO)
├── reset_gpio (复位GPIO)
└── cur_mode → supported_modes[0]

任务一:硬件资源准备

1. 硬件上电时序背景

sensor上电的物理要求

复制代码
1. 供电顺序: AVDD(模拟电源) → DOVDD(IO电源) → DVDD(数字电源)
2. 时钟稳定: XVCLK必须在24MHz稳定运行
3. 复位时序: RESET保持低电平足够时间后拉高
4. 稳定延时: 需要8192个时钟周期让内部电路稳定
5. I2C就绪: 之后才能进行I2C通信

上面这个看看就行

2. 读取DTS中的模块配置

c 复制代码
	ret = of_property_read_u32(node, RKMODULE_CAMERA_MODULE_INDEX,
				   &ov13855->module_index);
	ret |= of_property_read_string(node, RKMODULE_CAMERA_MODULE_FACING,
				       &ov13855->module_facing);
	ret |= of_property_read_string(node, RKMODULE_CAMERA_MODULE_NAME,
				       &ov13855->module_name);
	ret |= of_property_read_string(node, RKMODULE_CAMERA_LENS_NAME,
				       &ov13855->len_name);
	if (ret) {
		dev_err(dev, "could not get module information!\n");
		return -EINVAL;
	}

	ov13855->client = client;
	ov13855->cur_mode = &supported_modes[0];

作用:从设备树读取硬件模块的身份信息,保存I2C客户端指针用于后续寄存器访问,设置默认输出模式。

3. 获取时钟资源

c 复制代码
	ov13855->xvclk = devm_clk_get(dev, "xvclk");
	if (IS_ERR(ov13855->xvclk)) {
		dev_err(dev, "Failed to get xvclk\n");
		return -EINVAL;
	}

硬件连接:XVCLK由SoC的时钟控制器提供,通常是24MHz方波。sensor的所有内部时序都依赖这个时钟,包括MIPI输出、寄存器访问等。

4. 获取GPIO控制引脚

c 复制代码
	ov13855->power_gpio = devm_gpiod_get(dev, "power", GPIOD_OUT_LOW);
	if (IS_ERR(ov13855->power_gpio))
		dev_warn(dev, "Failed to get power-gpios, maybe no use\n");

	ov13855->reset_gpio = devm_gpiod_get(dev, "reset", GPIOD_OUT_LOW);
	if (IS_ERR(ov13855->reset_gpio))
		dev_warn(dev, "Failed to get reset-gpios\n");

	ov13855->pwdn_gpio = devm_gpiod_get(dev, "pwdn", GPIOD_OUT_LOW);
	if (IS_ERR(ov13855->pwdn_gpio))
		dev_warn(dev, "Failed to get pwdn-gpios\n");

三个GPIO的硬件作用

  • power_gpio:控制LDO使能,决定是否给sensor供电
  • reset_gpio:硬件复位线,低电平时sensor内部寄存器复位
  • pwdn_gpio:掉电模式控制,低电平进入超低功耗模式

初始状态GPIOD_OUT_LOW:获取GPIO的同时设为低电平,此时sensor处于关闭或复位状态。

5. 配置电源调节器

c 复制代码
	ret = ov13855_configure_regulators(ov13855);
	if (ret) {
		dev_err(dev, "Failed to get power regulators\n");
		return ret;
	}

电源调节器背景:sensor需要三路独立电源------AVDD(2.8V模拟)、DOVDD(1.8V IO)、DVDD(1.2V数字核心)。这个函数批量获取这些regulator的控制句柄。

ov13855_configure_regulators实现:(第二篇讲解设备树的时候也有简单讲过)

c 复制代码
static int ov13855_configure_regulators(struct ov13855 *ov13855)
{
	unsigned int i;

	for (i = 0; i < OV13855_NUM_SUPPLIES; i++)
		ov13855->supplies[i].supply = ov13855_supply_names[i];

	return devm_regulator_bulk_get(&ov13855->client->dev,
				       OV13855_NUM_SUPPLIES,
				       ov13855->supplies);
}

遍历电源名称数组,批量获取regulator,存入ov13855->supplies[]数组。

6. 获取引脚复用配置

c 复制代码
	ov13855->pinctrl = devm_pinctrl_get(dev);
	if (!IS_ERR(ov13855->pinctrl)) {
		ov13855->pins_default =
			pinctrl_lookup_state(ov13855->pinctrl,
					     OF_CAMERA_PINCTRL_STATE_DEFAULT);
		if (IS_ERR(ov13855->pins_default))
			dev_err(dev, "could not get default pinstate\n");

		ov13855->pins_sleep =
			pinctrl_lookup_state(ov13855->pinctrl,
					     OF_CAMERA_PINCTRL_STATE_SLEEP);
		if (IS_ERR(ov13855->pins_sleep))
			dev_err(dev, "could not get sleep pinstate\n");
	}

引脚复用管理:SoC的引脚是多功能的,同一个引脚可能是GPIO、I2C、SPI等。pinctrl子系统确保相关引脚配置为摄像头功能。获取"default"(工作态)和"sleep"(休眠态)两种配置。

7. 初始化同步机制

c 复制代码
mutex_init(&ov13855->mutex);

并发保护:创建互斥锁保护sensor状态变量。多个进程可能同时调用驱动接口(如开流、设置参数),mutex确保同一时刻只有一个操作在访问硬件寄存器。

8. 硬件上电

c 复制代码
	ret = __ov13855_power_on(ov13855);
	if (ret)
		goto err_free_handler;

__ov13855_power_on的上电时序实现

c 复制代码
static int __ov13855_power_on(struct ov13855 *ov13855)
{
	int ret;
	u32 delay_us;
	struct device *dev = &ov13855->client->dev;

	// 1. 使能主电源GPIO
	if (!IS_ERR(ov13855->power_gpio))
		gpiod_set_value_cansleep(ov13855->power_gpio, 1);

	usleep_range(1000, 2000);

	// 2. 切换pinctrl到工作状态
	if (!IS_ERR_OR_NULL(ov13855->pins_default)) {
		ret = pinctrl_select_state(ov13855->pinctrl,
					   ov13855->pins_default);
		if (ret < 0)
			dev_err(dev, "could not set pins\n");
	}
	
	// 3. 设置并启动外部时钟
	ret = clk_set_rate(ov13855->xvclk, OV13855_XVCLK_FREQ);
	if (ret < 0)
		dev_warn(dev, "Failed to set xvclk rate (24MHz)\n");
	
	ret = clk_prepare_enable(ov13855->xvclk);
	if (ret < 0) {
		dev_err(dev, "Failed to enable xvclk\n");
		return ret;
	}
	
	// 4. 释放复位信号(拉低)
	if (!IS_ERR(ov13855->reset_gpio))
		gpiod_set_value_cansleep(ov13855->reset_gpio, 0);

	// 5. 使能regulator电源
	ret = regulator_bulk_enable(OV13855_NUM_SUPPLIES, ov13855->supplies);
	if (ret < 0) {
		dev_err(dev, "Failed to enable regulators\n");
		goto disable_clk;
	}

	// 6. 拉高复位信号,sensor开始工作
	if (!IS_ERR(ov13855->reset_gpio))
		gpiod_set_value_cansleep(ov13855->reset_gpio, 1);

	usleep_range(5000, 6000);
	
	// 7. 退出掉电模式
	if (!IS_ERR(ov13855->pwdn_gpio))
		gpiod_set_value_cansleep(ov13855->pwdn_gpio, 1);

	// 8. 等待sensor内部稳定(8192时钟周期)
	delay_us = ov13855_cal_delay(8192);
	usleep_range(delay_us * 2, delay_us * 3);

	return 0;

disable_clk:
	clk_disable_unprepare(ov13855->xvclk);
	return ret;
}

上电时序的物理过程

  1. 主电源使能 → LDO输出电压
  2. 时钟启动 → 24MHz方波输出
  3. 电源稳定 → AVDD/DOVDD/DVDD按序上电
  4. 释放复位 → sensor内部开始初始化
  5. 延时等待 → 内部电路稳定,I2C控制器就绪

9. 验证硬件ID

c 复制代码
	ret = ov13855_check_sensor_id(ov13855, client);
	if (ret)
		goto err_power_off;

ov13855_check_sensor_id实现

c 复制代码
static int ov13855_check_sensor_id(struct ov13855 *ov13855,
				   struct i2c_client *client)
{
	struct device *dev = &ov13855->client->dev;
	u32 id = 0;
	int ret;

	// 读取3字节芯片ID寄存器
	ret = ov13855_read_reg(client, OV13855_REG_CHIP_ID,
			       OV13855_REG_VALUE_24BIT, &id);
	if (id != CHIP_ID) {
		dev_err(dev, "Unexpected sensor id(%06x), ret(%d)\n", id, ret);
		return -ENODEV;
	}

	// 读取芯片版本号
	ret = ov13855_read_reg(client, OV13855_CHIP_REVISION_REG,
			       OV13855_REG_VALUE_08BIT, &id);
	if (ret) {
		dev_err(dev, "Read chip revision register error\n");
		return ret;
	}

	dev_info(dev, "Detected OV%06x sensor, REVISION 0x%x\n", CHIP_ID, id);

	return 0;
}

硬件验证逻辑

  1. 通过I2C从寄存器OV13855_REG_CHIP_ID读取3字节
  2. 期望值是CHIP_ID = 0x00d855
  3. 如果匹配,说明硬件连接正确、上电时序正确、I2C通信正常
  4. 否则返回-ENODEV错误,probe失败

至此,硬件资源准备任务完成:所有资源已获取,硬件已上电并验证通过。


任务二:V4L2框架集成

1. V4L2子设备的作用

V4L2子设备概念 :把硬件模块抽象成一个具有标准化操作接口的软件对象。每个子设备通过v4l2_subdev_ops结构体向框架提供功能函数。

OV13855作为子设备:对外提供开流、关流、设置分辨率、读取帧率等标准操作,隐藏底层寄存器细节。

2. 初始化V4L2子设备

c 复制代码
	sd = &ov13855->subdev;
	v4l2_i2c_subdev_init(sd, client, &ov13855_subdev_ops);

v4l2_i2c_subdev_init做的事情

  1. 设置sd->owner = THIS_MODULE
  2. 设置sd->dev = &client->dev(指向I2C设备)
  3. 设置sd->ops = &ov13855_subdev_ops(操作函数集)
  4. 用I2C设备名初始化sd->name
  5. 调用v4l2_set_subdevdata(sd, client)(保存私有数据)

建立的关系

复制代码
v4l2_subdev (sd)
├── ops → ov13855_subdev_ops(提供标准操作)
├── dev → i2c_client->dev(关联硬件设备)
└── i2c_client(通过subdevdata获取)

3. 创建V4L2控制器

c 复制代码
	ret = ov13855_initialize_controls(ov13855);
	if (ret)
		goto err_destroy_mutex;

控制器的作用:让用户空间能够调整sensor参数(曝光、增益、测试图案等)。

ov13855_initialize_controls的实现逻辑

c 复制代码
static int ov13855_initialize_controls(struct ov13855 *ov13855)
{
	const struct ov13855_mode *mode;
	struct v4l2_ctrl_handler *handler;
	s64 exposure_max, vblank_def;
	u32 h_blank;
	int ret;

	handler = &ov13855->ctrl_handler;
	mode = ov13855->cur_mode;
	
	// 初始化控制器处理器
	ret = v4l2_ctrl_handler_init(handler, 8);
	if (ret)
		return ret;
	handler->lock = &ov13855->mutex;

	// 创建链路频率控制器(只读)
	ov13855->link_freq = v4l2_ctrl_new_int_menu(handler, NULL,
			V4L2_CID_LINK_FREQ,
			1, 0, link_freq_items);

	// 创建像素速率控制器(只读)
	ov13855->pixel_rate = v4l2_ctrl_new_std(handler, NULL,
			V4L2_CID_PIXEL_RATE,
			0, OV13855_PIXEL_RATE,
			1, dst_pixel_rate);

	// 创建水平消隐控制器(只读)
	ov13855->hblank = v4l2_ctrl_new_std(handler, NULL, V4L2_CID_HBLANK,
				h_blank, h_blank, 1, h_blank);
	if (ov13855->hblank)
		ov13855->hblank->flags |= V4L2_CTRL_FLAG_READ_ONLY;

	// 创建垂直消隐控制器(可写,影响帧率)
	ov13855->vblank = v4l2_ctrl_new_std(handler, &ov13855_ctrl_ops,
				V4L2_CID_VBLANK, vblank_def,
				OV13855_VTS_MAX - mode->height,
				1, vblank_def);

	// 创建曝光时间控制器(可写)
	ov13855->exposure = v4l2_ctrl_new_std(handler, &ov13855_ctrl_ops,
				V4L2_CID_EXPOSURE, OV13855_EXPOSURE_MIN,
				exposure_max, OV13855_EXPOSURE_STEP,
				mode->exp_def);

	// 创建模拟增益控制器(可写)
	ov13855->anal_gain = v4l2_ctrl_new_std(handler, &ov13855_ctrl_ops,
				V4L2_CID_ANALOGUE_GAIN, OV13855_GAIN_MIN,
				OV13855_GAIN_MAX, OV13855_GAIN_STEP,
				OV13855_GAIN_DEFAULT);

	// 创建测试图案控制器(可写)
	ov13855->test_pattern = v4l2_ctrl_new_std_menu_items(handler,
				&ov13855_ctrl_ops, V4L2_CID_TEST_PATTERN,
				ARRAY_SIZE(ov13855_test_pattern_menu) - 1,
				0, 0, ov13855_test_pattern_menu);

	if (handler->error) {
		ret = handler->error;
		dev_err(&ov13855->client->dev,
			"Failed to init controls(%d)\n", ret);
		goto err_free_handler;
	}

	// 关联控制器处理器到子设备
	ov13855->subdev.ctrl_handler = handler;

	return 0;

err_free_handler:
	v4l2_ctrl_handler_free(handler);
	return ret;
}

控制器的分类

  • 只读控制器link_freqpixel_ratehblank,用于查询sensor当前参数
  • 可写控制器vblankexposureanal_gaintest_pattern,用户可以调整

用户空间的使用

c 复制代码
// 查询控制器
ioctl(fd, VIDIOC_QUERYCTRL, &qctrl);

// 设置曝光时间
struct v4l2_control ctrl;
ctrl.id = V4L2_CID_EXPOSURE;
ctrl.value = 1000;
ioctl(fd, VIDIOC_S_CTRL, &ctrl);

内核会调用ov13855_ctrl_ops.s_ctrl函数,最终写入硬件寄存器。

4. 设置子设备ops函数集

文件中的ops定义

c 复制代码
static const struct v4l2_subdev_ops ov13855_subdev_ops = {
	.core	= &ov13855_core_ops,
	.video	= &ov13855_video_ops,
	.pad	= &ov13855_pad_ops,
};

这个ops在v4l2_i2c_subdev_init时已经设置到sd->ops

三个ops子集的作用

  • core_ops:电源管理(s_power)、ioctl处理
  • video_ops:流控制(s_stream)、帧率操作(g_frame_interval)
  • pad_ops:格式协商(set_fmt)、枚举支持的格式

关键函数s_stream

c 复制代码
static int ov13855_s_stream(struct v4l2_subdev *sd, int on)
{
	// 通过container_of宏从v4l2_subdev指针反向获取ov13855私有数据结构
	// 这样就能访问sensor的所有状态和硬件资源句柄
	struct ov13855 *ov13855 = to_ov13855(sd);
	
	// 获取I2C客户端指针,后续所有寄存器读写都通过它进行
	struct i2c_client *client = ov13855->client;
	int ret = 0;

	// 获取互斥锁,保护streaming状态和硬件寄存器访问
	// 防止多个进程同时调用造成状态混乱或硬件冲突
	mutex_lock(&ov13855->mutex);
	
	// 双重逻辑非运算:将任意非零值规范化为1,0保持为0
	// 例如:on=5 → !5=0 → !0=1 → on=1
	// 作用:统一参数格式,上层可能传入任意非零值表示开启
	on = !!on;
	
	// 状态去重检查:如果目标状态与当前状态相同则直接返回
	// 避免重复操作:已开流时再次开流、或已关流时再次关流
	// 防止重复写寄存器造成硬件异常,提高效率
	if (on == ov13855->streaming)
		goto unlock_and_return;

	// ===== 开流分支 =====
	if (on) {
		// 运行时电源管理:同步上电
		// 1. 增加设备使用计数
		// 2. 如果设备处于suspend状态,唤醒设备
		// 3. 调用ov13855_runtime_resume → __ov13855_power_on
		//    - 使能GPIO电源
		//    - 启动24MHz外部时钟
		//    - 按时序使能regulator电源
		//    - 释放复位信号
		// 4. 等待设备完全上电后返回
		ret = pm_runtime_get_sync(&client->dev);
		if (ret < 0) {
			// 上电失败(可能是电源异常、时钟失败等)
			// 使用put_noidle:降低计数但不触发suspend
			// 原因:设备根本没成功上电,不能执行下电操作
			pm_runtime_put_noidle(&client->dev);
			goto unlock_and_return;
		}

		// 启动数据流输出
		// 1. 写入当前模式的寄存器配置(分辨率、帧率、像素格式等)
		// 2. 应用控制器的值(曝光时间、增益等参数)
		// 3. 向control mode寄存器写0x01,sensor开始输出MIPI信号
		ret = __ov13855_start_stream(ov13855);
		if (ret) {
			// 启动流失败(I2C通信错误、寄存器配置错误等)
			v4l2_err(sd, "start stream failed while write regs\n");
			// 使用put:降低使用计数,允许PM框架正常关闭电源
			// 因为设备已经上电成功,需要执行完整的下电流程
			pm_runtime_put(&client->dev);
			goto unlock_and_return;
		}
	} 
	// ===== 关流分支 =====
	else {
		// 停止数据流输出
		// 向control mode寄存器写0x00,sensor进入standby模式
		// 硬件效果:停止输出MIPI信号,像素阵列停止工作,进入低功耗
		__ov13855_stop_stream(ov13855);
		
		// 运行时电源管理:允许下电
		// 1. 降低设备使用计数
		// 2. 如果计数降为0,PM框架调用ov13855_runtime_suspend
		// 3. 执行__ov13855_power_off:
		//    - 关闭regulator电源
		//    - 停止外部时钟
		//    - 拉低电源GPIO
		// 4. sensor进入最低功耗状态
		pm_runtime_put(&client->dev);
	}

	// 更新streaming状态标志
	// 用于:1) 下次调用时的状态去重判断
	//      2) 其他函数读取当前状态(如suspend流程)
	ov13855->streaming = on;

unlock_and_return:
	// 释放互斥锁,允许其他操作访问
	mutex_unlock(&ov13855->mutex);
	
	// 返回操作结果:0=成功,负数=失败
	return ret;
}

开流流程(on=1)

复制代码
1. 获取锁 (mutex_lock)
2. 规范化参数 (on = !!on)
3. 检查状态去重
   └─ 已经开流? → 直接返回
4. 上电 (pm_runtime_get_sync)
   ├─ 调用 ov13855_runtime_resume
   └─ 执行 __ov13855_power_on
       ├─ 使能GPIO电源
       ├─ 启动24MHz时钟
       ├─ 使能regulator
       └─ 释放复位信号
5. 启动流 (__ov13855_start_stream)
   ├─ 写入模式寄存器(分辨率、帧率)
   ├─ 应用控制器值(曝光、增益)
   └─ 写streaming寄存器0x01
6. 更新状态 (streaming = 1)
7. 释放锁返回
   ↓
[sensor开始输出MIPI数据]

关流流程(on=0)

复制代码
1. 获取锁
2. 规范化参数
3. 检查状态去重
   └─ 已经关流? → 直接返回
4. 停止流 (__ov13855_stop_stream)
   └─ 写streaming寄存器0x00
5. 下电 (pm_runtime_put)
   ├─ 降低使用计数
   └─ 计数=0时调用 ov13855_runtime_suspend
       └─ 执行 __ov13855_power_off
           ├─ 关闭regulator
           ├─ 停止时钟
           └─ 拉低电源GPIO
6. 更新状态 (streaming = 0)
7. 释放锁返回
   ↓
[sensor完全关闭,最低功耗]

5. 配置子设备标志位

c 复制代码
#ifdef CONFIG_VIDEO_V4L2_SUBDEV_API
	sd->internal_ops = &ov13855_internal_ops;
	sd->flags |= V4L2_SUBDEV_FL_HAS_DEVNODE;
#endif

V4L2_SUBDEV_FL_HAS_DEVNODE标志的含义

告诉V4L2框架"这个子设备需要创建独立的字符设备节点"。

为什么需要子设备节点

bash 复制代码
子设备节点 = sensor的直通接口

没有子设备节点:应用 → /dev/video0 → ISP驱动 → sensor(间接控制)
有子设备节点:应用 → /dev/v4l-subdev2 → sensor(直接控制)

应用程序可以直接打开/dev/v4l-subdev2,独立控制sensor而不经过ISP。这对调试和高级应用很有用。

6. 初始化media entity

c 复制代码
#if defined(CONFIG_MEDIA_CONTROLLER)
	ov13855->pad.flags = MEDIA_PAD_FL_SOURCE;
	sd->entity.function = MEDIA_ENT_F_CAM_SENSOR;
	ret = media_entity_pads_init(&sd->entity, 1, &ov13855->pad);
	if (ret < 0)
		goto err_power_off;
#endif

这个还是可以重点关注一下的,后面的文章也会提到

media框架的作用:描述硬件模块间的连接拓扑。

pad的概念:连接点,类似硬件接口。sensor有一个输出pad,连接到CSI2的输入pad。

media_entity_pads_init做的事

  1. 分配pad数组内存
  2. 设置pad属性(方向、标志)
  3. 关联pad到entity

拓扑关系

复制代码
[OV13855 entity]
    └─ pad[0] (SOURCE) --连接线--> [CSI2 entity] pad[0] (SINK)

7. 构造子设备名称

c 复制代码
	memset(facing, 0, sizeof(facing));
	if (strcmp(ov13855->module_facing, "back") == 0)
		facing[0] = 'b';
	else
		facing[0] = 'f';

	snprintf(sd->name, sizeof(sd->name), "m%02d_%s_%s %s",
		 ov13855->module_index, facing,
		 OV13855_NAME, dev_name(sd->dev));

命名规则m<模块号>_<朝向>_<sensor型号> <I2C设备名>

示例m00_b_ov13855 4-0010

  • m00:模块0
  • b:后置摄像头
  • ov13855:sensor型号
  • 4-0010:I2C总线4,设备地址0x10

用途:用户空间通过这个名字识别具体的摄像头。

至此,V4L2框架集成完成:子设备已初始化,控制器已创建,ops已设置,media entity已配置。


任务三:异步框架注册

1. 异步注册的必要性

问题背景:Linux设备驱动的probe顺序是不确定的。可能出现:

  • 场景1:sensor先probe,ISP后probe
  • 场景2:ISP先probe,sensor后probe

传统同步方式的问题

如果ISP驱动在probe时直接查找sensor子设备,当sensor还未probe时就会失败。

异步框架的解决方案

  1. sensor probe时注册到异步框架,声明"我是一个可用的子设备"
  2. ISP probe时建立notifier,声明"我需要这些子设备"
  3. 框架自动匹配,当所有需要的子设备都就绪时触发complete回调
  4. 在complete回调中建立连接、创建设备节点

2. 注册到异步框架

c 复制代码
	ret = v4l2_async_register_subdev_sensor_common(sd);
	if (ret) {
		dev_err(dev, "v4l2 async register subdev failed\n");
		goto err_clean_entity;
	}

v4l2_async_register_subdev_sensor_common做的事

  1. 设置子设备的fwnode(从设备树获取)
  2. 调用v4l2_async_register_subdev(sd)注册到全局异步子设备列表
  3. 框架开始尝试匹配:查找是否有notifier在等待这个fwnode的子设备

注册后的状态

复制代码
全局async子设备列表
├── ov13855_subdev (fwnode = /i2c@.../ov13855@10)
└── ... 其他子设备

3. ISP驱动的notifier建立

文件 : drivers/staging/media/rkisp1/rkisp1-dev.c

ISP驱动probe时的操作

c 复制代码
static int rkisp1_subdev_notifier(struct rkisp1_device *rkisp1)
{
	struct v4l2_async_notifier *ntf = &rkisp1->notifier;
	unsigned int next_id = 0;
	int ret;

	v4l2_async_notifier_init(ntf);

	// 遍历设备树中的endpoint
	while (1) {
		struct v4l2_fwnode_endpoint vep = {
			.bus_type = V4L2_MBUS_CSI2_DPHY
		};
		struct rkisp1_sensor_async *rk_asd = NULL;
		struct v4l2_async_subdev *asd;
		struct fwnode_handle *ep;

		// 获取endpoint节点
		ep = fwnode_graph_get_endpoint_by_id(dev_fwnode(rkisp1->dev),
			0, next_id, FWNODE_GRAPH_ENDPOINT_NEXT);
		if (!ep)
			break;

		// 解析endpoint参数
		ret = v4l2_fwnode_endpoint_parse(ep, &vep);
		if (ret)
			goto err_parse;

		// 添加到notifier,指定需要endpoint的remote端子设备
		asd = v4l2_async_notifier_add_fwnode_remote_subdev(ntf, ep,
							sizeof(*rk_asd));
		if (IS_ERR(asd)) {
			ret = PTR_ERR(asd);
			goto err_parse;
		}

		rk_asd = container_of(asd, struct rkisp1_sensor_async, asd);
		rk_asd->mbus_type = vep.bus_type;
		rk_asd->mbus_flags = vep.bus.mipi_csi2.flags;
		rk_asd->lanes = vep.bus.mipi_csi2.num_data_lanes;

		next_id = vep.base.id + 1;
		fwnode_handle_put(ep);
	}

	// 设置回调函数
	ntf->ops = &rkisp1_subdev_notifier_ops;
	
	// 注册notifier,框架开始匹配
	ret = v4l2_async_notifier_register(&rkisp1->v4l2_dev, ntf);
	...
}

Notifier的作用

声明"我需要这些fwnode对应的子设备",框架会自动匹配。

4. 异步匹配与回调

匹配过程

  1. ISP注册notifier,列出需要的fwnode列表
  2. 框架遍历已注册的子设备,比对fwnode
  3. 找到匹配的子设备,调用bound回调
  4. 所有需要的子设备都匹配后,调用complete回调

bound回调(每匹配一个子设备调用一次):

c 复制代码
static int rkisp1_subdev_notifier_bound(struct v4l2_async_notifier *notifier,
					struct v4l2_subdev *sd,
					struct v4l2_async_subdev *asd)
{
	struct rkisp1_device *rkisp1 =
		container_of(notifier, struct rkisp1_device, notifier);
	struct rkisp1_sensor_async *s_asd =
		container_of(asd, struct rkisp1_sensor_async, asd);

	s_asd->pixel_rate_ctrl = v4l2_ctrl_find(sd->ctrl_handler,
						V4L2_CID_PIXEL_RATE);
	s_asd->sd = sd;  // 保存sensor子设备指针
	s_asd->dphy = devm_phy_get(rkisp1->dev, "dphy");
	...
	phy_init(s_asd->dphy);

	return 0;
}

作用:保存子设备指针,初始化MIPI D-PHY。

5. complete回调:创建设备节点

complete回调(所有子设备都匹配后调用):

c 复制代码
static int rkisp1_subdev_notifier_complete(struct v4l2_async_notifier *notifier)
{
	struct rkisp1_device *rkisp1 =
		container_of(notifier, struct rkisp1_device, notifier);
	int ret;

	// 建立media连接(pad之间的link)
	ret = rkisp1_create_links(rkisp1);
	if (ret)
		return ret;

	// 创建子设备节点!关键代码!
	ret = v4l2_device_register_subdev_nodes(&rkisp1->v4l2_dev);
	if (ret)
		return ret;

	dev_dbg(rkisp1->dev, "Async subdev notifier completed\n");

	return 0;
}

6. 设备节点创建的实际执行

v4l2_device_register_subdev_nodes的实现逻辑

c 复制代码
int v4l2_device_register_subdev_nodes(struct v4l2_device *v4l2_dev)
{
	struct v4l2_subdev *sd;
	int err;

	// 遍历所有已注册的子设备
	list_for_each_entry(sd, &v4l2_dev->subdevs, list) {
		// 检查是否有HAS_DEVNODE标志
		if (!(sd->flags & V4L2_SUBDEV_FL_HAS_DEVNODE))
			continue;

		// 创建字符设备节点
		err = __video_register_device(&sd->devnode, VFL_TYPE_SUBDEV,
					-1, 1, sd->owner);
		if (err < 0)
			return err;
	}

	return 0;
}

创建过程

  1. 遍历v4l2_dev->subdevs链表
  2. 检查sd->flags & V4L2_SUBDEV_FL_HAS_DEVNODE
  3. 如果标志存在,调用__video_register_device
  4. 注册为VFL_TYPE_SUBDEV类型的video设备
  5. 内核分配次设备号,创建/dev/v4l-subdev<N>节点

最终结果

bash 复制代码
$ ls -l /dev/v4l-subdev*
crw-rw---- 1 root video 81, 2 ... /dev/v4l-subdev0  # ISP子设备
crw-rw---- 1 root video 81, 3 ... /dev/v4l-subdev1  # CSI2子设备
crw-rw---- 1 root video 81, 4 ... /dev/v4l-subdev2  # OV13855子设备

设备节点的文件操作

用户空间打开/dev/v4l-subdev2后,可以:

  • VIDIOC_QUERYCTRL查询支持的控制器
  • VIDIOC_G_CTRL/S_CTRL读写控制器
  • VIDIOC_SUBDEV_G_FMT/S_FMT查询/设置格式

7. 使能运行时电源管理

c 复制代码
	pm_runtime_set_active(dev);
	pm_runtime_enable(dev);
	pm_runtime_idle(dev);

	return 0;

运行时PM的作用

  • pm_runtime_set_active:标记设备当前为活动状态(因为probe中已上电)
  • pm_runtime_enable:使能运行时电源管理
  • pm_runtime_idle:设为空闲态,PM框架可以决定是否关闭电源

节能机制

当没有应用打开设备时,PM框架会调用ov13855_runtime_suspend,执行__ov13855_power_off关闭电源。需要时再通过ov13855_runtime_resume上电。

至此,异步框架注册完成:子设备已加入异步框架,等待ISP完成匹配,最终在complete回调中创建设备节点。


完整时序图

复制代码
时间线:
    ↓
[OV13855 probe开始]
    ├─ 任务一:硬件资源准备
    │   ├─ 分配私有数据结构
    │   ├─ 读取DTS模块配置
    │   ├─ 获取时钟、GPIO、regulator、pinctrl
    │   ├─ 初始化mutex
    │   ├─ 上电(按时序控制GPIO和regulator)
    │   └─ 验证芯片ID ✓
    │
    ├─ 任务二:V4L2框架集成
    │   ├─ 初始化v4l2_subdev
    │   ├─ 创建控制器(曝光、增益等)
    │   ├─ 设置ops函数集
    │   ├─ 设置V4L2_SUBDEV_FL_HAS_DEVNODE标志 ← 预约设备节点
    │   └─ 初始化media entity和pad
    │
    └─ 任务三:异步框架注册
        ├─ 调用v4l2_async_register_subdev_sensor_common
        ├─ 注册到全局async子设备列表
        └─ 使能runtime PM
[OV13855 probe结束]
    ↓
[等待ISP驱动probe...]
    ↓
[ISP probe]
    ├─ 建立async notifier
    ├─ 注册notifier
    └─ 框架开始匹配
        ↓
[框架匹配到OV13855]
    ├─ 调用bound回调
    │   └─ 保存sensor指针,初始化D-PHY
    ↓
[所有子设备匹配完成]
    ├─ 调用complete回调
    │   ├─ rkisp1_create_links() - 建立media连接
    │   └─ v4l2_device_register_subdev_nodes() ← 实际创建设备节点
    │       └─ 遍历子设备,检查V4L2_SUBDEV_FL_HAS_DEVNODE标志
    │           └─ 调用video_register_device()
    │               └─ 创建/dev/v4l-subdev2 ✓
    └─ 系统就绪

核心问题解答

Q: 哪一行代码创建了设备节点?

A: 两阶段过程

阶段1 - OV13855 probe(预约)

c 复制代码
// drivers/media/i2c/ov13855.c 第108行
sd->flags |= V4L2_SUBDEV_FL_HAS_DEVNODE;

这一行设置标志,告诉框架"需要设备节点"。

阶段2 - ISP complete回调(实际创建)

c 复制代码
// drivers/staging/media/rkisp1/rkisp1-dev.c complete回调中
ret = v4l2_device_register_subdev_nodes(&rkisp1->v4l2_dev);

这一行遍历所有子设备,为带HAS_DEVNODE标志的子设备创建/dev/v4l-subdevX

节点创建的物理位置
/dev/v4l-subdev2 → 字符设备 → 主设备号81,次设备号由内核分配


总结

probe函数按三大任务清晰组织:

任务一:硬件资源准备

  • 获取资源句柄 → 按上电时序控制硬件 → 验证ID

任务二:V4L2框架集成

  • 初始化subdev → 创建控制器 → 设置ops → 配置标志位和media entity

任务三:异步框架注册

  • 注册到async框架 → 等待ISP匹配 → complete回调创建节点

设备节点创建的关键

probe中设置V4L2_SUBDEV_FL_HAS_DEVNODE标志,ISP complete回调中调用v4l2_device_register_subdev_nodes()实际创建。这是一个"预约-兑现"的两阶段机制。

相关推荐
东方欲晓w2 小时前
STM32 UART篇
stm32·单片机·嵌入式硬件
悠哉悠哉愿意3 小时前
【ROS2学习笔记】分布式通信
笔记·学习·ros2
丰锋ff3 小时前
2010 年真题配套词汇单词笔记(考研真相)
笔记·学习·考研
驱动探索者4 小时前
linux 学习平台 arm+x86 搭建
linux·arm开发·学习
A9better4 小时前
嵌入式开发学习日志33——stm32之PWM舵机简单项目
stm32·单片机·嵌入式硬件·学习
CiLerLinux4 小时前
第三十八章 ESP32S3 SPIFFS 实验
图像处理·人工智能·单片机·嵌入式硬件
Mr_Xuhhh5 小时前
哈希扩展学习
学习·算法·哈希算法
国科安芯5 小时前
关于软错误的常见问题解答
单片机·嵌入式硬件·安全·硬件架构·软件工程
jianqiang.xue5 小时前
ESP32-S3入门第九天:摄像头入门与应用
单片机·嵌入式硬件·物联网