* [PATCH 0/4] wifi: ath9k: cut USB round trips on channel changes
@ 2026-08-29 5:43 Nerijus Bendžiūnas
2026-08-29 5:43 ` [PATCH 1/4] wifi: ath9k: name the register multi-read limit Nerijus Bendžiūnas
` (3 more replies)
0 siblings, 4 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
Every register access on the ath9k_htc devices is a synchronous WMI round
trip, so a channel change that is nearly free on PCI costs tens of
milliseconds over USB. These four patches take round trips out of that
path without changing what the hardware is asked to do.
Patch 1 writes down the eight-register limit on REG_READ_MULTI() that
ath9k_multi_regread() has always had and never stated, so patch 2 can
rely on it. Patch 2 replaces the ten-queue ath9k_hw_numtxpending() poll
in ath9k_hw_channel_change() with two multi-reads. Patch 3 wraps the
read-modify-write runs in ar5008_hw_set_delta_slope() and
ath9k_hw_start_nfcal() in the RMW buffer. Patch 4 drops the departing
channel's noise-floor readout on a fast channel change over USB.
Patches 2 and 3 sit in code that PCI runs too, but there the multi-read
is a loop of ordinary reads and the RMW buffer callbacks are not
installed, so the register traffic is unchanged. Patch 4 is gated on
ATH_USB.
Measured on an AR9271 (0cf3:9271), counting WMI commands with a debug
counter added locally around the driver's channel change:
full reset, unpatched 182 commands ~92 ms
channel change with 1-4 44 commands ~31 ms
The queue poll alone was around twenty reads per change, and the two
read-modify-write runs another ten commands.
The second row also needs two changes that are not in this series:
taking the fast channel-change path on same-band retunes, which mainline
does only for off-channel scan hops, and a mac80211 fix so a monitor
retune performs one driver channel change instead of two. Both are
separate, and I am not asking for them here. What this series
contributes on its own is the round-trip reduction that those then
benefit from; mainline as it stands sees it on scan hops.
One note on how this was checked, because it changed the series. Two
further patches that also cut round trips, skipping the PCU
re-initialisation and the WMI_SET_MODE on a fast change, measured about
twice as fast again on a channel-switch latency benchmark. They also left
the receiver dead: ath9k_host_rx_init() is what clears AR_DIAG_RX_DIS and
AR_DIAG_RX_ABORT, and without it the radio retunes correctly and hears
nothing. The latency benchmark could not see that, since it only timed
the tuning. What caught it was two AR9271s coupled over coax, one
injecting raw frames and the other capturing. Those two patches are
therefore not here. With patches 1-4 in place, the fast path delivered
90-97% of injected frames on the standard 2.4 GHz channels across
repeated runs, against 95-97% for a full reset on the same build. The
spread is the rig, not the patches: the same binary can read differently
minutes apart, and the fast path shows that drift first because it does
not recalibrate, which is the reason mainline limits it to scan hops.
Those numbers are from cards on root ports. With the receiving card two
USB hub tiers deep, fast-path reception was unreliable with and without
these patches while full resets were unaffected, so that is a property
of the fast path on this hardware rather than of the series.
Nerijus Bendžiūnas (4):
wifi: ath9k: name the register multi-read limit
wifi: ath9k: check all tx queues with one multi-read
wifi: ath9k: batch the read-modify-writes of a channel change
wifi: ath9k: skip the departing channel's noise floor on USB fast
changes
drivers/net/wireless/ath/ath9k/ar5008_phy.c | 2 ++
drivers/net/wireless/ath/ath9k/calib.c | 2 ++
drivers/net/wireless/ath/ath9k/htc_drv_init.c | 7 ++--
drivers/net/wireless/ath/ath9k/hw.c | 21 ++++++-----
drivers/net/wireless/ath/ath9k/hw.h | 7 ++++
drivers/net/wireless/ath/ath9k/mac.c | 36 +++++++++++++++++++
drivers/net/wireless/ath/ath9k/mac.h | 1 +
7 files changed, 66 insertions(+), 10 deletions(-)
base-commit: ca800a9302764c445de0da0e84d2252400a770ee
--
2.55.0
^ permalink raw reply [flat|nested] 5+ messages in thread
* [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
end of thread, other threads:[~2026-08-29 5:44 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [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
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox