All of lore.kernel.org
 help / color / mirror / Atom feed
From: Cody Kang via B4 Relay <devnull+codykang.hk.gmail.com@kernel.org>
To: "David Airlie" <airlied@gmail.com>,
	"Simona Vetter" <simona@ffwll.ch>,
	"Maarten Lankhorst" <maarten.lankhorst@linux.intel.com>,
	"Maxime Ripard" <mripard@kernel.org>,
	"Thomas Zimmermann" <tzimmermann@suse.de>,
	"Rob Herring" <robh@kernel.org>,
	"Krzysztof Kozlowski" <krzk+dt@kernel.org>,
	"Conor Dooley" <conor+dt@kernel.org>,
	"Yixun Lan" <dlan@kernel.org>, "Vinod Koul" <vkoul@kernel.org>,
	"Neil Armstrong" <neil.armstrong@linaro.org>,
	"Haylen Chu" <heylenay@4d2.org>,
	"Michael Turquette" <mturquette@baylibre.com>,
	"Stephen Boyd" <sboyd@kernel.org>,
	"Brian Masney" <bmasney@redhat.com>,
	"Philipp Zabel" <p.zabel@pengutronix.de>,
	"Paul Walmsley" <pjw@kernel.org>,
	"Palmer Dabbelt" <palmer@dabbelt.com>,
	"Albert Ou" <aou@eecs.berkeley.edu>,
	"Alexandre Ghiti" <alex@ghiti.fr>, "Yao Zi" <me@ziyao.cc>,
	"Uwe Kleine-König" <u.kleine-koenig@baylibre.com>,
	"Guodong Xu" <docular.xu@gmail.com>
Cc: dri-devel@lists.freedesktop.org, linux-riscv@lists.infradead.org,
	 devicetree@vger.kernel.org, spacemit@lists.linux.dev,
	 linux-kernel@vger.kernel.org, linux-phy@lists.infradead.org,
	 linux-clk@vger.kernel.org, Cody Kang <codykang.hk@gmail.com>
Subject: [PATCH v2 12/17] drm/spacemit: add Innosilicon DP/eDP controller bridge driver
Date: Sun, 09 Aug 2026 21:14:21 +0800	[thread overview]
Message-ID: <20260809-k3-display-v2-12-327d7910bf71@gmail.com> (raw)
In-Reply-To: <20260809-k3-display-v2-0-327d7910bf71@gmail.com>

From: Cody Kang <codykang.hk@gmail.com>

Add the DP/eDP controller that sits downstream of the Saturn DPU. Two
identical instances share one compatible; the eDP-vs-DP role is board
wiring, so it is taken from the devicetree: an eDP panel always sits
under an aux-bus child node, an external DP connector never does.

The link is driven through the generic PHY framework, so the controller
never touches a PLL register. The controller's HPD interrupt is gated by
the DP pixel clock, which can be off exactly when a plug has to be
caught, so the connector is also polled and the interrupt path re-reads
the live level when it does fire.

Signed-off-by: Cody Kang <codykang.hk@gmail.com>

---
v2:
- use devm_reset_control_get_exclusive_deasserted(), dropping the manual
  deassert/assert paths and the reset field (Philipp Zabel)
- hand the pxclk disable to a devm action registered after the PHY
  child is populated (Yao Zi)
- drop the dead NULL-clk guards (Yao Zi)
- drop the error message duplicated by devm_request_threaded_irq()
  (Yao Zi)
- ack the W1C DP_GENERAL_INTERRUPT bits with plain writes (Sashiko)
- return IRQ_HANDLED when the ISR consumed an HPD latch instead of
  IRQ_NONE (Sashiko)
- read EDID blocks through the aux I2C adapter (Sashiko)
- drop DRM_BRIDGE_OP_MODES from the DP branch (Sashiko)
- respond to Rob Herring's suggestion, port@1 required, compute
  possible_crtcs from the input endpoint only
---
 drivers/gpu/drm/spacemit/Kconfig            |   19 +
 drivers/gpu/drm/spacemit/Makefile           |    3 +
 drivers/gpu/drm/spacemit/spacemit_inno_dp.c | 2391 +++++++++++++++++++++++++++
 drivers/gpu/drm/spacemit/spacemit_inno_dp.h |  329 ++++
 4 files changed, 2742 insertions(+)

diff --git a/drivers/gpu/drm/spacemit/Kconfig b/drivers/gpu/drm/spacemit/Kconfig
index 51e38dabfdba5..afd6cdea27fc2 100644
--- a/drivers/gpu/drm/spacemit/Kconfig
+++ b/drivers/gpu/drm/spacemit/Kconfig
@@ -16,3 +16,22 @@ config DRM_SPACEMIT
 	  DisplayPort or embedded DisplayPort controller.
 
 	  If M is selected the module will be called spacemit-drm.
+
+config SPACEMIT_INNO_DP
+	tristate "SpacemiT Innosilicon DP/eDP controller support"
+	depends on DRM_SPACEMIT
+	imply PHY_SPACEMIT_K3_INNO_DP
+	select GENERIC_PHY
+	select DRM_PANEL
+	select DRM_BRIDGE_CONNECTOR
+	select DRM_DISPLAY_HELPER
+	select DRM_DISPLAY_DP_HELPER
+	select DRM_DISPLAY_DP_AUX_BUS
+	help
+	  Choose this option to enable the Innosilicon DisplayPort /
+	  embedded DisplayPort transmitter integrated in SpacemiT SoCs.
+	  The controller sits downstream of the Saturn DPU and drives an
+	  eDP panel (discovered over the DP AUX bus) or an external
+	  DisplayPort connector.
+
+	  If M is selected the module will be called spacemit-inno-dp.
diff --git a/drivers/gpu/drm/spacemit/Makefile b/drivers/gpu/drm/spacemit/Makefile
index b3b148000cf7b..260d316a271b5 100644
--- a/drivers/gpu/drm/spacemit/Makefile
+++ b/drivers/gpu/drm/spacemit/Makefile
@@ -9,4 +9,7 @@ spacemit-drm-y := spacemit_drm.o \
 		  dpu/dpu_saturn_hee.o \
 		  dpu/saturn_fbcmem.o
 
+spacemit-inno-dp-y := spacemit_inno_dp.o
+
 obj-$(CONFIG_DRM_SPACEMIT) += spacemit-drm.o
+obj-$(CONFIG_SPACEMIT_INNO_DP) += spacemit-inno-dp.o
diff --git a/drivers/gpu/drm/spacemit/spacemit_inno_dp.c b/drivers/gpu/drm/spacemit/spacemit_inno_dp.c
new file mode 100644
index 0000000000000..1ba4892607881
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/spacemit_inno_dp.c
@@ -0,0 +1,2391 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ */
+
+#include <linux/of.h>
+#include <linux/bits.h>
+#include <linux/bitfield.h>
+#include <linux/module.h>
+#include <linux/slab.h>
+#include <linux/platform_device.h>
+#include <linux/component.h>
+#include <linux/clk.h>
+#include <linux/cleanup.h>
+#include <linux/math64.h>
+#include <linux/mutex.h>
+#include <linux/reset.h>
+#include <linux/delay.h>
+#include <linux/i2c.h>
+#include <linux/iopoll.h>
+#include <linux/media-bus-format.h>
+#include <linux/of_graph.h>
+#include <linux/of_platform.h>
+#include <linux/phy/phy.h>
+#include <linux/phy/phy-dp.h>
+#include <linux/regmap.h>
+#include <drm/display/drm_dp_aux_bus.h>
+#include <drm/drm_of.h>
+#include <drm/drm_bridge.h>
+#include <drm/drm_bridge_connector.h>
+#include <drm/drm_device.h>
+#include <drm/drm_encoder.h>
+#include <drm/drm_connector.h>
+#include <drm/drm_drv.h>
+#include <drm/drm_edid.h>
+#include <drm/drm_modeset_helper_vtables.h>
+#include <drm/drm_gem_dma_helper.h>
+#include <drm/drm_probe_helper.h>
+#include <drm/drm_atomic.h>
+#include <drm/drm_atomic_state_helper.h>
+#include <drm/display/drm_dp_aux_bus.h>
+#include <drm/display/drm_dp.h>
+#include <drm/display/drm_dp_helper.h>
+#include <drm/drm_print.h>
+
+#include "spacemit_inno_dp.h"
+
+#define SPACEMIT_DP_SWING_MAX  2
+#define SPACEMIT_DP_PREEMP_MAX 2
+#define SPACEMIT_DP_AUX_MAX_RETRIES 3
+
+#define SPACEMIT_DP_DDC_SEGMENT_ADDR 0x30
+#define SPACEMIT_DP_SINK_READY_DELAY_MS 120
+#define SPACEMIT_DP_SINK_READY_RETRIES 3
+
+/*
+ * With DP routed through a Type-C PD controller the HPD pin is virtual and
+ * can re-assert seconds after a transmitter power-cycle. Asserted HPD
+ * returns immediately; the full wait is paid only when no sink shows up.
+ */
+#define SPACEMIT_DP_HPD_POLL_US     1000
+#define SPACEMIT_DP_HPD_DEBOUNCE_US (1000 * USEC_PER_MSEC)
+
+enum spacemit_dp_link_rate {
+	SPACEMIT_DP_LINK_RATE_1_62 = 1620000, /* 1.62 Gbps */
+	SPACEMIT_DP_LINK_RATE_2_70 = 2700000, /* 2.70 Gbps */
+	SPACEMIT_DP_LINK_RATE_5_40 = 5400000, /* 5.40 Gbps */
+	SPACEMIT_DP_LINK_RATE_8_10 = 8100000, /* 8.10 Gbps */
+};
+
+/* The K3 source tops out at HBR2: the PHY's MPLL has no 8.1 Gbps config. */
+#define SPACEMIT_DP_SOURCE_MAX_RATE	SPACEMIT_DP_LINK_RATE_5_40
+
+enum spacemit_dp_lane_count {
+	SPACEMIT_DP_LANE_1 = 1,
+	SPACEMIT_DP_LANE_2 = 2,
+	SPACEMIT_DP_LANE_4 = 4,
+};
+
+enum soc_video_format {
+	SOC_VIDEO_RGB_6BIT = 0,
+	SOC_VIDEO_RGB_8BIT = 1,
+	SOC_VIDEO_RGB_10BIT = 2,
+	SOC_VIDEO_RGB_12BIT = 3,
+	SOC_VIDEO_RGB_16BIT = 4,
+	SOC_VIDEO_YUV444_8BIT = 5,
+	SOC_VIDEO_YUV444_10BIT = 6,
+	SOC_VIDEO_YUV444_12BIT = 7,
+	SOC_VIDEO_YUV444_16BIT = 8,
+	SOC_VIDEO_YUV422_8BIT = 9,
+	SOC_VIDEO_YUV422_10BIT = 10,
+	SOC_VIDEO_YUV422_12BIT = 11,
+	SOC_VIDEO_YUV422_16BIT = 12,
+};
+
+enum spacemit_dp_ref_clk {
+	SPACEMIT_DP_REF_CLK_24M = 24000,
+};
+
+static const struct spacemit_dp_link_config {
+	enum spacemit_dp_link_rate rate;
+	enum spacemit_dp_lane_count lanes;
+} spacemit_dp_link_priority_table[] = {
+	{SPACEMIT_DP_LINK_RATE_1_62, SPACEMIT_DP_LANE_1}, /* 1.62 Gbps */
+	{SPACEMIT_DP_LINK_RATE_2_70, SPACEMIT_DP_LANE_1}, /* 2.70 Gbps */
+	{SPACEMIT_DP_LINK_RATE_1_62, SPACEMIT_DP_LANE_2}, /* 3.24 Gbps */
+
+	{SPACEMIT_DP_LINK_RATE_2_70, SPACEMIT_DP_LANE_2}, /* 5.40 Gbps */
+	{SPACEMIT_DP_LINK_RATE_1_62, SPACEMIT_DP_LANE_4}, /* 6.48 Gbps */
+
+	{SPACEMIT_DP_LINK_RATE_2_70, SPACEMIT_DP_LANE_4}, /* 10.8 Gbps */
+	{SPACEMIT_DP_LINK_RATE_5_40, SPACEMIT_DP_LANE_2}, /* 10.8 Gbps */
+
+	{SPACEMIT_DP_LINK_RATE_5_40, SPACEMIT_DP_LANE_4}, /* 21.6 Gbps */
+};
+
+static const struct soc_format_info {
+	u8 bpp; /* Bits Per Pixel */
+} format_info_table[] = {
+	[SOC_VIDEO_RGB_6BIT]      = { .bpp = 18 },
+	[SOC_VIDEO_RGB_8BIT]      = { .bpp = 24 },
+	[SOC_VIDEO_RGB_10BIT]     = { .bpp = 30 },
+	[SOC_VIDEO_RGB_12BIT]     = { .bpp = 36 },
+	[SOC_VIDEO_RGB_16BIT]     = { .bpp = 48 },
+
+	[SOC_VIDEO_YUV444_8BIT]   = { .bpp = 24 },
+	[SOC_VIDEO_YUV444_10BIT]  = { .bpp = 30 },
+	[SOC_VIDEO_YUV444_12BIT]  = { .bpp = 36 },
+	[SOC_VIDEO_YUV444_16BIT]  = { .bpp = 48 },
+
+	[SOC_VIDEO_YUV422_8BIT]   = { .bpp = 16 },
+	[SOC_VIDEO_YUV422_10BIT]  = { .bpp = 20 },
+	[SOC_VIDEO_YUV422_12BIT]  = { .bpp = 24 },
+	[SOC_VIDEO_YUV422_16BIT]  = { .bpp = 32 },
+};
+
+/**
+ * struct spacemit_dp_dev - Per-instance state for an Innosilicon DP/eDP
+ * controller
+ * @dev: Underlying platform device.
+ * @drm: Parent DRM device, set at component bind.
+ * @encoder: Passive encoder anchoring possible_crtcs; atomic goes through
+ * @bridge.
+ * @bridge: DRM bridge implementing this controller, attached NO_CONNECTOR.
+ * @connector: Connector built on top of @bridge by drm_bridge_connector_init().
+ * @connector_status: Last-known status, driven by the HPD ISR and by polling.
+ * @mode: Committed adjusted_mode, cached under @mode_lock for the async paths.
+ * @regs: Mapped MMIO base of the DP controller register block.
+ * @aux: DPCD AUX channel, registered at bind.
+ * @dpcd: Raw DPCD capability bytes.
+ * @lane_count: Lane count chosen by link training (1, 2 or 4).
+ * @link_rate: Link rate chosen by link training, in kHz.
+ * @link: Sink-advertised link capability snapshot.
+ * @link.revision: DPCD revision byte.
+ * @link.enhanced_framing: Whether the sink advertises enhanced framing.
+ * @link.max_rate: Sink-advertised maximum link rate, kHz.
+ * @link.max_num_lanes: Sink-advertised maximum lane count.
+ * @pxclk: Optional external pixel clock.
+ * @pll_clk: The PHY's pixel PLL; clk_set_rate() programs it.
+ * @phy: DP PHY handle.
+ * @phy_powered: Controller-side mirror of the PHY framework's power refcount.
+ * @next_bridge: Downstream bridge; panel-edp's drm_panel wrapper on eDP.
+ * @edp_mode: True when this instance drives an embedded panel.
+ * @use_ext_pixel_clock: True when the pixel clock is sourced externally.
+ * @pixel_clock: Cached external pixel-clock rate, kHz.
+ * @mode_lock: Serialises mode_set / enable / hotplug against each other.
+ * @modeset_retry_work: Deferred "link is bad" notification, see its handler.
+ * @irq: Controller interrupt line.
+ */
+struct spacemit_dp_dev {
+	struct device *dev;
+
+	struct drm_device *drm;
+	struct drm_encoder encoder;
+	struct drm_bridge bridge;
+	struct drm_connector *connector;
+
+	enum drm_connector_status connector_status;
+	struct drm_display_mode mode;
+
+	struct regmap *regs;
+	struct drm_dp_aux aux;
+
+	u8 dpcd[DP_RECEIVER_CAP_SIZE];
+	int lane_count;
+	u32 link_rate;
+
+	struct {
+		u8 revision;
+		u8 enhanced_framing;
+		u32 max_rate;
+		u32 max_num_lanes;
+	} link;
+
+	struct clk *pxclk;
+	struct clk *pll_clk;
+	struct phy *phy;
+	bool phy_powered;
+	struct drm_bridge *next_bridge;
+
+	bool edp_mode;
+	bool use_ext_pixel_clock;
+	int pixel_clock;
+	struct mutex mode_lock;
+
+	struct work_struct modeset_retry_work;
+
+	int irq;
+};
+
+static int spacemit_dp_get_bpp(struct spacemit_dp_dev *dp, u32 format)
+{
+	if (format >= ARRAY_SIZE(format_info_table)) {
+		dev_warn(dp->dev,
+			"invalid color format index %d, defaulting to RGB888\n",
+			format);
+		return 24;
+	}
+	return format_info_table[format].bpp;
+}
+
+/*
+ * drm_connector_set_link_status_property() takes connection_mutex, which the
+ * commit already holds: calling it from .atomic_enable deadlocks against
+ * itself.
+ */
+static void spacemit_dp_modeset_retry_work(struct work_struct *work)
+{
+	struct spacemit_dp_dev *dp = container_of(work, struct spacemit_dp_dev,
+						  modeset_retry_work);
+	struct drm_connector *connector = dp->connector;
+
+	if (!connector)
+		return;
+
+	mutex_lock(&connector->dev->mode_config.mutex);
+	drm_connector_set_link_status_property(connector,
+					       DRM_MODE_LINK_STATUS_BAD);
+	mutex_unlock(&connector->dev->mode_config.mutex);
+
+	drm_kms_helper_connector_hotplug_event(connector);
+}
+
+/**
+ * struct spacemit_dp_bridge_state - Per-state planned link configuration.
+ * @base: Parent &struct drm_bridge_state.
+ * @link_cfg_idx: Index into spacemit_dp_link_priority_table[] picked by
+ *	->atomic_check(); -1 means ->atomic_enable() walks the table from the start.
+ * @color_format: Output colour format selected by ->atomic_check().
+ */
+struct spacemit_dp_bridge_state {
+	struct drm_bridge_state base;
+	int link_cfg_idx;
+	u32 color_format;
+};
+
+static inline struct spacemit_dp_bridge_state *
+to_spacemit_dp_bridge_state(struct drm_bridge_state *state)
+{
+	return container_of(state, struct spacemit_dp_bridge_state, base);
+}
+
+static const struct regmap_config spacemit_dp_regmap_config = {
+	.name		= "dp",
+	.reg_bits	= 32,
+	.val_bits	= 32,
+	.reg_stride	= 4,
+	.max_register	= 0x3ffc,	/* full 0x4000 reg region from binding */
+};
+
+static u32 spacemit_dp_aux_get_cmd(struct drm_dp_aux_msg *msg)
+{
+	switch (msg->request & ~DP_AUX_I2C_MOT) {
+	case DP_AUX_NATIVE_WRITE:
+	case DP_AUX_I2C_WRITE:
+	case DP_AUX_I2C_WRITE_STATUS_UPDATE:
+		return msg->request;
+	case DP_AUX_NATIVE_READ:
+	case DP_AUX_I2C_READ:
+		return msg->request;
+	default:
+		return 0;
+	}
+
+	return 0;
+}
+
+static void spacemit_dp_aux_hw_reset(struct spacemit_dp_dev *dp)
+{
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_AUX_RESET,
+			  FIELD_PREP(DP_AUX_RESET, 0x1));
+	usleep_range(2000, 2500);
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_AUX_RESET,
+			  FIELD_PREP(DP_AUX_RESET, 0x0));
+	usleep_range(2000, 2500);
+	regmap_write(dp->regs, DP_GENERAL_INTERRUPT,
+		     DP_AUX_REPLY_EVENT_INT_STA);
+}
+
+static bool spacemit_dp_dpcd_caps_valid(const u8 *dpcd)
+{
+	u8 max_bw = dpcd[DP_MAX_LINK_RATE];
+	u8 max_lanes = dpcd[DP_MAX_LANE_COUNT] & DP_MAX_LANE_COUNT_MASK;
+
+	if (!dpcd[DP_DPCD_REV])
+		return false;
+
+	switch (max_bw) {
+	case DP_LINK_BW_1_62:
+	case DP_LINK_BW_2_7:
+	case DP_LINK_BW_5_4:
+	case DP_LINK_BW_8_1:
+		break;
+	default:
+		return false;
+	}
+
+	return max_lanes == 1 || max_lanes == 2 || max_lanes == 4;
+}
+
+static ssize_t spacemit_dp_aux_transfer(struct drm_dp_aux *aux,
+					struct drm_dp_aux_msg *msg)
+{
+	int ret, i, retries = 0;
+	unsigned long timeout;
+
+	struct spacemit_dp_dev *dp = container_of(aux, struct spacemit_dp_dev,
+						  aux);
+	u32 cmd, len, val, status;
+	u32 data[4] = {0};
+	u8 *buf = msg->buffer;
+	bool is_read = (msg->request & DP_AUX_I2C_READ) ||
+		((msg->request & DP_AUX_NATIVE_READ) == DP_AUX_NATIVE_READ);
+
+	if (msg->size > 16)
+		return -EINVAL;
+
+	cmd = spacemit_dp_aux_get_cmd(msg);
+
+retry_eio:
+	if (!is_read) {
+		/* Pack bytes into 32-bit words (Little Endian packing) */
+		for (i = 0; i < msg->size; i++)
+			data[i / 4] |= buf[i] << ((i % 4) * 8);
+
+		/* Write data to registers: DATA1(LSB)..DATA4(MSB) */
+		regmap_write_bits(dp->regs, DP_AUX_DATA1_REG, DP_AUX_DATA1,
+				  FIELD_PREP(DP_AUX_DATA1, data[0]));
+		regmap_write_bits(dp->regs, DP_AUX_DATA2_REG, DP_AUX_DATA2,
+				  FIELD_PREP(DP_AUX_DATA2, data[1]));
+		regmap_write_bits(dp->regs, DP_AUX_DATA3_REG, DP_AUX_DATA3,
+				  FIELD_PREP(DP_AUX_DATA3, data[2]));
+		regmap_write_bits(dp->regs, DP_AUX_DATA4_REG, DP_AUX_DATA4,
+				  FIELD_PREP(DP_AUX_DATA4, data[3]));
+	}
+
+	/* HW expects Length - 1 */
+	len = msg->size > 0 ? msg->size - 1 : 0;
+
+	regmap_write_bits(dp->regs, DP_AUX_CMD_REG, DP_AUX_LENGTH,
+			  FIELD_PREP(DP_AUX_LENGTH, len));
+	regmap_write_bits(dp->regs, DP_AUX_CMD_REG, DP_AUX_ADDR,
+			  FIELD_PREP(DP_AUX_ADDR, msg->address));
+	regmap_write_bits(dp->regs, DP_AUX_CMD_REG, DP_AUX_CMD_TYPE,
+			  FIELD_PREP(DP_AUX_CMD_TYPE, cmd));
+
+	regmap_write_bits(dp->regs, DP_AUX_START_REG, DP_AUX_START,
+			  FIELD_PREP(DP_AUX_START, 0));
+	regmap_write_bits(dp->regs, DP_AUX_START_REG, DP_AUX_START,
+			  FIELD_PREP(DP_AUX_START, 1));
+
+	timeout = jiffies + msecs_to_jiffies(200);
+	ret = -ETIMEDOUT;
+
+	while (1) {
+		regmap_read(dp->regs, DP_GENERAL_INTERRUPT, &val);
+		val = FIELD_GET(DP_AUX_REPLY_EVENT_INT_STA, val);
+		if (val) {
+			ret = 0;
+			break;
+		}
+		if (time_after(jiffies, timeout))
+			break;
+		usleep_range(100, 110);
+	}
+
+	if (ret) {
+		if (retries < SPACEMIT_DP_AUX_MAX_RETRIES) {
+			retries++;
+			spacemit_dp_aux_hw_reset(dp);
+			dev_dbg(dp->dev,
+				"AUX timeout, cmd=0x%x addr=0x%x size=%zu retry %d/%d\n",
+				cmd, msg->address, msg->size, retries, SPACEMIT_DP_AUX_MAX_RETRIES);
+			goto retry_eio;
+		}
+
+		dev_err(dp->dev,
+			"AUX transfer timeout, req: 0x%x, cmd: 0x%x, addr: 0x%x, size %zu\n",
+			msg->request, cmd, msg->address, msg->size);
+		return ret;
+	}
+
+	regmap_read(dp->regs, DP_AUX_STS_REG,
+		    &status); status = FIELD_GET(DP_AUX_STATUS, status);
+
+	/* Write 1 to clear. */
+	regmap_write(dp->regs, DP_GENERAL_INTERRUPT,
+		     DP_AUX_REPLY_EVENT_INT_STA);
+
+	/* Map HW status (the AUX reply command byte) to DRM reply codes */
+	switch (status) {
+	case 0: /* ACK */
+		msg->reply = DP_AUX_NATIVE_REPLY_ACK;
+		break;
+	case 1: /* NACK */
+		msg->reply = DP_AUX_NATIVE_REPLY_NACK;
+		return 0; /* Standard says return 0 on NACK for upper layer retry */
+	case 2: /* DEFER */
+		msg->reply = DP_AUX_NATIVE_REPLY_DEFER;
+		return 0;
+	case DP_AUX_I2C_REPLY_NACK: /* native ACK, I2C NACK */
+		msg->reply = DP_AUX_I2C_REPLY_NACK;
+		return 0;
+	case DP_AUX_I2C_REPLY_DEFER: /* native ACK, I2C DEFER */
+		msg->reply = DP_AUX_I2C_REPLY_DEFER;
+		return 0;
+	default:
+		regmap_read(dp->regs, DP_AUX_STS_REG, &val);
+		val = FIELD_GET(DP_AUX_REPLY_ERR_CODE, val);
+		if (retries < SPACEMIT_DP_AUX_MAX_RETRIES) {
+			retries++;
+			dev_dbg(dp->dev,
+				"AUX status, cmd=0x%x addr=0x%x size=%zu retry %d/%d\n",
+				cmd, msg->address, msg->size, retries, SPACEMIT_DP_AUX_MAX_RETRIES);
+			usleep_range(1000, 1100);
+			goto retry_eio;
+		}
+		dev_err(dp->dev,
+			"AUX error: cmd=0x%x addr=0x%x size=%zu status=0x%x code=0x%x\n",
+			cmd, msg->address, msg->size, status, val);
+		return -EIO;
+	}
+
+	if (is_read && msg->size > 0) {
+		regmap_read(dp->regs, DP_AUX_DATA1_REG, &data[0]);
+		data[0] = FIELD_GET(DP_AUX_DATA1, data[0]);
+		regmap_read(dp->regs, DP_AUX_DATA2_REG, &data[1]);
+		data[1] = FIELD_GET(DP_AUX_DATA2, data[1]);
+		regmap_read(dp->regs, DP_AUX_DATA3_REG, &data[2]);
+		data[2] = FIELD_GET(DP_AUX_DATA3, data[2]);
+		regmap_read(dp->regs, DP_AUX_DATA4_REG, &data[3]);
+		data[3] = FIELD_GET(DP_AUX_DATA4, data[3]);
+
+		/* Unpack 32-bit words back to bytes */
+		for (i = 0; i < msg->size; i++)
+			buf[i] = (data[i / 4] >> ((i % 4) * 8)) & 0xFF;
+	}
+
+	return msg->size;
+}
+
+static int update_edp_config(struct spacemit_dp_dev *dp, bool enable)
+{
+	u8 value;
+	int ret;
+
+	ret = drm_dp_dpcd_read_byte(&dp->aux, DP_EDP_CONFIGURATION_SET, &value);
+	if (ret < 0) {
+		dev_err(dp->dev,
+			"failed to read DP_EDP_CONFIGURATION_SET: %d\n", ret);
+		return ret;
+	}
+
+	if (enable)
+		value |= 0x01;
+	else
+		value &= ~0x01;
+
+	ret = drm_dp_dpcd_write_byte(&dp->aux, DP_EDP_CONFIGURATION_SET, value);
+	if (ret < 0) {
+		dev_err(dp->dev,
+			"failed to write DP_EDP_CONFIGURATION_SET: %d\n", ret);
+		return ret;
+	}
+
+	return 0;
+}
+
+/* Read the sink's DPCD link-layer capabilities. EDID is handled separately. */
+static int spacemit_dp_hw_read_sink_caps(struct spacemit_dp_dev *dp)
+{
+	ssize_t ret;
+	u8 max_bw;
+	int retry;
+
+	for (retry = 0; retry < SPACEMIT_DP_SINK_READY_RETRIES; retry++) {
+		if (retry) {
+			spacemit_dp_aux_hw_reset(dp);
+			msleep(SPACEMIT_DP_SINK_READY_DELAY_MS);
+		}
+
+		ret = drm_dp_read_dpcd_caps(&dp->aux, dp->dpcd);
+		if (ret < 0)
+			continue;
+
+		if (!spacemit_dp_dpcd_caps_valid(dp->dpcd)) {
+			dev_dbg(dp->dev,
+				"DPCD caps not ready: rev=0x%02x bw=0x%02x lanes=0x%02x\n",
+				dp->dpcd[DP_DPCD_REV], dp->dpcd[DP_MAX_LINK_RATE],
+				dp->dpcd[DP_MAX_LANE_COUNT]);
+			ret = -EAGAIN;
+			continue;
+		}
+
+		break;
+	}
+
+	if (ret < 0) {
+		dev_err(dp->dev, "failed to read DPCD after %d retries: %zd\n",
+			SPACEMIT_DP_SINK_READY_RETRIES, ret);
+		dp->link.revision = 0x14;
+		dp->link.max_rate = SPACEMIT_DP_LINK_RATE_5_40;
+		dp->link.max_num_lanes = SPACEMIT_DP_LANE_2;
+		dp->link.enhanced_framing = 1;
+		return ret;
+	}
+
+	dp->link.revision = dp->dpcd[DP_DPCD_REV];
+
+	max_bw = dp->dpcd[DP_MAX_LINK_RATE];
+	switch (max_bw) {
+	case DP_LINK_BW_1_62:
+		dp->link.max_rate = SPACEMIT_DP_LINK_RATE_1_62;
+		break;
+	case DP_LINK_BW_2_7:
+		dp->link.max_rate = SPACEMIT_DP_LINK_RATE_2_70;
+		break;
+	case DP_LINK_BW_5_4:
+		dp->link.max_rate = SPACEMIT_DP_LINK_RATE_5_40;
+		break;
+	case DP_LINK_BW_8_1:
+		dp->link.max_rate = SPACEMIT_DP_LINK_RATE_8_10;
+		break;
+	default:
+		dev_warn(dp->dev, "unknown DPCD max link rate 0x%x, defaulting to 5.40 Gbps\n",
+			 max_bw);
+		dp->link.revision = 0x14;
+		dp->link.max_rate = SPACEMIT_DP_LINK_RATE_5_40;
+		dp->link.max_num_lanes = SPACEMIT_DP_LANE_2;
+		dp->link.enhanced_framing = 1;
+		return -EINVAL;
+	}
+
+	/* Clamp to the source's actual ceiling (HBR2). */
+	dp->link.max_rate = min(dp->link.max_rate,
+				(u32)SPACEMIT_DP_SOURCE_MAX_RATE);
+
+	dp->link.max_num_lanes =
+			dp->dpcd[DP_MAX_LANE_COUNT] & DP_MAX_LANE_COUNT_MASK;
+
+	dp->link.enhanced_framing =
+		(dp->dpcd[DP_MAX_LANE_COUNT] & DP_ENHANCED_FRAME_CAP);
+
+	dev_dbg(dp->dev,
+		"DPCD: Rev %x.%x, MaxRate %d kHz, MaxLanes %d, EnhFrame %d\n",
+		dp->link.revision >> 4, dp->link.revision & 0xF,
+		dp->link.max_rate, dp->link.max_num_lanes,
+		dp->link.enhanced_framing);
+
+	return 0;
+}
+
+static int spacemit_dp_phy_set_lanes(struct spacemit_dp_dev *dp,
+				     enum spacemit_dp_lane_count lanes)
+{
+	union phy_configure_opts opts = {
+		.dp = {
+			.lanes = lanes,
+			.set_lanes = true,
+		},
+	};
+	int ret;
+
+	ret = phy_configure(dp->phy, &opts);
+	if (ret)
+		return ret;
+
+	dp->lane_count = lanes;
+	return 0;
+}
+
+static int spacemit_dp_phy_set_rate(struct spacemit_dp_dev *dp,
+				    enum spacemit_dp_link_rate rate)
+{
+	union phy_configure_opts opts = {
+		.dp = {
+			/*
+			 * enum value is in kbit/s; PHY framework wants Mbit/s.
+			 */
+			.link_rate = rate / 1000,
+			.set_rate = true,
+		},
+	};
+	int ret;
+
+	ret = phy_configure(dp->phy, &opts);
+	if (ret)
+		return ret;
+
+	dp->link_rate = rate;
+	return 0;
+}
+
+/*
+ * The link PLL rides phy_configure(); the pixel PLL is a clock the PHY
+ * registered. Link first: that is the order the analog blocks come up in.
+ */
+static int spacemit_dp_set_plls(struct spacemit_dp_dev *dp,
+				enum spacemit_dp_link_rate rate, u32 pclk_khz)
+{
+	int ret;
+
+	ret = spacemit_dp_phy_set_rate(dp, rate);
+	if (ret)
+		return ret;
+
+	return clk_set_rate(dp->pll_clk, (unsigned long)pclk_khz * 1000);
+}
+
+/*
+ * phy_power_on/off() are refcounted, but this driver's bring-up is idempotent
+ * rather than pair-balanced. Mirror the state so the refcount cannot go
+ * negative.
+ */
+static int spacemit_dp_phy_on(struct spacemit_dp_dev *dp)
+{
+	int ret;
+
+	if (dp->phy_powered)
+		return 0;
+
+	ret = phy_power_on(dp->phy);
+	if (ret)
+		return ret;
+
+	dp->phy_powered = true;
+	return 0;
+}
+
+static void spacemit_dp_phy_off(struct spacemit_dp_dev *dp)
+{
+	if (!dp->phy_powered)
+		return;
+
+	phy_power_off(dp->phy);
+	dp->phy_powered = false;
+}
+
+/*
+ * The AUX reset pulse is what leaves the next bring-up a clean state machine.
+ */
+static void spacemit_dp_link_disable(struct spacemit_dp_dev *dp)
+{
+	regmap_write_bits(dp->regs, DP_VIDEO_VSAMPLE_REG,
+			  DP_VIDEO_STREAM_ENABLE,
+			  FIELD_PREP(DP_VIDEO_STREAM_ENABLE, 0));
+	spacemit_dp_phy_off(dp);
+
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_AUX_RESET,
+			  FIELD_PREP(DP_AUX_RESET, 0x1));
+	usleep_range(5000, 8000);
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_AUX_RESET,
+			  FIELD_PREP(DP_AUX_RESET, 0x0));
+	usleep_range(2000, 4000);
+}
+
+static enum drm_connector_status
+spacemit_dp_hw_detect_hpd(struct spacemit_dp_dev *dp)
+{
+	u32 hpd_status;
+	enum drm_connector_status connector_status =
+			connector_status_disconnected;
+
+	regmap_read(dp->regs, DP_HPD_INTERRUPT_STATUS, &hpd_status);
+	hpd_status = FIELD_GET(DP_HPD_IN_STATUS, hpd_status);
+	if (hpd_status)
+		connector_status = connector_status_connected;
+	else
+		connector_status = connector_status_disconnected;
+
+	return connector_status;
+}
+
+/* Returns true when a latched HPD event was pending and got acked. */
+static bool spacemit_dp_hw_clean_hpd(struct spacemit_dp_dev *dp)
+{
+	u32 plug_event, unplug_event;
+
+	regmap_read(dp->regs, DP_HPD_INTERRUPT_STATUS, &plug_event);
+	plug_event = FIELD_GET(DP_HOT_PLUG_EVENT, plug_event);
+	regmap_read(dp->regs, DP_HPD_INTERRUPT_STATUS, &unplug_event);
+	unplug_event = FIELD_GET(DP_HOT_UNPLUG_EVENT, unplug_event);
+
+	if (plug_event)
+		regmap_write(dp->regs, DP_HPD_INTERRUPT_STATUS,
+			     FIELD_PREP(DP_HOT_PLUG_EVENT, 0x1));
+
+	if (unplug_event)
+		regmap_write(dp->regs, DP_HPD_INTERRUPT_STATUS,
+			     FIELD_PREP(DP_HOT_UNPLUG_EVENT, 0x1));
+
+	return plug_event || unplug_event;
+}
+
+static int spacemit_dp_set_training_pattern(struct spacemit_dp_dev *dp,
+					    u8 pattern)
+{
+	u32 tps_sel = 0;
+	u8 dpcd_pattern = pattern;
+	int ret;
+
+	if (pattern != DP_TRAINING_PATTERN_DISABLE)
+		dpcd_pattern |= DP_LINK_SCRAMBLING_DISABLE;
+
+	switch (pattern) {
+	case DP_TRAINING_PATTERN_DISABLE:
+		tps_sel = 0;
+		regmap_write_bits(dp->regs, DP_CORE_CONTROL,
+				  DP_SCRAMBLER_DISABLE,
+				  FIELD_PREP(DP_SCRAMBLER_DISABLE, 0));
+		break;
+	case DP_TRAINING_PATTERN_1:
+		tps_sel = 1;
+		regmap_write_bits(dp->regs, DP_CORE_CONTROL,
+				  DP_SCRAMBLER_DISABLE,
+				  FIELD_PREP(DP_SCRAMBLER_DISABLE, 1));
+		break;
+	case DP_TRAINING_PATTERN_2:
+		tps_sel = 2;
+		regmap_write_bits(dp->regs, DP_CORE_CONTROL,
+				  DP_SCRAMBLER_DISABLE,
+				  FIELD_PREP(DP_SCRAMBLER_DISABLE, 1));
+		break;
+	case DP_TRAINING_PATTERN_3:
+		tps_sel = 3;
+		regmap_write_bits(dp->regs, DP_CORE_CONTROL,
+				  DP_SCRAMBLER_DISABLE,
+				  FIELD_PREP(DP_SCRAMBLER_DISABLE, 1));
+		break;
+	default:
+		dev_err(dp->dev, "Unsupported training pattern: 0x%x\n",
+			pattern);
+		return -EINVAL;
+	}
+
+	regmap_write_bits(dp->regs, DP_PHYIF_CTRL_ADDR, DP_TPS_SEL,
+			  FIELD_PREP(DP_TPS_SEL, tps_sel));
+
+	ret = drm_dp_dpcd_write_byte(&dp->aux, DP_TRAINING_PATTERN_SET,
+				     dpcd_pattern);
+	if (ret < 0) {
+		dev_err(dp->dev, "failed to set DPCD training pattern: %d\n",
+			ret);
+		return ret;
+	}
+
+	return 0;
+}
+
+/*
+ * The adjust request has to reach the source PHY, not just the sink's DPCD:
+ * a transmitter left at its reset default trains any level > 0 with a bad eye.
+ */
+static int spacemit_dp_link_apply_adjust(struct spacemit_dp_dev *dp,
+					 const u8 *link_status,
+				    enum spacemit_dp_lane_count lanes,
+				    u8 *training_set)
+{
+	union phy_configure_opts opts = {
+		.dp = {
+			.lanes = lanes,
+			.set_voltages = true,
+		},
+	};
+	int i, ret;
+
+	for (i = 0; i < lanes; i++) {
+		u8 v = drm_dp_get_adjust_request_voltage(link_status, i);
+		u8 p = drm_dp_get_adjust_request_pre_emphasis(link_status, i);
+		u8 v_bits;
+
+		if (v >= SPACEMIT_DP_SWING_MAX)
+			v = SPACEMIT_DP_SWING_MAX;
+		if (p >= SPACEMIT_DP_PREEMP_MAX)
+			p = SPACEMIT_DP_PREEMP_MAX;
+
+		opts.dp.voltage[i] = v;
+		opts.dp.pre[i] = p;
+
+		v_bits = v;
+		if (v == SPACEMIT_DP_SWING_MAX)
+			v_bits |= DP_TRAIN_MAX_SWING_REACHED;
+		if (p == SPACEMIT_DP_PREEMP_MAX)
+			v_bits |= DP_TRAIN_MAX_PRE_EMPHASIS_REACHED;
+
+		training_set[i] = v_bits | (p << DP_TRAIN_PRE_EMPHASIS_SHIFT);
+	}
+
+	ret = phy_configure(dp->phy, &opts);
+	if (ret)
+		return ret;
+
+	return drm_dp_dpcd_write_data(&dp->aux, DP_TRAINING_LANE0_SET,
+				      training_set, lanes);
+}
+
+static int spacemit_dp_link_train_clock_recovery(struct spacemit_dp_dev *dp,
+						 enum spacemit_dp_link_rate rate,
+					    enum spacemit_dp_lane_count lanes)
+{
+	u8 link_status[DP_LINK_STATUS_SIZE];
+	u8 training_set[4] = {0};
+	int retries = 0;
+	int ret;
+
+	ret = drm_dp_dpcd_write_data(&dp->aux, DP_TRAINING_LANE0_SET,
+				     training_set, lanes);
+	if (ret < 0)
+		return ret;
+
+	ret = spacemit_dp_set_training_pattern(dp, DP_TRAINING_PATTERN_1);
+	if (ret < 0) {
+		spacemit_dp_set_training_pattern(dp,
+						 DP_TRAINING_PATTERN_DISABLE);
+		return ret;
+	}
+
+	while (retries < 8) {
+		drm_dp_link_train_clock_recovery_delay(&dp->aux, dp->dpcd);
+
+		ret = drm_dp_dpcd_read_link_status(&dp->aux, link_status);
+		if (ret < 0) {
+			spacemit_dp_set_training_pattern(dp,
+				DP_TRAINING_PATTERN_DISABLE);
+			return ret;
+		}
+
+		if (drm_dp_clock_recovery_ok(link_status, lanes))
+			return 0;
+
+		ret = spacemit_dp_link_apply_adjust(dp, link_status, lanes,
+						    training_set);
+		if (ret) {
+			spacemit_dp_set_training_pattern(dp,
+				DP_TRAINING_PATTERN_DISABLE);
+			return ret;
+		}
+
+		retries++;
+	}
+
+	dev_err(dp->dev, "link training: clock recovery failed\n");
+	spacemit_dp_set_training_pattern(dp, DP_TRAINING_PATTERN_DISABLE);
+	return -ETIMEDOUT;
+}
+
+static int spacemit_dp_link_train_channel_eq(struct spacemit_dp_dev *dp,
+					     enum spacemit_dp_link_rate rate,
+					enum spacemit_dp_lane_count lanes)
+{
+	u8 link_status[DP_LINK_STATUS_SIZE];
+	u8 training_set[4] = {0};
+	u8 training_pattern = DP_TRAINING_PATTERN_2;
+	int retries = 0;
+	int ret;
+
+	if (dp->dpcd[DP_MAX_LANE_COUNT] & DP_TPS3_SUPPORTED) {
+		training_pattern = DP_TRAINING_PATTERN_3;
+		dev_dbg(dp->dev, "link training: using TPS3\n");
+	} else {
+		dev_dbg(dp->dev, "link training: using TPS2\n");
+	}
+
+	ret = spacemit_dp_set_training_pattern(dp, training_pattern);
+	if (ret < 0) {
+		spacemit_dp_set_training_pattern(dp,
+						 DP_TRAINING_PATTERN_DISABLE);
+		return ret;
+	}
+
+	while (retries < 8) {
+		drm_dp_link_train_channel_eq_delay(&dp->aux, dp->dpcd);
+
+		ret = drm_dp_dpcd_read_link_status(&dp->aux, link_status);
+		if (ret < 0) {
+			spacemit_dp_set_training_pattern(dp,
+				DP_TRAINING_PATTERN_DISABLE);
+			return ret;
+		}
+
+		if (drm_dp_channel_eq_ok(link_status, lanes)) {
+			spacemit_dp_set_training_pattern(dp,
+				DP_TRAINING_PATTERN_DISABLE);
+			return 0;
+		}
+
+		ret = spacemit_dp_link_apply_adjust(dp, link_status, lanes,
+						    training_set);
+		if (ret) {
+			spacemit_dp_set_training_pattern(dp,
+				DP_TRAINING_PATTERN_DISABLE);
+			return ret;
+		}
+
+		retries++;
+	}
+
+	dev_err(dp->dev, "link training: channel EQ failed\n");
+	spacemit_dp_set_training_pattern(dp, DP_TRAINING_PATTERN_DISABLE);
+	return -ETIMEDOUT;
+}
+
+static int spacemit_dp_link_train(struct spacemit_dp_dev *dp, enum spacemit_dp_link_rate rate,
+				  enum spacemit_dp_lane_count lanes)
+{
+	int ret;
+	u8 link_config[1];
+	u8 bw_code;
+
+	/* Map Link Rate Enum to DPCD Bandwidth Code */
+	switch (rate) {
+	case SPACEMIT_DP_LINK_RATE_1_62:
+		bw_code = DP_LINK_BW_1_62;
+		break;
+	case SPACEMIT_DP_LINK_RATE_2_70:
+		bw_code = DP_LINK_BW_2_7;
+		break;
+	case SPACEMIT_DP_LINK_RATE_5_40:
+		bw_code = DP_LINK_BW_5_4;
+		break;
+	case SPACEMIT_DP_LINK_RATE_8_10:
+		bw_code = DP_LINK_BW_8_1;
+		break;
+	default:
+		bw_code = DP_LINK_BW_1_62;
+		break;
+	}
+
+	/*
+	 * A sink may act on the DP_LINK_BW_SET write, so set the lane count
+	 * first.
+	 */
+	link_config[0] = lanes;
+	if (dp->link.enhanced_framing)
+		link_config[0] |= DP_LANE_COUNT_ENHANCED_FRAME_EN;
+
+	ret = drm_dp_dpcd_write_byte(&dp->aux, DP_LANE_COUNT_SET,
+				     link_config[0]);
+	if (ret < 0) {
+		dev_err(dp->dev, "failed to set DPCD lane count\n");
+		return ret;
+	}
+
+	ret = drm_dp_dpcd_write_byte(&dp->aux, DP_LINK_BW_SET, bw_code);
+	if (ret < 0) {
+		dev_err(dp->dev, "failed to set DPCD link rate\n");
+		return ret;
+	}
+
+	ret = spacemit_dp_link_train_clock_recovery(dp, rate, lanes);
+	if (ret)
+		return ret;
+
+	ret = spacemit_dp_link_train_channel_eq(dp, rate, lanes);
+	if (ret)
+		return ret;
+
+	return 0;
+}
+
+static void spacemit_dp_hw_set_msa_and_enable_video(struct spacemit_dp_dev *dp,
+						    const struct drm_display_mode *mode,
+					       enum spacemit_dp_link_rate rate,
+					       enum spacemit_dp_lane_count lanes,
+		u32 color_format)
+{
+	u64 hb_num;
+	u32 link_rate;
+	u32 fp; /* Pixel clock in MHz */
+	u32 bpp, misc0;
+	u32 tu, tu_frac, tu_int, rd_thres;
+	u32 hsync_len;
+
+	/* mode->clock unit is kHz, fp unit is MHz */
+	if (dp->use_ext_pixel_clock)
+		fp = dp->pixel_clock / 1000;
+	else
+		fp = mode->clock / 1000;
+
+	if (fp == 0)
+		fp = 1; /* Prevent division by zero */
+
+	bpp = spacemit_dp_get_bpp(dp, color_format);
+
+	/* Calculate MISC0 */
+	/* bit0: 0 (Sync Clock) */
+	/* bits1-7: Color Format (000=RGB, 001=YCbCr422, 010=YCbCr444) */
+	/* bits5-7: BPC (001=8bpc, 010=10bpc, etc) */
+	switch (color_format) {
+	case SOC_VIDEO_RGB_6BIT:
+		misc0 = 0x00;
+		break;
+	case SOC_VIDEO_RGB_8BIT:
+		misc0 = 0x20;
+		break;
+	case SOC_VIDEO_RGB_10BIT:
+		misc0 = 0x40;
+		break;
+	case SOC_VIDEO_RGB_12BIT:
+		misc0 = 0x60;
+		break;
+	case SOC_VIDEO_RGB_16BIT:
+		misc0 = 0x80;
+		break;
+	case SOC_VIDEO_YUV422_8BIT:
+		misc0 = 0x22;
+		break;
+	case SOC_VIDEO_YUV422_10BIT:
+		misc0 = 0x42;
+		break;
+	case SOC_VIDEO_YUV422_12BIT:
+		misc0 = 0x62;
+		break;
+	case SOC_VIDEO_YUV422_16BIT:
+		misc0 = 0x82;
+		break;
+	case SOC_VIDEO_YUV444_8BIT:
+		misc0 = 0x24;
+		break;
+	case SOC_VIDEO_YUV444_10BIT:
+		misc0 = 0x44;
+		break;
+	case SOC_VIDEO_YUV444_12BIT:
+		misc0 = 0x64;
+		break;
+	case SOC_VIDEO_YUV444_16BIT:
+		misc0 = 0x84;
+		break;
+	default:
+		misc0 = 0x20;
+		break;
+	}
+
+	/*
+	 * HBlank interval = (htotal - hactive) * (LinkSymbolClock / 4) /
+	 * PixelClock, where LinkSymbolClock = LinkRate * 100 (1.62G -> 162MHz).
+	 * rate is in kHz, so link_rate = rate / 10000 gives the 162, 270, 540
+	 * form used below.
+	 */
+	link_rate = rate / 10000;
+
+	/* Multiply before dividing: hb_num = hblank * (link_rate / 4) / fp */
+	hb_num = (u64)(mode->htotal - mode->hdisplay) * link_rate;
+	do_div(hb_num, 4 * fp);
+
+	/* Transfer unit: tu = fp * bpp * 640 / (8 * lanes * link_rate) */
+	{
+		u64 temp_tu = (u64)fp * bpp * 640;
+		u32 den = 8 * lanes * link_rate;
+
+		do_div(temp_tu, den);
+		tu = temp_tu;
+	}
+	tu_frac = tu % 10;
+	tu_int  = tu / 10;
+
+	/* FIFO read threshold. */
+	if (tu_int < 6)
+		rd_thres = 32;
+	else if ((mode->htotal - mode->hdisplay) < 80)
+		rd_thres = 12;
+	else
+		rd_thres = 16;
+
+	dev_dbg(dp->dev,
+		"MSA: %dx%d, Rate:%d kHz, Lanes:%d, BPP:%d, TU:%d.%d\n",
+		mode->hdisplay, mode->vdisplay, rate, lanes, bpp, tu_int,
+		tu_frac);
+
+	regmap_write_bits(dp->regs, DP_VIDEO_VSAMPLE_REG, DP_VIDEO_MAPPING,
+			  FIELD_PREP(DP_VIDEO_MAPPING, color_format));
+
+	/*
+	 * The Saturn TMG raster is always positive-sync (hsp/vsp fixed to 1).
+	 */
+	regmap_write_bits(dp->regs, DP_VINPUT_POLARITY, DP_HSYNC_IN_POLARITY,
+			  FIELD_PREP(DP_HSYNC_IN_POLARITY, 1));
+	regmap_write_bits(dp->regs, DP_VINPUT_POLARITY, DP_VSYNC_IN_POLARITY,
+			  FIELD_PREP(DP_VSYNC_IN_POLARITY, 1));
+
+	/* Basic timing */
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG1, DP_HACTIVE,
+			  FIELD_PREP(DP_HACTIVE, mode->hdisplay));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG2, DP_VACTIVE,
+			  FIELD_PREP(DP_VACTIVE, mode->vdisplay));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG1, DP_HBLANK,
+			  FIELD_PREP(DP_HBLANK, mode->htotal - mode->hdisplay));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG2, DP_VBLANK,
+			  FIELD_PREP(DP_VBLANK, mode->vtotal - mode->vdisplay));
+
+	u32 hstart = mode->htotal - mode->hsync_end +
+			(mode->hsync_end - mode->hsync_start);
+	u32 vstart = mode->vtotal - mode->vsync_end +
+			(mode->vsync_end - mode->vsync_start);
+
+	regmap_write_bits(dp->regs, DP_VIDEO_MSA1, DP_HSTART,
+			  FIELD_PREP(DP_HSTART, hstart));
+	regmap_write_bits(dp->regs, DP_VIDEO_MSA1, DP_VSTART,
+			  FIELD_PREP(DP_VSTART, vstart));
+
+	hsync_len = mode->hsync_end - mode->hsync_start;
+
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG3, DP_H_SYNC_WIDTH,
+			  FIELD_PREP(DP_H_SYNC_WIDTH, hsync_len));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG4, DP_V_SYNC_WIDTH,
+			  FIELD_PREP(DP_V_SYNC_WIDTH, mode->vsync_end - mode->vsync_start));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG3, DP_H_FRONT_PORCH,
+			  FIELD_PREP(DP_H_FRONT_PORCH, mode->hsync_start - mode->hdisplay));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG4, DP_V_FRONT_PORCH,
+			  FIELD_PREP(DP_V_FRONT_PORCH, mode->vsync_start - mode->vdisplay));
+
+	/* MSA and MISC */
+	regmap_write_bits(dp->regs, DP_VIDEO_MSA2, DP_MISC0,
+			  FIELD_PREP(DP_MISC0, misc0));
+	regmap_write_bits(dp->regs, DP_VIDEO_MSA3, DP_MISC1,
+			  FIELD_PREP(DP_MISC1, 0));
+	regmap_write_bits(dp->regs, DP_VIDEO_MSA3, DP_NVID,
+			  FIELD_PREP(DP_NVID, 0));
+
+	/* Link layer parameters */
+	regmap_write_bits(dp->regs, DP_VIDEO_HBLANK_INTERVAL,
+			  DP_HBLANK_INTERVAL,
+			  FIELD_PREP(DP_HBLANK_INTERVAL, (u32)hb_num));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG5, DP_AVERAGE_BYTES_PER_TU,
+			  FIELD_PREP(DP_AVERAGE_BYTES_PER_TU, tu_int));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG5,
+			  DP_AVERAGE_BYTES_PER_TU_FRAC,
+			  FIELD_PREP(DP_AVERAGE_BYTES_PER_TU_FRAC, tu_frac));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG5, DP_INIT_THRESHOLD,
+			  FIELD_PREP(DP_INIT_THRESHOLD, rd_thres));
+
+	regmap_write_bits(dp->regs, DP_PHYIF_CTRL_ADDR, DP_PHY_SSC_DIS,
+			  FIELD_PREP(DP_PHY_SSC_DIS, 1));
+	regmap_write_bits(dp->regs, DP_CLK_INV_MUX, DP_REG_VID_CLK_SEL,
+			  FIELD_PREP(DP_REG_VID_CLK_SEL, 0));
+	regmap_write_bits(dp->regs, DP_VIDEO_COLORBAR_CFG, DP_VID_BIST_EN,
+			  FIELD_PREP(DP_VID_BIST_EN, 0));
+
+	dev_dbg(dp->dev, "enabling video stream\n");
+	regmap_write_bits(dp->regs, DP_VIDEO_VSAMPLE_REG,
+			  DP_VIDEO_STREAM_ENABLE,
+			  FIELD_PREP(DP_VIDEO_STREAM_ENABLE, 1));
+}
+
+static void spacemit_dp_hw_disable(struct spacemit_dp_dev *dp)
+{
+	dev_dbg(dp->dev, "disabling video and PHY\n");
+
+	regmap_write_bits(dp->regs, DP_VIDEO_VSAMPLE_REG,
+			  DP_VIDEO_STREAM_ENABLE,
+			  FIELD_PREP(DP_VIDEO_STREAM_ENABLE, 0));
+
+	regmap_write_bits(dp->regs, DP_PHYIF_CTRL_ADDR, DP_XMIT_ENABLE,
+			  FIELD_PREP(DP_XMIT_ENABLE, 0));
+
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_LDO_D0,
+			  FIELD_PREP(DP_ANA_TX_EN_LDO_D0, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_LDO_D1,
+			  FIELD_PREP(DP_ANA_TX_EN_LDO_D1, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_LDO_D2,
+			  FIELD_PREP(DP_ANA_TX_EN_LDO_D2, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_LDO_D3,
+			  FIELD_PREP(DP_ANA_TX_EN_LDO_D3, 0));
+
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_DRV_D0,
+			  FIELD_PREP(DP_ANA_TX_EN_DRV_D0, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_DRV_D1,
+			  FIELD_PREP(DP_ANA_TX_EN_DRV_D1, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_DRV_D2,
+			  FIELD_PREP(DP_ANA_TX_EN_DRV_D2, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_DRV_D3,
+			  FIELD_PREP(DP_ANA_TX_EN_DRV_D3, 0));
+
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_EN_P2S_D0,
+			  FIELD_PREP(DP_ANA_TX_EN_P2S_D0, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_EN_P2S_D1,
+			  FIELD_PREP(DP_ANA_TX_EN_P2S_D1, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_EN_P2S_D2,
+			  FIELD_PREP(DP_ANA_TX_EN_P2S_D2, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_EN_P2S_D3,
+			  FIELD_PREP(DP_ANA_TX_EN_P2S_D3, 0));
+
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL6, DP_ANA_BG_EN,
+			  FIELD_PREP(DP_ANA_BG_EN, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_BG_EN_RCAL,
+			  FIELD_PREP(DP_ANA_BG_EN_RCAL, 0));
+
+	/* Both PLLs. */
+	regmap_write_bits(dp->regs, DP_MPLL_CTRL0, DP_ANA_MPLL_PD,
+			  FIELD_PREP(DP_ANA_MPLL_PD, 1));
+	regmap_write_bits(dp->regs, DP_PREPLL_CTRL0, DP_ANA_PREPLL_PD,
+			  FIELD_PREP(DP_ANA_PREPLL_PD, 1));
+}
+
+/* Calculate required bandwidth in kbps (Pixel Clock * Bits Per Pixel) */
+static u32 spacemit_dp_calc_required_bw(const struct drm_display_mode *mode,
+					int bpp)
+{
+	return mode->clock * bpp;
+}
+
+/*
+ * Calculate available link capacity in kbps (taking 8b/10b overhead into
+ * account)
+ */
+static u32 spacemit_dp_calc_link_capacity(enum spacemit_dp_link_rate rate,
+					  enum spacemit_dp_lane_count lanes)
+{
+	u32 rate_khz = rate;
+	u32 lane_count = lanes;
+
+	/* Capacity = Rate(kHz) * Lanes * 0.8 */
+	return (rate_khz * lane_count * 8) / 10;
+}
+
+static enum drm_connector_status
+spacemit_dp_bridge_detect(struct drm_bridge *bridge, struct drm_connector *connector)
+{
+	struct spacemit_dp_dev *dp = container_of(bridge,
+						  struct spacemit_dp_dev,
+						  bridge);
+	enum drm_connector_status status;
+
+	/*
+	 * The eDP panel is fixed, and DP_HPD_IN_STATUS does not read
+	 * meaningfully on that path. Report CONNECTED only once panel-edp has
+	 * joined the chain.
+	 */
+	if (dp->edp_mode) {
+		if (!dp->next_bridge)
+			return connector_status_disconnected;
+		return connector_status_connected;
+	}
+
+	/*
+	 * The HPD interrupt is gated by the DP pixel clock, which is off
+	 * exactly when a plug must be caught. Read the level register live
+	 * instead.
+	 */
+	guard(mutex)(&dp->mode_lock);
+	status = spacemit_dp_hw_detect_hpd(dp);
+
+	/*
+	 * The bind-time cap read only covers a sink that was already there.
+	 * Without a re-read here, link.max_rate stays 0 and every link config
+	 * is filtered out.
+	 */
+	if (status == connector_status_connected &&
+	    dp->connector_status != connector_status_connected)
+		spacemit_dp_hw_read_sink_caps(dp);
+
+	dp->connector_status = status;
+
+	return status;
+}
+
+static int spacemit_dp_conn_get_edid_block(void *data, u8 *buf,
+					   unsigned int block, size_t len)
+{
+	struct spacemit_dp_dev *dp = data;
+	u8 offset = (block & 1) * EDID_LENGTH;
+	u8 segment = block >> 1;
+	struct i2c_msg msgs[] = {
+		{
+			.addr = SPACEMIT_DP_DDC_SEGMENT_ADDR,
+			.len = 1,
+			.buf = &segment,
+		}, {
+			.addr = DDC_ADDR,
+			.len = 1,
+			.buf = &offset,
+		}, {
+			.addr = DDC_ADDR,
+			.flags = I2C_M_RD,
+			.len = len,
+			.buf = buf,
+		},
+	};
+	/* Segment 0 needs no segment-pointer write. */
+	struct i2c_msg *first = segment ? &msgs[0] : &msgs[1];
+	int num = segment ? 3 : 2;
+	int ret, retry;
+
+	/*
+	 * One i2c_transfer per block: the DP I2C-over-AUX core splits it
+	 * into MOT-chained AUX transactions, so the sink sees a repeated
+	 * start and the segment pointer survives until the read ends.
+	 */
+	for (retry = 0; retry < 3; retry++) {
+		ret = i2c_transfer(&dp->aux.ddc, first, num);
+		if (ret == num)
+			return 0;
+	}
+
+	dev_err(dp->dev, "EDID read failed, block %u: %d\n", block, ret);
+	return ret < 0 ? ret : -EIO;
+}
+
+static const struct drm_edid *
+spacemit_dp_bridge_edid_read(struct drm_bridge *bridge, struct drm_connector *connector)
+{
+	struct spacemit_dp_dev *dp = container_of(bridge,
+						  struct spacemit_dp_dev,
+						  bridge);
+	const struct drm_edid *edid = NULL;
+	int retry;
+
+	scoped_guard(mutex, &dp->mode_lock) {
+		for (retry = 0; retry < SPACEMIT_DP_SINK_READY_RETRIES; retry++) {
+			if (retry) {
+				spacemit_dp_aux_hw_reset(dp);
+				msleep(SPACEMIT_DP_SINK_READY_DELAY_MS);
+			}
+
+			edid = drm_edid_read_custom(connector,
+				spacemit_dp_conn_get_edid_block, dp);
+			if (edid)
+				break;
+		}
+	}
+
+	if (!edid)
+		dev_err(dp->dev, "failed to read EDID\n");
+
+	return edid;
+}
+
+static enum drm_mode_status
+spacemit_dp_bridge_mode_valid(struct drm_bridge *bridge,
+			      const struct drm_display_info *info,
+			 const struct drm_display_mode *mode)
+{
+	struct spacemit_dp_dev *dp = container_of(bridge,
+						  struct spacemit_dp_dev,
+						  bridge);
+	unsigned int max_link_rate, max_lanes;
+	u64 max_bw, req_bw;
+	const unsigned int bpp = 24;
+
+	/*
+	 * drm_dp_max_link_rate() is in 10 kHz units; clamp to what the PHY can
+	 * do.
+	 */
+	max_link_rate = min_t(unsigned int, drm_dp_max_link_rate(dp->dpcd),
+			      SPACEMIT_DP_SOURCE_MAX_RATE / 10);
+	max_lanes = drm_dp_max_lane_count(dp->dpcd);
+	if (!max_link_rate || !max_lanes) {
+		/* Sink not enumerated yet -- accept and re-check at modeset. */
+		return MODE_OK;
+	}
+
+	/*
+	 * max_link_rate is per lane in 10 kHz units. The x10 to reach kHz and
+	 * the 8b/10b coding overhead cancel down to a plain "* 8".
+	 */
+	max_bw = (u64)max_link_rate * max_lanes * 8;
+
+	/* Required bandwidth: pixel clock (kHz) * bits per pixel. */
+	req_bw = (u64)mode->clock * bpp;
+
+	if (req_bw > max_bw) {
+		drm_dbg_kms(bridge->dev,
+			    "rejecting %dx%d@%d: %llu > %llu kbit/s (%u kHz x%u lanes)\n",
+			    mode->hdisplay, mode->vdisplay, drm_mode_vrefresh(mode),
+			    req_bw, max_bw, max_link_rate * 10, max_lanes);
+		return MODE_CLOCK_HIGH;
+	}
+
+	return MODE_OK;
+}
+
+static const struct drm_encoder_funcs spacemit_dp_encoder_funcs = {
+	.destroy = drm_encoder_cleanup,
+};
+
+static void spacemit_dp_bridge_atomic_enable(struct drm_bridge *bridge,
+					     struct drm_atomic_commit *state)
+{
+	struct spacemit_dp_dev *dp = container_of(bridge,
+						  struct spacemit_dp_dev,
+						  bridge);
+	struct drm_bridge_state *new_bridge_state =
+		drm_atomic_get_new_bridge_state(state, bridge);
+	struct spacemit_dp_bridge_state *st =
+			to_spacemit_dp_bridge_state(new_bridge_state);
+	struct drm_connector_state *conn_state =
+		drm_atomic_get_new_connector_state(state, dp->connector);
+	struct drm_crtc_state *crtc_state =
+		drm_atomic_get_new_crtc_state(state, conn_state->crtc);
+	struct drm_display_mode *adjusted_mode = &crtc_state->adjusted_mode;
+	const struct spacemit_dp_link_config *cfg;
+	bool trained;
+	u64 clk_val;
+	u64 set_clk_val;
+	u32 req_bw;
+	u32 hpd;
+	int start;
+	int bpp;
+	int i;
+
+	guard(mutex)(&dp->mode_lock);
+	drm_mode_copy(&dp->mode, adjusted_mode);
+
+	/* Enable-time retry for the caps .detect() could not read. */
+	if (!dp->link.max_rate || !dp->link.max_num_lanes)
+		spacemit_dp_hw_read_sink_caps(dp);
+
+	if (dp->use_ext_pixel_clock && dp->pxclk) {
+		set_clk_val = adjusted_mode->clock * 1000;
+		if (set_clk_val) {
+			set_clk_val = clk_round_rate(dp->pxclk, set_clk_val);
+			clk_val = clk_get_rate(dp->pxclk);
+			if (clk_val != set_clk_val) {
+				clk_set_rate(dp->pxclk, set_clk_val);
+				dev_dbg(dp->dev, "set dp pxclk=%lld\n",
+					set_clk_val);
+			}
+		}
+		clk_val = clk_get_rate(dp->pxclk);
+		dp->pixel_clock = clk_val / 1000;
+	}
+
+	bpp = spacemit_dp_get_bpp(dp, st->color_format);
+	req_bw = spacemit_dp_calc_required_bw(adjusted_mode, bpp);
+
+	/*
+	 * Start where atomic_check() left off, but keep the upgrade walk as
+	 * fallback.
+	 */
+	start = st->link_cfg_idx >= 0 ? st->link_cfg_idx : 0;
+
+	for (trained = false, i = start;
+	     i < ARRAY_SIZE(spacemit_dp_link_priority_table); i++) {
+		u32 capacity;
+
+		cfg = &spacemit_dp_link_priority_table[i];
+
+		/* Source and sink limits */
+		if (cfg->rate > dp->link.max_rate || cfg->lanes > dp->link.max_num_lanes)
+			continue;
+
+		/* Bandwidth requirement */
+		capacity = spacemit_dp_calc_link_capacity(cfg->rate,
+							  cfg->lanes);
+		if (capacity < req_bw)
+			continue;
+
+		dev_dbg(dp->dev, "trying link config: rate %d lanes %d (capacity %d, required %d)\n",
+			cfg->rate, cfg->lanes, capacity, req_bw);
+
+		spacemit_dp_link_disable(dp);
+
+		/* Apply Hardware Settings */
+		if (spacemit_dp_set_plls(dp, cfg->rate,
+					 dp->use_ext_pixel_clock ? dp->pixel_clock
+								 : adjusted_mode->clock))
+			continue;
+
+		if (spacemit_dp_phy_set_lanes(dp, cfg->lanes))
+			continue;
+
+		if (spacemit_dp_phy_on(dp))
+			continue;
+
+		/*
+		 * Debounce: the transmitter was power-cycled just now and a
+		 * sink may legitimately drop HPD across a link-down. eDP is
+		 * hardwired and noisy.
+		 */
+		if (!dp->edp_mode &&
+		    regmap_read_poll_timeout(dp->regs, DP_HPD_INTERRUPT_STATUS, hpd,
+					     hpd & DP_HPD_IN_STATUS,
+					     SPACEMIT_DP_HPD_POLL_US,
+					     SPACEMIT_DP_HPD_DEBOUNCE_US)) {
+			dev_warn(dp->dev,
+				 "sink is not asserting HPD; not training\n");
+			break;
+		}
+
+		if (dp->edp_mode) {
+			regmap_write_bits(dp->regs, DP_CORE_CONTROL,
+					  DP_ENABLE_EDP,
+					  FIELD_PREP(DP_ENABLE_EDP, 0x1));
+			regmap_write_bits(dp->regs, DP_VIDEO_VSAMPLE_REG,
+					  DP_STREAM_ENC_EN,
+					  FIELD_PREP(DP_STREAM_ENC_EN, 0x1));
+			update_edp_config(dp, true);
+		}
+
+		/* Execute Link Training */
+		if (spacemit_dp_link_train(dp, cfg->rate, cfg->lanes) == 0) {
+			dev_dbg(dp->dev,
+				"link training succeeded: rate %d lanes %d\n",
+				cfg->rate, cfg->lanes);
+			trained = true;
+			break;
+		}
+
+		dev_warn(dp->dev, "link training failed: rate %d lanes %d, trying next config\n",
+			 cfg->rate, cfg->lanes);
+	}
+
+	/*
+	 * The DPU's pixel clock comes from the PHY PLL, so scanning out into an
+	 * untrained link never reaches CFG_RDY and wedges the CRTC on
+	 * flip_done.
+	 */
+	if (!trained) {
+		dev_err(dp->dev,
+			"no usable link config; leaving video disabled (%u kHz x%u lanes)\n",
+			dp->link.max_rate, dp->link.max_num_lanes);
+		/*
+		 * The caps may be from a sink that has since been swapped out.
+		 */
+		dp->link.max_rate = 0;
+		dp->link.max_num_lanes = 0;
+
+		/*
+		 * Deferred: we are inside the commit and hold the modeset
+		 * locks.
+		 */
+		schedule_work(&dp->modeset_retry_work);
+		return;
+	}
+
+	spacemit_dp_hw_set_msa_and_enable_video(dp, adjusted_mode,
+						dp->link_rate, dp->lane_count,
+						st->color_format);
+}
+
+static void spacemit_dp_bridge_atomic_disable(struct drm_bridge *bridge,
+					      struct drm_atomic_commit *state)
+{
+	struct spacemit_dp_dev *dp = container_of(bridge,
+						  struct spacemit_dp_dev,
+						  bridge);
+
+	guard(mutex)(&dp->mode_lock);
+
+	spacemit_dp_link_disable(dp);
+	memset(&dp->mode, 0, sizeof(dp->mode));
+}
+
+static int spacemit_dp_bridge_atomic_check(struct drm_bridge *bridge,
+					   struct drm_bridge_state *bridge_state,
+				      struct drm_crtc_state *crtc_state,
+				      struct drm_connector_state *conn_state)
+{
+	struct spacemit_dp_dev *dp = container_of(bridge,
+						  struct spacemit_dp_dev,
+						  bridge);
+	struct spacemit_dp_bridge_state *st =
+			to_spacemit_dp_bridge_state(bridge_state);
+	const struct drm_display_mode *mode = &crtc_state->adjusted_mode;
+	int bpp;
+	u32 req_bw;
+	int i;
+
+	/* A single output bus format, see ->atomic_get_output_bus_fmts(). */
+	st->color_format = SOC_VIDEO_RGB_8BIT;
+	bpp = spacemit_dp_get_bpp(dp, st->color_format);
+	req_bw = spacemit_dp_calc_required_bw(mode, bpp);
+
+	/* No caps yet: leave idx unset and let ->atomic_enable() retry. */
+	if (!dp->link.max_rate || !dp->link.max_num_lanes) {
+		st->link_cfg_idx = -1;
+		return 0;
+	}
+
+	for (i = 0; i < ARRAY_SIZE(spacemit_dp_link_priority_table); i++) {
+		const struct spacemit_dp_link_config *cfg =
+				&spacemit_dp_link_priority_table[i];
+
+		if (cfg->rate > dp->link.max_rate ||
+		    cfg->lanes > dp->link.max_num_lanes)
+			continue;
+		if (spacemit_dp_calc_link_capacity(cfg->rate, cfg->lanes) < req_bw)
+			continue;
+
+		st->link_cfg_idx = i;
+		return 0;
+	}
+
+	drm_dbg_kms(bridge->dev,
+		    "no DP link cfg fits %dx%d@%d (need %u kbit/s; sink %u kHz x %u lanes)\n",
+		    mode->hdisplay, mode->vdisplay, drm_mode_vrefresh(mode),
+		    req_bw, dp->link.max_rate * 10, dp->link.max_num_lanes);
+	return -EINVAL;
+}
+
+static struct drm_bridge_state *
+spacemit_dp_bridge_atomic_duplicate_state(struct drm_bridge *bridge)
+{
+	struct spacemit_dp_bridge_state *cur =
+		to_spacemit_dp_bridge_state(drm_priv_to_bridge_state(bridge->base.state));
+	struct spacemit_dp_bridge_state *new;
+
+	new = kzalloc_obj(*new);
+	if (!new)
+		return NULL;
+
+	__drm_atomic_helper_bridge_duplicate_state(bridge, &new->base);
+	new->link_cfg_idx = cur->link_cfg_idx;
+	new->color_format = cur->color_format;
+	return &new->base;
+}
+
+static void
+spacemit_dp_bridge_atomic_destroy_state(struct drm_bridge *bridge,
+					struct drm_bridge_state *state)
+{
+	kfree(to_spacemit_dp_bridge_state(state));
+}
+
+static u32 *spacemit_dp_bridge_atomic_get_output_bus_fmts(struct drm_bridge *bridge,
+							  struct drm_bridge_state *bridge_state,
+				struct drm_crtc_state *crtc_state,
+				struct drm_connector_state *conn_state,
+				unsigned int *num_output_fmts)
+{
+	u32 *output_fmts;
+
+	output_fmts = kmalloc_obj(*output_fmts, GFP_KERNEL);
+	if (!output_fmts) {
+		*num_output_fmts = 0;
+		return NULL;
+	}
+
+	output_fmts[0] = MEDIA_BUS_FMT_RGB888_1X24;
+	*num_output_fmts = 1;
+	return output_fmts;
+}
+
+static struct drm_bridge_state *
+spacemit_dp_bridge_atomic_create_state(struct drm_bridge *bridge)
+{
+	struct spacemit_dp_bridge_state *new;
+
+	new = kzalloc_obj(*new);
+	if (!new)
+		return ERR_PTR(-ENOMEM);
+
+	__drm_atomic_helper_bridge_state_init(&new->base, bridge);
+	new->link_cfg_idx = -1;
+	new->color_format = SOC_VIDEO_RGB_8BIT;
+	return &new->base;
+}
+
+static int spacemit_dp_bridge_attach(struct drm_bridge *bridge,
+				     struct drm_encoder *encoder,
+				enum drm_bridge_attach_flags flags)
+{
+	struct spacemit_dp_dev *dp = container_of(bridge,
+						  struct spacemit_dp_dev,
+						  bridge);
+
+	if (!dp->next_bridge)
+		return 0;
+
+	return drm_bridge_attach(encoder, dp->next_bridge, bridge, flags);
+}
+
+/*
+ * AUX-bus done-probing callback. panel-edp may probe inline (before .attach) or
+ * off the deferred-probe queue (after it), so handle both orders.
+ */
+static int spacemit_dp_edp_link_panel(struct drm_dp_aux *aux)
+{
+	struct spacemit_dp_dev *dp = container_of(aux, struct spacemit_dp_dev,
+						  aux);
+	struct drm_bridge *next;
+	int ret;
+
+	next = devm_drm_of_get_bridge(dp->dev, dp->dev->of_node, 1, 0);
+	if (IS_ERR(next))
+		return PTR_ERR(next);
+
+	dp->next_bridge = next;
+
+	/*
+	 * A non-NULL bridge.encoder means .attach already ran: late-chain
+	 * panel-edp.
+	 */
+	if (dp->bridge.encoder) {
+		ret = drm_bridge_attach(dp->bridge.encoder, next,
+					&dp->bridge,
+					DRM_BRIDGE_ATTACH_NO_CONNECTOR);
+		if (ret)
+			return ret;
+
+		/*
+		 * eDP HPD-IN is unreliable, so bind may have skipped the cap
+		 * read and left link.max_rate at 0. The AUX bus just worked;
+		 * read them here.
+		 */
+		if (dp->edp_mode) {
+			guard(mutex)(&dp->mode_lock);
+			spacemit_dp_hw_read_sink_caps(dp);
+		}
+
+		drm_bridge_hpd_notify(&dp->bridge,
+				      spacemit_dp_bridge_detect(&dp->bridge,
+								dp->connector));
+	}
+
+	return 0;
+}
+
+static const struct drm_bridge_funcs spacemit_dp_bridge_funcs = {
+	.attach				= spacemit_dp_bridge_attach,
+	.atomic_enable			= spacemit_dp_bridge_atomic_enable,
+	.atomic_disable			= spacemit_dp_bridge_atomic_disable,
+	.atomic_check			= spacemit_dp_bridge_atomic_check,
+	.atomic_duplicate_state		= spacemit_dp_bridge_atomic_duplicate_state,
+	.atomic_destroy_state		= spacemit_dp_bridge_atomic_destroy_state,
+	.atomic_create_state		= spacemit_dp_bridge_atomic_create_state,
+	.atomic_get_output_bus_fmts	= spacemit_dp_bridge_atomic_get_output_bus_fmts,
+	.mode_valid			= spacemit_dp_bridge_mode_valid,
+	.detect				= spacemit_dp_bridge_detect,
+	.edid_read			= spacemit_dp_bridge_edid_read,
+};
+
+static irqreturn_t spacemit_dp_irq_handler(int irq, void *data)
+{
+	struct spacemit_dp_dev *dp = data;
+	irqreturn_t ret = IRQ_NONE;
+	int idx;
+
+	if (!drm_dev_enter(dp->drm, &idx))
+		return IRQ_NONE;
+
+	/*
+	 * eDP .detect() ignores the HPD level (and never records it), so a
+	 * level flip would wake the thread forever; ack and ignore.
+	 */
+	if (dp->edp_mode) {
+		spacemit_dp_hw_clean_hpd(dp);
+		drm_dev_exit(idx);
+		return IRQ_HANDLED;
+	}
+
+	/*
+	 * Only compare here: the authoritative connector_status update
+	 * happens in the thread under mode_lock, via .detect().
+	 */
+	if (spacemit_dp_hw_detect_hpd(dp) != READ_ONCE(dp->connector_status))
+		ret = IRQ_WAKE_THREAD; /* Call hotplug_event */
+	if (spacemit_dp_hw_clean_hpd(dp) && ret == IRQ_NONE)
+		ret = IRQ_HANDLED;
+
+	drm_dev_exit(idx);
+	return ret;
+}
+
+static irqreturn_t spacemit_dp_hotplug_event_handler(int irq, void *data)
+{
+	struct spacemit_dp_dev *dp = data;
+	enum drm_connector_status status;
+	int idx;
+
+	if (!drm_dev_enter(dp->drm, &idx))
+		return IRQ_HANDLED;
+
+	/*
+	 * .detect() owns mode_lock and re-reads the sink caps on a fresh plug
+	 */
+	status = spacemit_dp_bridge_detect(&dp->bridge, dp->connector);
+	drm_bridge_hpd_notify(&dp->bridge, status);
+
+	drm_dev_exit(idx);
+	return IRQ_HANDLED;
+}
+
+static int spacemit_dp_resource_init(struct spacemit_dp_dev *dp,
+				     struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	void __iomem *base;
+
+	base = devm_platform_ioremap_resource(pdev, 0);
+	if (IS_ERR(base))
+		return dev_err_probe(dev, PTR_ERR(base),
+				     "failed to map registers\n");
+	dp->regs = devm_regmap_init_mmio(dev, base, &spacemit_dp_regmap_config);
+	if (IS_ERR(dp->regs))
+		return dev_err_probe(dev, PTR_ERR(dp->regs),
+				     "failed to init DP regmap\n");
+
+	/*
+	 * The pixel clock comes from the PHY PLL, so the controller never sets
+	 * it.
+	 */
+	dp->use_ext_pixel_clock = false;
+
+	dp->irq = platform_get_irq(pdev, 0);
+	if (dp->irq < 0) {
+		dev_err(dev, "failed to get IRQ\n");
+		return dp->irq;
+	}
+
+	return 0;
+}
+
+static int spacemit_dp_dev_init(struct spacemit_dp_dev *dp)
+{
+	int ret;
+	u32 m_isel = 0x5, m_mainsel = 0x19;
+	u32 m_pre = 0x0, m_post = 0x2;
+	u32 tx_mode = 0x1, tx_pre = 0x0;
+	u32 clk_div = 24 * 1000 / 100;
+
+	regmap_write_bits(dp->regs, DP_PHYIF_CTRL_ADDR, DP_XMIT_ENABLE,
+			  FIELD_PREP(DP_XMIT_ENABLE, 0));
+	usleep_range(2000, 4000);
+
+	regmap_write_bits(dp->regs, DP_MPLL_CTRL0, DP_ANA_MPLL_PD,
+			  FIELD_PREP(DP_ANA_MPLL_PD, 1));
+	regmap_write_bits(dp->regs, DP_PREPLL_CTRL0, DP_ANA_PREPLL_PD,
+			  FIELD_PREP(DP_ANA_PREPLL_PD, 1));
+	usleep_range(2000, 4000);
+
+	/* Reset Controller and PHY */
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_CONTROLLER_RESET,
+			  FIELD_PREP(DP_CONTROLLER_RESET, 0x1));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_PHY_RESET,
+			  FIELD_PREP(DP_PHY_RESET, 0x1));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_HDCP_RESET,
+			  FIELD_PREP(DP_HDCP_RESET, 0x1));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_AUX_RESET,
+			  FIELD_PREP(DP_AUX_RESET, 0x1));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_VIDEO_RESET,
+			  FIELD_PREP(DP_VIDEO_RESET, 0x1));
+	usleep_range(5000, 8000);
+
+	/* Release the resets */
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_CONTROLLER_RESET,
+			  FIELD_PREP(DP_CONTROLLER_RESET, 0x0));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_PHY_RESET,
+			  FIELD_PREP(DP_PHY_RESET, 0x0));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_HDCP_RESET,
+			  FIELD_PREP(DP_HDCP_RESET, 0x0));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_AUX_RESET,
+			  FIELD_PREP(DP_AUX_RESET, 0x0));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_VIDEO_RESET,
+			  FIELD_PREP(DP_VIDEO_RESET, 0x0));
+	usleep_range(2000, 4000);
+
+	regmap_write(dp->regs, DP_GENERAL_INTERRUPT,
+		     DP_AUX_REPLY_EVENT_INT_STA);
+
+	regmap_write_bits(dp->regs, DP_CORE_CONTROL,
+			  DP_DEFAULT_FAST_LINK_TRAIN_EN,
+			  FIELD_PREP(DP_DEFAULT_FAST_LINK_TRAIN_EN, 0x0));
+	regmap_write_bits(dp->regs, DP_CORE_CONTROL, DP_SCRAMBLER_DISABLE,
+			  FIELD_PREP(DP_SCRAMBLER_DISABLE, 0x0));
+	regmap_write_bits(dp->regs, DP_CORE_CONTROL, DP_SCALE_DOWN_MODE,
+			  FIELD_PREP(DP_SCALE_DOWN_MODE, 0x0));
+	regmap_write_bits(dp->regs, DP_PHYIF_CTRL_ADDR, DP_XMIT_ENABLE,
+			  FIELD_PREP(DP_XMIT_ENABLE, 0));
+
+	regmap_write_bits(dp->regs, DP_VIDEO_VSAMPLE_REG,
+			  DP_VIDEO_STREAM_ENABLE,
+			  FIELD_PREP(DP_VIDEO_STREAM_ENABLE, 0));
+
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_AUX_REPLY_EVENT_INT_STA_MSK,
+			  FIELD_PREP(DP_AUX_REPLY_EVENT_INT_STA_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_HDCP_INT_STA_MSK,
+			  FIELD_PREP(DP_HDCP_INT_STA_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_ILLEGAL_AUX_CMD_INT_STA_MSK,
+			  FIELD_PREP(DP_ILLEGAL_AUX_CMD_INT_STA_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_TYPE_C_EVENT_MSK,
+			  FIELD_PREP(DP_TYPE_C_EVENT_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK, DP_DSC_EVENT_MSK,
+			  FIELD_PREP(DP_DSC_EVENT_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_SDP_INT_STA_S3_MSK,
+			  FIELD_PREP(DP_SDP_INT_STA_S3_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_SDP_INT_STA_S2_MSK,
+			  FIELD_PREP(DP_SDP_INT_STA_S2_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_SDP_INT_STA_S1_MSK,
+			  FIELD_PREP(DP_SDP_INT_STA_S1_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_SDP_INT_STA_S0_MSK,
+			  FIELD_PREP(DP_SDP_INT_STA_S0_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+		DP_VIDEO_FIFO_OVERFLOW_INT_STA_S3_MSK,
+		FIELD_PREP(DP_VIDEO_FIFO_OVERFLOW_INT_STA_S3_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+		DP_VIDEO_FIFO_OVERFLOW_INT_STA_S2_MSK,
+		FIELD_PREP(DP_VIDEO_FIFO_OVERFLOW_INT_STA_S2_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+		DP_VIDEO_FIFO_OVERFLOW_INT_STA_S1_MSK,
+		FIELD_PREP(DP_VIDEO_FIFO_OVERFLOW_INT_STA_S1_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+		DP_VIDEO_FIFO_OVERFLOW_INT_STA_S0_MSK,
+		FIELD_PREP(DP_VIDEO_FIFO_OVERFLOW_INT_STA_S0_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_HPD_INTERRUPT_ENABLE,
+			  DP_SINK_IRQ_EVENT_MSK,
+			  FIELD_PREP(DP_SINK_IRQ_EVENT_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_HPD_INT_STA_MSK,
+			  FIELD_PREP(DP_HPD_INT_STA_MSK, 0x1));
+	regmap_write_bits(dp->regs, DP_HPD_INTERRUPT_ENABLE,
+			  DP_HOT_PLUG_EVENT_MSK,
+			  FIELD_PREP(DP_HOT_PLUG_EVENT_MSK, 0x1));
+	regmap_write_bits(dp->regs, DP_HPD_INTERRUPT_ENABLE,
+			  DP_HOT_UNPLUG_EVENT_MSK,
+			  FIELD_PREP(DP_HOT_UNPLUG_EVENT_MSK, 0x1));
+	regmap_write_bits(dp->regs, DP_HPD_INTERRUPT_ENABLE,
+			  DP_SINK_UNPLUG_ERROR_EVENT_MSK,
+			  FIELD_PREP(DP_SINK_UNPLUG_ERROR_EVENT_MSK, 0x0));
+	usleep_range(2000, 4000);
+
+	regmap_write_bits(dp->regs, DP_PHYIF_CTRL_ADDR, DP_PHY_BUSY_BYP,
+			  FIELD_PREP(DP_PHY_BUSY_BYP, 0x1));
+
+	/* Enable enhanced framing */
+	regmap_write_bits(dp->regs, DP_CORE_CONTROL, DP_ENHANCE_FRAMING_EN,
+			  FIELD_PREP(DP_ENHANCE_FRAMING_EN, 0x1));
+
+	ret = spacemit_dp_set_plls(dp, SPACEMIT_DP_LINK_RATE_2_70,
+				   dp->use_ext_pixel_clock ? 150000 : 148500);
+	if (ret)
+		return ret;
+
+	ret = spacemit_dp_phy_set_lanes(dp, SPACEMIT_DP_LANE_2);
+	if (ret)
+		return ret;
+
+	ret = spacemit_dp_phy_on(dp);
+	if (ret)
+		return ret;
+
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D0,
+			  FIELD_PREP(DP_ANA_TX_MODE_D0, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D1,
+			  FIELD_PREP(DP_ANA_TX_MODE_D1, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D2,
+			  FIELD_PREP(DP_ANA_TX_MODE_D2, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D3,
+			  FIELD_PREP(DP_ANA_TX_MODE_D3, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_RTCAL_FREQDIV_HBIT,
+		FIELD_PREP(DP_ANA_RTCAL_FREQDIV_HBIT, (clk_div >> 8) & 0x7f));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_RTCAL_BYPASS,
+			  FIELD_PREP(DP_ANA_RTCAL_BYPASS, 1));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_RTCAL_FREQDIV_LBIT,
+		FIELD_PREP(DP_ANA_RTCAL_FREQDIV_LBIT, clk_div & 0xff));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_BG_RCAL_SEL,
+			  FIELD_PREP(DP_ANA_BG_RCAL_SEL, 0));
+
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL8, DP_ANA_TX_RTM_D3,
+			  FIELD_PREP(DP_ANA_TX_RTM_D3, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL8, DP_ANA_TX_RTM_D2,
+			  FIELD_PREP(DP_ANA_TX_RTM_D2, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL8, DP_ANA_TX_RTM_D1,
+			  FIELD_PREP(DP_ANA_TX_RTM_D1, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL8, DP_ANA_TX_RTM_D0,
+			  FIELD_PREP(DP_ANA_TX_RTM_D0, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_RTCAL_BYPASS,
+			  FIELD_PREP(DP_ANA_RTCAL_BYPASS, 1));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_RTCAL_BYPASS,
+			  FIELD_PREP(DP_ANA_RTCAL_BYPASS, 0));
+	msleep(100);
+
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_PRE_D3,
+			  FIELD_PREP(DP_ANA_TX_MODE_PRE_D3, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_PRE_D2,
+			  FIELD_PREP(DP_ANA_TX_MODE_PRE_D2, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_PRE_D1,
+			  FIELD_PREP(DP_ANA_TX_MODE_PRE_D1, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_PRE_D0,
+			  FIELD_PREP(DP_ANA_TX_MODE_PRE_D0, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_DE_D3,
+			  FIELD_PREP(DP_ANA_TX_MODE_DE_D3, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_DE_D2,
+			  FIELD_PREP(DP_ANA_TX_MODE_DE_D2, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_DE_D1,
+			  FIELD_PREP(DP_ANA_TX_MODE_DE_D1, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_DE_D0,
+			  FIELD_PREP(DP_ANA_TX_MODE_DE_D0, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_POSTSEL_PRE_D3,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_PRE_D3, tx_pre));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_POSTSEL_PRE_D2,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_PRE_D2, tx_pre));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_POSTSEL_PRE_D1,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_PRE_D1, tx_pre));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_POSTSEL_PRE_D0,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_PRE_D0, tx_pre));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_ISEL_DRV_D3,
+			  FIELD_PREP(DP_ANA_TX_ISEL_DRV_D3, m_isel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_ISEL_DRV_D2,
+			  FIELD_PREP(DP_ANA_TX_ISEL_DRV_D2, m_isel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL1, DP_ANA_TX_MAINSEL_D2,
+			  FIELD_PREP(DP_ANA_TX_MAINSEL_D2, m_mainsel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL1, DP_ANA_TX_MAINSEL_D3,
+			  FIELD_PREP(DP_ANA_TX_MAINSEL_D3, m_mainsel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL1, DP_ANA_TX_ISEL_DRV_D1,
+			  FIELD_PREP(DP_ANA_TX_ISEL_DRV_D1, m_isel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL1, DP_ANA_TX_ISEL_DRV_D0,
+			  FIELD_PREP(DP_ANA_TX_ISEL_DRV_D0, m_isel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL2, DP_ANA_TX_POSTSEL_D1,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_D1, m_post));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL2, DP_ANA_TX_POSTSEL_D0,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_D0, m_post));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL2, DP_ANA_TX_POSTSEL_D3,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_D3, m_post));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL2, DP_ANA_TX_POSTSEL_D2,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_D2, m_post));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL2, DP_DA_TX_MAINSEL_D0_4_0,
+			  FIELD_PREP(DP_DA_TX_MAINSEL_D0_4_0, m_mainsel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL2, DP_ANA_TX_MAINSEL_D1,
+			  FIELD_PREP(DP_ANA_TX_MAINSEL_D1, m_mainsel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_PRESEL_D1,
+			  FIELD_PREP(DP_ANA_TX_PRESEL_D1, m_pre));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_PRESEL_D0,
+			  FIELD_PREP(DP_ANA_TX_PRESEL_D0, m_pre));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_PRESEL_D3,
+			  FIELD_PREP(DP_ANA_TX_PRESEL_D3, m_pre));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_PRESEL_D2,
+			  FIELD_PREP(DP_ANA_TX_PRESEL_D2, m_pre));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D3,
+			  FIELD_PREP(DP_ANA_TX_MODE_D3, tx_mode));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D2,
+			  FIELD_PREP(DP_ANA_TX_MODE_D2, tx_mode));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D1,
+			  FIELD_PREP(DP_ANA_TX_MODE_D1, tx_mode));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D0,
+			  FIELD_PREP(DP_ANA_TX_MODE_D0, tx_mode));
+	regmap_write_bits(dp->regs, DP_ANA_TX_AUX_CTRL, DP_ANA_TX_AUX_RX_VSEL,
+			  FIELD_PREP(DP_ANA_TX_AUX_RX_VSEL, 0x0));
+
+	return 0;
+}
+
+static int spacemit_dp_bind(struct device *dev, struct device *master,
+			    void *data)
+{
+	struct spacemit_dp_dev *dp = dev_get_drvdata(dev);
+	struct drm_device *drm = data;
+	struct device_node *ep;
+	int ret;
+
+	dp->drm = drm;
+	dp->aux.drm_dev = drm;
+	ret = drm_dp_aux_register(&dp->aux);
+	if (ret) {
+		dev_err(dev, "failed to register DP AUX: %d\n", ret);
+		return ret;
+	}
+
+	ret = drm_encoder_init(drm, &dp->encoder, &spacemit_dp_encoder_funcs,
+			       DRM_MODE_ENCODER_NONE, NULL);
+	if (ret) {
+		dev_err(dev, "failed to init encoder\n");
+		goto err_aux;
+	}
+
+	ep = of_graph_get_endpoint_by_regs(dev->of_node, 0, -1);
+	if (ep) {
+		struct device_node *remote = of_graph_get_remote_port(ep);
+
+		dp->encoder.possible_crtcs = drm_of_crtc_port_mask(drm, remote);
+		of_node_put(remote);
+		of_node_put(ep);
+	}
+
+	ret = drm_bridge_attach(&dp->encoder, &dp->bridge, NULL,
+				DRM_BRIDGE_ATTACH_NO_CONNECTOR);
+	if (ret) {
+		dev_err(dev, "failed to attach bridge: %d\n", ret);
+		goto err_encoder;
+	}
+
+	dp->connector = drm_bridge_connector_init(drm, &dp->encoder);
+	if (IS_ERR(dp->connector)) {
+		ret = PTR_ERR(dp->connector);
+		dev_err(dev, "failed to init bridge connector: %d\n", ret);
+		goto err_encoder;
+	}
+
+	drm_connector_attach_encoder(dp->connector, &dp->encoder);
+
+	ret = spacemit_dp_dev_init(dp);
+	if (ret)
+		goto err_encoder;
+
+	spacemit_dp_link_disable(dp);
+
+	dp->connector_status = spacemit_dp_hw_detect_hpd(dp);
+	spacemit_dp_hw_clean_hpd(dp);
+
+	if (dp->connector_status == connector_status_connected)
+		spacemit_dp_hw_read_sink_caps(dp);
+
+	/*
+	 * With panel-edp still deferred the chain is incomplete: a CONNECTED
+	 * notification here makes fbdev commit a modeset that wedges on
+	 * flip_done.
+	 */
+	if (dp->next_bridge || !dp->edp_mode)
+		drm_bridge_hpd_notify(&dp->bridge,
+				      spacemit_dp_bridge_detect(&dp->bridge,
+								dp->connector));
+
+	enable_irq(dp->irq);
+
+	return 0;
+
+err_encoder:
+	drm_encoder_cleanup(&dp->encoder);
+err_aux:
+	drm_dp_aux_unregister(&dp->aux);
+	return ret;
+}
+
+static void spacemit_dp_unbind(struct device *dev, struct device *master,
+			       void *data)
+{
+	struct spacemit_dp_dev *dp = dev_get_drvdata(dev);
+
+	disable_irq(dp->irq);
+
+	/* The connector this work touches dies with the DRM device. */
+	cancel_work_sync(&dp->modeset_retry_work);
+
+	drm_dp_aux_unregister(&dp->aux);
+
+	spacemit_dp_hw_disable(dp);
+
+	drm_encoder_cleanup(&dp->encoder);
+
+	dp->drm = NULL;
+	dp->connector = NULL;
+	dp->aux.drm_dev = NULL;
+}
+
+static const struct component_ops spacemit_dp_ops = {
+	.bind = spacemit_dp_bind,
+	.unbind = spacemit_dp_unbind,
+};
+
+/*
+ * Registered after devm_of_platform_populate(), so devres runs it before
+ * the PHY child (and the PLL clock it provides) is torn down: unregistering
+ * a clock that still has an enabled child warns, a disabled child is
+ * orphaned silently.
+ */
+static void spacemit_dp_pxclk_disable(void *data)
+{
+	clk_disable_unprepare(data);
+}
+
+static int inno_dp_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct device_node *aux_bus_np;
+	struct spacemit_dp_dev *dp;
+	struct reset_control *rstc;
+	struct resource *res;
+	int ret;
+
+	dp = devm_drm_bridge_alloc(dev, struct spacemit_dp_dev, bridge,
+				   &spacemit_dp_bridge_funcs);
+	if (IS_ERR(dp))
+		return PTR_ERR(dp);
+
+	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
+	if (!res)
+		return dev_err_probe(dev, -ENODEV,
+				     "no MMIO resource\n");
+
+	dp->dev = dev;
+	INIT_WORK(&dp->modeset_retry_work, spacemit_dp_modeset_retry_work);
+
+	/* eDP instances have an aux-bus child node; DP instances do not. */
+	aux_bus_np = of_get_child_by_name(dev->of_node, "aux-bus");
+	dp->edp_mode = !!aux_bus_np;
+	of_node_put(aux_bus_np);
+
+	dp->bridge.of_node = dev->of_node;
+	dp->connector_status = connector_status_disconnected;
+	ret = devm_mutex_init(dev, &dp->mode_lock);
+	if (ret)
+		return ret;
+
+	rstc = devm_reset_control_get_exclusive_deasserted(&pdev->dev, NULL);
+	if (IS_ERR(rstc)) {
+		ret = dev_err_probe(dev, PTR_ERR(rstc),
+				    "failed to get reset\n");
+		return ret;
+	}
+
+	dp->pxclk = devm_clk_get(dev, "pxclk");
+	if (IS_ERR(dp->pxclk)) {
+		ret = dev_err_probe(dev, PTR_ERR(dp->pxclk),
+				    "failed to get pxclk\n");
+		return ret;
+	}
+
+	if (dp->edp_mode) {
+		dp->bridge.type = DRM_MODE_CONNECTOR_eDP;
+		/*
+		 * OP_DETECT is required: without it
+		 * drm_bridge_connector_detect() hard-codes CONNECTED for any
+		 * eDP connector and fbdev wedges at bind.
+		 */
+		dp->bridge.ops = DRM_BRIDGE_OP_DETECT | DRM_BRIDGE_OP_EDID |
+				 DRM_BRIDGE_OP_HPD;
+	} else {
+		dp->bridge.type = DRM_MODE_CONNECTOR_DisplayPort;
+		/*
+		 * No OP_HPD: the HPD interrupt is gated by the DP pixel clock.
+		 * Leaving it out marks the connector POLL_CONNECT |
+		 * POLL_DISCONNECT instead.
+		 */
+		dp->bridge.ops = DRM_BRIDGE_OP_DETECT | DRM_BRIDGE_OP_EDID;
+	}
+
+	ret = clk_prepare_enable(dp->pxclk);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to enable pxclk\n");
+
+	/*
+	 * The PHY child shares this device's regmap, so the regmap must exist
+	 * before devm_of_platform_populate() lets the PHY probe and
+	 * devm_phy_get() succeed.
+	 */
+	ret = spacemit_dp_resource_init(dp, pdev);
+	if (ret)
+		goto err_disable_pxclk;
+
+	ret = devm_of_platform_populate(dev);
+	if (ret) {
+		dev_err_probe(dev, ret, "failed to populate child devices\n");
+		goto err_disable_pxclk;
+	}
+
+	dp->phy = devm_phy_get(dev, "dp");
+	if (IS_ERR(dp->phy)) {
+		ret = dev_err_probe(dev, PTR_ERR(dp->phy),
+				    "failed to get DP PHY\n");
+		goto err_disable_pxclk;
+	}
+
+	/*
+	 * The PHY exposes its PLL as the APMU pixel-clock mux's external
+	 * parent.
+	 */
+	dp->pll_clk = devm_clk_get(dev, "pll");
+	if (IS_ERR(dp->pll_clk)) {
+		ret = dev_err_probe(dev, PTR_ERR(dp->pll_clk),
+				    "failed to get PHY pixel clock\n");
+		goto err_disable_pxclk;
+	}
+
+	ret = clk_set_parent(dp->pxclk, dp->pll_clk);
+	if (ret) {
+		dev_err(dev, "failed to route eDP pixel mux to PHY PLL: %d\n", ret);
+		goto err_disable_pxclk;
+	}
+
+	ret = devm_add_action_or_reset(dev, spacemit_dp_pxclk_disable,
+				       dp->pxclk);
+	if (ret)
+		return ret;
+
+	/*
+	 * Panel discovery needs the AUX channel here; registration waits for
+	 * bind.
+	 */
+	dp->aux.name = "spacemit-dp-aux";
+	dp->aux.dev = dev;
+	dp->aux.transfer = spacemit_dp_aux_transfer;
+	dp->aux.no_zero_sized = true;
+	drm_dp_aux_init(&dp->aux);
+	dp->bridge.ddc = &dp->aux.ddc;
+
+	/*
+	 * eDP hands panel power, enable-GPIO and backlight to panel-edp over
+	 * the AUX bus. A DP instance has no aux-bus child and takes the
+	 * -ENODEV path.
+	 */
+	if (dp->edp_mode) {
+		ret = devm_of_dp_aux_populate_bus(&dp->aux,
+						  spacemit_dp_edp_link_panel);
+		if (ret) {
+			dev_err_probe(dev, ret,
+				"eDP: failed to populate aux-bus panel\n");
+			return ret;
+		}
+	}
+
+	/*
+	 * drm_bridge_attach() expects the bridge to already be on the global
+	 * list.
+	 */
+	ret = devm_drm_bridge_add(dev, &dp->bridge);
+	if (ret) {
+		dev_err(dev, "failed to register DP bridge: %d\n", ret);
+		return ret;
+	}
+
+	platform_set_drvdata(pdev, dp);
+
+	/*
+	 * The handler needs bind-created state, so keep the line disabled until
+	 * bind.
+	 */
+	ret = devm_request_threaded_irq(dev, dp->irq, spacemit_dp_irq_handler,
+					spacemit_dp_hotplug_event_handler,
+					IRQF_NO_AUTOEN, dev_name(dev), dp);
+	if (ret)
+		return ret;
+
+	ret = component_add(dev, &spacemit_dp_ops);
+	if (ret) {
+		dev_err(dev, "failed to add component: %d\n", ret);
+		return ret;
+	}
+
+	return 0;
+
+err_disable_pxclk:
+	clk_disable_unprepare(dp->pxclk);
+	return ret;
+}
+
+static void inno_dp_remove(struct platform_device *pdev)
+{
+	component_del(&pdev->dev, &spacemit_dp_ops);
+}
+
+static void inno_dp_shutdown(struct platform_device *pdev)
+{
+	struct spacemit_dp_dev *dp = platform_get_drvdata(pdev);
+
+	/* A deferred or failed bind leaves drvdata NULL. */
+	if (!dp)
+		return;
+
+	spacemit_dp_hw_disable(dp);
+}
+
+static const struct of_device_id spacemit_dp_match[] = {
+	{ .compatible = "spacemit,k3-inno-dp" },
+	{}
+};
+MODULE_DEVICE_TABLE(of, spacemit_dp_match);
+
+static struct platform_driver inno_dp_driver = {
+	.probe = inno_dp_probe,
+	.remove = inno_dp_remove,
+	.shutdown = inno_dp_shutdown,
+	.driver = {
+		.name = "spacemit-inno-dp-drv",
+		.of_match_table = spacemit_dp_match,
+	},
+};
+
+module_platform_driver(inno_dp_driver);
+
+MODULE_DESCRIPTION("SpacemiT K3 Innosilicon DP/eDP bridge driver");
+MODULE_LICENSE("GPL");
+MODULE_SOFTDEP("pre: phy-k3-inno-dp");
diff --git a/drivers/gpu/drm/spacemit/spacemit_inno_dp.h b/drivers/gpu/drm/spacemit/spacemit_inno_dp.h
new file mode 100644
index 0000000000000..680e2f41cd486
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/spacemit_inno_dp.h
@@ -0,0 +1,329 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ */
+
+#ifndef __SPACEMIT_INNO_DP_H__
+#define __SPACEMIT_INNO_DP_H__
+
+/* Register field access uses GENMASK/BIT + FIELD_PREP/FIELD_GET. */
+#include <linux/bits.h>
+
+#define DP_CORE_CONTROL                      0x0018
+#define  DP_SCRAMBLER_DISABLE               BIT(31)
+#define  DP_ENHANCE_FRAMING_EN              BIT(30)
+#define  DP_DEFAULT_FAST_LINK_TRAIN_EN      BIT(29)
+#define  DP_SCALE_DOWN_MODE                 BIT(28)
+#define  DP_FORCE_HPD                       BIT(27)
+#define  DP_ENABLE_EDP                      BIT(4)
+
+#define DP_SOFT_RESET                        0x001c
+#define  DP_CONTROLLER_RESET                BIT(31)
+#define  DP_PHY_RESET                       BIT(30)
+#define  DP_HDCP_RESET                      BIT(29)
+#define  DP_AUDIO_RESET                     BIT(28)
+#define  DP_AUX_RESET                       BIT(27)
+#define  DP_VIDEO_RESET                     GENMASK(3, 0)
+
+#define DP_CLK_INV_MUX                       0x0028
+#define  DP_REG_VID_CLK_SEL                 BIT(0)
+
+/* Interrupt status latches; write 1 to clear. */
+#define DP_GENERAL_INTERRUPT                 0x0080
+#define  DP_AUX_REPLY_EVENT_INT_STA         BIT(16)
+
+#define DP_GENERAL_INTERRUPT_MASK            0x0084
+#define  DP_HPD_INT_STA_MSK                 BIT(17)
+#define  DP_AUX_REPLY_EVENT_INT_STA_MSK     BIT(16)
+#define  DP_HDCP_INT_STA_MSK                BIT(15)
+#define  DP_ILLEGAL_AUX_CMD_INT_STA_MSK     BIT(14)
+#define  DP_TYPE_C_EVENT_MSK                BIT(13)
+#define  DP_DSC_EVENT_MSK                   BIT(12)
+#define  DP_SDP_INT_STA_S3_MSK              BIT(11)
+#define  DP_SDP_INT_STA_S2_MSK              BIT(10)
+#define  DP_SDP_INT_STA_S1_MSK              BIT(9)
+#define  DP_SDP_INT_STA_S0_MSK              BIT(8)
+#define  DP_VIDEO_FIFO_OVERFLOW_INT_STA_S3_MSK BIT(7)
+#define  DP_VIDEO_FIFO_OVERFLOW_INT_STA_S2_MSK BIT(6)
+#define  DP_VIDEO_FIFO_OVERFLOW_INT_STA_S1_MSK BIT(5)
+#define  DP_VIDEO_FIFO_OVERFLOW_INT_STA_S0_MSK BIT(4)
+
+#define DP_HPD_INTERRUPT_STATUS              0x0088
+#define  DP_HOT_PLUG_EVENT                  BIT(29)
+#define  DP_HOT_UNPLUG_EVENT                BIT(28)
+#define  DP_HPD_IN_STATUS                   BIT(26)
+
+#define DP_HPD_INTERRUPT_ENABLE              0x008c
+#define  DP_SINK_IRQ_EVENT_MSK              BIT(31)
+#define  DP_HOT_PLUG_EVENT_MSK              BIT(29)
+#define  DP_HOT_UNPLUG_EVENT_MSK            BIT(28)
+#define  DP_SINK_UNPLUG_ERROR_EVENT_MSK     BIT(27)
+
+#define DP_PHYIF_CTRL_ADDR                   0x0100
+#define  DP_PHY_BUSY_BYP                    BIT(31)
+#define  DP_TPS_SEL                         GENMASK(28, 25)
+#define  DP_XMIT_ENABLE                     GENMASK(20, 17)
+#define  DP_PHY_POWERDOWN                   GENMASK(16, 13)
+#define  DP_PHY_SSC_DIS                     BIT(8)
+#define  DP_PHY_NUM_LANES                   GENMASK(6, 5)
+#define  DP_PHY_RATE                        GENMASK(1, 0)
+
+#define DP_PHYIF_TX_EQ_ADDR                  0x0104
+#define  DP_PHY_LANE3_TX_VSWING             GENMASK(21, 20)
+#define  DP_PHY_LANE3_TX_PREEMP             GENMASK(19, 18)
+#define  DP_PHY_LANE2_TX_VSWING             GENMASK(15, 14)
+#define  DP_PHY_LANE2_TX_PREEMP             GENMASK(13, 12)
+#define  DP_PHY_LANE1_TX_VSWING             GENMASK(9, 8)
+#define  DP_PHY_LANE1_TX_PREEMP             GENMASK(7, 6)
+#define  DP_PHY_LANE0_TX_VSWING             GENMASK(3, 2)
+#define  DP_PHY_LANE0_TX_PREEMP             GENMASK(1, 0)
+
+#define DP_MPLL_CTRL0                        0x0180
+#define  DP_ANA_MPLL_FBDIV_LBIT             GENMASK(31, 24)
+#define  DP_ANA_MPLL_SEL_EXTWAVE            BIT(22)
+#define  DP_ANA_MPLL_DISABLE_SSCG           BIT(21)
+#define  DP_ANA_MPLL_DOWNSPREAD             BIT(20)
+#define  DP_ANA_MPLL_FBDIV_HBIT             GENMASK(19, 16)
+#define  DP_ANA_MPLL_OBSEN                  BIT(15)
+#define  DP_ANA_MPLL_OBSSEL                 BIT(14)
+#define  DP_ANA_MPLL_PREDIV                 GENMASK(13, 8)
+#define  DP_AD_LOCK_COREPLL                 BIT(7)
+#define  DP_ANA_MPLL_DACPD                  BIT(5)
+#define  DP_ANA_MPLL_DSMPD                  BIT(4)
+#define  DP_ANA_MPLL_PD                     BIT(0)
+
+#define DP_MPLL_CTRL1                        0x0184
+#define  DP_ANA_MPLL_FRAC_LBIT              GENMASK(23, 16)
+#define  DP_ANA_MPLL_FRAC_MBIT              GENMASK(15, 8)
+#define  DP_ANA_MPLL_FRAC_HBIT              GENMASK(7, 0)
+
+#define DP_MPLL_CTRL2                        0x0188
+#define  DP_ANA_MPLL_VCOCLK_DIV8_EN         BIT(16)
+#define  DP_ANA_MPLL_POSTDIVEN              BIT(11)
+#define  DP_ANA_MPLL_POSTDIV                GENMASK(10, 8)
+
+#define DP_PREPLL_CTRL0                      0x0190
+#define  DP_ANA_PREPLL_FBDIV2_LBIT          GENMASK(31, 24)
+#define  DP_ANA_PREPLL_FBDIV2_HBIT          GENMASK(19, 16)
+#define  DP_ANA_PREPLL_PREDIV               GENMASK(13, 8)
+#define  DP_AD_LOCK_PIXELPLL                BIT(7)
+#define  DP_ANA_PREPLL_DACPD                BIT(5)
+#define  DP_ANA_PREPLL_DSMPD                BIT(4)
+#define  DP_REG_PCLK_OUTPUT_NORMAL          BIT(1)
+#define  DP_ANA_PREPLL_PD                   BIT(0)
+
+#define DP_PREPLL_CTRL1                      0x0194
+#define  DP_ANA_PREPLL_PRECLK_DIVM          GENMASK(17, 16)
+#define  DP_ANA_PREPLL_PRECLK_DIVAUX        GENMASK(12, 8)
+
+#define DP_PREPLL_CTRL2                      0x0198
+#define  DP_ANA_PREPLL_PCLKDIV5_EN          BIT(31)
+#define  DP_ANA_PREPLL_PCLK_DIVAUX          GENMASK(28, 24)
+#define  DP_ANA_PREPLL_LOCK_BYPEN           BIT(17)
+#define  DP_ANA_PREPLL_LOWFRE_EN            BIT(16)
+#define  DP_ANA_PREPLL_TESTEN               BIT(15)
+#define  DP_ANA_PREPLL_TESTSEL              GENMASK(14, 12)
+#define  DP_ANA_PREPLL_HDMI_EN              BIT(9)
+#define  DP_ANA_PREPLL_DP_EN                BIT(8)
+
+#define DP_PREPLL_SSC_CTRL                   0x019c
+#define  DP_ANA_PREPLL_FRAC2_LBIT           GENMASK(23, 16)
+#define  DP_ANA_PREPLL_FRAC2_MBIT           GENMASK(15, 8)
+#define  DP_ANA_PREPLL_FRAC2_HBIT           GENMASK(7, 0)
+
+#define DP_ANA_TX_CTRL0                      0x01a0
+#define  DP_ANA_MPLL_CLKDIV_16M             GENMASK(13, 8)
+
+#define DP_ANA_TX_ENABLE                     0x01a4
+#define  DP_ANA_TX_ISEL_DRV_D3              GENMASK(31, 28)
+#define  DP_ANA_TX_ISEL_DRV_D2              GENMASK(27, 24)
+#define  DP_AD_TX_ESDSENSE_D3               GENMASK(23, 22)
+#define  DP_AD_TX_ESDSENSE_D2               GENMASK(21, 20)
+#define  DP_AD_TX_ESDSENSE_D1               GENMASK(19, 18)
+#define  DP_AD_TX_ESDSENSE_D0               GENMASK(17, 16)
+#define  DP_ANA_TX_EN_LDO_D3                BIT(11)
+#define  DP_ANA_TX_EN_LDO_D2                BIT(10)
+#define  DP_ANA_TX_EN_LDO_D1                BIT(9)
+#define  DP_ANA_TX_EN_LDO_D0                BIT(8)
+#define  DP_ANA_TX_EN_BYP_D3                BIT(7)
+#define  DP_ANA_TX_EN_BYP_D2                BIT(6)
+#define  DP_ANA_TX_EN_BYP_D1                BIT(5)
+#define  DP_ANA_TX_EN_BYP_D0                BIT(4)
+#define  DP_ANA_TX_EN_DRV_D3                BIT(3)
+#define  DP_ANA_TX_EN_DRV_D2                BIT(2)
+#define  DP_ANA_TX_EN_DRV_D1                BIT(1)
+#define  DP_ANA_TX_EN_DRV_D0                BIT(0)
+
+#define DP_ANA_TX_CTRL1                      0x01a8
+#define  DP_ANA_TX_PHASE_D2                 GENMASK(31, 30)
+#define  DP_ANA_TX_MAINSEL_D2               GENMASK(28, 24)
+#define  DP_ANA_TX_PHASE_D3                 GENMASK(23, 22)
+#define  DP_ANA_TX_MAINSEL_D3               GENMASK(20, 16)
+#define  DP_ANA_TX_ISEL_LDO_D3              BIT(11)
+#define  DP_ANA_TX_ISEL_LDO_D2              BIT(10)
+#define  DP_ANA_TX_ISEL_LDO_D1              BIT(9)
+#define  DP_ANA_TX_ISEL_LDO_D0              BIT(8)
+#define  DP_ANA_TX_ISEL_DRV_D1              GENMASK(7, 4)
+#define  DP_ANA_TX_ISEL_DRV_D0              GENMASK(3, 0)
+
+#define DP_ANA_TX_CTRL2                      0x01ac
+#define  DP_ANA_TX_POSTSEL_D1               GENMASK(31, 28)
+#define  DP_ANA_TX_POSTSEL_D0               GENMASK(27, 24)
+#define  DP_ANA_TX_POSTSEL_D3               GENMASK(23, 20)
+#define  DP_ANA_TX_POSTSEL_D2               GENMASK(19, 16)
+#define  DP_ANA_TX_PHASE_D0                 GENMASK(15, 14)
+#define  DP_DA_TX_MAINSEL_D0_4_0            GENMASK(12, 8)
+#define  DP_ANA_TX_PHASE_D1                 GENMASK(7, 6)
+#define  DP_ANA_TX_MAINSEL_D1               GENMASK(4, 0)
+
+#define DP_ANA_TX_CTRL3                      0x01b0
+#define  DP_ANA_TX_EN_P2S_D3                BIT(31)
+#define  DP_ANA_TX_EN_P2S_D2                BIT(30)
+#define  DP_ANA_TX_EN_P2S_D1                BIT(29)
+#define  DP_ANA_TX_EN_P2S_D0                BIT(28)
+#define  DP_ANA_TX_MODE_D3                  BIT(27)
+#define  DP_ANA_TX_MODE_D2                  BIT(26)
+#define  DP_ANA_TX_MODE_D1                  BIT(25)
+#define  DP_ANA_TX_MODE_D0                  BIT(24)
+#define  DP_ANA_TX_PRESEL_D1                GENMASK(14, 12)
+#define  DP_ANA_TX_PRESEL_D0                GENMASK(10, 8)
+#define  DP_ANA_TX_PRESEL_D3                GENMASK(6, 4)
+#define  DP_ANA_TX_PRESEL_D2                GENMASK(2, 0)
+
+#define DP_ANA_TX_CTRL6                      0x01bc
+#define  DP_ANA_BG_VREF5                    GENMASK(15, 14)
+#define  DP_ANA_BG_VREF4                    GENMASK(13, 12)
+#define  DP_ANA_BG_VREF3                    GENMASK(11, 10)
+#define  DP_ANA_BG_VREF2                    GENMASK(9, 8)
+#define  DP_ANA_BG_VREF                     GENMASK(7, 6)
+#define  DP_ANA_BG_VREF1                    GENMASK(5, 4)
+#define  DP_ANA_BG_EN_CHOP                  BIT(3)
+#define  DP_ANA_BG_EN                       BIT(2)
+#define  DP_ANA_BG_ISEL                     BIT(1)
+#define  DP_ANA_BG_VSEL                     BIT(0)
+
+#define DP_ANA_TX_CTRL7                      0x01c0
+#define  DP_ANA_BG_EN_RCAL                  BIT(27)
+#define  DP_ANA_BG_RCAL_SEL                 GENMASK(26, 25)
+#define  DP_AD_BG_RCAL_OUT                  BIT(24)
+#define  DP_ANA_RTCAL_FREQDIV_LBIT          GENMASK(23, 16)
+#define  DP_ANA_RTCAL_BYPASS                BIT(15)
+#define  DP_ANA_RTCAL_FREQDIV_HBIT          GENMASK(14, 8)
+#define  DP_ANA_BG_RCAL_VAL                 GENMASK(5, 0)
+
+#define DP_ANA_TX_CTRL8                      0x01c4
+#define  DP_ANA_TX_RTM_D3                   GENMASK(29, 24)
+#define  DP_ANA_TX_RTM_D2                   GENMASK(21, 16)
+#define  DP_ANA_TX_RTM_D1                   GENMASK(13, 8)
+#define  DP_ANA_TX_RTM_D0                   GENMASK(5, 0)
+
+#define DP_ANA_TX_AUX_CTRL                   0x01d0
+#define  DP_ANA_TX_AUX_RX_VSEL              GENMASK(13, 12)
+
+#define DP_ANA_LANE_EMPH                     0x01dc
+#define  DP_ANA_TX_POSTSEL_PRE_D3           GENMASK(23, 20)
+#define  DP_ANA_TX_POSTSEL_PRE_D2           GENMASK(19, 16)
+#define  DP_ANA_TX_POSTSEL_PRE_D1           GENMASK(15, 12)
+#define  DP_ANA_TX_POSTSEL_PRE_D0           GENMASK(11, 8)
+#define  DP_ANA_TX_MODE_DE_D3               BIT(7)
+#define  DP_ANA_TX_MODE_DE_D2               BIT(6)
+#define  DP_ANA_TX_MODE_DE_D1               BIT(5)
+#define  DP_ANA_TX_MODE_DE_D0               BIT(4)
+#define  DP_ANA_TX_MODE_PRE_D3              BIT(3)
+#define  DP_ANA_TX_MODE_PRE_D2              BIT(2)
+#define  DP_ANA_TX_MODE_PRE_D1              BIT(1)
+#define  DP_ANA_TX_MODE_PRE_D0              BIT(0)
+
+#define DP_VIDEO_VSAMPLE_REG                 0x0200
+#define  DP_VIDEO_STREAM_ENABLE             BIT(28)
+#define  DP_VIDEO_MAPPING                   GENMASK(26, 22)
+#define  DP_STREAM_ENC_EN                   BIT(18)
+
+#define DP_VINPUT_POLARITY                   0x020c
+#define  DP_HSYNC_IN_POLARITY               BIT(29)
+#define  DP_VSYNC_IN_POLARITY               BIT(28)
+
+#define DP_VIDEO_CONFIG2                     0x0210
+#define  DP_VACTIVE                         GENMASK(31, 16)
+#define  DP_VBLANK                          GENMASK(15, 0)
+
+#define DP_VIDEO_CONFIG1                     0x0214
+#define  DP_HBLANK                          GENMASK(29, 16)
+#define  DP_HACTIVE                         GENMASK(15, 0)
+
+#define DP_VIDEO_CONFIG5                     0x0218
+#define  DP_AVERAGE_BYTES_PER_TU            GENMASK(31, 25)
+#define  DP_AVERAGE_BYTES_PER_TU_FRAC       GENMASK(24, 21)
+#define  DP_INIT_THRESHOLD                  GENMASK(18, 10)
+
+#define DP_VIDEO_CONFIG3                     0x021c
+#define  DP_H_FRONT_PORCH                   GENMASK(31, 16)
+#define  DP_H_SYNC_WIDTH                    GENMASK(15, 0)
+
+#define DP_VIDEO_CONFIG4                     0x0220
+#define  DP_V_FRONT_PORCH                   GENMASK(31, 16)
+#define  DP_V_SYNC_WIDTH                    GENMASK(15, 0)
+
+#define DP_VIDEO_MSA1                        0x0224
+#define  DP_HSTART                          GENMASK(31, 16)
+#define  DP_VSTART                          GENMASK(15, 0)
+
+#define DP_VIDEO_MSA2                        0x0228
+#define  DP_MISC0                           GENMASK(7, 0)
+
+#define DP_VIDEO_MSA3                        0x022c
+#define  DP_NVID                            GENMASK(31, 8)
+#define  DP_MISC1                           GENMASK(7, 0)
+
+#define DP_VIDEO_HBLANK_INTERVAL             0x0230
+#define  DP_HBLANK_INTERVAL                 GENMASK(31, 16)
+
+#define DP_VIDEO_COLORBAR_CFG                0x0238
+#define  DP_VID_BIST_EN                     BIT(0)
+
+#define DP_AUDIO_CONFIG1                     0x0300
+#define  DP_AUDIO_INF_SELECT                BIT(31)
+#define  DP_AUDIO_MUTE                      BIT(30)
+#define  DP_HBR_MODE_ENABLE                 BIT(29)
+#define  DP_AUDIO_DATA_IN_EN                GENMASK(28, 25)
+#define  DP_I2S_AUDIO_MODE                  GENMASK(24, 23)
+#define  DP_AUD_ADJUST_SEL                  BIT(22)
+#define  DP_AUDIO_DATA_WIDTH                GENMASK(21, 17)
+#define  DP_AUDIO_NUM_CHANNELS              GENMASK(16, 14)
+#define  DP_AUDIO_PACKET_ID                 GENMASK(13, 6)
+#define  DP_AUDIO_TIMESTAMP_VERSION_NUM     GENMASK(5, 0)
+
+#define DP_AUX_STS_REG                       0x0400
+#define  DP_AUX_STATUS                      GENMASK(31, 24)
+#define  DP_AUX_REPLY_ERR_CODE              GENMASK(6, 4)
+
+#define DP_AUX_CMD_REG                       0x0404
+#define  DP_AUX_LENGTH                      GENMASK(27, 24)
+#define  DP_AUX_ADDR                        GENMASK(23, 4)
+#define  DP_AUX_CMD_TYPE                    GENMASK(3, 0)
+
+#define DP_AUX_START_REG                     0x0408
+#define  DP_AUX_START                       BIT(31)
+
+#define DP_AUX_DATA4_REG                     0x040c
+#define  DP_AUX_DATA4                       GENMASK(31, 0)
+
+#define DP_AUX_DATA3_REG                     0x0410
+#define  DP_AUX_DATA3                       GENMASK(31, 0)
+
+#define DP_AUX_DATA2_REG                     0x0414
+#define  DP_AUX_DATA2                       GENMASK(31, 0)
+
+#define DP_AUX_DATA1_REG                     0x0418
+#define  DP_AUX_DATA1                       GENMASK(31, 0)
+
+#define DP_SDP_HORIZONTAL_CTRL               0x0420
+#define  DP_AUD_STREAM_HORIZONTAL_EN        BIT(13)
+#define  DP_AUD_TIMESTAMP_HORIZONTAL_EN     BIT(12)
+
+#define DP_SDP_VERTICAL_CTRL                 0x0424
+#define  DP_AUD_STREAM_VERTICAL_EN          BIT(13)
+#define  DP_AUD_TIMESTAMP_VERTICAL_EN       BIT(12)
+
+#endif /* __SPACEMIT_INNO_DP_H__ */

-- 
2.43.0



WARNING: multiple messages have this Message-ID (diff)
From: Cody Kang via B4 Relay <devnull+codykang.hk.gmail.com@kernel.org>
To: "David Airlie" <airlied@gmail.com>,
	"Simona Vetter" <simona@ffwll.ch>,
	"Maarten Lankhorst" <maarten.lankhorst@linux.intel.com>,
	"Maxime Ripard" <mripard@kernel.org>,
	"Thomas Zimmermann" <tzimmermann@suse.de>,
	"Rob Herring" <robh@kernel.org>,
	"Krzysztof Kozlowski" <krzk+dt@kernel.org>,
	"Conor Dooley" <conor+dt@kernel.org>,
	"Yixun Lan" <dlan@kernel.org>, "Vinod Koul" <vkoul@kernel.org>,
	"Neil Armstrong" <neil.armstrong@linaro.org>,
	"Haylen Chu" <heylenay@4d2.org>,
	"Michael Turquette" <mturquette@baylibre.com>,
	"Stephen Boyd" <sboyd@kernel.org>,
	"Brian Masney" <bmasney@redhat.com>,
	"Philipp Zabel" <p.zabel@pengutronix.de>,
	"Paul Walmsley" <pjw@kernel.org>,
	"Palmer Dabbelt" <palmer@dabbelt.com>,
	"Albert Ou" <aou@eecs.berkeley.edu>,
	"Alexandre Ghiti" <alex@ghiti.fr>, "Yao Zi" <me@ziyao.cc>,
	"Uwe Kleine-König" <u.kleine-koenig@baylibre.com>,
	"Guodong Xu" <docular.xu@gmail.com>
Cc: dri-devel@lists.freedesktop.org, linux-riscv@lists.infradead.org,
	 devicetree@vger.kernel.org, spacemit@lists.linux.dev,
	 linux-kernel@vger.kernel.org, linux-phy@lists.infradead.org,
	 linux-clk@vger.kernel.org, Cody Kang <codykang.hk@gmail.com>
Subject: [PATCH v2 12/17] drm/spacemit: add Innosilicon DP/eDP controller bridge driver
Date: Sun, 09 Aug 2026 21:14:21 +0800	[thread overview]
Message-ID: <20260809-k3-display-v2-12-327d7910bf71@gmail.com> (raw)
In-Reply-To: <20260809-k3-display-v2-0-327d7910bf71@gmail.com>

From: Cody Kang <codykang.hk@gmail.com>

Add the DP/eDP controller that sits downstream of the Saturn DPU. Two
identical instances share one compatible; the eDP-vs-DP role is board
wiring, so it is taken from the devicetree: an eDP panel always sits
under an aux-bus child node, an external DP connector never does.

The link is driven through the generic PHY framework, so the controller
never touches a PLL register. The controller's HPD interrupt is gated by
the DP pixel clock, which can be off exactly when a plug has to be
caught, so the connector is also polled and the interrupt path re-reads
the live level when it does fire.

Signed-off-by: Cody Kang <codykang.hk@gmail.com>

---
v2:
- use devm_reset_control_get_exclusive_deasserted(), dropping the manual
  deassert/assert paths and the reset field (Philipp Zabel)
- hand the pxclk disable to a devm action registered after the PHY
  child is populated (Yao Zi)
- drop the dead NULL-clk guards (Yao Zi)
- drop the error message duplicated by devm_request_threaded_irq()
  (Yao Zi)
- ack the W1C DP_GENERAL_INTERRUPT bits with plain writes (Sashiko)
- return IRQ_HANDLED when the ISR consumed an HPD latch instead of
  IRQ_NONE (Sashiko)
- read EDID blocks through the aux I2C adapter (Sashiko)
- drop DRM_BRIDGE_OP_MODES from the DP branch (Sashiko)
- respond to Rob Herring's suggestion, port@1 required, compute
  possible_crtcs from the input endpoint only
---
 drivers/gpu/drm/spacemit/Kconfig            |   19 +
 drivers/gpu/drm/spacemit/Makefile           |    3 +
 drivers/gpu/drm/spacemit/spacemit_inno_dp.c | 2391 +++++++++++++++++++++++++++
 drivers/gpu/drm/spacemit/spacemit_inno_dp.h |  329 ++++
 4 files changed, 2742 insertions(+)

diff --git a/drivers/gpu/drm/spacemit/Kconfig b/drivers/gpu/drm/spacemit/Kconfig
index 51e38dabfdba5..afd6cdea27fc2 100644
--- a/drivers/gpu/drm/spacemit/Kconfig
+++ b/drivers/gpu/drm/spacemit/Kconfig
@@ -16,3 +16,22 @@ config DRM_SPACEMIT
 	  DisplayPort or embedded DisplayPort controller.
 
 	  If M is selected the module will be called spacemit-drm.
+
+config SPACEMIT_INNO_DP
+	tristate "SpacemiT Innosilicon DP/eDP controller support"
+	depends on DRM_SPACEMIT
+	imply PHY_SPACEMIT_K3_INNO_DP
+	select GENERIC_PHY
+	select DRM_PANEL
+	select DRM_BRIDGE_CONNECTOR
+	select DRM_DISPLAY_HELPER
+	select DRM_DISPLAY_DP_HELPER
+	select DRM_DISPLAY_DP_AUX_BUS
+	help
+	  Choose this option to enable the Innosilicon DisplayPort /
+	  embedded DisplayPort transmitter integrated in SpacemiT SoCs.
+	  The controller sits downstream of the Saturn DPU and drives an
+	  eDP panel (discovered over the DP AUX bus) or an external
+	  DisplayPort connector.
+
+	  If M is selected the module will be called spacemit-inno-dp.
diff --git a/drivers/gpu/drm/spacemit/Makefile b/drivers/gpu/drm/spacemit/Makefile
index b3b148000cf7b..260d316a271b5 100644
--- a/drivers/gpu/drm/spacemit/Makefile
+++ b/drivers/gpu/drm/spacemit/Makefile
@@ -9,4 +9,7 @@ spacemit-drm-y := spacemit_drm.o \
 		  dpu/dpu_saturn_hee.o \
 		  dpu/saturn_fbcmem.o
 
+spacemit-inno-dp-y := spacemit_inno_dp.o
+
 obj-$(CONFIG_DRM_SPACEMIT) += spacemit-drm.o
+obj-$(CONFIG_SPACEMIT_INNO_DP) += spacemit-inno-dp.o
diff --git a/drivers/gpu/drm/spacemit/spacemit_inno_dp.c b/drivers/gpu/drm/spacemit/spacemit_inno_dp.c
new file mode 100644
index 0000000000000..1ba4892607881
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/spacemit_inno_dp.c
@@ -0,0 +1,2391 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ */
+
+#include <linux/of.h>
+#include <linux/bits.h>
+#include <linux/bitfield.h>
+#include <linux/module.h>
+#include <linux/slab.h>
+#include <linux/platform_device.h>
+#include <linux/component.h>
+#include <linux/clk.h>
+#include <linux/cleanup.h>
+#include <linux/math64.h>
+#include <linux/mutex.h>
+#include <linux/reset.h>
+#include <linux/delay.h>
+#include <linux/i2c.h>
+#include <linux/iopoll.h>
+#include <linux/media-bus-format.h>
+#include <linux/of_graph.h>
+#include <linux/of_platform.h>
+#include <linux/phy/phy.h>
+#include <linux/phy/phy-dp.h>
+#include <linux/regmap.h>
+#include <drm/display/drm_dp_aux_bus.h>
+#include <drm/drm_of.h>
+#include <drm/drm_bridge.h>
+#include <drm/drm_bridge_connector.h>
+#include <drm/drm_device.h>
+#include <drm/drm_encoder.h>
+#include <drm/drm_connector.h>
+#include <drm/drm_drv.h>
+#include <drm/drm_edid.h>
+#include <drm/drm_modeset_helper_vtables.h>
+#include <drm/drm_gem_dma_helper.h>
+#include <drm/drm_probe_helper.h>
+#include <drm/drm_atomic.h>
+#include <drm/drm_atomic_state_helper.h>
+#include <drm/display/drm_dp_aux_bus.h>
+#include <drm/display/drm_dp.h>
+#include <drm/display/drm_dp_helper.h>
+#include <drm/drm_print.h>
+
+#include "spacemit_inno_dp.h"
+
+#define SPACEMIT_DP_SWING_MAX  2
+#define SPACEMIT_DP_PREEMP_MAX 2
+#define SPACEMIT_DP_AUX_MAX_RETRIES 3
+
+#define SPACEMIT_DP_DDC_SEGMENT_ADDR 0x30
+#define SPACEMIT_DP_SINK_READY_DELAY_MS 120
+#define SPACEMIT_DP_SINK_READY_RETRIES 3
+
+/*
+ * With DP routed through a Type-C PD controller the HPD pin is virtual and
+ * can re-assert seconds after a transmitter power-cycle. Asserted HPD
+ * returns immediately; the full wait is paid only when no sink shows up.
+ */
+#define SPACEMIT_DP_HPD_POLL_US     1000
+#define SPACEMIT_DP_HPD_DEBOUNCE_US (1000 * USEC_PER_MSEC)
+
+enum spacemit_dp_link_rate {
+	SPACEMIT_DP_LINK_RATE_1_62 = 1620000, /* 1.62 Gbps */
+	SPACEMIT_DP_LINK_RATE_2_70 = 2700000, /* 2.70 Gbps */
+	SPACEMIT_DP_LINK_RATE_5_40 = 5400000, /* 5.40 Gbps */
+	SPACEMIT_DP_LINK_RATE_8_10 = 8100000, /* 8.10 Gbps */
+};
+
+/* The K3 source tops out at HBR2: the PHY's MPLL has no 8.1 Gbps config. */
+#define SPACEMIT_DP_SOURCE_MAX_RATE	SPACEMIT_DP_LINK_RATE_5_40
+
+enum spacemit_dp_lane_count {
+	SPACEMIT_DP_LANE_1 = 1,
+	SPACEMIT_DP_LANE_2 = 2,
+	SPACEMIT_DP_LANE_4 = 4,
+};
+
+enum soc_video_format {
+	SOC_VIDEO_RGB_6BIT = 0,
+	SOC_VIDEO_RGB_8BIT = 1,
+	SOC_VIDEO_RGB_10BIT = 2,
+	SOC_VIDEO_RGB_12BIT = 3,
+	SOC_VIDEO_RGB_16BIT = 4,
+	SOC_VIDEO_YUV444_8BIT = 5,
+	SOC_VIDEO_YUV444_10BIT = 6,
+	SOC_VIDEO_YUV444_12BIT = 7,
+	SOC_VIDEO_YUV444_16BIT = 8,
+	SOC_VIDEO_YUV422_8BIT = 9,
+	SOC_VIDEO_YUV422_10BIT = 10,
+	SOC_VIDEO_YUV422_12BIT = 11,
+	SOC_VIDEO_YUV422_16BIT = 12,
+};
+
+enum spacemit_dp_ref_clk {
+	SPACEMIT_DP_REF_CLK_24M = 24000,
+};
+
+static const struct spacemit_dp_link_config {
+	enum spacemit_dp_link_rate rate;
+	enum spacemit_dp_lane_count lanes;
+} spacemit_dp_link_priority_table[] = {
+	{SPACEMIT_DP_LINK_RATE_1_62, SPACEMIT_DP_LANE_1}, /* 1.62 Gbps */
+	{SPACEMIT_DP_LINK_RATE_2_70, SPACEMIT_DP_LANE_1}, /* 2.70 Gbps */
+	{SPACEMIT_DP_LINK_RATE_1_62, SPACEMIT_DP_LANE_2}, /* 3.24 Gbps */
+
+	{SPACEMIT_DP_LINK_RATE_2_70, SPACEMIT_DP_LANE_2}, /* 5.40 Gbps */
+	{SPACEMIT_DP_LINK_RATE_1_62, SPACEMIT_DP_LANE_4}, /* 6.48 Gbps */
+
+	{SPACEMIT_DP_LINK_RATE_2_70, SPACEMIT_DP_LANE_4}, /* 10.8 Gbps */
+	{SPACEMIT_DP_LINK_RATE_5_40, SPACEMIT_DP_LANE_2}, /* 10.8 Gbps */
+
+	{SPACEMIT_DP_LINK_RATE_5_40, SPACEMIT_DP_LANE_4}, /* 21.6 Gbps */
+};
+
+static const struct soc_format_info {
+	u8 bpp; /* Bits Per Pixel */
+} format_info_table[] = {
+	[SOC_VIDEO_RGB_6BIT]      = { .bpp = 18 },
+	[SOC_VIDEO_RGB_8BIT]      = { .bpp = 24 },
+	[SOC_VIDEO_RGB_10BIT]     = { .bpp = 30 },
+	[SOC_VIDEO_RGB_12BIT]     = { .bpp = 36 },
+	[SOC_VIDEO_RGB_16BIT]     = { .bpp = 48 },
+
+	[SOC_VIDEO_YUV444_8BIT]   = { .bpp = 24 },
+	[SOC_VIDEO_YUV444_10BIT]  = { .bpp = 30 },
+	[SOC_VIDEO_YUV444_12BIT]  = { .bpp = 36 },
+	[SOC_VIDEO_YUV444_16BIT]  = { .bpp = 48 },
+
+	[SOC_VIDEO_YUV422_8BIT]   = { .bpp = 16 },
+	[SOC_VIDEO_YUV422_10BIT]  = { .bpp = 20 },
+	[SOC_VIDEO_YUV422_12BIT]  = { .bpp = 24 },
+	[SOC_VIDEO_YUV422_16BIT]  = { .bpp = 32 },
+};
+
+/**
+ * struct spacemit_dp_dev - Per-instance state for an Innosilicon DP/eDP
+ * controller
+ * @dev: Underlying platform device.
+ * @drm: Parent DRM device, set at component bind.
+ * @encoder: Passive encoder anchoring possible_crtcs; atomic goes through
+ * @bridge.
+ * @bridge: DRM bridge implementing this controller, attached NO_CONNECTOR.
+ * @connector: Connector built on top of @bridge by drm_bridge_connector_init().
+ * @connector_status: Last-known status, driven by the HPD ISR and by polling.
+ * @mode: Committed adjusted_mode, cached under @mode_lock for the async paths.
+ * @regs: Mapped MMIO base of the DP controller register block.
+ * @aux: DPCD AUX channel, registered at bind.
+ * @dpcd: Raw DPCD capability bytes.
+ * @lane_count: Lane count chosen by link training (1, 2 or 4).
+ * @link_rate: Link rate chosen by link training, in kHz.
+ * @link: Sink-advertised link capability snapshot.
+ * @link.revision: DPCD revision byte.
+ * @link.enhanced_framing: Whether the sink advertises enhanced framing.
+ * @link.max_rate: Sink-advertised maximum link rate, kHz.
+ * @link.max_num_lanes: Sink-advertised maximum lane count.
+ * @pxclk: Optional external pixel clock.
+ * @pll_clk: The PHY's pixel PLL; clk_set_rate() programs it.
+ * @phy: DP PHY handle.
+ * @phy_powered: Controller-side mirror of the PHY framework's power refcount.
+ * @next_bridge: Downstream bridge; panel-edp's drm_panel wrapper on eDP.
+ * @edp_mode: True when this instance drives an embedded panel.
+ * @use_ext_pixel_clock: True when the pixel clock is sourced externally.
+ * @pixel_clock: Cached external pixel-clock rate, kHz.
+ * @mode_lock: Serialises mode_set / enable / hotplug against each other.
+ * @modeset_retry_work: Deferred "link is bad" notification, see its handler.
+ * @irq: Controller interrupt line.
+ */
+struct spacemit_dp_dev {
+	struct device *dev;
+
+	struct drm_device *drm;
+	struct drm_encoder encoder;
+	struct drm_bridge bridge;
+	struct drm_connector *connector;
+
+	enum drm_connector_status connector_status;
+	struct drm_display_mode mode;
+
+	struct regmap *regs;
+	struct drm_dp_aux aux;
+
+	u8 dpcd[DP_RECEIVER_CAP_SIZE];
+	int lane_count;
+	u32 link_rate;
+
+	struct {
+		u8 revision;
+		u8 enhanced_framing;
+		u32 max_rate;
+		u32 max_num_lanes;
+	} link;
+
+	struct clk *pxclk;
+	struct clk *pll_clk;
+	struct phy *phy;
+	bool phy_powered;
+	struct drm_bridge *next_bridge;
+
+	bool edp_mode;
+	bool use_ext_pixel_clock;
+	int pixel_clock;
+	struct mutex mode_lock;
+
+	struct work_struct modeset_retry_work;
+
+	int irq;
+};
+
+static int spacemit_dp_get_bpp(struct spacemit_dp_dev *dp, u32 format)
+{
+	if (format >= ARRAY_SIZE(format_info_table)) {
+		dev_warn(dp->dev,
+			"invalid color format index %d, defaulting to RGB888\n",
+			format);
+		return 24;
+	}
+	return format_info_table[format].bpp;
+}
+
+/*
+ * drm_connector_set_link_status_property() takes connection_mutex, which the
+ * commit already holds: calling it from .atomic_enable deadlocks against
+ * itself.
+ */
+static void spacemit_dp_modeset_retry_work(struct work_struct *work)
+{
+	struct spacemit_dp_dev *dp = container_of(work, struct spacemit_dp_dev,
+						  modeset_retry_work);
+	struct drm_connector *connector = dp->connector;
+
+	if (!connector)
+		return;
+
+	mutex_lock(&connector->dev->mode_config.mutex);
+	drm_connector_set_link_status_property(connector,
+					       DRM_MODE_LINK_STATUS_BAD);
+	mutex_unlock(&connector->dev->mode_config.mutex);
+
+	drm_kms_helper_connector_hotplug_event(connector);
+}
+
+/**
+ * struct spacemit_dp_bridge_state - Per-state planned link configuration.
+ * @base: Parent &struct drm_bridge_state.
+ * @link_cfg_idx: Index into spacemit_dp_link_priority_table[] picked by
+ *	->atomic_check(); -1 means ->atomic_enable() walks the table from the start.
+ * @color_format: Output colour format selected by ->atomic_check().
+ */
+struct spacemit_dp_bridge_state {
+	struct drm_bridge_state base;
+	int link_cfg_idx;
+	u32 color_format;
+};
+
+static inline struct spacemit_dp_bridge_state *
+to_spacemit_dp_bridge_state(struct drm_bridge_state *state)
+{
+	return container_of(state, struct spacemit_dp_bridge_state, base);
+}
+
+static const struct regmap_config spacemit_dp_regmap_config = {
+	.name		= "dp",
+	.reg_bits	= 32,
+	.val_bits	= 32,
+	.reg_stride	= 4,
+	.max_register	= 0x3ffc,	/* full 0x4000 reg region from binding */
+};
+
+static u32 spacemit_dp_aux_get_cmd(struct drm_dp_aux_msg *msg)
+{
+	switch (msg->request & ~DP_AUX_I2C_MOT) {
+	case DP_AUX_NATIVE_WRITE:
+	case DP_AUX_I2C_WRITE:
+	case DP_AUX_I2C_WRITE_STATUS_UPDATE:
+		return msg->request;
+	case DP_AUX_NATIVE_READ:
+	case DP_AUX_I2C_READ:
+		return msg->request;
+	default:
+		return 0;
+	}
+
+	return 0;
+}
+
+static void spacemit_dp_aux_hw_reset(struct spacemit_dp_dev *dp)
+{
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_AUX_RESET,
+			  FIELD_PREP(DP_AUX_RESET, 0x1));
+	usleep_range(2000, 2500);
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_AUX_RESET,
+			  FIELD_PREP(DP_AUX_RESET, 0x0));
+	usleep_range(2000, 2500);
+	regmap_write(dp->regs, DP_GENERAL_INTERRUPT,
+		     DP_AUX_REPLY_EVENT_INT_STA);
+}
+
+static bool spacemit_dp_dpcd_caps_valid(const u8 *dpcd)
+{
+	u8 max_bw = dpcd[DP_MAX_LINK_RATE];
+	u8 max_lanes = dpcd[DP_MAX_LANE_COUNT] & DP_MAX_LANE_COUNT_MASK;
+
+	if (!dpcd[DP_DPCD_REV])
+		return false;
+
+	switch (max_bw) {
+	case DP_LINK_BW_1_62:
+	case DP_LINK_BW_2_7:
+	case DP_LINK_BW_5_4:
+	case DP_LINK_BW_8_1:
+		break;
+	default:
+		return false;
+	}
+
+	return max_lanes == 1 || max_lanes == 2 || max_lanes == 4;
+}
+
+static ssize_t spacemit_dp_aux_transfer(struct drm_dp_aux *aux,
+					struct drm_dp_aux_msg *msg)
+{
+	int ret, i, retries = 0;
+	unsigned long timeout;
+
+	struct spacemit_dp_dev *dp = container_of(aux, struct spacemit_dp_dev,
+						  aux);
+	u32 cmd, len, val, status;
+	u32 data[4] = {0};
+	u8 *buf = msg->buffer;
+	bool is_read = (msg->request & DP_AUX_I2C_READ) ||
+		((msg->request & DP_AUX_NATIVE_READ) == DP_AUX_NATIVE_READ);
+
+	if (msg->size > 16)
+		return -EINVAL;
+
+	cmd = spacemit_dp_aux_get_cmd(msg);
+
+retry_eio:
+	if (!is_read) {
+		/* Pack bytes into 32-bit words (Little Endian packing) */
+		for (i = 0; i < msg->size; i++)
+			data[i / 4] |= buf[i] << ((i % 4) * 8);
+
+		/* Write data to registers: DATA1(LSB)..DATA4(MSB) */
+		regmap_write_bits(dp->regs, DP_AUX_DATA1_REG, DP_AUX_DATA1,
+				  FIELD_PREP(DP_AUX_DATA1, data[0]));
+		regmap_write_bits(dp->regs, DP_AUX_DATA2_REG, DP_AUX_DATA2,
+				  FIELD_PREP(DP_AUX_DATA2, data[1]));
+		regmap_write_bits(dp->regs, DP_AUX_DATA3_REG, DP_AUX_DATA3,
+				  FIELD_PREP(DP_AUX_DATA3, data[2]));
+		regmap_write_bits(dp->regs, DP_AUX_DATA4_REG, DP_AUX_DATA4,
+				  FIELD_PREP(DP_AUX_DATA4, data[3]));
+	}
+
+	/* HW expects Length - 1 */
+	len = msg->size > 0 ? msg->size - 1 : 0;
+
+	regmap_write_bits(dp->regs, DP_AUX_CMD_REG, DP_AUX_LENGTH,
+			  FIELD_PREP(DP_AUX_LENGTH, len));
+	regmap_write_bits(dp->regs, DP_AUX_CMD_REG, DP_AUX_ADDR,
+			  FIELD_PREP(DP_AUX_ADDR, msg->address));
+	regmap_write_bits(dp->regs, DP_AUX_CMD_REG, DP_AUX_CMD_TYPE,
+			  FIELD_PREP(DP_AUX_CMD_TYPE, cmd));
+
+	regmap_write_bits(dp->regs, DP_AUX_START_REG, DP_AUX_START,
+			  FIELD_PREP(DP_AUX_START, 0));
+	regmap_write_bits(dp->regs, DP_AUX_START_REG, DP_AUX_START,
+			  FIELD_PREP(DP_AUX_START, 1));
+
+	timeout = jiffies + msecs_to_jiffies(200);
+	ret = -ETIMEDOUT;
+
+	while (1) {
+		regmap_read(dp->regs, DP_GENERAL_INTERRUPT, &val);
+		val = FIELD_GET(DP_AUX_REPLY_EVENT_INT_STA, val);
+		if (val) {
+			ret = 0;
+			break;
+		}
+		if (time_after(jiffies, timeout))
+			break;
+		usleep_range(100, 110);
+	}
+
+	if (ret) {
+		if (retries < SPACEMIT_DP_AUX_MAX_RETRIES) {
+			retries++;
+			spacemit_dp_aux_hw_reset(dp);
+			dev_dbg(dp->dev,
+				"AUX timeout, cmd=0x%x addr=0x%x size=%zu retry %d/%d\n",
+				cmd, msg->address, msg->size, retries, SPACEMIT_DP_AUX_MAX_RETRIES);
+			goto retry_eio;
+		}
+
+		dev_err(dp->dev,
+			"AUX transfer timeout, req: 0x%x, cmd: 0x%x, addr: 0x%x, size %zu\n",
+			msg->request, cmd, msg->address, msg->size);
+		return ret;
+	}
+
+	regmap_read(dp->regs, DP_AUX_STS_REG,
+		    &status); status = FIELD_GET(DP_AUX_STATUS, status);
+
+	/* Write 1 to clear. */
+	regmap_write(dp->regs, DP_GENERAL_INTERRUPT,
+		     DP_AUX_REPLY_EVENT_INT_STA);
+
+	/* Map HW status (the AUX reply command byte) to DRM reply codes */
+	switch (status) {
+	case 0: /* ACK */
+		msg->reply = DP_AUX_NATIVE_REPLY_ACK;
+		break;
+	case 1: /* NACK */
+		msg->reply = DP_AUX_NATIVE_REPLY_NACK;
+		return 0; /* Standard says return 0 on NACK for upper layer retry */
+	case 2: /* DEFER */
+		msg->reply = DP_AUX_NATIVE_REPLY_DEFER;
+		return 0;
+	case DP_AUX_I2C_REPLY_NACK: /* native ACK, I2C NACK */
+		msg->reply = DP_AUX_I2C_REPLY_NACK;
+		return 0;
+	case DP_AUX_I2C_REPLY_DEFER: /* native ACK, I2C DEFER */
+		msg->reply = DP_AUX_I2C_REPLY_DEFER;
+		return 0;
+	default:
+		regmap_read(dp->regs, DP_AUX_STS_REG, &val);
+		val = FIELD_GET(DP_AUX_REPLY_ERR_CODE, val);
+		if (retries < SPACEMIT_DP_AUX_MAX_RETRIES) {
+			retries++;
+			dev_dbg(dp->dev,
+				"AUX status, cmd=0x%x addr=0x%x size=%zu retry %d/%d\n",
+				cmd, msg->address, msg->size, retries, SPACEMIT_DP_AUX_MAX_RETRIES);
+			usleep_range(1000, 1100);
+			goto retry_eio;
+		}
+		dev_err(dp->dev,
+			"AUX error: cmd=0x%x addr=0x%x size=%zu status=0x%x code=0x%x\n",
+			cmd, msg->address, msg->size, status, val);
+		return -EIO;
+	}
+
+	if (is_read && msg->size > 0) {
+		regmap_read(dp->regs, DP_AUX_DATA1_REG, &data[0]);
+		data[0] = FIELD_GET(DP_AUX_DATA1, data[0]);
+		regmap_read(dp->regs, DP_AUX_DATA2_REG, &data[1]);
+		data[1] = FIELD_GET(DP_AUX_DATA2, data[1]);
+		regmap_read(dp->regs, DP_AUX_DATA3_REG, &data[2]);
+		data[2] = FIELD_GET(DP_AUX_DATA3, data[2]);
+		regmap_read(dp->regs, DP_AUX_DATA4_REG, &data[3]);
+		data[3] = FIELD_GET(DP_AUX_DATA4, data[3]);
+
+		/* Unpack 32-bit words back to bytes */
+		for (i = 0; i < msg->size; i++)
+			buf[i] = (data[i / 4] >> ((i % 4) * 8)) & 0xFF;
+	}
+
+	return msg->size;
+}
+
+static int update_edp_config(struct spacemit_dp_dev *dp, bool enable)
+{
+	u8 value;
+	int ret;
+
+	ret = drm_dp_dpcd_read_byte(&dp->aux, DP_EDP_CONFIGURATION_SET, &value);
+	if (ret < 0) {
+		dev_err(dp->dev,
+			"failed to read DP_EDP_CONFIGURATION_SET: %d\n", ret);
+		return ret;
+	}
+
+	if (enable)
+		value |= 0x01;
+	else
+		value &= ~0x01;
+
+	ret = drm_dp_dpcd_write_byte(&dp->aux, DP_EDP_CONFIGURATION_SET, value);
+	if (ret < 0) {
+		dev_err(dp->dev,
+			"failed to write DP_EDP_CONFIGURATION_SET: %d\n", ret);
+		return ret;
+	}
+
+	return 0;
+}
+
+/* Read the sink's DPCD link-layer capabilities. EDID is handled separately. */
+static int spacemit_dp_hw_read_sink_caps(struct spacemit_dp_dev *dp)
+{
+	ssize_t ret;
+	u8 max_bw;
+	int retry;
+
+	for (retry = 0; retry < SPACEMIT_DP_SINK_READY_RETRIES; retry++) {
+		if (retry) {
+			spacemit_dp_aux_hw_reset(dp);
+			msleep(SPACEMIT_DP_SINK_READY_DELAY_MS);
+		}
+
+		ret = drm_dp_read_dpcd_caps(&dp->aux, dp->dpcd);
+		if (ret < 0)
+			continue;
+
+		if (!spacemit_dp_dpcd_caps_valid(dp->dpcd)) {
+			dev_dbg(dp->dev,
+				"DPCD caps not ready: rev=0x%02x bw=0x%02x lanes=0x%02x\n",
+				dp->dpcd[DP_DPCD_REV], dp->dpcd[DP_MAX_LINK_RATE],
+				dp->dpcd[DP_MAX_LANE_COUNT]);
+			ret = -EAGAIN;
+			continue;
+		}
+
+		break;
+	}
+
+	if (ret < 0) {
+		dev_err(dp->dev, "failed to read DPCD after %d retries: %zd\n",
+			SPACEMIT_DP_SINK_READY_RETRIES, ret);
+		dp->link.revision = 0x14;
+		dp->link.max_rate = SPACEMIT_DP_LINK_RATE_5_40;
+		dp->link.max_num_lanes = SPACEMIT_DP_LANE_2;
+		dp->link.enhanced_framing = 1;
+		return ret;
+	}
+
+	dp->link.revision = dp->dpcd[DP_DPCD_REV];
+
+	max_bw = dp->dpcd[DP_MAX_LINK_RATE];
+	switch (max_bw) {
+	case DP_LINK_BW_1_62:
+		dp->link.max_rate = SPACEMIT_DP_LINK_RATE_1_62;
+		break;
+	case DP_LINK_BW_2_7:
+		dp->link.max_rate = SPACEMIT_DP_LINK_RATE_2_70;
+		break;
+	case DP_LINK_BW_5_4:
+		dp->link.max_rate = SPACEMIT_DP_LINK_RATE_5_40;
+		break;
+	case DP_LINK_BW_8_1:
+		dp->link.max_rate = SPACEMIT_DP_LINK_RATE_8_10;
+		break;
+	default:
+		dev_warn(dp->dev, "unknown DPCD max link rate 0x%x, defaulting to 5.40 Gbps\n",
+			 max_bw);
+		dp->link.revision = 0x14;
+		dp->link.max_rate = SPACEMIT_DP_LINK_RATE_5_40;
+		dp->link.max_num_lanes = SPACEMIT_DP_LANE_2;
+		dp->link.enhanced_framing = 1;
+		return -EINVAL;
+	}
+
+	/* Clamp to the source's actual ceiling (HBR2). */
+	dp->link.max_rate = min(dp->link.max_rate,
+				(u32)SPACEMIT_DP_SOURCE_MAX_RATE);
+
+	dp->link.max_num_lanes =
+			dp->dpcd[DP_MAX_LANE_COUNT] & DP_MAX_LANE_COUNT_MASK;
+
+	dp->link.enhanced_framing =
+		(dp->dpcd[DP_MAX_LANE_COUNT] & DP_ENHANCED_FRAME_CAP);
+
+	dev_dbg(dp->dev,
+		"DPCD: Rev %x.%x, MaxRate %d kHz, MaxLanes %d, EnhFrame %d\n",
+		dp->link.revision >> 4, dp->link.revision & 0xF,
+		dp->link.max_rate, dp->link.max_num_lanes,
+		dp->link.enhanced_framing);
+
+	return 0;
+}
+
+static int spacemit_dp_phy_set_lanes(struct spacemit_dp_dev *dp,
+				     enum spacemit_dp_lane_count lanes)
+{
+	union phy_configure_opts opts = {
+		.dp = {
+			.lanes = lanes,
+			.set_lanes = true,
+		},
+	};
+	int ret;
+
+	ret = phy_configure(dp->phy, &opts);
+	if (ret)
+		return ret;
+
+	dp->lane_count = lanes;
+	return 0;
+}
+
+static int spacemit_dp_phy_set_rate(struct spacemit_dp_dev *dp,
+				    enum spacemit_dp_link_rate rate)
+{
+	union phy_configure_opts opts = {
+		.dp = {
+			/*
+			 * enum value is in kbit/s; PHY framework wants Mbit/s.
+			 */
+			.link_rate = rate / 1000,
+			.set_rate = true,
+		},
+	};
+	int ret;
+
+	ret = phy_configure(dp->phy, &opts);
+	if (ret)
+		return ret;
+
+	dp->link_rate = rate;
+	return 0;
+}
+
+/*
+ * The link PLL rides phy_configure(); the pixel PLL is a clock the PHY
+ * registered. Link first: that is the order the analog blocks come up in.
+ */
+static int spacemit_dp_set_plls(struct spacemit_dp_dev *dp,
+				enum spacemit_dp_link_rate rate, u32 pclk_khz)
+{
+	int ret;
+
+	ret = spacemit_dp_phy_set_rate(dp, rate);
+	if (ret)
+		return ret;
+
+	return clk_set_rate(dp->pll_clk, (unsigned long)pclk_khz * 1000);
+}
+
+/*
+ * phy_power_on/off() are refcounted, but this driver's bring-up is idempotent
+ * rather than pair-balanced. Mirror the state so the refcount cannot go
+ * negative.
+ */
+static int spacemit_dp_phy_on(struct spacemit_dp_dev *dp)
+{
+	int ret;
+
+	if (dp->phy_powered)
+		return 0;
+
+	ret = phy_power_on(dp->phy);
+	if (ret)
+		return ret;
+
+	dp->phy_powered = true;
+	return 0;
+}
+
+static void spacemit_dp_phy_off(struct spacemit_dp_dev *dp)
+{
+	if (!dp->phy_powered)
+		return;
+
+	phy_power_off(dp->phy);
+	dp->phy_powered = false;
+}
+
+/*
+ * The AUX reset pulse is what leaves the next bring-up a clean state machine.
+ */
+static void spacemit_dp_link_disable(struct spacemit_dp_dev *dp)
+{
+	regmap_write_bits(dp->regs, DP_VIDEO_VSAMPLE_REG,
+			  DP_VIDEO_STREAM_ENABLE,
+			  FIELD_PREP(DP_VIDEO_STREAM_ENABLE, 0));
+	spacemit_dp_phy_off(dp);
+
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_AUX_RESET,
+			  FIELD_PREP(DP_AUX_RESET, 0x1));
+	usleep_range(5000, 8000);
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_AUX_RESET,
+			  FIELD_PREP(DP_AUX_RESET, 0x0));
+	usleep_range(2000, 4000);
+}
+
+static enum drm_connector_status
+spacemit_dp_hw_detect_hpd(struct spacemit_dp_dev *dp)
+{
+	u32 hpd_status;
+	enum drm_connector_status connector_status =
+			connector_status_disconnected;
+
+	regmap_read(dp->regs, DP_HPD_INTERRUPT_STATUS, &hpd_status);
+	hpd_status = FIELD_GET(DP_HPD_IN_STATUS, hpd_status);
+	if (hpd_status)
+		connector_status = connector_status_connected;
+	else
+		connector_status = connector_status_disconnected;
+
+	return connector_status;
+}
+
+/* Returns true when a latched HPD event was pending and got acked. */
+static bool spacemit_dp_hw_clean_hpd(struct spacemit_dp_dev *dp)
+{
+	u32 plug_event, unplug_event;
+
+	regmap_read(dp->regs, DP_HPD_INTERRUPT_STATUS, &plug_event);
+	plug_event = FIELD_GET(DP_HOT_PLUG_EVENT, plug_event);
+	regmap_read(dp->regs, DP_HPD_INTERRUPT_STATUS, &unplug_event);
+	unplug_event = FIELD_GET(DP_HOT_UNPLUG_EVENT, unplug_event);
+
+	if (plug_event)
+		regmap_write(dp->regs, DP_HPD_INTERRUPT_STATUS,
+			     FIELD_PREP(DP_HOT_PLUG_EVENT, 0x1));
+
+	if (unplug_event)
+		regmap_write(dp->regs, DP_HPD_INTERRUPT_STATUS,
+			     FIELD_PREP(DP_HOT_UNPLUG_EVENT, 0x1));
+
+	return plug_event || unplug_event;
+}
+
+static int spacemit_dp_set_training_pattern(struct spacemit_dp_dev *dp,
+					    u8 pattern)
+{
+	u32 tps_sel = 0;
+	u8 dpcd_pattern = pattern;
+	int ret;
+
+	if (pattern != DP_TRAINING_PATTERN_DISABLE)
+		dpcd_pattern |= DP_LINK_SCRAMBLING_DISABLE;
+
+	switch (pattern) {
+	case DP_TRAINING_PATTERN_DISABLE:
+		tps_sel = 0;
+		regmap_write_bits(dp->regs, DP_CORE_CONTROL,
+				  DP_SCRAMBLER_DISABLE,
+				  FIELD_PREP(DP_SCRAMBLER_DISABLE, 0));
+		break;
+	case DP_TRAINING_PATTERN_1:
+		tps_sel = 1;
+		regmap_write_bits(dp->regs, DP_CORE_CONTROL,
+				  DP_SCRAMBLER_DISABLE,
+				  FIELD_PREP(DP_SCRAMBLER_DISABLE, 1));
+		break;
+	case DP_TRAINING_PATTERN_2:
+		tps_sel = 2;
+		regmap_write_bits(dp->regs, DP_CORE_CONTROL,
+				  DP_SCRAMBLER_DISABLE,
+				  FIELD_PREP(DP_SCRAMBLER_DISABLE, 1));
+		break;
+	case DP_TRAINING_PATTERN_3:
+		tps_sel = 3;
+		regmap_write_bits(dp->regs, DP_CORE_CONTROL,
+				  DP_SCRAMBLER_DISABLE,
+				  FIELD_PREP(DP_SCRAMBLER_DISABLE, 1));
+		break;
+	default:
+		dev_err(dp->dev, "Unsupported training pattern: 0x%x\n",
+			pattern);
+		return -EINVAL;
+	}
+
+	regmap_write_bits(dp->regs, DP_PHYIF_CTRL_ADDR, DP_TPS_SEL,
+			  FIELD_PREP(DP_TPS_SEL, tps_sel));
+
+	ret = drm_dp_dpcd_write_byte(&dp->aux, DP_TRAINING_PATTERN_SET,
+				     dpcd_pattern);
+	if (ret < 0) {
+		dev_err(dp->dev, "failed to set DPCD training pattern: %d\n",
+			ret);
+		return ret;
+	}
+
+	return 0;
+}
+
+/*
+ * The adjust request has to reach the source PHY, not just the sink's DPCD:
+ * a transmitter left at its reset default trains any level > 0 with a bad eye.
+ */
+static int spacemit_dp_link_apply_adjust(struct spacemit_dp_dev *dp,
+					 const u8 *link_status,
+				    enum spacemit_dp_lane_count lanes,
+				    u8 *training_set)
+{
+	union phy_configure_opts opts = {
+		.dp = {
+			.lanes = lanes,
+			.set_voltages = true,
+		},
+	};
+	int i, ret;
+
+	for (i = 0; i < lanes; i++) {
+		u8 v = drm_dp_get_adjust_request_voltage(link_status, i);
+		u8 p = drm_dp_get_adjust_request_pre_emphasis(link_status, i);
+		u8 v_bits;
+
+		if (v >= SPACEMIT_DP_SWING_MAX)
+			v = SPACEMIT_DP_SWING_MAX;
+		if (p >= SPACEMIT_DP_PREEMP_MAX)
+			p = SPACEMIT_DP_PREEMP_MAX;
+
+		opts.dp.voltage[i] = v;
+		opts.dp.pre[i] = p;
+
+		v_bits = v;
+		if (v == SPACEMIT_DP_SWING_MAX)
+			v_bits |= DP_TRAIN_MAX_SWING_REACHED;
+		if (p == SPACEMIT_DP_PREEMP_MAX)
+			v_bits |= DP_TRAIN_MAX_PRE_EMPHASIS_REACHED;
+
+		training_set[i] = v_bits | (p << DP_TRAIN_PRE_EMPHASIS_SHIFT);
+	}
+
+	ret = phy_configure(dp->phy, &opts);
+	if (ret)
+		return ret;
+
+	return drm_dp_dpcd_write_data(&dp->aux, DP_TRAINING_LANE0_SET,
+				      training_set, lanes);
+}
+
+static int spacemit_dp_link_train_clock_recovery(struct spacemit_dp_dev *dp,
+						 enum spacemit_dp_link_rate rate,
+					    enum spacemit_dp_lane_count lanes)
+{
+	u8 link_status[DP_LINK_STATUS_SIZE];
+	u8 training_set[4] = {0};
+	int retries = 0;
+	int ret;
+
+	ret = drm_dp_dpcd_write_data(&dp->aux, DP_TRAINING_LANE0_SET,
+				     training_set, lanes);
+	if (ret < 0)
+		return ret;
+
+	ret = spacemit_dp_set_training_pattern(dp, DP_TRAINING_PATTERN_1);
+	if (ret < 0) {
+		spacemit_dp_set_training_pattern(dp,
+						 DP_TRAINING_PATTERN_DISABLE);
+		return ret;
+	}
+
+	while (retries < 8) {
+		drm_dp_link_train_clock_recovery_delay(&dp->aux, dp->dpcd);
+
+		ret = drm_dp_dpcd_read_link_status(&dp->aux, link_status);
+		if (ret < 0) {
+			spacemit_dp_set_training_pattern(dp,
+				DP_TRAINING_PATTERN_DISABLE);
+			return ret;
+		}
+
+		if (drm_dp_clock_recovery_ok(link_status, lanes))
+			return 0;
+
+		ret = spacemit_dp_link_apply_adjust(dp, link_status, lanes,
+						    training_set);
+		if (ret) {
+			spacemit_dp_set_training_pattern(dp,
+				DP_TRAINING_PATTERN_DISABLE);
+			return ret;
+		}
+
+		retries++;
+	}
+
+	dev_err(dp->dev, "link training: clock recovery failed\n");
+	spacemit_dp_set_training_pattern(dp, DP_TRAINING_PATTERN_DISABLE);
+	return -ETIMEDOUT;
+}
+
+static int spacemit_dp_link_train_channel_eq(struct spacemit_dp_dev *dp,
+					     enum spacemit_dp_link_rate rate,
+					enum spacemit_dp_lane_count lanes)
+{
+	u8 link_status[DP_LINK_STATUS_SIZE];
+	u8 training_set[4] = {0};
+	u8 training_pattern = DP_TRAINING_PATTERN_2;
+	int retries = 0;
+	int ret;
+
+	if (dp->dpcd[DP_MAX_LANE_COUNT] & DP_TPS3_SUPPORTED) {
+		training_pattern = DP_TRAINING_PATTERN_3;
+		dev_dbg(dp->dev, "link training: using TPS3\n");
+	} else {
+		dev_dbg(dp->dev, "link training: using TPS2\n");
+	}
+
+	ret = spacemit_dp_set_training_pattern(dp, training_pattern);
+	if (ret < 0) {
+		spacemit_dp_set_training_pattern(dp,
+						 DP_TRAINING_PATTERN_DISABLE);
+		return ret;
+	}
+
+	while (retries < 8) {
+		drm_dp_link_train_channel_eq_delay(&dp->aux, dp->dpcd);
+
+		ret = drm_dp_dpcd_read_link_status(&dp->aux, link_status);
+		if (ret < 0) {
+			spacemit_dp_set_training_pattern(dp,
+				DP_TRAINING_PATTERN_DISABLE);
+			return ret;
+		}
+
+		if (drm_dp_channel_eq_ok(link_status, lanes)) {
+			spacemit_dp_set_training_pattern(dp,
+				DP_TRAINING_PATTERN_DISABLE);
+			return 0;
+		}
+
+		ret = spacemit_dp_link_apply_adjust(dp, link_status, lanes,
+						    training_set);
+		if (ret) {
+			spacemit_dp_set_training_pattern(dp,
+				DP_TRAINING_PATTERN_DISABLE);
+			return ret;
+		}
+
+		retries++;
+	}
+
+	dev_err(dp->dev, "link training: channel EQ failed\n");
+	spacemit_dp_set_training_pattern(dp, DP_TRAINING_PATTERN_DISABLE);
+	return -ETIMEDOUT;
+}
+
+static int spacemit_dp_link_train(struct spacemit_dp_dev *dp, enum spacemit_dp_link_rate rate,
+				  enum spacemit_dp_lane_count lanes)
+{
+	int ret;
+	u8 link_config[1];
+	u8 bw_code;
+
+	/* Map Link Rate Enum to DPCD Bandwidth Code */
+	switch (rate) {
+	case SPACEMIT_DP_LINK_RATE_1_62:
+		bw_code = DP_LINK_BW_1_62;
+		break;
+	case SPACEMIT_DP_LINK_RATE_2_70:
+		bw_code = DP_LINK_BW_2_7;
+		break;
+	case SPACEMIT_DP_LINK_RATE_5_40:
+		bw_code = DP_LINK_BW_5_4;
+		break;
+	case SPACEMIT_DP_LINK_RATE_8_10:
+		bw_code = DP_LINK_BW_8_1;
+		break;
+	default:
+		bw_code = DP_LINK_BW_1_62;
+		break;
+	}
+
+	/*
+	 * A sink may act on the DP_LINK_BW_SET write, so set the lane count
+	 * first.
+	 */
+	link_config[0] = lanes;
+	if (dp->link.enhanced_framing)
+		link_config[0] |= DP_LANE_COUNT_ENHANCED_FRAME_EN;
+
+	ret = drm_dp_dpcd_write_byte(&dp->aux, DP_LANE_COUNT_SET,
+				     link_config[0]);
+	if (ret < 0) {
+		dev_err(dp->dev, "failed to set DPCD lane count\n");
+		return ret;
+	}
+
+	ret = drm_dp_dpcd_write_byte(&dp->aux, DP_LINK_BW_SET, bw_code);
+	if (ret < 0) {
+		dev_err(dp->dev, "failed to set DPCD link rate\n");
+		return ret;
+	}
+
+	ret = spacemit_dp_link_train_clock_recovery(dp, rate, lanes);
+	if (ret)
+		return ret;
+
+	ret = spacemit_dp_link_train_channel_eq(dp, rate, lanes);
+	if (ret)
+		return ret;
+
+	return 0;
+}
+
+static void spacemit_dp_hw_set_msa_and_enable_video(struct spacemit_dp_dev *dp,
+						    const struct drm_display_mode *mode,
+					       enum spacemit_dp_link_rate rate,
+					       enum spacemit_dp_lane_count lanes,
+		u32 color_format)
+{
+	u64 hb_num;
+	u32 link_rate;
+	u32 fp; /* Pixel clock in MHz */
+	u32 bpp, misc0;
+	u32 tu, tu_frac, tu_int, rd_thres;
+	u32 hsync_len;
+
+	/* mode->clock unit is kHz, fp unit is MHz */
+	if (dp->use_ext_pixel_clock)
+		fp = dp->pixel_clock / 1000;
+	else
+		fp = mode->clock / 1000;
+
+	if (fp == 0)
+		fp = 1; /* Prevent division by zero */
+
+	bpp = spacemit_dp_get_bpp(dp, color_format);
+
+	/* Calculate MISC0 */
+	/* bit0: 0 (Sync Clock) */
+	/* bits1-7: Color Format (000=RGB, 001=YCbCr422, 010=YCbCr444) */
+	/* bits5-7: BPC (001=8bpc, 010=10bpc, etc) */
+	switch (color_format) {
+	case SOC_VIDEO_RGB_6BIT:
+		misc0 = 0x00;
+		break;
+	case SOC_VIDEO_RGB_8BIT:
+		misc0 = 0x20;
+		break;
+	case SOC_VIDEO_RGB_10BIT:
+		misc0 = 0x40;
+		break;
+	case SOC_VIDEO_RGB_12BIT:
+		misc0 = 0x60;
+		break;
+	case SOC_VIDEO_RGB_16BIT:
+		misc0 = 0x80;
+		break;
+	case SOC_VIDEO_YUV422_8BIT:
+		misc0 = 0x22;
+		break;
+	case SOC_VIDEO_YUV422_10BIT:
+		misc0 = 0x42;
+		break;
+	case SOC_VIDEO_YUV422_12BIT:
+		misc0 = 0x62;
+		break;
+	case SOC_VIDEO_YUV422_16BIT:
+		misc0 = 0x82;
+		break;
+	case SOC_VIDEO_YUV444_8BIT:
+		misc0 = 0x24;
+		break;
+	case SOC_VIDEO_YUV444_10BIT:
+		misc0 = 0x44;
+		break;
+	case SOC_VIDEO_YUV444_12BIT:
+		misc0 = 0x64;
+		break;
+	case SOC_VIDEO_YUV444_16BIT:
+		misc0 = 0x84;
+		break;
+	default:
+		misc0 = 0x20;
+		break;
+	}
+
+	/*
+	 * HBlank interval = (htotal - hactive) * (LinkSymbolClock / 4) /
+	 * PixelClock, where LinkSymbolClock = LinkRate * 100 (1.62G -> 162MHz).
+	 * rate is in kHz, so link_rate = rate / 10000 gives the 162, 270, 540
+	 * form used below.
+	 */
+	link_rate = rate / 10000;
+
+	/* Multiply before dividing: hb_num = hblank * (link_rate / 4) / fp */
+	hb_num = (u64)(mode->htotal - mode->hdisplay) * link_rate;
+	do_div(hb_num, 4 * fp);
+
+	/* Transfer unit: tu = fp * bpp * 640 / (8 * lanes * link_rate) */
+	{
+		u64 temp_tu = (u64)fp * bpp * 640;
+		u32 den = 8 * lanes * link_rate;
+
+		do_div(temp_tu, den);
+		tu = temp_tu;
+	}
+	tu_frac = tu % 10;
+	tu_int  = tu / 10;
+
+	/* FIFO read threshold. */
+	if (tu_int < 6)
+		rd_thres = 32;
+	else if ((mode->htotal - mode->hdisplay) < 80)
+		rd_thres = 12;
+	else
+		rd_thres = 16;
+
+	dev_dbg(dp->dev,
+		"MSA: %dx%d, Rate:%d kHz, Lanes:%d, BPP:%d, TU:%d.%d\n",
+		mode->hdisplay, mode->vdisplay, rate, lanes, bpp, tu_int,
+		tu_frac);
+
+	regmap_write_bits(dp->regs, DP_VIDEO_VSAMPLE_REG, DP_VIDEO_MAPPING,
+			  FIELD_PREP(DP_VIDEO_MAPPING, color_format));
+
+	/*
+	 * The Saturn TMG raster is always positive-sync (hsp/vsp fixed to 1).
+	 */
+	regmap_write_bits(dp->regs, DP_VINPUT_POLARITY, DP_HSYNC_IN_POLARITY,
+			  FIELD_PREP(DP_HSYNC_IN_POLARITY, 1));
+	regmap_write_bits(dp->regs, DP_VINPUT_POLARITY, DP_VSYNC_IN_POLARITY,
+			  FIELD_PREP(DP_VSYNC_IN_POLARITY, 1));
+
+	/* Basic timing */
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG1, DP_HACTIVE,
+			  FIELD_PREP(DP_HACTIVE, mode->hdisplay));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG2, DP_VACTIVE,
+			  FIELD_PREP(DP_VACTIVE, mode->vdisplay));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG1, DP_HBLANK,
+			  FIELD_PREP(DP_HBLANK, mode->htotal - mode->hdisplay));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG2, DP_VBLANK,
+			  FIELD_PREP(DP_VBLANK, mode->vtotal - mode->vdisplay));
+
+	u32 hstart = mode->htotal - mode->hsync_end +
+			(mode->hsync_end - mode->hsync_start);
+	u32 vstart = mode->vtotal - mode->vsync_end +
+			(mode->vsync_end - mode->vsync_start);
+
+	regmap_write_bits(dp->regs, DP_VIDEO_MSA1, DP_HSTART,
+			  FIELD_PREP(DP_HSTART, hstart));
+	regmap_write_bits(dp->regs, DP_VIDEO_MSA1, DP_VSTART,
+			  FIELD_PREP(DP_VSTART, vstart));
+
+	hsync_len = mode->hsync_end - mode->hsync_start;
+
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG3, DP_H_SYNC_WIDTH,
+			  FIELD_PREP(DP_H_SYNC_WIDTH, hsync_len));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG4, DP_V_SYNC_WIDTH,
+			  FIELD_PREP(DP_V_SYNC_WIDTH, mode->vsync_end - mode->vsync_start));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG3, DP_H_FRONT_PORCH,
+			  FIELD_PREP(DP_H_FRONT_PORCH, mode->hsync_start - mode->hdisplay));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG4, DP_V_FRONT_PORCH,
+			  FIELD_PREP(DP_V_FRONT_PORCH, mode->vsync_start - mode->vdisplay));
+
+	/* MSA and MISC */
+	regmap_write_bits(dp->regs, DP_VIDEO_MSA2, DP_MISC0,
+			  FIELD_PREP(DP_MISC0, misc0));
+	regmap_write_bits(dp->regs, DP_VIDEO_MSA3, DP_MISC1,
+			  FIELD_PREP(DP_MISC1, 0));
+	regmap_write_bits(dp->regs, DP_VIDEO_MSA3, DP_NVID,
+			  FIELD_PREP(DP_NVID, 0));
+
+	/* Link layer parameters */
+	regmap_write_bits(dp->regs, DP_VIDEO_HBLANK_INTERVAL,
+			  DP_HBLANK_INTERVAL,
+			  FIELD_PREP(DP_HBLANK_INTERVAL, (u32)hb_num));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG5, DP_AVERAGE_BYTES_PER_TU,
+			  FIELD_PREP(DP_AVERAGE_BYTES_PER_TU, tu_int));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG5,
+			  DP_AVERAGE_BYTES_PER_TU_FRAC,
+			  FIELD_PREP(DP_AVERAGE_BYTES_PER_TU_FRAC, tu_frac));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG5, DP_INIT_THRESHOLD,
+			  FIELD_PREP(DP_INIT_THRESHOLD, rd_thres));
+
+	regmap_write_bits(dp->regs, DP_PHYIF_CTRL_ADDR, DP_PHY_SSC_DIS,
+			  FIELD_PREP(DP_PHY_SSC_DIS, 1));
+	regmap_write_bits(dp->regs, DP_CLK_INV_MUX, DP_REG_VID_CLK_SEL,
+			  FIELD_PREP(DP_REG_VID_CLK_SEL, 0));
+	regmap_write_bits(dp->regs, DP_VIDEO_COLORBAR_CFG, DP_VID_BIST_EN,
+			  FIELD_PREP(DP_VID_BIST_EN, 0));
+
+	dev_dbg(dp->dev, "enabling video stream\n");
+	regmap_write_bits(dp->regs, DP_VIDEO_VSAMPLE_REG,
+			  DP_VIDEO_STREAM_ENABLE,
+			  FIELD_PREP(DP_VIDEO_STREAM_ENABLE, 1));
+}
+
+static void spacemit_dp_hw_disable(struct spacemit_dp_dev *dp)
+{
+	dev_dbg(dp->dev, "disabling video and PHY\n");
+
+	regmap_write_bits(dp->regs, DP_VIDEO_VSAMPLE_REG,
+			  DP_VIDEO_STREAM_ENABLE,
+			  FIELD_PREP(DP_VIDEO_STREAM_ENABLE, 0));
+
+	regmap_write_bits(dp->regs, DP_PHYIF_CTRL_ADDR, DP_XMIT_ENABLE,
+			  FIELD_PREP(DP_XMIT_ENABLE, 0));
+
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_LDO_D0,
+			  FIELD_PREP(DP_ANA_TX_EN_LDO_D0, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_LDO_D1,
+			  FIELD_PREP(DP_ANA_TX_EN_LDO_D1, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_LDO_D2,
+			  FIELD_PREP(DP_ANA_TX_EN_LDO_D2, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_LDO_D3,
+			  FIELD_PREP(DP_ANA_TX_EN_LDO_D3, 0));
+
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_DRV_D0,
+			  FIELD_PREP(DP_ANA_TX_EN_DRV_D0, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_DRV_D1,
+			  FIELD_PREP(DP_ANA_TX_EN_DRV_D1, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_DRV_D2,
+			  FIELD_PREP(DP_ANA_TX_EN_DRV_D2, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_DRV_D3,
+			  FIELD_PREP(DP_ANA_TX_EN_DRV_D3, 0));
+
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_EN_P2S_D0,
+			  FIELD_PREP(DP_ANA_TX_EN_P2S_D0, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_EN_P2S_D1,
+			  FIELD_PREP(DP_ANA_TX_EN_P2S_D1, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_EN_P2S_D2,
+			  FIELD_PREP(DP_ANA_TX_EN_P2S_D2, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_EN_P2S_D3,
+			  FIELD_PREP(DP_ANA_TX_EN_P2S_D3, 0));
+
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL6, DP_ANA_BG_EN,
+			  FIELD_PREP(DP_ANA_BG_EN, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_BG_EN_RCAL,
+			  FIELD_PREP(DP_ANA_BG_EN_RCAL, 0));
+
+	/* Both PLLs. */
+	regmap_write_bits(dp->regs, DP_MPLL_CTRL0, DP_ANA_MPLL_PD,
+			  FIELD_PREP(DP_ANA_MPLL_PD, 1));
+	regmap_write_bits(dp->regs, DP_PREPLL_CTRL0, DP_ANA_PREPLL_PD,
+			  FIELD_PREP(DP_ANA_PREPLL_PD, 1));
+}
+
+/* Calculate required bandwidth in kbps (Pixel Clock * Bits Per Pixel) */
+static u32 spacemit_dp_calc_required_bw(const struct drm_display_mode *mode,
+					int bpp)
+{
+	return mode->clock * bpp;
+}
+
+/*
+ * Calculate available link capacity in kbps (taking 8b/10b overhead into
+ * account)
+ */
+static u32 spacemit_dp_calc_link_capacity(enum spacemit_dp_link_rate rate,
+					  enum spacemit_dp_lane_count lanes)
+{
+	u32 rate_khz = rate;
+	u32 lane_count = lanes;
+
+	/* Capacity = Rate(kHz) * Lanes * 0.8 */
+	return (rate_khz * lane_count * 8) / 10;
+}
+
+static enum drm_connector_status
+spacemit_dp_bridge_detect(struct drm_bridge *bridge, struct drm_connector *connector)
+{
+	struct spacemit_dp_dev *dp = container_of(bridge,
+						  struct spacemit_dp_dev,
+						  bridge);
+	enum drm_connector_status status;
+
+	/*
+	 * The eDP panel is fixed, and DP_HPD_IN_STATUS does not read
+	 * meaningfully on that path. Report CONNECTED only once panel-edp has
+	 * joined the chain.
+	 */
+	if (dp->edp_mode) {
+		if (!dp->next_bridge)
+			return connector_status_disconnected;
+		return connector_status_connected;
+	}
+
+	/*
+	 * The HPD interrupt is gated by the DP pixel clock, which is off
+	 * exactly when a plug must be caught. Read the level register live
+	 * instead.
+	 */
+	guard(mutex)(&dp->mode_lock);
+	status = spacemit_dp_hw_detect_hpd(dp);
+
+	/*
+	 * The bind-time cap read only covers a sink that was already there.
+	 * Without a re-read here, link.max_rate stays 0 and every link config
+	 * is filtered out.
+	 */
+	if (status == connector_status_connected &&
+	    dp->connector_status != connector_status_connected)
+		spacemit_dp_hw_read_sink_caps(dp);
+
+	dp->connector_status = status;
+
+	return status;
+}
+
+static int spacemit_dp_conn_get_edid_block(void *data, u8 *buf,
+					   unsigned int block, size_t len)
+{
+	struct spacemit_dp_dev *dp = data;
+	u8 offset = (block & 1) * EDID_LENGTH;
+	u8 segment = block >> 1;
+	struct i2c_msg msgs[] = {
+		{
+			.addr = SPACEMIT_DP_DDC_SEGMENT_ADDR,
+			.len = 1,
+			.buf = &segment,
+		}, {
+			.addr = DDC_ADDR,
+			.len = 1,
+			.buf = &offset,
+		}, {
+			.addr = DDC_ADDR,
+			.flags = I2C_M_RD,
+			.len = len,
+			.buf = buf,
+		},
+	};
+	/* Segment 0 needs no segment-pointer write. */
+	struct i2c_msg *first = segment ? &msgs[0] : &msgs[1];
+	int num = segment ? 3 : 2;
+	int ret, retry;
+
+	/*
+	 * One i2c_transfer per block: the DP I2C-over-AUX core splits it
+	 * into MOT-chained AUX transactions, so the sink sees a repeated
+	 * start and the segment pointer survives until the read ends.
+	 */
+	for (retry = 0; retry < 3; retry++) {
+		ret = i2c_transfer(&dp->aux.ddc, first, num);
+		if (ret == num)
+			return 0;
+	}
+
+	dev_err(dp->dev, "EDID read failed, block %u: %d\n", block, ret);
+	return ret < 0 ? ret : -EIO;
+}
+
+static const struct drm_edid *
+spacemit_dp_bridge_edid_read(struct drm_bridge *bridge, struct drm_connector *connector)
+{
+	struct spacemit_dp_dev *dp = container_of(bridge,
+						  struct spacemit_dp_dev,
+						  bridge);
+	const struct drm_edid *edid = NULL;
+	int retry;
+
+	scoped_guard(mutex, &dp->mode_lock) {
+		for (retry = 0; retry < SPACEMIT_DP_SINK_READY_RETRIES; retry++) {
+			if (retry) {
+				spacemit_dp_aux_hw_reset(dp);
+				msleep(SPACEMIT_DP_SINK_READY_DELAY_MS);
+			}
+
+			edid = drm_edid_read_custom(connector,
+				spacemit_dp_conn_get_edid_block, dp);
+			if (edid)
+				break;
+		}
+	}
+
+	if (!edid)
+		dev_err(dp->dev, "failed to read EDID\n");
+
+	return edid;
+}
+
+static enum drm_mode_status
+spacemit_dp_bridge_mode_valid(struct drm_bridge *bridge,
+			      const struct drm_display_info *info,
+			 const struct drm_display_mode *mode)
+{
+	struct spacemit_dp_dev *dp = container_of(bridge,
+						  struct spacemit_dp_dev,
+						  bridge);
+	unsigned int max_link_rate, max_lanes;
+	u64 max_bw, req_bw;
+	const unsigned int bpp = 24;
+
+	/*
+	 * drm_dp_max_link_rate() is in 10 kHz units; clamp to what the PHY can
+	 * do.
+	 */
+	max_link_rate = min_t(unsigned int, drm_dp_max_link_rate(dp->dpcd),
+			      SPACEMIT_DP_SOURCE_MAX_RATE / 10);
+	max_lanes = drm_dp_max_lane_count(dp->dpcd);
+	if (!max_link_rate || !max_lanes) {
+		/* Sink not enumerated yet -- accept and re-check at modeset. */
+		return MODE_OK;
+	}
+
+	/*
+	 * max_link_rate is per lane in 10 kHz units. The x10 to reach kHz and
+	 * the 8b/10b coding overhead cancel down to a plain "* 8".
+	 */
+	max_bw = (u64)max_link_rate * max_lanes * 8;
+
+	/* Required bandwidth: pixel clock (kHz) * bits per pixel. */
+	req_bw = (u64)mode->clock * bpp;
+
+	if (req_bw > max_bw) {
+		drm_dbg_kms(bridge->dev,
+			    "rejecting %dx%d@%d: %llu > %llu kbit/s (%u kHz x%u lanes)\n",
+			    mode->hdisplay, mode->vdisplay, drm_mode_vrefresh(mode),
+			    req_bw, max_bw, max_link_rate * 10, max_lanes);
+		return MODE_CLOCK_HIGH;
+	}
+
+	return MODE_OK;
+}
+
+static const struct drm_encoder_funcs spacemit_dp_encoder_funcs = {
+	.destroy = drm_encoder_cleanup,
+};
+
+static void spacemit_dp_bridge_atomic_enable(struct drm_bridge *bridge,
+					     struct drm_atomic_commit *state)
+{
+	struct spacemit_dp_dev *dp = container_of(bridge,
+						  struct spacemit_dp_dev,
+						  bridge);
+	struct drm_bridge_state *new_bridge_state =
+		drm_atomic_get_new_bridge_state(state, bridge);
+	struct spacemit_dp_bridge_state *st =
+			to_spacemit_dp_bridge_state(new_bridge_state);
+	struct drm_connector_state *conn_state =
+		drm_atomic_get_new_connector_state(state, dp->connector);
+	struct drm_crtc_state *crtc_state =
+		drm_atomic_get_new_crtc_state(state, conn_state->crtc);
+	struct drm_display_mode *adjusted_mode = &crtc_state->adjusted_mode;
+	const struct spacemit_dp_link_config *cfg;
+	bool trained;
+	u64 clk_val;
+	u64 set_clk_val;
+	u32 req_bw;
+	u32 hpd;
+	int start;
+	int bpp;
+	int i;
+
+	guard(mutex)(&dp->mode_lock);
+	drm_mode_copy(&dp->mode, adjusted_mode);
+
+	/* Enable-time retry for the caps .detect() could not read. */
+	if (!dp->link.max_rate || !dp->link.max_num_lanes)
+		spacemit_dp_hw_read_sink_caps(dp);
+
+	if (dp->use_ext_pixel_clock && dp->pxclk) {
+		set_clk_val = adjusted_mode->clock * 1000;
+		if (set_clk_val) {
+			set_clk_val = clk_round_rate(dp->pxclk, set_clk_val);
+			clk_val = clk_get_rate(dp->pxclk);
+			if (clk_val != set_clk_val) {
+				clk_set_rate(dp->pxclk, set_clk_val);
+				dev_dbg(dp->dev, "set dp pxclk=%lld\n",
+					set_clk_val);
+			}
+		}
+		clk_val = clk_get_rate(dp->pxclk);
+		dp->pixel_clock = clk_val / 1000;
+	}
+
+	bpp = spacemit_dp_get_bpp(dp, st->color_format);
+	req_bw = spacemit_dp_calc_required_bw(adjusted_mode, bpp);
+
+	/*
+	 * Start where atomic_check() left off, but keep the upgrade walk as
+	 * fallback.
+	 */
+	start = st->link_cfg_idx >= 0 ? st->link_cfg_idx : 0;
+
+	for (trained = false, i = start;
+	     i < ARRAY_SIZE(spacemit_dp_link_priority_table); i++) {
+		u32 capacity;
+
+		cfg = &spacemit_dp_link_priority_table[i];
+
+		/* Source and sink limits */
+		if (cfg->rate > dp->link.max_rate || cfg->lanes > dp->link.max_num_lanes)
+			continue;
+
+		/* Bandwidth requirement */
+		capacity = spacemit_dp_calc_link_capacity(cfg->rate,
+							  cfg->lanes);
+		if (capacity < req_bw)
+			continue;
+
+		dev_dbg(dp->dev, "trying link config: rate %d lanes %d (capacity %d, required %d)\n",
+			cfg->rate, cfg->lanes, capacity, req_bw);
+
+		spacemit_dp_link_disable(dp);
+
+		/* Apply Hardware Settings */
+		if (spacemit_dp_set_plls(dp, cfg->rate,
+					 dp->use_ext_pixel_clock ? dp->pixel_clock
+								 : adjusted_mode->clock))
+			continue;
+
+		if (spacemit_dp_phy_set_lanes(dp, cfg->lanes))
+			continue;
+
+		if (spacemit_dp_phy_on(dp))
+			continue;
+
+		/*
+		 * Debounce: the transmitter was power-cycled just now and a
+		 * sink may legitimately drop HPD across a link-down. eDP is
+		 * hardwired and noisy.
+		 */
+		if (!dp->edp_mode &&
+		    regmap_read_poll_timeout(dp->regs, DP_HPD_INTERRUPT_STATUS, hpd,
+					     hpd & DP_HPD_IN_STATUS,
+					     SPACEMIT_DP_HPD_POLL_US,
+					     SPACEMIT_DP_HPD_DEBOUNCE_US)) {
+			dev_warn(dp->dev,
+				 "sink is not asserting HPD; not training\n");
+			break;
+		}
+
+		if (dp->edp_mode) {
+			regmap_write_bits(dp->regs, DP_CORE_CONTROL,
+					  DP_ENABLE_EDP,
+					  FIELD_PREP(DP_ENABLE_EDP, 0x1));
+			regmap_write_bits(dp->regs, DP_VIDEO_VSAMPLE_REG,
+					  DP_STREAM_ENC_EN,
+					  FIELD_PREP(DP_STREAM_ENC_EN, 0x1));
+			update_edp_config(dp, true);
+		}
+
+		/* Execute Link Training */
+		if (spacemit_dp_link_train(dp, cfg->rate, cfg->lanes) == 0) {
+			dev_dbg(dp->dev,
+				"link training succeeded: rate %d lanes %d\n",
+				cfg->rate, cfg->lanes);
+			trained = true;
+			break;
+		}
+
+		dev_warn(dp->dev, "link training failed: rate %d lanes %d, trying next config\n",
+			 cfg->rate, cfg->lanes);
+	}
+
+	/*
+	 * The DPU's pixel clock comes from the PHY PLL, so scanning out into an
+	 * untrained link never reaches CFG_RDY and wedges the CRTC on
+	 * flip_done.
+	 */
+	if (!trained) {
+		dev_err(dp->dev,
+			"no usable link config; leaving video disabled (%u kHz x%u lanes)\n",
+			dp->link.max_rate, dp->link.max_num_lanes);
+		/*
+		 * The caps may be from a sink that has since been swapped out.
+		 */
+		dp->link.max_rate = 0;
+		dp->link.max_num_lanes = 0;
+
+		/*
+		 * Deferred: we are inside the commit and hold the modeset
+		 * locks.
+		 */
+		schedule_work(&dp->modeset_retry_work);
+		return;
+	}
+
+	spacemit_dp_hw_set_msa_and_enable_video(dp, adjusted_mode,
+						dp->link_rate, dp->lane_count,
+						st->color_format);
+}
+
+static void spacemit_dp_bridge_atomic_disable(struct drm_bridge *bridge,
+					      struct drm_atomic_commit *state)
+{
+	struct spacemit_dp_dev *dp = container_of(bridge,
+						  struct spacemit_dp_dev,
+						  bridge);
+
+	guard(mutex)(&dp->mode_lock);
+
+	spacemit_dp_link_disable(dp);
+	memset(&dp->mode, 0, sizeof(dp->mode));
+}
+
+static int spacemit_dp_bridge_atomic_check(struct drm_bridge *bridge,
+					   struct drm_bridge_state *bridge_state,
+				      struct drm_crtc_state *crtc_state,
+				      struct drm_connector_state *conn_state)
+{
+	struct spacemit_dp_dev *dp = container_of(bridge,
+						  struct spacemit_dp_dev,
+						  bridge);
+	struct spacemit_dp_bridge_state *st =
+			to_spacemit_dp_bridge_state(bridge_state);
+	const struct drm_display_mode *mode = &crtc_state->adjusted_mode;
+	int bpp;
+	u32 req_bw;
+	int i;
+
+	/* A single output bus format, see ->atomic_get_output_bus_fmts(). */
+	st->color_format = SOC_VIDEO_RGB_8BIT;
+	bpp = spacemit_dp_get_bpp(dp, st->color_format);
+	req_bw = spacemit_dp_calc_required_bw(mode, bpp);
+
+	/* No caps yet: leave idx unset and let ->atomic_enable() retry. */
+	if (!dp->link.max_rate || !dp->link.max_num_lanes) {
+		st->link_cfg_idx = -1;
+		return 0;
+	}
+
+	for (i = 0; i < ARRAY_SIZE(spacemit_dp_link_priority_table); i++) {
+		const struct spacemit_dp_link_config *cfg =
+				&spacemit_dp_link_priority_table[i];
+
+		if (cfg->rate > dp->link.max_rate ||
+		    cfg->lanes > dp->link.max_num_lanes)
+			continue;
+		if (spacemit_dp_calc_link_capacity(cfg->rate, cfg->lanes) < req_bw)
+			continue;
+
+		st->link_cfg_idx = i;
+		return 0;
+	}
+
+	drm_dbg_kms(bridge->dev,
+		    "no DP link cfg fits %dx%d@%d (need %u kbit/s; sink %u kHz x %u lanes)\n",
+		    mode->hdisplay, mode->vdisplay, drm_mode_vrefresh(mode),
+		    req_bw, dp->link.max_rate * 10, dp->link.max_num_lanes);
+	return -EINVAL;
+}
+
+static struct drm_bridge_state *
+spacemit_dp_bridge_atomic_duplicate_state(struct drm_bridge *bridge)
+{
+	struct spacemit_dp_bridge_state *cur =
+		to_spacemit_dp_bridge_state(drm_priv_to_bridge_state(bridge->base.state));
+	struct spacemit_dp_bridge_state *new;
+
+	new = kzalloc_obj(*new);
+	if (!new)
+		return NULL;
+
+	__drm_atomic_helper_bridge_duplicate_state(bridge, &new->base);
+	new->link_cfg_idx = cur->link_cfg_idx;
+	new->color_format = cur->color_format;
+	return &new->base;
+}
+
+static void
+spacemit_dp_bridge_atomic_destroy_state(struct drm_bridge *bridge,
+					struct drm_bridge_state *state)
+{
+	kfree(to_spacemit_dp_bridge_state(state));
+}
+
+static u32 *spacemit_dp_bridge_atomic_get_output_bus_fmts(struct drm_bridge *bridge,
+							  struct drm_bridge_state *bridge_state,
+				struct drm_crtc_state *crtc_state,
+				struct drm_connector_state *conn_state,
+				unsigned int *num_output_fmts)
+{
+	u32 *output_fmts;
+
+	output_fmts = kmalloc_obj(*output_fmts, GFP_KERNEL);
+	if (!output_fmts) {
+		*num_output_fmts = 0;
+		return NULL;
+	}
+
+	output_fmts[0] = MEDIA_BUS_FMT_RGB888_1X24;
+	*num_output_fmts = 1;
+	return output_fmts;
+}
+
+static struct drm_bridge_state *
+spacemit_dp_bridge_atomic_create_state(struct drm_bridge *bridge)
+{
+	struct spacemit_dp_bridge_state *new;
+
+	new = kzalloc_obj(*new);
+	if (!new)
+		return ERR_PTR(-ENOMEM);
+
+	__drm_atomic_helper_bridge_state_init(&new->base, bridge);
+	new->link_cfg_idx = -1;
+	new->color_format = SOC_VIDEO_RGB_8BIT;
+	return &new->base;
+}
+
+static int spacemit_dp_bridge_attach(struct drm_bridge *bridge,
+				     struct drm_encoder *encoder,
+				enum drm_bridge_attach_flags flags)
+{
+	struct spacemit_dp_dev *dp = container_of(bridge,
+						  struct spacemit_dp_dev,
+						  bridge);
+
+	if (!dp->next_bridge)
+		return 0;
+
+	return drm_bridge_attach(encoder, dp->next_bridge, bridge, flags);
+}
+
+/*
+ * AUX-bus done-probing callback. panel-edp may probe inline (before .attach) or
+ * off the deferred-probe queue (after it), so handle both orders.
+ */
+static int spacemit_dp_edp_link_panel(struct drm_dp_aux *aux)
+{
+	struct spacemit_dp_dev *dp = container_of(aux, struct spacemit_dp_dev,
+						  aux);
+	struct drm_bridge *next;
+	int ret;
+
+	next = devm_drm_of_get_bridge(dp->dev, dp->dev->of_node, 1, 0);
+	if (IS_ERR(next))
+		return PTR_ERR(next);
+
+	dp->next_bridge = next;
+
+	/*
+	 * A non-NULL bridge.encoder means .attach already ran: late-chain
+	 * panel-edp.
+	 */
+	if (dp->bridge.encoder) {
+		ret = drm_bridge_attach(dp->bridge.encoder, next,
+					&dp->bridge,
+					DRM_BRIDGE_ATTACH_NO_CONNECTOR);
+		if (ret)
+			return ret;
+
+		/*
+		 * eDP HPD-IN is unreliable, so bind may have skipped the cap
+		 * read and left link.max_rate at 0. The AUX bus just worked;
+		 * read them here.
+		 */
+		if (dp->edp_mode) {
+			guard(mutex)(&dp->mode_lock);
+			spacemit_dp_hw_read_sink_caps(dp);
+		}
+
+		drm_bridge_hpd_notify(&dp->bridge,
+				      spacemit_dp_bridge_detect(&dp->bridge,
+								dp->connector));
+	}
+
+	return 0;
+}
+
+static const struct drm_bridge_funcs spacemit_dp_bridge_funcs = {
+	.attach				= spacemit_dp_bridge_attach,
+	.atomic_enable			= spacemit_dp_bridge_atomic_enable,
+	.atomic_disable			= spacemit_dp_bridge_atomic_disable,
+	.atomic_check			= spacemit_dp_bridge_atomic_check,
+	.atomic_duplicate_state		= spacemit_dp_bridge_atomic_duplicate_state,
+	.atomic_destroy_state		= spacemit_dp_bridge_atomic_destroy_state,
+	.atomic_create_state		= spacemit_dp_bridge_atomic_create_state,
+	.atomic_get_output_bus_fmts	= spacemit_dp_bridge_atomic_get_output_bus_fmts,
+	.mode_valid			= spacemit_dp_bridge_mode_valid,
+	.detect				= spacemit_dp_bridge_detect,
+	.edid_read			= spacemit_dp_bridge_edid_read,
+};
+
+static irqreturn_t spacemit_dp_irq_handler(int irq, void *data)
+{
+	struct spacemit_dp_dev *dp = data;
+	irqreturn_t ret = IRQ_NONE;
+	int idx;
+
+	if (!drm_dev_enter(dp->drm, &idx))
+		return IRQ_NONE;
+
+	/*
+	 * eDP .detect() ignores the HPD level (and never records it), so a
+	 * level flip would wake the thread forever; ack and ignore.
+	 */
+	if (dp->edp_mode) {
+		spacemit_dp_hw_clean_hpd(dp);
+		drm_dev_exit(idx);
+		return IRQ_HANDLED;
+	}
+
+	/*
+	 * Only compare here: the authoritative connector_status update
+	 * happens in the thread under mode_lock, via .detect().
+	 */
+	if (spacemit_dp_hw_detect_hpd(dp) != READ_ONCE(dp->connector_status))
+		ret = IRQ_WAKE_THREAD; /* Call hotplug_event */
+	if (spacemit_dp_hw_clean_hpd(dp) && ret == IRQ_NONE)
+		ret = IRQ_HANDLED;
+
+	drm_dev_exit(idx);
+	return ret;
+}
+
+static irqreturn_t spacemit_dp_hotplug_event_handler(int irq, void *data)
+{
+	struct spacemit_dp_dev *dp = data;
+	enum drm_connector_status status;
+	int idx;
+
+	if (!drm_dev_enter(dp->drm, &idx))
+		return IRQ_HANDLED;
+
+	/*
+	 * .detect() owns mode_lock and re-reads the sink caps on a fresh plug
+	 */
+	status = spacemit_dp_bridge_detect(&dp->bridge, dp->connector);
+	drm_bridge_hpd_notify(&dp->bridge, status);
+
+	drm_dev_exit(idx);
+	return IRQ_HANDLED;
+}
+
+static int spacemit_dp_resource_init(struct spacemit_dp_dev *dp,
+				     struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	void __iomem *base;
+
+	base = devm_platform_ioremap_resource(pdev, 0);
+	if (IS_ERR(base))
+		return dev_err_probe(dev, PTR_ERR(base),
+				     "failed to map registers\n");
+	dp->regs = devm_regmap_init_mmio(dev, base, &spacemit_dp_regmap_config);
+	if (IS_ERR(dp->regs))
+		return dev_err_probe(dev, PTR_ERR(dp->regs),
+				     "failed to init DP regmap\n");
+
+	/*
+	 * The pixel clock comes from the PHY PLL, so the controller never sets
+	 * it.
+	 */
+	dp->use_ext_pixel_clock = false;
+
+	dp->irq = platform_get_irq(pdev, 0);
+	if (dp->irq < 0) {
+		dev_err(dev, "failed to get IRQ\n");
+		return dp->irq;
+	}
+
+	return 0;
+}
+
+static int spacemit_dp_dev_init(struct spacemit_dp_dev *dp)
+{
+	int ret;
+	u32 m_isel = 0x5, m_mainsel = 0x19;
+	u32 m_pre = 0x0, m_post = 0x2;
+	u32 tx_mode = 0x1, tx_pre = 0x0;
+	u32 clk_div = 24 * 1000 / 100;
+
+	regmap_write_bits(dp->regs, DP_PHYIF_CTRL_ADDR, DP_XMIT_ENABLE,
+			  FIELD_PREP(DP_XMIT_ENABLE, 0));
+	usleep_range(2000, 4000);
+
+	regmap_write_bits(dp->regs, DP_MPLL_CTRL0, DP_ANA_MPLL_PD,
+			  FIELD_PREP(DP_ANA_MPLL_PD, 1));
+	regmap_write_bits(dp->regs, DP_PREPLL_CTRL0, DP_ANA_PREPLL_PD,
+			  FIELD_PREP(DP_ANA_PREPLL_PD, 1));
+	usleep_range(2000, 4000);
+
+	/* Reset Controller and PHY */
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_CONTROLLER_RESET,
+			  FIELD_PREP(DP_CONTROLLER_RESET, 0x1));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_PHY_RESET,
+			  FIELD_PREP(DP_PHY_RESET, 0x1));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_HDCP_RESET,
+			  FIELD_PREP(DP_HDCP_RESET, 0x1));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_AUX_RESET,
+			  FIELD_PREP(DP_AUX_RESET, 0x1));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_VIDEO_RESET,
+			  FIELD_PREP(DP_VIDEO_RESET, 0x1));
+	usleep_range(5000, 8000);
+
+	/* Release the resets */
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_CONTROLLER_RESET,
+			  FIELD_PREP(DP_CONTROLLER_RESET, 0x0));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_PHY_RESET,
+			  FIELD_PREP(DP_PHY_RESET, 0x0));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_HDCP_RESET,
+			  FIELD_PREP(DP_HDCP_RESET, 0x0));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_AUX_RESET,
+			  FIELD_PREP(DP_AUX_RESET, 0x0));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_VIDEO_RESET,
+			  FIELD_PREP(DP_VIDEO_RESET, 0x0));
+	usleep_range(2000, 4000);
+
+	regmap_write(dp->regs, DP_GENERAL_INTERRUPT,
+		     DP_AUX_REPLY_EVENT_INT_STA);
+
+	regmap_write_bits(dp->regs, DP_CORE_CONTROL,
+			  DP_DEFAULT_FAST_LINK_TRAIN_EN,
+			  FIELD_PREP(DP_DEFAULT_FAST_LINK_TRAIN_EN, 0x0));
+	regmap_write_bits(dp->regs, DP_CORE_CONTROL, DP_SCRAMBLER_DISABLE,
+			  FIELD_PREP(DP_SCRAMBLER_DISABLE, 0x0));
+	regmap_write_bits(dp->regs, DP_CORE_CONTROL, DP_SCALE_DOWN_MODE,
+			  FIELD_PREP(DP_SCALE_DOWN_MODE, 0x0));
+	regmap_write_bits(dp->regs, DP_PHYIF_CTRL_ADDR, DP_XMIT_ENABLE,
+			  FIELD_PREP(DP_XMIT_ENABLE, 0));
+
+	regmap_write_bits(dp->regs, DP_VIDEO_VSAMPLE_REG,
+			  DP_VIDEO_STREAM_ENABLE,
+			  FIELD_PREP(DP_VIDEO_STREAM_ENABLE, 0));
+
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_AUX_REPLY_EVENT_INT_STA_MSK,
+			  FIELD_PREP(DP_AUX_REPLY_EVENT_INT_STA_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_HDCP_INT_STA_MSK,
+			  FIELD_PREP(DP_HDCP_INT_STA_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_ILLEGAL_AUX_CMD_INT_STA_MSK,
+			  FIELD_PREP(DP_ILLEGAL_AUX_CMD_INT_STA_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_TYPE_C_EVENT_MSK,
+			  FIELD_PREP(DP_TYPE_C_EVENT_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK, DP_DSC_EVENT_MSK,
+			  FIELD_PREP(DP_DSC_EVENT_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_SDP_INT_STA_S3_MSK,
+			  FIELD_PREP(DP_SDP_INT_STA_S3_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_SDP_INT_STA_S2_MSK,
+			  FIELD_PREP(DP_SDP_INT_STA_S2_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_SDP_INT_STA_S1_MSK,
+			  FIELD_PREP(DP_SDP_INT_STA_S1_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_SDP_INT_STA_S0_MSK,
+			  FIELD_PREP(DP_SDP_INT_STA_S0_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+		DP_VIDEO_FIFO_OVERFLOW_INT_STA_S3_MSK,
+		FIELD_PREP(DP_VIDEO_FIFO_OVERFLOW_INT_STA_S3_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+		DP_VIDEO_FIFO_OVERFLOW_INT_STA_S2_MSK,
+		FIELD_PREP(DP_VIDEO_FIFO_OVERFLOW_INT_STA_S2_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+		DP_VIDEO_FIFO_OVERFLOW_INT_STA_S1_MSK,
+		FIELD_PREP(DP_VIDEO_FIFO_OVERFLOW_INT_STA_S1_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+		DP_VIDEO_FIFO_OVERFLOW_INT_STA_S0_MSK,
+		FIELD_PREP(DP_VIDEO_FIFO_OVERFLOW_INT_STA_S0_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_HPD_INTERRUPT_ENABLE,
+			  DP_SINK_IRQ_EVENT_MSK,
+			  FIELD_PREP(DP_SINK_IRQ_EVENT_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_HPD_INT_STA_MSK,
+			  FIELD_PREP(DP_HPD_INT_STA_MSK, 0x1));
+	regmap_write_bits(dp->regs, DP_HPD_INTERRUPT_ENABLE,
+			  DP_HOT_PLUG_EVENT_MSK,
+			  FIELD_PREP(DP_HOT_PLUG_EVENT_MSK, 0x1));
+	regmap_write_bits(dp->regs, DP_HPD_INTERRUPT_ENABLE,
+			  DP_HOT_UNPLUG_EVENT_MSK,
+			  FIELD_PREP(DP_HOT_UNPLUG_EVENT_MSK, 0x1));
+	regmap_write_bits(dp->regs, DP_HPD_INTERRUPT_ENABLE,
+			  DP_SINK_UNPLUG_ERROR_EVENT_MSK,
+			  FIELD_PREP(DP_SINK_UNPLUG_ERROR_EVENT_MSK, 0x0));
+	usleep_range(2000, 4000);
+
+	regmap_write_bits(dp->regs, DP_PHYIF_CTRL_ADDR, DP_PHY_BUSY_BYP,
+			  FIELD_PREP(DP_PHY_BUSY_BYP, 0x1));
+
+	/* Enable enhanced framing */
+	regmap_write_bits(dp->regs, DP_CORE_CONTROL, DP_ENHANCE_FRAMING_EN,
+			  FIELD_PREP(DP_ENHANCE_FRAMING_EN, 0x1));
+
+	ret = spacemit_dp_set_plls(dp, SPACEMIT_DP_LINK_RATE_2_70,
+				   dp->use_ext_pixel_clock ? 150000 : 148500);
+	if (ret)
+		return ret;
+
+	ret = spacemit_dp_phy_set_lanes(dp, SPACEMIT_DP_LANE_2);
+	if (ret)
+		return ret;
+
+	ret = spacemit_dp_phy_on(dp);
+	if (ret)
+		return ret;
+
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D0,
+			  FIELD_PREP(DP_ANA_TX_MODE_D0, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D1,
+			  FIELD_PREP(DP_ANA_TX_MODE_D1, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D2,
+			  FIELD_PREP(DP_ANA_TX_MODE_D2, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D3,
+			  FIELD_PREP(DP_ANA_TX_MODE_D3, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_RTCAL_FREQDIV_HBIT,
+		FIELD_PREP(DP_ANA_RTCAL_FREQDIV_HBIT, (clk_div >> 8) & 0x7f));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_RTCAL_BYPASS,
+			  FIELD_PREP(DP_ANA_RTCAL_BYPASS, 1));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_RTCAL_FREQDIV_LBIT,
+		FIELD_PREP(DP_ANA_RTCAL_FREQDIV_LBIT, clk_div & 0xff));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_BG_RCAL_SEL,
+			  FIELD_PREP(DP_ANA_BG_RCAL_SEL, 0));
+
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL8, DP_ANA_TX_RTM_D3,
+			  FIELD_PREP(DP_ANA_TX_RTM_D3, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL8, DP_ANA_TX_RTM_D2,
+			  FIELD_PREP(DP_ANA_TX_RTM_D2, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL8, DP_ANA_TX_RTM_D1,
+			  FIELD_PREP(DP_ANA_TX_RTM_D1, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL8, DP_ANA_TX_RTM_D0,
+			  FIELD_PREP(DP_ANA_TX_RTM_D0, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_RTCAL_BYPASS,
+			  FIELD_PREP(DP_ANA_RTCAL_BYPASS, 1));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_RTCAL_BYPASS,
+			  FIELD_PREP(DP_ANA_RTCAL_BYPASS, 0));
+	msleep(100);
+
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_PRE_D3,
+			  FIELD_PREP(DP_ANA_TX_MODE_PRE_D3, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_PRE_D2,
+			  FIELD_PREP(DP_ANA_TX_MODE_PRE_D2, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_PRE_D1,
+			  FIELD_PREP(DP_ANA_TX_MODE_PRE_D1, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_PRE_D0,
+			  FIELD_PREP(DP_ANA_TX_MODE_PRE_D0, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_DE_D3,
+			  FIELD_PREP(DP_ANA_TX_MODE_DE_D3, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_DE_D2,
+			  FIELD_PREP(DP_ANA_TX_MODE_DE_D2, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_DE_D1,
+			  FIELD_PREP(DP_ANA_TX_MODE_DE_D1, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_DE_D0,
+			  FIELD_PREP(DP_ANA_TX_MODE_DE_D0, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_POSTSEL_PRE_D3,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_PRE_D3, tx_pre));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_POSTSEL_PRE_D2,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_PRE_D2, tx_pre));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_POSTSEL_PRE_D1,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_PRE_D1, tx_pre));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_POSTSEL_PRE_D0,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_PRE_D0, tx_pre));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_ISEL_DRV_D3,
+			  FIELD_PREP(DP_ANA_TX_ISEL_DRV_D3, m_isel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_ISEL_DRV_D2,
+			  FIELD_PREP(DP_ANA_TX_ISEL_DRV_D2, m_isel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL1, DP_ANA_TX_MAINSEL_D2,
+			  FIELD_PREP(DP_ANA_TX_MAINSEL_D2, m_mainsel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL1, DP_ANA_TX_MAINSEL_D3,
+			  FIELD_PREP(DP_ANA_TX_MAINSEL_D3, m_mainsel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL1, DP_ANA_TX_ISEL_DRV_D1,
+			  FIELD_PREP(DP_ANA_TX_ISEL_DRV_D1, m_isel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL1, DP_ANA_TX_ISEL_DRV_D0,
+			  FIELD_PREP(DP_ANA_TX_ISEL_DRV_D0, m_isel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL2, DP_ANA_TX_POSTSEL_D1,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_D1, m_post));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL2, DP_ANA_TX_POSTSEL_D0,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_D0, m_post));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL2, DP_ANA_TX_POSTSEL_D3,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_D3, m_post));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL2, DP_ANA_TX_POSTSEL_D2,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_D2, m_post));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL2, DP_DA_TX_MAINSEL_D0_4_0,
+			  FIELD_PREP(DP_DA_TX_MAINSEL_D0_4_0, m_mainsel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL2, DP_ANA_TX_MAINSEL_D1,
+			  FIELD_PREP(DP_ANA_TX_MAINSEL_D1, m_mainsel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_PRESEL_D1,
+			  FIELD_PREP(DP_ANA_TX_PRESEL_D1, m_pre));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_PRESEL_D0,
+			  FIELD_PREP(DP_ANA_TX_PRESEL_D0, m_pre));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_PRESEL_D3,
+			  FIELD_PREP(DP_ANA_TX_PRESEL_D3, m_pre));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_PRESEL_D2,
+			  FIELD_PREP(DP_ANA_TX_PRESEL_D2, m_pre));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D3,
+			  FIELD_PREP(DP_ANA_TX_MODE_D3, tx_mode));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D2,
+			  FIELD_PREP(DP_ANA_TX_MODE_D2, tx_mode));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D1,
+			  FIELD_PREP(DP_ANA_TX_MODE_D1, tx_mode));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D0,
+			  FIELD_PREP(DP_ANA_TX_MODE_D0, tx_mode));
+	regmap_write_bits(dp->regs, DP_ANA_TX_AUX_CTRL, DP_ANA_TX_AUX_RX_VSEL,
+			  FIELD_PREP(DP_ANA_TX_AUX_RX_VSEL, 0x0));
+
+	return 0;
+}
+
+static int spacemit_dp_bind(struct device *dev, struct device *master,
+			    void *data)
+{
+	struct spacemit_dp_dev *dp = dev_get_drvdata(dev);
+	struct drm_device *drm = data;
+	struct device_node *ep;
+	int ret;
+
+	dp->drm = drm;
+	dp->aux.drm_dev = drm;
+	ret = drm_dp_aux_register(&dp->aux);
+	if (ret) {
+		dev_err(dev, "failed to register DP AUX: %d\n", ret);
+		return ret;
+	}
+
+	ret = drm_encoder_init(drm, &dp->encoder, &spacemit_dp_encoder_funcs,
+			       DRM_MODE_ENCODER_NONE, NULL);
+	if (ret) {
+		dev_err(dev, "failed to init encoder\n");
+		goto err_aux;
+	}
+
+	ep = of_graph_get_endpoint_by_regs(dev->of_node, 0, -1);
+	if (ep) {
+		struct device_node *remote = of_graph_get_remote_port(ep);
+
+		dp->encoder.possible_crtcs = drm_of_crtc_port_mask(drm, remote);
+		of_node_put(remote);
+		of_node_put(ep);
+	}
+
+	ret = drm_bridge_attach(&dp->encoder, &dp->bridge, NULL,
+				DRM_BRIDGE_ATTACH_NO_CONNECTOR);
+	if (ret) {
+		dev_err(dev, "failed to attach bridge: %d\n", ret);
+		goto err_encoder;
+	}
+
+	dp->connector = drm_bridge_connector_init(drm, &dp->encoder);
+	if (IS_ERR(dp->connector)) {
+		ret = PTR_ERR(dp->connector);
+		dev_err(dev, "failed to init bridge connector: %d\n", ret);
+		goto err_encoder;
+	}
+
+	drm_connector_attach_encoder(dp->connector, &dp->encoder);
+
+	ret = spacemit_dp_dev_init(dp);
+	if (ret)
+		goto err_encoder;
+
+	spacemit_dp_link_disable(dp);
+
+	dp->connector_status = spacemit_dp_hw_detect_hpd(dp);
+	spacemit_dp_hw_clean_hpd(dp);
+
+	if (dp->connector_status == connector_status_connected)
+		spacemit_dp_hw_read_sink_caps(dp);
+
+	/*
+	 * With panel-edp still deferred the chain is incomplete: a CONNECTED
+	 * notification here makes fbdev commit a modeset that wedges on
+	 * flip_done.
+	 */
+	if (dp->next_bridge || !dp->edp_mode)
+		drm_bridge_hpd_notify(&dp->bridge,
+				      spacemit_dp_bridge_detect(&dp->bridge,
+								dp->connector));
+
+	enable_irq(dp->irq);
+
+	return 0;
+
+err_encoder:
+	drm_encoder_cleanup(&dp->encoder);
+err_aux:
+	drm_dp_aux_unregister(&dp->aux);
+	return ret;
+}
+
+static void spacemit_dp_unbind(struct device *dev, struct device *master,
+			       void *data)
+{
+	struct spacemit_dp_dev *dp = dev_get_drvdata(dev);
+
+	disable_irq(dp->irq);
+
+	/* The connector this work touches dies with the DRM device. */
+	cancel_work_sync(&dp->modeset_retry_work);
+
+	drm_dp_aux_unregister(&dp->aux);
+
+	spacemit_dp_hw_disable(dp);
+
+	drm_encoder_cleanup(&dp->encoder);
+
+	dp->drm = NULL;
+	dp->connector = NULL;
+	dp->aux.drm_dev = NULL;
+}
+
+static const struct component_ops spacemit_dp_ops = {
+	.bind = spacemit_dp_bind,
+	.unbind = spacemit_dp_unbind,
+};
+
+/*
+ * Registered after devm_of_platform_populate(), so devres runs it before
+ * the PHY child (and the PLL clock it provides) is torn down: unregistering
+ * a clock that still has an enabled child warns, a disabled child is
+ * orphaned silently.
+ */
+static void spacemit_dp_pxclk_disable(void *data)
+{
+	clk_disable_unprepare(data);
+}
+
+static int inno_dp_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct device_node *aux_bus_np;
+	struct spacemit_dp_dev *dp;
+	struct reset_control *rstc;
+	struct resource *res;
+	int ret;
+
+	dp = devm_drm_bridge_alloc(dev, struct spacemit_dp_dev, bridge,
+				   &spacemit_dp_bridge_funcs);
+	if (IS_ERR(dp))
+		return PTR_ERR(dp);
+
+	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
+	if (!res)
+		return dev_err_probe(dev, -ENODEV,
+				     "no MMIO resource\n");
+
+	dp->dev = dev;
+	INIT_WORK(&dp->modeset_retry_work, spacemit_dp_modeset_retry_work);
+
+	/* eDP instances have an aux-bus child node; DP instances do not. */
+	aux_bus_np = of_get_child_by_name(dev->of_node, "aux-bus");
+	dp->edp_mode = !!aux_bus_np;
+	of_node_put(aux_bus_np);
+
+	dp->bridge.of_node = dev->of_node;
+	dp->connector_status = connector_status_disconnected;
+	ret = devm_mutex_init(dev, &dp->mode_lock);
+	if (ret)
+		return ret;
+
+	rstc = devm_reset_control_get_exclusive_deasserted(&pdev->dev, NULL);
+	if (IS_ERR(rstc)) {
+		ret = dev_err_probe(dev, PTR_ERR(rstc),
+				    "failed to get reset\n");
+		return ret;
+	}
+
+	dp->pxclk = devm_clk_get(dev, "pxclk");
+	if (IS_ERR(dp->pxclk)) {
+		ret = dev_err_probe(dev, PTR_ERR(dp->pxclk),
+				    "failed to get pxclk\n");
+		return ret;
+	}
+
+	if (dp->edp_mode) {
+		dp->bridge.type = DRM_MODE_CONNECTOR_eDP;
+		/*
+		 * OP_DETECT is required: without it
+		 * drm_bridge_connector_detect() hard-codes CONNECTED for any
+		 * eDP connector and fbdev wedges at bind.
+		 */
+		dp->bridge.ops = DRM_BRIDGE_OP_DETECT | DRM_BRIDGE_OP_EDID |
+				 DRM_BRIDGE_OP_HPD;
+	} else {
+		dp->bridge.type = DRM_MODE_CONNECTOR_DisplayPort;
+		/*
+		 * No OP_HPD: the HPD interrupt is gated by the DP pixel clock.
+		 * Leaving it out marks the connector POLL_CONNECT |
+		 * POLL_DISCONNECT instead.
+		 */
+		dp->bridge.ops = DRM_BRIDGE_OP_DETECT | DRM_BRIDGE_OP_EDID;
+	}
+
+	ret = clk_prepare_enable(dp->pxclk);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to enable pxclk\n");
+
+	/*
+	 * The PHY child shares this device's regmap, so the regmap must exist
+	 * before devm_of_platform_populate() lets the PHY probe and
+	 * devm_phy_get() succeed.
+	 */
+	ret = spacemit_dp_resource_init(dp, pdev);
+	if (ret)
+		goto err_disable_pxclk;
+
+	ret = devm_of_platform_populate(dev);
+	if (ret) {
+		dev_err_probe(dev, ret, "failed to populate child devices\n");
+		goto err_disable_pxclk;
+	}
+
+	dp->phy = devm_phy_get(dev, "dp");
+	if (IS_ERR(dp->phy)) {
+		ret = dev_err_probe(dev, PTR_ERR(dp->phy),
+				    "failed to get DP PHY\n");
+		goto err_disable_pxclk;
+	}
+
+	/*
+	 * The PHY exposes its PLL as the APMU pixel-clock mux's external
+	 * parent.
+	 */
+	dp->pll_clk = devm_clk_get(dev, "pll");
+	if (IS_ERR(dp->pll_clk)) {
+		ret = dev_err_probe(dev, PTR_ERR(dp->pll_clk),
+				    "failed to get PHY pixel clock\n");
+		goto err_disable_pxclk;
+	}
+
+	ret = clk_set_parent(dp->pxclk, dp->pll_clk);
+	if (ret) {
+		dev_err(dev, "failed to route eDP pixel mux to PHY PLL: %d\n", ret);
+		goto err_disable_pxclk;
+	}
+
+	ret = devm_add_action_or_reset(dev, spacemit_dp_pxclk_disable,
+				       dp->pxclk);
+	if (ret)
+		return ret;
+
+	/*
+	 * Panel discovery needs the AUX channel here; registration waits for
+	 * bind.
+	 */
+	dp->aux.name = "spacemit-dp-aux";
+	dp->aux.dev = dev;
+	dp->aux.transfer = spacemit_dp_aux_transfer;
+	dp->aux.no_zero_sized = true;
+	drm_dp_aux_init(&dp->aux);
+	dp->bridge.ddc = &dp->aux.ddc;
+
+	/*
+	 * eDP hands panel power, enable-GPIO and backlight to panel-edp over
+	 * the AUX bus. A DP instance has no aux-bus child and takes the
+	 * -ENODEV path.
+	 */
+	if (dp->edp_mode) {
+		ret = devm_of_dp_aux_populate_bus(&dp->aux,
+						  spacemit_dp_edp_link_panel);
+		if (ret) {
+			dev_err_probe(dev, ret,
+				"eDP: failed to populate aux-bus panel\n");
+			return ret;
+		}
+	}
+
+	/*
+	 * drm_bridge_attach() expects the bridge to already be on the global
+	 * list.
+	 */
+	ret = devm_drm_bridge_add(dev, &dp->bridge);
+	if (ret) {
+		dev_err(dev, "failed to register DP bridge: %d\n", ret);
+		return ret;
+	}
+
+	platform_set_drvdata(pdev, dp);
+
+	/*
+	 * The handler needs bind-created state, so keep the line disabled until
+	 * bind.
+	 */
+	ret = devm_request_threaded_irq(dev, dp->irq, spacemit_dp_irq_handler,
+					spacemit_dp_hotplug_event_handler,
+					IRQF_NO_AUTOEN, dev_name(dev), dp);
+	if (ret)
+		return ret;
+
+	ret = component_add(dev, &spacemit_dp_ops);
+	if (ret) {
+		dev_err(dev, "failed to add component: %d\n", ret);
+		return ret;
+	}
+
+	return 0;
+
+err_disable_pxclk:
+	clk_disable_unprepare(dp->pxclk);
+	return ret;
+}
+
+static void inno_dp_remove(struct platform_device *pdev)
+{
+	component_del(&pdev->dev, &spacemit_dp_ops);
+}
+
+static void inno_dp_shutdown(struct platform_device *pdev)
+{
+	struct spacemit_dp_dev *dp = platform_get_drvdata(pdev);
+
+	/* A deferred or failed bind leaves drvdata NULL. */
+	if (!dp)
+		return;
+
+	spacemit_dp_hw_disable(dp);
+}
+
+static const struct of_device_id spacemit_dp_match[] = {
+	{ .compatible = "spacemit,k3-inno-dp" },
+	{}
+};
+MODULE_DEVICE_TABLE(of, spacemit_dp_match);
+
+static struct platform_driver inno_dp_driver = {
+	.probe = inno_dp_probe,
+	.remove = inno_dp_remove,
+	.shutdown = inno_dp_shutdown,
+	.driver = {
+		.name = "spacemit-inno-dp-drv",
+		.of_match_table = spacemit_dp_match,
+	},
+};
+
+module_platform_driver(inno_dp_driver);
+
+MODULE_DESCRIPTION("SpacemiT K3 Innosilicon DP/eDP bridge driver");
+MODULE_LICENSE("GPL");
+MODULE_SOFTDEP("pre: phy-k3-inno-dp");
diff --git a/drivers/gpu/drm/spacemit/spacemit_inno_dp.h b/drivers/gpu/drm/spacemit/spacemit_inno_dp.h
new file mode 100644
index 0000000000000..680e2f41cd486
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/spacemit_inno_dp.h
@@ -0,0 +1,329 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ */
+
+#ifndef __SPACEMIT_INNO_DP_H__
+#define __SPACEMIT_INNO_DP_H__
+
+/* Register field access uses GENMASK/BIT + FIELD_PREP/FIELD_GET. */
+#include <linux/bits.h>
+
+#define DP_CORE_CONTROL                      0x0018
+#define  DP_SCRAMBLER_DISABLE               BIT(31)
+#define  DP_ENHANCE_FRAMING_EN              BIT(30)
+#define  DP_DEFAULT_FAST_LINK_TRAIN_EN      BIT(29)
+#define  DP_SCALE_DOWN_MODE                 BIT(28)
+#define  DP_FORCE_HPD                       BIT(27)
+#define  DP_ENABLE_EDP                      BIT(4)
+
+#define DP_SOFT_RESET                        0x001c
+#define  DP_CONTROLLER_RESET                BIT(31)
+#define  DP_PHY_RESET                       BIT(30)
+#define  DP_HDCP_RESET                      BIT(29)
+#define  DP_AUDIO_RESET                     BIT(28)
+#define  DP_AUX_RESET                       BIT(27)
+#define  DP_VIDEO_RESET                     GENMASK(3, 0)
+
+#define DP_CLK_INV_MUX                       0x0028
+#define  DP_REG_VID_CLK_SEL                 BIT(0)
+
+/* Interrupt status latches; write 1 to clear. */
+#define DP_GENERAL_INTERRUPT                 0x0080
+#define  DP_AUX_REPLY_EVENT_INT_STA         BIT(16)
+
+#define DP_GENERAL_INTERRUPT_MASK            0x0084
+#define  DP_HPD_INT_STA_MSK                 BIT(17)
+#define  DP_AUX_REPLY_EVENT_INT_STA_MSK     BIT(16)
+#define  DP_HDCP_INT_STA_MSK                BIT(15)
+#define  DP_ILLEGAL_AUX_CMD_INT_STA_MSK     BIT(14)
+#define  DP_TYPE_C_EVENT_MSK                BIT(13)
+#define  DP_DSC_EVENT_MSK                   BIT(12)
+#define  DP_SDP_INT_STA_S3_MSK              BIT(11)
+#define  DP_SDP_INT_STA_S2_MSK              BIT(10)
+#define  DP_SDP_INT_STA_S1_MSK              BIT(9)
+#define  DP_SDP_INT_STA_S0_MSK              BIT(8)
+#define  DP_VIDEO_FIFO_OVERFLOW_INT_STA_S3_MSK BIT(7)
+#define  DP_VIDEO_FIFO_OVERFLOW_INT_STA_S2_MSK BIT(6)
+#define  DP_VIDEO_FIFO_OVERFLOW_INT_STA_S1_MSK BIT(5)
+#define  DP_VIDEO_FIFO_OVERFLOW_INT_STA_S0_MSK BIT(4)
+
+#define DP_HPD_INTERRUPT_STATUS              0x0088
+#define  DP_HOT_PLUG_EVENT                  BIT(29)
+#define  DP_HOT_UNPLUG_EVENT                BIT(28)
+#define  DP_HPD_IN_STATUS                   BIT(26)
+
+#define DP_HPD_INTERRUPT_ENABLE              0x008c
+#define  DP_SINK_IRQ_EVENT_MSK              BIT(31)
+#define  DP_HOT_PLUG_EVENT_MSK              BIT(29)
+#define  DP_HOT_UNPLUG_EVENT_MSK            BIT(28)
+#define  DP_SINK_UNPLUG_ERROR_EVENT_MSK     BIT(27)
+
+#define DP_PHYIF_CTRL_ADDR                   0x0100
+#define  DP_PHY_BUSY_BYP                    BIT(31)
+#define  DP_TPS_SEL                         GENMASK(28, 25)
+#define  DP_XMIT_ENABLE                     GENMASK(20, 17)
+#define  DP_PHY_POWERDOWN                   GENMASK(16, 13)
+#define  DP_PHY_SSC_DIS                     BIT(8)
+#define  DP_PHY_NUM_LANES                   GENMASK(6, 5)
+#define  DP_PHY_RATE                        GENMASK(1, 0)
+
+#define DP_PHYIF_TX_EQ_ADDR                  0x0104
+#define  DP_PHY_LANE3_TX_VSWING             GENMASK(21, 20)
+#define  DP_PHY_LANE3_TX_PREEMP             GENMASK(19, 18)
+#define  DP_PHY_LANE2_TX_VSWING             GENMASK(15, 14)
+#define  DP_PHY_LANE2_TX_PREEMP             GENMASK(13, 12)
+#define  DP_PHY_LANE1_TX_VSWING             GENMASK(9, 8)
+#define  DP_PHY_LANE1_TX_PREEMP             GENMASK(7, 6)
+#define  DP_PHY_LANE0_TX_VSWING             GENMASK(3, 2)
+#define  DP_PHY_LANE0_TX_PREEMP             GENMASK(1, 0)
+
+#define DP_MPLL_CTRL0                        0x0180
+#define  DP_ANA_MPLL_FBDIV_LBIT             GENMASK(31, 24)
+#define  DP_ANA_MPLL_SEL_EXTWAVE            BIT(22)
+#define  DP_ANA_MPLL_DISABLE_SSCG           BIT(21)
+#define  DP_ANA_MPLL_DOWNSPREAD             BIT(20)
+#define  DP_ANA_MPLL_FBDIV_HBIT             GENMASK(19, 16)
+#define  DP_ANA_MPLL_OBSEN                  BIT(15)
+#define  DP_ANA_MPLL_OBSSEL                 BIT(14)
+#define  DP_ANA_MPLL_PREDIV                 GENMASK(13, 8)
+#define  DP_AD_LOCK_COREPLL                 BIT(7)
+#define  DP_ANA_MPLL_DACPD                  BIT(5)
+#define  DP_ANA_MPLL_DSMPD                  BIT(4)
+#define  DP_ANA_MPLL_PD                     BIT(0)
+
+#define DP_MPLL_CTRL1                        0x0184
+#define  DP_ANA_MPLL_FRAC_LBIT              GENMASK(23, 16)
+#define  DP_ANA_MPLL_FRAC_MBIT              GENMASK(15, 8)
+#define  DP_ANA_MPLL_FRAC_HBIT              GENMASK(7, 0)
+
+#define DP_MPLL_CTRL2                        0x0188
+#define  DP_ANA_MPLL_VCOCLK_DIV8_EN         BIT(16)
+#define  DP_ANA_MPLL_POSTDIVEN              BIT(11)
+#define  DP_ANA_MPLL_POSTDIV                GENMASK(10, 8)
+
+#define DP_PREPLL_CTRL0                      0x0190
+#define  DP_ANA_PREPLL_FBDIV2_LBIT          GENMASK(31, 24)
+#define  DP_ANA_PREPLL_FBDIV2_HBIT          GENMASK(19, 16)
+#define  DP_ANA_PREPLL_PREDIV               GENMASK(13, 8)
+#define  DP_AD_LOCK_PIXELPLL                BIT(7)
+#define  DP_ANA_PREPLL_DACPD                BIT(5)
+#define  DP_ANA_PREPLL_DSMPD                BIT(4)
+#define  DP_REG_PCLK_OUTPUT_NORMAL          BIT(1)
+#define  DP_ANA_PREPLL_PD                   BIT(0)
+
+#define DP_PREPLL_CTRL1                      0x0194
+#define  DP_ANA_PREPLL_PRECLK_DIVM          GENMASK(17, 16)
+#define  DP_ANA_PREPLL_PRECLK_DIVAUX        GENMASK(12, 8)
+
+#define DP_PREPLL_CTRL2                      0x0198
+#define  DP_ANA_PREPLL_PCLKDIV5_EN          BIT(31)
+#define  DP_ANA_PREPLL_PCLK_DIVAUX          GENMASK(28, 24)
+#define  DP_ANA_PREPLL_LOCK_BYPEN           BIT(17)
+#define  DP_ANA_PREPLL_LOWFRE_EN            BIT(16)
+#define  DP_ANA_PREPLL_TESTEN               BIT(15)
+#define  DP_ANA_PREPLL_TESTSEL              GENMASK(14, 12)
+#define  DP_ANA_PREPLL_HDMI_EN              BIT(9)
+#define  DP_ANA_PREPLL_DP_EN                BIT(8)
+
+#define DP_PREPLL_SSC_CTRL                   0x019c
+#define  DP_ANA_PREPLL_FRAC2_LBIT           GENMASK(23, 16)
+#define  DP_ANA_PREPLL_FRAC2_MBIT           GENMASK(15, 8)
+#define  DP_ANA_PREPLL_FRAC2_HBIT           GENMASK(7, 0)
+
+#define DP_ANA_TX_CTRL0                      0x01a0
+#define  DP_ANA_MPLL_CLKDIV_16M             GENMASK(13, 8)
+
+#define DP_ANA_TX_ENABLE                     0x01a4
+#define  DP_ANA_TX_ISEL_DRV_D3              GENMASK(31, 28)
+#define  DP_ANA_TX_ISEL_DRV_D2              GENMASK(27, 24)
+#define  DP_AD_TX_ESDSENSE_D3               GENMASK(23, 22)
+#define  DP_AD_TX_ESDSENSE_D2               GENMASK(21, 20)
+#define  DP_AD_TX_ESDSENSE_D1               GENMASK(19, 18)
+#define  DP_AD_TX_ESDSENSE_D0               GENMASK(17, 16)
+#define  DP_ANA_TX_EN_LDO_D3                BIT(11)
+#define  DP_ANA_TX_EN_LDO_D2                BIT(10)
+#define  DP_ANA_TX_EN_LDO_D1                BIT(9)
+#define  DP_ANA_TX_EN_LDO_D0                BIT(8)
+#define  DP_ANA_TX_EN_BYP_D3                BIT(7)
+#define  DP_ANA_TX_EN_BYP_D2                BIT(6)
+#define  DP_ANA_TX_EN_BYP_D1                BIT(5)
+#define  DP_ANA_TX_EN_BYP_D0                BIT(4)
+#define  DP_ANA_TX_EN_DRV_D3                BIT(3)
+#define  DP_ANA_TX_EN_DRV_D2                BIT(2)
+#define  DP_ANA_TX_EN_DRV_D1                BIT(1)
+#define  DP_ANA_TX_EN_DRV_D0                BIT(0)
+
+#define DP_ANA_TX_CTRL1                      0x01a8
+#define  DP_ANA_TX_PHASE_D2                 GENMASK(31, 30)
+#define  DP_ANA_TX_MAINSEL_D2               GENMASK(28, 24)
+#define  DP_ANA_TX_PHASE_D3                 GENMASK(23, 22)
+#define  DP_ANA_TX_MAINSEL_D3               GENMASK(20, 16)
+#define  DP_ANA_TX_ISEL_LDO_D3              BIT(11)
+#define  DP_ANA_TX_ISEL_LDO_D2              BIT(10)
+#define  DP_ANA_TX_ISEL_LDO_D1              BIT(9)
+#define  DP_ANA_TX_ISEL_LDO_D0              BIT(8)
+#define  DP_ANA_TX_ISEL_DRV_D1              GENMASK(7, 4)
+#define  DP_ANA_TX_ISEL_DRV_D0              GENMASK(3, 0)
+
+#define DP_ANA_TX_CTRL2                      0x01ac
+#define  DP_ANA_TX_POSTSEL_D1               GENMASK(31, 28)
+#define  DP_ANA_TX_POSTSEL_D0               GENMASK(27, 24)
+#define  DP_ANA_TX_POSTSEL_D3               GENMASK(23, 20)
+#define  DP_ANA_TX_POSTSEL_D2               GENMASK(19, 16)
+#define  DP_ANA_TX_PHASE_D0                 GENMASK(15, 14)
+#define  DP_DA_TX_MAINSEL_D0_4_0            GENMASK(12, 8)
+#define  DP_ANA_TX_PHASE_D1                 GENMASK(7, 6)
+#define  DP_ANA_TX_MAINSEL_D1               GENMASK(4, 0)
+
+#define DP_ANA_TX_CTRL3                      0x01b0
+#define  DP_ANA_TX_EN_P2S_D3                BIT(31)
+#define  DP_ANA_TX_EN_P2S_D2                BIT(30)
+#define  DP_ANA_TX_EN_P2S_D1                BIT(29)
+#define  DP_ANA_TX_EN_P2S_D0                BIT(28)
+#define  DP_ANA_TX_MODE_D3                  BIT(27)
+#define  DP_ANA_TX_MODE_D2                  BIT(26)
+#define  DP_ANA_TX_MODE_D1                  BIT(25)
+#define  DP_ANA_TX_MODE_D0                  BIT(24)
+#define  DP_ANA_TX_PRESEL_D1                GENMASK(14, 12)
+#define  DP_ANA_TX_PRESEL_D0                GENMASK(10, 8)
+#define  DP_ANA_TX_PRESEL_D3                GENMASK(6, 4)
+#define  DP_ANA_TX_PRESEL_D2                GENMASK(2, 0)
+
+#define DP_ANA_TX_CTRL6                      0x01bc
+#define  DP_ANA_BG_VREF5                    GENMASK(15, 14)
+#define  DP_ANA_BG_VREF4                    GENMASK(13, 12)
+#define  DP_ANA_BG_VREF3                    GENMASK(11, 10)
+#define  DP_ANA_BG_VREF2                    GENMASK(9, 8)
+#define  DP_ANA_BG_VREF                     GENMASK(7, 6)
+#define  DP_ANA_BG_VREF1                    GENMASK(5, 4)
+#define  DP_ANA_BG_EN_CHOP                  BIT(3)
+#define  DP_ANA_BG_EN                       BIT(2)
+#define  DP_ANA_BG_ISEL                     BIT(1)
+#define  DP_ANA_BG_VSEL                     BIT(0)
+
+#define DP_ANA_TX_CTRL7                      0x01c0
+#define  DP_ANA_BG_EN_RCAL                  BIT(27)
+#define  DP_ANA_BG_RCAL_SEL                 GENMASK(26, 25)
+#define  DP_AD_BG_RCAL_OUT                  BIT(24)
+#define  DP_ANA_RTCAL_FREQDIV_LBIT          GENMASK(23, 16)
+#define  DP_ANA_RTCAL_BYPASS                BIT(15)
+#define  DP_ANA_RTCAL_FREQDIV_HBIT          GENMASK(14, 8)
+#define  DP_ANA_BG_RCAL_VAL                 GENMASK(5, 0)
+
+#define DP_ANA_TX_CTRL8                      0x01c4
+#define  DP_ANA_TX_RTM_D3                   GENMASK(29, 24)
+#define  DP_ANA_TX_RTM_D2                   GENMASK(21, 16)
+#define  DP_ANA_TX_RTM_D1                   GENMASK(13, 8)
+#define  DP_ANA_TX_RTM_D0                   GENMASK(5, 0)
+
+#define DP_ANA_TX_AUX_CTRL                   0x01d0
+#define  DP_ANA_TX_AUX_RX_VSEL              GENMASK(13, 12)
+
+#define DP_ANA_LANE_EMPH                     0x01dc
+#define  DP_ANA_TX_POSTSEL_PRE_D3           GENMASK(23, 20)
+#define  DP_ANA_TX_POSTSEL_PRE_D2           GENMASK(19, 16)
+#define  DP_ANA_TX_POSTSEL_PRE_D1           GENMASK(15, 12)
+#define  DP_ANA_TX_POSTSEL_PRE_D0           GENMASK(11, 8)
+#define  DP_ANA_TX_MODE_DE_D3               BIT(7)
+#define  DP_ANA_TX_MODE_DE_D2               BIT(6)
+#define  DP_ANA_TX_MODE_DE_D1               BIT(5)
+#define  DP_ANA_TX_MODE_DE_D0               BIT(4)
+#define  DP_ANA_TX_MODE_PRE_D3              BIT(3)
+#define  DP_ANA_TX_MODE_PRE_D2              BIT(2)
+#define  DP_ANA_TX_MODE_PRE_D1              BIT(1)
+#define  DP_ANA_TX_MODE_PRE_D0              BIT(0)
+
+#define DP_VIDEO_VSAMPLE_REG                 0x0200
+#define  DP_VIDEO_STREAM_ENABLE             BIT(28)
+#define  DP_VIDEO_MAPPING                   GENMASK(26, 22)
+#define  DP_STREAM_ENC_EN                   BIT(18)
+
+#define DP_VINPUT_POLARITY                   0x020c
+#define  DP_HSYNC_IN_POLARITY               BIT(29)
+#define  DP_VSYNC_IN_POLARITY               BIT(28)
+
+#define DP_VIDEO_CONFIG2                     0x0210
+#define  DP_VACTIVE                         GENMASK(31, 16)
+#define  DP_VBLANK                          GENMASK(15, 0)
+
+#define DP_VIDEO_CONFIG1                     0x0214
+#define  DP_HBLANK                          GENMASK(29, 16)
+#define  DP_HACTIVE                         GENMASK(15, 0)
+
+#define DP_VIDEO_CONFIG5                     0x0218
+#define  DP_AVERAGE_BYTES_PER_TU            GENMASK(31, 25)
+#define  DP_AVERAGE_BYTES_PER_TU_FRAC       GENMASK(24, 21)
+#define  DP_INIT_THRESHOLD                  GENMASK(18, 10)
+
+#define DP_VIDEO_CONFIG3                     0x021c
+#define  DP_H_FRONT_PORCH                   GENMASK(31, 16)
+#define  DP_H_SYNC_WIDTH                    GENMASK(15, 0)
+
+#define DP_VIDEO_CONFIG4                     0x0220
+#define  DP_V_FRONT_PORCH                   GENMASK(31, 16)
+#define  DP_V_SYNC_WIDTH                    GENMASK(15, 0)
+
+#define DP_VIDEO_MSA1                        0x0224
+#define  DP_HSTART                          GENMASK(31, 16)
+#define  DP_VSTART                          GENMASK(15, 0)
+
+#define DP_VIDEO_MSA2                        0x0228
+#define  DP_MISC0                           GENMASK(7, 0)
+
+#define DP_VIDEO_MSA3                        0x022c
+#define  DP_NVID                            GENMASK(31, 8)
+#define  DP_MISC1                           GENMASK(7, 0)
+
+#define DP_VIDEO_HBLANK_INTERVAL             0x0230
+#define  DP_HBLANK_INTERVAL                 GENMASK(31, 16)
+
+#define DP_VIDEO_COLORBAR_CFG                0x0238
+#define  DP_VID_BIST_EN                     BIT(0)
+
+#define DP_AUDIO_CONFIG1                     0x0300
+#define  DP_AUDIO_INF_SELECT                BIT(31)
+#define  DP_AUDIO_MUTE                      BIT(30)
+#define  DP_HBR_MODE_ENABLE                 BIT(29)
+#define  DP_AUDIO_DATA_IN_EN                GENMASK(28, 25)
+#define  DP_I2S_AUDIO_MODE                  GENMASK(24, 23)
+#define  DP_AUD_ADJUST_SEL                  BIT(22)
+#define  DP_AUDIO_DATA_WIDTH                GENMASK(21, 17)
+#define  DP_AUDIO_NUM_CHANNELS              GENMASK(16, 14)
+#define  DP_AUDIO_PACKET_ID                 GENMASK(13, 6)
+#define  DP_AUDIO_TIMESTAMP_VERSION_NUM     GENMASK(5, 0)
+
+#define DP_AUX_STS_REG                       0x0400
+#define  DP_AUX_STATUS                      GENMASK(31, 24)
+#define  DP_AUX_REPLY_ERR_CODE              GENMASK(6, 4)
+
+#define DP_AUX_CMD_REG                       0x0404
+#define  DP_AUX_LENGTH                      GENMASK(27, 24)
+#define  DP_AUX_ADDR                        GENMASK(23, 4)
+#define  DP_AUX_CMD_TYPE                    GENMASK(3, 0)
+
+#define DP_AUX_START_REG                     0x0408
+#define  DP_AUX_START                       BIT(31)
+
+#define DP_AUX_DATA4_REG                     0x040c
+#define  DP_AUX_DATA4                       GENMASK(31, 0)
+
+#define DP_AUX_DATA3_REG                     0x0410
+#define  DP_AUX_DATA3                       GENMASK(31, 0)
+
+#define DP_AUX_DATA2_REG                     0x0414
+#define  DP_AUX_DATA2                       GENMASK(31, 0)
+
+#define DP_AUX_DATA1_REG                     0x0418
+#define  DP_AUX_DATA1                       GENMASK(31, 0)
+
+#define DP_SDP_HORIZONTAL_CTRL               0x0420
+#define  DP_AUD_STREAM_HORIZONTAL_EN        BIT(13)
+#define  DP_AUD_TIMESTAMP_HORIZONTAL_EN     BIT(12)
+
+#define DP_SDP_VERTICAL_CTRL                 0x0424
+#define  DP_AUD_STREAM_VERTICAL_EN          BIT(13)
+#define  DP_AUD_TIMESTAMP_VERTICAL_EN       BIT(12)
+
+#endif /* __SPACEMIT_INNO_DP_H__ */

-- 
2.43.0



-- 
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy

WARNING: multiple messages have this Message-ID (diff)
From: Cody Kang via B4 Relay <devnull+codykang.hk.gmail.com@kernel.org>
To: "David Airlie" <airlied@gmail.com>,
	"Simona Vetter" <simona@ffwll.ch>,
	"Maarten Lankhorst" <maarten.lankhorst@linux.intel.com>,
	"Maxime Ripard" <mripard@kernel.org>,
	"Thomas Zimmermann" <tzimmermann@suse.de>,
	"Rob Herring" <robh@kernel.org>,
	"Krzysztof Kozlowski" <krzk+dt@kernel.org>,
	"Conor Dooley" <conor+dt@kernel.org>,
	"Yixun Lan" <dlan@kernel.org>, "Vinod Koul" <vkoul@kernel.org>,
	"Neil Armstrong" <neil.armstrong@linaro.org>,
	"Haylen Chu" <heylenay@4d2.org>,
	"Michael Turquette" <mturquette@baylibre.com>,
	"Stephen Boyd" <sboyd@kernel.org>,
	"Brian Masney" <bmasney@redhat.com>,
	"Philipp Zabel" <p.zabel@pengutronix.de>,
	"Paul Walmsley" <pjw@kernel.org>,
	"Palmer Dabbelt" <palmer@dabbelt.com>,
	"Albert Ou" <aou@eecs.berkeley.edu>,
	"Alexandre Ghiti" <alex@ghiti.fr>, "Yao Zi" <me@ziyao.cc>,
	"Uwe Kleine-König" <u.kleine-koenig@baylibre.com>,
	"Guodong Xu" <docular.xu@gmail.com>
Cc: dri-devel@lists.freedesktop.org, linux-riscv@lists.infradead.org,
	 devicetree@vger.kernel.org, spacemit@lists.linux.dev,
	 linux-kernel@vger.kernel.org, linux-phy@lists.infradead.org,
	 linux-clk@vger.kernel.org, Cody Kang <codykang.hk@gmail.com>
Subject: [PATCH v2 12/17] drm/spacemit: add Innosilicon DP/eDP controller bridge driver
Date: Sun, 09 Aug 2026 21:14:21 +0800	[thread overview]
Message-ID: <20260809-k3-display-v2-12-327d7910bf71@gmail.com> (raw)
In-Reply-To: <20260809-k3-display-v2-0-327d7910bf71@gmail.com>

From: Cody Kang <codykang.hk@gmail.com>

Add the DP/eDP controller that sits downstream of the Saturn DPU. Two
identical instances share one compatible; the eDP-vs-DP role is board
wiring, so it is taken from the devicetree: an eDP panel always sits
under an aux-bus child node, an external DP connector never does.

The link is driven through the generic PHY framework, so the controller
never touches a PLL register. The controller's HPD interrupt is gated by
the DP pixel clock, which can be off exactly when a plug has to be
caught, so the connector is also polled and the interrupt path re-reads
the live level when it does fire.

Signed-off-by: Cody Kang <codykang.hk@gmail.com>

---
v2:
- use devm_reset_control_get_exclusive_deasserted(), dropping the manual
  deassert/assert paths and the reset field (Philipp Zabel)
- hand the pxclk disable to a devm action registered after the PHY
  child is populated (Yao Zi)
- drop the dead NULL-clk guards (Yao Zi)
- drop the error message duplicated by devm_request_threaded_irq()
  (Yao Zi)
- ack the W1C DP_GENERAL_INTERRUPT bits with plain writes (Sashiko)
- return IRQ_HANDLED when the ISR consumed an HPD latch instead of
  IRQ_NONE (Sashiko)
- read EDID blocks through the aux I2C adapter (Sashiko)
- drop DRM_BRIDGE_OP_MODES from the DP branch (Sashiko)
- respond to Rob Herring's suggestion, port@1 required, compute
  possible_crtcs from the input endpoint only
---
 drivers/gpu/drm/spacemit/Kconfig            |   19 +
 drivers/gpu/drm/spacemit/Makefile           |    3 +
 drivers/gpu/drm/spacemit/spacemit_inno_dp.c | 2391 +++++++++++++++++++++++++++
 drivers/gpu/drm/spacemit/spacemit_inno_dp.h |  329 ++++
 4 files changed, 2742 insertions(+)

diff --git a/drivers/gpu/drm/spacemit/Kconfig b/drivers/gpu/drm/spacemit/Kconfig
index 51e38dabfdba5..afd6cdea27fc2 100644
--- a/drivers/gpu/drm/spacemit/Kconfig
+++ b/drivers/gpu/drm/spacemit/Kconfig
@@ -16,3 +16,22 @@ config DRM_SPACEMIT
 	  DisplayPort or embedded DisplayPort controller.
 
 	  If M is selected the module will be called spacemit-drm.
+
+config SPACEMIT_INNO_DP
+	tristate "SpacemiT Innosilicon DP/eDP controller support"
+	depends on DRM_SPACEMIT
+	imply PHY_SPACEMIT_K3_INNO_DP
+	select GENERIC_PHY
+	select DRM_PANEL
+	select DRM_BRIDGE_CONNECTOR
+	select DRM_DISPLAY_HELPER
+	select DRM_DISPLAY_DP_HELPER
+	select DRM_DISPLAY_DP_AUX_BUS
+	help
+	  Choose this option to enable the Innosilicon DisplayPort /
+	  embedded DisplayPort transmitter integrated in SpacemiT SoCs.
+	  The controller sits downstream of the Saturn DPU and drives an
+	  eDP panel (discovered over the DP AUX bus) or an external
+	  DisplayPort connector.
+
+	  If M is selected the module will be called spacemit-inno-dp.
diff --git a/drivers/gpu/drm/spacemit/Makefile b/drivers/gpu/drm/spacemit/Makefile
index b3b148000cf7b..260d316a271b5 100644
--- a/drivers/gpu/drm/spacemit/Makefile
+++ b/drivers/gpu/drm/spacemit/Makefile
@@ -9,4 +9,7 @@ spacemit-drm-y := spacemit_drm.o \
 		  dpu/dpu_saturn_hee.o \
 		  dpu/saturn_fbcmem.o
 
+spacemit-inno-dp-y := spacemit_inno_dp.o
+
 obj-$(CONFIG_DRM_SPACEMIT) += spacemit-drm.o
+obj-$(CONFIG_SPACEMIT_INNO_DP) += spacemit-inno-dp.o
diff --git a/drivers/gpu/drm/spacemit/spacemit_inno_dp.c b/drivers/gpu/drm/spacemit/spacemit_inno_dp.c
new file mode 100644
index 0000000000000..1ba4892607881
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/spacemit_inno_dp.c
@@ -0,0 +1,2391 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ */
+
+#include <linux/of.h>
+#include <linux/bits.h>
+#include <linux/bitfield.h>
+#include <linux/module.h>
+#include <linux/slab.h>
+#include <linux/platform_device.h>
+#include <linux/component.h>
+#include <linux/clk.h>
+#include <linux/cleanup.h>
+#include <linux/math64.h>
+#include <linux/mutex.h>
+#include <linux/reset.h>
+#include <linux/delay.h>
+#include <linux/i2c.h>
+#include <linux/iopoll.h>
+#include <linux/media-bus-format.h>
+#include <linux/of_graph.h>
+#include <linux/of_platform.h>
+#include <linux/phy/phy.h>
+#include <linux/phy/phy-dp.h>
+#include <linux/regmap.h>
+#include <drm/display/drm_dp_aux_bus.h>
+#include <drm/drm_of.h>
+#include <drm/drm_bridge.h>
+#include <drm/drm_bridge_connector.h>
+#include <drm/drm_device.h>
+#include <drm/drm_encoder.h>
+#include <drm/drm_connector.h>
+#include <drm/drm_drv.h>
+#include <drm/drm_edid.h>
+#include <drm/drm_modeset_helper_vtables.h>
+#include <drm/drm_gem_dma_helper.h>
+#include <drm/drm_probe_helper.h>
+#include <drm/drm_atomic.h>
+#include <drm/drm_atomic_state_helper.h>
+#include <drm/display/drm_dp_aux_bus.h>
+#include <drm/display/drm_dp.h>
+#include <drm/display/drm_dp_helper.h>
+#include <drm/drm_print.h>
+
+#include "spacemit_inno_dp.h"
+
+#define SPACEMIT_DP_SWING_MAX  2
+#define SPACEMIT_DP_PREEMP_MAX 2
+#define SPACEMIT_DP_AUX_MAX_RETRIES 3
+
+#define SPACEMIT_DP_DDC_SEGMENT_ADDR 0x30
+#define SPACEMIT_DP_SINK_READY_DELAY_MS 120
+#define SPACEMIT_DP_SINK_READY_RETRIES 3
+
+/*
+ * With DP routed through a Type-C PD controller the HPD pin is virtual and
+ * can re-assert seconds after a transmitter power-cycle. Asserted HPD
+ * returns immediately; the full wait is paid only when no sink shows up.
+ */
+#define SPACEMIT_DP_HPD_POLL_US     1000
+#define SPACEMIT_DP_HPD_DEBOUNCE_US (1000 * USEC_PER_MSEC)
+
+enum spacemit_dp_link_rate {
+	SPACEMIT_DP_LINK_RATE_1_62 = 1620000, /* 1.62 Gbps */
+	SPACEMIT_DP_LINK_RATE_2_70 = 2700000, /* 2.70 Gbps */
+	SPACEMIT_DP_LINK_RATE_5_40 = 5400000, /* 5.40 Gbps */
+	SPACEMIT_DP_LINK_RATE_8_10 = 8100000, /* 8.10 Gbps */
+};
+
+/* The K3 source tops out at HBR2: the PHY's MPLL has no 8.1 Gbps config. */
+#define SPACEMIT_DP_SOURCE_MAX_RATE	SPACEMIT_DP_LINK_RATE_5_40
+
+enum spacemit_dp_lane_count {
+	SPACEMIT_DP_LANE_1 = 1,
+	SPACEMIT_DP_LANE_2 = 2,
+	SPACEMIT_DP_LANE_4 = 4,
+};
+
+enum soc_video_format {
+	SOC_VIDEO_RGB_6BIT = 0,
+	SOC_VIDEO_RGB_8BIT = 1,
+	SOC_VIDEO_RGB_10BIT = 2,
+	SOC_VIDEO_RGB_12BIT = 3,
+	SOC_VIDEO_RGB_16BIT = 4,
+	SOC_VIDEO_YUV444_8BIT = 5,
+	SOC_VIDEO_YUV444_10BIT = 6,
+	SOC_VIDEO_YUV444_12BIT = 7,
+	SOC_VIDEO_YUV444_16BIT = 8,
+	SOC_VIDEO_YUV422_8BIT = 9,
+	SOC_VIDEO_YUV422_10BIT = 10,
+	SOC_VIDEO_YUV422_12BIT = 11,
+	SOC_VIDEO_YUV422_16BIT = 12,
+};
+
+enum spacemit_dp_ref_clk {
+	SPACEMIT_DP_REF_CLK_24M = 24000,
+};
+
+static const struct spacemit_dp_link_config {
+	enum spacemit_dp_link_rate rate;
+	enum spacemit_dp_lane_count lanes;
+} spacemit_dp_link_priority_table[] = {
+	{SPACEMIT_DP_LINK_RATE_1_62, SPACEMIT_DP_LANE_1}, /* 1.62 Gbps */
+	{SPACEMIT_DP_LINK_RATE_2_70, SPACEMIT_DP_LANE_1}, /* 2.70 Gbps */
+	{SPACEMIT_DP_LINK_RATE_1_62, SPACEMIT_DP_LANE_2}, /* 3.24 Gbps */
+
+	{SPACEMIT_DP_LINK_RATE_2_70, SPACEMIT_DP_LANE_2}, /* 5.40 Gbps */
+	{SPACEMIT_DP_LINK_RATE_1_62, SPACEMIT_DP_LANE_4}, /* 6.48 Gbps */
+
+	{SPACEMIT_DP_LINK_RATE_2_70, SPACEMIT_DP_LANE_4}, /* 10.8 Gbps */
+	{SPACEMIT_DP_LINK_RATE_5_40, SPACEMIT_DP_LANE_2}, /* 10.8 Gbps */
+
+	{SPACEMIT_DP_LINK_RATE_5_40, SPACEMIT_DP_LANE_4}, /* 21.6 Gbps */
+};
+
+static const struct soc_format_info {
+	u8 bpp; /* Bits Per Pixel */
+} format_info_table[] = {
+	[SOC_VIDEO_RGB_6BIT]      = { .bpp = 18 },
+	[SOC_VIDEO_RGB_8BIT]      = { .bpp = 24 },
+	[SOC_VIDEO_RGB_10BIT]     = { .bpp = 30 },
+	[SOC_VIDEO_RGB_12BIT]     = { .bpp = 36 },
+	[SOC_VIDEO_RGB_16BIT]     = { .bpp = 48 },
+
+	[SOC_VIDEO_YUV444_8BIT]   = { .bpp = 24 },
+	[SOC_VIDEO_YUV444_10BIT]  = { .bpp = 30 },
+	[SOC_VIDEO_YUV444_12BIT]  = { .bpp = 36 },
+	[SOC_VIDEO_YUV444_16BIT]  = { .bpp = 48 },
+
+	[SOC_VIDEO_YUV422_8BIT]   = { .bpp = 16 },
+	[SOC_VIDEO_YUV422_10BIT]  = { .bpp = 20 },
+	[SOC_VIDEO_YUV422_12BIT]  = { .bpp = 24 },
+	[SOC_VIDEO_YUV422_16BIT]  = { .bpp = 32 },
+};
+
+/**
+ * struct spacemit_dp_dev - Per-instance state for an Innosilicon DP/eDP
+ * controller
+ * @dev: Underlying platform device.
+ * @drm: Parent DRM device, set at component bind.
+ * @encoder: Passive encoder anchoring possible_crtcs; atomic goes through
+ * @bridge.
+ * @bridge: DRM bridge implementing this controller, attached NO_CONNECTOR.
+ * @connector: Connector built on top of @bridge by drm_bridge_connector_init().
+ * @connector_status: Last-known status, driven by the HPD ISR and by polling.
+ * @mode: Committed adjusted_mode, cached under @mode_lock for the async paths.
+ * @regs: Mapped MMIO base of the DP controller register block.
+ * @aux: DPCD AUX channel, registered at bind.
+ * @dpcd: Raw DPCD capability bytes.
+ * @lane_count: Lane count chosen by link training (1, 2 or 4).
+ * @link_rate: Link rate chosen by link training, in kHz.
+ * @link: Sink-advertised link capability snapshot.
+ * @link.revision: DPCD revision byte.
+ * @link.enhanced_framing: Whether the sink advertises enhanced framing.
+ * @link.max_rate: Sink-advertised maximum link rate, kHz.
+ * @link.max_num_lanes: Sink-advertised maximum lane count.
+ * @pxclk: Optional external pixel clock.
+ * @pll_clk: The PHY's pixel PLL; clk_set_rate() programs it.
+ * @phy: DP PHY handle.
+ * @phy_powered: Controller-side mirror of the PHY framework's power refcount.
+ * @next_bridge: Downstream bridge; panel-edp's drm_panel wrapper on eDP.
+ * @edp_mode: True when this instance drives an embedded panel.
+ * @use_ext_pixel_clock: True when the pixel clock is sourced externally.
+ * @pixel_clock: Cached external pixel-clock rate, kHz.
+ * @mode_lock: Serialises mode_set / enable / hotplug against each other.
+ * @modeset_retry_work: Deferred "link is bad" notification, see its handler.
+ * @irq: Controller interrupt line.
+ */
+struct spacemit_dp_dev {
+	struct device *dev;
+
+	struct drm_device *drm;
+	struct drm_encoder encoder;
+	struct drm_bridge bridge;
+	struct drm_connector *connector;
+
+	enum drm_connector_status connector_status;
+	struct drm_display_mode mode;
+
+	struct regmap *regs;
+	struct drm_dp_aux aux;
+
+	u8 dpcd[DP_RECEIVER_CAP_SIZE];
+	int lane_count;
+	u32 link_rate;
+
+	struct {
+		u8 revision;
+		u8 enhanced_framing;
+		u32 max_rate;
+		u32 max_num_lanes;
+	} link;
+
+	struct clk *pxclk;
+	struct clk *pll_clk;
+	struct phy *phy;
+	bool phy_powered;
+	struct drm_bridge *next_bridge;
+
+	bool edp_mode;
+	bool use_ext_pixel_clock;
+	int pixel_clock;
+	struct mutex mode_lock;
+
+	struct work_struct modeset_retry_work;
+
+	int irq;
+};
+
+static int spacemit_dp_get_bpp(struct spacemit_dp_dev *dp, u32 format)
+{
+	if (format >= ARRAY_SIZE(format_info_table)) {
+		dev_warn(dp->dev,
+			"invalid color format index %d, defaulting to RGB888\n",
+			format);
+		return 24;
+	}
+	return format_info_table[format].bpp;
+}
+
+/*
+ * drm_connector_set_link_status_property() takes connection_mutex, which the
+ * commit already holds: calling it from .atomic_enable deadlocks against
+ * itself.
+ */
+static void spacemit_dp_modeset_retry_work(struct work_struct *work)
+{
+	struct spacemit_dp_dev *dp = container_of(work, struct spacemit_dp_dev,
+						  modeset_retry_work);
+	struct drm_connector *connector = dp->connector;
+
+	if (!connector)
+		return;
+
+	mutex_lock(&connector->dev->mode_config.mutex);
+	drm_connector_set_link_status_property(connector,
+					       DRM_MODE_LINK_STATUS_BAD);
+	mutex_unlock(&connector->dev->mode_config.mutex);
+
+	drm_kms_helper_connector_hotplug_event(connector);
+}
+
+/**
+ * struct spacemit_dp_bridge_state - Per-state planned link configuration.
+ * @base: Parent &struct drm_bridge_state.
+ * @link_cfg_idx: Index into spacemit_dp_link_priority_table[] picked by
+ *	->atomic_check(); -1 means ->atomic_enable() walks the table from the start.
+ * @color_format: Output colour format selected by ->atomic_check().
+ */
+struct spacemit_dp_bridge_state {
+	struct drm_bridge_state base;
+	int link_cfg_idx;
+	u32 color_format;
+};
+
+static inline struct spacemit_dp_bridge_state *
+to_spacemit_dp_bridge_state(struct drm_bridge_state *state)
+{
+	return container_of(state, struct spacemit_dp_bridge_state, base);
+}
+
+static const struct regmap_config spacemit_dp_regmap_config = {
+	.name		= "dp",
+	.reg_bits	= 32,
+	.val_bits	= 32,
+	.reg_stride	= 4,
+	.max_register	= 0x3ffc,	/* full 0x4000 reg region from binding */
+};
+
+static u32 spacemit_dp_aux_get_cmd(struct drm_dp_aux_msg *msg)
+{
+	switch (msg->request & ~DP_AUX_I2C_MOT) {
+	case DP_AUX_NATIVE_WRITE:
+	case DP_AUX_I2C_WRITE:
+	case DP_AUX_I2C_WRITE_STATUS_UPDATE:
+		return msg->request;
+	case DP_AUX_NATIVE_READ:
+	case DP_AUX_I2C_READ:
+		return msg->request;
+	default:
+		return 0;
+	}
+
+	return 0;
+}
+
+static void spacemit_dp_aux_hw_reset(struct spacemit_dp_dev *dp)
+{
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_AUX_RESET,
+			  FIELD_PREP(DP_AUX_RESET, 0x1));
+	usleep_range(2000, 2500);
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_AUX_RESET,
+			  FIELD_PREP(DP_AUX_RESET, 0x0));
+	usleep_range(2000, 2500);
+	regmap_write(dp->regs, DP_GENERAL_INTERRUPT,
+		     DP_AUX_REPLY_EVENT_INT_STA);
+}
+
+static bool spacemit_dp_dpcd_caps_valid(const u8 *dpcd)
+{
+	u8 max_bw = dpcd[DP_MAX_LINK_RATE];
+	u8 max_lanes = dpcd[DP_MAX_LANE_COUNT] & DP_MAX_LANE_COUNT_MASK;
+
+	if (!dpcd[DP_DPCD_REV])
+		return false;
+
+	switch (max_bw) {
+	case DP_LINK_BW_1_62:
+	case DP_LINK_BW_2_7:
+	case DP_LINK_BW_5_4:
+	case DP_LINK_BW_8_1:
+		break;
+	default:
+		return false;
+	}
+
+	return max_lanes == 1 || max_lanes == 2 || max_lanes == 4;
+}
+
+static ssize_t spacemit_dp_aux_transfer(struct drm_dp_aux *aux,
+					struct drm_dp_aux_msg *msg)
+{
+	int ret, i, retries = 0;
+	unsigned long timeout;
+
+	struct spacemit_dp_dev *dp = container_of(aux, struct spacemit_dp_dev,
+						  aux);
+	u32 cmd, len, val, status;
+	u32 data[4] = {0};
+	u8 *buf = msg->buffer;
+	bool is_read = (msg->request & DP_AUX_I2C_READ) ||
+		((msg->request & DP_AUX_NATIVE_READ) == DP_AUX_NATIVE_READ);
+
+	if (msg->size > 16)
+		return -EINVAL;
+
+	cmd = spacemit_dp_aux_get_cmd(msg);
+
+retry_eio:
+	if (!is_read) {
+		/* Pack bytes into 32-bit words (Little Endian packing) */
+		for (i = 0; i < msg->size; i++)
+			data[i / 4] |= buf[i] << ((i % 4) * 8);
+
+		/* Write data to registers: DATA1(LSB)..DATA4(MSB) */
+		regmap_write_bits(dp->regs, DP_AUX_DATA1_REG, DP_AUX_DATA1,
+				  FIELD_PREP(DP_AUX_DATA1, data[0]));
+		regmap_write_bits(dp->regs, DP_AUX_DATA2_REG, DP_AUX_DATA2,
+				  FIELD_PREP(DP_AUX_DATA2, data[1]));
+		regmap_write_bits(dp->regs, DP_AUX_DATA3_REG, DP_AUX_DATA3,
+				  FIELD_PREP(DP_AUX_DATA3, data[2]));
+		regmap_write_bits(dp->regs, DP_AUX_DATA4_REG, DP_AUX_DATA4,
+				  FIELD_PREP(DP_AUX_DATA4, data[3]));
+	}
+
+	/* HW expects Length - 1 */
+	len = msg->size > 0 ? msg->size - 1 : 0;
+
+	regmap_write_bits(dp->regs, DP_AUX_CMD_REG, DP_AUX_LENGTH,
+			  FIELD_PREP(DP_AUX_LENGTH, len));
+	regmap_write_bits(dp->regs, DP_AUX_CMD_REG, DP_AUX_ADDR,
+			  FIELD_PREP(DP_AUX_ADDR, msg->address));
+	regmap_write_bits(dp->regs, DP_AUX_CMD_REG, DP_AUX_CMD_TYPE,
+			  FIELD_PREP(DP_AUX_CMD_TYPE, cmd));
+
+	regmap_write_bits(dp->regs, DP_AUX_START_REG, DP_AUX_START,
+			  FIELD_PREP(DP_AUX_START, 0));
+	regmap_write_bits(dp->regs, DP_AUX_START_REG, DP_AUX_START,
+			  FIELD_PREP(DP_AUX_START, 1));
+
+	timeout = jiffies + msecs_to_jiffies(200);
+	ret = -ETIMEDOUT;
+
+	while (1) {
+		regmap_read(dp->regs, DP_GENERAL_INTERRUPT, &val);
+		val = FIELD_GET(DP_AUX_REPLY_EVENT_INT_STA, val);
+		if (val) {
+			ret = 0;
+			break;
+		}
+		if (time_after(jiffies, timeout))
+			break;
+		usleep_range(100, 110);
+	}
+
+	if (ret) {
+		if (retries < SPACEMIT_DP_AUX_MAX_RETRIES) {
+			retries++;
+			spacemit_dp_aux_hw_reset(dp);
+			dev_dbg(dp->dev,
+				"AUX timeout, cmd=0x%x addr=0x%x size=%zu retry %d/%d\n",
+				cmd, msg->address, msg->size, retries, SPACEMIT_DP_AUX_MAX_RETRIES);
+			goto retry_eio;
+		}
+
+		dev_err(dp->dev,
+			"AUX transfer timeout, req: 0x%x, cmd: 0x%x, addr: 0x%x, size %zu\n",
+			msg->request, cmd, msg->address, msg->size);
+		return ret;
+	}
+
+	regmap_read(dp->regs, DP_AUX_STS_REG,
+		    &status); status = FIELD_GET(DP_AUX_STATUS, status);
+
+	/* Write 1 to clear. */
+	regmap_write(dp->regs, DP_GENERAL_INTERRUPT,
+		     DP_AUX_REPLY_EVENT_INT_STA);
+
+	/* Map HW status (the AUX reply command byte) to DRM reply codes */
+	switch (status) {
+	case 0: /* ACK */
+		msg->reply = DP_AUX_NATIVE_REPLY_ACK;
+		break;
+	case 1: /* NACK */
+		msg->reply = DP_AUX_NATIVE_REPLY_NACK;
+		return 0; /* Standard says return 0 on NACK for upper layer retry */
+	case 2: /* DEFER */
+		msg->reply = DP_AUX_NATIVE_REPLY_DEFER;
+		return 0;
+	case DP_AUX_I2C_REPLY_NACK: /* native ACK, I2C NACK */
+		msg->reply = DP_AUX_I2C_REPLY_NACK;
+		return 0;
+	case DP_AUX_I2C_REPLY_DEFER: /* native ACK, I2C DEFER */
+		msg->reply = DP_AUX_I2C_REPLY_DEFER;
+		return 0;
+	default:
+		regmap_read(dp->regs, DP_AUX_STS_REG, &val);
+		val = FIELD_GET(DP_AUX_REPLY_ERR_CODE, val);
+		if (retries < SPACEMIT_DP_AUX_MAX_RETRIES) {
+			retries++;
+			dev_dbg(dp->dev,
+				"AUX status, cmd=0x%x addr=0x%x size=%zu retry %d/%d\n",
+				cmd, msg->address, msg->size, retries, SPACEMIT_DP_AUX_MAX_RETRIES);
+			usleep_range(1000, 1100);
+			goto retry_eio;
+		}
+		dev_err(dp->dev,
+			"AUX error: cmd=0x%x addr=0x%x size=%zu status=0x%x code=0x%x\n",
+			cmd, msg->address, msg->size, status, val);
+		return -EIO;
+	}
+
+	if (is_read && msg->size > 0) {
+		regmap_read(dp->regs, DP_AUX_DATA1_REG, &data[0]);
+		data[0] = FIELD_GET(DP_AUX_DATA1, data[0]);
+		regmap_read(dp->regs, DP_AUX_DATA2_REG, &data[1]);
+		data[1] = FIELD_GET(DP_AUX_DATA2, data[1]);
+		regmap_read(dp->regs, DP_AUX_DATA3_REG, &data[2]);
+		data[2] = FIELD_GET(DP_AUX_DATA3, data[2]);
+		regmap_read(dp->regs, DP_AUX_DATA4_REG, &data[3]);
+		data[3] = FIELD_GET(DP_AUX_DATA4, data[3]);
+
+		/* Unpack 32-bit words back to bytes */
+		for (i = 0; i < msg->size; i++)
+			buf[i] = (data[i / 4] >> ((i % 4) * 8)) & 0xFF;
+	}
+
+	return msg->size;
+}
+
+static int update_edp_config(struct spacemit_dp_dev *dp, bool enable)
+{
+	u8 value;
+	int ret;
+
+	ret = drm_dp_dpcd_read_byte(&dp->aux, DP_EDP_CONFIGURATION_SET, &value);
+	if (ret < 0) {
+		dev_err(dp->dev,
+			"failed to read DP_EDP_CONFIGURATION_SET: %d\n", ret);
+		return ret;
+	}
+
+	if (enable)
+		value |= 0x01;
+	else
+		value &= ~0x01;
+
+	ret = drm_dp_dpcd_write_byte(&dp->aux, DP_EDP_CONFIGURATION_SET, value);
+	if (ret < 0) {
+		dev_err(dp->dev,
+			"failed to write DP_EDP_CONFIGURATION_SET: %d\n", ret);
+		return ret;
+	}
+
+	return 0;
+}
+
+/* Read the sink's DPCD link-layer capabilities. EDID is handled separately. */
+static int spacemit_dp_hw_read_sink_caps(struct spacemit_dp_dev *dp)
+{
+	ssize_t ret;
+	u8 max_bw;
+	int retry;
+
+	for (retry = 0; retry < SPACEMIT_DP_SINK_READY_RETRIES; retry++) {
+		if (retry) {
+			spacemit_dp_aux_hw_reset(dp);
+			msleep(SPACEMIT_DP_SINK_READY_DELAY_MS);
+		}
+
+		ret = drm_dp_read_dpcd_caps(&dp->aux, dp->dpcd);
+		if (ret < 0)
+			continue;
+
+		if (!spacemit_dp_dpcd_caps_valid(dp->dpcd)) {
+			dev_dbg(dp->dev,
+				"DPCD caps not ready: rev=0x%02x bw=0x%02x lanes=0x%02x\n",
+				dp->dpcd[DP_DPCD_REV], dp->dpcd[DP_MAX_LINK_RATE],
+				dp->dpcd[DP_MAX_LANE_COUNT]);
+			ret = -EAGAIN;
+			continue;
+		}
+
+		break;
+	}
+
+	if (ret < 0) {
+		dev_err(dp->dev, "failed to read DPCD after %d retries: %zd\n",
+			SPACEMIT_DP_SINK_READY_RETRIES, ret);
+		dp->link.revision = 0x14;
+		dp->link.max_rate = SPACEMIT_DP_LINK_RATE_5_40;
+		dp->link.max_num_lanes = SPACEMIT_DP_LANE_2;
+		dp->link.enhanced_framing = 1;
+		return ret;
+	}
+
+	dp->link.revision = dp->dpcd[DP_DPCD_REV];
+
+	max_bw = dp->dpcd[DP_MAX_LINK_RATE];
+	switch (max_bw) {
+	case DP_LINK_BW_1_62:
+		dp->link.max_rate = SPACEMIT_DP_LINK_RATE_1_62;
+		break;
+	case DP_LINK_BW_2_7:
+		dp->link.max_rate = SPACEMIT_DP_LINK_RATE_2_70;
+		break;
+	case DP_LINK_BW_5_4:
+		dp->link.max_rate = SPACEMIT_DP_LINK_RATE_5_40;
+		break;
+	case DP_LINK_BW_8_1:
+		dp->link.max_rate = SPACEMIT_DP_LINK_RATE_8_10;
+		break;
+	default:
+		dev_warn(dp->dev, "unknown DPCD max link rate 0x%x, defaulting to 5.40 Gbps\n",
+			 max_bw);
+		dp->link.revision = 0x14;
+		dp->link.max_rate = SPACEMIT_DP_LINK_RATE_5_40;
+		dp->link.max_num_lanes = SPACEMIT_DP_LANE_2;
+		dp->link.enhanced_framing = 1;
+		return -EINVAL;
+	}
+
+	/* Clamp to the source's actual ceiling (HBR2). */
+	dp->link.max_rate = min(dp->link.max_rate,
+				(u32)SPACEMIT_DP_SOURCE_MAX_RATE);
+
+	dp->link.max_num_lanes =
+			dp->dpcd[DP_MAX_LANE_COUNT] & DP_MAX_LANE_COUNT_MASK;
+
+	dp->link.enhanced_framing =
+		(dp->dpcd[DP_MAX_LANE_COUNT] & DP_ENHANCED_FRAME_CAP);
+
+	dev_dbg(dp->dev,
+		"DPCD: Rev %x.%x, MaxRate %d kHz, MaxLanes %d, EnhFrame %d\n",
+		dp->link.revision >> 4, dp->link.revision & 0xF,
+		dp->link.max_rate, dp->link.max_num_lanes,
+		dp->link.enhanced_framing);
+
+	return 0;
+}
+
+static int spacemit_dp_phy_set_lanes(struct spacemit_dp_dev *dp,
+				     enum spacemit_dp_lane_count lanes)
+{
+	union phy_configure_opts opts = {
+		.dp = {
+			.lanes = lanes,
+			.set_lanes = true,
+		},
+	};
+	int ret;
+
+	ret = phy_configure(dp->phy, &opts);
+	if (ret)
+		return ret;
+
+	dp->lane_count = lanes;
+	return 0;
+}
+
+static int spacemit_dp_phy_set_rate(struct spacemit_dp_dev *dp,
+				    enum spacemit_dp_link_rate rate)
+{
+	union phy_configure_opts opts = {
+		.dp = {
+			/*
+			 * enum value is in kbit/s; PHY framework wants Mbit/s.
+			 */
+			.link_rate = rate / 1000,
+			.set_rate = true,
+		},
+	};
+	int ret;
+
+	ret = phy_configure(dp->phy, &opts);
+	if (ret)
+		return ret;
+
+	dp->link_rate = rate;
+	return 0;
+}
+
+/*
+ * The link PLL rides phy_configure(); the pixel PLL is a clock the PHY
+ * registered. Link first: that is the order the analog blocks come up in.
+ */
+static int spacemit_dp_set_plls(struct spacemit_dp_dev *dp,
+				enum spacemit_dp_link_rate rate, u32 pclk_khz)
+{
+	int ret;
+
+	ret = spacemit_dp_phy_set_rate(dp, rate);
+	if (ret)
+		return ret;
+
+	return clk_set_rate(dp->pll_clk, (unsigned long)pclk_khz * 1000);
+}
+
+/*
+ * phy_power_on/off() are refcounted, but this driver's bring-up is idempotent
+ * rather than pair-balanced. Mirror the state so the refcount cannot go
+ * negative.
+ */
+static int spacemit_dp_phy_on(struct spacemit_dp_dev *dp)
+{
+	int ret;
+
+	if (dp->phy_powered)
+		return 0;
+
+	ret = phy_power_on(dp->phy);
+	if (ret)
+		return ret;
+
+	dp->phy_powered = true;
+	return 0;
+}
+
+static void spacemit_dp_phy_off(struct spacemit_dp_dev *dp)
+{
+	if (!dp->phy_powered)
+		return;
+
+	phy_power_off(dp->phy);
+	dp->phy_powered = false;
+}
+
+/*
+ * The AUX reset pulse is what leaves the next bring-up a clean state machine.
+ */
+static void spacemit_dp_link_disable(struct spacemit_dp_dev *dp)
+{
+	regmap_write_bits(dp->regs, DP_VIDEO_VSAMPLE_REG,
+			  DP_VIDEO_STREAM_ENABLE,
+			  FIELD_PREP(DP_VIDEO_STREAM_ENABLE, 0));
+	spacemit_dp_phy_off(dp);
+
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_AUX_RESET,
+			  FIELD_PREP(DP_AUX_RESET, 0x1));
+	usleep_range(5000, 8000);
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_AUX_RESET,
+			  FIELD_PREP(DP_AUX_RESET, 0x0));
+	usleep_range(2000, 4000);
+}
+
+static enum drm_connector_status
+spacemit_dp_hw_detect_hpd(struct spacemit_dp_dev *dp)
+{
+	u32 hpd_status;
+	enum drm_connector_status connector_status =
+			connector_status_disconnected;
+
+	regmap_read(dp->regs, DP_HPD_INTERRUPT_STATUS, &hpd_status);
+	hpd_status = FIELD_GET(DP_HPD_IN_STATUS, hpd_status);
+	if (hpd_status)
+		connector_status = connector_status_connected;
+	else
+		connector_status = connector_status_disconnected;
+
+	return connector_status;
+}
+
+/* Returns true when a latched HPD event was pending and got acked. */
+static bool spacemit_dp_hw_clean_hpd(struct spacemit_dp_dev *dp)
+{
+	u32 plug_event, unplug_event;
+
+	regmap_read(dp->regs, DP_HPD_INTERRUPT_STATUS, &plug_event);
+	plug_event = FIELD_GET(DP_HOT_PLUG_EVENT, plug_event);
+	regmap_read(dp->regs, DP_HPD_INTERRUPT_STATUS, &unplug_event);
+	unplug_event = FIELD_GET(DP_HOT_UNPLUG_EVENT, unplug_event);
+
+	if (plug_event)
+		regmap_write(dp->regs, DP_HPD_INTERRUPT_STATUS,
+			     FIELD_PREP(DP_HOT_PLUG_EVENT, 0x1));
+
+	if (unplug_event)
+		regmap_write(dp->regs, DP_HPD_INTERRUPT_STATUS,
+			     FIELD_PREP(DP_HOT_UNPLUG_EVENT, 0x1));
+
+	return plug_event || unplug_event;
+}
+
+static int spacemit_dp_set_training_pattern(struct spacemit_dp_dev *dp,
+					    u8 pattern)
+{
+	u32 tps_sel = 0;
+	u8 dpcd_pattern = pattern;
+	int ret;
+
+	if (pattern != DP_TRAINING_PATTERN_DISABLE)
+		dpcd_pattern |= DP_LINK_SCRAMBLING_DISABLE;
+
+	switch (pattern) {
+	case DP_TRAINING_PATTERN_DISABLE:
+		tps_sel = 0;
+		regmap_write_bits(dp->regs, DP_CORE_CONTROL,
+				  DP_SCRAMBLER_DISABLE,
+				  FIELD_PREP(DP_SCRAMBLER_DISABLE, 0));
+		break;
+	case DP_TRAINING_PATTERN_1:
+		tps_sel = 1;
+		regmap_write_bits(dp->regs, DP_CORE_CONTROL,
+				  DP_SCRAMBLER_DISABLE,
+				  FIELD_PREP(DP_SCRAMBLER_DISABLE, 1));
+		break;
+	case DP_TRAINING_PATTERN_2:
+		tps_sel = 2;
+		regmap_write_bits(dp->regs, DP_CORE_CONTROL,
+				  DP_SCRAMBLER_DISABLE,
+				  FIELD_PREP(DP_SCRAMBLER_DISABLE, 1));
+		break;
+	case DP_TRAINING_PATTERN_3:
+		tps_sel = 3;
+		regmap_write_bits(dp->regs, DP_CORE_CONTROL,
+				  DP_SCRAMBLER_DISABLE,
+				  FIELD_PREP(DP_SCRAMBLER_DISABLE, 1));
+		break;
+	default:
+		dev_err(dp->dev, "Unsupported training pattern: 0x%x\n",
+			pattern);
+		return -EINVAL;
+	}
+
+	regmap_write_bits(dp->regs, DP_PHYIF_CTRL_ADDR, DP_TPS_SEL,
+			  FIELD_PREP(DP_TPS_SEL, tps_sel));
+
+	ret = drm_dp_dpcd_write_byte(&dp->aux, DP_TRAINING_PATTERN_SET,
+				     dpcd_pattern);
+	if (ret < 0) {
+		dev_err(dp->dev, "failed to set DPCD training pattern: %d\n",
+			ret);
+		return ret;
+	}
+
+	return 0;
+}
+
+/*
+ * The adjust request has to reach the source PHY, not just the sink's DPCD:
+ * a transmitter left at its reset default trains any level > 0 with a bad eye.
+ */
+static int spacemit_dp_link_apply_adjust(struct spacemit_dp_dev *dp,
+					 const u8 *link_status,
+				    enum spacemit_dp_lane_count lanes,
+				    u8 *training_set)
+{
+	union phy_configure_opts opts = {
+		.dp = {
+			.lanes = lanes,
+			.set_voltages = true,
+		},
+	};
+	int i, ret;
+
+	for (i = 0; i < lanes; i++) {
+		u8 v = drm_dp_get_adjust_request_voltage(link_status, i);
+		u8 p = drm_dp_get_adjust_request_pre_emphasis(link_status, i);
+		u8 v_bits;
+
+		if (v >= SPACEMIT_DP_SWING_MAX)
+			v = SPACEMIT_DP_SWING_MAX;
+		if (p >= SPACEMIT_DP_PREEMP_MAX)
+			p = SPACEMIT_DP_PREEMP_MAX;
+
+		opts.dp.voltage[i] = v;
+		opts.dp.pre[i] = p;
+
+		v_bits = v;
+		if (v == SPACEMIT_DP_SWING_MAX)
+			v_bits |= DP_TRAIN_MAX_SWING_REACHED;
+		if (p == SPACEMIT_DP_PREEMP_MAX)
+			v_bits |= DP_TRAIN_MAX_PRE_EMPHASIS_REACHED;
+
+		training_set[i] = v_bits | (p << DP_TRAIN_PRE_EMPHASIS_SHIFT);
+	}
+
+	ret = phy_configure(dp->phy, &opts);
+	if (ret)
+		return ret;
+
+	return drm_dp_dpcd_write_data(&dp->aux, DP_TRAINING_LANE0_SET,
+				      training_set, lanes);
+}
+
+static int spacemit_dp_link_train_clock_recovery(struct spacemit_dp_dev *dp,
+						 enum spacemit_dp_link_rate rate,
+					    enum spacemit_dp_lane_count lanes)
+{
+	u8 link_status[DP_LINK_STATUS_SIZE];
+	u8 training_set[4] = {0};
+	int retries = 0;
+	int ret;
+
+	ret = drm_dp_dpcd_write_data(&dp->aux, DP_TRAINING_LANE0_SET,
+				     training_set, lanes);
+	if (ret < 0)
+		return ret;
+
+	ret = spacemit_dp_set_training_pattern(dp, DP_TRAINING_PATTERN_1);
+	if (ret < 0) {
+		spacemit_dp_set_training_pattern(dp,
+						 DP_TRAINING_PATTERN_DISABLE);
+		return ret;
+	}
+
+	while (retries < 8) {
+		drm_dp_link_train_clock_recovery_delay(&dp->aux, dp->dpcd);
+
+		ret = drm_dp_dpcd_read_link_status(&dp->aux, link_status);
+		if (ret < 0) {
+			spacemit_dp_set_training_pattern(dp,
+				DP_TRAINING_PATTERN_DISABLE);
+			return ret;
+		}
+
+		if (drm_dp_clock_recovery_ok(link_status, lanes))
+			return 0;
+
+		ret = spacemit_dp_link_apply_adjust(dp, link_status, lanes,
+						    training_set);
+		if (ret) {
+			spacemit_dp_set_training_pattern(dp,
+				DP_TRAINING_PATTERN_DISABLE);
+			return ret;
+		}
+
+		retries++;
+	}
+
+	dev_err(dp->dev, "link training: clock recovery failed\n");
+	spacemit_dp_set_training_pattern(dp, DP_TRAINING_PATTERN_DISABLE);
+	return -ETIMEDOUT;
+}
+
+static int spacemit_dp_link_train_channel_eq(struct spacemit_dp_dev *dp,
+					     enum spacemit_dp_link_rate rate,
+					enum spacemit_dp_lane_count lanes)
+{
+	u8 link_status[DP_LINK_STATUS_SIZE];
+	u8 training_set[4] = {0};
+	u8 training_pattern = DP_TRAINING_PATTERN_2;
+	int retries = 0;
+	int ret;
+
+	if (dp->dpcd[DP_MAX_LANE_COUNT] & DP_TPS3_SUPPORTED) {
+		training_pattern = DP_TRAINING_PATTERN_3;
+		dev_dbg(dp->dev, "link training: using TPS3\n");
+	} else {
+		dev_dbg(dp->dev, "link training: using TPS2\n");
+	}
+
+	ret = spacemit_dp_set_training_pattern(dp, training_pattern);
+	if (ret < 0) {
+		spacemit_dp_set_training_pattern(dp,
+						 DP_TRAINING_PATTERN_DISABLE);
+		return ret;
+	}
+
+	while (retries < 8) {
+		drm_dp_link_train_channel_eq_delay(&dp->aux, dp->dpcd);
+
+		ret = drm_dp_dpcd_read_link_status(&dp->aux, link_status);
+		if (ret < 0) {
+			spacemit_dp_set_training_pattern(dp,
+				DP_TRAINING_PATTERN_DISABLE);
+			return ret;
+		}
+
+		if (drm_dp_channel_eq_ok(link_status, lanes)) {
+			spacemit_dp_set_training_pattern(dp,
+				DP_TRAINING_PATTERN_DISABLE);
+			return 0;
+		}
+
+		ret = spacemit_dp_link_apply_adjust(dp, link_status, lanes,
+						    training_set);
+		if (ret) {
+			spacemit_dp_set_training_pattern(dp,
+				DP_TRAINING_PATTERN_DISABLE);
+			return ret;
+		}
+
+		retries++;
+	}
+
+	dev_err(dp->dev, "link training: channel EQ failed\n");
+	spacemit_dp_set_training_pattern(dp, DP_TRAINING_PATTERN_DISABLE);
+	return -ETIMEDOUT;
+}
+
+static int spacemit_dp_link_train(struct spacemit_dp_dev *dp, enum spacemit_dp_link_rate rate,
+				  enum spacemit_dp_lane_count lanes)
+{
+	int ret;
+	u8 link_config[1];
+	u8 bw_code;
+
+	/* Map Link Rate Enum to DPCD Bandwidth Code */
+	switch (rate) {
+	case SPACEMIT_DP_LINK_RATE_1_62:
+		bw_code = DP_LINK_BW_1_62;
+		break;
+	case SPACEMIT_DP_LINK_RATE_2_70:
+		bw_code = DP_LINK_BW_2_7;
+		break;
+	case SPACEMIT_DP_LINK_RATE_5_40:
+		bw_code = DP_LINK_BW_5_4;
+		break;
+	case SPACEMIT_DP_LINK_RATE_8_10:
+		bw_code = DP_LINK_BW_8_1;
+		break;
+	default:
+		bw_code = DP_LINK_BW_1_62;
+		break;
+	}
+
+	/*
+	 * A sink may act on the DP_LINK_BW_SET write, so set the lane count
+	 * first.
+	 */
+	link_config[0] = lanes;
+	if (dp->link.enhanced_framing)
+		link_config[0] |= DP_LANE_COUNT_ENHANCED_FRAME_EN;
+
+	ret = drm_dp_dpcd_write_byte(&dp->aux, DP_LANE_COUNT_SET,
+				     link_config[0]);
+	if (ret < 0) {
+		dev_err(dp->dev, "failed to set DPCD lane count\n");
+		return ret;
+	}
+
+	ret = drm_dp_dpcd_write_byte(&dp->aux, DP_LINK_BW_SET, bw_code);
+	if (ret < 0) {
+		dev_err(dp->dev, "failed to set DPCD link rate\n");
+		return ret;
+	}
+
+	ret = spacemit_dp_link_train_clock_recovery(dp, rate, lanes);
+	if (ret)
+		return ret;
+
+	ret = spacemit_dp_link_train_channel_eq(dp, rate, lanes);
+	if (ret)
+		return ret;
+
+	return 0;
+}
+
+static void spacemit_dp_hw_set_msa_and_enable_video(struct spacemit_dp_dev *dp,
+						    const struct drm_display_mode *mode,
+					       enum spacemit_dp_link_rate rate,
+					       enum spacemit_dp_lane_count lanes,
+		u32 color_format)
+{
+	u64 hb_num;
+	u32 link_rate;
+	u32 fp; /* Pixel clock in MHz */
+	u32 bpp, misc0;
+	u32 tu, tu_frac, tu_int, rd_thres;
+	u32 hsync_len;
+
+	/* mode->clock unit is kHz, fp unit is MHz */
+	if (dp->use_ext_pixel_clock)
+		fp = dp->pixel_clock / 1000;
+	else
+		fp = mode->clock / 1000;
+
+	if (fp == 0)
+		fp = 1; /* Prevent division by zero */
+
+	bpp = spacemit_dp_get_bpp(dp, color_format);
+
+	/* Calculate MISC0 */
+	/* bit0: 0 (Sync Clock) */
+	/* bits1-7: Color Format (000=RGB, 001=YCbCr422, 010=YCbCr444) */
+	/* bits5-7: BPC (001=8bpc, 010=10bpc, etc) */
+	switch (color_format) {
+	case SOC_VIDEO_RGB_6BIT:
+		misc0 = 0x00;
+		break;
+	case SOC_VIDEO_RGB_8BIT:
+		misc0 = 0x20;
+		break;
+	case SOC_VIDEO_RGB_10BIT:
+		misc0 = 0x40;
+		break;
+	case SOC_VIDEO_RGB_12BIT:
+		misc0 = 0x60;
+		break;
+	case SOC_VIDEO_RGB_16BIT:
+		misc0 = 0x80;
+		break;
+	case SOC_VIDEO_YUV422_8BIT:
+		misc0 = 0x22;
+		break;
+	case SOC_VIDEO_YUV422_10BIT:
+		misc0 = 0x42;
+		break;
+	case SOC_VIDEO_YUV422_12BIT:
+		misc0 = 0x62;
+		break;
+	case SOC_VIDEO_YUV422_16BIT:
+		misc0 = 0x82;
+		break;
+	case SOC_VIDEO_YUV444_8BIT:
+		misc0 = 0x24;
+		break;
+	case SOC_VIDEO_YUV444_10BIT:
+		misc0 = 0x44;
+		break;
+	case SOC_VIDEO_YUV444_12BIT:
+		misc0 = 0x64;
+		break;
+	case SOC_VIDEO_YUV444_16BIT:
+		misc0 = 0x84;
+		break;
+	default:
+		misc0 = 0x20;
+		break;
+	}
+
+	/*
+	 * HBlank interval = (htotal - hactive) * (LinkSymbolClock / 4) /
+	 * PixelClock, where LinkSymbolClock = LinkRate * 100 (1.62G -> 162MHz).
+	 * rate is in kHz, so link_rate = rate / 10000 gives the 162, 270, 540
+	 * form used below.
+	 */
+	link_rate = rate / 10000;
+
+	/* Multiply before dividing: hb_num = hblank * (link_rate / 4) / fp */
+	hb_num = (u64)(mode->htotal - mode->hdisplay) * link_rate;
+	do_div(hb_num, 4 * fp);
+
+	/* Transfer unit: tu = fp * bpp * 640 / (8 * lanes * link_rate) */
+	{
+		u64 temp_tu = (u64)fp * bpp * 640;
+		u32 den = 8 * lanes * link_rate;
+
+		do_div(temp_tu, den);
+		tu = temp_tu;
+	}
+	tu_frac = tu % 10;
+	tu_int  = tu / 10;
+
+	/* FIFO read threshold. */
+	if (tu_int < 6)
+		rd_thres = 32;
+	else if ((mode->htotal - mode->hdisplay) < 80)
+		rd_thres = 12;
+	else
+		rd_thres = 16;
+
+	dev_dbg(dp->dev,
+		"MSA: %dx%d, Rate:%d kHz, Lanes:%d, BPP:%d, TU:%d.%d\n",
+		mode->hdisplay, mode->vdisplay, rate, lanes, bpp, tu_int,
+		tu_frac);
+
+	regmap_write_bits(dp->regs, DP_VIDEO_VSAMPLE_REG, DP_VIDEO_MAPPING,
+			  FIELD_PREP(DP_VIDEO_MAPPING, color_format));
+
+	/*
+	 * The Saturn TMG raster is always positive-sync (hsp/vsp fixed to 1).
+	 */
+	regmap_write_bits(dp->regs, DP_VINPUT_POLARITY, DP_HSYNC_IN_POLARITY,
+			  FIELD_PREP(DP_HSYNC_IN_POLARITY, 1));
+	regmap_write_bits(dp->regs, DP_VINPUT_POLARITY, DP_VSYNC_IN_POLARITY,
+			  FIELD_PREP(DP_VSYNC_IN_POLARITY, 1));
+
+	/* Basic timing */
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG1, DP_HACTIVE,
+			  FIELD_PREP(DP_HACTIVE, mode->hdisplay));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG2, DP_VACTIVE,
+			  FIELD_PREP(DP_VACTIVE, mode->vdisplay));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG1, DP_HBLANK,
+			  FIELD_PREP(DP_HBLANK, mode->htotal - mode->hdisplay));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG2, DP_VBLANK,
+			  FIELD_PREP(DP_VBLANK, mode->vtotal - mode->vdisplay));
+
+	u32 hstart = mode->htotal - mode->hsync_end +
+			(mode->hsync_end - mode->hsync_start);
+	u32 vstart = mode->vtotal - mode->vsync_end +
+			(mode->vsync_end - mode->vsync_start);
+
+	regmap_write_bits(dp->regs, DP_VIDEO_MSA1, DP_HSTART,
+			  FIELD_PREP(DP_HSTART, hstart));
+	regmap_write_bits(dp->regs, DP_VIDEO_MSA1, DP_VSTART,
+			  FIELD_PREP(DP_VSTART, vstart));
+
+	hsync_len = mode->hsync_end - mode->hsync_start;
+
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG3, DP_H_SYNC_WIDTH,
+			  FIELD_PREP(DP_H_SYNC_WIDTH, hsync_len));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG4, DP_V_SYNC_WIDTH,
+			  FIELD_PREP(DP_V_SYNC_WIDTH, mode->vsync_end - mode->vsync_start));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG3, DP_H_FRONT_PORCH,
+			  FIELD_PREP(DP_H_FRONT_PORCH, mode->hsync_start - mode->hdisplay));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG4, DP_V_FRONT_PORCH,
+			  FIELD_PREP(DP_V_FRONT_PORCH, mode->vsync_start - mode->vdisplay));
+
+	/* MSA and MISC */
+	regmap_write_bits(dp->regs, DP_VIDEO_MSA2, DP_MISC0,
+			  FIELD_PREP(DP_MISC0, misc0));
+	regmap_write_bits(dp->regs, DP_VIDEO_MSA3, DP_MISC1,
+			  FIELD_PREP(DP_MISC1, 0));
+	regmap_write_bits(dp->regs, DP_VIDEO_MSA3, DP_NVID,
+			  FIELD_PREP(DP_NVID, 0));
+
+	/* Link layer parameters */
+	regmap_write_bits(dp->regs, DP_VIDEO_HBLANK_INTERVAL,
+			  DP_HBLANK_INTERVAL,
+			  FIELD_PREP(DP_HBLANK_INTERVAL, (u32)hb_num));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG5, DP_AVERAGE_BYTES_PER_TU,
+			  FIELD_PREP(DP_AVERAGE_BYTES_PER_TU, tu_int));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG5,
+			  DP_AVERAGE_BYTES_PER_TU_FRAC,
+			  FIELD_PREP(DP_AVERAGE_BYTES_PER_TU_FRAC, tu_frac));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG5, DP_INIT_THRESHOLD,
+			  FIELD_PREP(DP_INIT_THRESHOLD, rd_thres));
+
+	regmap_write_bits(dp->regs, DP_PHYIF_CTRL_ADDR, DP_PHY_SSC_DIS,
+			  FIELD_PREP(DP_PHY_SSC_DIS, 1));
+	regmap_write_bits(dp->regs, DP_CLK_INV_MUX, DP_REG_VID_CLK_SEL,
+			  FIELD_PREP(DP_REG_VID_CLK_SEL, 0));
+	regmap_write_bits(dp->regs, DP_VIDEO_COLORBAR_CFG, DP_VID_BIST_EN,
+			  FIELD_PREP(DP_VID_BIST_EN, 0));
+
+	dev_dbg(dp->dev, "enabling video stream\n");
+	regmap_write_bits(dp->regs, DP_VIDEO_VSAMPLE_REG,
+			  DP_VIDEO_STREAM_ENABLE,
+			  FIELD_PREP(DP_VIDEO_STREAM_ENABLE, 1));
+}
+
+static void spacemit_dp_hw_disable(struct spacemit_dp_dev *dp)
+{
+	dev_dbg(dp->dev, "disabling video and PHY\n");
+
+	regmap_write_bits(dp->regs, DP_VIDEO_VSAMPLE_REG,
+			  DP_VIDEO_STREAM_ENABLE,
+			  FIELD_PREP(DP_VIDEO_STREAM_ENABLE, 0));
+
+	regmap_write_bits(dp->regs, DP_PHYIF_CTRL_ADDR, DP_XMIT_ENABLE,
+			  FIELD_PREP(DP_XMIT_ENABLE, 0));
+
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_LDO_D0,
+			  FIELD_PREP(DP_ANA_TX_EN_LDO_D0, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_LDO_D1,
+			  FIELD_PREP(DP_ANA_TX_EN_LDO_D1, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_LDO_D2,
+			  FIELD_PREP(DP_ANA_TX_EN_LDO_D2, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_LDO_D3,
+			  FIELD_PREP(DP_ANA_TX_EN_LDO_D3, 0));
+
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_DRV_D0,
+			  FIELD_PREP(DP_ANA_TX_EN_DRV_D0, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_DRV_D1,
+			  FIELD_PREP(DP_ANA_TX_EN_DRV_D1, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_DRV_D2,
+			  FIELD_PREP(DP_ANA_TX_EN_DRV_D2, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_DRV_D3,
+			  FIELD_PREP(DP_ANA_TX_EN_DRV_D3, 0));
+
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_EN_P2S_D0,
+			  FIELD_PREP(DP_ANA_TX_EN_P2S_D0, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_EN_P2S_D1,
+			  FIELD_PREP(DP_ANA_TX_EN_P2S_D1, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_EN_P2S_D2,
+			  FIELD_PREP(DP_ANA_TX_EN_P2S_D2, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_EN_P2S_D3,
+			  FIELD_PREP(DP_ANA_TX_EN_P2S_D3, 0));
+
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL6, DP_ANA_BG_EN,
+			  FIELD_PREP(DP_ANA_BG_EN, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_BG_EN_RCAL,
+			  FIELD_PREP(DP_ANA_BG_EN_RCAL, 0));
+
+	/* Both PLLs. */
+	regmap_write_bits(dp->regs, DP_MPLL_CTRL0, DP_ANA_MPLL_PD,
+			  FIELD_PREP(DP_ANA_MPLL_PD, 1));
+	regmap_write_bits(dp->regs, DP_PREPLL_CTRL0, DP_ANA_PREPLL_PD,
+			  FIELD_PREP(DP_ANA_PREPLL_PD, 1));
+}
+
+/* Calculate required bandwidth in kbps (Pixel Clock * Bits Per Pixel) */
+static u32 spacemit_dp_calc_required_bw(const struct drm_display_mode *mode,
+					int bpp)
+{
+	return mode->clock * bpp;
+}
+
+/*
+ * Calculate available link capacity in kbps (taking 8b/10b overhead into
+ * account)
+ */
+static u32 spacemit_dp_calc_link_capacity(enum spacemit_dp_link_rate rate,
+					  enum spacemit_dp_lane_count lanes)
+{
+	u32 rate_khz = rate;
+	u32 lane_count = lanes;
+
+	/* Capacity = Rate(kHz) * Lanes * 0.8 */
+	return (rate_khz * lane_count * 8) / 10;
+}
+
+static enum drm_connector_status
+spacemit_dp_bridge_detect(struct drm_bridge *bridge, struct drm_connector *connector)
+{
+	struct spacemit_dp_dev *dp = container_of(bridge,
+						  struct spacemit_dp_dev,
+						  bridge);
+	enum drm_connector_status status;
+
+	/*
+	 * The eDP panel is fixed, and DP_HPD_IN_STATUS does not read
+	 * meaningfully on that path. Report CONNECTED only once panel-edp has
+	 * joined the chain.
+	 */
+	if (dp->edp_mode) {
+		if (!dp->next_bridge)
+			return connector_status_disconnected;
+		return connector_status_connected;
+	}
+
+	/*
+	 * The HPD interrupt is gated by the DP pixel clock, which is off
+	 * exactly when a plug must be caught. Read the level register live
+	 * instead.
+	 */
+	guard(mutex)(&dp->mode_lock);
+	status = spacemit_dp_hw_detect_hpd(dp);
+
+	/*
+	 * The bind-time cap read only covers a sink that was already there.
+	 * Without a re-read here, link.max_rate stays 0 and every link config
+	 * is filtered out.
+	 */
+	if (status == connector_status_connected &&
+	    dp->connector_status != connector_status_connected)
+		spacemit_dp_hw_read_sink_caps(dp);
+
+	dp->connector_status = status;
+
+	return status;
+}
+
+static int spacemit_dp_conn_get_edid_block(void *data, u8 *buf,
+					   unsigned int block, size_t len)
+{
+	struct spacemit_dp_dev *dp = data;
+	u8 offset = (block & 1) * EDID_LENGTH;
+	u8 segment = block >> 1;
+	struct i2c_msg msgs[] = {
+		{
+			.addr = SPACEMIT_DP_DDC_SEGMENT_ADDR,
+			.len = 1,
+			.buf = &segment,
+		}, {
+			.addr = DDC_ADDR,
+			.len = 1,
+			.buf = &offset,
+		}, {
+			.addr = DDC_ADDR,
+			.flags = I2C_M_RD,
+			.len = len,
+			.buf = buf,
+		},
+	};
+	/* Segment 0 needs no segment-pointer write. */
+	struct i2c_msg *first = segment ? &msgs[0] : &msgs[1];
+	int num = segment ? 3 : 2;
+	int ret, retry;
+
+	/*
+	 * One i2c_transfer per block: the DP I2C-over-AUX core splits it
+	 * into MOT-chained AUX transactions, so the sink sees a repeated
+	 * start and the segment pointer survives until the read ends.
+	 */
+	for (retry = 0; retry < 3; retry++) {
+		ret = i2c_transfer(&dp->aux.ddc, first, num);
+		if (ret == num)
+			return 0;
+	}
+
+	dev_err(dp->dev, "EDID read failed, block %u: %d\n", block, ret);
+	return ret < 0 ? ret : -EIO;
+}
+
+static const struct drm_edid *
+spacemit_dp_bridge_edid_read(struct drm_bridge *bridge, struct drm_connector *connector)
+{
+	struct spacemit_dp_dev *dp = container_of(bridge,
+						  struct spacemit_dp_dev,
+						  bridge);
+	const struct drm_edid *edid = NULL;
+	int retry;
+
+	scoped_guard(mutex, &dp->mode_lock) {
+		for (retry = 0; retry < SPACEMIT_DP_SINK_READY_RETRIES; retry++) {
+			if (retry) {
+				spacemit_dp_aux_hw_reset(dp);
+				msleep(SPACEMIT_DP_SINK_READY_DELAY_MS);
+			}
+
+			edid = drm_edid_read_custom(connector,
+				spacemit_dp_conn_get_edid_block, dp);
+			if (edid)
+				break;
+		}
+	}
+
+	if (!edid)
+		dev_err(dp->dev, "failed to read EDID\n");
+
+	return edid;
+}
+
+static enum drm_mode_status
+spacemit_dp_bridge_mode_valid(struct drm_bridge *bridge,
+			      const struct drm_display_info *info,
+			 const struct drm_display_mode *mode)
+{
+	struct spacemit_dp_dev *dp = container_of(bridge,
+						  struct spacemit_dp_dev,
+						  bridge);
+	unsigned int max_link_rate, max_lanes;
+	u64 max_bw, req_bw;
+	const unsigned int bpp = 24;
+
+	/*
+	 * drm_dp_max_link_rate() is in 10 kHz units; clamp to what the PHY can
+	 * do.
+	 */
+	max_link_rate = min_t(unsigned int, drm_dp_max_link_rate(dp->dpcd),
+			      SPACEMIT_DP_SOURCE_MAX_RATE / 10);
+	max_lanes = drm_dp_max_lane_count(dp->dpcd);
+	if (!max_link_rate || !max_lanes) {
+		/* Sink not enumerated yet -- accept and re-check at modeset. */
+		return MODE_OK;
+	}
+
+	/*
+	 * max_link_rate is per lane in 10 kHz units. The x10 to reach kHz and
+	 * the 8b/10b coding overhead cancel down to a plain "* 8".
+	 */
+	max_bw = (u64)max_link_rate * max_lanes * 8;
+
+	/* Required bandwidth: pixel clock (kHz) * bits per pixel. */
+	req_bw = (u64)mode->clock * bpp;
+
+	if (req_bw > max_bw) {
+		drm_dbg_kms(bridge->dev,
+			    "rejecting %dx%d@%d: %llu > %llu kbit/s (%u kHz x%u lanes)\n",
+			    mode->hdisplay, mode->vdisplay, drm_mode_vrefresh(mode),
+			    req_bw, max_bw, max_link_rate * 10, max_lanes);
+		return MODE_CLOCK_HIGH;
+	}
+
+	return MODE_OK;
+}
+
+static const struct drm_encoder_funcs spacemit_dp_encoder_funcs = {
+	.destroy = drm_encoder_cleanup,
+};
+
+static void spacemit_dp_bridge_atomic_enable(struct drm_bridge *bridge,
+					     struct drm_atomic_commit *state)
+{
+	struct spacemit_dp_dev *dp = container_of(bridge,
+						  struct spacemit_dp_dev,
+						  bridge);
+	struct drm_bridge_state *new_bridge_state =
+		drm_atomic_get_new_bridge_state(state, bridge);
+	struct spacemit_dp_bridge_state *st =
+			to_spacemit_dp_bridge_state(new_bridge_state);
+	struct drm_connector_state *conn_state =
+		drm_atomic_get_new_connector_state(state, dp->connector);
+	struct drm_crtc_state *crtc_state =
+		drm_atomic_get_new_crtc_state(state, conn_state->crtc);
+	struct drm_display_mode *adjusted_mode = &crtc_state->adjusted_mode;
+	const struct spacemit_dp_link_config *cfg;
+	bool trained;
+	u64 clk_val;
+	u64 set_clk_val;
+	u32 req_bw;
+	u32 hpd;
+	int start;
+	int bpp;
+	int i;
+
+	guard(mutex)(&dp->mode_lock);
+	drm_mode_copy(&dp->mode, adjusted_mode);
+
+	/* Enable-time retry for the caps .detect() could not read. */
+	if (!dp->link.max_rate || !dp->link.max_num_lanes)
+		spacemit_dp_hw_read_sink_caps(dp);
+
+	if (dp->use_ext_pixel_clock && dp->pxclk) {
+		set_clk_val = adjusted_mode->clock * 1000;
+		if (set_clk_val) {
+			set_clk_val = clk_round_rate(dp->pxclk, set_clk_val);
+			clk_val = clk_get_rate(dp->pxclk);
+			if (clk_val != set_clk_val) {
+				clk_set_rate(dp->pxclk, set_clk_val);
+				dev_dbg(dp->dev, "set dp pxclk=%lld\n",
+					set_clk_val);
+			}
+		}
+		clk_val = clk_get_rate(dp->pxclk);
+		dp->pixel_clock = clk_val / 1000;
+	}
+
+	bpp = spacemit_dp_get_bpp(dp, st->color_format);
+	req_bw = spacemit_dp_calc_required_bw(adjusted_mode, bpp);
+
+	/*
+	 * Start where atomic_check() left off, but keep the upgrade walk as
+	 * fallback.
+	 */
+	start = st->link_cfg_idx >= 0 ? st->link_cfg_idx : 0;
+
+	for (trained = false, i = start;
+	     i < ARRAY_SIZE(spacemit_dp_link_priority_table); i++) {
+		u32 capacity;
+
+		cfg = &spacemit_dp_link_priority_table[i];
+
+		/* Source and sink limits */
+		if (cfg->rate > dp->link.max_rate || cfg->lanes > dp->link.max_num_lanes)
+			continue;
+
+		/* Bandwidth requirement */
+		capacity = spacemit_dp_calc_link_capacity(cfg->rate,
+							  cfg->lanes);
+		if (capacity < req_bw)
+			continue;
+
+		dev_dbg(dp->dev, "trying link config: rate %d lanes %d (capacity %d, required %d)\n",
+			cfg->rate, cfg->lanes, capacity, req_bw);
+
+		spacemit_dp_link_disable(dp);
+
+		/* Apply Hardware Settings */
+		if (spacemit_dp_set_plls(dp, cfg->rate,
+					 dp->use_ext_pixel_clock ? dp->pixel_clock
+								 : adjusted_mode->clock))
+			continue;
+
+		if (spacemit_dp_phy_set_lanes(dp, cfg->lanes))
+			continue;
+
+		if (spacemit_dp_phy_on(dp))
+			continue;
+
+		/*
+		 * Debounce: the transmitter was power-cycled just now and a
+		 * sink may legitimately drop HPD across a link-down. eDP is
+		 * hardwired and noisy.
+		 */
+		if (!dp->edp_mode &&
+		    regmap_read_poll_timeout(dp->regs, DP_HPD_INTERRUPT_STATUS, hpd,
+					     hpd & DP_HPD_IN_STATUS,
+					     SPACEMIT_DP_HPD_POLL_US,
+					     SPACEMIT_DP_HPD_DEBOUNCE_US)) {
+			dev_warn(dp->dev,
+				 "sink is not asserting HPD; not training\n");
+			break;
+		}
+
+		if (dp->edp_mode) {
+			regmap_write_bits(dp->regs, DP_CORE_CONTROL,
+					  DP_ENABLE_EDP,
+					  FIELD_PREP(DP_ENABLE_EDP, 0x1));
+			regmap_write_bits(dp->regs, DP_VIDEO_VSAMPLE_REG,
+					  DP_STREAM_ENC_EN,
+					  FIELD_PREP(DP_STREAM_ENC_EN, 0x1));
+			update_edp_config(dp, true);
+		}
+
+		/* Execute Link Training */
+		if (spacemit_dp_link_train(dp, cfg->rate, cfg->lanes) == 0) {
+			dev_dbg(dp->dev,
+				"link training succeeded: rate %d lanes %d\n",
+				cfg->rate, cfg->lanes);
+			trained = true;
+			break;
+		}
+
+		dev_warn(dp->dev, "link training failed: rate %d lanes %d, trying next config\n",
+			 cfg->rate, cfg->lanes);
+	}
+
+	/*
+	 * The DPU's pixel clock comes from the PHY PLL, so scanning out into an
+	 * untrained link never reaches CFG_RDY and wedges the CRTC on
+	 * flip_done.
+	 */
+	if (!trained) {
+		dev_err(dp->dev,
+			"no usable link config; leaving video disabled (%u kHz x%u lanes)\n",
+			dp->link.max_rate, dp->link.max_num_lanes);
+		/*
+		 * The caps may be from a sink that has since been swapped out.
+		 */
+		dp->link.max_rate = 0;
+		dp->link.max_num_lanes = 0;
+
+		/*
+		 * Deferred: we are inside the commit and hold the modeset
+		 * locks.
+		 */
+		schedule_work(&dp->modeset_retry_work);
+		return;
+	}
+
+	spacemit_dp_hw_set_msa_and_enable_video(dp, adjusted_mode,
+						dp->link_rate, dp->lane_count,
+						st->color_format);
+}
+
+static void spacemit_dp_bridge_atomic_disable(struct drm_bridge *bridge,
+					      struct drm_atomic_commit *state)
+{
+	struct spacemit_dp_dev *dp = container_of(bridge,
+						  struct spacemit_dp_dev,
+						  bridge);
+
+	guard(mutex)(&dp->mode_lock);
+
+	spacemit_dp_link_disable(dp);
+	memset(&dp->mode, 0, sizeof(dp->mode));
+}
+
+static int spacemit_dp_bridge_atomic_check(struct drm_bridge *bridge,
+					   struct drm_bridge_state *bridge_state,
+				      struct drm_crtc_state *crtc_state,
+				      struct drm_connector_state *conn_state)
+{
+	struct spacemit_dp_dev *dp = container_of(bridge,
+						  struct spacemit_dp_dev,
+						  bridge);
+	struct spacemit_dp_bridge_state *st =
+			to_spacemit_dp_bridge_state(bridge_state);
+	const struct drm_display_mode *mode = &crtc_state->adjusted_mode;
+	int bpp;
+	u32 req_bw;
+	int i;
+
+	/* A single output bus format, see ->atomic_get_output_bus_fmts(). */
+	st->color_format = SOC_VIDEO_RGB_8BIT;
+	bpp = spacemit_dp_get_bpp(dp, st->color_format);
+	req_bw = spacemit_dp_calc_required_bw(mode, bpp);
+
+	/* No caps yet: leave idx unset and let ->atomic_enable() retry. */
+	if (!dp->link.max_rate || !dp->link.max_num_lanes) {
+		st->link_cfg_idx = -1;
+		return 0;
+	}
+
+	for (i = 0; i < ARRAY_SIZE(spacemit_dp_link_priority_table); i++) {
+		const struct spacemit_dp_link_config *cfg =
+				&spacemit_dp_link_priority_table[i];
+
+		if (cfg->rate > dp->link.max_rate ||
+		    cfg->lanes > dp->link.max_num_lanes)
+			continue;
+		if (spacemit_dp_calc_link_capacity(cfg->rate, cfg->lanes) < req_bw)
+			continue;
+
+		st->link_cfg_idx = i;
+		return 0;
+	}
+
+	drm_dbg_kms(bridge->dev,
+		    "no DP link cfg fits %dx%d@%d (need %u kbit/s; sink %u kHz x %u lanes)\n",
+		    mode->hdisplay, mode->vdisplay, drm_mode_vrefresh(mode),
+		    req_bw, dp->link.max_rate * 10, dp->link.max_num_lanes);
+	return -EINVAL;
+}
+
+static struct drm_bridge_state *
+spacemit_dp_bridge_atomic_duplicate_state(struct drm_bridge *bridge)
+{
+	struct spacemit_dp_bridge_state *cur =
+		to_spacemit_dp_bridge_state(drm_priv_to_bridge_state(bridge->base.state));
+	struct spacemit_dp_bridge_state *new;
+
+	new = kzalloc_obj(*new);
+	if (!new)
+		return NULL;
+
+	__drm_atomic_helper_bridge_duplicate_state(bridge, &new->base);
+	new->link_cfg_idx = cur->link_cfg_idx;
+	new->color_format = cur->color_format;
+	return &new->base;
+}
+
+static void
+spacemit_dp_bridge_atomic_destroy_state(struct drm_bridge *bridge,
+					struct drm_bridge_state *state)
+{
+	kfree(to_spacemit_dp_bridge_state(state));
+}
+
+static u32 *spacemit_dp_bridge_atomic_get_output_bus_fmts(struct drm_bridge *bridge,
+							  struct drm_bridge_state *bridge_state,
+				struct drm_crtc_state *crtc_state,
+				struct drm_connector_state *conn_state,
+				unsigned int *num_output_fmts)
+{
+	u32 *output_fmts;
+
+	output_fmts = kmalloc_obj(*output_fmts, GFP_KERNEL);
+	if (!output_fmts) {
+		*num_output_fmts = 0;
+		return NULL;
+	}
+
+	output_fmts[0] = MEDIA_BUS_FMT_RGB888_1X24;
+	*num_output_fmts = 1;
+	return output_fmts;
+}
+
+static struct drm_bridge_state *
+spacemit_dp_bridge_atomic_create_state(struct drm_bridge *bridge)
+{
+	struct spacemit_dp_bridge_state *new;
+
+	new = kzalloc_obj(*new);
+	if (!new)
+		return ERR_PTR(-ENOMEM);
+
+	__drm_atomic_helper_bridge_state_init(&new->base, bridge);
+	new->link_cfg_idx = -1;
+	new->color_format = SOC_VIDEO_RGB_8BIT;
+	return &new->base;
+}
+
+static int spacemit_dp_bridge_attach(struct drm_bridge *bridge,
+				     struct drm_encoder *encoder,
+				enum drm_bridge_attach_flags flags)
+{
+	struct spacemit_dp_dev *dp = container_of(bridge,
+						  struct spacemit_dp_dev,
+						  bridge);
+
+	if (!dp->next_bridge)
+		return 0;
+
+	return drm_bridge_attach(encoder, dp->next_bridge, bridge, flags);
+}
+
+/*
+ * AUX-bus done-probing callback. panel-edp may probe inline (before .attach) or
+ * off the deferred-probe queue (after it), so handle both orders.
+ */
+static int spacemit_dp_edp_link_panel(struct drm_dp_aux *aux)
+{
+	struct spacemit_dp_dev *dp = container_of(aux, struct spacemit_dp_dev,
+						  aux);
+	struct drm_bridge *next;
+	int ret;
+
+	next = devm_drm_of_get_bridge(dp->dev, dp->dev->of_node, 1, 0);
+	if (IS_ERR(next))
+		return PTR_ERR(next);
+
+	dp->next_bridge = next;
+
+	/*
+	 * A non-NULL bridge.encoder means .attach already ran: late-chain
+	 * panel-edp.
+	 */
+	if (dp->bridge.encoder) {
+		ret = drm_bridge_attach(dp->bridge.encoder, next,
+					&dp->bridge,
+					DRM_BRIDGE_ATTACH_NO_CONNECTOR);
+		if (ret)
+			return ret;
+
+		/*
+		 * eDP HPD-IN is unreliable, so bind may have skipped the cap
+		 * read and left link.max_rate at 0. The AUX bus just worked;
+		 * read them here.
+		 */
+		if (dp->edp_mode) {
+			guard(mutex)(&dp->mode_lock);
+			spacemit_dp_hw_read_sink_caps(dp);
+		}
+
+		drm_bridge_hpd_notify(&dp->bridge,
+				      spacemit_dp_bridge_detect(&dp->bridge,
+								dp->connector));
+	}
+
+	return 0;
+}
+
+static const struct drm_bridge_funcs spacemit_dp_bridge_funcs = {
+	.attach				= spacemit_dp_bridge_attach,
+	.atomic_enable			= spacemit_dp_bridge_atomic_enable,
+	.atomic_disable			= spacemit_dp_bridge_atomic_disable,
+	.atomic_check			= spacemit_dp_bridge_atomic_check,
+	.atomic_duplicate_state		= spacemit_dp_bridge_atomic_duplicate_state,
+	.atomic_destroy_state		= spacemit_dp_bridge_atomic_destroy_state,
+	.atomic_create_state		= spacemit_dp_bridge_atomic_create_state,
+	.atomic_get_output_bus_fmts	= spacemit_dp_bridge_atomic_get_output_bus_fmts,
+	.mode_valid			= spacemit_dp_bridge_mode_valid,
+	.detect				= spacemit_dp_bridge_detect,
+	.edid_read			= spacemit_dp_bridge_edid_read,
+};
+
+static irqreturn_t spacemit_dp_irq_handler(int irq, void *data)
+{
+	struct spacemit_dp_dev *dp = data;
+	irqreturn_t ret = IRQ_NONE;
+	int idx;
+
+	if (!drm_dev_enter(dp->drm, &idx))
+		return IRQ_NONE;
+
+	/*
+	 * eDP .detect() ignores the HPD level (and never records it), so a
+	 * level flip would wake the thread forever; ack and ignore.
+	 */
+	if (dp->edp_mode) {
+		spacemit_dp_hw_clean_hpd(dp);
+		drm_dev_exit(idx);
+		return IRQ_HANDLED;
+	}
+
+	/*
+	 * Only compare here: the authoritative connector_status update
+	 * happens in the thread under mode_lock, via .detect().
+	 */
+	if (spacemit_dp_hw_detect_hpd(dp) != READ_ONCE(dp->connector_status))
+		ret = IRQ_WAKE_THREAD; /* Call hotplug_event */
+	if (spacemit_dp_hw_clean_hpd(dp) && ret == IRQ_NONE)
+		ret = IRQ_HANDLED;
+
+	drm_dev_exit(idx);
+	return ret;
+}
+
+static irqreturn_t spacemit_dp_hotplug_event_handler(int irq, void *data)
+{
+	struct spacemit_dp_dev *dp = data;
+	enum drm_connector_status status;
+	int idx;
+
+	if (!drm_dev_enter(dp->drm, &idx))
+		return IRQ_HANDLED;
+
+	/*
+	 * .detect() owns mode_lock and re-reads the sink caps on a fresh plug
+	 */
+	status = spacemit_dp_bridge_detect(&dp->bridge, dp->connector);
+	drm_bridge_hpd_notify(&dp->bridge, status);
+
+	drm_dev_exit(idx);
+	return IRQ_HANDLED;
+}
+
+static int spacemit_dp_resource_init(struct spacemit_dp_dev *dp,
+				     struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	void __iomem *base;
+
+	base = devm_platform_ioremap_resource(pdev, 0);
+	if (IS_ERR(base))
+		return dev_err_probe(dev, PTR_ERR(base),
+				     "failed to map registers\n");
+	dp->regs = devm_regmap_init_mmio(dev, base, &spacemit_dp_regmap_config);
+	if (IS_ERR(dp->regs))
+		return dev_err_probe(dev, PTR_ERR(dp->regs),
+				     "failed to init DP regmap\n");
+
+	/*
+	 * The pixel clock comes from the PHY PLL, so the controller never sets
+	 * it.
+	 */
+	dp->use_ext_pixel_clock = false;
+
+	dp->irq = platform_get_irq(pdev, 0);
+	if (dp->irq < 0) {
+		dev_err(dev, "failed to get IRQ\n");
+		return dp->irq;
+	}
+
+	return 0;
+}
+
+static int spacemit_dp_dev_init(struct spacemit_dp_dev *dp)
+{
+	int ret;
+	u32 m_isel = 0x5, m_mainsel = 0x19;
+	u32 m_pre = 0x0, m_post = 0x2;
+	u32 tx_mode = 0x1, tx_pre = 0x0;
+	u32 clk_div = 24 * 1000 / 100;
+
+	regmap_write_bits(dp->regs, DP_PHYIF_CTRL_ADDR, DP_XMIT_ENABLE,
+			  FIELD_PREP(DP_XMIT_ENABLE, 0));
+	usleep_range(2000, 4000);
+
+	regmap_write_bits(dp->regs, DP_MPLL_CTRL0, DP_ANA_MPLL_PD,
+			  FIELD_PREP(DP_ANA_MPLL_PD, 1));
+	regmap_write_bits(dp->regs, DP_PREPLL_CTRL0, DP_ANA_PREPLL_PD,
+			  FIELD_PREP(DP_ANA_PREPLL_PD, 1));
+	usleep_range(2000, 4000);
+
+	/* Reset Controller and PHY */
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_CONTROLLER_RESET,
+			  FIELD_PREP(DP_CONTROLLER_RESET, 0x1));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_PHY_RESET,
+			  FIELD_PREP(DP_PHY_RESET, 0x1));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_HDCP_RESET,
+			  FIELD_PREP(DP_HDCP_RESET, 0x1));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_AUX_RESET,
+			  FIELD_PREP(DP_AUX_RESET, 0x1));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_VIDEO_RESET,
+			  FIELD_PREP(DP_VIDEO_RESET, 0x1));
+	usleep_range(5000, 8000);
+
+	/* Release the resets */
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_CONTROLLER_RESET,
+			  FIELD_PREP(DP_CONTROLLER_RESET, 0x0));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_PHY_RESET,
+			  FIELD_PREP(DP_PHY_RESET, 0x0));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_HDCP_RESET,
+			  FIELD_PREP(DP_HDCP_RESET, 0x0));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_AUX_RESET,
+			  FIELD_PREP(DP_AUX_RESET, 0x0));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_VIDEO_RESET,
+			  FIELD_PREP(DP_VIDEO_RESET, 0x0));
+	usleep_range(2000, 4000);
+
+	regmap_write(dp->regs, DP_GENERAL_INTERRUPT,
+		     DP_AUX_REPLY_EVENT_INT_STA);
+
+	regmap_write_bits(dp->regs, DP_CORE_CONTROL,
+			  DP_DEFAULT_FAST_LINK_TRAIN_EN,
+			  FIELD_PREP(DP_DEFAULT_FAST_LINK_TRAIN_EN, 0x0));
+	regmap_write_bits(dp->regs, DP_CORE_CONTROL, DP_SCRAMBLER_DISABLE,
+			  FIELD_PREP(DP_SCRAMBLER_DISABLE, 0x0));
+	regmap_write_bits(dp->regs, DP_CORE_CONTROL, DP_SCALE_DOWN_MODE,
+			  FIELD_PREP(DP_SCALE_DOWN_MODE, 0x0));
+	regmap_write_bits(dp->regs, DP_PHYIF_CTRL_ADDR, DP_XMIT_ENABLE,
+			  FIELD_PREP(DP_XMIT_ENABLE, 0));
+
+	regmap_write_bits(dp->regs, DP_VIDEO_VSAMPLE_REG,
+			  DP_VIDEO_STREAM_ENABLE,
+			  FIELD_PREP(DP_VIDEO_STREAM_ENABLE, 0));
+
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_AUX_REPLY_EVENT_INT_STA_MSK,
+			  FIELD_PREP(DP_AUX_REPLY_EVENT_INT_STA_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_HDCP_INT_STA_MSK,
+			  FIELD_PREP(DP_HDCP_INT_STA_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_ILLEGAL_AUX_CMD_INT_STA_MSK,
+			  FIELD_PREP(DP_ILLEGAL_AUX_CMD_INT_STA_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_TYPE_C_EVENT_MSK,
+			  FIELD_PREP(DP_TYPE_C_EVENT_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK, DP_DSC_EVENT_MSK,
+			  FIELD_PREP(DP_DSC_EVENT_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_SDP_INT_STA_S3_MSK,
+			  FIELD_PREP(DP_SDP_INT_STA_S3_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_SDP_INT_STA_S2_MSK,
+			  FIELD_PREP(DP_SDP_INT_STA_S2_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_SDP_INT_STA_S1_MSK,
+			  FIELD_PREP(DP_SDP_INT_STA_S1_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_SDP_INT_STA_S0_MSK,
+			  FIELD_PREP(DP_SDP_INT_STA_S0_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+		DP_VIDEO_FIFO_OVERFLOW_INT_STA_S3_MSK,
+		FIELD_PREP(DP_VIDEO_FIFO_OVERFLOW_INT_STA_S3_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+		DP_VIDEO_FIFO_OVERFLOW_INT_STA_S2_MSK,
+		FIELD_PREP(DP_VIDEO_FIFO_OVERFLOW_INT_STA_S2_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+		DP_VIDEO_FIFO_OVERFLOW_INT_STA_S1_MSK,
+		FIELD_PREP(DP_VIDEO_FIFO_OVERFLOW_INT_STA_S1_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+		DP_VIDEO_FIFO_OVERFLOW_INT_STA_S0_MSK,
+		FIELD_PREP(DP_VIDEO_FIFO_OVERFLOW_INT_STA_S0_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_HPD_INTERRUPT_ENABLE,
+			  DP_SINK_IRQ_EVENT_MSK,
+			  FIELD_PREP(DP_SINK_IRQ_EVENT_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_HPD_INT_STA_MSK,
+			  FIELD_PREP(DP_HPD_INT_STA_MSK, 0x1));
+	regmap_write_bits(dp->regs, DP_HPD_INTERRUPT_ENABLE,
+			  DP_HOT_PLUG_EVENT_MSK,
+			  FIELD_PREP(DP_HOT_PLUG_EVENT_MSK, 0x1));
+	regmap_write_bits(dp->regs, DP_HPD_INTERRUPT_ENABLE,
+			  DP_HOT_UNPLUG_EVENT_MSK,
+			  FIELD_PREP(DP_HOT_UNPLUG_EVENT_MSK, 0x1));
+	regmap_write_bits(dp->regs, DP_HPD_INTERRUPT_ENABLE,
+			  DP_SINK_UNPLUG_ERROR_EVENT_MSK,
+			  FIELD_PREP(DP_SINK_UNPLUG_ERROR_EVENT_MSK, 0x0));
+	usleep_range(2000, 4000);
+
+	regmap_write_bits(dp->regs, DP_PHYIF_CTRL_ADDR, DP_PHY_BUSY_BYP,
+			  FIELD_PREP(DP_PHY_BUSY_BYP, 0x1));
+
+	/* Enable enhanced framing */
+	regmap_write_bits(dp->regs, DP_CORE_CONTROL, DP_ENHANCE_FRAMING_EN,
+			  FIELD_PREP(DP_ENHANCE_FRAMING_EN, 0x1));
+
+	ret = spacemit_dp_set_plls(dp, SPACEMIT_DP_LINK_RATE_2_70,
+				   dp->use_ext_pixel_clock ? 150000 : 148500);
+	if (ret)
+		return ret;
+
+	ret = spacemit_dp_phy_set_lanes(dp, SPACEMIT_DP_LANE_2);
+	if (ret)
+		return ret;
+
+	ret = spacemit_dp_phy_on(dp);
+	if (ret)
+		return ret;
+
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D0,
+			  FIELD_PREP(DP_ANA_TX_MODE_D0, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D1,
+			  FIELD_PREP(DP_ANA_TX_MODE_D1, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D2,
+			  FIELD_PREP(DP_ANA_TX_MODE_D2, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D3,
+			  FIELD_PREP(DP_ANA_TX_MODE_D3, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_RTCAL_FREQDIV_HBIT,
+		FIELD_PREP(DP_ANA_RTCAL_FREQDIV_HBIT, (clk_div >> 8) & 0x7f));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_RTCAL_BYPASS,
+			  FIELD_PREP(DP_ANA_RTCAL_BYPASS, 1));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_RTCAL_FREQDIV_LBIT,
+		FIELD_PREP(DP_ANA_RTCAL_FREQDIV_LBIT, clk_div & 0xff));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_BG_RCAL_SEL,
+			  FIELD_PREP(DP_ANA_BG_RCAL_SEL, 0));
+
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL8, DP_ANA_TX_RTM_D3,
+			  FIELD_PREP(DP_ANA_TX_RTM_D3, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL8, DP_ANA_TX_RTM_D2,
+			  FIELD_PREP(DP_ANA_TX_RTM_D2, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL8, DP_ANA_TX_RTM_D1,
+			  FIELD_PREP(DP_ANA_TX_RTM_D1, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL8, DP_ANA_TX_RTM_D0,
+			  FIELD_PREP(DP_ANA_TX_RTM_D0, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_RTCAL_BYPASS,
+			  FIELD_PREP(DP_ANA_RTCAL_BYPASS, 1));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_RTCAL_BYPASS,
+			  FIELD_PREP(DP_ANA_RTCAL_BYPASS, 0));
+	msleep(100);
+
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_PRE_D3,
+			  FIELD_PREP(DP_ANA_TX_MODE_PRE_D3, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_PRE_D2,
+			  FIELD_PREP(DP_ANA_TX_MODE_PRE_D2, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_PRE_D1,
+			  FIELD_PREP(DP_ANA_TX_MODE_PRE_D1, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_PRE_D0,
+			  FIELD_PREP(DP_ANA_TX_MODE_PRE_D0, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_DE_D3,
+			  FIELD_PREP(DP_ANA_TX_MODE_DE_D3, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_DE_D2,
+			  FIELD_PREP(DP_ANA_TX_MODE_DE_D2, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_DE_D1,
+			  FIELD_PREP(DP_ANA_TX_MODE_DE_D1, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_DE_D0,
+			  FIELD_PREP(DP_ANA_TX_MODE_DE_D0, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_POSTSEL_PRE_D3,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_PRE_D3, tx_pre));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_POSTSEL_PRE_D2,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_PRE_D2, tx_pre));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_POSTSEL_PRE_D1,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_PRE_D1, tx_pre));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_POSTSEL_PRE_D0,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_PRE_D0, tx_pre));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_ISEL_DRV_D3,
+			  FIELD_PREP(DP_ANA_TX_ISEL_DRV_D3, m_isel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_ISEL_DRV_D2,
+			  FIELD_PREP(DP_ANA_TX_ISEL_DRV_D2, m_isel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL1, DP_ANA_TX_MAINSEL_D2,
+			  FIELD_PREP(DP_ANA_TX_MAINSEL_D2, m_mainsel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL1, DP_ANA_TX_MAINSEL_D3,
+			  FIELD_PREP(DP_ANA_TX_MAINSEL_D3, m_mainsel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL1, DP_ANA_TX_ISEL_DRV_D1,
+			  FIELD_PREP(DP_ANA_TX_ISEL_DRV_D1, m_isel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL1, DP_ANA_TX_ISEL_DRV_D0,
+			  FIELD_PREP(DP_ANA_TX_ISEL_DRV_D0, m_isel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL2, DP_ANA_TX_POSTSEL_D1,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_D1, m_post));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL2, DP_ANA_TX_POSTSEL_D0,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_D0, m_post));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL2, DP_ANA_TX_POSTSEL_D3,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_D3, m_post));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL2, DP_ANA_TX_POSTSEL_D2,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_D2, m_post));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL2, DP_DA_TX_MAINSEL_D0_4_0,
+			  FIELD_PREP(DP_DA_TX_MAINSEL_D0_4_0, m_mainsel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL2, DP_ANA_TX_MAINSEL_D1,
+			  FIELD_PREP(DP_ANA_TX_MAINSEL_D1, m_mainsel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_PRESEL_D1,
+			  FIELD_PREP(DP_ANA_TX_PRESEL_D1, m_pre));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_PRESEL_D0,
+			  FIELD_PREP(DP_ANA_TX_PRESEL_D0, m_pre));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_PRESEL_D3,
+			  FIELD_PREP(DP_ANA_TX_PRESEL_D3, m_pre));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_PRESEL_D2,
+			  FIELD_PREP(DP_ANA_TX_PRESEL_D2, m_pre));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D3,
+			  FIELD_PREP(DP_ANA_TX_MODE_D3, tx_mode));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D2,
+			  FIELD_PREP(DP_ANA_TX_MODE_D2, tx_mode));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D1,
+			  FIELD_PREP(DP_ANA_TX_MODE_D1, tx_mode));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D0,
+			  FIELD_PREP(DP_ANA_TX_MODE_D0, tx_mode));
+	regmap_write_bits(dp->regs, DP_ANA_TX_AUX_CTRL, DP_ANA_TX_AUX_RX_VSEL,
+			  FIELD_PREP(DP_ANA_TX_AUX_RX_VSEL, 0x0));
+
+	return 0;
+}
+
+static int spacemit_dp_bind(struct device *dev, struct device *master,
+			    void *data)
+{
+	struct spacemit_dp_dev *dp = dev_get_drvdata(dev);
+	struct drm_device *drm = data;
+	struct device_node *ep;
+	int ret;
+
+	dp->drm = drm;
+	dp->aux.drm_dev = drm;
+	ret = drm_dp_aux_register(&dp->aux);
+	if (ret) {
+		dev_err(dev, "failed to register DP AUX: %d\n", ret);
+		return ret;
+	}
+
+	ret = drm_encoder_init(drm, &dp->encoder, &spacemit_dp_encoder_funcs,
+			       DRM_MODE_ENCODER_NONE, NULL);
+	if (ret) {
+		dev_err(dev, "failed to init encoder\n");
+		goto err_aux;
+	}
+
+	ep = of_graph_get_endpoint_by_regs(dev->of_node, 0, -1);
+	if (ep) {
+		struct device_node *remote = of_graph_get_remote_port(ep);
+
+		dp->encoder.possible_crtcs = drm_of_crtc_port_mask(drm, remote);
+		of_node_put(remote);
+		of_node_put(ep);
+	}
+
+	ret = drm_bridge_attach(&dp->encoder, &dp->bridge, NULL,
+				DRM_BRIDGE_ATTACH_NO_CONNECTOR);
+	if (ret) {
+		dev_err(dev, "failed to attach bridge: %d\n", ret);
+		goto err_encoder;
+	}
+
+	dp->connector = drm_bridge_connector_init(drm, &dp->encoder);
+	if (IS_ERR(dp->connector)) {
+		ret = PTR_ERR(dp->connector);
+		dev_err(dev, "failed to init bridge connector: %d\n", ret);
+		goto err_encoder;
+	}
+
+	drm_connector_attach_encoder(dp->connector, &dp->encoder);
+
+	ret = spacemit_dp_dev_init(dp);
+	if (ret)
+		goto err_encoder;
+
+	spacemit_dp_link_disable(dp);
+
+	dp->connector_status = spacemit_dp_hw_detect_hpd(dp);
+	spacemit_dp_hw_clean_hpd(dp);
+
+	if (dp->connector_status == connector_status_connected)
+		spacemit_dp_hw_read_sink_caps(dp);
+
+	/*
+	 * With panel-edp still deferred the chain is incomplete: a CONNECTED
+	 * notification here makes fbdev commit a modeset that wedges on
+	 * flip_done.
+	 */
+	if (dp->next_bridge || !dp->edp_mode)
+		drm_bridge_hpd_notify(&dp->bridge,
+				      spacemit_dp_bridge_detect(&dp->bridge,
+								dp->connector));
+
+	enable_irq(dp->irq);
+
+	return 0;
+
+err_encoder:
+	drm_encoder_cleanup(&dp->encoder);
+err_aux:
+	drm_dp_aux_unregister(&dp->aux);
+	return ret;
+}
+
+static void spacemit_dp_unbind(struct device *dev, struct device *master,
+			       void *data)
+{
+	struct spacemit_dp_dev *dp = dev_get_drvdata(dev);
+
+	disable_irq(dp->irq);
+
+	/* The connector this work touches dies with the DRM device. */
+	cancel_work_sync(&dp->modeset_retry_work);
+
+	drm_dp_aux_unregister(&dp->aux);
+
+	spacemit_dp_hw_disable(dp);
+
+	drm_encoder_cleanup(&dp->encoder);
+
+	dp->drm = NULL;
+	dp->connector = NULL;
+	dp->aux.drm_dev = NULL;
+}
+
+static const struct component_ops spacemit_dp_ops = {
+	.bind = spacemit_dp_bind,
+	.unbind = spacemit_dp_unbind,
+};
+
+/*
+ * Registered after devm_of_platform_populate(), so devres runs it before
+ * the PHY child (and the PLL clock it provides) is torn down: unregistering
+ * a clock that still has an enabled child warns, a disabled child is
+ * orphaned silently.
+ */
+static void spacemit_dp_pxclk_disable(void *data)
+{
+	clk_disable_unprepare(data);
+}
+
+static int inno_dp_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct device_node *aux_bus_np;
+	struct spacemit_dp_dev *dp;
+	struct reset_control *rstc;
+	struct resource *res;
+	int ret;
+
+	dp = devm_drm_bridge_alloc(dev, struct spacemit_dp_dev, bridge,
+				   &spacemit_dp_bridge_funcs);
+	if (IS_ERR(dp))
+		return PTR_ERR(dp);
+
+	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
+	if (!res)
+		return dev_err_probe(dev, -ENODEV,
+				     "no MMIO resource\n");
+
+	dp->dev = dev;
+	INIT_WORK(&dp->modeset_retry_work, spacemit_dp_modeset_retry_work);
+
+	/* eDP instances have an aux-bus child node; DP instances do not. */
+	aux_bus_np = of_get_child_by_name(dev->of_node, "aux-bus");
+	dp->edp_mode = !!aux_bus_np;
+	of_node_put(aux_bus_np);
+
+	dp->bridge.of_node = dev->of_node;
+	dp->connector_status = connector_status_disconnected;
+	ret = devm_mutex_init(dev, &dp->mode_lock);
+	if (ret)
+		return ret;
+
+	rstc = devm_reset_control_get_exclusive_deasserted(&pdev->dev, NULL);
+	if (IS_ERR(rstc)) {
+		ret = dev_err_probe(dev, PTR_ERR(rstc),
+				    "failed to get reset\n");
+		return ret;
+	}
+
+	dp->pxclk = devm_clk_get(dev, "pxclk");
+	if (IS_ERR(dp->pxclk)) {
+		ret = dev_err_probe(dev, PTR_ERR(dp->pxclk),
+				    "failed to get pxclk\n");
+		return ret;
+	}
+
+	if (dp->edp_mode) {
+		dp->bridge.type = DRM_MODE_CONNECTOR_eDP;
+		/*
+		 * OP_DETECT is required: without it
+		 * drm_bridge_connector_detect() hard-codes CONNECTED for any
+		 * eDP connector and fbdev wedges at bind.
+		 */
+		dp->bridge.ops = DRM_BRIDGE_OP_DETECT | DRM_BRIDGE_OP_EDID |
+				 DRM_BRIDGE_OP_HPD;
+	} else {
+		dp->bridge.type = DRM_MODE_CONNECTOR_DisplayPort;
+		/*
+		 * No OP_HPD: the HPD interrupt is gated by the DP pixel clock.
+		 * Leaving it out marks the connector POLL_CONNECT |
+		 * POLL_DISCONNECT instead.
+		 */
+		dp->bridge.ops = DRM_BRIDGE_OP_DETECT | DRM_BRIDGE_OP_EDID;
+	}
+
+	ret = clk_prepare_enable(dp->pxclk);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to enable pxclk\n");
+
+	/*
+	 * The PHY child shares this device's regmap, so the regmap must exist
+	 * before devm_of_platform_populate() lets the PHY probe and
+	 * devm_phy_get() succeed.
+	 */
+	ret = spacemit_dp_resource_init(dp, pdev);
+	if (ret)
+		goto err_disable_pxclk;
+
+	ret = devm_of_platform_populate(dev);
+	if (ret) {
+		dev_err_probe(dev, ret, "failed to populate child devices\n");
+		goto err_disable_pxclk;
+	}
+
+	dp->phy = devm_phy_get(dev, "dp");
+	if (IS_ERR(dp->phy)) {
+		ret = dev_err_probe(dev, PTR_ERR(dp->phy),
+				    "failed to get DP PHY\n");
+		goto err_disable_pxclk;
+	}
+
+	/*
+	 * The PHY exposes its PLL as the APMU pixel-clock mux's external
+	 * parent.
+	 */
+	dp->pll_clk = devm_clk_get(dev, "pll");
+	if (IS_ERR(dp->pll_clk)) {
+		ret = dev_err_probe(dev, PTR_ERR(dp->pll_clk),
+				    "failed to get PHY pixel clock\n");
+		goto err_disable_pxclk;
+	}
+
+	ret = clk_set_parent(dp->pxclk, dp->pll_clk);
+	if (ret) {
+		dev_err(dev, "failed to route eDP pixel mux to PHY PLL: %d\n", ret);
+		goto err_disable_pxclk;
+	}
+
+	ret = devm_add_action_or_reset(dev, spacemit_dp_pxclk_disable,
+				       dp->pxclk);
+	if (ret)
+		return ret;
+
+	/*
+	 * Panel discovery needs the AUX channel here; registration waits for
+	 * bind.
+	 */
+	dp->aux.name = "spacemit-dp-aux";
+	dp->aux.dev = dev;
+	dp->aux.transfer = spacemit_dp_aux_transfer;
+	dp->aux.no_zero_sized = true;
+	drm_dp_aux_init(&dp->aux);
+	dp->bridge.ddc = &dp->aux.ddc;
+
+	/*
+	 * eDP hands panel power, enable-GPIO and backlight to panel-edp over
+	 * the AUX bus. A DP instance has no aux-bus child and takes the
+	 * -ENODEV path.
+	 */
+	if (dp->edp_mode) {
+		ret = devm_of_dp_aux_populate_bus(&dp->aux,
+						  spacemit_dp_edp_link_panel);
+		if (ret) {
+			dev_err_probe(dev, ret,
+				"eDP: failed to populate aux-bus panel\n");
+			return ret;
+		}
+	}
+
+	/*
+	 * drm_bridge_attach() expects the bridge to already be on the global
+	 * list.
+	 */
+	ret = devm_drm_bridge_add(dev, &dp->bridge);
+	if (ret) {
+		dev_err(dev, "failed to register DP bridge: %d\n", ret);
+		return ret;
+	}
+
+	platform_set_drvdata(pdev, dp);
+
+	/*
+	 * The handler needs bind-created state, so keep the line disabled until
+	 * bind.
+	 */
+	ret = devm_request_threaded_irq(dev, dp->irq, spacemit_dp_irq_handler,
+					spacemit_dp_hotplug_event_handler,
+					IRQF_NO_AUTOEN, dev_name(dev), dp);
+	if (ret)
+		return ret;
+
+	ret = component_add(dev, &spacemit_dp_ops);
+	if (ret) {
+		dev_err(dev, "failed to add component: %d\n", ret);
+		return ret;
+	}
+
+	return 0;
+
+err_disable_pxclk:
+	clk_disable_unprepare(dp->pxclk);
+	return ret;
+}
+
+static void inno_dp_remove(struct platform_device *pdev)
+{
+	component_del(&pdev->dev, &spacemit_dp_ops);
+}
+
+static void inno_dp_shutdown(struct platform_device *pdev)
+{
+	struct spacemit_dp_dev *dp = platform_get_drvdata(pdev);
+
+	/* A deferred or failed bind leaves drvdata NULL. */
+	if (!dp)
+		return;
+
+	spacemit_dp_hw_disable(dp);
+}
+
+static const struct of_device_id spacemit_dp_match[] = {
+	{ .compatible = "spacemit,k3-inno-dp" },
+	{}
+};
+MODULE_DEVICE_TABLE(of, spacemit_dp_match);
+
+static struct platform_driver inno_dp_driver = {
+	.probe = inno_dp_probe,
+	.remove = inno_dp_remove,
+	.shutdown = inno_dp_shutdown,
+	.driver = {
+		.name = "spacemit-inno-dp-drv",
+		.of_match_table = spacemit_dp_match,
+	},
+};
+
+module_platform_driver(inno_dp_driver);
+
+MODULE_DESCRIPTION("SpacemiT K3 Innosilicon DP/eDP bridge driver");
+MODULE_LICENSE("GPL");
+MODULE_SOFTDEP("pre: phy-k3-inno-dp");
diff --git a/drivers/gpu/drm/spacemit/spacemit_inno_dp.h b/drivers/gpu/drm/spacemit/spacemit_inno_dp.h
new file mode 100644
index 0000000000000..680e2f41cd486
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/spacemit_inno_dp.h
@@ -0,0 +1,329 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ */
+
+#ifndef __SPACEMIT_INNO_DP_H__
+#define __SPACEMIT_INNO_DP_H__
+
+/* Register field access uses GENMASK/BIT + FIELD_PREP/FIELD_GET. */
+#include <linux/bits.h>
+
+#define DP_CORE_CONTROL                      0x0018
+#define  DP_SCRAMBLER_DISABLE               BIT(31)
+#define  DP_ENHANCE_FRAMING_EN              BIT(30)
+#define  DP_DEFAULT_FAST_LINK_TRAIN_EN      BIT(29)
+#define  DP_SCALE_DOWN_MODE                 BIT(28)
+#define  DP_FORCE_HPD                       BIT(27)
+#define  DP_ENABLE_EDP                      BIT(4)
+
+#define DP_SOFT_RESET                        0x001c
+#define  DP_CONTROLLER_RESET                BIT(31)
+#define  DP_PHY_RESET                       BIT(30)
+#define  DP_HDCP_RESET                      BIT(29)
+#define  DP_AUDIO_RESET                     BIT(28)
+#define  DP_AUX_RESET                       BIT(27)
+#define  DP_VIDEO_RESET                     GENMASK(3, 0)
+
+#define DP_CLK_INV_MUX                       0x0028
+#define  DP_REG_VID_CLK_SEL                 BIT(0)
+
+/* Interrupt status latches; write 1 to clear. */
+#define DP_GENERAL_INTERRUPT                 0x0080
+#define  DP_AUX_REPLY_EVENT_INT_STA         BIT(16)
+
+#define DP_GENERAL_INTERRUPT_MASK            0x0084
+#define  DP_HPD_INT_STA_MSK                 BIT(17)
+#define  DP_AUX_REPLY_EVENT_INT_STA_MSK     BIT(16)
+#define  DP_HDCP_INT_STA_MSK                BIT(15)
+#define  DP_ILLEGAL_AUX_CMD_INT_STA_MSK     BIT(14)
+#define  DP_TYPE_C_EVENT_MSK                BIT(13)
+#define  DP_DSC_EVENT_MSK                   BIT(12)
+#define  DP_SDP_INT_STA_S3_MSK              BIT(11)
+#define  DP_SDP_INT_STA_S2_MSK              BIT(10)
+#define  DP_SDP_INT_STA_S1_MSK              BIT(9)
+#define  DP_SDP_INT_STA_S0_MSK              BIT(8)
+#define  DP_VIDEO_FIFO_OVERFLOW_INT_STA_S3_MSK BIT(7)
+#define  DP_VIDEO_FIFO_OVERFLOW_INT_STA_S2_MSK BIT(6)
+#define  DP_VIDEO_FIFO_OVERFLOW_INT_STA_S1_MSK BIT(5)
+#define  DP_VIDEO_FIFO_OVERFLOW_INT_STA_S0_MSK BIT(4)
+
+#define DP_HPD_INTERRUPT_STATUS              0x0088
+#define  DP_HOT_PLUG_EVENT                  BIT(29)
+#define  DP_HOT_UNPLUG_EVENT                BIT(28)
+#define  DP_HPD_IN_STATUS                   BIT(26)
+
+#define DP_HPD_INTERRUPT_ENABLE              0x008c
+#define  DP_SINK_IRQ_EVENT_MSK              BIT(31)
+#define  DP_HOT_PLUG_EVENT_MSK              BIT(29)
+#define  DP_HOT_UNPLUG_EVENT_MSK            BIT(28)
+#define  DP_SINK_UNPLUG_ERROR_EVENT_MSK     BIT(27)
+
+#define DP_PHYIF_CTRL_ADDR                   0x0100
+#define  DP_PHY_BUSY_BYP                    BIT(31)
+#define  DP_TPS_SEL                         GENMASK(28, 25)
+#define  DP_XMIT_ENABLE                     GENMASK(20, 17)
+#define  DP_PHY_POWERDOWN                   GENMASK(16, 13)
+#define  DP_PHY_SSC_DIS                     BIT(8)
+#define  DP_PHY_NUM_LANES                   GENMASK(6, 5)
+#define  DP_PHY_RATE                        GENMASK(1, 0)
+
+#define DP_PHYIF_TX_EQ_ADDR                  0x0104
+#define  DP_PHY_LANE3_TX_VSWING             GENMASK(21, 20)
+#define  DP_PHY_LANE3_TX_PREEMP             GENMASK(19, 18)
+#define  DP_PHY_LANE2_TX_VSWING             GENMASK(15, 14)
+#define  DP_PHY_LANE2_TX_PREEMP             GENMASK(13, 12)
+#define  DP_PHY_LANE1_TX_VSWING             GENMASK(9, 8)
+#define  DP_PHY_LANE1_TX_PREEMP             GENMASK(7, 6)
+#define  DP_PHY_LANE0_TX_VSWING             GENMASK(3, 2)
+#define  DP_PHY_LANE0_TX_PREEMP             GENMASK(1, 0)
+
+#define DP_MPLL_CTRL0                        0x0180
+#define  DP_ANA_MPLL_FBDIV_LBIT             GENMASK(31, 24)
+#define  DP_ANA_MPLL_SEL_EXTWAVE            BIT(22)
+#define  DP_ANA_MPLL_DISABLE_SSCG           BIT(21)
+#define  DP_ANA_MPLL_DOWNSPREAD             BIT(20)
+#define  DP_ANA_MPLL_FBDIV_HBIT             GENMASK(19, 16)
+#define  DP_ANA_MPLL_OBSEN                  BIT(15)
+#define  DP_ANA_MPLL_OBSSEL                 BIT(14)
+#define  DP_ANA_MPLL_PREDIV                 GENMASK(13, 8)
+#define  DP_AD_LOCK_COREPLL                 BIT(7)
+#define  DP_ANA_MPLL_DACPD                  BIT(5)
+#define  DP_ANA_MPLL_DSMPD                  BIT(4)
+#define  DP_ANA_MPLL_PD                     BIT(0)
+
+#define DP_MPLL_CTRL1                        0x0184
+#define  DP_ANA_MPLL_FRAC_LBIT              GENMASK(23, 16)
+#define  DP_ANA_MPLL_FRAC_MBIT              GENMASK(15, 8)
+#define  DP_ANA_MPLL_FRAC_HBIT              GENMASK(7, 0)
+
+#define DP_MPLL_CTRL2                        0x0188
+#define  DP_ANA_MPLL_VCOCLK_DIV8_EN         BIT(16)
+#define  DP_ANA_MPLL_POSTDIVEN              BIT(11)
+#define  DP_ANA_MPLL_POSTDIV                GENMASK(10, 8)
+
+#define DP_PREPLL_CTRL0                      0x0190
+#define  DP_ANA_PREPLL_FBDIV2_LBIT          GENMASK(31, 24)
+#define  DP_ANA_PREPLL_FBDIV2_HBIT          GENMASK(19, 16)
+#define  DP_ANA_PREPLL_PREDIV               GENMASK(13, 8)
+#define  DP_AD_LOCK_PIXELPLL                BIT(7)
+#define  DP_ANA_PREPLL_DACPD                BIT(5)
+#define  DP_ANA_PREPLL_DSMPD                BIT(4)
+#define  DP_REG_PCLK_OUTPUT_NORMAL          BIT(1)
+#define  DP_ANA_PREPLL_PD                   BIT(0)
+
+#define DP_PREPLL_CTRL1                      0x0194
+#define  DP_ANA_PREPLL_PRECLK_DIVM          GENMASK(17, 16)
+#define  DP_ANA_PREPLL_PRECLK_DIVAUX        GENMASK(12, 8)
+
+#define DP_PREPLL_CTRL2                      0x0198
+#define  DP_ANA_PREPLL_PCLKDIV5_EN          BIT(31)
+#define  DP_ANA_PREPLL_PCLK_DIVAUX          GENMASK(28, 24)
+#define  DP_ANA_PREPLL_LOCK_BYPEN           BIT(17)
+#define  DP_ANA_PREPLL_LOWFRE_EN            BIT(16)
+#define  DP_ANA_PREPLL_TESTEN               BIT(15)
+#define  DP_ANA_PREPLL_TESTSEL              GENMASK(14, 12)
+#define  DP_ANA_PREPLL_HDMI_EN              BIT(9)
+#define  DP_ANA_PREPLL_DP_EN                BIT(8)
+
+#define DP_PREPLL_SSC_CTRL                   0x019c
+#define  DP_ANA_PREPLL_FRAC2_LBIT           GENMASK(23, 16)
+#define  DP_ANA_PREPLL_FRAC2_MBIT           GENMASK(15, 8)
+#define  DP_ANA_PREPLL_FRAC2_HBIT           GENMASK(7, 0)
+
+#define DP_ANA_TX_CTRL0                      0x01a0
+#define  DP_ANA_MPLL_CLKDIV_16M             GENMASK(13, 8)
+
+#define DP_ANA_TX_ENABLE                     0x01a4
+#define  DP_ANA_TX_ISEL_DRV_D3              GENMASK(31, 28)
+#define  DP_ANA_TX_ISEL_DRV_D2              GENMASK(27, 24)
+#define  DP_AD_TX_ESDSENSE_D3               GENMASK(23, 22)
+#define  DP_AD_TX_ESDSENSE_D2               GENMASK(21, 20)
+#define  DP_AD_TX_ESDSENSE_D1               GENMASK(19, 18)
+#define  DP_AD_TX_ESDSENSE_D0               GENMASK(17, 16)
+#define  DP_ANA_TX_EN_LDO_D3                BIT(11)
+#define  DP_ANA_TX_EN_LDO_D2                BIT(10)
+#define  DP_ANA_TX_EN_LDO_D1                BIT(9)
+#define  DP_ANA_TX_EN_LDO_D0                BIT(8)
+#define  DP_ANA_TX_EN_BYP_D3                BIT(7)
+#define  DP_ANA_TX_EN_BYP_D2                BIT(6)
+#define  DP_ANA_TX_EN_BYP_D1                BIT(5)
+#define  DP_ANA_TX_EN_BYP_D0                BIT(4)
+#define  DP_ANA_TX_EN_DRV_D3                BIT(3)
+#define  DP_ANA_TX_EN_DRV_D2                BIT(2)
+#define  DP_ANA_TX_EN_DRV_D1                BIT(1)
+#define  DP_ANA_TX_EN_DRV_D0                BIT(0)
+
+#define DP_ANA_TX_CTRL1                      0x01a8
+#define  DP_ANA_TX_PHASE_D2                 GENMASK(31, 30)
+#define  DP_ANA_TX_MAINSEL_D2               GENMASK(28, 24)
+#define  DP_ANA_TX_PHASE_D3                 GENMASK(23, 22)
+#define  DP_ANA_TX_MAINSEL_D3               GENMASK(20, 16)
+#define  DP_ANA_TX_ISEL_LDO_D3              BIT(11)
+#define  DP_ANA_TX_ISEL_LDO_D2              BIT(10)
+#define  DP_ANA_TX_ISEL_LDO_D1              BIT(9)
+#define  DP_ANA_TX_ISEL_LDO_D0              BIT(8)
+#define  DP_ANA_TX_ISEL_DRV_D1              GENMASK(7, 4)
+#define  DP_ANA_TX_ISEL_DRV_D0              GENMASK(3, 0)
+
+#define DP_ANA_TX_CTRL2                      0x01ac
+#define  DP_ANA_TX_POSTSEL_D1               GENMASK(31, 28)
+#define  DP_ANA_TX_POSTSEL_D0               GENMASK(27, 24)
+#define  DP_ANA_TX_POSTSEL_D3               GENMASK(23, 20)
+#define  DP_ANA_TX_POSTSEL_D2               GENMASK(19, 16)
+#define  DP_ANA_TX_PHASE_D0                 GENMASK(15, 14)
+#define  DP_DA_TX_MAINSEL_D0_4_0            GENMASK(12, 8)
+#define  DP_ANA_TX_PHASE_D1                 GENMASK(7, 6)
+#define  DP_ANA_TX_MAINSEL_D1               GENMASK(4, 0)
+
+#define DP_ANA_TX_CTRL3                      0x01b0
+#define  DP_ANA_TX_EN_P2S_D3                BIT(31)
+#define  DP_ANA_TX_EN_P2S_D2                BIT(30)
+#define  DP_ANA_TX_EN_P2S_D1                BIT(29)
+#define  DP_ANA_TX_EN_P2S_D0                BIT(28)
+#define  DP_ANA_TX_MODE_D3                  BIT(27)
+#define  DP_ANA_TX_MODE_D2                  BIT(26)
+#define  DP_ANA_TX_MODE_D1                  BIT(25)
+#define  DP_ANA_TX_MODE_D0                  BIT(24)
+#define  DP_ANA_TX_PRESEL_D1                GENMASK(14, 12)
+#define  DP_ANA_TX_PRESEL_D0                GENMASK(10, 8)
+#define  DP_ANA_TX_PRESEL_D3                GENMASK(6, 4)
+#define  DP_ANA_TX_PRESEL_D2                GENMASK(2, 0)
+
+#define DP_ANA_TX_CTRL6                      0x01bc
+#define  DP_ANA_BG_VREF5                    GENMASK(15, 14)
+#define  DP_ANA_BG_VREF4                    GENMASK(13, 12)
+#define  DP_ANA_BG_VREF3                    GENMASK(11, 10)
+#define  DP_ANA_BG_VREF2                    GENMASK(9, 8)
+#define  DP_ANA_BG_VREF                     GENMASK(7, 6)
+#define  DP_ANA_BG_VREF1                    GENMASK(5, 4)
+#define  DP_ANA_BG_EN_CHOP                  BIT(3)
+#define  DP_ANA_BG_EN                       BIT(2)
+#define  DP_ANA_BG_ISEL                     BIT(1)
+#define  DP_ANA_BG_VSEL                     BIT(0)
+
+#define DP_ANA_TX_CTRL7                      0x01c0
+#define  DP_ANA_BG_EN_RCAL                  BIT(27)
+#define  DP_ANA_BG_RCAL_SEL                 GENMASK(26, 25)
+#define  DP_AD_BG_RCAL_OUT                  BIT(24)
+#define  DP_ANA_RTCAL_FREQDIV_LBIT          GENMASK(23, 16)
+#define  DP_ANA_RTCAL_BYPASS                BIT(15)
+#define  DP_ANA_RTCAL_FREQDIV_HBIT          GENMASK(14, 8)
+#define  DP_ANA_BG_RCAL_VAL                 GENMASK(5, 0)
+
+#define DP_ANA_TX_CTRL8                      0x01c4
+#define  DP_ANA_TX_RTM_D3                   GENMASK(29, 24)
+#define  DP_ANA_TX_RTM_D2                   GENMASK(21, 16)
+#define  DP_ANA_TX_RTM_D1                   GENMASK(13, 8)
+#define  DP_ANA_TX_RTM_D0                   GENMASK(5, 0)
+
+#define DP_ANA_TX_AUX_CTRL                   0x01d0
+#define  DP_ANA_TX_AUX_RX_VSEL              GENMASK(13, 12)
+
+#define DP_ANA_LANE_EMPH                     0x01dc
+#define  DP_ANA_TX_POSTSEL_PRE_D3           GENMASK(23, 20)
+#define  DP_ANA_TX_POSTSEL_PRE_D2           GENMASK(19, 16)
+#define  DP_ANA_TX_POSTSEL_PRE_D1           GENMASK(15, 12)
+#define  DP_ANA_TX_POSTSEL_PRE_D0           GENMASK(11, 8)
+#define  DP_ANA_TX_MODE_DE_D3               BIT(7)
+#define  DP_ANA_TX_MODE_DE_D2               BIT(6)
+#define  DP_ANA_TX_MODE_DE_D1               BIT(5)
+#define  DP_ANA_TX_MODE_DE_D0               BIT(4)
+#define  DP_ANA_TX_MODE_PRE_D3              BIT(3)
+#define  DP_ANA_TX_MODE_PRE_D2              BIT(2)
+#define  DP_ANA_TX_MODE_PRE_D1              BIT(1)
+#define  DP_ANA_TX_MODE_PRE_D0              BIT(0)
+
+#define DP_VIDEO_VSAMPLE_REG                 0x0200
+#define  DP_VIDEO_STREAM_ENABLE             BIT(28)
+#define  DP_VIDEO_MAPPING                   GENMASK(26, 22)
+#define  DP_STREAM_ENC_EN                   BIT(18)
+
+#define DP_VINPUT_POLARITY                   0x020c
+#define  DP_HSYNC_IN_POLARITY               BIT(29)
+#define  DP_VSYNC_IN_POLARITY               BIT(28)
+
+#define DP_VIDEO_CONFIG2                     0x0210
+#define  DP_VACTIVE                         GENMASK(31, 16)
+#define  DP_VBLANK                          GENMASK(15, 0)
+
+#define DP_VIDEO_CONFIG1                     0x0214
+#define  DP_HBLANK                          GENMASK(29, 16)
+#define  DP_HACTIVE                         GENMASK(15, 0)
+
+#define DP_VIDEO_CONFIG5                     0x0218
+#define  DP_AVERAGE_BYTES_PER_TU            GENMASK(31, 25)
+#define  DP_AVERAGE_BYTES_PER_TU_FRAC       GENMASK(24, 21)
+#define  DP_INIT_THRESHOLD                  GENMASK(18, 10)
+
+#define DP_VIDEO_CONFIG3                     0x021c
+#define  DP_H_FRONT_PORCH                   GENMASK(31, 16)
+#define  DP_H_SYNC_WIDTH                    GENMASK(15, 0)
+
+#define DP_VIDEO_CONFIG4                     0x0220
+#define  DP_V_FRONT_PORCH                   GENMASK(31, 16)
+#define  DP_V_SYNC_WIDTH                    GENMASK(15, 0)
+
+#define DP_VIDEO_MSA1                        0x0224
+#define  DP_HSTART                          GENMASK(31, 16)
+#define  DP_VSTART                          GENMASK(15, 0)
+
+#define DP_VIDEO_MSA2                        0x0228
+#define  DP_MISC0                           GENMASK(7, 0)
+
+#define DP_VIDEO_MSA3                        0x022c
+#define  DP_NVID                            GENMASK(31, 8)
+#define  DP_MISC1                           GENMASK(7, 0)
+
+#define DP_VIDEO_HBLANK_INTERVAL             0x0230
+#define  DP_HBLANK_INTERVAL                 GENMASK(31, 16)
+
+#define DP_VIDEO_COLORBAR_CFG                0x0238
+#define  DP_VID_BIST_EN                     BIT(0)
+
+#define DP_AUDIO_CONFIG1                     0x0300
+#define  DP_AUDIO_INF_SELECT                BIT(31)
+#define  DP_AUDIO_MUTE                      BIT(30)
+#define  DP_HBR_MODE_ENABLE                 BIT(29)
+#define  DP_AUDIO_DATA_IN_EN                GENMASK(28, 25)
+#define  DP_I2S_AUDIO_MODE                  GENMASK(24, 23)
+#define  DP_AUD_ADJUST_SEL                  BIT(22)
+#define  DP_AUDIO_DATA_WIDTH                GENMASK(21, 17)
+#define  DP_AUDIO_NUM_CHANNELS              GENMASK(16, 14)
+#define  DP_AUDIO_PACKET_ID                 GENMASK(13, 6)
+#define  DP_AUDIO_TIMESTAMP_VERSION_NUM     GENMASK(5, 0)
+
+#define DP_AUX_STS_REG                       0x0400
+#define  DP_AUX_STATUS                      GENMASK(31, 24)
+#define  DP_AUX_REPLY_ERR_CODE              GENMASK(6, 4)
+
+#define DP_AUX_CMD_REG                       0x0404
+#define  DP_AUX_LENGTH                      GENMASK(27, 24)
+#define  DP_AUX_ADDR                        GENMASK(23, 4)
+#define  DP_AUX_CMD_TYPE                    GENMASK(3, 0)
+
+#define DP_AUX_START_REG                     0x0408
+#define  DP_AUX_START                       BIT(31)
+
+#define DP_AUX_DATA4_REG                     0x040c
+#define  DP_AUX_DATA4                       GENMASK(31, 0)
+
+#define DP_AUX_DATA3_REG                     0x0410
+#define  DP_AUX_DATA3                       GENMASK(31, 0)
+
+#define DP_AUX_DATA2_REG                     0x0414
+#define  DP_AUX_DATA2                       GENMASK(31, 0)
+
+#define DP_AUX_DATA1_REG                     0x0418
+#define  DP_AUX_DATA1                       GENMASK(31, 0)
+
+#define DP_SDP_HORIZONTAL_CTRL               0x0420
+#define  DP_AUD_STREAM_HORIZONTAL_EN        BIT(13)
+#define  DP_AUD_TIMESTAMP_HORIZONTAL_EN     BIT(12)
+
+#define DP_SDP_VERTICAL_CTRL                 0x0424
+#define  DP_AUD_STREAM_VERTICAL_EN          BIT(13)
+#define  DP_AUD_TIMESTAMP_VERTICAL_EN       BIT(12)
+
+#endif /* __SPACEMIT_INNO_DP_H__ */

-- 
2.43.0



_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv

WARNING: multiple messages have this Message-ID (diff)
From: Cody Kang <codykang.hk@gmail.com>
To: "David Airlie" <airlied@gmail.com>,
	"Simona Vetter" <simona@ffwll.ch>,
	"Maarten Lankhorst" <maarten.lankhorst@linux.intel.com>,
	"Maxime Ripard" <mripard@kernel.org>,
	"Thomas Zimmermann" <tzimmermann@suse.de>,
	"Rob Herring" <robh@kernel.org>,
	"Krzysztof Kozlowski" <krzk+dt@kernel.org>,
	"Conor Dooley" <conor+dt@kernel.org>,
	"Yixun Lan" <dlan@kernel.org>, "Vinod Koul" <vkoul@kernel.org>,
	"Neil Armstrong" <neil.armstrong@linaro.org>,
	"Haylen Chu" <heylenay@4d2.org>,
	"Michael Turquette" <mturquette@baylibre.com>,
	"Stephen Boyd" <sboyd@kernel.org>,
	"Brian Masney" <bmasney@redhat.com>,
	"Philipp Zabel" <p.zabel@pengutronix.de>,
	"Paul Walmsley" <pjw@kernel.org>,
	"Palmer Dabbelt" <palmer@dabbelt.com>,
	"Albert Ou" <aou@eecs.berkeley.edu>,
	"Alexandre Ghiti" <alex@ghiti.fr>, "Yao Zi" <me@ziyao.cc>,
	"Uwe Kleine-König" <u.kleine-koenig@baylibre.com>,
	"Guodong Xu" <docular.xu@gmail.com>
Cc: dri-devel@lists.freedesktop.org, linux-riscv@lists.infradead.org,
	 devicetree@vger.kernel.org, spacemit@lists.linux.dev,
	 linux-kernel@vger.kernel.org, linux-phy@lists.infradead.org,
	 linux-clk@vger.kernel.org, Cody Kang <codykang.hk@gmail.com>
Subject: [PATCH v2 12/17] drm/spacemit: add Innosilicon DP/eDP controller bridge driver
Date: Sun, 09 Aug 2026 21:14:21 +0800	[thread overview]
Message-ID: <20260809-k3-display-v2-12-327d7910bf71@gmail.com> (raw)
In-Reply-To: <20260809-k3-display-v2-0-327d7910bf71@gmail.com>

Add the DP/eDP controller that sits downstream of the Saturn DPU. Two
identical instances share one compatible; the eDP-vs-DP role is board
wiring, so it is taken from the devicetree: an eDP panel always sits
under an aux-bus child node, an external DP connector never does.

The link is driven through the generic PHY framework, so the controller
never touches a PLL register. The controller's HPD interrupt is gated by
the DP pixel clock, which can be off exactly when a plug has to be
caught, so the connector is also polled and the interrupt path re-reads
the live level when it does fire.

Signed-off-by: Cody Kang <codykang.hk@gmail.com>

---
v2:
- use devm_reset_control_get_exclusive_deasserted(), dropping the manual
  deassert/assert paths and the reset field (Philipp Zabel)
- hand the pxclk disable to a devm action registered after the PHY
  child is populated (Yao Zi)
- drop the dead NULL-clk guards (Yao Zi)
- drop the error message duplicated by devm_request_threaded_irq()
  (Yao Zi)
- ack the W1C DP_GENERAL_INTERRUPT bits with plain writes (Sashiko)
- return IRQ_HANDLED when the ISR consumed an HPD latch instead of
  IRQ_NONE (Sashiko)
- read EDID blocks through the aux I2C adapter (Sashiko)
- drop DRM_BRIDGE_OP_MODES from the DP branch (Sashiko)
- respond to Rob Herring's suggestion, port@1 required, compute
  possible_crtcs from the input endpoint only
---
 drivers/gpu/drm/spacemit/Kconfig            |   19 +
 drivers/gpu/drm/spacemit/Makefile           |    3 +
 drivers/gpu/drm/spacemit/spacemit_inno_dp.c | 2391 +++++++++++++++++++++++++++
 drivers/gpu/drm/spacemit/spacemit_inno_dp.h |  329 ++++
 4 files changed, 2742 insertions(+)

diff --git a/drivers/gpu/drm/spacemit/Kconfig b/drivers/gpu/drm/spacemit/Kconfig
index 51e38dabfdba5..afd6cdea27fc2 100644
--- a/drivers/gpu/drm/spacemit/Kconfig
+++ b/drivers/gpu/drm/spacemit/Kconfig
@@ -16,3 +16,22 @@ config DRM_SPACEMIT
 	  DisplayPort or embedded DisplayPort controller.
 
 	  If M is selected the module will be called spacemit-drm.
+
+config SPACEMIT_INNO_DP
+	tristate "SpacemiT Innosilicon DP/eDP controller support"
+	depends on DRM_SPACEMIT
+	imply PHY_SPACEMIT_K3_INNO_DP
+	select GENERIC_PHY
+	select DRM_PANEL
+	select DRM_BRIDGE_CONNECTOR
+	select DRM_DISPLAY_HELPER
+	select DRM_DISPLAY_DP_HELPER
+	select DRM_DISPLAY_DP_AUX_BUS
+	help
+	  Choose this option to enable the Innosilicon DisplayPort /
+	  embedded DisplayPort transmitter integrated in SpacemiT SoCs.
+	  The controller sits downstream of the Saturn DPU and drives an
+	  eDP panel (discovered over the DP AUX bus) or an external
+	  DisplayPort connector.
+
+	  If M is selected the module will be called spacemit-inno-dp.
diff --git a/drivers/gpu/drm/spacemit/Makefile b/drivers/gpu/drm/spacemit/Makefile
index b3b148000cf7b..260d316a271b5 100644
--- a/drivers/gpu/drm/spacemit/Makefile
+++ b/drivers/gpu/drm/spacemit/Makefile
@@ -9,4 +9,7 @@ spacemit-drm-y := spacemit_drm.o \
 		  dpu/dpu_saturn_hee.o \
 		  dpu/saturn_fbcmem.o
 
+spacemit-inno-dp-y := spacemit_inno_dp.o
+
 obj-$(CONFIG_DRM_SPACEMIT) += spacemit-drm.o
+obj-$(CONFIG_SPACEMIT_INNO_DP) += spacemit-inno-dp.o
diff --git a/drivers/gpu/drm/spacemit/spacemit_inno_dp.c b/drivers/gpu/drm/spacemit/spacemit_inno_dp.c
new file mode 100644
index 0000000000000..1ba4892607881
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/spacemit_inno_dp.c
@@ -0,0 +1,2391 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ */
+
+#include <linux/of.h>
+#include <linux/bits.h>
+#include <linux/bitfield.h>
+#include <linux/module.h>
+#include <linux/slab.h>
+#include <linux/platform_device.h>
+#include <linux/component.h>
+#include <linux/clk.h>
+#include <linux/cleanup.h>
+#include <linux/math64.h>
+#include <linux/mutex.h>
+#include <linux/reset.h>
+#include <linux/delay.h>
+#include <linux/i2c.h>
+#include <linux/iopoll.h>
+#include <linux/media-bus-format.h>
+#include <linux/of_graph.h>
+#include <linux/of_platform.h>
+#include <linux/phy/phy.h>
+#include <linux/phy/phy-dp.h>
+#include <linux/regmap.h>
+#include <drm/display/drm_dp_aux_bus.h>
+#include <drm/drm_of.h>
+#include <drm/drm_bridge.h>
+#include <drm/drm_bridge_connector.h>
+#include <drm/drm_device.h>
+#include <drm/drm_encoder.h>
+#include <drm/drm_connector.h>
+#include <drm/drm_drv.h>
+#include <drm/drm_edid.h>
+#include <drm/drm_modeset_helper_vtables.h>
+#include <drm/drm_gem_dma_helper.h>
+#include <drm/drm_probe_helper.h>
+#include <drm/drm_atomic.h>
+#include <drm/drm_atomic_state_helper.h>
+#include <drm/display/drm_dp_aux_bus.h>
+#include <drm/display/drm_dp.h>
+#include <drm/display/drm_dp_helper.h>
+#include <drm/drm_print.h>
+
+#include "spacemit_inno_dp.h"
+
+#define SPACEMIT_DP_SWING_MAX  2
+#define SPACEMIT_DP_PREEMP_MAX 2
+#define SPACEMIT_DP_AUX_MAX_RETRIES 3
+
+#define SPACEMIT_DP_DDC_SEGMENT_ADDR 0x30
+#define SPACEMIT_DP_SINK_READY_DELAY_MS 120
+#define SPACEMIT_DP_SINK_READY_RETRIES 3
+
+/*
+ * With DP routed through a Type-C PD controller the HPD pin is virtual and
+ * can re-assert seconds after a transmitter power-cycle. Asserted HPD
+ * returns immediately; the full wait is paid only when no sink shows up.
+ */
+#define SPACEMIT_DP_HPD_POLL_US     1000
+#define SPACEMIT_DP_HPD_DEBOUNCE_US (1000 * USEC_PER_MSEC)
+
+enum spacemit_dp_link_rate {
+	SPACEMIT_DP_LINK_RATE_1_62 = 1620000, /* 1.62 Gbps */
+	SPACEMIT_DP_LINK_RATE_2_70 = 2700000, /* 2.70 Gbps */
+	SPACEMIT_DP_LINK_RATE_5_40 = 5400000, /* 5.40 Gbps */
+	SPACEMIT_DP_LINK_RATE_8_10 = 8100000, /* 8.10 Gbps */
+};
+
+/* The K3 source tops out at HBR2: the PHY's MPLL has no 8.1 Gbps config. */
+#define SPACEMIT_DP_SOURCE_MAX_RATE	SPACEMIT_DP_LINK_RATE_5_40
+
+enum spacemit_dp_lane_count {
+	SPACEMIT_DP_LANE_1 = 1,
+	SPACEMIT_DP_LANE_2 = 2,
+	SPACEMIT_DP_LANE_4 = 4,
+};
+
+enum soc_video_format {
+	SOC_VIDEO_RGB_6BIT = 0,
+	SOC_VIDEO_RGB_8BIT = 1,
+	SOC_VIDEO_RGB_10BIT = 2,
+	SOC_VIDEO_RGB_12BIT = 3,
+	SOC_VIDEO_RGB_16BIT = 4,
+	SOC_VIDEO_YUV444_8BIT = 5,
+	SOC_VIDEO_YUV444_10BIT = 6,
+	SOC_VIDEO_YUV444_12BIT = 7,
+	SOC_VIDEO_YUV444_16BIT = 8,
+	SOC_VIDEO_YUV422_8BIT = 9,
+	SOC_VIDEO_YUV422_10BIT = 10,
+	SOC_VIDEO_YUV422_12BIT = 11,
+	SOC_VIDEO_YUV422_16BIT = 12,
+};
+
+enum spacemit_dp_ref_clk {
+	SPACEMIT_DP_REF_CLK_24M = 24000,
+};
+
+static const struct spacemit_dp_link_config {
+	enum spacemit_dp_link_rate rate;
+	enum spacemit_dp_lane_count lanes;
+} spacemit_dp_link_priority_table[] = {
+	{SPACEMIT_DP_LINK_RATE_1_62, SPACEMIT_DP_LANE_1}, /* 1.62 Gbps */
+	{SPACEMIT_DP_LINK_RATE_2_70, SPACEMIT_DP_LANE_1}, /* 2.70 Gbps */
+	{SPACEMIT_DP_LINK_RATE_1_62, SPACEMIT_DP_LANE_2}, /* 3.24 Gbps */
+
+	{SPACEMIT_DP_LINK_RATE_2_70, SPACEMIT_DP_LANE_2}, /* 5.40 Gbps */
+	{SPACEMIT_DP_LINK_RATE_1_62, SPACEMIT_DP_LANE_4}, /* 6.48 Gbps */
+
+	{SPACEMIT_DP_LINK_RATE_2_70, SPACEMIT_DP_LANE_4}, /* 10.8 Gbps */
+	{SPACEMIT_DP_LINK_RATE_5_40, SPACEMIT_DP_LANE_2}, /* 10.8 Gbps */
+
+	{SPACEMIT_DP_LINK_RATE_5_40, SPACEMIT_DP_LANE_4}, /* 21.6 Gbps */
+};
+
+static const struct soc_format_info {
+	u8 bpp; /* Bits Per Pixel */
+} format_info_table[] = {
+	[SOC_VIDEO_RGB_6BIT]      = { .bpp = 18 },
+	[SOC_VIDEO_RGB_8BIT]      = { .bpp = 24 },
+	[SOC_VIDEO_RGB_10BIT]     = { .bpp = 30 },
+	[SOC_VIDEO_RGB_12BIT]     = { .bpp = 36 },
+	[SOC_VIDEO_RGB_16BIT]     = { .bpp = 48 },
+
+	[SOC_VIDEO_YUV444_8BIT]   = { .bpp = 24 },
+	[SOC_VIDEO_YUV444_10BIT]  = { .bpp = 30 },
+	[SOC_VIDEO_YUV444_12BIT]  = { .bpp = 36 },
+	[SOC_VIDEO_YUV444_16BIT]  = { .bpp = 48 },
+
+	[SOC_VIDEO_YUV422_8BIT]   = { .bpp = 16 },
+	[SOC_VIDEO_YUV422_10BIT]  = { .bpp = 20 },
+	[SOC_VIDEO_YUV422_12BIT]  = { .bpp = 24 },
+	[SOC_VIDEO_YUV422_16BIT]  = { .bpp = 32 },
+};
+
+/**
+ * struct spacemit_dp_dev - Per-instance state for an Innosilicon DP/eDP
+ * controller
+ * @dev: Underlying platform device.
+ * @drm: Parent DRM device, set at component bind.
+ * @encoder: Passive encoder anchoring possible_crtcs; atomic goes through
+ * @bridge.
+ * @bridge: DRM bridge implementing this controller, attached NO_CONNECTOR.
+ * @connector: Connector built on top of @bridge by drm_bridge_connector_init().
+ * @connector_status: Last-known status, driven by the HPD ISR and by polling.
+ * @mode: Committed adjusted_mode, cached under @mode_lock for the async paths.
+ * @regs: Mapped MMIO base of the DP controller register block.
+ * @aux: DPCD AUX channel, registered at bind.
+ * @dpcd: Raw DPCD capability bytes.
+ * @lane_count: Lane count chosen by link training (1, 2 or 4).
+ * @link_rate: Link rate chosen by link training, in kHz.
+ * @link: Sink-advertised link capability snapshot.
+ * @link.revision: DPCD revision byte.
+ * @link.enhanced_framing: Whether the sink advertises enhanced framing.
+ * @link.max_rate: Sink-advertised maximum link rate, kHz.
+ * @link.max_num_lanes: Sink-advertised maximum lane count.
+ * @pxclk: Optional external pixel clock.
+ * @pll_clk: The PHY's pixel PLL; clk_set_rate() programs it.
+ * @phy: DP PHY handle.
+ * @phy_powered: Controller-side mirror of the PHY framework's power refcount.
+ * @next_bridge: Downstream bridge; panel-edp's drm_panel wrapper on eDP.
+ * @edp_mode: True when this instance drives an embedded panel.
+ * @use_ext_pixel_clock: True when the pixel clock is sourced externally.
+ * @pixel_clock: Cached external pixel-clock rate, kHz.
+ * @mode_lock: Serialises mode_set / enable / hotplug against each other.
+ * @modeset_retry_work: Deferred "link is bad" notification, see its handler.
+ * @irq: Controller interrupt line.
+ */
+struct spacemit_dp_dev {
+	struct device *dev;
+
+	struct drm_device *drm;
+	struct drm_encoder encoder;
+	struct drm_bridge bridge;
+	struct drm_connector *connector;
+
+	enum drm_connector_status connector_status;
+	struct drm_display_mode mode;
+
+	struct regmap *regs;
+	struct drm_dp_aux aux;
+
+	u8 dpcd[DP_RECEIVER_CAP_SIZE];
+	int lane_count;
+	u32 link_rate;
+
+	struct {
+		u8 revision;
+		u8 enhanced_framing;
+		u32 max_rate;
+		u32 max_num_lanes;
+	} link;
+
+	struct clk *pxclk;
+	struct clk *pll_clk;
+	struct phy *phy;
+	bool phy_powered;
+	struct drm_bridge *next_bridge;
+
+	bool edp_mode;
+	bool use_ext_pixel_clock;
+	int pixel_clock;
+	struct mutex mode_lock;
+
+	struct work_struct modeset_retry_work;
+
+	int irq;
+};
+
+static int spacemit_dp_get_bpp(struct spacemit_dp_dev *dp, u32 format)
+{
+	if (format >= ARRAY_SIZE(format_info_table)) {
+		dev_warn(dp->dev,
+			"invalid color format index %d, defaulting to RGB888\n",
+			format);
+		return 24;
+	}
+	return format_info_table[format].bpp;
+}
+
+/*
+ * drm_connector_set_link_status_property() takes connection_mutex, which the
+ * commit already holds: calling it from .atomic_enable deadlocks against
+ * itself.
+ */
+static void spacemit_dp_modeset_retry_work(struct work_struct *work)
+{
+	struct spacemit_dp_dev *dp = container_of(work, struct spacemit_dp_dev,
+						  modeset_retry_work);
+	struct drm_connector *connector = dp->connector;
+
+	if (!connector)
+		return;
+
+	mutex_lock(&connector->dev->mode_config.mutex);
+	drm_connector_set_link_status_property(connector,
+					       DRM_MODE_LINK_STATUS_BAD);
+	mutex_unlock(&connector->dev->mode_config.mutex);
+
+	drm_kms_helper_connector_hotplug_event(connector);
+}
+
+/**
+ * struct spacemit_dp_bridge_state - Per-state planned link configuration.
+ * @base: Parent &struct drm_bridge_state.
+ * @link_cfg_idx: Index into spacemit_dp_link_priority_table[] picked by
+ *	->atomic_check(); -1 means ->atomic_enable() walks the table from the start.
+ * @color_format: Output colour format selected by ->atomic_check().
+ */
+struct spacemit_dp_bridge_state {
+	struct drm_bridge_state base;
+	int link_cfg_idx;
+	u32 color_format;
+};
+
+static inline struct spacemit_dp_bridge_state *
+to_spacemit_dp_bridge_state(struct drm_bridge_state *state)
+{
+	return container_of(state, struct spacemit_dp_bridge_state, base);
+}
+
+static const struct regmap_config spacemit_dp_regmap_config = {
+	.name		= "dp",
+	.reg_bits	= 32,
+	.val_bits	= 32,
+	.reg_stride	= 4,
+	.max_register	= 0x3ffc,	/* full 0x4000 reg region from binding */
+};
+
+static u32 spacemit_dp_aux_get_cmd(struct drm_dp_aux_msg *msg)
+{
+	switch (msg->request & ~DP_AUX_I2C_MOT) {
+	case DP_AUX_NATIVE_WRITE:
+	case DP_AUX_I2C_WRITE:
+	case DP_AUX_I2C_WRITE_STATUS_UPDATE:
+		return msg->request;
+	case DP_AUX_NATIVE_READ:
+	case DP_AUX_I2C_READ:
+		return msg->request;
+	default:
+		return 0;
+	}
+
+	return 0;
+}
+
+static void spacemit_dp_aux_hw_reset(struct spacemit_dp_dev *dp)
+{
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_AUX_RESET,
+			  FIELD_PREP(DP_AUX_RESET, 0x1));
+	usleep_range(2000, 2500);
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_AUX_RESET,
+			  FIELD_PREP(DP_AUX_RESET, 0x0));
+	usleep_range(2000, 2500);
+	regmap_write(dp->regs, DP_GENERAL_INTERRUPT,
+		     DP_AUX_REPLY_EVENT_INT_STA);
+}
+
+static bool spacemit_dp_dpcd_caps_valid(const u8 *dpcd)
+{
+	u8 max_bw = dpcd[DP_MAX_LINK_RATE];
+	u8 max_lanes = dpcd[DP_MAX_LANE_COUNT] & DP_MAX_LANE_COUNT_MASK;
+
+	if (!dpcd[DP_DPCD_REV])
+		return false;
+
+	switch (max_bw) {
+	case DP_LINK_BW_1_62:
+	case DP_LINK_BW_2_7:
+	case DP_LINK_BW_5_4:
+	case DP_LINK_BW_8_1:
+		break;
+	default:
+		return false;
+	}
+
+	return max_lanes == 1 || max_lanes == 2 || max_lanes == 4;
+}
+
+static ssize_t spacemit_dp_aux_transfer(struct drm_dp_aux *aux,
+					struct drm_dp_aux_msg *msg)
+{
+	int ret, i, retries = 0;
+	unsigned long timeout;
+
+	struct spacemit_dp_dev *dp = container_of(aux, struct spacemit_dp_dev,
+						  aux);
+	u32 cmd, len, val, status;
+	u32 data[4] = {0};
+	u8 *buf = msg->buffer;
+	bool is_read = (msg->request & DP_AUX_I2C_READ) ||
+		((msg->request & DP_AUX_NATIVE_READ) == DP_AUX_NATIVE_READ);
+
+	if (msg->size > 16)
+		return -EINVAL;
+
+	cmd = spacemit_dp_aux_get_cmd(msg);
+
+retry_eio:
+	if (!is_read) {
+		/* Pack bytes into 32-bit words (Little Endian packing) */
+		for (i = 0; i < msg->size; i++)
+			data[i / 4] |= buf[i] << ((i % 4) * 8);
+
+		/* Write data to registers: DATA1(LSB)..DATA4(MSB) */
+		regmap_write_bits(dp->regs, DP_AUX_DATA1_REG, DP_AUX_DATA1,
+				  FIELD_PREP(DP_AUX_DATA1, data[0]));
+		regmap_write_bits(dp->regs, DP_AUX_DATA2_REG, DP_AUX_DATA2,
+				  FIELD_PREP(DP_AUX_DATA2, data[1]));
+		regmap_write_bits(dp->regs, DP_AUX_DATA3_REG, DP_AUX_DATA3,
+				  FIELD_PREP(DP_AUX_DATA3, data[2]));
+		regmap_write_bits(dp->regs, DP_AUX_DATA4_REG, DP_AUX_DATA4,
+				  FIELD_PREP(DP_AUX_DATA4, data[3]));
+	}
+
+	/* HW expects Length - 1 */
+	len = msg->size > 0 ? msg->size - 1 : 0;
+
+	regmap_write_bits(dp->regs, DP_AUX_CMD_REG, DP_AUX_LENGTH,
+			  FIELD_PREP(DP_AUX_LENGTH, len));
+	regmap_write_bits(dp->regs, DP_AUX_CMD_REG, DP_AUX_ADDR,
+			  FIELD_PREP(DP_AUX_ADDR, msg->address));
+	regmap_write_bits(dp->regs, DP_AUX_CMD_REG, DP_AUX_CMD_TYPE,
+			  FIELD_PREP(DP_AUX_CMD_TYPE, cmd));
+
+	regmap_write_bits(dp->regs, DP_AUX_START_REG, DP_AUX_START,
+			  FIELD_PREP(DP_AUX_START, 0));
+	regmap_write_bits(dp->regs, DP_AUX_START_REG, DP_AUX_START,
+			  FIELD_PREP(DP_AUX_START, 1));
+
+	timeout = jiffies + msecs_to_jiffies(200);
+	ret = -ETIMEDOUT;
+
+	while (1) {
+		regmap_read(dp->regs, DP_GENERAL_INTERRUPT, &val);
+		val = FIELD_GET(DP_AUX_REPLY_EVENT_INT_STA, val);
+		if (val) {
+			ret = 0;
+			break;
+		}
+		if (time_after(jiffies, timeout))
+			break;
+		usleep_range(100, 110);
+	}
+
+	if (ret) {
+		if (retries < SPACEMIT_DP_AUX_MAX_RETRIES) {
+			retries++;
+			spacemit_dp_aux_hw_reset(dp);
+			dev_dbg(dp->dev,
+				"AUX timeout, cmd=0x%x addr=0x%x size=%zu retry %d/%d\n",
+				cmd, msg->address, msg->size, retries, SPACEMIT_DP_AUX_MAX_RETRIES);
+			goto retry_eio;
+		}
+
+		dev_err(dp->dev,
+			"AUX transfer timeout, req: 0x%x, cmd: 0x%x, addr: 0x%x, size %zu\n",
+			msg->request, cmd, msg->address, msg->size);
+		return ret;
+	}
+
+	regmap_read(dp->regs, DP_AUX_STS_REG,
+		    &status); status = FIELD_GET(DP_AUX_STATUS, status);
+
+	/* Write 1 to clear. */
+	regmap_write(dp->regs, DP_GENERAL_INTERRUPT,
+		     DP_AUX_REPLY_EVENT_INT_STA);
+
+	/* Map HW status (the AUX reply command byte) to DRM reply codes */
+	switch (status) {
+	case 0: /* ACK */
+		msg->reply = DP_AUX_NATIVE_REPLY_ACK;
+		break;
+	case 1: /* NACK */
+		msg->reply = DP_AUX_NATIVE_REPLY_NACK;
+		return 0; /* Standard says return 0 on NACK for upper layer retry */
+	case 2: /* DEFER */
+		msg->reply = DP_AUX_NATIVE_REPLY_DEFER;
+		return 0;
+	case DP_AUX_I2C_REPLY_NACK: /* native ACK, I2C NACK */
+		msg->reply = DP_AUX_I2C_REPLY_NACK;
+		return 0;
+	case DP_AUX_I2C_REPLY_DEFER: /* native ACK, I2C DEFER */
+		msg->reply = DP_AUX_I2C_REPLY_DEFER;
+		return 0;
+	default:
+		regmap_read(dp->regs, DP_AUX_STS_REG, &val);
+		val = FIELD_GET(DP_AUX_REPLY_ERR_CODE, val);
+		if (retries < SPACEMIT_DP_AUX_MAX_RETRIES) {
+			retries++;
+			dev_dbg(dp->dev,
+				"AUX status, cmd=0x%x addr=0x%x size=%zu retry %d/%d\n",
+				cmd, msg->address, msg->size, retries, SPACEMIT_DP_AUX_MAX_RETRIES);
+			usleep_range(1000, 1100);
+			goto retry_eio;
+		}
+		dev_err(dp->dev,
+			"AUX error: cmd=0x%x addr=0x%x size=%zu status=0x%x code=0x%x\n",
+			cmd, msg->address, msg->size, status, val);
+		return -EIO;
+	}
+
+	if (is_read && msg->size > 0) {
+		regmap_read(dp->regs, DP_AUX_DATA1_REG, &data[0]);
+		data[0] = FIELD_GET(DP_AUX_DATA1, data[0]);
+		regmap_read(dp->regs, DP_AUX_DATA2_REG, &data[1]);
+		data[1] = FIELD_GET(DP_AUX_DATA2, data[1]);
+		regmap_read(dp->regs, DP_AUX_DATA3_REG, &data[2]);
+		data[2] = FIELD_GET(DP_AUX_DATA3, data[2]);
+		regmap_read(dp->regs, DP_AUX_DATA4_REG, &data[3]);
+		data[3] = FIELD_GET(DP_AUX_DATA4, data[3]);
+
+		/* Unpack 32-bit words back to bytes */
+		for (i = 0; i < msg->size; i++)
+			buf[i] = (data[i / 4] >> ((i % 4) * 8)) & 0xFF;
+	}
+
+	return msg->size;
+}
+
+static int update_edp_config(struct spacemit_dp_dev *dp, bool enable)
+{
+	u8 value;
+	int ret;
+
+	ret = drm_dp_dpcd_read_byte(&dp->aux, DP_EDP_CONFIGURATION_SET, &value);
+	if (ret < 0) {
+		dev_err(dp->dev,
+			"failed to read DP_EDP_CONFIGURATION_SET: %d\n", ret);
+		return ret;
+	}
+
+	if (enable)
+		value |= 0x01;
+	else
+		value &= ~0x01;
+
+	ret = drm_dp_dpcd_write_byte(&dp->aux, DP_EDP_CONFIGURATION_SET, value);
+	if (ret < 0) {
+		dev_err(dp->dev,
+			"failed to write DP_EDP_CONFIGURATION_SET: %d\n", ret);
+		return ret;
+	}
+
+	return 0;
+}
+
+/* Read the sink's DPCD link-layer capabilities. EDID is handled separately. */
+static int spacemit_dp_hw_read_sink_caps(struct spacemit_dp_dev *dp)
+{
+	ssize_t ret;
+	u8 max_bw;
+	int retry;
+
+	for (retry = 0; retry < SPACEMIT_DP_SINK_READY_RETRIES; retry++) {
+		if (retry) {
+			spacemit_dp_aux_hw_reset(dp);
+			msleep(SPACEMIT_DP_SINK_READY_DELAY_MS);
+		}
+
+		ret = drm_dp_read_dpcd_caps(&dp->aux, dp->dpcd);
+		if (ret < 0)
+			continue;
+
+		if (!spacemit_dp_dpcd_caps_valid(dp->dpcd)) {
+			dev_dbg(dp->dev,
+				"DPCD caps not ready: rev=0x%02x bw=0x%02x lanes=0x%02x\n",
+				dp->dpcd[DP_DPCD_REV], dp->dpcd[DP_MAX_LINK_RATE],
+				dp->dpcd[DP_MAX_LANE_COUNT]);
+			ret = -EAGAIN;
+			continue;
+		}
+
+		break;
+	}
+
+	if (ret < 0) {
+		dev_err(dp->dev, "failed to read DPCD after %d retries: %zd\n",
+			SPACEMIT_DP_SINK_READY_RETRIES, ret);
+		dp->link.revision = 0x14;
+		dp->link.max_rate = SPACEMIT_DP_LINK_RATE_5_40;
+		dp->link.max_num_lanes = SPACEMIT_DP_LANE_2;
+		dp->link.enhanced_framing = 1;
+		return ret;
+	}
+
+	dp->link.revision = dp->dpcd[DP_DPCD_REV];
+
+	max_bw = dp->dpcd[DP_MAX_LINK_RATE];
+	switch (max_bw) {
+	case DP_LINK_BW_1_62:
+		dp->link.max_rate = SPACEMIT_DP_LINK_RATE_1_62;
+		break;
+	case DP_LINK_BW_2_7:
+		dp->link.max_rate = SPACEMIT_DP_LINK_RATE_2_70;
+		break;
+	case DP_LINK_BW_5_4:
+		dp->link.max_rate = SPACEMIT_DP_LINK_RATE_5_40;
+		break;
+	case DP_LINK_BW_8_1:
+		dp->link.max_rate = SPACEMIT_DP_LINK_RATE_8_10;
+		break;
+	default:
+		dev_warn(dp->dev, "unknown DPCD max link rate 0x%x, defaulting to 5.40 Gbps\n",
+			 max_bw);
+		dp->link.revision = 0x14;
+		dp->link.max_rate = SPACEMIT_DP_LINK_RATE_5_40;
+		dp->link.max_num_lanes = SPACEMIT_DP_LANE_2;
+		dp->link.enhanced_framing = 1;
+		return -EINVAL;
+	}
+
+	/* Clamp to the source's actual ceiling (HBR2). */
+	dp->link.max_rate = min(dp->link.max_rate,
+				(u32)SPACEMIT_DP_SOURCE_MAX_RATE);
+
+	dp->link.max_num_lanes =
+			dp->dpcd[DP_MAX_LANE_COUNT] & DP_MAX_LANE_COUNT_MASK;
+
+	dp->link.enhanced_framing =
+		(dp->dpcd[DP_MAX_LANE_COUNT] & DP_ENHANCED_FRAME_CAP);
+
+	dev_dbg(dp->dev,
+		"DPCD: Rev %x.%x, MaxRate %d kHz, MaxLanes %d, EnhFrame %d\n",
+		dp->link.revision >> 4, dp->link.revision & 0xF,
+		dp->link.max_rate, dp->link.max_num_lanes,
+		dp->link.enhanced_framing);
+
+	return 0;
+}
+
+static int spacemit_dp_phy_set_lanes(struct spacemit_dp_dev *dp,
+				     enum spacemit_dp_lane_count lanes)
+{
+	union phy_configure_opts opts = {
+		.dp = {
+			.lanes = lanes,
+			.set_lanes = true,
+		},
+	};
+	int ret;
+
+	ret = phy_configure(dp->phy, &opts);
+	if (ret)
+		return ret;
+
+	dp->lane_count = lanes;
+	return 0;
+}
+
+static int spacemit_dp_phy_set_rate(struct spacemit_dp_dev *dp,
+				    enum spacemit_dp_link_rate rate)
+{
+	union phy_configure_opts opts = {
+		.dp = {
+			/*
+			 * enum value is in kbit/s; PHY framework wants Mbit/s.
+			 */
+			.link_rate = rate / 1000,
+			.set_rate = true,
+		},
+	};
+	int ret;
+
+	ret = phy_configure(dp->phy, &opts);
+	if (ret)
+		return ret;
+
+	dp->link_rate = rate;
+	return 0;
+}
+
+/*
+ * The link PLL rides phy_configure(); the pixel PLL is a clock the PHY
+ * registered. Link first: that is the order the analog blocks come up in.
+ */
+static int spacemit_dp_set_plls(struct spacemit_dp_dev *dp,
+				enum spacemit_dp_link_rate rate, u32 pclk_khz)
+{
+	int ret;
+
+	ret = spacemit_dp_phy_set_rate(dp, rate);
+	if (ret)
+		return ret;
+
+	return clk_set_rate(dp->pll_clk, (unsigned long)pclk_khz * 1000);
+}
+
+/*
+ * phy_power_on/off() are refcounted, but this driver's bring-up is idempotent
+ * rather than pair-balanced. Mirror the state so the refcount cannot go
+ * negative.
+ */
+static int spacemit_dp_phy_on(struct spacemit_dp_dev *dp)
+{
+	int ret;
+
+	if (dp->phy_powered)
+		return 0;
+
+	ret = phy_power_on(dp->phy);
+	if (ret)
+		return ret;
+
+	dp->phy_powered = true;
+	return 0;
+}
+
+static void spacemit_dp_phy_off(struct spacemit_dp_dev *dp)
+{
+	if (!dp->phy_powered)
+		return;
+
+	phy_power_off(dp->phy);
+	dp->phy_powered = false;
+}
+
+/*
+ * The AUX reset pulse is what leaves the next bring-up a clean state machine.
+ */
+static void spacemit_dp_link_disable(struct spacemit_dp_dev *dp)
+{
+	regmap_write_bits(dp->regs, DP_VIDEO_VSAMPLE_REG,
+			  DP_VIDEO_STREAM_ENABLE,
+			  FIELD_PREP(DP_VIDEO_STREAM_ENABLE, 0));
+	spacemit_dp_phy_off(dp);
+
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_AUX_RESET,
+			  FIELD_PREP(DP_AUX_RESET, 0x1));
+	usleep_range(5000, 8000);
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_AUX_RESET,
+			  FIELD_PREP(DP_AUX_RESET, 0x0));
+	usleep_range(2000, 4000);
+}
+
+static enum drm_connector_status
+spacemit_dp_hw_detect_hpd(struct spacemit_dp_dev *dp)
+{
+	u32 hpd_status;
+	enum drm_connector_status connector_status =
+			connector_status_disconnected;
+
+	regmap_read(dp->regs, DP_HPD_INTERRUPT_STATUS, &hpd_status);
+	hpd_status = FIELD_GET(DP_HPD_IN_STATUS, hpd_status);
+	if (hpd_status)
+		connector_status = connector_status_connected;
+	else
+		connector_status = connector_status_disconnected;
+
+	return connector_status;
+}
+
+/* Returns true when a latched HPD event was pending and got acked. */
+static bool spacemit_dp_hw_clean_hpd(struct spacemit_dp_dev *dp)
+{
+	u32 plug_event, unplug_event;
+
+	regmap_read(dp->regs, DP_HPD_INTERRUPT_STATUS, &plug_event);
+	plug_event = FIELD_GET(DP_HOT_PLUG_EVENT, plug_event);
+	regmap_read(dp->regs, DP_HPD_INTERRUPT_STATUS, &unplug_event);
+	unplug_event = FIELD_GET(DP_HOT_UNPLUG_EVENT, unplug_event);
+
+	if (plug_event)
+		regmap_write(dp->regs, DP_HPD_INTERRUPT_STATUS,
+			     FIELD_PREP(DP_HOT_PLUG_EVENT, 0x1));
+
+	if (unplug_event)
+		regmap_write(dp->regs, DP_HPD_INTERRUPT_STATUS,
+			     FIELD_PREP(DP_HOT_UNPLUG_EVENT, 0x1));
+
+	return plug_event || unplug_event;
+}
+
+static int spacemit_dp_set_training_pattern(struct spacemit_dp_dev *dp,
+					    u8 pattern)
+{
+	u32 tps_sel = 0;
+	u8 dpcd_pattern = pattern;
+	int ret;
+
+	if (pattern != DP_TRAINING_PATTERN_DISABLE)
+		dpcd_pattern |= DP_LINK_SCRAMBLING_DISABLE;
+
+	switch (pattern) {
+	case DP_TRAINING_PATTERN_DISABLE:
+		tps_sel = 0;
+		regmap_write_bits(dp->regs, DP_CORE_CONTROL,
+				  DP_SCRAMBLER_DISABLE,
+				  FIELD_PREP(DP_SCRAMBLER_DISABLE, 0));
+		break;
+	case DP_TRAINING_PATTERN_1:
+		tps_sel = 1;
+		regmap_write_bits(dp->regs, DP_CORE_CONTROL,
+				  DP_SCRAMBLER_DISABLE,
+				  FIELD_PREP(DP_SCRAMBLER_DISABLE, 1));
+		break;
+	case DP_TRAINING_PATTERN_2:
+		tps_sel = 2;
+		regmap_write_bits(dp->regs, DP_CORE_CONTROL,
+				  DP_SCRAMBLER_DISABLE,
+				  FIELD_PREP(DP_SCRAMBLER_DISABLE, 1));
+		break;
+	case DP_TRAINING_PATTERN_3:
+		tps_sel = 3;
+		regmap_write_bits(dp->regs, DP_CORE_CONTROL,
+				  DP_SCRAMBLER_DISABLE,
+				  FIELD_PREP(DP_SCRAMBLER_DISABLE, 1));
+		break;
+	default:
+		dev_err(dp->dev, "Unsupported training pattern: 0x%x\n",
+			pattern);
+		return -EINVAL;
+	}
+
+	regmap_write_bits(dp->regs, DP_PHYIF_CTRL_ADDR, DP_TPS_SEL,
+			  FIELD_PREP(DP_TPS_SEL, tps_sel));
+
+	ret = drm_dp_dpcd_write_byte(&dp->aux, DP_TRAINING_PATTERN_SET,
+				     dpcd_pattern);
+	if (ret < 0) {
+		dev_err(dp->dev, "failed to set DPCD training pattern: %d\n",
+			ret);
+		return ret;
+	}
+
+	return 0;
+}
+
+/*
+ * The adjust request has to reach the source PHY, not just the sink's DPCD:
+ * a transmitter left at its reset default trains any level > 0 with a bad eye.
+ */
+static int spacemit_dp_link_apply_adjust(struct spacemit_dp_dev *dp,
+					 const u8 *link_status,
+				    enum spacemit_dp_lane_count lanes,
+				    u8 *training_set)
+{
+	union phy_configure_opts opts = {
+		.dp = {
+			.lanes = lanes,
+			.set_voltages = true,
+		},
+	};
+	int i, ret;
+
+	for (i = 0; i < lanes; i++) {
+		u8 v = drm_dp_get_adjust_request_voltage(link_status, i);
+		u8 p = drm_dp_get_adjust_request_pre_emphasis(link_status, i);
+		u8 v_bits;
+
+		if (v >= SPACEMIT_DP_SWING_MAX)
+			v = SPACEMIT_DP_SWING_MAX;
+		if (p >= SPACEMIT_DP_PREEMP_MAX)
+			p = SPACEMIT_DP_PREEMP_MAX;
+
+		opts.dp.voltage[i] = v;
+		opts.dp.pre[i] = p;
+
+		v_bits = v;
+		if (v == SPACEMIT_DP_SWING_MAX)
+			v_bits |= DP_TRAIN_MAX_SWING_REACHED;
+		if (p == SPACEMIT_DP_PREEMP_MAX)
+			v_bits |= DP_TRAIN_MAX_PRE_EMPHASIS_REACHED;
+
+		training_set[i] = v_bits | (p << DP_TRAIN_PRE_EMPHASIS_SHIFT);
+	}
+
+	ret = phy_configure(dp->phy, &opts);
+	if (ret)
+		return ret;
+
+	return drm_dp_dpcd_write_data(&dp->aux, DP_TRAINING_LANE0_SET,
+				      training_set, lanes);
+}
+
+static int spacemit_dp_link_train_clock_recovery(struct spacemit_dp_dev *dp,
+						 enum spacemit_dp_link_rate rate,
+					    enum spacemit_dp_lane_count lanes)
+{
+	u8 link_status[DP_LINK_STATUS_SIZE];
+	u8 training_set[4] = {0};
+	int retries = 0;
+	int ret;
+
+	ret = drm_dp_dpcd_write_data(&dp->aux, DP_TRAINING_LANE0_SET,
+				     training_set, lanes);
+	if (ret < 0)
+		return ret;
+
+	ret = spacemit_dp_set_training_pattern(dp, DP_TRAINING_PATTERN_1);
+	if (ret < 0) {
+		spacemit_dp_set_training_pattern(dp,
+						 DP_TRAINING_PATTERN_DISABLE);
+		return ret;
+	}
+
+	while (retries < 8) {
+		drm_dp_link_train_clock_recovery_delay(&dp->aux, dp->dpcd);
+
+		ret = drm_dp_dpcd_read_link_status(&dp->aux, link_status);
+		if (ret < 0) {
+			spacemit_dp_set_training_pattern(dp,
+				DP_TRAINING_PATTERN_DISABLE);
+			return ret;
+		}
+
+		if (drm_dp_clock_recovery_ok(link_status, lanes))
+			return 0;
+
+		ret = spacemit_dp_link_apply_adjust(dp, link_status, lanes,
+						    training_set);
+		if (ret) {
+			spacemit_dp_set_training_pattern(dp,
+				DP_TRAINING_PATTERN_DISABLE);
+			return ret;
+		}
+
+		retries++;
+	}
+
+	dev_err(dp->dev, "link training: clock recovery failed\n");
+	spacemit_dp_set_training_pattern(dp, DP_TRAINING_PATTERN_DISABLE);
+	return -ETIMEDOUT;
+}
+
+static int spacemit_dp_link_train_channel_eq(struct spacemit_dp_dev *dp,
+					     enum spacemit_dp_link_rate rate,
+					enum spacemit_dp_lane_count lanes)
+{
+	u8 link_status[DP_LINK_STATUS_SIZE];
+	u8 training_set[4] = {0};
+	u8 training_pattern = DP_TRAINING_PATTERN_2;
+	int retries = 0;
+	int ret;
+
+	if (dp->dpcd[DP_MAX_LANE_COUNT] & DP_TPS3_SUPPORTED) {
+		training_pattern = DP_TRAINING_PATTERN_3;
+		dev_dbg(dp->dev, "link training: using TPS3\n");
+	} else {
+		dev_dbg(dp->dev, "link training: using TPS2\n");
+	}
+
+	ret = spacemit_dp_set_training_pattern(dp, training_pattern);
+	if (ret < 0) {
+		spacemit_dp_set_training_pattern(dp,
+						 DP_TRAINING_PATTERN_DISABLE);
+		return ret;
+	}
+
+	while (retries < 8) {
+		drm_dp_link_train_channel_eq_delay(&dp->aux, dp->dpcd);
+
+		ret = drm_dp_dpcd_read_link_status(&dp->aux, link_status);
+		if (ret < 0) {
+			spacemit_dp_set_training_pattern(dp,
+				DP_TRAINING_PATTERN_DISABLE);
+			return ret;
+		}
+
+		if (drm_dp_channel_eq_ok(link_status, lanes)) {
+			spacemit_dp_set_training_pattern(dp,
+				DP_TRAINING_PATTERN_DISABLE);
+			return 0;
+		}
+
+		ret = spacemit_dp_link_apply_adjust(dp, link_status, lanes,
+						    training_set);
+		if (ret) {
+			spacemit_dp_set_training_pattern(dp,
+				DP_TRAINING_PATTERN_DISABLE);
+			return ret;
+		}
+
+		retries++;
+	}
+
+	dev_err(dp->dev, "link training: channel EQ failed\n");
+	spacemit_dp_set_training_pattern(dp, DP_TRAINING_PATTERN_DISABLE);
+	return -ETIMEDOUT;
+}
+
+static int spacemit_dp_link_train(struct spacemit_dp_dev *dp, enum spacemit_dp_link_rate rate,
+				  enum spacemit_dp_lane_count lanes)
+{
+	int ret;
+	u8 link_config[1];
+	u8 bw_code;
+
+	/* Map Link Rate Enum to DPCD Bandwidth Code */
+	switch (rate) {
+	case SPACEMIT_DP_LINK_RATE_1_62:
+		bw_code = DP_LINK_BW_1_62;
+		break;
+	case SPACEMIT_DP_LINK_RATE_2_70:
+		bw_code = DP_LINK_BW_2_7;
+		break;
+	case SPACEMIT_DP_LINK_RATE_5_40:
+		bw_code = DP_LINK_BW_5_4;
+		break;
+	case SPACEMIT_DP_LINK_RATE_8_10:
+		bw_code = DP_LINK_BW_8_1;
+		break;
+	default:
+		bw_code = DP_LINK_BW_1_62;
+		break;
+	}
+
+	/*
+	 * A sink may act on the DP_LINK_BW_SET write, so set the lane count
+	 * first.
+	 */
+	link_config[0] = lanes;
+	if (dp->link.enhanced_framing)
+		link_config[0] |= DP_LANE_COUNT_ENHANCED_FRAME_EN;
+
+	ret = drm_dp_dpcd_write_byte(&dp->aux, DP_LANE_COUNT_SET,
+				     link_config[0]);
+	if (ret < 0) {
+		dev_err(dp->dev, "failed to set DPCD lane count\n");
+		return ret;
+	}
+
+	ret = drm_dp_dpcd_write_byte(&dp->aux, DP_LINK_BW_SET, bw_code);
+	if (ret < 0) {
+		dev_err(dp->dev, "failed to set DPCD link rate\n");
+		return ret;
+	}
+
+	ret = spacemit_dp_link_train_clock_recovery(dp, rate, lanes);
+	if (ret)
+		return ret;
+
+	ret = spacemit_dp_link_train_channel_eq(dp, rate, lanes);
+	if (ret)
+		return ret;
+
+	return 0;
+}
+
+static void spacemit_dp_hw_set_msa_and_enable_video(struct spacemit_dp_dev *dp,
+						    const struct drm_display_mode *mode,
+					       enum spacemit_dp_link_rate rate,
+					       enum spacemit_dp_lane_count lanes,
+		u32 color_format)
+{
+	u64 hb_num;
+	u32 link_rate;
+	u32 fp; /* Pixel clock in MHz */
+	u32 bpp, misc0;
+	u32 tu, tu_frac, tu_int, rd_thres;
+	u32 hsync_len;
+
+	/* mode->clock unit is kHz, fp unit is MHz */
+	if (dp->use_ext_pixel_clock)
+		fp = dp->pixel_clock / 1000;
+	else
+		fp = mode->clock / 1000;
+
+	if (fp == 0)
+		fp = 1; /* Prevent division by zero */
+
+	bpp = spacemit_dp_get_bpp(dp, color_format);
+
+	/* Calculate MISC0 */
+	/* bit0: 0 (Sync Clock) */
+	/* bits1-7: Color Format (000=RGB, 001=YCbCr422, 010=YCbCr444) */
+	/* bits5-7: BPC (001=8bpc, 010=10bpc, etc) */
+	switch (color_format) {
+	case SOC_VIDEO_RGB_6BIT:
+		misc0 = 0x00;
+		break;
+	case SOC_VIDEO_RGB_8BIT:
+		misc0 = 0x20;
+		break;
+	case SOC_VIDEO_RGB_10BIT:
+		misc0 = 0x40;
+		break;
+	case SOC_VIDEO_RGB_12BIT:
+		misc0 = 0x60;
+		break;
+	case SOC_VIDEO_RGB_16BIT:
+		misc0 = 0x80;
+		break;
+	case SOC_VIDEO_YUV422_8BIT:
+		misc0 = 0x22;
+		break;
+	case SOC_VIDEO_YUV422_10BIT:
+		misc0 = 0x42;
+		break;
+	case SOC_VIDEO_YUV422_12BIT:
+		misc0 = 0x62;
+		break;
+	case SOC_VIDEO_YUV422_16BIT:
+		misc0 = 0x82;
+		break;
+	case SOC_VIDEO_YUV444_8BIT:
+		misc0 = 0x24;
+		break;
+	case SOC_VIDEO_YUV444_10BIT:
+		misc0 = 0x44;
+		break;
+	case SOC_VIDEO_YUV444_12BIT:
+		misc0 = 0x64;
+		break;
+	case SOC_VIDEO_YUV444_16BIT:
+		misc0 = 0x84;
+		break;
+	default:
+		misc0 = 0x20;
+		break;
+	}
+
+	/*
+	 * HBlank interval = (htotal - hactive) * (LinkSymbolClock / 4) /
+	 * PixelClock, where LinkSymbolClock = LinkRate * 100 (1.62G -> 162MHz).
+	 * rate is in kHz, so link_rate = rate / 10000 gives the 162, 270, 540
+	 * form used below.
+	 */
+	link_rate = rate / 10000;
+
+	/* Multiply before dividing: hb_num = hblank * (link_rate / 4) / fp */
+	hb_num = (u64)(mode->htotal - mode->hdisplay) * link_rate;
+	do_div(hb_num, 4 * fp);
+
+	/* Transfer unit: tu = fp * bpp * 640 / (8 * lanes * link_rate) */
+	{
+		u64 temp_tu = (u64)fp * bpp * 640;
+		u32 den = 8 * lanes * link_rate;
+
+		do_div(temp_tu, den);
+		tu = temp_tu;
+	}
+	tu_frac = tu % 10;
+	tu_int  = tu / 10;
+
+	/* FIFO read threshold. */
+	if (tu_int < 6)
+		rd_thres = 32;
+	else if ((mode->htotal - mode->hdisplay) < 80)
+		rd_thres = 12;
+	else
+		rd_thres = 16;
+
+	dev_dbg(dp->dev,
+		"MSA: %dx%d, Rate:%d kHz, Lanes:%d, BPP:%d, TU:%d.%d\n",
+		mode->hdisplay, mode->vdisplay, rate, lanes, bpp, tu_int,
+		tu_frac);
+
+	regmap_write_bits(dp->regs, DP_VIDEO_VSAMPLE_REG, DP_VIDEO_MAPPING,
+			  FIELD_PREP(DP_VIDEO_MAPPING, color_format));
+
+	/*
+	 * The Saturn TMG raster is always positive-sync (hsp/vsp fixed to 1).
+	 */
+	regmap_write_bits(dp->regs, DP_VINPUT_POLARITY, DP_HSYNC_IN_POLARITY,
+			  FIELD_PREP(DP_HSYNC_IN_POLARITY, 1));
+	regmap_write_bits(dp->regs, DP_VINPUT_POLARITY, DP_VSYNC_IN_POLARITY,
+			  FIELD_PREP(DP_VSYNC_IN_POLARITY, 1));
+
+	/* Basic timing */
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG1, DP_HACTIVE,
+			  FIELD_PREP(DP_HACTIVE, mode->hdisplay));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG2, DP_VACTIVE,
+			  FIELD_PREP(DP_VACTIVE, mode->vdisplay));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG1, DP_HBLANK,
+			  FIELD_PREP(DP_HBLANK, mode->htotal - mode->hdisplay));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG2, DP_VBLANK,
+			  FIELD_PREP(DP_VBLANK, mode->vtotal - mode->vdisplay));
+
+	u32 hstart = mode->htotal - mode->hsync_end +
+			(mode->hsync_end - mode->hsync_start);
+	u32 vstart = mode->vtotal - mode->vsync_end +
+			(mode->vsync_end - mode->vsync_start);
+
+	regmap_write_bits(dp->regs, DP_VIDEO_MSA1, DP_HSTART,
+			  FIELD_PREP(DP_HSTART, hstart));
+	regmap_write_bits(dp->regs, DP_VIDEO_MSA1, DP_VSTART,
+			  FIELD_PREP(DP_VSTART, vstart));
+
+	hsync_len = mode->hsync_end - mode->hsync_start;
+
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG3, DP_H_SYNC_WIDTH,
+			  FIELD_PREP(DP_H_SYNC_WIDTH, hsync_len));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG4, DP_V_SYNC_WIDTH,
+			  FIELD_PREP(DP_V_SYNC_WIDTH, mode->vsync_end - mode->vsync_start));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG3, DP_H_FRONT_PORCH,
+			  FIELD_PREP(DP_H_FRONT_PORCH, mode->hsync_start - mode->hdisplay));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG4, DP_V_FRONT_PORCH,
+			  FIELD_PREP(DP_V_FRONT_PORCH, mode->vsync_start - mode->vdisplay));
+
+	/* MSA and MISC */
+	regmap_write_bits(dp->regs, DP_VIDEO_MSA2, DP_MISC0,
+			  FIELD_PREP(DP_MISC0, misc0));
+	regmap_write_bits(dp->regs, DP_VIDEO_MSA3, DP_MISC1,
+			  FIELD_PREP(DP_MISC1, 0));
+	regmap_write_bits(dp->regs, DP_VIDEO_MSA3, DP_NVID,
+			  FIELD_PREP(DP_NVID, 0));
+
+	/* Link layer parameters */
+	regmap_write_bits(dp->regs, DP_VIDEO_HBLANK_INTERVAL,
+			  DP_HBLANK_INTERVAL,
+			  FIELD_PREP(DP_HBLANK_INTERVAL, (u32)hb_num));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG5, DP_AVERAGE_BYTES_PER_TU,
+			  FIELD_PREP(DP_AVERAGE_BYTES_PER_TU, tu_int));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG5,
+			  DP_AVERAGE_BYTES_PER_TU_FRAC,
+			  FIELD_PREP(DP_AVERAGE_BYTES_PER_TU_FRAC, tu_frac));
+	regmap_write_bits(dp->regs, DP_VIDEO_CONFIG5, DP_INIT_THRESHOLD,
+			  FIELD_PREP(DP_INIT_THRESHOLD, rd_thres));
+
+	regmap_write_bits(dp->regs, DP_PHYIF_CTRL_ADDR, DP_PHY_SSC_DIS,
+			  FIELD_PREP(DP_PHY_SSC_DIS, 1));
+	regmap_write_bits(dp->regs, DP_CLK_INV_MUX, DP_REG_VID_CLK_SEL,
+			  FIELD_PREP(DP_REG_VID_CLK_SEL, 0));
+	regmap_write_bits(dp->regs, DP_VIDEO_COLORBAR_CFG, DP_VID_BIST_EN,
+			  FIELD_PREP(DP_VID_BIST_EN, 0));
+
+	dev_dbg(dp->dev, "enabling video stream\n");
+	regmap_write_bits(dp->regs, DP_VIDEO_VSAMPLE_REG,
+			  DP_VIDEO_STREAM_ENABLE,
+			  FIELD_PREP(DP_VIDEO_STREAM_ENABLE, 1));
+}
+
+static void spacemit_dp_hw_disable(struct spacemit_dp_dev *dp)
+{
+	dev_dbg(dp->dev, "disabling video and PHY\n");
+
+	regmap_write_bits(dp->regs, DP_VIDEO_VSAMPLE_REG,
+			  DP_VIDEO_STREAM_ENABLE,
+			  FIELD_PREP(DP_VIDEO_STREAM_ENABLE, 0));
+
+	regmap_write_bits(dp->regs, DP_PHYIF_CTRL_ADDR, DP_XMIT_ENABLE,
+			  FIELD_PREP(DP_XMIT_ENABLE, 0));
+
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_LDO_D0,
+			  FIELD_PREP(DP_ANA_TX_EN_LDO_D0, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_LDO_D1,
+			  FIELD_PREP(DP_ANA_TX_EN_LDO_D1, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_LDO_D2,
+			  FIELD_PREP(DP_ANA_TX_EN_LDO_D2, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_LDO_D3,
+			  FIELD_PREP(DP_ANA_TX_EN_LDO_D3, 0));
+
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_DRV_D0,
+			  FIELD_PREP(DP_ANA_TX_EN_DRV_D0, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_DRV_D1,
+			  FIELD_PREP(DP_ANA_TX_EN_DRV_D1, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_DRV_D2,
+			  FIELD_PREP(DP_ANA_TX_EN_DRV_D2, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_DRV_D3,
+			  FIELD_PREP(DP_ANA_TX_EN_DRV_D3, 0));
+
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_EN_P2S_D0,
+			  FIELD_PREP(DP_ANA_TX_EN_P2S_D0, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_EN_P2S_D1,
+			  FIELD_PREP(DP_ANA_TX_EN_P2S_D1, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_EN_P2S_D2,
+			  FIELD_PREP(DP_ANA_TX_EN_P2S_D2, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_EN_P2S_D3,
+			  FIELD_PREP(DP_ANA_TX_EN_P2S_D3, 0));
+
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL6, DP_ANA_BG_EN,
+			  FIELD_PREP(DP_ANA_BG_EN, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_BG_EN_RCAL,
+			  FIELD_PREP(DP_ANA_BG_EN_RCAL, 0));
+
+	/* Both PLLs. */
+	regmap_write_bits(dp->regs, DP_MPLL_CTRL0, DP_ANA_MPLL_PD,
+			  FIELD_PREP(DP_ANA_MPLL_PD, 1));
+	regmap_write_bits(dp->regs, DP_PREPLL_CTRL0, DP_ANA_PREPLL_PD,
+			  FIELD_PREP(DP_ANA_PREPLL_PD, 1));
+}
+
+/* Calculate required bandwidth in kbps (Pixel Clock * Bits Per Pixel) */
+static u32 spacemit_dp_calc_required_bw(const struct drm_display_mode *mode,
+					int bpp)
+{
+	return mode->clock * bpp;
+}
+
+/*
+ * Calculate available link capacity in kbps (taking 8b/10b overhead into
+ * account)
+ */
+static u32 spacemit_dp_calc_link_capacity(enum spacemit_dp_link_rate rate,
+					  enum spacemit_dp_lane_count lanes)
+{
+	u32 rate_khz = rate;
+	u32 lane_count = lanes;
+
+	/* Capacity = Rate(kHz) * Lanes * 0.8 */
+	return (rate_khz * lane_count * 8) / 10;
+}
+
+static enum drm_connector_status
+spacemit_dp_bridge_detect(struct drm_bridge *bridge, struct drm_connector *connector)
+{
+	struct spacemit_dp_dev *dp = container_of(bridge,
+						  struct spacemit_dp_dev,
+						  bridge);
+	enum drm_connector_status status;
+
+	/*
+	 * The eDP panel is fixed, and DP_HPD_IN_STATUS does not read
+	 * meaningfully on that path. Report CONNECTED only once panel-edp has
+	 * joined the chain.
+	 */
+	if (dp->edp_mode) {
+		if (!dp->next_bridge)
+			return connector_status_disconnected;
+		return connector_status_connected;
+	}
+
+	/*
+	 * The HPD interrupt is gated by the DP pixel clock, which is off
+	 * exactly when a plug must be caught. Read the level register live
+	 * instead.
+	 */
+	guard(mutex)(&dp->mode_lock);
+	status = spacemit_dp_hw_detect_hpd(dp);
+
+	/*
+	 * The bind-time cap read only covers a sink that was already there.
+	 * Without a re-read here, link.max_rate stays 0 and every link config
+	 * is filtered out.
+	 */
+	if (status == connector_status_connected &&
+	    dp->connector_status != connector_status_connected)
+		spacemit_dp_hw_read_sink_caps(dp);
+
+	dp->connector_status = status;
+
+	return status;
+}
+
+static int spacemit_dp_conn_get_edid_block(void *data, u8 *buf,
+					   unsigned int block, size_t len)
+{
+	struct spacemit_dp_dev *dp = data;
+	u8 offset = (block & 1) * EDID_LENGTH;
+	u8 segment = block >> 1;
+	struct i2c_msg msgs[] = {
+		{
+			.addr = SPACEMIT_DP_DDC_SEGMENT_ADDR,
+			.len = 1,
+			.buf = &segment,
+		}, {
+			.addr = DDC_ADDR,
+			.len = 1,
+			.buf = &offset,
+		}, {
+			.addr = DDC_ADDR,
+			.flags = I2C_M_RD,
+			.len = len,
+			.buf = buf,
+		},
+	};
+	/* Segment 0 needs no segment-pointer write. */
+	struct i2c_msg *first = segment ? &msgs[0] : &msgs[1];
+	int num = segment ? 3 : 2;
+	int ret, retry;
+
+	/*
+	 * One i2c_transfer per block: the DP I2C-over-AUX core splits it
+	 * into MOT-chained AUX transactions, so the sink sees a repeated
+	 * start and the segment pointer survives until the read ends.
+	 */
+	for (retry = 0; retry < 3; retry++) {
+		ret = i2c_transfer(&dp->aux.ddc, first, num);
+		if (ret == num)
+			return 0;
+	}
+
+	dev_err(dp->dev, "EDID read failed, block %u: %d\n", block, ret);
+	return ret < 0 ? ret : -EIO;
+}
+
+static const struct drm_edid *
+spacemit_dp_bridge_edid_read(struct drm_bridge *bridge, struct drm_connector *connector)
+{
+	struct spacemit_dp_dev *dp = container_of(bridge,
+						  struct spacemit_dp_dev,
+						  bridge);
+	const struct drm_edid *edid = NULL;
+	int retry;
+
+	scoped_guard(mutex, &dp->mode_lock) {
+		for (retry = 0; retry < SPACEMIT_DP_SINK_READY_RETRIES; retry++) {
+			if (retry) {
+				spacemit_dp_aux_hw_reset(dp);
+				msleep(SPACEMIT_DP_SINK_READY_DELAY_MS);
+			}
+
+			edid = drm_edid_read_custom(connector,
+				spacemit_dp_conn_get_edid_block, dp);
+			if (edid)
+				break;
+		}
+	}
+
+	if (!edid)
+		dev_err(dp->dev, "failed to read EDID\n");
+
+	return edid;
+}
+
+static enum drm_mode_status
+spacemit_dp_bridge_mode_valid(struct drm_bridge *bridge,
+			      const struct drm_display_info *info,
+			 const struct drm_display_mode *mode)
+{
+	struct spacemit_dp_dev *dp = container_of(bridge,
+						  struct spacemit_dp_dev,
+						  bridge);
+	unsigned int max_link_rate, max_lanes;
+	u64 max_bw, req_bw;
+	const unsigned int bpp = 24;
+
+	/*
+	 * drm_dp_max_link_rate() is in 10 kHz units; clamp to what the PHY can
+	 * do.
+	 */
+	max_link_rate = min_t(unsigned int, drm_dp_max_link_rate(dp->dpcd),
+			      SPACEMIT_DP_SOURCE_MAX_RATE / 10);
+	max_lanes = drm_dp_max_lane_count(dp->dpcd);
+	if (!max_link_rate || !max_lanes) {
+		/* Sink not enumerated yet -- accept and re-check at modeset. */
+		return MODE_OK;
+	}
+
+	/*
+	 * max_link_rate is per lane in 10 kHz units. The x10 to reach kHz and
+	 * the 8b/10b coding overhead cancel down to a plain "* 8".
+	 */
+	max_bw = (u64)max_link_rate * max_lanes * 8;
+
+	/* Required bandwidth: pixel clock (kHz) * bits per pixel. */
+	req_bw = (u64)mode->clock * bpp;
+
+	if (req_bw > max_bw) {
+		drm_dbg_kms(bridge->dev,
+			    "rejecting %dx%d@%d: %llu > %llu kbit/s (%u kHz x%u lanes)\n",
+			    mode->hdisplay, mode->vdisplay, drm_mode_vrefresh(mode),
+			    req_bw, max_bw, max_link_rate * 10, max_lanes);
+		return MODE_CLOCK_HIGH;
+	}
+
+	return MODE_OK;
+}
+
+static const struct drm_encoder_funcs spacemit_dp_encoder_funcs = {
+	.destroy = drm_encoder_cleanup,
+};
+
+static void spacemit_dp_bridge_atomic_enable(struct drm_bridge *bridge,
+					     struct drm_atomic_commit *state)
+{
+	struct spacemit_dp_dev *dp = container_of(bridge,
+						  struct spacemit_dp_dev,
+						  bridge);
+	struct drm_bridge_state *new_bridge_state =
+		drm_atomic_get_new_bridge_state(state, bridge);
+	struct spacemit_dp_bridge_state *st =
+			to_spacemit_dp_bridge_state(new_bridge_state);
+	struct drm_connector_state *conn_state =
+		drm_atomic_get_new_connector_state(state, dp->connector);
+	struct drm_crtc_state *crtc_state =
+		drm_atomic_get_new_crtc_state(state, conn_state->crtc);
+	struct drm_display_mode *adjusted_mode = &crtc_state->adjusted_mode;
+	const struct spacemit_dp_link_config *cfg;
+	bool trained;
+	u64 clk_val;
+	u64 set_clk_val;
+	u32 req_bw;
+	u32 hpd;
+	int start;
+	int bpp;
+	int i;
+
+	guard(mutex)(&dp->mode_lock);
+	drm_mode_copy(&dp->mode, adjusted_mode);
+
+	/* Enable-time retry for the caps .detect() could not read. */
+	if (!dp->link.max_rate || !dp->link.max_num_lanes)
+		spacemit_dp_hw_read_sink_caps(dp);
+
+	if (dp->use_ext_pixel_clock && dp->pxclk) {
+		set_clk_val = adjusted_mode->clock * 1000;
+		if (set_clk_val) {
+			set_clk_val = clk_round_rate(dp->pxclk, set_clk_val);
+			clk_val = clk_get_rate(dp->pxclk);
+			if (clk_val != set_clk_val) {
+				clk_set_rate(dp->pxclk, set_clk_val);
+				dev_dbg(dp->dev, "set dp pxclk=%lld\n",
+					set_clk_val);
+			}
+		}
+		clk_val = clk_get_rate(dp->pxclk);
+		dp->pixel_clock = clk_val / 1000;
+	}
+
+	bpp = spacemit_dp_get_bpp(dp, st->color_format);
+	req_bw = spacemit_dp_calc_required_bw(adjusted_mode, bpp);
+
+	/*
+	 * Start where atomic_check() left off, but keep the upgrade walk as
+	 * fallback.
+	 */
+	start = st->link_cfg_idx >= 0 ? st->link_cfg_idx : 0;
+
+	for (trained = false, i = start;
+	     i < ARRAY_SIZE(spacemit_dp_link_priority_table); i++) {
+		u32 capacity;
+
+		cfg = &spacemit_dp_link_priority_table[i];
+
+		/* Source and sink limits */
+		if (cfg->rate > dp->link.max_rate || cfg->lanes > dp->link.max_num_lanes)
+			continue;
+
+		/* Bandwidth requirement */
+		capacity = spacemit_dp_calc_link_capacity(cfg->rate,
+							  cfg->lanes);
+		if (capacity < req_bw)
+			continue;
+
+		dev_dbg(dp->dev, "trying link config: rate %d lanes %d (capacity %d, required %d)\n",
+			cfg->rate, cfg->lanes, capacity, req_bw);
+
+		spacemit_dp_link_disable(dp);
+
+		/* Apply Hardware Settings */
+		if (spacemit_dp_set_plls(dp, cfg->rate,
+					 dp->use_ext_pixel_clock ? dp->pixel_clock
+								 : adjusted_mode->clock))
+			continue;
+
+		if (spacemit_dp_phy_set_lanes(dp, cfg->lanes))
+			continue;
+
+		if (spacemit_dp_phy_on(dp))
+			continue;
+
+		/*
+		 * Debounce: the transmitter was power-cycled just now and a
+		 * sink may legitimately drop HPD across a link-down. eDP is
+		 * hardwired and noisy.
+		 */
+		if (!dp->edp_mode &&
+		    regmap_read_poll_timeout(dp->regs, DP_HPD_INTERRUPT_STATUS, hpd,
+					     hpd & DP_HPD_IN_STATUS,
+					     SPACEMIT_DP_HPD_POLL_US,
+					     SPACEMIT_DP_HPD_DEBOUNCE_US)) {
+			dev_warn(dp->dev,
+				 "sink is not asserting HPD; not training\n");
+			break;
+		}
+
+		if (dp->edp_mode) {
+			regmap_write_bits(dp->regs, DP_CORE_CONTROL,
+					  DP_ENABLE_EDP,
+					  FIELD_PREP(DP_ENABLE_EDP, 0x1));
+			regmap_write_bits(dp->regs, DP_VIDEO_VSAMPLE_REG,
+					  DP_STREAM_ENC_EN,
+					  FIELD_PREP(DP_STREAM_ENC_EN, 0x1));
+			update_edp_config(dp, true);
+		}
+
+		/* Execute Link Training */
+		if (spacemit_dp_link_train(dp, cfg->rate, cfg->lanes) == 0) {
+			dev_dbg(dp->dev,
+				"link training succeeded: rate %d lanes %d\n",
+				cfg->rate, cfg->lanes);
+			trained = true;
+			break;
+		}
+
+		dev_warn(dp->dev, "link training failed: rate %d lanes %d, trying next config\n",
+			 cfg->rate, cfg->lanes);
+	}
+
+	/*
+	 * The DPU's pixel clock comes from the PHY PLL, so scanning out into an
+	 * untrained link never reaches CFG_RDY and wedges the CRTC on
+	 * flip_done.
+	 */
+	if (!trained) {
+		dev_err(dp->dev,
+			"no usable link config; leaving video disabled (%u kHz x%u lanes)\n",
+			dp->link.max_rate, dp->link.max_num_lanes);
+		/*
+		 * The caps may be from a sink that has since been swapped out.
+		 */
+		dp->link.max_rate = 0;
+		dp->link.max_num_lanes = 0;
+
+		/*
+		 * Deferred: we are inside the commit and hold the modeset
+		 * locks.
+		 */
+		schedule_work(&dp->modeset_retry_work);
+		return;
+	}
+
+	spacemit_dp_hw_set_msa_and_enable_video(dp, adjusted_mode,
+						dp->link_rate, dp->lane_count,
+						st->color_format);
+}
+
+static void spacemit_dp_bridge_atomic_disable(struct drm_bridge *bridge,
+					      struct drm_atomic_commit *state)
+{
+	struct spacemit_dp_dev *dp = container_of(bridge,
+						  struct spacemit_dp_dev,
+						  bridge);
+
+	guard(mutex)(&dp->mode_lock);
+
+	spacemit_dp_link_disable(dp);
+	memset(&dp->mode, 0, sizeof(dp->mode));
+}
+
+static int spacemit_dp_bridge_atomic_check(struct drm_bridge *bridge,
+					   struct drm_bridge_state *bridge_state,
+				      struct drm_crtc_state *crtc_state,
+				      struct drm_connector_state *conn_state)
+{
+	struct spacemit_dp_dev *dp = container_of(bridge,
+						  struct spacemit_dp_dev,
+						  bridge);
+	struct spacemit_dp_bridge_state *st =
+			to_spacemit_dp_bridge_state(bridge_state);
+	const struct drm_display_mode *mode = &crtc_state->adjusted_mode;
+	int bpp;
+	u32 req_bw;
+	int i;
+
+	/* A single output bus format, see ->atomic_get_output_bus_fmts(). */
+	st->color_format = SOC_VIDEO_RGB_8BIT;
+	bpp = spacemit_dp_get_bpp(dp, st->color_format);
+	req_bw = spacemit_dp_calc_required_bw(mode, bpp);
+
+	/* No caps yet: leave idx unset and let ->atomic_enable() retry. */
+	if (!dp->link.max_rate || !dp->link.max_num_lanes) {
+		st->link_cfg_idx = -1;
+		return 0;
+	}
+
+	for (i = 0; i < ARRAY_SIZE(spacemit_dp_link_priority_table); i++) {
+		const struct spacemit_dp_link_config *cfg =
+				&spacemit_dp_link_priority_table[i];
+
+		if (cfg->rate > dp->link.max_rate ||
+		    cfg->lanes > dp->link.max_num_lanes)
+			continue;
+		if (spacemit_dp_calc_link_capacity(cfg->rate, cfg->lanes) < req_bw)
+			continue;
+
+		st->link_cfg_idx = i;
+		return 0;
+	}
+
+	drm_dbg_kms(bridge->dev,
+		    "no DP link cfg fits %dx%d@%d (need %u kbit/s; sink %u kHz x %u lanes)\n",
+		    mode->hdisplay, mode->vdisplay, drm_mode_vrefresh(mode),
+		    req_bw, dp->link.max_rate * 10, dp->link.max_num_lanes);
+	return -EINVAL;
+}
+
+static struct drm_bridge_state *
+spacemit_dp_bridge_atomic_duplicate_state(struct drm_bridge *bridge)
+{
+	struct spacemit_dp_bridge_state *cur =
+		to_spacemit_dp_bridge_state(drm_priv_to_bridge_state(bridge->base.state));
+	struct spacemit_dp_bridge_state *new;
+
+	new = kzalloc_obj(*new);
+	if (!new)
+		return NULL;
+
+	__drm_atomic_helper_bridge_duplicate_state(bridge, &new->base);
+	new->link_cfg_idx = cur->link_cfg_idx;
+	new->color_format = cur->color_format;
+	return &new->base;
+}
+
+static void
+spacemit_dp_bridge_atomic_destroy_state(struct drm_bridge *bridge,
+					struct drm_bridge_state *state)
+{
+	kfree(to_spacemit_dp_bridge_state(state));
+}
+
+static u32 *spacemit_dp_bridge_atomic_get_output_bus_fmts(struct drm_bridge *bridge,
+							  struct drm_bridge_state *bridge_state,
+				struct drm_crtc_state *crtc_state,
+				struct drm_connector_state *conn_state,
+				unsigned int *num_output_fmts)
+{
+	u32 *output_fmts;
+
+	output_fmts = kmalloc_obj(*output_fmts, GFP_KERNEL);
+	if (!output_fmts) {
+		*num_output_fmts = 0;
+		return NULL;
+	}
+
+	output_fmts[0] = MEDIA_BUS_FMT_RGB888_1X24;
+	*num_output_fmts = 1;
+	return output_fmts;
+}
+
+static struct drm_bridge_state *
+spacemit_dp_bridge_atomic_create_state(struct drm_bridge *bridge)
+{
+	struct spacemit_dp_bridge_state *new;
+
+	new = kzalloc_obj(*new);
+	if (!new)
+		return ERR_PTR(-ENOMEM);
+
+	__drm_atomic_helper_bridge_state_init(&new->base, bridge);
+	new->link_cfg_idx = -1;
+	new->color_format = SOC_VIDEO_RGB_8BIT;
+	return &new->base;
+}
+
+static int spacemit_dp_bridge_attach(struct drm_bridge *bridge,
+				     struct drm_encoder *encoder,
+				enum drm_bridge_attach_flags flags)
+{
+	struct spacemit_dp_dev *dp = container_of(bridge,
+						  struct spacemit_dp_dev,
+						  bridge);
+
+	if (!dp->next_bridge)
+		return 0;
+
+	return drm_bridge_attach(encoder, dp->next_bridge, bridge, flags);
+}
+
+/*
+ * AUX-bus done-probing callback. panel-edp may probe inline (before .attach) or
+ * off the deferred-probe queue (after it), so handle both orders.
+ */
+static int spacemit_dp_edp_link_panel(struct drm_dp_aux *aux)
+{
+	struct spacemit_dp_dev *dp = container_of(aux, struct spacemit_dp_dev,
+						  aux);
+	struct drm_bridge *next;
+	int ret;
+
+	next = devm_drm_of_get_bridge(dp->dev, dp->dev->of_node, 1, 0);
+	if (IS_ERR(next))
+		return PTR_ERR(next);
+
+	dp->next_bridge = next;
+
+	/*
+	 * A non-NULL bridge.encoder means .attach already ran: late-chain
+	 * panel-edp.
+	 */
+	if (dp->bridge.encoder) {
+		ret = drm_bridge_attach(dp->bridge.encoder, next,
+					&dp->bridge,
+					DRM_BRIDGE_ATTACH_NO_CONNECTOR);
+		if (ret)
+			return ret;
+
+		/*
+		 * eDP HPD-IN is unreliable, so bind may have skipped the cap
+		 * read and left link.max_rate at 0. The AUX bus just worked;
+		 * read them here.
+		 */
+		if (dp->edp_mode) {
+			guard(mutex)(&dp->mode_lock);
+			spacemit_dp_hw_read_sink_caps(dp);
+		}
+
+		drm_bridge_hpd_notify(&dp->bridge,
+				      spacemit_dp_bridge_detect(&dp->bridge,
+								dp->connector));
+	}
+
+	return 0;
+}
+
+static const struct drm_bridge_funcs spacemit_dp_bridge_funcs = {
+	.attach				= spacemit_dp_bridge_attach,
+	.atomic_enable			= spacemit_dp_bridge_atomic_enable,
+	.atomic_disable			= spacemit_dp_bridge_atomic_disable,
+	.atomic_check			= spacemit_dp_bridge_atomic_check,
+	.atomic_duplicate_state		= spacemit_dp_bridge_atomic_duplicate_state,
+	.atomic_destroy_state		= spacemit_dp_bridge_atomic_destroy_state,
+	.atomic_create_state		= spacemit_dp_bridge_atomic_create_state,
+	.atomic_get_output_bus_fmts	= spacemit_dp_bridge_atomic_get_output_bus_fmts,
+	.mode_valid			= spacemit_dp_bridge_mode_valid,
+	.detect				= spacemit_dp_bridge_detect,
+	.edid_read			= spacemit_dp_bridge_edid_read,
+};
+
+static irqreturn_t spacemit_dp_irq_handler(int irq, void *data)
+{
+	struct spacemit_dp_dev *dp = data;
+	irqreturn_t ret = IRQ_NONE;
+	int idx;
+
+	if (!drm_dev_enter(dp->drm, &idx))
+		return IRQ_NONE;
+
+	/*
+	 * eDP .detect() ignores the HPD level (and never records it), so a
+	 * level flip would wake the thread forever; ack and ignore.
+	 */
+	if (dp->edp_mode) {
+		spacemit_dp_hw_clean_hpd(dp);
+		drm_dev_exit(idx);
+		return IRQ_HANDLED;
+	}
+
+	/*
+	 * Only compare here: the authoritative connector_status update
+	 * happens in the thread under mode_lock, via .detect().
+	 */
+	if (spacemit_dp_hw_detect_hpd(dp) != READ_ONCE(dp->connector_status))
+		ret = IRQ_WAKE_THREAD; /* Call hotplug_event */
+	if (spacemit_dp_hw_clean_hpd(dp) && ret == IRQ_NONE)
+		ret = IRQ_HANDLED;
+
+	drm_dev_exit(idx);
+	return ret;
+}
+
+static irqreturn_t spacemit_dp_hotplug_event_handler(int irq, void *data)
+{
+	struct spacemit_dp_dev *dp = data;
+	enum drm_connector_status status;
+	int idx;
+
+	if (!drm_dev_enter(dp->drm, &idx))
+		return IRQ_HANDLED;
+
+	/*
+	 * .detect() owns mode_lock and re-reads the sink caps on a fresh plug
+	 */
+	status = spacemit_dp_bridge_detect(&dp->bridge, dp->connector);
+	drm_bridge_hpd_notify(&dp->bridge, status);
+
+	drm_dev_exit(idx);
+	return IRQ_HANDLED;
+}
+
+static int spacemit_dp_resource_init(struct spacemit_dp_dev *dp,
+				     struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	void __iomem *base;
+
+	base = devm_platform_ioremap_resource(pdev, 0);
+	if (IS_ERR(base))
+		return dev_err_probe(dev, PTR_ERR(base),
+				     "failed to map registers\n");
+	dp->regs = devm_regmap_init_mmio(dev, base, &spacemit_dp_regmap_config);
+	if (IS_ERR(dp->regs))
+		return dev_err_probe(dev, PTR_ERR(dp->regs),
+				     "failed to init DP regmap\n");
+
+	/*
+	 * The pixel clock comes from the PHY PLL, so the controller never sets
+	 * it.
+	 */
+	dp->use_ext_pixel_clock = false;
+
+	dp->irq = platform_get_irq(pdev, 0);
+	if (dp->irq < 0) {
+		dev_err(dev, "failed to get IRQ\n");
+		return dp->irq;
+	}
+
+	return 0;
+}
+
+static int spacemit_dp_dev_init(struct spacemit_dp_dev *dp)
+{
+	int ret;
+	u32 m_isel = 0x5, m_mainsel = 0x19;
+	u32 m_pre = 0x0, m_post = 0x2;
+	u32 tx_mode = 0x1, tx_pre = 0x0;
+	u32 clk_div = 24 * 1000 / 100;
+
+	regmap_write_bits(dp->regs, DP_PHYIF_CTRL_ADDR, DP_XMIT_ENABLE,
+			  FIELD_PREP(DP_XMIT_ENABLE, 0));
+	usleep_range(2000, 4000);
+
+	regmap_write_bits(dp->regs, DP_MPLL_CTRL0, DP_ANA_MPLL_PD,
+			  FIELD_PREP(DP_ANA_MPLL_PD, 1));
+	regmap_write_bits(dp->regs, DP_PREPLL_CTRL0, DP_ANA_PREPLL_PD,
+			  FIELD_PREP(DP_ANA_PREPLL_PD, 1));
+	usleep_range(2000, 4000);
+
+	/* Reset Controller and PHY */
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_CONTROLLER_RESET,
+			  FIELD_PREP(DP_CONTROLLER_RESET, 0x1));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_PHY_RESET,
+			  FIELD_PREP(DP_PHY_RESET, 0x1));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_HDCP_RESET,
+			  FIELD_PREP(DP_HDCP_RESET, 0x1));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_AUX_RESET,
+			  FIELD_PREP(DP_AUX_RESET, 0x1));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_VIDEO_RESET,
+			  FIELD_PREP(DP_VIDEO_RESET, 0x1));
+	usleep_range(5000, 8000);
+
+	/* Release the resets */
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_CONTROLLER_RESET,
+			  FIELD_PREP(DP_CONTROLLER_RESET, 0x0));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_PHY_RESET,
+			  FIELD_PREP(DP_PHY_RESET, 0x0));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_HDCP_RESET,
+			  FIELD_PREP(DP_HDCP_RESET, 0x0));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_AUX_RESET,
+			  FIELD_PREP(DP_AUX_RESET, 0x0));
+	regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_VIDEO_RESET,
+			  FIELD_PREP(DP_VIDEO_RESET, 0x0));
+	usleep_range(2000, 4000);
+
+	regmap_write(dp->regs, DP_GENERAL_INTERRUPT,
+		     DP_AUX_REPLY_EVENT_INT_STA);
+
+	regmap_write_bits(dp->regs, DP_CORE_CONTROL,
+			  DP_DEFAULT_FAST_LINK_TRAIN_EN,
+			  FIELD_PREP(DP_DEFAULT_FAST_LINK_TRAIN_EN, 0x0));
+	regmap_write_bits(dp->regs, DP_CORE_CONTROL, DP_SCRAMBLER_DISABLE,
+			  FIELD_PREP(DP_SCRAMBLER_DISABLE, 0x0));
+	regmap_write_bits(dp->regs, DP_CORE_CONTROL, DP_SCALE_DOWN_MODE,
+			  FIELD_PREP(DP_SCALE_DOWN_MODE, 0x0));
+	regmap_write_bits(dp->regs, DP_PHYIF_CTRL_ADDR, DP_XMIT_ENABLE,
+			  FIELD_PREP(DP_XMIT_ENABLE, 0));
+
+	regmap_write_bits(dp->regs, DP_VIDEO_VSAMPLE_REG,
+			  DP_VIDEO_STREAM_ENABLE,
+			  FIELD_PREP(DP_VIDEO_STREAM_ENABLE, 0));
+
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_AUX_REPLY_EVENT_INT_STA_MSK,
+			  FIELD_PREP(DP_AUX_REPLY_EVENT_INT_STA_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_HDCP_INT_STA_MSK,
+			  FIELD_PREP(DP_HDCP_INT_STA_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_ILLEGAL_AUX_CMD_INT_STA_MSK,
+			  FIELD_PREP(DP_ILLEGAL_AUX_CMD_INT_STA_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_TYPE_C_EVENT_MSK,
+			  FIELD_PREP(DP_TYPE_C_EVENT_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK, DP_DSC_EVENT_MSK,
+			  FIELD_PREP(DP_DSC_EVENT_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_SDP_INT_STA_S3_MSK,
+			  FIELD_PREP(DP_SDP_INT_STA_S3_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_SDP_INT_STA_S2_MSK,
+			  FIELD_PREP(DP_SDP_INT_STA_S2_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_SDP_INT_STA_S1_MSK,
+			  FIELD_PREP(DP_SDP_INT_STA_S1_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_SDP_INT_STA_S0_MSK,
+			  FIELD_PREP(DP_SDP_INT_STA_S0_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+		DP_VIDEO_FIFO_OVERFLOW_INT_STA_S3_MSK,
+		FIELD_PREP(DP_VIDEO_FIFO_OVERFLOW_INT_STA_S3_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+		DP_VIDEO_FIFO_OVERFLOW_INT_STA_S2_MSK,
+		FIELD_PREP(DP_VIDEO_FIFO_OVERFLOW_INT_STA_S2_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+		DP_VIDEO_FIFO_OVERFLOW_INT_STA_S1_MSK,
+		FIELD_PREP(DP_VIDEO_FIFO_OVERFLOW_INT_STA_S1_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+		DP_VIDEO_FIFO_OVERFLOW_INT_STA_S0_MSK,
+		FIELD_PREP(DP_VIDEO_FIFO_OVERFLOW_INT_STA_S0_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_HPD_INTERRUPT_ENABLE,
+			  DP_SINK_IRQ_EVENT_MSK,
+			  FIELD_PREP(DP_SINK_IRQ_EVENT_MSK, 0x0));
+	regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+			  DP_HPD_INT_STA_MSK,
+			  FIELD_PREP(DP_HPD_INT_STA_MSK, 0x1));
+	regmap_write_bits(dp->regs, DP_HPD_INTERRUPT_ENABLE,
+			  DP_HOT_PLUG_EVENT_MSK,
+			  FIELD_PREP(DP_HOT_PLUG_EVENT_MSK, 0x1));
+	regmap_write_bits(dp->regs, DP_HPD_INTERRUPT_ENABLE,
+			  DP_HOT_UNPLUG_EVENT_MSK,
+			  FIELD_PREP(DP_HOT_UNPLUG_EVENT_MSK, 0x1));
+	regmap_write_bits(dp->regs, DP_HPD_INTERRUPT_ENABLE,
+			  DP_SINK_UNPLUG_ERROR_EVENT_MSK,
+			  FIELD_PREP(DP_SINK_UNPLUG_ERROR_EVENT_MSK, 0x0));
+	usleep_range(2000, 4000);
+
+	regmap_write_bits(dp->regs, DP_PHYIF_CTRL_ADDR, DP_PHY_BUSY_BYP,
+			  FIELD_PREP(DP_PHY_BUSY_BYP, 0x1));
+
+	/* Enable enhanced framing */
+	regmap_write_bits(dp->regs, DP_CORE_CONTROL, DP_ENHANCE_FRAMING_EN,
+			  FIELD_PREP(DP_ENHANCE_FRAMING_EN, 0x1));
+
+	ret = spacemit_dp_set_plls(dp, SPACEMIT_DP_LINK_RATE_2_70,
+				   dp->use_ext_pixel_clock ? 150000 : 148500);
+	if (ret)
+		return ret;
+
+	ret = spacemit_dp_phy_set_lanes(dp, SPACEMIT_DP_LANE_2);
+	if (ret)
+		return ret;
+
+	ret = spacemit_dp_phy_on(dp);
+	if (ret)
+		return ret;
+
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D0,
+			  FIELD_PREP(DP_ANA_TX_MODE_D0, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D1,
+			  FIELD_PREP(DP_ANA_TX_MODE_D1, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D2,
+			  FIELD_PREP(DP_ANA_TX_MODE_D2, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D3,
+			  FIELD_PREP(DP_ANA_TX_MODE_D3, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_RTCAL_FREQDIV_HBIT,
+		FIELD_PREP(DP_ANA_RTCAL_FREQDIV_HBIT, (clk_div >> 8) & 0x7f));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_RTCAL_BYPASS,
+			  FIELD_PREP(DP_ANA_RTCAL_BYPASS, 1));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_RTCAL_FREQDIV_LBIT,
+		FIELD_PREP(DP_ANA_RTCAL_FREQDIV_LBIT, clk_div & 0xff));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_BG_RCAL_SEL,
+			  FIELD_PREP(DP_ANA_BG_RCAL_SEL, 0));
+
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL8, DP_ANA_TX_RTM_D3,
+			  FIELD_PREP(DP_ANA_TX_RTM_D3, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL8, DP_ANA_TX_RTM_D2,
+			  FIELD_PREP(DP_ANA_TX_RTM_D2, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL8, DP_ANA_TX_RTM_D1,
+			  FIELD_PREP(DP_ANA_TX_RTM_D1, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL8, DP_ANA_TX_RTM_D0,
+			  FIELD_PREP(DP_ANA_TX_RTM_D0, 0));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_RTCAL_BYPASS,
+			  FIELD_PREP(DP_ANA_RTCAL_BYPASS, 1));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_RTCAL_BYPASS,
+			  FIELD_PREP(DP_ANA_RTCAL_BYPASS, 0));
+	msleep(100);
+
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_PRE_D3,
+			  FIELD_PREP(DP_ANA_TX_MODE_PRE_D3, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_PRE_D2,
+			  FIELD_PREP(DP_ANA_TX_MODE_PRE_D2, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_PRE_D1,
+			  FIELD_PREP(DP_ANA_TX_MODE_PRE_D1, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_PRE_D0,
+			  FIELD_PREP(DP_ANA_TX_MODE_PRE_D0, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_DE_D3,
+			  FIELD_PREP(DP_ANA_TX_MODE_DE_D3, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_DE_D2,
+			  FIELD_PREP(DP_ANA_TX_MODE_DE_D2, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_DE_D1,
+			  FIELD_PREP(DP_ANA_TX_MODE_DE_D1, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_DE_D0,
+			  FIELD_PREP(DP_ANA_TX_MODE_DE_D0, 1));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_POSTSEL_PRE_D3,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_PRE_D3, tx_pre));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_POSTSEL_PRE_D2,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_PRE_D2, tx_pre));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_POSTSEL_PRE_D1,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_PRE_D1, tx_pre));
+	regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_POSTSEL_PRE_D0,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_PRE_D0, tx_pre));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_ISEL_DRV_D3,
+			  FIELD_PREP(DP_ANA_TX_ISEL_DRV_D3, m_isel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_ISEL_DRV_D2,
+			  FIELD_PREP(DP_ANA_TX_ISEL_DRV_D2, m_isel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL1, DP_ANA_TX_MAINSEL_D2,
+			  FIELD_PREP(DP_ANA_TX_MAINSEL_D2, m_mainsel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL1, DP_ANA_TX_MAINSEL_D3,
+			  FIELD_PREP(DP_ANA_TX_MAINSEL_D3, m_mainsel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL1, DP_ANA_TX_ISEL_DRV_D1,
+			  FIELD_PREP(DP_ANA_TX_ISEL_DRV_D1, m_isel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL1, DP_ANA_TX_ISEL_DRV_D0,
+			  FIELD_PREP(DP_ANA_TX_ISEL_DRV_D0, m_isel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL2, DP_ANA_TX_POSTSEL_D1,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_D1, m_post));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL2, DP_ANA_TX_POSTSEL_D0,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_D0, m_post));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL2, DP_ANA_TX_POSTSEL_D3,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_D3, m_post));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL2, DP_ANA_TX_POSTSEL_D2,
+			  FIELD_PREP(DP_ANA_TX_POSTSEL_D2, m_post));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL2, DP_DA_TX_MAINSEL_D0_4_0,
+			  FIELD_PREP(DP_DA_TX_MAINSEL_D0_4_0, m_mainsel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL2, DP_ANA_TX_MAINSEL_D1,
+			  FIELD_PREP(DP_ANA_TX_MAINSEL_D1, m_mainsel));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_PRESEL_D1,
+			  FIELD_PREP(DP_ANA_TX_PRESEL_D1, m_pre));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_PRESEL_D0,
+			  FIELD_PREP(DP_ANA_TX_PRESEL_D0, m_pre));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_PRESEL_D3,
+			  FIELD_PREP(DP_ANA_TX_PRESEL_D3, m_pre));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_PRESEL_D2,
+			  FIELD_PREP(DP_ANA_TX_PRESEL_D2, m_pre));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D3,
+			  FIELD_PREP(DP_ANA_TX_MODE_D3, tx_mode));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D2,
+			  FIELD_PREP(DP_ANA_TX_MODE_D2, tx_mode));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D1,
+			  FIELD_PREP(DP_ANA_TX_MODE_D1, tx_mode));
+	regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D0,
+			  FIELD_PREP(DP_ANA_TX_MODE_D0, tx_mode));
+	regmap_write_bits(dp->regs, DP_ANA_TX_AUX_CTRL, DP_ANA_TX_AUX_RX_VSEL,
+			  FIELD_PREP(DP_ANA_TX_AUX_RX_VSEL, 0x0));
+
+	return 0;
+}
+
+static int spacemit_dp_bind(struct device *dev, struct device *master,
+			    void *data)
+{
+	struct spacemit_dp_dev *dp = dev_get_drvdata(dev);
+	struct drm_device *drm = data;
+	struct device_node *ep;
+	int ret;
+
+	dp->drm = drm;
+	dp->aux.drm_dev = drm;
+	ret = drm_dp_aux_register(&dp->aux);
+	if (ret) {
+		dev_err(dev, "failed to register DP AUX: %d\n", ret);
+		return ret;
+	}
+
+	ret = drm_encoder_init(drm, &dp->encoder, &spacemit_dp_encoder_funcs,
+			       DRM_MODE_ENCODER_NONE, NULL);
+	if (ret) {
+		dev_err(dev, "failed to init encoder\n");
+		goto err_aux;
+	}
+
+	ep = of_graph_get_endpoint_by_regs(dev->of_node, 0, -1);
+	if (ep) {
+		struct device_node *remote = of_graph_get_remote_port(ep);
+
+		dp->encoder.possible_crtcs = drm_of_crtc_port_mask(drm, remote);
+		of_node_put(remote);
+		of_node_put(ep);
+	}
+
+	ret = drm_bridge_attach(&dp->encoder, &dp->bridge, NULL,
+				DRM_BRIDGE_ATTACH_NO_CONNECTOR);
+	if (ret) {
+		dev_err(dev, "failed to attach bridge: %d\n", ret);
+		goto err_encoder;
+	}
+
+	dp->connector = drm_bridge_connector_init(drm, &dp->encoder);
+	if (IS_ERR(dp->connector)) {
+		ret = PTR_ERR(dp->connector);
+		dev_err(dev, "failed to init bridge connector: %d\n", ret);
+		goto err_encoder;
+	}
+
+	drm_connector_attach_encoder(dp->connector, &dp->encoder);
+
+	ret = spacemit_dp_dev_init(dp);
+	if (ret)
+		goto err_encoder;
+
+	spacemit_dp_link_disable(dp);
+
+	dp->connector_status = spacemit_dp_hw_detect_hpd(dp);
+	spacemit_dp_hw_clean_hpd(dp);
+
+	if (dp->connector_status == connector_status_connected)
+		spacemit_dp_hw_read_sink_caps(dp);
+
+	/*
+	 * With panel-edp still deferred the chain is incomplete: a CONNECTED
+	 * notification here makes fbdev commit a modeset that wedges on
+	 * flip_done.
+	 */
+	if (dp->next_bridge || !dp->edp_mode)
+		drm_bridge_hpd_notify(&dp->bridge,
+				      spacemit_dp_bridge_detect(&dp->bridge,
+								dp->connector));
+
+	enable_irq(dp->irq);
+
+	return 0;
+
+err_encoder:
+	drm_encoder_cleanup(&dp->encoder);
+err_aux:
+	drm_dp_aux_unregister(&dp->aux);
+	return ret;
+}
+
+static void spacemit_dp_unbind(struct device *dev, struct device *master,
+			       void *data)
+{
+	struct spacemit_dp_dev *dp = dev_get_drvdata(dev);
+
+	disable_irq(dp->irq);
+
+	/* The connector this work touches dies with the DRM device. */
+	cancel_work_sync(&dp->modeset_retry_work);
+
+	drm_dp_aux_unregister(&dp->aux);
+
+	spacemit_dp_hw_disable(dp);
+
+	drm_encoder_cleanup(&dp->encoder);
+
+	dp->drm = NULL;
+	dp->connector = NULL;
+	dp->aux.drm_dev = NULL;
+}
+
+static const struct component_ops spacemit_dp_ops = {
+	.bind = spacemit_dp_bind,
+	.unbind = spacemit_dp_unbind,
+};
+
+/*
+ * Registered after devm_of_platform_populate(), so devres runs it before
+ * the PHY child (and the PLL clock it provides) is torn down: unregistering
+ * a clock that still has an enabled child warns, a disabled child is
+ * orphaned silently.
+ */
+static void spacemit_dp_pxclk_disable(void *data)
+{
+	clk_disable_unprepare(data);
+}
+
+static int inno_dp_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct device_node *aux_bus_np;
+	struct spacemit_dp_dev *dp;
+	struct reset_control *rstc;
+	struct resource *res;
+	int ret;
+
+	dp = devm_drm_bridge_alloc(dev, struct spacemit_dp_dev, bridge,
+				   &spacemit_dp_bridge_funcs);
+	if (IS_ERR(dp))
+		return PTR_ERR(dp);
+
+	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
+	if (!res)
+		return dev_err_probe(dev, -ENODEV,
+				     "no MMIO resource\n");
+
+	dp->dev = dev;
+	INIT_WORK(&dp->modeset_retry_work, spacemit_dp_modeset_retry_work);
+
+	/* eDP instances have an aux-bus child node; DP instances do not. */
+	aux_bus_np = of_get_child_by_name(dev->of_node, "aux-bus");
+	dp->edp_mode = !!aux_bus_np;
+	of_node_put(aux_bus_np);
+
+	dp->bridge.of_node = dev->of_node;
+	dp->connector_status = connector_status_disconnected;
+	ret = devm_mutex_init(dev, &dp->mode_lock);
+	if (ret)
+		return ret;
+
+	rstc = devm_reset_control_get_exclusive_deasserted(&pdev->dev, NULL);
+	if (IS_ERR(rstc)) {
+		ret = dev_err_probe(dev, PTR_ERR(rstc),
+				    "failed to get reset\n");
+		return ret;
+	}
+
+	dp->pxclk = devm_clk_get(dev, "pxclk");
+	if (IS_ERR(dp->pxclk)) {
+		ret = dev_err_probe(dev, PTR_ERR(dp->pxclk),
+				    "failed to get pxclk\n");
+		return ret;
+	}
+
+	if (dp->edp_mode) {
+		dp->bridge.type = DRM_MODE_CONNECTOR_eDP;
+		/*
+		 * OP_DETECT is required: without it
+		 * drm_bridge_connector_detect() hard-codes CONNECTED for any
+		 * eDP connector and fbdev wedges at bind.
+		 */
+		dp->bridge.ops = DRM_BRIDGE_OP_DETECT | DRM_BRIDGE_OP_EDID |
+				 DRM_BRIDGE_OP_HPD;
+	} else {
+		dp->bridge.type = DRM_MODE_CONNECTOR_DisplayPort;
+		/*
+		 * No OP_HPD: the HPD interrupt is gated by the DP pixel clock.
+		 * Leaving it out marks the connector POLL_CONNECT |
+		 * POLL_DISCONNECT instead.
+		 */
+		dp->bridge.ops = DRM_BRIDGE_OP_DETECT | DRM_BRIDGE_OP_EDID;
+	}
+
+	ret = clk_prepare_enable(dp->pxclk);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to enable pxclk\n");
+
+	/*
+	 * The PHY child shares this device's regmap, so the regmap must exist
+	 * before devm_of_platform_populate() lets the PHY probe and
+	 * devm_phy_get() succeed.
+	 */
+	ret = spacemit_dp_resource_init(dp, pdev);
+	if (ret)
+		goto err_disable_pxclk;
+
+	ret = devm_of_platform_populate(dev);
+	if (ret) {
+		dev_err_probe(dev, ret, "failed to populate child devices\n");
+		goto err_disable_pxclk;
+	}
+
+	dp->phy = devm_phy_get(dev, "dp");
+	if (IS_ERR(dp->phy)) {
+		ret = dev_err_probe(dev, PTR_ERR(dp->phy),
+				    "failed to get DP PHY\n");
+		goto err_disable_pxclk;
+	}
+
+	/*
+	 * The PHY exposes its PLL as the APMU pixel-clock mux's external
+	 * parent.
+	 */
+	dp->pll_clk = devm_clk_get(dev, "pll");
+	if (IS_ERR(dp->pll_clk)) {
+		ret = dev_err_probe(dev, PTR_ERR(dp->pll_clk),
+				    "failed to get PHY pixel clock\n");
+		goto err_disable_pxclk;
+	}
+
+	ret = clk_set_parent(dp->pxclk, dp->pll_clk);
+	if (ret) {
+		dev_err(dev, "failed to route eDP pixel mux to PHY PLL: %d\n", ret);
+		goto err_disable_pxclk;
+	}
+
+	ret = devm_add_action_or_reset(dev, spacemit_dp_pxclk_disable,
+				       dp->pxclk);
+	if (ret)
+		return ret;
+
+	/*
+	 * Panel discovery needs the AUX channel here; registration waits for
+	 * bind.
+	 */
+	dp->aux.name = "spacemit-dp-aux";
+	dp->aux.dev = dev;
+	dp->aux.transfer = spacemit_dp_aux_transfer;
+	dp->aux.no_zero_sized = true;
+	drm_dp_aux_init(&dp->aux);
+	dp->bridge.ddc = &dp->aux.ddc;
+
+	/*
+	 * eDP hands panel power, enable-GPIO and backlight to panel-edp over
+	 * the AUX bus. A DP instance has no aux-bus child and takes the
+	 * -ENODEV path.
+	 */
+	if (dp->edp_mode) {
+		ret = devm_of_dp_aux_populate_bus(&dp->aux,
+						  spacemit_dp_edp_link_panel);
+		if (ret) {
+			dev_err_probe(dev, ret,
+				"eDP: failed to populate aux-bus panel\n");
+			return ret;
+		}
+	}
+
+	/*
+	 * drm_bridge_attach() expects the bridge to already be on the global
+	 * list.
+	 */
+	ret = devm_drm_bridge_add(dev, &dp->bridge);
+	if (ret) {
+		dev_err(dev, "failed to register DP bridge: %d\n", ret);
+		return ret;
+	}
+
+	platform_set_drvdata(pdev, dp);
+
+	/*
+	 * The handler needs bind-created state, so keep the line disabled until
+	 * bind.
+	 */
+	ret = devm_request_threaded_irq(dev, dp->irq, spacemit_dp_irq_handler,
+					spacemit_dp_hotplug_event_handler,
+					IRQF_NO_AUTOEN, dev_name(dev), dp);
+	if (ret)
+		return ret;
+
+	ret = component_add(dev, &spacemit_dp_ops);
+	if (ret) {
+		dev_err(dev, "failed to add component: %d\n", ret);
+		return ret;
+	}
+
+	return 0;
+
+err_disable_pxclk:
+	clk_disable_unprepare(dp->pxclk);
+	return ret;
+}
+
+static void inno_dp_remove(struct platform_device *pdev)
+{
+	component_del(&pdev->dev, &spacemit_dp_ops);
+}
+
+static void inno_dp_shutdown(struct platform_device *pdev)
+{
+	struct spacemit_dp_dev *dp = platform_get_drvdata(pdev);
+
+	/* A deferred or failed bind leaves drvdata NULL. */
+	if (!dp)
+		return;
+
+	spacemit_dp_hw_disable(dp);
+}
+
+static const struct of_device_id spacemit_dp_match[] = {
+	{ .compatible = "spacemit,k3-inno-dp" },
+	{}
+};
+MODULE_DEVICE_TABLE(of, spacemit_dp_match);
+
+static struct platform_driver inno_dp_driver = {
+	.probe = inno_dp_probe,
+	.remove = inno_dp_remove,
+	.shutdown = inno_dp_shutdown,
+	.driver = {
+		.name = "spacemit-inno-dp-drv",
+		.of_match_table = spacemit_dp_match,
+	},
+};
+
+module_platform_driver(inno_dp_driver);
+
+MODULE_DESCRIPTION("SpacemiT K3 Innosilicon DP/eDP bridge driver");
+MODULE_LICENSE("GPL");
+MODULE_SOFTDEP("pre: phy-k3-inno-dp");
diff --git a/drivers/gpu/drm/spacemit/spacemit_inno_dp.h b/drivers/gpu/drm/spacemit/spacemit_inno_dp.h
new file mode 100644
index 0000000000000..680e2f41cd486
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/spacemit_inno_dp.h
@@ -0,0 +1,329 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ */
+
+#ifndef __SPACEMIT_INNO_DP_H__
+#define __SPACEMIT_INNO_DP_H__
+
+/* Register field access uses GENMASK/BIT + FIELD_PREP/FIELD_GET. */
+#include <linux/bits.h>
+
+#define DP_CORE_CONTROL                      0x0018
+#define  DP_SCRAMBLER_DISABLE               BIT(31)
+#define  DP_ENHANCE_FRAMING_EN              BIT(30)
+#define  DP_DEFAULT_FAST_LINK_TRAIN_EN      BIT(29)
+#define  DP_SCALE_DOWN_MODE                 BIT(28)
+#define  DP_FORCE_HPD                       BIT(27)
+#define  DP_ENABLE_EDP                      BIT(4)
+
+#define DP_SOFT_RESET                        0x001c
+#define  DP_CONTROLLER_RESET                BIT(31)
+#define  DP_PHY_RESET                       BIT(30)
+#define  DP_HDCP_RESET                      BIT(29)
+#define  DP_AUDIO_RESET                     BIT(28)
+#define  DP_AUX_RESET                       BIT(27)
+#define  DP_VIDEO_RESET                     GENMASK(3, 0)
+
+#define DP_CLK_INV_MUX                       0x0028
+#define  DP_REG_VID_CLK_SEL                 BIT(0)
+
+/* Interrupt status latches; write 1 to clear. */
+#define DP_GENERAL_INTERRUPT                 0x0080
+#define  DP_AUX_REPLY_EVENT_INT_STA         BIT(16)
+
+#define DP_GENERAL_INTERRUPT_MASK            0x0084
+#define  DP_HPD_INT_STA_MSK                 BIT(17)
+#define  DP_AUX_REPLY_EVENT_INT_STA_MSK     BIT(16)
+#define  DP_HDCP_INT_STA_MSK                BIT(15)
+#define  DP_ILLEGAL_AUX_CMD_INT_STA_MSK     BIT(14)
+#define  DP_TYPE_C_EVENT_MSK                BIT(13)
+#define  DP_DSC_EVENT_MSK                   BIT(12)
+#define  DP_SDP_INT_STA_S3_MSK              BIT(11)
+#define  DP_SDP_INT_STA_S2_MSK              BIT(10)
+#define  DP_SDP_INT_STA_S1_MSK              BIT(9)
+#define  DP_SDP_INT_STA_S0_MSK              BIT(8)
+#define  DP_VIDEO_FIFO_OVERFLOW_INT_STA_S3_MSK BIT(7)
+#define  DP_VIDEO_FIFO_OVERFLOW_INT_STA_S2_MSK BIT(6)
+#define  DP_VIDEO_FIFO_OVERFLOW_INT_STA_S1_MSK BIT(5)
+#define  DP_VIDEO_FIFO_OVERFLOW_INT_STA_S0_MSK BIT(4)
+
+#define DP_HPD_INTERRUPT_STATUS              0x0088
+#define  DP_HOT_PLUG_EVENT                  BIT(29)
+#define  DP_HOT_UNPLUG_EVENT                BIT(28)
+#define  DP_HPD_IN_STATUS                   BIT(26)
+
+#define DP_HPD_INTERRUPT_ENABLE              0x008c
+#define  DP_SINK_IRQ_EVENT_MSK              BIT(31)
+#define  DP_HOT_PLUG_EVENT_MSK              BIT(29)
+#define  DP_HOT_UNPLUG_EVENT_MSK            BIT(28)
+#define  DP_SINK_UNPLUG_ERROR_EVENT_MSK     BIT(27)
+
+#define DP_PHYIF_CTRL_ADDR                   0x0100
+#define  DP_PHY_BUSY_BYP                    BIT(31)
+#define  DP_TPS_SEL                         GENMASK(28, 25)
+#define  DP_XMIT_ENABLE                     GENMASK(20, 17)
+#define  DP_PHY_POWERDOWN                   GENMASK(16, 13)
+#define  DP_PHY_SSC_DIS                     BIT(8)
+#define  DP_PHY_NUM_LANES                   GENMASK(6, 5)
+#define  DP_PHY_RATE                        GENMASK(1, 0)
+
+#define DP_PHYIF_TX_EQ_ADDR                  0x0104
+#define  DP_PHY_LANE3_TX_VSWING             GENMASK(21, 20)
+#define  DP_PHY_LANE3_TX_PREEMP             GENMASK(19, 18)
+#define  DP_PHY_LANE2_TX_VSWING             GENMASK(15, 14)
+#define  DP_PHY_LANE2_TX_PREEMP             GENMASK(13, 12)
+#define  DP_PHY_LANE1_TX_VSWING             GENMASK(9, 8)
+#define  DP_PHY_LANE1_TX_PREEMP             GENMASK(7, 6)
+#define  DP_PHY_LANE0_TX_VSWING             GENMASK(3, 2)
+#define  DP_PHY_LANE0_TX_PREEMP             GENMASK(1, 0)
+
+#define DP_MPLL_CTRL0                        0x0180
+#define  DP_ANA_MPLL_FBDIV_LBIT             GENMASK(31, 24)
+#define  DP_ANA_MPLL_SEL_EXTWAVE            BIT(22)
+#define  DP_ANA_MPLL_DISABLE_SSCG           BIT(21)
+#define  DP_ANA_MPLL_DOWNSPREAD             BIT(20)
+#define  DP_ANA_MPLL_FBDIV_HBIT             GENMASK(19, 16)
+#define  DP_ANA_MPLL_OBSEN                  BIT(15)
+#define  DP_ANA_MPLL_OBSSEL                 BIT(14)
+#define  DP_ANA_MPLL_PREDIV                 GENMASK(13, 8)
+#define  DP_AD_LOCK_COREPLL                 BIT(7)
+#define  DP_ANA_MPLL_DACPD                  BIT(5)
+#define  DP_ANA_MPLL_DSMPD                  BIT(4)
+#define  DP_ANA_MPLL_PD                     BIT(0)
+
+#define DP_MPLL_CTRL1                        0x0184
+#define  DP_ANA_MPLL_FRAC_LBIT              GENMASK(23, 16)
+#define  DP_ANA_MPLL_FRAC_MBIT              GENMASK(15, 8)
+#define  DP_ANA_MPLL_FRAC_HBIT              GENMASK(7, 0)
+
+#define DP_MPLL_CTRL2                        0x0188
+#define  DP_ANA_MPLL_VCOCLK_DIV8_EN         BIT(16)
+#define  DP_ANA_MPLL_POSTDIVEN              BIT(11)
+#define  DP_ANA_MPLL_POSTDIV                GENMASK(10, 8)
+
+#define DP_PREPLL_CTRL0                      0x0190
+#define  DP_ANA_PREPLL_FBDIV2_LBIT          GENMASK(31, 24)
+#define  DP_ANA_PREPLL_FBDIV2_HBIT          GENMASK(19, 16)
+#define  DP_ANA_PREPLL_PREDIV               GENMASK(13, 8)
+#define  DP_AD_LOCK_PIXELPLL                BIT(7)
+#define  DP_ANA_PREPLL_DACPD                BIT(5)
+#define  DP_ANA_PREPLL_DSMPD                BIT(4)
+#define  DP_REG_PCLK_OUTPUT_NORMAL          BIT(1)
+#define  DP_ANA_PREPLL_PD                   BIT(0)
+
+#define DP_PREPLL_CTRL1                      0x0194
+#define  DP_ANA_PREPLL_PRECLK_DIVM          GENMASK(17, 16)
+#define  DP_ANA_PREPLL_PRECLK_DIVAUX        GENMASK(12, 8)
+
+#define DP_PREPLL_CTRL2                      0x0198
+#define  DP_ANA_PREPLL_PCLKDIV5_EN          BIT(31)
+#define  DP_ANA_PREPLL_PCLK_DIVAUX          GENMASK(28, 24)
+#define  DP_ANA_PREPLL_LOCK_BYPEN           BIT(17)
+#define  DP_ANA_PREPLL_LOWFRE_EN            BIT(16)
+#define  DP_ANA_PREPLL_TESTEN               BIT(15)
+#define  DP_ANA_PREPLL_TESTSEL              GENMASK(14, 12)
+#define  DP_ANA_PREPLL_HDMI_EN              BIT(9)
+#define  DP_ANA_PREPLL_DP_EN                BIT(8)
+
+#define DP_PREPLL_SSC_CTRL                   0x019c
+#define  DP_ANA_PREPLL_FRAC2_LBIT           GENMASK(23, 16)
+#define  DP_ANA_PREPLL_FRAC2_MBIT           GENMASK(15, 8)
+#define  DP_ANA_PREPLL_FRAC2_HBIT           GENMASK(7, 0)
+
+#define DP_ANA_TX_CTRL0                      0x01a0
+#define  DP_ANA_MPLL_CLKDIV_16M             GENMASK(13, 8)
+
+#define DP_ANA_TX_ENABLE                     0x01a4
+#define  DP_ANA_TX_ISEL_DRV_D3              GENMASK(31, 28)
+#define  DP_ANA_TX_ISEL_DRV_D2              GENMASK(27, 24)
+#define  DP_AD_TX_ESDSENSE_D3               GENMASK(23, 22)
+#define  DP_AD_TX_ESDSENSE_D2               GENMASK(21, 20)
+#define  DP_AD_TX_ESDSENSE_D1               GENMASK(19, 18)
+#define  DP_AD_TX_ESDSENSE_D0               GENMASK(17, 16)
+#define  DP_ANA_TX_EN_LDO_D3                BIT(11)
+#define  DP_ANA_TX_EN_LDO_D2                BIT(10)
+#define  DP_ANA_TX_EN_LDO_D1                BIT(9)
+#define  DP_ANA_TX_EN_LDO_D0                BIT(8)
+#define  DP_ANA_TX_EN_BYP_D3                BIT(7)
+#define  DP_ANA_TX_EN_BYP_D2                BIT(6)
+#define  DP_ANA_TX_EN_BYP_D1                BIT(5)
+#define  DP_ANA_TX_EN_BYP_D0                BIT(4)
+#define  DP_ANA_TX_EN_DRV_D3                BIT(3)
+#define  DP_ANA_TX_EN_DRV_D2                BIT(2)
+#define  DP_ANA_TX_EN_DRV_D1                BIT(1)
+#define  DP_ANA_TX_EN_DRV_D0                BIT(0)
+
+#define DP_ANA_TX_CTRL1                      0x01a8
+#define  DP_ANA_TX_PHASE_D2                 GENMASK(31, 30)
+#define  DP_ANA_TX_MAINSEL_D2               GENMASK(28, 24)
+#define  DP_ANA_TX_PHASE_D3                 GENMASK(23, 22)
+#define  DP_ANA_TX_MAINSEL_D3               GENMASK(20, 16)
+#define  DP_ANA_TX_ISEL_LDO_D3              BIT(11)
+#define  DP_ANA_TX_ISEL_LDO_D2              BIT(10)
+#define  DP_ANA_TX_ISEL_LDO_D1              BIT(9)
+#define  DP_ANA_TX_ISEL_LDO_D0              BIT(8)
+#define  DP_ANA_TX_ISEL_DRV_D1              GENMASK(7, 4)
+#define  DP_ANA_TX_ISEL_DRV_D0              GENMASK(3, 0)
+
+#define DP_ANA_TX_CTRL2                      0x01ac
+#define  DP_ANA_TX_POSTSEL_D1               GENMASK(31, 28)
+#define  DP_ANA_TX_POSTSEL_D0               GENMASK(27, 24)
+#define  DP_ANA_TX_POSTSEL_D3               GENMASK(23, 20)
+#define  DP_ANA_TX_POSTSEL_D2               GENMASK(19, 16)
+#define  DP_ANA_TX_PHASE_D0                 GENMASK(15, 14)
+#define  DP_DA_TX_MAINSEL_D0_4_0            GENMASK(12, 8)
+#define  DP_ANA_TX_PHASE_D1                 GENMASK(7, 6)
+#define  DP_ANA_TX_MAINSEL_D1               GENMASK(4, 0)
+
+#define DP_ANA_TX_CTRL3                      0x01b0
+#define  DP_ANA_TX_EN_P2S_D3                BIT(31)
+#define  DP_ANA_TX_EN_P2S_D2                BIT(30)
+#define  DP_ANA_TX_EN_P2S_D1                BIT(29)
+#define  DP_ANA_TX_EN_P2S_D0                BIT(28)
+#define  DP_ANA_TX_MODE_D3                  BIT(27)
+#define  DP_ANA_TX_MODE_D2                  BIT(26)
+#define  DP_ANA_TX_MODE_D1                  BIT(25)
+#define  DP_ANA_TX_MODE_D0                  BIT(24)
+#define  DP_ANA_TX_PRESEL_D1                GENMASK(14, 12)
+#define  DP_ANA_TX_PRESEL_D0                GENMASK(10, 8)
+#define  DP_ANA_TX_PRESEL_D3                GENMASK(6, 4)
+#define  DP_ANA_TX_PRESEL_D2                GENMASK(2, 0)
+
+#define DP_ANA_TX_CTRL6                      0x01bc
+#define  DP_ANA_BG_VREF5                    GENMASK(15, 14)
+#define  DP_ANA_BG_VREF4                    GENMASK(13, 12)
+#define  DP_ANA_BG_VREF3                    GENMASK(11, 10)
+#define  DP_ANA_BG_VREF2                    GENMASK(9, 8)
+#define  DP_ANA_BG_VREF                     GENMASK(7, 6)
+#define  DP_ANA_BG_VREF1                    GENMASK(5, 4)
+#define  DP_ANA_BG_EN_CHOP                  BIT(3)
+#define  DP_ANA_BG_EN                       BIT(2)
+#define  DP_ANA_BG_ISEL                     BIT(1)
+#define  DP_ANA_BG_VSEL                     BIT(0)
+
+#define DP_ANA_TX_CTRL7                      0x01c0
+#define  DP_ANA_BG_EN_RCAL                  BIT(27)
+#define  DP_ANA_BG_RCAL_SEL                 GENMASK(26, 25)
+#define  DP_AD_BG_RCAL_OUT                  BIT(24)
+#define  DP_ANA_RTCAL_FREQDIV_LBIT          GENMASK(23, 16)
+#define  DP_ANA_RTCAL_BYPASS                BIT(15)
+#define  DP_ANA_RTCAL_FREQDIV_HBIT          GENMASK(14, 8)
+#define  DP_ANA_BG_RCAL_VAL                 GENMASK(5, 0)
+
+#define DP_ANA_TX_CTRL8                      0x01c4
+#define  DP_ANA_TX_RTM_D3                   GENMASK(29, 24)
+#define  DP_ANA_TX_RTM_D2                   GENMASK(21, 16)
+#define  DP_ANA_TX_RTM_D1                   GENMASK(13, 8)
+#define  DP_ANA_TX_RTM_D0                   GENMASK(5, 0)
+
+#define DP_ANA_TX_AUX_CTRL                   0x01d0
+#define  DP_ANA_TX_AUX_RX_VSEL              GENMASK(13, 12)
+
+#define DP_ANA_LANE_EMPH                     0x01dc
+#define  DP_ANA_TX_POSTSEL_PRE_D3           GENMASK(23, 20)
+#define  DP_ANA_TX_POSTSEL_PRE_D2           GENMASK(19, 16)
+#define  DP_ANA_TX_POSTSEL_PRE_D1           GENMASK(15, 12)
+#define  DP_ANA_TX_POSTSEL_PRE_D0           GENMASK(11, 8)
+#define  DP_ANA_TX_MODE_DE_D3               BIT(7)
+#define  DP_ANA_TX_MODE_DE_D2               BIT(6)
+#define  DP_ANA_TX_MODE_DE_D1               BIT(5)
+#define  DP_ANA_TX_MODE_DE_D0               BIT(4)
+#define  DP_ANA_TX_MODE_PRE_D3              BIT(3)
+#define  DP_ANA_TX_MODE_PRE_D2              BIT(2)
+#define  DP_ANA_TX_MODE_PRE_D1              BIT(1)
+#define  DP_ANA_TX_MODE_PRE_D0              BIT(0)
+
+#define DP_VIDEO_VSAMPLE_REG                 0x0200
+#define  DP_VIDEO_STREAM_ENABLE             BIT(28)
+#define  DP_VIDEO_MAPPING                   GENMASK(26, 22)
+#define  DP_STREAM_ENC_EN                   BIT(18)
+
+#define DP_VINPUT_POLARITY                   0x020c
+#define  DP_HSYNC_IN_POLARITY               BIT(29)
+#define  DP_VSYNC_IN_POLARITY               BIT(28)
+
+#define DP_VIDEO_CONFIG2                     0x0210
+#define  DP_VACTIVE                         GENMASK(31, 16)
+#define  DP_VBLANK                          GENMASK(15, 0)
+
+#define DP_VIDEO_CONFIG1                     0x0214
+#define  DP_HBLANK                          GENMASK(29, 16)
+#define  DP_HACTIVE                         GENMASK(15, 0)
+
+#define DP_VIDEO_CONFIG5                     0x0218
+#define  DP_AVERAGE_BYTES_PER_TU            GENMASK(31, 25)
+#define  DP_AVERAGE_BYTES_PER_TU_FRAC       GENMASK(24, 21)
+#define  DP_INIT_THRESHOLD                  GENMASK(18, 10)
+
+#define DP_VIDEO_CONFIG3                     0x021c
+#define  DP_H_FRONT_PORCH                   GENMASK(31, 16)
+#define  DP_H_SYNC_WIDTH                    GENMASK(15, 0)
+
+#define DP_VIDEO_CONFIG4                     0x0220
+#define  DP_V_FRONT_PORCH                   GENMASK(31, 16)
+#define  DP_V_SYNC_WIDTH                    GENMASK(15, 0)
+
+#define DP_VIDEO_MSA1                        0x0224
+#define  DP_HSTART                          GENMASK(31, 16)
+#define  DP_VSTART                          GENMASK(15, 0)
+
+#define DP_VIDEO_MSA2                        0x0228
+#define  DP_MISC0                           GENMASK(7, 0)
+
+#define DP_VIDEO_MSA3                        0x022c
+#define  DP_NVID                            GENMASK(31, 8)
+#define  DP_MISC1                           GENMASK(7, 0)
+
+#define DP_VIDEO_HBLANK_INTERVAL             0x0230
+#define  DP_HBLANK_INTERVAL                 GENMASK(31, 16)
+
+#define DP_VIDEO_COLORBAR_CFG                0x0238
+#define  DP_VID_BIST_EN                     BIT(0)
+
+#define DP_AUDIO_CONFIG1                     0x0300
+#define  DP_AUDIO_INF_SELECT                BIT(31)
+#define  DP_AUDIO_MUTE                      BIT(30)
+#define  DP_HBR_MODE_ENABLE                 BIT(29)
+#define  DP_AUDIO_DATA_IN_EN                GENMASK(28, 25)
+#define  DP_I2S_AUDIO_MODE                  GENMASK(24, 23)
+#define  DP_AUD_ADJUST_SEL                  BIT(22)
+#define  DP_AUDIO_DATA_WIDTH                GENMASK(21, 17)
+#define  DP_AUDIO_NUM_CHANNELS              GENMASK(16, 14)
+#define  DP_AUDIO_PACKET_ID                 GENMASK(13, 6)
+#define  DP_AUDIO_TIMESTAMP_VERSION_NUM     GENMASK(5, 0)
+
+#define DP_AUX_STS_REG                       0x0400
+#define  DP_AUX_STATUS                      GENMASK(31, 24)
+#define  DP_AUX_REPLY_ERR_CODE              GENMASK(6, 4)
+
+#define DP_AUX_CMD_REG                       0x0404
+#define  DP_AUX_LENGTH                      GENMASK(27, 24)
+#define  DP_AUX_ADDR                        GENMASK(23, 4)
+#define  DP_AUX_CMD_TYPE                    GENMASK(3, 0)
+
+#define DP_AUX_START_REG                     0x0408
+#define  DP_AUX_START                       BIT(31)
+
+#define DP_AUX_DATA4_REG                     0x040c
+#define  DP_AUX_DATA4                       GENMASK(31, 0)
+
+#define DP_AUX_DATA3_REG                     0x0410
+#define  DP_AUX_DATA3                       GENMASK(31, 0)
+
+#define DP_AUX_DATA2_REG                     0x0414
+#define  DP_AUX_DATA2                       GENMASK(31, 0)
+
+#define DP_AUX_DATA1_REG                     0x0418
+#define  DP_AUX_DATA1                       GENMASK(31, 0)
+
+#define DP_SDP_HORIZONTAL_CTRL               0x0420
+#define  DP_AUD_STREAM_HORIZONTAL_EN        BIT(13)
+#define  DP_AUD_TIMESTAMP_HORIZONTAL_EN     BIT(12)
+
+#define DP_SDP_VERTICAL_CTRL                 0x0424
+#define  DP_AUD_STREAM_VERTICAL_EN          BIT(13)
+#define  DP_AUD_TIMESTAMP_VERTICAL_EN       BIT(12)
+
+#endif /* __SPACEMIT_INNO_DP_H__ */

-- 
2.43.0


  parent reply	other threads:[~2026-08-09 13:15 UTC|newest]

Thread overview: 94+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-09 13:14 [PATCH v2 00/17] drm/spacemit: add SpacemiT K3 display support Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:14 ` [PATCH v2 01/17] dt-bindings: display: spacemit: add K3 Saturn DPU controller Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:14 ` [PATCH v2 02/17] dt-bindings: phy: add SpacemiT K3 Innosilicon DP PHY Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:14 ` [PATCH v2 03/17] dt-bindings: display: spacemit: add K3 Innosilicon DP/eDP controller Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:25   ` sashiko-bot
2026-08-09 13:25     ` sashiko-bot
2026-08-09 13:14 ` [PATCH v2 04/17] dt-bindings: soc: spacemit: allow eDP/DP PHY PLL pixel clocks on K3 APMU Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 20:57   ` Rob Herring (Arm)
2026-08-09 20:57     ` Rob Herring (Arm)
2026-08-09 20:57     ` Rob Herring (Arm)
2026-08-10 14:33   ` Rob Herring (Arm)
2026-08-10 14:33     ` Rob Herring (Arm)
2026-08-10 14:33     ` Rob Herring (Arm)
2026-08-09 13:14 ` [PATCH v2 05/17] phy: spacemit: add Innosilicon DP TX PHY driver Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:26   ` sashiko-bot
2026-08-09 13:26     ` sashiko-bot
2026-08-09 13:14 ` [PATCH v2 06/17] clk: spacemit: k3: parent eDP/DP pixel clock to the PHY PLL Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:14 ` [PATCH v2 07/17] drm/spacemit: add Saturn DPU register model Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:14 ` [PATCH v2 08/17] drm/spacemit: add Saturn DPU core types, cmdlist and display MMU Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:32   ` sashiko-bot
2026-08-09 13:32     ` sashiko-bot
2026-08-09 13:14 ` [PATCH v2 09/17] drm/spacemit: add Saturn DPU hardware backend Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:31   ` sashiko-bot
2026-08-09 13:31     ` sashiko-bot
2026-08-09 13:14 ` [PATCH v2 10/17] drm/spacemit: add Saturn DPU KMS pipeline Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:35   ` sashiko-bot
2026-08-09 13:35     ` sashiko-bot
2026-08-09 13:14 ` [PATCH v2 11/17] drm/spacemit: add Saturn DPU DRM device driver Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:35   ` sashiko-bot
2026-08-09 13:35     ` sashiko-bot
2026-08-09 13:14 ` Cody Kang via B4 Relay [this message]
2026-08-09 13:14   ` [PATCH v2 12/17] drm/spacemit: add Innosilicon DP/eDP controller bridge driver Cody Kang
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:36   ` sashiko-bot
2026-08-09 13:36     ` sashiko-bot
2026-08-09 13:14 ` [PATCH v2 13/17] MAINTAINERS: add SpacemiT K3 display driver entry Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:14 ` [PATCH v2 14/17] riscv: dts: spacemit: k3: add display nodes Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:14 ` [PATCH v2 15/17] riscv: dts: spacemit: k3-pico-itx: enable the DisplayPort output Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:30   ` sashiko-bot
2026-08-09 13:30     ` sashiko-bot
2026-08-09 13:14 ` [PATCH v2 16/17] riscv: dts: spacemit: k3-com260-ifx: " Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:14 ` [PATCH v2 17/17] riscv: defconfig: spacemit: k3: enable display driver Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang
2026-08-09 13:14   ` Cody Kang via B4 Relay
2026-08-09 13:14   ` Cody Kang via B4 Relay

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260809-k3-display-v2-12-327d7910bf71@gmail.com \
    --to=devnull+codykang.hk.gmail.com@kernel.org \
    --cc=airlied@gmail.com \
    --cc=alex@ghiti.fr \
    --cc=aou@eecs.berkeley.edu \
    --cc=bmasney@redhat.com \
    --cc=codykang.hk@gmail.com \
    --cc=conor+dt@kernel.org \
    --cc=devicetree@vger.kernel.org \
    --cc=dlan@kernel.org \
    --cc=docular.xu@gmail.com \
    --cc=dri-devel@lists.freedesktop.org \
    --cc=heylenay@4d2.org \
    --cc=krzk+dt@kernel.org \
    --cc=linux-clk@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-phy@lists.infradead.org \
    --cc=linux-riscv@lists.infradead.org \
    --cc=maarten.lankhorst@linux.intel.com \
    --cc=me@ziyao.cc \
    --cc=mripard@kernel.org \
    --cc=mturquette@baylibre.com \
    --cc=neil.armstrong@linaro.org \
    --cc=p.zabel@pengutronix.de \
    --cc=palmer@dabbelt.com \
    --cc=pjw@kernel.org \
    --cc=robh@kernel.org \
    --cc=sboyd@kernel.org \
    --cc=simona@ffwll.ch \
    --cc=spacemit@lists.linux.dev \
    --cc=tzimmermann@suse.de \
    --cc=u.kleine-koenig@baylibre.com \
    --cc=vkoul@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.