[PATCH 2/3] media: sunxi: Add support for the D1 CSIC

Nguyen Minh Tien tien.nguyenminh at embeddedlinux.blog
Wed Sep 30 08:48:34 PDT 2026


Add a driver for the parallel input of the D1 CSIC and its first DMA
engine. The parser is a bridge subdev and the DMA engine a capture video
device, set up through the media controller API.

The DMA engine converts YUV 4:2:2 to NV12, NV16, planar or grey output,
and stores other formats as received.

Two things the manual doesn't say were found on the hardware: the buffer
addresses are in 32-bit words, and frame done also comes for a frame cut
short, so a frame only counts when the line counter saw its last line.

Signed-off-by: Nguyen Minh Tien <tien.nguyenminh at embeddedlinux.blog>
---
 MAINTAINERS                                   |    1 +
 drivers/media/platform/sunxi/Kconfig          |    1 +
 drivers/media/platform/sunxi/Makefile         |    1 +
 .../media/platform/sunxi/sun20i-csi/Kconfig   |   17 +
 .../media/platform/sunxi/sun20i-csi/Makefile  |    4 +
 .../platform/sunxi/sun20i-csi/sun20i_csi.c    | 1357 +++++++++++++++++
 6 files changed, 1381 insertions(+)
 create mode 100644 drivers/media/platform/sunxi/sun20i-csi/Kconfig
 create mode 100644 drivers/media/platform/sunxi/sun20i-csi/Makefile
 create mode 100644 drivers/media/platform/sunxi/sun20i-csi/sun20i_csi.c

diff --git a/MAINTAINERS b/MAINTAINERS
index f4c45c4678..b67850e98e 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -911,6 +911,7 @@ L:	linux-media at vger.kernel.org
 S:	Maintained
 T:	git git://linuxtv.org/media.git
 F:	Documentation/devicetree/bindings/media/allwinner,sun20i-d1-csi.yaml
+F:	drivers/media/platform/sunxi/sun20i-csi/
 
 ALLWINNER DMIC DRIVERS
 M:	Ban Tao <fengzheng923 at gmail.com>
diff --git a/drivers/media/platform/sunxi/Kconfig b/drivers/media/platform/sunxi/Kconfig
index 2dd15083a1..710a38b939 100644
--- a/drivers/media/platform/sunxi/Kconfig
+++ b/drivers/media/platform/sunxi/Kconfig
@@ -8,3 +8,4 @@ source "drivers/media/platform/sunxi/sun6i-mipi-csi2/Kconfig"
 source "drivers/media/platform/sunxi/sun8i-a83t-mipi-csi2/Kconfig"
 source "drivers/media/platform/sunxi/sun8i-di/Kconfig"
 source "drivers/media/platform/sunxi/sun8i-rotate/Kconfig"
+source "drivers/media/platform/sunxi/sun20i-csi/Kconfig"
diff --git a/drivers/media/platform/sunxi/Makefile b/drivers/media/platform/sunxi/Makefile
index 9aa01cb018..bb117f7048 100644
--- a/drivers/media/platform/sunxi/Makefile
+++ b/drivers/media/platform/sunxi/Makefile
@@ -6,3 +6,4 @@ obj-y		+= sun6i-mipi-csi2/
 obj-y		+= sun8i-a83t-mipi-csi2/
 obj-y		+= sun8i-di/
 obj-y		+= sun8i-rotate/
