[PATCH v2 05/17] phy: spacemit: add Innosilicon DP TX PHY driver

Cody Kang via B4 Relay <[email protected]>
Newsgroups org.infradead.lists.linux-phy,dev.linux.lists.spacemit,org.freedesktop.lists.dri-devel,org.infradead.lists.linux-riscv,org.kernel.feeds.b4-sent,org.kernel.vger.linux-clk,org.kernel.vger.linux-devicetree,org.kernel.vger.linux-kernel
Message-ID <[email protected]>
From: Cody Kang <[email protected]>

The PHY's registers are interleaved into its parent DP controller's MMIO
window rather than a window of its own, so it probes as a child of the
controller, carries no reg or reset, and reaches its registers through
the controller's regmap.

It registers its pixel PLL as a clock provider. That lets the display
controller ask for a mode's pixel rate through clk_set_rate(), and it
lets the APMU pixel-clock mux parent to the PHY through the clock
framework. Without it the controller and PHY would have to know each
other's registers.

Signed-off-by: Cody Kang <[email protected]>

---
v2:
- drop the mod_devicetable.h include (Uwe Kleine-Koenig)
- unwind the lanes and pixel PLL on the power_on lock-timeout paths
  (Sashiko)
- document why power_off keeps the MPLL up: it feeds the 16 MHz AUX
  reference, and AUX must survive link drops (Sashiko)
- register the PHY and clock providers last in probe (Sashiko)
---
 drivers/phy/spacemit/Kconfig          |  13 +
 drivers/phy/spacemit/Makefile         |   1 +
 drivers/phy/spacemit/phy-k3-inno-dp.c | 967 ++++++++++++++++++++++++++++++++++
 3 files changed, 981 insertions(+)

diff --git a/drivers/phy/spacemit/Kconfig b/drivers/phy/spacemit/Kconfig
index 50b0005acf663..098f33998f732 100644
--- a/drivers/phy/spacemit/Kconfig
+++ b/drivers/phy/spacemit/Kconfig
@@ -23,3 +23,16 @@ config PHY_SPACEMIT_K1_USB2
 	help
 	  Enable this to support K1 USB 2.0 PHY driver. This driver takes care of
 	  enabling and clock setup and will be used by K1 udc/ehci/otg/xhci driver.
+
+config PHY_SPACEMIT_K3_INNO_DP
+	tristate "SpacemiT K3 Innosilicon DisplayPort PHY driver"
+	depends on (ARCH_SPACEMIT || COMPILE_TEST) && OF
+	depends on COMMON_CLK
+	select GENERIC_PHY
+	select REGMAP
+	help
+	  Enable support for the Innosilicon DisplayPort transmit PHY
+	  integrated in the SpacemiT K3 SoC. The PHY shares its MMIO window
+	  with its parent DP/eDP controller (drivers/gpu/drm/spacemit/) and
+	  exposes lane / rate / vswing / pre-emphasis control through the
+	  generic PHY framework.
diff --git a/drivers/phy/spacemit/Makefile b/drivers/phy/spacemit/Makefile
index a821a21d61422..960c5827f5e5f 100644
--- a/drivers/phy/spacemit/Makefile
+++ b/drivers/phy/spacemit/Makefile
@@ -1,3 +1,4 @@
 # SPDX-License-Identifier: GPL-2.0-only
 obj-$(CONFIG_PHY_SPACEMIT_K1_PCIE)		+= phy-k1-pcie.o
 obj-$(CONFIG_PHY_SPACEMIT_K1_USB2)		+= phy-k1-usb2.o
