* [PATCH rtw-next v6 1/2] wifi: rtw88: download the beacon the reserved page was built with
2026-10-13 7:30 [PATCH rtw-next v6 0/2] wifi: rtw88: channel switch in AP mode Mehmet Fide
@ 2026-10-13 7:30 ` Mehmet Fide
2026-10-13 7:30 ` [PATCH rtw-next v6 2/2] wifi: rtw88: support channel switch in AP mode Mehmet Fide
1 sibling, 0 replies; 3+ messages in thread
From: Mehmet Fide @ 2026-10-13 7:30 UTC (permalink / raw)
To: Ping-Ke Shih; +Cc: 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 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.55.0
^ permalink raw reply related [flat|nested] 3+ messages in thread* [PATCH rtw-next v6 2/2] wifi: rtw88: support channel switch in AP mode
2026-10-13 7:30 [PATCH rtw-next v6 0/2] wifi: rtw88: channel switch in AP mode Mehmet Fide
2026-10-13 7:30 ` [PATCH rtw-next v6 1/2] wifi: rtw88: download the beacon the reserved page was built with Mehmet Fide
@ 2026-10-13 7:30 ` Mehmet Fide
1 sibling, 0 replies; 3+ messages in thread
From: Mehmet Fide @ 2026-10-13 7:30 UTC (permalink / raw)
To: Ping-Ke Shih; +Cc: 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. The other page downloads during a countdown, the
TIM update, the PG backup and the one on association, are skipped
since every beacon fetch advances it; the next countdown 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 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 | 81 ++++++++++++++++++-
drivers/net/wireless/realtek/rtw88/fw.h | 4 +
drivers/net/wireless/realtek/rtw88/mac80211.c | 39 +++++++--
drivers/net/wireless/realtek/rtw88/main.c | 6 +-
drivers/net/wireless/realtek/rtw88/main.h | 3 +
5 files changed, 125 insertions(+), 8 deletions(-)
diff --git a/drivers/net/wireless/realtek/rtw88/fw.c b/drivers/net/wireless/realtek/rtw88/fw.c
index 3cd17a3bb494..07412aacd7d9 100644
--- a/drivers/net/wireless/realtek/rtw88/fw.c
+++ b/drivers/net/wireless/realtek/rtw88/fw.c
@@ -1802,14 +1802,91 @@ 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 {
+ rtwdev->csa_vif = NULL;
+ 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 2a9b09fa76e7..182aecc76820 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);
@@ -893,20 +917,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 +1001,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 0f23498b5c96..48e8cad221b9 100644
--- a/drivers/net/wireless/realtek/rtw88/main.c
+++ b/drivers/net/wireless/realtek/rtw88/main.c
@@ -1550,11 +1550,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);
@@ -2170,6 +2172,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);
@@ -2293,7 +2296,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 d59f6e323adf..bc3ebfec11ae 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.55.0
^ permalink raw reply related [flat|nested] 3+ messages in thread