mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Doug Ledford <doug.ledford@hpe.com>
To: "Slingshot Open Source Team"
	<slingshot-os-team@groups.ext.hpe.com>,
	"Nathan Chancellor" <nathan@kernel.org>,
	"Nicolas Schier" <nsc@kernel.org>,
	"Richard Cochran" <richardcochran@gmail.com>,
	"Andrew Lunn" <andrew+netdev@lunn.ch>,
	"David S. Miller" <davem@davemloft.net>,
	"Eric Dumazet" <edumazet@kernel.org>,
	"Jakub Kicinski" <kuba@kernel.org>,
	"Paolo Abeni" <pabeni@redhat.com>,
	"Sumit Semwal" <sumit.semwal@linaro.org>,
	"Christian König" <christian.koenig@amd.com>,
	"Guenter Roeck" <linux@roeck-us.net>
Cc: <linux-kernel@vger.kernel.org>, <netdev@vger.kernel.org>,
	<linux-kbuild@vger.kernel.org>,
	Doug Ledford <doug.ledford@hpe.com>,
	"Mamila Santosh" <santosh-kumar.mamila@hpe.com>
Subject: [PATCH 04/23] net: hpe: sbl: add the SerDes layer
Date: Fri, 30 Oct 2026 13:59:00 -0700	[thread overview]
Message-ID: <20261030-cxi-netdev-submission-v1-4-8c2c391cc283@hpe.com> (raw)
In-Reply-To: <20261030-cxi-netdev-submission-v1-0-8c2c391cc283@hpe.com>

Add the SerDes layer: per-lane configuration, the autogenerated lane
map, and the function-pointer table that lets the PML layer drive the
SerDes without knowing which SerDes core is present.

sbl_serdes_map_autogen.h is generated from the hardware lane
description and is kept in generated form, so style checkers will flag
it for line length and macro layout.

checkpatch --strict (100 columns) still reports the following in this
patch. They need coordination with the internal repositories this code
is maintained in and will be fixed in V2:
- WARNING: LONG_LINE (45), BLOCK_COMMENT_STYLE (36), ALLOC_WITH_SIZEOF
  (2), ENOTSUPP (1), LONG_LINE_COMMENT (1), LONG_LINE_STRING (1),
  TYPO_SPELLING (1)
- CHECK: UNNECESSARY_PARENTHESES (37), BRACES (13), ALLOC_SIZEOF_STRUCT
  (2), PARENTHESIS_ALIGNMENT (2), COMPARISON_TO_NULL (1), LINE_SPACING
  (1)

Co-developed-by: Mamila Santosh <santosh-kumar.mamila@hpe.com>
Signed-off-by: Mamila Santosh <santosh-kumar.mamila@hpe.com>
Signed-off-by: Doug Ledford <doug.ledford@hpe.com>
---
 drivers/net/ethernet/hpe/sbl/sbl_serdes.c    | 1117 +++++++
 drivers/net/ethernet/hpe/sbl/sbl_serdes_fn.c | 4310 ++++++++++++++++++++++++++
 2 files changed, 5427 insertions(+)

diff --git a/drivers/net/ethernet/hpe/sbl/sbl_serdes.c b/drivers/net/ethernet/hpe/sbl/sbl_serdes.c
new file mode 100644
index 000000000000..4db5bb1a0bb9
--- /dev/null
+++ b/drivers/net/ethernet/hpe/sbl/sbl_serdes.c
@@ -0,0 +1,1117 @@
+// SPDX-License-Identifier: GPL-2.0
+
+/* Copyright 2019-2022, 2024-2025 Hewlett Packard Enterprise Development LP */
+
+#include <linux/kernel.h>
+#include <linux/device.h>
+#include <linux/slab.h>
+#include <linux/delay.h>
+
+#include <linux/hpe/sbl/sbl.h>
+#include <linux/hpe/sbl/sbl_kconfig.h>
+
+#include "sbl_constants.h"
+#include "sbl_serdes.h"
+#include "sbl_config_list.h"
+#include "sbl_serdes_map.h"
+#include "sbl_serdes_fn.h"
+#include "sbl_internal.h"
+
+static atomic_t sbl_next_serdes_config_tag = ATOMIC_INIT(-1);
+
+static int sbl_serdes_stop_internal(struct sbl_inst *sbl, int port_num)
+{
+	struct sbl_link *link = sbl->link + port_num;
+	int err = 0;
+	int serdes;
+
+	sbl_dev_dbg(sbl->dev, "p%d: SerDes stop", port_num);
+
+	err = sbl_port_stop_pcal(sbl, port_num);
+	if (err) {
+		sbl_dev_err(sbl->dev, "p%d: SerDes stop: port_pcal_stop failed [%d]",
+			    port_num, err);
+		goto out;
+	}
+
+	for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+		err = sbl_set_tx_rx_enable(sbl, port_num, serdes,
+					   false, false, false);
+		if (err) {
+			sbl_dev_err(sbl->dev,
+				    "p%d: SerDes stop: disable failed [%d]",
+				port_num, err);
+			goto out;
+		}
+	}
+
+	err = sbl_spico_reset(sbl, port_num);
+	if (err) {
+		sbl_dev_err(sbl->dev, "p%d: SerDes stop: spico_reset failed [%d]",
+			    port_num, err);
+		goto out;
+	}
+
+out:
+	if (err) {
+		link->sstate = SBL_SERDES_STATUS_ERROR;
+		/* Try and recover from errors with FW reload */
+		link->reload_serdes_fw = true;
+	} else {
+		sbl_dev_dbg(sbl->dev, "p%d: SerDes stop: done", port_num);
+		link->sstate = SBL_SERDES_STATUS_DOWN;
+	}
+	link->serr = err;
+
+	return err;
+}
+
+/* This delay is to give time for the optical transceivers to lock */
+static int sbl_serdes_optical_lock_delay(struct sbl_inst *sbl, int port_num)
+{
+	struct sbl_link *link = sbl->link + port_num;
+	unsigned long last_jiffy;
+	int err = 0;
+
+	sbl_dev_dbg(sbl->dev, "p%d: optical lock delay", port_num);
+
+	if (link->blattr.config_target != SBL_BASE_LINK_CONFIG_AOC) {
+		sbl_dev_err(sbl->dev, "p%d: optical lock delay - config not optical", port_num);
+		return -EINVAL;
+	}
+
+	link->optical_delay_active = true;
+	last_jiffy = jiffies + msecs_to_jiffies(link->blattr.aoc.optical_lock_delay);
+	while (time_is_after_jiffies(last_jiffy)) {
+		if (sbl_base_link_start_cancelled(sbl, port_num)) {
+			err = -ECANCELED;
+			goto out;
+		}
+		if (sbl_start_timeout(sbl, port_num)) {
+			err = -ETIMEDOUT;
+			goto out;
+		}
+		msleep(link->blattr.aoc.optical_lock_interval);
+	}
+out:
+	link->optical_delay_active = false;
+	return err;
+}
+
+/**
+ * sbl_serdes_load() - SerDes initial load function
+ * @sbl: pointer containing target config and state
+ * @port_num: target port number
+ * @force: reset and flash even if current fw is at the correct
+ *        rev and build number
+ *
+ *  When power cycle or controller boots, This function
+ *  Applies the SBus speedup and flashes all SBM/SerDes firmware images
+ *
+ * Return: 0 on success or negative errno on failure
+ */
+int sbl_serdes_load(struct sbl_inst *sbl, int port_num, bool force)
+{
+	int err;
+
+	if (!sbl->is_hw)
+		return 0;
+
+	if (port_num == SBL_ALL_PORTS) {
+		err = sbl_apply_sbus_divider(sbl, SBL_SBUS_DIVIDER_DFLT);
+		if (err) {
+			sbl_dev_err(sbl->dev, "p%d: SBus speedup failed [%d]", port_num, err);
+			return err;
+		}
+	}
+
+	/* retrieve, flash and release SBM firmware */
+	if (port_num == SBL_ALL_PORTS) {
+		err = sbl_sbm_firmware_flash(sbl);
+		if (err) {
+			sbl_dev_err(sbl->dev, "p%d: failed to flash SBM fw [%d]", port_num, err);
+			return err;
+		}
+	}
+
+	/* retrieve and flash SerDes firmware */
+	err = sbl_serdes_firmware_flash(sbl, port_num, force);
+	if (err) {
+		sbl_dev_err(sbl->dev, "p%d: failed to flash SerDes fw [%d]",
+			    port_num, err);
+		return err;
+	}
+
+	return 0;
+}
+EXPORT_SYMBOL(sbl_serdes_load);
+
+/* link partner detection
+ *
+ * This function will attempt a serdes minitune. This fast tune will only
+ * succeed if the lp is sending AM using the same mode as we are using.
+ *
+ */
+int sbl_serdes_lp_detect(struct sbl_inst *sbl, int port_num)
+{
+	struct sbl_link *link;
+	int err;
+	int tmp_err;
+
+	err = sbl_validate_instance(sbl);
+	if (err)
+		return err;
+
+	err = sbl_validate_port_num(sbl, port_num);
+	if (err)
+		return err;
+
+	link = sbl->link + port_num;
+
+	/* in loopback mode the link partner (ourself) is always present! */
+	if (link->loopback_mode == SBL_LOOPBACK_MODE_LOCAL)
+		return 0;
+
+	if (link->sstate != SBL_SERDES_STATUS_DOWN) {
+		sbl_dev_err(sbl->dev, "p%d: serdes_lpd wrong state to start (%s)",
+			    port_num, sbl_serdes_state_str(link->sstate));
+		/* leave state unchanged */
+		return -EUCLEAN;
+	}
+
+	link->sstate = SBL_SERDES_STATUS_LPD_MT;
+	link->serr = 0;
+
+	for (link->lpd_try_count = 0; true; ++link->lpd_try_count) {
+		err = sbl_serdes_minitune_setup(sbl, port_num);
+		if (err) {
+			sbl_dev_err(sbl->dev, "p%d: serdes_lpd setup failed [%d]",
+				    port_num, err);
+			goto out_err;
+		}
+
+		/* If the other end is there then we should block for no more
+		 * than 5s
+		 */
+		err = sbl_serdes_minitune_block(sbl, port_num);
+		switch (err) {
+		case 0:
+			sbl_dev_dbg(sbl->dev, "p%d: serdes_lpd done", port_num);
+			link->lp_detected = true;
+			goto out_done;
+
+		case -ETIMEDOUT:
+			sbl_dev_warn(sbl->dev, "p%d: serdes_lpd timed out", port_num);
+			/* Occasionally serdes minitune will fail even though the link
+			 * partner is actually there!
+			 * Sometimes we can recover by resetting the serdes PLLs and/or
+			 * reloading the serdes firmware
+			 */
+			if (!sbl_debug_option(sbl, port_num, SBL_DEBUG_INHIBIT_SPLL_RESET)) {
+				tmp_err = sbl_reset_serdes_plls(sbl, port_num);
+				if (tmp_err) {
+					/* if this fails nothing we can do */
+					sbl_dev_err(sbl->dev, "p%d: serdes_lpd pll reset failed [%d]",
+						    port_num, tmp_err);
+					/* TODO maybe force fw reload here? */
+				}
+			}
+
+			if (link->blattr.options & SBL_OPT_RELOAD_FW_ON_TIMEOUT)
+				link->reload_serdes_fw = true;
+
+			goto out_done;
+
+		case -ECANCELED:
+			sbl_dev_dbg(sbl->dev, "p%d: serdes_lpd cancelled", port_num);
+			goto out_done;
+
+		case -ETIME:
+			sbl_dev_dbg(sbl->dev, "p%d: serdes_lpd nothing detected", port_num);
+			/* just try again */
+			break;
+
+		default:
+			sbl_dev_err(sbl->dev, "p%d: serdes_lpd block failed [%d]", port_num, err);
+			goto out_err;
+		}
+	};
+
+out_done:
+	link->sstate = SBL_SERDES_STATUS_DOWN;
+	link->serr = 0;
+	return err;
+
+out_err:
+	/* serdes is broken and requires reset */
+	link->sstate = SBL_SERDES_STATUS_ERROR;
+	link->serr = err;
+	return err;
+}
+
+int sbl_serdes_start(struct sbl_inst *sbl, int port_num)
+{
+	struct sbl_link *link;
+	int err;
+	int tmp_err;
+
+	err = sbl_validate_instance(sbl);
+	if (err)
+		return err;
+
+	err = sbl_validate_port_num(sbl, port_num);
+	if (err)
+		return err;
+
+	sbl_dev_dbg(sbl->dev, "p%d: SerDes start", port_num);
+
+	link = sbl->link + port_num;
+
+	if (link->sstate != SBL_SERDES_STATUS_DOWN) {
+		sbl_dev_err(sbl->dev, "p%d: SerDes start: wrong state (%s)",
+			    port_num, sbl_serdes_state_str(link->sstate));
+		/* leave state unchanged */
+		return -EUCLEAN;
+	}
+
+	/* configure serdes */
+	err = sbl_serdes_config(sbl, port_num, false);
+	if (err) {
+		sbl_dev_err(sbl->dev,
+			    "p%d: SerDes start: serdes_config failed [%d]",
+			    port_num, err);
+		if (err == -EBADE)
+			link->reload_serdes_fw = true;
+		goto out;
+	}
+
+	/* wait a while before starting to tune to let the optics lock if present
+	 * (in local loopback mode however there are never any optics to wait for)
+	 */
+	if (link->blattr.loopback_mode != SBL_LOOPBACK_MODE_LOCAL) {
+		if (link->mattr.media == SBL_LINK_MEDIA_OPTICAL) {
+			err = sbl_serdes_optical_lock_delay(sbl, port_num);
+			if (err)
+				goto out;
+		}
+	}
+
+	/* make sure we have time left for at lease 2 tuning cycles */
+	sbl_start_timeout_ensure_remaining(sbl, port_num,
+					   2 * (link->blattr.dfe_timeout +
+						 link->blattr.dfe_pre_delay));
+
+	/* tune serdes */
+	link->sstate = SBL_SERDES_STATUS_TUNING;
+
+	err = sbl_serdes_tuning(sbl, port_num);
+	if (err) {
+		switch (err) {
+		case -ECANCELED:
+			sbl_dev_dbg(sbl->dev, "p%d: SerDes start: tuning cancelled", port_num);
+			break;
+		case -ETIMEDOUT:
+			sbl_dev_dbg(sbl->dev, "p%d: SerDes start: tuning timed out", port_num);
+			/* occasionally tuning can fail even thought the lp is good
+			 * resetting the serdes rx plls or reloading the serdes fw can
+			 * sometimes fix this
+			 */
+			if (!sbl_debug_option(sbl, port_num, SBL_DEBUG_INHIBIT_SPLL_RESET)) {
+				tmp_err = sbl_reset_serdes_plls(sbl, port_num);
+				if (tmp_err) {
+					/* if this fails nothing we can do */
+					sbl_dev_err(sbl->dev, "p%d: SerDes start: reset_serdes_plls failed [%d]",
+						    port_num, tmp_err);
+					/* TODO maybe force fw reload here? */
+				}
+			}
+
+			if (link->blattr.options & SBL_OPT_RELOAD_FW_ON_TIMEOUT)
+				link->reload_serdes_fw = true;
+			break;
+		default:
+			sbl_dev_err(sbl->dev,
+				    "p%d: SerDes start: serdes_tuning failed [%d]",
+				    port_num, err);
+			break;
+		}
+	}
+
+out:
+	/* update status */
+	if (err) {
+		link->sstate = SBL_SERDES_STATUS_ERROR;
+		link->serr = err;
+	} else {
+		sbl_dev_dbg(sbl->dev, "p%d: SerDes start: done", port_num);
+		link->sstate = SBL_SERDES_STATUS_RUNNING;
+		link->serr = 0;
+	}
+
+	return err;
+}
+
+int sbl_serdes_stop(struct sbl_inst *sbl, int port_num)
+{
+	struct sbl_link *link;
+	int err;
+
+	err = sbl_validate_instance(sbl);
+	if (err)
+		return err;
+
+	err = sbl_validate_port_num(sbl, port_num);
+	if (err)
+		return err;
+
+	link = sbl->link + port_num;
+
+	if (!(link->sstate & (SBL_SERDES_STATUS_RUNNING | SBL_SERDES_STATUS_AUTONEG))) {
+		sbl_dev_err(sbl->dev, "p%d: SerDes stop: wrong state (%s)",
+			    port_num, sbl_serdes_state_str(link->sstate));
+		/* leave state unchanged */
+		return -EUCLEAN;
+	}
+
+	if (sbl_debug_option(sbl, port_num, SBL_DEBUG_KEEP_SERDES_UP)) {
+		sbl_dev_err(sbl->dev, "p%d: KEEP SERDES UP", port_num);
+		return 0;
+	}
+
+	return sbl_serdes_stop_internal(sbl, port_num);
+}
+
+int sbl_serdes_reset(struct sbl_inst *sbl, int port_num)
+{
+	struct sbl_link *link;
+	int fail_reset_serdes = 0;
+	int serdes;
+	int err;
+
+	err = sbl_validate_instance(sbl);
+	if (err)
+		return err;
+
+	err = sbl_validate_port_num(sbl, port_num);
+	if (err)
+		return err;
+
+	sbl_dev_dbg(sbl->dev, "p%d: SerDes reset", port_num);
+
+	link = sbl->link + port_num;
+	link->sstate = SBL_SERDES_STATUS_RESETTING;
+
+	/* optionally clear any saved tuning params */
+	if (link->blattr.options & SBL_OPT_RESET_CLEAR_PARAMS) {
+		link->tuning_params.tp_state_hash0 = 0;
+		link->tuning_params.tp_state_hash1 = 0;
+		for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+			memset(link->tuning_params.params[serdes].ctle, 0,
+			       sizeof(u16) * NUM_CTLE_PARAMS);
+			memset(link->tuning_params.params[serdes].ffe, 0,
+			       sizeof(u16) * NUM_FFE_PARAMS);
+			memset(link->tuning_params.params[serdes].dfe, 0,
+			       sizeof(u16) * NUM_DFE_PARAMS);
+			memset(link->tuning_params.params[serdes].rxvs, 0,
+			       sizeof(u16) * NUM_RXVS_PARAMS);
+			memset(link->tuning_params.params[serdes].rxvc, 0,
+			       sizeof(u16) * NUM_RXVC_PARAMS);
+			memset(link->tuning_params.params[serdes].rsdo, 0,
+			       sizeof(u16) * NUM_RSDO_PARAMS);
+			memset(link->tuning_params.params[serdes].rsdc, 0,
+			       sizeof(u16) * NUM_RSDC_PARAMS);
+			memset(link->tuning_params.params[serdes].rsto, 0,
+			       sizeof(u16) * NUM_RSTO_PARAMS);
+			memset(link->tuning_params.params[serdes].rstc, 0,
+			       sizeof(u16) * NUM_RSTC_PARAMS);
+			memset(link->tuning_params.params[serdes].eh, 0,
+			       sizeof(u16) * NUM_EH_PARAMS);
+			memset(link->tuning_params.params[serdes].gtp, 0,
+			       sizeof(u16) * NUM_GTP_PARAMS);
+			memset(link->tuning_params.params[serdes].dccd, 0,
+			       sizeof(u16) * NUM_DCCD_PARAMS);
+			memset(link->tuning_params.params[serdes].p4lv, 0,
+			       sizeof(u16) * NUM_P4LV_PARAMS);
+			memset(link->tuning_params.params[serdes].afec, 0,
+			       sizeof(u16) * NUM_AFEC_PARAMS);
+		}
+	}
+
+	/* Stop continuous tune */
+	err = sbl_port_stop_pcal(sbl, port_num);
+	if (err)
+		sbl_dev_warn(sbl->dev, "p%d: SerDes reset: port_stop_pcal failed [%d]",
+			     port_num, err);
+
+	/* Previously sbl_spico_reset() is now changed to
+	 * sbl_serdes_soft_reset() to avoid SBUS lockups - SSHOTPLAT-2222.
+	 * Upon error, spico reset will reload serdes firmware.
+	 * Note, reloading the firmware can be forced with a
+	 * debug option.
+	 */
+	for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+		err = sbl_serdes_soft_reset(sbl, port_num, serdes);
+		if (err) {
+			fail_reset_serdes = serdes;
+			break;
+		}
+	}
+	if (err)
+		sbl_dev_err(sbl->dev, "p%ds%d: SerDes reset: serdes_soft_reset failed [%d]",
+			    port_num, fail_reset_serdes, err);
+	else if (sbl_debug_option(sbl, port_num, SBL_DEBUG_FORCE_RELOAD_SERDES_FW))
+		/* Skip out_success and continue to SPICO reload */
+		sbl_dev_info(sbl->dev, "p%d: SPICO force serdes_firmware_flash_safe", port_num);
+	else
+		goto out_success;
+
+	/* reload fw */
+	err = sbl_serdes_firmware_flash_safe(sbl, port_num, true);
+	if (err)
+		sbl_dev_err(sbl->dev, "p%d: SerDes reset: serdes_firmware_flash_safe failed [%d]",
+			    port_num, err);
+	else
+		goto out_success;
+
+	/* This is really bad!
+	 * There is nothing we can do but say we are in error and
+	 * be reset again
+	 */
+	sbl_dev_err(sbl->dev, "p%d: SerDes reset: failed [%d]", port_num, err);
+	link->sstate = SBL_SERDES_STATUS_ERROR;
+	link->serr = err;
+
+	return err;
+
+out_success:
+	/* serdes should be fine */
+	sbl_dev_dbg(sbl->dev, "p%d: SerDes reset: done", port_num);
+	link->sstate = SBL_SERDES_STATUS_DOWN;
+	link->serr = 0;
+
+	return 0;
+}
+
+int sbl_an_serdes_start(struct sbl_inst *sbl, int port_num)
+{
+	struct sbl_link *link;
+	int err;
+
+	err = sbl_validate_instance(sbl);
+	if (err)
+		return err;
+
+	err = sbl_validate_port_num(sbl, port_num);
+	if (err)
+		return err;
+
+	link = sbl->link + port_num;
+
+	if (link->mattr.media != SBL_LINK_MEDIA_ELECTRICAL) {
+		sbl_dev_err(sbl->dev, "p%d: SerDes AN start - media not electrical",
+			    port_num);
+		return -EINVAL;
+	}
+
+	if (link->blattr.pec.an_mode != SBL_AN_MODE_ON &&
+	    link->blattr.pec.an_mode != SBL_AN_MODE_FIXED) {
+		sbl_dev_err(sbl->dev, "p%d: SerDes AN start - bad mode specified (%s)",
+			    port_num, sbl_an_mode_str(link->blattr.pec.an_mode));
+		return -EINVAL;
+	}
+
+	if (link->sstate != SBL_SERDES_STATUS_DOWN) {
+		sbl_dev_err(sbl->dev, "p%d: SerDes AN start - wrong state (%s)",
+			    port_num, sbl_serdes_state_str(link->sstate));
+		/* leave state unchanged */
+		return -EUCLEAN;
+	}
+
+	err = sbl_serdes_stop_internal(sbl, port_num);
+	if (err) {
+		sbl_dev_err(sbl->dev, "p%d: SerDes AN start - stop internal failed [%d]",
+			    port_num, err);
+		goto out_err;
+	}
+
+	err = sbl_serdes_config(sbl, port_num, true);
+	if (err) {
+		sbl_dev_err(sbl->dev, "p%d: SerDes AN start - config failed [%d]",
+			    port_num, err);
+		goto out_err;
+	}
+
+	sbl_dev_dbg(sbl->dev, "p%d: SerDes AN started", port_num);
+	link->sstate = SBL_SERDES_STATUS_AUTONEG;
+	link->serr = 0;
+	return 0;
+
+out_err:
+	link->sstate = SBL_SERDES_STATUS_ERROR;
+	link->serr = err;
+	return err;
+}
+
+int sbl_an_serdes_stop(struct sbl_inst *sbl, int port_num)
+{
+	struct sbl_link *link;
+	int err;
+
+	err = sbl_validate_instance(sbl);
+	if (err)
+		return err;
+
+	err = sbl_validate_port_num(sbl, port_num);
+	if (err)
+		return err;
+
+	link = sbl->link + port_num;
+
+	if (link->sstate != SBL_SERDES_STATUS_AUTONEG) {
+		sbl_dev_err(sbl->dev, "p%d: SerDes in wrong state (%s) for AN stop\n",
+			    port_num, sbl_serdes_state_str(link->sstate));
+		/* leave state unchanged */
+		return -EUCLEAN;
+	}
+
+	err = sbl_serdes_stop_internal(sbl, port_num);
+	if (err) {
+		sbl_dev_err(sbl->dev, "p%d: SerDes AN stop failed [%d]\n",
+			    port_num, err);
+		link->sstate = SBL_SERDES_STATUS_ERROR;
+	} else {
+		sbl_dev_dbg(sbl->dev, "p%d: SerDes AN stopped", port_num);
+		link->sstate = SBL_SERDES_STATUS_DOWN;
+	}
+	link->serr = err;
+	return err;
+}
+
+/**
+ * sbl_serdes_get_tuning_params() - Save tuning params
+ * @sbl: A slingshot base link device instance
+ * @port_num: port number
+ * @tuning_params: tuning parameters used to configure a link,
+ *                allowing the SerDes to come up without needing to DFE tune.
+ *
+ * Get tuning parameters which was used to configure a link
+ *
+ * Context: Process, May sleep. Uses mutex_lock
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+int sbl_serdes_get_tuning_params(struct sbl_inst *sbl, int port_num,
+				 struct sbl_tuning_params *tuning_params)
+{
+	struct sbl_link *link;
+	int err;
+
+	err = sbl_validate_instance(sbl);
+	if (err)
+		return err;
+
+	err = sbl_validate_port_num(sbl, port_num);
+	if (err)
+		return err;
+
+	link = sbl->link + port_num;
+
+	/* don't return invalid parameters */
+	mutex_lock(&link->tuning_params_mtx);
+	if (link->tuning_params.magic != SBL_TUNING_PARAM_MAGIC) {
+		mutex_unlock(&link->tuning_params_mtx);
+		return -ENODATA;
+	}
+
+	sbl_dev_dbg(sbl->dev, "p%d: tp get - returning hash0 0x%llx hash1 0x%llx\n",
+		    port_num, link->tuning_params.tp_state_hash0,
+		link->tuning_params.tp_state_hash1);
+
+	memcpy(tuning_params, &link->tuning_params, sizeof(struct sbl_tuning_params));
+	mutex_unlock(&link->tuning_params_mtx);
+
+	return 0;
+}
+EXPORT_SYMBOL(sbl_serdes_get_tuning_params);
+
+/**
+ * sbl_serdes_set_tuning_params() - set tuning params for serdes
+ * @sbl: A slingshot base link device instance
+ * @port_num: port number
+ * @tuning_params: Pointer to struct sbl_tuning_params
+ *
+ *  Set tuning parameters used to configure a link, allowing the SerDes
+ *  to come up.
+ *
+ * Context: Process, May sleep. Uses mutex_lock
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+int sbl_serdes_set_tuning_params(struct sbl_inst *sbl, int port_num,
+				 struct sbl_tuning_params *tuning_params)
+{
+	struct sbl_link *link;
+	int err;
+
+	err = sbl_validate_instance(sbl);
+	if (err)
+		return err;
+
+	err = sbl_validate_port_num(sbl, port_num);
+	if (err)
+		return err;
+
+	link = sbl->link + port_num;
+
+	if (tuning_params->magic != SBL_TUNING_PARAM_MAGIC)
+		return -EINVAL;
+
+	/* only support one verion for now */
+	if (tuning_params->version != SBL_TUNING_PARAM_VERSION)
+		return -EINVAL;
+
+	mutex_lock(&link->tuning_params_mtx);
+	memcpy(&link->tuning_params, tuning_params, sizeof(struct sbl_tuning_params));
+	mutex_unlock(&link->tuning_params_mtx);
+
+	sbl_dev_dbg(sbl->dev, "p%d: tp set - received hash0 0x%llx hash1 0x%llx\n",
+		    port_num, link->tuning_params.tp_state_hash0,
+		link->tuning_params.tp_state_hash1);
+
+	return 0;
+}
+EXPORT_SYMBOL(sbl_serdes_set_tuning_params);
+
+/**
+ * sbl_serdes_invalidate_tuning_params() - Invalidate tuning params
+ * @sbl: A slingshot base link device instance
+ * @port_num: port number
+ *
+ * Clearing per port tuning parameters
+ *
+ * Context: Process, May sleep. Uses mutex_lock
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int sbl_serdes_invalidate_tuning_params(struct sbl_inst *sbl, int port_num)
+{
+	struct sbl_link *link;
+	int err;
+
+	err = sbl_validate_instance(sbl);
+	if (err)
+		return err;
+
+	err = sbl_validate_port_num(sbl, port_num);
+	if (err)
+		return err;
+
+	link = sbl->link + port_num;
+
+	mutex_lock(&link->tuning_params_mtx);
+	link->tuning_params.magic = 0;               /* prevent transfer to usr-space */
+	link->tuning_params.tp_state_hash0 = 0;      /* prevent use */
+	link->tuning_params.tp_state_hash1 = 0;      /* prevent use */
+	mutex_unlock(&link->tuning_params_mtx);
+
+	return 0;
+}
+EXPORT_SYMBOL(sbl_serdes_invalidate_tuning_params);
+
+/**
+ * sbl_serdes_invalidate_all_tuning_params() - Invalidate all tuning params
+ * @sbl: A slingshot base link device instance
+ *
+ * Clearing all ports tuning parameters
+ *
+ * Return: 0 on success
+ */
+int sbl_serdes_invalidate_all_tuning_params(struct sbl_inst *sbl)
+{
+	int i;
+
+	for (i = 0; i < sbl->switch_info->num_ports; ++i)
+		sbl_serdes_invalidate_tuning_params(sbl, i);
+
+	return 0;
+}
+EXPORT_SYMBOL(sbl_serdes_invalidate_all_tuning_params);
+
+/**
+ * sbl_serdes_add_config() - Serdes configuration addition
+ * @sbl: A slingshot base link device instance
+ * @tp_state_mask0: State lookup mask
+ * @tp_state_mask1: State lookup mask
+ * @tp_state_match0: match values after masking
+ * @tp_state_match1: match values after masking
+ * @port_mask: applicable ports
+ * @serdes_mask: applicable serdes
+ * @vals: Configuration values
+ * @is_default: Default configuration
+ *
+ * A SerDes configuration entry specifies a set of config values which
+ * are selected to get the SerDes to tune reliably for a given setup.
+ *
+ * Context: Process context, Acquires lock and release serdes_config_lock <spin_lock>
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+int sbl_serdes_add_config(struct sbl_inst *sbl,
+			  u64 tp_state_mask0, u64 tp_state_mask1,
+		u64 tp_state_match0, u64 tp_state_match1,
+		u64 port_mask, u8 serdes_mask, struct sbl_sc_values *vals,
+		bool is_default)
+{
+	struct sbl_serdes_config *new_sc;
+	struct sbl_serdes_config *sc;
+	int err;
+
+	err = sbl_validate_instance(sbl);
+	if (err)
+		return err;
+
+	if (vals->magic != SBL_SERDES_CONFIG_MAGIC)
+		return -EINVAL;
+
+	sbl_dev_dbg(sbl->dev, "serdes add config");
+
+	new_sc = kmalloc(sizeof(struct sbl_serdes_config), GFP_KERNEL);
+	if (!new_sc)
+		return -ENOMEM;
+
+	new_sc->tp_state_mask0  = tp_state_mask0;
+	new_sc->tp_state_mask1  = tp_state_mask1;
+	new_sc->tp_state_match0 = tp_state_match0;
+	new_sc->tp_state_match1 = tp_state_match1;
+	new_sc->port_mask       = port_mask;
+	new_sc->serdes_mask     = serdes_mask;
+	new_sc->is_default      = is_default;
+	new_sc->tag             = sbl_serdes_get_config_tag();
+	memcpy(&new_sc->vals, vals, sizeof(struct sbl_sc_values));
+	INIT_LIST_HEAD(&new_sc->list);
+
+	spin_lock(&sbl->serdes_config_lock);
+
+	/* check new entry is unique */
+	list_for_each_entry(sc, &sbl->serdes_config_list, list) {
+		if (sc->port_mask == port_mask &&
+		    sc->serdes_mask == serdes_mask &&
+		    sc->tp_state_mask0 == tp_state_mask0 &&
+		    sc->tp_state_mask1 == tp_state_mask1 &&
+		    sc->tp_state_match0 == tp_state_match0 &&
+		    sc->tp_state_match1 == tp_state_match1) {
+			spin_unlock(&sbl->serdes_config_lock);
+			kfree(new_sc);
+			return -EEXIST;
+		}
+	}
+
+	/* add the new entry */
+	list_add(&new_sc->list, &sbl->serdes_config_list);
+	sbl_dev_dbg(sbl->dev, "added serdes config, tag %d\n", new_sc->tag);
+	spin_unlock(&sbl->serdes_config_lock);
+	return 0;
+}
+EXPORT_SYMBOL(sbl_serdes_add_config);
+
+/**
+ * sbl_serdes_del_config() - Delete serdes configuration
+ * @sbl: A slingshot base link device instance
+ * @tp_state_mask0: State lookup mask
+ * @tp_state_mask1: State lookup mask
+ * @tp_state_match0: match values after masking
+ * @tp_state_match1: match values after masking
+ * @port_mask: applicable ports
+ * @serdes_mask: applicable serdes
+ *
+ * Delete SerDes config values which are selected to get the
+ * SerDes to tune
+ *
+ * Context: Process context, Acquires lock and release serdes_config_lock <spin_lock>
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+int sbl_serdes_del_config(struct sbl_inst *sbl,
+			  u64 tp_state_mask0, u64 tp_state_mask1,
+		u64 tp_state_match0, u64 tp_state_match1,
+		u64 port_mask, u8  serdes_mask)
+{
+	struct sbl_serdes_config *sc;
+	int err;
+
+	err = sbl_validate_instance(sbl);
+	if (err)
+		return err;
+
+	sbl_dev_dbg(sbl->dev, "serdes del config");
+
+	spin_lock(&sbl->serdes_config_lock);
+
+	/* find the entry and remove it */
+	list_for_each_entry(sc, &sbl->serdes_config_list, list) {
+		if (sc->port_mask == port_mask &&
+		    sc->serdes_mask == serdes_mask &&
+		    sc->tp_state_mask0 == tp_state_mask0 &&
+		    sc->tp_state_mask1 == tp_state_mask1 &&
+		    sc->tp_state_match0 == tp_state_match0 &&
+		    sc->tp_state_match1 == tp_state_match1) {
+			list_del(&sc->list);
+			spin_unlock(&sbl->serdes_config_lock);
+			sbl_dev_dbg(sbl->dev, "deleted serdes config, tag %d\n", sc->tag);
+			kfree(sc);
+			return 0;
+		}
+	}
+
+	/* not found */
+	spin_unlock(&sbl->serdes_config_lock);
+	return -ENOENT;
+}
+EXPORT_SYMBOL(sbl_serdes_del_config);
+
+/**
+ * sbl_serdes_clear_all_configs() - serdes clear all configurations
+ * @sbl: A slingshot base link device instance
+ * @clr_default: Clear default value
+ *
+ * Context: Process context, Acquires lock and release serdes_config_lock <spin_lock>
+ *
+ * Return: 0 on success, negative error on failure
+ */
+int sbl_serdes_clear_all_configs(struct sbl_inst *sbl, bool clr_default)
+{
+	struct sbl_serdes_config *sc;
+	struct sbl_serdes_config *tmp_sc;
+	int err;
+
+	err = sbl_validate_instance(sbl);
+	if (err)
+		return err;
+
+	sbl_dev_dbg(sbl->dev, "serdes clear all configs");
+
+	/* find the entries and remove them */
+	spin_lock(&sbl->serdes_config_lock);
+	list_for_each_entry_safe(sc, tmp_sc, &sbl->serdes_config_list, list) {
+		/* clear if not default or we want to clear the default */
+		if (!sc->is_default || clr_default) {
+			list_del(&sc->list);
+			kfree(sc);
+		}
+	}
+	spin_unlock(&sbl->serdes_config_lock);
+
+	return 0;
+}
+EXPORT_SYMBOL(sbl_serdes_clear_all_configs);
+
+/**
+ * sbl_serdes_dump_configs() - Print all serdes configs
+ * @sbl: A slingshot base link device instance
+ *
+ * Context: Process context, Acquires lock and release serdes_config_lock <spin_lock>
+ * Return: on error
+ */
+void sbl_serdes_dump_configs(struct sbl_inst *sbl)
+{
+	struct sbl_serdes_config *sc;
+	int count = 0;
+	int err;
+
+	err = sbl_validate_instance(sbl);
+	if (err)
+		return;
+
+	spin_lock(&sbl->serdes_config_lock);
+	list_for_each_entry_reverse(sc, &sbl->serdes_config_list, list) {
+		sbl_dev_info(sbl->dev,
+			     "serdes config %d: dflt: %d, tag %d, mask0 0x%llx, match0 0x%llx, mask1 0x%llx, match1 0x%llx, ports 0x%llx, serdes 0x%x\n",
+			count++, sc->is_default, sc->tag, sc->tp_state_mask0, sc->tp_state_match0, sc->tp_state_mask1, sc->tp_state_match1, sc->port_mask, sc->serdes_mask);
+	}
+	spin_unlock(&sbl->serdes_config_lock);
+}
+EXPORT_SYMBOL(sbl_serdes_dump_configs);
+
+u32 sbl_serdes_get_config_tag(void)
+{
+	return atomic_inc_return(&sbl_next_serdes_config_tag);
+}
+
+#ifdef CONFIG_SYSFS
+/**
+ * sbl_serdes_sysfs_sprint() - Format all seredes status into buffer
+ * @sbl: A slingshot base link device instance
+ * @port_num: port number
+ * @buf: Destination buffer to write the data
+ * @size: Size of @buf in bytes, the output is truncated to fit
+ *
+ * Inspects the current SERDES state and based on the value, formats
+ * relevant status or counter information into a readable string. The
+ * formatted output is written to the provided buffer. The output includes
+ * flags such as ":on", ":off", or serdes-reload-counters[fw,spico,pll] values
+ *
+ * Context: Process context, Acquires lock and release link->lock <spin_lock>
+ *
+ * Return: Number of characters written, at most @size - 1
+ */
+int sbl_serdes_sysfs_sprint(struct sbl_inst *sbl, int port_num, char *buf, size_t size)
+{
+	struct sbl_link *link = sbl->link + port_num;
+	int s = 0;
+	int i;
+	int s_fw_reload_count[4] = {0};
+	int s_spico_reset_count[4] = {0};
+	int s_pll_reset_count[4] = {0};
+
+	spin_lock(&link->lock);
+	s += scnprintf(buf + s, size - s, "serdes: %s",
+			sbl_serdes_state_str(link->sstate));
+	switch (link->sstate) {
+	case SBL_SERDES_STATUS_ERROR:
+		s += scnprintf(buf + s, size - s, " [%d]", link->serr);
+		break;
+	case SBL_SERDES_STATUS_LPD_MT:
+		s += scnprintf(buf + s, size - s, " cnt %d", link->lpd_try_count);
+		break;
+	case SBL_SERDES_STATUS_RUNNING:
+		if (link->loopback_mode != SBL_LOOPBACK_MODE_OFF) {
+			s += scnprintf(buf + s, size - s, ", loopback: %s",
+					sbl_loopback_mode_str(link->loopback_mode));
+		}
+		s += scnprintf(buf + s, size - s, ", precoding: %s",
+				(link->precoding_enabled) ? "on" : "off");
+		if (link->dfe_tune_count == SBL_DFE_USED_SAVED_PARAMS) {
+			s += scnprintf(buf + s, size - s, ", tune: used-saved-params");
+		} else {
+			s += scnprintf(buf + s, size - s,
+				       ", tune: cnt %d, eff %s (%lld.%.3ld/%lld.%.3ld)",
+					link->dfe_tune_count,
+					sbl_serdes_effort_str(link->ical_effort),
+					(long long)link->tune_time.tv_sec,
+					link->tune_time.tv_nsec / 1000000,
+					(long long)link->total_tune_time.tv_sec,
+					link->total_tune_time.tv_nsec / 1000000);
+		}
+		break;
+	case SBL_SERDES_STATUS_TUNING:
+		if (link->dfe_tune_count == SBL_DFE_USED_SAVED_PARAMS) {
+			s += scnprintf(buf + s, size - s, ", loading-saved-params");
+		} else {
+			s += scnprintf(buf + s, size - s, ", cnt %d, eff %s (%d/%d)",
+					link->dfe_tune_count,
+					sbl_serdes_effort_str(link->ical_effort),
+					sbl_link_tune_elapsed(sbl, port_num),
+					link->blattr.dfe_timeout);
+		}
+		break;
+	default:
+		break;
+	}
+
+	s += scnprintf(buf + s, size - s, ", pcal: %s",
+			(link->pcal_running) ? "on" : "off");
+	if (link->dfe_predelay_active)
+		s += scnprintf(buf + s, size - s, ", pre-delay");
+	if (link->optical_delay_active)
+		s += scnprintf(buf + s, size - s, ", optical-delay");
+	s += scnprintf(buf + s, size - s, ", fw_reload_skip_cnt: %d",
+			sbl_link_counters_read(sbl, port_num, serdes_fw_reload_skip));
+	s += scnprintf(buf + s, size - s, "\n");
+
+	s += scnprintf(buf + s, size - s, "serdes-reload-counters[fw,spico,pll]:");
+	sbl_link_counters_get(sbl, port_num, s_fw_reload_count,
+			      serdes0_fw_reload, 4);
+	sbl_link_counters_get(sbl, port_num, s_spico_reset_count,
+			      serdes0_spico_reset, 4);
+	sbl_link_counters_get(sbl, port_num, s_pll_reset_count,
+			      serdes0_pll_reset, 4);
+	for (i = 0; i < sbl->switch_info->num_serdes; ++i) {
+		s += scnprintf(buf + s, size - s, " s%d:[%d,%d,%d]", i, s_fw_reload_count[i],
+					s_spico_reset_count[i], s_pll_reset_count[i]);
+	}
+	s += scnprintf(buf + s, size - s, "\n");
+	spin_unlock(&link->lock);
+
+	return s;
+}
+EXPORT_SYMBOL(sbl_serdes_sysfs_sprint);
+
+/**
+ * sbl_serdes_fw_sysfs_sprint() - Format SERDES firmware rev and build into buffer
+ * @sbl: A slingshot base link device instance
+ * @port_num: port number
+ * @buf: Destination buffer to write the data
+ * @size: Size of @buf in bytes, the output is truncated to fit
+ *
+ * Get the serdes firmware version and build information for the port as a string.
+ * Multiple serdes firmware versions and build exist per port. Format those information
+ * into provided buffer
+ *
+ * Return: Number of characters written (at most @size - 1), negative error on failure
+ */
+int sbl_serdes_fw_sysfs_sprint(struct sbl_inst *sbl, int port_num, char *buf, size_t size)
+{
+	int serdes = 0;
+	struct fw {
+		uint rev;
+		uint build;
+	} fw[SBL_SERDES_LANES_PER_PORT];
+	ssize_t s = 0;
+
+	int err = sbl_validate_instance(sbl);
+
+	if (err)
+		return err;
+
+	err = sbl_validate_port_num(sbl, port_num);
+	if (err)
+		return err;
+
+	if (!buf || size == 0U)
+		return -ENOMEM;
+
+	memset(fw, 0, sizeof(fw));
+	for (serdes = 0; serdes < SBL_SERDES_LANES_PER_PORT; ++serdes) {
+		struct fw *pfw = fw + serdes;
+
+		sbl_serdes_get_fw_vers(sbl, port_num, serdes,
+				       &pfw->rev, &pfw->build);
+
+		s += scnprintf(buf + s, size - s,
+			"0x%4.4x_%4.4x\n", pfw->rev, pfw->build);
+	}
+
+	return s;
+}
+EXPORT_SYMBOL(sbl_serdes_fw_sysfs_sprint);
+
+/**
+ * sbl_sbm_fw_sysfs_sprint() - Get sbus master firmware version
+ * @sbl: A slingshot base link device instance
+ * @ring: Value
+ * @buf: Destination buffer to write the data
+ * @size: Size of @buf in bytes, the output is truncated to fit
+ *
+ * Return: Number of characters written (at most @size - 1), negative error on failure
+ */
+int sbl_sbm_fw_sysfs_sprint(struct sbl_inst *sbl, int ring, char *buf, size_t size)
+{
+	uint fw_rev, fw_build = 0;
+
+	int err = sbl_validate_instance(sbl);
+
+	if (err)
+		return err;
+
+	if (ring < 0 || ring >= sbl_get_num_sbus_rings(sbl))
+		return -EINVAL;
+
+	if (!buf || size == 0U)
+		return -ENOMEM;
+
+	sbl_sbm_get_fw_vers(sbl, ring, &fw_rev, &fw_build);
+	return scnprintf(buf, size, "0x%4.4x_%4.4x\n", fw_rev, fw_build);
+}
+EXPORT_SYMBOL(sbl_sbm_fw_sysfs_sprint);
+
+#endif
diff --git a/drivers/net/ethernet/hpe/sbl/sbl_serdes_fn.c b/drivers/net/ethernet/hpe/sbl/sbl_serdes_fn.c
new file mode 100644
index 000000000000..f085d5c41da1
--- /dev/null
+++ b/drivers/net/ethernet/hpe/sbl/sbl_serdes_fn.c
@@ -0,0 +1,4310 @@
+// SPDX-License-Identifier: GPL-2.0
+
+/* Copyright 2019-2026 Hewlett Packard Enterprise Development LP */
+
+#include <linux/kernel.h>
+#include <linux/device.h>
+#include <linux/delay.h>
+#include <linux/slab.h>
+
+#include <linux/hpe/sbl/sbl.h>
+#include <linux/hpe/sbl/sbl_kconfig.h>
+#include <linux/hpe/sbl/sbl_test.h>
+
+#include <linux/hpe/sbl/sbl_pml.h>
+
+#include <uapi/ethernet/sbl_sbm_constants.h>
+#include <uapi/ethernet/sbl_serdes_defaults.h>
+
+#include "sbl_constants.h"
+#include "sbl_serdes_map.h"
+#include "sbl_serdes.h"
+#include "sbl_sbm_serdes.h"
+#include "sbl_serdes_fn.h"
+#include "sbl_internal.h"
+#include "sbl_config_list.h"
+#include "sbl_pml_fn.h"
+
+static void sbl_send_serdes_fw_corruption_alert(struct sbl_inst *sbl, int port, int serdes)
+{
+	u32 alert_data;
+	struct serdes_info *serdes_info;
+
+	serdes_info = &sbl->switch_info->ports[port].serdes[serdes];
+	alert_data = ((serdes_info->sbus_ring & 0xffff) << 16) |
+		(serdes_info->rx_addr & 0xffff);
+
+	sbl_async_alert(sbl, port,
+			SBL_ASYNC_ALERT_SERDES_FW_CORRUPTION,
+			(void *)(uintptr_t)alert_data, 0);
+
+	/* Delay to allow userspace to get a serdes state dump */
+	msleep(SBL_SERDES_STATE_DUMP_DELAY);
+}
+
+static bool get_serdes_precoding(struct sbl_inst *sbl, int port_num)
+{
+	struct sbl_link *link = sbl->link + port_num;
+
+	if (sbl_debug_option(sbl, port_num, SBL_DEBUG_FORCE_PRECODING_ON))
+		return true;
+
+	if (sbl_debug_option(sbl, port_num, SBL_DEBUG_FORCE_PRECODING_OFF))
+		return false;
+
+	switch (link->blattr.precoding) {
+	case SBL_PRECODING_ON:
+		return true;
+	case SBL_PRECODING_OFF:
+		return false;
+	case SBL_PRECODING_DEFAULT:
+		return (link->blattr.options & SBL_OPT_FABRIC_LINK) ? true : false;
+	default:
+		sbl_dev_err(sbl->dev, "%d: invalid precoding (%d) - switching off",
+			    port_num, link->blattr.precoding);
+		/* switch off anyway */
+		return false;
+	}
+}
+
+/* Dummy function used when DEV_TRACE2 or DEV_TRACE3 are not defined */
+void dev_ignore(struct device *dev, const char *format, ...)
+{
+
+}
+
+/* Creates a u64 hash key based on the currently requested serdes state */
+/* NOTE: enums need to be bitwise values to use this hashing */
+static u64 sbl_get_tp_hash0(struct sbl_inst *sbl, int port_num)
+{
+	int media;
+
+	switch (sbl->link[port_num].mattr.media) {
+	case SBL_LINK_MEDIA_ELECTRICAL:
+		if (sbl->link[port_num].mattr.info & SBL_MEDIA_INFO_DIGITAL)
+			media = SBL_EXT_LINK_MEDIA_ELECTRICAL_ACT;
+		else
+			media = SBL_EXT_LINK_MEDIA_ELECTRICAL;
+
+		break;
+
+	case SBL_LINK_MEDIA_OPTICAL:
+		if (sbl->link[port_num].mattr.info & SBL_MEDIA_INFO_DIGITAL)
+			media = SBL_EXT_LINK_MEDIA_OPTICAL_DIGITAL;
+		else
+			media = SBL_EXT_LINK_MEDIA_OPTICAL_ANALOG;
+
+		break;
+
+	default:
+		media = SBL_EXT_LINK_MEDIA_ELECTRICAL;
+		break;
+	}
+
+	return sbl_create_tp_hash0(sbl->link[port_num].blattr.link_partner,
+			sbl->link[port_num].loopback_mode,
+			sbl->link[port_num].blattr.tuning_pattern,
+			sbl->link[port_num].link_mode,
+			media,
+			sbl->link[port_num].mattr.vendor);
+}
+
+static u64 sbl_get_tp_hash1(struct sbl_inst *sbl, int port_num)
+{
+	return sbl_create_tp_hash1(sbl->link[port_num].mattr.len);
+}
+
+void sbl_serdes_get_fw_vers(struct sbl_inst *sbl, int port_num, int serdes,
+			    uint *fw_rev, uint *fw_build)
+{
+	if (sbl_serdes_spico_int(sbl, port_num, serdes, SPICO_INT_CM4_REV_ID,
+				 SPICO_INT_DATA_NONE, (uint16_t *)fw_rev,
+				SPICO_INT_RETURN_RESULT)) {
+		/* Failure expected when Spico is in reset */
+		dev_dbg(sbl->dev, "p%ds%d: Failed to read firmware rev!", port_num, serdes);
+		*fw_rev = 0x0;
+	}
+	if (sbl_serdes_spico_int(sbl, port_num, serdes, SPICO_INT_CM4_BUILD_ID,
+				 SPICO_INT_DATA_NONE, (uint16_t *)fw_build,
+				SPICO_INT_RETURN_RESULT)) {
+		/* Failure expected when Spico is in reset */
+		dev_dbg(sbl->dev, "p%ds%d: Failed to read firmware build!", port_num, serdes);
+		*fw_build = 0x0;
+	}
+}
+
+/* Get mask of serdes used for target link mode */
+static u8 get_serdes_tx_mask(struct sbl_inst *sbl, int port_num)
+{
+	int serdes;
+	u8 serdes_mask = 0;
+
+	switch (sbl->link[port_num].link_mode) {
+	case SBL_LINK_MODE_CD_50G:
+		for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+			if (sbl->switch_info->ports[port_num].serdes[serdes].tx_lane_source == 0) {
+				serdes_mask |= 1 << serdes;
+				break;
+			}
+		}
+		break;
+	case SBL_LINK_MODE_CD_100G:
+		for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+			if ((sbl->switch_info->ports[port_num].serdes[serdes].tx_lane_source == 0) ||
+			    (sbl->switch_info->ports[port_num].serdes[serdes].tx_lane_source == 1)) {
+				serdes_mask |= 1 << serdes;
+			}
+		}
+		break;
+	case SBL_LINK_MODE_BS_200G:
+	case SBL_LINK_MODE_BJ_100G:
+		serdes_mask = 0xf;
+		break;
+	default:
+		sbl_dev_warn(sbl->dev, "%d: Unsupported link mode (%d)",
+			     port_num, sbl->link[port_num].link_mode);
+		serdes_mask = 0x0;
+		break;
+	}
+
+	return serdes_mask;
+}
+
+/* Get mask of serdes used for target link mode */
+static u8 get_serdes_rx_mask(struct sbl_inst *sbl, int port_num)
+{
+	int serdes;
+	u8 serdes_mask = 0;
+
+	/* if we are looped back then rx serdes are the same as the tx ones */
+	if (sbl->link[port_num].loopback_mode == SBL_LOOPBACK_MODE_LOCAL)
+		return get_serdes_tx_mask(sbl, port_num);
+
+	/* otherwise look them up */
+	switch (sbl->link[port_num].link_mode) {
+	case SBL_LINK_MODE_CD_50G:
+		for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+			if (sbl->switch_info->ports[port_num].serdes[serdes].rx_lane_source == 0) {
+				serdes_mask |= 1 << serdes;
+				break;
+			}
+		}
+		break;
+	case SBL_LINK_MODE_CD_100G:
+		for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+			if ((sbl->switch_info->ports[port_num].serdes[serdes].rx_lane_source == 0) ||
+			    (sbl->switch_info->ports[port_num].serdes[serdes].rx_lane_source == 1)) {
+				serdes_mask |= 1 << serdes;
+			}
+		}
+		break;
+	case SBL_LINK_MODE_BS_200G:
+	case SBL_LINK_MODE_BJ_100G:
+		serdes_mask = 0xf;
+		break;
+	default:
+		sbl_dev_warn(sbl->dev, "%d: Unsupported link mode (%d)",
+			     port_num, sbl->link[port_num].link_mode);
+		serdes_mask = 0x0;
+		break;
+	}
+
+	return serdes_mask;
+}
+
+/* Utility function to skip over irrelevant serdes lanes */
+static bool tx_serdes_required_for_link_mode(struct sbl_inst *sbl, int port_num,
+					     int serdes)
+{
+	u8 serdes_mask = get_serdes_tx_mask(sbl, port_num);
+
+	/* Enable physical lane 0 - this has the clock for all serdes and is
+	   always required. */
+	if ((serdes == 0) || (serdes_mask & (1 << serdes)))
+		return true;
+	else
+		return false;
+}
+
+/* Utility function to skip over irrelevant serdes lanes */
+static bool rx_serdes_required_for_link_mode(struct sbl_inst *sbl, int port_num,
+					     int serdes)
+{
+	u8 serdes_mask = get_serdes_rx_mask(sbl, port_num);
+
+	if (serdes_mask & (1 << serdes))
+		return true;
+	else
+		return false;
+}
+
+/* Returns a count of the number of bits set in input val */
+static int sbl_num_bits_set(u64 val)
+{
+	int bits_set = 0;
+	int i;
+
+	for (i = 0; i < 64; ++i) {
+		if (val & (1ULL << i))
+			bits_set++;
+	}
+	return bits_set;
+}
+
+/* Looks up sbl_sc_val struct for the given port, serdes, and hash */
+static int sbl_get_serdes_config_values(struct sbl_inst *sbl, int port_num,
+					int serdes, struct sbl_sc_values *vals)
+{
+	u64 hash0 = sbl_get_tp_hash0(sbl, port_num);
+	u64 hash1 = sbl_get_tp_hash1(sbl, port_num);
+	struct sbl_serdes_config *sc;
+	int num_ports_bits, num_serdes_bits, num_mask_bits;
+	int least_port_bits = 64;
+	int least_serdes_bits = 64;
+	int most_mask_bits = 0;
+	bool curr_best;
+	int rc = -1;
+
+	spin_lock(&sbl->serdes_config_lock);
+
+	list_for_each_entry(sc, &sbl->serdes_config_list, list) {
+
+		if ((sc->port_mask & (1ULL << port_num)) &&
+		    (sc->serdes_mask & (1ULL << serdes)) &&
+		    /* Ensure no bits are set in hash that are not set
+		       in tp_state_match for all bits included in the mask */
+		    (((sc->tp_state_mask0 & hash0) &
+		      ~(sc->tp_state_mask0 & sc->tp_state_match0)) == 0) &&
+		    (((sc->tp_state_mask1 & hash1) &
+		      ~(sc->tp_state_mask1 & sc->tp_state_match1)) == 0)) {
+			/* This is A match, but there may be >1. We want to
+			   choose the match that's most specific to this
+			   config. To determine this, we choose a match which:
+			   * [1] Has the least number of bits set in its
+			   port_mask
+			   * [2] If tie, has the least number of bits set in
+			    its serdes_mask
+			   * [3] If tie, has the most number bits set it its
+			    tp_state_mask0 and tp_state_mask1.
+			   * [4] If tie, pick the one with the lowest index */
+			sbl_dev_dbg(sbl->dev,
+				    "p%d: get values: hash0 0x%llx hash1 0x%llx matched 0x%llx 0x%llx, tag %d\n",
+				port_num, hash0, hash1, sc->tp_state_match0, sc->tp_state_match1, sc->tag);
+			curr_best = false;
+			num_ports_bits	= sbl_num_bits_set(sc->port_mask);
+			num_serdes_bits = sbl_num_bits_set(sc->serdes_mask);
+			num_mask_bits	= sbl_num_bits_set(sc->tp_state_mask0);
+			num_mask_bits  += sbl_num_bits_set(sc->tp_state_mask1);
+			if (num_ports_bits < least_port_bits) {
+				/* [1] */
+				curr_best = true;
+			} else if (num_ports_bits == least_port_bits) {
+				if (num_serdes_bits < least_serdes_bits) {
+					/* [2] */
+					curr_best = true;
+				} else if (num_serdes_bits == least_serdes_bits) {
+					if (num_mask_bits > most_mask_bits) {
+						/* [3] */
+						curr_best = true;
+					}
+				}
+			}
+
+			if (curr_best) {
+				sbl_dev_dbg(sbl->dev, "p%d: tag %d is current best match\n", port_num, sc->tag);
+				*vals = sc->vals;
+				least_port_bits = num_ports_bits;
+				least_serdes_bits = num_serdes_bits;
+				most_mask_bits = num_mask_bits;
+				rc = 0;
+			}
+		}
+	}
+	spin_unlock(&sbl->serdes_config_lock);
+
+	if (rc == 0)
+		return 0;
+
+	sbl_dev_err(sbl->dev, "%d: get values: no match for hash0 0x%llx hash1 0x%llx\n",
+		    port_num, hash0, hash1);
+	return -ENOENT;
+}
+
+/* Checks if there are valid tuning params in the sbl struct which can
+ * be used for this serdes tune
+ */
+static bool sbl_is_retune(struct sbl_inst *sbl, int port_num)
+{
+	int serdes, i;
+	u64 tp_state_hash0, tp_state_hash1;
+	struct sbl_link *link = sbl->link + port_num;
+
+	DEV_TRACE2(sbl->dev, "p%d", port_num);
+
+	/* Reuse of cached tuning params are disabled until AOC sync is implemented */
+	if (link->mattr.media == SBL_LINK_MEDIA_OPTICAL)
+		return false;
+
+	/* Check tuning params are for this target configuration */
+	tp_state_hash0 = sbl_get_tp_hash0(sbl, port_num);
+	tp_state_hash1 = sbl_get_tp_hash1(sbl, port_num);
+	if ((sbl->link[port_num].tuning_params.tp_state_hash0 != tp_state_hash0) ||
+	    (sbl->link[port_num].tuning_params.tp_state_hash1 != tp_state_hash1)) {
+		sbl_dev_dbg(sbl->dev,
+			    "p%d: tuning param mismatch (saved: 0x%llx 0x%llx curr:0x%llx 0x%llx) - not retune\n",
+			port_num, sbl->link[port_num].tuning_params.tp_state_hash0,
+			sbl->link[port_num].tuning_params.tp_state_hash1, tp_state_hash0, tp_state_hash1);
+		return false;
+	}
+
+	/* Check that we actually have tuning params to apply */
+	for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+		if (!(rx_serdes_required_for_link_mode(sbl, port_num, serdes) ||
+		      tx_serdes_required_for_link_mode(sbl, port_num, serdes)))
+			continue;
+		for (i = 0; i < NUM_CTLE_PARAMS; ++i) {
+			if (sbl->link[port_num].tuning_params.params[serdes].ctle[i]) {
+				sbl_dev_dbg(sbl->dev, "p%d: tuning params OK - retune\n", port_num);
+				return true;
+			}
+		}
+		for (i = 0; i < NUM_FFE_PARAMS; ++i) {
+			if (sbl->link[port_num].tuning_params.params[serdes].ffe[i]) {
+				sbl_dev_dbg(sbl->dev, "p%d: tuning params OK - retune\n", port_num);
+				return true;
+			}
+		}
+		for (i = 0; i < NUM_DFE_PARAMS; ++i) {
+			if (sbl->link[port_num].tuning_params.params[serdes].dfe[i]) {
+				sbl_dev_dbg(sbl->dev, "p%d: tuning params OK - retune\n", port_num);
+				return true;
+			}
+		}
+		/* Could continue on to check vernier, etc., but if all the above are 0,
+		    we've got other problems. */
+	}
+
+	sbl_dev_warn(sbl->dev, "p%d: Saved tuning parameters but all 0-forcing retune", port_num);
+
+	return false;
+}
+
+/* Parse the vers string into rev and build */
+static int sbl_parse_version_string(struct sbl_inst *sbl, const char *fw_fname,
+				    int *fw_rev, int *fw_build)
+{
+	char  rev_str[SBL_MAX_STR_LEN];
+	char  build_str[SBL_MAX_STR_LEN];
+	char  *p;
+	int err;
+
+	p = strstr(fw_fname, ".");
+	if (p == NULL) {
+		sbl_dev_err(sbl->dev, "Bad firmware file name: %s", fw_fname);
+		return -EINVAL;
+	}
+
+	if (strlen(p) < strlen(".0x0000_0000")) {
+		sbl_dev_err(sbl->dev, "Bad firmware file name: %s", fw_fname);
+		return -EINVAL;
+	}
+
+	memcpy(rev_str, &(p[3]), SBL_FW_REV_LEN);
+	rev_str[SBL_FW_REV_LEN] = '\n';
+	rev_str[SBL_FW_REV_LEN + 1] = '\0';
+	memcpy(build_str, &(p[8]), SBL_FW_BUILD_LEN);
+	build_str[SBL_FW_BUILD_LEN] = '\n';
+	build_str[SBL_FW_BUILD_LEN + 1] = '\0';
+
+	err = kstrtou32(rev_str, 16, fw_rev);
+	if (err) {
+		sbl_dev_err(sbl->dev, "Failed to convert %s to an integer [%d]", rev_str, err);
+		return err;
+	}
+
+	err = kstrtou32(build_str, 16, fw_build);
+	if (err) {
+		sbl_dev_err(sbl->dev, "Failed to convert %s to an integer [%d]", build_str, err);
+		return err;
+	}
+	return 0;
+}
+
+void sbl_sbm_get_fw_vers(struct sbl_inst *sbl, int sbus_ring, uint *fw_rev, uint *fw_build)
+{
+	uint sbus_addr = SBUS_ADDR(sbus_ring, SBUS_BCAST_SBM_SPICO);
+
+	/* SBUS Critical Section */
+	mutex_lock(&sbl->sbus_ring_mtx[sbus_ring]);
+
+	if (sbl_sbm_spico_int(sbl, sbus_addr, SPICO_INT_SBMS_REV_ID,
+			      SPICO_INT_DATA_NONE, fw_rev)) {
+		/* Failure expected when Spico is in reset */
+		dev_dbg(sbl->dev, "sbm%d: Failed to read firmware rev from 0x%x", sbus_ring, sbus_addr);
+		*fw_rev = 0x0;
+	}
+	if (sbl_sbm_spico_int(sbl, sbus_addr, SPICO_INT_SBMS_BUILD_ID,
+			      SPICO_INT_DATA_NONE, fw_build)) {
+		/* Failure expected when Spico is in reset */
+		dev_dbg(sbl->dev, "sbm%d: Failed to read firmware build from 0x%x", sbus_ring, sbus_addr);
+		*fw_build = 0x0;
+	}
+	mutex_unlock(&sbl->sbus_ring_mtx[sbus_ring]);
+}
+
+int sbl_sbm_firmware_flash(struct sbl_inst *sbl)
+{
+	int err;
+
+	err = sbl_sbm_firmware_flash_ring(sbl, 0,
+					  sbl->switch_info->num_sbus_rings - 1, false);
+	if (err)
+		/* sending port 0 in event as this event doesn't apply to any specific port */
+		sbl_async_alert(sbl, 0, SBL_ASYNC_ALERT_SBM_FW_LOAD_FAILURE, NULL, 0);
+
+	return err;
+}
+
+int sbl_sbm_firmware_flash_ring(struct sbl_inst *sbl, int first_ring,
+				int last_ring, bool force)
+{
+	bool flash_needed, fw_requested = false;
+	int sbus_ring;
+	int err;
+	int fw_rev, fw_build;
+	const struct firmware *fw = NULL;
+
+	if ((last_ring < first_ring) || (first_ring < 0) ||
+	    (last_ring > sbl->switch_info->num_sbus_rings - 1)) {
+		sbl_dev_err(sbl->dev, "Invalid rings specified first:%d last:%d",
+			    first_ring, last_ring);
+		return -EINVAL;
+	}
+
+	err = sbl_parse_version_string(sbl, sbl->iattr.sbm_fw_fname,
+				       &fw_rev, &fw_build);
+	if (err) {
+		sbl_dev_err(sbl->dev, "Failed to parse version string %s [%d]",
+			    sbl->iattr.sbm_fw_fname, err);
+		return err;
+	}
+
+	/* Check SBus Master firmware versions */
+	for (sbus_ring = first_ring; sbus_ring <= last_ring; ++sbus_ring) {
+		flash_needed = false;
+		if (sbl_validate_sbm_fw_vers(sbl, sbus_ring, fw_rev,
+					     fw_build)) {
+			flash_needed = true;
+		}
+
+		if (flash_needed || force) {
+			if (!fw_requested) {
+				err = request_firmware(&fw, sbl->iattr.sbm_fw_fname,
+						       sbl->dev);
+				if (err) {
+					sbl_dev_err(sbl->dev, "firmware request failed [%d]", err);
+					return err;
+				}
+				sbl_dev_dbg(sbl->dev, "loaded fw (size %zd)", fw->size);
+				fw_requested = true;
+			}
+			sbl_dev_dbg(sbl->dev, "ring %d sbus_master firmware out of date! Flashing...", sbus_ring);
+
+			err = sbl_sbm_firm_upload(sbl, sbus_ring, fw->size, fw->data);
+			if (err) {
+				sbl_dev_err(sbl->dev, "Failed to upload ring %d firmware!", sbus_ring);
+				goto out_release;
+			} else {
+				sbl_dev_info(sbl->dev, "Ring %d Sbus Master firmware flashed successfully.", sbus_ring);
+			}
+		}
+	}
+
+ out_release:
+	release_firmware(fw);
+
+	return err;
+}
+
+static void sbl_serdes_firmware_validation(struct sbl_inst *sbl, const struct firmware *fw,
+					   u32 sbus_addr, u32 result, u32 sbus_ring)
+{
+	int rc, i;
+	u8 data;
+	bool corruption_found = false;
+
+	for (i = 0; i < fw->size; ++i) {
+		if (i % 2 == 0) {
+			rc = sbl_sbus_wr(sbl, sbus_addr,
+					SPICO_SBR_ADDR_IMEM, i / 2);
+			if (rc)
+				break;
+			rc = sbl_sbus_rd(sbl, sbus_addr,
+					 SPICO_SBR_ADDR_RDATA,
+					&result);
+			if (rc)
+				break;
+			data = (result & 0xff00) >> 8;
+		} else {
+			data = result & 0xff;
+		}
+		if (data != fw->data[i]) {
+			corruption_found = true;
+			sbl_dev_warn(sbl->dev, "0x%x: Act 0x%4.4x Exp 0x%4.4x", i, data, fw->data[i]);
+		}
+	}
+	rc = sbl_sbus_wr(sbl, sbus_addr, SPICO_SBR_ADDR_CTL,
+			 SPICO_SBR_DATA_IMEM_CNTL_DIS);
+	if (rc)
+		sbl_dev_err(sbl->dev, "SBM Imem rd disable failed [%d]", rc);
+	if (corruption_found)
+		sbl_dev_err(sbl->dev, "r%d: SBM FW corruption found", sbus_ring);
+	else
+		sbl_dev_info(sbl->dev, "r%d: No SBM FW corruption found", sbus_ring);
+}
+
+int sbl_serdes_firmware_flash_safe(struct sbl_inst *sbl, int port_num,
+				   bool force)
+{
+	int rc;
+	u32 sbus_addr, crc_result, sbus_ring;
+	u32 curr_fw_rev = 0, curr_fw_build = 0;
+	u32 result;
+	const struct firmware *fw = NULL;
+	int fw_rev, fw_build;
+	int serdes = 0;
+
+	if (!sbl->is_hw)
+		return 0;
+
+	if (port_num == SBL_ALL_PORTS)
+		return -ENOTSUPP;
+
+	sbus_ring = sbl->switch_info->ports[port_num].serdes[serdes].sbus_ring;
+
+	/* First, try the FW flash */
+	rc = sbl_serdes_firmware_flash(sbl, port_num, force);
+	if (rc)
+		goto sbm_fw_reload;
+
+	/* Now, validate the SerDes FW - this also validates
+	 * SPICO interrupts are working correctly.
+	 */
+	rc = sbl_parse_version_string(sbl, sbl->iattr.serdes_fw_fname,
+				      &fw_rev, &fw_build);
+	if (rc) {
+		sbl_dev_err(sbl->dev,
+			    "Failed to parse version string %s [%d]",
+			sbl->iattr.sbm_fw_fname, rc);
+		return rc;
+	}
+	rc = sbl_validate_serdes_fw_vers(sbl, port_num, serdes, fw_rev, fw_build);
+	if (rc)
+		goto sbm_fw_reload;
+
+	/* Finally, validate the SBM FW - this also validates
+	 * Sbus reads/writes are working correctly.
+	 */
+	rc = sbl_parse_version_string(sbl, sbl->iattr.sbm_fw_fname,
+				      &fw_rev, &fw_build);
+	if (rc) {
+		sbl_dev_err(sbl->dev,
+			    "Failed to parse version string %s [%d]",
+			sbl->iattr.sbm_fw_fname, rc);
+		return rc;
+	}
+	rc = sbl_validate_sbm_fw_vers(sbl, sbus_ring, fw_rev, fw_build);
+	if (rc)
+		goto sbm_fw_reload;
+
+	/* If all the above succeed, we're done unless we force SBM FW reload */
+	if (sbl_debug_option(sbl, port_num,
+			     SBL_DEBUG_FORCE_RELOAD_SBM_FW)) {
+		sbl_dev_info(sbl->dev, "p%d: SBus Master FW reload forced", port_num);
+		goto sbm_fw_reload;
+	}
+	return 0;
+
+ sbm_fw_reload:
+	if (sbl_debug_option(sbl, port_num, SBL_DEBUG_INHIBIT_RELOAD_SBM_FW)) {
+		sbl_dev_warn(sbl->dev, "p%d: SBus Master FW reload inhibited", port_num);
+		return rc;
+	}
+
+	/* We may trigger a sbus master FW reload from multiple ports at the
+	 * same time. Ensure we only actually reload the firmware once per
+	 * sbus ring
+	 */
+	sbl->reload_sbm_fw[sbus_ring] = true;
+	mutex_lock(SBM_FW_MTX(sbl, sbus_ring));
+	if (!sbl->reload_sbm_fw[sbus_ring]) {
+		sbl_dev_info(sbl->dev, "r%d: Sbus master FW reload no longer needed", sbus_ring);
+	} else {
+
+		if (mutex_is_locked(SBUS_RING_MTX(sbl, sbus_ring)))
+			sbl_dev_dbg(sbl->dev,
+				    "%s: Sbus contention detected, sbus_ring_mtx[%d] locked",
+				    __func__, sbus_ring);
+
+		/* SBUS Critical Section */
+		mutex_lock(SBUS_RING_MTX(sbl, sbus_ring));
+
+		/* First, dump the SBM FW info for debug */
+		sbus_addr = SBUS_ADDR(sbus_ring, SBUS_BCAST_SBM_SPICO);
+		/* Version info: Rev.Build */
+		if (sbl_sbm_spico_int(sbl, sbus_addr, SPICO_INT_SBMS_REV_ID,
+				      SPICO_INT_DATA_NONE, &curr_fw_rev)) {
+			sbl_dev_warn(sbl->dev,
+				     "r%d: Failed to read firmware rev from 0x%x",
+				 sbus_ring, sbus_addr);
+		} else {
+			sbl_dev_info(sbl->dev, "r%d: firmware rev 0x%x", sbus_ring, curr_fw_rev);
+		}
+		if (sbl_sbm_spico_int(sbl, sbus_addr, SPICO_INT_SBMS_BUILD_ID,
+				      SPICO_INT_DATA_NONE, &curr_fw_build)) {
+			sbl_dev_warn(sbl->dev,
+				     "r%d: Failed to read firmware build from 0x%x",
+				 sbus_ring, sbus_addr);
+		} else {
+			sbl_dev_info(sbl->dev, "r%d: firmware build 0x%x",
+				     sbus_ring, curr_fw_build);
+		}
+		/* CRC */
+		rc = sbl_sbm_spico_int(sbl, sbus_addr, SPICO_INT_SBMS_DO_CRC,
+				       SPICO_INT_DATA_NONE, &crc_result);
+		if (rc) {
+			sbl_dev_err(sbl->dev,
+				    "p%d(0x%x): CRC check interrupt failed (%d)!",
+				port_num, sbus_addr, rc);
+		} else {
+			if (crc_result != SPICO_RESULT_SBR_CRC_PASS) {
+				sbl_dev_err(sbl->dev,
+					    "p%d(0x%x): CRC check fail (result: 0x%x exp: 0x%x)!",
+					port_num, sbus_addr, crc_result, SPICO_RESULT_SBR_CRC_PASS);
+			} else {
+				sbl_dev_info(sbl->dev,
+					     "p%d(0x%x): CRC check passed",
+					 port_num, sbus_addr);
+			}
+		}
+		/* FW Status */
+		rc = sbl_sbus_rd(sbl, sbus_addr, SPICO_INT_SBMS_FW_STS,
+				 &result);
+		if (rc) {
+			sbl_dev_err(sbl->dev,
+				    "p%d(0x%x): FW status read failed (%d)!",
+				port_num, sbus_addr, rc);
+		} else {
+			sbl_dev_info(sbl->dev, "p%d(0x%x): FW status: 0x%x",
+				     port_num, sbus_addr, result);
+		}
+
+		rc = request_firmware(&fw, sbl->iattr.sbm_fw_fname, sbl->dev);
+		if (rc != 0) {
+			sbl_dev_err(sbl->dev, "firmware request failed [%d]", rc);
+		} else {
+			sbl_dev_info(sbl->dev,
+				     "p%d(0x%x): Checking SBM FW for corruption...",
+				 port_num, sbus_addr);
+			rc = sbl_sbus_wr(sbl, sbus_addr, SPICO_SBR_ADDR_CTL,
+					 SPICO_SBR_DATA_IMEM_CNTL_EN_RD);
+			if (rc)
+				sbl_dev_err(sbl->dev, "SBM Imem rd enable failed [%d]", rc);
+			else
+				sbl_serdes_firmware_validation(sbl, fw, sbus_addr, result, sbus_ring);
+
+			release_firmware(fw);
+		}
+
+		mutex_unlock(SBUS_RING_MTX(sbl, sbus_ring));
+
+		/* Now, try reloading the sbus master FW */
+		rc = sbl_sbm_firmware_flash_ring(sbl, sbus_ring, sbus_ring, true);
+		if (rc)
+			sbl_dev_err(sbl->dev, "r%d: SBM FW flash failed (%d)!", sbus_ring, rc);
+		else
+			sbl_dev_info(sbl->dev, "r%d: SBM FW flash succeeded", sbus_ring);
+
+		sbl->reload_sbm_fw[sbus_ring] = false;
+	}
+	mutex_unlock(SBM_FW_MTX(sbl, sbus_ring));
+
+	/* Finally, retry the SerDes FW reload */
+	rc = sbl_serdes_firmware_flash(sbl, port_num, true);
+	if (rc != 0)
+		sbl_dev_err(sbl->dev,
+			    "p%d: SerDes FW flash failed (%d) despite SBM reload!",
+			port_num, rc);
+	else
+		sbl_dev_info(sbl->dev, "p%d: SerDes FW flash succeeded", port_num);
+
+	/* Regardless of the success/failure of the initial flash and recovery
+	 * attempt, we return the status of the final serdes firmware flash,
+	 * which is the true metric of if this function was successful.
+	 */
+	return rc;
+}
+
+int sbl_serdes_firmware_flash(struct sbl_inst *sbl, int port_num, bool force)
+{
+	bool flash_needed;
+	int serdes = 0;
+	int sr, err = 0;
+	int fw_rev, fw_build;
+	int port = port_num;
+	int first_port, last_port;
+	const struct firmware *fw = NULL;
+
+	/* Lock sbm_fw_mtx to ensure we don't reload the sbus master FW
+	 * while reloading the SerDes FW
+	 */
+	if (port_num == SBL_ALL_PORTS) {
+		for (sr = 0; sr < sbl->switch_info->num_sbus_rings; ++sr)
+			mutex_lock(SBM_FW_MTX(sbl, sr));
+	} else {
+		sr = sbl->switch_info->ports[port_num].serdes[0].sbus_ring;
+		mutex_lock(SBM_FW_MTX(sbl, sr));
+	}
+
+	if (!force) {
+		err = sbl_parse_version_string(sbl, sbl->iattr.serdes_fw_fname,
+					       &fw_rev, &fw_build);
+		if (err) {
+			sbl_dev_err(sbl->dev,
+				    "Failed to parse version string %s [%d]",
+				sbl->iattr.serdes_fw_fname, err);
+			goto out;
+		}
+
+		/* Check SerDes firmware versions */
+		flash_needed = false;
+		if (port_num == SBL_ALL_PORTS) {
+			first_port = 0;
+			last_port = sbl->switch_info->num_ports - 1;
+		} else {
+			first_port = port_num;
+			last_port = port_num;
+		}
+		for (port = first_port; port <= last_port; ++port) {
+			for (serdes = 0; serdes < sbl->switch_info->num_serdes;
+			     ++serdes) {
+				if (sbl_validate_serdes_fw_vers(sbl, port,
+								serdes, fw_rev,
+								fw_build)) {
+					sbl_dev_info(sbl->dev, "port %d serdes: %d firmware out of date! Flash required",
+						     port, serdes);
+					flash_needed = true;
+					break;
+				}
+			}
+			if (flash_needed)
+				break;
+		}
+	} else {
+		flash_needed = true;
+	}
+
+	if (flash_needed) {
+		err = request_firmware(&fw, sbl->iattr.serdes_fw_fname, sbl->dev);
+		if (err) {
+			sbl_dev_err(sbl->dev, "firmware request failed [%d]",
+				    err);
+			goto out;
+		}
+
+		err = sbl_serdes_firm_upload(sbl, port_num, fw->size, fw->data);
+		if (err) {
+			sbl_dev_err(sbl->dev, "%d: serdes firmware upload failed [%d]", port_num, err);
+			if (port_num == SBL_ALL_PORTS)
+				sbl_send_serdes_fw_corruption_alert(sbl, 0, 0);
+			else
+				sbl_send_serdes_fw_corruption_alert(sbl, port, serdes);
+			goto out;
+		} else {
+			if (port_num == SBL_ALL_PORTS)
+				sbl_dev_dbg(sbl->dev, "All SerDes firmware flashed successfully.");
+			else
+				sbl_dev_dbg(sbl->dev, "p%d SerDes firmware flashed successfully.", port_num);
+		}
+	} else {
+		/* Serdes firmware reload skipped as the firmware validation succeeded */
+		for (port = first_port; port <= last_port; ++port)
+			sbl_link_counters_incr(sbl, port, serdes_fw_reload_skip);
+	}
+
+ out:
+	release_firmware(fw);
+
+	if (port_num == SBL_ALL_PORTS) {
+		for (sr = 0; sr < sbl->switch_info->num_sbus_rings; ++sr)
+			mutex_unlock(SBM_FW_MTX(sbl, sr));
+	} else {
+		mutex_unlock(SBM_FW_MTX(sbl, sr));
+	}
+	return err;
+}
+
+int sbl_validate_sbm_fw_vers(struct sbl_inst *sbl, u32 sbus_ring,
+			     int fw_rev, int fw_build)
+{
+	u32 curr_fw_rev = 0, curr_fw_build = 0;
+	u32 sbus_addr = SBUS_ADDR(sbus_ring, SBUS_BCAST_SBM_SPICO);
+
+	DEV_TRACE2(sbl->dev,
+		   "sbus_addr: 0x%x, desired rev: 0x%x, desired build: 0x%x",
+		   sbus_ring, fw_rev, fw_build);
+
+	/* SBUS Critical Section */
+	mutex_lock(SBUS_RING_MTX(sbl, sbus_ring));
+
+	if (sbl_sbm_spico_int(sbl, sbus_addr, SPICO_INT_SBMS_REV_ID,
+			      SPICO_INT_DATA_NONE, &curr_fw_rev)) {
+		/* Failure expected when Spico is in reset */
+		sbl_dev_warn(sbl->dev, "r%d: Failed to read SBM firmware rev from 0x%x",
+			     sbus_ring, sbus_addr);
+	}
+	if (sbl_sbm_spico_int(sbl, sbus_addr, SPICO_INT_SBMS_BUILD_ID,
+			      SPICO_INT_DATA_NONE, &curr_fw_build)) {
+		/* Failure expected when Spico is in reset */
+		sbl_dev_warn(sbl->dev, "r%d: Failed to read SBM firmware build from 0x%x",
+			     sbus_ring, sbus_addr);
+	}
+
+	mutex_unlock(SBUS_RING_MTX(sbl, sbus_ring));
+
+	if (((int)curr_fw_rev == fw_rev) && ((int)curr_fw_build == fw_build)) {
+		sbl_dev_dbg(sbl->dev, "r%d: Found expected SBM rev: 0x%x_%x",
+			    sbus_ring, curr_fw_rev, curr_fw_build);
+		return 0;
+	}
+	sbl_dev_warn(sbl->dev,
+		     "r%d: Expected rev: 0x%x_%x Current SBM rev: 0x%x_%x",
+			sbus_ring, fw_rev, fw_build, curr_fw_rev, curr_fw_build);
+	return -1;
+}
+
+int sbl_validate_serdes_fw_crc(struct sbl_inst *sbl, int port, int serdes)
+{
+	u16 crc_result;
+	int err;
+
+	err = sbl_serdes_spico_int(sbl, port, serdes, SPICO_INT_CM4_CRC,
+				   SPICO_INT_DATA_NONE, &crc_result,
+					SPICO_INT_RETURN_RESULT);
+	if (err)
+		return err;
+
+	/* CRC failure can be injected for test purposes */
+	sbl_test_manipulate_serdes_fw_crc_result(&crc_result);
+
+	if (crc_result != SPICO_RESULT_SERDES_CRC_PASS) {
+		sbl_dev_dbg(sbl->dev,
+			    "p%ds%d: CRC check fail (result 0x%x, expected 0x%x)!",
+			port, serdes, crc_result, SPICO_RESULT_SERDES_CRC_PASS);
+		return -EBADE;
+	}
+
+	return 0;
+}
+
+int sbl_validate_serdes_fw_vers(struct sbl_inst *sbl, int port_num, int serdes,
+				int fw_rev, int fw_build)
+{
+	u16 curr_fw_rev = 0, curr_fw_build = 0;
+	int err;
+
+	DEV_TRACE2(sbl->dev, "p%ds%d: desired rev: 0x%x, desired build: 0x%x",
+		   port_num, serdes, fw_rev, fw_build);
+
+	if (!sbl->is_hw)
+		return 0;
+
+	if (sbl_serdes_spico_int(sbl, port_num, serdes, SPICO_INT_CM4_REV_ID,
+				 SPICO_INT_DATA_NONE, &curr_fw_rev,
+				 SPICO_INT_RETURN_RESULT)) {
+		/* Failure expected when Spico is in reset */
+		sbl_dev_dbg(sbl->dev, "p%ds%d: Failed to read firmware rev!",
+			    port_num, serdes);
+	}
+	if (sbl_serdes_spico_int(sbl, port_num, serdes, SPICO_INT_CM4_BUILD_ID,
+				 SPICO_INT_DATA_NONE, &curr_fw_build,
+				 SPICO_INT_RETURN_RESULT)) {
+		/* Failure expected when Spico is in reset */
+		sbl_dev_dbg(sbl->dev, "p%ds%d: Failed to read firmware build!",
+			    port_num, serdes);
+	}
+
+	err = sbl_validate_serdes_fw_crc(sbl, port_num, serdes);
+	if (err) {
+		sbl_dev_warn(sbl->dev, "p%ds%d: Failed CRC check",
+			     port_num, serdes);
+		return -1;
+	}
+
+	if (((int)curr_fw_rev == fw_rev) && ((int)curr_fw_build == fw_build)) {
+		sbl_dev_dbg(sbl->dev, "p%ds%d: Found expected rev: 0x%x_%x",
+			    port_num, serdes, curr_fw_rev, curr_fw_build);
+		return 0;
+	}
+	sbl_dev_warn(sbl->dev, "p%ds%d: Expected rev: 0x%x_%x Current rev: 0x%x_%x",
+		     port_num, serdes, fw_rev, fw_build,
+			curr_fw_rev, curr_fw_build);
+	return -1;
+}
+
+/* does the CRC check */
+int sbl_sbm_firm_upload(struct sbl_inst *sbl, int sbus_ring,
+			size_t fw_size, const u8 *fw_data)
+{
+	u32 sbus_addr, unused, crc_result;
+	int err;
+
+	if (!sbl->is_hw)
+		return 0;
+
+	/* SBUS Critical Section */
+	mutex_lock(SBUS_RING_MTX(sbl, sbus_ring));
+
+	sbus_addr = SBUS_ADDR(sbus_ring, SBUS_BCAST_SBM_SPICO);
+
+	err = sbl_sbus_op_aux(sbl, sbus_addr, SPICO_SBR_ADDR_IP_IDCODE,
+			      SBUS_IFACE_DST_CORE | SBUS_CMD_RESET,
+				   SPICO_SBR_DATA_NONE, &unused);
+	if (err)
+		goto err_out;
+
+	err = sbl_sbus_wr(sbl, sbus_addr, SPICO_SBR_ADDR_CTL,
+			  SPICO_SBR_DATA_RESET_HIGH);
+	if (err)
+		goto err_out;
+
+	err = sbl_sbus_wr(sbl, sbus_addr, SPICO_SBR_ADDR_CTL,
+			  SPICO_SBR_DATA_RESET_LOW);
+	if (err)
+		goto err_out;
+
+	err = sbl_sbus_wr(sbl, sbus_addr, SPICO_SBR_ADDR_CTL,
+			  SPICO_SBR_DATA_IMEM_CNTL_EN);
+	if (err)
+		goto err_out;
+
+	err = sbl_sbus_wr(sbl, sbus_addr, SPICO_SBR_ADDR_IMEM,
+			  SPICO_SBR_DATA_NONE);
+	if (err)
+		goto err_out;
+
+	err = sbl_sbus_wr(sbl, sbus_addr, SPICO_SBR_ADDR_IMEM,
+			  SPICO_SBR_DATA_SET_BURST_WR);
+	if (err)
+		goto err_out;
+
+	err = sbl_spico_burst_upload(sbl, sbus_addr,
+				     SPICO_SBR_ADDR_IMEM_BURST_DATA,
+					  fw_size, fw_data);
+	if (err) {
+		sbl_dev_err(sbl->dev, "Upload failed!");
+		goto err_out;
+	}
+
+	err = sbl_sbus_wr(sbl, sbus_addr, SPICO_SBR_ADDR_IMEM,
+			  SPICO_SBR_DATA_SET_BURST_WR | fw_size);
+	if (err)
+		goto err_out;
+
+	err = sbl_sbus_wr(sbl, sbus_addr, SPICO_SBR_ADDR_IMEM,
+			  SPICO_SBR_DATA_SET_BURST_WR |
+			       (fw_size + 1));
+	if (err)
+		goto err_out;
+
+	err = sbl_sbus_wr(sbl, sbus_addr, SPICO_SBR_ADDR_IMEM,
+			  SPICO_SBR_DATA_SET_BURST_WR |
+			       (fw_size + 2));
+	if (err)
+		goto err_out;
+
+	err = sbl_sbus_wr(sbl, sbus_addr, SPICO_SBR_ADDR_IMEM,
+			  SPICO_SBR_DATA_SET_BURST_WR |
+			       (fw_size + 3));
+	if (err)
+		goto err_out;
+
+	err = sbl_sbus_wr(sbl, sbus_addr, SPICO_SBR_ADDR_CTL,
+			  SPICO_SBR_DATA_IMEM_CNTL_DIS);
+	if (err)
+		goto err_out;
+
+	err = sbl_sbus_wr(sbl, sbus_addr, SPICO_SBR_ADDR_ECC,
+			  SPICO_SBR_DATA_ECC_EN);
+	if (err)
+		goto err_out;
+
+	err = sbl_sbus_wr(sbl, sbus_addr, SPICO_SBR_ADDR_CTL,
+			  SPICO_SBR_DATA_SPICO_EN);
+	if (err)
+		goto err_out;
+
+	err = sbl_sbm_spico_int(sbl, sbus_addr, SPICO_INT_SBMS_DO_CRC,
+				SPICO_INT_DATA_NONE, &crc_result);
+	if (err)
+		goto err_out;
+
+	if (crc_result != SPICO_RESULT_SBR_CRC_PASS) {
+		sbl_dev_err(sbl->dev, "0x%x: CRC check fail (result 0x%x, expected 0x%x)!",
+			    sbus_addr, crc_result, SPICO_RESULT_SBR_CRC_PASS);
+		err = -EBADE;
+	}
+
+	/* Increment sbus master fw reload counter */
+	atomic_inc(&sbl->sbm_fw_reload_count[sbus_ring]);
+
+err_out:
+	mutex_unlock(SBUS_RING_MTX(sbl, sbus_ring));
+	return err;
+}
+
+int sbl_serdes_firm_upload(struct sbl_inst *sbl, int port_num, size_t fw_size,
+			   const u8 *fw_data)
+{
+	int serdes;
+	u32 sbus_addr;
+	u64 core_status_value;
+	int sbus_ring;
+	int err = -1;
+	int port, first_port, last_port, first_ring, last_ring;
+	int first_serdes, last_serdes;
+	unsigned long last_jiffy;
+
+	if (port_num == SBL_ALL_PORTS) {
+		sbl_dev_dbg(sbl->dev, "Loading SerDes firmware for all ports...");
+		first_port = 0;
+		last_port = sbl->switch_info->num_ports - 1;
+		first_serdes = 0; /* force to a single iteration */
+		last_serdes = 0;
+		first_ring = 0;
+		last_ring = sbl->switch_info->num_sbus_rings - 1;
+	} else {
+		sbl_dev_dbg(sbl->dev, "p%d: Loading SerDes firmware...", port_num);
+		first_port = port_num;
+		last_port = port_num;
+		first_serdes = 0;
+		last_serdes = sbl->switch_info->num_serdes - 1;
+		/* All serdes for a given port are always on the same ring, so
+		   just use serdes 0 to determine the ring. */
+		first_ring = sbl->switch_info->ports[port_num].serdes[0].sbus_ring;
+		last_ring = sbl->switch_info->ports[port_num].serdes[0].sbus_ring;
+	}
+	for (port = first_port; port <= last_port; ++port) {
+		for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+			err = sbl_serdes_soft_reset(sbl, port, serdes);
+			if (err)
+				return err;
+		}
+	}
+
+	/* Don't allow SPICO interrupts while we are reloading firmware */
+	for (port = first_port; port <= last_port; ++port)
+		mutex_lock(&sbl->link[port].serdes_mtx);
+
+	sbl_dev_dbg(sbl->dev, "p%d: Flashing SerDes firmware...", port_num);
+	for (sbus_ring = first_ring; sbus_ring <= last_ring; ++sbus_ring) {
+
+		/* SBUS Critical Section */
+		mutex_lock(SBUS_RING_MTX(sbl, sbus_ring));
+
+		for (serdes = first_serdes; serdes <= last_serdes; ++serdes) {
+			if (port_num == SBL_ALL_PORTS)
+				sbus_addr = SBUS_ADDR(sbus_ring, SBUS_BCAST_CM4_SERDES_SPICO);
+			else
+				sbus_addr = SBUS_ADDR(sbl->switch_info->ports[port_num].serdes[serdes].sbus_ring,
+						      sbl->switch_info->ports[port_num].serdes[serdes].rx_addr);
+
+			err = sbl_sbus_wr(sbl, sbus_addr,
+					  SPICO_SERDES_ADDR_RESET_EN,
+				       SPICO_SERDES_DATA_SET_GLOBAL_RESET);
+			if (err)
+				break;
+
+			err = sbl_sbus_wr(sbl, sbus_addr,
+					  SPICO_SERDES_ADDR_RESET_EN,
+				       SPICO_SERDES_DATA_CLR_GLOBAL_RESET);
+			if (err)
+				break;
+
+			err = sbl_sbus_wr(sbl, sbus_addr,
+					  SPICO_SERDES_ADDR_INTR_DIS,
+				       SPICO_SERDES_DATA_SET_INTR_DIS);
+			if (err)
+				break;
+
+			err = sbl_sbus_wr(sbl, sbus_addr,
+					  SPICO_SERDES_ADDR_IMEM,
+				       SPICO_SERDES_DATA_SET_IMEM_CNTL_EN);
+			if (err)
+				break;
+
+			err = sbl_spico_burst_upload(sbl, sbus_addr,
+						     SPICO_SERDES_ADDR_IMEM_BURST,
+						  fw_size, fw_data);
+			if (err) {
+				sbl_dev_err(sbl->dev, "Upload failed!");
+				break;
+			}
+
+			err = sbl_sbus_wr(sbl, sbus_addr,
+					  SPICO_SERDES_ADDR_IMEM,
+				       SPICO_SERDES_DATA_CLR_IMEM_CNTL_EN);
+			if (err)
+				break;
+
+			err = sbl_sbus_wr(sbl, sbus_addr,
+					  SPICO_SERDES_ADDR_ECC,
+				       SPICO_SERDES_DATA_SET_ECC_EN);
+			if (err)
+				break;
+
+			err = sbl_sbus_wr(sbl, sbus_addr,
+					  SPICO_SERDES_ADDR_ECCLOG,
+				       SPICO_SERDES_DATA_CLR_ECC_ERR);
+			if (err)
+				break;
+
+			err = sbl_sbus_wr(sbl, sbus_addr,
+					  SPICO_SERDES_ADDR_RESET_EN,
+				       SPICO_SERDES_DATA_SET_SPICO_EN);
+			if (err)
+				break;
+
+			err = sbl_sbus_wr(sbl, sbus_addr,
+					  SPICO_SERDES_ADDR_INTR_DIS,
+				       SPICO_SERDES_DATA_SET_INTR_EN);
+			if (err)
+				break;
+		}
+
+		mutex_unlock(SBUS_RING_MTX(sbl, sbus_ring));
+
+		if (err)
+			break;
+	}
+
+	for (port = first_port; port <= last_port; ++port)
+		mutex_unlock(&sbl->link[port].serdes_mtx);
+
+	/* If any errors occurred above, return now we've unlocked all mutexes. */
+	if (err)
+		return err;
+
+	/* Increment SerDes firmware reload counters */
+	if (port_num != SBL_ALL_PORTS)
+		for (serdes = first_serdes; serdes <= last_serdes; ++serdes)
+			sbl_link_counters_incr(sbl, port_num, serdes0_fw_reload + serdes);
+
+	if (!sbl->is_hw)
+		return 0;
+
+	sbl_dev_dbg(sbl->dev, "p%d: Validating flash..", port_num);
+	for (port = first_port; port <= last_port; ++port) {
+		for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+			sbus_addr =
+				SBUS_ADDR(sbl->switch_info->ports[port].serdes[serdes].sbus_ring,
+					  sbl->switch_info->ports[port].serdes[serdes].rx_addr);
+			last_jiffy = jiffies + msecs_to_jiffies(1000 *
+								 sbl->iattr.core_status_rd_timeout);
+			do {
+				core_status_value = sbl_read64(sbl, SBL_PML_BASE(port) |
+							       SBL_PML_SERDES_CORE_STATUS_OFFSET(serdes));
+				if (core_status_value &
+				    SERDES_CORE_STATUS_SPICO_READY_MASK) {
+					break;
+				}
+				msleep(sbl->iattr.core_status_rd_poll_interval);
+			} while (time_is_after_jiffies(last_jiffy));
+
+			if (!(core_status_value & (1 << 5))) {
+				sbl_dev_err(sbl->dev, "p%ds%d Timeout reading o_core_status (timeout:%ds)",
+					    port_num, serdes, sbl->iattr.core_status_rd_timeout);
+				return -ETIME;
+			}
+
+			err = sbl_validate_serdes_fw_crc(sbl, port, serdes);
+			if (err)
+				return err;
+
+		}
+	}
+	sbl_dev_dbg(sbl->dev, "p%d: FW upload complete!", port_num);
+
+	return 0;
+}
+
+static int sbl_get_serdes_tuning_params(struct sbl_inst *sbl, int port_num,
+					struct sbl_tuning_params *tps)
+{
+	int i, serdes, err;
+
+	if (!tps)
+		return -EINVAL;
+
+	tps->magic = SBL_TUNING_PARAM_MAGIC;
+	tps->version = SBL_TUNING_PARAM_VERSION;
+
+	for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+		if (!rx_serdes_required_for_link_mode(sbl, port_num, serdes))
+			continue;
+
+		for (i = 0; i < NUM_CTLE_PARAMS; ++i) {
+			err = sbl_serdes_spico_int(sbl, port_num, serdes,
+						   SPICO_INT_CM4_HAL_READ,
+					SPICO_INT_DATA_HAL_CTLE_BASE | i,
+					&tps->params[serdes].ctle[i],
+					SPICO_INT_RETURN_RESULT);
+			if (err)
+				return err;
+
+			sbl_dev_dbg(sbl->dev, "p%ds%d: CTLE[%d]: %d",
+				    port_num, serdes, i,
+					tps->params[serdes].ctle[i]);
+		}
+		for (i = 0; i < NUM_FFE_PARAMS; ++i) {
+			err = sbl_serdes_spico_int(sbl, port_num, serdes,
+						   SPICO_INT_CM4_HAL_READ,
+					SPICO_INT_DATA_HAL_FFE_BASE | i,
+					&tps->params[serdes].ffe[i],
+					SPICO_INT_RETURN_RESULT);
+			if (err)
+				return err;
+
+			sbl_dev_dbg(sbl->dev, "p%ds%d: FFE[%d]: %d",
+				    port_num, serdes, i,
+					tps->params[serdes].ffe[i]);
+		}
+		for (i = 0; i < NUM_DFE_PARAMS; ++i) {
+			err = sbl_serdes_spico_int(sbl, port_num, serdes,
+						   SPICO_INT_CM4_HAL_READ,
+					SPICO_INT_DATA_HAL_DFE_BASE | i,
+					&tps->params[serdes].dfe[i],
+					SPICO_INT_RETURN_RESULT);
+			if (err)
+				return err;
+
+			sbl_dev_dbg(sbl->dev, "p%ds%d: DFE[%d]: %d",
+				    port_num, serdes, i,
+					tps->params[serdes].dfe[i]);
+		}
+		for (i = 0; i < NUM_RXVS_PARAMS; ++i) {
+			err = sbl_serdes_spico_int(sbl, port_num, serdes,
+						   SPICO_INT_CM4_HAL_READ,
+					SPICO_INT_DATA_HAL_RXVS_BASE |
+						(i + SPICO_INT_DATA_HAL_RXVS_OFFSET),
+					&tps->params[serdes].rxvs[i],
+					SPICO_INT_RETURN_RESULT);
+			if (err)
+				return err;
+
+			sbl_dev_dbg(sbl->dev, "p%ds%d: RXVS[%d]: %d",
+				    port_num, serdes, i,
+					tps->params[serdes].rxvs[i]);
+		}
+		for (i = 0; i < NUM_RXVC_PARAMS; ++i) {
+			err = sbl_serdes_spico_int(sbl, port_num, serdes,
+						   SPICO_INT_CM4_HAL_READ,
+					SPICO_INT_DATA_HAL_RXVC_BASE | i,
+					&tps->params[serdes].rxvc[i],
+					SPICO_INT_RETURN_RESULT);
+			if (err)
+				return err;
+
+			sbl_dev_dbg(sbl->dev, "p%ds%d: RXVC[%d]: %d",
+				    port_num, serdes, i,
+					tps->params[serdes].rxvc[i]);
+		}
+		for (i = 0; i < NUM_RSDO_PARAMS; ++i) {
+			err = sbl_serdes_spico_int(sbl, port_num, serdes,
+						   SPICO_INT_CM4_HAL_READ,
+					SPICO_INT_DATA_HAL_RSDO_BASE | i,
+					&tps->params[serdes].rsdo[i],
+					SPICO_INT_RETURN_RESULT);
+			if (err)
+				return err;
+
+			sbl_dev_dbg(sbl->dev, "p%ds%d: RSDO[%d]: %d",
+				    port_num, serdes, i,
+					tps->params[serdes].rsdo[i]);
+		}
+		for (i = 0; i < NUM_RSDC_PARAMS; ++i) {
+			err = sbl_serdes_spico_int(sbl, port_num, serdes,
+						   SPICO_INT_CM4_HAL_READ,
+					SPICO_INT_DATA_HAL_RSDC_BASE | i,
+					&tps->params[serdes].rsdc[i],
+					SPICO_INT_RETURN_RESULT);
+			if (err)
+				return err;
+
+			sbl_dev_dbg(sbl->dev, "p%ds%d: RSDC[%d]: %d",
+				    port_num, serdes, i,
+					tps->params[serdes].rsdc[i]);
+		}
+		for (i = 0; i < NUM_RSTO_PARAMS; ++i) {
+			err = sbl_serdes_spico_int(sbl, port_num, serdes,
+						   SPICO_INT_CM4_HAL_READ,
+					SPICO_INT_DATA_HAL_RSTO_BASE | i,
+					&tps->params[serdes].rsto[i],
+					SPICO_INT_RETURN_RESULT);
+			if (err)
+				return err;
+
+			sbl_dev_dbg(sbl->dev, "p%ds%d: RSTO[%d]: %d",
+				    port_num, serdes, i,
+					tps->params[serdes].rsto[i]);
+		}
+		for (i = 0; i < NUM_RSTC_PARAMS; ++i) {
+			err = sbl_serdes_spico_int(sbl, port_num, serdes,
+						   SPICO_INT_CM4_HAL_READ,
+					SPICO_INT_DATA_HAL_RSTC_BASE | i,
+					&tps->params[serdes].rstc[i],
+					SPICO_INT_RETURN_RESULT);
+			if (err)
+				return err;
+
+			sbl_dev_dbg(sbl->dev, "p%ds%d: RSTC[%d]: %d",
+				    port_num, serdes, i,
+					tps->params[serdes].rstc[i]);
+		}
+		for (i = 0; i < NUM_EH_PARAMS; ++i) {
+			err = sbl_serdes_spico_int(sbl, port_num, serdes,
+						   SPICO_INT_CM4_HAL_READ,
+					SPICO_INT_DATA_HAL_EH_BASE | i,
+					&tps->params[serdes].eh[i],
+					SPICO_INT_RETURN_RESULT);
+			if (err)
+				return err;
+
+			sbl_dev_dbg(sbl->dev, "p%ds%d: EH[%d]: %d",
+				    port_num, serdes, i,
+					tps->params[serdes].eh[i]);
+		}
+		for (i = 0; i < NUM_GTP_PARAMS; ++i) {
+			err = sbl_serdes_spico_int(sbl, port_num, serdes,
+						   SPICO_INT_CM4_HAL_READ,
+					SPICO_INT_DATA_HAL_GTP_BASE | i,
+					&tps->params[serdes].gtp[i],
+					SPICO_INT_RETURN_RESULT);
+			if (err)
+				return err;
+
+			sbl_dev_dbg(sbl->dev, "p%ds%d: GTP[%d]: %d",
+				    port_num, serdes, i,
+					tps->params[serdes].gtp[i]);
+		}
+		for (i = 0; i < NUM_DCCD_PARAMS; ++i) {
+			err = sbl_serdes_spico_int(sbl, port_num, serdes,
+						   SPICO_INT_CM4_HAL_READ,
+					SPICO_INT_DATA_HAL_DCCD_BASE | i,
+					&tps->params[serdes].dccd[i],
+					SPICO_INT_RETURN_RESULT);
+			if (err)
+				return err;
+
+			sbl_dev_dbg(sbl->dev, "p%ds%d: DCCD[%d]: %d",
+				    port_num, serdes, i,
+					tps->params[serdes].dccd[i]);
+		}
+		for (i = 0; i < NUM_P4LV_PARAMS; ++i) {
+			err = sbl_serdes_spico_int(sbl, port_num, serdes,
+						   SPICO_INT_CM4_HAL_READ,
+					SPICO_INT_DATA_HAL_P4LV_BASE | i,
+					&tps->params[serdes].p4lv[i],
+					SPICO_INT_RETURN_RESULT);
+			if (err)
+				return err;
+
+			sbl_dev_dbg(sbl->dev, "p%ds%d: P4LV[%d]: %d",
+				    port_num, serdes, i,
+					tps->params[serdes].p4lv[i]);
+		}
+
+		if (sbl_validate_serdes_fw_vers(sbl, port_num, serdes,
+						SBL_KNOWN_FW0_REV,
+					SBL_KNOWN_FW0_BUILD)) {
+			return -EADDRNOTAVAIL;
+		}
+
+		for (i = 0; i < NUM_AFEC_PARAMS; ++i) {
+			err = sbl_serdes_spico_int(sbl, port_num, serdes,
+						   SPICO_INT_CM4_MEM_READ |
+						(SBUS_AFE_CTRL_KNOWN_FW0_BASE + i), 0x0,
+					&tps->params[serdes].afec[i],
+					SPICO_INT_RETURN_RESULT);
+			if (err)
+				return err;
+
+			sbl_dev_dbg(sbl->dev, "p%ds%d: AFEC[%d]: %d",
+				    port_num, serdes, i,
+					tps->params[serdes].afec[i]);
+		}
+	}
+
+	return 0;
+}
+
+int sbl_check_serdes_tuning_params(struct sbl_inst *sbl, int port_num)
+{
+	int i, serdes, err;
+	struct sbl_tuning_params *tps;
+	int min, max;
+	int num_oob_params;
+	int max_oob_params = SBL_MAX_OOB_SERDES_PARAMS;
+
+	tps = kzalloc(sizeof(struct sbl_tuning_params), GFP_KERNEL);
+	if (!tps)
+		return -ENOMEM;
+
+	/* debug adjustment to number of oob tuning params allowed */
+	if (sbl_debug_option(sbl, port_num, SBL_DEBUG_BAD_PARAM_1))
+		max_oob_params += 1;
+	if (sbl_debug_option(sbl, port_num, SBL_DEBUG_BAD_PARAM_2))
+		max_oob_params += 2;
+
+	err = sbl_get_serdes_tuning_params(sbl, port_num, tps);
+	if (err) {
+		kfree(tps);
+		return err;
+	}
+
+	if (!sbl->is_hw) {
+		kfree(tps);
+		return 0;
+	}
+
+	for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+		if (!rx_serdes_required_for_link_mode(sbl, port_num, serdes))
+			continue;
+
+		num_oob_params = 0;
+		for (i = 0; i < NUM_CTLE_PARAMS; ++i) {
+			switch (i) {
+			case CTLE_HF_OFFSET:
+				min = SBL_CTLE_HF_MIN;
+				max = SBL_CTLE_HF_MAX;
+				break;
+			case CTLE_LF_OFFSET:
+				min = SBL_CTLE_LF_MIN;
+				max = SBL_CTLE_LF_MAX;
+				break;
+			case CTLE_DC_OFFSET:
+				min = SBL_CTLE_DC_MIN;
+				max = SBL_CTLE_DC_MAX;
+				break;
+			case CTLE_BW_OFFSET:
+				min = SBL_CTLE_BW_MIN;
+				max = SBL_CTLE_BW_MAX;
+				break;
+			case CTLE_GS1_OFFSET:
+				min = SBL_CTLE_GS1_MIN;
+				max = SBL_CTLE_GS1_MAX;
+				break;
+			case CTLE_GS2_OFFSET:
+				min = SBL_CTLE_GS2_MIN;
+				max = SBL_CTLE_GS2_MAX;
+				break;
+			case CTLE_SCE_OFFSET:
+				min = SBL_CTLE_SCE_MIN;
+				max = SBL_CTLE_SCE_MAX;
+				break;
+			case CTLE_HF_MIN_OFFSET:
+				min = SBL_CTLE_HF_MIN_MIN;
+				max = SBL_CTLE_HF_MIN_MAX;
+				break;
+			case CTLE_HF_MAX_OFFSET:
+				min = SBL_CTLE_HF_MAX_MIN;
+				max = SBL_CTLE_HF_MAX_MAX;
+				break;
+			case CTLE_LF_MIN_OFFSET:
+				min = SBL_CTLE_LF_MIN_MIN;
+				max = SBL_CTLE_LF_MIN_MAX;
+				break;
+			case CTLE_LF_MAX_OFFSET:
+				min = SBL_CTLE_LF_MAX_MIN;
+				max = SBL_CTLE_LF_MAX_MAX;
+				break;
+			default:
+				continue;
+			}
+			if ((tps->params[serdes].ctle[i] < min) ||
+			    (tps->params[serdes].ctle[i] > max)) {
+				sbl_dev_warn(sbl->dev, "p%ds%d: CTLE[%d] value(%d) out of bounds(%d:%d)!",
+					     port_num, serdes, i,
+					 tps->params[serdes].ctle[i], min, max);
+				num_oob_params++;
+			} else {
+				sbl_dev_dbg(sbl->dev, "p%ds%d: CTLE[%d] value(%d) within bounds(%d:%d)",
+					    port_num, serdes, i,
+					 tps->params[serdes].ctle[i], min, max);
+			}
+		}
+
+		for (i = 0; i < NUM_FFE_PARAMS; ++i) {
+			switch (i) {
+			case FFE_PRE2_OFFSET:
+				min = SBL_FFE_PRE2_MIN;
+				max = SBL_FFE_PRE2_MAX;
+				break;
+			case FFE_PRE1_OFFSET:
+				min = SBL_FFE_PRE1_MIN;
+				max = SBL_FFE_PRE1_MAX;
+				break;
+			case FFE_POST1_OFFSET:
+				min = SBL_FFE_POST1_MIN;
+				max = SBL_FFE_POST1_MAX;
+				break;
+			case FFE_BFLF_OFFSET:
+				min = SBL_FFE_BFLF_MIN;
+				max = SBL_FFE_BFLF_MAX;
+				break;
+			case FFE_BFHF_OFFSET:
+				min = SBL_FFE_BFHF_MIN;
+				max = SBL_FFE_BFHF_MAX;
+				break;
+			case FFE_DATARATE_OFFSET:
+				min = SBL_FFE_DATARATE_MIN;
+				max = SBL_FFE_DATARATE_MAX;
+				break;
+			case FFE_SCE_OFFSET:
+				min = SBL_FFE_SCE_MIN;
+				max = SBL_FFE_SCE_MAX;
+				break;
+			case FFE_PRE1_MIN_OFFSET:
+				min = SBL_FFE_PRE1_MIN_MIN;
+				max = SBL_FFE_PRE1_MIN_MAX;
+				break;
+			case FFE_PRE1_MAX_OFFSET:
+				min = SBL_FFE_PRE1_MAX_MIN;
+				max = SBL_FFE_PRE1_MAX_MAX;
+				break;
+			case FFE_PRE2_MIN_OFFSET:
+				min = SBL_FFE_PRE2_MIN_MIN;
+				max = SBL_FFE_PRE2_MIN_MAX;
+				break;
+			case FFE_PRE2_MAX_OFFSET:
+				min = SBL_FFE_PRE2_MAX_MIN;
+				max = SBL_FFE_PRE2_MAX_MAX;
+				break;
+			case FFE_BFLF_ICAL_OFFSET:
+				min = SBL_FFE_BFLF_ICAL_MIN;
+				max = SBL_FFE_BFLF_ICAL_MAX;
+				break;
+			case FFE_POST1_MIN_OFFSET:
+				min = SBL_FFE_POST1_MIN_MIN;
+				max = SBL_FFE_POST1_MIN_MAX;
+				break;
+			case FFE_POST1_MAX_OFFSET:
+				min = SBL_FFE_POST1_MAX_MIN;
+				max = SBL_FFE_POST1_MAX_MAX;
+				break;
+			default:
+				continue;
+			}
+			if ((tps->params[serdes].ffe[i] < min) ||
+			    (tps->params[serdes].ffe[i] > max)) {
+				sbl_dev_warn(sbl->dev, "p%ds%d: FFE[%d] value(%d) out of bounds(%d:%d)!",
+					     port_num, serdes, i,
+					 tps->params[serdes].ffe[i], min, max);
+				num_oob_params++;
+			} else {
+				sbl_dev_dbg(sbl->dev, "p%ds%d: FFE[%d] value(%d) within bounds(%d:%d)",
+					    port_num, serdes, i,
+					 tps->params[serdes].ffe[i], min, max);
+			}
+		}
+		for (i = 0; i < NUM_DFE_PARAMS; ++i) {
+			switch (i) {
+			case DFE_GT1_OFFSET:
+				min = SBL_DFE_GT1_MIN;
+				max = SBL_DFE_GT1_MAX;
+				break;
+			case DFE_GT2_OFFSET:
+				min = SBL_DFE_GT2_MIN;
+				max = SBL_DFE_GT2_MAX;
+				break;
+			case DFE_G2_OFFSET:
+				min = SBL_DFE_G2_MIN;
+				max = SBL_DFE_G2_MAX;
+				break;
+			case DFE_G3_OFFSET:
+				min = SBL_DFE_G3_MIN;
+				max = SBL_DFE_G3_MAX;
+				break;
+			case DFE_G4_OFFSET:
+				min = SBL_DFE_G4_MIN;
+				max = SBL_DFE_G4_MAX;
+				break;
+			case DFE_G5_OFFSET:
+				min = SBL_DFE_G5_MIN;
+				max = SBL_DFE_G5_MAX;
+				break;
+			case DFE_G6_OFFSET:
+				min = SBL_DFE_G6_MIN;
+				max = SBL_DFE_G6_MAX;
+				break;
+			case DFE_G7_OFFSET:
+				min = SBL_DFE_G7_MIN;
+				max = SBL_DFE_G7_MAX;
+				break;
+			case DFE_G8_OFFSET:
+				min = SBL_DFE_G8_MIN;
+				max = SBL_DFE_G8_MAX;
+				break;
+			case DFE_G9_OFFSET:
+				min = SBL_DFE_G9_MIN;
+				max = SBL_DFE_G9_MAX;
+				break;
+			case DFE_G10_OFFSET:
+				min = SBL_DFE_G10_MIN;
+				max = SBL_DFE_G10_MAX;
+				break;
+			case DFE_G11_OFFSET:
+				min = SBL_DFE_G11_MIN;
+				max = SBL_DFE_G11_MAX;
+				break;
+			case DFE_G12_OFFSET:
+				min = SBL_DFE_G12_MIN;
+				max = SBL_DFE_G12_MAX;
+				break;
+			case DFE_G13_OFFSET:
+				min = SBL_DFE_G13_MIN;
+				max = SBL_DFE_G13_MAX;
+				break;
+			default:
+				continue;
+			}
+			if ((tps->params[serdes].dfe[i] < min) ||
+			    (tps->params[serdes].dfe[i] > max)) {
+				sbl_dev_warn(sbl->dev, "p%ds%d: DFE[%d] value(%d) out of bounds(%d:%d)!",
+					     port_num, serdes, i,
+					 tps->params[serdes].dfe[i], min, max);
+				num_oob_params++;
+			} else {
+				sbl_dev_dbg(sbl->dev, "p%ds%d: DFE[%d] value(%d) within bounds(%d:%d)",
+					    port_num, serdes, i,
+					 tps->params[serdes].dfe[i], min, max);
+			}
+		}
+
+		if (num_oob_params > max_oob_params) {
+			sbl_dev_err(sbl->dev, "p%ds%d: Too many tuning params out of bounds(%d, max %d)!",
+				    port_num, serdes, num_oob_params, max_oob_params);
+			return -EDQUOT;
+		} else if (num_oob_params > 0) {
+			sbl_dev_info(sbl->dev, "p%ds%d: Some tuning params out of bounds(%d, max %d)",
+				     port_num, serdes, num_oob_params, max_oob_params);
+		}
+	}
+
+	kfree(tps);
+	return 0;
+}
+
+int sbl_save_serdes_tuning_params(struct sbl_inst *sbl, int port_num)
+{
+	int err;
+
+	sbl->link[port_num].tuning_params.tp_state_hash0 =
+		sbl_get_tp_hash0(sbl, port_num);
+	sbl->link[port_num].tuning_params.tp_state_hash1 =
+		sbl_get_tp_hash1(sbl, port_num);
+	sbl_dev_dbg(sbl->dev, "Updated hash0 to 0x%llx, hash1 to 0x%llx",
+		    sbl->link[port_num].tuning_params.tp_state_hash0,
+		sbl->link[port_num].tuning_params.tp_state_hash1);
+
+	err = sbl_get_serdes_tuning_params(sbl, port_num,
+					   &sbl->link[port_num].tuning_params);
+
+	return err;
+}
+
+int sbl_apply_serdes_tuning_params(struct sbl_inst *sbl, int port_num,
+				   int serdes)
+{
+	int i, err;
+	u16 result;
+
+	sbl_dev_dbg(sbl->dev, "p%ds%d: applying saved tuning params", port_num, serdes);
+
+	DEV_TRACE2(sbl->dev, "CTLE params");
+	for (i = 0; i < NUM_CTLE_PARAMS; ++i) {
+		DEV_TRACE2(sbl->dev, "%d ",
+			   sbl->link[port_num].tuning_params.params[serdes].ctle[i]);
+	}
+	DEV_TRACE2(sbl->dev, "RXFFE params");
+	for (i = 0; i < NUM_FFE_PARAMS; ++i) {
+		DEV_TRACE2(sbl->dev, "%d ",
+			   sbl->link[port_num].tuning_params.params[serdes].ffe[i]);
+	}
+	DEV_TRACE2(sbl->dev, "DFE params");
+	for (i = 0; i < NUM_DFE_PARAMS; ++i) {
+		DEV_TRACE2(sbl->dev, "%d ",
+			   sbl->link[port_num].tuning_params.params[serdes].dfe[i]);
+	}
+	DEV_TRACE2(sbl->dev, "RXVS params");
+	for (i = 0; i < NUM_RXVS_PARAMS; ++i) {
+		DEV_TRACE2(sbl->dev, "%d ",
+			   sbl->link[port_num].tuning_params.params[serdes].rxvs[i]);
+	}
+	DEV_TRACE2(sbl->dev, "RXVC params");
+	for (i = 0; i < NUM_RXVC_PARAMS; ++i) {
+		DEV_TRACE2(sbl->dev, "%d ",
+			   sbl->link[port_num].tuning_params.params[serdes].rxvc[i]);
+	}
+	DEV_TRACE2(sbl->dev, "RSDO params");
+	for (i = 0; i < NUM_RSDO_PARAMS; ++i) {
+		DEV_TRACE2(sbl->dev, "%d ",
+			   sbl->link[port_num].tuning_params.params[serdes].rsdo[i]);
+	}
+	DEV_TRACE2(sbl->dev, "RSDC params");
+	for (i = 0; i < NUM_RSDC_PARAMS; ++i) {
+		DEV_TRACE2(sbl->dev, "%d ",
+			   sbl->link[port_num].tuning_params.params[serdes].rsdc[i]);
+	}
+	DEV_TRACE2(sbl->dev, "RSTO params");
+	for (i = 0; i < NUM_RSTO_PARAMS; ++i) {
+		DEV_TRACE2(sbl->dev, "%d ",
+			   sbl->link[port_num].tuning_params.params[serdes].rsto[i]);
+	}
+	DEV_TRACE2(sbl->dev, "RSTC params");
+	for (i = 0; i < NUM_RSTC_PARAMS; ++i) {
+		DEV_TRACE2(sbl->dev, "%d ",
+			   sbl->link[port_num].tuning_params.params[serdes].rstc[i]);
+	}
+	DEV_TRACE2(sbl->dev, "EH params");
+	for (i = 0; i < NUM_EH_PARAMS; ++i) {
+		DEV_TRACE2(sbl->dev, "%d ",
+			   sbl->link[port_num].tuning_params.params[serdes].eh[i]);
+	}
+	DEV_TRACE2(sbl->dev, "GTP params");
+	for (i = 0; i < NUM_GTP_PARAMS; ++i) {
+		DEV_TRACE2(sbl->dev, "%d ",
+			   sbl->link[port_num].tuning_params.params[serdes].gtp[i]);
+	}
+	DEV_TRACE2(sbl->dev, "DCCD params");
+	for (i = 0; i < NUM_DCCD_PARAMS; ++i) {
+		DEV_TRACE2(sbl->dev, "%d ",
+			   sbl->link[port_num].tuning_params.params[serdes].dccd[i]);
+	}
+	DEV_TRACE2(sbl->dev, "P4LV params");
+	for (i = 0; i < NUM_P4LV_PARAMS; ++i) {
+		DEV_TRACE2(sbl->dev, "%d ",
+			   sbl->link[port_num].tuning_params.params[serdes].p4lv[i]);
+	}
+	DEV_TRACE2(sbl->dev, "AFEC params");
+	for (i = 0; i < NUM_AFEC_PARAMS; ++i) {
+		DEV_TRACE2(sbl->dev, "%d ",
+			   sbl->link[port_num].tuning_params.params[serdes].afec[i]);
+	}
+
+	for (i = 0; i < NUM_CTLE_PARAMS; ++i) {
+		/* GS1/GS2 are "config values" - don't apply them from saved
+		   "turning params", as this will override the desired config */
+		if (((SPICO_INT_DATA_HAL_CTLE_BASE | i) ==
+		     SPICO_INT_DATA_HAL_CTLE_GS1) ||
+		    ((SPICO_INT_DATA_HAL_CTLE_BASE | i) ==
+		     SPICO_INT_DATA_HAL_CTLE_GS2)) {
+			continue;
+		}
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_READ,
+						SPICO_INT_DATA_HAL_CTLE_BASE | i,
+						&result, SPICO_INT_RETURN_RESULT);
+		if (err)
+			return err;
+
+		DEV_TRACE2(sbl->dev, "Updating CLTE param %d from 0x%x to 0x%x",
+			   i, result, sbl->link[port_num].tuning_params.params[serdes].ctle[i]);
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_WRITE,
+						sbl->link[port_num].tuning_params.params[serdes].ctle[i],
+						&result, SPICO_INT_RETURN_RESULT);
+		if (err)
+			return err;
+
+		if (result != SPICO_INT_CM4_HAL_READ) {
+			sbl_dev_err(sbl->dev, "p%ds%d: Failed updating CTLE[%d] tuning param!",
+				    port_num, serdes, i);
+			return -EBADE;
+		}
+	}
+	for (i = 0; i < NUM_FFE_PARAMS; ++i) {
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_READ,
+						SPICO_INT_DATA_HAL_FFE_BASE | i,
+						&result, SPICO_INT_RETURN_RESULT);
+		if (err)
+			return err;
+
+		DEV_TRACE2(sbl->dev, "Updating FFE param %d from 0x%x to 0x%x",
+			   i, result, sbl->link[port_num].tuning_params.params[serdes].ffe[i]);
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_WRITE,
+						sbl->link[port_num].tuning_params.params[serdes].ffe[i],
+						&result, SPICO_INT_RETURN_RESULT);
+		if (err)
+			return err;
+
+		if (result != SPICO_INT_CM4_HAL_READ) {
+			sbl_dev_err(sbl->dev, "p%ds%d: Failed updating FFE[%d] tuning param!",
+				    port_num, serdes, i);
+			return -EBADE;
+		}
+	}
+	for (i = 0; i < NUM_DFE_PARAMS; ++i) {
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_READ,
+						SPICO_INT_DATA_HAL_DFE_BASE | i,
+						&result, SPICO_INT_RETURN_RESULT);
+		if (err)
+			return err;
+
+		DEV_TRACE2(sbl->dev, "Updating DFE param %d from 0x%x to 0x%x",
+			   i, result, sbl->link[port_num].tuning_params.params[serdes].dfe[i]);
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_WRITE,
+						sbl->link[port_num].tuning_params.params[serdes].dfe[i],
+						&result, SPICO_INT_RETURN_RESULT);
+		if (err)
+			return err;
+
+		if (result != SPICO_INT_CM4_HAL_READ) {
+			sbl_dev_err(sbl->dev, "p%ds%d: Failed updating DFE[%d] tuning param!",
+				    port_num, serdes, i);
+			return -EBADE;
+		}
+	}
+
+	for (i = 0; i < NUM_RXVS_PARAMS; ++i) {
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_READ,
+						SPICO_INT_DATA_HAL_RXVS_BASE |
+							(i + SPICO_INT_DATA_HAL_RXVS_OFFSET),
+						&result, SPICO_INT_RETURN_RESULT);
+		if (err)
+			return err;
+
+		DEV_TRACE2(sbl->dev, "Updating RXVS param %d from 0x%x to 0x%x",
+			   i, result, sbl->link[port_num].tuning_params.params[serdes].rxvs[i]);
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_WRITE,
+						sbl->link[port_num].tuning_params.params[serdes].rxvs[i],
+						&result, SPICO_INT_RETURN_RESULT);
+		if (err)
+			return err;
+
+		if (result != SPICO_INT_CM4_HAL_READ) {
+			sbl_dev_err(sbl->dev, "p%ds%d: Failed updating RXVS[%d] tuning param!",
+				    port_num, serdes, i);
+			return -EBADE;
+		}
+	}
+
+	for (i = 0; i < NUM_RXVC_PARAMS; ++i) {
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_READ,
+						SPICO_INT_DATA_HAL_RXVC_BASE | i,
+						&result, SPICO_INT_RETURN_RESULT);
+		if (err)
+			return err;
+
+		DEV_TRACE2(sbl->dev, "Updating RXVC param %d from 0x%x to 0x%x",
+			   i, result, sbl->link[port_num].tuning_params.params[serdes].rxvc[i]);
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_WRITE,
+						sbl->link[port_num].tuning_params.params[serdes].rxvc[i],
+						&result, SPICO_INT_RETURN_RESULT);
+		if (err)
+			return err;
+
+		if (result != SPICO_INT_CM4_HAL_READ) {
+			sbl_dev_err(sbl->dev, "p%ds%d: Failed updating RXVC[%d] tuning param!",
+				    port_num, serdes, i);
+			return -EBADE;
+		}
+	}
+
+	for (i = 0; i < NUM_RSDO_PARAMS; ++i) {
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_READ,
+						SPICO_INT_DATA_HAL_RSDO_BASE | i,
+						&result, SPICO_INT_RETURN_RESULT);
+		if (err)
+			return err;
+
+		DEV_TRACE2(sbl->dev, "Updating RSDO param %d from 0x%x to 0x%x",
+			   i, result, sbl->link[port_num].tuning_params.params[serdes].rsdo[i]);
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_WRITE,
+						sbl->link[port_num].tuning_params.params[serdes].rsdo[i],
+						&result, SPICO_INT_RETURN_RESULT);
+		if (err)
+			return err;
+
+		if (result != SPICO_INT_CM4_HAL_READ) {
+			sbl_dev_err(sbl->dev, "p%ds%d: Failed updating RSDO[%d] tuning param!",
+				    port_num, serdes, i);
+			return -EBADE;
+		}
+	}
+
+	for (i = 0; i < NUM_RSDC_PARAMS; ++i) {
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_READ,
+						SPICO_INT_DATA_HAL_RSDC_BASE | i,
+						&result, SPICO_INT_RETURN_RESULT);
+		if (err)
+			return err;
+
+		DEV_TRACE2(sbl->dev, "Updating RSDC param %d from 0x%x to 0x%x",
+			   i, result, sbl->link[port_num].tuning_params.params[serdes].rsdc[i]);
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_WRITE,
+						sbl->link[port_num].tuning_params.params[serdes].rsdc[i],
+						&result, SPICO_INT_RETURN_RESULT);
+		if (err)
+			return err;
+
+		if (result != SPICO_INT_CM4_HAL_READ) {
+			sbl_dev_err(sbl->dev, "p%ds%d: Failed updating RSDC[%d] tuning param!",
+				    port_num, serdes, i);
+			return -EBADE;
+		}
+	}
+
+	for (i = 0; i < NUM_RSTO_PARAMS; ++i) {
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_READ,
+						SPICO_INT_DATA_HAL_RSTO_BASE | i,
+						&result, SPICO_INT_RETURN_RESULT);
+		if (err)
+			return err;
+
+		DEV_TRACE2(sbl->dev, "Updating RSTO param %d from 0x%x to 0x%x",
+			   i, result, sbl->link[port_num].tuning_params.params[serdes].rsto[i]);
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_WRITE,
+						sbl->link[port_num].tuning_params.params[serdes].rsto[i],
+						&result, SPICO_INT_RETURN_RESULT);
+		if (err)
+			return err;
+
+		if (result != SPICO_INT_CM4_HAL_READ) {
+			sbl_dev_err(sbl->dev, "p%ds%d: Failed updating RSTO[%d] tuning param!",
+				    port_num, serdes, i);
+			return -EBADE;
+		}
+	}
+
+	for (i = 0; i < NUM_RSTC_PARAMS; ++i) {
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_READ,
+						SPICO_INT_DATA_HAL_RSTC_BASE | i,
+						&result, SPICO_INT_RETURN_RESULT);
+		if (err)
+			return err;
+
+		DEV_TRACE2(sbl->dev, "Updating RSTC param %d from 0x%x to 0x%x",
+			   i, result, sbl->link[port_num].tuning_params.params[serdes].rstc[i]);
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_WRITE,
+						sbl->link[port_num].tuning_params.params[serdes].rstc[i],
+						&result, SPICO_INT_RETURN_RESULT);
+		if (err)
+			return err;
+
+		if (result != SPICO_INT_CM4_HAL_READ) {
+			sbl_dev_err(sbl->dev, "p%ds%d: Failed updating RSTC[%d] tuning param!",
+				    port_num, serdes, i);
+			return -EBADE;
+		}
+	}
+
+	for (i = 0; i < NUM_EH_PARAMS; ++i) {
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_READ,
+						SPICO_INT_DATA_HAL_EH_BASE | i,
+						&result, SPICO_INT_RETURN_RESULT);
+		if (err)
+			return err;
+
+		DEV_TRACE2(sbl->dev, "Updating EH param %d from 0x%x to 0x%x",
+			   i, result, sbl->link[port_num].tuning_params.params[serdes].eh[i]);
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_WRITE,
+						sbl->link[port_num].tuning_params.params[serdes].eh[i],
+						&result, SPICO_INT_RETURN_RESULT);
+		if (err)
+			return err;
+
+		if (result != SPICO_INT_CM4_HAL_READ) {
+			sbl_dev_err(sbl->dev, "p%ds%d: Failed updating EH[%d] tuning param!",
+				    port_num, serdes, i);
+			return -EBADE;
+		}
+	}
+
+	for (i = 0; i < NUM_GTP_PARAMS; ++i) {
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_READ,
+						SPICO_INT_DATA_HAL_GTP_BASE | i,
+						&result, SPICO_INT_RETURN_RESULT);
+		if (err)
+			return err;
+
+		DEV_TRACE2(sbl->dev, "Updating GTP param %d from 0x%x to 0x%x",
+			   i, result, sbl->link[port_num].tuning_params.params[serdes].gtp[i]);
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_WRITE,
+						sbl->link[port_num].tuning_params.params[serdes].gtp[i],
+						&result, SPICO_INT_RETURN_RESULT);
+		if (err)
+			return err;
+
+		if (result != SPICO_INT_CM4_HAL_READ) {
+			sbl_dev_err(sbl->dev, "p%ds%d: Failed updating GTP[%d] tuning param!",
+				    port_num, serdes, i);
+			return -EBADE;
+		}
+	}
+
+	for (i = 0; i < NUM_DCCD_PARAMS; ++i) {
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_READ,
+						SPICO_INT_DATA_HAL_DCCD_BASE | i,
+						&result, SPICO_INT_RETURN_RESULT);
+		if (err)
+			return err;
+
+		DEV_TRACE2(sbl->dev, "Updating DCCD param %d from 0x%x to 0x%x",
+			   i, result, sbl->link[port_num].tuning_params.params[serdes].dccd[i]);
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_WRITE,
+						sbl->link[port_num].tuning_params.params[serdes].dccd[i],
+						&result, SPICO_INT_RETURN_RESULT);
+		if (err)
+			return err;
+
+		if (result != SPICO_INT_CM4_HAL_READ) {
+			sbl_dev_err(sbl->dev, "p%ds%d: Failed updating DCCD[%d] tuning param!",
+				    port_num, serdes, i);
+			return -EBADE;
+		}
+	}
+
+	for (i = 0; i < NUM_P4LV_PARAMS; ++i) {
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_READ,
+						SPICO_INT_DATA_HAL_P4LV_BASE | i,
+						&result, SPICO_INT_RETURN_RESULT);
+		if (err)
+			return err;
+
+		DEV_TRACE2(sbl->dev, "Updating P4LV param %d from 0x%x to 0x%x",
+			   i, result, sbl->link[port_num].tuning_params.params[serdes].p4lv[i]);
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_WRITE,
+						sbl->link[port_num].tuning_params.params[serdes].p4lv[i],
+						&result, SPICO_INT_RETURN_RESULT);
+		if (err)
+			return err;
+
+		if (result != SPICO_INT_CM4_HAL_READ) {
+			sbl_dev_err(sbl->dev, "p%ds%d: Failed updating P4LV[%d] tuning param!",
+				    port_num, serdes, i);
+			return -EBADE;
+		}
+	}
+
+	if (sbl_validate_serdes_fw_vers(sbl, port_num, serdes,
+					SBL_KNOWN_FW0_REV,
+									SBL_KNOWN_FW0_BUILD)) {
+		return -EADDRNOTAVAIL;
+	}
+	for (i = 0; i < NUM_AFEC_PARAMS; ++i) {
+		DEV_TRACE2(sbl->dev, "Updating AFEC param %d to 0x%x",
+			   i, sbl->link[port_num].tuning_params.params[serdes].afec[i]);
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_MEM_WRITE |
+							(SBUS_AFE_CTRL_KNOWN_FW0_BASE + i),
+						sbl->link[port_num].tuning_params.params[serdes].afec[i],
+						&result, SPICO_INT_IGNORE_RESULT);
+		if (err)
+			return err;
+	}
+
+	/* Apply values */
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_HAL_CALL,
+					SPICO_INT_DATA_HAL_CTLE_APPLY, NULL,
+					SPICO_INT_IGNORE_RESULT);
+	if (err)
+		return err;
+
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_HAL_CALL,
+					SPICO_INT_DATA_HAL_FFE_APPLY, NULL,
+					SPICO_INT_IGNORE_RESULT);
+	if (err)
+		return err;
+
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_HAL_CALL,
+					SPICO_INT_DATA_HAL_DFE_APPLY, NULL,
+					SPICO_INT_IGNORE_RESULT);
+	if (err)
+		return err;
+
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_HAL_CALL,
+					SPICO_INT_DATA_HAL_RXV_APPLY, NULL,
+					SPICO_INT_IGNORE_RESULT);
+	if (err)
+		return err;
+
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_HAL_CALL,
+					SPICO_INT_DATA_HAL_DC_APPLY, NULL,
+					SPICO_INT_IGNORE_RESULT);
+	if (err)
+		return err;
+
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_HAL_CALL,
+					SPICO_INT_DATA_HAL_TC_APPLY, NULL,
+					SPICO_INT_IGNORE_RESULT);
+	if (err)
+		return err;
+
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_HAL_CALL,
+					SPICO_INT_DATA_HAL_PCAL_SETUP, NULL,
+					SPICO_INT_IGNORE_RESULT);
+	if (err)
+		return err;
+
+	DEV_TRACE2(sbl->dev, "rc: 0");
+
+	return 0;
+}
+
+int sbl_get_eye_heights(struct sbl_inst *sbl, int port_num, int serdes,
+			int *eye_heights)
+{
+	u16 result;
+	int err;
+
+	if (!eye_heights) {
+		sbl_dev_err(sbl->dev, "Bad args");
+		return -EINVAL;
+	}
+
+	DEV_TRACE2(sbl->dev, "p%ds%d", port_num, serdes);
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_GET_RX_EQ,
+					SPICO_INT_DATA_RXEQ_EH_THLE, &result,
+					SPICO_INT_RETURN_RESULT);
+	if (err) {
+		sbl_dev_err(sbl->dev, "p%ds%d: Failed to read setting!", port_num, serdes);
+		return err;
+	}
+	eye_heights[0] = result;
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_GET_RX_EQ,
+					SPICO_INT_DATA_RXEQ_EH_THME, &result,
+					SPICO_INT_RETURN_RESULT);
+	if (err) {
+		sbl_dev_err(sbl->dev, "p%ds%d: Failed to read setting!", port_num, serdes);
+		return err;
+	}
+	eye_heights[1] = result;
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_GET_RX_EQ,
+					SPICO_INT_DATA_RXEQ_EH_THUE, &result,
+					SPICO_INT_RETURN_RESULT);
+	if (err) {
+		sbl_dev_err(sbl->dev, "p%ds%d: Failed to read setting!", port_num, serdes);
+		return err;
+	}
+	eye_heights[2] = result;
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_GET_RX_EQ,
+					SPICO_INT_DATA_RXEQ_EH_THLO, &result,
+					SPICO_INT_RETURN_RESULT);
+	if (err) {
+		sbl_dev_err(sbl->dev, "p%ds%d: Failed to read setting!", port_num, serdes);
+		return err;
+	}
+	eye_heights[3] = result;
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_GET_RX_EQ,
+					SPICO_INT_DATA_RXEQ_EH_THMO, &result,
+					SPICO_INT_RETURN_RESULT);
+	if (err) {
+		sbl_dev_err(sbl->dev, "p%ds%d: Failed to read setting!", port_num, serdes);
+		return err;
+	}
+	eye_heights[4] = result;
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_GET_RX_EQ,
+					SPICO_INT_DATA_RXEQ_EH_THUO, &result,
+					SPICO_INT_RETURN_RESULT);
+	if (err) {
+		sbl_dev_err(sbl->dev, "p%ds%d: Failed to read setting!", port_num, serdes);
+		return err;
+	}
+	eye_heights[5] = result;
+
+	DEV_TRACE2(sbl->dev, "rc: 0");
+
+	return 0;
+}
+
+int sbl_serdes_soft_reset(struct sbl_inst *sbl, int port_num, int serdes)
+{
+	u32 unused;
+	int err;
+	u32 sbus_ring = sbl->switch_info->ports[port_num].serdes[serdes].sbus_ring;
+	u32 sbus_addr =
+		SBUS_ADDR(sbus_ring,
+			  sbl->switch_info->ports[port_num].serdes[serdes].rx_addr);
+
+	DEV_TRACE2(sbl->dev, "p%ds%d", port_num, serdes);
+
+	/* Don't allow SPICO interrupts while we are resetting the SerDes */
+	mutex_lock(&sbl->link[port_num].serdes_mtx);
+	/* SBUS Critical Section */
+	mutex_lock(SBUS_RING_MTX(sbl, sbus_ring));
+
+	/* Reset high */
+	err = sbl_sbus_op_aux(sbl, sbus_addr, SPICO_SERDES_ADDR_IMEM,
+			      SBUS_IFACE_DST_CORE | SBUS_CMD_RESET,
+				   SPICO_SERDES_DATA_RESET, &unused);
+	if (err) {
+		mutex_unlock(SBUS_RING_MTX(sbl, sbus_ring));
+		mutex_unlock(&sbl->link[port_num].serdes_mtx);
+		return err;
+	}
+
+	/* Reset low */
+	err = sbl_sbus_wr(sbl, sbus_addr, SPICO_SERDES_ADDR_RESET_EN,
+			  SPICO_SERDES_DATA_CLR_GLOBAL_RESET);
+	if (err) {
+		mutex_unlock(SBUS_RING_MTX(sbl, sbus_ring));
+		mutex_unlock(&sbl->link[port_num].serdes_mtx);
+		return err;
+	}
+
+	/* Enable ECC */
+	err = sbl_sbus_wr(sbl, sbus_addr, SPICO_SERDES_ADDR_ECC,
+			  SPICO_SERDES_DATA_SET_ECC_EN);
+	if (err) {
+		mutex_unlock(SBUS_RING_MTX(sbl, sbus_ring));
+		mutex_unlock(&sbl->link[port_num].serdes_mtx);
+		return err;
+	}
+
+	/* Clear any ECC errors that occurred previously */
+	err = sbl_sbus_wr(sbl, sbus_addr, SPICO_SERDES_ADDR_ECCLOG,
+			  SPICO_SERDES_DATA_CLR_ECC_ERR);
+	if (err) {
+		mutex_unlock(SBUS_RING_MTX(sbl, sbus_ring));
+		mutex_unlock(&sbl->link[port_num].serdes_mtx);
+		return err;
+	}
+
+	/* Enable SPICO */
+	err = sbl_sbus_wr(sbl, sbus_addr, SPICO_SERDES_ADDR_RESET_EN,
+			  SPICO_SERDES_DATA_SET_SPICO_EN);
+	if (err) {
+		mutex_unlock(SBUS_RING_MTX(sbl, sbus_ring));
+		mutex_unlock(&sbl->link[port_num].serdes_mtx);
+		return err;
+	}
+
+	/* Enable interrupts */
+	err = sbl_sbus_wr(sbl, sbus_addr, SPICO_SERDES_ADDR_INTR_DIS,
+			  SPICO_SERDES_DATA_SET_INTR_EN);
+	if (err) {
+		mutex_unlock(SBUS_RING_MTX(sbl, sbus_ring));
+		mutex_unlock(&sbl->link[port_num].serdes_mtx);
+		return err;
+	}
+
+	mutex_unlock(SBUS_RING_MTX(sbl, sbus_ring));
+	mutex_unlock(&sbl->link[port_num].serdes_mtx);
+
+	DEV_TRACE2(sbl->dev, "rc: 0");
+
+	sbl->link[port_num].pcal_running = false;
+	return 0;
+}
+
+int sbl_serdes_init(struct sbl_inst *sbl, int port_num, int serdes,
+		    int encoding, int divisor, int width)
+{
+	int err;
+	bool tx_en, rx_en;
+
+	DEV_TRACE2(sbl->dev, "p%ds%d: encoding: %d divisor: %d width: %d",
+		   port_num, serdes, encoding, divisor, width);
+
+	err = sbl_set_tx_rx_enable(sbl, port_num, serdes, false, false,
+				   false);
+	if (err)
+		return err;
+
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_PLL_RECAL,
+					SPICO_INT_DATA_NONE, NULL,
+					SPICO_INT_VALIDATE_RESULT);
+	if (err)
+		return err;
+
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_TX_PHASE_CAL,
+					SPICO_INT_DATA_NONE, NULL,
+					SPICO_INT_VALIDATE_RESULT);
+	if (err)
+		return err;
+
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_TX_BAUD,
+					(divisor & SPICO_INT_DIVIDER_MASK) |
+					SPICO_INT_DATA_TXTX_RC_NOT_SS,
+					NULL, SPICO_INT_VALIDATE_RESULT);
+	if (err)
+		return err;
+
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_WIDTH_MODE,
+					encoding | width |
+					SPICO_INT_DATA_TXRX_FC_IGNORE, NULL,
+					SPICO_INT_VALIDATE_RESULT);
+	if (err)
+		return err;
+
+	/* tx/rx based on link mode, and always configuing physical lane 0 */
+	if (tx_serdes_required_for_link_mode(sbl, port_num, serdes))
+		tx_en = true;
+	else
+		tx_en = false;
+
+	if (rx_serdes_required_for_link_mode(sbl, port_num, serdes))
+		rx_en = true;
+	else
+		rx_en = false;
+
+	err = sbl_set_tx_rx_enable(sbl, port_num, serdes, tx_en, rx_en,
+				   false);
+	if (err)
+		return err;
+
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_PCIE_SLICES,
+					SPICO_INT_DATA_TX_OVERRIDE, NULL,
+					SPICO_INT_VALIDATE_RESULT);
+	if (err)
+		return err;
+
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_PCIE_SLICES,
+					SPICO_INT_DATA_RX_EID_EN, NULL,
+					SPICO_INT_VALIDATE_RESULT);
+	if (err)
+		return err;
+
+	/* Reset signal_ok */
+	sbl_serdes_mem_rmw(sbl, port_num, serdes, SERDES_MEM_ADDR_O_CORE_STATUS,
+			   0, SERDES_CORE_STATUS_RX_SIG_OK_MASK);
+
+	/* Set PRBS for loopback mode - will be changed later */
+	if (sbl->link[port_num].loopback_mode == SBL_LOOPBACK_MODE_LOCAL) {
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_PRBS_CTRL,
+						SPICO_INT_DATA_PRBS31_AS_TXGEN, NULL,
+						SPICO_INT_VALIDATE_RESULT);
+		if (err)
+			return err;
+
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_PRBS_CTRL,
+						SPICO_INT_DATA_PRBS31_AS_RXGEN, NULL,
+						SPICO_INT_VALIDATE_RESULT);
+		if (err)
+			return err;
+	}
+	DEV_TRACE2(sbl->dev, "rc: 0");
+
+	return 0;
+}
+
+int sbl_serdes_polarity_ctrl(struct sbl_inst *sbl, int port_num, int serdes,
+			     int encoding, bool an)
+{
+	struct sbl_link *link = sbl->link + port_num;
+	u32 datapath = 0;
+	int err;
+
+	DEV_TRACE2(sbl->dev, "p%ds%d: encoding:0x%x", port_num, serdes, encoding);
+
+	/* Set Polarity Inversions */
+	switch (sbl->link[port_num].loopback_mode) {
+	case SBL_LOOPBACK_MODE_REMOTE:
+	case SBL_LOOPBACK_MODE_OFF:
+		if (sbl->switch_info->ports[port_num].serdes[serdes].txinv)
+			datapath |= SPICO_INT_DATA_SET_TXINV;
+		else
+			datapath |= SPICO_INT_DATA_CLR_TXINV;
+
+		if (sbl->switch_info->ports[port_num].serdes[serdes].rxinv)
+			datapath |= SPICO_INT_DATA_SET_RXINV;
+		else
+			datapath |= SPICO_INT_DATA_CLR_RXINV;
+
+		break;
+	case SBL_LOOPBACK_MODE_LOCAL:
+		datapath |= SPICO_INT_DATA_CLR_TXINV;
+		datapath |= SPICO_INT_DATA_CLR_RXINV;
+		break;
+	default:
+		sbl_dev_warn(sbl->dev, "%d: Unsupported loopback mode (%d)",
+			     port_num, sbl->link[port_num].loopback_mode);
+		return -EINVAL;
+	}
+
+	/* Set Precode */
+	if (encoding == SBL_ENC_PAM4) {
+		link->precoding_enabled = get_serdes_precoding(sbl, port_num);
+		if (link->precoding_enabled)
+			datapath |= SPICO_INT_DATA_SET_PRECODE;
+		else
+			datapath |= SPICO_INT_DATA_CLR_PRECODE;
+	} else {
+		datapath |= SPICO_INT_DATA_CLR_PRECODE;
+	}
+
+	/* Set Graycode & Swizzle */
+	if (an || (encoding == SBL_ENC_NRZ))
+		datapath |= SPICO_INT_DATA_CLR_GRAY_SWZ;
+	else
+		datapath |= SPICO_INT_DATA_SET_GRAY_SWZ;
+
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_POLARITY_CTRL,
+					datapath, NULL,
+					SPICO_INT_VALIDATE_RESULT);
+	if (err)
+		return err;
+
+	return 0;
+}
+
+int sbl_set_tx_rx_enable(struct sbl_inst *sbl, int port_num, int serdes,
+			 bool tx_en, bool rx_en, bool txo_en)
+{
+	u32 int_data = 0;
+	u64 core_status_value;
+	int tx_rdy, rx_rdy;
+	unsigned long last_jiffy;
+	int err;
+
+	DEV_TRACE2(sbl->dev, "p%ds%d: tx_en: %d rx_en: %d txo_en: %d",
+		   port_num, serdes, tx_en, rx_en, txo_en);
+
+	if (!sbl->is_hw)
+		return 0;
+
+	if (tx_en)
+		int_data |= SPICO_INT_DATA_SET_TX_EN;
+
+	if (rx_en)
+		int_data |= SPICO_INT_DATA_SET_RX_EN;
+
+	if (txo_en)
+		int_data |= SPICO_INT_DATA_SET_TXO_EN;
+
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_SERDES_EN,
+					int_data, NULL,
+					SPICO_INT_VALIDATE_RESULT);
+	if (err)
+		return err;
+
+	/* Wait for tx_rdy and rx_rdy to be set. */
+	last_jiffy = jiffies + msecs_to_jiffies(1000 * sbl->iattr.serdes_en_timeout);
+	do {
+		core_status_value = sbl_read64(sbl, SBL_PML_BASE(port_num) |
+					       SBL_PML_SERDES_CORE_STATUS_OFFSET(serdes));
+		tx_rdy = (core_status_value & SERDES_CORE_STATUS_TX_RDY_MASK) ? 1 : 0;
+		rx_rdy = (core_status_value & SERDES_CORE_STATUS_RX_RDY_MASK) ? 1 : 0;
+		if ((tx_rdy == tx_en) && (rx_rdy == rx_en))
+			break;
+		msleep(sbl->iattr.serdes_en_poll_interval);
+	} while (time_is_after_jiffies(last_jiffy));
+
+	if ((tx_rdy != tx_en) || (rx_rdy != rx_en)) {
+		sbl_dev_err(sbl->dev,
+			    "p%ds%d: Timeout setting tx/rx/txo enable! tx_en:%d rx_en:%d txo_en:%d tx_rdy:%d rx_rdy:%d (timeout:%ds)",
+			    port_num, serdes, tx_en, rx_en, txo_en, tx_rdy, rx_rdy,
+			    sbl->iattr.serdes_en_timeout);
+		return -ETIME;
+	}
+	DEV_TRACE2(sbl->dev, "rc 0");
+
+	return 0;
+}
+
+int sbl_set_tx_eq(struct sbl_inst *sbl, int port_num, int serdes,
+		  int atten, int pre, int post, int pre2, int pre3)
+{
+	int err, rc = 0;
+
+	DEV_TRACE2(sbl->dev,
+		   "p%ds%d: atten: %d pre: %d post: %d pre2: %d pre3: %d",
+		   port_num, serdes, atten, pre, post, pre2, pre3);
+
+	if ((atten < TXEQ_ATTEN_MIN) || (atten > TXEQ_ATTEN_MAX)) {
+		sbl_dev_err(sbl->dev,
+			    "Invalid value for atten(%d) Expected in the range of %d to %d",
+			atten, TXEQ_ATTEN_MIN, TXEQ_ATTEN_MAX);
+		return -EINVAL;
+	}
+	if ((pre < TXEQ_PRE1_MIN) || (pre > TXEQ_PRE1_MAX)) {
+		sbl_dev_err(sbl->dev,
+			    "Invalid value for pre(%d) Expected in the range of %d to %d",
+			pre, TXEQ_PRE1_MIN, TXEQ_PRE1_MAX);
+		return -EINVAL;
+	}
+	if ((post < TXEQ_POST_MIN) || (post > TXEQ_POST_MAX)) {
+		sbl_dev_err(sbl->dev,
+			    "Invalid value for post(%d) Expected in the range of %d to %d",
+			post, TXEQ_POST_MIN, TXEQ_POST_MAX);
+		return -EINVAL;
+	}
+	if ((pre2 < TXEQ_PRE2_MIN) || (pre2 > TXEQ_PRE2_MAX)) {
+		sbl_dev_err(sbl->dev,
+			    "Invalid value for pre2(%d) Expected in the range of %d to %d",
+			pre2, TXEQ_PRE2_MIN, TXEQ_PRE2_MAX);
+		return -EINVAL;
+	}
+	if ((pre3 < TXEQ_PRE3_MIN) || (pre3 > TXEQ_PRE3_MAX)) {
+		sbl_dev_err(sbl->dev,
+			    "Invalid value for pre3(%d) Expected in the range of %d to %d",
+			pre3, TXEQ_PRE3_MIN, TXEQ_PRE3_MAX);
+		return -EINVAL;
+	}
+
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_TXEQ_LOAD,
+					SPICO_INT_DATA_SET_TXEQ_ATTEN |
+						(atten & SPICO_INT_DATA_TXEQ_DATA_MASK), NULL,
+					SPICO_INT_VALIDATE_RESULT);
+	if (err)
+		return err;
+
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_TXEQ_LOAD,
+					SPICO_INT_DATA_SET_TXEQ_PRE1 |
+						(pre & SPICO_INT_DATA_TXEQ_DATA_MASK), NULL,
+					SPICO_INT_VALIDATE_RESULT);
+	if (err)
+		return err;
+
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_TXEQ_LOAD,
+					SPICO_INT_DATA_SET_TXEQ_POST |
+						(post & SPICO_INT_DATA_TXEQ_DATA_MASK), NULL,
+					SPICO_INT_VALIDATE_RESULT);
+	if (err)
+		return err;
+
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_TXEQ_LOAD,
+					SPICO_INT_DATA_SET_TXEQ_PRE2 |
+						(pre2 & SPICO_INT_DATA_TXEQ_DATA_MASK), NULL,
+					SPICO_INT_VALIDATE_RESULT);
+	if (err)
+		return err;
+
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_TXEQ_LOAD,
+					SPICO_INT_DATA_SET_TXEQ_PRE3 |
+						(pre3 & SPICO_INT_DATA_TXEQ_DATA_MASK), NULL,
+					SPICO_INT_VALIDATE_RESULT);
+	if (err)
+		return err;
+
+	DEV_TRACE2(sbl->dev, "rc: %d", rc);
+
+	return rc;
+}
+
+int sbl_set_gs(struct sbl_inst *sbl, int port_num, int serdes, int gs1, int gs2)
+{
+	int err;
+	u16 result;
+
+	DEV_TRACE2(sbl->dev, "port: %d gs1: %d gs2: %d", port_num, gs1, gs2);
+
+	if ((gs1 < RXEQ_DFE_GS1_MIN) || (gs1 > RXEQ_DFE_GS1_MAX)) {
+		sbl_dev_err(sbl->dev,
+			    "Invalid value for gs1(%d) Expected in the range of %d to %d",
+			gs1, RXEQ_DFE_GS1_MIN, RXEQ_DFE_GS1_MAX);
+		return -EINVAL;
+	}
+	if ((gs2 < RXEQ_DFE_GS2_MIN) || (gs2 > RXEQ_DFE_GS2_MAX)) {
+		sbl_dev_err(sbl->dev,
+			    "Invalid value for gs2(%d) Expected in the range of %d to %d",
+			gs2, RXEQ_DFE_GS2_MIN, RXEQ_DFE_GS2_MAX);
+		return -EINVAL;
+	}
+
+	if (!sbl->is_hw)
+		return 0;
+
+	/* Write GS1 */
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_HAL_READ,
+					SPICO_INT_DATA_HAL_CTLE_GS1,
+					&result,
+					SPICO_INT_RETURN_RESULT);
+	if (err)
+		return err;
+
+	DEV_TRACE2(sbl->dev,
+		   "p%ds%d: Updating GS1 from 0x%x to 0x%x",
+		   port_num, serdes, result, gs1);
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_HAL_WRITE,
+					gs1, &result,
+					SPICO_INT_RETURN_RESULT);
+	if (err)
+		return err;
+
+	if (result != SPICO_INT_CM4_HAL_READ) {
+		sbl_dev_err(sbl->dev,
+			    "p%ds%d: Failed updating gs1 (0x%x)!",
+			port_num, serdes, gs1);
+		return -EBADE;
+	}
+
+	/* Write GS2 */
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_HAL_READ,
+					SPICO_INT_DATA_HAL_CTLE_GS2,
+					&result,
+					SPICO_INT_RETURN_RESULT);
+	if (err)
+		return err;
+
+	DEV_TRACE2(sbl->dev,
+		   "p%ds%d: Updating GS2 from 0x%x to 0x%x",
+		   port_num, serdes, result, gs2);
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_HAL_WRITE,
+					gs2, &result,
+					SPICO_INT_RETURN_RESULT);
+	if (err)
+		return err;
+
+	if (result != SPICO_INT_CM4_HAL_READ) {
+		sbl_dev_err(sbl->dev,
+			    "p%ds%d: Failed updating gs2 (0x%x)!",
+			port_num, serdes, gs2);
+		return -EBADE;
+	}
+
+	DEV_TRACE2(sbl->dev, "rc: 0");
+
+	return 0;
+}
+
+int sbl_set_tx_data_sel(struct sbl_inst *sbl, int port_num, int serdes,
+			int data_sel)
+{
+	int err;
+	u16 result;
+	int retry_cnt = 0;
+
+	DEV_TRACE2(sbl->dev, "p%ds%d data_sel: %d", port_num, serdes, data_sel);
+
+	if (!sbl->is_hw)
+		return 0;
+
+	if (data_sel == SBL_DS_CORE) {
+		do {
+			err = sbl_serdes_spico_int(sbl, port_num, serdes,
+						   SPICO_INT_CM4_PRBS_CTRL,
+						SPICO_INT_DATA_DISABLE_TXRXGEN,
+						&result,
+						SPICO_INT_RETURN_RESULT);
+			if (err)
+				return err;
+
+			if (result == SPICO_INT_DATA_PRBS_SUCCESS)
+				return 0;
+
+			sbl_dev_dbg(sbl->dev, "p%ds%d: attempt %d tx data sel returned %d",
+				    port_num, serdes, retry_cnt, result);
+			usleep_range(1000, 2000);
+		} while (retry_cnt++ < SPICO_INT_DATA_PRBS_RETRY_LIMIT);
+	} else { /* SBL_DS_PRBS */
+		do {
+			err = sbl_serdes_spico_int(sbl, port_num, serdes,
+						   SPICO_INT_CM4_PRBS_CTRL,
+						SPICO_INT_DATA_PRBS31_AS_TXGEN,
+						&result,
+						SPICO_INT_RETURN_RESULT);
+			if (err)
+				return err;
+
+			if (result == SPICO_INT_DATA_PRBS_SUCCESS)
+				return 0;
+
+			sbl_dev_dbg(sbl->dev, "p%ds%d: attempt %d tx data sel returned %d",
+				    port_num, serdes, retry_cnt, result);
+			usleep_range(1000, 2000);
+		} while (retry_cnt++ < SPICO_INT_DATA_PRBS_RETRY_LIMIT);
+	}
+
+	sbl_dev_err(sbl->dev, "p%ds%d: Failed to set TX data select (%d)!",
+		    port_num, serdes, data_sel);
+	return -EBADE;
+}
+
+int sbl_set_prbs_rx_mode(struct sbl_inst *sbl, int port_num, int serdes)
+{
+	int err;
+	/* Avago_serdes_data_qual_t qual; */
+
+	DEV_TRACE2(sbl->dev, "p%ds%d", port_num, serdes);
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_DFE_CTRL,
+					SPICO_INT_DATA_NONE, NULL,
+					SPICO_INT_VALIDATE_RESULT);
+	if (err)
+		return err;
+
+	err = sbl_serdes_dfe_tune_wait(sbl, port_num);
+	if (err)
+		return err;
+
+	DEV_TRACE2(sbl->dev, "rc: 0");
+
+	return 0;
+}
+
+int sbl_apply_sbus_divider(struct sbl_inst *sbl, int divider)
+{
+	int sbus_ring, err;
+	u32 sbus_addr;
+
+	/* Force divider to either SPEEDUP or DFLT */
+	if (divider == SBL_SBUS_DIVIDER_SPEEDUP) {
+		sbl_dev_dbg(sbl->dev, "Applying SBUS speedup.");
+	} else {
+		sbl_dev_dbg(sbl->dev, "Disabling SBUS speedup.");
+		divider = SBL_SBUS_DIVIDER_DFLT;
+	}
+
+	/* increase SBUS ring clock frequency */
+	for (sbus_ring = 0; sbus_ring < sbl->switch_info->num_sbus_rings; ++sbus_ring) {
+		sbus_addr = SBUS_ADDR(sbus_ring, SBUS_BCAST_SBM);
+		DEV_TRACE2(sbl->dev, "ring: %d divider_exp: %d", sbus_ring,
+			   divider);
+		/* SBUS Critical Section */
+		mutex_lock(SBUS_RING_MTX(sbl, sbus_ring));
+		err = sbl_sbus_wr(sbl, sbus_addr, SBM_CRM_ADDR_CLK_DIV, divider);
+		mutex_unlock(SBUS_RING_MTX(sbl, sbus_ring));
+		if (err)
+			return err;
+	}
+
+	/* adjust op flags */
+	if (divider == SBL_SBUS_DIVIDER_SPEEDUP)
+		sbl->sbus_op_flags = sbl->iattr.sbus_op_flags_fast;
+	else /* SBL_SBUS_DIVIDER_DFLT */
+		sbl->sbus_op_flags = sbl->iattr.sbus_op_flags_slow;
+
+	sbl_dev_dbg(sbl->dev, "SBus divider update complete");
+	return 0;
+}
+
+/* Note - cannot do broadcast here! */
+int sbl_serdes_dfe_tune_start(struct sbl_inst *sbl, int port_num, int serdes,
+			      bool is_retune)
+{
+	int err;
+	u16 result;
+	struct sbl_link *link = sbl->link + port_num;
+
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_GET_RX_EQ,
+					SPICO_INT_DATA_RXEQ_STS_DFE_STS,
+					&result, SPICO_INT_RETURN_RESULT);
+	if (err) {
+		sbl_dev_err(sbl->dev, "p%ds%d: Failed checking status of DFE tune!",
+			    port_num, serdes);
+		return -EIO;
+	}
+	if (result == DFE_CAL_DONE) {
+		sbl_dev_dbg(sbl->dev, "p%ds%d: DFE done before we started",
+			    port_num, serdes);
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_SET_RX_EQ,
+					SPICO_INT_DATA_RXEQ_STS_DFE_STS | 0x0,
+					NULL, SPICO_INT_IGNORE_RESULT);
+		if (err) {
+			sbl_dev_err(sbl->dev, "p%ds%d: Failed to clear DFE sts!",
+				    port_num, serdes);
+			return err;
+		}
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_GET_RX_EQ,
+					SPICO_INT_DATA_RXEQ_STS_DFE_STS,
+					&result, SPICO_INT_RETURN_RESULT);
+		if (err) {
+			sbl_dev_err(sbl->dev,
+				    "p%ds%d: Failed checking status of DFE tune!",
+				port_num, serdes);
+			return -EIO;
+		}
+		if (result == DFE_CAL_DONE) {
+			sbl_dev_err(sbl->dev, "p%ds%d: DFE done before we started!",
+				    port_num, serdes);
+			return -ENOMSG;
+		}
+	}
+
+	DEV_TRACE2(sbl->dev, "p%ds%d is_retune:%d", port_num, serdes,
+		   is_retune);
+
+	/* Set effort level */
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_HAL_READ,
+					SPICO_INT_DATA_ICAL_EFFORT_SEL, &result,
+					SPICO_INT_RETURN_RESULT);
+	if (err)
+		return err;
+
+	if (sbl_debug_option(sbl, port_num, SBL_DEBUG_FORCE_MAX_EFFORT)) {
+		link->ical_effort = SPICO_INT_DATA_ICAL_MAX_EFFORT;
+	} else if (sbl_debug_option(sbl, port_num, SBL_DEBUG_FORCE_MED_EFFORT)) {
+		link->ical_effort = SPICO_INT_DATA_ICAL_MED_EFFORT;
+	} else if (sbl_debug_option(sbl, port_num, SBL_DEBUG_FORCE_MIN_EFFORT)) {
+		link->ical_effort = SPICO_INT_DATA_ICAL_MIN_EFFORT;
+	} else if (link->blattr.options & SBL_OPT_DFE_ALWAYS_MAX_EFFORT) {
+		link->ical_effort = SPICO_INT_DATA_ICAL_MAX_EFFORT;
+	} else if (link->blattr.options & SBL_OPT_DFE_ALWAYS_MED_EFFORT) {
+		link->ical_effort = SPICO_INT_DATA_ICAL_MED_EFFORT;
+	} else if (link->blattr.options & SBL_OPT_DFE_ALWAYS_MIN_EFFORT) {
+		link->ical_effort = SPICO_INT_DATA_ICAL_MIN_EFFORT;
+	} else if (link->dfe_tune_count < 3) {
+		/* medium effort for first few attempts */
+		link->ical_effort = SPICO_INT_DATA_ICAL_MED_EFFORT;
+	} else {
+		/* rest full effort */
+		link->ical_effort = SPICO_INT_DATA_ICAL_MAX_EFFORT;
+	}
+	/* sync with sysfs */
+	mb();
+
+	if (!sbl->is_hw)
+		return 0;
+
+	DEV_TRACE2(sbl->dev, "p%ds%d: Updating ICAL effort from 0x%x to 0x%x",
+		   port_num, serdes, result, link->ical_effort);
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_HAL_WRITE,
+					link->ical_effort, &result,
+					SPICO_INT_RETURN_RESULT);
+	if (err)
+		return err;
+
+	if (result != SPICO_INT_CM4_HAL_READ) {
+		sbl_dev_err(sbl->dev, "p%ds%d: Failed updating ICAL effort (0x%x)!",
+			    port_num, serdes, link->ical_effort);
+		return -EBADE;
+	}
+	sbl_dev_dbg(sbl->dev, "p%ds%d: Setup ICAL effort 0x%x",
+		    port_num, serdes, link->ical_effort);
+
+	/* Initiate DFE tune */
+	err = sbl_serdes_spico_int(sbl, port_num, serdes,
+				   SPICO_INT_CM4_DFE_CTRL,
+					SPICO_INT_DATA_DFE_ICAL, NULL,
+					SPICO_INT_VALIDATE_RESULT);
+	if (err)
+		return err;
+
+	DEV_TRACE2(sbl->dev, "rc: 0");
+	return 0;
+}
+
+int sbl_serdes_dfe_tune_wait(struct sbl_inst *sbl, int port_num)
+{
+	struct sbl_link *link = sbl->link + port_num;
+	u16 result;
+	int err, serdes;
+	u8 in_progress_mask, serdes_mask, tuned_mask = 0;
+	unsigned long last_jiffy;
+
+	serdes_mask = get_serdes_rx_mask(sbl, port_num);
+	in_progress_mask = serdes_mask;
+
+	DEV_TRACE2(sbl->dev, "p%d serdes_mask:0x%x", port_num, serdes_mask);
+
+	last_jiffy = jiffies + msecs_to_jiffies(1000 * link->blattr.dfe_timeout);
+	do {
+		for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+			if (!rx_serdes_required_for_link_mode(sbl, port_num, serdes))
+				continue;
+
+			if (!((1 << serdes) & in_progress_mask))
+				continue;
+
+			err = sbl_serdes_spico_int(sbl, port_num, serdes,
+						   SPICO_INT_CM4_GET_RX_EQ,
+							SPICO_INT_DATA_RXEQ_STS_DFE_STS,
+							&result, SPICO_INT_RETURN_RESULT);
+			if (err) {
+				sbl_dev_err(sbl->dev, "p%ds%d: Failed checking status of DFE tune!",
+					    port_num, serdes);
+				return -EIO;
+			}
+			if (!sbl->is_hw)
+				result = DFE_CAL_DONE;
+
+			if (result & DFE_LOS_MASK) {
+				sbl_dev_warn(sbl->dev, "p%ds%d: Loss of signal when in DFE tune!",
+					     port_num, serdes);
+				return -ENOMSG;
+			} else if (result & DFE_CAL_RUN_IN_PRGRS_MASK) {
+				sbl_dev_dbg(sbl->dev, "p%ds%d: DFE still in progress", port_num,
+					    serdes);
+			} else if (result & DFE_CAL_DONE) {
+				sbl_dev_dbg(sbl->dev, "p%ds%d: DFE done", port_num,
+					    serdes);
+				tuned_mask      |= (1 << serdes);
+				in_progress_mask &= ~(1 << serdes);
+			} else {
+				sbl_dev_dbg(sbl->dev, "p%ds%d: DFE complete", port_num, serdes);
+				tuned_mask      |= (1 << serdes);
+				in_progress_mask &= ~(1 << serdes);
+			}
+		}
+		if (!in_progress_mask)
+			break;
+
+		msleep(link->blattr.dfe_poll_interval);
+	} while (time_is_after_jiffies(last_jiffy) &&
+		 !sbl_start_timeout(sbl, port_num) &&
+		 !sbl_base_link_start_cancelled(sbl, port_num));
+
+	if (serdes_mask == tuned_mask)
+		return 0;
+
+	if (sbl_base_link_start_cancelled(sbl, port_num))
+		return -ECANCELED;
+
+	if (sbl_start_timeout(sbl, port_num)) {
+		sbl_dev_dbg(sbl->dev, "p%d: start timeout waiting for DFE to tune to complete\n",
+			    port_num);
+		return -ETIMEDOUT;
+	}
+
+	/* just didnt finish */
+	sbl_dev_dbg(sbl->dev, "p%d serdes_mask:0x%x: Timeout waiting for DFE to complete (timeout:%ds)",
+		    port_num, serdes_mask, link->blattr.dfe_timeout);
+	return -ETIME;
+}
+
+int sbl_port_dfe_tune_start(struct sbl_inst *sbl, int port_num, bool is_retune)
+{
+	int err, serdes;
+
+	sbl_dev_dbg(sbl->dev, "p%d: DFE tune retune:%d", port_num, is_retune);
+
+	/* DFE Tune */
+	for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+		if (!(rx_serdes_required_for_link_mode(sbl, port_num, serdes) ||
+		      tx_serdes_required_for_link_mode(sbl, port_num, serdes)))
+			continue;
+		err = sbl_serdes_dfe_tune_start(sbl, port_num, serdes,
+						is_retune);
+		if (err) {
+			sbl_dev_err(sbl->dev, "p%d: Failed to start DFE tune!",
+				    port_num);
+			return err;
+		}
+	}
+
+	return 0;
+}
+
+int sbl_port_dfe_tune_wait(struct sbl_inst *sbl, int port_num)
+{
+	int err;
+
+	/* DFE Tune */
+	err = sbl_serdes_dfe_tune_wait(sbl, port_num);
+	if (err) {
+		switch (err) {
+		case -ECANCELED:
+			sbl_dev_dbg(sbl->dev, "p%d: DFE wait cancelled", port_num);
+			break;
+		case -ETIMEDOUT:
+			sbl_dev_dbg(sbl->dev, "p%d: DFE wait start timeout", port_num);
+			break;
+		case -ETIME:
+			sbl_dev_dbg(sbl->dev, "p%d: DFE wait timed out", port_num);
+			break;
+		default:
+			sbl_dev_err(sbl->dev, "p%d: DFE wait failed [%d]", port_num, err);
+		}
+		return err;
+	}
+
+	/* extra checks */
+	if (sbl->link[port_num].loopback_mode != SBL_LOOPBACK_MODE_LOCAL) {
+
+		/* Eye height check (without pcal active) */
+		err = sbl_port_check_eyes(sbl, port_num);
+		if (err) {
+			sbl_dev_dbg(sbl->dev, "p%d: some eyes bad", port_num);
+			return err;
+		}
+		sbl_dev_dbg(sbl->dev, "p%d: all eyes good", port_num);
+
+		/* Validate tuning params */
+		err = sbl_check_serdes_tuning_params(sbl, port_num);
+		if (err) {
+			sbl_dev_err(sbl->dev, "p%d: some tuning params bad", port_num);
+			sbl->link[port_num].tune_param_oob_count++;
+			return -ELNRNG;
+		}
+		sbl_dev_dbg(sbl->dev, "p%d: all tuning params good", port_num);
+		sbl->link[port_num].tune_param_oob_count = 0;
+
+		/* Set up PCAL */
+		/*   (This can reduce the eye heights but increase the eye widths).
+		     Future eye height checks will be held off for a period while
+		     the pcal process settles */
+		if (sbl->link[port_num].blattr.options & SBL_OPT_ENABLE_PCAL) {
+			if (sbl_debug_option(sbl, port_num, SBL_DEBUG_INHIBIT_PCAL)) {
+				sbl_dev_warn(sbl->dev, "p%d: pcal inhibited\n", port_num);
+			} else {
+				err = sbl_port_start_pcal(sbl, port_num);
+				if (err)
+					return err;
+			}
+		} else {
+			sbl_dev_dbg(sbl->dev, "p%d: PCAL is disabled!", port_num);
+		}
+	}
+
+	return 0;
+}
+
+void sbl_log_port_eye_heights(struct sbl_inst *sbl, int port_num)
+{
+	struct sbl_link *link = sbl->link + port_num;
+	int eye, num_eyes = 0;
+	int eye_heights[NUM_PAM4_EYES];
+	int serdes;
+
+	switch (link->link_mode) {
+	default:
+	case SBL_LINK_MODE_BJ_100G: /* NRZ */
+		num_eyes = NUM_NRZ_EYES;
+		break;
+	case SBL_LINK_MODE_CD_50G:
+	case SBL_LINK_MODE_CD_100G:
+	case SBL_LINK_MODE_BS_200G: /* PAM4 */
+		num_eyes = NUM_PAM4_EYES;
+		break;
+	}
+
+	for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+		if (!rx_serdes_required_for_link_mode(sbl, port_num, serdes))
+			continue;
+		if (sbl_get_eye_heights(sbl, port_num, serdes, eye_heights)) {
+			sbl_dev_err(sbl->dev, "p%ds%d: Failed to read eye heights",
+				    port_num, serdes);
+			return;
+		}
+		for (eye = 0; eye < num_eyes; ++eye) {
+			sbl_dev_info(sbl->dev, "p%ds%d eye[%d] height: 0x%x",
+				     port_num, serdes, eye, eye_heights[eye]);
+		}
+	}
+}
+
+int sbl_port_check_eyes(struct sbl_inst *sbl, int port_num)
+{
+	struct sbl_link *link = sbl->link + port_num;
+	int min_eye_height, max_eye_height;
+	int eye, num_eyes = 0;
+	int eye_heights[NUM_PAM4_EYES];
+	unsigned long holdoff_end_jiffies;
+	int serdes;
+
+	/* pcal can generate bad eyes temporarily when its starting up so
+	   if its not settled yet ignore this test */
+	if (link->pcal_running && link->blattr.pcal_eyecheck_holdoff) {
+		holdoff_end_jiffies = link->pcal_start_jiffies +
+				msecs_to_jiffies(link->blattr.pcal_eyecheck_holdoff);
+		if (time_is_after_jiffies(holdoff_end_jiffies)) {
+			sbl_dev_info(sbl->dev, "p%d: holding off eye checks",
+				     port_num);
+			return 0;
+		}
+	}
+
+	if (!sbl->is_hw)
+		return 0;
+
+	/* setup test criteria */
+	switch (link->link_mode) {
+	default:
+	case SBL_LINK_MODE_BJ_100G: /* NRZ */
+		num_eyes = NUM_NRZ_EYES;
+		min_eye_height =
+			link->blattr.nrz_min_eye_height;
+		max_eye_height =
+			link->blattr.nrz_max_eye_height;
+		break;
+	case SBL_LINK_MODE_CD_50G:
+	case SBL_LINK_MODE_CD_100G:
+	case SBL_LINK_MODE_BS_200G: /* PAM4 */
+		num_eyes = NUM_PAM4_EYES;
+		min_eye_height =
+			link->blattr.pam4_min_eye_height;
+		max_eye_height =
+			link->blattr.pam4_max_eye_height;
+		break;
+	}
+
+	/* check eye height */
+	for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+		if (!rx_serdes_required_for_link_mode(sbl, port_num, serdes))
+			continue;
+
+		sbl_dev_dbg(sbl->dev, "p%ds%d: checking eye height(s)", port_num, serdes);
+		if (sbl_get_eye_heights(sbl, port_num, serdes, eye_heights)) {
+			sbl_dev_err(sbl->dev, "p%ds%d: Failed to read eye heights",
+				    port_num, serdes);
+			return -EIO;
+		}
+
+		for (eye = 0; eye < num_eyes; ++eye) {
+			sbl_dev_dbg(sbl->dev, "p%ds%d eye[%d] height: 0x%x",
+				    port_num, serdes, eye, eye_heights[eye]);
+			if (eye_heights[eye] < min_eye_height) {
+				sbl_dev_dbg(sbl->dev,
+					    "p%ds%d eye[%d] height (0x%x) less than requirement (0x%x)!",
+					    port_num, serdes, eye, eye_heights[eye],
+					    min_eye_height);
+				return -ECHRNG;
+			} else if ((sbl->link[port_num].loopback_mode !=
+				    SBL_LOOPBACK_MODE_LOCAL) &&
+				   (eye_heights[eye] > max_eye_height)) {
+				sbl_dev_dbg(sbl->dev,
+					    "p%ds%d eye[%d] height (0x%x) greater than max (0x%x)!",
+					    port_num, serdes, eye, eye_heights[eye],
+					    max_eye_height);
+				return -ECHRNG;
+			}
+		}
+	}
+
+	return 0;
+}
+
+/* Constructs maps of */
+/* - active serdes (ones in use for this mode)
+   - serdes not reporting electrical idle (i.e. seeing a signal)
+   - serdes with all good eyes (i.e. data can be extracted from the signal) */
+
+/* Only called on Rosetta */
+/* active map is always valid and returned
+   other maps only valid if function succeeds otherwise zero */
+
+/* Electrical idle detect seems fast to respond but sometimes shows unexpected
+   and probably wrong, electrical idle. */
+
+/* TODO investigate EID filtering - if its fast enough? */
+
+/* Eye height measurements seem to be very slow at being updated after a change to the
+   serdes. BRCM say this is because the PLL takes a long time to unlock
+   There is an optional delay that can be added to give the eye heights time
+   to be updated. */
+
+int sbl_port_get_serdes_state_maps(struct sbl_inst *sbl, int port_num, u8 *active_map,
+				   u8 *not_idle_map, u8 *ok_eye_map)
+{
+	struct sbl_link *link = sbl->link + port_num;
+	int min_eye_height, max_eye_height;
+	int eye, num_eyes = 0;
+	u64 core_status;
+	int eye_heights[NUM_PAM4_EYES];
+	int err;
+	int serdes;
+
+	if (!active_map || !not_idle_map || !ok_eye_map)
+		return -EINVAL;
+
+	*active_map = *not_idle_map = *ok_eye_map = 0;
+
+	/* always build active serdes map */
+	for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes)
+		if (rx_serdes_required_for_link_mode(sbl, port_num, serdes))
+			*active_map  |= (1 << serdes);
+
+	/* must be configured to get criteria from attributes */
+	if (!link->blconfigured)
+		return -EUCLEAN;
+
+	/* get eye test criteria */
+	switch (link->link_mode) {
+	case SBL_LINK_MODE_BJ_100G: /* NRZ */
+		num_eyes = NUM_NRZ_EYES;
+		min_eye_height = link->blattr.nrz_min_eye_height;
+		max_eye_height = link->blattr.nrz_max_eye_height;
+		break;
+	case SBL_LINK_MODE_CD_50G:
+	case SBL_LINK_MODE_CD_100G:
+	case SBL_LINK_MODE_BS_200G: /* PAM4 */
+		num_eyes = NUM_PAM4_EYES;
+		min_eye_height = link->blattr.pam4_min_eye_height;
+		max_eye_height = link->blattr.pam4_max_eye_height;
+		break;
+	default:
+		sbl_dev_dbg(sbl->dev, "%d: get_serdes_state_maps, unrecognised link mode (%d)",
+			    port_num, link->link_mode);
+		return -ENODATA;
+	}
+
+	/* check electrical idle not asserted */
+	for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+
+		if (!rx_serdes_required_for_link_mode(sbl, port_num, serdes))
+			continue;
+
+		core_status = sbl_read64(sbl, SBL_PML_BASE(port_num) |
+		       SBL_PML_SERDES_CORE_STATUS_OFFSET(serdes));
+
+		/* TODO fix to use normal address map macro */
+		if (!(core_status & SERDES_CORE_STATUS_RX_IDLE_DETECT_MASK))
+			*not_idle_map |= 1 << serdes;
+	}
+
+	/* optionally wait a little to allow current eye heights to be available */
+	if (sbl_debug_option(sbl, port_num, SBL_DEBUG_SERDES_MAP_DELAY))
+		msleep(150);
+
+	/* check eyes */
+	for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+
+		/* assume good if serdes is used */
+		if (rx_serdes_required_for_link_mode(sbl, port_num, serdes))
+			*ok_eye_map |= (1 << serdes);
+		else
+			continue;
+
+		/* get eyes */
+		err = sbl_get_eye_heights(sbl, port_num, serdes, eye_heights);
+		if (err) {
+			sbl_dev_dbg(sbl->dev, "%d: get_serdes_state_maps, get_eye_heights failed [%d]",
+				    port_num, err);
+			*not_idle_map = *ok_eye_map = 0;
+			return err;
+		}
+
+		/* All eyes must be good
+		   drop from map if any bad eyes found */
+		for (eye = 0; eye < num_eyes; ++eye) {
+
+			if (eye_heights[eye] < min_eye_height) {
+				*ok_eye_map &= ~(1 << serdes);
+				break;
+			} else if ((link->loopback_mode != SBL_LOOPBACK_MODE_LOCAL) &&
+				   (eye_heights[eye] > max_eye_height)) {
+				*ok_eye_map &= ~(1 << serdes);
+				break;
+			}
+		}
+	}
+
+	return 0;
+}
+
+int sbl_port_start_pcal(struct sbl_inst *sbl, int port_num)
+{
+	struct sbl_link *link = sbl->link + port_num;
+	int serdes, err;
+
+	if (link->pcal_running)
+		return 0;
+
+	sbl_dev_dbg(sbl->dev, "p%d: starting PCAL", port_num);
+	for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+		if (!rx_serdes_required_for_link_mode(sbl, port_num, serdes))
+			continue;
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_DFE_CTRL,
+						SPICO_INT_DATA_DFE_CONT_PCAL,
+						NULL,
+						SPICO_INT_VALIDATE_RESULT);
+		if (err)
+			return err;
+	}
+	link->pcal_running = true;
+	link->pcal_start_jiffies = jiffies;
+
+	return 0;
+}
+
+int sbl_port_stop_pcal(struct sbl_inst *sbl, int port_num)
+{
+	int serdes, err;
+
+	if (!sbl->link[port_num].pcal_running)
+		return 0;
+
+	sbl_dev_dbg(sbl->dev, "p%d: stopping PCAL", port_num);
+	for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+		if (!rx_serdes_required_for_link_mode(sbl, port_num, serdes))
+			continue;
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_DFE_CTRL,
+						SPICO_INT_DATA_DFE_PAUSE_PCAL,
+						NULL,
+						SPICO_INT_VALIDATE_RESULT);
+		if (err)
+			return err;
+	}
+
+	sbl->link[port_num].pcal_running = false;
+	return 0;
+}
+
+int sbl_port_dfe_tune(struct sbl_inst *sbl, int port_num, bool is_retune)
+{
+	int err;
+
+	sbl_dev_dbg(sbl->dev, "p%d: Starting DFE %stune...", port_num,
+		    is_retune ? "re" : "");
+
+	sbl_link_tune_begin(sbl, port_num);
+
+	err = sbl_port_dfe_tune_start(sbl, port_num, is_retune);
+	if (err) {
+		sbl_dev_dbg(sbl->dev, "p%d: Failed to start DFE tune!", port_num);
+		goto out;
+	}
+
+	sbl_dev_dbg(sbl->dev, "p%d: Waiting for DFE tuning to complete...", port_num);
+	err = sbl_port_dfe_tune_wait(sbl, port_num);
+	if (err) {
+		sbl_dev_dbg(sbl->dev, "p%d: DFE tune failed!", port_num);
+		goto out;
+	}
+
+out:
+	sbl_link_tune_update_total_timespec(sbl, port_num);
+	return err;
+}
+
+int sbl_serdes_minitune_setup(struct sbl_inst *sbl, int port_num)
+{
+	int err;
+	u16 result;
+	int serdes;
+
+	err = sbl_serdes_config(sbl, port_num, false);
+	if (err) {
+		sbl_dev_err(sbl->dev,
+			    "mt: SerDes config failed for port %d with err %d",
+			port_num, err);
+		return err;
+	}
+
+	for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+		if (!rx_serdes_required_for_link_mode(sbl, port_num, serdes))
+			continue;
+		/* Set effort level */
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_READ,
+						SPICO_INT_DATA_ICAL_EFFORT_SEL,
+						&result,
+						SPICO_INT_RETURN_RESULT);
+		if (err)
+			return err;
+
+		DEV_TRACE2(sbl->dev,
+			   "p%ds%d: mt: Updating ICAL effort from 0x%x to 0x%x",
+			   port_num, serdes, result,
+			   SPICO_INT_DATA_ICAL_EFFORT_0);
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_WRITE,
+						SPICO_INT_DATA_ICAL_EFFORT_0,
+						&result,
+						SPICO_INT_RETURN_RESULT);
+		if (err)
+			return err;
+
+		if (result != SPICO_INT_CM4_HAL_READ) {
+			sbl_dev_err(sbl->dev,
+				    "p%ds%d: mt: Failed updating ICAL effort (0x%x)!",
+				port_num, serdes, SPICO_INT_DATA_ICAL_EFFORT_0);
+			return -EBADE;
+		}
+		/* Enable EID based on DFE tuning */
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_READ,
+						SPICO_INT_DATA_EID_FILTER_SEL,
+						&result,
+						SPICO_INT_RETURN_RESULT);
+		if (err)
+			return err;
+
+		DEV_TRACE2(sbl->dev,
+			   "p%ds%d: mt: Updating EID Filter from 0x%x to 0x%x",
+			   port_num, serdes, result,
+			   SPICO_INT_DATA_EID_FILTER_DFE);
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_WRITE,
+						SPICO_INT_DATA_EID_FILTER_DFE,
+						&result,
+						SPICO_INT_RETURN_RESULT);
+		if (err)
+			return err;
+
+		if (result != SPICO_INT_CM4_HAL_READ) {
+			sbl_dev_err(sbl->dev,
+				    "p%ds%d: mt: Failed updating EID Filter (0x%x)!",
+				port_num, serdes,
+				SPICO_INT_DATA_EID_FILTER_DFE);
+			return -EBADE;
+		}
+	}
+
+	return 0;
+}
+
+int sbl_serdes_minitune_block(struct sbl_inst *sbl, int port_num)
+{
+	u64 core_status_value;
+	u8 sig_ok_mask = 0;
+	u8 tgt_serdes = 0;
+	u16 result;
+	int serdes;
+	int err;
+	u8 serdes_mask;
+	unsigned long last_jiffy;
+
+	serdes_mask = get_serdes_rx_mask(sbl, port_num);
+
+	/* Wait for signal OK */
+	last_jiffy = jiffies + msecs_to_jiffies(1000 * sbl->link[port_num].blattr.lpd_timeout);
+	do {
+		for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+			if (!rx_serdes_required_for_link_mode(sbl, port_num, serdes))
+				continue;
+			tgt_serdes = 1 << serdes;
+
+			if (tgt_serdes & sig_ok_mask)
+				continue;
+
+			core_status_value = sbl_read64(sbl,
+						       SBL_PML_BASE(port_num) |
+			       SBL_PML_SERDES_CORE_STATUS_OFFSET(serdes));
+			if (((core_status_value &
+			      SERDES_CORE_STATUS_RX_IDLE_DETECT_MASK) == 0) &&
+			    ((core_status_value &
+			      SERDES_CORE_STATUS_RX_SIG_OK_MASK) == 0)) {
+				sbl_dev_dbg(sbl->dev, "p%ds%d: mt: Found EID 0 or SIG OK 0", port_num, serdes);
+				sig_ok_mask |= 1 << serdes;
+				continue;
+			} else {
+				DEV_TRACE2(sbl->dev, "p%ds%d: mt: Waiting for EID==0(%d) or SIG OK==0(%d)",
+					   port_num, serdes,
+						(core_status_value & SERDES_CORE_STATUS_RX_IDLE_DETECT_MASK) ? 1 : 0,
+						(core_status_value & SERDES_CORE_STATUS_RX_SIG_OK_MASK) ? 1 : 0);
+			}
+		}
+
+		if (sig_ok_mask == serdes_mask)
+			break;
+		if (sbl_base_link_start_cancelled(sbl, port_num))
+			break;
+		if (sbl_start_timeout(sbl, port_num))
+			break;
+
+		msleep(sbl->link[port_num].blattr.lpd_poll_interval);
+	} while (time_is_after_jiffies(last_jiffy));
+
+	/* Disable EID filter */
+	for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+		if (!rx_serdes_required_for_link_mode(sbl, port_num, serdes))
+			continue;
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_READ,
+						SPICO_INT_DATA_EID_FILTER_SEL,
+						&result,
+						SPICO_INT_RETURN_RESULT);
+		if (err) {
+			sbl_dev_err(sbl->dev,
+				    "p%ds%d: mt block exit@1: sbl_serdes_spico_int failed [%d]",
+				port_num, serdes, err);
+			return err;
+		}
+		DEV_TRACE2(sbl->dev,
+			   "p%ds%d: Updating EID Filter from 0x%x to 0x%x",
+			   port_num, serdes, result,
+			   SPICO_INT_DATA_EID_FILTER_OFF);
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_HAL_WRITE,
+						SPICO_INT_DATA_EID_FILTER_OFF,
+						&result,
+						SPICO_INT_RETURN_RESULT);
+		if (err) {
+			sbl_dev_err(sbl->dev,
+				    "p%ds%d: mt block exit@2: sbl_serdes_spico_int failed [%d]",
+				port_num, serdes, err);
+			return err;
+		}
+		if (result != SPICO_INT_CM4_HAL_READ) {
+			sbl_dev_err(sbl->dev,
+				    "p%ds%d: mt: Failed updating EID Filter (0x%x)!",
+				port_num, serdes,
+				SPICO_INT_DATA_EID_FILTER_OFF);
+			return -EBADE;
+		}
+	}
+	/* Make sure all tunes are complete */
+	err = sbl_serdes_dfe_tune_wait(sbl, port_num);
+	if (err) {
+		sbl_dev_dbg(sbl->dev, "%d: mt: Failed to detect with link partner!",
+			    port_num);
+	}
+
+	if (sbl_base_link_start_cancelled(sbl, port_num))
+		return -ECANCELED;
+	if (sbl_start_timeout(sbl, port_num))
+		return -ETIMEDOUT;
+
+	if (sig_ok_mask != serdes_mask) {
+		sbl_dev_dbg(sbl->dev,
+			    "p%ds<mask>0x%x: mt: SerDes signal ok not detected (timeout:%ds)",
+			    port_num, (serdes_mask & ~sig_ok_mask),
+			    sbl->link[port_num].blattr.lpd_timeout);
+		return -ETIME;
+	}
+
+	return err;
+}
+
+int sbl_serdes_config(struct sbl_inst *sbl, int port_num, bool allow_an)
+{
+	int err, encoding, divisor, width, serdes, j;
+	struct sbl_sc_values values = {0};
+	u16 result;
+	u32 rx_phase_slip_cnt;
+	u32 rx_phase_slip_reapply;
+	bool use_default_tx_eq, use_default_gs;
+	bool tx_en, rx_en, txo_en;
+
+	if (allow_an && (sbl->link[port_num].blattr.config_target != SBL_BASE_LINK_CONFIG_PEC)) {
+		sbl_dev_err(sbl->dev, "%d: AN allowed but has no config", port_num);
+		return -ENAVAIL;
+	}
+
+	/* Stop continuous tune */
+	err = sbl_port_stop_pcal(sbl, port_num);
+	if (err) {
+		sbl_dev_warn(sbl->dev, "%d: serdes config: stop pcal failed [%d]",
+			     port_num, err);
+		return err;
+	}
+
+	/* Handle requested speed */
+	if (allow_an) {
+		switch (sbl->link[port_num].blattr.pec.an_mode) {
+		case SBL_AN_MODE_FIXED:
+		case SBL_AN_MODE_ON:
+			encoding = SBL_ENC_NRZ;
+			divisor  = SBL_DIV_AN;
+			width    = SBL_WID_AN;
+			break;
+		case SBL_AN_MODE_OFF:
+		default:
+			sbl_dev_warn(sbl->dev, "%d: Unsupported an mode (%d)",
+				     port_num, sbl->link[port_num].blattr.pec.an_mode);
+			return -EINVAL;
+
+		}
+	} else {
+		switch (sbl->link[port_num].link_mode) {
+		case SBL_LINK_MODE_BJ_100G:
+			encoding = SBL_ENC_NRZ;
+			divisor  = SBL_DIV_25G;
+			width    = SBL_WID_25G;
+			break;
+		case SBL_LINK_MODE_CD_50G:
+		case SBL_LINK_MODE_CD_100G:
+		case SBL_LINK_MODE_BS_200G:
+			encoding = SBL_ENC_PAM4;
+			divisor  = SBL_DIV_50G;
+			width    = SBL_WID_50G;
+			break;
+		default:
+			sbl_dev_warn(sbl->dev, "%d: Unsupported link mode (%d)",
+				     port_num, sbl->link[port_num].link_mode);
+			return -EINVAL;
+		}
+	}
+
+	for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+		if (!(rx_serdes_required_for_link_mode(sbl, port_num, serdes) ||
+		      tx_serdes_required_for_link_mode(sbl, port_num, serdes)))
+			continue;
+		err = sbl_serdes_init(sbl, port_num, serdes, encoding,
+				      divisor, width);
+		if (err)
+			return err;
+	}
+
+	for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+		if (!(rx_serdes_required_for_link_mode(sbl, port_num, serdes) ||
+		      tx_serdes_required_for_link_mode(sbl, port_num, serdes)))
+			continue;
+		err = sbl_serdes_polarity_ctrl(sbl, port_num, serdes,
+					       encoding, allow_an);
+		if (err)
+			return err;
+	}
+
+	/* Set Rx Termination */
+	switch (sbl->link[port_num].blattr.link_partner) {
+	case SBL_LINK_PARTNER_SWITCH:
+		for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+			if (!(rx_serdes_required_for_link_mode(sbl, port_num, serdes) ||
+			      tx_serdes_required_for_link_mode(sbl, port_num, serdes)))
+				continue;
+			err = sbl_serdes_spico_int(sbl, port_num, serdes,
+						   SPICO_INT_CM4_INT_RX_TERM,
+						SPICO_INT_DATA_RXT_FLOAT, NULL,
+						SPICO_INT_VALIDATE_RESULT);
+			if (err)
+				return err;
+		}
+		break;
+	case SBL_LINK_PARTNER_NIC:
+	case SBL_LINK_PARTNER_NIC_C2:
+		for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+			if (!(rx_serdes_required_for_link_mode(sbl, port_num, serdes) ||
+			      tx_serdes_required_for_link_mode(sbl, port_num, serdes)))
+				continue;
+			err = sbl_serdes_spico_int(sbl, port_num, serdes,
+						   SPICO_INT_CM4_INT_RX_TERM,
+						SPICO_INT_DATA_RXT_AVDD, NULL,
+						SPICO_INT_VALIDATE_RESULT);
+			if (err)
+				return err;
+		}
+		break;
+	default:
+		sbl_dev_warn(sbl->dev, "p%d: Unsupported link partner mode (enum %d)!",
+			     port_num, sbl->link[port_num].blattr.link_partner);
+		return -EINVAL;
+	}
+
+	/* Handle requested loopback mode */
+	switch (sbl->link[port_num].loopback_mode) {
+	case SBL_LOOPBACK_MODE_LOCAL:
+		for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+			if (!(rx_serdes_required_for_link_mode(sbl, port_num, serdes) ||
+			      tx_serdes_required_for_link_mode(sbl, port_num, serdes)))
+				continue;
+			err = sbl_serdes_spico_int(sbl, port_num, serdes,
+						   SPICO_INT_CM4_LOOPBACK,
+						SPICO_INT_DATA_ILB, NULL,
+						SPICO_INT_VALIDATE_RESULT);
+			if (err)
+				return err;
+		}
+		break;
+	case SBL_LOOPBACK_MODE_REMOTE:
+	case SBL_LOOPBACK_MODE_OFF:
+		for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+			if (!(rx_serdes_required_for_link_mode(sbl, port_num, serdes) ||
+			      tx_serdes_required_for_link_mode(sbl, port_num, serdes)))
+				continue;
+			err = sbl_serdes_spico_int(sbl, port_num, serdes,
+						   SPICO_INT_CM4_LOOPBACK,
+						SPICO_INT_DATA_ELB, NULL,
+						SPICO_INT_VALIDATE_RESULT);
+			if (err)
+				return err;
+		}
+		break;
+	default:
+		sbl_dev_warn(sbl->dev, "Unsupported loopback mode (enum %d)!",
+			     sbl->link[port_num].loopback_mode);
+		return -EINVAL;
+	}
+
+	/* Set port config values */
+	if (!allow_an) {
+		for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+			if (!(rx_serdes_required_for_link_mode(sbl, port_num, serdes) ||
+			      tx_serdes_required_for_link_mode(sbl, port_num, serdes)))
+				continue;
+			use_default_tx_eq = false;
+			use_default_gs    = false;
+			err = sbl_get_serdes_config_values(sbl, port_num,
+							   serdes,
+								&values);
+			if (err) {
+				sbl_dev_warn(sbl->dev,
+						     "p%ds%d: Unable to read config list!",
+						     port_num, serdes);
+				use_default_tx_eq = true;
+				use_default_gs    = true;
+			}
+			/* Apply tx eq values */
+			if (!use_default_tx_eq) {
+				err = sbl_set_tx_eq(sbl, port_num, serdes,
+						    values.atten,
+							 values.pre,
+							 values.post,
+							 values.pre2,
+							 values.pre3);
+				if (err) {
+					sbl_dev_warn(sbl->dev,
+						     "Bad settings for port %d! Applying defaults.",
+						     port_num);
+					use_default_tx_eq = true;
+				}
+			}
+			if (use_default_tx_eq) {
+				err = sbl_set_tx_eq(sbl, port_num, serdes,
+						    SBL_DFLT_PORT_CONFIG_ATTEN,
+						 SBL_DFLT_PORT_CONFIG_PRE,
+						 SBL_DFLT_PORT_CONFIG_POST,
+						 SBL_DFLT_PORT_CONFIG_PRE2,
+						 SBL_DFLT_PORT_CONFIG_PRE3);
+				if (err) {
+					sbl_dev_err(sbl->dev, "Default serdes atten/pre/post settings failed!");
+					return err;
+				}
+			}
+			/* Apply gainshape values */
+			if (!use_default_gs) {
+				err = sbl_set_gs(sbl, port_num, serdes,
+						 values.gs1,
+						      values.gs2);
+				if (err) {
+					sbl_dev_warn(sbl->dev,
+						     "Bad gs1/gs2 settings for port %d! Applying defaults.",
+						     port_num);
+					use_default_gs = true;
+				}
+			}
+			if (use_default_gs) {
+				err = sbl_set_gs(sbl, port_num, serdes,
+						 SBL_DFLT_PORT_CONFIG_GS1,
+					      SBL_DFLT_PORT_CONFIG_GS2);
+				if (err) {
+					sbl_dev_err(sbl->dev,
+						    "Default serdes gainshape settings failed!");
+					return err;
+				}
+			}
+
+			if (values.num_intr) {
+				sbl_dev_dbg(sbl->dev,
+					    "p%ds%d: Applying %d interrupts",
+					port_num, serdes, values.num_intr);
+			}
+			for (j = 0; j < values.num_intr; ++j) {
+				sbl_dev_dbg(sbl->dev,
+					    "p%d: Applying interrupt 0x%x with data 0x%x",
+				port_num, values.intr_val[j],
+				values.data_val[j]);
+				err = sbl_serdes_spico_int(sbl, port_num,
+							   serdes, values.intr_val[j],
+						values.data_val[j], &result,
+						SPICO_INT_RETURN_RESULT);
+				if (err) {
+					sbl_dev_warn(sbl->dev,
+						     "p%ds%d: interrupt 0x%x data 0x%x failed!",
+						     port_num, serdes, values.intr_val[j],
+						     values.data_val[j]);
+				}
+				if (result != values.intr_val[j]) {
+					sbl_dev_dbg(sbl->dev,
+						    "p%ds%d: interrupt:0x%x data:0x%x result:0x%x != code:0x%x. This is okay in some cases.",
+						 port_num, serdes, values.intr_val[j], values.data_val[j], result, values.intr_val[j]);
+				}
+			}
+		}
+	}
+	/* TODO - Set PRBS here */
+
+	/* Set Tx Phase Cal */
+	for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+		if (!(rx_serdes_required_for_link_mode(sbl, port_num, serdes) ||
+		      tx_serdes_required_for_link_mode(sbl, port_num, serdes)))
+			continue;
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_TX_PHASE_CAL,
+						SPICO_INT_DATA_TPCE, NULL,
+						SPICO_INT_VALIDATE_RESULT);
+		if (err)
+			return err;
+	}
+
+	/* Set Rx Phase Slip */
+	if (sbl->link[port_num].loopback_mode == SBL_LOOPBACK_MODE_LOCAL)
+		rx_phase_slip_reapply = 1;
+	else
+		rx_phase_slip_reapply = 0;
+
+	rx_phase_slip_cnt = sbl->iattr.rx_phase_slip_cnt;
+	if (rx_phase_slip_cnt > SPICO_INT_DATA_RX_PHASE_MAX) {
+		sbl_dev_warn(sbl->dev,
+			     "Invalid value for rx_phase_slip_cnt (0x%x). Setting to 0x%x",
+			 rx_phase_slip_cnt, SPICO_INT_DATA_RX_PHASE_MAX);
+		rx_phase_slip_cnt = SPICO_INT_DATA_RX_PHASE_MAX;
+	}
+	sbl_dev_dbg(sbl->dev, "p%d: rx_phase_slip_cnt: 0x%x", port_num,
+		    rx_phase_slip_cnt);
+	sbl_dev_dbg(sbl->dev, "p%d: rx_phase_slip_reapply: 0x%x", port_num,
+		    rx_phase_slip_reapply);
+
+	for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+		if (!(rx_serdes_required_for_link_mode(sbl, port_num, serdes) ||
+		      tx_serdes_required_for_link_mode(sbl, port_num, serdes)))
+			continue;
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_RX_PHASE_SLIP,
+						(rx_phase_slip_reapply << SPICO_INT_DATA_RXP_APPLY_OFFSET) |
+						(rx_phase_slip_cnt << SPICO_INT_DATA_RX_PHASE_OFFSET), NULL,
+						SPICO_INT_VALIDATE_RESULT);
+		if (err)
+			return err;
+	}
+
+	switch (sbl->link[port_num].loopback_mode) {
+	case SBL_LOOPBACK_MODE_LOCAL:
+		/* Disable TX and RX */
+		for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+			err = sbl_set_tx_rx_enable(sbl, port_num, serdes,
+						   false, false, false);
+			if (err)
+				return err;
+		}
+		break;
+	case SBL_LOOPBACK_MODE_REMOTE:
+	case SBL_LOOPBACK_MODE_OFF:
+		/* Disable TX */
+		for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+			if (rx_serdes_required_for_link_mode(sbl, port_num, serdes))
+				rx_en = true;
+			else
+				rx_en = false;
+			err = sbl_set_tx_rx_enable(sbl, port_num, serdes,
+						   false, rx_en, false);
+			if (err)
+				return err;
+		}
+		break;
+	default:
+		sbl_dev_warn(sbl->dev, "Unsupported loopback mode (enum %d)!",
+			     sbl->link[port_num].loopback_mode);
+		return -EINVAL;
+	}
+
+	msleep(20);
+
+	/* Enable tx on physical lane 0 - this has the clock for all serdes and
+	   is always required. */
+	/* rx based on link mode */
+	if (rx_serdes_required_for_link_mode(sbl, port_num, 0))
+		rx_en = true;
+	else
+		rx_en = false;
+	/* txo based on link mode */
+	if (get_serdes_tx_mask(sbl, port_num) & (1 << 0))
+		txo_en = true;
+	else
+		txo_en = false;
+	err = sbl_set_tx_rx_enable(sbl, port_num, 0,
+				   true, rx_en, txo_en);
+	if (err)
+		return err;
+
+	usleep_range(1000, 2000);
+
+	/* Enable lane 1 2 3 as needed */
+	for (serdes = 1; serdes < sbl->switch_info->num_serdes; ++serdes) {
+		if (rx_serdes_required_for_link_mode(sbl, port_num, serdes))
+			rx_en = true;
+		else
+			rx_en = false;
+		if (tx_serdes_required_for_link_mode(sbl, port_num, serdes)) {
+			tx_en = true;
+			txo_en = true;
+		} else {
+			tx_en = false;
+			txo_en = false;
+		}
+		err = sbl_set_tx_rx_enable(sbl, port_num, serdes,
+					   tx_en, rx_en, txo_en);
+		if (err)
+			return err;
+	}
+
+	for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+		if (!(rx_serdes_required_for_link_mode(sbl, port_num, serdes) ||
+		      tx_serdes_required_for_link_mode(sbl, port_num, serdes)))
+			continue;
+		if (sbl->link[port_num].blattr.tuning_pattern ==
+		    SBL_TUNING_PATTERN_CORE) {
+			err = sbl_set_tx_data_sel(sbl, port_num, serdes,
+						  SBL_DS_CORE);
+			if (err)
+				return err;
+		} else {
+			err = sbl_set_tx_data_sel(sbl, port_num, serdes,
+						  SBL_DS_PRBS);
+			if (err)
+				return err;
+		}
+	}
+
+	return 0;
+}
+
+int sbl_serdes_tuning(struct sbl_inst *sbl, int port_num)
+{
+	struct sbl_link *link = sbl->link + port_num;
+	int err = 0;
+	int serdes;
+	bool is_retune;
+	int j;
+
+	sbl_dev_dbg(sbl->dev, "SerDes tuning for port %d", port_num);
+
+	mutex_lock(&link->tuning_params_mtx);
+	if (!(link->blattr.options & SBL_OPT_FABRIC_LINK)) {
+		sbl_dev_dbg(sbl->dev,
+			    "p%d: Saved tuning params disabled for edge links",
+				port_num);
+		is_retune = false;
+	} else if (link->tune_param_oob_count >= SBL_MAX_TUNE_PARAM_OOB_FAILS) {
+		sbl_dev_dbg(sbl->dev,
+			    "p%d: Ignoring saved param load - too many params oob!",
+				port_num);
+		link->tune_param_oob_count = 0;
+		is_retune = false;
+	} else if ((link->blattr.options & SBL_OPT_USE_SAVED_PARAMS) &&
+			(link->loopback_mode != SBL_LOOPBACK_MODE_LOCAL) &&
+			!sbl_debug_option(sbl, port_num, SBL_DEBUG_INHIBIT_USE_SAVED_TP)) {
+		is_retune = sbl_is_retune(sbl, port_num);
+	} else {
+		sbl_dev_dbg(sbl->dev,
+			    "p%d: Usage of saved tuning params is disabled!",
+				port_num);
+		is_retune = false;
+	}
+
+	if (is_retune) {
+		sbl_dev_dbg(sbl->dev, "p%d: Applying saved tuning params",
+			    port_num);
+		for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+			if (!rx_serdes_required_for_link_mode(sbl, port_num, serdes))
+				continue;
+			err = sbl_apply_serdes_tuning_params(sbl, port_num, serdes);
+			if (err) {
+				sbl_dev_err(sbl->dev, "p%ds%d: Failed to apply saved tuning params!",
+					    port_num, serdes);
+				mutex_unlock(&link->tuning_params_mtx);
+				return err;
+			}
+			sbl_dev_dbg(sbl->dev, "p%ds%d: saved tuning params applied", port_num, serdes);
+		}
+	} else {
+		sbl_dev_dbg(sbl->dev, "p%d: No saved tuning params found", port_num);
+	}
+	mutex_unlock(&link->tuning_params_mtx);
+
+	/* If we have applied saved params then the serdes should work straight away,
+	   we do not need to tune again. However we will start PCAL as normal.
+	   We mark the tune count to indicate we have done this */
+	if (is_retune) {
+		sbl_link_tune_zero_total_timespec(sbl, port_num);
+		link->dfe_tune_count = SBL_DFE_USED_SAVED_PARAMS;
+		link->serr = 0;
+
+		if (link->blattr.options & SBL_OPT_ENABLE_PCAL) {
+			if (sbl_debug_option(sbl, port_num, SBL_DEBUG_INHIBIT_PCAL)) {
+				sbl_dev_warn(sbl->dev, "%d: pcal inhibited\n", port_num);
+			} else {
+				err = sbl_port_start_pcal(sbl, port_num);
+				if (err)
+					return err;
+			}
+		} else {
+			sbl_dev_warn(sbl->dev, "p%d PCAL is disabled with SerDes saved params enabled!", port_num);
+		}
+
+		return link->serr;
+	}
+
+	/* There is already a delay after starting (configuring) the serdes to allow the
+	   optics to lock..
+	   This extra delay is about waiting for the link partner to start as we currently
+	   have no way of knowing this */
+	if (link->blattr.dfe_pre_delay) {
+		sbl_dev_dbg(sbl->dev, "p%d: pre delay of %d seconds...", port_num,
+			    link->blattr.dfe_pre_delay);
+		link->dfe_predelay_active = true;
+		for (j = 0; j < link->blattr.dfe_pre_delay; ++j) {
+			if (sbl_base_link_start_cancelled(sbl, port_num)) {
+				link->dfe_predelay_active = false;
+				return -ECANCELED;
+			}
+			if (sbl_start_timeout(sbl, port_num)) {
+				link->dfe_predelay_active = false;
+				return -ETIMEDOUT;
+			}
+			msleep(1000);
+		}
+		link->dfe_predelay_active = false;
+	}
+
+	/* Try to tune, keep going unless error, cancelled, timeout
+	   or too many failed param checks.
+	   In the latter case this is possibly because the saved tuning
+	   params are not good and we keep tuning to a bad place. We therefore
+	   need to fail here and reset the serdes on the next cycle to clear out
+	   the current tuning params */
+	sbl_dev_dbg(sbl->dev, "p%d: DFE %stune starting", port_num,
+		    is_retune ? "re" : "");
+
+	sbl_link_tune_zero_total_timespec(sbl, port_num);
+	link->dfe_tune_count = -1;
+	while (true) {
+		link->dfe_tune_count++;
+		link->serr = sbl_port_dfe_tune(sbl, port_num, is_retune);
+
+		switch (link->serr) {
+		case 0:
+			sbl_dev_dbg(sbl->dev, "p%d: dfe %stune good\n", port_num,
+				    is_retune ? "re" : "");
+
+			/* Setup the current tuning params for next time */
+			if (link->blattr.options & SBL_OPT_DFE_SAVE_PARAMS) {
+				sbl_dev_dbg(sbl->dev, "p%d: Saving tuning params...", port_num);
+				mutex_lock(&link->tuning_params_mtx);
+				err = sbl_save_serdes_tuning_params(sbl, port_num);
+				if (err) {
+					sbl_dev_warn(sbl->dev, "p%d: Failed to save tuning params [%d]",
+						     port_num, err);
+					/* not a fatal error */
+				}
+				mutex_unlock(&link->tuning_params_mtx);
+				sbl_dev_dbg(sbl->dev, "p%d: Saved tuning params", port_num);
+			}
+
+			if (sbl->link[port_num].blattr.tuning_pattern ==
+			    SBL_TUNING_PATTERN_PRBS) {
+				for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+					if (!rx_serdes_required_for_link_mode(sbl, port_num, serdes))
+						continue;
+					err = sbl_set_prbs_rx_mode(sbl, port_num, serdes);
+					if (err)
+						return err;
+
+				}
+			}
+			goto out;
+
+		case -ELNRNG:   /* tuning params out of range */
+			if (link->tune_param_oob_count >= SBL_MAX_TUNE_PARAM_OOB_FAILS) {
+				sbl_dev_info(sbl->dev, "%d: too many params oob fails\n", port_num);
+				/* give up and return error
+				   The calling framework should reset the spico to delete any
+				   applied tuning params so we can do a full tune */
+				goto out;
+			}
+			fallthrough;
+
+		case -ETIME:    /* tuning did not complete */
+		case -ECHRNG:   /* eye heights bad */
+
+			/*  try to tune again */
+
+			break;
+
+		case -ECANCELED:
+			sbl_dev_dbg(sbl->dev, "p%d: %stune cancelled\n", port_num,
+				    is_retune ? "re" : "");
+			goto out;
+
+		case -ETIMEDOUT:
+			sbl_dev_dbg(sbl->dev, "p%d: %stune timed out\n", port_num,
+				    is_retune ? "re" : "");
+			goto out;
+
+		default:
+			sbl_dev_dbg(sbl->dev, "p%d: %stune failed [%d]\n", port_num,
+				    is_retune ? "re" : "", link->serr);
+			goto out;
+		}
+	}
+
+out:
+	sbl_dev_dbg(sbl->dev, "p%d: SerDes tuning complete", port_num);
+	return link->serr;
+}
+
+int sbl_spico_reset(struct sbl_inst *sbl, int port_num)
+{
+	int serdes;
+	int err = 0;
+	int pass;
+	u32 sbus_addr;
+	u32 sbus_ring;
+	u32 result[SBL_SERDES_LANES_PER_PORT];
+	unsigned long last_jiffy;
+
+	sbl_dev_dbg(sbl->dev, "p%d: spico reset start", port_num);
+
+	sbus_ring = sbl->switch_info->ports[port_num].serdes[0].sbus_ring;
+	if (mutex_is_locked(SBUS_RING_MTX(sbl, sbus_ring)))
+		sbl_dev_dbg(sbl->dev, "spico_reset: Sbus contention detected, sbus_ring_mtx[%d] locked", sbus_ring);
+
+	/* SBUS Critical Section */
+	mutex_lock(SBUS_RING_MTX(sbl, sbus_ring));
+
+	/* Issue the reset */
+	for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+		if (!(rx_serdes_required_for_link_mode(sbl, port_num, serdes) ||
+		      tx_serdes_required_for_link_mode(sbl, port_num, serdes)))
+			continue;
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_PROC_RESET,
+				SPICO_INT_DATA_PROC_RESET, NULL,
+				SPICO_INT_IGNORE_RESULT);
+		if (err) {
+			mutex_unlock(SBUS_RING_MTX(sbl, sbus_ring));
+			sbl_dev_err(sbl->dev, "s%d: sbl_serdes_spico_int failed %d", sbus_ring, err);
+			return err;
+		}
+
+		/* Increment SPICO reset counter */
+		sbl_link_counters_incr(sbl, port_num, serdes0_spico_reset + serdes);
+	}
+
+	/* Check that each lane has been reset */
+	last_jiffy = jiffies + msecs_to_jiffies(SPICO_PROC_RESET_RETRY_TIMEOUT_MS);
+	pass = 0;
+	for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+		if (!(rx_serdes_required_for_link_mode(sbl, port_num, serdes) ||
+		      tx_serdes_required_for_link_mode(sbl, port_num, serdes)))
+			continue;
+		sbus_addr = SBUS_ADDR(sbl->switch_info->ports[port_num].serdes[serdes].sbus_ring,
+				      sbl->switch_info->ports[port_num].serdes[serdes].rx_addr);
+		do {
+			err = sbl_sbus_rd(sbl, sbus_addr,
+					  SBM_CRM_ADDR_PROC_STS,
+					       &result[serdes]);
+			if (err) {
+				mutex_unlock(SBUS_RING_MTX(sbl, sbus_ring));
+				return err;
+			}
+
+			if (!sbl->is_hw)
+				result[serdes] = SPICO_STATE_PAUSE;
+
+			if ((result[serdes] & SPICO_STATE_MASK) == SPICO_STATE_PAUSE) {
+				sbl_dev_dbg(sbl->dev, "p%ds%d: SPICO reset PAUSE: pass %d",
+					    port_num, serdes, pass);
+				break;
+			} else if ((result[serdes] & SPICO_STATE_MASK) == SPICO_STATE_ERROR) {
+				sbl_dev_dbg(sbl->dev, "p%ds%d: SPICO reset ERROR: pass %d",
+					    port_num, serdes, pass);
+				/* NOTE: I've seen the transition form ERROR -> PAUSE successfully */
+			} else {
+				sbl_dev_dbg(sbl->dev, "p%ds%d: SPICO reset OTHER 0x%x: pass %d",
+					    port_num, serdes, result[serdes], pass);
+			}
+			pass++;
+			/* No delay here, as we could miss the PAUSE state */
+		} while (time_is_after_jiffies(last_jiffy));
+
+	}
+	for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+		if (!(rx_serdes_required_for_link_mode(sbl, port_num, serdes) ||
+		      tx_serdes_required_for_link_mode(sbl, port_num, serdes)))
+			continue;
+		if ((result[serdes] & SPICO_STATE_MASK) != SPICO_STATE_PAUSE) {
+			sbl_dev_err(sbl->dev, "p%ds%d: SPICO reset failed! result: 0x%x after passes: %d",
+				    port_num, serdes, result[serdes], pass);
+			err = -ETIME;
+		}
+	}
+	if (err) {
+		mutex_unlock(SBUS_RING_MTX(sbl, sbus_ring));
+		return -ETIME;
+	}
+
+	sbl->link[port_num].pcal_running = false;
+
+	mutex_unlock(SBUS_RING_MTX(sbl, sbus_ring));
+
+	sbl_dev_dbg(sbl->dev, "p%d: spico reset done", port_num);
+	return 0;
+}
+
+
+#define SPICO_INT_DATA_PLL_RESET     0xffff
+
+int sbl_reset_serdes_plls(struct sbl_inst *sbl, int port_num)
+{
+	int serdes;
+	int err;
+
+	sbl_dev_info(sbl->dev, "p%d: resetting serdes PLLs", port_num);
+
+	for (serdes = 0; serdes < sbl->switch_info->num_serdes; ++serdes) {
+		if (!rx_serdes_required_for_link_mode(sbl, port_num, serdes))
+			continue;
+
+		err = sbl_serdes_spico_int(sbl, port_num, serdes,
+					   SPICO_INT_CM4_DFE_CTRL,
+				SPICO_INT_DATA_PLL_RESET, NULL,
+				SPICO_INT_VALIDATE_RESULT);
+		if (err) {
+			sbl_dev_err(sbl->dev, "p%ds%d: serdes PLL reset failed!", port_num, serdes);
+			return err;
+		}
+
+		/* Increment PLL reset counter */
+		sbl_link_counters_incr(sbl, port_num, serdes0_pll_reset + serdes);
+	}
+
+	return 0;
+}
+

-- 
2.56.0


  parent reply	other threads:[~2026-10-30 21:00 UTC|newest]

Thread overview: 25+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-30 20:58 [PATCH 00/23] Subject: [PATCH net-next 00/23] net/hpe: add HPE Slingshot NIC drivers Doug Ledford
2026-10-30 20:58 ` [PATCH 01/23] net: hpe: ss1: add the Cassini headers Doug Ledford
2026-10-30 20:58 ` [PATCH 02/23] net: hpe: sbl: add the Slingshot Base Link headers Doug Ledford
2026-10-30 20:58 ` [PATCH 03/23] net: hpe: sbl: add the core instance and configuration layer Doug Ledford
2026-10-30 20:59 ` Doug Ledford [this message]
2026-10-30 20:59 ` [PATCH 05/23] net: hpe: sbl: add the PML link layer Doug Ledford
2026-10-30 20:59 ` [PATCH 06/23] net: hpe: sbl: add link state management Doug Ledford
2026-10-30 20:59 ` [PATCH 07/23] net: hpe: sbl: add the SBM serial bus layer Doug Ledford
2026-10-30 20:59 ` [PATCH 08/23] net: hpe: sbl: add media, FEC and counter support Doug Ledford
2026-10-30 20:59 ` [PATCH 09/23] net: hpe: sl: add the Slingshot Link headers Doug Ledford
2026-10-30 20:59 ` [PATCH 10/23] net: hpe: sl: add the module entry points Doug Ledford
2026-10-30 20:59 ` [PATCH 11/23] net: hpe: sl: add the core link state machines Doug Ledford
2026-10-30 20:59 ` [PATCH 12/23] net: hpe: sl: add the control layer Doug Ledford
2026-10-30 20:59 ` [PATCH 13/23] net: hpe: sl: add media and cable handling Doug Ledford
2026-10-30 20:59 ` [PATCH 14/23] net: hpe: sl: add the sysfs interface Doug Ledford
2026-10-30 20:59 ` [PATCH 15/23] net: hpe: sl: add the NIC binding and logging helpers Doug Ledford
2026-10-30 20:59 ` [PATCH 16/23] net: hpe: ss1: add the Cassini core device layer Doug Ledford
2026-10-30 20:59 ` [PATCH 17/23] net: hpe: ss1: add the datapath and memory translation layers Doug Ledford
2026-10-30 20:59 ` [PATCH 18/23] net: hpe: ss1: add the link layer glue Doug Ledford
2026-10-30 20:59 ` [PATCH 19/23] net: hpe: ss1: add SR-IOV support Doug Ledford
2026-10-30 20:59 ` [PATCH 20/23] net: hpe: ss1: add the CXI core interfaces Doug Ledford
2026-10-30 20:59 ` [PATCH 21/23] net: hpe: ss1: add resource groups, QoS and services Doug Ledford
2026-10-30 20:59 ` [PATCH 22/23] net: hpe: ss1: add the Ethernet personality Doug Ledford
2026-10-30 20:59 ` [PATCH 23/23] net: hpe: wire up the Slingshot NIC drivers Doug Ledford
2026-10-30 22:11 ` [PATCH 00/23] Subject: [PATCH net-next 00/23] net/hpe: add HPE " Jakub Kicinski

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261030-cxi-netdev-submission-v1-4-8c2c391cc283@hpe.com \
    --to=doug.ledford@hpe.com \
    --cc=andrew+netdev@lunn.ch \
    --cc=christian.koenig@amd.com \
    --cc=davem@davemloft.net \
    --cc=edumazet@kernel.org \
    --cc=kuba@kernel.org \
    --cc=linux-kbuild@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux@roeck-us.net \
    --cc=nathan@kernel.org \
    --cc=netdev@vger.kernel.org \
    --cc=nsc@kernel.org \
    --cc=pabeni@redhat.com \
    --cc=richardcochran@gmail.com \
    --cc=santosh-kumar.mamila@hpe.com \
    --cc=slingshot-os-team@groups.ext.hpe.com \
    --cc=sumit.semwal@linaro.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®