mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH 0/3] media: Add GalaxyCore GC5035 and GC8034 sensor drivers
@ 2026-10-09  7:27 Nicola Fiorillo
  2026-10-09  7:27 ` [PATCH 1/3] media: i2c: Add GC5035 image sensor driver Nicola Fiorillo
                   ` (3 more replies)
  0 siblings, 4 replies; 15+ messages in thread
From: Nicola Fiorillo @ 2026-10-09  7:27 UTC (permalink / raw)
  To: linux-media
  Cc: Sakari Ailus, Laurent Pinchart, Mauro Carvalho Chehab,
	Hans Verkuil, Dan Scally, Hans de Goede, linux-kernel,
	Nicola Fiorillo

This series adds drivers for the GalaxyCore GC5035 and GC8034 raw Bayer
sensors, and the ipu-bridge entries needed to use them on Intel IPU6. Both
are found on the CHUWI Hi10 X1, an Intel N100 (Alder Lake-N) tablet:

  GC5035, 5 Mpixel, front, 2 CSI-2 lanes, ACPI HID GCTI5035
  GC8034, 8 Mpixel, rear,  4 CSI-2 lanes, ACPI HID GCTI8034

The drivers are ACPI only. I have no device tree system with either sensor,
so I have not added bindings I cannot test.

Link frequencies
----------------

Both drivers accept only the 19.2 MHz external clock they have been tested
with. The link frequencies in the vendor code are for a 24 MHz clock:

  - GC5035: Intel's 438 MHz is correct with a 24 MHz clock. For 19.2 MHz
    Intel changed the PLL but not the constant. With the new PLL the link
    frequency is 422.4 MHz, which matches the measurement.
  - GC8034: the vendor code declares 336 MHz for a 24 MHz clock. At
    19.2 MHz everything scales by 0.8: 268.8 MHz, and 24 fps instead of
    30.

The same values are used in the ipu-bridge entries in patch 3.

Gain, power sequencing and geometry
-----------------------------------

V4L2_CID_ANALOGUE_GAIN is the index of the analogue gain step, 17 steps
on the GC5035 and 7 on the GC8034, and the digital gain is left at the
1.0x the register tables program. The vendor drivers fill the gap between
two steps with digital gain; that is left to userspace.

No datasheet is available for either sensor. The GC5035 power sequence
follows the one Tomasz Figa described in the 2020 series (IOVDD first,
MCLK cycles before the first I2C transaction and after streaming stops),
the GC8034 one follows the Rockchip driver. On this machine only AVDD is
a real, GPIO switched regulator: DVDD and DOVDD are dummies, and the
GC8034 has no reset or powerdown GPIO.

The register tables read out a window slightly larger than the output
size and crop it: 2608x1960 cropped at (8, 8) on the GC5035, 3284x2464
cropped at (9, 8) on the GC8034. The size of the full pixel arrays is not
documented, so the readout windows are reported as the native sizes.

The GC8034 blanking register holds the frame length minus the height
and 36 lines. The 36 lines value comes from the Rockchip driver, and is
confirmed by the measurement with VBLANK set to 2000: the difference is
less than one line.

Provenance
----------

The GC5035 register tables come from the Alder Lake-M driver in
intel/ipu6-drivers, which derives from the ChromeOS driver posted in 2020
by Tomasz Figa [1]. That series got as far as v4 and was not merged. The
GC8034 register tables come from the Rockchip BSP driver (rockchip-linux
kernel, develop-5.10). The copyright lines of the original authors are
kept in both files. The register tables have been shortened: the GC5035
one from 323 to 160 writes, the GC8034 one from 233 to 182. Registers
written twice and those already written by the controls have been
removed. The final register state is identical to the one of the original
tables. There is no public documentation for the PLL and CSI-2 settings.

The driver code follows gc05a2.c and gc08a3.c: CCI helpers, subdev active
state, enable_streams/disable_streams and runtime PM with autosuspend.

Use of an LLM
-------------

I speak Italian and I use Claude to translate.

I used an LLM (Claude Opus 5.5) to collect the main points of the sensor
driver reviews posted to this list recently. Claude wrote the driver code
and updated it based on those points, and wrote the test scripts. I ran
the tests on the tablet with the hardware, and the results are listed
below. The code, the measurements and the notes (in Italian) are at
https://github.com/nic-fio/chuwi-hi10x1-cameras

The use of LLMs follows Documentation/process/coding-assistants.rst and
generated-content.rst.

Not covered
-----------

  - One mode per sensor: 2592x1944 on the GC5035, 3264x2448 on the GC8034.
    The binned modes of the vendor drivers have not been tested and were
    left out.
  - The sensor OTP is not read. On the GC5035 it holds factory register
    settings and defect pixel tables, which the 2020 series applied.
  - The GC8034 module has a dw9714 VCM, instantiated by ipu-bridge; focus
    has not been tested.
  - Tested on one machine, with one sample of each sensor.

Testing
-------

