* [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