mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH net-next 0/2] net: airoha: Add XDP support
@ 2026-09-20 15:43 Til Kaiser
  2026-09-20 15:43 ` [PATCH net-next 1/2] " Til Kaiser
  2026-09-20 15:43 ` [PATCH net-next 2/2] net: airoha: Add XDP statistics Til Kaiser
  0 siblings, 2 replies; 5+ messages in thread
From: Til Kaiser @ 2026-09-20 15:43 UTC (permalink / raw)
  To: netdev
  Cc: lorenzo, andrew+netdev, davem, edumazet, kuba, pabeni,
	linux-arm-kernel, linux-mediatek, linux-kernel, Til Kaiser

Hello,

This series adds XDP support to the Airoha Ethernet driver.

Patch 1 adds the RX- and TX-path support needed for XDP_PASS, XDP_DROP,
XDP_TX, and XDP_REDIRECT.

Patch 2 exposes XDP and page-pool statistics through ethtool.

Thanks,
Til

Til Kaiser (2):
  net: airoha: Add XDP support
  net: airoha: Add XDP statistics

 drivers/net/ethernet/airoha/Kconfig      |   1 +
 drivers/net/ethernet/airoha/airoha_eth.c | 493 +++++++++++++++++++++--
 drivers/net/ethernet/airoha/airoha_eth.h |  32 +-
 3 files changed, 496 insertions(+), 30 deletions(-)

-- 
2.55.0


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

* [PATCH net-next 1/2] net: airoha: Add XDP support
  2026-09-20 15:43 [PATCH net-next 0/2] net: airoha: Add XDP support Til Kaiser
