summaryrefslogtreecommitdiffstats
path: root/drivers/phy/spacemit
diff options
context:
space:
mode:
authorLinus Torvalds <torvalds@linux-foundation.org>2026-10-02 12:17:24 -0700
committerLinus Torvalds <torvalds@linux-foundation.org>2026-10-02 12:17:24 -0700
commit3f1fe48a36b0b6722dc3fd421d93512bac138e9a (patch)
treeb767d7f6e26bc64334d3f14f8422d187239dc45d /drivers/phy/spacemit
downloadlinux-stable-3f1fe48a36b0b6722dc3fd421d93512bac138e9a.tar.gz
linux-stable-3f1fe48a36b0b6722dc3fd421d93512bac138e9a.zip
Merge tag 'io_uring-7.3-20261002' of git://git.kernel.org/pub/scm/linux/kernel/git/axboe/linuxgrafted
Pull io_uring fixes from Jens Axboe: - Fix a task_work add use-after-free with SQPOLL. The sqpoll thread could pop and complete the last request while io_req_normal_work_add() was still looking at them after the mpscq push. Use the same approach as DEFER_TASKRUN to protect from that, holding an RCU read lock across the add, and have exit wait for an RCU grace period for SQPOLL rings as well. - CQE32 ring fixes: correct the free entry check for 32b CQEs, zero the big_cqe for aux CQEs, and only post the dummy skip CQE on CQE_MIXED rings - Mark the source filter table as COW when cloning bpf filters, so registering another filter on the source doesn't modify the shared table in place - Initialize the task context before running the BPF loop - Requeue zcrx multishot receives stopped by a local resource - End a TX_TIMESTAMP multishot cmd when the CQ is full (lollipopkit) * tag 'io_uring-7.3-20261002' of git://git.kernel.org/pub/scm/linux/kernel/git/axboe/linux: io_uring: fix task_work add use-after-free with SQPOLL io_uring/cmd_net: end TX_TIMESTAMP multishot when the CQ is full io_uring/zcrx: requeue multishot receives stopped by a local resource io_uring: initialize task context before running the BPF loop io_uring: zero big_cqe for aux CQEs on CQE32 rings io_uring: fix free entry check for 32b CQEs on CQE32 rings io_uring: only post the dummy skip CQE on CQE_MIXED rings io_uring/bpf_filter: mark source as COW when cloning filters
Diffstat (limited to 'drivers/phy/spacemit')
-rw-r--r--drivers/phy/spacemit/Kconfig41
-rw-r--r--drivers/phy/spacemit/Makefile5
-rw-r--r--drivers/phy/spacemit/phy-k1-pcie.c670
-rw-r--r--drivers/phy/spacemit/phy-k1-usb2.c237
-rw-r--r--drivers/phy/spacemit/phy-k3-combo.c252
-rw-r--r--drivers/phy/spacemit/phy-k3-common.c391
-rw-r--r--drivers/phy/spacemit/phy-k3-common.h27
7 files changed, 1623 insertions, 0 deletions
diff --git a/drivers/phy/spacemit/Kconfig b/drivers/phy/spacemit/Kconfig
new file mode 100644
index 000000000..5fdf18fce
--- /dev/null
+++ b/drivers/phy/spacemit/Kconfig
@@ -0,0 +1,41 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Phy drivers for SpacemiT platforms
+#
+config PHY_SPACEMIT_K1_PCIE
+ tristate "PCIe and combo PHY driver for the SpacemiT K1 SoC"
+ depends on ARCH_SPACEMIT || COMPILE_TEST
+ depends on COMMON_CLK
+ depends on HAS_IOMEM
+ depends on OF
+ select GENERIC_PHY
+ default ARCH_SPACEMIT
+ help
+ Enable support for the PCIe and USB 3 combo PHY and two
+ PCIe-only PHYs used in the SpacemiT K1 SoC.
+
+config PHY_SPACEMIT_K1_USB2
+ tristate "SpacemiT K1 USB 2.0 PHY support"
+ depends on (ARCH_SPACEMIT || COMPILE_TEST) && OF
+ depends on COMMON_CLK
+ depends on USB_COMMON
+ select GENERIC_PHY
+ 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_COMMON_OPS
+ tristate
+ select MFD_SYSCON
+ select GENERIC_PHY
+
+config PHY_SPACEMIT_K3_COMBO_PHY
+ tristate "SpacemiT K3 USB3/PCIe PHY support"
+ depends on (ARCH_SPACEMIT || COMPILE_TEST) && OF
+ depends on COMMON_CLK
+ select PHY_SPACEMIT_K3_COMMON_OPS
+ help
+ Enable this to support K3 USB3/PCIe combo PHY driver. This
+ driver takes care of enabling and clock setup and will be used
+ by K3 dwc3 driver.
+ If unsure, say N.
diff --git a/drivers/phy/spacemit/Makefile b/drivers/phy/spacemit/Makefile
new file mode 100644
index 000000000..f679c1851
--- /dev/null
+++ b/drivers/phy/spacemit/Makefile
@@ -0,0 +1,5 @@
+# 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_COMBO_PHY) += phy-k3-combo.o
+obj-$(CONFIG_PHY_SPACEMIT_K3_COMMON_OPS) += phy-k3-common.o
diff --git a/drivers/phy/spacemit/phy-k1-pcie.c b/drivers/phy/spacemit/phy-k1-pcie.c
new file mode 100644
index 000000000..75477bea7
--- /dev/null
+++ b/drivers/phy/spacemit/phy-k1-pcie.c
@@ -0,0 +1,670 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * SpacemiT K1 PCIe and PCIe/USB 3 combo PHY driver
+ *
+ * Copyright (C) 2025 by RISCstar Solutions Corporation. All rights reserved.
+ */
+
+#include <linux/bitfield.h>
+#include <linux/clk.h>
+#include <linux/clk-provider.h>
+#include <linux/iopoll.h>
+#include <linux/kernel.h>
+#include <linux/mfd/syscon.h>
+#include <linux/module.h>
+#include <linux/phy/phy.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+#include <linux/reset.h>
+
+#include <dt-bindings/phy/phy.h>
+
+/*
+ * Three PCIe ports are supported in the SpacemiT K1 SoC, and this driver
+ * supports their PHYs.
+ *
+ * The PHY for PCIe port A is different from the PHYs for ports B and C:
+ * - It has one lane, while ports B and C have two
+ * - It is a combo PHY can be used for PCIe or USB 3
+ * - It can automatically calibrate PCIe TX and RX termination settings
+ *
+ * The PHY functionality for PCIe ports B and C is identical:
+ * - They have two PCIe lanes (but can be restricted to 1 via device tree)
+ * - They are used for PCIe only
+ * - They are configured using TX and RX values computed for port A
+ *
+ * A given board is designed to use the combo PHY for either PCIe or USB 3.
+ * Whether the combo PHY is configured for PCIe or USB 3 is specified in
+ * device tree using a phandle plus an argument. The argument indicates
+ * the type (either PHY_TYPE_PCIE or PHY_TYPE_USB3).
+ *
+ * Each PHY has a reset that it gets and deasserts during initialization.
+ * Each depends also on other clocks and resets provided by the controller
+ * hardware (PCIe or USB) it is associated with. The controller drivers
+ * are required to enable any clocks and de-assert any resets that affect
+ * PHY operation. In addition each PHY implements an internal PLL, driven
+ * by an external (24 MHz) oscillator.
+ *
+ * PCIe PHYs must be programmed with RX and TX calibration values. The
+ * combo PHY is the only one that can determine these values. They are
+ * determined by temporarily enabling the combo PHY in PCIe mode at probe
+ * time (if necessary). This calibration only needs to be done once, and
+ * when it has completed the TX and RX values are saved.
+ *
+ * To allow the combo PHY to be enabled for calibration, the resets and
+ * clocks it uses in PCIe mode must be supplied.
+ */
+
+struct k1_pcie_phy {
+ struct device *dev; /* PHY provider device */
+ struct phy *phy;
+ void __iomem *regs;
+ u32 pcie_lanes; /* Max (1 or 2) unless limited by DT */
+ struct clk *pll;
+ struct clk_hw pll_hw; /* Private PLL clock */
+
+ /* The remaining fields are only used for the combo PHY */
+ u32 type; /* PHY_TYPE_PCIE or PHY_TYPE_USB3 */
+ struct regmap *pmu; /* MMIO regmap (no errors) */
+};
+
+#define CALIBRATION_TIMEOUT 500000 /* For combo PHY (usec) */
+#define PLL_TIMEOUT 500000 /* For PHY PLL lock (usec) */
+#define POLL_DELAY 500 /* Time between polls (usec) */
+
+/* Selecting the combo PHY operating mode requires APMU regmap access */
+#define SYSCON_APMU "spacemit,apmu"
+
+/* PMU space, for selecting between PCIe and USB 3 mode (combo PHY only) */
+
+#define PMUA_USB_PHY_CTRL0 0x0110
+#define COMBO_PHY_SEL BIT(3) /* 0: PCIe; 1: USB 3 */
+
+#define PCIE_CLK_RES_CTRL 0x03cc
+#define PCIE_APP_HOLD_PHY_RST BIT(30)
+
+/* PHY register space */
+
+/* Offset between lane 0 and lane 1 registers when there are two */
+#define PHY_LANE_OFFSET 0x0400
+
+/* PHY PLL configuration */
+#define PCIE_PU_ADDR_CLK_CFG 0x0008
+#define PLL_READY BIT(0) /* read-only */
+#define CFG_INTERNAL_TIMER_ADJ GENMASK(10, 7)
+#define TIMER_ADJ_USB 0x2
+#define TIMER_ADJ_PCIE 0x6
+#define CFG_SW_PHY_INIT_DONE BIT(11) /* We set after PLL config */
+
+#define PCIE_RC_DONE_STATUS 0x0018
+#define CFG_FORCE_RCV_RETRY BIT(10) /* Used for PCIe */
+
+/* PCIe PHY lane calibration; assumes 24MHz input clock */
+#define PCIE_RC_CAL_REG2 0x0020
+#define RC_CAL_TOGGLE BIT(22)
+#define CLKSEL GENMASK(31, 29)
+#define CLKSEL_24M 0x3
+
+/* Additional PHY PLL configuration (USB 3 and PCIe) */
+#define PCIE_PU_PLL_1 0x0048
+#define REF_100_WSSC BIT(12) /* 1: input is 100MHz, SSC */
+#define FREF_SEL GENMASK(15, 13)
+#define FREF_24M 0x1
+#define SSC_DEP_SEL GENMASK(19, 16)
+#define SSC_DEP_NONE 0x0
+#define SSC_DEP_5000PPM 0xa
+
+/* PCIe PHY configuration */
+#define PCIE_PU_PLL_2 0x004c
+#define GEN_REF100 BIT(4) /* 1: generate 100MHz clk */
+
+#define PCIE_RX_REG1 0x0050
+#define EN_RTERM BIT(3)
+#define AFE_RTERM_REG GENMASK(11, 8)
+
+#define PCIE_RX_REG2 0x0054
+#define RX_RTERM_SEL BIT(5) /* 0: use AFE_RTERM_REG value */
+
+#define PCIE_LTSSM_DIS_ENTRY 0x005c
+#define CFG_REFCLK_MODE GENMASK(9, 8)
+#define RFCLK_MODE_DRIVER 0x1
+#define OVRD_REFCLK_MODE BIT(10) /* 1: use CFG_RFCLK_MODE */
+
+#define PCIE_TX_REG1 0x0064
+#define TX_RTERM_REG GENMASK(15, 12)
+#define TX_RTERM_SEL BIT(25) /* 1: use TX_RTERM_REG */
+
+/* Zeroed for the combo PHY operating in USB mode */
+#define USB3_TEST_CTRL 0x0068
+
+/* PHY calibration values, determined by the combo PHY at probe time */
+#define PCIE_RCAL_RESULT 0x0084 /* Port A PHY only */
+#define RTERM_VALUE_RX GENMASK(3, 0)
+#define RTERM_VALUE_TX GENMASK(7, 4)
+#define R_TUNE_DONE BIT(10)
+
+static u32 k1_phy_rterm = ~0; /* Invalid initial value */
+
+/* Save the RX and TX receiver termination values */
+static void k1_phy_rterm_set(u32 val)
+{
+ k1_phy_rterm = val & (RTERM_VALUE_RX | RTERM_VALUE_TX);
+}
+
+static bool k1_phy_rterm_valid(void)
+{
+ /* Valid if no bits outside those we care about are set */
+ return !(k1_phy_rterm & ~(RTERM_VALUE_RX | RTERM_VALUE_TX));
+}
+
+static u32 k1_phy_rterm_rx(void)
+{
+ return FIELD_GET(RTERM_VALUE_RX, k1_phy_rterm);
+}
+
+static u32 k1_phy_rterm_tx(void)
+{
+ return FIELD_GET(RTERM_VALUE_TX, k1_phy_rterm);
+}
+
+/* Only the combo PHY has a PMU pointer defined */
+static bool k1_phy_port_a(struct k1_pcie_phy *k1_phy)
+{
+ return !!k1_phy->pmu;
+}
+
+/* The PLL clocks are driven by the external oscillator */
+static const struct clk_parent_data k1_pcie_phy_data[] = {
+ { .fw_name = "refclk", },
+};
+
+static struct k1_pcie_phy *clk_hw_to_k1_phy(struct clk_hw *clk_hw)
+{
+ return container_of(clk_hw, struct k1_pcie_phy, pll_hw);
+}
+
+/* USB mode only works on the combo PHY, which has only one lane */
+static void k1_pcie_phy_pll_prepare_usb(struct k1_pcie_phy *k1_phy)
+{
+ void __iomem *regs = k1_phy->regs;
+ u32 val;
+
+ val = readl(regs + PCIE_PU_ADDR_CLK_CFG);
+ val &= ~CFG_INTERNAL_TIMER_ADJ;
+ val |= FIELD_PREP(CFG_INTERNAL_TIMER_ADJ, TIMER_ADJ_USB);
+ writel(val, regs + PCIE_PU_ADDR_CLK_CFG);
+
+ val = readl(regs + PCIE_PU_PLL_1);
+ val &= ~SSC_DEP_SEL;
+ val |= FIELD_PREP(SSC_DEP_SEL, SSC_DEP_5000PPM);
+ writel(val, regs + PCIE_PU_PLL_1);
+}
+
+/* Perform PCIe-specific register updates before starting the PLL clock */
+static void k1_pcie_phy_pll_prepare_pcie(struct k1_pcie_phy *k1_phy)
+{
+ void __iomem *regs = k1_phy->regs;
+ u32 val;
+ u32 i;
+
+ for (i = 0; i < k1_phy->pcie_lanes; i++) {
+ val = readl(regs + PCIE_PU_ADDR_CLK_CFG);
+ val &= ~CFG_INTERNAL_TIMER_ADJ;
+ val |= FIELD_PREP(CFG_INTERNAL_TIMER_ADJ, TIMER_ADJ_PCIE);
+ writel(val, regs + PCIE_PU_ADDR_CLK_CFG);
+
+ regs += PHY_LANE_OFFSET; /* Next lane */
+ }
+
+ regs = k1_phy->regs;
+ val = readl(regs + PCIE_RC_DONE_STATUS);
+ val |= CFG_FORCE_RCV_RETRY;
+ writel(val, regs + PCIE_RC_DONE_STATUS);
+
+ val = readl(regs + PCIE_PU_PLL_1);
+ val &= ~SSC_DEP_SEL;
+ val |= FIELD_PREP(SSC_DEP_SEL, SSC_DEP_NONE);
+ writel(val, regs + PCIE_PU_PLL_1);
+
+ val = readl(regs + PCIE_PU_PLL_2);
+ val |= GEN_REF100; /* Enable 100 MHz PLL output clock */
+ writel(val, regs + PCIE_PU_PLL_2);
+}
+
+static int k1_pcie_phy_pll_prepare(struct clk_hw *clk_hw)
+{
+ struct k1_pcie_phy *k1_phy = clk_hw_to_k1_phy(clk_hw);
+ void __iomem *regs = k1_phy->regs;
+ u32 val;
+ u32 i;
+
+ if (k1_phy_port_a(k1_phy) && k1_phy->type == PHY_TYPE_USB3)
+ k1_pcie_phy_pll_prepare_usb(k1_phy);
+ else
+ k1_pcie_phy_pll_prepare_pcie(k1_phy);
+
+ /*
+ * Disable 100 MHz input reference with spread-spectrum
+ * clocking and select the 24 MHz clock input frequency
+ */
+ val = readl(regs + PCIE_PU_PLL_1);
+ val &= ~REF_100_WSSC;
+ val &= ~FREF_SEL;
+ val |= FIELD_PREP(FREF_SEL, FREF_24M);
+ writel(val, regs + PCIE_PU_PLL_1);
+
+ /* Mark PLL configuration done on all lanes */
+ for (i = 0; i < k1_phy->pcie_lanes; i++) {
+ val = readl(regs + PCIE_PU_ADDR_CLK_CFG);
+ val |= CFG_SW_PHY_INIT_DONE;
+ writel(val, regs + PCIE_PU_ADDR_CLK_CFG);
+
+ regs += PHY_LANE_OFFSET; /* Next lane */
+ }
+
+ /*
+ * Wait for indication the PHY PLL is locked. Lanes for ports
+ * B and C share a PLL, so it's enough to sample just lane 0.
+ */
+ return readl_poll_timeout(k1_phy->regs + PCIE_PU_ADDR_CLK_CFG,
+ val, val & PLL_READY,
+ POLL_DELAY, PLL_TIMEOUT);
+}
+
+/* Prepare implies enable, and once enabled, it's always on */
+static const struct clk_ops k1_pcie_phy_pll_ops = {
+ .prepare = k1_pcie_phy_pll_prepare,
+};
+
+/* We represent the PHY PLL as a private clock */
+static int k1_pcie_phy_pll_setup(struct k1_pcie_phy *k1_phy)
+{
+ struct clk_hw *hw = &k1_phy->pll_hw;
+ struct device *dev = k1_phy->dev;
+ struct clk_init_data init = { };
+ char *name;
+ int ret;
+
+ name = kasprintf(GFP_KERNEL, "pcie%u_phy_pll", k1_phy->phy->id);
+ if (!name)
+ return -ENOMEM;
+
+ init.name = name;
+ init.ops = &k1_pcie_phy_pll_ops;
+ init.parent_data = k1_pcie_phy_data;
+ init.num_parents = ARRAY_SIZE(k1_pcie_phy_data);
+
+ hw->init = &init;
+
+ ret = devm_clk_hw_register(dev, hw);
+
+ kfree(name); /* __clk_register() duplicates the name we provide */
+
+ if (ret)
+ return ret;
+
+ k1_phy->pll = devm_clk_hw_get_clk(dev, hw, "pll");
+ if (IS_ERR(k1_phy->pll))
+ return PTR_ERR(k1_phy->pll);
+
+ return 0;
+}
+
+/* Select PCIe or USB 3 mode for the combo PHY. */
+static void k1_combo_phy_sel(struct k1_pcie_phy *k1_phy, bool usb)
+{
+ struct regmap *pmu = k1_phy->pmu;
+
+ /* Only change it if it's not already in the desired state */
+ if (!regmap_test_bits(pmu, PMUA_USB_PHY_CTRL0, COMBO_PHY_SEL) == usb)
+ regmap_assign_bits(pmu, PMUA_USB_PHY_CTRL0, COMBO_PHY_SEL, usb);
+}
+
+static void k1_pcie_phy_init_pcie(struct k1_pcie_phy *k1_phy)
+{
+ u32 rx_rterm = k1_phy_rterm_rx();
+ u32 tx_rterm = k1_phy_rterm_tx();
+ void __iomem *regs;
+ u32 val;
+ int i;
+
+ /* For the combo PHY, set PHY to PCIe mode */
+ if (k1_phy_port_a(k1_phy))
+ k1_combo_phy_sel(k1_phy, false);
+
+ regs = k1_phy->regs;
+ for (i = 0; i < k1_phy->pcie_lanes; i++) {
+ val = readl(regs + PCIE_RX_REG1);
+
+ /* Set RX analog front-end receiver termination value */
+ val &= ~AFE_RTERM_REG;
+ val |= FIELD_PREP(AFE_RTERM_REG, rx_rterm);
+
+ /* And enable refclock receiver termination */
+ val |= EN_RTERM;
+ writel(val, regs + PCIE_RX_REG1);
+
+ val = readl(regs + PCIE_RX_REG2);
+ /* Use PCIE_RX_REG1 AFE_RTERM_REG value */
+ val &= ~RX_RTERM_SEL;
+ writel(val, regs + PCIE_RX_REG2);
+
+ val = readl(regs + PCIE_TX_REG1);
+
+ /* Set TX driver termination value */
+ val &= ~TX_RTERM_REG;
+ val |= FIELD_PREP(TX_RTERM_REG, tx_rterm);
+
+ /* Use PCIE_TX_REG1 TX_RTERM_REG value */
+ val |= TX_RTERM_SEL;
+ writel(val, regs + PCIE_TX_REG1);
+
+ /* Set the input clock to 24 MHz, and clear RC_CAL_TOGGLE */
+ val = readl(regs + PCIE_RC_CAL_REG2);
+ val &= CLKSEL;
+ val |= FIELD_PREP(CLKSEL, CLKSEL_24M);
+ val &= ~RC_CAL_TOGGLE;
+ writel(val, regs + PCIE_RC_CAL_REG2);
+
+ /* Now trigger recalibration by setting RC_CAL_TOGGLE again */
+ val |= RC_CAL_TOGGLE;
+ writel(val, regs + PCIE_RC_CAL_REG2);
+
+ val = readl(regs + PCIE_LTSSM_DIS_ENTRY);
+ /* Override the reference clock; set to refclk driver mode */
+ val |= OVRD_REFCLK_MODE;
+ val &= ~CFG_REFCLK_MODE;
+ val |= FIELD_PREP(CFG_REFCLK_MODE, RFCLK_MODE_DRIVER);
+ writel(val, regs + PCIE_LTSSM_DIS_ENTRY);
+
+ regs += PHY_LANE_OFFSET; /* Next lane */
+ }
+}
+
+/* Only called for combo PHY */
+static void k1_pcie_phy_init_usb(struct k1_pcie_phy *k1_phy)
+{
+ k1_combo_phy_sel(k1_phy, true);
+
+ /* We're not doing any testing */
+ writel(0, k1_phy->regs + USB3_TEST_CTRL);
+}
+
+static int k1_pcie_phy_init(struct phy *phy)
+{
+ struct k1_pcie_phy *k1_phy = phy_get_drvdata(phy);
+
+ /* Note: port type is only valid for port A (both checks needed) */
+ if (k1_phy_port_a(k1_phy) && k1_phy->type == PHY_TYPE_USB3)
+ k1_pcie_phy_init_usb(k1_phy);
+ else
+ k1_pcie_phy_init_pcie(k1_phy);
+
+
+ return clk_prepare_enable(k1_phy->pll);
+}
+
+static int k1_pcie_phy_exit(struct phy *phy)
+{
+ struct k1_pcie_phy *k1_phy = phy_get_drvdata(phy);
+
+ clk_disable_unprepare(k1_phy->pll);
+
+ return 0;
+}
+
+static const struct phy_ops k1_pcie_phy_ops = {
+ .init = k1_pcie_phy_init,
+ .exit = k1_pcie_phy_exit,
+ .owner = THIS_MODULE,
+};
+
+/*
+ * Get values needed for calibrating PHYs operating in PCIe mode. Only
+ * the combo PHY is able to do this, and its calibration values are used
+ * for configuring all PCIe PHYs.
+ *
+ * We always need to de-assert the "global" reset on the combo PHY,
+ * because the USB driver depends on it. If used for PCIe, that driver
+ * will (also) de-assert this, but by leaving it de-asserted for the
+ * combo PHY, the USB driver doesn't have to do this. Note: although
+ * SpacemiT refers to this as the global reset, we name the "phy" reset.
+ *
+ * In addition, we guarantee the APP_HOLD_PHY_RESET bit is clear for the
+ * combo PHY, so the USB driver doesn't have to manage that either. The
+ * PCIe driver is free to change this bit for normal operation.
+ *
+ * Calibration only needs to be done once. It's possible calibration has
+ * already completed (e.g., it might have happened in the boot loader, or
+ * -EPROBE_DEFER might result in this function being called again). So we
+ * check that early too, to avoid doing it more than once.
+ *
+ * Otherwise we temporarily power up the PHY using the PCIe app clocks
+ * and resets, wait for the hardware to indicate calibration is done,
+ * grab the value, then shut the PHY down again.
+ */
+static int k1_pcie_combo_phy_calibrate(struct k1_pcie_phy *k1_phy)
+{
+ struct reset_control_bulk_data resets[] = {
+ { .id = "dbi", },
+ { .id = "mstr", },
+ { .id = "slv", },
+ };
+ struct clk_bulk_data clocks[] = {
+ { .id = "dbi", },
+ { .id = "mstr", },
+ { .id = "slv", },
+ };
+ struct device *dev = k1_phy->dev;
+ int ret = 0;
+ int val;
+
+ /* Nothing to do if we already set the receiver termination value */
+ if (k1_phy_rterm_valid())
+ return 0;
+
+ /*
+ * We also guarantee the APP_HOLD_PHY_RESET bit is clear. We can
+ * leave this bit clear even if an error happens below.
+ */
+ regmap_assign_bits(k1_phy->pmu, PCIE_CLK_RES_CTRL,
+ PCIE_APP_HOLD_PHY_RST, false);
+
+ /* If the calibration already completed (e.g. by U-Boot), we're done */
+ val = readl(k1_phy->regs + PCIE_RCAL_RESULT);
+ if (val & R_TUNE_DONE)
+ goto out_tune_done;
+
+ /* Put the PHY into PCIe mode */
+ k1_combo_phy_sel(k1_phy, false);
+
+ /* Get and enable the PCIe app clocks */
+ ret = clk_bulk_get(dev, ARRAY_SIZE(clocks), clocks);
+ if (ret < 0)
+ goto out_tune_done;
+ ret = clk_bulk_prepare_enable(ARRAY_SIZE(clocks), clocks);
+ if (ret)
+ goto out_put_clocks;
+
+ /* Get the PCIe application resets (not the PHY reset) */
+ ret = reset_control_bulk_get_shared(dev, ARRAY_SIZE(resets), resets);
+ if (ret)
+ goto out_disable_clocks;
+
+ /* De-assert the PCIe application resets */
+ ret = reset_control_bulk_deassert(ARRAY_SIZE(resets), resets);
+ if (ret)
+ goto out_put_resets;
+
+ /*
+ * This is the core activity here. Wait for the hardware to
+ * signal that it has completed calibration/tuning. Once it
+ * has, the register value will contain the values we'll
+ * use to configure PCIe PHYs.
+ */
+ ret = readl_poll_timeout(k1_phy->regs + PCIE_RCAL_RESULT,
+ val, val & R_TUNE_DONE,
+ POLL_DELAY, CALIBRATION_TIMEOUT);
+
+ /* Clean up. We're done with the resets and clocks */
+ reset_control_bulk_assert(ARRAY_SIZE(resets), resets);
+out_put_resets:
+ reset_control_bulk_put(ARRAY_SIZE(resets), resets);
+out_disable_clocks:
+ clk_bulk_disable_unprepare(ARRAY_SIZE(clocks), clocks);
+out_put_clocks:
+ clk_bulk_put(ARRAY_SIZE(clocks), clocks);
+out_tune_done:
+ /* If we got the value without timing out, set k1_phy_rterm */
+ if (!ret)
+ k1_phy_rterm_set(val);
+
+ return ret;
+}
+
+static struct phy *
+k1_pcie_combo_phy_xlate(struct device *dev, const struct of_phandle_args *args)
+{
+ struct k1_pcie_phy *k1_phy = dev_get_drvdata(dev);
+ u32 type;
+
+ /* The argument specifying the PHY mode is required */
+ if (args->args_count != 1)
+ return ERR_PTR(-EINVAL);
+
+ /* We only support PCIe and USB 3 mode */
+ type = args->args[0];
+ if (type != PHY_TYPE_PCIE && type != PHY_TYPE_USB3)
+ return ERR_PTR(-EINVAL);
+
+ /* This PHY can only be used once */
+ if (k1_phy->type != PHY_NONE)
+ return ERR_PTR(-EBUSY);
+
+ k1_phy->type = type;
+
+ return k1_phy->phy;
+}
+
+/* Use the maximum number of PCIe lanes unless limited by device tree */
+static u32 k1_pcie_num_lanes(struct k1_pcie_phy *k1_phy, bool port_a)
+{
+ struct device *dev = k1_phy->dev;
+ u32 count = 0;
+ u32 max;
+ int ret;
+
+ ret = of_property_read_u32(dev_of_node(dev), "num-lanes", &count);
+ if (count == 1)
+ return 1;
+
+ if (count == 2 && !port_a)
+ return 2;
+
+ max = port_a ? 1 : 2;
+ if (ret != -EINVAL)
+ dev_warn(dev, "bad lane count %u for port; using %u\n",
+ count, max);
+
+ return max;
+}
+
+static int k1_pcie_combo_phy_probe(struct k1_pcie_phy *k1_phy)
+{
+ struct device *dev = k1_phy->dev;
+ struct regmap *regmap;
+ int ret;
+
+ /* Setting the PHY mode requires access to the PMU regmap */
+ regmap = syscon_regmap_lookup_by_phandle(dev_of_node(dev), SYSCON_APMU);
+ if (IS_ERR(regmap))
+ return dev_err_probe(dev, PTR_ERR(regmap), "failed to get PMU\n");
+ k1_phy->pmu = regmap;
+
+ ret = k1_pcie_combo_phy_calibrate(k1_phy);
+ if (ret)
+ return dev_err_probe(dev, ret, "calibration failed\n");
+
+ /* Needed by k1_pcie_combo_phy_xlate(), which also sets k1_phy->type */
+ dev_set_drvdata(dev, k1_phy);
+
+ return 0;
+}
+
+static int k1_pcie_phy_probe(struct platform_device *pdev)
+{
+ struct phy *(*xlate)(struct device *dev,
+ const struct of_phandle_args *args);
+ struct device *dev = &pdev->dev;
+ struct reset_control *phy_reset;
+ struct phy_provider *provider;
+ struct k1_pcie_phy *k1_phy;
+ bool probing_port_a;
+ int ret;
+
+ xlate = of_device_get_match_data(dev);
+ probing_port_a = xlate == k1_pcie_combo_phy_xlate;
+
+ /* Only the combo PHY can calibrate, so it must probe first */
+ if (!k1_phy_rterm_valid() && !probing_port_a)
+ return -EPROBE_DEFER;
+
+ k1_phy = devm_kzalloc(dev, sizeof(*k1_phy), GFP_KERNEL);
+ if (!k1_phy)
+ return -ENOMEM;
+ k1_phy->dev = dev;
+
+ k1_phy->regs = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(k1_phy->regs))
+ return dev_err_probe(dev, PTR_ERR(k1_phy->regs),
+ "error mapping registers\n");
+
+ /* De-assert the PHY (global) reset and leave it that way */
+ phy_reset = devm_reset_control_get_exclusive_deasserted(dev, "phy");
+ if (IS_ERR(phy_reset))
+ return PTR_ERR(phy_reset);
+
+ if (probing_port_a) {
+ ret = k1_pcie_combo_phy_probe(k1_phy);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "error probing combo phy\n");
+ }
+
+ k1_phy->pcie_lanes = k1_pcie_num_lanes(k1_phy, probing_port_a);
+
+ k1_phy->phy = devm_phy_create(dev, NULL, &k1_pcie_phy_ops);
+ if (IS_ERR(k1_phy->phy))
+ return dev_err_probe(dev, PTR_ERR(k1_phy->phy),
+ "error creating phy\n");
+ phy_set_drvdata(k1_phy->phy, k1_phy);
+
+ ret = k1_pcie_phy_pll_setup(k1_phy);
+ if (ret)
+ return dev_err_probe(dev, ret, "error initializing clock\n");
+
+ provider = devm_of_phy_provider_register(dev, xlate);
+ if (IS_ERR(provider))
+ return dev_err_probe(dev, PTR_ERR(provider),
+ "error registering provider\n");
+ return 0;
+}
+
+static const struct of_device_id k1_pcie_phy_of_match[] = {
+ { .compatible = "spacemit,k1-combo-phy", k1_pcie_combo_phy_xlate, },
+ { .compatible = "spacemit,k1-pcie-phy", of_phy_simple_xlate, },
+ { },
+};
+MODULE_DEVICE_TABLE(of, k1_pcie_phy_of_match);
+
+static struct platform_driver k1_pcie_phy_driver = {
+ .probe = k1_pcie_phy_probe,
+ .driver = {
+ .of_match_table = k1_pcie_phy_of_match,
+ .name = "spacemit-k1-pcie-phy",
+ }
+};
+module_platform_driver(k1_pcie_phy_driver);
+
+MODULE_DESCRIPTION("SpacemiT K1 PCIe and USB 3 PHY driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/phy/spacemit/phy-k1-usb2.c b/drivers/phy/spacemit/phy-k1-usb2.c
new file mode 100644
index 000000000..8df12eeb2
--- /dev/null
+++ b/drivers/phy/spacemit/phy-k1-usb2.c
@@ -0,0 +1,237 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * SpacemiT K1 USB 2.0 PHY driver
+ *
+ * Copyright (C) 2025 SpacemiT (Hangzhou) Technology Co. Ltd
+ * Copyright (C) 2025 Ze Huang <huang.ze@linux.dev>
+ */
+
+#include <linux/bitfield.h>
+#include <linux/clk.h>
+#include <linux/iopoll.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+#include <linux/usb/of.h>
+
+#define PHY_RST_MODE_CTRL 0x04
+#define PHY_PLL_RDY BIT(0)
+#define PHY_CLK_CDR_EN BIT(1)
+#define PHY_CLK_PLL_EN BIT(2)
+#define PHY_CLK_MAC_EN BIT(3)
+#define PHY_MAC_RSTN BIT(5)
+#define PHY_CDR_RSTN BIT(6)
+#define PHY_PLL_RSTN BIT(7)
+/*
+ * hs line state sel (Bit 13):
+ * - 1 (Default): Internal HS line state is set to 01 when usb_hs_tx_en is valid.
+ * - 0: Internal HS line state is always driven by usb_hs_lstate.
+ *
+ * fs line state sel (Bit 14):
+ * - 1 (Default): FS line state is determined by the output data
+ * (usb_fs_datain/b).
+ * - 0: FS line state is always determined by the input data (dmo/dpo).
+ */
+#define PHY_HS_LINE_TX_MODE BIT(13)
+#define PHY_FS_LINE_TX_MODE BIT(14)
+
+#define PHY_INIT_MODE_BITS (PHY_FS_LINE_TX_MODE | PHY_HS_LINE_TX_MODE)
+#define PHY_CLK_ENABLE_BITS (PHY_CLK_PLL_EN | PHY_CLK_CDR_EN | \
+ PHY_CLK_MAC_EN)
+#define PHY_DEASSERT_RST_BITS (PHY_PLL_RSTN | PHY_CDR_RSTN | \
+ PHY_MAC_RSTN)
+
+#define PHY_TX_HOST_CTRL 0x10
+#define PHY_HST_DISC_AUTO_CLR BIT(2) /* autoclear hs host disc when re-connect */
+
+#define PHY_HSTXP_HW_CTRL 0x34
+#define PHY_HSTXP_RSTN BIT(2) /* generate reset for clock hstxp */
+#define PHY_CLK_HSTXP_EN BIT(3) /* clock hstxp enable */
+#define PHY_HSTXP_MODE BIT(4) /* 0: force en_txp to be 1; 1: no force */
+
+#define PHY_K1_HS_HOST_DISC 0x40
+#define PHY_K1_HS_HOST_DISC_CLR BIT(0)
+
+#define PHY_K3_HS_HOST_DISC 0x20
+#define PHY_K3_HS_HOST_DISC_CLR BIT(8)
+
+#define PHY_PLL_DIV_CFG 0x98
+#define PHY_FDIV_FRACT_8_15 GENMASK(7, 0)
+#define PHY_FDIV_FRACT_16_19 GENMASK(11, 8)
+#define PHY_FDIV_FRACT_20_21 BIT(12) /* fdiv_reg<21>, <20>, bit21 == bit20 */
+/*
+ * freq_sel<1:0>
+ * if ref clk freq=24.0MHz-->freq_sel<2:0> == 3b'001, then internal divider value == 80
+ */
+#define PHY_FDIV_FRACT_0_1 GENMASK(14, 13)
+/*
+ * pll divider value selection
+ * 1: divider value will choose internal default value ,dependent on freq_sel<1:0>
+ * 0: divider value will be over ride by fdiv_reg<21:0>
+ */
+#define PHY_DIV_LOCAL_EN BIT(15)
+
+#define PHY_SEL_FREQ_24MHZ 0x01
+#define FDIV_REG_MASK (PHY_FDIV_FRACT_20_21 | PHY_FDIV_FRACT_16_19 | \
+ PHY_FDIV_FRACT_8_15)
+#define FDIV_REG_VAL 0x1ec4 /* 0x100 selects 24MHz, rest are default */
+
+#define K1_USB2PHY_RESET_TIME_MS 50
+
+struct spacemit_usb2phy {
+ struct phy *phy;
+ struct clk *clk;
+ struct regmap *regmap_base;
+};
+
+static const struct regmap_config phy_regmap_config = {
+ .reg_bits = 32,
+ .val_bits = 32,
+ .reg_stride = 4,
+ .max_register = 0x200,
+};
+
+static int spacemit_usb2phy_init(struct phy *phy)
+{
+ struct spacemit_usb2phy *sphy = phy_get_drvdata(phy);
+ struct regmap *map = sphy->regmap_base;
+ u32 val;
+ int ret;
+
+ ret = clk_enable(sphy->clk);
+ if (ret) {
+ dev_err(&phy->dev, "failed to enable clock\n");
+ return ret;
+ }
+
+ /*
+ * make sure the usb controller is not under reset process before
+ * any configuration
+ */
+ usleep_range(150, 200);
+
+ /* 24M ref clk */
+ val = FIELD_PREP(FDIV_REG_MASK, FDIV_REG_VAL) |
+ FIELD_PREP(PHY_FDIV_FRACT_0_1, PHY_SEL_FREQ_24MHZ) |
+ PHY_DIV_LOCAL_EN;
+ regmap_write(map, PHY_PLL_DIV_CFG, val);
+
+ ret = regmap_read_poll_timeout(map, PHY_RST_MODE_CTRL, val,
+ (val & PHY_PLL_RDY),
+ 500, K1_USB2PHY_RESET_TIME_MS * 1000);
+ if (ret) {
+ dev_err(&phy->dev, "wait PLLREADY timeout\n");
+ clk_disable(sphy->clk);
+ return ret;
+ }
+
+ /* release usb2 phy internal reset and enable clock gating */
+ val = (PHY_INIT_MODE_BITS | PHY_CLK_ENABLE_BITS | PHY_DEASSERT_RST_BITS);
+ regmap_write(map, PHY_RST_MODE_CTRL, val);
+
+ val = (PHY_HSTXP_RSTN | PHY_CLK_HSTXP_EN | PHY_HSTXP_MODE);
+ regmap_write(map, PHY_HSTXP_HW_CTRL, val);
+
+ /* auto clear host disc */
+ regmap_update_bits(map, PHY_TX_HOST_CTRL, PHY_HST_DISC_AUTO_CLR,
+ PHY_HST_DISC_AUTO_CLR);
+
+ return 0;
+}
+
+static int spacemit_usb2phy_exit(struct phy *phy)
+{
+ struct spacemit_usb2phy *sphy = phy_get_drvdata(phy);
+
+ clk_disable(sphy->clk);
+
+ return 0;
+}
+
+static int spacemit_k1_usb2phy_disconnect(struct phy *phy, int port)
+{
+ struct spacemit_usb2phy *sphy = phy_get_drvdata(phy);
+
+ regmap_update_bits(sphy->regmap_base, PHY_K1_HS_HOST_DISC,
+ PHY_K1_HS_HOST_DISC_CLR, PHY_K1_HS_HOST_DISC_CLR);
+
+ return 0;
+}
+
+static int spacemit_k3_usb2phy_disconnect(struct phy *phy, int port)
+{
+ struct spacemit_usb2phy *sphy = phy_get_drvdata(phy);
+
+ regmap_update_bits(sphy->regmap_base, PHY_K3_HS_HOST_DISC,
+ PHY_K3_HS_HOST_DISC_CLR, PHY_K3_HS_HOST_DISC_CLR);
+
+ return 0;
+}
+
+static const struct phy_ops spacemit_k1_usb2phy_ops = {
+ .init = spacemit_usb2phy_init,
+ .exit = spacemit_usb2phy_exit,
+ .disconnect = spacemit_k1_usb2phy_disconnect,
+ .owner = THIS_MODULE,
+};
+
+static const struct phy_ops spacemit_k3_usb2phy_ops = {
+ .init = spacemit_usb2phy_init,
+ .exit = spacemit_usb2phy_exit,
+ .disconnect = spacemit_k3_usb2phy_disconnect,
+ .owner = THIS_MODULE,
+};
+
+static int spacemit_usb2phy_probe(struct platform_device *pdev)
+{
+ struct phy_provider *phy_provider;
+ struct device *dev = &pdev->dev;
+ struct spacemit_usb2phy *sphy;
+ const struct phy_ops *ops;
+ void __iomem *base;
+
+ sphy = devm_kzalloc(dev, sizeof(*sphy), GFP_KERNEL);
+ if (!sphy)
+ return -ENOMEM;
+
+ ops = device_get_match_data(dev);
+
+ sphy->clk = devm_clk_get_prepared(&pdev->dev, NULL);
+ if (IS_ERR(sphy->clk))
+ return dev_err_probe(dev, PTR_ERR(sphy->clk), "Failed to get clock\n");
+
+ base = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(base))
+ return PTR_ERR(base);
+
+ sphy->regmap_base = devm_regmap_init_mmio(dev, base, &phy_regmap_config);
+ if (IS_ERR(sphy->regmap_base))
+ return dev_err_probe(dev, PTR_ERR(sphy->regmap_base), "Failed to init regmap\n");
+
+ sphy->phy = devm_phy_create(dev, NULL, ops);
+ if (IS_ERR(sphy->phy))
+ return dev_err_probe(dev, PTR_ERR(sphy->phy), "Failed to create phy\n");
+
+ phy_set_drvdata(sphy->phy, sphy);
+ phy_provider = devm_of_phy_provider_register(dev, of_phy_simple_xlate);
+
+ return PTR_ERR_OR_ZERO(phy_provider);
+}
+
+static const struct of_device_id spacemit_usb2phy_dt_match[] = {
+ { .compatible = "spacemit,k1-usb2-phy", .data = &spacemit_k1_usb2phy_ops },
+ { .compatible = "spacemit,k3-usb2-phy", .data = &spacemit_k3_usb2phy_ops },
+ { /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, spacemit_usb2phy_dt_match);
+
+static struct platform_driver spacemit_usb2_phy_driver = {
+ .probe = spacemit_usb2phy_probe,
+ .driver = {
+ .name = "spacemit-usb2-phy",
+ .of_match_table = spacemit_usb2phy_dt_match,
+ },
+};
+module_platform_driver(spacemit_usb2_phy_driver);
+
+MODULE_DESCRIPTION("Spacemit USB 2.0 PHY driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/phy/spacemit/phy-k3-combo.c b/drivers/phy/spacemit/phy-k3-combo.c
new file mode 100644
index 000000000..0bc08bb62
--- /dev/null
+++ b/drivers/phy/spacemit/phy-k3-combo.c
@@ -0,0 +1,252 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * phy-k3-combo.c - SpacemiT K3 PCIe/USB3 Combo PHY Driver
+ *
+ * Copyright (c) 2025 SpacemiT Technology Co. Ltd
+ */
+
+#include <linux/bitfield.h>
+#include <linux/io.h>
+#include <linux/module.h>
+#include <linux/mfd/syscon.h>
+#include <linux/iopoll.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+#include <linux/phy/phy.h>
+
+#include <dt-bindings/phy/phy.h>
+
+#include "phy-k3-common.h"
+
+/*
+ * The PCIE/USB Subsystem on SpacemiT K3 have 3 single lane PIPE3 PHYs
+ * (PHY2/3/4) shared by PCIE PortC/D and USB3 PortB/C/D.
+ *
+ * PMUA_PCIE_SUBSYS_MGMT[4:0]
+ *
+ * bit4 = 0 : PCIe A X8 mode, all 8 lanes dedicated to PCIe Port A
+ * 1 : PHY lanes shared between PCIe or USB according to [3:0]
+ *
+ * All PHY matrix combinations according to [4:0]:
+ *
+ * 0x0X : PCIe-A X8
+ * 0x10 : PCIe-C x2 (PHY2+PHY3) + PCIe-D x1 (PHY4)
+ * 0x11 : PCIe-C x2 (PHY2+PHY3) + USB-D (PHY4)
+ * 0x12 : PCIe-C x1 (PHY2) + USB-C (PHY3)
+ * 0x13 : PCIe-C x1 (PHY2) + USB-C (PHY3) + USB-D (PHY4)
+ * 0x14 : PCIe-C x1 (PHY3) + USB-B (PHY2)
+ * 0x15 : PCIe-C x1 (PHY3) + USB-B (PHY2) + USB-D (PHY4)
+ * 0x16 : USB-B (PHY2) + USB-C (PHY3) + PCIe D x1 (PHY4)
+ * 0x17 : USB-B (PHY2) + USB-C (PHY3) + USB-D (PHY4)
+ *
+ * So any USB Port B/C/D operation requires PCIe A X8 mode to be disabled.
+ */
+#define PMUA_PCIE_SUBSYS_MGMT 0x1d8
+#define PU_MATRIX_CONF_MASK GENMASK(4, 0)
+
+#define COMBPHY_MAX_SUBPHYS 6
+
+struct k3_combo_phy {
+ struct device *dev;
+ struct k3_lane_group groups[COMBPHY_MAX_SUBPHYS];
+ void __iomem *base;
+ struct regmap *apb_spare;
+};
+
+static const struct k3_phy_lane_group_data k3_combphy_lane_group0 = {
+ .lanes = 2,
+ .config = 0xff,
+ .mask = 0x00,
+ .offsets = {
+ 0x0, 0x400
+ },
+};
+
+static const struct k3_phy_lane_group_data k3_combphy_lane_group1 = {
+ .lanes = 2,
+ .config = 0xff,
+ .mask = 0x00,
+ .offsets = {
+ 0x100000, 0x100400
+ },
+};
+
+static const struct k3_phy_lane_group_data k3_combphy_lane_group2 = {
+ .lanes = 1,
+ .config = 0x14,
+ .mask = 0x14,
+ .offsets = {
+ 0x200000
+ },
+};
+
+static const struct k3_phy_lane_group_data k3_combphy_lane_group3 = {
+ .lanes = 1,
+ .config = 0x12,
+ .mask = 0x12,
+ .offsets = {
+ 0x300000
+ },
+};
+
+static const struct k3_phy_lane_group_data k3_combphy_lane_group4 = {
+ .lanes = 1,
+ .config = 0x11,
+ .mask = 0x11,
+ .offsets = {
+ 0x400000
+ },
+};
+
+static const struct k3_phy_lane_group_data k3_combphy_lane_group5 = {
+ .lanes = 1,
+ .config = 0xff,
+ .mask = 0x00,
+ .offsets = {
+ 0x500000
+ },
+};
+
+static const struct k3_phy_lane_group_data *k3_combphy_lane_datas[] = {
+ &k3_combphy_lane_group0,
+ &k3_combphy_lane_group1,
+ &k3_combphy_lane_group2,
+ &k3_combphy_lane_group3,
+ &k3_combphy_lane_group4,
+ &k3_combphy_lane_group5,
+};
+
+static int k3_combo_phy_init_lanes(struct k3_combo_phy *phy, unsigned int config)
+{
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(k3_combphy_lane_datas); i++) {
+ const struct k3_phy_lane_group_data *data = k3_combphy_lane_datas[i];
+ struct k3_lane_group *lg = &phy->groups[i];
+ const struct phy_ops *ops;
+ bool is_usb;
+
+ is_usb = (data->mask & config) == data->config;
+ if (is_usb)
+ ops = &k3_usb3_phy_ops;
+ else
+ ops = &k3_pcie_phy_ops;
+
+ dev_dbg(phy->dev, "phy %d is %s\n", i, is_usb ? "usb" : "pcie");
+
+ lg->phy = devm_phy_create(phy->dev, NULL, ops);
+ if (IS_ERR(lg->phy))
+ return PTR_ERR(lg->phy);
+
+ lg->is_pcie = !is_usb;
+ lg->data = data;
+ lg->base = phy->base;
+ phy_set_drvdata(lg->phy, lg);
+ }
+
+ return 0;
+}
+
+static int k3_combo_phy_update_config(struct regmap *apmu, unsigned int config)
+{
+ if (config & ~PU_MATRIX_CONF_MASK)
+ return -EINVAL;
+
+ return regmap_update_bits(apmu, PMUA_PCIE_SUBSYS_MGMT, PU_MATRIX_CONF_MASK, config);
+}
+
+static struct phy *k3_combo_phy_xlate(struct device *dev, const struct of_phandle_args *args)
+{
+ struct k3_combo_phy *phy = dev_get_drvdata(dev);
+ struct k3_lane_group *lg;
+
+ if (args->args_count != 2) {
+ dev_err(dev, "Invalid number of arguments\n");
+ return ERR_PTR(-EINVAL);
+ }
+
+ if (args->args[0] >= ARRAY_SIZE(k3_combphy_lane_datas)) {
+ dev_err(dev, "Invalid PHY id\n");
+ return ERR_PTR(-EINVAL);
+ }
+
+ lg = &phy->groups[args->args[0]];
+
+ if ((lg->is_pcie && args->args[1] != PHY_TYPE_PCIE) ||
+ (!lg->is_pcie && args->args[1] != PHY_TYPE_USB3)) {
+ dev_err(dev, "Invalid PHY mode\n");
+ return ERR_PTR(-EINVAL);
+ }
+
+ return lg->phy;
+}
+
+static int k3_combo_phy_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct device_node *node = dev->of_node;
+ struct phy_provider *provider;
+ struct k3_combo_phy *phy;
+ struct regmap *apmu;
+ u32 config = 0;
+ int ret;
+
+ phy = devm_kzalloc(dev, sizeof(*phy), GFP_KERNEL);
+ if (!phy)
+ return -ENOMEM;
+
+ phy->base = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(phy->base))
+ return PTR_ERR(phy->base);
+
+ phy->apb_spare = syscon_regmap_lookup_by_phandle(node, "spacemit,apb-spare");
+ if (IS_ERR(phy->apb_spare))
+ return dev_err_probe(dev, PTR_ERR(phy->apb_spare),
+ "Failed to find APB SPARE syscon");
+
+ apmu = syscon_regmap_lookup_by_phandle_args(node, "spacemit,apmu", 1, &config);
+ if (IS_ERR(apmu))
+ return dev_err_probe(dev, PTR_ERR(apmu),
+ "Failed to find APMU syscon");
+
+ ret = k3_combo_phy_update_config(apmu, config);
+ if (ret < 0)
+ return dev_err_probe(dev, ret, "Failed to set lane configuration");
+
+ phy->dev = dev;
+ platform_set_drvdata(pdev, phy);
+
+ ret = k3_phy_calibrate(phy->apb_spare);
+ if (ret < 0)
+ return dev_err_probe(dev, ret, "Failed to calibrate phy");
+
+ ret = k3_combo_phy_init_lanes(phy, config);
+ if (ret < 0)
+ return dev_err_probe(dev, ret, "Failed to init lanes");
+
+ provider = devm_of_phy_provider_register(dev, k3_combo_phy_xlate);
+ if (IS_ERR(provider))
+ return dev_err_probe(dev, PTR_ERR(provider),
+ "Failed to register provider\n");
+
+ return 0;
+}
+
+static const struct of_device_id k3_combo_phy_of_match[] = {
+ { .compatible = "spacemit,k3-combo-phy" },
+ { },
+};
+MODULE_DEVICE_TABLE(of, k3_combo_phy_of_match);
+
+static struct platform_driver k3_combo_phy_driver = {
+ .probe = k3_combo_phy_probe,
+ .driver = {
+ .name = "spacemit,k3-combo-phy",
+ .of_match_table = k3_combo_phy_of_match,
+ },
+};
+module_platform_driver(k3_combo_phy_driver);
+
+MODULE_DESCRIPTION("SpacemiT K3 USB3/PCIe combo PHY driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/phy/spacemit/phy-k3-common.c b/drivers/phy/spacemit/phy-k3-common.c
new file mode 100644
index 000000000..0b289e63d
--- /dev/null
+++ b/drivers/phy/spacemit/phy-k3-common.c
@@ -0,0 +1,391 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#include <linux/bitfield.h>
+#include <linux/cleanup.h>
+#include <linux/io.h>
+#include <linux/iopoll.h>
+#include <linux/module.h>
+#include <linux/regmap.h>
+#include <linux/usb.h>
+
+#include <dt-bindings/phy/phy.h>
+
+#include "phy-k3-common.h"
+
+/* PHY Registers */
+#define PHY_VERSION 0x0
+
+#define PHY_RESET_CFG 0x04
+
+#define PHY_RESET_RXBUF_RST BIT(0)
+#define PHY_RESET_SOFT_RST_PCS BIT(1)
+#define PHY_RESET_SOFT_RST_AHB BIT(2)
+#define PHY_RESET_EN_SD_AFTER_LOCK BIT(6)
+
+#define PHY_CLK_CFG 0x08
+
+#define PHY_CLK_PLL_READY BIT(0)
+#define PHY_CLK_TXCLK_INV BIT(2)
+#define PHY_CLK_RXCLK_EN BIT(3)
+#define PHY_CLK_TXCLK_EN BIT(4)
+#define PHY_CLK_PCLK_EN BIT(5)
+#define PHY_CLK_PIPE_PCLK_EN BIT(6)
+#define PHY_CLK_REFCLK_FREQ GENMASK(10, 7)
+#define PHY_CLK_REFCLK_24M 2
+#define PHY_CLK_SW_INIT_DONE BIT(11)
+#define PHY_CLK_PU_SSC_OUT BIT(23)
+
+#define PHY_MODE_CFG 0x0C
+
+#define PHY_MODE_PCIE_INT_EN BIT(0)
+#define PHY_MODE_LFPS_TPERIOD GENMASK(9, 8)
+#define PHY_MODE_LFPS_TPERIOD_USB 3
+
+#define PHY_PU_SEL 0x40
+
+#define PHY_PU_CFG_STATUS BIT(9)
+#define PHY_PU_OVRD_STATUS BIT(10)
+
+#define PHY_PU_CK_REG 0x54
+
+#define PHY_PU_REFCLK_100 BIT(25)
+
+#define PHY_PLL_REG1 0x58
+
+#define PHY_PLL_FREF_SEL GENMASK(15, 13)
+#define PHY_PLL_FREF_24M 0x1
+#define PHY_PLL_SSC_DEP_SEL GENMASK(27, 24)
+#define PHY_PLL_SSC_5000PPM 0xa
+#define PHY_PLL_SSC_MODE GENMASK(29, 28)
+#define PHY_PLL_SSC_MODE_CENTER_SPREAD 0
+#define PHY_PLL_SSC_MODE_UP_SPREAD 1
+#define PHY_PLL_SSC_MODE_DOWN_SPREAD 2
+#define PHY_PLL_SSC_MODE_DOWN_SPREAD1 3
+
+#define PHY_PLL_REG2 0x5c
+
+#define PHY_PLL_SEL_REF100 BIT(21)
+
+/* PHY RX Register Definitions */
+#define PHY_RX_REG_A 0x60
+
+#define PHY_RX_REG0_MASK GENMASK(7, 0)
+#define PHY_RX_REG1_MASK GENMASK(15, 8)
+#define PHY_RX_REG2_MASK GENMASK(23, 16)
+#define PHY_RX_REG3_MASK GENMASK(31, 24)
+
+#define PHY_RX_REG0_RLOAD BIT(4)
+#define PHY_RX_REG1_RTERM GENMASK(11, 8)
+#define PHY_RX_REG1_RC_CALI GENMASK(15, 12)
+#define PHY_RX_REG2_CSEL GENMASK(19, 16)
+#define PHY_RX_REG2_FORCE_CSEL BIT(20)
+#define PHY_RX_REG2_PSEL GENMASK(23, 21)
+#define PHY_RX_REG3_I_LOAD GENMASK(26, 24)
+#define PHY_RX_REG3_SEL_CBOOST_CODE BIT(27)
+#define PHY_RX_REG3_ADJ_BIAS GENMASK(29, 28)
+#define PHY_RX_REG3_RDEG1 GENMASK(31, 30)
+
+#define PHY_RX_REG_B 0x64
+
+#define PHY_RX_REG4_MASK GENMASK(7, 0)
+#define PHY_RX_REG5_MASK GENMASK(15, 8)
+#define PHY_RX_REG6_MASK GENMASK(23, 16)
+
+#define PHY_RX_REGB_MASK GENMASK(23, 0)
+
+#define PHY_RX_REG4_RDEG2 GENMASK(2, 1)
+#define PHY_RX_REG4_ENVOS BIT(4)
+#define PHY_RX_REG4_RTERM_SEL BIT(5)
+#define PHY_RX_REG4_MANUAL_CFG BIT(7)
+#define PHY_RX_REG5_RCELL_VCM GENMASK(11, 8)
+#define PHY_RX_REG5_RCELL_BIAS GENMASK(15, 12)
+#define PHY_RX_REG6_H1_REG GENMASK(19, 16)
+#define PHY_RX_REG6_ADAPT_GAIN GENMASK(21, 20)
+#define PHY_RX_REG6_BYPASS_ADPT BIT(22)
+
+#define PHY_ADPT_CFG0 0x140
+#define PHY_ADPT_AFE_RST_OVRD_EN BIT(1)
+#define PHY_ADPT_AFE_RST_OVRD_VAL BIT(4)
+
+#define PHY_RXEQ_TIME 0xb4
+#define PHY_RXEQ_TIME_OVRD_POST_C_SOC BIT(21)
+#define PHY_RXEQ_TIME_CFG_AMP_SOC GENMASK(23, 22)
+#define PHY_RXEQ_TIME_AMP_SOC_650M 0
+#define PHY_RXEQ_TIME_AMP_SOC_800M 1
+#define PHY_RXEQ_TIME_AMP_SOC_870M 2
+#define PHY_RXEQ_TIME_AMP_SOC_900M 3
+#define PHY_RXEQ_TIME_OVRD_AMP_SOC BIT(24)
+
+#define PCIE_PU_ADDR_CLK_CFG 0x0008
+#define PHY_CLK_PLL_READY BIT(0)
+#define PCIE_INITAL_TIMER GENMASK(6, 3)
+#define CFG_INTERNAL_TIMER_ADJ GENMASK(10, 7)
+#define CFG_SW_PHY_INIT_DONE BIT(11)
+
+/* Lane RX/TX configuration (per‑lane, at lane_base) */
+#define PCIE_RX_REG1 0x050
+#define PCIE_RX_REFCLK_MODE GENMASK(1, 0)
+#define PCIE_RX_REFCLK_MODE_DRIVER 1
+#define PCIE_RX_SEL_TRI_CODE BIT(2)
+#define PCIE_RX_LEGACY GENMASK(15, 8)
+#define PCIE_RX_LEGACY_DEFAULT 0x65
+
+#define PCIE_TX_REG1 0x064
+
+#define PCIE_PLL_TIMEOUT 500000
+#define PCIE_POLL_DELAY 500
+
+static int k3_usb3phy_init_single(struct k3_lane_group *lg, void __iomem *base)
+{
+ struct phy *phy = lg->phy;
+ u32 val, tmp;
+ int ret;
+
+ /* Do not wait CDR lock before sampling data */
+ val = readl(base + PHY_RESET_CFG);
+ val = u32_replace_bits(val, 0, PHY_RESET_EN_SD_AFTER_LOCK);
+ writel(val, base + PHY_RESET_CFG);
+
+ /* Power down 100MHz refclk buffer */
+ val = readl(base + PHY_PU_CK_REG);
+ val = u32_replace_bits(val, 0, PHY_PU_REFCLK_100);
+ writel(val, base + PHY_PU_CK_REG);
+
+ /* Program PLL REG1 configure the SSC */
+ val = FIELD_PREP(PHY_PLL_SSC_MODE, PHY_PLL_SSC_MODE_DOWN_SPREAD1) |
+ FIELD_PREP(PHY_PLL_SSC_DEP_SEL, PHY_PLL_SSC_5000PPM) |
+ FIELD_PREP(PHY_PLL_FREF_SEL, PHY_PLL_FREF_24M);
+ writel(val, base + PHY_PLL_REG1);
+
+ /* Un-select 100MHz PLL reference */
+ val = readl(base + PHY_PLL_REG2);
+ val = u32_replace_bits(val, 0, PHY_PLL_SEL_REF100);
+ writel(val, base + PHY_PLL_REG2);
+
+ /* USB LFPS period configuration */
+ val = readl(base + PHY_MODE_CFG);
+ val = u32_replace_bits(val, PHY_MODE_LFPS_TPERIOD_USB, PHY_MODE_LFPS_TPERIOD);
+ writel(val, base + PHY_MODE_CFG);
+
+ /* Force AFE adaptation reset */
+ val = readl(base + PHY_ADPT_CFG0);
+ val |= PHY_ADPT_AFE_RST_OVRD_EN | PHY_ADPT_AFE_RST_OVRD_VAL;
+ writel(val, base + PHY_ADPT_CFG0);
+
+ /* Override driver amplitude value to 900m */
+ val = readl(base + PHY_RXEQ_TIME);
+ val |= PHY_RXEQ_TIME_OVRD_AMP_SOC;
+ val = u32_replace_bits(val, PHY_RXEQ_TIME_AMP_SOC_900M, PHY_RXEQ_TIME_CFG_AMP_SOC);
+ writel(val, base + PHY_RXEQ_TIME);
+
+ /* Configure RX parameters */
+ val = PHY_RX_REG0_RLOAD |
+ FIELD_PREP(PHY_RX_REG1_RTERM, 0x8) |
+ FIELD_PREP(PHY_RX_REG1_RC_CALI, 0x7) |
+ FIELD_PREP(PHY_RX_REG2_CSEL, 0x8) |
+ PHY_RX_REG2_FORCE_CSEL |
+ FIELD_PREP(PHY_RX_REG2_PSEL, 0x4) |
+ FIELD_PREP(PHY_RX_REG3_I_LOAD, 0x7) |
+ PHY_RX_REG3_SEL_CBOOST_CODE |
+ FIELD_PREP(PHY_RX_REG3_ADJ_BIAS, 0x1) |
+ FIELD_PREP(PHY_RX_REG3_RDEG1, 0x3);
+ writel(val, base + PHY_RX_REG_A);
+
+ val = readl(base + PHY_RX_REG_B);
+ tmp = FIELD_PREP(PHY_RX_REG4_RDEG2, 0x2) |
+ PHY_RX_REG4_ENVOS | PHY_RX_REG4_RTERM_SEL | PHY_RX_REG4_MANUAL_CFG |
+ FIELD_PREP(PHY_RX_REG5_RCELL_VCM, 0x8) |
+ FIELD_PREP(PHY_RX_REG5_RCELL_BIAS, 0x8) |
+ FIELD_PREP(PHY_RX_REG6_H1_REG, 0x8) |
+ FIELD_PREP(PHY_RX_REG6_ADAPT_GAIN, 0x2);
+ val = u32_replace_bits(val, tmp, PHY_RX_REGB_MASK);
+ writel(val, base + PHY_RX_REG_B);
+
+ /*
+ * Inform PHY that all PLL-related configuration is done.
+ * PLL will not start locking until PHY_CLK_SW_INIT_DONE is set.
+ */
+ val = PHY_CLK_SW_INIT_DONE | PHY_CLK_PU_SSC_OUT |
+ FIELD_PREP(PHY_CLK_REFCLK_FREQ, PHY_CLK_REFCLK_24M) |
+ PHY_CLK_RXCLK_EN | PHY_CLK_TXCLK_EN |
+ PHY_CLK_PCLK_EN | PHY_CLK_PIPE_PCLK_EN;
+ writel(val, base + PHY_CLK_CFG);
+
+ ret = readl_poll_timeout(base + PHY_CLK_CFG, val,
+ (val & PHY_CLK_PLL_READY),
+ PCIE_POLL_DELAY, PCIE_PLL_TIMEOUT);
+ if (ret) {
+ dev_err(&phy->dev, "PHY PLL polling timeout\n");
+ return ret;
+ }
+
+ return 0;
+}
+
+static int k3_usb3phy_init(struct phy *phy)
+{
+ struct k3_lane_group *lg = phy_get_drvdata(phy);
+ int ret, i;
+
+ for (i = 0; i < lg->data->lanes; i++) {
+ ret = k3_usb3phy_init_single(lg, lg->base + lg->data->offsets[i]);
+ if (ret < 0)
+ return ret;
+ }
+
+ return 0;
+}
+
+const struct phy_ops k3_usb3_phy_ops = {
+ .init = k3_usb3phy_init,
+ .owner = THIS_MODULE,
+};
+EXPORT_SYMBOL_GPL(k3_usb3_phy_ops);
+
+static int k3_pcie_phy_init(struct phy *phy)
+{
+ struct k3_lane_group *lg = phy_get_drvdata(phy);
+ void __iomem *phy_base = lg->base + lg->data->offsets[0];
+ u32 val;
+ int ret;
+ int i;
+
+ val = readl(phy_base + PHY_PLL_REG1);
+ val = u32_replace_bits(val, 0x2, GENMASK(15, 12));
+ writel(val, phy_base + PHY_PLL_REG1);
+
+ val = readl(phy_base + PHY_PLL_REG2);
+ val = u32_replace_bits(val, 0, BIT(21));
+ writel(val, phy_base + PHY_PLL_REG2);
+
+ for (i = 0; i < lg->data->lanes; i++) {
+ void __iomem *lane_base = lg->base + lg->data->offsets[i];
+
+ val = readl(lane_base + PCIE_RX_REG1);
+ val = u32_replace_bits(val, 0, 0x3);
+ writel(val, lane_base + PCIE_RX_REG1);
+ }
+
+ val = readl(phy_base + PHY_PLL_REG2);
+ val |= BIT(20);
+ writel(val, phy_base + PHY_PLL_REG2);
+
+ /* The write is needed as clock requires renegotiation */
+ val = FIELD_PREP(PCIE_RX_REFCLK_MODE, PCIE_RX_REFCLK_MODE_DRIVER) |
+ PCIE_RX_SEL_TRI_CODE |
+ FIELD_PREP(PCIE_RX_LEGACY, PCIE_RX_LEGACY_DEFAULT);
+ writel(val, phy_base + PCIE_RX_REG1);
+
+ /* pll_reg1 of lane0, disable SSC: pll[27:24] = 0 */
+ val = readl(phy_base + PHY_PLL_REG1);
+ val = u32_replace_bits(val, 0, GENMASK(27, 24));
+ writel(val, phy_base + PHY_PLL_REG1);
+
+ for (i = 0; i < lg->data->lanes; i++) {
+ void __iomem *lane_base = lg->base + lg->data->offsets[i];
+
+ /* set cfg_tx_send_dummy_data to be 1'b1 for disable dash data */
+ val = readl(lane_base + PHY_PU_SEL);
+ val = u32_replace_bits(val, 1, BIT(13));
+ writel(val, lane_base + PHY_PU_SEL);
+
+ /* disable en_sample_data_after_cdr_locked */
+ val = readl(lane_base + PHY_RESET_CFG);
+ val = u32_replace_bits(val, 0, BIT(6));
+ writel(val, lane_base + PHY_RESET_CFG);
+
+ /* Dynamic Lock */
+ val = readl(lane_base + PHY_MODE_CFG);
+ val = u32_replace_bits(val, 1, BIT(2));
+ writel(val, lane_base + PHY_MODE_CFG);
+
+ val = FIELD_PREP(PHY_RX_REG0_MASK, 0x10) |
+ FIELD_PREP(PHY_RX_REG1_MASK, 0x78) |
+ FIELD_PREP(PHY_RX_REG2_MASK, 0x98) |
+ FIELD_PREP(PHY_RX_REG3_MASK, 0xdf);
+ writel(val, lane_base + PHY_RX_REG_A);
+
+ val = readl(lane_base + PHY_RX_REG_B);
+ val &= ~PHY_RX_REGB_MASK;
+ val |= FIELD_PREP(PHY_RX_REG4_MASK, 0xb4) |
+ FIELD_PREP(PHY_RX_REG5_MASK, 0x88) |
+ FIELD_PREP(PHY_RX_REG6_MASK, 0x28);
+ writel(val, lane_base + PHY_RX_REG_B);
+
+ /* Set init done */
+ val = readl(lane_base + PCIE_PU_ADDR_CLK_CFG);
+ val = u32_replace_bits(val, 1, CFG_SW_PHY_INIT_DONE);
+ writel(val, lane_base + PCIE_PU_ADDR_CLK_CFG);
+ }
+
+ ret = readl_poll_timeout(phy_base + PCIE_PU_ADDR_CLK_CFG, val,
+ (val & PHY_CLK_PLL_READY), PCIE_POLL_DELAY,
+ PCIE_PLL_TIMEOUT);
+ if (ret) {
+ dev_err(&lg->phy->dev, "PHY PLL lock timeout\n");
+ return ret;
+ }
+
+ return 0;
+}
+
+const struct phy_ops k3_pcie_phy_ops = {
+ .init = k3_pcie_phy_init,
+ .owner = THIS_MODULE,
+};
+EXPORT_SYMBOL_GPL(k3_pcie_phy_ops);
+
+/* PHY rcal init requires APB_SPARE regmap access */
+
+#define APB_SPARE_PU_CAL 0x178
+#define PU_CAL BIT(17)
+
+#define APB_SPARE_RCAL_HSIO 0x17c
+#define APB_SPARE_PU_CAL_DONE BIT(8)
+#define RCAL_OVRD_PTRIM GENMASK(23, 20)
+#define RCAL_OVRD_NTRIM GENMASK(27, 24)
+#define RCAL_OVRD_PTRIM_EN BIT(28)
+#define RCAL_OVRD_NTRIM_EN BIT(29)
+#define RCAL_OVRD_STABLE_VAL BIT(30)
+#define RCAL_OVRD_STABLE_EN BIT(31)
+
+#define RCAL_OVRD_TRIM_EN (RCAL_OVRD_NTRIM_EN | RCAL_OVRD_PTRIM_EN)
+#define RCAL_OVRD_TRIM_MASK (RCAL_OVRD_NTRIM | RCAL_OVRD_PTRIM)
+
+#define PU_CAL_TIMEOUT 2000000
+
+static DEFINE_MUTEX(calibrate_lock);
+
+int k3_phy_calibrate(struct regmap *apb_spare)
+{
+ unsigned int val = 0;
+ int ret;
+
+ guard(mutex)(&calibrate_lock);
+
+ regmap_read(apb_spare, APB_SPARE_RCAL_HSIO, &val);
+ if (val & APB_SPARE_PU_CAL_DONE)
+ return 0;
+
+ regmap_update_bits(apb_spare, APB_SPARE_PU_CAL, PU_CAL,
+ PU_CAL);
+
+ ret = regmap_read_poll_timeout(apb_spare, APB_SPARE_RCAL_HSIO,
+ val, (val & APB_SPARE_PU_CAL_DONE), PCIE_POLL_DELAY,
+ PU_CAL_TIMEOUT);
+
+ if (ret)
+ regmap_update_bits(apb_spare, APB_SPARE_RCAL_HSIO,
+ RCAL_OVRD_TRIM_EN | RCAL_OVRD_STABLE_VAL |
+ RCAL_OVRD_TRIM_MASK | RCAL_OVRD_STABLE_EN,
+ RCAL_OVRD_TRIM_EN | RCAL_OVRD_STABLE_VAL |
+ FIELD_PREP(RCAL_OVRD_NTRIM, 0x6) |
+ FIELD_PREP(RCAL_OVRD_PTRIM, 0xa) |
+ RCAL_OVRD_STABLE_EN);
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(k3_phy_calibrate);
+
+MODULE_DESCRIPTION("SpacemiT K3 PHY common ops");
+MODULE_LICENSE("GPL");
diff --git a/drivers/phy/spacemit/phy-k3-common.h b/drivers/phy/spacemit/phy-k3-common.h
new file mode 100644
index 000000000..49009c3c3
--- /dev/null
+++ b/drivers/phy/spacemit/phy-k3-common.h
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+
+#ifndef _PHY_K3_COMMON_H
+#define _PHY_K3_COMMON_H
+
+#include <linux/phy/phy.h>
+
+struct k3_phy_lane_group_data {
+ u32 lanes;
+ u8 config;
+ u8 mask;
+ u32 offsets[] __counted_by(lanes);
+};
+
+struct k3_lane_group {
+ const struct k3_phy_lane_group_data *data;
+ void __iomem *base;
+ struct phy *phy;
+ bool is_pcie;
+};
+
+extern const struct phy_ops k3_pcie_phy_ops;
+extern const struct phy_ops k3_usb3_phy_ops;
+
+int k3_phy_calibrate(struct regmap *apb_spare);
+
+#endif