+obj-$(CONFIG_PHY_SPACEMIT_K3_INNO_DP)		+= phy-k3-inno-dp.o
diff --git a/drivers/phy/spacemit/phy-k3-inno-dp.c b/drivers/phy/spacemit/phy-k3-inno-dp.c
new file mode 100644
index 0000000000000..42ebfc347b84b
--- /dev/null
+++ b/drivers/phy/spacemit/phy-k3-inno-dp.c
@@ -0,0 +1,967 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * SpacemiT K3 Innosilicon DisplayPort PHY driver.
+ *
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ */
+
+#include <linux/bitfield.h>
+#include <linux/bitops.h>
+#include <linux/clk-provider.h>
+#include <linux/delay.h>
+#include <linux/iopoll.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/phy/phy.h>
+#include <linux/phy/phy-dp.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+
+/* Offsets are into the parent controller's MMIO window */
+#define DPTX_PHY_CTRL			0x0100
+#define DPTX_PHY_CTRL_XMIT_EN		GENMASK(20, 17)
+#define DPTX_PHY_CTRL_NUM_LANES		GENMASK(6, 5)
+#define DPTX_PHY_CTRL_RATE		GENMASK(1, 0)
+
+/* One 6-bit slot per lane; only the low 4 bits of each slot are used. */
+#define DPTX_PHY_LANE_TRIM		0x0104
+#define DPTX_PHY_LANE_BITS_PER		6
+#define DPTX_PHY_LANE_FIELD_MASK	0xf	/* preemp[1:0] | vswing[1:0] */
+#define DPTX_PHY_LANE_VSWING_SHIFT	2	/* within the lane field */
+
+/* MPLL is the link PLL (lane rate) */
+#define DPTX_ANA_MPLL			0x0180
+#define DPTX_ANA_MPLL_FBDIV_LBIT	GENMASK(31, 24)
+#define DPTX_ANA_MPLL_FBDIV_HBIT	GENMASK(19, 16)
+#define DPTX_ANA_MPLL_PREDIV		GENMASK(13, 8)
+#define DPTX_ANA_MPLL_LOCKED		BIT(7)
+#define DPTX_ANA_MPLL_DACPD		BIT(5)
+#define DPTX_ANA_MPLL_DSMPD		BIT(4)
+#define DPTX_ANA_MPLL_PD		BIT(0)
+
+#define DPTX_ANA_MPLL_FRAC		0x0184
+#define DPTX_ANA_MPLL_FRAC_LBIT		GENMASK(23, 16)
+#define DPTX_ANA_MPLL_FRAC_MBIT		GENMASK(15, 8)
+#define DPTX_ANA_MPLL_FRAC_HBIT		GENMASK(7, 0)
+
+#define DPTX_ANA_MPLL_DIV		0x0188
+#define DPTX_ANA_MPLL_VCOCLK_DIV8_EN	BIT(16)
+#define DPTX_ANA_MPLL_POSTDIVEN		BIT(11)
+#define DPTX_ANA_MPLL_POSTDIV		GENMASK(10, 8)
+
+/* PREPLL is the pixel PLL exposed as a clock. */
+#define DPTX_ANA_PREPLL			0x0190
+#define DPTX_ANA_PREPLL_FBDIV2_LBIT	GENMASK(31, 24)
+#define DPTX_ANA_PREPLL_FBDIV2_HBIT	GENMASK(19, 16)
+#define DPTX_ANA_PREPLL_PREDIV		GENMASK(13, 8)
+#define DPTX_ANA_PREPLL_LOCKED		BIT(7)
+#define DPTX_ANA_PREPLL_DACPD		BIT(5)
+#define DPTX_ANA_PREPLL_DSMPD		BIT(4)
+#define DPTX_ANA_PREPLL_PCLK_NORMAL	BIT(1)
+#define DPTX_ANA_PREPLL_PD		BIT(0)
+
+#define DPTX_ANA_PREPLL_DIV		0x0194
+#define DPTX_ANA_PREPLL_PRECLK_DIVM	GENMASK(17, 16)
+#define DPTX_ANA_PREPLL_PRECLK_DIVAUX	GENMASK(12, 8)
+
+#define DPTX_ANA_PREPLL_CTRL		0x0198
+#define DPTX_ANA_PREPLL_PCLKDIV5_EN	BIT(31)
+#define DPTX_ANA_PREPLL_PCLK_DIVAUX	GENMASK(28, 24)
+#define DPTX_ANA_PREPLL_LOCK_BYPEN	BIT(17)
+#define DPTX_ANA_PREPLL_HDMI_EN		BIT(9)
+#define DPTX_ANA_PREPLL_DP_EN		BIT(8)
+
+#define DPTX_ANA_PREPLL_FRAC		0x019c
+#define DPTX_ANA_PREPLL_FRAC2_LBIT	GENMASK(23, 16)
+#define DPTX_ANA_PREPLL_FRAC2_MBIT	GENMASK(15, 8)
+#define DPTX_ANA_PREPLL_FRAC2_HBIT	GENMASK(7, 0)
+
+#define DPTX_ANA_TX_CTRL		0x01a0
+#define DPTX_ANA_MPLL_CLKDIV_16M	GENMASK(13, 8)
+
+/* PRECLK_DIVM register value -> divide factor */
+static const u8 k3_inno_dp_divm_factors[] = { 1, 2, 3, 5 };
+
+#define DPTX_VCO_MIN_KHZ		1000000
+#define DPTX_VCO_MAX_KHZ		3000000
+#define DPTX_PLL_FRAC_MOD		(1 << 24)
+#define DPTX_PLL_ERR_TOLERANCE		10
+
+struct k3_inno_dp_phy {
+	struct device	*dev;
+	struct regmap	*regmap;
+	struct phy	*phy;
+	u8		lanes;		/* tracks last set_lanes */
+
+	struct clk_hw	pxclk_hw;	/* the PREPLL's pixel clock */
+	unsigned long	pxclk_rate;	/* last programmed PREPLL rate */
+	u32		ref_clk_khz;	/* PLL reference */
+};
+
+static int k3_inno_dp_phy_power_on(struct phy *phy)
+{
+	struct k3_inno_dp_phy *p = phy_get_drvdata(phy);
+	u32 lane_en;
+	u32 val;
+	int ret;
+
+	switch (p->lanes) {
+	case 1:
+		lane_en = 0x1;
+		break;
+	case 2:
+		lane_en = 0x3;
+		break;
+	case 4:
+	default:
+		lane_en = 0xf;
+		break;
+	}
+
+	ret = regmap_write_bits(p->regmap, DPTX_ANA_MPLL, DPTX_ANA_MPLL_PD, 0);
+	if (ret)
+		return ret;
+	ret = regmap_write_bits(p->regmap, DPTX_ANA_PREPLL,
+				DPTX_ANA_PREPLL_PD, 0);
+	if (ret)
+		return ret;
+	usleep_range(2000, 4000);
+
+	ret = regmap_write_bits(p->regmap, DPTX_PHY_CTRL,
+				DPTX_PHY_CTRL_XMIT_EN,
+				FIELD_PREP(DPTX_PHY_CTRL_XMIT_EN, lane_en));
+	if (ret)
+		return ret;
+	usleep_range(2000, 4000);
+
+	ret = regmap_read_poll_timeout(p->regmap, DPTX_ANA_MPLL, val,
+				       val & DPTX_ANA_MPLL_LOCKED,
+				       2000, 10 * 1000);
+	if (ret) {
+		dev_err(p->dev, "DP PHY core PLL lock timed out\n");
+		goto err_power_down;
+	}
+
+	ret = regmap_read_poll_timeout(p->regmap, DPTX_ANA_PREPLL, val,
+				       val & DPTX_ANA_PREPLL_LOCKED,
+				       2000, 10 * 1000);
+	if (ret) {
+		dev_err(p->dev, "DP PHY pixel PLL lock timed out\n");
+		goto err_power_down;
+	}
+
+	return 0;
+
+	/*
+	 * The PHY core does not call power_off after a failed power_on, so
+	 * park what power_off parks. Not the MPLL: see power_off.
+	 */
+err_power_down:
+	regmap_write_bits(p->regmap, DPTX_PHY_CTRL, DPTX_PHY_CTRL_XMIT_EN, 0);
+	regmap_write_bits(p->regmap, DPTX_ANA_PREPLL, DPTX_ANA_PREPLL_PD,
+			  DPTX_ANA_PREPLL_PD);
+	return ret;
+}
+
+/*
+ * Asymmetric with power_on on purpose: the MPLL stays up because it also
+ * feeds the 16 MHz AUX reference (DPTX_ANA_MPLL_CLKDIV_16M), and AUX must
+ * keep working while the link is down (EDID, DPCD, panel detection). The
+ * controller parks the PHY through here on every link drop, including
+ * right after bind.
+ */
+static int k3_inno_dp_phy_power_off(struct phy *phy)
+{
+	struct k3_inno_dp_phy *p = phy_get_drvdata(phy);
+
+	regmap_write_bits(p->regmap, DPTX_PHY_CTRL, DPTX_PHY_CTRL_XMIT_EN, 0);
+	usleep_range(2000, 4000);
+
+	regmap_write_bits(p->regmap, DPTX_ANA_PREPLL, DPTX_ANA_PREPLL_PD,
+			  DPTX_ANA_PREPLL_PD);
+	usleep_range(2000, 4000);
+
+	return 0;
+}
+
+static int k3_inno_dp_phy_set_lanes(struct k3_inno_dp_phy *p, u8 lanes)
+{
+	u32 val;
+	int ret;
+
+	switch (lanes) {
+	case 1:
+		val = 0;
+		break;
+	case 2:
+		val = 1;
+		break;
+	case 4:
+		val = 2;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	ret = regmap_write_bits(p->regmap, DPTX_PHY_CTRL,
+				DPTX_PHY_CTRL_NUM_LANES,
+				FIELD_PREP(DPTX_PHY_CTRL_NUM_LANES, val));
+	if (ret)
+		return ret;
+
+	p->lanes = lanes;
+	return 0;
+}
+
+struct k3_inno_dp_mpll_cfg {
+	unsigned int link_rate;		/* Mbps per lane */
+	u8 prediv;
+	u16 fbdiv;
+	u32 frac;
+	u8 postdiv;
+	u8 postdiv_en;
+	u8 frac_pd;
+	u8 vcoclk_div8_en;
+	u8 clk_16mdiv;
+};
+
+static const struct k3_inno_dp_mpll_cfg k3_inno_dp_mpll_cfgs[] = {
+	{
+		.link_rate	= 1620,
+		.prediv		= 0x02,
+		.fbdiv		= 0x87,
+		.frac		= 0,
+		.postdiv	= 0,
+		.postdiv_en	= 1,
+		.frac_pd	= 0x3,
+		.vcoclk_div8_en	= 1,
+		.clk_16mdiv	= 12,
+	}, {
+		.link_rate	= 2700,
+		.prediv		= 0x02,
+		.fbdiv		= 0xe1,
+		.frac		= 0,
+		.postdiv	= 0,
+		.postdiv_en	= 1,
+		.frac_pd	= 0x3,
+		.vcoclk_div8_en	= 1,
+		.clk_16mdiv	= 21,
+	}, {
+		.link_rate	= 5400,
+		.prediv		= 0x02,
+		.fbdiv		= 0xe1,
+		.frac		= 0,
+		.postdiv	= 0,
+		.postdiv_en	= 0,
+		.frac_pd	= 0x3,
+		.vcoclk_div8_en	= 0,
+		.clk_16mdiv	= 42,
+	},
+};
+
+static void k3_inno_dp_phy_program_mpll(struct k3_inno_dp_phy *p,
+					const struct k3_inno_dp_mpll_cfg *cfg)
+{
+	struct regmap *rm = p->regmap;
+
+	/*
+	 * Shared with an unwired HDMI path: select DP first, otherwise the
+	 * PLLs still lock but nothing reaches the lanes and AUX times out.
+	 */
+	regmap_write_bits(rm, DPTX_ANA_PREPLL_CTRL, DPTX_ANA_PREPLL_DP_EN,
+		DPTX_ANA_PREPLL_DP_EN);
+	regmap_write_bits(rm, DPTX_ANA_PREPLL_CTRL, DPTX_ANA_PREPLL_HDMI_EN, 0);
+
+	regmap_write_bits(rm, DPTX_ANA_MPLL, DPTX_ANA_MPLL_PD,
+		FIELD_PREP(DPTX_ANA_MPLL_PD, 1));
+	usleep_range(2000, 4000);
+
+	regmap_write_bits(rm, DPTX_ANA_MPLL, DPTX_ANA_MPLL_PREDIV,
+		FIELD_PREP(DPTX_ANA_MPLL_PREDIV, cfg->prediv));
+	regmap_write_bits(rm, DPTX_ANA_MPLL, DPTX_ANA_MPLL_FBDIV_LBIT,
+		FIELD_PREP(DPTX_ANA_MPLL_FBDIV_LBIT, cfg->fbdiv & 0xff));
+	regmap_write_bits(rm, DPTX_ANA_MPLL, DPTX_ANA_MPLL_FBDIV_HBIT,
+		FIELD_PREP(DPTX_ANA_MPLL_FBDIV_HBIT, (cfg->fbdiv >> 8) & 0xf));
+	regmap_write_bits(rm, DPTX_ANA_MPLL, DPTX_ANA_MPLL_DACPD,
+		FIELD_PREP(DPTX_ANA_MPLL_DACPD, (cfg->frac_pd >> 1) & 0x1));
+	regmap_write_bits(rm, DPTX_ANA_MPLL, DPTX_ANA_MPLL_DSMPD,
+		FIELD_PREP(DPTX_ANA_MPLL_DSMPD, cfg->frac_pd & 0x1));
+
+	regmap_write_bits(rm, DPTX_ANA_MPLL_FRAC, DPTX_ANA_MPLL_FRAC_LBIT,
+		FIELD_PREP(DPTX_ANA_MPLL_FRAC_LBIT, cfg->frac & 0xff));
+	regmap_write_bits(rm, DPTX_ANA_MPLL_FRAC, DPTX_ANA_MPLL_FRAC_MBIT,
+		FIELD_PREP(DPTX_ANA_MPLL_FRAC_MBIT, (cfg->frac >> 8) & 0xff));
+	regmap_write_bits(rm, DPTX_ANA_MPLL_FRAC, DPTX_ANA_MPLL_FRAC_HBIT,
+		FIELD_PREP(DPTX_ANA_MPLL_FRAC_HBIT, (cfg->frac >> 16) & 0xff));
+
+	regmap_write_bits(rm, DPTX_ANA_MPLL_DIV, DPTX_ANA_MPLL_POSTDIV,
+		FIELD_PREP(DPTX_ANA_MPLL_POSTDIV, cfg->postdiv));
+	regmap_write_bits(rm, DPTX_ANA_MPLL_DIV, DPTX_ANA_MPLL_POSTDIVEN,
+		FIELD_PREP(DPTX_ANA_MPLL_POSTDIVEN, cfg->postdiv_en));
+	regmap_write_bits(rm, DPTX_ANA_MPLL_DIV, DPTX_ANA_MPLL_VCOCLK_DIV8_EN,
+		FIELD_PREP(DPTX_ANA_MPLL_VCOCLK_DIV8_EN, cfg->vcoclk_div8_en));
+
+	regmap_write_bits(rm, DPTX_ANA_TX_CTRL, DPTX_ANA_MPLL_CLKDIV_16M,
+		FIELD_PREP(DPTX_ANA_MPLL_CLKDIV_16M, cfg->clk_16mdiv));
+
+	regmap_write_bits(rm, DPTX_ANA_PREPLL_CTRL, DPTX_ANA_PREPLL_LOCK_BYPEN,
+		FIELD_PREP(DPTX_ANA_PREPLL_LOCK_BYPEN, 1));
+
+	regmap_write_bits(rm, DPTX_ANA_MPLL, DPTX_ANA_MPLL_PD,
+		FIELD_PREP(DPTX_ANA_MPLL_PD, 0));
+	usleep_range(2000, 4000);
+}
+
+static int k3_inno_dp_phy_set_rate(struct k3_inno_dp_phy *p,
+				   unsigned int link_rate)
+{
+	const struct k3_inno_dp_mpll_cfg *cfg = NULL;
+	unsigned int i;
+	u32 sel;
+
+	switch (link_rate) {
+	case 1620:
+		sel = 0;
+		break;
+	case 2700:
+		sel = 1;
+		break;
+	case 5400:
+		sel = 2;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	for (i = 0; i < ARRAY_SIZE(k3_inno_dp_mpll_cfgs); i++) {
+		if (k3_inno_dp_mpll_cfgs[i].link_rate == link_rate) {
+			cfg = &k3_inno_dp_mpll_cfgs[i];
+			break;
+		}
+	}
+	if (!cfg)
+		return -EINVAL;
+
+	k3_inno_dp_phy_program_mpll(p, cfg);
+
+	return regmap_write_bits(p->regmap, DPTX_PHY_CTRL,
+				 DPTX_PHY_CTRL_RATE,
+				 FIELD_PREP(DPTX_PHY_CTRL_RATE, sel));
+}
+
+static int k3_inno_dp_phy_set_voltages(struct k3_inno_dp_phy *p,
+				       struct phy_configure_opts_dp *opts)
+{
+	u8 lane;
+	int ret;
+
+	for (lane = 0; lane < opts->lanes; lane++) {
+		unsigned int shift = lane * DPTX_PHY_LANE_BITS_PER;
+		u32 mask = DPTX_PHY_LANE_FIELD_MASK << shift;
+		u32 val  = (((opts->voltage[lane] & 0x3) << DPTX_PHY_LANE_VSWING_SHIFT) |
+			    (opts->pre[lane] & 0x3)) << shift;
+
+		ret = regmap_write_bits(p->regmap, DPTX_PHY_LANE_TRIM, mask,
+					val);
+		if (ret)
+			return ret;
+	}
+
+	return 0;
+}
+
+static int k3_inno_dp_phy_configure(struct phy *phy,
+				    union phy_configure_opts *opts)
+{
+	struct k3_inno_dp_phy *p = phy_get_drvdata(phy);
+	int ret;
+
+	if (opts->dp.set_lanes) {
+		ret = k3_inno_dp_phy_set_lanes(p, opts->dp.lanes);
+		if (ret)
+			return ret;
+	}
+
+	if (opts->dp.set_rate) {
+		ret = k3_inno_dp_phy_set_rate(p, opts->dp.link_rate);
+		if (ret)
+			return ret;
+	}
+
+	if (opts->dp.set_voltages) {
+		ret = k3_inno_dp_phy_set_voltages(p, &opts->dp);
+		if (ret)
+			return ret;
+	}
+
+	return 0;
+}
+
+static u32 k3_inno_dp_div64(u64 *n, u32 base)
+{
+	return do_div(*n, base);
+}
+
+static int k3_inno_dp_better_cfg(bool new_valid, bool new_is_int, u8 new_pre, u32 new_vco,
+				 bool best_valid, bool best_is_int, u8 best_pre, u32 best_vco)
+{
+	if (!new_valid)
+		return 0;
+	if (!best_valid)
+		return 1;
+
+	if (new_is_int && !best_is_int)
+		return 1;
+	if (!new_is_int && best_is_int)
+		return 0;
+
+	if (new_pre < best_pre)
+		return 1;
+	if (new_pre > best_pre)
+		return 0;
+
+	if (new_vco > best_vco)
+		return 1;
+
+	return 0;
+}
+
+struct k3_inno_dp_prepll_cfg {
+	u32 target_pclk_khz;
+	u32 vco_freq_khz;
+	u8 prediv;
+	u16 fbdiv;
+	u32 frac_pd;
+	u32 frac;
+	u8 div5_en;
+	u8 divm;
+	u8 divaux;
+	u8 divp;
+	u32 actual_pclk_khz;
+	bool valid;
+};
+
+static const struct k3_inno_dp_prepll_cfg k3_inno_dp_prepll_cfgs[] = {
+	{ 614400, 2460000, 0x05, 512,  0x3, 0x0, 0x0, 0x1, 0x01, 0x1, 614400, true },
+	{ 594000, 2376000, 0x01, 99,   0x3, 0x0, 0x0, 0x1, 0x01, 0x1, 594000, true },
+	{ 551040, 2760000, 0x05, 574,  0x3, 0x0, 0x1, 0x0, 0x00, 0x0, 551040, true },
+	{ 533250, 2130000, 0x08, 711,  0x3, 0x0, 0x0, 0x1, 0x01, 0x1, 533250, true },
+	{ 443250, 1770000, 0x08, 591,  0x3, 0x0, 0x0, 0x1, 0x01, 0x1, 443250, true },
+	{ 375000, 3000000, 0x01, 125,  0x3, 0x0, 0x0, 0x0, 0x04, 0x1, 375000, true },
+	{ 372000, 2980000, 0x01, 124,  0x3, 0x0, 0x0, 0x0, 0x04, 0x1, 372000, true },
+	{ 348500, 2790000, 0x06, 697,  0x3, 0x0, 0x0, 0x0, 0x04, 0x1, 348500, true },
+	{ 307200, 1540000, 0x01, 64,   0x3, 0x0, 0x1, 0x0, 0x00, 0x0, 307200, true },
+	{ 297000, 2376000, 0x01, 99,   0x3, 0x0, 0x0, 0x0, 0x04, 0x1, 297000, true },
+	{ 280000, 1680000, 0x01, 70,   0x3, 0x0, 0x0, 0x2, 0x01, 0x1, 280000, true },
+	{ 277440, 2770000, 0x05, 578,  0x3, 0x0, 0x0, 0x3, 0x01, 0x1, 277440, true },
+	{ 245760, 2457600, 0x05, 512,  0x3, 0x0, 0x0, 0x3, 0x01, 0x1, 245760, true },
+	{ 241500, 1932000, 0x02, 161,  0x3, 0x0, 0x0, 0x0, 0x04, 0x1, 241500, true },
+	{ 236000, 2830000, 0x01, 118,  0x3, 0x0, 0x0, 0x0, 0x06, 0x1, 236000, true },
+	{ 204800, 2048000, 0x03, 256,  0x3, 0x0, 0x0, 0x3, 0x01, 0x1, 204800, true },
+	{ 193250, 2320000, 0x08, 773,  0x3, 0x0, 0x0, 0x0, 0x06, 0x1, 193250, true },
+	{ 189000, 1510000, 0x01,  63,  0x3, 0x0, 0x0, 0x0, 0x04, 0x1, 189000, true },
+	{ 187500, 3000000, 0x01, 125,  0x3, 0x0, 0x0, 0x0, 0x08, 0x1, 187500, true },
+	{ 162000, 2592000, 0x01, 108,  0x3, 0x0, 0x0, 0x0, 0x08, 0x1, 162000, true },
+	{ 156000, 2810000, 0x01, 117,  0x3, 0x0, 0x0, 0x0, 0x09, 0x1, 156000, true },
+	{ 150000, 3000000, 0x01, 125,  0x3, 0x0, 0x0, 0x0, 0x0a, 0x1, 150000, true },
+	{ 148500, 2376000, 0x01, 99,   0x3, 0x0, 0x0, 0x0, 0x08, 0x1, 148500, true },
+	{ 146000, 1750000, 0x01, 73,   0x3, 0x0, 0x0, 0x0, 0x06, 0x1, 146000, true },
+	{ 142860, 2860000, 0x14, 2381, 0x3, 0x0, 0x0, 0x0, 0x0a, 0x1, 142860, true },
+	{ 140000, 2520000, 0x01, 105,  0x3, 0x0, 0x0, 0x0, 0x09, 0x1, 140000, true },
+	{ 138500, 2220000, 0x03, 277,  0x3, 0x0, 0x0, 0x0, 0x08, 0x1, 138500, true },
+	{ 122000, 2930000, 0x01, 122,  0x3, 0x0, 0x0, 0x0, 0x0c, 0x1, 122000, true },
+	{ 121750, 2920000, 0x04, 487,  0x3, 0x0, 0x0, 0x0, 0x0c, 0x1, 121750, true },
+	{ 108000, 2810000, 0x01, 117,  0x3, 0x0, 0x0, 0x0, 0x0d, 0x1, 108000, true },
+	{ 106500, 1700000, 0x01,  71,  0x3, 0x0, 0x0, 0x0, 0x08, 0x1, 106500, true },
+	{ 83500,  2000000, 0x02, 167,  0x3, 0x0, 0x0, 0x0, 0x0c, 0x1, 83500,  true },
+	{ 79500,  2540000, 0x01, 106,  0x3, 0x0, 0x0, 0x0, 0x10, 0x1, 79500,  true },
+	{ 75000,  3000000, 0x01, 125,  0x3, 0x0, 0x0, 0x0, 0x14, 0x1, 75000,  true },
+	{ 74250,  2376000, 0x01,  99,  0x3, 0x0, 0x0, 0x0, 0x10, 0x1, 74250,  true },
+	{ 65000,  1560000, 0x01,  65,  0x3, 0x0, 0x0, 0x0, 0x0c, 0x1, 65000,  true },
+	{ 40000,  2880000, 0x01, 120,  0x3, 0x0, 0x0, 0x0, 0x12, 0x2, 40000,  true },
+	{ 27000,  2810000, 0x01, 117,  0x3, 0x0, 0x0, 0x0, 0x1a, 0x2, 27000,  true },
+	{ 25600,  2300000, 0x01,  96,  0x3, 0x0, 0x0, 0x0, 0x0f, 0x3, 25600,  true },
+	{ 25200,  2520000, 0x01, 105,  0x3, 0x0, 0x0, 0x0, 0x19, 0x2, 25200,  true },
+};
+
+static u64 k3_inno_dp_abs_diff(u64 a, u64 b)
+{
+	return (a > b) ? (a - b) : (b - a);
+}
+
+static u32 k3_inno_dp_pll_rate_khz(u8 pre, u16 fb, u32 frac,
+				   u32 ref_clk_khz, u32 total_div)
+{
+	u64 vco_hz;
+	u64 ref_hz = (u64)ref_clk_khz * 1000;
+	u64 int_part;
+	u64 frac_part;
+
+	/* Integer part: (Ref * FB) / Pre */
+	int_part = ref_hz * fb;
+	int_part += (pre / 2);
+	k3_inno_dp_div64(&int_part, pre);
+
+	/* Fractional part: (Ref * Frac) / (Pre * 2^24) */
+	frac_part = ref_hz * frac;
+
+	frac_part += (pre / 2);
+	k3_inno_dp_div64(&frac_part, pre);
+
+	frac_part += (DPTX_PLL_FRAC_MOD / 2);
+	k3_inno_dp_div64(&frac_part, DPTX_PLL_FRAC_MOD);
+
+	vco_hz = int_part + frac_part;
+
+	/* Final Rate = VCO / total_div */
+	vco_hz += (total_div / 2);
+	k3_inno_dp_div64(&vco_hz, total_div);
+
+	vco_hz += 500;
+	k3_inno_dp_div64(&vco_hz, 1000);
+
+	return (u32)vco_hz;
+}
+
+static int k3_inno_dp_prepll_div_total(const struct k3_inno_dp_prepll_cfg *cfg,
+				       u32 *div_total)
+{
+	if (!cfg || !div_total || !cfg->valid)
+		return -EINVAL;
+
+	/* div5_en fixes div_total at 5; divp is unused on this path */
+	if (cfg->div5_en) {
+		*div_total = 5;
+		return 0;
+	}
+
+	if (!cfg->divp)
+		return -EINVAL;
+
+	if (cfg->divaux == 1) {
+		if (cfg->divm >= ARRAY_SIZE(k3_inno_dp_divm_factors))
+			return -EINVAL;
+
+		*div_total = 2 * k3_inno_dp_divm_factors[cfg->divm] * cfg->divp;
+		return 0;
+	}
+
+	if (cfg->divaux < 2)
+		return -EINVAL;
+
+	*div_total = 2 * cfg->divaux * cfg->divp;
+	return 0;
+}
+
+static bool k3_inno_dp_prepll_cfg_matches(const struct k3_inno_dp_prepll_cfg *cfg,
+					  u32 target_pclk_khz,
+					 u32 ref_clk_khz)
+{
+	u32 div_total;
+	u32 actual_pclk_khz;
+
+	if (k3_inno_dp_prepll_div_total(cfg, &div_total))
+		return false;
+
+	actual_pclk_khz = k3_inno_dp_pll_rate_khz(cfg->prediv, cfg->fbdiv,
+						  cfg->frac, ref_clk_khz,
+						  div_total);
+
+	if (cfg->actual_pclk_khz &&
+	    k3_inno_dp_abs_diff(actual_pclk_khz,
+				cfg->actual_pclk_khz) > DPTX_PLL_ERR_TOLERANCE)
+		return false;
+
+	return k3_inno_dp_abs_diff(actual_pclk_khz,
+				    target_pclk_khz) <= DPTX_PLL_ERR_TOLERANCE;
+}
+
+static const struct k3_inno_dp_prepll_cfg *k3_inno_dp_find_prepll_cfg(u32 pclk_khz,
+								      u32 ref_clk_khz)
+{
+	const struct k3_inno_dp_prepll_cfg *best_match = NULL;
+	u32 min_diff = 0xFFFFFFFF;
+	int num_configs = ARRAY_SIZE(k3_inno_dp_prepll_cfgs);
+
+	for (int i = 0; i < num_configs; i++) {
+		u32 current_target = k3_inno_dp_prepll_cfgs[i].target_pclk_khz;
+		u32 diff = k3_inno_dp_abs_diff(pclk_khz, current_target);
+
+		if (diff == 0 && k3_inno_dp_prepll_cfg_matches(&k3_inno_dp_prepll_cfgs[i],
+							       pclk_khz, ref_clk_khz)) {
+			return &k3_inno_dp_prepll_cfgs[i];
+		}
+
+		if ((pclk_khz / 100) == (current_target / 100) &&
+		    k3_inno_dp_prepll_cfg_matches(&k3_inno_dp_prepll_cfgs[i],
+						  pclk_khz, ref_clk_khz)) {
+			return &k3_inno_dp_prepll_cfgs[i];
+		}
+
+		if (diff < min_diff && diff < 500 &&
+		    k3_inno_dp_prepll_cfg_matches(&k3_inno_dp_prepll_cfgs[i],
+						  pclk_khz, ref_clk_khz)) {
+			min_diff = diff;
+			best_match = &k3_inno_dp_prepll_cfgs[i];
+		}
+	}
+
+	return best_match;
+}
+
+static int k3_inno_dp_solve_frac(u32 target_vco_khz, u32 ref_clk_khz,
+				 u8 *best_pre, u16 *best_fb, u32 *best_frac)
+{
+	u64 min_err = ~0ULL;
+	int found = 0;
+	bool best_is_int = false;
+	int pre;
+
+	for (pre = 1; pre <= 63; pre++) {
+		u64 ref_clk_hz = (u64)ref_clk_khz * 1000;
+		u64 target_vco_hz = (u64)target_vco_khz * 1000;
+
+		/*
+		 * Calculate required multiplier: Mult = (TargetVCO * Pre) / Ref
+		 */
+		u64 num = target_vco_hz * pre;
+		u64 den = ref_clk_hz;
+		u64 remainder;
+		u64 fb_val;
+		u64 frac_val;
+		u64 actual_vco;
+		u64 diff;
+		bool current_is_int;
+
+		fb_val = num;
+		remainder = k3_inno_dp_div64(&fb_val, (u32)den);
+
+		if (fb_val > 4095)
+			continue;
+
+		/* Frac = (Remainder * 2^24 + Ref/2) / Ref */
+		frac_val = remainder * DPTX_PLL_FRAC_MOD;
+		frac_val += (den / 2);
+		k3_inno_dp_div64(&frac_val, (u32)den);
+
+		if (frac_val > 0xFFFFFF)
+			frac_val = 0xFFFFFF;
+
+		/* VCO = (Ref * FB / Pre) + (Ref * Frac / (Pre * 2^24)) */
+		{
+			u64 vco_int, vco_frac;
+
+			vco_int = ref_clk_hz * fb_val;
+			k3_inno_dp_div64(&vco_int, pre);
+			vco_frac = ref_clk_hz * frac_val;
+			k3_inno_dp_div64(&vco_frac, pre);
+			k3_inno_dp_div64(&vco_frac, DPTX_PLL_FRAC_MOD);
+
+			actual_vco = vco_int + vco_frac;
+		}
+
+		diff = k3_inno_dp_abs_diff(actual_vco, target_vco_hz);
+		current_is_int = (frac_val == 0);
+
+		if (diff < min_err) {
+			min_err = diff;
+			*best_pre = pre;
+			*best_fb = (u16)fb_val;
+			*best_frac = (u32)frac_val;
+			best_is_int = current_is_int;
+			found = 1;
+		} else if (diff == min_err) {
+			if (current_is_int && !best_is_int) {
+				*best_pre = pre;
+				*best_fb = (u16)fb_val;
+				*best_frac = (u32)frac_val;
+				best_is_int = true;
+				found = 1;
+			}
+		}
+	}
+
+	return found ? 0 : -EINVAL;
+}
+
+/* divaux must be 1 for the DivM path and > 1 for the DivAux path. */
+static void k3_inno_dp_try_prepll(u32 target_pclk_khz, u32 ref_clk_khz,
+				  u32 div_total, u8 div5_en, u8 divaux,
+				  u8 divm, u8 divp,
+				  struct k3_inno_dp_prepll_cfg *best)
+{
+	struct k3_inno_dp_prepll_cfg curr = {0};
+	u32 target_vco = target_pclk_khz * div_total;
+	u32 actual_pclk;
+	u32 frac;
+	u16 fb;
+	u8 pre;
+
+	if (target_vco < DPTX_VCO_MIN_KHZ || target_vco > DPTX_VCO_MAX_KHZ)
+		return;
+
+	if (k3_inno_dp_solve_frac(target_vco, ref_clk_khz, &pre, &fb, &frac))
+		return;
+
+	actual_pclk = k3_inno_dp_pll_rate_khz(pre, fb, frac, ref_clk_khz,
+					      div_total);
+	if (k3_inno_dp_abs_diff(actual_pclk, target_pclk_khz) > DPTX_PLL_ERR_TOLERANCE)
+		return;
+
+	curr.valid = true;
+	curr.vco_freq_khz = target_vco;
+	curr.actual_pclk_khz = actual_pclk;
+	curr.prediv = pre;
+	curr.fbdiv = fb;
+	curr.frac = frac;
+	curr.frac_pd = (frac == 0) ? 3 : 0;
+	curr.div5_en = div5_en;
+	curr.divaux = divaux;
+	curr.divm = divm;
+	curr.divp = divp;
+
+	if (k3_inno_dp_better_cfg(curr.valid, curr.frac == 0, curr.prediv,
+				  curr.vco_freq_khz, best->valid,
+				    best->frac == 0, best->prediv,
+				    best->vco_freq_khz))
+		*best = curr;
+}
+
+static int k3_inno_dp_solve_prepll(u32 target_pclk_khz, u32 ref_clk_khz,
+				   struct k3_inno_dp_prepll_cfg *cfg)
+{
+	struct k3_inno_dp_prepll_cfg best = {0};
+	int pclk_div;
+	int i;
+
+	/* Strategy 1: Div5 path (VCO = PCLK * 5) */
+	k3_inno_dp_try_prepll(target_pclk_khz, ref_clk_khz, 5, 1, 0, 0, 0,
+			      &best);
+
+	for (pclk_div = 1; pclk_div <= 31; pclk_div++) {
+		/* Strategy 2: DivM path */
+		for (i = 0; i < ARRAY_SIZE(k3_inno_dp_divm_factors); i++)
+			k3_inno_dp_try_prepll(target_pclk_khz, ref_clk_khz,
+				2 * k3_inno_dp_divm_factors[i] * pclk_div, 0, 1,
+				i, pclk_div, &best);
+
+		/* Strategy 3: DivAux path */
+		for (i = 2; i <= 31; i++)
+			k3_inno_dp_try_prepll(target_pclk_khz, ref_clk_khz,
+					      2 * i * pclk_div,
+					      0, i, 0, pclk_div, &best);
+	}
+
+	if (!best.valid)
+		return -EINVAL;
+
+	*cfg = best;
+	return 0;
+}
+
+static void k3_inno_dp_program_prepll(struct k3_inno_dp_phy *p,
+				      const struct k3_inno_dp_prepll_cfg *cfg)
+{
+	regmap_write_bits(p->regmap, DPTX_ANA_PREPLL, DPTX_ANA_PREPLL_PD,
+		FIELD_PREP(DPTX_ANA_PREPLL_PD, 1));
+	usleep_range(2000, 4000);
+
+	regmap_write_bits(p->regmap, DPTX_ANA_PREPLL, DPTX_ANA_PREPLL_PREDIV,
+		FIELD_PREP(DPTX_ANA_PREPLL_PREDIV, cfg->prediv));
+
+	regmap_write_bits(p->regmap, DPTX_ANA_PREPLL,
+		DPTX_ANA_PREPLL_FBDIV2_LBIT,
+		FIELD_PREP(DPTX_ANA_PREPLL_FBDIV2_LBIT, cfg->fbdiv & 0xFF));
+	regmap_write_bits(p->regmap, DPTX_ANA_PREPLL,
+		DPTX_ANA_PREPLL_FBDIV2_HBIT,
+		FIELD_PREP(DPTX_ANA_PREPLL_FBDIV2_HBIT, (cfg->fbdiv >> 8) & 0xF));
+
+	regmap_write_bits(p->regmap, DPTX_ANA_PREPLL, DPTX_ANA_PREPLL_DACPD,
+		FIELD_PREP(DPTX_ANA_PREPLL_DACPD, (cfg->frac_pd >> 1) & 0x1));
+	regmap_write_bits(p->regmap, DPTX_ANA_PREPLL, DPTX_ANA_PREPLL_DSMPD,
+		FIELD_PREP(DPTX_ANA_PREPLL_DSMPD, cfg->frac_pd & 0x1));
+
+	regmap_write_bits(p->regmap, DPTX_ANA_PREPLL_FRAC,
+		DPTX_ANA_PREPLL_FRAC2_LBIT,
+		FIELD_PREP(DPTX_ANA_PREPLL_FRAC2_LBIT, cfg->frac & 0xFF));
+	regmap_write_bits(p->regmap, DPTX_ANA_PREPLL_FRAC,
+		DPTX_ANA_PREPLL_FRAC2_MBIT,
+		FIELD_PREP(DPTX_ANA_PREPLL_FRAC2_MBIT, (cfg->frac >> 8) & 0xFF));
+	regmap_write_bits(p->regmap, DPTX_ANA_PREPLL_FRAC,
+		DPTX_ANA_PREPLL_FRAC2_HBIT,
+		FIELD_PREP(DPTX_ANA_PREPLL_FRAC2_HBIT, (cfg->frac >> 16) & 0xFF));
+
+	regmap_write_bits(p->regmap, DPTX_ANA_PREPLL_DIV,
+		DPTX_ANA_PREPLL_PRECLK_DIVM,
+		FIELD_PREP(DPTX_ANA_PREPLL_PRECLK_DIVM, cfg->divm));
+	regmap_write_bits(p->regmap, DPTX_ANA_PREPLL_DIV,
+		DPTX_ANA_PREPLL_PRECLK_DIVAUX,
+		FIELD_PREP(DPTX_ANA_PREPLL_PRECLK_DIVAUX, cfg->divaux));
+
+	regmap_write_bits(p->regmap, DPTX_ANA_PREPLL_CTRL,
+		DPTX_ANA_PREPLL_PCLKDIV5_EN,
+		FIELD_PREP(DPTX_ANA_PREPLL_PCLKDIV5_EN, cfg->div5_en));
+	regmap_write_bits(p->regmap, DPTX_ANA_PREPLL_CTRL,
+		DPTX_ANA_PREPLL_PCLK_DIVAUX,
+		FIELD_PREP(DPTX_ANA_PREPLL_PCLK_DIVAUX, cfg->divp));
+
+	regmap_write_bits(p->regmap, DPTX_ANA_PREPLL,
+		DPTX_ANA_PREPLL_PCLK_NORMAL,
+		FIELD_PREP(DPTX_ANA_PREPLL_PCLK_NORMAL, 1));
+	usleep_range(2000, 4000);
+
+	regmap_write_bits(p->regmap, DPTX_ANA_PREPLL, DPTX_ANA_PREPLL_PD,
+		FIELD_PREP(DPTX_ANA_PREPLL_PD, 0));
+	usleep_range(2000, 4000);
+}
+
+#define to_k3_inno_dp_phy_clk(_hw) \
+	container_of(_hw, struct k3_inno_dp_phy, pxclk_hw)
+
+static unsigned long k3_inno_dp_pxclk_recalc_rate(struct clk_hw *hw,
+						  unsigned long parent_rate)
+{
+	struct k3_inno_dp_phy *p = to_k3_inno_dp_phy_clk(hw);
+
+	return p->pxclk_rate;
+}
+
+static int k3_inno_dp_pxclk_determine_rate(struct clk_hw *hw,
+					   struct clk_rate_request *req)
+{
+	struct k3_inno_dp_phy *p = to_k3_inno_dp_phy_clk(hw);
+	const struct k3_inno_dp_prepll_cfg *cfg;
+	struct k3_inno_dp_prepll_cfg solved;
+	u32 khz = div_u64(req->rate, 1000);
+	int ret;
+
+	cfg = k3_inno_dp_find_prepll_cfg(khz, p->ref_clk_khz);
+	if (cfg) {
+		req->rate = (unsigned long)cfg->actual_pclk_khz * 1000;
+		return 0;
+	}
+
+	ret = k3_inno_dp_solve_prepll(khz, p->ref_clk_khz, &solved);
+	if (ret)
+		return ret;
+
+	req->rate = (unsigned long)solved.actual_pclk_khz * 1000;
+
+	return 0;
+}
+
+static int k3_inno_dp_pxclk_set_rate(struct clk_hw *hw, unsigned long rate,
+				     unsigned long parent_rate)
+{
+	struct k3_inno_dp_phy *p = to_k3_inno_dp_phy_clk(hw);
+	const struct k3_inno_dp_prepll_cfg *cfg;
+	struct k3_inno_dp_prepll_cfg solved;
+	u32 khz = div_u64(rate, 1000);
+	int ret;
+
+	cfg = k3_inno_dp_find_prepll_cfg(khz, p->ref_clk_khz);
+	if (!cfg) {
+		ret = k3_inno_dp_solve_prepll(khz, p->ref_clk_khz, &solved);
+		if (ret) {
+			dev_err(p->dev, "no PLL setting for %lu Hz\n", rate);
+			return ret;
+		}
+		cfg = &solved;
+	}
+
+	k3_inno_dp_program_prepll(p, cfg);
+	p->pxclk_rate = (unsigned long)cfg->actual_pclk_khz * 1000;
+
+	return 0;
+}
+
+static const struct clk_ops k3_inno_dp_pxclk_ops = {
+	.recalc_rate	= k3_inno_dp_pxclk_recalc_rate,
+	.determine_rate	= k3_inno_dp_pxclk_determine_rate,
+	.set_rate	= k3_inno_dp_pxclk_set_rate,
+};
+
+static const struct phy_ops k3_inno_dp_phy_ops = {
+	.power_on	= k3_inno_dp_phy_power_on,
+	.power_off	= k3_inno_dp_phy_power_off,
+	.configure	= k3_inno_dp_phy_configure,
+	.owner		= THIS_MODULE,
+};
+
+static int k3_inno_dp_phy_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct k3_inno_dp_phy *p;
+	struct phy_provider *provider;
+	struct clk_init_data init = {};
+	int ret;
+
+	p = devm_kzalloc(dev, sizeof(*p), GFP_KERNEL);
+	if (!p)
+		return -ENOMEM;
+
+	p->dev = dev;
+
+	p->regmap = dev_get_regmap(dev->parent, "dp");
+	if (!p->regmap)
+		return dev_err_probe(dev, -ENODEV,
+				     "parent DP regmap not available\n");
+
+	p->phy = devm_phy_create(dev, dev->of_node, &k3_inno_dp_phy_ops);
+	if (IS_ERR(p->phy))
+		return dev_err_probe(dev, PTR_ERR(p->phy),
+				     "failed to create DP PHY\n");
+
+	phy_set_drvdata(p->phy, p);
+
+	/* PHY takes a 24 MHz reference clock */
+	p->ref_clk_khz = 24000;
+
+	/* The APMU pixel-clock mux references this clock by name. */
+	of_property_read_string(dev->of_node, "clock-output-names", &init.name);
+	if (!init.name)
+		init.name = dev_name(dev);
+	init.ops = &k3_inno_dp_pxclk_ops;
+	init.parent_names = NULL;
+	init.num_parents = 0;
+	init.flags = 0;
+	p->pxclk_hw.init = &init;
+
+	ret = devm_clk_hw_register(dev, &p->pxclk_hw);
+	if (ret)
+		return dev_err_probe(dev, ret,
+				     "failed to register pixel clock\n");
+
+	ret = devm_of_clk_add_hw_provider(dev, of_clk_hw_simple_get,
+					  &p->pxclk_hw);
+	if (ret)
+		return dev_err_probe(dev, ret,
+				     "failed to add pixel clock provider\n");
+
+	/* Providers last: nothing may resolve a half-initialized PHY. */
+	provider = devm_of_phy_provider_register(dev, of_phy_simple_xlate);
+	if (IS_ERR(provider))
+		return dev_err_probe(dev, PTR_ERR(provider),
+				     "failed to register PHY provider\n");
+
+	return 0;
+}
+
+static const struct of_device_id k3_inno_dp_phy_of_match[] = {
+	{ .compatible = "spacemit,k3-inno-dp-phy" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, k3_inno_dp_phy_of_match);
+
+static struct platform_driver k3_inno_dp_phy_driver = {
+	.probe = k3_inno_dp_phy_probe,
+	.driver = {
+		.name		= "spacemit-k3-inno-dp-phy",
+		.of_match_table	= k3_inno_dp_phy_of_match,
+	},
+};
+module_platform_driver(k3_inno_dp_phy_driver);
+
+MODULE_DESCRIPTION("SpacemiT K3 Innosilicon DisplayPort PHY driver");
+MODULE_AUTHOR("Cody Kang <[email protected]>");
+MODULE_LICENSE("GPL");

-- 
2.43.0



-- 
linux-phy mailing list
[email protected]
https://lists.infradead.org/mailman/listinfo/linux-phy
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.