@ 2026-09-20 15:43 ` Til Kaiser
  2026-09-21  7:30   ` Lorenzo Bianconi
  2026-09-20 15:43 ` [PATCH net-next 2/2] net: airoha: Add XDP statistics Til Kaiser
  1 sibling, 1 reply; 5+ messages in thread
From: Til Kaiser @ 2026-09-20 15:43 UTC (permalink / raw)
  To: netdev
  Cc: lorenzo, andrew+netdev, davem, edumazet, kuba, pabeni,
	linux-arm-kernel, linux-mediatek, linux-kernel, Til Kaiser

Implement eXpress Data Path (XDP) support for the Airoha Ethernet driver.

XDP programs are attached per net_device via the ndo_bpf hook. The BPF
program reference is stored as an RCU-protected pointer in struct
airoha_gdm_dev and replaced atomically on program load/unload via
rcu_replace_pointer().

To support XDP, the following changes are made to the RX path:

- The page pool DMA direction is switched from DMA_FROM_DEVICE to
  DMA_BIDIRECTIONAL, which is required for XDP_TX since the same page
  can be reused for transmit.
- The RX headroom is increased from NET_SKB_PAD to XDP_PACKET_HEADROOM
  so that XDP programs have the required headroom available in the data
  buffer.
- RX buffers use a full page rather than a half-page fragment. This leaves
  sufficient space for XDP headroom, skb_shared_info tailroom, and a
  standard 1500-byte MTU.
- AIROHA_RX_MAX_BUF_SIZE expresses the maximum MTU that fits in an RX
  buffer after headroom, shared-info, Ethernet, VLAN, and FCS overhead.
  MTU changes are rejected when an XDP program is loaded and the requested
  MTU would exceed this limit.
- xdp_rxq_info is registered for each RX queue during initialisation and
  unregistered on cleanup, with the page pool set as the memory model.

The XDP program is run in airoha_run_xdp() for each received buffer.
The following actions are supported:

- XDP_PASS: the buffer is passed to the normal networking stack.
- XDP_TX: the buffer is transmitted back out the same interface using
  the new airoha_xdp_xmit_back() helper.
- XDP_REDIRECT: the buffer is redirected to another interface or map
  via xdp_do_redirect(), with xdp_do_flush() deferred to the end of
  the NAPI poll via a per-queue xdp_flush flag.
- XDP_DROP (and unknown actions): the page is returned to the page pool.

Two new TX path helpers are introduced:

- airoha_xdp_submit_frame() maps an xdp_frame for DMA (or reuses the
  page pool DMA address for XDP_TX) and enqueues it into a TX ring,
  handling multi-buffer frames via skb_shared_info fragments.
- airoha_xdp_xmit_back() converts an xdp_buff to an xdp_frame and
  submits it to the TX queue corresponding to the current CPU.

The ndo_xdp_xmit handler airoha_xdp_xmit() is also added to allow
XDP redirect from other drivers into the Airoha interface. Frames are
submitted using airoha_xdp_submit_frame() with dma_map=true.

struct airoha_queue_entry is extended with an is_xdpf flag and an
xdp_frame pointer (in a union with the existing sk_buff pointer) so
that the TX completion path can correctly free either XDP frames or
SKBs.

The advertised XDP feature flags are set to:
  NETDEV_XDP_ACT_BASIC | NETDEV_XDP_ACT_REDIRECT | NETDEV_XDP_ACT_NDO_XMIT

Signed-off-by: Til Kaiser <mail@tk154.de>
---
 drivers/net/ethernet/airoha/airoha_eth.c | 386 +++++++++++++++++++++--
 drivers/net/ethernet/airoha/airoha_eth.h |  19 +-
 2 files changed, 375 insertions(+), 30 deletions(-)

diff --git a/drivers/net/ethernet/airoha/airoha_eth.c b/drivers/net/ethernet/airoha/airoha_eth.c
index 64619e9a704d..71b25f225a8a 100644
--- a/drivers/net/ethernet/airoha/airoha_eth.c
+++ b/drivers/net/ethernet/airoha/airoha_eth.c
@@ -13,6 +13,7 @@
 #include <net/page_pool/helpers.h>
 #include <net/pkt_cls.h>
 #include <uapi/linux/ppp_defs.h>
+#include <linux/bpf_trace.h>
 
 #include "airoha_regs.h"
 #include "airoha_eth.h"
@@ -657,6 +658,255 @@ airoha_qdma_get_gdm_dev(struct airoha_eth *eth, struct airoha_qdma_desc *desc)
 	return port->devs[d] ? port->devs[d] : ERR_PTR(-ENODEV);
 }
 
+static void airoha_unmap_xmit_buf(struct airoha_eth *eth,
+				  struct airoha_queue_entry *e)
+{
+	switch (e->dma_type) {
+	case AIROHA_DMA_MAP_PAGE:
+		dma_unmap_page(eth->dev, e->dma_addr, e->dma_len,
+			       DMA_TO_DEVICE);
+		break;
+	case AIROHA_DMA_MAP_SINGLE:
+		dma_unmap_single(eth->dev, e->dma_addr, e->dma_len,
+				 DMA_TO_DEVICE);
+		break;
+	case AIROHA_DMA_UNMAPPED:
+	default:
+		break;
+	}
+	e->dma_type = AIROHA_DMA_UNMAPPED;
+}
+
+static int airoha_xdp_submit_frame(struct airoha_gdm_dev *dev,
+				   struct xdp_frame *xdpf,
+				   struct airoha_queue *q,
+				   int qid, u32 msg0, u32 msg1,
+				   bool dma_map)
+{
+	struct airoha_queue_entry *e, *next_e;
+	int len = xdpf->len, nr_frags, i;
+	struct airoha_qdma_desc *desc;
+	struct skb_shared_info *sinfo;
+	void *data = xdpf->data;
+	u16 index, next_index;
+	LIST_HEAD(tx_list);
+	dma_addr_t addr;
+	u32 val;
+
+	sinfo = xdp_get_shared_info_from_frame(xdpf);
+	nr_frags = unlikely(xdp_frame_has_frags(xdpf)) ? sinfo->nr_frags : 0;
+
+	if (q->queued >= q->ndesc - 1 - nr_frags)
+		return -EBUSY;
+
+	for (i = 0; i <= nr_frags; i++) {
+		if (dma_map) {
+			if (i == 0)
+				addr = dma_map_single(dev->eth->dev, data, len,
+						      DMA_TO_DEVICE);
+			else
+				addr = dma_map_page(dev->eth->dev, virt_to_page(data),
+						    offset_in_page(data), len,
+						    DMA_TO_DEVICE);
+			if (unlikely(dma_mapping_error(dev->eth->dev, addr)))
+				goto unmap;
+		} else {
+			struct page *page = virt_to_head_page(data);
+
+			addr = page_pool_get_dma_addr(page) +
+			       (data - (void *)page_address(page));
+			dma_sync_single_for_device(dev->eth->dev, addr, len,
+						   DMA_BIDIRECTIONAL);
+		}
+
+		e = list_first_entry(&q->tx_list, struct airoha_queue_entry, list);
+		list_move_tail(&e->list, &tx_list);
+
+		index = e - q->entry;
+		desc = &q->desc[index];
+
+		e->is_xdpf = true;
+		e->xdpf = (i == nr_frags) ? xdpf : NULL;
+		e->dma_addr = addr;
+		e->dma_len = len;
+		if (dma_map)
+			e->dma_type = i == 0 ? AIROHA_DMA_MAP_SINGLE : AIROHA_DMA_MAP_PAGE;
+		else
+			e->dma_type = AIROHA_DMA_UNMAPPED;
+
+		next_e = list_first_entry(&q->tx_list, struct airoha_queue_entry, list);
+		next_index = next_e - q->entry;
+
+		val = FIELD_PREP(QDMA_DESC_LEN_MASK, len);
+		if (i < nr_frags)
+			val |= FIELD_PREP(QDMA_DESC_MORE_MASK, 1);
+		WRITE_ONCE(desc->ctrl, cpu_to_le32(val));
+		WRITE_ONCE(desc->addr, cpu_to_le32(addr));
+		val = FIELD_PREP(QDMA_DESC_NEXT_ID_MASK, next_index);
+		WRITE_ONCE(desc->data, cpu_to_le32(val));
+		WRITE_ONCE(desc->msg0, cpu_to_le32(msg0));
+		WRITE_ONCE(desc->msg1, cpu_to_le32(msg1));
+		WRITE_ONCE(desc->msg2, cpu_to_le32(0xffff));
+
+		q->queued++;
+
+		if (i < nr_frags) {
+			skb_frag_t *frag = &sinfo->frags[i];
+
+			data = skb_frag_address(frag);
+			len = skb_frag_size(frag);
+		}
+	}
+
+	return next_index;
+
+unmap:
+	list_for_each_entry(e, &tx_list, list) {
+		airoha_unmap_xmit_buf(dev->eth, e);
+		e->is_xdpf = false;
+		e->xdpf = NULL;
+		q->queued--;
+	}
+	list_splice(&tx_list, &q->tx_list);
+
+	return -ENOMEM;
+}
+
+static int airoha_xdp_xmit_back(struct net_device *netdev, struct xdp_buff *xdp,
+				struct airoha_qdma *qdma)
+{
+	struct airoha_gdm_dev *dev = netdev_priv(netdev);
+	struct airoha_queue *q;
+	struct xdp_frame *xdpf;
+	u32 msg0, msg1;
+	int qid, index;
+	u8 fport;
+
+	xdpf = xdp_convert_buff_to_frame(xdp);
+	if (unlikely(!xdpf))
+		return -EOVERFLOW;
+
+	qid = airoha_qdma_get_txq(qdma, smp_processor_id());
+	q = &qdma->q_tx[qid];
+
+	msg0 = FIELD_PREP(QDMA_ETH_TXMSG_CHAN_MASK,
+			  qid / AIROHA_NUM_QOS_QUEUES) |
+	       FIELD_PREP(QDMA_ETH_TXMSG_QUEUE_MASK,
+			  qid % AIROHA_NUM_QOS_QUEUES);
+
+	fport = airoha_get_fe_port(dev);
+	msg1 = FIELD_PREP(QDMA_ETH_TXMSG_NBOQ_MASK, dev->nbq) |
+	       FIELD_PREP(QDMA_ETH_TXMSG_FPORT_MASK, fport) |
+	       FIELD_PREP(QDMA_ETH_TXMSG_METER_MASK, 0x7f);
+
+	spin_lock(&q->lock);
+	index = airoha_xdp_submit_frame(dev, xdpf, q, qid, msg0, msg1, false);
+	if (unlikely(index < 0)) {
+		spin_unlock(&q->lock);
+		return index;
+	}
+
+	airoha_qdma_rmw(qdma, REG_TX_CPU_IDX(qid),
+			TX_RING_CPU_IDX_MASK,
+			FIELD_PREP(TX_RING_CPU_IDX_MASK, index));
+	spin_unlock(&q->lock);
+
+	return 0;
+}
+
+static int airoha_xdp_xmit(struct net_device *netdev, int n,
+			   struct xdp_frame **frames, u32 flags)
+{
+	int qid, index = 0, last_index = -1, i, drops = 0;
+	struct airoha_gdm_dev *dev = netdev_priv(netdev);
+	struct airoha_qdma *qdma;
+	struct airoha_queue *q;
+	u32 msg0, msg1;
+	u8 fport;
+
+	if (unlikely(flags & ~XDP_XMIT_FLAGS_MASK))
+		return -EINVAL;
+
+	rcu_read_lock();
+	qdma = rcu_dereference(dev->qdma);
+	if (!qdma) {
+		rcu_read_unlock();
+		return -ENODEV;
+	}
+
+	qid = airoha_qdma_get_txq(qdma, smp_processor_id());
+	q = &qdma->q_tx[qid];
+
+	msg0 = FIELD_PREP(QDMA_ETH_TXMSG_CHAN_MASK,
+			  qid / AIROHA_NUM_QOS_QUEUES) |
+	       FIELD_PREP(QDMA_ETH_TXMSG_QUEUE_MASK,
+			  qid % AIROHA_NUM_QOS_QUEUES);
+
+	fport = airoha_get_fe_port(dev);
+	msg1 = FIELD_PREP(QDMA_ETH_TXMSG_NBOQ_MASK, dev->nbq) |
+	       FIELD_PREP(QDMA_ETH_TXMSG_FPORT_MASK, fport) |
+	       FIELD_PREP(QDMA_ETH_TXMSG_METER_MASK, 0x7f);
+
+	spin_lock(&q->lock);
+	for (i = 0; i < n; i++) {
+		index = airoha_xdp_submit_frame(dev, frames[i], q, qid, msg0, msg1, true);
+		if (unlikely(index < 0)) {
+			xdp_return_frame_rx_napi(frames[i]);
+			drops++;
+		} else {
+			last_index = index;
+		}
+	}
+	if (n > drops && (flags & XDP_XMIT_FLUSH))
+		airoha_qdma_rmw(qdma, REG_TX_CPU_IDX(qid),
+				TX_RING_CPU_IDX_MASK,
+				FIELD_PREP(TX_RING_CPU_IDX_MASK, last_index));
+	spin_unlock(&q->lock);
+	rcu_read_unlock();
+
+	return n - drops;
+}
+
+static bool airoha_run_xdp(struct net_device *netdev, struct bpf_prog *prog,
+			   struct xdp_buff *xdp, struct airoha_queue *q,
+			   struct airoha_queue_entry *e, struct page *page)
+{
+	u32 act = bpf_prog_run_xdp(prog, xdp);
+
+	switch (act) {
+	case XDP_PASS:
+		return false;
+	case XDP_TX:
+		if (unlikely(airoha_xdp_xmit_back(netdev, xdp, q->qdma) < 0)) {
+			trace_xdp_exception(netdev, prog, act);
+			page_pool_put_full_page(q->page_pool, page, true);
+		} else {
+			e->buf = NULL;
+		}
+		break;
+	case XDP_REDIRECT:
+		if (unlikely(xdp_do_redirect(netdev, xdp, prog) < 0)) {
+			trace_xdp_exception(netdev, prog, act);
+			page_pool_put_full_page(q->page_pool, page, true);
+		} else {
+			q->xdp_flush = true;
+			e->buf = NULL;
+		}
+		break;
+	default:
+		bpf_warn_invalid_xdp_action(netdev, prog, act);
+		fallthrough;
+	case XDP_ABORTED:
+		trace_xdp_exception(netdev, prog, act);
+		fallthrough;
+	case XDP_DROP:
+		page_pool_put_full_page(q->page_pool, page, true);
+		break;
+	}
+
+	return true;
+}
+
 static int airoha_qdma_rx_process(struct airoha_queue *q, int budget)
 {
 	enum dma_data_direction dir = page_pool_get_dma_dir(q->page_pool);
@@ -698,6 +948,27 @@ static int airoha_qdma_rx_process(struct airoha_queue *q, int budget)
 
 		netdev = netdev_from_priv(dev);
 		if (!q->skb) { /* first buffer */
+			struct bpf_prog *xdp_prog;
+
+			rcu_read_lock();
+			xdp_prog = rcu_dereference(dev->xdp_prog);
+			if (xdp_prog) {
+				struct xdp_buff xdp;
+
+				xdp_init_buff(&xdp, q->buf_size, &q->xdp_rxq);
+				xdp_prepare_buff(&xdp, e->buf - AIROHA_RX_HEADROOM,
+						 AIROHA_RX_HEADROOM, len, false);
+
+				if (airoha_run_xdp(netdev, xdp_prog, &xdp, q, e, page)) {
+					rcu_read_unlock();
+					continue;
+				}
+
+				len = xdp.data_end - xdp.data;
+				e->buf = xdp.data;
+			}
+			rcu_read_unlock();
+
 			q->skb = napi_build_skb(e->buf - AIROHA_RX_HEADROOM,
 						q->buf_size);
 			if (!q->skb)
@@ -779,6 +1050,11 @@ static int airoha_qdma_rx_napi_poll(struct napi_struct *napi, int budget)
 		done += cur;
 	} while (cur && done < budget);
 
+	if (q->xdp_flush) {
+		xdp_do_flush();
+		q->xdp_flush = false;
+	}
+
 	if (done < budget && napi_complete(napi)) {
 		struct airoha_qdma *qdma = q->qdma;
 		int i, qid = q - &qdma->q_rx[0];
@@ -804,17 +1080,17 @@ static int airoha_qdma_init_rx_queue(struct airoha_queue *q,
 		.order = 0,
 		.pool_size = 256,
 		.flags = PP_FLAG_DMA_MAP | PP_FLAG_DMA_SYNC_DEV,
-		.dma_dir = DMA_FROM_DEVICE,
+		.dma_dir = DMA_BIDIRECTIONAL,
 		.max_len = PAGE_SIZE,
 		.nid = NUMA_NO_NODE,
 		.dev = qdma->eth->dev,
 		.napi = &q->napi,
 	};
+	int qid = q - &qdma->q_rx[0], thr, err;
 	struct airoha_eth *eth = qdma->eth;
-	int qid = q - &qdma->q_rx[0], thr;
 	dma_addr_t dma_addr;
 
-	q->buf_size = PAGE_SIZE / 2;
+	q->buf_size = PAGE_SIZE;
 	q->qdma = qdma;
 
 	q->entry = devm_kzalloc(eth->dev, ndesc * sizeof(*q->entry),
@@ -829,15 +1105,22 @@ static int airoha_qdma_init_rx_queue(struct airoha_queue *q,
 
 	q->page_pool = page_pool_create(&pp_params);
 	if (IS_ERR(q->page_pool)) {
-		int err = PTR_ERR(q->page_pool);
-
-		q->page_pool = NULL;
-		return err;
+		err = PTR_ERR(q->page_pool);
+		goto err_page_pool_create;
 	}
 
 	q->ndesc = ndesc;
 	netif_napi_add(eth->napi_dev, &q->napi, airoha_qdma_rx_napi_poll);
 
+	err = xdp_rxq_info_reg(&q->xdp_rxq, eth->napi_dev, qid, q->napi.napi_id);
+	if (err)
+		goto err_xdp_rxq_info_reg;
+
+	err = xdp_rxq_info_reg_mem_model(&q->xdp_rxq, MEM_TYPE_PAGE_POOL,
+					 q->page_pool);
+	if (err)
+		goto err_xdp_rxq_info_reg_mem_model;
+
 	airoha_qdma_wr(qdma, REG_RX_RING_BASE(qid), dma_addr);
 	airoha_qdma_rmw(qdma, REG_RX_RING_SIZE(qid),
 			RX_RING_SIZE_MASK,
@@ -853,6 +1136,14 @@ static int airoha_qdma_init_rx_queue(struct airoha_queue *q,
 	airoha_qdma_fill_rx_queue(q);
 
 	return 0;
+
+err_xdp_rxq_info_reg_mem_model:
+	xdp_rxq_info_unreg(&q->xdp_rxq);
+err_xdp_rxq_info_reg:
+	page_pool_destroy(q->page_pool);
+err_page_pool_create:
+	q->page_pool = NULL;
+	return err;
 }
 
 static void airoha_qdma_cleanup_rx_queue(struct airoha_queue *q)
@@ -882,6 +1173,9 @@ static void airoha_qdma_cleanup_rx_queue(struct airoha_queue *q)
 		q->queued--;
 	}
 
+	if (xdp_rxq_info_is_reg(&q->xdp_rxq))
+		xdp_rxq_info_unreg(&q->xdp_rxq);
+
 	q->head = q->tail;
 	/* Set RX_DMA_IDX to RX_CPU_IDX to notify the hw the QDMA RX ring is
 	 * empty.
@@ -949,25 +1243,6 @@ static void airoha_qdma_wake_netdev_txqs(struct airoha_queue *q)
 	q->txq_stopped = false;
 }
 
-static void airoha_unmap_xmit_buf(struct airoha_eth *eth,
-				  struct airoha_queue_entry *e)
-{
-	switch (e->dma_type) {
-	case AIROHA_DMA_MAP_PAGE:
-		dma_unmap_page(eth->dev, e->dma_addr, e->dma_len,
-			       DMA_TO_DEVICE);
-		break;
-	case AIROHA_DMA_MAP_SINGLE:
-		dma_unmap_single(eth->dev, e->dma_addr, e->dma_len,
-				 DMA_TO_DEVICE);
-		break;
-	case AIROHA_DMA_UNMAPPED:
-	default:
-		break;
-	}
-	e->dma_type = AIROHA_DMA_UNMAPPED;
-}
-
 static int airoha_qdma_tx_napi_poll(struct napi_struct *napi, int budget)
 {
 	struct airoha_tx_irq_queue *irq_q;
@@ -1037,7 +1312,12 @@ static int airoha_qdma_tx_napi_poll(struct napi_struct *napi, int budget)
 		WRITE_ONCE(desc->msg1, 0);
 		q->queued--;
 
-		if (skb) {
+		if (e->is_xdpf) {
+			if (e->xdpf)
+				xdp_return_frame(e->xdpf);
+			e->is_xdpf = false;
+			e->xdpf = NULL;
+		} else if (e->skb) {
 			struct airoha_gdm_dev *dev = netdev_priv(skb->dev);
 			u16 qidx = skb_get_queue_mapping(skb);
 			struct netdev_queue *txq;
@@ -1218,7 +1498,12 @@ static void airoha_qdma_tx_cleanup(struct airoha_qdma *qdma)
 			WRITE_ONCE(desc->msg1, 0);
 			WRITE_ONCE(desc->msg2, 0);
 
-			if (skb) {
+			if (e->is_xdpf) {
+				if (e->xdpf)
+					xdp_return_frame(e->xdpf);
+				e->is_xdpf = false;
+				e->xdpf = NULL;
+			} else if (skb) {
 				struct netdev_queue *txq;
 
 				txq = skb_get_tx_queue(skb->dev, skb);
@@ -2201,6 +2486,12 @@ static int airoha_dev_change_mtu(struct net_device *netdev, int mtu)
 	struct airoha_gdm_dev *dev = netdev_priv(netdev);
 	struct airoha_gdm_port *port = dev->port;
 
+	if (rcu_access_pointer(dev->xdp_prog) && mtu > AIROHA_RX_MAX_BUF_SIZE) {
+		netdev_err(netdev, "MTU too large for XDP (max %lu)\n",
+			   AIROHA_RX_MAX_BUF_SIZE);
+		return -EINVAL;
+	}
+
 	WRITE_ONCE(netdev->mtu, mtu);
 	if (port->users)
 		airoha_dev_set_xmit_frame_size(netdev);
@@ -2377,6 +2668,7 @@ static netdev_tx_t airoha_dev_xmit(struct sk_buff *skb,
 
 		list_move_tail(&e->list, &tx_list);
 		e->skb = i == nr_frags - 1 ? skb : NULL;
+		e->is_xdpf = false;
 		e->dma_addr = addr;
 		e->dma_len = len;
 
@@ -3321,6 +3613,39 @@ static int airoha_tc_setup_qdisc_htb(struct net_device *netdev,
 	return 0;
 }
 
+static int airoha_xdp_setup(struct net_device *netdev, struct bpf_prog *prog,
+			    struct netlink_ext_ack *extack)
+{
+	struct airoha_gdm_dev *dev = netdev_priv(netdev);
+	struct bpf_prog *old_prog;
+
+	if (netdev->features & NETIF_F_LRO) {
+		NL_SET_ERR_MSG_MOD(extack, "XDP is not supported with LRO");
+		return -EOPNOTSUPP;
+	}
+
+	if (netdev->mtu > AIROHA_RX_MAX_BUF_SIZE) {
+		NL_SET_ERR_MSG_MOD(extack, "MTU too large for XDP");
+		return -EOPNOTSUPP;
+	}
+
+	old_prog = rcu_replace_pointer(dev->xdp_prog, prog, lockdep_rtnl_is_held());
+	if (old_prog)
+		bpf_prog_put(old_prog);
+
+	return 0;
+}
+
+static int airoha_xdp_bpf(struct net_device *netdev, struct netdev_bpf *xdp)
+{
+	switch (xdp->command) {
+	case XDP_SETUP_PROG:
+		return airoha_xdp_setup(netdev, xdp->prog, xdp->extack);
+	default:
+		return -EINVAL;
+	}
+}
+
 static int airoha_dev_tc_setup(struct net_device *dev,
 			       enum tc_setup_type type, void *type_data)
 {
@@ -3347,6 +3672,8 @@ static const struct net_device_ops airoha_netdev_ops = {
 	.ndo_get_stats64        = airoha_dev_get_stats64,
 	.ndo_set_mac_address	= airoha_dev_set_macaddr,
 	.ndo_setup_tc		= airoha_dev_tc_setup,
+	.ndo_bpf		= airoha_xdp_bpf,
+	.ndo_xdp_xmit		= airoha_xdp_xmit,
 };
 
 static const struct ethtool_ops airoha_ethtool_ops = {
@@ -3435,6 +3762,9 @@ static int airoha_alloc_gdm_device(struct airoha_eth *eth,
 			      NETIF_F_HW_TC;
 	netdev->features |= netdev->hw_features;
 	netdev->vlan_features = netdev->hw_features;
+	netdev->xdp_features = NETDEV_XDP_ACT_BASIC |
+				NETDEV_XDP_ACT_REDIRECT |
+				NETDEV_XDP_ACT_NDO_XMIT;
 	SET_NETDEV_DEV(netdev, eth->dev);
 
 	/* reserve hw queues for HTB offloading */
diff --git a/drivers/net/ethernet/airoha/airoha_eth.h b/drivers/net/ethernet/airoha/airoha_eth.h
index 8277c1c87bb3..cfbc0b8f5bf9 100644
--- a/drivers/net/ethernet/airoha/airoha_eth.h
+++ b/drivers/net/ethernet/airoha/airoha_eth.h
@@ -7,6 +7,7 @@
 #ifndef AIROHA_ETH_H
 #define AIROHA_ETH_H
 
+#include <linux/bpf.h>
 #include <linux/debugfs.h>
 #include <linux/etherdevice.h>
 #include <linux/iopoll.h>
@@ -15,6 +16,7 @@
 #include <linux/reset.h>
 #include <linux/soc/airoha/airoha_offload.h>
 #include <net/dsa.h>
+#include <net/xdp.h>
 
 #define AIROHA_MAX_NUM_GDM_PORTS	4
 #define AIROHA_MAX_NUM_GDM_DEVS		2
@@ -34,7 +36,11 @@
 #define AIROHA_FE_MC_MAX_VLAN_TABLE	64
 #define AIROHA_FE_MC_MAX_VLAN_PORT	16
 #define AIROHA_NUM_TX_IRQ		2
-#define AIROHA_RX_HEADROOM		(NET_SKB_PAD + NET_IP_ALIGN)
+#define AIROHA_RX_HEADROOM		(XDP_PACKET_HEADROOM + NET_IP_ALIGN)
+#define AIROHA_RX_PAD			(AIROHA_RX_HEADROOM + \
+					 SKB_DATA_ALIGN(sizeof(struct skb_shared_info)))
+#define AIROHA_RX_MAX_BUF_SIZE		(PAGE_SIZE - AIROHA_RX_PAD - \
+					 VLAN_ETH_HLEN - ETH_FCS_LEN)
 #define AIROHA_RX_LEN(_n)		((_n) - AIROHA_RX_HEADROOM)
 #define HW_DSCP_NUM			2048
 #define IRQ_QUEUE_LEN(_n)		((_n) ? 1024 : 2048)
@@ -182,12 +188,16 @@ struct airoha_queue_entry {
 		void *buf;
 		struct {
 			struct list_head list;
-			struct sk_buff *skb;
+			union {
+				struct sk_buff *skb;
+				struct xdp_frame *xdpf;
+			};
 			enum airoha_dma_map_type dma_type;
 		};
 	};
 	dma_addr_t dma_addr;
 	u16 dma_len;
+	bool is_xdpf;
 };
 
 struct airoha_queue {
@@ -211,6 +221,9 @@ struct airoha_queue {
 	struct page_pool *page_pool;
 	struct sk_buff *skb;
 
+	struct xdp_rxq_info xdp_rxq;
+	bool xdp_flush;
+
 	struct list_head tx_list;
 };
 
@@ -589,6 +602,8 @@ struct airoha_gdm_dev {
 	unsigned long flags;
 	int nbq;
 
+	struct bpf_prog __rcu *xdp_prog;
+
 	struct airoha_hw_stats stats;
 
 	/* Serialize netdev_tx_completed_queue() calls per TX queue during
-- 
2.55.0


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

* [PATCH net-next 2/2] net: airoha: Add XDP statistics
  2026-09-20 15:43 [PATCH net-next 0/2] net: airoha: Add XDP support Til Kaiser
  2026-09-20 15:43 ` [PATCH net-next 1/2] " Til Kaiser
@ 2026-09-20 15:43 ` Til Kaiser
  2026-09-21  8:05   ` Lorenzo Bianconi
  1 sibling, 1 reply; 5+ messages in thread
From: Til Kaiser @ 2026-09-20 15:43 UTC (permalink / raw)
  To: netdev
  Cc: lorenzo, andrew+netdev, davem, edumazet, kuba, pabeni,
	linux-arm-kernel, linux-mediatek, linux-kernel, Til Kaiser

Track and expose per-device XDP statistics via ethtool, covering both
the RX and TX sides of the XDP data path.

A new struct airoha_xdp_stats is introduced and embedded in the existing
struct airoha_hw_stats. It contains the following counters, all protected
by the existing u64_stats_sync:

  rx_xdp_pass            - frames passed to the networking stack
  rx_xdp_aborted         - frames with XDP_ABORTED or unknown action
  rx_xdp_drop            - frames dropped by the BPF program or on error
  rx_xdp_tx              - frames sent back out via XDP_TX
  rx_xdp_tx_errors       - XDP_TX failures
  rx_xdp_redirect        - frames successfully redirected
  rx_xdp_redirect_errors - XDP_REDIRECT failures
  tx_xdp_xmit            - frames successfully submitted via ndo_xdp_xmit
  tx_xdp_xmit_errors     - frames dropped in ndo_xdp_xmit

The airoha_run_xdp() function is reworked to update the appropriate
counter after each XDP action. The airoha_xdp_xmit() ndo_xdp_xmit handler
is updated to record the number of successfully transmitted and dropped
frames.

The ethtool interface is extended with three new callbacks:

- get_strings: emits the XDP counter names followed by the page pool
  stat strings obtained from page_pool_ethtool_stats_get_strings().
- get_sset_count: returns the combined count of XDP and page pool stats.
- get_ethtool_stats: reads the XDP counters under the u64_stats_sync
  seqcount and appends page pool stats collected from all active RX
  queues via page_pool_get_stats().

PAGE_POOL_STATS is selected in Kconfig to enable the page pool statistics
infrastructure required by the ethtool callbacks.

Signed-off-by: Til Kaiser <mail@tk154.de>
---
 drivers/net/ethernet/airoha/Kconfig      |   1 +
 drivers/net/ethernet/airoha/airoha_eth.c | 131 ++++++++++++++++++++---
 drivers/net/ethernet/airoha/airoha_eth.h |  14 +++
 3 files changed, 133 insertions(+), 13 deletions(-)

diff --git a/drivers/net/ethernet/airoha/Kconfig b/drivers/net/ethernet/airoha/Kconfig
index 1f6640a15fc9..3fa7683a79e9 100644
--- a/drivers/net/ethernet/airoha/Kconfig
+++ b/drivers/net/ethernet/airoha/Kconfig
@@ -20,6 +20,7 @@ config NET_AIROHA
 	depends on NET_DSA || !NET_DSA
 	select NET_AIROHA_NPU
 	select PAGE_POOL
+	select PAGE_POOL_STATS
 	help
 	  This driver supports the gigabit ethernet MACs in the
 	  Airoha SoC family.
diff --git a/drivers/net/ethernet/airoha/airoha_eth.c b/drivers/net/ethernet/airoha/airoha_eth.c
index 71b25f225a8a..29b837204842 100644
--- a/drivers/net/ethernet/airoha/airoha_eth.c
+++ b/drivers/net/ethernet/airoha/airoha_eth.c
@@ -864,6 +864,11 @@ static int airoha_xdp_xmit(struct net_device *netdev, int n,
 	spin_unlock(&q->lock);
 	rcu_read_unlock();
 
+	u64_stats_update_begin(&dev->stats.syncp);
+	dev->stats.xdp_stats.tx_xdp_xmit += n - drops;
+	dev->stats.xdp_stats.tx_xdp_xmit_errors += drops;
+	u64_stats_update_end(&dev->stats.syncp);
+
 	return n - drops;
 }
 
@@ -871,40 +876,56 @@ static bool airoha_run_xdp(struct net_device *netdev, struct bpf_prog *prog,
 			   struct xdp_buff *xdp, struct airoha_queue *q,
 			   struct airoha_queue_entry *e, struct page *page)
 {
+	struct airoha_gdm_dev *dev = netdev_priv(netdev);
+	struct airoha_hw_stats *hw_stats = &dev->stats;
 	u32 act = bpf_prog_run_xdp(prog, xdp);
+	u64 *count;
 
 	switch (act) {
 	case XDP_PASS:
-		return false;
+		count = &hw_stats->xdp_stats.rx_xdp_pass;
+		goto update_stats;
 	case XDP_TX:
 		if (unlikely(airoha_xdp_xmit_back(netdev, xdp, q->qdma) < 0)) {
+			count = &hw_stats->xdp_stats.rx_xdp_tx_errors;
 			trace_xdp_exception(netdev, prog, act);
-			page_pool_put_full_page(q->page_pool, page, true);
-		} else {
-			e->buf = NULL;
+			break;
 		}
-		break;
+
+		e->buf = NULL;
+		count = &hw_stats->xdp_stats.rx_xdp_tx;
+		goto update_stats;
 	case XDP_REDIRECT:
 		if (unlikely(xdp_do_redirect(netdev, xdp, prog) < 0)) {
+			count = &hw_stats->xdp_stats.rx_xdp_redirect_errors;
 			trace_xdp_exception(netdev, prog, act);
-			page_pool_put_full_page(q->page_pool, page, true);
-		} else {
-			q->xdp_flush = true;
-			e->buf = NULL;
+			break;
 		}
-		break;
+
+		e->buf = NULL;
+		q->xdp_flush = true;
+		count = &hw_stats->xdp_stats.rx_xdp_redirect;
+		goto update_stats;
 	default:
 		bpf_warn_invalid_xdp_action(netdev, prog, act);
 		fallthrough;
 	case XDP_ABORTED:
+		count = &hw_stats->xdp_stats.rx_xdp_aborted;
 		trace_xdp_exception(netdev, prog, act);
-		fallthrough;
+		break;
 	case XDP_DROP:
-		page_pool_put_full_page(q->page_pool, page, true);
+		count = &hw_stats->xdp_stats.rx_xdp_drop;
 		break;
 	}
 
-	return true;
+	page_pool_put_full_page(q->page_pool, page, true);
+
+update_stats:
+	u64_stats_update_begin(&hw_stats->syncp);
+	*count = *count + 1;
+	u64_stats_update_end(&hw_stats->syncp);
+
+	return act != XDP_PASS;
 }
 
 static int airoha_qdma_rx_process(struct airoha_queue *q, int budget)
@@ -3676,12 +3697,96 @@ static const struct net_device_ops airoha_netdev_ops = {
 	.ndo_xdp_xmit		= airoha_xdp_xmit,
 };
 
+#define AIROHA_ETHTOOL_XDP_STAT(x) { #x, \
+				  offsetof(struct airoha_hw_stats, xdp_stats.x) / \
+				  sizeof(u64) }
+
+static const struct {
+	const char name[ETH_GSTRING_LEN];
+	u32 offset;
+} airoha_ethtool_xdp_stats[] = {
+	AIROHA_ETHTOOL_XDP_STAT(rx_xdp_redirect),
+	AIROHA_ETHTOOL_XDP_STAT(rx_xdp_redirect_errors),
+	AIROHA_ETHTOOL_XDP_STAT(rx_xdp_pass),
+	AIROHA_ETHTOOL_XDP_STAT(rx_xdp_aborted),
+	AIROHA_ETHTOOL_XDP_STAT(rx_xdp_drop),
+	AIROHA_ETHTOOL_XDP_STAT(rx_xdp_tx),
+	AIROHA_ETHTOOL_XDP_STAT(rx_xdp_tx_errors),
+	AIROHA_ETHTOOL_XDP_STAT(tx_xdp_xmit),
+	AIROHA_ETHTOOL_XDP_STAT(tx_xdp_xmit_errors),
+};
+
+static void airoha_ethtool_get_strings(struct net_device *netdev, u32 stringset, u8 *data)
+{
+	int i;
+
+	switch (stringset) {
+	case ETH_SS_STATS:
+		for (i = 0; i < ARRAY_SIZE(airoha_ethtool_xdp_stats); i++) {
+			strscpy(data, airoha_ethtool_xdp_stats[i].name, ETH_GSTRING_LEN);
+			data += ETH_GSTRING_LEN;
+		}
+		page_pool_ethtool_stats_get_strings(data);
+		break;
+	}
+}
+
+static int airoha_ethtool_get_sset_count(struct net_device *netdev, int stringset)
+{
+	switch (stringset) {
+	case ETH_SS_STATS:
+		return ARRAY_SIZE(airoha_ethtool_xdp_stats) + page_pool_ethtool_stats_get_count();
+	default:
+		return -EOPNOTSUPP;
+	}
+}
+
+static void airoha_ethtool_get_ethtool_stats(struct net_device *netdev,
+					     struct ethtool_stats *stats,
+					     u64 *data)
+{
+	struct airoha_gdm_dev *dev = netdev_priv(netdev);
+	struct airoha_hw_stats *hw_stats = &dev->stats;
+	u64 *hw_stats_base = (u64 *)hw_stats;
+	struct page_pool_stats pp_stats = {};
+	struct airoha_qdma *qdma;
+	unsigned int start;
+	int i;
+
+	if (netif_running(netdev))
+		airoha_update_hw_stats(dev);
+
+	do {
+		start = u64_stats_fetch_begin(&hw_stats->syncp);
+		for (i = 0; i < ARRAY_SIZE(airoha_ethtool_xdp_stats); i++)
+			data[i] = hw_stats_base[airoha_ethtool_xdp_stats[i].offset];
+	} while (u64_stats_fetch_retry(&hw_stats->syncp, start));
+
+	rcu_read_lock();
+	qdma = rcu_dereference(dev->qdma);
+	if (qdma) {
+		for (i = 0; i < ARRAY_SIZE(qdma->q_rx); i++) {
+			struct airoha_queue *q = &qdma->q_rx[i];
+
+			if (q->page_pool)
+				page_pool_get_stats(q->page_pool, &pp_stats);
+		}
+	}
+	rcu_read_unlock();
+
+	page_pool_ethtool_stats_get(data + ARRAY_SIZE(airoha_ethtool_xdp_stats),
+				    &pp_stats);
+}
+
 static const struct ethtool_ops airoha_ethtool_ops = {
 	.get_drvinfo		= airoha_ethtool_get_drvinfo,
 	.get_eth_mac_stats      = airoha_ethtool_get_mac_stats,
 	.get_rmon_stats		= airoha_ethtool_get_rmon_stats,
 	.get_link_ksettings	= phy_ethtool_get_link_ksettings,
 	.get_link		= ethtool_op_get_link,
+	.get_strings		= airoha_ethtool_get_strings,
+	.get_sset_count		= airoha_ethtool_get_sset_count,
+	.get_ethtool_stats	= airoha_ethtool_get_ethtool_stats,
 };
 
 static int airoha_metadata_dst_alloc(struct airoha_gdm_port *port)
diff --git a/drivers/net/ethernet/airoha/airoha_eth.h b/drivers/net/ethernet/airoha/airoha_eth.h
index cfbc0b8f5bf9..d2ac295c49e9 100644
--- a/drivers/net/ethernet/airoha/airoha_eth.h
+++ b/drivers/net/ethernet/airoha/airoha_eth.h
@@ -236,6 +236,18 @@ struct airoha_tx_irq_queue {
 	u32 *q;
 };
 
