]> git.ipfire.org Git - thirdparty/linux.git/commitdiff
net: airoha: fix BQL underflow in shared QDMA TX ring
authorLorenzo Bianconi <lorenzo@kernel.org>
Sat, 20 Jun 2026 15:04:51 +0000 (17:04 +0200)
committerJakub Kicinski <kuba@kernel.org>
Thu, 25 Jun 2026 01:43:37 +0000 (18:43 -0700)
When multiple netdevs share a QDMA TX ring and one device is stopped,
netdev_tx_reset_subqueue() zeroes that device's BQL counters while its
pending skbs remain in the shared HW TX ring. When NAPI later completes
those skbs via netdev_tx_completed_queue(), the already-zeroed
dql->num_queued counter underflows.

Fix the issue:
- Remove netdev_tx_reset_subqueue() from airoha_dev_stop() so pending
  skbs are completed naturally by NAPI with proper BQL accounting.
- Rework airoha_qdma_tx_cleanup() to disable TX DMA, flush BQL
  counters, DMA-unmap and free all pending skbs while skb->dev
  references are still valid. Use a per-queue flushing flag checked
  under q->lock in airoha_dev_xmit() to prevent races between teardown
  and transmit. Call airoha_qdma_stop_napi() before
  airoha_qdma_tx_cleanup() at the call sites.
- Move DMA engine start into probe. Split DMA teardown so TX DMA is
  disabled in airoha_qdma_tx_cleanup() and RX DMA in
  airoha_qdma_cleanup().
- Remove qdma->users counter since DMA lifetime is now tied to
  probe/cleanup rather than per-netdev open/stop.

Fixes: a9c2ca61fec7 ("net: airoha: Support multiple net_devices for a single FE GDM port")
Signed-off-by: Lorenzo Bianconi <lorenzo@kernel.org>
Reviewed-by: Simon Horman <horms@kernel.org>
Link: https://patch.msgid.link/20260620-airoha-bql-fixes-v3-1-76b95374e63e@kernel.org
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
drivers/net/ethernet/airoha/airoha_eth.c
drivers/net/ethernet/airoha/airoha_eth.h

index 986fa4685bf92a3be01200434c6da3ad3ca33c2e..932b3a3df2e577b15e901b8c29507afe48bdfe4f 100644 (file)
@@ -1004,6 +1004,7 @@ static int airoha_qdma_tx_napi_poll(struct napi_struct *napi, int budget)
 
                e = &q->entry[index];
                skb = e->skb;
+               e->skb = NULL;
 
                dma_unmap_single(eth->dev, e->dma_addr, e->dma_len,
                                 DMA_TO_DEVICE);
@@ -1147,55 +1148,76 @@ static int airoha_qdma_init_tx(struct airoha_qdma *qdma)
        return 0;
 }
 
-static void airoha_qdma_cleanup_tx_queue(struct airoha_queue *q)
+static void airoha_qdma_tx_cleanup(struct airoha_qdma *qdma)
 {
-       struct airoha_qdma *qdma = q->qdma;
-       struct airoha_eth *eth = qdma->eth;
-       int i, qid = q - &qdma->q_tx[0];
-       u16 index = 0;
+       u32 status;
+       int i;
 
-       spin_lock_bh(&q->lock);
-       for (i = 0; i < q->ndesc; i++) {
-               struct airoha_queue_entry *e = &q->entry[i];
-               struct airoha_qdma_desc *desc = &q->desc[i];
+       airoha_qdma_clear(qdma, REG_QDMA_GLOBAL_CFG,
+                         GLOBAL_CFG_TX_DMA_EN_MASK);
+       if (read_poll_timeout(airoha_qdma_rr, status,
+                             !(status & GLOBAL_CFG_TX_DMA_BUSY_MASK),
+                             USEC_PER_MSEC, 50 * USEC_PER_MSEC, true,
+                             qdma, REG_QDMA_GLOBAL_CFG))
+               dev_warn(qdma->eth->dev, "QDMA TX DMA busy timeout\n");
 
-               if (!e->dma_addr)
+       for (i = 0; i < ARRAY_SIZE(qdma->q_tx); i++) {
+               struct airoha_queue *q = &qdma->q_tx[i];
+               u16 index = 0;
+               int j;
+
+               if (!q->ndesc)
                        continue;
 
-               dma_unmap_single(eth->dev, e->dma_addr, e->dma_len,
-                                DMA_TO_DEVICE);
-               dev_kfree_skb_any(e->skb);
-               e->dma_addr = 0;
-               e->skb = NULL;
-               list_add_tail(&e->list, &q->tx_list);
+               spin_lock_bh(&q->lock);
 
-               /* Reset DMA descriptor */
-               WRITE_ONCE(desc->ctrl, 0);
-               WRITE_ONCE(desc->addr, 0);
-               WRITE_ONCE(desc->data, 0);
-               WRITE_ONCE(desc->msg0, 0);
-               WRITE_ONCE(desc->msg1, 0);
-               WRITE_ONCE(desc->msg2, 0);
+               q->flushing = true;
+               for (j = 0; j < q->ndesc; j++) {
+                       struct airoha_queue_entry *e = &q->entry[j];
+                       struct airoha_qdma_desc *desc = &q->desc[j];
+                       struct sk_buff *skb = e->skb;
 
-               q->queued--;
-       }
+                       if (!e->dma_addr)
+                               continue;
 
-       if (!list_empty(&q->tx_list)) {
-               struct airoha_queue_entry *e;
+                       dma_unmap_single(qdma->eth->dev, e->dma_addr,
+                                        e->dma_len, DMA_TO_DEVICE);
+                       e->dma_addr = 0;
+                       list_add_tail(&e->list, &q->tx_list);
+
+                       WRITE_ONCE(desc->ctrl, 0);
+                       WRITE_ONCE(desc->addr, 0);
+                       WRITE_ONCE(desc->data, 0);
+                       WRITE_ONCE(desc->msg0, 0);
+                       WRITE_ONCE(desc->msg1, 0);
+                       WRITE_ONCE(desc->msg2, 0);
+
+                       if (skb) {
+                               struct netdev_queue *txq;
+
+                               txq = skb_get_tx_queue(skb->dev, skb);
+                               netdev_tx_completed_queue(txq, 1, skb->len);
+                               dev_kfree_skb_any(skb);
+                               e->skb = NULL;
+                       }
 
-               e = list_first_entry(&q->tx_list, struct airoha_queue_entry,
-                                    list);
-               index = e - q->entry;
-       }
-       /* Set TX_DMA_IDX to TX_CPU_IDX to notify the hw the QDMA TX ring is
-        * empty.
-        */
-       airoha_qdma_rmw(qdma, REG_TX_CPU_IDX(qid), TX_RING_CPU_IDX_MASK,
-                       FIELD_PREP(TX_RING_CPU_IDX_MASK, index));
-       airoha_qdma_rmw(qdma, REG_TX_DMA_IDX(qid), TX_RING_DMA_IDX_MASK,
-                       FIELD_PREP(TX_RING_DMA_IDX_MASK, index));
+                       q->queued--;
+               }
 
-       spin_unlock_bh(&q->lock);
+               if (!list_empty(&q->tx_list)) {
+                       struct airoha_queue_entry *e;
+
+                       e = list_first_entry(&q->tx_list,
+                                            struct airoha_queue_entry, list);
+                       index = e - q->entry;
+               }
+               airoha_qdma_rmw(qdma, REG_TX_CPU_IDX(i), TX_RING_CPU_IDX_MASK,
+                               FIELD_PREP(TX_RING_CPU_IDX_MASK, index));
+               airoha_qdma_rmw(qdma, REG_TX_DMA_IDX(i), TX_RING_DMA_IDX_MASK,
+                               FIELD_PREP(TX_RING_DMA_IDX_MASK, index));
+
+               spin_unlock_bh(&q->lock);
+       }
 }
 
 static int airoha_qdma_init_hfwd_queues(struct airoha_qdma *qdma)
@@ -1523,10 +1545,23 @@ static int airoha_qdma_init(struct platform_device *pdev,
        return airoha_qdma_hw_init(qdma);
 }
 
-static void airoha_qdma_cleanup(struct airoha_qdma *qdma)
+static void airoha_qdma_cleanup(struct airoha_eth *eth,
+                               struct airoha_qdma *qdma)
 {
        int i;
 
+       if (test_bit(DEV_STATE_INITIALIZED, &eth->state)) {
+               u32 status;
+
+               airoha_qdma_clear(qdma, REG_QDMA_GLOBAL_CFG,
+                                 GLOBAL_CFG_RX_DMA_EN_MASK);
+               if (read_poll_timeout(airoha_qdma_rr, status,
+                                     !(status & GLOBAL_CFG_RX_DMA_BUSY_MASK),
+                                     USEC_PER_MSEC, 50 * USEC_PER_MSEC, true,
+                                     qdma, REG_QDMA_GLOBAL_CFG))
+                       dev_warn(eth->dev, "QDMA RX DMA busy timeout\n");
+       }
+
        for (i = 0; i < ARRAY_SIZE(qdma->q_rx); i++) {
                if (!qdma->q_rx[i].ndesc)
                        continue;
@@ -1546,12 +1581,6 @@ static void airoha_qdma_cleanup(struct airoha_qdma *qdma)
                netif_napi_del(&qdma->q_tx_irq[i].napi);
        }
 
-       for (i = 0; i < ARRAY_SIZE(qdma->q_tx); i++) {
-               if (!qdma->q_tx[i].ndesc)
-                       continue;
-
-               airoha_qdma_cleanup_tx_queue(&qdma->q_tx[i]);
-       }
 }
 
 static int airoha_hw_init(struct platform_device *pdev,
@@ -1593,7 +1622,7 @@ static int airoha_hw_init(struct platform_device *pdev,
        return 0;
 error:
        for (i = 0; i < ARRAY_SIZE(eth->qdma); i++)
-               airoha_qdma_cleanup(&eth->qdma[i]);
+               airoha_qdma_cleanup(eth, &eth->qdma[i]);
 
        return err;
 }
@@ -1603,7 +1632,7 @@ static void airoha_hw_cleanup(struct airoha_eth *eth)
        int i;
 
        for (i = 0; i < ARRAY_SIZE(eth->qdma); i++)
-               airoha_qdma_cleanup(&eth->qdma[i]);
+               airoha_qdma_cleanup(eth, &eth->qdma[i]);
        airoha_ppe_deinit(eth);
 }
 
@@ -1837,11 +1866,6 @@ static int airoha_dev_open(struct net_device *netdev)
        }
        port->users++;
 
-       airoha_qdma_set(qdma, REG_QDMA_GLOBAL_CFG,
-                       GLOBAL_CFG_TX_DMA_EN_MASK |
-                       GLOBAL_CFG_RX_DMA_EN_MASK);
-       qdma->users++;
-
        if (!airoha_is_lan_gdm_dev(dev) &&
            airoha_ppe_is_enabled(qdma->eth, 1))
                pse_port = FE_PSE_PORT_PPE2;
@@ -1880,12 +1904,9 @@ static int airoha_dev_stop(struct net_device *netdev)
        struct airoha_gdm_dev *dev = netdev_priv(netdev);
        struct airoha_gdm_port *port = dev->port;
        struct airoha_qdma *qdma = dev->qdma;
-       int i;
 
        netif_tx_disable(netdev);
        airoha_set_vip_for_gdm_port(dev, false);
-       for (i = 0; i < netdev->num_tx_queues; i++)
-               netdev_tx_reset_subqueue(netdev, i);
 
        if (--port->users)
                airoha_set_port_mtu(dev->eth, port);
@@ -1893,20 +1914,6 @@ static int airoha_dev_stop(struct net_device *netdev)
                airoha_set_gdm_port_fwd_cfg(qdma->eth,
                                            REG_GDM_FWD_CFG(port->id),
                                            FE_PSE_PORT_DROP);
