camera调试:serdes camera调试

serdes是串行器和解串器的简写,顾名思义是一种将并行数据转换成串行数据发送,将接收的串行数据转换成并行数据的”器件“。

camera常用的接口是MIPI高速接口,MIPI的传输距离受限,传输距离过大容易导致信号质量不佳,影响图像数据的传输,所以经常会使用到serdes,以增加传输距离,在车载领域更加常见。

这篇文章简单介绍一下在RK3588上面serdes camera的调试。

目录

(1)RK3588 serdes camera应用框图

①sensor自带ISP

②sensor不带ISP

(2)基于V4L2 camera驱动实现

1)I2C设备驱动

2)驱动初始化代码

3)驱动数据流控制

4)dts配置

(3)多路camera输入应用

1)驱动接口

2)多路camera对接USBHAL

(4)热拔插功能

1)VICAP异常复位机制

2)驱动实现

①申请中断

②中断处理函数

③轮询方式实现

④ioctl接口实现

(5)总结


(1)RK3588 serdes camera应用框图

基于RK3588平台,带serdes的camera应用的框图如下所示:

相比于不带serdes的场景,会在senso和主控之间增加串行器和解串器的连接,以增加传输距离,串行器和解串器之间一般是用线缆连接,其中的传输协议一般是各个厂商自有的,比如美信提出的GMSL/GMSL2协议,传输带宽可以达到6G。

如上场景也可以分为两种情况:

①sensor自带ISP

有的模组厂会在sensor端自带ISP,sensor的图像直接经过模组的ISP处理,输出YUV422的图像进行传输,可以省下来在RK3588主控端ISP 3A算法处理的时间。这种场景情况下,在配置的时候不需要将图像经过ISP处理,直接连接到VICAP存储到DDR即可。

②sensor不带ISP

模组没有带ISP,直接输出RAWRGB的图像,RK3588主控端vicap收下之后,需要将数据给到ISP进行3A算法处理,有分回读模式和直通模式,具体可以看前面的文章。

(2)基于V4L2 camera驱动实现

前面的文章有介绍到RK3588的camera都是基于V4L2框架和media-framework系统设计实现的,将各级链路的数据流虚拟化成子设备节点,如下所示,将节点称为entity,每个entity都带有pad端口,pad端口可以想象成是数据流的进出口,entity之间的pad连接,我们使用link来描述。

#------------#                #------------#
|          __|__            __|__          |
|         |  |  |   link   |  |  |         |
|         | pad |<-------->| pad |         |
|         |__|__|          |__|__|         |
|            |                |            |
|   entity   |                |   entity   |
#------------#                #------------# 

在不带serdes的设计的时候,是将camera sensor虚拟化成一个子设备,如果增加了serdes的话,该如何设计框架。我觉得有两种思路:

  • 继续引进pipeline的概念,将ser和des都分别虚拟化成两个子设备节点,利用V4L2-pipeline框架进行配置连接,这种设计兼容性会好点,但是工作量比较大
  • 可将问题简单化,将sensor,serdes进行捆绑化设计,将三者捆绑成一个V4L2子设备驱动,并在同一个驱动初始化sensor和serdes。

后面主要讲一下第二种方式的做法,第一种方式只是本人的一种想法,并没有实践,仅供感兴趣的同学参考。

1)I2C设备驱动

无论是否带serdes,sensor和serdes都是i2c设备,简而言之,我的做法就是将sensor、serdes三者当成一个设备看待,只注册一个i2c设备,驱动通过i2c地址进行区分,分别控制三者,对三者的寄存器进行配置。下面以THCV241+THCV244 serdes为例进行介绍,在这个例子中,因为模组带固件,上电就直接跑内部固件配置寄存器,因此不需要主控这边进行控制。

这里有一个疑问,注册i2c设备的时候,dts需要配置i2c地址,和设备驱动的信息,这里应该配置解串器还是串行器?我个人建议是按照解串器进行配置,解串器与主控直接相连,串行器并没有与主控直接连接,甚至i2c控制都是通过解串器使用线缆进行转发。

2)驱动初始化代码

类似camera的配置,主要需要注意在配置的时候需要注意控制的时序,serdes的控制时序原厂一般会给参考,严格按照时序进行控制,其余与通常的camera没有较大的差别。需要注意的是配置ser和des的i2c地址不同,因此i2c读写的函数需要注意一下。如下所示是THCV244和THCV241的初始化控制流程

