* [PATCH mt76-next 02/27] wifi: mt76: mt7996: enable the SKU power limit table
2026-10-06 20:59 [PATCH mt76-next 01/27] wifi: mt76: abort a scan only for a link change on the scanning radio Felix Fietkau
@ 2026-10-06 20:59 ` Felix Fietkau
2026-10-06 20:59 ` [PATCH mt76-next 03/27] wifi: mt76: mt7996: set sniffer mode on a radio that is already running Felix Fietkau
` (24 subsequent siblings)
25 siblings, 0 replies; 27+ messages in thread
From: Felix Fietkau @ 2026-10-06 20:59 UTC (permalink / raw)
To: linux-wireless
mt7996_mcu_set_txpower_sku() uploads the per-rate power limit table, but
the firmware applies it only after the driver enables the SKU power
limit. mt7915 does that in mt7915_mcu_set_sku_en(); mt7996 never does,
so the radio keeps the EEPROM target power and the configured tx power
has no effect.
Add the power control command and enable the table per band in
mt7996_run(), as the MediaTek vendor driver does.
The backoff table stays disabled. This driver does not build the
per-path limits; it pads that part of the message with zeros.
Fixes: f75e4779d215 ("wifi: mt76: mt7996: add txpower setting support")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
.../net/wireless/mediatek/mt76/mt7996/main.c | 5 +++
.../net/wireless/mediatek/mt76/mt7996/mcu.c | 34 +++++++++++++++++++
.../net/wireless/mediatek/mt76/mt7996/mcu.h | 23 +++++++++++++
.../wireless/mediatek/mt76/mt7996/mt7996.h | 1 +
4 files changed, 63 insertions(+)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index d17d1dbb2038..c04e980f6ee7 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -34,6 +34,11 @@ int mt7996_run(struct mt7996_phy *phy)
if (ret)
return ret;
+ ret = mt7996_mcu_set_tx_power_ctrl(phy, UNI_TXPOWER_SKU_POWER_LIMIT_CTRL,
+ true);
+ if (ret)
+ return ret;
+
set_bit(MT76_STATE_RUNNING, &phy->mt76->state);
ieee80211_queue_delayed_work(dev->mphy.hw, &phy->mt76->mac_work,
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
index a534d07ea1e8..310ce18ec12c 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
@@ -5488,6 +5488,40 @@ int mt7996_mcu_set_sniffer_mode(struct mt7996_phy *phy, bool enabled)
sizeof(req), true);
}
+int mt7996_mcu_set_tx_power_ctrl(struct mt7996_phy *phy, u8 power_ctrl_id, u8 data)
+{
+ struct mt7996_dev *dev = phy->dev;
+ struct tx_power_ctrl req = {
+ .tag = cpu_to_le16(power_ctrl_id),
+ .len = cpu_to_le16(sizeof(req) - 4),
+ .power_ctrl_id = power_ctrl_id,
+ .band_idx = phy->mt76->band_idx,
+ };
+
+ switch (power_ctrl_id) {
+ case UNI_TXPOWER_SKU_POWER_LIMIT_CTRL:
+ req.sku_enable = !!data;
+ break;
+ case UNI_TXPOWER_PERCENTAGE_CTRL:
+ req.percentage_ctrl_enable = !!data;
+ break;
+ case UNI_TXPOWER_PERCENTAGE_DROP_CTRL:
+ req.power_drop_level = data;
+ break;
+ case UNI_TXPOWER_BACKOFF_POWER_LIMIT_CTRL:
+ req.bf_backoff_enable = !!data;
+ break;
+ case UNI_TXPOWER_ATE_MODE_CTRL:
+ req.ate_mode_enable = !!data;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return mt76_mcu_send_msg(&dev->mt76, MCU_WM_UNI_CMD(TXPOWER),
+ &req, sizeof(req), false);
+}
+
int mt7996_mcu_set_txpower_sku(struct mt7996_phy *phy)
{
#define TX_POWER_LIMIT_TABLE_RATE 0
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.h b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.h
index 74b70fb6da3d..87573688171b 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.h
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.h
@@ -1036,8 +1036,31 @@ enum {
UNI_CMD_THERMAL_PROTECT_DUTY_CONFIG,
};
+struct tx_power_ctrl {
+ u8 _rsv[4];
+
+ __le16 tag;
+ __le16 len;
+
+ u8 power_ctrl_id;
+ union {
+ bool sku_enable;
+ bool ate_mode_enable;
+ bool percentage_ctrl_enable;
+ bool bf_backoff_enable;
+ u8 power_drop_level;
+ };
+ u8 band_idx;
+ u8 rsv[1];
+} __packed;
+
enum {
+ UNI_TXPOWER_SKU_POWER_LIMIT_CTRL = 0,
+ UNI_TXPOWER_PERCENTAGE_CTRL = 1,
+ UNI_TXPOWER_PERCENTAGE_DROP_CTRL = 2,
+ UNI_TXPOWER_BACKOFF_POWER_LIMIT_CTRL = 3,
UNI_TXPOWER_POWER_LIMIT_TABLE_CTRL = 4,
+ UNI_TXPOWER_ATE_MODE_CTRL = 6,
};
enum {
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
index 2a0cdb56f822..dcfdcd5975c3 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
@@ -778,6 +778,7 @@ int mt7996_mcu_get_temperature(struct mt7996_phy *phy);
int mt7996_mcu_set_thermal_throttling(struct mt7996_phy *phy, u8 state);
int mt7996_mcu_set_thermal_protect(struct mt7996_phy *phy, bool enable);
int mt7996_mcu_set_txpower_sku(struct mt7996_phy *phy);
+int mt7996_mcu_set_tx_power_ctrl(struct mt7996_phy *phy, u8 power_ctrl_id, u8 data);
int mt7996_mcu_rdd_resume_tx(struct mt7996_phy *phy);
int mt7996_mcu_rdd_cmd(struct mt7996_dev *dev, int cmd, u8 rdd_idx, u8 val);
int mt7996_mcu_rdd_background_enable(struct mt7996_phy *phy,
--
2.47.3
^ permalink raw reply related [flat|nested] 27+ messages in thread* [PATCH mt76-next 03/27] wifi: mt76: mt7996: set sniffer mode on a radio that is already running
2026-10-06 20:59 [PATCH mt76-next 01/27] wifi: mt76: abort a scan only for a link change on the scanning radio Felix Fietkau
2026-10-06 20:59 ` [PATCH mt76-next 02/27] wifi: mt76: mt7996: enable the SKU power limit table Felix Fietkau
@ 2026-10-06 20:59 ` Felix Fietkau
2026-10-06 20:59 ` [PATCH mt76-next 04/27] wifi: mt76: mt7996: update the address of an AP link that is added again Felix Fietkau
` (23 subsequent siblings)
25 siblings, 0 replies; 27+ messages in thread
From: Felix Fietkau @ 2026-10-06 20:59 UTC (permalink / raw)
To: linux-wireless
mt7996_add_interface() skips a radio that has MT76_STATE_RUNNING set,
and the skip covers both the start of the radio and the call to
mt7996_set_monitor(). mt7996_init_wiphy_band() sets
MT_WF_RFCR_DROP_OTHER_UC in phy->rxfilter on every band, and
mt7996_configure_filter() does not clear it, so only
mt7996_set_monitor() does. A monitor interface that is added while an
AP runs on the same radio thus receives no unicast frame addressed to
another device.
Start only an idle radio, but put every radio of a monitor interface
into sniffer mode. mt7996_remove_interface() already clears sniffer
mode whether the radio runs or not.
Fixes: 69d54ce7491d ("wifi: mt76: mt7996: switch to single multi-radio wiphy")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
drivers/net/wireless/mediatek/mt76/mt7996/main.c | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index c04e980f6ee7..d013a472c28d 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -570,13 +570,14 @@ static int mt7996_add_interface(struct ieee80211_hw *hw,
for (i = 0; i < MT7996_MAX_RADIOS; i++) {
struct mt7996_phy *phy = dev->radio_phy[i];
- if (!phy || !(wdev->radio_mask & BIT(i)) ||
- test_bit(MT76_STATE_RUNNING, &phy->mt76->state))
+ if (!phy || !(wdev->radio_mask & BIT(i)))
continue;
- err = mt7996_run(phy);
- if (err)
- goto out;
+ if (!test_bit(MT76_STATE_RUNNING, &phy->mt76->state)) {
+ err = mt7996_run(phy);
+ if (err)
+ goto out;
+ }
if (vif->type == NL80211_IFTYPE_MONITOR)
mt7996_set_monitor(phy, true);
--
2.47.3
^ permalink raw reply related [flat|nested] 27+ messages in thread* [PATCH mt76-next 04/27] wifi: mt76: mt7996: update the address of an AP link that is added again
2026-10-06 20:59 [PATCH mt76-next 01/27] wifi: mt76: abort a scan only for a link change on the scanning radio Felix Fietkau
2026-10-06 20:59 ` [PATCH mt76-next 02/27] wifi: mt76: mt7996: enable the SKU power limit table Felix Fietkau
2026-10-06 20:59 ` [PATCH mt76-next 03/27] wifi: mt76: mt7996: set sniffer mode on a radio that is already running Felix Fietkau
@ 2026-10-06 20:59 ` Felix Fietkau
2026-10-06 20:59 ` [PATCH mt76-next 05/27] wifi: mt76: mt7996: size the MLD setup TLV by the links it describes Felix Fietkau
` (22 subsequent siblings)
25 siblings, 0 replies; 27+ messages in thread
From: Felix Fietkau @ 2026-10-06 20:59 UTC (permalink / raw)
To: linux-wireless
mt7996_vif_link_remove() only stops an AP link and keeps its BSS, so a
link that is added again takes the early branch of mt7996_vif_link_add().
For other interface types that branch sends DEV_INFO with the new link
address. For an AP it only restarts the link, and the firmware keeps
the own MAC address the link had when it was first created.
The hardware takes A2 of an encapsulation offload frame from that
address. A reload that gives an AP MLD link a new address therefore
makes every data frame on that link carry the old address as TA. The
receiver acknowledges the frame by its RA but cannot find a key for the
TA and drops it, so the AP counts no failure. The BSS_INFO update on
BSS_CHANGED_BEACON_ENABLED restores the BSSID, but nothing sends
DEV_INFO again.
Send DEV_INFO for an AP link as well, before the link is restarted.
Fixes: 08813703ac41 ("wifi: mt76: mt7996: Destroy vif active links in mt7996_remove_interface()")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
drivers/net/wireless/mediatek/mt76/mt7996/main.c | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index d013a472c28d..621e1337dec2 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -312,15 +312,15 @@ int mt7996_vif_link_add(struct mt76_phy *mphy, struct ieee80211_vif *vif,
if ((mvif->mt76.valid_links & BIT(link_conf->link_id)) &&
!mlink->offchannel) {
- if (vif->type == NL80211_IFTYPE_AP)
- return mt7996_mcu_mld_link_oper(dev, link_conf, link,
- true);
-
/* update the link address */
ret = mt7996_mcu_add_dev_info(phy, vif, link_conf, mlink, true);
if (ret)
return ret;
+ if (vif->type == NL80211_IFTYPE_AP)
+ return mt7996_mcu_mld_link_oper(dev, link_conf, link,
+ true);
+
return mt7996_mcu_add_bss_info(phy, vif, link_conf, mlink,
msta_link, true);
}
--
2.47.3
^ permalink raw reply related [flat|nested] 27+ messages in thread* [PATCH mt76-next 05/27] wifi: mt76: mt7996: size the MLD setup TLV by the links it describes
2026-10-06 20:59 [PATCH mt76-next 01/27] wifi: mt76: abort a scan only for a link change on the scanning radio Felix Fietkau
` (2 preceding siblings ...)
2026-10-06 20:59 ` [PATCH mt76-next 04/27] wifi: mt76: mt7996: update the address of an AP link that is added again Felix Fietkau
@ 2026-10-06 20:59 ` Felix Fietkau
2026-10-06 20:59 ` [PATCH mt76-next 06/27] wifi: mt76: mt7996: find the last MLD link among the links torn down Felix Fietkau
` (21 subsequent siblings)
25 siblings, 0 replies; 27+ messages in thread
From: Felix Fietkau @ 2026-10-06 20:59 UTC (permalink / raw)
To: linux-wireless
mt7996_mcu_sta_mld_setup_tlv() sets link_num and the TLV length from
hweight16(sta->valid_links), but fills an entry only for active links
that have driver state. On a station interface mac80211 activates only
the association link at first, and drv_change_sta_links() masks the
other links with vif->active_links, so the driver has no state for
them. A station with valid_links 0x3 and active_links 0x2 thus sends
STA_REC_MLD with link_num 2 and a second entry of zeroes, at
association and again at authorization. The zeroed entry names WTBL
index 0 and BSS index 0 as a link of the MLD.
Count the links with driver state first and describe exactly those,
with the secondary id taken from them. A link removed without flush
keeps its mt7996_sta_link but is no longer valid, so leave it out as
well.
Fixes: 00cef41d9d8f ("wifi: mt76: mt7996: Add mt7996_mcu_sta_mld_setup_tlv() and mt7996_mcu_sta_eht_mld_tlv()")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
.../net/wireless/mediatek/mt76/mt7996/main.c | 30 +++++++++++++++++++
.../net/wireless/mediatek/mt76/mt7996/mcu.c | 26 +++++++---------
.../wireless/mediatek/mt76/mt7996/mt7996.h | 2 ++
3 files changed, 42 insertions(+), 16 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index 621e1337dec2..0c229a8f071f 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -1262,6 +1262,36 @@ void mt7996_mac_sta_remove_link(struct mt7996_dev *dev,
}
}
+u16 mt7996_mac_sta_links(struct mt7996_dev *dev, struct ieee80211_vif *vif,
+ struct ieee80211_sta *sta)
+{
+ struct mt7996_sta *msta = (struct mt7996_sta *)sta->drv_priv;
+ struct ieee80211_link_sta *link_sta;
+ unsigned int link_id;
+ u16 links = 0;
+
+ for_each_sta_active_link(vif, sta, link_sta, link_id) {
+ struct mt7996_sta_link *msta_link;
+
+ msta_link = mt7996_sta_link_protected(dev, msta, link_id);
+ if (!msta_link)
+ continue;
+
+ if (sta->valid_links && !msta_link->wcid.link_valid)
+ continue;
+
+ if (!link_conf_dereference_protected(vif, link_id))
+ continue;
+
+ if (!mt7996_vif_link(dev, vif, link_id))
+ continue;
+
+ links |= BIT(link_id);
+ }
+
+ return links;
+}
+
static void
mt7996_mac_sta_remove_links(struct mt7996_dev *dev, struct ieee80211_vif *vif,
struct ieee80211_sta *sta, unsigned long links,
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
index 310ce18ec12c..02f9bcfdf5d9 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
@@ -2759,18 +2759,20 @@ mt7996_mcu_sta_mld_setup_tlv(struct mt7996_dev *dev, struct sk_buff *skb,
struct ieee80211_sta *sta)
{
struct mt7996_sta *msta = (struct mt7996_sta *)sta->drv_priv;
- unsigned int nlinks = hweight16(sta->valid_links);
+ unsigned long links = mt7996_mac_sta_links(dev, vif, sta);
+ unsigned int nlinks = hweight16(links);
struct mld_setup_link *mld_setup_link;
- struct ieee80211_link_sta *link_sta;
struct sta_rec_mld_setup *mld_setup;
struct mt7996_sta_link *msta_link;
+ u8 seclink_id = msta->deflink_id;
unsigned int link_id;
struct tlv *tlv;
- msta_link = mt7996_sta_link_protected(dev, msta, msta->deflink_id);
- if (!msta_link)
+ if (!(links & BIT(msta->deflink_id)))
return;
+ msta_link = mt7996_sta_link_protected(dev, msta, msta->deflink_id);
+
tlv = mt76_connac_mcu_add_tlv(skb, STA_REC_MLD,
sizeof(struct sta_rec_mld_setup) +
sizeof(struct mld_setup_link) * nlinks);
@@ -2780,26 +2782,18 @@ mt7996_mcu_sta_mld_setup_tlv(struct mt7996_dev *dev, struct sk_buff *skb,
mld_setup->setup_wcid = cpu_to_le16(msta_link->wcid.idx);
mld_setup->primary_id = cpu_to_le16(msta_link->wcid.idx);
- if (nlinks > 1) {
- msta_link = mt7996_sta_link_protected(dev, msta,
- msta->seclink_id);
- if (!msta_link)
- return;
- }
+ if (links & BIT(msta->seclink_id))
+ seclink_id = msta->seclink_id;
+ msta_link = mt7996_sta_link_protected(dev, msta, seclink_id);
mld_setup->seconed_id = cpu_to_le16(msta_link->wcid.idx);
mld_setup->link_num = nlinks;
mld_setup_link = (struct mld_setup_link *)mld_setup->link_info;
- for_each_sta_active_link(vif, sta, link_sta, link_id) {
+ for_each_set_bit(link_id, &links, IEEE80211_MLD_MAX_NUM_LINKS) {
struct mt7996_vif_link *link;
msta_link = mt7996_sta_link_protected(dev, msta, link_id);
- if (!msta_link)
- continue;
-
link = mt7996_vif_link(dev, vif, link_id);
- if (!link)
- continue;
mld_setup_link->wcid = cpu_to_le16(msta_link->wcid.idx);
mld_setup_link->bss_idx = link->mt76.idx;
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
index dcfdcd5975c3..82a2a8a5db69 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
@@ -894,6 +894,8 @@ void mt7996_mac_twt_teardown_flow(struct mt7996_dev *dev,
void mt7996_mac_sta_remove_link(struct mt7996_dev *dev,
struct ieee80211_sta *sta,
unsigned int link_id, bool flush);
+u16 mt7996_mac_sta_links(struct mt7996_dev *dev, struct ieee80211_vif *vif,
+ struct ieee80211_sta *sta);
void mt7996_mac_add_twt_setup(struct ieee80211_hw *hw,
struct ieee80211_sta *sta,
struct ieee80211_twt_setup *twt);
--
2.47.3
^ permalink raw reply related [flat|nested] 27+ messages in thread* [PATCH mt76-next 06/27] wifi: mt76: mt7996: find the last MLD link among the links torn down
2026-10-06 20:59 [PATCH mt76-next 01/27] wifi: mt76: abort a scan only for a link change on the scanning radio Felix Fietkau
` (3 preceding siblings ...)
2026-10-06 20:59 ` [PATCH mt76-next 05/27] wifi: mt76: mt7996: size the MLD setup TLV by the links it describes Felix Fietkau
@ 2026-10-06 20:59 ` Felix Fietkau
2026-10-06 20:59 ` [PATCH mt76-next 07/27] wifi: mt76: mt7996: upload only the keys of the link being set up Felix Fietkau
` (20 subsequent siblings)
25 siblings, 0 replies; 27+ messages in thread
From: Felix Fietkau @ 2026-10-06 20:59 UTC (permalink / raw)
To: linux-wireless
On disassociation, mt7996_mac_sta_event() sends STA_REC_MLD_OFF when
the link it handles is the last of sta->valid_links, but it handles
only active links with driver state. On a station interface with one
active link out of two valid links, no link is ever the last one, so
the firmware never gets STA_REC_MLD_OFF for the MLD.
Walk the same set of links that mt7996_mcu_sta_mld_setup_tlv()
describes, and detect the last link within that set. The per-link
event handling moves into a helper that takes the set of links, so
that a later change can bring up links that became active after
association.
Fixes: c1d6dd5d03eb ("wifi: mt76: mt7996: Add mt7996_mcu_teardown_mld_sta rouine")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
.../net/wireless/mediatek/mt76/mt7996/main.c | 47 ++++++++++---------
1 file changed, 26 insertions(+), 21 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index 0c229a8f071f..e5f74f23d469 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -1406,34 +1406,26 @@ mt7996_mac_sta_add(struct mt7996_dev *dev, struct ieee80211_vif *vif,
}
static int
-mt7996_mac_sta_event(struct mt7996_dev *dev, struct ieee80211_vif *vif,
- struct ieee80211_sta *sta, enum mt76_sta_event ev)
+mt7996_mac_sta_links_event(struct mt7996_dev *dev, struct ieee80211_vif *vif,
+ struct ieee80211_sta *sta, unsigned long links,
+ enum mt76_sta_event ev)
{
struct mt7996_sta *msta = (struct mt7996_sta *)sta->drv_priv;
- unsigned long links = sta->valid_links;
- struct ieee80211_link_sta *link_sta;
+ unsigned long iter_links = links;
unsigned int link_id;
- int err = 0;
+ int err;
- mutex_lock(&dev->mt76.mutex);
-
- for_each_sta_active_link(vif, sta, link_sta, link_id) {
+ for_each_set_bit(link_id, &iter_links, IEEE80211_MLD_MAX_NUM_LINKS) {
struct ieee80211_bss_conf *link_conf;
+ struct ieee80211_link_sta *link_sta;
struct mt7996_sta_link *msta_link;
struct mt7996_vif_link *link;
int i;
link_conf = link_conf_dereference_protected(vif, link_id);
- if (!link_conf)
- continue;
-
- link = mt7996_vif_link(dev, vif, link_id);
- if (!link)
- continue;
-
+ link_sta = link_sta_dereference_protected(sta, link_id);
msta_link = mt7996_sta_link_protected(dev, msta, link_id);
- if (!msta_link)
- continue;
+ link = mt7996_vif_link(dev, vif, link_id);
switch (ev) {
case MT76_STA_EVENT_ASSOC:
@@ -1441,12 +1433,12 @@ mt7996_mac_sta_event(struct mt7996_dev *dev, struct ieee80211_vif *vif,
link, msta_link,
CONN_STATE_CONNECT, true);
if (err)
- goto unlock;
+ return err;
err = mt7996_mcu_add_rate_ctrl(dev, msta_link->sta, vif,
link_id, false);
if (err)
- goto unlock;
+ return err;
msta_link->wcid.tx_info |= MT_WCID_TX_INFO_SET;
break;
@@ -1455,7 +1447,7 @@ mt7996_mac_sta_event(struct mt7996_dev *dev, struct ieee80211_vif *vif,
link, msta_link,
CONN_STATE_PORT_SECURE, false);
if (err)
- goto unlock;
+ return err;
break;
case MT76_STA_EVENT_DISASSOC:
for (i = 0; i < ARRAY_SIZE(msta_link->twt.flow); i++)
@@ -1475,7 +1467,20 @@ mt7996_mac_sta_event(struct mt7996_dev *dev, struct ieee80211_vif *vif,
break;
}
}
-unlock:
+
+ return 0;
+}
+
+static int
+mt7996_mac_sta_event(struct mt7996_dev *dev, struct ieee80211_vif *vif,
+ struct ieee80211_sta *sta, enum mt76_sta_event ev)
+{
+ int err;
+
+ mutex_lock(&dev->mt76.mutex);
+ err = mt7996_mac_sta_links_event(dev, vif, sta,
+ mt7996_mac_sta_links(dev, vif, sta),
+ ev);
mutex_unlock(&dev->mt76.mutex);
return err;
--
2.47.3
^ permalink raw reply related [flat|nested] 27+ messages in thread* [PATCH mt76-next 07/27] wifi: mt76: mt7996: upload only the keys of the link being set up
2026-10-06 20:59 [PATCH mt76-next 01/27] wifi: mt76: abort a scan only for a link change on the scanning radio Felix Fietkau
` (4 preceding siblings ...)
2026-10-06 20:59 ` [PATCH mt76-next 06/27] wifi: mt76: mt7996: find the last MLD link among the links torn down Felix Fietkau
@ 2026-10-06 20:59 ` Felix Fietkau
2026-10-06 20:59 ` [PATCH mt76-next 08/27] wifi: mt76: mt7996: do not walk the vif keys when a link is destroyed Felix Fietkau
` (19 subsequent siblings)
25 siblings, 0 replies; 27+ messages in thread
From: Felix Fietkau @ 2026-10-06 20:59 UTC (permalink / raw)
To: linux-wireless
mt7996_vif_link_add() and mt7996_vif_link_destroy() walk the vif keys
through mt7996_key_iter(), which passes every group key to
mt7996_set_hw_key() for the link being set up. On an MLD interface each
group key belongs to one link, as key->link_id says, so a new link gets
the GTK and BIGTK of the other links written to its group WTBL entry,
and its hw_key_idx set to their key index.
Skip keys that belong to a different link, as mt7996_set_key() already
does for keys that mac80211 installs.
Fixes: eddc7286f6bb ("wifi: mt76: mt7996: fix key add/remove imbalance")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
drivers/net/wireless/mediatek/mt76/mt7996/main.c | 3 +++
1 file changed, 3 insertions(+)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index e5f74f23d469..e74254928578 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -291,6 +291,9 @@ mt7996_key_iter(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
if (sta)
return;
+ if (key->link_id >= 0 && key->link_id != it->link_id)
+ return;
+
WARN_ON(mt7996_set_hw_key(hw, it->cmd, vif, NULL, it->link_id, key));
}
--
2.47.3
^ permalink raw reply related [flat|nested] 27+ messages in thread* [PATCH mt76-next 08/27] wifi: mt76: mt7996: do not walk the vif keys when a link is destroyed
2026-10-06 20:59 [PATCH mt76-next 01/27] wifi: mt76: abort a scan only for a link change on the scanning radio Felix Fietkau
` (5 preceding siblings ...)
2026-10-06 20:59 ` [PATCH mt76-next 07/27] wifi: mt76: mt7996: upload only the keys of the link being set up Felix Fietkau
@ 2026-10-06 20:59 ` Felix Fietkau
2026-10-06 20:59 ` [PATCH mt76-next 09/27] wifi: mt76: mt7996: report station statistics from the primary link Felix Fietkau
` (18 subsequent siblings)
25 siblings, 0 replies; 27+ messages in thread
From: Felix Fietkau @ 2026-10-06 20:59 UTC (permalink / raw)
To: linux-wireless
Since commit eddc7286f6bb ("wifi: mt76: mt7996: fix key add/remove
imbalance"), mt7996_vif_link_destroy() walks the vif keys with SET_KEY.
Before that commit the walk used DISABLE_KEY. The commit message says a
key for a link is added and removed only once, but a SET_KEY walk on
destroy uploads the keys again, and for a BIGTK with beaconing enabled
it also removes and adds the beacon of the link being destroyed.
Remove the walk. mac80211 removes the keys itself with set_key() calls,
which is the one removal that commit describes.
The walk currently finds no keys: mt7996_vif_link_destroy() runs for a
non-offchannel link only from mt7996_remove_interface(), and
ieee80211_do_stop() frees the keys of the interface before
drv_remove_interface(). An interface type change goes through the same
path, since mt7996 has no .change_interface. Found by inspection.
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
drivers/net/wireless/mediatek/mt76/mt7996/main.c | 7 -------
1 file changed, 7 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index e74254928578..f94b5fdfca24 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -425,19 +425,12 @@ static void mt7996_vif_link_destroy(struct mt7996_phy *phy,
struct mt7996_sta_link *msta_link = &link->msta_link;
unsigned int link_id = msta_link->wcid.link_id;
struct mt76_vif_link *mlink = &link->mt76;
- struct mt7996_key_iter_data it = {
- .cmd = SET_KEY,
- .link_id = link_id,
- };
struct mt7996_dev *dev = phy->dev;
int idx = msta_link->wcid.idx;
if (!link_conf)
link_conf = &vif->bss_conf;
- if (!mlink->wcid->offchannel)
- ieee80211_iter_keys(phy->mt76->hw, vif, mt7996_key_iter, &it);
-
mt7996_mcu_add_sta(dev, link_conf, NULL, link, NULL,
CONN_STATE_DISCONNECT, false);
mt7996_mcu_add_bss_info(phy, vif, link_conf, mlink, msta_link, false);
--
2.47.3
^ permalink raw reply related [flat|nested] 27+ messages in thread* [PATCH mt76-next 09/27] wifi: mt76: mt7996: report station statistics from the primary link
2026-10-06 20:59 [PATCH mt76-next 01/27] wifi: mt76: abort a scan only for a link change on the scanning radio Felix Fietkau
` (6 preceding siblings ...)
2026-10-06 20:59 ` [PATCH mt76-next 08/27] wifi: mt76: mt7996: do not walk the vif keys when a link is destroyed Felix Fietkau
@ 2026-10-06 20:59 ` Felix Fietkau
2026-10-06 20:59 ` [PATCH mt76-next 10/27] wifi: mt76: mt7996: clear all link wcid entries in sta_pre_rcu_remove Felix Fietkau
` (17 subsequent siblings)
25 siblings, 0 replies; 27+ messages in thread
From: Felix Fietkau @ 2026-10-06 20:59 UTC (permalink / raw)
To: linux-wireless
mt7996_sta_statistics() and mt7996_ethtool_worker() read the TX rate,
the TX counters and the ACK signal from msta->deflink. That is the
msta_link of the first link of the station, not of its primary link.
When the first link of an MLD station is removed, msta->deflink keeps
the values of the removed link, and the station reports them until it
is removed.
Read them from the link that deflink_id selects.
msta->deflink also holds the RX reorder state, the TX aggregation
state and the def_wcid target of all links. These are state of the
whole station, and wcid_to_sta() needs def_wcid at the start of
drv_priv, so they stay there.
Fixes: f520eceacd24 ("wifi: mt76: mt7996: Add mt7996_mac_sta_change_links callback")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
.../net/wireless/mediatek/mt76/mt7996/main.c | 20 ++++++++++++++++---
1 file changed, 17 insertions(+), 3 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index f94b5fdfca24..f44af31281b7 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -1920,9 +1920,16 @@ static void mt7996_sta_statistics(struct ieee80211_hw *hw,
{
struct mt7996_dev *dev = mt7996_hw_dev(hw);
struct mt7996_sta *msta = (struct mt7996_sta *)sta->drv_priv;
- struct mt7996_sta_link *msta_link = &msta->deflink;
- struct rate_info *txrate = &msta_link->wcid.rate;
+ struct mt7996_sta_link *msta_link;
+ struct rate_info *txrate;
+ rcu_read_lock();
+
+ msta_link = mt7996_sta_link(msta, msta->deflink_id);
+ if (!msta_link)
+ goto out;
+
+ txrate = &msta_link->wcid.rate;
if (txrate->legacy || txrate->flags) {
if (txrate->legacy) {
sinfo->txrate.legacy = txrate->legacy;
@@ -1965,6 +1972,9 @@ static void mt7996_sta_statistics(struct ieee80211_hw *hw,
sinfo->rx_packets = msta_link->wcid.stats.rx_packets;
sinfo->filled |= BIT_ULL(NL80211_STA_INFO_RX_PACKETS);
}
+
+out:
+ rcu_read_unlock();
}
static void mt7996_link_rate_ctrl_update(void *data,
@@ -2264,11 +2274,15 @@ static void mt7996_ethtool_worker(void *wi_data, struct ieee80211_sta *sta)
{
struct mt76_ethtool_worker_info *wi = wi_data;
struct mt7996_sta *msta = (struct mt7996_sta *)sta->drv_priv;
- struct mt7996_sta_link *msta_link = &msta->deflink;
+ struct mt7996_sta_link *msta_link;
if (msta->vif->deflink.mt76.idx != wi->idx)
return;
+ msta_link = mt7996_sta_link(msta, msta->deflink_id);
+ if (!msta_link)
+ return;
+
mt76_ethtool_worker(wi, &msta_link->wcid.stats, true);
}
--
2.47.3
^ permalink raw reply related [flat|nested] 27+ messages in thread* [PATCH mt76-next 10/27] wifi: mt76: mt7996: clear all link wcid entries in sta_pre_rcu_remove
2026-10-06 20:59 [PATCH mt76-next 01/27] wifi: mt76: abort a scan only for a link change on the scanning radio Felix Fietkau
` (7 preceding siblings ...)
2026-10-06 20:59 ` [PATCH mt76-next 09/27] wifi: mt76: mt7996: report station statistics from the primary link Felix Fietkau
@ 2026-10-06 20:59 ` Felix Fietkau
2026-10-06 20:59 ` [PATCH mt76-next 11/27] wifi: mt76: mt7996: free the WTBL index if a link allocation fails Felix Fietkau
` (16 subsequent siblings)
25 siblings, 0 replies; 27+ messages in thread
From: Felix Fietkau @ 2026-10-06 20:59 UTC (permalink / raw)
To: linux-wireless
mt76_sta_pre_rcu_remove() clears the wcid table entry of msta->deflink
only. mac80211 calls it before the synchronize_net() that ends the RCU
readers of the station, and frees the station right after
mt7996_mac_sta_remove(). For an MLD station, the wcid table entries of
the other links stay set until mt7996_mac_sta_remove(), after that
grace period. An RX or TX status reader that looks up one of these
entries in that window can follow msta_link->sta or def_wcid into the
freed station.
Clear the wcid table entries of all links of the station in
.sta_pre_rcu_remove.
Fixes: dd82a9e02c05 ("wifi: mt76: mt7996: Rely on mt7996_sta_link in sta_add/sta_remove callbacks")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
.../net/wireless/mediatek/mt76/mt7996/main.c | 26 ++++++++++++++++++-
1 file changed, 25 insertions(+), 1 deletion(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index f44af31281b7..3c27cc02d527 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -1513,6 +1513,30 @@ mt7996_set_active_links(struct ieee80211_vif *vif)
ieee80211_set_active_links_async(vif, active_links);
}
+static void
+mt7996_sta_pre_rcu_remove(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
+ struct ieee80211_sta *sta)
+{
+ struct mt7996_sta *msta = (struct mt7996_sta *)sta->drv_priv;
+ struct mt7996_dev *dev = mt7996_hw_dev(hw);
+ unsigned int link_id;
+
+ mutex_lock(&dev->mt76.mutex);
+ spin_lock_bh(&dev->mt76.status_lock);
+
+ for (link_id = 0; link_id < ARRAY_SIZE(msta->link); link_id++) {
+ struct mt7996_sta_link *msta_link;
+
+ msta_link = mt7996_sta_link_protected(dev, msta, link_id);
+ if (msta_link)
+ rcu_assign_pointer(dev->mt76.wcid[msta_link->wcid.idx],
+ NULL);
+ }
+
+ spin_unlock_bh(&dev->mt76.status_lock);
+ mutex_unlock(&dev->mt76.mutex);
+}
+
static int
mt7996_sta_state(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
struct ieee80211_sta *sta, enum ieee80211_sta_state old_state,
@@ -2618,7 +2642,7 @@ const struct ieee80211_ops mt7996_ops = {
.vif_cfg_changed = mt7996_vif_cfg_changed,
.link_info_changed = mt7996_link_info_changed,
.sta_state = mt7996_sta_state,
- .sta_pre_rcu_remove = mt76_sta_pre_rcu_remove,
+ .sta_pre_rcu_remove = mt7996_sta_pre_rcu_remove,
.link_sta_rc_update = mt7996_link_sta_rc_update,
.set_key = mt7996_set_key,
.ampdu_action = mt7996_ampdu_action,
--
2.47.3
^ permalink raw reply related [flat|nested] 27+ messages in thread* [PATCH mt76-next 11/27] wifi: mt76: mt7996: free the WTBL index if a link allocation fails
2026-10-06 20:59 [PATCH mt76-next 01/27] wifi: mt76: abort a scan only for a link change on the scanning radio Felix Fietkau
` (8 preceding siblings ...)
2026-10-06 20:59 ` [PATCH mt76-next 10/27] wifi: mt76: mt7996: clear all link wcid entries in sta_pre_rcu_remove Felix Fietkau
@ 2026-10-06 20:59 ` Felix Fietkau
2026-10-06 20:59 ` [PATCH mt76-next 12/27] wifi: mt76: mt7996: pick the secondary link from links with driver state Felix Fietkau
` (15 subsequent siblings)
25 siblings, 0 replies; 27+ messages in thread
From: Felix Fietkau @ 2026-10-06 20:59 UTC (permalink / raw)
To: linux-wireless
mt7996_mac_sta_init_link() takes a WTBL index from wcid_mask before it
allocates the msta_link of a link other than the first one. If that
allocation fails, the function returns -ENOMEM and the index stays set
in wcid_mask, so no later station can use it.
Return the index to wcid_mask on that error.
Fixes: dd82a9e02c05 ("wifi: mt76: mt7996: Rely on mt7996_sta_link in sta_add/sta_remove callbacks")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
drivers/net/wireless/mediatek/mt76/mt7996/main.c | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index 3c27cc02d527..5207def2fd36 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -1159,8 +1159,10 @@ mt7996_mac_sta_init_link(struct mt7996_dev *dev,
mt7996_sta_init_txq_wcid(sta, idx);
} else {
msta_link = kzalloc_obj(*msta_link);
- if (!msta_link)
+ if (!msta_link) {
+ mt76_wcid_mask_clear(dev->mt76.wcid_mask, idx);
return -ENOMEM;
+ }
if (msta->seclink_id == msta->deflink_id &&
(sta->valid_links & ~BIT(msta->deflink_id)))
--
2.47.3
^ permalink raw reply related [flat|nested] 27+ messages in thread* [PATCH mt76-next 12/27] wifi: mt76: mt7996: pick the secondary link from links with driver state
2026-10-06 20:59 [PATCH mt76-next 01/27] wifi: mt76: abort a scan only for a link change on the scanning radio Felix Fietkau
` (9 preceding siblings ...)
2026-10-06 20:59 ` [PATCH mt76-next 11/27] wifi: mt76: mt7996: free the WTBL index if a link allocation fails Felix Fietkau
@ 2026-10-06 20:59 ` Felix Fietkau
2026-10-06 20:59 ` [PATCH mt76-next 13/27] wifi: mt76: mt7996: make a returning link primary if a station has none Felix Fietkau
` (14 subsequent siblings)
25 siblings, 0 replies; 27+ messages in thread
From: Felix Fietkau @ 2026-10-06 20:59 UTC (permalink / raw)
To: linux-wireless
mt7996_mac_sta_init_link() takes the secondary link from
sta->valid_links. On a station interface mac80211 creates driver state
only for active links, since drv_change_sta_links() masks both link
sets with vif->active_links. With three valid links of which links 0
and 2 are active, seclink_id then points at link 1, which has no
mt7996_sta_link. mt7996_tx_prepare_skb() maps odd TIDs to seclink_id,
keeps the primary wcid for them and passes the inactive link id to
mt7996_mac_write_txwi(), which finds no vif link and falls back to OMAC
and WMM index 0.
Choose the secondary link among the links that have driver state and
are valid, and choose it again whenever such a link is added, restored
or removed. A removed secondary link now hands over to another
remaining link instead of the primary one, and a secondary link that
was promoted to primary gets a successor.
Fixes: 85cd5534a3f2 ("wifi: mt76: mt7996: use correct link_id when filling TXD and TXP")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
.../net/wireless/mediatek/mt76/mt7996/main.c | 42 +++++++++++++------
1 file changed, 30 insertions(+), 12 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index 5207def2fd36..9de455939125 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -1133,6 +1133,32 @@ mt7996_sta_init_txq_wcid(struct ieee80211_sta *sta, int idx)
}
}
+static u8
+mt7996_sta_seclink_get(struct mt7996_dev *dev, struct mt7996_sta *msta)
+{
+ struct mt7996_sta_link *msta_link;
+ unsigned int link_id;
+
+ if (msta->deflink_id == IEEE80211_LINK_UNSPECIFIED)
+ return IEEE80211_LINK_UNSPECIFIED;
+
+ msta_link = mt7996_sta_link_protected(dev, msta, msta->seclink_id);
+ if (msta->seclink_id != msta->deflink_id && msta_link &&
+ msta_link->wcid.link_valid)
+ return msta->seclink_id;
+
+ for (link_id = 0; link_id < ARRAY_SIZE(msta->link); link_id++) {
+ if (link_id == msta->deflink_id)
+ continue;
+
+ msta_link = mt7996_sta_link_protected(dev, msta, link_id);
+ if (msta_link && msta_link->wcid.link_valid)
+ return link_id;
+ }
+
+ return msta->deflink_id;
+}
+
static int
mt7996_mac_sta_init_link(struct mt7996_dev *dev,
struct ieee80211_bss_conf *link_conf,
@@ -1155,7 +1181,6 @@ mt7996_mac_sta_init_link(struct mt7996_dev *dev,
if (msta->deflink_id == IEEE80211_LINK_UNSPECIFIED) {
msta_link = &msta->deflink;
msta->deflink_id = link_id;
- msta->seclink_id = msta->deflink_id;
mt7996_sta_init_txq_wcid(sta, idx);
} else {
msta_link = kzalloc_obj(*msta_link);
@@ -1163,11 +1188,6 @@ mt7996_mac_sta_init_link(struct mt7996_dev *dev,
mt76_wcid_mask_clear(dev->mt76.wcid_mask, idx);
return -ENOMEM;
}
-
- if (msta->seclink_id == msta->deflink_id &&
- (sta->valid_links & ~BIT(msta->deflink_id)))
- msta->seclink_id = __ffs(sta->valid_links &
- ~BIT(msta->deflink_id));
}
INIT_LIST_HEAD(&msta_link->rc_list);
@@ -1186,6 +1206,7 @@ mt7996_mac_sta_init_link(struct mt7996_dev *dev,
ewma_signal_init(&msta_link->wcid.rssi);
rcu_assign_pointer(msta->link[link_id], msta_link);
+ msta->seclink_id = mt7996_sta_seclink_get(dev, msta);
mt7996_mac_wtbl_update(dev, idx, MT_WTBL_UPDATE_ADM_COUNT_CLEAR);
mt7996_mcu_add_sta(dev, link_conf, link_sta, link, msta_link,
@@ -1223,10 +1244,7 @@ void mt7996_mac_sta_remove_link(struct mt7996_dev *dev,
if (msta->deflink_id == link_id) {
msta->deflink_id = IEEE80211_LINK_UNSPECIFIED;
- if (msta->seclink_id == link_id) {
- /* no secondary link available */
- msta->seclink_id = msta->deflink_id;
- } else {
+ if (msta->seclink_id != link_id) {
struct mt7996_sta_link *msta_seclink;
/* switch to the secondary link */
@@ -1239,10 +1257,9 @@ void mt7996_mac_sta_remove_link(struct mt7996_dev *dev,
msta_seclink->wcid.idx);
}
}
- } else if (msta->seclink_id == link_id) {
- msta->seclink_id = msta->deflink_id;
}
msta_link->wcid.link_valid = false;
+ msta->seclink_id = mt7996_sta_seclink_get(dev, msta);
}
if (flush) {
@@ -1319,6 +1336,7 @@ mt7996_mac_sta_add_links(struct mt7996_dev *dev, struct ieee80211_vif *vif,
msta_link = mt76_dereference(msta->link[link_id], &dev->mt76);
if (msta_link) {
msta_link->wcid.link_valid = true;
+ msta->seclink_id = mt7996_sta_seclink_get(dev, msta);
continue;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 27+ messages in thread* [PATCH mt76-next 13/27] wifi: mt76: mt7996: make a returning link primary if a station has none
2026-10-06 20:59 [PATCH mt76-next 01/27] wifi: mt76: abort a scan only for a link change on the scanning radio Felix Fietkau
` (10 preceding siblings ...)
2026-10-06 20:59 ` [PATCH mt76-next 12/27] wifi: mt76: mt7996: pick the secondary link from links with driver state Felix Fietkau
@ 2026-10-06 20:59 ` Felix Fietkau
2026-10-06 20:59 ` [PATCH mt76-next 14/27] wifi: mt76: mt7996: do not reuse the deflink of a removed station link Felix Fietkau
` (13 subsequent siblings)
25 siblings, 0 replies; 27+ messages in thread
From: Felix Fietkau @ 2026-10-06 20:59 UTC (permalink / raw)
To: linux-wireless
mt7996_mac_sta_remove_link() clears deflink_id when it removes the
primary link and no secondary link is left to take its place. If the
removed link then comes back, mt7996_mac_sta_add_links() finds its
existing msta_link and only marks it valid again. deflink_id stays
IEEE80211_LINK_UNSPECIFIED, so the station has no primary link: the TXQs
keep the WLAN index that they had before, seclink_id stays unspecified,
mt7996_sta_rate_ctrl_update() finds no primary link, and
mt7996_sta_set_4addr() treats every link as a secondary link.
Make a returning link the primary link when the station has none, in
the same way as mt7996_mac_sta_init_link() does for a new link. Move the
two steps that set the primary link, and the switch to the secondary
link on removal, to helpers.
Fixes: e8c819df0243 ("wifi: mt76: mt7996: Destroy active sta links in mt7996_mac_sta_remove()")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
.../net/wireless/mediatek/mt76/mt7996/main.c | 55 +++++++++++++------
1 file changed, 37 insertions(+), 18 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index 9de455939125..8f483b1de852 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -1133,6 +1133,38 @@ mt7996_sta_init_txq_wcid(struct ieee80211_sta *sta, int idx)
}
}
+static void
+mt7996_sta_deflink_set(struct ieee80211_sta *sta, unsigned int link_id,
+ int idx)
+{
+ struct mt7996_sta *msta = (struct mt7996_sta *)sta->drv_priv;
+
+ msta->deflink_id = link_id;
+ mt7996_sta_init_txq_wcid(sta, idx);
+}
+
+static void
+mt7996_sta_deflink_release(struct mt7996_dev *dev, struct ieee80211_sta *sta,
+ unsigned int link_id)
+{
+ struct mt7996_sta *msta = (struct mt7996_sta *)sta->drv_priv;
+ struct mt7996_sta_link *msta_seclink;
+
+ if (msta->deflink_id != link_id)
+ return;
+
+ msta->deflink_id = IEEE80211_LINK_UNSPECIFIED;
+ if (msta->seclink_id == link_id)
+ return;
+
+ /* switch to the secondary link */
+ msta_seclink = mt76_dereference(msta->link[msta->seclink_id],
+ &dev->mt76);
+ if (msta_seclink)
+ mt7996_sta_deflink_set(sta, msta->seclink_id,
+ msta_seclink->wcid.idx);
+}
+
static u8
mt7996_sta_seclink_get(struct mt7996_dev *dev, struct mt7996_sta *msta)
{
@@ -1180,8 +1212,7 @@ mt7996_mac_sta_init_link(struct mt7996_dev *dev,
if (msta->deflink_id == IEEE80211_LINK_UNSPECIFIED) {
msta_link = &msta->deflink;
- msta->deflink_id = link_id;
- mt7996_sta_init_txq_wcid(sta, idx);
+ mt7996_sta_deflink_set(sta, link_id, idx);
} else {
msta_link = kzalloc_obj(*msta_link);
if (!msta_link) {
@@ -1242,22 +1273,7 @@ void mt7996_mac_sta_remove_link(struct mt7996_dev *dev,
mt7996_mac_wtbl_update(dev, msta_link->wcid.idx,
MT_WTBL_UPDATE_ADM_COUNT_CLEAR);
- if (msta->deflink_id == link_id) {
- msta->deflink_id = IEEE80211_LINK_UNSPECIFIED;
- if (msta->seclink_id != link_id) {
- struct mt7996_sta_link *msta_seclink;
-
- /* switch to the secondary link */
- msta_seclink = mt76_dereference(
- msta->link[msta->seclink_id],
- &dev->mt76);
- if (msta_seclink) {
- msta->deflink_id = msta->seclink_id;
- mt7996_sta_init_txq_wcid(sta,
- msta_seclink->wcid.idx);
- }
- }
- }
+ mt7996_sta_deflink_release(dev, sta, link_id);
msta_link->wcid.link_valid = false;
msta->seclink_id = mt7996_sta_seclink_get(dev, msta);
}
@@ -1336,6 +1352,9 @@ mt7996_mac_sta_add_links(struct mt7996_dev *dev, struct ieee80211_vif *vif,
msta_link = mt76_dereference(msta->link[link_id], &dev->mt76);
if (msta_link) {
msta_link->wcid.link_valid = true;
+ if (msta->deflink_id == IEEE80211_LINK_UNSPECIFIED)
+ mt7996_sta_deflink_set(sta, link_id,
+ msta_link->wcid.idx);
msta->seclink_id = mt7996_sta_seclink_get(dev, msta);
continue;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 27+ messages in thread* [PATCH mt76-next 14/27] wifi: mt76: mt7996: do not reuse the deflink of a removed station link
2026-10-06 20:59 [PATCH mt76-next 01/27] wifi: mt76: abort a scan only for a link change on the scanning radio Felix Fietkau
` (11 preceding siblings ...)
2026-10-06 20:59 ` [PATCH mt76-next 13/27] wifi: mt76: mt7996: make a returning link primary if a station has none Felix Fietkau
@ 2026-10-06 20:59 ` Felix Fietkau
2026-10-06 20:59 ` [PATCH mt76-next 15/27] wifi: mt76: mt7996: keep a returning link when adding station links fails Felix Fietkau
` (12 subsequent siblings)
25 siblings, 0 replies; 27+ messages in thread
From: Felix Fietkau @ 2026-10-06 20:59 UTC (permalink / raw)
To: linux-wireless
mt7996_mac_sta_init_link() puts a new link in msta->deflink whenever
the station has no primary link. A removed link keeps its msta_link
until the station is removed, and the station has no primary link
after one mt7996_mac_sta_change_links() call removes all of its links
and adds others. If the removed
primary link was in msta->deflink, the new link then overwrites it:
both link slots point to the same msta_link, and the WTBL index of the
removed link is lost, so mt7996_mac_sta_remove() never returns it to
wcid_mask.
Use msta->deflink only if no link slot of the station points to it,
and allocate the msta_link otherwise. Choose the primary link as
before.
Fixes: e8c819df0243 ("wifi: mt76: mt7996: Destroy active sta links in mt7996_mac_sta_remove()")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
.../net/wireless/mediatek/mt76/mt7996/main.c | 24 +++++++++++++++----
1 file changed, 20 insertions(+), 4 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index 8f483b1de852..8e70a4061ed6 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -1143,6 +1143,20 @@ mt7996_sta_deflink_set(struct ieee80211_sta *sta, unsigned int link_id,
mt7996_sta_init_txq_wcid(sta, idx);
}
+static bool
+mt7996_sta_deflink_busy(struct mt7996_dev *dev, struct mt7996_sta *msta)
+{
+ unsigned int link_id;
+
+ for (link_id = 0; link_id < ARRAY_SIZE(msta->link); link_id++) {
+ if (mt7996_sta_link_protected(dev, msta, link_id) ==
+ &msta->deflink)
+ return true;
+ }
+
+ return false;
+}
+
static void
mt7996_sta_deflink_release(struct mt7996_dev *dev, struct ieee80211_sta *sta,
unsigned int link_id)
@@ -1210,17 +1224,19 @@ mt7996_mac_sta_init_link(struct mt7996_dev *dev,
if (idx < 0)
return -ENOSPC;
- if (msta->deflink_id == IEEE80211_LINK_UNSPECIFIED) {
- msta_link = &msta->deflink;
- mt7996_sta_deflink_set(sta, link_id, idx);
- } else {
+ if (mt7996_sta_deflink_busy(dev, msta)) {
msta_link = kzalloc_obj(*msta_link);
if (!msta_link) {
mt76_wcid_mask_clear(dev->mt76.wcid_mask, idx);
return -ENOMEM;
}
+ } else {
+ msta_link = &msta->deflink;
}
+ if (msta->deflink_id == IEEE80211_LINK_UNSPECIFIED)
+ mt7996_sta_deflink_set(sta, link_id, idx);
+
INIT_LIST_HEAD(&msta_link->rc_list);
INIT_LIST_HEAD(&msta_link->wcid.poll_list);
msta_link->sta = msta;
--
2.47.3
^ permalink raw reply related [flat|nested] 27+ messages in thread* [PATCH mt76-next 15/27] wifi: mt76: mt7996: keep a returning link when adding station links fails
2026-10-06 20:59 [PATCH mt76-next 01/27] wifi: mt76: abort a scan only for a link change on the scanning radio Felix Fietkau
` (12 preceding siblings ...)
2026-10-06 20:59 ` [PATCH mt76-next 14/27] wifi: mt76: mt7996: do not reuse the deflink of a removed station link Felix Fietkau
@ 2026-10-06 20:59 ` Felix Fietkau
2026-10-06 20:59 ` [PATCH mt76-next 16/27] wifi: mt76: mt7996: add the station BMC entry as new once per BSSID Felix Fietkau
` (11 subsequent siblings)
25 siblings, 0 replies; 27+ messages in thread
From: Felix Fietkau @ 2026-10-06 20:59 UTC (permalink / raw)
To: linux-wireless
mt7996_mac_sta_add_links() removes all links that it was asked to add,
with flush set, when one of them fails. A link that comes back after
a removal kept its msta_link and may still have a firmware entry. The
flush frees its msta_link and returns its WTBL index to wcid_mask, so a
later station can be given an index whose entry still belongs to this
station.
Flush only the links that this call created, and remove a returning
link again without flush.
Fixes: e8c819df0243 ("wifi: mt76: mt7996: Destroy active sta links in mt7996_mac_sta_remove()")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
drivers/net/wireless/mediatek/mt76/mt7996/main.c | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index 8e70a4061ed6..58a487549ddf 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -1355,6 +1355,7 @@ mt7996_mac_sta_add_links(struct mt7996_dev *dev, struct ieee80211_vif *vif,
struct ieee80211_sta *sta, unsigned long new_links)
{
struct mt7996_sta *msta = (struct mt7996_sta *)sta->drv_priv;
+ unsigned long created = 0, returned = 0;
unsigned int link_id;
int err = 0;
@@ -1372,6 +1373,7 @@ mt7996_mac_sta_add_links(struct mt7996_dev *dev, struct ieee80211_vif *vif,
mt7996_sta_deflink_set(sta, link_id,
msta_link->wcid.idx);
msta->seclink_id = mt7996_sta_seclink_get(dev, msta);
+ returned |= BIT(link_id);
continue;
}
@@ -1405,12 +1407,14 @@ mt7996_mac_sta_add_links(struct mt7996_dev *dev, struct ieee80211_vif *vif,
goto error_unlink;
mphy->num_sta++;
+ created |= BIT(link_id);
}
return 0;
error_unlink:
- mt7996_mac_sta_remove_links(dev, vif, sta, new_links, true);
+ mt7996_mac_sta_remove_links(dev, vif, sta, created, true);
+ mt7996_mac_sta_remove_links(dev, vif, sta, returned, false);
return err;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 27+ messages in thread* [PATCH mt76-next 16/27] wifi: mt76: mt7996: add the station BMC entry as new once per BSSID
2026-10-06 20:59 [PATCH mt76-next 01/27] wifi: mt76: abort a scan only for a link change on the scanning radio Felix Fietkau
` (13 preceding siblings ...)
2026-10-06 20:59 ` [PATCH mt76-next 15/27] wifi: mt76: mt7996: keep a returning link when adding station links fails Felix Fietkau
@ 2026-10-06 20:59 ` Felix Fietkau
2026-10-06 20:59 ` [PATCH mt76-next 17/27] wifi: mt76: mt7996: keep 4-address and decap state per station Felix Fietkau
` (10 subsequent siblings)
25 siblings, 0 replies; 27+ messages in thread
From: Felix Fietkau @ 2026-10-06 20:59 UTC (permalink / raw)
To: linux-wireless
A station link sends its broadcast STA_REC with EXTRA_INFO_NEW on every
BSS_CHANGED_BSSID. Commit 8e3e7567b8c1 ("mt76: mt7915: add sta_rec with
EXTRA_INFO_NEW for the first time only") notes that the firmware may
remove an entry that is added as new twice. Since a removed vif link
keeps its firmware entry, this happens on a link that is deactivated and
activated again: _ieee80211_set_active_links() reports
BSS_CHANGED_BSSID with the same BSSID, after ieee80211_key_switch_links()
has installed the group keys of the link, and nothing in the driver
installs them again after the entry is added as new.
Record the BSSID the entry was added with, and add it as new only for
a different BSSID. A BSSID that goes to zero on disconnection clears
the record, so that the next association adds the entry as new again.
A link that is set up clears the record as well, since the firmware has
no entry for it then.
Fixes: 08813703ac41 ("wifi: mt76: mt7996: Destroy vif active links in mt7996_remove_interface()")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
.../net/wireless/mediatek/mt76/mt7996/main.c | 38 ++++++++++++-------
.../wireless/mediatek/mt76/mt7996/mt7996.h | 1 +
2 files changed, 26 insertions(+), 13 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index 58a487549ddf..d868e9641334 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -341,6 +341,7 @@ int mt7996_vif_link_add(struct mt76_phy *mphy, struct ieee80211_vif *vif,
return -ENOSPC;
link->mld_idx = mld_idx;
+ eth_zero_addr(link->bmc_bssid);
mlink->omac_idx = idx;
mlink->band_idx = band_idx;
mlink->wmm_idx = vif->type == NL80211_IFTYPE_AP ? 0 : 3;
@@ -898,6 +899,23 @@ mt7996_update_mu_group(struct ieee80211_hw *hw, struct mt7996_vif_link *link,
mt76_wr(dev, MT_WF_PHYRX_BAND_GID_TAB_POS3(band), mu[3]);
}
+static void
+mt7996_vif_link_bss_add(struct mt7996_phy *phy, struct ieee80211_vif *vif,
+ struct ieee80211_bss_conf *link_conf,
+ struct mt7996_vif_link *link, bool newly)
+{
+ if (vif->type == NL80211_IFTYPE_STATION) {
+ newly = !ether_addr_equal_unaligned(link->bmc_bssid,
+ link_conf->bssid);
+ memcpy(link->bmc_bssid, link_conf->bssid, ETH_ALEN);
+ }
+
+ mt7996_mcu_add_bss_info(phy, vif, link_conf, &link->mt76,
+ &link->msta_link, true);
+ mt7996_mcu_add_sta(phy->dev, link_conf, NULL, link, NULL,
+ CONN_STATE_PORT_SECURE, newly);
+}
+
static void
mt7996_vif_cfg_changed(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
u64 changed)
@@ -929,12 +947,8 @@ mt7996_vif_cfg_changed(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
if (!phy)
continue;
- mt7996_mcu_add_bss_info(phy, vif, link_conf,
- &link->mt76, &link->msta_link,
- true);
- mt7996_mcu_add_sta(dev, link_conf, NULL, link, NULL,
- CONN_STATE_PORT_SECURE,
- !!(changed & BSS_CHANGED_BSSID));
+ mt7996_vif_link_bss_add(phy, vif, link_conf, link,
+ !!(changed & BSS_CHANGED_BSSID));
}
}
@@ -980,13 +994,11 @@ mt7996_link_info_changed(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
* and then peer references bss_info_rfch to set bandwidth cap.
*/
if ((changed & BSS_CHANGED_BSSID && !is_zero_ether_addr(info->bssid)) ||
- (changed & BSS_CHANGED_BEACON_ENABLED && info->enable_beacon)) {
- mt7996_mcu_add_bss_info(phy, vif, info, &link->mt76,
- &link->msta_link, true);
- mt7996_mcu_add_sta(dev, info, NULL, link, NULL,
- CONN_STATE_PORT_SECURE,
- !!(changed & BSS_CHANGED_BSSID));
- }
+ (changed & BSS_CHANGED_BEACON_ENABLED && info->enable_beacon))
+ mt7996_vif_link_bss_add(phy, vif, info, link,
+ !!(changed & BSS_CHANGED_BSSID));
+ else if (changed & BSS_CHANGED_BSSID)
+ eth_zero_addr(link->bmc_bssid);
if (changed & BSS_CHANGED_HT || changed & BSS_CHANGED_ERP_CTS_PROT)
mt7996_mcu_set_protection(phy, link, info->ht_operation_mode,
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
index 82a2a8a5db69..03fc0e7fd4f1 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
@@ -279,6 +279,7 @@ struct mt7996_vif_link {
struct cfg80211_bitrate_mask bitrate_mask;
u8 mld_idx;
+ u8 bmc_bssid[ETH_ALEN];
};
struct mt7996_vif_link_info {
--
2.47.3
^ permalink raw reply related [flat|nested] 27+ messages in thread* [PATCH mt76-next 17/27] wifi: mt76: mt7996: keep 4-address and decap state per station
2026-10-06 20:59 [PATCH mt76-next 01/27] wifi: mt76: abort a scan only for a link change on the scanning radio Felix Fietkau
` (14 preceding siblings ...)
2026-10-06 20:59 ` [PATCH mt76-next 16/27] wifi: mt76: mt7996: add the station BMC entry as new once per BSSID Felix Fietkau
@ 2026-10-06 20:59 ` Felix Fietkau
2026-10-06 20:59 ` [PATCH mt76-next 18/27] wifi: mt76: mt7996: do not add a station link in disconnected state Felix Fietkau
` (9 subsequent siblings)
25 siblings, 0 replies; 27+ messages in thread
From: Felix Fietkau @ 2026-10-06 20:59 UTC (permalink / raw)
To: linux-wireless
mac80211 reports 4-address mode and decap offload once per station,
through sta_set_4addr and sta_set_decap_offload. mt7996_sta_set_4addr()
and mt7996_sta_set_decap_offload() set MT_WCID_FLAG_4ADDR and
MT_WCID_FLAG_HDR_TRANS only on the links that exist at that moment, but
the hardware keeps header translation per link. A link that is added
later starts without the flags, and a link that is removed at that time
keeps its old flags when it comes back, so the firmware gets the wrong
to_ds/from_ds pair for that link.
Keep both states in the station, and apply them to a link when the
callbacks run, when the link is set up and when it comes back.
Fixes: f520eceacd24 ("wifi: mt76: mt7996: Add mt7996_mac_sta_change_links callback")
Fixes: e8c819df0243 ("wifi: mt76: mt7996: Destroy active sta links in mt7996_mac_sta_remove()")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
.../net/wireless/mediatek/mt76/mt7996/main.c | 29 +++++++++++--------
.../wireless/mediatek/mt76/mt7996/mt7996.h | 2 ++
2 files changed, 19 insertions(+), 12 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index d868e9641334..bba5a8c6dec8 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -1155,6 +1155,16 @@ mt7996_sta_deflink_set(struct ieee80211_sta *sta, unsigned int link_id,
mt7996_sta_init_txq_wcid(sta, idx);
}
+static void
+mt7996_sta_link_hdr_trans_set(struct mt7996_sta *msta,
+ struct mt7996_sta_link *msta_link)
+{
+ assign_bit(MT_WCID_FLAG_4ADDR, &msta_link->wcid.flags,
+ test_bit(MT_WCID_FLAG_4ADDR, &msta->wcid_flags));
+ assign_bit(MT_WCID_FLAG_HDR_TRANS, &msta_link->wcid.flags,
+ test_bit(MT_WCID_FLAG_HDR_TRANS, &msta->wcid_flags));
+}
+
static bool
mt7996_sta_deflink_busy(struct mt7996_dev *dev, struct mt7996_sta *msta)
{
@@ -1261,6 +1271,8 @@ mt7996_mac_sta_init_link(struct mt7996_dev *dev,
if (link_sta->sta->tdls)
set_bit(MT_WCID_FLAG_TDLS_PEER, &msta_link->wcid.flags);
+ mt7996_sta_link_hdr_trans_set(msta, msta_link);
+
ewma_avg_signal_init(&msta_link->avg_ack_signal);
ewma_signal_init(&msta_link->wcid.rssi);
@@ -1381,6 +1393,7 @@ mt7996_mac_sta_add_links(struct mt7996_dev *dev, struct ieee80211_vif *vif,
msta_link = mt76_dereference(msta->link[link_id], &dev->mt76);
if (msta_link) {
msta_link->wcid.link_valid = true;
+ mt7996_sta_link_hdr_trans_set(msta, msta_link);
if (msta->deflink_id == IEEE80211_LINK_UNSPECIFIED)
mt7996_sta_deflink_set(sta, link_id,
msta_link->wcid.idx);
@@ -2166,6 +2179,7 @@ static void mt7996_sta_set_4addr(struct ieee80211_hw *hw,
mutex_lock(&dev->mt76.mutex);
+ assign_bit(MT_WCID_FLAG_4ADDR, &msta->wcid_flags, enabled);
for_each_sta_active_link(vif, sta, link_sta, link_id) {
struct mt7996_sta_link *msta_link;
struct mt7996_vif_link *link;
@@ -2178,11 +2192,7 @@ static void mt7996_sta_set_4addr(struct ieee80211_hw *hw,
if (!msta_link)
continue;
- if (enabled)
- set_bit(MT_WCID_FLAG_4ADDR, &msta_link->wcid.flags);
- else
- clear_bit(MT_WCID_FLAG_4ADDR, &msta_link->wcid.flags);
-
+ mt7996_sta_link_hdr_trans_set(msta, msta_link);
if (!msta_link->wcid.sta)
continue;
@@ -2212,6 +2222,7 @@ static void mt7996_sta_set_decap_offload(struct ieee80211_hw *hw,
mutex_lock(&dev->mt76.mutex);
+ assign_bit(MT_WCID_FLAG_HDR_TRANS, &msta->wcid_flags, enabled);
for_each_sta_active_link(vif, sta, link_sta, link_id) {
struct mt7996_sta_link *msta_link;
struct mt7996_vif_link *link;
@@ -2224,13 +2235,7 @@ static void mt7996_sta_set_decap_offload(struct ieee80211_hw *hw,
if (!msta_link)
continue;
- if (enabled)
- set_bit(MT_WCID_FLAG_HDR_TRANS,
- &msta_link->wcid.flags);
- else
- clear_bit(MT_WCID_FLAG_HDR_TRANS,
- &msta_link->wcid.flags);
-
+ mt7996_sta_link_hdr_trans_set(msta, msta_link);
if (!msta_link->wcid.sta)
continue;
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
index 03fc0e7fd4f1..949e659b8e2d 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
@@ -269,6 +269,8 @@ struct mt7996_sta {
u8 deflink_id;
u8 seclink_id;
+ unsigned long wcid_flags;
+
struct mt7996_vif *vif;
};
--
2.47.3
^ permalink raw reply related [flat|nested] 27+ messages in thread* [PATCH mt76-next 18/27] wifi: mt76: mt7996: do not add a station link in disconnected state
2026-10-06 20:59 [PATCH mt76-next 01/27] wifi: mt76: abort a scan only for a link change on the scanning radio Felix Fietkau
` (15 preceding siblings ...)
2026-10-06 20:59 ` [PATCH mt76-next 17/27] wifi: mt76: mt7996: keep 4-address and decap state per station Felix Fietkau
@ 2026-10-06 20:59 ` Felix Fietkau
2026-10-06 20:59 ` [PATCH mt76-next 19/27] wifi: mt76: mt7996: record the connection state of a station Felix Fietkau
` (8 subsequent siblings)
25 siblings, 0 replies; 27+ messages in thread
From: Felix Fietkau @ 2026-10-06 20:59 UTC (permalink / raw)
To: linux-wireless; +Cc: Howard Hsu
From: Howard Hsu <howard-yh.hsu@mediatek.com>
Since the driver uses the .sta_state op, mt7996_mac_sta_init_link()
sends a STA_REC with CONN_STATE_DISCONNECT when a station link is set
up. Before that change, the first STA_REC for a station was the one
that connects it. MediaTek reports that DL OFDMA fails after a station
reconnects.
Only send a STA_REC for a station link when it connects. Until then the
firmware has no STA_REC for the link, so track whether a link is
connected, and skip rate control updates and the 4-address and decap
updates for a link that is not. wcid.sta cannot be used for this,
since it is set when the link is set up.
Fixes: 13e2e9c6b348 ("wifi: mt76: mt7996: use mac80211 .sta_state op")
Signed-off-by: Howard Hsu <howard-yh.hsu@mediatek.com>
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
.../net/wireless/mediatek/mt76/mt7996/mac.c | 4 +++
.../net/wireless/mediatek/mt76/mt7996/main.c | 33 +++++++++++++------
.../wireless/mediatek/mt76/mt7996/mt7996.h | 1 +
3 files changed, 28 insertions(+), 10 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mac.c b/drivers/net/wireless/mediatek/mt76/mt7996/mac.c
index eb39526548e8..c4e82b1e259c 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mac.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mac.c
@@ -2948,6 +2948,10 @@ void mt7996_mac_sta_rc_work(struct work_struct *work)
changed = msta_link->changed;
msta_link->changed = 0;
+
+ if (!msta_link->connected)
+ continue;
+
mvif = msta_link->sta->vif;
vif = container_of((void *)mvif, struct ieee80211_vif,
drv_priv);
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index bba5a8c6dec8..a23d148eafa6 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -1229,7 +1229,6 @@ mt7996_sta_seclink_get(struct mt7996_dev *dev, struct mt7996_sta *msta)
static int
mt7996_mac_sta_init_link(struct mt7996_dev *dev,
- struct ieee80211_bss_conf *link_conf,
struct ieee80211_link_sta *link_sta,
struct mt7996_vif_link *link, unsigned int link_id)
{
@@ -1262,6 +1261,7 @@ mt7996_mac_sta_init_link(struct mt7996_dev *dev,
INIT_LIST_HEAD(&msta_link->rc_list);
INIT_LIST_HEAD(&msta_link->wcid.poll_list);
msta_link->sta = msta;
+ msta_link->connected = false;
msta_link->wcid.sta = 1;
msta_link->wcid.idx = idx;
msta_link->wcid.link_id = link_id;
@@ -1280,8 +1280,6 @@ mt7996_mac_sta_init_link(struct mt7996_dev *dev,
msta->seclink_id = mt7996_sta_seclink_get(dev, msta);
mt7996_mac_wtbl_update(dev, idx, MT_WTBL_UPDATE_ADM_COUNT_CLEAR);
- mt7996_mcu_add_sta(dev, link_conf, link_sta, link, msta_link,
- CONN_STATE_DISCONNECT, true);
rcu_assign_pointer(dev->mt76.wcid[idx], &msta_link->wcid);
mt76_wcid_init(&msta_link->wcid, phy->mt76->band_idx);
@@ -1384,7 +1382,6 @@ mt7996_mac_sta_add_links(struct mt7996_dev *dev, struct ieee80211_vif *vif,
int err = 0;
for_each_set_bit(link_id, &new_links, IEEE80211_MLD_MAX_NUM_LINKS) {
- struct ieee80211_bss_conf *link_conf;
struct ieee80211_link_sta *link_sta;
struct mt7996_sta_link *msta_link;
struct mt7996_vif_link *link;
@@ -1402,8 +1399,7 @@ mt7996_mac_sta_add_links(struct mt7996_dev *dev, struct ieee80211_vif *vif,
continue;
}
- link_conf = link_conf_dereference_protected(vif, link_id);
- if (!link_conf) {
+ if (!link_conf_dereference_protected(vif, link_id)) {
err = -EINVAL;
goto error_unlink;
}
@@ -1426,8 +1422,7 @@ mt7996_mac_sta_add_links(struct mt7996_dev *dev, struct ieee80211_vif *vif,
goto error_unlink;
}
- err = mt7996_mac_sta_init_link(dev, link_conf, link_sta, link,
- link_id);
+ err = mt7996_mac_sta_init_link(dev, link_sta, link, link_id);
if (err)
goto error_unlink;
@@ -1515,6 +1510,7 @@ mt7996_mac_sta_links_event(struct mt7996_dev *dev, struct ieee80211_vif *vif,
if (err)
return err;
+ msta_link->connected = true;
err = mt7996_mcu_add_rate_ctrl(dev, msta_link->sta, vif,
link_id, false);
if (err)
@@ -1551,16 +1547,33 @@ mt7996_mac_sta_links_event(struct mt7996_dev *dev, struct ieee80211_vif *vif,
return 0;
}
+static void
+mt7996_mac_sta_clear_connected(struct mt7996_dev *dev, struct mt7996_sta *msta)
+{
+ unsigned int link_id;
+
+ for (link_id = 0; link_id < ARRAY_SIZE(msta->link); link_id++) {
+ struct mt7996_sta_link *msta_link;
+
+ msta_link = mt7996_sta_link_protected(dev, msta, link_id);
+ if (msta_link)
+ msta_link->connected = false;
+ }
+}
+
static int
mt7996_mac_sta_event(struct mt7996_dev *dev, struct ieee80211_vif *vif,
struct ieee80211_sta *sta, enum mt76_sta_event ev)
{
+ struct mt7996_sta *msta = (struct mt7996_sta *)sta->drv_priv;
int err;
mutex_lock(&dev->mt76.mutex);
err = mt7996_mac_sta_links_event(dev, vif, sta,
mt7996_mac_sta_links(dev, vif, sta),
ev);
+ if (!err && ev == MT76_STA_EVENT_DISASSOC)
+ mt7996_mac_sta_clear_connected(dev, msta);
mutex_unlock(&dev->mt76.mutex);
return err;
@@ -2193,7 +2206,7 @@ static void mt7996_sta_set_4addr(struct ieee80211_hw *hw,
continue;
mt7996_sta_link_hdr_trans_set(msta, msta_link);
- if (!msta_link->wcid.sta)
+ if (!msta_link->connected)
continue;
mt7996_mcu_wtbl_update_hdr_trans(dev, vif, link, msta_link);
@@ -2236,7 +2249,7 @@ static void mt7996_sta_set_decap_offload(struct ieee80211_hw *hw,
continue;
mt7996_sta_link_hdr_trans_set(msta, msta_link);
- if (!msta_link->wcid.sta)
+ if (!msta_link->connected)
continue;
mt7996_mcu_wtbl_update_hdr_trans(dev, vif, link, msta_link);
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
index 949e659b8e2d..ec453f5ff3a5 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
@@ -244,6 +244,7 @@ struct mt7996_sta_link {
struct mt76_wcid wcid; /* must be first */
struct mt7996_sta *sta;
+ bool connected;
struct list_head rc_list;
u32 airtime_ac[8];
--
2.47.3
^ permalink raw reply related [flat|nested] 27+ messages in thread* [PATCH mt76-next 19/27] wifi: mt76: mt7996: record the connection state of a station
2026-10-06 20:59 [PATCH mt76-next 01/27] wifi: mt76: abort a scan only for a link change on the scanning radio Felix Fietkau
` (16 preceding siblings ...)
2026-10-06 20:59 ` [PATCH mt76-next 18/27] wifi: mt76: mt7996: do not add a station link in disconnected state Felix Fietkau
@ 2026-10-06 20:59 ` Felix Fietkau
2026-10-06 20:59 ` [PATCH mt76-next 20/27] wifi: mt76: mt7996: keep the block ack session parameters per station Felix Fietkau
` (7 subsequent siblings)
25 siblings, 0 replies; 27+ messages in thread
From: Felix Fietkau @ 2026-10-06 20:59 UTC (permalink / raw)
To: linux-wireless
mt7996_sta_state() turns the station state transitions into STA_REC
updates for the links of the station, but the driver keeps no record of
the state that the station reached. A link that is added after
association needs that state to be brought up to the same level as the
other links.
Record the connection state after each station event, and lower it to
CONN_STATE_CONNECT when the station leaves IEEE80211_STA_AUTHORIZED,
for example when hostapd de-authorizes it for a re-authentication.
No functional change on its own.
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
.../net/wireless/mediatek/mt76/mt7996/main.c | 30 ++++++++++++++++++-
.../wireless/mediatek/mt76/mt7996/mt7996.h | 1 +
2 files changed, 30 insertions(+), 1 deletion(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index a23d148eafa6..7792c3a878af 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -1472,6 +1472,7 @@ mt7996_mac_sta_add(struct mt7996_dev *dev, struct ieee80211_vif *vif,
msta->deflink_id = IEEE80211_LINK_UNSPECIFIED;
msta->seclink_id = IEEE80211_LINK_UNSPECIFIED;
+ msta->conn_state = CONN_STATE_DISCONNECT;
msta->vif = mvif;
err = mt7996_mac_sta_add_links(dev, vif, sta, links);
@@ -1569,11 +1570,27 @@ mt7996_mac_sta_event(struct mt7996_dev *dev, struct ieee80211_vif *vif,
int err;
mutex_lock(&dev->mt76.mutex);
+
err = mt7996_mac_sta_links_event(dev, vif, sta,
mt7996_mac_sta_links(dev, vif, sta),
ev);
- if (!err && ev == MT76_STA_EVENT_DISASSOC)
+ if (err)
+ goto out;
+
+ switch (ev) {
+ case MT76_STA_EVENT_ASSOC:
+ msta->conn_state = CONN_STATE_CONNECT;
+ break;
+ case MT76_STA_EVENT_AUTHORIZE:
+ msta->conn_state = CONN_STATE_PORT_SECURE;
+ break;
+ case MT76_STA_EVENT_DISASSOC:
+ msta->conn_state = CONN_STATE_DISCONNECT;
mt7996_mac_sta_clear_connected(dev, msta);
+ break;
+ }
+
+out:
mutex_unlock(&dev->mt76.mutex);
return err;
@@ -1650,6 +1667,17 @@ mt7996_sta_state(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
new_state == IEEE80211_STA_NOTEXIST)
mt7996_mac_sta_remove(dev, vif, sta);
+ if (old_state == IEEE80211_STA_AUTHORIZED &&
+ new_state == IEEE80211_STA_ASSOC) {
+ struct mt7996_sta *msta = (struct mt7996_sta *)sta->drv_priv;
+
+ mutex_lock(&dev->mt76.mutex);
+ msta->conn_state = CONN_STATE_CONNECT;
+ mutex_unlock(&dev->mt76.mutex);
+
+ return 0;
+ }
+
if (old_state == IEEE80211_STA_AUTH &&
new_state == IEEE80211_STA_ASSOC) {
mt7996_set_active_links(vif);
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
index ec453f5ff3a5..ead211391404 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
@@ -269,6 +269,7 @@ struct mt7996_sta {
struct mt7996_sta_link __rcu *link[IEEE80211_MLD_MAX_NUM_LINKS];
u8 deflink_id;
u8 seclink_id;
+ u8 conn_state;
unsigned long wcid_flags;
--
2.47.3
^ permalink raw reply related [flat|nested] 27+ messages in thread* [PATCH mt76-next 20/27] wifi: mt76: mt7996: keep the block ack session parameters per station
2026-10-06 20:59 [PATCH mt76-next 01/27] wifi: mt76: abort a scan only for a link change on the scanning radio Felix Fietkau
` (17 preceding siblings ...)
2026-10-06 20:59 ` [PATCH mt76-next 19/27] wifi: mt76: mt7996: record the connection state of a station Felix Fietkau
@ 2026-10-06 20:59 ` Felix Fietkau
2026-10-06 20:59 ` [PATCH mt76-next 21/27] wifi: mt76: mt7996: set up the BSS of a late station link before its peer Felix Fietkau
` (6 subsequent siblings)
25 siblings, 0 replies; 27+ messages in thread
From: Felix Fietkau @ 2026-10-06 20:59 UTC (permalink / raw)
To: linux-wireless
mt7996_mcu_add_tx_ba() and mt7996_mcu_add_rx_ba() send STA_REC_BA to
the links that are active when mac80211 starts the session. To set up a
running session on a link that is added later, the driver needs the
parameters of that session, but it records a TX session only as a bit
in ampdu_state and keeps no RX session parameters.
Store the parameters of each running TX and RX session per TID, and
pass the set of links to update to mt7996_mcu_add_tx_ba() and
mt7996_mcu_add_rx_ba(). mt7996_ampdu_action() passes the links from
mt7996_mac_sta_links(), which are the links that have driver state.
No functional change on its own.
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
.../net/wireless/mediatek/mt76/mt7996/main.c | 40 ++++++++++++++++---
.../net/wireless/mediatek/mt76/mt7996/mcu.c | 12 +++---
.../wireless/mediatek/mt76/mt7996/mt7996.h | 17 +++++++-
3 files changed, 56 insertions(+), 13 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index 7792c3a878af..d90ada7f0f2a 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -1473,6 +1473,8 @@ mt7996_mac_sta_add(struct mt7996_dev *dev, struct ieee80211_vif *vif,
msta->deflink_id = IEEE80211_LINK_UNSPECIFIED;
msta->seclink_id = IEEE80211_LINK_UNSPECIFIED;
msta->conn_state = CONN_STATE_DISCONNECT;
+ msta->ba_tx_mask = 0;
+ msta->ba_rx_mask = 0;
msta->vif = mvif;
err = mt7996_mac_sta_add_links(dev, vif, sta, links);
@@ -1809,6 +1811,21 @@ static int mt7996_set_rts_threshold(struct ieee80211_hw *hw, int radio_idx,
return ret;
}
+static void
+mt7996_sta_ba_update(u16 *mask, struct mt7996_sta_ba *ba,
+ struct ieee80211_ampdu_params *params, bool enable)
+{
+ if (!enable) {
+ *mask &= ~BIT(params->tid);
+ return;
+ }
+
+ *mask |= BIT(params->tid);
+ ba[params->tid].ssn = params->ssn;
+ ba[params->tid].buf_size = params->buf_size;
+ ba[params->tid].amsdu = params->amsdu;
+}
+
static int
mt7996_ampdu_action(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
struct ieee80211_ampdu_params *params)
@@ -1820,6 +1837,7 @@ mt7996_ampdu_action(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
u16 tid = params->tid;
u16 ssn = params->ssn;
struct mt76_txq *mtxq;
+ unsigned long links;
int ret = 0;
if (!txq)
@@ -1829,6 +1847,8 @@ mt7996_ampdu_action(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
mutex_lock(&dev->mt76.mutex);
+ links = mt7996_mac_sta_links(dev, vif, sta);
+
switch (params->action) {
case IEEE80211_AMPDU_RX_START:
/* Since packets belonging to the same TID can be split over
@@ -1837,22 +1857,30 @@ mt7996_ampdu_action(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
*/
mt76_rx_aggr_start(&dev->mt76, &msta->deflink.wcid, tid,
ssn, params->buf_size);
- ret = mt7996_mcu_add_rx_ba(dev, params, vif, true);
+ ret = mt7996_mcu_add_rx_ba(dev, params, vif, links, true);
+ mt7996_sta_ba_update(&msta->ba_rx_mask, msta->ba_rx, params,
+ !ret);
break;
case IEEE80211_AMPDU_RX_STOP:
mt76_rx_aggr_stop(&dev->mt76, &msta->deflink.wcid, tid);
- ret = mt7996_mcu_add_rx_ba(dev, params, vif, false);
+ mt7996_sta_ba_update(&msta->ba_rx_mask, msta->ba_rx, params,
+ false);
+ ret = mt7996_mcu_add_rx_ba(dev, params, vif, links, false);
break;
case IEEE80211_AMPDU_TX_OPERATIONAL:
mtxq->aggr = true;
mtxq->send_bar = false;
- ret = mt7996_mcu_add_tx_ba(dev, params, vif, true);
+ ret = mt7996_mcu_add_tx_ba(dev, params, vif, links, true);
+ mt7996_sta_ba_update(&msta->ba_tx_mask, msta->ba_tx, params,
+ !ret);
break;
case IEEE80211_AMPDU_TX_STOP_FLUSH:
case IEEE80211_AMPDU_TX_STOP_FLUSH_CONT:
mtxq->aggr = false;
clear_bit(tid, &msta->deflink.wcid.ampdu_state);
- ret = mt7996_mcu_add_tx_ba(dev, params, vif, false);
+ mt7996_sta_ba_update(&msta->ba_tx_mask, msta->ba_tx, params,
+ false);
+ ret = mt7996_mcu_add_tx_ba(dev, params, vif, links, false);
break;
case IEEE80211_AMPDU_TX_START:
set_bit(tid, &msta->deflink.wcid.ampdu_state);
@@ -1861,7 +1889,9 @@ mt7996_ampdu_action(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
case IEEE80211_AMPDU_TX_STOP_CONT:
mtxq->aggr = false;
clear_bit(tid, &msta->deflink.wcid.ampdu_state);
- ret = mt7996_mcu_add_tx_ba(dev, params, vif, false);
+ mt7996_sta_ba_update(&msta->ba_tx_mask, msta->ba_tx, params,
+ false);
+ ret = mt7996_mcu_add_tx_ba(dev, params, vif, links, false);
ieee80211_stop_tx_ba_cb_irqsafe(vif, sta->addr, tid);
break;
}
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
index 02f9bcfdf5d9..1d2ee1ca5298 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
@@ -1511,15 +1511,15 @@ mt7996_mcu_sta_ba(struct mt7996_dev *dev, struct mt76_vif_link *mvif,
/** starec & wtbl **/
int mt7996_mcu_add_tx_ba(struct mt7996_dev *dev,
struct ieee80211_ampdu_params *params,
- struct ieee80211_vif *vif, bool enable)
+ struct ieee80211_vif *vif, unsigned long links,
+ bool enable)
{
struct ieee80211_sta *sta = params->sta;
struct mt7996_sta *msta = (struct mt7996_sta *)sta->drv_priv;
- struct ieee80211_link_sta *link_sta;
unsigned int link_id;
int ret = 0;
- for_each_sta_active_link(vif, sta, link_sta, link_id) {
+ for_each_set_bit(link_id, &links, IEEE80211_MLD_MAX_NUM_LINKS) {
struct mt7996_sta_link *msta_link;
struct mt7996_vif_link *link;
@@ -1545,15 +1545,15 @@ int mt7996_mcu_add_tx_ba(struct mt7996_dev *dev,
int mt7996_mcu_add_rx_ba(struct mt7996_dev *dev,
struct ieee80211_ampdu_params *params,
- struct ieee80211_vif *vif, bool enable)
+ struct ieee80211_vif *vif, unsigned long links,
+ bool enable)
{
struct ieee80211_sta *sta = params->sta;
struct mt7996_sta *msta = (struct mt7996_sta *)sta->drv_priv;
- struct ieee80211_link_sta *link_sta;
unsigned int link_id;
int ret = 0;
- for_each_sta_active_link(vif, sta, link_sta, link_id) {
+ for_each_set_bit(link_id, &links, IEEE80211_MLD_MAX_NUM_LINKS) {
struct mt7996_sta_link *msta_link;
struct mt7996_vif_link *link;
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
index ead211391404..8744411e0717 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
@@ -264,6 +264,12 @@ struct mt7996_sta_link {
struct rcu_head rcu_head;
};
+struct mt7996_sta_ba {
+ u16 ssn;
+ u16 buf_size;
+ bool amsdu;
+};
+
struct mt7996_sta {
struct mt7996_sta_link deflink; /* must be first */
struct mt7996_sta_link __rcu *link[IEEE80211_MLD_MAX_NUM_LINKS];
@@ -273,6 +279,11 @@ struct mt7996_sta {
unsigned long wcid_flags;
+ u16 ba_tx_mask;
+ u16 ba_rx_mask;
+ struct mt7996_sta_ba ba_tx[IEEE80211_NUM_TIDS];
+ struct mt7996_sta_ba ba_rx[IEEE80211_NUM_TIDS];
+
struct mt7996_vif *vif;
};
@@ -733,10 +744,12 @@ int mt7996_mcu_teardown_mld_sta(struct mt7996_dev *dev,
void mt7996_mcu_update_sta_rec_bw(void *data, struct ieee80211_sta *sta);
int mt7996_mcu_add_tx_ba(struct mt7996_dev *dev,
struct ieee80211_ampdu_params *params,
- struct ieee80211_vif *vif, bool enable);
+ struct ieee80211_vif *vif, unsigned long links,
+ bool enable);
int mt7996_mcu_add_rx_ba(struct mt7996_dev *dev,
struct ieee80211_ampdu_params *params,
- struct ieee80211_vif *vif, bool enable);
+ struct ieee80211_vif *vif, unsigned long links,
+ bool enable);
int mt7996_mcu_update_bss_color(struct mt7996_dev *dev,
struct mt76_vif_link *mlink,
struct cfg80211_he_bss_color *he_bss_color);
--
2.47.3
^ permalink raw reply related [flat|nested] 27+ messages in thread* [PATCH mt76-next 21/27] wifi: mt76: mt7996: set up the BSS of a late station link before its peer
2026-10-06 20:59 [PATCH mt76-next 01/27] wifi: mt76: abort a scan only for a link change on the scanning radio Felix Fietkau
` (18 preceding siblings ...)
2026-10-06 20:59 ` [PATCH mt76-next 20/27] wifi: mt76: mt7996: keep the block ack session parameters per station Felix Fietkau
@ 2026-10-06 20:59 ` Felix Fietkau
2026-10-06 20:59 ` [PATCH mt76-next 22/27] wifi: mt76: mt7996: bring up station links added after association Felix Fietkau
` (5 subsequent siblings)
25 siblings, 0 replies; 27+ messages in thread
From: Felix Fietkau @ 2026-10-06 20:59 UTC (permalink / raw)
To: linux-wireless
On association, a station link gets its BSS_INFO with the BSSID and
its broadcast STA_REC from BSS_CHANGED_BSSID before the peer STA_REC is
created, and mt7996_link_info_changed() notes that station mode maps
the peer entry through the BSSID. For a link that becomes active later,
_ieee80211_set_active_links() calls drv_change_sta_links() first and
reports BSS_CHANGED_BSSID for the link only afterwards.
Send the BSS_INFO and the broadcast STA_REC of an added station link
from mt7996_mac_sta_change_links() before its peer entry is set up when
the station is already associated, in the order of an association. The
later BSS_CHANGED_BSSID finds the BSSID recorded and does not add the
broadcast entry as new again.
This prepares for bringing up such links in the following change.
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
.../net/wireless/mediatek/mt76/mt7996/main.c | 72 +++++++++++++------
1 file changed, 52 insertions(+), 20 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index d90ada7f0f2a..4f3ebe4cee65 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -1439,26 +1439,6 @@ mt7996_mac_sta_add_links(struct mt7996_dev *dev, struct ieee80211_vif *vif,
return err;
}
-static int
-mt7996_mac_sta_change_links(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
- struct ieee80211_sta *sta, u16 old_links,
- u16 new_links)
-{
- struct mt7996_dev *dev = mt7996_hw_dev(hw);
- unsigned long add = new_links & ~old_links;
- unsigned long rem = old_links & ~new_links;
- int ret;
-
- mutex_lock(&dev->mt76.mutex);
-
- mt7996_mac_sta_remove_links(dev, vif, sta, rem, false);
- ret = mt7996_mac_sta_add_links(dev, vif, sta, add);
-
- mutex_unlock(&dev->mt76.mutex);
-
- return ret;
-}
-
static int
mt7996_mac_sta_add(struct mt7996_dev *dev, struct ieee80211_vif *vif,
struct ieee80211_sta *sta)
@@ -1598,6 +1578,58 @@ mt7996_mac_sta_event(struct mt7996_dev *dev, struct ieee80211_vif *vif,
return err;
}
+static void
+mt7996_mac_sta_links_bss_add(struct mt7996_dev *dev, struct ieee80211_vif *vif,
+ struct ieee80211_sta *sta, unsigned long links)
+{
+ struct mt7996_sta *msta = (struct mt7996_sta *)sta->drv_priv;
+ unsigned int link_id;
+
+ if (vif->type != NL80211_IFTYPE_STATION ||
+ msta->conn_state == CONN_STATE_DISCONNECT)
+ return;
+
+ for_each_set_bit(link_id, &links, IEEE80211_MLD_MAX_NUM_LINKS) {
+ struct ieee80211_bss_conf *link_conf;
+ struct mt7996_vif_link *link;
+ struct mt7996_phy *phy;
+
+ link_conf = link_conf_dereference_protected(vif, link_id);
+ if (!link_conf || !link_conf->bssid ||
+ is_zero_ether_addr(link_conf->bssid))
+ continue;
+
+ link = mt7996_vif_link(dev, vif, link_id);
+ if (!link)
+ continue;
+
+ phy = mt7996_vif_link_phy(link);
+ if (phy)
+ mt7996_vif_link_bss_add(phy, vif, link_conf, link, true);
+ }
+}
+
+static int
+mt7996_mac_sta_change_links(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
+ struct ieee80211_sta *sta, u16 old_links,
+ u16 new_links)
+{
+ struct mt7996_dev *dev = mt7996_hw_dev(hw);
+ unsigned long add = new_links & ~old_links;
+ unsigned long rem = old_links & ~new_links;
+ int ret;
+
+ mutex_lock(&dev->mt76.mutex);
+
+ mt7996_mac_sta_remove_links(dev, vif, sta, rem, false);
+ mt7996_mac_sta_links_bss_add(dev, vif, sta, add);
+ ret = mt7996_mac_sta_add_links(dev, vif, sta, add);
+
+ mutex_unlock(&dev->mt76.mutex);
+
+ return ret;
+}
+
static void
mt7996_mac_sta_remove(struct mt7996_dev *dev, struct ieee80211_vif *vif,
struct ieee80211_sta *sta)
--
2.47.3
^ permalink raw reply related [flat|nested] 27+ messages in thread* [PATCH mt76-next 22/27] wifi: mt76: mt7996: bring up station links added after association
2026-10-06 20:59 [PATCH mt76-next 01/27] wifi: mt76: abort a scan only for a link change on the scanning radio Felix Fietkau
` (19 preceding siblings ...)
2026-10-06 20:59 ` [PATCH mt76-next 21/27] wifi: mt76: mt7996: set up the BSS of a late station link before its peer Felix Fietkau
@ 2026-10-06 20:59 ` Felix Fietkau
2026-10-06 20:59 ` [PATCH mt76-next 23/27] wifi: mt76: mt7996: set up block ack sessions on station links added later Felix Fietkau
` (4 subsequent siblings)
25 siblings, 0 replies; 27+ messages in thread
From: Felix Fietkau @ 2026-10-06 20:59 UTC (permalink / raw)
To: linux-wireless
mt7996_mac_sta_change_links() only sets up the driver state of a link
that is added to a station. The CONN_STATE_CONNECT and
CONN_STATE_PORT_SECURE STA_RECs and the rate control setup come from
mt7996_mac_sta_event(), which runs once per station state change, so a
link that becomes active after association has no firmware entry. This
is the normal case on a station interface: mac80211 activates only the
association link at first, and mt7996_sta_state() requests the other
links with ieee80211_set_active_links_async() when the station moves
to IEEE80211_STA_ASSOC. The STA_REC_MLD of the links already connected
also keeps the old set of links.
When links are added, replay the association on the links that are not
connected yet, install the keys of the station on them, and replay the
authorization if the station is authorized. mac80211 does not install
the pairwise key again, since ieee80211_key_switch_links() moves only
keys with key->link_id >= 0. Send the other links a STA_REC with only
STA_REC_MLD and STA_REC_EHT_MLD, so that their link list follows the
change. A 4-address link other than the primary link also gets the PS
leave request that mt7996_sta_set_4addr() sends on association.
If bringing up the added links fails, reset and remove them again
without flush, since the firmware can still hold an entry for their
WTBL index, and send the remaining links their STA_REC_MLD again.
While here, indent struct mt7996_key_iter_data with tabs.
Fixes: f520eceacd24 ("wifi: mt76: mt7996: Add mt7996_mac_sta_change_links callback")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
.../net/wireless/mediatek/mt76/mt7996/main.c | 152 ++++++++++++++++--
.../net/wireless/mediatek/mt76/mt7996/mcu.c | 21 +++
.../wireless/mediatek/mt76/mt7996/mt7996.h | 5 +
3 files changed, 166 insertions(+), 12 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index 4f3ebe4cee65..324d3b195a3f 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -277,8 +277,9 @@ mt7996_set_hw_key(struct ieee80211_hw *hw, enum set_key_cmd cmd,
}
struct mt7996_key_iter_data {
- enum set_key_cmd cmd;
- unsigned int link_id;
+ enum set_key_cmd cmd;
+ unsigned int link_id;
+ struct ieee80211_sta *sta;
};
static void
@@ -288,13 +289,13 @@ mt7996_key_iter(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
{
struct mt7996_key_iter_data *it = data;
- if (sta)
+ if (sta != it->sta)
return;
if (key->link_id >= 0 && key->link_id != it->link_id)
return;
- WARN_ON(mt7996_set_hw_key(hw, it->cmd, vif, NULL, it->link_id, key));
+ WARN_ON(mt7996_set_hw_key(hw, it->cmd, vif, sta, it->link_id, key));
}
int mt7996_vif_link_add(struct mt76_phy *mphy, struct ieee80211_vif *vif,
@@ -1530,6 +1531,23 @@ mt7996_mac_sta_links_event(struct mt7996_dev *dev, struct ieee80211_vif *vif,
return 0;
}
+static void
+mt7996_mac_sta_link_ps_leave(struct mt7996_dev *dev,
+ struct mt7996_vif_link *link,
+ struct mt7996_sta_link *msta_link)
+{
+ /* HW cannot derive the BSSID from 4-address non-AMSDU frames,
+ * so PS exit is missed on links other than the setup link.
+ * Let the firmware wake those links instead.
+ */
+ if (!is_mt7996(&dev->mt76) ||
+ !test_bit(MT_WCID_FLAG_4ADDR, &msta_link->wcid.flags) ||
+ msta_link->wcid.link_id == msta_link->sta->deflink_id)
+ return;
+
+ mt7996_mcu_ps_leave(dev, link, msta_link);
+}
+
static void
mt7996_mac_sta_clear_connected(struct mt7996_dev *dev, struct mt7996_sta *msta)
{
@@ -1578,6 +1596,113 @@ mt7996_mac_sta_event(struct mt7996_dev *dev, struct ieee80211_vif *vif,
return err;
}
+static void
+mt7996_mac_sta_links_set_keys(struct mt7996_dev *dev,
+ struct ieee80211_vif *vif,
+ struct ieee80211_sta *sta, unsigned long links)
+{
+ unsigned int link_id;
+
+ for_each_set_bit(link_id, &links, IEEE80211_MLD_MAX_NUM_LINKS) {
+ struct mt7996_key_iter_data it = {
+ .cmd = SET_KEY,
+ .link_id = link_id,
+ .sta = sta,
+ };
+
+ ieee80211_iter_keys(mt76_hw(dev), vif, mt7996_key_iter, &it);
+ }
+}
+
+static int
+mt7996_mac_sta_links_sync(struct mt7996_dev *dev, struct ieee80211_vif *vif,
+ struct ieee80211_sta *sta, unsigned long links,
+ unsigned long add)
+{
+ struct mt7996_sta *msta = (struct mt7996_sta *)sta->drv_priv;
+ unsigned long new_links = 0, updated;
+ unsigned int link_id;
+ int err;
+
+ if (msta->conn_state == CONN_STATE_DISCONNECT)
+ return 0;
+
+ links &= mt7996_mac_sta_links(dev, vif, sta);
+ for_each_set_bit(link_id, &links, IEEE80211_MLD_MAX_NUM_LINKS) {
+ struct mt7996_sta_link *msta_link;
+
+ msta_link = mt7996_sta_link_protected(dev, msta, link_id);
+ if (!msta_link->connected)
+ new_links |= BIT(link_id);
+ }
+
+ err = mt7996_mac_sta_links_event(dev, vif, sta, new_links,
+ MT76_STA_EVENT_ASSOC);
+ if (err)
+ return err;
+
+ mt7996_mac_sta_links_set_keys(dev, vif, sta, links & add);
+
+ updated = new_links;
+ if (msta->conn_state == CONN_STATE_PORT_SECURE) {
+ err = mt7996_mac_sta_links_event(dev, vif, sta, updated,
+ MT76_STA_EVENT_AUTHORIZE);
+ if (err)
+ return err;
+ }
+
+ for_each_set_bit(link_id, &new_links, IEEE80211_MLD_MAX_NUM_LINKS) {
+ struct mt7996_sta_link *msta_link;
+ struct mt7996_vif_link *link;
+
+ msta_link = mt7996_sta_link_protected(dev, msta, link_id);
+ link = mt7996_vif_link(dev, vif, link_id);
+ mt7996_mac_sta_link_ps_leave(dev, link, msta_link);
+ }
+
+ updated = sta->mlo ? links & ~updated : 0;
+ for_each_set_bit(link_id, &updated, IEEE80211_MLD_MAX_NUM_LINKS) {
+ struct mt7996_sta_link *msta_link;
+ struct mt7996_vif_link *link;
+
+ msta_link = mt7996_sta_link_protected(dev, msta, link_id);
+ link = mt7996_vif_link(dev, vif, link_id);
+ err = mt7996_mcu_update_mld_sta(dev, vif, sta, link, msta_link);
+ if (err)
+ return err;
+ }
+
+ return 0;
+}
+
+static void
+mt7996_mac_sta_links_reset(struct mt7996_dev *dev, struct ieee80211_vif *vif,
+ struct ieee80211_sta *sta, unsigned long links)
+{
+ struct mt7996_sta *msta = (struct mt7996_sta *)sta->drv_priv;
+ unsigned int link_id;
+
+ for_each_set_bit(link_id, &links, IEEE80211_MLD_MAX_NUM_LINKS) {
+ struct mt7996_sta_link *msta_link;
+ struct mt7996_vif_link *link;
+ int i;
+
+ msta_link = mt7996_sta_link_protected(dev, msta, link_id);
+ if (!msta_link)
+ continue;
+
+ link = mt7996_vif_link(dev, vif, link_id);
+ if (link) {
+ for (i = 0; i < ARRAY_SIZE(msta_link->twt.flow); i++)
+ mt7996_mac_twt_teardown_flow(dev, link,
+ msta_link, i);
+ }
+
+ msta_link->wcid.tx_info &= ~MT_WCID_TX_INFO_SET;
+ msta_link->wcid.hw_key_idx = -1;
+ }
+}
+
static void
mt7996_mac_sta_links_bss_add(struct mt7996_dev *dev, struct ieee80211_vif *vif,
struct ieee80211_sta *sta, unsigned long links)
@@ -1624,7 +1749,17 @@ mt7996_mac_sta_change_links(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
mt7996_mac_sta_remove_links(dev, vif, sta, rem, false);
mt7996_mac_sta_links_bss_add(dev, vif, sta, add);
ret = mt7996_mac_sta_add_links(dev, vif, sta, add);
+ if (ret)
+ goto out;
+ ret = mt7996_mac_sta_links_sync(dev, vif, sta, new_links, add);
+ if (ret) {
+ mt7996_mac_sta_links_reset(dev, vif, sta, add);
+ mt7996_mac_sta_remove_links(dev, vif, sta, add, false);
+ mt7996_mac_sta_links_sync(dev, vif, sta, new_links & ~add, 0);
+ }
+
+out:
mutex_unlock(&dev->mt76.mutex);
return ret;
@@ -2300,14 +2435,7 @@ static void mt7996_sta_set_4addr(struct ieee80211_hw *hw,
continue;
mt7996_mcu_wtbl_update_hdr_trans(dev, vif, link, msta_link);
-
- /* HW cannot derive the BSSID from 4-address non-AMSDU frames,
- * so PS exit is missed on links other than the setup link.
- * Let the firmware wake those links instead.
- */
- if (enabled && msta->deflink_id != link_id &&
- is_mt7996(&dev->mt76))
- mt7996_mcu_ps_leave(dev, link, msta_link);
+ mt7996_mac_sta_link_ps_leave(dev, link, msta_link);
}
mutex_unlock(&dev->mt76.mutex);
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
index 1d2ee1ca5298..c324d199b212 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
@@ -2910,6 +2910,27 @@ int mt7996_mcu_teardown_mld_sta(struct mt7996_dev *dev,
MCU_WMWA_UNI_CMD(STA_REC_UPDATE), true);
}
+int mt7996_mcu_update_mld_sta(struct mt7996_dev *dev,
+ struct ieee80211_vif *vif,
+ struct ieee80211_sta *sta,
+ struct mt7996_vif_link *link,
+ struct mt7996_sta_link *msta_link)
+{
+ struct sk_buff *skb;
+
+ skb = __mt76_connac_mcu_alloc_sta_req(&dev->mt76, &link->mt76,
+ &msta_link->wcid,
+ MT7996_STA_UPDATE_MAX_SIZE);
+ if (IS_ERR(skb))
+ return PTR_ERR(skb);
+
+ mt7996_mcu_sta_mld_setup_tlv(dev, skb, vif, sta);
+ mt7996_mcu_sta_eht_mld_tlv(dev, skb, sta);
+
+ return mt76_mcu_skb_send_msg(&dev->mt76, skb,
+ MCU_WMWA_UNI_CMD(STA_REC_UPDATE), true);
+}
+
void mt7996_mcu_update_sta_rec_bw(void *data, struct ieee80211_sta *sta)
{
struct mt7996_vif_link *link = (struct mt7996_vif_link *)data;
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
index 8744411e0717..ae97ca091673 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
@@ -741,6 +741,11 @@ int mt7996_mcu_add_sta(struct mt7996_dev *dev,
int mt7996_mcu_teardown_mld_sta(struct mt7996_dev *dev,
struct mt7996_vif_link *link,
struct mt7996_sta_link *msta_link);
+int mt7996_mcu_update_mld_sta(struct mt7996_dev *dev,
+ struct ieee80211_vif *vif,
+ struct ieee80211_sta *sta,
+ struct mt7996_vif_link *link,
+ struct mt7996_sta_link *msta_link);
void mt7996_mcu_update_sta_rec_bw(void *data, struct ieee80211_sta *sta);
int mt7996_mcu_add_tx_ba(struct mt7996_dev *dev,
struct ieee80211_ampdu_params *params,
--
2.47.3
^ permalink raw reply related [flat|nested] 27+ messages in thread* [PATCH mt76-next 23/27] wifi: mt76: mt7996: set up block ack sessions on station links added later
2026-10-06 20:59 [PATCH mt76-next 01/27] wifi: mt76: abort a scan only for a link change on the scanning radio Felix Fietkau
` (20 preceding siblings ...)
2026-10-06 20:59 ` [PATCH mt76-next 22/27] wifi: mt76: mt7996: bring up station links added after association Felix Fietkau
@ 2026-10-06 20:59 ` Felix Fietkau
2026-10-06 20:59 ` [PATCH mt76-next 24/27] wifi: mt76: mt7996: restore a station link that returns after a removal Felix Fietkau
` (3 subsequent siblings)
25 siblings, 0 replies; 27+ messages in thread
From: Felix Fietkau @ 2026-10-06 20:59 UTC (permalink / raw)
To: linux-wireless
mt7996_ampdu_action() sends STA_REC_BA to the links that the station
has when mac80211 starts a session. A link that is brought up later
gets no STA_REC_BA, so it transmits without aggregation and, as a
recipient, is not set up for hardware reordering. mac80211 does not
start the sessions again when a link is added.
Send STA_REC_BA for each running TX and RX session on the links that
mt7996_mac_sta_change_links() brings up. For an RX session, use the
current head of the reorder window, which mt76_rx_aggr_start() keeps
on the default link, as the starting sequence number.
That head moves only when the host reorders: with hardware RRO, mt7996
marks the frames as already aggregated and mt7996_mac_fill_rx() skips
the host reorder state; with WED or the NPU active,
mt76_rx_poll_complete() does not call mt76_rx_aggr_reorder(). The
driver has no MCU query for the current window. In those modes, stop
the RX sessions with ieee80211_stop_rx_ba_session() instead, so that
the originator sets up new sessions, which then reach all links through
the regular IEEE80211_AMPDU_RX_START path.
Fixes: ef3f5941e560 ("wifi: mt76: mt7996: rework mt7996_ampdu_action to support MLO")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
.../net/wireless/mediatek/mt76/mt7996/main.c | 81 ++++++++++++++++++-
1 file changed, 80 insertions(+), 1 deletion(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index 324d3b195a3f..89e9ef855a78 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -1614,6 +1614,85 @@ mt7996_mac_sta_links_set_keys(struct mt7996_dev *dev,
}
}
+static bool
+mt7996_rx_reorder_host(struct mt7996_dev *dev)
+{
+ return !mt7996_has_hwrro(dev) &&
+ !mtk_wed_device_active(&dev->mt76.mmio.wed) &&
+ !mt76_npu_device_active(&dev->mt76);
+}
+
+static u16
+mt7996_mac_sta_ba_rx_ssn(struct mt7996_dev *dev, struct mt7996_sta *msta,
+ unsigned int tid)
+{
+ struct mt76_rx_tid *tid_rx;
+ u16 ssn;
+
+ tid_rx = mt76_dereference(msta->deflink.wcid.aggr[tid], &dev->mt76);
+ if (!tid_rx)
+ return msta->ba_rx[tid].ssn;
+
+ spin_lock_bh(&tid_rx->lock);
+ ssn = tid_rx->head;
+ spin_unlock_bh(&tid_rx->lock);
+
+ return ssn;
+}
+
+static int
+mt7996_mac_sta_links_ba_add(struct mt7996_dev *dev, struct ieee80211_vif *vif,
+ struct ieee80211_sta *sta, unsigned long links)
+{
+ struct mt7996_sta *msta = (struct mt7996_sta *)sta->drv_priv;
+ u16 rx_restart = 0;
+ unsigned int tid;
+ int err;
+
+ if (!links)
+ return 0;
+
+ for (tid = 0; tid < IEEE80211_NUM_TIDS; tid++) {
+ struct ieee80211_ampdu_params params = {
+ .sta = sta,
+ .tid = tid,
+ };
+
+ if (msta->ba_tx_mask & BIT(tid)) {
+ params.ssn = msta->ba_tx[tid].ssn;
+ params.buf_size = msta->ba_tx[tid].buf_size;
+ params.amsdu = msta->ba_tx[tid].amsdu;
+ err = mt7996_mcu_add_tx_ba(dev, ¶ms, vif, links,
+ true);
+ if (err)
+ return err;
+ }
+
+ if (!(msta->ba_rx_mask & BIT(tid)))
+ continue;
+
+ /* without host reordering, the reorder head stays at the
+ * session start, so have the originator set up a new session
+ */
+ if (!mt7996_rx_reorder_host(dev)) {
+ rx_restart |= BIT(tid);
+ continue;
+ }
+
+ params.ssn = mt7996_mac_sta_ba_rx_ssn(dev, msta, tid);
+ params.buf_size = msta->ba_rx[tid].buf_size;
+ params.amsdu = msta->ba_rx[tid].amsdu;
+ err = mt7996_mcu_add_rx_ba(dev, ¶ms, vif, links, true);
+ if (err)
+ return err;
+ }
+
+ if (rx_restart)
+ ieee80211_stop_rx_ba_session(vif, rx_restart, sta->addr);
+
+ return 0;
+}
+
static int
mt7996_mac_sta_links_sync(struct mt7996_dev *dev, struct ieee80211_vif *vif,
struct ieee80211_sta *sta, unsigned long links,
@@ -1672,7 +1751,7 @@ mt7996_mac_sta_links_sync(struct mt7996_dev *dev, struct ieee80211_vif *vif,
return err;
}
- return 0;
+ return mt7996_mac_sta_links_ba_add(dev, vif, sta, new_links);
}
static void
--
2.47.3
^ permalink raw reply related [flat|nested] 27+ messages in thread* [PATCH mt76-next 24/27] wifi: mt76: mt7996: restore a station link that returns after a removal
2026-10-06 20:59 [PATCH mt76-next 01/27] wifi: mt76: abort a scan only for a link change on the scanning radio Felix Fietkau
` (21 preceding siblings ...)
2026-10-06 20:59 ` [PATCH mt76-next 23/27] wifi: mt76: mt7996: set up block ack sessions on station links added later Felix Fietkau
@ 2026-10-06 20:59 ` Felix Fietkau
2026-10-06 20:59 ` [PATCH mt76-next 25/27] wifi: mt76: mt7996: fix PLE free page and AC queue register offsets Felix Fietkau
` (2 subsequent siblings)
25 siblings, 0 replies; 27+ messages in thread
From: Felix Fietkau @ 2026-10-06 20:59 UTC (permalink / raw)
To: linux-wireless
When mt7996_mac_sta_change_links() removes a link from a station, it
keeps the msta_link and the firmware entry of the link. Commit
e077071e7ac4 ("wifi: mt76: mt7996: fix teardown command for an MLD
peer") removed the per-link DISCONNECT command for MLD peers, and a link
that comes back must not get a second STA_REC with EXTRA_INFO_NEW,
which commit 8e3e7567b8c1 ("mt76: mt7915: add sta_rec with
EXTRA_INFO_NEW for the first time only") says can remove the entry. A
link that comes back is therefore not connected again, and gets no rate
control, keys or authorization.
On removal, reset only the driver state of the link: its TWT flows, the
TX state and the key index. When the link comes back with its entry
still connected, send it the header translation state, rate control,
its keys and, for an authorized station, the CONN_STATE_PORT_SECURE
update without EXTRA_INFO_NEW, which also carries the header
translation state. Send the 4-address PS leave request and the block
ack sessions as for a new link, since the link can come back as a link
other than the primary link.
Fixes: e8c819df0243 ("wifi: mt76: mt7996: Destroy active sta links in mt7996_mac_sta_remove()")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
.../net/wireless/mediatek/mt76/mt7996/main.c | 33 +++++++++++++++++--
1 file changed, 30 insertions(+), 3 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index 89e9ef855a78..9965ac136bec 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -1263,6 +1263,7 @@ mt7996_mac_sta_init_link(struct mt7996_dev *dev,
INIT_LIST_HEAD(&msta_link->wcid.poll_list);
msta_link->sta = msta;
msta_link->connected = false;
+ msta_link->wcid.tx_info = 0;
msta_link->wcid.sta = 1;
msta_link->wcid.idx = idx;
msta_link->wcid.link_id = link_id;
@@ -1699,7 +1700,7 @@ mt7996_mac_sta_links_sync(struct mt7996_dev *dev, struct ieee80211_vif *vif,
unsigned long add)
{
struct mt7996_sta *msta = (struct mt7996_sta *)sta->drv_priv;
- unsigned long new_links = 0, updated;
+ unsigned long new_links = 0, resumed = 0, joined, updated;
unsigned int link_id;
int err;
@@ -1713,6 +1714,8 @@ mt7996_mac_sta_links_sync(struct mt7996_dev *dev, struct ieee80211_vif *vif,
msta_link = mt7996_sta_link_protected(dev, msta, link_id);
if (!msta_link->connected)
new_links |= BIT(link_id);
+ else if (!(msta_link->wcid.tx_info & MT_WCID_TX_INFO_SET))
+ resumed |= BIT(link_id);
}
err = mt7996_mac_sta_links_event(dev, vif, sta, new_links,
@@ -1720,17 +1723,40 @@ mt7996_mac_sta_links_sync(struct mt7996_dev *dev, struct ieee80211_vif *vif,
if (err)
return err;
+ for_each_set_bit(link_id, &resumed, IEEE80211_MLD_MAX_NUM_LINKS) {
+ struct mt7996_sta_link *msta_link;
+ struct mt7996_vif_link *link;
+
+ msta_link = mt7996_sta_link_protected(dev, msta, link_id);
+ link = mt7996_vif_link(dev, vif, link_id);
+
+ if (msta->conn_state != CONN_STATE_PORT_SECURE) {
+ err = mt7996_mcu_wtbl_update_hdr_trans(dev, vif, link,
+ msta_link);
+ if (err)
+ return err;
+ }
+
+ err = mt7996_mcu_add_rate_ctrl(dev, msta, vif, link_id, false);
+ if (err)
+ return err;
+
+ msta_link->wcid.tx_info |= MT_WCID_TX_INFO_SET;
+ }
+
mt7996_mac_sta_links_set_keys(dev, vif, sta, links & add);
updated = new_links;
if (msta->conn_state == CONN_STATE_PORT_SECURE) {
+ updated |= resumed;
err = mt7996_mac_sta_links_event(dev, vif, sta, updated,
MT76_STA_EVENT_AUTHORIZE);
if (err)
return err;
}
- for_each_set_bit(link_id, &new_links, IEEE80211_MLD_MAX_NUM_LINKS) {
+ joined = new_links | resumed;
+ for_each_set_bit(link_id, &joined, IEEE80211_MLD_MAX_NUM_LINKS) {
struct mt7996_sta_link *msta_link;
struct mt7996_vif_link *link;
@@ -1751,7 +1777,7 @@ mt7996_mac_sta_links_sync(struct mt7996_dev *dev, struct ieee80211_vif *vif,
return err;
}
- return mt7996_mac_sta_links_ba_add(dev, vif, sta, new_links);
+ return mt7996_mac_sta_links_ba_add(dev, vif, sta, new_links | resumed);
}
static void
@@ -1825,6 +1851,7 @@ mt7996_mac_sta_change_links(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
mutex_lock(&dev->mt76.mutex);
+ mt7996_mac_sta_links_reset(dev, vif, sta, rem);
mt7996_mac_sta_remove_links(dev, vif, sta, rem, false);
mt7996_mac_sta_links_bss_add(dev, vif, sta, add);
ret = mt7996_mac_sta_add_links(dev, vif, sta, add);
--
2.47.3
^ permalink raw reply related [flat|nested] 27+ messages in thread* [PATCH mt76-next 25/27] wifi: mt76: mt7996: fix PLE free page and AC queue register offsets
2026-10-06 20:59 [PATCH mt76-next 01/27] wifi: mt76: abort a scan only for a link change on the scanning radio Felix Fietkau
` (22 preceding siblings ...)
2026-10-06 20:59 ` [PATCH mt76-next 24/27] wifi: mt76: mt7996: restore a station link that returns after a removal Felix Fietkau
@ 2026-10-06 20:59 ` Felix Fietkau
2026-10-06 20:59 ` [PATCH mt76-next 26/27] wifi: mt76: mt7996: fix the hw-queues debugfs queue requests Felix Fietkau
2026-10-06 20:59 ` [PATCH mt76-next 27/27] wifi: mt76: mt7996: disable the RX NAPIs before the DMA cleanup Felix Fietkau
25 siblings, 0 replies; 27+ messages in thread
From: Felix Fietkau @ 2026-10-06 20:59 UTC (permalink / raw)
To: linux-wireless
The PLE register map in the MediaTek vendor driver places the free
page counter, the free page head and tail, and the HIF group page status
at 0x3a0, 0x3a4 and 0x3a8. The driver reads them at 0x380, 0x384 and
0x388, and the vendor map has the band 1 TXCMD queue empty status at
0x388. The same map has 0x100 bytes per AC for the station queue empty
bitmaps, with the MT7992 extension register at offset 0x80. The driver
uses 0x80, which cannot hold the 34 registers per AC of MT7996, so the
hw-queues file reads AC1 to AC3 of a station from the wrong register.
Use the offsets of the vendor register map.
Fixes: 98686cd21624 ("wifi: mt76: mt7996: add driver for MediaTek Wi-Fi 7 (802.11be) devices")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
drivers/net/wireless/mediatek/mt76/mt7996/regs.h | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/regs.h b/drivers/net/wireless/mediatek/mt76/mt7996/regs.h
index 8ff78cf6eb04..30bec7a0a987 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/regs.h
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/regs.h
@@ -155,12 +155,12 @@ enum offs_rev {
#define MT_FL_Q2_CTRL MT_PLE(0x3e8)
#define MT_FL_Q3_CTRL MT_PLE(0x3ec)
-#define MT_PLE_FREEPG_CNT MT_PLE(0x380)
-#define MT_PLE_FREEPG_HEAD_TAIL MT_PLE(0x384)
+#define MT_PLE_FREEPG_CNT MT_PLE(0x3a0)
+#define MT_PLE_FREEPG_HEAD_TAIL MT_PLE(0x3a4)
#define MT_PLE_PG_HIF_GROUP MT_PLE(0x00c)
-#define MT_PLE_HIF_PG_INFO MT_PLE(0x388)
+#define MT_PLE_HIF_PG_INFO MT_PLE(0x3a8)
-#define MT_PLE_AC_QEMPTY(ac, n) MT_PLE(0x600 + 0x80 * (ac) + ((n) << 2))
+#define MT_PLE_AC_QEMPTY(ac, n) MT_PLE(0x600 + 0x100 * (ac) + ((n) << 2))
#define MT_PLE_AMSDU_PACK_MSDU_CNT(n) MT_PLE(0x10e0 + ((n) << 2))
/* WF MDP TOP */
--
2.47.3
^ permalink raw reply related [flat|nested] 27+ messages in thread* [PATCH mt76-next 26/27] wifi: mt76: mt7996: fix the hw-queues debugfs queue requests
2026-10-06 20:59 [PATCH mt76-next 01/27] wifi: mt76: abort a scan only for a link change on the scanning radio Felix Fietkau
` (23 preceding siblings ...)
2026-10-06 20:59 ` [PATCH mt76-next 25/27] wifi: mt76: mt7996: fix PLE free page and AC queue register offsets Felix Fietkau
@ 2026-10-06 20:59 ` Felix Fietkau
2026-10-06 20:59 ` [PATCH mt76-next 27/27] wifi: mt76: mt7996: disable the RX NAPIs before the DMA cleanup Felix Fietkau
25 siblings, 0 replies; 27+ messages in thread
From: Felix Fietkau @ 2026-10-06 20:59 UTC (permalink / raw)
To: linux-wireless
The hw-queues debugfs file builds its own queue requests, and these do
not match the register map of the MediaTek vendor driver:
- The port ID goes to bits 11:10 of the PLE queue control register, but
the field is at bits 17:16. Bits 11:10 are part of the WLAN index, so
each request reads port 0 and, for a station, the WLAN index with
bit 11 set.
- The head and tail frame IDs, the packet count and the page counters
are read as 12-bit fields, but they have 13 bits.
- The PSE queues are read from the PLE empty and queue control
registers, with bit positions that do not match the PSE queue map.
- The station queues of MT7992 and MT7990 with a WLAN index from 512 to
543 are read from register 16, but these chips keep that group in the
extension register at index 32.
- The file iterates the stations of the same ieee80211_hw once for each
band, so it shows each station queue up to three times.
- mt7996_hw_queue_read() takes the private data of the file as a
struct mt7996_phy, but it is a struct mt7996_dev. The dev member
of the phy then aliases mt76.phy.dev of the device, which points back
to the device, so the file works by chance.
A queue request also writes the queue control register 0 and then reads
the result from the queue control registers 2 and 3, without a lock.
When the global and a per-station hw-queues file are read at the same
time, one reader can get the result of the request of the other.
Describe the PLE and PSE queues in tables with the field layout of the
vendor register map, and send all queue requests through one helper,
which holds a spinlock from the write of the request to the read of its
result. A spinlock is needed because the station requests are sent from
ieee80211_iterate_stations_atomic(). Iterate the stations once.
Fixes: 98686cd21624 ("wifi: mt76: mt7996: add driver for MediaTek Wi-Fi 7 (802.11be) devices")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
.../wireless/mediatek/mt76/mt7996/debugfs.c | 319 +++++++++++-------
.../net/wireless/mediatek/mt76/mt7996/regs.h | 28 ++
2 files changed, 230 insertions(+), 117 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/debugfs.c b/drivers/net/wireless/mediatek/mt76/mt7996/debugfs.c
index b85c3e549f8b..644abd0a0216 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/debugfs.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/debugfs.c
@@ -13,13 +13,6 @@
/** global debugfs **/
-struct hw_queue_map {
- const char *name;
- u8 index;
- u8 pid;
- u8 qid;
-};
-
static int
mt7996_implicit_txbf_set(void *data, u64 val)
{
@@ -604,87 +597,230 @@ mt7996_tx_stats_show(struct seq_file *file, void *data)
DEFINE_SHOW_ATTRIBUTE(mt7996_tx_stats);
+enum {
+ MT_QUEUE_PORT_HIF,
+ MT_QUEUE_PORT_CPU,
+ MT_QUEUE_PORT_LMAC,
+ MT_QUEUE_PORT_PLE_CTRL,
+};
+
+struct mt7996_hw_queue {
+ const char *name;
+ u8 pid;
+ u8 qid;
+ u8 tgid;
+};
+
+#define MT7996_HW_QUEUE(_name, _pid, _qid, _tgid) \
+ { .name = _name, .pid = MT_QUEUE_PORT_##_pid, .qid = _qid, .tgid = _tgid }
+
+static const struct mt7996_hw_queue mt7996_ple_queues[32] = {
+ [0] = MT7996_HW_QUEUE("CPU Q0", CPU, 0, 0),
+ [1] = MT7996_HW_QUEUE("CPU Q1", CPU, 1, 0),
+ [2] = MT7996_HW_QUEUE("CPU Q2", CPU, 2, 0),
+ [3] = MT7996_HW_QUEUE("CPU Q3", CPU, 3, 0),
+ [4] = MT7996_HW_QUEUE("ALTX Q0", LMAC, 0x10, 0),
+ [5] = MT7996_HW_QUEUE("BMC Q0", LMAC, 0x11, 0),
+ [6] = MT7996_HW_QUEUE("BCN Q0", LMAC, 0x12, 0),
+ [7] = MT7996_HW_QUEUE("PSMP Q0", LMAC, 0x13, 0),
+ [8] = MT7996_HW_QUEUE("ALTX Q1", LMAC, 0x10, 1),
+ [9] = MT7996_HW_QUEUE("BMC Q1", LMAC, 0x11, 1),
+ [10] = MT7996_HW_QUEUE("BCN Q1", LMAC, 0x12, 1),
+ [11] = MT7996_HW_QUEUE("PSMP Q1", LMAC, 0x13, 1),
+ [12] = MT7996_HW_QUEUE("ALTX Q2", LMAC, 0x1c, 2),
+ [13] = MT7996_HW_QUEUE("BMC Q2", LMAC, 0x1d, 2),
+ [14] = MT7996_HW_QUEUE("BCN Q2", LMAC, 0x1e, 2),
+ [15] = MT7996_HW_QUEUE("PSMP Q2", LMAC, 0x1f, 2),
+ [16] = MT7996_HW_QUEUE("NAF Q", LMAC, 0x18, 0),
+ [17] = MT7996_HW_QUEUE("NBCN Q", LMAC, 0x19, 0),
+ [20] = MT7996_HW_QUEUE("FIXFID Q", LMAC, 0x1a, 0),
+ [28] = MT7996_HW_QUEUE("RLS4 Q", PLE_CTRL, 0x7c, 0),
+ [29] = MT7996_HW_QUEUE("RLS3 Q", PLE_CTRL, 0x7d, 0),
+ [30] = MT7996_HW_QUEUE("RLS2 Q", PLE_CTRL, 0x7e, 0),
+ [31] = MT7996_HW_QUEUE("RLS Q", PLE_CTRL, 0x7f, 0),
+};
+
+static const struct mt7996_hw_queue mt7996_pse_queues[32] = {
+ [0] = MT7996_HW_QUEUE("CPU Q0", CPU, 0, 0),
+ [1] = MT7996_HW_QUEUE("CPU Q1", CPU, 1, 0),
+ [2] = MT7996_HW_QUEUE("CPU Q2", CPU, 2, 0),
+ [3] = MT7996_HW_QUEUE("CPU Q3", CPU, 3, 0),
+ [16] = MT7996_HW_QUEUE("LMAC Q", LMAC, 0, 0),
+ [17] = MT7996_HW_QUEUE("MDP TX Q0", LMAC, 1, 0),
+ [18] = MT7996_HW_QUEUE("MDP RX Q", LMAC, 2, 0),
+ [19] = MT7996_HW_QUEUE("SEC TX Q0", LMAC, 3, 0),
+ [20] = MT7996_HW_QUEUE("SEC RX Q", LMAC, 4, 0),
+ [21] = MT7996_HW_QUEUE("SFD PARK Q", LMAC, 5, 0),
+ [22] = MT7996_HW_QUEUE("MDP TXIOC Q0", LMAC, 6, 0),
+ [23] = MT7996_HW_QUEUE("MDP RXIOC Q0", LMAC, 7, 0),
+ [24] = MT7996_HW_QUEUE("MDP TX Q1", LMAC, 0x11, 0),
+ [25] = MT7996_HW_QUEUE("SEC TX Q1", LMAC, 0x13, 0),
+ [26] = MT7996_HW_QUEUE("MDP TXIOC Q1", LMAC, 0x16, 0),
+ [27] = MT7996_HW_QUEUE("MDP RXIOC Q1", LMAC, 0x17, 0),
+ [28] = MT7996_HW_QUEUE("CPU Q4", CPU, 4, 0),
+ [31] = MT7996_HW_QUEUE("RLS Q", PLE_CTRL, 0x1f, 0),
+};
+
+static const struct mt7996_hw_queue mt7996_pse_queues_1[32] = {
+ [0] = MT7996_HW_QUEUE("MDP TDPIOC Q0", LMAC, 0x08, 0),
+ [1] = MT7996_HW_QUEUE("MDP RDPIOC Q0", LMAC, 0x09, 0),
+ [2] = MT7996_HW_QUEUE("MDP TDPIOC Q1", LMAC, 0x18, 0),
+ [3] = MT7996_HW_QUEUE("MDP RDPIOC Q1", LMAC, 0x19, 0),
+ [4] = MT7996_HW_QUEUE("MDP TDPIOC Q2", LMAC, 0x28, 0),
+ [5] = MT7996_HW_QUEUE("MDP RDPIOC Q2", LMAC, 0x29, 0),
+ [7] = MT7996_HW_QUEUE("MDP RDPIOC Q3", LMAC, 0x39, 0),
+ [8] = MT7996_HW_QUEUE("MDP TX Q2", LMAC, 0x21, 0),
+ [9] = MT7996_HW_QUEUE("SEC TX Q2", LMAC, 0x23, 0),
+ [10] = MT7996_HW_QUEUE("MDP TXIOC Q2", LMAC, 0x26, 0),
+ [11] = MT7996_HW_QUEUE("MDP RXIOC Q2", LMAC, 0x27, 0),
+ [15] = MT7996_HW_QUEUE("MDP RXIOC Q3", LMAC, 0x37, 0),
+ [16] = MT7996_HW_QUEUE("HIF Q0", HIF, 0, 0),
+ [17] = MT7996_HW_QUEUE("HIF Q1", HIF, 1, 0),
+ [18] = MT7996_HW_QUEUE("HIF Q2", HIF, 2, 0),
+ [19] = MT7996_HW_QUEUE("HIF Q3", HIF, 3, 0),
+ [20] = MT7996_HW_QUEUE("HIF Q4", HIF, 4, 0),
+ [21] = MT7996_HW_QUEUE("HIF Q5", HIF, 5, 0),
+ [22] = MT7996_HW_QUEUE("HIF Q6", HIF, 6, 0),
+ [23] = MT7996_HW_QUEUE("HIF Q7", HIF, 7, 0),
+ [24] = MT7996_HW_QUEUE("HIF Q8", HIF, 8, 0),
+ [25] = MT7996_HW_QUEUE("HIF Q9", HIF, 9, 0),
+ [26] = MT7996_HW_QUEUE("HIF Q10", HIF, 10, 0),
+ [27] = MT7996_HW_QUEUE("HIF Q11", HIF, 11, 0),
+ [28] = MT7996_HW_QUEUE("HIF Q12", HIF, 12, 0),
+ [29] = MT7996_HW_QUEUE("HIF Q13", HIF, 13, 0),
+};
+
static void
-mt7996_hw_queue_read(struct seq_file *s, u32 size,
- const struct hw_queue_map *map)
+mt7996_fl_queue_show(struct seq_file *s, struct mt7996_dev *dev, bool pse,
+ const struct mt7996_hw_queue *q, u16 wlan_idx)
{
- struct mt7996_phy *phy = s->private;
- struct mt7996_dev *dev = phy->dev;
- u32 i, val;
+ static DEFINE_SPINLOCK(lock);
+ u32 val, fid, num;
- val = mt76_rr(dev, MT_FL_Q_EMPTY);
- for (i = 0; i < size; i++) {
- u32 ctrl, head, tail, queued;
+ val = MT_FL_Q0_CTRL_EXECUTE |
+ u32_encode_bits(q->qid, MT_FL_Q0_CTRL_QID) |
+ u32_encode_bits(q->tgid, MT_FL_Q0_CTRL_TGID) |
+ u32_encode_bits(q->pid, MT_FL_Q0_CTRL_PID) |
+ u32_encode_bits(wlan_idx, MT_FL_Q0_CTRL_WLAN_IDX);
- if (val & BIT(map[i].index))
+ spin_lock(&lock);
+ mt76_wr(dev, pse ? MT_PSE_FL_Q0_CTRL : MT_FL_Q0_CTRL, val);
+ fid = mt76_rr(dev, pse ? MT_PSE_FL_Q2_CTRL : MT_FL_Q2_CTRL);
+ num = mt76_rr(dev, pse ? MT_PSE_FL_Q3_CTRL : MT_FL_Q3_CTRL);
+ spin_unlock(&lock);
+
+ seq_printf(s, "tail/head fid 0x%04x/0x%04x, packets %u",
+ u32_get_bits(fid, MT_FL_Q2_CTRL_TAIL_FID),
+ u32_get_bits(fid, MT_FL_Q2_CTRL_HEAD_FID),
+ u32_get_bits(num, MT_FL_Q3_CTRL_PKT_NUM));
+}
+
+static void
+mt7996_fl_queues_show(struct seq_file *s, struct mt7996_dev *dev, bool pse,
+ const struct mt7996_hw_queue *queues, unsigned long empty)
+{
+ unsigned int i;
+
+ for_each_clear_bit(i, &empty, 32) {
+ if (!queues[i].name)
continue;
- ctrl = BIT(31) | (map[i].pid << 10) | ((u32)map[i].qid << 24);
- mt76_wr(dev, MT_FL_Q0_CTRL, ctrl);
-
- head = mt76_get_field(dev, MT_FL_Q2_CTRL,
- GENMASK(11, 0));
- tail = mt76_get_field(dev, MT_FL_Q2_CTRL,
- GENMASK(27, 16));
- queued = mt76_get_field(dev, MT_FL_Q3_CTRL,
- GENMASK(11, 0));
-
- seq_printf(s, "\t%s: ", map[i].name);
- seq_printf(s, "queued:0x%03x head:0x%03x tail:0x%03x\n",
- queued, head, tail);
+ seq_printf(s, "\t%s: ", queues[i].name);
+ mt7996_fl_queue_show(s, dev, pse, &queues[i], 0);
+ seq_puts(s, "\n");
}
}
+static void
+mt7996_freepg_show(struct seq_file *s, u32 cnt, u32 head_tail)
+{
+ seq_printf(s, "\tfree pages 0x%04x, free for all 0x%04x, head/tail 0x%04x/0x%04x\n",
+ u32_get_bits(cnt, MT_FREEPG_CNT_FREE),
+ u32_get_bits(cnt, MT_FREEPG_CNT_FFA),
+ u32_get_bits(head_tail, MT_FREEPG_HEAD_TAIL_HEAD),
+ u32_get_bits(head_tail, MT_FREEPG_HEAD_TAIL_TAIL));
+}
+
+static void
+mt7996_pg_group_show(struct seq_file *s, const char *name, u32 group, u32 info)
+{
+ seq_printf(s, "\t%s group: max/min quota 0x%04x/0x%04x, used/reserved 0x%04x/0x%04x\n",
+ name, u32_get_bits(group, MT_PG_GROUP_MAX_QUOTA),
+ u32_get_bits(group, MT_PG_GROUP_MIN_QUOTA),
+ u32_get_bits(info, MT_PG_INFO_SRC_CNT),
+ u32_get_bits(info, MT_PG_INFO_RSV_CNT));
+}
+
+static unsigned int
+mt7996_ple_sta_cr_num(struct mt7996_dev *dev)
+{
+ return is_mt7996(&dev->mt76) ? 34 : 17;
+}
+
+static unsigned int
+mt7996_ple_sta_cr(struct mt7996_dev *dev, unsigned int n)
+{
+ /* MT7992 and MT7990 keep the last station group in the
+ * extension register that follows the first 32 groups
+ */
+ if (!is_mt7996(&dev->mt76) && n == mt7996_ple_sta_cr_num(dev) - 1)
+ return 32;
+
+ return n;
+}
+
static void
mt7996_sta_hw_queue_read(void *data, struct ieee80211_sta *sta)
{
struct mt7996_sta *msta = (struct mt7996_sta *)sta->drv_priv;
struct mt7996_vif *mvif = msta->vif;
- struct mt7996_phy *phy = mt7996_vif_link_phy(&mvif->deflink);
struct ieee80211_link_sta *link_sta;
struct seq_file *s = data;
struct ieee80211_vif *vif;
- struct mt7996_dev *dev;
unsigned int link_id;
- if (!phy)
- return;
-
vif = container_of((void *)mvif, struct ieee80211_vif, drv_priv);
- dev = phy->dev;
rcu_read_lock();
for_each_sta_active_link(vif, sta, link_sta, link_id) {
struct mt7996_sta_link *msta_link;
struct mt76_vif_link *mlink;
+ struct mt7996_phy *phy;
+ unsigned int cr;
+ u16 wlan_idx;
u8 ac;
mlink = rcu_dereference(mvif->mt76.link[link_id]);
if (!mlink)
continue;
+ phy = mt7996_vif_link_phy(container_of(mlink,
+ struct mt7996_vif_link,
+ mt76));
+ if (!phy)
+ continue;
+
msta_link = mt7996_sta_link(msta, link_id);
if (!msta_link)
continue;
- for (ac = 0; ac < 4; ac++) {
- u32 idx = msta_link->wcid.idx >> 5, qlen, ctrl, val;
- u8 offs = msta_link->wcid.idx & GENMASK(4, 0);
+ wlan_idx = msta_link->wcid.idx;
+ cr = mt7996_ple_sta_cr(phy->dev, wlan_idx / 32);
+ for (ac = 0; ac < IEEE80211_NUM_ACS; ac++) {
+ struct mt7996_hw_queue q = {
+ .pid = MT_QUEUE_PORT_LMAC,
+ .qid = ac,
+ };
- ctrl = BIT(31) | BIT(11) | (ac << 24);
- val = mt76_rr(dev, MT_PLE_AC_QEMPTY(ac, idx));
-
- if (val & BIT(offs))
+ if (mt76_rr(phy->dev, MT_PLE_AC_QEMPTY(ac, cr)) &
+ BIT(wlan_idx % 32))
continue;
- mt76_wr(dev,
- MT_FL_Q0_CTRL, ctrl | msta_link->wcid.idx);
- qlen = mt76_get_field(dev, MT_FL_Q3_CTRL,
- GENMASK(11, 0));
- seq_printf(s, "\tSTA %pM wcid %d: AC%d%d queued:%d\n",
- sta->addr, msta_link->wcid.idx,
- mlink->wmm_idx, ac, qlen);
+ seq_printf(s, "\tSTA %pM wcid %u AC%u%u: ", sta->addr,
+ wlan_idx, mlink->wmm_idx, ac);
+ mt7996_fl_queue_show(s, phy->dev, false, &q, wlan_idx);
+ seq_puts(s, "\n");
}
}
@@ -692,78 +828,27 @@ mt7996_sta_hw_queue_read(void *data, struct ieee80211_sta *sta)
}
static int
-mt7996_hw_queues_show(struct seq_file *file, void *data)
+mt7996_hw_queues_show(struct seq_file *s, void *data)
{
- struct mt7996_dev *dev = file->private;
- struct mt7996_phy *phy = &dev->phy;
- static const struct hw_queue_map ple_queue_map[] = {
- { "CPU_Q0", 0, 1, MT_CTX0 },
- { "CPU_Q1", 1, 1, MT_CTX0 + 1 },
- { "CPU_Q2", 2, 1, MT_CTX0 + 2 },
- { "CPU_Q3", 3, 1, MT_CTX0 + 3 },
- { "ALTX_Q0", 8, 2, MT_LMAC_ALTX0 },
- { "BMC_Q0", 9, 2, MT_LMAC_BMC0 },
- { "BCN_Q0", 10, 2, MT_LMAC_BCN0 },
- { "PSMP_Q0", 11, 2, MT_LMAC_PSMP0 },
- { "ALTX_Q1", 12, 2, MT_LMAC_ALTX0 + 4 },
- { "BMC_Q1", 13, 2, MT_LMAC_BMC0 + 4 },
- { "BCN_Q1", 14, 2, MT_LMAC_BCN0 + 4 },
- { "PSMP_Q1", 15, 2, MT_LMAC_PSMP0 + 4 },
- };
- static const struct hw_queue_map pse_queue_map[] = {
- { "CPU Q0", 0, 1, MT_CTX0 },
- { "CPU Q1", 1, 1, MT_CTX0 + 1 },
- { "CPU Q2", 2, 1, MT_CTX0 + 2 },
- { "CPU Q3", 3, 1, MT_CTX0 + 3 },
- { "HIF_Q0", 8, 0, MT_HIF0 },
- { "HIF_Q1", 9, 0, MT_HIF0 + 1 },
- { "HIF_Q2", 10, 0, MT_HIF0 + 2 },
- { "HIF_Q3", 11, 0, MT_HIF0 + 3 },
- { "HIF_Q4", 12, 0, MT_HIF0 + 4 },
- { "HIF_Q5", 13, 0, MT_HIF0 + 5 },
- { "LMAC_Q", 16, 2, 0 },
- { "MDP_TXQ", 17, 2, 1 },
- { "MDP_RXQ", 18, 2, 2 },
- { "SEC_TXQ", 19, 2, 3 },
- { "SEC_RXQ", 20, 2, 4 },
- };
- u32 val, head, tail;
+ struct mt7996_dev *dev = s->private;
- /* ple queue */
- val = mt76_rr(dev, MT_PLE_FREEPG_CNT);
- head = mt76_get_field(dev, MT_PLE_FREEPG_HEAD_TAIL, GENMASK(11, 0));
- tail = mt76_get_field(dev, MT_PLE_FREEPG_HEAD_TAIL, GENMASK(27, 16));
- seq_puts(file, "PLE page info:\n");
- seq_printf(file,
- "\tTotal free page: 0x%08x head: 0x%03x tail: 0x%03x\n",
- val, head, tail);
+ seq_puts(s, "PLE page info:\n");
+ mt7996_freepg_show(s, mt76_rr(dev, MT_PLE_FREEPG_CNT),
+ mt76_rr(dev, MT_PLE_FREEPG_HEAD_TAIL));
+ mt7996_pg_group_show(s, "HIF", mt76_rr(dev, MT_PLE_PG_HIF_GROUP),
+ mt76_rr(dev, MT_PLE_HIF_PG_INFO));
- val = mt76_rr(dev, MT_PLE_PG_HIF_GROUP);
- head = mt76_get_field(dev, MT_PLE_HIF_PG_INFO, GENMASK(11, 0));
- tail = mt76_get_field(dev, MT_PLE_HIF_PG_INFO, GENMASK(27, 16));
- seq_printf(file, "\tHIF free page: 0x%03x res: 0x%03x used: 0x%03x\n",
- val, head, tail);
+ seq_puts(s, "PLE non-empty queue info:\n");
+ mt7996_fl_queues_show(s, dev, false, mt7996_ple_queues,
+ mt76_rr(dev, MT_FL_Q_EMPTY));
+ ieee80211_iterate_stations_atomic(dev->mphy.hw,
+ mt7996_sta_hw_queue_read, s);
- seq_puts(file, "PLE non-empty queue info:\n");
- mt7996_hw_queue_read(file, ARRAY_SIZE(ple_queue_map),
- &ple_queue_map[0]);
-
- /* iterate per-sta ple queue */
- ieee80211_iterate_stations_atomic(phy->mt76->hw,
- mt7996_sta_hw_queue_read, file);
- phy = mt7996_phy2(dev);
- if (phy)
- ieee80211_iterate_stations_atomic(phy->mt76->hw,
- mt7996_sta_hw_queue_read, file);
- phy = mt7996_phy3(dev);
- if (phy)
- ieee80211_iterate_stations_atomic(phy->mt76->hw,
- mt7996_sta_hw_queue_read, file);
-
- /* pse queue */
- seq_puts(file, "PSE non-empty queue info:\n");
- mt7996_hw_queue_read(file, ARRAY_SIZE(pse_queue_map),
- &pse_queue_map[0]);
+ seq_puts(s, "PSE non-empty queue info:\n");
+ mt7996_fl_queues_show(s, dev, true, mt7996_pse_queues,
+ mt76_rr(dev, MT_PSE_QUEUE_EMPTY));
+ mt7996_fl_queues_show(s, dev, true, mt7996_pse_queues_1,
+ mt76_rr(dev, MT_PSE_QUEUE_EMPTY_1));
return 0;
}
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/regs.h b/drivers/net/wireless/mediatek/mt76/mt7996/regs.h
index 30bec7a0a987..6df6e4c3d45d 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/regs.h
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/regs.h
@@ -155,6 +155,15 @@ enum offs_rev {
#define MT_FL_Q2_CTRL MT_PLE(0x3e8)
#define MT_FL_Q3_CTRL MT_PLE(0x3ec)
+#define MT_FL_Q0_CTRL_EXECUTE BIT(31)
+#define MT_FL_Q0_CTRL_QID GENMASK(30, 24)
+#define MT_FL_Q0_CTRL_TGID GENMASK(21, 20)
+#define MT_FL_Q0_CTRL_PID GENMASK(17, 16)
+#define MT_FL_Q0_CTRL_WLAN_IDX GENMASK(11, 0)
+#define MT_FL_Q2_CTRL_TAIL_FID GENMASK(28, 16)
+#define MT_FL_Q2_CTRL_HEAD_FID GENMASK(12, 0)
+#define MT_FL_Q3_CTRL_PKT_NUM GENMASK(12, 0)
+
#define MT_PLE_FREEPG_CNT MT_PLE(0x3a0)
#define MT_PLE_FREEPG_HEAD_TAIL MT_PLE(0x3a4)
#define MT_PLE_PG_HIF_GROUP MT_PLE(0x00c)
@@ -163,6 +172,25 @@ enum offs_rev {
#define MT_PLE_AC_QEMPTY(ac, n) MT_PLE(0x600 + 0x100 * (ac) + ((n) << 2))
#define MT_PLE_AMSDU_PACK_MSDU_CNT(n) MT_PLE(0x10e0 + ((n) << 2))
+#define MT_FREEPG_CNT_FFA GENMASK(28, 16)
+#define MT_FREEPG_CNT_FREE GENMASK(12, 0)
+#define MT_FREEPG_HEAD_TAIL_TAIL GENMASK(28, 16)
+#define MT_FREEPG_HEAD_TAIL_HEAD GENMASK(12, 0)
+#define MT_PG_GROUP_MAX_QUOTA GENMASK(28, 16)
+#define MT_PG_GROUP_MIN_QUOTA GENMASK(12, 0)
+#define MT_PG_INFO_SRC_CNT GENMASK(28, 16)
+#define MT_PG_INFO_RSV_CNT GENMASK(12, 0)
+
+/* PSE */
+#define MT_PSE_BASE 0x820c8000
+#define MT_PSE(ofs) (MT_PSE_BASE + (ofs))
+
+#define MT_PSE_QUEUE_EMPTY MT_PSE(0x0b0)
+#define MT_PSE_QUEUE_EMPTY_1 MT_PSE(0x0bc)
+#define MT_PSE_FL_Q0_CTRL MT_PSE(0x1b0)
+#define MT_PSE_FL_Q2_CTRL MT_PSE(0x1b8)
+#define MT_PSE_FL_Q3_CTRL MT_PSE(0x1bc)
+
/* WF MDP TOP */
#define MT_MDP_BASE 0x820cc000
#define MT_MDP(ofs) (MT_MDP_BASE + (ofs))
--
2.47.3
^ permalink raw reply related [flat|nested] 27+ messages in thread* [PATCH mt76-next 27/27] wifi: mt76: mt7996: disable the RX NAPIs before the DMA cleanup
2026-10-06 20:59 [PATCH mt76-next 01/27] wifi: mt76: abort a scan only for a link change on the scanning radio Felix Fietkau
` (24 preceding siblings ...)
2026-10-06 20:59 ` [PATCH mt76-next 26/27] wifi: mt76: mt7996: fix the hw-queues debugfs queue requests Felix Fietkau
@ 2026-10-06 20:59 ` Felix Fietkau
25 siblings, 0 replies; 27+ messages in thread
From: Felix Fietkau @ 2026-10-06 20:59 UTC (permalink / raw)
To: linux-wireless; +Cc: John Crispin
From: John Crispin <john@phrozen.org>
mt7996_unregister_device() leaves the RX NAPIs enabled, so
mt76_dma_cleanup() deletes enabled NAPIs and destroys page pools that
are still bound to them. On rmmod, the kernel warns in
__netif_napi_del_locked() for every RX queue and in
page_pool_disable_direct_recycling(). Until the IRQ tasklet is disabled,
an interrupt can still schedule a NAPI that was already deleted.
Disable every RX NAPI that was added before the MCU exit and the DMA
cleanup. This includes the host RRO queues: mt76_dma_init() skips them,
but mt7996_dma_rro_start() adds their NAPIs later when WED is not
active, so a check for mt76_queue_is_wed_rro() would miss them.
Fixes: 98686cd21624 ("wifi: mt76: mt7996: add driver for MediaTek Wi-Fi 7 (802.11be) devices")
Signed-off-by: John Crispin <john@phrozen.org>
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
drivers/net/wireless/mediatek/mt76/mt7996/init.c | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/init.c b/drivers/net/wireless/mediatek/mt76/mt7996/init.c
index 2db63c3be9d1..ad9919c516e7 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/init.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/init.c
@@ -1810,6 +1810,8 @@ int mt7996_register_device(struct mt7996_dev *dev)
void mt7996_unregister_device(struct mt7996_dev *dev)
{
+ int i;
+
cancel_work_sync(&dev->dump_work);
cancel_work_sync(&dev->wed_rro.work);
cancel_work_sync(&dev->reset_work);
@@ -1820,6 +1822,10 @@ void mt7996_unregister_device(struct mt7996_dev *dev)
mt7996_coredump_unregister(dev);
mt76_unregister_device(&dev->mt76);
mt7996_wed_rro_free(dev);
+ for (i = 0; i < ARRAY_SIZE(dev->mt76.napi); i++) {
+ if (dev->mt76.napi[i].dev)
+ napi_disable(&dev->mt76.napi[i]);
+ }
mt7996_mcu_exit(dev);
mt7996_tx_token_put(dev);
mt7996_dma_cleanup(dev);
--
2.47.3
^ permalink raw reply related [flat|nested] 27+ messages in thread