[PATCH v3 3/5] wifi: ath9k: check all tx queues with one multi-read

From: Nerijus Bendžiūnas

Date: Fri Sep 04 2026 - 15:06:04 EST


Before a channel change, ath9k_hw_channel_change() calls
ath9k_hw_numtxpending() for each of the 10 queues. Each call reads
AR_QSTS and then AR_Q_TXE, so confirming that the radio has drained
takes up to 20 register reads. On the USB devices each read is a
synchronous WMI round trip, paid on every channel change before tuning
starts.

Add ath9k_hw_first_txpending(), which collects the 10 queue status
registers and AR_Q_TXE with REG_READ_MULTI() and applies the same
pending test. Use it in ath9k_hw_channel_change(). On PCI the
multi-read is a loop of single reads, so the register traffic there is
unchanged.

A multi-read that fails over USB returns all ones, which counts as
pending, so a lost read still fails the fast channel change and the
caller falls back to a full reset.

Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Nerijus Bendžiūnas <nerijus.bendziunas@xxxxxxxxx>
---
drivers/net/wireless/ath/ath9k/hw.c | 13 +++++----
drivers/net/wireless/ath/ath9k/mac.c | 40 ++++++++++++++++++++++++++++
drivers/net/wireless/ath/ath9k/mac.h | 1 +
3 files changed, 47 insertions(+), 7 deletions(-)

diff --git a/drivers/net/wireless/ath/ath9k/hw.c b/drivers/net/wireless/ath/ath9k/hw.c
index e08ab73fcacb..d204cdf3fa8f 100644
--- a/drivers/net/wireless/ath/ath9k/hw.c
+++ b/drivers/net/wireless/ath/ath9k/hw.c
@@ -1547,7 +1547,7 @@ static bool ath9k_hw_channel_change(struct ath_hw *ah,
struct ath9k_hw_capabilities *pCap = &ah->caps;
bool band_switch = false, mode_diff = false;
u8 ini_reloaded = 0;
- u32 qnum;
+ int qnum;
int r;

if (pCap->hw_caps & ATH9K_HW_CAP_FCC_BAND_SWITCH) {
@@ -1556,12 +1556,11 @@ static bool ath9k_hw_channel_change(struct ath_hw *ah,
mode_diff = !!(flags_diff & ~CHANNEL_HT);
}

- for (qnum = 0; qnum < AR_NUM_QCU; qnum++) {
- if (ath9k_hw_numtxpending(ah, qnum)) {
- ath_dbg(common, QUEUE,
- "Transmit frames pending on queue %d\n", qnum);
- return false;
- }
+ qnum = ath9k_hw_first_txpending(ah);
+ if (qnum >= 0) {
+ ath_dbg(common, QUEUE,
+ "Transmit frames pending on queue %d\n", qnum);
+ return false;
}

if (!ath9k_hw_rfbus_req(ah)) {
diff --git a/drivers/net/wireless/ath/ath9k/mac.c b/drivers/net/wireless/ath/ath9k/mac.c
index b070403e083f..27926b67d628 100644
--- a/drivers/net/wireless/ath/ath9k/mac.c
+++ b/drivers/net/wireless/ath/ath9k/mac.c
@@ -77,6 +77,46 @@ u32 ath9k_hw_numtxpending(struct ath_hw *ah, u32 q)
}
EXPORT_SYMBOL(ath9k_hw_numtxpending);

+/**
+ * ath9k_hw_first_txpending - find a tx queue that still has frames pending
+ * @ah: hardware
+ *
+ * Asking ath9k_hw_numtxpending() about each queue in turn costs up to two
+ * register reads per queue, and on the USB devices every one of those is a
+ * synchronous WMI round trip. Collect the queue status registers and AR_Q_TXE
+ * with the multi-read op instead, in chunks of ATH9K_MULTI_READ_MAX. A
+ * multi-read that fails over USB reads as all ones, which counts as pending,
+ * so a WMI timeout still refuses the fast channel change.
+ *
+ * Return: the first queue with frames pending, or -1 if all are drained.
+ */
+int ath9k_hw_first_txpending(struct ath_hw *ah)
+{
+ u32 addr[AR_NUM_QCU + 1];
+ u32 val[AR_NUM_QCU + 1];
+ u32 q, txe, done = 0;
+
+ for (q = 0; q < AR_NUM_QCU; q++)
+ addr[q] = AR_QSTS(q);
+ addr[AR_NUM_QCU] = AR_Q_TXE;
+
+ while (done < ARRAY_SIZE(addr)) {
+ u32 count = min_t(u32, ARRAY_SIZE(addr) - done,
+ ATH9K_MULTI_READ_MAX);
+
+ REG_READ_MULTI(ah, addr + done, val + done, count);
+ done += count;
+ }
+
+ txe = val[AR_NUM_QCU];
+ for (q = 0; q < AR_NUM_QCU; q++) {
+ if ((val[q] & AR_Q_STS_PEND_FR_CNT) || (txe & BIT(q)))
+ return q;
+ }
+
+ return -1;
+}
+
/**
* ath9k_hw_updatetxtriglevel - adjusts the frame trigger level
*
diff --git a/drivers/net/wireless/ath/ath9k/mac.h b/drivers/net/wireless/ath/ath9k/mac.h
index 16203e7ecf29..5b94ce087be2 100644
--- a/drivers/net/wireless/ath/ath9k/mac.h
+++ b/drivers/net/wireless/ath/ath9k/mac.h
@@ -721,6 +721,7 @@ u32 ath9k_hw_gettxbuf(struct ath_hw *ah, u32 q);
void ath9k_hw_puttxbuf(struct ath_hw *ah, u32 q, u32 txdp);
void ath9k_hw_txstart(struct ath_hw *ah, u32 q);
u32 ath9k_hw_numtxpending(struct ath_hw *ah, u32 q);
+int ath9k_hw_first_txpending(struct ath_hw *ah);
bool ath9k_hw_updatetxtriglevel(struct ath_hw *ah, bool bIncTrigLevel);
bool ath9k_hw_stop_dma_queue(struct ath_hw *ah, u32 q);
void ath9k_hw_abort_tx_dma(struct ath_hw *ah);
--
2.55.0