[PATCH v2 2/2] wifi: ath9k: retry failed non-aggregate frames in software

From: Tomas Tonhauser

Date: Wed Oct 28 2026 - 13:20:03 EST


Aggregate frames that fail or are filtered by the hardware are put
back on tid->retry_q and retransmitted until ATH_MAX_SW_RETRIES is
reached. Non-aggregate frames have no such software retry: a frame
that exhausts the hardware retry chain is completed as failed, and a
filtered one is handed to mac80211, which retries it only once for
stations that are not in powersave.

On weak links this shows up as downlink loss to clients without an
A-MPDU session. With an ESP8266 client at about -75 dBm, 1000-byte
ping floods lost 4.2% of frames, all after ATH9K_TXERR_XRETRY.

Re-queue failed and filtered non-aggregate data frames to awake
stations on tid->retry_q, using the same retry accounting and limit
as the aggregate path. Frames to sleeping stations, management
frames, no-ack frames and frames sent on other queues keep the
current behaviour.

Suggested-by: Toke Høiland-Jørgensen <toke@xxxxxxx>
Assisted-by: LLM
Signed-off-by: Tomas Tonhauser <t.tonhauser@xxxxxxxx>
---

Notes:
v2: new patch

Open points:
- As in the aggregate path, rate control only gets status for the
final attempt of a re-queued frame.
- ath_tx_set_retry() counts the retry as "AMPDUs Retried" in
debugfs.

drivers/net/wireless/ath/ath9k/xmit.c | 56 +++++++++++++++++++++++++--
1 file changed, 53 insertions(+), 3 deletions(-)

diff --git a/drivers/net/wireless/ath/ath9k/xmit.c b/drivers/net/wireless/ath/ath9k/xmit.c
index 913aa280ae2a..d141292612b9 100644
--- a/drivers/net/wireless/ath/ath9k/xmit.c
+++ b/drivers/net/wireless/ath/ath9k/xmit.c
@@ -722,6 +722,55 @@ static void ath_tx_count_airtime(struct ath_softc *sc,
ieee80211_sta_register_airtime(sta, tid, airtime, 0);
}

+static bool ath_tx_retry_buf(struct ath_softc *sc, struct ath_txq *txq,
+ struct ath_buf *bf, struct ieee80211_sta *sta,
+ struct ath_atx_tid *tid, struct ath_tx_status *ts)
+{
+ struct sk_buff *skb = bf->bf_mpdu;
+ struct ieee80211_tx_info *tx_info = IEEE80211_SKB_CB(skb);
+ struct ieee80211_hdr *hdr = (struct ieee80211_hdr *)skb->data;
+ struct ath_frame_info *fi = get_frame_info(skb);
+ struct ath_node *an;
+ int i, retries;
+
+ if (!sta || tid->txq != txq)
+ return false;
+
+ if (!(ts->ts_status & (ATH9K_TXERR_FILT | ATH9K_TXERR_XRETRY)))
+ return false;
+
+ if (!ieee80211_is_data_present(hdr->frame_control) ||
+ (tx_info->flags & IEEE80211_TX_CTL_NO_ACK) ||
+ bf->bf_state.bfs_paprd)
+ return false;
+
+ an = (struct ath_node *)sta->drv_priv;
+ if (an->sleeping || fi->retries >= ATH_MAX_SW_RETRIES)
+ return false;
+
+ retries = ts->ts_longretry + 1;
+ for (i = 0; i < ts->ts_rateindex; i++)
+ retries += bf->rates[i].count;
+
+ if (bf->bf_state.stale) {
+ struct ath_buf *tbf = ath_clone_txbuf(sc, bf);
+
+ if (!tbf)
+ return false;
+ fi->bf = tbf;
+ } else {
+ list_del_init(&bf->list);
+ }
+
+ ath_tx_set_retry(sc, txq, skb, retries);
+ if (ts->ts_status & ATH9K_TXERR_FILT)
+ TX_STAT_INC(sc, txq->axq_qnum, txerr_filtered);
+
+ __skb_queue_tail(&tid->retry_q, skb);
+ ath_tx_queue_tid(sc, tid);
+ return true;
+}
+
static void ath_tx_process_buffer(struct ath_softc *sc, struct ath_txq *txq,
struct ath_tx_status *ts, struct ath_buf *bf,
struct list_head *bf_head)
@@ -754,7 +803,9 @@ static void ath_tx_process_buffer(struct ath_softc *sc, struct ath_txq *txq,
tid->clear_ps_filter = true;
}

- if (!bf_isampdu(bf)) {
+ if (bf_isampdu(bf)) {
+ ath_tx_complete_aggr(sc, txq, bf, bf_head, sta, tid, ts, txok);
+ } else if (flush || !ath_tx_retry_buf(sc, txq, bf, sta, tid, ts)) {
if (!flush) {
info = IEEE80211_SKB_CB(bf->bf_mpdu);
memcpy(info->control.rates, bf->rates,
@@ -764,8 +815,7 @@ static void ath_tx_process_buffer(struct ath_softc *sc, struct ath_txq *txq,
sta);
}
ath_tx_complete_buf(sc, bf, txq, bf_head, sta, ts, txok);
- } else
- ath_tx_complete_aggr(sc, txq, bf, bf_head, sta, tid, ts, txok);
+ }

if (!flush)
ath_txq_schedule(sc, txq);
--
2.34.1