mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH rtw-next v3 0/2] wifi: rtw88: channel switch in AP mode
@ 2026-10-05 14:35 Mehmet Fide
  2026-10-05 14:35 ` [PATCH rtw-next v3 1/2] wifi: rtw88: download the beacon the reserved page was built with Mehmet Fide
  2026-10-05 14:35 ` [PATCH rtw-next v3 2/2] wifi: rtw88: support channel switch in AP mode Mehmet Fide
  0 siblings, 2 replies; 12+ messages in thread
From: Mehmet Fide @ 2026-10-05 14:35 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>

An rtw88 AP cannot change its channel: the driver does not announce
channel switch support, so hostapd's CHAN_SWITCH is refused and the
only way is to tear the AP down. The series fixes the beacon download
the reserved page build makes twice, then implements
channel_switch_beacon on top of the firmware's beacon page.

v3:
- 1/2: the beacon out-pointer is initialised in the caller (Luka)
- 2/2: the countdown work lives in the interface, remove_interface
  cancels it unconditionally, the queue wrapper and the comments are
  gone (Ping-Ke)
- 2/2: the TIM update and the PG backup on set_key skip the page
  download while a switch is announced; every beacon fetch advances
  the countdown (Luka)
- 2/2: the commit message names the refused hardware scan (Ping-Ke)
- the hardware scan path ran on an RTL8822CU, whose firmware has scan
  offload: a scan with the AP up keeps the beacon and the station, a
  scan during a countdown is refused

v2:
- 2/2: a hardware scan is refused while a switch is announced
- the rtw89 mention is gone from the cover

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       | 93 ++++++++++++++++---
 drivers/net/wireless/realtek/rtw88/fw.h       |  2 +
 drivers/net/wireless/realtek/rtw88/mac80211.c | 53 ++++++++++-
 drivers/net/wireless/realtek/rtw88/main.c     |  3 +-
 drivers/net/wireless/realtek/rtw88/main.h     |  2 +
 5 files changed, 138 insertions(+), 15 deletions(-)

-- 
2.55.0


^ permalink raw reply	[flat|nested] 12+ messages in thread

* [PATCH rtw-next v3 1/2] wifi: rtw88: download the beacon the reserved page was built with
  2026-10-05 14:35 [PATCH rtw-next v3 0/2] wifi: rtw88: channel switch in AP mode Mehmet Fide
@ 2026-10-05 14:35 ` Mehmet Fide
  2026-10-05 17:36   ` Luka Gejak
  2026-10-05 14:35 ` [PATCH rtw-next v3 2/2] wifi: rtw88: support channel switch in AP mode Mehmet Fide
  1 sibling, 1 reply; 12+ messages in thread
From: Mehmet Fide @ 2026-10-05 14:35 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>
Acked-by: Ping-Ke Shih <pkshih@realtek.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..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	[flat|nested] 12+ messages in thread

* [PATCH rtw-next v3 2/2] wifi: rtw88: support channel switch in AP mode
  2026-10-05 14:35 [PATCH rtw-next v3 0/2] wifi: rtw88: channel switch in AP mode Mehmet Fide
  2026-10-05 14:35 ` [PATCH rtw-next v3 1/2] wifi: rtw88: download the beacon the reserved page was built with Mehmet Fide
@ 2026-10-05 14:35 ` Mehmet Fide
  2026-10-05 17:33   ` [PATCH " Luka Gejak
  2026-10-06  1:26   ` Ping-Ke Shih
  1 sibling, 2 replies; 12+ messages in thread
From: Mehmet Fide @ 2026-10-05 14:35 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 interface; the wiphy lock
serializes it with the mac80211 state it reads and the driver mutex
with the page build. It is cancelled when the AP stops, when the
interface goes away and on WoWLAN suspend, and a hardware restart,
which replays add_interface with the work still armed, does not
initialise it again. The other page downloads
that can run during a countdown, the TIM update and the PG backup on
set_key, are skipped then, since every beacon fetch advances the
countdown. A hardware scan is refused while a switch is announced: 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       | 59 +++++++++++++++++++
 drivers/net/wireless/realtek/rtw88/fw.h       |  2 +
 drivers/net/wireless/realtek/rtw88/mac80211.c | 55 ++++++++++++++++-
 drivers/net/wireless/realtek/rtw88/main.c     |  3 +-
 drivers/net/wireless/realtek/rtw88/main.h     |  2 +
 5 files changed, 119 insertions(+), 2 deletions(-)

diff --git a/drivers/net/wireless/realtek/rtw88/fw.c b/drivers/net/wireless/realtek/rtw88/fw.c
index 3cd17a3bb494..5e68ba73548a 100644
--- a/drivers/net/wireless/realtek/rtw88/fw.c
+++ b/drivers/net/wireless/realtek/rtw88/fw.c
@@ -1802,14 +1802,73 @@ int rtw_fw_download_rsvd_page(struct rtw_dev *rtwdev)
 	return ret;
 }
 
+static struct ieee80211_vif *rtw_fw_beacon_vif(struct rtw_dev *rtwdev)
+{
+	struct rtw_rsvd_page *rsvd_pkt;
+
+	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)
+		return NULL;
+
+	return rtwvif_to_vif(rsvd_pkt->rtwvif);
+}
+
+bool rtw_fw_csa_active(struct rtw_dev *rtwdev)
+{
+	struct ieee80211_vif *vif = rtw_fw_beacon_vif(rtwdev);
+
+	return vif && vif->bss_conf.csa_active;
+}
+
 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);
+
+	if (rtw_fw_csa_active(rtwdev))
+		goto out;
+
 	rtw_fw_download_rsvd_page(rtwdev);
 	rtw_send_rsvd_page_h2c(rtwdev);
+
+out:
+	mutex_unlock(&rtwdev->mutex);
+}
+
+void rtw_fw_csa_beacon_work(struct wiphy *wiphy, struct wiphy_work *work)
+{
+	struct rtw_vif *rtwvif = container_of(work, struct rtw_vif,
+					      csa_beacon_work.work);
+	struct rtw_dev *rtwdev = wiphy_to_ieee80211_hw(wiphy)->priv;
+	struct ieee80211_vif *vif = rtwvif_to_vif(rtwvif);
+	unsigned int delay;
+
+	lockdep_assert_wiphy(wiphy);
+
+	mutex_lock(&rtwdev->mutex);
+
+	if (!test_bit(RTW_FLAG_RUNNING, rtwdev->flags))
+		goto out;
+
+	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, &rtwvif->csa_beacon_work,
+					 usecs_to_jiffies(delay));
+	} 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..a0a1dcbf93de 100644
--- a/drivers/net/wireless/realtek/rtw88/fw.h
+++ b/drivers/net/wireless/realtek/rtw88/fw.h
@@ -863,7 +863,9 @@ 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_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..4c5be1bd9875 100644
--- a/drivers/net/wireless/realtek/rtw88/mac80211.c
+++ b/drivers/net/wireless/realtek/rtw88/mac80211.c
@@ -164,6 +164,9 @@ static int rtw_ops_add_interface(struct ieee80211_hw *hw,
 	memset(&rtwvif->bfee, 0, sizeof(struct rtw_bfee));
 	rtw_txq_init(rtwdev, vif->txq);
 	INIT_LIST_HEAD(&rtwvif->rsvd_page_list);
+	if (!test_bit(RTW_FLAG_RESTARTING, rtwdev->flags))
+		wiphy_delayed_work_init(&rtwvif->csa_beacon_work,
+					rtw_fw_csa_beacon_work);
 
 	mutex_lock(&rtwdev->mutex);
 
@@ -235,6 +238,8 @@ 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);
 
+	wiphy_delayed_work_cancel(hw->wiphy, &rtwvif->csa_beacon_work);
+
 	mutex_lock(&rtwdev->mutex);
 
 	rtw_leave_lps_deep(rtwdev);
@@ -438,6 +443,13 @@ 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);
+		if (conf->csa_active) {
+			u32 interval = ieee80211_tu_to_usec(conf->beacon_int);
+
+			wiphy_delayed_work_queue(hw->wiphy,
+						 &rtwvif->csa_beacon_work,
+						 usecs_to_jiffies(interval));
+		}
 	}
 
 	if (changed & BSS_CHANGED_BEACON_ENABLED) {
@@ -487,8 +499,11 @@ static void rtw_ops_stop_ap(struct ieee80211_hw *hw,
 			    struct ieee80211_vif *vif,
 			    struct ieee80211_bss_conf *link_conf)
 {
+	struct rtw_vif *rtwvif = (struct rtw_vif *)vif->drv_priv;
 	struct rtw_dev *rtwdev = hw->priv;
 
+	wiphy_delayed_work_cancel(hw->wiphy, &rtwvif->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 +571,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)
+{
+	u32 interval = ieee80211_tu_to_usec(vif->bss_conf.beacon_int);
+	struct rtw_vif *rtwvif = (struct rtw_vif *)vif->drv_priv;
+
+	wiphy_delayed_work_queue(hw->wiphy, &rtwvif->csa_beacon_work,
+				 usecs_to_jiffies(interval));
+}
+
 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 +652,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:
@@ -839,6 +866,14 @@ static int rtw_ops_get_antenna(struct ieee80211_hw *hw,
 }
 
 #ifdef CONFIG_PM
+static void rtw_csa_cancel_iter(void *data, struct ieee80211_vif *vif)
+{
+	struct rtw_vif *rtwvif = (struct rtw_vif *)vif->drv_priv;
+	struct rtw_dev *rtwdev = data;
+
+	wiphy_delayed_work_cancel(rtwdev->hw->wiphy, &rtwvif->csa_beacon_work);
+}
+
 static int rtw_ops_suspend(struct ieee80211_hw *hw,
 			   struct cfg80211_wowlan *wowlan)
 {
@@ -846,6 +881,7 @@ static int rtw_ops_suspend(struct ieee80211_hw *hw,
 	int ret;
 
 	mutex_lock(&rtwdev->mutex);
+	rtw_iterate_vifs(rtwdev, rtw_csa_cancel_iter, rtwdev);
 	ret = rtw_wow_suspend(rtwdev, wowlan);
 	if (ret)
 		rtw_err(rtwdev, "failed to suspend for wow %d\n", ret);
@@ -887,10 +923,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 +945,13 @@ static int rtw_ops_hw_scan(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
 		return -EBUSY;
 
 	mutex_lock(&rtwdev->mutex);
+
+	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 +1025,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..9f3da59b208f 100644
--- a/drivers/net/wireless/realtek/rtw88/main.c
+++ b/drivers/net/wireless/realtek/rtw88/main.c
@@ -2293,7 +2293,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..9ab8fbbe37c6 100644
--- a/drivers/net/wireless/realtek/rtw88/main.h
+++ b/drivers/net/wireless/realtek/rtw88/main.h
@@ -839,6 +839,8 @@ struct rtw_vif {
 	struct rtw_traffic_stats stats;
 
 	struct rtw_bfee bfee;
+
+	struct wiphy_delayed_work csa_beacon_work;
 };
 
 struct rtw_regulatory {
-- 
2.55.0


^ permalink raw reply	[flat|nested] 12+ messages in thread

* Re: [PATCH v3 2/2] wifi: rtw88: support channel switch in AP mode
  2026-10-05 14:35 ` [PATCH rtw-next v3 2/2] wifi: rtw88: support channel switch in AP mode Mehmet Fide
@ 2026-10-05 17:33   ` Luka Gejak
  2026-10-06  7:46     ` [PATCH rtw-next " Mehmet Fide
  2026-10-06  1:26   ` Ping-Ke Shih
  1 sibling, 1 reply; 12+ messages in thread
From: Luka Gejak @ 2026-10-05 17:33 UTC (permalink / raw)
  To: Mehmet Fide
  Cc: Ping-Ke Shih, Bitterblue Smith, mehmet.fide, linux-wireless,
	linux-kernel, Luka Gejak

On Mon, 05 Oct 2026, Mehmet Fide wrote:

> @@ -626,7 +652,8 @@ static int rtw_ops_set_key(struct ieee80211_hw *hw, enum set_key_cmd cmd,
> [...]
> -	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);

The assoc change downloads the same page, and it is not guarded:

	if (changed & BSS_CHANGED_ASSOC) {
		rtw_vif_assoc_changed(rtwvif, conf);
		if (vif->cfg.assoc) {
			rtw_coex_connect_notify(rtwdev, COEX_ASSOCIATE_FINISH);

			rtw_fw_download_rsvd_page(rtwdev);

The page holds the beacon of the AP vif, and building it fetches that
beacon:

	case RSVD_BEACON:
		skb_new = ieee80211_beacon_get_tim(hw, vif, &tim_offset, NULL, 0);

mac80211 steps the countdown on that fetch, not once per transmitted
beacon:

	if (beacon->cntdwn_counter_offsets[0]) {
		if (!is_template)
			ieee80211_beacon_update_cntdwn(vif, link->link_id);

so an association during the countdown moves the count a second time in
the same interval. Can this download skip the page while a switch is
announced, the way set_key does?

> @@ -900,6 +945,13 @@ static int rtw_ops_hw_scan(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
>  	mutex_lock(&rtwdev->mutex);
> +
> +	rtw_iterate_vifs(rtwdev, rtw_csa_active_iter, &csa_active);
> +	if (csa_active) {
> +		mutex_unlock(&rtwdev->mutex);
> +		return -EBUSY;
> +	}

The hw scan check sits below the offload test, so with firmware without
scan offload the op returns before it:

	if (!rtw_fw_feature_check(&rtwdev->fw, FW_FEATURE_SCAN_OFFLOAD))
		return 1;

A return of one tells mac80211 to run the scan in software:

	if (hw_scan && rc == 1) {
		/*
		 * we can't fall back to software for P2P-GO
		 * as it must update NoA etc.
		 */
		if (ieee80211_vif_type_p2p(&sdata->vif) ==
				NL80211_IFTYPE_P2P_GO)
			return -EOPNOTSUPP;
		hw_scan = false;
		goto again;
	}

so the AP leaves the channel during the countdown anyway. Can the check
move above the feature test?

Best regards,
Luka Gejak

^ permalink raw reply	[flat|nested] 12+ messages in thread

* Re: [PATCH rtw-next v3 1/2] wifi: rtw88: download the beacon the reserved page was built with
  2026-10-05 14:35 ` [PATCH rtw-next v3 1/2] wifi: rtw88: download the beacon the reserved page was built with Mehmet Fide
@ 2026-10-05 17:36   ` Luka Gejak
  2026-10-06  7:46     ` Mehmet Fide
  0 siblings, 1 reply; 12+ messages in thread
From: Luka Gejak @ 2026-10-05 17:36 UTC (permalink / raw)
  To: Mehmet Fide, Ping-Ke Shih
  Cc: Bitterblue Smith, linux-wireless, linux-kernel, mehmet.fide, luka.gejak

October 5, 2026 at 16:35, "Mehmet Fide" <mehmet.fide@gmail.com mailto:mehmet.fide@gmail.com?to=%22Mehmet%20Fide%22%20%3Cmehmet.fide%40gmail.com%3E > wrote:


> 
> 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>
> Acked-by: Ping-Ke Shih <pkshih@realtek.com>
> ---

Patch 1 looks good to me.

Reviewed-by: Luka Gejak <luka.gejak@linux.dev>

Best regards,
Luka Gejak

^ permalink raw reply	[flat|nested] 12+ messages in thread

* RE: [PATCH rtw-next v3 2/2] wifi: rtw88: support channel switch in AP mode
  2026-10-05 14:35 ` [PATCH rtw-next v3 2/2] wifi: rtw88: support channel switch in AP mode Mehmet Fide
  2026-10-05 17:33   ` [PATCH " Luka Gejak
@ 2026-10-06  1:26   ` Ping-Ke Shih
  2026-10-06  7:46     ` Mehmet Fide
  1 sibling, 1 reply; 12+ messages in thread
From: Ping-Ke Shih @ 2026-10-06  1:26 UTC (permalink / raw)
  To: Mehmet Fide
  Cc: Luka Gejak, Bitterblue Smith, linux-wireless, linux-kernel, mehmet.fide


Mehmet Fide <mehmet.fide@gmail.com> wrote:
> diff --git a/drivers/net/wireless/realtek/rtw88/fw.c b/drivers/net/wireless/realtek/rtw88/fw.c
> index 3cd17a3bb494..5e68ba73548a 100644
> --- a/drivers/net/wireless/realtek/rtw88/fw.c
> +++ b/drivers/net/wireless/realtek/rtw88/fw.c
> @@ -1802,14 +1802,73 @@ int rtw_fw_download_rsvd_page(struct rtw_dev *rtwdev)
>         return ret;
>  }
> 
> +static struct ieee80211_vif *rtw_fw_beacon_vif(struct rtw_dev *rtwdev)

There is single one caller, so just squash into rtw_fw_csa_active().

I asked to move CS work to vif, because I want to avoid this kind of
getting vif from rsvd_pkt->rtwvif. Is there any way to avoid this?

> +{
> +       struct rtw_rsvd_page *rsvd_pkt;
> +
> +       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)
> +               return NULL;
> +
> +       return rtwvif_to_vif(rsvd_pkt->rtwvif);
> +}
> +
> +bool rtw_fw_csa_active(struct rtw_dev *rtwdev)
> +{
> +       struct ieee80211_vif *vif = rtw_fw_beacon_vif(rtwdev);
> +
> +       return vif && vif->bss_conf.csa_active;
> +}
> +
>  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);
> +
> +       if (rtw_fw_csa_active(rtwdev))
> +               goto out;
> +
>         rtw_fw_download_rsvd_page(rtwdev);
>         rtw_send_rsvd_page_h2c(rtwdev);
> +
> +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..a0a1dcbf93de 100644
> --- a/drivers/net/wireless/realtek/rtw88/fw.h
> +++ b/drivers/net/wireless/realtek/rtw88/fw.h
> @@ -863,7 +863,9 @@ 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_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..4c5be1bd9875 100644
> --- a/drivers/net/wireless/realtek/rtw88/mac80211.c
> +++ b/drivers/net/wireless/realtek/rtw88/mac80211.c
> @@ -164,6 +164,9 @@ static int rtw_ops_add_interface(struct ieee80211_hw *hw,
>         memset(&rtwvif->bfee, 0, sizeof(struct rtw_bfee));
>         rtw_txq_init(rtwdev, vif->txq);
>         INIT_LIST_HEAD(&rtwvif->rsvd_page_list);
> +       if (!test_bit(RTW_FLAG_RESTARTING, rtwdev->flags))

Will it be a problem just unconditionally initializing csa work?

> +               wiphy_delayed_work_init(&rtwvif->csa_beacon_work,
> +                                       rtw_fw_csa_beacon_work);
> 
>         mutex_lock(&rtwdev->mutex);
> 
> @@ -235,6 +238,8 @@ 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);
> 
> +       wiphy_delayed_work_cancel(hw->wiphy, &rtwvif->csa_beacon_work);
> +
>         mutex_lock(&rtwdev->mutex);
> 
>         rtw_leave_lps_deep(rtwdev);
> @@ -438,6 +443,13 @@ 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);
> +               if (conf->csa_active) {
> +                       u32 interval = ieee80211_tu_to_usec(conf->beacon_int);
> +
> +                       wiphy_delayed_work_queue(hw->wiphy,
> +                                                &rtwvif->csa_beacon_work,
> +                                                usecs_to_jiffies(interval));
> +               }
>         }
> 
>         if (changed & BSS_CHANGED_BEACON_ENABLED) {
> @@ -487,8 +499,11 @@ static void rtw_ops_stop_ap(struct ieee80211_hw *hw,
>                             struct ieee80211_vif *vif,
>                             struct ieee80211_bss_conf *link_conf)
>  {
> +       struct rtw_vif *rtwvif = (struct rtw_vif *)vif->drv_priv;
>         struct rtw_dev *rtwdev = hw->priv;
> 
> +       wiphy_delayed_work_cancel(hw->wiphy, &rtwvif->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 +571,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)
> +{
> +       u32 interval = ieee80211_tu_to_usec(vif->bss_conf.beacon_int);
> +       struct rtw_vif *rtwvif = (struct rtw_vif *)vif->drv_priv;
> +
> +       wiphy_delayed_work_queue(hw->wiphy, &rtwvif->csa_beacon_work,
> +                                usecs_to_jiffies(interval));
> +}
> +
>  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 +652,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))

As the comment, this is to download new CAM settings. If CSA is ongoning,
you ignore the download. Then, my question is when will you download
this properly?

>                 rtw_fw_download_rsvd_page(rtwdev);
> 
>  out:
> @@ -839,6 +866,14 @@ static int rtw_ops_get_antenna(struct ieee80211_hw *hw,
>  }
> 
>  #ifdef CONFIG_PM
> +static void rtw_csa_cancel_iter(void *data, struct ieee80211_vif *vif)
> +{
> +       struct rtw_vif *rtwvif = (struct rtw_vif *)vif->drv_priv;
> +       struct rtw_dev *rtwdev = data;
> +
> +       wiphy_delayed_work_cancel(rtwdev->hw->wiphy, &rtwvif->csa_beacon_work);
> +}
> +
>  static int rtw_ops_suspend(struct ieee80211_hw *hw,
>                            struct cfg80211_wowlan *wowlan)
>  {
> @@ -846,6 +881,7 @@ static int rtw_ops_suspend(struct ieee80211_hw *hw,
>         int ret;
> 
>         mutex_lock(&rtwdev->mutex);
> +       rtw_iterate_vifs(rtwdev, rtw_csa_cancel_iter, rtwdev);

I'm thinking if we move back csa work to rtwdev (like v2) to avoid this kind of
iterative. 

>         ret = rtw_wow_suspend(rtwdev, wowlan);
>         if (ret)
>                 rtw_err(rtwdev, "failed to suspend for wow %d\n", ret);
> @@ -887,10 +923,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 +945,13 @@ static int rtw_ops_hw_scan(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
>                 return -EBUSY;
> 
>         mutex_lock(&rtwdev->mutex);
> +
> +       rtw_iterate_vifs(rtwdev, rtw_csa_active_iter, &csa_active);

and avoid this iterative. 

> +       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) {




^ permalink raw reply	[flat|nested] 12+ messages in thread

* Re: [PATCH rtw-next v3 2/2] wifi: rtw88: support channel switch in AP mode
  2026-10-06  1:26   ` Ping-Ke Shih
@ 2026-10-06  7:46     ` Mehmet Fide
  2026-10-06  7:54       ` Ping-Ke Shih
  0 siblings, 1 reply; 12+ messages in thread
From: Mehmet Fide @ 2026-10-06  7:46 UTC (permalink / raw)
  To: Ping-Ke Shih
  Cc: Luka Gejak, Bitterblue Smith, linux-wireless, linux-kernel, mehmet.fide

Hi Ping-Ke,

On 2026-10-06 Ping-Ke Shih wrote:
> There is single one caller, so just squash into rtw_fw_csa_active().
>
> I asked to move CS work to vif, because I want to avoid this kind of
> getting vif from rsvd_pkt->rtwvif. Is there any way to avoid this?

> I'm thinking if we move back csa work to rtwdev (like v2) to avoid this kind of
> iterative.

> and avoid this iterative.

Yes. v4 puts the work back in rtw_dev, initialised once in
rtw_core_init(), and adds rtwdev->csa_vif, set when the countdown
starts and cleared when it finishes or is cancelled. rtw_fw_csa_active()
is then a test of that pointer, and the lookup through the reserved page
and both iterations are gone.

> Will it be a problem just unconditionally initializing csa work?

It was: a hardware restart replays add_interface() with the work still
armed, and initialising it again would lose the pending timer. With the
work in rtw_dev the question does not arise any more.

> As the comment, this is to download new CAM settings. If CSA is ongoning,
> you ignore the download. Then, my question is when will you download
> this properly?

The countdown work downloads the whole reserved page, which includes
the PG info page built from the current CAM, once per beacon interval
while the switch is announced, so the new settings reach the firmware
at most one beacon interval later. The same holds for the TIM update
and the download on association, which v4 skips too (Luka). The commit
message says so now.

Best regards,
Mehmet

^ permalink raw reply	[flat|nested] 12+ messages in thread

* Re: [PATCH rtw-next v3 2/2] wifi: rtw88: support channel switch in AP mode
  2026-10-05 17:33   ` [PATCH " Luka Gejak
@ 2026-10-06  7:46     ` Mehmet Fide
  0 siblings, 0 replies; 12+ messages in thread
From: Mehmet Fide @ 2026-10-06  7:46 UTC (permalink / raw)
  To: Luka Gejak
  Cc: Ping-Ke Shih, Bitterblue Smith, linux-wireless, linux-kernel,
	mehmet.fide

Hi Luka,

On 2026-10-05 Luka Gejak wrote:
> so an association during the countdown moves the count a second time in
> the same interval. Can this download skip the page while a switch is
> announced, the way set_key does?

Yes. v4 skips the download and the H2C on association while a switch is
announced; the next countdown download carries the pages of the new
station. Of the remaining callers, the WoWLAN ones run after the
countdown work is cancelled and the beacon change is the countdown
itself.

> so the AP leaves the channel during the countdown anyway. Can the check
> move above the feature test?

Yes, v4 takes the mutex first and refuses the scan before the offload
test and the scanning flag, so firmware without scan offload gets -EBUSY
as well instead of a software scan.

Both verified on an RTL8821CU, whose firmware has no scan offload: a
scan during a countdown returned 32 BSSs with v3 and -EBUSY with v4,
and an association that completes inside a count-2 countdown hits the
mac80211 warning about the counter reaching 0 with v3, not with v4.

Best regards,
Mehmet

^ permalink raw reply	[flat|nested] 12+ messages in thread

* Re: [PATCH rtw-next v3 1/2] wifi: rtw88: download the beacon the reserved page was built with
  2026-10-05 17:36   ` Luka Gejak
@ 2026-10-06  7:46     ` Mehmet Fide
  0 siblings, 0 replies; 12+ messages in thread
From: Mehmet Fide @ 2026-10-06  7:46 UTC (permalink / raw)
  To: Luka Gejak
  Cc: Ping-Ke Shih, Bitterblue Smith, linux-wireless, linux-kernel,
	mehmet.fide

Hi Luka,

On 2026-10-05 Luka Gejak wrote:
> Reviewed-by: Luka Gejak <luka.gejak@linux.dev>

Thank you, v4 carries it.

Best regards,
Mehmet

^ permalink raw reply	[flat|nested] 12+ messages in thread

* RE: [PATCH rtw-next v3 2/2] wifi: rtw88: support channel switch in AP mode
  2026-10-06  7:46     ` Mehmet Fide
@ 2026-10-06  7:54       ` Ping-Ke Shih
  2026-10-06  8:16         ` Mehmet Fide
  0 siblings, 1 reply; 12+ messages in thread
From: Ping-Ke Shih @ 2026-10-06  7:54 UTC (permalink / raw)
  To: Mehmet Fide
  Cc: Luka Gejak, Bitterblue Smith, linux-wireless, linux-kernel, mehmet.fide

Mehmet Fide <mehmet.fide@gmail.com> wrote:
> > Will it be a problem just unconditionally initializing csa work?
> 
> It was: a hardware restart replays add_interface() with the work still
> armed, and initialising it again would lose the pending timer. With the
> work in rtw_dev the question does not arise any more.

Can we stop the work properly before hardware restart?
I just don't want many flags (conditions). 

Ping-Ke


^ permalink raw reply	[flat|nested] 12+ messages in thread

* Re: [PATCH rtw-next v3 2/2] wifi: rtw88: support channel switch in AP mode
  2026-10-06  7:54       ` Ping-Ke Shih
@ 2026-10-06  8:16         ` Mehmet Fide
  0 siblings, 0 replies; 12+ messages in thread
From: Mehmet Fide @ 2026-10-06  8:16 UTC (permalink / raw)
  To: Ping-Ke Shih
  Cc: Luka Gejak, Bitterblue Smith, linux-wireless, linux-kernel, mehmet.fide

Hi Ping-Ke,

On 2026-10-06 Ping-Ke Shih wrote:
> Can we stop the work properly before hardware restart?
> I just don't want many flags (conditions).

Yes. v4 cancels the countdown in rtw_fw_recovery_work(), under the
wiphy lock, before ieee80211_restart_hw(); the other stops are the AP
stop, the interface removal and the WoWLAN suspend. The work then
tests nothing but csa_active and the countdown itself; the running
flag check is gone. After the restart mac80211 reports the beacon
again and, if the switch is still announced, the countdown is armed
again from there.

Best regards,
Mehmet

^ permalink raw reply	[flat|nested] 12+ messages in thread

* [PATCH rtw-next v3 2/2] wifi: rtw88: support channel switch in AP mode
  2026-10-05 14:33 [PATCH rtw-next v3 0/2] wifi: rtw88: " Mehmet Fide
@ 2026-10-05 14:33 ` Mehmet Fide
  0 siblings, 0 replies; 12+ messages in thread
From: Mehmet Fide @ 2026-10-05 14:33 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 interface; the wiphy lock
serializes it with the mac80211 state it reads and the driver mutex
with the page build. It is cancelled when the AP stops, when the
interface goes away and on WoWLAN suspend, and a hardware restart,
which replays add_interface with the work still armed, does not
initialise it again. The other page downloads
that can run during a countdown, the TIM update and the PG backup on
set_key, are skipped then, since every beacon fetch advances the
countdown. A hardware scan is refused while a switch is announced: 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       | 59 +++++++++++++++++++
 drivers/net/wireless/realtek/rtw88/fw.h       |  2 +
 drivers/net/wireless/realtek/rtw88/mac80211.c | 55 ++++++++++++++++-
 drivers/net/wireless/realtek/rtw88/main.c     |  3 +-
 drivers/net/wireless/realtek/rtw88/main.h     |  2 +
 5 files changed, 119 insertions(+), 2 deletions(-)

diff --git a/drivers/net/wireless/realtek/rtw88/fw.c b/drivers/net/wireless/realtek/rtw88/fw.c
index 3cd17a3bb494..5e68ba73548a 100644
--- a/drivers/net/wireless/realtek/rtw88/fw.c
+++ b/drivers/net/wireless/realtek/rtw88/fw.c
@@ -1802,14 +1802,73 @@ int rtw_fw_download_rsvd_page(struct rtw_dev *rtwdev)
 	return ret;
 }
 
+static struct ieee80211_vif *rtw_fw_beacon_vif(struct rtw_dev *rtwdev)
+{
+	struct rtw_rsvd_page *rsvd_pkt;
+
+	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)
+		return NULL;
+
+	return rtwvif_to_vif(rsvd_pkt->rtwvif);
+}
+
+bool rtw_fw_csa_active(struct rtw_dev *rtwdev)
+{
+	struct ieee80211_vif *vif = rtw_fw_beacon_vif(rtwdev);
+
+	return vif && vif->bss_conf.csa_active;
+}
+
 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);
+
+	if (rtw_fw_csa_active(rtwdev))
+		goto out;
+
 	rtw_fw_download_rsvd_page(rtwdev);
 	rtw_send_rsvd_page_h2c(rtwdev);
+
+out:
+	mutex_unlock(&rtwdev->mutex);
+}
+
+void rtw_fw_csa_beacon_work(struct wiphy *wiphy, struct wiphy_work *work)
+{
+	struct rtw_vif *rtwvif = container_of(work, struct rtw_vif,
+					      csa_beacon_work.work);
+	struct rtw_dev *rtwdev = wiphy_to_ieee80211_hw(wiphy)->priv;
+	struct ieee80211_vif *vif = rtwvif_to_vif(rtwvif);
+	unsigned int delay;
+
+	lockdep_assert_wiphy(wiphy);
+
+	mutex_lock(&rtwdev->mutex);
+
+	if (!test_bit(RTW_FLAG_RUNNING, rtwdev->flags))
+		goto out;
+
+	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, &rtwvif->csa_beacon_work,
+					 usecs_to_jiffies(delay));
+	} 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..a0a1dcbf93de 100644
--- a/drivers/net/wireless/realtek/rtw88/fw.h
+++ b/drivers/net/wireless/realtek/rtw88/fw.h
@@ -863,7 +863,9 @@ 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_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..4c5be1bd9875 100644
--- a/drivers/net/wireless/realtek/rtw88/mac80211.c
+++ b/drivers/net/wireless/realtek/rtw88/mac80211.c
@@ -164,6 +164,9 @@ static int rtw_ops_add_interface(struct ieee80211_hw *hw,
 	memset(&rtwvif->bfee, 0, sizeof(struct rtw_bfee));
 	rtw_txq_init(rtwdev, vif->txq);
 	INIT_LIST_HEAD(&rtwvif->rsvd_page_list);
+	if (!test_bit(RTW_FLAG_RESTARTING, rtwdev->flags))
+		wiphy_delayed_work_init(&rtwvif->csa_beacon_work,
+					rtw_fw_csa_beacon_work);
 
 	mutex_lock(&rtwdev->mutex);
 
@@ -235,6 +238,8 @@ 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);
 
+	wiphy_delayed_work_cancel(hw->wiphy, &rtwvif->csa_beacon_work);
+
 	mutex_lock(&rtwdev->mutex);
 
 	rtw_leave_lps_deep(rtwdev);
@@ -438,6 +443,13 @@ 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);
+		if (conf->csa_active) {
+			u32 interval = ieee80211_tu_to_usec(conf->beacon_int);
+
+			wiphy_delayed_work_queue(hw->wiphy,
+						 &rtwvif->csa_beacon_work,
+						 usecs_to_jiffies(interval));
+		}
 	}
 
 	if (changed & BSS_CHANGED_BEACON_ENABLED) {
@@ -487,8 +499,11 @@ static void rtw_ops_stop_ap(struct ieee80211_hw *hw,
 			    struct ieee80211_vif *vif,
 			    struct ieee80211_bss_conf *link_conf)
 {
+	struct rtw_vif *rtwvif = (struct rtw_vif *)vif->drv_priv;
 	struct rtw_dev *rtwdev = hw->priv;
 
+	wiphy_delayed_work_cancel(hw->wiphy, &rtwvif->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 +571,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)
+{
+	u32 interval = ieee80211_tu_to_usec(vif->bss_conf.beacon_int);
+	struct rtw_vif *rtwvif = (struct rtw_vif *)vif->drv_priv;
+
+	wiphy_delayed_work_queue(hw->wiphy, &rtwvif->csa_beacon_work,
+				 usecs_to_jiffies(interval));
+}
+
 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 +652,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:
@@ -839,6 +866,14 @@ static int rtw_ops_get_antenna(struct ieee80211_hw *hw,
 }
 
 #ifdef CONFIG_PM
+static void rtw_csa_cancel_iter(void *data, struct ieee80211_vif *vif)
+{
+	struct rtw_vif *rtwvif = (struct rtw_vif *)vif->drv_priv;
+	struct rtw_dev *rtwdev = data;
+
+	wiphy_delayed_work_cancel(rtwdev->hw->wiphy, &rtwvif->csa_beacon_work);
+}
+
 static int rtw_ops_suspend(struct ieee80211_hw *hw,
 			   struct cfg80211_wowlan *wowlan)
 {
@@ -846,6 +881,7 @@ static int rtw_ops_suspend(struct ieee80211_hw *hw,
 	int ret;
 
 	mutex_lock(&rtwdev->mutex);
+	rtw_iterate_vifs(rtwdev, rtw_csa_cancel_iter, rtwdev);
 	ret = rtw_wow_suspend(rtwdev, wowlan);
 	if (ret)
 		rtw_err(rtwdev, "failed to suspend for wow %d\n", ret);
@@ -887,10 +923,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 +945,13 @@ static int rtw_ops_hw_scan(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
 		return -EBUSY;
 
 	mutex_lock(&rtwdev->mutex);
+
+	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 +1025,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..9f3da59b208f 100644
--- a/drivers/net/wireless/realtek/rtw88/main.c
+++ b/drivers/net/wireless/realtek/rtw88/main.c
@@ -2293,7 +2293,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..9ab8fbbe37c6 100644
--- a/drivers/net/wireless/realtek/rtw88/main.h
+++ b/drivers/net/wireless/realtek/rtw88/main.h
@@ -839,6 +839,8 @@ struct rtw_vif {
 	struct rtw_traffic_stats stats;
 
 	struct rtw_bfee bfee;
+
+	struct wiphy_delayed_work csa_beacon_work;
 };
 
 struct rtw_regulatory {
-- 
2.55.0


^ permalink raw reply	[flat|nested] 12+ messages in thread

end of thread, other threads:[~2026-10-06  8:16 UTC | newest]

Thread overview: 12+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-05 14:35 [PATCH rtw-next v3 0/2] wifi: rtw88: channel switch in AP mode Mehmet Fide
2026-10-05 14:35 ` [PATCH rtw-next v3 1/2] wifi: rtw88: download the beacon the reserved page was built with Mehmet Fide
2026-10-05 17:36   ` Luka Gejak
2026-10-06  7:46     ` Mehmet Fide
2026-10-05 14:35 ` [PATCH rtw-next v3 2/2] wifi: rtw88: support channel switch in AP mode Mehmet Fide
2026-10-05 17:33   ` [PATCH " Luka Gejak
2026-10-06  7:46     ` [PATCH rtw-next " Mehmet Fide
2026-10-06  1:26   ` Ping-Ke Shih
2026-10-06  7:46     ` Mehmet Fide
2026-10-06  7:54       ` Ping-Ke Shih
2026-10-06  8:16         ` Mehmet Fide
  -- strict thread matches above, loose matches on Subject: below --
2026-10-05 14:33 [PATCH rtw-next v3 0/2] wifi: rtw88: " Mehmet Fide
2026-10-05 14:33 ` [PATCH rtw-next v3 2/2] wifi: rtw88: support " 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®