From 74044a2d2e62fcc3ee2b6cf22742fa94609c529e Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Tue, 24 Mar 2026 18:14:33 +0000
Subject: [PATCH 05/19] net: dsa: mxl862xx: add SerDes ethtool statistics

Expose SerDes equalization parameters as ethtool statistics on
ports 9-16 (XPCS-backed ports). Uses the XPCS EQ_GET firmware
command to read TX/RX equalization coefficients and DFE taps.

The 15 additional stats (serdes_tx_*, serdes_rx_*) are appended
after the standard RMON counters and gated on firmware >= 1.0.84.

Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
 drivers/net/dsa/mxl862xx/mxl862xx-api.h     | 92 +++++++++++++++++++
 drivers/net/dsa/mxl862xx/mxl862xx-cmd.h     |  1 +
 drivers/net/dsa/mxl862xx/mxl862xx-phylink.c | 97 +++++++++++++++++++++
 drivers/net/dsa/mxl862xx/mxl862xx-phylink.h |  3 +
 drivers/net/dsa/mxl862xx/mxl862xx.c         |  6 +-
 5 files changed, 198 insertions(+), 1 deletion(-)

--- a/drivers/net/dsa/mxl862xx/mxl862xx-api.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-api.h
@@ -1668,4 +1668,96 @@ struct mxl862xx_xpcs_pcs_link_up {
 	__le16 result;
 } __packed;
 
+/**
+ * struct mxl862xx_xpcs_loopback_cfg - loopback control
+ * @port_id: XPCS port index
+ * @mode: loopback mode. See &enum mxl862xx_xpcs_loopback_mode
+ * @result: firmware result
+ */
+struct mxl862xx_xpcs_loopback_cfg {
+	u8 port_id;
+	u8 mode; /* enum mxl862xx_xpcs_loopback_mode */
+	__le16 result;
+} __packed;
+
+/**
+ * struct mxl862xx_xpcs_reset_cfg - XPCS reset
+ * @port_id: XPCS port index
+ * @reset_type: reset type. See &enum mxl862xx_xpcs_reset_type
+ * @result: firmware result
+ */
+struct mxl862xx_xpcs_reset_cfg {
+	u8 port_id;
+	u8 reset_type; /* enum mxl862xx_xpcs_reset_type */
+	__le16 result;
+} __packed;
+
+/**
+ * struct mxl862xx_xpcs_eq_item - single equalization parameter
+ * @value: current initial value
+ * @ovrd: override value
+ * @ovrd_en: override enable flag
+ */
+struct mxl862xx_xpcs_eq_item {
+	u8 value;
+	u8 ovrd;
+	u8 ovrd_en;
+} __packed;
+
+/**
+ * struct mxl862xx_xpcs_tx_eq_info - TX equalization status
+ * @main: TX main cursor (0-63)
+ * @pre: TX pre-cursor (0-63)
+ * @post: TX post-cursor (0-63)
+ * @iboost_lvl: TX iboost level (0-15)
+ * @vboost_lvl: TX vboost level (0-7)
+ * @vboost_en: TX vboost enable (0-1)
+ */
+struct mxl862xx_xpcs_tx_eq_info {
+	struct mxl862xx_xpcs_eq_item main;
+	struct mxl862xx_xpcs_eq_item pre;
+	struct mxl862xx_xpcs_eq_item post;
+	struct mxl862xx_xpcs_eq_item iboost_lvl;
+	struct mxl862xx_xpcs_eq_item vboost_lvl;
+	struct mxl862xx_xpcs_eq_item vboost_en;
+} __packed;
+
+/**
+ * struct mxl862xx_xpcs_rx_eq_info - RX equalization status
+ * @att_lvl: RX attenuation level (0-7)
+ * @vga1_gain: RX VGA1 gain (0-7)
+ * @vga2_gain: RX VGA2 gain (0-7)
+ * @ctle_boost: RX CTLE boost (0-31)
+ * @ctle_pole: RX CTLE pole (0-3)
+ * @dfe_tap1: RX DFE tap1 (0-255)
+ * @dfe_bypass: RX DFE bypass (0-1)
+ * @adapt_mode: RX adapt mode (0-3)
+ * @adapt_sel: RX adapt select (0-1)
+ */
+struct mxl862xx_xpcs_rx_eq_info {
+	struct mxl862xx_xpcs_eq_item att_lvl;
+	struct mxl862xx_xpcs_eq_item vga1_gain;
+	struct mxl862xx_xpcs_eq_item vga2_gain;
+	struct mxl862xx_xpcs_eq_item ctle_boost;
+	struct mxl862xx_xpcs_eq_item ctle_pole;
+	struct mxl862xx_xpcs_eq_item dfe_tap1;
+	struct mxl862xx_xpcs_eq_item dfe_bypass;
+	struct mxl862xx_xpcs_eq_item adapt_mode;
+	struct mxl862xx_xpcs_eq_item adapt_sel;
+} __packed;
+
+/**
+ * struct mxl862xx_xpcs_eq_get - EQ get request/response
+ * @port_id: XPCS port index (0 or 1)
+ * @result: firmware result
+ * @tx: TX equalization info
+ * @rx: RX equalization info
+ */
+struct mxl862xx_xpcs_eq_get {
+	u8 port_id;
+	__le16 result;
+	struct mxl862xx_xpcs_tx_eq_info tx;
+	struct mxl862xx_xpcs_rx_eq_info rx;
+} __packed;
+
 #endif /* __MXL862XX_API_H */
--- a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
@@ -79,6 +79,7 @@
 #define MXL862XX_XPCS_PCS_LINK_UP	(MXL862XX_XPCS_MAGIC + 0x7)
 #define MXL862XX_XPCS_LOOPBACK		(MXL862XX_XPCS_MAGIC + 0x8)
 #define MXL862XX_XPCS_RESET		(MXL862XX_XPCS_MAGIC + 0x9)
+#define MXL862XX_XPCS_EQ_GET		(MXL862XX_XPCS_MAGIC + 0xc)
 
 #define MMD_API_MAXIMUM_ID		0x7fff
 
--- a/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c
@@ -399,3 +399,100 @@ const struct phylink_mac_ops mxl862xx_ph
 	.mac_link_up = mxl862xx_phylink_mac_link_up,
 	.mac_select_pcs = mxl862xx_phylink_mac_select_pcs,
 };
+
+/* --- SerDes ethtool statistics --- */
+
+static const char mxl862xx_serdes_stats[][ETH_GSTRING_LEN] = {
+	"serdes_tx_main",
+	"serdes_tx_pre",
+	"serdes_tx_post",
+	"serdes_tx_iboost",
+	"serdes_tx_vboost",
+	"serdes_tx_vboost_en",
+	"serdes_rx_att",
+	"serdes_rx_vga1",
+	"serdes_rx_vga2",
+	"serdes_rx_ctle_boost",
+	"serdes_rx_ctle_pole",
+	"serdes_rx_dfe_tap1",
+	"serdes_rx_dfe_bypass",
+	"serdes_rx_adapt_mode",
+	"serdes_rx_adapt_sel",
+};
+
+static bool mxl862xx_port_has_serdes_stats(struct dsa_switch *ds, int port)
+{
+	struct mxl862xx_priv *priv = ds->priv;
+
+	/* Firmware reads EQ/signal status per XPCS, not per lane.  Expose
+	 * the stats only on the slot-0 port of each XPCS (9 and 13) so
+	 * users don't see four duplicate copies labelled as if they were
+	 * independent per-port readings.
+	 */
+	return (port == 9 || port == 13) &&
+	       MXL862XX_FW_VER_MIN(priv, 1, 0, 84);
+}
+
+int mxl862xx_serdes_stats_count(struct dsa_switch *ds, int port)
+{
+	if (mxl862xx_port_has_serdes_stats(ds, port))
+		return ARRAY_SIZE(mxl862xx_serdes_stats);
+
+	return 0;
+}
+
+void mxl862xx_serdes_get_strings(struct dsa_switch *ds, int port, u8 *data)
+{
+	int i;
+
+	if (!mxl862xx_port_has_serdes_stats(ds, port))
+		return;
+
+	for (i = 0; i < ARRAY_SIZE(mxl862xx_serdes_stats); i++)
+		ethtool_puts(&data, mxl862xx_serdes_stats[i]);
+}
+
+static u64 mxl862xx_eq_effective(const struct mxl862xx_xpcs_eq_item *it)
+{
+	return it->ovrd_en ? it->ovrd : it->value;
+}
+
+void mxl862xx_serdes_get_stats(struct dsa_switch *ds, int port, u64 *data)
+{
+	struct mxl862xx_xpcs_eq_get eq = {
+		.port_id = MXL862XX_SERDES_PORT_ID(port),
+	};
+	int ret;
+
+	if (!mxl862xx_port_has_serdes_stats(ds, port))
+		return;
+
+	ret = MXL862XX_API_READ(ds->priv, MXL862XX_XPCS_EQ_GET, eq);
+	if (ret) {
+		dev_err(ds->dev,
+			"port %d: XPCS EQ_GET failed: %d\n", port, ret);
+		return;
+	}
+	if ((s16)le16_to_cpu(eq.result) < 0) {
+		dev_err(ds->dev,
+			"port %d: XPCS EQ_GET firmware result: %d\n",
+			port, (s16)le16_to_cpu(eq.result));
+		return;
+	}
+
+	*data++ = mxl862xx_eq_effective(&eq.tx.main);
+	*data++ = mxl862xx_eq_effective(&eq.tx.pre);
+	*data++ = mxl862xx_eq_effective(&eq.tx.post);
+	*data++ = mxl862xx_eq_effective(&eq.tx.iboost_lvl);
+	*data++ = mxl862xx_eq_effective(&eq.tx.vboost_lvl);
+	*data++ = mxl862xx_eq_effective(&eq.tx.vboost_en);
+	*data++ = mxl862xx_eq_effective(&eq.rx.att_lvl);
+	*data++ = mxl862xx_eq_effective(&eq.rx.vga1_gain);
+	*data++ = mxl862xx_eq_effective(&eq.rx.vga2_gain);
+	*data++ = mxl862xx_eq_effective(&eq.rx.ctle_boost);
+	*data++ = mxl862xx_eq_effective(&eq.rx.ctle_pole);
+	*data++ = mxl862xx_eq_effective(&eq.rx.dfe_tap1);
+	*data++ = mxl862xx_eq_effective(&eq.rx.dfe_bypass);
+	*data++ = mxl862xx_eq_effective(&eq.rx.adapt_mode);
+	*data++ = mxl862xx_eq_effective(&eq.rx.adapt_sel);
+}
--- a/drivers/net/dsa/mxl862xx/mxl862xx-phylink.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-phylink.h
@@ -17,5 +17,8 @@ void mxl862xx_phylink_get_caps(struct ds
 			       struct phylink_config *config);
 void mxl862xx_setup_pcs(struct mxl862xx_priv *priv, struct mxl862xx_pcs *pcs,
 			int port);
+int mxl862xx_serdes_stats_count(struct dsa_switch *ds, int port);
+void mxl862xx_serdes_get_strings(struct dsa_switch *ds, int port, u8 *data);
+void mxl862xx_serdes_get_stats(struct dsa_switch *ds, int port, u64 *data);
 
 #endif /* __MXL862XX_PHYLINK_H */
--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
@@ -1776,6 +1776,8 @@ static void mxl862xx_get_strings(struct
 
 	for (i = 0; i < ARRAY_SIZE(mxl862xx_mib); i++)
 		ethtool_puts(&data, mxl862xx_mib[i].name);
+
+	mxl862xx_serdes_get_strings(ds, port, data);
 }
 
 static int mxl862xx_get_sset_count(struct dsa_switch *ds, int port, int sset)
@@ -1783,7 +1785,7 @@ static int mxl862xx_get_sset_count(struc
 	if (sset != ETH_SS_STATS)
 		return 0;
 
-	return ARRAY_SIZE(mxl862xx_mib);
+	return ARRAY_SIZE(mxl862xx_mib) + mxl862xx_serdes_stats_count(ds, port);
 }
 
 static int mxl862xx_read_rmon(struct dsa_switch *ds, int port,
@@ -1819,6 +1821,8 @@ static void mxl862xx_get_ethtool_stats(s
 		else
 			*data++ = le64_to_cpu(*(__le64 *)field);
 	}
+
+	mxl862xx_serdes_get_stats(ds, port, data);
 }
 
 static void mxl862xx_get_eth_mac_stats(struct dsa_switch *ds, int port,
