* [PATCH rtw-next v2 0/2] wifi: rtw88: channel switch in AP mode
@ 2026-09-30 7:44 Mehmet Fide
2026-09-30 7:44 ` [PATCH rtw-next v2 1/2] wifi: rtw88: download the beacon the reserved page was built with Mehmet Fide
2026-09-30 7:44 ` [PATCH rtw-next v2 2/2] wifi: rtw88: support channel switch in AP mode Mehmet Fide
0 siblings, 2 replies; 3+ messages in thread
From: Mehmet Fide @ 2026-09-30 7:44 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 fails on an rtw88 AP because the driver does not
announce channel switch support, so the only way to move the AP to
another channel is to stop and start it, and every client drops.
This adds the support the way rtw89 has it (edba3f107844 "wifi: rtw89:
implement channel switch support"). The first patch is needed for it:
rtw_fw_download_rsvd_page() fetched the beacon from mac80211 twice per
update, which advanced the CSA countdown twice per beacon interval.
Tested on 6.12.111 with RTL8821CU and RTL8822BU as an AP (hostapd 2.11,
one and two stations): CHAN_SWITCH with counts 1 to 5, the stations
follow without loss; aborted countdowns and STA mode scans are clean.
The hw scan path is compile-tested only, these firmwares have no scan
offload.
v2:
- rtw_download_beacon() takes the beacon from the page build and fetches
its own for the hw scan; v1 left the scan with -ENOENT (Luka Gejak).
- a hw scan is refused while a switch is announced (Ping-Ke).
- one wiphy delayed work per device instead of a delayed work per vif:
serialized with mac80211, not re-initialized by a hw restart, checks
RTW_FLAG_RUNNING, cancelled on stop_ap, vif removal and WoWLAN
suspend, covers IBSS; it starts one beacon interval after the
countdown beacon so the count on air is not skipped.
- commit message of 2/2 without the rtw89 reference (Ping-Ke).
Mehmet Fide (2):
wifi: rtw88: download the beacon the reserved page was built with
wifi: rtw88: support channel switch in AP mode
drivers/net/wireless/realtek/rtw88/fw.c | 75 +++++++++++++++----
drivers/net/wireless/realtek/rtw88/fw.h | 1 +
drivers/net/wireless/realtek/rtw88/mac80211.c | 46 ++++++++++++
drivers/net/wireless/realtek/rtw88/main.c | 4 +-
drivers/net/wireless/realtek/rtw88/main.h | 1 +
5 files changed, 113 insertions(+), 14 deletions(-)
--
2.55.0
^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH rtw-next v2 1/2] wifi: rtw88: download the beacon the reserved page was built with
2026-09-30 7:44 [PATCH rtw-next v2 0/2] wifi: rtw88: channel switch in AP mode Mehmet Fide
@ 2026-09-30 7:44 ` Mehmet Fide
2026-09-30 7:44 ` [PATCH rtw-next v2 2/2] wifi: rtw88: support channel switch in AP mode Mehmet Fide
1 sibling, 0 replies; 3+ messages in thread
From: Mehmet Fide @ 2026-09-30 7:44 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>
---
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..49f09a9f4ed6 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;
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 [flat|nested] 3+ messages in thread
* [PATCH rtw-next v2 2/2] wifi: rtw88: support channel switch in AP mode
2026-09-30 7:44 [PATCH rtw-next v2 0/2] wifi: rtw88: channel switch in AP mode Mehmet Fide
2026-09-30 7:44 ` [PATCH rtw-next v2 1/2] wifi: rtw88: download the beacon the reserved page was built with Mehmet Fide
@ 2026-09-30 7:44 ` Mehmet Fide
1 sibling, 0 replies; 3+ messages in thread
From: Mehmet Fide @ 2026-09-30 7:44 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 AP on rtw88 can only change its channel
by being torn down and started again.
Declare WIPHY_FLAG_HAS_CHANNEL_SWITCH and implement
ieee80211_ops::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 renew the countdown,
and ieee80211_csa_finish() is called once it completes. IBSS, which
the flag enables too, shares the page and the work.
The work is a wiphy delayed work of the device, like
update_beacon_work: rtw88 runs one beaconing interface, a hw restart
replays add_interface without remove_interface, and the wiphy lock
serializes it with the mac80211 state it reads. It is cancelled when
the AP stops, when the vif goes away and on WoWLAN suspend, and a
hardware scan is refused while a switch is announced, since it would
take the AP off the channel its stations count down to.
Signed-off-by: Mehmet Fide <mehmet.fide@screeningeagle.com>
---
drivers/net/wireless/realtek/rtw88/fw.c | 41 +++++++++++++++++
drivers/net/wireless/realtek/rtw88/fw.h | 1 +
drivers/net/wireless/realtek/rtw88/mac80211.c | 46 +++++++++++++++++++
drivers/net/wireless/realtek/rtw88/main.c | 4 +-
drivers/net/wireless/realtek/rtw88/main.h | 1 +
5 files changed, 92 insertions(+), 1 deletion(-)
diff --git a/drivers/net/wireless/realtek/rtw88/fw.c b/drivers/net/wireless/realtek/rtw88/fw.c
index 49f09a9f4ed6..1ad25e0539c4 100644
--- a/drivers/net/wireless/realtek/rtw88/fw.c
+++ b/drivers/net/wireless/realtek/rtw88/fw.c
@@ -1813,6 +1813,47 @@ void rtw_fw_update_beacon_work(struct work_struct *work)
mutex_unlock(&rtwdev->mutex);
}
+/* renew the countdown in the firmware's beacon page until it completes */
+void rtw_fw_csa_beacon_work(struct wiphy *wiphy, struct wiphy_work *work)
+{
+ struct rtw_dev *rtwdev = container_of(work, struct rtw_dev,
+ csa_beacon_work.work);
+ struct rtw_rsvd_page *rsvd_pkt;
+ struct ieee80211_vif *vif;
+ unsigned int delay;
+
+ lockdep_assert_wiphy(wiphy);
+
+ mutex_lock(&rtwdev->mutex);
+
+ if (!test_bit(RTW_FLAG_RUNNING, rtwdev->flags))
+ goto out;
+
+ rsvd_pkt = list_first_entry_or_null(&rtwdev->rsvd_page_list,
+ struct rtw_rsvd_page, build_list);
+ if (!rsvd_pkt || rsvd_pkt->type != RSVD_BEACON)
+ goto out;
+
+ vif = rtwvif_to_vif(rsvd_pkt->rtwvif);
+ if (!vif->bss_conf.csa_active)
+ goto out;
+
+ delay = ieee80211_tu_to_usec(vif->bss_conf.beacon_int);
+
+ if (!ieee80211_beacon_cntdwn_is_complete(vif, 0)) {
+ rtw_fw_download_rsvd_page(rtwdev);
+ rtw_send_rsvd_page_h2c(rtwdev);
+
+ wiphy_delayed_work_queue(wiphy, &rtwdev->csa_beacon_work,
+ usecs_to_jiffies(delay));
+ } else {
+ ieee80211_csa_finish(vif, 0);
+ }
+
+out:
+ mutex_unlock(&rtwdev->mutex);
+}
+
static void rtw_fw_read_fifo_page(struct rtw_dev *rtwdev, u32 offset, u32 size,
u32 *buf, u32 residue, u16 start_pg)
{
diff --git a/drivers/net/wireless/realtek/rtw88/fw.h b/drivers/net/wireless/realtek/rtw88/fw.h
index 48ad9ceab6ea..982f46c03644 100644
--- a/drivers/net/wireless/realtek/rtw88/fw.h
+++ b/drivers/net/wireless/realtek/rtw88/fw.h
@@ -864,6 +864,7 @@ void rtw_add_rsvd_page_sta(struct rtw_dev *rtwdev,
struct rtw_vif *rtwvif);
int rtw_fw_download_rsvd_page(struct rtw_dev *rtwdev);
void rtw_fw_update_beacon_work(struct work_struct *work);
+void rtw_fw_csa_beacon_work(struct wiphy *wiphy, struct wiphy_work *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..7c2a373faff8 100644
--- a/drivers/net/wireless/realtek/rtw88/mac80211.c
+++ b/drivers/net/wireless/realtek/rtw88/mac80211.c
@@ -235,6 +235,10 @@ static void rtw_ops_remove_interface(struct ieee80211_hw *hw,
rtw_dbg(rtwdev, RTW_DBG_STATE, "stop vif %pM mac_id %d on port %d\n",
vif->addr, rtwvif->mac_id, rtwvif->port);
+ if (rtwvif->net_type == RTW_NET_AP_MODE ||
+ rtwvif->net_type == RTW_NET_AD_HOC)
+ wiphy_delayed_work_cancel(hw->wiphy, &rtwdev->csa_beacon_work);
+
mutex_lock(&rtwdev->mutex);
rtw_leave_lps_deep(rtwdev);
@@ -375,6 +379,13 @@ static void rtw_conf_tx(struct rtw_dev *rtwdev,
__rtw_conf_tx(rtwdev, rtwvif, ac);
}
+/* renew the channel switch countdown one beacon interval from now */
+static void rtw_csa_beacon_queue(struct rtw_dev *rtwdev, u16 beacon_int)
+{
+ wiphy_delayed_work_queue(rtwdev->hw->wiphy, &rtwdev->csa_beacon_work,
+ usecs_to_jiffies(ieee80211_tu_to_usec(beacon_int)));
+}
+
static void rtw_ops_bss_info_changed(struct ieee80211_hw *hw,
struct ieee80211_vif *vif,
struct ieee80211_bss_conf *conf,
@@ -438,6 +449,9 @@ 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);
+ /* a hw restart replays the beacon, not channel_switch_beacon */
+ if (conf->csa_active)
+ rtw_csa_beacon_queue(rtwdev, conf->beacon_int);
}
if (changed & BSS_CHANGED_BEACON_ENABLED) {
@@ -489,6 +503,8 @@ static void rtw_ops_stop_ap(struct ieee80211_hw *hw,
{
struct rtw_dev *rtwdev = hw->priv;
+ wiphy_delayed_work_cancel(hw->wiphy, &rtwdev->csa_beacon_work);
+
mutex_lock(&rtwdev->mutex);
rtw_write32_clr(rtwdev, REG_TCR, BIT_TCR_UPDATE_HGQMD);
rtw_write16(rtwdev, REG_ATIMWND, ATIMWND_DEFAULT);
@@ -556,6 +572,16 @@ 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;
+
+ /* the beacon that starts the countdown was just downloaded */
+ rtw_csa_beacon_queue(rtwdev, vif->bss_conf.beacon_int);
+}
+
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)
@@ -845,6 +871,8 @@ static int rtw_ops_suspend(struct ieee80211_hw *hw,
struct rtw_dev *rtwdev = hw->priv;
int ret;
+ wiphy_delayed_work_cancel(hw->wiphy, &rtwdev->csa_beacon_work);
+
mutex_lock(&rtwdev->mutex);
ret = rtw_wow_suspend(rtwdev, wowlan);
if (ret)
@@ -887,10 +915,19 @@ static void rtw_reconfig_complete(struct ieee80211_hw *hw,
mutex_unlock(&rtwdev->mutex);
}
+static void rtw_csa_active_iter(void *data, struct ieee80211_vif *vif)
+{
+ bool *csa_active = data;
+
+ if (vif->bss_conf.csa_active)
+ *csa_active = true;
+}
+
static int rtw_ops_hw_scan(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
struct ieee80211_scan_request *req)
{
struct rtw_dev *rtwdev = hw->priv;
+ bool csa_active = false;
int ret;
if (!rtw_fw_feature_check(&rtwdev->fw, FW_FEATURE_SCAN_OFFLOAD))
@@ -900,6 +937,14 @@ static int rtw_ops_hw_scan(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
return -EBUSY;
mutex_lock(&rtwdev->mutex);
+
+ /* the stations count down to the new channel and expect the AP there */
+ rtw_iterate_vifs(rtwdev, rtw_csa_active_iter, &csa_active);
+ if (csa_active) {
+ mutex_unlock(&rtwdev->mutex);
+ return -EBUSY;
+ }
+
rtw_hw_scan_start(rtwdev, vif, req);
ret = rtw_hw_scan_offload(rtwdev, vif, true);
if (ret) {
@@ -973,6 +1018,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..6a2c333d1a01 100644
--- a/drivers/net/wireless/realtek/rtw88/main.c
+++ b/drivers/net/wireless/realtek/rtw88/main.c
@@ -2170,6 +2170,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);
+ wiphy_delayed_work_init(&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 +2294,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..21d65692e809 100644
--- a/drivers/net/wireless/realtek/rtw88/main.h
+++ b/drivers/net/wireless/realtek/rtw88/main.h
@@ -2094,6 +2094,7 @@ struct rtw_dev {
struct work_struct ips_work;
struct work_struct fw_recovery_work;
struct work_struct update_beacon_work;
+ struct wiphy_delayed_work csa_beacon_work;
/* used to protect txqs list */
spinlock_t txq_lock;
--
2.55.0
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-09-30 7:44 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-30 7:44 [PATCH rtw-next v2 0/2] wifi: rtw88: channel switch in AP mode Mehmet Fide
2026-09-30 7:44 ` [PATCH rtw-next v2 1/2] wifi: rtw88: download the beacon the reserved page was built with Mehmet Fide
2026-09-30 7:44 ` [PATCH rtw-next v2 2/2] wifi: rtw88: support channel switch in AP mode Mehmet Fide
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®