From 7735dd0736322cff23aff95490bae1d69937a9bf Mon Sep 17 00:00:00 2001
From: Nick Hollinghurst <nick.hollinghurst@raspberrypi.com>
Date: Tue, 10 Dec 2024 13:23:09 +0000
Subject: [PATCH] drm: rp1: rp1-dpi: Add interlaced modes and PIO program to
 fix VSYNC

Implement interlaced modes by wobbling the base pointer and VFP width
for every field. This results in correct pixels but incorrect VSYNC.

Now use PIO to generate a fixed-up VSYNC by sampling DE and HSYNC.
This requires DPI's DE output to be mapped to GPIO1, which we check.

When DE is not exposed, the internal fixup is disabled. VSYNC/GPIO2
becomes a modified signal, designed to help an external device or
PIO program synthesize CSYNC or VSYNC.

Signed-off-by: Nick Hollinghurst <nick.hollinghurst@raspberrypi.com>
---
 drivers/gpu/drm/rp1/rp1-dpi/Makefile      |   2 +-
 drivers/gpu/drm/rp1/rp1-dpi/rp1_dpi.c     |  34 ++-
 drivers/gpu/drm/rp1/rp1-dpi/rp1_dpi.h     |  18 ++
 drivers/gpu/drm/rp1/rp1-dpi/rp1_dpi_hw.c  | 253 ++++++++++++++++------
 drivers/gpu/drm/rp1/rp1-dpi/rp1_dpi_pio.c | 225 +++++++++++++++++++
 5 files changed, 461 insertions(+), 71 deletions(-)
 create mode 100644 drivers/gpu/drm/rp1/rp1-dpi/rp1_dpi_pio.c

--- a/drivers/gpu/drm/rp1/rp1-dpi/Makefile
+++ b/drivers/gpu/drm/rp1/rp1-dpi/Makefile
@@ -1,5 +1,5 @@
 # SPDX-License-Identifier: GPL-2.0-only
 
-drm-rp1-dpi-y := rp1_dpi.o rp1_dpi_hw.o rp1_dpi_cfg.o
+drm-rp1-dpi-y := rp1_dpi.o rp1_dpi_hw.o rp1_dpi_cfg.o rp1_dpi_pio.o
 
 obj-$(CONFIG_DRM_RP1_DPI) += drm-rp1-dpi.o
--- a/drivers/gpu/drm/rp1/rp1-dpi/rp1_dpi.c
+++ b/drivers/gpu/drm/rp1/rp1-dpi/rp1_dpi.c
@@ -80,6 +80,7 @@ static void rp1dpi_pipe_update(struct dr
 			if (dpi->dpi_running &&
 			    fb->format->format != dpi->cur_fmt) {
 				rp1dpi_hw_stop(dpi);
+				rp1dpi_pio_stop(dpi);
 				dpi->dpi_running = false;
 			}
 			if (!dpi->dpi_running) {
@@ -88,6 +89,7 @@ static void rp1dpi_pipe_update(struct dr
 						dpi->bus_fmt,
 						dpi->de_inv,
 						&pipe->crtc.state->mode);
+				rp1dpi_pio_start(dpi, &pipe->crtc.state->mode);
 				dpi->dpi_running = true;
 			}
 			dpi->cur_fmt = fb->format->format;
@@ -187,6 +189,7 @@ static void rp1dpi_pipe_disable(struct d
 	drm_crtc_vblank_off(&pipe->crtc);
 	if (dpi->dpi_running) {
 		rp1dpi_hw_stop(dpi);
+		rp1dpi_pio_stop(dpi);
 		dpi->dpi_running = false;
 	}
 	clk_disable_unprepare(dpi->clocks[RP1DPI_CLK_DPI]);
@@ -236,6 +239,7 @@ static void rp1dpi_stopall(struct drm_de
 		if (dpi->dpi_running || rp1dpi_hw_busy(dpi)) {
 			rp1dpi_hw_stop(dpi);
 			clk_disable_unprepare(dpi->clocks[RP1DPI_CLK_DPI]);
+			rp1dpi_pio_stop(dpi);
 			dpi->dpi_running = false;
 		}
 		rp1dpi_vidout_poweroff(dpi);
@@ -273,7 +277,7 @@ static int rp1dpi_platform_probe(struct
 	struct rp1_dpi *dpi;
 	struct drm_bridge *bridge = NULL;
 	struct drm_panel *panel;
-	int i, ret;
+	int i, j, ret;
 
 	dev_info(dev, __func__);
 	ret = drm_of_find_panel_or_bridge(pdev->dev.of_node, 0, 0,
@@ -295,6 +299,7 @@ static int rp1dpi_platform_probe(struct
 		return ret;
 	}
 	dpi->pdev = pdev;
+	spin_lock_init(&dpi->hw_lock);
 
 	dpi->bus_fmt = default_bus_fmt;
 	ret = of_property_read_u32(dev->of_node, "default_bus_fmt", &dpi->bus_fmt);
@@ -332,6 +337,33 @@ static int rp1dpi_platform_probe(struct
 	if (ret)
 		goto done_err;
 
+	/* Check if PIO can snoop on or override DPI's GPIO1 */
+	dpi->gpio1_used = false;
+	for (i = 0; !dpi->gpio1_used; i++) {
+		u32 p = 0;
+		const char *str = NULL;
+		struct device_node *np1 = of_parse_phandle(dev->of_node, "pinctrl-0", i);
+
+		if (!np1)
+			break;
+
+		if (!of_property_read_string(np1, "function", &str) && !strcmp(str, "dpi")) {
+			for (j = 0; !dpi->gpio1_used; j++) {
+				if (of_property_read_string_index(np1, "pins", j, &str))
+					break;
+				if (!strcmp(str, "gpio1"))
+					dpi->gpio1_used = true;
+			}
+			for (j = 0; !dpi->gpio1_used; j++) {
+				if (of_property_read_u32_index(np1, "brcm,pins", j, &p))
+					break;
+				if (p == 1)
+					dpi->gpio1_used = true;
+			}
+		}
+		of_node_put(np1);
+	}
+
 	/* Now we have all our resources, finish driver initialization */
 	dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64));
 	init_completion(&dpi->finished);
--- a/drivers/gpu/drm/rp1/rp1-dpi/rp1_dpi.h
+++ b/drivers/gpu/drm/rp1/rp1-dpi/rp1_dpi.h
@@ -46,6 +46,18 @@ struct rp1_dpi {
 	bool de_inv, clk_inv;
 	bool dpi_running, pipe_enabled;
 	struct completion finished;
+
+	/* Experimental stuff for interlace follows */
+	struct rp1_pio_client *pio;
+	bool gpio1_used;
+	bool pio_stole_gpio2;
+
+	spinlock_t hw_lock; /* the following are used in line-match ISR */
+	dma_addr_t last_dma_addr;
+	u32 last_stride;
+	u32 shorter_front_porch;
+	bool interlaced;
+	bool lower_field_flag;
 };
 
 /* ---------------------------------------------------------------------- */
@@ -67,3 +79,9 @@ void rp1dpi_hw_vblank_ctrl(struct rp1_dp
 
 void rp1dpi_vidout_setup(struct rp1_dpi *dpi, bool drive_negedge);
 void rp1dpi_vidout_poweroff(struct rp1_dpi *dpi);
+
+/* ---------------------------------------------------------------------- */
+/* PIO control -- we need PIO to generate VSync (from DE) when interlaced */
+
+int rp1dpi_pio_start(struct rp1_dpi *dpi, const struct drm_display_mode *mode);
+void rp1dpi_pio_stop(struct rp1_dpi *dpi);
--- a/drivers/gpu/drm/rp1/rp1-dpi/rp1_dpi_hw.c
+++ b/drivers/gpu/drm/rp1/rp1-dpi/rp1_dpi_hw.c
@@ -202,7 +202,7 @@
 // Status
 #define DPI_DMA_STATUS  0x3c
 
-#define BITS(field, val) (((val) << (field ## _SHIFT)) & (field ## _MASK))
+#define BITS(field, val) FIELD_PREP((field ## _MASK), val)
 
 static unsigned int rp1dpi_hw_read(struct rp1_dpi *dpi, unsigned int reg)
 {
@@ -231,69 +231,73 @@ struct rp1dpi_ipixfmt {
 	u32 rgbsz;  /* Shifts used for scaling; also (BPP/8-1)   */
 };
 
-#define IMASK_RGB(r, g, b)	(BITS(DPI_DMA_IMASK_R, r)  | \
-				 BITS(DPI_DMA_IMASK_G, g)  | \
-				 BITS(DPI_DMA_IMASK_B, b))
-#define OMASK_RGB(r, g, b)	(BITS(DPI_DMA_OMASK_R, r)  | \
-				 BITS(DPI_DMA_OMASK_G, g)  | \
-				 BITS(DPI_DMA_OMASK_B, b))
-#define ISHIFT_RGB(r, g, b)	(BITS(DPI_DMA_SHIFT_IR, r) | \
-				 BITS(DPI_DMA_SHIFT_IG, g) | \
-				 BITS(DPI_DMA_SHIFT_IB, b))
-#define OSHIFT_RGB(r, g, b)	(BITS(DPI_DMA_SHIFT_OR, r) | \
-				 BITS(DPI_DMA_SHIFT_OG, g) | \
-				 BITS(DPI_DMA_SHIFT_OB, b))
+#define IMASK_RGB(r, g, b)  (FIELD_PREP_CONST(DPI_DMA_IMASK_R_MASK, r)  | \
+			     FIELD_PREP_CONST(DPI_DMA_IMASK_G_MASK, g)  | \
+			     FIELD_PREP_CONST(DPI_DMA_IMASK_B_MASK, b))
+#define OMASK_RGB(r, g, b)  (FIELD_PREP_CONST(DPI_DMA_OMASK_R_MASK, r)  | \
+			     FIELD_PREP_CONST(DPI_DMA_OMASK_G_MASK, g)  | \
+			     FIELD_PREP_CONST(DPI_DMA_OMASK_B_MASK, b))
+#define ISHIFT_RGB(r, g, b) (FIELD_PREP_CONST(DPI_DMA_SHIFT_IR_MASK, r) | \
+			     FIELD_PREP_CONST(DPI_DMA_SHIFT_IG_MASK, g) | \
+			     FIELD_PREP_CONST(DPI_DMA_SHIFT_IB_MASK, b))
+#define OSHIFT_RGB(r, g, b) (FIELD_PREP_CONST(DPI_DMA_SHIFT_OR_MASK, r) | \
+			     FIELD_PREP_CONST(DPI_DMA_SHIFT_OG_MASK, g) | \
+			     FIELD_PREP_CONST(DPI_DMA_SHIFT_OB_MASK, b))
 
 static const struct rp1dpi_ipixfmt my_formats[] = {
 	{
 	  .format = DRM_FORMAT_XRGB8888,
 	  .mask	  = IMASK_RGB(0x3fc, 0x3fc, 0x3fc),
 	  .shift  = ISHIFT_RGB(23, 15, 7),
-	  .rgbsz  = BITS(DPI_DMA_RGBSZ_BPP, 3),
+	  .rgbsz  = FIELD_PREP_CONST(DPI_DMA_RGBSZ_BPP_MASK, 3),
 	},
 	{
 	  .format = DRM_FORMAT_XBGR8888,
 	  .mask	  = IMASK_RGB(0x3fc, 0x3fc, 0x3fc),
 	  .shift  = ISHIFT_RGB(7, 15, 23),
-	  .rgbsz  = BITS(DPI_DMA_RGBSZ_BPP, 3),
+	  .rgbsz  = FIELD_PREP_CONST(DPI_DMA_RGBSZ_BPP_MASK, 3),
 	},
 	{
 	  .format = DRM_FORMAT_ARGB8888,
 	  .mask	  = IMASK_RGB(0x3fc, 0x3fc, 0x3fc),
 	  .shift  = ISHIFT_RGB(23, 15, 7),
-	  .rgbsz  = BITS(DPI_DMA_RGBSZ_BPP, 3),
+	  .rgbsz  = FIELD_PREP_CONST(DPI_DMA_RGBSZ_BPP_MASK, 3),
 	},
 	{
 	  .format = DRM_FORMAT_ABGR8888,
 	  .mask	  = IMASK_RGB(0x3fc, 0x3fc, 0x3fc),
 	  .shift  = ISHIFT_RGB(7, 15, 23),
-	  .rgbsz  = BITS(DPI_DMA_RGBSZ_BPP, 3),
+	  .rgbsz  = FIELD_PREP_CONST(DPI_DMA_RGBSZ_BPP_MASK, 3),
 	},
 	{
 	  .format = DRM_FORMAT_RGB888,
 	  .mask	  = IMASK_RGB(0x3fc, 0x3fc, 0x3fc),
 	  .shift  = ISHIFT_RGB(23, 15, 7),
-	  .rgbsz  = BITS(DPI_DMA_RGBSZ_BPP, 2),
+	  .rgbsz  = FIELD_PREP_CONST(DPI_DMA_RGBSZ_BPP_MASK, 2),
 	},
 	{
 	  .format = DRM_FORMAT_BGR888,
 	  .mask	  = IMASK_RGB(0x3fc, 0x3fc, 0x3fc),
 	  .shift  = ISHIFT_RGB(7, 15, 23),
-	  .rgbsz  = BITS(DPI_DMA_RGBSZ_BPP, 2),
+	  .rgbsz  = FIELD_PREP_CONST(DPI_DMA_RGBSZ_BPP_MASK, 2),
 	},
 	{
 	  .format = DRM_FORMAT_RGB565,
 	  .mask	  = IMASK_RGB(0x3e0, 0x3f0, 0x3e0),
 	  .shift  = ISHIFT_RGB(15, 10, 4),
-	  .rgbsz  = BITS(DPI_DMA_RGBSZ_R, 5) | BITS(DPI_DMA_RGBSZ_G, 6) |
-		    BITS(DPI_DMA_RGBSZ_B, 5) | BITS(DPI_DMA_RGBSZ_BPP, 1),
+	  .rgbsz  = (FIELD_PREP_CONST(DPI_DMA_RGBSZ_R_MASK, 5) |
+		     FIELD_PREP_CONST(DPI_DMA_RGBSZ_G_MASK, 6) |
+		     FIELD_PREP_CONST(DPI_DMA_RGBSZ_B_MASK, 5) |
+		     FIELD_PREP_CONST(DPI_DMA_RGBSZ_BPP_MASK, 1)),
 	},
 	{
 	  .format = DRM_FORMAT_BGR565,
 	  .mask	  = IMASK_RGB(0x3e0, 0x3f0, 0x3e0),
 	  .shift  = ISHIFT_RGB(4, 10, 15),
-	  .rgbsz  = BITS(DPI_DMA_RGBSZ_R, 5) | BITS(DPI_DMA_RGBSZ_G, 6) |
-		    BITS(DPI_DMA_RGBSZ_B, 5) | BITS(DPI_DMA_RGBSZ_BPP, 1),
+	  .rgbsz  = (FIELD_PREP_CONST(DPI_DMA_RGBSZ_R_MASK, 5) |
+		     FIELD_PREP_CONST(DPI_DMA_RGBSZ_G_MASK, 6) |
+		     FIELD_PREP_CONST(DPI_DMA_RGBSZ_B_MASK, 5) |
+		     FIELD_PREP_CONST(DPI_DMA_RGBSZ_BPP_MASK, 1)),
 	}
 };
 
@@ -354,42 +358,26 @@ void rp1dpi_hw_setup(struct rp1_dpi *dpi
 		     u32 in_format, u32 bus_format, bool de_inv,
 		    struct drm_display_mode const *mode)
 {
-	u32 shift, imask, omask, rgbsz;
+	u32 shift, imask, omask, rgbsz, vctrl;
 	int i;
 
-	pr_info("%s: in_fmt=\'%c%c%c%c\' bus_fmt=0x%x mode=%dx%d total=%dx%d %dkHz %cH%cV%cD%cC",
-		__func__, in_format, in_format >> 8, in_format >> 16, in_format >> 24, bus_format,
-		mode->hdisplay, mode->vdisplay,
-		mode->htotal, mode->vtotal,
-		mode->clock,
-		(mode->flags & DRM_MODE_FLAG_NHSYNC) ? '-' : '+',
-		(mode->flags & DRM_MODE_FLAG_NVSYNC) ? '-' : '+',
-		de_inv ? '-' : '+',
-		dpi->clk_inv ? '-' : '+');
+	drm_info(&dpi->drm,
+		 "in_fmt=\'%c%c%c%c\' bus_fmt=0x%x mode=%dx%d total=%dx%d%s %dkHz %cH%cV%cD%cC",
+		 in_format, in_format >> 8, in_format >> 16, in_format >> 24, bus_format,
+		 mode->hdisplay, mode->vdisplay,
+		 mode->htotal, mode->vtotal,
+		 (mode->flags & DRM_MODE_FLAG_INTERLACE) ? "i" : "",
+		 mode->clock,
+		 (mode->flags & DRM_MODE_FLAG_NHSYNC) ? '-' : '+',
+		 (mode->flags & DRM_MODE_FLAG_NVSYNC) ? '-' : '+',
+		 de_inv ? '-' : '+',
+		 dpi->clk_inv ? '-' : '+');
 
 	/*
 	 * Configure all DPI/DMA block registers, except base address.
 	 * DMA will not actually start until a FB base address is specified
 	 * using rp1dpi_hw_update().
 	 */
-	rp1dpi_hw_write(dpi, DPI_DMA_VISIBLE_AREA,
-			BITS(DPI_DMA_VISIBLE_AREA_ROWSM1, mode->vdisplay - 1) |
-			BITS(DPI_DMA_VISIBLE_AREA_COLSM1, mode->hdisplay - 1));
-
-	rp1dpi_hw_write(dpi, DPI_DMA_SYNC_WIDTH,
-			BITS(DPI_DMA_SYNC_WIDTH_ROWSM1, mode->vsync_end - mode->vsync_start - 1) |
-			BITS(DPI_DMA_SYNC_WIDTH_COLSM1, mode->hsync_end - mode->hsync_start - 1));
-
-	/* In these registers, "back porch" time includes sync width */
-	rp1dpi_hw_write(dpi, DPI_DMA_BACK_PORCH,
-			BITS(DPI_DMA_BACK_PORCH_ROWSM1, mode->vtotal - mode->vsync_start - 1) |
-			BITS(DPI_DMA_BACK_PORCH_COLSM1, mode->htotal - mode->hsync_start - 1));
-
-	rp1dpi_hw_write(dpi, DPI_DMA_FRONT_PORCH,
-			BITS(DPI_DMA_FRONT_PORCH_ROWSM1, mode->vsync_start - mode->vdisplay - 1) |
-			BITS(DPI_DMA_FRONT_PORCH_COLSM1, mode->hsync_start - mode->hdisplay - 1));
-
-	/* Input to output pixel format conversion */
 	for (i = 0; i < ARRAY_SIZE(my_formats); ++i) {
 		if (my_formats[i].format == in_format)
 			break;
@@ -417,6 +405,89 @@ void rp1dpi_hw_setup(struct rp1_dpi *dpi
 			BITS(DPI_DMA_QOS_LLEV, 0x8) |
 			BITS(DPI_DMA_QOS_LQOS, 0x7));
 
+	if (!(mode->flags & DRM_MODE_FLAG_INTERLACE)) {
+		rp1dpi_hw_write(dpi, DPI_DMA_VISIBLE_AREA,
+				BITS(DPI_DMA_VISIBLE_AREA_ROWSM1, mode->vdisplay - 1) |
+				BITS(DPI_DMA_VISIBLE_AREA_COLSM1, mode->hdisplay - 1));
+
+		rp1dpi_hw_write(dpi, DPI_DMA_SYNC_WIDTH,
+				BITS(DPI_DMA_SYNC_WIDTH_ROWSM1,
+				     mode->vsync_end - mode->vsync_start - 1) |
+				BITS(DPI_DMA_SYNC_WIDTH_COLSM1,
+				     mode->hsync_end - mode->hsync_start - 1));
+
+		/* In these registers, "back porch" time includes sync width */
+		rp1dpi_hw_write(dpi, DPI_DMA_BACK_PORCH,
+				BITS(DPI_DMA_BACK_PORCH_ROWSM1,
+				     mode->vtotal - mode->vsync_start - 1) |
+				BITS(DPI_DMA_BACK_PORCH_COLSM1,
+				     mode->htotal - mode->hsync_start - 1));
+
+		rp1dpi_hw_write(dpi, DPI_DMA_FRONT_PORCH,
+				BITS(DPI_DMA_FRONT_PORCH_ROWSM1,
+				     mode->vsync_start - mode->vdisplay - 1) |
+				BITS(DPI_DMA_FRONT_PORCH_COLSM1,
+				     mode->hsync_start - mode->hdisplay - 1));
+
+		vctrl = BITS(DPI_DMA_CONTROL_VSYNC_POL, !!(mode->flags & DRM_MODE_FLAG_NVSYNC)) |
+			BITS(DPI_DMA_CONTROL_VBP_EN, (mode->vtotal != mode->vsync_start))       |
+			BITS(DPI_DMA_CONTROL_VFP_EN, (mode->vsync_start != mode->vdisplay))     |
+			BITS(DPI_DMA_CONTROL_VSYNC_EN, (mode->vsync_end != mode->vsync_start));
+
+		dpi->interlaced = false;
+	} else {
+		/*
+		 * Experimental interlace support
+		 *
+		 * RP1 DPI hardware wasn't designed to support interlace, but lets us change
+		 * both the VFP line count and the next DMA address while running. That allows
+		 * pixel data to be correctly timed for interlace, but VSYNC remains wrong.
+		 *
+		 * It is necessary to use external hardware (such as PIO) to regenerate VSYNC
+		 * based on HSYNC, DE (which *must* both be mapped to GPIOs 1, 3 respectively).
+		 * This driver includes a PIO program to do that, when DE is enabled.
+		 *
+		 * An alternative fixup is to synthesize CSYNC from HSYNC and modified-VSYNC.
+		 * We don't implement that here, but to facilitate it, DPI's VSYNC is replaced
+		 * by a "helper signal" that pulses low for 1 or 2 scan-lines, starting 2.0 or
+		 * 2.5 scan-lines respectively before nominal VSYNC start.
+		 */
+		int vact  = mode->vdisplay >> 1; /* visible lines per field. Can't do half-lines */
+		int vtot0 = mode->vtotal >> 1;   /* vtotal should always be odd when interlaced. */
+		int vfp0  = (mode->vsync_start >= mode->vdisplay + 4) ?
+			((mode->vsync_start - mode->vdisplay - 2) >> 1) : 1;
+		int vbp   = max(0, vtot0 - vact - vfp0);
+
+		rp1dpi_hw_write(dpi, DPI_DMA_VISIBLE_AREA,
+				BITS(DPI_DMA_VISIBLE_AREA_ROWSM1, vact - 1) |
+				BITS(DPI_DMA_VISIBLE_AREA_COLSM1, mode->hdisplay - 1));
+
+		rp1dpi_hw_write(dpi, DPI_DMA_SYNC_WIDTH,
+				BITS(DPI_DMA_SYNC_WIDTH_ROWSM1, vtot0 - 2) |
+				BITS(DPI_DMA_SYNC_WIDTH_COLSM1,
+				     mode->hsync_end - mode->hsync_start - 1));
+
+		rp1dpi_hw_write(dpi, DPI_DMA_BACK_PORCH,
+				BITS(DPI_DMA_BACK_PORCH_ROWSM1, vbp - 1) |
+				BITS(DPI_DMA_BACK_PORCH_COLSM1,
+				     mode->htotal - mode->hsync_start - 1));
+
+		dpi->shorter_front_porch =
+			BITS(DPI_DMA_FRONT_PORCH_ROWSM1, vfp0 - 1) |
+			BITS(DPI_DMA_FRONT_PORCH_COLSM1,
+			     mode->hsync_start - mode->hdisplay - 1);
+		rp1dpi_hw_write(dpi, DPI_DMA_FRONT_PORCH, dpi->shorter_front_porch);
+
+		vctrl = BITS(DPI_DMA_CONTROL_VSYNC_POL, 0)      |
+			BITS(DPI_DMA_CONTROL_VBP_EN, (vbp > 0)) |
+			BITS(DPI_DMA_CONTROL_VFP_EN, 1)         |
+			BITS(DPI_DMA_CONTROL_VSYNC_EN, 1);
+
+		dpi->interlaced = true;
+	}
+	dpi->lower_field_flag = false;
+	dpi->last_dma_addr = 0;
+
 	rp1dpi_hw_write(dpi, DPI_DMA_IRQ_FLAGS, -1);
 	rp1dpi_hw_vblank_ctrl(dpi, 1);
 
@@ -425,49 +496,64 @@ void rp1dpi_hw_setup(struct rp1_dpi *dpi
 		pr_warn("%s: Unexpectedly busy at start!", __func__);
 
 	rp1dpi_hw_write(dpi, DPI_DMA_CONTROL,
+			vctrl                                  |
 			BITS(DPI_DMA_CONTROL_ARM,          !i) |
 			BITS(DPI_DMA_CONTROL_AUTO_REPEAT,   1) |
 			BITS(DPI_DMA_CONTROL_HIGH_WATER,  448) |
 			BITS(DPI_DMA_CONTROL_DEN_POL,  de_inv) |
 			BITS(DPI_DMA_CONTROL_HSYNC_POL, !!(mode->flags & DRM_MODE_FLAG_NHSYNC)) |
-			BITS(DPI_DMA_CONTROL_VSYNC_POL, !!(mode->flags & DRM_MODE_FLAG_NVSYNC)) |
-			BITS(DPI_DMA_CONTROL_COLORM,	   0) |
-			BITS(DPI_DMA_CONTROL_SHUTDN,	   0) |
 			BITS(DPI_DMA_CONTROL_HBP_EN,    (mode->htotal != mode->hsync_end))      |
 			BITS(DPI_DMA_CONTROL_HFP_EN,    (mode->hsync_start != mode->hdisplay))  |
-			BITS(DPI_DMA_CONTROL_VBP_EN,    (mode->vtotal != mode->vsync_end))      |
-			BITS(DPI_DMA_CONTROL_VFP_EN,    (mode->vsync_start != mode->vdisplay))  |
-			BITS(DPI_DMA_CONTROL_HSYNC_EN,  (mode->hsync_end != mode->hsync_start)) |
-			BITS(DPI_DMA_CONTROL_VSYNC_EN,  (mode->vsync_end != mode->vsync_start)));
+			BITS(DPI_DMA_CONTROL_HSYNC_EN,  (mode->hsync_end != mode->hsync_start)));
 }
 
 void rp1dpi_hw_update(struct rp1_dpi *dpi, dma_addr_t addr, u32 offset, u32 stride)
 {
-	u64 a = addr + offset;
+	unsigned long flags;
+
+	spin_lock_irqsave(&dpi->hw_lock, flags);
 
 	/*
 	 * Update STRIDE, DMAH and DMAL only. When called after rp1dpi_hw_setup(),
 	 * DMA starts immediately; if already running, the buffer will flip at
-	 * the next vertical sync event.
+	 * the next vertical sync event. In interlaced mode, we need to adjust
+	 * the address and stride to display only the current field, saving
+	 * the original address (so it can be flipped for subsequent fields).
 	 */
+	addr += offset;
+	dpi->last_dma_addr = addr;
+	dpi->last_stride = stride;
+	if (dpi->interlaced) {
+		if (dpi->lower_field_flag)
+			addr += stride;
+		stride *= 2;
+	}
 	rp1dpi_hw_write(dpi, DPI_DMA_DMA_STRIDE, stride);
-	rp1dpi_hw_write(dpi, DPI_DMA_DMA_ADDR_H, a >> 32);
-	rp1dpi_hw_write(dpi, DPI_DMA_DMA_ADDR_L, a & 0xFFFFFFFFu);
+	rp1dpi_hw_write(dpi, DPI_DMA_DMA_ADDR_H, addr >> 32);
+	rp1dpi_hw_write(dpi, DPI_DMA_DMA_ADDR_L, addr & 0xFFFFFFFFu);
+
+	spin_unlock_irqrestore(&dpi->hw_lock, flags);
 }
 
 void rp1dpi_hw_stop(struct rp1_dpi *dpi)
 {
 	u32 ctrl;
+	unsigned long flags;
 
 	/*
-	 * Stop DMA by turning off the Auto-Repeat flag, and wait up to 100ms for
-	 * the current and any queued frame to end. "Force drain" flags are not used,
-	 * as they seem to prevent DMA from re-starting properly; it's safer to wait.
+	 * Stop DMA by turning off Auto-Repeat (and disable S/W field-flip),
+	 * then wait up to 100ms for the current and any queued frame to end.
+	 * (There is a "force drain" flag, but it can leave DPI in a broken
+	 * state which prevents it from restarting; it's safer to wait.)
 	 */
+	spin_lock_irqsave(&dpi->hw_lock, flags);
+	dpi->last_dma_addr = 0;
 	reinit_completion(&dpi->finished);
 	ctrl = rp1dpi_hw_read(dpi, DPI_DMA_CONTROL);
 	ctrl &= ~(DPI_DMA_CONTROL_ARM_MASK | DPI_DMA_CONTROL_AUTO_REPEAT_MASK);
 	rp1dpi_hw_write(dpi, DPI_DMA_CONTROL, ctrl);
+	spin_unlock_irqrestore(&dpi->hw_lock, flags);
+
 	if (!wait_for_completion_timeout(&dpi->finished, HZ / 10))
 		drm_err(&dpi->drm, "%s: timed out waiting for idle\n", __func__);
 	rp1dpi_hw_write(dpi, DPI_DMA_IRQ_EN, 0);
@@ -476,10 +562,11 @@ void rp1dpi_hw_stop(struct rp1_dpi *dpi)
 void rp1dpi_hw_vblank_ctrl(struct rp1_dpi *dpi, int enable)
 {
 	rp1dpi_hw_write(dpi, DPI_DMA_IRQ_EN,
-			BITS(DPI_DMA_IRQ_EN_AFIFO_EMPTY, 1)      |
-			BITS(DPI_DMA_IRQ_EN_UNDERFLOW, 1)        |
-			BITS(DPI_DMA_IRQ_EN_DMA_READY, !!enable) |
-			BITS(DPI_DMA_IRQ_EN_MATCH_LINE, 4095));
+			BITS(DPI_DMA_IRQ_EN_AFIFO_EMPTY, 1)         |
+			BITS(DPI_DMA_IRQ_EN_UNDERFLOW, 1)           |
+			BITS(DPI_DMA_IRQ_EN_DMA_READY, !!enable)    |
+			BITS(DPI_DMA_IRQ_EN_MATCH, dpi->interlaced) |
+			BITS(DPI_DMA_IRQ_EN_MATCH_LINE, 32));
 }
 
 irqreturn_t rp1dpi_hw_isr(int irq, void *dev)
@@ -498,7 +585,35 @@ irqreturn_t rp1dpi_hw_isr(int irq, void
 				drm_crtc_handle_vblank(&dpi->pipe.crtc);
 			if (u & DPI_DMA_IRQ_FLAGS_AFIFO_EMPTY_MASK)
 				complete(&dpi->finished);
+
+			/*
+			 * Added for interlace support: We use this mid-frame interrupt to
+			 * wobble the VFP between fields, re-submitting the next-buffer address
+			 * with an offset to display the opposite field. NB: rp1dpi_hw_update()
+			 * may be called at any time, before or after, so locking is needed.
+			 * H/W Auto-update is no longer needed (unless this IRQ is lost).
+			 */
+			if ((u & DPI_DMA_IRQ_FLAGS_MATCH_MASK) && dpi->interlaced) {
+				unsigned long flags;
+				dma_addr_t a;
+
+				spin_lock_irqsave(&dpi->hw_lock, flags);
+				dpi->lower_field_flag = !dpi->lower_field_flag;
+				rp1dpi_hw_write(dpi, DPI_DMA_FRONT_PORCH,
+						dpi->shorter_front_porch +
+						BITS(DPI_DMA_FRONT_PORCH_ROWSM1,
+						     dpi->lower_field_flag));
+				a = dpi->last_dma_addr;
+				if (a) {
+					if (dpi->lower_field_flag)
+						a += dpi->last_stride;
+					rp1dpi_hw_write(dpi, DPI_DMA_DMA_ADDR_H, a >> 32);
+					rp1dpi_hw_write(dpi, DPI_DMA_DMA_ADDR_L, a & 0xFFFFFFFFu);
+				}
+				spin_unlock_irqrestore(&dpi->hw_lock, flags);
+			}
 		}
 	}
+
 	return u ? IRQ_HANDLED : IRQ_NONE;
 }
--- /dev/null
+++ b/drivers/gpu/drm/rp1/rp1-dpi/rp1_dpi_pio.c
@@ -0,0 +1,225 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * PIO code for Raspberry Pi RP1 DPI driver
+ *
+ * Copyright (c) 2024 Raspberry Pi Limited.
+ */
+
+/*
+ * Use PIO to fix up VSYNC for interlaced modes.
+ *
+ * For this to work we *require* DPI's pinctrl to enable DE on GPIO1.
+ * PIO can then snoop on HSYNC and DE pins to generate corrected VSYNC.
+ *
+ * Note that corrected VSYNC outputs will not be synchronous to DPICLK,
+ * will lag HSYNC by about 30ns and may suffer up to 5ns of jitter.
+ */
+
+#include <linux/kernel.h>
+#include <linux/errno.h>
+#include <linux/of.h>
+#include <linux/pio_rp1.h>
+#include <linux/pinctrl/consumer.h>
+#include <linux/platform_device.h>
+#include <drm/drm_print.h>
+
+#include "rp1_dpi.h"
+
+/*
+ * Start a PIO SM to generate an interrupt just after HSYNC onset, then another
+ * after a fixed delay (during which we assume HSYNC will have been deasserted).
+ */
+
+static int rp1dpi_pio_start_timer_both(struct rp1_dpi *dpi, u32 flags, u32 tc)
+{
+	static const u16 instructions[2][5] = {
+		{ 0xa022, 0x2083, 0xc001, 0x0043, 0xc001 }, /* posedge */
+		{ 0xa022, 0x2003, 0xc001, 0x0043, 0xc001 }, /* negedge */
+	};
+	const struct pio_program prog = {
+		.instructions = instructions[(flags & DRM_MODE_FLAG_NHSYNC) ? 1 : 0],
+		.length = ARRAY_SIZE(instructions[0]),
+		.origin = -1
+	};
+	int offset, sm;
+
+	sm = pio_claim_unused_sm(dpi->pio, true);
+	if (sm < 0)
+		return -EBUSY;
+
+	offset = pio_add_program(dpi->pio, &prog);
+	if (offset == PIO_ORIGIN_ANY)
+		return -EBUSY;
+
+	pio_sm_config cfg = pio_get_default_sm_config();
+
+	pio_sm_set_enabled(dpi->pio, sm, false);
+	sm_config_set_wrap(&cfg, offset, offset + 4);
+	pio_sm_init(dpi->pio, sm, offset, &cfg);
+
+	pio_sm_put(dpi->pio, sm, tc - 4);
+	pio_sm_exec(dpi->pio, sm, pio_encode_pull(false, false));
+	pio_sm_exec(dpi->pio, sm, pio_encode_out(pio_y, 32));
+	pio_sm_set_enabled(dpi->pio, sm, true);
+
+	return 0;
+}
+
+/*
+ * Snoop on DE, HSYNC to count half-lines in the vertical blanking interval
+ * to determine when the VSYNC pulse should start and finish. Then, at a
+ * suitable moment (which should be an odd number of half-lines since the
+ * last active line), sample DE again to detect field phase.
+ *
+ * This version assumes VFP length is within 2..129 half-lines for any field
+ * (one half-line delay is needed to sample DE; we always wait for the next
+ * half-line boundary to improve VSync start accuracy).
+ */
+
+static int rp1dpi_pio_vsync_ilace(struct rp1_dpi *dpi,
+				  struct drm_display_mode const *mode)
+{
+	static const int wrap_target = 14;
+	static const int wrap = 26;
+	u16 instructions[] = {  /* This is mutable */
+		0xa0e6, //  0: mov    osr, isr    side 0     ; top: rewind parameters
+		0x2081, //  1: wait   1 gpio, 1   side 0     ; main: while (!DE) wait;
+		0x2783, //  2: wait   1 gpio, 3   side 0 [7] ;  do { @HSync
+		0xc041, //  3: irq    clear 1     side 0     ;   flush stale IRQs
+		0x20c1, //  4: wait   1 irq, 1    side 0     ;   @midline
+		0x00c1, //  5: jmp    pin, 1      side 0     ;  } while (DE)
+		0x0007, //  6: jmp    7           side 0     ;  <modify for -DE fixup>
+		0x6027, //  7: out    x, 7        side 0     ;  x = VFPlen - 2
+		0x000a, //  8: jmp    10          side 0     ;  while (x--) {
+		0x20c1, //  9: wait   1 irq, 1    side 0     ;    @halfline
+		0x0049, // 10: jmp    x--, 9      side 0     ;  }
+		0x6021, // 11: out    x, 1        side 0     ;  test for aligned case
+		0x003a, // 12: jmp    !x, 26      side 0     ;  if (!x) goto precise;
+		0x20c1, // 13: wait   1 irq, 1    side 0     ;  @halfline
+		//     .wrap_target                          ; vsjoin:
+		0xb722, // 14: mov    x, y        side 1 [7] ;  VSYNC=1; x = VSyncLen
+		0xd041, // 15: irq    clear 1     side 1     ;  VSYNC=1; flush stale IRQs
+		0x30c1, // 16: wait   1 irq, 1    side 1     ;  VSYNC=1; do { @halfline
+		0x1050, // 17: jmp    x--, 16     side 1     ;  VSYNC=1; } while (x--)
+		0x6028, // 18: out    x, 8        side 0     ;  VSYNC=0; x = VBPLen
+		0x0015, // 19: jmp    21          side 0     ;  while (x--) {
+		0x20c1, // 20: wait   1 irq, 1    side 0     ;    @halfline
+		0x0054, // 21: jmp    x--, 20     side 0     ;  }
+		0x00c0, // 22: jmp    pin, 0      side 0     ;  if (DE) reset phase
+		0x0018, // 23: jmp    24          side 0     ;  <modify for -DE fixup>
+		0x00e1, // 24: jmp    !osre, 1    side 0     ;  if (!phase) goto main
+		0x0000, // 25: jmp    0           side 0     ;  goto top
+		0x2083, // 26: wait   1 gpio, 3   side 0     ; precise: @HSync
+		//     .wrap                                 ;  goto vsjoin
+	};
+	struct pio_program prog = {
+		.instructions = instructions,
+		.length = ARRAY_SIZE(instructions),
+		.origin = -1
+	};
+	pio_sm_config cfg = pio_get_default_sm_config();
+	unsigned int i, offset;
+	u32 tc, vfp, vbp;
+	u32 sysclk = clock_get_hz(clk_sys);
+	int sm = pio_claim_unused_sm(dpi->pio, true);
+
+	if (sm < 0)
+		return -EBUSY;
+
+	/* Compute mid-line time constant and start the timer SM */
+	tc = (mode->htotal * (u64)sysclk) / (u64)(2000u * mode->clock);
+	if (rp1dpi_pio_start_timer_both(dpi, mode->flags, tc) < 0) {
+		pio_sm_unclaim(dpi->pio, sm);
+		return -EBUSY;
+	}
+
+	/* Adapt program code according to DE and Sync polarity; configure program */
+	pio_sm_set_enabled(dpi->pio, sm, false);
+	if (dpi->de_inv) {
+		instructions[1] ^= 0x0080;
+		instructions[5]  = 0x00c7;
+		instructions[6]  = 0x0001;
+		instructions[22] = 0x00d8;
+		instructions[23] = 0x0000;
+	}
+	for (i = 0; i < ARRAY_SIZE(instructions); i++) {
+		if (mode->flags & DRM_MODE_FLAG_NVSYNC)
+			instructions[i] ^= 0x1000;
+		if ((mode->flags & DRM_MODE_FLAG_NHSYNC) && (instructions[i] & 0xe07f) == 0x2003)
+			instructions[i] ^= 0x0080;
+	}
+	offset = pio_add_program(dpi->pio, &prog);
+	if (offset == PIO_ORIGIN_ANY)
+		return -EBUSY;
+
+	/* Configure pins and SM */
+	dpi->pio_stole_gpio2 = true;
+	sm_config_set_wrap(&cfg, offset + wrap_target, offset + wrap);
+	sm_config_set_sideset(&cfg, 1, false, false);
+	sm_config_set_sideset_pins(&cfg, 2);
+	pio_gpio_init(dpi->pio, 2);
+	sm_config_set_jmp_pin(&cfg, 1); /* "DE" is always GPIO1 */
+	pio_sm_init(dpi->pio, sm, offset, &cfg);
+	pio_sm_set_consecutive_pindirs(dpi->pio, sm, 2, 1, true);
+
+	/* Compute vertical times, remembering how we rounded vdisplay, vtotal */
+	vfp = mode->vsync_start - (mode->vdisplay & ~1);
+	vbp = (mode->vtotal | 1) - mode->vsync_end;
+	if (vfp > 128) {
+		vbp += vfp - 128;
+		vfp = 128;
+	} else if (vfp < 3) {
+		vbp = (vbp > 3 - vfp) ? (vbp - 3 + vfp) : 0;
+		vfp = 3;
+	}
+
+	pio_sm_put(dpi->pio, sm,
+		   (vfp - 2) + ((vfp & 1) << 7) + (vbp << 8) +
+		   ((vfp - 3) << 16) + (((~vfp) & 1) << 23) + ((vbp + 1) << 24));
+	pio_sm_put(dpi->pio, sm, mode->vsync_end - mode->vsync_start - 1);
+	pio_sm_exec(dpi->pio, sm, pio_encode_pull(false, false));
+	pio_sm_exec(dpi->pio, sm, pio_encode_out(pio_y, 32));
+	pio_sm_exec(dpi->pio, sm, pio_encode_in(pio_y, 32));
+	pio_sm_exec(dpi->pio, sm, pio_encode_pull(false, false));
+	pio_sm_exec(dpi->pio, sm, pio_encode_out(pio_y, 32));
+	pio_sm_set_enabled(dpi->pio, sm, true);
+
+	return 0;
+}
+
+int rp1dpi_pio_start(struct rp1_dpi *dpi, const struct drm_display_mode *mode)
+{
+	int r;
+
+	if (!(mode->flags & DRM_MODE_FLAG_INTERLACE) || !dpi->gpio1_used)
+		return 0;
+
+	if (dpi->pio)
+		pio_close(dpi->pio);
+
+	dpi->pio = pio_open();
+	if (IS_ERR(dpi->pio)) {
+		drm_err(&dpi->drm, "Could not open PIO\n");
+		dpi->pio = NULL;
+		return -ENODEV;
+	}
+
+	r = rp1dpi_pio_vsync_ilace(dpi, mode);
+	if (r) {
+		drm_err(&dpi->drm, "Failed to initialize PIO\n");
+		rp1dpi_pio_stop(dpi);
+	}
+
+	return r;
+}
+
+void rp1dpi_pio_stop(struct rp1_dpi *dpi)
+{
+	if (dpi->pio) {
+		if (dpi->pio_stole_gpio2)
+			pio_gpio_set_function(dpi->pio, 2, GPIO_FUNC_FSEL1);
+		pio_close(dpi->pio);
+		dpi->pio_stole_gpio2 = false;
+		dpi->pio = NULL;
+	}
+}
