summaryrefslogtreecommitdiffstats
path: root/drivers/phy/freescale
diff options
context:
space:
mode:
authorAdrian Betschart <adrian.betschart@cinemaone.ch>2026-09-11 17:13:14 +0200
committerAlex Deucher <alexander.deucher@amd.com>2026-10-01 17:18:05 -0400
commit106b4a2a78b2a2f14885604891957b84bba502db (patch)
tree5d40eef2428fb1a4791ab46a4b19091a5910eac2 /drivers/phy/freescale
downloadlinux-stable-106b4a2a78b2a2f14885604891957b84bba502db.tar.gz
linux-stable-106b4a2a78b2a2f14885604891957b84bba502db.zip
drm/amd/display: Try RGB before YCbCr 4:4:4 in stream validationgrafted
amdgpu_dm_create_validate_stream_for_sink() walks encoding_order[] and uses the first encoding that validates. YCbCr 4:4:4 is listed before RGB, so an HDMI sink that advertises 4:4:4 gets YCbCr 4:4:4 whenever the "color format" property is left at AUTO, even though RGB fits the same link. That contradicts the documented AUTO behaviour for HDMI in enum drm_connector_color_format (RGB, falling back to YCbCr 4:2:0 only when the bandwidth is not available or the mode is 4:2:0-only), which the amdgpu implementation of the property also describes. It also leaves the "Broadcast RGB" property without effect on such sinks, since the quantization range it selects only applies to RGB output. Try RGB first. The mask still holds every encoding the sink supports, so a mode that cannot carry RGB falls back exactly as before. For reference, v7.2 picked RGB here unless YCbCr 4:4:4 was forced through debugfs, while earlier kernels picked YCbCr 4:4:4 for any HDMI sink that advertised it. Fixes: 0b0ff65d3ca1 ("drm/amd/display: Refactor stream validation") Suggested-by: Adolfo Rodrigues <adolfotregosa@gmail.com> Assisted-by: Claude Code:claude-fable-5-1 Signed-off-by: Adrian Betschart <adrian.betschart@cinemaone.ch> Reviewed-by: Fangzhi Zuo <jerry.zuo@amd.com> Tested-by: Adolfo Rodrigues <adolfotregosa@gmail.com> Tested-by: Daniel Wheeler <daniel.wheeler@amd.com> Signed-off-by: Alex Deucher <alexander.deucher@amd.com> (cherry picked from commit 7de432bc753133764dc40d37f9388da56de251e7)
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");