+obj-y		+= sun20i-csi/
diff --git a/drivers/media/platform/sunxi/sun20i-csi/Kconfig b/drivers/media/platform/sunxi/sun20i-csi/Kconfig
new file mode 100644
index 0000000000..83ec00bf6a
--- /dev/null
+++ b/drivers/media/platform/sunxi/sun20i-csi/Kconfig
@@ -0,0 +1,17 @@
+# SPDX-License-Identifier: GPL-2.0-only
+config VIDEO_SUN20I_CSI
+	tristate "Allwinner D1 CMOS Sensor Interface Controller (CSIC) Driver"
+	depends on V4L_PLATFORM_DRIVERS && VIDEO_DEV
+	depends on ARCH_SUNXI || COMPILE_TEST
+	depends on PM && COMMON_CLK && RESET_CONTROLLER && HAS_DMA
+	select MEDIA_CONTROLLER
+	select VIDEO_V4L2_SUBDEV_API
+	select VIDEOBUF2_DMA_CONTIG
+	select V4L2_FWNODE
+	help
+	  Support for the parallel camera interface of the Allwinner D1
+	  CMOS Sensor Interface Controller (CSIC), also found on the D1s
+	  and the T113.
+
+	  To compile this driver as a module, choose M here: the module
+	  will be called sun20i-csi.
diff --git a/drivers/media/platform/sunxi/sun20i-csi/Makefile b/drivers/media/platform/sunxi/sun20i-csi/Makefile
new file mode 100644
index 0000000000..d799c60763
--- /dev/null
+++ b/drivers/media/platform/sunxi/sun20i-csi/Makefile
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0-only
+sun20i-csi-y += sun20i_csi.o
+
+obj-$(CONFIG_VIDEO_SUN20I_CSI) += sun20i-csi.o
diff --git a/drivers/media/platform/sunxi/sun20i-csi/sun20i_csi.c b/drivers/media/platform/sunxi/sun20i-csi/sun20i_csi.c
new file mode 100644
index 0000000000..eed29bc613
--- /dev/null
+++ b/drivers/media/platform/sunxi/sun20i-csi/sun20i_csi.c
@@ -0,0 +1,1357 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Allwinner D1/T113 CMOS Sensor Interface Controller (CSIC) driver
+ *
+ * Copyright (C) 2026 Nguyen Minh Tien <tien.nguyenminh at embeddedlinux.blog>
+ *
+ * The CSIC is split into a parser, which receives the parallel bus, and a
+ * DMA engine, which writes frames to memory and can convert YUV 4:2:2 input
+ * to semi-planar, planar or luma-only output. The parser is exposed as a
+ * bridge subdev and the DMA engine as the capture video device.
+ */
+
+#include <linux/bitfield.h>
+#include <linux/clk.h>
+#include <linux/interrupt.h>
+#include <linux/io.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/pm_runtime.h>
+#include <linux/reset.h>
+
+#include <media/media-device.h>
+#include <media/v4l2-async.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-fwnode.h>
+#include <media/v4l2-ioctl.h>
+#include <media/v4l2-mc.h>
+#include <media/v4l2-subdev.h>
+#include <media/videobuf2-dma-contig.h>
+
+/* Register and field names as in the T113-S3 user manual v1.3, section 6.1. */
+#define SUN20I_CSI_TOP_EN_REG			0x0800
+#define SUN20I_CSI_TOP_EN_CSIC_TOP_EN		BIT(0)
+
+#define SUN20I_CSI_PRS_EN_REG			0x1000
+#define SUN20I_CSI_PRS_EN_NCSIC_EN		BIT(16)
+#define SUN20I_CSI_PRS_EN_PCLK_EN		BIT(15)
+#define SUN20I_CSI_PRS_EN_PRS_EN		BIT(0)
+
+/*
+ * The parser takes HREF and VREF, which are the inverse of HSYNC and VSYNC,
+ * so an active low sync signal needs the positive (set) HREF_POL or VREF_POL,
+ * as explained for the A10 CSI in commit 1948dcf0f928 ("media: sun4i-csi:
+ * Fix [HV]sync polarity handling"). With CLK_POL set, the data changes on the
+ * falling edge of the pixel clock and is sampled on the rising edge.
+ */
+#define SUN20I_CSI_PRS_NCSIC_IF_CFG_REG		0x1004
+#define SUN20I_CSI_PRS_NCSIC_IF_CFG_VREF_POL	BIT(18)
+#define SUN20I_CSI_PRS_NCSIC_IF_CFG_HREF_POL	BIT(17)
+#define SUN20I_CSI_PRS_NCSIC_IF_CFG_CLK_POL	BIT(16)
+#define SUN20I_CSI_PRS_NCSIC_IF_CFG_INPUT_SEQ	GENMASK(7, 6)
+#define SUN20I_CSI_INPUT_SEQ_YUYV		0
+#define SUN20I_CSI_INPUT_SEQ_UYVY		2
+
+#define SUN20I_CSI_PRS_CAP_REG			0x100c
+#define SUN20I_CSI_PRS_CAP_CH0_VCAP_ON		BIT(1)
+
+#define SUN20I_CSI_PRS_CH0_INFMT_REG		0x1024
+#define SUN20I_CSI_INPUT_FMT_RAW		0
+#define SUN20I_CSI_INPUT_FMT_YUV422		3
+#define SUN20I_CSI_PRS_CH0_OUTPUT_HSIZE_REG	0x1028
+#define SUN20I_CSI_PRS_CH0_OUTPUT_VSIZE_REG	0x102c
+
+/* HOR_LEN and VER_LEN of the parser and DMA size registers, from bit 16. */
+#define SUN20I_CSI_LEN				GENMASK(28, 16)
+
+#define SUN20I_CSI_DMA_EN_REG			0x9000
+/* VFLIP_BUF_ADDR, BUF_LENGTH and FLIP_SIZE set by software, as on reset. */
+#define SUN20I_CSI_DMA_EN_SW_CFG_MODE		GENMASK(30, 28)
+#define SUN20I_CSI_DMA_EN_DMA_EN		BIT(4)
+#define SUN20I_CSI_DMA_EN_BK_TOP_EN		BIT(0)
+#define SUN20I_CSI_DMA_CFG_REG			0x9004
+#define SUN20I_CSI_DMA_CFG_OUTPUT_FMT		GENMASK(19, 16)
+#define SUN20I_CSI_DMA_HSIZE_REG		0x9010
+#define SUN20I_CSI_DMA_VSIZE_REG		0x9014
+/* The buffer addresses are in 32-bit words, which the manual doesn't say. */
+#define SUN20I_CSI_DMA_F0_BUFA_REG		0x9020
+#define SUN20I_CSI_DMA_F1_BUFA_REG		0x9028
+#define SUN20I_CSI_DMA_F2_BUFA_REG		0x9030
+#define SUN20I_CSI_DMA_BUF_LEN_REG		0x9038
+#define SUN20I_CSI_DMA_BUF_LEN_BUF_LEN_C	GENMASK(29, 16)
+#define SUN20I_CSI_DMA_BUF_LEN_BUF_LEN		GENMASK(13, 0)
+#define SUN20I_CSI_DMA_INT_EN_REG		0x9050
+#define SUN20I_CSI_DMA_INT_STA_REG		0x9054
+#define SUN20I_CSI_DMA_INT_VS			BIT(7)
+#define SUN20I_CSI_DMA_INT_HB_OF		BIT(6)
+#define SUN20I_CSI_DMA_INT_LC			BIT(5)
+#define SUN20I_CSI_DMA_INT_FIFO2_OF		BIT(4)
+#define SUN20I_CSI_DMA_INT_FIFO1_OF		BIT(3)
+#define SUN20I_CSI_DMA_INT_FIFO0_OF		BIT(2)
+#define SUN20I_CSI_DMA_INT_FD			BIT(1)
+#define SUN20I_CSI_DMA_INT_OF		(SUN20I_CSI_DMA_INT_HB_OF | \
+					 SUN20I_CSI_DMA_INT_FIFO2_OF | \
+					 SUN20I_CSI_DMA_INT_FIFO1_OF | \
+					 SUN20I_CSI_DMA_INT_FIFO0_OF)
+#define SUN20I_CSI_DMA_LINE_CNT_REG		0x9058
+#define SUN20I_CSI_DMA_LINE_CNT_LINE_CNT_NUM	GENMASK(12, 0)
+
+/* OUTPUT_FMT values: "frame" modes, for raw or YUV 4:2:2 input. */
+#define SUN20I_CSI_OUT_RAW_8			0x8
+#define SUN20I_CSI_OUT_PLANAR_420		0x2
+#define SUN20I_CSI_OUT_PLANAR_422		0x3
+#define SUN20I_CSI_OUT_UV_420			0x6
+#define SUN20I_CSI_OUT_UV_422			0x7
+#define SUN20I_CSI_OUT_VU_420			0xa
+#define SUN20I_CSI_OUT_VU_422			0xb
+#define SUN20I_CSI_OUT_Y400			0xf
+
+/* Rate of the module clock, csi_top_clk. */
+#define SUN20I_CSI_MOD_RATE			300000000
+#define SUN20I_CSI_MIN_SIZE			32
+/* HOR_LEN is 13 bits and counts bytes for raw input, 2 bytes per pixel. */
+#define SUN20I_CSI_MAX_WIDTH			4088
+#define SUN20I_CSI_MAX_HEIGHT			4096
+
+enum {
+	SUN20I_CSI_PAD_SINK,
+	SUN20I_CSI_PAD_SOURCE,
+	SUN20I_CSI_NUM_PADS,
+};
+
+struct sun20i_csi_format {
+	u32 fourcc;
+	/* The only bus code, or 0 for any YUV 4:2:2 code (converted). */
+	u32 code;
+	u8 output;
+	u8 bpp;		/* Bytes per pixel in the first plane */
+	u8 planes;	/* 1: packed or luma, 2: semi-planar, 3: planar */
+	u8 vsub;	/* Vertical chroma subsampling */
+	bool swap_uv;	/* Planar: V before U */
+	bool raw;	/* Stored as received, no conversion */
+	bool bayer;
+};
+
+static const u32 sun20i_csi_yuv_codes[] = {
+	MEDIA_BUS_FMT_YUYV8_2X8,
+	MEDIA_BUS_FMT_UYVY8_2X8,
+};
+
+static const u32 sun20i_csi_raw_codes[] = {
+	MEDIA_BUS_FMT_RGB565_2X8_LE,
+	MEDIA_BUS_FMT_RGB565_2X8_BE,
+	MEDIA_BUS_FMT_SBGGR8_1X8,
+	MEDIA_BUS_FMT_SGBRG8_1X8,
+	MEDIA_BUS_FMT_SGRBG8_1X8,
+	MEDIA_BUS_FMT_SRGGB8_1X8,
+};
+
+#define SUN20I_CSI_YUV(_fourcc, _output, _planes, _vsub, _swap_uv)	\
+	{ .fourcc = _fourcc, .output = _output, .bpp = 1,		\
+	  .planes = _planes, .vsub = _vsub, .swap_uv = _swap_uv }
+#define SUN20I_CSI_RAW(_fourcc, _code, _bpp, _bayer)			\
+	{ .fourcc = _fourcc, .code = _code, .output = SUN20I_CSI_OUT_RAW_8, \
+	  .bpp = _bpp, .planes = 1, .vsub = 1, .raw = true, .bayer = _bayer }
+
+static const struct sun20i_csi_format sun20i_csi_formats[] = {
+	SUN20I_CSI_YUV(V4L2_PIX_FMT_NV12, SUN20I_CSI_OUT_UV_420, 2, 2, false),
+	SUN20I_CSI_YUV(V4L2_PIX_FMT_NV21, SUN20I_CSI_OUT_VU_420, 2, 2, false),
+	SUN20I_CSI_YUV(V4L2_PIX_FMT_NV16, SUN20I_CSI_OUT_UV_422, 2, 1, false),
+	SUN20I_CSI_YUV(V4L2_PIX_FMT_NV61, SUN20I_CSI_OUT_VU_422, 2, 1, false),
+	SUN20I_CSI_YUV(V4L2_PIX_FMT_YUV420, SUN20I_CSI_OUT_PLANAR_420,
+		       3, 2, false),
+	SUN20I_CSI_YUV(V4L2_PIX_FMT_YVU420, SUN20I_CSI_OUT_PLANAR_420,
+		       3, 2, true),
+	SUN20I_CSI_YUV(V4L2_PIX_FMT_YUV422P, SUN20I_CSI_OUT_PLANAR_422,
+		       3, 1, false),
+	SUN20I_CSI_YUV(V4L2_PIX_FMT_GREY, SUN20I_CSI_OUT_Y400, 1, 1, false),
+	SUN20I_CSI_RAW(V4L2_PIX_FMT_YUYV, MEDIA_BUS_FMT_YUYV8_2X8, 2, false),
+	SUN20I_CSI_RAW(V4L2_PIX_FMT_UYVY, MEDIA_BUS_FMT_UYVY8_2X8, 2, false),
+	SUN20I_CSI_RAW(V4L2_PIX_FMT_RGB565, MEDIA_BUS_FMT_RGB565_2X8_LE,
+		       2, false),
+	SUN20I_CSI_RAW(V4L2_PIX_FMT_RGB565X, MEDIA_BUS_FMT_RGB565_2X8_BE,
+		       2, false),
+	SUN20I_CSI_RAW(V4L2_PIX_FMT_SBGGR8, MEDIA_BUS_FMT_SBGGR8_1X8, 1, true),
+	SUN20I_CSI_RAW(V4L2_PIX_FMT_SGBRG8, MEDIA_BUS_FMT_SGBRG8_1X8, 1, true),
+	SUN20I_CSI_RAW(V4L2_PIX_FMT_SGRBG8, MEDIA_BUS_FMT_SGRBG8_1X8, 1, true),
+	SUN20I_CSI_RAW(V4L2_PIX_FMT_SRGGB8, MEDIA_BUS_FMT_SRGGB8_1X8, 1, true),
+};
+
+struct sun20i_csi_buffer {
+	struct vb2_v4l2_buffer vb;
+	struct list_head list;
+};
+
+struct sun20i_csi {
+	struct device *dev;
+	void __iomem *regs;
+	struct clk *bus_clk;
+	struct clk *mod_clk;
+	struct clk *ram_clk;
+	struct reset_control *reset;
+	int irq;
+
+	struct media_device mdev;
+	struct v4l2_device v4l2_dev;
+	struct v4l2_async_notifier notifier;
+	struct v4l2_mbus_config_parallel bus;
+
+	/* The parser, as a bridge subdev */
+	struct v4l2_subdev subdev;
+	struct media_pad pads[SUN20I_CSI_NUM_PADS];
+	struct v4l2_subdev *source;
+	u16 source_pad;
+
+	/* The DMA engine, as the capture video device */
+	struct video_device vdev;
+	struct media_pad vdev_pad;
+	struct vb2_queue queue;
+	struct mutex lock;		/* The video device and the queue */
+	struct v4l2_pix_format format;
+	const struct sun20i_csi_format *fmt;
+
+	spinlock_t irqlock;		/* The fields below */
+	struct list_head queued;
+	/* The buffer being written, and the one in the address registers */
+	struct sun20i_csi_buffer *active;
+	struct sun20i_csi_buffer *next;
+	bool active_complete;
+	bool active_error;
+	u32 sequence;
+	u32 errors;
+};
+
+static inline void sun20i_csi_write(struct sun20i_csi *csi, u32 reg, u32 val)
+{
+	writel(val, csi->regs + reg);
+}
+
+static inline u32 sun20i_csi_read(struct sun20i_csi *csi, u32 reg)
+{
+	return readl(csi->regs + reg);
+}
+
+static bool sun20i_csi_code_supported(u32 code)
+{
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(sun20i_csi_yuv_codes); i++)
+		if (sun20i_csi_yuv_codes[i] == code)
+			return true;
+	for (i = 0; i < ARRAY_SIZE(sun20i_csi_raw_codes); i++)
+		if (sun20i_csi_raw_codes[i] == code)
+			return true;
+	return false;
+}
+
+static bool sun20i_csi_format_takes(const struct sun20i_csi_format *fmt,
+				    u32 code)
+{
+	unsigned int i;
+
+	if (fmt->code)
+		return fmt->code == code;
+
+	for (i = 0; i < ARRAY_SIZE(sun20i_csi_yuv_codes); i++)
+		if (sun20i_csi_yuv_codes[i] == code)
+			return true;
+	return false;
+}
+
+static const struct sun20i_csi_format *sun20i_csi_find_format(u32 fourcc)
+{
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(sun20i_csi_formats); i++)
+		if (sun20i_csi_formats[i].fourcc == fourcc)
+			return &sun20i_csi_formats[i];
+	return NULL;
+}
+
+/* Number of units in a line for the parser and the DMA engine. */
+static u32 sun20i_csi_line_units(const struct sun20i_csi_format *fmt, u32 width)
+{
+	/* Raw input is counted in bytes, YUV input in pixels. */
+	return fmt->raw ? width * fmt->bpp : width;
+}
+
+/* -------------------------------------------------------------------------
+ * Buffers and interrupt
+ */
+
+static void sun20i_csi_set_buffer(struct sun20i_csi *csi,
+				  struct sun20i_csi_buffer *buf)
+{
+	dma_addr_t addr = vb2_dma_contig_plane_dma_addr(&buf->vb.vb2_buf, 0);
+	u32 width = csi->format.width, height = csi->format.height;
+	const struct sun20i_csi_format *fmt = csi->fmt;
+	dma_addr_t cb, cr;
+
+	sun20i_csi_write(csi, SUN20I_CSI_DMA_F0_BUFA_REG, addr >> 2);
+	if (fmt->planes == 1)
+		return;
+
+	cb = addr + width * height;
+	if (fmt->planes == 3) {
+		cr = cb + width / 2 * (height / fmt->vsub);
+		if (fmt->swap_uv)
+			swap(cb, cr);
+		sun20i_csi_write(csi, SUN20I_CSI_DMA_F2_BUFA_REG, cr >> 2);
+	}
+	sun20i_csi_write(csi, SUN20I_CSI_DMA_F1_BUFA_REG, cb >> 2);
+}
+
+/*
+ * Frame done. It also comes for a frame cut short, such as the first one
+ * after the sensor starts, so only a frame whose last line was written (the
+ * line counter interrupt) counts.
+ */
+static void sun20i_csi_frame_done(struct sun20i_csi *csi)
+{
+	struct sun20i_csi_buffer *buf = csi->active;
+
+	csi->active = NULL;
+
+	/* No buffer was queued: the next frame is going into this one. */
+	if (buf == csi->next)
+		buf = NULL;
+
+	if (!csi->active_complete) {
+		if (buf)
+			list_add(&buf->list, &csi->queued);
+		return;
+	}
+
+	if (buf) {
+		buf->vb.vb2_buf.timestamp = ktime_get_ns();
+		buf->vb.sequence = csi->sequence;
+		buf->vb.field = V4L2_FIELD_NONE;
+		vb2_buffer_done(&buf->vb.vb2_buf, csi->active_error ?
+				VB2_BUF_STATE_ERROR : VB2_BUF_STATE_DONE);
+	}
+	csi->sequence++;
+}
+
+/*
+ * Frame start. The manual: "at this time load the buffer address for the
+ * coming frame. So after this irq come, changing the buffer address could
+ * only effect next frame". The address registers take the next buffer here.
+ */
+static void sun20i_csi_frame_start(struct sun20i_csi *csi)
+{
+	/* A frame that started but was never done: reuse its buffer. */
+	if (csi->active && csi->active != csi->next)
+		list_add(&csi->active->list, &csi->queued);
+
+	csi->active = csi->next;
+	csi->active_complete = false;
+	csi->active_error = false;
+
+	csi->next = list_first_entry_or_null(&csi->queued,
+					     struct sun20i_csi_buffer, list);
+	if (csi->next) {
+		list_del(&csi->next->list);
+		sun20i_csi_set_buffer(csi, csi->next);
+	} else {
+		/* Nothing queued: the next frame goes into the same buffer. */
+		csi->next = csi->active;
+	}
+}
+
+static irqreturn_t sun20i_csi_irq(int irq, void *data)
+{
+	struct sun20i_csi *csi = data;
+	u32 status;
+
+	status = sun20i_csi_read(csi, SUN20I_CSI_DMA_INT_STA_REG);
+	sun20i_csi_write(csi, SUN20I_CSI_DMA_INT_STA_REG, status);
+	status &= SUN20I_CSI_DMA_INT_VS | SUN20I_CSI_DMA_INT_LC |
+		  SUN20I_CSI_DMA_INT_FD | SUN20I_CSI_DMA_INT_OF;
+	if (!status)
+		return IRQ_NONE;
+
+	spin_lock(&csi->irqlock);
+
+	/* The line counter is set to the last line of the frame. */
+	if (status & SUN20I_CSI_DMA_INT_LC)
+		csi->active_complete = true;
+
+	if (status & SUN20I_CSI_DMA_INT_OF) {
+		csi->active_error = true;
+		csi->errors++;
+	}
+
+	/* A frame ends before the next one starts. */
+	if (status & SUN20I_CSI_DMA_INT_FD)
+		sun20i_csi_frame_done(csi);
+	if (status & SUN20I_CSI_DMA_INT_VS)
+		sun20i_csi_frame_start(csi);
+
+	spin_unlock(&csi->irqlock);
+
+	return IRQ_HANDLED;
+}
+
+/* -------------------------------------------------------------------------
+ * Bridge subdev (the parser)
+ */
+
+static const struct v4l2_mbus_framefmt sun20i_csi_default_fmt = {
+	.width = 640,
+	.height = 480,
+	.code = MEDIA_BUS_FMT_YUYV8_2X8,
+	.field = V4L2_FIELD_NONE,
+	.colorspace = V4L2_COLORSPACE_SRGB,
+	.ycbcr_enc = V4L2_YCBCR_ENC_DEFAULT,
+	.quantization = V4L2_QUANTIZATION_DEFAULT,
+	.xfer_func = V4L2_XFER_FUNC_DEFAULT,
+};
+
+static struct sun20i_csi *sd_to_csi(struct v4l2_subdev *sd)
+{
+	return container_of(sd, struct sun20i_csi, subdev);
+}
+
+static int sun20i_csi_bridge_init_state(struct v4l2_subdev *sd,
+					struct v4l2_subdev_state *state)
+{
+	*v4l2_subdev_state_get_format(state, SUN20I_CSI_PAD_SINK) =
+		sun20i_csi_default_fmt;
+	*v4l2_subdev_state_get_format(state, SUN20I_CSI_PAD_SOURCE) =
+		sun20i_csi_default_fmt;
+
+	return 0;
+}
+
+static int
+sun20i_csi_bridge_enum_mbus_code(struct v4l2_subdev *sd,
+				 struct v4l2_subdev_state *state,
+				 struct v4l2_subdev_mbus_code_enum *code)
+{
+	unsigned int nyuv = ARRAY_SIZE(sun20i_csi_yuv_codes);
+
+	/* The source pad passes on the sink pad format. */
+	if (code->pad == SUN20I_CSI_PAD_SOURCE) {
+		const struct v4l2_mbus_framefmt *fmt;
+
+		if (code->index)
+			return -EINVAL;
+		fmt = v4l2_subdev_state_get_format(state, SUN20I_CSI_PAD_SINK);
+		code->code = fmt->code;
+		return 0;
+	}
+
+	if (code->index < nyuv)
+		code->code = sun20i_csi_yuv_codes[code->index];
+	else if (code->index - nyuv < ARRAY_SIZE(sun20i_csi_raw_codes))
+		code->code = sun20i_csi_raw_codes[code->index - nyuv];
+	else
+		return -EINVAL;
+
+	return 0;
+}
+
+static int
+sun20i_csi_bridge_enum_frame_size(struct v4l2_subdev *sd,
+				  struct v4l2_subdev_state *state,
+				  struct v4l2_subdev_frame_size_enum *fse)
+{
+	if (fse->index)
+		return -EINVAL;
+
+	if (fse->pad == SUN20I_CSI_PAD_SOURCE) {
+		const struct v4l2_mbus_framefmt *fmt;
+
+		fmt = v4l2_subdev_state_get_format(state, SUN20I_CSI_PAD_SINK);
+		if (fse->code != fmt->code)
+			return -EINVAL;
+		fse->min_width = fmt->width;
+		fse->max_width = fmt->width;
+		fse->min_height = fmt->height;
+		fse->max_height = fmt->height;
+		return 0;
+	}
+
+	if (!sun20i_csi_code_supported(fse->code))
+		return -EINVAL;
+
+	fse->min_width = SUN20I_CSI_MIN_SIZE;
+	fse->max_width = SUN20I_CSI_MAX_WIDTH;
+	fse->min_height = SUN20I_CSI_MIN_SIZE;
+	fse->max_height = SUN20I_CSI_MAX_HEIGHT;
+
+	return 0;
+}
+
+static int sun20i_csi_bridge_set_fmt(struct v4l2_subdev *sd,
+				     const struct v4l2_subdev_client_info *ci,
+				     struct v4l2_subdev_state *state,
+				     struct v4l2_subdev_format *format)
+{
+	struct v4l2_mbus_framefmt *fmt = &format->format;
+
+	if (format->which == V4L2_SUBDEV_FORMAT_ACTIVE &&
+	    v4l2_subdev_is_streaming(sd))
+		return -EBUSY;
+
+	/* The source pad format follows the sink pad format. */
+	if (format->pad == SUN20I_CSI_PAD_SOURCE)
+		return v4l2_subdev_get_fmt(sd, state, format);
+
+	if (!sun20i_csi_code_supported(fmt->code))
+		fmt->code = sun20i_csi_default_fmt.code;
+	fmt->width = clamp_t(u32, ALIGN(fmt->width, 8), SUN20I_CSI_MIN_SIZE,
+			     SUN20I_CSI_MAX_WIDTH);
+	fmt->height = clamp_t(u32, ALIGN(fmt->height, 2), SUN20I_CSI_MIN_SIZE,
+			      SUN20I_CSI_MAX_HEIGHT);
+	fmt->field = V4L2_FIELD_NONE;
+	if (fmt->colorspace == V4L2_COLORSPACE_DEFAULT ||
+	    fmt->colorspace > V4L2_COLORSPACE_DCI_P3)
+		fmt->colorspace = V4L2_COLORSPACE_SRGB;
+	if (fmt->ycbcr_enc > V4L2_YCBCR_ENC_SMPTE240M)
+		fmt->ycbcr_enc = V4L2_YCBCR_ENC_DEFAULT;
+	if (fmt->quantization > V4L2_QUANTIZATION_LIM_RANGE)
+		fmt->quantization = V4L2_QUANTIZATION_DEFAULT;
+	if (fmt->xfer_func > V4L2_XFER_FUNC_SMPTE2084)
+		fmt->xfer_func = V4L2_XFER_FUNC_DEFAULT;
+
+	*v4l2_subdev_state_get_format(state, SUN20I_CSI_PAD_SINK) = *fmt;
+	*v4l2_subdev_state_get_format(state, SUN20I_CSI_PAD_SOURCE) = *fmt;
+
+	return 0;
+}
+
+static int sun20i_csi_bridge_enable_streams(struct v4l2_subdev *sd,
+					    struct v4l2_subdev_state *state,
+					    u32 pad, u64 streams_mask)
+{
+	const struct v4l2_mbus_framefmt *fmt =
+		v4l2_subdev_state_get_format(state, SUN20I_CSI_PAD_SINK);
+	struct sun20i_csi *csi = sd_to_csi(sd);
+	u32 cfg = 0, seq, infmt, width;
+	int ret;
+
+	if (csi->bus.flags & V4L2_MBUS_VSYNC_ACTIVE_LOW)
+		cfg |= SUN20I_CSI_PRS_NCSIC_IF_CFG_VREF_POL;
+	if (csi->bus.flags & V4L2_MBUS_HSYNC_ACTIVE_LOW)
+		cfg |= SUN20I_CSI_PRS_NCSIC_IF_CFG_HREF_POL;
+	if (csi->bus.flags & V4L2_MBUS_PCLK_SAMPLE_RISING)
+		cfg |= SUN20I_CSI_PRS_NCSIC_IF_CFG_CLK_POL;
+	if (fmt->code == MEDIA_BUS_FMT_UYVY8_2X8)
+		seq = SUN20I_CSI_INPUT_SEQ_UYVY;
+	else
+		seq = SUN20I_CSI_INPUT_SEQ_YUYV;
+	cfg |= FIELD_PREP(SUN20I_CSI_PRS_NCSIC_IF_CFG_INPUT_SEQ, seq);
+
+	/* The capture format is fixed while streaming. */
+	infmt = csi->fmt->raw ? SUN20I_CSI_INPUT_FMT_RAW :
+				SUN20I_CSI_INPUT_FMT_YUV422;
+	width = sun20i_csi_line_units(csi->fmt, fmt->width);
+
+	sun20i_csi_write(csi, SUN20I_CSI_PRS_EN_REG,
+			 SUN20I_CSI_PRS_EN_NCSIC_EN |
+			 SUN20I_CSI_PRS_EN_PCLK_EN | SUN20I_CSI_PRS_EN_PRS_EN);
+	sun20i_csi_write(csi, SUN20I_CSI_PRS_NCSIC_IF_CFG_REG, cfg);
+	sun20i_csi_write(csi, SUN20I_CSI_PRS_CH0_INFMT_REG, infmt);
+	sun20i_csi_write(csi, SUN20I_CSI_PRS_CH0_OUTPUT_HSIZE_REG,
+			 FIELD_PREP(SUN20I_CSI_LEN, width));
+	sun20i_csi_write(csi, SUN20I_CSI_PRS_CH0_OUTPUT_VSIZE_REG,
+			 FIELD_PREP(SUN20I_CSI_LEN, fmt->height));
+	sun20i_csi_write(csi, SUN20I_CSI_PRS_CAP_REG,
+			 SUN20I_CSI_PRS_CAP_CH0_VCAP_ON);
+
+	ret = v4l2_subdev_enable_streams(csi->source, csi->source_pad,
+					 BIT_ULL(0));
+	if (ret) {
+		sun20i_csi_write(csi, SUN20I_CSI_PRS_CAP_REG, 0);
+		sun20i_csi_write(csi, SUN20I_CSI_PRS_EN_REG, 0);
+	}
+
+	return ret;
+}
+
+static int sun20i_csi_bridge_disable_streams(struct v4l2_subdev *sd,
+					     struct v4l2_subdev_state *state,
+					     u32 pad, u64 streams_mask)
+{
+	struct sun20i_csi *csi = sd_to_csi(sd);
+	int ret;
+
+	ret = v4l2_subdev_disable_streams(csi->source, csi->source_pad,
+					  BIT_ULL(0));
+
+	sun20i_csi_write(csi, SUN20I_CSI_PRS_CAP_REG, 0);
+	sun20i_csi_write(csi, SUN20I_CSI_PRS_EN_REG, 0);
+
+	return ret;
+}
+
+static const struct v4l2_subdev_pad_ops sun20i_csi_bridge_pad_ops = {
+	.enum_mbus_code		= sun20i_csi_bridge_enum_mbus_code,
+	.enum_frame_size	= sun20i_csi_bridge_enum_frame_size,
+	.get_fmt		= v4l2_subdev_get_fmt,
+	.set_fmt		= sun20i_csi_bridge_set_fmt,
+	.enable_streams		= sun20i_csi_bridge_enable_streams,
+	.disable_streams	= sun20i_csi_bridge_disable_streams,
+};
+
+static const struct v4l2_subdev_ops sun20i_csi_bridge_ops = {
+	.pad			= &sun20i_csi_bridge_pad_ops,
+};
+
+static const struct v4l2_subdev_internal_ops sun20i_csi_bridge_internal_ops = {
+	.init_state		= sun20i_csi_bridge_init_state,
+};
+
+static const struct media_entity_operations sun20i_csi_bridge_entity_ops = {
+	.link_validate		= v4l2_subdev_link_validate,
+};
+
+/* -------------------------------------------------------------------------
+ * Capture video device (the DMA engine)
+ */
+
+static void sun20i_csi_fill_format(struct v4l2_pix_format *pix)
+{
+	const struct sun20i_csi_format *fmt;
+	u32 size, width, height;
+
+	fmt = sun20i_csi_find_format(pix->pixelformat);
+	if (!fmt)
+		fmt = &sun20i_csi_formats[0];
+
+	width = clamp_t(u32, ALIGN(pix->width, 8), SUN20I_CSI_MIN_SIZE,
+			SUN20I_CSI_MAX_WIDTH);
+	height = clamp_t(u32, ALIGN(pix->height, 2), SUN20I_CSI_MIN_SIZE,
+			 SUN20I_CSI_MAX_HEIGHT);
+
+	pix->pixelformat = fmt->fourcc;
+	pix->width = width;
+	pix->height = height;
+	pix->field = V4L2_FIELD_NONE;
+	pix->bytesperline = width * fmt->bpp;
+
+	size = pix->bytesperline * height;
+	/* Semi-planar: full-width UV lines. Planar: two half-width planes. */
+	if (fmt->planes > 1)
+		size += width * (height / fmt->vsub);
+	pix->sizeimage = size;
+
+	pix->colorspace = fmt->bayer ? V4L2_COLORSPACE_RAW :
+				       V4L2_COLORSPACE_SRGB;
+	pix->ycbcr_enc = V4L2_YCBCR_ENC_DEFAULT;
+	pix->quantization = V4L2_QUANTIZATION_DEFAULT;
+	pix->xfer_func = V4L2_XFER_FUNC_DEFAULT;
+	pix->flags = 0;
+}
+
+static int sun20i_csi_querycap(struct file *file, void *priv,
+			       struct v4l2_capability *cap)
+{
+	strscpy(cap->driver, "sun20i-csi", sizeof(cap->driver));
+	strscpy(cap->card, "Allwinner D1 CSIC", sizeof(cap->card));
+
+	return 0;
+}
+
+static int sun20i_csi_enum_fmt(struct file *file, void *priv,
+			       struct v4l2_fmtdesc *f)
+{
+	unsigned int i, index = 0;
+
+	for (i = 0; i < ARRAY_SIZE(sun20i_csi_formats); i++) {
+		const struct sun20i_csi_format *fmt = &sun20i_csi_formats[i];
+
+		if (f->mbus_code && !sun20i_csi_format_takes(fmt, f->mbus_code))
+			continue;
+		if (index++ == f->index) {
+			f->pixelformat = fmt->fourcc;
+			return 0;
+		}
+	}
+
+	return -EINVAL;
+}
+
+static int sun20i_csi_enum_framesizes(struct file *file, void *priv,
+				      struct v4l2_frmsizeenum *fsize)
+{
+	if (fsize->index || !sun20i_csi_find_format(fsize->pixel_format))
+		return -EINVAL;
+
+	fsize->type = V4L2_FRMSIZE_TYPE_STEPWISE;
+	fsize->stepwise.min_width = SUN20I_CSI_MIN_SIZE;
+	fsize->stepwise.max_width = SUN20I_CSI_MAX_WIDTH;
+	fsize->stepwise.step_width = 8;
+	fsize->stepwise.min_height = SUN20I_CSI_MIN_SIZE;
+	fsize->stepwise.max_height = SUN20I_CSI_MAX_HEIGHT;
+	fsize->stepwise.step_height = 2;
+
+	return 0;
+}
+
+static int sun20i_csi_g_fmt(struct file *file, void *priv,
+			    struct v4l2_format *f)
+{
+	struct sun20i_csi *csi = video_drvdata(file);
+
+	f->fmt.pix = csi->format;
+
+	return 0;
+}
+
+static int sun20i_csi_try_fmt(struct file *file, void *priv,
+			      struct v4l2_format *f)
+{
+	sun20i_csi_fill_format(&f->fmt.pix);
+
+	return 0;
+}
+
+static int sun20i_csi_s_fmt(struct file *file, void *priv,
+			    struct v4l2_format *f)
+{
+	struct sun20i_csi *csi = video_drvdata(file);
+
+	if (vb2_is_busy(&csi->queue))
+		return -EBUSY;
+
+	sun20i_csi_fill_format(&f->fmt.pix);
+	csi->format = f->fmt.pix;
+	csi->fmt = sun20i_csi_find_format(f->fmt.pix.pixelformat);
+
+	return 0;
+}
+
+static const struct v4l2_ioctl_ops sun20i_csi_ioctl_ops = {
+	.vidioc_querycap		= sun20i_csi_querycap,
+	.vidioc_enum_fmt_vid_cap	= sun20i_csi_enum_fmt,
+	.vidioc_enum_framesizes		= sun20i_csi_enum_framesizes,
+	.vidioc_g_fmt_vid_cap		= sun20i_csi_g_fmt,
+	.vidioc_try_fmt_vid_cap		= sun20i_csi_try_fmt,
+	.vidioc_s_fmt_vid_cap		= sun20i_csi_s_fmt,
+
+	.vidioc_reqbufs			= vb2_ioctl_reqbufs,
+	.vidioc_create_bufs		= vb2_ioctl_create_bufs,
+	.vidioc_prepare_buf		= vb2_ioctl_prepare_buf,
+	.vidioc_querybuf		= vb2_ioctl_querybuf,
+	.vidioc_qbuf			= vb2_ioctl_qbuf,
+	.vidioc_dqbuf			= vb2_ioctl_dqbuf,
+	.vidioc_expbuf			= vb2_ioctl_expbuf,
+	.vidioc_streamon		= vb2_ioctl_streamon,
+	.vidioc_streamoff		= vb2_ioctl_streamoff,
+};
+
+static const struct v4l2_file_operations sun20i_csi_fops = {
+	.owner		= THIS_MODULE,
+	.open		= v4l2_fh_open,
+	.release	= vb2_fop_release,
+	.unlocked_ioctl	= video_ioctl2,
+	.mmap		= vb2_fop_mmap,
+	.poll		= vb2_fop_poll,
+};
+
+static int sun20i_csi_queue_setup(struct vb2_queue *queue,
+				  unsigned int *num_buffers,
+				  unsigned int *num_planes,
+				  unsigned int sizes[],
+				  struct device *alloc_devs[])
+{
+	struct sun20i_csi *csi = vb2_get_drv_priv(queue);
+
+	if (*num_planes)
+		return sizes[0] < csi->format.sizeimage ? -EINVAL : 0;
+
+	*num_planes = 1;
+	sizes[0] = csi->format.sizeimage;
+
+	return 0;
+}
+
+static int sun20i_csi_buf_prepare(struct vb2_buffer *vb)
+{
+	struct sun20i_csi *csi = vb2_get_drv_priv(vb->vb2_queue);
+
+	if (vb2_plane_size(vb, 0) < csi->format.sizeimage)
+		return -EINVAL;
+
+	vb2_set_plane_payload(vb, 0, csi->format.sizeimage);
+
+	return 0;
+}
+
+static void sun20i_csi_buf_queue(struct vb2_buffer *vb)
+{
+	struct sun20i_csi *csi = vb2_get_drv_priv(vb->vb2_queue);
+	struct vb2_v4l2_buffer *vbuf = to_vb2_v4l2_buffer(vb);
+	struct sun20i_csi_buffer *buf =
+		container_of(vbuf, struct sun20i_csi_buffer, vb);
+	unsigned long flags;
+
+	spin_lock_irqsave(&csi->irqlock, flags);
+	list_add_tail(&buf->list, &csi->queued);
+	spin_unlock_irqrestore(&csi->irqlock, flags);
+}
+
+static void sun20i_csi_return_buffers(struct sun20i_csi *csi,
+				      enum vb2_buffer_state state)
+{
+	struct sun20i_csi_buffer *buf, *tmp;
+	unsigned long flags;
+
+	spin_lock_irqsave(&csi->irqlock, flags);
+
+	if (csi->active)
+		vb2_buffer_done(&csi->active->vb.vb2_buf, state);
+	if (csi->next && csi->next != csi->active)
+		vb2_buffer_done(&csi->next->vb.vb2_buf, state);
+	csi->active = NULL;
+	csi->next = NULL;
+
+	list_for_each_entry_safe(buf, tmp, &csi->queued, list) {
+		list_del(&buf->list);
+		vb2_buffer_done(&buf->vb.vb2_buf, state);
+	}
+
+	spin_unlock_irqrestore(&csi->irqlock, flags);
+}
+
+static void sun20i_csi_dma_start(struct sun20i_csi *csi)
+{
+	const struct sun20i_csi_format *fmt = csi->fmt;
+	u32 width = csi->format.width, height = csi->format.height;
+	u32 line_c = fmt->planes == 3 ? width / 2 : width;
+	unsigned long flags;
+
+	/*
+	 * This relies on two reset values: the CSIC's own clock gates are
+	 * bypassed (CCU_CLK_MODE_REG), and DMA0 takes input 0, "ISP0 CH0",
+	 * which carries the parser's channel 0 (CSIC_DMA0_INPUT_SEL_REG).
+	 */
+	sun20i_csi_write(csi, SUN20I_CSI_TOP_EN_REG,
+			 SUN20I_CSI_TOP_EN_CSIC_TOP_EN);
+
+	sun20i_csi_write(csi, SUN20I_CSI_DMA_CFG_REG,
+			 FIELD_PREP(SUN20I_CSI_DMA_CFG_OUTPUT_FMT,
+				    fmt->output));
+	sun20i_csi_write(csi, SUN20I_CSI_DMA_HSIZE_REG,
+			 FIELD_PREP(SUN20I_CSI_LEN,
+				    sun20i_csi_line_units(fmt, width)));
+	sun20i_csi_write(csi, SUN20I_CSI_DMA_VSIZE_REG,
+			 FIELD_PREP(SUN20I_CSI_LEN, height));
+	sun20i_csi_write(csi, SUN20I_CSI_DMA_BUF_LEN_REG,
+			 FIELD_PREP(SUN20I_CSI_DMA_BUF_LEN_BUF_LEN_C, line_c) |
+			 FIELD_PREP(SUN20I_CSI_DMA_BUF_LEN_BUF_LEN,
+				    csi->format.bytesperline));
+
+	spin_lock_irqsave(&csi->irqlock, flags);
+
+	csi->sequence = 0;
+	csi->errors = 0;
+	csi->active = NULL;
+	/* vb2 makes sure that min_queued_buffers are queued. */
+	csi->next = list_first_entry(&csi->queued, struct sun20i_csi_buffer,
+				     list);
+	list_del(&csi->next->list);
+	sun20i_csi_set_buffer(csi, csi->next);
+
+	spin_unlock_irqrestore(&csi->irqlock, flags);
+
+	/* The line index counts from 0: this fires on the last line. */
+	sun20i_csi_write(csi, SUN20I_CSI_DMA_LINE_CNT_REG,
+			 FIELD_PREP(SUN20I_CSI_DMA_LINE_CNT_LINE_CNT_NUM,
+				    height - 1));
+	sun20i_csi_write(csi, SUN20I_CSI_DMA_INT_STA_REG, ~0);
+	sun20i_csi_write(csi, SUN20I_CSI_DMA_INT_EN_REG,
+			 SUN20I_CSI_DMA_INT_VS | SUN20I_CSI_DMA_INT_LC |
+			 SUN20I_CSI_DMA_INT_FD | SUN20I_CSI_DMA_INT_OF);
+	sun20i_csi_write(csi, SUN20I_CSI_DMA_EN_REG,
+			 SUN20I_CSI_DMA_EN_SW_CFG_MODE |
+			 SUN20I_CSI_DMA_EN_DMA_EN |
+			 SUN20I_CSI_DMA_EN_BK_TOP_EN);
+}
+
+static void sun20i_csi_dma_stop(struct sun20i_csi *csi)
+{
+	sun20i_csi_write(csi, SUN20I_CSI_DMA_INT_EN_REG, 0);
+	sun20i_csi_write(csi, SUN20I_CSI_DMA_EN_REG,
+			 SUN20I_CSI_DMA_EN_SW_CFG_MODE);
+	sun20i_csi_write(csi, SUN20I_CSI_DMA_INT_STA_REG, ~0);
+	sun20i_csi_write(csi, SUN20I_CSI_TOP_EN_REG, 0);
+	synchronize_irq(csi->irq);
+}
+
+static int sun20i_csi_start_streaming(struct vb2_queue *queue,
+				      unsigned int count)
+{
+	struct sun20i_csi *csi = vb2_get_drv_priv(queue);
+	int ret;
+
+	ret = pm_runtime_resume_and_get(csi->dev);
+	if (ret)
+		goto err_return_buffers;
+
+	ret = video_device_pipeline_alloc_start(&csi->vdev);
+	if (ret)
+		goto err_pm_put;
+
+	sun20i_csi_dma_start(csi);
+
+	ret = v4l2_subdev_enable_streams(&csi->subdev, SUN20I_CSI_PAD_SOURCE,
+					 BIT_ULL(0));
+	if (ret)
+		goto err_dma_stop;
+
+	return 0;
+
+err_dma_stop:
+	sun20i_csi_dma_stop(csi);
+	video_device_pipeline_stop(&csi->vdev);
+err_pm_put:
+	pm_runtime_put(csi->dev);
+err_return_buffers:
+	sun20i_csi_return_buffers(csi, VB2_BUF_STATE_QUEUED);
+	return ret;
+}
+
+static void sun20i_csi_stop_streaming(struct vb2_queue *queue)
+{
+	struct sun20i_csi *csi = vb2_get_drv_priv(queue);
+
+	v4l2_subdev_disable_streams(&csi->subdev, SUN20I_CSI_PAD_SOURCE,
+				    BIT_ULL(0));
+	sun20i_csi_dma_stop(csi);
+	video_device_pipeline_stop(&csi->vdev);
+	sun20i_csi_return_buffers(csi, VB2_BUF_STATE_ERROR);
+	pm_runtime_put(csi->dev);
+
+	if (csi->errors)
+		dev_dbg(csi->dev, "%u FIFO overflows in %u frames\n",
+			csi->errors, csi->sequence);
+}
+
+static const struct vb2_ops sun20i_csi_vb2_ops = {
+	.queue_setup		= sun20i_csi_queue_setup,
+	.buf_prepare		= sun20i_csi_buf_prepare,
+	.buf_queue		= sun20i_csi_buf_queue,
+	.start_streaming	= sun20i_csi_start_streaming,
+	.stop_streaming		= sun20i_csi_stop_streaming,
+};
+
+static int sun20i_csi_capture_link_validate(struct media_link *link)
+{
+	struct video_device *vdev =
+		media_entity_to_video_device(link->sink->entity);
+	struct sun20i_csi *csi = video_get_drvdata(vdev);
+	const struct v4l2_mbus_framefmt *fmt;
+	struct v4l2_subdev_state *state;
+	int ret = 0;
+
+	state = v4l2_subdev_lock_and_get_active_state(&csi->subdev);
+	fmt = v4l2_subdev_state_get_format(state, SUN20I_CSI_PAD_SOURCE);
+
+	if (fmt->width != csi->format.width ||
+	    fmt->height != csi->format.height) {
+		dev_dbg(csi->dev, "size mismatch: %ux%u on the bridge, %ux%u on the video device\n",
+			fmt->width, fmt->height,
+			csi->format.width, csi->format.height);
+		ret = -EPIPE;
+	} else if (!sun20i_csi_format_takes(csi->fmt, fmt->code)) {
+		dev_dbg(csi->dev, "%p4cc can't be captured from bus code 0x%04x\n",
+			&csi->format.pixelformat, fmt->code);
+		ret = -EPIPE;
+	}
+
+	v4l2_subdev_unlock_state(state);
+
+	return ret;
+}
+
+static const struct media_entity_operations sun20i_csi_capture_entity_ops = {
+	.link_validate		= sun20i_csi_capture_link_validate,
+};
+
+/* -------------------------------------------------------------------------
+ * Probe
+ */
+
+static int sun20i_csi_notify_bound(struct v4l2_async_notifier *notifier,
+				   struct v4l2_subdev *sd,
+				   struct v4l2_async_connection *asc)
+{
+	struct sun20i_csi *csi = container_of(notifier, struct sun20i_csi,
+					      notifier);
+	struct media_pad *pad = &csi->pads[SUN20I_CSI_PAD_SINK];
+	int ret;
+
+	ret = v4l2_create_fwnode_links_to_pad(sd, pad, MEDIA_LNK_FL_ENABLED |
+					      MEDIA_LNK_FL_IMMUTABLE);
+	if (ret)
+		return ret;
+
+	pad = media_pad_remote_pad_first(pad);
+	if (!pad)
+		return -ENOLINK;
+
+	csi->source = sd;
+	csi->source_pad = pad->index;
+
+	return 0;
+}
+
+static int sun20i_csi_notify_complete(struct v4l2_async_notifier *notifier)
+{
+	struct sun20i_csi *csi = container_of(notifier, struct sun20i_csi,
+					      notifier);
+	int ret;
+
+	ret = v4l2_device_register_subdev_nodes(&csi->v4l2_dev);
+	if (ret)
+		return ret;
+
+	return media_device_register(&csi->mdev);
+}
+
+static const struct v4l2_async_notifier_operations sun20i_csi_notify_ops = {
+	.bound		= sun20i_csi_notify_bound,
+	.complete	= sun20i_csi_notify_complete,
+};
+
+static int sun20i_csi_parse_dt(struct sun20i_csi *csi)
+{
+	struct v4l2_fwnode_endpoint vep = { .bus_type = V4L2_MBUS_PARALLEL };
+	struct v4l2_async_connection *asc;
+	struct fwnode_handle *ep;
+	int ret;
+
+	ep = fwnode_graph_get_endpoint_by_id(dev_fwnode(csi->dev), 0, 0, 0);
+	if (!ep)
+		return dev_err_probe(csi->dev, -ENODEV, "no input endpoint\n");
+
+	ret = v4l2_fwnode_endpoint_parse(ep, &vep);
+	if (ret) {
+		dev_err_probe(csi->dev, ret, "invalid input endpoint\n");
+		goto out;
+	}
+	if (vep.bus.parallel.bus_width != 8 || vep.bus.parallel.data_shift) {
+		ret = dev_err_probe(csi->dev, -EINVAL,
+				    "only an 8-bit bus is supported\n");
+		goto out;
+	}
+	csi->bus = vep.bus.parallel;
+
+	asc = v4l2_async_nf_add_fwnode_remote(&csi->notifier, ep,
+					      struct v4l2_async_connection);
+	if (IS_ERR(asc))
+		ret = PTR_ERR(asc);
+
+out:
+	fwnode_handle_put(ep);
+	return ret;
+}
+
+static int sun20i_csi_register_bridge(struct sun20i_csi *csi)
+{
+	struct v4l2_subdev *sd = &csi->subdev;
+	int ret;
+
+	v4l2_subdev_init(sd, &sun20i_csi_bridge_ops);
+	sd->internal_ops = &sun20i_csi_bridge_internal_ops;
+	sd->owner = THIS_MODULE;
+	sd->dev = csi->dev;
+	sd->flags |= V4L2_SUBDEV_FL_HAS_DEVNODE;
+	strscpy(sd->name, "sun20i-csi-bridge", sizeof(sd->name));
+
+	sd->entity.function = MEDIA_ENT_F_VID_IF_BRIDGE;
+	sd->entity.ops = &sun20i_csi_bridge_entity_ops;
+	csi->pads[SUN20I_CSI_PAD_SINK].flags = MEDIA_PAD_FL_SINK |
+					       MEDIA_PAD_FL_MUST_CONNECT;
+	csi->pads[SUN20I_CSI_PAD_SOURCE].flags = MEDIA_PAD_FL_SOURCE |
+						 MEDIA_PAD_FL_MUST_CONNECT;
+	ret = media_entity_pads_init(&sd->entity, SUN20I_CSI_NUM_PADS,
+				     csi->pads);
+	if (ret)
+		return ret;
+
+	ret = v4l2_subdev_init_finalize(sd);
+	if (ret)
+		goto err_entity;
+
+	ret = v4l2_device_register_subdev(&csi->v4l2_dev, sd);
+	if (ret)
+		goto err_subdev;
+
+	return 0;
+
+err_subdev:
+	v4l2_subdev_cleanup(sd);
+err_entity:
+	media_entity_cleanup(&sd->entity);
+	return ret;
+}
+
+static void sun20i_csi_unregister_bridge(struct sun20i_csi *csi)
+{
+	v4l2_device_unregister_subdev(&csi->subdev);
+	v4l2_subdev_cleanup(&csi->subdev);
+	media_entity_cleanup(&csi->subdev.entity);
+}
+
+static int sun20i_csi_register_capture(struct sun20i_csi *csi)
+{
+	struct video_device *vdev = &csi->vdev;
+	struct vb2_queue *queue = &csi->queue;
+	int ret;
+
+	csi->format.pixelformat = sun20i_csi_formats[0].fourcc;
+	csi->format.width = sun20i_csi_default_fmt.width;
+	csi->format.height = sun20i_csi_default_fmt.height;
+	sun20i_csi_fill_format(&csi->format);
+	csi->fmt = &sun20i_csi_formats[0];
+
+	queue->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+	queue->io_modes = VB2_MMAP | VB2_DMABUF;
+	queue->dev = csi->dev;
+	queue->drv_priv = csi;
+	queue->buf_struct_size = sizeof(struct sun20i_csi_buffer);
+	queue->ops = &sun20i_csi_vb2_ops;
+	queue->mem_ops = &vb2_dma_contig_memops;
+	queue->timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC;
+	queue->min_queued_buffers = 2;
+	queue->lock = &csi->lock;
+	ret = vb2_queue_init(queue);
+	if (ret)
+		return ret;
+
+	csi->vdev_pad.flags = MEDIA_PAD_FL_SINK | MEDIA_PAD_FL_MUST_CONNECT;
+	vdev->entity.ops = &sun20i_csi_capture_entity_ops;
+	ret = media_entity_pads_init(&vdev->entity, 1, &csi->vdev_pad);
+	if (ret)
+		return ret;
+
+	strscpy(vdev->name, "sun20i-csi-capture", sizeof(vdev->name));
+	vdev->v4l2_dev = &csi->v4l2_dev;
+	vdev->fops = &sun20i_csi_fops;
+	vdev->ioctl_ops = &sun20i_csi_ioctl_ops;
+	vdev->release = video_device_release_empty;
+	vdev->lock = &csi->lock;
+	vdev->queue = queue;
+	vdev->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING |
+			    V4L2_CAP_IO_MC;
+	video_set_drvdata(vdev, csi);
+
+	ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
+	if (ret)
+		goto err_entity;
+
+	ret = media_create_pad_link(&csi->subdev.entity, SUN20I_CSI_PAD_SOURCE,
+				    &vdev->entity, 0, MEDIA_LNK_FL_ENABLED |
+				    MEDIA_LNK_FL_IMMUTABLE);
+	if (ret)
+		goto err_video;
+
+	return 0;
+
+err_video:
+	vb2_video_unregister_device(vdev);
+err_entity:
+	media_entity_cleanup(&vdev->entity);
+	return ret;
+}
+
+static int sun20i_csi_runtime_suspend(struct device *dev)
+{
+	struct sun20i_csi *csi = dev_get_drvdata(dev);
+
+	clk_disable_unprepare(csi->mod_clk);
+	clk_disable_unprepare(csi->ram_clk);
+	clk_disable_unprepare(csi->bus_clk);
+	reset_control_assert(csi->reset);
+
+	return 0;
+}
+
+static int sun20i_csi_runtime_resume(struct device *dev)
+{
+	struct sun20i_csi *csi = dev_get_drvdata(dev);
+	int ret;
+
+	ret = reset_control_deassert(csi->reset);
+	if (ret)
+		return ret;
+
+	ret = clk_prepare_enable(csi->bus_clk);
+	if (ret)
+		goto err_reset;
+
+	ret = clk_prepare_enable(csi->ram_clk);
+	if (ret)
+		goto err_bus_clk;
+
+	ret = clk_prepare_enable(csi->mod_clk);
+	if (ret)
+		goto err_ram_clk;
+
+	return 0;
+
+err_ram_clk:
+	clk_disable_unprepare(csi->ram_clk);
+err_bus_clk:
+	clk_disable_unprepare(csi->bus_clk);
+err_reset:
+	reset_control_assert(csi->reset);
+	return ret;
+}
+
+static int sun20i_csi_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct sun20i_csi *csi;
+	int ret;
+
+	csi = devm_kzalloc(dev, sizeof(*csi), GFP_KERNEL);
+	if (!csi)
+		return -ENOMEM;
+
+	csi->dev = dev;
+	platform_set_drvdata(pdev, csi);
+	spin_lock_init(&csi->irqlock);
+	INIT_LIST_HEAD(&csi->queued);
+
+	ret = devm_mutex_init(dev, &csi->lock);
+	if (ret)
+		return ret;
+
+	csi->regs = devm_platform_ioremap_resource(pdev, 0);
+	if (IS_ERR(csi->regs))
+		return PTR_ERR(csi->regs);
+
+	csi->bus_clk = devm_clk_get(dev, "bus");
+	if (IS_ERR(csi->bus_clk))
+		return dev_err_probe(dev, PTR_ERR(csi->bus_clk),
+				     "failed to get the bus clock\n");
+
+	csi->mod_clk = devm_clk_get(dev, "mod");
+	if (IS_ERR(csi->mod_clk))
+		return dev_err_probe(dev, PTR_ERR(csi->mod_clk),
+				     "failed to get the module clock\n");
+
+	csi->ram_clk = devm_clk_get(dev, "ram");
+	if (IS_ERR(csi->ram_clk))
+		return dev_err_probe(dev, PTR_ERR(csi->ram_clk),
+				     "failed to get the DRAM clock\n");
+
+	csi->reset = devm_reset_control_get_exclusive(dev, NULL);
+	if (IS_ERR(csi->reset))
+		return dev_err_probe(dev, PTR_ERR(csi->reset),
+				     "failed to get the reset\n");
+
+	ret = devm_clk_rate_exclusive_get(dev, csi->mod_clk);
+	if (ret)
+		return dev_err_probe(dev, ret,
+				     "failed to lock the module clock rate\n");
+
+	ret = clk_set_rate(csi->mod_clk, SUN20I_CSI_MOD_RATE);
+	if (ret)
+		return dev_err_probe(dev, ret,
+				     "failed to set the module clock rate\n");
+
+	csi->irq = platform_get_irq(pdev, 0);
+	if (csi->irq < 0)
+		return csi->irq;
+
+	ret = devm_request_irq(dev, csi->irq, sun20i_csi_irq, 0,
+			       dev_name(dev), csi);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to request the IRQ\n");
+
+	ret = devm_pm_runtime_enable(dev);
+	if (ret)
+		return ret;
+
+	csi->mdev.dev = dev;
+	strscpy(csi->mdev.model, "Allwinner D1 CSIC", sizeof(csi->mdev.model));
+	media_device_init(&csi->mdev);
+	csi->v4l2_dev.mdev = &csi->mdev;
+
+	ret = v4l2_device_register(dev, &csi->v4l2_dev);
+	if (ret)
+		goto err_media;
+
+	/* Check the DT before any device node is created. */
+	v4l2_async_nf_init(&csi->notifier, &csi->v4l2_dev);
+	csi->notifier.ops = &sun20i_csi_notify_ops;
+
+	ret = sun20i_csi_parse_dt(csi);
+	if (ret)
+		goto err_notifier;
+
+	ret = sun20i_csi_register_bridge(csi);
+	if (ret)
+		goto err_notifier;
+
+	ret = sun20i_csi_register_capture(csi);
+	if (ret)
+		goto err_bridge;
+
+	ret = v4l2_async_nf_register(&csi->notifier);
+	if (ret)
+		goto err_capture;
+
+	return 0;
+
+err_capture:
+	vb2_video_unregister_device(&csi->vdev);
+	media_entity_cleanup(&csi->vdev.entity);
+err_bridge:
+	sun20i_csi_unregister_bridge(csi);
+err_notifier:
+	v4l2_async_nf_cleanup(&csi->notifier);
+	v4l2_device_unregister(&csi->v4l2_dev);
+err_media:
+	media_device_cleanup(&csi->mdev);
+	return ret;
+}
+
+static void sun20i_csi_remove(struct platform_device *pdev)
+{
+	struct sun20i_csi *csi = platform_get_drvdata(pdev);
+
+	/* This stops streaming, which needs the whole pipeline. */
+	vb2_video_unregister_device(&csi->vdev);
+	media_entity_cleanup(&csi->vdev.entity);
+	v4l2_async_nf_unregister(&csi->notifier);
+	v4l2_async_nf_cleanup(&csi->notifier);
+	sun20i_csi_unregister_bridge(csi);
+	media_device_unregister(&csi->mdev);
+	v4l2_device_unregister(&csi->v4l2_dev);
+	media_device_cleanup(&csi->mdev);
+}
+
+/* Runtime PM only: a system sleep must not reset the block while streaming. */
+static const struct dev_pm_ops sun20i_csi_pm_ops = {
+	RUNTIME_PM_OPS(sun20i_csi_runtime_suspend, sun20i_csi_runtime_resume,
+		       NULL)
+};
+
+static const struct of_device_id sun20i_csi_of_match[] = {
+	{ .compatible = "allwinner,sun20i-d1-csi" },
+	{ /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, sun20i_csi_of_match);
+
+static struct platform_driver sun20i_csi_driver = {
+	.probe	= sun20i_csi_probe,
+	.remove	= sun20i_csi_remove,
+	.driver	= {
+		.name		= "sun20i-csi",
+		.of_match_table	= sun20i_csi_of_match,
+		.pm		= pm_ptr(&sun20i_csi_pm_ops),
+	},
+};
+module_platform_driver(sun20i_csi_driver);
+
+MODULE_DESCRIPTION("Allwinner D1/T113 CSIC driver");
+MODULE_AUTHOR("Nguyen Minh Tien <tien.nguyenminh at embeddedlinux.blog>");
+MODULE_LICENSE("GPL");
-- 
2.34.1




More information about the linux-riscv mailing list