summaryrefslogtreecommitdiffstats
path: root/drivers/phy/freescale
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/freescale
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/freescale')
-rw-r--r--drivers/phy/freescale/Kconfig79
-rw-r--r--drivers/phy/freescale/Makefile10
-rw-r--r--drivers/phy/freescale/phy-fsl-imx8-mipi-dphy.c749
-rw-r--r--drivers/phy/freescale/phy-fsl-imx8m-pcie.c291
-rw-r--r--drivers/phy/freescale/phy-fsl-imx8mq-usb.c849
-rw-r--r--drivers/phy/freescale/phy-fsl-imx8qm-hsio.c629
-rw-r--r--drivers/phy/freescale/phy-fsl-imx8qm-lvds-phy.c436
-rw-r--r--drivers/phy/freescale/phy-fsl-lynx-10g.c1326
-rw-r--r--drivers/phy/freescale/phy-fsl-lynx-28g.c1224
-rw-r--r--drivers/phy/freescale/phy-fsl-lynx-core.c445
-rw-r--r--drivers/phy/freescale/phy-fsl-lynx-core.h134
-rw-r--r--drivers/phy/freescale/phy-fsl-samsung-hdmi.c746
12 files changed, 6918 insertions, 0 deletions
diff --git a/drivers/phy/freescale/Kconfig b/drivers/phy/freescale/Kconfig
new file mode 100644
index 000000000..7557b624b
--- /dev/null
+++ b/drivers/phy/freescale/Kconfig
@@ -0,0 +1,79 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+if (ARCH_MXC && ARM64) || COMPILE_TEST
+
+config PHY_FSL_IMX8MQ_USB
+ tristate "Freescale i.MX8M USB3 PHY"
+ depends on OF && HAS_IOMEM
+ depends on TYPEC || TYPEC=n
+ select GENERIC_PHY
+ select REGMAP_MMIO
+ default ARCH_MXC && ARM64
+
+config PHY_MIXEL_LVDS_PHY
+ tristate "Mixel LVDS PHY support"
+ depends on OF
+ select GENERIC_PHY
+ select REGMAP_MMIO
+ help
+ Enable this to add support for the Mixel LVDS PHY as found
+ on NXP's i.MX8qm SoC.
+
+config PHY_MIXEL_MIPI_DPHY
+ tristate "Mixel MIPI DSI PHY support"
+ depends on OF && HAS_IOMEM
+ select GENERIC_PHY
+ select GENERIC_PHY_MIPI_DPHY
+ select REGMAP_MMIO
+ help
+ Enable this to add support for the Mixel DSI PHY as found
+ on NXP's i.MX8 family of SOCs.
+
+config PHY_FSL_IMX8M_PCIE
+ tristate "Freescale i.MX8M PCIE PHY"
+ depends on OF && HAS_IOMEM
+ select GENERIC_PHY
+ help
+ Enable this to add support for the PCIE PHY as found on
+ i.MX8M family of SOCs.
+
+config PHY_FSL_IMX8QM_HSIO
+ tristate "Freescale i.MX8QM HSIO PHY"
+ depends on OF && HAS_IOMEM
+ select GENERIC_PHY
+ help
+ Enable this to add support for the HSIO PHY as found on
+ i.MX8QM family of SOCs.
+
+config PHY_FSL_SAMSUNG_HDMI_PHY
+ tristate "Samsung HDMI PHY support"
+ depends on OF && HAS_IOMEM && COMMON_CLK
+ help
+ Enable this to add support for the Samsung HDMI PHY in i.MX8MP.
+endif
+
+config PHY_FSL_LYNX_CORE
+ tristate
+
+config PHY_FSL_LYNX_10G
+ tristate "Freescale Layerscape Lynx 10G SerDes PHY support"
+ depends on OF
+ depends on ARCH_LAYERSCAPE || COMPILE_TEST
+ select FSL_GUTS
+ select GENERIC_PHY
+ select PHY_FSL_LYNX_CORE
+ help
+ Enable this to add support for the Lynx 10G SerDes PHY as found on
+ NXP's Layerscape platform such as LS1088A or LS1028A.
+
+config PHY_FSL_LYNX_28G
+ tristate "Freescale Layerscape Lynx 28G SerDes PHY support"
+ depends on OF
+ depends on ARCH_LAYERSCAPE || COMPILE_TEST
+ select GENERIC_PHY
+ select PHY_FSL_LYNX_CORE
+ help
+ Enable this to add support for the Lynx SerDes 28G PHY as
+ found on NXP's Layerscape platforms such as LX2160A.
+ Used to change the protocol running on SerDes lanes at runtime.
+ Only useful for a restricted set of Ethernet protocols.
diff --git a/drivers/phy/freescale/Makefile b/drivers/phy/freescale/Makefile
new file mode 100644
index 000000000..5b0e180d6
--- /dev/null
+++ b/drivers/phy/freescale/Makefile
@@ -0,0 +1,10 @@
+# SPDX-License-Identifier: GPL-2.0-only
+obj-$(CONFIG_PHY_FSL_IMX8MQ_USB) += phy-fsl-imx8mq-usb.o
+obj-$(CONFIG_PHY_MIXEL_LVDS_PHY) += phy-fsl-imx8qm-lvds-phy.o
+obj-$(CONFIG_PHY_MIXEL_MIPI_DPHY) += phy-fsl-imx8-mipi-dphy.o
+obj-$(CONFIG_PHY_FSL_IMX8M_PCIE) += phy-fsl-imx8m-pcie.o
+obj-$(CONFIG_PHY_FSL_IMX8QM_HSIO) += phy-fsl-imx8qm-hsio.o
+obj-$(CONFIG_PHY_FSL_LYNX_CORE) += phy-fsl-lynx-core.o
+obj-$(CONFIG_PHY_FSL_LYNX_10G) += phy-fsl-lynx-10g.o
+obj-$(CONFIG_PHY_FSL_LYNX_28G) += phy-fsl-lynx-28g.o
+obj-$(CONFIG_PHY_FSL_SAMSUNG_HDMI_PHY) += phy-fsl-samsung-hdmi.o
diff --git a/drivers/phy/freescale/phy-fsl-imx8-mipi-dphy.c b/drivers/phy/freescale/phy-fsl-imx8-mipi-dphy.c
new file mode 100644
index 000000000..0928a526e
--- /dev/null
+++ b/drivers/phy/freescale/phy-fsl-imx8-mipi-dphy.c
@@ -0,0 +1,749 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright 2017,2018 NXP
+ * Copyright 2019 Purism SPC
+ */
+
+#include <linux/bitfield.h>
+#include <linux/clk.h>
+#include <linux/clk-provider.h>
+#include <linux/delay.h>
+#include <linux/firmware/imx/ipc.h>
+#include <linux/firmware/imx/svc/misc.h>
+#include <linux/io.h>
+#include <linux/kernel.h>
+#include <linux/mfd/syscon.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/of_platform.h>
+#include <linux/phy/phy.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+#include <dt-bindings/firmware/imx/rsrc.h>
+
+/* Control and Status Registers(CSR) */
+#define PHY_CTRL 0x00
+#define CCM_MASK GENMASK(7, 5)
+#define CCM(n) FIELD_PREP(CCM_MASK, (n))
+#define CCM_1_2V 0x5
+#define CA_MASK GENMASK(4, 2)
+#define CA_3_51MA 0x4
+#define CA(n) FIELD_PREP(CA_MASK, (n))
+#define RFB BIT(1)
+#define LVDS_EN BIT(0)
+
+/* DPHY registers */
+#define DPHY_PD_DPHY 0x00
+#define DPHY_M_PRG_HS_PREPARE 0x04
+#define DPHY_MC_PRG_HS_PREPARE 0x08
+#define DPHY_M_PRG_HS_ZERO 0x0c
+#define DPHY_MC_PRG_HS_ZERO 0x10
+#define DPHY_M_PRG_HS_TRAIL 0x14
+#define DPHY_MC_PRG_HS_TRAIL 0x18
+#define DPHY_PD_PLL 0x1c
+#define DPHY_TST 0x20
+#define DPHY_CN 0x24
+#define DPHY_CM 0x28
+#define DPHY_CO 0x2c
+#define DPHY_LOCK 0x30
+#define DPHY_LOCK_BYP 0x34
+#define DPHY_REG_BYPASS_PLL 0x4C
+
+#define MBPS(x) ((x) * 1000000)
+
+#define DATA_RATE_MAX_SPEED MBPS(1500)
+#define DATA_RATE_MIN_SPEED MBPS(80)
+
+#define PLL_LOCK_SLEEP 10
+#define PLL_LOCK_TIMEOUT 1000
+
+#define CN_BUF 0xcb7a89c0
+#define CO_BUF 0x63
+#define CM(x) ( \
+ ((x) < 32) ? 0xe0 | ((x) - 16) : \
+ ((x) < 64) ? 0xc0 | ((x) - 32) : \
+ ((x) < 128) ? 0x80 | ((x) - 64) : \
+ ((x) - 128))
+#define CN(x) (((x) == 1) ? 0x1f : (((CN_BUF) >> ((x) - 1)) & 0x1f))
+#define CO(x) ((CO_BUF) >> (8 - (x)) & 0x03)
+
+/* PHY power on is active low */
+#define PWR_ON 0
+#define PWR_OFF 1
+
+#define MIN_VCO_FREQ 640000000
+#define MAX_VCO_FREQ 1500000000
+
+#define MIN_LVDS_REFCLK_FREQ 24000000
+#define MAX_LVDS_REFCLK_FREQ 150000000
+
+enum mixel_dphy_devtype {
+ MIXEL_IMX8MQ,
+ MIXEL_IMX8QXP,
+};
+
+struct mixel_dphy_devdata {
+ u8 reg_tx_rcal;
+ u8 reg_auto_pd_en;
+ u8 reg_rxlprp;
+ u8 reg_rxcdrp;
+ u8 reg_rxhs_settle;
+ bool is_combo; /* MIPI DPHY and LVDS PHY combo */
+};
+
+static const struct mixel_dphy_devdata mixel_dphy_devdata[] = {
+ [MIXEL_IMX8MQ] = {
+ .reg_tx_rcal = 0x38,
+ .reg_auto_pd_en = 0x3c,
+ .reg_rxlprp = 0x40,
+ .reg_rxcdrp = 0x44,
+ .reg_rxhs_settle = 0x48,
+ .is_combo = false,
+ },
+ [MIXEL_IMX8QXP] = {
+ .is_combo = true,
+ },
+};
+
+struct mixel_dphy_cfg {
+ /* DPHY PLL parameters */
+ u32 cm;
+ u32 cn;
+ u32 co;
+ /* DPHY register values */
+ u8 mc_prg_hs_prepare;
+ u8 m_prg_hs_prepare;
+ u8 mc_prg_hs_zero;
+ u8 m_prg_hs_zero;
+ u8 mc_prg_hs_trail;
+ u8 m_prg_hs_trail;
+ u8 rxhs_settle;
+};
+
+struct mixel_dphy_priv {
+ struct mixel_dphy_cfg cfg;
+ struct regmap *regmap;
+ struct regmap *lvds_regmap;
+ struct clk *phy_ref_clk;
+ const struct mixel_dphy_devdata *devdata;
+ struct imx_sc_ipc *ipc_handle;
+ bool is_slave;
+ int id;
+};
+
+static const struct regmap_config mixel_dphy_regmap_config = {
+ .reg_bits = 8,
+ .val_bits = 32,
+ .reg_stride = 4,
+ .max_register = DPHY_REG_BYPASS_PLL,
+ .name = "mipi-dphy",
+};
+
+static int phy_write(struct phy *phy, u32 value, unsigned int reg)
+{
+ struct mixel_dphy_priv *priv = phy_get_drvdata(phy);
+ int ret;
+
+ ret = regmap_write(priv->regmap, reg, value);
+ if (ret < 0)
+ dev_err(&phy->dev, "Failed to write DPHY reg %d: %d\n", reg,
+ ret);
+ return ret;
+}
+
+/*
+ * Find a ratio close to the desired one using continued fraction
+ * approximation ending either at exact match or maximum allowed
+ * nominator, denominator.
+ */
+static void get_best_ratio(u32 *pnum, u32 *pdenom, u32 max_n, u32 max_d)
+{
+ u32 a = *pnum;
+ u32 b = *pdenom;
+ u32 c;
+ u32 n[] = {0, 1};
+ u32 d[] = {1, 0};
+ u32 whole;
+ unsigned int i = 1;
+
+ while (b) {
+ i ^= 1;
+ whole = a / b;
+ n[i] += (n[i ^ 1] * whole);
+ d[i] += (d[i ^ 1] * whole);
+ if ((n[i] > max_n) || (d[i] > max_d)) {
+ i ^= 1;
+ break;
+ }
+ c = a - (b * whole);
+ a = b;
+ b = c;
+ }
+ *pnum = n[i];
+ *pdenom = d[i];
+}
+
+static int mixel_dphy_config_from_opts(struct phy *phy,
+ struct phy_configure_opts_mipi_dphy *dphy_opts,
+ struct mixel_dphy_cfg *cfg)
+{
+ struct mixel_dphy_priv *priv = dev_get_drvdata(phy->dev.parent);
+ unsigned long ref_clk = clk_get_rate(priv->phy_ref_clk);
+ u32 lp_t, numerator, denominator;
+ unsigned long long tmp;
+ u32 n;
+ int i;
+
+ if (dphy_opts->hs_clk_rate > DATA_RATE_MAX_SPEED ||
+ dphy_opts->hs_clk_rate < DATA_RATE_MIN_SPEED)
+ return -EINVAL;
+
+ numerator = dphy_opts->hs_clk_rate;
+ denominator = ref_clk;
+ get_best_ratio(&numerator, &denominator, 255, 256);
+ if (!numerator || !denominator) {
+ dev_err(&phy->dev, "Invalid %d/%d for %ld/%ld\n",
+ numerator, denominator,
+ dphy_opts->hs_clk_rate, ref_clk);
+ return -EINVAL;
+ }
+
+ while ((numerator < 16) && (denominator <= 128)) {
+ numerator <<= 1;
+ denominator <<= 1;
+ }
+ /*
+ * CM ranges between 16 and 255
+ * CN ranges between 1 and 32
+ * CO is power of 2: 1, 2, 4, 8
+ */
+ i = __ffs(denominator);
+ if (i > 3)
+ i = 3;
+ cfg->cn = denominator >> i;
+ cfg->co = 1 << i;
+ cfg->cm = numerator;
+
+ if (cfg->cm < 16 || cfg->cm > 255 ||
+ cfg->cn < 1 || cfg->cn > 32 ||
+ cfg->co < 1 || cfg->co > 8) {
+ dev_err(&phy->dev, "Invalid CM/CN/CO values: %u/%u/%u\n",
+ cfg->cm, cfg->cn, cfg->co);
+ dev_err(&phy->dev, "for hs_clk/ref_clk=%ld/%ld ~ %d/%d\n",
+ dphy_opts->hs_clk_rate, ref_clk,
+ numerator, denominator);
+ return -EINVAL;
+ }
+
+ dev_dbg(&phy->dev, "hs_clk/ref_clk=%ld/%ld ~ %d/%d\n",
+ dphy_opts->hs_clk_rate, ref_clk, numerator, denominator);
+
+ /* LP clock period */
+ tmp = 1000000000000LL;
+ do_div(tmp, dphy_opts->lp_clk_rate); /* ps */
+ if (tmp > ULONG_MAX)
+ return -EINVAL;
+
+ lp_t = tmp;
+ dev_dbg(&phy->dev, "LP clock %lu, period: %u ps\n",
+ dphy_opts->lp_clk_rate, lp_t);
+
+ /* hs_prepare: in lp clock periods */
+ if (2 * dphy_opts->hs_prepare > 5 * lp_t) {
+ dev_err(&phy->dev,
+ "hs_prepare (%u) > 2.5 * lp clock period (%u)\n",
+ dphy_opts->hs_prepare, lp_t);
+ return -EINVAL;
+ }
+ /* 00: lp_t, 01: 1.5 * lp_t, 10: 2 * lp_t, 11: 2.5 * lp_t */
+ if (dphy_opts->hs_prepare < lp_t) {
+ n = 0;
+ } else {
+ tmp = 2 * (dphy_opts->hs_prepare - lp_t);
+ do_div(tmp, lp_t);
+ n = tmp;
+ }
+ cfg->m_prg_hs_prepare = n;
+
+ /* clk_prepare: in lp clock periods */
+ if (2 * dphy_opts->clk_prepare > 3 * lp_t) {
+ dev_err(&phy->dev,
+ "clk_prepare (%u) > 1.5 * lp clock period (%u)\n",
+ dphy_opts->clk_prepare, lp_t);
+ return -EINVAL;
+ }
+ /* 00: lp_t, 01: 1.5 * lp_t */
+ cfg->mc_prg_hs_prepare = dphy_opts->clk_prepare > lp_t ? 1 : 0;
+
+ /* hs_zero: formula from NXP BSP */
+ n = (144 * (dphy_opts->hs_clk_rate / 1000000) - 47500) / 10000;
+ cfg->m_prg_hs_zero = n < 1 ? 1 : n;
+
+ /* clk_zero: formula from NXP BSP */
+ n = (34 * (dphy_opts->hs_clk_rate / 1000000) - 2500) / 1000;
+ cfg->mc_prg_hs_zero = n < 1 ? 1 : n;
+
+ /* clk_trail, hs_trail: formula from NXP BSP */
+ n = (103 * (dphy_opts->hs_clk_rate / 1000000) + 10000) / 10000;
+ if (n > 15)
+ n = 15;
+ if (n < 1)
+ n = 1;
+ cfg->m_prg_hs_trail = n;
+ cfg->mc_prg_hs_trail = n;
+
+ /* rxhs_settle: formula from NXP BSP */
+ if (dphy_opts->hs_clk_rate < MBPS(80))
+ cfg->rxhs_settle = 0x0d;
+ else if (dphy_opts->hs_clk_rate < MBPS(90))
+ cfg->rxhs_settle = 0x0c;
+ else if (dphy_opts->hs_clk_rate < MBPS(125))
+ cfg->rxhs_settle = 0x0b;
+ else if (dphy_opts->hs_clk_rate < MBPS(150))
+ cfg->rxhs_settle = 0x0a;
+ else if (dphy_opts->hs_clk_rate < MBPS(225))
+ cfg->rxhs_settle = 0x09;
+ else if (dphy_opts->hs_clk_rate < MBPS(500))
+ cfg->rxhs_settle = 0x08;
+ else
+ cfg->rxhs_settle = 0x07;
+
+ dev_dbg(&phy->dev, "phy_config: %u %u %u %u %u %u %u\n",
+ cfg->m_prg_hs_prepare, cfg->mc_prg_hs_prepare,
+ cfg->m_prg_hs_zero, cfg->mc_prg_hs_zero,
+ cfg->m_prg_hs_trail, cfg->mc_prg_hs_trail,
+ cfg->rxhs_settle);
+
+ return 0;
+}
+
+static void mixel_phy_set_hs_timings(struct phy *phy)
+{
+ struct mixel_dphy_priv *priv = phy_get_drvdata(phy);
+
+ phy_write(phy, priv->cfg.m_prg_hs_prepare, DPHY_M_PRG_HS_PREPARE);
+ phy_write(phy, priv->cfg.mc_prg_hs_prepare, DPHY_MC_PRG_HS_PREPARE);
+ phy_write(phy, priv->cfg.m_prg_hs_zero, DPHY_M_PRG_HS_ZERO);
+ phy_write(phy, priv->cfg.mc_prg_hs_zero, DPHY_MC_PRG_HS_ZERO);
+ phy_write(phy, priv->cfg.m_prg_hs_trail, DPHY_M_PRG_HS_TRAIL);
+ phy_write(phy, priv->cfg.mc_prg_hs_trail, DPHY_MC_PRG_HS_TRAIL);
+ phy_write(phy, priv->cfg.rxhs_settle, priv->devdata->reg_rxhs_settle);
+}
+
+static int mixel_dphy_set_pll_params(struct phy *phy)
+{
+ struct mixel_dphy_priv *priv = dev_get_drvdata(phy->dev.parent);
+
+ if (priv->cfg.cm < 16 || priv->cfg.cm > 255 ||
+ priv->cfg.cn < 1 || priv->cfg.cn > 32 ||
+ priv->cfg.co < 1 || priv->cfg.co > 8) {
+ dev_err(&phy->dev, "Invalid CM/CN/CO values! (%u/%u/%u)\n",
+ priv->cfg.cm, priv->cfg.cn, priv->cfg.co);
+ return -EINVAL;
+ }
+ dev_dbg(&phy->dev, "Using CM:%u CN:%u CO:%u\n",
+ priv->cfg.cm, priv->cfg.cn, priv->cfg.co);
+ phy_write(phy, CM(priv->cfg.cm), DPHY_CM);
+ phy_write(phy, CN(priv->cfg.cn), DPHY_CN);
+ phy_write(phy, CO(priv->cfg.co), DPHY_CO);
+ return 0;
+}
+
+static int
+mixel_dphy_configure_mipi_dphy(struct phy *phy, union phy_configure_opts *opts)
+{
+ struct mixel_dphy_priv *priv = phy_get_drvdata(phy);
+ struct mixel_dphy_cfg cfg = { 0 };
+ int ret;
+
+ ret = mixel_dphy_config_from_opts(phy, &opts->mipi_dphy, &cfg);
+ if (ret)
+ return ret;
+
+ /* Update the configuration */
+ memcpy(&priv->cfg, &cfg, sizeof(struct mixel_dphy_cfg));
+
+ phy_write(phy, 0x00, DPHY_LOCK_BYP);
+ phy_write(phy, 0x01, priv->devdata->reg_tx_rcal);
+ phy_write(phy, 0x00, priv->devdata->reg_auto_pd_en);
+ phy_write(phy, 0x02, priv->devdata->reg_rxlprp);
+ phy_write(phy, 0x02, priv->devdata->reg_rxcdrp);
+ phy_write(phy, 0x25, DPHY_TST);
+
+ mixel_phy_set_hs_timings(phy);
+ ret = mixel_dphy_set_pll_params(phy);
+ if (ret < 0)
+ return ret;
+
+ return 0;
+}
+
+static int
+mixel_dphy_configure_lvds_phy(struct phy *phy, union phy_configure_opts *opts)
+{
+ struct mixel_dphy_priv *priv = phy_get_drvdata(phy);
+ struct phy_configure_opts_lvds *lvds_opts = &opts->lvds;
+ unsigned long data_rate;
+ unsigned long fvco;
+ u32 rsc;
+ u32 co;
+ int ret;
+
+ priv->is_slave = lvds_opts->is_slave;
+
+ /* LVDS interface pins */
+ regmap_write(priv->lvds_regmap, PHY_CTRL,
+ CCM(CCM_1_2V) | CA(CA_3_51MA) | RFB);
+
+ /* enable MODE8 only for slave LVDS PHY */
+ rsc = priv->id ? IMX_SC_R_MIPI_1 : IMX_SC_R_MIPI_0;
+ ret = imx_sc_misc_set_control(priv->ipc_handle, rsc, IMX_SC_C_DUAL_MODE,
+ lvds_opts->is_slave);
+ if (ret) {
+ dev_err(&phy->dev, "Failed to configure MODE8: %d\n", ret);
+ return ret;
+ }
+
+ /*
+ * Choose an appropriate divider ratio to meet the requirement of
+ * PLL VCO frequency range.
+ *
+ * ----- 640MHz ~ 1500MHz ------------ ---------------
+ * | VCO | ----------------> | CO divider | -> | LVDS data rate|
+ * ----- FVCO ------------ ---------------
+ * 1/2/4/8 div 7 * differential_clk_rate
+ */
+ data_rate = 7 * lvds_opts->differential_clk_rate;
+ for (co = 1; co <= 8; co *= 2) {
+ fvco = data_rate * co;
+
+ if (fvco >= MIN_VCO_FREQ)
+ break;
+ }
+
+ if (fvco < MIN_VCO_FREQ || fvco > MAX_VCO_FREQ) {
+ dev_err(&phy->dev, "VCO frequency %lu is out of range\n", fvco);
+ return -ERANGE;
+ }
+
+ /*
+ * CO is configurable, while CN and CM are not,
+ * as fixed ratios 1 and 7 are applied respectively.
+ */
+ phy_write(phy, __ffs(co), DPHY_CO);
+
+ /* set reference clock rate */
+ clk_set_rate(priv->phy_ref_clk, lvds_opts->differential_clk_rate);
+
+ return ret;
+}
+
+static int mixel_dphy_configure(struct phy *phy, union phy_configure_opts *opts)
+{
+ if (!opts) {
+ dev_err(&phy->dev, "No configuration options\n");
+ return -EINVAL;
+ }
+
+ if (phy->attrs.mode == PHY_MODE_MIPI_DPHY)
+ return mixel_dphy_configure_mipi_dphy(phy, opts);
+ else if (phy->attrs.mode == PHY_MODE_LVDS)
+ return mixel_dphy_configure_lvds_phy(phy, opts);
+
+ dev_err(&phy->dev,
+ "Failed to configure PHY with invalid PHY mode: %d\n", phy->attrs.mode);
+
+ return -EINVAL;
+}
+
+static int
+mixel_dphy_validate_lvds_phy(struct phy *phy, union phy_configure_opts *opts)
+{
+ struct phy_configure_opts_lvds *lvds_cfg = &opts->lvds;
+
+ if (lvds_cfg->bits_per_lane_and_dclk_cycle != 7) {
+ dev_err(&phy->dev, "Invalid bits per LVDS data lane: %u\n",
+ lvds_cfg->bits_per_lane_and_dclk_cycle);
+ return -EINVAL;
+ }
+
+ if (lvds_cfg->lanes != 4) {
+ dev_err(&phy->dev, "Invalid LVDS data lanes: %u\n", lvds_cfg->lanes);
+ return -EINVAL;
+ }
+
+ if (lvds_cfg->differential_clk_rate < MIN_LVDS_REFCLK_FREQ ||
+ lvds_cfg->differential_clk_rate > MAX_LVDS_REFCLK_FREQ) {
+ dev_err(&phy->dev,
+ "Invalid LVDS differential clock rate: %lu\n",
+ lvds_cfg->differential_clk_rate);
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int mixel_dphy_validate(struct phy *phy, enum phy_mode mode, int submode,
+ union phy_configure_opts *opts)
+{
+ if (mode == PHY_MODE_MIPI_DPHY) {
+ struct mixel_dphy_cfg mipi_dphy_cfg = { 0 };
+
+ return mixel_dphy_config_from_opts(phy, &opts->mipi_dphy,
+ &mipi_dphy_cfg);
+ } else if (mode == PHY_MODE_LVDS) {
+ return mixel_dphy_validate_lvds_phy(phy, opts);
+ }
+
+ dev_err(&phy->dev,
+ "Failed to validate PHY with invalid PHY mode: %d\n", mode);
+ return -EINVAL;
+}
+
+static int mixel_dphy_init(struct phy *phy)
+{
+ phy_write(phy, PWR_OFF, DPHY_PD_PLL);
+ phy_write(phy, PWR_OFF, DPHY_PD_DPHY);
+
+ return 0;
+}
+
+static int mixel_dphy_exit(struct phy *phy)
+{
+ phy_write(phy, 0, DPHY_CM);
+ phy_write(phy, 0, DPHY_CN);
+ phy_write(phy, 0, DPHY_CO);
+
+ return 0;
+}
+
+static int mixel_dphy_power_on_mipi_dphy(struct phy *phy)
+{
+ struct mixel_dphy_priv *priv = phy_get_drvdata(phy);
+ u32 locked;
+ int ret;
+
+ phy_write(phy, PWR_ON, DPHY_PD_PLL);
+ ret = regmap_read_poll_timeout(priv->regmap, DPHY_LOCK, locked,
+ locked, PLL_LOCK_SLEEP,
+ PLL_LOCK_TIMEOUT);
+ if (ret < 0) {
+ dev_err(&phy->dev, "Could not get DPHY lock (%d)!\n", ret);
+ return ret;
+ }
+ phy_write(phy, PWR_ON, DPHY_PD_DPHY);
+
+ return 0;
+}
+
+static int mixel_dphy_power_on_lvds_phy(struct phy *phy)
+{
+ struct mixel_dphy_priv *priv = phy_get_drvdata(phy);
+ u32 locked;
+ int ret;
+
+ regmap_update_bits(priv->lvds_regmap, PHY_CTRL, LVDS_EN, LVDS_EN);
+
+ phy_write(phy, PWR_ON, DPHY_PD_DPHY);
+ phy_write(phy, PWR_ON, DPHY_PD_PLL);
+
+ /* do not wait for slave LVDS PHY being locked */
+ if (priv->is_slave)
+ return 0;
+
+ ret = regmap_read_poll_timeout(priv->regmap, DPHY_LOCK, locked,
+ locked, PLL_LOCK_SLEEP,
+ PLL_LOCK_TIMEOUT);
+ if (ret < 0) {
+ dev_err(&phy->dev, "Could not get LVDS PHY lock (%d)!\n", ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+static int mixel_dphy_power_on(struct phy *phy)
+{
+ struct mixel_dphy_priv *priv = phy_get_drvdata(phy);
+ int ret;
+
+ ret = clk_prepare_enable(priv->phy_ref_clk);
+ if (ret < 0)
+ return ret;
+
+ if (phy->attrs.mode == PHY_MODE_MIPI_DPHY) {
+ ret = mixel_dphy_power_on_mipi_dphy(phy);
+ } else if (phy->attrs.mode == PHY_MODE_LVDS) {
+ ret = mixel_dphy_power_on_lvds_phy(phy);
+ } else {
+ dev_err(&phy->dev,
+ "Failed to power on PHY with invalid PHY mode: %d\n",
+ phy->attrs.mode);
+ ret = -EINVAL;
+ }
+
+ if (ret)
+ goto clock_disable;
+
+ return 0;
+clock_disable:
+ clk_disable_unprepare(priv->phy_ref_clk);
+ return ret;
+}
+
+static int mixel_dphy_power_off(struct phy *phy)
+{
+ struct mixel_dphy_priv *priv = phy_get_drvdata(phy);
+
+ phy_write(phy, PWR_OFF, DPHY_PD_PLL);
+ phy_write(phy, PWR_OFF, DPHY_PD_DPHY);
+
+ if (phy->attrs.mode == PHY_MODE_LVDS)
+ regmap_update_bits(priv->lvds_regmap, PHY_CTRL, LVDS_EN, 0);
+
+ clk_disable_unprepare(priv->phy_ref_clk);
+
+ return 0;
+}
+
+static int mixel_dphy_set_mode(struct phy *phy, enum phy_mode mode, int submode)
+{
+ struct mixel_dphy_priv *priv = phy_get_drvdata(phy);
+ int ret;
+
+ if (priv->devdata->is_combo && mode != PHY_MODE_LVDS) {
+ dev_err(&phy->dev, "Failed to set PHY mode for combo PHY\n");
+ return -EINVAL;
+ }
+
+ if (!priv->devdata->is_combo && mode != PHY_MODE_MIPI_DPHY) {
+ dev_err(&phy->dev, "Failed to set PHY mode to MIPI DPHY\n");
+ return -EINVAL;
+ }
+
+ if (priv->devdata->is_combo) {
+ u32 rsc = priv->id ? IMX_SC_R_MIPI_1 : IMX_SC_R_MIPI_0;
+
+ ret = imx_sc_misc_set_control(priv->ipc_handle,
+ rsc, IMX_SC_C_MODE,
+ mode == PHY_MODE_LVDS);
+ if (ret) {
+ dev_err(&phy->dev,
+ "Failed to set PHY mode via SCU ipc: %d\n", ret);
+ return ret;
+ }
+ }
+
+ return 0;
+}
+
+static const struct phy_ops mixel_dphy_phy_ops = {
+ .init = mixel_dphy_init,
+ .exit = mixel_dphy_exit,
+ .power_on = mixel_dphy_power_on,
+ .power_off = mixel_dphy_power_off,
+ .set_mode = mixel_dphy_set_mode,
+ .configure = mixel_dphy_configure,
+ .validate = mixel_dphy_validate,
+ .owner = THIS_MODULE,
+};
+
+static const struct of_device_id mixel_dphy_of_match[] = {
+ { .compatible = "fsl,imx8mq-mipi-dphy",
+ .data = &mixel_dphy_devdata[MIXEL_IMX8MQ] },
+ { .compatible = "fsl,imx8qxp-mipi-dphy",
+ .data = &mixel_dphy_devdata[MIXEL_IMX8QXP] },
+ { /* sentinel */ },
+};
+MODULE_DEVICE_TABLE(of, mixel_dphy_of_match);
+
+static int mixel_dphy_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct device_node *np = dev->of_node;
+ struct phy_provider *phy_provider;
+ struct mixel_dphy_priv *priv;
+ struct phy *phy;
+ void __iomem *base;
+ int ret;
+
+ if (!np)
+ return -ENODEV;
+
+ priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+ if (!priv)
+ return -ENOMEM;
+
+ priv->devdata = of_device_get_match_data(&pdev->dev);
+ if (!priv->devdata)
+ return -EINVAL;
+
+ base = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(base))
+ return PTR_ERR(base);
+
+ priv->regmap = devm_regmap_init_mmio(&pdev->dev, base,
+ &mixel_dphy_regmap_config);
+ if (IS_ERR(priv->regmap)) {
+ dev_err(dev, "Couldn't create the DPHY regmap\n");
+ return PTR_ERR(priv->regmap);
+ }
+
+ priv->phy_ref_clk = devm_clk_get(&pdev->dev, "phy_ref");
+ if (IS_ERR(priv->phy_ref_clk)) {
+ dev_err(dev, "No phy_ref clock found\n");
+ return PTR_ERR(priv->phy_ref_clk);
+ }
+ dev_dbg(dev, "phy_ref clock rate: %lu\n",
+ clk_get_rate(priv->phy_ref_clk));
+
+ if (priv->devdata->is_combo) {
+ priv->lvds_regmap =
+ syscon_regmap_lookup_by_phandle(np, "fsl,syscon");
+ if (IS_ERR(priv->lvds_regmap)) {
+ ret = PTR_ERR(priv->lvds_regmap);
+ dev_err_probe(dev, ret, "Failed to get LVDS regmap\n");
+ return ret;
+ }
+
+ priv->id = of_alias_get_id(np, "mipi-dphy");
+ if (priv->id < 0) {
+ dev_err(dev, "Failed to get phy node alias id: %d\n",
+ priv->id);
+ return priv->id;
+ }
+
+ ret = imx_scu_get_handle(&priv->ipc_handle);
+ if (ret) {
+ dev_err_probe(dev, ret,
+ "Failed to get SCU ipc handle\n");
+ return ret;
+ }
+ }
+
+ dev_set_drvdata(dev, priv);
+
+ phy = devm_phy_create(dev, np, &mixel_dphy_phy_ops);
+ if (IS_ERR(phy)) {
+ dev_err(dev, "Failed to create phy %ld\n", PTR_ERR(phy));
+ return PTR_ERR(phy);
+ }
+ phy_set_drvdata(phy, priv);
+
+ phy_provider = devm_of_phy_provider_register(dev, of_phy_simple_xlate);
+
+ return PTR_ERR_OR_ZERO(phy_provider);
+}
+
+static struct platform_driver mixel_dphy_driver = {
+ .probe = mixel_dphy_probe,
+ .driver = {
+ .name = "mixel-mipi-dphy",
+ .of_match_table = mixel_dphy_of_match,
+ }
+};
+module_platform_driver(mixel_dphy_driver);
+
+MODULE_AUTHOR("NXP Semiconductor");
+MODULE_DESCRIPTION("Mixel MIPI-DSI PHY driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/phy/freescale/phy-fsl-imx8m-pcie.c b/drivers/phy/freescale/phy-fsl-imx8m-pcie.c
new file mode 100644
index 000000000..7f5600103
--- /dev/null
+++ b/drivers/phy/freescale/phy-fsl-imx8m-pcie.c
@@ -0,0 +1,291 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright 2021 NXP
+ */
+
+#include <linux/bitfield.h>
+#include <linux/clk.h>
+#include <linux/delay.h>
+#include <linux/io.h>
+#include <linux/iopoll.h>
+#include <linux/mfd/syscon.h>
+#include <linux/mfd/syscon/imx7-iomuxc-gpr.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/phy/phy.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+#include <linux/reset.h>
+
+#include <dt-bindings/phy/phy-imx8-pcie.h>
+
+#define IMX8MM_PCIE_PHY_CMN_REG061 0x184
+#define ANA_PLL_CLK_OUT_TO_EXT_IO_EN BIT(0)
+#define IMX8MM_PCIE_PHY_CMN_REG062 0x188
+#define ANA_PLL_CLK_OUT_TO_EXT_IO_SEL BIT(3)
+#define IMX8MM_PCIE_PHY_CMN_REG063 0x18C
+#define AUX_PLL_REFCLK_SEL_SYS_PLL GENMASK(7, 6)
+#define IMX8MM_PCIE_PHY_CMN_REG064 0x190
+#define ANA_AUX_RX_TX_SEL_TX BIT(7)
+#define ANA_AUX_RX_TERM_GND_EN BIT(3)
+#define ANA_AUX_TX_TERM BIT(2)
+#define IMX8MM_PCIE_PHY_CMN_REG065 0x194
+#define ANA_AUX_RX_TERM (BIT(7) | BIT(4))
+#define ANA_AUX_TX_LVL GENMASK(3, 0)
+#define IMX8MM_PCIE_PHY_CMN_REG075 0x1D4
+#define ANA_PLL_DONE 0x3
+#define PCIE_PHY_TRSV_REG5 0x414
+#define PCIE_PHY_TRSV_REG6 0x418
+
+#define IMX8MM_GPR_PCIE_REF_CLK_SEL GENMASK(25, 24)
+#define IMX8MM_GPR_PCIE_REF_CLK_PLL FIELD_PREP(IMX8MM_GPR_PCIE_REF_CLK_SEL, 0x3)
+#define IMX8MM_GPR_PCIE_REF_CLK_EXT FIELD_PREP(IMX8MM_GPR_PCIE_REF_CLK_SEL, 0x2)
+#define IMX8MM_GPR_PCIE_AUX_EN BIT(19)
+#define IMX8MM_GPR_PCIE_CMN_RST BIT(18)
+#define IMX8MM_GPR_PCIE_POWER_OFF BIT(17)
+#define IMX8MM_GPR_PCIE_SSC_EN BIT(16)
+#define IMX8MM_GPR_PCIE_AUX_EN_OVERRIDE BIT(9)
+
+enum imx8_pcie_phy_type {
+ IMX8MM,
+ IMX8MP,
+};
+
+struct imx8_pcie_phy_drvdata {
+ const char *gpr;
+ enum imx8_pcie_phy_type variant;
+};
+
+struct imx8_pcie_phy {
+ void __iomem *base;
+ struct clk *clk;
+ struct phy *phy;
+ struct regmap *iomuxc_gpr;
+ struct reset_control *perst;
+ struct reset_control *reset;
+ u32 refclk_pad_mode;
+ u32 tx_deemph_gen1;
+ u32 tx_deemph_gen2;
+ bool clkreq_unused;
+ const struct imx8_pcie_phy_drvdata *drvdata;
+};
+
+static int imx8_pcie_phy_power_on(struct phy *phy)
+{
+ int ret;
+ u32 val, pad_mode;
+ struct imx8_pcie_phy *imx8_phy = phy_get_drvdata(phy);
+
+ pad_mode = imx8_phy->refclk_pad_mode;
+ switch (imx8_phy->drvdata->variant) {
+ case IMX8MM:
+ reset_control_assert(imx8_phy->reset);
+
+ /* Tune PHY de-emphasis setting to pass PCIe compliance. */
+ if (imx8_phy->tx_deemph_gen1)
+ writel(imx8_phy->tx_deemph_gen1,
+ imx8_phy->base + PCIE_PHY_TRSV_REG5);
+ if (imx8_phy->tx_deemph_gen2)
+ writel(imx8_phy->tx_deemph_gen2,
+ imx8_phy->base + PCIE_PHY_TRSV_REG6);
+ break;
+ case IMX8MP:
+ reset_control_assert(imx8_phy->reset);
+ break;
+ }
+
+ if (pad_mode == IMX8_PCIE_REFCLK_PAD_INPUT ||
+ pad_mode == IMX8_PCIE_REFCLK_PAD_UNUSED) {
+ /* Configure the pad as input */
+ val = readl(imx8_phy->base + IMX8MM_PCIE_PHY_CMN_REG061);
+ writel(val & ~ANA_PLL_CLK_OUT_TO_EXT_IO_EN,
+ imx8_phy->base + IMX8MM_PCIE_PHY_CMN_REG061);
+ } else {
+ /* Configure the PHY to output the refclock via pad */
+ writel(ANA_PLL_CLK_OUT_TO_EXT_IO_EN,
+ imx8_phy->base + IMX8MM_PCIE_PHY_CMN_REG061);
+ }
+
+ if (pad_mode == IMX8_PCIE_REFCLK_PAD_OUTPUT ||
+ pad_mode == IMX8_PCIE_REFCLK_PAD_UNUSED) {
+ /* Source clock from SoC internal PLL */
+ writel(ANA_PLL_CLK_OUT_TO_EXT_IO_SEL,
+ imx8_phy->base + IMX8MM_PCIE_PHY_CMN_REG062);
+ if (imx8_phy->drvdata->variant != IMX8MM) {
+ writel(AUX_PLL_REFCLK_SEL_SYS_PLL,
+ imx8_phy->base + IMX8MM_PCIE_PHY_CMN_REG063);
+ }
+ val = ANA_AUX_RX_TX_SEL_TX | ANA_AUX_TX_TERM;
+ writel(val | ANA_AUX_RX_TERM_GND_EN,
+ imx8_phy->base + IMX8MM_PCIE_PHY_CMN_REG064);
+ writel(ANA_AUX_RX_TERM | ANA_AUX_TX_LVL,
+ imx8_phy->base + IMX8MM_PCIE_PHY_CMN_REG065);
+ }
+
+ /* Set AUX_EN_OVERRIDE 1'b0, when the CLKREQ# isn't hooked */
+ regmap_update_bits(imx8_phy->iomuxc_gpr, IOMUXC_GPR14,
+ IMX8MM_GPR_PCIE_AUX_EN_OVERRIDE,
+ imx8_phy->clkreq_unused ?
+ 0 : IMX8MM_GPR_PCIE_AUX_EN_OVERRIDE);
+ regmap_update_bits(imx8_phy->iomuxc_gpr, IOMUXC_GPR14,
+ IMX8MM_GPR_PCIE_AUX_EN,
+ IMX8MM_GPR_PCIE_AUX_EN);
+ regmap_update_bits(imx8_phy->iomuxc_gpr, IOMUXC_GPR14,
+ IMX8MM_GPR_PCIE_POWER_OFF, 0);
+ regmap_update_bits(imx8_phy->iomuxc_gpr, IOMUXC_GPR14,
+ IMX8MM_GPR_PCIE_SSC_EN, 0);
+
+ regmap_update_bits(imx8_phy->iomuxc_gpr, IOMUXC_GPR14,
+ IMX8MM_GPR_PCIE_REF_CLK_SEL,
+ pad_mode == IMX8_PCIE_REFCLK_PAD_INPUT ?
+ IMX8MM_GPR_PCIE_REF_CLK_EXT :
+ IMX8MM_GPR_PCIE_REF_CLK_PLL);
+ usleep_range(100, 200);
+
+ reset_control_deassert(imx8_phy->perst);
+ reset_control_deassert(imx8_phy->reset);
+ usleep_range(200, 500);
+
+ /* Do the PHY common block reset */
+ regmap_update_bits(imx8_phy->iomuxc_gpr, IOMUXC_GPR14,
+ IMX8MM_GPR_PCIE_CMN_RST,
+ IMX8MM_GPR_PCIE_CMN_RST);
+
+ /* Polling to check the phy is ready or not. */
+ ret = readl_poll_timeout(imx8_phy->base + IMX8MM_PCIE_PHY_CMN_REG075,
+ val, val == ANA_PLL_DONE, 10, 20000);
+ return ret;
+}
+
+static int imx8_pcie_phy_power_off(struct phy *phy)
+{
+ struct imx8_pcie_phy *imx8_phy = phy_get_drvdata(phy);
+
+ reset_control_assert(imx8_phy->reset);
+ reset_control_assert(imx8_phy->perst);
+
+ return 0;
+}
+
+static int imx8_pcie_phy_init(struct phy *phy)
+{
+ struct imx8_pcie_phy *imx8_phy = phy_get_drvdata(phy);
+
+ return clk_prepare_enable(imx8_phy->clk);
+}
+
+static int imx8_pcie_phy_exit(struct phy *phy)
+{
+ struct imx8_pcie_phy *imx8_phy = phy_get_drvdata(phy);
+
+ clk_disable_unprepare(imx8_phy->clk);
+
+ return 0;
+}
+
+static const struct phy_ops imx8_pcie_phy_ops = {
+ .init = imx8_pcie_phy_init,
+ .exit = imx8_pcie_phy_exit,
+ .power_on = imx8_pcie_phy_power_on,
+ .power_off = imx8_pcie_phy_power_off,
+ .owner = THIS_MODULE,
+};
+
+static const struct imx8_pcie_phy_drvdata imx8mm_drvdata = {
+ .gpr = "fsl,imx8mm-iomuxc-gpr",
+ .variant = IMX8MM,
+};
+
+static const struct imx8_pcie_phy_drvdata imx8mp_drvdata = {
+ .gpr = "fsl,imx8mp-iomuxc-gpr",
+ .variant = IMX8MP,
+};
+
+static const struct of_device_id imx8_pcie_phy_of_match[] = {
+ {.compatible = "fsl,imx8mm-pcie-phy", .data = &imx8mm_drvdata, },
+ {.compatible = "fsl,imx8mp-pcie-phy", .data = &imx8mp_drvdata, },
+ { },
+};
+MODULE_DEVICE_TABLE(of, imx8_pcie_phy_of_match);
+
+static int imx8_pcie_phy_probe(struct platform_device *pdev)
+{
+ struct phy_provider *phy_provider;
+ struct device *dev = &pdev->dev;
+ struct device_node *np = dev->of_node;
+ struct imx8_pcie_phy *imx8_phy;
+
+ imx8_phy = devm_kzalloc(dev, sizeof(*imx8_phy), GFP_KERNEL);
+ if (!imx8_phy)
+ return -ENOMEM;
+
+ imx8_phy->drvdata = of_device_get_match_data(dev);
+
+ /* get PHY refclk pad mode */
+ of_property_read_u32(np, "fsl,refclk-pad-mode",
+ &imx8_phy->refclk_pad_mode);
+
+ if (of_property_read_u32(np, "fsl,tx-deemph-gen1",
+ &imx8_phy->tx_deemph_gen1))
+ imx8_phy->tx_deemph_gen1 = 0;
+
+ if (of_property_read_u32(np, "fsl,tx-deemph-gen2",
+ &imx8_phy->tx_deemph_gen2))
+ imx8_phy->tx_deemph_gen2 = 0;
+
+ if (of_property_read_bool(np, "fsl,clkreq-unsupported"))
+ imx8_phy->clkreq_unused = true;
+ else
+ imx8_phy->clkreq_unused = false;
+
+ imx8_phy->clk = devm_clk_get(dev, "ref");
+ if (IS_ERR(imx8_phy->clk))
+ return dev_err_probe(dev, PTR_ERR(imx8_phy->clk),
+ "failed to get imx pcie phy clock\n");
+
+ /* Grab GPR config register range */
+ imx8_phy->iomuxc_gpr =
+ syscon_regmap_lookup_by_compatible(imx8_phy->drvdata->gpr);
+ if (IS_ERR(imx8_phy->iomuxc_gpr))
+ return dev_err_probe(dev, PTR_ERR(imx8_phy->iomuxc_gpr),
+ "unable to find iomuxc registers\n");
+
+ imx8_phy->reset = devm_reset_control_get_exclusive(dev, "pciephy");
+ if (IS_ERR(imx8_phy->reset))
+ return dev_err_probe(dev, PTR_ERR(imx8_phy->reset),
+ "Failed to get PCIEPHY reset control\n");
+
+ if (imx8_phy->drvdata->variant == IMX8MP) {
+ imx8_phy->perst =
+ devm_reset_control_get_exclusive(dev, "perst");
+ if (IS_ERR(imx8_phy->perst))
+ return dev_err_probe(dev, PTR_ERR(imx8_phy->perst),
+ "Failed to get PCIE PHY PERST control\n");
+ }
+
+ imx8_phy->base = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(imx8_phy->base))
+ return PTR_ERR(imx8_phy->base);
+
+ imx8_phy->phy = devm_phy_create(dev, NULL, &imx8_pcie_phy_ops);
+ if (IS_ERR(imx8_phy->phy))
+ return PTR_ERR(imx8_phy->phy);
+
+ phy_set_drvdata(imx8_phy->phy, imx8_phy);
+
+ phy_provider = devm_of_phy_provider_register(dev, of_phy_simple_xlate);
+
+ return PTR_ERR_OR_ZERO(phy_provider);
+}
+
+static struct platform_driver imx8_pcie_phy_driver = {
+ .probe = imx8_pcie_phy_probe,
+ .driver = {
+ .name = "imx8-pcie-phy",
+ .of_match_table = imx8_pcie_phy_of_match,
+ }
+};
+module_platform_driver(imx8_pcie_phy_driver);
+
+MODULE_DESCRIPTION("FSL IMX8 PCIE PHY driver");
+MODULE_LICENSE("GPL v2");
diff --git a/drivers/phy/freescale/phy-fsl-imx8mq-usb.c b/drivers/phy/freescale/phy-fsl-imx8mq-usb.c
new file mode 100644
index 000000000..885983554
--- /dev/null
+++ b/drivers/phy/freescale/phy-fsl-imx8mq-usb.c
@@ -0,0 +1,849 @@
+// SPDX-License-Identifier: GPL-2.0+
+/* Copyright 2017-2026 NXP. */
+
+#include <linux/bitfield.h>
+#include <linux/clk.h>
+#include <linux/delay.h>
+#include <linux/io.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/phy/phy.h>
+#include <linux/platform_device.h>
+#include <linux/pm_domain.h>
+#include <linux/pm_runtime.h>
+#include <linux/regulator/consumer.h>
+#include <linux/regmap.h>
+#include <linux/usb/typec_mux.h>
+
+#define PHY_CTRL0 0x0
+#define PHY_CTRL0_REF_SSP_EN BIT(2)
+#define PHY_CTRL0_FSEL_MASK GENMASK(10, 5)
+#define PHY_CTRL0_FSEL_24M 0x2a
+#define PHY_CTRL0_FSEL_100M 0x27
+#define PHY_CTRL0_SSC_RANGE_MASK GENMASK(23, 21)
+#define PHY_CTRL0_SSC_RANGE_4003PPM 0x2
+#define PHY_CTRL0_SSC_RANGE_4492PPM 0x1
+#define PHY_CTRL0_SSC_RANGE_4980PPM 0x0
+
+#define PHY_CTRL1 0x4
+#define PHY_CTRL1_RESET BIT(0)
+#define PHY_CTRL1_COMMONONN BIT(1)
+#define PHY_CTRL1_ATERESET BIT(3)
+#define PHY_CTRL1_VDATSRCENB0 BIT(19)
+#define PHY_CTRL1_VDATDETENB0 BIT(20)
+
+#define PHY_CTRL2 0x8
+#define PHY_CTRL2_TXENABLEN0 BIT(8)
+#define PHY_CTRL2_OTG_DISABLE BIT(9)
+
+#define PHY_CTRL3 0xc
+#define PHY_CTRL3_COMPDISTUNE_MASK GENMASK(2, 0)
+#define PHY_CTRL3_TXPREEMP_TUNE_MASK GENMASK(16, 15)
+#define PHY_CTRL3_TXRISE_TUNE_MASK GENMASK(21, 20)
+#define PHY_CTRL3_TXVREF_TUNE_MASK GENMASK(25, 22)
+#define PHY_CTRL3_TX_VBOOST_LEVEL_MASK GENMASK(31, 29)
+
+#define PHY_CTRL4 0x10
+#define PHY_CTRL4_PCS_TX_DEEMPH_3P5DB_MASK GENMASK(20, 15)
+
+#define PHY_CTRL5 0x14
+#define PHY_CTRL5_DMPWD_OVERRIDE_SEL BIT(23)
+#define PHY_CTRL5_DMPWD_OVERRIDE BIT(22)
+#define PHY_CTRL5_DPPWD_OVERRIDE_SEL BIT(21)
+#define PHY_CTRL5_DPPWD_OVERRIDE BIT(20)
+#define PHY_CTRL5_PCS_TX_SWING_FULL_MASK GENMASK(6, 0)
+
+#define PHY_CTRL6 0x18
+#define PHY_CTRL6_RXTERM_OVERRIDE_SEL BIT(29)
+#define PHY_CTRL6_ALT_CLK_EN BIT(1)
+#define PHY_CTRL6_ALT_CLK_SEL BIT(0)
+
+#define PHY_CRCTL 0x30
+
+#define PHY_TUNE_DEFAULT 0xffffffff
+
+#define TCA_CLK_RST 0x00
+#define TCA_CLK_RST_SW BIT(9)
+#define TCA_CLK_RST_REF_CLK_EN BIT(1)
+#define TCA_CLK_RST_SUSPEND_CLK_EN BIT(0)
+
+#define TCA_INTR_EN 0x04
+#define TCA_INTR_STS 0x08
+
+#define TCA_GCFG 0x10
+#define TCA_GCFG_ROLE_HSTDEV BIT(4)
+#define TCA_GCFG_OP_MODE GENMASK(1, 0)
+#define TCA_GCFG_OP_MODE_SYSMODE 0
+#define TCA_GCFG_OP_MODE_SYNCMODE 1
+
+#define TCA_TCPC 0x14
+#define TCA_TCPC_VALID BIT(4)
+#define TCA_TCPC_LOW_POWER_EN BIT(3)
+#define TCA_TCPC_ORIENTATION_NORMAL BIT(2)
+#define TCA_TCPC_MUX_CONTRL GENMASK(1, 0)
+#define TCA_TCPC_MUX_CONTRL_NO_CONN 0
+#define TCA_TCPC_MUX_CONTRL_USB_CONN 1
+
+#define TCA_SYSMODE_CFG 0x18
+#define TCA_SYSMODE_TCPC_DISABLE BIT(3)
+#define TCA_SYSMODE_TCPC_FLIP BIT(2)
+
+#define TCA_CTRLSYNCMODE_CFG0 0x20
+#define TCA_CTRLSYNCMODE_CFG1 0x20
+
+#define TCA_PSTATE 0x30
+#define TCA_PSTATE_CM_STS BIT(4)
+#define TCA_PSTATE_TX_STS BIT(3)
+#define TCA_PSTATE_RX_PLL_STS BIT(2)
+#define TCA_PSTATE_PIPE0_POWER_DOWN GENMASK(1, 0)
+
+#define TCA_GEN_STATUS 0x34
+#define TCA_GEN_DEV_POR BIT(12)
+#define TCA_GEN_REF_CLK_SEL BIT(8)
+#define TCA_GEN_TYPEC_FLIP_INVERT BIT(4)
+#define TCA_GEN_PHY_TYPEC_DISABLE BIT(3)
+#define TCA_GEN_PHY_TYPEC_FLIP BIT(2)
+
+#define TCA_VBUS_CTRL 0x40
+#define TCA_VBUS_STATUS 0x44
+
+#define TCA_INFO 0xfc
+
+struct tca_blk {
+ struct typec_switch_dev *sw;
+ void __iomem *base;
+ struct mutex mutex;
+ enum typec_orientation orientation;
+};
+
+struct imx8mq_usb_phy {
+ struct phy *phy;
+ struct clk *clk;
+ struct clk *alt_clk;
+ void __iomem *base;
+ struct regulator *vbus;
+ struct tca_blk *tca;
+ struct regmap *cr_regmap;
+ u32 pcs_tx_swing_full;
+ u32 pcs_tx_deemph_3p5db;
+ u32 tx_vref_tune;
+ u32 tx_rise_tune;
+ u32 tx_preemp_amp_tune;
+ u32 tx_vboost_level;
+ u32 comp_dis_tune;
+};
+
+struct imx8mq_usb_phy_drvdata {
+ const struct phy_ops *ops;
+ bool need_genpd_rpm_on;
+};
+
+static void tca_blk_orientation_set(struct tca_blk *tca,
+ enum typec_orientation orientation);
+
+static int tca_blk_typec_switch_set(struct typec_switch_dev *sw,
+ enum typec_orientation orientation)
+{
+ struct imx8mq_usb_phy *imx_phy = typec_switch_get_drvdata(sw);
+ struct tca_blk *tca = imx_phy->tca;
+
+ if (tca->orientation == orientation)
+ return 0;
+
+ PM_RUNTIME_ACQUIRE_IF_ENABLED(&imx_phy->phy->dev, pm);
+ if (PM_RUNTIME_ACQUIRE_ERR(&pm))
+ return -ENXIO;
+
+ tca_blk_orientation_set(tca, orientation);
+
+ return 0;
+}
+
+static struct typec_switch_dev *tca_blk_get_typec_switch(struct platform_device *pdev,
+ struct imx8mq_usb_phy *imx_phy)
+{
+ struct device *dev = &pdev->dev;
+ struct typec_switch_dev *sw;
+ struct typec_switch_desc sw_desc = { };
+
+ sw_desc.drvdata = imx_phy;
+ sw_desc.fwnode = dev->fwnode;
+ sw_desc.set = tca_blk_typec_switch_set;
+ sw_desc.name = NULL;
+
+ sw = typec_switch_register(dev, &sw_desc);
+ if (IS_ERR(sw)) {
+ dev_err(dev, "Error register tca orientation switch: %ld",
+ PTR_ERR(sw));
+ return NULL;
+ }
+
+ return sw;
+}
+
+static void tca_blk_put_typec_switch(void *data)
+{
+ typec_switch_unregister(data);
+}
+
+static void tca_blk_orientation_set(struct tca_blk *tca,
+ enum typec_orientation orientation)
+{
+ u32 val;
+
+ mutex_lock(&tca->mutex);
+
+ if (orientation == TYPEC_ORIENTATION_NONE) {
+ /*
+ * use Controller Synced Mode for TCA low power enable and
+ * put PHY to USB safe state.
+ */
+ val = FIELD_PREP(TCA_GCFG_OP_MODE, TCA_GCFG_OP_MODE_SYNCMODE);
+ writel(val, tca->base + TCA_GCFG);
+
+ val = TCA_TCPC_VALID | TCA_TCPC_LOW_POWER_EN;
+ writel(val, tca->base + TCA_TCPC);
+
+ goto out;
+ }
+
+ /* use System Configuration Mode for TCA mux control. */
+ val = FIELD_PREP(TCA_GCFG_OP_MODE, TCA_GCFG_OP_MODE_SYSMODE);
+ writel(val, tca->base + TCA_GCFG);
+
+ /* Disable TCA module */
+ val = readl(tca->base + TCA_SYSMODE_CFG);
+ val |= TCA_SYSMODE_TCPC_DISABLE;
+ writel(val, tca->base + TCA_SYSMODE_CFG);
+
+ if (orientation == TYPEC_ORIENTATION_REVERSE)
+ val |= TCA_SYSMODE_TCPC_FLIP;
+ else if (orientation == TYPEC_ORIENTATION_NORMAL)
+ val &= ~TCA_SYSMODE_TCPC_FLIP;
+
+ writel(val, tca->base + TCA_SYSMODE_CFG);
+
+ /* Enable TCA module */
+ val &= ~TCA_SYSMODE_TCPC_DISABLE;
+ writel(val, tca->base + TCA_SYSMODE_CFG);
+
+out:
+ tca->orientation = orientation;
+ mutex_unlock(&tca->mutex);
+}
+
+static void tca_blk_init(struct tca_blk *tca)
+{
+ u32 val;
+
+ /* reset XBar block */
+ val = readl(tca->base + TCA_CLK_RST);
+ val &= ~TCA_CLK_RST_SW;
+ writel(val, tca->base + TCA_CLK_RST);
+
+ udelay(100);
+
+ /* clear reset */
+ val |= TCA_CLK_RST_SW;
+ writel(val, tca->base + TCA_CLK_RST);
+
+ tca_blk_orientation_set(tca, tca->orientation);
+}
+
+static struct tca_blk *imx95_usb_phy_get_tca(struct platform_device *pdev,
+ struct imx8mq_usb_phy *imx_phy)
+{
+ struct device *dev = &pdev->dev;
+ struct resource *res;
+ struct tca_blk *tca;
+ int ret;
+
+ res = platform_get_resource(pdev, IORESOURCE_MEM, 1);
+ if (!res)
+ return NULL;
+
+ tca = devm_kzalloc(dev, sizeof(*tca), GFP_KERNEL);
+ if (!tca)
+ return ERR_PTR(-ENOMEM);
+
+ tca->base = devm_ioremap_resource(&pdev->dev, res);
+ if (IS_ERR(tca->base))
+ return ERR_CAST(tca->base);
+
+ mutex_init(&tca->mutex);
+
+ tca->orientation = TYPEC_ORIENTATION_NORMAL;
+ tca->sw = tca_blk_get_typec_switch(pdev, imx_phy);
+
+ ret = devm_add_action_or_reset(&pdev->dev, tca_blk_put_typec_switch, tca->sw);
+ if (ret)
+ return ERR_PTR(ret);
+
+ return tca;
+}
+
+static u32 phy_tx_vref_tune_from_property(u32 percent)
+{
+ percent = clamp(percent, 94U, 124U);
+
+ return DIV_ROUND_CLOSEST(percent - 94U, 2);
+}
+
+static u32 imx95_phy_tx_vref_tune_from_property(u32 percent)
+{
+ percent = clamp(percent, 90U, 108U);
+
+ switch (percent) {
+ case 90 ... 91:
+ percent = 0;
+ break;
+ case 92 ... 96:
+ percent -= 91;
+ break;
+ case 97 ... 104:
+ percent -= 92;
+ break;
+ case 105 ... 108:
+ percent -= 93;
+ break;
+ }
+
+ return percent;
+}
+
+static u32 phy_tx_rise_tune_from_property(u32 percent)
+{
+ switch (percent) {
+ case 0 ... 98:
+ return 3;
+ case 99:
+ return 2;
+ case 100 ... 101:
+ return 1;
+ default:
+ return 0;
+ }
+}
+
+static u32 imx95_phy_tx_rise_tune_from_property(u32 percent)
+{
+ percent = clamp(percent, 90U, 120U);
+
+ switch (percent) {
+ case 90 ... 99:
+ return 3;
+ case 101 ... 115:
+ return 1;
+ case 116 ... 120:
+ return 0;
+ default:
+ return 2;
+ }
+}
+
+static u32 phy_tx_preemp_amp_tune_from_property(u32 microamp)
+{
+ microamp = min(microamp, 1800U);
+
+ return microamp / 600;
+}
+
+static u32 phy_tx_vboost_level_from_property(u32 microvolt)
+{
+ switch (microvolt) {
+ case 1156:
+ return 5;
+ case 844:
+ return 3;
+ default:
+ return 4;
+ }
+}
+
+static u32 phy_pcs_tx_deemph_3p5db_from_property(u32 decibel)
+{
+ return min(decibel, 36U);
+}
+
+static u32 phy_comp_dis_tune_from_property(u32 percent)
+{
+ switch (percent) {
+ case 0 ... 92:
+ return 0;
+ case 93 ... 95:
+ return 1;
+ case 96 ... 97:
+ return 2;
+ case 98 ... 102:
+ return 3;
+ case 103 ... 105:
+ return 4;
+ case 106 ... 109:
+ return 5;
+ case 110 ... 113:
+ return 6;
+ default:
+ return 7;
+ }
+}
+
+static u32 imx95_phy_comp_dis_tune_from_property(u32 percent)
+{
+ percent = clamp(percent, 94, 104);
+
+ switch (percent) {
+ case 94 ... 95:
+ percent = 0;
+ break;
+ case 96 ... 98:
+ percent -= 95;
+ break;
+ case 99 ... 102:
+ percent -= 96;
+ break;
+ case 103 ... 104:
+ percent -= 97;
+ break;
+ }
+
+ return percent;
+}
+
+static u32 phy_pcs_tx_swing_full_from_property(u32 percent)
+{
+ percent = min(percent, 100U);
+
+ return (percent * 127) / 100;
+}
+
+static void imx8m_get_phy_tuning_data(struct imx8mq_usb_phy *imx_phy)
+{
+ struct device *dev = imx_phy->phy->dev.parent;
+ bool is_imx95 = false;
+
+ if (device_is_compatible(dev, "fsl,imx95-usb-phy"))
+ is_imx95 = true;
+
+ if (device_property_read_u32(dev, "fsl,phy-tx-vref-tune-percent",
+ &imx_phy->tx_vref_tune))
+ imx_phy->tx_vref_tune = PHY_TUNE_DEFAULT;
+ else if (is_imx95)
+ imx_phy->tx_vref_tune =
+ imx95_phy_tx_vref_tune_from_property(imx_phy->tx_vref_tune);
+ else
+ imx_phy->tx_vref_tune =
+ phy_tx_vref_tune_from_property(imx_phy->tx_vref_tune);
+
+ if (device_property_read_u32(dev, "fsl,phy-tx-rise-tune-percent",
+ &imx_phy->tx_rise_tune))
+ imx_phy->tx_rise_tune = PHY_TUNE_DEFAULT;
+ else if (is_imx95)
+ imx_phy->tx_rise_tune =
+ imx95_phy_tx_rise_tune_from_property(imx_phy->tx_rise_tune);
+ else
+ imx_phy->tx_rise_tune =
+ phy_tx_rise_tune_from_property(imx_phy->tx_rise_tune);
+
+ if (device_property_read_u32(dev, "fsl,phy-tx-preemp-amp-tune-microamp",
+ &imx_phy->tx_preemp_amp_tune))
+ imx_phy->tx_preemp_amp_tune = PHY_TUNE_DEFAULT;
+ else
+ imx_phy->tx_preemp_amp_tune =
+ phy_tx_preemp_amp_tune_from_property(imx_phy->tx_preemp_amp_tune);
+
+ if (device_property_read_u32(dev, "fsl,phy-tx-vboost-level-microvolt",
+ &imx_phy->tx_vboost_level))
+ imx_phy->tx_vboost_level = PHY_TUNE_DEFAULT;
+ else
+ imx_phy->tx_vboost_level =
+ phy_tx_vboost_level_from_property(imx_phy->tx_vboost_level);
+
+ if (device_property_read_u32(dev, "fsl,phy-comp-dis-tune-percent",
+ &imx_phy->comp_dis_tune))
+ imx_phy->comp_dis_tune = PHY_TUNE_DEFAULT;
+ else if (is_imx95)
+ imx_phy->comp_dis_tune =
+ imx95_phy_comp_dis_tune_from_property(imx_phy->comp_dis_tune);
+ else
+ imx_phy->comp_dis_tune =
+ phy_comp_dis_tune_from_property(imx_phy->comp_dis_tune);
+
+ if (device_property_read_u32(dev, "fsl,phy-pcs-tx-deemph-3p5db-attenuation-db",
+ &imx_phy->pcs_tx_deemph_3p5db))
+ imx_phy->pcs_tx_deemph_3p5db = PHY_TUNE_DEFAULT;
+ else
+ imx_phy->pcs_tx_deemph_3p5db =
+ phy_pcs_tx_deemph_3p5db_from_property(imx_phy->pcs_tx_deemph_3p5db);
+
+ if (device_property_read_u32(dev, "fsl,phy-pcs-tx-swing-full-percent",
+ &imx_phy->pcs_tx_swing_full))
+ imx_phy->pcs_tx_swing_full = PHY_TUNE_DEFAULT;
+ else
+ imx_phy->pcs_tx_swing_full =
+ phy_pcs_tx_swing_full_from_property(imx_phy->pcs_tx_swing_full);
+}
+
+static void imx8m_phy_tune(struct imx8mq_usb_phy *imx_phy)
+{
+ u32 value;
+
+ /* PHY tuning */
+ if (imx_phy->pcs_tx_deemph_3p5db != PHY_TUNE_DEFAULT) {
+ value = readl(imx_phy->base + PHY_CTRL4);
+ value &= ~PHY_CTRL4_PCS_TX_DEEMPH_3P5DB_MASK;
+ value |= FIELD_PREP(PHY_CTRL4_PCS_TX_DEEMPH_3P5DB_MASK,
+ imx_phy->pcs_tx_deemph_3p5db);
+ writel(value, imx_phy->base + PHY_CTRL4);
+ }
+
+ if (imx_phy->pcs_tx_swing_full != PHY_TUNE_DEFAULT) {
+ value = readl(imx_phy->base + PHY_CTRL5);
+ value &= ~PHY_CTRL5_PCS_TX_SWING_FULL_MASK;
+ value |= FIELD_PREP(PHY_CTRL5_PCS_TX_SWING_FULL_MASK,
+ imx_phy->pcs_tx_swing_full);
+ writel(value, imx_phy->base + PHY_CTRL5);
+ }
+
+ if ((imx_phy->tx_vref_tune & imx_phy->tx_rise_tune &
+ imx_phy->tx_preemp_amp_tune & imx_phy->comp_dis_tune &
+ imx_phy->tx_vboost_level) == PHY_TUNE_DEFAULT)
+ /* If all are the default values, no need update. */
+ return;
+
+ value = readl(imx_phy->base + PHY_CTRL3);
+
+ if (imx_phy->tx_vref_tune != PHY_TUNE_DEFAULT) {
+ value &= ~PHY_CTRL3_TXVREF_TUNE_MASK;
+ value |= FIELD_PREP(PHY_CTRL3_TXVREF_TUNE_MASK,
+ imx_phy->tx_vref_tune);
+ }
+
+ if (imx_phy->tx_rise_tune != PHY_TUNE_DEFAULT) {
+ value &= ~PHY_CTRL3_TXRISE_TUNE_MASK;
+ value |= FIELD_PREP(PHY_CTRL3_TXRISE_TUNE_MASK,
+ imx_phy->tx_rise_tune);
+ }
+
+ if (imx_phy->tx_preemp_amp_tune != PHY_TUNE_DEFAULT) {
+ value &= ~PHY_CTRL3_TXPREEMP_TUNE_MASK;
+ value |= FIELD_PREP(PHY_CTRL3_TXPREEMP_TUNE_MASK,
+ imx_phy->tx_preemp_amp_tune);
+ }
+
+ if (imx_phy->comp_dis_tune != PHY_TUNE_DEFAULT) {
+ value &= ~PHY_CTRL3_COMPDISTUNE_MASK;
+ value |= FIELD_PREP(PHY_CTRL3_COMPDISTUNE_MASK,
+ imx_phy->comp_dis_tune);
+ }
+
+ if (imx_phy->tx_vboost_level != PHY_TUNE_DEFAULT) {
+ value &= ~PHY_CTRL3_TX_VBOOST_LEVEL_MASK;
+ value |= FIELD_PREP(PHY_CTRL3_TX_VBOOST_LEVEL_MASK,
+ imx_phy->tx_vboost_level);
+ }
+
+ writel(value, imx_phy->base + PHY_CTRL3);
+}
+
+static int imx8mq_usb_phy_init(struct phy *phy)
+{
+ struct imx8mq_usb_phy *imx_phy = phy_get_drvdata(phy);
+ u32 value;
+
+ value = readl(imx_phy->base + PHY_CTRL1);
+ value &= ~(PHY_CTRL1_VDATSRCENB0 | PHY_CTRL1_VDATDETENB0 |
+ PHY_CTRL1_COMMONONN);
+ value |= PHY_CTRL1_RESET | PHY_CTRL1_ATERESET;
+ writel(value, imx_phy->base + PHY_CTRL1);
+
+ value = readl(imx_phy->base + PHY_CTRL0);
+ value |= PHY_CTRL0_REF_SSP_EN;
+ writel(value, imx_phy->base + PHY_CTRL0);
+
+ value = readl(imx_phy->base + PHY_CTRL2);
+ value |= PHY_CTRL2_TXENABLEN0;
+ writel(value, imx_phy->base + PHY_CTRL2);
+
+ value = readl(imx_phy->base + PHY_CTRL1);
+ value &= ~(PHY_CTRL1_RESET | PHY_CTRL1_ATERESET);
+ writel(value, imx_phy->base + PHY_CTRL1);
+
+ return 0;
+}
+
+static int imx8mp_usb_phy_init(struct phy *phy)
+{
+ struct imx8mq_usb_phy *imx_phy = phy_get_drvdata(phy);
+ u32 value;
+
+ /* USB3.0 PHY signal fsel for 24M ref */
+ value = readl(imx_phy->base + PHY_CTRL0);
+ value &= ~PHY_CTRL0_FSEL_MASK;
+ value |= FIELD_PREP(PHY_CTRL0_FSEL_MASK, imx_phy->alt_clk ?
+ PHY_CTRL0_FSEL_100M : PHY_CTRL0_FSEL_24M);
+ writel(value, imx_phy->base + PHY_CTRL0);
+
+ /* Disable alt_clk_en and use internal MPLL clocks */
+ value = readl(imx_phy->base + PHY_CTRL6);
+ value &= ~(PHY_CTRL6_ALT_CLK_SEL | PHY_CTRL6_ALT_CLK_EN);
+ writel(value, imx_phy->base + PHY_CTRL6);
+
+ value = readl(imx_phy->base + PHY_CTRL1);
+ value &= ~(PHY_CTRL1_VDATSRCENB0 | PHY_CTRL1_VDATDETENB0);
+ value |= PHY_CTRL1_RESET | PHY_CTRL1_ATERESET;
+ writel(value, imx_phy->base + PHY_CTRL1);
+
+ value = readl(imx_phy->base + PHY_CTRL0);
+ value |= PHY_CTRL0_REF_SSP_EN;
+ value &= ~PHY_CTRL0_SSC_RANGE_MASK;
+ value |= FIELD_PREP(PHY_CTRL0_SSC_RANGE_MASK,
+ PHY_CTRL0_SSC_RANGE_4003PPM);
+ writel(value, imx_phy->base + PHY_CTRL0);
+
+ value = readl(imx_phy->base + PHY_CTRL2);
+ value |= PHY_CTRL2_TXENABLEN0 | PHY_CTRL2_OTG_DISABLE;
+ writel(value, imx_phy->base + PHY_CTRL2);
+
+ udelay(10);
+
+ value = readl(imx_phy->base + PHY_CTRL1);
+ value &= ~(PHY_CTRL1_RESET | PHY_CTRL1_ATERESET);
+ writel(value, imx_phy->base + PHY_CTRL1);
+
+ imx8m_phy_tune(imx_phy);
+
+ if (imx_phy->tca)
+ tca_blk_init(imx_phy->tca);
+
+ return 0;
+}
+
+static int imx8mq_phy_power_on(struct phy *phy)
+{
+ struct imx8mq_usb_phy *imx_phy = phy_get_drvdata(phy);
+ u32 value;
+ int ret;
+
+ ret = regulator_enable(imx_phy->vbus);
+ if (ret)
+ return ret;
+
+ /* Disable rx term override */
+ value = readl(imx_phy->base + PHY_CTRL6);
+ value &= ~PHY_CTRL6_RXTERM_OVERRIDE_SEL;
+ writel(value, imx_phy->base + PHY_CTRL6);
+
+ return 0;
+}
+
+static int imx8mq_phy_power_off(struct phy *phy)
+{
+ struct imx8mq_usb_phy *imx_phy = phy_get_drvdata(phy);
+ u32 value;
+
+ /* Override rx term to be 0 */
+ value = readl(imx_phy->base + PHY_CTRL6);
+ value |= PHY_CTRL6_RXTERM_OVERRIDE_SEL;
+ writel(value, imx_phy->base + PHY_CTRL6);
+
+ regulator_disable(imx_phy->vbus);
+
+ return 0;
+}
+
+static const struct phy_ops imx8mq_usb_phy_ops = {
+ .init = imx8mq_usb_phy_init,
+ .power_on = imx8mq_phy_power_on,
+ .power_off = imx8mq_phy_power_off,
+ .owner = THIS_MODULE,
+};
+
+static const struct phy_ops imx8mp_usb_phy_ops = {
+ .init = imx8mp_usb_phy_init,
+ .power_on = imx8mq_phy_power_on,
+ .power_off = imx8mq_phy_power_off,
+ .owner = THIS_MODULE,
+};
+
+static const struct imx8mq_usb_phy_drvdata imx8mq_usb_phy_data = {
+ .ops = &imx8mq_usb_phy_ops,
+};
+
+static const struct imx8mq_usb_phy_drvdata imx8mp_usb_phy_data = {
+ .ops = &imx8mp_usb_phy_ops,
+ .need_genpd_rpm_on = true,
+};
+
+static const struct imx8mq_usb_phy_drvdata imx95_usb_phy_data = {
+ .ops = &imx8mp_usb_phy_ops,
+};
+
+static const struct of_device_id imx8mq_usb_phy_of_match[] = {
+ {.compatible = "fsl,imx8mq-usb-phy",
+ .data = &imx8mq_usb_phy_data,},
+ {.compatible = "fsl,imx8mp-usb-phy",
+ .data = &imx8mp_usb_phy_data,},
+ {.compatible = "fsl,imx95-usb-phy",
+ .data = &imx95_usb_phy_data,},
+ { }
+};
+MODULE_DEVICE_TABLE(of, imx8mq_usb_phy_of_match);
+
+static const struct regmap_config imx_cr_regmap_config = {
+ .name = "cr",
+ .reg_bits = 32,
+ .val_bits = 32,
+ .reg_stride = 4,
+ .max_register = 0x7,
+};
+
+static int imx8mq_usb_phy_probe(struct platform_device *pdev)
+{
+ struct phy_provider *phy_provider;
+ struct device *dev = &pdev->dev;
+ struct imx8mq_usb_phy *imx_phy;
+ const struct imx8mq_usb_phy_drvdata *phy_data;
+ int ret;
+
+ imx_phy = devm_kzalloc(dev, sizeof(*imx_phy), GFP_KERNEL);
+ if (!imx_phy)
+ return -ENOMEM;
+
+ platform_set_drvdata(pdev, imx_phy);
+
+ imx_phy->clk = devm_clk_get_enabled(dev, "phy");
+ if (IS_ERR(imx_phy->clk)) {
+ dev_err(dev, "failed to get imx8mq usb phy clock\n");
+ return PTR_ERR(imx_phy->clk);
+ }
+
+ imx_phy->alt_clk = devm_clk_get_optional_enabled(dev, "alt");
+ if (IS_ERR(imx_phy->alt_clk))
+ return dev_err_probe(dev, PTR_ERR(imx_phy->alt_clk),
+ "Failed to get alt clk\n");
+
+ imx_phy->base = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(imx_phy->base))
+ return PTR_ERR(imx_phy->base);
+
+ imx_phy->cr_regmap = devm_regmap_init_mmio(dev, imx_phy->base + PHY_CRCTL,
+ &imx_cr_regmap_config);
+ if (IS_ERR(imx_phy->cr_regmap)) {
+ dev_warn(dev, "Fail to init debug register regmap\n");
+ imx_phy->cr_regmap = NULL;
+ }
+
+ imx_phy->vbus = devm_regulator_get(dev, "vbus");
+ if (IS_ERR(imx_phy->vbus))
+ return dev_err_probe(dev, PTR_ERR(imx_phy->vbus), "failed to get vbus\n");
+
+ phy_data = of_device_get_match_data(dev);
+ if (!phy_data)
+ return -EINVAL;
+
+ if (phy_data->need_genpd_rpm_on) {
+ ret = dev_pm_genpd_rpm_always_on(dev, true);
+ if (ret && ret != -EOPNOTSUPP)
+ dev_warn(dev, "failed to set genpd rpm always on\n");
+ }
+
+ pm_runtime_set_active(dev);
+ pm_runtime_enable(dev);
+
+ imx_phy->phy = devm_phy_create(dev, NULL, phy_data->ops);
+ if (IS_ERR(imx_phy->phy)) {
+ ret = dev_err_probe(dev, PTR_ERR(imx_phy->phy),
+ "failed to create PHY\n");
+ goto disable_rpm;
+ }
+
+ phy_set_drvdata(imx_phy->phy, imx_phy);
+
+ imx_phy->tca = imx95_usb_phy_get_tca(pdev, imx_phy);
+ if (IS_ERR(imx_phy->tca)) {
+ ret = dev_err_probe(dev, PTR_ERR(imx_phy->tca),
+ "failed to get tca\n");
+ goto disable_rpm;
+ }
+
+ imx8m_get_phy_tuning_data(imx_phy);
+ device_set_wakeup_capable(dev, true);
+
+ phy_provider = devm_of_phy_provider_register(dev, of_phy_simple_xlate);
+ if (IS_ERR(phy_provider)) {
+ ret = dev_err_probe(dev, PTR_ERR(phy_provider),
+ "failed to register phy provider\n");
+ goto disable_rpm;
+ }
+
+ return 0;
+
+disable_rpm:
+ pm_runtime_disable(dev);
+ return ret;
+}
+
+static void imx8mq_usb_phy_remove(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ int ret;
+
+ ret = pm_runtime_get_sync(dev);
+ if (ret < 0)
+ dev_warn(dev, "failed to resume on remove: %d\n", ret);
+
+ pm_runtime_disable(dev);
+ pm_runtime_put_noidle(dev);
+}
+
+static int imx8mq_usb_phy_runtime_suspend(struct device *dev)
+{
+ struct imx8mq_usb_phy *imx_phy = dev_get_drvdata(dev);
+
+ if (imx_phy->cr_regmap)
+ regcache_cache_only(imx_phy->cr_regmap, true);
+
+ clk_disable_unprepare(imx_phy->alt_clk);
+ clk_disable_unprepare(imx_phy->clk);
+
+ return 0;
+}
+
+static int imx8mq_usb_phy_runtime_resume(struct device *dev)
+{
+ struct imx8mq_usb_phy *imx_phy = dev_get_drvdata(dev);
+ int ret;
+
+ ret = clk_prepare_enable(imx_phy->clk);
+ if (ret)
+ return ret;
+
+ ret = clk_prepare_enable(imx_phy->alt_clk);
+ if (ret) {
+ clk_disable_unprepare(imx_phy->clk);
+ return ret;
+ }
+
+ if (imx_phy->cr_regmap)
+ regcache_cache_only(imx_phy->cr_regmap, false);
+
+ return 0;
+}
+
+static DEFINE_RUNTIME_DEV_PM_OPS(imx8mq_usb_phy_pm_ops, imx8mq_usb_phy_runtime_suspend,
+ imx8mq_usb_phy_runtime_resume, NULL);
+
+static struct platform_driver imx8mq_usb_phy_driver = {
+ .probe = imx8mq_usb_phy_probe,
+ .remove = imx8mq_usb_phy_remove,
+ .driver = {
+ .name = "imx8mq-usb-phy",
+ .of_match_table = imx8mq_usb_phy_of_match,
+ .pm = pm_ptr(&imx8mq_usb_phy_pm_ops),
+ .suppress_bind_attrs = true,
+ }
+};
+module_platform_driver(imx8mq_usb_phy_driver);
+
+MODULE_DESCRIPTION("FSL IMX8MQ USB PHY driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/phy/freescale/phy-fsl-imx8qm-hsio.c b/drivers/phy/freescale/phy-fsl-imx8qm-hsio.c
new file mode 100644
index 000000000..4ab45c9f5
--- /dev/null
+++ b/drivers/phy/freescale/phy-fsl-imx8qm-hsio.c
@@ -0,0 +1,629 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright 2024 NXP
+ */
+
+#include <linux/bitfield.h>
+#include <linux/clk.h>
+#include <linux/delay.h>
+#include <linux/io.h>
+#include <linux/iopoll.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/pci_regs.h>
+#include <linux/phy/phy.h>
+#include <linux/phy/pcie.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+
+#include <dt-bindings/phy/phy.h>
+#include <dt-bindings/phy/phy-imx8-pcie.h>
+
+#define MAX_NUM_LANE 3
+#define LANE_NUM_CLKS 5
+
+/* Parameters for the waiting for PCIe PHY PLL to lock */
+#define PHY_INIT_WAIT_USLEEP_MAX 10
+#define PHY_INIT_WAIT_TIMEOUT (1000 * PHY_INIT_WAIT_USLEEP_MAX)
+
+/* i.MX8Q HSIO registers */
+#define HSIO_CTRL0 0x0
+#define HSIO_APB_RSTN_0 BIT(0)
+#define HSIO_APB_RSTN_1 BIT(1)
+#define HSIO_PIPE_RSTN_0_MASK GENMASK(25, 24)
+#define HSIO_PIPE_RSTN_1_MASK GENMASK(27, 26)
+#define HSIO_MODE_MASK GENMASK(20, 17)
+#define HSIO_MODE_PCIE 0x0
+#define HSIO_MODE_SATA 0x4
+#define HSIO_DEVICE_TYPE_MASK GENMASK(27, 24)
+#define HSIO_EPCS_TXDEEMP BIT(5)
+#define HSIO_EPCS_TXDEEMP_SEL BIT(6)
+#define HSIO_EPCS_PHYRESET_N BIT(7)
+#define HSIO_RESET_N BIT(12)
+
+#define HSIO_IOB_RXENA BIT(0)
+#define HSIO_IOB_TXENA BIT(1)
+#define HSIO_IOB_A_0_TXOE BIT(2)
+#define HSIO_IOB_A_0_M1M0_2 BIT(4)
+#define HSIO_IOB_A_0_M1M0_MASK GENMASK(4, 3)
+#define HSIO_PHYX1_EPCS_SEL BIT(12)
+#define HSIO_PCIE_AB_SELECT BIT(13)
+
+#define HSIO_PHY_STS0 0x4
+#define HSIO_LANE0_TX_PLL_LOCK BIT(4)
+#define HSIO_LANE1_TX_PLL_LOCK BIT(12)
+
+#define HSIO_CTRL2 0x8
+#define HSIO_LTSSM_ENABLE BIT(4)
+#define HSIO_BUTTON_RST_N BIT(21)
+#define HSIO_PERST_N BIT(22)
+#define HSIO_POWER_UP_RST_N BIT(23)
+
+#define HSIO_PCIE_STS0 0xc
+#define HSIO_PM_REQ_CORE_RST BIT(19)
+
+#define HSIO_REG48_PMA_STATUS 0x30
+#define HSIO_REG48_PMA_RDY BIT(7)
+
+struct imx_hsio_drvdata {
+ int lane_num;
+};
+
+struct imx_hsio_lane {
+ u32 ctrl_index;
+ u32 ctrl_off;
+ u32 idx;
+ u32 phy_off;
+ u32 phy_type;
+ const char * const *clk_names;
+ struct clk_bulk_data clks[LANE_NUM_CLKS];
+ struct imx_hsio_priv *priv;
+ struct phy *phy;
+ enum phy_mode phy_mode;
+};
+
+struct imx_hsio_priv {
+ void __iomem *base;
+ struct device *dev;
+ struct mutex lock;
+ const char *hsio_cfg;
+ const char *refclk_pad;
+ u32 open_cnt;
+ struct regmap *phy;
+ struct regmap *ctrl;
+ struct regmap *misc;
+ const struct imx_hsio_drvdata *drvdata;
+ struct imx_hsio_lane lane[MAX_NUM_LANE];
+};
+
+static const char * const lan0_pcie_clks[] = {"apb_pclk0", "pclk0", "ctl0_crr",
+ "phy0_crr", "misc_crr"};
+static const char * const lan1_pciea_clks[] = {"apb_pclk1", "pclk1", "ctl0_crr",
+ "phy0_crr", "misc_crr"};
+static const char * const lan1_pcieb_clks[] = {"apb_pclk1", "pclk1", "ctl1_crr",
+ "phy0_crr", "misc_crr"};
+static const char * const lan2_pcieb_clks[] = {"apb_pclk2", "pclk2", "ctl1_crr",
+ "phy1_crr", "misc_crr"};
+static const char * const lan2_sata_clks[] = {"pclk2", "epcs_tx", "epcs_rx",
+ "phy1_crr", "misc_crr"};
+
+static const struct regmap_config regmap_phy_config = {
+ .name = "phy",
+ .reg_bits = 32,
+ .val_bits = 32,
+ .reg_stride = 4,
+};
+
+static const struct regmap_config regmap_ctrl_config = {
+ .name = "ctrl",
+ .reg_bits = 32,
+ .val_bits = 32,
+ .reg_stride = 4,
+};
+
+static const struct regmap_config regmap_misc_config = {
+ .name = "misc",
+ .reg_bits = 32,
+ .val_bits = 32,
+ .reg_stride = 4,
+};
+
+static int imx_hsio_init(struct phy *phy)
+{
+ int ret, i;
+ struct imx_hsio_lane *lane = phy_get_drvdata(phy);
+ struct imx_hsio_priv *priv = lane->priv;
+ struct device *dev = priv->dev;
+
+ /* Assign clocks refer to different modes */
+ switch (lane->phy_type) {
+ case PHY_TYPE_PCIE:
+ lane->phy_mode = PHY_MODE_PCIE;
+ if (lane->ctrl_index == 0) { /* PCIEA */
+ lane->ctrl_off = 0;
+ lane->phy_off = 0;
+
+ for (i = 0; i < LANE_NUM_CLKS; i++) {
+ if (lane->idx == 0)
+ lane->clks[i].id = lan0_pcie_clks[i];
+ else
+ lane->clks[i].id = lan1_pciea_clks[i];
+ }
+ } else { /* PCIEB */
+ if (lane->idx == 0) { /* i.MX8QXP */
+ lane->ctrl_off = 0;
+ lane->phy_off = 0;
+ } else {
+ /*
+ * On i.MX8QM, only second or third lane can be
+ * bound to PCIEB.
+ */
+ lane->ctrl_off = SZ_64K;
+ if (lane->idx == 1)
+ lane->phy_off = 0;
+ else /* the third lane is bound to PCIEB */
+ lane->phy_off = SZ_64K;
+ }
+
+ for (i = 0; i < LANE_NUM_CLKS; i++) {
+ if (lane->idx == 1)
+ lane->clks[i].id = lan1_pcieb_clks[i];
+ else if (lane->idx == 2)
+ lane->clks[i].id = lan2_pcieb_clks[i];
+ else /* i.MX8QXP only has PCIEB, idx is 0 */
+ lane->clks[i].id = lan0_pcie_clks[i];
+ }
+ }
+ break;
+ case PHY_TYPE_SATA:
+ /* On i.MX8QM, only the third lane can be bound to SATA */
+ lane->phy_mode = PHY_MODE_SATA;
+ lane->ctrl_off = SZ_128K;
+ lane->phy_off = SZ_64K;
+
+ for (i = 0; i < LANE_NUM_CLKS; i++)
+ lane->clks[i].id = lan2_sata_clks[i];
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ /* Fetch clocks and enable them */
+ ret = devm_clk_bulk_get(dev, LANE_NUM_CLKS, lane->clks);
+ if (ret)
+ return ret;
+ ret = clk_bulk_prepare_enable(LANE_NUM_CLKS, lane->clks);
+ if (ret)
+ return ret;
+
+ /* allow the clocks to stabilize */
+ usleep_range(200, 500);
+ return 0;
+}
+
+static int imx_hsio_exit(struct phy *phy)
+{
+ struct imx_hsio_lane *lane = phy_get_drvdata(phy);
+
+ clk_bulk_disable_unprepare(LANE_NUM_CLKS, lane->clks);
+
+ return 0;
+}
+
+static void imx_hsio_pcie_phy_resets(struct phy *phy)
+{
+ struct imx_hsio_lane *lane = phy_get_drvdata(phy);
+ struct imx_hsio_priv *priv = lane->priv;
+
+ regmap_clear_bits(priv->ctrl, lane->ctrl_off + HSIO_CTRL2,
+ HSIO_BUTTON_RST_N);
+ regmap_clear_bits(priv->ctrl, lane->ctrl_off + HSIO_CTRL2,
+ HSIO_PERST_N);
+ regmap_clear_bits(priv->ctrl, lane->ctrl_off + HSIO_CTRL2,
+ HSIO_POWER_UP_RST_N);
+ regmap_set_bits(priv->ctrl, lane->ctrl_off + HSIO_CTRL2,
+ HSIO_BUTTON_RST_N);
+ regmap_set_bits(priv->ctrl, lane->ctrl_off + HSIO_CTRL2,
+ HSIO_PERST_N);
+ regmap_set_bits(priv->ctrl, lane->ctrl_off + HSIO_CTRL2,
+ HSIO_POWER_UP_RST_N);
+
+ if (lane->idx == 1) {
+ regmap_set_bits(priv->phy, lane->phy_off + HSIO_CTRL0,
+ HSIO_APB_RSTN_1);
+ regmap_set_bits(priv->phy, lane->phy_off + HSIO_CTRL0,
+ HSIO_PIPE_RSTN_1_MASK);
+ } else {
+ regmap_set_bits(priv->phy, lane->phy_off + HSIO_CTRL0,
+ HSIO_APB_RSTN_0);
+ regmap_set_bits(priv->phy, lane->phy_off + HSIO_CTRL0,
+ HSIO_PIPE_RSTN_0_MASK);
+ }
+}
+
+static void imx_hsio_sata_phy_resets(struct phy *phy)
+{
+ struct imx_hsio_lane *lane = phy_get_drvdata(phy);
+ struct imx_hsio_priv *priv = lane->priv;
+
+ /* clear PHY RST, then set it */
+ regmap_clear_bits(priv->ctrl, lane->ctrl_off + HSIO_CTRL0,
+ HSIO_EPCS_PHYRESET_N);
+ regmap_set_bits(priv->ctrl, lane->ctrl_off + HSIO_CTRL0,
+ HSIO_EPCS_PHYRESET_N);
+
+ /* CTRL RST: SET -> delay 1 us -> CLEAR -> SET */
+ regmap_set_bits(priv->ctrl, lane->ctrl_off + HSIO_CTRL0, HSIO_RESET_N);
+ udelay(1);
+ regmap_clear_bits(priv->ctrl, lane->ctrl_off + HSIO_CTRL0,
+ HSIO_RESET_N);
+ regmap_set_bits(priv->ctrl, lane->ctrl_off + HSIO_CTRL0, HSIO_RESET_N);
+}
+
+static void imx_hsio_configure_clk_pad(struct phy *phy)
+{
+ bool pll = false;
+ struct imx_hsio_lane *lane = phy_get_drvdata(phy);
+ struct imx_hsio_priv *priv = lane->priv;
+
+ if (priv->refclk_pad && strncmp(priv->refclk_pad, "output", 6) == 0) {
+ pll = true;
+ regmap_update_bits(priv->misc, HSIO_CTRL0,
+ HSIO_IOB_A_0_TXOE | HSIO_IOB_A_0_M1M0_MASK,
+ HSIO_IOB_A_0_TXOE | HSIO_IOB_A_0_M1M0_2);
+ } else {
+ regmap_update_bits(priv->misc, HSIO_CTRL0,
+ HSIO_IOB_A_0_TXOE | HSIO_IOB_A_0_M1M0_MASK,
+ 0);
+ }
+
+ regmap_update_bits(priv->misc, HSIO_CTRL0, HSIO_IOB_RXENA,
+ pll ? 0 : HSIO_IOB_RXENA);
+ regmap_update_bits(priv->misc, HSIO_CTRL0, HSIO_IOB_TXENA,
+ pll ? HSIO_IOB_TXENA : 0);
+}
+
+static void imx_hsio_pre_set(struct phy *phy)
+{
+ struct imx_hsio_lane *lane = phy_get_drvdata(phy);
+ struct imx_hsio_priv *priv = lane->priv;
+
+ if (strncmp(priv->hsio_cfg, "pciea-x2-pcieb", 14) == 0) {
+ regmap_set_bits(priv->misc, HSIO_CTRL0, HSIO_PCIE_AB_SELECT);
+ } else if (strncmp(priv->hsio_cfg, "pciea-x2-sata", 13) == 0) {
+ regmap_set_bits(priv->misc, HSIO_CTRL0, HSIO_PHYX1_EPCS_SEL);
+ } else if (strncmp(priv->hsio_cfg, "pciea-pcieb-sata", 16) == 0) {
+ regmap_set_bits(priv->misc, HSIO_CTRL0, HSIO_PCIE_AB_SELECT);
+ regmap_set_bits(priv->misc, HSIO_CTRL0, HSIO_PHYX1_EPCS_SEL);
+ }
+
+ imx_hsio_configure_clk_pad(phy);
+}
+
+static int imx_hsio_pcie_power_on(struct phy *phy)
+{
+ int ret;
+ u32 val, addr, cond;
+ struct imx_hsio_lane *lane = phy_get_drvdata(phy);
+ struct imx_hsio_priv *priv = lane->priv;
+
+ imx_hsio_pcie_phy_resets(phy);
+
+ /* Toggle apb_pclk to make sure PM_REQ_CORE_RST is cleared. */
+ clk_disable_unprepare(lane->clks[0].clk);
+ mdelay(1);
+ ret = clk_prepare_enable(lane->clks[0].clk);
+ if (ret) {
+ dev_err(priv->dev, "unable to enable phy apb_pclk\n");
+ return ret;
+ }
+
+ addr = lane->ctrl_off + HSIO_PCIE_STS0;
+ cond = HSIO_PM_REQ_CORE_RST;
+ ret = regmap_read_poll_timeout(priv->ctrl, addr, val,
+ (val & cond) == 0,
+ PHY_INIT_WAIT_USLEEP_MAX,
+ PHY_INIT_WAIT_TIMEOUT);
+ if (ret)
+ dev_err(priv->dev, "HSIO_PM_REQ_CORE_RST is set\n");
+ return ret;
+}
+
+static int imx_hsio_sata_power_on(struct phy *phy)
+{
+ int ret;
+ u32 val, cond;
+ struct imx_hsio_lane *lane = phy_get_drvdata(phy);
+ struct imx_hsio_priv *priv = lane->priv;
+
+ regmap_set_bits(priv->phy, lane->phy_off + HSIO_CTRL0, HSIO_APB_RSTN_0);
+ regmap_set_bits(priv->ctrl, lane->ctrl_off + HSIO_CTRL0,
+ HSIO_EPCS_TXDEEMP);
+ regmap_set_bits(priv->ctrl, lane->ctrl_off + HSIO_CTRL0,
+ HSIO_EPCS_TXDEEMP_SEL);
+
+ imx_hsio_sata_phy_resets(phy);
+
+ cond = HSIO_REG48_PMA_RDY;
+ ret = read_poll_timeout(readb, val, ((val & cond) == cond),
+ PHY_INIT_WAIT_USLEEP_MAX,
+ PHY_INIT_WAIT_TIMEOUT, false,
+ priv->base + HSIO_REG48_PMA_STATUS);
+ if (ret)
+ dev_err(priv->dev, "PHY calibration is timeout\n");
+ else
+ dev_dbg(priv->dev, "PHY calibration is done\n");
+
+ return ret;
+}
+
+static int imx_hsio_power_on(struct phy *phy)
+{
+ int ret;
+ u32 val, cond;
+ struct imx_hsio_lane *lane = phy_get_drvdata(phy);
+ struct imx_hsio_priv *priv = lane->priv;
+
+ scoped_guard(mutex, &priv->lock) {
+ if (!priv->open_cnt)
+ imx_hsio_pre_set(phy);
+ priv->open_cnt++;
+ }
+
+ if (lane->phy_mode == PHY_MODE_PCIE)
+ ret = imx_hsio_pcie_power_on(phy);
+ else /* SATA */
+ ret = imx_hsio_sata_power_on(phy);
+ if (ret)
+ return ret;
+
+ /* Polling to check the PHY is ready or not. */
+ if (lane->idx == 1)
+ cond = HSIO_LANE1_TX_PLL_LOCK;
+ else
+ /*
+ * Except the phy_off, the bit-offset of lane2 is same to lane0.
+ * Merge the lane0 and lane2 bit-operations together.
+ */
+ cond = HSIO_LANE0_TX_PLL_LOCK;
+
+ ret = regmap_read_poll_timeout(priv->phy, lane->phy_off + HSIO_PHY_STS0,
+ val, ((val & cond) == cond),
+ PHY_INIT_WAIT_USLEEP_MAX,
+ PHY_INIT_WAIT_TIMEOUT);
+ if (ret) {
+ dev_err(priv->dev, "IMX8Q PHY%d PLL lock timeout\n", lane->idx);
+ return ret;
+ }
+ dev_dbg(priv->dev, "IMX8Q PHY%d PLL is locked\n", lane->idx);
+
+ return ret;
+}
+
+static int imx_hsio_power_off(struct phy *phy)
+{
+ struct imx_hsio_lane *lane = phy_get_drvdata(phy);
+ struct imx_hsio_priv *priv = lane->priv;
+
+ scoped_guard(mutex, &priv->lock) {
+ priv->open_cnt--;
+ if (priv->open_cnt == 0) {
+ regmap_clear_bits(priv->misc, HSIO_CTRL0,
+ HSIO_PCIE_AB_SELECT);
+ regmap_clear_bits(priv->misc, HSIO_CTRL0,
+ HSIO_PHYX1_EPCS_SEL);
+
+ if (lane->phy_mode == PHY_MODE_PCIE) {
+ regmap_clear_bits(priv->ctrl,
+ lane->ctrl_off + HSIO_CTRL2,
+ HSIO_BUTTON_RST_N);
+ regmap_clear_bits(priv->ctrl,
+ lane->ctrl_off + HSIO_CTRL2,
+ HSIO_PERST_N);
+ regmap_clear_bits(priv->ctrl,
+ lane->ctrl_off + HSIO_CTRL2,
+ HSIO_POWER_UP_RST_N);
+ } else {
+ regmap_clear_bits(priv->ctrl,
+ lane->ctrl_off + HSIO_CTRL0,
+ HSIO_EPCS_TXDEEMP);
+ regmap_clear_bits(priv->ctrl,
+ lane->ctrl_off + HSIO_CTRL0,
+ HSIO_EPCS_TXDEEMP_SEL);
+ regmap_clear_bits(priv->ctrl,
+ lane->ctrl_off + HSIO_CTRL0,
+ HSIO_RESET_N);
+ }
+
+ if (lane->idx == 1) {
+ regmap_clear_bits(priv->phy,
+ lane->phy_off + HSIO_CTRL0,
+ HSIO_APB_RSTN_1);
+ regmap_clear_bits(priv->phy,
+ lane->phy_off + HSIO_CTRL0,
+ HSIO_PIPE_RSTN_1_MASK);
+ } else {
+ /*
+ * Except the phy_off, the bit-offset of lane2 is same
+ * to lane0. Merge the lane0 and lane2 bit-operations
+ * together.
+ */
+ regmap_clear_bits(priv->phy,
+ lane->phy_off + HSIO_CTRL0,
+ HSIO_APB_RSTN_0);
+ regmap_clear_bits(priv->phy,
+ lane->phy_off + HSIO_CTRL0,
+ HSIO_PIPE_RSTN_0_MASK);
+ }
+ }
+ }
+
+ return 0;
+}
+
+static int imx_hsio_set_mode(struct phy *phy, enum phy_mode mode,
+ int submode)
+{
+ u32 val;
+ struct imx_hsio_lane *lane = phy_get_drvdata(phy);
+ struct imx_hsio_priv *priv = lane->priv;
+
+ if (lane->phy_mode != mode)
+ return -EINVAL;
+
+ val = (mode == PHY_MODE_PCIE) ? HSIO_MODE_PCIE : HSIO_MODE_SATA;
+ val = FIELD_PREP(HSIO_MODE_MASK, val);
+ regmap_update_bits(priv->phy, lane->phy_off + HSIO_CTRL0,
+ HSIO_MODE_MASK, val);
+
+ switch (submode) {
+ case PHY_MODE_PCIE_RC:
+ val = FIELD_PREP(HSIO_DEVICE_TYPE_MASK, PCI_EXP_TYPE_ROOT_PORT);
+ break;
+ case PHY_MODE_PCIE_EP:
+ val = FIELD_PREP(HSIO_DEVICE_TYPE_MASK, PCI_EXP_TYPE_ENDPOINT);
+ break;
+ default: /* Support only PCIe EP and RC now. */
+ return 0;
+ }
+ if (submode)
+ regmap_update_bits(priv->ctrl, lane->ctrl_off + HSIO_CTRL0,
+ HSIO_DEVICE_TYPE_MASK, val);
+
+ return 0;
+}
+
+static int imx_hsio_set_speed(struct phy *phy, int speed)
+{
+ struct imx_hsio_lane *lane = phy_get_drvdata(phy);
+ struct imx_hsio_priv *priv = lane->priv;
+
+ regmap_update_bits(priv->ctrl, lane->ctrl_off + HSIO_CTRL2,
+ HSIO_LTSSM_ENABLE,
+ speed ? HSIO_LTSSM_ENABLE : 0);
+ return 0;
+}
+
+static const struct phy_ops imx_hsio_ops = {
+ .init = imx_hsio_init,
+ .exit = imx_hsio_exit,
+ .power_on = imx_hsio_power_on,
+ .power_off = imx_hsio_power_off,
+ .set_mode = imx_hsio_set_mode,
+ .set_speed = imx_hsio_set_speed,
+ .owner = THIS_MODULE,
+};
+
+static const struct imx_hsio_drvdata imx8qxp_hsio_drvdata = {
+ .lane_num = 0x1,
+};
+
+static const struct imx_hsio_drvdata imx8qm_hsio_drvdata = {
+ .lane_num = 0x3,
+};
+
+static const struct of_device_id imx_hsio_of_match[] = {
+ {.compatible = "fsl,imx8qm-hsio", .data = &imx8qm_hsio_drvdata},
+ {.compatible = "fsl,imx8qxp-hsio", .data = &imx8qxp_hsio_drvdata},
+ { },
+};
+MODULE_DEVICE_TABLE(of, imx_hsio_of_match);
+
+static struct phy *imx_hsio_xlate(struct device *dev,
+ const struct of_phandle_args *args)
+{
+ struct imx_hsio_priv *priv = dev_get_drvdata(dev);
+ int idx = args->args[0];
+ int phy_type = args->args[1];
+ int ctrl_index = args->args[2];
+
+ if (idx < 0 || idx >= priv->drvdata->lane_num)
+ return ERR_PTR(-EINVAL);
+ priv->lane[idx].idx = idx;
+ priv->lane[idx].phy_type = phy_type;
+ priv->lane[idx].ctrl_index = ctrl_index;
+
+ return priv->lane[idx].phy;
+}
+
+static int imx_hsio_probe(struct platform_device *pdev)
+{
+ int i, ret;
+ void __iomem *off;
+ struct device *dev = &pdev->dev;
+ struct device_node *np = dev->of_node;
+ struct imx_hsio_priv *priv;
+ struct phy_provider *provider;
+
+ priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
+ if (!priv)
+ return -ENOMEM;
+ priv->dev = &pdev->dev;
+ priv->drvdata = of_device_get_match_data(dev);
+ ret = devm_mutex_init(dev, &priv->lock);
+ if (ret)
+ return ret;
+
+ /* Get HSIO configuration mode */
+ if (of_property_read_string(np, "fsl,hsio-cfg", &priv->hsio_cfg))
+ priv->hsio_cfg = "pciea-pcieb-sata";
+ /* Get PHY refclk pad mode */
+ if (of_property_read_string(np, "fsl,refclk-pad-mode",
+ &priv->refclk_pad))
+ priv->refclk_pad = NULL;
+
+ priv->base = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(priv->base))
+ return PTR_ERR(priv->base);
+
+ off = devm_platform_ioremap_resource_byname(pdev, "phy");
+ priv->phy = devm_regmap_init_mmio(dev, off, &regmap_phy_config);
+ if (IS_ERR(priv->phy))
+ return dev_err_probe(dev, PTR_ERR(priv->phy),
+ "unable to find phy csr registers\n");
+
+ off = devm_platform_ioremap_resource_byname(pdev, "ctrl");
+ priv->ctrl = devm_regmap_init_mmio(dev, off, &regmap_ctrl_config);
+ if (IS_ERR(priv->ctrl))
+ return dev_err_probe(dev, PTR_ERR(priv->ctrl),
+ "unable to find ctrl csr registers\n");
+
+ off = devm_platform_ioremap_resource_byname(pdev, "misc");
+ priv->misc = devm_regmap_init_mmio(dev, off, &regmap_misc_config);
+ if (IS_ERR(priv->misc))
+ return dev_err_probe(dev, PTR_ERR(priv->misc),
+ "unable to find misc csr registers\n");
+
+ for (i = 0; i < priv->drvdata->lane_num; i++) {
+ struct imx_hsio_lane *lane = &priv->lane[i];
+ struct phy *phy;
+
+ phy = devm_phy_create(&pdev->dev, NULL, &imx_hsio_ops);
+ if (IS_ERR(phy))
+ return PTR_ERR(phy);
+
+ lane->priv = priv;
+ lane->phy = phy;
+ lane->idx = i;
+ phy_set_drvdata(phy, lane);
+ }
+
+ dev_set_drvdata(dev, priv);
+ dev_set_drvdata(&pdev->dev, priv);
+
+ provider = devm_of_phy_provider_register(&pdev->dev, imx_hsio_xlate);
+
+ return PTR_ERR_OR_ZERO(provider);
+}
+
+static struct platform_driver imx_hsio_driver = {
+ .probe = imx_hsio_probe,
+ .driver = {
+ .name = "imx8qm-hsio-phy",
+ .of_match_table = imx_hsio_of_match,
+ }
+};
+module_platform_driver(imx_hsio_driver);
+
+MODULE_DESCRIPTION("FSL IMX8QM HSIO SERDES PHY driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/phy/freescale/phy-fsl-imx8qm-lvds-phy.c b/drivers/phy/freescale/phy-fsl-imx8qm-lvds-phy.c
new file mode 100644
index 000000000..368679436
--- /dev/null
+++ b/drivers/phy/freescale/phy-fsl-imx8qm-lvds-phy.c
@@ -0,0 +1,436 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright 2017-2020,2022 NXP
+ */
+
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+#include <linux/clk.h>
+#include <linux/mfd/syscon.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/phy/phy.h>
+#include <linux/platform_device.h>
+#include <linux/pm_runtime.h>
+#include <linux/regmap.h>
+#include <linux/units.h>
+
+#define REG_SET 0x4
+#define REG_CLR 0x8
+
+#define PHY_CTRL 0x0
+#define M_MASK GENMASK(18, 17)
+#define M(n) FIELD_PREP(M_MASK, (n))
+#define CCM_MASK GENMASK(16, 14)
+#define CCM(n) FIELD_PREP(CCM_MASK, (n))
+#define CA_MASK GENMASK(13, 11)
+#define CA(n) FIELD_PREP(CA_MASK, (n))
+#define TST_MASK GENMASK(10, 5)
+#define TST(n) FIELD_PREP(TST_MASK, (n))
+#define CH_EN(id) BIT(3 + (id))
+#define NB BIT(2)
+#define RFB BIT(1)
+#define PD BIT(0)
+
+/* Power On Reset(POR) value */
+#define CTRL_RESET_VAL (M(0x0) | CCM(0x4) | CA(0x4) | TST(0x25))
+
+/* PHY initialization value and mask */
+#define CTRL_INIT_MASK (M_MASK | CCM_MASK | CA_MASK | TST_MASK | NB | RFB)
+#define CTRL_INIT_VAL (M(0x0) | CCM(0x5) | CA(0x4) | TST(0x25) | RFB)
+
+#define PHY_STATUS 0x10
+#define LOCK BIT(0)
+
+#define PHY_NUM 2
+
+#define MIN_CLKIN_FREQ (25 * MEGA)
+#define MAX_CLKIN_FREQ (165 * MEGA)
+
+#define PLL_LOCK_SLEEP 10
+#define PLL_LOCK_TIMEOUT 1000
+
+struct mixel_lvds_phy {
+ struct phy *phy;
+ struct phy_configure_opts_lvds cfg;
+ unsigned int id;
+};
+
+struct mixel_lvds_phy_priv {
+ struct regmap *regmap;
+ struct mutex lock; /* protect remap access and cfg of our own */
+ struct clk *phy_ref_clk;
+ struct mixel_lvds_phy *phys[PHY_NUM];
+};
+
+static int mixel_lvds_phy_init(struct phy *phy)
+{
+ struct mixel_lvds_phy_priv *priv = dev_get_drvdata(phy->dev.parent);
+
+ mutex_lock(&priv->lock);
+ regmap_update_bits(priv->regmap,
+ PHY_CTRL, CTRL_INIT_MASK, CTRL_INIT_VAL);
+ mutex_unlock(&priv->lock);
+
+ return 0;
+}
+
+static int mixel_lvds_phy_power_on(struct phy *phy)
+{
+ struct mixel_lvds_phy_priv *priv = dev_get_drvdata(phy->dev.parent);
+ struct mixel_lvds_phy *lvds_phy = phy_get_drvdata(phy);
+ struct mixel_lvds_phy *companion = priv->phys[lvds_phy->id ^ 1];
+ struct phy_configure_opts_lvds *cfg = &lvds_phy->cfg;
+ u32 val = 0;
+ u32 locked;
+ int ret;
+
+ /* The master PHY would power on the slave PHY. */
+ if (cfg->is_slave)
+ return 0;
+
+ ret = clk_prepare_enable(priv->phy_ref_clk);
+ if (ret < 0) {
+ dev_err(&phy->dev,
+ "failed to enable PHY reference clock: %d\n", ret);
+ return ret;
+ }
+
+ mutex_lock(&priv->lock);
+ if (cfg->bits_per_lane_and_dclk_cycle == 7) {
+ if (cfg->differential_clk_rate < 44000000)
+ val |= M(0x2);
+ else if (cfg->differential_clk_rate < 90000000)
+ val |= M(0x1);
+ else
+ val |= M(0x0);
+ } else {
+ val = NB;
+
+ if (cfg->differential_clk_rate < 32000000)
+ val |= M(0x2);
+ else if (cfg->differential_clk_rate < 63000000)
+ val |= M(0x1);
+ else
+ val |= M(0x0);
+ }
+ regmap_update_bits(priv->regmap, PHY_CTRL, M_MASK | NB, val);
+
+ /*
+ * Enable two channels synchronously,
+ * if the companion PHY is a slave PHY.
+ */
+ if (companion->cfg.is_slave)
+ val = CH_EN(0) | CH_EN(1);
+ else
+ val = CH_EN(lvds_phy->id);
+ regmap_write(priv->regmap, PHY_CTRL + REG_SET, val);
+
+ ret = regmap_read_poll_timeout(priv->regmap, PHY_STATUS, locked,
+ locked, PLL_LOCK_SLEEP,
+ PLL_LOCK_TIMEOUT);
+ if (ret < 0) {
+ dev_err(&phy->dev, "failed to get PHY lock: %d\n", ret);
+ clk_disable_unprepare(priv->phy_ref_clk);
+ }
+ mutex_unlock(&priv->lock);
+
+ return ret;
+}
+
+static int mixel_lvds_phy_power_off(struct phy *phy)
+{
+ struct mixel_lvds_phy_priv *priv = dev_get_drvdata(phy->dev.parent);
+ struct mixel_lvds_phy *lvds_phy = phy_get_drvdata(phy);
+ struct mixel_lvds_phy *companion = priv->phys[lvds_phy->id ^ 1];
+ struct phy_configure_opts_lvds *cfg = &lvds_phy->cfg;
+
+ /* The master PHY would power off the slave PHY. */
+ if (cfg->is_slave)
+ return 0;
+
+ mutex_lock(&priv->lock);
+ if (companion->cfg.is_slave)
+ regmap_write(priv->regmap, PHY_CTRL + REG_CLR,
+ CH_EN(0) | CH_EN(1));
+ else
+ regmap_write(priv->regmap, PHY_CTRL + REG_CLR,
+ CH_EN(lvds_phy->id));
+ mutex_unlock(&priv->lock);
+
+ clk_disable_unprepare(priv->phy_ref_clk);
+
+ return 0;
+}
+
+static int mixel_lvds_phy_configure(struct phy *phy,
+ union phy_configure_opts *opts)
+{
+ struct mixel_lvds_phy_priv *priv = dev_get_drvdata(phy->dev.parent);
+ struct phy_configure_opts_lvds *cfg = &opts->lvds;
+ int ret;
+
+ ret = clk_set_rate(priv->phy_ref_clk, cfg->differential_clk_rate);
+ if (ret)
+ dev_err(&phy->dev, "failed to set PHY reference clock rate(%lu): %d\n",
+ cfg->differential_clk_rate, ret);
+
+ return ret;
+}
+
+/* Assume the master PHY's configuration set is cached first. */
+static int mixel_lvds_phy_check_slave(struct phy *slave_phy)
+{
+ struct device *dev = &slave_phy->dev;
+ struct mixel_lvds_phy_priv *priv = dev_get_drvdata(dev->parent);
+ struct mixel_lvds_phy *slv = phy_get_drvdata(slave_phy);
+ struct mixel_lvds_phy *mst = priv->phys[slv->id ^ 1];
+ struct phy_configure_opts_lvds *mst_cfg = &mst->cfg;
+ struct phy_configure_opts_lvds *slv_cfg = &slv->cfg;
+
+ if (mst_cfg->bits_per_lane_and_dclk_cycle !=
+ slv_cfg->bits_per_lane_and_dclk_cycle) {
+ dev_err(dev, "number bits mismatch(mst: %u vs slv: %u)\n",
+ mst_cfg->bits_per_lane_and_dclk_cycle,
+ slv_cfg->bits_per_lane_and_dclk_cycle);
+ return -EINVAL;
+ }
+
+ if (mst_cfg->differential_clk_rate !=
+ slv_cfg->differential_clk_rate) {
+ dev_err(dev, "dclk rate mismatch(mst: %lu vs slv: %lu)\n",
+ mst_cfg->differential_clk_rate,
+ slv_cfg->differential_clk_rate);
+ return -EINVAL;
+ }
+
+ if (mst_cfg->lanes != slv_cfg->lanes) {
+ dev_err(dev, "lanes mismatch(mst: %u vs slv: %u)\n",
+ mst_cfg->lanes, slv_cfg->lanes);
+ return -EINVAL;
+ }
+
+ if (mst_cfg->is_slave == slv_cfg->is_slave) {
+ dev_err(dev, "master PHY is not found\n");
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int mixel_lvds_phy_validate(struct phy *phy, enum phy_mode mode,
+ int submode, union phy_configure_opts *opts)
+{
+ struct mixel_lvds_phy_priv *priv = dev_get_drvdata(phy->dev.parent);
+ struct mixel_lvds_phy *lvds_phy = phy_get_drvdata(phy);
+ struct phy_configure_opts_lvds *cfg = &opts->lvds;
+ int ret = 0;
+
+ if (mode != PHY_MODE_LVDS) {
+ dev_err(&phy->dev, "invalid PHY mode(%d)\n", mode);
+ return -EINVAL;
+ }
+
+ if (cfg->bits_per_lane_and_dclk_cycle != 7 &&
+ cfg->bits_per_lane_and_dclk_cycle != 10) {
+ dev_err(&phy->dev, "invalid bits per data lane(%u)\n",
+ cfg->bits_per_lane_and_dclk_cycle);
+ return -EINVAL;
+ }
+
+ if (cfg->lanes != 4 && cfg->lanes != 3) {
+ dev_err(&phy->dev, "invalid data lanes(%u)\n", cfg->lanes);
+ return -EINVAL;
+ }
+
+ if (cfg->differential_clk_rate < MIN_CLKIN_FREQ ||
+ cfg->differential_clk_rate > MAX_CLKIN_FREQ) {
+ dev_err(&phy->dev, "invalid differential clock rate(%lu)\n",
+ cfg->differential_clk_rate);
+ return -EINVAL;
+ }
+
+ mutex_lock(&priv->lock);
+ /* cache configuration set of our own for check */
+ memcpy(&lvds_phy->cfg, cfg, sizeof(*cfg));
+
+ if (cfg->is_slave) {
+ ret = mixel_lvds_phy_check_slave(phy);
+ if (ret)
+ dev_err(&phy->dev, "failed to check slave PHY: %d\n", ret);
+ }
+ mutex_unlock(&priv->lock);
+
+ return ret;
+}
+
+static const struct phy_ops mixel_lvds_phy_ops = {
+ .init = mixel_lvds_phy_init,
+ .power_on = mixel_lvds_phy_power_on,
+ .power_off = mixel_lvds_phy_power_off,
+ .configure = mixel_lvds_phy_configure,
+ .validate = mixel_lvds_phy_validate,
+ .owner = THIS_MODULE,
+};
+
+static int mixel_lvds_phy_reset(struct device *dev)
+{
+ struct mixel_lvds_phy_priv *priv = dev_get_drvdata(dev);
+ int ret;
+
+ ret = pm_runtime_resume_and_get(dev);
+ if (ret < 0) {
+ dev_err(dev, "failed to get PM runtime: %d\n", ret);
+ return ret;
+ }
+
+ regmap_write(priv->regmap, PHY_CTRL, CTRL_RESET_VAL);
+
+ pm_runtime_put_sync(dev);
+
+ return 0;
+}
+
+static struct phy *mixel_lvds_phy_xlate(struct device *dev,
+ const struct of_phandle_args *args)
+{
+ struct mixel_lvds_phy_priv *priv = dev_get_drvdata(dev);
+ unsigned int phy_id;
+
+ if (args->args_count != 1) {
+ dev_err(dev,
+ "invalid argument number(%d) for 'phys' property\n",
+ args->args_count);
+ return ERR_PTR(-EINVAL);
+ }
+
+ phy_id = args->args[0];
+
+ if (phy_id >= PHY_NUM) {
+ dev_err(dev, "invalid PHY index(%d)\n", phy_id);
+ return ERR_PTR(-ENODEV);
+ }
+
+ return priv->phys[phy_id]->phy;
+}
+
+static int mixel_lvds_phy_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct phy_provider *phy_provider;
+ struct mixel_lvds_phy_priv *priv;
+ struct mixel_lvds_phy *lvds_phy;
+ struct phy *phy;
+ int i;
+ int ret;
+
+ if (!dev->of_node)
+ return -ENODEV;
+
+ priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+ if (!priv)
+ return -ENOMEM;
+
+ priv->regmap = syscon_node_to_regmap(dev->of_node->parent);
+ if (IS_ERR(priv->regmap))
+ return dev_err_probe(dev, PTR_ERR(priv->regmap),
+ "failed to get regmap\n");
+
+ priv->phy_ref_clk = devm_clk_get(dev, NULL);
+ if (IS_ERR(priv->phy_ref_clk))
+ return dev_err_probe(dev, PTR_ERR(priv->phy_ref_clk),
+ "failed to get PHY reference clock\n");
+
+ mutex_init(&priv->lock);
+
+ dev_set_drvdata(dev, priv);
+
+ ret = devm_pm_runtime_enable(dev);
+ if (ret)
+ return ret;
+
+ ret = mixel_lvds_phy_reset(dev);
+ if (ret) {
+ dev_err(dev, "failed to do POR reset: %d\n", ret);
+ return ret;
+ }
+
+ for (i = 0; i < PHY_NUM; i++) {
+ lvds_phy = devm_kzalloc(dev, sizeof(*lvds_phy), GFP_KERNEL);
+ if (!lvds_phy)
+ return -ENOMEM;
+
+ phy = devm_phy_create(dev, NULL, &mixel_lvds_phy_ops);
+ if (IS_ERR(phy)) {
+ ret = PTR_ERR(phy);
+ dev_err(dev, "failed to create PHY for channel%d: %d\n",
+ i, ret);
+ return ret;
+ }
+
+ lvds_phy->phy = phy;
+ lvds_phy->id = i;
+ priv->phys[i] = lvds_phy;
+
+ phy_set_drvdata(phy, lvds_phy);
+ }
+
+ phy_provider = devm_of_phy_provider_register(dev, mixel_lvds_phy_xlate);
+ if (IS_ERR(phy_provider)) {
+ ret = PTR_ERR(phy_provider);
+ dev_err(dev, "failed to register PHY provider: %d\n", ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+static int __maybe_unused mixel_lvds_phy_runtime_suspend(struct device *dev)
+{
+ struct mixel_lvds_phy_priv *priv = dev_get_drvdata(dev);
+
+ /* power down */
+ mutex_lock(&priv->lock);
+ regmap_write(priv->regmap, PHY_CTRL + REG_SET, PD);
+ mutex_unlock(&priv->lock);
+
+ return 0;
+}
+
+static int __maybe_unused mixel_lvds_phy_runtime_resume(struct device *dev)
+{
+ struct mixel_lvds_phy_priv *priv = dev_get_drvdata(dev);
+
+ /* power up + control initialization */
+ mutex_lock(&priv->lock);
+ regmap_update_bits(priv->regmap, PHY_CTRL,
+ CTRL_INIT_MASK | PD, CTRL_INIT_VAL);
+ mutex_unlock(&priv->lock);
+
+ return 0;
+}
+
+static const struct dev_pm_ops mixel_lvds_phy_pm_ops = {
+ SET_RUNTIME_PM_OPS(mixel_lvds_phy_runtime_suspend,
+ mixel_lvds_phy_runtime_resume, NULL)
+};
+
+static const struct of_device_id mixel_lvds_phy_of_match[] = {
+ { .compatible = "fsl,imx8qm-lvds-phy" },
+ { /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, mixel_lvds_phy_of_match);
+
+static struct platform_driver mixel_lvds_phy_driver = {
+ .probe = mixel_lvds_phy_probe,
+ .driver = {
+ .pm = &mixel_lvds_phy_pm_ops,
+ .name = "mixel-lvds-phy",
+ .of_match_table = mixel_lvds_phy_of_match,
+ }
+};
+module_platform_driver(mixel_lvds_phy_driver);
+
+MODULE_DESCRIPTION("Mixel LVDS PHY driver");
+MODULE_AUTHOR("Liu Ying <victor.liu@nxp.com>");
+MODULE_LICENSE("GPL");
diff --git a/drivers/phy/freescale/phy-fsl-lynx-10g.c b/drivers/phy/freescale/phy-fsl-lynx-10g.c
new file mode 100644
index 000000000..54717465f
--- /dev/null
+++ b/drivers/phy/freescale/phy-fsl-lynx-10g.c
@@ -0,0 +1,1326 @@
+// SPDX-License-Identifier: GPL-2.0+
+/* Copyright 2021-2026 NXP */
+
+#include <linux/delay.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/phy.h>
+#include <linux/phy/phy.h>
+#include <linux/platform_device.h>
+#include <linux/workqueue.h>
+#include <linux/fsl/guts.h>
+
+#include "phy-fsl-lynx-core.h"
+
+/* SoC IP wrapper for protocol converters */
+#define PCCR8 0x220
+#define PCCR8_SGMIIa_KX BIT(3)
+#define PCCR8_SGMIIa_CFG GENMASK(2, 0)
+
+#define PCCR9 0x224
+#define PCCR9_QSGMIIa_CFG GENMASK(2, 0)
+#define PCCR9_QXGMIIa_CFG GENMASK(2, 0)
+
+#define PCCRB 0x22c
+#define PCCRB_XFIa_CFG GENMASK(2, 0)
+#define PCCRB_SXGMIIa_CFG GENMASK(2, 0)
+
+#define SGMII_CFG(id) (28 - (id) * 4)
+#define QSGMII_CFG(id) (28 - (id) * 4)
+#define SXGMII_CFG(id) (28 - (id) * 4)
+#define QXGMII_CFG(id) (12 - (id) * 4)
+#define XFI_CFG(id) (28 - (id) * 4)
+
+#define CR(x) ((x) * 4)
+
+#define A 0
+#define B 1
+#define C 2
+#define D 3
+#define E 4
+#define F 5
+#define G 6
+#define H 7
+
+#define SGMIIaCR0(id) (0x1800 + (id) * 0x10)
+#define QSGMIIaCR0(id) (0x1880 + (id) * 0x10)
+#define XAUIaCR0(id) (0x1900 + (id) * 0x10)
+#define XFIaCR0(id) (0x1980 + (id) * 0x10)
+#define SXGMIIaCR0(id) (0x1a80 + (id) * 0x10)
+#define QXGMIIaCR0(id) (0x1b00 + (id) * 0x20)
+
+#define SGMIIaCR0_RST_SGM BIT(31)
+#define SGMIIaCR0_RST_SGM_OFF SGMIIaCR0_RST_SGM
+#define SGMIIaCR0_RST_SGM_ON 0
+#define SGMIIaCR0_PD_SGM BIT(30)
+#define SGMIIaCR1_SGPCS_EN BIT(11)
+#define SGMIIaCR1_SGPCS_DIS 0x0
+
+#define QSGMIIaCR0_RST_QSGM BIT(31)
+#define QSGMIIaCR0_RST_QSGM_OFF QSGMIIaCR0_RST_QSGM
+#define QSGMIIaCR0_RST_QSGM_ON 0
+#define QSGMIIaCR0_PD_QSGM BIT(30)
+
+/* Per PLL registers */
+#define PLLnCR0(pll) ((pll) * 0x20 + 0x4)
+
+#define PLLnCR0_POFF BIT(31)
+
+#define PLLnCR0_REFCLK_SEL GENMASK(30, 28)
+#define PLLnCR0_REFCLK_SEL_100MHZ 0x0
+#define PLLnCR0_REFCLK_SEL_125MHZ 0x1
+#define PLLnCR0_REFCLK_SEL_156MHZ 0x2
+#define PLLnCR0_REFCLK_SEL_150MHZ 0x3
+#define PLLnCR0_REFCLK_SEL_161MHZ 0x4
+#define PLLnCR0_PLL_LCK BIT(23)
+#define PLLnCR0_FRATE_SEL GENMASK(19, 16)
+#define PLLnCR0_FRATE_5G 0x0
+#define PLLnCR0_FRATE_5_15625G 0x6
+#define PLLnCR0_FRATE_4G 0x7
+#define PLLnCR0_FRATE_3_125G 0x9
+#define PLLnCR0_FRATE_3G 0xa
+
+/* Per SerDes lane registers */
+
+/* Lane a Protocol Select status register */
+#define LNaPSSR0(lane) (0x100 + (lane) * 0x20)
+#define LNaPSSR0_TYPE GENMASK(30, 26)
+#define LNaPSSR0_IS_QUAD GENMASK(25, 24)
+#define LNaPSSR0_MAC GENMASK(19, 16)
+#define LNaPSSR0_PCS GENMASK(10, 8)
+#define LNaPSSR0_LANE GENMASK(2, 0)
+
+/* Lane a General Control Register */
+#define LNaGCR0(lane) (0x800 + (lane) * 0x40 + 0x0)
+#define LNaGCR0_RPLL_PLLF BIT(31)
+#define LNaGCR0_RPLL_PLLS 0x0
+#define LNaGCR0_RPLL_MSK BIT(31)
+#define LNaGCR0_RRAT_SEL GENMASK(29, 28)
+#define LNaGCR0_TRAT_SEL GENMASK(25, 24)
+#define LNaGCR0_TPLL_PLLF BIT(27)
+#define LNaGCR0_TPLL_PLLS 0x0
+#define LNaGCR0_TPLL_MSK BIT(27)
+#define LNaGCR0_RRST_OFF LNaGCR0_RRST
+#define LNaGCR0_TRST_OFF LNaGCR0_TRST
+#define LNaGCR0_RRST_ON 0x0
+#define LNaGCR0_TRST_ON 0x0
+#define LNaGCR0_RRST BIT(22)
+#define LNaGCR0_TRST BIT(21)
+#define LNaGCR0_RX_PD BIT(20)
+#define LNaGCR0_TX_PD BIT(19)
+#define LNaGCR0_IF20BIT_EN BIT(18)
+#define LNaGCR0_PROTS GENMASK(11, 7)
+
+#define LNaGCR1(lane) (0x800 + (lane) * 0x40 + 0x4)
+#define LNaGCR1_RDAT_INV BIT(31)
+#define LNaGCR1_TDAT_INV BIT(30)
+#define LNaGCR1_OPAD_CTL BIT(26)
+#define LNaGCR1_REIDL_TH GENMASK(22, 20)
+#define LNaGCR1_REIDL_EX_SEL GENMASK(19, 18)
+#define LNaGCR1_REIDL_ET_SEL GENMASK(17, 16)
+#define LNaGCR1_REIDL_EX_MSB BIT(15)
+#define LNaGCR1_REIDL_ET_MSB BIT(14)
+#define LNaGCR1_REQ_CTL_SNP BIT(13)
+#define LNaGCR1_REQ_CDR_SNP BIT(12)
+#define LNaGCR1_TRSTDIR BIT(7)
+#define LNaGCR1_REQ_BIN_SNP BIT(6)
+#define LNaGCR1_ISLEW_RCTL GENMASK(5, 4)
+#define LNaGCR1_OSLEW_RCTL GENMASK(1, 0)
+
+#define LNaRECR0(lane) (0x800 + (lane) * 0x40 + 0x10)
+#define LNaRECR0_RXEQ_BST BIT(28)
+#define LNaRECR0_GK2OVD GENMASK(27, 24)
+#define LNaRECR0_GK3OVD GENMASK(19, 16)
+#define LNaRECR0_GK2OVD_EN BIT(15)
+#define LNaRECR0_GK3OVD_EN BIT(14)
+#define LNaRECR0_OSETOVD_EN BIT(13)
+#define LNaRECR0_BASE_WAND GENMASK(11, 10)
+#define LNaRECR0_OSETOVD GENMASK(6, 0)
+
+#define LNaTECR0(lane) (0x800 + (lane) * 0x40 + 0x18)
+#define LNaTECR0_TEQ_TYPE GENMASK(29, 28)
+#define LNaTECR0_SGN_PREQ BIT(26)
+#define LNaTECR0_RATIO_PREQ GENMASK(25, 22)
+#define LNaTECR0_SGN_POST1Q BIT(21)
+#define LNaTECR0_RATIO_PST1Q GENMASK(20, 16)
+#define LNaTECR0_ADPT_EQ GENMASK(13, 8)
+#define LNaTECR0_AMP_RED GENMASK(5, 0)
+
+#define LNaTTLCR0(lane) (0x800 + (lane) * 0x40 + 0x20)
+#define LNaTTLCR1(lane) (0x800 + (lane) * 0x40 + 0x24)
+#define LNaTTLCR2(lane) (0x800 + (lane) * 0x40 + 0x28)
+
+#define LNaTCSR3(lane) (0x800 + (lane) * 0x40 + 0x3C)
+#define LNaTCSR3_CDR_LCK BIT(27)
+
+enum lynx_10g_rat_sel {
+ RAT_SEL_FULL = 0x0,
+ RAT_SEL_HALF = 0x1,
+ RAT_SEL_QUARTER = 0x2,
+ RAT_SEL_DOUBLE = 0x3,
+};
+
+enum lynx_10g_eq_type {
+ EQ_TYPE_NO_EQ = 0,
+ EQ_TYPE_2TAP = 1,
+ EQ_TYPE_3TAP = 2,
+};
+
+enum lynx_10g_proto_sel {
+ PROTO_SEL_PCIE = 0,
+ PROTO_SEL_SGMII_BASEX_KX_QSGMII = 1,
+ PROTO_SEL_SATA = 2,
+ PROTO_SEL_XAUI = 4,
+ PROTO_SEL_XFI_10GBASER_KR_SXGMII = 0xa,
+};
+
+struct lynx_10g_proto_conf {
+ int proto_sel;
+ int if20bit_en;
+ int reidl_th;
+ int reidl_et_msb;
+ int reidl_et_sel;
+ int reidl_ex_msb;
+ int reidl_ex_sel;
+ int islew_rctl;
+ int oslew_rctl;
+ int rxeq_bst;
+ int gk2ovd;
+ int gk3ovd;
+ int gk2ovd_en;
+ int gk3ovd_en;
+ int base_wand;
+ int teq_type;
+ int sgn_preq;
+ int ratio_preq;
+ int sgn_post1q;
+ int ratio_post1q;
+ int adpt_eq;
+ int amp_red;
+ int ttlcr0;
+};
+
+static const struct lynx_10g_proto_conf lynx_10g_proto_conf[LANE_MODE_MAX] = {
+ [LANE_MODE_1000BASEX_SGMII] = {
+ .proto_sel = PROTO_SEL_SGMII_BASEX_KX_QSGMII,
+ .reidl_th = 1,
+ .reidl_ex_sel = 3,
+ .reidl_et_msb = 1,
+ .islew_rctl = 1,
+ .oslew_rctl = 1,
+ .gk2ovd = 15,
+ .gk3ovd = 15,
+ .gk2ovd_en = 1,
+ .gk3ovd_en = 1,
+ .teq_type = EQ_TYPE_NO_EQ,
+ .adpt_eq = 48,
+ .amp_red = 6,
+ .ttlcr0 = 0x39000400,
+ },
+ [LANE_MODE_2500BASEX] = {
+ .proto_sel = PROTO_SEL_SGMII_BASEX_KX_QSGMII,
+ .islew_rctl = 2,
+ .oslew_rctl = 2,
+ .teq_type = EQ_TYPE_2TAP,
+ .sgn_post1q = 1,
+ .ratio_post1q = 6,
+ .adpt_eq = 48,
+ .ttlcr0 = 0x00000400,
+ },
+ [LANE_MODE_QSGMII] = {
+ .proto_sel = PROTO_SEL_SGMII_BASEX_KX_QSGMII,
+ .islew_rctl = 1,
+ .oslew_rctl = 1,
+ .teq_type = EQ_TYPE_2TAP,
+ .sgn_post1q = 1,
+ .ratio_post1q = 6,
+ .adpt_eq = 48,
+ .amp_red = 2,
+ .ttlcr0 = 0x00000400,
+ },
+ [LANE_MODE_10G_QXGMII] = {
+ .proto_sel = PROTO_SEL_XFI_10GBASER_KR_SXGMII,
+ .if20bit_en = 1,
+ .islew_rctl = 1,
+ .oslew_rctl = 1,
+ .base_wand = 1,
+ .teq_type = EQ_TYPE_NO_EQ,
+ .adpt_eq = 48,
+ .ttlcr0 = 0x00000400,
+ },
+ [LANE_MODE_USXGMII] = {
+ .proto_sel = PROTO_SEL_XFI_10GBASER_KR_SXGMII,
+ .if20bit_en = 1,
+ .islew_rctl = 1,
+ .oslew_rctl = 1,
+ .base_wand = 1,
+ .teq_type = EQ_TYPE_NO_EQ,
+ .sgn_post1q = 1,
+ .adpt_eq = 48,
+ .ttlcr0 = 0x00000400,
+ },
+ [LANE_MODE_10GBASER] = {
+ .proto_sel = PROTO_SEL_XFI_10GBASER_KR_SXGMII,
+ .if20bit_en = 1,
+ .islew_rctl = 2,
+ .oslew_rctl = 2,
+ .rxeq_bst = 1,
+ .base_wand = 1,
+ .teq_type = EQ_TYPE_2TAP,
+ .sgn_post1q = 1,
+ .ratio_post1q = 3,
+ .adpt_eq = 48,
+ .amp_red = 7,
+ .ttlcr0 = 0x00000400,
+ },
+};
+
+static void lynx_10g_cdr_lock_check(struct lynx_lane *lane)
+{
+ u32 tcsr3 = lynx_lane_read(lane, LNaTCSR3);
+
+ if (tcsr3 & LNaTCSR3_CDR_LCK)
+ return;
+
+ dev_dbg(&lane->phy->dev,
+ "Lane %c CDR unlocked, resetting receiver...\n",
+ 'A' + lane->id);
+
+ lynx_lane_rmw(lane, LNaGCR0, LNaGCR0_RRST_ON, LNaGCR0_RRST);
+ usleep_range(1, 2);
+ lynx_lane_rmw(lane, LNaGCR0, LNaGCR0_RRST_OFF, LNaGCR0_RRST);
+
+ usleep_range(1, 2);
+}
+
+static void lynx_10g_pll_read_configuration(struct lynx_pll *pll)
+{
+ u32 val;
+
+ val = lynx_pll_read(pll, PLLnCR0);
+ pll->frate_sel = FIELD_GET(PLLnCR0_FRATE_SEL, val);
+ pll->refclk_sel = FIELD_GET(PLLnCR0_REFCLK_SEL, val);
+ pll->enabled = !(val & PLLnCR0_POFF);
+ pll->locked = !!(val & PLLnCR0_PLL_LCK);
+
+ if (!pll->enabled)
+ return;
+
+ switch (pll->frate_sel) {
+ case PLLnCR0_FRATE_5G:
+ /* 5GHz clock net */
+ __set_bit(LANE_MODE_1000BASEX_SGMII, pll->supported);
+ __set_bit(LANE_MODE_QSGMII, pll->supported);
+ break;
+ case PLLnCR0_FRATE_3_125G:
+ __set_bit(LANE_MODE_2500BASEX, pll->supported);
+ break;
+ case PLLnCR0_FRATE_5_15625G:
+ /* 10.3125GHz clock net */
+ __set_bit(LANE_MODE_10GBASER, pll->supported);
+ __set_bit(LANE_MODE_USXGMII, pll->supported);
+ __set_bit(LANE_MODE_10G_QXGMII, pll->supported);
+ break;
+ default:
+ break;
+ }
+}
+
+/* On LS1028A, SGMIIA_CFG, SGMIIB_CFG, and SGMIIC_CFG from PCCR8 have the
+ * ability to map either an ENETC PCS (PCCR8_SGMIIa_CFG=2) or a Felix switch
+ * PCS (PCCR8_SGMIIa_CFG=1) to the same lane.
+ *
+ * On LS1088A, the same QSGMII PCS B can be connected to SerDes lane 1
+ * (PCCR9_QSGMIIa_CFG=1) or to lane 3 (PCCR9_QSGMIIa_CFG=2).
+ *
+ * The PHY API lacks the capability to distinguish anything about the consumer,
+ * so we don't support changing the initial muxing done by the RCW.
+ *
+ * However, after disabling a PCS through PCCR8, we need to properly restore
+ * the original value to keep the same muxing, and for that we need to back
+ * it up (here).
+ */
+static void lynx_10g_backup_pccr_val(struct lynx_lane *lane)
+{
+ u32 val;
+ int err;
+
+ if (lane->mode == LANE_MODE_UNKNOWN)
+ return;
+
+ err = lynx_pccr_read(lane, lane->mode, &val);
+ if (err) {
+ dev_warn(&lane->phy->dev,
+ "The driver doesn't know how to access the PCCR for lane mode %s\n",
+ lynx_lane_mode_str(lane->mode));
+ lane->mode = LANE_MODE_UNKNOWN;
+ return;
+ }
+
+ lane->default_pccr[lane->mode] = val;
+
+ /* 1000Base-X, 1000Base-KX, 2500Base-KX and SGMII use the same PCCR8.
+ * Only the KX bit differs (set for 1000Base-KX). Since we back up PCCR
+ * values per lane mode, make sure to not back up the PCCR8 value with
+ * the KX bit set for the non-KX modes, if the lane was in KX mode at
+ * boot time. Just preserve bits 2:0, which tell whether the (and
+ * which) 1G PCS was enabled.
+ */
+ switch (lane->mode) {
+ case LANE_MODE_1000BASEX_SGMII:
+ case LANE_MODE_2500BASEX:
+ lane->default_pccr[LANE_MODE_1000BASEX_SGMII] = val & ~PCCR8_SGMIIa_KX;
+ lane->default_pccr[LANE_MODE_2500BASEX] = val & ~PCCR8_SGMIIa_KX;
+ break;
+ default:
+ break;
+ }
+}
+
+/* Is the PCS enabled, according to the value backed up from the PCCR register
+ * for this lane mode?
+ *
+ * Normally we'd need to ask "what lane mode are we talking about?", but the
+ * answer is invariably the same regardless - PCCR8_SGMIIa_CFG has the same
+ * layout as PCCR9_QSGMIIa_CFG, PCCRB_XFIa_CFG etc etc, and the value 0
+ * universally means "PCS disabled". So this is just a shorthand answer.
+ */
+static bool lynx_10g_pccr_val_enabled(u32 pccr)
+{
+ return FIELD_GET(PCCR8_SGMIIa_CFG, pccr) != 0;
+}
+
+static bool lynx_10g_lane_is_3_125g(struct lynx_lane *lane)
+{
+ struct lynx_priv *priv = lane->priv;
+ struct lynx_pll *pll;
+ u32 gcr0;
+
+ gcr0 = lynx_lane_read(lane, LNaGCR0);
+
+ if (gcr0 & LNaGCR0_TPLL_PLLF)
+ pll = &priv->pll[0];
+ else
+ pll = &priv->pll[1];
+
+ if (pll->frate_sel != PLLnCR0_FRATE_3_125G)
+ return false;
+
+ if (FIELD_GET(LNaGCR0_TRAT_SEL, gcr0) != RAT_SEL_FULL ||
+ FIELD_GET(LNaGCR0_RRAT_SEL, gcr0) != RAT_SEL_FULL)
+ return false;
+
+ return true;
+}
+
+static void lynx_10g_lane_read_configuration(struct lynx_lane *lane)
+{
+ u32 pssr0 = lynx_lane_read(lane, LNaPSSR0);
+ struct lynx_priv *priv = lane->priv;
+ int proto;
+
+ proto = FIELD_GET(LNaPSSR0_TYPE, pssr0);
+ switch (proto) {
+ case PROTO_SEL_SGMII_BASEX_KX_QSGMII:
+ if (lynx_10g_lane_is_3_125g(lane))
+ lane->mode = LANE_MODE_2500BASEX;
+ else if (FIELD_GET(LNaPSSR0_IS_QUAD, pssr0))
+ lane->mode = LANE_MODE_QSGMII;
+ else
+ lane->mode = LANE_MODE_1000BASEX_SGMII;
+ break;
+ case PROTO_SEL_XFI_10GBASER_KR_SXGMII:
+ if (FIELD_GET(LNaPSSR0_IS_QUAD, pssr0))
+ lane->mode = LANE_MODE_10G_QXGMII;
+ else if (priv->info->quirks & LYNX_QUIRK_HAS_HARDCODED_USXGMII)
+ lane->mode = LANE_MODE_USXGMII;
+ else
+ lane->mode = LANE_MODE_10GBASER;
+ break;
+ case PROTO_SEL_PCIE:
+ case PROTO_SEL_SATA:
+ case PROTO_SEL_XAUI:
+ break;
+ default:
+ dev_warn(&lane->phy->dev, "Unknown lane protocol 0x%x\n",
+ proto);
+ }
+
+ lynx_10g_backup_pccr_val(lane);
+}
+
+static int ls1028a_get_pccr(enum lynx_lane_mode lane_mode, int lane,
+ struct lynx_pccr *pccr)
+{
+ switch (lane_mode) {
+ case LANE_MODE_1000BASEX_SGMII:
+ case LANE_MODE_2500BASEX:
+ pccr->offset = PCCR8;
+ pccr->width = 4;
+ pccr->shift = SGMII_CFG(lane);
+ break;
+ case LANE_MODE_QSGMII:
+ if (lane != 1)
+ return -EINVAL;
+
+ pccr->offset = PCCR9;
+ pccr->width = 3;
+ pccr->shift = QSGMII_CFG(A);
+ break;
+ case LANE_MODE_10G_QXGMII:
+ if (lane != 1)
+ return -EINVAL;
+
+ pccr->offset = PCCR9;
+ pccr->width = 3;
+ pccr->shift = QXGMII_CFG(A);
+ break;
+ case LANE_MODE_USXGMII:
+ if (lane != 0)
+ return -EINVAL;
+
+ pccr->offset = PCCRB;
+ pccr->width = 3;
+ pccr->shift = SXGMII_CFG(A);
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int ls1028a_get_pcvt_offset(int lane, enum lynx_lane_mode mode)
+{
+ switch (mode) {
+ case LANE_MODE_1000BASEX_SGMII:
+ case LANE_MODE_2500BASEX:
+ return SGMIIaCR0(lane);
+ case LANE_MODE_QSGMII:
+ return lane == 1 ? QSGMIIaCR0(A) : -EINVAL;
+ case LANE_MODE_USXGMII:
+ return lane == 0 ? SXGMIIaCR0(A) : -EINVAL;
+ case LANE_MODE_10G_QXGMII:
+ return lane == 1 ? QXGMIIaCR0(A) : -EINVAL;
+ default:
+ return -EINVAL;
+ }
+}
+
+static const struct lynx_info lynx_info_ls1028a = {
+ .get_pccr = ls1028a_get_pccr,
+ .get_pcvt_offset = ls1028a_get_pcvt_offset,
+ .pll_read_configuration = lynx_10g_pll_read_configuration,
+ .lane_read_configuration = lynx_10g_lane_read_configuration,
+ .cdr_lock_check = lynx_10g_cdr_lock_check,
+ .num_lanes = 4,
+ .index = 1,
+ .quirks = LYNX_QUIRK_HAS_HARDCODED_USXGMII,
+};
+
+static int ls1046a_serdes1_get_pccr(enum lynx_lane_mode lane_mode, int lane,
+ struct lynx_pccr *pccr)
+{
+ switch (lane_mode) {
+ case LANE_MODE_1000BASEX_SGMII:
+ case LANE_MODE_2500BASEX:
+ pccr->offset = PCCR8;
+ pccr->width = 4;
+ pccr->shift = SGMII_CFG(lane);
+ break;
+ case LANE_MODE_QSGMII:
+ if (lane != 1)
+ return -EINVAL;
+
+ pccr->offset = PCCR9;
+ pccr->width = 3;
+ pccr->shift = QSGMII_CFG(B);
+ break;
+ case LANE_MODE_10GBASER:
+ switch (lane) {
+ case 2:
+ pccr->shift = XFI_CFG(A);
+ break;
+ case 3:
+ pccr->shift = XFI_CFG(B);
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ pccr->offset = PCCRB;
+ pccr->width = 3;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int ls1046a_serdes1_get_pcvt_offset(int lane, enum lynx_lane_mode mode)
+{
+ switch (mode) {
+ case LANE_MODE_1000BASEX_SGMII:
+ case LANE_MODE_2500BASEX:
+ return SGMIIaCR0(lane);
+ case LANE_MODE_QSGMII:
+ if (lane != 1)
+ return -EINVAL;
+
+ return QSGMIIaCR0(B);
+ case LANE_MODE_10GBASER:
+ switch (lane) {
+ case 2:
+ return XFIaCR0(A);
+ case 3:
+ return XFIaCR0(B);
+ default:
+ return -EINVAL;
+ }
+ default:
+ return -EINVAL;
+ }
+}
+
+static const struct lynx_info lynx_info_ls1046a_serdes1 = {
+ .get_pccr = ls1046a_serdes1_get_pccr,
+ .get_pcvt_offset = ls1046a_serdes1_get_pcvt_offset,
+ .pll_read_configuration = lynx_10g_pll_read_configuration,
+ .lane_read_configuration = lynx_10g_lane_read_configuration,
+ .cdr_lock_check = lynx_10g_cdr_lock_check,
+ .num_lanes = 4,
+ .index = 1,
+};
+
+static int ls1046a_serdes2_get_pccr(enum lynx_lane_mode lane_mode, int lane,
+ struct lynx_pccr *pccr)
+{
+ switch (lane_mode) {
+ case LANE_MODE_1000BASEX_SGMII:
+ case LANE_MODE_2500BASEX:
+ if (lane != 1)
+ return -EINVAL;
+
+ pccr->offset = PCCR8;
+ pccr->width = 4;
+ pccr->shift = SGMII_CFG(B);
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int ls1046a_serdes2_get_pcvt_offset(int lane, enum lynx_lane_mode mode)
+{
+ switch (mode) {
+ case LANE_MODE_1000BASEX_SGMII:
+ case LANE_MODE_2500BASEX:
+ if (lane != 1)
+ return -EINVAL;
+
+ return SGMIIaCR0(B);
+ default:
+ return -EINVAL;
+ }
+}
+
+static const struct lynx_info lynx_info_ls1046a_serdes2 = {
+ .get_pccr = ls1046a_serdes2_get_pccr,
+ .get_pcvt_offset = ls1046a_serdes2_get_pcvt_offset,
+ .pll_read_configuration = lynx_10g_pll_read_configuration,
+ .lane_read_configuration = lynx_10g_lane_read_configuration,
+ .cdr_lock_check = lynx_10g_cdr_lock_check,
+ .num_lanes = 4,
+ .index = 2,
+};
+
+static int ls1088a_serdes1_get_pccr(enum lynx_lane_mode lane_mode, int lane,
+ struct lynx_pccr *pccr)
+{
+ switch (lane_mode) {
+ case LANE_MODE_1000BASEX_SGMII:
+ pccr->offset = PCCR8;
+ pccr->width = 4;
+ pccr->shift = SGMII_CFG(lane);
+ break;
+ case LANE_MODE_QSGMII:
+ switch (lane) {
+ case 0:
+ pccr->shift = QSGMII_CFG(A);
+ break;
+ case 1:
+ case 3:
+ pccr->shift = QSGMII_CFG(B);
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ pccr->offset = PCCR9;
+ pccr->width = 3;
+ break;
+ case LANE_MODE_10GBASER:
+ switch (lane) {
+ case 2:
+ pccr->shift = XFI_CFG(A);
+ break;
+ case 3:
+ pccr->shift = XFI_CFG(B);
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ pccr->offset = PCCRB;
+ pccr->width = 3;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int ls1088a_serdes1_get_pcvt_offset(int lane, enum lynx_lane_mode mode)
+{
+ switch (mode) {
+ case LANE_MODE_1000BASEX_SGMII:
+ return SGMIIaCR0(lane);
+ case LANE_MODE_QSGMII:
+ switch (lane) {
+ case 0:
+ return QSGMIIaCR0(A);
+ case 1:
+ case 3:
+ return QSGMIIaCR0(B);
+ default:
+ return -EINVAL;
+ }
+ case LANE_MODE_10GBASER:
+ switch (lane) {
+ case 2:
+ return XFIaCR0(A);
+ case 3:
+ return XFIaCR0(B);
+ default:
+ return -EINVAL;
+ }
+ default:
+ return -EINVAL;
+ }
+}
+
+static const struct lynx_info lynx_info_ls1088a_serdes1 = {
+ .get_pccr = ls1088a_serdes1_get_pccr,
+ .get_pcvt_offset = ls1088a_serdes1_get_pcvt_offset,
+ .pll_read_configuration = lynx_10g_pll_read_configuration,
+ .lane_read_configuration = lynx_10g_lane_read_configuration,
+ .cdr_lock_check = lynx_10g_cdr_lock_check,
+ .num_lanes = 4,
+ .index = 1,
+};
+
+static int ls2088a_serdes1_get_pccr(enum lynx_lane_mode lane_mode, int lane,
+ struct lynx_pccr *pccr)
+{
+ switch (lane_mode) {
+ case LANE_MODE_1000BASEX_SGMII:
+ case LANE_MODE_2500BASEX:
+ pccr->offset = PCCR8;
+ pccr->width = 4;
+ pccr->shift = SGMII_CFG(lane);
+ break;
+ case LANE_MODE_QSGMII:
+ switch (lane) {
+ case 2:
+ case 6:
+ pccr->shift = QSGMII_CFG(A);
+ break;
+ case 7:
+ pccr->shift = QSGMII_CFG(B);
+ break;
+ case 0:
+ case 4:
+ pccr->shift = QSGMII_CFG(C);
+ break;
+ case 1:
+ case 5:
+ pccr->shift = QSGMII_CFG(D);
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ pccr->offset = PCCR9;
+ pccr->width = 3;
+ break;
+ case LANE_MODE_10GBASER:
+ pccr->offset = PCCRB;
+ pccr->width = 3;
+ pccr->shift = XFI_CFG(lane);
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int ls2088a_serdes1_get_pcvt_offset(int lane, enum lynx_lane_mode mode)
+{
+ switch (mode) {
+ case LANE_MODE_1000BASEX_SGMII:
+ case LANE_MODE_2500BASEX:
+ return SGMIIaCR0(lane);
+ case LANE_MODE_QSGMII:
+ switch (lane) {
+ case 2:
+ case 6:
+ return QSGMIIaCR0(A);
+ case 7:
+ return QSGMIIaCR0(B);
+ case 0:
+ case 4:
+ return QSGMIIaCR0(C);
+ case 1:
+ case 5:
+ return QSGMIIaCR0(D);
+ default:
+ return -EINVAL;
+ }
+ case LANE_MODE_10GBASER:
+ return XFIaCR0(lane);
+ default:
+ return -EINVAL;
+ }
+}
+
+static const struct lynx_info lynx_info_ls2088a_serdes1 = {
+ .get_pccr = ls2088a_serdes1_get_pccr,
+ .get_pcvt_offset = ls2088a_serdes1_get_pcvt_offset,
+ .pll_read_configuration = lynx_10g_pll_read_configuration,
+ .lane_read_configuration = lynx_10g_lane_read_configuration,
+ .cdr_lock_check = lynx_10g_cdr_lock_check,
+ .num_lanes = 8,
+ .index = 1,
+};
+
+static int ls2088a_serdes2_get_pccr(enum lynx_lane_mode lane_mode, int lane,
+ struct lynx_pccr *pccr)
+{
+ switch (lane_mode) {
+ case LANE_MODE_1000BASEX_SGMII:
+ case LANE_MODE_2500BASEX:
+ pccr->offset = PCCR8;
+ pccr->width = 4;
+ pccr->shift = SGMII_CFG(lane);
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int ls2088a_serdes2_get_pcvt_offset(int lane, enum lynx_lane_mode mode)
+{
+ switch (mode) {
+ case LANE_MODE_1000BASEX_SGMII:
+ case LANE_MODE_2500BASEX:
+ return SGMIIaCR0(lane);
+ default:
+ return -EINVAL;
+ }
+}
+
+static const struct lynx_info lynx_info_ls2088a_serdes2 = {
+ .get_pccr = ls2088a_serdes2_get_pccr,
+ .get_pcvt_offset = ls2088a_serdes2_get_pcvt_offset,
+ .pll_read_configuration = lynx_10g_pll_read_configuration,
+ .lane_read_configuration = lynx_10g_lane_read_configuration,
+ .cdr_lock_check = lynx_10g_cdr_lock_check,
+ .num_lanes = 8,
+ .index = 2,
+};
+
+/* Halting puts the lane in a mode in which it can be reconfigured */
+static void lynx_10g_lane_halt(struct phy *phy)
+{
+ struct lynx_lane *lane = phy_get_drvdata(phy);
+
+ /* Issue a reset request */
+ lynx_lane_rmw(lane, LNaGCR0,
+ LNaGCR0_RRST_ON | LNaGCR0_TRST_ON,
+ LNaGCR0_RRST | LNaGCR0_TRST);
+
+ /* The RM says to wait for at least 50ns */
+ usleep_range(1, 2);
+}
+
+static void lynx_10g_lane_reset(struct phy *phy)
+{
+ struct lynx_lane *lane = phy_get_drvdata(phy);
+
+ /* Finalize the reset request */
+ lynx_lane_rmw(lane, LNaGCR0,
+ LNaGCR0_RRST_OFF | LNaGCR0_TRST_OFF,
+ LNaGCR0_RRST | LNaGCR0_TRST);
+}
+
+static int lynx_10g_power_off(struct phy *phy)
+{
+ struct lynx_lane *lane = phy_get_drvdata(phy);
+
+ if (!lane->powered_up)
+ return 0;
+
+ /* Issue a reset request with the power down bits set */
+ lynx_lane_rmw(lane, LNaGCR0,
+ LNaGCR0_RRST_ON | LNaGCR0_TRST_ON |
+ LNaGCR0_RX_PD | LNaGCR0_TX_PD,
+ LNaGCR0_RRST | LNaGCR0_TRST |
+ LNaGCR0_RX_PD | LNaGCR0_TX_PD);
+
+ /* The RM says to wait for at least 50ns */
+ usleep_range(1, 2);
+
+ lane->powered_up = false;
+
+ return 0;
+}
+
+static int lynx_10g_power_on(struct phy *phy)
+{
+ struct lynx_lane *lane = phy_get_drvdata(phy);
+
+ if (lane->powered_up)
+ return 0;
+
+ /* RM says that to enable a previously powered down lane, set
+ * LNmGCR0[{R,T}X_PD]=0, wait 15 us, then set LNmGCR0[{R,T}RST]=1.
+ */
+ lynx_lane_rmw(lane, LNaGCR0, 0, LNaGCR0_RX_PD | LNaGCR0_TX_PD);
+ usleep_range(150, 300);
+ lynx_10g_lane_reset(phy);
+
+ lane->powered_up = true;
+
+ return 0;
+}
+
+static void lynx_10g_lane_set_nrate(struct lynx_lane *lane,
+ struct lynx_pll *pll,
+ enum lynx_lane_mode mode)
+{
+ enum lynx_10g_rat_sel nrate;
+
+ switch (pll->frate_sel) {
+ case PLLnCR0_FRATE_5G:
+ switch (mode) {
+ case LANE_MODE_1000BASEX_SGMII:
+ nrate = RAT_SEL_QUARTER;
+ break;
+ case LANE_MODE_QSGMII:
+ nrate = RAT_SEL_FULL;
+ break;
+ default:
+ return;
+ }
+ break;
+ case PLLnCR0_FRATE_3_125G:
+ switch (mode) {
+ case LANE_MODE_2500BASEX:
+ nrate = RAT_SEL_FULL;
+ break;
+ default:
+ return;
+ }
+ break;
+ case PLLnCR0_FRATE_5_15625G:
+ switch (mode) {
+ case LANE_MODE_10GBASER:
+ case LANE_MODE_USXGMII:
+ case LANE_MODE_10G_QXGMII:
+ nrate = RAT_SEL_DOUBLE;
+ break;
+ default:
+ return;
+ }
+ break;
+ default:
+ return;
+ }
+
+ lynx_lane_rmw(lane, LNaGCR0,
+ FIELD_PREP(LNaGCR0_TRAT_SEL, nrate) |
+ FIELD_PREP(LNaGCR0_RRAT_SEL, nrate),
+ LNaGCR0_RRAT_SEL | LNaGCR0_TRAT_SEL);
+}
+
+static void lynx_10g_lane_set_pll(struct lynx_lane *lane,
+ struct lynx_pll *pll)
+{
+ if (pll->id == 0) {
+ lynx_lane_rmw(lane, LNaGCR0,
+ LNaGCR0_RPLL_PLLF | LNaGCR0_TPLL_PLLF,
+ LNaGCR0_RPLL_MSK | LNaGCR0_TPLL_MSK);
+ } else {
+ lynx_lane_rmw(lane, LNaGCR0,
+ LNaGCR0_RPLL_PLLS | LNaGCR0_TPLL_PLLS,
+ LNaGCR0_RPLL_MSK | LNaGCR0_TPLL_MSK);
+ }
+}
+
+static void lynx_10g_lane_remap_pll(struct lynx_lane *lane,
+ enum lynx_lane_mode lane_mode)
+{
+ struct lynx_priv *priv = lane->priv;
+ struct lynx_pll *pll;
+
+ /* Switch to the PLL that works with this interface type */
+ pll = lynx_pll_get(priv, lane_mode);
+ if (unlikely(!pll))
+ return;
+
+ lynx_10g_lane_set_pll(lane, pll);
+
+ /* Choose the portion of clock net to be used on this lane */
+ lynx_10g_lane_set_nrate(lane, pll, lane_mode);
+}
+
+static void lynx_10g_lane_change_proto_conf(struct lynx_lane *lane,
+ enum lynx_lane_mode mode)
+{
+ const struct lynx_10g_proto_conf *conf = &lynx_10g_proto_conf[mode];
+
+ lynx_lane_rmw(lane, LNaGCR0,
+ FIELD_PREP(LNaGCR0_PROTS, conf->proto_sel) |
+ FIELD_PREP(LNaGCR0_IF20BIT_EN, conf->if20bit_en),
+ LNaGCR0_PROTS | LNaGCR0_IF20BIT_EN);
+ lynx_lane_rmw(lane, LNaGCR1,
+ FIELD_PREP(LNaGCR1_REIDL_TH, conf->reidl_th) |
+ FIELD_PREP(LNaGCR1_REIDL_ET_MSB, conf->reidl_et_msb) |
+ FIELD_PREP(LNaGCR1_REIDL_ET_SEL, conf->reidl_et_sel) |
+ FIELD_PREP(LNaGCR1_REIDL_EX_MSB, conf->reidl_ex_msb) |
+ FIELD_PREP(LNaGCR1_REIDL_EX_SEL, conf->reidl_ex_sel) |
+ FIELD_PREP(LNaGCR1_ISLEW_RCTL, conf->islew_rctl) |
+ FIELD_PREP(LNaGCR1_OSLEW_RCTL, conf->oslew_rctl),
+ LNaGCR1_REIDL_TH |
+ LNaGCR1_REIDL_ET_MSB | LNaGCR1_REIDL_ET_SEL |
+ LNaGCR1_REIDL_EX_MSB | LNaGCR1_REIDL_EX_SEL |
+ LNaGCR1_ISLEW_RCTL | LNaGCR1_OSLEW_RCTL);
+ lynx_lane_rmw(lane, LNaRECR0,
+ FIELD_PREP(LNaRECR0_RXEQ_BST, conf->rxeq_bst) |
+ FIELD_PREP(LNaRECR0_GK2OVD, conf->gk2ovd) |
+ FIELD_PREP(LNaRECR0_GK3OVD, conf->gk3ovd) |
+ FIELD_PREP(LNaRECR0_GK2OVD_EN, conf->gk2ovd_en) |
+ FIELD_PREP(LNaRECR0_GK3OVD_EN, conf->gk3ovd_en) |
+ FIELD_PREP(LNaRECR0_BASE_WAND, conf->base_wand),
+ LNaRECR0_RXEQ_BST | LNaRECR0_GK2OVD | LNaRECR0_GK3OVD |
+ LNaRECR0_GK2OVD_EN | LNaRECR0_GK3OVD_EN |
+ LNaRECR0_BASE_WAND);
+ lynx_lane_rmw(lane, LNaTECR0,
+ FIELD_PREP(LNaTECR0_TEQ_TYPE, conf->teq_type) |
+ FIELD_PREP(LNaTECR0_SGN_PREQ, conf->sgn_preq) |
+ FIELD_PREP(LNaTECR0_RATIO_PREQ, conf->ratio_preq) |
+ FIELD_PREP(LNaTECR0_SGN_POST1Q, conf->sgn_post1q) |
+ FIELD_PREP(LNaTECR0_RATIO_PST1Q, conf->ratio_post1q) |
+ FIELD_PREP(LNaTECR0_ADPT_EQ, conf->adpt_eq) |
+ FIELD_PREP(LNaTECR0_AMP_RED, conf->amp_red),
+ LNaTECR0_TEQ_TYPE | LNaTECR0_SGN_PREQ |
+ LNaTECR0_RATIO_PREQ | LNaTECR0_SGN_POST1Q |
+ LNaTECR0_RATIO_PST1Q | LNaTECR0_ADPT_EQ |
+ LNaTECR0_AMP_RED);
+ lynx_lane_write(lane, LNaTTLCR0, conf->ttlcr0);
+}
+
+static int lynx_10g_lane_disable_pcvt(struct lynx_lane *lane,
+ enum lynx_lane_mode mode)
+{
+ struct lynx_priv *priv = lane->priv;
+ int err;
+
+ spin_lock(&priv->pcc_lock);
+
+ err = lynx_pccr_write(lane, mode, 0);
+ if (err)
+ goto out;
+
+ switch (mode) {
+ case LANE_MODE_1000BASEX_SGMII:
+ case LANE_MODE_2500BASEX:
+ err = lynx_pcvt_rmw(lane, mode, CR(1), SGMIIaCR1_SGPCS_DIS,
+ SGMIIaCR1_SGPCS_EN);
+ if (err)
+ goto out;
+
+ lynx_pcvt_rmw(lane, mode, CR(0),
+ SGMIIaCR0_RST_SGM_ON | SGMIIaCR0_PD_SGM,
+ SGMIIaCR0_RST_SGM | SGMIIaCR0_PD_SGM);
+ break;
+ case LANE_MODE_QSGMII:
+ err = lynx_pcvt_rmw(lane, mode, CR(0),
+ QSGMIIaCR0_RST_QSGM_ON | QSGMIIaCR0_PD_QSGM,
+ QSGMIIaCR0_RST_QSGM | QSGMIIaCR0_PD_QSGM);
+ if (err)
+ goto out;
+ break;
+ default:
+ err = 0;
+ }
+
+out:
+ spin_unlock(&priv->pcc_lock);
+
+ return err;
+}
+
+static int lynx_10g_lane_enable_pcvt(struct lynx_lane *lane,
+ enum lynx_lane_mode mode)
+{
+ struct lynx_priv *priv = lane->priv;
+ u32 val;
+ int err;
+
+ spin_lock(&priv->pcc_lock);
+
+ switch (mode) {
+ case LANE_MODE_1000BASEX_SGMII:
+ case LANE_MODE_2500BASEX:
+ err = lynx_pcvt_rmw(lane, mode, CR(1), SGMIIaCR1_SGPCS_EN,
+ SGMIIaCR1_SGPCS_EN);
+ if (err)
+ goto out;
+
+ lynx_pcvt_rmw(lane, mode, CR(0), SGMIIaCR0_RST_SGM_OFF,
+ SGMIIaCR0_RST_SGM | SGMIIaCR0_PD_SGM);
+ break;
+ case LANE_MODE_QSGMII:
+ err = lynx_pcvt_rmw(lane, mode, CR(0), QSGMIIaCR0_RST_QSGM_OFF,
+ QSGMIIaCR0_RST_QSGM | QSGMIIaCR0_PD_QSGM);
+ if (err)
+ goto out;
+ break;
+ default:
+ err = 0;
+ }
+
+ /* If the PCS was enabled at boot time, use the backed up PCCR value to
+ * re-enable it here, to preserve the muxing.
+ */
+ if (lynx_10g_pccr_val_enabled(lane->default_pccr[mode])) {
+ err = lynx_pccr_write(lane, mode, lane->default_pccr[mode]);
+ goto out;
+ }
+
+ /* If the PCS was not enabled, set the PCCR to a default value which
+ * enables it (1). The assumption is that this is the only PCS <->
+ * SerDes lane muxing value possible.
+ *
+ * This is mostly useful for SGMII <-> 10GBase-R major protocol
+ * reconfiguration, where at boot time, either the SGMII or the
+ * 10GBase-R PCS is enabled for the lane, but not both.
+ *
+ * In fact, if there are multiple lane muxing options, this function
+ * will most likely not choose the right one. For correct functionality
+ * there, we assume that the PCS we are enabling here was found enabled
+ * at boot time (reset default, or through PBL, or...), and we preserve
+ * its muxing through the default_pccr branch above.
+ */
+ val = 0;
+
+ switch (mode) {
+ case LANE_MODE_1000BASEX_SGMII:
+ case LANE_MODE_2500BASEX:
+ val |= FIELD_PREP(PCCR8_SGMIIa_CFG, 1);
+ break;
+ case LANE_MODE_QSGMII:
+ val |= FIELD_PREP(PCCR9_QSGMIIa_CFG, 1);
+ break;
+ case LANE_MODE_10G_QXGMII:
+ val |= FIELD_PREP(PCCR9_QXGMIIa_CFG, 1);
+ break;
+ case LANE_MODE_10GBASER:
+ val |= FIELD_PREP(PCCRB_XFIa_CFG, 1);
+ break;
+ case LANE_MODE_USXGMII:
+ val |= FIELD_PREP(PCCRB_SXGMIIa_CFG, 1);
+ break;
+ default:
+ err = 0;
+ goto out;
+ }
+
+ err = lynx_pccr_write(lane, mode, val);
+out:
+ spin_unlock(&priv->pcc_lock);
+
+ return err;
+}
+
+static bool lynx_10g_lane_mode_needs_rcw_override(struct lynx_lane *lane,
+ enum lynx_lane_mode new)
+{
+ enum lynx_lane_mode curr = lane->mode;
+
+ /* Major protocol changes, which involve changing the PCS connection to
+ * the GMII MAC with the one to the XGMII MAC, require an RCW override
+ * procedure to reconfigure an internal mux.
+ */
+ if ((lynx_lane_mode_uses_gmii_mac(curr) &&
+ lynx_lane_mode_uses_xgmii_mac(new)) ||
+ (lynx_lane_mode_uses_xgmii_mac(curr) &&
+ lynx_lane_mode_uses_gmii_mac(new)))
+ return true;
+
+ return false;
+}
+
+static int lynx_10g_validate(struct phy *phy, enum phy_mode mode, int submode,
+ union phy_configure_opts *opts)
+{
+ struct lynx_lane *lane = phy_get_drvdata(phy);
+ struct lynx_priv *priv = lane->priv;
+ enum lynx_lane_mode lane_mode;
+ int err;
+
+ err = lynx_phy_mode_to_lane_mode(phy, mode, submode, &lane_mode);
+ if (err)
+ return err;
+
+ if (lynx_10g_lane_mode_needs_rcw_override(lane, lane_mode))
+ return fsl_guts_lane_validate(priv->info->index, lane->id,
+ lane_mode);
+
+ return 0;
+}
+
+static int lynx_10g_set_mode(struct phy *phy, enum phy_mode mode, int submode)
+{
+ struct lynx_lane *lane = phy_get_drvdata(phy);
+ struct lynx_priv *priv = lane->priv;
+ bool powered_up = lane->powered_up;
+ enum lynx_lane_mode lane_mode;
+ int err;
+
+ err = lynx_10g_validate(phy, mode, submode, NULL);
+ if (err)
+ return err;
+
+ lane_mode = phy_interface_to_lane_mode(submode);
+ /* lynx_10g_validate() already made sure the lane_mode is supported */
+
+ if (lane_mode == lane->mode)
+ return 0;
+
+ /* If the lane is powered up, put the lane into the halt state while
+ * the reconfiguration is being done.
+ */
+ if (powered_up)
+ lynx_10g_lane_halt(phy);
+
+ if (lynx_10g_lane_mode_needs_rcw_override(lane, lane_mode)) {
+ err = fsl_guts_lane_set_mode(priv->info->index, lane->id,
+ lane_mode);
+ if (err)
+ goto out;
+ }
+
+ err = lynx_10g_lane_disable_pcvt(lane, lane->mode);
+ if (err)
+ goto out;
+
+ lynx_10g_lane_change_proto_conf(lane, lane_mode);
+ lynx_10g_lane_remap_pll(lane, lane_mode);
+ WARN_ON(lynx_10g_lane_enable_pcvt(lane, lane_mode));
+
+ lane->mode = lane_mode;
+
+out:
+ if (powered_up) {
+ /* The RM says to wait for at least 120 ns */
+ usleep_range(1, 2);
+ lynx_10g_lane_reset(phy);
+ }
+
+ return err;
+}
+
+static int lynx_10g_init(struct phy *phy)
+{
+ struct lynx_lane *lane = phy_get_drvdata(phy);
+
+ /* Mark the fact that the lane was init */
+ lane->init = true;
+
+ /* SerDes lanes are powered on at boot time. Any lane that is
+ * managed by this driver will get powered off when its consumer
+ * calls phy_init().
+ */
+ lane->powered_up = true;
+ lynx_10g_power_off(phy);
+
+ return 0;
+}
+
+static int lynx_10g_exit(struct phy *phy)
+{
+ struct lynx_lane *lane = phy_get_drvdata(phy);
+
+ /* The lane returns to the state where it isn't managed by the
+ * consumer, so we must treat is as if it isn't initialized, and always
+ * powered on.
+ */
+ lane->init = false;
+ lane->powered_up = false;
+ lynx_10g_power_on(phy);
+
+ return 0;
+}
+
+static const struct phy_ops lynx_10g_ops = {
+ .init = lynx_10g_init,
+ .exit = lynx_10g_exit,
+ .power_on = lynx_10g_power_on,
+ .power_off = lynx_10g_power_off,
+ .set_mode = lynx_10g_set_mode,
+ .validate = lynx_10g_validate,
+ .owner = THIS_MODULE,
+};
+
+static int lynx_10g_probe(struct platform_device *pdev)
+{
+ return lynx_probe(pdev, of_device_get_match_data(&pdev->dev),
+ &lynx_10g_ops);
+}
+
+static const struct of_device_id lynx_10g_of_match_table[] = {
+ { .compatible = "fsl,ls1028a-serdes", .data = &lynx_info_ls1028a },
+ { .compatible = "fsl,ls1046a-serdes1", .data = &lynx_info_ls1046a_serdes1 },
+ { .compatible = "fsl,ls1046a-serdes2", .data = &lynx_info_ls1046a_serdes2 },
+ { .compatible = "fsl,ls1088a-serdes1", .data = &lynx_info_ls1088a_serdes1 },
+ { .compatible = "fsl,ls2088a-serdes1", .data = &lynx_info_ls2088a_serdes1 },
+ { .compatible = "fsl,ls2088a-serdes2", .data = &lynx_info_ls2088a_serdes2 },
+ {}
+};
+MODULE_DEVICE_TABLE(of, lynx_10g_of_match_table);
+
+static struct platform_driver lynx_10g_driver = {
+ .probe = lynx_10g_probe,
+ .remove = lynx_remove,
+ .driver = {
+ .name = "lynx-10g",
+ .of_match_table = lynx_10g_of_match_table,
+ },
+};
+module_platform_driver(lynx_10g_driver);
+
+MODULE_IMPORT_NS("FSL_GUTS");
+MODULE_IMPORT_NS("PHY_FSL_LYNX");
+MODULE_AUTHOR("Ioana Ciornei <ioana.ciornei@nxp.com>");
+MODULE_AUTHOR("Vladimir Oltean <vladimir.oltean@nxp.com>");
+MODULE_DESCRIPTION("Lynx 10G SerDes PHY driver for Layerscape SoCs");
+MODULE_LICENSE("GPL");
diff --git a/drivers/phy/freescale/phy-fsl-lynx-28g.c b/drivers/phy/freescale/phy-fsl-lynx-28g.c
new file mode 100644
index 000000000..38afcd081
--- /dev/null
+++ b/drivers/phy/freescale/phy-fsl-lynx-28g.c
@@ -0,0 +1,1224 @@
+// SPDX-License-Identifier: GPL-2.0+
+/* Copyright (c) 2021-2022 NXP. */
+
+#include <linux/bitfield.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/phy.h>
+#include <linux/phy/phy.h>
+#include <linux/platform_device.h>
+#include <linux/workqueue.h>
+
+#include "phy-fsl-lynx-core.h"
+
+#define LYNX_28G_NUM_LANE 8
+
+/* SoC IP wrapper for protocol converters */
+#define PCC8 0x10a0
+#define PCC8_SGMIIa_KX BIT(3)
+#define PCC8_SGMIIa_CFG BIT(0)
+
+#define PCCC 0x10b0
+#define PCCC_SXGMIIn_XFI BIT(3)
+#define PCCC_SXGMIIn_CFG BIT(0)
+
+#define PCCD 0x10b4
+#define PCCD_E25Gn_CFG BIT(0)
+
+#define PCCE 0x10b8
+#define PCCE_E40Gn_LRV BIT(3)
+#define PCCE_E40Gn_CFG BIT(0)
+#define PCCE_E50Gn_LRV BIT(3)
+#define PCCE_E50GnCFG BIT(0)
+#define PCCE_E100Gn_LRV BIT(3)
+#define PCCE_E100Gn_CFG BIT(0)
+
+#define SGMII_CFG(id) (28 - (id) * 4) /* Offset into PCC8 */
+#define SXGMII_CFG(id) (28 - (id) * 4) /* Offset into PCCC */
+#define E25G_CFG(id) (28 - (id) * 4) /* Offset into PCCD */
+#define E40G_CFG(id) (28 - (id) * 4) /* Offset into PCCE */
+#define E50G_CFG(id) (20 - (id) * 4) /* Offset into PCCE */
+#define E100G_CFG(id) (12 - (id) * 4) /* Offset into PCCE */
+
+/* Per PLL registers */
+#define PLLnRSTCTL(pll) (0x400 + (pll) * 0x100 + 0x0)
+#define PLLnRSTCTL_DIS BIT(24)
+#define PLLnRSTCTL_LOCK BIT(23)
+
+#define PLLnCR0(pll) (0x400 + (pll) * 0x100 + 0x4)
+#define PLLnCR0_REFCLK_SEL GENMASK(20, 16)
+#define PLLnCR0_REFCLK_SEL_100MHZ 0x0
+#define PLLnCR0_REFCLK_SEL_125MHZ 0x1
+#define PLLnCR0_REFCLK_SEL_156MHZ 0x2
+#define PLLnCR0_REFCLK_SEL_150MHZ 0x3
+#define PLLnCR0_REFCLK_SEL_161MHZ 0x4
+
+#define PLLnCR1(pll) (0x400 + (pll) * 0x100 + 0x8)
+#define PLLnCR1_FRATE_SEL GENMASK(28, 24)
+#define PLLnCR1_FRATE_5G_10GVCO 0x0
+#define PLLnCR1_FRATE_5G_25GVCO 0x10
+#define PLLnCR1_FRATE_10G_20GVCO 0x6
+#define PLLnCR1_FRATE_12G_25GVCO 0x16
+
+/* Per SerDes lane registers */
+/* Lane a General Control Register */
+#define LNaGCR0(lane) (0x800 + (lane) * 0x100 + 0x0)
+#define LNaGCR0_PROTO_SEL GENMASK(7, 3)
+#define LNaGCR0_PROTO_SEL_SGMII 0x1
+#define LNaGCR0_PROTO_SEL_XFI 0xa
+#define LNaGCR0_PROTO_SEL_25G 0x1a
+#define LNaGCR0_IF_WIDTH GENMASK(2, 0)
+#define LNaGCR0_IF_WIDTH_10_BIT 0x0
+#define LNaGCR0_IF_WIDTH_20_BIT 0x2
+#define LNaGCR0_IF_WIDTH_40_BIT 0x4
+
+/* Lane a Tx Reset Control Register */
+#define LNaTRSTCTL(lane) (0x800 + (lane) * 0x100 + 0x20)
+#define LNaTRSTCTL_RST_REQ BIT(31)
+#define LNaTRSTCTL_RST_DONE BIT(30)
+#define LNaTRSTCTL_HLT_REQ BIT(27)
+#define LNaTRSTCTL_STP_REQ BIT(26)
+#define LNaTRSTCTL_DIS BIT(24)
+
+/* Lane a Tx General Control Register */
+#define LNaTGCR0(lane) (0x800 + (lane) * 0x100 + 0x24)
+#define LNaTGCR0_USE_PLL BIT(28)
+#define LNaTGCR0_USE_PLLF 0x0
+#define LNaTGCR0_USE_PLLS 0x1
+#define LNaTGCR0_N_RATE GENMASK(26, 24)
+#define LNaTGCR0_N_RATE_FULL 0x0
+#define LNaTGCR0_N_RATE_HALF 0x1
+#define LNaTGCR0_N_RATE_QUARTER 0x2
+#define LNaTGCR0_N_RATE_DOUBLE 0x3
+
+#define LNaTECR0(lane) (0x800 + (lane) * 0x100 + 0x30)
+#define LNaTECR0_EQ_TYPE GENMASK(30, 28)
+#define LNaTECR0_EQ_SGN_PREQ BIT(23)
+#define LNaTECR0_EQ_PREQ GENMASK(19, 16)
+#define LNaTECR0_EQ_SGN_POST1Q BIT(15)
+#define LNaTECR0_EQ_POST1Q GENMASK(12, 8)
+#define LNaTECR0_EQ_AMP_RED GENMASK(5, 0)
+
+#define LNaTECR1(lane) (0x800 + (lane) * 0x100 + 0x34)
+#define LNaTECR1_EQ_ADPT_EQ_DRVR_DIS BIT(31)
+#define LNaTECR1_EQ_ADPT_EQ GENMASK(29, 24)
+
+/* Lane a Rx Reset Control Register */
+#define LNaRRSTCTL(lane) (0x800 + (lane) * 0x100 + 0x40)
+#define LNaRRSTCTL_RST_REQ BIT(31)
+#define LNaRRSTCTL_RST_DONE BIT(30)
+#define LNaRRSTCTL_HLT_REQ BIT(27)
+#define LNaRRSTCTL_STP_REQ BIT(26)
+#define LNaRRSTCTL_DIS BIT(24)
+#define LNaRRSTCTL_CDR_LOCK BIT(12)
+
+/* Lane a Rx General Control Register */
+#define LNaRGCR0(lane) (0x800 + (lane) * 0x100 + 0x44)
+#define LNaRGCR0_USE_PLL BIT(28)
+#define LNaRGCR0_USE_PLLF 0x0
+#define LNaRGCR0_USE_PLLS 0x1
+#define LNaRGCR0_N_RATE GENMASK(26, 24)
+#define LNaRGCR0_N_RATE_FULL 0x0
+#define LNaRGCR0_N_RATE_HALF 0x1
+#define LNaRGCR0_N_RATE_QUARTER 0x2
+#define LNaRGCR0_N_RATE_DOUBLE 0x3
+
+#define LNaRGCR1(lane) (0x800 + (lane) * 0x100 + 0x48)
+#define LNaRGCR1_RX_ORD_ELECIDLE BIT(31)
+#define LNaRGCR1_DATA_LOST_FLT BIT(30)
+#define LNaRGCR1_DATA_LOST BIT(29)
+#define LNaRGCR1_IDLE_CONFIG BIT(28)
+#define LNaRGCR1_ENTER_IDLE_FLT_SEL GENMASK(26, 24)
+#define LNaRGCR1_EXIT_IDLE_FLT_SEL GENMASK(22, 20)
+#define LNaRGCR1_DATA_LOST_TH_SEL GENMASK(18, 16)
+#define LNaRGCR1_EXT_REC_CLK_SEL GENMASK(10, 8)
+#define LNaRGCR1_WAKE_TX_DIS BIT(5)
+#define LNaRGCR1_PHY_RDY BIT(4)
+#define LNaRGCR1_CHANGE_RX_CLK BIT(3)
+#define LNaRGCR1_PWR_MGT GENMASK(2, 0)
+
+#define LNaRECR0(lane) (0x800 + (lane) * 0x100 + 0x50)
+#define LNaRECR0_EQ_GAINK2_HF_OV_EN BIT(31)
+#define LNaRECR0_EQ_GAINK2_HF_OV GENMASK(28, 24)
+#define LNaRECR0_EQ_GAINK3_MF_OV_EN BIT(23)
+#define LNaRECR0_EQ_GAINK3_MF_OV GENMASK(20, 16)
+#define LNaRECR0_EQ_GAINK4_LF_OV_EN BIT(7)
+#define LNaRECR0_EQ_GAINK4_LF_DIS BIT(6)
+#define LNaRECR0_EQ_GAINK4_LF_OV GENMASK(4, 0)
+
+#define LNaRECR1(lane) (0x800 + (lane) * 0x100 + 0x54)
+#define LNaRECR1_EQ_BLW_OV_EN BIT(31)
+#define LNaRECR1_EQ_BLW_OV GENMASK(28, 24)
+#define LNaRECR1_EQ_OFFSET_OV_EN BIT(23)
+#define LNaRECR1_EQ_OFFSET_OV GENMASK(21, 16)
+
+#define LNaRECR2(lane) (0x800 + (lane) * 0x100 + 0x58)
+#define LNaRECR2_EQ_OFFSET_RNG_DBL BIT(31)
+#define LNaRECR2_EQ_BOOST GENMASK(29, 28)
+#define LNaRECR2_EQ_BLW_SEL GENMASK(25, 24)
+#define LNaRECR2_EQ_ZERO GENMASK(17, 16)
+#define LNaRECR2_EQ_IND GENMASK(13, 12)
+#define LNaRECR2_EQ_BIN_DATA_AVG_TC GENMASK(5, 4)
+#define LNaRECR2_SPARE_IN GENMASK(1, 0)
+
+#define LNaRECR3(lane) (0x800 + (lane) * 0x100 + 0x5c)
+#define LNaRECR3_EQ_SNAP_START BIT(31)
+#define LNaRECR3_EQ_SNAP_DONE BIT(30)
+#define LNaRECR3_EQ_GAINK2_HF_STAT GENMASK(28, 24)
+#define LNaRECR3_EQ_GAINK3_MF_STAT GENMASK(20, 16)
+#define LNaRECR3_SPARE_OUT GENMASK(13, 12)
+#define LNaRECR3_EQ_GAINK4_LF_STAT GENMASK(4, 0)
+
+#define LNaRECR4(lane) (0x800 + (lane) * 0x100 + 0x60)
+#define LNaRECR4_BLW_STAT GENMASK(28, 24)
+#define LNaRECR4_EQ_OFFSET_STAT GENMASK(21, 16)
+#define LNaRECR4_EQ_BIN_DATA_SEL GENMASK(15, 12)
+#define LNaRECR4_EQ_BIN_DATA GENMASK(8, 0) /* bit 9 is reserved */
+#define LNaRECR4_EQ_BIN_DATA_SGN BIT(8)
+
+#define LNaRCCR0(lane) (0x800 + (lane) * 0x100 + 0x68)
+#define LNaRCCR0_CAL_EN BIT(31)
+#define LNaRCCR0_MEAS_EN BIT(30)
+#define LNaRCCR0_CAL_BIN_SEL BIT(28)
+#define LNaRCCR0_CAL_DC3_DIS BIT(27)
+#define LNaRCCR0_CAL_DC2_DIS BIT(26)
+#define LNaRCCR0_CAL_DC1_DIS BIT(25)
+#define LNaRCCR0_CAL_DC0_DIS BIT(24)
+#define LNaRCCR0_CAL_AC3_OV_EN BIT(15)
+#define LNaRCCR0_CAL_AC3_OV GENMASK(11, 8)
+#define LNaRCCR0_CAL_AC2_OV_EN BIT(7)
+
+#define LNaRSCCR0(lane) (0x800 + (lane) * 0x100 + 0x74)
+#define LNaRSCCR0_SMP_OFF_EN BIT(31)
+#define LNaRSCCR0_SMP_OFF_OV_EN BIT(30)
+#define LNaRSCCR0_SMP_MAN_OFF_EN BIT(29)
+#define LNaRSCCR0_SMP_OFF_RNG_OV_EN BIT(27)
+#define LNaRSCCR0_SMP_OFF_RNG_4X_OV BIT(25)
+#define LNaRSCCR0_SMP_OFF_RNG_2X_OV BIT(24)
+#define LNaRSCCR0_SMP_AUTOZ_PD BIT(23)
+#define LNaRSCCR0_SMP_AUTOZ_CTRL GENMASK(19, 16)
+#define LNaRSCCR0_SMP_AUTOZ_D1R GENMASK(13, 12)
+#define LNaRSCCR0_SMP_AUTOZ_D1F GENMASK(9, 8)
+#define LNaRSCCR0_SMP_AUTOZ_EG1R GENMASK(5, 4)
+#define LNaRSCCR0_SMP_AUTOZ_EG1F GENMASK(1, 0)
+
+#define LNaTTLCR0(lane) (0x800 + (lane) * 0x100 + 0x80)
+#define LNaTTLCR0_TTL_FLT_SEL GENMASK(29, 24)
+#define LNaTTLCR0_TTL_SLO_PM_BYP BIT(22)
+#define LNaTTLCR0_STALL_DET_DIS BIT(21)
+#define LNaTTLCR0_INACT_MON_DIS BIT(20)
+#define LNaTTLCR0_CDR_OV GENMASK(18, 16)
+#define LNaTTLCR0_DATA_IN_SSC BIT(15)
+#define LNaTTLCR0_CDR_MIN_SMP_ON GENMASK(1, 0)
+
+#define LNaTCSR0(lane) (0x800 + (lane) * 0x100 + 0xa0)
+#define LNaTCSR0_SD_STAT_OBS_EN BIT(31)
+#define LNaTCSR0_SD_LPBK_SEL GENMASK(29, 28)
+
+#define LNaPSS(lane) (0x1000 + (lane) * 0x4)
+#define LNaPSS_TYPE GENMASK(30, 24)
+#define LNaPSS_TYPE_SGMII (PROTO_SEL_SGMII_BASEX_KX << 2)
+#define LNaPSS_TYPE_XFI (PROTO_SEL_XFI_10GBASER_KR_SXGMII << 2)
+#define LNaPSS_TYPE_40G ((PROTO_SEL_XFI_10GBASER_KR_SXGMII << 2) | 3)
+#define LNaPSS_TYPE_25G (PROTO_SEL_25G_50G_100G << 2)
+#define LNaPSS_TYPE_100G ((PROTO_SEL_25G_50G_100G << 2) | 2)
+
+/* MDEV_PORT is at the same bitfield address for all protocol converters */
+#define MDEV_PORT GENMASK(31, 27)
+
+#define SGMIIaCR0(lane) (0x1800 + (lane) * 0x10)
+#define SGMIIaCR1(lane) (0x1804 + (lane) * 0x10)
+#define SGMIIaCR1_SGPCS_EN BIT(11)
+
+#define ANLTaCR0(lane) (0x1a00 + (lane) * 0x10)
+#define ANLTaCR1(lane) (0x1a04 + (lane) * 0x10)
+
+#define SXGMIIaCR0(lane) (0x1a80 + (lane) * 0x10)
+#define SXGMIIaCR0_RST BIT(31)
+#define SXGMIIaCR0_PD BIT(30)
+
+#define SXGMIIaCR1(lane) (0x1a84 + (lane) * 0x10)
+
+#define E25GaCR0(lane) (0x1b00 + (lane) * 0x10)
+#define E25GaCR0_RST BIT(31)
+#define E25GaCR0_PD BIT(30)
+
+#define E25GaCR1(lane) (0x1b04 + (lane) * 0x10)
+
+#define E25GaCR2(lane) (0x1b08 + (lane) * 0x10)
+#define E25GaCR2_FEC_ENA BIT(23)
+#define E25GaCR2_FEC_ERR_ENA BIT(22)
+#define E25GaCR2_FEC91_ENA BIT(20)
+
+#define E40GaCR0(pcvt) (0x1b40 + (pcvt) * 0x20)
+#define E40GaCR1(pcvt) (0x1b44 + (pcvt) * 0x20)
+
+#define E50GaCR1(pcvt) (0x1b84 + (pcvt) * 0x10)
+
+#define E100GaCR1(pcvt) (0x1c04 + (pcvt) * 0x20)
+
+#define CR(x) ((x) * 4)
+
+#define LYNX_28G_LANE_HALT_SLEEP_US 100
+#define LYNX_28G_LANE_HALT_TIMEOUT_US 1000000
+
+#define LYNX_28G_LANE_RESET_SLEEP_US 100
+#define LYNX_28G_LANE_RESET_TIMEOUT_US 1000000
+
+#define LYNX_28G_LANE_STOP_SLEEP_US 100
+#define LYNX_28G_LANE_STOP_TIMEOUT_US 1000000
+
+#define lynx_28g_lane_rmw lynx_lane_rmw
+#define lynx_28g_lane_read lynx_lane_read
+#define lynx_28g_lane_write lynx_lane_write
+
+#define lynx_28g_priv lynx_priv
+#define lynx_28g_lane lynx_lane
+#define lynx_28g_pll lynx_pll
+
+enum lynx_28g_eq_type {
+ EQ_TYPE_NO_EQ = 0,
+ EQ_TYPE_2TAP = 1,
+ EQ_TYPE_3TAP = 2,
+};
+
+enum lynx_28g_proto_sel {
+ PROTO_SEL_PCIE = 0,
+ PROTO_SEL_SGMII_BASEX_KX = 1,
+ PROTO_SEL_SATA = 2,
+ PROTO_SEL_XAUI = 4,
+ PROTO_SEL_XFI_10GBASER_KR_SXGMII = 0xa,
+ PROTO_SEL_25G_50G_100G = 0x1a,
+};
+
+struct lynx_28g_proto_conf {
+ /* LNaGCR0 */
+ int proto_sel;
+ int if_width;
+ /* LNaTECR0 */
+ int teq_type;
+ int sgn_preq;
+ int ratio_preq;
+ int sgn_post1q;
+ int ratio_post1q;
+ int amp_red;
+ /* LNaTECR1 */
+ int adpt_eq;
+ /* LNaRGCR1 */
+ int enter_idle_flt_sel;
+ int exit_idle_flt_sel;
+ int data_lost_th_sel;
+ /* LNaRECR0 */
+ int gk2ovd;
+ int gk3ovd;
+ int gk4ovd;
+ int gk2ovd_en;
+ int gk3ovd_en;
+ int gk4ovd_en;
+ /* LNaRECR1 ? */
+ int eq_offset_ovd;
+ int eq_offset_ovd_en;
+ /* LNaRECR2 */
+ int eq_offset_rng_dbl;
+ int eq_blw_sel;
+ int eq_boost;
+ int spare_in;
+ /* LNaRSCCR0 */
+ int smp_autoz_d1r;
+ int smp_autoz_eg1r;
+ /* LNaRCCR0 */
+ int rccr0;
+ /* LNaTTLCR0 */
+ int ttlcr0;
+};
+
+static const struct lynx_28g_proto_conf lynx_28g_proto_conf[LANE_MODE_MAX] = {
+ [LANE_MODE_1000BASEX_SGMII] = {
+ .proto_sel = LNaGCR0_PROTO_SEL_SGMII,
+ .if_width = LNaGCR0_IF_WIDTH_10_BIT,
+ .teq_type = EQ_TYPE_NO_EQ,
+ .sgn_preq = 1,
+ .ratio_preq = 0,
+ .sgn_post1q = 1,
+ .ratio_post1q = 0,
+ .amp_red = 6,
+ .adpt_eq = 48,
+ .enter_idle_flt_sel = 4,
+ .exit_idle_flt_sel = 3,
+ .data_lost_th_sel = 1,
+ .gk2ovd = 0x1f,
+ .gk3ovd = 0,
+ .gk4ovd = 0,
+ .gk2ovd_en = 1,
+ .gk3ovd_en = 1,
+ .gk4ovd_en = 0,
+ .eq_offset_ovd = 0x1f,
+ .eq_offset_ovd_en = 0,
+ .eq_offset_rng_dbl = 0,
+ .eq_blw_sel = 0,
+ .eq_boost = 0,
+ .spare_in = 0,
+ .smp_autoz_d1r = 0,
+ .smp_autoz_eg1r = 0,
+ .rccr0 = LNaRCCR0_CAL_EN,
+ .ttlcr0 = LNaTTLCR0_TTL_SLO_PM_BYP |
+ LNaTTLCR0_DATA_IN_SSC,
+ },
+ [LANE_MODE_USXGMII] = {
+ .proto_sel = LNaGCR0_PROTO_SEL_XFI,
+ .if_width = LNaGCR0_IF_WIDTH_20_BIT,
+ .teq_type = EQ_TYPE_2TAP,
+ .sgn_preq = 1,
+ .ratio_preq = 0,
+ .sgn_post1q = 1,
+ .ratio_post1q = 3,
+ .amp_red = 7,
+ .adpt_eq = 48,
+ .enter_idle_flt_sel = 0,
+ .exit_idle_flt_sel = 0,
+ .data_lost_th_sel = 0,
+ .gk2ovd = 0,
+ .gk3ovd = 0,
+ .gk4ovd = 0,
+ .gk2ovd_en = 0,
+ .gk3ovd_en = 0,
+ .gk4ovd_en = 0,
+ .eq_offset_ovd = 0x1f,
+ .eq_offset_ovd_en = 0,
+ .eq_offset_rng_dbl = 1,
+ .eq_blw_sel = 1,
+ .eq_boost = 0,
+ .spare_in = 0,
+ .smp_autoz_d1r = 2,
+ .smp_autoz_eg1r = 0,
+ .rccr0 = LNaRCCR0_CAL_EN,
+ .ttlcr0 = LNaTTLCR0_TTL_SLO_PM_BYP |
+ LNaTTLCR0_DATA_IN_SSC,
+ },
+ [LANE_MODE_10GBASER] = {
+ .proto_sel = LNaGCR0_PROTO_SEL_XFI,
+ .if_width = LNaGCR0_IF_WIDTH_20_BIT,
+ .teq_type = EQ_TYPE_2TAP,
+ .sgn_preq = 1,
+ .ratio_preq = 0,
+ .sgn_post1q = 1,
+ .ratio_post1q = 3,
+ .amp_red = 7,
+ .adpt_eq = 48,
+ .enter_idle_flt_sel = 0,
+ .exit_idle_flt_sel = 0,
+ .data_lost_th_sel = 0,
+ .gk2ovd = 0,
+ .gk3ovd = 0,
+ .gk4ovd = 0,
+ .gk2ovd_en = 0,
+ .gk3ovd_en = 0,
+ .gk4ovd_en = 0,
+ .eq_offset_ovd = 0x1f,
+ .eq_offset_ovd_en = 0,
+ .eq_offset_rng_dbl = 1,
+ .eq_blw_sel = 1,
+ .eq_boost = 0,
+ .spare_in = 0,
+ .smp_autoz_d1r = 2,
+ .smp_autoz_eg1r = 0,
+ .rccr0 = LNaRCCR0_CAL_EN,
+ .ttlcr0 = LNaTTLCR0_TTL_SLO_PM_BYP |
+ LNaTTLCR0_DATA_IN_SSC,
+ },
+ [LANE_MODE_25GBASER] = {
+ .proto_sel = LNaGCR0_PROTO_SEL_25G,
+ .if_width = LNaGCR0_IF_WIDTH_40_BIT,
+ .teq_type = EQ_TYPE_3TAP,
+ .sgn_preq = 1,
+ .ratio_preq = 2,
+ .sgn_post1q = 1,
+ .ratio_post1q = 7,
+ .amp_red = 0,
+ .adpt_eq = 48,
+ .enter_idle_flt_sel = 0,
+ .exit_idle_flt_sel = 0,
+ .data_lost_th_sel = 0,
+ .gk2ovd = 0,
+ .gk3ovd = 0,
+ .gk4ovd = 5,
+ .gk2ovd_en = 0,
+ .gk3ovd_en = 0,
+ .gk4ovd_en = 1,
+ .eq_offset_ovd = 0x1f,
+ .eq_offset_ovd_en = 0,
+ .eq_offset_rng_dbl = 1,
+ .eq_blw_sel = 1,
+ .eq_boost = 2,
+ .spare_in = 3,
+ .smp_autoz_d1r = 2,
+ .smp_autoz_eg1r = 2,
+ .rccr0 = LNaRCCR0_CAL_EN |
+ LNaRCCR0_CAL_DC3_DIS |
+ LNaRCCR0_CAL_DC2_DIS |
+ LNaRCCR0_CAL_DC1_DIS |
+ LNaRCCR0_CAL_DC0_DIS,
+ .ttlcr0 = LNaTTLCR0_DATA_IN_SSC |
+ FIELD_PREP_CONST(LNaTTLCR0_CDR_MIN_SMP_ON, 1),
+ },
+};
+
+static void lynx_28g_lane_set_nrate(struct lynx_28g_lane *lane,
+ struct lynx_28g_pll *pll,
+ enum lynx_lane_mode lane_mode)
+{
+ switch (pll->frate_sel) {
+ case PLLnCR1_FRATE_5G_10GVCO:
+ case PLLnCR1_FRATE_5G_25GVCO:
+ switch (lane_mode) {
+ case LANE_MODE_1000BASEX_SGMII:
+ lynx_28g_lane_rmw(lane, LNaTGCR0,
+ FIELD_PREP(LNaTGCR0_N_RATE, LNaTGCR0_N_RATE_QUARTER),
+ LNaTGCR0_N_RATE);
+ lynx_28g_lane_rmw(lane, LNaRGCR0,
+ FIELD_PREP(LNaRGCR0_N_RATE, LNaRGCR0_N_RATE_QUARTER),
+ LNaRGCR0_N_RATE);
+ break;
+ default:
+ break;
+ }
+ break;
+ case PLLnCR1_FRATE_10G_20GVCO:
+ switch (lane_mode) {
+ case LANE_MODE_10GBASER:
+ case LANE_MODE_USXGMII:
+ lynx_28g_lane_rmw(lane, LNaTGCR0,
+ FIELD_PREP(LNaTGCR0_N_RATE, LNaTGCR0_N_RATE_FULL),
+ LNaTGCR0_N_RATE);
+ lynx_28g_lane_rmw(lane, LNaRGCR0,
+ FIELD_PREP(LNaRGCR0_N_RATE, LNaRGCR0_N_RATE_FULL),
+ LNaRGCR0_N_RATE);
+ break;
+ default:
+ break;
+ }
+ break;
+ case PLLnCR1_FRATE_12G_25GVCO:
+ switch (lane_mode) {
+ case LANE_MODE_25GBASER:
+ lynx_28g_lane_rmw(lane, LNaTGCR0,
+ FIELD_PREP(LNaTGCR0_N_RATE, LNaTGCR0_N_RATE_DOUBLE),
+ LNaTGCR0_N_RATE);
+ lynx_28g_lane_rmw(lane, LNaRGCR0,
+ FIELD_PREP(LNaRGCR0_N_RATE, LNaRGCR0_N_RATE_DOUBLE),
+ LNaRGCR0_N_RATE);
+ break;
+ default:
+ break;
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+static void lynx_28g_lane_set_pll(struct lynx_28g_lane *lane,
+ struct lynx_28g_pll *pll)
+{
+ if (pll->id == 0) {
+ lynx_28g_lane_rmw(lane, LNaTGCR0,
+ FIELD_PREP(LNaTGCR0_USE_PLL, LNaTGCR0_USE_PLLF),
+ LNaTGCR0_USE_PLL);
+ lynx_28g_lane_rmw(lane, LNaRGCR0,
+ FIELD_PREP(LNaRGCR0_USE_PLL, LNaRGCR0_USE_PLLF),
+ LNaRGCR0_USE_PLL);
+ } else {
+ lynx_28g_lane_rmw(lane, LNaTGCR0,
+ FIELD_PREP(LNaTGCR0_USE_PLL, LNaTGCR0_USE_PLLS),
+ LNaTGCR0_USE_PLL);
+ lynx_28g_lane_rmw(lane, LNaRGCR0,
+ FIELD_PREP(LNaRGCR0_USE_PLL, LNaRGCR0_USE_PLLS),
+ LNaRGCR0_USE_PLL);
+ }
+}
+
+static bool lynx_28g_lane_halt_done(struct lynx_28g_lane *lane)
+{
+ u32 trstctl = lynx_28g_lane_read(lane, LNaTRSTCTL);
+ u32 rrstctl = lynx_28g_lane_read(lane, LNaRRSTCTL);
+
+ return !(trstctl & LNaTRSTCTL_HLT_REQ) &&
+ !(rrstctl & LNaRRSTCTL_HLT_REQ);
+}
+
+static bool lynx_28g_lane_stop_done(struct lynx_28g_lane *lane)
+{
+ u32 trstctl = lynx_28g_lane_read(lane, LNaTRSTCTL);
+ u32 rrstctl = lynx_28g_lane_read(lane, LNaRRSTCTL);
+
+ return !(trstctl & LNaTRSTCTL_STP_REQ) &&
+ !(rrstctl & LNaRRSTCTL_STP_REQ);
+}
+
+static bool lynx_28g_lane_reset_done(struct lynx_28g_lane *lane)
+{
+ u32 trstctl = lynx_28g_lane_read(lane, LNaTRSTCTL);
+ u32 rrstctl = lynx_28g_lane_read(lane, LNaRRSTCTL);
+
+ return (trstctl & LNaTRSTCTL_RST_DONE) &&
+ (rrstctl & LNaRRSTCTL_RST_DONE);
+}
+
+/* Halting puts the lane in a mode in which it can be reconfigured */
+static int lynx_28g_lane_halt(struct phy *phy)
+{
+ struct lynx_28g_lane *lane = phy_get_drvdata(phy);
+ bool done;
+ int err;
+
+ /* Issue a halt request */
+ lynx_28g_lane_rmw(lane, LNaTRSTCTL, LNaTRSTCTL_HLT_REQ,
+ LNaTRSTCTL_HLT_REQ);
+ lynx_28g_lane_rmw(lane, LNaRRSTCTL, LNaRRSTCTL_HLT_REQ,
+ LNaRRSTCTL_HLT_REQ);
+
+ /* Wait until the halting process is complete */
+ err = read_poll_timeout(lynx_28g_lane_halt_done, done, done,
+ LYNX_28G_LANE_HALT_SLEEP_US,
+ LYNX_28G_LANE_HALT_TIMEOUT_US,
+ false, lane);
+ if (err) {
+ dev_err(&phy->dev, "Lane %c halt failed: %pe\n",
+ 'A' + lane->id, ERR_PTR(err));
+ }
+
+ return err;
+}
+
+static int lynx_28g_lane_reset(struct phy *phy)
+{
+ struct lynx_28g_lane *lane = phy_get_drvdata(phy);
+ bool done;
+ int err;
+
+ /* Issue a reset request on the lane */
+ lynx_28g_lane_rmw(lane, LNaTRSTCTL, LNaTRSTCTL_RST_REQ,
+ LNaTRSTCTL_RST_REQ);
+ lynx_28g_lane_rmw(lane, LNaRRSTCTL, LNaRRSTCTL_RST_REQ,
+ LNaRRSTCTL_RST_REQ);
+
+ /* Wait until the reset sequence is completed */
+ err = read_poll_timeout(lynx_28g_lane_reset_done, done, done,
+ LYNX_28G_LANE_RESET_SLEEP_US,
+ LYNX_28G_LANE_RESET_TIMEOUT_US,
+ false, lane);
+ if (err) {
+ dev_err(&phy->dev, "Lane %c reset failed: %pe\n",
+ 'A' + lane->id, ERR_PTR(err));
+ }
+
+ return err;
+}
+
+static int lynx_28g_power_off(struct phy *phy)
+{
+ struct lynx_28g_lane *lane = phy_get_drvdata(phy);
+ bool done;
+ int err;
+
+ if (!lane->powered_up)
+ return 0;
+
+ /* Issue a stop request */
+ lynx_28g_lane_rmw(lane, LNaTRSTCTL, LNaTRSTCTL_STP_REQ,
+ LNaTRSTCTL_STP_REQ);
+ lynx_28g_lane_rmw(lane, LNaRRSTCTL, LNaRRSTCTL_STP_REQ,
+ LNaRRSTCTL_STP_REQ);
+
+ /* Wait until the stop process is complete */
+ err = read_poll_timeout(lynx_28g_lane_stop_done, done, done,
+ LYNX_28G_LANE_STOP_SLEEP_US,
+ LYNX_28G_LANE_STOP_TIMEOUT_US,
+ false, lane);
+ if (err) {
+ dev_err(&phy->dev, "Lane %c stop failed: %pe\n",
+ 'A' + lane->id, ERR_PTR(err));
+ }
+
+ /* Power down the RX and TX portions of the lane */
+ lynx_28g_lane_rmw(lane, LNaRRSTCTL, LNaRRSTCTL_DIS,
+ LNaRRSTCTL_DIS);
+ lynx_28g_lane_rmw(lane, LNaTRSTCTL, LNaTRSTCTL_DIS,
+ LNaTRSTCTL_DIS);
+
+ lane->powered_up = false;
+
+ return 0;
+}
+
+static int lynx_28g_power_on(struct phy *phy)
+{
+ struct lynx_28g_lane *lane = phy_get_drvdata(phy);
+ int err;
+
+ if (lane->powered_up)
+ return 0;
+
+ /* Power up the RX and TX portions of the lane */
+ lynx_28g_lane_rmw(lane, LNaRRSTCTL, 0, LNaRRSTCTL_DIS);
+ lynx_28g_lane_rmw(lane, LNaTRSTCTL, 0, LNaTRSTCTL_DIS);
+
+ err = lynx_28g_lane_reset(phy);
+ if (err)
+ return err;
+
+ lane->powered_up = true;
+
+ return 0;
+}
+
+static int lynx_28g_e25g_pcvt(int lane)
+{
+ return 7 - lane;
+}
+
+static int lynx_28g_get_pccr(enum lynx_lane_mode lane_mode, int lane,
+ struct lynx_pccr *pccr)
+{
+ switch (lane_mode) {
+ case LANE_MODE_1000BASEX_SGMII:
+ pccr->offset = PCC8;
+ pccr->width = 4;
+ pccr->shift = SGMII_CFG(lane);
+ break;
+ case LANE_MODE_USXGMII:
+ case LANE_MODE_10GBASER:
+ pccr->offset = PCCC;
+ pccr->width = 4;
+ pccr->shift = SXGMII_CFG(lane);
+ break;
+ case LANE_MODE_25GBASER:
+ pccr->offset = PCCD;
+ pccr->width = 4;
+ pccr->shift = E25G_CFG(lynx_28g_e25g_pcvt(lane));
+ break;
+ default:
+ return -EOPNOTSUPP;
+ }
+
+ return 0;
+}
+
+static int lynx_28g_get_pcvt_offset(int lane, enum lynx_lane_mode lane_mode)
+{
+ switch (lane_mode) {
+ case LANE_MODE_1000BASEX_SGMII:
+ return SGMIIaCR0(lane);
+ case LANE_MODE_USXGMII:
+ case LANE_MODE_10GBASER:
+ return SXGMIIaCR0(lane);
+ case LANE_MODE_25GBASER:
+ return E25GaCR0(lynx_28g_e25g_pcvt(lane));
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static bool lx2160a_serdes1_lane_supports_mode(int lane,
+ enum lynx_lane_mode mode)
+{
+ switch (mode) {
+ case LANE_MODE_25GBASER:
+ return lane != 2 && lane != 3;
+ default:
+ return true;
+ }
+}
+
+static bool lx2160a_serdes2_lane_supports_mode(int lane,
+ enum lynx_lane_mode mode)
+{
+ switch (mode) {
+ case LANE_MODE_1000BASEX_SGMII:
+ return true;
+ case LANE_MODE_USXGMII:
+ case LANE_MODE_10GBASER:
+ return lane == 6 || lane == 7;
+ default:
+ return false;
+ }
+}
+
+static bool lx2160a_serdes3_lane_supports_mode(int lane,
+ enum lynx_lane_mode mode)
+{
+ /*
+ * Non-networking SerDes, and this driver supports only
+ * networking protocols
+ */
+ return false;
+}
+
+static bool lx2162a_serdes1_lane_supports_mode(int lane,
+ enum lynx_lane_mode mode)
+{
+ return true;
+}
+
+static bool lx2162a_serdes2_lane_supports_mode(int lane,
+ enum lynx_lane_mode mode)
+{
+ return lx2160a_serdes2_lane_supports_mode(lane, mode);
+}
+
+/* Feature set is not expected to grow for the deprecated compatible string */
+static bool lynx_28g_compat_lane_supports_mode(int lane,
+ enum lynx_lane_mode mode)
+{
+ switch (mode) {
+ case LANE_MODE_1000BASEX_SGMII:
+ case LANE_MODE_USXGMII:
+ case LANE_MODE_10GBASER:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static void lynx_28g_cdr_lock_check(struct lynx_lane *lane)
+{
+ u32 rrstctl;
+ int err;
+
+ rrstctl = lynx_28g_lane_read(lane, LNaRRSTCTL);
+ if (!!(rrstctl & LNaRRSTCTL_CDR_LOCK))
+ return;
+
+ lynx_28g_lane_rmw(lane, LNaRRSTCTL, LNaRRSTCTL_RST_REQ,
+ LNaRRSTCTL_RST_REQ);
+
+ err = read_poll_timeout(lynx_28g_lane_read, rrstctl,
+ !!(rrstctl & LNaRRSTCTL_RST_DONE),
+ LYNX_28G_LANE_RESET_SLEEP_US,
+ LYNX_28G_LANE_RESET_TIMEOUT_US,
+ false, lane, LNaRRSTCTL);
+ if (err) {
+ dev_warn_once(&lane->phy->dev,
+ "Lane %c receiver reset failed: %pe\n",
+ 'A' + lane->id, ERR_PTR(err));
+ }
+}
+
+static void lynx_28g_lane_remap_pll(struct lynx_lane *lane,
+ enum lynx_lane_mode lane_mode)
+{
+ struct lynx_priv *priv = lane->priv;
+ struct lynx_pll *pll;
+
+ /* Switch to the PLL that works with this interface type */
+ pll = lynx_pll_get(priv, lane_mode);
+ if (unlikely(!pll))
+ return;
+
+ lynx_28g_lane_set_pll(lane, pll);
+
+ /* Choose the portion of clock net to be used on this lane */
+ lynx_28g_lane_set_nrate(lane, pll, lane_mode);
+}
+
+static void lynx_28g_lane_change_proto_conf(struct lynx_28g_lane *lane,
+ enum lynx_lane_mode lane_mode)
+{
+ const struct lynx_28g_proto_conf *conf = &lynx_28g_proto_conf[lane_mode];
+
+ lynx_28g_lane_rmw(lane, LNaGCR0,
+ FIELD_PREP(LNaGCR0_PROTO_SEL, conf->proto_sel) |
+ FIELD_PREP(LNaGCR0_IF_WIDTH, conf->if_width),
+ LNaGCR0_PROTO_SEL | LNaGCR0_IF_WIDTH);
+
+ lynx_28g_lane_rmw(lane, LNaTECR0,
+ FIELD_PREP(LNaTECR0_EQ_TYPE, conf->teq_type) |
+ FIELD_PREP(LNaTECR0_EQ_SGN_PREQ, conf->sgn_preq) |
+ FIELD_PREP(LNaTECR0_EQ_PREQ, conf->ratio_preq) |
+ FIELD_PREP(LNaTECR0_EQ_SGN_POST1Q, conf->sgn_post1q) |
+ FIELD_PREP(LNaTECR0_EQ_POST1Q, conf->ratio_post1q) |
+ FIELD_PREP(LNaTECR0_EQ_AMP_RED, conf->amp_red),
+ LNaTECR0_EQ_TYPE |
+ LNaTECR0_EQ_SGN_PREQ |
+ LNaTECR0_EQ_PREQ |
+ LNaTECR0_EQ_SGN_POST1Q |
+ LNaTECR0_EQ_POST1Q |
+ LNaTECR0_EQ_AMP_RED);
+
+ lynx_28g_lane_rmw(lane, LNaTECR1,
+ FIELD_PREP(LNaTECR1_EQ_ADPT_EQ, conf->adpt_eq),
+ LNaTECR1_EQ_ADPT_EQ);
+
+ lynx_28g_lane_rmw(lane, LNaRGCR1,
+ FIELD_PREP(LNaRGCR1_ENTER_IDLE_FLT_SEL, conf->enter_idle_flt_sel) |
+ FIELD_PREP(LNaRGCR1_EXIT_IDLE_FLT_SEL, conf->exit_idle_flt_sel) |
+ FIELD_PREP(LNaRGCR1_DATA_LOST_TH_SEL, conf->data_lost_th_sel),
+ LNaRGCR1_ENTER_IDLE_FLT_SEL |
+ LNaRGCR1_EXIT_IDLE_FLT_SEL |
+ LNaRGCR1_DATA_LOST_TH_SEL);
+
+ lynx_28g_lane_rmw(lane, LNaRECR0,
+ FIELD_PREP(LNaRECR0_EQ_GAINK2_HF_OV_EN, conf->gk2ovd_en) |
+ FIELD_PREP(LNaRECR0_EQ_GAINK3_MF_OV_EN, conf->gk3ovd_en) |
+ FIELD_PREP(LNaRECR0_EQ_GAINK4_LF_OV_EN, conf->gk4ovd_en) |
+ FIELD_PREP(LNaRECR0_EQ_GAINK2_HF_OV, conf->gk2ovd) |
+ FIELD_PREP(LNaRECR0_EQ_GAINK3_MF_OV, conf->gk3ovd) |
+ FIELD_PREP(LNaRECR0_EQ_GAINK4_LF_OV, conf->gk4ovd),
+ LNaRECR0_EQ_GAINK2_HF_OV |
+ LNaRECR0_EQ_GAINK3_MF_OV |
+ LNaRECR0_EQ_GAINK4_LF_OV |
+ LNaRECR0_EQ_GAINK2_HF_OV_EN |
+ LNaRECR0_EQ_GAINK3_MF_OV_EN |
+ LNaRECR0_EQ_GAINK4_LF_OV_EN);
+
+ lynx_28g_lane_rmw(lane, LNaRECR1,
+ FIELD_PREP(LNaRECR1_EQ_OFFSET_OV, conf->eq_offset_ovd) |
+ FIELD_PREP(LNaRECR1_EQ_OFFSET_OV_EN, conf->eq_offset_ovd_en),
+ LNaRECR1_EQ_OFFSET_OV |
+ LNaRECR1_EQ_OFFSET_OV_EN);
+
+ lynx_28g_lane_rmw(lane, LNaRECR2,
+ FIELD_PREP(LNaRECR2_EQ_OFFSET_RNG_DBL, conf->eq_offset_rng_dbl) |
+ FIELD_PREP(LNaRECR2_EQ_BLW_SEL, conf->eq_blw_sel) |
+ FIELD_PREP(LNaRECR2_EQ_BOOST, conf->eq_boost) |
+ FIELD_PREP(LNaRECR2_SPARE_IN, conf->spare_in),
+ LNaRECR2_EQ_OFFSET_RNG_DBL |
+ LNaRECR2_EQ_BLW_SEL |
+ LNaRECR2_EQ_BOOST |
+ LNaRECR2_SPARE_IN);
+
+ lynx_28g_lane_rmw(lane, LNaRSCCR0,
+ FIELD_PREP(LNaRSCCR0_SMP_AUTOZ_D1R, conf->smp_autoz_d1r) |
+ FIELD_PREP(LNaRSCCR0_SMP_AUTOZ_EG1R, conf->smp_autoz_eg1r),
+ LNaRSCCR0_SMP_AUTOZ_D1R |
+ LNaRSCCR0_SMP_AUTOZ_EG1R);
+
+ lynx_28g_lane_write(lane, LNaRCCR0, conf->rccr0);
+ lynx_28g_lane_write(lane, LNaTTLCR0, conf->ttlcr0);
+}
+
+static int lynx_28g_lane_disable_pcvt(struct lynx_28g_lane *lane,
+ enum lynx_lane_mode lane_mode)
+{
+ struct lynx_28g_priv *priv = lane->priv;
+ int err;
+
+ spin_lock(&priv->pcc_lock);
+
+ err = lynx_pccr_write(lane, lane_mode, 0);
+ if (err)
+ goto out;
+
+ switch (lane_mode) {
+ case LANE_MODE_1000BASEX_SGMII:
+ err = lynx_pcvt_rmw(lane, lane_mode, CR(1), 0,
+ SGMIIaCR1_SGPCS_EN);
+ break;
+ default:
+ err = 0;
+ }
+
+out:
+ spin_unlock(&priv->pcc_lock);
+
+ return err;
+}
+
+static int lynx_28g_lane_enable_pcvt(struct lynx_28g_lane *lane,
+ enum lynx_lane_mode lane_mode)
+{
+ struct lynx_28g_priv *priv = lane->priv;
+ u32 val;
+ int err;
+
+ spin_lock(&priv->pcc_lock);
+
+ switch (lane_mode) {
+ case LANE_MODE_1000BASEX_SGMII:
+ err = lynx_pcvt_rmw(lane, lane_mode, CR(1), SGMIIaCR1_SGPCS_EN,
+ SGMIIaCR1_SGPCS_EN);
+ break;
+ default:
+ err = 0;
+ }
+
+ val = 0;
+
+ switch (lane_mode) {
+ case LANE_MODE_1000BASEX_SGMII:
+ val |= PCC8_SGMIIa_CFG;
+ break;
+ case LANE_MODE_10GBASER:
+ val |= PCCC_SXGMIIn_XFI;
+ fallthrough;
+ case LANE_MODE_USXGMII:
+ val |= PCCC_SXGMIIn_CFG;
+ break;
+ case LANE_MODE_25GBASER:
+ val |= PCCD_E25Gn_CFG;
+ break;
+ default:
+ break;
+ }
+
+ err = lynx_pccr_write(lane, lane_mode, val);
+
+ spin_unlock(&priv->pcc_lock);
+
+ return err;
+}
+
+static int lynx_28g_validate(struct phy *phy, enum phy_mode mode, int submode,
+ union phy_configure_opts *opts)
+{
+ return lynx_phy_mode_to_lane_mode(phy, mode, submode, NULL);
+}
+
+static int lynx_28g_set_mode(struct phy *phy, enum phy_mode mode, int submode)
+{
+ struct lynx_lane *lane = phy_get_drvdata(phy);
+ int powered_up = lane->powered_up;
+ enum lynx_lane_mode lane_mode;
+ int err;
+
+ err = lynx_phy_mode_to_lane_mode(phy, mode, submode, &lane_mode);
+ if (err)
+ return err;
+
+ if (lane_mode == lane->mode)
+ return 0;
+
+ /* If the lane is powered up, put the lane into the halt state while
+ * the reconfiguration is being done.
+ */
+ if (powered_up) {
+ err = lynx_28g_lane_halt(phy);
+ if (err)
+ goto out;
+ }
+
+ err = lynx_28g_lane_disable_pcvt(lane, lane->mode);
+ if (err)
+ goto out;
+
+ lynx_28g_lane_change_proto_conf(lane, lane_mode);
+ lynx_28g_lane_remap_pll(lane, lane_mode);
+ WARN_ON(lynx_28g_lane_enable_pcvt(lane, lane_mode));
+
+ lane->mode = lane_mode;
+
+out:
+ if (powered_up)
+ lynx_28g_lane_reset(phy);
+
+ return err;
+}
+
+static int lynx_28g_init(struct phy *phy)
+{
+ struct lynx_28g_lane *lane = phy_get_drvdata(phy);
+
+ /* Mark the fact that the lane was init */
+ lane->init = true;
+
+ /* SerDes lanes are powered on at boot time. Any lane that is managed
+ * by this driver will get powered down at init time aka at dpaa2-eth
+ * probe time.
+ */
+ lane->powered_up = true;
+ lynx_28g_power_off(phy);
+
+ return 0;
+}
+
+static int lynx_28g_exit(struct phy *phy)
+{
+ struct lynx_28g_lane *lane = phy_get_drvdata(phy);
+
+ /* The lane returns to the state where it isn't managed by the
+ * consumer, so we must treat is as if it isn't initialized, and
+ * always powered on.
+ */
+ lane->init = false;
+ lane->powered_up = false;
+
+ lynx_28g_power_on(phy);
+
+ return 0;
+}
+
+static const struct phy_ops lynx_28g_ops = {
+ .init = lynx_28g_init,
+ .exit = lynx_28g_exit,
+ .power_on = lynx_28g_power_on,
+ .power_off = lynx_28g_power_off,
+ .set_mode = lynx_28g_set_mode,
+ .validate = lynx_28g_validate,
+ .owner = THIS_MODULE,
+};
+
+static void lynx_28g_pll_read_configuration(struct lynx_pll *pll)
+{
+ u32 val;
+
+ val = lynx_pll_read(pll, PLLnRSTCTL);
+ pll->enabled = !(val & PLLnRSTCTL_DIS);
+ pll->locked = !!(val & PLLnRSTCTL_LOCK);
+
+ val = lynx_pll_read(pll, PLLnCR0);
+ pll->refclk_sel = FIELD_GET(PLLnCR0_REFCLK_SEL, val);
+
+ val = lynx_pll_read(pll, PLLnCR1);
+ pll->frate_sel = FIELD_GET(PLLnCR1_FRATE_SEL, val);
+
+ if (!pll->enabled)
+ return;
+
+ switch (pll->frate_sel) {
+ case PLLnCR1_FRATE_5G_10GVCO:
+ case PLLnCR1_FRATE_5G_25GVCO:
+ /* 5GHz clock net */
+ __set_bit(LANE_MODE_1000BASEX_SGMII, pll->supported);
+ break;
+ case PLLnCR1_FRATE_10G_20GVCO:
+ /* 10.3125GHz clock net */
+ __set_bit(LANE_MODE_10GBASER, pll->supported);
+ __set_bit(LANE_MODE_USXGMII, pll->supported);
+ break;
+ case PLLnCR1_FRATE_12G_25GVCO:
+ /* 12.890625GHz clock net */
+ __set_bit(LANE_MODE_25GBASER, pll->supported);
+ break;
+ default:
+ /* 6GHz, 8GHz */
+ break;
+ }
+}
+
+static void lynx_28g_lane_read_configuration(struct lynx_28g_lane *lane)
+{
+ u32 pccr, pss, protocol;
+
+ pss = lynx_28g_lane_read(lane, LNaPSS);
+ protocol = FIELD_GET(LNaPSS_TYPE, pss);
+ switch (protocol) {
+ case LNaPSS_TYPE_SGMII:
+ lane->mode = LANE_MODE_1000BASEX_SGMII;
+ break;
+ case LNaPSS_TYPE_XFI:
+ lynx_pccr_read(lane, LANE_MODE_10GBASER, &pccr);
+ if (pccr & PCCC_SXGMIIn_XFI)
+ lane->mode = LANE_MODE_10GBASER;
+ else
+ lane->mode = LANE_MODE_USXGMII;
+ break;
+ case LNaPSS_TYPE_25G:
+ lane->mode = LANE_MODE_25GBASER;
+ break;
+ default:
+ lane->mode = LANE_MODE_UNKNOWN;
+ }
+}
+
+static const struct lynx_info lynx_info_compat = {
+ .get_pccr = lynx_28g_get_pccr,
+ .get_pcvt_offset = lynx_28g_get_pcvt_offset,
+ .lane_supports_mode = lynx_28g_compat_lane_supports_mode,
+ .pll_read_configuration = lynx_28g_pll_read_configuration,
+ .lane_read_configuration = lynx_28g_lane_read_configuration,
+ .cdr_lock_check = lynx_28g_cdr_lock_check,
+ .num_lanes = LYNX_28G_NUM_LANE,
+};
+
+static const struct lynx_info lynx_info_lx2160a_serdes1 = {
+ .get_pccr = lynx_28g_get_pccr,
+ .get_pcvt_offset = lynx_28g_get_pcvt_offset,
+ .lane_supports_mode = lx2160a_serdes1_lane_supports_mode,
+ .pll_read_configuration = lynx_28g_pll_read_configuration,
+ .lane_read_configuration = lynx_28g_lane_read_configuration,
+ .cdr_lock_check = lynx_28g_cdr_lock_check,
+ .num_lanes = LYNX_28G_NUM_LANE,
+};
+
+static const struct lynx_info lynx_info_lx2160a_serdes2 = {
+ .get_pccr = lynx_28g_get_pccr,
+ .get_pcvt_offset = lynx_28g_get_pcvt_offset,
+ .lane_supports_mode = lx2160a_serdes2_lane_supports_mode,
+ .pll_read_configuration = lynx_28g_pll_read_configuration,
+ .lane_read_configuration = lynx_28g_lane_read_configuration,
+ .cdr_lock_check = lynx_28g_cdr_lock_check,
+ .num_lanes = LYNX_28G_NUM_LANE,
+};
+
+static const struct lynx_info lynx_info_lx2160a_serdes3 = {
+ .get_pccr = lynx_28g_get_pccr,
+ .get_pcvt_offset = lynx_28g_get_pcvt_offset,
+ .lane_supports_mode = lx2160a_serdes3_lane_supports_mode,
+ .pll_read_configuration = lynx_28g_pll_read_configuration,
+ .lane_read_configuration = lynx_28g_lane_read_configuration,
+ .cdr_lock_check = lynx_28g_cdr_lock_check,
+ .num_lanes = LYNX_28G_NUM_LANE,
+};
+
+static const struct lynx_info lynx_info_lx2162a_serdes1 = {
+ .get_pccr = lynx_28g_get_pccr,
+ .get_pcvt_offset = lynx_28g_get_pcvt_offset,
+ .lane_supports_mode = lx2162a_serdes1_lane_supports_mode,
+ .pll_read_configuration = lynx_28g_pll_read_configuration,
+ .lane_read_configuration = lynx_28g_lane_read_configuration,
+ .cdr_lock_check = lynx_28g_cdr_lock_check,
+ .first_lane = 4,
+ .num_lanes = LYNX_28G_NUM_LANE,
+};
+
+static const struct lynx_info lynx_info_lx2162a_serdes2 = {
+ .get_pccr = lynx_28g_get_pccr,
+ .get_pcvt_offset = lynx_28g_get_pcvt_offset,
+ .lane_supports_mode = lx2162a_serdes2_lane_supports_mode,
+ .pll_read_configuration = lynx_28g_pll_read_configuration,
+ .lane_read_configuration = lynx_28g_lane_read_configuration,
+ .cdr_lock_check = lynx_28g_cdr_lock_check,
+ .num_lanes = LYNX_28G_NUM_LANE,
+};
+
+static int lynx_28g_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ const struct lynx_info *info;
+
+ /*
+ * If we get here it means we probed on a device tree where
+ * "fsl,lynx-28g" wasn't the fallback, but the sole compatible string.
+ */
+ info = of_device_get_match_data(dev);
+ if (info == &lynx_info_compat)
+ dev_warn(dev, "Please update device tree to use per-device compatible strings\n");
+
+ return lynx_probe(pdev, info, &lynx_28g_ops);
+}
+
+static const struct of_device_id lynx_28g_of_match_table[] = {
+ { .compatible = "fsl,lx2160a-serdes1", .data = &lynx_info_lx2160a_serdes1 },
+ { .compatible = "fsl,lx2160a-serdes2", .data = &lynx_info_lx2160a_serdes2 },
+ { .compatible = "fsl,lx2160a-serdes3", .data = &lynx_info_lx2160a_serdes3 },
+ { .compatible = "fsl,lx2162a-serdes1", .data = &lynx_info_lx2162a_serdes1 },
+ { .compatible = "fsl,lx2162a-serdes2", .data = &lynx_info_lx2162a_serdes2 },
+ { .compatible = "fsl,lynx-28g", .data = &lynx_info_compat }, /* fallback, keep last */
+ { },
+};
+MODULE_DEVICE_TABLE(of, lynx_28g_of_match_table);
+
+static struct platform_driver lynx_28g_driver = {
+ .probe = lynx_28g_probe,
+ .remove = lynx_remove,
+ .driver = {
+ .name = "lynx-28g",
+ .of_match_table = lynx_28g_of_match_table,
+ },
+};
+module_platform_driver(lynx_28g_driver);
+
+MODULE_IMPORT_NS("PHY_FSL_LYNX");
+MODULE_AUTHOR("Ioana Ciornei <ioana.ciornei@nxp.com>");
+MODULE_DESCRIPTION("Lynx 28G SerDes PHY driver for Layerscape SoCs");
+MODULE_LICENSE("GPL v2");
diff --git a/drivers/phy/freescale/phy-fsl-lynx-core.c b/drivers/phy/freescale/phy-fsl-lynx-core.c
new file mode 100644
index 000000000..2cfe9236f
--- /dev/null
+++ b/drivers/phy/freescale/phy-fsl-lynx-core.c
@@ -0,0 +1,445 @@
+// SPDX-License-Identifier: GPL-2.0+
+/* Copyright 2025-2026 NXP */
+
+#include <linux/module.h>
+#include <linux/platform_device.h>
+
+#include "phy-fsl-lynx-core.h"
+
+const char *lynx_lane_mode_str(enum lynx_lane_mode lane_mode)
+{
+ switch (lane_mode) {
+ case LANE_MODE_1000BASEX_SGMII:
+ return "1000Base-X/SGMII";
+ case LANE_MODE_2500BASEX:
+ return "2500Base-X";
+ case LANE_MODE_QSGMII:
+ return "QSGMII";
+ case LANE_MODE_10G_QXGMII:
+ return "10G-QXGMII";
+ case LANE_MODE_10GBASER:
+ return "10GBase-R";
+ case LANE_MODE_USXGMII:
+ return "USXGMII";
+ case LANE_MODE_25GBASER:
+ return "25GBase-R";
+ default:
+ return "unknown";
+ }
+}
+EXPORT_SYMBOL_NS_GPL(lynx_lane_mode_str, "PHY_FSL_LYNX");
+
+enum lynx_lane_mode phy_interface_to_lane_mode(phy_interface_t intf)
+{
+ switch (intf) {
+ case PHY_INTERFACE_MODE_SGMII:
+ case PHY_INTERFACE_MODE_1000BASEX:
+ return LANE_MODE_1000BASEX_SGMII;
+ case PHY_INTERFACE_MODE_2500BASEX:
+ return LANE_MODE_2500BASEX;
+ case PHY_INTERFACE_MODE_QSGMII:
+ return LANE_MODE_QSGMII;
+ case PHY_INTERFACE_MODE_10G_QXGMII:
+ return LANE_MODE_10G_QXGMII;
+ case PHY_INTERFACE_MODE_10GBASER:
+ return LANE_MODE_10GBASER;
+ case PHY_INTERFACE_MODE_USXGMII:
+ return LANE_MODE_USXGMII;
+ case PHY_INTERFACE_MODE_25GBASER:
+ return LANE_MODE_25GBASER;
+ default:
+ return LANE_MODE_UNKNOWN;
+ }
+}
+EXPORT_SYMBOL_NS_GPL(phy_interface_to_lane_mode, "PHY_FSL_LYNX");
+
+/* By default, assume that if we know how to get the PCCR register and
+ * protocol converter for a lane, that protocol is supported.
+ */
+static bool lynx_lane_supports_mode_default(struct lynx_lane *lane,
+ enum lynx_lane_mode mode)
+{
+ struct lynx_priv *priv = lane->priv;
+ struct lynx_pccr pccr;
+
+ if (!priv->info->get_pccr || !priv->info->get_pcvt_offset)
+ return false;
+
+ if (priv->info->get_pccr(mode, lane->id, &pccr) < 0)
+ return false;
+
+ if (priv->info->get_pcvt_offset(lane->id, mode) < 0)
+ return false;
+
+ return true;
+}
+
+/* A lane mode is supported if we have a PLL that can provide its required
+ * clock net, and if there is a protocol converter for that mode on that lane.
+ */
+bool lynx_lane_supports_mode(struct lynx_lane *lane, enum lynx_lane_mode mode)
+{
+ struct lynx_priv *priv = lane->priv;
+ int i;
+
+ if (priv->info->lane_supports_mode) {
+ if (!priv->info->lane_supports_mode(lane->id, mode))
+ return false;
+ } else if (!lynx_lane_supports_mode_default(lane, mode)) {
+ return false;
+ }
+
+ for (i = 0; i < LYNX_NUM_PLL; i++) {
+ if (!priv->pll[i].enabled)
+ continue;
+
+ if (test_bit(mode, priv->pll[i].supported))
+ return true;
+ }
+
+ return false;
+}
+EXPORT_SYMBOL_NS_GPL(lynx_lane_supports_mode, "PHY_FSL_LYNX");
+
+/* The quad protocols are fixed because the lane has multiple consumers, and
+ * one phy_set_mode_ext() affects the other consumers as well. We have no use
+ * case for dynamic protocol changing here, so disallow it.
+ */
+static enum lynx_lane_mode lynx_fixed_protocols[] = {
+ LANE_MODE_QSGMII,
+ LANE_MODE_10G_QXGMII,
+};
+
+static bool lynx_lane_restrict_fixed_mode_change(struct lynx_lane *lane,
+ enum lynx_lane_mode new)
+{
+ enum lynx_lane_mode curr = lane->mode;
+
+ for (int i = 0; i < ARRAY_SIZE(lynx_fixed_protocols); i++)
+ if ((curr == lynx_fixed_protocols[i] ||
+ new == lynx_fixed_protocols[i]) &&
+ curr != new)
+ return true;
+
+ return false;
+}
+
+/* Translate the mode/submode from phy_validate() and phy_set_mode_ext() to a
+ * lane_mode and return 0 if it is supported and we can transition to it from
+ * the current lane mode, or return negative error otherwise.
+ */
+int lynx_phy_mode_to_lane_mode(struct phy *phy, enum phy_mode mode,
+ int submode, enum lynx_lane_mode *lane_mode)
+{
+ struct lynx_lane *lane = phy_get_drvdata(phy);
+ enum lynx_lane_mode tmp_lane_mode;
+
+ /* The protocol configuration tables are incomplete for full lane
+ * reconfiguration from an arbitrary protocol.
+ */
+ if (lane->mode == LANE_MODE_UNKNOWN)
+ return -EINVAL;
+
+ if (mode != PHY_MODE_ETHERNET)
+ return -EINVAL;
+
+ tmp_lane_mode = phy_interface_to_lane_mode(submode);
+ if (!lynx_lane_supports_mode(lane, tmp_lane_mode))
+ return -EINVAL;
+
+ if (lynx_lane_restrict_fixed_mode_change(lane, tmp_lane_mode))
+ return -EINVAL;
+
+ if (lane_mode)
+ *lane_mode = tmp_lane_mode;
+
+ return 0;
+}
+EXPORT_SYMBOL_NS_GPL(lynx_phy_mode_to_lane_mode, "PHY_FSL_LYNX");
+
+struct lynx_pll *lynx_pll_get(struct lynx_priv *priv, enum lynx_lane_mode mode)
+{
+ struct lynx_pll *pll;
+ int i;
+
+ for (i = 0; i < LYNX_NUM_PLL; i++) {
+ pll = &priv->pll[i];
+
+ if (!pll->enabled)
+ continue;
+
+ if (test_bit(mode, pll->supported))
+ return pll;
+ }
+
+ /* no pll supports requested mode, either caller forgot to check
+ * lynx_lane_supports_mode(), or this is a bug.
+ */
+ dev_WARN_ONCE(priv->dev, 1, "no pll for lane mode %s\n",
+ lynx_lane_mode_str(mode));
+ return NULL;
+}
+EXPORT_SYMBOL_NS_GPL(lynx_pll_get, "PHY_FSL_LYNX");
+
+int lynx_pccr_read(struct lynx_lane *lane, enum lynx_lane_mode mode, u32 *val)
+{
+ struct lynx_priv *priv = lane->priv;
+ struct lynx_pccr pccr;
+ u32 tmp;
+ int err;
+
+ err = priv->info->get_pccr(mode, lane->id, &pccr);
+ if (err)
+ return err;
+
+ tmp = lynx_read(priv, pccr.offset);
+ *val = (tmp >> pccr.shift) & GENMASK(pccr.width - 1, 0);
+
+ return 0;
+}
+EXPORT_SYMBOL_NS_GPL(lynx_pccr_read, "PHY_FSL_LYNX");
+
+int lynx_pccr_write(struct lynx_lane *lane, enum lynx_lane_mode mode, u32 val)
+{
+ struct lynx_priv *priv = lane->priv;
+ struct lynx_pccr pccr;
+ u32 old, tmp, mask;
+ int err;
+
+ err = priv->info->get_pccr(mode, lane->id, &pccr);
+ if (err)
+ return err;
+
+ old = lynx_read(priv, pccr.offset);
+ mask = GENMASK(pccr.width - 1, 0) << pccr.shift;
+ tmp = (old & ~mask) | (val << pccr.shift);
+ lynx_write(priv, pccr.offset, tmp);
+
+ dev_dbg(&lane->phy->dev, "PCCR@0x%x: 0x%x -> 0x%x\n",
+ pccr.offset, old, tmp);
+
+ return 0;
+}
+EXPORT_SYMBOL_NS_GPL(lynx_pccr_write, "PHY_FSL_LYNX");
+
+int lynx_pcvt_read(struct lynx_lane *lane, enum lynx_lane_mode mode, int cr,
+ u32 *val)
+{
+ struct lynx_priv *priv = lane->priv;
+ int offset;
+
+ offset = priv->info->get_pcvt_offset(lane->id, mode);
+ if (offset < 0)
+ return offset;
+
+ *val = lynx_read(priv, offset + cr);
+
+ return 0;
+}
+EXPORT_SYMBOL_NS_GPL(lynx_pcvt_read, "PHY_FSL_LYNX");
+
+int lynx_pcvt_write(struct lynx_lane *lane, enum lynx_lane_mode mode, int cr,
+ u32 val)
+{
+ struct lynx_priv *priv = lane->priv;
+ int offset;
+
+ offset = priv->info->get_pcvt_offset(lane->id, mode);
+ if (offset < 0)
+ return offset;
+
+ lynx_write(priv, offset + cr, val);
+
+ return 0;
+}
+EXPORT_SYMBOL_NS_GPL(lynx_pcvt_write, "PHY_FSL_LYNX");
+
+int lynx_pcvt_rmw(struct lynx_lane *lane, enum lynx_lane_mode mode, int cr,
+ u32 val, u32 mask)
+{
+ int err;
+ u32 tmp;
+
+ err = lynx_pcvt_read(lane, mode, cr, &tmp);
+ if (err)
+ return err;
+
+ tmp &= ~mask;
+ tmp |= val;
+
+ return lynx_pcvt_write(lane, mode, cr, tmp);
+}
+EXPORT_SYMBOL_NS_GPL(lynx_pcvt_rmw, "PHY_FSL_LYNX");
+
+#define work_to_lynx(w) container_of((w), struct lynx_priv, cdr_check.work)
+
+static void lynx_cdr_lock_check(struct work_struct *work)
+{
+ struct lynx_priv *priv = work_to_lynx(work);
+ struct lynx_lane *lane;
+
+ for (int i = priv->info->first_lane; i < priv->info->num_lanes; i++) {
+ lane = &priv->lane[i];
+ if (!lane->phy)
+ continue;
+
+ mutex_lock(&lane->phy->mutex);
+
+ if (!lane->init || !lane->powered_up) {
+ mutex_unlock(&lane->phy->mutex);
+ continue;
+ }
+
+ priv->info->cdr_lock_check(lane);
+
+ mutex_unlock(&lane->phy->mutex);
+ }
+
+ queue_delayed_work(system_power_efficient_wq, &priv->cdr_check,
+ msecs_to_jiffies(1000));
+}
+
+static struct phy *lynx_xlate(struct device *dev,
+ const struct of_phandle_args *args)
+{
+ struct lynx_priv *priv = dev_get_drvdata(dev);
+ int idx;
+
+ if (args->args_count == 0)
+ return of_phy_simple_xlate(dev, args);
+ else if (args->args_count != 1)
+ return ERR_PTR(-ENODEV);
+
+ idx = args->args[0];
+
+ if (WARN_ON(idx >= priv->info->num_lanes ||
+ idx < priv->info->first_lane))
+ return ERR_PTR(-EINVAL);
+
+ return priv->lane[idx].phy ?: ERR_PTR(-ENODEV);
+}
+
+static int lynx_probe_lane(struct lynx_priv *priv, int id,
+ struct device_node *dn,
+ const struct phy_ops *phy_ops)
+{
+ struct lynx_lane *lane = &priv->lane[id];
+ struct phy *phy;
+
+ phy = devm_phy_create(priv->dev, dn, phy_ops);
+ if (IS_ERR(phy))
+ return PTR_ERR(phy);
+
+ lane->priv = priv;
+ lane->phy = phy;
+ lane->id = id;
+ phy_set_drvdata(phy, lane);
+ priv->info->lane_read_configuration(lane);
+
+ return 0;
+}
+
+int lynx_probe(struct platform_device *pdev, const struct lynx_info *info,
+ const struct phy_ops *phy_ops)
+{
+ struct device *dev = &pdev->dev;
+ struct phy_provider *provider;
+ struct device_node *dn;
+ struct lynx_priv *priv;
+ int err;
+
+ dn = dev_of_node(dev);
+ if (!dn) {
+ dev_err(dev, "Device requires an OF node\n");
+ return -EINVAL;
+ }
+
+ if (!info)
+ return -ENODEV;
+
+ priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+ if (!priv)
+ return -ENOMEM;
+
+ priv->dev = dev;
+ priv->info = info;
+ priv->big_endian = device_property_read_bool(dev, "big-endian");
+ dev_set_drvdata(dev, priv);
+ spin_lock_init(&priv->pcc_lock);
+ INIT_DELAYED_WORK(&priv->cdr_check, lynx_cdr_lock_check);
+
+ priv->lane = devm_kcalloc(dev, priv->info->num_lanes,
+ sizeof(*priv->lane), GFP_KERNEL);
+ if (!priv->lane)
+ return -ENOMEM;
+
+ priv->base = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(priv->base))
+ return PTR_ERR(priv->base);
+
+ for (int i = 0; i < LYNX_NUM_PLL; i++) {
+ struct lynx_pll *pll = &priv->pll[i];
+
+ pll->priv = priv;
+ pll->id = i;
+ priv->info->pll_read_configuration(pll);
+ }
+
+ if (of_get_child_count(dn)) {
+ struct device_node *child;
+
+ for_each_available_child_of_node(dn, child) {
+ u32 reg;
+
+ /* PHY subnode name must be 'phy'. */
+ if (!(of_node_name_eq(child, "phy")))
+ continue;
+
+ if (of_property_read_u32(child, "reg", &reg)) {
+ dev_err(dev, "No \"reg\" property for %pOF\n", child);
+ of_node_put(child);
+ return -EINVAL;
+ }
+
+ if (reg < priv->info->first_lane || reg >= priv->info->num_lanes) {
+ dev_err(dev, "\"reg\" property out of range for %pOF\n", child);
+ of_node_put(child);
+ return -EINVAL;
+ }
+
+ err = lynx_probe_lane(priv, reg, child, phy_ops);
+ if (err) {
+ of_node_put(child);
+ return err;
+ }
+ }
+ } else {
+ for (int i = priv->info->first_lane; i < priv->info->num_lanes; i++) {
+ err = lynx_probe_lane(priv, i, NULL, phy_ops);
+ if (err)
+ return err;
+ }
+ }
+
+ provider = devm_of_phy_provider_register(dev, lynx_xlate);
+ if (IS_ERR(provider))
+ return PTR_ERR(provider);
+
+ queue_delayed_work(system_power_efficient_wq, &priv->cdr_check,
+ msecs_to_jiffies(1000));
+
+ return 0;
+}
+EXPORT_SYMBOL_NS_GPL(lynx_probe, "PHY_FSL_LYNX");
+
+void lynx_remove(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct lynx_priv *priv = dev_get_drvdata(dev);
+
+ cancel_delayed_work_sync(&priv->cdr_check);
+}
+EXPORT_SYMBOL_NS_GPL(lynx_remove, "PHY_FSL_LYNX");
+
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("Freescale Lynx SerDes core functionality");
diff --git a/drivers/phy/freescale/phy-fsl-lynx-core.h b/drivers/phy/freescale/phy-fsl-lynx-core.h
new file mode 100644
index 000000000..a60429ba9
--- /dev/null
+++ b/drivers/phy/freescale/phy-fsl-lynx-core.h
@@ -0,0 +1,134 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/* Copyright 2025-2026 NXP */
+
+#ifndef _PHY_FSL_LYNX_CORE_H
+#define _PHY_FSL_LYNX_CORE_H
+
+#include <linux/phy/phy.h>
+#include <linux/phy.h>
+#include <soc/fsl/phy-fsl-lynx.h>
+
+#define LYNX_NUM_PLL 2
+#define LYNX_QUIRK_HAS_HARDCODED_USXGMII BIT(0)
+
+struct lynx_priv;
+struct lynx_lane;
+
+struct lynx_pccr {
+ int offset;
+ int width;
+ int shift;
+};
+
+struct lynx_pll {
+ struct lynx_priv *priv;
+ int id;
+ int refclk_sel;
+ int frate_sel;
+ bool enabled;
+ bool locked;
+ DECLARE_BITMAP(supported, LANE_MODE_MAX);
+};
+
+struct lynx_lane {
+ struct lynx_priv *priv;
+ struct phy *phy;
+ bool powered_up;
+ bool init;
+ unsigned int id;
+ enum lynx_lane_mode mode;
+ u32 default_pccr[LANE_MODE_MAX];
+};
+
+struct lynx_info {
+ int (*get_pccr)(enum lynx_lane_mode lane_mode, int lane,
+ struct lynx_pccr *pccr);
+ int (*get_pcvt_offset)(int lane, enum lynx_lane_mode mode);
+ bool (*lane_supports_mode)(int lane, enum lynx_lane_mode mode);
+ void (*pll_read_configuration)(struct lynx_pll *pll);
+ void (*lane_read_configuration)(struct lynx_lane *lane);
+ void (*cdr_lock_check)(struct lynx_lane *lane);
+ int first_lane;
+ int num_lanes;
+ int index;
+ unsigned long quirks;
+};
+
+struct lynx_priv {
+ void __iomem *base;
+ struct device *dev;
+ const struct lynx_info *info;
+ /* Serialize concurrent access to registers shared between lanes,
+ * like PCCn
+ */
+ spinlock_t pcc_lock;
+ bool big_endian;
+ struct lynx_pll pll[LYNX_NUM_PLL];
+ struct lynx_lane *lane;
+
+ struct delayed_work cdr_check;
+};
+
+static inline u32 lynx_read(struct lynx_priv *priv, unsigned long off)
+{
+ void __iomem *reg = priv->base + off;
+
+ if (priv->big_endian)
+ return ioread32be(reg);
+
+ return ioread32(reg);
+}
+
+static inline void lynx_write(struct lynx_priv *priv, unsigned long off, u32 val)
+{
+ void __iomem *reg = priv->base + off;
+
+ if (priv->big_endian)
+ return iowrite32be(val, reg);
+
+ return iowrite32(val, reg);
+}
+
+static inline void lynx_rmw(struct lynx_priv *priv, unsigned long off, u32 val,
+ u32 mask)
+{
+ u32 orig, tmp;
+
+ orig = lynx_read(priv, off);
+ tmp = orig & ~mask;
+ tmp |= val;
+ if (orig != tmp)
+ lynx_write(priv, off, tmp);
+}
+
+#define lynx_lane_rmw(lane, reg, val, mask) \
+ lynx_rmw((lane)->priv, reg(lane->id), val, mask)
+#define lynx_lane_read(lane, reg) \
+ lynx_read((lane)->priv, reg((lane)->id))
+#define lynx_lane_write(lane, reg, val) \
+ lynx_write((lane)->priv, reg((lane)->id), val)
+#define lynx_pll_read(pll, reg) \
+ lynx_read((pll)->priv, reg((pll)->id))
+
+int lynx_probe(struct platform_device *pdev, const struct lynx_info *info,
+ const struct phy_ops *phy_ops);
+void lynx_remove(struct platform_device *pdev);
+
+const char *lynx_lane_mode_str(enum lynx_lane_mode lane_mode);
+enum lynx_lane_mode phy_interface_to_lane_mode(phy_interface_t intf);
+bool lynx_lane_supports_mode(struct lynx_lane *lane, enum lynx_lane_mode mode);
+int lynx_phy_mode_to_lane_mode(struct phy *phy, enum phy_mode mode,
+ int submode, enum lynx_lane_mode *lane_mode);
+
+struct lynx_pll *lynx_pll_get(struct lynx_priv *priv, enum lynx_lane_mode mode);
+
+int lynx_pccr_read(struct lynx_lane *lane, enum lynx_lane_mode mode, u32 *val);
+int lynx_pccr_write(struct lynx_lane *lane, enum lynx_lane_mode mode, u32 val);
+int lynx_pcvt_read(struct lynx_lane *lane, enum lynx_lane_mode mode, int cr,
+ u32 *val);
+int lynx_pcvt_write(struct lynx_lane *lane, enum lynx_lane_mode mode, int cr,
+ u32 val);
+int lynx_pcvt_rmw(struct lynx_lane *lane, enum lynx_lane_mode mode, int cr,
+ u32 val, u32 mask);
+
+#endif
diff --git a/drivers/phy/freescale/phy-fsl-samsung-hdmi.c b/drivers/phy/freescale/phy-fsl-samsung-hdmi.c
new file mode 100644
index 000000000..0f25d81de
--- /dev/null
+++ b/drivers/phy/freescale/phy-fsl-samsung-hdmi.c
@@ -0,0 +1,746 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright 2020 NXP
+ * Copyright 2022 Pengutronix, Lucas Stach <kernel@pengutronix.de>
+ */
+
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+#include <linux/clk.h>
+#include <linux/clk-provider.h>
+#include <linux/delay.h>
+#include <linux/iopoll.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/pm_runtime.h>
+
+#define PHY_REG(reg) (reg * 4)
+
+#define REG01_PMS_P_MASK GENMASK(3, 0)
+#define REG03_PMS_S_MASK GENMASK(7, 4)
+#define REG12_CK_DIV_MASK GENMASK(5, 4)
+
+#define REG13_TG_CODE_LOW_MASK GENMASK(7, 0)
+
+#define REG14_TOL_MASK GENMASK(7, 4)
+#define REG14_RP_CODE_MASK GENMASK(3, 1)
+#define REG14_TG_CODE_HIGH_MASK GENMASK(0, 0)
+
+#define REG21_SEL_TX_CK_INV BIT(7)
+#define REG21_PMS_S_MASK GENMASK(3, 0)
+/*
+ * REG33 does not match the ref manual. According to Sandor Yu from NXP,
+ * "There is a doc issue on the i.MX8MP latest RM"
+ * REG33 is being used per guidance from Sandor
+ */
+#define REG33_MODE_SET_DONE BIT(7)
+#define REG33_FIX_DA BIT(1)
+
+#define REG34_PHY_READY BIT(7)
+#define REG34_PLL_LOCK BIT(6)
+#define REG34_PHY_CLK_READY BIT(5)
+
+#ifndef MHZ
+#define MHZ (1000UL * 1000UL)
+#endif
+
+#define PHY_PLL_DIV_REGS_NUM 7
+
+struct phy_config {
+ u32 pixclk;
+ u8 pll_div_regs[PHY_PLL_DIV_REGS_NUM];
+};
+
+/*
+ * The calculated_phy_pll_cfg only handles integer divider for PMS,
+ * meaning the last four entries will be fixed, but the first three will
+ * be calculated by the PMS calculator.
+ */
+static struct phy_config calculated_phy_pll_cfg = {
+ .pixclk = 0,
+ .pll_div_regs = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00 },
+};
+
+/* The lookup table contains values for which the fractional divder is used */
+static const struct phy_config phy_pll_cfg[] = {
+ {
+ .pixclk = 22250000,
+ .pll_div_regs = { 0xd1, 0x4b, 0xf1, 0x89, 0x88, 0x80, 0x40 },
+ }, {
+ .pixclk = 23750000,
+ .pll_div_regs = { 0xd1, 0x50, 0xf1, 0x86, 0x85, 0x80, 0x40 },
+ }, {
+ .pixclk = 24024000,
+ .pll_div_regs = { 0xd1, 0x50, 0xf1, 0x99, 0x02, 0x80, 0x40 },
+ }, {
+ .pixclk = 25175000,
+ .pll_div_regs = { 0xd1, 0x54, 0xfc, 0xcc, 0x91, 0x80, 0x40 },
+ }, {
+ .pixclk = 26750000,
+ .pll_div_regs = { 0xd1, 0x5a, 0xf2, 0x89, 0x88, 0x80, 0x40 },
+ }, {
+ .pixclk = 27027000,
+ .pll_div_regs = { 0xd1, 0x5a, 0xf2, 0xfd, 0x0c, 0x80, 0x40 },
+ }, {
+ .pixclk = 29500000,
+ .pll_div_regs = { 0xd1, 0x62, 0xf4, 0x95, 0x08, 0x80, 0x40 },
+ }, {
+ .pixclk = 30750000,
+ .pll_div_regs = { 0xd1, 0x66, 0xf4, 0x82, 0x01, 0x88, 0x45 },
+ }, {
+ .pixclk = 30888000,
+ .pll_div_regs = { 0xd1, 0x66, 0xf4, 0x99, 0x18, 0x88, 0x45 },
+ }, {
+ .pixclk = 33750000,
+ .pll_div_regs = { 0xd1, 0x70, 0xf4, 0x82, 0x01, 0x80, 0x40 },
+ }, {
+ .pixclk = 35000000,
+ .pll_div_regs = { 0xd1, 0x58, 0xb8, 0x8b, 0x88, 0x80, 0x40 },
+ }, {
+ .pixclk = 36036000,
+ .pll_div_regs = { 0xd1, 0x5a, 0xb2, 0xfd, 0x0c, 0x80, 0x40 },
+ }, {
+ .pixclk = 43243200,
+ .pll_div_regs = { 0xd1, 0x5a, 0x92, 0xfd, 0x0c, 0x80, 0x40 },
+ }, {
+ .pixclk = 44500000,
+ .pll_div_regs = { 0xd1, 0x5c, 0x92, 0x98, 0x11, 0x84, 0x41 },
+ }, {
+ .pixclk = 47000000,
+ .pll_div_regs = { 0xd1, 0x62, 0x94, 0x95, 0x82, 0x80, 0x40 },
+ }, {
+ .pixclk = 47500000,
+ .pll_div_regs = { 0xd1, 0x63, 0x96, 0xa1, 0x82, 0x80, 0x40 },
+ }, {
+ .pixclk = 50349650,
+ .pll_div_regs = { 0xd1, 0x54, 0x7c, 0xc3, 0x8f, 0x80, 0x40 },
+ }, {
+ .pixclk = 53250000,
+ .pll_div_regs = { 0xd1, 0x58, 0x72, 0x84, 0x03, 0x82, 0x41 },
+ }, {
+ .pixclk = 53500000,
+ .pll_div_regs = { 0xd1, 0x5a, 0x72, 0x89, 0x88, 0x80, 0x40 },
+ }, {
+ .pixclk = 54054000,
+ .pll_div_regs = { 0xd1, 0x5a, 0x72, 0xfd, 0x0c, 0x80, 0x40 },
+ }, {
+ .pixclk = 59000000,
+ .pll_div_regs = { 0xd1, 0x62, 0x74, 0x95, 0x08, 0x80, 0x40 },
+ }, {
+ .pixclk = 59340659,
+ .pll_div_regs = { 0xd1, 0x62, 0x74, 0xdb, 0x52, 0x88, 0x47 },
+ }, {
+ .pixclk = 61500000,
+ .pll_div_regs = { 0xd1, 0x66, 0x74, 0x82, 0x01, 0x88, 0x45 },
+ }, {
+ .pixclk = 63500000,
+ .pll_div_regs = { 0xd1, 0x69, 0x74, 0x89, 0x08, 0x80, 0x40 },
+ }, {
+ .pixclk = 67500000,
+ .pll_div_regs = { 0xd1, 0x54, 0x52, 0x87, 0x03, 0x80, 0x40 },
+ }, {
+ .pixclk = 70000000,
+ .pll_div_regs = { 0xd1, 0x58, 0x58, 0x8b, 0x88, 0x80, 0x40 },
+ }, {
+ .pixclk = 72072000,
+ .pll_div_regs = { 0xd1, 0x5a, 0x52, 0xfd, 0x0c, 0x80, 0x40 },
+ }, {
+ .pixclk = 74176000,
+ .pll_div_regs = { 0xd1, 0x5d, 0x58, 0xdb, 0xA2, 0x88, 0x41 },
+ }, {
+ .pixclk = 74250000,
+ .pll_div_regs = { 0xd1, 0x5c, 0x52, 0x90, 0x0d, 0x84, 0x41 },
+ }, {
+ .pixclk = 78500000,
+ .pll_div_regs = { 0xd1, 0x62, 0x54, 0x87, 0x01, 0x80, 0x40 },
+ }, {
+ .pixclk = 82000000,
+ .pll_div_regs = { 0xd1, 0x66, 0x54, 0x82, 0x01, 0x88, 0x45 },
+ }, {
+ .pixclk = 82500000,
+ .pll_div_regs = { 0xd1, 0x67, 0x54, 0x88, 0x01, 0x90, 0x49 },
+ }, {
+ .pixclk = 89000000,
+ .pll_div_regs = { 0xd1, 0x70, 0x54, 0x84, 0x83, 0x80, 0x40 },
+ }, {
+ .pixclk = 90000000,
+ .pll_div_regs = { 0xd1, 0x70, 0x54, 0x82, 0x01, 0x80, 0x40 },
+ }, {
+ .pixclk = 94000000,
+ .pll_div_regs = { 0xd1, 0x4e, 0x32, 0xa7, 0x10, 0x80, 0x40 },
+ }, {
+ .pixclk = 95000000,
+ .pll_div_regs = { 0xd1, 0x50, 0x31, 0x86, 0x85, 0x80, 0x40 },
+ }, {
+ .pixclk = 98901099,
+ .pll_div_regs = { 0xd1, 0x52, 0x3a, 0xdb, 0x4c, 0x88, 0x47 },
+ }, {
+ .pixclk = 99000000,
+ .pll_div_regs = { 0xd1, 0x52, 0x32, 0x82, 0x01, 0x88, 0x47 },
+ }, {
+ .pixclk = 100699300,
+ .pll_div_regs = { 0xd1, 0x54, 0x3c, 0xc3, 0x8f, 0x80, 0x40 },
+ }, {
+ .pixclk = 102500000,
+ .pll_div_regs = { 0xd1, 0x55, 0x32, 0x8c, 0x05, 0x90, 0x4b },
+ }, {
+ .pixclk = 104750000,
+ .pll_div_regs = { 0xd1, 0x57, 0x32, 0x98, 0x07, 0x90, 0x49 },
+ }, {
+ .pixclk = 106500000,
+ .pll_div_regs = { 0xd1, 0x58, 0x32, 0x84, 0x03, 0x82, 0x41 },
+ }, {
+ .pixclk = 107000000,
+ .pll_div_regs = { 0xd1, 0x5a, 0x32, 0x89, 0x88, 0x80, 0x40 },
+ }, {
+ .pixclk = 108108000,
+ .pll_div_regs = { 0xd1, 0x5a, 0x32, 0xfd, 0x0c, 0x80, 0x40 },
+ }, {
+ .pixclk = 118000000,
+ .pll_div_regs = { 0xd1, 0x62, 0x34, 0x95, 0x08, 0x80, 0x40 },
+ }, {
+ .pixclk = 123000000,
+ .pll_div_regs = { 0xd1, 0x66, 0x34, 0x82, 0x01, 0x88, 0x45 },
+ }, {
+ .pixclk = 127000000,
+ .pll_div_regs = { 0xd1, 0x69, 0x34, 0x89, 0x08, 0x80, 0x40 },
+ }, {
+ .pixclk = 135000000,
+ .pll_div_regs = { 0xd1, 0x70, 0x34, 0x82, 0x01, 0x80, 0x40 },
+ }, {
+ .pixclk = 135580000,
+ .pll_div_regs = { 0xd1, 0x71, 0x39, 0xe9, 0x82, 0x9c, 0x5b },
+ }, {
+ .pixclk = 137520000,
+ .pll_div_regs = { 0xd1, 0x72, 0x38, 0x99, 0x10, 0x85, 0x41 },
+ }, {
+ .pixclk = 138750000,
+ .pll_div_regs = { 0xd1, 0x73, 0x35, 0x88, 0x05, 0x90, 0x4d },
+ }, {
+ .pixclk = 140000000,
+ .pll_div_regs = { 0xd1, 0x75, 0x36, 0xa7, 0x90, 0x80, 0x40 },
+ }, {
+ .pixclk = 148352000,
+ .pll_div_regs = { 0xd1, 0x7b, 0x35, 0xdb, 0x39, 0x90, 0x45 },
+ }, {
+ .pixclk = 148500000,
+ .pll_div_regs = { 0xd1, 0x7b, 0x35, 0x84, 0x03, 0x90, 0x45 },
+ }, {
+ .pixclk = 154000000,
+ .pll_div_regs = { 0xd1, 0x40, 0x18, 0x83, 0x01, 0x00, 0x40 },
+ }, {
+ .pixclk = 157000000,
+ .pll_div_regs = { 0xd1, 0x41, 0x11, 0xa7, 0x14, 0x80, 0x40 },
+ }, {
+ .pixclk = 160000000,
+ .pll_div_regs = { 0xd1, 0x42, 0x12, 0xa1, 0x20, 0x80, 0x40 },
+ }, {
+ .pixclk = 162000000,
+ .pll_div_regs = { 0xd1, 0x43, 0x18, 0x8b, 0x08, 0x96, 0x55 },
+ }, {
+ .pixclk = 164000000,
+ .pll_div_regs = { 0xd1, 0x45, 0x11, 0x83, 0x82, 0x90, 0x4b },
+ }, {
+ .pixclk = 165000000,
+ .pll_div_regs = { 0xd1, 0x45, 0x11, 0x84, 0x81, 0x90, 0x4b },
+ }, {
+ .pixclk = 185625000,
+ .pll_div_regs = { 0xd1, 0x4e, 0x12, 0x9a, 0x95, 0x80, 0x40 },
+ }, {
+ .pixclk = 188000000,
+ .pll_div_regs = { 0xd1, 0x4e, 0x12, 0xa7, 0x10, 0x80, 0x40 },
+ }, {
+ .pixclk = 198000000,
+ .pll_div_regs = { 0xd1, 0x52, 0x12, 0x82, 0x01, 0x88, 0x47 },
+ }, {
+ .pixclk = 205000000,
+ .pll_div_regs = { 0xd1, 0x55, 0x12, 0x8c, 0x05, 0x90, 0x4b },
+ }, {
+ .pixclk = 209500000,
+ .pll_div_regs = { 0xd1, 0x57, 0x12, 0x98, 0x07, 0x90, 0x49 },
+ }, {
+ .pixclk = 213000000,
+ .pll_div_regs = { 0xd1, 0x58, 0x12, 0x84, 0x03, 0x82, 0x41 },
+ }, {
+ .pixclk = 216216000,
+ .pll_div_regs = { 0xd1, 0x5a, 0x12, 0xfd, 0x0c, 0x80, 0x40 },
+ }, {
+ .pixclk = 254000000,
+ .pll_div_regs = { 0xd1, 0x69, 0x14, 0x89, 0x08, 0x80, 0x40 },
+ }, {
+ .pixclk = 277500000,
+ .pll_div_regs = { 0xd1, 0x73, 0x15, 0x88, 0x05, 0x90, 0x4d },
+ }, {
+ .pixclk = 297000000,
+ .pll_div_regs = { 0xd1, 0x7b, 0x15, 0x84, 0x03, 0x90, 0x45 },
+ },
+};
+
+struct reg_settings {
+ u8 reg;
+ u8 val;
+};
+
+static const struct reg_settings common_phy_cfg[] = {
+ { PHY_REG(0), 0x00 },
+ /* PHY_REG(1-7) pix clk specific */
+ { PHY_REG(8), 0x4f }, { PHY_REG(9), 0x30 },
+ { PHY_REG(10), 0x33 }, { PHY_REG(11), 0x65 },
+ /* REG12 pixclk specific */
+ /* REG13 pixclk specific */
+ /* REG14 pixclk specific */
+ { PHY_REG(15), 0x80 }, { PHY_REG(16), 0x6c },
+ { PHY_REG(17), 0xf2 }, { PHY_REG(18), 0x67 },
+ { PHY_REG(19), 0x00 }, { PHY_REG(20), 0x10 },
+ /* REG21 pixclk specific */
+ { PHY_REG(22), 0x30 }, { PHY_REG(23), 0x32 },
+ { PHY_REG(24), 0x60 }, { PHY_REG(25), 0x8f },
+ { PHY_REG(26), 0x00 }, { PHY_REG(27), 0x00 },
+ { PHY_REG(28), 0x08 }, { PHY_REG(29), 0x00 },
+ { PHY_REG(30), 0x00 }, { PHY_REG(31), 0x00 },
+ { PHY_REG(32), 0x00 }, { PHY_REG(33), 0x80 },
+ { PHY_REG(34), 0x00 }, { PHY_REG(35), 0x00 },
+ { PHY_REG(36), 0x00 }, { PHY_REG(37), 0x00 },
+ { PHY_REG(38), 0x00 }, { PHY_REG(39), 0x00 },
+ { PHY_REG(40), 0x00 }, { PHY_REG(41), 0xe0 },
+ { PHY_REG(42), 0x83 }, { PHY_REG(43), 0x0f },
+ { PHY_REG(44), 0x3E }, { PHY_REG(45), 0xf8 },
+ { PHY_REG(46), 0x00 }, { PHY_REG(47), 0x00 }
+};
+
+struct fsl_samsung_hdmi_phy {
+ struct device *dev;
+ void __iomem *regs;
+ struct clk *apbclk;
+ struct clk *refclk;
+
+ /* clk provider */
+ struct clk_hw hw;
+ const struct phy_config *cur_cfg;
+};
+
+static inline struct fsl_samsung_hdmi_phy *
+to_fsl_samsung_hdmi_phy(struct clk_hw *hw)
+{
+ return container_of(hw, struct fsl_samsung_hdmi_phy, hw);
+}
+
+static int
+fsl_samsung_hdmi_phy_configure_pll_lock_det(struct fsl_samsung_hdmi_phy *phy,
+ const struct phy_config *cfg)
+{
+ u32 pclk = cfg->pixclk;
+ u32 fld_tg_code;
+ u32 int_pllclk;
+ u8 div;
+
+ /* Find int_pllclk speed */
+ for (div = 0; div < 4; div++) {
+ int_pllclk = pclk / (1 << div);
+ if (int_pllclk < (50 * MHZ))
+ break;
+ }
+
+ if (unlikely(div == 4))
+ return -EINVAL;
+
+ writeb(FIELD_PREP(REG12_CK_DIV_MASK, div), phy->regs + PHY_REG(12));
+
+ /*
+ * Calculation for the frequency lock detector target code (fld_tg_code)
+ * is based on reference manual register description of PHY_REG13
+ * (13.10.3.1.14.2):
+ * 1st) Calculate int_pllclk which is determinded by FLD_CK_DIV
+ * 2nd) Increase resolution to avoid rounding issues
+ * 3th) Do the div (256 / Freq. of int_pllclk) * 24
+ * 4th) Reduce the resolution and always round up since the NXP
+ * settings rounding up always too. TODO: Check if that is
+ * correct.
+ */
+
+ fld_tg_code = DIV_ROUND_UP(24 * MHZ * 256, int_pllclk);
+
+ /* FLD_TOL and FLD_RP_CODE taken from downstream driver */
+ writeb(FIELD_PREP(REG13_TG_CODE_LOW_MASK, fld_tg_code),
+ phy->regs + PHY_REG(13));
+ writeb(FIELD_PREP(REG14_TOL_MASK, 2) |
+ FIELD_PREP(REG14_RP_CODE_MASK, 2) |
+ FIELD_PREP(REG14_TG_CODE_HIGH_MASK, fld_tg_code >> 8),
+ phy->regs + PHY_REG(14));
+
+ return 0;
+}
+
+static unsigned long fsl_samsung_hdmi_phy_find_pms(unsigned long fout, u8 *p, u16 *m, u8 *s)
+{
+ unsigned long best_freq = 0;
+ u32 min_delta = 0xffffffff;
+ u8 _p, best_p;
+ u16 _m, best_m;
+ u8 _s, best_s;
+
+ /*
+ * Figure 13-78 of the reference manual states the PLL should be TMDS x 5
+ * while the TMDS_CLKO should be the PLL / 5. So to calculate the PLL,
+ * take the pix clock x 5, then return the value of the PLL / 5.
+ */
+ fout *= 5;
+
+ /* The ref manual states the values of 'P' range from 1 to 11 */
+ for (_p = 1; _p <= 11; ++_p) {
+ for (_s = 1; _s <= 16; ++_s) {
+ u64 tmp;
+ u32 delta;
+
+ /* s must be one or even */
+ if (_s > 1 && (_s & 0x01) == 1)
+ _s++;
+
+ /* _s cannot be 14 per the TRM */
+ if (_s == 14)
+ continue;
+
+ /*
+ * The Ref manual doesn't explicitly state the range of M,
+ * but it does show it as an 8-bit value, so reject
+ * any value above 255.
+ */
+ tmp = (u64)fout * (_p * _s);
+ do_div(tmp, 24 * MHZ);
+ if (tmp > 255)
+ continue;
+ _m = tmp;
+
+ /*
+ * Rev 2 of the Ref Manual states the
+ * VCO can range between 750MHz and
+ * 3GHz. The VCO is assumed to be
+ * Fvco = (M * f_ref) / P,
+ * where f_ref is 24MHz.
+ */
+ tmp = div64_ul((u64)_m * 24 * MHZ, _p);
+ if (tmp < 750 * MHZ ||
+ tmp > 3000 * MHZ)
+ continue;
+
+ /* Final frequency after post-divider */
+ do_div(tmp, _s);
+
+ delta = abs(fout - tmp);
+ if (delta < min_delta) {
+ best_p = _p;
+ best_s = _s;
+ best_m = _m;
+ min_delta = delta;
+ best_freq = tmp;
+ }
+
+ /* If we have an exact match, stop looking for a better value */
+ if (!delta)
+ goto done;
+ }
+ }
+done:
+ if (best_freq) {
+ *p = best_p;
+ *m = best_m;
+ *s = best_s;
+ }
+
+ return best_freq / 5;
+}
+
+static int fsl_samsung_hdmi_phy_configure(struct fsl_samsung_hdmi_phy *phy,
+ const struct phy_config *cfg)
+{
+ int i, ret;
+ u8 val;
+
+ phy->cur_cfg = cfg;
+
+ /* HDMI PHY init */
+ writeb(REG33_FIX_DA, phy->regs + PHY_REG(33));
+
+ /* common PHY registers */
+ for (i = 0; i < ARRAY_SIZE(common_phy_cfg); i++)
+ writeb(common_phy_cfg[i].val, phy->regs + common_phy_cfg[i].reg);
+
+ /* set individual PLL registers PHY_REG1 ... PHY_REG7 */
+ for (i = 0; i < PHY_PLL_DIV_REGS_NUM; i++)
+ writeb(cfg->pll_div_regs[i], phy->regs + PHY_REG(1) + i * 4);
+
+ /* High nibble of PHY_REG3 and low nibble of PHY_REG21 both contain 'S' */
+ writeb(REG21_SEL_TX_CK_INV | FIELD_PREP(REG21_PMS_S_MASK,
+ cfg->pll_div_regs[2] >> 4), phy->regs + PHY_REG(21));
+
+ ret = fsl_samsung_hdmi_phy_configure_pll_lock_det(phy, cfg);
+ if (ret) {
+ dev_err(phy->dev, "pixclock too large\n");
+ return ret;
+ }
+
+ writeb(REG33_FIX_DA | REG33_MODE_SET_DONE, phy->regs + PHY_REG(33));
+
+ ret = readb_poll_timeout(phy->regs + PHY_REG(34), val,
+ val & REG34_PLL_LOCK, 50, 20000);
+ if (ret)
+ dev_err(phy->dev, "PLL failed to lock\n");
+
+ return ret;
+}
+
+static unsigned long phy_clk_recalc_rate(struct clk_hw *hw,
+ unsigned long parent_rate)
+{
+ struct fsl_samsung_hdmi_phy *phy = to_fsl_samsung_hdmi_phy(hw);
+
+ if (!phy->cur_cfg)
+ return 74250000;
+
+ return phy->cur_cfg->pixclk;
+}
+
+/* Helper function to lookup the available fractional-divider rate */
+static const struct phy_config *fsl_samsung_hdmi_phy_lookup_rate(unsigned long rate)
+{
+ int i;
+
+ /* Search the lookup table */
+ for (i = ARRAY_SIZE(phy_pll_cfg) - 1; i >= 0; i--)
+ if (phy_pll_cfg[i].pixclk <= rate)
+ break;
+
+ /* If there is an exact match, or the array has been searched, return the value*/
+ if (phy_pll_cfg[i].pixclk == rate || i + 1 > ARRAY_SIZE(phy_pll_cfg) - 1)
+ return &phy_pll_cfg[i];
+
+ /* See if the next entry is closer to nominal than this one */
+ return (abs((long) rate - (long) phy_pll_cfg[i].pixclk) <
+ abs((long) rate - (long) phy_pll_cfg[i+1].pixclk) ?
+ &phy_pll_cfg[i] : &phy_pll_cfg[i+1]);
+}
+
+static void fsl_samsung_hdmi_calculate_phy(struct phy_config *cal_phy, unsigned long rate,
+ u8 p, u16 m, u8 s)
+{
+ cal_phy->pixclk = rate;
+ cal_phy->pll_div_regs[0] = FIELD_PREP(REG01_PMS_P_MASK, p);
+ cal_phy->pll_div_regs[1] = m;
+ cal_phy->pll_div_regs[2] = FIELD_PREP(REG03_PMS_S_MASK, s-1);
+ /* pll_div_regs 3-6 are fixed and pre-defined already */
+}
+
+static
+const struct phy_config *fsl_samsung_hdmi_phy_find_settings(struct fsl_samsung_hdmi_phy *phy,
+ unsigned long rate)
+{
+ const struct phy_config *fract_div_phy;
+ u32 int_div_clk;
+ u16 m;
+ u8 p, s;
+
+ /* If the clock is out of range return error instead of searching */
+ if (rate > 297000000 || rate < 22250000)
+ return NULL;
+
+ /* Search the fractional divider lookup table */
+ fract_div_phy = fsl_samsung_hdmi_phy_lookup_rate(rate);
+ if (fract_div_phy->pixclk == rate) {
+ dev_dbg(phy->dev, "fractional divider match = %u\n", fract_div_phy->pixclk);
+ return fract_div_phy;
+ }
+
+ /* Calculate the integer divider */
+ int_div_clk = fsl_samsung_hdmi_phy_find_pms(rate, &p, &m, &s);
+ fsl_samsung_hdmi_calculate_phy(&calculated_phy_pll_cfg, int_div_clk, p, m, s);
+ if (int_div_clk == rate) {
+ dev_dbg(phy->dev, "integer divider match = %u\n", calculated_phy_pll_cfg.pixclk);
+ return &calculated_phy_pll_cfg;
+ }
+
+ /* Calculate the absolute value of the differences and return whichever is closest */
+ if (abs((long)rate - (long)int_div_clk) <
+ abs((long)rate - (long)fract_div_phy->pixclk)) {
+ dev_dbg(phy->dev, "integer divider = %u\n", calculated_phy_pll_cfg.pixclk);
+ return &calculated_phy_pll_cfg;
+ }
+
+ dev_dbg(phy->dev, "fractional divider = %u\n", phy->cur_cfg->pixclk);
+
+ return fract_div_phy;
+}
+
+static int fsl_samsung_hdmi_phy_clk_determine_rate(struct clk_hw *hw,
+ struct clk_rate_request *req)
+{
+ struct fsl_samsung_hdmi_phy *phy = to_fsl_samsung_hdmi_phy(hw);
+ const struct phy_config *target_settings = fsl_samsung_hdmi_phy_find_settings(phy,
+ req->rate);
+
+ if (target_settings == NULL)
+ return -EINVAL;
+
+ dev_dbg(phy->dev, "round_rate, closest rate = %u\n", target_settings->pixclk);
+ req->rate = target_settings->pixclk;
+
+ return 0;
+}
+
+static int fsl_samsung_hdmi_phy_clk_set_rate(struct clk_hw *hw,
+ unsigned long rate, unsigned long parent_rate)
+{
+ struct fsl_samsung_hdmi_phy *phy = to_fsl_samsung_hdmi_phy(hw);
+ const struct phy_config *target_settings = fsl_samsung_hdmi_phy_find_settings(phy, rate);
+
+ if (target_settings == NULL)
+ return -EINVAL;
+
+ dev_dbg(phy->dev, "set_rate, closest rate = %u\n", target_settings->pixclk);
+
+ return fsl_samsung_hdmi_phy_configure(phy, target_settings);
+}
+
+static const struct clk_ops phy_clk_ops = {
+ .recalc_rate = phy_clk_recalc_rate,
+ .determine_rate = fsl_samsung_hdmi_phy_clk_determine_rate,
+ .set_rate = fsl_samsung_hdmi_phy_clk_set_rate,
+};
+
+static int phy_clk_register(struct fsl_samsung_hdmi_phy *phy)
+{
+ struct device *dev = phy->dev;
+ struct device_node *np = dev->of_node;
+ struct clk_init_data init;
+ const char *parent_name;
+ struct clk *phyclk;
+ int ret;
+
+ parent_name = __clk_get_name(phy->refclk);
+
+ init.parent_names = &parent_name;
+ init.num_parents = 1;
+ init.flags = 0;
+ init.name = "hdmi_pclk";
+ init.ops = &phy_clk_ops;
+
+ phy->hw.init = &init;
+
+ phyclk = devm_clk_register(dev, &phy->hw);
+ if (IS_ERR(phyclk))
+ return dev_err_probe(dev, PTR_ERR(phyclk),
+ "failed to register clock\n");
+
+ ret = of_clk_add_hw_provider(np, of_clk_hw_simple_get, phyclk);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "failed to register clock provider\n");
+
+ return 0;
+}
+
+static int fsl_samsung_hdmi_phy_probe(struct platform_device *pdev)
+{
+ struct fsl_samsung_hdmi_phy *phy;
+ int ret;
+
+ phy = devm_kzalloc(&pdev->dev, sizeof(*phy), GFP_KERNEL);
+ if (!phy)
+ return -ENOMEM;
+
+ platform_set_drvdata(pdev, phy);
+ phy->dev = &pdev->dev;
+
+ phy->regs = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(phy->regs))
+ return PTR_ERR(phy->regs);
+
+ phy->apbclk = devm_clk_get_enabled(phy->dev, "apb");
+ if (IS_ERR(phy->apbclk))
+ return dev_err_probe(phy->dev, PTR_ERR(phy->apbclk),
+ "failed to get apb clk\n");
+
+ phy->refclk = devm_clk_get(phy->dev, "ref");
+ if (IS_ERR(phy->refclk))
+ return dev_err_probe(phy->dev, PTR_ERR(phy->refclk),
+ "failed to get ref clk\n");
+
+ pm_runtime_get_noresume(phy->dev);
+ pm_runtime_set_active(phy->dev);
+ pm_runtime_enable(phy->dev);
+
+ ret = phy_clk_register(phy);
+ if (ret) {
+ dev_err(&pdev->dev, "register clk failed\n");
+ goto register_clk_failed;
+ }
+
+ pm_runtime_put(phy->dev);
+
+ return 0;
+
+register_clk_failed:
+ pm_runtime_disable(phy->dev);
+ pm_runtime_put_noidle(phy->dev);
+ return ret;
+}
+
+static void fsl_samsung_hdmi_phy_remove(struct platform_device *pdev)
+{
+ of_clk_del_provider(pdev->dev.of_node);
+ pm_runtime_disable(&pdev->dev);
+}
+
+static int __maybe_unused fsl_samsung_hdmi_phy_suspend(struct device *dev)
+{
+ struct fsl_samsung_hdmi_phy *phy = dev_get_drvdata(dev);
+
+ clk_disable_unprepare(phy->apbclk);
+
+ return 0;
+}
+
+static int __maybe_unused fsl_samsung_hdmi_phy_resume(struct device *dev)
+{
+ struct fsl_samsung_hdmi_phy *phy = dev_get_drvdata(dev);
+ int ret = 0;
+
+ ret = clk_prepare_enable(phy->apbclk);
+ if (ret) {
+ dev_err(phy->dev, "failed to enable apbclk\n");
+ return ret;
+ }
+
+ if (phy->cur_cfg)
+ ret = fsl_samsung_hdmi_phy_configure(phy, phy->cur_cfg);
+
+ return ret;
+
+}
+
+static DEFINE_RUNTIME_DEV_PM_OPS(fsl_samsung_hdmi_phy_pm_ops,
+ fsl_samsung_hdmi_phy_suspend,
+ fsl_samsung_hdmi_phy_resume, NULL);
+
+static const struct of_device_id fsl_samsung_hdmi_phy_of_match[] = {
+ {
+ .compatible = "fsl,imx8mp-hdmi-phy",
+ }, {
+ /* sentinel */
+ }
+};
+MODULE_DEVICE_TABLE(of, fsl_samsung_hdmi_phy_of_match);
+
+static struct platform_driver fsl_samsung_hdmi_phy_driver = {
+ .probe = fsl_samsung_hdmi_phy_probe,
+ .remove = fsl_samsung_hdmi_phy_remove,
+ .driver = {
+ .name = "fsl-samsung-hdmi-phy",
+ .of_match_table = fsl_samsung_hdmi_phy_of_match,
+ .pm = pm_ptr(&fsl_samsung_hdmi_phy_pm_ops),
+ },
+};
+module_platform_driver(fsl_samsung_hdmi_phy_driver);
+
+MODULE_AUTHOR("Sandor Yu <Sandor.yu@nxp.com>");
+MODULE_DESCRIPTION("SAMSUNG HDMI 2.0 Transmitter PHY Driver");
+MODULE_LICENSE("GPL");