Netdev List
 help / color / mirror / Atom feed
* [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; 4+ messages in thread
From: Selvamani Rajagopal @ 2026-05-11 18:19 UTC (permalink / raw)
  To: Piergiorgio Beruto, andrew+netdev@lunn.ch, davem@davemloft.net,
	edumazet@google.com, kuba@kernel.org, pabeni@redhat.com,
	netdev@vger.kernel.org, linux-kernel@vger.kernel.org

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 related	[flat|nested] 4+ 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
  1 sibling, 0 replies; 4+ messages in thread
From: Andrew Lunn @ 2026-05-11 20:12 UTC (permalink / raw)
  To: Selvamani Rajagopal
  Cc: Piergiorgio Beruto, andrew+netdev@lunn.ch, davem@davemloft.net,
	edumazet@google.com, kuba@kernel.org, pabeni@redhat.com,
	netdev@vger.kernel.org, linux-kernel@vger.kernel.org

> +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] 4+ 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; 4+ messages in thread
From: Andrew Lunn @ 2026-05-11 20:18 UTC (permalink / raw)
  To: Selvamani Rajagopal
  Cc: Piergiorgio Beruto, andrew+netdev@lunn.ch, davem@davemloft.net,
	edumazet@google.com, kuba@kernel.org, pabeni@redhat.com,
	netdev@vger.kernel.org, linux-kernel@vger.kernel.org

> +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] 4+ 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; 4+ messages in thread
From: Selvamani Rajagopal @ 2026-05-11 20:31 UTC (permalink / raw)
  To: Andrew Lunn
  Cc: Piergiorgio Beruto, andrew+netdev@lunn.ch, davem@davemloft.net,
	edumazet@google.com, kuba@kernel.org, pabeni@redhat.com,
	netdev@vger.kernel.org, linux-kernel@vger.kernel.org

> 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] 4+ messages in thread

end of thread, other threads:[~2026-05-11 20:32 UTC | newest]

Thread overview: 4+ 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-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