ARM Sunxi Platform Development
 help / color / mirror / Atom feed
* [PATCH 0/3] media: sunxi: Add the Allwinner D1/T113 camera interface
@ 2026-09-30 15:48 Nguyen Minh Tien
  2026-09-30 15:48 ` [PATCH 1/3] dt-bindings: media: Add Allwinner D1 CSIC Nguyen Minh Tien
                   ` (2 more replies)
  0 siblings, 3 replies; 7+ messages in thread
From: Nguyen Minh Tien @ 2026-09-30 15:48 UTC (permalink / raw)
  To: Mauro Carvalho Chehab, Hans Verkuil
  Cc: Nguyen Minh Tien, Sakari Ailus, Laurent Pinchart,
	Paul Kocialkowski, Chen-Yu Tsai, Jernej Skrabec, Samuel Holland,
	Rob Herring, Krzysztof Kozlowski, Conor Dooley, Paul Walmsley,
	Palmer Dabbelt, Albert Ou, Alexandre Ghiti, Philipp Zabel,
	linux-media, devicetree, linux-arm-kernel, linux-sunxi,
	linux-riscv, linux-kernel

This series adds a driver for the camera interface of the Allwinner D1,
D1s and T113, which the manual calls the CSIC. It is not the A31 CSI
that sun6i-csi drives: the parser and the DMA engines are separate
blocks with their own registers, so it gets a new driver.

1. The DT binding.
2. The driver: the parallel input and the first DMA engine, as a bridge
   subdev and a capture video device (media controller API).
3. The CSIC node in the dtsi shared by the D1, D1s and T113.

I tested it on a MangoPi MQ-Dual (T113-S3) with an OV5640. With this
series on top of media next, v4l2-compliance passes for the media
device, the bridge and the video node (streaming output below; the two
failures on the sensor's subdev are the OV5640 driver's frame size and
interval enumeration). With the sensor's colour bars, every converted
YUV format matches the raw capture byte for byte, and 640x480 and
1920x1080 run at 30 fps.

On the board's 6.18 kernel with the driver backported (only the new
set_fmt argument differs), 160x120, 176x144, 320x240, 640x480, 720x480,
720x576, 1280x720 and 1920x1080 all run at 30 fps.

The OV5640's 1024x768 and 2592x1944 modes give no valid frames on this
board, and I haven't found out why yet.

I couldn't make the FIFO overflow, not even at 1920x1080 with both CPUs
and the 2D engine keeping the DRAM busy, so the overflow path (the
buffer is returned with an error and capture goes on) has never run.
sun6i-csi restarts the block on an overflow instead; I left that out,
as I can't test it.

Not tested: a D1 board (build and dtbs_check only), BT.656 (not
supported yet), other sensors, the second DMA engine, the IOMMU and
system suspend while streaming.

The series is based on the media tree's next branch.

v4l2-compliance from v4l-utils git, on media next, with vivid as the
DMABUF exporter (v4l2-compliance -d /dev/video4 -s -e /dev/video0):

v4l2-compliance 1.33.0-5506, 32 bits, 64-bit time_t
v4l2-compliance SHA: 91dda32e4cc9 2026-09-28 07:58:16

Compliance test for sun20i-csi device /dev/video4:

Driver Info:
	Driver name      : sun20i-csi
	Card type        : Allwinner D1 CSIC
	Bus info         : platform:5800000.csi
	Driver version   : 7.3.0
	Capabilities     : 0xa4200001
		Video Capture
		I/O MC
		Streaming
		Extended Pix Format
		Device Capabilities
	Device Caps      : 0x24200001
		Video Capture
		I/O MC
		Streaming
		Extended Pix Format
Media Driver Info:
	Driver name      : sun20i-csi
	Model            : Allwinner D1 CSIC
	Serial           :
	Bus info         : platform:5800000.csi
	Media version    : 7.3.0
	Hardware revision: 0x00000000 (0)
	Driver version   : 7.3.0
Interface Info:
	ID               : 0x03000006
	Type             : V4L Video
Entity Info:
	ID               : 0x00000004 (4)
	Name             : sun20i-csi-capture
	Function         : V4L2 I/O
	Pad 0x01000005   : 0: Sink, Must Connect
	  Link 0x02000008: from remote pad 0x1000003 of entity 'sun20i-csi-bridge' (Video Interface Bridge): Data, Enabled, Immutable

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

Allow for multiple opens:
	test second /dev/video4 open: OK
	test VIDIOC_QUERYCAP: OK
	test VIDIOC_G/S_PRIORITY: OK
	test for unlimited opens: OK

Debug ioctls:
	test VIDIOC_DBG_G/S_REGISTER: OK
	test VIDIOC_LOG_STATUS: OK (Not Supported)

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

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

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

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

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

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

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

Test input 0:

Streaming ioctls:
	test read/write: OK (Not Supported)
	test MMAP (no poll, REQBUFS): OK
	test MMAP (select, REQBUFS): OK
	test MMAP (epoll, REQBUFS): OK
	test MMAP (no poll, CREATE_BUFS): OK
	test MMAP (select, CREATE_BUFS): OK
	test MMAP (epoll, CREATE_BUFS): OK
	test USERPTR (no poll): OK (Not Supported)
	test USERPTR (select): OK (Not Supported)
	test DMABUF (no poll): OK
	test DMABUF (select): OK

Total for sun20i-csi device /dev/video4: 61, Succeeded: 61, Failed: 0, Warnings: 0

Nguyen Minh Tien (3):
  dt-bindings: media: Add Allwinner D1 CSIC
  media: sunxi: Add support for the D1 CSIC
  riscv: dts: allwinner: d1s-t113: Add the CSIC node

 .../media/allwinner,sun20i-d1-csi.yaml        |  111 ++
 MAINTAINERS                                   |    8 +
 .../boot/dts/allwinner/sunxi-d1s-t113.dtsi    |   31 +
 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 +++++++++++++++++
 8 files changed, 1530 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/media/allwinner,sun20i-d1-csi.yaml
 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


base-commit: 5f21cd9a4ae4a891b273178b40e0e9eb5b9ac650
-- 
2.34.1


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

* [PATCH 1/3] dt-bindings: media: Add Allwinner D1 CSIC
  2026-09-30 15:48 [PATCH 0/3] media: sunxi: Add the Allwinner D1/T113 camera interface Nguyen Minh Tien
@ 2026-09-30 15:48 ` Nguyen Minh Tien
  2026-10-02  6:14   ` Krzysztof Kozlowski
  2026-09-30 15:48 ` [PATCH 2/3] media: sunxi: Add support for the " Nguyen Minh Tien
  2026-09-30 15:48 ` [PATCH 3/3] riscv: dts: allwinner: d1s-t113: Add the CSIC node Nguyen Minh Tien
  2 siblings, 1 reply; 7+ messages in thread
From: Nguyen Minh Tien @ 2026-09-30 15:48 UTC (permalink / raw)
  To: Mauro Carvalho Chehab, Hans Verkuil
  Cc: Nguyen Minh Tien, Sakari Ailus, Laurent Pinchart,
	Paul Kocialkowski, Chen-Yu Tsai, Jernej Skrabec, Samuel Holland,
	Rob Herring, Krzysztof Kozlowski, Conor Dooley, Paul Walmsley,
	Palmer Dabbelt, Albert Ou, Alexandre Ghiti, Philipp Zabel,
	linux-media, devicetree, linux-arm-kernel, linux-sunxi,
	linux-riscv, linux-kernel

Add a binding for the camera interface (CSIC) of the Allwinner D1, D1s
and T113. It is not the A31 CSI: its parser and DMA engines are separate
blocks. Only the 8-bit parallel input is pinned out on these SoCs.

Signed-off-by: Nguyen Minh Tien <tien.nguyenminh@embeddedlinux.blog>
---
 .../media/allwinner,sun20i-d1-csi.yaml        | 111 ++++++++++++++++++
 MAINTAINERS                                   |   7 ++
 2 files changed, 118 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/media/allwinner,sun20i-d1-csi.yaml

diff --git a/Documentation/devicetree/bindings/media/allwinner,sun20i-d1-csi.yaml b/Documentation/devicetree/bindings/media/allwinner,sun20i-d1-csi.yaml
new file mode 100644
index 0000000000..2bfe2cc9af
--- /dev/null
+++ b/Documentation/devicetree/bindings/media/allwinner,sun20i-d1-csi.yaml
@@ -0,0 +1,111 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/media/allwinner,sun20i-d1-csi.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Allwinner D1 CMOS Sensor Interface Controller (CSIC)
+
+maintainers:
+  - Nguyen Minh Tien <tien.nguyenminh@embeddedlinux.blog>
+
+description:
+  The CSIC found in the Allwinner D1, D1s and T113 receives video from a
+  parallel camera interface and writes it to memory. It contains a parser for
+  the camera bus and two DMA engines.
+
+properties:
+  compatible:
+    const: allwinner,sun20i-d1-csi
+
+  reg:
+    maxItems: 1
+
+  interrupts:
+    items:
+      - description: DMA engine 0
+      - description: DMA engine 1
+      - description: Parser 0
+      - description: Top-level block (CSI_TOP_PKT)
+
+  clocks:
+    items:
+      - description: Bus clock
+      - description: Module clock
+      - description: DRAM clock
+
+  clock-names:
+    items:
+      - const: bus
+      - const: mod
+      - const: ram
+
+  resets:
+    maxItems: 1
+
+  iommus:
+    maxItems: 1
+
+  port:
+    $ref: /schemas/graph.yaml#/$defs/port-base
+    description: Parallel input port, connect to a parallel sensor
+    unevaluatedProperties: false
+
+    properties:
+      endpoint:
+        $ref: video-interfaces.yaml#
+        unevaluatedProperties: false
+
+        properties:
+          bus-width:
+            const: 8
+
+          pclk-sample: true
+          hsync-active: true
+          vsync-active: true
+
+        required:
+          - bus-width
+
+required:
+  - compatible
+  - reg
+  - interrupts
+  - clocks
+  - clock-names
+  - resets
+  - port
+
+additionalProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/clock/sun20i-d1-ccu.h>
+    #include <dt-bindings/interrupt-controller/irq.h>
+    #include <dt-bindings/reset/sun20i-d1-ccu.h>
+
+    csi@5800000 {
+        compatible = "allwinner,sun20i-d1-csi";
+        reg = <0x05800000 0x400000>;
+        interrupts = <111 IRQ_TYPE_LEVEL_HIGH>,
+                     <112 IRQ_TYPE_LEVEL_HIGH>,
+                     <116 IRQ_TYPE_LEVEL_HIGH>,
+                     <122 IRQ_TYPE_LEVEL_HIGH>;
+        clocks = <&ccu CLK_BUS_CSI>,
+                 <&ccu CLK_CSI_TOP>,
+                 <&ccu CLK_MBUS_CSI>;
+        clock-names = "bus", "mod", "ram";
+        resets = <&ccu RST_BUS_CSI>;
+
+        port {
+            endpoint {
+                remote-endpoint = <&ov5640_ep>;
+                bus-width = <8>;
+                hsync-active = <1>; /* Active high */
+                vsync-active = <0>; /* Active low */
+                pclk-sample = <1>;  /* Rising */
+            };
+        };
+    };
+
+...
diff --git a/MAINTAINERS b/MAINTAINERS
index f5eff489a8..f4c45c4678 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -905,6 +905,13 @@ L:	linux-crypto@vger.kernel.org
 S:	Maintained
 F:	drivers/crypto/allwinner/
 
+ALLWINNER D1 CSIC DRIVER
+M:	Nguyen Minh Tien <tien.nguyenminh@embeddedlinux.blog>
+L:	linux-media@vger.kernel.org
+S:	Maintained
+T:	git git://linuxtv.org/media.git
+F:	Documentation/devicetree/bindings/media/allwinner,sun20i-d1-csi.yaml
+
 ALLWINNER DMIC DRIVERS
 M:	Ban Tao <fengzheng923@gmail.com>
 L:	linux-sound@vger.kernel.org
-- 
2.34.1


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

* [PATCH 2/3] media: sunxi: Add support for the D1 CSIC
  2026-09-30 15:48 [PATCH 0/3] media: sunxi: Add the Allwinner D1/T113 camera interface Nguyen Minh Tien
  2026-09-30 15:48 ` [PATCH 1/3] dt-bindings: media: Add Allwinner D1 CSIC Nguyen Minh Tien
@ 2026-09-30 15:48 ` Nguyen Minh Tien
  2026-09-30 16:02   ` sashiko-bot
  2026-09-30 15:48 ` [PATCH 3/3] riscv: dts: allwinner: d1s-t113: Add the CSIC node Nguyen Minh Tien
  2 siblings, 1 reply; 7+ messages in thread
From: Nguyen Minh Tien @ 2026-09-30 15:48 UTC (permalink / raw)
  To: Mauro Carvalho Chehab, Hans Verkuil
  Cc: Nguyen Minh Tien, Sakari Ailus, Laurent Pinchart,
	Paul Kocialkowski, Chen-Yu Tsai, Jernej Skrabec, Samuel Holland,
	Rob Herring, Krzysztof Kozlowski, Conor Dooley, Paul Walmsley,
	Palmer Dabbelt, Albert Ou, Alexandre Ghiti, Philipp Zabel,
	linux-media, devicetree, linux-arm-kernel, linux-sunxi,
	linux-riscv, linux-kernel

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@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@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@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@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@embeddedlinux.blog>");
+MODULE_LICENSE("GPL");
-- 
2.34.1


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

* [PATCH 3/3] riscv: dts: allwinner: d1s-t113: Add the CSIC node
  2026-09-30 15:48 [PATCH 0/3] media: sunxi: Add the Allwinner D1/T113 camera interface Nguyen Minh Tien
  2026-09-30 15:48 ` [PATCH 1/3] dt-bindings: media: Add Allwinner D1 CSIC Nguyen Minh Tien
  2026-09-30 15:48 ` [PATCH 2/3] media: sunxi: Add support for the " Nguyen Minh Tien
@ 2026-09-30 15:48 ` Nguyen Minh Tien
  2026-09-30 15:57   ` sashiko-bot
  2 siblings, 1 reply; 7+ messages in thread
From: Nguyen Minh Tien @ 2026-09-30 15:48 UTC (permalink / raw)
  To: Mauro Carvalho Chehab, Hans Verkuil
  Cc: Nguyen Minh Tien, Sakari Ailus, Laurent Pinchart,
	Paul Kocialkowski, Chen-Yu Tsai, Jernej Skrabec, Samuel Holland,
	Rob Herring, Krzysztof Kozlowski, Conor Dooley, Paul Walmsley,
	Palmer Dabbelt, Albert Ou, Alexandre Ghiti, Philipp Zabel,
	linux-media, devicetree, linux-arm-kernel, linux-sunxi,
	linux-riscv, linux-kernel

Add the CSIC node to the dtsi shared by the D1, D1s and T113, with the
pin groups of its parallel input. MCLK has its own group, for the sensor
node.

Signed-off-by: Nguyen Minh Tien <tien.nguyenminh@embeddedlinux.blog>
---
 .../boot/dts/allwinner/sunxi-d1s-t113.dtsi    | 31 +++++++++++++++++++
 1 file changed, 31 insertions(+)

diff --git a/arch/riscv/boot/dts/allwinner/sunxi-d1s-t113.dtsi b/arch/riscv/boot/dts/allwinner/sunxi-d1s-t113.dtsi
index 13b1e57166..dd4c873457 100644
--- a/arch/riscv/boot/dts/allwinner/sunxi-d1s-t113.dtsi
+++ b/arch/riscv/boot/dts/allwinner/sunxi-d1s-t113.dtsi
@@ -70,6 +70,20 @@ clk_pg11_pin: clk-pg11-pin {
 				function = "clk";
 			};
 
+			/omit-if-no-ref/
+			csi0_8bit_pins: csi0-8bit-pins {
+				pins = "PE0", "PE1", "PE2", "PE4", "PE5",
+				       "PE6", "PE7", "PE8", "PE9", "PE10",
+				       "PE11";
+				function = "ncsi0";
+			};
+
+			/omit-if-no-ref/
+			csi0_mclk_pin: csi0-mclk-pin {
+				pins = "PE3";
+				function = "ncsi0";
+			};
+
 			/omit-if-no-ref/
 			dsi_4lane_pins: dsi-4lane-pins {
 				pins = "PD0", "PD1", "PD2", "PD3", "PD4", "PD5",
@@ -997,6 +1011,23 @@ tcon_tv0_out_tcon_top_hdmi: endpoint {
 			};
 		};
 
+		csi: csi@5800000 {
+			compatible = "allwinner,sun20i-d1-csi";
+			reg = <0x5800000 0x400000>;
+			interrupts = <SOC_PERIPHERAL_IRQ(95) IRQ_TYPE_LEVEL_HIGH>,
+				     <SOC_PERIPHERAL_IRQ(96) IRQ_TYPE_LEVEL_HIGH>,
+				     <SOC_PERIPHERAL_IRQ(100) IRQ_TYPE_LEVEL_HIGH>,
+				     <SOC_PERIPHERAL_IRQ(106) IRQ_TYPE_LEVEL_HIGH>;
+			clocks = <&ccu CLK_BUS_CSI>,
+				 <&ccu CLK_CSI_TOP>,
+				 <&ccu CLK_MBUS_CSI>;
+			clock-names = "bus", "mod", "ram";
+			resets = <&ccu RST_BUS_CSI>;
+			pinctrl-names = "default";
+			pinctrl-0 = <&csi0_8bit_pins>;
+			status = "disabled";
+		};
+
 		ppu: power-controller@7001000 {
 			compatible = "allwinner,sun20i-d1-ppu";
 			reg = <0x7001000 0x1000>;
-- 
2.34.1


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

* Re: [PATCH 3/3] riscv: dts: allwinner: d1s-t113: Add the CSIC node
  2026-09-30 15:48 ` [PATCH 3/3] riscv: dts: allwinner: d1s-t113: Add the CSIC node Nguyen Minh Tien
@ 2026-09-30 15:57   ` sashiko-bot
  0 siblings, 0 replies; 7+ messages in thread
From: sashiko-bot @ 2026-09-30 15:57 UTC (permalink / raw)
  To: Nguyen Minh Tien; +Cc: media-ci, devicetree, robh, linux-sunxi, conor+dt

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] dts: missing mandatory 'port' child node in csi@5800000
--

commit 748644695d3510624f461ae96dba4a0eda614827
Author: Nguyen Minh Tien <tien.nguyenminh@embeddedlinux.blog>

riscv: dts: allwinner: d1s-t113: Add the CSIC node

Add the CSIC node to the dtsi shared by the D1, D1s and T113, with the
pin groups of its parallel input. MCLK has its own group, for the sensor
node.

> diff --git a/arch/riscv/boot/dts/allwinner/sunxi-d1s-t113.dtsi b/arch/riscv/boot/dts/allwinner/sunxi-d1s-t113.dtsi
> index 13b1e57166040..dd4c873457e22 100644
> --- a/arch/riscv/boot/dts/allwinner/sunxi-d1s-t113.dtsi
> +++ b/arch/riscv/boot/dts/allwinner/sunxi-d1s-t113.dtsi

[ ... ]

> @@ -997,6 +1011,23 @@ tcon_tv0_out_tcon_top_hdmi: endpoint {
>  			};
>  		};
>  
> +		csi: csi@5800000 {
> +			compatible = "allwinner,sun20i-d1-csi";
> +			reg = <0x5800000 0x400000>;
> +			interrupts = <SOC_PERIPHERAL_IRQ(95) IRQ_TYPE_LEVEL_HIGH>,
> +				     <SOC_PERIPHERAL_IRQ(96) IRQ_TYPE_LEVEL_HIGH>,
> +				     <SOC_PERIPHERAL_IRQ(100) IRQ_TYPE_LEVEL_HIGH>,
> +				     <SOC_PERIPHERAL_IRQ(106) IRQ_TYPE_LEVEL_HIGH>;
> +			clocks = <&ccu CLK_BUS_CSI>,
> +				 <&ccu CLK_CSI_TOP>,
> +				 <&ccu CLK_MBUS_CSI>;
> +			clock-names = "bus", "mod", "ram";
> +			resets = <&ccu RST_BUS_CSI>;
> +			pinctrl-names = "default";
> +			pinctrl-0 = <&csi0_8bit_pins>;
> +			status = "disabled";
> +		};

[Severity: Low]
Will this missing child node cause make dtbs_check validation to fail?

The allwinner,sun20i-d1-csi device tree binding explicitly mandates the port
property, but the csi node is defined here without the mandatory port child
node.

> +
>  		ppu: power-controller@7001000 {

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260930154835.402684-1-tien.nguyenminh@embeddedlinux.blog?part=3

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

* Re: [PATCH 2/3] media: sunxi: Add support for the D1 CSIC
  2026-09-30 15:48 ` [PATCH 2/3] media: sunxi: Add support for the " Nguyen Minh Tien
@ 2026-09-30 16:02   ` sashiko-bot
  0 siblings, 0 replies; 7+ messages in thread
From: sashiko-bot @ 2026-09-30 16:02 UTC (permalink / raw)
  To: Nguyen Minh Tien; +Cc: devicetree, conor+dt, media-ci, linux-sunxi, robh

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [High] pm: crash in IRQ handler due to missing pm_runtime_get_if_active()
- [High] media: UAF of sun20i_csi structure on device unbind
- [High] media: UAF of csi->source when sensor is unbound while streaming
- [Medium] pm: hardware left powered on after driver removal due to async pm_runtime_put
- [Medium] media: potential NULL dereference in sun20i_csi_bridge_enum_mbus_code()
--

commit 48923ca6acd2a7bc065ae49a8c19a8a6d69a0531
Author: Nguyen Minh Tien <tien.nguyenminh@embeddedlinux.blog>

    media: sunxi: Add support for the D1 CSIC
    
    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.

> 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 0000000000000..eed29bc61348b
> --- /dev/null
> +++ b/drivers/media/platform/sunxi/sun20i-csi/sun20i_csi.c
> @@ -0,0 +1,1357 @@

[ ... ]

> +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);
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[Severity: High]
Does this code crash the system if an interrupt arrives while the device is
suspended?

The IRQ handler accesses hardware registers unconditionally without verifying
if the device is active. devm_request_irq() is called before
pm_runtime_enable() in sun20i_csi_probe(). If the bootloader left the
interrupt pending, the IRQ fires immediately before the device is powered on.
Additionally, since the IRQ is not disabled during runtime suspend, any
spurious interrupt while suspended triggers a synchronous external abort when
reading hardware registers while clocks are disabled.

Should this use pm_runtime_get_if_active() to verify the device is active
before accessing registers?

[ ... ]

> +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;
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[Severity: Medium]
Can this cause a NULL pointer dereference?

The format pointer for the unvalidated sink pad is dereferenced directly in
an initialization callback without framework validation guarantees.
v4l2_subdev_state_get_format() can return NULL.

> +	*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)
> +{

[ ... ]

> +	/* 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;
                ^^^^^^^^^^^^^^^^^^^^^^^
[Severity: Medium]
Is it safe to assume fmt is non-NULL here?

In enum_mbus_code(), the core wrapper validates the state for the SOURCE pad.
The driver then unconditionally dereferences the format returned for the
opposite SINK pad. A check for one pad does not validate another.

[ ... ]

> +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));
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[Severity: High]
Does this code dereference a freed pointer if the sensor is unbound while
streaming?

If the sensor subdevice is unbound (e.g., via sysfs) while the CSIC is
streaming, the async notifier core unbinds the sensor, but the CSIC driver
lacks an .unbind callback to clear the csi->source pointer. When streaming is
subsequently stopped by userspace, sun20i_csi_bridge_disable_streams()
dereferences the freed csi->source pointer.

[ ... ]

> +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);
        ^^^^^^^^^^^^^^^^^^^^^^^^^
[Severity: Medium]
Will this leave hardware powered on if the driver is unbound?

This queues an asynchronous suspend which is vulnerable to being cancelled by
driver removal. If the driver is unbound while streaming,
vb2_video_unregister_device() stops streaming synchronously, queuing an async
suspend via pm_runtime_put(). The remove function then finishes and devres
calls pm_runtime_disable(), which cancels the pending async suspend.

Should this use pm_runtime_put_sync() instead?

[ ... ]

> +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);
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[Severity: High]
Does this allocation tie the video device lifetime to the driver binding
rather than the V4L2 file descriptors?

The structure embedding the video device is managed by devres. If userspace
holds /dev/videoX open when the driver unbinds, the remove function
completes and devres frees the structure. When userspace later closes the
file descriptor, v4l2_release() accesses the video_device embedded in the
freed structure, causing a Use-After-Free.

> +	if (!csi)
> +		return -ENOMEM;

[ ... ]

> +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);
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[Severity: High]
Does this code unregister the device without waiting for file descriptors to
close?

This synchronously stops streaming during driver unbind and unregisters the
device but does not wait for file descriptors to close. This allows the
devres cleanup to race with userspace closing the file, leading to the Use-
After-Free issue mentioned in sun20i_csi_probe().

Additionally, it immediately precedes the devres cleanup that cancels the
async put queued in sun20i_csi_stop_streaming().

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260930154835.402684-1-tien.nguyenminh@embeddedlinux.blog?part=2

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

* Re: [PATCH 1/3] dt-bindings: media: Add Allwinner D1 CSIC
  2026-09-30 15:48 ` [PATCH 1/3] dt-bindings: media: Add Allwinner D1 CSIC Nguyen Minh Tien
@ 2026-10-02  6:14   ` Krzysztof Kozlowski
  0 siblings, 0 replies; 7+ messages in thread
From: Krzysztof Kozlowski @ 2026-10-02  6:14 UTC (permalink / raw)
  To: Nguyen Minh Tien
  Cc: Mauro Carvalho Chehab, Hans Verkuil, Sakari Ailus,
	Laurent Pinchart, Paul Kocialkowski, Chen-Yu Tsai, Jernej Skrabec,
	Samuel Holland, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti,
	Philipp Zabel, linux-media, devicetree, linux-arm-kernel,
	linux-sunxi, linux-riscv, linux-kernel

On Wed, Sep 30, 2026 at 10:48:33PM +0700, Nguyen Minh Tien wrote:
> +maintainers:
> +  - Nguyen Minh Tien <tien.nguyenminh@embeddedlinux.blog>
> +
> +description:
> +  The CSIC found in the Allwinner D1, D1s and T113 receives video from a
> +  parallel camera interface and writes it to memory. It contains a parser for
> +  the camera bus and two DMA engines.
> +
> +properties:
> +  compatible:
> +    const: allwinner,sun20i-d1-csi
> +
> +  reg:
> +    maxItems: 1
> +
> +  interrupts:
> +    items:
> +      - description: DMA engine 0
> +      - description: DMA engine 1
> +      - description: Parser 0
> +      - description: Top-level block (CSI_TOP_PKT)
> +
> +  clocks:
> +    items:
> +      - description: Bus clock
> +      - description: Module clock
> +      - description: DRAM clock
> +
> +  clock-names:
> +    items:
> +      - const: bus
> +      - const: mod
> +      - const: ram
> +
> +  resets:
> +    maxItems: 1
> +
> +  iommus:
> +    maxItems: 1
> +
> +  port:
> +    $ref: /schemas/graph.yaml#/$defs/port-base
> +    description: Parallel input port, connect to a parallel sensor
> +    unevaluatedProperties: false
> +
> +    properties:
> +      endpoint:
> +        $ref: video-interfaces.yaml#
> +        unevaluatedProperties: false
> +
> +        properties:
> +          bus-width:
> +            const: 8

If it is const, why do you exactly need it in the DT?

> +
> +          pclk-sample: true
> +          hsync-active: true
> +          vsync-active: true

Drop these three.

> +
> +        required:
> +          - bus-width

No second port to the ISP?

> +
> +required:
> +  - compatible
> +  - reg
> +  - interrupts
> +  - clocks
> +  - clock-names
> +  - resets
> +  - port
> +
> +additionalProperties: false
> +
> +examples:
> +  - |
> +    #include <dt-bindings/clock/sun20i-d1-ccu.h>
> +    #include <dt-bindings/interrupt-controller/irq.h>
> +    #include <dt-bindings/reset/sun20i-d1-ccu.h>
> +
> +    csi@5800000 {
> +        compatible = "allwinner,sun20i-d1-csi";
> +        reg = <0x05800000 0x400000>;
> +        interrupts = <111 IRQ_TYPE_LEVEL_HIGH>,
> +                     <112 IRQ_TYPE_LEVEL_HIGH>,
> +                     <116 IRQ_TYPE_LEVEL_HIGH>,
> +                     <122 IRQ_TYPE_LEVEL_HIGH>;
> +        clocks = <&ccu CLK_BUS_CSI>,
> +                 <&ccu CLK_CSI_TOP>,
> +                 <&ccu CLK_MBUS_CSI>;
> +        clock-names = "bus", "mod", "ram";
> +        resets = <&ccu RST_BUS_CSI>;
> +
> +        port {
> +            endpoint {
> +                remote-endpoint = <&ov5640_ep>;
> +                bus-width = <8>;
> +                hsync-active = <1>; /* Active high */
> +                vsync-active = <0>; /* Active low */
> +                pclk-sample = <1>;  /* Rising */
> +            };
> +        };
> +    };
> +
> +...
> diff --git a/MAINTAINERS b/MAINTAINERS
> index f5eff489a8..f4c45c4678 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -905,6 +905,13 @@ L:	linux-crypto@vger.kernel.org
>  S:	Maintained
>  F:	drivers/crypto/allwinner/
>  
> +ALLWINNER D1 CSIC DRIVER
> +M:	Nguyen Minh Tien <tien.nguyenminh@embeddedlinux.blog>
> +L:	linux-media@vger.kernel.org
> +S:	Maintained
> +T:	git git://linuxtv.org/media.git

Drop, unless you actually have their commit rights and manage patches. I
would say mailing list is also here redundant (duplicating), but I
stopped pointing to it.

Best regards,
Krzysztof


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

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

Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-30 15:48 [PATCH 0/3] media: sunxi: Add the Allwinner D1/T113 camera interface Nguyen Minh Tien
2026-09-30 15:48 ` [PATCH 1/3] dt-bindings: media: Add Allwinner D1 CSIC Nguyen Minh Tien
2026-10-02  6:14   ` Krzysztof Kozlowski
2026-09-30 15:48 ` [PATCH 2/3] media: sunxi: Add support for the " Nguyen Minh Tien
2026-09-30 16:02   ` sashiko-bot
2026-09-30 15:48 ` [PATCH 3/3] riscv: dts: allwinner: d1s-t113: Add the CSIC node Nguyen Minh Tien
2026-09-30 15:57   ` sashiko-bot

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