[PATCH 1/3] phy: spacemit: add K1 PCIe/USB3 combo PHY driver
Eric Chung
eric.chung at riscstar.com
Sun Jul 5 15:06:27 CEST 2026
Add PHY driver for Spacemit K1 SoC supporting:
- Combo PHY (PCIe/USB3) with auto-calibration of RX/TX termination
- PCIe-only PHY (2 lanes) using combo PHY calibration values
The combo PHY calibrates at probe time, and the result is shared
globally so PCIe-only PHYs can apply the same termination settings.
Signed-off-by: Eric Chung <eric.chung at riscstar.com>
---
drivers/phy/Kconfig | 1 +
drivers/phy/Makefile | 1 +
drivers/phy/spacemit/Kconfig | 11 +
drivers/phy/spacemit/Makefile | 1 +
drivers/phy/spacemit/phy-k1-pcie.c | 492 +++++++++++++++++++++++++++++++++++++
5 files changed, 506 insertions(+)
diff --git a/drivers/phy/Kconfig b/drivers/phy/Kconfig
index 89d84df96ae..fe3443d1489 100644
--- a/drivers/phy/Kconfig
+++ b/drivers/phy/Kconfig
@@ -349,6 +349,7 @@ source "drivers/phy/ti/Kconfig"
source "drivers/phy/qcom/Kconfig"
source "drivers/phy/renesas/Kconfig"
source "drivers/phy/starfive/Kconfig"
+source "drivers/phy/spacemit/Kconfig"
config PHY_COMMON_PROPS
bool "Common PHY properties support"
diff --git a/drivers/phy/Makefile b/drivers/phy/Makefile
index e46c362878d..9933a406d54 100644
--- a/drivers/phy/Makefile
+++ b/drivers/phy/Makefile
@@ -49,3 +49,4 @@ obj-y += ti/
obj-y += qcom/
obj-y += renesas/
obj-y += starfive/
+obj-y += spacemit/
diff --git a/drivers/phy/spacemit/Kconfig b/drivers/phy/spacemit/Kconfig
new file mode 100644
index 00000000000..3c47601c1fd
--- /dev/null
+++ b/drivers/phy/spacemit/Kconfig
@@ -0,0 +1,11 @@
+config PHY_SPACEMIT_K1_PCIE
+ bool "Spacemit K1 PCIe PHY driver"
+ depends on PHY
+ help
+ Support for the Spacemit K1 SoC PCIe and Combo (PCIe/USB3) PHYs.
+
+ This driver handles both the combo PHY used for port A (which
+ calibrates RX/TX termination values) and the PCIe-only PHYs for
+ ports B and C. The combo PHY must be enabled even if port A
+ is not used for PCIe, because it provides calibration values
+ required by the other ports.
diff --git a/drivers/phy/spacemit/Makefile b/drivers/phy/spacemit/Makefile
new file mode 100644
index 00000000000..3496c099cdf
--- /dev/null
+++ b/drivers/phy/spacemit/Makefile
@@ -0,0 +1 @@
+obj-$(CONFIG_PHY_SPACEMIT_K1_PCIE) += phy-k1-pcie.o
diff --git a/drivers/phy/spacemit/phy-k1-pcie.c b/drivers/phy/spacemit/phy-k1-pcie.c
new file mode 100644
index 00000000000..e54742a2a9c
--- /dev/null
+++ b/drivers/phy/spacemit/phy-k1-pcie.c
@@ -0,0 +1,492 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Spacemit K1 PCIe and PCIe/USB 3 combo PHY driver
+ *
+ * Copyright (C) 2026 RISCstar Ltd. All rights reserved.
+ */
+
+#include <asm/io.h>
+#include <clk.h>
+#include <dm.h>
+#include <dm/device_compat.h>
+#include <dm/ofnode.h>
+#include <errno.h>
+#include <generic-phy.h>
+#include <linux/bitfield.h>
+#include <linux/bitops.h>
+#include <linux/delay.h>
+#include <linux/iopoll.h>
+#include <reset.h>
+
+#include <dt-bindings/phy/phy.h>
+
+/*
+ * Three PCIe ports are supported in the SpacemiT K1 SoC.
+ *
+ * Port A (combo PHY): 1 lane, can switch between PCIe and USB 3.
+ * The only PHY that can auto-calibrate RX/TX termination.
+ * Port B (pcie-phy): 2 lanes, PCIe only.
+ * Port C (pcie-phy): 2 lanes, PCIe only.
+ *
+ * Calibration values from port A are used to configure ports B and C.
+ * Port B/C must not probe until port A has completed calibration.
+ */
+
+/* Offset between lane 0 and lane 1 registers */
+#define PHY_LANE_OFFSET 0x0400
+
+/* PHY PLL configuration */
+#define PCIE_PU_ADDR_CLK_CFG 0x0008
+#define PLL_READY BIT(0)
+#define CFG_INTERNAL_TIMER_ADJ GENMASK(10, 7)
+#define TIMER_ADJ_PCIE 0x6
+#define CFG_SW_PHY_INIT_DONE BIT(11)
+
+#define PCIE_RC_DONE_STATUS 0x0018
+#define CFG_FORCE_RCV_RETRY BIT(10)
+
+/* PCIe PHY lane calibration */
+#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 */
+#define PCIE_PU_PLL_1 0x0048
+#define REF_100_WSSC BIT(12)
+#define FREF_SEL GENMASK(15, 13)
+#define FREF_24M 0x1
+#define SSC_DEP_SEL GENMASK(19, 16)
+#define SSC_DEP_NONE 0x0
+
+/* PCIe PHY configuration */
+#define PCIE_PU_PLL_2 0x004c
+#define GEN_REF100 BIT(4)
+
+#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)
+
+#define PCIE_LTSSM_DIS_ENTRY 0x005c
+#define CFG_REFCLK_MODE GENMASK(9, 8)
+#define RFCLK_MODE_DRIVER 0x1
+#define RFCLK_MODE_RECEIVER 0x2
+#define OVRD_REFCLK_MODE BIT(10)
+
+#define PCIE_TX_REG1 0x0064
+#define TX_RTERM_REG GENMASK(15, 12)
+#define TX_RTERM_SEL BIT(25)
+
+/* PHY calibration values */
+#define PCIE_RCAL_RESULT 0x0084
+#define RTERM_VALUE_RX GENMASK(3, 0)
+#define RTERM_VALUE_TX GENMASK(7, 4)
+#define R_TUNE_DONE BIT(10)
+
+/* PMU register offsets (shared APMU space) */
+#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)
+#define DEVICE_TYPE_RC BIT(31)
+#define GLOBAL_PHY_RST BIT(8)
+
+#define APMU_CLK_GATE_MASK 0x3f
+
+#define CALIBRATION_TIMEOUT_US 500000
+#define PLL_TIMEOUT_US 500000
+
+/* Global calibration value shared across PHY ports */
+static u32 k1_phy_rterm = ~0;
+
+struct k1_pcie_phy {
+ void __iomem *regs;
+ void __iomem *apmu_base; /* only for combo PHY */
+ u32 pcie_lanes;
+ bool is_combo;
+ struct clk_bulk clks;
+ struct reset_ctl_bulk rst;
+};
+
+static bool k1_phy_rterm_valid(void)
+{
+ return !(k1_phy_rterm & ~(RTERM_VALUE_RX | RTERM_VALUE_TX));
+}
+
+u32 k1_phy_rterm_rx(void)
+{
+ return FIELD_GET(RTERM_VALUE_RX, k1_phy_rterm);
+}
+
+u32 k1_phy_rterm_tx(void)
+{
+ return FIELD_GET(RTERM_VALUE_TX, k1_phy_rterm);
+}
+
+static void k1_phy_rterm_set(u32 val)
+{
+ k1_phy_rterm = val & (RTERM_VALUE_RX | RTERM_VALUE_TX);
+}
+
+static void k1_combo_phy_sel(struct k1_pcie_phy *k1_phy, bool usb)
+{
+ void __iomem *pmu = k1_phy->apmu_base;
+ u32 val;
+
+ val = readl(pmu + PMUA_USB_PHY_CTRL0);
+ if (usb)
+ val |= COMBO_PHY_SEL;
+ else
+ val &= ~COMBO_PHY_SEL;
+ writel(val, pmu + PMUA_USB_PHY_CTRL0);
+}
+
+int k1_pcie_combo_phy_calibrate(struct udevice *dev, struct k1_pcie_phy *k1_phy)
+{
+ void __iomem *regs = k1_phy->regs;
+ int ret = 0;
+ u32 val;
+
+ if (k1_phy_rterm_valid())
+ return 0;
+
+ /*
+ * Initialize the APMU control register: set RC mode, enable
+ * clock gates (bits 0-5), deassert PHY global reset, then
+ * release the PHY hold. Without this the PHY registers are
+ * not accessible and reads will hang.
+ */
+ val = readl(k1_phy->apmu_base + PCIE_CLK_RES_CTRL);
+ val |= DEVICE_TYPE_RC | PCIE_APP_HOLD_PHY_RST | APMU_CLK_GATE_MASK;
+ val &= ~GLOBAL_PHY_RST;
+ writel(val, k1_phy->apmu_base + PCIE_CLK_RES_CTRL);
+ val &= ~PCIE_APP_HOLD_PHY_RST;
+ writel(val, k1_phy->apmu_base + PCIE_CLK_RES_CTRL);
+
+ /* Put the combo PHY into PCIe mode for calibration */
+ k1_combo_phy_sel(k1_phy, false);
+
+ /* Set refclk override + clear CFG_REFCLK_MODE */
+ val = readl(regs + PCIE_LTSSM_DIS_ENTRY);
+ val |= OVRD_REFCLK_MODE;
+ writel(val, regs + PCIE_LTSSM_DIS_ENTRY);
+
+ val = readl(regs + PCIE_LTSSM_DIS_ENTRY);
+ val &= ~CFG_REFCLK_MODE;
+ writel(val, regs + PCIE_LTSSM_DIS_ENTRY);
+
+ val = readl(regs + PCIE_LTSSM_DIS_ENTRY);
+ val |= FIELD_PREP(CFG_REFCLK_MODE, RFCLK_MODE_DRIVER);
+ writel(val, regs + PCIE_LTSSM_DIS_ENTRY);
+
+ /* Configure PLL: select 24MHz reference, enable 100MHz output */
+ val = readl(regs + PCIE_PU_PLL_1);
+ val &= ~(FREF_SEL | REF_100_WSSC);
+ writel(val, regs + PCIE_PU_PLL_1);
+
+ val = readl(regs + PCIE_PU_PLL_1);
+ val |= FIELD_PREP(FREF_SEL, FREF_24M);
+ writel(val, regs + PCIE_PU_PLL_1);
+
+ val = readl(regs + PCIE_PU_PLL_2);
+ val |= GEN_REF100;
+ writel(val, regs + PCIE_PU_PLL_2);
+
+ val = readl(regs + PCIE_PU_PLL_1);
+ val &= ~SSC_DEP_SEL;
+ writel(val, regs + PCIE_PU_PLL_1);
+
+ /* Start PHY init + force receiver */
+ writel(0x00000B78, regs + PCIE_PU_ADDR_CLK_CFG);
+ writel(CFG_FORCE_RCV_RETRY, regs + PCIE_RC_DONE_STATUS);
+
+ /* Wait for calibration to complete */
+ ret = readl_poll_sleep_timeout(regs + PCIE_RCAL_RESULT,
+ val, val & R_TUNE_DONE,
+ 500, CALIBRATION_TIMEOUT_US);
+ if (ret)
+ dev_err(dev, "PHY calib timeout, RCAL=0x%08x\n", val);
+
+ if (!ret)
+ k1_phy_rterm_set(val);
+
+ return ret;
+}
+
+static int k1_pcie_phy_init(struct phy *phy)
+{
+ struct k1_pcie_phy *k1_phy = dev_get_priv(phy->dev);
+ void __iomem *regs = k1_phy->regs;
+ int ret;
+ u32 val;
+ int i, lane;
+
+ /* If combo PHY is configured for USB 3 mode */
+ if (k1_phy->is_combo && phy->id == PHY_TYPE_USB3) {
+ k1_combo_phy_sel(k1_phy, true);
+ return 0;
+ }
+
+ /* For combo PHY, ensure it's in PCIe mode */
+ if (k1_phy->is_combo)
+ k1_combo_phy_sel(k1_phy, false);
+
+ dev_dbg(phy->dev, "init: combo=%d lanes=%u rterm=0x%02x\n",
+ k1_phy->is_combo, k1_phy->pcie_lanes, k1_phy_rterm);
+
+ lane = (k1_phy->pcie_lanes == 1) ? 1 : 2;
+
+ /* Apply rterm calibration value: LSB to RX_REG1 */
+ for (i = 0; i < lane; i++) {
+ val = readl(regs + PCIE_RX_REG1 + PHY_LANE_OFFSET * i);
+ val |= FIELD_PREP(AFE_RTERM_REG, k1_phy_rterm_rx());
+ writel(val, regs + PCIE_RX_REG1 + PHY_LANE_OFFSET * i);
+ }
+
+ /* Disable RX_RTERM_SEL for all lanes */
+ for (i = 0; i < lane; i++) {
+ val = readl(regs + PCIE_RX_REG2 + PHY_LANE_OFFSET * i);
+ val &= ~RX_RTERM_SEL;
+ writel(val, regs + PCIE_RX_REG2 + PHY_LANE_OFFSET * i);
+ }
+
+ /* Apply rterm MSB to TX_REG1 */
+ for (i = 0; i < lane; i++) {
+ val = readl(regs + PCIE_TX_REG1 + PHY_LANE_OFFSET * i);
+ val |= FIELD_PREP(TX_RTERM_REG, k1_phy_rterm_tx());
+ writel(val, regs + PCIE_TX_REG1 + PHY_LANE_OFFSET * i);
+ }
+
+ /* Enable TX_RTERM_SEL for all lanes */
+ for (i = 0; i < lane; i++) {
+ val = readl(regs + PCIE_TX_REG1 + PHY_LANE_OFFSET * i);
+ val |= TX_RTERM_SEL;
+ writel(val, regs + PCIE_TX_REG1 + PHY_LANE_OFFSET * i);
+ }
+
+ /* Set CLKSEL_24M on lane0 */
+ val = readl(regs + PCIE_RC_CAL_REG2);
+ val |= FIELD_PREP(CLKSEL, CLKSEL_24M);
+ writel(val, regs + PCIE_RC_CAL_REG2);
+
+ /* Toggle RC_CAL_TOGGLE for all lanes */
+ for (i = 0; i < lane; i++) {
+ val = readl(regs + PCIE_RC_CAL_REG2 + PHY_LANE_OFFSET * i);
+ val &= ~RC_CAL_TOGGLE;
+ writel(val, regs + PCIE_RC_CAL_REG2 + PHY_LANE_OFFSET * i);
+ }
+ for (i = 0; i < lane; i++) {
+ val = readl(regs + PCIE_RC_CAL_REG2 + PHY_LANE_OFFSET * i);
+ val |= RC_CAL_TOGGLE;
+ writel(val, regs + PCIE_RC_CAL_REG2 + PHY_LANE_OFFSET * i);
+ }
+
+ /* Configure refclk as driver mode, override */
+ val = readl(regs + PCIE_LTSSM_DIS_ENTRY);
+ val |= OVRD_REFCLK_MODE;
+ writel(val, regs + PCIE_LTSSM_DIS_ENTRY);
+
+ val = readl(regs + PCIE_LTSSM_DIS_ENTRY);
+ val &= ~CFG_REFCLK_MODE;
+ writel(val, regs + PCIE_LTSSM_DIS_ENTRY);
+
+ val = readl(regs + PHY_LANE_OFFSET + PCIE_LTSSM_DIS_ENTRY);
+ val |= OVRD_REFCLK_MODE;
+ writel(val, regs + PHY_LANE_OFFSET + PCIE_LTSSM_DIS_ENTRY);
+
+ val = readl(regs + PHY_LANE_OFFSET + PCIE_LTSSM_DIS_ENTRY);
+ val &= ~CFG_REFCLK_MODE;
+ writel(val, regs + PHY_LANE_OFFSET + PCIE_LTSSM_DIS_ENTRY);
+
+ /* Set driver mode for both lanes */
+ val = readl(regs + PCIE_LTSSM_DIS_ENTRY);
+ val |= FIELD_PREP(CFG_REFCLK_MODE, RFCLK_MODE_DRIVER);
+ writel(val, regs + PCIE_LTSSM_DIS_ENTRY);
+
+ val = readl(regs + PHY_LANE_OFFSET + PCIE_LTSSM_DIS_ENTRY);
+ val |= FIELD_PREP(CFG_REFCLK_MODE, RFCLK_MODE_DRIVER);
+ writel(val, regs + PHY_LANE_OFFSET + PCIE_LTSSM_DIS_ENTRY);
+
+ /* Configure PLL: select 24MHz reference */
+ val = readl(regs + PCIE_PU_PLL_1);
+ val &= ~FREF_SEL;
+ writel(val, regs + PCIE_PU_PLL_1);
+
+ val = readl(regs + PCIE_PU_PLL_1);
+ val |= FIELD_PREP(FREF_SEL, FREF_24M);
+ writel(val, regs + PCIE_PU_PLL_1);
+
+ /* Enable 100MHz reference output */
+ val = readl(regs + PCIE_PU_PLL_2);
+ val |= GEN_REF100;
+ writel(val, regs + PCIE_PU_PLL_2);
+
+ /* Disable SSC */
+ val = readl(regs + PCIE_PU_PLL_1);
+ val &= ~SSC_DEP_SEL;
+ writel(val, regs + PCIE_PU_PLL_1);
+
+ /* Set PU_ADDR_CLK_CFG for both lanes */
+ val = 0x00000B78;
+ writel(val, regs + PCIE_PU_ADDR_CLK_CFG);
+ writel(val, regs + PHY_LANE_OFFSET + PCIE_PU_ADDR_CLK_CFG);
+
+ /* Force receiver done */
+ val = CFG_FORCE_RCV_RETRY;
+ writel(val, regs + PCIE_RC_DONE_STATUS);
+
+ /* Wait for PLL lock */
+ ret = readl_poll_timeout(regs + PCIE_PU_ADDR_CLK_CFG, val,
+ val & PLL_READY, PLL_TIMEOUT_US);
+ if (ret) {
+ dev_err(phy->dev, "PLL lock timeout! reg[0x08]=0x%08x\n", val);
+ return ret;
+ }
+
+ dev_dbg(phy->dev, "PLL locked (combo=%d, lanes=%d)\n",
+ k1_phy->is_combo, k1_phy->pcie_lanes);
+
+ return 0;
+}
+
+static int k1_pcie_phy_exit(struct phy *phy)
+{
+ return 0;
+}
+
+static int k1_pcie_phy_of_xlate(struct phy *phy,
+ struct ofnode_phandle_args *args)
+{
+ struct k1_pcie_phy *k1_phy = dev_get_priv(phy->dev);
+
+ if (k1_phy->is_combo) {
+ if (args->args_count != 1)
+ return -EINVAL;
+ if (args->args[0] != PHY_TYPE_PCIE &&
+ args->args[0] != PHY_TYPE_USB3)
+ return -EINVAL;
+ }
+
+ if (args->args_count > 0)
+ phy->id = args->args[0];
+ else
+ phy->id = 0;
+
+ return 0;
+}
+
+static const struct phy_ops k1_pcie_phy_ops = {
+ .of_xlate = k1_pcie_phy_of_xlate,
+ .init = k1_pcie_phy_init,
+ .exit = k1_pcie_phy_exit,
+};
+
+static int k1_pcie_phy_probe(struct udevice *dev)
+{
+ struct k1_pcie_phy *k1_phy = dev_get_priv(dev);
+ struct ofnode_phandle_args apmu_args;
+ bool is_combo;
+ int ret;
+ u32 lanes;
+
+ is_combo = (bool)dev_get_driver_data(dev);
+ k1_phy->is_combo = is_combo;
+
+ k1_phy->regs = dev_read_addr_ptr(dev);
+ if (!k1_phy->regs)
+ return -EINVAL;
+
+ /* Determine number of lanes */
+ if (is_combo) {
+ lanes = 1;
+ } else {
+ u32 count = 0;
+
+ ret = dev_read_u32(dev, "num-lanes", &count);
+ if (ret == 0 && count == 1)
+ lanes = 1;
+ else
+ lanes = 2;
+ }
+ k1_phy->pcie_lanes = lanes;
+
+ /* Get APMU base for combo PHY (for mode select + calibration) */
+ if (is_combo) {
+ ret = dev_read_phandle_with_args(dev, "spacemit,apmu",
+ NULL, 0, 0,
+ &apmu_args);
+ if (ret)
+ return ret;
+
+ k1_phy->apmu_base = (void __iomem *)ofnode_get_addr(apmu_args.node);
+ if (!k1_phy->apmu_base)
+ return -EINVAL;
+
+ ret = clk_get_bulk(dev, &k1_phy->clks);
+ if (ret)
+ dev_warn(dev, "failed to get clocks: %d\n", ret);
+
+ ret = reset_get_bulk(dev, &k1_phy->rst);
+ if (ret)
+ dev_warn(dev, "failed to get resets: %d\n", ret);
+
+ ret = k1_pcie_combo_phy_calibrate(dev, k1_phy);
+ if (ret)
+ dev_warn(dev, "calibration failed: %d\n", ret);
+ } else {
+ /*
+ * PCIe-only PHYs need the calibration value from the
+ * combo PHY. In device-tree order the combo PHY probes
+ * first, but handle the case where it hasn't yet by
+ * explicitly looking it up.
+ */
+ if (!k1_phy_rterm_valid()) {
+ struct udevice *combo_dev;
+
+ ret = uclass_first_device_drvdata(UCLASS_PHY, 1UL,
+ &combo_dev);
+ if (ret)
+ dev_warn(dev,
+ "failed to probe combo PHY: %d\n",
+ ret);
+ }
+
+ if (!k1_phy_rterm_valid()) {
+ dev_err(dev,
+ "calibration value not available; "
+ "is the combo PHY enabled?\n");
+ return -ENOENT;
+ }
+
+ ret = clk_get_bulk(dev, &k1_phy->clks);
+ if (ret)
+ dev_warn(dev, "failed to get clocks: %d\n", ret);
+
+ ret = reset_get_bulk(dev, &k1_phy->rst);
+ if (ret)
+ dev_warn(dev, "failed to get resets: %d\n", ret);
+ }
+
+ dev_dbg(dev, "probed (combo=%d lanes=%u rterm=0x%02x)\n",
+ is_combo, lanes, k1_phy_rterm);
+
+ return 0;
+}
+
+static const struct udevice_id k1_pcie_phy_ids[] = {
+ { .compatible = "spacemit,k1-combo-phy", .data = 1UL },
+ { .compatible = "spacemit,k1-pcie-phy", .data = 0UL },
+ { }
+};
+
+U_BOOT_DRIVER(k1_pcie_phy) = {
+ .name = "k1_pcie_phy",
+ .id = UCLASS_PHY,
+ .of_match = k1_pcie_phy_ids,
+ .ops = &k1_pcie_phy_ops,
+ .probe = k1_pcie_phy_probe,
+ .priv_auto = sizeof(struct k1_pcie_phy),
+};
--
2.51.0
More information about the U-Boot
mailing list