diff mbox series

[RFC,net-next,15/16] net: pcs: Add Xilinx PCS driver

Message ID 20211004191527.1610759-16-sean.anderson@seco.com (mailing list archive)
State RFC
Delegated to: Netdev Maintainers
Headers show
Series Add support for Xilinx PCS | expand

Commit Message

Sean Anderson Oct. 4, 2021, 7:15 p.m. UTC
This adds support for the Xilinx 1G/2.5G Ethernet PCS/PMA or SGMII
device. This is a soft device which converts between GMII and either
SGMII, 2.5G SGMII, 1000BASE-X, or 2500BASE-X. If configured correctly,
it can also switch between SGMII and 1000BASE-X at runtime. Internally,
these are referred to as the "standard" to match the terminology in the
datasheet. I am not sure how to handle the 2.5G SGMII case yet (since
AIUI Linux doesn't think it exists), and I do not have the hardware to
test it, so I have left out support.

This device has a c22-compliant PHY interface, so for the most part we
can just use the phylink helpers. Where we have to do things manually is
usually just to set the interface. We also set the speed and duplex when
the link comes up. I don't know what the permit_pause_to_mac parameter
is for in pcs_config, so I ignore it...

This device supports an interrupt which is triggered on link status
change. While according to Documentation/networking/sfp-phylink.rst the
mac should call phylink_mac_change on link state change, it is unclear
to me what the PCS should call. For now I have just set the PCS to
poll.

This device supports sharing some logic between different
implementations of the device. In this case, one device has the shared
logic, and several output (clocks, resets, etc.) are connected between
it and the consuming devices. I am not sure how to model this (create a
clock? add a devlink?) so I have left it out, but I would like to
include support for this use-case.

Signed-off-by: Sean Anderson <sean.anderson@seco.com>
---

 MAINTAINERS                  |   6 +
 drivers/net/pcs/Kconfig      |  19 ++
 drivers/net/pcs/Makefile     |   1 +
 drivers/net/pcs/pcs-xilinx.c | 326 +++++++++++++++++++++++++++++++++++
 4 files changed, 352 insertions(+)
 create mode 100644 drivers/net/pcs/pcs-xilinx.c
diff mbox series

Patch

diff --git a/MAINTAINERS b/MAINTAINERS
index 7f4615336fa5..eaee1028a7ba 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -20591,6 +20591,12 @@  F:	Documentation/devicetree/bindings/gpio/gpio-zynq.yaml
 F:	drivers/gpio/gpio-xilinx.c
 F:	drivers/gpio/gpio-zynq.c
 
+XILINX PCS DRIVER
+M:	Sean Anderson <sean.anderso@seco.com>
+S:	Maintained
+F:	Documentation/devicetree/bindings/net/xilinx,pcs.yaml
+F:	drivers/net/pcs/pcs-xilinx.c
+
 XILINX SD-FEC IP CORES
 M:	Derek Kiernan <derek.kiernan@xilinx.com>
 M:	Dragan Cvetic <dragan.cvetic@xilinx.com>
diff --git a/drivers/net/pcs/Kconfig b/drivers/net/pcs/Kconfig
index 22ba7b0b476d..3ee415504eeb 100644
--- a/drivers/net/pcs/Kconfig
+++ b/drivers/net/pcs/Kconfig
@@ -18,4 +18,23 @@  config PCS_LYNX
 	  This module provides helpers to phylink for managing the Lynx PCS
 	  which is part of the Layerscape and QorIQ Ethernet SERDES.
 
+config PCS_XILINX
+	depends on OF
+	depends on GPIOLIB
+	depends on COMMON_CLK
+	depends on PHYLINK
+	tristate "Xilinx PCS driver"
+	help
+	  PCS driver for the Xilinx 1G/2.5G Ethernet PCS/PMA or SGMII device.
+	  This device can either act as a PCS+PMA for 1000BASE-X or 2500BASE-X,
+	  or as a GMII-to-SGMII bridge. It can also switch between 1000BASE-X
+	  and SGMII dynamically if configured correctly when synthesized.
+	  Typical applications use this device on an FPGA connected to a GEM or
+	  TEMAC on the GMII side. The other side is typically connected to
+	  on-device gigabit transceivers, off-device SERDES devices using TBI,
+	  or LVDS IO resources directly.
+
+	  To compile this driver as a module, choose M here: the module
+	  will be called pcs-xilinx.
+
 endmenu
diff --git a/drivers/net/pcs/Makefile b/drivers/net/pcs/Makefile
index 0603d469bd57..4a8580ca4134 100644
--- a/drivers/net/pcs/Makefile
+++ b/drivers/net/pcs/Makefile
@@ -5,3 +5,4 @@  pcs_xpcs-$(CONFIG_PCS_XPCS)	:= pcs-xpcs.o pcs-xpcs-nxp.o
 
 obj-$(CONFIG_PCS_XPCS)		+= pcs_xpcs.o
 obj-$(CONFIG_PCS_LYNX)		+= pcs-lynx.o
+obj-$(CONFIG_PCS_XILINX)	+= pcs-xilinx.o
diff --git a/drivers/net/pcs/pcs-xilinx.c b/drivers/net/pcs/pcs-xilinx.c
new file mode 100644
index 000000000000..297c9881b401
--- /dev/null
+++ b/drivers/net/pcs/pcs-xilinx.c
@@ -0,0 +1,326 @@ 
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright 2021 Sean Anderson <sean.anderson@seco.com>
+ *
+ * This is the driver for the Xilinx 1G/2.5G Ethernet PCS/PMA or SGMII LogiCORE
+ * IP. A typical setup will look something like
+ *
+ * MAC <--GMII--> PCS+PMA <--internal/TBI--> PMD (SERDES) <--SGMII/1000BASE-X
+ *
+ * The link to the PMD is not modeled by this driver, except for refclk. It is
+ * assumed that the SERDES needs no configuration. It is also possible to go
+ * from SGMII to GMII (PHY mode), but this is not supported.
+ *
+ * This driver was written with reference to PG047:
+ * https://www.xilinx.com/support/documentation/ip_documentation/gig_ethernet_pcs_pma/v16_2/pg047-gig-eth-pcs-pma.pdf
+ */
+
+#include <linux/clk.h>
+#include <linux/mdio.h>
+#include <linux/of.h>
+#include <linux/phylink.h>
+#include <linux/reset.h>
+
+/* Vendor-specific MDIO registers */
+#define XILINX_PCS_ANICR 16 /* Auto-Negotiation Interrupt Control Register */
+#define XILINX_PCS_SSR   17 /* Standard Selection Register */
+
+#define XILINX_PCS_ANICR_IE BIT(0) /* Interrupt Enable */
+#define XILINX_PCS_ANICR_IS BIT(1) /* Interrupt Status */
+
+#define XILINX_PCS_SSR_SGMII BIT(0) /* Select SGMII standard */
+
+/**
+ * enum xilinx_pcs_standard - Support for interface standards
+ * @XILINX_PCS_STD_SGMII: SGMII for 10/100/1000BASE-T
+ * @XILINX_PCS_STD_1000BASEX: 1000BASE-X PMD Support Interface
+ * @XILINX_PCS_STD_BOTH: Support for both SGMII and 1000BASE-X
+ * @XILINX_PCS_STD_2500BASEX: 2500BASE-X PMD Support Interface
+ * @XILINX_PCS_STD_2500SGMII: 2.5G SGMII for 2.5GBASE-T
+ */
+enum xilinx_pcs_standard {
+	XILINX_PCS_STD_SGMII,
+	XILINX_PCS_STD_1000BASEX,
+	XILINX_PCS_STD_BOTH,
+	XILINX_PCS_STD_2500BASEX,
+	XILINX_PCS_STD_2500SGMII,
+};
+
+/**
+ * struct xilinx_pcs - Private data for Xilinx PCS devices
+ * @pcs: The phylink PCS
+ * @mdiodev: The mdiodevice used to access the PCS
+ * @refclk: The reference clock for the PMD
+ * @reset: The reset controller for the PCS
+ * @standard: The supported interface standard
+ */
+struct xilinx_pcs {
+	struct phylink_pcs pcs;
+	struct mdio_device *mdiodev;
+	struct clk *refclk;
+	struct reset_control *reset;
+	enum xilinx_pcs_standard standard;
+};
+
+static inline struct xilinx_pcs *pcs_to_xilinx(struct phylink_pcs *pcs)
+{
+	return container_of(pcs, struct xilinx_pcs, pcs);
+}
+
+static void xilinx_pcs_get_state(struct phylink_pcs *pcs,
+				 struct phylink_link_state *state)
+{
+	struct xilinx_pcs *xp = pcs_to_xilinx(pcs);
+
+	switch (xp->standard) {
+	case XILINX_PCS_STD_SGMII:
+		state->interface = PHY_INTERFACE_MODE_SGMII;
+		break;
+	case XILINX_PCS_STD_1000BASEX:
+		state->interface = PHY_INTERFACE_MODE_1000BASEX;
+		break;
+	case XILINX_PCS_STD_BOTH: {
+		int ssr = mdiodev_read(xp->mdiodev, XILINX_PCS_SSR);
+
+		if (ssr < 0) {
+			dev_err(pcs->dev, "could not read SSR (err=%d)\n", ssr);
+			return;
+		}
+
+		if (ssr & XILINX_PCS_SSR_SGMII)
+			state->interface = PHY_INTERFACE_MODE_SGMII;
+		else
+			state->interface = PHY_INTERFACE_MODE_1000BASEX;
+		break;
+	}
+	case XILINX_PCS_STD_2500BASEX:
+		state->interface = PHY_INTERFACE_MODE_2500BASEX;
+		break;
+	default:
+		return;
+	}
+
+	phylink_mii_c22_pcs_get_state(xp->mdiodev, state);
+}
+
+static int xilinx_pcs_config(struct phylink_pcs *pcs, unsigned int mode,
+			     phy_interface_t interface,
+			     const unsigned long *advertising,
+			     bool permit_pause_to_mac)
+{
+	int ret;
+	bool changed = false;
+	struct xilinx_pcs *xp = pcs_to_xilinx(pcs);
+
+	switch (xp->standard) {
+	case XILINX_PCS_STD_SGMII:
+		if (interface != PHY_INTERFACE_MODE_SGMII)
+			return -EOPNOTSUPP;
+		break;
+	case XILINX_PCS_STD_1000BASEX:
+		if (interface != PHY_INTERFACE_MODE_1000BASEX)
+			return -EOPNOTSUPP;
+		break;
+	case XILINX_PCS_STD_BOTH: {
+		u16 ssr;
+
+		if (interface == PHY_INTERFACE_MODE_SGMII)
+			ssr = XILINX_PCS_SSR_SGMII;
+		else if (interface == PHY_INTERFACE_MODE_1000BASEX)
+			ssr = 0;
+		else
+			return -EOPNOTSUPP;
+
+		ret = mdiodev_read(xp->mdiodev, XILINX_PCS_SSR);
+		if (ret < 0)
+			return ret;
+
+		if (ret == ssr)
+			break;
+
+		changed = true;
+		ret = mdiodev_write(xp->mdiodev, XILINX_PCS_SSR, ssr);
+		if (ret)
+			return ret;
+		break;
+	}
+	case XILINX_PCS_STD_2500BASEX:
+		if (interface != PHY_INTERFACE_MODE_2500BASEX)
+			return -EOPNOTSUPP;
+		break;
+	default:
+		return -EOPNOTSUPP;
+	}
+
+	ret = phylink_mii_c22_pcs_config(xp->mdiodev, mode, interface,
+					 advertising);
+	if (ret)
+		return ret;
+	return changed;
+}
+
+static void xilinx_pcs_an_restart(struct phylink_pcs *pcs)
+{
+	struct xilinx_pcs *xp = pcs_to_xilinx(pcs);
+
+	phylink_mii_c22_pcs_an_restart(xp->mdiodev);
+}
+
+static void xilinx_pcs_link_up(struct phylink_pcs *pcs, unsigned int mode,
+			       phy_interface_t interface, int speed, int duplex)
+{
+	int bmcr;
+	struct xilinx_pcs *xp = pcs_to_xilinx(pcs);
+
+	if (phylink_autoneg_inband(mode))
+		return;
+
+	bmcr = mdiodev_read(xp->mdiodev, MII_BMCR);
+	if (bmcr < 0) {
+		dev_err(pcs->dev, "could not read BMCR (err=%d)\n", bmcr);
+		return;
+	}
+
+	bmcr &= ~BMCR_FULLDPLX;
+	if (duplex == DUPLEX_FULL)
+		bmcr |= BMCR_FULLDPLX;
+	else if (duplex != DUPLEX_HALF)
+		dev_err(pcs->dev, "unknown duplex %d\n", duplex);
+
+	bmcr &= ~(BMCR_SPEED1000 | BMCR_SPEED100);
+	switch (speed) {
+	case SPEED_2500:
+	case SPEED_1000:
+		bmcr |= BMCR_SPEED1000;
+		break;
+	case SPEED_100:
+		bmcr |= BMCR_SPEED100;
+		break;
+	case SPEED_10:
+		bmcr |= BMCR_SPEED10;
+		break;
+	default:
+		dev_err(pcs->dev, "invalid speed %d\n", speed);
+	}
+
+	bmcr = mdiodev_write(xp->mdiodev, MII_BMCR, bmcr);
+	if (bmcr < 0)
+		dev_err(pcs->dev, "could not write BMCR (err=%d)\n", bmcr);
+}
+
+static const struct phylink_pcs_ops xilinx_pcs_ops = {
+	.pcs_get_state = xilinx_pcs_get_state,
+	.pcs_config = xilinx_pcs_config,
+	.pcs_an_restart = xilinx_pcs_an_restart,
+	.pcs_link_up = xilinx_pcs_link_up,
+};
+
+static int xilinx_pcs_probe(struct mdio_device *mdiodev)
+{
+	const char *standard;
+	int ret;
+	struct xilinx_pcs *xp;
+	struct device *dev = &mdiodev->dev;
+	struct device_node *np = dev->of_node;
+	u32 phy_id;
+
+	xp = devm_kzalloc(dev, sizeof(*xp), GFP_KERNEL);
+	if (!xp)
+		return -ENOMEM;
+	xp->mdiodev = mdiodev;
+	dev_set_drvdata(dev, xp);
+
+	ret = of_property_read_string(np, "standard", &standard);
+	if (ret)
+		return dev_err_probe(dev, ret, "could not read standard\n");
+
+	if (!strncmp(standard, "1000base-x", 10))
+		xp->standard = XILINX_PCS_STD_1000BASEX;
+	else if (!strncmp(standard, "sgmii/1000base-x", 16))
+		xp->standard = XILINX_PCS_STD_BOTH;
+	else if (!strncmp(standard, "sgmii", 5))
+		xp->standard = XILINX_PCS_STD_SGMII;
+	else if (!strncmp(standard, "2500base-x", 10))
+		xp->standard = XILINX_PCS_STD_2500BASEX;
+	/* TODO: 2.5G SGMII support */
+	else
+		return dev_err_probe(dev, -EINVAL,
+				     "unknown/unsupported standard %s\n",
+				     standard);
+
+	xp->refclk = devm_clk_get(dev, "refclk");
+	if (IS_ERR(xp->refclk))
+		return dev_err_probe(dev, PTR_ERR(xp->refclk),
+				     "could not get reference clock\n");
+
+	xp->reset = devm_reset_control_get_exclusive(dev, "pcs");
+	if (IS_ERR(xp->reset))
+		return dev_err_probe(dev, PTR_ERR(xp->reset),
+				     "could not get reset\n");
+
+	ret = reset_control_assert(xp->reset);
+	if (ret)
+		return dev_err_probe(dev, ret, "could not enter reset\n");
+
+	ret = clk_prepare_enable(xp->refclk);
+	if (ret)
+		return dev_err_probe(dev, ret,
+				     "could not enable reference clock\n");
+
+	ret = reset_control_deassert(xp->reset);
+	if (ret) {
+		dev_err_probe(dev, ret, "could not exit reset\n");
+		goto err_unprepare;
+	}
+
+	/* Sanity check */
+	ret = get_phy_c22_id(mdiodev->bus, mdiodev->addr, &phy_id);
+	if (ret) {
+		dev_err_probe(dev, ret, "could not read id\n");
+		goto err_unprepare;
+	}
+	if ((phy_id & 0xfffffff0) != 0x01740c00)
+		dev_warn(dev, "unknown phy id %x\n", phy_id);
+
+	xp->pcs.dev = dev;
+	xp->pcs.ops = &xilinx_pcs_ops;
+	xp->pcs.poll = true;
+	ret = phylink_register_pcs(&xp->pcs);
+	if (ret)
+		return dev_err_probe(dev, ret, "could not register PCS\n");
+	dev_info(dev, "probed (standard=%s)\n", standard);
+	return 0;
+
+err_unprepare:
+	clk_disable_unprepare(xp->refclk);
+	return ret;
+}
+
+static void xilinx_pcs_remove(struct mdio_device *mdiodev)
+{
+	struct xilinx_pcs *xp = dev_get_drvdata(&mdiodev->dev);
+
+	phylink_unregister_pcs(&xp->pcs);
+	reset_control_assert(xp->reset);
+	clk_disable_unprepare(xp->refclk);
+}
+
+static const struct of_device_id xilinx_pcs_of_match[] = {
+	{ .compatible = "xlnx,pcs-16.2", },
+	{},
+};
+MODULE_DEVICE_TABLE(of, xilinx_timer_of_match);
+
+static struct mdio_driver xilinx_pcs_driver = {
+	.probe = xilinx_pcs_probe,
+	.remove = xilinx_pcs_remove,
+	.mdiodrv.driver = {
+		.name = "xilinx-pcs",
+		.of_match_table = of_match_ptr(xilinx_pcs_of_match),
+	},
+};
+mdio_module_driver(xilinx_pcs_driver);
+
+MODULE_ALIAS("platform:xilinx-pcs");
+MODULE_DESCRIPTION("Xilinx PCS driver");
+MODULE_LICENSE("GPL v2");