[RFC PATCH 6/8] media: i2c: ov2312: add Omnivison OV2312 driver
From: Rishikesh Donadkar
Date: Fri Sep 25 2026 - 09:34:48 EST
From: Jai Luthra <j-luthra@xxxxxx>
Omnivision OV2312 is an RGB-IR sensor, i.e. it uses a 4x4 R,G,B,Ir bayer
pattern to capture both visible and near-infrared light. Every alternate
frame, the sensor changes the exposure and IR flash strobe registers to
stream an -
A. IR-dominant frame on CSI-2 virtual channel 0
B. RGB-dominant frame on CSI-2 virtual channel 1
These A/B frames are routed as separate v4l2 streams, which may be
mapped to two separate /dev/videoX nodes by the CSI-RX DMA driver.
Both of these streams are captured at a resolution of 1600x1301, 30 fps
each (60fps total). The extra row (1301 vs 1300) is an embedded line
prepended to each frame by the sensor, containing the following register
values:
0x4813 - VC (Virtual Channel)
0x321A - Group ID
0x3920 - Strobe
0x3501 - Exposure HI
0x3502 - Exposure LO
0x3508 - Gain HI
0x3509 - Gain LO
0x350e - Current Exposure HI
0x350f - Current Exposure LO
This driver also supports a few v4l2 controls like horizontal/vertical
flip, multi exposure and multi gain controls.
Signed-off-by: Jai Luthra <j-luthra@xxxxxx>
Signed-off-by: Rishikesh Donadkar <r-donadkar@xxxxxx>
---
drivers/media/i2c/Kconfig | 12 +
drivers/media/i2c/Makefile | 1 +
drivers/media/i2c/ov2312.c | 939 +++++++++++++++++++++++++++++++++++++
drivers/media/i2c/ov2312.h | 285 +++++++++++
4 files changed, 1237 insertions(+)
create mode 100644 drivers/media/i2c/ov2312.c
create mode 100644 drivers/media/i2c/ov2312.h
diff --git a/drivers/media/i2c/Kconfig b/drivers/media/i2c/Kconfig
index 4d9946479160..8d9d8d491b2e 100644
--- a/drivers/media/i2c/Kconfig
+++ b/drivers/media/i2c/Kconfig
@@ -496,6 +496,18 @@ config VIDEO_OV13B10
This is a Video4Linux2 sensor driver for the OmniVision
OV13B10 camera.
+config VIDEO_OV2312
+ tristate "OmniVision OV2312 sensor support"
+ depends on I2C && VIDEO_DEV
+ select MEDIA_CONTROLLER
+ select VIDEO_V4L2_SUBDEV_API
+ help
+ This is a Video4Linux2 sensor driver for the OmniVision
+ OV2312 camera.
+
+ To compile this driver as a module, choose M here: the
+ module will be called ov2312.
+
config VIDEO_OV2640
tristate "OmniVision OV2640 sensor support"
help
diff --git a/drivers/media/i2c/Makefile b/drivers/media/i2c/Makefile
index fd1cb25718c0..e019f174c5c0 100644
--- a/drivers/media/i2c/Makefile
+++ b/drivers/media/i2c/Makefile
@@ -98,6 +98,7 @@ obj-$(CONFIG_VIDEO_OV08D10) += ov08d10.o
obj-$(CONFIG_VIDEO_OV08X40) += ov08x40.o
obj-$(CONFIG_VIDEO_OV13858) += ov13858.o
obj-$(CONFIG_VIDEO_OV13B10) += ov13b10.o
+obj-$(CONFIG_VIDEO_OV2312) += ov2312.o
obj-$(CONFIG_VIDEO_OV2640) += ov2640.o
obj-$(CONFIG_VIDEO_OV2659) += ov2659.o
obj-$(CONFIG_VIDEO_OV2680) += ov2680.o
diff --git a/drivers/media/i2c/ov2312.c b/drivers/media/i2c/ov2312.c
new file mode 100644
index 000000000000..1561e1b3602d
--- /dev/null
+++ b/drivers/media/i2c/ov2312.c
@@ -0,0 +1,939 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Omnivision OV2312 RGB-IR Image Sensor driver
+ *
+ * Copyright (c) 2022 Jai Luthra <j-luthra@xxxxxx>
+ */
+
+#include <linux/delay.h>
+#include <linux/clk.h>
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/pm_runtime.h>
+#include <linux/regmap.h>
+#include <linux/types.h>
+#include <linux/v4l2-mediabus.h>
+#include <linux/videodev2.h>
+#include <media/v4l2-subdev.h>
+#include <media/v4l2-ctrls.h>
+
+#include "ov2312.h"
+
+struct ov2312 {
+ struct device *dev;
+
+ struct clk *clk;
+ unsigned long clk_rate;
+
+ struct i2c_client *client;
+ struct regmap *regmap;
+ struct gpio_desc *reset_gpio;
+
+ struct v4l2_subdev sd;
+ struct media_pad pad;
+ struct v4l2_mbus_framefmt format;
+
+ struct v4l2_ctrl_handler ctrls;
+ struct v4l2_ctrl *exposure_multi;
+ struct v4l2_ctrl *again_multi;
+ struct v4l2_ctrl *dgain_multi;
+ struct v4l2_ctrl *h_flip;
+ struct v4l2_ctrl *v_flip;
+
+ u32 fps;
+
+ struct mutex lock; /* For streaming status */
+ unsigned int enable_count;
+};
+
+static inline struct ov2312 *to_ov2312(struct v4l2_subdev *sd)
+{
+ return container_of(sd, struct ov2312, sd);
+}
+
+static int ov2312_read(struct ov2312 *ov2312, u16 addr, u32 *val, size_t nbytes)
+{
+ int ret;
+ __le32 val_le = 0;
+
+ ret = regmap_bulk_read(ov2312->regmap, addr, &val_le, nbytes);
+ if (ret < 0) {
+ dev_err(ov2312->dev, "%s: failed to read reg 0x%04x: %d\n",
+ __func__, addr, ret);
+ return ret;
+ }
+
+ *val = le32_to_cpu(val_le);
+ return 0;
+}
+
+static int ov2312_write(struct ov2312 *ov2312, u16 addr, u32 val, size_t nbytes)
+{
+ int ret;
+ __le32 val_le = cpu_to_le32(val);
+
+ ret = regmap_bulk_write(ov2312->regmap, addr, &val_le, nbytes);
+ if (ret < 0)
+ dev_err(ov2312->dev, "%s: failed to write reg 0x%04x: %d\n",
+ __func__, addr, ret);
+ return ret;
+}
+
+static int ov2312_write_table(struct ov2312 *ov2312,
+ const struct reg_sequence *regs,
+ unsigned int nr_regs)
+{
+ int ret, i;
+
+ for (i = 0; i < nr_regs; i++) {
+ ret = regmap_write(ov2312->regmap, regs[i].reg, regs[i].def);
+ if (ret < 0) {
+ dev_err(ov2312->dev,
+ "%s: failed to write reg[%d] 0x%04x = 0x%02x (%d)!\n",
+ __func__, i, regs[i].reg, regs[i].def, ret);
+ return ret;
+ }
+ }
+ return 0;
+}
+
+static void ov2312_init_formats(struct v4l2_subdev_state *state)
+{
+ struct v4l2_mbus_framefmt *format;
+ int i;
+
+ for (i = 0; i < 2; ++i) {
+ format = v4l2_subdev_state_get_format(state, 0, i);
+ format->code = ov2312_mbus_formats[0];
+ format->width = ov2312_framesizes[0].width;
+ format->height = ov2312_framesizes[0].height;
+ format->field = V4L2_FIELD_NONE;
+ format->colorspace = V4L2_COLORSPACE_DEFAULT;
+ }
+}
+
+static int ov2312_set_fmt(struct v4l2_subdev *sd,
+ const struct v4l2_subdev_client_info *ci,
+ struct v4l2_subdev_state *state,
+ struct v4l2_subdev_format *fmt)
+{
+ struct ov2312 *ov2312 = to_ov2312(sd);
+ struct v4l2_mbus_framefmt *format;
+ const struct v4l2_area *fsize;
+ u32 code;
+ int ret = 0;
+
+ if (fmt->pad != 0)
+ return -EINVAL;
+
+ if (fmt->stream != 0)
+ return -EINVAL;
+
+ /* Sensor only supports a single format. */
+ code = ov2312_mbus_formats[0];
+
+ /* Find the nearest supported frame size. */
+ fsize = v4l2_find_nearest_size(ov2312_framesizes,
+ ARRAY_SIZE(ov2312_framesizes), width,
+ height, fmt->format.width,
+ fmt->format.height);
+
+ v4l2_subdev_lock_state(state);
+
+ /* Update the stored format and return it. */
+ format = v4l2_subdev_state_get_format(state, fmt->pad, fmt->stream);
+
+ if (fmt->which == V4L2_SUBDEV_FORMAT_ACTIVE && ov2312->enable_count) {
+ ret = -EBUSY;
+ goto done;
+ }
+
+ format->code = code;
+ format->width = fsize->width;
+ format->height = fsize->height;
+
+ fmt->format = *format;
+
+done:
+ v4l2_subdev_unlock_state(state);
+
+ return ret;
+}
+
+static int _ov2312_set_routing(struct v4l2_subdev *sd,
+ struct v4l2_subdev_state *state)
+{
+ struct v4l2_subdev_route routes[] = {
+ {
+ .source_pad = 0,
+ .source_stream = 0,
+ .flags = V4L2_SUBDEV_ROUTE_FL_ACTIVE,
+ },
+ {
+ .source_pad = 0,
+ .source_stream = 1,
+ .flags = V4L2_SUBDEV_ROUTE_FL_ACTIVE,
+ },
+ };
+
+ struct v4l2_subdev_krouting routing = {
+ .num_routes = ARRAY_SIZE(routes),
+ .routes = routes,
+ };
+
+ int ret;
+
+ ret = v4l2_subdev_set_routing(sd, state, &routing);
+ if (ret < 0)
+ return ret;
+
+ ov2312_init_formats(state);
+
+ return 0;
+}
+
+static int ov2312_get_frame_desc(struct v4l2_subdev *sd, unsigned int pad,
+ struct v4l2_mbus_frame_desc *fd)
+{
+ struct v4l2_subdev_state *state;
+ struct v4l2_mbus_framefmt *fmt;
+ u32 bpp;
+ int ret = 0;
+ unsigned int i;
+
+ if (pad != 0)
+ return -EINVAL;
+
+ state = v4l2_subdev_lock_and_get_active_state(sd);
+
+ fmt = v4l2_subdev_state_get_format(state, 0, 0);
+ if (!fmt) {
+ ret = -EPIPE;
+ goto out;
+ }
+
+ memset(fd, 0, sizeof(*fd));
+
+ fd->type = V4L2_MBUS_FRAME_DESC_TYPE_CSI2;
+
+ /* pixel stream - 2 virtual channels */
+
+ bpp = 10;
+
+ for (i = 0; i < 2; ++i) {
+ fd->entry[fd->num_entries].stream = i;
+
+ fd->entry[fd->num_entries].flags = V4L2_MBUS_FRAME_DESC_FL_LEN_MAX;
+ fd->entry[fd->num_entries].length = fmt->width * fmt->height * bpp / 8;
+ fd->entry[fd->num_entries].pixelcode = fmt->code;
+ fd->entry[fd->num_entries].bus.csi2.vc = i;
+ fd->entry[fd->num_entries].bus.csi2.dt = 0x2b; /* RAW10 */
+
+ fd->num_entries++;
+ }
+
+out:
+ v4l2_subdev_unlock_state(state);
+
+ return ret;
+}
+
+static int ov2312_set_routing(struct v4l2_subdev *sd,
+ struct v4l2_subdev_state *state,
+ enum v4l2_subdev_format_whence which,
+ struct v4l2_subdev_krouting *routing)
+{
+ int ret;
+
+ if (routing->num_routes == 0 || routing->num_routes > 2)
+ return -EINVAL;
+
+ v4l2_subdev_lock_state(state);
+
+ ret = _ov2312_set_routing(sd, state);
+
+ v4l2_subdev_unlock_state(state);
+
+ return ret;
+}
+
+static int ov2312_init_state(struct v4l2_subdev *sd,
+ struct v4l2_subdev_state *state)
+{
+ int ret;
+
+ ret = _ov2312_set_routing(sd, state);
+
+ return ret;
+}
+
+static int ov2312_enum_mbus_code(struct v4l2_subdev *sd,
+ struct v4l2_subdev_state *state,
+ struct v4l2_subdev_mbus_code_enum *code)
+{
+ if (code->index >= ARRAY_SIZE(ov2312_mbus_formats))
+ return -EINVAL;
+
+ code->code = ov2312_mbus_formats[code->index];
+
+ return 0;
+}
+
+static int ov2312_enum_frame_sizes(struct v4l2_subdev *sd,
+ struct v4l2_subdev_state *state,
+ struct v4l2_subdev_frame_size_enum *fse)
+{
+ unsigned int i;
+
+ for (i = 0; i < ARRAY_SIZE(ov2312_mbus_formats); ++i) {
+ if (ov2312_mbus_formats[i] == fse->code)
+ break;
+ }
+
+ if (i == ARRAY_SIZE(ov2312_mbus_formats))
+ return -EINVAL;
+
+ if (fse->index >= ARRAY_SIZE(ov2312_framesizes))
+ return -EINVAL;
+
+ fse->min_width = ov2312_framesizes[fse->index].width;
+ fse->max_width = fse->min_width;
+ fse->max_height = ov2312_framesizes[fse->index].height;
+ fse->min_height = fse->max_height;
+
+ return 0;
+}
+
+static int ov2312_get_frame_interval(struct v4l2_subdev *sd,
+ struct v4l2_subdev_state *state,
+ struct v4l2_subdev_frame_interval *fi)
+{
+ struct ov2312 *ov2312 = to_ov2312(sd);
+
+ fi->interval.numerator = 1;
+ fi->interval.denominator = ov2312->fps / 2;
+
+ return 0;
+}
+
+static int ov2312_set_frame_interval(struct v4l2_subdev *sd,
+ struct v4l2_subdev_state *state,
+ struct v4l2_subdev_frame_interval *fi)
+{
+ struct ov2312 *ov2312 = to_ov2312(sd);
+
+ if (!fi->interval.numerator)
+ return -EINVAL;
+
+ dev_dbg(ov2312->dev, "%s: Set framerate %dfps\n", __func__,
+ fi->interval.denominator / fi->interval.numerator);
+
+ if ((fi->interval.denominator / fi->interval.numerator) != ov2312->fps / 2) {
+ dev_err(ov2312->dev, "%s: Framerate can only be %dfps\n",
+ __func__, ov2312->fps / 2);
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int ov2312_detect(struct ov2312 *ov2312)
+{
+ int ret;
+ u32 id;
+
+ ret = ov2312_read(ov2312, OV2312_SC_CHIP_ID_HI, &id, 2);
+ if (ret < 0)
+ return ret;
+
+ id = cpu_to_be16(id);
+
+ if (id != OV2312_CHIP_ID) {
+ dev_err(ov2312->dev,
+ "%s: unknown chip ID 0x%04x\n", __func__, id);
+ return -ENODEV;
+ }
+
+ dev_dbg(ov2312->dev, "%s: detected chip ID 0x%04x\n", __func__, id);
+ return 0;
+}
+
+/*
+ */
+static int ov2312_set_group_a(struct ov2312 *ov2312)
+{
+ u32 ir_exposure = ov2312->exposure_multi->p_new.p_u32[1];
+ u32 ir_again = ov2312->again_multi->p_new.p_u32[1];
+ u32 ir_dgain = ov2312->dgain_multi->p_new.p_u32[1];
+ u32 ir_strobe_start = OV2312_VTS - ir_exposure - 7;
+ int ret;
+
+ struct reg_sequence ov2312_groupA[] = {
+ {0x3208, 0x00},/* Group A (IR Dominant VC0) */
+ {OV2312_AEC_PK_EXPO_HI, (ir_exposure >> 8) & 0xff},
+ {OV2312_AEC_PK_EXPO_LO, ir_exposure & 0xff},
+ {OV2312_AEC_PK_AGAIN_HI, (ir_again >> 4) & 0xff},
+ {OV2312_AEC_PK_AGAIN_LO, (ir_again & 0x0f) << 4},
+ {OV2312_AEC_PK_DGAIN_HI, (ir_dgain >> 8) & 0xff},
+ {OV2312_AEC_PK_DGAIN_LO, ir_dgain & 0xff},
+ {0x3920, 0xff},/* IR Strobe duty cycle */
+ {0x3927, (ir_exposure >> 8) & 0xff},
+ {0x3928, ir_exposure & 0xff},
+ {0x3929, (ir_strobe_start >> 8) & 0xff},
+ {0x392a, ir_strobe_start & 0xff},
+ {0x4813, 0x01},/* VC=1. This register takes effect from next frame */
+ {0x3208, 0x10},
+ {0x320D, 0x00},/* Auto mode switch between group0 and group1 ;setting to switch */
+ {0x320D, 0x31},
+ {0x3208, 0xA0},
+ };
+
+ ret = regmap_register_patch(ov2312->regmap, ov2312_groupA,
+ ARRAY_SIZE(ov2312_groupA));
+ if (ret < 0)
+ dev_err(ov2312->dev,
+ "%s: failed to apply Group A register patch (%d)!\n",
+ __func__, ret);
+ return ret;
+}
+
+static int ov2312_set_group_b(struct ov2312 *ov2312)
+{
+ u32 rgb_exposure = ov2312->exposure_multi->p_new.p_u32[0];
+ u32 rgb_again = ov2312->again_multi->p_new.p_u32[0];
+ u32 rgb_dgain = ov2312->dgain_multi->p_new.p_u32[0];
+ int ret;
+
+ struct reg_sequence ov2312_groupB[] = {
+ {0x3208, 0x01},/* Group B (RGB Dominant VC1) */
+ {OV2312_AEC_PK_EXPO_HI, (rgb_exposure >> 8) & 0xff},
+ {OV2312_AEC_PK_EXPO_LO, rgb_exposure & 0xff},
+ {OV2312_AEC_PK_AGAIN_HI, (rgb_again >> 4) & 0xff},
+ {OV2312_AEC_PK_AGAIN_LO, (rgb_again & 0x0f) << 4},
+ {OV2312_AEC_PK_DGAIN_HI, (rgb_dgain >> 8) & 0xff},
+ {OV2312_AEC_PK_DGAIN_LO, rgb_dgain & 0xff},
+ {0x3920, 0x00},
+ {0x4813, 0x00},/* VC=0. This register takes effect from next frame */
+ {0x3208, 0x11},
+ {0x320D, 0x00},/* Auto mode switch between group0 and group1 ;setting to switch */
+ {0x320D, 0x30},
+ {0x3208, 0xA0},
+ };
+
+ ret = regmap_register_patch(ov2312->regmap, ov2312_groupB,
+ ARRAY_SIZE(ov2312_groupB));
+ if (ret < 0)
+ dev_err(ov2312->dev,
+ "%s: failed to apply Group B register patch (%d)!\n",
+ __func__, ret);
+ return ret;
+}
+
+static int ov2312_set_AB_mode(struct ov2312 *ov2312)
+{
+ bool ir_ready = ov2312->exposure_multi->p_new.p_u32[1] &&
+ ov2312->again_multi->p_new.p_u32[1] &&
+ ov2312->dgain_multi->p_new.p_u32[1];
+ bool rgb_ready = ov2312->exposure_multi->p_new.p_u32[0] &&
+ ov2312->again_multi->p_new.p_u32[0] &&
+ ov2312->dgain_multi->p_new.p_u32[0];
+ int ret;
+
+ if (ir_ready) {
+ ret = ov2312_set_group_a(ov2312);
+ if (ret < 0)
+ return ret;
+ }
+
+ if (rgb_ready) {
+ ret = ov2312_set_group_b(ov2312);
+ if (ret < 0)
+ return ret;
+ }
+
+ /* Wait for 1 frame duration after setting AB mode registers */
+ if (ir_ready || rgb_ready)
+ msleep(33);
+
+ return 0;
+}
+
+static int ov2312_set_orientation(struct ov2312 *ov2312)
+{
+ bool v_flip = ov2312->v_flip->val;
+ bool h_flip = ov2312->h_flip->val;
+ u32 reg = (v_flip ? 0x4400 : 0) | (h_flip ? 0x0004 : 0);
+
+ return ov2312_write(ov2312, OV2312_TIMING_VFLIP, be16_to_cpu(reg), 2);
+}
+
+static int ov2312_set_ctrl(struct v4l2_ctrl *ctrl)
+{
+ struct ov2312 *ov2312 = container_of(ctrl->handler,
+ struct ov2312, ctrls);
+ int ret;
+
+ /*
+ * If the device is not powered up by the host driver do
+ * not apply any controls to H/W at this time. Instead
+ * the controls will be restored right after power-up.
+ */
+ if (pm_runtime_suspended(ov2312->dev))
+ return 0;
+
+ switch (ctrl->id) {
+ case V4L2_CID_EXPOSURE_MULTI:
+ case V4L2_CID_AGAIN_MULTI:
+ case V4L2_CID_DGAIN_MULTI:
+ dev_dbg(ov2312->dev, "debug: %s: %s = [%u, %u]\n", __func__,
+ ctrl->name, ctrl->p_new.p_u32[0], ctrl->p_new.p_u32[1]);
+
+ ret = ov2312_set_AB_mode(ov2312);
+ break;
+
+ case V4L2_CID_HFLIP:
+ case V4L2_CID_VFLIP:
+ ret = ov2312_set_orientation(ov2312);
+ break;
+
+ default:
+ ret = -EINVAL;
+ }
+
+ return ret;
+}
+
+static int ov2312_power_on(struct ov2312 *ov2312)
+{
+ int ret;
+
+ ret = clk_prepare_enable(ov2312->clk);
+ if (ret < 0)
+ return ret;
+
+ if (ov2312->reset_gpio) {
+ gpiod_set_value_cansleep(ov2312->reset_gpio, 0);
+ usleep_range(100, 1000);
+ gpiod_set_value_cansleep(ov2312->reset_gpio, 1);
+ msleep(30);
+ }
+ return 0;
+}
+
+static int ov2312_power_off(struct ov2312 *ov2312)
+{
+ if (ov2312->reset_gpio) {
+ gpiod_set_value_cansleep(ov2312->reset_gpio, 0);
+ usleep_range(1, 10);
+ }
+
+ clk_disable_unprepare(ov2312->clk);
+
+ return 0;
+}
+
+static int ov2312_resume(struct device *dev)
+{
+ struct i2c_client *client = to_i2c_client(dev);
+ struct v4l2_subdev *sd = i2c_get_clientdata(client);
+ struct ov2312 *ov2312 = to_ov2312(sd);
+
+ return ov2312_power_on(ov2312);
+}
+
+static int ov2312_suspend(struct device *dev)
+{
+ struct i2c_client *client = to_i2c_client(dev);
+ struct v4l2_subdev *sd = i2c_get_clientdata(client);
+ struct ov2312 *ov2312 = to_ov2312(sd);
+
+ return ov2312_power_off(ov2312);
+}
+
+static int ov2312_start_stream(struct ov2312 *ov2312)
+{
+ int ret;
+
+ ret = ov2312_write_table(ov2312, ov2312_1600x1300_60fps_AB,
+ ARRAY_SIZE(ov2312_1600x1300_60fps_AB));
+ if (ret < 0)
+ return ret;
+
+ /* Update controls on wake up */
+ ret = ov2312_set_orientation(ov2312);
+ if (ret < 0)
+ return ret;
+
+ ret = ov2312_set_AB_mode(ov2312);
+ if (ret < 0)
+ return ret;
+
+ msleep(100);
+
+ /* Set active */
+ ret = ov2312_write(ov2312, OV2312_SYS_MODE_SEL, 1, 1);
+ if (ret < 0)
+ return ret;
+
+ /* No communication is possible for a while after exiting standby */
+ msleep(20);
+ return 0;
+}
+
+static int ov2312_stop_stream(struct ov2312 *ov2312)
+{
+ int ret;
+
+ /* Set standby */
+ ret = ov2312_write(ov2312, OV2312_SYS_MODE_SEL, 0, 1);
+ if (ret < 0)
+ return ret;
+
+ /* No communication is possible for a while after entering standby */
+ usleep_range(10000, 20000);
+ return 0;
+}
+
+static int ov2312_sd_enable_streams(struct v4l2_subdev *sd,
+ struct v4l2_subdev_state *state,
+ u32 pad, u64 streams_mask)
+{
+ struct ov2312 *ov2312 = to_ov2312(sd);
+ int ret;
+
+ guard(mutex)(&ov2312->lock);
+
+ if (!ov2312->enable_count) {
+ ret = pm_runtime_resume_and_get(ov2312->dev);
+ if (ret < 0)
+ goto err;
+
+ ret = ov2312_start_stream(ov2312);
+ if (ret < 0) {
+ pm_runtime_put(ov2312->dev);
+ goto err;
+ }
+ }
+
+ ov2312->enable_count++;
+
+ return 0;
+
+err:
+ dev_err(ov2312->dev,
+ "%s: failed to enable streams %d\n", __func__, ret);
+ return ret;
+}
+
+static int ov2312_sd_disable_streams(struct v4l2_subdev *sd,
+ struct v4l2_subdev_state *state,
+ u32 pad, u64 streams_mask)
+{
+ struct ov2312 *ov2312 = to_ov2312(sd);
+ int ret;
+
+ mutex_lock(&ov2312->lock);
+
+ if (ov2312->enable_count == 1) {
+ ret = ov2312_stop_stream(ov2312);
+ if (ret < 0)
+ goto err_runtime_put;
+ }
+
+ ov2312->enable_count--;
+ mutex_unlock(&ov2312->lock);
+
+ if (!ov2312->enable_count)
+ pm_runtime_put(ov2312->dev);
+
+ return 0;
+
+err_runtime_put:
+ mutex_unlock(&ov2312->lock);
+ pm_runtime_put(ov2312->dev);
+ return ret;
+}
+
+static const struct v4l2_subdev_pad_ops ov2312_subdev_pad_ops = {
+ .get_fmt = v4l2_subdev_get_fmt,
+ .set_fmt = ov2312_set_fmt,
+ .enum_mbus_code = ov2312_enum_mbus_code,
+ .enum_frame_size = ov2312_enum_frame_sizes,
+ .set_routing = ov2312_set_routing,
+ .get_frame_desc = ov2312_get_frame_desc,
+ .get_frame_interval = ov2312_get_frame_interval,
+ .set_frame_interval = ov2312_set_frame_interval,
+ .enable_streams = ov2312_sd_enable_streams,
+ .disable_streams = ov2312_sd_disable_streams,
+};
+
+static const struct v4l2_subdev_internal_ops ov2312_internal_ops = {
+ .init_state = ov2312_init_state,
+};
+
+static const struct v4l2_subdev_ops ov2312_subdev_ops = {
+ .pad = &ov2312_subdev_pad_ops,
+};
+
+static const struct v4l2_ctrl_ops ov2312_ctrl_ops = {
+ .s_ctrl = ov2312_set_ctrl,
+};
+
+static const struct dev_pm_ops ov2312_pm_ops = {
+ SET_RUNTIME_PM_OPS(ov2312_suspend, ov2312_resume, NULL)
+};
+
+static int ov2312_probe(struct i2c_client *client)
+{
+ struct ov2312 *ov2312;
+ struct v4l2_subdev *sd;
+ struct v4l2_ctrl_handler *ctrl_hdr;
+ int ret;
+
+ /* Allocate internal struct */
+ ov2312 = devm_kzalloc(&client->dev, sizeof(*ov2312), GFP_KERNEL);
+ if (!ov2312)
+ return -ENOMEM;
+
+ ov2312->dev = &client->dev;
+ ov2312->client = client;
+
+ /* Initialize I2C Regmap */
+ ov2312->regmap = devm_regmap_init_i2c(client, &ov2312_regmap_config);
+ if (IS_ERR(ov2312->regmap))
+ return PTR_ERR(ov2312->regmap);
+
+ /* Initialize Shutdown GPIO */
+ ov2312->reset_gpio = devm_gpiod_get_optional(ov2312->dev, "reset",
+ GPIOD_OUT_HIGH);
+ if (IS_ERR(ov2312->reset_gpio))
+ return PTR_ERR(ov2312->reset_gpio);
+
+ ov2312->clk = devm_clk_get(ov2312->dev, "xvclk");
+ if (IS_ERR(ov2312->clk))
+ return PTR_ERR(ov2312->clk);
+
+ ov2312->clk_rate = clk_get_rate(ov2312->clk);
+ dev_info(ov2312->dev, "xvclk rate: %lu Hz\n", ov2312->clk_rate);
+
+ if (ov2312->clk_rate < 6000000 || ov2312->clk_rate > 27000000)
+ return -EINVAL;
+
+ /* Power on */
+ ret = ov2312_power_on(ov2312);
+ if (ret < 0)
+ return ret;
+
+ /* Detect sensor */
+ ret = ov2312_detect(ov2312);
+ if (ret < 0)
+ goto err_power_off;
+
+ /* Initialize the subdev and its controls. */
+ sd = &ov2312->sd;
+ v4l2_i2c_subdev_init(sd, client, &ov2312_subdev_ops);
+ sd->internal_ops = &ov2312_internal_ops;
+ sd->flags |= V4L2_SUBDEV_FL_HAS_DEVNODE |
+ V4L2_SUBDEV_FL_HAS_EVENTS | V4L2_SUBDEV_FL_STREAMS;
+
+ /* Initialize the media entity. */
+ ov2312->pad.flags = MEDIA_PAD_FL_SOURCE;
+ sd->entity.function = MEDIA_ENT_F_CAM_SENSOR;
+ ret = media_entity_pads_init(&sd->entity, 1, &ov2312->pad);
+ if (ret < 0) {
+ dev_err_probe(ov2312->dev, ret, "media entity init failed\n");
+ goto err_power_off;
+ }
+
+ ov2312->fps = OV2312_FRAMERATE_DEFAULT;
+ ret = devm_mutex_init(ov2312->dev, &ov2312->lock);
+ if (ret) {
+ dev_err_probe(ov2312->dev, ret, "mutex init failed\n");
+ goto err_media_cleanup;
+ }
+
+ /* Initialize controls */
+ ctrl_hdr = &ov2312->ctrls;
+ /* 3 multi-capture + 2 flip + 3 single backward-compat */
+ ret = v4l2_ctrl_handler_init(ctrl_hdr, 8);
+ if (ret < 0) {
+ ret = dev_err_probe(ov2312->dev, ret,
+ "ctrl handler init failed\n");
+ goto err_media_cleanup;
+ }
+
+ ov2312->ctrls.lock = &ov2312->lock;
+
+ /* Multi-capture controls: index 0 = RGB (longest), index 1 = IR */
+ {
+ const struct v4l2_ctrl_config exposure_multi_cfg = {
+ .ops = &ov2312_ctrl_ops,
+ .id = V4L2_CID_EXPOSURE_MULTI,
+ .type = V4L2_CTRL_TYPE_U32,
+ .min = 0,
+ .max = OV2312_EXPOSURE_MAX,
+ .step = 1,
+ .def = OV2312_EXPOSURE_DEFAULT,
+ .dims = { 2 },
+ };
+ const struct v4l2_ctrl_config again_multi_cfg = {
+ .ops = &ov2312_ctrl_ops,
+ .id = V4L2_CID_AGAIN_MULTI,
+ .type = V4L2_CTRL_TYPE_U32,
+ .min = 0,
+ .max = OV2312_AGAIN_MAX,
+ .step = 1,
+ .def = OV2312_AGAIN_DEFAULT,
+ .dims = { 2 },
+ };
+ const struct v4l2_ctrl_config dgain_multi_cfg = {
+ .ops = &ov2312_ctrl_ops,
+ .id = V4L2_CID_DGAIN_MULTI,
+ .type = V4L2_CTRL_TYPE_U32,
+ .min = 0,
+ .max = OV2312_DGAIN_MAX,
+ .step = 1,
+ .def = OV2312_DGAIN_DEFAULT,
+ .dims = { 2 },
+ };
+
+ ov2312->exposure_multi = v4l2_ctrl_new_custom(ctrl_hdr,
+ &exposure_multi_cfg,
+ NULL);
+ ov2312->again_multi = v4l2_ctrl_new_custom(ctrl_hdr,
+ &again_multi_cfg,
+ NULL);
+ ov2312->dgain_multi = v4l2_ctrl_new_custom(ctrl_hdr,
+ &dgain_multi_cfg,
+ NULL);
+ }
+
+ if (ov2312->exposure_multi && ov2312->again_multi && ov2312->dgain_multi) {
+ /* Set per-element initial values: [0]=RGB default, [1]=IR default */
+ ov2312->exposure_multi->p_cur.p_u32[0] = OV2312_EXPOSURE_DEFAULT;
+ ov2312->exposure_multi->p_cur.p_u32[1] = OV2312_IR_EXPOSURE;
+ ov2312->exposure_multi->p_new.p_u32[0] = OV2312_EXPOSURE_DEFAULT;
+ ov2312->exposure_multi->p_new.p_u32[1] = OV2312_IR_EXPOSURE;
+
+ ov2312->again_multi->p_cur.p_u32[0] = OV2312_AGAIN_DEFAULT;
+ ov2312->again_multi->p_cur.p_u32[1] = OV2312_AGAIN_DEFAULT;
+ ov2312->again_multi->p_new.p_u32[0] = OV2312_AGAIN_DEFAULT;
+ ov2312->again_multi->p_new.p_u32[1] = OV2312_AGAIN_DEFAULT;
+
+ ov2312->dgain_multi->p_cur.p_u32[0] = OV2312_DGAIN_DEFAULT;
+ ov2312->dgain_multi->p_cur.p_u32[1] = OV2312_DGAIN_DEFAULT;
+ ov2312->dgain_multi->p_new.p_u32[0] = OV2312_DGAIN_DEFAULT;
+ ov2312->dgain_multi->p_new.p_u32[1] = OV2312_DGAIN_DEFAULT;
+ }
+
+ ov2312->h_flip = v4l2_ctrl_new_std(ctrl_hdr, &ov2312_ctrl_ops,
+ V4L2_CID_HFLIP, 0, 1, 1, 0);
+
+ ov2312->v_flip = v4l2_ctrl_new_std(ctrl_hdr, &ov2312_ctrl_ops,
+ V4L2_CID_VFLIP, 0, 1, 1, 0);
+
+ /*
+ * Register single-capture controls with proper ranges for backward
+ * compatibility with older userspace. The v4l2 core redirects s_ctrl
+ * for these to the corresponding multi-capture control.
+ */
+ v4l2_ctrl_new_std(ctrl_hdr, &ov2312_ctrl_ops,
+ V4L2_CID_EXPOSURE, 1, OV2312_EXPOSURE_MAX,
+ 1, OV2312_EXPOSURE_DEFAULT);
+ v4l2_ctrl_new_std(ctrl_hdr, &ov2312_ctrl_ops,
+ V4L2_CID_ANALOGUE_GAIN, 0, OV2312_AGAIN_MAX,
+ 1, OV2312_AGAIN_DEFAULT);
+ v4l2_ctrl_new_std(ctrl_hdr, &ov2312_ctrl_ops,
+ V4L2_CID_DIGITAL_GAIN, 0, OV2312_DGAIN_MAX,
+ 1, OV2312_DGAIN_DEFAULT);
+
+ ov2312->sd.ctrl_handler = ctrl_hdr;
+ if (ov2312->ctrls.error) {
+ ret = ov2312->ctrls.error;
+ ret = dev_err_probe(ov2312->dev, ret,
+ "failed to add the ctrls\n");
+ goto err_ctrl_free;
+ }
+
+ /* PM Runtime */
+ pm_runtime_enable(ov2312->dev);
+ pm_runtime_set_suspended(ov2312->dev);
+
+ ret = v4l2_subdev_init_finalize(sd);
+ if (ret < 0)
+ goto err_pm_disable;
+
+ /* Finally, register the subdev. */
+ ret = v4l2_async_register_subdev(sd);
+ if (ret < 0) {
+ ret = dev_err_probe(ov2312->dev, ret,
+ "v4l2 subdev register failed\n");
+ goto err_subdev_cleanup;
+ }
+
+ dev_info(ov2312->dev, "ov2312 probed\n");
+ return 0;
+
+err_subdev_cleanup:
+ v4l2_subdev_cleanup(&ov2312->sd);
+
+err_pm_disable:
+ pm_runtime_disable(ov2312->dev);
+
+err_ctrl_free:
+ v4l2_ctrl_handler_free(ctrl_hdr);
+
+err_media_cleanup:
+ media_entity_cleanup(&ov2312->sd.entity);
+
+err_power_off:
+ ov2312_power_off(ov2312);
+
+ return ret;
+}
+
+static void ov2312_remove(struct i2c_client *client)
+{
+ struct v4l2_subdev *sd = i2c_get_clientdata(client);
+ struct ov2312 *ov2312 = to_ov2312(sd);
+
+ v4l2_async_unregister_subdev(sd);
+ v4l2_ctrl_handler_free(&ov2312->ctrls);
+ v4l2_subdev_cleanup(&ov2312->sd);
+ media_entity_cleanup(&sd->entity);
+
+ pm_runtime_disable(ov2312->dev);
+}
+
+static const struct i2c_device_id ov2312_id[] = {
+ { "ov2312", 0 },
+ { /* sentinel */ },
+};
+MODULE_DEVICE_TABLE(i2c, ov2312_id);
+
+#if IS_ENABLED(CONFIG_OF)
+static const struct of_device_id ov2312_of_match[] = {
+ { .compatible = "ovti,ov2312", },
+ { /* sentinel */ },
+};
+MODULE_DEVICE_TABLE(of, ov2312_of_match);
+#endif
+
+static struct i2c_driver ov2312_i2c_driver = {
+ .driver = {
+ .name = "ov2312",
+ .pm = &ov2312_pm_ops,
+ .of_match_table = of_match_ptr(ov2312_of_match),
+ },
+ .probe = ov2312_probe,
+ .remove = ov2312_remove,
+ .id_table = ov2312_id,
+};
+
+module_i2c_driver(ov2312_i2c_driver);
+
+MODULE_AUTHOR("Jai Luthra <j-luthra@xxxxxx>");
+MODULE_DESCRIPTION("OV2312 RGB-IR Image Sensor driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/media/i2c/ov2312.h b/drivers/media/i2c/ov2312.h
new file mode 100644
index 000000000000..8ed435878638
--- /dev/null
+++ b/drivers/media/i2c/ov2312.h
@@ -0,0 +1,285 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Omnivision OV2312 RGB-IR Image Sensor driver
+ *
+ * Copyright (c) 2022 Jai Luthra <j-luthra@xxxxxx>
+ */
+
+#include <linux/types.h>
+#include <linux/media-bus-format.h>
+
+#define OV2312_CHIP_ID 0x2311
+#define OV2312_FRAMERATE_DEFAULT 60
+
+#define OV2312_ED_LINES 1
+#define OV2312_OUT_WIDTH 1600
+#define OV2312_OUT_HEIGHT (1300 + OV2312_ED_LINES)
+#define OV2312_VTS 0x0588
+
+#define OV2312_SYS_MODE_SEL 0x0100
+#define OV2312_SC_CHIP_ID_HI 0x300a
+#define OV2312_SC_CHIP_ID_LO 0x300b
+#define OV2312_AEC_PK_EXPO_HI 0x3501
+#define OV2312_AEC_PK_EXPO_LO 0x3502
+#define OV2312_AEC_PK_AGAIN_HI 0x3508
+#define OV2312_AEC_PK_AGAIN_LO 0x3509
+#define OV2312_AEC_PK_DGAIN_HI 0x350a
+#define OV2312_AEC_PK_DGAIN_LO 0x350b
+#define OV2312_TIMING_VFLIP 0x3820
+#define OV2312_TIMING_HFLIP 0x3821
+
+/* Exposure control */
+#define OV2312_EXPOSURE_MAX (OV2312_VTS - 12)
+#define OV2312_EXPOSURE_DEFAULT 0x057c
+#define OV2312_IR_EXPOSURE 0x0090
+#define OV2312_IR_STROBE OV2312_IR_EXPOSURE
+#define OV2312_IR_STROBE_START (OV2312_VTS - OV2312_IR_EXPOSURE - 7)
+
+/* Analog gain control */
+#define OV2312_AGAIN_MAX 0x1FF
+#define OV2312_AGAIN_DEFAULT 0x010
+
+/* Digital gain control */
+#define OV2312_DGAIN_MAX 0x0FFF
+#define OV2312_DGAIN_DEFAULT 0x0100
+
+static const struct v4l2_area ov2312_framesizes[] = {
+ {
+ .width = OV2312_OUT_WIDTH,
+ .height = OV2312_OUT_HEIGHT,
+ },
+};
+
+static const u32 ov2312_mbus_formats[] = {
+ MEDIA_BUS_FMT_SBGGI10_1X10,
+};
+
+static const struct regmap_config ov2312_regmap_config = {
+ .reg_bits = 16,
+ .val_bits = 8,
+};
+
+static const struct reg_sequence ov2312_1600x1300_60fps_AB[] = {
+ {0x0103, 0x01},
+ {0x0100, 0x00},
+ {0x010c, 0x02},
+ {0x010b, 0x01},
+ {0x0300, 0x01},
+ {0x0302, 0x32},
+ {0x0303, 0x00},
+ {0x0304, 0x03},
+ {0x0305, 0x02},
+ {0x0306, 0x01},
+ {0x030d, 0x5a},
+ {0x030e, 0x04},
+ {0x3001, 0x02},
+ {0x3004, 0x00},
+ {0x3005, 0x00},
+ {0x3006, 0x0a},
+ {0x3011, 0x0d},
+ {0x3014, 0x04},
+ {0x301c, 0xf0},
+ {0x3020, 0x20},
+ {0x302c, 0x00},
+ {0x302d, 0x00},
+ {0x302e, 0x00},
+ {0x302f, 0x03},
+ {0x3030, 0x10},
+ {0x303f, 0x03},
+ {0x3103, 0x00},
+ {0x3106, 0x08},
+ {0x31ff, 0x01},
+ {0x3501, 0x05},
+ {0x3502, 0x7c},
+ {0x3506, 0x00},
+ {0x3507, 0x00},
+ {0x3620, 0x67},
+ {0x3633, 0x78},
+ {0x3662, 0x65},
+ {0x3664, 0xb0},
+ {0x3666, 0x70},
+ {0x3670, 0x68},
+ {0x3674, 0x10},
+ {0x3675, 0x00},
+ {0x367e, 0x90},
+ {0x3680, 0x84},
+ {0x36a2, 0x04},
+ {0x36a3, 0x80},
+ {0x36b0, 0x00},
+ {0x3700, 0x35},
+ {0x3704, 0x39},
+ {0x370a, 0x50},
+ {0x3712, 0x00},
+ {0x3713, 0x02},
+ {0x3778, 0x00},
+ {0x379b, 0x01},
+ {0x379c, 0x10},
+ {0x3800, 0x00},
+ {0x3801, 0x00},
+ {0x3802, 0x00},
+ {0x3803, 0x00},
+ {0x3804, 0x06},
+ {0x3805, 0x4f},
+ {0x3806, 0x05},
+ {0x3807, 0x23},
+ {0x3808, 0x06},
+ {0x3809, 0x40},
+ {0x380a, 0x05},
+ {0x380b, 0x14},
+ {0x380c, 0x03},
+ {0x380d, 0xa8},
+ {0x380e, (OV2312_VTS >> 8) & 0xff},
+ {0x380f, OV2312_VTS & 0xff},
+ {0x3810, 0x00},
+ {0x3811, 0x08},
+ {0x3812, 0x00},
+ {0x3813, 0x08},
+ {0x3814, 0x11},
+ {0x3815, 0x11},
+ {0x3816, 0x00},
+ {0x3817, 0x01},
+ {0x3818, 0x00},
+ {0x3819, 0x05},
+ {0x382b, 0x5a},
+ {0x382c, 0x0a},
+ {0x382d, 0xf8},
+ {0x3881, 0x44},
+ {0x3882, 0x02},
+ {0x3883, 0x8c},
+ {0x3885, 0x07},
+ {0x389d, 0x03},
+ {0x38a6, 0x00},
+ {0x38a7, 0x01},
+ {0x38b3, 0x07},
+ {0x38b1, 0x00},
+ {0x38e5, 0x02},
+ {0x38e7, 0x00},
+ {0x38e8, 0x00},
+ {0x3910, 0xff},
+ {0x3911, 0xff},
+ {0x3912, 0x08},
+ {0x3913, 0x00},
+ {0x3914, 0x00},
+ {0x3915, 0x00},
+ {0x391c, 0x00},
+ {0x3920, 0xff},
+ {0x3921, 0x80},
+ {0x3922, 0x00},
+ {0x3923, 0x00},
+ {0x3924, 0x05},
+ {0x3925, 0x00},
+ {0x3926, 0x00},
+ {0x3927, 0x00},
+ {0x3928, 0x1a},
+ {0x392d, 0x03},
+ {0x392e, 0xa8},
+ {0x392f, 0x08},
+ {0x4001, 0x00},
+ {0x4003, 0x40},
+ {0x4008, 0x04},
+ {0x4009, 0x1b},
+ {0x400c, 0x04},
+ {0x400d, 0x1b},
+ {0x4010, 0xf4},
+ {0x4011, 0x00},
+ {0x4016, 0x00},
+ {0x4017, 0x04},
+ {0x4042, 0x11},
+ {0x4043, 0x70},
+ {0x4045, 0x00},
+ {0x4409, 0x5f},
+ {0x4509, 0x00},
+ {0x450b, 0x00},
+ {0x4600, 0x00},
+ {0x4601, 0x80},
+ {0x4708, 0x09},
+ {0x470c, 0x81},
+ {0x4710, 0x06},
+ {0x4711, 0x00},
+ {0x4800, 0x00},
+ {0x481f, 0x30},
+ {0x4837, 0x14},
+ {0x4f00, 0x00},
+ {0x4f07, 0x00},
+ {0x4f08, 0x03},
+ {0x4f09, 0x08},
+ {0x4f0c, 0x05},
+ {0x4f0d, 0xb4},
+ {0x4f10, 0x00},
+ {0x4f11, 0x00},
+ {0x4f12, 0x07},
+ {0x4f13, 0xe2},
+ {0x5000, 0x9f},
+ {0x5001, 0x20},
+ {0x5026, 0x00},
+ {0x5c00, 0x00},
+ {0x5c01, 0x2c},
+ {0x5c02, 0x00},
+ {0x5c03, 0x7f},
+ {0x5e00, 0x00},
+ {0x5e01, 0x41},
+ {0x38b1, 0x02},
+ {0x0100, 0x01},
+ {0x3006, 0x08},/* Strobe control */
+ {0x3004, 0x02},
+ {0x3007, 0x02},
+ {0x301c, 0x20},
+ {0x3020, 0x20},
+ {0x3025, 0x02},
+ {0x382c, 0x0a},
+ {0x382d, 0xf8},
+ {0x3920, 0xff},
+ {0x3921, 0x00},
+ {0x3923, 0x00},
+ {0x3924, 0x00},
+ {0x3925, 0x00},
+ {0x3926, 0x00},
+ {0x3927, 0x00},
+ {0x3928, 0x80},
+ {0x392b, 0x00},
+ {0x392c, 0x00},
+ {0x392d, 0x03},
+ {0x392e, 0xa8},
+ {0x392f, 0x0b},
+ {0x38b3, 0x07},
+ {0x3885, 0x07},
+ {0x382b, 0x3a},
+ {0x3670, 0x68},
+ {0x3016, 0xF1},
+ {0x0100, 0x01},
+ {0x4814, 0x6B},
+ {0x3218, 0x32},
+ {0x3216, 0x01},
+ {0x3208, 0x04},
+ {0x4813, 0x01},
+ {0x321a, 0x01},
+ {0x3920, 0x01},
+ {0x3501, 0x02},
+ {0x3508, 0x02},
+ {0x350e, 0x02},
+ {0x3208, 0x14},
+ {0x3662, 0x65},
+ {0x366F, 0x1A},
+ {0x3674, 0x11},
+ {0x3016, 0xF0},
+ {0x301C, 0xF0},/* AB mode - Group auto switch example setting */
+ {0x3209, 0x01},/* Stay in Group A for 1 Frame */
+ {0x320A, 0x01},/* Stay in Group B for 1 Frame */
+ {0x320B, 0x00},
+ {0x320C, 0x00},
+ {0x3208, 0x00},/* Group A (IR Dominant VC0) default values */
+ {OV2312_AEC_PK_EXPO_HI, (OV2312_IR_EXPOSURE >> 8) & 0xff},
+ {OV2312_AEC_PK_EXPO_LO, OV2312_IR_EXPOSURE & 0xff},
+ {OV2312_AEC_PK_AGAIN_HI, 0x01},
+ {OV2312_AEC_PK_AGAIN_LO, 0x00},
+ {OV2312_AEC_PK_DGAIN_HI, 0x01},
+ {OV2312_AEC_PK_DGAIN_LO, 0x00},
+ {0x3920, 0xff},/* IR Strobe duty cycle */
+ {0x3927, (OV2312_IR_STROBE >> 8) & 0xff},
+ {0x3928, OV2312_IR_STROBE & 0xff},
+ {0x3929, (OV2312_IR_STROBE_START >> 8) & 0xff},
+ {0x392a, OV2312_IR_STROBE_START & 0xff},
+ {0x4813, 0x01},/* VC=1. This register takes effect from next frame */
+ {0x3208, 0x10},
+
+};
--
2.34.1