net: airoha: fix BQL underflow in shared QDMA TX ring

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: a9c2ca61fe ("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>
This commit is contained in:
Lorenzo Bianconi 2026-06-20 17:04:51 +02:00 committed by Jakub Kicinski
parent 510a283f4d
commit 6117098309
2 changed files with 95 additions and 78 deletions

View 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;
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;
}
q->queued--;
}
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);
}
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;
}
/* 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));
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 @@ static netdev_tx_t airoha_dev_xmit(struct sk_buff *skb,
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];

View 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];