-
-       if (!--qdma->users) {
-               airoha_qdma_clear(qdma, REG_QDMA_GLOBAL_CFG,
-                                 GLOBAL_CFG_TX_DMA_EN_MASK |
-                                 GLOBAL_CFG_RX_DMA_EN_MASK);
-
-               for (i = 0; i < ARRAY_SIZE(qdma->q_tx); i++) {
-                       if (!qdma->q_tx[i].ndesc)
-                               continue;
-
-                       airoha_qdma_cleanup_tx_queue(&qdma->q_tx[i]);
-               }
-       }
-
        return 0;
 }
 
@@ -2229,6 +2236,9 @@ static netdev_tx_t airoha_dev_xmit(struct sk_buff *skb,
 
        spin_lock_bh(&q->lock);
 
+       if (q->flushing)
+               goto error_unlock;
+
        txq = skb_get_tx_queue(netdev, skb);
        nr_frags = 1 + skb_shinfo(skb)->nr_frags;
 
@@ -2309,7 +2319,7 @@ error_unmap:
                e->dma_addr = 0;
        }
        list_splice(&tx_list, &q->tx_list);
-
+error_unlock:
        spin_unlock_bh(&q->lock);
 error:
        dev_kfree_skb_any(skb);
@@ -3420,8 +3430,12 @@ static int airoha_probe(struct platform_device *pdev)
        if (err)
                goto error_netdev_free;
 
-       for (i = 0; i < ARRAY_SIZE(eth->qdma); i++)
+       for (i = 0; i < ARRAY_SIZE(eth->qdma); i++) {
                airoha_qdma_start_napi(&eth->qdma[i]);
+               airoha_qdma_set(&eth->qdma[i], REG_QDMA_GLOBAL_CFG,
+                               GLOBAL_CFG_TX_DMA_EN_MASK |
+                               GLOBAL_CFG_RX_DMA_EN_MASK);
+       }
 
        for_each_child_of_node(pdev->dev.of_node, np) {
                if (!of_device_is_compatible(np, "airoha,eth-mac"))
@@ -3444,8 +3458,10 @@ static int airoha_probe(struct platform_device *pdev)
        return 0;
 
 error_napi_stop:
-       for (i = 0; i < ARRAY_SIZE(eth->qdma); i++)
+       for (i = 0; i < ARRAY_SIZE(eth->qdma); i++) {
                airoha_qdma_stop_napi(&eth->qdma[i]);
+               airoha_qdma_tx_cleanup(&eth->qdma[i]);
+       }
 
        for (i = 0; i < ARRAY_SIZE(eth->ports); i++) {
                struct airoha_gdm_port *port = eth->ports[i];
@@ -3481,8 +3497,10 @@ static void airoha_remove(struct platform_device *pdev)
        struct airoha_eth *eth = platform_get_drvdata(pdev);
        int i;
 
-       for (i = 0; i < ARRAY_SIZE(eth->qdma); i++)
+       for (i = 0; i < ARRAY_SIZE(eth->qdma); i++) {
                airoha_qdma_stop_napi(&eth->qdma[i]);
+               airoha_qdma_tx_cleanup(&eth->qdma[i]);
+       }
 
        for (i = 0; i < ARRAY_SIZE(eth->ports); i++) {
                struct airoha_gdm_port *port = eth->ports[i];
index 41d2e7a1f9fb66e275e0461b3f0342242968015f..d7ff8c5200e20cca1c54032e19d7b47b07054cb0 100644 (file)
@@ -197,6 +197,7 @@ struct airoha_queue {
        int free_thr;
        int buf_size;
        bool txq_stopped;
+       bool flushing;
 
        struct napi_struct napi;
        struct page_pool *page_pool;
@@ -524,8 +525,6 @@ struct airoha_qdma {
        struct airoha_eth *eth;
        void __iomem *regs;
 
-       int users;
-
        struct airoha_irq_bank irq_banks[AIROHA_MAX_NUM_IRQ_BANKS];
 
        struct airoha_tx_irq_queue q_tx_irq[AIROHA_NUM_TX_IRQ];