Camera 点亮流程
线性模式 Sensor 驱动移植根据 MIPI 接口和 DVP 接口来进行说明。
MIPI接口
获取 Sensor 初始化寄存器配置
MIPI接口线性模式Senso驱动r移植以格科微 gc2053 为例,在调试Sensor驱动之前,需要确认以下几点:
图像规格 |
---|
Sensor使用哪类接口进行图像传输(MIPI、DVP) |
需要用到的分辨率和帧率 |
MCLK频率(常见:24/27M) |
连接模组的外围功能电路是否按照sensor原厂提供硬件参考设计来实现的 |
根据当前方案所需要的分辨率和帧率,对应的MCLK(一般建议是24M)以及sensor硬件设计上所使用到接口(mipi/dvp)和lane数,联系sensor原厂提供一份对应的初始化寄存器配置,提供的配置需要和当前使用的模组匹配。如图所示是格科微 gc2053 MIPI接口 2lane 1080p 12帧 的寄存器配置:
添加驱动文件
1.添加 Makefile 文件
2.添加 Sensor 驱动文件
3.添加 Kconfig
4.配置 kernel_menuconfig
添加 MakeFile
进入 Sensor 目录 bsp/drivers/vin/modules/sensor
,打开 MakeFile 文件,添加指定语句,这一步的作用是将.c 源文件编译为.o 文件,如下图所示:
添加 Sensor 驱动文件
进入 Sensor 目录 bsp/drivers/vin/modules/sensor
,如果需要调试的模组对应的其他型号之前有在全志平台上点过的话,建议以 SDK 中的某个现成的驱动为基础修改,如格科微的 Sensor 驱动命名都是以 gc 开头的,思特威的 Sensor 驱动命名都是以 sc 开头的,索尼的 Sensor 驱动命名都是以 imx 开头的。
如果没有,可以找一份硬件配置(如 mipi lane 数,图像输出格式等)相近的驱动,在此基础上进行修改。本例中复用格科微其中一个 Sensor 的源文件,重命名成 gc2053_mipi.c。
添加 Kconfig
在同级目录下 bsp/drivers/vin/modules/sensor
的 Kconfig 文件中,将本文件按照格式添加进去,用于 kernel_menuconfig 来选择配置
配置 kernel_menuconfig
使用 make kernel_menuconfig, 进入内核 menuconfig,选中本Senor驱动为 M,M 代表编译成模块(.ko文件),Y 代表编译进内核
→ Allwinner BSP → Device Drivers → VIN (camera) Drivers → Sensor driver select
修改引脚配置
由于模组硬件外围电路因为实际需求差异与公版开发板使用的 Sensor 模组的外围电路存在差异,所以需要根据当前的硬件设计原理图,修改板级配置 board.dts。
- MCLK引脚配置
- TWI引脚配置
- PWDN(sensor使能引脚)和RSTN(sensor复位引脚)配置
- 需要检查当前板级配置,是否有其他模块复用同组TWI或者GPIO的
sun300iw1p1.dtsi:bsp/configs/linux-5.4-ansc/sun300iw1p1.dtsi
board.dts:device/config/chips/v821/configs/xxx(具体板型)/linux-5.4-ansc/board.dts
这里以公版 V821 perf2 为例进行引脚配置说明,MCLK 使用的PA1,其引脚硬件连接图和对应引脚配置如下:
csi_mclk0_pins_a: csi_mclk0@0 {
pins = "PA1";
function = "mipi";
allwinner,drive = <0>;
};
csi_mclk0_pins_b: csi_mclk0@1 {
pins = "PA1";
function = "gpio_in";
};
TWI 引脚硬件对应引脚配置如下:
twi0_pins_default: twi0@0 {
pins = "PA3", "PA4";
function = "twi0";
bias-pull-up;
allwinner,drive = <0>;
};
twi0_pins_sleep: twi0@1 {
pins = "PA3", "PA4";
function = "gpio_in";
};
&twi0 {
clock-frequency = <400000>;
pinctrl-0 = <&twi0_pins_default>;
pinctrl-1 = <&twi0_pins_sleep>;
pinctrl-names = "default", "sleep";
/* For stability and backwards compatibility, we recommend setting 'twi_drv_used' to 1 */
twi_drv_used = <1>;
status = "okay"; /* 此处需检查为 okay *、
};
PWDN 引脚硬件连接图和 RSTN 引脚硬件连接图:
PWDN 引脚和 RSTN 引脚配置是在 board.dts
sensor
属性中配置
sensor0:sensor@0 {
device_type = "sensor0";
sensor0_mname = "gc1084_mipi"; /* 必须要和驱动的 SENSOR_NAME 一致 */
sensor0_twi_cci_id = <0>; /* 所使用的twi id号,本例中使用的是twi1,故填写为1 */
sensor0_twi_addr = <0x6e>; /* Sensor 设备ID地址,必须与驱动中的I2C_ADDR一致 */
sensor0_mclk_id = <0>; /* 所使用的mclk id号,本例中使用的是MCLK0,故填写为0 */
sensor0_pos = "rear";
sensor0_isp_used = <1>; /* 所使用的sensor为raw sensor,需要过ISP处理,故填写为1 */
sensor0_fmt = <1>; /* sensor输出的图像格式,YUV:0,RAW:1 */
sensor0_stby_mode = <0>;
sensor0_vflip = <0>; /* VIPP 图像垂直翻转 */
sensor0_hflip = <0>; /* VIPP 图像水平翻转 */
sensor0_iovdd-supply = <>;/* Sensor iovdd 连接的 ldo,根据硬件原理图的连接来决定(在硬件原理图中搜索aldo,然后找到CSI-iovdd对应的是哪一个aldo即可) */
sensor0_iovdd_vol = <>; /* iovdd的电压 */
sensor0_avdd-supply = <>; /* Sensor avdd连接的 ldo,根据硬件原理图的连接来决定 */
sensor0_avdd_vol = <>; /* 同上 */
sensor0_dvdd-supply = <>; /* 同上 */
sensor0_dvdd_vol = <>; /* 同上 */
sensor0_power_en = <>;
sensor0_reset = <&pio PD 12 GPIO_ACTIVE_LOW>; /* GPIO 信息配置, 配置 PD12,低有效 */
sensor0_pwdn = <>; /* GPIO 信息配置:未配置 */
flash_handle = <>;
act_handle = <>;
status = "okay";
};
移植 Sensor 驱动
修改驱动文件 gc2053_mipi.c
以适配当前使用的模组,如下修改的内容是调试 Sensor 驱动所必须填写的,如下:
修改 Sensor Name
修改驱动文件中宏定义 SENSOR_NAME 为"gc2053_mipi",SENSOR_NAME 要与 board.dts 中的 sensor0_mname 一致,SENSOR_NUM 定义为1表示当前这份驱动只适配一个 gc2053 摄像头。
#define SENSOR_NUM 0x1
#define SENSOR_NAME "gc2053_mipi"
配置 MCLK 时钟
下面的设置代表 Sensor 输入时钟频率,可通过 Sensor 的 datasheet 查看类似 input clock frequency 对应的数据,其中 MCLK 和使用的寄存器配置强相关,在模组厂提供寄存器配置时,可直接询问当前配置使用的 MCLK 频率是多少,一般原厂提供的初始化寄存器配置里面就包含当前配置所使用的 MCLK 频率,如下是格科微 gc2053 初始化寄存器配置所填写的 MCLK。MCLK 是主控端发出来给到 sensor`,MCLK 的配置会影响 Sensor 出图的帧率以及上电时序的表现,部分Sensor没有接收到预期的MCLK时,可能存在I2C不通的情况。
对应驱动文件 gc2053_mipi.c
里面的 MCLK 宏定义修改为:
#define MCLK (24*1000*1000)
配置 Sensor ID/CHIP ID
每个 Sensor 的 Sensor ID/CHIP ID 都是独一无二,根据这个 ID 号可以探测当前使用的 Sensor 是否与驱动相匹配,也可以用于初步校验主控端与 Sensor 的 TWI 通讯是否正常以及多 Sensor list 适配也是通过探测 ID 号来实现的,Sensor ID/CHIP ID 一般在 Sensor 的 datasheet 中找到,可以搜索关键字 Sensor ID 或 CHIP ID 进行查找。
如下是格科微 gc2053 datasheet 中找到 CHIP ID
对应驱动文件里面的 V4L2_IDENT_SENSOR
宏定义修改为:
#define V4L2_IDENT_Sensor 0x2053
若 Sensor 驱动中有宏定义是与 Sensor ID/CHIP ID 的寄存器地址以及 ID 值偏移有关,则对应需要进行填写:
#define ID_REG_HIGH 0xf0 //gc2053 CHIP ID高8位寄存器地址
#define ID_REG_LOW 0xf1 //gc2053 CHIP ID低8位寄存器地址
#define ID_VAL_HIGH ((V4L2_IDENT_SENSOR) >> 8) //gc2053 CHIP ID高8位寄存器读出来的值
#define ID_VAL_LOW ((V4L2_IDENT_SENSOR) & 0xff) //gc2053 CHIP ID低8位寄存器读出来的值
上述与 Sensor ID/CHIP ID 有关的宏定义在 sensor_detect 函数中会被使用,这个函数在 Sensor 驱动被挂载的时候会被调用执行,同时也可以用来检测主控与 Sensor TWI 通讯是否正常。
static int sensor_detect(struct v4l2_subdev *sd)
{
data_type rdval;
int eRet;
int times_out = 3;
do {
eRet = sensor_read(sd, ID_REG_HIGH, &rdval);
sensor_dbg("eRet:%d, ID_VAL_HIGH:0x%x, times_out:%d\n", eRet, rdval, times_out);
usleep_range(200, 220);
times_out--;
} while (eRet < 0 && times_out > 0);
sensor_read(sd, ID_REG_HIGH, &rdval);
sensor_dbg("ID_VAL_HIGH = %2x, Done!\n", rdval);
if (rdval != ID_VAL_HIGH)
return -ENODEV;
sensor_read(sd, ID_REG_LOW, &rdval);
sensor_dbg("ID_VAL_LOW = %2x, Done!\n", rdval);
if (rdval != ID_VAL_LOW)
return -ENODEV;
sensor_dbg("Done!\n");
return 0;
}
配置 Sensor I2C 设备地址、数据/地址位宽
#define I2C_ADDR 0x6e /* sensor的TWI地址,I2C_ADDR要和board.dts中的sensor0_twi_addr一致*/
...
static struct cci_driver cci_drv[] = {
{
.name = SENSOR_NAME,
.addr_width = CCI_BITS_8,
.data_width = CCI_BITS_8,
},
};
添加和注册 Sensor 初始化寄存器配置
static struct regval_list sensor_1080p12_regs[] = {
/* 1928*1088@12fps */
/****system****/
{0xfe, 0x80},
{0xfe, 0x80},
{0xfe, 0x80},
{0xfe, 0x00},
{0xf2, 0x00},
{0xf3, 0x00},
...
{0x02, 0x56},
{0x03, 0x8e},
{0x12, 0x80},
{0x13, 0x07},
{0x15, 0x12},
{0xfe, 0x00},
{0x17, 0x83},
};
...
static struct sensor_win_size sensor_win_sizes[] = {
{
.width = 1920,
.height = 1088,
.hoffset = 4,//0,
.voffset = 4,//0,
.hts = 2200,
.vts = 2700,
.pclk = 74250000,
.mipi_bps = 297 * 1000 * 1000,
.fps_fixed = 12,//12.5
.bin_factor = 1,
.intg_min = 1 << 4,
.intg_max = (2700 - 16) << 4,
.gain_min = 1 << 4,
.gain_max = 110 << 4,
.regs = sensor_1080p12_regs,
.regs_size = ARRAY_SIZE(sensor_1080p12_regs),
.set_size = NULL,
},
...
};
配置图像数据格式
static struct sensor_format_struct sensor_formats[] = {
/* 定义 Sensor 输出的图像数据格式,根据Sensor输出格式填写,下述为 RAW 数据格式示例 */
{
.desc = "Raw RGB Bayer",
.mbus_code = MEDIA_BUS_FMT_SRGGB10_1X10,
.regs = sensor_fmt_raw,
.regs_size = ARRAY_SIZE(sensor_fmt_raw),
.bpp = 1
},
};
#define N_FMTS ARRAY_SIZE(sensor_formats)
配置MIPI接口
static int sensor_g_mbus_config(struct v4l2_subdev *sd, struct v4l2_mbus_config *cfg)
{
cfg->type = V4L2_MBUS_CSI2;
cfg->flags = 0 | V4L2_MBUS_CSI2_2_LANE | V4L2_MBUS_CSI2_CHANNEL_0;
return 0;
}
上电时序配置
Sensor 的上电时序控制在 sensor_power 函数中实现,上电时序必须严格按照 Sensor datasheet 中的说明来进行,上电时序不对会导致 TWI 异常,主控端与 Sensor 通讯失败。
如下图所示是 gc2053 的上电时序图,就是 IOVDD->DVDD->AVDD 的上电顺序,此外 PWDN 和 RESET 引脚的使用也需要结合时序图配置。
static int sensor_power(struct v4l2_subdev *sd, int on)
{
switch (on) {
/* STBY_ON 和 STBY_OFF 基本不使用,可忽略这两个选项的配置 */
case STBY_ON:
...
case STBY_OFF:
...
/* 进行上电操作 */
case PWR_ON:
sensor_dbg("PWR_ON!\n");
cci_lock(sd);
vin_set_mclk(sd, ON); /* 开启mclk时钟 */
usleep_range(1000, 1200);
vin_set_mclk_freq(sd, MCLK); /* 设置MCLK */
usleep_range(1000, 1200);
vin_gpio_set_status(sd, PWDN, 1); /* 将PWDN引脚置为输出 */
vin_gpio_set_status(sd, RESET, 1); /* 将RESET引脚置为输出 */
vin_gpio_write(sd, PWDN, CSI_GPIO_LOW); /* 将PWDN引脚电频置为低电平 */
vin_gpio_write(sd, RESET, CSI_GPIO_LOW); /* 将RESET引脚电频置为低电平 */
usleep_range(1000, 1200);
vin_set_pmu_channel(sd, IOVDD, ON); /* 按照时序来分别给IOVDD、DVDD、AVDD供电 */
usleep_range(1000, 1200);
vin_set_pmu_channel(sd, DVDD, ON);
usleep_range(1000, 1200);
vin_set_pmu_channel(sd, AVDD, ON);
usleep_range(1000, 1200);
vin_gpio_write(sd, PWDN, CSI_GPIO_HIGH); /* 将PWDN、RESET置为高电平 */
usleep_range(10000, 12000);
vin_gpio_write(sd, RESET, CSI_GPIO_HIGH);
usleep_range(10000, 12000);
cci_unlock(sd);
break;
}
曝光接口实现
Sensor 曝光接口是在 sensor_s_exp
函数中实现的(YUV 格式的 Sensor 不需要实现返回0即可),需要翻阅 Sensor datasheet
查找控制曝光的相关寄存器,一般使用的是手动曝光模式。如下是 gc2053 控制曝光所需要操作的寄存器。
由于主控端的 ISP 曝光是以 16 为一行的,而本例中 gc2053 也是以一行为单位的,所以只需要将传入的曝光值exp_val 除以 16 按照 Sensor datasheet 将值写入对应的寄存器。
static int sensor_s_exp(struct v4l2_subdev *sd, unsigned int exp_val)
{
struct sensor_info *info = to_state(sd);
int tmp_exp_val = exp_val / 16; /* exp_val / 16 得到一行的曝光值 */
sensor_dbg("exp_val:%d\n", exp_val);
sensor_write(sd, 0x03, (tmp_exp_val >> 8) & 0xFF); /* 将曝光值的高8位填入gc2053的控制曝光高位寄存器 */
sensor_write(sd, 0x04, (tmp_exp_val & 0xFF)); /* 将曝光值的低8位填入gc2053的控制曝光低位寄存器 */
info->exp = exp_val;
return 0;
}
注:若所调试的 Sensor 是以 1/16 行为单位的,则传入的曝光值 exp_val 可以直接写入 Sensor 曝光寄存器,若所调试的 Sensor 是以半行为单位的,则传入的曝光值 exp_val 需要除以 16 得到一行的曝光值然后乘以 2 后写入 Sensor 曝光寄存器
增益接口实现
Sensor 增益接口是在 sensor_s_gain
函数中实现的(YUV 格式的 Sensor 不需要实现返回0即可),每个 Sensor 的 Gain Table 都是不一样的,这个可以询问一下 Sensor 原厂 Gain Table 填写方式或者翻阅 Sensor datasheet 进行查找。
本次以思特威 sc1346 为例对 Sensor 增益接口实现进行一个简单的说明,如下是 sc1346 的模拟增益表,一般会使用模拟增益,思特威的 Sensor 会使用数字增益对模拟增益进行一个平滑过渡。主控端的 ISP 增益精度是 1/16(也就是 16 为一倍增益)。
static int sensor_s_gain(struct v4l2_subdev *sd, int gain_val)
{
struct sensor_info *info = to_state(sd);
data_type anagain = 0x00;
data_type gaindiglow = 0x80;
data_type gaindighigh = 0x00;
int gain_tmp;
gain_tmp = gain_val << 3;
if (gain_val < 32) { /* 对应数据手册一倍模拟增益,一倍模拟增益到两倍模拟增益(不含两倍模拟增益)以内对应的寄存器值为0x00 */
anagain = 0x00;
gaindighigh = 0x00;
gaindiglow = gain_tmp; /* 数字增益的计算方式需要询问sensor原厂,这里以思特威提供的计算方式进行一个示例 */
} else if (gain_val < 64) {
anagain = 0x08; /* 对应数据手册两倍模拟增益,两倍模拟增益到三倍模拟增益(不含三倍模拟增益)以内对应的寄存器值为0x08 */
gaindighigh = 0x00;
gaindiglow = gain_tmp * 100 / 200/ 1;
} else if (gain_val < 128) {
anagain = 0x09;
gaindighigh = 0x00;
gaindiglow = gain_tmp * 100 / 200 / 2;
} else if (gain_val < 256) {
anagain = 0x0b;
gaindighigh = 0x00;
gaindiglow = gain_tmp * 100 / 200 / 4;
} else if (gain_val < 512) {
anagain = 0x0f;
gaindighigh = 0x00;
gaindiglow = gain_tmp * 100 / 200 / 8;
} else if (gain_val < 1024) {
anagain = 0x1f;
gaindighigh = 0x00;
gaindiglow = gain_tmp * 100 / 200 / 16;
} else if (gain_val < 2048) { /* 数据手册标明最大增益只能到32倍,所以后面的增益模拟增益寄存器值都填写为0x1f */
anagain = 0x1f;
gaindighigh = 0x01;
gaindiglow = gain_tmp * 100 / 200 / 32;
} else if (gain_val < 4096) {
anagain = 0x1f;
gaindighigh = 0x03;
gaindiglow = gain_tmp * 100 / 200 / 64;
} else if (gain_val < 8192) {
anagain = 0x1f;
gaindighigh = 0x07;
gaindiglow = gain_tmp * 100 / 200 / 128;
} else {
anagain = 0x1f;
gaindighigh = 0x07;
gaindiglow = 0xfc;
}
sensor_write(sd, 0x3e09, (unsigned char)anagain); /* 将增益值写入sensor模拟增益寄存器 */
sensor_write(sd, 0x3e07, (unsigned char)gaindiglow); /* 将增益值写入sensor数字增益低位寄存器 */
sensor_write(sd, 0x3e06, (unsigned char)gaindighigh); /* 将增益值写入sensor数字增益高位寄存器 */
sensor_dbg("sensor_set_anagain = %d, 0x%x, 0x%x, 0x%x Done!\n", gain_val, anagain, gaindighigh, gaindiglow);
//sensor_dbg("digital_gain = 0x%x, 0x%x Done!\n", gaindighigh, gaindiglow);
info->gain = gain_val;
return 0;
}
上述 Sensor 驱动的曝光接口和增益接口实现之后,还需要将接口注册到 vin v4l2 框架中,以便上层应用可以调用到相关的接口控制 Sensor 的曝光和增益。
sensor_init_controls
函数添加增益和曝光控件选项,如下:
static int sensor_init_controls(struct v4l2_subdev *sd, const struct v4l2_ctrl_ops *ops)
{
v4l2_ctrl_handler_init(handler, 2);
ctrl = v4l2_ctrl_new_std(handler, ops, V4L2_CID_GAIN, 1 * 1600, 256 * 1600, 1, 1 * 1600);
ctrl = v4l2_ctrl_new_std(handler, ops, V4L2_CID_EXPOSURE, 1, 65536 * 16, 1, 1);
if (ctrl != NULL)
ctrl->flags |= V4L2_CTRL_FLAG_VOLATILE;
return ret;
}
sensor_s_ctrl
函数添加 V4L2_CID_GAIN
和 V4L2_CID_EXPOSURE
,对应之前实现的 sensor_s_gain
和 sensor_s_exp
函数。sensor_s_ctrl
下曝光和增益的实现,在于当处于手动曝光模式下,上层可以通过 API 调用到 Sensor 驱动实现的 sensor_s_exp
和 sensor_s_gain
函数。
static int sensor_s_ctrl(struct v4l2_ctrl *ctrl)
{
struct sensor_info *info =
container_of(ctrl->handler, struct sensor_info, handler);
struct v4l2_subdev *sd = &info->sd;
switch (ctrl->id) {
case V4L2_CID_GAIN:
return sensor_s_gain(sd, ctrl->val);
case V4L2_CID_EXPOSURE:
return sensor_s_exp(sd, ctrl->val);
}
return -EINVAL;
}
sensor_s_exp_gain
接口主要用于上层 ISP 算法库做完 3A 算法之后通过 VIDIOC_VIN_SENSOR_EXP_GAIN
给 Sensor 驱动更新曝光和增益。
static int sensor_s_exp_gain(struct v4l2_subdev *sd, struct sensor_exp_gain *exp_gain)
{
int exp_val, gain_val;
int shutter = 0, frame_length = 0;
struct sensor_info *info = to_state(sd);
exp_val = exp_gain->exp_val;
gain_val = exp_gain->gain_val;
if (gain_val < (1 * 16)) { /* 限制最小增益为1倍 */
gain_val = 16;
}
if (exp_val > 0xfffff) /* 限制最大曝光值 */
exp_val = 0xfffff;
sensor_s_exp(sd, exp_val);
sensor_s_gain(sd, gain_val);
info->exp = exp_val;
info->gain = gain_val;
return 0;
}
static long sensor_ioctl(struct v4l2_subdev *sd, unsigned int cmd, void *arg)
{
int ret = 0;
struct sensor_info *info = to_state(sd);
switch (cmd) {
case VIDIOC_VIN_SENSOR_EXP_GAIN:
sensor_s_exp_gain(sd, (struct sensor_exp_gain *)arg);
break;
...
default:
return -EINVAL;
}
return ret;
}
翻转接口的实现
Sensor 设置翻转场景:
-
由于物理结构设计错误,导致镜头或者 Sensor 反装,需要在软件上电时就默认翻转回来正确的方向。
-
软件规格上需要支持在线设置翻转的功能(SDV、IPC、CDR),供设备倒装放置。
翻转功能有两种实现方式:VIPP 翻转,Sensor 驱动翻转, 应用层 API 如下:
// V821平台
platform/allwinner/eyesee-mpp/middleware/sun300iw1/media/mpi_vi.c
// 设置 VIPP 翻转,如果输出的图像格式为 LBC 压缩格式的话,不可以使用 VIPP 翻转
AW_S32 AW_MPI_VI_SetVippFlip(VI_DEV ViDev, int Value);
AW_S32 AW_MPI_VI_SetVippMirror(VI_DEV ViDev, int Value);
// 设置 Sensor 驱动翻转,需要按照下文实现驱动的翻转函数
AW_S32 AW_MPI_ISP_SetMirror(VI_DEV ViDev, int Value);
AW_S32 AW_MPI_ISP_SetFlip(VI_DEV ViDev, int Value);
VIPP 实现翻转
主控端 VIPP 硬件已集成翻转功能,可以完成图像水平、垂直两个方向翻转,但使用时需要留意,当 VIPP 输出格式为 LBC 压缩格式时,VIPP 翻转功能不能使用,这个时候可以选择使用 Sensor 驱动完成翻转功能。
Sensor 驱动实现翻转
执行 make kernel_menuconfig
,将 CONFIG_ENABLE_SENSOR_FLIP_OPTION
打开,意味着使用 Sensor 驱动完成翻转,不使用 VIPP 翻转:
→ Allwinner BSP → Device Drivers → VIN (camera) Drivers → select Sensor flip to replace vipp flip
第一步,实现 sensor_s_vflip
和 sensor_s_hflip
函数。sensor_s_vflip
用于设置垂直翻转,sensor_s_hflip
用于设置水平翻转。
以思特威 sc3336 为例,规格书中会有翻转寄存器及翻转方向的说明,使用细节及注意事项可以咨询 Sensor 原厂。 下图是 sc3336 规格书中对翻转功能的描述:
Sensor 驱动翻转接口实现如下:
/* 设置水平翻转 */
static int sensor_s_hflip(struct v4l2_subdev *sd, int enable)
{
data_type get_value;
data_type set_value;
if (!(enable == 0 || enable == 1))
return -1;
sensor_read(sd, 0x3221, &get_value); /* 读取sc3336翻转寄存器值,获取当前的翻转状态 */
sensor_dbg("ready to flip, regs_data = 0x%x\n", get_value);
if (enable)
set_value = get_value | 0x06; /* 水平翻转被使能,设置sc3336翻转寄存器的[2:1]为11 */
else
set_value = get_value & 0xf9; /* 水平翻转被禁用,为了不影响垂直翻转效果以及其他寄存器位,此处 & 0xF9保留水平翻转效果 */
sensor_write(sd, 0x3221, set_value); /* 将翻转值写入翻转寄存器 */
sensor_flip_status = set_value; /* 全局变量记录当前翻转的状态 */
return 0;
}
/* 设置垂直翻转 */
static int sensor_s_vflip(struct v4l2_subdev *sd, int enable)
{
data_type get_value;
data_type set_value;
if (!(enable == 0 || enable == 1))
return -1;
sensor_read(sd, 0x3221, &get_value); /* 读取sc3336翻转寄存器值,获取当前的翻转状态 */
sensor_dbg("ready to vflip, regs_data = 0x%x\n", get_value);
if (enable) {
set_value = get_value | 0x60; /* 垂直翻转被使能,设置sc3336翻转寄存器的[6:5]为11 */
} else {
set_value = get_value & 0x9f; /* 垂直翻转被禁用,为了不影响水平翻转效果以及其他寄存器位,此处 & 0x9f保留水平翻转效果 */
}
sensor_write(sd, 0x3221, set_value); /* 将翻转值写入翻转寄存器 */
sensor_flip_status = set_value; /* 全局变量记录当前翻转的状态 */
return 0;
}
第二步,修改 sensor_get_fmt_mbus_core
接口
RAW Sensor 图像是以 Bayer 格式传输的(每个像素只表示 RGB 其中一个分量),常见的 Bayer 格式:RGGB、BGGR、GRBG、GBRG,如图所示,Sensor 规格书中会有说明,也可以直接咨询 Sensor 原厂获取。
因为 sc3336 在翻转后自身的 Bayer 格式也会随之发生变化,需要修改 sensor_get_fmt_mbus_core 函数调整 Bayer 格式,如下:
/* 以sc3336为例 */
默认格式:BGGR 执行水平翻转:GBRG 再执行垂直翻转:RGGB
排列位置: 排列位置: 排列位置:
BG ==> GB ==> RG
GR RG GB
默认格式:BGGR 执行垂直翻转:GRBG 再执行水平翻转:RGGB
排列位置: 排列位置: 排列位置:
BG ==> GR ==> RG
GR BG GB
/* 将四种情况一一列举,并更新给 *code */
static int sensor_get_fmt_mbus_core(struct v4l2_subdev *sd, int *code)
{
struct sensor_info *info = to_state(sd);
data_type get_value;
sensor_read(sd, 0x3221, &get_value);
sensor_dbg("read value:0x%x\n", get_value); //读取翻转寄存器获取当前sensor的翻转状态
switch (get_value & 0x66) {
case 0x00: /* sensor当前没有做翻转操作,Bayer格式为sensor初始化时默认的Bayer格式 */
*code = MEDIA_BUS_FMT_SBGGR10_1X10;
break;
case 0x06: /* sensor当前处于水平翻转,Bayer格式为GBRG */
*code = MEDIA_BUS_FMT_SGBRG10_1X10;
break;
case 0x60: /* sensor当前处于垂直翻转,Bayer格式为GRBG */
*code = MEDIA_BUS_FMT_SGRBG10_1X10;
break;
case 0x66: /* sensor当前水平和垂直同时翻转,Bayer格式为RGGB */
*code = MEDIA_BUS_FMT_SRGGB10_1X10;
break;
default:
*code = info->fmt->mbus_code;
}
return 0;
}
第三步,上述 Sensor 驱动的翻转接口实现之后,还需要将接口注册到 vin v4l2 框架中,以便上层应用可以调用到相关的接口控制 Sensor 的翻转。
sensor_init_controls
函数添加水平和垂直翻转控件选项,如下:
static int sensor_init_controls(struct v4l2_subdev *sd, const struct v4l2_ctrl_ops *ops)
{
v4l2_ctrl_handler_init(handler, 2);
v4l2_ctrl_new_std(handler, ops, V4L2_CID_HFLIP, 0, 1, 1, 0);
v4l2_ctrl_new_std(handler, ops, V4L2_CID_VFLIP, 0, 1, 1, 0);
if (ctrl != NULL)
ctrl->flags |= V4L2_CTRL_FLAG_VOLATILE;
return ret;
}
sensor_s_ctrl
函数添加 V4L2_CID_HFLIP
和 V4L2_CID_VFLIP
,对应上述的 sensor_s_hflip
和 sensor_s_vflip
函数。
static int sensor_s_ctrl(struct v4l2_ctrl *ctrl)
{
struct sensor_info *info =
container_of(ctrl->handler, struct sensor_info, handler);
struct v4l2_subdev *sd = &info->sd;
switch (ctrl->id) {
case V4L2_CID_HFLIP:
return sensor_s_hflip(sd, ctrl->val);
case V4L2_CID_VFLIP:
return sensor_s_vflip(sd, ctrl->val);
}
return -EINVAL;
}
第四步,sensor_ctrl_ops
添加 .try_ctrl = sensor_try_ctrl,
防止重复调用会失效:
// 仅添加对应代码即可
static const struct v4l2_ctrl_ops sensor_ctrl_ops = {
.g_volatile_ctrl = sensor_g_ctrl,
.s_ctrl = sensor_s_ctrl,
.try_ctrl = sensor_try_ctrl, // 防止重复调用会失效
};
注:如果需要在上电初始化时就完成翻转动作,那么可以在 Sensor 初始化寄存器列表中添加翻转寄存器设置,是否支持需要咨询 Sensor 原厂。
自动降帧的实现
Sensor 帧率调节场景:
- 环境照度变化(如照度降低),Sensor 在 ISP 的引导下被动降低帧率,以延长曝光时间来提升画面亮度和信噪比。
- 高温环境、缩时录影、高负载下降帧率等应用场景,通过 API 给 Sensor 设置帧率,以达到主动调节帧率应对不同场景。
实现自动降帧功能需要对以下几个参数有一个了解:
- 帧:描述一个图像帧(一副图像)
- 最大曝光时间:当前稳定帧率下,Sensor 支持的最大曝光时间,比如帧率是20fps,则最大曝光时间:1 / 20fps = 50ms,如果要加长曝光时间,那么意味着帧率需要下调,以此来满足 Sensor 更长的曝光时间
- PCLK:像素时钟,是指控制图像传感器中每个像素采样和传输的时钟信号。
- HTS:行长,一行的长度,可以理解为包含了 hblank 一行的像素数。
- VTS:帧长,一帧的长度,可以理解为包含的行数。
- FPS:sensor单位时间内曝光并输出图像的频率,比如帧率是20fps,则代表1秒内输出20帧图像。
- PCLK、HTS、VTS、FPS满足以下公式(主流 Sensor 通用): PCLK = HTS * VTS * FPS
由此可以推导出 Sensor 帧率由 HTS、VTS、PCLK 决定:FPS = PCLK / (HTS * VTS)
注:Sensor 动态调整帧率的实现方式(大部分 Sensor 都是支持,有些 Sensor 可能操作上存在一点差异,需要咨询 Sensor 原厂),正是通过动态调整 VTS(HTS 也是可以的,只不过不太常用,也有部分 Sensor 可能只能通过调整 HTS 来实现)来实时调整帧率的。
动态调整帧率
第一步,确认 Sensor 的 VTS、
HTS 寄存器,Sensor 规格书中,一般会标明 VTS 寄存器的详情和使用方式。如下图所示为思特威 SC3336 VTS 寄存器说明:
第二步,实现 sensor_s_fps 函数
Sensor 驱动中提供 sensor_s_fps
函数来动态设置 Sensor 帧率:
static int sensor_s_fps(struct v4l2_subdev *sd, struct sensor_fps *fps)
以 sc3336 为例(提前与 Sensor 原厂确认 Sensor 是否支持自动降帧且满足公式),通过公式:FPS = PCLK / (HTS * VTS)
来实时计算目标帧率需要的 VTS 并动态设置寄存器,其中要注意做好最大/低帧率限制,防止出现异常情况导致不出图,提供参考代码如下:
static int sc3336_sensor_vts; // 用于自动降帧策略
static int sc3336_fps_change_flag; // 帧率变化标志位
static int sensor_s_fps(struct v4l2_subdev *sd, struct sensor_fps *fps)
{
struct sensor_info *info = to_state(sd);
struct sensor_win_size *wsize = info->current_wins;
int sc3336_sensor_target_vts = 0;
if (fps->fps <= 0 || wsize->hts <= 0) {
sensor_err("fps->fps = %d, wsize->hts = %d\n!!!!", fps->fps, wsize->hts);
return -1;
}
sc3336_fps_change_flag = 1;
sc3336_sensor_target_vts = wsize->pclk / fps->fps / wsize->hts; // 计算目标帧率需要的 VTS 大小
if (sc3336_sensor_target_vts <= wsize->vts) { // the max fps // 最大帧率保持,最大帧率限定在原始 setting 输出的帧率,可根据需求自行调整
sc3336_sensor_target_vts = wsize->vts;
} else if (sc3336_sensor_target_vts >= (wsize->pclk / wsize->hts)) { // the min fps 最小帧率限制在 1fps,需要与 FAE 沟通此帧率下是否有注意事项,如 VTS 偏移量的控制
sc3336_sensor_target_vts = (wsize->pclk / wsize->hts) - 8;
}
sc3336_sensor_vts = sc3336_sensor_target_vts;
sensor_dbg("target_fps = %d, sc3336_sensor_target_vts = %d, 0x320e = 0x%x, 0x320f = 0x%x\n", fps->fps,
sc3336_sensor_target_vts, sc3336_sensor_target_vts >> 8, sc3336_sensor_target_vts & 0xff);
sensor_write(sd, 0x320f, (sc3336_sensor_target_vts & 0xff)); // 更新 VTS 寄存器,具体寄存器地址以具体 Sensor 为准
sensor_write(sd, 0x320e, (sc3336_sensor_target_vts >> 8));
sc3336_fps_change_flag = 0;
return 0;
}
第三步,实现 sensor_g_fps
函数用于获取帧率
static int sensor_g_fps(struct v4l2_subdev *sd, struct sensor_fps *fps)
{
struct sensor_info *info = to_state(sd);
struct sensor_win_size *wsize = info->current_wins;
data_type frame_length = 0, act_vts = 0;
sensor_read(sd, 0x320f, &frame_length);
act_vts = frame_length << 8;
sensor_read(sd, 0x320e, &frame_length);
act_vts |= frame_length;
fps->fps = wsize->pclk / (wsize->hts * act_vts);
sensor_dbg("fps = %d\n", fps->fps);
return 0;
}
第四步, 修改 sensor_ioctl
函数,添加 VIDIOC_VIN_SENSOR_SET_FPS
和 VIDIOC_VIN_SENSOR_GET_FPS
static long sensor_ioctl(struct v4l2_subdev *sd, unsigned int cmd, void *arg)
{
int ret = 0;
struct sensor_info *info = to_state(sd);
switch (cmd) {
case VIDIOC_VIN_SENSOR_GET_FPS:
ret = sensor_g_fps(sd, (struct sensor_fps *)arg);
break;
case VIDIOC_VIN_SENSOR_SET_FPS:
ret = sensor_s_fps(sd, (struct sensor_fps *)arg);
break;
default:
return -EINVAL;
}
return ret;
}
第五步,定义一个全局变量(双目场景共用驱动时需要做区分),用于记录当前的 vts 值,并在 sensor_reg_init
中初始化,如下:
static int sensor_reg_init(struct sensor_info *info)
{
int ret;
struct v4l2_subdev *sd = &info->sd;
struct sensor_format_struct *sensor_fmt = info->fmt;
struct sensor_win_size *wsize = info->current_wins;
ret = sensor_write_array(sd, sensor_default_regs,ARRAY_SIZE(sensor_default_regs));
if (ret < 0) {
sensor_err("write sensor_default_regs error\n");
return ret;
}
sensor_dbg("sensor_reg_init\n");
sensor_write_array(sd, sensor_fmt->regs, sensor_fmt->regs_size);
if (wsize->regs)
sensor_write_array(sd, wsize->regs, wsize->regs_size);
if (wsize->set_size)
wsize->set_size(sd);
info->width = wsize->width;
info->height = wsize->height;
sc3336_sensor_vts = wsize->vts; //初始化sc3336_sensor_vts
sensor_dbg("s_fmt set width = %d, height = %d\n", wsize->width, wsize->height);
return 0;
}
第六步,应用层调用 API 设置帧率
platform/allwinner/eyesee-mpp/middleware/sun300iw1/media/mpi_vi.c
AW_S32 AW_MPI_ISP_SetSensorFps(ISP_DEV IspDev, int fps);
自动降帧
自动降帧场景一般比较常见:当拍摄场景进入低照度后,ISP 通过延长曝光时间使得 Sensor 自动降帧,从而提高信噪比。
第一步,自动降帧的实现方式大同小异,一般是在 sensor_s_exp_gain
函数中检查当前设置的曝光时间是否超过当前帧率对应的 vts,如果超过则动态调整 vts 来延长曝光时间,如果没有则不需要调整 vts,以 sc3336 为例:
static int sensor_s_exp_gain(struct v4l2_subdev *sd, struct sensor_exp_gain *exp_gain)
{
struct sensor_info *info = to_state(sd);
int shutter, frame_length;
int exp_val, gain_val;
exp_val = exp_gain->exp_val;
gain_val = exp_gain->gain_val;
if (gain_val < 1 * 16)
gain_val = 16;
if (exp_val > 0xfffff)
exp_val = 0xfffff;
if (!sc3336_fps_change_flag) { // 检查是否在主动调节帧率,通过标志位或者锁互斥即可
shutter = exp_val >> 4; // 主控 曝光行(时间)是以16为1行,所以将上层传下来的曝光行除以16,换算当前实际曝光行
if (shutter > sc3336_sensor_vts - 8) { // 判断当前曝光时间是否大于当前帧率下的 VTS(“-8”只是偏移量,每个sensor都是不一样的)
frame_length = shutter + 8; // 如果大于当前帧率下的 VTS,那么意味着需要增加 VTS 来达到降帧的目的,以此实现自动降帧
} else
frame_length = sc3336_sensor_vts; // 如果曝光时间未达到需要调整 VTS 来降帧的情况下,那么还是保持当前帧率需要 VTS 值即可
sensor_write(sd, 0x320f, (frame_length & 0xff));
sensor_write(sd, 0x320e, (frame_length >> 8));
}
sensor_s_exp(sd, exp_val);
sensor_s_gain(sd, gain_val);
sensor_dbg("sensor_set_gain exp = %d, %d Done!\n", gain_val, exp_val);
info->exp = exp_val;
info->gain = gain_val;
return 0;
}
第二步,调整 ISP 效果文件的最大曝光时间
以下是对 AE Table 进行一个简要的说明, AE Table 用于设定当前效果的曝光表,如图所示,Min Exp 代表最小曝光时间(倒数)—— 1/22000s,Max Exp 代表最大曝光时间(倒数)—— 1/20 s,Min Gain 代表最小增益、Max Gain 代表最大增益(均以 256 为一倍),光圈调节暂不支持,故需要设置为默认值266。
AE 调整亮度的逻辑:第 0 档为起始档位,`AE 会保持一倍增益的前提下,优先提高曝光时间来达到提升亮度的目的,曝光时间的可调范围为 1/22000s,1/20s,如果曝光时间提高到 1/20s 的状态还没有达到期望亮度,那么会开始向下顺延执行第1档的设定:维持曝光时间为 1/20s 的前提,开始提高增益的倍数,增益的可调范围[256,32768](也就是 1x ~ 128x),以此类推,直到达到最大曝光时间和最大增益。
如果当前帧率为 20fps,则需要限制最大曝光时间为 1/20s,那么意味着 ISP 给驱动设置的最大曝光时间则不会大于 1/20s(50ms),如果有自动降帧需求,那么可以将最大曝光时间设置大于 1/20s,例如设置为 1/10s,意味着最大曝光时间从 50ms 调整到 100ms,驱动发现当前曝光时间大于当前 vts 时,便会重新调整 vts 来降帧延长曝光时间。
同样,也可以通过修改当前 Sensor 的 ISP 效果头文件来验证自动降帧功能是否生效。
可以通过指令 cat /sys/kernel/debug/mpp/vi
查看 vi 结点的帧间隔来检查降帧是否有生效,如下,internal 指的是当前 Sensor 输入图像的帧间隔时长,50ms 对应 20fps:
当降帧策略生效时(API 设置帧率或自动降帧),可以看到 50ms 更新为 100ms,意味着帧率从 20fps 下降到 10fps:
两个 Sensor 共用一份 Sensor 驱动
全志平台支持两个 Sensor 共用一份 Sensor 驱动,需要修改的内容如下,也可以参考 SDK 中已经支持两个 Sensor 共用一份 Sensor 驱动的驱动。
/* 下面的设置代表有两个sensor,同名sensor可以共用同一个驱动,SENSOR_NAME要与board.dts中的sensor0_mname一致 */
#define SENSOR_NUM 0x2
#define SENSOR_NAME "gc2053_mipi"
#define SENSOR_NAME_2 "gc2053_mipi_2"
static int sensor_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
...
if (client) {
for (i = 0; i < SENSOR_NUM; i++) {
if (!strcmp(cci_drv[i].name, client->name))
break;
}
cci_dev_probe_helper(sd, client, &sensor_ops, &cci_drv[i]);
} else {
cci_dev_probe_helper(sd, client, &sensor_ops, &cci_drv[sensor_dev_id++]);
}
...
return 0;
}
static int sensor_remove(struct i2c_client *client)
{
...
if (client) {
for (i = 0; i < SENSOR_NUM; i++) {
if (!strcmp(cci_drv[i].name, client->name))
break;
}
sd = cci_dev_remove_helper(client, &cci_drv[i]);
} else {
sd = cci_dev_remove_helper(client, &cci_drv[sensor_dev_id++]);
}
...
return 0;
}
static const struct i2c_device_id sensor_id[] = {
{SENSOR_NAME, 0},
{}
};
static const struct i2c_device_id sensor_id_2[] = {
{SENSOR_NAME_2, 0},
{}
};
MODULE_DEVICE_TABLE(i2c, sensor_id);
MODULE_DEVICE_TABLE(i2c, sensor_id_2);
static struct i2c_driver sensor_driver[] = {
{
.driver = {
.owner = THIS_MODULE,
.name = SENSOR_NAME,
},
.probe = sensor_probe,
.remove = sensor_remove,
.id_table = sensor_id,
}, {
.driver = {
.owner = THIS_MODULE,
.name = SENSOR_NAME_2,
},
.probe = sensor_probe,
.remove = sensor_remove,
.id_table = sensor_id_2,
},
};
static __init int init_sensor(void)
{
int i, ret = 0;
sensor_dev_id = 0;
for (i = 0; i < SENSOR_NUM; i++)
ret = cci_dev_init_helper(&sensor_driver[i]);
return ret;
}
static __exit void exit_sensor(void)
{
int i;
sensor_dev_id = 0;
for (i = 0; i < SENSOR_NUM; i++)
cci_dev_exit_helper(&sensor_driver[i]);
}
DVP接口
DVP 接口 Sensor 配置方式大部分内容与 MIPI 接口一样,有以下几处地方需要进行修改:
定义分辨率相关配置
分辨率相关配置填写在 sensor_win_sizes 数组内成员变量,各成员变量定义和填写规则如下:
//dvp接口的YUV sensor
static struct sensor_win_size sensor_win_sizes[] = {
{
.width = HD1080_WIDTH,
.height = HD1080_HEIGHT,
.hoffset = 0,
.voffset = 0,
.fps_fixed = 25,
.regs = reg_1080p25_2ch,
.regs_size = ARRAY_SIZE(reg_1080p25_2ch),
.set_size = NULL,
},
}
//dvp接口的raw sensor
static struct sensor_win_size sensor_win_sizes[] = {
{
.width = VGA_WIDTH,
.height = VGA_HEIGHT,
.hoffset = 0,
.voffset = 0,
.hts = 640,
.vts = 480,
.pclk = 9216 * 1000,
.fps_fixed = 1,
.bin_factor = 1,
.intg_min = 1,
.intg_max = 480 << 4,
.gain_min = 1 << 4,
.gain_max = 10 << 4,
.regs = sensor_vga_regs,
.regs_size = ARRAY_SIZE(sensor_vga_regs),
.set_size = NULL,
},
}
配置DVP接口
sensor_g_mbus_config 函数赋值也需要进行一个修改,如下:
//dvp接口 BT601 协议的 raw/YUV sensor
static int sensor_g_mbus_config(struct v4l2_subdev *sd, struct v4l2_mbus_config *cfg)
{
cfg->type = V4L2_MBUS_PARALLEL;
cfg->flags = V4L2_MBUS_MASTER | VREF_POL | HREF_POL | CLK_POL;
return 0;
}
//dvp接口 BT656/BT1120 协议的 YUV sensor
static int sensor_g_mbus_config(struct v4l2_subdev *sd, struct v4l2_mbus_config *cfg)
{
cfg->type = V4L2_MBUS_BT656;
cfg->flags = CLK_POL | CLK_POH | CSI_CH_0 | CSI_CH_1; //与当前AHD RX使用的初始化寄存器配置有关,当前使用的是2chn
return 0;
}
对于 interlace 隔行输入 dvp 接口的 sensor,需要在 sensor_probe 函数将成员变量 sensor_field 修改为 V4L2_FIELD_INTERLACED
,如下:
static int sensor_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
struct v4l2_subdev *sd;
struct sensor_info *info;
int ret;
info = kzalloc(sizeof(struct sensor_info), GFP_KERNEL);
if (info == NULL)
return -ENOMEM;
sd = &info->sd;
cci_dev_probe_helper(sd, client, &sensor_ops, &cci_drv);
sensor_init_controls(sd, &sensor_ctrl_ops);
mutex_init(&info->lock);
info->fmt = &sensor_formats[0];
info->fmt_pt = &sensor_formats[0];
info->win_pt = &sensor_win_sizes[0];
info->fmt_num = N_FMTS;
info->win_size_num = N_WIN_SIZES;
info->sensor_field = V4L2_FIELD_INTERLACED; /* sensor_field 修改为 V4L2_FIELD_INTERLACED */
}