[PATCH 2/4] phy: s32g: Add serdes subsystem phy
From: Vincent Guittot <vincent.guittot@linaro.org>
Date: 2026-01-26 09:22:06
Also in:
linux-devicetree, linux-phy, lkml, netdev
Subsystem:
generic phy framework, the rest · Maintainers:
Vinod Koul, Linus Torvalds
s32g SoC family includes 2 serdes subsystems which are made of one PCIe controller, 2 XPCS and one Phy. The Phy got 2 lanes that can be configure to output PCIe lanes and/or SGMII. Implement PCIe phy support Co-developed-by: Ciprian Marian Costea <ciprianmarian.costea@oss.nxp.com> Signed-off-by: Ciprian Marian Costea <ciprianmarian.costea@oss.nxp.com> Co-developed-by: Alexandru-Catalin Ionita <redacted> Signed-off-by: Alexandru-Catalin Ionita <redacted> Co-developed-by: Ghennadi Procopciuc <redacted> Signed-off-by: Ghennadi Procopciuc <redacted> Co-developed-by: Ionut Vicovan <redacted> Signed-off-by: Ionut Vicovan <redacted> Co-developed-by: Bogdan Roman <redacted> Signed-off-by: Bogdan Roman <redacted> Signed-off-by: Vincent Guittot <vincent.guittot@linaro.org> --- drivers/phy/freescale/Kconfig | 9 + drivers/phy/freescale/Makefile | 1 + drivers/phy/freescale/phy-nxp-s32g-serdes.c | 569 ++++++++++++++++++++ 3 files changed, 579 insertions(+) create mode 100644 drivers/phy/freescale/phy-nxp-s32g-serdes.c
diff --git a/drivers/phy/freescale/Kconfig b/drivers/phy/freescale/Kconfig
index 81f53564ee15..45184a3cdd69 100644
--- a/drivers/phy/freescale/Kconfig
+++ b/drivers/phy/freescale/Kconfig@@ -61,3 +61,12 @@ config PHY_FSL_LYNX_28G 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. + +config PHY_S32G_SERDES + tristate "NXP S32G SERDES support" + depends on ARCH_S32 || COMPILE_TEST + select GENERIC_PHY + help + This option enables support for S23G SerDes PHY used for + PCIe & Ethernet +
diff --git a/drivers/phy/freescale/Makefile b/drivers/phy/freescale/Makefile
index 658eac7d0a62..86d948417252 100644
--- a/drivers/phy/freescale/Makefile
+++ b/drivers/phy/freescale/Makefile@@ -6,3 +6,4 @@ 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_28G) += phy-fsl-lynx-28g.o obj-$(CONFIG_PHY_FSL_SAMSUNG_HDMI_PHY) += phy-fsl-samsung-hdmi.o +obj-$(CONFIG_PHY_S32G_SERDES) += phy-nxp-s32g-serdes.o
diff --git a/drivers/phy/freescale/phy-nxp-s32g-serdes.c b/drivers/phy/freescale/phy-nxp-s32g-serdes.c
new file mode 100644
index 000000000000..8336c868c8dc
--- /dev/null
+++ b/drivers/phy/freescale/phy-nxp-s32g-serdes.c@@ -0,0 +1,569 @@ +// SPDX-License-Identifier: GPL-2.0 +/** + * SerDes driver for S32G SoCs + * + * Copyright 2021-2026 NXP + */ + +#include <dt-bindings/phy/phy.h> +#include <linux/clk.h> +#include <linux/delay.h> +#include <linux/module.h> +#include <linux/of_platform.h> +#include <linux/of_address.h> +#include <linux/phy/phy.h> +#include <linux/platform_device.h> +#include <linux/processor.h> +#include <linux/reset.h> +#include <linux/units.h> + +#define S32G_SERDES_MODE_MAX 5 + +#define EXTERNAL_CLK_NAME "ext" +#define INTERNAL_CLK_NAME "ref" + +/* Serdes Sub system registers */ + +#define S32G_PCIE_PHY_GEN_CTRL 0x0 +#define REF_USE_PAD BIT(17) +#define RX_SRIS_MODE BIT(9) + +#define S32G_PCIE_PHY_MPLLA_CTRL 0x10 +#define MPLL_STATE BIT(30) + +#define S32G_SS_RW_REG_0 0xF0 +#define SUBMODE_MASK GENMASK(3, 0) +#define CLKEN_MASK BIT(23) +#define PHY0_CR_PARA_SEL BIT(9) + +/* PCIe phy subsystem registers */ + +#define S32G_PHY_REG_ADDR 0x0 +#define PHY_REG_EN BIT(31) + +#define S32G_PHY_REG_DATA 0x4 + +#define RAWLANE0_DIG_PCS_XF_RX_EQ_DELTA_IQ_OVRD_IN 0x3019 +#define RAWLANE1_DIG_PCS_XF_RX_EQ_DELTA_IQ_OVRD_IN 0x3119 + +/* + * Until now, there is no generic way to describe and set PCIe clock mode. + * PCIe controller uses the default CRNS = 0 mode. + */ +enum pcie_phy_mode { + CRNS = 0, /* Common Reference Clock, No Spread Spectrum */ + CRSS = 1, /* Common Reference Clock, Spread Spectrum */ + SRNS = 2, /* Separate Reference Clock, No Spread Spectrum */ + SRIS = 3 /* Separate Reference Clock, Spread Spectrum */ +}; + +struct s32g_serdes_ctrl { + void __iomem *ss_base; + struct reset_control *rst; + struct clk_bulk_data *clks; + int nclks; + u32 ss_mode; + unsigned long ref_clk_rate; + bool ext_clk; +}; + +struct s32g_pcie_ctrl { + void __iomem *phy_base; + struct reset_control *rst; + struct phy *phy; + enum pcie_phy_mode phy_mode; + bool powered_on; +}; + +struct s32g_serdes { + struct s32g_serdes_ctrl ctrl; + struct s32g_pcie_ctrl pcie; + struct device *dev; +}; + +/* PCIe phy subsystem */ + +#define S32G_SERDES_PCIE_FREQ (100 * HZ_PER_MHZ) + +static int s32g_pcie_check_clk(struct s32g_serdes *serdes) +{ + struct s32g_serdes_ctrl *sctrl = &serdes->ctrl; + unsigned long rate = sctrl->ref_clk_rate; + + if (rate != S32G_SERDES_PCIE_FREQ) { + dev_err(serdes->dev, "PCIe PHY cannot operate at %lu HZ\n", rate); + return -EINVAL; + } + + return 0; +} + +static bool s32g_pcie_phy_is_locked(struct s32g_serdes *serdes) +{ + u32 mplla = readl(serdes->ctrl.ss_base + S32G_PCIE_PHY_MPLLA_CTRL); + const u32 mask = MPLL_STATE; + + return (mplla & mask) == mask; +} + +/* Serdes RFM says between 3.4 and 5.2ms depending of pll */ +#define S32G_SERDES_LOCK_TIMEOUT_MS 6 + +static int s32g_pcie_phy_power_on_common(struct s32g_serdes *serdes) +{ + struct s32g_serdes_ctrl *sctrl = &serdes->ctrl; + struct s32g_pcie_ctrl *pcie = &serdes->pcie; + u32 reg; + int val, ret = 0; + + ret = s32g_pcie_check_clk(serdes); + if (ret) + return ret; + + reg = readl(sctrl->ss_base + S32G_PCIE_PHY_GEN_CTRL); + + /* if PCIE PHY is in SRIS mode */ + if (pcie->phy_mode == SRIS) + reg |= RX_SRIS_MODE; + + if (sctrl->ext_clk) + reg |= REF_USE_PAD; + else + reg &= ~REF_USE_PAD; + + writel(reg, sctrl->ss_base + S32G_PCIE_PHY_GEN_CTRL); + + /* Monitor Serdes MPLL state */ + ret = read_poll_timeout(s32g_pcie_phy_is_locked, val, + (val), + 0, + S32G_SERDES_LOCK_TIMEOUT_MS, false, serdes); + if (ret) { + dev_err(serdes->dev, "Failed to lock PCIE phy\n"); + return -ETIMEDOUT; + } + + /* Set PHY register access to CR interface */ + reg = readl(sctrl->ss_base + S32G_SS_RW_REG_0); + reg |= PHY0_CR_PARA_SEL; + writel(reg, sctrl->ss_base + S32G_SS_RW_REG_0); + + return ret; +} + +static void s32g_pcie_phy_write(struct s32g_serdes *serdes, u32 reg, u32 val) +{ + writel(PHY_REG_EN, serdes->pcie.phy_base + S32G_PHY_REG_ADDR); + writel(reg | PHY_REG_EN, serdes->pcie.phy_base + S32G_PHY_REG_ADDR); + usleep_range(100, 110); + writel(val, serdes->pcie.phy_base + S32G_PHY_REG_DATA); + usleep_range(100, 110); + writel(0, serdes->pcie.phy_base + S32G_PHY_REG_ADDR); +} + +static int s32g_pcie_phy_power_on(struct s32g_serdes *serdes) +{ + struct s32g_pcie_ctrl *pcie = &serdes->pcie; + struct s32g_serdes_ctrl *ctrl = &serdes->ctrl; + u32 iq_ovrd_in; + int ret = 0; + + ret = s32g_pcie_phy_power_on_common(serdes); + if (ret) + return ret; + + /* RX_EQ_DELTA_IQ_OVRD enable and override value for PCIe lanes */ + iq_ovrd_in = RAWLANE0_DIG_PCS_XF_RX_EQ_DELTA_IQ_OVRD_IN; + + s32g_pcie_phy_write(serdes, iq_ovrd_in, 0x3); + s32g_pcie_phy_write(serdes, iq_ovrd_in, 0x13); + + if (ctrl->ss_mode == 0) { + iq_ovrd_in = RAWLANE1_DIG_PCS_XF_RX_EQ_DELTA_IQ_OVRD_IN; + + s32g_pcie_phy_write(serdes, iq_ovrd_in, 0x3); + s32g_pcie_phy_write(serdes, iq_ovrd_in, 0x13); + } + + pcie->powered_on = true; + + return 0; +} + +/* PCIe phy ops function */ + +static int s32g_serdes_phy_power_on(struct phy *p) +{ + struct s32g_serdes *serdes = phy_get_drvdata(p); + + return s32g_pcie_phy_power_on(serdes); +} + +static inline bool is_pcie_phy_mode_valid(int mode) +{ + switch (mode) { + case CRNS: + case CRSS: + case SRNS: + case SRIS: + return true; + default: + return false; + } +} + +static int s32g_serdes_phy_set_mode_ext(struct phy *p, + enum phy_mode mode, int submode) +{ + struct s32g_serdes *serdes = phy_get_drvdata(p); + + if (mode == PHY_MODE_PCIE) + return -EINVAL; + + if (!is_pcie_phy_mode_valid(submode)) + return -EINVAL; + + /* + * Do not configure SRIS or CRSS PHY MODE in conjunction + * with any SGMII mode on the same SerDes subsystem + */ + if ((submode == CRSS || submode == SRIS) && + serdes->ctrl.ss_mode != 0) + return -EINVAL; + + /* + * Internal reference clock cannot be used with either Common clock + * or Spread spectrum, leaving only SRNSS + */ + if (submode != SRNS && !serdes->ctrl.ext_clk) + return -EINVAL; + + serdes->pcie.phy_mode = submode; + + return 0; +} + +static const struct phy_ops serdes_pcie_ops = { + .power_on = s32g_serdes_phy_power_on, + .set_mode = s32g_serdes_phy_set_mode_ext, +}; + +static struct phy *s32g_serdes_phy_xlate(struct device *dev, + const struct of_phandle_args *args) +{ + struct s32g_serdes *serdes; + struct phy *phy; + + serdes = dev_get_drvdata(dev); + if (!serdes) + return ERR_PTR(-EINVAL); + + phy = serdes->pcie.phy; + + return phy; +} + +/* Serdes subsystem */ + +static int s32g_serdes_assert_reset(struct s32g_serdes *serdes) +{ + struct device *dev = serdes->dev; + int ret; + + ret = reset_control_assert(serdes->pcie.rst); + if (ret) { + dev_err(dev, "Failed to assert PCIE reset: %d\n", ret); + return ret; + } + + ret = reset_control_assert(serdes->ctrl.rst); + if (ret) { + dev_err(dev, "Failed to assert SerDes reset: %d\n", ret); + return ret; + } + + return 0; +} + +static int s32g_serdes_deassert_reset(struct s32g_serdes *serdes) +{ + struct device *dev = serdes->dev; + int ret; + + ret = reset_control_deassert(serdes->pcie.rst); + if (ret) { + dev_err(dev, "Failed to assert PCIE reset: %d\n", ret); + return ret; + } + + ret = reset_control_deassert(serdes->ctrl.rst); + if (ret) { + dev_err(dev, "Failed to assert SerDes reset: %d\n", ret); + return ret; + } + + return ret; +} + +static int s32g_serdes_init(struct s32g_serdes *serdes) +{ + struct s32g_serdes_ctrl *ctrl = &serdes->ctrl; + u32 reg0; + int ret; + + ret = clk_bulk_prepare_enable(ctrl->nclks, ctrl->clks); + if (ret) { + dev_err(serdes->dev, "Failed to enable SerDes clocks\n"); + return ret; + } + + ret = s32g_serdes_assert_reset(serdes); + if (ret) + goto disable_clks; + + /* Set serdes mode */ + reg0 = readl(ctrl->ss_base + S32G_SS_RW_REG_0); + reg0 &= ~SUBMODE_MASK; + if (ctrl->ss_mode == 5) + reg0 |= 2; + else + reg0 |= ctrl->ss_mode; + writel(reg0, ctrl->ss_base + S32G_SS_RW_REG_0); + + /* Set Clock source: internal or external */ + reg0 = readl(ctrl->ss_base + S32G_SS_RW_REG_0); + if (ctrl->ext_clk) + reg0 &= ~CLKEN_MASK; + else + reg0 |= CLKEN_MASK; + + writel(reg0, ctrl->ss_base + S32G_SS_RW_REG_0); + + /* Wait for the selection of working mode (as per the manual specs) */ + usleep_range(100, 110); + + ret = s32g_serdes_deassert_reset(serdes); + if (ret) + goto disable_clks; + + dev_info(serdes->dev, "Using mode %d for SerDes subsystem\n", + ctrl->ss_mode); + + return 0; + +disable_clks: + clk_bulk_disable_unprepare(serdes->ctrl.nclks, + serdes->ctrl.clks); + + return ret; +} + +static int s32g_serdes_get_ctrl_resources(struct platform_device *pdev, struct s32g_serdes *serdes) +{ + struct s32g_serdes_ctrl *ctrl = &serdes->ctrl; + struct device *dev = &pdev->dev; + int ret, idx; + + ret = of_property_read_u32(dev->of_node, "nxp,sys-mode", + &ctrl->ss_mode); + if (ret) { + dev_err(dev, "Failed to get SerDes subsystem mode\n"); + return -EINVAL; + } + + if (ctrl->ss_mode > S32G_SERDES_MODE_MAX) { + dev_err(dev, "Invalid SerDes subsystem mode %u\n", + ctrl->ss_mode); + return -EINVAL; + } + + ctrl->ss_base = devm_platform_ioremap_resource_byname(pdev, "ss_pcie"); + if (IS_ERR(ctrl->ss_base)) { + dev_err(dev, "Failed to map 'ss_pcie'\n"); + return PTR_ERR(ctrl->ss_base); + } + + ctrl->rst = devm_reset_control_get(dev, "serdes"); + if (IS_ERR(ctrl->rst)) + return dev_err_probe(dev, PTR_ERR(ctrl->rst), + "Failed to get 'serdes' reset control\n"); + + ctrl->nclks = devm_clk_bulk_get_all(dev, &ctrl->clks); + if (ctrl->nclks < 1) + return dev_err_probe(dev, ctrl->nclks, + "Failed to get SerDes clocks\n"); + + idx = of_property_match_string(dev->of_node, "clock-names", EXTERNAL_CLK_NAME); + if (idx < 0) + idx = of_property_match_string(dev->of_node, "clock-names", INTERNAL_CLK_NAME); + else + ctrl->ext_clk = true; + + if (idx < 0) { + dev_err(dev, "Failed to get Phy reference clock source\n"); + return -EINVAL; + } + + ctrl->ref_clk_rate = clk_get_rate(ctrl->clks[idx].clk); + if (!ctrl->ref_clk_rate) { + dev_err(dev, "Failed to get Phy reference clock rate\n"); + return -EINVAL; + } + + return ret; +} + +static int s32g_serdes_get_pcie_resources(struct platform_device *pdev, struct s32g_serdes *serdes) +{ + struct s32g_pcie_ctrl *pcie = &serdes->pcie; + struct device *dev = &pdev->dev; + + pcie->phy_base = devm_platform_ioremap_resource_byname(pdev, + "pcie_phy"); + if (IS_ERR(pcie->phy_base)) { + dev_err(dev, "Failed to map 'pcie_phy'\n"); + return PTR_ERR(pcie->phy_base); + } + + pcie->rst = devm_reset_control_get(dev, "pcie"); + if (IS_ERR(pcie->rst)) + return dev_err_probe(dev, IS_ERR(pcie->rst), + "Failed to get 'pcie' reset control\n"); + + return 0; +} + +static int s32g2_serdes_create_phy(struct s32g_serdes *serdes, struct device_node *child_node) +{ + struct s32g_serdes_ctrl *ctrl = &serdes->ctrl; + struct phy_provider *phy_provider; + struct device *dev = serdes->dev; + int ss_mode = ctrl->ss_mode; + struct phy *phy; + + if (of_device_is_compatible(child_node, "nxp,s32g2-serdes-pcie-phy")) { + /* no PCIe phy lane */ + if (ss_mode > 2) + return 0; + + phy = devm_phy_create(dev, child_node, &serdes_pcie_ops); + if (IS_ERR(phy)) + return PTR_ERR(phy); + + phy_set_drvdata(phy, serdes); + + phy->attrs.mode = PHY_MODE_PCIE; + phy->id = 0; + serdes->pcie.phy = phy; + + phy_provider = devm_of_phy_provider_register(&phy->dev, s32g_serdes_phy_xlate); + if (IS_ERR(phy_provider)) + return PTR_ERR(phy_provider); + + } else { + dev_warn(dev, "Skipping unknown child node %pOFn\n", child_node); + } + + return 0; +} + +static int s32g_serdes_parse_lanes(struct device *dev, struct s32g_serdes *serdes) +{ + int ret; + + for_each_available_child_of_node_scoped(dev->of_node, of_port) { + ret = s32g2_serdes_create_phy(serdes, of_port); + if (ret) + break; + } + + return ret; +} + +static int s32g_serdes_probe(struct platform_device *pdev) +{ + struct s32g_serdes *serdes; + struct device *dev = &pdev->dev; + int ret; + + serdes = devm_kzalloc(dev, sizeof(*serdes), GFP_KERNEL); + if (!serdes) + return -ENOMEM; + + platform_set_drvdata(pdev, serdes); + serdes->dev = dev; + + ret = s32g_serdes_get_ctrl_resources(pdev, serdes); + if (ret) + return ret; + + ret = s32g_serdes_get_pcie_resources(pdev, serdes); + if (ret) + return ret; + + ret = s32g_serdes_parse_lanes(dev, serdes); + if (ret) + return ret; + + ret = s32g_serdes_init(serdes); + + return ret; +} + +static int __maybe_unused s32g_serdes_suspend(struct device *device) +{ + struct s32g_serdes *serdes = dev_get_drvdata(device); + + clk_bulk_disable_unprepare(serdes->ctrl.nclks, serdes->ctrl.clks); + + return 0; +} + +static int __maybe_unused s32g_serdes_resume(struct device *device) +{ + struct s32g_serdes *serdes = dev_get_drvdata(device); + struct s32g_pcie_ctrl *pcie = &serdes->pcie; + int ret; + + ret = s32g_serdes_init(serdes); + if (ret) { + dev_err(device, "Failed to initialize\n"); + return ret; + } + + /* Restore PCIe phy power */ + if (pcie->powered_on) { + ret = s32g_pcie_phy_power_on(serdes); + if (ret) + dev_err(device, "Failed to power-on PCIe phy\n"); + } + + return ret; +} + +static const struct of_device_id s32g_serdes_match[] = { + { + .compatible = "nxp,s32g2-serdes", + }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, s32g_serdes_match); + +static const struct dev_pm_ops s32g_serdes_pm_ops = { + NOIRQ_SYSTEM_SLEEP_PM_OPS(s32g_serdes_suspend, + s32g_serdes_resume) +}; + +static struct platform_driver s32g_serdes_driver = { + .probe = s32g_serdes_probe, + .driver = { + .name = "phy-s32g-serdes", + .of_match_table = s32g_serdes_match, + .pm = &s32g_serdes_pm_ops, + }, +}; +module_platform_driver(s32g_serdes_driver); + +MODULE_AUTHOR("Ghennadi Procopciuc <ghennadi.procopciuc@nxp.com>"); +MODULE_DESCRIPTION("S32CC SerDes driver"); +MODULE_LICENSE("GPL");
--
2.43.0