linux-media.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* [RFC PATCH] media: new driver mt9m02x for Onsemi MT9M024 and AR0141CS
@ 2020-10-20  9:27 Helmut Grohne
  2020-10-21  9:50 ` Sakari Ailus
  0 siblings, 1 reply; 6+ messages in thread
From: Helmut Grohne @ 2020-10-20  9:27 UTC (permalink / raw)
  To: Mauro Carvalho Chehab, Sakari Ailus, Laurent Pinchart, linux-media

Add basic support for Onsemi (formerly Aptina) image sensors MT9M024 and
AR0141CS.
 * Absolute exposure
 * Analogue gain as iso sensitivity
 * Digital gain
 * HDR (MT9M024)
 * Temperature sensor (MT9M024)
 * Support for monochrome/color models
 * Vertical/horziontal flip
 * Binning
 * Reset via GPIO or I2C.

Signed-off-by: Helmut Grohne <helmut.grohne@intenta.de>
Signed-off-by: Ringo Schulz <ringo.schulz@intenta.de>
Signed-off-by: Peter Will <peter.will@intenta.de>
---
 drivers/media/i2c/Kconfig   |   10 +
 drivers/media/i2c/Makefile  |    1 +
 drivers/media/i2c/mt9m02x.c | 1596 +++++++++++++++++++++++++++++++++++
 3 files changed, 1607 insertions(+)
 create mode 100644 drivers/media/i2c/mt9m02x.c

Hi,

I've promised this RFC patch quite a while ago. In the mean time, legal
did catch up and we're entitled to publish it.

This goes as far back as 2018, where I proposed[1] changing the
aptina_pll_calculate function to solve the pix_clock approximately
instead of giving up.

The driver is in practical use. It mostly passes checkpatch.pl. Known
issues:
 * It presently defines 3 custom V4L2 CIDs inside the .c file. Those
   will need a proper place. Possibly, there is some standard CID that
   could replace them. Suggestions welcome.
 * There is no streaming functionality yet. In our use, the imager is
   triggered externally.
 * The driver needs the modified aptina_pll_calculate[1] to work in our
   setting. Otherwise probing fails finding a valid pix_clock.
 * For similarity with other drivers, I added mt9m02x_platform_data. For
   actually using it, it needs to be moved to a header.

Questions:
 * What are the big issues that need to be solved before considering the
   driver for merging?
 * Would it be useful to use the regmap framework?
 * The analogue gain is mapped to V4L2_CID_ISO_SENSITIVITY. That's not a
   perfect match, but it is a menu with scale. Is there something better
   with these properties?
 * Should I keep the MT9M024 and AR0141CS drivers fused like this or
   separate them? If fused, is the current naming ok? It originated from
   a brief period of looking into MT9M021 before adding AR0141CS
   support.
 * Is mt9m02x_platform_data (i.e. support for non-DT configurations)
   worth keeping?

Please save a detailed review for a later iteration. I do expect
requests for structural changes that could easily render detailed
changes irrelevant. Of course, if you spot repeated patterns of details
that I got wrong, that's worth a mention still.

Thank you for looking into this

Helmut

[1] https://lore.kernel.org/linux-media/20180823075208.mqjctv4ax4dakfws@laureti-dev/#t

diff --git a/drivers/media/i2c/Kconfig b/drivers/media/i2c/Kconfig
index c7ba76fee599..ddcec63ea531 100644
--- a/drivers/media/i2c/Kconfig
+++ b/drivers/media/i2c/Kconfig
@@ -1245,6 +1245,16 @@ config VIDEO_S5C73M3
 	  This is a V4L2 sensor driver for Samsung S5C73M3
 	  8 Mpixel camera.
 
+config VIDEO_MT9M02X
+	tristate "Aptina/Onsemi MT9M024 and AR0141CS sensor support"
+	depends on HWMON && I2C && VIDEO_V4L2
+	select V4L2_FWNODE
+	select VIDEO_APTINA_PLL
+	select VIDEO_V4L2_SUBDEV_API
+	help
+	  This is a V4L2 sensor driver for Onsemi (formerly Aptina)
+	  image sensors MT9M024 and AR0141CS (monochrome/color).
+
 endmenu
 
 menu "Lens drivers"
diff --git a/drivers/media/i2c/Makefile b/drivers/media/i2c/Makefile
index f0a77473979d..4023bea35c4f 100644
--- a/drivers/media/i2c/Makefile
+++ b/drivers/media/i2c/Makefile
@@ -85,6 +85,7 @@ obj-$(CONFIG_VIDEO_OV9640) += ov9640.o
 obj-$(CONFIG_VIDEO_OV9650) += ov9650.o
 obj-$(CONFIG_VIDEO_OV13858) += ov13858.o
 obj-$(CONFIG_VIDEO_MT9M001) += mt9m001.o
+obj-$(CONFIG_VIDEO_MT9M02X) += mt9m02x.o
 obj-$(CONFIG_VIDEO_MT9M032) += mt9m032.o
 obj-$(CONFIG_VIDEO_MT9M111) += mt9m111.o
 obj-$(CONFIG_VIDEO_MT9P031) += mt9p031.o
diff --git a/drivers/media/i2c/mt9m02x.c b/drivers/media/i2c/mt9m02x.c
new file mode 100644
index 000000000000..d0eb5e4fc4ea
--- /dev/null
+++ b/drivers/media/i2c/mt9m02x.c
@@ -0,0 +1,1596 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2016-2020 Intenta GmbH
+ */
+
+#include <linux/gpio/consumer.h>
+#include <linux/hwmon.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/device.h>
+#include <linux/i2c.h>
+#include <linux/delay.h>
+#include <linux/math64.h>
+#include <linux/mutex.h>
+#include <media/v4l2-async.h>
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-subdev.h>
+#include "aptina-pll.h"
+
+#define MT9M02X_V4L2_CID_USER_BASE	(V4L2_CID_USER_BASE + 0x1000)
+#define MT9M02X_V4L2_CID_HDR_RATIO_T1T2	(MT9M02X_V4L2_CID_USER_BASE + 2)
+#define MT9M02X_V4L2_CID_HDR_RATIO_T2T3	(MT9M02X_V4L2_CID_USER_BASE + 3)
+
+/* registers */
+#define APT_CHIP_VERSION			0x3000
+#define APT_Y_ADDR_START			0x3002
+#define APT_X_ADDR_START			0x3004
+#define APT_Y_ADDR_END				0x3006
+#define APT_X_ADDR_END				0x3008
+#define APT_LINE_LENGTH_PCK			0x300C
+#define		APT_LINE_LENGTH_PCK_VALUE	0x0672
+#define APT_FRAME_LENGTH_LINES			0x300A
+#define		APT_FRAME_LENGTH_LINES_VALUE	0x08CA
+#define APT_COARSE_INTEGRATION_TIME		0x3012
+#define		APT_COARSE_INTEGRATION_TIME_DEFAULT 0x0384
+#define APT_FINE_INTEGRATION_TIME		0x3014
+#define		APT_FINE_INTEGRATION_TIME_VALUE	0
+
+#define APT_RESET_REGISTER			0x301A
+#define		APT_RESET_DO_RESET		BIT(0)
+#define		APT_RESET_ENABLE_STREAMING	BIT(2)
+/* Can be cleared to make some registers writeable. Not recommended. */
+#define		APT_RESET_LOCK_REG		BIT(3)
+/* When switching between serial vs. parallel output, a few bits are dependent.
+ * 6 -> drive the parallel pins
+ * 7 -> enable the parallel interface
+ * 12 -> disable the serializer
+ */
+#define		APT_RESET_PARALLEL_MODE		(BIT(6) | BIT(7) | BIT(12))
+/* Enable a number of input pins including OE_BAR, TRIGGER and STANDBY. */
+#define		APT_RESET_GPI_ENABLE		BIT(8)
+
+#define APT_ROW_SPEED				0x3028
+#define APT_VT_PIX_CLK_DIV			0x302A
+#define APT_VT_SYS_CLK_DIV			0x302C
+#define APT_PRE_PLL_CLK_DIV			0x302E
+#define APT_PLL_MULTIPLIER			0x3030
+#define APT_DIGITAL_BINNING			0x3032
+
+#define APT_READ_MODE				0x3040
+/* row + column binning: (context B at bits 10:11) */
+#define		APT_READ_MODE_BINNING		(BIT(12) | BIT(13))
+#define		APT_READ_MODE_HFLIP		BIT(14)
+#define		APT_READ_MODE_VFLIP		BIT(15)
+
+#define APT_FLASH				0x3046
+#define		APT_FLASH_ENABLED		0x0100
+
+#define APT_GLOBAL_GAIN				0x305E
+#define AR0141CS_ANALOG_GAIN			0x3060
+
+#define APT_EMBEDDED_DATA_CTRL			0x3064
+
+#define APT_OPERATION_MODE_CTRL			0x3082
+#define APT_X_ODD_INC				0x30A2
+#define APT_Y_ODD_INC				0x30A6
+
+#define APT_DIGITAL_TEST			0x30B0
+/* MT9M02X-only: analogue gain at 5:4 (A) and 9:8 (B) */
+#define		APT_DIGITAL_TEST_MONOCHROME	BIT(7)
+
+#define APT_TEMP_VALUE				0x30B2
+#define APT_TEMP				0x30B4
+#define APT_TEMP_CALIB1				0x30C6
+#define APT_TEMP_CALIB2				0x30C8
+#define APT_HISPI_CONTROL_STATUS		0x31C6
+#define APT_HDR_COMPANDING			0x31D0
+#define APT_COLUMN_CORRECTION			0x30D4
+
+#define APT_DIGITAL_CTRL			0x30BA
+#define		APT_DIGITAL_CTRL_DITHERING	BIT(5)
+
+#define APT_DIGITAL_LATERAL			0x3190
+
+#define APT_DARK_CONTROL			0x3044
+#define		APT_DARK_CONTROL_ROW_NOISE	BIT(10)
+
+#define APT_SEQ_CTRL_PORT			0x3088
+/* GENMASK(8, 0): Sequencer RAM address. Number of bits depends on chip. */
+/* BIT(14): defines whether reading APT_SEQ_DATA_PORT advances position */
+#define		APT_SEQ_CTRL_PORT_STOPPED	BIT(15)
+
+#define APT_SEQ_DATA_PORT			0x3086
+#define APT_ERS_PROG_START_ADDR			0x309E
+#define APT_DATA_PEDESTAL			0x301E
+#define APT_DAC_LD_14_15			0x3EDA
+#define APT_DAC_LD_18_19			0x3EDE
+#define APT_DAC_LD_12_13			0x3ED8
+#define APT_DAC_LD_22_23			0x3EE2
+#define APT_DAC_LD_20_21			0x3EE0
+#define APT_DAC_LD_16_17			0x3EDC
+#define APT_DAC_LD_26_27			0x3EE6
+#define APT_DAC_LD_24_25			0x3EE4
+#define APT_DAC_LD_10_11			0x3ED6
+#define APT_ADC_BITS_6_7			0x30E4
+#define APT_ADC_BITS_4_5			0x30E2
+#define APT_ADC_BITS_2_3			0x30E0
+#define APT_ADC_CONFIG1				0x30E6
+#define APT_ADC_CONFIG2				0x30E8
+
+/* can be supplied via struct i2c_client ->dev.platform_data or device tree.
+ * TODO(mainline): Move this structure to a header such that another driver
+ *      can supply it. Though one only needs that when not using DT.
+ */
+struct mt9m02x_platform_data {
+	unsigned int ext_freq; /* in Hz */
+	unsigned int pix_clock; /* in Hz */
+};
+
+struct mt9m02x_state;
+
+/**
+ * struct reg_entry - a configuration option for an imager
+ * @reg:   The register id. Must be a valid u16 or the special value MSLEEP.
+ * @value: The value to write into the register or the duration of MSLEEP in ms.
+ */
+struct reg_entry {
+	s32 reg;
+	u16 value;
+};
+
+#define MSLEEP -1
+
+struct chip_info {
+	u16 version;
+
+	const struct aptina_pll_limits *pll_limits;
+	/* Number of ext_clock cycles to wait after lifting reset_gpio. */
+	unsigned int reset_wait_cycles;
+
+	/* Initialization of the sequencer RAM */
+	const u16 *sequencer;
+	size_t sequencer_length;
+	const u16 *linear_sequencer;
+	size_t linear_sequencer_length;
+	/* Static configuration */
+	const struct reg_entry *configuration;
+	size_t configuration_length;
+
+	u16 width, height;
+	u16 xstart, ystart;
+
+	/* Digital gain is a fixed-point number with chip-dependent number of
+	 * bits before and after the point. The v4l2 ctrl expects 8 bits after
+	 * the point. v4l2value >> digital_gain_shift gives the register value.
+	 * digital_gain_min is the minimum v4l2 value.
+	 * (1 << digital_gain_bits) - 1 is the maximum register value.
+	 */
+	u16 digital_gain_shift, digital_gain_min, digital_gain_bits;
+
+	/* Array of valid V4L2_CID_ISO_SENSITIVITY values sized
+	 * again_menu_length.
+	 */
+	const s64 *again_menu;
+	size_t again_menu_length;
+	/* Given a valid index into again_menu, set the corresponding gain. */
+	int (*again_set)(struct mt9m02x_state *state, s32 index);
+
+	/* Temperature sensor calibration */
+	long temp_point1, temp_point2; /* milli degree C */
+	u16 temp_mask; /* register mask, 0 -> disable temperature sensor */
+};
+
+struct model_info {
+	const struct chip_info *chip;
+	bool monochrome;
+};
+
+struct mt9m02x_state {
+	struct i2c_client *client;
+	struct gpio_desc *reset_gpio, *standby_gpio;
+
+	/* Only written to while probing: */
+	const struct model_info *model;
+	struct aptina_pll pll;
+	/* register values, equal points -> disabled */
+	u16 temp_value1, temp_value2;
+
+	/* Protected by ctrls.lock: */
+	bool binning;
+
+	struct v4l2_subdev sd;
+	struct media_pad pad;
+	struct v4l2_ctrl_handler ctrls;
+	struct {
+		struct v4l2_ctrl *hflip, *vflip;
+	} mode_cluster;
+	struct {
+		struct v4l2_ctrl *wdr, *ratio_t1t2, *ratio_t2t3;
+	} opmode_cluster;
+	struct v4l2_ctrl *digital_gain_ctrl;
+	struct v4l2_ctrl *exposure_ctrl;
+};
+
+static const u16 ar0141cs_sequencer[] = {
+	0x4558, 0x6E9B, 0x4A31, 0x4342, 0x8E03, 0x2714, 0x4578, 0x7B3D,
+	0xFF3D, 0xFF3D, 0xEA27, 0x043D, 0x1027, 0x0527, 0x1535, 0x2705,
+	0x3D10, 0x4558, 0x2704, 0x2714, 0x3DFF, 0x3DFF, 0x3DEA, 0x2704,
+	0x6227, 0x288E, 0x0036, 0x2708, 0x3D64, 0x7A3D, 0x0444, 0x2C4B,
+	0x8F01, 0x4372, 0x719F, 0x4643, 0x166F, 0x9F92, 0x1244, 0x1646,
+	0x4316, 0x9326, 0x0426, 0x848E, 0x0327, 0xFC5C, 0x0D57, 0x5417,
+	0x0955, 0x5649, 0x5F53, 0x0553, 0x0728, 0x6C4C, 0x0928, 0x2C72,
+	0xA37C, 0x9728, 0xA879, 0x6026, 0x9C5C, 0x1B45, 0x4845, 0x0845,
+	0x8826, 0xBE8E, 0x0127, 0xF817, 0x0227, 0xFA17, 0x095C, 0x0B17,
+	0x1026, 0xBA5C, 0x0317, 0x1026, 0xB217, 0x065F, 0x2888, 0x9060,
+	0x27F2, 0x1710, 0x26A2, 0x26A3, 0x5F4D, 0x2808, 0x1A27, 0xFA84,
+	0x69A0, 0x785D, 0x2888, 0x8710, 0x8C82, 0x8926, 0xB217, 0x036B,
+	0x9C60, 0x9417, 0x2926, 0x8345, 0xA817, 0x0727, 0xFB17, 0x2945,
+	0x8820, 0x1708, 0x27FA, 0x5D87, 0x108C, 0x8289, 0x170E, 0x4826,
+	0x9A28, 0x884C, 0x0B79, 0x1730, 0x2692, 0x1709, 0x9160, 0x27F2,
+	0x1710, 0x2682, 0x2683, 0x5F4D, 0x2808, 0x1A27, 0xFA84, 0x69A1,
+	0x785D, 0x2888, 0x8710, 0x8C80, 0x8A26, 0x9217, 0x036B, 0x9D95,
+	0x2603, 0x5C01, 0x4558, 0x8E00, 0x2798, 0x170A, 0x4A0A, 0x4316,
+	0x0B43, 0x5B43, 0x1659, 0x4316, 0x8E03, 0x279C, 0x4578, 0x1707,
+	0x279D, 0x1722, 0x5D87, 0x1028, 0x0853, 0x0D8C, 0x808A, 0x4558,
+	0x1708, 0x8E01, 0x2798, 0x8E00, 0x76A2, 0x77A2, 0x4644, 0x1616,
+	0x967A, 0x2644, 0x5C05, 0x1244, 0x4B71, 0x759E, 0x8B86, 0x184A,
+	0x0343, 0x1606, 0x4316, 0x0743, 0x1604, 0x4316, 0x5843, 0x165A,
+	0x4316, 0x4558, 0x8E03, 0x279C, 0x4578, 0x7B17, 0x0727, 0x9D17,
+	0x2245, 0x5822, 0x1710, 0x8E01, 0x2798, 0x8E00, 0x1710, 0x1244,
+	0x4B8D, 0x602C, 0x2C2C, 0x2C00,
+};
+
+// A-1000 Hidy and linear sequencer load August 2 2011
+static const u16 sequencer_mt9m024[] = {
+	0x0025, 0x5050, 0x2D26, 0x0828, 0x0D17, 0x0926, 0x0028, 0x0526,
+	0xA728, 0x0725, 0x8080, 0x2925, 0x0040, 0x2702, 0x1616, 0x2706,
+	0x1F17, 0x3626, 0xA617, 0x0326, 0xA417, 0x1F28, 0x0526, 0x2028,
+	0x0425, 0x2020, 0x2700, 0x171D, 0x2500, 0x2017, 0x1219, 0x1703,
+	0x2706, 0x1728, 0x2805, 0x171A, 0x2660, 0x175A, 0x2317, 0x1122,
+	0x1741, 0x2500, 0x9027, 0x0026, 0x1828, 0x002E, 0x2A28, 0x081C,
+	0x1470, 0x7003, 0x1470, 0x7004, 0x1470, 0x7005, 0x1470, 0x7009,
+	0x170C, 0x0014, 0x0020, 0x0014, 0x0050, 0x0314, 0x0020, 0x0314,
+	0x0050, 0x0414, 0x0020, 0x0414, 0x0050, 0x0514, 0x0020, 0x2405,
+	0x1400, 0x5001, 0x2550, 0x502D, 0x2608, 0x280D, 0x1709, 0x2600,
+	0x2805, 0x26A7, 0x2807, 0x2580, 0x8029, 0x2500, 0x4027, 0x0216,
+	0x1627, 0x0620, 0x1736, 0x26A6, 0x1703, 0x26A4, 0x171F, 0x2805,
+	0x2620, 0x2804, 0x2520, 0x2027, 0x0017, 0x1D25, 0x0020, 0x1712,
+	0x1A17, 0x0327, 0x0617, 0x2828, 0x0517, 0x1A26, 0x6017, 0xAE25,
+	0x0090, 0x2700, 0x2618, 0x2800, 0x2E2A, 0x2808, 0x1D05, 0x1470,
+	0x7009, 0x1720, 0x1400, 0x2024, 0x1400, 0x5002, 0x2550, 0x502D,
+	0x2608, 0x280D, 0x1709, 0x2600, 0x2805, 0x26A7, 0x2807, 0x2580,
+	0x8029, 0x2500, 0x4027, 0x0216, 0x1627, 0x0617, 0x3626, 0xA617,
+	0x0326, 0xA417, 0x1F28, 0x0526, 0x2028, 0x0425, 0x2020, 0x2700,
+	0x171D, 0x2500, 0x2021, 0x1712, 0x1B17, 0x0327, 0x0617, 0x2828,
+	0x0517, 0x1A26, 0x6017, 0xAE25, 0x0090, 0x2700, 0x2618, 0x2800,
+	0x2E2A, 0x2808, 0x1E17, 0x0A05, 0x1470, 0x7009, 0x1616, 0x1616,
+	0x1616, 0x1616, 0x1616, 0x1616, 0x1616, 0x1616, 0x1616, 0x1616,
+	0x1616, 0x1616, 0x1616, 0x1614, 0x0020, 0x2414, 0x0050, 0x2B2B,
+	0x2C2C, 0x2C2C, 0x2C00,
+};
+
+static const u16 sequencer_mt9m024_linear[] = {
+	0x0225, 0x5050, 0x2D26, 0x0828, 0x0D17, 0x0926, 0x0028, 0x0526,
+	0xA728, 0x0725, 0x8080, 0x2917, 0x0525, 0x0040, 0x2702, 0x1616,
+	0x2706, 0x1736, 0x26A6, 0x1703, 0x26A4, 0x171F, 0x2805, 0x2620,
+	0x2804, 0x2520, 0x2027, 0x0017, 0x1E25, 0x0020, 0x2117, 0x1028,
+	0x051B, 0x1703, 0x2706, 0x1703, 0x1747, 0x2660, 0x17AE, 0x2500,
+	0x9027, 0x0026, 0x1828, 0x002E, 0x2A28, 0x081E, 0x0831, 0x1440,
+	0x4014, 0x2020, 0x1410, 0x1034, 0x1400, 0x1014, 0x0020, 0x1400,
+	0x4013, 0x1802, 0x1470, 0x7004, 0x1470, 0x7003, 0x1470, 0x7017,
+	0x2002, 0x1400, 0x2002, 0x1400, 0x5004, 0x1400, 0x2004, 0x1400,
+	0x5022, 0x0314, 0x0020, 0x0314, 0x0050, 0x2C2C, 0x2C2C,
+};
+
+static const struct reg_entry ar0141cs_configuration[] = {
+	/* Optimized and Sequencer settings (referenced by AR0141CS-REV1.ini) */
+	{APT_DARK_CONTROL,		APT_DARK_CONTROL_ROW_NOISE},
+	{0x3052,			0xA124},
+	{0x3092,			0x010F},
+	{0x30FE,			0x0080},
+	{0x3ECE,			0x40FF},
+	{0x3ED0,			0xFF40},
+	{0x3ED2,			0xA906},
+	{0x3ED4,			0x001F},
+	{0x3ED6,			0x638F},
+	{0x3ED8,			0xCC99},
+	{0x3EDA,			0x0888},
+	{0x3EDE,			0x8878},
+	{0x3EE0,			0x7744},
+	{0x3EE2,			0x4463},
+	{0x3EE4,			0xAAE0},
+	{0x3EE6,			0x1400},
+	{0x3EEA,			0xA4FF},
+	{0x3EEC,			0x80F0},
+	{0x3EEE,			0x0000},
+	{0x31E0,			0x1701},
+	{0x304C,			0x2000},
+	{0x304A,			0x0210},
+	/* It's not clear why and how long we need to sleep here. If we don't,
+	 * i2c writes start to fail.
+	 */
+	{MSLEEP,			200},
+	/* imager configuration */
+	{APT_ROW_SPEED,			0x0010},
+	{APT_HDR_COMPANDING,		0x0000},
+	{APT_DIGITAL_CTRL,		APT_DIGITAL_CTRL_DITHERING | 0x010C},
+	{0x31AC,			0x0C0C}, /* data_format_bits */
+	/* serial format:
+	 * GENMASK(9, 8) must be 3, selects HiSPi if serial interface is enabled
+	 * GENMASK(2, 0) line-count, 1 -> parallel
+	 */
+	{0x31AE,			0x0301},
+	{APT_FRAME_LENGTH_LINES,	APT_FRAME_LENGTH_LINES_VALUE},
+	{APT_LINE_LENGTH_PCK,		APT_LINE_LENGTH_PCK_VALUE},
+	{APT_EMBEDDED_DATA_CTRL,	0x1802},
+};
+
+static const struct reg_entry mt9m024_configuration[] = {
+	/* OnSemi A-1000ERS Optimized settings (August 2 2011),
+	 * but with a reduced pedestal for better black level
+	 */
+	{APT_DATA_PEDESTAL,		0x00A8},
+	{APT_DAC_LD_14_15,		0x0F03},
+	{APT_DAC_LD_18_19,		0xC005},
+	{APT_DAC_LD_12_13,		0x09EF},
+	{APT_DAC_LD_22_23,		0xA46B},
+	{APT_DAC_LD_20_21,		0x067D},
+	{APT_DAC_LD_16_17,		0x0070},
+	{APT_DAC_LD_26_27,		0x8303},
+	{APT_DAC_LD_24_25,		0xD208},
+	{APT_DAC_LD_10_11,		0x00BD},
+	{APT_ADC_BITS_6_7,		0x6372},
+	{APT_ADC_BITS_4_5,		0x7253},
+	{APT_ADC_BITS_2_3,		0x5470},
+	{APT_ADC_CONFIG1,		0xC4CC},
+	{APT_ADC_CONFIG2,		0x8050},
+	/* imager configuration */
+	{APT_ROW_SPEED,			0x0010},
+	{APT_DIGITAL_BINNING,		0x0000},
+	{APT_COLUMN_CORRECTION,		0xE007},
+	{APT_DIGITAL_CTRL,		0x0008},
+	{APT_FRAME_LENGTH_LINES,	APT_FRAME_LENGTH_LINES_VALUE},
+	{APT_LINE_LENGTH_PCK,		APT_LINE_LENGTH_PCK_VALUE},
+	{APT_FINE_INTEGRATION_TIME,	APT_FINE_INTEGRATION_TIME_VALUE},
+	{APT_Y_ODD_INC,			0x0001},
+	{APT_EMBEDDED_DATA_CTRL,	0x1802},
+	{APT_HISPI_CONTROL_STATUS,	0x8008},
+	{APT_DARK_CONTROL,		APT_DARK_CONTROL_ROW_NOISE | 4},
+	/* BIT(0): off/on
+	 * BIT(1): 12bit/14bit
+	 */
+	{APT_HDR_COMPANDING,		BIT(0)}, /* 12bit enabled */
+};
+
+static const struct aptina_pll_limits ar0141cs_limits = {
+	.ext_clock_min = 6000000,
+	.ext_clock_max = 50000000,
+	/* The int_clock is not bounded by the data sheet. */
+	.int_clock_min = 1,
+	.int_clock_max = 50000000,
+	.out_clock_min = 384000000,
+	.out_clock_max = 768000000,
+	.pix_clock_max = 74250000,
+
+	.n_min = 1,
+	.n_max = 63,
+	.m_min = 32,
+	/* The data sheet revision 7 page 16 says that the maximum multiplier
+	 * is 384. However the register reference revision 3 page 6 say that
+	 * the corresponding register has only 8 bits.
+	 */
+	.m_max = 255,
+	/* We fix p1 = 1 and control p2 here. */
+	.p1_min = 4,
+	.p1_max = 16,
+};
+
+static const struct aptina_pll_limits mt9m024_limits = {
+	.ext_clock_min = 6000000,
+	.ext_clock_max = 50000000,
+	.int_clock_min = 2000000,
+	.int_clock_max = 24000000,
+	.out_clock_min = 384000000,
+	.out_clock_max = 768000000,
+	.pix_clock_max = 74250000,
+
+	.n_min = 1,
+	.n_max = 63,
+	.m_min = 32,
+	.m_max = 255,
+	/* We fix p1 = 1 and control p2 here. */
+	.p1_min = 4,
+	.p1_max = 16,
+};
+
+static int read_word_from_i2c_device(struct i2c_client *client, u16 register_id,
+				     u16 *return_value)
+{
+	__be16 reg_buffer, val_buffer;
+	int ret = 0;
+	struct i2c_msg msg[2];
+
+	if (!client || !return_value)
+		return -EINVAL;
+
+	reg_buffer = cpu_to_be16(register_id);
+
+	msg[0].addr  = client->addr;
+	msg[0].flags = 0;
+	msg[0].buf   = (u8 *)&reg_buffer;
+	msg[0].len   = sizeof(reg_buffer);
+
+	msg[1].addr  = client->addr;
+	msg[1].flags = I2C_M_RD;
+	msg[1].buf   = (u8 *)&val_buffer;
+	msg[1].len   = sizeof(val_buffer);
+
+	ret = i2c_transfer(client->adapter, msg, ARRAY_SIZE(msg));
+
+	if (ret == 2) {
+		*return_value = be16_to_cpu(val_buffer);
+		return 0;
+	} else if (ret >= 0) {
+		return -EINTR;
+	}
+	return ret;
+}
+
+static int write_word_to_i2c_device(struct i2c_client *client, u16 register_id,
+				    u16 value)
+{
+	__be16 buffer[2];
+	int ret;
+
+	if (!client)
+		return -EINVAL;
+
+	buffer[0] = cpu_to_be16(register_id);
+	buffer[1] = cpu_to_be16(value);
+
+	ret = i2c_master_send(client, (const char *)&buffer, sizeof(buffer));
+	if (ret == 4)
+		return 0;
+	else if (ret >= 0)
+		return -EINTR;
+	return ret;
+}
+
+static int write_i2c_word_sequence(struct i2c_client *client,
+				   const struct reg_entry *data,
+				   size_t count)
+{
+	while (count) {
+		if (data->reg == MSLEEP) {
+			msleep(data->value);
+		} else if (data->reg >= 0 && data->reg <= 0xffff) {
+			int ret = write_word_to_i2c_device(client, data->reg,
+							   data->value);
+			if (ret) {
+				dev_err(&client->dev,
+					"write register of image sensor failed (reg=0x%x value=0x%x, err=%d)!\n",
+					data->reg, data->value, ret);
+				return ret;
+			}
+		} else {
+			return -EINVAL;
+		}
+
+		--count;
+		++data;
+	}
+	return 0;
+}
+
+/* Refer to the AR0141CS data sheet revision 7 page 15 for details on
+ * the gain computation. The following macro is another way of writing:
+ * 100000 * pow(2, coarse) * (1 + fine/16)
+ */
+#define G2ISO(coarse, fine) \
+	((1 << (coarse)) * (100000 + (100000 / 16) * (fine)))
+static const s64 ar0141cs_again_menu[] = {
+	G2ISO(0, 0),  G2ISO(0, 1),  G2ISO(0, 2),  G2ISO(0, 3),
+	G2ISO(0, 4),  G2ISO(0, 5),  G2ISO(0, 6),  G2ISO(0, 7),
+	G2ISO(0, 8),  G2ISO(0, 9),  G2ISO(0, 10), G2ISO(0, 11),
+	G2ISO(0, 12), G2ISO(0, 13), G2ISO(0, 14), G2ISO(0, 15),
+	G2ISO(1, 0),  G2ISO(1, 1),  G2ISO(1, 2),  G2ISO(1, 3),
+	G2ISO(1, 4),  G2ISO(1, 5),  G2ISO(1, 6),  G2ISO(1, 7),
+	G2ISO(1, 8),  G2ISO(1, 9),  G2ISO(1, 10), G2ISO(1, 11),
+	G2ISO(1, 12), G2ISO(1, 13), G2ISO(1, 14), G2ISO(1, 15),
+	G2ISO(2, 0),  G2ISO(2, 1),  G2ISO(2, 2),  G2ISO(2, 3),
+	G2ISO(2, 4),  G2ISO(2, 5),  G2ISO(2, 6),  G2ISO(2, 7),
+	G2ISO(2, 8),  G2ISO(2, 9),  G2ISO(2, 10), G2ISO(2, 11),
+	G2ISO(2, 12), G2ISO(2, 13), G2ISO(2, 14), G2ISO(2, 15),
+	G2ISO(3, 0),  G2ISO(3, 1),  G2ISO(3, 2),  G2ISO(3, 3),
+	G2ISO(3, 4),  G2ISO(3, 5),  G2ISO(3, 6),  G2ISO(3, 7),
+	G2ISO(3, 8),
+	/* The last gain corresponds to 12x (8x coarse times 1.5x fine). It is
+	 * the maximum recommended gain in the AR0141CS data sheet revision 7
+	 * page 15.
+	 */
+};
+#undef G2I
+
+static int ar0141cs_again_set(struct mt9m02x_state *state, s32 index)
+{
+	/* The 4 low bits of index hold the fine gain. The next 3 bits hold the
+	 * coarse gain. That means we're lucky. Context A uses exactly the same
+	 * layout. Context B has the same representation shifted by 8.
+	 */
+	return write_word_to_i2c_device(state->client, AR0141CS_ANALOG_GAIN,
+					index);
+}
+
+static const s64 mt9m02x_again_menu[] = {
+	100000, /* 1x */
+	125000, /* 1.25x */
+	200000, /* 2x */
+	250000, /* 2.5x */
+	400000, /* 4x */
+	500000, /* 5x */
+	800000, /* 8x */
+	1000000, /* 10x */
+};
+
+static int mt9m02x_again_set(struct mt9m02x_state *state, s32 index)
+{
+	int ret;
+	u16 v = BIT(12);
+
+	if (state->model->monochrome)
+		v |= APT_DIGITAL_TEST_MONOCHROME;
+
+	/* Bank A, ignore bank B (shift 8 instead of 4) for now. */
+	v &= ~(3 << 4);
+	/* index >> 1 is the stage1 gain in powers of 2. */
+	v |= (index >> 1) << 4;
+	ret = write_word_to_i2c_device(state->client, APT_DIGITAL_TEST, v);
+	if (ret)
+		return ret;
+
+	/* set stage2, multiplier */
+	ret = read_word_from_i2c_device(state->client, APT_DAC_LD_24_25, &v);
+	if (ret)
+		return ret;
+
+	/* The least significant bit controls the 1.25x amplifier,
+	 * which is bank-independent.
+	 */
+	if (index & 1)
+		v |= BIT(8);
+	else
+		v &= ~BIT(8);
+	v |= BIT(9);
+
+	return write_word_to_i2c_device(state->client, APT_DAC_LD_24_25, v);
+}
+
+static const struct chip_info chip_ar0141cs = {
+	.version = 0x0151,
+	.pll_limits = &ar0141cs_limits,
+	.reset_wait_cycles = 1800,
+	.sequencer = ar0141cs_sequencer,
+	.sequencer_length = ARRAY_SIZE(ar0141cs_sequencer),
+	.configuration = ar0141cs_configuration,
+	.configuration_length = ARRAY_SIZE(ar0141cs_configuration),
+	.width = 1280,
+	.height = 800,
+	.xstart = 32,
+	.ystart = 24,
+	.digital_gain_shift = 8 - 7,
+	/* When setting a gain below 1.375, we see static noise. */
+	.digital_gain_min = 0x160, /* 1.375 * BIT(8) */
+	.digital_gain_bits = 11,
+	.again_menu = ar0141cs_again_menu,
+	.again_menu_length = ARRAY_SIZE(ar0141cs_again_menu),
+	.again_set = &ar0141cs_again_set,
+};
+
+static const struct model_info model_ar0141cs = {
+	.chip = &chip_ar0141cs,
+	.monochrome = false,
+};
+
+static const struct model_info model_ar0141csm = {
+	.chip = &chip_ar0141cs,
+	.monochrome = true,
+};
+
+static const struct chip_info chip_mt9m024 = {
+	.version = 0x2400,
+	.pll_limits = &mt9m024_limits,
+	.sequencer = sequencer_mt9m024,
+	.sequencer_length = ARRAY_SIZE(sequencer_mt9m024),
+	.linear_sequencer = sequencer_mt9m024_linear,
+	.linear_sequencer_length = ARRAY_SIZE(sequencer_mt9m024_linear),
+	.configuration = mt9m024_configuration,
+	.configuration_length = ARRAY_SIZE(mt9m024_configuration),
+	.width = 1280,
+	.height = 960,
+	.digital_gain_shift = 8 - 5,
+	.digital_gain_bits = 8,
+	.again_menu = mt9m02x_again_menu,
+	.again_menu_length = ARRAY_SIZE(mt9m02x_again_menu),
+	.again_set = &mt9m02x_again_set,
+	.temp_point1 = 70000,
+	.temp_point2 = 55000,
+	.temp_mask = GENMASK(10, 0),
+};
+
+static const struct model_info model_mt9m024 = {
+	.chip = &chip_mt9m024,
+	.monochrome = false,
+};
+
+static const struct model_info model_mt9m024m = {
+	.chip = &chip_mt9m024,
+	.monochrome = true,
+};
+
+static int write_sequencer(struct i2c_client *client, const u16 *data,
+			   size_t length)
+{
+	int ret;
+
+	while (length > 0) {
+		ret = write_word_to_i2c_device(client, APT_SEQ_DATA_PORT,
+					       *data);
+		if (ret)
+			return ret;
+		++data;
+		--length;
+	}
+	return 0;
+}
+
+static int initialize_sequencer(struct mt9m02x_state *state)
+{
+	u16 temp_value;
+	int ret;
+	const struct chip_info *chip = state->model->chip;
+
+	if (!chip->sequencer && !chip->linear_sequencer)
+		return 0;
+
+	ret = read_word_from_i2c_device(state->client, APT_SEQ_CTRL_PORT,
+					&temp_value);
+	if (ret)
+		return ret;
+	if ((temp_value & APT_SEQ_CTRL_PORT_STOPPED) == 0) {
+		dev_err(&state->client->dev,
+			"attempt to write sequencer without standby.\n");
+		return -EIO;
+	}
+
+	/* Rewind sequencer RAM address. */
+	if (temp_value != APT_SEQ_CTRL_PORT_STOPPED) {
+		ret = write_word_to_i2c_device(state->client, APT_SEQ_CTRL_PORT,
+					       APT_SEQ_CTRL_PORT_STOPPED);
+		if (ret)
+			return ret;
+	}
+
+	if (chip->sequencer) {
+		ret = write_sequencer(state->client, chip->sequencer,
+				      chip->sequencer_length);
+		if (ret)
+			return ret;
+	}
+	if (chip->linear_sequencer) {
+		ret = write_sequencer(state->client, chip->linear_sequencer,
+				      chip->linear_sequencer_length);
+		if (ret)
+			return ret;
+		/* Write start address of the linear sequencer in bytes. */
+		ret = write_word_to_i2c_device(state->client,
+					       APT_ERS_PROG_START_ADDR,
+					       2 * chip->sequencer_length);
+		if (ret)
+			return ret;
+	}
+	return 0;
+}
+
+static int mt9m02x_update_read_mode(struct mt9m02x_state *state)
+{
+	u16 v = 0;
+
+	if (state->binning)
+		v |= APT_READ_MODE_BINNING;
+	if (state->mode_cluster.hflip->val)
+		v |= APT_READ_MODE_HFLIP;
+	if (state->mode_cluster.vflip->val)
+		v |= APT_READ_MODE_VFLIP;
+	return write_word_to_i2c_device(state->client, APT_READ_MODE, v);
+}
+
+static int ar0141cs_update_odd_inc(struct mt9m02x_state *state)
+{
+	u16 inc = state->binning ? 3 : 1;
+	int ret = write_word_to_i2c_device(state->client, APT_X_ODD_INC, inc);
+
+	if (ret)
+		return ret;
+	return write_word_to_i2c_device(state->client, APT_Y_ODD_INC, inc);
+}
+
+static int initialize_temperature_sensor(struct mt9m02x_state *state)
+{
+	int ret;
+	u16 reg;
+
+	if (!state->model->chip->temp_mask)
+		return -EINVAL;
+
+	ret = read_word_from_i2c_device(state->client, APT_TEMP, &reg);
+	if (ret) {
+		dev_err(&state->client->dev,
+			"reading from temperature register failed (%d)", ret);
+		return ret;
+	}
+
+	ret = write_word_to_i2c_device(state->client, APT_TEMP, reg | 0x11);
+	if (ret) {
+		dev_err(&state->client->dev,
+			"writing to temperature register failed (%d)", ret);
+		return ret;
+	}
+
+	ret = read_word_from_i2c_device(state->client, APT_TEMP_CALIB1,
+					&state->temp_value1);
+
+	if (!ret)
+		ret = read_word_from_i2c_device(state->client, APT_TEMP_CALIB2,
+						&state->temp_value2);
+
+	if (ret) {
+		dev_err(&state->client->dev,
+			"reading temperature calibration values failed (%d)",
+			ret);
+
+		/* disable */
+		state->temp_value1 = 0;
+		state->temp_value2 = 0;
+		return ret;
+	}
+
+	dev_dbg(&state->client->dev, "temp calib: %ldmC -> %u, %ldmC -> %u",
+		state->model->chip->temp_point1,
+		(unsigned int)state->temp_value1,
+		state->model->chip->temp_point2,
+		(unsigned int)state->temp_value2);
+
+	if (state->temp_value1 == state->temp_value2) {
+		dev_err(&state->client->dev,
+			"bad temperature calibration values");
+		return -EIO;
+	}
+
+	dev_dbg(&state->client->dev, "temperature sensor enabled");
+	return 0;
+}
+
+static void sleep_extcycles(const struct mt9m02x_state *state,
+			    unsigned long cycles,
+			    unsigned long us)
+{
+	/* We'd want mult_frac(cycles, 1000000, ext_clock), but that can cause
+	 * an integer overflow.
+	 */
+	WARN_ON(cycles >= ~0UL / 1000);
+	us += mult_frac(cycles, 1000, state->pll.ext_clock / 1000);
+	usleep_range(us, us + 1000);
+}
+
+static void hard_reset(struct mt9m02x_state *state)
+{
+	/* MT9M024 datasheet rev G: see page 59
+	 * AR0141CS datasheet rev 7: see page 45
+	 */
+	gpiod_set_value_cansleep(state->reset_gpio, 1);
+	/* MT9M024 and AR0141CS need at least 1ms. */
+	usleep_range(1000, 2000);
+	gpiod_set_value_cansleep(state->reset_gpio, 0);
+	/* Give an additional 1ms for the GPIO to settle. */
+	sleep_extcycles(state, state->model->chip->reset_wait_cycles, 1000);
+}
+
+static int init_image_sensor(struct mt9m02x_state *state)
+{
+	int ret = 0;
+	u16 temp_value;
+
+	if (state->standby_gpio)
+		gpiod_set_value_cansleep(state->standby_gpio, 0);
+	if (state->reset_gpio)
+		hard_reset(state);
+
+	// init image sensor
+	ret = read_word_from_i2c_device(state->client, APT_CHIP_VERSION,
+					&temp_value);
+	if (ret != 0) {
+		dev_err(&state->client->dev,
+			"read from chip version register failed\n");
+
+		return ret;
+	}
+
+	/* AR0141CS has a documented value of 0x151, but it sometimes reports
+	 * 0x51. It is unclear why, but this is even the case for the reference
+	 * design. Assumption: Hardware bug.
+	 */
+	if (temp_value == 0x51)
+		temp_value = 0x151;
+
+	if (temp_value != state->model->chip->version) {
+		dev_err(&state->client->dev, "expected chip %x found %x\n",
+			state->model->chip->version, temp_value);
+		return -ENODEV;
+	}
+
+	ret = aptina_pll_calculate(&state->client->dev,
+				   state->model->chip->pll_limits,
+				   &state->pll);
+	if (ret)
+		return ret;
+
+	if (!state->reset_gpio) {
+		// reset_register_reset (full reset)
+		ret = write_word_to_i2c_device(state->client,
+					       APT_RESET_REGISTER,
+					       APT_RESET_DO_RESET);
+		if (ret != 0) {
+			dev_err(&state->client->dev,
+				"resetting image sensor failed\n");
+			return ret;
+		}
+
+		// wait for imager go out of reset
+		msleep(200);
+	}
+
+	// configure register_reset
+	ret = write_word_to_i2c_device(state->client, APT_RESET_REGISTER,
+				       APT_RESET_LOCK_REG |
+				       APT_RESET_PARALLEL_MODE |
+				       APT_RESET_GPI_ENABLE);
+	if (ret != 0) {
+		dev_err(&state->client->dev,
+			"configuring of imager \"register_reset\" failed\n");
+		return ret;
+	}
+
+	if (!state->reset_gpio)
+		// wait for imager go out of reset
+		msleep(200);
+
+	ret = initialize_sequencer(state);
+	if (ret)
+		return ret;
+
+	ret = write_i2c_word_sequence(state->client,
+				      state->model->chip->configuration,
+				      state->model->chip->configuration_length);
+	if (ret)
+		return ret;
+
+	if (state->model->chip == &chip_ar0141cs) {
+		ret = ar0141cs_update_odd_inc(state);
+		if (ret)
+			return ret;
+		ret = write_word_to_i2c_device(state->client, APT_DIGITAL_TEST,
+					       state->model->monochrome ?
+					       APT_DIGITAL_TEST_MONOCHROME : 0);
+		if (ret)
+			return ret;
+
+		/* AR0141CS datasheet rev 7 page 45:
+		 * Wait for OTPM after writing 0x304A.
+		 */
+		sleep_extcycles(state, 185135, 0);
+	} else if (state->model->chip == &chip_mt9m024) {
+		ret = read_word_from_i2c_device(state->client,
+						APT_DIGITAL_LATERAL,
+						&temp_value);
+		if (ret)
+			return ret;
+		// Bit 13 -> disable/enable dlo,
+		// Bit 14 -> disable/enable noise filter
+		temp_value |= BIT(13) | BIT(14);
+		ret = write_word_to_i2c_device(state->client,
+					       APT_DIGITAL_LATERAL,
+					       temp_value);
+		if (ret)
+			return ret;
+	}
+
+	{
+		struct reg_entry values[] = {
+			{ APT_X_ADDR_START, state->model->chip->xstart },
+			{ APT_Y_ADDR_START, state->model->chip->ystart },
+			{ APT_X_ADDR_END,
+			  state->model->chip->xstart +
+			  state->model->chip->width - 1 },
+			{ APT_Y_ADDR_END,
+			  state->model->chip->ystart +
+			  state->model->chip->height - 1 },
+		};
+		ret = write_i2c_word_sequence(state->client, values,
+					      ARRAY_SIZE(values));
+		if (ret)
+			return ret;
+	}
+
+	{
+		struct reg_entry values[] = {
+			{ APT_VT_PIX_CLK_DIV, state->pll.p1 },
+			{ APT_VT_SYS_CLK_DIV, 1 },
+			{ APT_PRE_PLL_CLK_DIV, state->pll.n },
+			{ APT_PLL_MULTIPLIER, state->pll.m },
+		};
+		ret = write_i2c_word_sequence(state->client, values,
+					      ARRAY_SIZE(values));
+		if (ret)
+			return ret;
+
+		/* AR0141CS datasheet rev 7 page 45
+		 * MT9M024 datasheet rev G page 59
+		 */
+		usleep_range(1000, 2000);
+	}
+
+	/* Enable flash strobe pin. Actual flash is activated/deactivated
+	 * elsewhere.
+	 */
+	ret = read_word_from_i2c_device(state->client, APT_FLASH, &temp_value);
+	if (ret)
+		return ret;
+	temp_value |= APT_FLASH_ENABLED;
+	ret = write_word_to_i2c_device(state->client, APT_FLASH, temp_value);
+	if (ret)
+		return ret;
+
+	/* Perform column correction triggering on startup (see datasheet
+	 * "AR0132AT-D.pdf" p.34).
+	 */
+	ret = read_word_from_i2c_device(state->client, APT_RESET_REGISTER,
+					&temp_value);
+	if (ret == 0) {
+		temp_value |= APT_RESET_ENABLE_STREAMING;
+
+		ret = write_word_to_i2c_device(state->client,
+					       APT_RESET_REGISTER, temp_value);
+		if (ret != 0)
+			dev_err(&state->client->dev,
+				"write to reset register failed (value=%u)\n",
+				(u32)temp_value);
+	} else {
+		dev_err(&state->client->dev,
+			"read from reset register failed\n");
+	}
+
+	if (ret != 0)
+		return ret;
+
+	// wait for 500ms -> 10 frames
+	msleep(500);
+
+	ret = read_word_from_i2c_device(state->client, APT_RESET_REGISTER,
+					&temp_value);
+	if (ret == 0) {
+		temp_value &= ~APT_RESET_ENABLE_STREAMING;
+
+		// stop sensor streaming
+		ret = write_word_to_i2c_device(state->client,
+					       APT_RESET_REGISTER, temp_value);
+		if (ret != 0)
+			dev_err(&state->client->dev,
+				"write to reset register failed (value=%u)\n",
+				(u32)temp_value);
+	} else {
+		dev_err(&state->client->dev,
+			"read from reset register failed\n");
+	}
+
+	if (ret != 0)
+		return ret;
+
+	/* optional, continue in the presence of errors */
+	initialize_temperature_sensor(state);
+
+	return 0;
+}
+
+static umode_t mt9m02x_hwmon_is_visible(const void *private_data,
+					enum hwmon_sensor_types type, u32 attr,
+					int channel)
+{
+	return type == hwmon_temp && attr == hwmon_temp_input ? 0444 : 0;
+}
+
+static int mt9m02x_hwmon_read(struct device *dev, enum hwmon_sensor_types type,
+			      u32 attr, int channel, long *val)
+{
+	struct mt9m02x_state *state = dev_get_drvdata(dev);
+	const struct chip_info *chip = state->model->chip;
+	int ret;
+	u16 reg_value;
+
+	if (type != hwmon_temp || attr != hwmon_temp_input)
+		return -EINVAL;
+
+	ret = read_word_from_i2c_device(state->client, APT_TEMP_VALUE,
+					&reg_value);
+	if (ret)
+		return ret;
+	reg_value &= chip->temp_mask;
+	/* reg_value is temp_valueX at temp_pointX milli degree C with linear
+	 * interpolation.
+	 */
+	*val = (((long)reg_value - (long)state->temp_value2) *
+		(chip->temp_point1 - chip->temp_point2)) /
+	       ((long)state->temp_value1 - (long)state->temp_value2) +
+	       chip->temp_point2;
+	return 0;
+}
+
+static const struct hwmon_ops mt9m02x_hwmon_ops = {
+	.is_visible = &mt9m02x_hwmon_is_visible,
+	.read = &mt9m02x_hwmon_read,
+};
+
+static const u32 mt9m02x_hwmon_temp_config[] = {
+	HWMON_T_INPUT,
+	0
+};
+
+static const struct hwmon_channel_info mt9m02x_hwmon_temp_channel = {
+	.type = hwmon_temp,
+	.config = mt9m02x_hwmon_temp_config,
+};
+
+static const struct hwmon_channel_info *mt9m02x_hwmon_channel_info[] = {
+	&mt9m02x_hwmon_temp_channel,
+	NULL
+};
+
+static const struct hwmon_chip_info mt9m02x_hwmon_info = {
+	.ops = &mt9m02x_hwmon_ops,
+	.info = mt9m02x_hwmon_channel_info,
+};
+
+static bool mt9m02x_is_wdr(struct mt9m02x_state *state)
+{
+	return state->model->chip == &chip_mt9m024 &&
+	       state->opmode_cluster.wdr->val;
+}
+
+static u32 mt9m02x_exposure_100us_to_coarse(struct mt9m02x_state *state, u32 v)
+{
+	u64 t = mul_u32_u32(v, state->pll.pix_clock);
+
+	/* ctrl->val is s/10000 and pix_clock is Hz. */
+	do_div(t, 10000);
+
+	/* MT9M024 datasheet rev. G page 24:
+	 * When HDR is enabled, fine integration time is ignored.
+	 */
+	if (!mt9m02x_is_wdr(state))
+		t -= APT_FINE_INTEGRATION_TIME_VALUE;
+	do_div(t, APT_LINE_LENGTH_PCK_VALUE);
+	dev_dbg(&state->client->dev,
+		"exposure = %u -> coarse integration = %llu\n", v, t);
+	return t;
+}
+
+static u32 mt9m02x_exposure_coarse_to_100us(struct mt9m02x_state *state, u32 v)
+{
+	u64 t = mul_u32_u32(v, APT_LINE_LENGTH_PCK_VALUE);
+
+	/* MT9M024 datasheet rev. G page 24:
+	 * When HDR is enabled, fine integration time is ignored.
+	 */
+	if (!mt9m02x_is_wdr(state))
+		t += APT_FINE_INTEGRATION_TIME_VALUE;
+	t = mul_u64_u32_div(t, 10000, state->pll.pix_clock);
+	dev_dbg(&state->client->dev,
+		"coarse integration = %u -> exposure = %llu\n", v, t);
+	return t;
+}
+
+static int mt9m02x_update_exposure_range(struct mt9m02x_state *state)
+{
+	u32 low, high, def;
+
+	if (mt9m02x_is_wdr(state)) {
+		/* MT9M024 datasheet rev. G, page 24:
+		 * t1/t2 = 4 << ratio_t1t2
+		 * t2/t3 = 4 << ratio_t2t3
+		 * low >= (t1/t2) * (t2/t3) * 0.5
+		 */
+		low = 8 << (state->opmode_cluster.ratio_t1t2->val +
+			    state->opmode_cluster.ratio_t2t3->val);
+		/* high <= 42 * (t1/t2) */
+		high = min(APT_FRAME_LENGTH_LINES_VALUE - 45,
+			   42 * 4 << state->opmode_cluster.ratio_t1t2->val);
+	} else {
+		/* MT9M024 datasheet rev. G, page 21: */
+		low = 2;
+		/* MT9M024 datasheet rev. G, page 24: */
+		high = APT_FRAME_LENGTH_LINES_VALUE - 1;
+	}
+	WARN_ON(low > high);
+
+	low = max_t(u32, 1, mt9m02x_exposure_coarse_to_100us(state, low));
+	high = mt9m02x_exposure_coarse_to_100us(state, high);
+	def = mt9m02x_exposure_coarse_to_100us(
+		state, APT_COARSE_INTEGRATION_TIME_DEFAULT);
+
+	return __v4l2_ctrl_modify_range(state->exposure_ctrl, low, high, 1,
+					clamp(def, low, high));
+}
+
+static int mt9m02x_s_ctrl(struct v4l2_ctrl *ctrl)
+{
+	struct mt9m02x_state *state =
+		container_of(ctrl->handler, struct mt9m02x_state, ctrls);
+	int ret;
+	u16 temp_value;
+
+	switch (ctrl->id) {
+	case V4L2_CID_ISO_SENSITIVITY:
+		return state->model->chip->again_set(state, ctrl->val);
+
+	case V4L2_CID_DIGITAL_GAIN:
+		if (mt9m02x_is_wdr(state))
+			return 0; /* see V4L2_CID_WIDE_DYNAMIC_RANGE */
+
+		return write_word_to_i2c_device(
+			state->client, APT_GLOBAL_GAIN,
+			ctrl->val >> state->model->chip->digital_gain_shift);
+
+	case V4L2_CID_HFLIP:
+	/* case V4L2_CID_VFLIP: cluster */
+		return mt9m02x_update_read_mode(state);
+
+	case V4L2_CID_EXPOSURE_ABSOLUTE:
+		return write_word_to_i2c_device(
+			state->client, APT_COARSE_INTEGRATION_TIME,
+			mt9m02x_exposure_100us_to_coarse(state, ctrl->val));
+
+	case V4L2_CID_WIDE_DYNAMIC_RANGE:
+	/* case MT9M02X_V4L2_CID_HDR_RATIO_T1T2: cluster */
+	/* case MT9M02X_V4L2_CID_HDR_RATIO_T2T3: cluster */
+		if (state->model->chip != &chip_mt9m024)
+			return -EOPNOTSUPP;
+		/* Bits:
+		 * 0: 0 = HDR, 1 = linear
+		 * 1: fixed 0
+		 * 2-3: ratio_t1t2
+		 * 4-5: ratio_t2t3
+		 * 6-15: fixed 0
+		 */
+		temp_value =
+			(state->opmode_cluster.wdr->val ? 0 : 1) |
+			(state->opmode_cluster.ratio_t1t2->val << 2) |
+			(state->opmode_cluster.ratio_t2t3->val << 4);
+		ret = write_word_to_i2c_device(state->client,
+					       APT_OPERATION_MODE_CTRL,
+					       temp_value);
+		if (ret)
+			return ret;
+
+		ret = mt9m02x_update_exposure_range(state);
+		if (ret)
+			return ret;
+
+		/* Ratios are only active when HDR is enabled. */
+		v4l2_ctrl_activate(state->opmode_cluster.ratio_t1t2,
+				   state->opmode_cluster.wdr->val);
+		v4l2_ctrl_activate(state->opmode_cluster.ratio_t2t3,
+				   state->opmode_cluster.wdr->val);
+
+		/* When HDR is enabled, digital gain is dependent on ratios
+		 * and cannot be controlled otherwise.
+		 */
+		v4l2_ctrl_activate(state->digital_gain_ctrl,
+				   !state->opmode_cluster.wdr->val);
+		if (state->opmode_cluster.wdr->val)
+			/* MT9M024 datasheet rev. G, table 7 */
+			temp_value = 1 << (1 +
+				state->opmode_cluster.ratio_t1t2->val +
+				state->opmode_cluster.ratio_t2t3->val);
+		else
+			temp_value = state->digital_gain_ctrl->val >>
+				state->model->chip->digital_gain_shift;
+		return write_word_to_i2c_device(state->client, APT_GLOBAL_GAIN,
+						temp_value);
+	}
+	return -EINVAL;
+}
+
+static const struct v4l2_ctrl_ops mt9m02x_ctrl_ops = {
+	.s_ctrl = mt9m02x_s_ctrl,
+};
+
+static const struct v4l2_ctrl_config mt9m02x_hdr_ratio_t1t2_ctrl = {
+	.ops = &mt9m02x_ctrl_ops,
+	.id = MT9M02X_V4L2_CID_HDR_RATIO_T1T2,
+	.type = V4L2_CTRL_TYPE_INTEGER,
+	.name = "HDR ratio T1T2",
+	.min = 0,
+	.max = 2,
+	.step = 1,
+	.def = 0,
+	.flags = 0,
+};
+
+static const struct v4l2_ctrl_config mt9m02x_hdr_ratio_t2t3_ctrl = {
+	.ops = &mt9m02x_ctrl_ops,
+	.id = MT9M02X_V4L2_CID_HDR_RATIO_T2T3,
+	.type = V4L2_CTRL_TYPE_INTEGER,
+	.name = "HDR ratio T2T3",
+	.min = 0,
+	.max = 2,
+	.step = 1,
+	.def = 0,
+	.flags = 0,
+};
+
+#ifdef CONFIG_VIDEO_ADV_DEBUG
+static int mt9m02x_g_register(struct v4l2_subdev *sd,
+			      struct v4l2_dbg_register *reg)
+{
+	struct i2c_client *client = v4l2_get_subdevdata(sd);
+	u16 value;
+	int ret;
+
+	if (reg->reg < 0x3000 || reg->reg > 0x3FFF)
+		return -EINVAL;
+
+	ret = read_word_from_i2c_device(client, reg->reg, &value);
+	if (ret)
+		return ret;
+
+	reg->val = value;
+	reg->size = 2;
+	return 0;
+}
+
+static int mt9m02x_s_register(struct v4l2_subdev *sd,
+			      const struct v4l2_dbg_register *reg)
+{
+	struct i2c_client *client = v4l2_get_subdevdata(sd);
+
+	if (reg->reg < 0x3000 || reg->reg > 0x3FFF)
+		return -EINVAL;
+
+	return write_word_to_i2c_device(client, reg->reg, reg->val);
+}
+#endif
+
+static const struct v4l2_subdev_core_ops mt9m02x_v4l2_subdev_core_ops = {
+#ifdef CONFIG_VIDEO_ADV_DEBUG
+	.g_register = &mt9m02x_g_register,
+	.s_register = &mt9m02x_s_register,
+#endif
+};
+
+static inline u32 get_mbus_code(struct mt9m02x_state *state)
+{
+	return state->model->monochrome ? MEDIA_BUS_FMT_Y12_1X12 :
+					  MEDIA_BUS_FMT_SGRBG12_1X12;
+}
+
+static int mt9m02x_enum_mbus_code(struct v4l2_subdev *sd,
+				  struct v4l2_subdev_pad_config *cfg,
+				  struct v4l2_subdev_mbus_code_enum *code)
+{
+	struct mt9m02x_state *state = container_of(sd, struct mt9m02x_state,
+						   sd);
+
+	if (code->pad || code->index)
+		return -EINVAL;
+	code->code = get_mbus_code(state);
+	return 0;
+}
+
+static int mt9m02x_enum_frame_size(struct v4l2_subdev *sd,
+				   struct v4l2_subdev_pad_config *cfg,
+				   struct v4l2_subdev_frame_size_enum *fse)
+{
+	struct mt9m02x_state *state = container_of(sd, struct mt9m02x_state,
+						   sd);
+
+	if (fse->pad || fse->code != get_mbus_code(state) || fse->index > 1)
+		return -EINVAL;
+	fse->min_width = state->model->chip->width / (fse->index + 1);
+	fse->max_width = fse->min_width;
+	fse->min_height = state->model->chip->height / (fse->index + 1);
+	fse->max_height = fse->min_height;
+	return 0;
+}
+
+static int mt9m02x_get_fmt(struct v4l2_subdev *sd,
+			   struct v4l2_subdev_pad_config *cfg,
+			   struct v4l2_subdev_format *format)
+{
+	struct mt9m02x_state *state = container_of(sd, struct mt9m02x_state,
+						   sd);
+
+	if (format->pad)
+		return -EINVAL;
+
+	format->format.field = V4L2_FIELD_NONE;
+	format->format.code = get_mbus_code(state);
+	format->format.colorspace = V4L2_COLORSPACE_SRGB;
+	mutex_lock(state->ctrls.lock);
+	format->format.width = state->model->chip->width /
+		(state->binning + 1);
+	format->format.height = state->model->chip->height /
+		(state->binning + 1);
+	mutex_unlock(state->ctrls.lock);
+	return 0;
+}
+
+static int mt9m02x_set_fmt(struct v4l2_subdev *sd,
+			   struct v4l2_subdev_pad_config *cfg,
+			   struct v4l2_subdev_format *format)
+{
+	struct mt9m02x_state *state = container_of(sd, struct mt9m02x_state,
+						   sd);
+	bool binning = false;
+	int ret;
+
+	if (format->pad)
+		return -EINVAL;
+
+	format->format.field = V4L2_FIELD_NONE;
+	format->format.code = get_mbus_code(state);
+	format->format.colorspace = V4L2_COLORSPACE_SRGB;
+	if (format->format.width * 4 <= state->model->chip->width * 3 &&
+	    format->format.height * 4 <= state->model->chip->height * 3)
+		binning = true;
+	format->format.width = state->model->chip->width / (binning + 1);
+	format->format.height = state->model->chip->height / (binning + 1);
+	if (format->which == V4L2_SUBDEV_FORMAT_TRY) {
+		cfg->try_fmt = format->format;
+		return 0;
+	}
+	mutex_lock(state->ctrls.lock);
+	state->binning = binning;
+	ret = mt9m02x_update_read_mode(state);
+	if (state->model->chip == &chip_ar0141cs && !ret)
+		ret = ar0141cs_update_odd_inc(state);
+	mutex_unlock(state->ctrls.lock);
+	return ret;
+}
+
+static const struct v4l2_subdev_pad_ops mt9m02x_v4l2_subdev_pad_ops = {
+	.enum_mbus_code = &mt9m02x_enum_mbus_code,
+	.enum_frame_size = &mt9m02x_enum_frame_size,
+	.get_fmt = &mt9m02x_get_fmt,
+	.set_fmt = &mt9m02x_set_fmt,
+};
+
+static const struct v4l2_subdev_ops mt9m02x_v4l2_subdev_ops = {
+	.core = &mt9m02x_v4l2_subdev_core_ops,
+	.pad = &mt9m02x_v4l2_subdev_pad_ops,
+};
+
+// read out device tree and probe driver
+static int mt9m02x_probe(struct i2c_client *client,
+			 const struct i2c_device_id *did)
+{
+	int ret;
+	struct mt9m02x_state *state;
+	struct mt9m02x_platform_data pdatabuffer;
+	struct mt9m02x_platform_data *pdata;
+	struct v4l2_ctrl *ctrl;
+
+	dev_dbg(&client->dev, "start probing i2c device %s!\n", client->name);
+
+	if (client->dev.of_node) {
+		if (of_property_read_u32(client->dev.of_node,
+					 "input-clock-frequency",
+					 &pdatabuffer.ext_freq)) {
+			dev_err(&client->dev,
+				"input-clock-frequency property missing");
+			return -EINVAL;
+		}
+		if (of_property_read_u32(client->dev.of_node,
+					 "pixel-clock-frequency",
+					 &pdatabuffer.pix_clock)) {
+			dev_err(&client->dev,
+				"pixel-clock-frequency property missing");
+			return -EINVAL;
+		}
+		pdata = &pdatabuffer;
+	} else {
+		pdata = client->dev.platform_data;
+		if (!pdata) {
+			dev_err(&client->dev, "No platform data found.");
+			return -EINVAL;
+		}
+	}
+
+	state = devm_kzalloc(&client->dev, sizeof(*state), GFP_KERNEL);
+	if (!state)
+		return -ENOMEM;
+
+	state->client = client;
+	state->model = (const void *)did->driver_data;
+	state->pll.ext_clock = pdata->ext_freq;
+	dev_dbg(&client->dev, "ext_clock = %d\n", state->pll.ext_clock);
+	state->pll.pix_clock = pdata->pix_clock;
+	dev_dbg(&client->dev, "pix_clock = %d\n", state->pll.pix_clock);
+
+	state->reset_gpio = devm_gpiod_get_optional(&client->dev, "reset",
+						    GPIOD_OUT_LOW);
+	if (IS_ERR(state->reset_gpio))
+		return PTR_ERR(state->reset_gpio);
+	state->standby_gpio = devm_gpiod_get_optional(&client->dev, "standby",
+						      GPIOD_OUT_HIGH);
+	if (IS_ERR(state->standby_gpio))
+		return PTR_ERR(state->standby_gpio);
+
+	ret = init_image_sensor(state);
+	if (ret) {
+		dev_err(&client->dev, "init image sensor failed!\n");
+		return ret;
+	}
+
+	dev_dbg(&client->dev, "initialization succeeded!\n");
+
+	v4l2_ctrl_handler_init(&state->ctrls,
+			       state->model->chip == &chip_mt9m024 ? 9 : 6);
+	/* Monotone mapping of available gains to 0..N. */
+	v4l2_ctrl_new_int_menu(&state->ctrls, &mt9m02x_ctrl_ops,
+			       V4L2_CID_ISO_SENSITIVITY,
+			       state->model->chip->again_menu_length - 1, 0,
+			       state->model->chip->again_menu);
+	state->digital_gain_ctrl = v4l2_ctrl_new_std(
+		&state->ctrls, &mt9m02x_ctrl_ops, V4L2_CID_DIGITAL_GAIN,
+		state->model->chip->digital_gain_min,
+		GENMASK(state->model->chip->digital_gain_bits - 1, 0) *
+		BIT(state->model->chip->digital_gain_shift),
+		BIT(state->model->chip->digital_gain_shift),
+		max_t(s64, BIT(8), state->model->chip->digital_gain_min));
+	state->mode_cluster.hflip = v4l2_ctrl_new_std(
+		&state->ctrls, &mt9m02x_ctrl_ops, V4L2_CID_HFLIP,
+		0, 1, 1, 1);
+	state->mode_cluster.vflip = v4l2_ctrl_new_std(
+		&state->ctrls, &mt9m02x_ctrl_ops, V4L2_CID_VFLIP,
+		0, 1, 1, 1);
+	ctrl = v4l2_ctrl_new_std(&state->ctrls, &mt9m02x_ctrl_ops,
+				 V4L2_CID_PIXEL_RATE, state->pll.pix_clock,
+				 state->pll.pix_clock, 1,
+				 state->pll.pix_clock);
+	if (ctrl)
+		ctrl->flags |= V4L2_CTRL_FLAG_READ_ONLY;
+	state->exposure_ctrl = v4l2_ctrl_new_std(
+		&state->ctrls,
+		&mt9m02x_ctrl_ops,
+		V4L2_CID_EXPOSURE_ABSOLUTE,
+		/* Actual range is set later. */
+		mt9m02x_exposure_coarse_to_100us(
+			state, APT_COARSE_INTEGRATION_TIME_DEFAULT),
+		mt9m02x_exposure_coarse_to_100us(
+			state, APT_COARSE_INTEGRATION_TIME_DEFAULT),
+		1,
+		mt9m02x_exposure_coarse_to_100us(
+			state, APT_COARSE_INTEGRATION_TIME_DEFAULT));
+
+	if (state->model->chip == &chip_mt9m024) {
+		state->opmode_cluster.wdr = v4l2_ctrl_new_std(
+			&state->ctrls, &mt9m02x_ctrl_ops,
+			V4L2_CID_WIDE_DYNAMIC_RANGE, 0, 1, 1, 0);
+		state->opmode_cluster.ratio_t1t2 = v4l2_ctrl_new_custom(
+			&state->ctrls, &mt9m02x_hdr_ratio_t1t2_ctrl,
+			NULL);
+		state->opmode_cluster.ratio_t2t3 = v4l2_ctrl_new_custom(
+			&state->ctrls, &mt9m02x_hdr_ratio_t2t3_ctrl,
+			NULL);
+	}
+
+	if (state->ctrls.error) {
+		dev_err(&client->dev,
+			"v4l2 control handler registration failed!\n");
+		return state->ctrls.error;
+	}
+	v4l2_ctrl_cluster(2, &state->mode_cluster.hflip);
+	if (state->model->chip == &chip_mt9m024)
+		v4l2_ctrl_cluster(3, &state->opmode_cluster.wdr);
+
+	state->sd.ctrl_handler = &state->ctrls;
+
+	ret = v4l2_ctrl_handler_setup(&state->ctrls);
+	if (ret) {
+		dev_err(&client->dev, "v4l2_ctrl_handler_setup failed!\n");
+		return ret;
+	}
+
+	v4l2_ctrl_lock(state->exposure_ctrl);
+	ret = mt9m02x_update_exposure_range(state);
+	v4l2_ctrl_unlock(state->exposure_ctrl);
+	if (ret)
+		return ret;
+
+	v4l2_i2c_subdev_init(&state->sd, client, &mt9m02x_v4l2_subdev_ops);
+
+	state->sd.flags |= V4L2_SUBDEV_FL_HAS_DEVNODE;
+
+	state->sd.entity.function = MEDIA_ENT_F_CAM_SENSOR;
+	state->pad.flags = MEDIA_PAD_FL_SOURCE;
+	ret = media_entity_pads_init(&state->sd.entity, 1, &state->pad);
+	if (ret)
+		return ret;
+
+	ret = v4l2_async_register_subdev(&state->sd);
+	if (ret) {
+		dev_err(&client->dev, "v4l2_async_register_subdev failed!\n");
+		goto error_entity;
+	}
+
+	if (state->temp_value1 != state->temp_value2) {
+		struct device *hwmon_dev;
+		/* Ignore errors from hwmon device as this functionality is
+		 * optional.
+		 */
+		hwmon_dev = devm_hwmon_device_register_with_info(
+			&client->dev, client->name, state,
+			&mt9m02x_hwmon_info, NULL);
+		if (IS_ERR(hwmon_dev))
+			dev_warn(&client->dev,
+				 "adding hwmon device failed (%ld).",
+				 PTR_ERR(hwmon_dev));
+	}
+
+	dev_dbg(&client->dev, "probing i2c device %s successful.\n",
+		client->name);
+	return 0;
+error_entity:
+	media_entity_cleanup(&state->sd.entity);
+	return ret;
+}
+
+static int mt9m02x_remove(struct i2c_client *client)
+{
+	struct v4l2_subdev *subdev = i2c_get_clientdata(client);
+	struct mt9m02x_state *state =
+		container_of(subdev, struct mt9m02x_state, sd);
+
+	v4l2_device_unregister_subdev(subdev);
+	media_entity_cleanup(&subdev->entity);
+
+	if (state->standby_gpio)
+		gpiod_set_value_cansleep(state->standby_gpio, 1);
+	return 0;
+}
+
+static const struct i2c_device_id mt9m02x_id[] = {
+	{ "ar0141cs", (kernel_ulong_t)&model_ar0141cs },
+	{ "ar0141csm", (kernel_ulong_t)&model_ar0141csm },
+	{ "mt9m024", (kernel_ulong_t)&model_mt9m024 },
+	{ "mt9m024m", (kernel_ulong_t)&model_mt9m024m },
+	{ }
+};
+
+MODULE_DEVICE_TABLE(i2c, mt9m02x_id);
+
+static struct i2c_driver mt9m02x_driver = {
+	.driver = {
+		.name = "mt9m02x",
+	},
+	.probe = mt9m02x_probe,
+	.remove = mt9m02x_remove,
+	.id_table = mt9m02x_id,
+};
+
+module_i2c_driver(mt9m02x_driver);
+
+MODULE_AUTHOR("Helmut Grohne <helmut.grohne@intenta.de>");
+MODULE_DESCRIPTION("mt9m024/ar0141cs imager driver");
+MODULE_LICENSE("GPL");
-- 
2.20.1


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

end of thread, other threads:[~2020-10-22 12:39 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2020-10-20  9:27 [RFC PATCH] media: new driver mt9m02x for Onsemi MT9M024 and AR0141CS Helmut Grohne
2020-10-21  9:50 ` Sakari Ailus
2020-10-21 11:21   ` Helmut Grohne
2020-10-21 12:37     ` Sakari Ailus
2020-10-22 12:17       ` Helmut Grohne
2020-10-22 12:39         ` Sakari Ailus

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).