* [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