static int thcv244_thcv241_init(struct thcv244 *thcv244)
{
	struct device *dev = &thcv244->client->dev;
	int ret;

	ret = thcv244_write_array(thcv244->client, thcv244_global_init_table);
	ret |= thine_write_reg(thcv244->client, THCV241_ADDR, 0x00fe,
					2, THCV244_REG_VALUE_08BIT, 0x11);
	ret |= thine_write_reg(thcv244->client, THCV244_ADDR, 0x0032,
					2, THCV244_REG_VALUE_08BIT, 0x00);
	ret |= thcv241_write_array(thcv244->client, thcv241_init_table);
	ret |= thcv244_write_array(thcv244->client, thcv244_1080p30_init_table);
	ret |= thine_write_reg(thcv244->client, THCV244_ADDR, 0x0032,
					2, THCV244_REG_VALUE_08BIT, 0x00);
	ret |= thine_write_reg(thcv244->client, THCV241_ADDR, 0xfe,
					1, THCV244_REG_VALUE_08BIT, 0x21);
	ret |= thine_write_reg(thcv244->client, THCV241_ADDR, 0x3e,
					1, THCV244_REG_VALUE_08BIT, 0x00);
	msleep(200);
	ret |= thine_write_reg(thcv244->client, THCV241_ADDR, 0x3e,
					1, THCV244_REG_VALUE_08BIT, 0x10);
	ret |= thine_write_reg(thcv244->client, THCV244_ADDR, 0x1600,
					2, THCV244_REG_VALUE_08BIT, 0x00);
	if (ret)
		dev_err(dev, "fail to init thcv244 and thcv 241!\n");

	return ret;
}

如下是i2c的读写函数,根据传递的i2c地址进行控制:

static int thine_write_reg(struct i2c_client *client, u16 client_addr, u16 reg,
			    u32 reg_len, u32 val_len, u32 val)
{
	u32 buf_i, val_i;
	u8 buf[6];
	u8 *val_p;
	__be32 val_be;

	if (val_len > 4)
		return -EINVAL;
	if (reg_len == 2) {
		buf[0] = reg >> 8;
		buf[1] = reg & 0xff;
	} else {
		buf[0] = reg & 0xff;
	}
	val_be = cpu_to_be32(val);
	val_p = (u8 *)&val_be;
	if (reg_len == 2) {
		buf_i = 2;
		val_i = 4 - val_len;
	} else {
		buf_i = 1;
		val_i = 4 - val_len;
	}
	while (val_i < 4)
		buf[buf_i++] = val_p[val_i++];
	client->addr = client_addr;

	if (i2c_master_send(client, buf, val_len + reg_len) != val_len + reg_len) {
		dev_err(&client->dev,
			"%s, i2c_master_send err, client->addr = 0x%x, reg = 0x%x, val = 0x%x\n",
			__func__, client->addr, reg, val);
		return -EIO;
	}

	dev_dbg(&client->dev,
		"%s, i2c_master_send ok, client->addr = 0x%x, reg = 0x%x, val = 0x%x\n",
		__func__, client->addr, reg, val);

	return 0;
}

3)驱动数据流控制

标准驱动需要对数据流的开关进行控制,主要控制解串器的MIPI输出数据流即可,因为解串器才是与主控直接相连,串行器的输出初始化的时候直接打开就好,控不控制影响不大。

其实这里有一个问题,就是解串器输出的MIPI使用的是连续时钟模式还是非连续时钟模式,相关的概念大家可以自行百度,这里不再赘述。

连续时钟,对pipeline的数据流控制有时序要求:必须先打开RK3588主控端的MIPI RX,再打开解串器的MIPI TX。

非连续时钟,对时序控制要求不严格,先开RX 或者先开TX影响不大。后续其他文章会提到为何需要这样要求。

控制数据流的接口:

static int thcv244_s_stream(struct v4l2_subdev *sd, int on)
{
	struct thcv244 *thcv244 = to_thcv244(sd);
	struct i2c_client *client = thcv244->client;
	int ret = 0;

	dev_info(&client->dev, "%s: on: %d, %dx%d@%d\n", __func__, on,
				thcv244->cur_mode->width,
				thcv244->cur_mode->height,
		DIV_ROUND_CLOSEST(thcv244->cur_mode->max_fps.denominator,
				  thcv244->cur_mode->max_fps.numerator));

	mutex_lock(&thcv244->mutex);
	on = !!on;
	if (on == thcv244->streaming)
		goto unlock_and_return;

	if (on) {
		ret = pm_runtime_get_sync(&client->dev);
		if (ret < 0) {
			pm_runtime_put_noidle(&client->dev);
			goto unlock_and_return;
		}

		ret = __thcv244_start_stream(thcv244);
		if (ret) {
			v4l2_err(sd, "start stream failed while write regs\n");
			pm_runtime_put(&client->dev);
			goto unlock_and_return;
		}
	} else {
		__thcv244_stop_stream(thcv244);
		pm_runtime_put(&client->dev);
	}

	thcv244->streaming = on;

unlock_and_return:
	mutex_unlock(&thcv244->mutex);

	return ret;
}

4)dts配置

dts配置参考camera的即可。THCV244配置如下:

&i2c8 {
	status = "okay";
	pinctrl-names = "default";
	pinctrl-0 = <&i2c8m2_xfer>;

	thcv244: thcv244@b {
		compatible = "thine,thcv244";
		status = "okay";
		reg = <0xb>;
		// clocks = <&cru CLK_MIPI_CAMARAOUT_M1>;
		// clock-names = "xvclk";
		// power-domains = <&power RK3588_PD_VI>;
		// pinctrl-names = "default";
		// pinctrl-0 = <&mipim0_camera1_clk>;
		// rockchip,grf = <&sys_grf>;
		/*power-gpios = <&gpio3 RK_PC6 GPIO_ACTIVE_HIGH>;*/
		// reset-gpios = <&gpio3 RK_PA0 GPIO_ACTIVE_HIGH>;
		rockchip,camera-module-index = <0>;
		rockchip,camera-module-facing = "back";
		rockchip,camera-module-name = "thcv244";
		rockchip,camera-module-lens-name = "thcv244";

		port {
			thcv244_out: endpoint {
				remote-endpoint = <&mipi_dcphy0_in>;
				data-lanes = <1 2 3 4>;
			};
		};
	};
};

(3)多路camera输入应用

 这里指的多路camera是串行器支持多个摄像头输出,解串器使用一个mipi接口输出多路摄像头的数据,其中使用了mipi虚拟通道输出多路。主要注意驱动相关接口的设计:

1)驱动接口

如下代码所示,g_mbus_config配置为多路MIPI虚拟通道即可:V4L2_MBUS_CSI2_CHANNELS 

static int thcv244_g_mbus_config(struct v4l2_subdev *sd, unsigned int pad,
				struct v4l2_mbus_config *config)
{
	struct thcv244 *thcv244 = to_thcv244(sd);
	u32 lane_num = thcv244->bus_cfg.bus.mipi_csi2.num_data_lanes;

	config->type = V4L2_MBUS_CSI2_DPHY;
	config->flags = 1 << (lane_num - 1) |
			V4L2_MBUS_CSI2_CHANNELS |
			V4L2_MBUS_CSI2_CONTINUOUS_CLOCK;

	return 0;
}

2)多路camera对接USBHAL

对接应用层的时候,可以分别从4路video取数据流,实现多路camera。

也可以使用RK的USBcameraHAL,然后apk上层去打开4路camera出图,对应配置如下:

&rkcif {
	status = "okay";
	rockchip,android-usb-camerahal-enable;
};

(4)热拔插功能

serdes的应用场景经常会用到热拔插的功能,尤其是车载的应用,需要保证热拔插之后,图像能够恢复正常。RK3588的vicap驱动引进了异常复位机制,可以利用这个机制来实现热拔插的功能。

1)VICAP异常复位机制

当前vicap驱动存在复位机制,该机制用于当vicap出现异常情况时,对vicap进行cru复位操作。
早期的驱动代码版本是通过dts进行配置,添加rockchip,cif-monitor参数,若dts不设置该参数,则默认不使能复位机制。

&rkcif_mipi_lvds {
	status = "okay";
	/* parameters for do cif reset detecting:
	 * index0: monitor mode,
		   0 for idle,
		   1 for continue,
		   2 for trigger,
		   3 for hotplug (for nextchip)
	 * index1: the frame id to start timer,
		   min is 2
	 * index2: frame num of monitoring cycle
	 * index3: err time for keep monitoring
		   after finding out err (ms)
	 * index4: csi2 err reference val for resetting
	 */
	rockchip,cif-monitor = <3 2 1 1000 5>;

	port {
		cif_mipi0_in: endpoint {
			remote-endpoint = <&mipi0_csi2_output>;
		};
	};
};

这里仅介绍一下热拔插的机制,热拔插需要配置index为3,用于解决拔插图像割裂的问题,同时该模式具有continue模式的功能,即实时连续监测vicap是否mipi出错及断流,当发生出错及断流时进行vicap复位。

最新的RK3588 VICAP驱动代码,是通过config进行配置上述的参数,而不再是dts配置:

config ROCKCHIP_CIF_USE_MONITOR
	bool "rkcif use monitor"
	depends on VIDEO_ROCKCHIP_CIF
	default n
	help
	  Support for CIF to monitor capture error.

config ROCKCHIP_CIF_MONITOR_MODE
	hex "rkcif monitor mode"
	default 0x1
	depends on ROCKCHIP_CIF_USE_MONITOR

config ROCKCHIP_CIF_MONITOR_START_FRAME
	hex "the frame id to start monitor"
	default 0
	depends on ROCKCHIP_CIF_USE_MONITOR

config ROCKCHIP_CIF_MONITOR_CYCLE
	hex "frame num of monitoring cycle"
	default 0x8
	depends on ROCKCHIP_CIF_USE_MONITOR

config ROCKCHIP_CIF_MONITOR_KEEP_TIME
	hex "timeout for keep monitoring after finding out error, unit(ms)"
	default 0x3e8
	depends on ROCKCHIP_CIF_USE_MONITOR

config ROCKCHIP_CIF_MONITOR_ERR_CNT
	hex "error reference val for resetting"
	default 0x5
	depends on ROCKCHIP_CIF_USE_MONITOR

2)驱动实现

需要实现热拔插功能,首先需要的是驱动实现对热拔插的检测,一般有两种方式实现:

  1. 中断方式,可以利用一些解串器带的lockpin引脚检测连接性来判断是否拔插,例如max96714,在发生拔插的时候,lockpin会产生脉冲,将lockpin连接主控,申请中断进行检测。
  2. 轮询方式,如果没法使用中断进行判断拔插动作的话,可以使用这种方式,一般解串器都有对应的寄存器可以判断线缆的连接性,通过线程轮询,定时查询寄存器的状态来判断拔插的动作。

这边以解串器max96714的实现为例进行介绍。max96714带lockpin引脚,可以检测与串行器的之间的线缆连接性。同时驱动代码也实现了轮询的工作方式。

①申请中断

dts配置拔插的中断脚,驱动申请拔插中断,如果打开WORK_QUEUE,则按照轮询的方式。

	max96714->plugin_irq = gpiod_to_irq(max96714->max_lock_gpio);
	if (max96714->plugin_irq < 0)
		dev_err(dev, "failed to get plugin det irq, maybe no use\n");

	ret = devm_request_threaded_irq(dev, max96714->plugin_irq, NULL,
			plugin_detect_irq_handler, IRQF_TRIGGER_FALLING |
			IRQF_TRIGGER_RISING | IRQF_ONESHOT, "max96714_plugin",
			max96714);
	if (ret)
		dev_err(dev, "failed to register plugin det irq (%d), maybe no use\n", ret);
#ifdef WORK_QUEUE
	INIT_DELAYED_WORK(&max96714->plug_state_check.d_work, max96714_plug_state_check_work);
	max96714->plug_state_check.state_check_wq =
		create_singlethread_workqueue("max96714_work_queue");
	if (max96714->plug_state_check.state_check_wq == NULL) {
		dev_err(dev, "%s(%d): %s create failed.\n", __func__, __LINE__,
			"max96714_work_queue");
	}
#endif

②中断处理函数

中断处理的函数,主要设置max96714->hot_plug_flag标志位,后面vicap驱动会通过ioctl读取这个标志位来判断拔插的动作。

static irqreturn_t plugin_detect_irq_handler(int irq, void *dev_id)
{
	struct max96714 *max96714 = dev_id;
	struct device *dev = &max96714->client->dev;
	// int value = 0;
	int lock_state = 0;

	if (max96714->streaming) {
		// value = plugin_gpio_present(max96714);
		lock_state = max96714_check_lock_state(max96714);

		if (lock_state) {
			max96714->cur_detect_status = PLUG_IN;
			dev_info(dev, "detect max96717 camera plug in!\n");
		} else {
			max96714->cur_detect_status = PLUG_OUT;
			dev_info(dev, "detect max96717 camera plug out!\n");
		}

		max96714->hot_plug_flag = max96714->cur_detect_status ^
						max96714->last_detect_status;
#ifndef WORK_QUEUE
		if (max96714->hot_plug_flag)
			max96714->hot_plug = true;
		else
			max96714->hot_plug = false;
		max96714->last_detect_status = max96714->cur_detect_status;
		max96714->hot_plug_flag = max96714->cur_detect_status ^ max96714->last_detect_status;
		if (max96714->hot_plug)
			dev_info(dev, "%s has plug motion? (%s)", __func__,
				max96714->hot_plug ? "true" : "false");
#endif
	}

	return IRQ_HANDLED;
}

③轮询方式实现

同样的,也是设置max96714->hot_plug_flag标志位。

#ifdef WORK_QUEUE
static void max96714_plug_state_check_work(struct work_struct *work)
{
	struct sensor_state_check_work *params_check =
		container_of(work, struct sensor_state_check_work, d_work.work);
	struct max96714 *max96714 =
		container_of(params_check, struct max96714, plug_state_check);
	struct i2c_client *client = max96714->client;

	if (max96714->hot_plug_flag)
		max96714->hot_plug = true;
	else
		max96714->hot_plug = false;
	max96714->last_detect_status = max96714->cur_detect_status;
	max96714->hot_plug_flag = max96714->cur_detect_status ^ max96714->last_detect_status;

	if (max96714->hot_plug)
		dev_info(&client->dev, "%s has plug motion? (%s)", __func__,
				max96714->hot_plug ? "true" : "false");
	if (max96714->hot_plug) {
		dev_dbg(&client->dev, "queue_delayed_work 1500ms, if has hot plug motion.");
		queue_delayed_work(max96714->plug_state_check.state_check_wq,
				   &max96714->plug_state_check.d_work, msecs_to_jiffies(100));
	} else {
		dev_dbg(&client->dev, "queue_delayed_work 100ms, if no hot plug motion.");
		queue_delayed_work(max96714->plug_state_check.state_check_wq,
				   &max96714->plug_state_check.d_work, msecs_to_jiffies(100));
	}
}
#endif

④ioctl接口实现

vicap驱动端通过RKMODULE_GET_VICAP_RST_INFO的ioctl读取max96714_get_vicap_rst_inf函数,获取拔插的动作,判断是否需要对vicap实现reset操作,如果发生拔插的动作,则vicap驱动读取的rst_info->is_reset为true,vicap就会复位cru,并且调用ioctl接口对解串器进行开关流操作。

static void max96714_get_vicap_rst_inf(struct max96714 *max96714,
				   struct rkmodule_vicap_reset_info *rst_info)
{
	struct i2c_client *client = max96714->client;

	rst_info->is_reset = max96714->hot_plug;
	max96714->hot_plug = false;
	rst_info->src = RKCIF_RESET_SRC_ERR_HOTPLUG;
	if (rst_info->is_reset)
		dev_info(&client->dev, "%s: rst_info->is_reset:%d.\n", __func__, rst_info->is_reset);
}

vicap通过如下ioctl调用到解串器的驱动,并对解串器重新开关数据流:

	case RKMODULE_SET_QUICK_STREAM:
		stream = *((u32 *)arg);
		max96714_set_streaming(max96714, !!stream);
		break;

通过max96714_set_streaming函数对max96714进行开关数据流的动作,具体实现如下:

static void max96714_set_streaming(struct max96714 *max96714, int on)
{
	struct i2c_client *client = max96714->client;

	dev_info(&client->dev, "%s: on: %d\n", __func__, on);

	if (on) {
		/* enter mipi clk normal operation */
		// max96714_write_array(client, max96714->frame_size->regs);
		max96714_write(max96714->client, MAX96714_REG_CTRL_MODE,
			MAX96714_MODE_STREAMING);
		// msleep(100);
	} else {
		/* enter mipi clk powerdown */
		max96714_write(max96714->client, MAX96714_REG_CTRL_MODE,
			MAX96714_MODE_SW_STANDBY);
		// msleep(100);
	}
}

(5)总结

文章内容有点多,但都是本人调试的经验干货,希望这篇文章对想了解或者正常调试serdes camera 的同学们有帮助。

猜你喜欢

转载自blog.csdn.net/qq_34341546/article/details/129138301