From: Michael Shih <michael.shih@mediatek.com>
To: <nbd@nbd.name>, <lorenzo@kernel.org>
Cc: <linux-wireless@vger.kernel.org>,
<linux-mediatek@lists.infradead.org>, <kun.wu@mediatek.com>,
<deren.wu@mediatek.com>, <sean.wang@mediatek.com>,
<quan.zhou@mediatek.com>, <ryder.lee@mediatek.com>,
<leon.yen@mediatek.com>, <litien.chang@mediatek.com>,
<jb.tsai@mediatek.com>, <eason.Lai@mediatek.com>,
Michael Shih <michael.shih@mediatek.com>
Subject: [PATCH v5 2/2] wifi: mt76: mt7925: support per-channel survey dump during scan
Date: Thu, 12 Nov 2026 15:55:57 +0800 [thread overview]
Message-ID: <20261112075558.1813785-3-michael.shih@mediatek.com> (raw)
In-Reply-To: <20261112075558.1813785-1-michael.shih@mediatek.com>
Report per-channel survey airtime (active/busy/rx/tx) so that
"iw <dev> survey dump" shows non-zero airtime not only for the
connected home channel, but also for every channel visited during
a firmware-offloaded scan.
Two accounting paths are wired up:
(1) Home/operating channel (periodic mac_work):
mt7925 is firmware/chanctx managed and never goes through
__mt76_set_channel(), so phy->chan_state used to stay pinned to
its init default and the periodic survey was charged to the wrong
channel. mt7925_survey_switch_channel() is added and called from
assign_vif_chanctx() to settle the old channel, re-point
chan_state/chandef to the operating channel, and re-base the
firmware MIB snapshot so active/busy/rx/tx keep accumulating on
the channel actually dwelt on.
(2) Scanned channels (scan-done event):
The scan-done CHNLINFO event now carries per-channel airtime
(firmware Ch-Survey extension). mt7925_scan_update_survey() parses
the per-channel array and folds the airtime deltas into each
channel's mt76_channel_state. The firmware 6G channel id is
converted back to the real channel number before lookup.
mt76_channel_state() is exported so the mt7925 module can map a
band/channel pair to its channel state.
Usage:
(1) Connect STA to AP and generate traffic:
dhclient ${WLAN_IF} -v
ping 192.168.0.XXX -i0.1
(2) (Optional) Trigger a scan to collect other channels:
iw ${WLAN_IF} scan
or use wpa_supplicant / wpa_cli scan.
(3) Dump the survey:
iw ${WLAN_IF} survey dump
(4) You will see per-channel airtime, e.g.:
Survey data from wls1
frequency: 5240 MHz
channel active time: 163258 ms
channel busy time: 34076 ms
channel receive time: 32084 ms
channel transmit time: 253 ms
- The connected/dwelt channel keeps increasing over time.
- Channels visited during scan accumulate airtime each scan.
Signed-off-by: Michael Shih <michael.shih@mediatek.com>
---
drivers/net/wireless/mediatek/mt76/mac80211.c | 3 +-
drivers/net/wireless/mediatek/mt76/mt76.h | 2 +
.../net/wireless/mediatek/mt76/mt7925/main.c | 12 +
.../net/wireless/mediatek/mt76/mt7925/mcu.h | 30 ++
.../wireless/mediatek/mt76/mt7925/mt7925.h | 7 +
.../net/wireless/mediatek/mt76/mt7925/pci.c | 2 +-
.../net/wireless/mediatek/mt76/mt7925/stats.c | 301 +++++++++++++++++-
.../net/wireless/mediatek/mt76/mt7925/stats.h | 6 +
.../net/wireless/mediatek/mt76/mt7925/usb.c | 2 +-
drivers/net/wireless/mediatek/mt76/mt792x.h | 6 +
10 files changed, 367 insertions(+), 4 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mac80211.c b/drivers/net/wireless/mediatek/mt76/mac80211.c
index 08ed3fc6d386..11301428c5f9 100644
--- a/drivers/net/wireless/mediatek/mt76/mac80211.c
+++ b/drivers/net/wireless/mediatek/mt76/mac80211.c
@@ -991,7 +991,7 @@ bool mt76_has_tx_pending(struct mt76_phy *phy)
}
EXPORT_SYMBOL_GPL(mt76_has_tx_pending);
-static struct mt76_channel_state *
+struct mt76_channel_state *
mt76_channel_state(struct mt76_phy *phy, struct ieee80211_channel *c)
{
struct mt76_sband *msband;
@@ -1007,6 +1007,7 @@ mt76_channel_state(struct mt76_phy *phy, struct ieee80211_channel *c)
idx = c - &msband->sband.channels[0];
return &msband->chan[idx];
}
+EXPORT_SYMBOL_GPL(mt76_channel_state);
void mt76_update_survey_active_time(struct mt76_phy *phy, ktime_t time)
{
diff --git a/drivers/net/wireless/mediatek/mt76/mt76.h b/drivers/net/wireless/mediatek/mt76/mt76.h
index 477d33a0022d..f74c6a4b0f9c 100644
--- a/drivers/net/wireless/mediatek/mt76/mt76.h
+++ b/drivers/net/wireless/mediatek/mt76/mt76.h
@@ -1565,6 +1565,8 @@ void mt76_update_survey(struct mt76_phy *phy);
void mt76_phy_chandef_set(struct mt76_phy *phy,
struct cfg80211_chan_def *chandef);
void mt76_update_survey_active_time(struct mt76_phy *phy, ktime_t time);
+struct mt76_channel_state *
+mt76_channel_state(struct mt76_phy *phy, struct ieee80211_channel *c);
int mt76_get_survey(struct ieee80211_hw *hw, int idx,
struct survey_info *survey);
int mt76_rx_signal(u8 chain_mask, s8 *chain_signal);
diff --git a/drivers/net/wireless/mediatek/mt76/mt7925/main.c b/drivers/net/wireless/mediatek/mt76/mt7925/main.c
index ffbc6b954632..8e48ef0cb3e5 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7925/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7925/main.c
@@ -1612,6 +1612,10 @@ void mt7925_scan_work(struct work_struct *work)
mt7925_regd_change(phy, evt->alpha2);
+ mt7925_scan_update_survey(phy, evt,
+ le16_to_cpu(tlv->len) -
+ sizeof(struct tlv));
+
break;
case UNI_EVENT_SCAN_DONE_NLO:
ieee80211_sched_scan_results(phy->mt76->hw);
@@ -2440,7 +2444,15 @@ static int mt7925_assign_vif_chanctx(struct ieee80211_hw *hw,
mconf->mt76.ctx = ctx;
mctx->bss_conf = mconf;
+
+ /* Re-target op-channel survey accounting to the operating channel just
+ * assigned, so cc_active/airtime accrue on the channel we actually
+ * dwell on (mt7925 never goes through __mt76_set_channel()).
+ */
+ mt7925_survey_switch_channel(mvif->phy, ctx->def.chan);
+
mt76_phy_chandef_set(mvif->phy->mt76, &ctx->def);
+
mutex_unlock(&dev->mt76.mutex);
return 0;
diff --git a/drivers/net/wireless/mediatek/mt76/mt7925/mcu.h b/drivers/net/wireless/mediatek/mt76/mt7925/mcu.h
index 11f9eac13ffc..2ed95b00e10f 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7925/mcu.h
+++ b/drivers/net/wireless/mediatek/mt76/mt7925/mcu.h
@@ -199,6 +199,36 @@ enum DMASHDL_GROUP_IDX {
struct mt7925_mcu_scan_chinfo_event {
u8 nr_chan;
u8 alpha2[3];
+ /* Followed by nr_chan entries. The entry layout matches the firmware
+ * UNI_EVENT_IE_CHNLINFO_T: a base part, optionally followed by the
+ * Ch-Survey airtime extension (present when the firmware was built with
+ * CONFIG_WIFI_RAM_CCN16_CHANNEL_SURVEY_STATS). Presence is detected at
+ * runtime from the TLV length (see mt7925_scan_update_survey()).
+ */
+ u8 chan_info[];
+} __packed;
+
+/* Base per-channel info (firmware SCAN_DONE_VERSION_SUPPORT_CU layout). */
+struct mt7925_mcu_scan_chinfo {
+ u8 channel_num;
+ u8 bcn_probe_cnt;
+ u8 mdrdy_cnt;
+ u8 util_val;
+ __le16 idle_time;
+ __le16 scan_time; /* dwell time (ms) */
+} __packed;
+
+/* Ch-Survey extension appended after the base part
+ * (firmware SCAN_DONE_VERSION_SUPPORT_CH_SURVEY layout).
+ */
+struct mt7925_mcu_scan_chinfo_survey {
+ struct mt7925_mcu_scan_chinfo base;
+ u8 band; /* 1=2G, 2=5G, 3=6G (firmware CMD_BAND_*) */
+ u8 rsv[3];
+ __le32 tx_time_us;
+ __le32 rx_time_us; /* rx + obss */
+ __le32 busy_time_us; /* tx + rx + obss + nonwifi */
+ __le32 active_time_us; /* busy + idle = total */
} __packed;
enum {
diff --git a/drivers/net/wireless/mediatek/mt76/mt7925/mt7925.h b/drivers/net/wireless/mediatek/mt76/mt7925/mt7925.h
index 636efe038392..ac66c0a5daf6 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7925/mt7925.h
+++ b/drivers/net/wireless/mediatek/mt76/mt7925/mt7925.h
@@ -282,6 +282,13 @@ extern const struct ieee80211_ops mt7925_ops;
int __mt7925_start(struct mt792x_phy *phy);
int mt7925_register_device(struct mt792x_dev *dev);
void mt7925_unregister_device(struct mt792x_dev *dev);
+void mt7925_update_channel(struct mt76_phy *mphy);
+void mt7925_survey_switch_channel(struct mt792x_phy *phy,
+ struct ieee80211_channel *chan);
+struct mt7925_mcu_scan_chinfo_event;
+void mt7925_scan_update_survey(struct mt792x_phy *phy,
+ struct mt7925_mcu_scan_chinfo_event *evt,
+ int tlv_len);
int mt7925_run_firmware(struct mt792x_dev *dev);
int mt7925_mcu_set_bss_pm(struct mt792x_dev *dev,
struct ieee80211_bss_conf *link_conf,
diff --git a/drivers/net/wireless/mediatek/mt76/mt7925/pci.c b/drivers/net/wireless/mediatek/mt76/mt7925/pci.c
index 0d477dc1086c..939a46c0428b 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7925/pci.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7925/pci.c
@@ -604,7 +604,7 @@ static int mt7925_pci_probe(struct pci_dev *pdev,
.sta_add = mt7925_mac_sta_add,
.sta_event = mt7925_mac_sta_event,
.sta_remove = mt7925_mac_sta_remove,
- .update_survey = mt792x_update_channel,
+ .update_survey = mt7925_update_channel,
};
static const struct mt792x_hif_ops mt7925_pcie_ops = {
.init_reset = mt7925e_init_reset,
diff --git a/drivers/net/wireless/mediatek/mt76/mt7925/stats.c b/drivers/net/wireless/mediatek/mt76/mt7925/stats.c
index 9a47953062bc..5d2c9eb81fcd 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7925/stats.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7925/stats.c
@@ -11,7 +11,7 @@
#define MT7925_STA_STATS_VERSION 1
#define MT7925_STA_STATS_VALID 1U
-#define MT7925_MIB_MAX_CNT 32
+#define MT7925_MIB_MAX_CNT 40
#define MT7925_MIB_CNT_MAX_NUM 512 /* firmware UNI_CMD_MIB_CNT_MAX_NUM upper bound */
#define MT7925_LQ_BAND_NUM 4 /* Link quality band */
@@ -519,6 +519,305 @@ int mt792x_mcu_get_wtbl_rate(struct mt792x_dev *dev, u16 wlan_idx,
return ret;
}
+/* mt7925_mcu_get_chan_mib_info - refresh per-channel survey airtime from FW.
+ *
+ * Unlike mt7921 (which reads the MT_MIB_SDR* registers directly), MT7925/MT7935
+ * obtain channel airtime through the firmware GET_MIB_INFO query. The firmware
+ * reports *accumulated* counters (unit: microseconds), so we keep the previous
+ * snapshot in phy->state_ts and accumulate only the delta into chan_state,
+ * mirroring mt7996_mcu_get_chan_mib_info().
+ */
+int mt7925_mcu_get_chan_mib_info(struct mt792x_phy *phy)
+{
+ enum {
+ IDX_TX_TIME,
+ IDX_RX_TIME,
+ IDX_OBSS_TIME,
+ IDX_NON_WIFI_TIME,
+ IDX_CHANNEL_IDLE,
+ IDX_NUM
+ };
+ static const u32 counters[IDX_NUM] = {
+ [IDX_TX_TIME] = UNI_CMD_MIB_CNT_TX_DUR_CNT,
+ [IDX_RX_TIME] = UNI_CMD_MIB_CNT_RX_DUR_CNT,
+ [IDX_OBSS_TIME] = UNI_CMD_RMAC_CNT_OBSS_AIRTIME,
+ [IDX_NON_WIFI_TIME] = UNI_CMD_RMAC_CNT_NONWIFI_AIRTIME,
+ [IDX_CHANNEL_IDLE] = UNI_CMD_MIB_CNT_CHANNEL_IDLE,
+ };
+ struct mt76_channel_state *state = phy->mt76->chan_state;
+ struct mt76_channel_state *state_ts = &phy->state_ts;
+ u64 busy, active;
+ u64 val[IDX_NUM];
+ int ret;
+
+ ret = mt7925_mcu_get_mib_info(phy->dev, phy->mt76->band_idx,
+ counters, IDX_NUM, val);
+ if (ret)
+ return ret;
+
+ /* FW counters are accumulated (us); add only the delta since the last
+ * snapshot. busy = tx + rx + obss + nonwifi; active = busy + idle.
+ *
+ * active is sourced from the FW here (not the host clock) because
+ * mt7925 runtime-suspends between activity: the host-side survey_time
+ * accounting in mt76_update_survey_active_time() only advances while
+ * the host is awake, which badly under-counts active and can make
+ * active < busy. Deriving active from the FW busy/idle counters keeps
+ * both in the same (firmware) clock domain, so busy <= active always
+ * holds and the channel-utilisation ratio stays correct across PM
+ * sleep. The host-clock contribution is neutralised in
+ * mt7925_update_channel() by re-basing phy->survey_time to now.
+ */
+ busy = val[IDX_TX_TIME] + val[IDX_RX_TIME] +
+ val[IDX_OBSS_TIME] + val[IDX_NON_WIFI_TIME];
+ active = busy + val[IDX_CHANNEL_IDLE];
+
+ state->cc_tx += val[IDX_TX_TIME] - state_ts->cc_tx;
+ state->cc_bss_rx += val[IDX_RX_TIME] - state_ts->cc_bss_rx;
+ state->cc_rx += val[IDX_RX_TIME] + val[IDX_OBSS_TIME] -
+ state_ts->cc_rx;
+ state->cc_busy += busy - state_ts->cc_busy;
+ state->cc_active += active - state_ts->cc_active;
+
+ state_ts->cc_tx = val[IDX_TX_TIME];
+ state_ts->cc_bss_rx = val[IDX_RX_TIME];
+ state_ts->cc_rx = val[IDX_RX_TIME] + val[IDX_OBSS_TIME];
+ state_ts->cc_busy = busy;
+ state_ts->cc_active = active;
+
+ return 0;
+}
+
+/* mt7925_update_channel - survey update_survey callback for MT7925/MT7935.
+ *
+ * Replaces mt792x_update_channel() (register-based) for chips that source
+ * channel airtime from the firmware MIB query.
+ */
+void mt7925_update_channel(struct mt76_phy *mphy)
+{
+ struct mt792x_dev *dev = container_of(mphy->dev, struct mt792x_dev,
+ mt76);
+ struct mt792x_phy *phy = mphy->priv;
+
+ if (mt76_connac_pm_wake(mphy, &dev->pm))
+ goto out;
+
+ mt7925_mcu_get_chan_mib_info(phy);
+
+ mt76_connac_power_save_sched(mphy, &dev->pm);
+
+out:
+ /* cc_active is sourced from the FW busy/idle counters above, so
+ * neutralise the host-clock contribution the framework is about to
+ * add in mt76_update_survey_active_time(): re-base survey_time to now
+ * so its delta is ~0. This must happen on every call (including the
+ * pm_wake failure path) or the host clock would otherwise charge the
+ * whole suspend/off-channel gap to cc_active.
+ */
+ mphy->survey_time = ktime_get_boottime();
+}
+EXPORT_SYMBOL_GPL(mt7925_update_channel);
+
+/* mt7925_survey_switch_channel - re-target op-channel survey accounting.
+ *
+ * mt7925 is firmware/chanctx managed and never goes through
+ * __mt76_set_channel(), so phy->chan_state stays pinned to its init default
+ * and the periodic mac_work survey (both cc_active time and the FW-MIB airtime
+ * delta) is charged to the wrong channel. Call this when the operating channel
+ * is (re)assigned so that:
+ * 1) the airtime accrued so far is settled into the old channel;
+ * 2) chan_state/chandef are re-pointed to the new operating channel;
+ * 3) the FW-MIB snapshot (state_ts) is re-based to the current counters so
+ * the first delta on the new channel is measured from now, not
+ * underflowed.
+ */
+void mt7925_survey_switch_channel(struct mt792x_phy *phy,
+ struct ieee80211_channel *chan)
+{
+ enum {
+ IDX_TX_TIME,
+ IDX_RX_TIME,
+ IDX_OBSS_TIME,
+ IDX_NON_WIFI_TIME,
+ IDX_CHANNEL_IDLE,
+ IDX_NUM
+ };
+ static const u32 counters[IDX_NUM] = {
+ [IDX_TX_TIME] = UNI_CMD_MIB_CNT_TX_DUR_CNT,
+ [IDX_RX_TIME] = UNI_CMD_MIB_CNT_RX_DUR_CNT,
+ [IDX_OBSS_TIME] = UNI_CMD_RMAC_CNT_OBSS_AIRTIME,
+ [IDX_NON_WIFI_TIME] = UNI_CMD_RMAC_CNT_NONWIFI_AIRTIME,
+ [IDX_CHANNEL_IDLE] = UNI_CMD_MIB_CNT_CHANNEL_IDLE,
+ };
+ struct mt76_phy *mphy = phy->mt76;
+ struct mt76_channel_state *state_ts = &phy->state_ts;
+ struct mt76_channel_state *new_state;
+ u64 val[IDX_NUM];
+
+ if (!chan)
+ return;
+
+ new_state = mt76_channel_state(mphy, chan);
+ if (new_state == mphy->chan_state)
+ return; /* same channel, nothing to switch */
+
+ /* 1) settle the old channel with airtime accrued up to now */
+ if (mphy->chan_state)
+ mt76_update_survey(mphy);
+
+ /* 2) re-target accounting to the new operating channel */
+ mphy->chan_state = new_state;
+ mphy->chandef.chan = chan;
+
+ /* drop any stale airtime left on this channel from a previous session
+ * so cc_active/cc_busy start fresh for this association (mt7925 never
+ * clears per-channel state via __mt76_set_channel()).
+ */
+ memset(new_state, 0, sizeof(*new_state));
+
+ /* 3) re-base the FW-MIB snapshot so the next delta starts from now.
+ * active = busy + idle (see mt7925_mcu_get_chan_mib_info()).
+ */
+ if (!mt7925_mcu_get_mib_info(phy->dev, mphy->band_idx,
+ counters, IDX_NUM, val)) {
+ u64 busy = val[IDX_TX_TIME] + val[IDX_RX_TIME] +
+ val[IDX_OBSS_TIME] + val[IDX_NON_WIFI_TIME];
+
+ state_ts->cc_tx = val[IDX_TX_TIME];
+ state_ts->cc_bss_rx = val[IDX_RX_TIME];
+ state_ts->cc_rx = val[IDX_RX_TIME] + val[IDX_OBSS_TIME];
+ state_ts->cc_busy = busy;
+ state_ts->cc_active = busy + val[IDX_CHANNEL_IDLE];
+ }
+
+ /* reset the host-clock reference so the framework's
+ * mt76_update_survey_active_time() does not charge the switch gap
+ * to cc_active (which is now FW-sourced).
+ */
+ mphy->survey_time = ktime_get_boottime();
+}
+EXPORT_SYMBOL_GPL(mt7925_survey_switch_channel);
+
+/* Map a firmware scan-done channel entry (band + channel number) back to the
+ * corresponding ieee80211_channel. The firmware band uses CMD_BAND_* encoding
+ * (1=2G, 2=5G, 3=6G).
+ *
+ * For 2G/5G, the reported number is the real channel number (== hw_value).
+ * For 6G, the firmware reports an internal channel id produced by
+ * rlmUtilGetChnlId(): chnl_id = ((real_ch - 1) >> 2) + CHNL_NUM_BASE_6G, where
+ * CHNL_NUM_BASE_6G is 181. We invert it back to the real 6G channel number
+ * (1, 5, 9, ... == hw_value) before looking it up.
+ */
+/* firmware CMD_BAND_* encoding used in scan-done per-channel entries */
+enum mt7925_fw_cmd_band {
+ MT7925_FW_CMD_BAND_2G = 1,
+ MT7925_FW_CMD_BAND_5G = 2,
+ MT7925_FW_CMD_BAND_6G = 3,
+};
+
+/* firmware rlmUtilGetChnlId() 6G base (CHNL_NUM_BASE_6G) */
+#define MT7925_FW_CHNL_NUM_BASE_6G 181
+
+/* invert rlmUtilGetChnlId() 6G encoding: chnl_id -> real 6G channel number */
+#define MT7925_FW_6G_CHNL_ID_TO_NUM(_id) \
+ ((((_id) - MT7925_FW_CHNL_NUM_BASE_6G) << 2) + 1)
+
+static struct ieee80211_channel *
+mt7925_scan_survey_get_chan(struct mt76_phy *mphy, u8 fw_band, u8 chan_num)
+{
+ struct mt76_sband *msband;
+ int i;
+
+ switch (fw_band) {
+ case MT7925_FW_CMD_BAND_2G:
+ msband = &mphy->sband_2g;
+ break;
+ case MT7925_FW_CMD_BAND_5G:
+ msband = &mphy->sband_5g;
+ break;
+ case MT7925_FW_CMD_BAND_6G:
+ msband = &mphy->sband_6g;
+ if (chan_num >= MT7925_FW_CHNL_NUM_BASE_6G)
+ chan_num = MT7925_FW_6G_CHNL_ID_TO_NUM(chan_num);
+ break;
+ default:
+ return NULL;
+ }
+
+ for (i = 0; i < msband->sband.n_channels; i++) {
+ if (msband->sband.channels[i].hw_value == chan_num)
+ return &msband->sband.channels[i];
+ }
+
+ return NULL;
+}
+
+/* mt7925_scan_update_survey - fold per-channel scan airtime into survey state.
+ *
+ * The scan-done CHNLINFO event carries a per-channel array. When the firmware
+ * is built with the Ch-Survey extension, each entry also reports the airtime
+ * (tx/rx/busy/active, unit: us) accumulated while scanning that channel. We
+ * accumulate those deltas into each channel's mt76_channel_state so that
+ * "iw survey dump" shows non-zero airtime for scanned channels, not only the
+ * connected home channel.
+ *
+ * @tlv_len is the total CHNLINFO TLV payload length (after the tlv header),
+ * used to (a) detect whether the airtime extension is present from the entry
+ * stride and (b) bound the array walk safely.
+ */
+void mt7925_scan_update_survey(struct mt792x_phy *phy,
+ struct mt7925_mcu_scan_chinfo_event *evt,
+ int tlv_len)
+{
+ struct mt76_phy *mphy = phy->mt76;
+ struct mt76_dev *dev = mphy->dev;
+ size_t hdr_len = sizeof(*evt);
+ size_t stride, avail;
+ const u8 *p;
+ int i;
+
+ if (tlv_len <= (int)hdr_len || !evt->nr_chan)
+ return;
+
+ avail = (size_t)tlv_len - hdr_len;
+
+ /* The airtime extension is only present when each entry is at least as
+ * large as the extended layout. Older firmware sends only the base
+ * entry and thus reports no airtime; nothing to accumulate then.
+ */
+ stride = avail / evt->nr_chan;
+ if (stride < sizeof(struct mt7925_mcu_scan_chinfo_survey))
+ return;
+
+ p = evt->chan_info;
+
+ spin_lock_bh(&dev->cc_lock);
+ for (i = 0; i < evt->nr_chan; i++, p += stride) {
+ const struct mt7925_mcu_scan_chinfo_survey *ci;
+ struct ieee80211_channel *chan;
+ struct mt76_channel_state *state;
+
+ ci = (const void *)p;
+
+ /* ensure the full extended entry is within the payload */
+ if ((size_t)((i + 1) * stride) > avail)
+ break;
+
+ chan = mt7925_scan_survey_get_chan(mphy, ci->band,
+ ci->base.channel_num);
+ if (!chan)
+ continue;
+
+ state = mt76_channel_state(mphy, chan);
+ state->cc_tx += le32_to_cpu(ci->tx_time_us);
+ state->cc_rx += le32_to_cpu(ci->rx_time_us);
+ state->cc_busy += le32_to_cpu(ci->busy_time_us);
+ state->cc_active += le32_to_cpu(ci->active_time_us);
+ }
+ spin_unlock_bh(&dev->cc_lock);
+}
+EXPORT_SYMBOL_GPL(mt7925_scan_update_survey);
+
/**
* mt7925_mcu_get_link_quality - query RSSI and TX link speed for a band
* @dev: device
diff --git a/drivers/net/wireless/mediatek/mt76/mt7925/stats.h b/drivers/net/wireless/mediatek/mt76/mt7925/stats.h
index a70b9eb3aa80..a4cb8c4453ea 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7925/stats.h
+++ b/drivers/net/wireless/mediatek/mt76/mt7925/stats.h
@@ -291,6 +291,12 @@ int mt7925_mcu_get_mib_info(struct mt792x_dev *dev, u8 band_idx,
const u32 *counters, u8 counter_num,
u64 *values);
+/* Refresh per-channel survey airtime (TX/RX/OBSS/non-wifi) from the firmware
+ * MIB query and accumulate the delta into chan_state. Used by the MT7925/MT7935
+ * update_survey callback.
+ */
+int mt7925_mcu_get_chan_mib_info(struct mt792x_phy *phy);
+
/* GET_STAT_INFO (UNI_CMD_ID_GET_STATISTICS = 0x23),
* sub-command UNI_CMD_GET_STATISTICS_TAG_LINK_QUALITY = 1.
*
diff --git a/drivers/net/wireless/mediatek/mt76/mt7925/usb.c b/drivers/net/wireless/mediatek/mt76/mt7925/usb.c
index f97f787b2198..2d3bf035fdfc 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7925/usb.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7925/usb.c
@@ -174,7 +174,7 @@ static int mt7925u_probe(struct usb_interface *usb_intf,
.sta_add = mt7925_mac_sta_add,
.sta_event = mt7925_mac_sta_event,
.sta_remove = mt7925_mac_sta_remove,
- .update_survey = mt792x_update_channel,
+ .update_survey = mt7925_update_channel,
};
static const struct mt792x_hif_ops hif_ops = {
.mcu_init = mt7925u_mcu_init,
diff --git a/drivers/net/wireless/mediatek/mt76/mt792x.h b/drivers/net/wireless/mediatek/mt76/mt792x.h
index a7d71e007da0..2c7170c6a99e 100644
--- a/drivers/net/wireless/mediatek/mt76/mt792x.h
+++ b/drivers/net/wireless/mediatek/mt76/mt792x.h
@@ -222,6 +222,12 @@ struct mt792x_phy {
struct mt76_mib_stats mib;
+ /* survey: firmware reports accumulated airtime counters; keep the
+ * previous snapshot so mt7925_mcu_get_chan_mib_info() can accumulate
+ * deltas into chan_state (mirror mt7996 phy->state_ts).
+ */
+ struct mt76_channel_state state_ts;
+
u8 sta_work_count;
u8 clc_chan_conf;
enum mt792x_reg_power_type power_type;
--
2.45.2
prev parent reply other threads:[~2026-11-12 7:56 UTC|newest]
Thread overview: 11+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-24 3:07 [PATCH v1] wifi: mt76: mt7928: Add debugfs for stats to provide debug purpose Eason Lai
2026-08-25 4:00 ` kernel test robot
2026-08-25 7:55 ` kernel test robot
2026-08-27 5:54 ` [PATCH v2] " Eason Lai
2026-08-27 8:44 ` [PATCH v3] " Eason Lai
2026-08-28 7:39 ` kernel test robot
2026-08-31 3:28 ` [PATCH v4] " Eason Lai
2026-11-12 7:55 ` [PATCH v5 0/2] wifi: mt76: mt7925: per-channel survey dump during scan Michael Shih
2026-11-12 7:55 ` [PATCH v5 1/2] wifi: mt76: mt7928: Add debugfs for stats to provide debug purpose Michael Shih
2026-11-13 0:46 ` kernel test robot
2026-11-12 7:55 ` Michael Shih [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261112075558.1813785-3-michael.shih@mediatek.com \
--to=michael.shih@mediatek.com \
--cc=deren.wu@mediatek.com \
--cc=eason.Lai@mediatek.com \
--cc=jb.tsai@mediatek.com \
--cc=kun.wu@mediatek.com \
--cc=leon.yen@mediatek.com \
--cc=linux-mediatek@lists.infradead.org \
--cc=linux-wireless@vger.kernel.org \
--cc=litien.chang@mediatek.com \
--cc=lorenzo@kernel.org \
--cc=nbd@nbd.name \
--cc=quan.zhou@mediatek.com \
--cc=ryder.lee@mediatek.com \
--cc=sean.wang@mediatek.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox