* [PATCH rtw-next v10 1/3] wifi: rtw88: download the beacon the reserved page was built with
2026-11-11 12:15 [PATCH rtw-next v10 0/3] wifi: rtw88: channel switch in AP mode Mehmet Fide
@ 2026-11-11 12:15 ` Mehmet Fide
2026-11-11 12:15 ` [PATCH rtw-next v10 2/3] wifi: rtw88: support channel switch in AP mode Mehmet Fide
2026-11-11 12:15 ` [PATCH rtw-next v10 3/3] wifi: rtw88: time the countdown downloads from the TBTT Mehmet Fide
2 siblings, 0 replies; 4+ messages in thread
From: Mehmet Fide @ 2026-11-11 12:15 UTC (permalink / raw)
To: Ping-Ke Shih
Cc: Luka Gejak, Bitterblue Smith, linux-wireless, linux-kernel, mehmet.fide
From: Mehmet Fide <mehmet.fide@screeningeagle.com>
rtw_fw_download_rsvd_page() downloads the beacon twice: once as part of
the reserved page and once more on its own, so that the firmware ends up
with a TX descriptor that describes the beacon rather than the whole
page. The second download fetched a new beacon from mac80211 instead of
downloading the one the page already holds.
Besides the extra work, every beacon fetch advances the DTIM count and,
while a channel switch is announced, the CSA countdown; doing it twice
per update lets a countdown that starts at 2 reach 0, which mac80211
warns about. Hand the beacon skb out of the page build and download
that. The hw scan, which downloads the beacon without a page build,
keeps fetching its own.
Signed-off-by: Mehmet Fide <mehmet.fide@screeningeagle.com>
Reviewed-by: Luka Gejak <luka.gejak@linux.dev>
Acked-by: Ping-Ke Shih <pkshih@realtek.com>
---
Notes:
Changes in v10:
- none, rebased on rtw-next
Changes in v9:
- none
Changes in v8:
- none
Changes in v7:
- none, rebased on rtw-next
Changes in v6:
- none
Changes in v5:
- none
Changes in v4:
- none, rebased on rtw-next
Changes in v3:
- the beacon out-pointer is initialised in the caller
drivers/net/wireless/realtek/rtw88/fw.c | 34 +++++++++++++++----------
1 file changed, 21 insertions(+), 13 deletions(-)
diff --git a/drivers/net/wireless/realtek/rtw88/fw.c b/drivers/net/wireless/realtek/rtw88/fw.c
index 945fedcd375b..3cd17a3bb494 100644
--- a/drivers/net/wireless/realtek/rtw88/fw.c
+++ b/drivers/net/wireless/realtek/rtw88/fw.c
@@ -1622,7 +1622,8 @@ static int __rtw_build_rsvd_page_from_vifs(struct rtw_dev *rtwdev)
return 0;
}
-static u8 *rtw_build_rsvd_page(struct rtw_dev *rtwdev, u32 *size)
+static u8 *rtw_build_rsvd_page(struct rtw_dev *rtwdev, u32 *size,
+ struct sk_buff **beacon)
{
const struct rtw_chip_info *chip = rtwdev->chip;
struct ieee80211_hw *hw = rtwdev->hw;
@@ -1702,13 +1703,15 @@ static u8 *rtw_build_rsvd_page(struct rtw_dev *rtwdev, u32 *size)
list_for_each_entry(rsvd_pkt, &rtwdev->rsvd_page_list, build_list) {
rtw_rsvd_page_list_to_buf(rtwdev, page_size, page_margin,
page, buf, rsvd_pkt);
- if (page == 0)
+ if (page == 0) {
page += rtw_len_to_page(rsvd_pkt->skb->len +
tx_desc_sz, page_size);
- else
+ /* the caller downloads it once more on its own */
+ *beacon = rsvd_pkt->skb;
+ } else {
page += rtw_len_to_page(rsvd_pkt->skb->len, page_size);
-
- kfree_skb(rsvd_pkt->skb);
+ kfree_skb(rsvd_pkt->skb);
+ }
rsvd_pkt->skb = NULL;
}
@@ -1723,11 +1726,12 @@ static u8 *rtw_build_rsvd_page(struct rtw_dev *rtwdev, u32 *size)
return NULL;
}
-static int rtw_download_beacon(struct rtw_dev *rtwdev)
+/* the beacon the page was built with, or a fresh one for the hw scan */
+static int rtw_download_beacon(struct rtw_dev *rtwdev, struct sk_buff *beacon)
{
struct ieee80211_hw *hw = rtwdev->hw;
struct rtw_rsvd_page *rsvd_pkt;
- struct sk_buff *skb;
+ struct sk_buff *skb = beacon;
int ret = 0;
rsvd_pkt = list_first_entry_or_null(&rtwdev->rsvd_page_list,
@@ -1744,7 +1748,8 @@ static int rtw_download_beacon(struct rtw_dev *rtwdev)
return -EINVAL;
}
- skb = rtw_get_rsvd_page_skb(hw, rsvd_pkt);
+ if (!skb)
+ skb = rtw_get_rsvd_page_skb(hw, rsvd_pkt);
if (!skb) {
rtw_err(rtwdev, "failed to get beacon skb\n");
return -ENOMEM;
@@ -1754,18 +1759,20 @@ static int rtw_download_beacon(struct rtw_dev *rtwdev)
if (ret)
rtw_err(rtwdev, "failed to download drv rsvd page\n");
- dev_kfree_skb(skb);
+ if (!beacon)
+ dev_kfree_skb(skb);
return ret;
}
int rtw_fw_download_rsvd_page(struct rtw_dev *rtwdev)
{
- u8 *buf;
+ struct sk_buff *beacon = NULL;
u32 size;
+ u8 *buf;
int ret;
- buf = rtw_build_rsvd_page(rtwdev, &size);
+ buf = rtw_build_rsvd_page(rtwdev, &size, &beacon);
if (!buf) {
rtw_err(rtwdev, "failed to build rsvd page pkt\n");
return -ENOMEM;
@@ -1782,13 +1789,14 @@ int rtw_fw_download_rsvd_page(struct rtw_dev *rtwdev)
* the beacon again to replace the TX desc header, and we will get
* a correct tx_desc for the beacon in the rsvd page.
*/
- ret = rtw_download_beacon(rtwdev);
+ ret = rtw_download_beacon(rtwdev, beacon);
if (ret) {
rtw_err(rtwdev, "failed to download beacon\n");
goto free;
}
free:
+ dev_kfree_skb(beacon);
kfree(buf);
return ret;
@@ -2345,7 +2353,7 @@ int rtw_hw_scan_offload(struct rtw_dev *rtwdev, struct ieee80211_vif *vif,
rtw_fw_set_scan_offload(rtwdev, &cs_option, rtwvif, &chan_list);
out:
if (rtwdev->ap_active) {
- ret = rtw_download_beacon(rtwdev);
+ ret = rtw_download_beacon(rtwdev, NULL);
if (ret)
rtw_err(rtwdev, "HW scan download beacon failed\n");
}
--
2.56.0
^ permalink raw reply [flat|nested] 4+ messages in thread* [PATCH rtw-next v10 2/3] wifi: rtw88: support channel switch in AP mode
2026-11-11 12:15 [PATCH rtw-next v10 0/3] wifi: rtw88: channel switch in AP mode Mehmet Fide
2026-11-11 12:15 ` [PATCH rtw-next v10 1/3] wifi: rtw88: download the beacon the reserved page was built with Mehmet Fide
@ 2026-11-11 12:15 ` Mehmet Fide
2026-11-11 12:15 ` [PATCH rtw-next v10 3/3] wifi: rtw88: time the countdown downloads from the TBTT Mehmet Fide
2 siblings, 0 replies; 4+ messages in thread
From: Mehmet Fide @ 2026-11-11 12:15 UTC (permalink / raw)
To: Ping-Ke Shih
Cc: Luka Gejak, Bitterblue Smith, linux-wireless, linux-kernel, mehmet.fide
From: Mehmet Fide <mehmet.fide@screeningeagle.com>
hostapd's CHAN_SWITCH is refused because the driver does not announce
channel switch support, so an rtw88 AP can only change its channel by
being torn down and started again.
Declare WIPHY_FLAG_HAS_CHANNEL_SWITCH and implement
channel_switch_beacon: the firmware repeats the beacon held in the
first reserved page, so while a switch is announced the page is
downloaded again every beacon interval to advance the countdown, and
ieee80211_csa_finish() is called once it completes. IBSS shares the
page and the work.
The work is a delayed work of the device and csa_vif names the
interface, both under the driver mutex. The countdown is cancelled when
the AP stops, when the interface goes away, on WoWLAN suspend and in
rtw_core_stop(); the BSS_CHANGED_BEACON replay after a hardware restart
resumes it, and so does the driver after a WoWLAN resume, which mac80211
follows with no replay. The other page downloads during a countdown, the
TIM update, the PG backup and the one on association, are skipped until
the beacon after the switch, since every beacon fetch advances the
countdown; the next download carries their changes. A scan is refused:
it would take the AP off the channel its stations count down to.
Signed-off-by: Mehmet Fide <mehmet.fide@screeningeagle.com>
---
Notes:
Changes in v10:
- rtw_core_stop() clears csa_vif and cancels the countdown work again
once it has the mutex back: a switch started while the mutex was
dropped kept csa_vif, so the restart after a firmware recovery did
not resume the countdown, and a work queued then could run early into
the countdown the restart resumes (Luka)
- rebased on rtw-next
Changes in v9:
- the countdown starts again after a WoWLAN resume that worked, which
mac80211 follows with no replay (Luka)
- Acked-by from Ping-Ke dropped for the new code
Changes in v8:
- Acked-by from Ping-Ke
Changes in v7:
- csa_vif is kept until the beacon after the switch: a TIM update
before it fetched one beacon past count 1 and hit the WARN in
ieee80211_beacon_update_cntdwn()
- rebased on rtw-next
Changes in v6:
- the countdown work is an ieee80211 delayed work under the driver
mutex, csa_vif is driver state under the mutex; no wiphy lock in
rtw88 (Ping-Ke)
- rtw_core_stop() cancels the work and clears csa_vif, which covers the
IPS path and the firmware recovery (Ping-Ke)
- the downloads are paced from the first one with an absolute due time:
queued after each download, the countdown drifted by about 10 ms per
beacon interval on the ordered mac80211 workqueue, and a station
switched ahead of the AP on a count of 50
- the BSS_CHANGED_BEACON path says why it starts the countdown (the
replay after a hardware restart) and stops it when the beacon comes
back without a switch announced (Ping-Ke)
Changes in v5:
- rtw_fw_csa_active/start/stop() assert the wiphy lock
- hw_scan() takes the mutex with guard()
- the ASSOC path says why it skips the download: the STA+AP combination
Changes in v4:
- the work and csa_vif live in rtw_dev; no lookup through the reserved page
- the countdown is stopped before a firmware recovery restarts the hardware
- a scan is refused while a switch is announced
- one beacon fetch per interval during the countdown
drivers/net/wireless/realtek/rtw88/fw.c | 80 ++++++++++++++++++-
drivers/net/wireless/realtek/rtw88/fw.h | 4 +
drivers/net/wireless/realtek/rtw88/mac80211.c | 50 ++++++++++--
drivers/net/wireless/realtek/rtw88/main.c | 9 ++-
drivers/net/wireless/realtek/rtw88/main.h | 3 +
5 files changed, 138 insertions(+), 8 deletions(-)
diff --git a/drivers/net/wireless/realtek/rtw88/fw.c b/drivers/net/wireless/realtek/rtw88/fw.c
index 3cd17a3bb494..2d81aaeed0d7 100644
--- a/drivers/net/wireless/realtek/rtw88/fw.c
+++ b/drivers/net/wireless/realtek/rtw88/fw.c
@@ -1802,14 +1802,90 @@ int rtw_fw_download_rsvd_page(struct rtw_dev *rtwdev)
return ret;
}
+bool rtw_fw_csa_active(struct rtw_dev *rtwdev)
+{
+ lockdep_assert_held(&rtwdev->mutex);
+
+ return !!rtwdev->csa_vif;
+}
+
+/* paced from the first download: a late one does not delay the next */
+static void rtw_fw_csa_queue(struct rtw_dev *rtwdev, struct ieee80211_vif *vif)
+{
+ u32 interval = ieee80211_tu_to_usec(vif->bss_conf.beacon_int);
+ s64 delay;
+
+ rtwdev->csa_due = ktime_add_us(rtwdev->csa_due, interval);
+ delay = ktime_us_delta(rtwdev->csa_due, ktime_get());
+ ieee80211_queue_delayed_work(rtwdev->hw, &rtwdev->csa_beacon_work,
+ usecs_to_jiffies(max(delay, 0)));
+}
+
+void rtw_fw_csa_start(struct rtw_dev *rtwdev, struct ieee80211_vif *vif)
+{
+ lockdep_assert_held(&rtwdev->mutex);
+
+ if (rtwdev->csa_vif == vif)
+ return;
+
+ rtwdev->csa_vif = vif;
+ rtwdev->csa_due = ktime_get();
+ rtw_fw_csa_queue(rtwdev, vif);
+}
+
+void rtw_fw_csa_stop(struct rtw_dev *rtwdev, struct ieee80211_vif *vif)
+{
+ lockdep_assert_held(&rtwdev->mutex);
+
+ if (!vif || rtwdev->csa_vif != vif)
+ return;
+
+ rtwdev->csa_vif = NULL;
+ cancel_delayed_work(&rtwdev->csa_beacon_work);
+}
+
void rtw_fw_update_beacon_work(struct work_struct *work)
{
struct rtw_dev *rtwdev = container_of(work, struct rtw_dev,
update_beacon_work);
mutex_lock(&rtwdev->mutex);
- rtw_fw_download_rsvd_page(rtwdev);
- rtw_send_rsvd_page_h2c(rtwdev);
+
+ /* a fetch advances the countdown, the next download carries the TIM */
+ if (!rtw_fw_csa_active(rtwdev)) {
+ rtw_fw_download_rsvd_page(rtwdev);
+ rtw_send_rsvd_page_h2c(rtwdev);
+ }
+
+ mutex_unlock(&rtwdev->mutex);
+}
+
+void rtw_fw_csa_beacon_work(struct work_struct *work)
+{
+ struct rtw_dev *rtwdev = container_of(work, struct rtw_dev,
+ csa_beacon_work.work);
+ struct ieee80211_vif *vif;
+
+ mutex_lock(&rtwdev->mutex);
+
+ vif = rtwdev->csa_vif;
+ if (!vif)
+ goto out;
+
+ if (!vif->bss_conf.csa_active) {
+ rtwdev->csa_vif = NULL;
+ goto out;
+ }
+
+ if (!ieee80211_beacon_cntdwn_is_complete(vif, 0)) {
+ rtw_fw_download_rsvd_page(rtwdev);
+ rtw_send_rsvd_page_h2c(rtwdev);
+ rtw_fw_csa_queue(rtwdev, vif);
+ } else {
+ ieee80211_csa_finish(vif, 0);
+ }
+
+out:
mutex_unlock(&rtwdev->mutex);
}
diff --git a/drivers/net/wireless/realtek/rtw88/fw.h b/drivers/net/wireless/realtek/rtw88/fw.h
index 48ad9ceab6ea..d8e58e4ed836 100644
--- a/drivers/net/wireless/realtek/rtw88/fw.h
+++ b/drivers/net/wireless/realtek/rtw88/fw.h
@@ -863,7 +863,11 @@ void rtw_add_rsvd_page_pno(struct rtw_dev *rtwdev,
void rtw_add_rsvd_page_sta(struct rtw_dev *rtwdev,
struct rtw_vif *rtwvif);
int rtw_fw_download_rsvd_page(struct rtw_dev *rtwdev);
+bool rtw_fw_csa_active(struct rtw_dev *rtwdev);
+void rtw_fw_csa_start(struct rtw_dev *rtwdev, struct ieee80211_vif *vif);
+void rtw_fw_csa_stop(struct rtw_dev *rtwdev, struct ieee80211_vif *vif);
void rtw_fw_update_beacon_work(struct work_struct *work);
+void rtw_fw_csa_beacon_work(struct work_struct *work);
void rtw_send_rsvd_page_h2c(struct rtw_dev *rtwdev);
int rtw_dump_drv_rsvd_page(struct rtw_dev *rtwdev,
u32 offset, u32 size, u32 *buf);
diff --git a/drivers/net/wireless/realtek/rtw88/mac80211.c b/drivers/net/wireless/realtek/rtw88/mac80211.c
index 5e40cf06a9e3..e6c9866fd599 100644
--- a/drivers/net/wireless/realtek/rtw88/mac80211.c
+++ b/drivers/net/wireless/realtek/rtw88/mac80211.c
@@ -237,6 +237,8 @@ static void rtw_ops_remove_interface(struct ieee80211_hw *hw,
mutex_lock(&rtwdev->mutex);
+ rtw_fw_csa_stop(rtwdev, vif);
+
rtw_leave_lps_deep(rtwdev);
rtw_txq_cleanup(rtwdev, vif->txq);
@@ -395,8 +397,11 @@ static void rtw_ops_bss_info_changed(struct ieee80211_hw *hw,
if (vif->cfg.assoc) {
rtw_coex_connect_notify(rtwdev, COEX_ASSOCIATE_FINISH);
- rtw_fw_download_rsvd_page(rtwdev);
- rtw_send_rsvd_page_h2c(rtwdev);
+ /* a station associating while the AP counts down */
+ if (!rtw_fw_csa_active(rtwdev)) {
+ rtw_fw_download_rsvd_page(rtwdev);
+ rtw_send_rsvd_page_h2c(rtwdev);
+ }
rtw_fw_default_port(rtwdev, rtwvif);
rtw_coex_media_status_notify(rtwdev, vif->cfg.assoc);
if (rtw_bf_support)
@@ -438,6 +443,11 @@ static void rtw_ops_bss_info_changed(struct ieee80211_hw *hw,
rtw_set_dtim_period(rtwdev, conf->dtim_period);
rtw_fw_download_rsvd_page(rtwdev);
rtw_send_rsvd_page_h2c(rtwdev);
+ /* restart replay resumes it, a beacon without CSA ends it */
+ if (conf->csa_active)
+ rtw_fw_csa_start(rtwdev, vif);
+ else
+ rtw_fw_csa_stop(rtwdev, vif);
}
if (changed & BSS_CHANGED_BEACON_ENABLED) {
@@ -490,6 +500,7 @@ static void rtw_ops_stop_ap(struct ieee80211_hw *hw,
struct rtw_dev *rtwdev = hw->priv;
mutex_lock(&rtwdev->mutex);
+ rtw_fw_csa_stop(rtwdev, vif);
rtw_write32_clr(rtwdev, REG_TCR, BIT_TCR_UPDATE_HGQMD);
rtw_write16(rtwdev, REG_ATIMWND, ATIMWND_DEFAULT);
rtwdev->ap_active = false;
@@ -556,6 +567,17 @@ static int rtw_ops_set_tim(struct ieee80211_hw *hw, struct ieee80211_sta *sta,
return 0;
}
+static void rtw_ops_channel_switch_beacon(struct ieee80211_hw *hw,
+ struct ieee80211_vif *vif,
+ struct cfg80211_chan_def *chandef)
+{
+ struct rtw_dev *rtwdev = hw->priv;
+
+ mutex_lock(&rtwdev->mutex);
+ rtw_fw_csa_start(rtwdev, vif);
+ mutex_unlock(&rtwdev->mutex);
+}
+
static int rtw_ops_set_key(struct ieee80211_hw *hw, enum set_key_cmd cmd,
struct ieee80211_vif *vif, struct ieee80211_sta *sta,
struct ieee80211_key_conf *key)
@@ -626,7 +648,8 @@ static int rtw_ops_set_key(struct ieee80211_hw *hw, enum set_key_cmd cmd,
}
/* download new cam settings for PG to backup */
- if (rtw_get_lps_deep_mode(rtwdev) == LPS_DEEP_MODE_PG)
+ if (rtw_get_lps_deep_mode(rtwdev) == LPS_DEEP_MODE_PG &&
+ !rtw_fw_csa_active(rtwdev))
rtw_fw_download_rsvd_page(rtwdev);
out:
@@ -846,6 +869,7 @@ static int rtw_ops_suspend(struct ieee80211_hw *hw,
int ret;
mutex_lock(&rtwdev->mutex);
+ rtw_fw_csa_stop(rtwdev, rtwdev->csa_vif);
ret = rtw_wow_suspend(rtwdev, wowlan);
if (ret)
rtw_err(rtwdev, "failed to suspend for wow %d\n", ret);
@@ -854,6 +878,15 @@ static int rtw_ops_suspend(struct ieee80211_hw *hw,
return ret ? 1 : 0;
}
+static void rtw_ops_csa_resume_iter(void *data, struct ieee80211_vif *vif)
+{
+ struct rtw_dev *rtwdev = data;
+
+ if ((vif->type == NL80211_IFTYPE_AP ||
+ vif->type == NL80211_IFTYPE_ADHOC) && vif->bss_conf.csa_active)
+ rtw_fw_csa_start(rtwdev, vif);
+}
+
static int rtw_ops_resume(struct ieee80211_hw *hw)
{
struct rtw_dev *rtwdev = hw->priv;
@@ -863,6 +896,8 @@ static int rtw_ops_resume(struct ieee80211_hw *hw)
ret = rtw_wow_resume(rtwdev);
if (ret)
rtw_err(rtwdev, "failed to resume for wow %d\n", ret);
+ else
+ rtw_iterate_vifs(rtwdev, rtw_ops_csa_resume_iter, rtwdev);
mutex_unlock(&rtwdev->mutex);
return ret ? 1 : 0;
@@ -893,20 +928,24 @@ static int rtw_ops_hw_scan(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
struct rtw_dev *rtwdev = hw->priv;
int ret;
+ guard(mutex)(&rtwdev->mutex);
+
+ /* a scan takes the AP off the channel its stations count down to */
+ if (rtw_fw_csa_active(rtwdev))
+ return -EBUSY;
+
if (!rtw_fw_feature_check(&rtwdev->fw, FW_FEATURE_SCAN_OFFLOAD))
return 1;
if (test_bit(RTW_FLAG_SCANNING, rtwdev->flags))
return -EBUSY;
- mutex_lock(&rtwdev->mutex);
rtw_hw_scan_start(rtwdev, vif, req);
ret = rtw_hw_scan_offload(rtwdev, vif, true);
if (ret) {
rtw_hw_scan_abort(rtwdev);
rtw_err(rtwdev, "HW scan failed with status: %d\n", ret);
}
- mutex_unlock(&rtwdev->mutex);
return ret;
}
@@ -973,6 +1012,7 @@ const struct ieee80211_ops rtw_ops = {
.sta_add = rtw_ops_sta_add,
.sta_remove = rtw_ops_sta_remove,
.set_tim = rtw_ops_set_tim,
+ .channel_switch_beacon = rtw_ops_channel_switch_beacon,
.set_key = rtw_ops_set_key,
.ampdu_action = rtw_ops_ampdu_action,
.can_aggregate_in_amsdu = rtw_ops_can_aggregate_in_amsdu,
diff --git a/drivers/net/wireless/realtek/rtw88/main.c b/drivers/net/wireless/realtek/rtw88/main.c
index f7708ad17b6f..4d9837b07ebf 100644
--- a/drivers/net/wireless/realtek/rtw88/main.c
+++ b/drivers/net/wireless/realtek/rtw88/main.c
@@ -1548,11 +1548,13 @@ void rtw_core_stop(struct rtw_dev *rtwdev)
clear_bit(RTW_FLAG_RUNNING, rtwdev->flags);
clear_bit(RTW_FLAG_FW_RUNNING, rtwdev->flags);
+ rtwdev->csa_vif = NULL;
mutex_unlock(&rtwdev->mutex);
cancel_work_sync(&rtwdev->c2h_work);
cancel_work_sync(&rtwdev->update_beacon_work);
+ cancel_delayed_work_sync(&rtwdev->csa_beacon_work);
cancel_delayed_work_sync(&rtwdev->watch_dog_work);
cancel_delayed_work_sync(&coex->bt_relink_work);
cancel_delayed_work_sync(&coex->bt_reenable_work);
@@ -1565,6 +1567,9 @@ void rtw_core_stop(struct rtw_dev *rtwdev)
mutex_lock(&rtwdev->mutex);
+ rtwdev->csa_vif = NULL;
+ cancel_delayed_work(&rtwdev->csa_beacon_work);
+
rtwdev->chip->ops->power_off(rtwdev);
}
@@ -2168,6 +2173,7 @@ int rtw_core_init(struct rtw_dev *rtwdev)
INIT_WORK(&rtwdev->ips_work, rtw_ips_work);
INIT_WORK(&rtwdev->fw_recovery_work, rtw_fw_recovery_work);
INIT_WORK(&rtwdev->update_beacon_work, rtw_fw_update_beacon_work);
+ INIT_DELAYED_WORK(&rtwdev->csa_beacon_work, rtw_fw_csa_beacon_work);
INIT_WORK(&rtwdev->ba_work, rtw_txq_ba_work);
skb_queue_head_init(&rtwdev->c2h_queue);
skb_queue_head_init(&rtwdev->coex.queue);
@@ -2291,7 +2297,8 @@ int rtw_register_hw(struct rtw_dev *rtwdev, struct ieee80211_hw *hw)
hw->wiphy->available_antennas_rx = hal->antenna_rx;
hw->wiphy->flags |= WIPHY_FLAG_SUPPORTS_TDLS |
- WIPHY_FLAG_TDLS_EXTERNAL_SETUP;
+ WIPHY_FLAG_TDLS_EXTERNAL_SETUP |
+ WIPHY_FLAG_HAS_CHANNEL_SWITCH;
hw->wiphy->features |= NL80211_FEATURE_SCAN_RANDOM_MAC_ADDR;
hw->wiphy->max_scan_ssids = RTW_SCAN_MAX_SSIDS;
diff --git a/drivers/net/wireless/realtek/rtw88/main.h b/drivers/net/wireless/realtek/rtw88/main.h
index 6ecbeb4ffe4b..5d18841a705f 100644
--- a/drivers/net/wireless/realtek/rtw88/main.h
+++ b/drivers/net/wireless/realtek/rtw88/main.h
@@ -2094,6 +2094,9 @@ struct rtw_dev {
struct work_struct ips_work;
struct work_struct fw_recovery_work;
struct work_struct update_beacon_work;
+ struct delayed_work csa_beacon_work;
+ struct ieee80211_vif *csa_vif;
+ ktime_t csa_due;
/* used to protect txqs list */
spinlock_t txq_lock;
--
2.56.0
^ permalink raw reply [flat|nested] 4+ messages in thread* [PATCH rtw-next v10 3/3] wifi: rtw88: time the countdown downloads from the TBTT
2026-11-11 12:15 [PATCH rtw-next v10 0/3] wifi: rtw88: channel switch in AP mode Mehmet Fide
2026-11-11 12:15 ` [PATCH rtw-next v10 1/3] wifi: rtw88: download the beacon the reserved page was built with Mehmet Fide
2026-11-11 12:15 ` [PATCH rtw-next v10 2/3] wifi: rtw88: support channel switch in AP mode Mehmet Fide
@ 2026-11-11 12:15 ` Mehmet Fide
2 siblings, 0 replies; 4+ messages in thread
From: Mehmet Fide @ 2026-11-11 12:15 UTC (permalink / raw)
To: Ping-Ke Shih
Cc: Luka Gejak, Bitterblue Smith, linux-wireless, linux-kernel, mehmet.fide
From: Mehmet Fide <mehmet.fide@screeningeagle.com>
The countdown downloads are queued one beacon interval apart, with no
relation to the TBTT. A download that runs late past the next TBTT
leaves a count out of the air.
Queue each download 10 ms after the next TBTT, read from the TSF, so it
may run late by almost a beacon interval; a later one is followed by
the next at once. A TSF jump, an IBSS merge or a reset, restarts the
schedule from the next TBTT.
The first count is downloaded the same way, not when mac80211 changes
the beacon: a download close to a TBTT keeps beacons off the air. On an
RTL8821CU and an RTL8822CU a TBTT during the download sent no beacon or
the old one. On an RTL8822CE a TBTT up to about 100 us before the
download sent none and the next beacon stayed out too, and with a count
of 2 no CSA beacon went out. The switch completes up to one beacon
interval later. The replay after a hardware restart still downloads at
once.
The USB chips raise no TBTT interrupt, so the TSF register is read. The
beaconing interface is always on port 0: rtw_core_port_switch() moves
an AP there, an IBSS runs alone.
Signed-off-by: Mehmet Fide <mehmet.fide@screeningeagle.com>
---
Notes:
Changes in v10:
- the first count is downloaded by the countdown work, 10 ms after the
next TBTT, and no longer when mac80211 changes the beacon. v9 took a
TBTT up to 10 ms before the start as the one that sent the first
count, but a TBTT during the download sends no CSA beacon, and with
a count of 2 none went out at all (Luka). No download runs close to
a TBTT now, so there is nothing left to guess
- rebased on rtw-next
- Acked-by from Ping-Ke and Reviewed-by from Luka dropped for the
changed code
Changes in v9:
- Acked-by from Ping-Ke (on v8) and Reviewed-by from Luka
Changes in v8:
- the TSF read retries once, if the high word changed, instead of
looping (Ping-Ke)
- rtw_fw_csa_queue() takes a start argument and reads the TSF once,
also on start (Ping-Ke)
- the port 0 comment is gone, the commit message says it (Ping-Ke)
- empty lines where asked (Ping-Ke)
Changes in v7:
- new patch, from Ping-Ke's review of the countdown pacing in v6
drivers/net/wireless/realtek/rtw88/fw.c | 48 ++++++++++++++++---
drivers/net/wireless/realtek/rtw88/mac80211.c | 7 ++-
drivers/net/wireless/realtek/rtw88/main.h | 2 +-
drivers/net/wireless/realtek/rtw88/reg.h | 1 +
4 files changed, 48 insertions(+), 10 deletions(-)
diff --git a/drivers/net/wireless/realtek/rtw88/fw.c b/drivers/net/wireless/realtek/rtw88/fw.c
index 2d81aaeed0d7..c30aa938f9a9 100644
--- a/drivers/net/wireless/realtek/rtw88/fw.c
+++ b/drivers/net/wireless/realtek/rtw88/fw.c
@@ -3,6 +3,7 @@
*/
#include <linux/iopoll.h>
+#include <linux/math64.h>
#include "main.h"
#include "coex.h"
@@ -1809,14 +1810,47 @@ bool rtw_fw_csa_active(struct rtw_dev *rtwdev)
return !!rtwdev->csa_vif;
}
-/* paced from the first download: a late one does not delay the next */
-static void rtw_fw_csa_queue(struct rtw_dev *rtwdev, struct ieee80211_vif *vif)
+#define RTW_CSA_TBTT_MARGIN_US 10000
+
+static u64 rtw_fw_read_tsf(struct rtw_dev *rtwdev)
+{
+ u32 hi, lo, tmp;
+
+ hi = rtw_read32(rtwdev, REG_TSFTR + 4);
+ lo = rtw_read32(rtwdev, REG_TSFTR);
+ tmp = rtw_read32(rtwdev, REG_TSFTR + 4);
+ if (tmp != hi) {
+ hi = tmp;
+ lo = rtw_read32(rtwdev, REG_TSFTR);
+ }
+
+ return ((u64)hi << 32) | lo;
+}
+
+static void rtw_fw_csa_queue(struct rtw_dev *rtwdev, struct ieee80211_vif *vif,
+ bool start)
{
u32 interval = ieee80211_tu_to_usec(vif->bss_conf.beacon_int);
+ u64 tsf = rtw_fw_read_tsf(rtwdev);
s64 delay;
+ u32 rem;
+
+ if (start) {
+ div_u64_rem(tsf, interval, &rem);
+ rtwdev->csa_tbtt = tsf - rem + interval;
+ } else {
+ rtwdev->csa_tbtt += interval;
+ }
+
+ delay = rtwdev->csa_tbtt + RTW_CSA_TBTT_MARGIN_US - tsf;
+ if (delay > interval + RTW_CSA_TBTT_MARGIN_US ||
+ delay < -(s64)interval) {
+ /* the TSF jumped: restart from the next TBTT */
+ div_u64_rem(tsf, interval, &rem);
+ rtwdev->csa_tbtt = tsf - rem + interval;
+ delay = rtwdev->csa_tbtt + RTW_CSA_TBTT_MARGIN_US - tsf;
+ }
- rtwdev->csa_due = ktime_add_us(rtwdev->csa_due, interval);
- delay = ktime_us_delta(rtwdev->csa_due, ktime_get());
ieee80211_queue_delayed_work(rtwdev->hw, &rtwdev->csa_beacon_work,
usecs_to_jiffies(max(delay, 0)));
}
@@ -1829,8 +1863,8 @@ void rtw_fw_csa_start(struct rtw_dev *rtwdev, struct ieee80211_vif *vif)
return;
rtwdev->csa_vif = vif;
- rtwdev->csa_due = ktime_get();
- rtw_fw_csa_queue(rtwdev, vif);
+
+ rtw_fw_csa_queue(rtwdev, vif, true);
}
void rtw_fw_csa_stop(struct rtw_dev *rtwdev, struct ieee80211_vif *vif)
@@ -1880,7 +1914,7 @@ void rtw_fw_csa_beacon_work(struct work_struct *work)
if (!ieee80211_beacon_cntdwn_is_complete(vif, 0)) {
rtw_fw_download_rsvd_page(rtwdev);
rtw_send_rsvd_page_h2c(rtwdev);
- rtw_fw_csa_queue(rtwdev, vif);
+ rtw_fw_csa_queue(rtwdev, vif, false);
} else {
ieee80211_csa_finish(vif, 0);
}
diff --git a/drivers/net/wireless/realtek/rtw88/mac80211.c b/drivers/net/wireless/realtek/rtw88/mac80211.c
index e6c9866fd599..dbd148760b9b 100644
--- a/drivers/net/wireless/realtek/rtw88/mac80211.c
+++ b/drivers/net/wireless/realtek/rtw88/mac80211.c
@@ -441,8 +441,11 @@ static void rtw_ops_bss_info_changed(struct ieee80211_hw *hw,
if (changed & BSS_CHANGED_BEACON) {
rtw_set_dtim_period(rtwdev, conf->dtim_period);
- rtw_fw_download_rsvd_page(rtwdev);
- rtw_send_rsvd_page_h2c(rtwdev);
+ /* the countdown downloads its beacons after a TBTT */
+ if (!conf->csa_active || changed & BSS_CHANGED_BEACON_ENABLED) {
+ rtw_fw_download_rsvd_page(rtwdev);
+ rtw_send_rsvd_page_h2c(rtwdev);
+ }
/* restart replay resumes it, a beacon without CSA ends it */
if (conf->csa_active)
rtw_fw_csa_start(rtwdev, vif);
diff --git a/drivers/net/wireless/realtek/rtw88/main.h b/drivers/net/wireless/realtek/rtw88/main.h
index 5d18841a705f..466992b85c43 100644
--- a/drivers/net/wireless/realtek/rtw88/main.h
+++ b/drivers/net/wireless/realtek/rtw88/main.h
@@ -2096,7 +2096,7 @@ struct rtw_dev {
struct work_struct update_beacon_work;
struct delayed_work csa_beacon_work;
struct ieee80211_vif *csa_vif;
- ktime_t csa_due;
+ u64 csa_tbtt;
/* used to protect txqs list */
spinlock_t txq_lock;
diff --git a/drivers/net/wireless/realtek/rtw88/reg.h b/drivers/net/wireless/realtek/rtw88/reg.h
index afcdb66fdb9a..3ff4ae0885ef 100644
--- a/drivers/net/wireless/realtek/rtw88/reg.h
+++ b/drivers/net/wireless/realtek/rtw88/reg.h
@@ -506,6 +506,7 @@
#define REG_USTIME_TSF 0x055C
#define REG_BCN_MAX_ERR 0x055D
#define REG_RXTSF_OFFSET_CCK 0x055E
+#define REG_TSFTR 0x0560
#define REG_MISC_CTRL 0x0577
#define BIT_EN_FREE_CNT BIT(3)
#define BIT_DIS_SECOND_CCA (BIT(0) | BIT(1))
--
2.56.0
^ permalink raw reply [flat|nested] 4+ messages in thread