diff mbox series

[net-next,4/5] net: ipqess: add a PSGMII calibration procedure to the IPQESS driver

Message ID 20231023155013.512999-5-romain.gantois@bootlin.com (mailing list archive)
State New, archived
Headers show
Series net: ipqess: introduce Qualcomm IPQESS driver | expand

Commit Message

Romain Gantois Oct. 23, 2023, 3:50 p.m. UTC
The IPQ4019 Ethernet Switch Subsystem uses a PSGMII link to communicate
with a QCA8075 5-port PHY. This 1G link requires calibration before it can
be used reliably.

This commit introduces a calibration procedure followed by thourough
testing of the link between each switch port and its corresponding PHY
port.

Signed-off-by: Romain Gantois <romain.gantois@bootlin.com>
---
 drivers/net/ethernet/qualcomm/ipqess/Makefile |   2 +-
 .../ethernet/qualcomm/ipqess/ipqess_calib.c   | 495 ++++++++++++++++++
 .../ethernet/qualcomm/ipqess/ipqess_port.c    |   3 +-
 .../ethernet/qualcomm/ipqess/ipqess_port.h    |   4 +
 4 files changed, 502 insertions(+), 2 deletions(-)
 create mode 100644 drivers/net/ethernet/qualcomm/ipqess/ipqess_calib.c

Comments

Andrew Lunn Oct. 23, 2023, 6:01 p.m. UTC | #1
On Mon, Oct 23, 2023 at 05:50:11PM +0200, Romain Gantois wrote:
> The IPQ4019 Ethernet Switch Subsystem uses a PSGMII link to communicate
> with a QCA8075 5-port PHY. This 1G link requires calibration before it can
> be used reliably.
> 
> This commit introduces a calibration procedure followed by thourough
> testing of the link between each switch port and its corresponding PHY
> port.

Could you explain the architecture in a bit more detail.

When i see MAC code messing with a PHY, i normally say move it into
the PHY driver. But its not clear to me if you are talking about the
real PHYs here, or this is the switch end of the link, and it has some
sort of a PHY to talk to the quint PHY?

     Andrew
Romain Gantois Oct. 24, 2023, 9:03 a.m. UTC | #2
Hello Andrew,

On Mon, 23 Oct 2023, Andrew Lunn wrote:

> On Mon, Oct 23, 2023 at 05:50:11PM +0200, Romain Gantois wrote:
> > The IPQ4019 Ethernet Switch Subsystem uses a PSGMII link to communicate
> > with a QCA8075 5-port PHY. This 1G link requires calibration before it can
> > be used reliably.
> > 
> > This commit introduces a calibration procedure followed by thourough
> > testing of the link between each switch port and its corresponding PHY
> > port.
> 
> Could you explain the architecture in a bit more detail.
> 
> When i see MAC code messing with a PHY, i normally say move it into
> the PHY driver. But its not clear to me if you are talking about the
> real PHYs here, or this is the switch end of the link, and it has some
> sort of a PHY to talk to the quint PHY?

Yes, I'll add more detailed comments to the code in the v2. The calibration 
procedure itself targets the PSGMII device, which is internal to the SoC and can 
be logically accessed as a PHY device on the MDIO bus. This component is a 
little opaque and has some nonstandard MII register definitions.

The "testing" phase that follows the calibration accesses both the internal 
QCA8K switch ports and the external QCA8075 PHY. For example, it puts both the 
switch ports and the PHY ports in loopback before starting packet generation on 
the external PHYs. This is done to verify that the PSGMII link works correctly 
after being calibrated.

So this code interacts with both internal ESS devices and external PHYs, but 
mostly the former, which is why I kept everything in the MAC/switch driver.

Thanks,

Romain
Andrew Lunn Oct. 24, 2023, 2:13 p.m. UTC | #3
> Yes, I'll add more detailed comments to the code in the v2. The calibration 
> procedure itself targets the PSGMII device, which is internal to the SoC and can 
> be logically accessed as a PHY device on the MDIO bus. This component is a 
> little opaque and has some nonstandard MII register definitions.
> 
> The "testing" phase that follows the calibration accesses both the internal 
> QCA8K switch ports and the external QCA8075 PHY. For example, it puts both the 
> switch ports and the PHY ports in loopback before starting packet generation on 
> the external PHYs. This is done to verify that the PSGMII link works correctly 
> after being calibrated.
> 
> So this code interacts with both internal ESS devices and external PHYs, but 
> mostly the former, which is why I kept everything in the MAC/switch driver.

Accessing the external PHYs i would suggest go over the normal phylib
API. Somebody might build a board using a different PHY, with
different registers. If all you need is loopback, there is a phylib
call for that.

Directly accessing the internal ESS is fine, it cannot be changed, but
if there are phylib helpers which do the same thing, consider using
them.

     Andrew
Robert Marko Oct. 24, 2023, 4:49 p.m. UTC | #4
On Tue, Oct 24, 2023 at 4:13 PM Andrew Lunn <andrew@lunn.ch> wrote:
>
> > Yes, I'll add more detailed comments to the code in the v2. The calibration
> > procedure itself targets the PSGMII device, which is internal to the SoC and can
> > be logically accessed as a PHY device on the MDIO bus. This component is a
> > little opaque and has some nonstandard MII register definitions.
> >
> > The "testing" phase that follows the calibration accesses both the internal
> > QCA8K switch ports and the external QCA8075 PHY. For example, it puts both the
> > switch ports and the PHY ports in loopback before starting packet generation on
> > the external PHYs. This is done to verify that the PSGMII link works correctly
> > after being calibrated.
> >
> > So this code interacts with both internal ESS devices and external PHYs, but
> > mostly the former, which is why I kept everything in the MAC/switch driver.
>
> Accessing the external PHYs i would suggest go over the normal phylib
> API. Somebody might build a board using a different PHY, with
> different registers. If all you need is loopback, there is a phylib
> call for that.
>
> Directly accessing the internal ESS is fine, it cannot be changed, but
> if there are phylib helpers which do the same thing, consider using
> them.

Hi,
This SoC is a bit special as it only supports using RGMII and PSGMII.
PSGMII is Qualcomm-s modification of SGMII with 5 SGMII lines to provide
1G of bandwidth from the switch to PHY-s.

However, PSGMII is also weird in the sense that it requires
calibration to be carried
on each boot, as otherwise packet loss will start to occur.
But for calibration to work, you must enable loopback on the switch ports and
on the PHY-s, both loopback and CRC verification must be enabled.
Then you can actually enable the PSGMII serdes calibration in the SoC but it
must occur on all of the PHYs at the same time, hence why broadcast is used.

As far as the PHY-s go, there are only 2 PHY models supporting PSGMII,
QCA8072 and QCA8075, both from Qualcomm, and differing only in the
number of ports.
QCA8072 has 2 ports while QCA8075 has 5 ports.
Each of them also has a serdes PHY exposed over MDIO.
These PHY-s are still being used in IPQ8074 and IPQ6018 802.11ax SoC-s.

So in a nutshell, this is how stuff is connected (To the best of my knowledge):
https://asciiflow.com/#/share/eJzVVktuwjAQvYrlFUggwk9p2fGHBVVoVAmkbAJxRSTjoGDUIMQtKg5T9TScpG4T8iEOARLaYs3iZTx%2B9oxnMt5Aos4RrJEVxjmI1TUyYQ1uFGgpsPZYKeUUuGaoJIoMUWRR9qFA4I79%2B%2BeloigksB606QyZBFFXE7JoGoSaBsbIjLIIDp5Fpi8N27KcjbaI5UjkKY%2FT0fUJZQFQMW%2Bu2J3olDfRYTcGWqsFRta5%2B8TqAufffVzh8q0l8tI8fBodLwoRSoZJgeDDRYZ%2BQAkcUNmbrXiwai8aGJr%2BqiMt9aOCxkrHNK%2BTcG43pZfsYUGxG4scZg8PVyqeGvN5%2BmceNusP5bL4lLSWflmA%2FKbT6SxUMbzoxEfkH9NcEJXdjfURUzH%2FaQAkuTvo94HUGxfk9nOrLXPDE0tTtWye6UwlBOElP8qxNJk6XhqAcWgYLcHzN2X2CpozRoKr%2FCPhJHCk1rlWd%2FJ8chtflnj3ILZT3LTZX%2FN7PoqWW0j%2BNrh3Ol7RRSe6IBuHRhgklpGpsWLwt519fKvyQ9BbT0xdC524N3bdahqLBXsgMnga%2BQw5WefrhOkEmzkxtTc0TLcjCmKpUE1vC0EQGuoS5UeFoxc1i0%2F6u3RGgTtITNzRJ%2FbD3tHwi%2FfORYFbuP0C5MQClA%3D%3D)

Sorry for the external link, but I cannot get the ASCII diagram to
show properly via plain-text.

Regards,
Robert
>      Andrew
diff mbox series

Patch

diff --git a/drivers/net/ethernet/qualcomm/ipqess/Makefile b/drivers/net/ethernet/qualcomm/ipqess/Makefile
index 51d7163ef0fc..110f6003f04b 100644
--- a/drivers/net/ethernet/qualcomm/ipqess/Makefile
+++ b/drivers/net/ethernet/qualcomm/ipqess/Makefile
@@ -5,4 +5,4 @@ 
 
 obj-$(CONFIG_QCOM_IPQ4019_ESS) += ipqess.o
 
-ipqess-objs := ipqess_port.o ipqess_switch.o ipqess_notifiers.o ipqess_edma.o
+ipqess-objs := ipqess_port.o ipqess_switch.o ipqess_notifiers.o ipqess_edma.o ipqess_calib.o
diff --git a/drivers/net/ethernet/qualcomm/ipqess/ipqess_calib.c b/drivers/net/ethernet/qualcomm/ipqess/ipqess_calib.c
new file mode 100644
index 000000000000..ca9b5593a200
--- /dev/null
+++ b/drivers/net/ethernet/qualcomm/ipqess/ipqess_calib.c
@@ -0,0 +1,495 @@ 
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Calibration procedure for the IPQ4019 PSGMII link
+ *
+ * Copyright (C) 2009 Felix Fietkau <nbd@nbd.name>
+ * Copyright (C) 2011-2012, 2020-2021 Gabor Juhos <juhosg@openwrt.org>
+ * Copyright (c) 2015, 2019, The Linux Foundation. All rights reserved.
+ * Copyright (c) 2016 John Crispin <john@phrozen.org>
+ * Copyright (c) 2022 Robert Marko <robert.marko@sartura.hr>
+ * Copyright (c) 2023 Romain Gantois <romain.gantois@bootlin.com>
+ */
+
+#include <linux/dsa/qca8k.h>
+#include <linux/phylink.h>
+#include <linux/of_net.h>
+#include <linux/of_mdio.h>
+#include <linux/regmap.h>
+
+#include "ipqess_port.h"
+
+/* Nonstandard MII registers for the psgmii
+ * device on the IPQ4019 MDIO bus.
+ */
+
+#define PSGMII_RSTCTRL      0x0     /* Reset control register */
+#define PSGMII_RSTCTRL_RST  BIT(6)
+#define PSGMII_RSTCTRL_RX20 BIT(2)  /* Fix/release RX 20 bit */
+
+#define PSGMII_CDRCTRL         0x1a /* Clock and data recovery control register */
+#define PSGMII_CDRCTRL_RELEASE BIT(12)
+
+#define PSGMII_VCO_CALIB_CTRL  0x28 /* VCO PLL calibration */
+#define PSGMII_VCO_CALIB_READY BIT(0)
+
+/* Delays and timeouts */
+
+#define PSGMII_WAIT_AFTER_CALIB     50
+#define PSGMII_WAIT_AFTER_RELEASE   200
+#define PSGMII_VCO_CALIB_INTERVAL   1000000
+#define PSGMII_VCO_CALIB_TIMEOUT    10000
+#define PSGMII_CALIB_RETRIES        50
+#define PSGMII_CALIB_RETRIES_BURST  5
+#define PSGMII_CALIB_RETRY_DELAY    100
+
+/* Calibration data */
+
+struct psgmii_port_data {
+	struct list_head list;
+	struct phy_device *phy;
+	int id;
+
+	/* calibration test results */
+	u32 test_ok;
+	u32 tx_loss;
+	u32 rx_loss;
+	u32 tx_errors;
+	u32 rx_errors;
+};
+
+static LIST_HEAD(calib);
+
+static int psgmii_vco_calibrate(struct qca8k_priv *priv)
+{
+	int val, ret;
+
+	if (!priv->psgmii_ethphy) {
+		dev_err(priv->dev,
+			"PSGMII eth PHY missing, calibration failed!\n");
+		return -ENODEV;
+	}
+
+	/* Fix PSGMII RX 20bit */
+	ret = phy_clear_bits(priv->psgmii_ethphy, PSGMII_RSTCTRL,
+			     PSGMII_RSTCTRL_RX20);
+	/* Reset PHY PSGMII */
+	ret = phy_clear_bits(priv->psgmii_ethphy, PSGMII_RSTCTRL,
+			     PSGMII_RSTCTRL_RST);
+	/* Release PHY PSGMII reset */
+	ret = phy_set_bits(priv->psgmii_ethphy, PSGMII_RSTCTRL,
+			   PSGMII_RSTCTRL_RST);
+
+	/* Poll for VCO PLL calibration finish - Malibu(QCA8075) */
+	ret = phy_read_mmd_poll_timeout(priv->psgmii_ethphy,
+					MDIO_MMD_PMAPMD,
+					PSGMII_VCO_CALIB_CTRL,
+					val,
+					val & PSGMII_VCO_CALIB_READY,
+					PSGMII_VCO_CALIB_INTERVAL,
+					PSGMII_VCO_CALIB_TIMEOUT,
+					false);
+	if (ret) {
+		dev_err(priv->dev,
+			"QCA807x PSGMII VCO calibration PLL not ready\n");
+		return ret;
+	}
+	mdelay(PSGMII_WAIT_AFTER_CALIB);
+
+	/* Freeze PSGMII RX CDR */
+	ret = phy_clear_bits(priv->psgmii_ethphy, PSGMII_CDRCTRL,
+			     PSGMII_CDRCTRL_RELEASE);
+
+	/* Start PSGMIIPHY VCO PLL calibration */
+	ret = regmap_set_bits(priv->psgmii,
+			      PSGMIIPHY_VCO_CALIBRATION_CONTROL_REGISTER_1,
+			      PSGMIIPHY_REG_PLL_VCO_CALIB_RESTART);
+
+	/* Poll for PSGMIIPHY PLL calibration finish - Dakota(IPQ40xx) */
+	ret = regmap_read_poll_timeout(priv->psgmii,
+				       PSGMIIPHY_VCO_CALIBRATION_CONTROL_REGISTER_2,
+				       val,
+				       val & PSGMIIPHY_REG_PLL_VCO_CALIB_READY,
+				       PSGMII_VCO_CALIB_INTERVAL,
+				       PSGMII_VCO_CALIB_TIMEOUT);
+	if (ret) {
+		dev_err(priv->dev,
+			"IPQ PSGMIIPHY VCO calibration PLL not ready\n");
+		return ret;
+	}
+	mdelay(PSGMII_WAIT_AFTER_CALIB);
+
+	/* Release PSGMII RX CDR */
+	ret = phy_set_bits(priv->psgmii_ethphy, PSGMII_CDRCTRL,
+			   PSGMII_CDRCTRL_RELEASE);
+	/* Release PSGMII RX 20bit */
+	ret = phy_set_bits(priv->psgmii_ethphy, PSGMII_RSTCTRL,
+			   PSGMII_RSTCTRL_RX20);
+	mdelay(PSGMII_WAIT_AFTER_RELEASE);
+
+	return ret;
+}
+
+static int
+qca8k_wait_for_phy_link_state(struct phy_device *phy, int need_link)
+{
+	u16 status;
+	int ret;
+
+	ret = phy_read_poll_timeout(phy, MII_QCA8075_SSTATUS, status,
+				    !!(status & QCA8075_PHY_SPEC_STATUS_LINK) == need_link,
+				    MII_QCA8075_SSTATUS_WAIT, MII_QCA8075_SSTATUS_TIMEOUT, 1);
+	if (ret == -ETIMEDOUT)
+		return -EINVAL;
+
+	return 0;
+}
+
+static void
+psgmii_phy_loopback_enable(struct qca8k_priv *priv, struct phy_device *phy,
+			   int sw_port)
+{
+	phy_write(phy, MII_BMCR, BMCR_ANENABLE | BMCR_RESET);
+	phy_modify(phy, MII_BMCR, BMCR_PDOWN, BMCR_PDOWN);
+	qca8k_wait_for_phy_link_state(phy, 0);
+	qca8k_write(priv, QCA8K_REG_PORT_STATUS(sw_port), 0);
+	phy_write(phy, MII_BMCR,
+		  BMCR_SPEED1000
+		  | BMCR_FULLDPLX
+		  | BMCR_LOOPBACK);
+	qca8k_wait_for_phy_link_state(phy, 1);
+	qca8k_write(priv, QCA8K_REG_PORT_STATUS(sw_port),
+		    QCA8K_PORT_STATUS_SPEED_1000
+		    | QCA8K_PORT_STATUS_TXMAC
+		    | QCA8K_PORT_STATUS_RXMAC
+		    | QCA8K_PORT_STATUS_DUPLEX);
+	qca8k_rmw(priv, QCA8K_PORT_LOOKUP_CTRL(sw_port),
+		  QCA8K_PORT_LOOKUP_STATE_FORWARD,
+		  QCA8K_PORT_LOOKUP_STATE_FORWARD);
+}
+
+static void
+psgmii_phy_loopback_disable(struct qca8k_priv *priv, struct phy_device *phy,
+			    int sw_port)
+{
+	qca8k_write(priv, QCA8K_REG_PORT_STATUS(sw_port), 0);
+	qca8k_rmw(priv, QCA8K_PORT_LOOKUP_CTRL(sw_port),
+		  QCA8K_PORT_LOOKUP_STATE_DISABLED,
+		  QCA8K_PORT_LOOKUP_STATE_DISABLED);
+	phy_write(phy, MII_BMCR,
+		  BMCR_SPEED1000 | BMCR_ANENABLE | BMCR_RESET);
+	/* turn off the power of the phys - so that unused
+	 * ports do not raise links
+	 */
+	phy_modify(phy, MII_BMCR, BMCR_PDOWN, BMCR_PDOWN);
+}
+
+static void
+qca8k_wait_for_phy_pkt_gen_fin(struct qca8k_priv *priv, struct phy_device *phy)
+{
+	int val;
+
+	/* Wait for all traffic to end:
+	 * 4096(pkt num)*1524(size)*8ns(125MHz)=49938us
+	 */
+	phy_read_mmd_poll_timeout(phy, MDIO_MMD_AN, QCA8075_MMD7_PKT_GEN_CTRL,
+				  val, !(val & QCA8075_MMD7_PKT_GEN_INPROGR),
+				  50000, 1000000, true);
+}
+
+static int
+psgmii_start_parallel_pkt_gen(struct qca8k_priv *priv)
+{
+	struct phy_device *phy;
+
+	phy = phy_device_create(priv->bus, QCA8075_MDIO_BRDCST_PHY_ADDR,
+				0, 0, NULL);
+	if (!phy) {
+		dev_err(priv->dev,
+			"unable to create mdio broadcast PHY(0x%x)\n",
+			QCA8075_MDIO_BRDCST_PHY_ADDR);
+		return -ENODEV;
+	}
+
+	/* start packet generation */
+	phy_write_mmd(phy, MDIO_MMD_AN, QCA8075_MMD7_PKT_GEN_CTRL,
+		      QCA8075_MMD7_PKT_GEN_START | QCA8075_MMD7_PKT_GEN_INPROGR);
+
+	phy_device_free(phy);
+	return 0;
+}
+
+static void
+qca8k_get_phy_pkt_gen_test_result(struct psgmii_port_data *port_data)
+{
+	struct phy_device *phy = port_data->phy;
+	u32 tx_all_ok, rx_all_ok;
+	u32 tx_ok, tx_errors;
+	u32 rx_ok, rx_errors;
+	u32 tx_ok_high16;
+	u32 rx_ok_high16;
+
+	/* check counters */
+	tx_ok = phy_read_mmd(phy, MDIO_MMD_AN, QCA8075_MMD7_EG_FRAME_RECV_CNT_LO);
+	tx_ok_high16 = phy_read_mmd(phy, MDIO_MMD_AN,
+				    QCA8075_MMD7_EG_FRAME_RECV_CNT_HI);
+	tx_errors = phy_read_mmd(phy, MDIO_MMD_AN, QCA8075_MMD7_EG_FRAME_ERR_CNT);
+	rx_ok = phy_read_mmd(phy, MDIO_MMD_AN, QCA8075_MMD7_IG_FRAME_RECV_CNT_LO);
+	rx_ok_high16 = phy_read_mmd(phy, MDIO_MMD_AN,
+				    QCA8075_MMD7_IG_FRAME_RECV_CNT_HI);
+	rx_errors = phy_read_mmd(phy, MDIO_MMD_AN, QCA8075_MMD7_IG_FRAME_ERR_CNT);
+	tx_all_ok = tx_ok + (tx_ok_high16 << 16);
+	rx_all_ok = rx_ok + (rx_ok_high16 << 16);
+
+	port_data->tx_loss = QCA8075_PKT_GEN_PKTS_COUNT - tx_all_ok;
+	port_data->rx_loss = QCA8075_PKT_GEN_PKTS_COUNT - rx_all_ok;
+	port_data->tx_errors = tx_errors;
+	port_data->rx_errors = rx_errors;
+	port_data->test_ok = !(port_data->tx_loss | port_data->rx_loss | tx_errors | rx_errors);
+}
+
+static void psgmii_port_cleanup_test(struct qca8k_priv *priv,
+				     struct psgmii_port_data *port_data)
+{
+	struct phy_device *phy = port_data->phy;
+	int port_id = port_data->id;
+
+	/* set packet count to 0 */
+	phy_write_mmd(phy, MDIO_MMD_AN, QCA8075_MMD7_PKT_GEN_PKT_NUMB, 0);
+
+	/* disable CRC checker and packet counter */
+	phy_write_mmd(phy, MDIO_MMD_AN, QCA8075_MMD7_CRC_AND_PKTS_COUNT, 0);
+
+	/* disable traffic gen */
+	phy_write_mmd(phy, MDIO_MMD_AN, QCA8075_MMD7_PKT_GEN_CTRL, 0);
+
+	/* disable broadcasts on MDIO bus */
+	phy_clear_bits_mmd(phy, MDIO_MMD_AN, QCA8075_MMD7_MDIO_BRDCST_WRITE,
+			   QCA8075_MMD7_MDIO_BRDCST_WRITE_EN);
+
+	/* disable loopback on switch port and PHY */
+	qca8k_clear_bits(priv, QCA8K_PORT_LOOKUP_CTRL(port_id),
+			 QCA8K_PORT_LOOKUP_LOOPBACK_EN);
+	psgmii_phy_loopback_disable(priv, phy, port_id);
+}
+
+static void psgmii_port_prep_test(struct qca8k_priv *priv,
+				  struct psgmii_port_data *port_data)
+{
+	struct phy_device *phy = port_data->phy;
+	int port_id = port_data->id;
+
+	/* put PHY and switch port in loopback */
+	psgmii_phy_loopback_enable(priv, phy, port_id);
+	qca8k_set_bits(priv, QCA8K_PORT_LOOKUP_CTRL(port_id),
+		       QCA8K_PORT_LOOKUP_LOOPBACK_EN);
+
+	/* enable broadcasts on MDIO bus */
+	phy_set_bits_mmd(phy, MDIO_MMD_AN, QCA8075_MMD7_MDIO_BRDCST_WRITE,
+			 QCA8075_MMD7_MDIO_BRDCST_WRITE_EN);
+
+	/* enable PHY CRC checker and packet counters */
+	phy_write_mmd(phy, MDIO_MMD_AN, QCA8075_MMD7_CRC_AND_PKTS_COUNT,
+		      QCA8075_MMD7_CNT_FRAME_CHK_EN | QCA8075_MMD7_CNT_SELFCLR);
+	qca8k_wait_for_phy_link_state(phy, 1);
+
+	/* set number of packets to send during the test */
+	phy_write_mmd(phy, MDIO_MMD_AN, QCA8075_MMD7_PKT_GEN_PKT_NUMB,
+		      QCA8075_PKT_GEN_PKTS_COUNT);
+	/* set packet size */
+	phy_write_mmd(phy, MDIO_MMD_AN, QCA8075_MMD7_PKT_GEN_PKT_SIZE,
+		      QCA8075_PKT_GEN_PKTS_SIZE);
+}
+
+static int psgmii_link_parallel_test(struct qca8k_priv *priv)
+{
+	struct psgmii_port_data *port_data;
+	bool test_failed = false;
+
+	list_for_each_entry(port_data, &calib, list) {
+		/* prep switch port for test */
+		psgmii_port_prep_test(priv, port_data);
+	}
+
+	psgmii_start_parallel_pkt_gen(priv);
+
+	list_for_each_entry(port_data, &calib, list) {
+		/* wait for test results */
+		qca8k_wait_for_phy_pkt_gen_fin(priv, port_data->phy);
+		qca8k_get_phy_pkt_gen_test_result(port_data);
+
+		if (!port_data->test_ok) {
+			dev_dbg(priv->dev,
+				"PSGMII calibration: failed parallel test on port %d errors: %d %d %d %d\n",
+				port_data->id, port_data->tx_loss, port_data->rx_loss,
+				port_data->tx_errors, port_data->rx_errors);
+
+			test_failed = true;
+		}
+
+		psgmii_port_cleanup_test(priv, port_data);
+	}
+
+	return test_failed;
+}
+
+static int psgmii_link_serial_test(struct qca8k_priv *priv)
+{
+	struct psgmii_port_data *port_data;
+	bool test_failed = false;
+
+	list_for_each_entry(port_data, &calib, list) {
+		/* prep switch port for test */
+		psgmii_port_prep_test(priv, port_data);
+
+		/* start packet generation */
+		phy_write_mmd(port_data->phy,
+			      MDIO_MMD_AN, QCA8075_MMD7_PKT_GEN_CTRL,
+			      QCA8075_MMD7_PKT_GEN_START |
+			      QCA8075_MMD7_PKT_GEN_INPROGR);
+
+		/* wait for test results */
+		qca8k_wait_for_phy_pkt_gen_fin(priv, port_data->phy);
+		qca8k_get_phy_pkt_gen_test_result(port_data);
+
+		if (!port_data->test_ok) {
+			dev_dbg(priv->dev,
+				"PSGMII calibration: failed serial test on port %d errors: %d %d %d %d\n",
+				port_data->id, port_data->tx_loss, port_data->rx_loss,
+				port_data->tx_errors, port_data->rx_errors);
+
+			test_failed = true;
+		}
+
+		psgmii_port_cleanup_test(priv, port_data);
+	}
+
+	return test_failed;
+}
+
+static void psgmii_free_calib_data(void)
+{
+	struct psgmii_port_data *port_data, *temp;
+
+	list_for_each_entry_safe(port_data, temp, &calib, list) {
+		list_del(&port_data->list);
+		kfree(port_data);
+	}
+}
+
+static int psgmii_alloc_calib_data(struct qca8k_priv *priv)
+{
+	struct device_node *phy_dn, *ports, *port_dn;
+	struct psgmii_port_data *port_data;
+	struct phy_device *phy;
+	int err, port_id;
+
+	/* get port data from device tree */
+	ports = of_get_child_by_name(priv->dev->of_node, "ports");
+	if (!ports) {
+		dev_err(priv->dev, "no ports child node found\n");
+		return -EINVAL;
+	}
+	for_each_available_child_of_node(ports, port_dn) {
+		/* alloc port data */
+		port_data = kzalloc(sizeof(port_data), GFP_KERNEL);
+		if (!port_data) {
+			err = -ENOMEM;
+			goto out_free;
+		}
+
+		list_add(&port_data->list, &calib);
+
+		/* get port ID */
+		err = of_property_read_u32(port_dn, "reg", &port_id);
+		if (err) {
+			dev_err(priv->dev, "error: missing 'reg' property in device node\n");
+			goto out_free;
+		}
+
+		if (port_id >= QCA8K_NUM_PORTS) {
+			dev_err(priv->dev, "error: port ID out of range\n");
+			err = -EINVAL;
+			goto out_free;
+		}
+
+		/* get PHY device */
+		phy_dn = of_parse_phandle(port_dn, "phy-handle", 0);
+		if (!phy_dn) {
+			dev_err(priv->dev, "error: missing 'phy-handle' property in device node\n");
+			err = -EINVAL;
+			goto out_free;
+		}
+		phy = of_phy_find_device(phy_dn);
+		of_node_put(phy_dn);
+		if (!phy) {
+			dev_err(priv->dev,
+				"error: unable to fetch PHY device for port %d\n",
+				port_id);
+			err = -EINVAL;
+			goto out_free;
+		}
+
+		port_data->phy = phy;
+		port_data->id = port_id;
+	}
+
+	return 0;
+
+out_free:
+	psgmii_free_calib_data();
+	return err;
+}
+
+int psgmii_calibrate_and_test(struct qca8k_priv *priv)
+{
+	struct psgmii_port_data *port_data;
+	bool test_failed = false;
+	int ret, attempt;
+
+	ret = psgmii_alloc_calib_data(priv);
+	if (ret)
+		return ret;
+
+	for (attempt = 0; attempt <= PSGMII_CALIB_RETRIES; attempt++) {
+		/* first we run the VCO calibration */
+		ret = psgmii_vco_calibrate(priv);
+		if (ret)
+			goto out_free;
+
+		/* then, we test the link */
+		test_failed = psgmii_link_serial_test(priv);
+		if (!test_failed)
+			test_failed = psgmii_link_parallel_test(priv);
+
+		qca8k_fdb_flush(priv);
+
+		if (!test_failed) {
+			dev_dbg(priv->dev,
+				"PSGMII link stabilized after %d attempts\n",
+				attempt + 1);
+			ret = 0;
+			goto out_free;
+		}
+
+		/* On tested hardware, the link often stabilizes in 4 or 5 retries.
+		 * If it still isn't stable, we wait a bit, then try another set
+		 * of calibration attempts.
+		 */
+		dev_warn(priv->dev, "PSGMII link is unstable! Retrying... %d/QCA8K_PSGMII_CALIB_RETRIES\n",
+			 attempt + 1);
+		if (attempt % PSGMII_CALIB_RETRIES_BURST == 0)
+			schedule_timeout_interruptible(msecs_to_jiffies(PSGMII_CALIB_RETRY_DELAY));
+		else
+			schedule();
+	}
+
+	dev_err(priv->dev, "PSGMII work is unstable! Repeated recalibration attempts did not help!\n");
+	ret = -EFAULT;
+
+out_free:
+	list_for_each_entry(port_data, &calib, list) {
+		put_device(&port_data->phy->mdio.dev);
+	}
+	psgmii_free_calib_data();
+	return ret;
+}
diff --git a/drivers/net/ethernet/qualcomm/ipqess/ipqess_port.c b/drivers/net/ethernet/qualcomm/ipqess/ipqess_port.c
index 95407a008971..757d937dd711 100644
--- a/drivers/net/ethernet/qualcomm/ipqess/ipqess_port.c
+++ b/drivers/net/ethernet/qualcomm/ipqess/ipqess_port.c
@@ -1315,7 +1315,8 @@  ipqess_psgmii_configure(struct qca8k_priv *priv)
 	int ret;
 
 	if (!atomic_fetch_inc(&priv->psgmii_calibrated)) {
-		dev_warn(priv->dev, "Unable to calibrate PSGMII, link will be unstable!\n");
+		dev_dbg(priv->dev, "starting PSGMII calibration...\n");
+		psgmii_calibrate_and_test(priv);
 
 		ret = regmap_clear_bits(priv->psgmii, PSGMIIPHY_MODE_CONTROL,
 					PSGMIIPHY_MODE_ATHR_CSCO_MODE_25M);
diff --git a/drivers/net/ethernet/qualcomm/ipqess/ipqess_port.h b/drivers/net/ethernet/qualcomm/ipqess/ipqess_port.h
index a0639933e8bb..6e6a5d15f588 100644
--- a/drivers/net/ethernet/qualcomm/ipqess/ipqess_port.h
+++ b/drivers/net/ethernet/qualcomm/ipqess/ipqess_port.h
@@ -92,4 +92,8 @@  int ipqess_port_obj_del(struct net_device *netdev, const void *ctx,
 
 bool ipqess_port_offloads_bridge_port(struct ipqess_port *port,
 				      const struct net_device *netdev);
+
+/* Defined in ipqess_calib.c */
+int psgmii_calibrate_and_test(struct qca8k_priv *priv);
+
 #endif