From 5e4192952cfb2110aaba1b03a3c66c84d74a27db Mon Sep 17 00:00:00 2001
From: Lei Wei <quic_leiwei@quicinc.com>
Date: Mon, 29 Jan 2024 11:39:36 +0800
Subject: [PATCH 11/50] net: pcs: Add driver for Qualcomm IPQ UNIPHY PCS

The UNIPHY hardware block in Qualcomm's IPQ SoC based boards enables
PCS and XPCS functions, and helps in interfacing the Ethernet MAC in
IPQ SoC to external PHYs.

This patch adds the PCS driver support for the UNIPHY hardware used in
IPQ SoC based boards. Support for SGMII/QSGMII/PSGMII and USXGMII
interface modes are added in the driver.

Change-Id: Id2c8f993f121098f7b02186b53770b75bb539a93
Signed-off-by: Lei Wei <quic_leiwei@quicinc.com>
---
 MAINTAINERS                             |   8 +
 drivers/net/pcs/Kconfig                 |  10 +
 drivers/net/pcs/Makefile                |   1 +
 drivers/net/pcs/pcs-qcom-ipq-uniphy.c   | 943 ++++++++++++++++++++++++
 include/linux/pcs/pcs-qcom-ipq-uniphy.h |  13 +
 5 files changed, 975 insertions(+)
 create mode 100644 drivers/net/pcs/pcs-qcom-ipq-uniphy.c
 create mode 100644 include/linux/pcs/pcs-qcom-ipq-uniphy.h

# diff --git a/MAINTAINERS b/MAINTAINERS
# index 8836b2200acf..1940990ae342 100644
# --- a/MAINTAINERS
# +++ b/MAINTAINERS
# @@ -18900,6 +18900,14 @@ S:	Maintained
#  F:	Documentation/devicetree/bindings/regulator/vqmmc-ipq4019-regulator.yaml
#  F:	drivers/regulator/vqmmc-ipq4019-regulator.c
 
# +QUALCOMM IPQ Ethernet UNIPHY PCS DRIVER
# +M:	Lei Wei <quic_leiwei@quicinc.com>
# +L:	netdev@vger.kernel.org
# +S:	Supported
# +F:	Documentation/devicetree/bindings/net/pcs/qcom,ipq-uniphy.yaml
# +F:	drivers/net/pcs/pcs-qcom-ipq-uniphy.c
# +F:	include/linux/pcs/pcs-qcom-ipq-uniphy.h
# +
#  QUALCOMM NAND CONTROLLER DRIVER
#  M:	Manivannan Sadhasivam <manivannan.sadhasivam@linaro.org>
#  L:	linux-mtd@lists.infradead.org
--- a/drivers/net/pcs/Kconfig
+++ b/drivers/net/pcs/Kconfig
@@ -44,4 +44,14 @@ config PCS_RZN1_MIIC
 	  on RZ/N1 SoCs. This PCS converts MII to RMII/RGMII or can be set in
 	  pass-through mode for MII.
 
+config PCS_QCOM_IPQ_UNIPHY
+	tristate "Qualcomm IPQ UNIPHY PCS driver"
+	depends on OF && (ARCH_QCOM || COMPILE_TEST)
+	depends on HAS_IOMEM
+	help
+	  This module provides PCS driver for Qualcomm IPQ UNIPHY that is
+	  available on Qualcomm IPQ SoCs. The UNIPHY provides both PCS and XPCS
+	  functions to support different interface modes for MAC to PHY connections.
+	  These modes help to support various combination of ethernet switch/PHY on
+	  IPQ SoC based boards.
 endmenu
--- a/drivers/net/pcs/Makefile
+++ b/drivers/net/pcs/Makefile
@@ -8,3 +8,4 @@ obj-$(CONFIG_PCS_LYNX)		+= pcs-lynx.o
 obj-$(CONFIG_PCS_MTK_LYNXI)	+= pcs-mtk-lynxi.o
 obj-$(CONFIG_PCS_RZN1_MIIC)	+= pcs-rzn1-miic.o
 obj-$(CONFIG_PCS_MTK_USXGMII)	+= pcs-mtk-usxgmii.o
