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
next prev 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