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)驱动实现

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

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

这边以解串器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 的同学们有帮助。

好文阅读

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。