+struct airoha_xdp_stats {
+	u64 rx_xdp_redirect;
+	u64 rx_xdp_redirect_errors;
+	u64 rx_xdp_pass;
+	u64 rx_xdp_aborted;
+	u64 rx_xdp_drop;
+	u64 rx_xdp_tx;
+	u64 rx_xdp_tx_errors;
+	u64 tx_xdp_xmit;
+	u64 tx_xdp_xmit_errors;
+};
+
 struct airoha_hw_stats {
 	struct u64_stats_sync syncp;
 
@@ -259,6 +271,8 @@ struct airoha_hw_stats {
 	u64 rx_jabber;
 	u64 rx_len[7];
 
+	struct airoha_xdp_stats xdp_stats;
+
 	struct {
 		/* Previous HW register values for 32-bit counter delta
 		 * tracking. Storing the last seen value and accumulating
-- 
2.55.0


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

* Re: [PATCH net-next 1/2] net: airoha: Add XDP support
  2026-09-20 15:43 ` [PATCH net-next 1/2] " Til Kaiser
@ 2026-09-21  7:30   ` Lorenzo Bianconi
  0 siblings, 0 replies; 5+ messages in thread
From: Lorenzo Bianconi @ 2026-09-21  7:30 UTC (permalink / raw)
  To: Til Kaiser
  Cc: netdev, lorenzo, andrew+netdev, davem, edumazet, kuba, pabeni,
	linux-arm-kernel, linux-mediatek, linux-kernel

[-- Attachment #1: Type: text/plain, Size: 22666 bytes --]

> Implement eXpress Data Path (XDP) support for the Airoha Ethernet driver.

Hi Til,

thanks to work on it. Some comments inline.

Regards,
Lorenzo

> 
> XDP programs are attached per net_device via the ndo_bpf hook. The BPF
> program reference is stored as an RCU-protected pointer in struct
> airoha_gdm_dev and replaced atomically on program load/unload via
> rcu_replace_pointer().
> 
> To support XDP, the following changes are made to the RX path:
> 
> - The page pool DMA direction is switched from DMA_FROM_DEVICE to
>   DMA_BIDIRECTIONAL, which is required for XDP_TX since the same page
>   can be reused for transmit.
> - The RX headroom is increased from NET_SKB_PAD to XDP_PACKET_HEADROOM
>   so that XDP programs have the required headroom available in the data
>   buffer.
> - RX buffers use a full page rather than a half-page fragment. This leaves
>   sufficient space for XDP headroom, skb_shared_info tailroom, and a
>   standard 1500-byte MTU.
> - AIROHA_RX_MAX_BUF_SIZE expresses the maximum MTU that fits in an RX
>   buffer after headroom, shared-info, Ethernet, VLAN, and FCS overhead.
>   MTU changes are rejected when an XDP program is loaded and the requested
>   MTU would exceed this limit.
> - xdp_rxq_info is registered for each RX queue during initialisation and
>   unregistered on cleanup, with the page pool set as the memory model.
> 
> The XDP program is run in airoha_run_xdp() for each received buffer.
> The following actions are supported:
> 
> - XDP_PASS: the buffer is passed to the normal networking stack.
> - XDP_TX: the buffer is transmitted back out the same interface using
>   the new airoha_xdp_xmit_back() helper.
> - XDP_REDIRECT: the buffer is redirected to another interface or map
>   via xdp_do_redirect(), with xdp_do_flush() deferred to the end of
>   the NAPI poll via a per-queue xdp_flush flag.
> - XDP_DROP (and unknown actions): the page is returned to the page pool.
> 
> Two new TX path helpers are introduced:
> 
> - airoha_xdp_submit_frame() maps an xdp_frame for DMA (or reuses the
>   page pool DMA address for XDP_TX) and enqueues it into a TX ring,
>   handling multi-buffer frames via skb_shared_info fragments.
> - airoha_xdp_xmit_back() converts an xdp_buff to an xdp_frame and
>   submits it to the TX queue corresponding to the current CPU.
> 
> The ndo_xdp_xmit handler airoha_xdp_xmit() is also added to allow
> XDP redirect from other drivers into the Airoha interface. Frames are
> submitted using airoha_xdp_submit_frame() with dma_map=true.
> 
> struct airoha_queue_entry is extended with an is_xdpf flag and an
> xdp_frame pointer (in a union with the existing sk_buff pointer) so
> that the TX completion path can correctly free either XDP frames or
> SKBs.
> 
> The advertised XDP feature flags are set to:
>   NETDEV_XDP_ACT_BASIC | NETDEV_XDP_ACT_REDIRECT | NETDEV_XDP_ACT_NDO_XMIT
> 
> Signed-off-by: Til Kaiser <mail@tk154.de>
> ---
>  drivers/net/ethernet/airoha/airoha_eth.c | 386 +++++++++++++++++++++--
>  drivers/net/ethernet/airoha/airoha_eth.h |  19 +-
>  2 files changed, 375 insertions(+), 30 deletions(-)
> 
> diff --git a/drivers/net/ethernet/airoha/airoha_eth.c b/drivers/net/ethernet/airoha/airoha_eth.c
> index 64619e9a704d..71b25f225a8a 100644
> --- a/drivers/net/ethernet/airoha/airoha_eth.c
> +++ b/drivers/net/ethernet/airoha/airoha_eth.c
> @@ -13,6 +13,7 @@
>  #include <net/page_pool/helpers.h>
>  #include <net/pkt_cls.h>
>  #include <uapi/linux/ppp_defs.h>
> +#include <linux/bpf_trace.h>

please respect alphabetic order

>  
>  #include "airoha_regs.h"
>  #include "airoha_eth.h"
> @@ -657,6 +658,255 @@ airoha_qdma_get_gdm_dev(struct airoha_eth *eth, struct airoha_qdma_desc *desc)
>  	return port->devs[d] ? port->devs[d] : ERR_PTR(-ENODEV);
>  }
>  
> +static void airoha_unmap_xmit_buf(struct airoha_eth *eth,
> +				  struct airoha_queue_entry *e)
> +{
> +	switch (e->dma_type) {
> +	case AIROHA_DMA_MAP_PAGE:
> +		dma_unmap_page(eth->dev, e->dma_addr, e->dma_len,
> +			       DMA_TO_DEVICE);
> +		break;
> +	case AIROHA_DMA_MAP_SINGLE:
> +		dma_unmap_single(eth->dev, e->dma_addr, e->dma_len,
> +				 DMA_TO_DEVICE);
> +		break;
> +	case AIROHA_DMA_UNMAPPED:
> +	default:
> +		break;
> +	}
> +	e->dma_type = AIROHA_DMA_UNMAPPED;
> +}
> +
> +static int airoha_xdp_submit_frame(struct airoha_gdm_dev *dev,
> +				   struct xdp_frame *xdpf,
> +				   struct airoha_queue *q,
> +				   int qid, u32 msg0, u32 msg1,
> +				   bool dma_map)

can you please run checkpatch.pl on the patch?

> +{
> +	struct airoha_queue_entry *e, *next_e;
> +	int len = xdpf->len, nr_frags, i;
> +	struct airoha_qdma_desc *desc;
> +	struct skb_shared_info *sinfo;
> +	void *data = xdpf->data;
> +	u16 index, next_index;
> +	LIST_HEAD(tx_list);
> +	dma_addr_t addr;
> +	u32 val;
> +
> +	sinfo = xdp_get_shared_info_from_frame(xdpf);
> +	nr_frags = unlikely(xdp_frame_has_frags(xdpf)) ? sinfo->nr_frags : 0;
> +
> +	if (q->queued >= q->ndesc - 1 - nr_frags)

nit: I guess it is more readable if you do:

	if (q->queued + 1 + nr_frags >= q->ndesc)
		return -EBUSY;

> +		return -EBUSY;
> +
> +	for (i = 0; i <= nr_frags; i++) {
> +		if (dma_map) {
> +			if (i == 0)
> +				addr = dma_map_single(dev->eth->dev, data, len,
> +						      DMA_TO_DEVICE);
> +			else
> +				addr = dma_map_page(dev->eth->dev, virt_to_page(data),
> +						    offset_in_page(data), len,
> +						    DMA_TO_DEVICE);
> +			if (unlikely(dma_mapping_error(dev->eth->dev, addr)))
> +				goto unmap;
> +		} else {
> +			struct page *page = virt_to_head_page(data);
> +
> +			addr = page_pool_get_dma_addr(page) +
> +			       (data - (void *)page_address(page));
> +			dma_sync_single_for_device(dev->eth->dev, addr, len,
> +						   DMA_BIDIRECTIONAL);
> +		}
> +
> +		e = list_first_entry(&q->tx_list, struct airoha_queue_entry, list);
> +		list_move_tail(&e->list, &tx_list);
> +
> +		index = e - q->entry;
> +		desc = &q->desc[index];
> +
> +		e->is_xdpf = true;
> +		e->xdpf = (i == nr_frags) ? xdpf : NULL;

nit: you do not need brackets here.

> +		e->dma_addr = addr;
> +		e->dma_len = len;
> +		if (dma_map)
> +			e->dma_type = i == 0 ? AIROHA_DMA_MAP_SINGLE : AIROHA_DMA_MAP_PAGE;
> +		else
> +			e->dma_type = AIROHA_DMA_UNMAPPED;

I thin you can move this chunk in the above if/else block

> +
> +		next_e = list_first_entry(&q->tx_list, struct airoha_queue_entry, list);
> +		next_index = next_e - q->entry;
> +
> +		val = FIELD_PREP(QDMA_DESC_LEN_MASK, len);
> +		if (i < nr_frags)
> +			val |= FIELD_PREP(QDMA_DESC_MORE_MASK, 1);
> +		WRITE_ONCE(desc->ctrl, cpu_to_le32(val));
> +		WRITE_ONCE(desc->addr, cpu_to_le32(addr));
> +		val = FIELD_PREP(QDMA_DESC_NEXT_ID_MASK, next_index);
> +		WRITE_ONCE(desc->data, cpu_to_le32(val));
> +		WRITE_ONCE(desc->msg0, cpu_to_le32(msg0));
> +		WRITE_ONCE(desc->msg1, cpu_to_le32(msg1));
> +		WRITE_ONCE(desc->msg2, cpu_to_le32(0xffff));
> +
> +		q->queued++;
> +
> +		if (i < nr_frags) {
> +			skb_frag_t *frag = &sinfo->frags[i];
> +
> +			data = skb_frag_address(frag);
> +			len = skb_frag_size(frag);
> +		}
> +	}
> +
> +	return next_index;
> +
> +unmap:
> +	list_for_each_entry(e, &tx_list, list) {
> +		airoha_unmap_xmit_buf(dev->eth, e);
> +		e->is_xdpf = false;
> +		e->xdpf = NULL;
> +		q->queued--;
> +	}
> +	list_splice(&tx_list, &q->tx_list);
> +
> +	return -ENOMEM;
> +}
> +
> +static int airoha_xdp_xmit_back(struct net_device *netdev, struct xdp_buff *xdp,
> +				struct airoha_qdma *qdma)
> +{
> +	struct airoha_gdm_dev *dev = netdev_priv(netdev);
> +	struct airoha_queue *q;
> +	struct xdp_frame *xdpf;
> +	u32 msg0, msg1;
> +	int qid, index;
> +	u8 fport;
> +
> +	xdpf = xdp_convert_buff_to_frame(xdp);
> +	if (unlikely(!xdpf))
> +		return -EOVERFLOW;
> +
> +	qid = airoha_qdma_get_txq(qdma, smp_processor_id());
> +	q = &qdma->q_tx[qid];
> +
> +	msg0 = FIELD_PREP(QDMA_ETH_TXMSG_CHAN_MASK,
> +			  qid / AIROHA_NUM_QOS_QUEUES) |
> +	       FIELD_PREP(QDMA_ETH_TXMSG_QUEUE_MASK,
> +			  qid % AIROHA_NUM_QOS_QUEUES);
> +
> +	fport = airoha_get_fe_port(dev);
> +	msg1 = FIELD_PREP(QDMA_ETH_TXMSG_NBOQ_MASK, dev->nbq) |
> +	       FIELD_PREP(QDMA_ETH_TXMSG_FPORT_MASK, fport) |
> +	       FIELD_PREP(QDMA_ETH_TXMSG_METER_MASK, 0x7f);

msg0/msg1 are in common with airoha_xdp_xmit(), right? Can you please move them
in airoha_xdp_submit_frame()?

> +
> +	spin_lock(&q->lock);
> +	index = airoha_xdp_submit_frame(dev, xdpf, q, qid, msg0, msg1, false);
> +	if (unlikely(index < 0)) {
> +		spin_unlock(&q->lock);
> +		return index;
> +	}
> +
> +	airoha_qdma_rmw(qdma, REG_TX_CPU_IDX(qid),
> +			TX_RING_CPU_IDX_MASK,
> +			FIELD_PREP(TX_RING_CPU_IDX_MASK, index));
> +	spin_unlock(&q->lock);
> +
> +	return 0;
> +}
> +
> +static int airoha_xdp_xmit(struct net_device *netdev, int n,
> +			   struct xdp_frame **frames, u32 flags)
> +{
> +	int qid, index = 0, last_index = -1, i, drops = 0;
> +	struct airoha_gdm_dev *dev = netdev_priv(netdev);
> +	struct airoha_qdma *qdma;
> +	struct airoha_queue *q;
> +	u32 msg0, msg1;
> +	u8 fport;
> +
> +	if (unlikely(flags & ~XDP_XMIT_FLAGS_MASK))
> +		return -EINVAL;
> +
> +	rcu_read_lock();
> +	qdma = rcu_dereference(dev->qdma);
> +	if (!qdma) {
> +		rcu_read_unlock();
> +		return -ENODEV;
> +	}
> +
> +	qid = airoha_qdma_get_txq(qdma, smp_processor_id());
> +	q = &qdma->q_tx[qid];
> +
> +	msg0 = FIELD_PREP(QDMA_ETH_TXMSG_CHAN_MASK,
> +			  qid / AIROHA_NUM_QOS_QUEUES) |
> +	       FIELD_PREP(QDMA_ETH_TXMSG_QUEUE_MASK,
> +			  qid % AIROHA_NUM_QOS_QUEUES);
> +
> +	fport = airoha_get_fe_port(dev);
> +	msg1 = FIELD_PREP(QDMA_ETH_TXMSG_NBOQ_MASK, dev->nbq) |
> +	       FIELD_PREP(QDMA_ETH_TXMSG_FPORT_MASK, fport) |
> +	       FIELD_PREP(QDMA_ETH_TXMSG_METER_MASK, 0x7f);
> +
> +	spin_lock(&q->lock);
> +	for (i = 0; i < n; i++) {
> +		index = airoha_xdp_submit_frame(dev, frames[i], q, qid, msg0, msg1, true);
> +		if (unlikely(index < 0)) {
> +			xdp_return_frame_rx_napi(frames[i]);
> +			drops++;
> +		} else {
> +			last_index = index;
> +		}
> +	}
> +	if (n > drops && (flags & XDP_XMIT_FLUSH))

Do you mean to skip it if drops == n? If so, I guess it is more readable to
count number of successful transmissions


> +		airoha_qdma_rmw(qdma, REG_TX_CPU_IDX(qid),
> +				TX_RING_CPU_IDX_MASK,
> +				FIELD_PREP(TX_RING_CPU_IDX_MASK, last_index));
> +	spin_unlock(&q->lock);
> +	rcu_read_unlock();
> +
> +	return n - drops;

same here

> +}
> +
> +static bool airoha_run_xdp(struct net_device *netdev, struct bpf_prog *prog,
> +			   struct xdp_buff *xdp, struct airoha_queue *q,
> +			   struct airoha_queue_entry *e, struct page *page)
> +{
> +	u32 act = bpf_prog_run_xdp(prog, xdp);
> +
> +	switch (act) {
> +	case XDP_PASS:
> +		return false;
> +	case XDP_TX:
> +		if (unlikely(airoha_xdp_xmit_back(netdev, xdp, q->qdma) < 0)) {
> +			trace_xdp_exception(netdev, prog, act);
> +			page_pool_put_full_page(q->page_pool, page, true);
> +		} else {
> +			e->buf = NULL;
> +		}
> +		break;
> +	case XDP_REDIRECT:
> +		if (unlikely(xdp_do_redirect(netdev, xdp, prog) < 0)) {
> +			trace_xdp_exception(netdev, prog, act);
> +			page_pool_put_full_page(q->page_pool, page, true);
> +		} else {
> +			q->xdp_flush = true;
> +			e->buf = NULL;
> +		}
> +		break;
> +	default:
> +		bpf_warn_invalid_xdp_action(netdev, prog, act);
> +		fallthrough;
> +	case XDP_ABORTED:
> +		trace_xdp_exception(netdev, prog, act);
> +		fallthrough;
> +	case XDP_DROP:
> +		page_pool_put_full_page(q->page_pool, page, true);
> +		break;
> +	}
> +
> +	return true;
> +}
> +
>  static int airoha_qdma_rx_process(struct airoha_queue *q, int budget)
>  {
>  	enum dma_data_direction dir = page_pool_get_dma_dir(q->page_pool);
> @@ -698,6 +948,27 @@ static int airoha_qdma_rx_process(struct airoha_queue *q, int budget)
>  
>  		netdev = netdev_from_priv(dev);
>  		if (!q->skb) { /* first buffer */
> +			struct bpf_prog *xdp_prog;

nit: prog seems better to me

> +
> +			rcu_read_lock();
> +			xdp_prog = rcu_dereference(dev->xdp_prog);
> +			if (xdp_prog) {
> +				struct xdp_buff xdp;
> +
> +				xdp_init_buff(&xdp, q->buf_size, &q->xdp_rxq);
> +				xdp_prepare_buff(&xdp, e->buf - AIROHA_RX_HEADROOM,
> +						 AIROHA_RX_HEADROOM, len, false);
> +
> +				if (airoha_run_xdp(netdev, xdp_prog, &xdp, q, e, page)) {
> +					rcu_read_unlock();
> +					continue;
> +				}
> +
> +				len = xdp.data_end - xdp.data;
> +				e->buf = xdp.data;
> +			}
> +			rcu_read_unlock();
> +
>  			q->skb = napi_build_skb(e->buf - AIROHA_RX_HEADROOM,
>  						q->buf_size);
>  			if (!q->skb)
> @@ -779,6 +1050,11 @@ static int airoha_qdma_rx_napi_poll(struct napi_struct *napi, int budget)
>  		done += cur;
>  	} while (cur && done < budget);
>  
> +	if (q->xdp_flush) {
> +		xdp_do_flush();
> +		q->xdp_flush = false;
> +	}
> +
>  	if (done < budget && napi_complete(napi)) {
>  		struct airoha_qdma *qdma = q->qdma;
>  		int i, qid = q - &qdma->q_rx[0];
> @@ -804,17 +1080,17 @@ static int airoha_qdma_init_rx_queue(struct airoha_queue *q,
>  		.order = 0,
>  		.pool_size = 256,
>  		.flags = PP_FLAG_DMA_MAP | PP_FLAG_DMA_SYNC_DEV,
> -		.dma_dir = DMA_FROM_DEVICE,
> +		.dma_dir = DMA_BIDIRECTIONAL,
>  		.max_len = PAGE_SIZE,
>  		.nid = NUMA_NO_NODE,
>  		.dev = qdma->eth->dev,
>  		.napi = &q->napi,
>  	};
> +	int qid = q - &qdma->q_rx[0], thr, err;
>  	struct airoha_eth *eth = qdma->eth;
> -	int qid = q - &qdma->q_rx[0], thr;
>  	dma_addr_t dma_addr;
>  
> -	q->buf_size = PAGE_SIZE / 2;
> +	q->buf_size = PAGE_SIZE;

I guess this can introduce a performance penalty in the non-xdp case since now
we need a full page for a single buffer while in the current codebase we can
have 2 fragments in a single page. Moreover, it seems to me you are not using
latest codebase here.

>  	q->qdma = qdma;
>  
>  	q->entry = devm_kzalloc(eth->dev, ndesc * sizeof(*q->entry),
> @@ -829,15 +1105,22 @@ static int airoha_qdma_init_rx_queue(struct airoha_queue *q,
>  
>  	q->page_pool = page_pool_create(&pp_params);
>  	if (IS_ERR(q->page_pool)) {
> -		int err = PTR_ERR(q->page_pool);
> -
> -		q->page_pool = NULL;
> -		return err;
> +		err = PTR_ERR(q->page_pool);
> +		goto err_page_pool_create;
>  	}
>  
>  	q->ndesc = ndesc;
>  	netif_napi_add(eth->napi_dev, &q->napi, airoha_qdma_rx_napi_poll);
>  
> +	err = xdp_rxq_info_reg(&q->xdp_rxq, eth->napi_dev, qid, q->napi.napi_id);
> +	if (err)
> +		goto err_xdp_rxq_info_reg;
> +
> +	err = xdp_rxq_info_reg_mem_model(&q->xdp_rxq, MEM_TYPE_PAGE_POOL,
> +					 q->page_pool);
> +	if (err)
> +		goto err_xdp_rxq_info_reg_mem_model;
> +
>  	airoha_qdma_wr(qdma, REG_RX_RING_BASE(qid), dma_addr);
>  	airoha_qdma_rmw(qdma, REG_RX_RING_SIZE(qid),
>  			RX_RING_SIZE_MASK,
> @@ -853,6 +1136,14 @@ static int airoha_qdma_init_rx_queue(struct airoha_queue *q,
>  	airoha_qdma_fill_rx_queue(q);
>  
>  	return 0;
> +
> +err_xdp_rxq_info_reg_mem_model:
> +	xdp_rxq_info_unreg(&q->xdp_rxq);
> +err_xdp_rxq_info_reg:
> +	page_pool_destroy(q->page_pool);
> +err_page_pool_create:
> +	q->page_pool = NULL;
> +	return err;
>  }
>  
>  static void airoha_qdma_cleanup_rx_queue(struct airoha_queue *q)
> @@ -882,6 +1173,9 @@ static void airoha_qdma_cleanup_rx_queue(struct airoha_queue *q)
>  		q->queued--;
>  	}
>  
> +	if (xdp_rxq_info_is_reg(&q->xdp_rxq))
> +		xdp_rxq_info_unreg(&q->xdp_rxq);
> +
>  	q->head = q->tail;
>  	/* Set RX_DMA_IDX to RX_CPU_IDX to notify the hw the QDMA RX ring is
>  	 * empty.
> @@ -949,25 +1243,6 @@ static void airoha_qdma_wake_netdev_txqs(struct airoha_queue *q)
>  	q->txq_stopped = false;
>  }
>  
> -static void airoha_unmap_xmit_buf(struct airoha_eth *eth,
> -				  struct airoha_queue_entry *e)
> -{
> -	switch (e->dma_type) {
> -	case AIROHA_DMA_MAP_PAGE:
> -		dma_unmap_page(eth->dev, e->dma_addr, e->dma_len,
> -			       DMA_TO_DEVICE);
> -		break;
> -	case AIROHA_DMA_MAP_SINGLE:
> -		dma_unmap_single(eth->dev, e->dma_addr, e->dma_len,
> -				 DMA_TO_DEVICE);
> -		break;
> -	case AIROHA_DMA_UNMAPPED:
> -	default:
> -		break;
> -	}
> -	e->dma_type = AIROHA_DMA_UNMAPPED;
> -}
> -
>  static int airoha_qdma_tx_napi_poll(struct napi_struct *napi, int budget)
>  {
>  	struct airoha_tx_irq_queue *irq_q;
> @@ -1037,7 +1312,12 @@ static int airoha_qdma_tx_napi_poll(struct napi_struct *napi, int budget)
>  		WRITE_ONCE(desc->msg1, 0);
>  		q->queued--;
>  
> -		if (skb) {
> +		if (e->is_xdpf) {
> +			if (e->xdpf)
> +				xdp_return_frame(e->xdpf);
> +			e->is_xdpf = false;
> +			e->xdpf = NULL;
> +		} else if (e->skb) {
>  			struct airoha_gdm_dev *dev = netdev_priv(skb->dev);
>  			u16 qidx = skb_get_queue_mapping(skb);
>  			struct netdev_queue *txq;
> @@ -1218,7 +1498,12 @@ static void airoha_qdma_tx_cleanup(struct airoha_qdma *qdma)
>  			WRITE_ONCE(desc->msg1, 0);
>  			WRITE_ONCE(desc->msg2, 0);
>  
> -			if (skb) {
> +			if (e->is_xdpf) {
> +				if (e->xdpf)
> +					xdp_return_frame(e->xdpf);
> +				e->is_xdpf = false;
> +				e->xdpf = NULL;
> +			} else if (skb) {
>  				struct netdev_queue *txq;
>  
>  				txq = skb_get_tx_queue(skb->dev, skb);
> @@ -2201,6 +2486,12 @@ static int airoha_dev_change_mtu(struct net_device *netdev, int mtu)
>  	struct airoha_gdm_dev *dev = netdev_priv(netdev);
>  	struct airoha_gdm_port *port = dev->port;
>  
> +	if (rcu_access_pointer(dev->xdp_prog) && mtu > AIROHA_RX_MAX_BUF_SIZE) {
> +		netdev_err(netdev, "MTU too large for XDP (max %lu)\n",
> +			   AIROHA_RX_MAX_BUF_SIZE);
> +		return -EINVAL;
> +	}
> +
>  	WRITE_ONCE(netdev->mtu, mtu);
>  	if (port->users)
>  		airoha_dev_set_xmit_frame_size(netdev);
> @@ -2377,6 +2668,7 @@ static netdev_tx_t airoha_dev_xmit(struct sk_buff *skb,
>  
>  		list_move_tail(&e->list, &tx_list);
>  		e->skb = i == nr_frags - 1 ? skb : NULL;
> +		e->is_xdpf = false;
>  		e->dma_addr = addr;
>  		e->dma_len = len;
>  
> @@ -3321,6 +3613,39 @@ static int airoha_tc_setup_qdisc_htb(struct net_device *netdev,
>  	return 0;
>  }
>  
> +static int airoha_xdp_setup(struct net_device *netdev, struct bpf_prog *prog,
> +			    struct netlink_ext_ack *extack)
> +{
> +	struct airoha_gdm_dev *dev = netdev_priv(netdev);
> +	struct bpf_prog *old_prog;
> +
> +	if (netdev->features & NETIF_F_LRO) {
> +		NL_SET_ERR_MSG_MOD(extack, "XDP is not supported with LRO");
> +		return -EOPNOTSUPP;
> +	}
> +
> +	if (netdev->mtu > AIROHA_RX_MAX_BUF_SIZE) {
> +		NL_SET_ERR_MSG_MOD(extack, "MTU too large for XDP");
> +		return -EOPNOTSUPP;
> +	}
> +
> +	old_prog = rcu_replace_pointer(dev->xdp_prog, prog, lockdep_rtnl_is_held());
> +	if (old_prog)
> +		bpf_prog_put(old_prog);
> +
> +	return 0;
> +}
> +
> +static int airoha_xdp_bpf(struct net_device *netdev, struct netdev_bpf *xdp)

nit: airoha_dev_bpf()?

> +{
> +	switch (xdp->command) {
> +	case XDP_SETUP_PROG:
> +		return airoha_xdp_setup(netdev, xdp->prog, xdp->extack);
> +	default:
> +		return -EINVAL;
> +	}
> +}
> +
>  static int airoha_dev_tc_setup(struct net_device *dev,
>  			       enum tc_setup_type type, void *type_data)
>  {
> @@ -3347,6 +3672,8 @@ static const struct net_device_ops airoha_netdev_ops = {
>  	.ndo_get_stats64        = airoha_dev_get_stats64,
>  	.ndo_set_mac_address	= airoha_dev_set_macaddr,
>  	.ndo_setup_tc		= airoha_dev_tc_setup,
> +	.ndo_bpf		= airoha_xdp_bpf,
> +	.ndo_xdp_xmit		= airoha_xdp_xmit,
>  };
>  
>  static const struct ethtool_ops airoha_ethtool_ops = {
> @@ -3435,6 +3762,9 @@ static int airoha_alloc_gdm_device(struct airoha_eth *eth,
>  			      NETIF_F_HW_TC;
>  	netdev->features |= netdev->hw_features;
>  	netdev->vlan_features = netdev->hw_features;
> +	netdev->xdp_features = NETDEV_XDP_ACT_BASIC |
> +				NETDEV_XDP_ACT_REDIRECT |
> +				NETDEV_XDP_ACT_NDO_XMIT;

please align it:

	netdev->xdp_features = NETDEV_XDP_ACT_BASIC | NETDEV_XDP_ACT_REDIRECT |
			       NETDEV_XDP_ACT_NDO_XMIT;

>  	SET_NETDEV_DEV(netdev, eth->dev);
>  
>  	/* reserve hw queues for HTB offloading */
> diff --git a/drivers/net/ethernet/airoha/airoha_eth.h b/drivers/net/ethernet/airoha/airoha_eth.h
> index 8277c1c87bb3..cfbc0b8f5bf9 100644
> --- a/drivers/net/ethernet/airoha/airoha_eth.h
> +++ b/drivers/net/ethernet/airoha/airoha_eth.h
> @@ -7,6 +7,7 @@
>  #ifndef AIROHA_ETH_H
>  #define AIROHA_ETH_H
>  
> +#include <linux/bpf.h>
>  #include <linux/debugfs.h>
>  #include <linux/etherdevice.h>
>  #include <linux/iopoll.h>
> @@ -15,6 +16,7 @@
>  #include <linux/reset.h>
>  #include <linux/soc/airoha/airoha_offload.h>
>  #include <net/dsa.h>
> +#include <net/xdp.h>
>  
>  #define AIROHA_MAX_NUM_GDM_PORTS	4
>  #define AIROHA_MAX_NUM_GDM_DEVS		2
> @@ -34,7 +36,11 @@
>  #define AIROHA_FE_MC_MAX_VLAN_TABLE	64
>  #define AIROHA_FE_MC_MAX_VLAN_PORT	16
>  #define AIROHA_NUM_TX_IRQ		2
> -#define AIROHA_RX_HEADROOM		(NET_SKB_PAD + NET_IP_ALIGN)
> +#define AIROHA_RX_HEADROOM		(XDP_PACKET_HEADROOM + NET_IP_ALIGN)
> +#define AIROHA_RX_PAD			(AIROHA_RX_HEADROOM + \
> +					 SKB_DATA_ALIGN(sizeof(struct skb_shared_info)))
> +#define AIROHA_RX_MAX_BUF_SIZE		(PAGE_SIZE - AIROHA_RX_PAD - \
> +					 VLAN_ETH_HLEN - ETH_FCS_LEN)

same as above, I think this can introduce a performance penalty in the non-xdp
case. Can you please check? (e.g. on a 10Gbps link).

>  #define AIROHA_RX_LEN(_n)		((_n) - AIROHA_RX_HEADROOM)
>  #define HW_DSCP_NUM			2048
>  #define IRQ_QUEUE_LEN(_n)		((_n) ? 1024 : 2048)
> @@ -182,12 +188,16 @@ struct airoha_queue_entry {
>  		void *buf;
>  		struct {
>  			struct list_head list;
> -			struct sk_buff *skb;
> +			union {
> +				struct sk_buff *skb;
> +				struct xdp_frame *xdpf;
> +			};
>  			enum airoha_dma_map_type dma_type;
>  		};
>  	};
>  	dma_addr_t dma_addr;
>  	u16 dma_len;
> +	bool is_xdpf;
>  };
>  
>  struct airoha_queue {
> @@ -211,6 +221,9 @@ struct airoha_queue {
>  	struct page_pool *page_pool;
>  	struct sk_buff *skb;
>  
> +	struct xdp_rxq_info xdp_rxq;
> +	bool xdp_flush;
> +
>  	struct list_head tx_list;
>  };
>  
> @@ -589,6 +602,8 @@ struct airoha_gdm_dev {
>  	unsigned long flags;
>  	int nbq;
>  
> +	struct bpf_prog __rcu *xdp_prog;

nit: prog

> +
>  	struct airoha_hw_stats stats;
>  
>  	/* Serialize netdev_tx_completed_queue() calls per TX queue during
> -- 
> 2.55.0
> 
> 

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]

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

* Re: [PATCH net-next 2/2] net: airoha: Add XDP statistics
  2026-09-20 15:43 ` [PATCH net-next 2/2] net: airoha: Add XDP statistics Til Kaiser
@ 2026-09-21  8:05   ` Lorenzo Bianconi
  0 siblings, 0 replies; 5+ messages in thread
From: Lorenzo Bianconi @ 2026-09-21  8:05 UTC (permalink / raw)
  To: Til Kaiser
  Cc: netdev, lorenzo, andrew+netdev, davem, edumazet, kuba, pabeni,
	linux-arm-kernel, linux-mediatek, linux-kernel

[-- Attachment #1: Type: text/plain, Size: 10127 bytes --]

> Track and expose per-device XDP statistics via ethtool, covering both
> the RX and TX sides of the XDP data path.
> 
> A new struct airoha_xdp_stats is introduced and embedded in the existing
> struct airoha_hw_stats. It contains the following counters, all protected
> by the existing u64_stats_sync:
> 
>   rx_xdp_pass            - frames passed to the networking stack
>   rx_xdp_aborted         - frames with XDP_ABORTED or unknown action
>   rx_xdp_drop            - frames dropped by the BPF program or on error
>   rx_xdp_tx              - frames sent back out via XDP_TX
>   rx_xdp_tx_errors       - XDP_TX failures
>   rx_xdp_redirect        - frames successfully redirected
>   rx_xdp_redirect_errors - XDP_REDIRECT failures
>   tx_xdp_xmit            - frames successfully submitted via ndo_xdp_xmit
>   tx_xdp_xmit_errors     - frames dropped in ndo_xdp_xmit
> 
> The airoha_run_xdp() function is reworked to update the appropriate
> counter after each XDP action. The airoha_xdp_xmit() ndo_xdp_xmit handler
> is updated to record the number of successfully transmitted and dropped
> frames.
> 
> The ethtool interface is extended with three new callbacks:
> 
> - get_strings: emits the XDP counter names followed by the page pool
>   stat strings obtained from page_pool_ethtool_stats_get_strings().
> - get_sset_count: returns the combined count of XDP and page pool stats.
> - get_ethtool_stats: reads the XDP counters under the u64_stats_sync
>   seqcount and appends page pool stats collected from all active RX
>   queues via page_pool_get_stats().
> 
> PAGE_POOL_STATS is selected in Kconfig to enable the page pool statistics
> infrastructure required by the ethtool callbacks.
> 
> Signed-off-by: Til Kaiser <mail@tk154.de>
> ---
>  drivers/net/ethernet/airoha/Kconfig      |   1 +
>  drivers/net/ethernet/airoha/airoha_eth.c | 131 ++++++++++++++++++++---
>  drivers/net/ethernet/airoha/airoha_eth.h |  14 +++
>  3 files changed, 133 insertions(+), 13 deletions(-)
> 
> diff --git a/drivers/net/ethernet/airoha/Kconfig b/drivers/net/ethernet/airoha/Kconfig
> index 1f6640a15fc9..3fa7683a79e9 100644
> --- a/drivers/net/ethernet/airoha/Kconfig
> +++ b/drivers/net/ethernet/airoha/Kconfig
> @@ -20,6 +20,7 @@ config NET_AIROHA
>  	depends on NET_DSA || !NET_DSA
>  	select NET_AIROHA_NPU
>  	select PAGE_POOL
> +	select PAGE_POOL_STATS
>  	help
>  	  This driver supports the gigabit ethernet MACs in the
>  	  Airoha SoC family.
> diff --git a/drivers/net/ethernet/airoha/airoha_eth.c b/drivers/net/ethernet/airoha/airoha_eth.c
> index 71b25f225a8a..29b837204842 100644
> --- a/drivers/net/ethernet/airoha/airoha_eth.c
> +++ b/drivers/net/ethernet/airoha/airoha_eth.c
> @@ -864,6 +864,11 @@ static int airoha_xdp_xmit(struct net_device *netdev, int n,
>  	spin_unlock(&q->lock);
>  	rcu_read_unlock();
>  
> +	u64_stats_update_begin(&dev->stats.syncp);
> +	dev->stats.xdp_stats.tx_xdp_xmit += n - drops;
> +	dev->stats.xdp_stats.tx_xdp_xmit_errors += drops;
> +	u64_stats_update_end(&dev->stats.syncp);
> +
>  	return n - drops;
>  }
>  
> @@ -871,40 +876,56 @@ static bool airoha_run_xdp(struct net_device *netdev, struct bpf_prog *prog,
>  			   struct xdp_buff *xdp, struct airoha_queue *q,
>  			   struct airoha_queue_entry *e, struct page *page)
>  {
> +	struct airoha_gdm_dev *dev = netdev_priv(netdev);
> +	struct airoha_hw_stats *hw_stats = &dev->stats;
>  	u32 act = bpf_prog_run_xdp(prog, xdp);
> +	u64 *count;
>  
>  	switch (act) {
>  	case XDP_PASS:
> -		return false;
> +		count = &hw_stats->xdp_stats.rx_xdp_pass;
> +		goto update_stats;
>  	case XDP_TX:
>  		if (unlikely(airoha_xdp_xmit_back(netdev, xdp, q->qdma) < 0)) {
> +			count = &hw_stats->xdp_stats.rx_xdp_tx_errors;
>  			trace_xdp_exception(netdev, prog, act);
> -			page_pool_put_full_page(q->page_pool, page, true);
> -		} else {
> -			e->buf = NULL;
> +			break;
>  		}
> -		break;
> +
> +		e->buf = NULL;
> +		count = &hw_stats->xdp_stats.rx_xdp_tx;
> +		goto update_stats;
>  	case XDP_REDIRECT:
>  		if (unlikely(xdp_do_redirect(netdev, xdp, prog) < 0)) {
> +			count = &hw_stats->xdp_stats.rx_xdp_redirect_errors;
>  			trace_xdp_exception(netdev, prog, act);
> -			page_pool_put_full_page(q->page_pool, page, true);
> -		} else {
> -			q->xdp_flush = true;
> -			e->buf = NULL;
> +			break;
>  		}
> -		break;
> +
> +		e->buf = NULL;
> +		q->xdp_flush = true;
> +		count = &hw_stats->xdp_stats.rx_xdp_redirect;
> +		goto update_stats;
>  	default:
>  		bpf_warn_invalid_xdp_action(netdev, prog, act);
>  		fallthrough;
>  	case XDP_ABORTED:
> +		count = &hw_stats->xdp_stats.rx_xdp_aborted;
>  		trace_xdp_exception(netdev, prog, act);
> -		fallthrough;
> +		break;
>  	case XDP_DROP:
> -		page_pool_put_full_page(q->page_pool, page, true);
> +		count = &hw_stats->xdp_stats.rx_xdp_drop;
>  		break;
>  	}
>  
> -	return true;
> +	page_pool_put_full_page(q->page_pool, page, true);
> +
> +update_stats:
> +	u64_stats_update_begin(&hw_stats->syncp);
> +	*count = *count + 1;
> +	u64_stats_update_end(&hw_stats->syncp);

Would it better if we update the stats in airoha_qdma_rx_process() passing a
airoha_xdp_stats struct to airoha_run_xdp(). I guess doing so we can even drop
xdp_flush field in airoha_qdma_queue struct.

> +
> +	return act != XDP_PASS;
>  }
>  
>  static int airoha_qdma_rx_process(struct airoha_queue *q, int budget)
> @@ -3676,12 +3697,96 @@ static const struct net_device_ops airoha_netdev_ops = {
>  	.ndo_xdp_xmit		= airoha_xdp_xmit,
>  };
>  
> +#define AIROHA_ETHTOOL_XDP_STAT(x) { #x, \
> +				  offsetof(struct airoha_hw_stats, xdp_stats.x) / \
> +				  sizeof(u64) }
> +
> +static const struct {
> +	const char name[ETH_GSTRING_LEN];
> +	u32 offset;
> +} airoha_ethtool_xdp_stats[] = {
> +	AIROHA_ETHTOOL_XDP_STAT(rx_xdp_redirect),
> +	AIROHA_ETHTOOL_XDP_STAT(rx_xdp_redirect_errors),
> +	AIROHA_ETHTOOL_XDP_STAT(rx_xdp_pass),
> +	AIROHA_ETHTOOL_XDP_STAT(rx_xdp_aborted),
> +	AIROHA_ETHTOOL_XDP_STAT(rx_xdp_drop),
> +	AIROHA_ETHTOOL_XDP_STAT(rx_xdp_tx),
> +	AIROHA_ETHTOOL_XDP_STAT(rx_xdp_tx_errors),
> +	AIROHA_ETHTOOL_XDP_STAT(tx_xdp_xmit),
> +	AIROHA_ETHTOOL_XDP_STAT(tx_xdp_xmit_errors),
> +};
> +
> +static void airoha_ethtool_get_strings(struct net_device *netdev, u32 stringset, u8 *data)

nit I like old style :)

	static void airoha_ethtool_get_strings(struct net_device *netdev,
					       u32 stringset, u8 *data)
	{
		....
	}

> +{
> +	int i;
> +
> +	switch (stringset) {
> +	case ETH_SS_STATS:
> +		for (i = 0; i < ARRAY_SIZE(airoha_ethtool_xdp_stats); i++) {
> +			strscpy(data, airoha_ethtool_xdp_stats[i].name, ETH_GSTRING_LEN);
> +			data += ETH_GSTRING_LEN;
> +		}
> +		page_pool_ethtool_stats_get_strings(data);
> +		break;
> +	}
> +}
> +
> +static int airoha_ethtool_get_sset_count(struct net_device *netdev, int stringset)
> +{
> +	switch (stringset) {
> +	case ETH_SS_STATS:
> +		return ARRAY_SIZE(airoha_ethtool_xdp_stats) + page_pool_ethtool_stats_get_count();

same here:
		return ARRAY_SIZE(airoha_ethtool_xdp_stats) +
		       page_pool_ethtool_stats_get_count();

> +	default:
> +		return -EOPNOTSUPP;
> +	}
> +}
> +
> +static void airoha_ethtool_get_ethtool_stats(struct net_device *netdev,
> +					     struct ethtool_stats *stats,
> +					     u64 *data)
> +{
> +	struct airoha_gdm_dev *dev = netdev_priv(netdev);
> +	struct airoha_hw_stats *hw_stats = &dev->stats;
> +	u64 *hw_stats_base = (u64 *)hw_stats;
> +	struct page_pool_stats pp_stats = {};
> +	struct airoha_qdma *qdma;
> +	unsigned int start;
> +	int i;
> +
> +	if (netif_running(netdev))
> +		airoha_update_hw_stats(dev);
> +
> +	do {
> +		start = u64_stats_fetch_begin(&hw_stats->syncp);
> +		for (i = 0; i < ARRAY_SIZE(airoha_ethtool_xdp_stats); i++)
> +			data[i] = hw_stats_base[airoha_ethtool_xdp_stats[i].offset];
> +	} while (u64_stats_fetch_retry(&hw_stats->syncp, start));
> +
> +	rcu_read_lock();
> +	qdma = rcu_dereference(dev->qdma);

IIRC airoha_ethtool_get_ethtool_stats() is running under RTNL so you can just
use airoha_qdma_deref() here.

Regards,
Lorenzo

> +	if (qdma) {
> +		for (i = 0; i < ARRAY_SIZE(qdma->q_rx); i++) {
> +			struct airoha_queue *q = &qdma->q_rx[i];
> +
> +			if (q->page_pool)
> +				page_pool_get_stats(q->page_pool, &pp_stats);
> +		}
> +	}
> +	rcu_read_unlock();
> +
> +	page_pool_ethtool_stats_get(data + ARRAY_SIZE(airoha_ethtool_xdp_stats),
> +				    &pp_stats);
> +}
> +
>  static const struct ethtool_ops airoha_ethtool_ops = {
>  	.get_drvinfo		= airoha_ethtool_get_drvinfo,
>  	.get_eth_mac_stats      = airoha_ethtool_get_mac_stats,
>  	.get_rmon_stats		= airoha_ethtool_get_rmon_stats,
>  	.get_link_ksettings	= phy_ethtool_get_link_ksettings,
>  	.get_link		= ethtool_op_get_link,
> +	.get_strings		= airoha_ethtool_get_strings,
> +	.get_sset_count		= airoha_ethtool_get_sset_count,
> +	.get_ethtool_stats	= airoha_ethtool_get_ethtool_stats,
>  };
>  
>  static int airoha_metadata_dst_alloc(struct airoha_gdm_port *port)
> diff --git a/drivers/net/ethernet/airoha/airoha_eth.h b/drivers/net/ethernet/airoha/airoha_eth.h
> index cfbc0b8f5bf9..d2ac295c49e9 100644
> --- a/drivers/net/ethernet/airoha/airoha_eth.h
> +++ b/drivers/net/ethernet/airoha/airoha_eth.h
> @@ -236,6 +236,18 @@ struct airoha_tx_irq_queue {
>  	u32 *q;
>  };
>  
> +struct airoha_xdp_stats {
> +	u64 rx_xdp_redirect;
> +	u64 rx_xdp_redirect_errors;
> +	u64 rx_xdp_pass;
> +	u64 rx_xdp_aborted;
> +	u64 rx_xdp_drop;
> +	u64 rx_xdp_tx;
> +	u64 rx_xdp_tx_errors;
> +	u64 tx_xdp_xmit;
> +	u64 tx_xdp_xmit_errors;
> +};
> +
>  struct airoha_hw_stats {
>  	struct u64_stats_sync syncp;
>  
> @@ -259,6 +271,8 @@ struct airoha_hw_stats {
>  	u64 rx_jabber;
>  	u64 rx_len[7];
>  
> +	struct airoha_xdp_stats xdp_stats;
> +
>  	struct {
>  		/* Previous HW register values for 32-bit counter delta
>  		 * tracking. Storing the last seen value and accumulating
> -- 
> 2.55.0
> 
> 

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]

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

end of thread, other threads:[~2026-09-21  8:05 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-20 15:43 [PATCH net-next 0/2] net: airoha: Add XDP support Til Kaiser
2026-09-20 15:43 ` [PATCH net-next 1/2] " Til Kaiser
2026-09-21  7:30   ` Lorenzo Bianconi
2026-09-20 15:43 ` [PATCH net-next 2/2] net: airoha: Add XDP statistics Til Kaiser
2026-09-21  8:05   ` Lorenzo Bianconi

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

all inboxes | Powered by JetHome®