[PATCH v1] lx2160: add lx2160_serdes split and management

Vincent Jardin <[email protected]>
Newsgroups org.u-boot-project.lists.u-boot
Message-ID <[email protected]>
Add Serdes information and status for boards that support SerDes 1
split as 1x 100G + 4x 25G
RCWSR shall be 13, MC needs follow_hw_pssr to read the hardware
rather than a table, and now U-Boot side dumps the real configuration.

Example:
  RCWSR29  raw=0x000d -> protocol 13
  port grouping (PORT_LN0_B)  [LNA-LND] [LNE] [LNF] [LNG] [LNH]
      PCCD    11110000  (E25G)  E25Ga=1 E25Gb=1 E25Gc=1 E25Gd=1
      PCCE    00000100  (E40G/E50G/E100G)  E100Ga=0 E100Gb=1

It can be enforced using:
  setenv hwconfig serdes1:split && saveenv

then lx2160a_serdes_apply_hwconfig() reads that and applies the split.
hwconfig is the established Layerscape mean for this kind of per-board
runtime choice.

Currently, only QDS boards have daughter boards for this feature.

It requires LX2160A CPU that has Lynx 28G Serdes.

Signed-off-by: Vincent Jardin <[email protected]>

---

 board/nxp/lx2160a/Makefile         |   1 +
 board/nxp/lx2160a/eth_lx2160aqds.c |   9 +
 board/nxp/lx2160a/serdes_split.c   | 509 +++++++++++++++++++++++++++++
 board/nxp/lx2160a/serdes_split.h   |  35 ++
 4 files changed, 554 insertions(+)
 create mode 100644 board/nxp/lx2160a/serdes_split.c
 create mode 100644 board/nxp/lx2160a/serdes_split.h

diff --git a/board/nxp/lx2160a/Makefile b/board/nxp/lx2160a/Makefile
index c9561bfadea..4df6d8a8c72 100644
--- a/board/nxp/lx2160a/Makefile
+++ b/board/nxp/lx2160a/Makefile
@@ -6,6 +6,7 @@
 
 obj-y += lx2160a.o
 obj-y += ddr.o
+obj-y += serdes_split.o
 obj-$(CONFIG_TARGET_LX2160ARDB) += eth_lx2160ardb.o
 obj-$(CONFIG_TARGET_LX2160AQDS) += eth_lx2160aqds.o
 obj-$(CONFIG_TARGET_LX2162AQDS) += eth_lx2162aqds.o
diff --git a/board/nxp/lx2160a/eth_lx2160aqds.c b/board/nxp/lx2160a/eth_lx2160aqds.c
index 4c16f565b69..c1a060877cd 100644
--- a/board/nxp/lx2160a/eth_lx2160aqds.c
+++ b/board/nxp/lx2160a/eth_lx2160aqds.c
@@ -9,11 +9,20 @@
 #include <exports.h>
 #include <fsl-mc/fsl_mc.h>
 
