Netdev List
 help / color / mirror / Atom feed
* [PATCH v2 net-next 0/7] net: dsa: netc: add PTP support for NETC switch
@ 2026-08-08  3:21 wei.fang
  2026-08-08  3:21 ` [PATCH] net: dsa: netc: add PTP one-step timestamping support wei.fang
                   ` (7 more replies)
  0 siblings, 8 replies; 14+ messages in thread
From: wei.fang @ 2026-08-08  3:21 UTC (permalink / raw)
  To: xiaoning.wang, andrew, olteanv, andrew+netdev, davem, edumazet,
	kuba, pabeni, horms, richardcochran
  Cc: wei.fang, imx, netdev, linux-kernel, linuxppc-dev,
	linux-arm-kernel

From: Wei Fang <wei.fang@nxp.com>

This series adds PTP hardware timestamping support to the NETC switch
DSA driver. The NETC switch has no time registers of its own; it shares
the PTP time base of the NETC Timer, which is a separate PCIe function
driven by the ptp_netc timer driver. The series therefore first prepares
the timer driver to expose the current PTP time to other drivers, then
builds RX, two-step TX and one-step TX timestamping on top of it in the
switch driver and its DSA tagger.

The series is organized in two parts.

Preparation of the NETC Timer driver (patches 1-3):

  1) Convert the open-coded 64-bit register accesses to the
     ioread64_lo_hi()/iowrite64_lo_hi() helpers, wrapped in
     netc_timer_rd64()/netc_timer_wr64(), to reduce boilerplate and make
     the access width explicit. No functional change.

  2) Drop the pcie_flr() call in probe. Per the reference manual, function
     level reset does not apply to the Timer as a supporting function, so
     the call has no effect.

  3) Export netc_timer_get_current_time() so the switch driver, a separate
     PCIe function, can read the shared PTP time. The helper is declared
     in <linux/fsl/netc_global.h> under CONFIG_PTP_NETC_V4_TIMER with a
     stub returning 0 when the timer is disabled, so callers need no hard
     dependency on the timer driver, and returns 0 when the timer has not
     probed or is gone so the caller can handle it gracefully.

NETC switch PTP support (patches 4-7):

  4) Track the host flood rule by entry ID instead of by ipft_entry_data
     pointer, freeing the descriptor as soon as the hardware entry is
     committed. This removes a long-lived heap allocation and lets
     netc_free_host_flood_rules() go away, in preparation for the
     timestamping rules added next.

  5) Enable the ingress port filtering lookup (IPFT) by default. A frame
     that matches no entry is simply passed on, so leaving the lookup
     always enabled simplifies the logic and avoids tracking whether a
     port already has an IPFT entry, which the RX timestamping rules rely
     on.

  6) Add two-step TX timestamping and RX timestamping. RX installs IPFT
     rules that redirect PTP frames (L2, L4 over IPv4/IPv6, event and
     general) to the CPU port; the hardware prepends a To_Host tag with
     the 64-bit ingress timestamp, which the tagger hands to
     netc_port_rxtstamp(). Two-step TX clones the skb, allocates a 4-bit
     request ID carried in a To_Port subtype 2 tag, and completes the
     clone when the hardware echoes the ID and transmit timestamp back in
     a To_Host response frame.

  7) Add one-step TX timestamping for PTP Sync frames. The MAC updates the
     correction field in flight using the per-port PM_SINGLE_STEP register,
     which is a single register that must be programmed per frame, so
     one-step Sync transmission is serialized per port with a per-port PTP
     spinlock guarding an in-flight slot and a deferral queue.

---
v2:
1. Change IS_ENABLED to IS_REACHABLE in netc_global.h
2. Move spin_lock_init() before pci_set_drvdata() in
   netc_timer_pci_probe()
3. Add device_lock() in netc_timer_get_current_time() to avoid
   use-after-free
4. Remove SOF_TIMESTAMPING_*_SOFTWARE flags
5. Use READ_ONCE/WRITE_ONCE to access np->ptp_tx_type
6. Clear NETC_SKB_CB(skb)->tstamp in netc_rcv()
7. Set np->ptp_tx_type at the end of netc_port_hwtstamp_set()
8. netc_port_txtstamp_twostep() return type changed from int from void
9. Add netc_port_purge_txtstamp_queue, which is called in
   netc_port_hwtstamp_set() when HWTSTAMP_TX_OFF is set
10. Remove clone from struct netc_skb_cb
11. Get ts_req_id from skb->cb (since clone is removed from netc_skb_cb)
    in netc_fill_tp_tag_subtype2()
12. Change the return type of netc_rx_tstamp_process() to int and add
    pskb_may_pull() to check whether the Ethertype header is in the
    linear buffer of the skb
13. netc_get_phc_index() return -1 instead of -ENODEV when priv->tmr_dev
    is NULL
14. Refactor the entire patch 7, introduce struct netc_onestep and
    related interfaces, such as netc_onestep_get/put/release and so on
v1 link: https://lore.kernel.org/imx/20260728104548.3301214-1-wei.fang@oss.nxp.com/
---

Wei Fang (7):
  ptp: netc: use ioread64_lo_hi/iowrite64_lo_hi for 64-bit register
    access
  ptp: netc: remove unnecessary pcie_flr() call in probe
  ptp: netc: export netc_timer_get_current_time() for cross-driver use
  net: dsa: netc: use entry ID instead of pointer to track host flood
    rule
  net: dsa: netc: enable ingress port filtering lookup by default
  net: dsa: netc: add PTP two-step timestamping support
  net: dsa: netc: add PTP one-step timestamping support

 drivers/net/dsa/netc/Kconfig          |   1 +
 drivers/net/dsa/netc/Makefile         |   3 +-
 drivers/net/dsa/netc/netc_main.c      | 183 ++++--
 drivers/net/dsa/netc/netc_platform.c  |   1 +
 drivers/net/dsa/netc/netc_ptp.c       | 866 ++++++++++++++++++++++++++
 drivers/net/dsa/netc/netc_switch.h    |  91 ++-
 drivers/net/dsa/netc/netc_switch_hw.h |   5 +
 drivers/ptp/ptp_netc.c                | 110 ++--
 include/linux/dsa/tag_netc.h          |  43 ++
 include/linux/fsl/netc_global.h       |  10 +
 net/dsa/tag_netc.c                    | 214 ++++++-
 11 files changed, 1428 insertions(+), 99 deletions(-)
 create mode 100644 drivers/net/dsa/netc/netc_ptp.c

-- 
2.34.1


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [PATCH] net: dsa: netc: add PTP one-step timestamping support
  2026-08-08  3:21 [PATCH v2 net-next 0/7] net: dsa: netc: add PTP support for NETC switch wei.fang
@ 2026-08-08  3:21 ` wei.fang
  2026-08-08  3:26   ` Wei Fang
  2026-08-08  3:21 ` [PATCH v2 net-next 1/7] ptp: netc: use ioread64_lo_hi/iowrite64_lo_hi for 64-bit register access wei.fang
                   ` (6 subsequent siblings)
  7 siblings, 1 reply; 14+ messages in thread
From: wei.fang @ 2026-08-08  3:21 UTC (permalink / raw)
  To: xiaoning.wang, andrew, olteanv, andrew+netdev, davem, edumazet,
	kuba, pabeni, horms, richardcochran
  Cc: wei.fang, imx, netdev, linux-kernel, linuxppc-dev,
	linux-arm-kernel

From: Wei Fang <wei.fang@nxp.com>

The NETC switch supports one-step TX timestamping for PTP Sync frames.
The MAC captures the SFD transmit time, adds the residence time to the
correction field at the offset given by PM_SINGLE_STEP[OFFSET], and
writes the result back before the frame leaves the wire. The software
timestamp (low 30 bits of the PTP Timer value) is carried in the
To_Port SubType 1 tag.

PM_SINGLE_STEP is a per-port register that can describe only one
in-flight frame at a time, and programming it requires reading the
current PTP time, which may sleep. Both constraints rule out handling
one-step Sync on the xmit path.

Instead, defer transmission to a per-port process-context work. The
xmit path classifies the frame in netc_port_txtstamp(): a genuine
one-step Sync (twoStepFlag cleared) has its PTP header offsets cached
in the skb control block; frames that cannot be handled as one-step
fall back to the two-step path or are sent as normal frames.
netc_xmit() hands the classified frame to the switch driver via the
onestep_sync_enqueue tagger callback, which queues it and kicks the
work if no frame is currently in flight.

The work dequeues one frame at a time, reads a fresh PTP time,
programs PM_SINGLE_STEP, updates the originTimestamp field, and
transmits the frame directly to the conduit via the onestep_sync_xmit
tagger callback, bypassing dsa_user_xmit() to avoid double-counting
TX stats. Only one frame is in flight at a time: the frame carries a
TX-completion destructor that reschedules the work when the conduit
frees the skb, keeping PM_SINGLE_STEP always matched to the frame
being transmitted.

The one-step context is reference-counted and its lifetime is decoupled
from the devm-allocated netc_port. In-flight skbs hold a reference via
their destructor, so the context outlives port disable until the conduit
frees the last in-flight skb. Port disable clears @active and purges the
queue under work_lock; a work that runs afterwards observes @active
cleared and returns without touching the freed port resources.

Assisted-by: Wchat:claude-opus-4-8
Signed-off-by: Wei Fang <wei.fang@nxp.com>
---
 drivers/net/dsa/netc/netc_main.c      |  61 +++-
 drivers/net/dsa/netc/netc_ptp.c       | 410 +++++++++++++++++++++++++-
 drivers/net/dsa/netc/netc_switch.h    |  48 +++
 drivers/net/dsa/netc/netc_switch_hw.h |   5 +
 include/linux/dsa/tag_netc.h          |  21 ++
 net/dsa/tag_netc.c                    |  68 +++++
 6 files changed, 605 insertions(+), 8 deletions(-)

diff --git a/drivers/net/dsa/netc/netc_main.c b/drivers/net/dsa/netc/netc_main.c
index 4e139ffc2f76..967f20a94d99 100644
--- a/drivers/net/dsa/netc/netc_main.c
+++ b/drivers/net/dsa/netc/netc_main.c
@@ -74,6 +74,7 @@ static int netc_connect_tag_protocol(struct dsa_switch *ds,
 		return -EPROTONOSUPPORT;
 
 	tagger_data = ds->tagger_data;
+	tagger_data->onestep_sync_enqueue = netc_port_onestep_sync_enqueue;
 	tagger_data->twostep_tstamp_handler = netc_port_twostep_tstamp_handler;
 
 	return 0;
@@ -94,7 +95,7 @@ static void netc_port_rmw(struct netc_port *np, u32 reg,
 	netc_port_wr(np, reg, new);
 }
 
-static void netc_mac_port_wr(struct netc_port *np, u32 reg, u32 val)
+void netc_mac_port_wr(struct netc_port *np, u32 reg, u32 val)
 {
 	if (is_netc_pseudo_port(np))
 		return;
@@ -252,6 +253,21 @@ static void netc_get_switch_capabilities(struct netc_switch *priv)
 	priv->num_bp = FIELD_GET(BPCAPR_NUM_BP, val);
 }
 
+static void netc_free_user_ports(struct netc_switch *priv)
+{
+	struct dsa_switch *ds = priv->ds;
+	struct dsa_port *dp;
+
+	dsa_switch_for_each_user_port(dp, ds) {
+		struct netc_port *np = NETC_PORT(ds, dp->index);
+
+		if (!np->onestep) {
+			netc_onestep_put(np->onestep);
+			np->onestep = NULL;
+		}
+	}
+}
+
 static int netc_init_all_ports(struct netc_switch *priv)
 {
 	struct device *dev = priv->dev;
@@ -292,13 +308,13 @@ static int netc_init_all_ports(struct netc_switch *priv)
 
 		err = netc_port_get_info_from_dt(np, dp->dn, dev);
 		if (err)
-			return err;
+			goto free_user_ports;
 
 		if (dsa_port_is_user(dp)) {
 			err = netc_port_create_mdio_bus(np, dp->dn);
 			if (err) {
 				dev_err(dev, "Failed to create MDIO bus\n");
-				return err;
+				goto free_user_ports;
 			}
 
 			/* The ipft_hf_eid is initialized to an invalid entry
@@ -314,11 +330,16 @@ static int netc_init_all_ports(struct netc_switch *priv)
 			 */
 			err = netc_port_ptp_init(np);
 			if (err)
-				return err;
+				goto free_user_ports;
 		}
 	}
 
 	return 0;
+
+free_user_ports:
+	netc_free_user_ports(priv);
+
+	return err;
 }
 
 static void netc_init_ntmp_tbl_versions(struct netc_switch *priv)
@@ -941,7 +962,7 @@ static int netc_setup(struct dsa_switch *ds)
 
 	err = netc_init_ntmp_user(priv);
 	if (err)
-		goto put_ptp_timer;
+		goto free_user_ports;
 
 	INIT_HLIST_HEAD(&priv->fdb_list);
 	mutex_init(&priv->fdbt_lock);
@@ -980,6 +1001,8 @@ static int netc_setup(struct dsa_switch *ds)
 	mutex_destroy(&priv->fdbt_lock);
 	mutex_destroy(&priv->vft_lock);
 	netc_free_ntmp_user(priv);
+free_user_ports:
+	netc_free_user_ports(priv);
 put_ptp_timer:
 	pci_dev_put(priv->tmr_dev);
 
@@ -1005,6 +1028,19 @@ static void netc_free_ports_resources(struct netc_switch *priv)
 			continue;
 
 		netc_port_purge_txtstamp_queue(np);
+
+		/* dsa_tree_teardown() calls dsa_tree_teardown_ports() before
+		 * dsa_tree_teardown_switches(), so netc_port_disable() is
+		 * executed before netc_teardown() and purges onestep->queue,
+		 * so here we only need to drop the port's owner reference.
+		 * In-flight one-step skbs still hold references via the
+		 * destructor; the context (and its work) is freed only after
+		 * the conduit frees the last in-flight skb. By then np may
+		 * be gone, but the work no longer dereferences np because
+		 * onestep->active has been cleared.
+		 */
+		netc_onestep_put(np->onestep);
+		np->onestep = NULL;
 	}
 }
 
@@ -1559,6 +1595,7 @@ static int netc_port_enable(struct dsa_switch *ds, int port,
 			    struct phy_device *phy)
 {
 	struct netc_port *np = NETC_PORT(ds, port);
+	struct netc_onestep *onestep = np->onestep;
 	int err;
 
 	if (np->enable)
@@ -1571,6 +1608,12 @@ static int netc_port_enable(struct dsa_switch *ds, int port,
 		return err;
 	}
 
+	if (onestep) {
+		mutex_lock(&onestep->work_lock);
+		onestep->active = true;
+		mutex_unlock(&onestep->work_lock);
+	}
+
 	np->enable = true;
 
 	return 0;
@@ -1579,6 +1622,7 @@ static int netc_port_enable(struct dsa_switch *ds, int port,
 static void netc_port_disable(struct dsa_switch *ds, int port)
 {
 	struct netc_port *np = NETC_PORT(ds, port);
+	struct netc_onestep *onestep = np->onestep;
 
 	/* When .port_disable() is called, .port_enable() may not have been
 	 * called. In this case, both the prepare_count and enable_count of
@@ -1588,6 +1632,13 @@ static void netc_port_disable(struct dsa_switch *ds, int port)
 	if (!np->enable)
 		return;
 
+	if (onestep) {
+		mutex_lock(&onestep->work_lock);
+		onestep->active = false;
+		netc_port_purge_onestep_queue(onestep, true);
+		mutex_unlock(&onestep->work_lock);
+	}
+
 	clk_disable_unprepare(np->ref_clk);
 	np->enable = false;
 }
diff --git a/drivers/net/dsa/netc/netc_ptp.c b/drivers/net/dsa/netc/netc_ptp.c
index 1384a6f31d1c..881b07b616ca 100644
--- a/drivers/net/dsa/netc/netc_ptp.c
+++ b/drivers/net/dsa/netc/netc_ptp.c
@@ -4,14 +4,270 @@
  * Copyright 2025-2026 NXP
  */
 
+#include <linux/kref.h>
 #include <linux/ptp_classify.h>
 #include <linux/ptp_clock_kernel.h>
+#include <linux/slab.h>
 
 #include "netc_switch.h"
 
 #define NETC_NUM_TS_REQ_ID		16
 #define NETC_TXTSTAMP_TIMEOUT		(5 * HZ)
 
+static void netc_port_set_onestep_control(struct netc_port *np,
+					  bool csum_update, int offset)
+{
+	u32 val;
+
+	val = PM_SINGLE_STEP_EN | FIELD_PREP(PM_SINGLE_STEP_OFFSET, offset);
+	if (csum_update)
+		val |= PM_SINGLE_STEP_CH;
+	netc_mac_port_wr(np, NETC_PM_SINGLE_STEP(0), val);
+}
+
+static void netc_onestep_destroy_work(struct work_struct *work)
+{
+	struct netc_onestep *onestep = container_of(work, struct netc_onestep,
+						    destroy_work);
+
+	/* refcnt reaching zero does not by itself mean onestep->work has
+	 * stopped: the last in-flight skb destructor calls schedule_work(&work)
+	 * *before* the netc_onestep_put() that drops the final reference, so at
+	 * the moment refcnt hits zero onestep->work may still be pending or
+	 * running on another CPU. destroy_work and work are distinct work_structs
+	 * and can run concurrently, so cancel_work_sync() is required to drain
+	 * onestep->work before mutex_destroy()/kfree() below, otherwise a
+	 * still-running work would touch freed memory. No new schedule_work(&work)
+	 * can occur after this point because no references remain, so this
+	 * cancel is final.
+	 */
+	cancel_work_sync(&onestep->work);
+	mutex_destroy(&onestep->work_lock);
+	kfree(onestep);
+}
+
+static void netc_onestep_release(struct kref *ref)
+{
+	struct netc_onestep *onestep = container_of(ref, struct netc_onestep,
+						    refcnt);
+
+	/* This may be called from the skb destructor in softirq context
+	 * (napi_consume_skb()), where cancel_work_sync() must not be used.
+	 * Defer the final teardown to process context.
+	 */
+	schedule_work(&onestep->destroy_work);
+}
+
+static void netc_onestep_get(struct netc_onestep *onestep)
+{
+	kref_get(&onestep->refcnt);
+}
+
+void netc_onestep_put(struct netc_onestep *onestep)
+{
+	kref_put(&onestep->refcnt, netc_onestep_release);
+}
+
+static void netc_onestep_skb_destructor(struct sk_buff *skb)
+{
+	struct netc_onestep *onestep = skb_shinfo(skb)->destructor_arg;
+
+	/* skb has been transmitted by hardware. Schedule work to send the next
+	 * queued one-step Sync packet, then release this skb's reference on the
+	 * context. If the port has already been torn down and this is the last
+	 * reference, the context is freed via netc_onestep_release().
+	 */
+	schedule_work(&onestep->work);
+	netc_onestep_put(onestep);
+}
+
+static void netc_port_program_onestep(struct netc_port *np,
+				      struct netc_onestep *onestep,
+				      struct sk_buff *skb,
+				      u64 tstamp)
+{
+	u16 correction_offset = NETC_SKB_CB(skb)->correction_offset;
+	u16 tstamp_offset = NETC_SKB_CB(skb)->timestamp_offset;
+	u8 *hdr = skb_mac_header(skb);
+	bool csum_update = false;
+	__be32 new_sec_l, new_ns;
+	__be16 new_sec_h;
+	u64 sec;
+	u32 ns;
+
+	NETC_SKB_CB(skb)->tstamp = tstamp;
+	NETC_SKB_CB(skb)->ptp_flag = NETC_PTP_FLAG_ONESTEP;
+
+	/* Update originTimestamp field of Sync packet
+	 * - 48 bits seconds field
+	 * - 32 bits nanoseconds field
+	 */
+	sec = div_u64_rem(tstamp, NSEC_PER_SEC, &ns);
+	new_sec_h = htons((sec >> 32) & 0xffff);
+	new_sec_l = htonl(sec & 0xffffffff);
+	new_ns = htonl(ns);
+
+	if (NETC_SKB_CB(skb)->is_udp) {
+		__be32 old_sec_l, old_ns;
+		struct udphdr *uh;
+		__be16 old_sec_h;
+
+		if (skb->ip_summed == CHECKSUM_PARTIAL) {
+			csum_update = true;
+			goto update_timestamp;
+		}
+
+		if (unlikely(!skb_transport_header_was_set(skb)))
+			uh = (struct udphdr *)(hdr + tstamp_offset -
+					       sizeof(struct ptp_header) -
+					       sizeof(struct udphdr));
+		else
+			uh = udp_hdr(skb);
+
+		/* For IPv4, a UDP checksum of zero on the wire means "no
+		 * checksum". For IPv6, its UDP checksum is mandatory and
+		 * never zero.
+		 */
+		if (!uh->check)
+			goto update_timestamp;
+
+		old_sec_h = __get_unaligned_t(__be16, hdr + tstamp_offset);
+		old_sec_l = __get_unaligned_t(__be32, hdr + tstamp_offset + 2);
+		old_ns = __get_unaligned_t(__be32, hdr + tstamp_offset + 6);
+		inet_proto_csum_replace2(&uh->check, skb, old_sec_h,
+					 new_sec_h, false);
+		inet_proto_csum_replace4(&uh->check, skb, old_sec_l,
+					 new_sec_l, false);
+		inet_proto_csum_replace4(&uh->check, skb, old_ns,
+					 new_ns, false);
+		csum_update = true;
+	}
+
+update_timestamp:
+	__put_unaligned_t(__be16, new_sec_h, hdr + tstamp_offset);
+	__put_unaligned_t(__be32, new_sec_l, hdr + tstamp_offset + 2);
+	__put_unaligned_t(__be32, new_ns, hdr + tstamp_offset + 6);
+
+	netc_port_set_onestep_control(np, csum_update, correction_offset);
+
+	/* Orphan the skb to release the socket send buffer quota immediately.
+	 * This is safe because sock_wfree() does not access skb->data or any
+	 * frame content. After skb_orphan(), we install our own destructor so
+	 * that when the conduit driver frees the skb after TX completion, we
+	 * get notified to send the next queued Sync packet.
+	 */
+	skb_orphan(skb);
+	netc_onestep_get(onestep); /* in-flight reference */
+	skb_shinfo(skb)->destructor_arg = onestep;
+	skb->destructor = netc_onestep_skb_destructor;
+}
+
+static u64 netc_get_phc_time(struct netc_switch *priv)
+{
+	if (unlikely(!priv->tmr_dev))
+		return 0;
+
+	return netc_timer_get_current_time(priv->tmr_dev);
+}
+
+void netc_port_onestep_work(struct work_struct *work)
+{
+	struct netc_onestep *onestep = container_of(work, struct netc_onestep,
+						    work);
+	struct netc_tagger_data *tagger_data;
+	struct netc_switch *priv;
+	struct netc_port *np;
+	struct sk_buff *skb;
+	u64 tstamp;
+
+	/* Serialize the whole hardware access against port disable. work_lock
+	 * is a mutex (this runs in process context and netc_get_phc_time() may
+	 * sleep). If the port has been disabled, bail out immediately; np and
+	 * priv are only dereferenced after the @active check passes, so they
+	 * are always valid here.
+	 */
+	mutex_lock(&onestep->work_lock);
+	if (unlikely(!onestep->active)) {
+		netc_port_purge_onestep_queue(onestep, true);
+		goto unlock_work;
+	}
+
+	/* Send only one queued Sync per run. The shared SINGLE_STEP register
+	 * must match the frame currently being transmitted, so the next frame
+	 * is programmed only after this one completes TX, when its skb
+	 * destructor reschedules this work. Dequeue under onestep->queue_lock,
+	 * and if the queue has drained, release the in-flight slot so a later
+	 * frame from the xmit path kicks the work again.
+	 */
+	spin_lock_bh(&onestep->queue_lock);
+	skb = __skb_dequeue(&onestep->queue);
+	if (!skb) {
+		onestep->in_flight = false;
+		spin_unlock_bh(&onestep->queue_lock);
+		goto unlock_work;
+	}
+	spin_unlock_bh(&onestep->queue_lock);
+
+	np = onestep->np;
+	priv = np->switch_priv;
+	tstamp = netc_get_phc_time(priv);
+	if (unlikely(!tstamp)) {
+		/* The PTP timer is not available, so there is no correct
+		 * timestamp to program. Drop this frame and re-kick to process
+		 * the remaining queued frames.
+		 *
+		 * netc_port_program_onestep() has not run for this skb yet, so
+		 * netc_onestep_skb_destructor() is not installed on it. Freeing
+		 * it therefore does not reschedule the work, so the work must be
+		 * rescheduled explicitly to keep draining the queue.
+		 */
+		dev_dbg_ratelimited(priv->dev,
+				    "Port %d PTP timer unavailable, drop Sync\n",
+				    np->dp->index);
+		kfree_skb(skb);
+		schedule_work(&onestep->work);
+		goto unlock_work;
+	}
+
+	/* Reuse the offsets cached at enqueue time; only the timestamp is
+	 * read fresh so it reflects the actual TX moment.
+	 */
+	netc_port_program_onestep(np, onestep, skb, tstamp);
+
+	/* Tag and hand the frame directly to the conduit via the tagger,
+	 * bypassing dsa_user_xmit() so the TX stats are not counted twice.
+	 * And there is no need to check if tagger_data is NULL, because
+	 * dsa_tree_teardown_ports() executes before
+	 * dsa_switch_teardown_tag_protocol(), so tagger_data cannot be
+	 * NULL when onestep->active is set.
+	 */
+	tagger_data = priv->ds->tagger_data;
+	tagger_data->onestep_sync_xmit(skb, np->dp->user);
+
+unlock_work:
+	mutex_unlock(&onestep->work_lock);
+}
+
+static int netc_port_onestep_alloc(struct netc_port *np)
+{
+	struct netc_onestep *onestep;
+
+	onestep = kzalloc_obj(*onestep);
+	if (!onestep)
+		return -ENOMEM;
+
+	kref_init(&onestep->refcnt); /* port (owner) reference */
+	np->onestep = onestep;
+	onestep->np = np;
+	mutex_init(&onestep->work_lock);
+	spin_lock_init(&onestep->queue_lock);
+	__skb_queue_head_init(&onestep->queue);
+	INIT_WORK(&onestep->work, netc_port_onestep_work);
+	INIT_WORK(&onestep->destroy_work, netc_onestep_destroy_work);
+
+	return 0;
+}
+
 int netc_port_ptp_init(struct netc_port *np)
 {
 	/* Initialize to invalid entry IDs */
@@ -21,7 +277,7 @@ int netc_port_ptp_init(struct netc_port *np)
 	spin_lock_init(&np->tstamp_lock);
 	__skb_queue_head_init(&np->skb_txtstamp_queue);
 
-	return 0;
+	return netc_port_onestep_alloc(np);
 }
 
 static int netc_get_phc_index(struct netc_switch *priv)
@@ -45,7 +301,8 @@ int netc_get_ts_info(struct dsa_switch *ds, int port,
 				 SOF_TIMESTAMPING_RX_HARDWARE |
 				 SOF_TIMESTAMPING_RAW_HARDWARE;
 
-	info->tx_types = BIT(HWTSTAMP_TX_OFF) | BIT(HWTSTAMP_TX_ON);
+	info->tx_types = BIT(HWTSTAMP_TX_OFF) | BIT(HWTSTAMP_TX_ON) |
+			 BIT(HWTSTAMP_TX_ONESTEP_SYNC);
 
 	info->rx_filters = BIT(HWTSTAMP_FILTER_NONE) |
 			   BIT(HWTSTAMP_FILTER_PTP_V2_EVENT) |
@@ -262,6 +519,22 @@ void netc_port_purge_txtstamp_queue(struct netc_port *np)
 	__skb_queue_purge(&free_list);
 }
 
+void netc_port_purge_onestep_queue(struct netc_onestep *onestep,
+				   bool clear_flight)
+{
+	struct sk_buff_head free_list;
+
+	__skb_queue_head_init(&free_list);
+
+	spin_lock_bh(&onestep->queue_lock);
+	skb_queue_splice_init(&onestep->queue, &free_list);
+	if (clear_flight)
+		onestep->in_flight = false;
+	spin_unlock_bh(&onestep->queue_lock);
+
+	__skb_queue_purge(&free_list);
+}
+
 int netc_port_hwtstamp_set(struct dsa_switch *ds, int port,
 			   struct kernel_hwtstamp_config *config,
 			   struct netlink_ext_ack *extack)
@@ -278,6 +551,7 @@ int netc_port_hwtstamp_set(struct dsa_switch *ds, int port,
 	switch (config->tx_type) {
 	case HWTSTAMP_TX_ON:
 	case HWTSTAMP_TX_OFF:
+	case HWTSTAMP_TX_ONESTEP_SYNC:
 		break;
 	default:
 		return -ERANGE;
@@ -316,6 +590,9 @@ int netc_port_hwtstamp_set(struct dsa_switch *ds, int port,
 	if (config->tx_type == HWTSTAMP_TX_OFF)
 		netc_port_purge_txtstamp_queue(np);
 
+	if (config->tx_type != HWTSTAMP_TX_ONESTEP_SYNC)
+		netc_port_purge_onestep_queue(np->onestep, false);
+
 	config->rx_filter = rx_filter;
 
 	return 0;
@@ -439,9 +716,93 @@ bool netc_port_rxtstamp(struct dsa_switch *ds, int port, struct sk_buff *skb,
 	return false;
 }
 
+static void netc_port_prepare_onestep_sync(struct netc_port *np,
+					   struct sk_buff *skb,
+					   u32 ptp_class, bool *twostep)
+{
+	struct netc_switch *priv = np->switch_priv;
+	u16 correction_offset, tstamp_offset;
+	struct ptp_header *ptp_hdr;
+	u8 msg_type, twostep_flag;
+	bool is_udp = false;
+	u32 pkt_type;
+	u8 *pkt_hdr;
+
+	if (unlikely(skb_linearize(skb)))
+		return;
+
+	ptp_hdr = ptp_parse_header(skb, ptp_class);
+	if (unlikely(!ptp_hdr)) {
+		dev_dbg_ratelimited(priv->dev,
+				    "Port %d failed to parse Sync header\n",
+				    np->dp->index);
+		return;
+	}
+
+	msg_type = ptp_get_msgtype(ptp_hdr, ptp_class);
+	twostep_flag = ptp_hdr->flag_field[0] & 0x2;
+
+	pkt_hdr = skb_mac_header(skb);
+	correction_offset = (u8 *)&ptp_hdr->correction - pkt_hdr;
+	tstamp_offset = (u8 *)ptp_hdr + sizeof(*ptp_hdr) - pkt_hdr;
+
+	/* Ensure that the entire originTimestamp field is present in the
+	 * linear buffer of the skb.
+	 */
+	if (unlikely(tstamp_offset + 10 > skb_headlen(skb))) {
+		dev_dbg_ratelimited(priv->dev,
+				    "Port %d Sync header not in linear area\n",
+				    np->dp->index);
+		return;
+	}
+
+	/* Only a Sync frame with the twoStepFlag cleared can use one-step
+	 * timestamping. A frame that requests two-step (or is not a Sync)
+	 * carries different on-wire fields, so this is a real classification;
+	 * report it through *twostep so the caller falls back to the two-step
+	 * path.
+	 */
+	if (msg_type != PTP_MSGTYPE_SYNC || twostep_flag != 0) {
+		*twostep = true;
+		return;
+	}
+
+	/* This is a genuine one-step Sync frame. skb_shinfo()->destructor_arg
+	 * is later used to pass the np->onestep pointer to
+	 * netc_onestep_skb_destructor() for TX completion notification.
+	 * MSG_ZEROCOPY also uses destructor_arg (via skb_zcopy_init()) to
+	 * track user-space page references. Overwriting it in that case would
+	 * leak the ubuf_info reference and prevent user pages from being
+	 * released. PTP applications do not use MSG_ZEROCOPY, but guard
+	 * against it defensively.
+	 */
+	if (skb_zcopy(skb)) {
+		dev_dbg_ratelimited(priv->dev,
+				    "Port %d one-step Sync not supported on zerocopy skb\n",
+				    np->dp->index);
+		return;
+	}
+
+	pkt_type = ptp_class & PTP_CLASS_PMASK;
+	if (pkt_type == PTP_CLASS_IPV4 || pkt_type == PTP_CLASS_IPV6)
+		is_udp = true;
+
+	/* Cache the parsing results so the tagger xmit path and the deferred
+	 * work do not need to re-parse the PTP header, and so that
+	 * netc_port_program_onestep() can derive these parameters from the
+	 * skb.
+	 */
+	NETC_SKB_CB(skb)->correction_offset = correction_offset;
+	NETC_SKB_CB(skb)->timestamp_offset = tstamp_offset;
+	NETC_SKB_CB(skb)->is_udp = is_udp;
+	NETC_SKB_CB(skb)->ptp_flag = NETC_PTP_FLAG_ONESTEP;
+}
+
 void netc_port_txtstamp(struct dsa_switch *ds, int port, struct sk_buff *skb)
 {
 	struct netc_port *np = NETC_PORT(ds, port);
+	int tx_type = READ_ONCE(np->ptp_tx_type);
+	bool twostep = false;
 	u32 ptp_class;
 
 	NETC_SKB_CB(skb)->ptp_flag = 0;
@@ -449,6 +810,49 @@ void netc_port_txtstamp(struct dsa_switch *ds, int port, struct sk_buff *skb)
 	if (ptp_class == PTP_CLASS_NONE)
 		return;
 
-	if (READ_ONCE(np->ptp_tx_type) == HWTSTAMP_TX_ON)
+	if (tx_type == HWTSTAMP_TX_ONESTEP_SYNC)
+		netc_port_prepare_onestep_sync(np, skb, ptp_class, &twostep);
+
+	if (tx_type == HWTSTAMP_TX_ON || twostep)
 		netc_port_txtstamp_twostep(np, skb);
 }
+
+void netc_port_onestep_sync_enqueue(struct dsa_switch *ds, int port,
+				    struct sk_buff *skb)
+{
+	struct netc_port *np = NETC_PORT(ds, port);
+	struct netc_onestep *onestep = np->onestep;
+	bool kick = false;
+
+	/* This runs in the xmit path (softirq / BH-disabled), so it must not
+	 * sleep: only queue the frame here and let netc_port_onestep_work()
+	 * program the SINGLE_STEP register and transmit it from process
+	 * context. The shared SINGLE_STEP register can describe only one frame
+	 * at a time, so at most one one-step Sync may be in flight. Track that
+	 * with @in_flight under onestep->queue_lock.
+	 *
+	 * Enqueue the frame and, only if no frame is currently in flight, claim
+	 * the in-flight slot and kick the work. When a frame is already in
+	 * flight, just queue: its skb destructor will kick the work to send the
+	 * next one once it completes TX, so the frames are transmitted strictly
+	 * one at a time in order.
+	 *
+	 * PTP Sync frames are periodic, low-rate control-plane frames and only
+	 * reach this TX path when the local socket requested hardware TX
+	 * timestamping on a one-step port, so the queue cannot be flooded and
+	 * needs no depth cap.
+	 */
+	spin_lock_bh(&onestep->queue_lock);
+	__skb_queue_tail(&onestep->queue, skb);
+	if (!onestep->in_flight) {
+		onestep->in_flight = true;
+		kick = true;
+	}
+	spin_unlock_bh(&onestep->queue_lock);
+
+	/* Ownership is transferred to the queue; netc_xmit() stops processing
+	 * this skb. The work will program and transmit it.
+	 */
+	if (kick)
+		schedule_work(&onestep->work);
+}
diff --git a/drivers/net/dsa/netc/netc_switch.h b/drivers/net/dsa/netc/netc_switch.h
index 86fd15889733..98d4842441df 100644
--- a/drivers/net/dsa/netc/netc_switch.h
+++ b/drivers/net/dsa/netc/netc_switch.h
@@ -9,6 +9,7 @@
 #include <linux/dsa/tag_netc.h>
 #include <linux/fsl/netc_global.h>
 #include <linux/fsl/ntmp.h>
+#include <linux/mutex.h>
 #include <linux/of_device.h>
 #include <linux/of_net.h>
 #include <linux/pci.h>
@@ -87,6 +88,44 @@ enum netc_host_reason {
 	NETC_HR_PTP_TRAP   = 9,
 };
 
+/* One-step Sync serialization context.
+ *
+ * Its lifetime is decoupled from the devm-allocated netc_port. An in-flight
+ * one-step Sync skb keeps a reference on this context via its skb destructor,
+ * so the context outlives the port teardown until the conduit frees the last
+ * in-flight skb after TX completion. Once the port is disabled, @active is
+ * cleared and the work stops touching any devm memory (netc_port/netc_switch)
+ * or the unregistered user netdev or the tagger_data.
+ */
+struct netc_onestep {
+	struct netc_port *np;
+	struct kref refcnt;
+	/* Process-context lock: serializes the deferred TX work against port
+	 * teardown, so the work never touches the devm-allocated netc_port /
+	 * netc_switch or the unregistered user netdev after teardown. Held
+	 * across netc_get_phc_time(), which may sleep, hence a mutex.
+	 */
+	struct mutex work_lock;
+	/* Serialize access to in_flight and queue */
+	spinlock_t queue_lock;
+	bool active;	/* set when port is enabled, under @work_lock */
+	/* In-flight slot: true while one one-step Sync frame is programmed
+	 * into the shared SINGLE_STEP register and being transmitted. Only one
+	 * frame may be in flight at a time, so the next queued frame is sent
+	 * only after the current one completes TX (its skb destructor kicks
+	 * the work). Accessed under queue_lock, from both the softirq xmit
+	 * path and the process-context work.
+	 */
+	bool in_flight;
+	/* Pending one-step Sync frames. Enqueued from the softirq xmit path and
+	 * dequeued by the process-context work; the list is serialized by
+	 * queue_lock together with @in_flight.
+	 */
+	struct sk_buff_head queue;
+	struct work_struct work;	/* drains @queue */
+	struct work_struct destroy_work; /* frees the context in process ctx */
+};
+
 struct netc_port {
 	void __iomem *iobase;
 	struct netc_switch *switch_priv;
@@ -106,6 +145,8 @@ struct netc_port {
 	spinlock_t tstamp_lock;
 	/* skb queue for two-step timestamp frames */
 	struct sk_buff_head skb_txtstamp_queue;
+	/* one-step Sync serialization context (ref-counted, kzalloc'd) */
+	struct netc_onestep *onestep;
 	int ptp_tx_type;
 	int ptp_rx_filter;
 	u32 ptp_ipft_eid[NETC_PTP_MAX];
@@ -212,6 +253,7 @@ static inline void netc_del_vlan_entry(struct netc_vlan_entry *entry)
 }
 
 int netc_switch_platform_probe(struct netc_switch *priv);
+void netc_mac_port_wr(struct netc_port *np, u32 reg, u32 val);
 
 /* ethtool APIs */
 void netc_port_get_pause_stats(struct dsa_switch *ds, int port,
@@ -243,5 +285,11 @@ void netc_port_twostep_tstamp_handler(struct dsa_switch *ds, int port,
 bool netc_port_rxtstamp(struct dsa_switch *ds, int port, struct sk_buff *skb,
 			unsigned int type);
 void netc_port_txtstamp(struct dsa_switch *ds, int port, struct sk_buff *skb);
+void netc_onestep_put(struct netc_onestep *onestep);
+void netc_port_purge_onestep_queue(struct netc_onestep *onestep,
+				   bool clear_flight);
+void netc_port_onestep_work(struct work_struct *work);
+void netc_port_onestep_sync_enqueue(struct dsa_switch *ds, int port,
+				    struct sk_buff *skb);
 
 #endif
diff --git a/drivers/net/dsa/netc/netc_switch_hw.h b/drivers/net/dsa/netc/netc_switch_hw.h
index 1404ae41c7bc..37d1dd7ec2c7 100644
--- a/drivers/net/dsa/netc/netc_switch_hw.h
+++ b/drivers/net/dsa/netc/netc_switch_hw.h
@@ -203,6 +203,11 @@ enum netc_stg_stage {
 #define   SSP_10M			1
 #define   SSP_1G			2
 
+#define NETC_PM_SINGLE_STEP(a)		(0x10c0 + (a) * 0x400)
+#define  PM_SINGLE_STEP_CH		BIT(6)
+#define  PM_SINGLE_STEP_OFFSET		GENMASK(15, 7)
+#define  PM_SINGLE_STEP_EN		BIT(31)
+
 /* Port MAC 0/1 Receive Ethernet Octets Counter */
 #define NETC_PM_REOCT(a)		(0x1100 + (a) * 0x400)
 
diff --git a/include/linux/dsa/tag_netc.h b/include/linux/dsa/tag_netc.h
index da200e3ba8ad..8aeb865cecab 100644
--- a/include/linux/dsa/tag_netc.h
+++ b/include/linux/dsa/tag_netc.h
@@ -10,6 +10,7 @@
 #include <net/dsa.h>
 
 #define NETC_TAG_MAX_LEN			14
+#define NETC_PTP_FLAG_ONESTEP			BIT(0)
 #define NETC_PTP_FLAG_TWOSTEP			BIT(1)
 
 struct netc_skb_cb {
@@ -17,6 +18,14 @@ struct netc_skb_cb {
 	u64 tstamp;
 	u8 ptp_flag;
 	u8 ts_req_id;
+	/* One-step Sync parsing results, computed in netc_port_txtstamp()
+	 * and reused in the tagger xmit path and the deferred work, to avoid
+	 * re-parsing the PTP header. Valid only while
+	 * ptp_flag == NETC_PTP_FLAG_ONESTEP.
+	 */
+	u16 correction_offset;
+	u16 timestamp_offset;
+	bool is_udp;
 };
 
 #define NETC_SKB_CB(skb)	((struct netc_skb_cb *)((skb)->cb))
@@ -26,10 +35,22 @@ struct netc_skb_cb {
  * @twostep_tstamp_handler: Called by the tagger when a two-step transmit
  *	timestamp response is received, to deliver the timestamp to the
  *	switch driver.
+ * @onestep_sync_enqueue: Called from the tagger xmit path for a one-step Sync
+ *	frame. The switch driver takes ownership of the skb and queues it for
+ *	deferred transmission from process context, where the shared
+ *	PM_SINGLE_STEP register can be programmed and the PTP timer read
+ *	(which may sleep). The tagger must not touch the skb after this call
+ *	and returns NULL to dsa_user_xmit().
+ * @onestep_sync_xmit: Called by the switch driver to transmit a deferred
+ *	one-step Sync frame directly to the conduit, bypassing dsa_user_xmit().
  */
 struct netc_tagger_data {
 	void (*twostep_tstamp_handler)(struct dsa_switch *ds, int port,
 				       u8 ts_req_id, u64 ts);
+	void (*onestep_sync_enqueue)(struct dsa_switch *ds, int port,
+				     struct sk_buff *skb);
+	netdev_tx_t (*onestep_sync_xmit)(struct sk_buff *skb,
+					 struct net_device *ndev);
 };
 
 #endif
diff --git a/net/dsa/tag_netc.c b/net/dsa/tag_netc.c
index 9f9a61d8133b..0c36aeaade6a 100644
--- a/net/dsa/tag_netc.c
+++ b/net/dsa/tag_netc.c
@@ -16,6 +16,8 @@
 #define NETC_TAG_TO_PORT		1
 /* SubType0: No request to perform timestamping */
 #define NETC_TAG_TP_SUBTYPE0		0
+/* SubType1: Request to perform one-step timestamping */
+#define NETC_TAG_TP_SUBTYPE1		1
 /* SubType2: Request to perform two-step timestamping */
 #define NETC_TAG_TP_SUBTYPE2		2
 
@@ -31,6 +33,7 @@
 /* NETC switch tag lengths */
 #define NETC_TAG_FORWARD_LEN		6
 #define NETC_TAG_TP_SUBTYPE0_LEN	6
+#define NETC_TAG_TP_SUBTYPE1_LEN	10
 #define NETC_TAG_TP_SUBTYPE2_LEN	6
 #define NETC_TAG_TH_SUBTYPE0_LEN	6
 #define NETC_TAG_TH_SUBTYPE1_LEN	14
@@ -44,6 +47,7 @@
 #define NETC_TAG_SWITCH			GENMASK(2, 0)
 #define NETC_TAG_PORT			GENMASK(7, 3)
 #define NETC_TAG_TS_REQ_ID		GENMASK(3, 0)
+#define NETC_TAG_TIMESTAMP		GENMASK(29, 0)
 
 struct netc_tag_cmn {
 	__be16 tpid;
@@ -52,6 +56,12 @@ struct netc_tag_cmn {
 	u8 switch_port;
 } __packed;
 
+struct netc_tag_tp_subtype1 {
+	struct netc_tag_cmn cmn;
+	u8 resv;
+	__be32 timestamp;
+} __packed;
+
 struct netc_tag_tp_subtype2 {
 	struct netc_tag_cmn cmn;
 	u8 ts_req_id;
@@ -118,6 +128,17 @@ static void netc_fill_tp_tag_subtype0(struct sk_buff *skb,
 				NETC_TAG_TP_SUBTYPE0_LEN);
 }
 
+static void netc_fill_tp_tag_subtype1(struct sk_buff *skb,
+				      struct net_device *ndev)
+{
+	u32 ts = FIELD_PREP(NETC_TAG_TIMESTAMP, NETC_SKB_CB(skb)->tstamp);
+	struct netc_tag_tp_subtype1 *tag;
+
+	tag = netc_fill_common_tp_tag(skb, ndev, NETC_TAG_TP_SUBTYPE1,
+				      NETC_TAG_TP_SUBTYPE1_LEN);
+	tag->timestamp = htonl(ts);
+}
+
 static void netc_fill_tp_tag_subtype2(struct sk_buff *skb,
 				      struct net_device *ndev)
 {
@@ -129,6 +150,42 @@ static void netc_fill_tp_tag_subtype2(struct sk_buff *skb,
 	tag->ts_req_id = FIELD_PREP(NETC_TAG_TS_REQ_ID, ts_req_id);
 }
 
+static netdev_tx_t netc_onestep_sync_xmit(struct sk_buff *skb,
+					  struct net_device *dev)
+{
+	/* This deferred one-step Sync frame already went through
+	 * dsa_user_xmit()'s skb_ensure_writable_head_tail() and eth_skb_pad()
+	 * before it was queued in netc_xmit(), and nothing has cloned it or
+	 * shrunk its head/tail room since. So the head/tail room is still
+	 * guaranteed and the skb is still writable; only the tag needs to be
+	 * pushed before handing it directly to the conduit, bypassing
+	 * dsa_user_xmit() so that dev_sw_netstats_tx_add() is not invoked a
+	 * second time for the same frame.
+	 */
+	netc_fill_tp_tag_subtype1(skb, dev);
+
+	return dsa_enqueue_skb(skb, dev);
+}
+
+static void netc_onestep_sync_enqueue(struct sk_buff *skb,
+				      struct net_device *ndev)
+{
+	struct dsa_port *dp = dsa_user_to_port(ndev);
+	struct netc_tagger_data *tagger_data;
+
+	tagger_data = dp->ds->tagger_data;
+	if (unlikely(!tagger_data->onestep_sync_enqueue)) {
+		kfree_skb(skb);
+		return;
+	}
+
+	/* Hand the one-step Sync to the switch driver, which takes ownership
+	 * and queues it for deferred transmission from its work. The tagger
+	 * must not touch the skb after this point.
+	 */
+	tagger_data->onestep_sync_enqueue(dp->ds, dp->index, skb);
+}
+
 static struct sk_buff *netc_xmit(struct sk_buff *skb,
 				 struct net_device *ndev)
 {
@@ -138,6 +195,16 @@ static struct sk_buff *netc_xmit(struct sk_buff *skb,
 	if (likely(!ptp_flag)) {
 		netc_fill_tp_tag_subtype0(skb, ndev);
 		return skb;
+	}
+
+	if (ptp_flag == NETC_PTP_FLAG_ONESTEP) {
+		/* The switch driver takes ownership of the one-step Sync and
+		 * queues it for deferred TX; the deferred work tags it subtype 1
+		 * and transmits it directly to the conduit. Return NULL so
+		 * dsa_user_xmit() stops processing this skb.
+		 */
+		netc_onestep_sync_enqueue(skb, ndev);
+		return NULL;
 	} else if (ptp_flag == NETC_PTP_FLAG_TWOSTEP) {
 		netc_fill_tp_tag_subtype2(skb, ndev);
 	} else {
@@ -317,6 +384,7 @@ static int netc_connect(struct dsa_switch *ds)
 	if (!tagger_data)
 		return -ENOMEM;
 
+	tagger_data->onestep_sync_xmit = netc_onestep_sync_xmit;
 	ds->tagger_data = tagger_data;
 
 	return 0;
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH v2 net-next 1/7] ptp: netc: use ioread64_lo_hi/iowrite64_lo_hi for 64-bit register access
  2026-08-08  3:21 [PATCH v2 net-next 0/7] net: dsa: netc: add PTP support for NETC switch wei.fang
  2026-08-08  3:21 ` [PATCH] net: dsa: netc: add PTP one-step timestamping support wei.fang
@ 2026-08-08  3:21 ` wei.fang
  2026-08-08  3:21 ` [PATCH v2 net-next 2/7] ptp: netc: remove unnecessary pcie_flr() call in probe wei.fang
                   ` (5 subsequent siblings)
  7 siblings, 0 replies; 14+ messages in thread
From: wei.fang @ 2026-08-08  3:21 UTC (permalink / raw)
  To: xiaoning.wang, andrew, olteanv, andrew+netdev, davem, edumazet,
	kuba, pabeni, horms, richardcochran
  Cc: wei.fang, imx, netdev, linux-kernel, linuxppc-dev,
	linux-arm-kernel

From: Wei Fang <wei.fang@nxp.com>

Replace the open-coded 64-bit register read/write sequences with
ioread64_lo_hi() and iowrite64_lo_hi() helpers. Introduce two new macros
netc_timer_rd64() and netc_timer_wr64() that wrap these helpers and use
them throughout the driver. This reduces boilerplate and makes the intent
of each operation clearer.

The high-half register defines (NETC_TMR_*_H) are kept to document the
register map; they are not used directly since netc_timer_rd/wr64()
address the 64-bit register pair via the low-half offset, relying on the
hardware layout where H is always at L + 4.

Signed-off-by: Wei Fang <wei.fang@nxp.com>
---
 drivers/ptp/ptp_netc.c | 72 ++++++++++++------------------------------
 1 file changed, 21 insertions(+), 51 deletions(-)

diff --git a/drivers/ptp/ptp_netc.c b/drivers/ptp/ptp_netc.c
index 1c20d7efab92..0e0972596d88 100644
--- a/drivers/ptp/ptp_netc.c
+++ b/drivers/ptp/ptp_netc.c
@@ -127,6 +127,17 @@ struct netc_timer {
 
 #define netc_timer_rd(p, o)		netc_read((p)->base + (o))
 #define netc_timer_wr(p, o, v)		netc_write((p)->base + (o), v)
+
+/* The 64-bit timer registers consist of a low (L) and high (H) register pair.
+ * Hardware requires a strict access order: for writes, TMR_xxx_L must be
+ * written first, which latches the value into a shadow register; the write
+ * to TMR_xxx_H then atomically transfers both shadow registers into the live
+ * counter. For reads, TMR_xxx_L must be read first to capture a coherent
+ * snapshot. iowrite64_lo_hi() and ioread64_lo_hi() enforce this L-before-H
+ * ordering.
+ */
+#define netc_timer_rd64(p, o)		ioread64_lo_hi((p)->base + (o))
+#define netc_timer_wr64(p, o, v)	iowrite64_lo_hi(v, (p)->base + (o))
 #define ptp_to_netc_timer(ptp)		container_of((ptp), struct netc_timer, caps)
 
 static const char *const timer_clk_src[] = {
@@ -136,66 +147,28 @@ static const char *const timer_clk_src[] = {
 
 static void netc_timer_cnt_write(struct netc_timer *priv, u64 ns)
 {
-	u32 tmr_cnt_h = upper_32_bits(ns);
-	u32 tmr_cnt_l = lower_32_bits(ns);
-
-	/* Writes to the TMR_CNT_L register copies the written value
-	 * into the shadow TMR_CNT_L register. Writes to the TMR_CNT_H
-	 * register copies the values written into the shadow TMR_CNT_H
-	 * register. Contents of the shadow registers are copied into
-	 * the TMR_CNT_L and TMR_CNT_H registers following a write into
-	 * the TMR_CNT_H register. So the user must writes to TMR_CNT_L
-	 * register first. Other H/L registers should have the same
-	 * behavior.
-	 */
-	netc_timer_wr(priv, NETC_TMR_CNT_L, tmr_cnt_l);
-	netc_timer_wr(priv, NETC_TMR_CNT_H, tmr_cnt_h);
+	netc_timer_wr64(priv, NETC_TMR_CNT_L, ns);
 }
 
 static u64 netc_timer_offset_read(struct netc_timer *priv)
 {
-	u32 tmr_off_l, tmr_off_h;
-	u64 offset;
-
-	tmr_off_l = netc_timer_rd(priv, NETC_TMR_OFF_L);
-	tmr_off_h = netc_timer_rd(priv, NETC_TMR_OFF_H);
-	offset = (((u64)tmr_off_h) << 32) | tmr_off_l;
-
-	return offset;
+	return netc_timer_rd64(priv, NETC_TMR_OFF_L);
 }
 
 static void netc_timer_offset_write(struct netc_timer *priv, u64 offset)
 {
-	u32 tmr_off_h = upper_32_bits(offset);
-	u32 tmr_off_l = lower_32_bits(offset);
-
-	netc_timer_wr(priv, NETC_TMR_OFF_L, tmr_off_l);
-	netc_timer_wr(priv, NETC_TMR_OFF_H, tmr_off_h);
+	netc_timer_wr64(priv, NETC_TMR_OFF_L, offset);
 }
 
 static u64 netc_timer_cur_time_read(struct netc_timer *priv)
 {
-	u32 time_h, time_l;
-	u64 ns;
-
-	/* The user should read NETC_TMR_CUR_TIME_L first to
-	 * get correct current time.
-	 */
-	time_l = netc_timer_rd(priv, NETC_TMR_CUR_TIME_L);
-	time_h = netc_timer_rd(priv, NETC_TMR_CUR_TIME_H);
-	ns = (u64)time_h << 32 | time_l;
-
-	return ns;
+	return netc_timer_rd64(priv, NETC_TMR_CUR_TIME_L);
 }
 
 static void netc_timer_alarm_write(struct netc_timer *priv,
 				   u64 alarm, int index)
 {
-	u32 alarm_h = upper_32_bits(alarm);
-	u32 alarm_l = lower_32_bits(alarm);
-
-	netc_timer_wr(priv, NETC_TMR_ALARM_L(index), alarm_l);
-	netc_timer_wr(priv, NETC_TMR_ALARM_H(index), alarm_h);
+	netc_timer_wr64(priv, NETC_TMR_ALARM_L(index), alarm);
 }
 
 static u32 netc_timer_get_integral_period(struct netc_timer *priv)
@@ -497,22 +470,19 @@ static void netc_timer_handle_etts_event(struct netc_timer *priv, int index,
 					 bool update_event)
 {
 	struct ptp_clock_event event;
-	u32 etts_l = 0, etts_h = 0;
+	u64 etts = 0;
 
-	while (netc_timer_rd(priv, NETC_TMR_STAT) & TMR_STAT_ETS_VLD(index)) {
-		etts_l = netc_timer_rd(priv, NETC_TMR_ETTS_L(index));
-		etts_h = netc_timer_rd(priv, NETC_TMR_ETTS_H(index));
-	}
+	while (netc_timer_rd(priv, NETC_TMR_STAT) & TMR_STAT_ETS_VLD(index))
+		etts = netc_timer_rd64(priv, NETC_TMR_ETTS_L(index));
 
 	/* Invalid time stamp */
-	if (!etts_l && !etts_h)
+	if (!etts)
 		return;
 
 	if (update_event) {
 		event.type = PTP_CLOCK_EXTTS;
 		event.index = index;
-		event.timestamp = (u64)etts_h << 32;
-		event.timestamp |= etts_l;
+		event.timestamp = etts;
 		ptp_clock_event(priv->clock, &event);
 	}
 }
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH v2 net-next 2/7] ptp: netc: remove unnecessary pcie_flr() call in probe
  2026-08-08  3:21 [PATCH v2 net-next 0/7] net: dsa: netc: add PTP support for NETC switch wei.fang
  2026-08-08  3:21 ` [PATCH] net: dsa: netc: add PTP one-step timestamping support wei.fang
  2026-08-08  3:21 ` [PATCH v2 net-next 1/7] ptp: netc: use ioread64_lo_hi/iowrite64_lo_hi for 64-bit register access wei.fang
@ 2026-08-08  3:21 ` wei.fang
  2026-08-08  3:21 ` [PATCH v2 net-next 3/7] ptp: netc: export netc_timer_get_current_time() for cross-driver use wei.fang
                   ` (4 subsequent siblings)
  7 siblings, 0 replies; 14+ messages in thread
From: wei.fang @ 2026-08-08  3:21 UTC (permalink / raw)
  To: xiaoning.wang, andrew, olteanv, andrew+netdev, davem, edumazet,
	kuba, pabeni, horms, richardcochran
  Cc: wei.fang, imx, netdev, linux-kernel, linuxppc-dev,
	linux-arm-kernel

From: Wei Fang <wei.fang@nxp.com>

According to the NETC reference manual, function level reset is not
applicable to the timer as a supporting function. Remove the pcie_flr()
call from netc_timer_pci_probe() as it has no effect.

Signed-off-by: Wei Fang <wei.fang@nxp.com>
---
 drivers/ptp/ptp_netc.c | 1 -
 1 file changed, 1 deletion(-)

diff --git a/drivers/ptp/ptp_netc.c b/drivers/ptp/ptp_netc.c
index 0e0972596d88..d33c49c86ac4 100644
--- a/drivers/ptp/ptp_netc.c
+++ b/drivers/ptp/ptp_netc.c
@@ -774,7 +774,6 @@ static int netc_timer_pci_probe(struct pci_dev *pdev)
 	if (!priv)
 		return -ENOMEM;
 
-	pcie_flr(pdev);
 	err = pci_enable_device_mem(pdev);
 	if (err)
 		return dev_err_probe(dev, err, "Failed to enable device\n");
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH v2 net-next 3/7] ptp: netc: export netc_timer_get_current_time() for cross-driver use
  2026-08-08  3:21 [PATCH v2 net-next 0/7] net: dsa: netc: add PTP support for NETC switch wei.fang
                   ` (2 preceding siblings ...)
  2026-08-08  3:21 ` [PATCH v2 net-next 2/7] ptp: netc: remove unnecessary pcie_flr() call in probe wei.fang
@ 2026-08-08  3:21 ` wei.fang
       [not found]   ` <20260809031904.90B581F000E9@smtp.kernel.org>
  2026-08-08  3:21 ` [PATCH v2 net-next 4/7] net: dsa: netc: use entry ID instead of pointer to track host flood rule wei.fang
                   ` (3 subsequent siblings)
  7 siblings, 1 reply; 14+ messages in thread
From: wei.fang @ 2026-08-08  3:21 UTC (permalink / raw)
  To: xiaoning.wang, andrew, olteanv, andrew+netdev, davem, edumazet,
	kuba, pabeni, horms, richardcochran
  Cc: wei.fang, imx, netdev, linux-kernel, linuxppc-dev,
	linux-arm-kernel

From: Wei Fang <wei.fang@nxp.com>

The NETC Switch does not have its own time registers and must obtain
the current PTP time from the NETC Timer bound to it. Since the two
are separate PCIe functions with independent drivers, add
netc_timer_get_current_time() to the Timer driver and export it via
EXPORT_SYMBOL_GPL().

The function takes the Timer's pci_dev pointer, acquires the per-device
spinlock to protect against concurrent register access, and reads
TMR_CUR_TIME via netc_timer_cur_time_read(). It returns 0 if the Timer
driver has not yet probed or has already been removed, allowing the
caller to handle the unavailable case gracefully.

Hold the device lock around pci_get_drvdata() and the register read to
serialize against concurrent driver unbind. The remove() callback runs
under the same device lock, so this guarantees that priv and the MMIO
mapping remain valid for the entire duration of the read.

Move spin_lock_init() from netc_timer_probe() into netc_timer_pci_probe(),
before pci_set_drvdata(), so that the spinlock is fully initialized before
the driver data becomes visible to other drivers.

Signed-off-by: Wei Fang <wei.fang@nxp.com>
---
 drivers/ptp/ptp_netc.c          | 43 ++++++++++++++++++++++++++++++++-
 include/linux/fsl/netc_global.h | 10 ++++++++
 2 files changed, 52 insertions(+), 1 deletion(-)

diff --git a/drivers/ptp/ptp_netc.c b/drivers/ptp/ptp_netc.c
index d33c49c86ac4..27f85818d6a8 100644
--- a/drivers/ptp/ptp_netc.c
+++ b/drivers/ptp/ptp_netc.c
@@ -165,6 +165,47 @@ static u64 netc_timer_cur_time_read(struct netc_timer *priv)
 	return netc_timer_rd64(priv, NETC_TMR_CUR_TIME_L);
 }
 
+/**
+ * netc_timer_get_current_time - read the current PTP time from the NETC Timer
+ * @pdev: PCI device of the NETC Timer
+ *
+ * Reads the 64-bit current time register (TMR_CUR_TIME) from the NETC Timer
+ * device associated with @pdev. Returns 0 if the Timer driver has not yet
+ * probed or has already been removed.
+ *
+ * Context: Process context only. Acquires the device mutex via device_lock(),
+ *          which may sleep. Must not be called from atomic context, softirq,
+ *          BH, or while holding a spinlock.
+ *
+ * Return: Current PTP time in nanoseconds, or 0 if the timer is unavailable.
+ */
+u64 netc_timer_get_current_time(struct pci_dev *pdev)
+{
+	struct netc_timer *priv;
+	unsigned long flags;
+	u64 cur_time = 0;
+
+	/* Serialize against driver unbind: the remove() callback runs under
+	 * the device lock, so holding it here ensures that priv remains valid
+	 * for the entire duration of the register read.
+	 */
+	device_lock(&pdev->dev);
+
+	priv = pci_get_drvdata(pdev);
+	if (!priv)
+		goto unlock_device;
+
+	spin_lock_irqsave(&priv->lock, flags);
+	cur_time = netc_timer_cur_time_read(priv);
+	spin_unlock_irqrestore(&priv->lock, flags);
+
+unlock_device:
+	device_unlock(&pdev->dev);
+
+	return cur_time;
+}
+EXPORT_SYMBOL_GPL(netc_timer_get_current_time);
+
 static void netc_timer_alarm_write(struct netc_timer *priv,
 				   u64 alarm, int index)
 {
@@ -795,6 +836,7 @@ static int netc_timer_pci_probe(struct pci_dev *pdev)
 		goto release_mem_regions;
 	}
 
+	spin_lock_init(&priv->lock);
 	pci_set_drvdata(pdev, priv);
 
 	return 0;
@@ -965,7 +1007,6 @@ static int netc_timer_probe(struct pci_dev *pdev,
 	priv->caps = netc_timer_ptp_caps;
 	priv->oclk_prsc = NETC_TMR_DEFAULT_PRSC;
 	priv->pps_channel = NETC_TMR_INVALID_CHANNEL;
-	spin_lock_init(&priv->lock);
 	snprintf(priv->irq_name, sizeof(priv->irq_name), "ptp-netc %s",
 		 pci_name(pdev));
 
diff --git a/include/linux/fsl/netc_global.h b/include/linux/fsl/netc_global.h
index 5b8ff528d369..ace0bcde1e2c 100644
--- a/include/linux/fsl/netc_global.h
+++ b/include/linux/fsl/netc_global.h
@@ -6,6 +6,7 @@
 
 #include <linux/io.h>
 #include <linux/io-64-nonatomic-lo-hi.h>
+#include <linux/pci.h>
 
 static inline u32 netc_read(void __iomem *reg)
 {
@@ -22,4 +23,13 @@ static inline u64 netc_read64(void __iomem *reg)
 	return ioread64(reg);
 }
 
+#if IS_REACHABLE(CONFIG_PTP_NETC_V4_TIMER)
+u64 netc_timer_get_current_time(struct pci_dev *timer_dev);
+#else
+static inline u64 netc_timer_get_current_time(struct pci_dev *timer_dev)
+{
+	return 0;
+}
+#endif
+
 #endif
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH v2 net-next 4/7] net: dsa: netc: use entry ID instead of pointer to track host flood rule
  2026-08-08  3:21 [PATCH v2 net-next 0/7] net: dsa: netc: add PTP support for NETC switch wei.fang
                   ` (3 preceding siblings ...)
  2026-08-08  3:21 ` [PATCH v2 net-next 3/7] ptp: netc: export netc_timer_get_current_time() for cross-driver use wei.fang
@ 2026-08-08  3:21 ` wei.fang
  2026-08-08  3:21 ` [PATCH v2 net-next 5/7] net: dsa: netc: enable ingress port filtering lookup by default wei.fang
                   ` (2 subsequent siblings)
  7 siblings, 0 replies; 14+ messages in thread
From: wei.fang @ 2026-08-08  3:21 UTC (permalink / raw)
  To: xiaoning.wang, andrew, olteanv, andrew+netdev, davem, edumazet,
	kuba, pabeni, horms, richardcochran
  Cc: wei.fang, imx, netdev, linux-kernel, linuxppc-dev,
	linux-arm-kernel

From: Wei Fang <wei.fang@nxp.com>

Replace the struct ipft_entry_data pointer in struct netc_port with a
plain u32 entry ID (ipft_hf_eid), using NTMP_NULL_ENTRY_ID as the
sentinel value. The ipft_entry_data allocation is now freed immediately
inside netc_port_add_host_flood_rule() after the hardware entry is
committed, so no heap memory survives beyond that function. As a result,
netc_free_host_flood_rules() is no longer needed and can be removed.

Signed-off-by: Wei Fang <wei.fang@nxp.com>
---
 drivers/net/dsa/netc/netc_main.c   | 63 ++++++++++++------------------
 drivers/net/dsa/netc/netc_switch.h |  2 +-
 2 files changed, 25 insertions(+), 40 deletions(-)

diff --git a/drivers/net/dsa/netc/netc_main.c b/drivers/net/dsa/netc/netc_main.c
index 77077352c1a5..d326a00104e1 100644
--- a/drivers/net/dsa/netc/netc_main.c
+++ b/drivers/net/dsa/netc/netc_main.c
@@ -286,6 +286,12 @@ static int netc_init_all_ports(struct netc_switch *priv)
 				dev_err(dev, "Failed to create MDIO bus\n");
 				return err;
 			}
+
+			/* The ipft_hf_eid is initialized to an invalid entry
+			 * ID because the host flood rule (IPFT entry) has not
+			 * been created.
+			 */
+			np->ipft_hf_eid = NTMP_NULL_ENTRY_ID;
 		}
 	}
 
@@ -938,30 +944,12 @@ static void netc_destroy_all_lists(struct netc_switch *priv)
 	mutex_destroy(&priv->vft_lock);
 }
 
-static void netc_free_host_flood_rules(struct netc_switch *priv)
-{
-	struct dsa_port *dp;
-
-	dsa_switch_for_each_user_port(dp, priv->ds) {
-		struct netc_port *np = priv->ports[dp->index];
-
-		/* No need to clear the hardware IPFT entry. Because PCIe
-		 * FLR will be performed when the switch is re-registered,
-		 * it will reset hardware state. So only need to free the
-		 * memory to avoid memory leak.
-		 */
-		kfree(np->host_flood);
-		np->host_flood = NULL;
-	}
-}
-
 static void netc_teardown(struct dsa_switch *ds)
 {
 	struct netc_switch *priv = ds->priv;
 
 	disable_delayed_work_sync(&priv->fdbt_ageing_work);
 	netc_destroy_all_lists(priv);
-	netc_free_host_flood_rules(priv);
 	netc_free_ntmp_user(priv);
 }
 
@@ -1759,37 +1747,36 @@ static int netc_port_add_host_flood_rule(struct netc_port *np,
 	cfge->cfg = cpu_to_le32(cfg);
 
 	err = ntmp_ipft_add_entry(&priv->ntmp, host_flood);
-	if (err) {
-		kfree(host_flood);
-		return err;
-	}
+	if (err)
+		goto free_host_flood;
 
 	np->uc = uc;
 	np->mc = mc;
-	np->host_flood = host_flood;
+	np->ipft_hf_eid = host_flood->entry_id;
 	/* Enable ingress port filter table lookup */
 	netc_port_wr(np, NETC_PIPFCR, PIPFCR_EN);
 
-	return 0;
+free_host_flood:
+	kfree(host_flood);
+
+	return err;
 }
 
-static void netc_port_remove_host_flood(struct netc_port *np,
-					struct ipft_entry_data *host_flood)
+static void netc_port_remove_host_flood(struct netc_port *np, u32 entry_id)
 {
 	struct netc_switch *priv = np->switch_priv;
 	bool disable_host_flood = false;
 
-	if (!host_flood)
+	if (entry_id == NTMP_NULL_ENTRY_ID)
 		return;
 
-	if (np->host_flood == host_flood)
+	if (np->ipft_hf_eid == entry_id)
 		disable_host_flood = true;
 
-	ntmp_ipft_delete_entry(&priv->ntmp, host_flood->entry_id);
-	kfree(host_flood);
+	ntmp_ipft_delete_entry(&priv->ntmp, entry_id);
 
 	if (disable_host_flood) {
-		np->host_flood = NULL;
+		np->ipft_hf_eid = NTMP_NULL_ENTRY_ID;
 		np->uc = false;
 		np->mc = false;
 		netc_port_wr(np, NETC_PIPFCR, 0);
@@ -1800,7 +1787,7 @@ static void netc_port_set_host_flood(struct dsa_switch *ds, int port,
 				     bool uc, bool mc)
 {
 	struct netc_port *np = NETC_PORT(ds, port);
-	struct ipft_entry_data *old_host_flood;
+	u32 old_entry_id;
 
 	/* Do not add host flood rule to ingress port filter table when
 	 * the port has joined a bridge. Otherwise, the ingress frames
@@ -1808,7 +1795,7 @@ static void netc_port_set_host_flood(struct dsa_switch *ds, int port,
 	 * will be redirected directly to the CPU port.
 	 */
 	if (dsa_port_bridge_dev_get(np->dp)) {
-		netc_port_remove_host_flood(np, np->host_flood);
+		netc_port_remove_host_flood(np, np->ipft_hf_eid);
 
 		return;
 	}
@@ -1818,20 +1805,18 @@ static void netc_port_set_host_flood(struct dsa_switch *ds, int port,
 
 	/* IPFT does not support in-place updates to the KEYE element,
 	 * we need to add a new entry and then delete the old one. So
-	 * save the old entry first.
+	 * save the old entry ID first.
 	 */
-	old_host_flood = np->host_flood;
-	np->host_flood = NULL;
+	old_entry_id = np->ipft_hf_eid;
 
 	if (netc_port_add_host_flood_rule(np, uc, mc)) {
-		np->host_flood = old_host_flood;
 		dev_err(ds->dev, "Failed to add host flood rule on port %d\n",
 			port);
 		return;
 	}
 
 	/* Remove the old host flood entry */
-	netc_port_remove_host_flood(np, old_host_flood);
+	netc_port_remove_host_flood(np, old_entry_id);
 }
 
 static int netc_single_vlan_aware_bridge(struct dsa_switch *ds,
@@ -2020,7 +2005,7 @@ static int netc_port_bridge_join(struct dsa_switch *ds, int port,
 	netc_port_set_pvid(np, vlan_unaware_pvid);
 
 out:
-	netc_port_remove_host_flood(np, np->host_flood);
+	netc_port_remove_host_flood(np, np->ipft_hf_eid);
 
 	if (atomic_inc_return(&priv->br_cnt) == 1)
 		schedule_delayed_work(&priv->fdbt_ageing_work,
diff --git a/drivers/net/dsa/netc/netc_switch.h b/drivers/net/dsa/netc/netc_switch.h
index 305f2a92e2f9..fd36ec2d0e90 100644
--- a/drivers/net/dsa/netc/netc_switch.h
+++ b/drivers/net/dsa/netc/netc_switch.h
@@ -84,7 +84,7 @@ struct netc_port {
 	u16 uc:1;
 	u16 mc:1;
 	u16 pvid;
-	struct ipft_entry_data *host_flood;
+	u32 ipft_hf_eid;
 };
 
 struct netc_switch_regs {
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH v2 net-next 5/7] net: dsa: netc: enable ingress port filtering lookup by default
  2026-08-08  3:21 [PATCH v2 net-next 0/7] net: dsa: netc: add PTP support for NETC switch wei.fang
                   ` (4 preceding siblings ...)
  2026-08-08  3:21 ` [PATCH v2 net-next 4/7] net: dsa: netc: use entry ID instead of pointer to track host flood rule wei.fang
@ 2026-08-08  3:21 ` wei.fang
       [not found]   ` <20260809031905.520531F00A3D@smtp.kernel.org>
  2026-08-08  3:21 ` [PATCH v2 net-next 6/7] net: dsa: netc: add PTP two-step timestamping support wei.fang
  2026-08-08  3:21 ` [PATCH v2 net-next 7/7] net: dsa: netc: add PTP one-step " wei.fang
  7 siblings, 1 reply; 14+ messages in thread
From: wei.fang @ 2026-08-08  3:21 UTC (permalink / raw)
  To: xiaoning.wang, andrew, olteanv, andrew+netdev, davem, edumazet,
	kuba, pabeni, horms, richardcochran
  Cc: wei.fang, imx, netdev, linux-kernel, linuxppc-dev,
	linux-arm-kernel

From: Wei Fang <wei.fang@nxp.com>

The ingress port filtering lookup function involves performing a lookup
against the ingress port filter table (IPFT). If the frame matches an
entry, subsequent frame processing functions will perform corresponding
operations based on the parameters specified in that entry. If no entry
matches the frame, the frame is allowed in, and passed to the next frame
processing function. Therefore, the ingress port filtering lookup is
enabled by default to simplify code logic, eliminating the need to track
whether the current IPFT has already added an entry for the port.

Signed-off-by: Wei Fang <wei.fang@nxp.com>
---
 drivers/net/dsa/netc/netc_main.c | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/drivers/net/dsa/netc/netc_main.c b/drivers/net/dsa/netc/netc_main.c
index d326a00104e1..9cb9e618661e 100644
--- a/drivers/net/dsa/netc/netc_main.c
+++ b/drivers/net/dsa/netc/netc_main.c
@@ -555,6 +555,12 @@ static void netc_port_fixed_config(struct netc_port *np)
 	netc_port_rmw(np, NETC_PCR, PCR_L2DOSE | PCR_L3DOSE,
 		      PCR_L2DOSE | PCR_L3DOSE);
 
+	/* Enable ingress port filter table lookup, if no match is found,
+	 * the frame is allowed and passed to the next frame processing
+	 * function.
+	 */
+	netc_port_wr(np, NETC_PIPFCR, PIPFCR_EN);
+
 	/* Set the quanta value of TX PAUSE frame */
 	netc_mac_port_wr(np, NETC_PM_PAUSE_QUANTA(0), NETC_PAUSE_QUANTA);
 
@@ -1708,8 +1714,6 @@ static int netc_port_add_host_flood_rule(struct netc_port *np,
 	int err;
 
 	if (!uc && !mc) {
-		/* Disable ingress port filter table lookup */
-		netc_port_wr(np, NETC_PIPFCR, 0);
 		np->uc = false;
 		np->mc = false;
 
@@ -1753,8 +1757,6 @@ static int netc_port_add_host_flood_rule(struct netc_port *np,
 	np->uc = uc;
 	np->mc = mc;
 	np->ipft_hf_eid = host_flood->entry_id;
-	/* Enable ingress port filter table lookup */
-	netc_port_wr(np, NETC_PIPFCR, PIPFCR_EN);
 
 free_host_flood:
 	kfree(host_flood);
@@ -1779,7 +1781,6 @@ static void netc_port_remove_host_flood(struct netc_port *np, u32 entry_id)
 		np->ipft_hf_eid = NTMP_NULL_ENTRY_ID;
 		np->uc = false;
 		np->mc = false;
-		netc_port_wr(np, NETC_PIPFCR, 0);
 	}
 }
 
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH v2 net-next 6/7] net: dsa: netc: add PTP two-step timestamping support
  2026-08-08  3:21 [PATCH v2 net-next 0/7] net: dsa: netc: add PTP support for NETC switch wei.fang
                   ` (5 preceding siblings ...)
  2026-08-08  3:21 ` [PATCH v2 net-next 5/7] net: dsa: netc: enable ingress port filtering lookup by default wei.fang
@ 2026-08-08  3:21 ` wei.fang
       [not found]   ` <20260809031907.888511F00A3D@smtp.kernel.org>
  2026-08-08  3:21 ` [PATCH v2 net-next 7/7] net: dsa: netc: add PTP one-step " wei.fang
  7 siblings, 1 reply; 14+ messages in thread
From: wei.fang @ 2026-08-08  3:21 UTC (permalink / raw)
  To: xiaoning.wang, andrew, olteanv, andrew+netdev, davem, edumazet,
	kuba, pabeni, horms, richardcochran
  Cc: wei.fang, imx, netdev, linux-kernel, linuxppc-dev,
	linux-arm-kernel

From: Wei Fang <wei.fang@nxp.com>

Add two-step TX timestamping and RX timestamping for the NETC switch.

For RX, install ingress port filter table (IPFT) rules that redirect PTP
frames to the CPU port. Support L2, L4 over IPv4 and L4 over IPv6, for
both event and general messages, selected through the hwtstamp rx_filter.
The hardware prepends a To_Host subtype 1 tag carrying the 64-bit ingress
timestamp. The tagger extracts it into the skb control buffer, and
netc_port_rxtstamp() copies it into skb_hwtstamps().

For two-step TX, clone the skb and allocate a 4-bit timestamp request ID,
then queue the clone on a per-port list. netc_xmit() emits a To_Port
subtype 2 tag carrying that ID. The hardware echoes the ID back in a
generated To_Host subtype 2 response frame together with the 64-bit
transmit timestamp. The tagger dispatches the ID and timestamp to the
switch driver through the twostep_tstamp_handler callback registered in
netc_tagger_data, which matches the queued clone and completes it via
skb_complete_tx_timestamp(), then frees the response skb. Non-PTP frames
keep using the To_Port subtype 0 tag on the xmit fast path.

Reclaim the request ID when the matching response arrives or when the
request times out after 5 seconds. This timeout is orders of magnitude
larger than the hardware's normal response latency, so a still missing
response after the window means the frame never reached the hardware, the
frames should have been dropped by software. In that case the hardware
will never echo that ID, so reusing the reclaimed ID for a later request
cannot cause a stale response to be matched against the wrong clone. A
response arriving later than 5 seconds only happens under a hardware
fault, where the timestamp is already meaningless for PTP synchronization
and no amount of ID bookkeeping would recover a usable value. Store the
allocated ID in the control block of the transmit skb as well as in its
queued clone. The transmit path reads the ID from the transmit skb when
building the To_Port subtype 2 tag; the response handler reads it from
the clone to match the queued entry, which is completed and freed only
by the handler or the timeout path.

The two-step response frame carries no payload; its total length is only
26 bytes (12 bytes of DMAC and SMAC plus a 14-byte switch tag).
eth_type_trans() pulls ETH_HLEN bytes, advancing skb->data past the
DMAC, SMAC, and the TPID that opens the switch tag, leaving skb->len
at 12. Since the tag pointer is at (skb->data - 2), the pskb_may_pull()
check must use (NETC_TAG_MAX_LEN - 2) rather than NETC_TAG_MAX_LEN.
Otherwise pskb_may_pull() drops the response frame and breaks PTP
synchronization.

Add the To_Port subtype 2 and To_Host subtype 1/2 tag structures, extend
netc_xmit() to select the tag based on ptp_flag in the skb control buffer,
and add netc_connect()/netc_disconnect() to manage the per-switch
netc_tagger_data allocation. Grab the PTP timer's pci_dev in netc_setup()
so get_ts_info() can report its PHC index, and release it in the teardown
and error paths.

Signed-off-by: Wei Fang <wei.fang@nxp.com>
---
 drivers/net/dsa/netc/Kconfig         |   1 +
 drivers/net/dsa/netc/Makefile        |   3 +-
 drivers/net/dsa/netc/netc_main.c     |  70 ++++-
 drivers/net/dsa/netc/netc_platform.c |   1 +
 drivers/net/dsa/netc/netc_ptp.c      | 454 +++++++++++++++++++++++++++
 drivers/net/dsa/netc/netc_switch.h   |  41 +++
 include/linux/dsa/tag_netc.h         |  21 ++
 net/dsa/tag_netc.c                   | 146 ++++++++-
 8 files changed, 727 insertions(+), 10 deletions(-)
 create mode 100644 drivers/net/dsa/netc/netc_ptp.c

diff --git a/drivers/net/dsa/netc/Kconfig b/drivers/net/dsa/netc/Kconfig
index 793f7691a24f..8770b65d0f62 100644
--- a/drivers/net/dsa/netc/Kconfig
+++ b/drivers/net/dsa/netc/Kconfig
@@ -4,6 +4,7 @@ config NET_DSA_NETC_SWITCH
 	depends on ARM64 || COMPILE_TEST
 	depends on NET_DSA && PCI
 	depends on NET_VENDOR_FREESCALE
+	depends on PTP_1588_CLOCK_OPTIONAL
 	select NET_DSA_TAG_NETC
 	select FSL_ENETC_MDIO
 	select NXP_NTMP
diff --git a/drivers/net/dsa/netc/Makefile b/drivers/net/dsa/netc/Makefile
index f40b13c702e0..572b833ad80f 100644
--- a/drivers/net/dsa/netc/Makefile
+++ b/drivers/net/dsa/netc/Makefile
@@ -1,3 +1,4 @@
 # SPDX-License-Identifier: GPL-2.0-only
 obj-$(CONFIG_NET_DSA_NETC_SWITCH) += nxp-netc-switch.o
-nxp-netc-switch-objs := netc_main.o netc_platform.o netc_ethtool.o
+nxp-netc-switch-objs := netc_main.o netc_platform.o netc_ethtool.o \
+			netc_ptp.o
diff --git a/drivers/net/dsa/netc/netc_main.c b/drivers/net/dsa/netc/netc_main.c
index 9cb9e618661e..4e139ffc2f76 100644
--- a/drivers/net/dsa/netc/netc_main.c
+++ b/drivers/net/dsa/netc/netc_main.c
@@ -65,6 +65,20 @@ netc_get_tag_protocol(struct dsa_switch *ds, int port,
 	return DSA_TAG_PROTO_NETC;
 }
 
+static int netc_connect_tag_protocol(struct dsa_switch *ds,
+				     enum dsa_tag_protocol proto)
+{
+	struct netc_tagger_data *tagger_data;
+
+	if (proto != DSA_TAG_PROTO_NETC)
+		return -EPROTONOSUPPORT;
+
+	tagger_data = ds->tagger_data;
+	tagger_data->twostep_tstamp_handler = netc_port_twostep_tstamp_handler;
+
+	return 0;
+}
+
 static void netc_port_rmw(struct netc_port *np, u32 reg,
 			  u32 mask, u32 val)
 {
@@ -292,6 +306,15 @@ static int netc_init_all_ports(struct netc_switch *priv)
 			 * been created.
 			 */
 			np->ipft_hf_eid = NTMP_NULL_ENTRY_ID;
+
+			/* Only the user port needs to support PTP feature, so
+			 * PTP-related resources, such as skb_txtstamp_queue,
+			 * tstamp_lock, etc., are initialized only for the user
+			 * port.
+			 */
+			err = netc_port_ptp_init(np);
+			if (err)
+				return err;
 		}
 	}
 
@@ -881,6 +904,15 @@ static int netc_switch_bpt_default_config(struct netc_switch *priv)
 	return 0;
 }
 
+static struct pci_dev *netc_get_ptp_timer(struct netc_switch *priv)
+{
+	struct pci_bus *bus = priv->pdev->bus;
+	u32 devfn = priv->info->tmr_devfn;
+
+	return pci_get_domain_bus_and_slot(pci_domain_nr(bus),
+					   bus->number, devfn);
+}
+
 static int netc_setup(struct dsa_switch *ds)
 {
 	struct netc_switch *priv = ds->priv;
@@ -893,13 +925,23 @@ static int netc_setup(struct dsa_switch *ds)
 
 	netc_get_switch_capabilities(priv);
 
+	/* The PTP timer sits on the same PCI bus as the switch. PCI creates
+	 * every function's pci_dev during bus enumeration, before any driver
+	 * probes, so we can grab the timer's pci_dev here even if the timer
+	 * driver has not probed yet.
+	 */
+	priv->tmr_dev = netc_get_ptp_timer(priv);
+	if (!priv->tmr_dev)
+		dev_info(priv->dev,
+			 "PTP timer PCI device not found\n");
+
 	err = netc_init_all_ports(priv);
 	if (err)
-		return err;
+		goto put_ptp_timer;
 
 	err = netc_init_ntmp_user(priv);
 	if (err)
-		return err;
+		goto put_ptp_timer;
 
 	INIT_HLIST_HEAD(&priv->fdb_list);
 	mutex_init(&priv->fdbt_lock);
@@ -938,6 +980,8 @@ static int netc_setup(struct dsa_switch *ds)
 	mutex_destroy(&priv->fdbt_lock);
 	mutex_destroy(&priv->vft_lock);
 	netc_free_ntmp_user(priv);
+put_ptp_timer:
+	pci_dev_put(priv->tmr_dev);
 
 	return err;
 }
@@ -950,6 +994,20 @@ static void netc_destroy_all_lists(struct netc_switch *priv)
 	mutex_destroy(&priv->vft_lock);
 }
 
+static void netc_free_ports_resources(struct netc_switch *priv)
+{
+	struct dsa_port *dp;
+
+	dsa_switch_for_each_available_port(dp, priv->ds) {
+		struct netc_port *np = priv->ports[dp->index];
+
+		if (!dsa_port_is_user(dp))
+			continue;
+
+		netc_port_purge_txtstamp_queue(np);
+	}
+}
+
 static void netc_teardown(struct dsa_switch *ds)
 {
 	struct netc_switch *priv = ds->priv;
@@ -957,6 +1015,8 @@ static void netc_teardown(struct dsa_switch *ds)
 	disable_delayed_work_sync(&priv->fdbt_ageing_work);
 	netc_destroy_all_lists(priv);
 	netc_free_ntmp_user(priv);
+	netc_free_ports_resources(priv);
+	pci_dev_put(priv->tmr_dev);
 }
 
 static bool netc_port_is_emdio_consumer(struct device_node *node)
@@ -2388,6 +2448,7 @@ static const struct phylink_mac_ops netc_phylink_mac_ops = {
 
 static const struct dsa_switch_ops netc_switch_ops = {
 	.get_tag_protocol		= netc_get_tag_protocol,
+	.connect_tag_protocol		= netc_connect_tag_protocol,
 	.setup				= netc_setup,
 	.teardown			= netc_teardown,
 	.phylink_get_caps		= netc_phylink_get_caps,
@@ -2416,6 +2477,11 @@ static const struct dsa_switch_ops netc_switch_ops = {
 	.get_sset_count			= netc_port_get_sset_count,
 	.get_strings			= netc_port_get_strings,
 	.get_ethtool_stats		= netc_port_get_ethtool_stats,
+	.get_ts_info			= netc_get_ts_info,
+	.port_hwtstamp_set		= netc_port_hwtstamp_set,
+	.port_hwtstamp_get		= netc_port_hwtstamp_get,
+	.port_rxtstamp			= netc_port_rxtstamp,
+	.port_txtstamp			= netc_port_txtstamp,
 };
 
 static int netc_switch_probe(struct pci_dev *pdev,
diff --git a/drivers/net/dsa/netc/netc_platform.c b/drivers/net/dsa/netc/netc_platform.c
index 34aeb6fceb3c..4fd0ce6770c3 100644
--- a/drivers/net/dsa/netc/netc_platform.c
+++ b/drivers/net/dsa/netc/netc_platform.c
@@ -50,6 +50,7 @@ static void imx94_switch_phylink_get_caps(int port,
 
 static const struct netc_switch_info imx94_info = {
 	.num_ports = 4,
+	.tmr_devfn = PCI_DEVFN(0, 1),
 	.phylink_get_caps = imx94_switch_phylink_get_caps,
 };
 
diff --git a/drivers/net/dsa/netc/netc_ptp.c b/drivers/net/dsa/netc/netc_ptp.c
new file mode 100644
index 000000000000..1384a6f31d1c
--- /dev/null
+++ b/drivers/net/dsa/netc/netc_ptp.c
@@ -0,0 +1,454 @@
+// SPDX-License-Identifier: (GPL-2.0+ OR BSD-3-Clause)
+/*
+ * NXP NETC switch driver
+ * Copyright 2025-2026 NXP
+ */
+
+#include <linux/ptp_classify.h>
+#include <linux/ptp_clock_kernel.h>
+
+#include "netc_switch.h"
+
+#define NETC_NUM_TS_REQ_ID		16
+#define NETC_TXTSTAMP_TIMEOUT		(5 * HZ)
+
+int netc_port_ptp_init(struct netc_port *np)
+{
+	/* Initialize to invalid entry IDs */
+	for (int i = 0; i < NETC_PTP_MAX; i++)
+		np->ptp_ipft_eid[i] = NTMP_NULL_ENTRY_ID;
+
+	spin_lock_init(&np->tstamp_lock);
+	__skb_queue_head_init(&np->skb_txtstamp_queue);
+
+	return 0;
+}
+
+static int netc_get_phc_index(struct netc_switch *priv)
+{
+	if (!priv->tmr_dev)
+		return -1;
+
+	return ptp_clock_index_by_dev(&priv->tmr_dev->dev);
+}
+
+int netc_get_ts_info(struct dsa_switch *ds, int port,
+		     struct kernel_ethtool_ts_info *info)
+{
+	struct netc_switch *priv = ds->priv;
+
+	info->phc_index = netc_get_phc_index(priv);
+	if (info->phc_index < 0)
+		return 0;
+
+	info->so_timestamping |= SOF_TIMESTAMPING_TX_HARDWARE |
+				 SOF_TIMESTAMPING_RX_HARDWARE |
+				 SOF_TIMESTAMPING_RAW_HARDWARE;
+
+	info->tx_types = BIT(HWTSTAMP_TX_OFF) | BIT(HWTSTAMP_TX_ON);
+
+	info->rx_filters = BIT(HWTSTAMP_FILTER_NONE) |
+			   BIT(HWTSTAMP_FILTER_PTP_V2_EVENT) |
+			   BIT(HWTSTAMP_FILTER_PTP_V2_L2_EVENT) |
+			   BIT(HWTSTAMP_FILTER_PTP_V2_L4_EVENT);
+
+	return 0;
+}
+
+static void netc_port_del_ptp_filter(struct netc_port *np)
+{
+	struct netc_switch *priv = np->switch_priv;
+	u32 entry_id;
+	int i;
+
+	for (i = 0; i < NETC_PTP_MAX; i++) {
+		entry_id = np->ptp_ipft_eid[i];
+		if (entry_id != NTMP_NULL_ENTRY_ID) {
+			/* There is no need to check the return value of
+			 * ntmp_ipft_delete_entry(); in general, deleted
+			 * existing entries will be successful.
+			 */
+			ntmp_ipft_delete_entry(&priv->ntmp, entry_id);
+			np->ptp_ipft_eid[i] = NTMP_NULL_ENTRY_ID;
+		}
+	}
+}
+
+static int netc_build_ptp_ipft_keye(struct ipft_keye_data *keye, int port,
+				    enum netc_ptp_type type)
+{
+	u16 src_port, frm_attr_flags;
+
+	keye->precedence = cpu_to_le16(NETC_IPFT_PTP_PRECEDENCE);
+	src_port = FIELD_PREP(IPFT_SRC_PORT, port);
+	src_port |= IPFT_SRC_PORT_MASK;
+	keye->src_port = cpu_to_le16(src_port);
+
+	switch (type) {
+	case NETC_PTP_L2:
+		keye->ethertype = htons(ETH_P_1588);
+		keye->ethertype_mask = htons(0xffff);
+		break;
+	case NETC_PTP_L4_IPV4_EVENT:
+	case NETC_PTP_L4_IPV4_GENERAL:
+	case NETC_PTP_L4_IPV6_EVENT:
+	case NETC_PTP_L4_IPV6_GENERAL:
+		frm_attr_flags = IPFT_FAF_IP_HDR | FIELD_PREP(IPFT_FAF_L4_CODE,
+				 IPFT_FAF_UDP_HDR);
+		if (type == NETC_PTP_L4_IPV6_EVENT ||
+		    type == NETC_PTP_L4_IPV6_GENERAL)
+			frm_attr_flags |= IPFT_FAF_IP_VER6;
+
+		keye->frm_attr_flags = cpu_to_le16(frm_attr_flags);
+
+		/* Set IP version bit in flags_mask to match IPv4 or IPv6
+		 * packets
+		 */
+		frm_attr_flags |= IPFT_FAF_IP_VER6;
+		keye->frm_attr_flags_mask = cpu_to_le16(frm_attr_flags);
+		keye->ip_protocol = IPPROTO_UDP;
+		keye->ip_protocol_mask = 0xff;
+
+		if (type == NETC_PTP_L4_IPV4_EVENT ||
+		    type == NETC_PTP_L4_IPV6_EVENT)
+			keye->l4_dst_port = htons(PTP_EV_PORT);
+		else
+			keye->l4_dst_port = htons(PTP_GEN_PORT);
+
+		keye->l4_dst_port_mask = htons(0xffff);
+		break;
+	default:
+		return -ERANGE;
+	}
+
+	return 0;
+}
+
+static int netc_port_add_ipft_ptp_entry(struct netc_port *np,
+					enum netc_ptp_type type)
+{
+	struct netc_switch *priv = np->switch_priv;
+	struct ipft_entry_data *entry;
+	struct ipft_keye_data *keye;
+	u32 cfg;
+	int err;
+
+	entry = kzalloc_obj(*entry);
+	if (!entry)
+		return -ENOMEM;
+
+	keye = &entry->keye;
+	err = netc_build_ptp_ipft_keye(keye, np->dp->index, type);
+	if (err)
+		goto free_entry;
+
+	cfg = FIELD_PREP(IPFT_FLTFA, IPFT_FLTFA_REDIRECT);
+	cfg |= FIELD_PREP(IPFT_HR, NETC_HR_PTP_TRAP);
+	cfg |= IPFT_TIMECAPE | IPFT_RRT;
+	entry->cfge.cfg = cpu_to_le32(cfg);
+
+	err = ntmp_ipft_add_entry(&priv->ntmp, entry);
+	if (err)
+		goto free_entry;
+
+	np->ptp_ipft_eid[type] = entry->entry_id;
+
+free_entry:
+	kfree(entry);
+
+	return err;
+}
+
+static int netc_port_add_l2_ptp_filter(struct netc_port *np)
+{
+	return netc_port_add_ipft_ptp_entry(np, NETC_PTP_L2);
+}
+
+static int netc_port_add_l4_ptp_filter(struct netc_port *np)
+{
+	int err;
+
+	err = netc_port_add_ipft_ptp_entry(np, NETC_PTP_L4_IPV4_EVENT);
+	if (err)
+		return err;
+
+	err = netc_port_add_ipft_ptp_entry(np, NETC_PTP_L4_IPV4_GENERAL);
+	if (err)
+		goto del_ptp_filter;
+
+	err = netc_port_add_ipft_ptp_entry(np, NETC_PTP_L4_IPV6_EVENT);
+	if (err)
+		goto del_ptp_filter;
+
+	err = netc_port_add_ipft_ptp_entry(np, NETC_PTP_L4_IPV6_GENERAL);
+	if (err)
+		goto del_ptp_filter;
+
+	return 0;
+
+del_ptp_filter:
+	netc_port_del_ptp_filter(np);
+
+	return err;
+}
+
+static int netc_port_add_l2_l4_ptp_filter(struct netc_port *np)
+{
+	int err;
+
+	err = netc_port_add_l2_ptp_filter(np);
+	if (err)
+		return err;
+
+	err = netc_port_add_l4_ptp_filter(np);
+	if (err)
+		goto del_ptp_filter;
+
+	return 0;
+
+del_ptp_filter:
+	netc_port_del_ptp_filter(np);
+
+	return err;
+}
+
+static int netc_port_set_ptp_filter(struct netc_port *np, int rx_filter)
+{
+	int err = 0;
+
+	if (np->ptp_rx_filter == rx_filter)
+		return 0;
+
+	if (np->ptp_rx_filter != HWTSTAMP_FILTER_NONE ||
+	    rx_filter == HWTSTAMP_FILTER_NONE) {
+		netc_port_del_ptp_filter(np);
+		np->ptp_rx_filter = HWTSTAMP_FILTER_NONE;
+	}
+
+	switch (rx_filter) {
+	case HWTSTAMP_FILTER_NONE:
+		break;
+	case HWTSTAMP_FILTER_PTP_V2_L2_EVENT:
+		err = netc_port_add_l2_ptp_filter(np);
+		break;
+	case HWTSTAMP_FILTER_PTP_V2_L4_EVENT:
+		err = netc_port_add_l4_ptp_filter(np);
+		break;
+	case HWTSTAMP_FILTER_PTP_V2_EVENT:
+		err = netc_port_add_l2_l4_ptp_filter(np);
+		break;
+	default:
+		err = -ERANGE;
+	}
+
+	if (err)
+		return err;
+
+	np->ptp_rx_filter = rx_filter;
+
+	return 0;
+}
+
+void netc_port_purge_txtstamp_queue(struct netc_port *np)
+{
+	struct sk_buff_head free_list;
+
+	__skb_queue_head_init(&free_list);
+
+	spin_lock_bh(&np->tstamp_lock);
+	skb_queue_splice_init(&np->skb_txtstamp_queue, &free_list);
+	spin_unlock_bh(&np->tstamp_lock);
+
+	__skb_queue_purge(&free_list);
+}
+
+int netc_port_hwtstamp_set(struct dsa_switch *ds, int port,
+			   struct kernel_hwtstamp_config *config,
+			   struct netlink_ext_ack *extack)
+{
+	struct netc_port *np = NETC_PORT(ds, port);
+	struct netc_switch *priv = ds->priv;
+	int rx_filter, err;
+
+	if ((config->tx_type != HWTSTAMP_TX_OFF ||
+	     config->rx_filter != HWTSTAMP_FILTER_NONE) &&
+	    !priv->tmr_dev)
+		return -EOPNOTSUPP;
+
+	switch (config->tx_type) {
+	case HWTSTAMP_TX_ON:
+	case HWTSTAMP_TX_OFF:
+		break;
+	default:
+		return -ERANGE;
+	}
+
+	switch (config->rx_filter) {
+	case HWTSTAMP_FILTER_NONE:
+		rx_filter = HWTSTAMP_FILTER_NONE;
+		break;
+	case HWTSTAMP_FILTER_PTP_V2_L4_EVENT:
+	case HWTSTAMP_FILTER_PTP_V2_L4_SYNC:
+	case HWTSTAMP_FILTER_PTP_V2_L4_DELAY_REQ:
+		rx_filter = HWTSTAMP_FILTER_PTP_V2_L4_EVENT;
+		break;
+	case HWTSTAMP_FILTER_PTP_V2_L2_EVENT:
+	case HWTSTAMP_FILTER_PTP_V2_L2_SYNC:
+	case HWTSTAMP_FILTER_PTP_V2_L2_DELAY_REQ:
+		rx_filter = HWTSTAMP_FILTER_PTP_V2_L2_EVENT;
+		break;
+	case HWTSTAMP_FILTER_PTP_V2_EVENT:
+	case HWTSTAMP_FILTER_PTP_V2_SYNC:
+	case HWTSTAMP_FILTER_PTP_V2_DELAY_REQ:
+		rx_filter = HWTSTAMP_FILTER_PTP_V2_EVENT;
+		break;
+	default:
+		return -ERANGE;
+	}
+
+	err = netc_port_set_ptp_filter(np, rx_filter);
+	if (err) {
+		NL_SET_ERR_MSG_MOD(extack, "Failed to set PTP filter");
+		return err;
+	}
+
+	WRITE_ONCE(np->ptp_tx_type, config->tx_type);
+	if (config->tx_type == HWTSTAMP_TX_OFF)
+		netc_port_purge_txtstamp_queue(np);
+
+	config->rx_filter = rx_filter;
+
+	return 0;
+}
+
+int netc_port_hwtstamp_get(struct dsa_switch *ds, int port,
+			   struct kernel_hwtstamp_config *config)
+{
+	struct netc_port *np = NETC_PORT(ds, port);
+
+	config->tx_type = READ_ONCE(np->ptp_tx_type);
+	config->rx_filter = np->ptp_rx_filter;
+
+	return 0;
+}
+
+static void netc_port_txtstamp_twostep(struct netc_port *np,
+				       struct sk_buff *nskb)
+{
+	DECLARE_BITMAP(ts_req_id_bitmap, NETC_NUM_TS_REQ_ID);
+	struct sk_buff *clone = skb_clone_sk(nskb);
+	struct netc_switch *priv = np->switch_priv;
+	struct sk_buff_head free_list;
+	struct sk_buff *skb, *skb_tmp;
+	unsigned long ts_req_id;
+	int err = 0;
+
+	if (unlikely(!clone))
+		return;
+
+	bitmap_zero(ts_req_id_bitmap, NETC_NUM_TS_REQ_ID);
+	__skb_queue_head_init(&free_list);
+	spin_lock_bh(&np->tstamp_lock);
+
+	skb_queue_walk_safe(&np->skb_txtstamp_queue, skb, skb_tmp) {
+		if (time_before(NETC_SKB_CB(skb)->ptp_tx_time +
+				NETC_TXTSTAMP_TIMEOUT, jiffies)) {
+			dev_dbg_ratelimited(priv->dev,
+					    "Port %d ts_req_id %u which seems lost\n",
+					    np->dp->index, NETC_SKB_CB(skb)->ts_req_id);
+
+			__skb_unlink(skb, &np->skb_txtstamp_queue);
+			__skb_queue_tail(&free_list, skb);
+		} else {
+			__set_bit(NETC_SKB_CB(skb)->ts_req_id, ts_req_id_bitmap);
+		}
+	}
+
+	ts_req_id = find_first_zero_bit(ts_req_id_bitmap, NETC_NUM_TS_REQ_ID);
+	if (ts_req_id == NETC_NUM_TS_REQ_ID) {
+		dev_dbg_ratelimited(priv->dev,
+				    "Port %d has no available ts_req_id\n",
+				    np->dp->index);
+		err = -EBUSY;
+		goto unlock_tstamp;
+	}
+
+	NETC_SKB_CB(nskb)->ptp_flag = NETC_PTP_FLAG_TWOSTEP;
+	NETC_SKB_CB(nskb)->ts_req_id = ts_req_id;
+	NETC_SKB_CB(clone)->ts_req_id = ts_req_id;
+	NETC_SKB_CB(clone)->ptp_tx_time = jiffies;
+	skb_shinfo(clone)->tx_flags |= SKBTX_IN_PROGRESS;
+	__skb_queue_tail(&np->skb_txtstamp_queue, clone);
+
+unlock_tstamp:
+	spin_unlock_bh(&np->tstamp_lock);
+
+	if (err)
+		kfree_skb(clone);
+
+	/* Free timed-out SKBs outside the spinlock to avoid calling
+	 * kfree_skb() with a destructor (sock_efree) under a spinlock.
+	 */
+	__skb_queue_purge(&free_list);
+}
+
+void netc_port_twostep_tstamp_handler(struct dsa_switch *ds, int port,
+				      u8 ts_req_id, u64 ts)
+{
+	struct sk_buff *skb, *skb_tmp, *skb_match = NULL;
+	struct netc_port *np = NETC_PORT(ds, port);
+	struct skb_shared_hwtstamps hwtstamps;
+	struct netc_switch *priv = ds->priv;
+
+	spin_lock_bh(&np->tstamp_lock);
+	skb_queue_walk_safe(&np->skb_txtstamp_queue, skb, skb_tmp) {
+		if (NETC_SKB_CB(skb)->ts_req_id != ts_req_id)
+			continue;
+
+		__skb_unlink(skb, &np->skb_txtstamp_queue);
+		skb_match = skb;
+		break;
+	}
+	spin_unlock_bh(&np->tstamp_lock);
+
+	if (!skb_match) {
+		dev_dbg_ratelimited(priv->dev,
+				    "Port %d ts_req_id %u which seems lost\n",
+				    port, ts_req_id);
+		return;
+	}
+
+	hwtstamps.hwtstamp = ns_to_ktime(ts);
+	skb_complete_tx_timestamp(skb_match, &hwtstamps);
+}
+
+bool netc_port_rxtstamp(struct dsa_switch *ds, int port, struct sk_buff *skb,
+			unsigned int type)
+{
+	struct skb_shared_hwtstamps *hwtstamps = skb_hwtstamps(skb);
+	u64 ts = NETC_SKB_CB(skb)->tstamp;
+
+	/* ts == 0 indicates the hardware did not capture the RX timestamp
+	 * of the frame.
+	 */
+	if (!ts)
+		return false;
+
+	hwtstamps->hwtstamp = ns_to_ktime(ts);
+
+	return false;
+}
+
+void netc_port_txtstamp(struct dsa_switch *ds, int port, struct sk_buff *skb)
+{
+	struct netc_port *np = NETC_PORT(ds, port);
+	u32 ptp_class;
+
+	NETC_SKB_CB(skb)->ptp_flag = 0;
+	ptp_class = ptp_classify_raw(skb);
+	if (ptp_class == PTP_CLASS_NONE)
+		return;
+
+	if (READ_ONCE(np->ptp_tx_type) == HWTSTAMP_TX_ON)
+		netc_port_txtstamp_twostep(np, skb);
+}
diff --git a/drivers/net/dsa/netc/netc_switch.h b/drivers/net/dsa/netc/netc_switch.h
index fd36ec2d0e90..86fd15889733 100644
--- a/drivers/net/dsa/netc/netc_switch.h
+++ b/drivers/net/dsa/netc/netc_switch.h
@@ -53,10 +53,16 @@
 #define NETC_FDBT_AGEING_DELAY		(3 * HZ)
 #define NETC_FDBT_AGEING_THRESH		100
 
+/* PTP frames have a higher priority, so a higher priority is defined to
+ * prioritize matching.
+ */
+#define NETC_IPFT_PTP_PRECEDENCE	0xf000
+
 struct netc_switch;
 
 struct netc_switch_info {
 	u32 num_ports;
+	u32 tmr_devfn;
 	void (*phylink_get_caps)(int port, struct phylink_config *config);
 };
 
@@ -66,9 +72,19 @@ struct netc_port_caps {
 	u32 pseudo_link:1;
 };
 
+enum netc_ptp_type {
+	NETC_PTP_L2,
+	NETC_PTP_L4_IPV4_EVENT,
+	NETC_PTP_L4_IPV4_GENERAL,
+	NETC_PTP_L4_IPV6_EVENT,
+	NETC_PTP_L4_IPV6_GENERAL,
+	NETC_PTP_MAX,
+};
+
 enum netc_host_reason {
 	/* Software defined host reasons */
 	NETC_HR_HOST_FLOOD = 8,
+	NETC_HR_PTP_TRAP   = 9,
 };
 
 struct netc_port {
@@ -85,6 +101,14 @@ struct netc_port {
 	u16 mc:1;
 	u16 pvid;
 	u32 ipft_hf_eid;
+
+	/* Serialize access to skb_txtstamp_queue and ts_req_id */
+	spinlock_t tstamp_lock;
+	/* skb queue for two-step timestamp frames */
+	struct sk_buff_head skb_txtstamp_queue;
+	int ptp_tx_type;
+	int ptp_rx_filter;
+	u32 ptp_ipft_eid[NETC_PTP_MAX];
 };
 
 struct netc_switch_regs {
@@ -139,6 +163,7 @@ struct netc_switch {
 	u32 num_bp;
 
 	struct bpt_cfge_data *bpt_list;
+	struct pci_dev *tmr_dev; /* The PTP Timer PCI device */
 };
 
 #define NETC_PRIV(ds)			((struct netc_switch *)((ds)->priv))
@@ -203,4 +228,20 @@ void netc_port_get_strings(struct dsa_switch *ds, int port,
 			   u32 sset, u8 *data);
 void netc_port_get_ethtool_stats(struct dsa_switch *ds, int port, u64 *data);
 
+/* PTP APIs */
+int netc_port_ptp_init(struct netc_port *np);
+int netc_get_ts_info(struct dsa_switch *ds, int port,
+		     struct kernel_ethtool_ts_info *info);
+void netc_port_purge_txtstamp_queue(struct netc_port *np);
+int netc_port_hwtstamp_set(struct dsa_switch *ds, int port,
+			   struct kernel_hwtstamp_config *config,
+			   struct netlink_ext_ack *extack);
+int netc_port_hwtstamp_get(struct dsa_switch *ds, int port,
+			   struct kernel_hwtstamp_config *config);
+void netc_port_twostep_tstamp_handler(struct dsa_switch *ds, int port,
+				      u8 ts_req_id, u64 ts);
+bool netc_port_rxtstamp(struct dsa_switch *ds, int port, struct sk_buff *skb,
+			unsigned int type);
+void netc_port_txtstamp(struct dsa_switch *ds, int port, struct sk_buff *skb);
+
 #endif
diff --git a/include/linux/dsa/tag_netc.h b/include/linux/dsa/tag_netc.h
index fe964722e5b0..da200e3ba8ad 100644
--- a/include/linux/dsa/tag_netc.h
+++ b/include/linux/dsa/tag_netc.h
@@ -10,5 +10,26 @@
 #include <net/dsa.h>
 
 #define NETC_TAG_MAX_LEN			14
+#define NETC_PTP_FLAG_TWOSTEP			BIT(1)
+
+struct netc_skb_cb {
+	unsigned long ptp_tx_time;
+	u64 tstamp;
+	u8 ptp_flag;
+	u8 ts_req_id;
+};
+
+#define NETC_SKB_CB(skb)	((struct netc_skb_cb *)((skb)->cb))
+
+/**
+ * struct netc_tagger_data - NETC tagger/switch-driver shared operations
+ * @twostep_tstamp_handler: Called by the tagger when a two-step transmit
+ *	timestamp response is received, to deliver the timestamp to the
+ *	switch driver.
+ */
+struct netc_tagger_data {
+	void (*twostep_tstamp_handler)(struct dsa_switch *ds, int port,
+				       u8 ts_req_id, u64 ts);
+};
 
 #endif
diff --git a/net/dsa/tag_netc.c b/net/dsa/tag_netc.c
index df72a61796ad..9f9a61d8133b 100644
--- a/net/dsa/tag_netc.c
+++ b/net/dsa/tag_netc.c
@@ -16,6 +16,8 @@
 #define NETC_TAG_TO_PORT		1
 /* SubType0: No request to perform timestamping */
 #define NETC_TAG_TP_SUBTYPE0		0
+/* SubType2: Request to perform two-step timestamping */
+#define NETC_TAG_TP_SUBTYPE2		2
 
 /* To_Host NXP switch tag */
 #define NETC_TAG_TO_HOST		2
@@ -29,6 +31,7 @@
 /* NETC switch tag lengths */
 #define NETC_TAG_FORWARD_LEN		6
 #define NETC_TAG_TP_SUBTYPE0_LEN	6
+#define NETC_TAG_TP_SUBTYPE2_LEN	6
 #define NETC_TAG_TH_SUBTYPE0_LEN	6
 #define NETC_TAG_TH_SUBTYPE1_LEN	14
 #define NETC_TAG_TH_SUBTYPE2_LEN	14
@@ -40,6 +43,7 @@
 #define NETC_TAG_IPV			GENMASK(4, 2)
 #define NETC_TAG_SWITCH			GENMASK(2, 0)
 #define NETC_TAG_PORT			GENMASK(7, 3)
+#define NETC_TAG_TS_REQ_ID		GENMASK(3, 0)
 
 struct netc_tag_cmn {
 	__be16 tpid;
@@ -48,6 +52,23 @@ struct netc_tag_cmn {
 	u8 switch_port;
 } __packed;
 
+struct netc_tag_tp_subtype2 {
+	struct netc_tag_cmn cmn;
+	u8 ts_req_id;
+} __packed;
+
+struct netc_tag_th_subtype1 {
+	struct netc_tag_cmn cmn;
+	u8 host_reason;
+	__be64 timestamp;
+} __packed;
+
+struct netc_tag_th_subtype2 {
+	struct netc_tag_cmn cmn;
+	u8 hr_tsreq_id;
+	__be64 timestamp;
+} __packed;
+
 static void netc_fill_common_tag(struct netc_tag_cmn *tag, u8 type,
 				 u8 subtype, u8 sw_id, u8 port, u8 ipv)
 {
@@ -97,15 +118,69 @@ static void netc_fill_tp_tag_subtype0(struct sk_buff *skb,
 				NETC_TAG_TP_SUBTYPE0_LEN);
 }
 
-/* Currently only support To_Port tag, subtype 0 */
+static void netc_fill_tp_tag_subtype2(struct sk_buff *skb,
+				      struct net_device *ndev)
+{
+	u8 ts_req_id = NETC_SKB_CB(skb)->ts_req_id;
+	struct netc_tag_tp_subtype2 *tag;
+
+	tag = netc_fill_common_tp_tag(skb, ndev, NETC_TAG_TP_SUBTYPE2,
+				      NETC_TAG_TP_SUBTYPE2_LEN);
+	tag->ts_req_id = FIELD_PREP(NETC_TAG_TS_REQ_ID, ts_req_id);
+}
+
 static struct sk_buff *netc_xmit(struct sk_buff *skb,
 				 struct net_device *ndev)
 {
-	netc_fill_tp_tag_subtype0(skb, ndev);
+	u8 ptp_flag = NETC_SKB_CB(skb)->ptp_flag;
+
+	/* Fast path: the overwhelming majority of frames are not PTP frames */
+	if (likely(!ptp_flag)) {
+		netc_fill_tp_tag_subtype0(skb, ndev);
+		return skb;
+	} else if (ptp_flag == NETC_PTP_FLAG_TWOSTEP) {
+		netc_fill_tp_tag_subtype2(skb, ndev);
+	} else {
+		kfree_skb(skb);
+		return NULL;
+	}
 
 	return skb;
 }
 
+static int netc_rx_tstamp_process(struct netc_tag_th_subtype1 *tag,
+				  struct sk_buff *skb)
+{
+	u64 ts = get_unaligned_be64(&tag->timestamp);
+
+	/* To_Host Subtype 1 tag is 14 bytes, ensure it and the EtherType
+	 * behind it are fully present in the linear area before netc_rcv()
+	 * calls dsa_strip_etype_header() to strip the tag.
+	 */
+	if (unlikely(!pskb_may_pull(skb, NETC_TAG_TH_SUBTYPE1_LEN)))
+		return -ENOBUFS;
+
+	NETC_SKB_CB(skb)->tstamp = ts;
+
+	return 0;
+}
+
+static void netc_twostep_tstamp_process(struct netc_tag_th_subtype2 *tag,
+					struct sk_buff *skb)
+{
+	u8 ts_req_id = FIELD_GET(NETC_TAG_TS_REQ_ID, tag->hr_tsreq_id);
+	struct dsa_port *dp = dsa_user_to_port(skb->dev);
+	u64 ts = get_unaligned_be64(&tag->timestamp);
+	struct netc_tagger_data *tagger_data;
+	struct dsa_switch *ds = dp->ds;
+
+	tagger_data = ds->tagger_data;
+	if (unlikely(!tagger_data->twostep_tstamp_handler))
+		return;
+
+	tagger_data->twostep_tstamp_handler(ds, dp->index, ts_req_id, ts);
+}
+
 static int netc_get_rx_tag_len(int type, int subtype)
 {
 	/* Only NETC_TAG_TO_HOST and NETC_TAG_FORWARD are expected in RX,
@@ -129,11 +204,22 @@ static struct sk_buff *netc_rcv(struct sk_buff *skb,
 	struct netc_tag_cmn *tag_cmn;
 	int tag_len, sw_id, port;
 	int type, subtype;
+	void *tag;
 
-	if (unlikely(!pskb_may_pull(skb, NETC_TAG_MAX_LEN)))
+	/* eth_type_trans() pulled ETH_HLEN bytes, so skb->data sits 2 bytes
+	 * past the start of the switch tag (past the TPID) and skb->len is
+	 * ETH_HLEN bytes shorter than the original frame length. The longest
+	 * switch tag is NETC_TAG_MAX_LEN (14) bytes, but since 2 of those
+	 * bytes are already behind skb->data, only NETC_TAG_MAX_LEN - 2 bytes
+	 * need to be in the linear buffer. For the To_Host subtype 2 response
+	 * frame, whose total length is only 26 bytes with no payload after the
+	 * tag, this check is the only guard against a too-short frame.
+	 */
+	if (unlikely(!pskb_may_pull(skb, NETC_TAG_MAX_LEN - 2)))
 		goto err_free_skb;
 
-	tag_cmn = dsa_etype_header_pos_rx(skb);
+	tag = dsa_etype_header_pos_rx(skb);
+	tag_cmn = tag;
 	if (ntohs(tag_cmn->tpid) != ETH_P_NXP_NETC) {
 		dev_warn_ratelimited(&ndev->dev, "Unknown TPID 0x%04x\n",
 				     ntohs(tag_cmn->tpid));
@@ -156,17 +242,40 @@ static struct sk_buff *netc_rcv(struct sk_buff *skb,
 	if (!skb->dev)
 		goto err_free_skb;
 
+	/* skb->cb may be used to store hardware RX timestamp, so it must
+	 * be cleared before processing to avoid data pollution from the
+	 * previous layer.
+	 */
+	NETC_SKB_CB(skb)->tstamp = 0;
+
 	type = FIELD_GET(NETC_TAG_TYPE, tag_cmn->type);
 	subtype = FIELD_GET(NETC_TAG_SUBTYPE, tag_cmn->type);
 	if (type == NETC_TAG_FORWARD) {
 		dsa_default_offload_fwd_mark(skb);
 	} else if (type == NETC_TAG_TO_HOST) {
-		/* Currently only subtype0 supported */
-		if (subtype != NETC_TAG_TH_SUBTYPE0)
+		switch (subtype) {
+		case NETC_TAG_TH_SUBTYPE0:
+			break;
+		case NETC_TAG_TH_SUBTYPE1:
+			if (netc_rx_tstamp_process(tag, skb))
+				goto err_free_skb;
+			break;
+		case NETC_TAG_TH_SUBTYPE2:
+			/* This skb is a hardware-generated response to a
+			 * two-step transmit timestamp request. The tag
+			 * driver must free the skb after processing.
+			 */
+			netc_twostep_tstamp_process(tag, skb);
+			goto err_free_skb;
+		default:
+			dev_warn_ratelimited(&ndev->dev,
+					     "Unsupported To_Host subtype: %d\n",
+					     subtype);
 			goto err_free_skb;
+		}
 	} else {
 		dev_warn_ratelimited(&ndev->dev,
-				     "Unexpected  tag type %d\n", type);
+				     "Unexpected tag type %d\n", type);
 		goto err_free_skb;
 	}
 
@@ -200,6 +309,27 @@ static void netc_flow_dissect(const struct sk_buff *skb, __be16 *proto,
 	*proto = ((__be16 *)skb->data)[(tag_len / 2) - 1];
 }
 
+static int netc_connect(struct dsa_switch *ds)
+{
+	struct netc_tagger_data *tagger_data;
+
+	tagger_data = kzalloc_obj(*tagger_data);
+	if (!tagger_data)
+		return -ENOMEM;
+
+	ds->tagger_data = tagger_data;
+
+	return 0;
+}
+
+static void netc_disconnect(struct dsa_switch *ds)
+{
+	struct netc_tagger_data *tagger_data = ds->tagger_data;
+
+	kfree(tagger_data);
+	ds->tagger_data = NULL;
+}
+
 static const struct dsa_device_ops netc_netdev_ops = {
 	.name			= NETC_NAME,
 	.proto			= DSA_TAG_PROTO_NETC,
@@ -207,6 +337,8 @@ static const struct dsa_device_ops netc_netdev_ops = {
 	.rcv			= netc_rcv,
 	.needed_headroom	= NETC_TAG_MAX_LEN,
 	.flow_dissect		= netc_flow_dissect,
+	.connect		= netc_connect,
+	.disconnect		= netc_disconnect,
 };
 
 MODULE_DESCRIPTION("DSA tag driver for NXP NETC switch family");
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH v2 net-next 7/7] net: dsa: netc: add PTP one-step timestamping support
  2026-08-08  3:21 [PATCH v2 net-next 0/7] net: dsa: netc: add PTP support for NETC switch wei.fang
                   ` (6 preceding siblings ...)
  2026-08-08  3:21 ` [PATCH v2 net-next 6/7] net: dsa: netc: add PTP two-step timestamping support wei.fang
@ 2026-08-08  3:21 ` wei.fang
       [not found]   ` <20260809031906.C98761F000E9@smtp.kernel.org>
  7 siblings, 1 reply; 14+ messages in thread
From: wei.fang @ 2026-08-08  3:21 UTC (permalink / raw)
  To: xiaoning.wang, andrew, olteanv, andrew+netdev, davem, edumazet,
	kuba, pabeni, horms, richardcochran
  Cc: wei.fang, imx, netdev, linux-kernel, linuxppc-dev,
	linux-arm-kernel

From: Wei Fang <wei.fang@nxp.com>

The NETC switch supports one-step TX timestamping for PTP Sync frames.
The MAC captures the SFD transmit time, adds the residence time to the
correction field at the offset given by PM_SINGLE_STEP[OFFSET], and
writes the result back before the frame leaves the wire. The software
timestamp (low 30 bits of the PTP Timer value) is carried in the
To_Port SubType 1 tag.

PM_SINGLE_STEP is a per-port register that can describe only one
in-flight frame at a time, and programming it requires reading the
current PTP time, which may sleep. Both constraints rule out handling
one-step Sync on the xmit path.

Instead, defer transmission to a per-port process-context work. The
xmit path classifies the frame in netc_port_txtstamp(): a genuine
one-step Sync (twoStepFlag cleared) has its PTP header offsets cached
in the skb control block; frames that cannot be handled as one-step
fall back to the two-step path or are sent as normal frames.
netc_xmit() hands the classified frame to the switch driver via the
onestep_sync_enqueue tagger callback, which queues it and kicks the
work if no frame is currently in flight.

The work dequeues one frame at a time, reads a fresh PTP time,
programs PM_SINGLE_STEP, updates the originTimestamp field, and
transmits the frame directly to the conduit via the onestep_sync_xmit
tagger callback, bypassing dsa_user_xmit() to avoid double-counting
TX stats. Only one frame is in flight at a time: the frame carries a
TX-completion destructor that reschedules the work when the conduit
frees the skb, keeping PM_SINGLE_STEP always matched to the frame
being transmitted.

The one-step context is reference-counted and its lifetime is decoupled
from the devm-allocated netc_port. In-flight skbs hold a reference via
their destructor, so the context outlives port disable until the conduit
frees the last in-flight skb. Port disable clears @active and purges the
queue under work_lock; a work that runs afterwards observes @active
cleared and returns without touching the freed port resources.

Assisted-by: Wchat:claude-opus-4-8
Signed-off-by: Wei Fang <wei.fang@nxp.com>
---
 drivers/net/dsa/netc/netc_main.c      |  61 +++-
 drivers/net/dsa/netc/netc_ptp.c       | 418 +++++++++++++++++++++++++-
 drivers/net/dsa/netc/netc_switch.h    |  48 +++
 drivers/net/dsa/netc/netc_switch_hw.h |   5 +
 include/linux/dsa/tag_netc.h          |  22 ++
 net/dsa/tag_netc.c                    |  70 ++++-
 6 files changed, 615 insertions(+), 9 deletions(-)

diff --git a/drivers/net/dsa/netc/netc_main.c b/drivers/net/dsa/netc/netc_main.c
index 4e139ffc2f76..55664045ba19 100644
--- a/drivers/net/dsa/netc/netc_main.c
+++ b/drivers/net/dsa/netc/netc_main.c
@@ -74,6 +74,7 @@ static int netc_connect_tag_protocol(struct dsa_switch *ds,
 		return -EPROTONOSUPPORT;
 
 	tagger_data = ds->tagger_data;
+	tagger_data->onestep_sync_enqueue = netc_port_onestep_sync_enqueue;
 	tagger_data->twostep_tstamp_handler = netc_port_twostep_tstamp_handler;
 
 	return 0;
@@ -94,7 +95,7 @@ static void netc_port_rmw(struct netc_port *np, u32 reg,
 	netc_port_wr(np, reg, new);
 }
 
-static void netc_mac_port_wr(struct netc_port *np, u32 reg, u32 val)
+void netc_mac_port_wr(struct netc_port *np, u32 reg, u32 val)
 {
 	if (is_netc_pseudo_port(np))
 		return;
@@ -252,6 +253,21 @@ static void netc_get_switch_capabilities(struct netc_switch *priv)
 	priv->num_bp = FIELD_GET(BPCAPR_NUM_BP, val);
 }
 
+static void netc_free_user_ports(struct netc_switch *priv)
+{
+	struct dsa_switch *ds = priv->ds;
+	struct dsa_port *dp;
+
+	dsa_switch_for_each_user_port(dp, ds) {
+		struct netc_port *np = NETC_PORT(ds, dp->index);
+
+		if (np->onestep) {
+			netc_onestep_put(np->onestep);
+			np->onestep = NULL;
+		}
+	}
+}
+
 static int netc_init_all_ports(struct netc_switch *priv)
 {
 	struct device *dev = priv->dev;
@@ -292,13 +308,13 @@ static int netc_init_all_ports(struct netc_switch *priv)
 
 		err = netc_port_get_info_from_dt(np, dp->dn, dev);
 		if (err)
-			return err;
+			goto free_user_ports;
 
 		if (dsa_port_is_user(dp)) {
 			err = netc_port_create_mdio_bus(np, dp->dn);
 			if (err) {
 				dev_err(dev, "Failed to create MDIO bus\n");
-				return err;
+				goto free_user_ports;
 			}
 
 			/* The ipft_hf_eid is initialized to an invalid entry
@@ -314,11 +330,16 @@ static int netc_init_all_ports(struct netc_switch *priv)
 			 */
 			err = netc_port_ptp_init(np);
 			if (err)
-				return err;
+				goto free_user_ports;
 		}
 	}
 
 	return 0;
+
+free_user_ports:
+	netc_free_user_ports(priv);
+
+	return err;
 }
 
 static void netc_init_ntmp_tbl_versions(struct netc_switch *priv)
@@ -941,7 +962,7 @@ static int netc_setup(struct dsa_switch *ds)
 
 	err = netc_init_ntmp_user(priv);
 	if (err)
-		goto put_ptp_timer;
+		goto free_user_ports;
 
 	INIT_HLIST_HEAD(&priv->fdb_list);
 	mutex_init(&priv->fdbt_lock);
@@ -980,6 +1001,8 @@ static int netc_setup(struct dsa_switch *ds)
 	mutex_destroy(&priv->fdbt_lock);
 	mutex_destroy(&priv->vft_lock);
 	netc_free_ntmp_user(priv);
+free_user_ports:
+	netc_free_user_ports(priv);
 put_ptp_timer:
 	pci_dev_put(priv->tmr_dev);
 
@@ -1005,6 +1028,19 @@ static void netc_free_ports_resources(struct netc_switch *priv)
 			continue;
 
 		netc_port_purge_txtstamp_queue(np);
+
+		/* dsa_tree_teardown() calls dsa_tree_teardown_ports() before
+		 * dsa_tree_teardown_switches(), so netc_port_disable() is
+		 * executed before netc_teardown() and purges onestep->queue,
+		 * so here we only need to drop the port's owner reference.
+		 * In-flight one-step skbs still hold references via the
+		 * destructor; the context (and its work) is freed only after
+		 * the conduit frees the last in-flight skb. By then np may
+		 * be gone, but the work no longer dereferences np because
+		 * onestep->active has been cleared.
+		 */
+		netc_onestep_put(np->onestep);
+		np->onestep = NULL;
 	}
 }
 
@@ -1559,6 +1595,7 @@ static int netc_port_enable(struct dsa_switch *ds, int port,
 			    struct phy_device *phy)
 {
 	struct netc_port *np = NETC_PORT(ds, port);
+	struct netc_onestep *onestep = np->onestep;
 	int err;
 
 	if (np->enable)
@@ -1571,6 +1608,12 @@ static int netc_port_enable(struct dsa_switch *ds, int port,
 		return err;
 	}
 
+	if (onestep) {
+		mutex_lock(&onestep->work_lock);
+		onestep->active = true;
+		mutex_unlock(&onestep->work_lock);
+	}
+
 	np->enable = true;
 
 	return 0;
@@ -1579,6 +1622,7 @@ static int netc_port_enable(struct dsa_switch *ds, int port,
 static void netc_port_disable(struct dsa_switch *ds, int port)
 {
 	struct netc_port *np = NETC_PORT(ds, port);
+	struct netc_onestep *onestep = np->onestep;
 
 	/* When .port_disable() is called, .port_enable() may not have been
 	 * called. In this case, both the prepare_count and enable_count of
@@ -1588,6 +1632,13 @@ static void netc_port_disable(struct dsa_switch *ds, int port)
 	if (!np->enable)
 		return;
 
+	if (onestep) {
+		mutex_lock(&onestep->work_lock);
+		onestep->active = false;
+		netc_port_purge_onestep_queue(onestep, true);
+		mutex_unlock(&onestep->work_lock);
+	}
+
 	clk_disable_unprepare(np->ref_clk);
 	np->enable = false;
 }
diff --git a/drivers/net/dsa/netc/netc_ptp.c b/drivers/net/dsa/netc/netc_ptp.c
index 1384a6f31d1c..d0423b3c8c33 100644
--- a/drivers/net/dsa/netc/netc_ptp.c
+++ b/drivers/net/dsa/netc/netc_ptp.c
@@ -4,13 +4,270 @@
  * Copyright 2025-2026 NXP
  */
 
+#include <linux/kref.h>
 #include <linux/ptp_classify.h>
 #include <linux/ptp_clock_kernel.h>
+#include <linux/slab.h>
 
 #include "netc_switch.h"
 
 #define NETC_NUM_TS_REQ_ID		16
 #define NETC_TXTSTAMP_TIMEOUT		(5 * HZ)
+#define NETC_MAX_STEP_OFFSET		0x1ff
+
+static void netc_port_set_onestep_control(struct netc_port *np,
+					  bool csum_update, int offset)
+{
+	u32 val;
+
+	val = PM_SINGLE_STEP_EN | FIELD_PREP(PM_SINGLE_STEP_OFFSET, offset);
+	if (csum_update)
+		val |= PM_SINGLE_STEP_CH;
+	netc_mac_port_wr(np, NETC_PM_SINGLE_STEP(0), val);
+}
+
+static void netc_onestep_destroy_work(struct work_struct *work)
+{
+	struct netc_onestep *onestep = container_of(work, struct netc_onestep,
+						    destroy_work);
+
+	/* refcnt reaching zero does not by itself mean onestep->work has
+	 * stopped: the last in-flight skb destructor calls schedule_work(&work)
+	 * *before* the netc_onestep_put() that drops the final reference, so at
+	 * the moment refcnt hits zero onestep->work may still be pending or
+	 * running on another CPU. destroy_work and work are distinct work_structs
+	 * and can run concurrently, so cancel_work_sync() is required to drain
+	 * onestep->work before mutex_destroy()/kfree() below, otherwise a
+	 * still-running work would touch freed memory. No new schedule_work(&work)
+	 * can occur after this point because no references remain, so this
+	 * cancel is final.
+	 */
+	cancel_work_sync(&onestep->work);
+	mutex_destroy(&onestep->work_lock);
+	kfree(onestep);
+}
+
+static void netc_onestep_release(struct kref *ref)
+{
+	struct netc_onestep *onestep = container_of(ref, struct netc_onestep,
+						    refcnt);
+
+	/* This may be called from the skb destructor in softirq context
+	 * (napi_consume_skb()), where cancel_work_sync() must not be used.
+	 * Defer the final teardown to process context.
+	 */
+	schedule_work(&onestep->destroy_work);
+}
+
+static void netc_onestep_get(struct netc_onestep *onestep)
+{
+	kref_get(&onestep->refcnt);
+}
+
+void netc_onestep_put(struct netc_onestep *onestep)
+{
+	kref_put(&onestep->refcnt, netc_onestep_release);
+}
+
+static void netc_onestep_skb_destructor(struct sk_buff *skb)
+{
+	struct netc_onestep *onestep = skb_shinfo(skb)->destructor_arg;
+
+	/* skb has been transmitted by hardware. Schedule work to send the next
+	 * queued one-step Sync packet, then release this skb's reference on the
+	 * context. If the port has already been torn down and this is the last
+	 * reference, the context is freed via netc_onestep_release().
+	 */
+	schedule_work(&onestep->work);
+	netc_onestep_put(onestep);
+}
+
+static void netc_port_program_onestep(struct netc_port *np,
+				      struct netc_onestep *onestep,
+				      struct sk_buff *skb,
+				      u64 tstamp)
+{
+	u16 correction_offset = NETC_SKB_CB(skb)->correction_offset;
+	u16 tstamp_offset = NETC_SKB_CB(skb)->timestamp_offset;
+	u8 *hdr = skb_mac_header(skb);
+	bool csum_update = false;
+	__be32 new_sec_l, new_ns;
+	__be16 new_sec_h;
+	u64 sec;
+	u32 ns;
+
+	NETC_SKB_CB(skb)->tstamp = tstamp;
+	NETC_SKB_CB(skb)->ptp_flag = NETC_PTP_FLAG_ONESTEP;
+
+	/* Update originTimestamp field of Sync packet
+	 * - 48 bits seconds field
+	 * - 32 bits nanoseconds field
+	 */
+	sec = div_u64_rem(tstamp, NSEC_PER_SEC, &ns);
+	new_sec_h = htons((sec >> 32) & 0xffff);
+	new_sec_l = htonl(sec & 0xffffffff);
+	new_ns = htonl(ns);
+
+	if (NETC_SKB_CB(skb)->is_udp) {
+		__be32 old_sec_l, old_ns;
+		struct udphdr *uh;
+		__be16 old_sec_h;
+
+		if (skb->ip_summed == CHECKSUM_PARTIAL) {
+			csum_update = true;
+			goto update_timestamp;
+		}
+
+		if (unlikely(!skb_transport_header_was_set(skb)))
+			uh = (struct udphdr *)(hdr + tstamp_offset -
+					       sizeof(struct ptp_header) -
+					       sizeof(struct udphdr));
+		else
+			uh = udp_hdr(skb);
+
+		/* For IPv4, a UDP checksum of zero on the wire means "no
+		 * checksum". For IPv6, its UDP checksum is mandatory and
+		 * never zero.
+		 */
+		if (!uh->check)
+			goto update_timestamp;
+
+		old_sec_h = __get_unaligned_t(__be16, hdr + tstamp_offset);
+		old_sec_l = __get_unaligned_t(__be32, hdr + tstamp_offset + 2);
+		old_ns = __get_unaligned_t(__be32, hdr + tstamp_offset + 6);
+		inet_proto_csum_replace2(&uh->check, skb, old_sec_h,
+					 new_sec_h, false);
+		inet_proto_csum_replace4(&uh->check, skb, old_sec_l,
+					 new_sec_l, false);
+		inet_proto_csum_replace4(&uh->check, skb, old_ns,
+					 new_ns, false);
+		csum_update = true;
+	}
+
+update_timestamp:
+	__put_unaligned_t(__be16, new_sec_h, hdr + tstamp_offset);
+	__put_unaligned_t(__be32, new_sec_l, hdr + tstamp_offset + 2);
+	__put_unaligned_t(__be32, new_ns, hdr + tstamp_offset + 6);
+
+	netc_port_set_onestep_control(np, csum_update, correction_offset);
+
+	/* Orphan the skb to release the socket send buffer quota immediately.
+	 * This is safe because sock_wfree() does not access skb->data or any
+	 * frame content. After skb_orphan(), we install our own destructor so
+	 * that when the conduit driver frees the skb after TX completion, we
+	 * get notified to send the next queued Sync packet.
+	 */
+	skb_orphan(skb);
+	netc_onestep_get(onestep); /* in-flight reference */
+	skb_shinfo(skb)->destructor_arg = onestep;
+	skb->destructor = netc_onestep_skb_destructor;
+}
+
+static u64 netc_get_phc_time(struct netc_switch *priv)
+{
+	if (unlikely(!priv->tmr_dev))
+		return 0;
+
+	return netc_timer_get_current_time(priv->tmr_dev);
+}
+
+void netc_port_onestep_work(struct work_struct *work)
+{
+	struct netc_onestep *onestep = container_of(work, struct netc_onestep,
+						    work);
+	struct netc_tagger_data *tagger_data;
+	struct netc_switch *priv;
+	struct netc_port *np;
+	struct sk_buff *skb;
+	u64 tstamp;
+
+	/* Serialize the whole hardware access against port disable. work_lock
+	 * is a mutex (this runs in process context and netc_get_phc_time() may
+	 * sleep). If the port has been disabled, bail out immediately; np and
+	 * priv are only dereferenced after the @active check passes, so they
+	 * are always valid here.
+	 */
+	mutex_lock(&onestep->work_lock);
+	if (unlikely(!onestep->active)) {
+		netc_port_purge_onestep_queue(onestep, true);
+		goto unlock_work;
+	}
+
+	/* Send only one queued Sync per run. The shared SINGLE_STEP register
+	 * must match the frame currently being transmitted, so the next frame
+	 * is programmed only after this one completes TX, when its skb
+	 * destructor reschedules this work. Dequeue under onestep->queue_lock,
+	 * and if the queue has drained, release the in-flight slot so a later
+	 * frame from the xmit path kicks the work again.
+	 */
+	spin_lock_bh(&onestep->queue_lock);
+	skb = __skb_dequeue(&onestep->queue);
+	if (!skb) {
+		onestep->in_flight = false;
+		spin_unlock_bh(&onestep->queue_lock);
+		goto unlock_work;
+	}
+	spin_unlock_bh(&onestep->queue_lock);
+
+	np = onestep->np;
+	priv = np->switch_priv;
+	tstamp = netc_get_phc_time(priv);
+	if (unlikely(!tstamp)) {
+		/* The PTP timer is not available, so there is no correct
+		 * timestamp to program. Drop this frame and re-kick to process
+		 * the remaining queued frames.
+		 *
+		 * netc_port_program_onestep() has not run for this skb yet, so
+		 * netc_onestep_skb_destructor() is not installed on it. Freeing
+		 * it therefore does not reschedule the work, so the work must be
+		 * rescheduled explicitly to keep draining the queue.
+		 */
+		dev_dbg_ratelimited(priv->dev,
+				    "Port %d PTP timer unavailable, drop Sync\n",
+				    np->dp->index);
+		kfree_skb(skb);
+		schedule_work(&onestep->work);
+		goto unlock_work;
+	}
+
+	/* Reuse the offsets cached at enqueue time; only the timestamp is
+	 * read fresh so it reflects the actual TX moment.
+	 */
+	netc_port_program_onestep(np, onestep, skb, tstamp);
+
+	/* Tag and hand the frame directly to the conduit via the tagger,
+	 * bypassing dsa_user_xmit() so the TX stats are not counted twice.
+	 * And there is no need to check if tagger_data is NULL, because
+	 * dsa_tree_teardown_ports() executes before
+	 * dsa_switch_teardown_tag_protocol(), so tagger_data cannot be
+	 * NULL when onestep->active is set.
+	 */
+	tagger_data = priv->ds->tagger_data;
+	tagger_data->onestep_sync_xmit(skb, np->dp->user);
+
+unlock_work:
+	mutex_unlock(&onestep->work_lock);
+}
+
+static int netc_port_onestep_alloc(struct netc_port *np)
+{
+	struct netc_onestep *onestep;
+
+	onestep = kzalloc_obj(*onestep);
+	if (!onestep)
+		return -ENOMEM;
+
+	kref_init(&onestep->refcnt); /* port (owner) reference */
+	np->onestep = onestep;
+	onestep->np = np;
+	mutex_init(&onestep->work_lock);
+	spin_lock_init(&onestep->queue_lock);
+	__skb_queue_head_init(&onestep->queue);
+	INIT_WORK(&onestep->work, netc_port_onestep_work);
+	INIT_WORK(&onestep->destroy_work, netc_onestep_destroy_work);
+
+	return 0;
+}
 
 int netc_port_ptp_init(struct netc_port *np)
 {
@@ -21,7 +278,7 @@ int netc_port_ptp_init(struct netc_port *np)
 	spin_lock_init(&np->tstamp_lock);
 	__skb_queue_head_init(&np->skb_txtstamp_queue);
 
-	return 0;
+	return netc_port_onestep_alloc(np);
 }
 
 static int netc_get_phc_index(struct netc_switch *priv)
@@ -45,7 +302,8 @@ int netc_get_ts_info(struct dsa_switch *ds, int port,
 				 SOF_TIMESTAMPING_RX_HARDWARE |
 				 SOF_TIMESTAMPING_RAW_HARDWARE;
 
-	info->tx_types = BIT(HWTSTAMP_TX_OFF) | BIT(HWTSTAMP_TX_ON);
+	info->tx_types = BIT(HWTSTAMP_TX_OFF) | BIT(HWTSTAMP_TX_ON) |
+			 BIT(HWTSTAMP_TX_ONESTEP_SYNC);
 
 	info->rx_filters = BIT(HWTSTAMP_FILTER_NONE) |
 			   BIT(HWTSTAMP_FILTER_PTP_V2_EVENT) |
@@ -262,6 +520,22 @@ void netc_port_purge_txtstamp_queue(struct netc_port *np)
 	__skb_queue_purge(&free_list);
 }
 
+void netc_port_purge_onestep_queue(struct netc_onestep *onestep,
+				   bool clear_flight)
+{
+	struct sk_buff_head free_list;
+
+	__skb_queue_head_init(&free_list);
+
+	spin_lock_bh(&onestep->queue_lock);
+	skb_queue_splice_init(&onestep->queue, &free_list);
+	if (clear_flight)
+		onestep->in_flight = false;
+	spin_unlock_bh(&onestep->queue_lock);
+
+	__skb_queue_purge(&free_list);
+}
+
 int netc_port_hwtstamp_set(struct dsa_switch *ds, int port,
 			   struct kernel_hwtstamp_config *config,
 			   struct netlink_ext_ack *extack)
@@ -278,6 +552,7 @@ int netc_port_hwtstamp_set(struct dsa_switch *ds, int port,
 	switch (config->tx_type) {
 	case HWTSTAMP_TX_ON:
 	case HWTSTAMP_TX_OFF:
+	case HWTSTAMP_TX_ONESTEP_SYNC:
 		break;
 	default:
 		return -ERANGE;
@@ -316,6 +591,9 @@ int netc_port_hwtstamp_set(struct dsa_switch *ds, int port,
 	if (config->tx_type == HWTSTAMP_TX_OFF)
 		netc_port_purge_txtstamp_queue(np);
 
+	if (config->tx_type != HWTSTAMP_TX_ONESTEP_SYNC)
+		netc_port_purge_onestep_queue(np->onestep, false);
+
 	config->rx_filter = rx_filter;
 
 	return 0;
@@ -439,9 +717,100 @@ bool netc_port_rxtstamp(struct dsa_switch *ds, int port, struct sk_buff *skb,
 	return false;
 }
 
+static void netc_port_prepare_onestep_sync(struct netc_port *np,
+					   struct sk_buff *skb,
+					   u32 ptp_class, bool *twostep)
+{
+	struct netc_switch *priv = np->switch_priv;
+	u16 correction_offset, tstamp_offset;
+	struct ptp_header *ptp_hdr;
+	u8 msg_type, twostep_flag;
+	bool is_udp = false;
+	u32 pkt_type;
+	u8 *pkt_hdr;
+
+	if (unlikely(skb_linearize(skb))) {
+		NETC_SKB_CB(skb)->ptp_flag = NETC_PTP_FLAG_DROP;
+		return;
+	}
+
+	ptp_hdr = ptp_parse_header(skb, ptp_class);
+	if (unlikely(!ptp_hdr)) {
+		NETC_SKB_CB(skb)->ptp_flag = NETC_PTP_FLAG_DROP;
+		dev_dbg_ratelimited(priv->dev,
+				    "Port %d failed to parse Sync header\n",
+				    np->dp->index);
+		return;
+	}
+
+	msg_type = ptp_get_msgtype(ptp_hdr, ptp_class);
+	twostep_flag = ptp_hdr->flag_field[0] & 0x2;
+
+	pkt_hdr = skb_mac_header(skb);
+	correction_offset = (u8 *)&ptp_hdr->correction - pkt_hdr;
+	tstamp_offset = (u8 *)ptp_hdr + sizeof(*ptp_hdr) - pkt_hdr;
+
+	/* Ensure that the entire originTimestamp field is present in the
+	 * linear buffer of the skb and the correction_offset must be within
+	 * the hardware capability.
+	 */
+	if (unlikely(tstamp_offset + 10 > skb_headlen(skb) ||
+		     correction_offset > NETC_MAX_STEP_OFFSET)) {
+		NETC_SKB_CB(skb)->ptp_flag = NETC_PTP_FLAG_DROP;
+		dev_dbg_ratelimited(priv->dev,
+				    "Port %d PTP offset check error\n",
+				    np->dp->index);
+		return;
+	}
+
+	/* Only a Sync frame with the twoStepFlag cleared can use one-step
+	 * timestamping. A frame that requests two-step (or is not a Sync)
+	 * carries different on-wire fields, so this is a real classification;
+	 * report it through *twostep so the caller falls back to the two-step
+	 * path.
+	 */
+	if (msg_type != PTP_MSGTYPE_SYNC || twostep_flag != 0) {
+		*twostep = true;
+		return;
+	}
+
+	/* This is a genuine one-step Sync frame. skb_shinfo()->destructor_arg
+	 * is later used to pass the np->onestep pointer to
+	 * netc_onestep_skb_destructor() for TX completion notification.
+	 * MSG_ZEROCOPY also uses destructor_arg (via skb_zcopy_init()) to
+	 * track user-space page references. Overwriting it in that case would
+	 * leak the ubuf_info reference and prevent user pages from being
+	 * released. PTP applications do not use MSG_ZEROCOPY, but guard
+	 * against it defensively.
+	 */
+	if (skb_zcopy(skb)) {
+		NETC_SKB_CB(skb)->ptp_flag = NETC_PTP_FLAG_DROP;
+		dev_dbg_ratelimited(priv->dev,
+				    "Port %d one-step Sync not supported on zerocopy skb\n",
+				    np->dp->index);
+		return;
+	}
+
+	pkt_type = ptp_class & PTP_CLASS_PMASK;
+	if (pkt_type == PTP_CLASS_IPV4 || pkt_type == PTP_CLASS_IPV6)
+		is_udp = true;
+
+	/* Cache the parsing results so the tagger xmit path and the deferred
+	 * work do not need to re-parse the PTP header, and so that
+	 * netc_port_program_onestep() can derive these parameters from the
+	 * skb.
+	 */
+	NETC_SKB_CB(skb)->correction_offset = correction_offset;
+	NETC_SKB_CB(skb)->timestamp_offset = tstamp_offset;
+	NETC_SKB_CB(skb)->is_udp = is_udp;
+	NETC_SKB_CB(skb)->ptp_flag = NETC_PTP_FLAG_ONESTEP;
+}
+
 void netc_port_txtstamp(struct dsa_switch *ds, int port, struct sk_buff *skb)
 {
 	struct netc_port *np = NETC_PORT(ds, port);
+	int tx_type = READ_ONCE(np->ptp_tx_type);
+	bool twostep = false;
 	u32 ptp_class;
 
 	NETC_SKB_CB(skb)->ptp_flag = 0;
@@ -449,6 +818,49 @@ void netc_port_txtstamp(struct dsa_switch *ds, int port, struct sk_buff *skb)
 	if (ptp_class == PTP_CLASS_NONE)
 		return;
 
-	if (READ_ONCE(np->ptp_tx_type) == HWTSTAMP_TX_ON)
+	if (tx_type == HWTSTAMP_TX_ONESTEP_SYNC)
+		netc_port_prepare_onestep_sync(np, skb, ptp_class, &twostep);
+
+	if (tx_type == HWTSTAMP_TX_ON || twostep)
 		netc_port_txtstamp_twostep(np, skb);
 }
+
+void netc_port_onestep_sync_enqueue(struct dsa_switch *ds, int port,
+				    struct sk_buff *skb)
+{
+	struct netc_port *np = NETC_PORT(ds, port);
+	struct netc_onestep *onestep = np->onestep;
+	bool kick = false;
+
+	/* This runs in the xmit path (softirq / BH-disabled), so it must not
+	 * sleep: only queue the frame here and let netc_port_onestep_work()
+	 * program the SINGLE_STEP register and transmit it from process
+	 * context. The shared SINGLE_STEP register can describe only one frame
+	 * at a time, so at most one one-step Sync may be in flight. Track that
+	 * with @in_flight under onestep->queue_lock.
+	 *
+	 * Enqueue the frame and, only if no frame is currently in flight, claim
+	 * the in-flight slot and kick the work. When a frame is already in
+	 * flight, just queue: its skb destructor will kick the work to send the
+	 * next one once it completes TX, so the frames are transmitted strictly
+	 * one at a time in order.
+	 *
+	 * PTP Sync frames are periodic, low-rate control-plane frames and only
+	 * reach this TX path when the local socket requested hardware TX
+	 * timestamping on a one-step port, so the queue cannot be flooded and
+	 * needs no depth cap.
+	 */
+	spin_lock_bh(&onestep->queue_lock);
+	__skb_queue_tail(&onestep->queue, skb);
+	if (!onestep->in_flight) {
+		onestep->in_flight = true;
+		kick = true;
+	}
+	spin_unlock_bh(&onestep->queue_lock);
+
+	/* Ownership is transferred to the queue; netc_xmit() stops processing
+	 * this skb. The work will program and transmit it.
+	 */
+	if (kick)
+		schedule_work(&onestep->work);
+}
diff --git a/drivers/net/dsa/netc/netc_switch.h b/drivers/net/dsa/netc/netc_switch.h
index 86fd15889733..98d4842441df 100644
--- a/drivers/net/dsa/netc/netc_switch.h
+++ b/drivers/net/dsa/netc/netc_switch.h
@@ -9,6 +9,7 @@
 #include <linux/dsa/tag_netc.h>
 #include <linux/fsl/netc_global.h>
 #include <linux/fsl/ntmp.h>
+#include <linux/mutex.h>
 #include <linux/of_device.h>
 #include <linux/of_net.h>
 #include <linux/pci.h>
@@ -87,6 +88,44 @@ enum netc_host_reason {
 	NETC_HR_PTP_TRAP   = 9,
 };
 
+/* One-step Sync serialization context.
+ *
+ * Its lifetime is decoupled from the devm-allocated netc_port. An in-flight
+ * one-step Sync skb keeps a reference on this context via its skb destructor,
+ * so the context outlives the port teardown until the conduit frees the last
+ * in-flight skb after TX completion. Once the port is disabled, @active is
+ * cleared and the work stops touching any devm memory (netc_port/netc_switch)
+ * or the unregistered user netdev or the tagger_data.
+ */
+struct netc_onestep {
+	struct netc_port *np;
+	struct kref refcnt;
+	/* Process-context lock: serializes the deferred TX work against port
+	 * teardown, so the work never touches the devm-allocated netc_port /
+	 * netc_switch or the unregistered user netdev after teardown. Held
+	 * across netc_get_phc_time(), which may sleep, hence a mutex.
+	 */
+	struct mutex work_lock;
+	/* Serialize access to in_flight and queue */
+	spinlock_t queue_lock;
+	bool active;	/* set when port is enabled, under @work_lock */
+	/* In-flight slot: true while one one-step Sync frame is programmed
+	 * into the shared SINGLE_STEP register and being transmitted. Only one
+	 * frame may be in flight at a time, so the next queued frame is sent
+	 * only after the current one completes TX (its skb destructor kicks
+	 * the work). Accessed under queue_lock, from both the softirq xmit
+	 * path and the process-context work.
+	 */
+	bool in_flight;
+	/* Pending one-step Sync frames. Enqueued from the softirq xmit path and
+	 * dequeued by the process-context work; the list is serialized by
+	 * queue_lock together with @in_flight.
+	 */
+	struct sk_buff_head queue;
+	struct work_struct work;	/* drains @queue */
+	struct work_struct destroy_work; /* frees the context in process ctx */
+};
+
 struct netc_port {
 	void __iomem *iobase;
 	struct netc_switch *switch_priv;
@@ -106,6 +145,8 @@ struct netc_port {
 	spinlock_t tstamp_lock;
 	/* skb queue for two-step timestamp frames */
 	struct sk_buff_head skb_txtstamp_queue;
+	/* one-step Sync serialization context (ref-counted, kzalloc'd) */
+	struct netc_onestep *onestep;
 	int ptp_tx_type;
 	int ptp_rx_filter;
 	u32 ptp_ipft_eid[NETC_PTP_MAX];
@@ -212,6 +253,7 @@ static inline void netc_del_vlan_entry(struct netc_vlan_entry *entry)
 }
 
 int netc_switch_platform_probe(struct netc_switch *priv);
+void netc_mac_port_wr(struct netc_port *np, u32 reg, u32 val);
 
 /* ethtool APIs */
 void netc_port_get_pause_stats(struct dsa_switch *ds, int port,
@@ -243,5 +285,11 @@ void netc_port_twostep_tstamp_handler(struct dsa_switch *ds, int port,
 bool netc_port_rxtstamp(struct dsa_switch *ds, int port, struct sk_buff *skb,
 			unsigned int type);
 void netc_port_txtstamp(struct dsa_switch *ds, int port, struct sk_buff *skb);
+void netc_onestep_put(struct netc_onestep *onestep);
+void netc_port_purge_onestep_queue(struct netc_onestep *onestep,
+				   bool clear_flight);
+void netc_port_onestep_work(struct work_struct *work);
+void netc_port_onestep_sync_enqueue(struct dsa_switch *ds, int port,
+				    struct sk_buff *skb);
 
 #endif
diff --git a/drivers/net/dsa/netc/netc_switch_hw.h b/drivers/net/dsa/netc/netc_switch_hw.h
index 1404ae41c7bc..37d1dd7ec2c7 100644
--- a/drivers/net/dsa/netc/netc_switch_hw.h
+++ b/drivers/net/dsa/netc/netc_switch_hw.h
@@ -203,6 +203,11 @@ enum netc_stg_stage {
 #define   SSP_10M			1
 #define   SSP_1G			2
 
+#define NETC_PM_SINGLE_STEP(a)		(0x10c0 + (a) * 0x400)
+#define  PM_SINGLE_STEP_CH		BIT(6)
+#define  PM_SINGLE_STEP_OFFSET		GENMASK(15, 7)
+#define  PM_SINGLE_STEP_EN		BIT(31)
+
 /* Port MAC 0/1 Receive Ethernet Octets Counter */
 #define NETC_PM_REOCT(a)		(0x1100 + (a) * 0x400)
 
diff --git a/include/linux/dsa/tag_netc.h b/include/linux/dsa/tag_netc.h
index da200e3ba8ad..5ac5e2e72dff 100644
--- a/include/linux/dsa/tag_netc.h
+++ b/include/linux/dsa/tag_netc.h
@@ -10,13 +10,23 @@
 #include <net/dsa.h>
 
 #define NETC_TAG_MAX_LEN			14
+#define NETC_PTP_FLAG_ONESTEP			BIT(0)
 #define NETC_PTP_FLAG_TWOSTEP			BIT(1)
+#define NETC_PTP_FLAG_DROP			BIT(2)
 
 struct netc_skb_cb {
 	unsigned long ptp_tx_time;
 	u64 tstamp;
 	u8 ptp_flag;
 	u8 ts_req_id;
+	/* One-step Sync parsing results, computed in netc_port_txtstamp()
+	 * and reused in the tagger xmit path and the deferred work, to avoid
+	 * re-parsing the PTP header. Valid only while
+	 * ptp_flag == NETC_PTP_FLAG_ONESTEP.
+	 */
+	u16 correction_offset;
+	u16 timestamp_offset;
+	bool is_udp;
 };
 
 #define NETC_SKB_CB(skb)	((struct netc_skb_cb *)((skb)->cb))
@@ -26,10 +36,22 @@ struct netc_skb_cb {
  * @twostep_tstamp_handler: Called by the tagger when a two-step transmit
  *	timestamp response is received, to deliver the timestamp to the
  *	switch driver.
+ * @onestep_sync_enqueue: Called from the tagger xmit path for a one-step Sync
+ *	frame. The switch driver takes ownership of the skb and queues it for
+ *	deferred transmission from process context, where the shared
+ *	PM_SINGLE_STEP register can be programmed and the PTP timer read
+ *	(which may sleep). The tagger must not touch the skb after this call
+ *	and returns NULL to dsa_user_xmit().
+ * @onestep_sync_xmit: Called by the switch driver to transmit a deferred
+ *	one-step Sync frame directly to the conduit, bypassing dsa_user_xmit().
  */
 struct netc_tagger_data {
 	void (*twostep_tstamp_handler)(struct dsa_switch *ds, int port,
 				       u8 ts_req_id, u64 ts);
+	void (*onestep_sync_enqueue)(struct dsa_switch *ds, int port,
+				     struct sk_buff *skb);
+	netdev_tx_t (*onestep_sync_xmit)(struct sk_buff *skb,
+					 struct net_device *ndev);
 };
 
 #endif
diff --git a/net/dsa/tag_netc.c b/net/dsa/tag_netc.c
index 9f9a61d8133b..91548fb7ed1b 100644
--- a/net/dsa/tag_netc.c
+++ b/net/dsa/tag_netc.c
@@ -16,6 +16,8 @@
 #define NETC_TAG_TO_PORT		1
 /* SubType0: No request to perform timestamping */
 #define NETC_TAG_TP_SUBTYPE0		0
+/* SubType1: Request to perform one-step timestamping */
+#define NETC_TAG_TP_SUBTYPE1		1
 /* SubType2: Request to perform two-step timestamping */
 #define NETC_TAG_TP_SUBTYPE2		2
 
@@ -31,6 +33,7 @@
 /* NETC switch tag lengths */
 #define NETC_TAG_FORWARD_LEN		6
 #define NETC_TAG_TP_SUBTYPE0_LEN	6
+#define NETC_TAG_TP_SUBTYPE1_LEN	10
 #define NETC_TAG_TP_SUBTYPE2_LEN	6
 #define NETC_TAG_TH_SUBTYPE0_LEN	6
 #define NETC_TAG_TH_SUBTYPE1_LEN	14
@@ -44,6 +47,7 @@
 #define NETC_TAG_SWITCH			GENMASK(2, 0)
 #define NETC_TAG_PORT			GENMASK(7, 3)
 #define NETC_TAG_TS_REQ_ID		GENMASK(3, 0)
+#define NETC_TAG_TIMESTAMP		GENMASK(29, 0)
 
 struct netc_tag_cmn {
 	__be16 tpid;
@@ -52,6 +56,12 @@ struct netc_tag_cmn {
 	u8 switch_port;
 } __packed;
 
+struct netc_tag_tp_subtype1 {
+	struct netc_tag_cmn cmn;
+	u8 resv;
+	__be32 timestamp;
+} __packed;
+
 struct netc_tag_tp_subtype2 {
 	struct netc_tag_cmn cmn;
 	u8 ts_req_id;
@@ -118,6 +128,17 @@ static void netc_fill_tp_tag_subtype0(struct sk_buff *skb,
 				NETC_TAG_TP_SUBTYPE0_LEN);
 }
 
+static void netc_fill_tp_tag_subtype1(struct sk_buff *skb,
+				      struct net_device *ndev)
+{
+	u32 ts = FIELD_PREP(NETC_TAG_TIMESTAMP, NETC_SKB_CB(skb)->tstamp);
+	struct netc_tag_tp_subtype1 *tag;
+
+	tag = netc_fill_common_tp_tag(skb, ndev, NETC_TAG_TP_SUBTYPE1,
+				      NETC_TAG_TP_SUBTYPE1_LEN);
+	tag->timestamp = htonl(ts);
+}
+
 static void netc_fill_tp_tag_subtype2(struct sk_buff *skb,
 				      struct net_device *ndev)
 {
@@ -129,6 +150,42 @@ static void netc_fill_tp_tag_subtype2(struct sk_buff *skb,
 	tag->ts_req_id = FIELD_PREP(NETC_TAG_TS_REQ_ID, ts_req_id);
 }
 
+static netdev_tx_t netc_onestep_sync_xmit(struct sk_buff *skb,
+					  struct net_device *dev)
+{
+	/* This deferred one-step Sync frame already went through
+	 * dsa_user_xmit()'s skb_ensure_writable_head_tail() and eth_skb_pad()
+	 * before it was queued in netc_xmit(), and nothing has cloned it or
+	 * shrunk its head/tail room since. So the head/tail room is still
+	 * guaranteed and the skb is still writable; only the tag needs to be
+	 * pushed before handing it directly to the conduit, bypassing
+	 * dsa_user_xmit() so that dev_sw_netstats_tx_add() is not invoked a
+	 * second time for the same frame.
+	 */
+	netc_fill_tp_tag_subtype1(skb, dev);
+
+	return dsa_enqueue_skb(skb, dev);
+}
+
+static void netc_onestep_sync_enqueue(struct sk_buff *skb,
+				      struct net_device *ndev)
+{
+	struct dsa_port *dp = dsa_user_to_port(ndev);
+	struct netc_tagger_data *tagger_data;
+
+	tagger_data = dp->ds->tagger_data;
+	if (unlikely(!tagger_data->onestep_sync_enqueue)) {
+		kfree_skb(skb);
+		return;
+	}
+
+	/* Hand the one-step Sync to the switch driver, which takes ownership
+	 * and queues it for deferred transmission from its work. The tagger
+	 * must not touch the skb after this point.
+	 */
+	tagger_data->onestep_sync_enqueue(dp->ds, dp->index, skb);
+}
+
 static struct sk_buff *netc_xmit(struct sk_buff *skb,
 				 struct net_device *ndev)
 {
@@ -138,9 +195,19 @@ static struct sk_buff *netc_xmit(struct sk_buff *skb,
 	if (likely(!ptp_flag)) {
 		netc_fill_tp_tag_subtype0(skb, ndev);
 		return skb;
+	}
+
+	if (ptp_flag == NETC_PTP_FLAG_ONESTEP) {
+		/* The switch driver takes ownership of the one-step Sync and
+		 * queues it for deferred TX; the deferred work tags it subtype 1
+		 * and transmits it directly to the conduit. Return NULL so
+		 * dsa_user_xmit() stops processing this skb.
+		 */
+		netc_onestep_sync_enqueue(skb, ndev);
+		return NULL;
 	} else if (ptp_flag == NETC_PTP_FLAG_TWOSTEP) {
 		netc_fill_tp_tag_subtype2(skb, ndev);
-	} else {
+	} else { /* NETC_PTP_FLAG_DROP */
 		kfree_skb(skb);
 		return NULL;
 	}
@@ -317,6 +384,7 @@ static int netc_connect(struct dsa_switch *ds)
 	if (!tagger_data)
 		return -ENOMEM;
 
+	tagger_data->onestep_sync_xmit = netc_onestep_sync_xmit;
 	ds->tagger_data = tagger_data;
 
 	return 0;
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* RE: [PATCH] net: dsa: netc: add PTP one-step timestamping support
  2026-08-08  3:21 ` [PATCH] net: dsa: netc: add PTP one-step timestamping support wei.fang
@ 2026-08-08  3:26   ` Wei Fang
  0 siblings, 0 replies; 14+ messages in thread
From: Wei Fang @ 2026-08-08  3:26 UTC (permalink / raw)
  To: Wei Fang (OSS), Clark Wang, andrew@lunn.ch, olteanv@gmail.com,
	andrew+netdev@lunn.ch, davem@davemloft.net, edumazet@google.com,
	kuba@kernel.org, pabeni@redhat.com, horms@kernel.org,
	richardcochran@gmail.com
  Cc: imx@lists.linux.dev, netdev@vger.kernel.org,
	linux-kernel@vger.kernel.org, linuxppc-dev@lists.ozlabs.org,
	linux-arm-kernel@lists.infradead.org

Hi all,

I accidentally included this patch in the series ― it was left over in
my local branch and should not have been sent. Please disregard it.

Sorry for the noise.

Best Regards,
Wei Fang

> -----Original Message-----
> From: Wei Fang (OSS) <wei.fang@oss.nxp.com>
> Sent: 2026年8月8日 11:22
> To: Clark Wang <xiaoning.wang@nxp.com>; andrew@lunn.ch;
> olteanv@gmail.com; andrew+netdev@lunn.ch; davem@davemloft.net;
> edumazet@google.com; kuba@kernel.org; pabeni@redhat.com;
> horms@kernel.org; richardcochran@gmail.com
> Cc: Wei Fang <wei.fang@nxp.com>; imx@lists.linux.dev;
> netdev@vger.kernel.org; linux-kernel@vger.kernel.org;
> linuxppc-dev@lists.ozlabs.org; linux-arm-kernel@lists.infradead.org
> Subject: [PATCH] net: dsa: netc: add PTP one-step timestamping support
> 
> From: Wei Fang <wei.fang@nxp.com>
> 
> The NETC switch supports one-step TX timestamping for PTP Sync frames.
> The MAC captures the SFD transmit time, adds the residence time to the
> correction field at the offset given by PM_SINGLE_STEP[OFFSET], and
> writes the result back before the frame leaves the wire. The software
> timestamp (low 30 bits of the PTP Timer value) is carried in the
> To_Port SubType 1 tag.
> 
> PM_SINGLE_STEP is a per-port register that can describe only one
> in-flight frame at a time, and programming it requires reading the
> current PTP time, which may sleep. Both constraints rule out handling
> one-step Sync on the xmit path.
> 
> Instead, defer transmission to a per-port process-context work. The
> xmit path classifies the frame in netc_port_txtstamp(): a genuine
> one-step Sync (twoStepFlag cleared) has its PTP header offsets cached
> in the skb control block; frames that cannot be handled as one-step
> fall back to the two-step path or are sent as normal frames.
> netc_xmit() hands the classified frame to the switch driver via the
> onestep_sync_enqueue tagger callback, which queues it and kicks the
> work if no frame is currently in flight.
> 
> The work dequeues one frame at a time, reads a fresh PTP time,
> programs PM_SINGLE_STEP, updates the originTimestamp field, and
> transmits the frame directly to the conduit via the onestep_sync_xmit
> tagger callback, bypassing dsa_user_xmit() to avoid double-counting
> TX stats. Only one frame is in flight at a time: the frame carries a
> TX-completion destructor that reschedules the work when the conduit
> frees the skb, keeping PM_SINGLE_STEP always matched to the frame
> being transmitted.
> 
> The one-step context is reference-counted and its lifetime is decoupled
> from the devm-allocated netc_port. In-flight skbs hold a reference via
> their destructor, so the context outlives port disable until the conduit
> frees the last in-flight skb. Port disable clears @active and purges the
> queue under work_lock; a work that runs afterwards observes @active
> cleared and returns without touching the freed port resources.
> 
> Assisted-by: Wchat:claude-opus-4-8
> Signed-off-by: Wei Fang <wei.fang@nxp.com>
> ---
>  drivers/net/dsa/netc/netc_main.c      |  61 +++-
>  drivers/net/dsa/netc/netc_ptp.c       | 410
> +++++++++++++++++++++++++-
>  drivers/net/dsa/netc/netc_switch.h    |  48 +++
>  drivers/net/dsa/netc/netc_switch_hw.h |   5 +
>  include/linux/dsa/tag_netc.h          |  21 ++
>  net/dsa/tag_netc.c                    |  68 +++++
>  6 files changed, 605 insertions(+), 8 deletions(-)
> 
> diff --git a/drivers/net/dsa/netc/netc_main.c
> b/drivers/net/dsa/netc/netc_main.c
> index 4e139ffc2f76..967f20a94d99 100644
> --- a/drivers/net/dsa/netc/netc_main.c
> +++ b/drivers/net/dsa/netc/netc_main.c
> @@ -74,6 +74,7 @@ static int netc_connect_tag_protocol(struct dsa_switch
> *ds,
>  		return -EPROTONOSUPPORT;
> 
>  	tagger_data = ds->tagger_data;
> +	tagger_data->onestep_sync_enqueue =
> netc_port_onestep_sync_enqueue;
>  	tagger_data->twostep_tstamp_handler =
> netc_port_twostep_tstamp_handler;
> 
>  	return 0;
> @@ -94,7 +95,7 @@ static void netc_port_rmw(struct netc_port *np, u32 reg,
>  	netc_port_wr(np, reg, new);
>  }
> 
> -static void netc_mac_port_wr(struct netc_port *np, u32 reg, u32 val)
> +void netc_mac_port_wr(struct netc_port *np, u32 reg, u32 val)
>  {
>  	if (is_netc_pseudo_port(np))
>  		return;
> @@ -252,6 +253,21 @@ static void netc_get_switch_capabilities(struct
> netc_switch *priv)
>  	priv->num_bp = FIELD_GET(BPCAPR_NUM_BP, val);
>  }
> 
> +static void netc_free_user_ports(struct netc_switch *priv)
> +{
> +	struct dsa_switch *ds = priv->ds;
> +	struct dsa_port *dp;
> +
> +	dsa_switch_for_each_user_port(dp, ds) {
> +		struct netc_port *np = NETC_PORT(ds, dp->index);
> +
> +		if (!np->onestep) {
> +			netc_onestep_put(np->onestep);
> +			np->onestep = NULL;
> +		}
> +	}
> +}
> +
>  static int netc_init_all_ports(struct netc_switch *priv)
>  {
>  	struct device *dev = priv->dev;
> @@ -292,13 +308,13 @@ static int netc_init_all_ports(struct netc_switch
> *priv)
> 
>  		err = netc_port_get_info_from_dt(np, dp->dn, dev);
>  		if (err)
> -			return err;
> +			goto free_user_ports;
> 
>  		if (dsa_port_is_user(dp)) {
>  			err = netc_port_create_mdio_bus(np, dp->dn);
>  			if (err) {
>  				dev_err(dev, "Failed to create MDIO bus\n");
> -				return err;
> +				goto free_user_ports;
>  			}
> 
>  			/* The ipft_hf_eid is initialized to an invalid entry
> @@ -314,11 +330,16 @@ static int netc_init_all_ports(struct netc_switch
> *priv)
>  			 */
>  			err = netc_port_ptp_init(np);
>  			if (err)
> -				return err;
> +				goto free_user_ports;
>  		}
>  	}
> 
>  	return 0;
> +
> +free_user_ports:
> +	netc_free_user_ports(priv);
> +
> +	return err;
>  }
> 
>  static void netc_init_ntmp_tbl_versions(struct netc_switch *priv)
> @@ -941,7 +962,7 @@ static int netc_setup(struct dsa_switch *ds)
> 
>  	err = netc_init_ntmp_user(priv);
>  	if (err)
> -		goto put_ptp_timer;
> +		goto free_user_ports;
> 
>  	INIT_HLIST_HEAD(&priv->fdb_list);
>  	mutex_init(&priv->fdbt_lock);
> @@ -980,6 +1001,8 @@ static int netc_setup(struct dsa_switch *ds)
>  	mutex_destroy(&priv->fdbt_lock);
>  	mutex_destroy(&priv->vft_lock);
>  	netc_free_ntmp_user(priv);
> +free_user_ports:
> +	netc_free_user_ports(priv);
>  put_ptp_timer:
>  	pci_dev_put(priv->tmr_dev);
> 
> @@ -1005,6 +1028,19 @@ static void netc_free_ports_resources(struct
> netc_switch *priv)
>  			continue;
> 
>  		netc_port_purge_txtstamp_queue(np);
> +
> +		/* dsa_tree_teardown() calls dsa_tree_teardown_ports() before
> +		 * dsa_tree_teardown_switches(), so netc_port_disable() is
> +		 * executed before netc_teardown() and purges onestep->queue,
> +		 * so here we only need to drop the port's owner reference.
> +		 * In-flight one-step skbs still hold references via the
> +		 * destructor; the context (and its work) is freed only after
> +		 * the conduit frees the last in-flight skb. By then np may
> +		 * be gone, but the work no longer dereferences np because
> +		 * onestep->active has been cleared.
> +		 */
> +		netc_onestep_put(np->onestep);
> +		np->onestep = NULL;
>  	}
>  }
> 
> @@ -1559,6 +1595,7 @@ static int netc_port_enable(struct dsa_switch *ds,
> int port,
>  			    struct phy_device *phy)
>  {
>  	struct netc_port *np = NETC_PORT(ds, port);
> +	struct netc_onestep *onestep = np->onestep;
>  	int err;
> 
>  	if (np->enable)
> @@ -1571,6 +1608,12 @@ static int netc_port_enable(struct dsa_switch *ds,
> int port,
>  		return err;
>  	}
> 
> +	if (onestep) {
> +		mutex_lock(&onestep->work_lock);
> +		onestep->active = true;
> +		mutex_unlock(&onestep->work_lock);
> +	}
> +
>  	np->enable = true;
> 
>  	return 0;
> @@ -1579,6 +1622,7 @@ static int netc_port_enable(struct dsa_switch *ds,
> int port,
>  static void netc_port_disable(struct dsa_switch *ds, int port)
>  {
>  	struct netc_port *np = NETC_PORT(ds, port);
> +	struct netc_onestep *onestep = np->onestep;
> 
>  	/* When .port_disable() is called, .port_enable() may not have been
>  	 * called. In this case, both the prepare_count and enable_count of
> @@ -1588,6 +1632,13 @@ static void netc_port_disable(struct dsa_switch
> *ds, int port)
>  	if (!np->enable)
>  		return;
> 
> +	if (onestep) {
> +		mutex_lock(&onestep->work_lock);
> +		onestep->active = false;
> +		netc_port_purge_onestep_queue(onestep, true);
> +		mutex_unlock(&onestep->work_lock);
> +	}
> +
>  	clk_disable_unprepare(np->ref_clk);
>  	np->enable = false;
>  }
> diff --git a/drivers/net/dsa/netc/netc_ptp.c b/drivers/net/dsa/netc/netc_ptp.c
> index 1384a6f31d1c..881b07b616ca 100644
> --- a/drivers/net/dsa/netc/netc_ptp.c
> +++ b/drivers/net/dsa/netc/netc_ptp.c
> @@ -4,14 +4,270 @@
>   * Copyright 2025-2026 NXP
>   */
> 
> +#include <linux/kref.h>
>  #include <linux/ptp_classify.h>
>  #include <linux/ptp_clock_kernel.h>
> +#include <linux/slab.h>
> 
>  #include "netc_switch.h"
> 
>  #define NETC_NUM_TS_REQ_ID		16
>  #define NETC_TXTSTAMP_TIMEOUT		(5 * HZ)
> 
> +static void netc_port_set_onestep_control(struct netc_port *np,
> +					  bool csum_update, int offset)
> +{
> +	u32 val;
> +
> +	val = PM_SINGLE_STEP_EN | FIELD_PREP(PM_SINGLE_STEP_OFFSET,
> offset);
> +	if (csum_update)
> +		val |= PM_SINGLE_STEP_CH;
> +	netc_mac_port_wr(np, NETC_PM_SINGLE_STEP(0), val);
> +}
> +
> +static void netc_onestep_destroy_work(struct work_struct *work)
> +{
> +	struct netc_onestep *onestep = container_of(work, struct netc_onestep,
> +						    destroy_work);
> +
> +	/* refcnt reaching zero does not by itself mean onestep->work has
> +	 * stopped: the last in-flight skb destructor calls schedule_work(&work)
> +	 * *before* the netc_onestep_put() that drops the final reference, so at
> +	 * the moment refcnt hits zero onestep->work may still be pending or
> +	 * running on another CPU. destroy_work and work are distinct
> work_structs
> +	 * and can run concurrently, so cancel_work_sync() is required to drain
> +	 * onestep->work before mutex_destroy()/kfree() below, otherwise a
> +	 * still-running work would touch freed memory. No new
> schedule_work(&work)
> +	 * can occur after this point because no references remain, so this
> +	 * cancel is final.
> +	 */
> +	cancel_work_sync(&onestep->work);
> +	mutex_destroy(&onestep->work_lock);
> +	kfree(onestep);
> +}
> +
> +static void netc_onestep_release(struct kref *ref)
> +{
> +	struct netc_onestep *onestep = container_of(ref, struct netc_onestep,
> +						    refcnt);
> +
> +	/* This may be called from the skb destructor in softirq context
> +	 * (napi_consume_skb()), where cancel_work_sync() must not be used.
> +	 * Defer the final teardown to process context.
> +	 */
> +	schedule_work(&onestep->destroy_work);
> +}
> +
> +static void netc_onestep_get(struct netc_onestep *onestep)
> +{
> +	kref_get(&onestep->refcnt);
> +}
> +
> +void netc_onestep_put(struct netc_onestep *onestep)
> +{
> +	kref_put(&onestep->refcnt, netc_onestep_release);
> +}
> +
> +static void netc_onestep_skb_destructor(struct sk_buff *skb)
> +{
> +	struct netc_onestep *onestep = skb_shinfo(skb)->destructor_arg;
> +
> +	/* skb has been transmitted by hardware. Schedule work to send the next
> +	 * queued one-step Sync packet, then release this skb's reference on the
> +	 * context. If the port has already been torn down and this is the last
> +	 * reference, the context is freed via netc_onestep_release().
> +	 */
> +	schedule_work(&onestep->work);
> +	netc_onestep_put(onestep);
> +}
> +
> +static void netc_port_program_onestep(struct netc_port *np,
> +				      struct netc_onestep *onestep,
> +				      struct sk_buff *skb,
> +				      u64 tstamp)
> +{
> +	u16 correction_offset = NETC_SKB_CB(skb)->correction_offset;
> +	u16 tstamp_offset = NETC_SKB_CB(skb)->timestamp_offset;
> +	u8 *hdr = skb_mac_header(skb);
> +	bool csum_update = false;
> +	__be32 new_sec_l, new_ns;
> +	__be16 new_sec_h;
> +	u64 sec;
> +	u32 ns;
> +
> +	NETC_SKB_CB(skb)->tstamp = tstamp;
> +	NETC_SKB_CB(skb)->ptp_flag = NETC_PTP_FLAG_ONESTEP;
> +
> +	/* Update originTimestamp field of Sync packet
> +	 * - 48 bits seconds field
> +	 * - 32 bits nanoseconds field
> +	 */
> +	sec = div_u64_rem(tstamp, NSEC_PER_SEC, &ns);
> +	new_sec_h = htons((sec >> 32) & 0xffff);
> +	new_sec_l = htonl(sec & 0xffffffff);
> +	new_ns = htonl(ns);
> +
> +	if (NETC_SKB_CB(skb)->is_udp) {
> +		__be32 old_sec_l, old_ns;
> +		struct udphdr *uh;
> +		__be16 old_sec_h;
> +
> +		if (skb->ip_summed == CHECKSUM_PARTIAL) {
> +			csum_update = true;
> +			goto update_timestamp;
> +		}
> +
> +		if (unlikely(!skb_transport_header_was_set(skb)))
> +			uh = (struct udphdr *)(hdr + tstamp_offset -
> +					       sizeof(struct ptp_header) -
> +					       sizeof(struct udphdr));
> +		else
> +			uh = udp_hdr(skb);
> +
> +		/* For IPv4, a UDP checksum of zero on the wire means "no
> +		 * checksum". For IPv6, its UDP checksum is mandatory and
> +		 * never zero.
> +		 */
> +		if (!uh->check)
> +			goto update_timestamp;
> +
> +		old_sec_h = __get_unaligned_t(__be16, hdr + tstamp_offset);
> +		old_sec_l = __get_unaligned_t(__be32, hdr + tstamp_offset + 2);
> +		old_ns = __get_unaligned_t(__be32, hdr + tstamp_offset + 6);
> +		inet_proto_csum_replace2(&uh->check, skb, old_sec_h,
> +					 new_sec_h, false);
> +		inet_proto_csum_replace4(&uh->check, skb, old_sec_l,
> +					 new_sec_l, false);
> +		inet_proto_csum_replace4(&uh->check, skb, old_ns,
> +					 new_ns, false);
> +		csum_update = true;
> +	}
> +
> +update_timestamp:
> +	__put_unaligned_t(__be16, new_sec_h, hdr + tstamp_offset);
> +	__put_unaligned_t(__be32, new_sec_l, hdr + tstamp_offset + 2);
> +	__put_unaligned_t(__be32, new_ns, hdr + tstamp_offset + 6);
> +
> +	netc_port_set_onestep_control(np, csum_update, correction_offset);
> +
> +	/* Orphan the skb to release the socket send buffer quota immediately.
> +	 * This is safe because sock_wfree() does not access skb->data or any
> +	 * frame content. After skb_orphan(), we install our own destructor so
> +	 * that when the conduit driver frees the skb after TX completion, we
> +	 * get notified to send the next queued Sync packet.
> +	 */
> +	skb_orphan(skb);
> +	netc_onestep_get(onestep); /* in-flight reference */
> +	skb_shinfo(skb)->destructor_arg = onestep;
> +	skb->destructor = netc_onestep_skb_destructor;
> +}
> +
> +static u64 netc_get_phc_time(struct netc_switch *priv)
> +{
> +	if (unlikely(!priv->tmr_dev))
> +		return 0;
> +
> +	return netc_timer_get_current_time(priv->tmr_dev);
> +}
> +
> +void netc_port_onestep_work(struct work_struct *work)
> +{
> +	struct netc_onestep *onestep = container_of(work, struct netc_onestep,
> +						    work);
> +	struct netc_tagger_data *tagger_data;
> +	struct netc_switch *priv;
> +	struct netc_port *np;
> +	struct sk_buff *skb;
> +	u64 tstamp;
> +
> +	/* Serialize the whole hardware access against port disable. work_lock
> +	 * is a mutex (this runs in process context and netc_get_phc_time() may
> +	 * sleep). If the port has been disabled, bail out immediately; np and
> +	 * priv are only dereferenced after the @active check passes, so they
> +	 * are always valid here.
> +	 */
> +	mutex_lock(&onestep->work_lock);
> +	if (unlikely(!onestep->active)) {
> +		netc_port_purge_onestep_queue(onestep, true);
> +		goto unlock_work;
> +	}
> +
> +	/* Send only one queued Sync per run. The shared SINGLE_STEP register
> +	 * must match the frame currently being transmitted, so the next frame
> +	 * is programmed only after this one completes TX, when its skb
> +	 * destructor reschedules this work. Dequeue under
> onestep->queue_lock,
> +	 * and if the queue has drained, release the in-flight slot so a later
> +	 * frame from the xmit path kicks the work again.
> +	 */
> +	spin_lock_bh(&onestep->queue_lock);
> +	skb = __skb_dequeue(&onestep->queue);
> +	if (!skb) {
> +		onestep->in_flight = false;
> +		spin_unlock_bh(&onestep->queue_lock);
> +		goto unlock_work;
> +	}
> +	spin_unlock_bh(&onestep->queue_lock);
> +
> +	np = onestep->np;
> +	priv = np->switch_priv;
> +	tstamp = netc_get_phc_time(priv);
> +	if (unlikely(!tstamp)) {
> +		/* The PTP timer is not available, so there is no correct
> +		 * timestamp to program. Drop this frame and re-kick to process
> +		 * the remaining queued frames.
> +		 *
> +		 * netc_port_program_onestep() has not run for this skb yet, so
> +		 * netc_onestep_skb_destructor() is not installed on it. Freeing
> +		 * it therefore does not reschedule the work, so the work must be
> +		 * rescheduled explicitly to keep draining the queue.
> +		 */
> +		dev_dbg_ratelimited(priv->dev,
> +				    "Port %d PTP timer unavailable, drop Sync\n",
> +				    np->dp->index);
> +		kfree_skb(skb);
> +		schedule_work(&onestep->work);
> +		goto unlock_work;
> +	}
> +
> +	/* Reuse the offsets cached at enqueue time; only the timestamp is
> +	 * read fresh so it reflects the actual TX moment.
> +	 */
> +	netc_port_program_onestep(np, onestep, skb, tstamp);
> +
> +	/* Tag and hand the frame directly to the conduit via the tagger,
> +	 * bypassing dsa_user_xmit() so the TX stats are not counted twice.
> +	 * And there is no need to check if tagger_data is NULL, because
> +	 * dsa_tree_teardown_ports() executes before
> +	 * dsa_switch_teardown_tag_protocol(), so tagger_data cannot be
> +	 * NULL when onestep->active is set.
> +	 */
> +	tagger_data = priv->ds->tagger_data;
> +	tagger_data->onestep_sync_xmit(skb, np->dp->user);
> +
> +unlock_work:
> +	mutex_unlock(&onestep->work_lock);
> +}
> +
> +static int netc_port_onestep_alloc(struct netc_port *np)
> +{
> +	struct netc_onestep *onestep;
> +
> +	onestep = kzalloc_obj(*onestep);
> +	if (!onestep)
> +		return -ENOMEM;
> +
> +	kref_init(&onestep->refcnt); /* port (owner) reference */
> +	np->onestep = onestep;
> +	onestep->np = np;
> +	mutex_init(&onestep->work_lock);
> +	spin_lock_init(&onestep->queue_lock);
> +	__skb_queue_head_init(&onestep->queue);
> +	INIT_WORK(&onestep->work, netc_port_onestep_work);
> +	INIT_WORK(&onestep->destroy_work, netc_onestep_destroy_work);
> +
> +	return 0;
> +}
> +
>  int netc_port_ptp_init(struct netc_port *np)
>  {
>  	/* Initialize to invalid entry IDs */
> @@ -21,7 +277,7 @@ int netc_port_ptp_init(struct netc_port *np)
>  	spin_lock_init(&np->tstamp_lock);
>  	__skb_queue_head_init(&np->skb_txtstamp_queue);
> 
> -	return 0;
> +	return netc_port_onestep_alloc(np);
>  }
> 
>  static int netc_get_phc_index(struct netc_switch *priv)
> @@ -45,7 +301,8 @@ int netc_get_ts_info(struct dsa_switch *ds, int port,
>  				 SOF_TIMESTAMPING_RX_HARDWARE |
>  				 SOF_TIMESTAMPING_RAW_HARDWARE;
> 
> -	info->tx_types = BIT(HWTSTAMP_TX_OFF) | BIT(HWTSTAMP_TX_ON);
> +	info->tx_types = BIT(HWTSTAMP_TX_OFF) | BIT(HWTSTAMP_TX_ON) |
> +			 BIT(HWTSTAMP_TX_ONESTEP_SYNC);
> 
>  	info->rx_filters = BIT(HWTSTAMP_FILTER_NONE) |
>  			   BIT(HWTSTAMP_FILTER_PTP_V2_EVENT) |
> @@ -262,6 +519,22 @@ void netc_port_purge_txtstamp_queue(struct
> netc_port *np)
>  	__skb_queue_purge(&free_list);
>  }
> 
> +void netc_port_purge_onestep_queue(struct netc_onestep *onestep,
> +				   bool clear_flight)
> +{
> +	struct sk_buff_head free_list;
> +
> +	__skb_queue_head_init(&free_list);
> +
> +	spin_lock_bh(&onestep->queue_lock);
> +	skb_queue_splice_init(&onestep->queue, &free_list);
> +	if (clear_flight)
> +		onestep->in_flight = false;
> +	spin_unlock_bh(&onestep->queue_lock);
> +
> +	__skb_queue_purge(&free_list);
> +}
> +
>  int netc_port_hwtstamp_set(struct dsa_switch *ds, int port,
>  			   struct kernel_hwtstamp_config *config,
>  			   struct netlink_ext_ack *extack)
> @@ -278,6 +551,7 @@ int netc_port_hwtstamp_set(struct dsa_switch *ds, int
> port,
>  	switch (config->tx_type) {
>  	case HWTSTAMP_TX_ON:
>  	case HWTSTAMP_TX_OFF:
> +	case HWTSTAMP_TX_ONESTEP_SYNC:
>  		break;
>  	default:
>  		return -ERANGE;
> @@ -316,6 +590,9 @@ int netc_port_hwtstamp_set(struct dsa_switch *ds, int
> port,
>  	if (config->tx_type == HWTSTAMP_TX_OFF)
>  		netc_port_purge_txtstamp_queue(np);
> 
> +	if (config->tx_type != HWTSTAMP_TX_ONESTEP_SYNC)
> +		netc_port_purge_onestep_queue(np->onestep, false);
> +
>  	config->rx_filter = rx_filter;
> 
>  	return 0;
> @@ -439,9 +716,93 @@ bool netc_port_rxtstamp(struct dsa_switch *ds, int
> port, struct sk_buff *skb,
>  	return false;
>  }
> 
> +static void netc_port_prepare_onestep_sync(struct netc_port *np,
> +					   struct sk_buff *skb,
> +					   u32 ptp_class, bool *twostep)
> +{
> +	struct netc_switch *priv = np->switch_priv;
> +	u16 correction_offset, tstamp_offset;
> +	struct ptp_header *ptp_hdr;
> +	u8 msg_type, twostep_flag;
> +	bool is_udp = false;
> +	u32 pkt_type;
> +	u8 *pkt_hdr;
> +
> +	if (unlikely(skb_linearize(skb)))
> +		return;
> +
> +	ptp_hdr = ptp_parse_header(skb, ptp_class);
> +	if (unlikely(!ptp_hdr)) {
> +		dev_dbg_ratelimited(priv->dev,
> +				    "Port %d failed to parse Sync header\n",
> +				    np->dp->index);
> +		return;
> +	}
> +
> +	msg_type = ptp_get_msgtype(ptp_hdr, ptp_class);
> +	twostep_flag = ptp_hdr->flag_field[0] & 0x2;
> +
> +	pkt_hdr = skb_mac_header(skb);
> +	correction_offset = (u8 *)&ptp_hdr->correction - pkt_hdr;
> +	tstamp_offset = (u8 *)ptp_hdr + sizeof(*ptp_hdr) - pkt_hdr;
> +
> +	/* Ensure that the entire originTimestamp field is present in the
> +	 * linear buffer of the skb.
> +	 */
> +	if (unlikely(tstamp_offset + 10 > skb_headlen(skb))) {
> +		dev_dbg_ratelimited(priv->dev,
> +				    "Port %d Sync header not in linear area\n",
> +				    np->dp->index);
> +		return;
> +	}
> +
> +	/* Only a Sync frame with the twoStepFlag cleared can use one-step
> +	 * timestamping. A frame that requests two-step (or is not a Sync)
> +	 * carries different on-wire fields, so this is a real classification;
> +	 * report it through *twostep so the caller falls back to the two-step
> +	 * path.
> +	 */
> +	if (msg_type != PTP_MSGTYPE_SYNC || twostep_flag != 0) {
> +		*twostep = true;
> +		return;
> +	}
> +
> +	/* This is a genuine one-step Sync frame. skb_shinfo()->destructor_arg
> +	 * is later used to pass the np->onestep pointer to
> +	 * netc_onestep_skb_destructor() for TX completion notification.
> +	 * MSG_ZEROCOPY also uses destructor_arg (via skb_zcopy_init()) to
> +	 * track user-space page references. Overwriting it in that case would
> +	 * leak the ubuf_info reference and prevent user pages from being
> +	 * released. PTP applications do not use MSG_ZEROCOPY, but guard
> +	 * against it defensively.
> +	 */
> +	if (skb_zcopy(skb)) {
> +		dev_dbg_ratelimited(priv->dev,
> +				    "Port %d one-step Sync not supported on zerocopy
> skb\n",
> +				    np->dp->index);
> +		return;
> +	}
> +
> +	pkt_type = ptp_class & PTP_CLASS_PMASK;
> +	if (pkt_type == PTP_CLASS_IPV4 || pkt_type == PTP_CLASS_IPV6)
> +		is_udp = true;
> +
> +	/* Cache the parsing results so the tagger xmit path and the deferred
> +	 * work do not need to re-parse the PTP header, and so that
> +	 * netc_port_program_onestep() can derive these parameters from the
> +	 * skb.
> +	 */
> +	NETC_SKB_CB(skb)->correction_offset = correction_offset;
> +	NETC_SKB_CB(skb)->timestamp_offset = tstamp_offset;
> +	NETC_SKB_CB(skb)->is_udp = is_udp;
> +	NETC_SKB_CB(skb)->ptp_flag = NETC_PTP_FLAG_ONESTEP;
> +}
> +
>  void netc_port_txtstamp(struct dsa_switch *ds, int port, struct sk_buff *skb)
>  {
>  	struct netc_port *np = NETC_PORT(ds, port);
> +	int tx_type = READ_ONCE(np->ptp_tx_type);
> +	bool twostep = false;
>  	u32 ptp_class;
> 
>  	NETC_SKB_CB(skb)->ptp_flag = 0;
> @@ -449,6 +810,49 @@ void netc_port_txtstamp(struct dsa_switch *ds, int
> port, struct sk_buff *skb)
>  	if (ptp_class == PTP_CLASS_NONE)
>  		return;
> 
> -	if (READ_ONCE(np->ptp_tx_type) == HWTSTAMP_TX_ON)
> +	if (tx_type == HWTSTAMP_TX_ONESTEP_SYNC)
> +		netc_port_prepare_onestep_sync(np, skb, ptp_class, &twostep);
> +
> +	if (tx_type == HWTSTAMP_TX_ON || twostep)
>  		netc_port_txtstamp_twostep(np, skb);
>  }
> +
> +void netc_port_onestep_sync_enqueue(struct dsa_switch *ds, int port,
> +				    struct sk_buff *skb)
> +{
> +	struct netc_port *np = NETC_PORT(ds, port);
> +	struct netc_onestep *onestep = np->onestep;
> +	bool kick = false;
> +
> +	/* This runs in the xmit path (softirq / BH-disabled), so it must not
> +	 * sleep: only queue the frame here and let netc_port_onestep_work()
> +	 * program the SINGLE_STEP register and transmit it from process
> +	 * context. The shared SINGLE_STEP register can describe only one frame
> +	 * at a time, so at most one one-step Sync may be in flight. Track that
> +	 * with @in_flight under onestep->queue_lock.
> +	 *
> +	 * Enqueue the frame and, only if no frame is currently in flight, claim
> +	 * the in-flight slot and kick the work. When a frame is already in
> +	 * flight, just queue: its skb destructor will kick the work to send the
> +	 * next one once it completes TX, so the frames are transmitted strictly
> +	 * one at a time in order.
> +	 *
> +	 * PTP Sync frames are periodic, low-rate control-plane frames and only
> +	 * reach this TX path when the local socket requested hardware TX
> +	 * timestamping on a one-step port, so the queue cannot be flooded and
> +	 * needs no depth cap.
> +	 */
> +	spin_lock_bh(&onestep->queue_lock);
> +	__skb_queue_tail(&onestep->queue, skb);
> +	if (!onestep->in_flight) {
> +		onestep->in_flight = true;
> +		kick = true;
> +	}
> +	spin_unlock_bh(&onestep->queue_lock);
> +
> +	/* Ownership is transferred to the queue; netc_xmit() stops processing
> +	 * this skb. The work will program and transmit it.
> +	 */
> +	if (kick)
> +		schedule_work(&onestep->work);
> +}
> diff --git a/drivers/net/dsa/netc/netc_switch.h
> b/drivers/net/dsa/netc/netc_switch.h
> index 86fd15889733..98d4842441df 100644
> --- a/drivers/net/dsa/netc/netc_switch.h
> +++ b/drivers/net/dsa/netc/netc_switch.h
> @@ -9,6 +9,7 @@
>  #include <linux/dsa/tag_netc.h>
>  #include <linux/fsl/netc_global.h>
>  #include <linux/fsl/ntmp.h>
> +#include <linux/mutex.h>
>  #include <linux/of_device.h>
>  #include <linux/of_net.h>
>  #include <linux/pci.h>
> @@ -87,6 +88,44 @@ enum netc_host_reason {
>  	NETC_HR_PTP_TRAP   = 9,
>  };
> 
> +/* One-step Sync serialization context.
> + *
> + * Its lifetime is decoupled from the devm-allocated netc_port. An in-flight
> + * one-step Sync skb keeps a reference on this context via its skb destructor,
> + * so the context outlives the port teardown until the conduit frees the last
> + * in-flight skb after TX completion. Once the port is disabled, @active is
> + * cleared and the work stops touching any devm memory
> (netc_port/netc_switch)
> + * or the unregistered user netdev or the tagger_data.
> + */
> +struct netc_onestep {
> +	struct netc_port *np;
> +	struct kref refcnt;
> +	/* Process-context lock: serializes the deferred TX work against port
> +	 * teardown, so the work never touches the devm-allocated netc_port /
> +	 * netc_switch or the unregistered user netdev after teardown. Held
> +	 * across netc_get_phc_time(), which may sleep, hence a mutex.
> +	 */
> +	struct mutex work_lock;
> +	/* Serialize access to in_flight and queue */
> +	spinlock_t queue_lock;
> +	bool active;	/* set when port is enabled, under @work_lock */
> +	/* In-flight slot: true while one one-step Sync frame is programmed
> +	 * into the shared SINGLE_STEP register and being transmitted. Only one
> +	 * frame may be in flight at a time, so the next queued frame is sent
> +	 * only after the current one completes TX (its skb destructor kicks
> +	 * the work). Accessed under queue_lock, from both the softirq xmit
> +	 * path and the process-context work.
> +	 */
> +	bool in_flight;
> +	/* Pending one-step Sync frames. Enqueued from the softirq xmit path
> and
> +	 * dequeued by the process-context work; the list is serialized by
> +	 * queue_lock together with @in_flight.
> +	 */
> +	struct sk_buff_head queue;
> +	struct work_struct work;	/* drains @queue */
> +	struct work_struct destroy_work; /* frees the context in process ctx */
> +};
> +
>  struct netc_port {
>  	void __iomem *iobase;
>  	struct netc_switch *switch_priv;
> @@ -106,6 +145,8 @@ struct netc_port {
>  	spinlock_t tstamp_lock;
>  	/* skb queue for two-step timestamp frames */
>  	struct sk_buff_head skb_txtstamp_queue;
> +	/* one-step Sync serialization context (ref-counted, kzalloc'd) */
> +	struct netc_onestep *onestep;
>  	int ptp_tx_type;
>  	int ptp_rx_filter;
>  	u32 ptp_ipft_eid[NETC_PTP_MAX];
> @@ -212,6 +253,7 @@ static inline void netc_del_vlan_entry(struct
> netc_vlan_entry *entry)
>  }
> 
>  int netc_switch_platform_probe(struct netc_switch *priv);
> +void netc_mac_port_wr(struct netc_port *np, u32 reg, u32 val);
> 
>  /* ethtool APIs */
>  void netc_port_get_pause_stats(struct dsa_switch *ds, int port,
> @@ -243,5 +285,11 @@ void netc_port_twostep_tstamp_handler(struct
> dsa_switch *ds, int port,
>  bool netc_port_rxtstamp(struct dsa_switch *ds, int port, struct sk_buff *skb,
>  			unsigned int type);
>  void netc_port_txtstamp(struct dsa_switch *ds, int port, struct sk_buff *skb);
> +void netc_onestep_put(struct netc_onestep *onestep);
> +void netc_port_purge_onestep_queue(struct netc_onestep *onestep,
> +				   bool clear_flight);
> +void netc_port_onestep_work(struct work_struct *work);
> +void netc_port_onestep_sync_enqueue(struct dsa_switch *ds, int port,
> +				    struct sk_buff *skb);
> 
>  #endif
> diff --git a/drivers/net/dsa/netc/netc_switch_hw.h
> b/drivers/net/dsa/netc/netc_switch_hw.h
> index 1404ae41c7bc..37d1dd7ec2c7 100644
> --- a/drivers/net/dsa/netc/netc_switch_hw.h
> +++ b/drivers/net/dsa/netc/netc_switch_hw.h
> @@ -203,6 +203,11 @@ enum netc_stg_stage {
>  #define   SSP_10M			1
>  #define   SSP_1G			2
> 
> +#define NETC_PM_SINGLE_STEP(a)		(0x10c0 + (a) * 0x400)
> +#define  PM_SINGLE_STEP_CH		BIT(6)
> +#define  PM_SINGLE_STEP_OFFSET		GENMASK(15, 7)
> +#define  PM_SINGLE_STEP_EN		BIT(31)
> +
>  /* Port MAC 0/1 Receive Ethernet Octets Counter */
>  #define NETC_PM_REOCT(a)		(0x1100 + (a) * 0x400)
> 
> diff --git a/include/linux/dsa/tag_netc.h b/include/linux/dsa/tag_netc.h
> index da200e3ba8ad..8aeb865cecab 100644
> --- a/include/linux/dsa/tag_netc.h
> +++ b/include/linux/dsa/tag_netc.h
> @@ -10,6 +10,7 @@
>  #include <net/dsa.h>
> 
>  #define NETC_TAG_MAX_LEN			14
> +#define NETC_PTP_FLAG_ONESTEP			BIT(0)
>  #define NETC_PTP_FLAG_TWOSTEP			BIT(1)
> 
>  struct netc_skb_cb {
> @@ -17,6 +18,14 @@ struct netc_skb_cb {
>  	u64 tstamp;
>  	u8 ptp_flag;
>  	u8 ts_req_id;
> +	/* One-step Sync parsing results, computed in netc_port_txtstamp()
> +	 * and reused in the tagger xmit path and the deferred work, to avoid
> +	 * re-parsing the PTP header. Valid only while
> +	 * ptp_flag == NETC_PTP_FLAG_ONESTEP.
> +	 */
> +	u16 correction_offset;
> +	u16 timestamp_offset;
> +	bool is_udp;
>  };
> 
>  #define NETC_SKB_CB(skb)	((struct netc_skb_cb *)((skb)->cb))
> @@ -26,10 +35,22 @@ struct netc_skb_cb {
>   * @twostep_tstamp_handler: Called by the tagger when a two-step transmit
>   *	timestamp response is received, to deliver the timestamp to the
>   *	switch driver.
> + * @onestep_sync_enqueue: Called from the tagger xmit path for a one-step
> Sync
> + *	frame. The switch driver takes ownership of the skb and queues it for
> + *	deferred transmission from process context, where the shared
> + *	PM_SINGLE_STEP register can be programmed and the PTP timer read
> + *	(which may sleep). The tagger must not touch the skb after this call
> + *	and returns NULL to dsa_user_xmit().
> + * @onestep_sync_xmit: Called by the switch driver to transmit a deferred
> + *	one-step Sync frame directly to the conduit, bypassing dsa_user_xmit().
>   */
>  struct netc_tagger_data {
>  	void (*twostep_tstamp_handler)(struct dsa_switch *ds, int port,
>  				       u8 ts_req_id, u64 ts);
> +	void (*onestep_sync_enqueue)(struct dsa_switch *ds, int port,
> +				     struct sk_buff *skb);
> +	netdev_tx_t (*onestep_sync_xmit)(struct sk_buff *skb,
> +					 struct net_device *ndev);
>  };
> 
>  #endif
> diff --git a/net/dsa/tag_netc.c b/net/dsa/tag_netc.c
> index 9f9a61d8133b..0c36aeaade6a 100644
> --- a/net/dsa/tag_netc.c
> +++ b/net/dsa/tag_netc.c
> @@ -16,6 +16,8 @@
>  #define NETC_TAG_TO_PORT		1
>  /* SubType0: No request to perform timestamping */
>  #define NETC_TAG_TP_SUBTYPE0		0
> +/* SubType1: Request to perform one-step timestamping */
> +#define NETC_TAG_TP_SUBTYPE1		1
>  /* SubType2: Request to perform two-step timestamping */
>  #define NETC_TAG_TP_SUBTYPE2		2
> 
> @@ -31,6 +33,7 @@
>  /* NETC switch tag lengths */
>  #define NETC_TAG_FORWARD_LEN		6
>  #define NETC_TAG_TP_SUBTYPE0_LEN	6
> +#define NETC_TAG_TP_SUBTYPE1_LEN	10
>  #define NETC_TAG_TP_SUBTYPE2_LEN	6
>  #define NETC_TAG_TH_SUBTYPE0_LEN	6
>  #define NETC_TAG_TH_SUBTYPE1_LEN	14
> @@ -44,6 +47,7 @@
>  #define NETC_TAG_SWITCH			GENMASK(2, 0)
>  #define NETC_TAG_PORT			GENMASK(7, 3)
>  #define NETC_TAG_TS_REQ_ID		GENMASK(3, 0)
> +#define NETC_TAG_TIMESTAMP		GENMASK(29, 0)
> 
>  struct netc_tag_cmn {
>  	__be16 tpid;
> @@ -52,6 +56,12 @@ struct netc_tag_cmn {
>  	u8 switch_port;
>  } __packed;
> 
> +struct netc_tag_tp_subtype1 {
> +	struct netc_tag_cmn cmn;
> +	u8 resv;
> +	__be32 timestamp;
> +} __packed;
> +
>  struct netc_tag_tp_subtype2 {
>  	struct netc_tag_cmn cmn;
>  	u8 ts_req_id;
> @@ -118,6 +128,17 @@ static void netc_fill_tp_tag_subtype0(struct sk_buff
> *skb,
>  				NETC_TAG_TP_SUBTYPE0_LEN);
>  }
> 
> +static void netc_fill_tp_tag_subtype1(struct sk_buff *skb,
> +				      struct net_device *ndev)
> +{
> +	u32 ts = FIELD_PREP(NETC_TAG_TIMESTAMP,
> NETC_SKB_CB(skb)->tstamp);
> +	struct netc_tag_tp_subtype1 *tag;
> +
> +	tag = netc_fill_common_tp_tag(skb, ndev, NETC_TAG_TP_SUBTYPE1,
> +				      NETC_TAG_TP_SUBTYPE1_LEN);
> +	tag->timestamp = htonl(ts);
> +}
> +
>  static void netc_fill_tp_tag_subtype2(struct sk_buff *skb,
>  				      struct net_device *ndev)
>  {
> @@ -129,6 +150,42 @@ static void netc_fill_tp_tag_subtype2(struct sk_buff
> *skb,
>  	tag->ts_req_id = FIELD_PREP(NETC_TAG_TS_REQ_ID, ts_req_id);
>  }
> 
> +static netdev_tx_t netc_onestep_sync_xmit(struct sk_buff *skb,
> +					  struct net_device *dev)
> +{
> +	/* This deferred one-step Sync frame already went through
> +	 * dsa_user_xmit()'s skb_ensure_writable_head_tail() and eth_skb_pad()
> +	 * before it was queued in netc_xmit(), and nothing has cloned it or
> +	 * shrunk its head/tail room since. So the head/tail room is still
> +	 * guaranteed and the skb is still writable; only the tag needs to be
> +	 * pushed before handing it directly to the conduit, bypassing
> +	 * dsa_user_xmit() so that dev_sw_netstats_tx_add() is not invoked a
> +	 * second time for the same frame.
> +	 */
> +	netc_fill_tp_tag_subtype1(skb, dev);
> +
> +	return dsa_enqueue_skb(skb, dev);
> +}
> +
> +static void netc_onestep_sync_enqueue(struct sk_buff *skb,
> +				      struct net_device *ndev)
> +{
> +	struct dsa_port *dp = dsa_user_to_port(ndev);
> +	struct netc_tagger_data *tagger_data;
> +
> +	tagger_data = dp->ds->tagger_data;
> +	if (unlikely(!tagger_data->onestep_sync_enqueue)) {
> +		kfree_skb(skb);
> +		return;
> +	}
> +
> +	/* Hand the one-step Sync to the switch driver, which takes ownership
> +	 * and queues it for deferred transmission from its work. The tagger
> +	 * must not touch the skb after this point.
> +	 */
> +	tagger_data->onestep_sync_enqueue(dp->ds, dp->index, skb);
> +}
> +
>  static struct sk_buff *netc_xmit(struct sk_buff *skb,
>  				 struct net_device *ndev)
>  {
> @@ -138,6 +195,16 @@ static struct sk_buff *netc_xmit(struct sk_buff *skb,
>  	if (likely(!ptp_flag)) {
>  		netc_fill_tp_tag_subtype0(skb, ndev);
>  		return skb;
> +	}
> +
> +	if (ptp_flag == NETC_PTP_FLAG_ONESTEP) {
> +		/* The switch driver takes ownership of the one-step Sync and
> +		 * queues it for deferred TX; the deferred work tags it subtype 1
> +		 * and transmits it directly to the conduit. Return NULL so
> +		 * dsa_user_xmit() stops processing this skb.
> +		 */
> +		netc_onestep_sync_enqueue(skb, ndev);
> +		return NULL;
>  	} else if (ptp_flag == NETC_PTP_FLAG_TWOSTEP) {
>  		netc_fill_tp_tag_subtype2(skb, ndev);
>  	} else {
> @@ -317,6 +384,7 @@ static int netc_connect(struct dsa_switch *ds)
>  	if (!tagger_data)
>  		return -ENOMEM;
> 
> +	tagger_data->onestep_sync_xmit = netc_onestep_sync_xmit;
>  	ds->tagger_data = tagger_data;
> 
>  	return 0;
> --
> 2.34.1


^ permalink raw reply	[flat|nested] 14+ messages in thread

* RE: [PATCH v2 net-next 3/7] ptp: netc: export netc_timer_get_current_time() for cross-driver use
       [not found]   ` <20260809031904.90B581F000E9@smtp.kernel.org>
@ 2026-08-10  1:30     ` Wei Fang (OSS)
  0 siblings, 0 replies; 14+ messages in thread
From: Wei Fang (OSS) @ 2026-08-10  1:30 UTC (permalink / raw)
  To: sashiko-reviews@lists.linux.dev
  Cc: Frank.Li@kernel.org, imx@lists.linux.dev, network dev

> > @@ -165,6 +165,47 @@ static u64 netc_timer_cur_time_read(struct
> netc_timer *priv)
> >  	return netc_timer_rd64(priv, NETC_TMR_CUR_TIME_L);
> >  }
> >
> > +/**
> > + * netc_timer_get_current_time - read the current PTP time from the NETC
> Timer
> > + * @pdev: PCI device of the NETC Timer
> > + *
> > + * Reads the 64-bit current time register (TMR_CUR_TIME) from the NETC
> Timer
> > + * device associated with @pdev. Returns 0 if the Timer driver has not yet
> > + * probed or has already been removed.
> > + *
> > + * Context: Process context only. Acquires the device mutex via
> device_lock(),
> > + *          which may sleep. Must not be called from atomic context,
> softirq,
> > + *          BH, or while holding a spinlock.
> > + *
> > + * Return: Current PTP time in nanoseconds, or 0 if the timer is unavailable.
> > + */
> > +u64 netc_timer_get_current_time(struct pci_dev *pdev)
> > +{
> > +	struct netc_timer *priv;
> > +	unsigned long flags;
> > +	u64 cur_time = 0;
> > +
> > +	/* Serialize against driver unbind: the remove() callback runs under
> > +	 * the device lock, so holding it here ensures that priv remains valid
> > +	 * for the entire duration of the register read.
> > +	 */
> > +	device_lock(&pdev->dev);
> > +
> > +	priv = pci_get_drvdata(pdev);
> > +	if (!priv)
> > +		goto unlock_device;
> > +
> > +	spin_lock_irqsave(&priv->lock, flags);
> 
> [Severity: High]
> If a privileged user unbinds the ptp_netc driver from this PCI device via
> sysfs and binds a different driver (like vfio-pci), won't pci_get_drvdata()
> return the new driver's private data structure instead of struct netc_timer?
> 
> Could this cause type confusion and memory corruption when subsequently
> accessing priv->lock and priv->base during the register read?
> 
> Would it be safer to verify that pdev->driver is actually the netc_timer_driver
> before dereferencing the driver data?
> 

It does make sense, I will add a check for this unusual case.


^ permalink raw reply	[flat|nested] 14+ messages in thread

* RE: [PATCH v2 net-next 5/7] net: dsa: netc: enable ingress port filtering lookup by default
       [not found]   ` <20260809031905.520531F00A3D@smtp.kernel.org>
@ 2026-08-10  2:40     ` Wei Fang (OSS)
  0 siblings, 0 replies; 14+ messages in thread
From: Wei Fang (OSS) @ 2026-08-10  2:40 UTC (permalink / raw)
  To: sashiko-reviews@lists.linux.dev
  Cc: imx@lists.linux.dev, Frank.Li@kernel.org, network dev

> > @@ -1753,8 +1757,6 @@ static int netc_port_add_host_flood_rule(struct
> netc_port *np,
> >  	np->uc = uc;
> >  	np->mc = mc;
> >  	np->ipft_hf_eid = host_flood->entry_id;
> > -	/* Enable ingress port filter table lookup */
> > -	netc_port_wr(np, NETC_PIPFCR, PIPFCR_EN);
> >
> >  free_host_flood:
> >  	kfree(host_flood);
> 
> [Severity: High]
> This isn't a bug introduced by this patch, but does
> netc_port_add_host_flood_rule() execute sleepable operations in atomic
> context?
> 
> When userspace changes interface flags (e.g., ip link set dev ethX promisc on),
> the network core calls ndo_change_rx_flags while holding
> netif_addr_lock_bh(),
> which disables bottom halves and establishes an atomic context.
> 
> The DSA core delegates this to the port_set_host_flood callback, which calls
> netc_port_set_host_flood() -> netc_port_add_host_flood_rule() synchronously.
> 
> Looking at the code, do these functions execute sleepable operations?
> 
> - Allocating memory using GFP_KERNEL (via kzalloc_obj() in
>   netc_port_add_host_flood_rule())
> - Acquiring a sleeping lock (via mutex_lock() in ntmp_select_and_lock_cbdr())
> - Yielding to the scheduler (via read_poll_timeout() with sleep=true in
>   netc_xmit_ntmp_cmd())
> 
> Could this lead to a "scheduling while atomic" panic if
> CONFIG_DEBUG_ATOMIC_SLEEP is enabled? Should this hardware
> configuration
> be deferred to a workqueue?
> 

This a false positive, dev_change_rx_flags() is not called in the
netif_addr_lock_bh context.

dev_change_rx_flags()
├── __dev_set_promiscuity()
│   ├── netif_set_promiscuity()
│   │   └── dev_set_promiscuity()          [netdev_lock_ops = mutex]
│   ├── __dev_change_flags()
│   │   ├── netif_change_flags()
│   │   │   ├── do_setlink()               [rtnl_nets_lock = mutex]
│   │   │   ├── dev_change_flags()         [netdev_lock_ops = mutex]
│   │   │   └── vlan_dev_work/ndo_work     [netdev_lock_ops = mutex]
│   │   └── rtnl_configure_link()          [netdev_lock_ops = mutex]
│   └── netif_rx_mode_run()                [might_sleep() + netdev_assert_locked_ops_compat]
│       ├── netif_rx_mode_sync()           [netdev_lock_ops = mutex]
│       └── netdev_work_proc()             [rtnl_lock + netdev_lock_ops = mutex]
├── netif_set_allmulti()
│   ├── __dev_change_flags()               (Same as above)
│   └── dev_set_allmulti()                 [netdev_lock_ops = mutex]
└── __dev_change_flags()                   (Same as above)

__dev_set_rx_mode() is called with netif_addr_lock_bh() held, placing it
in atomic context. However, drivers implementing ndo_change_rx_flags
are not invoked directly from this path.

Inside __dev_set_rx_mode(), there is an explicit guard:

if (ops->ndo_set_rx_mode_async || ops->ndo_change_rx_flags ||
    netdev_need_ops_lock(dev)) {
    netif_rx_mode_queue(dev);
    return;
}

If a driver implements ndo_change_rx_flags, the function
immediately enqueues a work item via netif_rx_mode_queue() and
returns without invoking any driver callback. The actual
ndo_change_rx_flags call is deferred to a workqueue, which runs in
process context under rtnl_lock and netdev_lock_ops() — both mutexes
that allow sleeping.


^ permalink raw reply	[flat|nested] 14+ messages in thread

* RE: [PATCH v2 net-next 6/7] net: dsa: netc: add PTP two-step timestamping support
       [not found]   ` <20260809031907.888511F00A3D@smtp.kernel.org>
@ 2026-08-10  3:33     ` Wei Fang (OSS)
  0 siblings, 0 replies; 14+ messages in thread
From: Wei Fang (OSS) @ 2026-08-10  3:33 UTC (permalink / raw)
  To: sashiko-reviews@lists.linux.dev
  Cc: imx@lists.linux.dev, Frank.Li@kernel.org, network dev

> > +static void netc_port_txtstamp_twostep(struct netc_port *np,
> > +				       struct sk_buff *nskb)
> > +{
> > +	DECLARE_BITMAP(ts_req_id_bitmap, NETC_NUM_TS_REQ_ID);
> > +	struct sk_buff *clone = skb_clone_sk(nskb);
> > +	struct netc_switch *priv = np->switch_priv;
> > +	struct sk_buff_head free_list;
> > +	struct sk_buff *skb, *skb_tmp;
> > +	unsigned long ts_req_id;
> > +	int err = 0;
> > +
> > +	if (unlikely(!clone))
> > +		return;
> > +
> > +	bitmap_zero(ts_req_id_bitmap, NETC_NUM_TS_REQ_ID);
> > +	__skb_queue_head_init(&free_list);
> > +	spin_lock_bh(&np->tstamp_lock);
> > +
> > +	skb_queue_walk_safe(&np->skb_txtstamp_queue, skb, skb_tmp) {
> > +		if (time_before(NETC_SKB_CB(skb)->ptp_tx_time +
> > +				NETC_TXTSTAMP_TIMEOUT, jiffies)) {
> 
> [Severity: High]
> Does this lazy garbage collection of timed-out TX SKBs permanently leak
> sockets
> and network namespaces?
> 
> When a frame requests a two-step hardware TX timestamp, skb_clone_sk() is
> used,
> which takes a reference on the socket. If the frame is dropped in software
> (e.g., due to qdisc drops or congestion) before reaching the hardware, the
> hardware never generates a timestamp response, and the clone remains in
> np->skb_txtstamp_queue.
> 
> Because the driver only lazily checks for timed-out SKBs here when a new PTP
> packet is transmitted, the queue will not be purged if the application stops
> transmitting or closes the socket. The stranded clone holds a reference to the
> socket indefinitely, which in turn prevents the associated network namespace
> from being destroyed.

When tx_type is set to HWTSTAMP_TX_OFF, np->skb_txtstamp_queue will
be cleared. If the application closes the socket, tx_type should be set to
HWTSTAMP_TX_OFF. In this case, I don't think it is an issue.


^ permalink raw reply	[flat|nested] 14+ messages in thread

* RE: [PATCH v2 net-next 7/7] net: dsa: netc: add PTP one-step timestamping support
       [not found]   ` <20260809031906.C98761F000E9@smtp.kernel.org>
@ 2026-08-10  7:31     ` Wei Fang (OSS)
  0 siblings, 0 replies; 14+ messages in thread
From: Wei Fang (OSS) @ 2026-08-10  7:31 UTC (permalink / raw)
  To: sashiko-reviews@lists.linux.dev
  Cc: Frank.Li@kernel.org, imx@lists.linux.dev, network dev

> > @@ -1005,6 +1028,19 @@ static void netc_free_ports_resources(struct
> netc_switch *priv)
> >  			continue;
> >
> >  		netc_port_purge_txtstamp_queue(np);
> > +
> > +		/* dsa_tree_teardown() calls dsa_tree_teardown_ports() before
> > +		 * dsa_tree_teardown_switches(), so netc_port_disable() is
> > +		 * executed before netc_teardown() and purges onestep->queue,
> > +		 * so here we only need to drop the port's owner reference.
> > +		 * In-flight one-step skbs still hold references via the
> > +		 * destructor; the context (and its work) is freed only after
> > +		 * the conduit frees the last in-flight skb. By then np may
> > +		 * be gone, but the work no longer dereferences np because
> > +		 * onestep->active has been cleared.
> > +		 */
> > +		netc_onestep_put(np->onestep);
> > +		np->onestep = NULL;
> >  	}
> >  }
> 
> [Severity: High]
> Does unloading the netc driver module wait for in-flight one-step Sync SKBs
> to complete transmission by the conduit device?
> 
> Because the driver drops its reference here and allows the module unload to
> complete, the kernel could attempt to execute netc_onestep_skb_destructor()
> after the module memory has been unmapped when the conduit eventually
> frees
> the SKB, leading to a panic.

This is a theoretical issue, I do not think this issue will occur in practical use,
and there won't even be a use case where the switch driver is uninstalled
while running PTP.

I can fix this theoretically existing issue, for example, by calling
try_module_get() in netc_port_onestep_alloc() and module_put() in
netc_onestep_destroy_work().

But what I'm really concerned about is that AI is currently reporting many
theoretical problems that don't actually occur in practice. Fixing these
problems involves introducing a lot of largely useless code, and the effort
expended is far less than the final benefit—is it really worth it?

> 
> [ ... ]
> > @@ -1579,6 +1622,7 @@ static int netc_port_enable(struct dsa_switch *ds,
> int port,
> >  static void netc_port_disable(struct dsa_switch *ds, int port)
> >  {
> >  	struct netc_port *np = NETC_PORT(ds, port);
> > +	struct netc_onestep *onestep = np->onestep;
> >
> >  	/* When .port_disable() is called, .port_enable() may not have been
> >  	 * called. In this case, both the prepare_count and enable_count of
> > @@ -1588,6 +1632,13 @@ static void netc_port_disable(struct dsa_switch
> *ds, int port)
> >  	if (!np->enable)
> >  		return;
> >
> > +	if (onestep) {
> > +		mutex_lock(&onestep->work_lock);
> > +		onestep->active = false;
> > +		netc_port_purge_onestep_queue(onestep, true);
> > +		mutex_unlock(&onestep->work_lock);
> > +	}
> > +
> >  	clk_disable_unprepare(np->ref_clk);
> >  	np->enable = false;
> >  }
> > diff --git a/drivers/net/dsa/netc/netc_ptp.c
> b/drivers/net/dsa/netc/netc_ptp.c
> > index 1384a6f31d1c1..d0423b3c8c33a 100644
> > --- a/drivers/net/dsa/netc/netc_ptp.c
> > +++ b/drivers/net/dsa/netc/netc_ptp.c
> [ ... ]
> > +static void netc_port_program_onestep(struct netc_port *np,
> > +				      struct netc_onestep *onestep,
> > +				      struct sk_buff *skb,
> > +				      u64 tstamp)
> > +{
> [ ... ]
> > +		old_sec_h = __get_unaligned_t(__be16, hdr + tstamp_offset);
> > +		old_sec_l = __get_unaligned_t(__be32, hdr + tstamp_offset + 2);
> > +		old_ns = __get_unaligned_t(__be32, hdr + tstamp_offset + 6);
> > +		inet_proto_csum_replace2(&uh->check, skb, old_sec_h,
> > +					 new_sec_h, false);
> > +		inet_proto_csum_replace4(&uh->check, skb, old_sec_l,
> > +					 new_sec_l, false);
> > +		inet_proto_csum_replace4(&uh->check, skb, old_ns,
> > +					 new_ns, false);
> > +		csum_update = true;
> > +	}
> 
> [Severity: Medium]
> Could this manual software modification of the UDP checksum evaluate to 0?
> 
> A UDP checksum of 0 is invalid for IPv6 and disables checksums for IPv4.
> Does this require a check to convert a 0 result into CSUM_MANGLED_0
> (0xFFFF)
> to prevent non-compliant packets?

This is a false positive, the hardware will modify the correction field and
update the checksum.

> 
> [ ... ]
> > +void netc_port_purge_onestep_queue(struct netc_onestep *onestep,
> > +				   bool clear_flight)
> > +{
> > +	struct sk_buff_head free_list;
> > +
> > +	__skb_queue_head_init(&free_list);
> > +
> > +	spin_lock_bh(&onestep->queue_lock);
> > +	skb_queue_splice_init(&onestep->queue, &free_list);
> > +	if (clear_flight)
> > +		onestep->in_flight = false;
> > +	spin_unlock_bh(&onestep->queue_lock);
> > +
> > +	__skb_queue_purge(&free_list);
> > +}
> 
> [Severity: High]
> Does forcefully clearing the in_flight flag here during port disablement
> allow a new frame to be dequeued if the port is quickly brought back up?
> 
> If so, a new frame could be programmed into the shared SINGLE_STEP
> hardware
> register while the previous frame is still physically in flight in the conduit.
> Could this break the 1-to-1 serialization required by the hardware and lead to
> corrupted timestamps?
> 

This is a potential issue, I will fix it.


^ permalink raw reply	[flat|nested] 14+ messages in thread

end of thread, other threads:[~2026-08-10  7:31 UTC | newest]

Thread overview: 14+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-08  3:21 [PATCH v2 net-next 0/7] net: dsa: netc: add PTP support for NETC switch wei.fang
2026-08-08  3:21 ` [PATCH] net: dsa: netc: add PTP one-step timestamping support wei.fang
2026-08-08  3:26   ` Wei Fang
2026-08-08  3:21 ` [PATCH v2 net-next 1/7] ptp: netc: use ioread64_lo_hi/iowrite64_lo_hi for 64-bit register access wei.fang
2026-08-08  3:21 ` [PATCH v2 net-next 2/7] ptp: netc: remove unnecessary pcie_flr() call in probe wei.fang
2026-08-08  3:21 ` [PATCH v2 net-next 3/7] ptp: netc: export netc_timer_get_current_time() for cross-driver use wei.fang
     [not found]   ` <20260809031904.90B581F000E9@smtp.kernel.org>
2026-08-10  1:30     ` Wei Fang (OSS)
2026-08-08  3:21 ` [PATCH v2 net-next 4/7] net: dsa: netc: use entry ID instead of pointer to track host flood rule wei.fang
2026-08-08  3:21 ` [PATCH v2 net-next 5/7] net: dsa: netc: enable ingress port filtering lookup by default wei.fang
     [not found]   ` <20260809031905.520531F00A3D@smtp.kernel.org>
2026-08-10  2:40     ` Wei Fang (OSS)
2026-08-08  3:21 ` [PATCH v2 net-next 6/7] net: dsa: netc: add PTP two-step timestamping support wei.fang
     [not found]   ` <20260809031907.888511F00A3D@smtp.kernel.org>
2026-08-10  3:33     ` Wei Fang (OSS)
2026-08-08  3:21 ` [PATCH v2 net-next 7/7] net: dsa: netc: add PTP one-step " wei.fang
     [not found]   ` <20260809031906.C98761F000E9@smtp.kernel.org>
2026-08-10  7:31     ` Wei Fang (OSS)

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox