From: Cody Kang via B4 Relay <devnull+codykang.hk.gmail.com@kernel.org>
To: "David Airlie" <airlied@gmail.com>,
"Simona Vetter" <simona@ffwll.ch>,
"Maarten Lankhorst" <maarten.lankhorst@linux.intel.com>,
"Maxime Ripard" <mripard@kernel.org>,
"Thomas Zimmermann" <tzimmermann@suse.de>,
"Rob Herring" <robh@kernel.org>,
"Krzysztof Kozlowski" <krzk+dt@kernel.org>,
"Conor Dooley" <conor+dt@kernel.org>,
"Yixun Lan" <dlan@kernel.org>, "Vinod Koul" <vkoul@kernel.org>,
"Neil Armstrong" <neil.armstrong@linaro.org>,
"Haylen Chu" <heylenay@4d2.org>,
"Michael Turquette" <mturquette@baylibre.com>,
"Stephen Boyd" <sboyd@kernel.org>,
"Brian Masney" <bmasney@redhat.com>,
"Philipp Zabel" <p.zabel@pengutronix.de>,
"Paul Walmsley" <pjw@kernel.org>,
"Palmer Dabbelt" <palmer@dabbelt.com>,
"Albert Ou" <aou@eecs.berkeley.edu>,
"Alexandre Ghiti" <alex@ghiti.fr>, "Yao Zi" <me@ziyao.cc>,
"Uwe Kleine-König" <u.kleine-koenig@baylibre.com>,
"Guodong Xu" <docular.xu@gmail.com>
Cc: dri-devel@lists.freedesktop.org, linux-riscv@lists.infradead.org,
devicetree@vger.kernel.org, spacemit@lists.linux.dev,
linux-kernel@vger.kernel.org, linux-phy@lists.infradead.org,
linux-clk@vger.kernel.org, Cody Kang <codykang.hk@gmail.com>
Subject: [PATCH v2 05/17] phy: spacemit: add Innosilicon DP TX PHY driver
Date: Sun, 09 Aug 2026 21:14:14 +0800 [thread overview]
Message-ID: <20260809-k3-display-v2-5-327d7910bf71@gmail.com> (raw)
In-Reply-To: <20260809-k3-display-v2-0-327d7910bf71@gmail.com>
From: Cody Kang <codykang.hk@gmail.com>
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 <codykang.hk@gmail.com>
---
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 <codykang.hk@gmail.com>");
+MODULE_LICENSE("GPL");
--
2.43.0
WARNING: multiple messages have this Message-ID (diff)
From: Cody Kang via B4 Relay <devnull+codykang.hk.gmail.com@kernel.org>
To: "David Airlie" <airlied@gmail.com>,
"Simona Vetter" <simona@ffwll.ch>,
"Maarten Lankhorst" <maarten.lankhorst@linux.intel.com>,
"Maxime Ripard" <mripard@kernel.org>,
"Thomas Zimmermann" <tzimmermann@suse.de>,
"Rob Herring" <robh@kernel.org>,
"Krzysztof Kozlowski" <krzk+dt@kernel.org>,
"Conor Dooley" <conor+dt@kernel.org>,
"Yixun Lan" <dlan@kernel.org>, "Vinod Koul" <vkoul@kernel.org>,
"Neil Armstrong" <neil.armstrong@linaro.org>,
"Haylen Chu" <heylenay@4d2.org>,
"Michael Turquette" <mturquette@baylibre.com>,
"Stephen Boyd" <sboyd@kernel.org>,
"Brian Masney" <bmasney@redhat.com>,
"Philipp Zabel" <p.zabel@pengutronix.de>,
"Paul Walmsley" <pjw@kernel.org>,
"Palmer Dabbelt" <palmer@dabbelt.com>,
"Albert Ou" <aou@eecs.berkeley.edu>,
"Alexandre Ghiti" <alex@ghiti.fr>, "Yao Zi" <me@ziyao.cc>,
"Uwe Kleine-König" <u.kleine-koenig@baylibre.com>,
"Guodong Xu" <docular.xu@gmail.com>
Cc: dri-devel@lists.freedesktop.org, linux-riscv@lists.infradead.org,
devicetree@vger.kernel.org, spacemit@lists.linux.dev,
linux-kernel@vger.kernel.org, linux-phy@lists.infradead.org,
linux-clk@vger.kernel.org, Cody Kang <codykang.hk@gmail.com>
Subject: [PATCH v2 05/17] phy: spacemit: add Innosilicon DP TX PHY driver
Date: Sun, 09 Aug 2026 21:14:14 +0800 [thread overview]
Message-ID: <20260809-k3-display-v2-5-327d7910bf71@gmail.com> (raw)
In-Reply-To: <20260809-k3-display-v2-0-327d7910bf71@gmail.com>
From: Cody Kang <codykang.hk@gmail.com>
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 <codykang.hk@gmail.com>
---
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 <codykang.hk@gmail.com>");
+MODULE_LICENSE("GPL");
--
2.43.0
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
WARNING: multiple messages have this Message-ID (diff)
From: Cody Kang via B4 Relay <devnull+codykang.hk.gmail.com@kernel.org>
To: "David Airlie" <airlied@gmail.com>,
"Simona Vetter" <simona@ffwll.ch>,
"Maarten Lankhorst" <maarten.lankhorst@linux.intel.com>,
"Maxime Ripard" <mripard@kernel.org>,
"Thomas Zimmermann" <tzimmermann@suse.de>,
"Rob Herring" <robh@kernel.org>,
"Krzysztof Kozlowski" <krzk+dt@kernel.org>,
"Conor Dooley" <conor+dt@kernel.org>,
"Yixun Lan" <dlan@kernel.org>, "Vinod Koul" <vkoul@kernel.org>,
"Neil Armstrong" <neil.armstrong@linaro.org>,
"Haylen Chu" <heylenay@4d2.org>,
"Michael Turquette" <mturquette@baylibre.com>,
"Stephen Boyd" <sboyd@kernel.org>,
"Brian Masney" <bmasney@redhat.com>,
"Philipp Zabel" <p.zabel@pengutronix.de>,
"Paul Walmsley" <pjw@kernel.org>,
"Palmer Dabbelt" <palmer@dabbelt.com>,
"Albert Ou" <aou@eecs.berkeley.edu>,
"Alexandre Ghiti" <alex@ghiti.fr>, "Yao Zi" <me@ziyao.cc>,
"Uwe Kleine-König" <u.kleine-koenig@baylibre.com>,
"Guodong Xu" <docular.xu@gmail.com>
Cc: dri-devel@lists.freedesktop.org, linux-riscv@lists.infradead.org,
devicetree@vger.kernel.org, spacemit@lists.linux.dev,
linux-kernel@vger.kernel.org, linux-phy@lists.infradead.org,
linux-clk@vger.kernel.org, Cody Kang <codykang.hk@gmail.com>
Subject: [PATCH v2 05/17] phy: spacemit: add Innosilicon DP TX PHY driver
Date: Sun, 09 Aug 2026 21:14:14 +0800 [thread overview]
Message-ID: <20260809-k3-display-v2-5-327d7910bf71@gmail.com> (raw)
In-Reply-To: <20260809-k3-display-v2-0-327d7910bf71@gmail.com>
From: Cody Kang <codykang.hk@gmail.com>
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 <codykang.hk@gmail.com>
---
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 <codykang.hk@gmail.com>");
+MODULE_LICENSE("GPL");
--
2.43.0
_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv
WARNING: multiple messages have this Message-ID (diff)
From: Cody Kang <codykang.hk@gmail.com>
To: "David Airlie" <airlied@gmail.com>,
"Simona Vetter" <simona@ffwll.ch>,
"Maarten Lankhorst" <maarten.lankhorst@linux.intel.com>,
"Maxime Ripard" <mripard@kernel.org>,
"Thomas Zimmermann" <tzimmermann@suse.de>,
"Rob Herring" <robh@kernel.org>,
"Krzysztof Kozlowski" <krzk+dt@kernel.org>,
"Conor Dooley" <conor+dt@kernel.org>,
"Yixun Lan" <dlan@kernel.org>, "Vinod Koul" <vkoul@kernel.org>,
"Neil Armstrong" <neil.armstrong@linaro.org>,
"Haylen Chu" <heylenay@4d2.org>,
"Michael Turquette" <mturquette@baylibre.com>,
"Stephen Boyd" <sboyd@kernel.org>,
"Brian Masney" <bmasney@redhat.com>,
"Philipp Zabel" <p.zabel@pengutronix.de>,
"Paul Walmsley" <pjw@kernel.org>,
"Palmer Dabbelt" <palmer@dabbelt.com>,
"Albert Ou" <aou@eecs.berkeley.edu>,
"Alexandre Ghiti" <alex@ghiti.fr>, "Yao Zi" <me@ziyao.cc>,
"Uwe Kleine-König" <u.kleine-koenig@baylibre.com>,
"Guodong Xu" <docular.xu@gmail.com>
Cc: dri-devel@lists.freedesktop.org, linux-riscv@lists.infradead.org,
devicetree@vger.kernel.org, spacemit@lists.linux.dev,
linux-kernel@vger.kernel.org, linux-phy@lists.infradead.org,
linux-clk@vger.kernel.org, Cody Kang <codykang.hk@gmail.com>
Subject: [PATCH v2 05/17] phy: spacemit: add Innosilicon DP TX PHY driver
Date: Sun, 09 Aug 2026 21:14:14 +0800 [thread overview]
Message-ID: <20260809-k3-display-v2-5-327d7910bf71@gmail.com> (raw)
In-Reply-To: <20260809-k3-display-v2-0-327d7910bf71@gmail.com>
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 <codykang.hk@gmail.com>
---
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 <codykang.hk@gmail.com>");
+MODULE_LICENSE("GPL");
--
2.43.0
next prev parent reply other threads:[~2026-08-09 13:15 UTC|newest]
Thread overview: 94+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-09 13:14 [PATCH v2 00/17] drm/spacemit: add SpacemiT K3 display support Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:14 ` [PATCH v2 01/17] dt-bindings: display: spacemit: add K3 Saturn DPU controller Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:14 ` [PATCH v2 02/17] dt-bindings: phy: add SpacemiT K3 Innosilicon DP PHY Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:14 ` [PATCH v2 03/17] dt-bindings: display: spacemit: add K3 Innosilicon DP/eDP controller Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:25 ` sashiko-bot
2026-08-09 13:25 ` sashiko-bot
2026-08-09 13:14 ` [PATCH v2 04/17] dt-bindings: soc: spacemit: allow eDP/DP PHY PLL pixel clocks on K3 APMU Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 20:57 ` Rob Herring (Arm)
2026-08-09 20:57 ` Rob Herring (Arm)
2026-08-09 20:57 ` Rob Herring (Arm)
2026-08-10 14:33 ` Rob Herring (Arm)
2026-08-10 14:33 ` Rob Herring (Arm)
2026-08-10 14:33 ` Rob Herring (Arm)
2026-08-09 13:14 ` Cody Kang via B4 Relay [this message]
2026-08-09 13:14 ` [PATCH v2 05/17] phy: spacemit: add Innosilicon DP TX PHY driver Cody Kang
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:26 ` sashiko-bot
2026-08-09 13:26 ` sashiko-bot
2026-08-09 13:14 ` [PATCH v2 06/17] clk: spacemit: k3: parent eDP/DP pixel clock to the PHY PLL Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:14 ` [PATCH v2 07/17] drm/spacemit: add Saturn DPU register model Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:14 ` [PATCH v2 08/17] drm/spacemit: add Saturn DPU core types, cmdlist and display MMU Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:32 ` sashiko-bot
2026-08-09 13:32 ` sashiko-bot
2026-08-09 13:14 ` [PATCH v2 09/17] drm/spacemit: add Saturn DPU hardware backend Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:31 ` sashiko-bot
2026-08-09 13:31 ` sashiko-bot
2026-08-09 13:14 ` [PATCH v2 10/17] drm/spacemit: add Saturn DPU KMS pipeline Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:35 ` sashiko-bot
2026-08-09 13:35 ` sashiko-bot
2026-08-09 13:14 ` [PATCH v2 11/17] drm/spacemit: add Saturn DPU DRM device driver Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:35 ` sashiko-bot
2026-08-09 13:35 ` sashiko-bot
2026-08-09 13:14 ` [PATCH v2 12/17] drm/spacemit: add Innosilicon DP/eDP controller bridge driver Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:36 ` sashiko-bot
2026-08-09 13:36 ` sashiko-bot
2026-08-09 13:14 ` [PATCH v2 13/17] MAINTAINERS: add SpacemiT K3 display driver entry Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:14 ` [PATCH v2 14/17] riscv: dts: spacemit: k3: add display nodes Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:14 ` [PATCH v2 15/17] riscv: dts: spacemit: k3-pico-itx: enable the DisplayPort output Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:30 ` sashiko-bot
2026-08-09 13:30 ` sashiko-bot
2026-08-09 13:14 ` [PATCH v2 16/17] riscv: dts: spacemit: k3-com260-ifx: " Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:14 ` [PATCH v2 17/17] riscv: defconfig: spacemit: k3: enable display driver Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang
2026-08-09 13:14 ` Cody Kang via B4 Relay
2026-08-09 13:14 ` Cody Kang via B4 Relay
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260809-k3-display-v2-5-327d7910bf71@gmail.com \
--to=devnull+codykang.hk.gmail.com@kernel.org \
--cc=airlied@gmail.com \
--cc=alex@ghiti.fr \
--cc=aou@eecs.berkeley.edu \
--cc=bmasney@redhat.com \
--cc=codykang.hk@gmail.com \
--cc=conor+dt@kernel.org \
--cc=devicetree@vger.kernel.org \
--cc=dlan@kernel.org \
--cc=docular.xu@gmail.com \
--cc=dri-devel@lists.freedesktop.org \
--cc=heylenay@4d2.org \
--cc=krzk+dt@kernel.org \
--cc=linux-clk@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-phy@lists.infradead.org \
--cc=linux-riscv@lists.infradead.org \
--cc=maarten.lankhorst@linux.intel.com \
--cc=me@ziyao.cc \
--cc=mripard@kernel.org \
--cc=mturquette@baylibre.com \
--cc=neil.armstrong@linaro.org \
--cc=p.zabel@pengutronix.de \
--cc=palmer@dabbelt.com \
--cc=pjw@kernel.org \
--cc=robh@kernel.org \
--cc=sboyd@kernel.org \
--cc=simona@ffwll.ch \
--cc=spacemit@lists.linux.dev \
--cc=tzimmermann@suse.de \
--cc=u.kleine-koenig@baylibre.com \
--cc=vkoul@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.