+obj-$(CONFIG_PCS_QCOM_IPQ_UNIPHY)	+= pcs-qcom-ipq-uniphy.o
--- /dev/null
+++ b/drivers/net/pcs/pcs-qcom-ipq-uniphy.c
@@ -0,0 +1,943 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2024 Qualcomm Innovation Center, Inc. All rights reserved.
+ *
+ */
+
+#include <linux/clk.h>
+#include <linux/clk-provider.h>
+#include <linux/device.h>
+#include <linux/of.h>
+#include <linux/of_platform.h>
+#include <linux/pcs/pcs-qcom-ipq-uniphy.h>
+#include <linux/phylink.h>
+#include <linux/platform_device.h>
+#include <linux/reset.h>
+
+/* Maximum PCS channel numbers, For PSGMII it has 5 channels */
+#define PCS_MAX_CHANNELS		5
+
+#define PCS_CALIBRATION			0x1e0
+#define PCS_CALIBRATION_DONE		BIT(7)
+
+#define PCS_MODE_CTRL			0x46c
+#define PCS_MODE_SEL_MASK		GENMASK(12, 8)
+#define PCS_MODE_SGMII			FIELD_PREP(PCS_MODE_SEL_MASK, 0x4)
+#define PCS_MODE_QSGMII			FIELD_PREP(PCS_MODE_SEL_MASK, 0x1)
+#define PCS_MODE_PSGMII			FIELD_PREP(PCS_MODE_SEL_MASK, 0x2)
+#define PCS_MODE_XPCS			FIELD_PREP(PCS_MODE_SEL_MASK, 0x10)
+#define PCS_MODE_AN_MODE		BIT(0)
+
+#define PCS_CHANNEL_CTRL(x)		(0x480 + 0x18 * (x))
+#define PCS_CHANNEL_ADPT_RESET		BIT(11)
+#define PCS_CHANNEL_FORCE_MODE		BIT(3)
+#define PCS_CHANNEL_SPEED_MASK		GENMASK(2, 1)
+#define PCS_CHANNEL_SPEED_1000		FIELD_PREP(PCS_CHANNEL_SPEED_MASK, 0x2)
+#define PCS_CHANNEL_SPEED_100		FIELD_PREP(PCS_CHANNEL_SPEED_MASK, 0x1)
+#define PCS_CHANNEL_SPEED_10		FIELD_PREP(PCS_CHANNEL_SPEED_MASK, 0x0)
+
+#define PCS_CHANNEL_STS(x)		(0x488 + 0x18 * (x))
+#define PCS_CHANNEL_LINK_STS		BIT(7)
+#define PCS_CHANNEL_STS_DUPLEX_FULL	BIT(6)
+#define PCS_CHANNEL_STS_SPEED_MASK	GENMASK(5, 4)
+#define PCS_CHANNEL_STS_SPEED_10	0
+#define PCS_CHANNEL_STS_SPEED_100	1
+#define PCS_CHANNEL_STS_SPEED_1000	2
+#define PCS_CHANNEL_STS_PAUSE_TX_EN	BIT(1)
+#define PCS_CHANNEL_STS_PAUSE_RX_EN	BIT(0)
+
+#define PCS_PLL_RESET			0x780
+#define PCS_ANA_SW_RESET		BIT(6)
+
+#define XPCS_INDIRECT_ADDR		0x8000
+#define XPCS_INDIRECT_AHB_ADDR		0x83fc
+#define XPCS_INDIRECT_ADDR_H		GENMASK(20, 8)
+#define XPCS_INDIRECT_ADDR_L		GENMASK(7, 0)
+#define XPCS_INDIRECT_DATA_ADDR(reg)	(FIELD_PREP(GENMASK(15, 10), 0x20) | \
+					 FIELD_PREP(GENMASK(9, 2), \
+					 FIELD_GET(XPCS_INDIRECT_ADDR_L, reg)))
+
+#define XPCS_DIG_CTRL			0x38000
+#define XPCS_USXG_ADPT_RESET		BIT(10)
+#define XPCS_USXG_EN			BIT(9)
+
+#define XPCS_MII_CTRL			0x1f0000
+#define XPCS_MII_AN_EN			BIT(12)
+#define XPCS_DUPLEX_FULL		BIT(8)
+#define XPCS_SPEED_MASK			(BIT(13) | BIT(6) | BIT(5))
+#define XPCS_SPEED_10000		(BIT(13) | BIT(6))
+#define XPCS_SPEED_5000			(BIT(13) | BIT(5))
+#define XPCS_SPEED_2500			BIT(5)
+#define XPCS_SPEED_1000			BIT(6)
+#define XPCS_SPEED_100			BIT(13)
+#define XPCS_SPEED_10			0
+
+#define XPCS_MII_AN_CTRL		0x1f8001
+#define XPCS_MII_AN_8BIT		BIT(8)
+
+#define XPCS_MII_AN_INTR_STS		0x1f8002
+#define XPCS_USXG_AN_LINK_STS		BIT(14)
+#define XPCS_USXG_AN_DUPLEX_FULL	BIT(13)
+#define XPCS_USXG_AN_SPEED_MASK		GENMASK(12, 10)
+#define XPCS_USXG_AN_SPEED_10		0
+#define XPCS_USXG_AN_SPEED_100		1
+#define XPCS_USXG_AN_SPEED_1000		2
+#define XPCS_USXG_AN_SPEED_2500		4
+#define XPCS_USXG_AN_SPEED_5000		5
+#define XPCS_USXG_AN_SPEED_10000	3
+
+/* UNIPHY PCS RAW clock ID */
+enum {
+	PCS_RAW_RX_CLK = 0,
+	PCS_RAW_TX_CLK,
+	PCS_RAW_CLK_MAX
+};
+
+/* UNIPHY PCS raw clock */
+struct ipq_unipcs_raw_clk {
+	struct clk_hw hw;
+	unsigned long rate;
+};
+
+/* UNIPHY PCS clock ID */
+enum {
+	PCS_SYS_CLK,
+	PCS_AHB_CLK,
+	PCS_CLK_MAX
+};
+
+/* UNIPHY PCS reset ID */
+enum {
+	PCS_SYS_RESET,
+	PCS_AHB_RESET,
+	XPCS_RESET,
+	PCS_RESET_MAX
+};
+
+/* UNIPHY PCS clock name */
+static const char *const pcs_clock_name[PCS_CLK_MAX] = {
+	"sys",
+	"ahb",
+};
+
+/* UNIPHY PCS reset name */
+static const char *const pcs_reset_name[PCS_RESET_MAX] = {
+	"sys",
+	"ahb",
+	"xpcs",
+};
+
+/* UNIPHY PCS channel clock ID */
+enum {
+	PCS_CH_RX_CLK,
+	PCS_CH_TX_CLK,
+	PCS_CH_CLK_MAX
+};
+
+/* UNIPHY PCS channel clock name */
+static const char *const pcs_ch_clock_name[PCS_CH_CLK_MAX] = {
+	"ch_rx",
+	"ch_tx",
+};
+
+/* UNIPHY PCS private data instance */
+struct ipq_uniphy_pcs {
+	void __iomem *base;
+	struct device *dev;
+	phy_interface_t interface;
+	struct mutex shared_lock; /* Lock to protect shared config */
+	struct clk *clk[PCS_CLK_MAX];
+	struct reset_control *reset[PCS_RESET_MAX];
+	struct ipq_unipcs_raw_clk raw_clk[PCS_RAW_CLK_MAX];
+};
+
+/* UNIPHY PCS channel private data instance */
+struct ipq_uniphy_pcs_ch {
+	struct ipq_uniphy_pcs *qunipcs;
+	struct phylink_pcs pcs;
+	int channel;
+	struct clk *clk[PCS_CH_CLK_MAX];
+};
+
+#define to_unipcs_raw_clk(_hw)		\
+	container_of(_hw, struct ipq_unipcs_raw_clk, hw)
+#define phylink_pcs_to_unipcs(_pcs)	\
+	container_of(_pcs, struct ipq_uniphy_pcs_ch, pcs)
+
+static unsigned long ipq_unipcs_raw_clk_recalc_rate(struct clk_hw *hw,
+						    unsigned long parent_rate)
+{
+	struct ipq_unipcs_raw_clk *raw_clk = to_unipcs_raw_clk(hw);
+
+	return raw_clk->rate;
+}
+
+static int ipq_unipcs_raw_clk_determine_rate(struct clk_hw *hw,
+					     struct clk_rate_request *req)
+{
+	switch (req->rate) {
+	case 125000000:
+	case 312500000:
+		return 0;
+	default:
+		return -EINVAL;
+	}
+}
+
+static int ipq_unipcs_raw_clk_set_rate(struct clk_hw *hw,
+				       unsigned long rate,
+				       unsigned long parent_rate)
+{
+	struct ipq_unipcs_raw_clk *raw_clk = to_unipcs_raw_clk(hw);
+
+	switch (rate) {
+	case 125000000:
+	case 312500000:
+		raw_clk->rate = rate;
+		return 0;
+	default:
+		return -EINVAL;
+	}
+}
+
+static const struct clk_ops ipq_unipcs_raw_clk_ops = {
+	.recalc_rate = ipq_unipcs_raw_clk_recalc_rate,
+	.determine_rate = ipq_unipcs_raw_clk_determine_rate,
+	.set_rate = ipq_unipcs_raw_clk_set_rate,
+};
+
+static u32 ipq_unipcs_reg_read32(struct ipq_uniphy_pcs *qunipcs, u32 reg)
+{
+	/* PCS use direct AHB access while XPCS use indirect AHB access */
+	if (reg >= XPCS_INDIRECT_ADDR) {
+		writel(FIELD_GET(XPCS_INDIRECT_ADDR_H, reg),
+		       qunipcs->base + XPCS_INDIRECT_AHB_ADDR);
+		return readl(qunipcs->base + XPCS_INDIRECT_DATA_ADDR(reg));
+	} else {
+		return readl(qunipcs->base + reg);
+	}
+}
+
+static void ipq_unipcs_reg_write32(struct ipq_uniphy_pcs *qunipcs,
+				   u32 reg, u32 val)
+{
+	if (reg >= XPCS_INDIRECT_ADDR) {
+		writel(FIELD_GET(XPCS_INDIRECT_ADDR_H, reg),
+		       qunipcs->base + XPCS_INDIRECT_AHB_ADDR);
+		writel(val, qunipcs->base + XPCS_INDIRECT_DATA_ADDR(reg));
+	} else {
+		writel(val, qunipcs->base + reg);
+	}
+}
+
+static void ipq_unipcs_reg_modify32(struct ipq_uniphy_pcs *qunipcs,
+				    u32 reg, u32 mask, u32 set)
+{
+	u32 val;
+
+	val = ipq_unipcs_reg_read32(qunipcs, reg);
+	val &= ~mask;
+	val |= set;
+	ipq_unipcs_reg_write32(qunipcs, reg, val);
+}
+
+static void ipq_unipcs_get_state_sgmii(struct ipq_uniphy_pcs *qunipcs,
+				       int channel,
+				       struct phylink_link_state *state)
+{
+	u32 val;
+
+	val = ipq_unipcs_reg_read32(qunipcs, PCS_CHANNEL_STS(channel));
+
+	state->link = !!(val & PCS_CHANNEL_LINK_STS);
+
+	if (!state->link)
+		return;
+
+	switch (FIELD_GET(PCS_CHANNEL_STS_SPEED_MASK, val)) {
+	case PCS_CHANNEL_STS_SPEED_1000:
+		state->speed = SPEED_1000;
+		break;
+	case PCS_CHANNEL_STS_SPEED_100:
+		state->speed = SPEED_100;
+		break;
+	case PCS_CHANNEL_STS_SPEED_10:
+		state->speed = SPEED_10;
+		break;
+	default:
+		return;
+	}
+
+	if (val & PCS_CHANNEL_STS_DUPLEX_FULL)
+		state->duplex = DUPLEX_FULL;
+	else
+		state->duplex = DUPLEX_HALF;
+
+	if (val & PCS_CHANNEL_STS_PAUSE_TX_EN)
+		state->pause |= MLO_PAUSE_TX;
+	if (val & PCS_CHANNEL_STS_PAUSE_RX_EN)
+		state->pause |= MLO_PAUSE_RX;
+}
+
+static void ipq_unipcs_get_state_usxgmii(struct ipq_uniphy_pcs *qunipcs,
+					 struct phylink_link_state *state)
+{
+	u32 val;
+
+	val = ipq_unipcs_reg_read32(qunipcs, XPCS_MII_AN_INTR_STS);
+
+	state->link = !!(val & XPCS_USXG_AN_LINK_STS);
+
+	if (!state->link)
+		return;
+
+	switch (FIELD_GET(XPCS_USXG_AN_SPEED_MASK, val)) {
+	case XPCS_USXG_AN_SPEED_10000:
+		state->speed = SPEED_10000;
+		break;
+	case XPCS_USXG_AN_SPEED_5000:
+		state->speed = SPEED_5000;
+		break;
+	case XPCS_USXG_AN_SPEED_2500:
+		state->speed = SPEED_2500;
+		break;
+	case XPCS_USXG_AN_SPEED_1000:
+		state->speed = SPEED_1000;
+		break;
+	case XPCS_USXG_AN_SPEED_100:
+		state->speed = SPEED_100;
+		break;
+	case XPCS_USXG_AN_SPEED_10:
+		state->speed = SPEED_10;
+		break;
+	default:
+		return;
+	}
+
+	if (val & XPCS_USXG_AN_DUPLEX_FULL)
+		state->duplex = DUPLEX_FULL;
+	else
+		state->duplex = DUPLEX_HALF;
+}
+
+static int ipq_unipcs_config_mode(struct ipq_uniphy_pcs *qunipcs,
+				  phy_interface_t interface)
+{
+	unsigned long rate = 0;
+	u32 val;
+	int ret;
+
+	/* Assert XPCS reset */
+	reset_control_assert(qunipcs->reset[XPCS_RESET]);
+
+	/* Config PCS interface mode */
+	switch (interface) {
+	case PHY_INTERFACE_MODE_SGMII:
+		rate = 125000000;
+		/* Select Qualcomm SGMII AN mode */
+		ipq_unipcs_reg_modify32(qunipcs, PCS_MODE_CTRL,
+					PCS_MODE_SEL_MASK | PCS_MODE_AN_MODE,
+					PCS_MODE_SGMII);
+		break;
+	case PHY_INTERFACE_MODE_QSGMII:
+		rate = 125000000;
+		/* Select Qualcomm SGMII AN mode */
+		ipq_unipcs_reg_modify32(qunipcs, PCS_MODE_CTRL,
+					PCS_MODE_SEL_MASK | PCS_MODE_AN_MODE,
+					PCS_MODE_QSGMII);
+		break;
+	case PHY_INTERFACE_MODE_PSGMII:
+		rate = 125000000;
+		/* Select Qualcomm SGMII AN mode */
+		ipq_unipcs_reg_modify32(qunipcs, PCS_MODE_CTRL,
+					PCS_MODE_SEL_MASK | PCS_MODE_AN_MODE,
+					PCS_MODE_PSGMII);
+		break;
+	case PHY_INTERFACE_MODE_USXGMII:
+		rate = 312500000;
+		ipq_unipcs_reg_modify32(qunipcs, PCS_MODE_CTRL,
+					PCS_MODE_SEL_MASK,
+					PCS_MODE_XPCS);
+		break;
+	default:
+		dev_err(qunipcs->dev,
+			"interface %s not supported\n", phy_modes(interface));
+		return -EOPNOTSUPP;
+	}
+
+	/* PCS PLL reset */
+	ipq_unipcs_reg_modify32(qunipcs, PCS_PLL_RESET, PCS_ANA_SW_RESET, 0);
+	fsleep(10000);
+	ipq_unipcs_reg_modify32(qunipcs, PCS_PLL_RESET,
+				PCS_ANA_SW_RESET, PCS_ANA_SW_RESET);
+
+	/* Wait for calibration completion */
+	ret = read_poll_timeout(ipq_unipcs_reg_read32, val,
+				val & PCS_CALIBRATION_DONE,
+				1000, 100000, true,
+				qunipcs, PCS_CALIBRATION);
+	if (ret) {
+		dev_err(qunipcs->dev, "UNIPHY PCS calibration timed-out\n");
+		return ret;
+	}
+
+	/* Configure raw clock rate */
+	clk_set_rate(qunipcs->raw_clk[PCS_RAW_RX_CLK].hw.clk, rate);
+	clk_set_rate(qunipcs->raw_clk[PCS_RAW_TX_CLK].hw.clk, rate);
+
+	return 0;
+}
+
+static int ipq_unipcs_config_sgmii(struct ipq_uniphy_pcs *qunipcs,
+				   int channel,
+				   unsigned int neg_mode,
+				   phy_interface_t interface)
+{
+	int ret;
+
+	/* PCS configurations shared by multi channels should be
+	 * configured for only once.
+	 */
+	if (phy_interface_num_ports(interface) > 1)
+		mutex_lock(&qunipcs->shared_lock);
+
+	if (qunipcs->interface != interface) {
+		ret = ipq_unipcs_config_mode(qunipcs, interface);
+		if (ret)
+			goto err;
+
+		qunipcs->interface = interface;
+	}
+
+	if (phy_interface_num_ports(interface) > 1)
+		mutex_unlock(&qunipcs->shared_lock);
+
+	/* In-band autoneg mode is enabled by default for each PCS channel */
+	if (neg_mode == PHYLINK_PCS_NEG_INBAND_ENABLED)
+		return 0;
+
+	/* Force speed mode */
+	ipq_unipcs_reg_modify32(qunipcs, PCS_CHANNEL_CTRL(channel),
+				PCS_CHANNEL_FORCE_MODE, PCS_CHANNEL_FORCE_MODE);
+
+	return 0;
+
+err:
+	if (phy_interface_num_ports(interface) > 1)
+		mutex_unlock(&qunipcs->shared_lock);
+
+	return ret;
+}
+
+static int ipq_unipcs_config_usxgmii(struct ipq_uniphy_pcs *qunipcs,
+				     unsigned int neg_mode,
+				     phy_interface_t interface)
+{
+	int ret;
+
+	if (qunipcs->interface != interface) {
+		ret = ipq_unipcs_config_mode(qunipcs, interface);
+		if (ret)
+			return ret;
+
+		/* Deassert XPCS and configure XPCS USXGMII */
+		reset_control_deassert(qunipcs->reset[XPCS_RESET]);
+
+		ipq_unipcs_reg_modify32(qunipcs, XPCS_DIG_CTRL,
+					XPCS_USXG_EN, XPCS_USXG_EN);
+
+		if (neg_mode == PHYLINK_PCS_NEG_INBAND_ENABLED) {
+			ipq_unipcs_reg_modify32(qunipcs, XPCS_MII_AN_CTRL,
+						XPCS_MII_AN_8BIT,
+						XPCS_MII_AN_8BIT);
+
+			ipq_unipcs_reg_modify32(qunipcs, XPCS_MII_CTRL,
+						XPCS_MII_AN_EN, XPCS_MII_AN_EN);
+		}
+
+		qunipcs->interface = interface;
+	}
+
+	return 0;
+}
+
+static unsigned long ipq_unipcs_clock_rate_get_gmii(int speed)
+{
+	unsigned long rate = 0;
+
+	switch (speed) {
+	case SPEED_1000:
+		rate = 125000000;
+		break;
+	case SPEED_100:
+		rate = 25000000;
+		break;
+	case SPEED_10:
+		rate = 2500000;
+		break;
+	default:
+		break;
+	}
+
+	return rate;
+}
+
+static unsigned long ipq_unipcs_clock_rate_get_xgmii(int speed)
+{
+	unsigned long rate = 0;
+
+	switch (speed) {
+	case SPEED_10000:
+		rate = 312500000;
+		break;
+	case SPEED_5000:
+		rate = 156250000;
+		break;
+	case SPEED_2500:
+		rate = 78125000;
+		break;
+	case SPEED_1000:
+		rate = 125000000;
+		break;
+	case SPEED_100:
+		rate = 12500000;
+		break;
+	case SPEED_10:
+		rate = 1250000;
+		break;
+	default:
+		break;
+	}
+
+	return rate;
+}
+
+static void
+ipq_unipcs_link_up_clock_rate_set(struct ipq_uniphy_pcs_ch *qunipcs_ch,
+				  phy_interface_t interface,
+				  int speed)
+{
+	struct ipq_uniphy_pcs *qunipcs = qunipcs_ch->qunipcs;
+	unsigned long rate = 0;
+
+	switch (interface) {
+	case PHY_INTERFACE_MODE_SGMII:
+	case PHY_INTERFACE_MODE_QSGMII:
+	case PHY_INTERFACE_MODE_PSGMII:
+		rate = ipq_unipcs_clock_rate_get_gmii(speed);
+		break;
+	case PHY_INTERFACE_MODE_USXGMII:
+		rate = ipq_unipcs_clock_rate_get_xgmii(speed);
+		break;
+	default:
+		dev_err(qunipcs->dev,
+			"interface %s not supported\n", phy_modes(interface));
+		return;
+	}
+
+	if (rate == 0) {
+		dev_err(qunipcs->dev, "Invalid PCS clock rate\n");
+		return;
+	}
+
+	clk_set_rate(qunipcs_ch->clk[PCS_CH_RX_CLK], rate);
+	clk_set_rate(qunipcs_ch->clk[PCS_CH_TX_CLK], rate);
+	fsleep(10000);
+}
+
+static void ipq_unipcs_link_up_config_sgmii(struct ipq_uniphy_pcs *qunipcs,
+					    int channel,
+					    unsigned int neg_mode,
+					    int speed)
+{
+	/* No need to config PCS speed if in-band autoneg is enabled */
+	if (neg_mode == PHYLINK_PCS_NEG_INBAND_ENABLED)
+		goto pcs_adapter_reset;
+
+	/* PCS speed set for force mode */
+	switch (speed) {
+	case SPEED_1000:
+		ipq_unipcs_reg_modify32(qunipcs, PCS_CHANNEL_CTRL(channel),
+					PCS_CHANNEL_SPEED_MASK,
+					PCS_CHANNEL_SPEED_1000);
+		break;
+	case SPEED_100:
+		ipq_unipcs_reg_modify32(qunipcs, PCS_CHANNEL_CTRL(channel),
+					PCS_CHANNEL_SPEED_MASK,
+					PCS_CHANNEL_SPEED_100);
+		break;
+	case SPEED_10:
+		ipq_unipcs_reg_modify32(qunipcs, PCS_CHANNEL_CTRL(channel),
+					PCS_CHANNEL_SPEED_MASK,
+					PCS_CHANNEL_SPEED_10);
+		break;
+	default:
+		dev_err(qunipcs->dev, "Force speed %d not supported\n", speed);
+		return;
+	}
+
+pcs_adapter_reset:
+	/* PCS channel adapter reset */
+	ipq_unipcs_reg_modify32(qunipcs, PCS_CHANNEL_CTRL(channel),
+				PCS_CHANNEL_ADPT_RESET,
+				0);
+	ipq_unipcs_reg_modify32(qunipcs, PCS_CHANNEL_CTRL(channel),
+				PCS_CHANNEL_ADPT_RESET,
+				PCS_CHANNEL_ADPT_RESET);
+}
+
+static void ipq_unipcs_link_up_config_usxgmii(struct ipq_uniphy_pcs *qunipcs,
+					      int speed)
+{
+	u32 val;
+
+	switch (speed) {
+	case SPEED_10000:
+		val = XPCS_SPEED_10000;
+		break;
+	case SPEED_5000:
+		val = XPCS_SPEED_5000;
+		break;
+	case SPEED_2500:
+		val = XPCS_SPEED_2500;
+		break;
+	case SPEED_1000:
+		val = XPCS_SPEED_1000;
+		break;
+	case SPEED_100:
+		val = XPCS_SPEED_100;
+		break;
+	case SPEED_10:
+		val = XPCS_SPEED_10;
+		break;
+	default:
+		return;
+	}
+
+	/* USXGMII only support full duplex mode */
+	val |= XPCS_DUPLEX_FULL;
+
+	/* Config XPCS speed */
+	ipq_unipcs_reg_modify32(qunipcs, XPCS_MII_CTRL,
+				XPCS_SPEED_MASK | XPCS_DUPLEX_FULL,
+				val);
+
+	/* XPCS adapter reset */
+	ipq_unipcs_reg_modify32(qunipcs, XPCS_DIG_CTRL,
+				XPCS_USXG_ADPT_RESET,
+				XPCS_USXG_ADPT_RESET);
+}
+
+static void ipq_unipcs_get_state(struct phylink_pcs *pcs,
+				 struct phylink_link_state *state)
+{
+	struct ipq_uniphy_pcs_ch *qunipcs_ch = phylink_pcs_to_unipcs(pcs);
+	struct ipq_uniphy_pcs *qunipcs = qunipcs_ch->qunipcs;
+	int channel = qunipcs_ch->channel;
+
+	switch (state->interface) {
+	case PHY_INTERFACE_MODE_SGMII:
+	case PHY_INTERFACE_MODE_QSGMII:
+	case PHY_INTERFACE_MODE_PSGMII:
+		ipq_unipcs_get_state_sgmii(qunipcs, channel, state);
+		break;
+	case PHY_INTERFACE_MODE_USXGMII:
+		ipq_unipcs_get_state_usxgmii(qunipcs, state);
+		break;
+	default:
+		break;
+	}
+
+	dev_dbg(qunipcs->dev,
+		"mode=%s/%s/%s link=%u\n",
+		phy_modes(state->interface),
+		phy_speed_to_str(state->speed),
+		phy_duplex_to_str(state->duplex),
+		state->link);
+}
+
+static int ipq_unipcs_config(struct phylink_pcs *pcs,
+			     unsigned int neg_mode,
+			     phy_interface_t interface,
+			     const unsigned long *advertising,
+			     bool permit)
+{
+	struct ipq_uniphy_pcs_ch *qunipcs_ch = phylink_pcs_to_unipcs(pcs);
+	struct ipq_uniphy_pcs *qunipcs = qunipcs_ch->qunipcs;
+	int channel = qunipcs_ch->channel;
+
+	switch (interface) {
+	case PHY_INTERFACE_MODE_SGMII:
+	case PHY_INTERFACE_MODE_QSGMII:
+	case PHY_INTERFACE_MODE_PSGMII:
+		return ipq_unipcs_config_sgmii(qunipcs, channel,
+					       neg_mode, interface);
+	case PHY_INTERFACE_MODE_USXGMII:
+		return ipq_unipcs_config_usxgmii(qunipcs,
+						 neg_mode, interface);
+	default:
+		dev_err(qunipcs->dev,
+			"interface %s not supported\n", phy_modes(interface));
+		return -EOPNOTSUPP;
+	};
+}
+
+static void ipq_unipcs_link_up(struct phylink_pcs *pcs,
+			       unsigned int neg_mode,
+			       phy_interface_t interface,
+			       int speed, int duplex)
+{
+	struct ipq_uniphy_pcs_ch *qunipcs_ch = phylink_pcs_to_unipcs(pcs);
+	struct ipq_uniphy_pcs *qunipcs = qunipcs_ch->qunipcs;
+	int channel = qunipcs_ch->channel;
+
+	/* Configure PCS channel interface clock rate */
+	ipq_unipcs_link_up_clock_rate_set(qunipcs_ch, interface, speed);
+
+	/* Configure PCS speed and reset PCS adapter */
+	switch (interface) {
+	case PHY_INTERFACE_MODE_SGMII:
+	case PHY_INTERFACE_MODE_QSGMII:
+	case PHY_INTERFACE_MODE_PSGMII:
+		ipq_unipcs_link_up_config_sgmii(qunipcs, channel,
+						neg_mode, speed);
+		break;
+	case PHY_INTERFACE_MODE_USXGMII:
+		ipq_unipcs_link_up_config_usxgmii(qunipcs, speed);
+		break;
+	default:
+		dev_err(qunipcs->dev,
+			"interface %s not supported\n", phy_modes(interface));
+		break;
+	}
+}
+
+static const struct phylink_pcs_ops ipq_unipcs_phylink_ops = {
+	.pcs_get_state = ipq_unipcs_get_state,
+	.pcs_config = ipq_unipcs_config,
+	.pcs_link_up = ipq_unipcs_link_up,
+};
+
+/**
+ * ipq_unipcs_create() - Create Qualcomm IPQ UNIPHY PCS
+ * @np: Device tree node to the PCS
+ *
+ * Description: Create a phylink PCS instance for a PCS node @np.
+ *
+ * Return: A pointer to the phylink PCS instance or an error-pointer value.
+ */
+struct phylink_pcs *ipq_unipcs_create(struct device_node *np)
+{
+	struct ipq_uniphy_pcs_ch *qunipcs_ch;
+	struct ipq_uniphy_pcs *qunipcs;
+	struct device_node *uniphy_np;
+	struct platform_device *pdev;
+	u32 channel;
+	int i, j;
+
+	if (!of_device_is_available(np))
+		return ERR_PTR(-ENODEV);
+
+	if (of_property_read_u32(np, "reg", &channel))
+		return ERR_PTR(-EINVAL);
+
+	if (channel >= PCS_MAX_CHANNELS)
+		return ERR_PTR(-EINVAL);
+
+	uniphy_np = of_get_parent(np);
+	if (!uniphy_np)
+		return ERR_PTR(-ENODEV);
+
+	if (!of_device_is_available(uniphy_np)) {
+		of_node_put(uniphy_np);
+		return ERR_PTR(-ENODEV);
+	}
+
+	pdev = of_find_device_by_node(uniphy_np);
+	of_node_put(uniphy_np);
+	if (!pdev)
+		return ERR_PTR(-ENODEV);
+
+	qunipcs = platform_get_drvdata(pdev);
+	platform_device_put(pdev);
+
+	/* If probe is not yet completed, return DEFER to
+	 * the dependent driver.
+	 */
+	if (!qunipcs)
+		return ERR_PTR(-EPROBE_DEFER);
+
+	qunipcs_ch = kzalloc(sizeof(*qunipcs_ch), GFP_KERNEL);
+	if (!qunipcs_ch)
+		return ERR_PTR(-ENOMEM);
+
+	qunipcs_ch->qunipcs = qunipcs;
+	qunipcs_ch->channel = channel;
+	qunipcs_ch->pcs.ops = &ipq_unipcs_phylink_ops;
+	qunipcs_ch->pcs.neg_mode = true;
+	qunipcs_ch->pcs.poll = true;
+
+	for (i = 0; i < PCS_CH_CLK_MAX; i++) {
+		qunipcs_ch->clk[i] = of_clk_get_by_name(np,
+							pcs_ch_clock_name[i]);
+		if (IS_ERR(qunipcs_ch->clk[i])) {
+			dev_err(qunipcs->dev,
+				"Failed to get PCS channel %d clock ID %s\n",
+				channel, pcs_ch_clock_name[i]);
+			goto free_pcs;
+		}
+
+		clk_prepare_enable(qunipcs_ch->clk[i]);
+	}
+
+	return &qunipcs_ch->pcs;
+
+free_pcs:
+	for (j = 0; j < i; j++) {
+		clk_disable_unprepare(qunipcs_ch->clk[j]);
+		clk_put(qunipcs_ch->clk[j]);
+	}
+
+	kfree(qunipcs_ch);
+	return ERR_PTR(-ENODEV);
+}
+EXPORT_SYMBOL(ipq_unipcs_create);
+
+/**
+ * ipq_unipcs_destroy() - Destroy Qualcomm IPQ UNIPHY PCS
+ * @pcs: PCS instance
+ *
+ * Description: Destroy a phylink PCS instance.
+ */
+void ipq_unipcs_destroy(struct phylink_pcs *pcs)
+{
+	struct ipq_uniphy_pcs_ch *qunipcs_ch;
+	int i;
+
+	if (!pcs)
+		return;
+
+	qunipcs_ch = phylink_pcs_to_unipcs(pcs);
+
+	for (i = 0; i < PCS_CH_CLK_MAX; i++) {
+		clk_disable_unprepare(qunipcs_ch->clk[i]);
+		clk_put(qunipcs_ch->clk[i]);
+	}
+
+	kfree(qunipcs_ch);
+}
+EXPORT_SYMBOL(ipq_unipcs_destroy);
+
+static int ipq_uniphy_clk_register(struct ipq_uniphy_pcs *qunipcs)
+{
+	struct ipq_unipcs_raw_clk *raw_clk;
+	struct device *dev = qunipcs->dev;
+	struct clk_hw_onecell_data *data;
+
+	struct clk_init_data init = { };
+	int i, ret;
+
+	data = devm_kzalloc(dev,
+			    struct_size(data, hws, PCS_RAW_CLK_MAX),
+			    GFP_KERNEL);
+	if (!data)
+		return -ENOMEM;
+
+	data->num = PCS_RAW_CLK_MAX;
+	for (i = 0; i < PCS_RAW_CLK_MAX; i++) {
+		ret = of_property_read_string_index(dev->of_node,
+						    "clock-output-names",
+						    i, &init.name);
+		if (ret) {
+			dev_err(dev,
+				"%pOFn: No clock-output-names\n", dev->of_node);
+			return ret;
+		}
+
+		init.ops = &ipq_unipcs_raw_clk_ops;
+		raw_clk = &qunipcs->raw_clk[i];
+
+		raw_clk->rate = 125000000;
+		raw_clk->hw.init = &init;
+
+		ret = devm_clk_hw_register(dev, &raw_clk->hw);
+		if (ret) {
+			dev_err(dev, "Failed to register UNIPHY PCS raw clock\n");
+			return ret;
+		}
+
+		data->hws[i] = &raw_clk->hw;
+	}
+
+	return devm_of_clk_add_hw_provider(dev, of_clk_hw_onecell_get, data);
+}
+
+static int ipq_uniphy_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct ipq_uniphy_pcs *priv;
+	int i, ret;
+
+	priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+	if (!priv)
+		return -ENOMEM;
+
+	priv->dev = dev;
+
+	priv->base = devm_platform_ioremap_resource(pdev, 0);
+	if (IS_ERR(priv->base))
+		return PTR_ERR(priv->base);
+
+	for (i = 0; i < PCS_CLK_MAX; i++) {
+		priv->clk[i] = devm_clk_get_optional_enabled(dev,
+							     pcs_clock_name[i]);
+
+		if (IS_ERR(priv->clk[i]))
+			dev_err(dev, "Failed to get the clock ID %s\n",
+				pcs_clock_name[i]);
+	}
+
+	for (i = 0; i < PCS_RESET_MAX; i++) {
+		priv->reset[i] =
+			devm_reset_control_get_optional_exclusive(dev,
+								  pcs_reset_name[i]);
+
+		if (IS_ERR(priv->reset[i]))
+			dev_err(dev, "Failed to get the reset ID %s\n",
+				pcs_reset_name[i]);
+	}
+
+	/* Set UNIPHY PCS system and AHB clock rate */
+	clk_set_rate(priv->clk[PCS_SYS_CLK], 24000000);
+	clk_set_rate(priv->clk[PCS_AHB_CLK], 100000000);
+
+	ret = ipq_uniphy_clk_register(priv);
+	if (ret)
+		return ret;
+
+	mutex_init(&priv->shared_lock);
+
+	platform_set_drvdata(pdev, priv);
+
+	return 0;
+}
+
+static const struct of_device_id ipq_uniphy_of_mtable[] = {
+	{ .compatible = "qcom,ipq5332-uniphy" },
+	{ .compatible = "qcom,ipq9574-uniphy" },
+	{ /* sentinel */ },
+};
+MODULE_DEVICE_TABLE(of, ipq_uniphy_of_mtable);
+
+static struct platform_driver ipq_uniphy_driver = {
+	.driver = {
+		.name = "ipq_uniphy",
+		.of_match_table = ipq_uniphy_of_mtable,
+	},
+	.probe = ipq_uniphy_probe,
+};
+module_platform_driver(ipq_uniphy_driver);
+
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("Qualcomm IPQ UNIPHY PCS driver");
+MODULE_AUTHOR("Lei Wei <quic_leiwei@quicinc.com>");
--- /dev/null
+++ b/include/linux/pcs/pcs-qcom-ipq-uniphy.h
@@ -0,0 +1,13 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) 2024 Qualcomm Innovation Center, Inc. All rights reserved.
+ *
+ */
+
+#ifndef __LINUX_PCS_QCOM_IPQ_UNIPHY_H
+#define __LINUX_PCS_QCOM_IPQ_UNIPHY_H
+
+struct phylink_pcs *ipq_unipcs_create(struct device_node *np);
+void ipq_unipcs_destroy(struct phylink_pcs *pcs);
+
+#endif /* __LINUX_PCS_QCOM_IPQ_UNIPHY_H */