Based on media-committers next, commit 8e26d4c20ed2 ("media: hantro:
disable runtime PM before hardware teardown"), and tested on the CHUWI
Hi10 X1 with that kernel and these patches:

  - both sensors stream at full resolution
  - frame rate: the frame rates expected from the driver match the
    measured ones: 28.82 fps on the GC5035, 24.01 fps against 24.00 on
    the GC8034. They also match with VBLANK set to 2000: 14.67 fps on
    the GC5035 and 13.47 fps on the GC8034.
  - analogue gain, from the first to the last step: 15.6x requested and
    15.35x measured on the GC5035, 7.66x requested and 7.74x measured on
    the GC8034.
  - GC5035 test pattern: two consecutive frames are 100% identical.
  - v4l2-compliance from git (1616bf9e3c81), run with -z on the media
    device: 54 out of 54 tests pass on both sensors, with 2 warnings on
    S_SELECTION. The warnings are there because the crop is fixed;
    imx219, gc05a2 and gc08a3 do the same. Output below.

[1] https://lore.kernel.org/linux-media/20200902224813.14283-1-tfiga@chromium.org/

GC5035:

v4l2-compliance 1.33.0-5515, 64 bits, 64-bit time_t
v4l2-compliance SHA: 1616bf9e3c81 2026-10-06 07:30:20

Compliance test for intel-ipu6 device /dev/v4l-subdev4:

Driver Info:
	Driver version   : 7.3.0
	Capabilities     : 0x00000000
	Client Capabilities: 0x0000000000000002
interval-uses-which Media Driver Info:
	Driver name      : intel-ipu6
	Model            : ipu6
	Serial           :
	Bus info         : PCI:0000:00:05.0
	Media version    : 7.3.0
	Hardware revision: 0x00000000 (0)
	Driver version   : 7.3.0
Interface Info:
	ID               : 0x030000f5
	Type             : V4L Sub-Device
Entity Info:
	ID               : 0x000000e9 (233)
	Name             : gc5035 2-003f
	Function         : Camera Sensor
	Pad 0x010000ea   : 0: Source
	  Link 0x020000eb: to remote pad 0x1000096 of entity 'Intel IPU6 CSI2 2' (Video Interface Bridge): Data, Enabled, Immutable

Required ioctls:
	test MC information (see 'Media Driver Info' above): OK
	test VIDIOC_SUDBEV_QUERYCAP: OK
	test invalid ioctls: OK

Allow for multiple opens:
	test second /dev/v4l-subdev4 open: OK
	test VIDIOC_SUBDEV_QUERYCAP: OK
	test for unlimited opens: OK

Debug ioctls:
	test VIDIOC_LOG_STATUS: OK (Not Supported)

Input ioctls:
	test VIDIOC_G/S_TUNER/ENUM_FREQ_BANDS: OK (Not Supported)
	test VIDIOC_G/S_FREQUENCY: OK (Not Supported)
	test VIDIOC_S_HW_FREQ_SEEK: OK (Not Supported)
	test VIDIOC_ENUMAUDIO: OK (Not Supported)
	test VIDIOC_G/S/ENUMINPUT: OK (Not Supported)
	test VIDIOC_G/S_AUDIO: OK (Not Supported)
	Inputs: 0 Audio Inputs: 0 Tuners: 0

Output ioctls:
	test VIDIOC_G/S_MODULATOR: OK (Not Supported)
	test VIDIOC_G/S_FREQUENCY: OK (Not Supported)
	test VIDIOC_ENUMAUDOUT: OK (Not Supported)
	test VIDIOC_G/S/ENUMOUTPUT: OK (Not Supported)
	test VIDIOC_G/S_AUDOUT: OK (Not Supported)
	Outputs: 0 Audio Outputs: 0 Modulators: 0

Input/Output configuration ioctls:
	test VIDIOC_ENUM/G/S/QUERY_STD: OK (Not Supported)
	test VIDIOC_ENUM/G/S/QUERY_DV_TIMINGS: OK (Not Supported)
	test VIDIOC_DV_TIMINGS_CAP: OK (Not Supported)
	test VIDIOC_G/S_EDID: OK (Not Supported)

Sub-Device ioctls (Source Pad 0):
	Try Stream 0
	test Try VIDIOC_SUBDEV_ENUM_MBUS_CODE/FRAME_SIZE/FRAME_INTERVAL: OK
	test Try VIDIOC_SUBDEV_G/S_FMT: OK
		warn: v4l2-test-subdevs.cpp(566): VIDIOC_SUBDEV_G_SELECTION is supported for target 0 but not VIDIOC_SUBDEV_S_SELECTION
	test Try VIDIOC_SUBDEV_G/S_SELECTION/CROP: OK
	Active Stream 0
	test Active VIDIOC_SUBDEV_ENUM_MBUS_CODE/FRAME_SIZE/FRAME_INTERVAL: OK
	test Active VIDIOC_SUBDEV_G/S_FMT: OK
		warn: v4l2-test-subdevs.cpp(566): VIDIOC_SUBDEV_G_SELECTION is supported for target 0 but not VIDIOC_SUBDEV_S_SELECTION
	test Active VIDIOC_SUBDEV_G/S_SELECTION/CROP: OK
	test Active VIDIOC_SUBDEV_G/S_FRAME_INTERVAL: OK (Not Supported)

Control ioctls:
	test VIDIOC_QUERY_EXT_CTRL/QUERYMENU: OK
	test VIDIOC_QUERYCTRL: OK
	test VIDIOC_G/S_CTRL: OK
	test VIDIOC_G/S/TRY_EXT_CTRLS: OK
	test VIDIOC_(UN)SUBSCRIBE_EVENT/DQEVENT: OK
	test VIDIOC_G/S_JPEGCOMP: OK (Not Supported)
	Standard Controls: 13 Private Controls: 0

Format ioctls:
	test VIDIOC_ENUM_FMT/FRAMESIZES/FRAMEINTERVALS: OK (Not Supported)
	test VIDIOC_G/S_PARM: OK (Not Supported)
	test VIDIOC_G_FBUF: OK (Not Supported)
	test VIDIOC_G_FMT: OK (Not Supported)
	test VIDIOC_TRY_FMT: OK (Not Supported)
	test VIDIOC_S_FMT: OK (Not Supported)
	test VIDIOC_G_SLICED_VBI_CAP: OK (Not Supported)
	test Cropping: OK (Not Supported)
	test Composing: OK (Not Supported)
	test Scaling: OK (Not Supported)

Codec ioctls:
	test VIDIOC_(TRY_)ENCODER_CMD: OK (Not Supported)
	test VIDIOC_G_ENC_INDEX: OK (Not Supported)
	test VIDIOC_(TRY_)DECODER_CMD: OK (Not Supported)

Buffer ioctls:
	test VIDIOC_REQBUFS/CREATE_BUFS/QUERYBUF: OK (Not Supported)
	test CREATE_BUFS maximum buffers: OK
	test VIDIOC_REMOVE_BUFS: OK
	test VIDIOC_EXPBUF: OK (Not Supported)
	test Requests: OK (Not Supported)
	test blocking wait: OK (Not Supported)

Total for intel-ipu6 device /dev/v4l-subdev4: 54, Succeeded: 54, Failed: 0, Warnings: 2

GC8034:

v4l2-compliance 1.33.0-5515, 64 bits, 64-bit time_t
v4l2-compliance SHA: 1616bf9e3c81 2026-10-06 07:30:20

Compliance test for intel-ipu6 device /dev/v4l-subdev5:

Driver Info:
	Driver version   : 7.3.0
	Capabilities     : 0x00000000
	Client Capabilities: 0x0000000000000002
interval-uses-which Media Driver Info:
	Driver name      : intel-ipu6
	Model            : ipu6
	Serial           :
	Bus info         : PCI:0000:00:05.0
	Media version    : 7.3.0
	Hardware revision: 0x00000000 (0)
	Driver version   : 7.3.0
Interface Info:
	ID               : 0x030000fb
	Type             : V4L Sub-Device
Entity Info:
	ID               : 0x000000f7 (247)
	Name             : gc8034 1-0037
	Function         : Camera Sensor
	Pad 0x010000f8   : 0: Source
	  Link 0x020000f9: to remote pad 0x100008c of entity 'Intel IPU6 CSI2 1' (Video Interface Bridge): Data, Enabled, Immutable

Required ioctls:
	test MC information (see 'Media Driver Info' above): OK
	test VIDIOC_SUDBEV_QUERYCAP: OK
	test invalid ioctls: OK

Allow for multiple opens:
	test second /dev/v4l-subdev5 open: OK
	test VIDIOC_SUBDEV_QUERYCAP: OK
	test for unlimited opens: OK

Debug ioctls:
	test VIDIOC_LOG_STATUS: OK (Not Supported)

Input ioctls:
	test VIDIOC_G/S_TUNER/ENUM_FREQ_BANDS: OK (Not Supported)
	test VIDIOC_G/S_FREQUENCY: OK (Not Supported)
	test VIDIOC_S_HW_FREQ_SEEK: OK (Not Supported)
	test VIDIOC_ENUMAUDIO: OK (Not Supported)
	test VIDIOC_G/S/ENUMINPUT: OK (Not Supported)
	test VIDIOC_G/S_AUDIO: OK (Not Supported)
	Inputs: 0 Audio Inputs: 0 Tuners: 0

Output ioctls:
	test VIDIOC_G/S_MODULATOR: OK (Not Supported)
	test VIDIOC_G/S_FREQUENCY: OK (Not Supported)
	test VIDIOC_ENUMAUDOUT: OK (Not Supported)
	test VIDIOC_G/S/ENUMOUTPUT: OK (Not Supported)
	test VIDIOC_G/S_AUDOUT: OK (Not Supported)
	Outputs: 0 Audio Outputs: 0 Modulators: 0

Input/Output configuration ioctls:
	test VIDIOC_ENUM/G/S/QUERY_STD: OK (Not Supported)
	test VIDIOC_ENUM/G/S/QUERY_DV_TIMINGS: OK (Not Supported)
	test VIDIOC_DV_TIMINGS_CAP: OK (Not Supported)
	test VIDIOC_G/S_EDID: OK (Not Supported)

Sub-Device ioctls (Source Pad 0):
	Try Stream 0
	test Try VIDIOC_SUBDEV_ENUM_MBUS_CODE/FRAME_SIZE/FRAME_INTERVAL: OK
	test Try VIDIOC_SUBDEV_G/S_FMT: OK
		warn: v4l2-test-subdevs.cpp(566): VIDIOC_SUBDEV_G_SELECTION is supported for target 0 but not VIDIOC_SUBDEV_S_SELECTION
	test Try VIDIOC_SUBDEV_G/S_SELECTION/CROP: OK
	Active Stream 0
	test Active VIDIOC_SUBDEV_ENUM_MBUS_CODE/FRAME_SIZE/FRAME_INTERVAL: OK
	test Active VIDIOC_SUBDEV_G/S_FMT: OK
		warn: v4l2-test-subdevs.cpp(566): VIDIOC_SUBDEV_G_SELECTION is supported for target 0 but not VIDIOC_SUBDEV_S_SELECTION
	test Active VIDIOC_SUBDEV_G/S_SELECTION/CROP: OK
	test Active VIDIOC_SUBDEV_G/S_FRAME_INTERVAL: OK (Not Supported)

Control ioctls:
	test VIDIOC_QUERY_EXT_CTRL/QUERYMENU: OK
	test VIDIOC_QUERYCTRL: OK
	test VIDIOC_G/S_CTRL: OK
	test VIDIOC_G/S/TRY_EXT_CTRLS: OK
	test VIDIOC_(UN)SUBSCRIBE_EVENT/DQEVENT: OK
	test VIDIOC_G/S_JPEGCOMP: OK (Not Supported)
	Standard Controls: 12 Private Controls: 0

Format ioctls:
	test VIDIOC_ENUM_FMT/FRAMESIZES/FRAMEINTERVALS: OK (Not Supported)
	test VIDIOC_G/S_PARM: OK (Not Supported)
	test VIDIOC_G_FBUF: OK (Not Supported)
	test VIDIOC_G_FMT: OK (Not Supported)
	test VIDIOC_TRY_FMT: OK (Not Supported)
	test VIDIOC_S_FMT: OK (Not Supported)
	test VIDIOC_G_SLICED_VBI_CAP: OK (Not Supported)
	test Cropping: OK (Not Supported)
	test Composing: OK (Not Supported)
	test Scaling: OK (Not Supported)

Codec ioctls:
	test VIDIOC_(TRY_)ENCODER_CMD: OK (Not Supported)
	test VIDIOC_G_ENC_INDEX: OK (Not Supported)
	test VIDIOC_(TRY_)DECODER_CMD: OK (Not Supported)

Buffer ioctls:
	test VIDIOC_REQBUFS/CREATE_BUFS/QUERYBUF: OK (Not Supported)
	test CREATE_BUFS maximum buffers: OK
	test VIDIOC_REMOVE_BUFS: OK
	test VIDIOC_EXPBUF: OK (Not Supported)
	test Requests: OK (Not Supported)
	test blocking wait: OK (Not Supported)

Total for intel-ipu6 device /dev/v4l-subdev5: 54, Succeeded: 54, Failed: 0, Warnings: 2

Nicola Fiorillo (3):
  media: i2c: Add GC5035 image sensor driver
  media: i2c: Add GC8034 image sensor driver
  media: ipu-bridge: Add GalaxyCore GC5035 and GC8034

 MAINTAINERS                          |  12 +
 drivers/media/i2c/Kconfig            |  20 +
 drivers/media/i2c/Makefile           |   2 +
 drivers/media/i2c/gc5035.c           | 953 ++++++++++++++++++++++++++
 drivers/media/i2c/gc8034.c           | 969 +++++++++++++++++++++++++++
 drivers/media/pci/intel/ipu-bridge.c |   4 +
 6 files changed, 1960 insertions(+)
 create mode 100644 drivers/media/i2c/gc5035.c
 create mode 100644 drivers/media/i2c/gc8034.c


base-commit: 8e26d4c20ed218db6c3123480a4a4e2545680566
-- 
2.47.3


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

* [PATCH 1/3] media: i2c: Add GC5035 image sensor driver
  2026-10-09  7:27 [PATCH 0/3] media: Add GalaxyCore GC5035 and GC8034 sensor drivers Nicola Fiorillo
@ 2026-10-09  7:27 ` Nicola Fiorillo
  2026-10-09  9:48   ` Laurent Pinchart
  2026-10-12 11:20   ` Uwe Kleine-König
  2026-10-09  7:27 ` [PATCH 2/3] media: i2c: Add GC8034 " Nicola Fiorillo
                   ` (2 subsequent siblings)
  3 siblings, 2 replies; 15+ messages in thread
From: Nicola Fiorillo @ 2026-10-09  7:27 UTC (permalink / raw)
  To: linux-media
  Cc: Sakari Ailus, Laurent Pinchart, Mauro Carvalho Chehab,
	Hans Verkuil, Dan Scally, Hans de Goede, linux-kernel,
	Nicola Fiorillo

Add a driver for the GalaxyCore GC5035, a 5 Mpixel raw Bayer sensor with
a two lane MIPI CSI-2 interface. The driver supports a single 2592x1944
mode, and the exposure, analogue gain, vertical blanking and test pattern
controls.

The register tables come from the Alder Lake-M driver in
intel/ipu6-drivers, which derives from the ChromeOS driver posted in 2020.

Assisted-by: claude-opus-5-5 coccinelle sparse smatch
Signed-off-by: Nicola Fiorillo <nicfio@gmail.com>
---
 MAINTAINERS                |   6 +
 drivers/media/i2c/Kconfig  |  10 +
 drivers/media/i2c/Makefile |   1 +
 drivers/media/i2c/gc5035.c | 953 +++++++++++++++++++++++++++++++++++++
 4 files changed, 970 insertions(+)
 create mode 100644 drivers/media/i2c/gc5035.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 6bdc32e7c..c4d5f81f4 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -10875,6 +10875,12 @@ T:	git git://linuxtv.org/media.git
 F:	Documentation/devicetree/bindings/media/i2c/galaxycore,gc2145.yaml
 F:	drivers/media/i2c/gc2145.c
 
+GALAXYCORE GC5035 CAMERA SENSOR DRIVER
+M:	Nicola Fiorillo <nicfio@gmail.com>
+L:	linux-media@vger.kernel.org
+S:	Maintained
+F:	drivers/media/i2c/gc5035.c
+
 GATEWORKS SYSTEM CONTROLLER (GSC) DRIVER
 M:	Tim Harvey <tharvey@gateworks.com>
 S:	Maintained
diff --git a/drivers/media/i2c/Kconfig b/drivers/media/i2c/Kconfig
index b9961f77f..bde998fca 100644
--- a/drivers/media/i2c/Kconfig
+++ b/drivers/media/i2c/Kconfig
@@ -110,6 +110,16 @@ config VIDEO_GC2145
 	  To compile this driver as a module, choose M here: the
 	  module will be called gc2145.
 
+config VIDEO_GC5035
+	tristate "GalaxyCore GC5035 sensor support"
+	select V4L2_CCI_I2C
+	help
+	  This is a Video4Linux2 sensor driver for the GalaxyCore GC5035
+	  5 Mpixel camera.
+
+	  To compile this driver as a module, choose M here: the
+	  module will be called gc5035.
+
 config VIDEO_HI556
 	tristate "Hynix Hi-556 sensor support"
 	help
diff --git a/drivers/media/i2c/Makefile b/drivers/media/i2c/Makefile
index 351c41ef2..c3a7f39bd 100644
--- a/drivers/media/i2c/Makefile
+++ b/drivers/media/i2c/Makefile
@@ -42,6 +42,7 @@ obj-$(CONFIG_VIDEO_GC0310) += gc0310.o
 obj-$(CONFIG_VIDEO_GC05A2) += gc05a2.o
 obj-$(CONFIG_VIDEO_GC08A3) += gc08a3.o
 obj-$(CONFIG_VIDEO_GC2145) += gc2145.o
+obj-$(CONFIG_VIDEO_GC5035) += gc5035.o
 obj-$(CONFIG_VIDEO_HI556) += hi556.o
 obj-$(CONFIG_VIDEO_HI846) += hi846.o
 obj-$(CONFIG_VIDEO_HI847) += hi847.o
diff --git a/drivers/media/i2c/gc5035.c b/drivers/media/i2c/gc5035.c
new file mode 100644
index 000000000..95d83cde8
--- /dev/null
+++ b/drivers/media/i2c/gc5035.c
@@ -0,0 +1,953 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Driver for GalaxyCore GC5035 image sensor
+ *
+ * Copyright (c) 2020 Bitland Inc.
+ * Copyright 2020 Google LLC.
+ * Copyright (c) 2022 Intel Corporation.
+ * Copyright (C) 2026 Nicola Fiorillo <nicfio@gmail.com>
+ *
+ * The register table comes from the GC5035 driver in intel/ipu6-drivers by
+ * Liang Wang <liang1.wang@intel.com>, derived from the ChromeOS series by
+ * Tomasz Figa <tfiga@chromium.org>.
+ */
+#include <linux/array_size.h>
+#include <linux/clk.h>
+#include <linux/container_of.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/math.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/pm_runtime.h>
+#include <linux/property.h>
+#include <linux/regulator/consumer.h>
+#include <linux/time64.h>
+#include <linux/types.h>
+#include <linux/units.h>
+
+#include <media/mipi-csi2.h>
+#include <media/v4l2-cci.h>
+#include <media/v4l2-common.h>
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-fwnode.h>
+#include <media/v4l2-subdev.h>
+
+/*
+ * The GC5035 uses 8 bit register addresses with banked pages: register
+ * 0xfe selects the page. Every register below is on page 0 unless noted
+ * otherwise. The vendor sequence also writes 0x10 to register 0xfe, which is
+ * not a page number and is not documented, so the pages are not handled with
+ * regmap ranges.
+ */
+#define GC5035_REG_PAGE_SELECT		CCI_REG8(0xfe)
+#define GC5035_PAGE_0			0x00
+#define GC5035_PAGE_1			0x01
+
+/* readable on any page */
+#define GC5035_REG_CHIP_ID		CCI_REG16(0xf0)
+#define GC5035_CHIP_ID			0x5035
+
+#define GC5035_REG_EXPOSURE		CCI_REG16(0x03)
+#define GC5035_REG_LINE_LENGTH		CCI_REG16(0x05)
+#define GC5035_REG_WIN_TOP		CCI_REG16(0x09)
+#define GC5035_REG_WIN_LEFT		CCI_REG16(0x0b)
+#define GC5035_REG_WIN_HEIGHT		CCI_REG16(0x0d)
+#define GC5035_REG_WIN_WIDTH		CCI_REG16(0x0f)
+#define GC5035_REG_STREAM		CCI_REG8(0x3e)
+#define GC5035_STREAM_ON		0x91
+#define GC5035_STREAM_OFF		0x01
+#define GC5035_REG_FRAME_LENGTH		CCI_REG16(0x41)
+#define GC5035_REG_DIGITAL_GAIN	CCI_REG16(0xb1)
+#define GC5035_DIGITAL_GAIN_1X	0x100
+#define GC5035_REG_ANALOGUE_GAIN	CCI_REG8(0xb6)
+#define GC5035_REG_PLL_MULT		CCI_REG8(0xf8)
+
+/* page 1 */
+#define GC5035_REG_TEST_PATTERN		CCI_REG8(0x8c)
+#define GC5035_TEST_PATTERN_ON		0x11
+#define GC5035_TEST_PATTERN_OFF		0x10
+#define GC5035_REG_CROP_TOP		CCI_REG16(0x91)
+#define GC5035_REG_CROP_LEFT		CCI_REG16(0x93)
+#define GC5035_REG_OUT_HEIGHT		CCI_REG16(0x95)
+#define GC5035_REG_OUT_WIDTH		CCI_REG16(0x97)
+
+/*
+ * The sensor reads out a 2608x1960 window starting at (3, 4) and crops
+ * 2592x1944 from it at (8, 8). The size of the full pixel array is not
+ * documented, so the readout window is reported as the native size.
+ */
+#define GC5035_WIN_LEFT			3
+#define GC5035_WIN_TOP			4
+#define GC5035_NATIVE_WIDTH		2608
+#define GC5035_NATIVE_HEIGHT		1960
+#define GC5035_CROP_LEFT		8
+#define GC5035_CROP_TOP			8
+#define GC5035_WIDTH			2592
+#define GC5035_HEIGHT			1944
+
+/* The line length register counts units of 4 pixels. */
+#define GC5035_HTS			2920
+#define GC5035_VTS_DEF			2008
+
+#define GC5035_EXP_MIN			4
+#define GC5035_EXP_STEP			1
+#define GC5035_EXP_MARGIN		16
+#define GC5035_EXP_DEF			984
+#define GC5035_VTS_MAX			0x3fff
+/* The sensor rounds the frame length to a multiple of 4 lines. */
+#define GC5035_VBLANK_STEP		4
+
+/*
+ * The register table has only been tested with a 19.2 MHz external clock. The
+ * link frequency is the external clock multiplied by the PLL setting and
+ * divided by 4: 422.4 MHz.
+ */
+#define GC5035_XCLK_FREQ		(19200 * HZ_PER_KHZ)
+#define GC5035_PLL_MULT			0x58
+#define GC5035_LINK_FREQ		(GC5035_XCLK_FREQ * GC5035_PLL_MULT / 4)
+#define GC5035_DATA_LANES		2
+#define GC5035_RGB_DEPTH		10
+#define GC5035_PIXEL_RATE		(GC5035_LINK_FREQ * 2 * \
+					 GC5035_DATA_LANES / GC5035_RGB_DEPTH)
+
+/*
+ * No datasheet is available. From the GC5035 driver posted by Tomasz Figa in
+ * 2020: IOVDD at least 50 us before AVDD and DVDD, at least 1200 MCLK cycles
+ * before the first I2C transaction, and 2000 MCLK cycles after streaming stops
+ * before the clock is switched off. From the intel/ipu6-drivers driver: the
+ * clock is enabled before reset is released and disabled after reset is
+ * asserted, and 5 ms elapse before the first I2C transaction, which also
+ * covers the 1200 cycles.
+ */
+#define GC5035_IOVDD_DELAY_US		50
+#define GC5035_RESET_DELAY_US		(5 * USEC_PER_MSEC)
+#define GC5035_STOP_DELAY_US		DIV_ROUND_UP(2000 * USEC_PER_MSEC, \
+					     GC5035_XCLK_FREQ / MSEC_PER_SEC)
+
+#define GC5035_MBUS_CODE		MEDIA_BUS_FMT_SGRBG10_1X10
+
+static const s64 gc5035_link_freq_menu[] = {
+	GC5035_LINK_FREQ,
+};
+
+static const char * const gc5035_test_pattern_menu[] = {
+	"No Pattern",
+	"Test Chart",
+};
+
+/* DOVDD (IOVDD) comes first: it has to be enabled before the others. */
+static const char * const gc5035_supply_name[] = {
+	"dovdd",
+	"avdd",
+	"dvdd",
+};
+
+/*
+ * V4L2_CID_ANALOGUE_GAIN is an index into this table of register 0xb6 values,
+ * one per analogue gain step. The digital gain is left at the 1.0x the register
+ * table programs.
+ */
+static const u8 gc5035_again_code[] = {
+	0,	/*  1.000x */
+	1,	/*  1.180x */
+	2,	/*  1.398x */
+	3,	/*  1.660x */
+	8,	/*  1.961x */
+	9,	/*  2.340x */
+	10,	/*  2.801x */
+	11,	/*  3.301x */
+	12,	/*  3.898x */
+	13,	/*  4.699x */
+	14,	/*  5.602x */
+	15,	/*  6.680x */
+	16,	/*  7.801x */
+	17,	/*  9.199x */
+	18,	/* 11.000x */
+	19,	/* 12.961x */
+	20,	/* 15.602x */
+};
+
+struct gc5035 {
+	struct device *dev;
+	struct v4l2_subdev sd;
+	struct media_pad pad;
+
+	struct clk *xclk;
+	struct regulator_bulk_data supplies[ARRAY_SIZE(gc5035_supply_name)];
+	struct gpio_desc *reset_gpio;
+	struct gpio_desc *powerdown_gpio;
+
+	struct v4l2_ctrl_handler ctrls;
+	struct v4l2_ctrl *exposure;
+
+	struct regmap *regmap;
+};
+
+/*
+ * The register sequence is the vendor one for the 2592x1944 mode, without the
+ * registers written by the controls. No register documentation is available:
+ * the names of the geometry registers are inferred from their values.
+ */
+static const struct cci_reg_sequence gc5035_regs[] = {
+	/* System */
+	{ GC5035_REG_PAGE_SELECT, 0x00 },
+	{ GC5035_REG_STREAM, GC5035_STREAM_OFF },
+	{ CCI_REG8(0xfc), 0x01 },
+	{ CCI_REG8(0xf4), 0x40 },
+	{ CCI_REG8(0xf5), 0xe9 },
+	{ CCI_REG8(0xf6), 0x14 },
+	{ GC5035_REG_PLL_MULT, GC5035_PLL_MULT },
+	{ CCI_REG8(0xf9), 0x82 },
+	{ CCI_REG8(0xfa), 0x00 },
+	{ CCI_REG8(0xfc), 0x81 },
+	{ GC5035_REG_PAGE_SELECT, 0x00 },
+	{ CCI_REG8(0x36), 0x01 },
+	{ CCI_REG8(0xd3), 0x87 },
+	{ CCI_REG8(0x36), 0x00 },
+	{ CCI_REG8(0x33), 0x00 },
+	{ GC5035_REG_PAGE_SELECT, 0x03 },
+	{ CCI_REG8(0x01), 0xe7 },
+	{ CCI_REG8(0xf7), 0x01 },
+	{ CCI_REG8(0xfc), 0x8f },
+	{ CCI_REG8(0xfc), 0x8f },
+	{ CCI_REG8(0xfc), 0x8e },
+	{ GC5035_REG_PAGE_SELECT, 0x00 },
+	{ CCI_REG8(0xee), 0x30 },
+	{ CCI_REG8(0x87), 0x18 },
+	{ GC5035_REG_PAGE_SELECT, 0x01 },
+	{ CCI_REG8(0x8c), 0x90 },
+	{ GC5035_REG_PAGE_SELECT, 0x00 },
+	/* Analog & CISCTL */
+	{ GC5035_REG_LINE_LENGTH, GC5035_HTS / 4 },
+	{ CCI_REG8(0x9d), 0x18 },
+	{ GC5035_REG_WIN_TOP, GC5035_WIN_TOP },
+	{ GC5035_REG_WIN_LEFT, GC5035_WIN_LEFT },
+	{ GC5035_REG_WIN_HEIGHT, GC5035_NATIVE_HEIGHT },
+	{ GC5035_REG_WIN_WIDTH, GC5035_NATIVE_WIDTH },
+	{ CCI_REG8(0x11), 0x02 },
+	{ CCI_REG8(0x17), 0x80 },
+	{ CCI_REG8(0x19), 0x05 },
+	{ GC5035_REG_PAGE_SELECT, 0x02 },
+	{ CCI_REG8(0x30), 0x03 },
+	{ CCI_REG8(0x31), 0x03 },
+	{ GC5035_REG_PAGE_SELECT, 0x00 },
+	{ CCI_REG8(0xd9), 0xc0 },
+	{ CCI_REG8(0x1b), 0x20 },
+	{ CCI_REG8(0x21), 0x40 },
+	{ CCI_REG8(0x28), 0x22 },
+	{ CCI_REG8(0x29), 0x56 },
+	{ CCI_REG8(0x44), 0x20 },
+	{ CCI_REG8(0x4b), 0x10 },
+	{ CCI_REG8(0x4e), 0x1a },
+	{ CCI_REG8(0x50), 0x11 },
+	{ CCI_REG8(0x52), 0x33 },
+	{ CCI_REG8(0x53), 0x44 },
+	{ CCI_REG8(0x55), 0x10 },
+	{ CCI_REG8(0x5b), 0x11 },
+	{ CCI_REG8(0xc5), 0x02 },
+	{ CCI_REG8(0x8c), 0x1a },
+	{ GC5035_REG_PAGE_SELECT, 0x02 },
+	{ CCI_REG8(0x33), 0x05 },
+	{ CCI_REG8(0x32), 0x38 },
+	{ GC5035_REG_PAGE_SELECT, 0x00 },
+	{ CCI_REG8(0x91), 0x80 },
+	{ CCI_REG8(0x92), 0x28 },
+	{ CCI_REG8(0x93), 0x20 },
+	{ CCI_REG8(0x95), 0xa0 },
+	{ CCI_REG8(0x96), 0xe0 },
+	{ CCI_REG8(0xd5), 0xfc },
+	{ CCI_REG8(0x97), 0x28 },
+	{ CCI_REG8(0x16), 0x0c },
+	{ CCI_REG8(0x1a), 0x1a },
+	{ CCI_REG8(0x1f), 0x11 },
+	{ CCI_REG8(0x20), 0x10 },
+	{ CCI_REG8(0x46), 0x83 },
+	{ CCI_REG8(0x4a), 0x04 },
+	{ CCI_REG8(0x54), 0x02 },
+	{ CCI_REG8(0x62), 0x00 },
+	{ CCI_REG8(0x72), 0x8f },
+	{ CCI_REG8(0x73), 0x89 },
+	{ CCI_REG8(0x7a), 0x05 },
+	{ CCI_REG8(0x7d), 0xcc },
+	{ CCI_REG8(0x90), 0x00 },
+	{ CCI_REG8(0xce), 0x18 },
+	{ CCI_REG8(0xd0), 0xb2 },
+	{ CCI_REG8(0xd2), 0x40 },
+	{ CCI_REG8(0xe6), 0xe0 },
+	{ GC5035_REG_PAGE_SELECT, 0x02 },
+	{ CCI_REG8(0x12), 0x01 },
+	{ CCI_REG8(0x13), 0x01 },
+	{ CCI_REG8(0x14), 0x01 },
+	{ CCI_REG8(0x15), 0x02 },
+	{ CCI_REG8(0x22), 0x7c },
+	{ CCI_REG8(0x91), 0x00 },
+	{ CCI_REG8(0x92), 0x00 },
+	{ CCI_REG8(0x93), 0x00 },
+	{ CCI_REG8(0x94), 0x00 },
+	{ GC5035_REG_PAGE_SELECT, 0x00 },
+	{ CCI_REG8(0xfc), 0x88 },
+	{ GC5035_REG_PAGE_SELECT, 0x10 },	/* not a page */
+	{ GC5035_REG_PAGE_SELECT, 0x00 },
+	{ CCI_REG8(0xfc), 0x8e },
+	{ GC5035_REG_PAGE_SELECT, 0x00 },
+	{ GC5035_REG_PAGE_SELECT, 0x00 },
+	{ GC5035_REG_PAGE_SELECT, 0x00 },
+	{ CCI_REG8(0xfc), 0x88 },
+	{ GC5035_REG_PAGE_SELECT, 0x10 },	/* not a page */
+	{ GC5035_REG_PAGE_SELECT, 0x00 },
+	{ CCI_REG8(0xfc), 0x8e },
+	/* GAIN */
+	{ GC5035_REG_PAGE_SELECT, 0x00 },
+	{ CCI_REG8(0xb0), 0x6e },
+	{ GC5035_REG_DIGITAL_GAIN, GC5035_DIGITAL_GAIN_1X },
+	{ CCI_REG8(0xb3), 0x00 },
+	{ CCI_REG8(0xb4), 0x00 },
+	/* ISP */
+	{ GC5035_REG_PAGE_SELECT, 0x01 },
+	{ CCI_REG8(0x53), 0x00 },
+	{ CCI_REG8(0x89), 0x03 },
+	{ CCI_REG8(0x60), 0x40 },
+	/* BLK */
+	{ GC5035_REG_PAGE_SELECT, 0x01 },
+	{ CCI_REG8(0x42), 0x21 },
+	{ CCI_REG8(0x49), 0x03 },
+	{ CCI_REG8(0x4a), 0xff },
+	{ CCI_REG8(0x4b), 0xc0 },
+	{ CCI_REG8(0x55), 0x00 },
+	/* anti_blooming */
+	{ GC5035_REG_PAGE_SELECT, 0x01 },
+	{ CCI_REG8(0x41), 0x28 },
+	{ CCI_REG8(0x4c), 0x00 },
+	{ CCI_REG8(0x4d), 0x00 },
+	{ CCI_REG8(0x4e), 0x3c },
+	{ CCI_REG8(0x44), 0x08 },
+	{ CCI_REG8(0x48), 0x02 },
+	/* CROP */
+	{ GC5035_REG_PAGE_SELECT, 0x01 },
+	{ GC5035_REG_CROP_TOP, GC5035_CROP_TOP },
+	{ GC5035_REG_CROP_LEFT, GC5035_CROP_LEFT },
+	{ GC5035_REG_OUT_HEIGHT, GC5035_HEIGHT },
+	{ GC5035_REG_OUT_WIDTH, GC5035_WIDTH },
+	{ CCI_REG8(0x99), 0x00 },
+	/* MIPI */
+	{ GC5035_REG_PAGE_SELECT, 0x03 },
+	{ CCI_REG8(0x02), 0x57 },
+	{ CCI_REG8(0x03), 0xb7 },
+	{ CCI_REG8(0x15), 0x14 },
+	{ CCI_REG8(0x18), 0x0f },
+	{ CCI_REG8(0x21), 0x22 },
+	{ CCI_REG8(0x22), 0x06 },
+	{ CCI_REG8(0x23), 0x48 },
+	{ CCI_REG8(0x24), 0x12 },
+	{ CCI_REG8(0x25), 0x28 },
+	{ CCI_REG8(0x26), 0x08 },
+	{ CCI_REG8(0x29), 0x06 },
+	{ CCI_REG8(0x2a), 0x58 },
+	{ CCI_REG8(0x2b), 0x08 },
+	{ GC5035_REG_PAGE_SELECT, 0x00 },
+	{ GC5035_REG_STREAM, GC5035_STREAM_OFF },
+};
+
+static inline struct gc5035 *to_gc5035(struct v4l2_subdev *sd)
+{
+	return container_of(sd, struct gc5035, sd);
+}
+
+static int gc5035_set_page(struct gc5035 *gc5035, u8 page)
+{
+	return cci_write(gc5035->regmap, GC5035_REG_PAGE_SELECT, page, NULL);
+}
+
+static int gc5035_power_on(struct device *dev)
+{
+	struct v4l2_subdev *sd = dev_get_drvdata(dev);
+	struct gc5035 *gc5035 = to_gc5035(sd);
+	int ret;
+
+	ret = regulator_enable(gc5035->supplies[0].consumer);
+	if (ret) {
+		dev_err(dev, "failed to enable dovdd: %d\n", ret);
+		return ret;
+	}
+
+	fsleep(GC5035_IOVDD_DELAY_US);
+
+	ret = regulator_bulk_enable(ARRAY_SIZE(gc5035_supply_name) - 1,
+				    &gc5035->supplies[1]);
+	if (ret) {
+		dev_err(dev, "failed to enable regulators: %d\n", ret);
+		goto err_dovdd;
+	}
+
+	ret = clk_prepare_enable(gc5035->xclk);
+	if (ret) {
+		dev_err(dev, "failed to enable clock: %d\n", ret);
+		goto err_regulators;
+	}
+
+	gpiod_set_value_cansleep(gc5035->powerdown_gpio, 0);
+	gpiod_set_value_cansleep(gc5035->reset_gpio, 0);
+
+	fsleep(GC5035_RESET_DELAY_US);
+
+	return 0;
+
+err_regulators:
+	regulator_bulk_disable(ARRAY_SIZE(gc5035_supply_name) - 1,
+			       &gc5035->supplies[1]);
+err_dovdd:
+	regulator_disable(gc5035->supplies[0].consumer);
+
+	return ret;
+}
+
+static int gc5035_power_off(struct device *dev)
+{
+	struct v4l2_subdev *sd = dev_get_drvdata(dev);
+	struct gc5035 *gc5035 = to_gc5035(sd);
+
+	fsleep(GC5035_STOP_DELAY_US);
+
+	gpiod_set_value_cansleep(gc5035->powerdown_gpio, 1);
+	gpiod_set_value_cansleep(gc5035->reset_gpio, 1);
+	clk_disable_unprepare(gc5035->xclk);
+	regulator_bulk_disable(ARRAY_SIZE(gc5035_supply_name) - 1,
+			       &gc5035->supplies[1]);
+	regulator_disable(gc5035->supplies[0].consumer);
+
+	return 0;
+}
+
+static int gc5035_enum_mbus_code(struct v4l2_subdev *sd,
+				 struct v4l2_subdev_state *sd_state,
+				 struct v4l2_subdev_mbus_code_enum *code)
+{
+	if (code->index > 0)
+		return -EINVAL;
+
+	code->code = GC5035_MBUS_CODE;
+
+	return 0;
+}
+
+static int gc5035_enum_frame_size(struct v4l2_subdev *sd,
+				  struct v4l2_subdev_state *sd_state,
+				  struct v4l2_subdev_frame_size_enum *fse)
+{
+	if (fse->code != GC5035_MBUS_CODE)
+		return -EINVAL;
+
+	if (fse->index > 0)
+		return -EINVAL;
+
+	fse->min_width = GC5035_WIDTH;
+	fse->max_width = GC5035_WIDTH;
+	fse->min_height = GC5035_HEIGHT;
+	fse->max_height = GC5035_HEIGHT;
+
+	return 0;
+}
+
+static void gc5035_fill_state(struct v4l2_subdev_state *state)
+{
+	struct v4l2_mbus_framefmt *fmt;
+	struct v4l2_rect *crop;
+
+	crop = v4l2_subdev_state_get_crop(state, 0);
+	crop->left = GC5035_CROP_LEFT;
+	crop->top = GC5035_CROP_TOP;
+	crop->width = GC5035_WIDTH;
+	crop->height = GC5035_HEIGHT;
+
+	fmt = v4l2_subdev_state_get_format(state, 0);
+	fmt->width = GC5035_WIDTH;
+	fmt->height = GC5035_HEIGHT;
+	fmt->code = GC5035_MBUS_CODE;
+	fmt->field = V4L2_FIELD_NONE;
+	fmt->colorspace = V4L2_COLORSPACE_RAW;
+	fmt->ycbcr_enc = V4L2_MAP_YCBCR_ENC_DEFAULT(fmt->colorspace);
+	fmt->quantization = V4L2_QUANTIZATION_FULL_RANGE;
+	fmt->xfer_func = V4L2_XFER_FUNC_NONE;
+}
+
+static int gc5035_get_selection(struct v4l2_subdev *sd,
+				const struct v4l2_subdev_client_info *ci,
+				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, 0);
+		break;
+	case V4L2_SEL_TGT_CROP_DEFAULT:
+		sel->r.top = GC5035_CROP_TOP;
+		sel->r.left = GC5035_CROP_LEFT;
+		sel->r.width = GC5035_WIDTH;
+		sel->r.height = GC5035_HEIGHT;
+		break;
+	case V4L2_SEL_TGT_CROP_BOUNDS:
+	case V4L2_SEL_TGT_NATIVE_SIZE:
+		sel->r.top = 0;
+		sel->r.left = 0;
+		sel->r.width = GC5035_NATIVE_WIDTH;
+		sel->r.height = GC5035_NATIVE_HEIGHT;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+static int gc5035_init_state(struct v4l2_subdev *sd,
+			     struct v4l2_subdev_state *state)
+{
+	gc5035_fill_state(state);
+
+	return 0;
+}
+
+static int gc5035_get_frame_desc(struct v4l2_subdev *sd, unsigned int pad,
+				 struct v4l2_mbus_frame_desc *fd)
+{
+	fd->type = V4L2_MBUS_FRAME_DESC_TYPE_CSI2;
+	fd->num_entries = 1;
+	fd->entry[0].pixelcode = GC5035_MBUS_CODE;
+	fd->entry[0].stream = 0;
+	fd->entry[0].bus.csi2.vc = 0;
+	fd->entry[0].bus.csi2.dt = MIPI_CSI2_DT_RAW10;
+
+	return 0;
+}
+
+static int gc5035_test_pattern(struct gc5035 *gc5035, u32 pattern)
+{
+	int ret;
+
+	ret = gc5035_set_page(gc5035, GC5035_PAGE_1);
+	if (ret)
+		return ret;
+
+	ret = cci_write(gc5035->regmap, GC5035_REG_TEST_PATTERN,
+			pattern ? GC5035_TEST_PATTERN_ON :
+				  GC5035_TEST_PATTERN_OFF, NULL);
+
+	/* Back to page 0 even on error, the other controls expect it. */
+	return gc5035_set_page(gc5035, GC5035_PAGE_0) ?: ret;
+}
+
+static int gc5035_set_ctrl(struct v4l2_ctrl *ctrl)
+{
+	struct gc5035 *gc5035 =
+		container_of(ctrl->handler, struct gc5035, ctrls);
+	const struct v4l2_mbus_framefmt *format;
+	struct v4l2_subdev_state *state;
+	s64 exposure_max;
+	int ret;
+
+	state = v4l2_subdev_get_locked_active_state(&gc5035->sd);
+	format = v4l2_subdev_state_get_format(state, 0);
+
+	if (ctrl->id == V4L2_CID_VBLANK) {
+		exposure_max = format->height + ctrl->val -
+			       GC5035_EXP_MARGIN;
+		ret = __v4l2_ctrl_modify_range(gc5035->exposure,
+					       GC5035_EXP_MIN, exposure_max,
+					       GC5035_EXP_STEP, GC5035_EXP_DEF);
+		if (ret)
+			return ret;
+	}
+
+	if (!pm_runtime_get_if_active(gc5035->dev))
+		return 0;
+
+	ret = gc5035_set_page(gc5035, GC5035_PAGE_0);
+	if (ret)
+		goto out;
+
+	switch (ctrl->id) {
+	case V4L2_CID_EXPOSURE:
+		ret = cci_write(gc5035->regmap, GC5035_REG_EXPOSURE,
+				ctrl->val, NULL);
+		break;
+	case V4L2_CID_ANALOGUE_GAIN:
+		ret = cci_write(gc5035->regmap, GC5035_REG_ANALOGUE_GAIN,
+				gc5035_again_code[ctrl->val], NULL);
+		break;
+	case V4L2_CID_VBLANK:
+		ret = cci_write(gc5035->regmap, GC5035_REG_FRAME_LENGTH,
+				format->height + ctrl->val, NULL);
+		break;
+	case V4L2_CID_TEST_PATTERN:
+		ret = gc5035_test_pattern(gc5035, ctrl->val);
+		break;
+	default:
+		ret = -EINVAL;
+		break;
+	}
+
+out:
+	pm_runtime_put_autosuspend(gc5035->dev);
+
+	return ret;
+}
+
+static const struct v4l2_ctrl_ops gc5035_ctrl_ops = {
+	.s_ctrl = gc5035_set_ctrl,
+};
+
+/* Only ever called from probe, with the sensor powered up. */
+static int gc5035_identify_module(struct gc5035 *gc5035)
+{
+	u64 val;
+	int ret;
+
+	ret = cci_read(gc5035->regmap, GC5035_REG_CHIP_ID, &val, NULL);
+	if (ret)
+		return ret;
+
+	if (val != GC5035_CHIP_ID)
+		return dev_err_probe(gc5035->dev, -ENXIO,
+				     "chip id mismatch: %x != %llx\n",
+				     GC5035_CHIP_ID, val);
+
+	return 0;
+}
+
+static int gc5035_enable_streams(struct v4l2_subdev *sd,
+				 struct v4l2_subdev_state *state,
+				 u32 pad, u64 streams_mask)
+{
+	struct gc5035 *gc5035 = to_gc5035(sd);
+	int ret;
+
+	ret = pm_runtime_resume_and_get(gc5035->dev);
+	if (ret < 0)
+		return ret;
+
+	ret = cci_multi_reg_write(gc5035->regmap, gc5035_regs,
+				  ARRAY_SIZE(gc5035_regs), NULL);
+	if (ret)
+		goto err_rpm_put;
+
+	ret = __v4l2_ctrl_handler_setup(&gc5035->ctrls);
+	if (ret)
+		goto err_rpm_put;
+
+	ret = cci_write(gc5035->regmap, GC5035_REG_STREAM,
+			GC5035_STREAM_ON, NULL);
+	if (ret)
+		goto err_rpm_put;
+
+	return 0;
+
+err_rpm_put:
+	pm_runtime_put_autosuspend(gc5035->dev);
+
+	return ret;
+}
+
+static int gc5035_disable_streams(struct v4l2_subdev *sd,
+				  struct v4l2_subdev_state *state,
+				  u32 pad, u64 streams_mask)
+{
+	struct gc5035 *gc5035 = to_gc5035(sd);
+	int ret = 0;
+
+	/* cci_write() logs errors, nothing else can be done about them. */
+	cci_write(gc5035->regmap, GC5035_REG_PAGE_SELECT, GC5035_PAGE_0, &ret);
+	cci_write(gc5035->regmap, GC5035_REG_STREAM, GC5035_STREAM_OFF, &ret);
+
+	pm_runtime_put_autosuspend(gc5035->dev);
+
+	return 0;
+}
+
+static const struct v4l2_subdev_video_ops gc5035_video_ops = {
+	.s_stream = v4l2_subdev_s_stream_helper,
+};
+
+static const struct v4l2_subdev_pad_ops gc5035_pad_ops = {
+	.enum_mbus_code = gc5035_enum_mbus_code,
+	.enum_frame_size = gc5035_enum_frame_size,
+	.get_fmt = v4l2_subdev_get_fmt,
+	.get_selection = gc5035_get_selection,
+	.get_frame_desc = gc5035_get_frame_desc,
+	.enable_streams = gc5035_enable_streams,
+	.disable_streams = gc5035_disable_streams,
+};
+
+static const struct v4l2_subdev_ops gc5035_subdev_ops = {
+	.video = &gc5035_video_ops,
+	.pad = &gc5035_pad_ops,
+};
+
+static const struct v4l2_subdev_internal_ops gc5035_internal_ops = {
+	.init_state = gc5035_init_state,
+};
+
+static int gc5035_parse_fwnode(struct gc5035 *gc5035)
+{
+	struct v4l2_fwnode_endpoint bus_cfg = {
+		.bus_type = V4L2_MBUS_CSI2_DPHY,
+	};
+	struct device *dev = gc5035->dev;
+	struct fwnode_handle *endpoint;
+	unsigned long link_freq_bitmap;
+	int ret;
+
+	endpoint = fwnode_graph_get_endpoint_by_id(dev_fwnode(dev), 0, 0,
+						   FWNODE_GRAPH_ENDPOINT_NEXT);
+	ret = v4l2_fwnode_endpoint_alloc_parse(endpoint, &bus_cfg);
+	fwnode_handle_put(endpoint);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to parse endpoint\n");
+
+	/* The register table configures the CSI-2 transmitter for 2 lanes. */
+	if (bus_cfg.bus.mipi_csi2.num_data_lanes != GC5035_DATA_LANES) {
+		ret = dev_err_probe(dev, -EINVAL,
+				    "unsupported number of data lanes %u\n",
+				    bus_cfg.bus.mipi_csi2.num_data_lanes);
+		goto done;
+	}
+
+	ret = v4l2_link_freq_to_bitmap(dev, bus_cfg.link_frequencies,
+				       bus_cfg.nr_of_link_frequencies,
+				       gc5035_link_freq_menu,
+				       ARRAY_SIZE(gc5035_link_freq_menu),
+				       &link_freq_bitmap);
+
+done:
+	v4l2_fwnode_endpoint_free(&bus_cfg);
+
+	return ret;
+}
+
+static int gc5035_init_controls(struct gc5035 *gc5035)
+{
+	struct v4l2_fwnode_device_properties props;
+	struct v4l2_ctrl_handler *ctrl_hdlr;
+	struct v4l2_ctrl *link_freq, *hblank;
+	s64 exposure_max;
+	int ret;
+
+	ret = v4l2_fwnode_device_parse(gc5035->dev, &props);
+	if (ret)
+		return ret;
+
+	ctrl_hdlr = &gc5035->ctrls;
+	v4l2_ctrl_handler_init(ctrl_hdlr, 9);
+
+	link_freq = v4l2_ctrl_new_int_menu(ctrl_hdlr, NULL, V4L2_CID_LINK_FREQ,
+					   ARRAY_SIZE(gc5035_link_freq_menu) -
+					   1, 0, gc5035_link_freq_menu);
+
+	v4l2_ctrl_new_std(ctrl_hdlr, NULL, V4L2_CID_PIXEL_RATE,
+			  GC5035_PIXEL_RATE, GC5035_PIXEL_RATE, 1,
+			  GC5035_PIXEL_RATE);
+
+	v4l2_ctrl_new_std(ctrl_hdlr, &gc5035_ctrl_ops, V4L2_CID_VBLANK,
+			  GC5035_VTS_DEF - GC5035_HEIGHT,
+			  GC5035_VTS_DEF - GC5035_HEIGHT +
+			  round_down(GC5035_VTS_MAX - GC5035_VTS_DEF,
+				     GC5035_VBLANK_STEP),
+			  GC5035_VBLANK_STEP, GC5035_VTS_DEF - GC5035_HEIGHT);
+
+	hblank = v4l2_ctrl_new_std(ctrl_hdlr, NULL, V4L2_CID_HBLANK,
+				   GC5035_HTS - GC5035_WIDTH,
+				   GC5035_HTS - GC5035_WIDTH, 1,
+				   GC5035_HTS - GC5035_WIDTH);
+
+	exposure_max = GC5035_VTS_DEF - GC5035_EXP_MARGIN;
+	gc5035->exposure = v4l2_ctrl_new_std(ctrl_hdlr, &gc5035_ctrl_ops,
+					     V4L2_CID_EXPOSURE, GC5035_EXP_MIN,
+					     exposure_max, GC5035_EXP_STEP,
+					     GC5035_EXP_DEF);
+
+	v4l2_ctrl_new_std(ctrl_hdlr, &gc5035_ctrl_ops, V4L2_CID_ANALOGUE_GAIN,
+			  0, ARRAY_SIZE(gc5035_again_code) - 1, 1, 0);
+
+	v4l2_ctrl_new_std_menu_items(ctrl_hdlr, &gc5035_ctrl_ops,
+				     V4L2_CID_TEST_PATTERN,
+				     ARRAY_SIZE(gc5035_test_pattern_menu) - 1,
+				     0, 0, gc5035_test_pattern_menu);
+
+	v4l2_ctrl_new_fwnode_properties(ctrl_hdlr, &gc5035_ctrl_ops, &props);
+
+	if (ctrl_hdlr->error)
+		return v4l2_ctrl_handler_free(ctrl_hdlr);
+
+	link_freq->flags |= V4L2_CTRL_FLAG_READ_ONLY;
+	hblank->flags |= V4L2_CTRL_FLAG_READ_ONLY;
+
+	gc5035->sd.ctrl_handler = ctrl_hdlr;
+
+	return 0;
+}
+
+static int gc5035_probe(struct i2c_client *client)
+{
+	struct device *dev = &client->dev;
+	struct gc5035 *gc5035;
+	unsigned long freq;
+	int ret;
+
+	gc5035 = devm_kzalloc(dev, sizeof(*gc5035), GFP_KERNEL);
+	if (!gc5035)
+		return -ENOMEM;
+
+	gc5035->dev = dev;
+
+	v4l2_i2c_subdev_init(&gc5035->sd, client, &gc5035_subdev_ops);
+	gc5035->sd.internal_ops = &gc5035_internal_ops;
+
+	ret = gc5035_parse_fwnode(gc5035);
+	if (ret)
+		return ret;
+
+	gc5035->xclk = devm_v4l2_sensor_clk_get(dev, NULL);
+	if (IS_ERR(gc5035->xclk))
+		return dev_err_probe(dev, PTR_ERR(gc5035->xclk),
+				     "failed to get clock\n");
+
+	freq = clk_get_rate(gc5035->xclk);
+	if (freq != GC5035_XCLK_FREQ)
+		return dev_err_probe(dev, -EINVAL,
+				     "external clock %lu Hz not supported\n",
+				     freq);
+
+	gc5035->regmap = devm_cci_regmap_init_i2c(client, 8);
+	if (IS_ERR(gc5035->regmap))
+		return dev_err_probe(dev, PTR_ERR(gc5035->regmap),
+				     "failed to init CCI\n");
+
+	gc5035->reset_gpio = devm_gpiod_get_optional(dev, "reset",
+						     GPIOD_OUT_HIGH);
+	if (IS_ERR(gc5035->reset_gpio))
+		return dev_err_probe(dev, PTR_ERR(gc5035->reset_gpio),
+				     "failed to get reset GPIO\n");
+
+	gc5035->powerdown_gpio = devm_gpiod_get_optional(dev, "powerdown",
+							 GPIOD_OUT_HIGH);
+	if (IS_ERR(gc5035->powerdown_gpio))
+		return dev_err_probe(dev, PTR_ERR(gc5035->powerdown_gpio),
+				     "failed to get powerdown GPIO\n");
+
+	for (unsigned int i = 0; i < ARRAY_SIZE(gc5035_supply_name); i++)
+		gc5035->supplies[i].supply = gc5035_supply_name[i];
+
+	ret = devm_regulator_bulk_get(dev, ARRAY_SIZE(gc5035_supply_name),
+				      gc5035->supplies);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to get regulators\n");
+
+	ret = gc5035_power_on(dev);
+	if (ret)
+		return ret;
+
+	ret = gc5035_identify_module(gc5035);
+	if (ret)
+		goto err_power_off;
+
+	ret = gc5035_init_controls(gc5035);
+	if (ret) {
+		dev_err_probe(dev, ret, "failed to init controls\n");
+		goto err_power_off;
+	}
+
+	gc5035->sd.flags |= V4L2_SUBDEV_FL_HAS_DEVNODE;
+	gc5035->pad.flags = MEDIA_PAD_FL_SOURCE;
+	gc5035->sd.entity.function = MEDIA_ENT_F_CAM_SENSOR;
+
+	ret = media_entity_pads_init(&gc5035->sd.entity, 1, &gc5035->pad);
+	if (ret < 0) {
+		dev_err_probe(dev, ret, "failed to init media entity\n");
+		goto err_ctrl_handler_free;
+	}
+
+	gc5035->sd.state_lock = gc5035->ctrls.lock;
+	ret = v4l2_subdev_init_finalize(&gc5035->sd);
+	if (ret < 0) {
+		dev_err_probe(dev, ret, "failed to finalize subdev\n");
+		goto err_media_entity_cleanup;
+	}
+
+	pm_runtime_set_active(dev);
+	pm_runtime_enable(dev);
+	pm_runtime_set_autosuspend_delay(dev, 1000);
+	pm_runtime_use_autosuspend(dev);
+
+	ret = v4l2_async_register_subdev_sensor(&gc5035->sd);
+	if (ret < 0) {
+		dev_err_probe(dev, ret, "failed to register subdev\n");
+		goto err_rpm;
+	}
+
+	pm_runtime_idle(dev);
+
+	return 0;
+
+err_rpm:
+	pm_runtime_disable(dev);
+	pm_runtime_dont_use_autosuspend(dev);
+	pm_runtime_set_suspended(dev);
+	v4l2_subdev_cleanup(&gc5035->sd);
+
+err_media_entity_cleanup:
+	media_entity_cleanup(&gc5035->sd.entity);
+
+err_ctrl_handler_free:
+	v4l2_ctrl_handler_free(&gc5035->ctrls);
+
+err_power_off:
+	gc5035_power_off(dev);
+
+	return ret;
+}
+
+static void gc5035_remove(struct i2c_client *client)
+{
+	struct v4l2_subdev *sd = i2c_get_clientdata(client);
+	struct gc5035 *gc5035 = to_gc5035(sd);
+
+	v4l2_async_unregister_subdev(sd);
+	v4l2_subdev_cleanup(sd);
+	media_entity_cleanup(&sd->entity);
+	v4l2_ctrl_handler_free(&gc5035->ctrls);
+
+	pm_runtime_disable(&client->dev);
+	if (!pm_runtime_status_suspended(&client->dev)) {
+		gc5035_power_off(&client->dev);
+		pm_runtime_set_suspended(&client->dev);
+	}
+	pm_runtime_dont_use_autosuspend(&client->dev);
+}
+
+static const struct dev_pm_ops gc5035_pm_ops = {
+	RUNTIME_PM_OPS(gc5035_power_off, gc5035_power_on, NULL)
+};
+
+static const struct acpi_device_id gc5035_acpi_ids[] = {
+	{ "GCTI5035" },
+	{ }
+};
+MODULE_DEVICE_TABLE(acpi, gc5035_acpi_ids);
+
+static struct i2c_driver gc5035_i2c_driver = {
+	.driver = {
+		.name = "gc5035",
+		.acpi_match_table = gc5035_acpi_ids,
+		.pm = pm_ptr(&gc5035_pm_ops),
+	},
+	.probe = gc5035_probe,
+	.remove = gc5035_remove,
+};
+module_i2c_driver(gc5035_i2c_driver);
+
+MODULE_AUTHOR("Nicola Fiorillo <nicfio@gmail.com>");
+MODULE_DESCRIPTION("GalaxyCore GC5035 sensor driver");
+MODULE_LICENSE("GPL");
-- 
2.47.3


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

* [PATCH 2/3] media: i2c: Add GC8034 image sensor driver
  2026-10-09  7:27 [PATCH 0/3] media: Add GalaxyCore GC5035 and GC8034 sensor drivers Nicola Fiorillo
  2026-10-09  7:27 ` [PATCH 1/3] media: i2c: Add GC5035 image sensor driver Nicola Fiorillo
@ 2026-10-09  7:27 ` Nicola Fiorillo
  2026-10-09  7:27 ` [PATCH 3/3] media: ipu-bridge: Add GalaxyCore GC5035 and GC8034 Nicola Fiorillo
  2026-10-12 14:34 ` [PATCH v2 0/7] media: Add GalaxyCore GC5035 and GC8034 sensor drivers Nicola Fiorillo
  3 siblings, 0 replies; 15+ messages in thread
From: Nicola Fiorillo @ 2026-10-09  7:27 UTC (permalink / raw)
  To: linux-media
  Cc: Sakari Ailus, Laurent Pinchart, Mauro Carvalho Chehab,
	Hans Verkuil, Dan Scally, Hans de Goede, linux-kernel,
	Nicola Fiorillo

Add a driver for the GalaxyCore GC8034, an 8 Mpixel raw Bayer sensor with
a four lane MIPI CSI-2 interface. The driver supports a single 3264x2448
mode, and the exposure, analogue gain and vertical blanking controls.

The register tables come from the Rockchip BSP driver.

Assisted-by: claude-opus-5-5 coccinelle sparse smatch
Signed-off-by: Nicola Fiorillo <nicfio@gmail.com>
---
 MAINTAINERS                |   6 +
 drivers/media/i2c/Kconfig  |  10 +
 drivers/media/i2c/Makefile |   1 +
 drivers/media/i2c/gc8034.c | 969 +++++++++++++++++++++++++++++++++++++
 4 files changed, 986 insertions(+)
 create mode 100644 drivers/media/i2c/gc8034.c

diff --git a/MAINTAINERS b/MAINTAINERS
index c4d5f81f4..ba60329a2 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -10881,6 +10881,12 @@ L:	linux-media@vger.kernel.org
 S:	Maintained
 F:	drivers/media/i2c/gc5035.c
 
+GALAXYCORE GC8034 CAMERA SENSOR DRIVER
+M:	Nicola Fiorillo <nicfio@gmail.com>
+L:	linux-media@vger.kernel.org
+S:	Maintained
+F:	drivers/media/i2c/gc8034.c
+
 GATEWORKS SYSTEM CONTROLLER (GSC) DRIVER
 M:	Tim Harvey <tharvey@gateworks.com>
 S:	Maintained
diff --git a/drivers/media/i2c/Kconfig b/drivers/media/i2c/Kconfig
index bde998fca..80e0d1428 100644
--- a/drivers/media/i2c/Kconfig
+++ b/drivers/media/i2c/Kconfig
@@ -120,6 +120,16 @@ config VIDEO_GC5035
 	  To compile this driver as a module, choose M here: the
 	  module will be called gc5035.
 
+config VIDEO_GC8034
+	tristate "GalaxyCore GC8034 sensor support"
+	select V4L2_CCI_I2C
+	help
+	  This is a Video4Linux2 sensor driver for the GalaxyCore GC8034
+	  8 Mpixel camera.
+
+	  To compile this driver as a module, choose M here: the
+	  module will be called gc8034.
+
 config VIDEO_HI556
 	tristate "Hynix Hi-556 sensor support"
 	help
diff --git a/drivers/media/i2c/Makefile b/drivers/media/i2c/Makefile
index c3a7f39bd..2c00dc96d 100644
--- a/drivers/media/i2c/Makefile
+++ b/drivers/media/i2c/Makefile
@@ -43,6 +43,7 @@ obj-$(CONFIG_VIDEO_GC05A2) += gc05a2.o
 obj-$(CONFIG_VIDEO_GC08A3) += gc08a3.o
 obj-$(CONFIG_VIDEO_GC2145) += gc2145.o
 obj-$(CONFIG_VIDEO_GC5035) += gc5035.o
+obj-$(CONFIG_VIDEO_GC8034) += gc8034.o
 obj-$(CONFIG_VIDEO_HI556) += hi556.o
 obj-$(CONFIG_VIDEO_HI846) += hi846.o
 obj-$(CONFIG_VIDEO_HI847) += hi847.o
diff --git a/drivers/media/i2c/gc8034.c b/drivers/media/i2c/gc8034.c
new file mode 100644
index 000000000..a74c190d2
--- /dev/null
+++ b/drivers/media/i2c/gc8034.c
@@ -0,0 +1,969 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Driver for GalaxyCore GC8034 image sensor
+ *
+ * Copyright (C) 2017 Fuzhou Rockchip Electronics Co., Ltd.
+ * Copyright (C) 2026 Nicola Fiorillo <nicfio@gmail.com>
+ *
+ * The registers and sequences come from the Rockchip BSP driver,
+ * rockchip-linux/kernel, branch develop-5.10.
+ */
+#include <linux/array_size.h>
+#include <linux/clk.h>
+#include <linux/container_of.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/math.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/pm_runtime.h>
+#include <linux/property.h>
+#include <linux/regulator/consumer.h>
+#include <linux/time64.h>
+#include <linux/types.h>
+#include <linux/units.h>
+
+#include <media/mipi-csi2.h>
+#include <media/v4l2-cci.h>
+#include <media/v4l2-common.h>
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-fwnode.h>
+#include <media/v4l2-subdev.h>
+
+/*
+ * 8 bit register addresses, with the page selected by register 0xfe. The
+ * vendor sequence also writes 0x10 to register 0xfe, which is not a page
+ * number and is not documented, so the pages are not handled with regmap
+ * ranges.
+ */
+#define GC8034_REG_PAGE_SELECT		CCI_REG8(0xfe)
+#define GC8034_PAGE_0			0x00
+
+/* readable on any page */
+#define GC8034_REG_CHIP_ID		CCI_REG16(0xf0)
+#define GC8034_CHIP_ID			0x8044	/* not 0x8034 */
+
+#define GC8034_REG_EXPOSURE		CCI_REG16(0x03)
+#define GC8034_REG_LINE_LENGTH		CCI_REG16(0x05)
+#define GC8034_REG_BLANKING		CCI_REG16(0x07)
+#define GC8034_REG_WIN_TOP		CCI_REG8(0x0a)
+#define GC8034_REG_WIN_LEFT		CCI_REG16(0x0b)
+#define GC8034_REG_WIN_HEIGHT		CCI_REG16(0x0d)
+#define GC8034_REG_WIN_WIDTH		CCI_REG16(0x0f)
+#define GC8034_REG_MIRROR		CCI_REG8(0x17)
+#define GC8034_MIRROR_NONE		0xc0
+#define GC8034_REG_STREAM		CCI_REG8(0x3f)
+#define GC8034_STREAM_ON		0xd0
+#define GC8034_STREAM_OFF		0x00
+#define GC8034_REG_CROP_TOP		CCI_REG8(0x92)
+#define GC8034_REG_CROP_LEFT		CCI_REG8(0x94)
+#define GC8034_REG_OUT_HEIGHT		CCI_REG16(0x95)
+#define GC8034_REG_OUT_WIDTH		CCI_REG16(0x97)
+#define GC8034_REG_DIGITAL_GAIN	CCI_REG16(0xb1)
+#define GC8034_DIGITAL_GAIN_1X	0x100
+#define GC8034_REG_ANALOGUE_GAIN	CCI_REG8(0xb6)
+
+/*
+ * The sensor reads out a 3284x2464 window starting at (4, 58) and crops
+ * 3264x2448 from it at (9, 8). The odd crop column gives the RGGB order; the
+ * vendor code moves it by one column when mirroring. The size of the full
+ * pixel array is not documented, so the readout window is reported as the
+ * native size.
+ */
+#define GC8034_WIN_LEFT			4
+#define GC8034_WIN_TOP			58
+#define GC8034_NATIVE_WIDTH		3284
+#define GC8034_NATIVE_HEIGHT		2464
+#define GC8034_CROP_LEFT		9
+#define GC8034_CROP_TOP			8
+#define GC8034_WIDTH			3264
+#define GC8034_HEIGHT			2448
+
+/*
+ * The blanking register holds the frame length minus the active height and
+ * 36 lines, as computed by the vendor code.
+ */
+#define GC8034_VTS_OFFSET		(GC8034_HEIGHT + 36)
+#define GC8034_VTS_MAX			0x1fff
+
+/* The sensor only takes even exposure values. */
+#define GC8034_EXP_MIN			4
+#define GC8034_EXP_STEP			2
+#define GC8034_EXP_MARGIN		4
+#define GC8034_EXP_DEF			2246
+
+/*
+ * The register table has only been tested with a 19.2 MHz external clock. The
+ * vendor code declares 336 MHz and 30 fps for a 24 MHz clock; at 19.2 MHz
+ * everything runs at 0.8 times that: 268.8 MHz and 24 fps, as measured. The
+ * PLL settings are not documented, so the pixel rate is not computed from
+ * them: it is the one that gives the measured 24 fps with the line and frame
+ * lengths below. The line length register counts units of 8 pixels.
+ */
+#define GC8034_XCLK_FREQ		(19200 * HZ_PER_KHZ)
+#define GC8034_LINK_FREQ		(268800 * HZ_PER_KHZ)
+#define GC8034_DATA_LANES		4
+#define GC8034_HTS			4272
+#define GC8034_VTS_DEF			2496
+#define GC8034_PIXEL_RATE		(GC8034_HTS * GC8034_VTS_DEF * 24)
+
+/*
+ * No datasheet is available. The power sequence and delays follow the vendor
+ * driver: supplies in the order of gc8034_supply_name, 100 us, clock, 1 ms,
+ * powerdown released, 500 us, reset released, then 6 ms and 8192 clock cycles
+ * before the first I2C access.
+ */
+#define GC8034_SUPPLY_DELAY_US		100
+#define GC8034_CLK_DELAY_US		1000
+#define GC8034_PWDN_DELAY_US		500
+#define GC8034_RESET_DELAY_US		(6000 + \
+					 DIV_ROUND_UP(8192 * USEC_PER_MSEC, \
+						      GC8034_XCLK_FREQ / \
+						      MSEC_PER_SEC))
+
+#define GC8034_MBUS_CODE		MEDIA_BUS_FMT_SRGGB10_1X10
+
+static const s64 gc8034_link_freq_menu[] = {
+	GC8034_LINK_FREQ,
+};
+
+static const char * const gc8034_supply_name[] = {
+	"dovdd",
+	"dvdd",
+	"avdd",
+};
+
+/*
+ * V4L2_CID_ANALOGUE_GAIN is the value of register 0xb6, the index of the
+ * analogue gain step. Only the seven steps the vendor code uses are exposed.
+ * Each step also needs a set of analogue bias registers to be rewritten; the
+ * sequence selects its own pages through register 0xfe. The digital gain is
+ * left at the 1.0x the register table programs.
+ */
+static const u8 gc8034_agc_bias_reg[] = {
+	0xfe, 0x20, 0x33, 0xfe, 0xdf, 0xe7, 0xe8,
+	0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xfe,
+};
+
+static const u8 gc8034_agc_bias[][ARRAY_SIZE(gc8034_agc_bias_reg)] = {
+	/* 1.000x */
+	{ 0x00, 0x55, 0x83, 0x01, 0x06, 0x18, 0x20,
+	  0x16, 0x17, 0x50, 0x6c, 0x9b, 0xd8, 0x00 },
+	/* 1.375x */
+	{ 0x00, 0x55, 0x83, 0x01, 0x06, 0x18, 0x20,
+	  0x16, 0x17, 0x50, 0x6c, 0x9b, 0xd8, 0x00 },
+	/* 1.950x */
+	{ 0x00, 0x4e, 0x84, 0x01, 0x0c, 0x2e, 0x2d,
+	  0x15, 0x19, 0x47, 0x70, 0x9f, 0xd8, 0x00 },
+	/* 2.700x */
+	{ 0x00, 0x51, 0x80, 0x01, 0x07, 0x28, 0x32,
+	  0x22, 0x20, 0x49, 0x70, 0x91, 0xd9, 0x00 },
+	/* 3.800x */
+	{ 0x00, 0x4d, 0x83, 0x01, 0x0f, 0x3b, 0x3b,
+	  0x1c, 0x1f, 0x47, 0x6f, 0x9b, 0xd3, 0x00 },
+	/* 5.400x */
+	{ 0x00, 0x50, 0x83, 0x01, 0x08, 0x35, 0x46,
+	  0x1e, 0x22, 0x4c, 0x70, 0x9a, 0xd2, 0x00 },
+	/* 7.660x */
+	{ 0x00, 0x52, 0x80, 0x01, 0x0c, 0x35, 0x3a,
+	  0x2b, 0x2d, 0x4c, 0x67, 0x8d, 0xc0, 0x00 },
+};
+
+struct gc8034 {
+	struct device *dev;
+	struct v4l2_subdev sd;
+	struct media_pad pad;
+
+	struct clk *xclk;
+	struct regulator_bulk_data supplies[ARRAY_SIZE(gc8034_supply_name)];
+	struct gpio_desc *reset_gpio;
+	struct gpio_desc *powerdown_gpio;
+
+	struct v4l2_ctrl_handler ctrls;
+	struct v4l2_ctrl *exposure;
+
+	struct regmap *regmap;
+};
+
+/*
+ * The register sequence is the vendor one for the 3264x2448 mode, without the
+ * registers written by the controls and without the settings of a binned mode
+ * that it programs first and then overwrites. No register documentation is
+ * available: the names of the geometry registers are inferred from their
+ * values.
+ */
+static const struct cci_reg_sequence gc8034_regs[] = {
+	/* SYS */
+	{ CCI_REG8(0xf2), 0x00 },
+	{ CCI_REG8(0xf4), 0x80 },
+	{ CCI_REG8(0xf5), 0x19 },
+	{ CCI_REG8(0xf6), 0x44 },
+	{ CCI_REG8(0xf8), 0x63 },
+	{ CCI_REG8(0xfa), 0x45 },
+	{ CCI_REG8(0xf9), 0x00 },
+	{ CCI_REG8(0xf7), 0x9d },
+	{ CCI_REG8(0xfc), 0x00 },
+	{ CCI_REG8(0xfc), 0x00 },
+	{ CCI_REG8(0xfc), 0xea },
+	{ GC8034_REG_PAGE_SELECT, 0x03 },
+	{ CCI_REG8(0x03), 0x9a },
+	{ CCI_REG8(0x18), 0x07 },
+	{ CCI_REG8(0x01), 0x07 },
+	{ CCI_REG8(0xfc), 0xee },
+	/* Cisctl&Analog */
+	{ GC8034_REG_PAGE_SELECT, 0x00 },
+	{ GC8034_REG_LINE_LENGTH, GC8034_HTS / 8 },
+	{ GC8034_REG_WIN_TOP, GC8034_WIN_TOP },
+	{ GC8034_REG_WIN_LEFT, GC8034_WIN_LEFT },
+	{ GC8034_REG_WIN_HEIGHT, GC8034_NATIVE_HEIGHT },
+	{ GC8034_REG_WIN_WIDTH, GC8034_NATIVE_WIDTH },
+	{ GC8034_REG_MIRROR, GC8034_MIRROR_NONE },
+	{ CCI_REG8(0x18), 0x02 },
+	{ CCI_REG8(0x19), 0x17 },
+	{ CCI_REG8(0x1e), 0x50 },
+	{ CCI_REG8(0x1f), 0x80 },
+	{ CCI_REG8(0x21), 0x4c },
+	{ CCI_REG8(0x25), 0x00 },
+	{ CCI_REG8(0x28), 0x4a },
+	{ CCI_REG8(0x2d), 0x89 },
+	{ CCI_REG8(0xca), 0x02 },
+	{ CCI_REG8(0xcb), 0x00 },
+	{ CCI_REG8(0xcc), 0x39 },
+	{ CCI_REG8(0xce), 0xd0 },
+	{ CCI_REG8(0xcf), 0x93 },
+	{ CCI_REG8(0xd0), 0x19 },
+	{ CCI_REG8(0xd1), 0xaa },
+	{ CCI_REG8(0xd2), 0xcb },
+	{ CCI_REG8(0xd8), 0x40 },
+	{ CCI_REG8(0xd9), 0xff },
+	{ CCI_REG8(0xda), 0x0e },
+	{ CCI_REG8(0xdb), 0xb0 },
+	{ CCI_REG8(0xdc), 0x0e },
+	{ CCI_REG8(0xde), 0x08 },
+	{ CCI_REG8(0xe4), 0xc6 },
+	{ CCI_REG8(0xe5), 0x08 },
+	{ CCI_REG8(0xe6), 0x10 },
+	{ CCI_REG8(0xed), 0x2a },
+	{ GC8034_REG_PAGE_SELECT, 0x02 },
+	{ CCI_REG8(0x59), 0x02 },
+	{ CCI_REG8(0x5a), 0x04 },
+	{ CCI_REG8(0x5b), 0x08 },
+	{ CCI_REG8(0x5c), 0x20 },
+	{ GC8034_REG_PAGE_SELECT, 0x00 },
+	{ CCI_REG8(0x1a), 0x09 },
+	{ CCI_REG8(0x1d), 0x13 },
+	{ GC8034_REG_PAGE_SELECT, 0x10 },	/* not a page */
+	{ GC8034_REG_PAGE_SELECT, 0x00 },
+	{ GC8034_REG_PAGE_SELECT, 0x10 },	/* not a page */
+	/* ISP */
+	{ GC8034_REG_PAGE_SELECT, 0x00 },
+	{ CCI_REG8(0x84), 0x01 },
+	{ CCI_REG8(0x88), 0x03 },
+	{ CCI_REG8(0x89), 0x03 },
+	{ CCI_REG8(0x8d), 0x03 },
+	{ CCI_REG8(0x8f), 0x14 },
+	{ CCI_REG8(0x66), 0x2c },
+	{ CCI_REG8(0xbc), 0x49 },
+	{ CCI_REG8(0xc2), 0x7f },
+	{ CCI_REG8(0xc3), 0xff },
+	/* Gain */
+	{ CCI_REG8(0xb0), 0x90 },
+	{ GC8034_REG_DIGITAL_GAIN, GC8034_DIGITAL_GAIN_1X },
+	/* BLK */
+	{ GC8034_REG_PAGE_SELECT, 0x00 },
+	{ CCI_REG8(0x40), 0x22 },
+	{ CCI_REG8(0x41), 0x20 },
+	{ CCI_REG8(0x42), 0x02 },
+	{ CCI_REG8(0x43), 0x08 },
+	{ CCI_REG8(0x4e), 0x0f },
+	{ CCI_REG8(0x4f), 0xf0 },
+	{ CCI_REG8(0x58), 0x80 },
+	{ CCI_REG8(0x59), 0x80 },
+	{ CCI_REG8(0x5a), 0x80 },
+	{ CCI_REG8(0x5b), 0x80 },
+	{ CCI_REG8(0x5c), 0x00 },
+	{ CCI_REG8(0x5d), 0x00 },
+	{ CCI_REG8(0x5e), 0x00 },
+	{ CCI_REG8(0x5f), 0x00 },
+	{ CCI_REG8(0x6b), 0x01 },
+	{ CCI_REG8(0x6c), 0x00 },
+	{ CCI_REG8(0x6d), 0x0c },
+	/* WB offset */
+	{ GC8034_REG_PAGE_SELECT, 0x01 },
+	{ CCI_REG8(0xbf), 0x40 },
+	/* Dark Sun */
+	{ GC8034_REG_PAGE_SELECT, 0x01 },
+	{ CCI_REG8(0x68), 0x77 },
+	/* DPC */
+	{ GC8034_REG_PAGE_SELECT, 0x01 },
+	{ CCI_REG8(0x60), 0x00 },
+	{ CCI_REG8(0x61), 0x10 },
+	{ CCI_REG8(0x64), 0x02 },
+	/* LSC */
+	{ GC8034_REG_PAGE_SELECT, 0x01 },
+	{ CCI_REG8(0xa8), 0x60 },
+	{ CCI_REG8(0xa2), 0xd1 },
+	{ CCI_REG8(0xc8), 0x57 },
+	{ CCI_REG8(0xa1), 0xb8 },
+	{ CCI_REG8(0xa3), 0x91 },
+	{ CCI_REG8(0xc0), 0x50 },
+	{ CCI_REG8(0xd0), 0x05 },
+	{ CCI_REG8(0xd1), 0xb2 },
+	{ CCI_REG8(0xd2), 0x1f },
+	{ CCI_REG8(0xd3), 0x00 },
+	{ CCI_REG8(0xd4), 0x00 },
+	{ CCI_REG8(0xd5), 0x00 },
+	{ CCI_REG8(0xd6), 0x00 },
+	{ CCI_REG8(0xd7), 0x00 },
+	{ CCI_REG8(0xd8), 0x00 },
+	{ CCI_REG8(0xd9), 0x00 },
+	{ CCI_REG8(0xa4), 0x10 },
+	{ CCI_REG8(0xa5), 0x20 },
+	{ CCI_REG8(0xa6), 0x60 },
+	{ CCI_REG8(0xa7), 0x80 },
+	{ CCI_REG8(0xab), 0x18 },
+	{ CCI_REG8(0xc7), 0xc0 },
+	/* ABB */
+	{ GC8034_REG_PAGE_SELECT, 0x01 },
+	{ CCI_REG8(0x20), 0x02 },
+	{ CCI_REG8(0x21), 0x02 },
+	{ CCI_REG8(0x23), 0x42 },
+	{ GC8034_REG_PAGE_SELECT, 0x00 },
+	/* SYS */
+	{ CCI_REG8(0xf2), 0x00 },
+	{ CCI_REG8(0xf4), 0x80 },
+	{ CCI_REG8(0xf5), 0x19 },
+	{ CCI_REG8(0xf6), 0x44 },
+	{ CCI_REG8(0xf8), 0x63 },
+	{ CCI_REG8(0xfa), 0x45 },
+	{ CCI_REG8(0xf9), 0x00 },
+	{ CCI_REG8(0xf7), 0x95 },
+	{ CCI_REG8(0xfc), 0x00 },
+	{ CCI_REG8(0xfc), 0x00 },
+	{ CCI_REG8(0xfc), 0xea },
+	{ GC8034_REG_PAGE_SELECT, 0x03 },
+	{ CCI_REG8(0x03), 0x9a },
+	{ CCI_REG8(0x18), 0x07 },
+	{ CCI_REG8(0x01), 0x07 },
+	{ CCI_REG8(0xfc), 0xee },
+	/* ISP */
+	{ GC8034_REG_PAGE_SELECT, 0x00 },
+	{ CCI_REG8(0x80), 0x13 },
+	{ CCI_REG8(0xad), 0x00 },
+	/* Crop window */
+	{ CCI_REG8(0x90), 0x01 },
+	{ GC8034_REG_CROP_TOP, GC8034_CROP_TOP },
+	{ GC8034_REG_CROP_LEFT, GC8034_CROP_LEFT },
+	{ GC8034_REG_OUT_HEIGHT, GC8034_HEIGHT },
+	{ GC8034_REG_OUT_WIDTH, GC8034_WIDTH },
+	/* DPC */
+	{ GC8034_REG_PAGE_SELECT, 0x01 },
+	{ CCI_REG8(0x62), 0x60 },
+	{ CCI_REG8(0x63), 0x48 },
+	/* MIPI */
+	{ GC8034_REG_PAGE_SELECT, 0x03 },
+	{ CCI_REG8(0x02), 0x03 },
+	{ CCI_REG8(0x04), 0x80 },
+	{ CCI_REG8(0x11), 0x2b },
+	{ CCI_REG8(0x12), 0xf0 },
+	{ CCI_REG8(0x13), 0x0f },
+	{ CCI_REG8(0x15), 0x10 },
+	{ CCI_REG8(0x16), 0x29 },
+	{ CCI_REG8(0x17), 0xff },
+	{ CCI_REG8(0x19), 0xaa },
+	{ CCI_REG8(0x1a), 0x02 },
+	{ CCI_REG8(0x21), 0x05 },
+	{ CCI_REG8(0x22), 0x06 },
+	{ CCI_REG8(0x23), 0x2b },
+	{ CCI_REG8(0x24), 0x00 },
+	{ CCI_REG8(0x25), 0x12 },
+	{ CCI_REG8(0x26), 0x07 },
+	{ CCI_REG8(0x29), 0x07 },
+	{ CCI_REG8(0x2a), 0x12 },
+	{ CCI_REG8(0x2b), 0x07 },
+	{ GC8034_REG_PAGE_SELECT, 0x00 },
+	{ GC8034_REG_STREAM, GC8034_STREAM_OFF },
+};
+
+static inline struct gc8034 *to_gc8034(struct v4l2_subdev *sd)
+{
+	return container_of(sd, struct gc8034, sd);
+}
+
+static void gc8034_disable_supplies(struct gc8034 *gc8034, unsigned int num)
+{
+	while (num--)
+		regulator_disable(gc8034->supplies[num].consumer);
+}
+
+static int gc8034_power_on(struct device *dev)
+{
+	struct v4l2_subdev *sd = dev_get_drvdata(dev);
+	struct gc8034 *gc8034 = to_gc8034(sd);
+	unsigned int i;
+	int ret;
+
+	/* One by one: regulator_bulk_enable() works in parallel. */
+	for (i = 0; i < ARRAY_SIZE(gc8034_supply_name); i++) {
+		ret = regulator_enable(gc8034->supplies[i].consumer);
+		if (ret) {
+			dev_err(dev, "failed to enable %s: %d\n",
+				gc8034_supply_name[i], ret);
+			gc8034_disable_supplies(gc8034, i);
+			return ret;
+		}
+	}
+
+	fsleep(GC8034_SUPPLY_DELAY_US);
+
+	ret = clk_prepare_enable(gc8034->xclk);
+	if (ret) {
+		dev_err(dev, "failed to enable clock: %d\n", ret);
+		gc8034_disable_supplies(gc8034, ARRAY_SIZE(gc8034_supply_name));
+		return ret;
+	}
+
+	fsleep(GC8034_CLK_DELAY_US);
+	gpiod_set_value_cansleep(gc8034->powerdown_gpio, 0);
+	fsleep(GC8034_PWDN_DELAY_US);
+	gpiod_set_value_cansleep(gc8034->reset_gpio, 0);
+	fsleep(GC8034_RESET_DELAY_US);
+
+	return 0;
+}
+
+static int gc8034_power_off(struct device *dev)
+{
+	struct v4l2_subdev *sd = dev_get_drvdata(dev);
+	struct gc8034 *gc8034 = to_gc8034(sd);
+
+	gpiod_set_value_cansleep(gc8034->powerdown_gpio, 1);
+	gpiod_set_value_cansleep(gc8034->reset_gpio, 1);
+	clk_disable_unprepare(gc8034->xclk);
+	gc8034_disable_supplies(gc8034, ARRAY_SIZE(gc8034_supply_name));
+
+	return 0;
+}
+
+static int gc8034_enum_mbus_code(struct v4l2_subdev *sd,
+				 struct v4l2_subdev_state *sd_state,
+				 struct v4l2_subdev_mbus_code_enum *code)
+{
+	if (code->index > 0)
+		return -EINVAL;
+
+	code->code = GC8034_MBUS_CODE;
+
+	return 0;
+}
+
+static int gc8034_enum_frame_size(struct v4l2_subdev *sd,
+				  struct v4l2_subdev_state *sd_state,
+				  struct v4l2_subdev_frame_size_enum *fse)
+{
+	if (fse->code != GC8034_MBUS_CODE)
+		return -EINVAL;
+
+	if (fse->index > 0)
+		return -EINVAL;
+
+	fse->min_width = GC8034_WIDTH;
+	fse->max_width = GC8034_WIDTH;
+	fse->min_height = GC8034_HEIGHT;
+	fse->max_height = GC8034_HEIGHT;
+
+	return 0;
+}
+
+static void gc8034_fill_state(struct v4l2_subdev_state *state)
+{
+	struct v4l2_mbus_framefmt *fmt;
+	struct v4l2_rect *crop;
+
+	crop = v4l2_subdev_state_get_crop(state, 0);
+	crop->left = GC8034_CROP_LEFT;
+	crop->top = GC8034_CROP_TOP;
+	crop->width = GC8034_WIDTH;
+	crop->height = GC8034_HEIGHT;
+
+	fmt = v4l2_subdev_state_get_format(state, 0);
+	fmt->width = GC8034_WIDTH;
+	fmt->height = GC8034_HEIGHT;
+	fmt->code = GC8034_MBUS_CODE;
+	fmt->field = V4L2_FIELD_NONE;
+	fmt->colorspace = V4L2_COLORSPACE_RAW;
+	fmt->ycbcr_enc = V4L2_MAP_YCBCR_ENC_DEFAULT(fmt->colorspace);
+	fmt->quantization = V4L2_QUANTIZATION_FULL_RANGE;
+	fmt->xfer_func = V4L2_XFER_FUNC_NONE;
+}
+
+static int gc8034_get_selection(struct v4l2_subdev *sd,
+				const struct v4l2_subdev_client_info *ci,
+				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, 0);
+		break;
+	case V4L2_SEL_TGT_CROP_DEFAULT:
+		sel->r.top = GC8034_CROP_TOP;
+		sel->r.left = GC8034_CROP_LEFT;
+		sel->r.width = GC8034_WIDTH;
+		sel->r.height = GC8034_HEIGHT;
+		break;
+	case V4L2_SEL_TGT_CROP_BOUNDS:
+	case V4L2_SEL_TGT_NATIVE_SIZE:
+		sel->r.top = 0;
+		sel->r.left = 0;
+		sel->r.width = GC8034_NATIVE_WIDTH;
+		sel->r.height = GC8034_NATIVE_HEIGHT;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+static int gc8034_init_state(struct v4l2_subdev *sd,
+			     struct v4l2_subdev_state *state)
+{
+	gc8034_fill_state(state);
+
+	return 0;
+}
+
+static int gc8034_get_frame_desc(struct v4l2_subdev *sd, unsigned int pad,
+				 struct v4l2_mbus_frame_desc *fd)
+{
+	fd->type = V4L2_MBUS_FRAME_DESC_TYPE_CSI2;
+	fd->num_entries = 1;
+	fd->entry[0].pixelcode = GC8034_MBUS_CODE;
+	fd->entry[0].stream = 0;
+	fd->entry[0].bus.csi2.vc = 0;
+	fd->entry[0].bus.csi2.dt = MIPI_CSI2_DT_RAW10;
+
+	return 0;
+}
+
+static int gc8034_set_analogue_gain(struct gc8034 *gc8034, u32 idx)
+{
+	unsigned int i;
+	int ret = 0;
+
+	cci_write(gc8034->regmap, GC8034_REG_ANALOGUE_GAIN, idx, &ret);
+
+	for (i = 0; i < ARRAY_SIZE(gc8034_agc_bias_reg); i++)
+		cci_write(gc8034->regmap, CCI_REG8(gc8034_agc_bias_reg[i]),
+			  gc8034_agc_bias[idx][i], &ret);
+
+	return ret;
+}
+
+static int gc8034_set_ctrl(struct v4l2_ctrl *ctrl)
+{
+	struct gc8034 *gc8034 =
+		container_of(ctrl->handler, struct gc8034, ctrls);
+	const struct v4l2_mbus_framefmt *format;
+	struct v4l2_subdev_state *state;
+	s64 exposure_max;
+	int ret;
+
+	state = v4l2_subdev_get_locked_active_state(&gc8034->sd);
+	format = v4l2_subdev_state_get_format(state, 0);
+
+	if (ctrl->id == V4L2_CID_VBLANK) {
+		exposure_max = round_down(format->height + ctrl->val -
+					  GC8034_EXP_MARGIN, GC8034_EXP_STEP);
+		ret = __v4l2_ctrl_modify_range(gc8034->exposure,
+					       GC8034_EXP_MIN, exposure_max,
+					       GC8034_EXP_STEP, GC8034_EXP_DEF);
+		if (ret)
+			return ret;
+	}
+
+	if (!pm_runtime_get_if_active(gc8034->dev))
+		return 0;
+
+	ret = cci_write(gc8034->regmap, GC8034_REG_PAGE_SELECT, GC8034_PAGE_0,
+			NULL);
+	if (ret)
+		goto out;
+
+	switch (ctrl->id) {
+	case V4L2_CID_EXPOSURE:
+		ret = cci_write(gc8034->regmap, GC8034_REG_EXPOSURE,
+				ctrl->val, NULL);
+		break;
+	case V4L2_CID_ANALOGUE_GAIN:
+		ret = gc8034_set_analogue_gain(gc8034, ctrl->val);
+		break;
+	case V4L2_CID_VBLANK:
+		/* The register holds an offset, not the VTS. */
+		ret = cci_write(gc8034->regmap, GC8034_REG_BLANKING,
+				format->height + ctrl->val -
+				GC8034_VTS_OFFSET, NULL);
+		break;
+	default:
+		ret = -EINVAL;
+		break;
+	}
+
+out:
+	pm_runtime_put_autosuspend(gc8034->dev);
+
+	return ret;
+}
+
+static const struct v4l2_ctrl_ops gc8034_ctrl_ops = {
+	.s_ctrl = gc8034_set_ctrl,
+};
+
+/* Only ever called from probe, with the sensor powered up. */
+static int gc8034_identify_module(struct gc8034 *gc8034)
+{
+	u64 val;
+	int ret;
+
+	ret = cci_read(gc8034->regmap, GC8034_REG_CHIP_ID, &val, NULL);
+	if (ret)
+		return ret;
+
+	if (val != GC8034_CHIP_ID)
+		return dev_err_probe(gc8034->dev, -ENXIO,
+				     "chip id mismatch: %x != %llx\n",
+				     GC8034_CHIP_ID, val);
+
+	return 0;
+}
+
+static int gc8034_enable_streams(struct v4l2_subdev *sd,
+				 struct v4l2_subdev_state *state,
+				 u32 pad, u64 streams_mask)
+{
+	struct gc8034 *gc8034 = to_gc8034(sd);
+	int ret;
+
+	ret = pm_runtime_resume_and_get(gc8034->dev);
+	if (ret < 0)
+		return ret;
+
+	ret = cci_multi_reg_write(gc8034->regmap, gc8034_regs,
+				  ARRAY_SIZE(gc8034_regs), NULL);
+	if (ret)
+		goto err_rpm_put;
+
+	ret = __v4l2_ctrl_handler_setup(&gc8034->ctrls);
+	if (ret)
+		goto err_rpm_put;
+
+	ret = cci_write(gc8034->regmap, GC8034_REG_STREAM, GC8034_STREAM_ON,
+			NULL);
+	if (ret)
+		goto err_rpm_put;
+
+	return 0;
+
+err_rpm_put:
+	pm_runtime_put_autosuspend(gc8034->dev);
+
+	return ret;
+}
+
+static int gc8034_disable_streams(struct v4l2_subdev *sd,
+				  struct v4l2_subdev_state *state,
+				  u32 pad, u64 streams_mask)
+{
+	struct gc8034 *gc8034 = to_gc8034(sd);
+	int ret = 0;
+
+	/* cci_write() logs errors, nothing else can be done about them. */
+	cci_write(gc8034->regmap, GC8034_REG_PAGE_SELECT, GC8034_PAGE_0, &ret);
+	cci_write(gc8034->regmap, GC8034_REG_STREAM, GC8034_STREAM_OFF, &ret);
+
+	pm_runtime_put_autosuspend(gc8034->dev);
+
+	return 0;
+}
+
+static const struct v4l2_subdev_video_ops gc8034_video_ops = {
+	.s_stream = v4l2_subdev_s_stream_helper,
+};
+
+static const struct v4l2_subdev_pad_ops gc8034_pad_ops = {
+	.enum_mbus_code = gc8034_enum_mbus_code,
+	.enum_frame_size = gc8034_enum_frame_size,
+	.get_fmt = v4l2_subdev_get_fmt,
+	.get_selection = gc8034_get_selection,
+	.get_frame_desc = gc8034_get_frame_desc,
+	.enable_streams = gc8034_enable_streams,
+	.disable_streams = gc8034_disable_streams,
+};
+
+static const struct v4l2_subdev_ops gc8034_subdev_ops = {
+	.video = &gc8034_video_ops,
+	.pad = &gc8034_pad_ops,
+};
+
+static const struct v4l2_subdev_internal_ops gc8034_internal_ops = {
+	.init_state = gc8034_init_state,
+};
+
+static int gc8034_parse_fwnode(struct gc8034 *gc8034)
+{
+	struct v4l2_fwnode_endpoint bus_cfg = {
+		.bus_type = V4L2_MBUS_CSI2_DPHY,
+	};
+	struct device *dev = gc8034->dev;
+	struct fwnode_handle *endpoint;
+	unsigned long link_freq_bitmap;
+	int ret;
+
+	endpoint = fwnode_graph_get_endpoint_by_id(dev_fwnode(dev), 0, 0,
+						   FWNODE_GRAPH_ENDPOINT_NEXT);
+	ret = v4l2_fwnode_endpoint_alloc_parse(endpoint, &bus_cfg);
+	fwnode_handle_put(endpoint);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to parse endpoint\n");
+
+	/* The register table configures the CSI-2 transmitter for 4 lanes. */
+	if (bus_cfg.bus.mipi_csi2.num_data_lanes != GC8034_DATA_LANES) {
+		ret = dev_err_probe(dev, -EINVAL,
+				    "unsupported number of data lanes %u\n",
+				    bus_cfg.bus.mipi_csi2.num_data_lanes);
+		goto done;
+	}
+
+	ret = v4l2_link_freq_to_bitmap(dev, bus_cfg.link_frequencies,
+				       bus_cfg.nr_of_link_frequencies,
+				       gc8034_link_freq_menu,
+				       ARRAY_SIZE(gc8034_link_freq_menu),
+				       &link_freq_bitmap);
+
+done:
+	v4l2_fwnode_endpoint_free(&bus_cfg);
+
+	return ret;
+}
+
+static int gc8034_init_controls(struct gc8034 *gc8034)
+{
+	struct v4l2_fwnode_device_properties props;
+	struct v4l2_ctrl_handler *ctrl_hdlr;
+	struct v4l2_ctrl *link_freq, *hblank;
+	s64 exposure_max;
+	int ret;
+
+	ret = v4l2_fwnode_device_parse(gc8034->dev, &props);
+	if (ret)
+		return ret;
+
+	ctrl_hdlr = &gc8034->ctrls;
+	v4l2_ctrl_handler_init(ctrl_hdlr, 8);
+
+	link_freq = v4l2_ctrl_new_int_menu(ctrl_hdlr, NULL, V4L2_CID_LINK_FREQ,
+					   ARRAY_SIZE(gc8034_link_freq_menu) -
+					   1, 0, gc8034_link_freq_menu);
+
+	v4l2_ctrl_new_std(ctrl_hdlr, NULL, V4L2_CID_PIXEL_RATE,
+			  GC8034_PIXEL_RATE, GC8034_PIXEL_RATE, 1,
+			  GC8034_PIXEL_RATE);
+
+	v4l2_ctrl_new_std(ctrl_hdlr, &gc8034_ctrl_ops, V4L2_CID_VBLANK,
+			  GC8034_VTS_DEF - GC8034_HEIGHT,
+			  GC8034_VTS_MAX - GC8034_HEIGHT, 1,
+			  GC8034_VTS_DEF - GC8034_HEIGHT);
+
+	hblank = v4l2_ctrl_new_std(ctrl_hdlr, NULL, V4L2_CID_HBLANK,
+				   GC8034_HTS - GC8034_WIDTH,
+				   GC8034_HTS - GC8034_WIDTH, 1,
+				   GC8034_HTS - GC8034_WIDTH);
+
+	exposure_max = GC8034_VTS_DEF - GC8034_EXP_MARGIN;
+	gc8034->exposure = v4l2_ctrl_new_std(ctrl_hdlr, &gc8034_ctrl_ops,
+					     V4L2_CID_EXPOSURE, GC8034_EXP_MIN,
+					     exposure_max, GC8034_EXP_STEP,
+					     GC8034_EXP_DEF);
+
+	v4l2_ctrl_new_std(ctrl_hdlr, &gc8034_ctrl_ops, V4L2_CID_ANALOGUE_GAIN,
+			  0, ARRAY_SIZE(gc8034_agc_bias) - 1, 1, 0);
+
+	v4l2_ctrl_new_fwnode_properties(ctrl_hdlr, &gc8034_ctrl_ops, &props);
+
+	if (ctrl_hdlr->error)
+		return v4l2_ctrl_handler_free(ctrl_hdlr);
+
+	link_freq->flags |= V4L2_CTRL_FLAG_READ_ONLY;
+	hblank->flags |= V4L2_CTRL_FLAG_READ_ONLY;
+
+	gc8034->sd.ctrl_handler = ctrl_hdlr;
+
+	return 0;
+}
+
+static int gc8034_probe(struct i2c_client *client)
+{
+	struct device *dev = &client->dev;
+	struct gc8034 *gc8034;
+	unsigned long freq;
+	int ret;
+
+	gc8034 = devm_kzalloc(dev, sizeof(*gc8034), GFP_KERNEL);
+	if (!gc8034)
+		return -ENOMEM;
+
+	gc8034->dev = dev;
+
+	v4l2_i2c_subdev_init(&gc8034->sd, client, &gc8034_subdev_ops);
+	gc8034->sd.internal_ops = &gc8034_internal_ops;
+
+	ret = gc8034_parse_fwnode(gc8034);
+	if (ret)
+		return ret;
+
+	gc8034->xclk = devm_v4l2_sensor_clk_get(dev, NULL);
+	if (IS_ERR(gc8034->xclk))
+		return dev_err_probe(dev, PTR_ERR(gc8034->xclk),
+				     "failed to get clock\n");
+
+	freq = clk_get_rate(gc8034->xclk);
+	if (freq != GC8034_XCLK_FREQ)
+		return dev_err_probe(dev, -EINVAL,
+				     "external clock %lu Hz not supported\n",
+				     freq);
+
+	gc8034->regmap = devm_cci_regmap_init_i2c(client, 8);
+	if (IS_ERR(gc8034->regmap))
+		return dev_err_probe(dev, PTR_ERR(gc8034->regmap),
+				     "failed to init CCI\n");
+
+	gc8034->reset_gpio = devm_gpiod_get_optional(dev, "reset",
+						     GPIOD_OUT_HIGH);
+	if (IS_ERR(gc8034->reset_gpio))
+		return dev_err_probe(dev, PTR_ERR(gc8034->reset_gpio),
+				     "failed to get reset GPIO\n");
+
+	gc8034->powerdown_gpio = devm_gpiod_get_optional(dev, "powerdown",
+							 GPIOD_OUT_HIGH);
+	if (IS_ERR(gc8034->powerdown_gpio))
+		return dev_err_probe(dev, PTR_ERR(gc8034->powerdown_gpio),
+				     "failed to get powerdown GPIO\n");
+
+	for (unsigned int i = 0; i < ARRAY_SIZE(gc8034_supply_name); i++)
+		gc8034->supplies[i].supply = gc8034_supply_name[i];
+
+	ret = devm_regulator_bulk_get(dev, ARRAY_SIZE(gc8034_supply_name),
+				      gc8034->supplies);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to get regulators\n");
+
+	ret = gc8034_power_on(dev);
+	if (ret)
+		return ret;
+
+	ret = gc8034_identify_module(gc8034);
+	if (ret)
+		goto err_power_off;
+
+	ret = gc8034_init_controls(gc8034);
+	if (ret) {
+		dev_err_probe(dev, ret, "failed to init controls\n");
+		goto err_power_off;
+	}
+
+	gc8034->sd.flags |= V4L2_SUBDEV_FL_HAS_DEVNODE;
+	gc8034->pad.flags = MEDIA_PAD_FL_SOURCE;
+	gc8034->sd.entity.function = MEDIA_ENT_F_CAM_SENSOR;
+
+	ret = media_entity_pads_init(&gc8034->sd.entity, 1, &gc8034->pad);
+	if (ret < 0) {
+		dev_err_probe(dev, ret, "failed to init media entity\n");
+		goto err_ctrl_handler_free;
+	}
+
+	gc8034->sd.state_lock = gc8034->ctrls.lock;
+	ret = v4l2_subdev_init_finalize(&gc8034->sd);
+	if (ret < 0) {
+		dev_err_probe(dev, ret, "failed to finalize subdev\n");
+		goto err_media_entity_cleanup;
+	}
+
+	pm_runtime_set_active(dev);
+	pm_runtime_enable(dev);
+	pm_runtime_set_autosuspend_delay(dev, 1000);
+	pm_runtime_use_autosuspend(dev);
+
+	ret = v4l2_async_register_subdev_sensor(&gc8034->sd);
+	if (ret < 0) {
+		dev_err_probe(dev, ret, "failed to register subdev\n");
+		goto err_rpm;
+	}
+
+	pm_runtime_idle(dev);
+
+	return 0;
+
+err_rpm:
+	pm_runtime_disable(dev);
+	pm_runtime_dont_use_autosuspend(dev);
+	pm_runtime_set_suspended(dev);
+	v4l2_subdev_cleanup(&gc8034->sd);
+
+err_media_entity_cleanup:
+	media_entity_cleanup(&gc8034->sd.entity);
+
+err_ctrl_handler_free:
+	v4l2_ctrl_handler_free(&gc8034->ctrls);
+
+err_power_off:
+	gc8034_power_off(dev);
+
+	return ret;
+}
+
+static void gc8034_remove(struct i2c_client *client)
+{
+	struct v4l2_subdev *sd = i2c_get_clientdata(client);
+	struct gc8034 *gc8034 = to_gc8034(sd);
+
+	v4l2_async_unregister_subdev(sd);
+	v4l2_subdev_cleanup(sd);
+	media_entity_cleanup(&sd->entity);
+	v4l2_ctrl_handler_free(&gc8034->ctrls);
+
+	pm_runtime_disable(&client->dev);
+	if (!pm_runtime_status_suspended(&client->dev)) {
+		gc8034_power_off(&client->dev);
+		pm_runtime_set_suspended(&client->dev);
+	}
+	pm_runtime_dont_use_autosuspend(&client->dev);
+}
+
+static const struct dev_pm_ops gc8034_pm_ops = {
+	RUNTIME_PM_OPS(gc8034_power_off, gc8034_power_on, NULL)
+};
+
+static const struct acpi_device_id gc8034_acpi_ids[] = {
+	{ "GCTI8034" },
+	{ }
+};
+MODULE_DEVICE_TABLE(acpi, gc8034_acpi_ids);
+
+static struct i2c_driver gc8034_i2c_driver = {
+	.driver = {
+		.name = "gc8034",
+		.acpi_match_table = gc8034_acpi_ids,
+		.pm = pm_ptr(&gc8034_pm_ops),
+	},
+	.probe = gc8034_probe,
+	.remove = gc8034_remove,
+};
+module_i2c_driver(gc8034_i2c_driver);
+
+MODULE_AUTHOR("Nicola Fiorillo <nicfio@gmail.com>");
+MODULE_DESCRIPTION("GalaxyCore GC8034 sensor driver");
+MODULE_LICENSE("GPL");
-- 
2.47.3


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

* [PATCH 3/3] media: ipu-bridge: Add GalaxyCore GC5035 and GC8034
  2026-10-09  7:27 [PATCH 0/3] media: Add GalaxyCore GC5035 and GC8034 sensor drivers Nicola Fiorillo
  2026-10-09  7:27 ` [PATCH 1/3] media: i2c: Add GC5035 image sensor driver Nicola Fiorillo
  2026-10-09  7:27 ` [PATCH 2/3] media: i2c: Add GC8034 " Nicola Fiorillo
@ 2026-10-09  7:27 ` Nicola Fiorillo
  2026-10-09  9:24   ` Dan Scally
  2026-10-12 14:34 ` [PATCH v2 0/7] media: Add GalaxyCore GC5035 and GC8034 sensor drivers Nicola Fiorillo
  3 siblings, 1 reply; 15+ messages in thread
From: Nicola Fiorillo @ 2026-10-09  7:27 UTC (permalink / raw)
  To: linux-media
  Cc: Sakari Ailus, Laurent Pinchart, Mauro Carvalho Chehab,
	Hans Verkuil, Dan Scally, Hans de Goede, linux-kernel,
	Nicola Fiorillo

Add the GC5035 and GC8034 ACPI HIDs, with the link frequencies the two
sensors use with a 19.2 MHz external clock. Both are found on the CHUWI
Hi10 X1.

Assisted-by: claude-opus-5-5
Signed-off-by: Nicola Fiorillo <nicfio@gmail.com>
---
 drivers/media/pci/intel/ipu-bridge.c | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/drivers/media/pci/intel/ipu-bridge.c b/drivers/media/pci/intel/ipu-bridge.c
index 3739c4a2a..851096d7d 100644
--- a/drivers/media/pci/intel/ipu-bridge.c
+++ b/drivers/media/pci/intel/ipu-bridge.c
@@ -59,6 +59,10 @@ static const u16 ipu6_ov5693_ncont_clk[] = {
 };
 
 static const struct ipu_sensor_config ipu_supported_sensors[] = {
+	/* GalaxyCore GC5035 */
+	IPU_SENSOR_CONFIG("GCTI5035", 1, 422400000),
+	/* GalaxyCore GC8034 */
+	IPU_SENSOR_CONFIG("GCTI8034", 1, 268800000),
 	/* Himax HM1092 */
 	IPU_SENSOR_CONFIG("HIMX1092", 2, 180000000, 180480000),
 	/* Himax HM11B1 */
-- 
2.47.3


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

* Re: [PATCH 3/3] media: ipu-bridge: Add GalaxyCore GC5035 and GC8034
  2026-10-09  7:27 ` [PATCH 3/3] media: ipu-bridge: Add GalaxyCore GC5035 and GC8034 Nicola Fiorillo
@ 2026-10-09  9:24   ` Dan Scally
  0 siblings, 0 replies; 15+ messages in thread
From: Dan Scally @ 2026-10-09  9:24 UTC (permalink / raw)
  To: Nicola Fiorillo, linux-media
  Cc: Sakari Ailus, Laurent Pinchart, Mauro Carvalho Chehab,
	Hans Verkuil, Hans de Goede, linux-kernel

Morning Nicola

On 09/10/2026 08:27, Nicola Fiorillo wrote:
> Add the GC5035 and GC8034 ACPI HIDs, with the link frequencies the two
> sensors use with a 19.2 MHz external clock. Both are found on the CHUWI
> Hi10 X1.
> 
> Assisted-by: claude-opus-5-5
> Signed-off-by: Nicola Fiorillo <nicfio@gmail.com>
> ---
>   drivers/media/pci/intel/ipu-bridge.c | 4 ++++
>   1 file changed, 4 insertions(+)
> 
> diff --git a/drivers/media/pci/intel/ipu-bridge.c b/drivers/media/pci/intel/ipu-bridge.c
> index 3739c4a2a..851096d7d 100644
> --- a/drivers/media/pci/intel/ipu-bridge.c
> +++ b/drivers/media/pci/intel/ipu-bridge.c
> @@ -59,6 +59,10 @@ static const u16 ipu6_ov5693_ncont_clk[] = {
>   };
>   
>   static const struct ipu_sensor_config ipu_supported_sensors[] = {
> +	/* GalaxyCore GC5035 */
> +	IPU_SENSOR_CONFIG("GCTI5035", 1, 422400000),
> +	/* GalaxyCore GC8034 */
> +	IPU_SENSOR_CONFIG("GCTI8034", 1, 268800000),

These entries look ok:

Reviewed-by: Daniel Scally <dan.scally@ideasonboard.com>

>   	/* Himax HM1092 */
>   	IPU_SENSOR_CONFIG("HIMX1092", 2, 180000000, 180480000),
>   	/* Himax HM11B1 */


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

* Re: [PATCH 1/3] media: i2c: Add GC5035 image sensor driver
  2026-10-09  7:27 ` [PATCH 1/3] media: i2c: Add GC5035 image sensor driver Nicola Fiorillo
@ 2026-10-09  9:48   ` Laurent Pinchart
  2026-10-12 11:20   ` Uwe Kleine-König
  1 sibling, 0 replies; 15+ messages in thread
From: Laurent Pinchart @ 2026-10-09  9:48 UTC (permalink / raw)
  To: Nicola Fiorillo
  Cc: linux-media, Sakari Ailus, Mauro Carvalho Chehab, Hans Verkuil,
	Dan Scally, Hans de Goede, linux-kernel

On Fri, Oct 09, 2026 at 09:27:31AM +0200, Nicola Fiorillo wrote:
> Add a driver for the GalaxyCore GC5035, a 5 Mpixel raw Bayer sensor with
> a two lane MIPI CSI-2 interface. The driver supports a single 2592x1944
> mode, and the exposure, analogue gain, vertical blanking and test pattern
> controls.

Please make the driver freely configurable, with support for analog crop
(and binning if supported by the device).

> The register tables come from the Alder Lake-M driver in
> intel/ipu6-drivers, which derives from the ChromeOS driver posted in 2020.
> 
> Assisted-by: claude-opus-5-5 coccinelle sparse smatch
> Signed-off-by: Nicola Fiorillo <nicfio@gmail.com>
> ---
>  MAINTAINERS                |   6 +
>  drivers/media/i2c/Kconfig  |  10 +
>  drivers/media/i2c/Makefile |   1 +
>  drivers/media/i2c/gc5035.c | 953 +++++++++++++++++++++++++++++++++++++
>  4 files changed, 970 insertions(+)
>  create mode 100644 drivers/media/i2c/gc5035.c

-- 
Regards,

Laurent Pinchart

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

* Re: [PATCH 1/3] media: i2c: Add GC5035 image sensor driver
  2026-10-09  7:27 ` [PATCH 1/3] media: i2c: Add GC5035 image sensor driver Nicola Fiorillo
  2026-10-09  9:48   ` Laurent Pinchart
@ 2026-10-12 11:20   ` Uwe Kleine-König
  1 sibling, 0 replies; 15+ messages in thread
From: Uwe Kleine-König @ 2026-10-12 11:20 UTC (permalink / raw)
  To: Nicola Fiorillo
  Cc: linux-media, Sakari Ailus, Laurent Pinchart,
	Mauro Carvalho Chehab, Hans Verkuil, Dan Scally, Hans de Goede,
	linux-kernel

[-- Attachment #1: Type: text/plain, Size: 423 bytes --]

On Fri, Oct 09, 2026 at 09:27:31AM +0200, Nicola Fiorillo wrote:
> +#include <linux/mod_devicetable.h>

Please don't use linux/mod_devicetable.h, the needed *_device_id
structures are already provided by the respective subsystem headers.

> +static const struct acpi_device_id gc5035_acpi_ids[] = {
> +	{ "GCTI5035" },

	{ .id = "GCTI5035" },

> +	{ }
> +};
> +MODULE_DEVICE_TABLE(acpi, gc5035_acpi_ids);

Best regards
Uwe

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 488 bytes --]

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

* [PATCH v2 0/7] media: Add GalaxyCore GC5035 and GC8034 sensor drivers
  2026-10-09  7:27 [PATCH 0/3] media: Add GalaxyCore GC5035 and GC8034 sensor drivers Nicola Fiorillo
                   ` (2 preceding siblings ...)
  2026-10-09  7:27 ` [PATCH 3/3] media: ipu-bridge: Add GalaxyCore GC5035 and GC8034 Nicola Fiorillo
@ 2026-10-12 14:34 ` Nicola Fiorillo
  2026-10-12 14:34   ` [PATCH v2 1/7] media: i2c: Add GC5035 image sensor driver Nicola Fiorillo
                     ` (6 more replies)
  3 siblings, 7 replies; 15+ messages in thread
From: Nicola Fiorillo @ 2026-10-12 14:34 UTC (permalink / raw)
  To: Laurent Pinchart, Sakari Ailus
  Cc: Mauro Carvalho Chehab, Hans Verkuil, Dan Scally, Hans de Goede,
	Uwe Kleine-König, linux-media, linux-kernel

Hi all,

this is v2 of the drivers for the GalaxyCore GC5035 (front camera, 2
lanes) and GC8034 (back camera, 4 lanes) sensors of the Chuwi Hi10 X1
tablet, with IPU6.

Laurent, on v1 you asked to make the drivers configurable, with analog crop
and binning if the sensor supports it.

This version does the following:

- No more mode tables for the geometry: the readout window and the sensor
  output crop are computed from the crop rectangle every time streaming
  starts (patches 1 and 2).
- The crop is set with the CROP selection on the source pad (patches 4 and
  5). Vertically it is an analog crop: the readout window gets smaller and
  the frame gets shorter (the GC5035 at 640x480 runs at up to 106 fps).
  Horizontally it is the sensor internal output crop instead: narrowing
  the readout window horizontally gives black images on the GC5035, and on
  the GC8034 it goes through a column register that gives corrupted images
  at many values.
- 2x2 binning is there on the GC5035 (patches 6 and 7): the 1296x972
  format gives the average of four samples of the same colour, with the
  same field of view as the default crop. The vendor sequence changes the
  PLL, so I measured the link frequency in that mode: 176.3 +/- 0.9 MHz,
  against the 175.2 MHz given by the PLL registers (with the same method,
  at full resolution, 420.7 +/- 1.5 against 422.4). Patch 6 adds this
  frequency to ipu-bridge. On the GC8034 binning only exists in the 2-lane
  vendor sequences; with 4 lanes it breaks the CSI-2 stream, so it isn't
  there.

I kept the crop in patches 4 and 5 and the binning in 6 and 7, separate,
because I saw that for the IMX908 you decided to postpone the crop until
the common raw sensor model
(<20261001133138.GM944070@killaraus.ideasonboard.com>). If you want to do
the same here, patches 1-3 work on their own, at a fixed resolution like
v1, and I'll redo crop and binning on the common model when it lands.

Some choices you may disagree with, and that I'm happy to change:

- set_fmt changes the crop: for userspace that does not set the crop
  first, it centres it on the default crop, as ov01a10 does. And 1296x972
  selects binning and sets the default crop back, unless the crop is
  already 1296x972: libcamera first sets the largest size and then the one
  it wants, and with binning tied to the current crop it never got it. If
  you prefer binning through COMPOSE, or waiting for the common model,
  I'll change it.
- In binning the timings are in sensor lines (the sensor reads one row of
  the array per line and outputs one every two lines): exposure and VBLANK
  keep the register unit, with a one line step, and the frame period
  computed from HBLANK and VBLANK is the measured one. This is why
  PIXEL_RATE is 70.08 MHz, HBLANK 164 and VBLANK at least 1024.
- enum_frame_size lists 1296x972 separately, although it is within the
  full mode range, so that libcamera sees the binned mode.
- The GC8034 PIXEL_RATE (256 MHz) is the one that gives the measured frame
  periods with the sensor line length; it is more than the 4-lane link
  carries (215 Mpixel/s), but over a whole line, blanking included, the
  average is about 196 Mpixel/s.
- The analogue gain is the index of the sensor gain steps, which are not
  linear, as in the vendor code.

I don't have the datasheets: I measured the geometry, the limits and the
timings on the tablet, and the comments in the code say what is measured.
In short:

- The sizes reported as NATIVE_SIZE and CROP_BOUNDS (2608x1964 and
  3282x2500) are the largest area that gives image data; around it there
  are constant rows and columns. They are not the physical array, which is
  not documented.
- CROP_DEFAULT is the image of the vendor sequence, the same as v1: at the
  default crop v2 produces the same image as v1.
- The timings follow a model that matches the measurements from 64 lines
  to the full height; for the GC8034 this corrected PIXEL_RATE to 256 MHz
  (v1 declared 255.9 MHz).
- The size limits (GC8034 at least 512 and at most 3280 wide, at most 2448
  high; GC5035 at least 64x64) come from tests where outside those limits
  the stream breaks or the timings leave the model.

The series is based on media next (d7e7bca71). I tested it on the
tablet, with a 7.3-rc1 kernel and the modules built from these patches:
- v4l2-compliance from git (1616bf9e3c81) with -z: 54/54, no warnings, on
  both, and on the GC5035 both at full resolution and in binning; full
  output below.
- A set of crop tests (default, bounds, centre, corners, minimum, requests
  to be rounded or outside the area): rectangle applied, measured frame
  period, complete frames and position to the pixel, all as expected, in
  two runs per sensor.
- Binning: controls, frame period (also with a long VBLANK and at the
  maximum exposure), signal doubling with double exposure, output equal to
  the per-colour 2x2 average of a full resolution frame with no offset, 20
  starts and stops in a row, and switching from the full mode to binning.
- libcamera (simple pipeline): 60 frames out of 60 in three
  configurations per camera; on the GC5035 at 1280x960 it picks binning.
- W=1, sparse, smatch, checkpatch --strict and coccinelle clean on every
  patch; i386 build clean.

Changes since v1:
- Patches 1 and 2: geometry removed from the register table and computed
  from the crop, all written as 16-bit values; NATIVE_SIZE and CROP_BOUNDS
  are the measured area instead of the vendor readout window; the default
  exposure no longer exceeds the maximum when VBLANK is reduced; GC8034:
  PIXEL_RATE set to 256 MHz and the blanking register computed from
  VBLANK; dropped linux/mod_devicetable.h and used a designated
  initializer in the ACPI table, as Uwe asked.
- Added Dan's Reviewed-by to patch 3.
- Patches 4, 5, 6 and 7: new.

I used a frontier LLM (Claude) to write the code and to do the
measurements; I stated it in the Assisted-by tags. The English of this
letter is a translation from my Italian.

Nicola

v4l2-compliance (git 1616bf9e3c81, -z PCI:0000:00:05.0 -u <subdev>):

GC5035 (2592x1944):

v4l2-compliance 1.33.0-5515, 64 bits, 64-bit time_t
v4l2-compliance SHA: 1616bf9e3c81 2026-10-06 07:30:20

Compliance test for intel-ipu6 device /dev/v4l-subdev4:

Driver Info:
	Driver version   : 7.3.0
	Capabilities     : 0x00000000
	Client Capabilities: 0x0000000000000002
interval-uses-which Media Driver Info:
	Driver name      : intel-ipu6
	Model            : ipu6
	Serial           : 
	Bus info         : PCI:0000:00:05.0
	Media version    : 7.3.0
	Hardware revision: 0x00000000 (0)
	Driver version   : 7.3.0
Interface Info:
	ID               : 0x030001fd
	Type             : V4L Sub-Device
Entity Info:
	ID               : 0x000001f9 (505)
	Name             : gc5035 2-003f
	Function         : Camera Sensor
	Pad 0x010001fa   : 0: Source
	  Link 0x020001fb: to remote pad 0x1000096 of entity 'Intel IPU6 CSI2 2' (Video Interface Bridge): Data, Enabled, Immutable

Required ioctls:
	test MC information (see 'Media Driver Info' above): OK
	test VIDIOC_SUDBEV_QUERYCAP: OK
	test invalid ioctls: OK

Allow for multiple opens:
	test second /dev/v4l-subdev4 open: OK
	test VIDIOC_SUBDEV_QUERYCAP: OK
	test for unlimited opens: OK

Debug ioctls:
	test VIDIOC_LOG_STATUS: OK (Not Supported)

Input ioctls:
	test VIDIOC_G/S_TUNER/ENUM_FREQ_BANDS: OK (Not Supported)
	test VIDIOC_G/S_FREQUENCY: OK (Not Supported)
	test VIDIOC_S_HW_FREQ_SEEK: OK (Not Supported)
	test VIDIOC_ENUMAUDIO: OK (Not Supported)
	test VIDIOC_G/S/ENUMINPUT: OK (Not Supported)
	test VIDIOC_G/S_AUDIO: OK (Not Supported)
	Inputs: 0 Audio Inputs: 0 Tuners: 0

Output ioctls:
	test VIDIOC_G/S_MODULATOR: OK (Not Supported)
	test VIDIOC_G/S_FREQUENCY: OK (Not Supported)
	test VIDIOC_ENUMAUDOUT: OK (Not Supported)
	test VIDIOC_G/S/ENUMOUTPUT: OK (Not Supported)
	test VIDIOC_G/S_AUDOUT: OK (Not Supported)
	Outputs: 0 Audio Outputs: 0 Modulators: 0

Input/Output configuration ioctls:
	test VIDIOC_ENUM/G/S/QUERY_STD: OK (Not Supported)
	test VIDIOC_ENUM/G/S/QUERY_DV_TIMINGS: OK (Not Supported)
	test VIDIOC_DV_TIMINGS_CAP: OK (Not Supported)
	test VIDIOC_G/S_EDID: OK (Not Supported)

Sub-Device ioctls (Source Pad 0):
	Try Stream 0
	test Try VIDIOC_SUBDEV_ENUM_MBUS_CODE/FRAME_SIZE/FRAME_INTERVAL: OK
	test Try VIDIOC_SUBDEV_G/S_FMT: OK
	test Try VIDIOC_SUBDEV_G/S_SELECTION/CROP: OK
	Active Stream 0
	test Active VIDIOC_SUBDEV_ENUM_MBUS_CODE/FRAME_SIZE/FRAME_INTERVAL: OK
	test Active VIDIOC_SUBDEV_G/S_FMT: OK
	test Active VIDIOC_SUBDEV_G/S_SELECTION/CROP: OK
	test Active VIDIOC_SUBDEV_G/S_FRAME_INTERVAL: OK (Not Supported)

Control ioctls:
	test VIDIOC_QUERY_EXT_CTRL/QUERYMENU: OK
	test VIDIOC_QUERYCTRL: OK
	test VIDIOC_G/S_CTRL: OK
	test VIDIOC_G/S/TRY_EXT_CTRLS: OK
	test VIDIOC_(UN)SUBSCRIBE_EVENT/DQEVENT: OK
	test VIDIOC_G/S_JPEGCOMP: OK (Not Supported)
	Standard Controls: 13 Private Controls: 0

Format ioctls:
	test VIDIOC_ENUM_FMT/FRAMESIZES/FRAMEINTERVALS: OK (Not Supported)
	test VIDIOC_G/S_PARM: OK (Not Supported)
	test VIDIOC_G_FBUF: OK (Not Supported)
	test VIDIOC_G_FMT: OK (Not Supported)
	test VIDIOC_TRY_FMT: OK (Not Supported)
	test VIDIOC_S_FMT: OK (Not Supported)
	test VIDIOC_G_SLICED_VBI_CAP: OK (Not Supported)
	test Cropping: OK (Not Supported)
	test Composing: OK (Not Supported)
	test Scaling: OK (Not Supported)

Codec ioctls:
	test VIDIOC_(TRY_)ENCODER_CMD: OK (Not Supported)
	test VIDIOC_G_ENC_INDEX: OK (Not Supported)
	test VIDIOC_(TRY_)DECODER_CMD: OK (Not Supported)

Buffer ioctls:
	test VIDIOC_REQBUFS/CREATE_BUFS/QUERYBUF: OK (Not Supported)
	test CREATE_BUFS maximum buffers: OK
	test VIDIOC_REMOVE_BUFS: OK
	test VIDIOC_EXPBUF: OK (Not Supported)
	test Requests: OK (Not Supported)
	test blocking wait: OK (Not Supported)

Total for intel-ipu6 device /dev/v4l-subdev4: 54, Succeeded: 54, Failed: 0, Warnings: 0

GC5035 in binning (1296x972), same output except:

Total for intel-ipu6 device /dev/v4l-subdev4: 54, Succeeded: 54, Failed: 0, Warnings: 0

GC8034:

v4l2-compliance 1.33.0-5515, 64 bits, 64-bit time_t
v4l2-compliance SHA: 1616bf9e3c81 2026-10-06 07:30:20

Compliance test for intel-ipu6 device /dev/v4l-subdev5:

Driver Info:
	Driver version   : 7.3.0
	Capabilities     : 0x00000000
	Client Capabilities: 0x0000000000000002
interval-uses-which Media Driver Info:
	Driver name      : intel-ipu6
	Model            : ipu6
	Serial           : 
	Bus info         : PCI:0000:00:05.0
	Media version    : 7.3.0
	Hardware revision: 0x00000000 (0)
	Driver version   : 7.3.0
Interface Info:
	ID               : 0x03000203
	Type             : V4L Sub-Device
Entity Info:
	ID               : 0x000001ff (511)
	Name             : gc8034 1-0037
	Function         : Camera Sensor
	Pad 0x01000200   : 0: Source
	  Link 0x02000201: to remote pad 0x100008c of entity 'Intel IPU6 CSI2 1' (Video Interface Bridge): Data, Enabled, Immutable

Required ioctls:
	test MC information (see 'Media Driver Info' above): OK
	test VIDIOC_SUDBEV_QUERYCAP: OK
	test invalid ioctls: OK

Allow for multiple opens:
	test second /dev/v4l-subdev5 open: OK
	test VIDIOC_SUBDEV_QUERYCAP: OK
	test for unlimited opens: OK

Debug ioctls:
	test VIDIOC_LOG_STATUS: OK (Not Supported)

Input ioctls:
	test VIDIOC_G/S_TUNER/ENUM_FREQ_BANDS: OK (Not Supported)
	test VIDIOC_G/S_FREQUENCY: OK (Not Supported)
	test VIDIOC_S_HW_FREQ_SEEK: OK (Not Supported)
	test VIDIOC_ENUMAUDIO: OK (Not Supported)
	test VIDIOC_G/S/ENUMINPUT: OK (Not Supported)
	test VIDIOC_G/S_AUDIO: OK (Not Supported)
	Inputs: 0 Audio Inputs: 0 Tuners: 0

Output ioctls:
	test VIDIOC_G/S_MODULATOR: OK (Not Supported)
	test VIDIOC_G/S_FREQUENCY: OK (Not Supported)
	test VIDIOC_ENUMAUDOUT: OK (Not Supported)
	test VIDIOC_G/S/ENUMOUTPUT: OK (Not Supported)
	test VIDIOC_G/S_AUDOUT: OK (Not Supported)
	Outputs: 0 Audio Outputs: 0 Modulators: 0

Input/Output configuration ioctls:
	test VIDIOC_ENUM/G/S/QUERY_STD: OK (Not Supported)
	test VIDIOC_ENUM/G/S/QUERY_DV_TIMINGS: OK (Not Supported)
	test VIDIOC_DV_TIMINGS_CAP: OK (Not Supported)
	test VIDIOC_G/S_EDID: OK (Not Supported)

Sub-Device ioctls (Source Pad 0):
	Try Stream 0
	test Try VIDIOC_SUBDEV_ENUM_MBUS_CODE/FRAME_SIZE/FRAME_INTERVAL: OK
	test Try VIDIOC_SUBDEV_G/S_FMT: OK
	test Try VIDIOC_SUBDEV_G/S_SELECTION/CROP: OK
	Active Stream 0
	test Active VIDIOC_SUBDEV_ENUM_MBUS_CODE/FRAME_SIZE/FRAME_INTERVAL: OK
	test Active VIDIOC_SUBDEV_G/S_FMT: OK
	test Active VIDIOC_SUBDEV_G/S_SELECTION/CROP: OK
	test Active VIDIOC_SUBDEV_G/S_FRAME_INTERVAL: OK (Not Supported)

Control ioctls:
	test VIDIOC_QUERY_EXT_CTRL/QUERYMENU: OK
	test VIDIOC_QUERYCTRL: OK
	test VIDIOC_G/S_CTRL: OK
	test VIDIOC_G/S/TRY_EXT_CTRLS: OK
	test VIDIOC_(UN)SUBSCRIBE_EVENT/DQEVENT: OK
	test VIDIOC_G/S_JPEGCOMP: OK (Not Supported)
	Standard Controls: 12 Private Controls: 0

Format ioctls:
	test VIDIOC_ENUM_FMT/FRAMESIZES/FRAMEINTERVALS: OK (Not Supported)
	test VIDIOC_G/S_PARM: OK (Not Supported)
	test VIDIOC_G_FBUF: OK (Not Supported)
	test VIDIOC_G_FMT: OK (Not Supported)
	test VIDIOC_TRY_FMT: OK (Not Supported)
	test VIDIOC_S_FMT: OK (Not Supported)
	test VIDIOC_G_SLICED_VBI_CAP: OK (Not Supported)
	test Cropping: OK (Not Supported)
	test Composing: OK (Not Supported)
	test Scaling: OK (Not Supported)

Codec ioctls:
	test VIDIOC_(TRY_)ENCODER_CMD: OK (Not Supported)
	test VIDIOC_G_ENC_INDEX: OK (Not Supported)
	test VIDIOC_(TRY_)DECODER_CMD: OK (Not Supported)

Buffer ioctls:
	test VIDIOC_REQBUFS/CREATE_BUFS/QUERYBUF: OK (Not Supported)
	test CREATE_BUFS maximum buffers: OK
	test VIDIOC_REMOVE_BUFS: OK
	test VIDIOC_EXPBUF: OK (Not Supported)
	test Requests: OK (Not Supported)
	test blocking wait: OK (Not Supported)

Total for intel-ipu6 device /dev/v4l-subdev5: 54, Succeeded: 54, Failed: 0, Warnings: 0


Nicola Fiorillo (7):
  media: i2c: Add GC5035 image sensor driver
  media: i2c: Add GC8034 image sensor driver
  media: ipu-bridge: Add GalaxyCore GC5035 and GC8034
  media: i2c: gc5035: Add crop support
  media: i2c: gc8034: Add crop support
  media: ipu-bridge: Add the GC5035 binning link frequency
  media: i2c: gc5035: Add 2x2 binning support

 MAINTAINERS                          |   12 +
 drivers/media/i2c/Kconfig            |   20 +
 drivers/media/i2c/Makefile           |    2 +
 drivers/media/i2c/gc5035.c           | 1351 ++++++++++++++++++++++++++
 drivers/media/i2c/gc8034.c           | 1175 ++++++++++++++++++++++
 drivers/media/pci/intel/ipu-bridge.c |    4 +
 6 files changed, 2564 insertions(+)
 create mode 100644 drivers/media/i2c/gc5035.c
 create mode 100644 drivers/media/i2c/gc8034.c


base-commit: d7e7bca7169873b46e857c025c10f3336c1258d8
-- 
2.47.3


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

* [PATCH v2 1/7] media: i2c: Add GC5035 image sensor driver
  2026-10-12 14:34 ` [PATCH v2 0/7] media: Add GalaxyCore GC5035 and GC8034 sensor drivers Nicola Fiorillo
@ 2026-10-12 14:34   ` Nicola Fiorillo
  2026-10-12 14:34   ` [PATCH v2 2/7] media: i2c: Add GC8034 " Nicola Fiorillo
                     ` (5 subsequent siblings)
  6 siblings, 0 replies; 15+ messages in thread
From: Nicola Fiorillo @ 2026-10-12 14:34 UTC (permalink / raw)
  To: Laurent Pinchart, Sakari Ailus
  Cc: Mauro Carvalho Chehab, Hans Verkuil, Dan Scally, Hans de Goede,
	Uwe Kleine-König, linux-media, linux-kernel

Add a driver for the GalaxyCore GC5035, a 5 Mpixel raw Bayer sensor with
a two lane MIPI CSI-2 interface. The driver supports the exposure,
analogue gain, vertical blanking and test pattern controls, and outputs
2592x1944 RAW10.

I don't have a datasheet for the sensor. The initialisation sequence comes
from the Alder Lake-M driver in intel/ipu6-drivers, which derives from the
ChromeOS driver posted in 2020. The readout window and the output crop are
not part of the sequence: they are computed from the crop rectangle, in
the coordinates of the largest area measured to give image data
(2608x1964). The driver reports that area as the native size and the crop
bounds; it is not the physical pixel array. The default crop rectangle
gives the same image as the vendor sequence. The frame period,
(height + vblank) * 2920 / 168.96 MHz, matches the measurements.

Assisted-by: claude-opus-5-5 coccinelle sparse smatch
Signed-off-by: Nicola Fiorillo <nicfio@gmail.com>
---
 MAINTAINERS                |    6 +
 drivers/media/i2c/Kconfig  |   10 +
 drivers/media/i2c/Makefile |    1 +
 drivers/media/i2c/gc5035.c | 1002 ++++++++++++++++++++++++++++++++++++
 4 files changed, 1019 insertions(+)
 create mode 100644 drivers/media/i2c/gc5035.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 760791e36..ca264ceef 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -10875,6 +10875,12 @@ T:	git git://linuxtv.org/media.git
 F:	Documentation/devicetree/bindings/media/i2c/galaxycore,gc2145.yaml
 F:	drivers/media/i2c/gc2145.c
 
+GALAXYCORE GC5035 CAMERA SENSOR DRIVER
+M:	Nicola Fiorillo <nicfio@gmail.com>
+L:	linux-media@vger.kernel.org
+S:	Maintained
+F:	drivers/media/i2c/gc5035.c
+
 GATEWORKS SYSTEM CONTROLLER (GSC) DRIVER
 M:	Tim Harvey <tharvey@gateworks.com>
 S:	Maintained
diff --git a/drivers/media/i2c/Kconfig b/drivers/media/i2c/Kconfig
index 7de2d095c..fca5bcf98 100644
--- a/drivers/media/i2c/Kconfig
+++ b/drivers/media/i2c/Kconfig
@@ -110,6 +110,16 @@ config VIDEO_GC2145
 	  To compile this driver as a module, choose M here: the
 	  module will be called gc2145.
 
+config VIDEO_GC5035
+	tristate "GalaxyCore GC5035 sensor support"
+	select V4L2_CCI_I2C
+	help
+	  This is a Video4Linux2 sensor driver for the GalaxyCore GC5035
+	  5 Mpixel camera.
+
+	  To compile this driver as a module, choose M here: the
+	  module will be called gc5035.
+
 config VIDEO_HI556
 	tristate "Hynix Hi-556 sensor support"
 	help
diff --git a/drivers/media/i2c/Makefile b/drivers/media/i2c/Makefile
index 59e31af45..bb8552bbd 100644
--- a/drivers/media/i2c/Makefile
+++ b/drivers/media/i2c/Makefile
@@ -42,6 +42,7 @@ obj-$(CONFIG_VIDEO_GC0310) += gc0310.o
 obj-$(CONFIG_VIDEO_GC05A2) += gc05a2.o
 obj-$(CONFIG_VIDEO_GC08A3) += gc08a3.o
 obj-$(CONFIG_VIDEO_GC2145) += gc2145.o
+obj-$(CONFIG_VIDEO_GC5035) += gc5035.o
 obj-$(CONFIG_VIDEO_HI556) += hi556.o
 obj-$(CONFIG_VIDEO_HI846) += hi846.o
 obj-$(CONFIG_VIDEO_HI847) += hi847.o
diff --git a/drivers/media/i2c/gc5035.c b/drivers/media/i2c/gc5035.c
new file mode 100644
index 000000000..87c3742f1
--- /dev/null
+++ b/drivers/media/i2c/gc5035.c
@@ -0,0 +1,1002 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Driver for GalaxyCore GC5035 image sensor
+ *
+ * Copyright (c) 2020 Bitland Inc.
+ * Copyright 2020 Google LLC.
+ * Copyright (c) 2022 Intel Corporation.
+ * Copyright (C) 2026 Nicola Fiorillo <nicfio@gmail.com>
+ *
+ * The register table comes from the GC5035 driver in intel/ipu6-drivers by
+ * Liang Wang <liang1.wang@intel.com>, derived from the ChromeOS series by
+ * Tomasz Figa <tfiga@chromium.org>.
+ */
+#include <linux/array_size.h>
+#include <linux/clk.h>
+#include <linux/container_of.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/math.h>
+#include <linux/module.h>
+#include <linux/pm_runtime.h>
+#include <linux/property.h>
+#include <linux/regulator/consumer.h>
+#include <linux/time64.h>
+#include <linux/types.h>
+#include <linux/units.h>
+
+#include <media/mipi-csi2.h>
+#include <media/v4l2-cci.h>
+#include <media/v4l2-common.h>
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-fwnode.h>
+#include <media/v4l2-subdev.h>
+
+/*
+ * The GC5035 uses 8 bit register addresses with banked pages: register
+ * 0xfe selects the page. Every register below is on page 0 unless noted
+ * otherwise. The vendor sequence also writes 0x10 to register 0xfe, which is
+ * not a page number and is not documented, so the pages are not handled with
+ * regmap ranges.
+ */
+#define GC5035_REG_PAGE_SELECT		CCI_REG8(0xfe)
+#define GC5035_PAGE_0			0x00
+#define GC5035_PAGE_1			0x01
+
+/* readable on any page */
+#define GC5035_REG_CHIP_ID		CCI_REG16(0xf0)
+#define GC5035_CHIP_ID			0x5035
+
+#define GC5035_REG_EXPOSURE		CCI_REG16(0x03)
+#define GC5035_REG_LINE_LENGTH		CCI_REG16(0x05)
+#define GC5035_REG_WIN_TOP		CCI_REG16(0x09)
+#define GC5035_REG_WIN_LEFT		CCI_REG16(0x0b)
+#define GC5035_REG_WIN_HEIGHT		CCI_REG16(0x0d)
+#define GC5035_REG_WIN_WIDTH		CCI_REG16(0x0f)
+#define GC5035_REG_STREAM		CCI_REG8(0x3e)
+#define GC5035_STREAM_ON		0x91
+#define GC5035_STREAM_OFF		0x01
+#define GC5035_REG_FRAME_LENGTH		CCI_REG16(0x41)
+#define GC5035_REG_DIGITAL_GAIN	CCI_REG16(0xb1)
+#define GC5035_DIGITAL_GAIN_1X	0x100
+#define GC5035_REG_ANALOGUE_GAIN	CCI_REG8(0xb6)
+#define GC5035_REG_PLL_MULT		CCI_REG8(0xf8)
+
+/* page 1 */
+#define GC5035_REG_TEST_PATTERN		CCI_REG8(0x8c)
+#define GC5035_TEST_PATTERN_ON		0x11
+#define GC5035_TEST_PATTERN_OFF		0x10
+#define GC5035_REG_CROP_TOP		CCI_REG16(0x91)
+#define GC5035_REG_CROP_LEFT		CCI_REG16(0x93)
+#define GC5035_REG_OUT_HEIGHT		CCI_REG16(0x95)
+#define GC5035_REG_OUT_WIDTH		CCI_REG16(0x97)
+
+/*
+ * No register documentation is available: the geometry below was measured.
+ *
+ * Crop rectangles are expressed in the coordinates of the largest area of the
+ * pixel array that was measured to give image data, 2608x1964. The rows and
+ * columns around it are constant (black, or stuck at full scale), and the
+ * size of the full pixel array is not known, so this area is reported as the
+ * native size. Its first row is read out with the row start register at 4.
+ * Its first column is the fifth column of a readout window starting at column
+ * start 1, and the first column of the vendor window (column start 3).
+ *
+ * The row start register moves the image by one row per unit. The column
+ * start register moves it in steps of four columns, and its even values give
+ * corrupted images; a readout window narrower than the full width gives black
+ * images. The readout window therefore always covers the full width, from
+ * column start 1, and the horizontal crop is done by the output crop, which
+ * must end at least two columns before the end of the window. The vertical
+ * crop is done by the readout window itself, which shortens the frame.
+ *
+ * The default crop rectangle is the 2592x1944 image of the vendor register
+ * sequence, at (8, 8) in the area. Even offsets keep the GRBG order.
+ */
+#define GC5035_AREA_WIDTH		2608
+#define GC5035_AREA_HEIGHT		1964
+#define GC5035_ROW_START_OFFSET		4
+#define GC5035_COL_START		1
+#define GC5035_WIN_WIDTH		2616
+#define GC5035_OUT_LEFT_OFFSET		4
+#define GC5035_CROP_LEFT		8
+#define GC5035_CROP_TOP			8
+#define GC5035_WIDTH			2592
+#define GC5035_HEIGHT			1944
+
+/*
+ * The line length register counts units of 4 pixels. The frame length is the
+ * output height plus the vertical blanking; the vendor sequence uses 64 lines
+ * of blanking at full resolution, which is kept as the minimum. The frame
+ * period measured with every crop height tried, from 64 to 1964 lines, is
+ * (height + vblank) * HTS / pixel rate.
+ */
+#define GC5035_HTS			2920
+#define GC5035_VBLANK_MIN		64
+
+#define GC5035_EXP_MIN			4
+#define GC5035_EXP_STEP			1
+#define GC5035_EXP_MARGIN		16
+#define GC5035_EXP_DEF			984
+#define GC5035_VTS_MAX			0x3fff
+/* The sensor rounds the frame length to a multiple of 4 lines. */
+#define GC5035_VBLANK_STEP		4
+
+/*
+ * The register table has only been tested with a 19.2 MHz external clock. The
+ * link frequency is the external clock multiplied by the PLL setting and
+ * divided by 4: 422.4 MHz.
+ */
+#define GC5035_XCLK_FREQ		(19200 * HZ_PER_KHZ)
+#define GC5035_PLL_MULT			0x58
+#define GC5035_LINK_FREQ		(GC5035_XCLK_FREQ * GC5035_PLL_MULT / 4)
+#define GC5035_DATA_LANES		2
+#define GC5035_RGB_DEPTH		10
+#define GC5035_PIXEL_RATE		(GC5035_LINK_FREQ * 2 * \
+					 GC5035_DATA_LANES / GC5035_RGB_DEPTH)
+
+/*
+ * No datasheet is available. From the GC5035 driver posted by Tomasz Figa in
+ * 2020: IOVDD at least 50 us before AVDD and DVDD, at least 1200 MCLK cycles
+ * before the first I2C transaction, and 2000 MCLK cycles after streaming stops
+ * before the clock is switched off. From the intel/ipu6-drivers driver: the
+ * clock is enabled before reset is released and disabled after reset is
+ * asserted, and 5 ms elapse before the first I2C transaction, which also
+ * covers the 1200 cycles.
+ */
+#define GC5035_IOVDD_DELAY_US		50
+#define GC5035_RESET_DELAY_US		(5 * USEC_PER_MSEC)
+#define GC5035_STOP_DELAY_US		DIV_ROUND_UP(2000 * USEC_PER_MSEC, \
+					     GC5035_XCLK_FREQ / MSEC_PER_SEC)
+
+#define GC5035_MBUS_CODE		MEDIA_BUS_FMT_SGRBG10_1X10
+
+static const s64 gc5035_link_freq_menu[] = {
+	GC5035_LINK_FREQ,
+};
+
+static const char * const gc5035_test_pattern_menu[] = {
+	"No Pattern",
+	"Test Chart",
+};
+
+/* DOVDD (IOVDD) comes first: it has to be enabled before the others. */
+static const char * const gc5035_supply_name[] = {
+	"dovdd",
+	"avdd",
+	"dvdd",
+};
+
+/*
+ * V4L2_CID_ANALOGUE_GAIN is an index into this table of register 0xb6 values,
+ * one per analogue gain step. The digital gain is left at the 1.0x the register
+ * table programs.
+ */
+static const u8 gc5035_again_code[] = {
+	0,	/*  1.000x */
+	1,	/*  1.180x */
+	2,	/*  1.398x */
+	3,	/*  1.660x */
+	8,	/*  1.961x */
+	9,	/*  2.340x */
+	10,	/*  2.801x */
+	11,	/*  3.301x */
+	12,	/*  3.898x */
+	13,	/*  4.699x */
+	14,	/*  5.602x */
+	15,	/*  6.680x */
+	16,	/*  7.801x */
+	17,	/*  9.199x */
+	18,	/* 11.000x */
+	19,	/* 12.961x */
+	20,	/* 15.602x */
+};
+
+struct gc5035 {
+	struct device *dev;
+	struct v4l2_subdev sd;
+	struct media_pad pad;
+
+	struct clk *xclk;
+	struct regulator_bulk_data supplies[ARRAY_SIZE(gc5035_supply_name)];
+	struct gpio_desc *reset_gpio;
+	struct gpio_desc *powerdown_gpio;
+
+	struct v4l2_ctrl_handler ctrls;
+	struct v4l2_ctrl *exposure;
+	struct v4l2_ctrl *hblank;
+
+	struct regmap *regmap;
+};
+
+/*
+ * The register sequence is the vendor one for the 2592x1944 mode, without the
+ * registers written by the controls and without the geometry, which is
+ * computed from the crop rectangle. No register documentation is available:
+ * the names of the geometry registers are inferred from their values and from
+ * measurements.
+ */
+static const struct cci_reg_sequence gc5035_regs[] = {
+	/* System */
+	{ GC5035_REG_PAGE_SELECT, 0x00 },
+	{ GC5035_REG_STREAM, GC5035_STREAM_OFF },
+	{ CCI_REG8(0xfc), 0x01 },
+	{ CCI_REG8(0xf4), 0x40 },
+	{ CCI_REG8(0xf5), 0xe9 },
+	{ CCI_REG8(0xf6), 0x14 },
+	{ GC5035_REG_PLL_MULT, GC5035_PLL_MULT },
+	{ CCI_REG8(0xf9), 0x82 },
+	{ CCI_REG8(0xfa), 0x00 },
+	{ CCI_REG8(0xfc), 0x81 },
+	{ GC5035_REG_PAGE_SELECT, 0x00 },
+	{ CCI_REG8(0x36), 0x01 },
+	{ CCI_REG8(0xd3), 0x87 },
+	{ CCI_REG8(0x36), 0x00 },
+	{ CCI_REG8(0x33), 0x00 },
+	{ GC5035_REG_PAGE_SELECT, 0x03 },
+	{ CCI_REG8(0x01), 0xe7 },
+	{ CCI_REG8(0xf7), 0x01 },
+	{ CCI_REG8(0xfc), 0x8f },
+	{ CCI_REG8(0xfc), 0x8f },
+	{ CCI_REG8(0xfc), 0x8e },
+	{ GC5035_REG_PAGE_SELECT, 0x00 },
+	{ CCI_REG8(0xee), 0x30 },
+	{ CCI_REG8(0x87), 0x18 },
+	{ GC5035_REG_PAGE_SELECT, 0x01 },
+	{ CCI_REG8(0x8c), 0x90 },
+	{ GC5035_REG_PAGE_SELECT, 0x00 },
+	/* Analog & CISCTL */
+	{ GC5035_REG_LINE_LENGTH, GC5035_HTS / 4 },
+	{ CCI_REG8(0x9d), 0x18 },
+	{ CCI_REG8(0x11), 0x02 },
+	{ CCI_REG8(0x17), 0x80 },
+	{ CCI_REG8(0x19), 0x05 },
+	{ GC5035_REG_PAGE_SELECT, 0x02 },
+	{ CCI_REG8(0x30), 0x03 },
+	{ CCI_REG8(0x31), 0x03 },
+	{ GC5035_REG_PAGE_SELECT, 0x00 },
+	{ CCI_REG8(0xd9), 0xc0 },
+	{ CCI_REG8(0x1b), 0x20 },
+	{ CCI_REG8(0x21), 0x40 },
+	{ CCI_REG8(0x28), 0x22 },
+	{ CCI_REG8(0x29), 0x56 },
+	{ CCI_REG8(0x44), 0x20 },
+	{ CCI_REG8(0x4b), 0x10 },
+	{ CCI_REG8(0x4e), 0x1a },
+	{ CCI_REG8(0x50), 0x11 },
+	{ CCI_REG8(0x52), 0x33 },
+	{ CCI_REG8(0x53), 0x44 },
+	{ CCI_REG8(0x55), 0x10 },
+	{ CCI_REG8(0x5b), 0x11 },
+	{ CCI_REG8(0xc5), 0x02 },
+	{ CCI_REG8(0x8c), 0x1a },
+	{ GC5035_REG_PAGE_SELECT, 0x02 },
+	{ CCI_REG8(0x33), 0x05 },
+	{ CCI_REG8(0x32), 0x38 },
+	{ GC5035_REG_PAGE_SELECT, 0x00 },
+	{ CCI_REG8(0x91), 0x80 },
+	{ CCI_REG8(0x92), 0x28 },
+	{ CCI_REG8(0x93), 0x20 },
+	{ CCI_REG8(0x95), 0xa0 },
+	{ CCI_REG8(0x96), 0xe0 },
+	{ CCI_REG8(0xd5), 0xfc },
+	{ CCI_REG8(0x97), 0x28 },
+	{ CCI_REG8(0x16), 0x0c },
+	{ CCI_REG8(0x1a), 0x1a },
+	{ CCI_REG8(0x1f), 0x11 },
+	{ CCI_REG8(0x20), 0x10 },
+	{ CCI_REG8(0x46), 0x83 },
+	{ CCI_REG8(0x4a), 0x04 },
+	{ CCI_REG8(0x54), 0x02 },
+	{ CCI_REG8(0x62), 0x00 },
+	{ CCI_REG8(0x72), 0x8f },
+	{ CCI_REG8(0x73), 0x89 },
+	{ CCI_REG8(0x7a), 0x05 },
+	{ CCI_REG8(0x7d), 0xcc },
+	{ CCI_REG8(0x90), 0x00 },
+	{ CCI_REG8(0xce), 0x18 },
+	{ CCI_REG8(0xd0), 0xb2 },
+	{ CCI_REG8(0xd2), 0x40 },
+	{ CCI_REG8(0xe6), 0xe0 },
+	{ GC5035_REG_PAGE_SELECT, 0x02 },
+	{ CCI_REG8(0x12), 0x01 },
+	{ CCI_REG8(0x13), 0x01 },
+	{ CCI_REG8(0x14), 0x01 },
+	{ CCI_REG8(0x15), 0x02 },
+	{ CCI_REG8(0x22), 0x7c },
+	{ CCI_REG8(0x91), 0x00 },
+	{ CCI_REG8(0x92), 0x00 },
+	{ CCI_REG8(0x93), 0x00 },
+	{ CCI_REG8(0x94), 0x00 },
+	{ GC5035_REG_PAGE_SELECT, 0x00 },
+	{ CCI_REG8(0xfc), 0x88 },
+	{ GC5035_REG_PAGE_SELECT, 0x10 },	/* not a page */
+	{ GC5035_REG_PAGE_SELECT, 0x00 },
+	{ CCI_REG8(0xfc), 0x8e },
+	{ GC5035_REG_PAGE_SELECT, 0x00 },
+	{ GC5035_REG_PAGE_SELECT, 0x00 },
+	{ GC5035_REG_PAGE_SELECT, 0x00 },
+	{ CCI_REG8(0xfc), 0x88 },
+	{ GC5035_REG_PAGE_SELECT, 0x10 },	/* not a page */
+	{ GC5035_REG_PAGE_SELECT, 0x00 },
+	{ CCI_REG8(0xfc), 0x8e },
+	/* GAIN */
+	{ GC5035_REG_PAGE_SELECT, 0x00 },
+	{ CCI_REG8(0xb0), 0x6e },
+	{ GC5035_REG_DIGITAL_GAIN, GC5035_DIGITAL_GAIN_1X },
+	{ CCI_REG8(0xb3), 0x00 },
+	{ CCI_REG8(0xb4), 0x00 },
+	/* ISP */
+	{ GC5035_REG_PAGE_SELECT, 0x01 },
+	{ CCI_REG8(0x53), 0x00 },
+	{ CCI_REG8(0x89), 0x03 },
+	{ CCI_REG8(0x60), 0x40 },
+	/* BLK */
+	{ GC5035_REG_PAGE_SELECT, 0x01 },
+	{ CCI_REG8(0x42), 0x21 },
+	{ CCI_REG8(0x49), 0x03 },
+	{ CCI_REG8(0x4a), 0xff },
+	{ CCI_REG8(0x4b), 0xc0 },
+	{ CCI_REG8(0x55), 0x00 },
+	/* anti_blooming */
+	{ GC5035_REG_PAGE_SELECT, 0x01 },
+	{ CCI_REG8(0x41), 0x28 },
+	{ CCI_REG8(0x4c), 0x00 },
+	{ CCI_REG8(0x4d), 0x00 },
+	{ CCI_REG8(0x4e), 0x3c },
+	{ CCI_REG8(0x44), 0x08 },
+	{ CCI_REG8(0x48), 0x02 },
+	/* CROP, the geometry is written by gc5035_write_geometry() */
+	{ GC5035_REG_PAGE_SELECT, 0x01 },
+	{ CCI_REG8(0x99), 0x00 },
+	/* MIPI */
+	{ GC5035_REG_PAGE_SELECT, 0x03 },
+	{ CCI_REG8(0x02), 0x57 },
+	{ CCI_REG8(0x03), 0xb7 },
+	{ CCI_REG8(0x15), 0x14 },
+	{ CCI_REG8(0x18), 0x0f },
+	{ CCI_REG8(0x21), 0x22 },
+	{ CCI_REG8(0x22), 0x06 },
+	{ CCI_REG8(0x23), 0x48 },
+	{ CCI_REG8(0x24), 0x12 },
+	{ CCI_REG8(0x25), 0x28 },
+	{ CCI_REG8(0x26), 0x08 },
+	{ CCI_REG8(0x29), 0x06 },
+	{ CCI_REG8(0x2a), 0x58 },
+	{ CCI_REG8(0x2b), 0x08 },
+	{ GC5035_REG_PAGE_SELECT, 0x00 },
+	{ GC5035_REG_STREAM, GC5035_STREAM_OFF },
+};
+
+static inline struct gc5035 *to_gc5035(struct v4l2_subdev *sd)
+{
+	return container_of(sd, struct gc5035, sd);
+}
+
+static int gc5035_set_page(struct gc5035 *gc5035, u8 page)
+{
+	return cci_write(gc5035->regmap, GC5035_REG_PAGE_SELECT, page, NULL);
+}
+
+static int gc5035_power_on(struct device *dev)
+{
+	struct v4l2_subdev *sd = dev_get_drvdata(dev);
+	struct gc5035 *gc5035 = to_gc5035(sd);
+	int ret;
+
+	ret = regulator_enable(gc5035->supplies[0].consumer);
+	if (ret) {
+		dev_err(dev, "failed to enable dovdd: %d\n", ret);
+		return ret;
+	}
+
+	fsleep(GC5035_IOVDD_DELAY_US);
+
+	ret = regulator_bulk_enable(ARRAY_SIZE(gc5035_supply_name) - 1,
+				    &gc5035->supplies[1]);
+	if (ret) {
+		dev_err(dev, "failed to enable regulators: %d\n", ret);
+		goto err_dovdd;
+	}
+
+	ret = clk_prepare_enable(gc5035->xclk);
+	if (ret) {
+		dev_err(dev, "failed to enable clock: %d\n", ret);
+		goto err_regulators;
+	}
+
+	gpiod_set_value_cansleep(gc5035->powerdown_gpio, 0);
+	gpiod_set_value_cansleep(gc5035->reset_gpio, 0);
+
+	fsleep(GC5035_RESET_DELAY_US);
+
+	return 0;
+
+err_regulators:
+	regulator_bulk_disable(ARRAY_SIZE(gc5035_supply_name) - 1,
+			       &gc5035->supplies[1]);
+err_dovdd:
+	regulator_disable(gc5035->supplies[0].consumer);
+
+	return ret;
+}
+
+static int gc5035_power_off(struct device *dev)
+{
+	struct v4l2_subdev *sd = dev_get_drvdata(dev);
+	struct gc5035 *gc5035 = to_gc5035(sd);
+
+	fsleep(GC5035_STOP_DELAY_US);
+
+	gpiod_set_value_cansleep(gc5035->powerdown_gpio, 1);
+	gpiod_set_value_cansleep(gc5035->reset_gpio, 1);
+	clk_disable_unprepare(gc5035->xclk);
+	regulator_bulk_disable(ARRAY_SIZE(gc5035_supply_name) - 1,
+			       &gc5035->supplies[1]);
+	regulator_disable(gc5035->supplies[0].consumer);
+
+	return 0;
+}
+
+static int gc5035_enum_mbus_code(struct v4l2_subdev *sd,
+				 struct v4l2_subdev_state *sd_state,
+				 struct v4l2_subdev_mbus_code_enum *code)
+{
+	if (code->index > 0)
+		return -EINVAL;
+
+	code->code = GC5035_MBUS_CODE;
+
+	return 0;
+}
+
+static int gc5035_enum_frame_size(struct v4l2_subdev *sd,
+				  struct v4l2_subdev_state *sd_state,
+				  struct v4l2_subdev_frame_size_enum *fse)
+{
+	if (fse->code != GC5035_MBUS_CODE)
+		return -EINVAL;
+
+	if (fse->index > 0)
+		return -EINVAL;
+
+	fse->min_width = GC5035_WIDTH;
+	fse->max_width = GC5035_WIDTH;
+	fse->min_height = GC5035_HEIGHT;
+	fse->max_height = GC5035_HEIGHT;
+
+	return 0;
+}
+
+static void gc5035_fill_format(struct v4l2_mbus_framefmt *fmt,
+			       const struct v4l2_rect *crop)
+{
+	fmt->width = crop->width;
+	fmt->height = crop->height;
+	fmt->code = GC5035_MBUS_CODE;
+	fmt->field = V4L2_FIELD_NONE;
+	fmt->colorspace = V4L2_COLORSPACE_RAW;
+	fmt->ycbcr_enc = V4L2_MAP_YCBCR_ENC_DEFAULT(fmt->colorspace);
+	fmt->quantization = V4L2_QUANTIZATION_FULL_RANGE;
+	fmt->xfer_func = V4L2_XFER_FUNC_NONE;
+}
+
+static int gc5035_get_selection(struct v4l2_subdev *sd,
+				const struct v4l2_subdev_client_info *ci,
+				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, 0);
+		break;
+	case V4L2_SEL_TGT_CROP_DEFAULT:
+		sel->r.top = GC5035_CROP_TOP;
+		sel->r.left = GC5035_CROP_LEFT;
+		sel->r.width = GC5035_WIDTH;
+		sel->r.height = GC5035_HEIGHT;
+		break;
+	case V4L2_SEL_TGT_CROP_BOUNDS:
+	case V4L2_SEL_TGT_NATIVE_SIZE:
+		sel->r.top = 0;
+		sel->r.left = 0;
+		sel->r.width = GC5035_AREA_WIDTH;
+		sel->r.height = GC5035_AREA_HEIGHT;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+static int gc5035_init_state(struct v4l2_subdev *sd,
+			     struct v4l2_subdev_state *state)
+{
+	struct v4l2_rect *crop = v4l2_subdev_state_get_crop(state, 0);
+
+	crop->left = GC5035_CROP_LEFT;
+	crop->top = GC5035_CROP_TOP;
+	crop->width = GC5035_WIDTH;
+	crop->height = GC5035_HEIGHT;
+
+	gc5035_fill_format(v4l2_subdev_state_get_format(state, 0), crop);
+
+	return 0;
+}
+
+static int gc5035_get_frame_desc(struct v4l2_subdev *sd, unsigned int pad,
+				 struct v4l2_mbus_frame_desc *fd)
+{
+	fd->type = V4L2_MBUS_FRAME_DESC_TYPE_CSI2;
+	fd->num_entries = 1;
+	fd->entry[0].pixelcode = GC5035_MBUS_CODE;
+	fd->entry[0].stream = 0;
+	fd->entry[0].bus.csi2.vc = 0;
+	fd->entry[0].bus.csi2.dt = MIPI_CSI2_DT_RAW10;
+
+	return 0;
+}
+
+static int gc5035_test_pattern(struct gc5035 *gc5035, u32 pattern)
+{
+	int ret;
+
+	ret = gc5035_set_page(gc5035, GC5035_PAGE_1);
+	if (ret)
+		return ret;
+
+	ret = cci_write(gc5035->regmap, GC5035_REG_TEST_PATTERN,
+			pattern ? GC5035_TEST_PATTERN_ON :
+				  GC5035_TEST_PATTERN_OFF, NULL);
+
+	/* Back to page 0 even on error, the other controls expect it. */
+	return gc5035_set_page(gc5035, GC5035_PAGE_0) ?: ret;
+}
+
+static int gc5035_set_ctrl(struct v4l2_ctrl *ctrl)
+{
+	struct gc5035 *gc5035 =
+		container_of(ctrl->handler, struct gc5035, ctrls);
+	const struct v4l2_mbus_framefmt *format;
+	struct v4l2_subdev_state *state;
+	s64 exposure_max;
+	int ret;
+
+	state = v4l2_subdev_get_locked_active_state(&gc5035->sd);
+	format = v4l2_subdev_state_get_format(state, 0);
+
+	if (ctrl->id == V4L2_CID_VBLANK) {
+		exposure_max = format->height + ctrl->val -
+			       GC5035_EXP_MARGIN;
+		ret = __v4l2_ctrl_modify_range(gc5035->exposure,
+					       GC5035_EXP_MIN, exposure_max,
+					       GC5035_EXP_STEP,
+					       min_t(s64, GC5035_EXP_DEF,
+						     exposure_max));
+		if (ret)
+			return ret;
+	}
+
+	if (!pm_runtime_get_if_active(gc5035->dev))
+		return 0;
+
+	ret = gc5035_set_page(gc5035, GC5035_PAGE_0);
+	if (ret)
+		goto out;
+
+	switch (ctrl->id) {
+	case V4L2_CID_EXPOSURE:
+		ret = cci_write(gc5035->regmap, GC5035_REG_EXPOSURE,
+				ctrl->val, NULL);
+		break;
+	case V4L2_CID_ANALOGUE_GAIN:
+		ret = cci_write(gc5035->regmap, GC5035_REG_ANALOGUE_GAIN,
+				gc5035_again_code[ctrl->val], NULL);
+		break;
+	case V4L2_CID_VBLANK:
+		ret = cci_write(gc5035->regmap, GC5035_REG_FRAME_LENGTH,
+				format->height + ctrl->val, NULL);
+		break;
+	case V4L2_CID_TEST_PATTERN:
+		ret = gc5035_test_pattern(gc5035, ctrl->val);
+		break;
+	default:
+		ret = -EINVAL;
+		break;
+	}
+
+out:
+	pm_runtime_put_autosuspend(gc5035->dev);
+
+	return ret;
+}
+
+static const struct v4l2_ctrl_ops gc5035_ctrl_ops = {
+	.s_ctrl = gc5035_set_ctrl,
+};
+
+/* Only ever called from probe, with the sensor powered up. */
+static int gc5035_identify_module(struct gc5035 *gc5035)
+{
+	u64 val;
+	int ret;
+
+	ret = cci_read(gc5035->regmap, GC5035_REG_CHIP_ID, &val, NULL);
+	if (ret)
+		return ret;
+
+	if (val != GC5035_CHIP_ID)
+		return dev_err_probe(gc5035->dev, -ENXIO,
+				     "chip id mismatch: %x != %llx\n",
+				     GC5035_CHIP_ID, val);
+
+	return 0;
+}
+
+static int gc5035_write_geometry(struct gc5035 *gc5035,
+				 const struct v4l2_rect *crop)
+{
+	int ret = 0;
+
+	/* Readout window: full width, the rows of the crop rectangle. */
+	cci_write(gc5035->regmap, GC5035_REG_PAGE_SELECT, GC5035_PAGE_0, &ret);
+	cci_write(gc5035->regmap, GC5035_REG_WIN_TOP,
+		  crop->top + GC5035_ROW_START_OFFSET, &ret);
+	cci_write(gc5035->regmap, GC5035_REG_WIN_LEFT, GC5035_COL_START, &ret);
+	cci_write(gc5035->regmap, GC5035_REG_WIN_HEIGHT, crop->height, &ret);
+	cci_write(gc5035->regmap, GC5035_REG_WIN_WIDTH, GC5035_WIN_WIDTH, &ret);
+
+	/* Output crop: the columns of the crop rectangle. */
+	cci_write(gc5035->regmap, GC5035_REG_PAGE_SELECT, GC5035_PAGE_1, &ret);
+	cci_write(gc5035->regmap, GC5035_REG_CROP_TOP, 0, &ret);
+	cci_write(gc5035->regmap, GC5035_REG_CROP_LEFT,
+		  crop->left + GC5035_OUT_LEFT_OFFSET, &ret);
+	cci_write(gc5035->regmap, GC5035_REG_OUT_HEIGHT, crop->height, &ret);
+	cci_write(gc5035->regmap, GC5035_REG_OUT_WIDTH, crop->width, &ret);
+	cci_write(gc5035->regmap, GC5035_REG_PAGE_SELECT, GC5035_PAGE_0, &ret);
+
+	return ret;
+}
+
+static int gc5035_enable_streams(struct v4l2_subdev *sd,
+				 struct v4l2_subdev_state *state,
+				 u32 pad, u64 streams_mask)
+{
+	struct gc5035 *gc5035 = to_gc5035(sd);
+	int ret;
+
+	ret = pm_runtime_resume_and_get(gc5035->dev);
+	if (ret < 0)
+		return ret;
+
+	ret = cci_multi_reg_write(gc5035->regmap, gc5035_regs,
+				  ARRAY_SIZE(gc5035_regs), NULL);
+	if (ret)
+		goto err_rpm_put;
+
+	ret = gc5035_write_geometry(gc5035,
+				    v4l2_subdev_state_get_crop(state, 0));
+	if (ret)
+		goto err_rpm_put;
+
+	ret = __v4l2_ctrl_handler_setup(&gc5035->ctrls);
+	if (ret)
+		goto err_rpm_put;
+
+	ret = cci_write(gc5035->regmap, GC5035_REG_STREAM,
+			GC5035_STREAM_ON, NULL);
+	if (ret)
+		goto err_rpm_put;
+
+	return 0;
+
+err_rpm_put:
+	pm_runtime_put_autosuspend(gc5035->dev);
+
+	return ret;
+}
+
+static int gc5035_disable_streams(struct v4l2_subdev *sd,
+				  struct v4l2_subdev_state *state,
+				  u32 pad, u64 streams_mask)
+{
+	struct gc5035 *gc5035 = to_gc5035(sd);
+	int ret = 0;
+
+	/* cci_write() logs errors, nothing else can be done about them. */
+	cci_write(gc5035->regmap, GC5035_REG_PAGE_SELECT, GC5035_PAGE_0, &ret);
+	cci_write(gc5035->regmap, GC5035_REG_STREAM, GC5035_STREAM_OFF, &ret);
+
+	pm_runtime_put_autosuspend(gc5035->dev);
+
+	return 0;
+}
+
+static const struct v4l2_subdev_video_ops gc5035_video_ops = {
+	.s_stream = v4l2_subdev_s_stream_helper,
+};
+
+static const struct v4l2_subdev_pad_ops gc5035_pad_ops = {
+	.enum_mbus_code = gc5035_enum_mbus_code,
+	.enum_frame_size = gc5035_enum_frame_size,
+	.get_fmt = v4l2_subdev_get_fmt,
+	.get_selection = gc5035_get_selection,
+	.get_frame_desc = gc5035_get_frame_desc,
+	.enable_streams = gc5035_enable_streams,
+	.disable_streams = gc5035_disable_streams,
+};
+
+static const struct v4l2_subdev_ops gc5035_subdev_ops = {
+	.video = &gc5035_video_ops,
+	.pad = &gc5035_pad_ops,
+};
+
+static const struct v4l2_subdev_internal_ops gc5035_internal_ops = {
+	.init_state = gc5035_init_state,
+};
+
+static int gc5035_parse_fwnode(struct gc5035 *gc5035)
+{
+	struct v4l2_fwnode_endpoint bus_cfg = {
+		.bus_type = V4L2_MBUS_CSI2_DPHY,
+	};
+	struct device *dev = gc5035->dev;
+	struct fwnode_handle *endpoint;
+	unsigned long link_freq_bitmap;
+	int ret;
+
+	endpoint = fwnode_graph_get_endpoint_by_id(dev_fwnode(dev), 0, 0,
+						   FWNODE_GRAPH_ENDPOINT_NEXT);
+	ret = v4l2_fwnode_endpoint_alloc_parse(endpoint, &bus_cfg);
+	fwnode_handle_put(endpoint);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to parse endpoint\n");
+
+	/* The register table configures the CSI-2 transmitter for 2 lanes. */
+	if (bus_cfg.bus.mipi_csi2.num_data_lanes != GC5035_DATA_LANES) {
+		ret = dev_err_probe(dev, -EINVAL,
+				    "unsupported number of data lanes %u\n",
+				    bus_cfg.bus.mipi_csi2.num_data_lanes);
+		goto done;
+	}
+
+	ret = v4l2_link_freq_to_bitmap(dev, bus_cfg.link_frequencies,
+				       bus_cfg.nr_of_link_frequencies,
+				       gc5035_link_freq_menu,
+				       ARRAY_SIZE(gc5035_link_freq_menu),
+				       &link_freq_bitmap);
+
+done:
+	v4l2_fwnode_endpoint_free(&bus_cfg);
+
+	return ret;
+}
+
+static int gc5035_init_controls(struct gc5035 *gc5035)
+{
+	struct v4l2_fwnode_device_properties props;
+	struct v4l2_ctrl_handler *ctrl_hdlr;
+	struct v4l2_ctrl *link_freq;
+	s64 exposure_max;
+	int ret;
+
+	ret = v4l2_fwnode_device_parse(gc5035->dev, &props);
+	if (ret)
+		return ret;
+
+	ctrl_hdlr = &gc5035->ctrls;
+	v4l2_ctrl_handler_init(ctrl_hdlr, 9);
+
+	link_freq = v4l2_ctrl_new_int_menu(ctrl_hdlr, NULL, V4L2_CID_LINK_FREQ,
+					   ARRAY_SIZE(gc5035_link_freq_menu) -
+					   1, 0, gc5035_link_freq_menu);
+
+	v4l2_ctrl_new_std(ctrl_hdlr, NULL, V4L2_CID_PIXEL_RATE,
+			  GC5035_PIXEL_RATE, GC5035_PIXEL_RATE, 1,
+			  GC5035_PIXEL_RATE);
+
+	v4l2_ctrl_new_std(ctrl_hdlr, &gc5035_ctrl_ops, V4L2_CID_VBLANK,
+			  GC5035_VBLANK_MIN,
+			  GC5035_VBLANK_MIN +
+			  round_down(GC5035_VTS_MAX - GC5035_HEIGHT -
+				     GC5035_VBLANK_MIN, GC5035_VBLANK_STEP),
+			  GC5035_VBLANK_STEP, GC5035_VBLANK_MIN);
+
+	gc5035->hblank = v4l2_ctrl_new_std(ctrl_hdlr, NULL, V4L2_CID_HBLANK,
+					   GC5035_HTS - GC5035_WIDTH,
+					   GC5035_HTS - GC5035_WIDTH, 1,
+					   GC5035_HTS - GC5035_WIDTH);
+
+	exposure_max = GC5035_HEIGHT + GC5035_VBLANK_MIN - GC5035_EXP_MARGIN;
+	gc5035->exposure = v4l2_ctrl_new_std(ctrl_hdlr, &gc5035_ctrl_ops,
+					     V4L2_CID_EXPOSURE, GC5035_EXP_MIN,
+					     exposure_max, GC5035_EXP_STEP,
+					     GC5035_EXP_DEF);
+
+	v4l2_ctrl_new_std(ctrl_hdlr, &gc5035_ctrl_ops, V4L2_CID_ANALOGUE_GAIN,
+			  0, ARRAY_SIZE(gc5035_again_code) - 1, 1, 0);
+
+	v4l2_ctrl_new_std_menu_items(ctrl_hdlr, &gc5035_ctrl_ops,
+				     V4L2_CID_TEST_PATTERN,
+				     ARRAY_SIZE(gc5035_test_pattern_menu) - 1,
+				     0, 0, gc5035_test_pattern_menu);
+
+	v4l2_ctrl_new_fwnode_properties(ctrl_hdlr, &gc5035_ctrl_ops, &props);
+
+	if (ctrl_hdlr->error)
+		return v4l2_ctrl_handler_free(ctrl_hdlr);
+
+	link_freq->flags |= V4L2_CTRL_FLAG_READ_ONLY;
+	gc5035->hblank->flags |= V4L2_CTRL_FLAG_READ_ONLY;
+
+	gc5035->sd.ctrl_handler = ctrl_hdlr;
+
+	return 0;
+}
+
+static int gc5035_probe(struct i2c_client *client)
+{
+	struct device *dev = &client->dev;
+	struct gc5035 *gc5035;
+	unsigned long freq;
+	int ret;
+
+	gc5035 = devm_kzalloc(dev, sizeof(*gc5035), GFP_KERNEL);
+	if (!gc5035)
+		return -ENOMEM;
+
+	gc5035->dev = dev;
+
+	v4l2_i2c_subdev_init(&gc5035->sd, client, &gc5035_subdev_ops);
+	gc5035->sd.internal_ops = &gc5035_internal_ops;
+
+	ret = gc5035_parse_fwnode(gc5035);
+	if (ret)
+		return ret;
+
+	gc5035->xclk = devm_v4l2_sensor_clk_get(dev, NULL);
+	if (IS_ERR(gc5035->xclk))
+		return dev_err_probe(dev, PTR_ERR(gc5035->xclk),
+				     "failed to get clock\n");
+
+	freq = clk_get_rate(gc5035->xclk);
+	if (freq != GC5035_XCLK_FREQ)
+		return dev_err_probe(dev, -EINVAL,
+				     "external clock %lu Hz not supported\n",
+				     freq);
+
+	gc5035->regmap = devm_cci_regmap_init_i2c(client, 8);
+	if (IS_ERR(gc5035->regmap))
+		return dev_err_probe(dev, PTR_ERR(gc5035->regmap),
+				     "failed to init CCI\n");
+
+	gc5035->reset_gpio = devm_gpiod_get_optional(dev, "reset",
+						     GPIOD_OUT_HIGH);
+	if (IS_ERR(gc5035->reset_gpio))
+		return dev_err_probe(dev, PTR_ERR(gc5035->reset_gpio),
+				     "failed to get reset GPIO\n");
+
+	gc5035->powerdown_gpio = devm_gpiod_get_optional(dev, "powerdown",
+							 GPIOD_OUT_HIGH);
+	if (IS_ERR(gc5035->powerdown_gpio))
+		return dev_err_probe(dev, PTR_ERR(gc5035->powerdown_gpio),
+				     "failed to get powerdown GPIO\n");
+
+	for (unsigned int i = 0; i < ARRAY_SIZE(gc5035_supply_name); i++)
+		gc5035->supplies[i].supply = gc5035_supply_name[i];
+
+	ret = devm_regulator_bulk_get(dev, ARRAY_SIZE(gc5035_supply_name),
+				      gc5035->supplies);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to get regulators\n");
+
+	ret = gc5035_power_on(dev);
+	if (ret)
+		return ret;
+
+	ret = gc5035_identify_module(gc5035);
+	if (ret)
+		goto err_power_off;
+
+	ret = gc5035_init_controls(gc5035);
+	if (ret) {
+		dev_err_probe(dev, ret, "failed to init controls\n");
+		goto err_power_off;
+	}
+
+	gc5035->sd.flags |= V4L2_SUBDEV_FL_HAS_DEVNODE;
+	gc5035->pad.flags = MEDIA_PAD_FL_SOURCE;
+	gc5035->sd.entity.function = MEDIA_ENT_F_CAM_SENSOR;
+
+	ret = media_entity_pads_init(&gc5035->sd.entity, 1, &gc5035->pad);
+	if (ret < 0) {
+		dev_err_probe(dev, ret, "failed to init media entity\n");
+		goto err_ctrl_handler_free;
+	}
+
+	gc5035->sd.state_lock = gc5035->ctrls.lock;
+	ret = v4l2_subdev_init_finalize(&gc5035->sd);
+	if (ret < 0) {
+		dev_err_probe(dev, ret, "failed to finalize subdev\n");
+		goto err_media_entity_cleanup;
+	}
+
+	pm_runtime_set_active(dev);
+	pm_runtime_enable(dev);
+	pm_runtime_set_autosuspend_delay(dev, 1000);
+	pm_runtime_use_autosuspend(dev);
+
+	ret = v4l2_async_register_subdev_sensor(&gc5035->sd);
+	if (ret < 0) {
+		dev_err_probe(dev, ret, "failed to register subdev\n");
+		goto err_rpm;
+	}
+
+	pm_runtime_idle(dev);
+
+	return 0;
+
+err_rpm:
+	pm_runtime_disable(dev);
+	pm_runtime_dont_use_autosuspend(dev);
+	pm_runtime_set_suspended(dev);
+	v4l2_subdev_cleanup(&gc5035->sd);
+
+err_media_entity_cleanup:
+	media_entity_cleanup(&gc5035->sd.entity);
+
+err_ctrl_handler_free:
+	v4l2_ctrl_handler_free(&gc5035->ctrls);
+
+err_power_off:
+	gc5035_power_off(dev);
+
+	return ret;
+}
+
+static void gc5035_remove(struct i2c_client *client)
+{
+	struct v4l2_subdev *sd = i2c_get_clientdata(client);
+	struct gc5035 *gc5035 = to_gc5035(sd);
+
+	v4l2_async_unregister_subdev(sd);
+	v4l2_subdev_cleanup(sd);
+	media_entity_cleanup(&sd->entity);
+	v4l2_ctrl_handler_free(&gc5035->ctrls);
+
+	pm_runtime_disable(&client->dev);
+	if (!pm_runtime_status_suspended(&client->dev)) {
+		gc5035_power_off(&client->dev);
+		pm_runtime_set_suspended(&client->dev);
+	}
+	pm_runtime_dont_use_autosuspend(&client->dev);
+}
+
+static const struct dev_pm_ops gc5035_pm_ops = {
+	RUNTIME_PM_OPS(gc5035_power_off, gc5035_power_on, NULL)
+};
+
+static const struct acpi_device_id gc5035_acpi_ids[] = {
+	{ .id = "GCTI5035" },
+	{ }
+};
+MODULE_DEVICE_TABLE(acpi, gc5035_acpi_ids);
+
+static struct i2c_driver gc5035_i2c_driver = {
+	.driver = {
+		.name = "gc5035",
+		.acpi_match_table = gc5035_acpi_ids,
+		.pm = pm_ptr(&gc5035_pm_ops),
+	},
+	.probe = gc5035_probe,
+	.remove = gc5035_remove,
+};
+module_i2c_driver(gc5035_i2c_driver);
+
+MODULE_AUTHOR("Nicola Fiorillo <nicfio@gmail.com>");
+MODULE_DESCRIPTION("GalaxyCore GC5035 sensor driver");
+MODULE_LICENSE("GPL");
-- 
2.47.3


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

* [PATCH v2 2/7] media: i2c: Add GC8034 image sensor driver
  2026-10-12 14:34 ` [PATCH v2 0/7] media: Add GalaxyCore GC5035 and GC8034 sensor drivers Nicola Fiorillo
  2026-10-12 14:34   ` [PATCH v2 1/7] media: i2c: Add GC5035 image sensor driver Nicola Fiorillo
@ 2026-10-12 14:34   ` Nicola Fiorillo
  2026-10-12 14:34   ` [PATCH v2 3/7] media: ipu-bridge: Add GalaxyCore GC5035 and GC8034 Nicola Fiorillo
                     ` (4 subsequent siblings)
  6 siblings, 0 replies; 15+ messages in thread
From: Nicola Fiorillo @ 2026-10-12 14:34 UTC (permalink / raw)
  To: Laurent Pinchart, Sakari Ailus
  Cc: Mauro Carvalho Chehab, Hans Verkuil, Dan Scally, Hans de Goede,
	Uwe Kleine-König, linux-media, linux-kernel

Add a driver for the GalaxyCore GC8034, an 8 Mpixel raw Bayer sensor with
a four lane MIPI CSI-2 interface. The driver supports the exposure,
analogue gain and vertical blanking controls, and outputs 3264x2448
RAW10.

I don't have a datasheet for the sensor. The initialisation sequence comes
from the Rockchip BSP driver. The readout window, the output crop and the
CSI-2 line length are computed from the crop rectangle, in the coordinates
of a 3282x2500 area measured to contain image data. The driver reports
that area as the native size and the crop bounds; it is not the physical
pixel array. The default crop rectangle gives the same image as the vendor
sequence. A frame lasts the readout window plus 20 lines plus the blanking
register, which therefore holds vblank minus 36. The pixel rate, 256 MHz,
is the one that gives the measured frame periods, from 1.869 ms for 112
frame lines to 41.653 ms for 2496 frame lines.

Assisted-by: claude-opus-5-5 coccinelle sparse smatch
Signed-off-by: Nicola Fiorillo <nicfio@gmail.com>
---
 MAINTAINERS                |    6 +
 drivers/media/i2c/Kconfig  |   10 +
 drivers/media/i2c/Makefile |    1 +
 drivers/media/i2c/gc8034.c | 1021 ++++++++++++++++++++++++++++++++++++
 4 files changed, 1038 insertions(+)
 create mode 100644 drivers/media/i2c/gc8034.c

diff --git a/MAINTAINERS b/MAINTAINERS
index ca264ceef..a51768324 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -10881,6 +10881,12 @@ L:	linux-media@vger.kernel.org
 S:	Maintained
 F:	drivers/media/i2c/gc5035.c
 
+GALAXYCORE GC8034 CAMERA SENSOR DRIVER
+M:	Nicola Fiorillo <nicfio@gmail.com>
+L:	linux-media@vger.kernel.org
+S:	Maintained
+F:	drivers/media/i2c/gc8034.c
+
 GATEWORKS SYSTEM CONTROLLER (GSC) DRIVER
 M:	Tim Harvey <tharvey@gateworks.com>
 S:	Maintained
diff --git a/drivers/media/i2c/Kconfig b/drivers/media/i2c/Kconfig
index fca5bcf98..f9fdb7b51 100644
--- a/drivers/media/i2c/Kconfig
+++ b/drivers/media/i2c/Kconfig
@@ -120,6 +120,16 @@ config VIDEO_GC5035
 	  To compile this driver as a module, choose M here: the
 	  module will be called gc5035.
 
+config VIDEO_GC8034
+	tristate "GalaxyCore GC8034 sensor support"
+	select V4L2_CCI_I2C
+	help
+	  This is a Video4Linux2 sensor driver for the GalaxyCore GC8034
+	  8 Mpixel camera.
+
+	  To compile this driver as a module, choose M here: the
+	  module will be called gc8034.
+
 config VIDEO_HI556
 	tristate "Hynix Hi-556 sensor support"
 	help
diff --git a/drivers/media/i2c/Makefile b/drivers/media/i2c/Makefile
index bb8552bbd..d177cc3d6 100644
--- a/drivers/media/i2c/Makefile
+++ b/drivers/media/i2c/Makefile
@@ -43,6 +43,7 @@ obj-$(CONFIG_VIDEO_GC05A2) += gc05a2.o
 obj-$(CONFIG_VIDEO_GC08A3) += gc08a3.o
 obj-$(CONFIG_VIDEO_GC2145) += gc2145.o
 obj-$(CONFIG_VIDEO_GC5035) += gc5035.o
+obj-$(CONFIG_VIDEO_GC8034) += gc8034.o
 obj-$(CONFIG_VIDEO_HI556) += hi556.o
 obj-$(CONFIG_VIDEO_HI846) += hi846.o
 obj-$(CONFIG_VIDEO_HI847) += hi847.o
diff --git a/drivers/media/i2c/gc8034.c b/drivers/media/i2c/gc8034.c
new file mode 100644
index 000000000..c21fb8510
--- /dev/null
+++ b/drivers/media/i2c/gc8034.c
@@ -0,0 +1,1021 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Driver for GalaxyCore GC8034 image sensor
+ *
+ * Copyright (C) 2017 Fuzhou Rockchip Electronics Co., Ltd.
+ * Copyright (C) 2026 Nicola Fiorillo <nicfio@gmail.com>
+ *
+ * The registers and sequences come from the Rockchip BSP driver,
+ * rockchip-linux/kernel, branch develop-5.10.
+ */
+#include <linux/array_size.h>
+#include <linux/clk.h>
+#include <linux/container_of.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/math.h>
+#include <linux/module.h>
+#include <linux/pm_runtime.h>
+#include <linux/property.h>
+#include <linux/regulator/consumer.h>
+#include <linux/time64.h>
+#include <linux/types.h>
+#include <linux/units.h>
+
+#include <media/mipi-csi2.h>
+#include <media/v4l2-cci.h>
+#include <media/v4l2-common.h>
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-fwnode.h>
+#include <media/v4l2-subdev.h>
+
+/*
+ * 8 bit register addresses, with the page selected by register 0xfe. The
+ * vendor sequence also writes 0x10 to register 0xfe, which is not a page
+ * number and is not documented, so the pages are not handled with regmap
+ * ranges.
+ */
+#define GC8034_REG_PAGE_SELECT		CCI_REG8(0xfe)
+#define GC8034_PAGE_0			0x00
+
+/* readable on any page */
+#define GC8034_REG_CHIP_ID		CCI_REG16(0xf0)
+#define GC8034_CHIP_ID			0x8044	/* not 0x8034 */
+
+#define GC8034_REG_EXPOSURE		CCI_REG16(0x03)
+#define GC8034_REG_LINE_LENGTH		CCI_REG16(0x05)
+#define GC8034_REG_BLANKING		CCI_REG16(0x07)
+#define GC8034_REG_WIN_TOP		CCI_REG16(0x09)
+#define GC8034_REG_WIN_LEFT		CCI_REG16(0x0b)
+#define GC8034_REG_WIN_HEIGHT		CCI_REG16(0x0d)
+#define GC8034_REG_WIN_WIDTH		CCI_REG16(0x0f)
+#define GC8034_REG_MIRROR		CCI_REG8(0x17)
+#define GC8034_MIRROR_NONE		0xc0
+#define GC8034_REG_STREAM		CCI_REG8(0x3f)
+#define GC8034_STREAM_ON		0xd0
+#define GC8034_STREAM_OFF		0x00
+#define GC8034_REG_CROP_TOP		CCI_REG16(0x91)
+#define GC8034_REG_CROP_LEFT		CCI_REG16(0x93)
+#define GC8034_REG_OUT_HEIGHT		CCI_REG16(0x95)
+#define GC8034_REG_OUT_WIDTH		CCI_REG16(0x97)
+#define GC8034_REG_DIGITAL_GAIN	CCI_REG16(0xb1)
+#define GC8034_DIGITAL_GAIN_1X	0x100
+#define GC8034_REG_ANALOGUE_GAIN	CCI_REG8(0xb6)
+
+/* page 3 */
+#define GC8034_PAGE_3			0x03
+/* CSI-2 line length in bytes, low byte first */
+#define GC8034_REG_LINE_BYTES		CCI_REG16_LE(0x12)
+
+/*
+ * No register documentation is available: the geometry below was measured.
+ *
+ * Crop rectangles are expressed in the coordinates of a 3282x2500 area: the
+ * columns of the vendor readout window (column start 4, 3284 columns) from
+ * its second column, and rows 14 to 2513 of the sensor. Rows 14 to 2523 give
+ * image data; with the readout window reaching past row 2521 the frame
+ * timing changes, so the area stops there. The size of the full pixel array
+ * is not known, so this area is reported as the native size.
+ *
+ * The column start register only gives valid images at some values, so the
+ * readout window keeps the vendor columns and the horizontal crop is done by
+ * the output crop, at least 4 columns narrower than the window. The vertical
+ * crop is done by the readout window, 16 rows taller than the output and
+ * cropped at row 8 as in the vendor sequence; it shortens the frame. Even
+ * offsets in the area keep the RGGB order.
+ *
+ * The default crop rectangle is the 3264x2448 image of the vendor register
+ * sequence, at (8, 52) in the area.
+ */
+#define GC8034_AREA_WIDTH		3282
+#define GC8034_AREA_HEIGHT		2500
+#define GC8034_ROW_START_OFFSET		6
+#define GC8034_WIN_LEFT			4
+#define GC8034_WIN_WIDTH		3284
+#define GC8034_WIN_EXTRA_ROWS		16
+#define GC8034_OUT_TOP			8
+#define GC8034_OUT_LEFT_OFFSET		1
+#define GC8034_CROP_LEFT		8
+#define GC8034_CROP_TOP			52
+#define GC8034_WIDTH			3264
+#define GC8034_HEIGHT			2448
+
+/*
+ * The frame is the readout window plus 20 lines plus the blanking register.
+ * With the window 16 rows taller than the output, the frame length is the
+ * output height plus the vertical blanking, and the register holds the
+ * vertical blanking minus 36 lines. This was measured with output heights
+ * from 64 to 2448 lines. The vendor sequence uses 48 lines of blanking, which
+ * is kept as the minimum.
+ */
+#define GC8034_BLANKING_OFFSET		36
+#define GC8034_VBLANK_MIN		48
+#define GC8034_VTS_MAX			0x1fff
+
+/* The sensor only takes even exposure values. */
+#define GC8034_EXP_MIN			4
+#define GC8034_EXP_STEP			2
+#define GC8034_EXP_MARGIN		4
+#define GC8034_EXP_DEF			2246
+
+/*
+ * The register table has only been tested with a 19.2 MHz external clock. The
+ * vendor code declares 336 MHz and 30 fps for a 24 MHz clock; at 19.2 MHz
+ * everything runs at 0.8 times that: 268.8 MHz and 24 fps, as measured. The
+ * PLL settings are not documented, so the pixel rate is not computed from
+ * them: it is the one that gives the measured frame periods with the line
+ * length below, from 1.869 ms for 112 lines to 41.653 ms for 2496 lines. The
+ * line length register counts units of 8 pixels.
+ */
+#define GC8034_XCLK_FREQ		(19200 * HZ_PER_KHZ)
+#define GC8034_LINK_FREQ		(268800 * HZ_PER_KHZ)
+#define GC8034_DATA_LANES		4
+#define GC8034_HTS			4272
+#define GC8034_PIXEL_RATE		(256 * HZ_PER_MHZ)
+
+/*
+ * No datasheet is available. The power sequence and delays follow the vendor
+ * driver: supplies in the order of gc8034_supply_name, 100 us, clock, 1 ms,
+ * powerdown released, 500 us, reset released, then 6 ms and 8192 clock cycles
+ * before the first I2C access.
+ */
+#define GC8034_SUPPLY_DELAY_US		100
+#define GC8034_CLK_DELAY_US		1000
+#define GC8034_PWDN_DELAY_US		500
+#define GC8034_RESET_DELAY_US		(6000 + \
+					 DIV_ROUND_UP(8192 * USEC_PER_MSEC, \
+						      GC8034_XCLK_FREQ / \
+						      MSEC_PER_SEC))
+
+#define GC8034_MBUS_CODE		MEDIA_BUS_FMT_SRGGB10_1X10
+
+static const s64 gc8034_link_freq_menu[] = {
+	GC8034_LINK_FREQ,
+};
+
+static const char * const gc8034_supply_name[] = {
+	"dovdd",
+	"dvdd",
+	"avdd",
+};
+
+/*
+ * V4L2_CID_ANALOGUE_GAIN is the value of register 0xb6, the index of the
+ * analogue gain step. Only the seven steps the vendor code uses are exposed.
+ * Each step also needs a set of analogue bias registers to be rewritten; the
+ * sequence selects its own pages through register 0xfe. The digital gain is
+ * left at the 1.0x the register table programs.
+ */
+static const u8 gc8034_agc_bias_reg[] = {
+	0xfe, 0x20, 0x33, 0xfe, 0xdf, 0xe7, 0xe8,
+	0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xfe,
+};
+
+static const u8 gc8034_agc_bias[][ARRAY_SIZE(gc8034_agc_bias_reg)] = {
+	/* 1.000x */
+	{ 0x00, 0x55, 0x83, 0x01, 0x06, 0x18, 0x20,
+	  0x16, 0x17, 0x50, 0x6c, 0x9b, 0xd8, 0x00 },
+	/* 1.375x */
+	{ 0x00, 0x55, 0x83, 0x01, 0x06, 0x18, 0x20,
+	  0x16, 0x17, 0x50, 0x6c, 0x9b, 0xd8, 0x00 },
+	/* 1.950x */
+	{ 0x00, 0x4e, 0x84, 0x01, 0x0c, 0x2e, 0x2d,
+	  0x15, 0x19, 0x47, 0x70, 0x9f, 0xd8, 0x00 },
+	/* 2.700x */
+	{ 0x00, 0x51, 0x80, 0x01, 0x07, 0x28, 0x32,
+	  0x22, 0x20, 0x49, 0x70, 0x91, 0xd9, 0x00 },
+	/* 3.800x */
+	{ 0x00, 0x4d, 0x83, 0x01, 0x0f, 0x3b, 0x3b,
+	  0x1c, 0x1f, 0x47, 0x6f, 0x9b, 0xd3, 0x00 },
+	/* 5.400x */
+	{ 0x00, 0x50, 0x83, 0x01, 0x08, 0x35, 0x46,
+	  0x1e, 0x22, 0x4c, 0x70, 0x9a, 0xd2, 0x00 },
+	/* 7.660x */
+	{ 0x00, 0x52, 0x80, 0x01, 0x0c, 0x35, 0x3a,
+	  0x2b, 0x2d, 0x4c, 0x67, 0x8d, 0xc0, 0x00 },
+};
+
+struct gc8034 {
+	struct device *dev;
+	struct v4l2_subdev sd;
+	struct media_pad pad;
+
+	struct clk *xclk;
+	struct regulator_bulk_data supplies[ARRAY_SIZE(gc8034_supply_name)];
+	struct gpio_desc *reset_gpio;
+	struct gpio_desc *powerdown_gpio;
+
+	struct v4l2_ctrl_handler ctrls;
+	struct v4l2_ctrl *exposure;
+	struct v4l2_ctrl *hblank;
+
+	struct regmap *regmap;
+};
+
+/*
+ * The register sequence is the vendor one for the 3264x2448 mode, without the
+ * registers written by the controls and without the settings of a binned mode
+ * that it programs first and then overwrites. No register documentation is
+ * available: the names of the geometry registers are inferred from their
+ * values.
+ */
+static const struct cci_reg_sequence gc8034_regs[] = {
+	/* SYS */
+	{ CCI_REG8(0xf2), 0x00 },
+	{ CCI_REG8(0xf4), 0x80 },
+	{ CCI_REG8(0xf5), 0x19 },
+	{ CCI_REG8(0xf6), 0x44 },
+	{ CCI_REG8(0xf8), 0x63 },
+	{ CCI_REG8(0xfa), 0x45 },
+	{ CCI_REG8(0xf9), 0x00 },
+	{ CCI_REG8(0xf7), 0x9d },
+	{ CCI_REG8(0xfc), 0x00 },
+	{ CCI_REG8(0xfc), 0x00 },
+	{ CCI_REG8(0xfc), 0xea },
+	{ GC8034_REG_PAGE_SELECT, 0x03 },
+	{ CCI_REG8(0x03), 0x9a },
+	{ CCI_REG8(0x18), 0x07 },
+	{ CCI_REG8(0x01), 0x07 },
+	{ CCI_REG8(0xfc), 0xee },
+	/* Cisctl&Analog */
+	{ GC8034_REG_PAGE_SELECT, 0x00 },
+	{ GC8034_REG_LINE_LENGTH, GC8034_HTS / 8 },
+	{ GC8034_REG_MIRROR, GC8034_MIRROR_NONE },
+	{ CCI_REG8(0x18), 0x02 },
+	{ CCI_REG8(0x19), 0x17 },
+	{ CCI_REG8(0x1e), 0x50 },
+	{ CCI_REG8(0x1f), 0x80 },
+	{ CCI_REG8(0x21), 0x4c },
+	{ CCI_REG8(0x25), 0x00 },
+	{ CCI_REG8(0x28), 0x4a },
+	{ CCI_REG8(0x2d), 0x89 },
+	{ CCI_REG8(0xca), 0x02 },
+	{ CCI_REG8(0xcb), 0x00 },
+	{ CCI_REG8(0xcc), 0x39 },
+	{ CCI_REG8(0xce), 0xd0 },
+	{ CCI_REG8(0xcf), 0x93 },
+	{ CCI_REG8(0xd0), 0x19 },
+	{ CCI_REG8(0xd1), 0xaa },
+	{ CCI_REG8(0xd2), 0xcb },
+	{ CCI_REG8(0xd8), 0x40 },
+	{ CCI_REG8(0xd9), 0xff },
+	{ CCI_REG8(0xda), 0x0e },
+	{ CCI_REG8(0xdb), 0xb0 },
+	{ CCI_REG8(0xdc), 0x0e },
+	{ CCI_REG8(0xde), 0x08 },
+	{ CCI_REG8(0xe4), 0xc6 },
+	{ CCI_REG8(0xe5), 0x08 },
+	{ CCI_REG8(0xe6), 0x10 },
+	{ CCI_REG8(0xed), 0x2a },
+	{ GC8034_REG_PAGE_SELECT, 0x02 },
+	{ CCI_REG8(0x59), 0x02 },
+	{ CCI_REG8(0x5a), 0x04 },
+	{ CCI_REG8(0x5b), 0x08 },
+	{ CCI_REG8(0x5c), 0x20 },
+	{ GC8034_REG_PAGE_SELECT, 0x00 },
+	{ CCI_REG8(0x1a), 0x09 },
+	{ CCI_REG8(0x1d), 0x13 },
+	{ GC8034_REG_PAGE_SELECT, 0x10 },	/* not a page */
+	{ GC8034_REG_PAGE_SELECT, 0x00 },
+	{ GC8034_REG_PAGE_SELECT, 0x10 },	/* not a page */
+	/* ISP */
+	{ GC8034_REG_PAGE_SELECT, 0x00 },
+	{ CCI_REG8(0x84), 0x01 },
+	{ CCI_REG8(0x88), 0x03 },
+	{ CCI_REG8(0x89), 0x03 },
+	{ CCI_REG8(0x8d), 0x03 },
+	{ CCI_REG8(0x8f), 0x14 },
+	{ CCI_REG8(0x66), 0x2c },
+	{ CCI_REG8(0xbc), 0x49 },
+	{ CCI_REG8(0xc2), 0x7f },
+	{ CCI_REG8(0xc3), 0xff },
+	/* Gain */
+	{ CCI_REG8(0xb0), 0x90 },
+	{ GC8034_REG_DIGITAL_GAIN, GC8034_DIGITAL_GAIN_1X },
+	/* BLK */
+	{ GC8034_REG_PAGE_SELECT, 0x00 },
+	{ CCI_REG8(0x40), 0x22 },
+	{ CCI_REG8(0x41), 0x20 },
+	{ CCI_REG8(0x42), 0x02 },
+	{ CCI_REG8(0x43), 0x08 },
+	{ CCI_REG8(0x4e), 0x0f },
+	{ CCI_REG8(0x4f), 0xf0 },
+	{ CCI_REG8(0x58), 0x80 },
+	{ CCI_REG8(0x59), 0x80 },
+	{ CCI_REG8(0x5a), 0x80 },
+	{ CCI_REG8(0x5b), 0x80 },
+	{ CCI_REG8(0x5c), 0x00 },
+	{ CCI_REG8(0x5d), 0x00 },
+	{ CCI_REG8(0x5e), 0x00 },
+	{ CCI_REG8(0x5f), 0x00 },
+	{ CCI_REG8(0x6b), 0x01 },
+	{ CCI_REG8(0x6c), 0x00 },
+	{ CCI_REG8(0x6d), 0x0c },
+	/* WB offset */
+	{ GC8034_REG_PAGE_SELECT, 0x01 },
+	{ CCI_REG8(0xbf), 0x40 },
+	/* Dark Sun */
+	{ GC8034_REG_PAGE_SELECT, 0x01 },
+	{ CCI_REG8(0x68), 0x77 },
+	/* DPC */
+	{ GC8034_REG_PAGE_SELECT, 0x01 },
+	{ CCI_REG8(0x60), 0x00 },
+	{ CCI_REG8(0x61), 0x10 },
+	{ CCI_REG8(0x64), 0x02 },
+	/* LSC */
+	{ GC8034_REG_PAGE_SELECT, 0x01 },
+	{ CCI_REG8(0xa8), 0x60 },
+	{ CCI_REG8(0xa2), 0xd1 },
+	{ CCI_REG8(0xc8), 0x57 },
+	{ CCI_REG8(0xa1), 0xb8 },
+	{ CCI_REG8(0xa3), 0x91 },
+	{ CCI_REG8(0xc0), 0x50 },
+	{ CCI_REG8(0xd0), 0x05 },
+	{ CCI_REG8(0xd1), 0xb2 },
+	{ CCI_REG8(0xd2), 0x1f },
+	{ CCI_REG8(0xd3), 0x00 },
+	{ CCI_REG8(0xd4), 0x00 },
+	{ CCI_REG8(0xd5), 0x00 },
+	{ CCI_REG8(0xd6), 0x00 },
+	{ CCI_REG8(0xd7), 0x00 },
+	{ CCI_REG8(0xd8), 0x00 },
+	{ CCI_REG8(0xd9), 0x00 },
+	{ CCI_REG8(0xa4), 0x10 },
+	{ CCI_REG8(0xa5), 0x20 },
+	{ CCI_REG8(0xa6), 0x60 },
+	{ CCI_REG8(0xa7), 0x80 },
+	{ CCI_REG8(0xab), 0x18 },
+	{ CCI_REG8(0xc7), 0xc0 },
+	/* ABB */
+	{ GC8034_REG_PAGE_SELECT, 0x01 },
+	{ CCI_REG8(0x20), 0x02 },
+	{ CCI_REG8(0x21), 0x02 },
+	{ CCI_REG8(0x23), 0x42 },
+	{ GC8034_REG_PAGE_SELECT, 0x00 },
+	/* SYS */
+	{ CCI_REG8(0xf2), 0x00 },
+	{ CCI_REG8(0xf4), 0x80 },
+	{ CCI_REG8(0xf5), 0x19 },
+	{ CCI_REG8(0xf6), 0x44 },
+	{ CCI_REG8(0xf8), 0x63 },
+	{ CCI_REG8(0xfa), 0x45 },
+	{ CCI_REG8(0xf9), 0x00 },
+	{ CCI_REG8(0xf7), 0x95 },
+	{ CCI_REG8(0xfc), 0x00 },
+	{ CCI_REG8(0xfc), 0x00 },
+	{ CCI_REG8(0xfc), 0xea },
+	{ GC8034_REG_PAGE_SELECT, 0x03 },
+	{ CCI_REG8(0x03), 0x9a },
+	{ CCI_REG8(0x18), 0x07 },
+	{ CCI_REG8(0x01), 0x07 },
+	{ CCI_REG8(0xfc), 0xee },
+	/* ISP */
+	{ GC8034_REG_PAGE_SELECT, 0x00 },
+	{ CCI_REG8(0x80), 0x13 },
+	{ CCI_REG8(0xad), 0x00 },
+	/* Crop window, the geometry is written by gc8034_write_geometry() */
+	{ CCI_REG8(0x90), 0x01 },
+	/* DPC */
+	{ GC8034_REG_PAGE_SELECT, 0x01 },
+	{ CCI_REG8(0x62), 0x60 },
+	{ CCI_REG8(0x63), 0x48 },
+	/* MIPI */
+	{ GC8034_REG_PAGE_SELECT, 0x03 },
+	{ CCI_REG8(0x02), 0x03 },
+	{ CCI_REG8(0x04), 0x80 },
+	{ CCI_REG8(0x11), 0x2b },
+	{ CCI_REG8(0x15), 0x10 },
+	{ CCI_REG8(0x16), 0x29 },
+	{ CCI_REG8(0x17), 0xff },
+	{ CCI_REG8(0x19), 0xaa },
+	{ CCI_REG8(0x1a), 0x02 },
+	{ CCI_REG8(0x21), 0x05 },
+	{ CCI_REG8(0x22), 0x06 },
+	{ CCI_REG8(0x23), 0x2b },
+	{ CCI_REG8(0x24), 0x00 },
+	{ CCI_REG8(0x25), 0x12 },
+	{ CCI_REG8(0x26), 0x07 },
+	{ CCI_REG8(0x29), 0x07 },
+	{ CCI_REG8(0x2a), 0x12 },
+	{ CCI_REG8(0x2b), 0x07 },
+	{ GC8034_REG_PAGE_SELECT, 0x00 },
+	{ GC8034_REG_STREAM, GC8034_STREAM_OFF },
+};
+
+static inline struct gc8034 *to_gc8034(struct v4l2_subdev *sd)
+{
+	return container_of(sd, struct gc8034, sd);
+}
+
+static void gc8034_disable_supplies(struct gc8034 *gc8034, unsigned int num)
+{
+	while (num--)
+		regulator_disable(gc8034->supplies[num].consumer);
+}
+
+static int gc8034_power_on(struct device *dev)
+{
+	struct v4l2_subdev *sd = dev_get_drvdata(dev);
+	struct gc8034 *gc8034 = to_gc8034(sd);
+	unsigned int i;
+	int ret;
+
+	/* One by one: regulator_bulk_enable() works in parallel. */
+	for (i = 0; i < ARRAY_SIZE(gc8034_supply_name); i++) {
+		ret = regulator_enable(gc8034->supplies[i].consumer);
+		if (ret) {
+			dev_err(dev, "failed to enable %s: %d\n",
+				gc8034_supply_name[i], ret);
+			gc8034_disable_supplies(gc8034, i);
+			return ret;
+		}
+	}
+
+	fsleep(GC8034_SUPPLY_DELAY_US);
+
+	ret = clk_prepare_enable(gc8034->xclk);
+	if (ret) {
+		dev_err(dev, "failed to enable clock: %d\n", ret);
+		gc8034_disable_supplies(gc8034, ARRAY_SIZE(gc8034_supply_name));
+		return ret;
+	}
+
+	fsleep(GC8034_CLK_DELAY_US);
+	gpiod_set_value_cansleep(gc8034->powerdown_gpio, 0);
+	fsleep(GC8034_PWDN_DELAY_US);
+	gpiod_set_value_cansleep(gc8034->reset_gpio, 0);
+	fsleep(GC8034_RESET_DELAY_US);
+
+	return 0;
+}
+
+static int gc8034_power_off(struct device *dev)
+{
+	struct v4l2_subdev *sd = dev_get_drvdata(dev);
+	struct gc8034 *gc8034 = to_gc8034(sd);
+
+	gpiod_set_value_cansleep(gc8034->powerdown_gpio, 1);
+	gpiod_set_value_cansleep(gc8034->reset_gpio, 1);
+	clk_disable_unprepare(gc8034->xclk);
+	gc8034_disable_supplies(gc8034, ARRAY_SIZE(gc8034_supply_name));
+
+	return 0;
+}
+
+static int gc8034_enum_mbus_code(struct v4l2_subdev *sd,
+				 struct v4l2_subdev_state *sd_state,
+				 struct v4l2_subdev_mbus_code_enum *code)
+{
+	if (code->index > 0)
+		return -EINVAL;
+
+	code->code = GC8034_MBUS_CODE;
+
+	return 0;
+}
+
+static int gc8034_enum_frame_size(struct v4l2_subdev *sd,
+				  struct v4l2_subdev_state *sd_state,
+				  struct v4l2_subdev_frame_size_enum *fse)
+{
+	if (fse->code != GC8034_MBUS_CODE)
+		return -EINVAL;
+
+	if (fse->index > 0)
+		return -EINVAL;
+
+	fse->min_width = GC8034_WIDTH;
+	fse->max_width = GC8034_WIDTH;
+	fse->min_height = GC8034_HEIGHT;
+	fse->max_height = GC8034_HEIGHT;
+
+	return 0;
+}
+
+static void gc8034_fill_format(struct v4l2_mbus_framefmt *fmt,
+			       const struct v4l2_rect *crop)
+{
+	fmt->width = crop->width;
+	fmt->height = crop->height;
+	fmt->code = GC8034_MBUS_CODE;
+	fmt->field = V4L2_FIELD_NONE;
+	fmt->colorspace = V4L2_COLORSPACE_RAW;
+	fmt->ycbcr_enc = V4L2_MAP_YCBCR_ENC_DEFAULT(fmt->colorspace);
+	fmt->quantization = V4L2_QUANTIZATION_FULL_RANGE;
+	fmt->xfer_func = V4L2_XFER_FUNC_NONE;
+}
+
+static int gc8034_get_selection(struct v4l2_subdev *sd,
+				const struct v4l2_subdev_client_info *ci,
+				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, 0);
+		break;
+	case V4L2_SEL_TGT_CROP_DEFAULT:
+		sel->r.top = GC8034_CROP_TOP;
+		sel->r.left = GC8034_CROP_LEFT;
+		sel->r.width = GC8034_WIDTH;
+		sel->r.height = GC8034_HEIGHT;
+		break;
+	case V4L2_SEL_TGT_CROP_BOUNDS:
+	case V4L2_SEL_TGT_NATIVE_SIZE:
+		sel->r.top = 0;
+		sel->r.left = 0;
+		sel->r.width = GC8034_AREA_WIDTH;
+		sel->r.height = GC8034_AREA_HEIGHT;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+static int gc8034_init_state(struct v4l2_subdev *sd,
+			     struct v4l2_subdev_state *state)
+{
+	struct v4l2_rect *crop = v4l2_subdev_state_get_crop(state, 0);
+
+	crop->left = GC8034_CROP_LEFT;
+	crop->top = GC8034_CROP_TOP;
+	crop->width = GC8034_WIDTH;
+	crop->height = GC8034_HEIGHT;
+
+	gc8034_fill_format(v4l2_subdev_state_get_format(state, 0), crop);
+
+	return 0;
+}
+
+static int gc8034_get_frame_desc(struct v4l2_subdev *sd, unsigned int pad,
+				 struct v4l2_mbus_frame_desc *fd)
+{
+	fd->type = V4L2_MBUS_FRAME_DESC_TYPE_CSI2;
+	fd->num_entries = 1;
+	fd->entry[0].pixelcode = GC8034_MBUS_CODE;
+	fd->entry[0].stream = 0;
+	fd->entry[0].bus.csi2.vc = 0;
+	fd->entry[0].bus.csi2.dt = MIPI_CSI2_DT_RAW10;
+
+	return 0;
+}
+
+static int gc8034_set_analogue_gain(struct gc8034 *gc8034, u32 idx)
+{
+	unsigned int i;
+	int ret = 0;
+
+	cci_write(gc8034->regmap, GC8034_REG_ANALOGUE_GAIN, idx, &ret);
+
+	for (i = 0; i < ARRAY_SIZE(gc8034_agc_bias_reg); i++)
+		cci_write(gc8034->regmap, CCI_REG8(gc8034_agc_bias_reg[i]),
+			  gc8034_agc_bias[idx][i], &ret);
+
+	return ret;
+}
+
+static int gc8034_set_ctrl(struct v4l2_ctrl *ctrl)
+{
+	struct gc8034 *gc8034 =
+		container_of(ctrl->handler, struct gc8034, ctrls);
+	const struct v4l2_mbus_framefmt *format;
+	struct v4l2_subdev_state *state;
+	s64 exposure_max;
+	int ret;
+
+	state = v4l2_subdev_get_locked_active_state(&gc8034->sd);
+	format = v4l2_subdev_state_get_format(state, 0);
+
+	if (ctrl->id == V4L2_CID_VBLANK) {
+		exposure_max = round_down(format->height + ctrl->val -
+					  GC8034_EXP_MARGIN, GC8034_EXP_STEP);
+		ret = __v4l2_ctrl_modify_range(gc8034->exposure,
+					       GC8034_EXP_MIN, exposure_max,
+					       GC8034_EXP_STEP,
+					       min_t(s64, GC8034_EXP_DEF,
+						     exposure_max));
+		if (ret)
+			return ret;
+	}
+
+	if (!pm_runtime_get_if_active(gc8034->dev))
+		return 0;
+
+	ret = cci_write(gc8034->regmap, GC8034_REG_PAGE_SELECT, GC8034_PAGE_0,
+			NULL);
+	if (ret)
+		goto out;
+
+	switch (ctrl->id) {
+	case V4L2_CID_EXPOSURE:
+		ret = cci_write(gc8034->regmap, GC8034_REG_EXPOSURE,
+				ctrl->val, NULL);
+		break;
+	case V4L2_CID_ANALOGUE_GAIN:
+		ret = gc8034_set_analogue_gain(gc8034, ctrl->val);
+		break;
+	case V4L2_CID_VBLANK:
+		/* The register holds an offset, not the VTS. */
+		ret = cci_write(gc8034->regmap, GC8034_REG_BLANKING,
+				ctrl->val - GC8034_BLANKING_OFFSET, NULL);
+		break;
+	default:
+		ret = -EINVAL;
+		break;
+	}
+
+out:
+	pm_runtime_put_autosuspend(gc8034->dev);
+
+	return ret;
+}
+
+static const struct v4l2_ctrl_ops gc8034_ctrl_ops = {
+	.s_ctrl = gc8034_set_ctrl,
+};
+
+/* Only ever called from probe, with the sensor powered up. */
+static int gc8034_identify_module(struct gc8034 *gc8034)
+{
+	u64 val;
+	int ret;
+
+	ret = cci_read(gc8034->regmap, GC8034_REG_CHIP_ID, &val, NULL);
+	if (ret)
+		return ret;
+
+	if (val != GC8034_CHIP_ID)
+		return dev_err_probe(gc8034->dev, -ENXIO,
+				     "chip id mismatch: %x != %llx\n",
+				     GC8034_CHIP_ID, val);
+
+	return 0;
+}
+
+static int gc8034_write_geometry(struct gc8034 *gc8034,
+				 const struct v4l2_rect *crop)
+{
+	int ret = 0;
+
+	cci_write(gc8034->regmap, GC8034_REG_PAGE_SELECT, GC8034_PAGE_0, &ret);
+
+	/* Readout window: vendor columns, the rows of the crop rectangle. */
+	cci_write(gc8034->regmap, GC8034_REG_WIN_TOP,
+		  crop->top + GC8034_ROW_START_OFFSET, &ret);
+	cci_write(gc8034->regmap, GC8034_REG_WIN_LEFT, GC8034_WIN_LEFT, &ret);
+	cci_write(gc8034->regmap, GC8034_REG_WIN_HEIGHT,
+		  crop->height + GC8034_WIN_EXTRA_ROWS, &ret);
+	cci_write(gc8034->regmap, GC8034_REG_WIN_WIDTH, GC8034_WIN_WIDTH, &ret);
+
+	/* Output crop: the columns of the crop rectangle. */
+	cci_write(gc8034->regmap, GC8034_REG_CROP_TOP, GC8034_OUT_TOP, &ret);
+	cci_write(gc8034->regmap, GC8034_REG_CROP_LEFT,
+		  crop->left + GC8034_OUT_LEFT_OFFSET, &ret);
+	cci_write(gc8034->regmap, GC8034_REG_OUT_HEIGHT, crop->height, &ret);
+	cci_write(gc8034->regmap, GC8034_REG_OUT_WIDTH, crop->width, &ret);
+
+	/* RAW10: 5 bytes per 4 pixels. */
+	cci_write(gc8034->regmap, GC8034_REG_PAGE_SELECT, GC8034_PAGE_3, &ret);
+	cci_write(gc8034->regmap, GC8034_REG_LINE_BYTES, crop->width * 5 / 4,
+		  &ret);
+	cci_write(gc8034->regmap, GC8034_REG_PAGE_SELECT, GC8034_PAGE_0, &ret);
+
+	return ret;
+}
+
+static int gc8034_enable_streams(struct v4l2_subdev *sd,
+				 struct v4l2_subdev_state *state,
+				 u32 pad, u64 streams_mask)
+{
+	struct gc8034 *gc8034 = to_gc8034(sd);
+	int ret;
+
+	ret = pm_runtime_resume_and_get(gc8034->dev);
+	if (ret < 0)
+		return ret;
+
+	ret = cci_multi_reg_write(gc8034->regmap, gc8034_regs,
+				  ARRAY_SIZE(gc8034_regs), NULL);
+	if (ret)
+		goto err_rpm_put;
+
+	ret = gc8034_write_geometry(gc8034,
+				    v4l2_subdev_state_get_crop(state, 0));
+	if (ret)
+		goto err_rpm_put;
+
+	ret = __v4l2_ctrl_handler_setup(&gc8034->ctrls);
+	if (ret)
+		goto err_rpm_put;
+
+	ret = cci_write(gc8034->regmap, GC8034_REG_STREAM, GC8034_STREAM_ON,
+			NULL);
+	if (ret)
+		goto err_rpm_put;
+
+	return 0;
+
+err_rpm_put:
+	pm_runtime_put_autosuspend(gc8034->dev);
+
+	return ret;
+}
+
+static int gc8034_disable_streams(struct v4l2_subdev *sd,
+				  struct v4l2_subdev_state *state,
+				  u32 pad, u64 streams_mask)
+{
+	struct gc8034 *gc8034 = to_gc8034(sd);
+	int ret = 0;
+
+	/* cci_write() logs errors, nothing else can be done about them. */
+	cci_write(gc8034->regmap, GC8034_REG_PAGE_SELECT, GC8034_PAGE_0, &ret);
+	cci_write(gc8034->regmap, GC8034_REG_STREAM, GC8034_STREAM_OFF, &ret);
+
+	pm_runtime_put_autosuspend(gc8034->dev);
+
+	return 0;
+}
+
+static const struct v4l2_subdev_video_ops gc8034_video_ops = {
+	.s_stream = v4l2_subdev_s_stream_helper,
+};
+
+static const struct v4l2_subdev_pad_ops gc8034_pad_ops = {
+	.enum_mbus_code = gc8034_enum_mbus_code,
+	.enum_frame_size = gc8034_enum_frame_size,
+	.get_fmt = v4l2_subdev_get_fmt,
+	.get_selection = gc8034_get_selection,
+	.get_frame_desc = gc8034_get_frame_desc,
+	.enable_streams = gc8034_enable_streams,
+	.disable_streams = gc8034_disable_streams,
+};
+
+static const struct v4l2_subdev_ops gc8034_subdev_ops = {
+	.video = &gc8034_video_ops,
+	.pad = &gc8034_pad_ops,
+};
+
+static const struct v4l2_subdev_internal_ops gc8034_internal_ops = {
+	.init_state = gc8034_init_state,
+};
+
+static int gc8034_parse_fwnode(struct gc8034 *gc8034)
+{
+	struct v4l2_fwnode_endpoint bus_cfg = {
+		.bus_type = V4L2_MBUS_CSI2_DPHY,
+	};
+	struct device *dev = gc8034->dev;
+	struct fwnode_handle *endpoint;
+	unsigned long link_freq_bitmap;
+	int ret;
+
+	endpoint = fwnode_graph_get_endpoint_by_id(dev_fwnode(dev), 0, 0,
+						   FWNODE_GRAPH_ENDPOINT_NEXT);
+	ret = v4l2_fwnode_endpoint_alloc_parse(endpoint, &bus_cfg);
+	fwnode_handle_put(endpoint);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to parse endpoint\n");
+
+	/* The register table configures the CSI-2 transmitter for 4 lanes. */
+	if (bus_cfg.bus.mipi_csi2.num_data_lanes != GC8034_DATA_LANES) {
+		ret = dev_err_probe(dev, -EINVAL,
+				    "unsupported number of data lanes %u\n",
+				    bus_cfg.bus.mipi_csi2.num_data_lanes);
+		goto done;
+	}
+
+	ret = v4l2_link_freq_to_bitmap(dev, bus_cfg.link_frequencies,
+				       bus_cfg.nr_of_link_frequencies,
+				       gc8034_link_freq_menu,
+				       ARRAY_SIZE(gc8034_link_freq_menu),
+				       &link_freq_bitmap);
+
+done:
+	v4l2_fwnode_endpoint_free(&bus_cfg);
+
+	return ret;
+}
+
+static int gc8034_init_controls(struct gc8034 *gc8034)
+{
+	struct v4l2_fwnode_device_properties props;
+	struct v4l2_ctrl_handler *ctrl_hdlr;
+	struct v4l2_ctrl *link_freq;
+	s64 exposure_max;
+	int ret;
+
+	ret = v4l2_fwnode_device_parse(gc8034->dev, &props);
+	if (ret)
+		return ret;
+
+	ctrl_hdlr = &gc8034->ctrls;
+	v4l2_ctrl_handler_init(ctrl_hdlr, 8);
+
+	link_freq = v4l2_ctrl_new_int_menu(ctrl_hdlr, NULL, V4L2_CID_LINK_FREQ,
+					   ARRAY_SIZE(gc8034_link_freq_menu) -
+					   1, 0, gc8034_link_freq_menu);
+
+	v4l2_ctrl_new_std(ctrl_hdlr, NULL, V4L2_CID_PIXEL_RATE,
+			  GC8034_PIXEL_RATE, GC8034_PIXEL_RATE, 1,
+			  GC8034_PIXEL_RATE);
+
+	v4l2_ctrl_new_std(ctrl_hdlr, &gc8034_ctrl_ops, V4L2_CID_VBLANK,
+			  GC8034_VBLANK_MIN, GC8034_VTS_MAX - GC8034_HEIGHT, 1,
+			  GC8034_VBLANK_MIN);
+
+	gc8034->hblank = v4l2_ctrl_new_std(ctrl_hdlr, NULL, V4L2_CID_HBLANK,
+					   GC8034_HTS - GC8034_WIDTH,
+					   GC8034_HTS - GC8034_WIDTH, 1,
+					   GC8034_HTS - GC8034_WIDTH);
+
+	exposure_max = round_down(GC8034_HEIGHT + GC8034_VBLANK_MIN -
+				  GC8034_EXP_MARGIN, GC8034_EXP_STEP);
+	gc8034->exposure = v4l2_ctrl_new_std(ctrl_hdlr, &gc8034_ctrl_ops,
+					     V4L2_CID_EXPOSURE, GC8034_EXP_MIN,
+					     exposure_max, GC8034_EXP_STEP,
+					     GC8034_EXP_DEF);
+
+	v4l2_ctrl_new_std(ctrl_hdlr, &gc8034_ctrl_ops, V4L2_CID_ANALOGUE_GAIN,
+			  0, ARRAY_SIZE(gc8034_agc_bias) - 1, 1, 0);
+
+	v4l2_ctrl_new_fwnode_properties(ctrl_hdlr, &gc8034_ctrl_ops, &props);
+
+	if (ctrl_hdlr->error)
+		return v4l2_ctrl_handler_free(ctrl_hdlr);
+
+	link_freq->flags |= V4L2_CTRL_FLAG_READ_ONLY;
+	gc8034->hblank->flags |= V4L2_CTRL_FLAG_READ_ONLY;
+
+	gc8034->sd.ctrl_handler = ctrl_hdlr;
+
+	return 0;
+}
+
+static int gc8034_probe(struct i2c_client *client)
+{
+	struct device *dev = &client->dev;
+	struct gc8034 *gc8034;
+	unsigned long freq;
+	int ret;
+
+	gc8034 = devm_kzalloc(dev, sizeof(*gc8034), GFP_KERNEL);
+	if (!gc8034)
+		return -ENOMEM;
+
+	gc8034->dev = dev;
+
+	v4l2_i2c_subdev_init(&gc8034->sd, client, &gc8034_subdev_ops);
+	gc8034->sd.internal_ops = &gc8034_internal_ops;
+
+	ret = gc8034_parse_fwnode(gc8034);
+	if (ret)
+		return ret;
+
+	gc8034->xclk = devm_v4l2_sensor_clk_get(dev, NULL);
+	if (IS_ERR(gc8034->xclk))
+		return dev_err_probe(dev, PTR_ERR(gc8034->xclk),
+				     "failed to get clock\n");
+
+	freq = clk_get_rate(gc8034->xclk);
+	if (freq != GC8034_XCLK_FREQ)
+		return dev_err_probe(dev, -EINVAL,
+				     "external clock %lu Hz not supported\n",
+				     freq);
+
+	gc8034->regmap = devm_cci_regmap_init_i2c(client, 8);
+	if (IS_ERR(gc8034->regmap))
+		return dev_err_probe(dev, PTR_ERR(gc8034->regmap),
+				     "failed to init CCI\n");
+
+	gc8034->reset_gpio = devm_gpiod_get_optional(dev, "reset",
+						     GPIOD_OUT_HIGH);
+	if (IS_ERR(gc8034->reset_gpio))
+		return dev_err_probe(dev, PTR_ERR(gc8034->reset_gpio),
+				     "failed to get reset GPIO\n");
+
+	gc8034->powerdown_gpio = devm_gpiod_get_optional(dev, "powerdown",
+							 GPIOD_OUT_HIGH);
+	if (IS_ERR(gc8034->powerdown_gpio))
+		return dev_err_probe(dev, PTR_ERR(gc8034->powerdown_gpio),
+				     "failed to get powerdown GPIO\n");
+
+	for (unsigned int i = 0; i < ARRAY_SIZE(gc8034_supply_name); i++)
+		gc8034->supplies[i].supply = gc8034_supply_name[i];
+
+	ret = devm_regulator_bulk_get(dev, ARRAY_SIZE(gc8034_supply_name),
+				      gc8034->supplies);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to get regulators\n");
+
+	ret = gc8034_power_on(dev);
+	if (ret)
+		return ret;
+
+	ret = gc8034_identify_module(gc8034);
+	if (ret)
+		goto err_power_off;
+
+	ret = gc8034_init_controls(gc8034);
+	if (ret) {
+		dev_err_probe(dev, ret, "failed to init controls\n");
+		goto err_power_off;
+	}
+
+	gc8034->sd.flags |= V4L2_SUBDEV_FL_HAS_DEVNODE;
+	gc8034->pad.flags = MEDIA_PAD_FL_SOURCE;
+	gc8034->sd.entity.function = MEDIA_ENT_F_CAM_SENSOR;
+
+	ret = media_entity_pads_init(&gc8034->sd.entity, 1, &gc8034->pad);
+	if (ret < 0) {
+		dev_err_probe(dev, ret, "failed to init media entity\n");
+		goto err_ctrl_handler_free;
+	}
+
+	gc8034->sd.state_lock = gc8034->ctrls.lock;
+	ret = v4l2_subdev_init_finalize(&gc8034->sd);
+	if (ret < 0) {
+		dev_err_probe(dev, ret, "failed to finalize subdev\n");
+		goto err_media_entity_cleanup;
+	}
+
+	pm_runtime_set_active(dev);
+	pm_runtime_enable(dev);
+	pm_runtime_set_autosuspend_delay(dev, 1000);
+	pm_runtime_use_autosuspend(dev);
+
+	ret = v4l2_async_register_subdev_sensor(&gc8034->sd);
+	if (ret < 0) {
+		dev_err_probe(dev, ret, "failed to register subdev\n");
+		goto err_rpm;
+	}
+
+	pm_runtime_idle(dev);
+
+	return 0;
+
+err_rpm:
+	pm_runtime_disable(dev);
+	pm_runtime_dont_use_autosuspend(dev);
+	pm_runtime_set_suspended(dev);
+	v4l2_subdev_cleanup(&gc8034->sd);
+
+err_media_entity_cleanup:
+	media_entity_cleanup(&gc8034->sd.entity);
+
+err_ctrl_handler_free:
+	v4l2_ctrl_handler_free(&gc8034->ctrls);
+
+err_power_off:
+	gc8034_power_off(dev);
+
+	return ret;
+}
+
+static void gc8034_remove(struct i2c_client *client)
+{
+	struct v4l2_subdev *sd = i2c_get_clientdata(client);
+	struct gc8034 *gc8034 = to_gc8034(sd);
+
+	v4l2_async_unregister_subdev(sd);
+	v4l2_subdev_cleanup(sd);
+	media_entity_cleanup(&sd->entity);
+	v4l2_ctrl_handler_free(&gc8034->ctrls);
+
+	pm_runtime_disable(&client->dev);
+	if (!pm_runtime_status_suspended(&client->dev)) {
+		gc8034_power_off(&client->dev);
+		pm_runtime_set_suspended(&client->dev);
+	}
+	pm_runtime_dont_use_autosuspend(&client->dev);
+}
+
+static const struct dev_pm_ops gc8034_pm_ops = {
+	RUNTIME_PM_OPS(gc8034_power_off, gc8034_power_on, NULL)
+};
+
+static const struct acpi_device_id gc8034_acpi_ids[] = {
+	{ .id = "GCTI8034" },
+	{ }
+};
+MODULE_DEVICE_TABLE(acpi, gc8034_acpi_ids);
+
+static struct i2c_driver gc8034_i2c_driver = {
+	.driver = {
+		.name = "gc8034",
+		.acpi_match_table = gc8034_acpi_ids,
+		.pm = pm_ptr(&gc8034_pm_ops),
+	},
+	.probe = gc8034_probe,
+	.remove = gc8034_remove,
+};
+module_i2c_driver(gc8034_i2c_driver);
+
+MODULE_AUTHOR("Nicola Fiorillo <nicfio@gmail.com>");
+MODULE_DESCRIPTION("GalaxyCore GC8034 sensor driver");
+MODULE_LICENSE("GPL");
-- 
2.47.3


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

* [PATCH v2 3/7] media: ipu-bridge: Add GalaxyCore GC5035 and GC8034
  2026-10-12 14:34 ` [PATCH v2 0/7] media: Add GalaxyCore GC5035 and GC8034 sensor drivers Nicola Fiorillo
  2026-10-12 14:34   ` [PATCH v2 1/7] media: i2c: Add GC5035 image sensor driver Nicola Fiorillo
  2026-10-12 14:34   ` [PATCH v2 2/7] media: i2c: Add GC8034 " Nicola Fiorillo
@ 2026-10-12 14:34   ` Nicola Fiorillo
  2026-10-12 14:34   ` [PATCH v2 4/7] media: i2c: gc5035: Add crop support Nicola Fiorillo
                     ` (3 subsequent siblings)
  6 siblings, 0 replies; 15+ messages in thread
From: Nicola Fiorillo @ 2026-10-12 14:34 UTC (permalink / raw)
  To: Laurent Pinchart, Sakari Ailus
  Cc: Mauro Carvalho Chehab, Hans Verkuil, Dan Scally, Hans de Goede,
	Uwe Kleine-König, linux-media, linux-kernel

Add the GC5035 and GC8034 ACPI HIDs, with the link frequencies the two
sensors use with a 19.2 MHz external clock. Both are found on the CHUWI
Hi10 X1.

Reviewed-by: Daniel Scally <dan.scally@ideasonboard.com>
Assisted-by: claude-opus-5-5
Signed-off-by: Nicola Fiorillo <nicfio@gmail.com>
---
 drivers/media/pci/intel/ipu-bridge.c | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/drivers/media/pci/intel/ipu-bridge.c b/drivers/media/pci/intel/ipu-bridge.c
index 3739c4a2a..851096d7d 100644
--- a/drivers/media/pci/intel/ipu-bridge.c
+++ b/drivers/media/pci/intel/ipu-bridge.c
@@ -59,6 +59,10 @@ static const u16 ipu6_ov5693_ncont_clk[] = {
 };
 
 static const struct ipu_sensor_config ipu_supported_sensors[] = {
+	/* GalaxyCore GC5035 */
+	IPU_SENSOR_CONFIG("GCTI5035", 1, 422400000),
+	/* GalaxyCore GC8034 */
+	IPU_SENSOR_CONFIG("GCTI8034", 1, 268800000),
 	/* Himax HM1092 */
 	IPU_SENSOR_CONFIG("HIMX1092", 2, 180000000, 180480000),
 	/* Himax HM11B1 */
-- 
2.47.3


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

* [PATCH v2 4/7] media: i2c: gc5035: Add crop support
  2026-10-12 14:34 ` [PATCH v2 0/7] media: Add GalaxyCore GC5035 and GC8034 sensor drivers Nicola Fiorillo
                     ` (2 preceding siblings ...)
  2026-10-12 14:34   ` [PATCH v2 3/7] media: ipu-bridge: Add GalaxyCore GC5035 and GC8034 Nicola Fiorillo
@ 2026-10-12 14:34   ` Nicola Fiorillo
  2026-10-12 14:34   ` [PATCH v2 5/7] media: i2c: gc8034: " Nicola Fiorillo
                     ` (2 subsequent siblings)
  6 siblings, 0 replies; 15+ messages in thread
From: Nicola Fiorillo @ 2026-10-12 14:34 UTC (permalink / raw)
  To: Laurent Pinchart, Sakari Ailus
  Cc: Mauro Carvalho Chehab, Hans Verkuil, Dan Scally, Hans de Goede,
	Uwe Kleine-König, linux-media, linux-kernel

Implement the crop selection on the source pad. The vertical crop is done
by the readout window, which shortens the frame: 640x480 runs at up to
106 fps with the minimum vertical blanking. The horizontal crop is done by
the sensor output crop, as a narrower readout window gives black images
and the column start register only moves in steps of four columns, with
even values giving corrupted images. Crop offsets are even, to keep the
GRBG order, and sizes are multiples of 4, from 64x64 to 2608x1964.

Without binning or scaling, the format size is the crop size. As in
ov01a10, set_fmt centres the crop rectangle on CROP_DEFAULT for userspace
that does not set the crop first, and setting the format or the crop
rectangle resets the vertical blanking to its default. The crop and the
format can't be changed while streaming.

Tested with v4l2-compliance (54/54, no warning) and with crop rectangles
at the bounds, in the corners, at the minimum size and in the centre: the
position was checked to the pixel and the frame period is the computed
one.

Assisted-by: claude-opus-5-5 coccinelle sparse smatch
Signed-off-by: Nicola Fiorillo <nicfio@gmail.com>
---
 drivers/media/i2c/gc5035.c | 167 ++++++++++++++++++++++++++++++++++---
 1 file changed, 157 insertions(+), 10 deletions(-)

diff --git a/drivers/media/i2c/gc5035.c b/drivers/media/i2c/gc5035.c
index 87c3742f1..b7190aa52 100644
--- a/drivers/media/i2c/gc5035.c
+++ b/drivers/media/i2c/gc5035.c
@@ -11,6 +11,7 @@
  * Liang Wang <liang1.wang@intel.com>, derived from the ChromeOS series by
  * Tomasz Figa <tfiga@chromium.org>.
  */
+#include <linux/align.h>
 #include <linux/array_size.h>
 #include <linux/clk.h>
 #include <linux/container_of.h>
@@ -20,6 +21,7 @@
 #include <linux/gpio/consumer.h>
 #include <linux/i2c.h>
 #include <linux/math.h>
+#include <linux/minmax.h>
 #include <linux/module.h>
 #include <linux/pm_runtime.h>
 #include <linux/property.h>
@@ -106,6 +108,8 @@
 #define GC5035_CROP_TOP			8
 #define GC5035_WIDTH			2592
 #define GC5035_HEIGHT			1944
+#define GC5035_MIN_WIDTH		64
+#define GC5035_MIN_HEIGHT		64
 
 /*
  * The line length register counts units of 4 pixels. The frame length is the
@@ -207,6 +211,7 @@ struct gc5035 {
 
 	struct v4l2_ctrl_handler ctrls;
 	struct v4l2_ctrl *exposure;
+	struct v4l2_ctrl *vblank;
 	struct v4l2_ctrl *hblank;
 
 	struct regmap *regmap;
@@ -463,14 +468,58 @@ static int gc5035_enum_frame_size(struct v4l2_subdev *sd,
 	if (fse->index > 0)
 		return -EINVAL;
 
-	fse->min_width = GC5035_WIDTH;
-	fse->max_width = GC5035_WIDTH;
-	fse->min_height = GC5035_HEIGHT;
-	fse->max_height = GC5035_HEIGHT;
+	/* The output size is the size of the crop rectangle. */
+	fse->min_width = GC5035_MIN_WIDTH;
+	fse->max_width = GC5035_AREA_WIDTH;
+	fse->min_height = GC5035_MIN_HEIGHT;
+	fse->max_height = GC5035_AREA_HEIGHT;
 
 	return 0;
 }
 
+/*
+ * Update the controls that depend on the output size. Called with the state
+ * lock held, which is also the control handler lock.
+ */
+static int gc5035_update_ctrls(struct gc5035 *gc5035,
+			       const struct v4l2_mbus_framefmt *format)
+{
+	s64 vblank_max, exposure_max;
+	int ret;
+
+	ret = __v4l2_ctrl_modify_range(gc5035->hblank,
+				       GC5035_HTS - format->width,
+				       GC5035_HTS - format->width, 1,
+				       GC5035_HTS - format->width);
+	if (ret)
+		return ret;
+
+	vblank_max = GC5035_VBLANK_MIN +
+		     round_down(GC5035_VTS_MAX - format->height -
+				GC5035_VBLANK_MIN, GC5035_VBLANK_STEP);
+	/*
+	 * As in ov01a10, setting the format or the crop rectangle resets the
+	 * vertical blanking.
+	 */
+	ret = __v4l2_ctrl_modify_range(gc5035->vblank, GC5035_VBLANK_MIN,
+				       vblank_max, GC5035_VBLANK_STEP,
+				       GC5035_VBLANK_MIN);
+	if (ret)
+		return ret;
+
+	ret = __v4l2_ctrl_s_ctrl(gc5035->vblank, GC5035_VBLANK_MIN);
+	if (ret)
+		return ret;
+
+	exposure_max = format->height + gc5035->vblank->val -
+		       GC5035_EXP_MARGIN;
+
+	return __v4l2_ctrl_modify_range(gc5035->exposure, GC5035_EXP_MIN,
+					exposure_max, GC5035_EXP_STEP,
+					min_t(s64, GC5035_EXP_DEF,
+					      exposure_max));
+}
+
 static void gc5035_fill_format(struct v4l2_mbus_framefmt *fmt,
 			       const struct v4l2_rect *crop)
 {
@@ -513,6 +562,98 @@ static int gc5035_get_selection(struct v4l2_subdev *sd,
 	return 0;
 }
 
+static int gc5035_set_selection(struct v4l2_subdev *sd,
+				const struct v4l2_subdev_client_info *ci,
+				struct v4l2_subdev_state *state,
+				struct v4l2_subdev_selection *sel)
+{
+	struct gc5035 *gc5035 = to_gc5035(sd);
+	struct v4l2_mbus_framefmt *format;
+	struct v4l2_rect rect;
+
+	if (sel->target != V4L2_SEL_TGT_CROP)
+		return -EINVAL;
+
+	if (sel->which == V4L2_SUBDEV_FORMAT_ACTIVE &&
+	    v4l2_subdev_is_streaming(sd))
+		return -EBUSY;
+
+	/*
+	 * Even offsets keep the GRBG order. The sizes are multiples of 4: the
+	 * sensor rounds the frame length to a multiple of 4 lines.
+	 */
+	rect.left = ALIGN_DOWN(clamp(sel->r.left, 0,
+				     GC5035_AREA_WIDTH - GC5035_MIN_WIDTH), 2);
+	rect.top = ALIGN_DOWN(clamp(sel->r.top, 0,
+				    GC5035_AREA_HEIGHT - GC5035_MIN_HEIGHT), 2);
+	rect.width = ALIGN_DOWN(clamp_t(u32, sel->r.width, GC5035_MIN_WIDTH,
+					GC5035_AREA_WIDTH - rect.left), 4);
+	rect.height = ALIGN_DOWN(clamp_t(u32, sel->r.height, GC5035_MIN_HEIGHT,
+					 GC5035_AREA_HEIGHT - rect.top), 4);
+
+	*v4l2_subdev_state_get_crop(state, 0) = rect;
+	sel->r = rect;
+
+	/* No binning or scaling: the output size is the crop size. */
+	format = v4l2_subdev_state_get_format(state, 0);
+	gc5035_fill_format(format, &rect);
+
+	if (sel->which == V4L2_SUBDEV_FORMAT_ACTIVE)
+		return gc5035_update_ctrls(gc5035, format);
+
+	return 0;
+}
+
+/*
+ * No binning or scaling: the output size is the size of the crop rectangle. A
+ * new width or height centres the crop rectangle on the default crop
+ * rectangle along that axis, within the area, for userspace that does not set
+ * the crop rectangle first. The default size gives the default rectangle back.
+ */
+static int gc5035_set_fmt(struct v4l2_subdev *sd,
+			  const struct v4l2_subdev_client_info *ci,
+			  struct v4l2_subdev_state *state,
+			  struct v4l2_subdev_format *fmt)
+{
+	struct gc5035 *gc5035 = to_gc5035(sd);
+	struct v4l2_mbus_framefmt *format;
+	struct v4l2_rect *crop;
+	u32 width, height;
+	int pos;
+
+	if (fmt->which == V4L2_SUBDEV_FORMAT_ACTIVE &&
+	    v4l2_subdev_is_streaming(sd))
+		return -EBUSY;
+
+	width = ALIGN_DOWN(clamp_t(u32, fmt->format.width, GC5035_MIN_WIDTH,
+				   GC5035_AREA_WIDTH), 4);
+	height = ALIGN_DOWN(clamp_t(u32, fmt->format.height, GC5035_MIN_HEIGHT,
+				    GC5035_AREA_HEIGHT), 4);
+
+	crop = v4l2_subdev_state_get_crop(state, 0);
+	if (width != crop->width) {
+		pos = GC5035_CROP_LEFT + (GC5035_WIDTH - (int)width) / 2;
+		pos = clamp(pos, 0, GC5035_AREA_WIDTH - (int)width);
+		crop->left = ALIGN_DOWN(pos, 2);
+		crop->width = width;
+	}
+	if (height != crop->height) {
+		pos = GC5035_CROP_TOP + (GC5035_HEIGHT - (int)height) / 2;
+		pos = clamp(pos, 0, GC5035_AREA_HEIGHT - (int)height);
+		crop->top = ALIGN_DOWN(pos, 2);
+		crop->height = height;
+	}
+
+	format = v4l2_subdev_state_get_format(state, 0);
+	gc5035_fill_format(format, crop);
+	fmt->format = *format;
+
+	if (fmt->which == V4L2_SUBDEV_FORMAT_ACTIVE)
+		return gc5035_update_ctrls(gc5035, format);
+
+	return 0;
+}
+
 static int gc5035_init_state(struct v4l2_subdev *sd,
 			     struct v4l2_subdev_state *state)
 {
@@ -724,7 +865,9 @@ static const struct v4l2_subdev_pad_ops gc5035_pad_ops = {
 	.enum_mbus_code = gc5035_enum_mbus_code,
 	.enum_frame_size = gc5035_enum_frame_size,
 	.get_fmt = v4l2_subdev_get_fmt,
+	.set_fmt = gc5035_set_fmt,
 	.get_selection = gc5035_get_selection,
+	.set_selection = gc5035_set_selection,
 	.get_frame_desc = gc5035_get_frame_desc,
 	.enable_streams = gc5035_enable_streams,
 	.disable_streams = gc5035_disable_streams,
@@ -799,12 +942,16 @@ static int gc5035_init_controls(struct gc5035 *gc5035)
 			  GC5035_PIXEL_RATE, GC5035_PIXEL_RATE, 1,
 			  GC5035_PIXEL_RATE);
 
-	v4l2_ctrl_new_std(ctrl_hdlr, &gc5035_ctrl_ops, V4L2_CID_VBLANK,
-			  GC5035_VBLANK_MIN,
-			  GC5035_VBLANK_MIN +
-			  round_down(GC5035_VTS_MAX - GC5035_HEIGHT -
-				     GC5035_VBLANK_MIN, GC5035_VBLANK_STEP),
-			  GC5035_VBLANK_STEP, GC5035_VBLANK_MIN);
+	/* Ranges for the default crop rectangle, see gc5035_update_ctrls(). */
+	gc5035->vblank = v4l2_ctrl_new_std(ctrl_hdlr, &gc5035_ctrl_ops,
+					   V4L2_CID_VBLANK, GC5035_VBLANK_MIN,
+					   GC5035_VBLANK_MIN +
+					   round_down(GC5035_VTS_MAX -
+						      GC5035_HEIGHT -
+						      GC5035_VBLANK_MIN,
+						      GC5035_VBLANK_STEP),
+					   GC5035_VBLANK_STEP,
+					   GC5035_VBLANK_MIN);
 
 	gc5035->hblank = v4l2_ctrl_new_std(ctrl_hdlr, NULL, V4L2_CID_HBLANK,
 					   GC5035_HTS - GC5035_WIDTH,
-- 
2.47.3


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

* [PATCH v2 5/7] media: i2c: gc8034: Add crop support
  2026-10-12 14:34 ` [PATCH v2 0/7] media: Add GalaxyCore GC5035 and GC8034 sensor drivers Nicola Fiorillo
                     ` (3 preceding siblings ...)
  2026-10-12 14:34   ` [PATCH v2 4/7] media: i2c: gc5035: Add crop support Nicola Fiorillo
@ 2026-10-12 14:34   ` Nicola Fiorillo
  2026-10-12 14:34   ` [PATCH v2 6/7] media: ipu-bridge: Add the GC5035 binning link frequency Nicola Fiorillo
  2026-10-12 14:34   ` [PATCH v2 7/7] media: i2c: gc5035: Add 2x2 binning support Nicola Fiorillo
  6 siblings, 0 replies; 15+ messages in thread
From: Nicola Fiorillo @ 2026-10-12 14:34 UTC (permalink / raw)
  To: Laurent Pinchart, Sakari Ailus
  Cc: Mauro Carvalho Chehab, Hans Verkuil, Dan Scally, Hans de Goede,
	Uwe Kleine-König, linux-media, linux-kernel

Implement the crop selection on the source pad, as for the GC5035: the
vertical crop is done by the readout window, the horizontal crop by the
sensor output crop, as the column start register only gives valid images
at some values. Crop offsets are even, to keep the RGGB order, and sizes
are multiples of 4. The width ranges from 512 to 3280: up to 384 columns
the CSI-2 receiver gets corrupted frames, 448 worked and 512 is kept as a
conservative minimum; an output as wide as the readout window causes
CSI-2 errors. The height ranges from 64 to 2448: with readout windows
taller than the vendor one (2464 rows) the frames get longer than
computed.

set_fmt and the vertical blanking behave as in the GC5035 driver.

Tested in the same way as the GC5035: v4l2-compliance and crop rectangles
at the bounds, in the corners, at the minimum size and in the centre.

Assisted-by: claude-opus-5-5 coccinelle sparse smatch
Signed-off-by: Nicola Fiorillo <nicfio@gmail.com>
---
 drivers/media/i2c/gc8034.c | 168 +++++++++++++++++++++++++++++++++++--
 1 file changed, 161 insertions(+), 7 deletions(-)

diff --git a/drivers/media/i2c/gc8034.c b/drivers/media/i2c/gc8034.c
index c21fb8510..732ee6c78 100644
--- a/drivers/media/i2c/gc8034.c
+++ b/drivers/media/i2c/gc8034.c
@@ -8,6 +8,7 @@
  * The registers and sequences come from the Rockchip BSP driver,
  * rockchip-linux/kernel, branch develop-5.10.
  */
+#include <linux/align.h>
 #include <linux/array_size.h>
 #include <linux/clk.h>
 #include <linux/container_of.h>
@@ -17,6 +18,7 @@
 #include <linux/gpio/consumer.h>
 #include <linux/i2c.h>
 #include <linux/math.h>
+#include <linux/minmax.h>
 #include <linux/module.h>
 #include <linux/pm_runtime.h>
 #include <linux/property.h>
@@ -89,6 +91,12 @@
  *
  * The default crop rectangle is the 3264x2448 image of the vendor register
  * sequence, at (8, 52) in the area.
+ *
+ * Output widths up to 384 pixels gave corrupted frames on the CSI-2 receiver,
+ * 448 pixels and above worked; 512 is kept as the minimum. With a readout
+ * window taller than the 2464 rows of the vendor sequence, the frames were
+ * longer than the timing model below by the extra rows, so the output height
+ * is limited to 2448.
  */
 #define GC8034_AREA_WIDTH		3282
 #define GC8034_AREA_HEIGHT		2500
@@ -102,6 +110,10 @@
 #define GC8034_CROP_TOP			52
 #define GC8034_WIDTH			3264
 #define GC8034_HEIGHT			2448
+#define GC8034_MIN_WIDTH		512
+#define GC8034_MIN_HEIGHT		64
+#define GC8034_MAX_WIDTH		(GC8034_WIN_WIDTH - 4)
+#define GC8034_MAX_HEIGHT		2448
 
 /*
  * The frame is the readout window plus 20 lines plus the blanking register.
@@ -210,6 +222,7 @@ struct gc8034 {
 
 	struct v4l2_ctrl_handler ctrls;
 	struct v4l2_ctrl *exposure;
+	struct v4l2_ctrl *vblank;
 	struct v4l2_ctrl *hblank;
 
 	struct regmap *regmap;
@@ -487,14 +500,55 @@ static int gc8034_enum_frame_size(struct v4l2_subdev *sd,
 	if (fse->index > 0)
 		return -EINVAL;
 
-	fse->min_width = GC8034_WIDTH;
-	fse->max_width = GC8034_WIDTH;
-	fse->min_height = GC8034_HEIGHT;
-	fse->max_height = GC8034_HEIGHT;
+	/* The output size is the size of the crop rectangle. */
+	fse->min_width = GC8034_MIN_WIDTH;
+	fse->max_width = GC8034_MAX_WIDTH;
+	fse->min_height = GC8034_MIN_HEIGHT;
+	fse->max_height = GC8034_MAX_HEIGHT;
 
 	return 0;
 }
 
+/*
+ * Update the controls that depend on the output size. Called with the state
+ * lock held, which is also the control handler lock.
+ */
+static int gc8034_update_ctrls(struct gc8034 *gc8034,
+			       const struct v4l2_mbus_framefmt *format)
+{
+	s64 exposure_max;
+	int ret;
+
+	ret = __v4l2_ctrl_modify_range(gc8034->hblank,
+				       GC8034_HTS - format->width,
+				       GC8034_HTS - format->width, 1,
+				       GC8034_HTS - format->width);
+	if (ret)
+		return ret;
+
+	/*
+	 * As in ov01a10, setting the format or the crop rectangle resets the
+	 * vertical blanking.
+	 */
+	ret = __v4l2_ctrl_modify_range(gc8034->vblank, GC8034_VBLANK_MIN,
+				       GC8034_VTS_MAX - format->height, 1,
+				       GC8034_VBLANK_MIN);
+	if (ret)
+		return ret;
+
+	ret = __v4l2_ctrl_s_ctrl(gc8034->vblank, GC8034_VBLANK_MIN);
+	if (ret)
+		return ret;
+
+	exposure_max = round_down(format->height + gc8034->vblank->val -
+				  GC8034_EXP_MARGIN, GC8034_EXP_STEP);
+
+	return __v4l2_ctrl_modify_range(gc8034->exposure, GC8034_EXP_MIN,
+					exposure_max, GC8034_EXP_STEP,
+					min_t(s64, GC8034_EXP_DEF,
+					      exposure_max));
+}
+
 static void gc8034_fill_format(struct v4l2_mbus_framefmt *fmt,
 			       const struct v4l2_rect *crop)
 {
@@ -537,6 +591,102 @@ static int gc8034_get_selection(struct v4l2_subdev *sd,
 	return 0;
 }
 
+static int gc8034_set_selection(struct v4l2_subdev *sd,
+				const struct v4l2_subdev_client_info *ci,
+				struct v4l2_subdev_state *state,
+				struct v4l2_subdev_selection *sel)
+{
+	struct gc8034 *gc8034 = to_gc8034(sd);
+	struct v4l2_mbus_framefmt *format;
+	struct v4l2_rect rect;
+	u32 max_width, max_height;
+
+	if (sel->target != V4L2_SEL_TGT_CROP)
+		return -EINVAL;
+
+	if (sel->which == V4L2_SUBDEV_FORMAT_ACTIVE &&
+	    v4l2_subdev_is_streaming(sd))
+		return -EBUSY;
+
+	/*
+	 * Even offsets keep the RGGB order. The width is a multiple of 4 so
+	 * that RAW10 lines are a whole number of bytes; only heights that are
+	 * multiples of 4 have been tested.
+	 */
+	rect.left = ALIGN_DOWN(clamp(sel->r.left, 0,
+				     GC8034_AREA_WIDTH - GC8034_MIN_WIDTH), 2);
+	rect.top = ALIGN_DOWN(clamp(sel->r.top, 0,
+				    GC8034_AREA_HEIGHT - GC8034_MIN_HEIGHT), 2);
+	max_width = min(GC8034_MAX_WIDTH, GC8034_AREA_WIDTH - rect.left);
+	max_height = min(GC8034_MAX_HEIGHT, GC8034_AREA_HEIGHT - rect.top);
+	rect.width = ALIGN_DOWN(clamp_t(u32, sel->r.width, GC8034_MIN_WIDTH,
+					max_width), 4);
+	rect.height = ALIGN_DOWN(clamp_t(u32, sel->r.height, GC8034_MIN_HEIGHT,
+					 max_height), 4);
+
+	*v4l2_subdev_state_get_crop(state, 0) = rect;
+	sel->r = rect;
+
+	/* No binning or scaling: the output size is the crop size. */
+	format = v4l2_subdev_state_get_format(state, 0);
+	gc8034_fill_format(format, &rect);
+
+	if (sel->which == V4L2_SUBDEV_FORMAT_ACTIVE)
+		return gc8034_update_ctrls(gc8034, format);
+
+	return 0;
+}
+
+/*
+ * No binning or scaling: the output size is the size of the crop rectangle. A
+ * new width or height centres the crop rectangle on the default crop
+ * rectangle along that axis, within the area, for userspace that does not set
+ * the crop rectangle first. The default size gives the default rectangle back.
+ */
+static int gc8034_set_fmt(struct v4l2_subdev *sd,
+			  const struct v4l2_subdev_client_info *ci,
+			  struct v4l2_subdev_state *state,
+			  struct v4l2_subdev_format *fmt)
+{
+	struct gc8034 *gc8034 = to_gc8034(sd);
+	struct v4l2_mbus_framefmt *format;
+	struct v4l2_rect *crop;
+	u32 width, height;
+	int pos;
+
+	if (fmt->which == V4L2_SUBDEV_FORMAT_ACTIVE &&
+	    v4l2_subdev_is_streaming(sd))
+		return -EBUSY;
+
+	width = ALIGN_DOWN(clamp_t(u32, fmt->format.width, GC8034_MIN_WIDTH,
+				   GC8034_MAX_WIDTH), 4);
+	height = ALIGN_DOWN(clamp_t(u32, fmt->format.height, GC8034_MIN_HEIGHT,
+				    GC8034_MAX_HEIGHT), 4);
+
+	crop = v4l2_subdev_state_get_crop(state, 0);
+	if (width != crop->width) {
+		pos = GC8034_CROP_LEFT + (GC8034_WIDTH - (int)width) / 2;
+		pos = clamp(pos, 0, GC8034_AREA_WIDTH - (int)width);
+		crop->left = ALIGN_DOWN(pos, 2);
+		crop->width = width;
+	}
+	if (height != crop->height) {
+		pos = GC8034_CROP_TOP + (GC8034_HEIGHT - (int)height) / 2;
+		pos = clamp(pos, 0, GC8034_AREA_HEIGHT - (int)height);
+		crop->top = ALIGN_DOWN(pos, 2);
+		crop->height = height;
+	}
+
+	format = v4l2_subdev_state_get_format(state, 0);
+	gc8034_fill_format(format, crop);
+	fmt->format = *format;
+
+	if (fmt->which == V4L2_SUBDEV_FORMAT_ACTIVE)
+		return gc8034_update_ctrls(gc8034, format);
+
+	return 0;
+}
+
 static int gc8034_init_state(struct v4l2_subdev *sd,
 			     struct v4l2_subdev_state *state)
 {
@@ -750,7 +900,9 @@ static const struct v4l2_subdev_pad_ops gc8034_pad_ops = {
 	.enum_mbus_code = gc8034_enum_mbus_code,
 	.enum_frame_size = gc8034_enum_frame_size,
 	.get_fmt = v4l2_subdev_get_fmt,
+	.set_fmt = gc8034_set_fmt,
 	.get_selection = gc8034_get_selection,
+	.set_selection = gc8034_set_selection,
 	.get_frame_desc = gc8034_get_frame_desc,
 	.enable_streams = gc8034_enable_streams,
 	.disable_streams = gc8034_disable_streams,
@@ -825,9 +977,11 @@ static int gc8034_init_controls(struct gc8034 *gc8034)
 			  GC8034_PIXEL_RATE, GC8034_PIXEL_RATE, 1,
 			  GC8034_PIXEL_RATE);
 
-	v4l2_ctrl_new_std(ctrl_hdlr, &gc8034_ctrl_ops, V4L2_CID_VBLANK,
-			  GC8034_VBLANK_MIN, GC8034_VTS_MAX - GC8034_HEIGHT, 1,
-			  GC8034_VBLANK_MIN);
+	/* Ranges for the default crop rectangle, see gc8034_update_ctrls(). */
+	gc8034->vblank = v4l2_ctrl_new_std(ctrl_hdlr, &gc8034_ctrl_ops,
+					   V4L2_CID_VBLANK, GC8034_VBLANK_MIN,
+					   GC8034_VTS_MAX - GC8034_HEIGHT, 1,
+					   GC8034_VBLANK_MIN);
 
 	gc8034->hblank = v4l2_ctrl_new_std(ctrl_hdlr, NULL, V4L2_CID_HBLANK,
 					   GC8034_HTS - GC8034_WIDTH,
-- 
2.47.3


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

* [PATCH v2 6/7] media: ipu-bridge: Add the GC5035 binning link frequency
  2026-10-12 14:34 ` [PATCH v2 0/7] media: Add GalaxyCore GC5035 and GC8034 sensor drivers Nicola Fiorillo
                     ` (4 preceding siblings ...)
  2026-10-12 14:34   ` [PATCH v2 5/7] media: i2c: gc8034: " Nicola Fiorillo
@ 2026-10-12 14:34   ` Nicola Fiorillo
  2026-10-12 14:34   ` [PATCH v2 7/7] media: i2c: gc5035: Add 2x2 binning support Nicola Fiorillo
  6 siblings, 0 replies; 15+ messages in thread
From: Nicola Fiorillo @ 2026-10-12 14:34 UTC (permalink / raw)
  To: Laurent Pinchart, Sakari Ailus
  Cc: Mauro Carvalho Chehab, Hans Verkuil, Dan Scally, Hans de Goede,
	Uwe Kleine-König, linux-media, linux-kernel

In its 2x2 binned mode the GC5035 uses a 175.2 MHz link frequency, set by
the PLL of the vendor register sequence for that mode. List it, so that
the driver can offer binning.

Assisted-by: claude-opus-5-5
Signed-off-by: Nicola Fiorillo <nicfio@gmail.com>
---
 drivers/media/pci/intel/ipu-bridge.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/drivers/media/pci/intel/ipu-bridge.c b/drivers/media/pci/intel/ipu-bridge.c
index 851096d7d..3caa07529 100644
--- a/drivers/media/pci/intel/ipu-bridge.c
+++ b/drivers/media/pci/intel/ipu-bridge.c
@@ -60,7 +60,7 @@ static const u16 ipu6_ov5693_ncont_clk[] = {
 
 static const struct ipu_sensor_config ipu_supported_sensors[] = {
 	/* GalaxyCore GC5035 */
-	IPU_SENSOR_CONFIG("GCTI5035", 1, 422400000),
+	IPU_SENSOR_CONFIG("GCTI5035", 2, 175200000, 422400000),
 	/* GalaxyCore GC8034 */
 	IPU_SENSOR_CONFIG("GCTI8034", 1, 268800000),
 	/* Himax HM1092 */
-- 
2.47.3


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

* [PATCH v2 7/7] media: i2c: gc5035: Add 2x2 binning support
  2026-10-12 14:34 ` [PATCH v2 0/7] media: Add GalaxyCore GC5035 and GC8034 sensor drivers Nicola Fiorillo
                     ` (5 preceding siblings ...)
  2026-10-12 14:34   ` [PATCH v2 6/7] media: ipu-bridge: Add the GC5035 binning link frequency Nicola Fiorillo
@ 2026-10-12 14:34   ` Nicola Fiorillo
  6 siblings, 0 replies; 15+ messages in thread
From: Nicola Fiorillo @ 2026-10-12 14:34 UTC (permalink / raw)
  To: Laurent Pinchart, Sakari Ailus
  Cc: Mauro Carvalho Chehab, Hans Verkuil, Dan Scally, Hans de Goede,
	Uwe Kleine-König, linux-media, linux-kernel

Setting the format to half the size of the default crop rectangle
selects 2x2 binning of that rectangle: 1296x972 with the same field of
view as 2592x1944, averaging four pixels of the same colour. The crop
rectangle goes back to the default one, so that the size alone selects
the mode, whatever crop rectangle earlier calls left; only a crop
rectangle of that size, set first, keeps 1296x972 unbinned. The register
settings are the ones of the vendor sequence for that mode, which only
work as a whole. Any other format, or a new crop rectangle, gives the
unbinned output as before. enum_frame_size lists 1296x972 as a second,
discrete size.

The vendor sequence changes the PLL. The link frequency in this mode is
175.2 MHz: the time needed to send a line of a given width, found from
the shortest line length that gives complete frames, gives 176.3 +/- 0.9
MHz (and 420.7 +/- 1.5 MHz at full resolution, where it is 422.4 MHz).
LINK_FREQ and PIXEL_RATE follow the mode. The timings are expressed in
sensor lines, as at full resolution: the sensor reads one row of the pixel
array per line and outputs one row every two lines. This keeps the frame
length and exposure registers in their own unit, with a one line exposure
step, and the frame period computed from HBLANK and VBLANK is the measured
one. With the vendor readout window a frame lasts at least 1996 lines, so
binning runs at up to 24 fps, against 28.8 fps at 2592x1944: it is meant
for lower noise and bandwidth, not for speed.

Binning is only offered when the firmware lists the 175.2 MHz link
frequency.

Tested with v4l2-compliance (54/54, no warning, in both modes) and
libcamera, which picks the mode for 1280x960 at 24 fps: the frame period
is the computed one, also with a longer VBLANK and at the maximum
exposure, the signal doubles with the exposure in sensor lines, and the
output matches the 2x2 average per colour of a full resolution frame,
with no offset.

Assisted-by: claude-opus-5-5 coccinelle sparse smatch
Signed-off-by: Nicola Fiorillo <nicfio@gmail.com>
---
 drivers/media/i2c/gc5035.c | 296 +++++++++++++++++++++++++++++++------
 1 file changed, 249 insertions(+), 47 deletions(-)

diff --git a/drivers/media/i2c/gc5035.c b/drivers/media/i2c/gc5035.c
index b7190aa52..f8cd56a46 100644
--- a/drivers/media/i2c/gc5035.c
+++ b/drivers/media/i2c/gc5035.c
@@ -13,6 +13,7 @@
  */
 #include <linux/align.h>
 #include <linux/array_size.h>
+#include <linux/bits.h>
 #include <linux/clk.h>
 #include <linux/container_of.h>
 #include <linux/delay.h>
@@ -142,6 +143,36 @@
 #define GC5035_PIXEL_RATE		(GC5035_LINK_FREQ * 2 * \
 					 GC5035_DATA_LANES / GC5035_RGB_DEPTH)
 
+/*
+ * 2x2 binning, with the vendor register sequence for 1296x972. It sets the
+ * PLL multiplier to 0x49 and adds a divider by 2 (register 0xfa), which
+ * gives a 175.2 MHz link frequency. The frame period scales with the PLL
+ * setting as expected, and the bandwidth limit of the link was measured at
+ * 176.3 +/- 0.9 MHz in this mode (420.7 +/- 1.5 MHz at full resolution with
+ * the same method): the time needed to send a line of a given width was
+ * found from the shortest line length that gives complete frames.
+ *
+ * The sensor reads one row of the pixel array per line and outputs one row
+ * every two lines. Expressing the timings in sensor lines keeps the frame
+ * length, vertical blanking and exposure registers in the same units as at
+ * full resolution: the line length is 1460 pixels at 70.08 MHz, and the
+ * vendor readout window, which is used in this mode, needs at least 1996
+ * lines per frame. Binning is only available on the default crop rectangle,
+ * and the output starts at (4, 4) in binned coordinates, which is (8, 8) in
+ * the area: GRBG, like the default rectangle.
+ */
+#define GC5035_PLL_MULT_BIN		0x49
+#define GC5035_LINK_FREQ_BIN		(GC5035_XCLK_FREQ * \
+					 GC5035_PLL_MULT_BIN / 8)
+#define GC5035_PIXEL_RATE_BIN		(GC5035_LINK_FREQ_BIN * 2 * \
+					 GC5035_DATA_LANES / GC5035_RGB_DEPTH)
+#define GC5035_HTS_BIN			1460
+#define GC5035_VTS_MIN_BIN		1996
+#define GC5035_WIN_TOP_BIN		GC5035_ROW_START_OFFSET
+#define GC5035_WIN_LEFT_BIN		3
+#define GC5035_WIN_HEIGHT_BIN		1960
+#define GC5035_WIN_WIDTH_BIN		2608
+
 /*
  * No datasheet is available. From the GC5035 driver posted by Tomasz Figa in
  * 2020: IOVDD at least 50 us before AVDD and DVDD, at least 1200 MCLK cycles
@@ -158,8 +189,14 @@
 
 #define GC5035_MBUS_CODE		MEDIA_BUS_FMT_SGRBG10_1X10
 
+enum {
+	GC5035_LINK_FREQ_FULL_INDEX,
+	GC5035_LINK_FREQ_BIN_INDEX,
+};
+
 static const s64 gc5035_link_freq_menu[] = {
-	GC5035_LINK_FREQ,
+	[GC5035_LINK_FREQ_FULL_INDEX] = GC5035_LINK_FREQ,
+	[GC5035_LINK_FREQ_BIN_INDEX] = GC5035_LINK_FREQ_BIN,
 };
 
 static const char * const gc5035_test_pattern_menu[] = {
@@ -210,11 +247,16 @@ struct gc5035 {
 	struct gpio_desc *powerdown_gpio;
 
 	struct v4l2_ctrl_handler ctrls;
+	struct v4l2_ctrl *link_freq;
+	struct v4l2_ctrl *pixel_rate;
 	struct v4l2_ctrl *exposure;
 	struct v4l2_ctrl *vblank;
 	struct v4l2_ctrl *hblank;
 
 	struct regmap *regmap;
+
+	/* The firmware lists the link frequency of the binned mode. */
+	bool binning;
 };
 
 /*
@@ -376,6 +418,56 @@ static const struct cci_reg_sequence gc5035_regs[] = {
 	{ GC5035_REG_STREAM, GC5035_STREAM_OFF },
 };
 
+/*
+ * Written after gc5035_regs for 2x2 binning: the registers that the vendor
+ * sequence for 1296x972 sets differently, in its order, without the geometry.
+ * The sequence only works as a whole: without the PLL settings the image is
+ * saturated, without the MIPI ones (page 3) the CSI-2 receiver gets errors,
+ * and without the analog ones the four pixels are not averaged.
+ */
+static const struct cci_reg_sequence gc5035_bin_regs[] = {
+	{ GC5035_REG_PAGE_SELECT, 0x00 },
+	{ CCI_REG8(0xf5), 0xe4 },
+	{ GC5035_REG_PLL_MULT, GC5035_PLL_MULT_BIN },
+	{ CCI_REG8(0xf9), 0x12 },
+	{ CCI_REG8(0xfa), 0x01 },
+	{ CCI_REG8(0x33), 0x20 },
+	{ GC5035_REG_PAGE_SELECT, 0x03 },
+	{ CCI_REG8(0x01), 0x87 },
+	{ CCI_REG8(0xf7), 0x11 },
+	{ GC5035_REG_PAGE_SELECT, 0x00 },
+	{ CCI_REG8(0x9d), 0x0c },
+	{ CCI_REG8(0x21), 0x60 },
+	{ CCI_REG8(0x29), 0x30 },
+	{ CCI_REG8(0x44), 0x18 },
+	{ CCI_REG8(0x4e), 0x20 },
+	{ CCI_REG8(0x8c), 0x20 },
+	{ CCI_REG8(0x91), 0x15 },
+	{ CCI_REG8(0x92), 0x3a },
+	{ CCI_REG8(0x95), 0x45 },
+	{ CCI_REG8(0x96), 0x35 },
+	{ CCI_REG8(0xd5), 0xf0 },
+	{ CCI_REG8(0x97), 0x20 },
+	{ CCI_REG8(0x1f), 0x19 },
+	{ CCI_REG8(0xd0), 0xb3 },
+	{ GC5035_REG_PAGE_SELECT, 0x02 },
+	{ CCI_REG8(0x14), 0x02 },
+	{ CCI_REG8(0x15), 0x00 },
+	{ GC5035_REG_PAGE_SELECT, 0x01 },
+	{ CCI_REG8(0x49), 0x00 },
+	{ CCI_REG8(0x4a), 0x01 },
+	{ CCI_REG8(0x4b), 0xf8 },
+	{ CCI_REG8(0x4e), 0x06 },
+	{ CCI_REG8(0x44), 0x02 },
+	{ GC5035_REG_PAGE_SELECT, 0x03 },
+	{ CCI_REG8(0x02), 0x58 },
+	{ CCI_REG8(0x22), 0x03 },
+	{ CCI_REG8(0x26), 0x06 },
+	{ CCI_REG8(0x29), 0x03 },
+	{ CCI_REG8(0x2b), 0x06 },
+	{ GC5035_REG_PAGE_SELECT, 0x00 },
+};
+
 static inline struct gc5035 *to_gc5035(struct v4l2_subdev *sd)
 {
 	return container_of(sd, struct gc5035, sd);
@@ -462,52 +554,89 @@ static int gc5035_enum_frame_size(struct v4l2_subdev *sd,
 				  struct v4l2_subdev_state *sd_state,
 				  struct v4l2_subdev_frame_size_enum *fse)
 {
+	struct gc5035 *gc5035 = to_gc5035(sd);
+
 	if (fse->code != GC5035_MBUS_CODE)
 		return -EINVAL;
 
-	if (fse->index > 0)
+	switch (fse->index) {
+	case 0:
+		/* Without binning, the output size is the crop size. */
+		fse->min_width = GC5035_MIN_WIDTH;
+		fse->max_width = GC5035_AREA_WIDTH;
+		fse->min_height = GC5035_MIN_HEIGHT;
+		fse->max_height = GC5035_AREA_HEIGHT;
+		break;
+	case 1:
+		/* 2x2 binning of the default crop rectangle. */
+		if (!gc5035->binning)
+			return -EINVAL;
+		fse->min_width = GC5035_WIDTH / 2;
+		fse->max_width = GC5035_WIDTH / 2;
+		fse->min_height = GC5035_HEIGHT / 2;
+		fse->max_height = GC5035_HEIGHT / 2;
+		break;
+	default:
 		return -EINVAL;
-
-	/* The output size is the size of the crop rectangle. */
-	fse->min_width = GC5035_MIN_WIDTH;
-	fse->max_width = GC5035_AREA_WIDTH;
-	fse->min_height = GC5035_MIN_HEIGHT;
-	fse->max_height = GC5035_AREA_HEIGHT;
+	}
 
 	return 0;
 }
 
+/* Binning is the only case where the output is smaller than the crop. */
+static bool gc5035_is_binned(const struct v4l2_rect *crop,
+			     const struct v4l2_mbus_framefmt *format)
+{
+	return format->width < crop->width;
+}
+
 /*
- * Update the controls that depend on the output size. Called with the state
- * lock held, which is also the control handler lock.
+ * Update the controls that depend on the output size and on binning. Called
+ * with the state lock held, which is also the control handler lock.
  */
 static int gc5035_update_ctrls(struct gc5035 *gc5035,
+			       const struct v4l2_rect *crop,
 			       const struct v4l2_mbus_framefmt *format)
 {
-	s64 vblank_max, exposure_max;
+	bool binned = gc5035_is_binned(crop, format);
+	s64 vblank_min, vblank_max, exposure_max;
+	u32 hts;
 	int ret;
 
-	ret = __v4l2_ctrl_modify_range(gc5035->hblank,
-				       GC5035_HTS - format->width,
-				       GC5035_HTS - format->width, 1,
-				       GC5035_HTS - format->width);
+	ret = __v4l2_ctrl_s_ctrl(gc5035->link_freq,
+				 binned ? GC5035_LINK_FREQ_BIN_INDEX :
+					  GC5035_LINK_FREQ_FULL_INDEX);
+	if (ret)
+		return ret;
+
+	ret = __v4l2_ctrl_s_ctrl_int64(gc5035->pixel_rate,
+				       binned ? GC5035_PIXEL_RATE_BIN :
+						GC5035_PIXEL_RATE);
+	if (ret)
+		return ret;
+
+	hts = binned ? GC5035_HTS_BIN : GC5035_HTS;
+	ret = __v4l2_ctrl_modify_range(gc5035->hblank, hts - format->width,
+				       hts - format->width, 1,
+				       hts - format->width);
 	if (ret)
 		return ret;
 
-	vblank_max = GC5035_VBLANK_MIN +
-		     round_down(GC5035_VTS_MAX - format->height -
-				GC5035_VBLANK_MIN, GC5035_VBLANK_STEP);
+	vblank_min = binned ? GC5035_VTS_MIN_BIN - format->height :
+			      GC5035_VBLANK_MIN;
+	vblank_max = vblank_min +
+		     round_down(GC5035_VTS_MAX - format->height - vblank_min,
+				GC5035_VBLANK_STEP);
 	/*
 	 * As in ov01a10, setting the format or the crop rectangle resets the
 	 * vertical blanking.
 	 */
-	ret = __v4l2_ctrl_modify_range(gc5035->vblank, GC5035_VBLANK_MIN,
-				       vblank_max, GC5035_VBLANK_STEP,
-				       GC5035_VBLANK_MIN);
+	ret = __v4l2_ctrl_modify_range(gc5035->vblank, vblank_min, vblank_max,
+				       GC5035_VBLANK_STEP, vblank_min);
 	if (ret)
 		return ret;
 
-	ret = __v4l2_ctrl_s_ctrl(gc5035->vblank, GC5035_VBLANK_MIN);
+	ret = __v4l2_ctrl_s_ctrl(gc5035->vblank, vblank_min);
 	if (ret)
 		return ret;
 
@@ -521,10 +650,10 @@ static int gc5035_update_ctrls(struct gc5035 *gc5035,
 }
 
 static void gc5035_fill_format(struct v4l2_mbus_framefmt *fmt,
-			       const struct v4l2_rect *crop)
+			       const struct v4l2_rect *crop, unsigned int bin)
 {
-	fmt->width = crop->width;
-	fmt->height = crop->height;
+	fmt->width = crop->width / bin;
+	fmt->height = crop->height / bin;
 	fmt->code = GC5035_MBUS_CODE;
 	fmt->field = V4L2_FIELD_NONE;
 	fmt->colorspace = V4L2_COLORSPACE_RAW;
@@ -594,31 +723,42 @@ static int gc5035_set_selection(struct v4l2_subdev *sd,
 	*v4l2_subdev_state_get_crop(state, 0) = rect;
 	sel->r = rect;
 
-	/* No binning or scaling: the output size is the crop size. */
+	/* A new crop rectangle disables binning: the output is its size. */
 	format = v4l2_subdev_state_get_format(state, 0);
-	gc5035_fill_format(format, &rect);
+	gc5035_fill_format(format, &rect, 1);
 
 	if (sel->which == V4L2_SUBDEV_FORMAT_ACTIVE)
-		return gc5035_update_ctrls(gc5035, format);
+		return gc5035_update_ctrls(gc5035, &rect, format);
 
 	return 0;
 }
 
 /*
- * No binning or scaling: the output size is the size of the crop rectangle. A
- * new width or height centres the crop rectangle on the default crop
- * rectangle along that axis, within the area, for userspace that does not set
- * the crop rectangle first. The default size gives the default rectangle back.
+ * Half the size of the default crop rectangle selects 2x2 binning and sets
+ * the default crop rectangle back, unless the crop rectangle already has
+ * that size: the size alone selects the binned mode, with the full field of
+ * view, and a crop rectangle of that size set first stays unbinned.
+ * Otherwise the output size is the size of the crop rectangle: a new width
+ * or height centres the crop rectangle on the default crop rectangle along
+ * that axis, within the area, for userspace that does not set the crop
+ * rectangle first. The default size gives the default rectangle back.
  */
 static int gc5035_set_fmt(struct v4l2_subdev *sd,
 			  const struct v4l2_subdev_client_info *ci,
 			  struct v4l2_subdev_state *state,
 			  struct v4l2_subdev_format *fmt)
 {
+	static const struct v4l2_rect crop_default = {
+		.left = GC5035_CROP_LEFT,
+		.top = GC5035_CROP_TOP,
+		.width = GC5035_WIDTH,
+		.height = GC5035_HEIGHT,
+	};
 	struct gc5035 *gc5035 = to_gc5035(sd);
 	struct v4l2_mbus_framefmt *format;
 	struct v4l2_rect *crop;
 	u32 width, height;
+	unsigned int bin;
 	int pos;
 
 	if (fmt->which == V4L2_SUBDEV_FORMAT_ACTIVE &&
@@ -631,13 +771,18 @@ static int gc5035_set_fmt(struct v4l2_subdev *sd,
 				    GC5035_AREA_HEIGHT), 4);
 
 	crop = v4l2_subdev_state_get_crop(state, 0);
-	if (width != crop->width) {
+	bin = gc5035->binning && width == GC5035_WIDTH / 2 &&
+	      height == GC5035_HEIGHT / 2 &&
+	      (width != crop->width || height != crop->height) ? 2 : 1;
+	if (bin == 2)
+		*crop = crop_default;
+	if (bin == 1 && width != crop->width) {
 		pos = GC5035_CROP_LEFT + (GC5035_WIDTH - (int)width) / 2;
 		pos = clamp(pos, 0, GC5035_AREA_WIDTH - (int)width);
 		crop->left = ALIGN_DOWN(pos, 2);
 		crop->width = width;
 	}
-	if (height != crop->height) {
+	if (bin == 1 && height != crop->height) {
 		pos = GC5035_CROP_TOP + (GC5035_HEIGHT - (int)height) / 2;
 		pos = clamp(pos, 0, GC5035_AREA_HEIGHT - (int)height);
 		crop->top = ALIGN_DOWN(pos, 2);
@@ -645,11 +790,11 @@ static int gc5035_set_fmt(struct v4l2_subdev *sd,
 	}
 
 	format = v4l2_subdev_state_get_format(state, 0);
-	gc5035_fill_format(format, crop);
+	gc5035_fill_format(format, crop, bin);
 	fmt->format = *format;
 
 	if (fmt->which == V4L2_SUBDEV_FORMAT_ACTIVE)
-		return gc5035_update_ctrls(gc5035, format);
+		return gc5035_update_ctrls(gc5035, crop, format);
 
 	return 0;
 }
@@ -664,7 +809,7 @@ static int gc5035_init_state(struct v4l2_subdev *sd,
 	crop->width = GC5035_WIDTH;
 	crop->height = GC5035_HEIGHT;
 
-	gc5035_fill_format(v4l2_subdev_state_get_format(state, 0), crop);
+	gc5035_fill_format(v4l2_subdev_state_get_format(state, 0), crop, 1);
 
 	return 0;
 }
@@ -803,11 +948,42 @@ static int gc5035_write_geometry(struct gc5035 *gc5035,
 	return ret;
 }
 
+/*
+ * Binning: the vendor readout window, which starts at the first row and
+ * column of the area, and the output crop in binned coordinates.
+ */
+static int gc5035_write_geometry_bin(struct gc5035 *gc5035,
+				     const struct v4l2_rect *crop,
+				     const struct v4l2_mbus_framefmt *format)
+{
+	int ret = 0;
+
+	cci_write(gc5035->regmap, GC5035_REG_PAGE_SELECT, GC5035_PAGE_0, &ret);
+	cci_write(gc5035->regmap, GC5035_REG_WIN_TOP, GC5035_WIN_TOP_BIN, &ret);
+	cci_write(gc5035->regmap, GC5035_REG_WIN_LEFT, GC5035_WIN_LEFT_BIN,
+		  &ret);
+	cci_write(gc5035->regmap, GC5035_REG_WIN_HEIGHT, GC5035_WIN_HEIGHT_BIN,
+		  &ret);
+	cci_write(gc5035->regmap, GC5035_REG_WIN_WIDTH, GC5035_WIN_WIDTH_BIN,
+		  &ret);
+
+	cci_write(gc5035->regmap, GC5035_REG_PAGE_SELECT, GC5035_PAGE_1, &ret);
+	cci_write(gc5035->regmap, GC5035_REG_CROP_TOP, crop->top / 2, &ret);
+	cci_write(gc5035->regmap, GC5035_REG_CROP_LEFT, crop->left / 2, &ret);
+	cci_write(gc5035->regmap, GC5035_REG_OUT_HEIGHT, format->height, &ret);
+	cci_write(gc5035->regmap, GC5035_REG_OUT_WIDTH, format->width, &ret);
+	cci_write(gc5035->regmap, GC5035_REG_PAGE_SELECT, GC5035_PAGE_0, &ret);
+
+	return ret;
+}
+
 static int gc5035_enable_streams(struct v4l2_subdev *sd,
 				 struct v4l2_subdev_state *state,
 				 u32 pad, u64 streams_mask)
 {
 	struct gc5035 *gc5035 = to_gc5035(sd);
+	const struct v4l2_mbus_framefmt *format;
+	const struct v4l2_rect *crop;
 	int ret;
 
 	ret = pm_runtime_resume_and_get(gc5035->dev);
@@ -819,8 +995,18 @@ static int gc5035_enable_streams(struct v4l2_subdev *sd,
 	if (ret)
 		goto err_rpm_put;
 
-	ret = gc5035_write_geometry(gc5035,
-				    v4l2_subdev_state_get_crop(state, 0));
+	crop = v4l2_subdev_state_get_crop(state, 0);
+	format = v4l2_subdev_state_get_format(state, 0);
+	if (gc5035_is_binned(crop, format)) {
+		ret = cci_multi_reg_write(gc5035->regmap, gc5035_bin_regs,
+					  ARRAY_SIZE(gc5035_bin_regs), NULL);
+		if (ret)
+			goto err_rpm_put;
+
+		ret = gc5035_write_geometry_bin(gc5035, crop, format);
+	} else {
+		ret = gc5035_write_geometry(gc5035, crop);
+	}
 	if (ret)
 		goto err_rpm_put;
 
@@ -912,6 +1098,17 @@ static int gc5035_parse_fwnode(struct gc5035 *gc5035)
 				       gc5035_link_freq_menu,
 				       ARRAY_SIZE(gc5035_link_freq_menu),
 				       &link_freq_bitmap);
+	if (ret)
+		goto done;
+
+	if (!(link_freq_bitmap & BIT(GC5035_LINK_FREQ_FULL_INDEX))) {
+		ret = dev_err_probe(dev, -EINVAL,
+				    "link frequency %lu Hz not supported\n",
+				    GC5035_LINK_FREQ);
+		goto done;
+	}
+
+	gc5035->binning = link_freq_bitmap & BIT(GC5035_LINK_FREQ_BIN_INDEX);
 
 done:
 	v4l2_fwnode_endpoint_free(&bus_cfg);
@@ -923,7 +1120,6 @@ static int gc5035_init_controls(struct gc5035 *gc5035)
 {
 	struct v4l2_fwnode_device_properties props;
 	struct v4l2_ctrl_handler *ctrl_hdlr;
-	struct v4l2_ctrl *link_freq;
 	s64 exposure_max;
 	int ret;
 
@@ -934,13 +1130,19 @@ static int gc5035_init_controls(struct gc5035 *gc5035)
 	ctrl_hdlr = &gc5035->ctrls;
 	v4l2_ctrl_handler_init(ctrl_hdlr, 9);
 
-	link_freq = v4l2_ctrl_new_int_menu(ctrl_hdlr, NULL, V4L2_CID_LINK_FREQ,
-					   ARRAY_SIZE(gc5035_link_freq_menu) -
-					   1, 0, gc5035_link_freq_menu);
+	/* The binned mode frequency is only listed when the firmware has it. */
+	gc5035->link_freq =
+		v4l2_ctrl_new_int_menu(ctrl_hdlr, NULL, V4L2_CID_LINK_FREQ,
+				       gc5035->binning ?
+				       GC5035_LINK_FREQ_BIN_INDEX :
+				       GC5035_LINK_FREQ_FULL_INDEX,
+				       GC5035_LINK_FREQ_FULL_INDEX,
+				       gc5035_link_freq_menu);
 
-	v4l2_ctrl_new_std(ctrl_hdlr, NULL, V4L2_CID_PIXEL_RATE,
-			  GC5035_PIXEL_RATE, GC5035_PIXEL_RATE, 1,
-			  GC5035_PIXEL_RATE);
+	gc5035->pixel_rate =
+		v4l2_ctrl_new_std(ctrl_hdlr, NULL, V4L2_CID_PIXEL_RATE,
+				  GC5035_PIXEL_RATE_BIN, GC5035_PIXEL_RATE, 1,
+				  GC5035_PIXEL_RATE);
 
 	/* Ranges for the default crop rectangle, see gc5035_update_ctrls(). */
 	gc5035->vblank = v4l2_ctrl_new_std(ctrl_hdlr, &gc5035_ctrl_ops,
@@ -977,7 +1179,7 @@ static int gc5035_init_controls(struct gc5035 *gc5035)
 	if (ctrl_hdlr->error)
 		return v4l2_ctrl_handler_free(ctrl_hdlr);
 
-	link_freq->flags |= V4L2_CTRL_FLAG_READ_ONLY;
+	gc5035->link_freq->flags |= V4L2_CTRL_FLAG_READ_ONLY;
 	gc5035->hblank->flags |= V4L2_CTRL_FLAG_READ_ONLY;
 
 	gc5035->sd.ctrl_handler = ctrl_hdlr;
-- 
2.47.3


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

end of thread, other threads:[~2026-10-12 14:35 UTC | newest]

Thread overview: 15+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-09  7:27 [PATCH 0/3] media: Add GalaxyCore GC5035 and GC8034 sensor drivers Nicola Fiorillo
2026-10-09  7:27 ` [PATCH 1/3] media: i2c: Add GC5035 image sensor driver Nicola Fiorillo
2026-10-09  9:48   ` Laurent Pinchart
2026-10-12 11:20   ` Uwe Kleine-König
2026-10-09  7:27 ` [PATCH 2/3] media: i2c: Add GC8034 " Nicola Fiorillo
2026-10-09  7:27 ` [PATCH 3/3] media: ipu-bridge: Add GalaxyCore GC5035 and GC8034 Nicola Fiorillo
2026-10-09  9:24   ` Dan Scally
2026-10-12 14:34 ` [PATCH v2 0/7] media: Add GalaxyCore GC5035 and GC8034 sensor drivers Nicola Fiorillo
2026-10-12 14:34   ` [PATCH v2 1/7] media: i2c: Add GC5035 image sensor driver Nicola Fiorillo
2026-10-12 14:34   ` [PATCH v2 2/7] media: i2c: Add GC8034 " Nicola Fiorillo
2026-10-12 14:34   ` [PATCH v2 3/7] media: ipu-bridge: Add GalaxyCore GC5035 and GC8034 Nicola Fiorillo
2026-10-12 14:34   ` [PATCH v2 4/7] media: i2c: gc5035: Add crop support Nicola Fiorillo
2026-10-12 14:34   ` [PATCH v2 5/7] media: i2c: gc8034: " Nicola Fiorillo
2026-10-12 14:34   ` [PATCH v2 6/7] media: ipu-bridge: Add the GC5035 binning link frequency Nicola Fiorillo
2026-10-12 14:34   ` [PATCH v2 7/7] media: i2c: gc5035: Add 2x2 binning support Nicola Fiorillo

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

all inboxes | Powered by JetHome®