Devicetree
 help / color / mirror / Atom feed
* [PATCH v3 0/2] media: i2c: add Aptina MT9M113 SoC sensor driver
@ 2026-06-05  7:02 Herman van Hazendonk via B4 Relay
  2026-06-05  7:03 ` [PATCH v3 1/2] dt-bindings: media: i2c: add aptina,mt9m113 Herman van Hazendonk via B4 Relay
  2026-06-05  7:03 ` [PATCH v3 2/2] media: i2c: add Aptina MT9M113 1.3 Mpx SoC sensor driver Herman van Hazendonk via B4 Relay
  0 siblings, 2 replies; 5+ messages in thread
From: Herman van Hazendonk via B4 Relay @ 2026-06-05  7:02 UTC (permalink / raw)
  To: Mauro Carvalho Chehab, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Sakari Ailus
  Cc: linux-media, devicetree, linux-kernel, Laurent Pinchart,
	Hans Verkuil, Konrad Dybcio

v3:
 - Add MAINTAINERS entry for the new driver + binding (patchwork /
   checkpatch warned about the missing entry on v2).
 - No other changes.

v2 (sent 2026-06-05): pure re-send.  v1 was sent on 2026-06-04 but
PATCH 2/2 (the driver itself) was silently dropped by my outbound
SMTP relay during the original send - the cover letter and PATCH 1/2
reached the lists, the driver patch did not.  v2 routed through b4
+ kernel.org so the driver actually landed.

To: Herman van Hazendonk <github.com@herrie.org>
To: Mauro Carvalho Chehab <mchehab@kernel.org>
To: Rob Herring <robh@kernel.org>
To: Krzysztof Kozlowski <krzk+dt@kernel.org>
To: Conor Dooley <conor+dt@kernel.org>
To: Sakari Ailus <sakari.ailus@linux.intel.com>
Cc: linux-media@vger.kernel.org
Cc: devicetree@vger.kernel.org
Cc: linux-kernel@vger.kernel.org

This series adds a V4L2 subdev driver and DT binding for the Aptina
MT9M113, a 1.3 Mpx SoC sensor with an on-chip ISP and a small MCU
running a "SOC1040" firmware that performs auto-exposure, auto white
balance and lens-shading correction.  The sensor talks to the host
over MIPI CSI-2 (1 or 2 data lanes); register access is over I2C
using the Qualcomm CCI helper.

Hardware layout exposed to userspace:

    +-------------------+    +-----------------+
    | Pixel Array (PA)  |--->| IFP / ISP       |--->[CSI-2]
    | sink-less subdev  |    | MCU + pipe      |
    +-------------------+    +-----------------+

PA carries V4L2_CID_PIXEL_RATE and V4L2_CID_LINK_FREQ so that
downstream receivers walking the graph from the CSIPHY (camss does
this) find them on the entity flagged MEDIA_ENT_F_CAM_SENSOR.

V4L2 controls implemented:
  - exposure / gain / hflip / vflip
  - test pattern
  - V4L2_CID_COLORFX (sepia / mono / negative / aqua / vivid)
  - V4L2_CID_POWER_LINE_FREQUENCY (50/60 Hz flicker avoidance)

Tested on a HP TouchPad (APQ8060, MSM8x60 family) with the on-board
front 1.3 Mpx MT9M113.  Probes cleanly, both PA and IFP subdevs
register, csiphy_stream_on() picks up link_freq from PA, and the
preview pipeline up through CAMSS produces frames.  A bounded
3-attempt retry around s_stream(1) handles a residual silicon-level
wedge that occasionally leaves the MCU's SEQ_CMD in an inconsistent
state on cold start.

The DTS patches that enable the sensor on the HP TouchPad will be
sent separately to the ARM/DTS tree.

Signed-off-by: Herman van Hazendonk <github.com@herrie.org>
---
Herman van Hazendonk (2):
      dt-bindings: media: i2c: add aptina,mt9m113
      media: i2c: add Aptina MT9M113 1.3 Mpx SoC sensor driver

 .../bindings/media/i2c/aptina,mt9m113.yaml         |  127 +
 MAINTAINERS                                        |    8 +
 drivers/media/i2c/Kconfig                          |   12 +
 drivers/media/i2c/Makefile                         |    1 +
 drivers/media/i2c/mt9m113.c                        | 2971 ++++++++++++++++++++
 5 files changed, 3119 insertions(+)
---
base-commit: 944125b4c454b58d2fe6e35f1087a932b2050dff
change-id: 20260605-submit-media-mt9m113-5ac3137971fa

Best regards,
-- 
Herman van Hazendonk <github.com@herrie.org>



^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH v3 1/2] dt-bindings: media: i2c: add aptina,mt9m113
  2026-06-05  7:02 [PATCH v3 0/2] media: i2c: add Aptina MT9M113 SoC sensor driver Herman van Hazendonk via B4 Relay
@ 2026-06-05  7:03 ` Herman van Hazendonk via B4 Relay
  2026-06-05  7:11   ` sashiko-bot
  2026-06-05  7:03 ` [PATCH v3 2/2] media: i2c: add Aptina MT9M113 1.3 Mpx SoC sensor driver Herman van Hazendonk via B4 Relay
  1 sibling, 1 reply; 5+ messages in thread
From: Herman van Hazendonk via B4 Relay @ 2026-06-05  7:03 UTC (permalink / raw)
  To: Mauro Carvalho Chehab, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Sakari Ailus
  Cc: linux-media, devicetree, linux-kernel, Laurent Pinchart,
	Hans Verkuil, Konrad Dybcio

From: Herman van Hazendonk <github.com@herrie.org>

Add the binding for the Aptina (now ON Semiconductor) MT9M113 1.3
megapixel SoC image sensor with on-die ISP. The chip is used as the
front (user-facing) camera on the HP TouchPad tablet and connects
to the host SoC over MIPI CSI-2.

The binding describes the chip's i2c address, optional reset and
standby GPIOs, the per-supply regulators (VDD, VDD_IO, VDDA), the
external clock input, and the CSI-2 endpoint pads exposed via the
"port" subnode.

Signed-off-by: Herman van Hazendonk <github.com@herrie.org>
---
 .../bindings/media/i2c/aptina,mt9m113.yaml         | 127 +++++++++++++++++++++
 1 file changed, 127 insertions(+)

diff --git a/Documentation/devicetree/bindings/media/i2c/aptina,mt9m113.yaml b/Documentation/devicetree/bindings/media/i2c/aptina,mt9m113.yaml
new file mode 100644
index 000000000000..7fa7cb2fedf9
--- /dev/null
+++ b/Documentation/devicetree/bindings/media/i2c/aptina,mt9m113.yaml
@@ -0,0 +1,127 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/media/i2c/aptina,mt9m113.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Aptina MT9M113 1.3 Megapixel CMOS Digital Image Sensor
+
+maintainers:
+  - Herman van Hazendonk <github.com@herrie.org>
+
+description: |-
+  The Aptina MT9M113 is a 1.3 Megapixel CMOS digital image sensor with
+  1280x1024 active pixels. It supports dual context operation:
+  Context A (640x480 preview mode with binning) and Context B (1280x1024
+  capture mode). It is programmable through an I2C interface and outputs
+  image data over a 1-lane MIPI CSI-2 connection.
+
+properties:
+  compatible:
+    const: aptina,mt9m113
+
+  reg:
+    description: I2C device address
+    enum:
+      - 0x3c
+      - 0x48
+      - 0x5d
+
+  clocks:
+    description: EXTCLK clock signal (24 MHz typical)
+    maxItems: 1
+
+  vdd-supply:
+    description:
+      Core digital voltage supply, 1.8V
+
+  vddio-supply:
+    description:
+      I/O digital voltage supply, 1.8V or 2.8V
+
+  vaa-supply:
+    description:
+      Analog voltage supply, 2.8V
+
+  reset-gpios:
+    description: |-
+      Reference to the GPIO connected to the RESET_BAR pin, if any (active
+      low).
+
+  powerdown-gpios:
+    description: |-
+      Reference to the GPIO connected to the STANDBY/PWDN pin, if any (active
+      high). When asserted, the sensor enters low-power standby mode.
+
+  port:
+    $ref: /schemas/graph.yaml#/$defs/port-base
+    additionalProperties: false
+
+    properties:
+      endpoint:
+        $ref: /schemas/media/video-interfaces.yaml#
+        unevaluatedProperties: false
+
+        properties:
+          bus-type:
+            const: 4
+            description: MIPI CSI-2 D-PHY
+
+          link-frequencies: true
+          remote-endpoint: true
+          data-lanes:
+            minItems: 1
+            maxItems: 1
+
+        required:
+          - bus-type
+          - link-frequencies
+          - data-lanes
+
+    required:
+      - endpoint
+
+required:
+  - compatible
+  - reg
+  - clocks
+  - vdd-supply
+  - vddio-supply
+  - vaa-supply
+  - port
+
+additionalProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/gpio/gpio.h>
+    #include <dt-bindings/media/video-interfaces.h>
+
+    i2c0 {
+        #address-cells = <1>;
+        #size-cells = <0>;
+
+        camera@3c {
+            compatible = "aptina,mt9m113";
+            reg = <0x3c>;
+
+            clocks = <&mmcc 48>;
+
+            reset-gpios = <&pm8058_gpio 29 GPIO_ACTIVE_LOW>;
+            powerdown-gpios = <&pm8058_gpio 30 GPIO_ACTIVE_HIGH>;
+
+            vddio-supply = <&pm8058_l15>;
+            vdd-supply = <&pm8058_l15>;
+            vaa-supply = <&pm8058_l14>;
+
+            port {
+                mt9m113_ep: endpoint {
+                    bus-type = <MEDIA_BUS_TYPE_CSI2_DPHY>;
+                    link-frequencies = /bits/ 64 <384000000>;
+                    data-lanes = <1>;
+                    remote-endpoint = <&csiphy1_ep>;
+                };
+            };
+        };
+    };
+...

-- 
2.43.0



^ permalink raw reply related	[flat|nested] 5+ messages in thread

* [PATCH v3 2/2] media: i2c: add Aptina MT9M113 1.3 Mpx SoC sensor driver
  2026-06-05  7:02 [PATCH v3 0/2] media: i2c: add Aptina MT9M113 SoC sensor driver Herman van Hazendonk via B4 Relay
  2026-06-05  7:03 ` [PATCH v3 1/2] dt-bindings: media: i2c: add aptina,mt9m113 Herman van Hazendonk via B4 Relay
@ 2026-06-05  7:03 ` Herman van Hazendonk via B4 Relay
  2026-06-05  7:16   ` sashiko-bot
  1 sibling, 1 reply; 5+ messages in thread
From: Herman van Hazendonk via B4 Relay @ 2026-06-05  7:03 UTC (permalink / raw)
  To: Mauro Carvalho Chehab, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Sakari Ailus
  Cc: linux-media, devicetree, linux-kernel, Laurent Pinchart,
	Hans Verkuil, Konrad Dybcio

From: Herman van Hazendonk <github.com@herrie.org>

Add a V4L2 subdev driver for the Aptina MT9M113 1.3 Megapixel SoC
image sensor (1280x1024 active pixel array) with on-chip ISP. The
sensor exposes:

  - dual context operation: Context A (640x480 preview, binned) and
    Context B (1280x1024 capture) selectable at runtime;
  - single-lane MIPI CSI-2 output, YUV422 in UYVY or YUYV byte order
    (the byte order is selected via the chroma/luma swap bit in
    MODE_OUTPUT_FORMAT_{A,B});
  - I2C MCU-indirect register access through the 0x098C/0x0990 page
    indirection pair, in addition to the directly addressable
    register space;
  - V4L2 controls: H/V flip, color effects (none / mono / sepia /
    negative / solarization), power-line frequency, saturation,
    manual or auto exposure, analog gain, auto white balance, and
    a four-entry test pattern selector;
  - a 24 MHz EXTCLK and 1.8/2.8 V supplies.

The MT9M113 MCU intermittently wedges on stream start (SEQ_CMD
stuck, SEQ_STATE never reaching preview/capture; only a full power
cycle recovers it). The driver works around this in two layers:
runtime-PM resume always performs a full power-cycle plus init-table
replay - matching the legacy vendor kernel's per-open behaviour -
so a wedged MCU is recovered each session; and the s_stream(1) path
retries a bounded number of times with a runtime-PM power cycle
between attempts as fallback insurance for the residual failure
rate.

Found on the HP TouchPad (Tenderloin) as the front-facing camera
and on a number of other 1280x1024 mobile/embedded boards.

Signed-off-by: Herman van Hazendonk <github.com@herrie.org>
---
 MAINTAINERS                 |    8 +
 drivers/media/i2c/Kconfig   |   12 +
 drivers/media/i2c/Makefile  |    1 +
 drivers/media/i2c/mt9m113.c | 2971 +++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 2992 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index 2fb1c75afd16..c1ca196180b0 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -18165,6 +18165,14 @@ L:	linux-mtd@lists.infradead.org
 S:	Maintained
 F:	drivers/mtd/devices/docg3*
 
+MT9M113 APTINA SENSOR DRIVER
+M:	Herman van Hazendonk <github.com@herrie.org>
+L:	linux-media@vger.kernel.org
+S:	Maintained
+T:	git git://linuxtv.org/media.git
+F:	Documentation/devicetree/bindings/media/i2c/aptina,mt9m113.yaml
+F:	drivers/media/i2c/mt9m113.c
+
 MT9M114 ONSEMI SENSOR DRIVER
 M:	Laurent Pinchart <laurent.pinchart@ideasonboard.com>
 L:	linux-media@vger.kernel.org
diff --git a/drivers/media/i2c/Kconfig b/drivers/media/i2c/Kconfig
index 8f2ba4121586..77c2e432578f 100644
--- a/drivers/media/i2c/Kconfig
+++ b/drivers/media/i2c/Kconfig
@@ -302,6 +302,18 @@ config VIDEO_MT9M111
 	  This driver supports MT9M111, MT9M112 and MT9M131 cameras from
 	  Micron/Aptina
 
+config VIDEO_MT9M113
+	tristate "Aptina MT9M113 sensor support"
+	select V4L2_CCI_I2C
+	help
+	  This is a Video4Linux2 sensor driver for the Aptina MT9M113
+	  1.3 Megapixel CMOS camera sensor. The MT9M113 has dual context
+	  support: Context A (640x480 preview) and Context B (1280x1024
+	  capture).
+
+	  To compile this driver as a module, choose M here: the
+	  module will be called mt9m113.
+
 config VIDEO_MT9M114
 	tristate "onsemi MT9M114 sensor support"
 	select V4L2_CCI_I2C
diff --git a/drivers/media/i2c/Makefile b/drivers/media/i2c/Makefile
index 90b276a7417a..f2ba184dd0e4 100644
--- a/drivers/media/i2c/Makefile
+++ b/drivers/media/i2c/Makefile
@@ -76,6 +76,7 @@ obj-$(CONFIG_VIDEO_ML86V7667) += ml86v7667.o
 obj-$(CONFIG_VIDEO_MSP3400) += msp3400.o
 obj-$(CONFIG_VIDEO_MT9M001) += mt9m001.o
 obj-$(CONFIG_VIDEO_MT9M111) += mt9m111.o
+obj-$(CONFIG_VIDEO_MT9M113) += mt9m113.o
 obj-$(CONFIG_VIDEO_MT9M114) += mt9m114.o
 obj-$(CONFIG_VIDEO_MT9P031) += mt9p031.o
 obj-$(CONFIG_VIDEO_MT9T112) += mt9t112.o
diff --git a/drivers/media/i2c/mt9m113.c b/drivers/media/i2c/mt9m113.c
new file mode 100644
index 000000000000..7a4a8a50822d
--- /dev/null
+++ b/drivers/media/i2c/mt9m113.c
@@ -0,0 +1,2971 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * mt9m113.c Aptina MT9M113 sensor driver
+ *
+ * Copyright (C) 2024 Herman van Hazendonk <github.com@herrie.org>
+ *
+ * MT9M113 is a 1.3MP SOC sensor with dual context support:
+ *   Context A: 640x480 preview mode (binned)
+ *   Context B: 1280x1024 capture mode (full resolution)
+ *
+ * Ported from legacy vendor kernel mt9m113.c/mt9m113_reg.c.
+ * Uses MCU indirect access (0x098C/0x0990) for configuration.
+ */
+
+#include <linux/clk.h>
+#include <linux/delay.h>
+#include <linux/errno.h>
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/math64.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/pm_runtime.h>
+#include <linux/property.h>
+#include <linux/regmap.h>
+#include <linux/regulator/consumer.h>
+#include <linux/types.h>
+#include <linux/videodev2.h>
+
+#include <media/v4l2-async.h>
+#include <media/v4l2-cci.h>
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-fwnode.h>
+#include <media/v4l2-mediabus.h>
+#include <media/v4l2-subdev.h>
+
+/* Delay before enabling MIPI output, allowing CSIPHY to stabilize. */
+#define MT9M113_PRE_MIPI_DELAY_MS	10
+
+/*
+ * The MT9M113 MCU intermittently wedges on stream start (SEQ_CMD stuck,
+ * 0xa103 timeout, SEQ_STATE never reaching preview 0x04 / capture 0x07).
+ * The failures cluster at the start of a run and then clear, so a bounded
+ * retry that power-cycles the sensor between attempts reliably gets the
+ * stream up.  3 attempts is enough in practice (with the per-session
+ * runtime-PM re-init as the primary fix); a larger cap would stack
+ * power-cycle + ~600-entry init-table replay into a STREAMON latency
+ * userspace clients treat as a hang.  Total attempts (1 initial + retries).
+ */
+#define MT9M113_STREAM_START_RETRIES	3
+
+/* MT9M113 chip ID */
+#define MT9M113_CHIP_ID				CCI_REG16(0x0000)
+#define MT9M113_CHIP_ID_VALUE			0x2480
+
+/* Sysctl registers */
+#define MT9M113_PLL_DIVIDERS			CCI_REG16(0x0010)
+#define MT9M113_PLL_P_DIVIDERS			CCI_REG16(0x0012)
+#define MT9M113_PLL_CONTROL			CCI_REG16(0x0014)
+#define MT9M113_CLOCKS_CONTROL			CCI_REG16(0x0016)
+#define MT9M113_STANDBY_CONTROL			CCI_REG16(0x0018)
+#define MT9M113_STANDBY_CONTROL_ACTIVE		0x0028	/* MCU running, not in standby */
+#define MT9M113_RESET_AND_MISC_CONTROL		CCI_REG16(0x001a)
+#define MT9M113_RESET_SOC			BIT(0)
+#define MT9M113_MCU_BOOT_MODE			CCI_REG16(0x001c)
+
+/* MCU indirect access registers */
+#define MT9M113_MCU_ADDRESS			CCI_REG16(0x098c)
+#define MT9M113_MCU_DATA			CCI_REG16(0x0990)
+
+/* MCU variable addresses */
+#define MT9M113_SEQ_CMD				0xa103
+#define MT9M113_SEQ_CMD_RUN			0x0001
+#define MT9M113_SEQ_CMD_CAPTURE			0x0002
+#define MT9M113_SEQ_CMD_REFRESH			0x0005
+#define MT9M113_SEQ_CMD_REFRESH_MODE		0x0006
+#define MT9M113_SEQ_STATE			0xa104
+#define MT9M113_SEQ_STATE_PREVIEW		0x04	/* stable Context A preview */
+#define MT9M113_SEQ_STATE_CAPTURE		0x07	/* Context B capture */
+#define MT9M113_SEQ_CAP_MODE			0xa115
+#define MT9M113_SEQ_CAP_MODE_PREVIEW		0x0030	/* continuous preview */
+#define MT9M113_SEQ_CAP_MODE_VIDEO		0x0002	/* stay in Context B */
+
+/* Mode Output dimension/format MCU variables (Driver ID 7) */
+#define MT9M113_MODE_OUTPUT_WIDTH_A		0x2703
+#define MT9M113_MODE_OUTPUT_HEIGHT_A		0x2705
+#define MT9M113_MODE_OUTPUT_WIDTH_B		0x2707
+#define MT9M113_MODE_OUTPUT_HEIGHT_B		0x2709
+#define MT9M113_MODE_OUTPUT_FORMAT_A		0x2755
+#define MT9M113_MODE_OUTPUT_FORMAT_B		0x2757
+#define MT9M113_MODE_OUTPUT_FORMAT_YUV		0x0000
+/*
+ * YUV byte ordering (datasheet table 31 "mode_output_format_a/b"):
+ *   default (0x0000)   = Cb Y0 Cr Y1 = UYVY
+ *   SWAP_LUMA_CHROMA   = Y0 Cb Y1 Cr = YUYV
+ */
+#define MT9M113_MODE_OUTPUT_FORMAT_SWAP_LUMA_CHROMA	BIT(1)
+
+/* Special effects MCU variables */
+#define MT9M113_MODE_SPEC_EFFECTS_A		0x2759
+#define MT9M113_MODE_SPEC_EFFECTS_B		0x275b
+#define MT9M113_SPEC_EFFECTS_DEFAULT		0x6440
+#define MT9M113_SPEC_EFFECTS_MASK		0x0007
+#define MT9M113_SPEC_EFFECTS_NONE		0x0000
+#define MT9M113_SPEC_EFFECTS_MONOCHROME		0x0001
+#define MT9M113_SPEC_EFFECTS_SEPIA		0x0002
+#define MT9M113_SPEC_EFFECTS_NEGATIVE		0x0003
+#define MT9M113_SPEC_EFFECTS_SOLARIZE		0x0004
+
+/* Sensor Read Mode MCU variables (for flip/mirror control) */
+#define MT9M113_SENSOR_READ_MODE_A		0x2717
+#define MT9M113_SENSOR_READ_MODE_B		0x272d
+#define MT9M113_SENSOR_READ_MODE_HMIRROR	BIT(0)
+#define MT9M113_SENSOR_READ_MODE_VMIRROR	BIT(1)
+
+/*
+ * Context A/B sensor configuration MCU variables
+ * These control the sensor readout window and must be re-applied when
+ * switching contexts to ensure correct line counts.
+ */
+#define MT9M113_MODE_SENSOR_ROW_START_A		0x270D
+#define MT9M113_MODE_SENSOR_COL_START_A		0x270F
+#define MT9M113_MODE_SENSOR_ROW_END_A		0x2711
+#define MT9M113_MODE_SENSOR_COL_END_A		0x2713
+#define MT9M113_MODE_SENSOR_ROW_SPEED_A		0x2715
+#define MT9M113_MODE_SENSOR_FRAME_LENGTH_A	0x271F
+#define MT9M113_MODE_SENSOR_LINE_LENGTH_PCK_A	0x2721
+
+#define MT9M113_MODE_SENSOR_ROW_START_B		0x2723
+#define MT9M113_MODE_SENSOR_COL_START_B		0x2725
+#define MT9M113_MODE_SENSOR_ROW_END_B		0x2727
+#define MT9M113_MODE_SENSOR_COL_END_B		0x2729
+#define MT9M113_MODE_SENSOR_ROW_SPEED_B		0x272B
+#define MT9M113_MODE_SENSOR_FRAME_LENGTH_B	0x2735
+#define MT9M113_MODE_SENSOR_LINE_LENGTH_PCK_B	0x2737
+
+/* Context A sensor config values (from legacy vendor driver, 640x480 binned) */
+#define MT9M113_CONTEXT_A_ROW_START		0x0000
+#define MT9M113_CONTEXT_A_COL_START		0x0000
+#define MT9M113_CONTEXT_A_ROW_END		0x03CD	/* 973 */
+#define MT9M113_CONTEXT_A_COL_END		0x050D	/* 1293 */
+#define MT9M113_CONTEXT_A_ROW_SPEED		0x2111
+#define MT9M113_CONTEXT_A_READ_MODE		0x046C	/* Binning enabled */
+#define MT9M113_CONTEXT_A_FRAME_LENGTH		0x032E	/* 814 lines (incl vblank) */
+#define MT9M113_CONTEXT_A_LINE_LENGTH_PCK	0x04CC	/* 1228 pixclks/line */
+
+/* Context B sensor config values (from legacy vendor driver, 1280x1024 full res) */
+#define MT9M113_CONTEXT_B_ROW_START		0x0004
+#define MT9M113_CONTEXT_B_COL_START		0x0004
+#define MT9M113_CONTEXT_B_ROW_END		0x040B	/* 1035 */
+#define MT9M113_CONTEXT_B_COL_END		0x050B	/* 1291 */
+#define MT9M113_CONTEXT_B_ROW_SPEED		0x2111
+#define MT9M113_CONTEXT_B_READ_MODE		0x0024	/* Full resolution */
+#define MT9M113_CONTEXT_B_FRAME_LENGTH		0x0559	/* 1369 lines (incl vblank) */
+#define MT9M113_CONTEXT_B_LINE_LENGTH_PCK	0x0722	/* 1826 pixclks/line */
+
+/* Auto Exposure MCU variables (for preview vs snapshot optimization) */
+#define MT9M113_AE_MAX_INDEX			0xa20c
+#define MT9M113_AE_MAX_VIRTGAIN			0xa20e
+#define MT9M113_AE_MAX_DGAIN_AE1		0xa21a
+#define MT9M113_AE_JUMP_DIVISOR			0xa21c
+#define MT9M113_AE_SKIP_FRAMES			0xa21e
+
+/* Flicker detection MCU variable */
+#define MT9M113_FD_MODE				0xa404
+#define MT9M113_FD_MODE_DISABLED		0x00
+#define MT9M113_FD_MODE_50HZ			0x01
+#define MT9M113_FD_MODE_60HZ			0x02
+#define MT9M113_FD_MODE_AUTO			0x03
+
+/* AWB/Color MCU variables */
+#define MT9M113_AWB_SATURATION			0xa354
+#define MT9M113_AWB_MODE			0xa34a
+
+/* Test pattern MCU variables (mode_common_mode_settings) */
+#define MT9M113_CAM_MODE_SELECT			0xc84c
+#define MT9M113_CAM_MODE_SELECT_NORMAL		0x00
+#define MT9M113_CAM_MODE_SELECT_TEST_PATTERN	0x02
+#define MT9M113_CAM_MODE_TEST_PATTERN_SELECT	0xc84d
+#define MT9M113_TEST_PATTERN_SOLID_COLOR	0x01
+#define MT9M113_TEST_PATTERN_COLOR_BARS		0x04
+#define MT9M113_TEST_PATTERN_FADE_TO_GRAY	0x08
+
+/* Double buffer control register */
+#define MT9M113_DOUBLE_BUFFER_CONTROL		CCI_REG16(0x0248)
+#define MT9M113_DOUBLE_BUFFER_SUSPEND		BIT(15)
+
+/* Auto Exposure MCU variables */
+#define MT9M113_AE_GATE				0xa207
+#define MT9M113_AE_GATE_ENABLE			0x0000
+#define MT9M113_AE_GATE_DISABLE			0x00FF
+
+/* Sensor core exposure/gain registers */
+#define MT9M113_COARSE_IT_TIME_A		CCI_REG16(0x3012)
+#define MT9M113_COARSE_IT_TIME_B		CCI_REG16(0x3014)
+#define MT9M113_ANALOG_GAIN			CCI_REG16(0x3028)
+
+/* Sensor core registers */
+#define MT9M113_RESET_REGISTER			CCI_REG16(0x301a)
+#define MT9M113_RESET_REG_STREAMING		0x120C
+#define MT9M113_OFIFO_CONTROL_STATUS		CCI_REG16(0x321c)
+#define MT9M113_OFIFO_BYPASS			0x0003	/* FIFO bypass (YUV path) */
+
+/* Color pipeline (ISP) control */
+#define MT9M113_COLOR_PIPELINE_CONTROL		CCI_REG16(0x3210)
+#define MT9M113_COLOR_PIPELINE_DEFAULT		0x01b8	/* scaler+gamma+ap+shading */
+
+/* OUTPUT_CONTROL register (0x3400) - MIPI control */
+#define MT9M113_OUTPUT_CONTROL			CCI_REG16(0x3400)
+#define MT9M113_OUTPUT_CONTROL_MIPI_ENABLE	0x7A08	/* YUV422 dt=0x1E */
+
+/* CUSTOM_SHORT_PKT register */
+#define MT9M113_CUSTOM_SHORT_PKT		CCI_REG16(0x3404)
+#define MT9M113_CUSTOM_SHORT_PKT_FRAME_CNT_EN	0x0080
+
+/* Pixel array dimensions */
+#define MT9M113_PIXEL_ARRAY_WIDTH		1296U
+#define MT9M113_PIXEL_ARRAY_HEIGHT		1040U
+
+/* -----------------------------------------------------------------------------
+ * Data Structures
+ */
+
+struct mt9m113_format_info {
+	u32 code;
+};
+
+struct mt9m113 {
+	struct i2c_client *client;
+	struct regmap *regmap;
+
+	struct clk *clk;
+	struct gpio_desc *reset;
+	struct gpio_desc *powerdown;
+	struct regulator_bulk_data supplies[3];
+	struct v4l2_fwnode_endpoint bus_cfg;
+
+	unsigned int pixrate;
+	s64 link_freq;
+	bool streaming;
+	bool test_pattern_active;
+
+	/* Pixel Array sub-device */
+	struct {
+		struct v4l2_subdev sd;
+		struct media_pad pad;
+		struct v4l2_ctrl_handler hdl;
+	} pa;
+
+	/* Image Flow Processor sub-device */
+	struct {
+		struct v4l2_subdev sd;
+		struct media_pad pads[2];
+
+		struct v4l2_ctrl_handler hdl;
+	} ifp;
+};
+
+/* -----------------------------------------------------------------------------
+ * Formats
+ */
+
+/*
+ * Source-pad output formats.  Only YUV422 is exposed: MT9M113_OUTPUT_CONTROL
+ * advertises the YUV422 CSI-2 data type (0x1E) unconditionally on the wire,
+ * and adding RGB565 would require also driving the matching CSI-2 dt (0x22)
+ * out of OUTPUT_CONTROL — verifying the exact bit layout for that needs the
+ * sensor datasheet and a hardware re-test, so RGB is deferred to a follow-up.
+ */
+static const struct mt9m113_format_info mt9m113_format_infos[] = {
+	{ .code = MEDIA_BUS_FMT_UYVY8_1X16 },
+	{ .code = MEDIA_BUS_FMT_YUYV8_1X16 },
+};
+
+static const struct mt9m113_format_info *
+mt9m113_format_info(u32 code)
+{
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(mt9m113_format_infos); i++) {
+		if (mt9m113_format_infos[i].code == code)
+			return &mt9m113_format_infos[i];
+	}
+
+	return &mt9m113_format_infos[0];
+}
+
+/*
+ * Map a V4L2 mbus code to the value programmed into MODE_OUTPUT_FORMAT_{A,B}.
+ * Driving the chroma/luma swap bit is what differentiates UYVY from YUYV;
+ * without it both codes would emit the same bytes on the MIPI bus.
+ */
+static u16 mt9m113_format_val(u32 code)
+{
+	switch (code) {
+	case MEDIA_BUS_FMT_YUYV8_1X16:
+		return MT9M113_MODE_OUTPUT_FORMAT_YUV |
+		       MT9M113_MODE_OUTPUT_FORMAT_SWAP_LUMA_CHROMA;
+	case MEDIA_BUS_FMT_UYVY8_1X16:
+	default:
+		return MT9M113_MODE_OUTPUT_FORMAT_YUV;
+	}
+}
+
+/* -----------------------------------------------------------------------------
+ * MCU Variable Access Helpers
+ */
+
+static int mt9m113_write_mcu_var(struct mt9m113 *sensor, u16 addr, u16 value)
+{
+	int ret = 0;
+
+	cci_write(sensor->regmap, MT9M113_MCU_ADDRESS, addr, &ret);
+	cci_write(sensor->regmap, MT9M113_MCU_DATA, value, &ret);
+	return ret;
+}
+
+static int mt9m113_read_mcu_var(struct mt9m113 *sensor, u16 addr, u64 *value)
+{
+	int ret;
+
+	ret = cci_write(sensor->regmap, MT9M113_MCU_ADDRESS, addr, NULL);
+	if (ret)
+		return ret;
+	return cci_read(sensor->regmap, MT9M113_MCU_DATA, value, NULL);
+}
+
+static int mt9m113_poll_mcu_var(struct mt9m113 *sensor, u16 addr,
+				u16 expected, unsigned int timeout_ms)
+{
+	unsigned int i;
+	u64 value = 0;
+	int ret;
+
+	for (i = 0; i < timeout_ms / 10; i++) {
+		ret = mt9m113_read_mcu_var(sensor, addr, &value);
+		if (ret < 0)
+			return ret;
+		if (value == expected)
+			return 0;
+		usleep_range(10000, 11000);
+	}
+
+	dev_err(&sensor->client->dev,
+		"MCU var 0x%04x timeout (got 0x%llx, expected 0x%04x)\n",
+		addr, value, expected);
+	return -ETIMEDOUT;
+}
+
+/*
+ * Ensure MCU sequencer is idle before issuing a new command.
+ * This prevents cascading failures where a timed-out command
+ * causes subsequent commands to also fail.
+ */
+static int mt9m113_seq_cmd_ready(struct mt9m113 *sensor)
+{
+	u64 seq_cmd;
+	int ret;
+
+	ret = mt9m113_read_mcu_var(sensor, MT9M113_SEQ_CMD, &seq_cmd);
+	if (ret < 0)
+		return ret;
+
+	if (seq_cmd == 0)
+		return 0;  /* Already idle */
+
+	/*
+	 * A previous command is still pending. This can happen if:
+	 * 1. Previous streaming start failed mid-way
+	 * 2. The MCU is slow to process a command
+	 *
+	 * Wait up to 2 seconds for it to complete. This is longer than
+	 * normal command timeouts to handle recovery scenarios.
+	 */
+	dev_dbg(&sensor->client->dev,
+		 "MT9M113: SEQ_CMD=0x%llx, waiting for idle\n", seq_cmd);
+
+	return mt9m113_poll_mcu_var(sensor, MT9M113_SEQ_CMD, 0x0000, 2000);
+}
+
+/*
+ * mt9m113_refresh - Issue REFRESH_MODE + REFRESH sequence
+ *
+ * Per datasheet: "It is recommended that refresh and refresh mode commands
+ * always be run together, and that refresh mode should be issued BEFORE
+ * the refresh command."
+ */
+static int mt9m113_refresh(struct mt9m113 *sensor)
+{
+	int ret;
+
+	ret = mt9m113_write_mcu_var(sensor, MT9M113_SEQ_CMD,
+				    MT9M113_SEQ_CMD_REFRESH_MODE);
+	if (ret)
+		return ret;
+
+	ret = mt9m113_poll_mcu_var(sensor, MT9M113_SEQ_CMD, 0x0000, 500);
+	if (ret < 0)
+		dev_warn(&sensor->client->dev, "MT9M113: REFRESH_MODE timeout\n");
+
+	ret = mt9m113_write_mcu_var(sensor, MT9M113_SEQ_CMD,
+				    MT9M113_SEQ_CMD_REFRESH);
+	if (ret)
+		return ret;
+
+	ret = mt9m113_poll_mcu_var(sensor, MT9M113_SEQ_CMD, 0x0000, 500);
+	if (ret < 0)
+		dev_warn(&sensor->client->dev, "MT9M113: REFRESH timeout\n");
+
+	return 0;
+}
+
+/*
+ * Re-program SENSOR_READ_MODE_{A,B} for the context geometry while keeping
+ * the live HFLIP/VFLIP bits, which the V4L2 control handler has already
+ * applied via s_ctrl().  Writing CONTEXT_X_READ_MODE blindly would clear
+ * mirror bits 0/1 and silently undo the user's flip on first stream-start.
+ */
+static int mt9m113_apply_read_mode(struct mt9m113 *sensor, u16 reg, u16 base)
+{
+	const u16 mirror_mask = MT9M113_SENSOR_READ_MODE_HMIRROR |
+				MT9M113_SENSOR_READ_MODE_VMIRROR;
+	u64 cur = 0;
+	int ret;
+
+	ret = mt9m113_read_mcu_var(sensor, reg, &cur);
+	if (ret)
+		return ret;
+
+	return mt9m113_write_mcu_var(sensor, reg,
+				     (base & ~mirror_mask) |
+				     ((u16)cur & mirror_mask));
+}
+
+/**
+ * mt9m113_configure_sensor_context - Configure sensor readout parameters
+ * @sensor: MT9M113 sensor device
+ * @use_context_b: true for Context B (1280x1024), false for Context A (640x480)
+ *
+ * Re-applies the sensor configuration registers for the selected context.
+ * This is necessary because the MT9M113's MCU variables can become corrupted
+ * or stale after switching between contexts. Without explicitly re-writing
+ * these values, Context A may use incorrect row_end and read_mode values
+ * after running Context B, resulting in wrong line counts and corrupted images.
+ *
+ * Must be called before mt9m113_refresh() to ensure the new values take effect.
+ */
+static int mt9m113_configure_sensor_context(struct mt9m113 *sensor,
+					    bool use_context_b)
+{
+	struct device *dev = &sensor->client->dev;
+	int ret;
+
+	if (use_context_b) {
+		/* Context B: 1280x1024 full resolution */
+		dev_dbg(dev, "MT9M113: Configuring Context B sensor params\n");
+
+		ret = mt9m113_write_mcu_var(sensor,
+					    MT9M113_MODE_SENSOR_ROW_START_B,
+					    MT9M113_CONTEXT_B_ROW_START);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor,
+					    MT9M113_MODE_SENSOR_COL_START_B,
+					    MT9M113_CONTEXT_B_COL_START);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor,
+					    MT9M113_MODE_SENSOR_ROW_END_B,
+					    MT9M113_CONTEXT_B_ROW_END);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor,
+					    MT9M113_MODE_SENSOR_COL_END_B,
+					    MT9M113_CONTEXT_B_COL_END);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor,
+					    MT9M113_MODE_SENSOR_ROW_SPEED_B,
+					    MT9M113_CONTEXT_B_ROW_SPEED);
+		if (ret)
+			return ret;
+		ret = mt9m113_apply_read_mode(sensor,
+					      MT9M113_SENSOR_READ_MODE_B,
+					      MT9M113_CONTEXT_B_READ_MODE);
+		if (ret)
+			return ret;
+		/*
+		 * Frame/line length define the total frame period (active +
+		 * blanking). They must be re-applied with the rest of the
+		 * context geometry: the VFE CAMIF counts lines to find frame
+		 * boundaries (no MIPI FE short packet), so a stale period makes
+		 * the captured image walk vertically frame-over-frame.
+		 */
+		ret = mt9m113_write_mcu_var(sensor,
+					    MT9M113_MODE_SENSOR_FRAME_LENGTH_B,
+					    MT9M113_CONTEXT_B_FRAME_LENGTH);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor,
+					    MT9M113_MODE_SENSOR_LINE_LENGTH_PCK_B,
+					    MT9M113_CONTEXT_B_LINE_LENGTH_PCK);
+		if (ret)
+			return ret;
+	} else {
+		/* Context A: 640x480 binned preview */
+		dev_dbg(dev, "MT9M113: Configuring Context A sensor params\n");
+
+		ret = mt9m113_write_mcu_var(sensor,
+					    MT9M113_MODE_SENSOR_ROW_START_A,
+					    MT9M113_CONTEXT_A_ROW_START);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor,
+					    MT9M113_MODE_SENSOR_COL_START_A,
+					    MT9M113_CONTEXT_A_COL_START);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor,
+					    MT9M113_MODE_SENSOR_ROW_END_A,
+					    MT9M113_CONTEXT_A_ROW_END);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor,
+					    MT9M113_MODE_SENSOR_COL_END_A,
+					    MT9M113_CONTEXT_A_COL_END);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor,
+					    MT9M113_MODE_SENSOR_ROW_SPEED_A,
+					    MT9M113_CONTEXT_A_ROW_SPEED);
+		if (ret)
+			return ret;
+		ret = mt9m113_apply_read_mode(sensor,
+					      MT9M113_SENSOR_READ_MODE_A,
+					      MT9M113_CONTEXT_A_READ_MODE);
+		if (ret)
+			return ret;
+		/* See Context B above: frame period must track the geometry. */
+		ret = mt9m113_write_mcu_var(sensor,
+					    MT9M113_MODE_SENSOR_FRAME_LENGTH_A,
+					    MT9M113_CONTEXT_A_FRAME_LENGTH);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor,
+					    MT9M113_MODE_SENSOR_LINE_LENGTH_PCK_A,
+					    MT9M113_CONTEXT_A_LINE_LENGTH_PCK);
+		if (ret)
+			return ret;
+	}
+
+	return 0;
+}
+
+/* -----------------------------------------------------------------------------
+ * Double Buffer Control
+ *
+ * The MT9M113 uses double buffering for context parameters. Setting bit 15
+ * of register 0x0248 suspends updates from shadow to active registers,
+ * allowing atomic multi-register configuration changes.
+ */
+
+static int mt9m113_double_buffer_suspend(struct mt9m113 *sensor)
+{
+	u64 value;
+	int ret;
+
+	ret = cci_read(sensor->regmap, MT9M113_DOUBLE_BUFFER_CONTROL, &value, NULL);
+	if (ret)
+		return ret;
+
+	value |= MT9M113_DOUBLE_BUFFER_SUSPEND;
+	return cci_write(sensor->regmap, MT9M113_DOUBLE_BUFFER_CONTROL, value, NULL);
+}
+
+static int mt9m113_double_buffer_resume(struct mt9m113 *sensor)
+{
+	u64 value;
+	int ret;
+
+	ret = cci_read(sensor->regmap, MT9M113_DOUBLE_BUFFER_CONTROL, &value, NULL);
+	if (ret)
+		return ret;
+
+	value &= ~MT9M113_DOUBLE_BUFFER_SUSPEND;
+	return cci_write(sensor->regmap, MT9M113_DOUBLE_BUFFER_CONTROL, value, NULL);
+}
+
+/* -----------------------------------------------------------------------------
+ * Initialization Table (from legacy vendor kernel mt9m113_reg.c)
+ */
+
+struct mt9m113_reg_entry {
+	u16 reg;
+	u16 value;
+	u16 delay_ms;
+};
+
+static const struct mt9m113_reg_entry mt9m113_init_table[] = {
+	/* OFIFO control */
+	{ 0x321C, 0x0003, 0 },
+
+	/* Context A output (640x480 preview) */
+	{ 0x098C, 0x2703, 0 },
+	{ 0x0990, 0x0280, 0 },
+	{ 0x098C, 0x2705, 0 },
+	{ 0x0990, 0x01E0, 0 },
+
+	/* Context B output (1280x1024 capture) */
+	{ 0x098C, 0x2707, 0 },
+	{ 0x0990, 0x0500, 0 },
+	{ 0x098C, 0x2709, 0 },
+	{ 0x0990, 0x0400, 0 },
+
+	/* Context A sensor configuration */
+	{ 0x098C, 0x270D, 0 },
+	{ 0x0990, 0x0000, 0 },
+	{ 0x098C, 0x270F, 0 },
+	{ 0x0990, 0x0000, 0 },
+	{ 0x098C, 0x2711, 0 },		/* MODE_SENSOR_ROW_END_A */
+	{ 0x0990, 0x03CD, 0 },		/* 973 (from legacy vendor driver) */
+	{ 0x098C, 0x2713, 0 },
+	{ 0x0990, 0x050D, 0 },
+	{ 0x098C, 0x2715, 0 },
+	{ 0x0990, 0x2111, 0 },
+	{ 0x098C, 0x2717, 0 },
+	{ 0x0990, 0x046C, 0 },
+	{ 0x098C, 0x2719, 0 },
+	{ 0x0990, 0x00AC, 0 },
+	{ 0x098C, 0x271B, 0 },
+	{ 0x0990, 0x01F1, 0 },
+	{ 0x098C, 0x271D, 0 },
+	{ 0x0990, 0x013F, 0 },
+	{ 0x098C, 0x271F, 0 },
+	{ 0x0990, 0x032E, 0 },
+	{ 0x098C, 0x2721, 0 },
+	{ 0x0990, 0x04CC, 0 },
+
+	/* Context B sensor configuration */
+	{ 0x098C, 0x2723, 0 },
+	{ 0x0990, 0x0004, 0 },
+	{ 0x098C, 0x2725, 0 },
+	{ 0x0990, 0x0004, 0 },
+	{ 0x098C, 0x2727, 0 },
+	{ 0x0990, 0x040B, 0 },
+	{ 0x098C, 0x2729, 0 },
+	{ 0x0990, 0x050B, 0 },
+	{ 0x098C, 0x272B, 0 },
+	{ 0x0990, 0x2111, 0 },
+	{ 0x098C, 0x272D, 0 },
+	{ 0x0990, 0x0024, 0 },
+	{ 0x098C, 0x272F, 0 },
+	{ 0x0990, 0x004C, 0 },
+	{ 0x098C, 0x2731, 0 },
+	{ 0x0990, 0x00F9, 0 },
+	{ 0x098C, 0x2733, 0 },
+	{ 0x0990, 0x00A7, 0 },
+	{ 0x098C, 0x2735, 0 },
+	{ 0x0990, 0x0559, 0 },
+	{ 0x098C, 0x2737, 0 },
+	{ 0x0990, 0x0722, 0 },
+
+	/* Crop configuration - Context A */
+	{ 0x098C, 0x2739, 0 },
+	{ 0x0990, 0x0000, 0 },
+	{ 0x098C, 0x273B, 0 },
+	{ 0x0990, 0x027F, 0 },
+	{ 0x098C, 0x273D, 0 },
+	{ 0x0990, 0x0000, 0 },
+	{ 0x098C, 0x273F, 0 },
+	{ 0x0990, 0x01DF, 0 },
+
+	/* Crop configuration - Context B */
+	{ 0x098C, 0x2747, 0 },
+	{ 0x0990, 0x0000, 0 },
+	{ 0x098C, 0x2749, 0 },
+	{ 0x0990, 0x04FF, 0 },
+	{ 0x098C, 0x274B, 0 },
+	{ 0x0990, 0x0000, 0 },
+	{ 0x098C, 0x274D, 0 },
+	{ 0x0990, 0x03FF, 0 },
+
+	/* Flicker detection */
+	{ 0x098C, 0x222D, 0 },
+	{ 0x0990, 0x00CC, 0 },
+	{ 0x098C, 0xA404, 0 },
+	{ 0x0990, 0x0010, 0 },
+	{ 0x098C, 0xA408, 0 },
+	{ 0x0990, 0x0032, 0 },
+	{ 0x098C, 0xA409, 0 },
+	{ 0x0990, 0x0034, 0 },
+	{ 0x098C, 0xA40A, 0 },
+	{ 0x0990, 0x003C, 0 },
+	{ 0x098C, 0xA40B, 0 },
+	{ 0x0990, 0x003E, 0 },
+	{ 0x098C, 0x2411, 0 },
+	{ 0x0990, 0x00CC, 0 },
+	{ 0x098C, 0x2413, 0 },
+	{ 0x0990, 0x00F4, 0 },
+	{ 0x098C, 0x2415, 0 },
+	{ 0x0990, 0x0089, 0 },
+	{ 0x098C, 0x2417, 0 },
+	{ 0x0990, 0x00A4, 0 },
+	{ 0x098C, 0xA40D, 0 },
+	{ 0x0990, 0x0002, 0 },
+	{ 0x098C, 0xA40E, 0 },
+	{ 0x0990, 0x0003, 0 },
+	{ 0x098C, 0xA410, 0 },
+	{ 0x0990, 0x000A, 0 },
+
+	/* Sensor core reserved registers */
+	{ 0x3044, 0x0504, 0 },
+	{ 0x3086, 0x24F7, 0 },
+	{ 0x3088, 0xF059, 0 },
+	{ 0x3090, 0x0716, 0 },
+	{ 0x3092, 0xAB1F, 0 },
+	{ 0x30D4, 0x9020, 0 },
+	{ 0x30E2, 0x6645, 0 },
+	{ 0x30E4, 0x7A66, 0 },
+	{ 0x30E6, 0x6652, 0 },
+	{ 0x30E8, 0x7766, 0 },
+	{ 0x30EA, 0x2E03, 0 },
+	{ 0x30EC, 0x452E, 0 },
+	{ 0x30EE, 0x2E17, 0 },
+	{ 0x30F0, 0x452E, 0 },
+	{ 0x30F6, 0x0501, 0 },
+	{ 0x30F8, 0x0501, 0 },
+	{ 0x30FA, 0x0401, 0 },
+	{ 0x30FC, 0x0401, 0 },
+	{ 0x30FE, 0x5145, 0 },
+	{ 0x3100, 0x4F45, 0 },
+	{ 0x3102, 0x652E, 0 },
+	{ 0x3104, 0x7552, 0 },
+	{ 0x3106, 0x2D05, 0 },
+	{ 0x3108, 0x4405, 0 },
+	{ 0x311A, 0x5045, 0 },
+	{ 0x311E, 0x0601, 0 },
+	{ 0x3122, 0x0601, 0 },
+	{ 0x316C, 0x8406, 0 },
+
+	/* Noise reduction */
+	{ 0x098C, 0xAB2D, 0 },
+	{ 0x0990, 0x002A, 0 },
+	{ 0x098C, 0xAB31, 0 },
+	{ 0x0990, 0x002E, 0 },
+
+	/* Low-light enhancement */
+	{ 0x098C, 0x2B28, 0 },
+	{ 0x0990, 0x1F40, 0 },
+	{ 0x098C, 0x2B2A, 0 },
+	{ 0x0990, 0x3A98, 0 },
+	{ 0x098C, 0x2B38, 0 },
+	{ 0x0990, 0x1F40, 0 },
+	{ 0x098C, 0x2B3A, 0 },
+	{ 0x0990, 0x3A98, 0 },
+
+	/* AE settings */
+	{ 0x098C, 0x2257, 0 },
+	{ 0x0990, 0x2710, 0 },
+	{ 0x098C, 0x2250, 0 },
+	{ 0x0990, 0x1B58, 0 },
+	{ 0x098C, 0x2252, 0 },
+	{ 0x0990, 0x32C8, 0 },
+	{ 0x098C, 0xA24B, 0 },
+	{ 0x0990, 0x0082, 0 },
+
+	/* Aperture */
+	{ 0x326C, 0x0C00, 0 },
+
+	/* More Context A settings */
+	{ 0x098C, 0x2717, 0 },
+	{ 0x0990, 0x046C, 0 },
+	{ 0x098C, 0x2719, 0 },
+	{ 0x0990, 0x00AC, 0 },
+	{ 0x098C, 0x271B, 0 },
+	{ 0x0990, 0x01F1, 0 },
+	{ 0x098C, 0x271D, 0 },
+	{ 0x0990, 0x013F, 0 },
+	{ 0x098C, 0x271F, 0 },
+	{ 0x0990, 0x032E, 0 },
+	{ 0x098C, 0x2721, 0 },
+	{ 0x0990, 0x04CC, 0 },
+	{ 0x098C, 0x275F, 0 },
+	{ 0x0990, 0x0596, 0 },
+	{ 0x098C, 0x2761, 0 },
+	{ 0x0990, 0x0094, 0 },
+
+	/* Lens shading correction */
+	{ 0x364E, 0x07B0, 0 },
+	{ 0x3650, 0x7E0E, 0 },
+	{ 0x3652, 0x3D31, 0 },
+	{ 0x3654, 0x80AE, 0 },
+	{ 0x3656, 0xE131, 0 },
+	{ 0x3658, 0x01B0, 0 },
+	{ 0x365A, 0x878D, 0 },
+	{ 0x365C, 0x2671, 0 },
+	{ 0x365E, 0x7D2D, 0 },
+	{ 0x3660, 0xA5D1, 0 },
+	{ 0x3662, 0x03B0, 0 },
+	{ 0x3664, 0x5A0E, 0 },
+	{ 0x3666, 0x0E71, 0 },
+	{ 0x3668, 0x99EE, 0 },
+	{ 0x366A, 0xA671, 0 },
+	{ 0x366C, 0x0170, 0 },
+	{ 0x366E, 0xF44D, 0 },
+	{ 0x3670, 0x2971, 0 },
+	{ 0x3672, 0x2D4A, 0 },
+	{ 0x3674, 0xD671, 0 },
+	{ 0x3676, 0x674C, 0 },
+	{ 0x3678, 0x748D, 0 },
+	{ 0x367A, 0x3FEE, 0 },
+	{ 0x367C, 0x89AE, 0 },
+	{ 0x367E, 0xB410, 0 },
+	{ 0x3680, 0x168C, 0 },
+	{ 0x3682, 0xC56D, 0 },
+	{ 0x3684, 0x7CAC, 0 },
+	{ 0x3686, 0x038F, 0 },
+	{ 0x3688, 0xA86F, 0 },
+	{ 0x368A, 0xDB6B, 0 },
+	{ 0x368C, 0xA2AE, 0 },
+	{ 0x368E, 0xFA8D, 0 },
+	{ 0x3690, 0x5C8E, 0 },
+	{ 0x3692, 0x740C, 0 },
+	{ 0x3694, 0x9F4B, 0 },
+	{ 0x3696, 0x1C4D, 0 },
+	{ 0x3698, 0x978D, 0 },
+	{ 0x369A, 0x21EC, 0 },
+	{ 0x369C, 0xF5AD, 0 },
+	{ 0x369E, 0x7D10, 0 },
+	{ 0x36A0, 0x3E2E, 0 },
+	{ 0x36A2, 0x8953, 0 },
+	{ 0x36A4, 0xD910, 0 },
+	{ 0x36A6, 0x3033, 0 },
+	{ 0x36A8, 0x06D1, 0 },
+	{ 0x36AA, 0xAD4E, 0 },
+	{ 0x36AC, 0xD2D2, 0 },
+	{ 0x36AE, 0x5CCE, 0 },
+	{ 0x36B0, 0x3B93, 0 },
+	{ 0x36B2, 0x50D0, 0 },
+	{ 0x36B4, 0x79AD, 0 },
+	{ 0x36B6, 0xDFF2, 0 },
+	{ 0x36B8, 0x88AF, 0 },
+	{ 0x36BA, 0x2453, 0 },
+	{ 0x36BC, 0x0051, 0 },
+	{ 0x36BE, 0x81CF, 0 },
+	{ 0x36C0, 0x8313, 0 },
+	{ 0x36C2, 0x2250, 0 },
+	{ 0x36C4, 0x4A53, 0 },
+	{ 0x36C6, 0x0C8D, 0 },
+	{ 0x36C8, 0x362B, 0 },
+	{ 0x36CA, 0xAD51, 0 },
+	{ 0x36CC, 0xA470, 0 },
+	{ 0x36CE, 0x3DD2, 0 },
+	{ 0x36D0, 0x174C, 0 },
+	{ 0x36D2, 0x152F, 0 },
+	{ 0x36D4, 0x82F1, 0 },
+	{ 0x36D6, 0xDED0, 0 },
+	{ 0x36D8, 0x6F12, 0 },
+	{ 0x36DA, 0xD36C, 0 },
+	{ 0x36DC, 0x51AE, 0 },
+	{ 0x36DE, 0xD0AE, 0 },
+	{ 0x36E0, 0x274E, 0 },
+	{ 0x36E2, 0x25F2, 0 },
+	{ 0x36E4, 0xDCCA, 0 },
+	{ 0x36E6, 0x438E, 0 },
+	{ 0x36E8, 0xD64E, 0 },
+	{ 0x36EA, 0x8A71, 0 },
+	{ 0x36EC, 0x1492, 0 },
+	{ 0x36EE, 0xD5B1, 0 },
+	{ 0x36F0, 0xEBF0, 0 },
+	{ 0x36F2, 0x53F3, 0 },
+	{ 0x36F4, 0x3492, 0 },
+	{ 0x36F6, 0x9AF4, 0 },
+	{ 0x36F8, 0x8BF1, 0 },
+	{ 0x36FA, 0x204F, 0 },
+	{ 0x36FC, 0x3A93, 0 },
+	{ 0x36FE, 0xB551, 0 },
+	{ 0x3700, 0xE214, 0 },
+	{ 0x3702, 0xF2B0, 0 },
+	{ 0x3704, 0x8C30, 0 },
+	{ 0x3706, 0x3053, 0 },
+	{ 0x3708, 0x64F0, 0 },
+	{ 0x370A, 0xFC73, 0 },
+	{ 0x370C, 0xD311, 0 },
+	{ 0x370E, 0x336F, 0 },
+	{ 0x3710, 0x5AF3, 0 },
+	{ 0x3712, 0x4EAF, 0 },
+	{ 0x3714, 0xDBD4, 0 },
+
+	/* Lens shading origin */
+	{ 0x3644, 0x02A0, 0 },
+	{ 0x3642, 0x01FC, 0 },
+	{ 0x3210, 0x01B8, 0 },
+
+	/* Color correction matrix - Low light */
+	{ 0x098C, 0x2306, 0 },
+	{ 0x0990, 0x0233, 0 },
+	{ 0x098C, 0x2308, 0 },
+	{ 0x0990, 0xFF0B, 0 },
+	{ 0x098C, 0x230A, 0 },
+	{ 0x0990, 0x0024, 0 },
+	{ 0x098C, 0x230C, 0 },
+	{ 0x0990, 0xFFC8, 0 },
+	{ 0x098C, 0x230E, 0 },
+	{ 0x0990, 0x01DE, 0 },
+	{ 0x098C, 0x2310, 0 },
+	{ 0x0990, 0xFFBD, 0 },
+	{ 0x098C, 0x2312, 0 },
+	{ 0x0990, 0x0019, 0 },
+	{ 0x098C, 0x2314, 0 },
+	{ 0x0990, 0xFF2B, 0 },
+	{ 0x098C, 0x2316, 0 },
+	{ 0x0990, 0x01E8, 0 },
+	{ 0x098C, 0x2318, 0 },
+	{ 0x0990, 0x0024, 0 },
+	{ 0x098C, 0x231A, 0 },
+	{ 0x0990, 0x0030, 0 },
+
+	/* Color correction matrix - RL (delta) */
+	{ 0x098C, 0x231C, 0 },
+	{ 0x0990, 0xFF7D, 0 },
+	{ 0x098C, 0x231E, 0 },
+	{ 0x0990, 0x002C, 0 },
+	{ 0x098C, 0x2320, 0 },
+	{ 0x0990, 0x002C, 0 },
+	{ 0x098C, 0x2322, 0 },
+	{ 0x0990, 0x0006, 0 },
+	{ 0x098C, 0x2324, 0 },
+	{ 0x0990, 0x00A3, 0 },
+	{ 0x098C, 0x2326, 0 },
+	{ 0x0990, 0xFF75, 0 },
+	{ 0x098C, 0x2328, 0 },
+	{ 0x0990, 0xFFF4, 0 },
+	{ 0x098C, 0x232A, 0 },
+	{ 0x0990, 0x00AC, 0 },
+	{ 0x098C, 0x232C, 0 },
+	{ 0x0990, 0xFF75, 0 },
+	{ 0x098C, 0x232E, 0 },
+	{ 0x0990, 0x0010, 0 },
+	{ 0x098C, 0x2330, 0 },
+	{ 0x0990, 0xFFF4, 0 },
+
+	/* AWB settings */
+	{ 0x098C, 0xA348, 0 },
+	{ 0x0990, 0x0008, 0 },
+	{ 0x098C, 0xA349, 0 },
+	{ 0x0990, 0x0002, 0 },
+	{ 0x098C, 0xA34A, 0 },
+	{ 0x0990, 0x0059, 0 },
+	{ 0x098C, 0xA34B, 0 },
+	{ 0x0990, 0x00A6, 0 },
+	{ 0x098C, 0xA351, 0 },
+	{ 0x0990, 0x0000, 0 },
+	{ 0x098C, 0xA352, 0 },
+	{ 0x0990, 0x007F, 0 },
+	{ 0x098C, 0xA35D, 0 },
+	{ 0x0990, 0x0078, 0 },
+	{ 0x098C, 0xA35E, 0 },
+	{ 0x0990, 0x0086, 0 },
+	{ 0x098C, 0xA35F, 0 },
+	{ 0x0990, 0x007E, 0 },
+	{ 0x098C, 0xA360, 0 },
+	{ 0x0990, 0x0082, 0 },
+
+	/* Cold color adjustment */
+	{ 0x098C, 0xA369, 0 },
+	{ 0x0990, 0x0097, 0 },
+	{ 0x098C, 0xA36A, 0 },
+	{ 0x0990, 0x008C, 0 },
+	{ 0x098C, 0xA36B, 0 },
+	{ 0x0990, 0x0080, 0 },
+
+	/* AWB window */
+	{ 0x098C, 0xA302, 0 },
+	{ 0x0990, 0x0000, 0 },
+	{ 0x098C, 0xA303, 0 },
+	{ 0x0990, 0x00FF, 0 },
+
+	/* AE preview settings */
+	{ 0x098C, 0xA11D, 0 },
+	{ 0x0990, 0x0002, 0 },
+	{ 0x098C, 0x271F, 0 },
+	{ 0x0990, 0x032E, 0 },
+	{ 0x098C, 0x2721, 0 },
+	{ 0x0990, 0x04CC, 0 },
+
+	/* AE gain settings */
+	{ 0x098C, 0xA216, 0 },
+	{ 0x0990, 0x0060, 0 },
+	{ 0x098C, 0xA215, 0 },
+	{ 0x0990, 0x000A, 0 },
+	{ 0x098C, 0xA20C, 0 },
+	{ 0x0990, 0x0028, 0 },
+	{ 0x098C, 0xA24F, 0 },
+	{ 0x0990, 0x0042, 0 },
+	{ 0x098C, 0xA20E, 0 },
+	{ 0x0990, 0x0060, 0 },
+
+	/* AE window */
+	{ 0x098C, 0xA202, 0 },
+	{ 0x0990, 0x0000, 0 },
+	{ 0x098C, 0xA203, 0 },
+	{ 0x0990, 0x00FF, 0 },
+	{ 0x098C, 0xA207, 0 },
+	{ 0x0990, 0x0004, 0 },
+
+	/* Gamma morph control */
+	{ 0x098C, 0xAB37, 0 },
+	{ 0x0990, 0x0003, 0 },
+	{ 0x098C, 0x2B38, 0 },
+	{ 0x0990, 0x3A98, 0 },
+	{ 0x098C, 0x2B3A, 0 },
+	{ 0x0990, 0x5000, 0 },
+
+	/* Saturation */
+	{ 0x098C, 0xAB20, 0 },
+	{ 0x0990, 0x0023, 0 },
+	{ 0x098C, 0xAB24, 0 },
+	{ 0x0990, 0x0010, 0 },
+
+	/* AE speed */
+	{ 0x098C, 0xA109, 0 },
+	{ 0x0990, 0x0020, 0 },
+	{ 0x098C, 0xA10A, 0 },
+	{ 0x0990, 0x0002, 0 },
+
+	/* Gamma table A */
+	{ 0x098C, 0xAB3C, 0 },
+	{ 0x0990, 0x0000, 0 },
+	{ 0x098C, 0xAB3D, 0 },
+	{ 0x0990, 0x0006, 0 },
+	{ 0x098C, 0xAB3E, 0 },
+	{ 0x0990, 0x0014, 0 },
+	{ 0x098C, 0xAB3F, 0 },
+	{ 0x0990, 0x0038, 0 },
+	{ 0x098C, 0xAB40, 0 },
+	{ 0x0990, 0x005F, 0 },
+	{ 0x098C, 0xAB41, 0 },
+	{ 0x0990, 0x0079, 0 },
+	{ 0x098C, 0xAB42, 0 },
+	{ 0x0990, 0x008D, 0 },
+	{ 0x098C, 0xAB43, 0 },
+	{ 0x0990, 0x009E, 0 },
+	{ 0x098C, 0xAB44, 0 },
+	{ 0x0990, 0x00AC, 0 },
+	{ 0x098C, 0xAB45, 0 },
+	{ 0x0990, 0x00B8, 0 },
+	{ 0x098C, 0xAB46, 0 },
+	{ 0x0990, 0x00C3, 0 },
+	{ 0x098C, 0xAB47, 0 },
+	{ 0x0990, 0x00CD, 0 },
+	{ 0x098C, 0xAB48, 0 },
+	{ 0x0990, 0x00D5, 0 },
+	{ 0x098C, 0xAB49, 0 },
+	{ 0x0990, 0x00DE, 0 },
+	{ 0x098C, 0xAB4A, 0 },
+	{ 0x0990, 0x00E5, 0 },
+	{ 0x098C, 0xAB4B, 0 },
+	{ 0x0990, 0x00EC, 0 },
+	{ 0x098C, 0xAB4C, 0 },
+	{ 0x0990, 0x00F3, 0 },
+	{ 0x098C, 0xAB4D, 0 },
+	{ 0x0990, 0x00F9, 0 },
+	{ 0x098C, 0xAB4E, 0 },
+	{ 0x0990, 0x00FF, 0 },
+
+	/* Noise reduction RGB */
+	{ 0x098C, 0xAB2C, 0 },
+	{ 0x0990, 0x0010, 0 },
+	{ 0x098C, 0xAB2D, 0 },
+	{ 0x0990, 0x002A, 0 },
+	{ 0x098C, 0xAB2E, 0 },
+	{ 0x0990, 0x0010, 0 },
+	{ 0x098C, 0xAB2F, 0 },
+	{ 0x0990, 0x0010, 0 },
+
+	/* Gamma table B */
+	{ 0x098C, 0xAB4F, 0 },
+	{ 0x0990, 0x0000, 0 },
+	{ 0x098C, 0xAB50, 0 },
+	{ 0x0990, 0x0004, 0 },
+	{ 0x098C, 0xAB51, 0 },
+	{ 0x0990, 0x000D, 0 },
+	{ 0x098C, 0xAB52, 0 },
+	{ 0x0990, 0x0028, 0 },
+	{ 0x098C, 0xAB53, 0 },
+	{ 0x0990, 0x0053, 0 },
+	{ 0x098C, 0xAB54, 0 },
+	{ 0x0990, 0x0075, 0 },
+	{ 0x098C, 0xAB55, 0 },
+	{ 0x0990, 0x0092, 0 },
+	{ 0x098C, 0xAB56, 0 },
+	{ 0x0990, 0x00A7, 0 },
+	{ 0x098C, 0xAB57, 0 },
+	{ 0x0990, 0x00B7, 0 },
+	{ 0x098C, 0xAB58, 0 },
+	{ 0x0990, 0x00C4, 0 },
+	{ 0x098C, 0xAB59, 0 },
+	{ 0x0990, 0x00CF, 0 },
+	{ 0x098C, 0xAB5A, 0 },
+	{ 0x0990, 0x00D8, 0 },
+	{ 0x098C, 0xAB5B, 0 },
+	{ 0x0990, 0x00DF, 0 },
+	{ 0x098C, 0xAB5C, 0 },
+	{ 0x0990, 0x00E6, 0 },
+	{ 0x098C, 0xAB5D, 0 },
+	{ 0x0990, 0x00EC, 0 },
+	{ 0x098C, 0xAB5E, 0 },
+	{ 0x0990, 0x00F2, 0 },
+	{ 0x098C, 0xAB5F, 0 },
+	{ 0x0990, 0x00F6, 0 },
+	{ 0x098C, 0xAB60, 0 },
+	{ 0x0990, 0x00FB, 0 },
+	{ 0x098C, 0xAB61, 0 },
+	{ 0x0990, 0x00FF, 0 },
+
+	/* Read mode - no mirror/flip */
+	{ 0x098C, 0x2717, 0 },
+	{ 0x0990, 0x046C, 0 },
+	{ 0x098C, 0x272D, 0 },
+	{ 0x0990, 0x0024, 0 },
+
+	/* Reset command before sequencer */
+	{ 0x001A, 0x021C, 0 },
+
+	/* Issue refresh command */
+	{ 0x098C, 0xA103, 0 },
+	{ 0x0990, 0x0006, 0 },
+};
+
+/* Apply the MT9M113 initialization table */
+static int mt9m113_sensor_init(struct mt9m113 *sensor)
+{
+	struct device *dev = &sensor->client->dev;
+	int ret = 0;
+	unsigned int i;
+
+	/* MCU is about to be fully re-initialised, so any prior test-pattern
+	 * override is gone. Clear the bookkeeping so a subsequent
+	 * V4L2_CID_TEST_PATTERN=0 does not re-run the disable sequence.
+	 */
+	sensor->test_pattern_active = false;
+
+	dev_dbg(dev, "MT9M113: applying init table (%zu entries)\n",
+		 ARRAY_SIZE(mt9m113_init_table));
+
+	/*
+	 * Suspend double buffer updates so all init table writes take effect
+	 * atomically at frame start. Per datasheet, R0x0248[15] inhibits
+	 * transfers from pending to live registers.
+	 */
+	ret = mt9m113_double_buffer_suspend(sensor);
+	if (ret) {
+		dev_warn(dev, "Failed to suspend double buffer: %d\n", ret);
+		/* Continue anyway - not fatal */
+	}
+
+	for (i = 0; i < ARRAY_SIZE(mt9m113_init_table); i++) {
+		const struct mt9m113_reg_entry *entry = &mt9m113_init_table[i];
+
+		ret = cci_write(sensor->regmap, CCI_REG16(entry->reg),
+				entry->value, NULL);
+		if (ret < 0) {
+			dev_err(dev, "MT9M113: reg 0x%04x write failed: %d\n",
+				entry->reg, ret);
+			mt9m113_double_buffer_resume(sensor);
+			return ret;
+		}
+
+		if (entry->delay_ms > 0)
+			msleep(entry->delay_ms);
+	}
+
+	/* Resume double buffer updates - changes take effect at next frame start */
+	ret = mt9m113_double_buffer_resume(sensor);
+	if (ret) {
+		dev_warn(dev, "Failed to resume double buffer: %d\n", ret);
+		/* Continue anyway - not fatal */
+	}
+
+	/* Wait for MCU to complete refresh */
+	ret = mt9m113_poll_mcu_var(sensor, MT9M113_SEQ_CMD, 0x0000, 1000);
+	if (ret < 0) {
+		dev_err(dev, "MT9M113: MCU refresh timeout\n");
+		return ret;
+	}
+
+	/* Issue sequencer refresh */
+	ret = mt9m113_write_mcu_var(sensor, MT9M113_SEQ_CMD,
+				    MT9M113_SEQ_CMD_REFRESH);
+	if (ret < 0)
+		return ret;
+
+	ret = mt9m113_poll_mcu_var(sensor, MT9M113_SEQ_CMD, 0x0000, 1000);
+	if (ret < 0) {
+		dev_err(dev, "MT9M113: SEQ refresh timeout\n");
+		return ret;
+	}
+
+	/*
+	 * Enable MIPI Frame-Start/End short packets. Without them the VFE CAMIF
+	 * has no per-frame boundary marker and runs in line-counting (APS) mode,
+	 * which slips against the sensor's free-running readout and makes the
+	 * captured image walk vertically (~tens of lines/frame, resolution
+	 * dependent). Emitting the frame-sync short packets locks every frame.
+	 */
+	ret = cci_write(sensor->regmap, MT9M113_CUSTOM_SHORT_PKT,
+			MT9M113_CUSTOM_SHORT_PKT_FRAME_CNT_EN, NULL);
+	if (ret < 0)
+		return ret;
+
+	/*
+	 * Do NOT enable MIPI output here. It is enabled in start_streaming once
+	 * the output config is programmed, followed by REFRESH_MODE + REFRESH.
+	 * Enabling MIPI before the CSIPHY is configured causes problems.
+	 */
+
+	dev_dbg(dev, "MT9M113: init complete\n");
+	return 0;
+}
+
+/* -----------------------------------------------------------------------------
+ * Streaming
+ */
+
+/*
+ * Configure AE (Auto Exposure) parameters for preview vs snapshot/video mode.
+ * From legacy vendor driver: snapshot mode allows longer exposure for better quality,
+ * while preview mode optimizes for higher frame rate and lower power.
+ */
+static int mt9m113_configure_ae_mode(struct mt9m113 *sensor, bool snapshot_mode)
+{
+	struct device *dev = &sensor->client->dev;
+	int ret;
+
+	if (snapshot_mode) {
+		/* Snapshot/Capture mode: allow longer exposure for quality */
+		dev_dbg(dev, "MT9M113: configuring AE for snapshot mode\n");
+		ret = mt9m113_write_mcu_var(sensor, MT9M113_AE_MAX_INDEX, 0x0028);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor, MT9M113_AE_MAX_VIRTGAIN, 0x0060);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor, MT9M113_AE_MAX_DGAIN_AE1, 0x00C8);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor, MT9M113_AE_JUMP_DIVISOR, 0x0002);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor, MT9M113_AE_SKIP_FRAMES, 0x0002);
+		if (ret)
+			return ret;
+	} else {
+		/* Preview mode: optimize for frame rate and responsiveness */
+		dev_dbg(dev, "MT9M113: configuring AE for preview mode\n");
+		ret = mt9m113_write_mcu_var(sensor, MT9M113_AE_MAX_INDEX, 0x0008);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor, MT9M113_AE_MAX_VIRTGAIN, 0x00A0);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor, MT9M113_AE_MAX_DGAIN_AE1, 0x0150);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor, MT9M113_AE_JUMP_DIVISOR, 0x0001);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor, MT9M113_AE_SKIP_FRAMES, 0x0001);
+		if (ret)
+			return ret;
+	}
+
+	return 0;
+}
+
+/*
+ * Program the per-context output dimensions and pixel format, then re-apply the
+ * sensor readout geometry. Output configuration is double-buffered so it
+ * latches atomically at the next frame start.
+ */
+static int mt9m113_configure_output(struct mt9m113 *sensor, bool use_context_b,
+				    u32 code)
+{
+	struct device *dev = &sensor->client->dev;
+	u16 width_reg, height_reg, format_reg;
+	u16 width_val, height_val, format_val;
+	int ret;
+
+	if (use_context_b) {
+		width_reg = MT9M113_MODE_OUTPUT_WIDTH_B;
+		height_reg = MT9M113_MODE_OUTPUT_HEIGHT_B;
+		format_reg = MT9M113_MODE_OUTPUT_FORMAT_B;
+		width_val = 1280;
+		height_val = 1024;
+	} else {
+		width_reg = MT9M113_MODE_OUTPUT_WIDTH_A;
+		height_reg = MT9M113_MODE_OUTPUT_HEIGHT_A;
+		format_reg = MT9M113_MODE_OUTPUT_FORMAT_A;
+		width_val = 640;
+		height_val = 480;
+	}
+
+	format_val = mt9m113_format_val(code);
+
+	ret = mt9m113_double_buffer_suspend(sensor);
+	if (ret)
+		dev_warn(dev, "Failed to suspend double buffer: %d\n", ret);
+
+	ret = mt9m113_write_mcu_var(sensor, width_reg, width_val);
+	if (!ret)
+		ret = mt9m113_write_mcu_var(sensor, height_reg, height_val);
+	if (!ret)
+		ret = mt9m113_write_mcu_var(sensor, format_reg, format_val);
+
+	{
+		int resume_ret = mt9m113_double_buffer_resume(sensor);
+
+		if (!ret)
+			ret = resume_ret;
+	}
+	if (ret)
+		return ret;
+
+	dev_dbg(dev, "MT9M113: Context %c: %ux%u, format=0x%04x\n",
+		use_context_b ? 'B' : 'A', width_val, height_val, format_val);
+
+	/*
+	 * Re-apply the sensor readout geometry. The MCU variables for
+	 * row_end/read_mode can be left stale after switching from Context B to
+	 * Context A, producing wrong line counts and a corrupted image.
+	 */
+	ret = mt9m113_configure_sensor_context(sensor, use_context_b);
+	if (ret)
+		dev_err(dev, "Failed to configure sensor context: %d\n", ret);
+
+	return ret;
+}
+
+/*
+ * The MCU resets OUTPUT_CONTROL and MODE_OUTPUT_FORMAT to their defaults during
+ * sequencer state transitions, so re-apply them once the stream is running.
+ * Best-effort: a failure is logged but does not abort the stream.
+ */
+static void mt9m113_reassert_output(struct mt9m113 *sensor, u16 output_ctrl_val,
+				    u16 format_reg, u32 code)
+{
+	struct device *dev = &sensor->client->dev;
+	u16 format_val = mt9m113_format_val(code);
+
+	if (cci_write(sensor->regmap, MT9M113_OUTPUT_CONTROL, output_ctrl_val,
+		      NULL))
+		dev_warn(dev, "MT9M113: OUTPUT_CONTROL re-write failed\n");
+	if (mt9m113_write_mcu_var(sensor, format_reg, format_val))
+		dev_warn(dev, "MT9M113: MODE_OUTPUT_FORMAT re-write failed\n");
+}
+
+/* Context A (640x480 preview): enable output and run the preview sequencer. */
+static int mt9m113_stream_context_a(struct mt9m113 *sensor, u16 output_ctrl_val,
+				    u32 code)
+{
+	struct device *dev = &sensor->client->dev;
+	int ret;
+
+	ret = cci_write(sensor->regmap, MT9M113_OUTPUT_CONTROL, output_ctrl_val,
+			NULL);
+	if (ret)
+		return ret;
+	dev_dbg(dev, "MT9M113: OUTPUT_CONTROL=0x%04x enabled\n", output_ctrl_val);
+
+	ret = cci_write(sensor->regmap, MT9M113_RESET_REGISTER,
+			MT9M113_RESET_REG_STREAMING, NULL);
+	if (ret)
+		return ret;
+
+	ret = mt9m113_write_mcu_var(sensor, MT9M113_SEQ_CAP_MODE,
+				    MT9M113_SEQ_CAP_MODE_PREVIEW);
+	if (ret)
+		return ret;
+	msleep(40);
+
+	dev_dbg(dev, "MT9M113: Writing SEQ_CMD_RUN\n");
+	ret = mt9m113_write_mcu_var(sensor, MT9M113_SEQ_CMD, MT9M113_SEQ_CMD_RUN);
+	if (ret)
+		return ret;
+
+	ret = mt9m113_poll_mcu_var(sensor, MT9M113_SEQ_CMD, 0x0000, 500);
+	if (ret < 0)
+		dev_warn(dev, "MT9M113: SEQ_CMD_RUN did not complete\n");
+
+	mt9m113_reassert_output(sensor, output_ctrl_val,
+				MT9M113_MODE_OUTPUT_FORMAT_A, code);
+	msleep(20);
+	return 0;
+}
+
+/*
+ * Context B (1280x1024 capture): the MCU only accepts CAPTURE from stable
+ * preview, so enter preview first, wait for SEQ_STATE=PREVIEW, then switch.
+ */
+static int mt9m113_stream_context_b(struct mt9m113 *sensor, u16 output_ctrl_val,
+				    u32 code)
+{
+	struct device *dev = &sensor->client->dev;
+	u64 seq_state = 0;
+	unsigned int i;
+	int ret;
+
+	ret = cci_write(sensor->regmap, MT9M113_OUTPUT_CONTROL, output_ctrl_val,
+			NULL);
+	if (ret)
+		return ret;
+	dev_dbg(dev, "MT9M113: OUTPUT_CONTROL=0x%04x enabled\n", output_ctrl_val);
+
+	ret = cci_write(sensor->regmap, MT9M113_RESET_REGISTER,
+			MT9M113_RESET_REG_STREAMING, NULL);
+	if (ret)
+		return ret;
+
+	/* Enter preview first. */
+	ret = mt9m113_write_mcu_var(sensor, MT9M113_SEQ_CAP_MODE,
+				    MT9M113_SEQ_CAP_MODE_PREVIEW);
+	if (ret)
+		return ret;
+	msleep(40);
+
+	dev_dbg(dev, "MT9M113: Entering preview mode first (SEQ_CMD_RUN)\n");
+	ret = mt9m113_write_mcu_var(sensor, MT9M113_SEQ_CMD, MT9M113_SEQ_CMD_RUN);
+	if (ret)
+		return ret;
+
+	ret = mt9m113_poll_mcu_var(sensor, MT9M113_SEQ_CMD, 0x0000, 500);
+	if (ret < 0)
+		dev_warn(dev, "MT9M113: SEQ_CMD_RUN did not complete\n");
+
+	for (i = 0; i < 100; i++) {
+		mt9m113_read_mcu_var(sensor, MT9M113_SEQ_STATE, &seq_state);
+		if (seq_state == MT9M113_SEQ_STATE_PREVIEW)
+			break;
+		usleep_range(10000, 11000);
+	}
+	if (seq_state != MT9M113_SEQ_STATE_PREVIEW)
+		dev_warn(dev,
+			 "MT9M113: preview state not reached (SEQ_STATE=0x%llx)\n",
+			 seq_state);
+
+	/* Switch to video mode (stays in Context B). */
+	ret = mt9m113_write_mcu_var(sensor, MT9M113_SEQ_CAP_MODE,
+				    MT9M113_SEQ_CAP_MODE_VIDEO);
+	if (ret)
+		return ret;
+	msleep(40);
+
+	ret = mt9m113_seq_cmd_ready(sensor);
+	if (ret < 0) {
+		dev_err(dev, "MT9M113: MCU not ready for CAPTURE command\n");
+		return ret;
+	}
+
+	dev_dbg(dev, "MT9M113: switching to Context B (SEQ_CMD_CAPTURE)\n");
+	ret = mt9m113_write_mcu_var(sensor, MT9M113_SEQ_CMD,
+				    MT9M113_SEQ_CMD_CAPTURE);
+	if (ret)
+		return ret;
+
+	ret = mt9m113_poll_mcu_var(sensor, MT9M113_SEQ_CMD, 0x0000, 500);
+	if (ret < 0)
+		dev_warn(dev, "MT9M113: SEQ_CMD_CAPTURE did not complete\n");
+
+	for (i = 0; i < 100; i++) {
+		mt9m113_read_mcu_var(sensor, MT9M113_SEQ_STATE, &seq_state);
+		if (seq_state == MT9M113_SEQ_STATE_CAPTURE)
+			break;
+		usleep_range(10000, 11000);
+	}
+	if (seq_state != MT9M113_SEQ_STATE_CAPTURE) {
+		dev_err(dev, "MT9M113: Context B switch failed (SEQ_STATE=0x%llx)\n",
+			seq_state);
+		return -ETIMEDOUT;
+	}
+	dev_dbg(dev, "MT9M113: reached capture state\n");
+
+	mt9m113_reassert_output(sensor, output_ctrl_val,
+				MT9M113_MODE_OUTPUT_FORMAT_B, code);
+
+	/* The sensor pipeline needs time to reconfigure for 1280x1024. */
+	msleep(200);
+	return 0;
+}
+
+static int mt9m113_stream_on(struct mt9m113 *sensor,
+			     struct v4l2_subdev_state *state)
+{
+	struct device *dev = &sensor->client->dev;
+	const struct v4l2_mbus_framefmt *format;
+	const struct v4l2_rect *compose;
+	u64 health_check, seq_cmd;
+	bool use_context_b;
+	u16 output_ctrl_val;
+	int ret;
+
+	/*
+	 * Wake the MCU out of standby and give it time to settle (50ms matches
+	 * the legacy vendor driver).
+	 */
+	dev_dbg(dev, "MT9M113: start_streaming\n");
+	cci_write(sensor->regmap, MT9M113_STANDBY_CONTROL,
+		  MT9M113_STANDBY_CONTROL_ACTIVE, NULL);
+	msleep(50);
+
+	/*
+	 * MCU health check: MODE_OUTPUT_WIDTH_A reads back non-zero once the
+	 * MCU is running. If it is unresponsive, re-apply the init table.
+	 */
+	ret = mt9m113_read_mcu_var(sensor, MT9M113_MODE_OUTPUT_WIDTH_A,
+				   &health_check);
+	if (ret < 0 || health_check == 0) {
+		dev_warn(dev, "MT9M113: MCU unresponsive, re-init\n");
+		ret = mt9m113_sensor_init(sensor);
+		if (ret < 0)
+			return ret;
+	}
+
+	/*
+	 * Clear stale SEQ_CMD. The init table ends with REFRESH (SEQ_CMD=0x05)
+	 * which may not complete before the sensor enters standby/suspend. On
+	 * resume, SEQ_CMD is stuck at 0x05 and all subsequent commands fail.
+	 * Force-clear it.
+	 */
+	ret = mt9m113_read_mcu_var(sensor, MT9M113_SEQ_CMD, &seq_cmd);
+	if (ret == 0 && seq_cmd != 0) {
+		dev_warn(dev, "MT9M113: Clearing stale SEQ_CMD=0x%llx\n", seq_cmd);
+		mt9m113_write_mcu_var(sensor, MT9M113_SEQ_CMD, 0x0000);
+		msleep(50);
+	}
+
+	/*
+	 * Re-assert the MIPI Frame-Start/End short packets on every stream
+	 * start. The MCU clears CUSTOM_SHORT_PKT across standby/refresh
+	 * cycles, and mt9m113_sensor_init() (which also sets it) only runs on
+	 * the MCU-recovery path - so without this the bit is lost after the
+	 * first stream. Without the per-frame boundary marker the VFE CAMIF
+	 * falls back to line-counting and the image walks vertically.
+	 */
+	ret = cci_write(sensor->regmap, MT9M113_CUSTOM_SHORT_PKT,
+			MT9M113_CUSTOM_SHORT_PKT_FRAME_CNT_EN, NULL);
+	if (ret < 0)
+		return ret;
+
+	/* Apply all V4L2 controls (color effects, etc.) before streaming */
+	ret = __v4l2_ctrl_handler_setup(&sensor->ifp.hdl);
+	if (ret) {
+		dev_err(dev, "Failed to setup controls: %d\n", ret);
+		return ret;
+	}
+
+	compose = v4l2_subdev_state_get_compose(state, 0);
+	/* Source pad (pad 1) carries the MIPI output format. */
+	format = v4l2_subdev_state_get_format(state, 1);
+
+	use_context_b = (compose->width > 640 || compose->height > 480);
+	dev_dbg(dev, "MT9M113: %ux%u -> Context %c\n",
+		compose->width, compose->height, use_context_b ? 'B' : 'A');
+
+	/*
+	 * Configure AE for the selected context: Context B (capture) allows a
+	 * longer exposure for quality, Context A (preview) optimises for frame
+	 * rate.
+	 */
+	ret = mt9m113_configure_ae_mode(sensor, use_context_b);
+	if (ret) {
+		dev_err(dev, "Failed to configure AE mode: %d\n", ret);
+		return ret;
+	}
+
+	ret = mt9m113_configure_output(sensor, use_context_b, format->code);
+	if (ret)
+		return ret;
+
+	/* Wait for CSIPHY stabilization */
+	msleep(MT9M113_PRE_MIPI_DELAY_MS);
+
+	/*
+	 * YUV/RGB output: enable MIPI CSI-2 in FIFO-bypass mode and restore the
+	 * default ISP colour pipeline.
+	 */
+	output_ctrl_val = MT9M113_OUTPUT_CONTROL_MIPI_ENABLE;
+	ret = cci_write(sensor->regmap, MT9M113_OFIFO_CONTROL_STATUS,
+			MT9M113_OFIFO_BYPASS, NULL);
+	if (ret)
+		return ret;
+	ret = cci_write(sensor->regmap, MT9M113_COLOR_PIPELINE_CONTROL,
+			MT9M113_COLOR_PIPELINE_DEFAULT, NULL);
+	if (ret)
+		return ret;
+
+	/* The MCU must be idle before it will accept a sequencer command. */
+	ret = mt9m113_seq_cmd_ready(sensor);
+	if (ret < 0) {
+		dev_err(dev, "MT9M113: MCU not ready for streaming command\n");
+		return ret;
+	}
+
+	/*
+	 * One REFRESH applies the per-session output dimensions/format; without
+	 * it SEQ_CMD_RUN hangs (the MCU never clears SEQ_CMD). OFIFO and the
+	 * colour pipeline are already programmed above - they must not be
+	 * written while a REFRESH is pending or it times out.
+	 */
+	ret = mt9m113_refresh(sensor);
+	if (ret)
+		dev_warn(dev, "MT9M113: REFRESH failed, continuing\n");
+
+	/*
+	 * For Context B the MCU must reach stable preview (SEQ_STATE=0x04)
+	 * before it will accept the CAPTURE command, so enter preview first and
+	 * then switch. Context A simply runs preview.
+	 */
+	if (use_context_b)
+		ret = mt9m113_stream_context_b(sensor, output_ctrl_val,
+					       format->code);
+	else
+		ret = mt9m113_stream_context_a(sensor, output_ctrl_val,
+					       format->code);
+	if (ret)
+		return ret;
+
+	dev_dbg(dev, "MT9M113: streaming started\n");
+	return 0;
+}
+
+/*
+ * The MT9M113 MCU intermittently wedges on stream start; the failures cluster
+ * at the start of a run and then clear. Retry a bounded number of times, fully
+ * power-cycling the sensor (runtime-PM suspend -> resume = power_off + power_on
+ * + sensor_init) between attempts. Exactly one pm_runtime get/put pair runs per
+ * attempt; on success the reference is held for the streaming session and
+ * released by mt9m113_stop_streaming().
+ */
+static int mt9m113_start_streaming(struct mt9m113 *sensor,
+				   struct v4l2_subdev_state *state)
+{
+	struct device *dev = &sensor->client->dev;
+	unsigned int attempt;
+	int ret = 0;
+
+	/*
+	 * Guard against a redundant s_stream(1): a second
+	 * pm_runtime_resume_and_get() here would leak a PM reference and pin
+	 * the device awake forever, because mt9m113_stop_streaming() only
+	 * drops one reference per stop.
+	 */
+	if (sensor->streaming)
+		return 0;
+
+	for (attempt = 0; attempt < MT9M113_STREAM_START_RETRIES; attempt++) {
+		ret = pm_runtime_resume_and_get(dev);
+		if (ret)
+			return ret;
+
+		ret = mt9m113_stream_on(sensor, state);
+		if (!ret) {
+			sensor->streaming = true;
+			return 0;
+		}
+
+		pm_runtime_put_sync_suspend(dev);
+		if (attempt + 1 < MT9M113_STREAM_START_RETRIES)
+			dev_warn(dev,
+				 "MT9M113: stream start failed (%d), power-cycle + retry %u/%u\n",
+				 ret, attempt + 1, MT9M113_STREAM_START_RETRIES - 1);
+	}
+
+	return ret;
+}
+
+static int mt9m113_stop_streaming(struct mt9m113 *sensor)
+{
+	struct device *dev = &sensor->client->dev;
+	int ret;
+
+	if (!sensor->streaming)
+		return 0;
+
+	sensor->streaming = false;
+
+	/* Disable MIPI output */
+	ret = cci_write(sensor->regmap, MT9M113_OUTPUT_CONTROL, 0x0000, NULL);
+	if (ret < 0)
+		dev_warn(dev,
+			 "MT9M113: MIPI output disable failed (%d), receiver may still see traffic\n",
+			 ret);
+
+	/*
+	 * Wait briefly for any pending SEQ_CMD to complete.
+	 * Don't issue REFRESH here - it can leave MCU stuck if it times out.
+	 * Start_streaming will handle full re-initialization.
+	 */
+	ret = mt9m113_poll_mcu_var(sensor, MT9M113_SEQ_CMD, 0x0000, 100);
+	if (ret < 0)
+		dev_dbg(dev, "MT9M113: SEQ_CMD did not complete before stop\n");
+
+	dev_dbg(dev, "MT9M113: streaming stopped\n");
+
+	pm_runtime_put_autosuspend(dev);
+	return 0;
+}
+
+/* -----------------------------------------------------------------------------
+ * V4L2 Subdev Operations
+ */
+
+static inline struct mt9m113 *ifp_to_mt9m113(struct v4l2_subdev *sd)
+{
+	return container_of(sd, struct mt9m113, ifp.sd);
+}
+
+static int mt9m113_ifp_s_stream(struct v4l2_subdev *sd, int enable)
+{
+	struct mt9m113 *sensor = ifp_to_mt9m113(sd);
+	struct v4l2_subdev_state *state;
+	int ret;
+
+	state = v4l2_subdev_lock_and_get_active_state(sd);
+
+	if (enable)
+		ret = mt9m113_start_streaming(sensor, state);
+	else
+		ret = mt9m113_stop_streaming(sensor);
+
+	v4l2_subdev_unlock_state(state);
+	return ret;
+}
+
+/* -----------------------------------------------------------------------------
+ * Pixel Array Subdev Operations
+ */
+
+static int mt9m113_pa_init_state(struct v4l2_subdev *sd,
+				 struct v4l2_subdev_state *state)
+{
+	struct v4l2_mbus_framefmt *format;
+	struct v4l2_rect *crop;
+
+	crop = v4l2_subdev_state_get_crop(state, 0);
+	crop->left = 0;
+	crop->top = 0;
+	crop->width = MT9M113_PIXEL_ARRAY_WIDTH;
+	crop->height = MT9M113_PIXEL_ARRAY_HEIGHT;
+
+	format = v4l2_subdev_state_get_format(state, 0);
+	format->width = MT9M113_PIXEL_ARRAY_WIDTH;
+	format->height = MT9M113_PIXEL_ARRAY_HEIGHT;
+	format->code = MEDIA_BUS_FMT_SGRBG10_1X10;
+	format->field = V4L2_FIELD_NONE;
+	format->colorspace = V4L2_COLORSPACE_RAW;
+
+	return 0;
+}
+
+static int mt9m113_pa_enum_mbus_code(struct v4l2_subdev *sd,
+				     struct v4l2_subdev_state *state,
+				     struct v4l2_subdev_mbus_code_enum *code)
+{
+	if (code->index > 0)
+		return -EINVAL;
+
+	code->code = MEDIA_BUS_FMT_SGRBG10_1X10;
+	return 0;
+}
+
+static int mt9m113_pa_enum_framesizes(struct v4l2_subdev *sd,
+				      struct v4l2_subdev_state *state,
+				      struct v4l2_subdev_frame_size_enum *fse)
+{
+	if (fse->index > 0)
+		return -EINVAL;
+
+	if (fse->code != MEDIA_BUS_FMT_SGRBG10_1X10)
+		return -EINVAL;
+
+	fse->min_width = MT9M113_PIXEL_ARRAY_WIDTH;
+	fse->max_width = MT9M113_PIXEL_ARRAY_WIDTH;
+	fse->min_height = MT9M113_PIXEL_ARRAY_HEIGHT;
+	fse->max_height = MT9M113_PIXEL_ARRAY_HEIGHT;
+
+	return 0;
+}
+
+static int mt9m113_pa_get_selection(struct v4l2_subdev *sd,
+				    struct v4l2_subdev_state *state,
+				    struct v4l2_subdev_selection *sel)
+{
+	switch (sel->target) {
+	case V4L2_SEL_TGT_CROP:
+		sel->r = *v4l2_subdev_state_get_crop(state, sel->pad);
+		return 0;
+
+	case V4L2_SEL_TGT_CROP_DEFAULT:
+	case V4L2_SEL_TGT_CROP_BOUNDS:
+	case V4L2_SEL_TGT_NATIVE_SIZE:
+		sel->r.left = 0;
+		sel->r.top = 0;
+		sel->r.width = MT9M113_PIXEL_ARRAY_WIDTH;
+		sel->r.height = MT9M113_PIXEL_ARRAY_HEIGHT;
+		return 0;
+
+	default:
+		return -EINVAL;
+	}
+}
+
+static const struct v4l2_subdev_pad_ops mt9m113_pa_pad_ops = {
+	.enum_mbus_code = mt9m113_pa_enum_mbus_code,
+	.enum_frame_size = mt9m113_pa_enum_framesizes,
+	.get_fmt = v4l2_subdev_get_fmt,
+	.get_selection = mt9m113_pa_get_selection,
+};
+
+static const struct v4l2_subdev_ops mt9m113_pa_ops = {
+	.pad = &mt9m113_pa_pad_ops,
+};
+
+static const struct v4l2_subdev_internal_ops mt9m113_pa_internal_ops = {
+	.init_state = mt9m113_pa_init_state,
+};
+
+/* -----------------------------------------------------------------------------
+ * IFP Subdev Operations
+ */
+
+static int mt9m113_ifp_init_state(struct v4l2_subdev *sd,
+				  struct v4l2_subdev_state *state)
+{
+	struct v4l2_mbus_framefmt *format;
+	struct v4l2_rect *crop;
+	struct v4l2_rect *compose;
+
+	/* Sink pad - receives raw data from PA */
+	format = v4l2_subdev_state_get_format(state, 0);
+	format->width = MT9M113_PIXEL_ARRAY_WIDTH;
+	format->height = MT9M113_PIXEL_ARRAY_HEIGHT;
+	format->code = MEDIA_BUS_FMT_SGRBG10_1X10;
+	format->field = V4L2_FIELD_NONE;
+	format->colorspace = V4L2_COLORSPACE_RAW;
+
+	crop = v4l2_subdev_state_get_crop(state, 0);
+	crop->left = 0;
+	crop->top = 0;
+	crop->width = MT9M113_PIXEL_ARRAY_WIDTH;
+	crop->height = MT9M113_PIXEL_ARRAY_HEIGHT;
+
+	compose = v4l2_subdev_state_get_compose(state, 0);
+	compose->left = 0;
+	compose->top = 0;
+	compose->width = 640;
+	compose->height = 480;
+
+	/* Source pad - outputs processed data to host */
+	format = v4l2_subdev_state_get_format(state, 1);
+	format->width = 640;
+	format->height = 480;
+	format->code = MEDIA_BUS_FMT_UYVY8_1X16;
+	format->field = V4L2_FIELD_NONE;
+	format->colorspace = V4L2_COLORSPACE_SRGB;
+
+	return 0;
+}
+
+static int mt9m113_ifp_enum_mbus_code(struct v4l2_subdev *sd,
+				      struct v4l2_subdev_state *state,
+				      struct v4l2_subdev_mbus_code_enum *code)
+{
+	/* Sink pad only accepts raw from PA */
+	if (code->pad == 0) {
+		if (code->index > 0)
+			return -EINVAL;
+		code->code = MEDIA_BUS_FMT_SGRBG10_1X10;
+		return 0;
+	}
+
+	/* Source pad supports multiple output formats */
+	if (code->index >= ARRAY_SIZE(mt9m113_format_infos))
+		return -EINVAL;
+
+	code->code = mt9m113_format_infos[code->index].code;
+	return 0;
+}
+
+static int mt9m113_ifp_enum_frame_size(struct v4l2_subdev *sd,
+				       struct v4l2_subdev_state *state,
+				       struct v4l2_subdev_frame_size_enum *fse)
+{
+	/* Sink pad - fixed raw input size */
+	if (fse->pad == 0) {
+		if (fse->index > 0)
+			return -EINVAL;
+		if (fse->code != MEDIA_BUS_FMT_SGRBG10_1X10)
+			return -EINVAL;
+		fse->min_width = MT9M113_PIXEL_ARRAY_WIDTH;
+		fse->max_width = MT9M113_PIXEL_ARRAY_WIDTH;
+		fse->min_height = MT9M113_PIXEL_ARRAY_HEIGHT;
+		fse->max_height = MT9M113_PIXEL_ARRAY_HEIGHT;
+		return 0;
+	}
+
+	/* Source pad - Context A (640x480) and Context B (1280x1024) */
+	if (fse->index > 1)
+		return -EINVAL;
+	if (mt9m113_format_info(fse->code)->code != fse->code)
+		return -EINVAL;
+
+	if (fse->index == 0) {
+		fse->min_width = 640;
+		fse->max_width = 640;
+		fse->min_height = 480;
+		fse->max_height = 480;
+	} else {
+		fse->min_width = 1280;
+		fse->max_width = 1280;
+		fse->min_height = 1024;
+		fse->max_height = 1024;
+	}
+
+	return 0;
+}
+
+static int mt9m113_ifp_enum_frame_interval(struct v4l2_subdev *sd,
+					   struct v4l2_subdev_state *state,
+					   struct v4l2_subdev_frame_interval_enum *fie)
+{
+	/* Only source pad (pad 1) supports frame interval enumeration */
+	if (fie->pad != 1)
+		return -EINVAL;
+
+	/* One interval per resolution */
+	if (fie->index > 0)
+		return -EINVAL;
+
+	/* Context A: 640x480 @ 30fps, Context B: 1280x1024 @ 15fps */
+	if (fie->width == 640 && fie->height == 480) {
+		fie->interval.numerator = 1;
+		fie->interval.denominator = 30;
+	} else if (fie->width == 1280 && fie->height == 1024) {
+		fie->interval.numerator = 1;
+		fie->interval.denominator = 15;
+	} else {
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+static int mt9m113_ifp_g_frame_interval(struct v4l2_subdev *sd,
+					struct v4l2_subdev_state *state,
+					struct v4l2_subdev_frame_interval *fi)
+{
+	struct v4l2_mbus_framefmt *format;
+
+	/* Frame intervals are only defined on the source pad. */
+	if (fi->pad != 1)
+		return -EINVAL;
+
+	format = v4l2_subdev_state_get_format(state, 1);
+
+	/* Return interval based on current resolution */
+	if (format->width <= 640 && format->height <= 480) {
+		/* Context A: 30fps */
+		fi->interval.numerator = 1;
+		fi->interval.denominator = 30;
+	} else {
+		/* Context B: 15fps */
+		fi->interval.numerator = 1;
+		fi->interval.denominator = 15;
+	}
+
+	return 0;
+}
+
+static int mt9m113_ifp_set_fmt(struct v4l2_subdev *sd,
+			       struct v4l2_subdev_state *state,
+			       struct v4l2_subdev_format *fmt)
+{
+	struct mt9m113 *sensor = ifp_to_mt9m113(sd);
+	struct v4l2_mbus_framefmt *format;
+	struct v4l2_rect *compose;
+	const struct mt9m113_format_info *info;
+
+	/*
+	 * Reject geometry/format changes while the pipeline is live.
+	 * The sensor is only programmed in mt9m113_stream_on(); silently
+	 * updating the active state would let userspace see one format while
+	 * the wire still carries the previous one.
+	 */
+	if (sensor->streaming)
+		return -EBUSY;
+
+	/* Sink pad format is fixed */
+	if (fmt->pad == 0) {
+		format = v4l2_subdev_state_get_format(state, 0);
+		fmt->format = *format;
+		return 0;
+	}
+
+	/* Source pad */
+	info = mt9m113_format_info(fmt->format.code);
+
+	/* Clamp to supported sizes (Context A or Context B) */
+	if (fmt->format.width <= 640) {
+		fmt->format.width = 640;
+		fmt->format.height = 480;
+	} else {
+		fmt->format.width = 1280;
+		fmt->format.height = 1024;
+	}
+
+	format = v4l2_subdev_state_get_format(state, 1);
+	format->width = fmt->format.width;
+	format->height = fmt->format.height;
+	format->code = info->code;
+	format->field = V4L2_FIELD_NONE;
+	format->colorspace = V4L2_COLORSPACE_SRGB;
+
+	compose = v4l2_subdev_state_get_compose(state, 0);
+	compose->width = format->width;
+	compose->height = format->height;
+
+	fmt->format = *format;
+	return 0;
+}
+
+static int mt9m113_ifp_get_selection(struct v4l2_subdev *sd,
+				     struct v4l2_subdev_state *state,
+				     struct v4l2_subdev_selection *sel)
+{
+	if (sel->pad != 0)
+		return -EINVAL;
+
+	switch (sel->target) {
+	case V4L2_SEL_TGT_CROP:
+		sel->r = *v4l2_subdev_state_get_crop(state, 0);
+		return 0;
+
+	case V4L2_SEL_TGT_COMPOSE:
+		sel->r = *v4l2_subdev_state_get_compose(state, 0);
+		return 0;
+
+	case V4L2_SEL_TGT_CROP_DEFAULT:
+	case V4L2_SEL_TGT_CROP_BOUNDS:
+		sel->r.left = 0;
+		sel->r.top = 0;
+		sel->r.width = MT9M113_PIXEL_ARRAY_WIDTH;
+		sel->r.height = MT9M113_PIXEL_ARRAY_HEIGHT;
+		return 0;
+
+	case V4L2_SEL_TGT_COMPOSE_DEFAULT:
+	case V4L2_SEL_TGT_COMPOSE_BOUNDS:
+		sel->r.left = 0;
+		sel->r.top = 0;
+		sel->r.width = MT9M113_PIXEL_ARRAY_WIDTH;
+		sel->r.height = MT9M113_PIXEL_ARRAY_HEIGHT;
+		return 0;
+
+	default:
+		return -EINVAL;
+	}
+}
+
+/* IFP registered callback - registers PA and creates link */
+static void mt9m113_ifp_unregistered(struct v4l2_subdev *sd)
+{
+	struct mt9m113 *sensor = ifp_to_mt9m113(sd);
+
+	v4l2_device_unregister_subdev(&sensor->pa.sd);
+}
+
+static int mt9m113_ifp_registered(struct v4l2_subdev *sd)
+{
+	struct mt9m113 *sensor = ifp_to_mt9m113(sd);
+	int ret;
+
+	ret = v4l2_device_register_subdev(sd->v4l2_dev, &sensor->pa.sd);
+	if (ret < 0) {
+		dev_err(&sensor->client->dev,
+			"Failed to register pixel array subdev\n");
+		return ret;
+	}
+
+	ret = media_create_pad_link(&sensor->pa.sd.entity, 0,
+				    &sensor->ifp.sd.entity, 0,
+				    MEDIA_LNK_FL_ENABLED |
+				    MEDIA_LNK_FL_IMMUTABLE);
+	if (ret < 0) {
+		dev_err(&sensor->client->dev,
+			"Failed to link pixel array to ifp\n");
+		v4l2_device_unregister_subdev(&sensor->pa.sd);
+		return ret;
+	}
+
+	return 0;
+}
+
+static const struct v4l2_subdev_video_ops mt9m113_ifp_video_ops = {
+	.s_stream = mt9m113_ifp_s_stream,
+};
+
+static const struct v4l2_subdev_pad_ops mt9m113_ifp_pad_ops = {
+	.enum_mbus_code = mt9m113_ifp_enum_mbus_code,
+	.enum_frame_size = mt9m113_ifp_enum_frame_size,
+	.enum_frame_interval = mt9m113_ifp_enum_frame_interval,
+	.get_fmt = v4l2_subdev_get_fmt,
+	.set_fmt = mt9m113_ifp_set_fmt,
+	.get_frame_interval = mt9m113_ifp_g_frame_interval,
+	.get_selection = mt9m113_ifp_get_selection,
+};
+
+static const struct v4l2_subdev_ops mt9m113_ifp_ops = {
+	.video = &mt9m113_ifp_video_ops,
+	.pad = &mt9m113_ifp_pad_ops,
+};
+
+static const struct v4l2_subdev_internal_ops mt9m113_ifp_internal_ops = {
+	.init_state = mt9m113_ifp_init_state,
+	.registered = mt9m113_ifp_registered,
+	.unregistered = mt9m113_ifp_unregistered,
+};
+
+/* -----------------------------------------------------------------------------
+ * Controls
+ */
+
+static const char * const mt9m113_test_pattern_menu[] = {
+	"Disabled",
+	"Solid Color",
+	"Color Bars",
+	"Fade to Gray",
+};
+
+static const u8 mt9m113_test_pattern_value[] = {
+	/* Values written to CAM_MODE_TEST_PATTERN_SELECT (indexed by menu - 1) */
+	MT9M113_TEST_PATTERN_SOLID_COLOR,
+	MT9M113_TEST_PATTERN_COLOR_BARS,
+	MT9M113_TEST_PATTERN_FADE_TO_GRAY,
+};
+
+static int mt9m113_s_ctrl(struct v4l2_ctrl *ctrl)
+{
+	struct mt9m113 *sensor = container_of(ctrl->handler,
+					       struct mt9m113, ifp.hdl);
+	int ret = 0;
+
+	/*
+	 * pm_runtime_get_if_in_use() returns >0 on success, 0 if not active,
+	 * and -EINVAL if runtime PM is disabled.  Both 0 and -EINVAL mean we
+	 * must not touch the hardware (and must not pair with a put), so the
+	 * defensive comparison is "<= 0" rather than the naive "!ret".
+	 */
+	if (pm_runtime_get_if_in_use(&sensor->client->dev) <= 0)
+		return 0;
+
+	switch (ctrl->id) {
+	case V4L2_CID_HFLIP: {
+		u64 mode_a, mode_b;
+
+		ret = mt9m113_read_mcu_var(sensor, MT9M113_SENSOR_READ_MODE_A,
+					   &mode_a);
+		if (ret)
+			break;
+		ret = mt9m113_read_mcu_var(sensor, MT9M113_SENSOR_READ_MODE_B,
+					   &mode_b);
+		if (ret)
+			break;
+
+		if (ctrl->val) {
+			mode_a |= MT9M113_SENSOR_READ_MODE_HMIRROR;
+			mode_b |= MT9M113_SENSOR_READ_MODE_HMIRROR;
+		} else {
+			mode_a &= ~MT9M113_SENSOR_READ_MODE_HMIRROR;
+			mode_b &= ~MT9M113_SENSOR_READ_MODE_HMIRROR;
+		}
+
+		ret = mt9m113_write_mcu_var(sensor, MT9M113_SENSOR_READ_MODE_A,
+					    mode_a);
+		if (!ret)
+			ret = mt9m113_write_mcu_var(sensor,
+						    MT9M113_SENSOR_READ_MODE_B,
+						    mode_b);
+		/* Only refresh if streaming - otherwise MCU may not be ready */
+		if (!ret && sensor->streaming)
+			mt9m113_refresh(sensor);
+		break;
+	}
+
+	case V4L2_CID_VFLIP: {
+		u64 mode_a, mode_b;
+
+		ret = mt9m113_read_mcu_var(sensor, MT9M113_SENSOR_READ_MODE_A,
+					   &mode_a);
+		if (ret)
+			break;
+		ret = mt9m113_read_mcu_var(sensor, MT9M113_SENSOR_READ_MODE_B,
+					   &mode_b);
+		if (ret)
+			break;
+
+		if (ctrl->val) {
+			mode_a |= MT9M113_SENSOR_READ_MODE_VMIRROR;
+			mode_b |= MT9M113_SENSOR_READ_MODE_VMIRROR;
+		} else {
+			mode_a &= ~MT9M113_SENSOR_READ_MODE_VMIRROR;
+			mode_b &= ~MT9M113_SENSOR_READ_MODE_VMIRROR;
+		}
+
+		ret = mt9m113_write_mcu_var(sensor, MT9M113_SENSOR_READ_MODE_A,
+					    mode_a);
+		if (!ret)
+			ret = mt9m113_write_mcu_var(sensor,
+						    MT9M113_SENSOR_READ_MODE_B,
+						    mode_b);
+		/* Only refresh if streaming - otherwise MCU may not be ready */
+		if (!ret && sensor->streaming)
+			mt9m113_refresh(sensor);
+		break;
+	}
+
+	case V4L2_CID_COLORFX: {
+		u16 effect;
+
+		switch (ctrl->val) {
+		case V4L2_COLORFX_NONE:
+			effect = MT9M113_SPEC_EFFECTS_NONE;
+			break;
+		case V4L2_COLORFX_BW:
+			effect = MT9M113_SPEC_EFFECTS_MONOCHROME;
+			break;
+		case V4L2_COLORFX_SEPIA:
+			effect = MT9M113_SPEC_EFFECTS_SEPIA;
+			break;
+		case V4L2_COLORFX_NEGATIVE:
+			effect = MT9M113_SPEC_EFFECTS_NEGATIVE;
+			break;
+		case V4L2_COLORFX_SOLARIZATION:
+			effect = MT9M113_SPEC_EFFECTS_SOLARIZE;
+			break;
+		default:
+			ret = -EINVAL;
+			break;
+		}
+
+		if (!ret) {
+			effect |= (MT9M113_SPEC_EFFECTS_DEFAULT &
+				   ~MT9M113_SPEC_EFFECTS_MASK);
+			ret = mt9m113_write_mcu_var(sensor,
+						    MT9M113_MODE_SPEC_EFFECTS_A,
+						    effect);
+			if (!ret)
+				ret = mt9m113_write_mcu_var(sensor,
+							    MT9M113_MODE_SPEC_EFFECTS_B,
+							    effect);
+
+			/* Only refresh if streaming - otherwise MCU may not be ready */
+			if (!ret && sensor->streaming)
+				mt9m113_refresh(sensor);
+		}
+		break;
+	}
+
+	case V4L2_CID_POWER_LINE_FREQUENCY: {
+		u8 fd_mode;
+
+		switch (ctrl->val) {
+		case V4L2_CID_POWER_LINE_FREQUENCY_DISABLED:
+			fd_mode = MT9M113_FD_MODE_DISABLED;
+			break;
+		case V4L2_CID_POWER_LINE_FREQUENCY_50HZ:
+			fd_mode = MT9M113_FD_MODE_50HZ;
+			break;
+		case V4L2_CID_POWER_LINE_FREQUENCY_60HZ:
+			fd_mode = MT9M113_FD_MODE_60HZ;
+			break;
+		case V4L2_CID_POWER_LINE_FREQUENCY_AUTO:
+			fd_mode = MT9M113_FD_MODE_AUTO;
+			break;
+		default:
+			ret = -EINVAL;
+			break;
+		}
+
+		if (!ret)
+			ret = mt9m113_write_mcu_var(sensor, MT9M113_FD_MODE,
+						    fd_mode);
+		break;
+	}
+
+	case V4L2_CID_SATURATION:
+		ret = mt9m113_write_mcu_var(sensor, MT9M113_AWB_SATURATION,
+					    ctrl->val);
+		/* Only refresh if streaming - otherwise MCU may not be ready */
+		if (!ret && sensor->streaming)
+			mt9m113_refresh(sensor);
+		break;
+
+	case V4L2_CID_EXPOSURE_AUTO:
+		/* Enable/disable internal AE algorithm via AE_GATE */
+		if (ctrl->val == V4L2_EXPOSURE_AUTO)
+			ret = mt9m113_write_mcu_var(sensor, MT9M113_AE_GATE,
+						    MT9M113_AE_GATE_ENABLE);
+		else
+			ret = mt9m113_write_mcu_var(sensor, MT9M113_AE_GATE,
+						    MT9M113_AE_GATE_DISABLE);
+		break;
+
+	case V4L2_CID_EXPOSURE:
+		/* Write coarse integration time to both contexts */
+		ret = cci_write(sensor->regmap, MT9M113_COARSE_IT_TIME_A,
+				ctrl->val, NULL);
+		if (!ret)
+			ret = cci_write(sensor->regmap, MT9M113_COARSE_IT_TIME_B,
+					ctrl->val, NULL);
+		break;
+
+	case V4L2_CID_ANALOGUE_GAIN:
+		/* Write analog gain register */
+		ret = cci_write(sensor->regmap, MT9M113_ANALOG_GAIN,
+				ctrl->val, NULL);
+		break;
+
+	case V4L2_CID_AUTO_WHITE_BALANCE:
+		/* Enable/disable internal AWB via awb_mode */
+		ret = mt9m113_write_mcu_var(sensor, MT9M113_AWB_MODE,
+					    ctrl->val ? 0x02 : 0x00);
+		break;
+
+	case V4L2_CID_TEST_PATTERN:
+		if (ctrl->val == 0) {
+			/*
+			 * Disable test pattern. Only restart MCU if test
+			 * pattern was previously active. On first call
+			 * (ctrl_handler_setup default), the MCU is already
+			 * in normal mode - restarting it here would disrupt
+			 * the MCU state and cause REFRESH timeouts.
+			 *
+			 * Keep test_pattern_active set until both writes
+			 * succeed: a half-applied disable leaves the MCU
+			 * halted, and a subsequent CID=0 must be allowed to
+			 * retry the full sequence.
+			 */
+			if (sensor->test_pattern_active) {
+				dev_dbg(&sensor->client->dev,
+					 "MT9M113: Disabling test pattern, restarting MCU\n");
+				cci_write(sensor->regmap, MT9M113_MCU_BOOT_MODE,
+					  0x0000, &ret);
+				if (!ret) {
+					usleep_range(10000, 15000);
+					ret = mt9m113_write_mcu_var(sensor,
+								    MT9M113_CAM_MODE_SELECT,
+								    MT9M113_CAM_MODE_SELECT_NORMAL);
+				}
+				if (!ret)
+					sensor->test_pattern_active = false;
+			}
+		} else {
+			/*
+			 * Enable test pattern mode.
+			 * Per datasheet: "Disabling the MCU is recommended
+			 * before enabling test patterns."
+			 *
+			 * Sequence:
+			 * 1. Configure test pattern via MCU variables (MCU running)
+			 * 2. Issue refresh to apply settings
+			 * 3. Hold MCU in boot mode to prevent override - but only
+			 *    while streaming.  If the MCU is halted outside of an
+			 *    active session, a STREAMON inside the autosuspend
+			 *    window finds a wedged MCU and the in-flight REFRESH
+			 *    / sequencer commands time out; the next runtime
+			 *    resume's power_on + sensor_init clears it instead.
+			 */
+			sensor->test_pattern_active = true;
+			dev_dbg(&sensor->client->dev,
+				 "MT9M113: Enabling test pattern %d\n", ctrl->val);
+			ret = mt9m113_write_mcu_var(sensor,
+						    MT9M113_CAM_MODE_TEST_PATTERN_SELECT,
+						    mt9m113_test_pattern_value[ctrl->val - 1]);
+			if (!ret)
+				ret = mt9m113_write_mcu_var(sensor,
+							    MT9M113_CAM_MODE_SELECT,
+							    MT9M113_CAM_MODE_SELECT_TEST_PATTERN);
+			if (!ret && sensor->streaming) {
+				mt9m113_refresh(sensor);
+				dev_dbg(&sensor->client->dev,
+					 "MT9M113: Stopping MCU for test pattern\n");
+				cci_write(sensor->regmap, MT9M113_MCU_BOOT_MODE,
+					  0x0001, &ret);
+			}
+		}
+		break;
+
+	default:
+		ret = -EINVAL;
+		break;
+	}
+
+	pm_runtime_put_autosuspend(&sensor->client->dev);
+	return ret;
+}
+
+static const struct v4l2_ctrl_ops mt9m113_ctrl_ops = {
+	.s_ctrl = mt9m113_s_ctrl,
+};
+
+/* -----------------------------------------------------------------------------
+ * Power Management
+ */
+
+static int mt9m113_power_on(struct mt9m113 *sensor)
+{
+	struct device *dev = &sensor->client->dev;
+	int ret;
+
+	ret = regulator_bulk_enable(ARRAY_SIZE(sensor->supplies),
+				    sensor->supplies);
+	if (ret < 0)
+		return ret;
+
+	/*
+	 * Aptina/onsemi SOC sensors require EXTCLK to be running before
+	 * STANDBY (powerdown) is released; otherwise the internal
+	 * clock-domain state machines come out of reset non-deterministically
+	 * and the MCU can wedge on SEQ_CMD.  Enable the clock first, let the
+	 * supplies and clock settle, then deassert powerdown.
+	 */
+	ret = clk_prepare_enable(sensor->clk);
+	if (ret < 0)
+		goto error_regulator;
+
+	usleep_range(20000, 25000);
+
+	if (sensor->powerdown)
+		gpiod_set_value_cansleep(sensor->powerdown, 0);
+
+	msleep(20);
+
+	if (sensor->reset) {
+		gpiod_set_value_cansleep(sensor->reset, 1);
+		usleep_range(1000, 2000);
+		gpiod_set_value_cansleep(sensor->reset, 0);
+		usleep_range(44500, 50000);
+	} else if (sensor->powerdown) {
+		usleep_range(44500, 50000);
+	}
+
+	/* MT9M113 MCU boot sequence */
+	{
+		u64 clocks_val = 0;
+
+		cci_read(sensor->regmap, MT9M113_CLOCKS_CONTROL, &clocks_val, NULL);
+		if (clocks_val != 0) {
+			u64 seq_cmd = 0;
+			int read_ret;
+
+			/*
+			 * Sensor already has clocks running (warm reboot or
+			 * resume). Check if MCU is responsive by reading
+			 * SEQ_CMD. If stuck (non-zero) or if the read itself
+			 * failed (I2C error), force the soft-reset path instead
+			 * of silently returning success on an uninitialised
+			 * sensor.
+			 */
+			read_ret = mt9m113_read_mcu_var(sensor, MT9M113_SEQ_CMD,
+							&seq_cmd);
+			if (read_ret == 0 && seq_cmd == 0) {
+				dev_dbg(dev, "MT9M113 already initialized, MCU OK\n");
+				msleep(50);
+				return 0;
+			}
+			if (read_ret < 0)
+				dev_warn(dev,
+					 "MT9M113: SEQ_CMD read failed (%d), forcing soft reset\n",
+					 read_ret);
+			else
+				dev_warn(dev,
+					 "MT9M113: MCU stuck (SEQ_CMD=0x%llx), forcing soft reset\n",
+					 seq_cmd);
+		}
+
+		/* Soft reset */
+		cci_write(sensor->regmap, MT9M113_RESET_AND_MISC_CONTROL,
+			  MT9M113_RESET_SOC, &ret);
+		cci_write(sensor->regmap, MT9M113_RESET_AND_MISC_CONTROL,
+			  0, &ret);
+		if (ret < 0)
+			goto error_clock;
+		msleep(200);
+
+		/* Boot MCU */
+		cci_write(sensor->regmap, MT9M113_MCU_BOOT_MODE, 0x0001, &ret);
+		usleep_range(1000, 2000);
+		cci_write(sensor->regmap, MT9M113_MCU_BOOT_MODE, 0x0000, &ret);
+		if (ret < 0)
+			goto error_clock;
+		msleep(200);
+
+		/* Configure PLL */
+		cci_write(sensor->regmap, MT9M113_CLOCKS_CONTROL, 0x00FF, &ret);
+		cci_write(sensor->regmap, MT9M113_STANDBY_CONTROL,
+			  MT9M113_STANDBY_CONTROL_ACTIVE, &ret);
+		cci_write(sensor->regmap, MT9M113_PLL_CONTROL, 0x2145, &ret);
+		cci_write(sensor->regmap, MT9M113_PLL_CONTROL, 0x2145, &ret);
+		cci_write(sensor->regmap, MT9M113_PLL_CONTROL, 0x2145, &ret);
+		cci_write(sensor->regmap, MT9M113_PLL_DIVIDERS, 0x0114, &ret);
+		cci_write(sensor->regmap, MT9M113_PLL_P_DIVIDERS, 0x00F1, &ret);
+		cci_write(sensor->regmap, MT9M113_PLL_CONTROL, 0x2545, &ret);
+		cci_write(sensor->regmap, MT9M113_PLL_CONTROL, 0x2547, &ret);
+		cci_write(sensor->regmap, MT9M113_PLL_CONTROL, 0x3447, &ret);
+		if (ret < 0)
+			goto error_clock;
+		msleep(20);
+		cci_write(sensor->regmap, MT9M113_PLL_CONTROL, 0x3047, &ret);
+		cci_write(sensor->regmap, MT9M113_PLL_CONTROL, 0x3046, &ret);
+		cci_write(sensor->regmap, MT9M113_RESET_AND_MISC_CONTROL, 0x0218, &ret);
+		/* Keep the MCU active (do not enter standby 0x002A) after power-on. */
+		cci_write(sensor->regmap, MT9M113_STANDBY_CONTROL,
+			  MT9M113_STANDBY_CONTROL_ACTIVE, &ret);
+		if (ret < 0)
+			goto error_clock;
+		msleep(50);
+
+		/* Configure OFIFO */
+		cci_write(sensor->regmap, MT9M113_OFIFO_CONTROL_STATUS, 0x0003, &ret);
+		if (ret < 0)
+			goto error_clock;
+	}
+
+	return 0;
+
+error_clock:
+	clk_disable_unprepare(sensor->clk);
+error_regulator:
+	if (sensor->powerdown)
+		gpiod_set_value_cansleep(sensor->powerdown, 1);
+	else if (sensor->reset)
+		gpiod_set_value_cansleep(sensor->reset, 1);
+	regulator_bulk_disable(ARRAY_SIZE(sensor->supplies),
+			       sensor->supplies);
+	return ret;
+}
+
+static void mt9m113_power_off(struct mt9m113 *sensor)
+{
+	/*
+	 * Symmetric with mt9m113_power_on(): hold the chip in reset, then
+	 * gate the clock and drop the supplies so the enable/disable refcount
+	 * stays balanced across runtime-PM cycles and the MCU is fully reset.
+	 *
+	 * Prefer the powerdown GPIO when available; fall back to driving
+	 * RESET_BAR active so it is not left floating after VDD drops on
+	 * boards that only wire reset-gpios.  Mirror the settling gaps from
+	 * power_on so the MCU sees a clean down-then-up sequence across
+	 * runtime-PM cycles.
+	 */
+	if (sensor->powerdown)
+		gpiod_set_value_cansleep(sensor->powerdown, 1);
+	else if (sensor->reset)
+		gpiod_set_value_cansleep(sensor->reset, 1);
+	usleep_range(1000, 2000);
+	clk_disable_unprepare(sensor->clk);
+	usleep_range(1000, 2000);
+	regulator_bulk_disable(ARRAY_SIZE(sensor->supplies),
+			       sensor->supplies);
+}
+
+static int mt9m113_runtime_resume(struct device *dev)
+{
+	struct v4l2_subdev *sd = dev_get_drvdata(dev);
+	struct mt9m113 *sensor = ifp_to_mt9m113(sd);
+	int ret;
+
+	/*
+	 * Always power-cycle and re-initialise on resume, including when a
+	 * powerdown GPIO is present. The MT9M113 MCU must be hardware-reset
+	 * (powerdown + clock) and re-initialised on each session, otherwise a
+	 * wedged MCU (SEQ_CMD/0xA103 stuck) is never recovered and only a
+	 * physical power cycle helps - matching the legacy vendor kernel per-open behaviour.
+	 */
+	ret = mt9m113_power_on(sensor);
+	if (ret)
+		return ret;
+
+	ret = mt9m113_sensor_init(sensor);
+	if (ret) {
+		/*
+		 * sensor_init() can fail (-ETIMEDOUT on an MCU lockup, I2C
+		 * errors); on that path the PM core reverts runtime_status to
+		 * SUSPENDED without touching driver-side state, so the
+		 * clk/regulator refcounts taken in power_on() would otherwise
+		 * leak and the next resume would double-enable them.
+		 */
+		mt9m113_power_off(sensor);
+		return ret;
+	}
+
+	return 0;
+}
+
+static int mt9m113_runtime_suspend(struct device *dev)
+{
+	struct v4l2_subdev *sd = dev_get_drvdata(dev);
+	struct mt9m113 *sensor = ifp_to_mt9m113(sd);
+
+	/* Power down (powerdown GPIO + clock off) so the MCU is reset next resume. */
+	mt9m113_power_off(sensor);
+	return 0;
+}
+
+static DEFINE_RUNTIME_DEV_PM_OPS(mt9m113_pm_ops,
+				 mt9m113_runtime_suspend,
+				 mt9m113_runtime_resume,
+				 NULL);
+
+/* -----------------------------------------------------------------------------
+ * Probe & Remove
+ */
+
+static int mt9m113_identify(struct mt9m113 *sensor)
+{
+	u64 value;
+	int ret;
+
+	ret = cci_read(sensor->regmap, MT9M113_CHIP_ID, &value, NULL);
+	if (ret) {
+		dev_err(&sensor->client->dev, "Failed to read chip ID\n");
+		return -ENXIO;
+	}
+
+	if (value != MT9M113_CHIP_ID_VALUE) {
+		dev_err(&sensor->client->dev,
+			"Invalid chip ID 0x%04llx (expected 0x%04x)\n",
+			value, MT9M113_CHIP_ID_VALUE);
+		return -ENXIO;
+	}
+
+	dev_dbg(&sensor->client->dev, "MT9M113 detected (ID 0x%04llx)\n",
+		 value);
+	return 0;
+}
+
+static int mt9m113_parse_dt(struct mt9m113 *sensor)
+{
+	struct fwnode_handle *fwnode = dev_fwnode(&sensor->client->dev);
+	struct fwnode_handle *ep;
+	int ret;
+
+	ep = fwnode_graph_get_next_endpoint(fwnode, NULL);
+	if (!ep)
+		return -EINVAL;
+
+	sensor->bus_cfg.bus_type = V4L2_MBUS_UNKNOWN;
+	ret = v4l2_fwnode_endpoint_alloc_parse(ep, &sensor->bus_cfg);
+	fwnode_handle_put(ep);
+	if (ret < 0)
+		return ret;
+
+	if (sensor->bus_cfg.bus_type != V4L2_MBUS_CSI2_DPHY) {
+		dev_err(&sensor->client->dev, "Unsupported bus type %d\n",
+			sensor->bus_cfg.bus_type);
+		v4l2_fwnode_endpoint_free(&sensor->bus_cfg);
+		return -EINVAL;
+	}
+
+	if (sensor->bus_cfg.nr_of_link_frequencies < 1) {
+		dev_err(&sensor->client->dev, "no link-frequencies specified\n");
+		v4l2_fwnode_endpoint_free(&sensor->bus_cfg);
+		return -EINVAL;
+	}
+
+	/*
+	 * data-lanes is required by the binding, but defend against a DTS that
+	 * passes dt_binding_check yet leaves num_data_lanes at zero: the pixel
+	 * rate is derived from it and a zero would be programmed into
+	 * V4L2_CID_PIXEL_RATE, which receivers like camss use to size the
+	 * CSI-2 link budget.
+	 */
+	if (sensor->bus_cfg.bus.mipi_csi2.num_data_lanes < 1) {
+		dev_err(&sensor->client->dev, "data-lanes missing or zero\n");
+		v4l2_fwnode_endpoint_free(&sensor->bus_cfg);
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+static int mt9m113_probe(struct i2c_client *client)
+{
+	struct device *dev = &client->dev;
+	struct mt9m113 *sensor;
+	int ret;
+
+	sensor = devm_kzalloc(dev, sizeof(*sensor), GFP_KERNEL);
+	if (!sensor)
+		return -ENOMEM;
+
+	sensor->client = client;
+
+	sensor->regmap = devm_cci_regmap_init_i2c(client, 16);
+	if (IS_ERR(sensor->regmap))
+		return PTR_ERR(sensor->regmap);
+
+	ret = mt9m113_parse_dt(sensor);
+	if (ret < 0)
+		return ret;
+
+	sensor->clk = devm_clk_get(dev, NULL);
+	if (IS_ERR(sensor->clk)) {
+		ret = PTR_ERR(sensor->clk);
+		goto error_ep_free;
+	}
+
+	sensor->reset = devm_gpiod_get_optional(dev, "reset", GPIOD_OUT_LOW);
+	if (IS_ERR(sensor->reset)) {
+		ret = PTR_ERR(sensor->reset);
+		goto error_ep_free;
+	}
+
+	sensor->powerdown = devm_gpiod_get_optional(dev, "powerdown",
+						    GPIOD_OUT_HIGH);
+	if (IS_ERR(sensor->powerdown)) {
+		ret = PTR_ERR(sensor->powerdown);
+		goto error_ep_free;
+	}
+
+	sensor->supplies[0].supply = "vddio";
+	sensor->supplies[1].supply = "vdd";
+	sensor->supplies[2].supply = "vaa";
+
+	ret = devm_regulator_bulk_get(dev, ARRAY_SIZE(sensor->supplies),
+				      sensor->supplies);
+	if (ret < 0)
+		goto error_ep_free;
+
+	/* Power on and identify */
+	ret = mt9m113_power_on(sensor);
+	if (ret < 0)
+		goto error_ep_free;
+
+	ret = mt9m113_identify(sensor);
+	if (ret < 0)
+		goto error_power_off;
+
+	ret = mt9m113_sensor_init(sensor);
+	if (ret < 0)
+		goto error_power_off;
+
+	/*
+	 * The CSI-2 link frequency is mandatory (enforced by the binding and
+	 * mt9m113_parse_dt()). Derive the pixel rate from it: every supported
+	 * source format is 16 bits/pixel on the bus and the D-PHY clocks data
+	 * DDR, so pixel_rate = link_freq * 2 * lanes / bpp.
+	 */
+	sensor->link_freq = sensor->bus_cfg.link_frequencies[0];
+	sensor->pixrate = div_u64(2ULL * sensor->link_freq *
+				  sensor->bus_cfg.bus.mipi_csi2.num_data_lanes,
+				  16);
+
+	dev_dbg(dev, "MT9M113: link freq %lld Hz, pixel rate %u Hz\n",
+		sensor->link_freq, sensor->pixrate);
+
+	/* Initialize Pixel Array subdev */
+	v4l2_subdev_init(&sensor->pa.sd, &mt9m113_pa_ops);
+	sensor->pa.sd.internal_ops = &mt9m113_pa_internal_ops;
+	v4l2_i2c_subdev_set_name(&sensor->pa.sd, client, NULL, " pixel array");
+	sensor->pa.sd.flags |= V4L2_SUBDEV_FL_HAS_DEVNODE;
+	sensor->pa.sd.owner = THIS_MODULE;
+	sensor->pa.sd.dev = dev;
+	v4l2_set_subdevdata(&sensor->pa.sd, client);
+	sensor->pa.sd.entity.function = MEDIA_ENT_F_CAM_SENSOR;
+	sensor->pa.pad.flags = MEDIA_PAD_FL_SOURCE;
+	ret = media_entity_pads_init(&sensor->pa.sd.entity, 1, &sensor->pa.pad);
+	if (ret < 0)
+		goto error_pa_entity;
+
+	/*
+	 * LINK_FREQ and PIXEL_RATE live on the pixel-array subdev because
+	 * receivers walk the media graph upstream until they reach the
+	 * MEDIA_ENT_F_CAM_SENSOR entity and read these controls from that
+	 * subdev's handler (see camss_find_sensor_pad() / v4l2_get_link_freq()
+	 * in drivers/media/platform/qcom/camss/camss.c). The IFP is a
+	 * MEDIA_ENT_F_PROC_VIDEO_ISP and the walk does not stop there, so
+	 * controls on the IFP alone are invisible to the receiver and the
+	 * pipeline fails at CSIPHY stream-on with -EINVAL.
+	 */
+	v4l2_ctrl_handler_init(&sensor->pa.hdl, 2);
+	{
+		struct v4l2_ctrl *link_freq_ctrl;
+
+		link_freq_ctrl = v4l2_ctrl_new_int_menu(&sensor->pa.hdl, NULL,
+				V4L2_CID_LINK_FREQ,
+				sensor->bus_cfg.nr_of_link_frequencies - 1, 0,
+				sensor->bus_cfg.link_frequencies);
+		if (link_freq_ctrl)
+			link_freq_ctrl->flags |= V4L2_CTRL_FLAG_READ_ONLY;
+	}
+	v4l2_ctrl_new_std(&sensor->pa.hdl, NULL, V4L2_CID_PIXEL_RATE,
+			  sensor->pixrate, sensor->pixrate, 1, sensor->pixrate);
+	if (sensor->pa.hdl.error) {
+		ret = sensor->pa.hdl.error;
+		goto error_pa_handler;
+	}
+	sensor->pa.sd.ctrl_handler = &sensor->pa.hdl;
+	sensor->pa.sd.state_lock = sensor->pa.hdl.lock;
+
+	ret = v4l2_subdev_init_finalize(&sensor->pa.sd);
+	if (ret < 0)
+		goto error_pa_handler;
+
+	/* Initialize IFP subdev */
+	v4l2_i2c_subdev_init(&sensor->ifp.sd, client, &mt9m113_ifp_ops);
+	v4l2_i2c_subdev_set_name(&sensor->ifp.sd, client, NULL, " ifp");
+	sensor->ifp.sd.flags |= V4L2_SUBDEV_FL_HAS_DEVNODE;
+	sensor->ifp.sd.internal_ops = &mt9m113_ifp_internal_ops;
+	sensor->ifp.sd.entity.function = MEDIA_ENT_F_PROC_VIDEO_ISP;
+	sensor->ifp.pads[0].flags = MEDIA_PAD_FL_SINK;
+	sensor->ifp.pads[1].flags = MEDIA_PAD_FL_SOURCE;
+	ret = media_entity_pads_init(&sensor->ifp.sd.entity, 2, sensor->ifp.pads);
+	if (ret < 0)
+		goto error_ifp_entity;
+
+	/* Initialize controls on IFP */
+	v4l2_ctrl_handler_init(&sensor->ifp.hdl, 10);
+	v4l2_ctrl_new_std(&sensor->ifp.hdl, &mt9m113_ctrl_ops,
+			  V4L2_CID_HFLIP, 0, 1, 1, 0);
+	v4l2_ctrl_new_std(&sensor->ifp.hdl, &mt9m113_ctrl_ops,
+			  V4L2_CID_VFLIP, 0, 1, 1, 0);
+	/* COLORFX: mask out unsupported effects 4-12 (EMBOSS through SILHOUETTE) */
+	v4l2_ctrl_new_std_menu(&sensor->ifp.hdl, &mt9m113_ctrl_ops,
+			       V4L2_CID_COLORFX,
+			       V4L2_COLORFX_SOLARIZATION, 0x1ff0,
+			       V4L2_COLORFX_NONE);
+	v4l2_ctrl_new_std_menu(&sensor->ifp.hdl, &mt9m113_ctrl_ops,
+			       V4L2_CID_POWER_LINE_FREQUENCY,
+			       V4L2_CID_POWER_LINE_FREQUENCY_AUTO, 0,
+			       V4L2_CID_POWER_LINE_FREQUENCY_AUTO);
+	v4l2_ctrl_new_std(&sensor->ifp.hdl, &mt9m113_ctrl_ops,
+			  V4L2_CID_SATURATION, 0, 255, 1, 128);
+	v4l2_ctrl_new_std_menu(&sensor->ifp.hdl, &mt9m113_ctrl_ops,
+			       V4L2_CID_EXPOSURE_AUTO,
+			       V4L2_EXPOSURE_MANUAL, 0,
+			       V4L2_EXPOSURE_AUTO);
+	v4l2_ctrl_new_std(&sensor->ifp.hdl, &mt9m113_ctrl_ops,
+			  V4L2_CID_EXPOSURE, 1, 1000, 1, 100);
+	v4l2_ctrl_new_std(&sensor->ifp.hdl, &mt9m113_ctrl_ops,
+			  V4L2_CID_ANALOGUE_GAIN, 0, 127, 1, 32);
+	v4l2_ctrl_new_std(&sensor->ifp.hdl, &mt9m113_ctrl_ops,
+			  V4L2_CID_AUTO_WHITE_BALANCE, 0, 1, 1, 1);
+	v4l2_ctrl_new_std_menu_items(&sensor->ifp.hdl, &mt9m113_ctrl_ops,
+				     V4L2_CID_TEST_PATTERN,
+				     ARRAY_SIZE(mt9m113_test_pattern_menu) - 1,
+				     0, 0, mt9m113_test_pattern_menu);
+
+	if (sensor->ifp.hdl.error) {
+		ret = sensor->ifp.hdl.error;
+		goto error_ifp_handler;
+	}
+
+	sensor->ifp.sd.ctrl_handler = &sensor->ifp.hdl;
+	sensor->ifp.sd.state_lock = sensor->ifp.hdl.lock;
+
+	ret = v4l2_subdev_init_finalize(&sensor->ifp.sd);
+	if (ret < 0)
+		goto error_ifp_handler;
+
+	/*
+	 * Enable runtime PM.  Configure autosuspend before enabling the
+	 * runtime PM core so a put issued between enable and use_autosuspend
+	 * cannot race with autosuspend being unconfigured.  The 2 s autosuspend
+	 * delay amortises the unconditional power-cycle + full init-table
+	 * replay in runtime_resume over typical preview/snapshot sequences:
+	 * the cold init is load-bearing for MCU-wedge recovery so it cannot
+	 * be conditionalised on health, only amortised.
+	 */
+	pm_runtime_set_active(dev);
+	pm_runtime_get_noresume(dev);
+	pm_runtime_set_autosuspend_delay(dev, 2000);
+	pm_runtime_use_autosuspend(dev);
+	pm_runtime_enable(dev);
+
+	/* Register only the IFP - PA will be registered in ifp_registered callback */
+	ret = v4l2_async_register_subdev(&sensor->ifp.sd);
+	if (ret < 0)
+		goto error_pm;
+
+	pm_runtime_put_autosuspend(dev);
+
+	dev_dbg(dev, "MT9M113 driver with IFP sub-device initialized\n");
+	return 0;
+
+error_pm:
+	pm_runtime_disable(dev);
+	pm_runtime_set_suspended(dev);
+	pm_runtime_dont_use_autosuspend(dev);
+	pm_runtime_put_noidle(dev);
+error_ifp_handler:
+	v4l2_subdev_cleanup(&sensor->ifp.sd);
+	v4l2_ctrl_handler_free(&sensor->ifp.hdl);
+error_ifp_entity:
+	media_entity_cleanup(&sensor->ifp.sd.entity);
+	v4l2_subdev_cleanup(&sensor->pa.sd);
+error_pa_handler:
+	v4l2_ctrl_handler_free(&sensor->pa.hdl);
+error_pa_entity:
+	media_entity_cleanup(&sensor->pa.sd.entity);
+error_power_off:
+	mt9m113_power_off(sensor);
+error_ep_free:
+	v4l2_fwnode_endpoint_free(&sensor->bus_cfg);
+	return ret;
+}
+
+static void mt9m113_remove(struct i2c_client *client)
+{
+	struct v4l2_subdev *sd = i2c_get_clientdata(client);
+	struct mt9m113 *sensor = ifp_to_mt9m113(sd);
+	struct device *dev = &client->dev;
+
+	v4l2_async_unregister_subdev(&sensor->ifp.sd);
+	v4l2_subdev_cleanup(&sensor->ifp.sd);
+	v4l2_ctrl_handler_free(&sensor->ifp.hdl);
+	media_entity_cleanup(&sensor->ifp.sd.entity);
+
+	v4l2_subdev_cleanup(&sensor->pa.sd);
+	v4l2_ctrl_handler_free(&sensor->pa.hdl);
+	media_entity_cleanup(&sensor->pa.sd.entity);
+
+	v4l2_fwnode_endpoint_free(&sensor->bus_cfg);
+
+	pm_runtime_disable(dev);
+	pm_runtime_dont_use_autosuspend(dev);
+	if (!pm_runtime_status_suspended(dev))
+		mt9m113_power_off(sensor);
+	pm_runtime_set_suspended(dev);
+}
+
+static const struct of_device_id mt9m113_of_ids[] = {
+	{ .compatible = "aptina,mt9m113" },
+	{ /* sentinel */ },
+};
+MODULE_DEVICE_TABLE(of, mt9m113_of_ids);
+
+static const struct i2c_device_id mt9m113_id[] = {
+	{ "mt9m113" },
+	{ /* sentinel */ },
+};
+MODULE_DEVICE_TABLE(i2c, mt9m113_id);
+
+static struct i2c_driver mt9m113_driver = {
+	.driver = {
+		.name	= "mt9m113",
+		.pm	= pm_ptr(&mt9m113_pm_ops),
+		.of_match_table = mt9m113_of_ids,
+	},
+	.probe		= mt9m113_probe,
+	.remove		= mt9m113_remove,
+	.id_table	= mt9m113_id,
+};
+
+module_i2c_driver(mt9m113_driver);
+
+MODULE_AUTHOR("Herman van Hazendonk <github.com@herrie.org>");
+MODULE_DESCRIPTION("Aptina MT9M113 Sensor Driver");
+MODULE_LICENSE("GPL");

-- 
2.43.0



^ permalink raw reply related	[flat|nested] 5+ messages in thread

* Re: [PATCH v3 1/2] dt-bindings: media: i2c: add aptina,mt9m113
  2026-06-05  7:03 ` [PATCH v3 1/2] dt-bindings: media: i2c: add aptina,mt9m113 Herman van Hazendonk via B4 Relay
@ 2026-06-05  7:11   ` sashiko-bot
  0 siblings, 0 replies; 5+ messages in thread
From: sashiko-bot @ 2026-06-05  7:11 UTC (permalink / raw)
  To: Herman van Hazendonk via B4 Relay; +Cc: robh, conor+dt, devicetree, linux-media

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] The `reset-gpios` and `powerdown-gpios` properties lack the `maxItems: 1` constraint.
--

commit 5ab8d70c4704ffb84f721fd8458f27f181d12b4a
Author: Herman van Hazendonk <github.com@herrie.org>

dt-bindings: media: i2c: add aptina,mt9m113

This commit introduces the device tree binding for the Aptina MT9M113 1.3
megapixel image sensor. It defines the required and optional properties,
including the I2C address, regulators, and GPIOs for reset and powerdown.

> diff --git a/Documentation/devicetree/bindings/media/i2c/aptina,mt9m113.yaml b/Documentation/devicetree/bindings/media/i2c/aptina,mt9m113.yaml
> new file mode 100644
> index 0000000000000..7fa7cb2fedf96
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/media/i2c/aptina,mt9m113.yaml
[ ... ]
> @@ -0,0 +1,127 @@
[ ... ]
> +  vaa-supply:
> +    description:
> +      Analog voltage supply, 2.8V
> +
> +  reset-gpios:
> +    description: |-
> +      Reference to the GPIO connected to the RESET_BAR pin, if any (active
> +      low).

[Severity: Low]
Does this property need a maxItems constraint? Since the MT9M113 hardware has
exactly one RESET_BAR pin, adding maxItems: 1 would prevent the schema from
silently permitting invalid device trees that specify multiple GPIOs here.

> +
> +  powerdown-gpios:
> +    description: |-
> +      Reference to the GPIO connected to the STANDBY/PWDN pin, if any (active
> +      high). When asserted, the sensor enters low-power standby mode.

[Severity: Low]
Should we also limit powerdown-gpios with a maxItems: 1 constraint to properly
match the single STANDBY/PWDN pin on the hardware?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260605-submit-media-mt9m113-v3-0-dc3e07d5d70b@herrie.org?part=1

^ permalink raw reply	[flat|nested] 5+ messages in thread

* Re: [PATCH v3 2/2] media: i2c: add Aptina MT9M113 1.3 Mpx SoC sensor driver
  2026-06-05  7:03 ` [PATCH v3 2/2] media: i2c: add Aptina MT9M113 1.3 Mpx SoC sensor driver Herman van Hazendonk via B4 Relay
@ 2026-06-05  7:16   ` sashiko-bot
  0 siblings, 0 replies; 5+ messages in thread
From: sashiko-bot @ 2026-06-05  7:16 UTC (permalink / raw)
  To: Herman van Hazendonk via B4 Relay; +Cc: conor+dt, devicetree, robh, linux-media

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] The test pattern implementation has critical state machine flaws that prevent it from activating on stream start and break the video stream when disabled.
- [High] V4L2 controls silently fail to apply when CONFIG_PM is disabled.
- [Medium] Modifying V4L2 controls while the test pattern is active causes 1-second stalls and false-positive timeout warnings.
--

commit 089f461b38f980bbf1bb0f31d1f94bb7bd1d0721
Author: Herman van Hazendonk <github.com@herrie.org>

media: i2c: add Aptina MT9M113 1.3 Mpx SoC sensor driver

This commit adds a V4L2 subdev driver for the Aptina MT9M113 1.3 Megapixel
SoC image sensor. It supports dual contexts (preview and capture), various
V4L2 controls, and test patterns. The driver also includes a workaround for
intermittent MCU wedges during stream start by using runtime-PM retries.

> diff --git a/drivers/media/i2c/mt9m113.c b/drivers/media/i2c/mt9m113.c
> new file mode 100644
> index 0000000000000..7a4a8a50822d3
> --- /dev/null
> +++ b/drivers/media/i2c/mt9m113.c
[ ... ]
> +static int mt9m113_s_ctrl(struct v4l2_ctrl *ctrl)
> +{
> +	struct mt9m113 *sensor = container_of(ctrl->handler,
> +					       struct mt9m113, ifp.hdl);
> +	int ret = 0;
> +
> +	/*
> +	 * pm_runtime_get_if_in_use() returns >0 on success, 0 if not active,
> +	 * and -EINVAL if runtime PM is disabled.  Both 0 and -EINVAL mean we
> +	 * must not touch the hardware (and must not pair with a put), so the
> +	 * defensive comparison is "<= 0" rather than the naive "!ret".
> +	 */
> +	if (pm_runtime_get_if_in_use(&sensor->client->dev) <= 0)
> +		return 0;

[Severity: High]
Does this code silently ignore V4L2 control updates when CONFIG_PM is
disabled?

When CONFIG_PM is not enabled, pm_runtime_get_if_in_use() will return
-EINVAL. By treating -EINVAL the same as an inactive state (0), all hardware
control updates such as exposure, gain, saturation, and flip will be
skipped, while still returning success to userspace. Should the driver treat
-EINVAL as an active state to ensure controls are applied?

> +
> +	switch (ctrl->id) {
> +	case V4L2_CID_HFLIP: {
[ ... ]
> +		/* Only refresh if streaming - otherwise MCU may not be ready */
> +		if (!ret && sensor->streaming)
> +			mt9m113_refresh(sensor);
> +		break;

[Severity: Medium]
Will this result in 1-second stalls and false-positive timeout warnings if a
control is modified while the test pattern is active?

When the test pattern is active, the MCU is halted (BOOT_MODE = 1). If a
standard control like V4L2_CID_HFLIP is changed, mt9m113_s_ctrl() attempts
to call mt9m113_refresh(), which tries to send SEQ_CMD_REFRESH. Because the
MCU is halted, it cannot process the command, leading to two 500ms timeouts
in mt9m113_poll_mcu_var(). Should the driver check if the MCU is halted by
the test pattern before attempting a refresh?

[ ... ]
> +	case V4L2_CID_TEST_PATTERN:
> +		if (ctrl->val == 0) {
[ ... ]
> +			if (sensor->test_pattern_active) {
> +				dev_dbg(&sensor->client->dev,
> +					 "MT9M113: Disabling test pattern, restarting MCU\n");
> +				cci_write(sensor->regmap, MT9M113_MCU_BOOT_MODE,
> +					  0x0000, &ret);
> +				if (!ret) {
> +					usleep_range(10000, 15000);
> +					ret = mt9m113_write_mcu_var(sensor,
> +								    MT9M113_CAM_MODE_SELECT,
> +								    MT9M113_CAM_MODE_SELECT_NORMAL);
> +				}
> +				if (!ret)
> +					sensor->test_pattern_active = false;
> +			}

[Severity: High]
Can disabling the test pattern permanently freeze the camera stream?

When userspace disables the test pattern while streaming, writing 0 to
MT9M113_MCU_BOOT_MODE releases the halt and reboots the MCU. However, the
driver does not appear to reapply the sensor configuration (such as the init
table, output dimensions, and AE) or issue SEQ_CMD_RUN afterward. Will the
stream remain frozen without these initialization steps?

> +		} else {
> +			/*
> +			 * Enable test pattern mode.
> +			 * Per datasheet: "Disabling the MCU is recommended
> +			 * before enabling test patterns."
[ ... ]
> +			sensor->test_pattern_active = true;
> +			dev_dbg(&sensor->client->dev,
> +				 "MT9M113: Enabling test pattern %d\n", ctrl->val);
> +			ret = mt9m113_write_mcu_var(sensor,
> +						    MT9M113_CAM_MODE_TEST_PATTERN_SELECT,
> +						    mt9m113_test_pattern_value[ctrl->val - 1]);
> +			if (!ret)
> +				ret = mt9m113_write_mcu_var(sensor,
> +							    MT9M113_CAM_MODE_SELECT,
> +							    MT9M113_CAM_MODE_SELECT_TEST_PATTERN);
> +			if (!ret && sensor->streaming) {
> +				mt9m113_refresh(sensor);
> +				dev_dbg(&sensor->client->dev,
> +					 "MT9M113: Stopping MCU for test pattern\n");
> +				cci_write(sensor->regmap, MT9M113_MCU_BOOT_MODE,
> +					  0x0001, &ret);
> +			}
> +		}
> +		break;

[Severity: High]
Does the test pattern fail to activate if it is set before streaming starts?

If the test pattern is configured before streaming begins,
__v4l2_ctrl_handler_setup() (called from mt9m113_stream_on()) applies this
control while sensor->streaming is still false. This skips halting the MCU
with BOOT_MODE = 1. When mt9m113_stream_on() then starts the normal
sequencer, the test pattern settings will be overwritten by the normal
streaming configuration. How should the test pattern state be maintained
across stream starts?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260605-submit-media-mt9m113-v3-0-dc3e07d5d70b@herrie.org?part=2

^ permalink raw reply	[flat|nested] 5+ messages in thread

end of thread, other threads:[~2026-06-05  7:16 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-05  7:02 [PATCH v3 0/2] media: i2c: add Aptina MT9M113 SoC sensor driver Herman van Hazendonk via B4 Relay
2026-06-05  7:03 ` [PATCH v3 1/2] dt-bindings: media: i2c: add aptina,mt9m113 Herman van Hazendonk via B4 Relay
2026-06-05  7:11   ` sashiko-bot
2026-06-05  7:03 ` [PATCH v3 2/2] media: i2c: add Aptina MT9M113 1.3 Mpx SoC sensor driver Herman van Hazendonk via B4 Relay
2026-06-05  7:16   ` sashiko-bot

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox