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

Reply via email to