mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Lachlan Michael <lachlan.michael@sony.com>
To: mchehab@kernel.org, sakari.ailus@linux.intel.com,
	hverkuil+cisco@kernel.org, laurent.pinchart@ideasonboard.com,
	linux-media@vger.kernel.org, devicetree@vger.kernel.org
Cc: robh@kernel.org, krzk+dt@kernel.org, conor+dt@kernel.org,
	kieran.bingham@ideasonboard.com, jai.luthra@ideasonboard.com,
	Ryuichi.Tadano@sony.com, Kengo.Hayasaka@sony.com,
	Tim.Bird@sony.com, Kazumi.A.Sato@sony.com,
	linux-kernel@vger.kernel.org,
	Lachlan Michael <lachlan.michael@sony.com>
Subject: [PATCH 2/2] media: i2c: Add Sony IMX908 image sensor driver
Date: Thu, 30 Jul 2026 11:15:25 +0900	[thread overview]
Message-ID: <20260730021525.166811-3-lachlan.michael@sony.com> (raw)
In-Reply-To: <20260730021525.166811-1-lachlan.michael@sony.com>

Add a V4L2 sub-device driver for the Sony IMX908 CMOS image sensor.

The driver supports:
- RAW10 and RAW12 output formats
- Exposure and analogue gain controls
- Horizontal and vertical image flipping
- HBLANK and VBLANK controls
- Full-resolution and window-cropping modes
- Test pattern generation
 
HDR modes and RAW16 output are not currently supported.

The driver has been tested with libcamera on Raspberry Pi 5 based
hardware.

Signed-off-by: Lachlan Michael <lachlan.michael@sony.com>
---
 drivers/media/i2c/Kconfig  |   11 +
 drivers/media/i2c/Makefile |    1 +
 drivers/media/i2c/imx908.c | 1593 ++++++++++++++++++++++++++++++++++++
 3 files changed, 1605 insertions(+)
 create mode 100644 drivers/media/i2c/imx908.c

diff --git a/drivers/media/i2c/Kconfig b/drivers/media/i2c/Kconfig
index 5d173e0ecf42..8e521f21414b 100644
--- a/drivers/media/i2c/Kconfig
+++ b/drivers/media/i2c/Kconfig
@@ -287,6 +287,17 @@ config VIDEO_IMX415
 	  To compile this driver as a module, choose M here: the
 	  module will be called imx415.
 
+config VIDEO_IMX908
+	tristate "Sony IMX908 sensor support"
+	depends on GPIOLIB
+	select V4L2_CCI_I2C
+	help
+	  This is a Video4Linux2 sensor driver for the Sony
+	  IMX908 CMOS image sensor.
+
+	  To compile this driver as a module, choose M here: the
+	  module will called imx908.
+
 config VIDEO_MAX9271_LIB
 	tristate
 
diff --git a/drivers/media/i2c/Makefile b/drivers/media/i2c/Makefile
index e45359efe0e4..3ccf6d29bbfd 100644
--- a/drivers/media/i2c/Makefile
+++ b/drivers/media/i2c/Makefile
@@ -61,6 +61,7 @@ obj-$(CONFIG_VIDEO_IMX335) += imx335.o
 obj-$(CONFIG_VIDEO_IMX355) += imx355.o
 obj-$(CONFIG_VIDEO_IMX412) += imx412.o
 obj-$(CONFIG_VIDEO_IMX415) += imx415.o
+obj-$(CONFIG_VIDEO_IMX908) += imx908.o
 obj-$(CONFIG_VIDEO_IR_I2C) += ir-kbd-i2c.o
 obj-$(CONFIG_VIDEO_ISL7998X) += isl7998x.o
 obj-$(CONFIG_VIDEO_KS0127) += ks0127.o
diff --git a/drivers/media/i2c/imx908.c b/drivers/media/i2c/imx908.c
new file mode 100644
index 000000000000..6ec1322f9c16
--- /dev/null
+++ b/drivers/media/i2c/imx908.c
@@ -0,0 +1,1593 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * V4L2 driver for Sony IMX908
+ *
+ * Diagonal 6.42 mm (Type 1/2.8) CMOS image sensor with 8.39 M pixels.
+ *
+ * Copyright 2026 Sony Semiconductor Solutions Corporation
+ *
+ */
+
+#include <linux/align.h>
+#include <linux/array_size.h>
+#include <linux/bitops.h>
+#include <linux/clk.h>
+#include <linux/delay.h>
+#include <linux/err.h>
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/kernel.h>
+#include <linux/math64.h>
+#include <linux/minmax.h>
+#include <linux/module.h>
+#include <linux/pm_runtime.h>
+#include <linux/property.h>
+#include <linux/regulator/consumer.h>
+
+#include <media/media-entity.h>
+#include <media/v4l2-cci.h>
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-event.h>
+#include <media/v4l2-fwnode.h>
+#include <media/v4l2-mediabus.h>
+#include <media/v4l2-rect.h>
+#include <media/v4l2-subdev.h>
+
+/* ---- IMX908 Registers --------- */
+#define IMX908_REG_STANDBY	CCI_REG8(0x3000)
+#define IMX908_STANDBY_EN	BIT(0)		/* Standby mode */
+#define IMX908_STANDBY_CANCEL	0x00		/* Operating mode */
+#define IMX908_REG_XMSTA	CCI_REG8(0x3002)	/* [0] Init 1h */
+#define IMX908_START_CONTROLLER	0x00	/* Controller mode operation start */
+#define IMX908_STOP_CONTROLLER	0x01	/* Controller mode operation stop */
+#define IMX908_REG_XMASTER	CCI_REG8(0x3003)	/* [0] Init 0h */
+#define IMX908_CONTROLLER_MODE	0x00
+#define IMX908_REG_INCK_SEL	CCI_REG8(0x3014)	/* 0:74.25 1:37.125 2:72 3:27 4:24 */
+#define IMX908_REG_DATARATE_SEL	CCI_REG8(0x3015)
+
+/* ---- Crop */
+#define IMX908_REG_WINMODE	CCI_REG8(0x3018)	/* [3:0] */
+#define IMX908_WINMODE_ALLPIX	0x00	/* All-pixel readout mode */
+#define IMX908_WINMODE_CROP	0x04	/* Window cropping mode */
+
+/* ---- Flip */
+#define IMX908_REG_HREVERSE	CCI_REG8(0x3020)	/* 0:Normal, 1:Inverted */
+#define IMX908_REG_VREVERSE	CCI_REG8(0x3021)	/* 0:Normal, 1:Inverted */
+
+/* ---- Internal Analog to Digital conversion bit width */
+#define IMX908_REG_ADBIT	CCI_REG8(0x3022)
+#define IMX908_ADBIT_10BIT	0x00	/* 10-bit */
+#define IMX908_ADBIT_12BIT	0x01	/* 11-bit + digital dither */
+
+#define IMX908_REG_MDBIT	CCI_REG8(0x3023)
+#define IMX908_MDBIT_RAW10	0x00
+#define IMX908_MDBIT_RAW12	0x01
+
+/* ---- VMAX  Frame length in lines */
+#define IMX908_REG_VMAX		CCI_REG24_LE(0x3028)	/* [19:0] */
+#define IMX908_VMAX_MAX		0xfffff
+#define IMX908_VMAX_DEFAULT	2250U
+/*
+ * Window cropping mode restrictions:
+ * VMAX >= PIX_VWIDTH + 70
+ * VMAX >= 1206
+ */
+#define IMX908_VMAX_CROP_MIN_MARGIN	70
+#define IMX908_VMAX_CROP_MIN_LIMIT	1206
+
+/* ---- HMAX Line length in clocks */
+#define IMX908_REG_HMAX		CCI_REG16_LE(0x302c)	/* [15:0] */
+#define IMX908_HMAX_MAX		0xffff
+#define IMX908_HMAX_DEFAULT	1100U
+
+/* ---- Cropping related */
+#define IMX908_REG_PIX_HST	CCI_REG16_LE(0x303c)	/* [12:0] Init 0h */
+#define IMX908_REG_PIX_HWIDTH	CCI_REG16_LE(0x303e)	/* [12:0] Init 0F10h */
+#define IMX908_REG_LANEMODE	CCI_REG8(0x3040)	/* 1:2-lane, 3:4-lane */
+#define IMX908_REG_PIX_VST	CCI_REG16_LE(0x3044)	/* [12:0] Init 0h */
+#define IMX908_REG_PIX_VWIDTH	CCI_REG16_LE(0x3046)	/* [12:0] Init 0884h */
+#define IMX908_CROP_ALIGN_VSTART	4U
+#define IMX908_CROP_ALIGN_VWIDTH	4U
+#define IMX908_CROP_ALIGN_HSTART	4U
+#define IMX908_CROP_ALIGN_HWIDTH	16U
+#define IMX908_CROP_MIN_HEIGHT		1136U
+#define IMX908_CROP_MIN_WIDTH		1040U
+
+/* ---- Shutter */
+#define IMX908_REG_SHR0		CCI_REG24_LE(0x3050)	/* [19:0] */
+#define IMX908_MIN_SHR0		7U
+
+/*
+ * Analogue gain control
+ * Range is from 0 to 100 (0dB - 30dB) with 0.3dB step size
+ * Values from 101 to 240 are valid but correspond to additional digital gain
+ * (0.3dB - 42dB) so don't expose it to userspace
+ */
+#define IMX908_REG_GAIN		CCI_REG16_LE(0x3070)	/* [10:0] */
+#define IMX908_ANA_GAIN_MAX	100
+#define IMX908_ANA_GAIN_MIN	0
+#define IMX908_ANA_GAIN_STEP	1
+#define IMX908_ANA_GAIN_DEFAULT	0
+
+/* ---- XVS/XHS output mode */
+#define IMX908_XXS_DRV		CCI_REG8(0x30a6)	/* [1:0] XVS_DRV [3:2] XHS_DRV */
+
+#define IMX908_REG_BLKLEVEL	CCI_REG16_LE(0x30dc)	/* [15:0] black level */
+
+/* ---- Test Pattern Generator (TPG) registers */
+#define IMX908_REG_TPG_EN	CCI_REG8(0x30e0)
+#define IMX908_TPG_EN_BIT	BIT(0)
+#define IMX908_REG_TPG_PATSEL	CCI_REG8(0x30e2)	/* [4:0] pattern idx */
+#define IMX908_REG_TESTCLKEN	CCI_REG8(0x4900)
+#define IMX908_TESTCLKEN_BIT	BIT(3)
+
+#define IMX908_REG_TYPE_ID	CCI_REG16_LE(0x4c0c)
+#define IMX908_CHIP_ID		0x038c			/* 908 */
+
+/* ---- IMX908 Common Register List ---- */
+static const struct cci_reg_sequence imx908_common_regs[] = {
+	{ IMX908_XXS_DRV, IMX908_CONTROLLER_MODE },
+	{ CCI_REG8(0x039c), 0x03 },
+	{ CCI_REG8(0x3416), 0x20 },
+	{ CCI_REG8(0x3417), 0x00 },
+	{ CCI_REG8(0x3456), 0xf4 },
+	{ CCI_REG8(0x345d), 0x01 },
+	{ CCI_REG8(0x3460), 0x00 },
+	{ CCI_REG8(0x3461), 0x0b },
+	{ CCI_REG8(0x3471), 0x00 },
+	{ CCI_REG8(0x3472), 0x23 },
+	{ CCI_REG8(0x347b), 0x02 },
+	{ CCI_REG8(0x3481), 0x01 },
+	{ CCI_REG8(0x380c), 0x00 },
+	{ CCI_REG8(0x380f), 0x0c },
+	{ CCI_REG8(0x381c), 0x11 },
+	{ CCI_REG8(0x3820), 0x22 },
+	{ CCI_REG8(0x3824), 0x33 },
+	{ CCI_REG8(0x3828), 0x22 },
+	{ CCI_REG8(0x382c), 0x33 },
+	{ CCI_REG8(0x3830), 0x33 },
+	{ CCI_REG8(0x3838), 0x05 },
+	{ CCI_REG8(0x383c), 0x07 },
+	{ CCI_REG8(0x3840), 0x06 },
+	{ CCI_REG8(0x3848), 0x17 },
+	{ CCI_REG8(0x384c), 0x0b },
+	{ CCI_REG8(0x3850), 0x10 },
+	{ CCI_REG8(0x3854), 0x12 },
+	{ CCI_REG8(0x385c), 0x20 },
+	{ CCI_REG8(0x38c4), 0x64 },
+	{ CCI_REG8(0x38c5), 0x64 },
+	{ CCI_REG8(0x38c6), 0x64 },
+	{ CCI_REG8(0x3c4a), 0x15 },
+	{ CCI_REG8(0x3c4c), 0x13 },
+	{ CCI_REG8(0x3c4d), 0x13 },
+	{ CCI_REG8(0x3c4e), 0x13 },
+	{ CCI_REG8(0x3c50), 0x77 },
+	{ CCI_REG8(0x3c51), 0x07 },
+	{ CCI_REG8(0x3db4), 0x00 },
+	{ CCI_REG8(0x4419), 0x06 },
+	{ CCI_REG8(0x441c), 0x00 },
+	{ CCI_REG8(0x4426), 0x00 },
+	{ CCI_REG8(0x4538), 0x20 },
+	{ CCI_REG8(0x4539), 0x19 },
+	{ CCI_REG8(0x453a), 0x19 },
+	{ CCI_REG8(0x453b), 0x19 },
+	{ CCI_REG8(0x453c), 0x19 },
+	{ CCI_REG8(0x453d), 0x19 },
+	{ CCI_REG8(0x453e), 0x19 },
+	{ CCI_REG8(0x453f), 0x19 },
+	{ CCI_REG8(0x4540), 0x19 },
+	{ CCI_REG8(0x4544), 0x11 },
+	{ CCI_REG8(0x4545), 0x11 },
+	{ CCI_REG8(0x4546), 0x11 },
+	{ CCI_REG8(0x463c), 0x20 },
+	{ CCI_REG8(0x465e), 0xcf },
+	{ CCI_REG8(0x4684), 0x20 },
+	{ CCI_REG8(0x46a6), 0xcf },
+	{ CCI_REG8(0x46e2), 0xf3 },
+};
+
+static const u32 tuning_regs[] = {
+	CCI_REG8(0x3d78),
+	CCI_REG8(0x3d79),
+	CCI_REG8(0x3d80),
+	CCI_REG8(0x3d81),
+	CCI_REG8(0x3d88),
+	CCI_REG8(0x3d89),
+	CCI_REG8(0x3d90),
+	CCI_REG8(0x3d91),
+};
+
+#define IMX908_EXPOSURE_MIN	1
+#define IMX908_EXPOSURE_STEP	1
+
+/* ---- Speed of internal clock */
+#define IMX908_XHS_HZ		74250000ULL
+
+/* ---- Subdev Pads */
+#define IMX908_SOURCE_PAD	0
+
+#define IMX908_DEFAULT_MBUS_CODE MEDIA_BUS_FMT_SRGGB10_1X10
+
+/* IMX908 total area includes effective area plus
+ * 10 effective OB pixels in vertical direction,
+ * 10 pixels OB side ignored area in height
+ */
+static const struct v4l2_rect imx908_total_area = {
+	.top = 0,
+	.left = 0,
+	.width = 3856,
+	.height = 2200,
+};
+
+static const struct v4l2_rect imx908_active_area = {
+	.top = 0,
+	.left = 0,
+	.width = 3856,
+	.height = 2176,
+};
+
+/* Recommended 4K recording area centered within the active area */
+static const struct v4l2_rect imx908_recording_area = {
+	.top = 8,
+	.left = 8,
+	.width = 3840,
+	.height = 2160,
+};
+
+static const char *const imx908_supply_names[] = {
+	"avdd",  /* Analog (3.3V) supply */
+	"dvdd",  /* Digital Core (1.1V) supply */
+	"ovdd",  /* IF (1.8V) supply */
+};
+
+static const u32 imx908_mbus_codes[] = {
+	MEDIA_BUS_FMT_SRGGB10_1X10, /* RAW10 */
+	MEDIA_BUS_FMT_SRGGB12_1X12, /* RAW12 */
+};
+
+enum {
+	IMX908_LINK_FREQ_297MHZ,
+	IMX908_LINK_FREQ_360MHZ,
+	IMX908_LINK_FREQ_445MHZ,
+	IMX908_LINK_FREQ_594MHZ,
+	IMX908_LINK_FREQ_720MHZ,
+	IMX908_LINK_FREQ_891MHZ,
+	IMX908_LINK_FREQ_1039MHZ,
+	IMX908_LINK_FREQ_1188MHZ,
+};
+
+static const u64 imx908_link_freqs[] = {
+	[IMX908_LINK_FREQ_297MHZ]  =  297000000ULL,
+	[IMX908_LINK_FREQ_360MHZ]  =  360000000ULL,
+	[IMX908_LINK_FREQ_445MHZ]  =  445000000ULL,
+	[IMX908_LINK_FREQ_594MHZ]  =  594000000ULL,
+	[IMX908_LINK_FREQ_720MHZ]  =  720000000ULL,
+	[IMX908_LINK_FREQ_891MHZ]  =  891000000ULL,
+	[IMX908_LINK_FREQ_1039MHZ] = 1039500000ULL,
+	[IMX908_LINK_FREQ_1188MHZ] = 1188000000ULL,
+};
+
+/* DDR lane_rate (Mbps/lane) = 2 x link_freq (Hz) / 1e6 */
+static const u8 imx908_datarate_sel[] = {
+	[IMX908_LINK_FREQ_297MHZ]  = 0x07, /*  594 Mbps */
+	[IMX908_LINK_FREQ_360MHZ]  = 0x06, /*  720 Mbps */
+	[IMX908_LINK_FREQ_445MHZ]  = 0x05, /*  891 Mbps */
+	[IMX908_LINK_FREQ_594MHZ]  = 0x04, /* 1188 Mbps */
+	[IMX908_LINK_FREQ_720MHZ]  = 0x03, /* 1440 Mbps */
+	[IMX908_LINK_FREQ_891MHZ]  = 0x02, /* 1782 Mbps */
+	[IMX908_LINK_FREQ_1039MHZ] = 0x01, /* 2079 Mbps */
+	[IMX908_LINK_FREQ_1188MHZ] = 0x00, /* 2376 Mbps */
+};
+
+struct imx908_inck_cfg {
+	u32 xclk_hz;
+	u8  inck_sel;
+};
+
+/* Allowed clock values in Hz */
+static const u32 imx908_inck_table[] = {
+	74250000,
+	37125000,
+	72000000,
+	27000000,
+	24000000,
+};
+
+struct imx908_mode {
+	/* Sensor timing */
+	u16 hmax;	/* clocks per line */
+	u32 vmax;	/* lines per frame */
+
+	/* Derived values */
+	u64 pixel_rate;
+};
+
+struct imx908 {
+	/* Core V4L2 objects */
+	struct v4l2_subdev sd;
+	struct media_pad pad;
+	struct device *dev;
+
+	struct regmap *cci;
+
+	/* Hardware resources */
+	struct clk *xclk;
+	struct gpio_desc *reset_gpio;
+	struct regulator_bulk_data supplies[ARRAY_SIZE(imx908_supply_names)];
+
+	/* Clock configuration */
+	u32 xclk_freq;
+	u8  inck_sel;
+
+	/* Link/bus configuration */
+	u8 num_lanes;
+	unsigned long link_freq_bitmap;
+	unsigned int link_freq_idx;
+
+	struct imx908_mode mode; /* Current mode */
+
+	/* V4L2 controls */
+	struct {
+		struct v4l2_ctrl_handler handler;
+
+		struct v4l2_ctrl *pixel_rate;
+		struct v4l2_ctrl *exposure;
+		struct v4l2_ctrl *vblank;
+		struct v4l2_ctrl *hblank;
+		struct v4l2_ctrl *test_pattern;
+	} ctrls;
+};
+
+static inline struct imx908 *to_imx908(struct v4l2_subdev *_sd)
+{
+	return container_of(_sd, struct imx908, sd);
+}
+
+/* ----------------------- Basic Helper Functions ---------------------------- */
+
+/* Compute pixel-rate from link-freq/lanes/bpp */
+static inline u64 imx908_pixel_rate(u8 lanes, u8 bpp, u64 linkfreq_hz)
+{
+	u64 total = lanes * linkfreq_hz * 2; /* DDR: 2 * linkfreq */
+
+	return div_u64(total, bpp);
+}
+
+static int imx908_link_freq_to_datarate_sel(struct imx908 *imx, u64 link_freq_hz, u8 *sel)
+{
+	for (unsigned int i = 0; i < ARRAY_SIZE(imx908_link_freqs); i++) {
+		if (imx908_link_freqs[i] == link_freq_hz) {
+			*sel = imx908_datarate_sel[i];
+			return 0;
+		}
+	}
+	dev_err(imx->dev, "Unsupported link frequency %llu Hz\n", link_freq_hz);
+	return -EINVAL;
+}
+
+static bool imx908_mbus_code_supported(struct imx908 *imx, u32 code)
+{
+	for (unsigned int i = 0; i < ARRAY_SIZE(imx908_mbus_codes); i++) {
+		if (imx908_mbus_codes[i] == code)
+			return true;
+	}
+
+	return false;
+}
+
+static u16 imx908_calc_hmax(u32 width, u32 hblank, u64 pixel_rate)
+{
+	u64 hmax;
+
+	/* HMAX is expressed in cycles of the internal 74.25 MHz clock */
+	hmax = DIV_ROUND_UP_ULL((width + hblank) * IMX908_XHS_HZ, pixel_rate);
+
+	return min_t(u64, hmax, IMX908_HMAX_MAX);
+}
+
+static u32 imx908_calc_vmax(u32 height, u32 vblank)
+{
+	return min_t(u32, height + vblank, IMX908_VMAX_MAX);
+}
+
+static u16 imx908_calc_min_hmax(u32 width, u64 pixel_rate)
+{
+	/*
+	 * Calculate the minimum HMAX that guarantees:
+	 *     line_length_pixels >= width
+	 * where:
+	 *     line_length_pixels =
+	 *         ceil(HMAX * pixel_rate / IMX908_XHS_HZ)
+	 */
+	return DIV_ROUND_UP((u64)width * IMX908_XHS_HZ, pixel_rate);
+}
+
+static u32 imx908_calc_min_vmax(const struct v4l2_rect *crop)
+{
+	/* In window crop mode constrain VMAX (from datasheet) */
+	if (!v4l2_rect_equal(crop, &imx908_active_area))
+		return max(crop->height + IMX908_VMAX_CROP_MIN_MARGIN,
+			   IMX908_VMAX_CROP_MIN_LIMIT);
+
+	return IMX908_VMAX_DEFAULT;
+}
+
+static u32 imx908_calc_min_vblank(const struct v4l2_rect *crop)
+{
+	u32 min_vmax = imx908_calc_min_vmax(crop);
+
+	if (min_vmax <= crop->height)
+		return 0;
+
+	return min_vmax - crop->height;
+}
+
+static u32 imx908_calc_max_vblank(u32 height)
+{
+	return IMX908_VMAX_MAX - height;
+}
+
+static int imx908_set_exposure_lines(struct imx908 *imx, u32 exposure_lines)
+{
+	u32 max_lines = imx->mode.vmax - IMX908_MIN_SHR0;
+
+	exposure_lines = clamp_t(u32, exposure_lines, 1, max_lines);
+
+	/* Calculate the shutter register value based on current VMAX */
+	u32 shr0 = imx->mode.vmax - exposure_lines;
+
+	return cci_write(imx->cci, IMX908_REG_SHR0, shr0, NULL);
+}
+
+static u8 imx908_bits_per_pixel(u32 code)
+{
+	switch (code) {
+	case MEDIA_BUS_FMT_SRGGB12_1X12:
+		return 12;
+	case MEDIA_BUS_FMT_SRGGB10_1X10:
+	default:
+		return 10;
+	}
+}
+
+static int imx908_sync_pixel_rate_ctrl(struct imx908 *imx)
+{
+	s64 pixel_rate = imx->mode.pixel_rate;
+	int ret;
+
+	if (!imx->ctrls.pixel_rate)
+		return 0;
+
+	/* Update control range so pixel_rate is fixed (min = max = default) */
+	__v4l2_ctrl_modify_range(imx->ctrls.pixel_rate, pixel_rate, pixel_rate,
+				 1, pixel_rate);
+
+	/* Update the current control value to match the new mode pixel_rate */
+	ret = __v4l2_ctrl_s_ctrl_int64(imx->ctrls.pixel_rate, pixel_rate);
+	if (ret) {
+		dev_err(imx->dev, "failed to update pixel rate control: %d\n", ret);
+		return ret;
+	}
+
+	return ret;
+}
+
+static u32 imx908_hmax_to_hblank(u16 hmax, u64 pixel_rate, u32 width)
+{
+	/* Convert HMAX to line length in pixels */
+	u64 line_len_pixels = div_u64((u64)hmax * pixel_rate, IMX908_XHS_HZ);
+
+	if (line_len_pixels <= width)
+		return 0;
+
+	/* Subtract active image width to obtain horizontal blanking interval */
+	return line_len_pixels - width;
+}
+
+static void imx908_update_hblank_limits(struct imx908 *imx, u32 width, u64 pixel_rate)
+{
+	u16 min_hmax = imx908_calc_min_hmax(width, pixel_rate);
+	u32 min_hblank = imx908_hmax_to_hblank(min_hmax, pixel_rate, width);
+	u32 max_hblank = imx908_hmax_to_hblank(IMX908_HMAX_MAX, pixel_rate, width);
+	u32 hblank = imx908_hmax_to_hblank(imx->mode.hmax, pixel_rate, width);
+
+	hblank = clamp_t(u32, hblank, min_hblank, max_hblank);
+
+	__v4l2_ctrl_modify_range(imx->ctrls.hblank, min_hblank, max_hblank, 1, hblank);
+
+	if (imx->ctrls.hblank->val != hblank)
+		__v4l2_ctrl_s_ctrl(imx->ctrls.hblank, hblank);
+}
+
+static void imx908_update_vblank_limits(struct imx908 *imx,
+					const struct v4l2_rect *crop)
+{
+	u32 min_vblank;
+	u32 max_vblank;
+	u32 vblank;
+
+	if (!imx->ctrls.vblank)
+		return;
+
+	min_vblank = imx908_calc_min_vblank(crop);
+	max_vblank = imx908_calc_max_vblank(crop->height);
+	vblank = clamp_t(u32, imx->ctrls.vblank->val,
+			 min_vblank, max_vblank);
+
+	__v4l2_ctrl_modify_range(imx->ctrls.vblank,
+				 min_vblank,
+				 max_vblank,
+				 1,
+				 vblank);
+}
+
+/* ------------------ Pattern Generator (TPG) ----------------------- */
+static const char * const imx908_tpg_menu[] = {
+	"Disabled",
+	"All 000h",
+	"All FFFh",
+	"All 555h",
+	"All AAAh",
+	"555h / AAAh Toggle",
+	"AAAh / 555h Toggle",
+	"000h / 555h Toggle",
+	"555h / 000h Toggle",
+	"000h / FFFh Toggle",
+	"FFFh / 000h Toggle",
+	"Horiz Color Bars",
+	"Vert Color Bars",
+};
+
+static int imx908_update_test_pattern(struct imx908 *imx, u32 index)
+{
+	int ret = 0;
+
+	if (index == 0) {
+		/* Picture (not TPG) output setting */
+		cci_update_bits(imx->cci, IMX908_REG_TPG_EN, IMX908_TPG_EN_BIT,
+				0x00, &ret);
+
+		/* Disable TESTCLKEN (write-only register) */
+		cci_write(imx->cci, IMX908_REG_TESTCLKEN, 0x00, &ret);
+
+		return ret;
+	}
+
+	cci_write(imx->cci, IMX908_REG_TESTCLKEN, IMX908_TESTCLKEN_BIT, &ret);
+	cci_write(imx->cci, IMX908_REG_TPG_PATSEL, index - 1, &ret);
+	cci_update_bits(imx->cci, IMX908_REG_TPG_EN, IMX908_TPG_EN_BIT,
+			IMX908_TPG_EN_BIT, &ret);
+
+	return ret;
+}
+
+/* -------------------- Mode Building ----------------------- */
+static void imx908_mode_build(struct imx908 *imx,
+			      const struct v4l2_mbus_framefmt *fmt,
+			      const struct v4l2_rect *crop)
+{
+	struct imx908_mode *m = &imx->mode;
+	u8 bpp = imx908_bits_per_pixel(fmt->code);
+	u64 link_freq = imx908_link_freqs[imx->link_freq_idx];
+
+	m->pixel_rate = imx908_pixel_rate(imx->num_lanes, bpp, link_freq);
+
+	/* --- H timing (HMAX) --- */
+	if (v4l2_rect_equal(crop, &imx908_active_area)) {
+		/* Update HBLANK HMAX in all pixel mode */
+		u16 min_hmax = imx908_calc_min_hmax(fmt->width, m->pixel_rate);
+		u32 min_hblank = imx908_hmax_to_hblank(min_hmax, m->pixel_rate,
+						       fmt->width);
+		u32 max_hblank = imx908_hmax_to_hblank(IMX908_HMAX_MAX,
+						       m->pixel_rate, fmt->width);
+		u32 hblank = clamp_t(u32, imx->ctrls.hblank->val, min_hblank,
+				     max_hblank);
+		m->hmax = imx908_calc_hmax(fmt->width, hblank, m->pixel_rate);
+	} else if (!m->hmax) {
+		u32 hblank = imx908_hmax_to_hblank(IMX908_HMAX_DEFAULT,
+						   m->pixel_rate,
+						   imx908_active_area.width);
+
+		m->hmax = imx908_calc_hmax(fmt->width, hblank, m->pixel_rate);
+	}
+
+	/* --- V timing (VMAX) --- */
+	u32 min_vblank = imx908_calc_min_vblank(crop);
+	u32 max_vblank = imx908_calc_max_vblank(fmt->height);
+
+	u32 vblank = clamp_t(u32,
+			 imx->ctrls.vblank->val,
+			 min_vblank,
+			 max_vblank);
+
+	m->vmax = imx908_calc_vmax(fmt->height, vblank);
+
+	u64 line_length_pixels = div_u64((u64)m->hmax * m->pixel_rate, IMX908_XHS_HZ);
+
+	if (line_length_pixels < fmt->width)
+		dev_warn(imx->dev, "Invalid timing (line=%llu < width=%u)\n",
+			 line_length_pixels, fmt->width);
+}
+
+static void imx908_update_framing_limits(struct imx908 *imx, struct v4l2_subdev_state *state)
+{
+	const struct v4l2_mbus_framefmt *fmt;
+	const struct v4l2_rect *crop;
+
+	fmt = v4l2_subdev_state_get_format(state, IMX908_SOURCE_PAD);
+	crop = v4l2_subdev_state_get_crop(state, IMX908_SOURCE_PAD);
+
+	u8 bpp = imx908_bits_per_pixel(fmt->code);
+	u64 link_freq = imx908_link_freqs[imx->link_freq_idx];
+	u64 pixel_rate = imx908_pixel_rate(imx->num_lanes, bpp, link_freq);
+
+	/* Only update HBLANK in all pixel mode */
+	if (v4l2_rect_equal(crop, &imx908_active_area))
+		imx908_update_hblank_limits(imx, fmt->width, pixel_rate);
+
+	imx908_update_vblank_limits(imx, crop);
+
+	/* Build mode so cached timing uses current control values */
+	imx908_mode_build(imx, fmt, crop);
+
+	/* Exposure handling */
+	__v4l2_ctrl_modify_range(imx->ctrls.exposure,
+				 1,
+				 imx->mode.vmax - IMX908_MIN_SHR0,
+				 1,
+				 min_t(u32, imx->ctrls.exposure->val,
+				       imx->mode.vmax - IMX908_MIN_SHR0));
+}
+
+/* ----------------------- HW Programming ---------------------- */
+
+static int imx908_mode_apply(struct imx908 *imx,
+			     const struct v4l2_mbus_framefmt *fmt)
+{
+	int ret = 0;
+	u8 adbit, mdbit;
+
+	switch (fmt->code) {
+	case MEDIA_BUS_FMT_SRGGB12_1X12:
+		adbit = IMX908_ADBIT_12BIT;
+		mdbit = IMX908_MDBIT_RAW12;
+		break;
+
+	case MEDIA_BUS_FMT_SRGGB10_1X10:
+	default:
+		adbit = IMX908_ADBIT_10BIT;
+		mdbit = IMX908_MDBIT_RAW10;
+		break;
+	}
+
+	cci_write(imx->cci, IMX908_REG_XMASTER, IMX908_CONTROLLER_MODE, &ret);
+	cci_write(imx->cci, IMX908_REG_ADBIT, adbit, &ret);
+	cci_write(imx->cci, IMX908_REG_MDBIT, mdbit, &ret);
+	if (ret) {
+		dev_err(imx->dev, "Mode register write failed: %d\n", ret);
+		return ret;
+	}
+
+	/* Tuning registers that need to be changed for RAW10/10-bit */
+	u16 value = adbit == IMX908_ADBIT_10BIT &&
+		    mdbit == IMX908_MDBIT_RAW10 ? 0x0c : 0x05;
+
+	for (unsigned int i = 0; i < ARRAY_SIZE(tuning_regs); ++i)
+		cci_write(imx->cci, tuning_regs[i], value, &ret);
+
+	if (ret) {
+		dev_err(imx->dev, "Tuning register write failed: %d\n", ret);
+		return ret;
+	}
+
+	/* INCK */
+	cci_write(imx->cci, IMX908_REG_INCK_SEL, imx->inck_sel, &ret);
+	if (ret) {
+		dev_err(imx->dev, "INCK register write failed: %d\n", ret);
+		return ret;
+	}
+
+	/* MIPI lane rate selections */
+	u8 datarate_sel;
+	u64 link_freq = imx908_link_freqs[imx->link_freq_idx];
+
+	ret = imx908_link_freq_to_datarate_sel(imx, link_freq, &datarate_sel);
+	if (ret) {
+		dev_err(imx->dev, "Unsupported link_freq=%llu for DATARATE_SEL\n",
+			link_freq);
+		return ret;
+	}
+	cci_write(imx->cci, IMX908_REG_DATARATE_SEL, datarate_sel, &ret);
+
+	/* Lane config */
+	cci_write(imx->cci, IMX908_REG_LANEMODE, imx->num_lanes - 1, &ret);
+	if (ret) {
+		dev_err(imx->dev, "Clock/lane configuration failed: %d\n", ret);
+		return ret;
+	}
+
+	/* Recommended black level offset is 50 in 10-bit, 200 for others */
+	u16 blklevel = (mdbit == IMX908_MDBIT_RAW10) ? 50 : 200;
+
+	cci_write(imx->cci, IMX908_REG_BLKLEVEL, blklevel, &ret);
+
+	return ret;
+}
+
+static int imx908_program_window(struct imx908 *imx, const struct v4l2_rect *crop)
+{
+	bool all_pixel_mode = v4l2_rect_equal(crop, &imx908_active_area);
+	int ret = 0;
+
+	cci_write(imx->cci, IMX908_REG_WINMODE,
+		  all_pixel_mode ? IMX908_WINMODE_ALLPIX : IMX908_WINMODE_CROP,
+		  &ret);
+
+	/* Update window size in window crop mode */
+	if (!all_pixel_mode) {
+		cci_write(imx->cci, IMX908_REG_PIX_HST,    crop->left,   &ret);
+		cci_write(imx->cci, IMX908_REG_PIX_HWIDTH, crop->width,  &ret);
+		cci_write(imx->cci, IMX908_REG_PIX_VST,    crop->top,    &ret);
+		cci_write(imx->cci, IMX908_REG_PIX_VWIDTH, crop->height, &ret);
+	}
+
+	return ret;
+}
+
+/* ----------------------- Start/Stop streaming ---------------------- */
+
+static int imx908_start_streaming(struct imx908 *imx)
+{
+	struct v4l2_subdev_state *sd_state;
+	const struct v4l2_rect *crop;
+	const struct v4l2_mbus_framefmt *format;
+	int ret = 0;
+
+	/* Get active state */
+	sd_state = v4l2_subdev_get_locked_active_state(&imx->sd);
+	crop = v4l2_subdev_state_get_crop(sd_state, IMX908_SOURCE_PAD);
+	format = v4l2_subdev_state_get_format(sd_state, IMX908_SOURCE_PAD);
+
+	/* Build the mode based on active state before streaming */
+	imx908_mode_build(imx, format, crop);
+
+	/* Write common registers: static sensor tuning only */
+	cci_multi_reg_write(imx->cci, imx908_common_regs,
+			    ARRAY_SIZE(imx908_common_regs), &ret);
+	if (ret)
+		return ret;
+
+	/* HW write all other registers (which depend on mode) */
+	ret = imx908_mode_apply(imx, format);
+	if (ret)
+		return ret;
+
+	/* Geometry */
+	ret = imx908_program_window(imx, crop);
+	if (ret)
+		return ret;
+
+	/* Apply the current control values */
+	ret = __v4l2_ctrl_handler_setup(imx->sd.ctrl_handler);
+	if (ret)
+		return ret;
+
+	/* Cancel standby state */
+	cci_write(imx->cci, IMX908_REG_STANDBY, IMX908_STANDBY_CANCEL, &ret);
+	msleep(24); /* SRM >=24 ms regulator stabilization after standby cancel */
+
+	/* Start controller mode */
+	cci_write(imx->cci, IMX908_REG_XMSTA, IMX908_START_CONTROLLER, &ret);
+	return ret;
+}
+
+static int imx908_stop_streaming(struct imx908 *imx)
+{
+	int ret = 0;
+
+	cci_write(imx->cci, IMX908_REG_STANDBY, IMX908_STANDBY_EN, &ret);
+	cci_write(imx->cci, IMX908_REG_XMSTA, IMX908_STOP_CONTROLLER, &ret);
+	return ret;
+}
+
+/* --------------------------- V4L2 controls ------------------------------ */
+
+static int imx908_set_ctrl(struct v4l2_ctrl *ctrl)
+{
+	struct imx908 *imx = container_of(ctrl->handler, struct imx908, ctrls.handler);
+	struct v4l2_subdev_state *state;
+	struct i2c_client *client = v4l2_get_subdevdata(&imx->sd);
+	const struct v4l2_mbus_framefmt *format;
+	int ret = 0;
+
+	/* Retrieve the active, locked subdev state format dimensions */
+	state = v4l2_subdev_get_locked_active_state(&imx->sd);
+	format = v4l2_subdev_state_get_format(state, IMX908_SOURCE_PAD);
+
+	/* Applying V4L2 controls only if powered up */
+	if (!pm_runtime_get_if_in_use(&client->dev))
+		return 0;
+
+	switch (ctrl->id) {
+	case V4L2_CID_EXPOSURE: {
+		u32 lines = clamp_t(u32, ctrl->val, 1,
+				    imx->mode.vmax - IMX908_MIN_SHR0);
+
+		ret = imx908_set_exposure_lines(imx, lines);
+		break;
+	}
+
+	case V4L2_CID_ANALOGUE_GAIN:
+
+		u32 reg = clamp_t(u32, ctrl->val, IMX908_ANA_GAIN_MIN,
+				  IMX908_ANA_GAIN_MAX);
+
+		cci_write(imx->cci, IMX908_REG_GAIN, reg, &ret);
+		break;
+
+	case V4L2_CID_VBLANK: {
+		const struct v4l2_rect *crop;
+
+		crop = v4l2_subdev_state_get_crop(state, IMX908_SOURCE_PAD);
+
+		u32 min_vblank = imx908_calc_min_vblank(crop);
+		u32 max_vblank = imx908_calc_max_vblank(format->height);
+		u32 vblank = clamp_t(u32, ctrl->val, min_vblank, max_vblank);
+
+		imx->mode.vmax = imx908_calc_vmax(format->height, vblank);
+
+		u32 exposure = min_t(u32, imx->ctrls.exposure->val,
+				     imx->mode.vmax - IMX908_MIN_SHR0);
+
+		__v4l2_ctrl_modify_range(imx->ctrls.exposure,
+					 IMX908_EXPOSURE_MIN,
+					 imx->mode.vmax - IMX908_MIN_SHR0,
+					 IMX908_EXPOSURE_STEP, exposure);
+
+		ret = cci_write(imx->cci, IMX908_REG_VMAX,
+				imx->mode.vmax, NULL);
+		if (!ret)
+			ret = imx908_set_exposure_lines(imx, exposure);
+
+		break;
+	}
+
+	case V4L2_CID_HBLANK: {
+		u16 hmax;
+		u32 width = format->width;
+		u64 pixel_rate = imx->mode.pixel_rate;
+		const struct v4l2_rect *crop;
+
+		crop = v4l2_subdev_state_get_crop(state, IMX908_SOURCE_PAD);
+
+		/* Ignore HBLANK update in window crop mode */
+		if (!v4l2_rect_equal(crop, &imx908_active_area))
+			break;
+
+		/* Skip timing calculation when not initialized */
+		if (!pixel_rate || !width)
+			break;
+
+		/* Recompute hmax based on HBLANK */
+		hmax = imx908_calc_hmax(width, ctrl->val, pixel_rate);
+		imx->mode.hmax = hmax;
+		cci_write(imx->cci, IMX908_REG_HMAX, hmax, &ret);
+		break;
+	}
+
+	case V4L2_CID_PIXEL_RATE:
+	case V4L2_CID_LINK_FREQ:
+		break;
+
+	case V4L2_CID_TEST_PATTERN:
+		ret = imx908_update_test_pattern(imx, ctrl->val);
+		break;
+
+	case V4L2_CID_HFLIP:
+		cci_write(imx->cci, IMX908_REG_HREVERSE, ctrl->val, &ret);
+		break;
+
+	case V4L2_CID_VFLIP:
+		cci_write(imx->cci, IMX908_REG_VREVERSE, ctrl->val, &ret);
+		break;
+
+	default:
+		dev_warn(&client->dev,
+			 "ctrl(id:0x%x,val:0x%x) is not handled\n",
+			 ctrl->id, ctrl->val);
+		break;
+	}
+
+	pm_runtime_put(&client->dev);
+	return ret;
+}
+
+static const struct v4l2_ctrl_ops imx908_ctrl_ops = {
+	.s_ctrl = imx908_set_ctrl,
+};
+
+/* ------------------------ Pad/video ops ------------------ */
+
+static int imx908_enum_mbus_code(struct v4l2_subdev *sd,
+				 struct v4l2_subdev_state *sd_state,
+				 struct v4l2_subdev_mbus_code_enum *code)
+{
+	if (code->index >= ARRAY_SIZE(imx908_mbus_codes))
+		return -EINVAL;
+	code->code = imx908_mbus_codes[code->index];
+	return 0;
+}
+
+static int imx908_enum_frame_size(struct v4l2_subdev *sd,
+				  struct v4l2_subdev_state *sd_state,
+				  struct v4l2_subdev_frame_size_enum *fse)
+{
+	const struct v4l2_rect *crop = v4l2_subdev_state_get_crop(sd_state, IMX908_SOURCE_PAD);
+	struct imx908 *imx = to_imx908(sd);
+
+	if (fse->index > 0)
+		return -EINVAL;
+
+	if (!imx908_mbus_code_supported(imx, fse->code))
+		return -EINVAL;
+
+	fse->min_width  = crop->width;
+	fse->max_width  = crop->width;
+	fse->min_height = crop->height;
+	fse->max_height = crop->height;
+
+	return 0;
+}
+
+static int imx908_set_pad_format(struct v4l2_subdev *sd,
+				 struct v4l2_subdev_state *sd_state,
+				 struct v4l2_subdev_format *fmt)
+{
+	struct imx908 *imx = to_imx908(sd);
+	struct v4l2_mbus_framefmt *format;
+	int ret = 0;
+
+	if (fmt->which == V4L2_SUBDEV_FORMAT_ACTIVE && v4l2_subdev_is_streaming(sd))
+		return -EBUSY;
+
+	format = v4l2_subdev_state_get_format(sd_state, fmt->pad);
+
+	if (imx908_mbus_code_supported(imx, fmt->format.code))
+		format->code = fmt->format.code;
+	else
+		format->code = IMX908_DEFAULT_MBUS_CODE;
+
+	/* Fully define metadata */
+	format->field        = V4L2_FIELD_NONE;
+	format->colorspace   = V4L2_COLORSPACE_RAW;
+	format->ycbcr_enc    = V4L2_YCBCR_ENC_DEFAULT;
+	format->quantization = V4L2_QUANTIZATION_FULL_RANGE;
+	format->xfer_func    = V4L2_XFER_FUNC_NONE;
+
+	/* Update pixel rate range and hblank (ACTIVE ONLY) */
+	if (fmt->which == V4L2_SUBDEV_FORMAT_ACTIVE) {
+		imx908_update_framing_limits(imx, sd_state);
+		ret = imx908_sync_pixel_rate_ctrl(imx);
+		if (ret)
+			return ret;
+	}
+
+	/* Return final applied format to userspace */
+	fmt->format = *format;
+
+	return 0;
+}
+
+static int imx908_set_selection(struct v4l2_subdev *sd,
+				struct v4l2_subdev_state *sd_state,
+				struct v4l2_subdev_selection *sel)
+{
+	struct imx908 *imx = to_imx908(sd);
+	struct v4l2_rect *crop; /* current crop rectangle in subdev state */
+	struct v4l2_rect *r = &sel->r;
+	struct v4l2_mbus_framefmt *format;
+
+	if (sel->target != V4L2_SEL_TGT_CROP)
+		return -EINVAL;
+
+	if (sel->which == V4L2_SUBDEV_FORMAT_ACTIVE && v4l2_subdev_is_streaming(sd))
+		return -EBUSY;
+
+	crop = v4l2_subdev_state_get_crop(sd_state, sel->pad);
+	format = v4l2_subdev_state_get_format(sd_state, sel->pad);
+
+	/*
+	 * Round the crop rectangle size down to the hardware alignment
+	 * constraints, then clamp it to the supported size range.
+	 */
+	r->width = clamp_t(s32, ALIGN_DOWN(r->width, IMX908_CROP_ALIGN_HWIDTH),
+			   IMX908_CROP_MIN_WIDTH, imx908_active_area.width);
+	r->height = clamp_t(s32, ALIGN_DOWN(r->height, IMX908_CROP_ALIGN_VWIDTH),
+			    IMX908_CROP_MIN_HEIGHT, imx908_active_area.height);
+
+	/*
+	 * Round and clamp the crop position similarly, with the maximum value
+	 * chosen so that the crop rectangle remains fully inside the active
+	 * pixel array.
+	 */
+	r->left = clamp_t(s32, ALIGN_DOWN(r->left, IMX908_CROP_ALIGN_HSTART), 0,
+			  imx908_active_area.width - r->width);
+	r->top = clamp_t(s32, ALIGN_DOWN(r->top, IMX908_CROP_ALIGN_VSTART), 0,
+			 imx908_active_area.height - r->height);
+
+	/* Update the crop rectangle in subdev state */
+	*crop = *r;
+
+	/* Binning is not supported in IMX908 sensor */
+	format->width  = crop->width;
+	format->height = crop->height;
+
+	if (sel->which == V4L2_SUBDEV_FORMAT_ACTIVE)
+		imx908_update_framing_limits(imx, sd_state);
+
+	return 0;
+}
+
+static int imx908_get_selection(struct v4l2_subdev *sd,
+				struct v4l2_subdev_state *sd_state,
+				struct v4l2_subdev_selection *sel)
+{
+	switch (sel->target) {
+	case V4L2_SEL_TGT_CROP:
+		sel->r = *v4l2_subdev_state_get_crop(sd_state, IMX908_SOURCE_PAD);
+		return 0;
+
+	case V4L2_SEL_TGT_NATIVE_SIZE:
+		sel->r = imx908_total_area;
+		return 0;
+
+	case V4L2_SEL_TGT_CROP_DEFAULT:
+		sel->r = imx908_recording_area;
+		return 0;
+
+	case V4L2_SEL_TGT_CROP_BOUNDS:
+		sel->r = imx908_active_area;
+		return 0;
+
+	default:
+		return -EINVAL;
+	}
+}
+
+static int imx908_enable_streams(struct v4l2_subdev *sd,
+				 struct v4l2_subdev_state *sd_state,
+				 u32 pad,
+				 u64 streams_mask)
+{
+	struct imx908 *imx = to_imx908(sd);
+	int ret;
+
+	ret = pm_runtime_resume_and_get(imx->dev);
+	if (ret)
+		return ret;
+
+	ret = imx908_start_streaming(imx);
+	if (ret) {
+		pm_runtime_mark_last_busy(imx->dev);
+		pm_runtime_put_autosuspend(imx->dev);
+	}
+
+	return ret;
+}
+
+static int imx908_disable_streams(struct v4l2_subdev *sd,
+				  struct v4l2_subdev_state *sd_state,
+				  u32 pad,
+				  u64 streams_mask)
+{
+	struct imx908 *imx = to_imx908(sd);
+	int ret;
+
+	ret = imx908_stop_streaming(imx);
+
+	pm_runtime_mark_last_busy(imx->dev);
+	pm_runtime_put_autosuspend(imx->dev);
+
+	return ret;
+}
+
+static int imx908_init_state(struct v4l2_subdev *sd,
+			     struct v4l2_subdev_state *sd_state)
+{
+	int ret = 0;
+	struct v4l2_subdev_selection sel = {
+		.which  = V4L2_SUBDEV_FORMAT_TRY,
+		.pad    = IMX908_SOURCE_PAD,
+		.target = V4L2_SEL_TGT_CROP,
+		.r      = imx908_active_area,
+	};
+	struct v4l2_subdev_format fmt = {
+		.which = V4L2_SUBDEV_FORMAT_TRY,
+		.pad   = IMX908_SOURCE_PAD,
+		.format = {
+			.code   = IMX908_DEFAULT_MBUS_CODE,
+			.width  = imx908_active_area.width,
+			.height = imx908_active_area.height,
+		},
+	};
+
+	ret = imx908_set_selection(sd, sd_state, &sel);
+	ret = imx908_set_pad_format(sd, sd_state, &fmt);
+
+	return ret;
+}
+
+static const struct v4l2_subdev_core_ops imx908_core_ops = {
+	.subscribe_event   = v4l2_ctrl_subdev_subscribe_event,
+	.unsubscribe_event = v4l2_event_subdev_unsubscribe,
+};
+
+static const struct v4l2_subdev_video_ops imx908_video_ops = {
+	.s_stream = v4l2_subdev_s_stream_helper,
+};
+
+static const struct v4l2_subdev_pad_ops imx908_pad_ops = {
+	.enum_mbus_code      = imx908_enum_mbus_code,
+	.enum_frame_size     = imx908_enum_frame_size,
+	.get_fmt             = v4l2_subdev_get_fmt,
+	.set_fmt             = imx908_set_pad_format,
+	.get_selection       = imx908_get_selection,
+	.set_selection       = imx908_set_selection,
+	.enable_streams      = imx908_enable_streams,
+	.disable_streams     = imx908_disable_streams,
+};
+
+static const struct v4l2_subdev_internal_ops imx908_internal_ops = {
+	.init_state = imx908_init_state,
+};
+
+static const struct v4l2_subdev_ops imx908_subdev_ops = {
+	.core  = &imx908_core_ops,
+	.video = &imx908_video_ops,
+	.pad   = &imx908_pad_ops,
+};
+
+/* ----------------------- Power management ---------------------- */
+
+static int imx908_power_on(struct imx908 *imx)
+{
+	int ret;
+
+	/* Power Rails */
+	ret = regulator_bulk_enable(ARRAY_SIZE(imx908_supply_names), imx->supplies);
+	if (ret) {
+		dev_err(imx->dev, "%s: failed to enable regulators\n", __func__);
+		return ret;
+	}
+	msleep(200); /* IMX908 power ok after 200ms */
+
+	if (imx->reset_gpio) {
+		gpiod_set_value_cansleep(imx->reset_gpio, 1); /* XCLR low */
+		udelay(1); /* >= 500ns T_low */
+		gpiod_set_value_cansleep(imx->reset_gpio, 0); /* Sensor starts */
+	}
+
+	/* Start INCK */
+	ret = clk_prepare_enable(imx->xclk);
+	if (ret) {
+		dev_err(imx->dev, "failed to enable xclk: %d\n", ret);
+		goto err_reset;
+	}
+
+	/* T_1 >=20us delay before initial SDA/SCL  */
+	usleep_range(20, 25);
+
+	return 0;
+
+err_reset:
+	gpiod_set_value_cansleep(imx->reset_gpio, 1); /* assert reset */
+	regulator_bulk_disable(ARRAY_SIZE(imx908_supply_names), imx->supplies);
+	return ret;
+}
+
+static void imx908_power_off(struct imx908 *imx)
+{
+	gpiod_set_value_cansleep(imx->reset_gpio, 1);
+	clk_disable_unprepare(imx->xclk);
+	regulator_bulk_disable(ARRAY_SIZE(imx908_supply_names), imx->supplies);
+}
+
+static int imx908_runtime_resume(struct device *dev)
+{
+	struct v4l2_subdev *sd = dev_get_drvdata(dev);
+	struct imx908 *imx = to_imx908(sd);
+
+	return imx908_power_on(imx);
+}
+
+static int imx908_runtime_suspend(struct device *dev)
+{
+	struct v4l2_subdev *sd = dev_get_drvdata(dev);
+	struct imx908 *imx = to_imx908(sd);
+
+	imx908_power_off(imx);
+	return 0;
+}
+
+static DEFINE_RUNTIME_DEV_PM_OPS(imx908_pm_ops,
+				 imx908_runtime_suspend,
+				 imx908_runtime_resume, NULL);
+
+/* ------------------------------- Probe/remove --------------------------- */
+
+static int imx908_get_inck_sel(struct imx908 *imx)
+{
+	for (unsigned int i = 0; i < ARRAY_SIZE(imx908_inck_table); i++) {
+		if (imx908_inck_table[i] == imx->xclk_freq) {
+			imx->inck_sel = i;
+			return 0;
+		}
+	}
+
+	return dev_err_probe(imx->dev, -EINVAL, "unsupported xclk %u Hz\n", imx->xclk_freq);
+}
+
+static int imx908_get_regulators(struct imx908 *imx)
+{
+	for (unsigned int i = 0; i < ARRAY_SIZE(imx908_supply_names); i++)
+		imx->supplies[i].supply = imx908_supply_names[i];
+
+	return devm_regulator_bulk_get(imx->dev,
+			ARRAY_SIZE(imx908_supply_names),
+			imx->supplies);
+}
+
+static int imx908_parse_fwnode(struct imx908 *imx)
+{
+	struct fwnode_handle *ep;
+	struct v4l2_fwnode_endpoint bus_cfg = {
+		.bus_type = V4L2_MBUS_CSI2_DPHY
+	};
+	int ret = 0;
+
+	/* Get firmware node from device tree */
+	ep = fwnode_graph_get_next_endpoint(dev_fwnode(imx->dev), NULL);
+	if (!ep) {
+		dev_err(imx->dev, "Failed to find endpoint in device tree\n");
+		return -ENXIO;
+	}
+
+	/* Parse properties: data-lanes, link-frequencies */
+	ret = v4l2_fwnode_endpoint_alloc_parse(ep, &bus_cfg);
+	fwnode_handle_put(ep);
+	if (ret)
+		return ret;
+
+	imx->num_lanes = bus_cfg.bus.mipi_csi2.num_data_lanes;
+
+	if (imx->num_lanes != 2 && imx->num_lanes != 4) {
+		dev_err(imx->dev, "only 2 or 4 CSI-2 data lanes are supported (got %u)\n",
+			imx->num_lanes);
+		ret = -EINVAL;
+		goto out_free;
+	}
+
+	ret = v4l2_link_freq_to_bitmap(imx->dev,
+				       bus_cfg.link_frequencies,
+				       bus_cfg.nr_of_link_frequencies,
+				       imx908_link_freqs,
+				       ARRAY_SIZE(imx908_link_freqs),
+				       &imx->link_freq_bitmap);
+	if (ret) {
+		dev_err(imx->dev, "Failed to parse link frequencies from DT\n");
+		goto out_free;
+	}
+
+	if (bitmap_empty(&imx->link_freq_bitmap, ARRAY_SIZE(imx908_link_freqs))) {
+		dev_err(imx->dev,
+			"No common link frequencies between driver and DT\n");
+		ret = -EINVAL;
+		goto out_free;
+	}
+
+	imx->link_freq_idx = __ffs(imx->link_freq_bitmap);
+
+out_free:
+	v4l2_fwnode_endpoint_free(&bus_cfg);
+	return ret;
+}
+
+static int imx908_init_controls(struct imx908 *imx)
+{
+	struct v4l2_ctrl_handler *hdl = &imx->ctrls.handler;
+	struct v4l2_ctrl *link_freq_ctl;
+	struct v4l2_fwnode_device_properties props;
+	int ret;
+	s32 hblank, min_hblank;
+	u32 max_exp, max_hblank, min_vblank, max_vblank;
+	u16 min_hmax;
+
+	ret = v4l2_ctrl_handler_init(hdl, 11);
+	if (ret)
+		return ret;
+
+	/* Initialize the pixel_rate control */
+	u8 bpp = imx908_bits_per_pixel(IMX908_DEFAULT_MBUS_CODE);
+	u64 link_freq = imx908_link_freqs[imx->link_freq_idx];
+	u64 pixel_rate = imx908_pixel_rate(imx->num_lanes, bpp, link_freq);
+
+	imx->ctrls.pixel_rate = v4l2_ctrl_new_std(hdl, &imx908_ctrl_ops, V4L2_CID_PIXEL_RATE,
+						  pixel_rate, pixel_rate, 1, pixel_rate);
+	if (imx->ctrls.pixel_rate)
+		imx->ctrls.pixel_rate->flags |= V4L2_CTRL_FLAG_READ_ONLY;
+
+	/* Set link Frequency */
+	link_freq_ctl = v4l2_ctrl_new_int_menu(hdl, &imx908_ctrl_ops,
+					       V4L2_CID_LINK_FREQ,
+					       ARRAY_SIZE(imx908_link_freqs) - 1,
+					       imx->link_freq_idx,
+					       imx908_link_freqs);
+
+	if (link_freq_ctl)
+		link_freq_ctl->flags |= V4L2_CTRL_FLAG_READ_ONLY;
+
+	/* Initialize HMAX and VMAX */
+	imx->mode.hmax = IMX908_HMAX_DEFAULT;
+	imx->mode.vmax = IMX908_VMAX_DEFAULT;
+
+	/* Set vblank */
+	min_vblank = IMX908_VMAX_DEFAULT - imx908_active_area.height;
+	max_vblank = imx908_calc_max_vblank(imx908_active_area.height);
+
+	imx->ctrls.vblank = v4l2_ctrl_new_std(hdl, &imx908_ctrl_ops,
+					      V4L2_CID_VBLANK,
+					      min_vblank,
+					      max_vblank,
+					      1,
+					      min_vblank);
+
+	/* Set hblank */
+	min_hmax = imx908_calc_min_hmax(imx908_active_area.width, pixel_rate);
+	min_hblank = imx908_hmax_to_hblank(min_hmax, pixel_rate,
+					   imx908_active_area.width);
+	max_hblank = imx908_hmax_to_hblank(IMX908_HMAX_MAX, pixel_rate,
+					   imx908_active_area.width);
+	hblank = imx908_hmax_to_hblank(IMX908_HMAX_DEFAULT, pixel_rate,
+				       imx908_active_area.width);
+
+	imx->ctrls.hblank = v4l2_ctrl_new_std(hdl, &imx908_ctrl_ops,
+					      V4L2_CID_HBLANK,
+					      min_hblank,
+					      max_hblank,
+					      1,
+					      hblank);
+	max_exp = imx->mode.vmax - IMX908_MIN_SHR0;
+
+	imx->ctrls.exposure = v4l2_ctrl_new_std(hdl, &imx908_ctrl_ops,
+						V4L2_CID_EXPOSURE,
+						IMX908_EXPOSURE_MIN,
+						max_exp,
+						IMX908_EXPOSURE_STEP,
+						max_exp / 2);
+
+	v4l2_ctrl_new_std(hdl, &imx908_ctrl_ops, V4L2_CID_ANALOGUE_GAIN,
+			  IMX908_ANA_GAIN_MIN, IMX908_ANA_GAIN_MAX,
+			  IMX908_ANA_GAIN_STEP, IMX908_ANA_GAIN_DEFAULT);
+
+	/* Set test pattern. Menu (13 entries: Disabled + 12 patterns) */
+	imx->ctrls.test_pattern = v4l2_ctrl_new_std_menu_items(hdl,
+							       &imx908_ctrl_ops,
+							       V4L2_CID_TEST_PATTERN,
+							       ARRAY_SIZE(imx908_tpg_menu) - 1,
+							       0,
+							       0,
+							       imx908_tpg_menu);
+	if (imx->ctrls.test_pattern)
+		imx->ctrls.test_pattern->flags |= V4L2_CTRL_FLAG_EXECUTE_ON_WRITE;
+
+	v4l2_ctrl_new_std(hdl, &imx908_ctrl_ops, V4L2_CID_HFLIP, 0, 1, 1, 0);
+	v4l2_ctrl_new_std(hdl, &imx908_ctrl_ops, V4L2_CID_VFLIP, 0, 1, 1, 0);
+
+	/* Pick up rotation and orientation if defined in overlay */
+	ret = v4l2_fwnode_device_parse(imx->dev, &props);
+	if (ret)
+		return ret;
+
+	ret = v4l2_ctrl_new_fwnode_properties(hdl, &imx908_ctrl_ops, &props);
+	if (ret)
+		return ret;
+
+	if (hdl->error) {
+		ret = hdl->error;
+		v4l2_ctrl_handler_free(hdl);
+		return ret;
+	}
+
+	imx->sd.ctrl_handler = hdl;
+
+	return 0;
+}
+
+/* Confirm the ID of the HW device */
+static int imx908_identify_model(struct imx908 *imx)
+{
+	int ret;
+	int err;
+	u64 val;
+	u16 chip_id;
+
+	/*
+	 * The TYPE_ID registers are not accessible after power-up while
+	 * the device remains in standby. Exit standby and wait for the
+	 * required stabilization period before reading the chip ID.
+	 */
+	ret = cci_write(imx->cci, IMX908_REG_STANDBY, IMX908_STANDBY_CANCEL, NULL);
+	if (ret)
+		return ret;
+
+	msleep(24); /* Regulator stabilization after standby cancel. */
+
+	ret = cci_read(imx->cci, IMX908_REG_TYPE_ID, &val, &err);
+	if (ret || err)
+		return ret ? ret : err;
+
+	chip_id = val;
+	dev_info(imx->dev, "IMX908 chip ID: 0x%04x\n", chip_id);
+
+	if (chip_id != IMX908_CHIP_ID) {
+		dev_err(imx->dev, "Unexpected chip ID 0x%04x (expected 0x%04x)\n",
+			chip_id, IMX908_CHIP_ID);
+		return -ENXIO;
+	}
+
+	/* Set to standby mode */
+	ret = cci_write(imx->cci, IMX908_REG_STANDBY, IMX908_STANDBY_EN, NULL);
+	if (ret)
+		dev_err(imx->dev, "failed to enter standby state: %d\n", ret);
+
+	return 0;
+}
+
+static int imx908_probe(struct i2c_client *client)
+{
+	struct imx908 *imx;
+	int ret;
+
+	/* Allocate Memory */
+	imx = devm_kzalloc(&client->dev, sizeof(*imx), GFP_KERNEL);
+	if (!imx)
+		return dev_err_probe(&client->dev, -ENOMEM,
+				     "failed to allocate IMX908 device structure\n");
+	imx->dev = &client->dev;
+
+	/* Initialize V4L2 subdevice */
+	v4l2_i2c_subdev_init(&imx->sd, client, &imx908_subdev_ops);
+	imx->sd.internal_ops = &imx908_internal_ops;
+
+	/* Register access initialization. Set 2-byte (16-bit) addresses */
+	imx->cci = devm_cci_regmap_init_i2c(client, 16);
+	if (IS_ERR(imx->cci))
+		return dev_err_probe(&client->dev, PTR_ERR(imx->cci),
+				     "CCI regmap init failed\n");
+
+	/* Get mandatory input clock from DT (INCK) */
+	imx->xclk = devm_clk_get(imx->dev, "xclk");
+	if (IS_ERR(imx->xclk))
+		return dev_err_probe(imx->dev, PTR_ERR(imx->xclk), "xclk\n");
+
+	/* Get clock frequency and check against acceptable HW values */
+	imx->xclk_freq = clk_get_rate(imx->xclk);
+	ret = imx908_get_inck_sel(imx);
+	if (ret)
+		return ret;
+
+	/* GPIO reset acquisition */
+	imx->reset_gpio = devm_gpiod_get_optional(imx->dev, "reset",
+						  GPIOD_OUT_HIGH);
+
+	if (IS_ERR(imx->reset_gpio))
+		return dev_err_probe(imx->dev, PTR_ERR(imx->reset_gpio),
+				     "reset gpio\n");
+
+	/* Link to power supplies */
+	ret = imx908_get_regulators(imx);
+	if (ret)
+		return dev_err_probe(&client->dev, ret,
+				     "regulator get failed\n");
+
+	/* Parse Device Tree endpoint */
+	ret = imx908_parse_fwnode(imx);
+	if (ret)
+		return dev_err_probe(&client->dev, ret,
+				     "device tree parse failed\n");
+
+	/* Power on IMX908 image sensor */
+	ret = imx908_power_on(imx);
+	if (ret)
+		return dev_err_probe(&client->dev, ret, "power-on failed\n");
+
+	/* Read IMX908 device ID */
+	ret = imx908_identify_model(imx);
+	if (ret) {
+		dev_err(imx->dev, "failed to identify model: %d\n", ret);
+		goto err_power_off;
+	}
+
+	pm_runtime_set_active(imx->dev);
+	pm_runtime_enable(imx->dev);
+
+	ret = imx908_init_controls(imx);
+	if (ret)
+		goto err_pm_disable;
+
+	/* Initialize V4L2 subdevice, media entity, and pad */
+	imx->sd.flags |= V4L2_SUBDEV_FL_HAS_DEVNODE | V4L2_SUBDEV_FL_HAS_EVENTS;
+	imx->sd.entity.function = MEDIA_ENT_F_CAM_SENSOR;
+	imx->pad.flags = MEDIA_PAD_FL_SOURCE;
+	ret = media_entity_pads_init(&imx->sd.entity, 1, &imx->pad);
+	if (ret)
+		goto err_hdl;
+
+	/* Use ctrl handler mutex for centrally managed subdev state */
+	imx->sd.state_lock = imx->ctrls.handler.lock;
+
+	ret = v4l2_subdev_init_finalize(&imx->sd);
+	if (ret)
+		goto err_entity;
+
+	pm_runtime_set_autosuspend_delay(imx->dev, 1000);
+	pm_runtime_use_autosuspend(imx->dev);
+	pm_runtime_mark_last_busy(imx->dev);
+
+	ret = v4l2_async_register_subdev_sensor(&imx->sd);
+	if (ret)
+		goto err_pm_disable;
+
+	return 0;
+
+err_pm_disable:
+	pm_runtime_disable(imx->dev);
+	pm_runtime_set_suspended(imx->dev);
+	v4l2_subdev_cleanup(&imx->sd);
+err_entity:
+	media_entity_cleanup(&imx->sd.entity);
+err_hdl:
+	v4l2_ctrl_handler_free(imx->sd.ctrl_handler);
+err_power_off:
+	imx908_power_off(imx);
+
+	return ret;
+}
+
+static void imx908_remove(struct i2c_client *client)
+{
+	struct v4l2_subdev *sd = i2c_get_clientdata(client);
+	struct imx908 *imx = to_imx908(sd);
+
+	v4l2_async_unregister_subdev(sd);
+	v4l2_subdev_cleanup(sd);
+	media_entity_cleanup(&sd->entity);
+	v4l2_ctrl_handler_free(sd->ctrl_handler);
+
+	pm_runtime_disable(imx->dev);
+	if (!pm_runtime_status_suspended(imx->dev))
+		imx908_power_off(imx);
+	pm_runtime_set_suspended(imx->dev);
+}
+
+static const struct of_device_id imx908_of_ids[] = {
+	{ .compatible = "sony,imx908" },
+	{ /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, imx908_of_ids);
+
+static struct i2c_driver imx908_i2c_driver = {
+	.driver = {
+		.name           = "imx908",
+		.pm             = pm_ptr(&imx908_pm_ops),
+		.of_match_table = imx908_of_ids,
+	},
+	.probe  = imx908_probe,
+	.remove = imx908_remove,
+};
+module_i2c_driver(imx908_i2c_driver);
+
+MODULE_AUTHOR("Lachlan Michael <Lachlan.Michael@sony.com>");
+MODULE_DESCRIPTION("Sony IMX908 Sensor Driver");
+MODULE_LICENSE("GPL");
-- 
2.47.3


  parent reply	other threads:[~2026-07-30  2:16 UTC|newest]

Thread overview: 16+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-30  2:15 [PATCH 0/2] Add bindings and driver for Sony IMX908 Lachlan Michael
2026-07-30  2:15 ` [PATCH 1/2] media: dt-bindings: imx908: Add Sony IMX908 sensor bindings Lachlan Michael
2026-07-30  3:24   ` Rob Herring (Arm)
2026-07-30  9:06   ` Krzysztof Kozlowski
2026-08-02 23:03     ` Laurent Pinchart
2026-08-03  6:16       ` Krzysztof Kozlowski
2026-08-03 19:18         ` Laurent Pinchart
2026-08-05  9:15         ` Lachlan.Michael
2026-07-30  2:15 ` Lachlan Michael [this message]
2026-07-30  9:09   ` [PATCH 2/2] media: i2c: Add Sony IMX908 image sensor driver Krzysztof Kozlowski
2026-08-05  9:23     ` Lachlan.Michael
2026-08-03  0:53   ` Laurent Pinchart
2026-08-03 17:37     ` Dave Stevenson
2026-08-03 19:12       ` Laurent Pinchart
2026-08-04  1:00       ` Lachlan.Michael
2026-08-05 10:08     ` Lachlan.Michael

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260730021525.166811-3-lachlan.michael@sony.com \
    --to=lachlan.michael@sony.com \
    --cc=Kazumi.A.Sato@sony.com \
    --cc=Kengo.Hayasaka@sony.com \
    --cc=Ryuichi.Tadano@sony.com \
    --cc=Tim.Bird@sony.com \
    --cc=conor+dt@kernel.org \
    --cc=devicetree@vger.kernel.org \
    --cc=hverkuil+cisco@kernel.org \
    --cc=jai.luthra@ideasonboard.com \
    --cc=kieran.bingham@ideasonboard.com \
    --cc=krzk+dt@kernel.org \
    --cc=laurent.pinchart@ideasonboard.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-media@vger.kernel.org \
    --cc=mchehab@kernel.org \
    --cc=robh@kernel.org \
    --cc=sakari.ailus@linux.intel.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®