[PATCH 2/3] media: sunxi: Add support for the D1 CSIC
flat view
COOLING8d
From: Nguyen Minh Tien <hidden>
Date: 2026-09-30 15:49:16
Also in:
linux-devicetree, linux-media, linux-riscv, linux-sunxi, lkml
Subsystem:
media input infrastructure (v4l/dvb), the rest · Maintainers:
Mauro Carvalho Chehab, Linus Torvalds
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 <redacted> --- 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