* [PATCH net-next v2 8/9] onsemi: s2500: Add driver support for TS2500 MAC-PHY
@ 2026-05-11 18:19 Selvamani Rajagopal
2026-05-11 20:12 ` Andrew Lunn
2026-05-11 20:18 ` Andrew Lunn
0 siblings, 2 replies; 9+ messages in thread
From: Selvamani Rajagopal @ 2026-05-11 18:19 UTC (permalink / raw)
To: Piergiorgio Beruto, andrew+netdev, davem, edumazet, kuba, pabeni,
netdev, linux-kernel
Support for onsemi's S2500, 802.3 cg compliant Ethernet
transceiver with integrated MAC-PHY. Works with
Open Alliance TC6 framework.
Updated MAINTAINERS file
Signed-off-by: Selvamani Rajagopal <Selvamani.Rajagopal@onsemi.com>
---
MAINTAINERS | 7 +
drivers/net/ethernet/Kconfig | 1 +
drivers/net/ethernet/Makefile | 1 +
drivers/net/ethernet/onsemi/Kconfig | 21 +
drivers/net/ethernet/onsemi/Makefile | 7 +
drivers/net/ethernet/onsemi/s2500/Kconfig | 21 +
drivers/net/ethernet/onsemi/s2500/Makefile | 7 +
.../net/ethernet/onsemi/s2500/s2500_ethtool.c | 674 +++++++++++++++++
.../net/ethernet/onsemi/s2500/s2500_hw_def.h | 255 +++++++
.../net/ethernet/onsemi/s2500/s2500_main.c | 698 ++++++++++++++++++
drivers/net/ethernet/onsemi/s2500/s2500_ptp.c | 247 +++++++
11 files changed, 1939 insertions(+)
create mode 100644 drivers/net/ethernet/onsemi/Kconfig
create mode 100644 drivers/net/ethernet/onsemi/Makefile
create mode 100644 drivers/net/ethernet/onsemi/s2500/Kconfig
create mode 100644 drivers/net/ethernet/onsemi/s2500/Makefile
create mode 100644 drivers/net/ethernet/onsemi/s2500/s2500_ethtool.c
create mode 100644 drivers/net/ethernet/onsemi/s2500/s2500_hw_def.h
create mode 100644 drivers/net/ethernet/onsemi/s2500/s2500_main.c
create mode 100644 drivers/net/ethernet/onsemi/s2500/s2500_ptp.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 6ed1e1e5f..f75269202 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -19947,6 +19947,13 @@ S: Supported
W: http://www.onsemi.com
F: drivers/net/phy/ncn*
+ONSEMI S2500 10BASE-T1S MACPHY ETHERNET DRIVER
+M: Piergiorgio Beruto <pier.beruto@onsemi.com>
+M: Selva Rajagopal <selvamani.rajagopal@onsemi.com>
+L: netdev@vger.kernel.org
+S: Maintained
+F: drivers/net/ethernet/onsemi/s2500/s2500_*
+
OP-TEE DRIVER
M: Jens Wiklander <jens.wiklander@linaro.org>
L: op-tee@lists.trustedfirmware.org (moderated for non-subscribers)
diff --git a/drivers/net/ethernet/Kconfig b/drivers/net/ethernet/Kconfig
index b8f70e2a1..a42656120 100644
--- a/drivers/net/ethernet/Kconfig
+++ b/drivers/net/ethernet/Kconfig
@@ -134,6 +134,7 @@ source "drivers/net/ethernet/8390/Kconfig"
source "drivers/net/ethernet/nvidia/Kconfig"
source "drivers/net/ethernet/nxp/Kconfig"
source "drivers/net/ethernet/oki-semi/Kconfig"
+source "drivers/net/ethernet/onsemi/Kconfig"
config ETHOC
tristate "OpenCores 10/100 Mbps Ethernet MAC support"
diff --git a/drivers/net/ethernet/Makefile b/drivers/net/ethernet/Makefile
index 57344fec6..38527c249 100644
--- a/drivers/net/ethernet/Makefile
+++ b/drivers/net/ethernet/Makefile
@@ -71,6 +71,7 @@ obj-$(CONFIG_NET_VENDOR_NI) += ni/
obj-$(CONFIG_NET_VENDOR_NVIDIA) += nvidia/
obj-$(CONFIG_LPC_ENET) += nxp/
obj-$(CONFIG_NET_VENDOR_OKI) += oki-semi/
+obj-$(CONFIG_NET_VENDOR_ONSEMI) += onsemi/
obj-$(CONFIG_ETHOC) += ethoc.o
obj-$(CONFIG_NET_VENDOR_PASEMI) += pasemi/
obj-$(CONFIG_NET_VENDOR_QLOGIC) += qlogic/
diff --git a/drivers/net/ethernet/onsemi/Kconfig b/drivers/net/ethernet/onsemi/Kconfig
new file mode 100644
index 000000000..8dd3a3f07
--- /dev/null
+++ b/drivers/net/ethernet/onsemi/Kconfig
@@ -0,0 +1,21 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# onsemi network device configuration
+#
+
+config NET_VENDOR_ONSEMI
+ bool "onsemi network devices"
+ help
+ If you have a network card belonging to this class, say Y.
+
+ Note that the answer to this question doesn't directly affect the
+ kernel: saying N will just cause the configurator to skip all
+ the questions about onsemi ethernet devices. If you say Y, you
+ will be asked for your specific card in the following questions.
+
+if NET_VENDOR_ONSEMI
+
+source "drivers/net/ethernet/onsemi/s2500/Kconfig"
+
+endif # NET_VENDOR_ONSEMI
+
diff --git a/drivers/net/ethernet/onsemi/Makefile b/drivers/net/ethernet/onsemi/Makefile
new file mode 100644
index 000000000..f3d4eb154
--- /dev/null
+++ b/drivers/net/ethernet/onsemi/Makefile
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Makefile for the onsemi network device drivers.
+#
+
+obj-$(CONFIG_S2500_MACPHY) += s2500/
+
diff --git a/drivers/net/ethernet/onsemi/s2500/Kconfig b/drivers/net/ethernet/onsemi/s2500/Kconfig
new file mode 100644
index 000000000..22b0afad7
--- /dev/null
+++ b/drivers/net/ethernet/onsemi/s2500/Kconfig
@@ -0,0 +1,21 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# onsemi S2500 Driver Support
+#
+
+if NET_VENDOR_ONSEMI
+
+config S2500_MACPHY
+ tristate "S2500 support"
+ depends on SPI
+ select NCN26000_PHY
+ select OA_TC6
+ help
+ Support for the onsemi TS2500 MACPHY Ethernet chip.
+ It works under the framework that conform to OPEN Alliance
+ 10BASE-T1x Serial Interface specification.
+
+ To compile this driver as a module, choose M here. The module will be
+ called s2500.
+
+endif # NET_VENDOR_ONSEMI
diff --git a/drivers/net/ethernet/onsemi/s2500/Makefile b/drivers/net/ethernet/onsemi/s2500/Makefile
new file mode 100644
index 000000000..61ec705cd
--- /dev/null
+++ b/drivers/net/ethernet/onsemi/s2500/Makefile
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Makefile for the onsemi network device drivers.
+#
+obj-$(CONFIG_S2500_MACPHY) := s2500.o
+s2500-objs := s2500_main.o s2500_ethtool.o s2500_ptp.o
+
diff --git a/drivers/net/ethernet/onsemi/s2500/s2500_ethtool.c b/drivers/net/ethernet/onsemi/s2500/s2500_ethtool.c
new file mode 100644
index 000000000..892b0f632
--- /dev/null
+++ b/drivers/net/ethernet/onsemi/s2500/s2500_ethtool.c
@@ -0,0 +1,674 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright 2026 Semiconductor Components Industries, LLC ("onsemi").
+ * onsemi's S2500 10BASE-T1S MAC-PHY driver
+ */
+
+#include "s2500_hw_def.h"
+#include <linux/ethtool.h>
+#include <linux/completion.h>
+#include <linux/mii.h>
+#include <linux/phy.h>
+
+#define S2500_NUM_REGS 38
+#define S2500_REGDUMP_LEN (sizeof(u32) * (S2500_NUM_REGS * 2))
+
+#define S2500_LB_ETH_P 0x88B5
+#define S2500_LB_PAYLOAD_LEN 64
+#define S2500_LB_DATA_SIGNATURE 0xC0DEFADE
+#define S2500_LB_DATA_PATTERN 0xA5
+#define S2500_LB_TEST_DATA_OFFSET 8
+#define S2500_LB_TEST_FRAMES 20
+#define S2500_LB_PASS_PERCENT 90
+#define S2500_LB_CAPTURE_TIMEOUT_MS 500
+#define S2500_LB_FRAME_GAP_US 1000
+
+struct s2500_lb_counters {
+ u32 tx_frames;
+ u32 rx_frames;
+};
+
+struct s2500_lb_capture {
+ struct net_device *ndev;
+ struct completion done;
+ const char *test_name;
+ struct packet_type pt;
+ u32 expected_frames;
+ u32 received_frames;
+ u32 expected_seq;
+ u32 payload_len;
+ /* Protect the callback */
+ spinlock_t lock;
+ bool failed;
+};
+
+static u32 phy_addr_list[S2500_NUM_REGS] = {
+ (0x4 << 16) | 0x8000,
+ (0x4 << 16) | 0x8001,
+ (0x4 << 16) | 0x8002,
+ (0x4 << 16) | 0x8003,
+ (0x4 << 16) | 0x8004,
+ (0x4 << 16) | 0x8007,
+ (0x4 << 16) | 0xCC01,
+ (0x4 << 16) | 0xCC02,
+ (0x4 << 16) | 0xCC03,
+ (0x4 << 16) | 0xCC04,
+ (0x4 << 16) | 0xCD00,
+ (0x4 << 16) | 0xCD01,
+ (0x4 << 16) | 0xCD02,
+ (0x4 << 16) | 0xD000,
+ (0x4 << 16) | 0xD001,
+ (0x4 << 16) | 0xD100,
+ (0x4 << 16) | 0xD101,
+ (0xC << 16) | 0x10,
+ (0xC << 16) | 0x11,
+ (0xC << 16) | 0x12,
+ (0xC << 16) | 0x1000,
+ (0xC << 16) | 0x1001,
+ (0xC << 16) | 0x1002,
+ (0xC << 16) | 0x1003,
+ (0xC << 16) | 0x1005,
+ (0xC << 16) | 0x1010,
+ (0xC << 16) | 0x1011,
+ (0xC << 16) | 0x1012,
+ (0xC << 16) | 0x1013,
+ (0xC << 16) | 0x1014,
+ (0xC << 16) | 0x1015,
+ (0xC << 16) | 0x1016,
+ (0xC << 16) | 0x1017,
+ (0xC << 16) | 0x1018,
+ (0xC << 16) | 0x1019,
+ (0xC << 16) | 0x101A,
+ (0xC << 16) | 0x101B,
+ (0xC << 16) | 0x101C,
+};
+
+static const char s2500_stat_strings[][ETH_GSTRING_LEN] = {
+ "tx-bytes-ok",
+ "tx-frames",
+ "tx-broadcast-frames",
+ "tx-multicast-frames",
+ "tx-64-frames",
+ "tx-65-127-frames",
+ "tx-128-255-frames",
+ "tx-256-511-frames",
+ "tx-512-1023-frames",
+ "tx-1024-1518-frames",
+ "tx-underrun-errors",
+ "tx-single-collision",
+ "tx-multiple-collision",
+ "tx-excessive-collision",
+ "tx-deferred-frames",
+ "tx-carrier-sense-errors",
+ "rx-bytes-ok",
+ "rx-frames",
+ "rx-broadcast-frames",
+ "rx-multicast-frames",
+ "rx-64-frames",
+ "rx-65-127-frames",
+ "rx-128-255-frames",
+ "rx-256-511-frames",
+ "rx-512-1023-frames",
+ "rx-1024-1518-frames",
+ "rx-runt",
+ "rx-too-long-frames",
+ "rx-crc-errors",
+ "rx-symbol-errors",
+ "rx-alignment-errors",
+ "rx-busy-drop-frames",
+ "rx-mismatch-drop-frames",
+ "ts_frames",
+};
+
+#define S2500_STATS_LEN ARRAY_SIZE(s2500_stat_strings)
+static_assert(S2500_STATS_LEN == S2500_STATS_NUM);
+
+#define S2500_TESTS_LEN 2
+
+static const char s2500_test_strings[][ETH_GSTRING_LEN] = {
+ "MAC loopback",
+ "PHY loopback",
+};
+
+#define STAT_REG_OFFSET(x) ((S2500_REG_MAC_ST##x) - \
+ S2500_REG_MAC_FIRST_STAT)
+
+static void s2500_update_mac_stats(struct s2500_info *priv)
+{
+ u64 *data = priv->stats_data;
+ u32 *regs, *rptr;
+ int ret;
+
+ regs = kmalloc_array(S2500_NUMBER_OF_STAT_REGS, sizeof(u32), GFP_KERNEL);
+ if (!regs)
+ return;
+
+ ret = oa_tc6_read_registers(priv->tc6, S2500_REG_MAC_STOCTECTSTXL,
+ regs, S2500_NUMBER_OF_STAT_REGS);
+ if (ret)
+ goto out;
+
+ rptr = regs;
+
+ /* TX bytes is a 64-bit register that spans over two 32-bit regs
+ * note: HW does auto-freeze when reading LSB and un-freeze on MSB
+ */
+ *(data++) += ((u64)*rptr) | (((u64)*(rptr + 1)) << 32);
+
+ /* run until the next 64-bit register */
+ for (rptr += 2; (rptr - regs) < STAT_REG_OFFSET(OCTECTSRXL); ++rptr)
+ *(data++) += *rptr;
+
+ /* RX bytes is a 64-bit register that spans over two 32-bit regs
+ * note: HW does auto-freeze when reading LSB and un-freeze on MSB
+ */
+ *(data++) += ((u64)*rptr) | (((u64)*(rptr + 1)) << 32);
+
+ for (rptr += 2; (rptr - regs) < S2500_NUMBER_OF_STAT_REGS; ++rptr)
+ *(data++) += *rptr;
+ priv->stats_data[S2500_STATS_NUM - 1] = priv->ts_frames;
+out:
+ kfree(regs);
+}
+
+static void s2500_get_drvinfo(struct net_device *ndev,
+ struct ethtool_drvinfo *info)
+{
+ strscpy(info->driver, DRV_NAME, sizeof(info->driver));
+ strscpy(info->bus_info, dev_name(&ndev->dev), sizeof(info->bus_info));
+ strscpy(info->version, DRV_VERSION, sizeof(info->version));
+}
+
+static int s2500_ethtool_set_link_ksettings(struct net_device *ndev,
+ const struct ethtool_link_ksettings *cmd)
+{
+ phy_ethtool_ksettings_set(ndev->phydev, cmd);
+ return 0;
+}
+
+static int s2500_ethtool_get_link_ksettings(struct net_device *ndev,
+ struct ethtool_link_ksettings *cmd)
+{
+ phy_ethtool_ksettings_get(ndev->phydev, cmd);
+ return 0;
+}
+
+static int s2500_get_sset_count(struct net_device *ndev, int sset)
+{
+ if (sset == ETH_SS_STATS)
+ return S2500_STATS_LEN;
+ else if (sset == ETH_SS_TEST)
+ return S2500_TESTS_LEN;
+ else
+ return -EOPNOTSUPP;
+}
+
+static void s2500_get_strings(struct net_device *ndev, u32 stringset,
+ u8 *buf)
+{
+ if (stringset == ETH_SS_STATS)
+ memcpy(buf, s2500_stat_strings,
+ S2500_STATS_LEN * ETH_GSTRING_LEN);
+ else if (stringset == ETH_SS_TEST)
+ memcpy(buf, s2500_test_strings,
+ S2500_TESTS_LEN * ETH_GSTRING_LEN);
+}
+
+static void s2500_get_ethtool_stats(struct net_device *ndev,
+ struct ethtool_stats *stats, u64 *data)
+{
+ struct s2500_info *priv = netdev_priv(ndev);
+
+ s2500_update_mac_stats(priv);
+ memcpy(data, priv->stats_data, sizeof(priv->stats_data));
+}
+
+static int s2500_get_ts_info(struct net_device *ndev,
+ struct kernel_ethtool_ts_info *ts_info)
+{
+ struct s2500_info *priv = netdev_priv(ndev);
+
+ if (!priv->ptp_clock)
+ return ethtool_op_get_ts_info(ndev, ts_info);
+
+ ts_info->so_timestamping = SOF_TIMESTAMPING_RAW_HARDWARE |
+ SOF_TIMESTAMPING_TX_HARDWARE |
+ SOF_TIMESTAMPING_RX_HARDWARE;
+ ts_info->phc_index = ptp_clock_index(priv->ptp_clock);
+ ts_info->tx_types = BIT(HWTSTAMP_TX_ON);
+ ts_info->rx_filters = BIT(HWTSTAMP_FILTER_ALL);
+ return 0;
+}
+
+static int s2500_get_regs_len(struct net_device *dev)
+{
+ return S2500_REGDUMP_LEN;
+}
+
+static void s2500_get_regs(struct net_device *ndev,
+ struct ethtool_regs *regs, void *p)
+{
+ struct s2500_info *priv = netdev_priv(ndev);
+ u32 *pbuff = (u32 *)p;
+ u32 val, reg;
+ int ret = 0;
+ int i;
+
+ regs->version = 0;
+ memset(p, 0, S2500_REGDUMP_LEN);
+
+ if (!netif_running(ndev))
+ return;
+
+ for (i = 0; i < S2500_NUM_REGS; i++) {
+ val = 0;
+ reg = phy_addr_list[i];
+ ret = oa_tc6_read_register(priv->tc6, reg, &val);
+ if (ret)
+ continue;
+ *pbuff++ = cpu_to_be32(reg);
+ *pbuff++ = cpu_to_be32(val);
+ }
+}
+
+static void s2500_lb_fill_payload(u8 *payload, u32 seq, u32 payload_len)
+{
+ u32 sig = S2500_LB_DATA_SIGNATURE;
+ int seq_offset;
+ u32 i;
+
+ seq_offset = sizeof(sig);
+ memcpy(payload, &sig, sizeof(sig));
+ memcpy(&payload[seq_offset], &seq, sizeof(seq));
+
+ for (i = S2500_LB_TEST_DATA_OFFSET; i < payload_len; i++)
+ payload[i] = (u8)(S2500_LB_DATA_PATTERN ^ (seq + i));
+}
+
+static bool s2500_lb_payload_valid(const u8 *payload, u32 exp_seq,
+ u32 payload_len)
+{
+ int seq_offset;
+ u32 sig, seq;
+ u32 j;
+
+ if (payload_len < S2500_LB_TEST_DATA_OFFSET)
+ return false;
+
+ memcpy(&sig, payload, sizeof(sig));
+ if (sig != S2500_LB_DATA_SIGNATURE)
+ return false;
+
+ seq_offset = sizeof(sig);
+ memcpy(&seq, &payload[seq_offset], sizeof(seq));
+ if (seq != exp_seq)
+ return false;
+
+ for (j = S2500_LB_TEST_DATA_OFFSET; j < payload_len; j++) {
+ if (payload[j] != (u8)(S2500_LB_DATA_PATTERN ^ (exp_seq + j)))
+ return false;
+ }
+
+ return true;
+}
+
+static void s2500_log_lb_failure(struct net_device *ndev,
+ const char *test_name, const char *reason)
+{
+ netdev_err(ndev, "%s loopback test failed: %s\n", test_name, reason);
+}
+
+static void s2500_lb_capture_init(struct s2500_lb_capture *cap,
+ struct net_device *ndev,
+ const char *test_name,
+ u32 expected_frames)
+{
+ init_completion(&cap->done);
+ spin_lock_init(&cap->lock);
+ cap->ndev = ndev;
+ cap->test_name = test_name;
+ cap->expected_frames = expected_frames;
+ cap->received_frames = 0;
+ cap->expected_seq = 0;
+ cap->payload_len = S2500_LB_PAYLOAD_LEN;
+ cap->failed = false;
+ memset(&cap->pt, 0, sizeof(cap->pt));
+ cap->pt.type = cpu_to_be16(S2500_LB_ETH_P);
+ cap->pt.dev = ndev;
+ cap->pt.func = NULL;
+}
+
+static int s2500_lb_capture_rx(struct sk_buff *skb, struct net_device *dev,
+ struct packet_type *pt,
+ struct net_device *orig_dev)
+{
+ struct s2500_lb_capture *cap = container_of(pt,
+ struct s2500_lb_capture,
+ pt);
+ u8 payload[S2500_LB_PAYLOAD_LEN];
+ unsigned long flags;
+ bool failed = false;
+ u32 frame_seq;
+
+ (void)dev;
+
+ if (orig_dev != cap->ndev)
+ goto out_free;
+
+ if (skb->len != cap->payload_len) {
+ s2500_log_lb_failure(cap->ndev, cap->test_name,
+ "RX frame length mismatch");
+ failed = true;
+ } else if (skb_copy_bits(skb, 0, payload, cap->payload_len) != 0) {
+ s2500_log_lb_failure(cap->ndev, cap->test_name,
+ "unable to copy RX payload");
+ failed = true;
+ }
+
+ spin_lock_irqsave(&cap->lock, flags);
+ frame_seq = cap->expected_seq;
+ if (failed || cap->failed ||
+ cap->received_frames >= cap->expected_frames) {
+ if (cap->received_frames >= cap->expected_frames)
+ s2500_log_lb_failure(cap->ndev, cap->test_name,
+ "extra frame received");
+ cap->failed = true;
+ complete_all(&cap->done);
+ spin_unlock_irqrestore(&cap->lock, flags);
+ goto out_free;
+ }
+
+ if (!s2500_lb_payload_valid(payload, frame_seq,
+ cap->payload_len)) {
+ s2500_log_lb_failure(cap->ndev, cap->test_name,
+ "payload mismatch");
+ cap->failed = true;
+ complete_all(&cap->done);
+ spin_unlock_irqrestore(&cap->lock, flags);
+ goto out_free;
+ }
+
+ if (frame_seq != cap->received_frames) {
+ netdev_err(cap->ndev,
+ "out-of-order RX frame, expected %u got %u\n",
+ cap->received_frames, frame_seq);
+ cap->failed = true;
+ complete_all(&cap->done);
+ spin_unlock_irqrestore(&cap->lock, flags);
+ goto out_free;
+ }
+
+ cap->received_frames++;
+ cap->expected_seq++;
+ if (cap->received_frames == cap->expected_frames)
+ complete_all(&cap->done);
+ spin_unlock_irqrestore(&cap->lock, flags);
+
+out_free:
+ kfree_skb(skb);
+ return 0;
+}
+
+static int s2500_lb_enable(struct s2500_info *priv, u32 reg, u32 *saved)
+{
+ int ret;
+ u32 val;
+
+ ret = oa_tc6_read_register(priv->tc6, reg, &val);
+ if (ret)
+ return ret;
+
+ *saved = val;
+ val |= BMCR_LOOPBACK;
+
+ return oa_tc6_write_register(priv->tc6, reg, val);
+}
+
+static int s2500_lb_restore(struct s2500_info *priv, u32 reg, u32 saved)
+{
+ return oa_tc6_write_register(priv->tc6, reg, saved);
+}
+
+static int s2500_read_lb_counters(struct s2500_info *priv,
+ struct s2500_lb_counters *cnt)
+{
+ int ret;
+
+ ret = oa_tc6_read_register(priv->tc6, S2500_REG_MAC_STFRAMESTXOK,
+ &cnt->tx_frames);
+ if (ret)
+ return ret;
+
+ ret = oa_tc6_read_register(priv->tc6, S2500_REG_MAC_STFRAMESRXOK,
+ &cnt->rx_frames);
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
+static int s2500_send_lb_frames(struct net_device *ndev, int frame_count,
+ u16 eth_proto)
+{
+ struct s2500_info *priv = netdev_priv(ndev);
+ const int payload_len = 64;
+ unsigned char *frame;
+ struct sk_buff *skb;
+ int frame_len;
+ int sent = 0;
+ int ret, i;
+
+ frame_len = ETH_HLEN + payload_len;
+ for (i = 0; i < frame_count; i++) {
+ skb = alloc_skb(frame_len + NET_IP_ALIGN, GFP_KERNEL);
+ if (skb)
+ skb_reserve(skb, NET_IP_ALIGN);
+ if (!skb)
+ break;
+
+ frame = skb_put(skb, frame_len);
+ memset(frame, 0xff, ETH_ALEN);
+ memcpy(frame + ETH_ALEN, ndev->dev_addr, ETH_ALEN);
+ frame[12] = (u8)((eth_proto >> 8) & 0xff);
+ frame[13] = (u8)(eth_proto & 0xff);
+ s2500_lb_fill_payload(frame + ETH_HLEN, i, payload_len);
+
+ skb->dev = ndev;
+ skb->protocol = htons(eth_proto);
+ skb->priority = 0;
+
+ ret = oa_tc6_start_xmit(priv->tc6, skb);
+ if (ret == NETDEV_TX_OK) {
+ sent++;
+ } else {
+ netdev_warn(ndev, "TX frame %d failed\n", i);
+ kfree_skb(skb);
+ break;
+ }
+
+ udelay(S2500_LB_FRAME_GAP_US);
+ }
+ return sent;
+}
+
+static void s2500_eval_lb_result(struct net_device *ndev,
+ struct ethtool_test *eth_test, u64 *data,
+ unsigned int idx,
+ const char *test_name,
+ const struct s2500_lb_counters *before,
+ const struct s2500_lb_counters *after,
+ const struct s2500_lb_capture *capture,
+ int req_frames,
+ int sent_frames)
+{
+ u64 tx_delta, rx_delta;
+ bool failed = false;
+
+ tx_delta = (after->tx_frames >= before->tx_frames) ?
+ (u64)(after->tx_frames - before->tx_frames) : 0;
+ rx_delta = (after->rx_frames >= before->rx_frames) ?
+ (u64)(after->rx_frames - before->rx_frames) : 0;
+
+ if (sent_frames < req_frames) {
+ netdev_err(ndev,
+ "%s: transmit failure, sent %d of %d frames\n",
+ test_name, sent_frames, req_frames);
+ failed = true;
+ }
+ if (tx_delta < (u64)req_frames) {
+ netdev_err(ndev,
+ "%s: TX counter did not increment, delta=%llu expected=%d\n",
+ test_name, tx_delta, req_frames);
+ failed = true;
+ }
+ if (rx_delta == 0) {
+ netdev_err(ndev,
+ "%s: no frames received back\n",
+ test_name);
+ failed = true;
+ } else if (rx_delta * 100 < (u64)req_frames * S2500_LB_PASS_PERCENT) {
+ netdev_err(ndev,
+ "%s: received too few frames, rx delta=%llu expected=%d\n",
+ test_name, rx_delta, req_frames);
+ failed = true;
+ }
+
+ if (capture->failed ||
+ capture->received_frames != capture->expected_frames) {
+ if (capture->received_frames == 0)
+ netdev_err(ndev,
+ "%s: no received frames captured\n",
+ test_name);
+ else if (capture->received_frames != capture->expected_frames)
+ netdev_err(ndev,
+ "%s: received frame count mismatch, got %u expected %u\n",
+ test_name, capture->received_frames, capture->expected_frames);
+ failed = true;
+ }
+
+ data[idx] = failed ? 1 : 0;
+ if (failed)
+ eth_test->flags |= ETH_TEST_FL_FAILED;
+}
+
+static int s2500_run_lb_test(struct net_device *ndev,
+ struct s2500_info *priv,
+ u32 loop_reg, bool use_phy_loopback,
+ const char *test_name,
+ struct ethtool_test *eth_test,
+ u64 *data, unsigned int idx)
+{
+ struct s2500_lb_counters before;
+ struct s2500_lb_capture capture;
+ struct s2500_lb_counters after;
+ u32 saved_val = 0;
+ long timeout;
+ int sent;
+ int ret;
+
+ s2500_lb_capture_init(&capture, ndev, test_name, S2500_LB_TEST_FRAMES);
+ capture.pt.func = s2500_lb_capture_rx;
+
+ if (use_phy_loopback)
+ ret = phy_loopback(ndev->phydev, true, 0);
+ else
+ ret = s2500_lb_enable(priv, loop_reg, &saved_val);
+ if (ret) {
+ data[idx] = 1;
+ eth_test->flags |= ETH_TEST_FL_FAILED;
+ return 0;
+ }
+
+ /* First call clears the counters */
+ ret = s2500_read_lb_counters(priv, &before);
+
+ /* Counter values before starting the test (likely to be 0) */
+ ret = s2500_read_lb_counters(priv, &before);
+ if (ret) {
+ data[idx] = 1;
+ eth_test->flags |= ETH_TEST_FL_FAILED;
+ goto restore;
+ }
+
+ dev_add_pack(&capture.pt);
+ sent = s2500_send_lb_frames(ndev, S2500_LB_TEST_FRAMES,
+ S2500_LB_ETH_P);
+ timeout = wait_for_completion_timeout(&capture.done,
+ msecs_to_jiffies(S2500_LB_CAPTURE_TIMEOUT_MS));
+ dev_remove_pack(&capture.pt);
+ if (!timeout)
+ capture.failed = true;
+
+ ret = s2500_read_lb_counters(priv, &after);
+ if (ret) {
+ data[idx] = 1;
+ eth_test->flags |= ETH_TEST_FL_FAILED;
+ goto restore;
+ }
+
+ s2500_eval_lb_result(ndev, eth_test, data, idx, test_name,
+ &before, &after, &capture,
+ S2500_LB_TEST_FRAMES, sent);
+
+restore:
+ if (use_phy_loopback)
+ ret = phy_loopback(ndev->phydev, false, 0);
+ else
+ ret = s2500_lb_restore(priv, loop_reg, saved_val);
+ if (ret) {
+ data[idx] = 1;
+ eth_test->flags |= ETH_TEST_FL_FAILED;
+ }
+
+ return 0;
+}
+
+static void s2500_self_test(struct net_device *ndev,
+ struct ethtool_test *eth_test, u64 *data)
+{
+ struct s2500_info *priv = netdev_priv(ndev);
+
+ if (!priv)
+ return;
+
+ if (!mutex_trylock(&priv->selftest_lock)) {
+ netdev_warn(ndev, "selftest already running\n");
+ return;
+ }
+
+ eth_test->flags &= ~ETH_TEST_FL_FAILED;
+ data[0] = 0;
+ data[1] = 0;
+
+ netif_stop_queue(ndev);
+ if (eth_test->len < S2500_TESTS_LEN)
+ goto unlock;
+
+ /* MAC loopback is controlled directly through the PCS loopback bit;
+ * PHY loopback is routed through phylib so the PHY driver handles the
+ * standard BMCR loopback state transition.
+ */
+ s2500_run_lb_test(ndev, priv, S2500_REG_PCS_CTRL,
+ false, "MAC", eth_test, data, 0);
+ s2500_run_lb_test(ndev, priv, S2500_REG_PHY_CTRL,
+ true, "PHY", eth_test, data, 1);
+
+unlock:
+ netif_start_queue(ndev);
+ mutex_unlock(&priv->selftest_lock);
+}
+
+const struct ethtool_ops s2500_ethtool_ops = {
+ .get_drvinfo = s2500_get_drvinfo,
+ .get_link = ethtool_op_get_link,
+ .get_link_ksettings = s2500_ethtool_get_link_ksettings,
+ .set_link_ksettings = s2500_ethtool_set_link_ksettings,
+ .get_sset_count = s2500_get_sset_count,
+ .get_strings = s2500_get_strings,
+ .get_ethtool_stats = s2500_get_ethtool_stats,
+ .get_ts_info = s2500_get_ts_info,
+ .get_regs_len = s2500_get_regs_len,
+ .get_regs = s2500_get_regs,
+ .self_test = s2500_self_test,
+};
+
diff --git a/drivers/net/ethernet/onsemi/s2500/s2500_hw_def.h b/drivers/net/ethernet/onsemi/s2500/s2500_hw_def.h
new file mode 100644
index 000000000..fa79a2ee0
--- /dev/null
+++ b/drivers/net/ethernet/onsemi/s2500/s2500_hw_def.h
@@ -0,0 +1,255 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright 2026 Semiconductor Components Industries, LLC ("onsemi").
+ * onsemi's S2500 10BASE-T1S MAC-PHY driver
+ */
+
+#ifndef S2500_HW_DEF_H
+#define S2500_HW_DEF_H
+
+#include <linux/hrtimer.h>
+#include <linux/irq.h>
+#include <linux/irqdomain.h>
+#include <linux/kernel.h>
+#include <linux/netdevice.h>
+#include <linux/phylink.h>
+#include <linux/spi/spi.h>
+#include <linux/oa_tc6.h>
+#include <linux/net_tstamp.h>
+#include <linux/ptp_clock_kernel.h>
+#include <linux/delay.h>
+#include <linux/mutex.h>
+#include <linux/ktime.h>
+#include <linux/errno.h>
+
+#define DRV_NAME "s2500"
+#define DRV_VERSION "1.0.3.1"
+
+#define S2500_N_MCAST_FILTERS 3
+
+#define S2500_MODEL_ID 0xA7A8
+
+/* Number of stat counters for ethtool */
+#define S2500_STATS_NUM 34
+
+/* List of device capabilities */
+#define S2500_CAP_MACADDR BIT(0) /* MAC address in hardware */
+#define S2500_CAP_PTP BIT(1) /* PTP support */
+
+/* S2500 registers */
+
+/* Definitions for MMS defined in Table 6 Open Alliance TC6 standard
+ * that not present in oa_tc6.h
+ */
+#define S2500_OA_TC6_MACPHY_MMS0 0
+#define S2500_OA_TC6_MAC_MMS1 1
+#define S2500_OA_TC6_VEND1_MMS12 12
+
+#define S2500_MMS_MII (S2500_OA_TC6_MACPHY_MMS0 << 16)
+#define S2500_MMS_MAC (S2500_OA_TC6_MAC_MMS1 << 16)
+#define S2500_MMS_PCS (2 << 16)
+#define S2500_MMS_PMDPMA (3 << 16)
+#define S2500_MMS_VS1 (S2500_OA_TC6_VEND1_MMS12 << 16)
+#define S2500_MMS_VS2 (4 << 16)
+
+/* SPI OID and model register */
+#define S2500_REG_SPI_PHYID 0x1
+
+#define S2500_SPI_PHYID_OUI_SHIFT 10
+#define S2500_SPI_PHYID_OUI_MASK GENMASK(31, S2500_SPI_PHYID_OUI_SHIFT)
+#define S2500_SPI_PHYID_MODEL_SHIFT 4
+#define S2500_SPI_PHYID_MODEL_MASK GENMASK(9, S2500_SPI_PHYID_MODEL_SHIFT)
+#define S2500_SPI_PHYID_REV_SHIFT 0
+#define S2500_SPI_PHYID_REV_MASK GENMASK(3, S2500_SPI_PHYID_REV_SHIFT)
+
+/* SPI configuration register #0 */
+#define S2500_REG_SPI_CFG0 (S2500_MMS_MII + 0x4)
+
+#define S2500_SPI_CFG0_SYNC_BIT BIT(15)
+#define S2500_SPI_CFG0_TXFCSVE_BIT BIT(14)
+#define S2500_SPI_CFG0_CSARFE_BIT BIT(13)
+#define S2500_SPI_CFG0_TXCTHRESH_SHIFT 10
+#define S2500_SPI_CFG0_TXCTHRESH_MASK GENMASK(11, S2500_SPI_CFG0_TXCTHRESH_SHIFT)
+#define S2500_SPI_CFG0_TXCTE_BIT BIT(9)
+#define S2500_SPI_CFG0_RXCTE_BIT BIT(8)
+#define S2500_SPI_CFG0_FTSE_BIT BIT(7)
+#define S2500_SPI_CFG0_FTSS_BIT BIT(6)
+#define S2500_SPI_CFG0_PROTE_BIT BIT(5)
+#define S2500_SPI_CFG0_SEQE_BIT BIT(4)
+#define S2500_SPI_CFG0_CPS_SHIFT 0
+#define S2500_SPI_CFG0_CPS_MASK GENMASK(2, S2500_SPI_CFG0_CPS_SHIFT)
+
+#define S2500_TXCTHRESH_1 0x0
+#define S2500_TXCTHRESH_4 0x1
+#define S2500_TXCTHRESH_8 0x2
+
+#define S2500_CPS_8 0x3
+#define S2500_CPS_16 0x4
+#define S2500_CPS_32 0x5
+#define S2500_CPS_64 0x6
+
+/* SPI status register #0 */
+#define S2500_REG_SPI_ST0 (S2500_MMS_MII + 8)
+
+#define S2500_SPI_ST0_CDPE_BIT BIT(12)
+#define S2500_SPI_ST0_TXFCSE_BIT BIT(11)
+#define S2500_SPI_ST0_TTSCAC_BIT BIT(10)
+#define S2500_SPI_ST0_TTSCAB_BIT BIT(9)
+#define S2500_SPI_ST0_TTSCAA_BIT BIT(8)
+#define S2500_SPI_ST0_PHYINT_BIT BIT(7)
+#define S2500_SPI_ST0_RESETC_BIT BIT(6)
+#define S2500_SPI_ST0_HDRE_BIT BIT(5)
+#define S2500_SPI_ST0_LOFE_BIT BIT(4)
+#define S2500_SPI_ST0_RXBOE_BIT BIT(3)
+#define S2500_SPI_ST0_TXBUE_BIT BIT(2)
+#define S2500_SPI_ST0_TXBOE_BIT BIT(1)
+#define S2500_SPI_ST0_TXPE_BIT BIT(0)
+
+/* SPI IRQ enable register #0 (use the S2500_SPI_ST0_*_BIT constants) */
+#define S2500_REG_SPI_IRQM0 (S2500_MMS_MII + 0xc)
+
+/* SPI buffer status register */
+#define S2500_REG_SPI_BUFST 0xb
+
+#define S2500_SPI_BUFST_TXC_SHIFT 8
+#define S2500_SPI_BUFST_TXC_MASK GENMASK(15, S2500_SPI_BUFST_TXC_SHIFT)
+
+#define S2500_REG_MAC_CONTROL (S2500_MMS_MAC + 0)
+
+#define S2500_MAC_CONTROL_MCSF_BIT BIT(18)
+#define S2500_MAC_CONTROL_ADRF_BIT BIT(16)
+#define S2500_MAC_CONTROL_FCSA_BIT BIT(8)
+#define S2500_MAC_CONTROL_TXEN_BIT BIT(1)
+#define S2500_MAC_CONTROL_RXEN_BIT BIT(0)
+
+#define S2500_REG_PHY_CTRL (S2500_MMS_MII + 0xFF00)
+
+#define S2500_REG_PCS_CTRL (S2500_MMS_PCS + 0x8F3)
+
+/* MAC address filter registers */
+#define S2500_REG_MAC_ADDRFILTL(n) (S2500_MMS_MAC + (16 + 2 * (n)))
+#define S2500_REG_MAC_ADDRFILTH(n) (S2500_MMS_MAC + (17 + 2 * (n)))
+#define S2500_REG_MAC_ADDRMASKL(n) (S2500_MMS_MAC + (32 + 2 * (n)))
+#define S2500_REG_MAC_ADDRMASKH(n) (S2500_MMS_MAC + (33 + 2 * (n)))
+
+#define S2500_MAC_ADDRFILT_EN_BIT BIT(31)
+
+/* MAC statistic registers */
+#define S2500_REG_MAC_STOCTECTSTXL (S2500_MMS_MAC + 48)
+#define S2500_REG_MAC_STOCTECTSTXH (S2500_MMS_MAC + 49)
+#define S2500_REG_MAC_STFRAMESTXOK (S2500_MMS_MAC + 50)
+#define S2500_REG_MAC_STBCASTTXOK (S2500_MMS_MAC + 51)
+#define S2500_REG_MAC_STMCASTTXOK (S2500_MMS_MAC + 52)
+#define S2500_REG_MAC_STFRAMESTX64 (S2500_MMS_MAC + 53)
+#define S2500_REG_MAC_STFRAMESTX65 (S2500_MMS_MAC + 54)
+#define S2500_REG_MAC_STFRAMESTX128 (S2500_MMS_MAC + 55)
+#define S2500_REG_MAC_STFRAMESTX256 (S2500_MMS_MAC + 56)
+#define S2500_REG_MAC_STFRAMESTX512 (S2500_MMS_MAC + 57)
+#define S2500_REG_MAC_STFRAMESTX1024 (S2500_MMS_MAC + 58)
+#define S2500_REG_MAC_STTXUNDEFLOW (S2500_MMS_MAC + 59)
+#define S2500_REG_MAC_STSINGLECOL (S2500_MMS_MAC + 60)
+#define S2500_REG_MAC_STMULTICOL (S2500_MMS_MAC + 61)
+#define S2500_REG_MAC_STEXCESSCOL (S2500_MMS_MAC + 62)
+#define S2500_REG_MAC_STDEFERREDTX (S2500_MMS_MAC + 63)
+#define S2500_REG_MAC_STCRSERR (S2500_MMS_MAC + 64)
+#define S2500_REG_MAC_STOCTECTSRXL (S2500_MMS_MAC + 65)
+#define S2500_REG_MAC_STOCTECTSRXH (S2500_MMS_MAC + 66)
+#define S2500_REG_MAC_STFRAMESRXOK (S2500_MMS_MAC + 67)
+#define S2500_REG_MAC_STBCASTRXOK (S2500_MMS_MAC + 68)
+#define S2500_REG_MAC_STMCASTRXOK (S2500_MMS_MAC + 69)
+#define S2500_REG_MAC_STFRAMESRX64 (S2500_MMS_MAC + 60)
+#define S2500_REG_MAC_STFRAMESRX65 (S2500_MMS_MAC + 71)
+#define S2500_REG_MAC_STFRAMESRX128 (S2500_MMS_MAC + 72)
+#define S2500_REG_MAC_STFRAMESRX256 (S2500_MMS_MAC + 73)
+#define S2500_REG_MAC_STFRAMESRX512 (S2500_MMS_MAC + 74)
+#define S2500_REG_MAC_STFRAMESRX1024 (S2500_MMS_MAC + 75)
+#define S2500_REG_MAC_STRUNTSERR (S2500_MMS_MAC + 76)
+#define S2500_REG_MAC_STRXTOOLONG (S2500_MMS_MAC + 77)
+#define S2500_REG_MAC_STFCSERRS (S2500_MMS_MAC + 78)
+#define S2500_REG_MAC_STSYMBOLERRS (S2500_MMS_MAC + 79)
+#define S2500_REG_MAC_STALIGNERRS (S2500_MMS_MAC + 80)
+#define S2500_REG_MAC_STRXOVERFLOW (S2500_MMS_MAC + 81)
+#define S2500_REG_MAC_STRXDROPPED (S2500_MMS_MAC + 82)
+
+/* First/last statistic register for sequential access */
+#define S2500_REG_MAC_FIRST_STAT S2500_REG_MAC_STOCTECTSTXL
+#define S2500_REG_MAC_LAST_STAT S2500_REG_MAC_STRXDROPPED
+
+#define S2500_NUMBER_OF_STAT_REGS \
+ (S2500_REG_MAC_LAST_STAT - S2500_REG_MAC_FIRST_STAT + 1)
+
+/* Permanent MAC address register */
+#define S2500_REG_VS_MACID0 (S2500_MMS_VS1 + 0x1002)
+#define S2500_REG_VS_MACID1 (S2500_MMS_VS1 + 0x1003)
+
+#define S2500_MACID1_UID_SHIFT 0
+#define S2500_MACID1_UID_MASK GENMASK(7, S2500_MACID1_UID_SHIFT)
+
+/* Chip identification register */
+#define S2500_REG_VS_CHIPID (S2500_MMS_VS1 + 0x1000)
+
+#define S2500_CHIPID_MODEL_SHIFT 16
+#define S2500_CHIPID_MODEL_MASK GENMASK(31, S2500_CHIPID_MODEL_SHIFT)
+#define S2500_CHIPID_REVISION_SHIFT 0
+#define S2500_CHIPID_REVISION_MASK GENMASK(15, S2500_CHIPID_REVISION_SHIFT)
+
+/* MIIM IRQ status register */
+#define S2500_REG_MIIM_IRQ_STATUS (S2500_MMS_VS1 + 0x11)
+#define MIIM_IRQ_STATUS_RSTS_SHIFT 15
+#define MIIM_IRQ_STATUS_RSTS BIT(MIIM_IRQ_STATUS_RSTS_SHIFT)
+
+/* PTP registers */
+#define S2500_REG_VS_PTP_SEC (S2500_MMS_VS1 + 0x1010)
+#define S2500_REG_VS_PTP_SETSEC (S2500_MMS_VS1 + 0x1012)
+#define S2500_REG_VS_PTP_ADJ (S2500_MMS_VS1 + 0x1014)
+
+/* prototypes / forward declarations */
+extern const struct ethtool_ops s2500_ethtool_ops;
+
+struct s2500_info;
+
+struct s2500_info {
+ struct device *dev;
+ struct net_device *ndev;
+
+ /* model information */
+ u32 model;
+ u32 version;
+ unsigned int capabilities;
+
+ /* To have atomic set_rx_mode operation */
+ spinlock_t lock;
+
+ /* To avoid simultaneous selftest request */
+ struct mutex selftest_lock;
+
+ /* To have atomic operation when time is adjusted */
+ struct mutex ptp_adj_lock;
+ struct task_struct *thread;
+
+ /* global state variables */
+ bool event_pending;
+ unsigned int ndev_flags;
+ bool rx_flags_upd;
+
+ bool tx_fcs_calc;
+
+ signed long poll_jiff;
+
+ struct spi_device *spi;
+
+ /* statistic counters variables */
+ u64 stats_data[S2500_STATS_NUM];
+
+ /* PTP related variables */
+ struct ptp_clock_info ptp_clock_info;
+ struct ptp_clock *ptp_clock;
+ u32 ts_frames;
+ void *tc6;
+};
+
+void s2500_ptp_unregister(struct s2500_info *priv);
+void s2500_ptp_register(struct s2500_info *priv);
+
+#endif /* S2500_HW_DEF_H */
+
diff --git a/drivers/net/ethernet/onsemi/s2500/s2500_main.c b/drivers/net/ethernet/onsemi/s2500/s2500_main.c
new file mode 100644
index 000000000..5eb245d65
--- /dev/null
+++ b/drivers/net/ethernet/onsemi/s2500/s2500_main.c
@@ -0,0 +1,698 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright 2026 Semiconductor Components Industries, LLC ("onsemi").
+ * onsemi's S2500 10BASE-T1S MAC-PHY driver
+ */
+
+#include "s2500_hw_def.h"
+
+#include <linux/etherdevice.h>
+#include <linux/if_ether.h>
+#include <linux/irqchip.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/phy.h>
+
+/* S2500 functions & definitions */
+
+#define S2500_STATUS0_MASK (S2500_SPI_ST0_CDPE_BIT | \
+ S2500_SPI_ST0_TXFCSE_BIT | \
+ S2500_SPI_ST0_TTSCAC_BIT | \
+ S2500_SPI_ST0_TTSCAB_BIT | \
+ S2500_SPI_ST0_TTSCAA_BIT | \
+ S2500_SPI_ST0_RESETC_BIT | \
+ S2500_SPI_ST0_HDRE_BIT | \
+ S2500_SPI_ST0_LOFE_BIT | \
+ S2500_SPI_ST0_RXBOE_BIT | \
+ S2500_SPI_ST0_TXBUE_BIT | \
+ S2500_SPI_ST0_TXBOE_BIT | \
+ S2500_SPI_ST0_TXPE_BIT)
+
+/* Converts a MACPHY ID to a device name */
+static inline const char *s2500_id_to_name(u32 id)
+{
+ if (id == S2500_MODEL_ID)
+ return "S2500";
+ return "unknown";
+}
+
+/* Initializes the net device MAC address by reading the UID stored
+ * into the device internal non-volatile memory.
+ */
+static int s2500_read_mac_from_nvmem(struct s2500_info *priv)
+{
+ u8 addr[ETH_ALEN];
+ u32 mac1 = 0;
+ u32 mac0 = 0;
+ int i, j;
+ u32 val;
+ int ret;
+
+ ret = oa_tc6_read_register(priv->tc6, S2500_REG_SPI_PHYID, &val);
+ if (ret)
+ return ret;
+
+ val = (val & S2500_SPI_PHYID_OUI_MASK) >> S2500_SPI_PHYID_OUI_SHIFT;
+
+ /* Convert the OID in host byte order */
+ for (i = 2; i >= 0; --i) {
+ addr[i] = 0;
+ for (j = 0; j < 8; ++j) {
+ addr[i] |= (val & 1) << (7 - j);
+ val >>= 1;
+ }
+ }
+
+ ret = oa_tc6_read_register(priv->tc6, S2500_REG_VS_MACID1, &mac1);
+ if (ret)
+ return ret;
+
+ ret = oa_tc6_read_register(priv->tc6, S2500_REG_VS_MACID0, &mac0);
+ if (ret)
+ return ret;
+
+ /* Pre-production parts may have 0 */
+ if (mac0 == 0 && mac1 == 0)
+ return -ENXIO;
+
+ addr[3] = mac1 & 0xff;
+ addr[4] = (mac0 >> 8) & 0xff;
+ addr[5] = mac0 & 0xff;
+
+ __dev_addr_set(priv->ndev, addr, ETH_ALEN);
+ priv->ndev->addr_assign_type = NET_ADDR_PERM;
+
+ return ret;
+}
+
+/* Writes MAC address to macphy registers */
+static int s2500_set_mac_filter(struct net_device *ndev, const u8 *mac)
+{
+ struct s2500_info *priv = netdev_priv(ndev);
+ u32 val;
+ int ret;
+
+ /* Set unicast address filter */
+ ret = oa_tc6_write_register(priv->tc6, S2500_REG_MAC_ADDRMASKL(0),
+ 0xffffffff);
+ if (ret)
+ return ret;
+
+ ret = oa_tc6_write_register(priv->tc6, S2500_REG_MAC_ADDRMASKH(0),
+ 0xffff);
+ if (ret)
+ return ret;
+
+ val = ((u32)mac[2] << 24) |
+ ((u32)mac[3] << 16) |
+ ((u32)mac[4] << 8) |
+ ((u32)mac[5]);
+
+ ret = oa_tc6_write_register(priv->tc6, S2500_REG_MAC_ADDRFILTL(0), val);
+ if (ret)
+ return ret;
+
+ val = S2500_MAC_ADDRFILT_EN_BIT | ((u32)mac[0] << 8) | ((u32)mac[1]);
+
+ return oa_tc6_write_register(priv->tc6, S2500_REG_MAC_ADDRFILTH(0), val);
+}
+
+static int s2500_mac_ctrl_clear_bits(struct s2500_info *priv, u32 in_bits,
+ bool clr)
+{
+ u32 reg = S2500_REG_MAC_CONTROL;
+ u32 rval = 0;
+ int ret;
+
+ ret = oa_tc6_read_register(priv->tc6, reg, &rval);
+ if (!ret) {
+ u32 wval = 0;
+
+ if (clr)
+ wval = rval & ~in_bits;
+ else
+ wval = rval | in_bits;
+ if (rval != wval)
+ ret = oa_tc6_write_register(priv->tc6, reg, wval);
+ }
+ return ret;
+}
+
+static int s2500_init(struct s2500_info *priv)
+{
+ u32 val;
+ int ret;
+
+ /* Configure the SPI protocol */
+ val = (S2500_SPI_CFG0_SYNC_BIT) | S2500_SPI_CFG0_RXCTE_BIT |
+ (S2500_TXCTHRESH_8 << S2500_SPI_CFG0_TXCTHRESH_SHIFT) |
+ (S2500_CPS_64 << S2500_SPI_CFG0_CPS_SHIFT);
+
+ if (priv->tx_fcs_calc)
+ val |= S2500_SPI_CFG0_TXFCSVE_BIT;
+
+ ret = oa_tc6_write_register(priv->tc6, S2500_REG_SPI_CFG0, val);
+ if (ret)
+ return ret;
+
+ val = (u32)~(S2500_SPI_ST0_RESETC_BIT |
+ S2500_SPI_ST0_HDRE_BIT | S2500_SPI_ST0_LOFE_BIT |
+ S2500_SPI_ST0_RXBOE_BIT | S2500_SPI_ST0_TXBOE_BIT |
+ S2500_SPI_ST0_TXPE_BIT);
+
+ ret = oa_tc6_write_register(priv->tc6, S2500_REG_SPI_IRQM0, val);
+ if (ret)
+ return ret;
+
+ /* Read the initial value of TX credits */
+ ret = oa_tc6_read_register(priv->tc6, S2500_REG_SPI_BUFST, &val);
+ if (ret)
+ return ret;
+
+ /* Program the source MAC address into the device */
+ ret = s2500_set_mac_filter(priv->ndev, priv->ndev->dev_addr);
+
+ val = S2500_MAC_CONTROL_ADRF_BIT;
+ if (!priv->tx_fcs_calc)
+ val |= S2500_MAC_CONTROL_FCSA_BIT;
+
+ return s2500_mac_ctrl_clear_bits(priv, val, false);
+}
+
+static void s2500_shutdown(struct s2500_info *priv)
+{
+ u32 val = S2500_MAC_CONTROL_TXEN_BIT | S2500_MAC_CONTROL_RXEN_BIT;
+ struct net_device *ndev = priv->ndev;
+
+ netif_stop_queue(ndev);
+ phy_stop(ndev->phydev);
+
+ s2500_mac_ctrl_clear_bits(priv, val, true);
+}
+
+static int s2500_set_promiscuous_mode(struct s2500_info *priv,
+ unsigned int rx_flags)
+{
+ u32 val = S2500_MAC_CONTROL_ADRF_BIT;
+ bool clr = false;
+
+ if (rx_flags & IFF_PROMISC)
+ clr = true;
+ return s2500_mac_ctrl_clear_bits(priv, val, clr);
+}
+
+static int s2500_set_multicast_mode(struct s2500_info *priv,
+ unsigned int rx_flags)
+{
+ int i, ret = 0;
+ u32 val;
+
+ if ((rx_flags & IFF_ALLMULTI) ||
+ (netdev_mc_count(priv->ndev) > S2500_N_MCAST_FILTERS)) {
+ /* Disable multicast filter */
+ ret = s2500_mac_ctrl_clear_bits(priv,
+ S2500_MAC_CONTROL_MCSF_BIT,
+ true);
+ if (ret)
+ return ret;
+
+ /* Accept all multicasts */
+ ret = oa_tc6_write_register(priv->tc6,
+ S2500_REG_MAC_ADDRMASKL(1), 0);
+ if (ret)
+ return ret;
+
+ ret = oa_tc6_write_register(priv->tc6,
+ S2500_REG_MAC_ADDRMASKH(1), 0x100);
+ if (ret)
+ return ret;
+
+ ret = oa_tc6_write_register(priv->tc6,
+ S2500_REG_MAC_ADDRFILTL(1), 0);
+ if (ret)
+ return ret;
+
+ val = S2500_MAC_ADDRFILT_EN_BIT | 0x00000100;
+ ret = oa_tc6_write_register(priv->tc6,
+ S2500_REG_MAC_ADDRFILTH(1), val);
+ } else if (netdev_mc_count(priv->ndev) == 0) {
+ /* Enable multicast filter */
+ ret = s2500_mac_ctrl_clear_bits(priv,
+ S2500_MAC_CONTROL_MCSF_BIT,
+ false);
+ if (ret)
+ return ret;
+
+ /* Disable filters */
+ for (i = 1; i <= S2500_N_MCAST_FILTERS; i++) {
+ ret = oa_tc6_write_register(priv->tc6,
+ S2500_REG_MAC_ADDRFILTH(i),
+ 0);
+ if (ret)
+ return ret;
+ }
+ } else {
+ struct netdev_hw_addr *ha;
+ u32 addrh, addrl;
+
+ /* Disable multicast filter */
+ ret = s2500_mac_ctrl_clear_bits(priv,
+ S2500_MAC_CONTROL_MCSF_BIT,
+ true);
+ if (ret)
+ return ret;
+
+ /* Disable filters */
+ for (i = 1; i <= S2500_N_MCAST_FILTERS; i++) {
+ ret = oa_tc6_write_register(priv->tc6,
+ S2500_REG_MAC_ADDRFILTH(i),
+ 0);
+ if (ret)
+ return ret;
+ }
+
+ i = 1;
+ netdev_for_each_mc_addr(ha, priv->ndev) {
+ if (i > S2500_N_MCAST_FILTERS)
+ break;
+
+ addrh = ((ha->addr[0] << 8) | ha->addr[1] |
+ S2500_MAC_ADDRFILT_EN_BIT);
+ addrl = ((ha->addr[2] << 24) | (ha->addr[3] << 16) |
+ (ha->addr[4] << 8) | (ha->addr[5]));
+
+ ret = oa_tc6_write_register(priv->tc6,
+ S2500_REG_MAC_ADDRFILTH(i),
+ addrh);
+ if (ret)
+ return ret;
+
+ ret = oa_tc6_write_register(priv->tc6,
+ S2500_REG_MAC_ADDRFILTL(i),
+ addrl);
+ if (ret)
+ return ret;
+
+ ret = oa_tc6_write_register(priv->tc6,
+ S2500_REG_MAC_ADDRMASKL(i),
+ 0xffffffff);
+ if (ret)
+ return ret;
+
+ ret = oa_tc6_write_register(priv->tc6,
+ S2500_REG_MAC_ADDRMASKH(i),
+ 0xffff);
+ if (ret)
+ return ret;
+ i++;
+ }
+ }
+ return ret;
+}
+
+/* Deferred function for applying RX mode flags in non-atomic context */
+static int s2500_rx_mode_update(struct s2500_info *priv)
+{
+ unsigned int rx_flags;
+ unsigned long flags;
+ int ret;
+
+ spin_lock_irqsave(&priv->lock, flags);
+
+ rx_flags = priv->ndev_flags;
+ priv->rx_flags_upd = false;
+
+ spin_unlock_irqrestore(&priv->lock, flags);
+
+ ret = s2500_set_promiscuous_mode(priv, rx_flags);
+ if (ret)
+ goto out;
+
+ ret = s2500_set_multicast_mode(priv, rx_flags);
+out:
+ return ret;
+}
+
+static int s2500_ioctl(struct net_device *ndev, struct ifreq *rq, int cmd)
+{
+ struct s2500_info *priv = netdev_priv(ndev);
+ int ret = -EOPNOTSUPP;
+
+ if (cmd == SIOCSHWTSTAMP || cmd == SIOCGHWTSTAMP) {
+ if (priv->capabilities & S2500_CAP_PTP)
+ ret = oa_tc6_hwtstamp_ioctl(priv->tc6, rq, cmd);
+ } else {
+ ret = phy_do_ioctl_running(ndev, rq, cmd);
+ }
+ return ret;
+}
+
+static void s2500_set_rx_mode(struct net_device *ndev)
+{
+ struct s2500_info *priv = netdev_priv(ndev);
+ unsigned long flags;
+
+ spin_lock_irqsave(&priv->lock, flags);
+
+ priv->rx_flags_upd = true;
+ priv->ndev_flags = ndev->flags;
+
+ spin_unlock_irqrestore(&priv->lock, flags);
+
+ if (priv->thread)
+ wake_up_process(priv->thread);
+}
+
+static int s2500_set_mac_address(struct net_device *ndev, void *p)
+{
+ struct sockaddr *addr = p;
+
+ if (!is_valid_ether_addr(addr->sa_data))
+ return -EADDRNOTAVAIL;
+
+ eth_hw_addr_set(ndev, addr->sa_data);
+ return s2500_set_mac_filter(ndev, addr->sa_data);
+}
+
+static netdev_tx_t s2500_start_xmit(struct sk_buff *skb,
+ struct net_device *ndev)
+{
+ struct s2500_info *priv = netdev_priv(ndev);
+
+ if (skb_shinfo(skb)->tx_flags & SKBTX_HW_TSTAMP)
+ priv->ts_frames++;
+
+ return oa_tc6_start_xmit(priv->tc6, skb);
+}
+
+static void s2500_process_events(struct s2500_info *priv)
+{
+ u32 val;
+ int ret;
+
+ if (!priv->event_pending)
+ return;
+
+ priv->event_pending = false;
+
+ ret = oa_tc6_read_register(priv->tc6, S2500_REG_SPI_ST0, &val);
+ if (ret) {
+ dev_err(&priv->spi->dev, "Error reading ST0 register");
+ return;
+ }
+}
+
+static int s2500_thread_fun(void *data)
+{
+ struct s2500_info *priv = data;
+ bool update_rx_mode = false;
+ unsigned long flags;
+ signed long tout;
+ int ret = 0;
+
+ tout = priv->poll_jiff;
+
+ do {
+ if (update_rx_mode) {
+ ret = s2500_rx_mode_update(priv);
+ if (unlikely(ret)) {
+ dev_err(&priv->spi->dev, "Failed to set new RX mode");
+ break;
+ }
+ }
+
+ if (tout == 0) {
+ tout = priv->poll_jiff;
+
+ /* Force checking the status register */
+ priv->event_pending = true;
+ }
+
+ s2500_process_events(priv);
+
+ spin_lock_irqsave(&priv->lock, flags);
+ __set_current_state(TASK_INTERRUPTIBLE);
+
+ update_rx_mode = priv->rx_flags_upd;
+ ret = update_rx_mode;
+
+ spin_unlock_irqrestore(&priv->lock, flags);
+
+ if (!ret)
+ tout = schedule_timeout(tout);
+ else
+ set_current_state(TASK_RUNNING);
+ } while (!kthread_should_stop());
+ return 0;
+}
+
+static int s2500_open(struct net_device *ndev)
+{
+ struct s2500_info *priv = netdev_priv(ndev);
+ int ret = 0;
+ u32 val;
+
+ dev_info(&priv->spi->dev, "%s", "s2500_open");
+ phy_start(priv->ndev->phydev);
+
+ priv->thread = kthread_run(s2500_thread_fun, priv, DRV_NAME "/%s:%d",
+ dev_name(&priv->spi->dev),
+ spi_get_chipselect(priv->spi, 0));
+
+ if (IS_ERR(priv->thread)) {
+ ret = PTR_ERR(priv->thread);
+ } else {
+ val = S2500_MAC_CONTROL_TXEN_BIT | S2500_MAC_CONTROL_RXEN_BIT;
+ ret = s2500_mac_ctrl_clear_bits(priv, val, false);
+
+ netif_start_queue(priv->ndev);
+ }
+ return ret;
+}
+
+static int s2500_stop(struct net_device *ndev)
+{
+ struct s2500_info *priv = netdev_priv(ndev);
+
+ dev_info(&priv->spi->dev, "%s", "s2500_stop");
+
+ s2500_shutdown(priv);
+
+ kthread_stop(priv->thread);
+ priv->thread = NULL;
+
+ return 0;
+}
+
+static int s2500_hwtstamp_get(struct net_device *ndev,
+ struct kernel_hwtstamp_config *cfg)
+{
+ struct s2500_info *priv = netdev_priv(ndev);
+
+ if (!priv->ptp_clock) {
+ cfg->tx_type = 0;
+ cfg->rx_filter = 0;
+ } else {
+ oa_tc6_hwtstamp_get(priv->tc6, cfg);
+ }
+ return 0;
+}
+
+static int s2500_hwtstamp_set(struct net_device *ndev,
+ struct kernel_hwtstamp_config *cfg,
+ struct netlink_ext_ack *extack)
+{
+ struct s2500_info *priv = netdev_priv(ndev);
+
+ if (!netif_running(ndev))
+ return -EIO;
+ if (!priv->ptp_clock)
+ return -EPROTONOSUPPORT;
+ return oa_tc6_hwtstamp_set(priv->tc6, cfg);
+}
+
+static const struct net_device_ops s2500_netdev_ops = {
+ .ndo_open = s2500_open,
+ .ndo_stop = s2500_stop,
+ .ndo_start_xmit = s2500_start_xmit,
+ .ndo_set_mac_address = s2500_set_mac_address,
+ .ndo_set_rx_mode = s2500_set_rx_mode,
+ .ndo_eth_ioctl = s2500_ioctl,
+ .ndo_hwtstamp_get = s2500_hwtstamp_get,
+ .ndo_hwtstamp_set = s2500_hwtstamp_set,
+};
+
+static int s2500_update_model(struct s2500_info *priv)
+{
+ u32 val = 0;
+ int ret;
+
+ ret = oa_tc6_read_register(priv->tc6, S2500_REG_VS_CHIPID, &val);
+ if (ret)
+ return ret;
+
+ priv->capabilities = S2500_CAP_MACADDR;
+ priv->version = val & S2500_CHIPID_REVISION_MASK;
+ priv->version >>= S2500_CHIPID_REVISION_SHIFT;
+
+ priv->model = val & S2500_CHIPID_MODEL_MASK;
+ priv->model >>= S2500_CHIPID_MODEL_SHIFT;
+ if (priv->model == S2500_MODEL_ID)
+ priv->capabilities |= S2500_CAP_PTP;
+ else
+ ret = -ENODEV;
+ dev_info(&priv->spi->dev, "Macphy model %s, version %u\n",
+ s2500_id_to_name(priv->model), priv->version);
+ return ret;
+}
+
+static int s2500_probe(struct spi_device *spi)
+{
+ struct device *dev = &spi->dev;
+ struct net_device *ndev;
+ struct s2500_info *priv;
+ u32 val;
+ int ret;
+
+ if (spi->irq < 0)
+ return -ENODEV;
+
+ ndev = devm_alloc_etherdev(dev, sizeof(struct s2500_info));
+ if (!ndev)
+ return -ENOMEM;
+
+ priv = netdev_priv(ndev);
+ priv->ndev = ndev;
+ priv->spi = spi;
+ priv->dev = dev;
+
+ SET_NETDEV_DEV(ndev, dev);
+
+ spin_lock_init(&priv->lock);
+ mutex_init(&priv->selftest_lock);
+ mutex_init(&priv->ptp_adj_lock);
+ ndev->irq = spi->irq;
+
+ spi->dev.platform_data = priv;
+ spi_set_drvdata(spi, priv);
+
+ ndev->netdev_ops = &s2500_netdev_ops;
+ ndev->ethtool_ops = &s2500_ethtool_ops;
+ ndev->if_port = IF_PORT_10BASET;
+ ndev->priv_flags |= IFF_UNICAST_FLT;
+ ndev->hw_features = NETIF_F_RXALL;
+
+ priv->tx_fcs_calc = false;
+ priv->poll_jiff = HZ * 5; /* Poll interval */
+
+ if (!priv->tx_fcs_calc)
+ priv->ndev->hw_features |= NETIF_F_RXFCS;
+
+ priv->tc6 = oa_tc6_init(spi, ndev);
+ if (!priv->tc6) {
+ dev_err(&spi->dev, "OA TC6 init failed");
+ return -ENODEV;
+ }
+ oa_tc6_set_vend1_mms(priv->tc6, S2500_OA_TC6_VEND1_MMS12);
+ s2500_update_model(priv);
+
+ /* Clear RSTS, if set */
+ oa_tc6_read_register(priv->tc6, S2500_REG_MIIM_IRQ_STATUS, &val);
+ val &= MIIM_IRQ_STATUS_RSTS;
+ if (val != 0)
+ oa_tc6_write_register(priv->tc6, S2500_REG_MIIM_IRQ_STATUS,
+ MIIM_IRQ_STATUS_RSTS);
+
+ /* Acknowledge all IRQ status bits */
+ ret = oa_tc6_read_register(priv->tc6, S2500_REG_SPI_ST0, &val);
+ if (!ret) {
+ u32 mask = S2500_STATUS0_MASK;
+
+ val &= mask;
+ oa_tc6_write_register(priv->tc6, S2500_REG_SPI_ST0, val);
+ }
+
+ /* Start with non-protected control accesses for single register reads.
+ * NOTE: the device starts in this mode after reset, but it is possible
+ * that the PROTE bit was set by a previous module load/unload.
+ * In this case, non-protected register writes won't work, but -single-
+ * unprotected register reads will. Therefore, we can safely probe the
+ * device using regular control accesses and switch to protected mode
+ * later, when resetting the device.
+ */
+
+ ret = oa_tc6_read_register(priv->tc6, S2500_REG_SPI_CFG0, &val);
+ if (ret)
+ goto err_reg_read;
+
+ ret = device_get_ethdev_address(priv->dev, ndev);
+ if (ret && (priv->capabilities & S2500_CAP_MACADDR))
+ ret = s2500_read_mac_from_nvmem(priv);
+
+ if (ret) {
+ eth_hw_addr_random(ndev);
+ dev_warn(&spi->dev, "Using random MAC address %pM", ndev->dev_addr);
+ }
+
+ ret = s2500_init(priv);
+ if (unlikely(ret)) {
+ dev_err(&spi->dev, "failed to s2500_init the device");
+ goto err_reg_read;
+ }
+
+ /* Configure PTP if the model supports it */
+ if (priv->capabilities & S2500_CAP_PTP)
+ s2500_ptp_register(priv);
+
+ ret = register_netdev(ndev);
+ if (ret) {
+ dev_err(&spi->dev, "failed to register the S2500 device\n");
+ ret = -ENODEV;
+
+ goto err_reg_read;
+ }
+ return 0;
+
+err_reg_read:
+ dev_err(&spi->dev, "could not initialize macphy");
+ return ret;
+}
+
+static void s2500_remove(struct spi_device *spi)
+{
+ struct s2500_info *priv = spi->dev.platform_data;
+
+ dev_info(&spi->dev, "%s", "s2500_remove");
+
+ s2500_ptp_unregister(priv);
+ unregister_netdev(priv->ndev);
+ oa_tc6_exit(priv->tc6);
+}
+
+static const struct of_device_id s2500_of_match[] = {
+ { .compatible = "onnn,s2500" },
+ {}
+};
+
+static const struct spi_device_id s2500_ids[] = {
+ { "s2500" },
+ {}
+};
+
+MODULE_DEVICE_TABLE(spi, s2500_ids);
+
+static struct spi_driver s2500_driver = {
+ .driver = {
+ .name = DRV_NAME,
+ .of_match_table = s2500_of_match,
+ },
+ .probe = s2500_probe,
+ .remove = s2500_remove,
+ .id_table = s2500_ids,
+};
+
+module_spi_driver(s2500_driver);
+
+MODULE_AUTHOR("Piergiorgio Beruto <Pier.Beruto@onsemi.com>");
+MODULE_DESCRIPTION("onsemi MACPHY ethernet driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/net/ethernet/onsemi/s2500/s2500_ptp.c b/drivers/net/ethernet/onsemi/s2500/s2500_ptp.c
new file mode 100644
index 000000000..26449a58e
--- /dev/null
+++ b/drivers/net/ethernet/onsemi/s2500/s2500_ptp.c
@@ -0,0 +1,247 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright 2026 Semiconductor Components Industries, LLC ("onsemi").
+ * onsemi's S2500 10BASE-T1S MAC-PHY driver
+ */
+
+#include "s2500_hw_def.h"
+
+static int s2500_ptp_get_time64(struct ptp_clock_info *ptp,
+ struct timespec64 *ts,
+ struct ptp_system_timestamp *ptp_sts)
+{
+ struct s2500_info *priv = container_of(ptp, struct s2500_info,
+ ptp_clock_info);
+ u32 data[2];
+ int ret;
+
+ ptp_read_system_prets(ptp_sts);
+ ret = oa_tc6_read_registers(priv->tc6, S2500_REG_VS_PTP_SEC,
+ &data[0], 2);
+ ptp_read_system_postts(ptp_sts);
+
+ if (!ret) {
+ ts->tv_sec = data[0];
+ ts->tv_nsec = data[1];
+ }
+
+ return ret;
+}
+
+static int s2500_ptp_set_time64(struct ptp_clock_info *ptp,
+ const struct timespec64 *ts)
+{
+ struct s2500_info *priv = container_of(ptp, struct s2500_info,
+ ptp_clock_info);
+ u32 data[2];
+
+ if (ts->tv_sec >= (1ULL << 32))
+ return -ERANGE;
+
+ data[0] = (u32)ts->tv_sec;
+ data[1] = ts->tv_nsec | BIT(31); /* bit 31 = execute set command */
+
+ return oa_tc6_write_registers(priv->tc6, S2500_REG_VS_PTP_SETSEC,
+ &data[0], 2);
+}
+
+static int s2500_ptp_adjfine(struct ptp_clock_info *ptp, long scaled_ppm)
+{
+ struct s2500_info *priv = container_of(ptp, struct s2500_info,
+ ptp_clock_info);
+ u32 sign_bit = 0;
+ long adj;
+ u32 val;
+ u64 ppm;
+
+ if (scaled_ppm < 0) {
+ /* split sign / mod */
+ sign_bit = 1U << 31;
+ scaled_ppm = ~scaled_ppm + 1;
+ }
+
+ /**
+ * Convert unsigned scaled_ppm to atto-seconds per clock cycles.
+ * The scaled_ppm format is Qx.16 --> 1 lsb = 1/65536 ppm.
+ * The clock period of the S2500 is 8ns (125 MHz), so 1 lsb of
+ * adj register LSB is 1 atto-sec / 8ns = 0.000125 ppm.
+ * Represented in Qx.16 format, this is 0.000125 * 2^16 = 8(.192)
+ * To convert scaled_ppm into a register value we need to divide
+ * it by the LSB value, hence adj = (scaled_ppm * 1000) / 8192 to
+ * minimize the precision loss due to the integer arithmetic.
+ * That further reduces to (scaled_ppm * 125) / 1024.
+ */
+ ppm = (u64)scaled_ppm * 125;
+ do_div(ppm, 1024);
+ adj = (long)ppm;
+
+ /* check overflow */
+ if (adj >= (1L << 28))
+ return -ERANGE;
+
+ val = (u32)adj | sign_bit;
+ return oa_tc6_write_register(priv->tc6, S2500_REG_VS_PTP_ADJ, val);
+}
+
+/* Implemented by using "settime" */
+static int s2500_ptp_adjtime(struct ptp_clock_info *ptp, s64 delta)
+{
+ struct s2500_info *priv = container_of(ptp, struct s2500_info,
+ ptp_clock_info);
+ struct ptp_system_timestamp sts;
+ struct timespec64 target;
+ unsigned int period_ms;
+ struct timespec64 now;
+ int max_iters = 3;
+ s64 scaled_ppm;
+ s64 remaining;
+ s64 target_ns;
+ int ret = 0;
+ s64 now_ns;
+ s64 num;
+ s64 den;
+
+ if (!ptp)
+ return -EINVAL;
+
+ /* Nothing to do */
+ if (delta == 0)
+ return 0;
+
+ if (mutex_lock_interruptible(&priv->ptp_adj_lock))
+ return -EINTR;
+
+ /* Try to slew the clock using adjfine for better accuracy. For large
+ * adjustments fall back to setting time directly.
+ */
+ remaining = delta;
+
+ while (remaining != 0 && max_iters--) {
+ s64 abs_delta = remaining > 0 ? remaining : -remaining;
+
+ /* If the adjustment is very large, more than 1 second,
+ * use settime to avoid very long slewing periods or
+ * excessive frequency offsets.
+ */
+ if (abs_delta > 1000000000LL) {
+ memset(&sts, 0, sizeof(sts));
+ ret = ptp->gettimex64(ptp, &now, &sts);
+ if (!ret) {
+ struct timespec64 delta_ts;
+
+ if (remaining >= 0) {
+ delta_ts = ns_to_timespec64(remaining);
+ target = timespec64_add(now, delta_ts);
+ } else {
+ delta_ts = ns_to_timespec64(-remaining);
+ target = timespec64_sub(now, delta_ts);
+ }
+ }
+
+ if (target.tv_sec < 0 || target.tv_sec >= (1ULL << 32))
+ ret = -ERANGE;
+ else
+ ret = ptp->settime64(ptp, &target);
+
+ remaining = 0;
+ break;
+ }
+
+ /* Choose a slewing period depending on magnitude */
+ if (abs_delta <= 1000000LL) /* <= 1ms */
+ period_ms = 1000; /* 1 s */
+ else if (abs_delta <= 100000000LL) /* <= 100ms */
+ period_ms = 10000; /* 10 s */
+ else
+ period_ms = 60000; /* 60 s */
+
+ /* compute current time and fixed target for this iteration */
+ memset(&sts, 0, sizeof(sts));
+ ret = ptp->gettimex64(ptp, &now, &sts);
+ if (ret)
+ break;
+
+ if (remaining >= 0)
+ target = timespec64_add(now, ns_to_timespec64(remaining));
+ else
+ target = timespec64_sub(now, ns_to_timespec64(-remaining));
+
+ /* Compute scaled_ppm (Qx.16). scaled_ppm = ppm * 2^16
+ * ppm = (delta_seconds / period_seconds) * 1e6
+ * => scaled_ppm = delta_ns * 65536 / (period_ms * 1000)
+ */
+ num = remaining * 65536LL;
+ den = (s64)period_ms * 1000LL;
+
+ /* Integer division rounds toward zero; keep sign in numerator */
+ scaled_ppm = div_s64(num, den);
+
+ /* Apply frequency adjustment */
+ ret = ptp->adjfine(ptp, (long)scaled_ppm);
+ if (ret)
+ break;
+
+ /* Sleep for the slew period (interruptible). If interrupted, clear
+ * the adjfine and return with -EINTR.
+ */
+ if (msleep_interruptible(period_ms)) {
+ /* Clear adjfine */
+ ptp->adjfine(ptp, 0);
+ ret = -EINTR;
+ break;
+ }
+
+ /* Clear adjfine and measure remaining offset */
+ ptp->adjfine(ptp, 0);
+
+ memset(&sts, 0, sizeof(sts));
+ ret = ptp->gettimex64(ptp, &now, &sts);
+ if (ret)
+ break;
+
+ /* remaining = target - now (in ns) */
+ target_ns = timespec64_to_ns(&target);
+ now_ns = timespec64_to_ns(&now);
+ remaining = target_ns - now_ns;
+
+ /* If remaining is small (< 1us), finish */
+ if (remaining > -1000 && remaining < 1000)
+ remaining = 0;
+ }
+
+ mutex_unlock(&priv->ptp_adj_lock);
+ return ret;
+}
+
+/* Support is not available for alarms, programmable periodic signals,
+ * pin configuration, external timestamping, programmable pins, and
+ * PPS support.
+ */
+void s2500_ptp_register(struct s2500_info *priv)
+{
+ struct ptp_clock_info *info = &priv->ptp_clock_info;
+
+ snprintf(info->name, sizeof(info->name), "%s", "S2500 PTP Clock");
+ info->max_adj = 100000000;
+ info->owner = THIS_MODULE;
+ info->adjfine = s2500_ptp_adjfine;
+ info->gettimex64 = s2500_ptp_get_time64;
+ info->settime64 = s2500_ptp_set_time64;
+ info->adjtime = s2500_ptp_adjtime;
+
+ priv->ptp_clock = ptp_clock_register(info, priv->dev);
+ if (IS_ERR(priv->ptp_clock)) {
+ dev_err(&priv->spi->dev, "Registration of %s failed",
+ info->name);
+ return;
+ }
+ dev_info(&priv->spi->dev, "Registered %s index %d", info->name,
+ ptp_clock_index(priv->ptp_clock));
+}
+
+void s2500_ptp_unregister(struct s2500_info *priv)
+{
+ if (priv->ptp_clock)
+ ptp_clock_unregister(priv->ptp_clock);
+}
+
--
2.43.0
^ permalink raw reply [flat|nested] 9+ messages in thread* Re: [PATCH net-next v2 8/9] onsemi: s2500: Add driver support for TS2500 MAC-PHY
2026-05-11 18:19 [PATCH net-next v2 8/9] onsemi: s2500: Add driver support for TS2500 MAC-PHY Selvamani Rajagopal
@ 2026-05-11 20:12 ` Andrew Lunn
2026-05-18 4:03 ` Selvamani Rajagopal
2026-05-11 20:18 ` Andrew Lunn
1 sibling, 1 reply; 9+ messages in thread
From: Andrew Lunn @ 2026-05-11 20:12 UTC (permalink / raw)
To: Selvamani Rajagopal
Cc: Piergiorgio Beruto, andrew+netdev, davem, edumazet, kuba, pabeni,
netdev, linux-kernel
> +static const char s2500_stat_strings[][ETH_GSTRING_LEN] = {
> + "tx-bytes-ok",
> + "tx-frames",
> + "tx-broadcast-frames",
> + "tx-multicast-frames",
> + "tx-64-frames",
> + "tx-65-127-frames",
> + "tx-128-255-frames",
> + "tx-256-511-frames",
> + "tx-512-1023-frames",
> + "tx-1024-1518-frames",
> + "tx-underrun-errors",
> + "tx-single-collision",
> + "tx-multiple-collision",
> + "tx-excessive-collision",
> + "tx-deferred-frames",
> + "tx-carrier-sense-errors",
> + "rx-bytes-ok",
> + "rx-frames",
> + "rx-broadcast-frames",
> + "rx-multicast-frames",
> + "rx-64-frames",
> + "rx-65-127-frames",
> + "rx-128-255-frames",
> + "rx-256-511-frames",
> + "rx-512-1023-frames",
> + "rx-1024-1518-frames",
> + "rx-runt",
> + "rx-too-long-frames",
> + "rx-crc-errors",
> + "rx-symbol-errors",
> + "rx-alignment-errors",
> + "rx-busy-drop-frames",
> + "rx-mismatch-drop-frames",
> + "ts_frames",
Looks like many of these should be returned via .get_rmon_stats
> +static void s2500_get_drvinfo(struct net_device *ndev,
> + struct ethtool_drvinfo *info)
> +{
> + strscpy(info->driver, DRV_NAME, sizeof(info->driver));
> + strscpy(info->bus_info, dev_name(&ndev->dev), sizeof(info->bus_info));
> + strscpy(info->version, DRV_VERSION, sizeof(info->version));
version is pointless because it never changes, but the kernel is
always changing. If you don't fill it, the core will return the kernel
versions.
> +static int s2500_ethtool_set_link_ksettings(struct net_device *ndev,
> + const struct ethtool_link_ksettings *cmd)
> +{
> + phy_ethtool_ksettings_set(ndev->phydev, cmd);
> + return 0;
> +}
phy_ethtool_set_link_ksettings()
> +
> +static int s2500_ethtool_get_link_ksettings(struct net_device *ndev,
> + struct ethtool_link_ksettings *cmd)
> +{
> + phy_ethtool_ksettings_get(ndev->phydev, cmd);
> + return 0;
> +}
phy_ethtool_get_link_ksettings()
> +++ b/drivers/net/ethernet/onsemi/s2500/s2500_main.c
> @@ -0,0 +1,698 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Copyright 2026 Semiconductor Components Industries, LLC ("onsemi").
> + * onsemi's S2500 10BASE-T1S MAC-PHY driver
> + */
> +
> +#include "s2500_hw_def.h"
> +
> +#include <linux/etherdevice.h>
> +#include <linux/if_ether.h>
> +#include <linux/irqchip.h>
> +#include <linux/module.h>
> +#include <linux/platform_device.h>
> +#include <linux/phy.h>
> +
> +/* S2500 functions & definitions */
> +
> +#define S2500_STATUS0_MASK (S2500_SPI_ST0_CDPE_BIT | \
> + S2500_SPI_ST0_TXFCSE_BIT | \
> + S2500_SPI_ST0_TTSCAC_BIT | \
> + S2500_SPI_ST0_TTSCAB_BIT | \
> + S2500_SPI_ST0_TTSCAA_BIT | \
> + S2500_SPI_ST0_RESETC_BIT | \
> + S2500_SPI_ST0_HDRE_BIT | \
> + S2500_SPI_ST0_LOFE_BIT | \
> + S2500_SPI_ST0_RXBOE_BIT | \
> + S2500_SPI_ST0_TXBUE_BIT | \
> + S2500_SPI_ST0_TXBOE_BIT | \
> + S2500_SPI_ST0_TXPE_BIT)
> +
> +/* Converts a MACPHY ID to a device name */
> +static inline const char *s2500_id_to_name(u32 id)
> +{
> + if (id == S2500_MODEL_ID)
> + return "S2500";
> + return "unknown";
> +}
No inline functions in .c file please.
> +static int s2500_open(struct net_device *ndev)
> +{
> + struct s2500_info *priv = netdev_priv(ndev);
> + int ret = 0;
> + u32 val;
> +
> + dev_info(&priv->spi->dev, "%s", "s2500_open");
dev_dbg() or not at all. Please don't spam the kernel log.
> +static int s2500_hwtstamp_get(struct net_device *ndev,
> + struct kernel_hwtstamp_config *cfg)
> +{
> + struct s2500_info *priv = netdev_priv(ndev);
> +
> + if (!priv->ptp_clock) {
> + cfg->tx_type = 0;
> + cfg->rx_filter = 0;
> + } else {
> + oa_tc6_hwtstamp_get(priv->tc6, cfg);
> + }
> + return 0;
What is actually specific to the s2500 here? Your aim should be to put
everything you can into the core.
> + /* Configure PTP if the model supports it */
> + if (priv->capabilities & S2500_CAP_PTP)
> + s2500_ptp_register(priv);
That should be in the core, based on FTSC.
Andrew
^ permalink raw reply [flat|nested] 9+ messages in thread* RE: [PATCH net-next v2 8/9] onsemi: s2500: Add driver support for TS2500 MAC-PHY
2026-05-11 20:12 ` Andrew Lunn
@ 2026-05-18 4:03 ` Selvamani Rajagopal
2026-05-18 12:00 ` Andrew Lunn
0 siblings, 1 reply; 9+ messages in thread
From: Selvamani Rajagopal @ 2026-05-18 4:03 UTC (permalink / raw)
To: Andrew Lunn
Cc: Piergiorgio Beruto, andrew+netdev, davem, edumazet, kuba, pabeni,
netdev, linux-kernel
> > +static int s2500_hwtstamp_get(struct net_device *ndev,
> > + struct kernel_hwtstamp_config *cfg)
> > +{
> > + struct s2500_info *priv = netdev_priv(ndev);
> > +
> > + if (!priv->ptp_clock) {
> > + cfg->tx_type = 0;
> > + cfg->rx_filter = 0;
> > + } else {
> > + oa_tc6_hwtstamp_get(priv->tc6, cfg);
> > + }
> > + return 0;
>
> What is actually specific to the s2500 here? Your aim should be to put
> everything you can into the core.
I agree on this and the next comment as well. Nothing really vendor specific.
Question is,
1) Should we add the PTP code in oa_tc6.c? This PTP code add 200+ lines. My preference is to add a new file oa_tc6_ptp.c under the directory where oa_tc6.c resides.
2) If we are adding a new file, should it be under same CONFIG_OA_TC6 or add a new CONFIG_OA_TC6_HW_TIMESTAMP or something similar?
Let me know.
>
> > + /* Configure PTP if the model supports it */
> > + if (priv->capabilities & S2500_CAP_PTP)
> > + s2500_ptp_register(priv);
>
> That should be in the core, based on FTSC.
>
> Andrew
^ permalink raw reply [flat|nested] 9+ messages in thread* Re: [PATCH net-next v2 8/9] onsemi: s2500: Add driver support for TS2500 MAC-PHY
2026-05-18 4:03 ` Selvamani Rajagopal
@ 2026-05-18 12:00 ` Andrew Lunn
2026-05-21 17:51 ` Selvamani Rajagopal
0 siblings, 1 reply; 9+ messages in thread
From: Andrew Lunn @ 2026-05-18 12:00 UTC (permalink / raw)
To: Selvamani Rajagopal
Cc: Piergiorgio Beruto, andrew+netdev, davem, edumazet, kuba, pabeni,
netdev, linux-kernel
On Mon, May 18, 2026 at 04:03:38AM +0000, Selvamani Rajagopal wrote:
>
>
>
> > > +static int s2500_hwtstamp_get(struct net_device *ndev,
> > > + struct kernel_hwtstamp_config *cfg)
> > > +{
> > > + struct s2500_info *priv = netdev_priv(ndev);
> > > +
> > > + if (!priv->ptp_clock) {
> > > + cfg->tx_type = 0;
> > > + cfg->rx_filter = 0;
> > > + } else {
> > > + oa_tc6_hwtstamp_get(priv->tc6, cfg);
> > > + }
> > > + return 0;
> >
> > What is actually specific to the s2500 here? Your aim should be to put
> > everything you can into the core.
>
> I agree on this and the next comment as well. Nothing really vendor specific.
>
> Question is,
> 1) Should we add the PTP code in oa_tc6.c? This PTP code add 200+ lines. My preference is to add a new file oa_tc6_ptp.c under the directory where oa_tc6.c resides.
oa_tc6_ptp.c is good. Maybe create a directory oa_tc6 and move the
code into there. Especially if you need to add a local header which is
shared by oa_tc6.c and oa_tc6_ptp.c, but is not needed by the drivers.
> 2) If we are adding a new file, should it be under same CONFIG_OA_TC6 or add a new CONFIG_OA_TC6_HW_TIMESTAMP or something similar?
I would not bother with a new symbol. It adds build testing complexity
having to build with and without it, to test the stubs are correct
etc.
Andrew
^ permalink raw reply [flat|nested] 9+ messages in thread* RE: [PATCH net-next v2 8/9] onsemi: s2500: Add driver support for TS2500 MAC-PHY
2026-05-18 12:00 ` Andrew Lunn
@ 2026-05-21 17:51 ` Selvamani Rajagopal
2026-05-21 18:52 ` Andrew Lunn
0 siblings, 1 reply; 9+ messages in thread
From: Selvamani Rajagopal @ 2026-05-21 17:51 UTC (permalink / raw)
To: Andrew Lunn
Cc: Piergiorgio Beruto, andrew+netdev, davem, edumazet, kuba, pabeni,
netdev, linux-kernel
> > >
> > > What is actually specific to the s2500 here? Your aim should be to put
> > > everything you can into the core.
> >
> > I agree on this and the next comment as well. Nothing really vendor specific.
> >
> > Question is,
> > 1) Should we add the PTP code in oa_tc6.c? This PTP code add 200+ lines. My
> preference is to add a new file oa_tc6_ptp.c under the directory where oa_tc6.c resides.
>
> oa_tc6_ptp.c is good. Maybe create a directory oa_tc6 and move the
> code into there. Especially if you need to add a local header which is
> shared by oa_tc6.c and oa_tc6_ptp.c, but is not needed by the drivers.
Andrew,
Little correction to what I said. As timestamp registers are in vendor specific MMS, read/write/adjust time callbacks have to be in vendor's code. I am putting register/unregister of ptp clock to oa_tc6_ptp.c. I will also move all timestamp related additions from oa_tc6.c to oa_tc6_ptp.c. This will reduce the changes to oa_tc6.c significantly.
This is besides moving oa_tc6.c to its own directory (drivers/net/ethernet/oa_tc6) as you suggested. We will have a newer, local header file to accommodate shared definitions within OA TC6 framework code.
>
> > 2) If we are adding a new file, should it be under same CONFIG_OA_TC6 or add a new
> CONFIG_OA_TC6_HW_TIMESTAMP or something similar?
>
> I would not bother with a new symbol. It adds build testing complexity
> having to build with and without it, to test the stubs are correct
> etc.
>
> Andrew
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH net-next v2 8/9] onsemi: s2500: Add driver support for TS2500 MAC-PHY
2026-05-21 17:51 ` Selvamani Rajagopal
@ 2026-05-21 18:52 ` Andrew Lunn
2026-05-21 21:01 ` Selvamani Rajagopal
0 siblings, 1 reply; 9+ messages in thread
From: Andrew Lunn @ 2026-05-21 18:52 UTC (permalink / raw)
To: Selvamani Rajagopal
Cc: Piergiorgio Beruto, andrew+netdev, davem, edumazet, kuba, pabeni,
netdev, linux-kernel
> Little correction to what I said. As timestamp registers are in
> vendor specific MMS, read/write/adjust time callbacks have to be in
> vendor's code.
I've not read that part of the specification, so maybe i'm asking a
dumb question:
Why are these registers in the vendor specific MMS? Is the
specification missing something?
Andrew
^ permalink raw reply [flat|nested] 9+ messages in thread
* RE: [PATCH net-next v2 8/9] onsemi: s2500: Add driver support for TS2500 MAC-PHY
2026-05-21 18:52 ` Andrew Lunn
@ 2026-05-21 21:01 ` Selvamani Rajagopal
0 siblings, 0 replies; 9+ messages in thread
From: Selvamani Rajagopal @ 2026-05-21 21:01 UTC (permalink / raw)
To: Andrew Lunn
Cc: Piergiorgio Beruto, andrew+netdev, davem, edumazet, kuba, pabeni,
netdev, linux-kernel
>
> This Message Is From an External Sender
> This message came from outside your organization.
>
> > Little correction to what I said. As timestamp registers are in
> > vendor specific MMS, read/write/adjust time callbacks have to be in
> > vendor's code.
>
> I've not read that part of the specification, so maybe i'm asking a
> dumb question:
>
> Why are these registers in the vendor specific MMS? Is the
> specification missing something?
My wrong use of terms may be the cause for the confusion. Sorry about that. There are two kinds of registers when it comes to hardware timestamping. The registers that have the timestamp capture related registers. These are in MMS0 and therefore timestamping handling code would be in the core code.
But the registers that control the precise clock and compensate them for accuracy are in MDIO_MMD_VEND1, (MMS12 for onsemi). These registers needed for PTP callbacks (like adjfine, settime64 and gettimex64). This part would have to reside in vendor's code.
Hope it is clear now.
>
> Andrew
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH net-next v2 8/9] onsemi: s2500: Add driver support for TS2500 MAC-PHY
2026-05-11 18:19 [PATCH net-next v2 8/9] onsemi: s2500: Add driver support for TS2500 MAC-PHY Selvamani Rajagopal
2026-05-11 20:12 ` Andrew Lunn
@ 2026-05-11 20:18 ` Andrew Lunn
2026-05-11 20:31 ` Selvamani Rajagopal
1 sibling, 1 reply; 9+ messages in thread
From: Andrew Lunn @ 2026-05-11 20:18 UTC (permalink / raw)
To: Selvamani Rajagopal
Cc: Piergiorgio Beruto, andrew+netdev, davem, edumazet, kuba, pabeni,
netdev, linux-kernel
> +static u32 phy_addr_list[S2500_NUM_REGS] = {
> + (0x4 << 16) | 0x8000,
> + (0xC << 16) | 0x10,
Is 4 OA_TC6_PHY_C45_VS_PLCA_MMS4?
0xC is your vendor 1?
These should probably be moved somewhere public:
#define OA_TC6_PHY_C45_PCS_MMS2 2 /* MMD 3 */
#define OA_TC6_PHY_C45_PMA_PMD_MMS3 3 /* MMD 1 */
#define OA_TC6_PHY_C45_VS_PLCA_MMS4 4 /* MMD 31 */
#define OA_TC6_PHY_C45_AUTO_NEG_MMS5 5 /* MMD 7 */
#define OA_TC6_PHY_C45_POWER_UNIT_MMS6 6 /* MMD 13 */
and think about how you can represent 12 using a #define.
Andrew
^ permalink raw reply [flat|nested] 9+ messages in thread* RE: [PATCH net-next v2 8/9] onsemi: s2500: Add driver support for TS2500 MAC-PHY
2026-05-11 20:18 ` Andrew Lunn
@ 2026-05-11 20:31 ` Selvamani Rajagopal
0 siblings, 0 replies; 9+ messages in thread
From: Selvamani Rajagopal @ 2026-05-11 20:31 UTC (permalink / raw)
To: Andrew Lunn
Cc: Piergiorgio Beruto, andrew+netdev, davem, edumazet, kuba, pabeni,
netdev, linux-kernel
> This Message Is From an External Sender
> This message came from outside your organization.
>
> > +static u32 phy_addr_list[S2500_NUM_REGS] = {
> > + (0x4 << 16) | 0x8000,
>
> > + (0xC << 16) | 0x10,
>
> Is 4 OA_TC6_PHY_C45_VS_PLCA_MMS4?
>
> 0xC is your vendor 1?
Yes. VEND1 is 12 (0xC). I will use label instead of numbers
>
> These should probably be moved somewhere public:
In v1 patch, I had moved to include/linux/oa_tc6.h, I should have kept that way. Will do
>
> #define OA_TC6_PHY_C45_PCS_MMS2 2 /* MMD 3 */
> #define OA_TC6_PHY_C45_PMA_PMD_MMS3 3 /* MMD 1 */
> #define OA_TC6_PHY_C45_VS_PLCA_MMS4 4 /* MMD 31 */
> #define OA_TC6_PHY_C45_AUTO_NEG_MMS5 5 /* MMD 7 */
> #define OA_TC6_PHY_C45_POWER_UNIT_MMS6 6 /* MMD 13 */
>
> and think about how you can represent 12 using a #define.
>
> Andrew
^ permalink raw reply [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-05-21 21:01 UTC | newest]
Thread overview: 9+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-05-11 18:19 [PATCH net-next v2 8/9] onsemi: s2500: Add driver support for TS2500 MAC-PHY Selvamani Rajagopal
2026-05-11 20:12 ` Andrew Lunn
2026-05-18 4:03 ` Selvamani Rajagopal
2026-05-18 12:00 ` Andrew Lunn
2026-05-21 17:51 ` Selvamani Rajagopal
2026-05-21 18:52 ` Andrew Lunn
2026-05-21 21:01 ` Selvamani Rajagopal
2026-05-11 20:18 ` Andrew Lunn
2026-05-11 20:31 ` Selvamani Rajagopal
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®