[PATCH net 1/4] net: airoha: handle RX_NO_CPU_DSCP interrupt, not just RX_DONE

From: Vitaliy Sochnev

Date: Sun Aug 30 2026 - 03:58:28 EST


The QDMA hardware raises a dedicated interrupt (NO_CPU_DSCP, one bit
per ring in QDMA_CSR_INT_ENABLE2/3) when an RX ring runs out of free
CPU descriptors. airoha_qdma_hw_init() already unmasks this interrupt
for every ring (INT_RX1_MASK()/INT_RX2_MASK() OR it together with the
RX_DONE bits before writing the enable register), but
airoha_irq_handler() only ever extracts the RX_DONE bits from the same
status word - the NO_CPU_DSCP bits are read and acknowledged (cleared)
along with everything else at the top of the handler, then silently
dropped.

This matters because once a ring is genuinely drained to zero posted
descriptors, no further RX_DONE interrupt can fire for it: hardware
has nothing left to receive a frame into, so NAPI is never rescheduled
and airoha_qdma_fill_rx_queue() (which reposts descriptors) is never
called again. The ring is stuck until the interface is brought down
and back up.

This is most visible on rings that carry low, bursty volumes of
protocol control traffic, in particular RX ring 4, to which
airoha_fe_vip_setup() force-routes ~15 unrelated VIP-classified
protocols (BOOTP, PPPoE Discovery, ISAKMP, DHCPv6, SIP, LLDP, PPP
LCP/IPCP/CHAP/PAP/IPv6CP, ...) via PATN_FCPU_EN_MASK, all sharing the
same RX_DSCP_NUM() default of 16 descriptors. A short burst on that
ring (e.g. a DHCP lease renewal exchange, or the LCP/IPCP/CHAP/PAP
negotiation that follows a PPPoE PADO) can drain it faster than the
CPU reposts descriptors, after which every one of those protocols
silently stops being received on that device until it is reconfigured
- with no error, warning, or netdev/ethtool counter indicating why.

Fix airoha_irq_handler() to treat NO_CPU_DSCP the same as RX_DONE for
the purpose of scheduling NAPI: airoha_qdma_rx_process() already calls
airoha_qdma_fill_rx_queue() unconditionally at the end of every poll,
even when zero descriptors were reaped, so scheduling NAPI in response
to NO_CPU_DSCP is sufficient to make an emptied ring recover on its
own. airoha_qdma_rx_napi_poll() is updated to re-enable the
NO_CPU_DSCP bit alongside RX_DONE when napi_complete() runs, mirroring
the existing disable/enable dance so the interrupt isn't left masked
after its first use.

One open question worth flagging explicitly: if the underlying
no-free-descriptor condition re-latches this bit immediately after the
ack write (rather than only on the next empty->non-empty transition),
a ring that airoha_qdma_fill_rx_queue() genuinely cannot repost into
(e.g. page_pool_dev_alloc_frag() returning NULL under memory pressure)
would turn this into a self-reasserting interrupt storm on the hard
IRQ path: mask -> napi_schedule() -> poll reaps 0, refills 0 ->
napi_complete() -> unmask -> NO_CPU_DSCP fires again immediately. I
don't have documentation confirming which behavior this bit actually
has. Regardless of the answer, masking NO_CPU_DSCP until a refill
actually succeeds - the natural-looking alternative - is worse: a
fully memory-starved ring can never fire RX_DONE either (nothing was
posted for hw to complete), so that would leave it permanently dead
once the memory pressure clears rather than self-healing. A bounded
storm tied to genuine memory pressure, if that's what this is, seems
preferable to a ring with no way back either way.

Fixes: 23290c7bc190 ("net: airoha: Introduce Airoha NPU support")
Link: https://github.com/openwrt/openwrt/issues/24715
Signed-off-by: Vitaliy Sochnev <sochnev.v.74@xxxxxxxxx>
---
drivers/net/ethernet/airoha/airoha_eth.c | 28 +++++++++++++++++++-----
1 file changed, 23 insertions(+), 5 deletions(-)

diff --git a/drivers/net/ethernet/airoha/airoha_eth.c b/drivers/net/ethernet/airoha/airoha_eth.c
index 64619e9a704d..a3e5aaeb75b3 100644
--- a/drivers/net/ethernet/airoha/airoha_eth.c
+++ b/drivers/net/ethernet/airoha/airoha_eth.c
@@ -784,13 +784,15 @@ static int airoha_qdma_rx_napi_poll(struct napi_struct *napi, int budget)
int i, qid = q - &qdma->q_rx[0];
int intr_reg = qid < RX_DONE_HIGH_OFFSET ? QDMA_INT_REG_IDX1
: QDMA_INT_REG_IDX2;
+ u32 bit = qid % RX_DONE_HIGH_OFFSET;

for (i = 0; i < ARRAY_SIZE(qdma->irq_banks); i++) {
if (!(BIT(qid) & RX_IRQ_BANK_PIN_MASK(i)))
continue;

airoha_qdma_irq_enable(&qdma->irq_banks[i], intr_reg,
- BIT(qid % RX_DONE_HIGH_OFFSET));
+ BIT(bit) |
+ BIT(bit + RX_NO_CPU_DSCP_LOW_OFFSET));
}
}

@@ -1468,16 +1470,32 @@ static irqreturn_t airoha_irq_handler(int irq, void *dev_instance)
if (!test_bit(DEV_STATE_INITIALIZED, &qdma->eth->state))
return IRQ_NONE;

- rx_intr1 = intr[1] & RX_DONE_LOW_INT_MASK;
+ /* A ring can also raise NO_CPU_DSCP when it runs out of free RX
+ * descriptors (e.g. a burst of VIP-classified control traffic
+ * forced onto a small ring). Once a ring is fully drained no more
+ * RX_DONE interrupts can fire for it, since there are no free
+ * descriptors left for hardware to receive into, so without this
+ * NAPI is never rescheduled and the ring never gets refilled again.
+ * Treat NO_CPU_DSCP the same as RX_DONE for scheduling NAPI:
+ * airoha_qdma_rx_process() unconditionally calls
+ * airoha_qdma_fill_rx_queue() at the end of every poll, even when
+ * zero descriptors were reaped, so this alone is enough to recover
+ * the ring.
+ */
+ rx_intr1 = intr[1] & (RX_DONE_LOW_INT_MASK | RX_NO_CPU_DSCP_LOW_INT_MASK);
if (rx_intr1) {
airoha_qdma_irq_disable(irq_bank, QDMA_INT_REG_IDX1, rx_intr1);
- rx_intr_mask |= rx_intr1;
+ rx_intr_mask |= (rx_intr1 & RX_DONE_LOW_INT_MASK) |
+ ((rx_intr1 & RX_NO_CPU_DSCP_LOW_INT_MASK) >>
+ RX_NO_CPU_DSCP_LOW_OFFSET);
}

- rx_intr2 = intr[2] & RX_DONE_HIGH_INT_MASK;
+ rx_intr2 = intr[2] & (RX_DONE_HIGH_INT_MASK | RX_NO_CPU_DSCP_HIGH_INT_MASK);
if (rx_intr2) {
airoha_qdma_irq_disable(irq_bank, QDMA_INT_REG_IDX2, rx_intr2);
- rx_intr_mask |= (rx_intr2 << 16);
+ rx_intr_mask |= ((rx_intr2 & RX_DONE_HIGH_INT_MASK) |
+ ((rx_intr2 & RX_NO_CPU_DSCP_HIGH_INT_MASK) >>
+ RX_NO_CPU_DSCP_LOW_OFFSET)) << 16;
}

for (i = 0; rx_intr_mask && i < ARRAY_SIZE(qdma->q_rx); i++) {
--
2.55.0