Linux wireless drivers development
 help / color / mirror / Atom feed
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


      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