[PATCH rtw-next v5 3/3] wifi: rtw88: support channel switch in AP mode

From: Mehmet Fide

Date: Thu Oct 08 2026 - 02:25:31 EST


From: Mehmet Fide <mehmet.fide@xxxxxxxxxxxxxxxxxx>

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 and csa_vif names the
interface it counts down for; the wiphy lock serializes the work with
the mac80211 state it reads and the driver mutex with the page build.
The countdown is cancelled when the AP stops, when the interface goes
away, on WoWLAN suspend and before a firmware recovery restarts the
hardware. The other page downloads that can run
during a countdown, the TIM update, the PG backup on set_key and the
one on association, are skipped then, since every beacon fetch
advances the countdown; the next countdown download, at most one
beacon interval later, carries their changes. A scan is refused while
a switch is announced, before the fallback to a software scan: it
would take the AP off the channel its stations count down to.

Signed-off-by: Mehmet Fide <mehmet.fide@xxxxxxxxxxxxxxxxxx>
---
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 | 65 ++++++++++++++++++-
drivers/net/wireless/realtek/rtw88/fw.h | 4 ++
drivers/net/wireless/realtek/rtw88/mac80211.c | 34 ++++++++--
drivers/net/wireless/realtek/rtw88/main.c | 9 ++-
drivers/net/wireless/realtek/rtw88/main.h | 2 +
5 files changed, 107 insertions(+), 7 deletions(-)

diff --git a/drivers/net/wireless/realtek/rtw88/fw.c b/drivers/net/wireless/realtek/rtw88/fw.c
index e01d79a70e37..16319233c9cc 100644
--- a/drivers/net/wireless/realtek/rtw88/fw.c
+++ b/drivers/net/wireless/realtek/rtw88/fw.c
@@ -1802,6 +1802,35 @@ int rtw_fw_download_rsvd_page(struct rtw_dev *rtwdev)
return ret;
}

+bool rtw_fw_csa_active(struct rtw_dev *rtwdev)
+{
+ lockdep_assert_wiphy(rtwdev->hw->wiphy);
+
+ return rtwdev->csa_vif && rtwdev->csa_vif->bss_conf.csa_active;
+}
+
+void rtw_fw_csa_start(struct rtw_dev *rtwdev, struct ieee80211_vif *vif)
+{
+ u32 interval = ieee80211_tu_to_usec(vif->bss_conf.beacon_int);
+
+ lockdep_assert_wiphy(rtwdev->hw->wiphy);
+
+ rtwdev->csa_vif = vif;
+ wiphy_delayed_work_queue(rtwdev->hw->wiphy, &rtwdev->csa_beacon_work,
+ usecs_to_jiffies(interval));
+}
+
+void rtw_fw_csa_stop(struct rtw_dev *rtwdev, struct ieee80211_vif *vif)
+{
+ lockdep_assert_wiphy(rtwdev->hw->wiphy);
+
+ if (!vif || rtwdev->csa_vif != vif)
+ return;
+
+ wiphy_delayed_work_cancel(rtwdev->hw->wiphy, &rtwdev->csa_beacon_work);
+ rtwdev->csa_vif = NULL;
+}
+
void rtw_fw_update_beacon_work(struct wiphy *wiphy, struct wiphy_work *work)
{
struct rtw_dev *rtwdev = container_of(work, struct rtw_dev,
@@ -1811,7 +1840,8 @@ void rtw_fw_update_beacon_work(struct wiphy *wiphy, struct wiphy_work *work)

mutex_lock(&rtwdev->mutex);

- if (!test_bit(RTW_FLAG_RUNNING, rtwdev->flags))
+ if (!test_bit(RTW_FLAG_RUNNING, rtwdev->flags) ||
+ rtw_fw_csa_active(rtwdev))
goto out;

rtw_fw_download_rsvd_page(rtwdev);
@@ -1821,6 +1851,39 @@ void rtw_fw_update_beacon_work(struct wiphy *wiphy, struct wiphy_work *work)
mutex_unlock(&rtwdev->mutex);
}

+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 ieee80211_vif *vif = rtwdev->csa_vif;
+ unsigned int delay;
+
+ lockdep_assert_wiphy(wiphy);
+
+ mutex_lock(&rtwdev->mutex);
+
+ if (!vif->bss_conf.csa_active) {
+ rtwdev->csa_vif = NULL;
+ 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 {
+ rtwdev->csa_vif = NULL;
+ 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 d433f454094d..7277af739d9e 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 wiphy *wiphy, struct wiphy_work *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 7fcf91bfe761..d99564353a0b 100644
--- a/drivers/net/wireless/realtek/rtw88/mac80211.c
+++ b/drivers/net/wireless/realtek/rtw88/mac80211.c
@@ -235,6 +235,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);

+ rtw_fw_csa_stop(rtwdev, vif);
+
mutex_lock(&rtwdev->mutex);

rtw_leave_lps_deep(rtwdev);
@@ -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 of the STA+AP combination: keep the AP's countdown */
+ 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,8 @@ 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)
+ rtw_fw_csa_start(rtwdev, vif);
}

if (changed & BSS_CHANGED_BEACON_ENABLED) {
@@ -489,6 +496,8 @@ static void rtw_ops_stop_ap(struct ieee80211_hw *hw,
{
struct rtw_dev *rtwdev = hw->priv;

+ rtw_fw_csa_stop(rtwdev, vif);
+
mutex_lock(&rtwdev->mutex);
rtw_write32_clr(rtwdev, REG_TCR, BIT_TCR_UPDATE_HGQMD);
rtw_write16(rtwdev, REG_ATIMWND, ATIMWND_DEFAULT);
@@ -556,6 +565,15 @@ 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;
+
+ rtw_fw_csa_start(rtwdev, vif);
+}
+
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 +644,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 +865,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 +913,23 @@ 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);
+
+ 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 +996,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 647393af9fd9..e3bbfbc9d976 100644
--- a/drivers/net/wireless/realtek/rtw88/main.c
+++ b/drivers/net/wireless/realtek/rtw88/main.c
@@ -676,6 +676,10 @@ static void rtw_fw_recovery_work(struct work_struct *work)
struct rtw_dev *rtwdev = container_of(work, struct rtw_dev,
fw_recovery_work);

+ wiphy_lock(rtwdev->hw->wiphy);
+ rtw_fw_csa_stop(rtwdev, rtwdev->csa_vif);
+ wiphy_unlock(rtwdev->hw->wiphy);
+
mutex_lock(&rtwdev->mutex);
__fw_recovery_work(rtwdev);
mutex_unlock(&rtwdev->mutex);
@@ -2170,6 +2174,8 @@ int rtw_core_init(struct rtw_dev *rtwdev)
INIT_WORK(&rtwdev->fw_recovery_work, rtw_fw_recovery_work);
wiphy_work_init(&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 +2299,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 bb48529759c3..a39416272cff 100644
--- a/drivers/net/wireless/realtek/rtw88/main.h
+++ b/drivers/net/wireless/realtek/rtw88/main.h
@@ -2094,6 +2094,8 @@ struct rtw_dev {
struct work_struct ips_work;
struct work_struct fw_recovery_work;
struct wiphy_work update_beacon_work;
+ struct wiphy_delayed_work csa_beacon_work;
+ struct ieee80211_vif *csa_vif;

/* used to protect txqs list */
spinlock_t txq_lock;
--
2.55.0