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进行开关数据流的动作,具体实现如下:文章来源:https://www.toymoban.com/news/detail-497383.html
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 的同学们有帮助。文章来源地址https://www.toymoban.com/news/detail-497383.html
到了这里,关于camera调试:serdes camera调试的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!