Devicetree
 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>
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 RESEND 12/17] drm/spacemit: add Innosilicon DP/eDP controller bridge driver
Date: Sat, 25 Jul 2026 00:51:21 -0400	[thread overview]
Message-ID: <20260725-k3-display-v1-12-6de34d80e86c@gmail.com> (raw)
In-Reply-To: <20260725-k3-display-v1-0-6de34d80e86c@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>
---
 drivers/gpu/drm/spacemit/Kconfig            |   19 +
 drivers/gpu/drm/spacemit/Makefile           |    3 +
 drivers/gpu/drm/spacemit/spacemit_inno_dp.c | 2443 +++++++++++++++++++++++++++
 drivers/gpu/drm/spacemit/spacemit_inno_dp.h |  328 ++++
 4 files changed, 2793 insertions(+)

diff --git a/drivers/gpu/drm/spacemit/Kconfig b/drivers/gpu/drm/spacemit/Kconfig
index 51e38dabfdba..afd6cdea27fc 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 b3b148000cf7..260d316a271b 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 000000000000..e88731d9b18b
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/spacemit_inno_dp.c
@@ -0,0 +1,2443 @@
+// 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/iopoll.h>
+#include <linux/media-bus-format.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_EDID_CHUNK_SIZE 16
+#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.
+ * @reset: Optional shared reset line.
+ * @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 reset_control *reset;
+	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_bits(dp->regs, DP_GENERAL_INTERRUPT,
+			  DP_AUX_REPLY_EVENT_INT_STA,
+			  FIELD_PREP(DP_AUX_REPLY_EVENT_INT_STA, 1));
+}
+
+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_bits(dp->regs, DP_GENERAL_INTERRUPT,
+			  DP_AUX_REPLY_EVENT_INT_STA,
+			  FIELD_PREP(DP_AUX_REPLY_EVENT_INT_STA, 1));
+
+	/* 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;
+}
+
+static void 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));
+}
+
+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_aux_i2c_write(struct spacemit_dp_dev *dp, u32 address,
+				     const u8 *buf, size_t len)
+{
+	struct drm_dp_aux_msg msg = {
+		.address = address,
+		.request = DP_AUX_I2C_WRITE,
+		.buffer = (u8 *)buf,
+		.size = len,
+	};
+	int ret;
+
+	ret = spacemit_dp_aux_transfer(&dp->aux, &msg);
+	if (ret < 0)
+		return ret;
+
+	return ret == len ? 0 : -EIO;
+}
+
+static int spacemit_dp_aux_i2c_read(struct spacemit_dp_dev *dp, u32 address,
+				    u8 *buf, size_t len)
+{
+	struct drm_dp_aux_msg msg = {
+		.address = address,
+		.request = DP_AUX_I2C_READ,
+		.buffer = buf,
+		.size = len,
+	};
+	int ret;
+
+	ret = spacemit_dp_aux_transfer(&dp->aux, &msg);
+	if (ret < 0)
+		return ret;
+
+	return ret == len ? 0 : -EIO;
+}
+
+static int spacemit_dp_conn_get_edid_block(void *data, u8 *buf,
+					   unsigned int block, size_t len)
+{
+	struct spacemit_dp_dev *dp = data;
+	unsigned int start = block * EDID_LENGTH;
+	u8 segment = block >> 1;
+	int ret, retry;
+	size_t offset;
+
+	if (segment) {
+		for (retry = 0; retry < 3; retry++) {
+			ret = spacemit_dp_aux_i2c_write(dp,
+				SPACEMIT_DP_DDC_SEGMENT_ADDR, &segment, 1);
+			if (!ret)
+				break;
+		}
+
+		if (ret) {
+			dev_err(dp->dev,
+				"EDID segment write failed, block %u segment %u: %d\n",
+				block, segment, ret);
+			return ret;
+		}
+	}
+
+	for (offset = 0; offset < len; offset += SPACEMIT_DP_EDID_CHUNK_SIZE) {
+		u8 edid_offset = (start + offset) & 0xff;
+		size_t chunk = min_t(size_t, SPACEMIT_DP_EDID_CHUNK_SIZE,
+				     len - offset);
+
+		for (retry = 0; retry < 3; retry++) {
+			ret = spacemit_dp_aux_i2c_write(dp, DDC_ADDR,
+							&edid_offset, 1);
+			if (ret)
+				continue;
+
+			ret = spacemit_dp_aux_i2c_read(dp, DDC_ADDR,
+						       buf + offset, chunk);
+			if (!ret)
+				break;
+		}
+
+		if (ret) {
+			dev_err(dp->dev,
+				"EDID read failed, block %u offset 0x%02x len %zu: %d\n",
+				block, edid_offset, chunk, ret);
+			return ret;
+		}
+	}
+
+	return 0;
+}
+
+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 */
+	spacemit_dp_hw_clean_hpd(dp);
+
+	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_bits(dp->regs, DP_GENERAL_INTERRUPT,
+			  DP_AUX_REPLY_EVENT_INT_STA,
+			  FIELD_PREP(DP_AUX_REPLY_EVENT_INT_STA, 1));
+
+	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;
+	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;
+	}
+
+	dp->encoder.possible_crtcs = drm_of_find_possible_crtcs(drm,
+								dev->of_node);
+
+	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,
+};
+
+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 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;
+
+	dp->reset = devm_reset_control_get_optional_exclusive(&pdev->dev, NULL);
+	if (IS_ERR(dp->reset)) {
+		ret = dev_err_probe(dev, PTR_ERR(dp->reset),
+				    "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 |
+				 DRM_BRIDGE_OP_MODES;
+	}
+
+	if (dp->reset) {
+		ret = reset_control_deassert(dp->reset);
+		if (ret) {
+			dev_err(dev, "failed to deassert reset: %d\n", ret);
+			return ret;
+		}
+	}
+
+	if (dp->pxclk) {
+		ret = clk_prepare_enable(dp->pxclk);
+		if (ret) {
+			dev_err(dev, "failed to enable pxclk: %d\n", ret);
+			goto err_reset;
+		}
+	}
+
+	/*
+	 * 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_clk;
+
+	ret = devm_of_platform_populate(dev);
+	if (ret) {
+		dev_err_probe(dev, ret, "failed to populate child devices\n");
+		goto err_clk;
+	}
+
+	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_clk;
+	}
+
+	/*
+	 * 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_clk;
+	}
+
+	if (dp->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_clk;
+		}
+	}
+
+	/*
+	 * 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");
+			goto err_clk;
+		}
+	}
+
+	/*
+	 * 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);
+		goto err_clk;
+	}
+
+	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) {
+		dev_err(dev, "failed to request irq %d: %d\n", dp->irq, ret);
+		goto err_clk;
+	}
+
+	ret = component_add(dev, &spacemit_dp_ops);
+	if (ret) {
+		dev_err(dev, "failed to add component: %d\n", ret);
+		goto err_clk;
+	}
+
+	return 0;
+
+err_clk:
+	clk_disable_unprepare(dp->pxclk);
+err_reset:
+	if (dp->reset)
+		reset_control_assert(dp->reset);
+
+	return ret;
+}
+
+static void inno_dp_remove(struct platform_device *pdev)
+{
+	struct spacemit_dp_dev *dp = platform_get_drvdata(pdev);
+
+	component_del(&pdev->dev, &spacemit_dp_ops);
+
+	if (dp) {
+		clk_disable_unprepare(dp->pxclk);
+		if (dp->reset)
+			reset_control_assert(dp->reset);
+	}
+}
+
+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 000000000000..412ef0c0f27e
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/spacemit_inno_dp.h
@@ -0,0 +1,328 @@
+/* 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)
+
+#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-07-25  4:51 UTC|newest]

Thread overview: 26+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-25  4:51 [PATCH RESEND 00/17] drm/spacemit: add SpacemiT K3 display support Cody Kang via B4 Relay
2026-07-25  4:51 ` [PATCH RESEND 01/17] dt-bindings: display: spacemit: add K3 Saturn DPU controller Cody Kang via B4 Relay
2026-07-25  4:51 ` [PATCH RESEND 02/17] dt-bindings: phy: add SpacemiT K3 Innosilicon DP PHY Cody Kang via B4 Relay
2026-07-25  4:51 ` [PATCH RESEND 03/17] dt-bindings: display: spacemit: add K3 Innosilicon DP/eDP controller Cody Kang via B4 Relay
2026-07-25  4:51 ` [PATCH RESEND 04/17] dt-bindings: soc: spacemit: allow eDP/DP PHY PLL pixel clocks on K3 APMU Cody Kang via B4 Relay
2026-07-25  4:51 ` [PATCH RESEND 05/17] phy: spacemit: add Innosilicon DP TX PHY driver Cody Kang via B4 Relay
2026-07-25  5:02   ` sashiko-bot
2026-07-25  4:51 ` [PATCH RESEND 06/17] clk: spacemit: k3: parent eDP/DP pixel clock to the PHY PLL Cody Kang via B4 Relay
2026-07-25  4:51 ` [PATCH RESEND 07/17] drm/spacemit: add Saturn DPU register model Cody Kang via B4 Relay
2026-07-25  4:51 ` [PATCH RESEND 08/17] drm/spacemit: add Saturn DPU core types, cmdlist and display MMU Cody Kang via B4 Relay
2026-07-25  5:04   ` sashiko-bot
2026-07-25  4:51 ` [PATCH RESEND 09/17] drm/spacemit: add Saturn DPU hardware backend Cody Kang via B4 Relay
2026-07-25  5:12   ` sashiko-bot
2026-07-25  4:51 ` [PATCH RESEND 10/17] drm/spacemit: add Saturn DPU KMS pipeline Cody Kang via B4 Relay
2026-07-25  5:04   ` sashiko-bot
2026-07-25  4:51 ` [PATCH RESEND 11/17] drm/spacemit: add Saturn DPU DRM device driver Cody Kang via B4 Relay
2026-07-25  5:05   ` sashiko-bot
2026-07-25  4:51 ` Cody Kang via B4 Relay [this message]
2026-07-25  5:04   ` [PATCH RESEND 12/17] drm/spacemit: add Innosilicon DP/eDP controller bridge driver sashiko-bot
2026-07-25  4:51 ` [PATCH RESEND 13/17] MAINTAINERS: add SpacemiT K3 display driver entry Cody Kang via B4 Relay
2026-07-25  4:51 ` [PATCH RESEND 14/17] riscv: dts: spacemit: k3: add display nodes Cody Kang via B4 Relay
2026-07-25  5:08   ` sashiko-bot
2026-07-25  4:51 ` [PATCH RESEND 15/17] riscv: dts: spacemit: k3-pico-itx: enable the DisplayPort output Cody Kang via B4 Relay
2026-07-25  5:12   ` sashiko-bot
2026-07-25  4:51 ` [PATCH RESEND 16/17] riscv: dts: spacemit: k3-com260-ifx: " Cody Kang via B4 Relay
2026-07-25  4:51 ` [PATCH RESEND 17/17] riscv: defconfig: spacemit: k3: enable display driver 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=20260725-k3-display-v1-12-6de34d80e86c@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=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=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=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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox