* [PATCH v3 1/5] wifi: ath9k_htc: report a failed multi-read as all ones
2026-09-04 18:52 [PATCH v3 0/5] wifi: ath9k: cut USB round trips on channel changes Nerijus Bendžiūnas
@ 2026-09-04 18:52 ` Nerijus Bendžiūnas
2026-09-04 18:52 ` [PATCH v3 2/5] wifi: ath9k: name the register multi-read limit Nerijus Bendžiūnas
` (3 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Nerijus Bendžiūnas @ 2026-09-04 18:52 UTC (permalink / raw)
To: Toke Høiland-Jørgensen, linux-wireless; +Cc: linux-kernel
When the WMI command fails or times out, ath9k_multi_regread() logs
the failure and still copies its result buffer to the caller, so the
caller receives uninitialised stack data for every register it asked
for. ath9k_hw_update_mibstats(), ath9k_hw_usb_gen_fill_eeprom() and
ath9k_hw_read_array() all read through this path. ath9k_regread()
reports the same failure as -1.
Fill the caller's buffer with all ones instead, so a failed multi-read
looks like a failed single read. ath9k_hw_first_txpending(), added
later in this series, relies on this: a timed-out tx queue status read
must count as frames pending.
Fixes: 09a525d33870 ("ath9k_htc: Add multiple register read API")
Cc: stable@vger.kernel.org
Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Nerijus Bendžiūnas <nerijus.bendziunas@gmail.com>
---
drivers/net/wireless/ath/ath9k/htc_drv_init.c | 3 +++
1 file changed, 3 insertions(+)
diff --git a/drivers/net/wireless/ath/ath9k/htc_drv_init.c b/drivers/net/wireless/ath/ath9k/htc_drv_init.c
index 6de78ae85726..f5844e9bdd2d 100644
--- a/drivers/net/wireless/ath/ath9k/htc_drv_init.c
+++ b/drivers/net/wireless/ath/ath9k/htc_drv_init.c
@@ -273,6 +273,9 @@ static void ath9k_multi_regread(void *hw_priv, u32 *addr,
if (unlikely(ret)) {
ath_dbg(common, WMI,
"Multiple REGISTER READ FAILED (count: %d)\n", count);
+ /* Callers expect what a failed single read returns. */
+ memset(val, 0xff, sizeof(*val) * count);
+ return;
}
for (i = 0; i < count; i++) {
--
2.55.0
^ permalink raw reply [flat|nested] 6+ messages in thread* [PATCH v3 2/5] wifi: ath9k: name the register multi-read limit
2026-09-04 18:52 [PATCH v3 0/5] wifi: ath9k: cut USB round trips on channel changes Nerijus Bendžiūnas
2026-09-04 18:52 ` [PATCH v3 1/5] wifi: ath9k_htc: report a failed multi-read as all ones Nerijus Bendžiūnas
@ 2026-09-04 18:52 ` Nerijus Bendžiūnas
2026-09-04 18:52 ` [PATCH v3 3/5] wifi: ath9k: check all tx queues with one multi-read Nerijus Bendžiūnas
` (2 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Nerijus Bendžiūnas @ 2026-09-04 18:52 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 8-entry arrays and does not check the count it is given, so every
REG_READ_MULTI() caller has to stay at 8 or below, which nothing
states. Two callers ask for exactly 8: ar9271_hw_pa_cal() through
REG_READ_ARRAY() and ath9k_hw_usb_gen_fill_eeprom().
Define ATH9K_MULTI_READ_MAX next to REG_READ_MULTI() and size the
arrays with it. A larger count warns once and is reported as a failed
read, all ones, instead of writing past the arrays.
Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Nerijus Bendžiūnas <nerijus.bendziunas@gmail.com>
---
drivers/net/wireless/ath/ath9k/htc_drv_init.c | 9 +++++++--
drivers/net/wireless/ath/ath9k/hw.h | 7 +++++++
2 files changed, 14 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 f5844e9bdd2d..3d4c6f9f6e95 100644
--- a/drivers/net/wireless/ath/ath9k/htc_drv_init.c
+++ b/drivers/net/wireless/ath/ath9k/htc_drv_init.c
@@ -258,10 +258,15 @@ 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)) {
+ memset(val, 0xff, sizeof(*val) * count);
+ 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..211d42c92796 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))
+/*
+ * Maximum number of registers one REG_READ_MULTI() may ask for. The ath9k_htc
+ * implementation converts the addresses and the results through fixed arrays
+ * of this size; 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 [flat|nested] 6+ messages in thread* [PATCH v3 3/5] wifi: ath9k: check all tx queues with one multi-read
2026-09-04 18:52 [PATCH v3 0/5] wifi: ath9k: cut USB round trips on channel changes Nerijus Bendžiūnas
2026-09-04 18:52 ` [PATCH v3 1/5] wifi: ath9k_htc: report a failed multi-read as all ones Nerijus Bendžiūnas
2026-09-04 18:52 ` [PATCH v3 2/5] wifi: ath9k: name the register multi-read limit Nerijus Bendžiūnas
@ 2026-09-04 18:52 ` Nerijus Bendžiūnas
2026-09-04 18:52 ` [PATCH v3 4/5] wifi: ath9k: batch the read-modify-writes of a channel change Nerijus Bendžiūnas
2026-09-04 18:52 ` [PATCH v3 5/5] wifi: ath9k: skip the old channel's noise floor on USB fast changes Nerijus Bendžiūnas
4 siblings, 0 replies; 6+ messages in thread
From: Nerijus Bendžiūnas @ 2026-09-04 18:52 UTC (permalink / raw)
To: Toke Høiland-Jørgensen, linux-wireless; +Cc: linux-kernel
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@gmail.com>
---
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
^ permalink raw reply [flat|nested] 6+ messages in thread* [PATCH v3 4/5] wifi: ath9k: batch the read-modify-writes of a channel change
2026-09-04 18:52 [PATCH v3 0/5] wifi: ath9k: cut USB round trips on channel changes Nerijus Bendžiūnas
` (2 preceding siblings ...)
2026-09-04 18:52 ` [PATCH v3 3/5] wifi: ath9k: check all tx queues with one multi-read Nerijus Bendžiūnas
@ 2026-09-04 18:52 ` Nerijus Bendžiūnas
2026-09-04 18:52 ` [PATCH v3 5/5] wifi: ath9k: skip the old channel's noise floor on USB fast changes Nerijus Bendžiūnas
4 siblings, 0 replies; 6+ messages in thread
From: Nerijus Bendžiūnas @ 2026-09-04 18:52 UTC (permalink / raw)
To: Toke Høiland-Jørgensen, linux-wireless; +Cc: linux-kernel
ar5008_hw_set_delta_slope() and ath9k_hw_start_nfcal() each issue a run
of read-modify-writes with no register read in between. Both run on
every channel change, and on the USB devices each REG_RMW is a separate
WMI command.
Wrap both runs in ENABLE_REG_RMW_BUFFER() and REG_RMW_BUFFER_FLUSH() so
the USB transport sends each run as one command. PCI does not install
the buffer callbacks, so the macros do nothing there and the writes are
issued as before. The same holds for USB devices with firmware older
than 1.4, which have no WMI_REG_RMW_CMDID. ar5008_hw_set_delta_slope()
is the AR5008 and AR9002 callback; AR9003 has its own and is not
touched.
Assisted-by: Claude:claude-fable-5-1
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 [flat|nested] 6+ messages in thread* [PATCH v3 5/5] wifi: ath9k: skip the old channel's noise floor on USB fast changes
2026-09-04 18:52 [PATCH v3 0/5] wifi: ath9k: cut USB round trips on channel changes Nerijus Bendžiūnas
` (3 preceding siblings ...)
2026-09-04 18:52 ` [PATCH v3 4/5] wifi: ath9k: batch the read-modify-writes of a channel change Nerijus Bendžiūnas
@ 2026-09-04 18:52 ` Nerijus Bendžiūnas
4 siblings, 0 replies; 6+ messages in thread
From: Nerijus Bendžiūnas @ 2026-09-04 18:52 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 to
keep that channel's calibration history current. On a single-chain
AR9271 that is three register reads, and on the USB devices each one is
a synchronous WMI round trip paid on every channel change.
Skip the readout on a fast channel change over USB. The arriving
channel's noise floor is loaded as before, and periodic calibration
updates the history while a channel is in use. Only a channel that is
left and revisited without a calibration in between sees an older
history entry.
Assisted-by: Claude:claude-fable-5-1
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 [flat|nested] 6+ messages in thread