* [PATCH 1/4] wifi: ath9k: name the register multi-read limit
2026-08-29 5:43 [PATCH 0/4] wifi: ath9k: cut USB round trips on channel changes Nerijus Bendžiūnas
@ 2026-08-29 5:43 ` Nerijus Bendžiūnas
2026-08-29 5:43 ` [PATCH 2/4] wifi: ath9k: check all tx queues with one multi-read Nerijus Bendžiūnas
` (2 subsequent siblings)
3 siblings, 0 replies; 5+ messages in thread
From: Nerijus Bendžiūnas @ 2026-08-29 5:43 UTC (permalink / raw)
To: Toke Høiland-Jørgensen, linux-wireless; +Cc: linux-kernel
ath9k_multi_regread() converts the addresses and the results through
fixed eight-entry arrays without checking the count it was given, so
every REG_READ_MULTI() caller has to stay at or below eight to be safe
over USB. Nothing says so. The only caller that exists today,
ar9271_hw_pa_cal(), happens to ask for exactly eight.
Give the limit a name next to REG_READ_MULTI(), size the arrays with it,
and warn rather than write past them.
Signed-off-by: Nerijus Bendžiūnas <nerijus.bendziunas@gmail.com>
---
drivers/net/wireless/ath/ath9k/htc_drv_init.c | 7 +++++--
drivers/net/wireless/ath/ath9k/hw.h | 7 +++++++
2 files changed, 12 insertions(+), 2 deletions(-)
diff --git a/drivers/net/wireless/ath/ath9k/htc_drv_init.c b/drivers/net/wireless/ath/ath9k/htc_drv_init.c
index 6de78ae85726..7fdec25c76ef 100644
--- a/drivers/net/wireless/ath/ath9k/htc_drv_init.c
+++ b/drivers/net/wireless/ath/ath9k/htc_drv_init.c
@@ -258,10 +258,13 @@ static void ath9k_multi_regread(void *hw_priv, u32 *addr,
struct ath_hw *ah = hw_priv;
struct ath_common *common = ath9k_hw_common(ah);
struct ath9k_htc_priv *priv = common->priv;
- __be32 tmpaddr[8];
- __be32 tmpval[8];
+ __be32 tmpaddr[ATH9K_MULTI_READ_MAX];
+ __be32 tmpval[ATH9K_MULTI_READ_MAX];
int i, ret;
+ if (WARN_ON_ONCE(count > ATH9K_MULTI_READ_MAX))
+ return;
+
for (i = 0; i < count; i++) {
tmpaddr[i] = cpu_to_be32(addr[i]);
}
diff --git a/drivers/net/wireless/ath/ath9k/hw.h b/drivers/net/wireless/ath/ath9k/hw.h
index b942b8303d8f..946c4d307b91 100644
--- a/drivers/net/wireless/ath/ath9k/hw.h
+++ b/drivers/net/wireless/ath/ath9k/hw.h
@@ -83,6 +83,13 @@
#define REG_READ(_ah, _reg) \
(_ah)->reg_ops.read((_ah), (_reg))
+/*
+ * Registers a single REG_READ_MULTI() may ask for. The USB transport carries
+ * the addresses and the results in one WMI command each, so its buffers put a
+ * hard cap on the count; callers must split larger reads themselves.
+ */
+#define ATH9K_MULTI_READ_MAX 8
+
#define REG_READ_MULTI(_ah, _addr, _val, _cnt) \
(_ah)->reg_ops.multi_read((_ah), (_addr), (_val), (_cnt))
--
2.55.0
^ permalink raw reply related [flat|nested] 5+ messages in thread* [PATCH 2/4] wifi: ath9k: check all tx queues with one multi-read
2026-08-29 5:43 [PATCH 0/4] wifi: ath9k: cut USB round trips on channel changes Nerijus Bendžiūnas
2026-08-29 5:43 ` [PATCH 1/4] wifi: ath9k: name the register multi-read limit Nerijus Bendžiūnas
@ 2026-08-29 5:43 ` Nerijus Bendžiūnas
2026-08-29 5:44 ` [PATCH 3/4] wifi: ath9k: batch the read-modify-writes of a channel change Nerijus Bendžiūnas
2026-08-29 5:44 ` [PATCH 4/4] wifi: ath9k: skip the departing channel's noise floor on USB fast changes Nerijus Bendžiūnas
3 siblings, 0 replies; 5+ messages in thread
From: Nerijus Bendžiūnas @ 2026-08-29 5:43 UTC (permalink / raw)
To: Toke Høiland-Jørgensen, linux-wireless; +Cc: linux-kernel
Before a channel change ath9k_hw_channel_change() asks
ath9k_hw_numtxpending() about each of the ten queues in turn, and each
call reads AR_QSTS and then AR_Q_TXE, so confirming a drained radio
takes up to twenty register reads. On the USB devices those are twenty
synchronous WMI round trips, paid on every hop before any tuning starts.
Collect the ten queue status registers and AR_Q_TXE through
REG_READ_MULTI() instead, split into chunks the transport can carry, and
apply the same pending test to the results. PCI implements the
multi-read as a loop of ordinary reads, so it sees no change beyond the
loop moving.
Signed-off-by: Nerijus Bendžiūnas <nerijus.bendziunas@gmail.com>
---
drivers/net/wireless/ath/ath9k/hw.c | 13 +++++-----
drivers/net/wireless/ath/ath9k/mac.c | 36 ++++++++++++++++++++++++++++
drivers/net/wireless/ath/ath9k/mac.h | 1 +
3 files changed, 43 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..0d8369bbbadf 100644
--- a/drivers/net/wireless/ath/ath9k/mac.c
+++ b/drivers/net/wireless/ath/ath9k/mac.c
@@ -77,6 +77,42 @@ u32 ath9k_hw_numtxpending(struct ath_hw *ah, u32 q)
}
EXPORT_SYMBOL(ath9k_hw_numtxpending);
+/*
+ * 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 the transport can carry.
+ *
+ * Returns the first queue that still has frames pending, or -1 if they are
+ * all 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
^ permalink raw reply related [flat|nested] 5+ messages in thread* [PATCH 3/4] wifi: ath9k: batch the read-modify-writes of a channel change
2026-08-29 5:43 [PATCH 0/4] wifi: ath9k: cut USB round trips on channel changes Nerijus Bendžiūnas
2026-08-29 5:43 ` [PATCH 1/4] wifi: ath9k: name the register multi-read limit Nerijus Bendžiūnas
2026-08-29 5:43 ` [PATCH 2/4] wifi: ath9k: check all tx queues with one multi-read Nerijus Bendžiūnas
@ 2026-08-29 5:44 ` Nerijus Bendžiūnas
2026-08-29 5:44 ` [PATCH 4/4] wifi: ath9k: skip the departing channel's noise floor on USB fast changes Nerijus Bendžiūnas
3 siblings, 0 replies; 5+ messages in thread
From: Nerijus Bendžiūnas @ 2026-08-29 5:44 UTC (permalink / raw)
To: Toke Høiland-Jørgensen, linux-wireless; +Cc: linux-kernel
ar5008_hw_set_delta_slope() and ath9k_hw_start_nfcal() both issue a run
of read-modify-writes with nothing between them that reads a register
back. Both run on every channel change, and on the USB devices each
REG_RMW is its own WMI command.
Wrap the two runs in the RMW buffer so the transport sends each as a
single command. PCI does not install the buffer callbacks, so
ENABLE_REG_RMW_BUFFER() and REG_RMW_BUFFER_FLUSH() are empty there and
the writes are issued exactly as before.
Signed-off-by: Nerijus Bendžiūnas <nerijus.bendziunas@gmail.com>
---
drivers/net/wireless/ath/ath9k/ar5008_phy.c | 2 ++
drivers/net/wireless/ath/ath9k/calib.c | 2 ++
2 files changed, 4 insertions(+)
diff --git a/drivers/net/wireless/ath/ath9k/ar5008_phy.c b/drivers/net/wireless/ath/ath9k/ar5008_phy.c
index 7a45f5f62826..af05dcf95a61 100644
--- a/drivers/net/wireless/ath/ath9k/ar5008_phy.c
+++ b/drivers/net/wireless/ath/ath9k/ar5008_phy.c
@@ -868,6 +868,7 @@ static void ar5008_hw_set_delta_slope(struct ath_hw *ah,
ath9k_hw_get_delta_slope_vals(ah, coef_scaled, &ds_coef_man,
&ds_coef_exp);
+ ENABLE_REG_RMW_BUFFER(ah);
REG_RMW_FIELD(ah, AR_PHY_TIMING3,
AR_PHY_TIMING3_DSC_MAN, ds_coef_man);
REG_RMW_FIELD(ah, AR_PHY_TIMING3,
@@ -882,6 +883,7 @@ static void ar5008_hw_set_delta_slope(struct ath_hw *ah,
AR_PHY_HALFGI_DSC_MAN, ds_coef_man);
REG_RMW_FIELD(ah, AR_PHY_HALFGI,
AR_PHY_HALFGI_DSC_EXP, ds_coef_exp);
+ REG_RMW_BUFFER_FLUSH(ah);
}
static bool ar5008_hw_rfbus_req(struct ath_hw *ah)
diff --git a/drivers/net/wireless/ath/ath9k/calib.c b/drivers/net/wireless/ath/ath9k/calib.c
index b4ab85bd7895..73c63ab32d53 100644
--- a/drivers/net/wireless/ath/ath9k/calib.c
+++ b/drivers/net/wireless/ath/ath9k/calib.c
@@ -224,6 +224,7 @@ void ath9k_hw_start_nfcal(struct ath_hw *ah, bool update)
if (ah->caldata)
set_bit(NFCAL_PENDING, &ah->caldata->cal_flags);
+ ENABLE_REG_RMW_BUFFER(ah);
REG_SET_BIT(ah, AR_PHY_AGC_CONTROL(ah),
AR_PHY_AGC_CONTROL_ENABLE_NF);
@@ -235,6 +236,7 @@ void ath9k_hw_start_nfcal(struct ath_hw *ah, bool update)
AR_PHY_AGC_CONTROL_NO_UPDATE_NF);
REG_SET_BIT(ah, AR_PHY_AGC_CONTROL(ah), AR_PHY_AGC_CONTROL_NF);
+ REG_RMW_BUFFER_FLUSH(ah);
}
int ath9k_hw_loadnf(struct ath_hw *ah, struct ath9k_channel *chan)
--
2.55.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH 4/4] wifi: ath9k: skip the departing channel's noise floor on USB fast changes
2026-08-29 5:43 [PATCH 0/4] wifi: ath9k: cut USB round trips on channel changes Nerijus Bendžiūnas
` (2 preceding siblings ...)
2026-08-29 5:44 ` [PATCH 3/4] wifi: ath9k: batch the read-modify-writes of a channel change Nerijus Bendžiūnas
@ 2026-08-29 5:44 ` Nerijus Bendžiūnas
3 siblings, 0 replies; 5+ messages in thread
From: Nerijus Bendžiūnas @ 2026-08-29 5:44 UTC (permalink / raw)
To: Toke Høiland-Jørgensen, linux-wireless; +Cc: linux-kernel
ath9k_hw_reset() reads the noise floor of the channel it is leaving so
that channel's calibration history stays current. The readout is several
register reads, which on the USB devices are as many synchronous WMI
round trips, and a frequency-hopping monitor pays them on every hop for
a value that only matters if the radio returns to that channel later.
Leave the readout out when a fast channel change is asked for on a USB
device. Periodic calibration refreshes the history while a channel is in
use, and the arriving channel's noise floor is loaded either way, so
only a channel that is left and revisited without calibrating in between
sees an older history.
Signed-off-by: Nerijus Bendžiūnas <nerijus.bendziunas@gmail.com>
---
drivers/net/wireless/ath/ath9k/hw.c | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/drivers/net/wireless/ath/ath9k/hw.c b/drivers/net/wireless/ath/ath9k/hw.c
index d204cdf3fa8f..64fefbec46d5 100644
--- a/drivers/net/wireless/ath/ath9k/hw.c
+++ b/drivers/net/wireless/ath/ath9k/hw.c
@@ -1878,7 +1878,13 @@ int ath9k_hw_reset(struct ath_hw *ah, struct ath9k_channel *chan,
if (!ath9k_hw_setpower(ah, ATH9K_PM_AWAKE))
return -EIO;
- if (ah->curchan && !ah->chip_fullsleep)
+ /*
+ * Over USB the departing channel's noise-floor readout costs several
+ * round trips and only feeds its history; the fast path reloads the
+ * arriving channel's noise floor regardless.
+ */
+ if (ah->curchan && !ah->chip_fullsleep &&
+ !(fastcc && common->bus_ops->ath_bus_type == ATH_USB))
ath9k_hw_getnf(ah, ah->curchan);
ah->caldata = caldata;
--
2.55.0
^ permalink raw reply related [flat|nested] 5+ messages in thread