+#include "serdes_split.h"
+
 DECLARE_GLOBAL_DATA_PTR;
 
 #if defined(CONFIG_RESET_PHY_R)
 void reset_phy(void)
 {
+	/*
+	 * Before the MC boots: it latches the live protocol-converter
+	 * configuration when it starts, so `hwconfig=serdes1:split` has to
+	 * be applied here and not later.
+	 */
+	lx2160a_serdes_apply_hwconfig();
+
 #if defined(CONFIG_FSL_MC_ENET)
 	mc_env_boot();
 #endif
diff --git a/board/nxp/lx2160a/serdes_split.c b/board/nxp/lx2160a/serdes_split.c
new file mode 100644
index 00000000000..ca3d7228977
--- /dev/null
+++ b/board/nxp/lx2160a/serdes_split.c
@@ -0,0 +1,509 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright 2026 Free Mobile - Vincent Jardin
+ *
+ * LX2160A 28G-Lynx SerDes inspection (`lx2160_serdes`).
+ */
+
+#include <command.h>
+#include <hwconfig.h>
+#include <vsprintf.h>
+#include <asm/io.h>
+#include <asm/arch/fsl_serdes.h>
+#include <asm/arch/immap_lsch3.h>
+#include <asm/arch/soc.h>		/* gur_in32() picks the GUR endianness */
+#include <linux/kernel.h>
+#include <linux/string.h>
+#include <linux/delay.h>
+#include <linux/errno.h>
+
+#include "serdes_split.h"
+
+#if defined(CONFIG_SYS_NXP_SRDS_3)
+#define SRDS_COUNT		3
+#else
+#define SRDS_COUNT		2	/* LX2162A has no SerDes 3 */
+#endif
+#define SRDS_LANES		8
+#define SRDS_BLOCK_STRIDE	0x10000
+#define SRDS_LANE_STRIDE	0x100
+
+#define SRDS_LNmGCR0(b, m)	((b) + 0x800 + (m) * SRDS_LANE_STRIDE)
+#define SRDS_LNmTRSTCTL(b, m)	((b) + 0x820 + (m) * SRDS_LANE_STRIDE)
+#define SRDS_LNmRRSTCTL(b, m)	((b) + 0x840 + (m) * SRDS_LANE_STRIDE)
+#define SRDS_PCCC(b)		((b) + 0x10B0)
+#define SRDS_PCCD(b)		((b) + 0x10B4)
+#define SRDS_PCCE(b)		((b) + 0x10B8)
+
+/* LNmGCR0 */
+#define GCR0_PORT_RST_LEFT	BIT(17)
+#define GCR0_PORT_LN0_B		BIT(16)
+#define GCR0_PROTO_SEL_MASK	0x000000F8
+#define GCR0_PROTO_SEL_SHIFT	3
+#define GCR0_IF_WIDTH_MASK	0x00000007
+
+/* LNmTRSTCTL and LNmRRSTCTL share a layout */
+#define RSTCTL_RST_REQ		BIT(31)
+#define RSTCTL_RST_DONE		BIT(30)
+#define RSTCTL_HLT_REQ		BIT(27)
+#define RSTCTL_STP_REQ		BIT(26)
+#define RSTCTL_DIS		BIT(24)
+
+/* PCCE: E100Ga at 14-12 (LRV 15), E100Gb at 10-8 (LRV 11) */
+#define PCCE_E100GA_CFG(v)	(((v) >> 12) & 0x7)
+#define PCCE_E100GA_LRV(v)	(((v) >> 15) & 0x1)
+#define PCCE_E100GB_CFG(v)	(((v) >>  8) & 0x7)
+#define PCCE_E100GB_LRV(v)	(((v) >> 11) & 0x1)
+
+/* PCCD: E25Ga..E25Gh, 3 bits each, a at 30-28 down to h at 2-0 */
+#define PCCD_E25G_CFG(v, i)	(((v) >> (28 - 4 * (i))) & 0x7)
+
+static unsigned long srds_base(int sd)
+{
+	return CFG_SYS_FSL_LSCH3_SERDES_ADDR + (unsigned long)sd * SRDS_BLOCK_STRIDE;
+}
+
+static const char *proto_sel_name(u32 sel)
+{
+	switch (sel) {
+	case 0x00: return "PCIe";
+	case 0x01: return "SGMII/1000Base-KX";
+	case 0x02: return "SATA";
+	case 0x0A: return "XFI/SFI/10GBase-R/KR, 10G-SXGMII, 40GBase-R/KR";
+	case 0x1A: return "25GBase-R/KR, 50GAUI2, 100G CAUI4";
+	default:   return "reserved";
+	}
+}
+
+static const char *if_width_name(u32 w)
+{
+	switch (w) {
+	case 0: return "10-bit";
+	case 1: return "16-bit";
+	case 2: return "20-bit";
+	case 3: return "32-bit";
+	case 4: return "40-bit";
+	default: return "?";
+	}
+}
+
+static void print_rstctl(const char *what, u32 v)
+{
+	printf("      %s %08x  [%s%s%s%s%s ]\n", what, v,
+	       (v & RSTCTL_RST_REQ)  ? " RST_REQ"  : "",
+	       (v & RSTCTL_RST_DONE) ? " RST_DONE" : "",
+	       (v & RSTCTL_HLT_REQ)  ? " HLT_REQ"  : "",
+	       (v & RSTCTL_STP_REQ)  ? " STP_REQ"  : "",
+	       (v & RSTCTL_DIS)      ? " DIS"      : "");
+}
+
+/*
+ * PORT_LN0_B is the single most informative bit here. It
+ * must be 0 only on the port's master lane, and 0 on every single-lane
+ * port. So a run of lanes where exactly one has PORT_LN0_B=0 is one
+ * multi-lane port, and a run where all of them are 0 is that many
+ * one-lane ports. That is precisely what the 100G -> 4x 25G split
+ * changes, and it is invisible in RCWSR.
+ */
+static void print_lane(unsigned long base, int m)
+{
+	u32 gcr0 = in_le32((void __iomem *)SRDS_LNmGCR0(base, m));
+	u32 sel  = (gcr0 & GCR0_PROTO_SEL_MASK) >> GCR0_PROTO_SEL_SHIFT;
+	u32 wid  = gcr0 & GCR0_IF_WIDTH_MASK;
+
+	printf("  lane %d (LN%c)\n", m, 'A' + m);
+	printf("      GCR0    %08x  PORT_RST_LEFT=%d PORT_LN0_B=%d (%s)\n",
+	       gcr0,
+	       (gcr0 & GCR0_PORT_RST_LEFT) ? 1 : 0,
+	       (gcr0 & GCR0_PORT_LN0_B) ? 1 : 0,
+	       (gcr0 & GCR0_PORT_LN0_B) ? "follows a master lane"
+					: "port master / single-lane port");
+	printf("                        PROTO_SEL=%02x (%s) IF_WIDTH=%d (%s)\n",
+	       sel, proto_sel_name(sel), wid, if_width_name(wid));
+	print_rstctl("TRSTCTL", in_le32((void __iomem *)SRDS_LNmTRSTCTL(base, m)));
+	print_rstctl("RRSTCTL", in_le32((void __iomem *)SRDS_LNmRRSTCTL(base, m)));
+}
+
+/*
+ * Summarise the port grouping, which is the one line that says whether a
+ * split happened. With PORT_RST_LEFT=0 the port's master is
+ * its right-most lane (towards LNH), with PORT_RST_LEFT=1 it is the
+ * left-most: PORT_LN0_B=0 marks that master, and is also 0 on every
+ * single-lane port. So a group is a run of PORT_LN0_B=1 lanes terminated
+ * (or led) by a PORT_LN0_B=0 lane.
+ *
+ * Checked on board with the PBI split applied:
+ *   [LNA-LND] [LNE] [LNF] [LNG] [LNH]   -- 1x 100G + 4x 25G
+ * and without it lanes E-H would read as a second [LNE-LNH] group.
+ */
+static void print_grouping(unsigned long base)
+{
+	u32 gcr0[SRDS_LANES];
+	bool claimed[SRDS_LANES];
+	int m, orphan = 0;
+
+	for (m = 0; m < SRDS_LANES; m++) {
+		gcr0[m] = in_le32((void __iomem *)SRDS_LNmGCR0(base, m));
+		claimed[m] = false;
+	}
+
+	printf("  port grouping (PORT_LN0_B)  ");
+	for (m = 0; m < SRDS_LANES; m++) {
+		int s, e;
+
+		if (gcr0[m] & GCR0_PORT_LN0_B)
+			continue;               /* a follower, not a master */
+
+		s = m;
+		e = m;
+		if (gcr0[m] & GCR0_PORT_RST_LEFT) {
+			while (e + 1 < SRDS_LANES &&
+			       (gcr0[e + 1] & GCR0_PORT_LN0_B))
+				e++;
+		} else {
+			while (s - 1 >= 0 && (gcr0[s - 1] & GCR0_PORT_LN0_B))
+				s--;
+		}
+
+		if (s == e)
+			printf("[LN%c] ", 'A' + s);
+		else
+			printf("[LN%c-LN%c] ", 'A' + s, 'A' + e);
+
+		while (s <= e)
+			claimed[s++] = true;
+	}
+
+	for (m = 0; m < SRDS_LANES; m++)
+		if (!claimed[m])
+			orphan++;
+	if (orphan)
+		printf(" (%d lane%s claimed by no master)", orphan,
+		       orphan == 1 ? "" : "s");
+	printf("\n");
+}
+
+static void print_converters(unsigned long base)
+{
+	u32 pccc = in_le32((void __iomem *)SRDS_PCCC(base));
+	u32 pccd = in_le32((void __iomem *)SRDS_PCCD(base));
+	u32 pcce = in_le32((void __iomem *)SRDS_PCCE(base));
+	int i, any = 0;
+
+	printf("  protocol converters\n");
+	printf("      PCCC    %08x  (SXGMII/XFI)\n", pccc);
+	printf("      PCCD    %08x  (E25G)  ", pccd);
+	for (i = 0; i < SRDS_LANES; i++) {
+		u32 cfg = PCCD_E25G_CFG(pccd, i);
+
+		if (cfg)
+			printf("%sE25G%c=%d", any++ ? " " : "", 'a' + i, cfg);
+	}
+	printf("%s\n", any ? "" : "none enabled");
+
+	printf("      PCCE    %08x  (E40G/E50G/E100G)  E100Ga=%d%s E100Gb=%d%s\n",
+	       pcce,
+	       PCCE_E100GA_CFG(pcce), PCCE_E100GA_LRV(pcce) ? " (lane-reversed)" : "",
+	       PCCE_E100GB_CFG(pcce), PCCE_E100GB_LRV(pcce) ? " (lane-reversed)" : "");
+}
+
+static void print_rcw_view(int sd)
+{
+	struct ccsr_gur __iomem *gur = (void *)(CFG_SYS_FSL_GUTS_ADDR);
+	u32 cfg;
+	int regsr, shift;
+	u32 mask;
+
+	switch (sd) {
+	case FSL_SRDS_1:
+		regsr = FSL_CHASSIS3_SRDS1_REGSR;
+		mask  = FSL_CHASSIS3_SRDS1_PRTCL_MASK;
+		shift = FSL_CHASSIS3_SRDS1_PRTCL_SHIFT;
+		break;
+	case FSL_SRDS_2:
+		regsr = FSL_CHASSIS3_SRDS2_REGSR;
+		mask  = FSL_CHASSIS3_SRDS2_PRTCL_MASK;
+		shift = FSL_CHASSIS3_SRDS2_PRTCL_SHIFT;
+		break;
+#if defined(CONFIG_SYS_NXP_SRDS_3)
+	case NXP_SRDS_3:
+		regsr = FSL_CHASSIS3_SRDS3_REGSR;
+		mask  = FSL_CHASSIS3_SRDS3_PRTCL_MASK;
+		shift = FSL_CHASSIS3_SRDS3_PRTCL_SHIFT;
+		break;
+#endif
+	default:
+		return;
+	}
+
+	cfg = (gur_in32(&gur->rcwsr[regsr - 1]) & mask) >> shift;
+	printf("  RCWSR%-2d  raw=0x%04x -> protocol %d\n",
+	       regsr, cfg, serdes_get_number(sd, cfg));
+	printf("           this is what the Service Processor latched before the PBI ran,\n");
+	printf("           and what fsl_serdes_init() built its DPMAC map from.\n");
+}
+
+#if defined(CONFIG_ARCH_LX2160A)
+
+/*
+ * The split itself.
+ *
+ * This is NXP's e100g1_split.rcw sequence, moved from the PBI into
+ * U-Boot.
+ */
+#define SPLIT_FIRST_LANE	4	/* LNE */
+#define SPLIT_LAST_LANE		7	/* LNH */
+
+/*
+ * PROTO_SEL=11010b (25G/50G/100G, shared) + IF_WIDTH=100b (40-bit),
+ * PORT_RST_LEFT=0 and PORT_LN0_B=0 so every lane becomes its own
+ * single-lane port. Only those last two bits actually change: the
+ * protocol and width are already what CAUI-4 was using.
+ */
+#define SPLIT_GCR0		0x000000D4
+#define SPLIT_PCCE		0x00000100	/* E100Ga off, E100Gb on   */
+#define SPLIT_PCCD		0x11110000	/* E25Ga..E25Gd on         */
+
+#define SPLIT_POLL_US		10000		/* NXP waits ~100 cycles   */
+
+static bool srds1_split_state(unsigned long base)
+{
+	u32 pccd = in_le32((void __iomem *)SRDS_PCCD(base));
+	u32 pcce = in_le32((void __iomem *)SRDS_PCCE(base));
+
+	return PCCE_E100GA_CFG(pcce) == 0 &&
+	       PCCD_E25G_CFG(pccd, 0) && PCCD_E25G_CFG(pccd, 1) &&
+	       PCCD_E25G_CFG(pccd, 2) && PCCD_E25G_CFG(pccd, 3);
+}
+
+bool lx2160a_serdes1_is_split(void)
+{
+	return srds1_split_state(srds_base(FSL_SRDS_1));
+}
+
+static bool srds1_is_unsplit(unsigned long base)
+{
+	u32 pccd = in_le32((void __iomem *)SRDS_PCCD(base));
+	u32 pcce = in_le32((void __iomem *)SRDS_PCCE(base));
+
+	return PCCE_E100GA_CFG(pcce) != 0 && PCCE_E100GB_CFG(pcce) != 0 &&
+	       PCCD_E25G_CFG(pccd, 0) == 0 && PCCD_E25G_CFG(pccd, 1) == 0 &&
+	       PCCD_E25G_CFG(pccd, 2) == 0 && PCCD_E25G_CFG(pccd, 3) == 0;
+}
+
+/*
+ * Wait for a self-clearing request bit. Per the doc, hardware
+ * clears HLT_REQ when the halt completes and RST_REQ when the reset
+ * completes.
+ */
+static int poll_clear(unsigned long reg, u32 bit)
+{
+	int us = SPLIT_POLL_US;
+
+	while (us--) {
+		if (!(in_le32((void __iomem *)reg) & bit))
+			return 0;
+		udelay(1);
+	}
+
+	return -ETIMEDOUT;
+}
+
+int lx2160a_serdes1_split(void)
+{
+	unsigned long base = srds_base(FSL_SRDS_1);
+	int m, master = -1, ret = 0;
+
+	if (srds1_split_state(base)) {
+		debug("lx2160a serdes: SD1 already split, nothing to do\n");
+		return 0;
+	}
+
+	if (!srds1_is_unsplit(base)) {
+		printf("lx2160a serdes: SD1 is in neither the split nor the unsplit state (PCCD=%08x PCCE=%08x), refusing\n",
+		       in_le32((void __iomem *)SRDS_PCCD(base)),
+		       in_le32((void __iomem *)SRDS_PCCE(base)));
+		return -EINVAL;
+	}
+
+	/*
+	 * Halt. "Halt only has meaning on the master source
+	 * clock lane of a port (LNmGCR0[PORT_LN0_B]=0)". Lanes E-H are one
+	 * 100G port at this point, so exactly one of them is the master and
+	 * the other three requests are ignored. Issue all four anyway
+	 * (that is what NXP's sequence does) but only wait on the lane
+	 * where the bit can actually clear.
+	 */
+	for (m = SPLIT_FIRST_LANE; m <= SPLIT_LAST_LANE; m++) {
+		if (!(in_le32((void __iomem *)SRDS_LNmGCR0(base, m)) &
+		      GCR0_PORT_LN0_B))
+			master = m;
+		out_le32((void __iomem *)SRDS_LNmTRSTCTL(base, m),
+			 RSTCTL_HLT_REQ);
+		out_le32((void __iomem *)SRDS_LNmRRSTCTL(base, m),
+			 RSTCTL_HLT_REQ);
+	}
+
+	if (master < 0) {
+		printf("lx2160a serdes: no master lane among LNE-LNH, refusing\n");
+		return -EINVAL;
+	}
+
+	if (poll_clear(SRDS_LNmTRSTCTL(base, master), RSTCTL_HLT_REQ) ||
+	    poll_clear(SRDS_LNmRRSTCTL(base, master), RSTCTL_HLT_REQ)) {
+		printf("lx2160a serdes: LN%c did not halt within %d us, continuing\n",
+		       'A' + master, SPLIT_POLL_US);
+		ret = -ETIMEDOUT;
+	}
+
+	/*
+	 * Reconfigure while halted: four one-lane ports instead of one
+	 * four-lane port. Everything else about the lane is unchanged.
+	 */
+	for (m = SPLIT_FIRST_LANE; m <= SPLIT_LAST_LANE; m++)
+		out_le32((void __iomem *)SRDS_LNmGCR0(base, m), SPLIT_GCR0);
+
+	/* Re-point the protocol converters. */
+	out_le32((void __iomem *)SRDS_PCCE(base), SPLIT_PCCE);
+	out_le32((void __iomem *)SRDS_PCCD(base), SPLIT_PCCD);
+
+	/* Reset. Every lane is its own master now, so all four count. */
+	for (m = SPLIT_FIRST_LANE; m <= SPLIT_LAST_LANE; m++) {
+		out_le32((void __iomem *)SRDS_LNmTRSTCTL(base, m),
+			 RSTCTL_RST_REQ);
+		out_le32((void __iomem *)SRDS_LNmRRSTCTL(base, m),
+			 RSTCTL_RST_REQ);
+	}
+
+	for (m = SPLIT_FIRST_LANE; m <= SPLIT_LAST_LANE; m++) {
+		u32 t, r;
+
+		if (poll_clear(SRDS_LNmTRSTCTL(base, m), RSTCTL_RST_REQ) ||
+		    poll_clear(SRDS_LNmRRSTCTL(base, m), RSTCTL_RST_REQ)) {
+			printf("lx2160a serdes: LN%c reset did not complete within %d us\n",
+			       'A' + m, SPLIT_POLL_US);
+			ret = -ETIMEDOUT;
+			continue;
+		}
+
+		t = in_le32((void __iomem *)SRDS_LNmTRSTCTL(base, m));
+		r = in_le32((void __iomem *)SRDS_LNmRRSTCTL(base, m));
+		if (!(t & RSTCTL_RST_DONE) || !(r & RSTCTL_RST_DONE)) {
+			printf("lx2160a serdes: LN%c reset finished without RST_DONE (TRSTCTL=%08x RRSTCTL=%08x)\n",
+			       'A' + m, t, r);
+			ret = -EIO;
+		}
+	}
+
+	if (!srds1_split_state(base)) {
+		printf("lx2160a serdes: split did not take (PCCD=%08x PCCE=%08x)\n",
+		       in_le32((void __iomem *)SRDS_PCCD(base)),
+		       in_le32((void __iomem *)SRDS_PCCE(base)));
+		return -EIO;
+	}
+
+	printf("lx2160a serdes: SD1 100GE.1 split into 4x 25G%s\n",
+	       ret ? " (with warnings above)" : "");
+
+	return ret;
+}
+
+/*
+ *   setenv hwconfig serdes1:split
+ *
+ * Idempotent: a board whose PBI already split SD1 skip it too.
+ */
+void lx2160a_serdes_apply_hwconfig(void)
+{
+	if (!hwconfig_sub("serdes1", "split"))
+		return;
+
+	if (lx2160a_serdes1_is_split())
+		return;
+
+	lx2160a_serdes1_split();
+}
+
+#endif /* CONFIG_ARCH_LX2160A */
+
+static int do_serdes_status(int sd)
+{
+	unsigned long base = srds_base(sd);
+	int m;
+
+	printf("SerDes %d @ 0x%08lx\n", sd + 1, base);
+	print_rcw_view(sd);
+	print_grouping(base);
+	print_converters(base);
+	for (m = 0; m < SRDS_LANES; m++)
+		print_lane(base, m);
+
+	return CMD_RET_SUCCESS;
+}
+
+static int do_lx2160_serdes(struct cmd_tbl *cmdtp, int flag,
+			    int argc, char *const argv[])
+{
+	int sd;
+
+	if (argc < 2)
+		return CMD_RET_USAGE;
+
+	if (!strcmp(argv[1], "split")) {
+#if !defined(CONFIG_ARCH_LX2160A)
+		printf("no 100GE on SerDes 1 to split on this SoC\n");
+		return CMD_RET_FAILURE;
+#else
+		if (argc != 2)
+			return CMD_RET_USAGE;
+		if (lx2160a_serdes1_is_split()) {
+			printf("SD1 is already split, nothing to do\n");
+			return CMD_RET_SUCCESS;
+		}
+		if (lx2160a_serdes1_split())
+			return CMD_RET_FAILURE;
+		return CMD_RET_SUCCESS;
+#endif
+	}
+
+	if (strcmp(argv[1], "status"))
+		return CMD_RET_USAGE;
+
+	if (argc == 2) {
+		for (sd = FSL_SRDS_1; sd < SRDS_COUNT; sd++) {
+			do_serdes_status(sd);
+			if (sd != SRDS_COUNT - 1)
+				printf("\n");
+		}
+		return CMD_RET_SUCCESS;
+	}
+
+	sd = (int)dectoul(argv[2], NULL) - 1;
+	if (sd < FSL_SRDS_1 || sd >= SRDS_COUNT) {
+		printf("SerDes must be 1, 2 or 3\n");
+		return CMD_RET_FAILURE;
+	}
+
+	return do_serdes_status(sd);
+}
+
+#if defined(CONFIG_ARCH_LX2160A)
+#define SPLIT_CMD_HELP							\
+	"lx2160_serdes split\n"						\
+	"    - split Lynx 28G SD1 100GE.1 (lanes LNE-LNH) into 4x 25G.\n"	\
+	"      Runs before the MC starts. To make it permanent on a board\n"	\
+	"      that needs it: setenv hwconfig serdes1:split && saveenv"
+#else
+#define SPLIT_CMD_HELP ""
+#endif
+
+U_BOOT_CMD(
+	lx2160_serdes, 3, 0, do_lx2160_serdes,
+	"inspect the LX2160A 28G-Lynx SerDes protocol configuration",
+	"status [1|2|3]\n"
+	"    - print the live protocol converters (PCCC/PCCD/PCCE) and the\n"
+	"      per-lane port grouping and protocol (LNmGCR0), next to the\n"
+	"      protocol number the RCW latched.\n"
+	"      No argument prints all three SerDes."
+	SPLIT_CMD_HELP
+);
diff --git a/board/nxp/lx2160a/serdes_split.h b/board/nxp/lx2160a/serdes_split.h
new file mode 100644
index 00000000000..67e8c4873a7
--- /dev/null
+++ b/board/nxp/lx2160a/serdes_split.h
@@ -0,0 +1,35 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/*
+ * Copyright 2026 Free Mobile - Vincent Jardin
+ */
+
+#ifndef __LX2160A_SERDES_SPLIT_H
+#define __LX2160A_SERDES_SPLIT_H
+
+/*
+ * The split is LX2160A only: LX2162A's SerDes 1 is four lanes with no
+ * 100GE in its protocol table.
+ */
+#if defined(CONFIG_ARCH_LX2160A)
+
+/* Is SerDes 1 already 1x 100G + 4x 25G? */
+bool lx2160a_serdes1_is_split(void);
+
+/*
+ * Split SerDes 1's 100GE.1 (lanes LNE-LNH) into 4x 25G.
+ *
+ * Refuses unless SD1 is in the unsplit state, and must be called before
+ * the Management Complex starts: MC latches the live protocol-converter
+ * configuration when it boots.
+ */
+int lx2160a_serdes1_split(void);
+
+/*
+ * Apply `hwconfig=serdes1:split` if the board's environment asks for it.
+ * Boards call this from reset_phy(), before mc_env_boot().
+ */
+void lx2160a_serdes_apply_hwconfig(void);
+
+#endif /* CONFIG_ARCH_LX2160A */
+
+#endif /* __LX2160A_SERDES_SPLIT_H */
---
base-commit: 964ad5b5c91b7be56e443e899d7f873e6aa8c9fc
branch: for-upstream/lx2160a-serdes-split-v1

-- 
2.43.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.